Gedächtnis: nur Relevantes lernen/erinnern (Anti-Selbstbezug + Cache-Stabilität)
Der auto-lernende Mem0-Store sammelte trotz Relevanz-Direktive viel Müll: Fakten ÜBER den Assistenten selbst (Hermes/Lucy), transiente Zustände (Commit-Rückstand, Cronjob-Läufe, Onboarding-/Test-Fragen) und Gesprächs-Meta. Live-Store war zu ~40% solcher Ballast; der niedrige Recall-Schwellwert (0.3) blendete zudem marginale Fakten pro Turn ein und brach damit --cache-reuse. - mem0_service/app.py: custom_instructions verschärft (NIEMALS Fakten über den Assistenten selbst / keine zeitgebundenen Zustände) + deterministischer Post-Extraktions-Guard (_is_junk_fact) löscht Assistenten-Meta/transiente Fakten, die mem0-OSS trotz Direktive extrahiert — unabhängig vom LLM. - hermes/plugins/mc2-memory/__init__.py: RECALL_MIN_SCORE 0.3->0.5 (nur starke Treffer, oft leer -> stabilerer Prompt-Prefix -> --cache-reuse greift), MIN_USER_LEN 12->25 + Trivial-Turn-Skip (Begrüßung/Quittung/Aufwärmen). Alles env-überschreibbar und reversibel. Backend/Frontend unberührt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,7 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import queue
|
import queue
|
||||||
|
import re
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from typing import Any, Dict, List, Optional
|
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_URL = os.environ.get("MC_URL", "http://127.0.0.1:9001").rstrip("/")
|
||||||
MC_TOKEN = os.environ.get("MC_TOKEN", "")
|
MC_TOKEN = os.environ.get("MC_TOKEN", "")
|
||||||
RECALL_LIMIT = int(os.environ.get("MC2_MEMORY_RECALL_LIMIT", "5"))
|
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"))
|
# 0.5 statt 0.3: nur starke Treffer einblenden → weniger Rausch-Fakten UND ein stabilerer
|
||||||
MIN_USER_LEN = int(os.environ.get("MC2_MEMORY_MIN_USER_LEN", "12")) # zu Kurzes nicht lernen
|
# 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
|
# 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
|
# 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
|
# 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")
|
SKIP_UNTRUSTED = os.environ.get("MC2_MEMORY_SKIP_UNTRUSTED", "1") not in ("0", "false", "False", "no")
|
||||||
UNTRUSTED_MARKERS = ("[Bildschirm-Sicht",)
|
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:
|
def _headers() -> dict:
|
||||||
return {"X-MC-Token": MC_TOKEN} if MC_TOKEN else {}
|
return {"X-MC-Token": MC_TOKEN} if MC_TOKEN else {}
|
||||||
@@ -124,6 +142,9 @@ class MC2MemoryProvider(MemoryProvider):
|
|||||||
u = (user_content or "").strip()
|
u = (user_content or "").strip()
|
||||||
if len(u) < MIN_USER_LEN:
|
if len(u) < MIN_USER_LEN:
|
||||||
return
|
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):
|
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")
|
log.debug("mc2-memory: Turn mit untrusted Bildschirm-/Web-Inhalt — Auto-Lernen übersprungen")
|
||||||
return
|
return
|
||||||
|
|||||||
+51
-6
@@ -20,6 +20,7 @@ Bind: 127.0.0.1 (nur lokal; MC2 proxyt nach außen).
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
from fastapi import FastAPI, HTTPException
|
from fastapi import FastAPI, HTTPException
|
||||||
@@ -95,12 +96,19 @@ def build_memory() -> Memory:
|
|||||||
"custom_instructions": (
|
"custom_instructions": (
|
||||||
"RELEVANZ (ZWINGEND, WICHTIGSTE REGEL): Extrahiere NUR dauerhaft nützliche Fakten ÜBER DEN "
|
"RELEVANZ (ZWINGEND, WICHTIGSTE REGEL): Extrahiere NUR dauerhaft nützliche Fakten ÜBER DEN "
|
||||||
"NUTZER (Identität, Vorlieben, Arbeitsweise), seine Projekte/Entscheidungen/Stände, stabile "
|
"NUTZER (Identität, Vorlieben, Arbeitsweise), seine Projekte/Entscheidungen/Stände, stabile "
|
||||||
"Fakten über Stack/Tools/Infrastruktur sowie verbindliche Regeln und Workflows. IGNORIERE "
|
"Fakten über Stack/Tools/Infrastruktur sowie verbindliche Regeln und Workflows. "
|
||||||
"VOLLSTÄNDIG und gib KEINE Fakten zurück bei: Tagesnachrichten und Welt-/Politik-/Börsen-/"
|
"NIEMALS FAKTEN ÜBER DEN ASSISTENTEN SELBST (ZWINGEND): Extrahiere KEINE Aussagen über den "
|
||||||
"Promi-Ereignissen, Wetter, allgemeinem Weltwissen ohne Bezug zum Nutzer, vom Assistenten "
|
"Assistenten/die KI (Hermes, Lucy), seine Fähigkeiten, seinen Zustand, seine Tools/MCP-Server, "
|
||||||
"erzeugten Zusammenfassungen oder Recherche-Ergebnissen, Smalltalk, sowie Meta-Aussagen über "
|
"seine Cronjobs, Commit-/Versionsstände oder welche Modelle er nutzt. Das Subjekt eines Fakts "
|
||||||
"den Gesprächsverlauf (z.B. 'der Nutzer fragt nach X', 'der Nutzer möchte Y prüfen', 'der "
|
"ist IMMER der Nutzer oder sein Projekt — niemals 'der Assistent', 'Hermes' oder 'Lucy'. "
|
||||||
"Assistent tat Z'). Speichere nur, was in Wochen noch nützlich ist; im Zweifel NICHT speichern. "
|
"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 "
|
"SPRACHE (ZWINGEND): Formuliere ALLE Fakten auf DEUTSCH — unabhängig von der "
|
||||||
"Eingabesprache, niemals auf Englisch übersetzen. Eigennamen, Befehle, Pfade, Modell- und "
|
"Eingabesprache, niemals auf Englisch übersetzen. Eigennamen, Befehle, Pfade, Modell- und "
|
||||||
"Produktnamen sowie etablierte Fachbegriffe (z.B. PowerShell-Push, Deploy, Qwen3.6, Neovim, "
|
"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 {}
|
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")
|
@app.post("/learn")
|
||||||
def learn(body: LearnIn) -> dict:
|
def learn(body: LearnIn) -> dict:
|
||||||
"""Auto-Lernen: Gesprächs-Turns/Text durchreichen → Mem0 EXTRAHIERT Fakten selbst
|
"""Auto-Lernen: Gesprächs-Turns/Text durchreichen → Mem0 EXTRAHIERT Fakten selbst
|
||||||
@@ -342,6 +374,19 @@ def learn(body: LearnIn) -> dict:
|
|||||||
)
|
)
|
||||||
results = res.get("results", [])
|
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 = {}
|
cats: dict = {}
|
||||||
if body.classify:
|
if body.classify:
|
||||||
to_classify = [(r["id"], r.get("memory", "")) for r in results
|
to_classify = [(r["id"], r.get("memory", "")) for r in results
|
||||||
|
|||||||
Reference in New Issue
Block a user