- app/services/plugins.py + migration 35 : table plugins (slug, name,
description, enabled) pré-remplie avec 3 modules câblés — web-tools,
web-clipper, automations ; ligne absente = activé (défaut sûr)
- automations OFF → dépendance FastAPI posée à l'include_router dans main.py
(aucun router touché) → toutes les routes /workspace/automations* refusées +
garde de tick du scheduler en arrière-plan
- web-clipper OFF → GET /extensions et tout /api/v2/web-clipper/* refusés
- web-tools OFF → web_search et fetch_url retirés du schéma ET de execute()
via ToolRegistry._all() : le LLM ne les voit plus
- UI rendue côté serveur : global Jinja plugin_enabled(slug) — nav
« Extensions » / « Automations » en {% if %} (absentes du DOM), sections
conditionnées en x-show dans settings.html
- menu + : l'entrée « Add plugins » devient vivante (fini disabled:true) —
liste des 3 plugins avec bascule, GET/PATCH /api/agent/plugins[/slug]
(slug inconnu → 404, 401 sans session)
- tests : tests/test_v758_plugins.py (10 tests) — routes refusées (302 hors
/api, 404 JSON pour /api*), outils retirés, nav disparue, persistance,
câblage ; assertions disabled:true == 0 dans les tests des phases 1/3/4/5/7
- livraison : VERSION + app/main = 7.58.0, OpenAPI 525 chemins, CHANGELOG,
ROADMAP phase 8 cochée (menu + complet), avenant phase 8 (docs)
138 lines
5.2 KiB
Python
138 lines
5.2 KiB
Python
"""v7.54.0 — Mémoire de l'agent : toggle par conversation, injection au contexte."""
|
|
|
|
import asyncio
|
|
from pathlib import Path
|
|
|
|
from app.db import get_conn
|
|
from app.services import agent_memory
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
JS_PATH = ROOT / "static" / "js" / "agent_panel_2.js"
|
|
ENGINE = ROOT / "app" / "services" / "agent_engine.py"
|
|
|
|
|
|
def _conv(client) -> tuple[int, dict]:
|
|
r = client.post("/api/agent/conversations", json={"title": "mémoire"})
|
|
assert r.status_code == 200, r.text
|
|
return r.json()["id"], r.json()
|
|
|
|
|
|
def test_migration_created_column_and_table(client):
|
|
with get_conn() as conn:
|
|
cols = {r[1] for r in conn.execute("PRAGMA table_info(agent_conversations)")}
|
|
assert "memory_enabled" in cols
|
|
tbl = conn.execute(
|
|
"SELECT name FROM sqlite_master WHERE type='table' AND name='agent_memory'"
|
|
).fetchone()
|
|
assert tbl is not None
|
|
|
|
|
|
def test_create_conversation_defaults_to_memory_on(client):
|
|
_, created = _conv(client)
|
|
assert created["memory_enabled"] == 1 # settings.agent_memory_default
|
|
|
|
|
|
def test_patch_conversation_toggles_memory_persisted(client):
|
|
cid, _ = _conv(client)
|
|
assert client.patch(f"/api/agent/conversations/{cid}", json={"memory_enabled": 0}).status_code == 200
|
|
rows = {c["id"]: c for c in client.get("/api/agent/conversations").json()["conversations"]}
|
|
assert rows[cid]["memory_enabled"] == 0
|
|
|
|
client.patch(f"/api/agent/conversations/{cid}", json={"memory_enabled": 1})
|
|
rows = {c["id"]: c for c in client.get("/api/agent/conversations").json()["conversations"]}
|
|
assert rows[cid]["memory_enabled"] == 1
|
|
|
|
|
|
def test_context_for_on_off_and_persistence(client):
|
|
cid, _ = _conv(client)
|
|
agent_memory.remember(cid, "Où en est le design system ?", "Les tokens sont dans design-tokens.css.")
|
|
ctx = agent_memory.context_for(cid)
|
|
assert "Mémoire de la conversation" in ctx
|
|
assert "design-tokens.css" in ctx
|
|
|
|
# OFF → plus rien n'est lu ni écrit
|
|
client.patch(f"/api/agent/conversations/{cid}", json={"memory_enabled": 0})
|
|
assert agent_memory.context_for(cid) == ""
|
|
agent_memory.remember(cid, "Effacé ?", "Ne doit pas apparaître.")
|
|
assert agent_memory.context_for(cid) == ""
|
|
|
|
# ON à nouveau → la mémoire écrite avant le OFF est toujours là (persistée)
|
|
client.patch(f"/api/agent/conversations/{cid}", json={"memory_enabled": 1})
|
|
assert "design-tokens.css" in agent_memory.context_for(cid)
|
|
assert "Ne doit pas apparaître" not in agent_memory.context_for(cid)
|
|
|
|
|
|
def test_memory_budget_and_note_lines_bounded(client):
|
|
cid, _ = _conv(client)
|
|
for i in range(30):
|
|
agent_memory.remember(cid, f"question {i} " + "x" * 300, "réponse " + "y" * 900)
|
|
ctx = agent_memory.context_for(cid)
|
|
header = "## Mémoire de la conversation (échanges précédents)"
|
|
assert ctx.startswith(header)
|
|
assert len(ctx) <= agent_memory.INJECT_BUDGET + len(header) + 60
|
|
with get_conn() as conn:
|
|
content = conn.execute(
|
|
"SELECT content FROM agent_memory WHERE conversation_id=?", (cid,)
|
|
).fetchone()["content"]
|
|
assert content.count("\n") + 1 <= agent_memory.NOTE_LINES
|
|
|
|
|
|
async def _drain(engine, cid, objective):
|
|
"""Consomme le générateur SSE de engine.run()."""
|
|
return [ev async for ev in engine.run(cid, objective)]
|
|
|
|
|
|
class _Resp:
|
|
def __init__(self, text=""):
|
|
self.text = text
|
|
self.tool_calls = []
|
|
self.usage = {}
|
|
|
|
|
|
class _RecorderLLM:
|
|
def __init__(self):
|
|
self.last_user = ""
|
|
|
|
async def complete(self, messages, *, model=None, tools=None, stream=False):
|
|
self.last_user = next(m["content"] for m in messages if m["role"] == "user")
|
|
return _Resp("C'est noté.")
|
|
|
|
|
|
def test_engine_run_injects_memory_when_on(client):
|
|
"""Le run réel pousse la mémoire dans le prompt utilisateur (toggle ON)."""
|
|
from app.services.agent_engine import AgentEngine
|
|
|
|
cid, _ = _conv(client)
|
|
agent_memory.remember(cid, "Priorité du sprint", "Livrer le menu + en priorité.")
|
|
fake = _RecorderLLM()
|
|
engine = AgentEngine(1, llm=fake)
|
|
asyncio.run(_drain(engine, cid, "Quelle est la priorité ?"))
|
|
assert "Mémoire de la conversation" in fake.last_user
|
|
assert "menu +" in fake.last_user
|
|
|
|
|
|
def test_engine_run_omits_memory_when_off(client):
|
|
from app.services.agent_engine import AgentEngine
|
|
|
|
cid, _ = _conv(client)
|
|
agent_memory.remember(cid, "Secret", "ne pas injecter")
|
|
client.patch(f"/api/agent/conversations/{cid}", json={"memory_enabled": 0})
|
|
fake = _RecorderLLM()
|
|
engine = AgentEngine(1, llm=fake)
|
|
asyncio.run(_drain(engine, cid, "Bonjour"))
|
|
assert "Mémoire de la conversation" not in fake.last_user
|
|
assert "ne pas injecter" not in fake.last_user
|
|
|
|
|
|
def test_memory_menu_toggle_wired():
|
|
src = JS_PATH.read_text(encoding="utf-8")
|
|
root = src.split("var FD_PLUS_MENU = [", 1)[1].split("];", 1)[0]
|
|
assert "action:'memory'" in root
|
|
assert "bientôt (phase 4)" not in root
|
|
assert root.count("disabled:true") == 0 # v7.58.0 : menu complet
|
|
assert "toggleMemory()" in src
|
|
assert "memory_enabled: next" in src
|
|
engine = ENGINE.read_text(encoding="utf-8")
|
|
assert "agent_memory.context_for(" in engine
|
|
assert "agent_memory.remember(" in engine
|