b383711f6d
Saubere Neuordnung der Gedaechtnis-Kategorien an der etablierten Memory-Taxonomie (semantisch/prozedural/episodisch), bewusst knapp (Best Practice: 3-5, klar beschrieben): identity (Identitaet & Vorlieben) · knowledge (Wissen & Fakten) · rules (Regeln & Konventionen) · events (Ereignisse & Entscheidungen) Loest die alten gemischten 5 (user/instruction/stable/versioned/ephemeral) ab. Auto-Einordnung: OSS-mem0 kann nicht nativ kategorisieren (Cloud-Feature) -> nach der Fakt-Extraktion ordnet dasselbe (Thinking-freie) Hirn jeden neuen Fakt per JSON-Call genau einer Kategorie zu (classify_facts im Sidecar /learn). Behebt den "alles ist stable"- Bug. Manuelle Eintraege: Kategorie weiter waehlbar (Default knowledge). Umgesetzt in mem0_service, backend (services/routers), mcp_memory (Tool-Docs) und Frontend (MemoryView + GraphView: Labels/Farben/Filter). Kein Migrationsbedarf (leerer Start). Live verifiziert: gemischter Absatz -> identity/rules/knowledge/events korrekt zugeordnet. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
148 lines
5.6 KiB
Python
148 lines
5.6 KiB
Python
"""
|
|
Geteiltes Gedächtnis (die „Verfassung") — jetzt auto-lernend & semantisch über Mem0.
|
|
|
|
Dieser Service ist nur noch ein dünner HTTP-Client auf den Mem0-Sidecar (mem0_service/app.py,
|
|
läuft im ~/.mem0/venv unter Python 3.12). Die `/api/memory`-API-Form bleibt unverändert, damit
|
|
UI und MCP-Server kompatibel bleiben. Neu gegenüber der alten flachen SQLite:
|
|
|
|
- search (q gesetzt) ist SEMANTISCH (Vektor/Embeddings) statt LIKE-Textsuche, mit Relevanz-Score.
|
|
- learn() reicht Gesprächs-Turns durch → Mem0 EXTRAHIERT Fakten selbst (Auto-Lernen).
|
|
- Dedup macht Mem0 beim Auto-Lernen selbst; der manuelle Kurator unten bleibt als Komfort.
|
|
|
|
5 Kategorien (user · instruction · stable · versioned · ephemeral) bleiben als Metadaten erhalten.
|
|
"""
|
|
|
|
import re
|
|
from difflib import SequenceMatcher
|
|
|
|
import httpx
|
|
|
|
from config import MEM0_SERVICE_URL
|
|
|
|
CATEGORIES = ("identity", "knowledge", "rules", "events")
|
|
|
|
_TIMEOUT = httpx.Timeout(60.0, connect=5.0) # LLM-Extraktion kann ein paar Sekunden dauern
|
|
|
|
|
|
def _get(path: str, **params) -> list | dict:
|
|
r = httpx.get(f"{MEM0_SERVICE_URL}{path}", params=params, timeout=_TIMEOUT)
|
|
r.raise_for_status()
|
|
return r.json()
|
|
|
|
|
|
def _post(path: str, data: dict) -> dict:
|
|
r = httpx.post(f"{MEM0_SERVICE_URL}{path}", json=data, timeout=_TIMEOUT)
|
|
r.raise_for_status()
|
|
return r.json()
|
|
|
|
|
|
def _put(path: str, data: dict) -> dict:
|
|
r = httpx.put(f"{MEM0_SERVICE_URL}{path}", json=data, timeout=_TIMEOUT)
|
|
r.raise_for_status()
|
|
return r.json()
|
|
|
|
|
|
def _delete(path: str) -> dict:
|
|
r = httpx.delete(f"{MEM0_SERVICE_URL}{path}", timeout=_TIMEOUT)
|
|
r.raise_for_status()
|
|
return r.json()
|
|
|
|
|
|
def list_memories(q: str = "", category: str = "") -> list[dict]:
|
|
"""Alle Fakten oder — wenn q gesetzt — die semantisch ähnlichsten (mit `score`)."""
|
|
return _get("/memory", **{k: v for k, v in (("q", q), ("category", category)) if v})
|
|
|
|
|
|
def add_memory(content: str, category: str = "stable", source: str = "manual") -> dict:
|
|
"""Einen Fakt VERBATIM speichern (keine LLM-Umformung). Auto-Lernen → learn()."""
|
|
return _post("/memory", {"content": content.strip(), "category": category, "source": source})
|
|
|
|
|
|
def update_memory(mid: str, content: str | None = None, category: str | None = None) -> dict | None:
|
|
try:
|
|
return _put(f"/memory/{mid}", {"content": content, "category": category})
|
|
except httpx.HTTPStatusError as exc:
|
|
if exc.response.status_code == 404:
|
|
return None
|
|
raise
|
|
|
|
|
|
def delete_memory(mid: str) -> bool:
|
|
try:
|
|
_delete(f"/memory/{mid}")
|
|
return True
|
|
except httpx.HTTPStatusError as exc:
|
|
if exc.response.status_code == 404:
|
|
return False
|
|
raise
|
|
|
|
|
|
def learn(text: str | None = None, messages: list[dict] | None = None,
|
|
source: str = "auto", category: str = "stable") -> dict:
|
|
"""Auto-Lernen: Text/Gesprächs-Turns durchreichen → Mem0 extrahiert die Fakten selbst."""
|
|
return _post("/learn", {"text": text, "messages": messages,
|
|
"source": source, "category": category})
|
|
|
|
|
|
def graph(min_score: float = 0.45, top_k: int = 3) -> dict:
|
|
"""Fakten als Ähnlichkeits-Graph (Knoten + semantische Kanten) für die UI-Visualisierung."""
|
|
return _get("/graph", min_score=min_score, top_k=top_k)
|
|
|
|
|
|
def export_text() -> dict:
|
|
rows = sorted(list_memories(), key=lambda r: (r.get("category", ""), r.get("updated_at", "")))
|
|
lines = ["# Mission Control — Gedächtnis\n"]
|
|
current = ""
|
|
for r in rows:
|
|
if r.get("category") != current:
|
|
current = r.get("category", "")
|
|
lines.append(f"\n## {current}\n")
|
|
src = r.get("source", "")
|
|
when = (r.get("updated_at") or "")[:10]
|
|
lines.append(f"- {r.get('content', '')} _(Quelle: {src}, {when})_")
|
|
return {"text": "\n".join(lines), "count": len(rows)}
|
|
|
|
|
|
# --- Manueller Kurator (deterministisch, kein LLM) ---------------------------
|
|
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:
|
|
"""Findet Dubletten (exakt/enthalten/ähnlich) je Kategorie, behält den längsten
|
|
Eintrag. Mem0 dedupliziert beim Auto-Lernen schon semantisch — das hier ist der
|
|
manuelle Komfort-Knopf fürs UI (z.B. nach vielen Verbatim-Importen)."""
|
|
rows = sorted(list_memories(), key=lambda r: (-len(r.get("content", "")), r.get("created_at", "")))
|
|
used: set[str] = set()
|
|
groups: list[dict] = []
|
|
for i, a in enumerate(rows):
|
|
if a["id"] in used:
|
|
continue
|
|
na = _norm(a.get("content", ""))
|
|
if not na:
|
|
continue
|
|
dups = []
|
|
for b in rows[i + 1:]:
|
|
if b["id"] in used or b.get("category") != a.get("category"):
|
|
continue
|
|
nb = _norm(b.get("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.get("content"), "category": a.get("category")},
|
|
"remove": [{"id": d["id"], "content": d.get("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"]:
|
|
if delete_memory(d["id"]):
|
|
removed += 1
|
|
return {"groups": groups, "duplicate_count": dup_count, "removed": removed, "applied": apply}
|