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:
@@ -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
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user