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",
|
||||
"function": {
|
||||
@@ -540,6 +598,91 @@ class Agent:
|
||||
else:
|
||||
lines.append(f"- {t['name']} ({t['type']}, {state})")
|
||||
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":
|
||||
title = (arguments.get("title") or "").strip()
|
||||
content = (arguments.get("content") or "").strip()
|
||||
|
||||
Reference in New Issue
Block a user