diff --git a/docs/wissen/GEDAECHTNIS-BEREICHE.md b/docs/wissen/GEDAECHTNIS-BEREICHE.md new file mode 100644 index 0000000..aacbdd3 --- /dev/null +++ b/docs/wissen/GEDAECHTNIS-BEREICHE.md @@ -0,0 +1,45 @@ +# Gedächtnis-Bereiche: alltag vs. projekt (Stand 19.07.2026) + +## Warum +Lucys mem0 sammelte Coding-/Ops-Schutt (execute_code-Bugs, Session-Zahlen, +Hardware-Debugging) — Rauschen im Alltags-Recall. Entscheid (Commander, 19.07.): +**KEIN zweites mem0** (das Coding-Gedächtnis ist das Repo: AGENTS.md, Code, Git, +Karten). Stattdessen bekommt jeder Fakt ein Bereichs-Feld — die Trennung liegt in +den Daten, nicht in einer zweiten Datenbank. + +## Was seit 19.07. existiert (gebaut, live) +- `mem0_service/app.py` → `classify_facts()` ordnet jeden neu gelernten Fakt + zusätzlich einem **Bereich** zu: `alltag` (Nutzer als Person: Vorlieben, Leben, + Arbeitsweise, Grundsatz-Entscheidungen) oder `projekt` (Technik: Code, Modelle, + Server, Infrastruktur, Fehler-/Projektstände). Gespeichert als Metadatum + `bereich`, ausgegeben in jedem `/memory`-Item. +- `GET /memory?bereich=alltag|projekt` filtert. **Alt-Fakten ohne Feld laufen bei + jedem Filter MIT** (nichts verschwindet durch die Umstellung). +- Quellen-Hygiene: Die IDE-Welt (Zed+OpenCode) schreibt seit 19.07. GAR NICHT mehr + ins mem0; Junk-Guard + Lern-Bündelung + monatliche Konsolidierung (3., 06:40) + laufen. Der Neuzugang an Schutt sollte dadurch bereits stark sinken. + +## Selbstprüfung nach 4 Wochen (Cron „gedaechtnis-check", einmalig Mitte August) +Auftrag an Lucy: Die 50 neuesten Fakten ansehen (`GET /api/memory` sortiert +neueste zuerst). Frage: Wie viele davon sind Tool-/Fehler-/Momentaufnahmen-Schutt +(Muster: Tool-Namen, Fehlermeldungen, Session-/Commit-Zahlen, „blockiert/behoben"- +Zustände)? **> 10 von 50 → Werkstatt-Karte anlegen** mit Verweis auf dieses +Dokument und den Bauauftrag unten. Sonst: kurze Entwarnung an den Commander. + +## Bauauftrag FALLS nötig: Recall-Filter (klein, KEINE neue Schicht) +Ziel: In Alltags-Gesprächen streut kein Projekt-Schutt in den Recall. +Umsetzung in `hermes/plugins/mc2-memory/__init__.py`, Funktion `prefetch()`: +1. Neuer Env-Schalter `MC2_MEMORY_RECALL_BEREICH` (leer = aus, Default). +2. Wenn gesetzt: an den Request `params={"q": q, "bereich": }` anhängen — + der Sidecar filtert serverseitig (Fakten ohne Feld laufen mit, siehe oben). +3. Setzen NUR für Lucys Voice-/Chat-Gateway (Umgebung der hermes-gateway-Unit), + NICHT für Worker-Profile — Worker sollen Projekt-Fakten ja sehen. +4. Verify: zwei Test-Learns (ein Alltags-, ein Projekt-Fakt), dann `/memory?q=…` + mit und ohne Filter vergleichen; Latenz unverändert (< 1 s Overhead). +Aufwand: < 1 Stunde. Regeln: deploy braucht Commander-Ja; Plugin-Änderung = +Gateway-Neustart. + +## Bewusst NICHT tun +- Kein zweites mem0/zweite Collection (doppelte Pflege, doppelte Konsolidierung). +- Keinen Alt-Bestand automatisch umtaggen (die Monats-Konsolidierung räumt ohnehin; + der Filter behandelt ungetaggte Fakten konservativ als „immer sichtbar"). diff --git a/mem0_service/app.py b/mem0_service/app.py index c8c24f5..bee8123 100644 --- a/mem0_service/app.py +++ b/mem0_service/app.py @@ -166,6 +166,7 @@ def _to_item(r: dict, score=None) -> dict: "id": r.get("id"), "content": r.get("memory", ""), "category": meta.get("category") or "stable", + "bereich": meta.get("bereich") or "", "source": meta.get("source") or "auto", "created_at": r.get("created_at") or "", "updated_at": r.get("updated_at") or r.get("created_at") or "", @@ -235,8 +236,10 @@ def _rerank(query: str, items: list[dict]) -> list[dict]: @app.get("/memory") -def list_memory(q: str = "", category: str = "") -> list[dict]: - """q gesetzt → semantische Suche (mit Relevanz-Score). Sonst → alle Fakten.""" +def list_memory(q: str = "", category: str = "", bereich: str = "") -> list[dict]: + """q gesetzt → semantische Suche (mit Relevanz-Score). Sonst → alle Fakten. + bereich='alltag'|'projekt' filtert (Fakten OHNE Bereichs-Feld laufen immer mit, + damit Alt-Bestand nie verschwindet).""" if q: res = mem().search(q, filters={"user_id": USER_ID}, top_k=SEARCH_TOP_K) items = [_to_item(r, r.get("score")) for r in res.get("results", [])] @@ -248,6 +251,8 @@ def list_memory(q: str = "", category: str = "") -> list[dict]: items.sort(key=lambda i: i.get("created_at", ""), reverse=True) if category: items = [i for i in items if i["category"] == category] + if bereich in ("alltag", "projekt"): + items = [i for i in items if i.get("bereich") in (bereich, "")] return items @@ -349,8 +354,14 @@ def graph(min_score: float = 0.45, top_k: int = 3) -> dict: 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).""" + """Ordnet jeden Fakt per LLM einer Kategorie UND einem Bereich zu → + {id: {"category": , "bereich": "alltag"|"projekt"}}. + + Bereich (19.07.2026, Trennung der Welten): "alltag" = der Nutzer als Person + (Vorlieben, Leben, Arbeitsweise, Entscheidungen), "projekt" = Technik-/Code-/ + Infrastruktur-Stände. Grundlage für einen späteren Recall-Filter, damit + Projekt-Schutt nicht in Alltags-Gespräche streut — OHNE zweites Gedächtnis. + Nutzt dasselbe (Thinking-freie) Hirn wie die Extraktion. Fehler = leeres Mapping.""" if not facts: return {} import json as _json @@ -359,7 +370,11 @@ def classify_facts(facts: list[tuple[str, str]]) -> dict: 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 {\"\": \"\", …}." + "\nOrdne zusätzlich jeden Fakt GENAU EINEM Bereich zu: 'alltag' (der Nutzer als Person: " + "Vorlieben, Leben, Anrede, Arbeitsweise, grundsätzliche Entscheidungen) oder 'projekt' " + "(Technik: Code, Modelle, Server, Infrastruktur, Projekt- und Fehlerstände). " + "Antworte NUR als JSON-Objekt der Form " + "{\"\": {\"kategorie\": \"\", \"bereich\": \"alltag|projekt\"}, …}." ) body = "\n".join(f"{fid}: {txt}" for fid, txt in facts) try: @@ -370,7 +385,18 @@ def classify_facts(facts: list[tuple[str, str]]) -> dict: ) 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} + out: dict = {} + for k, v in data.items(): + if isinstance(v, str): # Alt-Form {"id": "slug"} weiter akzeptieren + if v in CATEGORIES: + out[str(k)] = {"category": v, "bereich": ""} + elif isinstance(v, dict): + cat = v.get("kategorie") or v.get("category") or "" + ber = v.get("bereich") or "" + if cat in CATEGORIES or ber in ("alltag", "projekt"): + out[str(k)] = {"category": cat if cat in CATEGORIES else DEFAULT_CATEGORY, + "bereich": ber if ber in ("alltag", "projekt") else ""} + return out except Exception: log.exception("Auto-Klassifikation fehlgeschlagen") return {} @@ -434,17 +460,20 @@ def learn(body: LearnIn) -> dict: 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: + info = cats.get(r.get("id")) + if info: + meta = {"category": info["category"], "source": body.source} + if info.get("bereich"): + meta["bereich"] = info["bereich"] try: - mem().update(r["id"], data=r.get("memory", ""), - metadata={"category": cat, "source": body.source}) + mem().update(r["id"], data=r.get("memory", ""), metadata=meta) 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"), - "category": cats.get(r.get("id"), body.category)} + "category": (cats.get(r.get("id")) or {}).get("category", body.category), + "bereich": (cats.get(r.get("id")) or {}).get("bereich", "")} for r in results ]}