"""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)