fix(deploy): Mem0-Logik aus Backup/Restore/Postchecks/Venv-Audit/Hooks entfernt, mem0_service/ gelöscht

This commit is contained in:
Hitonabi
2026-08-23 21:27:32 +02:00
parent 84b4643f1a
commit 084dd73f71
12 changed files with 17 additions and 669 deletions
+1 -1
View File
@@ -28,7 +28,7 @@ hint=""
# Stabile Anti-Flail-Orientierung: WER bin ich, WO laufen die Modelle, WAS ist tabu. # Stabile Anti-Flail-Orientierung: WER bin ich, WO laufen die Modelle, WAS ist tabu.
ctx="$(cat <<EOF ctx="$(cat <<EOF
[BOX-ORIENTIERUNG — du bist ein Hintergrund-Worker auf der lokalen AI-Box (Linux, Zeit UTC / lokal Europe/Berlin, locale de_DE), NICHT Lucy und kein Dev-Laptop. Du arbeitest EINE Kanban-Aufgabe ab.] [BOX-ORIENTIERUNG — du bist ein Hintergrund-Worker auf der lokalen AI-Box (Linux, Zeit UTC / lokal Europe/Berlin, locale de_DE), NICHT Lucy und kein Dev-Laptop. Du arbeitest EINE Kanban-Aufgabe ab.]
Die Modelle / das "Hirn" laufen ausschliesslich ueber den Router llama-swap auf 127.0.0.1:8080 (startet je Modell einen llama-server) bzw. das OpenAI-kompatible Gateway von MC2 auf 127.0.0.1:9001/v1 — es gibt KEINEN anderen Modell-Server, also nicht danach suchen. Weitere Dienste: hermes-gateway :8642, mem0-Gedaechtnis :8765. Der MC2-Live-Checkout ~/mission-control-v2 ist nur zum LESEN da (Schreiben/committen TABU — Code-Arbeit immer im frischen Klon deines Task-Workspaces). Grenzen (was gehoert wohin): MC2 = Steuerpult (verwalten/klicken/zusehen, KEIN Chat-UI), Lucy = Chat & Stimme (eigenes Repo), Hermes-Quelle ist TABU (Agenten-Verhalten nur ueber Config/SOUL/Skill/Hook/MCP aendern). Feste Entscheide/Verbote + volle Grenzen-Landkarte: ~/mission-control-v2/docs/wissen/ (GRENZEN/VERDIKTE/FALLEN/STACK) + ~/wissens-vault/eigenbau-landkarte.md.${hint} Die Modelle / das "Hirn" laufen ausschliesslich ueber den Router llama-swap auf 127.0.0.1:8080 (startet je Modell einen llama-server) bzw. das OpenAI-kompatible Gateway von MC2 auf 127.0.0.1:9001/v1 — es gibt KEINEN anderen Modell-Server, also nicht danach suchen. Weitere Dienste: hermes-gateway :8642. Der MC2-Live-Checkout ~/mission-control-v2 ist nur zum LESEN da (Schreiben/committen TABU — Code-Arbeit immer im frischen Klon deines Task-Workspaces). Grenzen (was gehoert wohin): MC2 = Steuerpult (verwalten/klicken/zusehen, KEIN Chat-UI), Lucy = Chat & Stimme (eigenes Repo), Hermes-Quelle ist TABU (Agenten-Verhalten nur ueber Config/SOUL/Skill/Hook/MCP aendern). Feste Entscheide/Verbote + volle Grenzen-Landkarte: ~/mission-control-v2/docs/wissen/ (GRENZEN/VERDIKTE/FALLEN/STACK) + ~/wissens-vault/eigenbau-landkarte.md.${hint}
TRIAGE (19.07. — Drei-Bahnen-Regel): Ordne deine Karte ZUERST einer Bahn zu und bleib darin: Denken (Konzept/Plan), Bauen (Code schreiben/aendern), Betrieb (SSH/Deploy/Dienste). Fuer Betrieb-Arbeit lade IMMER zuerst den Skill betrieb-playbook (Diagnose-Checklisten; max 3 Anlaeufe pro Hindernis, dann kanban_block mit exaktem Fehlertext — die No-Progress-Bremse erzwingt das mechanisch). TRIAGE (19.07. — Drei-Bahnen-Regel): Ordne deine Karte ZUERST einer Bahn zu und bleib darin: Denken (Konzept/Plan), Bauen (Code schreiben/aendern), Betrieb (SSH/Deploy/Dienste). Fuer Betrieb-Arbeit lade IMMER zuerst den Skill betrieb-playbook (Diagnose-Checklisten; max 3 Anlaeufe pro Hindernis, dann kanban_block mit exaktem Fehlertext — die No-Progress-Bremse erzwingt das mechanisch).
PROTOKOLL-PFLICHT (15.07.): Beende deine Aufgabe IMMER mit dem Tool-Aufruf kanban_complete (fertig) oder kanban_block (Frage/Sackgasse). Eine Text-Zusammenfassung OHNE diesen Aufruf zaehlt als Absturz (Protokollverstoss) und blockiert die Karte — genau so ging am 14.07. eine fertige Arbeit verloren. PROTOKOLL-PFLICHT (15.07.): Beende deine Aufgabe IMMER mit dem Tool-Aufruf kanban_complete (fertig) oder kanban_block (Frage/Sackgasse). Eine Text-Zusammenfassung OHNE diesen Aufruf zaehlt als Absturz (Protokollverstoss) und blockiert die Karte — genau so ging am 14.07. eine fertige Arbeit verloren.
REPO-WEGWEISER mission-control-v2 (NICHT raten/suchen — so liegt es): backend/app.py = Einstieg + Router-Mounting · backend/routers/*.py = REST-Endpunkte · backend/services/*.py = Logik · backend/gateway_app.py = /v1-Gateway-Prozess (:9010) · frontend/src/views/*.tsx = Ansichten · frontend/src/lib/api.ts = Typen+Fetch · frontend/src/lib/queries.ts = TanStack-Query-Hooks (Polling) · frontend/src/nav.ts = Sidebar · deploy/ = Skripte/Units/Hooks/Skills · Projekt-Regeln: AGENTS.md (Wurzel) + docs/wissen/. REPO-WEGWEISER mission-control-v2 (NICHT raten/suchen — so liegt es): backend/app.py = Einstieg + Router-Mounting · backend/routers/*.py = REST-Endpunkte · backend/services/*.py = Logik · backend/gateway_app.py = /v1-Gateway-Prozess (:9010) · frontend/src/views/*.tsx = Ansichten · frontend/src/lib/api.ts = Typen+Fetch · frontend/src/lib/queries.ts = TanStack-Query-Hooks (Polling) · frontend/src/nav.ts = Sidebar · deploy/ = Skripte/Units/Hooks/Skills · Projekt-Regeln: AGENTS.md (Wurzel) + docs/wissen/.
+1 -1
View File
@@ -123,7 +123,7 @@ check_hermes(){
done done
if [ "$state" = "done" ]; then if [ "$state" = "done" ]; then
SUMMARY+=("$name: $behind Commits eingespielt, Gehirn-Check grün.") SUMMARY+=("$name: $behind Commits eingespielt, Gehirn-Check grün.")
notify "Hermes-Agent aktualisiert ($behind Commits). Gehirn-Check (Mem0, Tools, Voice) grün — alles läuft." notify "Hermes-Agent aktualisiert ($behind Commits). Gehirn-Check (Gedächtnis, Tools, Voice) grün — alles läuft."
return return
fi fi
# Rot/Timeout: ERST Selbstreparatur versuchen (Config-Bruch selbst ziehen, neue Version behalten) — # Rot/Timeout: ERST Selbstreparatur versuchen (Config-Bruch selbst ziehen, neue Version behalten) —
+6 -9
View File
@@ -3,8 +3,8 @@
# zurückkommt. Erzeugt EIN Tarball mc2-state-<ts>.tar.gz, behält die letzten N. # zurückkommt. Erzeugt EIN Tarball mc2-state-<ts>.tar.gz, behält die letzten N.
# Läuft per systemd-Timer (täglich), manuell, oder über den UI-Snapshot-Button. # Läuft per systemd-Timer (täglich), manuell, oder über den UI-Snapshot-Button.
# #
# Inhalt: mem0 (Chroma + history.db) · ~/.hermes (config.yaml, .env, plugins/) · # Inhalt: ~/.hermes (config.yaml, .env, plugins/) · /etc/llama-swap/config.yaml ·
# /etc/llama-swap/config.yaml · known-good/ (Versions-Manifest, s. u.) # known-good/ (Versions-Manifest, s. u.)
# NICHT enthalten (bewusst): GGUF-Modelle (riesig, neu ladbar), MC2-Code (Git), venvs. # NICHT enthalten (bewusst): GGUF-Modelle (riesig, neu ladbar), MC2-Code (Git), venvs.
# #
# ACHTUNG: das Tarball enthält ~/.hermes/.env (Secrets) → chmod 600, nicht in Git. # ACHTUNG: das Tarball enthält ~/.hermes/.env (Secrets) → chmod 600, nicht in Git.
@@ -20,16 +20,14 @@ RETAIN="${MC_BACKUP_RETAIN:-14}"
# Off-Box-Ziel (leer = deaktiviert). Default: Proxmox-Host, eigener Key mit minimalem Recht. # Off-Box-Ziel (leer = deaktiviert). Default: Proxmox-Host, eigener Key mit minimalem Recht.
OFFSITE="${MC_BACKUP_OFFSITE:-root@192.168.178.108:/var/lib/vz/mc2-backups}" OFFSITE="${MC_BACKUP_OFFSITE:-root@192.168.178.108:/var/lib/vz/mc2-backups}"
OFFSITE_KEY="${MC_BACKUP_OFFSITE_KEY:-$HOME/.ssh/mc2_offsite}" OFFSITE_KEY="${MC_BACKUP_OFFSITE_KEY:-$HOME/.ssh/mc2_offsite}"
MEM0_DIR="${MC_MEM0_DIR:-/srv/models/mem0}"
LSWAP="${MC_CONFIG_PATH:-/etc/llama-swap/config.yaml}" LSWAP="${MC_CONFIG_PATH:-/etc/llama-swap/config.yaml}"
HERMES="${HERMES_HOME:-$HOME/.hermes}" HERMES="${HERMES_HOME:-$HOME/.hermes}"
TS="$(date +%Y%m%d-%H%M%S)" TS="$(date +%Y%m%d-%H%M%S)"
STAGE="$(mktemp -d)" STAGE="$(mktemp -d)"
trap 'rm -rf "$STAGE"' EXIT trap 'rm -rf "$STAGE"' EXIT
mkdir -p "$STAGE/mem0" "$STAGE/hermes" "$STAGE/llama-swap" mkdir -p "$STAGE/hermes" "$STAGE/llama-swap"
[ -d "$MEM0_DIR" ] && cp -a "$MEM0_DIR/." "$STAGE/mem0/" || true
[ -f "$HERMES/config.yaml" ] && cp -a "$HERMES/config.yaml" "$STAGE/hermes/" || true [ -f "$HERMES/config.yaml" ] && cp -a "$HERMES/config.yaml" "$STAGE/hermes/" || true
[ -f "$HERMES/.env" ] && cp -a "$HERMES/.env" "$STAGE/hermes/" || true [ -f "$HERMES/.env" ] && cp -a "$HERMES/.env" "$STAGE/hermes/" || true
[ -d "$HERMES/plugins" ] && cp -a "$HERMES/plugins" "$STAGE/hermes/plugins" || true [ -d "$HERMES/plugins" ] && cp -a "$HERMES/plugins" "$STAGE/hermes/plugins" || true
@@ -60,7 +58,7 @@ SRC="${MC2_SRC:-$HOME/mission-control-v2}"
HERMES_AGENT="${HERMES_AGENT_DIR:-$HOME/.hermes/hermes-agent}" HERMES_AGENT="${HERMES_AGENT_DIR:-$HOME/.hermes/hermes-agent}"
UV="$(command -v uv || echo "$HOME/.local/bin/uv")" UV="$(command -v uv || echo "$HOME/.local/bin/uv")"
# pip freeze je venv; mem0-venv ist uv-managed (kein pip-Modul) → uv als Fallback. # pip freeze je venv; voice-venv ist uv-managed (kein pip-Modul) → uv als Fallback.
freeze_venv() { # $1 = python-Pfad, $2 = Zieldatei freeze_venv() { # $1 = python-Pfad, $2 = Zieldatei
if [ ! -x "$1" ]; then echo "venv fehlt: $1" > "$2"; return 0; fi if [ ! -x "$1" ]; then echo "venv fehlt: $1" > "$2"; return 0; fi
"$1" -m pip freeze > "$2" 2>/dev/null \ "$1" -m pip freeze > "$2" 2>/dev/null \
@@ -68,7 +66,6 @@ freeze_venv() { # $1 = python-Pfad, $2 = Zieldatei
|| echo "pip freeze fehlgeschlagen: $1" > "$2" || echo "pip freeze fehlgeschlagen: $1" > "$2"
} }
freeze_venv "$SRC/backend/.venv/bin/python" "$KG/pip-backend.txt" freeze_venv "$SRC/backend/.venv/bin/python" "$KG/pip-backend.txt"
freeze_venv "$HOME/.mem0/venv/bin/python" "$KG/pip-mem0.txt"
freeze_venv "$HOME/.voice/venv/bin/python" "$KG/pip-voice.txt" freeze_venv "$HOME/.voice/venv/bin/python" "$KG/pip-voice.txt"
{ {
@@ -79,7 +76,7 @@ freeze_venv "$HOME/.voice/venv/bin/python" "$KG/pip-voice.txt"
echo "llama.cpp : $(LD_LIBRARY_PATH=/opt/llamacpp-vulkan /usr/local/bin/llama-server --version 2>&1 | grep -m1 -o 'version: .*' || echo '?')" echo "llama.cpp : $(LD_LIBRARY_PATH=/opt/llamacpp-vulkan /usr/local/bin/llama-server --version 2>&1 | grep -m1 -o 'version: .*' || echo '?')"
echo "llama-swap : $(/usr/local/bin/llama-swap --version 2>/dev/null | head -1 || echo '?')" echo "llama-swap : $(/usr/local/bin/llama-swap --version 2>/dev/null | head -1 || echo '?')"
echo "python-venvs :" echo "python-venvs :"
for P in "$SRC/backend/.venv/bin/python" "$HOME/.mem0/venv/bin/python" "$HOME/.voice/venv/bin/python"; do for P in "$SRC/backend/.venv/bin/python" "$HOME/.voice/venv/bin/python"; do
echo " $P$("$P" --version 2>&1 || echo 'fehlt')" echo " $P$("$P" --version 2>&1 || echo 'fehlt')"
done done
} > "$KG/versions.txt" || true } > "$KG/versions.txt" || true
@@ -92,7 +89,7 @@ cat > "$STAGE/MANIFEST.txt" <<EOF
mc2-state backup mc2-state backup
created : $TS created : $TS
host : $(hostname) host : $(hostname)
inhalt : mem0 (chroma + history.db), hermes (config.yaml, .env, plugins/), llama-swap (config.yaml), known-good/ (Versions-Manifest: pip freeze, Engine, Modelle) inhalt : hermes (config.yaml, .env, plugins/), llama-swap (config.yaml), known-good/ (Versions-Manifest: pip freeze, Engine, Modelle)
restore : bash ~/mission-control-v2/deploy/restore.sh mc2-state-$TS.tar.gz restore : bash ~/mission-control-v2/deploy/restore.sh mc2-state-$TS.tar.gz
EOF EOF
+1 -14
View File
@@ -1,28 +1,15 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Post-Update-Check nach einem Hermes-Agent-Update: läuft unser geteiltes „Gehirn" # Post-Update-Check nach einem Hermes-Agent-Update: läuft unser geteiltes „Gehirn"
# (Mem0 + die mc2-memory-Integration) noch? Exit 0 = alles ok, sonst 1 → der Job wird # (die mc2-memory-Integration) noch? Exit 0 = alles ok, sonst 1 → der Job wird
# im UI rot, damit ein kaputtes Gehirn sofort auffällt. # im UI rot, damit ein kaputtes Gehirn sofort auffällt.
set -uo pipefail set -uo pipefail
MC_URL="${MC_URL:-http://127.0.0.1:9001}" MC_URL="${MC_URL:-http://127.0.0.1:9001}"
MEM0_URL="${MEM0_SERVICE_URL:-http://127.0.0.1:8765}"
HERMES="${HERMES_HOME:-$HOME/.hermes}" HERMES="${HERMES_HOME:-$HOME/.hermes}"
fail=0 fail=0
echo "=== Hermes Post-Update: Gehirn-Check ===" echo "=== Hermes Post-Update: Gehirn-Check ==="
if curl -sf -m 5 "$MEM0_URL/health" >/dev/null 2>&1; then
echo "PASS · Mem0-Sidecar erreichbar"
else
echo "FAIL · Mem0-Sidecar NICHT erreichbar"; fail=1
fi
if curl -sf -m 8 "$MC_URL/api/memory" >/dev/null 2>&1; then
echo "PASS · /api/memory antwortet"
else
echo "FAIL · /api/memory antwortet nicht"; fail=1
fi
if grep -qE "^[[:space:]]*provider:[[:space:]]*'?mc2-memory'?" "$HERMES/config.yaml" 2>/dev/null \ if grep -qE "^[[:space:]]*provider:[[:space:]]*'?mc2-memory'?" "$HERMES/config.yaml" 2>/dev/null \
&& grep -qE "memory_enabled:[[:space:]]*true" "$HERMES/config.yaml" 2>/dev/null; then && grep -qE "memory_enabled:[[:space:]]*true" "$HERMES/config.yaml" 2>/dev/null; then
echo "PASS · memory.provider=mc2-memory aktiv" echo "PASS · memory.provider=mc2-memory aktiv"
+1 -4
View File
@@ -15,11 +15,10 @@ set -euo pipefail
MODELS_DIR="${MC_MODELS_DIR:-/srv/models}" MODELS_DIR="${MC_MODELS_DIR:-/srv/models}"
DEST_DIR="$MODELS_DIR/mc2-backups" DEST_DIR="$MODELS_DIR/mc2-backups"
MEM0_DIR="${MC_MEM0_DIR:-/srv/models/mem0}"
LSWAP="${MC_CONFIG_PATH:-/etc/llama-swap/config.yaml}" LSWAP="${MC_CONFIG_PATH:-/etc/llama-swap/config.yaml}"
HERMES="${HERMES_HOME:-$HOME/.hermes}" HERMES="${HERMES_HOME:-$HOME/.hermes}"
SRC="${MC2_SRC:-$HOME/mission-control-v2}" SRC="${MC2_SRC:-$HOME/mission-control-v2}"
SERVICES="mem0-service mission-control-2 hermes-gateway" SERVICES="mission-control-2 hermes-gateway"
OFFSITE="${MC_BACKUP_OFFSITE:-root@192.168.178.108:/var/lib/vz/mc2-backups}" OFFSITE="${MC_BACKUP_OFFSITE:-root@192.168.178.108:/var/lib/vz/mc2-backups}"
OFFSITE_KEY="${MC_BACKUP_OFFSITE_KEY:-$HOME/.ssh/mc2_offsite}" OFFSITE_KEY="${MC_BACKUP_OFFSITE_KEY:-$HOME/.ssh/mc2_offsite}"
OFFSITE_SSH="ssh -i $OFFSITE_KEY -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new" OFFSITE_SSH="ssh -i $OFFSITE_KEY -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new"
@@ -98,7 +97,6 @@ echo "--- Inhalt ---"; cat "$STAGE/MANIFEST.txt" 2>/dev/null || true; echo "----
if [ "$DRY" = 1 ]; then if [ "$DRY" = 1 ]; then
echo "[dry-run] würde zurücksetzen:" echo "[dry-run] würde zurücksetzen:"
[ -d "$STAGE/mem0" ] && echo " • mem0 → $MEM0_DIR (wird ersetzt)"
[ -f "$STAGE/hermes/config.yaml" ] && echo "$HERMES/config.yaml" [ -f "$STAGE/hermes/config.yaml" ] && echo "$HERMES/config.yaml"
[ -f "$STAGE/hermes/.env" ] && echo "$HERMES/.env" [ -f "$STAGE/hermes/.env" ] && echo "$HERMES/.env"
[ -d "$STAGE/hermes/plugins" ] && echo "$HERMES/plugins/" [ -d "$STAGE/hermes/plugins" ] && echo "$HERMES/plugins/"
@@ -119,7 +117,6 @@ bash "$SRC/deploy/backup.sh" || echo " (Pre-Restore-Backup fehlgeschlagen — f
echo "→ Dienste stoppen…" echo "→ Dienste stoppen…"
systemctl --user stop $SERVICES 2>/dev/null || true systemctl --user stop $SERVICES 2>/dev/null || true
if [ -d "$STAGE/mem0" ]; then rm -rf "$MEM0_DIR"; mkdir -p "$MEM0_DIR"; cp -a "$STAGE/mem0/." "$MEM0_DIR/"; fi
[ -f "$STAGE/hermes/config.yaml" ] && { mkdir -p "$HERMES"; cp -a "$STAGE/hermes/config.yaml" "$HERMES/"; } [ -f "$STAGE/hermes/config.yaml" ] && { mkdir -p "$HERMES"; cp -a "$STAGE/hermes/config.yaml" "$HERMES/"; }
[ -f "$STAGE/hermes/.env" ] && cp -a "$STAGE/hermes/.env" "$HERMES/" [ -f "$STAGE/hermes/.env" ] && cp -a "$STAGE/hermes/.env" "$HERMES/"
[ -d "$STAGE/hermes/plugins" ] && { mkdir -p "$HERMES/plugins"; cp -a "$STAGE/hermes/plugins/." "$HERMES/plugins/"; } [ -d "$STAGE/hermes/plugins" ] && { mkdir -p "$HERMES/plugins"; cp -a "$STAGE/hermes/plugins/." "$HERMES/plugins/"; }
+4 -12
View File
@@ -3,15 +3,14 @@
# dass der komplette Inferenz-Stack noch FUNKTIONIERT — nicht nur "Befehl lief durch". # dass der komplette Inferenz-Stack noch FUNKTIONIERT — nicht nur "Befehl lief durch".
# Exit 0 = alles ok, sonst 1 → der jobengine-Job wird im UI ROT (state=failed). # Exit 0 = alles ok, sonst 1 → der jobengine-Job wird im UI ROT (state=failed).
# #
# Pendant zu hermes-postcheck.sh (prüft das Gehirn/Mem0); dieser prüft Router+Engine+MC2 # Pendant zu hermes-postcheck.sh (prüft das Gehirn); dieser prüft Router+Engine+MC2
# inkl. einer ECHTEN 1-Token-Inferenz (beweist, dass ein Modell wirklich lädt & generiert). # inkl. einer ECHTEN 1-Token-Inferenz (beweist, dass ein Modell wirklich lädt & generiert).
set -uo pipefail set -uo pipefail
SWAP_URL="${MC_LLAMA_SWAP_URL:-http://127.0.0.1:8080}" SWAP_URL="${MC_LLAMA_SWAP_URL:-http://127.0.0.1:8080}"
MC_URL="${MC_URL:-http://127.0.0.1:9001}" MC_URL="${MC_URL:-http://127.0.0.1:9001}"
MEM0_URL="${MEM0_SERVICE_URL:-http://127.0.0.1:8765}"
BRAIN="${MC_WARMUP_MODELS:-fast}"; BRAIN="${BRAIN%% *}" # erstes Modell, falls Liste BRAIN="${MC_WARMUP_MODELS:-fast}"; BRAIN="${BRAIN%% *}" # erstes Modell, falls Liste
EMBED="${MC_WARMUP_EMBED:-embed}"; EMBED="${EMBED%% *}" # Embedding-Modell (Mem0/Gedächtnis) EMBED="${MC_WARMUP_EMBED:-embed}"; EMBED="${EMBED%% *}" # Embedding-Modell (Gedächtnis)
fail=0 fail=0
echo "=== Stack Post-Update: Funktionsprüfung ===" echo "=== Stack Post-Update: Funktionsprüfung ==="
@@ -48,14 +47,14 @@ else
echo "FAIL · Inferenz auf '$BRAIN' fehlgeschlagen (Modell lädt/generiert nicht)"; fail=1 echo "FAIL · Inferenz auf '$BRAIN' fehlgeschlagen (Modell lädt/generiert nicht)"; fail=1
fi fi
# 3b. Embedding-Modell (für Mem0/Gedächtnis) lädt und liefert einen Vektor? # 3b. Embedding-Modell (für Gedächtnis) lädt und liefert einen Vektor?
eresp="$(curl -s -m 120 -X POST "$SWAP_URL/v1/embeddings" \ eresp="$(curl -s -m 120 -X POST "$SWAP_URL/v1/embeddings" \
-H 'Content-Type: application/json' \ -H 'Content-Type: application/json' \
-d "{\"model\":\"$EMBED\",\"input\":\"ping\"}" 2>/dev/null)" -d "{\"model\":\"$EMBED\",\"input\":\"ping\"}" 2>/dev/null)"
if printf '%s' "$eresp" | grep -q '"embedding"'; then if printf '%s' "$eresp" | grep -q '"embedding"'; then
echo "PASS · Embedding-Modell '$EMBED' liefert Vektoren" echo "PASS · Embedding-Modell '$EMBED' liefert Vektoren"
else else
echo "FAIL · Embedding-Modell '$EMBED' lädt/antwortet nicht (Mem0/Gedächtnis betroffen)"; fail=1 echo "FAIL · Embedding-Modell '$EMBED' lädt/antwortet nicht (Gedächtnis betroffen)"; fail=1
fi fi
# 4. MC2 selbst gesund (Engine + Gateway erreichbar)? # 4. MC2 selbst gesund (Engine + Gateway erreichbar)?
@@ -85,13 +84,6 @@ if systemctl --user is-enabled --quiet mc2-steward 2>/dev/null; then
fi fi
fi fi
# 5. Mem0-Sidecar (Gedächtnis) erreichbar?
if curl -sf -m 5 "$MEM0_URL/health" >/dev/null 2>&1; then
echo "PASS · Mem0-Sidecar erreichbar"
else
echo "FAIL · Mem0-Sidecar NICHT erreichbar"; fail=1
fi
if [ "$fail" -eq 0 ]; then if [ "$fail" -eq 0 ]; then
echo "=== STACK OK ✓ ===" echo "=== STACK OK ✓ ==="
else else
+2 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Venv-Audit (monatlich, 17.07.2026): die Python-Nebendienste (Mem0, Voice) altern # Venv-Audit (monatlich, 17.07.2026): die Python-Nebendienste (Voice) altern
# eingefroren — kein Auto-Update-Kanal deckt sie ab. Dieser Audit MELDET nur # eingefroren — kein Auto-Update-Kanal deckt sie ab. Dieser Audit MELDET nur
# (bewusst kein Auto-Update: Python-Deps brechen gern): # (bewusst kein Auto-Update: Python-Deps brechen gern):
# je venv: veraltete Pakete zählen (pip list --outdated) + bekannte # je venv: veraltete Pakete zählen (pip list --outdated) + bekannte
@@ -9,7 +9,7 @@
# idempotent je Monat). Läuft als Hermes-Cron --no-agent (stdout = Telegram). # idempotent je Monat). Läuft als Hermes-Cron --no-agent (stdout = Telegram).
set -uo pipefail set -uo pipefail
VENVS=("$HOME/.mem0/venv:mem0" "$HOME/.voice/venv:voice") VENVS=("$HOME/.voice/venv:voice")
AUDIT_VENV="$HOME/.venv-audit" AUDIT_VENV="$HOME/.venv-audit"
HERMES="$HOME/.local/bin/hermes" HERMES="$HOME/.local/bin/hermes"
MONAT="$(date '+%Y-%m')" MONAT="$(date '+%Y-%m')"
+1 -1
View File
@@ -4,7 +4,7 @@
# Eingehängt als ExecStartPost=-/usr/local/bin/llama-swap-warmup.sh in llama-swap. # Eingehängt als ExecStartPost=-/usr/local/bin/llama-swap-warmup.sh in llama-swap.
# #
# `fast` = das Agent-Hirn (Qwen3.6-35B-A3B), `embed` = das Embedding-Modell # `fast` = das Agent-Hirn (Qwen3.6-35B-A3B), `embed` = das Embedding-Modell
# (Qwen3-Embedding-0.6B, für Mem0/Gedächtnis). Die Augen (vision/VL-30B) sind seit 07.07. # (Qwen3-Embedding-0.6B, für Gedächtnis). Die Augen (vision/VL-30B) sind seit 07.07.
# ON-DEMAND (User-Entscheid) und gehören NICHT mehr ins Warm-Set — die Lucy-App wärmt sie # ON-DEMAND (User-Entscheid) und gehören NICHT mehr ins Warm-Set — die Lucy-App wärmt sie
# beim Start selbst an. Modelle werden NICHT automatisch zusammen geladen — jedes einzeln # beim Start selbst an. Modelle werden NICHT automatisch zusammen geladen — jedes einzeln
# anstoßen, sonst bleibt es kalt. # anstoßen, sonst bleibt es kalt.
-483
View File
@@ -1,483 +0,0 @@
#!/usr/bin/env python3
"""
Mem0-Sidecar für Mission Control 2.0 — das auto-lernende, semantische Gedächtnis.
WARUM ein eigener Dienst? Mem0 + chromadb laufen nur unter Python 3.12 (`~/.mem0/venv`),
das MC2-Backend aber unter Python 3.14 (kann mem0 nicht importieren). Darum kapselt dieser
schlanke FastAPI-Dienst die Mem0-Memory-Instanz und exponiert sie auf localhost. MC2
(`backend/services/memory.py`) spricht ihn per HTTP an — die `/api/memory`-API-Form bleibt
nach außen unverändert (UI + MCP-Server kompatibel).
Aufbau:
- Vektor-Store : Chroma embedded (kein Docker auf der Box) — Pfad MEM0_CHROMA_PATH.
- Embeddings : llama.cpp `embed`-Rolle über llama-swap (/v1/embeddings, 1024 Dim).
- LLM-Extraktion: lokales `fast`-Hirn (Qwen3.6) über llama-swap (/v1/chat/completions).
Thinking wird abgeschaltet (NoThinkLLM), sonst bricht die JSON-Extraktion ab.
Läuft als systemd-User-Dienst (deploy/mem0-service.service) im `~/.mem0/venv`.
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
from mem0 import Memory
from mem0.configs.llms.openai import OpenAIConfig
from mem0.llms.openai import OpenAILLM
from pydantic import BaseModel
log = logging.getLogger("mem0_service")
# --- Konfiguration (alles über Env überschreibbar; Defaults = Box-Stand) ----------
USER_ID = os.environ.get("MEM0_USER_ID", "mission-control") # ein geteiltes Gehirn
EMBED_URL = os.environ.get("MEM0_EMBED_URL", "http://127.0.0.1:8080/v1")
LLM_URL = os.environ.get("MEM0_LLM_URL", "http://127.0.0.1:8080/v1")
EMBED_MODEL = os.environ.get("MEM0_EMBED_MODEL", "embed")
LLM_MODEL = os.environ.get("MEM0_LLM_MODEL", "fast")
EMBED_DIMS = int(os.environ.get("MEM0_EMBED_DIMS", "1024"))
CHROMA_PATH = os.environ.get("MEM0_CHROMA_PATH", "/srv/models/mem0/chroma")
HISTORY_DB = os.environ.get("MEM0_HISTORY_DB", "/srv/models/mem0/history.db")
COLLECTION = os.environ.get("MEM0_COLLECTION", "mc2")
API_KEY = os.environ.get("MEM0_API_KEY", "sk-local") # llama.cpp ignoriert den Key
LLM_MAX_TOKENS = int(os.environ.get("MEM0_LLM_MAX_TOKENS", "2048"))
SEARCH_TOP_K = int(os.environ.get("MEM0_SEARCH_TOP_K", "50"))
# --- Rerank-Zweitstufe (07.07.2026): Die Vektor-Suche findet ÄHNLICHES, der Reranker ordnet
# nach ECHTER Relevanz (Qwen3-Reranker via llama-swap /v1/rerank). Es ändert sich NUR die
# Reihenfolge: `score` bleibt der Vektor-Score (RECALL_MIN_SCORE im Hermes-Plugin bleibt
# kalibriert), Konsumenten, die vorne abschneiden (RECALL_LIMIT), bekommen automatisch die
# relevantesten Fakten. Jeder Fehler → lautlos Vektor-Reihenfolge (Gedächtnis bricht NIE).
RERANK_ENABLED = os.environ.get("MC_RERANK_ENABLED", "1") != "0"
RERANK_URL = os.environ.get("MC_RERANK_URL", "http://127.0.0.1:8080/v1/rerank")
RERANK_MODEL = os.environ.get("MC_RERANK_MODEL", "Qwen3-Reranker-0.6B")
RERANK_TIMEOUT = float(os.environ.get("MC_RERANK_TIMEOUT", "8"))
RERANK_CANDIDATES = int(os.environ.get("MC_RERANK_CANDIDATES", "20")) # nur die Top-N neu ordnen (Latenz)
# Kategorien = fundierte Memory-Taxonomie (semantisch/prozedural/episodisch), bewusst knapp (4).
# Mem0-OSS kann nicht nativ klassifizieren (Cloud-Feature) → wir lassen das Hirn beim Lernen
# einordnen (classify_facts). Die Beschreibungen steuern die Treffsicherheit der Auto-Zuordnung.
CATEGORIES = ("identity", "knowledge", "rules", "events")
DEFAULT_CATEGORY = "knowledge"
CATEGORY_DESCRIPTIONS = {
"identity": "Identität & Vorlieben des Nutzers: wer er ist (Name, Rolle), wie er angesprochen werden "
"will, persönliche Vorlieben und seine Arbeitsweise.",
"knowledge": "Wissen & Fakten: stabile Fakten über Stack, Projekte, Tools, Infrastruktur und Umgebung.",
"rules": "Regeln & Konventionen: verbindliche Anweisungen und Workflows — wie etwas gemacht werden "
"soll, Do's und Don'ts.",
"events": "Ereignisse & Entscheidungen: was passiert ist, getroffene Entscheidungen, zeitgebundene Vorgänge.",
}
class NoThinkLLM(OpenAILLM):
"""`fast` (Qwen3.6) ist ein Thinking-Modell. Unter response_format=json_object
verbrennt das Reasoning Tokens und schneidet die JSON-Fakten ab (leeres Ergebnis).
`enable_thinking=false` (chat_template_kwargs) → deterministische, schnelle Extraktion."""
def generate_response(self, messages, response_format=None, tools=None,
tool_choice="auto", **kwargs):
eb = dict(kwargs.pop("extra_body", {}) or {})
eb.setdefault("chat_template_kwargs", {"enable_thinking": False})
return super().generate_response(
messages, response_format=response_format, tools=tools,
tool_choice=tool_choice, extra_body=eb, **kwargs,
)
def build_memory() -> Memory:
os.environ.setdefault("OPENAI_API_KEY", API_KEY)
os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
os.makedirs(CHROMA_PATH, exist_ok=True)
os.makedirs(os.path.dirname(HISTORY_DB), exist_ok=True)
cfg = {
"vector_store": {"provider": "chroma", "config": {
"collection_name": COLLECTION, "path": CHROMA_PATH}},
"embedder": {"provider": "openai", "config": {
"model": EMBED_MODEL, "embedding_dims": EMBED_DIMS,
"openai_base_url": EMBED_URL, "api_key": API_KEY}},
"llm": {"provider": "openai", "config": {
"model": LLM_MODEL, "openai_base_url": LLM_URL, "api_key": API_KEY,
"temperature": 0.1, "max_tokens": LLM_MAX_TOKENS}},
"history_db_path": HISTORY_DB,
# Sprache zwingend Deutsch erzwingen — mem0s englischer Extraktions-Prompt zieht sonst
# Richtung Englisch. Eigennamen/Fachbegriffe bleiben unangetastet.
"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. "
"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, "
"Vulkan) bleiben unverändert. Schreibe jeden Fakt knapp und atomar — genau eine Aussage."
),
}
m = Memory.from_config(cfg)
# LLM gegen die Thinking-freie Variante tauschen (gleiche Verbindung).
m.llm = NoThinkLLM(OpenAIConfig(
model=LLM_MODEL, openai_base_url=LLM_URL, api_key=API_KEY,
temperature=0.1, max_tokens=LLM_MAX_TOKENS,
))
return m
_mem: Memory | None = None
def mem() -> Memory:
global _mem
if _mem is None:
_mem = build_memory()
return _mem
@asynccontextmanager
async def lifespan(_app: FastAPI):
logging.basicConfig(level=logging.INFO)
try:
mem() # Memory beim Start aufbauen (Chroma öffnen, Modelle anpingen)
log.info("Mem0 bereit (chroma=%s, embed=%s, llm=%s)", CHROMA_PATH, EMBED_MODEL, LLM_MODEL)
except Exception:
log.exception("Mem0-Init fehlgeschlagen (Dienst läuft, /health meldet down)")
yield
app = FastAPI(title="MC2 Mem0 Sidecar", lifespan=lifespan)
# --- Mapping Mem0 <-> MC2-API-Form -----------------------------------------------
def _to_item(r: dict, score=None) -> dict:
meta = r.get("metadata") or {}
out = {
"id": r.get("id"),
"content": r.get("memory", ""),
"category": meta.get("category") or "stable",
"bereich": meta.get("bereich") or "",
"source": meta.get("source") or "auto",
"created_at": r.get("created_at") or "",
"updated_at": r.get("updated_at") or r.get("created_at") or "",
}
if score is not None:
try:
out["score"] = round(float(score), 4)
except (TypeError, ValueError):
pass
return out
class MemIn(BaseModel):
content: str
category: str = DEFAULT_CATEGORY
source: str = "manual"
class MemUp(BaseModel):
content: str | None = None
category: str | None = None
class LearnIn(BaseModel):
text: str | None = None
messages: list[dict] | None = None
source: str = "auto"
category: str = DEFAULT_CATEGORY
classify: bool = True # Hirn ordnet die gelernten Fakten automatisch ein
@app.get("/health")
def health() -> dict:
try:
mem().get_all(filters={"user_id": USER_ID}, top_k=1)
return {"ok": True}
except Exception as exc:
raise HTTPException(503, f"mem0 nicht bereit: {exc}")
def _rerank(query: str, items: list[dict]) -> list[dict]:
"""Suchtreffer nach echter Relevanz umordnen; bei jedem Fehler unverändert zurück."""
if not (RERANK_ENABLED and len(items) > 1):
return items
try:
import httpx
r = httpx.post(RERANK_URL, json={
"model": RERANK_MODEL,
"query": query,
"documents": [i.get("content", "") for i in items],
"top_n": len(items),
}, timeout=RERANK_TIMEOUT)
r.raise_for_status()
results = r.json().get("results") or []
ranked = []
for s in sorted(results, key=lambda x: x.get("relevance_score", 0.0), reverse=True):
idx = s.get("index")
if isinstance(idx, int) and 0 <= idx < len(items):
item = dict(items[idx])
item["rerank_score"] = round(float(s.get("relevance_score", 0.0)), 4)
ranked.append(item)
# Nur übernehmen, wenn der Reranker ALLE Kandidaten bewertet hat (sonst Verlustgefahr).
return ranked if len(ranked) == len(items) else items
except Exception as exc:
log.debug("rerank übersprungen: %s", exc)
return items
@app.get("/memory")
def list_memory(q: str = "", category: str = "", bereich: str = "") -> list[dict]:
"""q gesetzt → semantische Suche (mit Relevanz-Score). Sonst → alle Fakten.
bereich='alltag'|'projekt' filtert (Fakten OHNE Bereichs-Feld laufen immer mit,
damit Alt-Bestand nie verschwindet)."""
if q:
res = mem().search(q, filters={"user_id": USER_ID}, top_k=SEARCH_TOP_K)
items = [_to_item(r, r.get("score")) for r in res.get("results", [])]
# Zweitstufe: die vorderen Kandidaten nach echter Relevanz ordnen (Rest bleibt hinten dran).
items = _rerank(q, items[:RERANK_CANDIDATES]) + items[RERANK_CANDIDATES:]
else:
res = mem().get_all(filters={"user_id": USER_ID}, top_k=1000)
items = [_to_item(r) for r in res.get("results", [])]
items.sort(key=lambda i: i.get("created_at", ""), reverse=True)
if category:
items = [i for i in items if i["category"] == category]
if bereich in ("alltag", "projekt"):
items = [i for i in items if i.get("bereich") in (bereich, "")]
return items
@app.post("/memory", status_code=201)
def add_memory(body: MemIn) -> dict:
"""Manueller/agentischer Einzel-Fakt → VERBATIM speichern (infer=False, keine
LLM-Umformung). Auto-Lernen aus Gesprächen läuft über /learn (infer=True)."""
content = body.content.strip()
if not content:
raise HTTPException(400, "content leer")
res = mem().add(
[{"role": "user", "content": content}],
user_id=USER_ID, infer=False,
metadata={"category": body.category, "source": body.source},
)
results = res.get("results", [])
if not results:
raise HTTPException(500, "Mem0 hat nichts gespeichert")
new_id = results[0]["id"]
full = mem().get(new_id) or {}
return _to_item(full) if full else _to_item({"id": new_id, "memory": content,
"metadata": {"category": body.category, "source": body.source}})
@app.put("/memory/{mid}")
def update_memory(mid: str, body: MemUp) -> dict:
existing = mem().get(mid)
if not existing:
raise HTTPException(404, "Eintrag nicht gefunden")
cur = _to_item(existing)
new_content = body.content.strip() if body.content is not None else cur["content"]
new_cat = body.category if body.category is not None else cur["category"]
mem().update(mid, data=new_content,
metadata={"category": new_cat, "source": cur["source"]})
full = mem().get(mid) or {}
return _to_item(full)
@app.delete("/memory/{mid}")
def delete_memory(mid: str) -> dict:
if not mem().get(mid):
raise HTTPException(404, "Eintrag nicht gefunden")
mem().delete(mid)
return {"ok": True}
@app.get("/graph")
def graph(min_score: float = 0.45, top_k: int = 3) -> dict:
"""Fakten als Graph: Knoten = Fakten, Kanten = semantische Ähnlichkeit (Kosinus
der gespeicherten Embeddings, je Knoten die top_k Nachbarn ≥ min_score). Für die
Obsidian-artige Gedächtnis-Visualisierung im UI."""
items = mem().get_all(filters={"user_id": USER_ID}, top_k=2000).get("results", [])
nodes = [_to_item(r) for r in items]
node_ids = [n["id"] for n in nodes]
idx = {nid: i for i, nid in enumerate(node_ids)}
# Embeddings direkt aus der Chroma-Collection ziehen (kein Re-Embedding).
col = mem().vector_store.collection
raw = col.get(include=["embeddings"])
raw_ids = raw.get("ids") or []
raw_embs = raw.get("embeddings")
edges: list[dict] = []
vecs = [None] * len(node_ids)
have = 0
if raw_embs is not None:
for rid, emb in zip(raw_ids, raw_embs):
if rid in idx and emb is not None:
vecs[idx[rid]] = emb
have += 1
if have >= 2:
import numpy as np
present = [i for i, v in enumerate(vecs) if v is not None]
M = np.array([vecs[i] for i in present], dtype=float)
norms = np.linalg.norm(M, axis=1, keepdims=True)
norms[norms == 0] = 1.0
Mn = M / norms
sim = Mn @ Mn.T
seen: set[tuple[int, int]] = set()
for a in range(len(present)):
order = np.argsort(-sim[a])
cnt = 0
for b in order:
if b == a:
continue
s = float(sim[a][b])
if s < min_score:
break
i, j = present[a], present[b]
key = (min(i, j), max(i, j))
if key not in seen:
seen.add(key)
edges.append({"source": node_ids[i], "target": node_ids[j],
"weight": round(s, 3)})
cnt += 1
if cnt >= top_k:
break
return {"nodes": nodes, "edges": edges}
def classify_facts(facts: list[tuple[str, str]]) -> dict:
"""Ordnet jeden Fakt per LLM einer Kategorie UND einem Bereich zu →
{id: {"category": <slug>, "bereich": "alltag"|"projekt"}}.
Bereich (19.07.2026, Trennung der Welten): "alltag" = der Nutzer als Person
(Vorlieben, Leben, Arbeitsweise, Entscheidungen), "projekt" = Technik-/Code-/
Infrastruktur-Stände. Grundlage für einen späteren Recall-Filter, damit
Projekt-Schutt nicht in Alltags-Gespräche streut — OHNE zweites Gedächtnis.
Nutzt dasselbe (Thinking-freie) Hirn wie die Extraktion. Fehler = leeres Mapping."""
if not facts:
return {}
import json as _json
import re as _re
cat_lines = "\n".join(f"- {k}: {v}" for k, v in CATEGORY_DESCRIPTIONS.items())
system = (
"Du bist ein präziser Klassifikator für ein Langzeitgedächtnis. Ordne jeden Fakt GENAU EINER "
"Kategorie zu. Verfügbare Kategorien (nur diese Slugs verwenden):\n" + cat_lines +
"\nOrdne zusätzlich jeden Fakt GENAU EINEM Bereich zu: 'alltag' (der Nutzer als Person: "
"Vorlieben, Leben, Anrede, Arbeitsweise, grundsätzliche Entscheidungen) oder 'projekt' "
"(Technik: Code, Modelle, Server, Infrastruktur, Projekt- und Fehlerstände). "
"Antworte NUR als JSON-Objekt der Form "
"{\"<id>\": {\"kategorie\": \"<slug>\", \"bereich\": \"alltag|projekt\"}, …}."
)
body = "\n".join(f"{fid}: {txt}" for fid, txt in facts)
try:
resp = mem().llm.generate_response(
messages=[{"role": "system", "content": system},
{"role": "user", "content": "Fakten:\n" + body}],
response_format={"type": "json_object"},
)
m = _re.search(r"\{.*\}", resp or "", _re.DOTALL)
data = _json.loads(m.group(0)) if m else {}
out: dict = {}
for k, v in data.items():
if isinstance(v, str): # Alt-Form {"id": "slug"} weiter akzeptieren
if v in CATEGORIES:
out[str(k)] = {"category": v, "bereich": ""}
elif isinstance(v, dict):
cat = v.get("kategorie") or v.get("category") or ""
ber = v.get("bereich") or ""
if cat in CATEGORIES or ber in ("alltag", "projekt"):
out[str(k)] = {"category": cat if cat in CATEGORIES else DEFAULT_CATEGORY,
"bereich": ber if ber in ("alltag", "projekt") else ""}
return out
except Exception:
log.exception("Auto-Klassifikation fehlgeschlagen")
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
(infer=True). Anschließend ordnet das Hirn jeden neuen Fakt automatisch einer Kategorie zu."""
msgs = body.messages
if not msgs and body.text:
msgs = [{"role": "user", "content": body.text}]
if not msgs:
raise HTTPException(400, "text oder messages erforderlich")
res = mem().add(
msgs, user_id=USER_ID, infer=True,
metadata={"category": body.category, "source": body.source},
)
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
if r.get("id") and r.get("memory") and r.get("event") in ("ADD", "UPDATE")]
cats = classify_facts(to_classify)
for r in results:
info = cats.get(r.get("id"))
if info:
meta = {"category": info["category"], "source": body.source}
if info.get("bereich"):
meta["bereich"] = info["bereich"]
try:
mem().update(r["id"], data=r.get("memory", ""), metadata=meta)
except Exception:
log.debug("Kategorie-Update fehlgeschlagen für %s", r.get("id"))
return {"results": [
{"id": r.get("id"), "content": r.get("memory"), "event": r.get("event"),
"category": (cats.get(r.get("id")) or {}).get("category", body.category),
"bereich": (cats.get(r.get("id")) or {}).get("bereich", "")}
for r in results
]}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="127.0.0.1", port=int(os.environ.get("MEM0_PORT", "8765")))
-62
View File
@@ -1,62 +0,0 @@
#!/usr/bin/env python3
"""
Einmalige Migration: alte SQLite-Fakten (`memories`-Tabelle aus dem v1/v2-Memory)
→ Mem0. Läuft im ~/.mem0/venv. Idempotent über eine Marker-Datei.
Jeder Alt-Eintrag wird VERBATIM übernommen (infer=False), Kategorie/Source als
Metadaten. Beim aktuellen Box-Stand ist die DB leer → No-Op, aber für
Reproduzierbarkeit/künftige Migrationen vorhanden.
Aufruf: ~/.mem0/venv/bin/python migrate.py [pfad/zur/mc2-memory.db]
"""
import os
import sqlite3
import sys
from app import USER_ID, mem # nutzt dieselbe Mem0-Instanz/Config
DEFAULT_DB = os.environ.get("MC_MEMORY_DB", "/srv/models/mc2-memory.db")
MARKER = os.environ.get("MEM0_MIGRATION_MARKER", "/srv/models/mem0/.migrated_sqlite")
def main() -> int:
db_path = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_DB
if os.path.exists(MARKER):
print(f"Migration bereits erledigt (Marker {MARKER}).")
return 0
if not os.path.exists(db_path):
print(f"Keine Alt-DB unter {db_path} — nichts zu migrieren.")
os.makedirs(os.path.dirname(MARKER), exist_ok=True)
open(MARKER, "w").close()
return 0
conn = sqlite3.connect(db_path)
conn.row_factory = sqlite3.Row
try:
rows = conn.execute(
"SELECT content, category, source FROM memories ORDER BY created_at"
).fetchall()
except sqlite3.OperationalError:
rows = []
conn.close()
m = mem()
n = 0
for r in rows:
content = (r["content"] or "").strip()
if not content:
continue
m.add([{"role": "user", "content": content}], user_id=USER_ID, infer=False,
metadata={"category": r["category"] or "stable",
"source": r["source"] or "migrated"})
n += 1
os.makedirs(os.path.dirname(MARKER), exist_ok=True)
open(MARKER, "w").close()
print(f"Migriert: {n} Einträge aus {db_path} → Mem0.")
return 0
if __name__ == "__main__":
raise SystemExit(main())
-9
View File
@@ -1,9 +0,0 @@
# Mem0-Sidecar-Deps im ~/.mem0/venv (Python 3.12). BEWUSST GEPINNT — Upgrades nur
# absichtlich, weil der Sidecar mem0-INTERNA nutzt (NoThink-LLM-Swap, Roh-Zugriff auf die
# Chroma-Collection für den Graphen, Annahmen übers Result-Format). Ein unkontrolliertes
# mem0/chromadb-Upgrade kann das still brechen.
# Upgrade-Prozedur (Backup → Pin anheben → smoke_test.py): siehe docs/UPGRADE.md.
mem0ai==2.0.8
chromadb==1.5.9
fastapi==0.138.1
uvicorn[standard]==0.49.0
-71
View File
@@ -1,71 +0,0 @@
#!/usr/bin/env python3
"""
Smoke-Test für den Mem0-Sidecar — VOR und NACH jedem mem0/chromadb-Upgrade laufen lassen.
Prüft genau die Pfade, die mem0-INTERNA nutzen (LLM-Extraktion/NoThink, Auto-Einordnung,
Embeddings/Suche, Roh-Chroma-Graph). Nutzt eine WEGWERF-Collection unter /tmp — das echte
Gedächtnis (/srv/models/mem0) wird NICHT angefasst.
Aufruf: ~/.mem0/venv/bin/python ~/mission-control-v2/mem0_service/smoke_test.py
Exit 0 = alles grün, sonst 1.
"""
import os
import shutil
import sys
import tempfile
import time
# WICHTIG: Env vor dem Import von app setzen (app liest die Pfade beim Modul-Load).
_TMP = tempfile.mkdtemp(prefix="mem0_smoke_")
os.environ["MEM0_CHROMA_PATH"] = os.path.join(_TMP, "chroma")
os.environ["MEM0_HISTORY_DB"] = os.path.join(_TMP, "history.db")
os.environ["MEM0_COLLECTION"] = f"smoke_{int(time.time())}"
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import app
_fails: list[str] = []
def check(name: str, cond: bool, detail: str = "") -> None:
print(("PASS" if cond else "FAIL"), "", name, ("" if cond else f":: {detail}"))
if not cond:
_fails.append(name)
def main() -> int:
app.mem() # Memory bauen (öffnet Chroma, pingt Modelle)
# 1) Verbatim add + Rücklesen
r = app.add_memory(app.MemIn(content="Smoke: der Himmel ist blau.", category="knowledge", source="smoke"))
check("verbatim add + get", bool(r.get("id")) and "blau" in r.get("content", ""), str(r)[:160])
# 2) Embeddings / semantische Suche (Bedeutung, nicht Wort)
res = app.list_memory(q="Welche Farbe hat der Himmel?")
check("semantische Suche", any("blau" in i.get("content", "") for i in res), str(res)[:160])
# 3) Auto-Lernen: LLM-Extraktion (NoThink) + Deutsch erzwungen
lr = app.learn(app.LearnIn(text="Ich heiße Max und programmiere am liebsten in Go.", source="smoke"))
items = lr.get("results", [])
check("Auto-Lernen extrahiert Fakten", len(items) > 0, str(lr)[:160])
joined = " ".join((i.get("content") or "") for i in items).lower()
check("Extraktion auf Deutsch", any(w in joined for w in ("heißt", "programmiert", "mag", "bevorzugt", "name", "nutzt")), joined[:160])
# 4) Auto-Einordnung in die 4 gültigen Kategorien
cats = {i.get("category") for i in items if i.get("category")}
check("Auto-Einordnung gültige Kategorie", bool(cats) and cats <= set(app.CATEGORIES), str(cats))
# 5) Graph aus den Roh-Embeddings (Chroma-Interna)
g = app.graph()
check("Graph liefert Knoten", len(g.get("nodes", [])) >= 1, str({k: len(v) for k, v in g.items()}))
print("\n" + ("ALLE TESTS GRÜN ✓" if not _fails else f"FEHLGESCHLAGEN: {_fails}"))
return 0 if not _fails else 1
if __name__ == "__main__":
try:
code = main()
finally:
shutil.rmtree(_TMP, ignore_errors=True)
sys.exit(code)