diff --git a/hermes/plugins/mc2-memory/__init__.py b/hermes/plugins/mc2-memory/__init__.py index e19c56e..9fba4fb 100644 --- a/hermes/plugins/mc2-memory/__init__.py +++ b/hermes/plugins/mc2-memory/__init__.py @@ -24,6 +24,7 @@ from __future__ import annotations import logging import os import queue +import re import threading import time from typing import Any, Dict, List, Optional @@ -37,8 +38,11 @@ log = logging.getLogger(__name__) MC_URL = os.environ.get("MC_URL", "http://127.0.0.1:9001").rstrip("/") MC_TOKEN = os.environ.get("MC_TOKEN", "") RECALL_LIMIT = int(os.environ.get("MC2_MEMORY_RECALL_LIMIT", "5")) -RECALL_MIN_SCORE = float(os.environ.get("MC2_MEMORY_RECALL_MIN_SCORE", "0.3")) -MIN_USER_LEN = int(os.environ.get("MC2_MEMORY_MIN_USER_LEN", "12")) # zu Kurzes nicht lernen +# 0.5 statt 0.3: nur starke Treffer einblenden → weniger Rausch-Fakten UND ein stabilerer +# Prompt-Prefix (oft leer), damit --cache-reuse über Turns greift. +RECALL_MIN_SCORE = float(os.environ.get("MC2_MEMORY_RECALL_MIN_SCORE", "0.5")) +# 25 statt 12: triviale Kurz-Turns (Begrüßung/Quittung) gar nicht erst zum Lernen schicken. +MIN_USER_LEN = int(os.environ.get("MC2_MEMORY_MIN_USER_LEN", "25")) # zu Kurzes nicht lernen # Anti-Memory-Poisoning (Stufe 0 Security): aus Turns mit untrusted Bildschirm-/Web-Inhalt NICHT # auto-lernen. Lucys Bildschirm-Sicht prependet diesen Marker an die User-Message (voice.py); auf dem # Schirm sichtbarer Text könnte Injection-Anweisungen tragen, die sonst DAUERHAFT ins Gedächtnis @@ -46,6 +50,20 @@ MIN_USER_LEN = int(os.environ.get("MC2_MEMORY_MIN_USER_LEN", "12")) # zu Kurzes SKIP_UNTRUSTED = os.environ.get("MC2_MEMORY_SKIP_UNTRUSTED", "1") not in ("0", "false", "False", "no") UNTRUSTED_MARKERS = ("[Bildschirm-Sicht",) +# Triviale Turns, die keinen dauerhaften Fakt tragen (reine Begrüßung/Quittung, Aufwärm-/Test- +# Kommandos) → gar nicht erst zum Auto-Lernen schicken. Spart Extraktions-Läufe und verhindert +# Müll-Fakten aus Meta-/Ablauf-Turns. +_TRIVIAL_TURN_RE = re.compile( + r"^\s*(?:(?:hi|hallo|hey|moin|servus|guten\s+(?:morgen|tag|abend)|danke\w*|" + r"ok(?:ay)?|alles\s+klar|passt|super|top|cool|nice|ja|nein|jo|jup|jap)[\s.,!?]*)+$" + r"|^\s*(?:aufw(?:ä|ae)rmen|/?test)\b", + re.IGNORECASE, +) + + +def _is_trivial_turn(text: str) -> bool: + return bool(_TRIVIAL_TURN_RE.search(text or "")) + def _headers() -> dict: return {"X-MC-Token": MC_TOKEN} if MC_TOKEN else {} @@ -124,6 +142,9 @@ class MC2MemoryProvider(MemoryProvider): u = (user_content or "").strip() if len(u) < MIN_USER_LEN: return + if _is_trivial_turn(u): + log.debug("mc2-memory: trivialer Turn (Begrüßung/Quittung/Aufwärmen) — Auto-Lernen übersprungen") + return if SKIP_UNTRUSTED and any(m in u for m in UNTRUSTED_MARKERS): log.debug("mc2-memory: Turn mit untrusted Bildschirm-/Web-Inhalt — Auto-Lernen übersprungen") return diff --git a/mem0_service/app.py b/mem0_service/app.py index c9cb1b9..75b65b5 100644 --- a/mem0_service/app.py +++ b/mem0_service/app.py @@ -20,6 +20,7 @@ Bind: 127.0.0.1 (nur lokal; MC2 proxyt nach außen). import logging import os +import re from contextlib import asynccontextmanager from fastapi import FastAPI, HTTPException @@ -95,12 +96,19 @@ def build_memory() -> Memory: "custom_instructions": ( "RELEVANZ (ZWINGEND, WICHTIGSTE REGEL): Extrahiere NUR dauerhaft nützliche Fakten ÜBER DEN " "NUTZER (Identität, Vorlieben, Arbeitsweise), seine Projekte/Entscheidungen/Stände, stabile " - "Fakten über Stack/Tools/Infrastruktur sowie verbindliche Regeln und Workflows. IGNORIERE " - "VOLLSTÄNDIG und gib KEINE Fakten zurück bei: Tagesnachrichten und Welt-/Politik-/Börsen-/" - "Promi-Ereignissen, Wetter, allgemeinem Weltwissen ohne Bezug zum Nutzer, vom Assistenten " - "erzeugten Zusammenfassungen oder Recherche-Ergebnissen, Smalltalk, sowie Meta-Aussagen über " - "den Gesprächsverlauf (z.B. 'der Nutzer fragt nach X', 'der Nutzer möchte Y prüfen', 'der " - "Assistent tat Z'). Speichere nur, was in Wochen noch nützlich ist; im Zweifel NICHT speichern. " + "Fakten über Stack/Tools/Infrastruktur sowie verbindliche Regeln und Workflows. " + "NIEMALS FAKTEN ÜBER DEN ASSISTENTEN SELBST (ZWINGEND): Extrahiere KEINE Aussagen über den " + "Assistenten/die KI (Hermes, Lucy), seine Fähigkeiten, seinen Zustand, seine Tools/MCP-Server, " + "seine Cronjobs, Commit-/Versionsstände oder welche Modelle er nutzt. Das Subjekt eines Fakts " + "ist IMMER der Nutzer oder sein Projekt — niemals 'der Assistent', 'Hermes' oder 'Lucy'. " + "KEINE ZEITGEBUNDENEN ZUSTÄNDE: Ignoriere transiente Momentaufnahmen (Fehler-/Job-Läufe, " + "Commit-Rückstand, Versions-Updates, 'gerade dabei'-/'hochgefahren'-/'aufgewärmt'-Status, " + "Test- und Onboarding-Fragen). IGNORIERE VOLLSTÄNDIG und gib KEINE Fakten zurück bei: " + "Tagesnachrichten und Welt-/Politik-/Börsen-/Promi-Ereignissen, Wetter, allgemeinem Weltwissen " + "ohne Bezug zum Nutzer, vom Assistenten erzeugten Zusammenfassungen oder Recherche-Ergebnissen, " + "Smalltalk, sowie Meta-Aussagen über den Gesprächsverlauf (z.B. 'der Nutzer fragt nach X', " + "'der Nutzer möchte Y prüfen', 'der Assistent tat Z'). Speichere nur, was in Wochen noch " + "nützlich ist; im Zweifel NICHT speichern. " "SPRACHE (ZWINGEND): Formuliere ALLE Fakten auf DEUTSCH — unabhängig von der " "Eingabesprache, niemals auf Englisch übersetzen. Eigennamen, Befehle, Pfade, Modell- und " "Produktnamen sowie etablierte Fachbegriffe (z.B. PowerShell-Push, Deploy, Qwen3.6, Neovim, " @@ -327,6 +335,30 @@ def classify_facts(facts: list[tuple[str, str]]) -> dict: return {} +# Deterministischer Guard: mem0-OSS extrahiert trotz custom_instructions hartnäckig Fakten ÜBER +# den Assistenten selbst (Hermes/Lucy) und transiente Zustände (Commit-Rückstand, Cronjob-Läufe, +# Onboarding-/Test-Fragen). Diese Muster werden nach der Extraktion verworfen (der Fakt wird wieder +# gelöscht) — reine Sicherungsschicht, unabhängig vom Extraktions-LLM. +_JUNK_FACT_PATTERNS = [ + r"^\s*(?:der|die|das)\s+assistent", # "Der Assistent ..." (Selbstbeschreibung) + r"\bassistent(?:in)?\s+(?:hermes|lucy)\b", # "... Assistent Hermes/Lucy ..." + r"\bhermes[- ]agent\b", + r"commits?\s+hinter", # Versions-Rückstand (transient) + r"\bcronjob\b", + r"beim\s+letzten\s+(?:lauf|mal)", + r"onboarding[-\s]?frage|\btest-?frage\b", + r"hat\s+den\s+assistenten\s+gebeten", + r"auf\s+betriebstemperatur|hochgefahren", + r"aktualisiert\s+sein\s+ged(?:ä|ae)chtnis", # Selbst-Meta über das Gedächtnis +] +_JUNK_FACT_RE = re.compile("|".join(_JUNK_FACT_PATTERNS), re.IGNORECASE) + + +def _is_junk_fact(text: str) -> bool: + """True, wenn der extrahierte Fakt Assistenten-Meta oder ein transienter Zustand ist.""" + return bool(_JUNK_FACT_RE.search(text or "")) + + @app.post("/learn") def learn(body: LearnIn) -> dict: """Auto-Lernen: Gesprächs-Turns/Text durchreichen → Mem0 EXTRAHIERT Fakten selbst @@ -342,6 +374,19 @@ def learn(body: LearnIn) -> dict: ) results = res.get("results", []) + # Guard: frisch extrahierte Assistenten-Meta/transiente Fakten sofort wieder verwerfen. + kept = [] + for r in results: + if r.get("event") in ("ADD", "UPDATE") and _is_junk_fact(r.get("memory", "")): + try: + mem().delete(r["id"]) + except Exception: + log.debug("Junk-Fakt-Löschung fehlgeschlagen für %s", r.get("id")) + log.info("mem0 guard: Junk-Fakt verworfen: %s", (r.get("memory") or "")[:80]) + continue + kept.append(r) + results = kept + cats: dict = {} if body.classify: to_classify = [(r["id"], r.get("memory", "")) for r in results