fix(deploy): Mem0-Logik aus Backup/Restore/Postchecks/Venv-Audit/Hooks entfernt, mem0_service/ gelöscht
This commit is contained in:
@@ -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/.
|
||||||
|
|||||||
@@ -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
@@ -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,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
@@ -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/"; }
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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.
|
||||||
|
|||||||
@@ -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")))
|
|
||||||
@@ -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())
|
|
||||||
@@ -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
|
|
||||||
@@ -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)
|
|
||||||
Reference in New Issue
Block a user