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 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
+51 -6
View File
@@ -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
+27 -9
View File
@@ -7,30 +7,48 @@ set -euo pipefail
SRC="${MC2_SRC:-$HOME/mission-control-v2}"
VENV="$HOME/.voice/venv"
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
echo "[voice] Erstelle venv ($PY) → $VENV"
"$PY" -m venv "$VENV"
echo "[voice] Erstelle venv (Python 3.12) → $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
"$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 ----------------------------
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) ----
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 \
&& "$VENV/bin/python" -m pip install -q chatterbox-tts; then
if pipi -q --index-url https://download.pytorch.org/whl/cpu torch torchaudio \
&& pipi -q chatterbox-tts; then
echo "[voice] Chatterbox bereit."
else
echo "[voice] WARN: Chatterbox-Install fehlgeschlagen — Piper bleibt als Default-Stimme aktiv."
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 ---------------------------
mkdir -p "$VOICES"
BASE="https://huggingface.co/rhasspy/piper-voices/resolve/main/de/de_DE"