Feat: Fundierte 4-Typen-Memory-Taxonomie + LLM-Auto-Einordnung

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>
This commit is contained in:
Hitonabi
2026-06-27 21:04:55 +02:00
parent 689ea48d72
commit b383711f6d
10 changed files with 128 additions and 78 deletions
+64 -8
View File
@@ -45,9 +45,19 @@ API_KEY = os.environ.get("MEM0_API_KEY", "sk-local") # llama.cpp ignoriert den
LLM_MAX_TOKENS = int(os.environ.get("MEM0_LLM_MAX_TOKENS", "2048"))
SEARCH_TOP_K = int(os.environ.get("MEM0_SEARCH_TOP_K", "50"))
# 5 Kategorien wie im alten System (UI-Kompatibilität). Mem0 selbst kennt keine
# Kategorien — wir führen sie als Metadaten mit.
CATEGORIES = ("user", "instruction", "stable", "versioned", "ephemeral")
# Kategorien = fundierte Memory-Taxonomie (semantisch/prozedural/episodisch), bewusst knapp (4).
# Mem0-OSS kann nicht nativ klassifizieren (Cloud-Feature) → wir lassen das Hirn beim Lernen
# einordnen (classify_facts). Die Beschreibungen steuern die Treffsicherheit der Auto-Zuordnung.
CATEGORIES = ("identity", "knowledge", "rules", "events")
DEFAULT_CATEGORY = "knowledge"
CATEGORY_DESCRIPTIONS = {
"identity": "Identität & Vorlieben des Nutzers: wer er ist (Name, Rolle), wie er angesprochen werden "
"will, persönliche Vorlieben und seine Arbeitsweise.",
"knowledge": "Wissen & Fakten: stabile Fakten über Stack, Projekte, Tools, Infrastruktur und Umgebung.",
"rules": "Regeln & Konventionen: verbindliche Anweisungen und Workflows — wie etwas gemacht werden "
"soll, Do's und Don'ts.",
"events": "Ereignisse & Entscheidungen: was passiert ist, getroffene Entscheidungen, zeitgebundene Vorgänge.",
}
class NoThinkLLM(OpenAILLM):
@@ -140,7 +150,7 @@ def _to_item(r: dict, score=None) -> dict:
class MemIn(BaseModel):
content: str
category: str = "stable"
category: str = DEFAULT_CATEGORY
source: str = "manual"
@@ -153,7 +163,8 @@ class LearnIn(BaseModel):
text: str | None = None
messages: list[dict] | None = None
source: str = "auto"
category: str = "stable"
category: str = DEFAULT_CATEGORY
classify: bool = True # Hirn ordnet die gelernten Fakten automatisch ein
@app.get("/health")
@@ -277,10 +288,38 @@ def graph(min_score: float = 0.45, top_k: int = 3) -> dict:
return {"nodes": nodes, "edges": edges}
def classify_facts(facts: list[tuple[str, str]]) -> dict:
"""Ordnet jeden Fakt per LLM GENAU einer Kategorie zu → {id: category}.
Nutzt dasselbe (Thinking-freie) Hirn wie die Extraktion. Fehler = leeres Mapping (Fallback)."""
if not facts:
return {}
import json as _json
import re as _re
cat_lines = "\n".join(f"- {k}: {v}" for k, v in CATEGORY_DESCRIPTIONS.items())
system = (
"Du bist ein präziser Klassifikator für ein Langzeitgedächtnis. Ordne jeden Fakt GENAU EINER "
"Kategorie zu. Verfügbare Kategorien (nur diese Slugs verwenden):\n" + cat_lines +
"\nAntworte NUR als JSON-Objekt der Form {\"<id>\": \"<slug>\", …}."
)
body = "\n".join(f"{fid}: {txt}" for fid, txt in facts)
try:
resp = mem().llm.generate_response(
messages=[{"role": "system", "content": system},
{"role": "user", "content": "Fakten:\n" + body}],
response_format={"type": "json_object"},
)
m = _re.search(r"\{.*\}", resp or "", _re.S)
data = _json.loads(m.group(0)) if m else {}
return {str(k): v for k, v in data.items() if v in CATEGORIES}
except Exception:
log.exception("Auto-Klassifikation fehlgeschlagen")
return {}
@app.post("/learn")
def learn(body: LearnIn) -> dict:
"""Auto-Lernen: Gesprächs-Turns/Text durchreichen → Mem0 EXTRAHIERT Fakten selbst
(infer=True) und entscheidet ADD/UPDATE/NONE gegen das bestehende Gedächtnis."""
(infer=True). Anschließend ordnet das Hirn jeden neuen Fakt automatisch einer Kategorie zu."""
msgs = body.messages
if not msgs and body.text:
msgs = [{"role": "user", "content": body.text}]
@@ -290,9 +329,26 @@ def learn(body: LearnIn) -> dict:
msgs, user_id=USER_ID, infer=True,
metadata={"category": body.category, "source": body.source},
)
results = res.get("results", [])
cats: dict = {}
if body.classify:
to_classify = [(r["id"], r.get("memory", "")) for r in results
if r.get("id") and r.get("memory") and r.get("event") in ("ADD", "UPDATE")]
cats = classify_facts(to_classify)
for r in results:
cat = cats.get(r.get("id"))
if cat:
try:
mem().update(r["id"], data=r.get("memory", ""),
metadata={"category": cat, "source": body.source})
except Exception:
log.debug("Kategorie-Update fehlgeschlagen für %s", r.get("id"))
return {"results": [
{"id": r.get("id"), "content": r.get("memory"), "event": r.get("event")}
for r in res.get("results", [])
{"id": r.get("id"), "content": r.get("memory"), "event": r.get("event"),
"category": cats.get(r.get("id"), body.category)}
for r in results
]}