Files
bruno 64c85caffc
FlowDeck CI / lint (push) Successful in 2m13s
FlowDeck CI / test (push) Failing after 3h10m19s
FlowDeck CI / docker (push) Skipped
feat: menu + — Mémoire de l'agent — phase 4/8 (v7.54.0)
- Le moteur n'envoie jamais l'historique (system + objectif courant seulement)
  : chaque run repartait de zéro. Nouveau service app/services/agent_memory.py —
  une ligne résumé par conversation (table agent_memory, migration 31),
  écrite en fin de run et ré-injectée au contexte du run suivant.
- context_for() : bloc « ## Mémoire de la conversation », budget 4000 car.
  (troncature gauche + marqueur). remember() : 1 note/échange (180 → 700 car.),
  20 notes max, upsert, erreurs journalisées — un run ne peut pas échouer à
  cause de la mémoire. Toggle OFF = ni lecture ni écriture.
- Toggle « Mémoire » dans le menu + : entrée racine à l'état réel (🧠/💭 +
  libellé), PATCH /api/agent/conversations/{id} avec memory_enabled ajouté au
  whitelist, état lu à l'ouverture et à la création, défaut AGENT_MEMORY_DEFAULT
  (settings.agent_memory_default) + memory_enabled dans la réponse de création.
- Décision : mémoire d'espace (lignes workspace_id NULL) remise — YAGNI, rien
  ne l'écrivait ; noté ponytail: dans le service avec le chemin de montée.
- Tests : tests/test_v754_memory.py (8) dont 2 runs réels du moteur (FakeLLM
  capture le prompt : mémoire présente ON / absente OFF) ; test_v751 et
  test_v753 adaptés (2 sections « bientôt » restantes). Suite complète 1293
  verts (-n auto, 2e run ; 1 échec intermittent sur test_v56_import URL au
  1er run, vert isolé + vert au run suivant = flake préexistant), ruff 0,
  eslint 0 problème. OpenAPI 516 chemins / 7.54.0.
2026-10-06 22:53:15 -04:00

90 lines
3.4 KiB
Python

"""Mémoire de l'agent (v7.54.0).
Une ligne résumé **par conversation**, mise à jour à chaque run et ré-injectée
au contexte du run suivant — le moteur n'envoie jamais l'historique des messages
(`AgentEngine.run()` ne construit que system + objectif courant), donc sans
mémoire chaque run repart de zéro.
Toggle : colonne `agent_conversations.memory_enabled` — OFF = aucune lecture ni
écriture (contexte strict du run courant).
ponytail : mémoire par conversation seulement. Si besoin de notes partagées
entre conversations, ajouter des lignes avec ``conversation_id NULL`` +
``workspace_id`` et élargir la lecture dans ``context_for()``.
"""
from __future__ import annotations
import logging
import sqlite3
from app.db import get_conn
logger = logging.getLogger(__name__)
INJECT_BUDGET = 4000 # caractères de mémoire max injectés dans un prompt
NOTE_LINES = 20 # échanges max conservés
NOTE_OBJECTIVE = 180 # troncature de l'objectif
NOTE_ANSWER = 700 # troncature de la réponse
def _enabled(conn: sqlite3.Connection, conversation_id: int) -> bool:
row = conn.execute(
"SELECT memory_enabled FROM agent_conversations WHERE id=?",
(conversation_id,),
).fetchone()
if row is None:
return False
return bool(row["memory_enabled"])
def _one_line(text: str, limit: int) -> str:
return " ".join((text or "").split())[:limit]
def context_for(conversation_id: int) -> str:
"""Bloc « mémoire » à préfixer au contexte, ou ``''`` (toggle OFF / vide)."""
with get_conn() as conn:
if not _enabled(conn, conversation_id):
return ""
row = conn.execute(
"SELECT content FROM agent_memory WHERE conversation_id=?",
(conversation_id,),
).fetchone()
content = ((row["content"] if row else "") or "").strip()
if not content:
return ""
if len(content) > INJECT_BUDGET:
content = "…(début de mémoire tronqué)\n" + content[-INJECT_BUDGET:]
return "## Mémoire de la conversation (échanges précédents)\n" + content
def remember(conversation_id: int, objective: str, final_text: str) -> None:
"""Ajoute un échange à la mémoire — no-op si le toggle est OFF.
Ne doit **jamais** faire échouer un run : toute erreur est journalisée.
"""
note = f"- **{_one_line(objective, NOTE_OBJECTIVE)}** → {_one_line(final_text, NOTE_ANSWER)}"
try:
with get_conn() as conn:
if not _enabled(conn, conversation_id):
return
row = conn.execute(
"SELECT content FROM agent_memory WHERE conversation_id=?",
(conversation_id,),
).fetchone()
lines = [ln for ln in ((row["content"] if row else "") or "").split("\n") if ln.strip()]
lines.append(note)
if len(lines) > NOTE_LINES:
lines = lines[-NOTE_LINES:]
conn.execute(
"INSERT INTO agent_memory (conversation_id, content, updated_at) "
"VALUES (?, ?, CURRENT_TIMESTAMP) "
"ON CONFLICT(conversation_id) DO UPDATE SET "
"content=excluded.content, updated_at=CURRENT_TIMESTAMP",
(conversation_id, "\n".join(lines)),
)
conn.commit()
except Exception: # noqa: BLE001 — la mémoire ne casse jamais un run
logger.exception("Agent memory save failed for conversation #%s", conversation_id)