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