Memory-Guards + Voice-Sidecar-Install robuster

- mem0_service: Junk-Fact-Guard post-extraction (Regex + sofortiges Delete), custom_instructions praezisiert
- mc2-memory-Plugin: Recall-Schwelle 0.3->0.5, Trivial-Turn-Filter (Begruessung/Quittungen nicht lernen)
- voice_service/install.sh: Python-3.12-venv via uv, Piper-Binary + DE-Stimmen, Chatterbox best-effort

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-07-02 10:29:12 +02:00
parent 9967155332
commit 2db839aef9
3 changed files with 101 additions and 17 deletions
+23 -2
View File
@@ -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
View File
@@ -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
+27 -9
View File
@@ -7,30 +7,48 @@ set -euo pipefail
SRC="${MC2_SRC:-$HOME/mission-control-v2}" SRC="${MC2_SRC:-$HOME/mission-control-v2}"
VENV="$HOME/.voice/venv" VENV="$HOME/.voice/venv"
VOICES="$HOME/.voice/voices" VOICES="$HOME/.voice/voices"
PY="${VOICE_PYTHON:-python3.12}" UV="$(command -v uv || echo "$HOME/.local/bin/uv")"
command -v "$PY" >/dev/null 2>&1 || PY=python3 # Fallback, falls python3.12 nicht im PATH # torch/chatterbox brauchen Python ≤3.13 — das Default-python3 der Box ist 3.14. Darum das venv
# bevorzugt per uv mit gemanagtem Python 3.12 anlegen (wie das mem0-venv). pip läuft via `uv pip`.
pipi() {
if [ -x "$UV" ]; then "$UV" pip install --python "$VENV/bin/python" "$@"
else "$VENV/bin/python" -m pip install "$@"; fi
}
# --- venv ------------------------------------------------------------------- # --- venv (Python 3.12) -----------------------------------------------------
if [ ! -x "$VENV/bin/python" ]; then if [ ! -x "$VENV/bin/python" ]; then
echo "[voice] Erstelle venv ($PY) → $VENV" echo "[voice] Erstelle venv (Python 3.12) → $VENV"
"$PY" -m venv "$VENV" if [ -x "$UV" ]; then "$UV" venv --python 3.12 "$VENV"
elif command -v python3.12 >/dev/null 2>&1; then python3.12 -m venv "$VENV"
else echo "[voice] FATAL: weder uv noch python3.12 vorhanden."; exit 1; fi
fi fi
"$VENV/bin/python" -m pip install -q --upgrade pip echo "[voice] venv-Python: $("$VENV/bin/python" --version 2>&1)"
# --- Kern-Deps (STT + Piper + Server) — required ---------------------------- # --- Kern-Deps (STT + Piper + Server) — required ----------------------------
echo "[voice] Installiere Kern-Abhängigkeiten …" echo "[voice] Installiere Kern-Abhängigkeiten …"
"$VENV/bin/python" -m pip install -q -r "$SRC/voice_service/requirements.txt" pipi -q -r "$SRC/voice_service/requirements.txt"
# --- Chatterbox (Premium-TTS) — best-effort, CPU-torch (kein 2-GB-CUDA-Wheel) ---- # --- Chatterbox (Premium-TTS) — best-effort, CPU-torch (kein 2-GB-CUDA-Wheel) ----
echo "[voice] Installiere Chatterbox (Premium-TTS, CPU-torch) — best-effort …" echo "[voice] Installiere Chatterbox (Premium-TTS, CPU-torch) — best-effort …"
if "$VENV/bin/python" -m pip install -q --index-url https://download.pytorch.org/whl/cpu torch torchaudio \ if pipi -q --index-url https://download.pytorch.org/whl/cpu torch torchaudio \
&& "$VENV/bin/python" -m pip install -q chatterbox-tts; then && pipi -q chatterbox-tts; then
echo "[voice] Chatterbox bereit." echo "[voice] Chatterbox bereit."
else else
echo "[voice] WARN: Chatterbox-Install fehlgeschlagen — Piper bleibt als Default-Stimme aktiv." echo "[voice] WARN: Chatterbox-Install fehlgeschlagen — Piper bleibt als Default-Stimme aktiv."
fi fi
# --- Piper-Binary (offiziell, bringt espeak-ng mit) → ~/.voice/piper -----------
if [ ! -x "$HOME/.voice/piper/piper" ]; then
echo "[voice] Lade Piper-Binary …"
curl -fsSL "https://github.com/rhasspy/piper/releases/download/2023.11.14-2/piper_linux_x86_64.tar.gz" \
-o /tmp/piper.tgz \
&& tar xzf /tmp/piper.tgz -C "$HOME/.voice" \
&& rm -f /tmp/piper.tgz \
&& echo "[voice] Piper-Binary: $("$HOME/.voice/piper/piper" --version 2>&1 | head -1 || echo installiert)" \
|| echo "[voice] WARN: Piper-Binary-Download fehlgeschlagen."
fi
# --- Piper-Stimmen (Deutsch) nach ~/.voice/voices --------------------------- # --- Piper-Stimmen (Deutsch) nach ~/.voice/voices ---------------------------
mkdir -p "$VOICES" mkdir -p "$VOICES"
BASE="https://huggingface.co/rhasspy/piper-voices/resolve/main/de/de_DE" BASE="https://huggingface.co/rhasspy/piper-voices/resolve/main/de/de_DE"