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:
@@ -10,7 +10,7 @@ router = APIRouter(prefix="/api")
|
|||||||
|
|
||||||
class MemIn(BaseModel):
|
class MemIn(BaseModel):
|
||||||
content: str
|
content: str
|
||||||
category: str = "stable"
|
category: str = "knowledge"
|
||||||
source: str = "manual"
|
source: str = "manual"
|
||||||
|
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ class LearnIn(BaseModel):
|
|||||||
text: str | None = None
|
text: str | None = None
|
||||||
messages: list[dict] | None = None
|
messages: list[dict] | None = None
|
||||||
source: str = "auto"
|
source: str = "auto"
|
||||||
category: str = "stable"
|
category: str = "knowledge"
|
||||||
|
|
||||||
|
|
||||||
@router.get("/memory/export")
|
@router.get("/memory/export")
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import httpx
|
|||||||
|
|
||||||
from config import MEM0_SERVICE_URL
|
from config import MEM0_SERVICE_URL
|
||||||
|
|
||||||
CATEGORIES = ("user", "instruction", "stable", "versioned", "ephemeral")
|
CATEGORIES = ("identity", "knowledge", "rules", "events")
|
||||||
|
|
||||||
_TIMEOUT = httpx.Timeout(60.0, connect=5.0) # LLM-Extraktion kann ein paar Sekunden dauern
|
_TIMEOUT = httpx.Timeout(60.0, connect=5.0) # LLM-Extraktion kann ein paar Sekunden dauern
|
||||||
|
|
||||||
|
|||||||
+2
-2
File diff suppressed because one or more lines are too long
+41
-46
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -7,8 +7,8 @@
|
|||||||
<link rel="manifest" href="/manifest.webmanifest" />
|
<link rel="manifest" href="/manifest.webmanifest" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<title>Mission Control 2.0</title>
|
<title>Mission Control 2.0</title>
|
||||||
<script type="module" crossorigin src="/assets/index-BVXzL-Al.js"></script>
|
<script type="module" crossorigin src="/assets/index-BbiMdr0-.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-DGdkjhdp.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-De4PPCGb.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import { Trash2, Sparkles, Share2 } from "lucide-react"
|
|||||||
import { type MemoryGraph } from "@/lib/api"
|
import { type MemoryGraph } from "@/lib/api"
|
||||||
|
|
||||||
const CAT_COLOR: Record<string, string> = {
|
const CAT_COLOR: Record<string, string> = {
|
||||||
user: "#22d3ee", instruction: "#a78bfa", stable: "#6366f1", versioned: "#fbbf24", ephemeral: "#f472b6",
|
identity: "#22d3ee", knowledge: "#6366f1", rules: "#a78bfa", events: "#fbbf24",
|
||||||
}
|
}
|
||||||
const CAT_LABEL: Record<string, string> = {
|
const CAT_LABEL: Record<string, string> = {
|
||||||
user: "User", instruction: "Regel", stable: "Fakt", versioned: "Version", ephemeral: "Temporär",
|
identity: "Identität", knowledge: "Wissen", rules: "Regeln", events: "Ereignisse",
|
||||||
}
|
}
|
||||||
const AUTO = new Set(["auto", "agent", "hermes"])
|
const AUTO = new Set(["auto", "agent", "hermes"])
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useMemo, lazy, Suspense } from "react"
|
import { useState, useMemo, lazy, Suspense } from "react"
|
||||||
import {
|
import {
|
||||||
Trash2, Sparkles, User, Scroll, Shield, Tag, Clock, Search, Plus, BookOpen,
|
Trash2, Sparkles, User, Scroll, Shield, Clock, Search, Plus, BookOpen,
|
||||||
Share2, List, MessagesSquare, X, Copy, Check, Terminal as TerminalIcon, Send,
|
Share2, List, MessagesSquare, X, Copy, Check, Terminal as TerminalIcon, Send,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import { api, type DedupeResult } from "@/lib/api"
|
import { api, type DedupeResult } from "@/lib/api"
|
||||||
@@ -12,23 +12,22 @@ import { GraphErrorBoundary } from "./GraphErrorBoundary"
|
|||||||
// reagraph + three.js sind schwer → erst laden, wenn der Graph-Tab geöffnet wird.
|
// reagraph + three.js sind schwer → erst laden, wenn der Graph-Tab geöffnet wird.
|
||||||
const GraphView = lazy(() => import("./GraphView").then((m) => ({ default: m.GraphView })))
|
const GraphView = lazy(() => import("./GraphView").then((m) => ({ default: m.GraphView })))
|
||||||
|
|
||||||
const CATEGORIES = ["user", "instruction", "stable", "versioned", "ephemeral"]
|
const CATEGORIES = ["identity", "knowledge", "rules", "events"]
|
||||||
|
|
||||||
// Herkunft: automatisch gelernt (Mem0-Extraktion / Agent / Hermes) vs. manuell/Vorlage.
|
// Herkunft: automatisch gelernt (Mem0-Extraktion / Agent / Hermes) vs. manuell/Vorlage.
|
||||||
const AUTO_SOURCES = new Set(["auto", "agent", "hermes"])
|
const AUTO_SOURCES = new Set(["auto", "agent", "hermes"])
|
||||||
|
|
||||||
const CAT_CONFIG: Record<string, { label: string; icon: any; bg: string; text: string }> = {
|
const CAT_CONFIG: Record<string, { label: string; icon: any; bg: string; text: string }> = {
|
||||||
user: { label: "User", icon: User, bg: "bg-cyan-500/10", text: "text-cyan-400" },
|
identity: { label: "Identität", icon: User, bg: "bg-cyan-500/10", text: "text-cyan-400" },
|
||||||
instruction: { label: "Regel", icon: Scroll, bg: "bg-violet-500/10", text: "text-violet-400" },
|
knowledge: { label: "Wissen", icon: Shield, bg: "bg-indigo-500/10", text: "text-indigo-400" },
|
||||||
stable: { label: "Fakt", icon: Shield, bg: "bg-indigo-500/10", text: "text-indigo-400" },
|
rules: { label: "Regeln", icon: Scroll, bg: "bg-violet-500/10", text: "text-violet-400" },
|
||||||
versioned: { label: "Version", icon: Tag, bg: "bg-amber-500/10", text: "text-amber-400" },
|
events: { label: "Ereignisse", icon: Clock, bg: "bg-amber-500/10", text: "text-amber-400" },
|
||||||
ephemeral: { label: "Temporär", icon: Clock, bg: "bg-pink-500/10", text: "text-pink-400" },
|
|
||||||
}
|
}
|
||||||
const DEFAULT_CAT = { label: "Gedächtnis", icon: BookOpen, bg: "bg-muted/10", text: "text-muted-foreground" }
|
const DEFAULT_CAT = { label: "Gedächtnis", icon: BookOpen, bg: "bg-muted/10", text: "text-muted-foreground" }
|
||||||
|
|
||||||
const BORDER_CLASSES: Record<string, string> = {
|
const BORDER_CLASSES: Record<string, string> = {
|
||||||
user: "border-l-cyan-500/80", instruction: "border-l-violet-500/80", stable: "border-l-indigo-500/80",
|
identity: "border-l-cyan-500/80", knowledge: "border-l-indigo-500/80",
|
||||||
versioned: "border-l-amber-500/80", ephemeral: "border-l-pink-500/80",
|
rules: "border-l-violet-500/80", events: "border-l-amber-500/80",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Onboarding-Prompt: der Nutzer startet damit ein Gespräch mit Hermes (Terminal/Telegram).
|
// Onboarding-Prompt: der Nutzer startet damit ein Gespräch mit Hermes (Terminal/Telegram).
|
||||||
@@ -46,7 +45,7 @@ export function MemoryView() {
|
|||||||
const [filter, setFilter] = useState("")
|
const [filter, setFilter] = useState("")
|
||||||
const [q, setQ] = useState("")
|
const [q, setQ] = useState("")
|
||||||
const [content, setContent] = useState("")
|
const [content, setContent] = useState("")
|
||||||
const [category, setCategory] = useState("stable")
|
const [category, setCategory] = useState("knowledge")
|
||||||
const [deduping, setDeduping] = useState(false)
|
const [deduping, setDeduping] = useState(false)
|
||||||
const [copied, setCopied] = useState(false)
|
const [copied, setCopied] = useState(false)
|
||||||
const [view, setView] = useState<"liste" | "graph">("graph")
|
const [view, setView] = useState<"liste" | "graph">("graph")
|
||||||
|
|||||||
+4
-4
@@ -55,11 +55,11 @@ def _delete(path: str):
|
|||||||
def get_memories(category: str = "") -> str:
|
def get_memories(category: str = "") -> str:
|
||||||
"""Lädt gespeicherte Fakten/Entscheidungen aus dem geteilten Gedächtnis.
|
"""Lädt gespeicherte Fakten/Entscheidungen aus dem geteilten Gedächtnis.
|
||||||
Nutze dies EINMAL am Session-Beginn, wenn du Projekt-Kontext brauchst — nicht wiederholt.
|
Nutze dies EINMAL am Session-Beginn, wenn du Projekt-Kontext brauchst — nicht wiederholt.
|
||||||
category: user | instruction | stable | versioned | ephemeral | (leer = alle)"""
|
category: identity | knowledge | rules | events | (leer = alle)"""
|
||||||
items = _get("/api/memory", **({"category": category} if category else {}))
|
items = _get("/api/memory", **({"category": category} if category else {}))
|
||||||
if not items:
|
if not items:
|
||||||
return "Keine Memories gespeichert."
|
return "Keine Memories gespeichert."
|
||||||
icon = {"stable": "🔵", "versioned": "🟡", "ephemeral": "⏱", "user": "👤", "instruction": "📋"}
|
icon = {"identity": "👤", "knowledge": "🔵", "rules": "📋", "events": "🕒"}
|
||||||
return "\n".join(
|
return "\n".join(
|
||||||
f"{icon.get(m['category'], '·')} [{m['category']}] {m['content']} (ID: {m['id'][:8]})"
|
f"{icon.get(m['category'], '·')} [{m['category']}] {m['content']} (ID: {m['id'][:8]})"
|
||||||
for m in items
|
for m in items
|
||||||
@@ -81,12 +81,12 @@ def search_memories(q: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def add_memory(content: str, category: str = "stable", source: str = "agent") -> str:
|
def add_memory(content: str, category: str = "knowledge", source: str = "agent") -> str:
|
||||||
"""Speichert EINEN dauerhaften Fakt im geteilten Gedächtnis (alle Tools sehen ihn).
|
"""Speichert EINEN dauerhaften Fakt im geteilten Gedächtnis (alle Tools sehen ihn).
|
||||||
Nur aufrufen, WENN gerade etwas Dauerhaftes entstanden ist (Konvention, Architektur-
|
Nur aufrufen, WENN gerade etwas Dauerhaftes entstanden ist (Konvention, Architektur-
|
||||||
Entscheidung, Tech-Version, Nutzer-Präferenz) UND es noch nicht existiert (vorher
|
Entscheidung, Tech-Version, Nutzer-Präferenz) UND es noch nicht existiert (vorher
|
||||||
search_memories!). Knapp & atomar. Existiert ein passender Eintrag → update_memory.
|
search_memories!). Knapp & atomar. Existiert ein passender Eintrag → update_memory.
|
||||||
category: user | instruction | stable | versioned | ephemeral"""
|
category: identity (Nutzer/Vorlieben) | knowledge (Fakten/Stack) | rules (Regeln/Konventionen) | events (Ereignisse/Entscheidungen)"""
|
||||||
m = _post("/api/memory", {"content": content, "category": category, "source": source})
|
m = _post("/api/memory", {"content": content, "category": category, "source": source})
|
||||||
return f"Gespeichert (ID: {m['id'][:8]}): {content}"
|
return f"Gespeichert (ID: {m['id'][:8]}): {content}"
|
||||||
|
|
||||||
|
|||||||
+64
-8
@@ -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"))
|
LLM_MAX_TOKENS = int(os.environ.get("MEM0_LLM_MAX_TOKENS", "2048"))
|
||||||
SEARCH_TOP_K = int(os.environ.get("MEM0_SEARCH_TOP_K", "50"))
|
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 = fundierte Memory-Taxonomie (semantisch/prozedural/episodisch), bewusst knapp (4).
|
||||||
# Kategorien — wir führen sie als Metadaten mit.
|
# Mem0-OSS kann nicht nativ klassifizieren (Cloud-Feature) → wir lassen das Hirn beim Lernen
|
||||||
CATEGORIES = ("user", "instruction", "stable", "versioned", "ephemeral")
|
# 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):
|
class NoThinkLLM(OpenAILLM):
|
||||||
@@ -140,7 +150,7 @@ def _to_item(r: dict, score=None) -> dict:
|
|||||||
|
|
||||||
class MemIn(BaseModel):
|
class MemIn(BaseModel):
|
||||||
content: str
|
content: str
|
||||||
category: str = "stable"
|
category: str = DEFAULT_CATEGORY
|
||||||
source: str = "manual"
|
source: str = "manual"
|
||||||
|
|
||||||
|
|
||||||
@@ -153,7 +163,8 @@ class LearnIn(BaseModel):
|
|||||||
text: str | None = None
|
text: str | None = None
|
||||||
messages: list[dict] | None = None
|
messages: list[dict] | None = None
|
||||||
source: str = "auto"
|
source: str = "auto"
|
||||||
category: str = "stable"
|
category: str = DEFAULT_CATEGORY
|
||||||
|
classify: bool = True # Hirn ordnet die gelernten Fakten automatisch ein
|
||||||
|
|
||||||
|
|
||||||
@app.get("/health")
|
@app.get("/health")
|
||||||
@@ -277,10 +288,38 @@ def graph(min_score: float = 0.45, top_k: int = 3) -> dict:
|
|||||||
return {"nodes": nodes, "edges": edges}
|
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")
|
@app.post("/learn")
|
||||||
def learn(body: LearnIn) -> dict:
|
def learn(body: LearnIn) -> dict:
|
||||||
"""Auto-Lernen: Gesprächs-Turns/Text durchreichen → Mem0 EXTRAHIERT Fakten selbst
|
"""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
|
msgs = body.messages
|
||||||
if not msgs and body.text:
|
if not msgs and body.text:
|
||||||
msgs = [{"role": "user", "content": body.text}]
|
msgs = [{"role": "user", "content": body.text}]
|
||||||
@@ -290,9 +329,26 @@ def learn(body: LearnIn) -> dict:
|
|||||||
msgs, user_id=USER_ID, infer=True,
|
msgs, user_id=USER_ID, infer=True,
|
||||||
metadata={"category": body.category, "source": body.source},
|
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": [
|
return {"results": [
|
||||||
{"id": r.get("id"), "content": r.get("memory"), "event": r.get("event")}
|
{"id": r.get("id"), "content": r.get("memory"), "event": r.get("event"),
|
||||||
for r in res.get("results", [])
|
"category": cats.get(r.get("id"), body.category)}
|
||||||
|
for r in results
|
||||||
]}
|
]}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user