Feat: Gedaechtnis-Graph-Ansicht (Reagraph) + /api/memory/graph
Obsidian-artige Visualisierung des Gedaechtnisses: Knoten = Fakten, Kanten = semantische Aehnlichkeit (Kosinus der gespeicherten Embeddings, kNN je Knoten), Farbe = Kategorie, Groesse = Vernetzung. Klick auf Knoten -> Detailpanel mit verwandten Fakten + vergessen. - mem0_service/app.py: /graph rechnet Aehnlichkeitskanten aus den Chroma-Embeddings. - backend: services.memory.graph() + /api/memory/graph (Passthrough). - frontend: GraphView (reagraph, WebGL), Graph/Liste-Umschalter in MemoryView, GraphErrorBoundary, lazy-load (three.js nur bei Bedarf -> Hauptbundle bleibt schlank). reagraph auf 4.22.0 gepinnt (4.23+ braucht @react-three/fiber v9 = React 19; Projekt ist React 18). Live gegen die Box verifiziert (Graph rendert, Kategorien-Farben, Kanten, dunkel). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -223,6 +223,60 @@ def delete_memory(mid: str) -> dict:
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@app.get("/graph")
|
||||
def graph(min_score: float = 0.45, top_k: int = 3) -> dict:
|
||||
"""Fakten als Graph: Knoten = Fakten, Kanten = semantische Ähnlichkeit (Kosinus
|
||||
der gespeicherten Embeddings, je Knoten die top_k Nachbarn ≥ min_score). Für die
|
||||
Obsidian-artige Gedächtnis-Visualisierung im UI."""
|
||||
items = mem().get_all(filters={"user_id": USER_ID}, top_k=2000).get("results", [])
|
||||
nodes = [_to_item(r) for r in items]
|
||||
node_ids = [n["id"] for n in nodes]
|
||||
idx = {nid: i for i, nid in enumerate(node_ids)}
|
||||
|
||||
# Embeddings direkt aus der Chroma-Collection ziehen (kein Re-Embedding).
|
||||
col = mem().vector_store.collection
|
||||
raw = col.get(include=["embeddings"])
|
||||
raw_ids = raw.get("ids") or []
|
||||
raw_embs = raw.get("embeddings")
|
||||
edges: list[dict] = []
|
||||
vecs = [None] * len(node_ids)
|
||||
have = 0
|
||||
if raw_embs is not None:
|
||||
for rid, emb in zip(raw_ids, raw_embs):
|
||||
if rid in idx and emb is not None:
|
||||
vecs[idx[rid]] = emb
|
||||
have += 1
|
||||
|
||||
if have >= 2:
|
||||
import numpy as np
|
||||
present = [i for i, v in enumerate(vecs) if v is not None]
|
||||
M = np.array([vecs[i] for i in present], dtype=float)
|
||||
norms = np.linalg.norm(M, axis=1, keepdims=True)
|
||||
norms[norms == 0] = 1.0
|
||||
Mn = M / norms
|
||||
sim = Mn @ Mn.T
|
||||
seen: set[tuple[int, int]] = set()
|
||||
for a in range(len(present)):
|
||||
order = np.argsort(-sim[a])
|
||||
cnt = 0
|
||||
for b in order:
|
||||
if b == a:
|
||||
continue
|
||||
s = float(sim[a][b])
|
||||
if s < min_score:
|
||||
break
|
||||
i, j = present[a], present[b]
|
||||
key = (min(i, j), max(i, j))
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
edges.append({"source": node_ids[i], "target": node_ids[j],
|
||||
"weight": round(s, 3)})
|
||||
cnt += 1
|
||||
if cnt >= top_k:
|
||||
break
|
||||
return {"nodes": nodes, "edges": edges}
|
||||
|
||||
|
||||
@app.post("/learn")
|
||||
def learn(body: LearnIn) -> dict:
|
||||
"""Auto-Lernen: Gesprächs-Turns/Text durchreichen → Mem0 EXTRAHIERT Fakten selbst
|
||||
|
||||
Reference in New Issue
Block a user