feat(brain): memory_search + memory_update Tools — ARIA findet Updates aktiv
Bug-Report von Stefan: er hat im Diagnostic den Baujahr-Memory von
1972 auf 1974 geaendert, ARIA wusste das nicht und beharrte auf 1972
(weil ihr letzter Conversation-Turn noch '1972' enthielt). Sie konnte
auch nicht nachpruefen, sagte selbst: "Qdrant kann ich nicht aktiv
durchsuchen".
Fix: zwei neue Meta-Tools im agent.py.
memory_search(query, mode='text'|'semantic', k=5):
- Volltext oder semantic via store.search_text / store.search
- Liefert Liste mit Titel, ID, Content, Anhaengen
- Tool-Description sagt explizit: "Memory ist Truth ueber dem
Conversation-Window" — wenn beide unterschiedlich sind, gilt
Memory. Plus Anker-Anwendungsfaelle: 'schau in deinem Gedaechtnis',
'ich hab das aktualisiert', 'pruef ob's schon was zum Thema gibt'
memory_update(id, title?, content?, category?, tags?, pinned?):
- Patch existierender Memory per ID (aus memory_search oder Cold-Memory)
- Content-Change triggert Re-Embedding fuer Search, sonst nur
Payload-Update
- Pushed memory_saved-Event analog zu memory_save (App/Diagnostic
refreshen)
- Tool-Description empfiehlt explizit Update statt neuem Save bei
Korrekturen/Ergaenzungen — vermeidet Fragmentierung
Damit kann Stefan jetzt sagen "schau in deinem Gedaechtnis" und ARIA
findet den aktualisierten Eintrag. Plus bei spaeteren Korrekturen
("ach nee, 1974") nutzt ARIA memory_update statt memory_save +
hinterlaesst einen sauberen Eintrag.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -206,6 +206,64 @@ META_TOOLS = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "memory_search",
|
||||||
|
"description": (
|
||||||
|
"Durchsuche aktiv dein Gedaechtnis (Qdrant-DB). Nutze das wenn:\n"
|
||||||
|
"- der User sagt 'schau in deinem Gedaechtnis' / 'ich hab das Memory aktualisiert'\n"
|
||||||
|
"- du dir bei einer Info aus dem Konversations-Verlauf unsicher bist "
|
||||||
|
"(z.B. ob das noch der aktuelle Stand ist)\n"
|
||||||
|
"- du pruefen willst ob's schon einen Memory zu einem Thema gibt bevor "
|
||||||
|
"du via memory_save einen neuen anlegst (vermeidet Fragmentierung)\n\n"
|
||||||
|
"**WICHTIG: Memory ist Truth ueber dem Conversation-Window.** "
|
||||||
|
"Wenn dort was anders steht als in deinem Gespraechs-Verlauf, gilt das "
|
||||||
|
"was im Memory steht — der User koennte gerade was korrigiert haben.\n\n"
|
||||||
|
"Mode 'text' = Substring (case-insensitive), gut fuer exakte Begriffe "
|
||||||
|
"wie 'cessna'. Mode 'semantic' = Embedder-Search, gut fuer 'wann hatten "
|
||||||
|
"wir ueber X gesprochen'-Fragen."
|
||||||
|
),
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"query": {"type": "string", "description": "Such-Begriff"},
|
||||||
|
"mode": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["text", "semantic"],
|
||||||
|
"description": "Default 'text' (Substring). 'semantic' fuer aehnlichkeits-Suche.",
|
||||||
|
},
|
||||||
|
"k": {"type": "integer", "description": "Wieviele Treffer (Default 5, max 20)"},
|
||||||
|
},
|
||||||
|
"required": ["query"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "memory_update",
|
||||||
|
"description": (
|
||||||
|
"Aktualisiere einen existierenden Memory-Eintrag — gibt die ID aus "
|
||||||
|
"memory_search oder dem Cold-Memory an. Nur die uebergebenen Felder werden "
|
||||||
|
"ueberschrieben, der Rest bleibt unangetastet. **Bevorzuge das ueber "
|
||||||
|
"memory_save** wenn der User eine Korrektur macht oder du zusaetzliche "
|
||||||
|
"Details zum gleichen Thema hast — vermeidet doppelte Eintraege."
|
||||||
|
),
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {"type": "string", "description": "Memory-ID (UUID, aus memory_search oder Cold-Memory)"},
|
||||||
|
"title": {"type": "string", "description": "Neuer Titel (optional)"},
|
||||||
|
"content": {"type": "string", "description": "Neuer Content — wird neu embedded fuer Search (optional)"},
|
||||||
|
"category": {"type": "string", "description": "Neue Kategorie (optional)"},
|
||||||
|
"tags": {"type": "array", "items": {"type": "string"}, "description": "Neue Tags (ueberschreibt komplett)"},
|
||||||
|
"pinned": {"type": "boolean", "description": "Pinning aendern (optional)"},
|
||||||
|
},
|
||||||
|
"required": ["id"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "function",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
@@ -540,6 +598,91 @@ class Agent:
|
|||||||
else:
|
else:
|
||||||
lines.append(f"- {t['name']} ({t['type']}, {state})")
|
lines.append(f"- {t['name']} ({t['type']}, {state})")
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
if name == "memory_search":
|
||||||
|
query = (arguments.get("query") or "").strip()
|
||||||
|
if not query:
|
||||||
|
return "FEHLER: query ist Pflicht."
|
||||||
|
mode = arguments.get("mode") or "text"
|
||||||
|
try:
|
||||||
|
k = int(arguments.get("k", 5))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
k = 5
|
||||||
|
k = max(1, min(k, 20))
|
||||||
|
try:
|
||||||
|
if mode == "semantic":
|
||||||
|
qvec = self.embedder.embed(query)
|
||||||
|
results = self.store.search(
|
||||||
|
qvec, k=k, exclude_pinned=False, score_threshold=0.30,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
results = self.store.search_text(query, k=k, exclude_pinned=False)
|
||||||
|
if not results:
|
||||||
|
return f"Keine Treffer fuer '{query}' (mode={mode})."
|
||||||
|
lines = [f"{len(results)} Treffer fuer '{query}' (mode={mode}):"]
|
||||||
|
for m in results:
|
||||||
|
score_part = f" [score={m.score:.2f}]" if m.score is not None else ""
|
||||||
|
pin = "📌 " if m.pinned else ""
|
||||||
|
atts = m.attachments or []
|
||||||
|
att_part = f" 📎{len(atts)}" if atts else ""
|
||||||
|
lines.append("")
|
||||||
|
lines.append(f"## {pin}{m.title} ({m.type}){score_part}{att_part}")
|
||||||
|
lines.append(f"id: {m.id}")
|
||||||
|
lines.append(m.content or "")
|
||||||
|
if atts:
|
||||||
|
for a in atts:
|
||||||
|
lines.append(f" 📎 {a.get('name', '?')} ({a.get('mime', '')}) — {a.get('path', '')}")
|
||||||
|
return "\n".join(lines)
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("memory_search fehlgeschlagen")
|
||||||
|
return f"FEHLER: {e}"
|
||||||
|
if name == "memory_update":
|
||||||
|
pid = (arguments.get("id") or "").strip()
|
||||||
|
if not pid:
|
||||||
|
return "FEHLER: id ist Pflicht."
|
||||||
|
existing = self.store.get(pid)
|
||||||
|
if not existing:
|
||||||
|
return f"FEHLER: Memory mit id={pid[:8]} nicht gefunden."
|
||||||
|
try:
|
||||||
|
from memory.vector_store import COLLECTION
|
||||||
|
import datetime as _dt
|
||||||
|
content_changed = False
|
||||||
|
if "title" in arguments and arguments["title"] is not None:
|
||||||
|
existing.title = str(arguments["title"]).strip()
|
||||||
|
if "content" in arguments and arguments["content"] is not None:
|
||||||
|
new_content = str(arguments["content"]).strip()
|
||||||
|
if new_content != existing.content:
|
||||||
|
content_changed = True
|
||||||
|
existing.content = new_content
|
||||||
|
if "category" in arguments and arguments["category"] is not None:
|
||||||
|
existing.category = str(arguments["category"]).strip()
|
||||||
|
if "tags" in arguments and arguments["tags"] is not None:
|
||||||
|
existing.tags = [str(t).strip() for t in (arguments["tags"] or []) if str(t).strip()]
|
||||||
|
if "pinned" in arguments and arguments["pinned"] is not None:
|
||||||
|
existing.pinned = bool(arguments["pinned"])
|
||||||
|
existing.updated_at = _dt.datetime.now(_dt.timezone.utc).isoformat()
|
||||||
|
if content_changed:
|
||||||
|
vec = self.embedder.embed(existing.content)
|
||||||
|
self.store.upsert(existing, vec)
|
||||||
|
else:
|
||||||
|
self.store.client.set_payload(
|
||||||
|
collection_name=COLLECTION,
|
||||||
|
payload=existing.to_payload() | {"updated_at": existing.updated_at},
|
||||||
|
points=[pid],
|
||||||
|
)
|
||||||
|
saved = self.store.get(pid)
|
||||||
|
self._pending_events.append({
|
||||||
|
"type": "memory_saved",
|
||||||
|
"memory": {
|
||||||
|
"id": saved.id, "type": saved.type, "title": saved.title,
|
||||||
|
"content_preview": (saved.content or "")[:140],
|
||||||
|
"category": saved.category, "pinned": saved.pinned,
|
||||||
|
"attachments": saved.attachments or [],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return f"OK — Memory '{saved.title}' aktualisiert (id={pid[:8]})."
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("memory_update fehlgeschlagen")
|
||||||
|
return f"FEHLER: {e}"
|
||||||
if name == "memory_save":
|
if name == "memory_save":
|
||||||
title = (arguments.get("title") or "").strip()
|
title = (arguments.get("title") or "").strip()
|
||||||
content = (arguments.get("content") or "").strip()
|
content = (arguments.get("content") or "").strip()
|
||||||
|
|||||||
Reference in New Issue
Block a user