Files
mission-control-v2/backend/services/memory.py
T
Hitonabi 81468df9c0 feat(2.0): Phase 3 — Memory + MCP
Geteiltes Gedaechtnis: services/memory.py (SQLite/WAL, 5 Kategorien,
Dedupe-Kurator deterministisch), routers/memory.py (CRUD/export/dedupe).
MCP: mcp/mcp_memory.py (Guard-Beschreibungen gegen 14B-Loop) + mcp/mcp_mc.py
NEU (Stack-Management fuer Hermes: list/discover/register/route/restart/
status). Frontend MemoryView (Add/Filter/Suche/Delete/Aufraeumen).

Lokal verifiziert: CRUD + Dedupe (TestClient), MCP-Server syntax-OK,
Frontend-Build + Browser (MemoryView). Docs aktualisiert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 08:16:12 +02:00

150 lines
5.5 KiB
Python

"""
Geteiltes Gedächtnis (die „Verfassung") — SQLite aus stdlib, WAL-Mode.
Portiert aus Mission Control v1 (routers/memory.py), DB-Logik als Service isoliert.
5 Kategorien: user · instruction · stable · versioned · ephemeral (7-Tage-TTL).
Dedupe = deterministischer Kurator (exakt/enthalten/ähnlich), KEIN LLM.
"""
import re
import sqlite3
import uuid
from datetime import datetime, timezone
from difflib import SequenceMatcher
from config import MEMORY_DB
CATEGORIES = ("user", "instruction", "stable", "versioned", "ephemeral")
_conn: sqlite3.Connection | None = None
def db() -> sqlite3.Connection:
global _conn
if _conn is None:
MEMORY_DB.parent.mkdir(parents=True, exist_ok=True)
_conn = sqlite3.connect(str(MEMORY_DB), check_same_thread=False)
_conn.row_factory = sqlite3.Row
_conn.execute("PRAGMA journal_mode=WAL")
_conn.execute("""
CREATE TABLE IF NOT EXISTS memories (
id TEXT PRIMARY KEY,
content TEXT NOT NULL,
category TEXT NOT NULL DEFAULT 'stable',
source TEXT NOT NULL DEFAULT 'manual',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)
""")
_conn.execute(
"DELETE FROM memories WHERE category='ephemeral'"
" AND datetime(created_at) < datetime('now','-7 days')"
)
_conn.commit()
return _conn
def _now() -> str:
return datetime.now(timezone.utc).isoformat()
def list_memories(q: str = "", category: str = "") -> list[dict]:
sql, params, conds = "SELECT * FROM memories", [], []
if q:
conds.append("content LIKE ?"); params.append(f"%{q}%")
if category:
conds.append("category = ?"); params.append(category)
if conds:
sql += " WHERE " + " AND ".join(conds)
sql += " ORDER BY created_at DESC"
return [dict(r) for r in db().execute(sql, params).fetchall()]
def add_memory(content: str, category: str = "stable", source: str = "manual") -> dict:
now, mid = _now(), str(uuid.uuid4())
db().execute(
"INSERT INTO memories (id,content,category,source,created_at,updated_at) VALUES (?,?,?,?,?,?)",
(mid, content.strip(), category, source, now, now),
)
db().commit()
return {"id": mid, "content": content.strip(), "category": category,
"source": source, "created_at": now, "updated_at": now}
def update_memory(mid: str, content: str | None = None, category: str | None = None) -> dict | None:
row = db().execute("SELECT * FROM memories WHERE id=?", (mid,)).fetchone()
if not row:
return None
now = _now()
new_content = content.strip() if content is not None else row["content"]
new_cat = category if category is not None else row["category"]
db().execute("UPDATE memories SET content=?,category=?,updated_at=? WHERE id=?",
(new_content, new_cat, now, mid))
db().commit()
return {"id": mid, "content": new_content, "category": new_cat,
"source": row["source"], "created_at": row["created_at"], "updated_at": now}
def delete_memory(mid: str) -> bool:
if not db().execute("SELECT id FROM memories WHERE id=?", (mid,)).fetchone():
return False
db().execute("DELETE FROM memories WHERE id=?", (mid,))
db().commit()
return True
def export_text() -> dict:
rows = db().execute("SELECT * FROM memories ORDER BY category, updated_at DESC").fetchall()
lines = ["# Mission Control — Gedächtnis\n"]
current = ""
for r in rows:
if r["category"] != current:
lines.append(f"\n## {r['category']}\n")
current = r["category"]
lines.append(f"- {r['content']} _(Quelle: {r['source']}, {r['updated_at'][:10]})_")
return {"text": "\n".join(lines), "count": len(rows)}
def _norm(s: str) -> str:
s = re.sub(r"[^\w\s]", " ", s.lower(), flags=re.UNICODE)
return re.sub(r"\s+", " ", s).strip()
def dedupe(apply: bool = False, threshold: float = 0.85) -> dict:
"""Deterministischer Kurator: findet Dubletten (exakt/enthalten/ähnlich) je Kategorie,
behält den vollständigsten (längsten) Eintrag. Konservativ, kein LLM."""
rows = [dict(r) for r in db().execute(
"SELECT * FROM memories ORDER BY length(content) DESC, created_at ASC").fetchall()]
used: set[str] = set()
groups: list[dict] = []
for i, a in enumerate(rows):
if a["id"] in used:
continue
na = _norm(a["content"])
if not na:
continue
dups = []
for b in rows[i + 1:]:
if b["id"] in used or b["category"] != a["category"]:
continue
nb = _norm(b["content"])
if not nb:
continue
if nb in na or na in nb or SequenceMatcher(None, na, nb).ratio() >= threshold:
dups.append(b); used.add(b["id"])
if dups:
used.add(a["id"])
groups.append({
"keep": {"id": a["id"], "content": a["content"], "category": a["category"]},
"remove": [{"id": d["id"], "content": d["content"]} for d in dups],
})
dup_count = sum(len(g["remove"]) for g in groups)
removed = 0
if apply:
for g in groups:
for d in g["remove"]:
db().execute("DELETE FROM memories WHERE id=?", (d["id"],)); removed += 1
if removed:
db().commit()
return {"groups": groups, "duplicate_count": dup_count, "removed": removed, "applied": apply}