From 16ebaa652f1a52a34fcc998fd482c747aecb99f9 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Wed, 13 May 2026 03:23:08 +0200 Subject: [PATCH] =?UTF-8?q?feat(brain):=20memory=5Fsearch=20+=20memory=5Fu?= =?UTF-8?q?pdate=20Tools=20=E2=80=94=20ARIA=20findet=20Updates=20aktiv?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- aria-brain/agent.py | 143 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) diff --git a/aria-brain/agent.py b/aria-brain/agent.py index 3c076ce..08ca8eb 100644 --- a/aria-brain/agent.py +++ b/aria-brain/agent.py @@ -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()