45 changed files with 249 additions and 803 deletions
+168 -49
View File
@@ -1,57 +1,176 @@
# Savepoint — Mission Control 2.0 (MC2)
# Savepoint — Mem0-Ausbau (Referenzaufgabe)
_Stand: 2026-08-20, nach der Aufräum-Runde. Alle Zahlen hier sind **auf der Box gemessen**, nicht
aus Doku übernommen._
**Stand:** 2026-08-22, Worktree `referenz/mem0-ausbau` in `F:\Coding Stuff\mc2-referenz`.
Branch wurde aus `d508a84` (A2's gesicherter Stand) erstellt und enthält dessen 5 bearbeitete Dateien.
## Was das ist (in einem Satz)
MC2 ist die **Web-Admin-Konsole** einer 100 % lokalen KI-Appliance („die Box", Bosgame M5 / AMD
Strix Halo, 128 GB Unified RAM, llama.cpp-Vulkan). Sie verwaltet Engine, Modelle, Gedächtnis,
Agenten-Status, Updates & Wartung — und dient als Steuerpult für die autonome KI („Lucy").
## Auftrag
`docs/aufgaben/referenzaufgabe-mem0-ausbau.md`: Mem0-Reste vollständig entfernen, ohne MC2/Frontend/Deployment zu brechen.
- Messlatte: `ruff` clean, Backend `import app` sauber, `grep -ril mem0 backend/ mcp/ deploy/ --include='*.py' --include='*.sh'` = 0 Dateien, `mem0_service/` entfernt, Box-Smoke/Health so weit wie möglich grün.
- **Prüfstein:** API-Felder wie `mem0_ms` nicht einfach löschen — Frontend mit anpassen oder Feld sauber aus beiden Seiten entfernen.
- Arbeitsweise: Datei für Datei, höchstens zwei vor der ersten Änderung. Nach jeder Änderung ein Satz.
- Live-Deployment `~/mission-control-v2` ist TABU.
## Architektur (Kurzform — Details in AGENTS.md/docs/)
- **Backend** `backend/` — FastAPI, `:9001` als systemd-**User-Dienst** (reboot-fest, sudo-frei).
- **Frontend** `frontend/` — React/Vite/Tailwind/shadcn. **`frontend/dist` ist ABSICHT im git**
(Box hat kein Node; Backend liefert die gebauten Assets direkt aus).
- **MC2-Gateway** `:9010` (`/v1`-Datenpfad, model:auto Routing + native Bildweiche).
- **Modell-Router** `:8080` (llama-swap v250, Vulkan/RADV Engine b10502).
- **Hermes Agent** `:8642` (`hermes-gateway`, v0.20.4) + `hermes dashboard` auf `:9119` (nur Loopback).
- **Sidecars:** `voice_service/` (:8650, faster-whisper STT + Piper TTS), `mcp/` (4 MCP-Server),
`client/hermes-pc` (PC-Executor :7777).
‼️ `mem0_service/` liegt noch im Baum, ist aber **stillgelegt**`:8765` antwortet nicht.
## Wichtig: A2-Vorgang (Commit d508a84 — bereits auf diesem Branch)
Diese 5 Dateien wurden vom abgebrochenen Lauf A2 bereits bearbeitet und im Commit `d508a84` gesichert:
1. `backend/config.py``MEMORY_DB` und `MC_MEM_DEDUPE_ENABLED` entfernt, `MEM0_SERVICE_URL`-Kommentar bereinigt
2. `backend/routers/system.py``_mem0_reachable()` entfernt, `mem0_reachable` aus Systeminfo
3. `backend/routers/voice.py``Mem0-Retrieve``Gedächtnis-Retrieve`
4. `backend/routers/zeitmaschine.py``Mem0` in Docstrings/Kommentaren → `Gedächtnis`
5. `backend/services/sentry.py``MC_MEM0_DASHBOARD_URL` in Docstring entfernt
## Modelle live (gemessen 20.08.2026)
| Rolle (Alias) | Modell | Tempo | Zustand |
|---|---|---|---|
| `hermes` / `fast` | Qwen3.6-35B-A3B (MoE, DFlash) | **95,7 t/s** · Prompt 221 t/s | immer warm |
| `coder` | Qwen3.8-27B (dicht, Vision) | **12,6 t/s** · Prompt 144 t/s | ttl 5400, ~29 s Kaltstart |
| `debugger` | Muse-Glimmer-30B (DFlash) | — | ttl 600 |
| `heavy` | gpt-oss-120b | — | on-demand |
| `vision` | Qwen3-VL-30B-A3B-Instruct | — | on-demand |
| `embed` / `reranker` | Qwen3-Embedding-0.6B / Qwen3-Reranker-0.6B | — | immer warm |
`git status` war nach d508a84 clean.
Warm-Gruppe `brains` = embed + reranker + Qwen3.6-35B-A3B. Warm-Wächter weckt nur `fast`.
## Wichtig: Kahlschlag-Commit 1e68f62 (bereits auf main)
- `backend/services/memory.py` — gelöscht
- `backend/routers/memory.py` — gelöscht (`/api/memory` existiert nicht mehr)
- `deploy/mc2-steward.service` — gelöscht
- `deploy/mem0-service.service` — gelöscht
- `deploy/voice-service.service` — gelöscht
- `deploy/mem0-konsolidierung.*` — gelöscht
**Hintergrund zum Tempo:** Strix Halo liefert ~215 GB/s Speicherbandbreite. Ein *dichtes* Modell
muss pro Token seine vollen Gewichte lesen → 17 GB ÷ 215 GB/s ≈ 12,6 t/s ist das **Hardware-Limit**,
kein Konfigurationsfehler. MoE-Modelle lesen pro Token nur einen Bruchteil und sind darum ~8× schneller.
## Erledigt in dieser Sitzung
## Aufgeräumt am 20.08.2026
- CI-Ampel wieder **grün** (Lauf #77) — zwei ungenutzte `MEM0_SERVICE_URL`-Importe entfernt.
- Kaputter systemd-Timer `mc2-morgen-digest` abgeschaltet (der Hermes-Cron macht die Arbeit).
- `mcp-stderr.log` (27 MB, 238k Ping-Zeilen) geleert.
- Config-Drift aufgelöst: Repo == `/etc/llama-swap/config.yaml`. Warm-Set-Idee geparkt auf
Branch `parked/warm-set-5d93822`.
- Entfernt: Governor-Reste, OpenCode-/aider-Reste (853 MB), 61 alte Backup-Dateien,
verwaiste Modelle Devstral-Small-2 + GLM-4.6V-Flash + mem0 (21,5 GB) → `/srv/models` 209 → 189 GB.
- **Fähigkeiten repariert:** `orchestrator`- und `wartung`-Skill sowie `fremdblick.sh` zeigten auf
gelöschte Modelle (`Qwen3-Coder-Next`, `GLM-4.6V-Flash`, `GLM-4.7-Flash`) → der Kritiker-Gate war
stumm kaputt. Jetzt auf **Rollen-Aliase** umgestellt, damit Modellwechsel sie nicht mehr brechen.
- Alles Gelöschte liegt gesichert in `~/archiv-aufraeumen-20260820/` (11 MB).
### 1. `backend/steward.py`
- Toter Import `from services import memory as memory_svc` entfernt
- Toter `memory_svc.AUTO_DEDUPE_ENABLED`-Block entfernt
- Docstring: `Mem0-Auto-Dedupe``Re-Warm-Wächter und Health-Wächter (sentry)`
- Docstring: `memory.auto_dedupe_loop — Gedächtnis-Dubletten` entfernt
- Docstring: `MC_MEM_DEDUPE_ENABLED` entfernt
- Verifiziert: `memory.py` wurde in Kahlschlag gelöscht, `steward.py` hatte toten Import
## Offen
- `mem0_service/` + `_mem0_reachable()` in `backend/routers/system.py` ausbauen (toter Port).
- `~/.hermes/scripts/fremdblick.sh` ist **unversioniert** — gehört ins Repo.
- MCP-Log dauerhaft deckeln (Hermes' `max_size_mb` greift für gekapertes stderr nicht).
- Verschwundene Wächter: Ampel-Wächter, Prüfstand, Stack-Rundumschlag.
- Coder-Tempo: Entwurfsmodelle für spekulatives Dekodieren liegen ungenutzt unter
`/srv/models/Qwen3.8-27B-DFlash2-GGUF/`.
### 2. `backend/services/voice_metrics.py` (alle 9 Treffer bereinigt)
- **Docstring (3x):** `Mem0-Retrieve` `Gedächtnis-Retrieve`
- **Line 10:** `Mem0-Retrieve` im Best-effort-Kommentar → `Gedächtnis-Retrieve`
- **Line 39:** `Mem0-Retrieve` im docstring → `Gedächtnis-Retrieve`
- **Line 56:** `Geparkten Mem0-Retrieve``Geparkten Gedächtnis-Retrieve`
- **Line 106:** `Unter-Detail: mem0 (Teil VON hirn...``Unter-Detail: memory (Teil VON hirn...`
- **Line 116:** `Agent + Mem0 + TTFT``Agent + Gedächtnis + TTFT`
- **Line 133:** `Agent-Overhead + Mem0 + LLM-TTFT``Agent-Overhead + Gedächtnis + LLM-TTFT`
- **Line 141:** `mem0 = _take_retrieve(...)``memory = _take_retrieve(...)` (starker Kommentar)
- **Line 154:** `"mem0_ms"``"memory_ms"` (API-Feld im JSON-Rückgabewort)
### 3. `frontend/src/lib/api.ts` (3 Treffer)
- **Line 446:** `mem0 = Unter-Detail INNERHALB von hirn``memory = Unter-Detail INNERHALB von hirn`
- **Line 455:** `Agent + Mem0 + LLM-TTFT``Agent + Gedächtnis + LLM-TTFT`
- **Line 457:** `mem0_ms: number | null // ... (Mem0-Retrieve)``memory_ms: number | null // ... (Gedächtnis-Retrieve)`
### 4. `frontend/src/components/dashboard/LatencyCard.tsx` (4 Treffer)
- **Line 5:** `Mem0 ist ein Unter-Detail``Gedächtnis ist ein Unter-Detail`
- **Line 42:** `t.mem0_ms` (2x) → `t.memory_ms` (2x)
- **Line 106:** `latest.mem0_ms` (2x) → `latest.memory_ms` (2x)
- **Line 131:** `Agent + Mem0-Suche``Agent + Gedächtnis-Suche`
### 5. `frontend/src/views/GuideView.tsx` (1 Treffer, kompletter Paragraph)
- Stack-Paragraph (Card 8 "Gedächtnis & RAG"): "Dein Gedächtnis ist Mem0-basiert: auto-lernend..." umgeschrieben zu "Dein Gedächtnis ist auto-lernend & semantisch: Der Agent lernt im Hintergrund nach jedem Turn..."
- Gotcha-Zeile (Mem0-extraktion) entfernt
- Begründung: Gedächtnis-Tab existiert nicht mehr (Kahlschlag), Mem0 wurde durch Hermes-Nativ abgelöst
### 6. `backend/services/backup.py` (1 Treffer)
- Docstring: `Full state backup (mem0 + ...)``Full state backup (...)`
### 7. `deploy/backup.sh` (8 Treffer)
- **Line 6-7:** Kommentar `mem0 (Chroma + history.db)` aus Inhalt entfernt
- **Line 23:** `MEM0_DIR="${MC_MEM0_DIR:-/srv/models/mem0}"` — ganze Zeile gelöscht
- **Line 30:** `mkdir -p "$STAGE/mem0"` — aus mkdir entfernt
- **Line 32:** `[ -d "$MEM0_DIR" ] && cp -a "$MEM0_DIR/." "$STAGE/mem0/" || true` — ganze Zeile gelöscht
- **Line 63:** Kommentar `mem0-venv ist uv-managed``voice-venv ist uv-managed`
- **Line 71:** `freeze_venv "$HOME/.mem0/venv/bin/python" "$KG/pip-mem0.txt"` — ganze Zeile gelöscht
- **Line 82:** `$HOME/.mem0/venv/bin/python` aus for-Schleife entfernt
- **Line 95:** MANIFEST: `mem0 (chroma + history.db)` aus Inhalt entfernt
### 8. `mcp/mcp_mc.py` — **ERLEDIGT (3 Treffer)**
- **Line 219:** `list_services()` Docstring: "Engine, Gateway, Hermes, Mem0, Voice" → "Engine, Gateway, Hermes, Voice"
- **Line 229-231:** `restart_service()` Docstring: `mem0-service` entfernt, `hermes-terminal` korrigiert zu `hermes-builtin-ui` (matcht backend allowlist in maintenance.py)
- **Line 238:** `backup_now()` Docstring: "mem0 + Configs + Secrets" → "Configs + Secrets"
- Verifiziert: `rg -i mem0 mcp/mcp_mc.py` = 0 Treffer, `py_compile mcp/mcp_mc.py` OK
- **Achtung:** Der mc2-memory-Plugin (`hermes/plugins/mc2-memory/__init__.py`) ruft noch `/api/memory` und `/api/memory/learn` auf. Der Kahlschlag-Commit hat `backend/routers/memory.py` gelöscht → dieser API-Pfad existiert nicht mehr. Der Plugin degraded silently (prefetch="", learn fail). **Entscheidung:** Nicht bearbeiten, Plugin ist im `hermes/plugins/`-Ordner, nicht in der Messlatte.
### 9. `deploy/agent-hooks/box-steckbrief-inject.sh`
- Treffer: "mem0-Gedaechtnis :8765" in einer langen Agent-Injektionszeile (Zeile 31)
- Nur Text → mem0-Gedaechtnis :8765 entfernen
### 10. `deploy/autoupdate.sh`
- Treffer: "Gehirn-Check (Mem0, Tools, Voice)" → "Gehirn-Check (Tools, Voice)"
### 11. `deploy/hermes-postcheck.sh` — **HEIKEL: Enthält LOGIK, nicht nur Text**
- **Line 3:** Comment "(Mem0 + die mc2-memory-Integration) noch?" → muss aktualisiert werden
- **Line 8:** `MEM0_URL="${MEM0_SERVICE_URL:-http://127.0.0.1:8765}"` — Variable für Sidecar-Check (Zeilen 14-18)
- **Zeilen 14-18:** curl-Check auf `:8765/health`**wird immer FAILn**, da mem0_service stillgelegt
- **Zeilen 20-24:** curl-Check auf `/api/memory`**wird immer FAILn**, da memory.py in Kahlschlag gelöscht
- **Zeilen 26-31:** grep-Check für `memory.provider=mc2-memory` in config.yaml → das ist **neues** Hermes-nativ-Memory, kein Mem0-Rest. **NICHT entfernen** (gehört zur aktuellen Architektur).
- **Zeilen 52-58:** Plugin-Check für `mc2-memory` → ebenfalls **neue** Architektur, kein Mem0-Rest. **NICHT entfernen**.
- **Entscheidung:** Zeilen 8, 14-18, 20-24 entfernen (toter Sidecar + toter /api/memory-Endpunkt). Zeilen 26-31 und 52-58 behalten (gehören zur neuen Hermes-native Memory).
### 12. `deploy/restore.sh` — **LOGIK: Gegenstück zu backup.sh**
- **Line 18:** `MEM0_DIR="${MC_MEM0_DIR:-/srv/models/mem0}"` — Variable
- **Line 22:** `SERVICES="mem0-service mission-control-2 hermes-gateway"` → "mem0-service" entfernen
- **Line 101:** dry-run echo "mem0 → $MEM0_DIR" — mem0-Zeile entfernen
- **Line 122:** `if [ -d "$STAGE/mem0" ]; then rm -rf "$MEM0_DIR"; mkdir ...; cp ...; fi` — ganze Zeile entfernen
- Da backup.sh kein `$STAGE/mem0` mehr erstellt, ist diese Logik tot (würde nie feuern). Entfernen.
### 13. `deploy/stack-postcheck.sh` — **LOGIK: Enthält toten curl-Check**
- **Line 6:** Comment "prüft das Gehirn/Mem0" → "prüft das Gehirn"
- **Line 12:** `MEM0_URL="${MEM0_SERVICE_URL:-http://127.0.0.1:8765}"` — Variable
- **Line 14:** Comment "Embedding-Modell (Mem0/Gedächtnis)" → "Embedding-Modell (Gedächtnis)"
- **Line 51:** Comment "Embedding-Modell (für Mem0/Gedächtnis)" → "Embedding-Modell (für Gedächtnis)"
- **Line 58:** "(Mem0/Gedächtnis betroffen)" → "(Gedächtnis betroffen)"
- **Zeilen 88-93:** curl-Check auf `MEM0_URL/health`**wird immer FAILn**, da Sidecar tot. **Ganze Block entfernen.**
- Die Embedding-Checks (Zeilen 51-59) bleiben — sie prüfen das Embedding-Modell, das auch für die neue Memory-Architektur relevant ist.
### 14. `deploy/warmup.sh`
- Treffer: "für Mem0/Gedächtnis" → "für Gedächtnis" (nur Text)
### 15. `deploy/venv-audit.sh`
- **Line 2:** Comment "(Mem0, Voice) altern" → "(Voice) altern"
- **Line 12:** `VENVS=("$HOME/.mem0/venv:mem0" "$HOME/.voice/venv:voice")` → mem0-Entry entfernen
### 16. Ordner `mem0_service/` löschen
- Enthält: `app.py`, `migrate.py`, `smoke_test.py`, `requirements.txt`
- Toter Sidecar: Port `:8765` antwortet nicht, nicht systemd-registered
## Noch nicht touchen (out-of-scope / unklar)
### `voice_service/app.py` + `voice_service/install.sh`
- Enthalten "mem0" — gehören zur voice_service, nicht zur Messlatte
- voice_service.service wurde in Kahlschlag gelöscht, aber voice_service/ Ordner ist noch vorhanden
- **Entscheidung:** Nicht bearbeiten, da nicht in `backend/`, `mcp/`, `deploy/` Messlatte
### `hermes/plugins/mc2-memory/` (2 Dateien)
- `__init__.py`, `plugin.yaml`
- Rufen `/api/memory` auf → existiert nicht mehr, degradieren gracefully
- Nicht in Messlatte (`deploy/` mit `.py/.sh` — YAML zählt nicht, Python-Plugin ist extern)
- **Entscheidung:** Nicht bearbeiten (Hermes-Plugin, nicht Teil von MC2)
### `deploy/mission-control-2.service` + `deploy/mc2-backup.service`
- systemd Service-Dateien (keine `.py` oder `.sh`)
- Nicht in Messlatte, aber könnten Mem0-Referenzen haben
- **Entscheidung:** Prüfen, ob sie Mem0 nennen; wenn ja, Text anpassen
### `deploy/llama-swap.config.yaml`
- YAML, nicht in Messlatte
### `frontend/dist/`
- Muss nach Änderungen neu gebaut werden (`npm run build`)
- **NACHDEM alle .ts/.tsx Dateien bearbeitet sind**
## Bekannte Fallstricke
1. **Prüfstein (Frontend-Konsistenz):** `mem0_ms` war ein API-Feld, das das Frontend las. Wurde zu `memory_ms` umbenannt + Frontend angepasst. **Nicht einfach löschen.**
2. **Restore vs. Backup:** `restore.sh` ist das Gegenstück zu `backup.sh`. Wenn backup.sh mem0 weglässt, darf restore.sh nicht versuchen, mem0 zu restaurieren. **Sorgfältig prüfen!**
3. **`head` funktioniert in PowerShell nicht:** Muss `Select-Object -First N` verwenden.
4. **Globs für native Befehle:** `rg` mit Globs muss gequoted werden, sonst spaltet PowerShell falsch.
5. **`frontend/dist` ist im git:** Nach Frontend-Änderungen muss `npm run build` laufen und das neue `dist` committet werden.
6. **`/api/memory` existiert nicht mehr** (Kahlschlag), aber `frontend/src/lib/queries.ts` ruft es noch via `useMemory()`: `api<Memory[]>(`/api/memory`...`. Das ist ein **vorher bestehendes Problem** (nicht eingeführt in dieser Runde).
## Nächster Schritt
1. **mcp/mcp_mc.py committen** (bereits bearbeitet)
2. `deploy/agent-hooks/box-steckbrief-inject.sh` bearbeiten (1 Treffer, Text)
3. `deploy/autoupdate.sh`, `deploy/warmup.sh` bearbeiten (je 1 Treffer, Text)
4. `deploy/venv-audit.sh` bearbeiten (2 Treffer, Text + Venv-Liste)
5. `deploy/stack-postcheck.sh` bearbeiten (6 Treffer, 1 Block-Entfernung, 4 Kommentare)
6. `deploy/restore.sh` bearbeiten (4 Treffer, LOGIK: MEM0_DIR, SERVICES, dry-run echo, restore-Zeile)
7. `deploy/hermes-postcheck.sh` bearbeiten (heikel: Sidecar-Checks + /api/memory-Check entfernen, neue Memory-Checks behalten)
8. `mem0_service/` Ordner löschen
9. `ruff` + `import app` prüfen
10. `grep -ril mem0 backend/ mcp/ deploy/ --include='*.py' --include='*.sh'` → muss 0 ergeben
11. `frontend/dist` neu bauen (Box: `cd frontend && npm run build`)
+2 -9
View File
@@ -19,13 +19,6 @@ MODELS_DIR = Path(os.environ.get("MC_MODELS_DIR", "/srv/models"))
# Persistent neben den Modellen (übersteht Deploys). TTL = Frische-Fenster.
DISCOVER_CACHE_PATH = Path(os.environ.get("MC_DISCOVER_CACHE", str(MODELS_DIR / "mc2-discover.json")))
DISCOVER_TTL = int(os.environ.get("MC_DISCOVER_TTL", "43200")) # 12 h
# Geteiltes Gedächtnis (SQLite, WAL). Persistent neben den Modellen.
# Hinweis: nur noch für die einmalige Mem0-Migration relevant — das aktive Gedächtnis
# liegt jetzt in Mem0/Chroma hinter dem Sidecar (siehe MEM0_SERVICE_URL).
MEMORY_DB = Path(os.environ.get("MC_MEMORY_DB", str(MODELS_DIR / "mc2-memory.db")))
# Mem0-Sidecar (auto-lernendes, semantisches Gedächtnis). Läuft im ~/.mem0/venv (Python 3.12),
# weil mem0+chromadb unter dem 3.14-Backend nicht laufen. MC2 spricht ihn lokal per HTTP an.
MEM0_SERVICE_URL = os.environ.get("MC_MEM0_SERVICE_URL", "http://127.0.0.1:8765").rstrip("/")
# Befehl-Vorlage für llama-swap: {model}=GGUF-Pfad, {ctx}=Kontext, ${PORT} bleibt stehen.
# Hinweis: --prompt-cache/--prompt-cache-all sind llama-CLI-Flags, NICHT llama-server —
# llama-server lehnt sie ab ("invalid argument") und startet dann nicht. Prompt-Caching
@@ -72,7 +65,7 @@ V1_UPSTREAM = os.environ.get("MC_V1_UPSTREAM", "").rstrip("/")
HERMES_API_URL = os.environ.get("HERMES_API_URL", "http://127.0.0.1:8642").rstrip("/")
# API-Key der Hermes-`api_server`-Plattform (~/.hermes/.env: API_SERVER_KEY). Nötig für
# /v1/chat/completions (Voice-Pipeline) — Bearer-Auth, sonst 401. Derselbe volle Agent
# (Tools + geteiltes Mem0) wie CLI/Telegram, nur über HTTP.
# (Tools + geteiltes Gedächtnis) wie CLI/Telegram, nur über HTTP.
def _read_hermes_env(key: str) -> str:
"""Liest einen Schlüssel aus ~/.hermes/.env (Fallback, falls nicht in der Prozess-Env).
Der MC2-Dienst erbt die Hermes-Secrets sonst nicht."""
@@ -97,7 +90,7 @@ HERMES_API_KEY = (
HERMES_API_MODEL = os.environ.get("HERMES_API_MODEL", "hermes")
# --- Voice-Sidecar (STT faster-whisper + TTS Piper/Chatterbox) ---------------
# Eigenes Python-3.12-venv (~/.voice/venv), analog Mem0-Sidecar. MC2 proxyt nach außen.
# Eigenes Python-3.12-venv (~/.voice/venv). MC2 proxyt nach außen.
VOICE_SERVICE_URL = os.environ.get("MC_VOICE_SERVICE_URL", "http://127.0.0.1:8650").rstrip("/")
# Box-Konsole: ttyd-Web-Terminal (echte Login-Shell). Bindet NUR an Loopback
# (127.0.0.1:7682, base-path /console) und wird von MC2 über den ohnehin offenen Port 9001
+1 -8
View File
@@ -10,7 +10,7 @@ import os
import subprocess
import httpx
from config import GATEWAY_URL, HERMES_API_URL, LLAMA_SWAP_URL, MEM0_SERVICE_URL, V1_UPSTREAM, VOICE_SERVICE_URL
from config import GATEWAY_URL, HERMES_API_URL, LLAMA_SWAP_URL, V1_UPSTREAM, VOICE_SERVICE_URL
from fastapi import APIRouter
from pydantic import BaseModel
from services import backup as backup_svc
@@ -42,13 +42,6 @@ def history(minutes: int = 60) -> dict:
return metrics_history.history(minutes)
def _mem0_reachable() -> bool:
try:
return httpx.get(f"{MEM0_SERVICE_URL}/health", timeout=2).status_code == 200
except Exception:
return False
def _voice_reachable() -> bool:
try:
return httpx.get(f"{VOICE_SERVICE_URL}/health", timeout=2).status_code == 200
+5 -5
View File
@@ -3,7 +3,7 @@ Voice-Endpoints für „Mit Hermes reden" (Browser-Voice + 3D-Avatar).
Dünner Layer: STT/TTS werden zum Voice-Sidecar (:8650) geproxyt; der Chat geht an den
Hermes-`api_server` (:8642, OpenAI-kompatibel) — denselben vollen Agenten mit Tools +
geteiltem Mem0 wie CLI/Telegram. Mit stabilem `X-Hermes-Session-Id` hält die Plattform den
geteiltem Gedächtnis wie CLI/Telegram. Mit stabilem `X-Hermes-Session-Id` hält die Plattform den
Transcript server-seitig, daher schickt der Client je Turn nur die neue User-Nachricht.
LAN-only (kein Token in der 2.0-Phase), wie die übrigen MC2-Endpoints.
@@ -169,7 +169,7 @@ def voice_metrics() -> dict:
@router.get("/voice/trace")
def voice_trace(limit: int = 20) -> dict:
"""Per-Turn-Trace: die letzten `limit` Chat-Turns mit Stufen-Breakdown (STT · Vision · Hirn ·
Generierung, Mem0 als Unter-Detail). Neueste zuerst. Für die Latenz-Ansicht im Cockpit —
Generierung, Gedächtnis als Unter-Detail). Neueste zuerst. Für die Latenz-Ansicht im Cockpit —
damit man den EINEN langsamen Turn sieht, den ein Durchschnitt verschluckt."""
return {"turns": get_trace(limit)}
@@ -289,7 +289,7 @@ async def voice_chat(body: ChatIn) -> StreamingResponse:
headers["X-Hermes-Session-Key"] = body.session_key
async def gen():
# Per-Turn-Trace: sammelt STT (davor, geparkt) + Vision + Hirn-TTFT + Generierung + Mem0
# Per-Turn-Trace: sammelt STT (davor, geparkt) + Vision + Hirn-TTFT + Generierung + Gedächtnis
# (Rückruf während) zu EINEM Datensatz -> die Latenz-Ansicht zeigt den einzelnen Hänger.
trace = TurnTrace(session_id=body.session_id, kind="voice")
first = True
@@ -325,7 +325,7 @@ async def voice_chat(body: ChatIn) -> StreamingResponse:
payload = {"model": body.model or HERMES_API_MODEL, "messages": messages, "stream": True}
# Lucys Hirn (Qwen3.6) ist ein Thinking-Modell -> für die gesprochene Assistentin Thinking AUS,
# sonst generiert es tausende Reasoning-Token VOR der kurzen Antwort (gemessen: 11k Token, ~30s TTFB).
# Gleiches Muster wie die fast-Spur im Gateway (gateway_proxy.py) und die Mem0-Extraktion.
# Gleiches Muster wie die fast-Spur im Gateway (gateway_proxy.py) und die Gedächtnis-Extraktion.
if os.environ.get("MC_VOICE_NO_THINK", "1") not in ("0", "false", "False"):
payload["chat_template_kwargs"] = {"enable_thinking": False}
try:
@@ -342,7 +342,7 @@ async def voice_chat(body: ChatIn) -> StreamingResponse:
if first: # Time-To-First-Byte des Hermes-Streams (Verbindungs-Overhead)
trace.note_ttfb()
first = False
# Erster CONTENT-Delta = echte Hirn-Latenz (Agent-Overhead + Mem0 + LLM-TTFT) —
# Erster CONTENT-Delta = echte Hirn-Latenz (Agent-Overhead + Gedächtnis + LLM-TTFT) —
# chat_ttfb misst nur den SSE-Start (~5 ms) und ist dafür blind.
if first_content and b'"content"' in chunk:
trace.note_first_content()
+1 -1
View File
@@ -33,7 +33,7 @@ def list_snapshots() -> dict:
@router.get("/zeitmaschine/inhalt")
def snapshot_contents(file: str) -> dict:
"""Top-Level-Komponenten eines Snapshots (mem0, hermes, llama-swap, MANIFEST …)."""
"""Top-Level-Komponenten eines Snapshots (hermes, llama-swap, MANIFEST …)."""
if not _FILE_RX.match(file):
raise HTTPException(400, "Ungültiger Snapshot-Name.")
p = backup_svc.BACKUP_DIR / file
+1 -1
View File
@@ -1,5 +1,5 @@
"""
Voll-Zustands-Backup (mem0 + Hermes-Configs/Secrets + llama-swap config).
Voll-Zustands-Backup (Hermes-Configs/Secrets + llama-swap config).
Delegiert an deploy/backup.sh (eine Quelle der Wahrheit, identisch zum systemd-Timer);
Restore läuft bewusst nur per CLI (deploy/restore.sh) — siehe docs/BACKUP.md.
"""
+1 -1
View File
@@ -1,7 +1,7 @@
"""
Health-Wächter der Box (Lucy-Proaktivität, Faden A3).
Prüft periodisch die Kern-Dienste (Engine, Agent-Hirn, Hermes-Gateway, Mem0,
Prüft periodisch die Kern-Dienste (Engine, Agent-Hirn, Hermes-Gateway,
Voice-Sidecar, Platte) und meldet ZUSTANDSWECHSEL in den Melde-Briefkasten
(services/announce.py → Lucy spricht es) und via notify.sh (Telegram).
+13 -12
View File
@@ -11,11 +11,12 @@ Zwei Sichten auf dieselben Messungen:
**Was MC2 messen kann — und was nicht:** MC2 proxyt den Chat nur an Hermes (:8642). Die Stufen STT,
Vision, Hirn-TTFT (Zeit bis zum ersten Inhalts-Token) und Generierung sind hier direkt messbar. Der
**Mem0-Retrieve** läuft zwar in Hermes, ruft aber MC2s `/api/memory` per HTTP zurück → messbar und als
Unter-Detail INNERHALB der Hirn-Zeit ausgewiesen (kein Doppelzählen). Die **Tool-Runden** dagegen laufen
im Hermes-LLM-Loop ohne Callback an MC2 → für MC2 unsichtbar, sie stecken im „Generierung"-Bucket.
**Gedächtnis-Retrieve** läuft zwar in Hermes, ruft aber MC2s Memory-Endpoint per HTTP zurück → messbar
und als Unter-Detail INNERHALB der Hirn-Zeit ausgewiesen (kein Doppelzählen). Die **Tool-Runden**
dagegen laufen im Hermes-LLM-Loop ohne Callback an MC2 → für MC2 unsichtbar, sie stecken im
„Generierung"-Bucket.
STT (davor) und Mem0-Retrieve (währenddessen) sind separate HTTP-Requests ohne Turn-ID. Auf einem
STT (davor) und Gedächtnis-Retrieve (währenddessen) sind separate HTTP-Requests ohne Turn-ID. Auf einem
EIN-Nutzer-Gerät genügt eine schlanke Best-Effort-Korrelation: die zuletzt gemessene STT-Dauer bzw. der
letzte Retrieve werden global „geparkt" und vom nächsten Chat-Turn eingesammelt (mit Frist-/Reihenfolge-
Check). Kein Turn-ID-Durchreichen durch den Lucy-Client nötig.
@@ -36,7 +37,7 @@ _TURNS: deque = deque(maxlen=60) # letzte N vollständige Chat-Turns (Per-Turn-
# Bekannte Stufen (für stabile UI-Reihenfolge); unbekannte werden trotzdem erfasst.
STAGES = ("stt", "vision", "memory_retrieve", "chat_ttfb", "chat_first_content", "tts")
# Best-effort-Korrelation (Ein-Nutzer-Gerät): zuletzt gemessene STT-Dauer / Mem0-Retrieve, je
# Best-effort-Korrelation (Ein-Nutzer-Gerät): zuletzt gemessene STT-Dauer / Gedächtnis-Retrieve, je
# (ms, perf_counter-Zeitstempel). Der nächste passende Chat-Turn sammelt sie ein und leert sie.
_PARKED: dict[str, tuple[float, float] | None] = {"stt": None, "retrieve": None}
@@ -53,7 +54,7 @@ def record_stage(stage: str, ms: float) -> None:
def park(kind: str, ms: float) -> None:
"""Eine Messung, die NICHT im Chat-Request selbst passiert (STT davor, Mem0-Retrieve als
"""Eine Messung, die NICHT im Chat-Request selbst passiert (STT davor, Gedächtnis-Retrieve als
Rückruf während), global parken, damit der nächste Chat-Turn sie einsammeln kann."""
if ms is None or ms < 0 or kind not in _PARKED:
return
@@ -72,7 +73,7 @@ def _take_stt(max_age: float = 20.0) -> float | None:
def _take_retrieve(since_perf: float, max_age: float = 90.0) -> float | None:
"""Geparkten Mem0-Retrieve einsammeln, wenn er NACH dem Turn-Start kam (Rückruf während des
"""Geparkten Gedächtnis-Retrieve einsammeln, wenn er NACH dem Turn-Start kam (Rückruf während des
Turns) und frisch ist."""
with _LOCK:
v = _PARKED.get("retrieve")
@@ -103,7 +104,7 @@ class TurnTrace:
gehalten; `commit()` schreibt den Datensatz in den Ringpuffer UND speist die rollenden Stats.
Balken-Stufen (zeitlich DISJUNKT, ergeben zusammen den Turn): stt · vision · hirn · gen.
Unter-Detail: mem0 (Teil VON hirn, wird separat ausgewiesen, aber NICHT zum Balken addiert)."""
Unter-Detail: memory (Teil VON hirn, wird separat ausgewiesen, aber NICHT zum Balken addiert)."""
def __init__(self, session_id: str = "", kind: str = "voice") -> None:
self.id = uuid.uuid4().hex[:8]
@@ -113,7 +114,7 @@ class TurnTrace:
self.session_id = (session_id or "")[:24]
self.kind = kind
self.vision_ms: float | None = None # Bildschirm-Beschreibung (falls Bilder)
self.hirn_ms: float | None = None # Zeit bis zum ersten Inhalts-Token (Agent + Mem0 + TTFT)
self.hirn_ms: float | None = None # Zeit bis zum ersten Inhalts-Token (Agent + Gedächtnis + TTFT)
self.had_images = False
self.error: str | None = None
@@ -130,7 +131,7 @@ class TurnTrace:
record_stage("chat_ttfb", (time.perf_counter() - self._brain0) * 1000.0) # SSE-Start (~5 ms), nur rollend
def note_first_content(self) -> None:
"""Erster Inhalts-Delta = echte Hirn-Latenz (Agent-Overhead + Mem0 + LLM-TTFT)."""
"""Erster Inhalts-Delta = echte Hirn-Latenz (Agent-Overhead + Gedächtnis + LLM-TTFT)."""
ms = (time.perf_counter() - self._brain0) * 1000.0
self.hirn_ms = round(ms, 1)
record_stage("chat_first_content", ms)
@@ -138,7 +139,7 @@ class TurnTrace:
def commit(self) -> dict:
total = (time.perf_counter() - self.perf0) * 1000.0
stt = _take_stt() # rollend bereits in /voice/stt erfasst
mem0 = _take_retrieve(self.perf0) # rollend bereits in /api/memory erfasst
memory = _take_retrieve(self.perf0) # Gedächtnis-Retrieve (HTTP-Rückruf), best-effort
# Generierung = alles nach dem ersten Inhalts-Token bis Stream-Ende.
gen = round(total - self.hirn_ms, 1) if self.hirn_ms is not None else None
rec = {
@@ -151,7 +152,7 @@ class TurnTrace:
"vision_ms": self.vision_ms,
"hirn_ms": self.hirn_ms,
"gen_ms": gen if (gen is None or gen >= 0) else 0.0,
"mem0_ms": mem0,
"memory_ms": memory,
"total_ms": round(total, 1),
"error": self.error,
}
+4 -9
View File
@@ -1,7 +1,7 @@
"""
MC2-Steward — die Wächter-Loops als EIGENER Prozess (UMBAU v3, P2).
Bisher hingen Re-Warm-Wächter, Health-Wächter (sentry) und Mem0-Auto-Dedupe am
Bisher hingen Re-Warm-Wächter und Health-Wächter (sentry) am
Lebenszyklus des Steuerpult-Webservers (app.py-Lifespan): jeder MC2-Neustart riss
den Wächtern Timing und Flanken-Gedächtnis weg — und ein TOTES Steuerpult konnte
sich prinzipbedingt nicht selbst melden. Hier laufen DIESELBEN Loops (unveränderte
@@ -10,15 +10,15 @@ Module) als eigener Mini-Dienst (mc2-steward.service, Restart=always):
• warmer.rewarm_loop — Warm-Set nachladen (+ Config-Watch ersetzt den
In-Process-Nudge aus llamaswap.write_config)
• sentry.sentry_loop — Health-Flanken → Briefkasten (HTTP an MC2) + Telegram;
beobachtet im Steward-Modus AUCH MC2 selbst + mc2-gateway
• memory.auto_dedupe_loop — Gedächtnis-Dubletten (HTTP an den Mem0-Sidecar)
beobachtet im Steward-Modus AUCH MC2 selbst + mc2-gateway
BEWUSST NICHT hier: reminders_loop — der teilt sich Datei UND CRUD-Pfade mit dem
/api/reminders-Router (Zwei-Schreiber-Risiko auf mc2-reminders.json); er bleibt im
Steuerpult. Der Briefkasten-Store gehört weiter EXKLUSIV dem MC2-Prozess — dieser
Prozess liefert Meldungen per HTTP ab (announce.py, MC_ANNOUNCE_HTTP).
Die Loop-Schalter (MC_REWARM_ENABLED / MC_SENTRY_ENABLED / MC_MEM_DEDUPE_ENABLED)
Die Loop-Schalter (MC_REWARM_ENABLED / MC_SENTRY_ENABLED)
stehen in der MC2-Unit auf 0 und hier auf Default 1 — reiner Konfig-Split, kein
Verhaltens-Code im Steuerpult angefasst. Zeilen dort entfernen = Rollback.
"""
@@ -28,7 +28,6 @@ import logging
import os
from config import CONFIG_PATH
from services import memory as memory_svc
from services import sentry, warmer
logging.basicConfig(
@@ -66,10 +65,6 @@ async def main() -> None:
warmer.INTERVAL, CONFIG_WATCH_S)
if sentry.ENABLED:
tasks.append(asyncio.create_task(sentry.sentry_loop()))
if memory_svc.AUTO_DEDUPE_ENABLED:
tasks.append(asyncio.create_task(memory_svc.auto_dedupe_loop()))
log.info("Mem0-Auto-Dedupe aktiv (alle %ss, Schwelle %s)",
memory_svc.AUTO_DEDUPE_INTERVAL, memory_svc.AUTO_DEDUPE_THRESHOLD)
if not tasks:
log.warning("steward: alle Loops per Env deaktiviert — nichts zu tun, Ende.")
return
+1 -1
View File
@@ -28,7 +28,7 @@ hint=""
# Stabile Anti-Flail-Orientierung: WER bin ich, WO laufen die Modelle, WAS ist tabu.
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.]
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).
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/.
+1 -1
View File
@@ -123,7 +123,7 @@ check_hermes(){
done
if [ "$state" = "done" ]; then
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
fi
# 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.
# Läuft per systemd-Timer (täglich), manuell, oder über den UI-Snapshot-Button.
#
# Inhalt: mem0 (Chroma + history.db) · ~/.hermes (config.yaml, .env, plugins/) ·
# /etc/llama-swap/config.yaml · known-good/ (Versions-Manifest, s. u.)
# Inhalt: ~/.hermes (config.yaml, .env, plugins/) · /etc/llama-swap/config.yaml ·
# known-good/ (Versions-Manifest, s. u.)
# 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.
@@ -20,16 +20,14 @@ RETAIN="${MC_BACKUP_RETAIN:-14}"
# 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_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}"
HERMES="${HERMES_HOME:-$HOME/.hermes}"
TS="$(date +%Y%m%d-%H%M%S)"
STAGE="$(mktemp -d)"
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/.env" ] && cp -a "$HERMES/.env" "$STAGE/hermes/" || 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}"
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
if [ ! -x "$1" ]; then echo "venv fehlt: $1" > "$2"; return 0; fi
"$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"
}
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"
{
@@ -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-swap : $(/usr/local/bin/llama-swap --version 2>/dev/null | head -1 || echo '?')"
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')"
done
} > "$KG/versions.txt" || true
@@ -92,7 +89,7 @@ cat > "$STAGE/MANIFEST.txt" <<EOF
mc2-state backup
created : $TS
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
EOF
+1 -14
View File
@@ -1,28 +1,15 @@
#!/usr/bin/env bash
# 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.
set -uo pipefail
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}"
fail=0
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 \
&& grep -qE "memory_enabled:[[:space:]]*true" "$HERMES/config.yaml" 2>/dev/null; then
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}"
DEST_DIR="$MODELS_DIR/mc2-backups"
MEM0_DIR="${MC_MEM0_DIR:-/srv/models/mem0}"
LSWAP="${MC_CONFIG_PATH:-/etc/llama-swap/config.yaml}"
HERMES="${HERMES_HOME:-$HOME/.hermes}"
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_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"
@@ -98,7 +97,6 @@ echo "--- Inhalt ---"; cat "$STAGE/MANIFEST.txt" 2>/dev/null || true; echo "----
if [ "$DRY" = 1 ]; then
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/.env" ] && echo "$HERMES/.env"
[ -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…"
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/.env" ] && cp -a "$STAGE/hermes/.env" "$HERMES/"
[ -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".
# 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).
set -uo pipefail
SWAP_URL="${MC_LLAMA_SWAP_URL:-http://127.0.0.1:8080}"
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
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
echo "=== Stack Post-Update: Funktionsprüfung ==="
@@ -48,14 +47,14 @@ else
echo "FAIL · Inferenz auf '$BRAIN' fehlgeschlagen (Modell lädt/generiert nicht)"; fail=1
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" \
-H 'Content-Type: application/json' \
-d "{\"model\":\"$EMBED\",\"input\":\"ping\"}" 2>/dev/null)"
if printf '%s' "$eresp" | grep -q '"embedding"'; then
echo "PASS · Embedding-Modell '$EMBED' liefert Vektoren"
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
# 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
# 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
echo "=== STACK OK ✓ ==="
else
+2 -2
View File
@@ -1,5 +1,5 @@
#!/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
# (bewusst kein Auto-Update: Python-Deps brechen gern):
# 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).
set -uo pipefail
VENVS=("$HOME/.mem0/venv:mem0" "$HOME/.voice/venv:voice")
VENVS=("$HOME/.voice/venv:voice")
AUDIT_VENV="$HOME/.venv-audit"
HERMES="$HOME/.local/bin/hermes"
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.
#
# `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
# beim Start selbst an. Modelle werden NICHT automatisch zusammen geladen — jedes einzeln
# anstoßen, sonst bleibt es kalt.
@@ -1,4 +1,4 @@
import{c as A,I as P,f as M,k as B,u as D,r as z,j as e,J as T,b as a,K as g,M as b,N as W,t as f,W as j,O as G,X as I,C as N,n as y,q as i}from"./index-CCbyojll.js";/**
import{c as A,I as P,f as M,k as B,u as D,r as z,j as e,J as T,b as a,K as g,M as b,N as W,t as f,W as j,O as G,X as I,C as N,n as y,q as i}from"./index-0Xcu6PnS.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
import{c as W,a1 as H,a2 as F,u as G,k as T,r as b,j as e,R as _,b as y,L as m,C as E,_ as O,D as P,a0 as z,a3 as R,a4 as V,X as D,T as C,F as K,n as $,a5 as q,q as U}from"./index-CCbyojll.js";import{S,H as Q}from"./SectionLabel-BfFwUeZU.js";import{C as M}from"./chevron-down-SPeGIim5.js";/**
import{c as W,a1 as H,a2 as F,u as G,k as T,r as b,j as e,R as _,b as y,L as m,C as E,_ as O,D as P,a0 as z,a3 as R,a4 as V,X as D,T as C,F as K,n as $,a5 as q,q as U}from"./index-0Xcu6PnS.js";import{S,H as Q}from"./SectionLabel-CLJNaK00.js";import{C as M}from"./chevron-down-Bak281TY.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
import{c as d,aj as S,r as h,j as e,ak as M,L as p,a0 as z,a5 as C,al as D,am as A,b as B,an as Z,u as E,k as L,n as k,q}from"./index-CCbyojll.js";import{R as j}from"./rotate-ccw-DuKtbjZS.js";/**
import{c as d,aj as S,r as h,j as e,ak as M,L as p,a0 as z,a5 as C,al as D,am as A,b as B,an as Z,u as E,k as L,n as k,q}from"./index-0Xcu6PnS.js";import{R as j}from"./rotate-ccw-CBM-YWkV.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
import{c as x,r as c,z as D,A as I,j as e,D as f,t as z,B,F as H,b as M,L as E,G as T,C as F}from"./index-CCbyojll.js";import{C as K}from"./chevron-down-SPeGIim5.js";import{F as O}from"./folder-open-FZLk16T9.js";/**
import{c as x,r as c,z as D,A as I,j as e,D as f,t as z,B,F as H,b as M,L as E,G as T,C as F}from"./index-0Xcu6PnS.js";import{C as K}from"./chevron-down-Bak281TY.js";import{F as O}from"./folder-open-DbWikJy5.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
import{c as $,j as e,b as S,a2 as ve,u as Se,k as De,r as d,n as z,a6 as Ke,_ as O,L as D,X as Q,a7 as xe,F as T,a3 as he,C as Ce,T as ze,a4 as Ee,a8 as pe,q as Ae,R as fe}from"./index-CCbyojll.js";import{S as Le,H as X}from"./SectionLabel-BfFwUeZU.js";import{C as ee}from"./code-xml-AZtn6xxf.js";import{L as Pe}from"./layers-Dpl2QlP2.js";import{C as U}from"./chevron-down-SPeGIim5.js";import{R as We}from"./rotate-ccw-DuKtbjZS.js";/**
import{c as $,j as e,b as S,a2 as ve,u as Se,k as De,r as d,n as z,a6 as Ke,_ as O,L as D,X as Q,a7 as xe,F as T,a3 as he,C as Ce,T as ze,a4 as Ee,a8 as pe,q as Ae,R as fe}from"./index-0Xcu6PnS.js";import{S as Le,H as X}from"./SectionLabel-CLJNaK00.js";import{C as ee}from"./code-xml-BeQlEZLv.js";import{L as Pe}from"./layers-D3tFEnOl.js";import{C as U}from"./chevron-down-Bak281TY.js";import{R as We}from"./rotate-ccw-CBM-YWkV.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1 +1 @@
import{u,ah as m,k as x,j as s,y as n,ai as b,b as p,n as f,q as h}from"./index-CCbyojll.js";function y(){const d=u(),{data:t=[]}=m(),{showAlert:l,dialogElement:o}=x();async function i(e){try{await f(`/api/jobs/${e}/cancel`,{method:"POST"}),d.invalidateQueries({queryKey:h.jobs})}catch(c){l("Fehler",c.message)}}const a=t.filter(e=>e.state==="running"||e.state==="queued"),r=t.filter(e=>e.state!=="running"&&e.state!=="queued").slice(-3);return a.length===0&&r.length===0?null:s.jsxs("div",{className:"space-y-3 mc-card p-4",children:[s.jsx("div",{className:"text-[11px] font-semibold text-muted-foreground uppercase tracking-wider",children:"Aktive Downloads"}),a.map(e=>s.jsxs("div",{className:"space-y-1.5 p-3 rounded-xl bg-background/20 border border-border/40",children:[s.jsxs("div",{className:"flex justify-between items-center text-xs",children:[s.jsx("span",{className:"font-semibold truncate max-w-[250px]",children:e.label}),s.jsxs("div",{className:"flex items-center gap-3",children:[s.jsxs("span",{className:"text-muted-foreground font-mono",children:[e.progress??0,"% • ",n(e.done_bytes),"/",n(e.total_bytes),e.eta_s?` • ETA ${b(e.eta_s)}`:""]}),s.jsx("button",{onClick:()=>i(e.id),className:"text-[10px] text-red-400 hover:text-red-300 font-semibold border border-red-500/25 bg-red-500/5 px-2 py-0.5 rounded transition-all cursor-pointer",children:"Abbrechen"})]})]}),s.jsx("div",{className:"h-1.5 overflow-hidden rounded-full bg-muted",children:s.jsx("div",{className:"h-full rounded-full bg-primary transition-all duration-500",style:{width:`${e.progress??0}%`}})})]},e.id)),r.map(e=>s.jsxs("div",{className:"flex justify-between items-center text-xs text-muted-foreground px-1",children:[s.jsx("span",{className:"truncate",children:e.label}),s.jsx("span",{className:p("font-semibold text-[10px] px-1.5 py-0.5 rounded uppercase font-mono",e.state==="done"?"bg-emerald-500/10 text-emerald-400":"bg-amber-500/10 text-amber-400"),children:e.state})]},e.id)),o]})}export{y as J};
import{u,ah as m,k as x,j as s,y as n,ai as b,b as p,n as f,q as h}from"./index-0Xcu6PnS.js";function y(){const d=u(),{data:t=[]}=m(),{showAlert:l,dialogElement:o}=x();async function i(e){try{await f(`/api/jobs/${e}/cancel`,{method:"POST"}),d.invalidateQueries({queryKey:h.jobs})}catch(c){l("Fehler",c.message)}}const a=t.filter(e=>e.state==="running"||e.state==="queued"),r=t.filter(e=>e.state!=="running"&&e.state!=="queued").slice(-3);return a.length===0&&r.length===0?null:s.jsxs("div",{className:"space-y-3 mc-card p-4",children:[s.jsx("div",{className:"text-[11px] font-semibold text-muted-foreground uppercase tracking-wider",children:"Aktive Downloads"}),a.map(e=>s.jsxs("div",{className:"space-y-1.5 p-3 rounded-xl bg-background/20 border border-border/40",children:[s.jsxs("div",{className:"flex justify-between items-center text-xs",children:[s.jsx("span",{className:"font-semibold truncate max-w-[250px]",children:e.label}),s.jsxs("div",{className:"flex items-center gap-3",children:[s.jsxs("span",{className:"text-muted-foreground font-mono",children:[e.progress??0,"% • ",n(e.done_bytes),"/",n(e.total_bytes),e.eta_s?` • ETA ${b(e.eta_s)}`:""]}),s.jsx("button",{onClick:()=>i(e.id),className:"text-[10px] text-red-400 hover:text-red-300 font-semibold border border-red-500/25 bg-red-500/5 px-2 py-0.5 rounded transition-all cursor-pointer",children:"Abbrechen"})]})]}),s.jsx("div",{className:"h-1.5 overflow-hidden rounded-full bg-muted",children:s.jsx("div",{className:"h-full rounded-full bg-primary transition-all duration-500",style:{width:`${e.progress??0}%`}})})]},e.id)),r.map(e=>s.jsxs("div",{className:"flex justify-between items-center text-xs text-muted-foreground px-1",children:[s.jsx("span",{className:"truncate",children:e.label}),s.jsx("span",{className:p("font-semibold text-[10px] px-1.5 py-0.5 rounded uppercase font-mono",e.state==="done"?"bg-emerald-500/10 text-emerald-400":"bg-amber-500/10 text-amber-400"),children:e.state})]},e.id)),o]})}export{y as J};
@@ -1 +1 @@
import{I as b,u as f,K as h,r as d,j as e,b as c,J as g,T as p,R as j,Q as N,n as v,U as w,q as x}from"./index-CCbyojll.js";function y(){const{data:s}=b(),m=f(),r=s!=null&&s.box_console_url?h(s.box_console_url):void 0,a=s==null?void 0:s.box_console_reachable,[l,o]=d.useState(!1),[i,n]=d.useState("");async function u(){o(!0),n("");try{const t=await v("/api/maintenance/restart",{method:"POST",body:JSON.stringify({service:"box-console"})});n(t.ok?"Neu gestartet — einen Moment, dann lädt das Terminal.":`Fehlgeschlagen: ${t.err||"Unbekannter Fehler"}`),w(m,x.agentStatus,x.services)}catch(t){n(`Fehlgeschlagen: ${(t==null?void 0:t.message)||t}`)}finally{o(!1)}}return e.jsxs("div",{className:"flex h-full flex-col gap-4",children:[e.jsxs("div",{className:"flex flex-wrap items-end justify-between gap-3",children:[e.jsxs("div",{children:[e.jsx("h1",{className:"bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text font-space text-2xl font-bold tracking-tight text-transparent",children:"Konsole"}),e.jsx("p",{className:"flex items-center gap-2 text-sm text-muted-foreground",children:"Direkte Shell auf der Box — wie ein SSH-Fenster, mitten im Browser."})]}),e.jsxs("div",{className:"flex items-center gap-3",children:[e.jsxs("span",{className:"flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground",children:[e.jsx("span",{className:c("h-2 w-2 rounded-full",a?"bg-emerald-500 animate-pulse":"bg-amber-500")}),a?"online":"offline"]}),r&&e.jsxs("a",{href:r,target:"_blank",rel:"noopener",className:"flex h-8 items-center gap-1.5 rounded-lg border border-border/60 bg-background/20 px-3 text-xs font-semibold text-muted-foreground transition-all hover:border-primary/50 hover:text-foreground",children:[e.jsx(g,{className:"h-3.5 w-3.5"})," In neuem Tab"]})]})]}),r?e.jsxs("div",{className:"relative min-h-[58vh] flex-1 overflow-hidden rounded-2xl border border-border/60 bg-black/50 shadow-lg shadow-black/25",children:[a===!1&&e.jsxs("div",{className:"absolute inset-0 z-10 flex flex-col items-center justify-center gap-3 bg-black/70 text-center",children:[e.jsx(p,{className:"h-8 w-8 text-amber-400"}),e.jsx("div",{className:"text-sm font-semibold text-amber-300",children:"Konsole nicht erreichbar"}),e.jsxs("p",{className:"max-w-sm text-[11px] leading-normal text-muted-foreground",children:["Der Terminal-Dienst (",e.jsx("code",{className:"font-mono text-primary",children:"box-console"}),") läuft gerade nicht."]}),e.jsxs("button",{onClick:u,disabled:l,className:"flex h-9 items-center gap-1.5 rounded-lg border border-amber-500/40 bg-amber-500/10 px-4 text-[11px] font-bold uppercase tracking-wide text-amber-300 transition-all hover:bg-amber-500/20 cursor-pointer disabled:opacity-50",children:[e.jsx(j,{className:c("h-3.5 w-3.5",l&&"animate-spin")})," Dienst neu starten"]}),i&&e.jsx("p",{className:"max-w-sm text-[11px] text-muted-foreground",children:i})]}),e.jsx("iframe",{src:r,title:"Box-Konsole",className:"h-full w-full border-0",style:{minHeight:"58vh"}})]}):e.jsxs("div",{className:"flex min-h-[58vh] flex-1 items-center justify-center rounded-2xl border border-border/60 bg-background/20 text-xs text-muted-foreground",children:[e.jsx(N,{className:"mr-2 h-4 w-4"})," Lade Konsole…"]})]})}export{y as KonsoleView};
import{I as b,u as f,K as h,r as d,j as e,b as c,J as g,T as p,R as j,Q as N,n as v,U as w,q as x}from"./index-0Xcu6PnS.js";function y(){const{data:s}=b(),m=f(),r=s!=null&&s.box_console_url?h(s.box_console_url):void 0,a=s==null?void 0:s.box_console_reachable,[l,o]=d.useState(!1),[i,n]=d.useState("");async function u(){o(!0),n("");try{const t=await v("/api/maintenance/restart",{method:"POST",body:JSON.stringify({service:"box-console"})});n(t.ok?"Neu gestartet — einen Moment, dann lädt das Terminal.":`Fehlgeschlagen: ${t.err||"Unbekannter Fehler"}`),w(m,x.agentStatus,x.services)}catch(t){n(`Fehlgeschlagen: ${(t==null?void 0:t.message)||t}`)}finally{o(!1)}}return e.jsxs("div",{className:"flex h-full flex-col gap-4",children:[e.jsxs("div",{className:"flex flex-wrap items-end justify-between gap-3",children:[e.jsxs("div",{children:[e.jsx("h1",{className:"bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text font-space text-2xl font-bold tracking-tight text-transparent",children:"Konsole"}),e.jsx("p",{className:"flex items-center gap-2 text-sm text-muted-foreground",children:"Direkte Shell auf der Box — wie ein SSH-Fenster, mitten im Browser."})]}),e.jsxs("div",{className:"flex items-center gap-3",children:[e.jsxs("span",{className:"flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground",children:[e.jsx("span",{className:c("h-2 w-2 rounded-full",a?"bg-emerald-500 animate-pulse":"bg-amber-500")}),a?"online":"offline"]}),r&&e.jsxs("a",{href:r,target:"_blank",rel:"noopener",className:"flex h-8 items-center gap-1.5 rounded-lg border border-border/60 bg-background/20 px-3 text-xs font-semibold text-muted-foreground transition-all hover:border-primary/50 hover:text-foreground",children:[e.jsx(g,{className:"h-3.5 w-3.5"})," In neuem Tab"]})]})]}),r?e.jsxs("div",{className:"relative min-h-[58vh] flex-1 overflow-hidden rounded-2xl border border-border/60 bg-black/50 shadow-lg shadow-black/25",children:[a===!1&&e.jsxs("div",{className:"absolute inset-0 z-10 flex flex-col items-center justify-center gap-3 bg-black/70 text-center",children:[e.jsx(p,{className:"h-8 w-8 text-amber-400"}),e.jsx("div",{className:"text-sm font-semibold text-amber-300",children:"Konsole nicht erreichbar"}),e.jsxs("p",{className:"max-w-sm text-[11px] leading-normal text-muted-foreground",children:["Der Terminal-Dienst (",e.jsx("code",{className:"font-mono text-primary",children:"box-console"}),") läuft gerade nicht."]}),e.jsxs("button",{onClick:u,disabled:l,className:"flex h-9 items-center gap-1.5 rounded-lg border border-amber-500/40 bg-amber-500/10 px-4 text-[11px] font-bold uppercase tracking-wide text-amber-300 transition-all hover:bg-amber-500/20 cursor-pointer disabled:opacity-50",children:[e.jsx(j,{className:c("h-3.5 w-3.5",l&&"animate-spin")})," Dienst neu starten"]}),i&&e.jsx("p",{className:"max-w-sm text-[11px] text-muted-foreground",children:i})]}),e.jsx("iframe",{src:r,title:"Box-Konsole",className:"h-full w-full border-0",style:{minHeight:"58vh"}})]}):e.jsxs("div",{className:"flex min-h-[58vh] flex-1 items-center justify-center rounded-2xl border border-border/60 bg-background/20 text-xs text-muted-foreground",children:[e.jsx(N,{className:"mr-2 h-4 w-4"})," Lade Konsole…"]})]})}export{y as KonsoleView};
@@ -1,4 +1,4 @@
import{c as E,u as he,a as qe,r as j,j as e,C as Y,L as Fe,b as k,d as Re,q as V,B as re,e as Ie,E as ze,f as le,g as Be,h as Ae,i as Pe,k as Ge,l as ee,X as me,H as be,T as se,s as Oe,m as Te,n as q,o as U,p as He,S as ge,t as Qe,P as Ue,v as Ve,w as We,x as Ze,y as Je}from"./index-CCbyojll.js";import{J as Xe}from"./JobsBar-Ckecoyqy.js";import{C as Ye}from"./code-xml-AZtn6xxf.js";import{R as et}from"./rotate-ccw-DuKtbjZS.js";import{C as ue,Z as ie,L as tt,a as rt}from"./zap-CRNNnxfs.js";import{C as De}from"./chevron-down-SPeGIim5.js";import{L as st}from"./layers-Dpl2QlP2.js";/**
import{c as E,u as he,a as qe,r as j,j as e,C as Y,L as Fe,b as k,d as Re,q as V,B as re,e as Ie,E as ze,f as le,g as Be,h as Ae,i as Pe,k as Ge,l as ee,X as me,H as be,T as se,s as Oe,m as Te,n as q,o as U,p as He,S as ge,t as Qe,P as Ue,v as Ve,w as We,x as Ze,y as Je}from"./index-0Xcu6PnS.js";import{J as Xe}from"./JobsBar-JgL4-cSI.js";import{C as Ye}from"./code-xml-BeQlEZLv.js";import{R as et}from"./rotate-ccw-CBM-YWkV.js";import{C as ue,Z as ie,L as tt,a as rt}from"./zap-BHzSmHiL.js";import{C as De}from"./chevron-down-Bak281TY.js";import{L as st}from"./layers-D3tFEnOl.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
import{c as s,j as e}from"./index-CCbyojll.js";/**
import{c as s,j as e}from"./index-0Xcu6PnS.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
var M=r=>{throw TypeError(r)};var O=(r,e,t)=>e.has(r)||M("Cannot "+t);var a=(r,e,t)=>(O(r,e,"read from private field"),t?t.call(r):e.get(r)),y=(r,e,t)=>e.has(r)?M("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(r):e.set(r,t),v=(r,e,t,n)=>(O(r,e,"write to private field"),n?n.call(r,t):e.set(r,t),t),j=(r,e,t)=>(O(r,e,"access private method"),t);import{a9 as B,aa as F,ab as E,ac as I,ad as P,u as A,r as f,ae as J,af as Q,c as T,ag as U,j as s,G as D,T as H,b as K,L,M as G,q as V,n as R}from"./index-CCbyojll.js";import{J as _}from"./JobsBar-Ckecoyqy.js";var c,g,o,h,m,w,C,q,z=(q=class extends B{constructor(e,t){super();y(this,m);y(this,c);y(this,g);y(this,o);y(this,h);v(this,c,e),this.setOptions(t),this.bindMethods(),j(this,m,w).call(this)}bindMethods(){this.mutate=this.mutate.bind(this),this.reset=this.reset.bind(this)}setOptions(e){var n;const t=this.options;this.options=a(this,c).defaultMutationOptions(e),F(this.options,t)||a(this,c).getMutationCache().notify({type:"observerOptionsUpdated",mutation:a(this,o),observer:this}),t!=null&&t.mutationKey&&this.options.mutationKey&&E(t.mutationKey)!==E(this.options.mutationKey)?this.reset():((n=a(this,o))==null?void 0:n.state.status)==="pending"&&a(this,o).setOptions(this.options)}onUnsubscribe(){var e;this.hasListeners()||(e=a(this,o))==null||e.removeObserver(this)}onMutationUpdate(e){j(this,m,w).call(this),j(this,m,C).call(this,e)}getCurrentResult(){return a(this,g)}reset(){var e;(e=a(this,o))==null||e.removeObserver(this),v(this,o,void 0),j(this,m,w).call(this),j(this,m,C).call(this)}mutate(e,t){var n;return v(this,h,t),(n=a(this,o))==null||n.removeObserver(this),v(this,o,a(this,c).getMutationCache().build(a(this,c),this.options)),a(this,o).addObserver(this),a(this,o).execute(e)}},c=new WeakMap,g=new WeakMap,o=new WeakMap,h=new WeakMap,m=new WeakSet,w=function(){var t;const e=((t=a(this,o))==null?void 0:t.state)??I();v(this,g,{...e,isPending:e.status==="pending",isSuccess:e.status==="success",isError:e.status==="error",isIdle:e.status==="idle",mutate:this.mutate,reset:this.reset})},C=function(e){P.batch(()=>{var t,n,l,x,d,p,k,i;if(a(this,h)&&this.hasListeners()){const u=a(this,g).variables,N=a(this,g).context,S={client:a(this,c),meta:this.options.meta,mutationKey:this.options.mutationKey};if((e==null?void 0:e.type)==="success"){try{(n=(t=a(this,h)).onSuccess)==null||n.call(t,e.data,u,N,S)}catch(b){Promise.reject(b)}try{(x=(l=a(this,h)).onSettled)==null||x.call(l,e.data,null,u,N,S)}catch(b){Promise.reject(b)}}else if((e==null?void 0:e.type)==="error"){try{(p=(d=a(this,h)).onError)==null||p.call(d,e.error,u,N,S)}catch(b){Promise.reject(b)}try{(i=(k=a(this,h)).onSettled)==null||i.call(k,void 0,e.error,u,N,S)}catch(b){Promise.reject(b)}}}this.listeners.forEach(u=>{u(a(this,g))})})},q);function W(r,e){const t=A(),[n]=f.useState(()=>new z(t,r));f.useEffect(()=>{n.setOptions(r)},[n,r]);const l=f.useSyncExternalStore(f.useCallback(d=>n.subscribe(P.batchCalls(d)),[n]),()=>n.getCurrentResult(),()=>n.getCurrentResult()),x=f.useCallback((d,p)=>{n.mutate(d,p).catch(J)},[n]);if(l.error&&Q(n.options.throwOnError,[l.error]))throw l.error;return{...l,mutate:x,mutateAsync:l.mutate}}/**
var M=r=>{throw TypeError(r)};var O=(r,e,t)=>e.has(r)||M("Cannot "+t);var a=(r,e,t)=>(O(r,e,"read from private field"),t?t.call(r):e.get(r)),y=(r,e,t)=>e.has(r)?M("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(r):e.set(r,t),v=(r,e,t,n)=>(O(r,e,"write to private field"),n?n.call(r,t):e.set(r,t),t),j=(r,e,t)=>(O(r,e,"access private method"),t);import{a9 as B,aa as F,ab as E,ac as I,ad as P,u as A,r as f,ae as J,af as Q,c as T,ag as U,j as s,G as D,T as H,b as K,L,M as G,q as V,n as R}from"./index-0Xcu6PnS.js";import{J as _}from"./JobsBar-JgL4-cSI.js";var c,g,o,h,m,w,C,q,z=(q=class extends B{constructor(e,t){super();y(this,m);y(this,c);y(this,g);y(this,o);y(this,h);v(this,c,e),this.setOptions(t),this.bindMethods(),j(this,m,w).call(this)}bindMethods(){this.mutate=this.mutate.bind(this),this.reset=this.reset.bind(this)}setOptions(e){var n;const t=this.options;this.options=a(this,c).defaultMutationOptions(e),F(this.options,t)||a(this,c).getMutationCache().notify({type:"observerOptionsUpdated",mutation:a(this,o),observer:this}),t!=null&&t.mutationKey&&this.options.mutationKey&&E(t.mutationKey)!==E(this.options.mutationKey)?this.reset():((n=a(this,o))==null?void 0:n.state.status)==="pending"&&a(this,o).setOptions(this.options)}onUnsubscribe(){var e;this.hasListeners()||(e=a(this,o))==null||e.removeObserver(this)}onMutationUpdate(e){j(this,m,w).call(this),j(this,m,C).call(this,e)}getCurrentResult(){return a(this,g)}reset(){var e;(e=a(this,o))==null||e.removeObserver(this),v(this,o,void 0),j(this,m,w).call(this),j(this,m,C).call(this)}mutate(e,t){var n;return v(this,h,t),(n=a(this,o))==null||n.removeObserver(this),v(this,o,a(this,c).getMutationCache().build(a(this,c),this.options)),a(this,o).addObserver(this),a(this,o).execute(e)}},c=new WeakMap,g=new WeakMap,o=new WeakMap,h=new WeakMap,m=new WeakSet,w=function(){var t;const e=((t=a(this,o))==null?void 0:t.state)??I();v(this,g,{...e,isPending:e.status==="pending",isSuccess:e.status==="success",isError:e.status==="error",isIdle:e.status==="idle",mutate:this.mutate,reset:this.reset})},C=function(e){P.batch(()=>{var t,n,l,x,d,p,k,i;if(a(this,h)&&this.hasListeners()){const u=a(this,g).variables,N=a(this,g).context,S={client:a(this,c),meta:this.options.meta,mutationKey:this.options.mutationKey};if((e==null?void 0:e.type)==="success"){try{(n=(t=a(this,h)).onSuccess)==null||n.call(t,e.data,u,N,S)}catch(b){Promise.reject(b)}try{(x=(l=a(this,h)).onSettled)==null||x.call(l,e.data,null,u,N,S)}catch(b){Promise.reject(b)}}else if((e==null?void 0:e.type)==="error"){try{(p=(d=a(this,h)).onError)==null||p.call(d,e.error,u,N,S)}catch(b){Promise.reject(b)}try{(i=(k=a(this,h)).onSettled)==null||i.call(k,void 0,e.error,u,N,S)}catch(b){Promise.reject(b)}}}this.listeners.forEach(u=>{u(a(this,g))})})},q);function W(r,e){const t=A(),[n]=f.useState(()=>new z(t,r));f.useEffect(()=>{n.setOptions(r)},[n,r]);const l=f.useSyncExternalStore(f.useCallback(d=>n.subscribe(P.batchCalls(d)),[n]),()=>n.getCurrentResult(),()=>n.getCurrentResult()),x=f.useCallback((d,p)=>{n.mutate(d,p).catch(J)},[n]);if(l.error&&Q(n.options.throwOnError,[l.error]))throw l.error;return{...l,mutate:x,mutateAsync:l.mutate}}/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
import{c as o}from"./index-CCbyojll.js";/**
import{c as o}from"./index-0Xcu6PnS.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
import{c as e}from"./index-CCbyojll.js";/**
import{c as e}from"./index-0Xcu6PnS.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
import{c as a}from"./index-CCbyojll.js";/**
import{c as a}from"./index-0Xcu6PnS.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
import{c as a}from"./index-CCbyojll.js";/**
import{c as a}from"./index-0Xcu6PnS.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
import{c as t}from"./index-CCbyojll.js";/**
import{c as t}from"./index-0Xcu6PnS.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
import{c as a}from"./index-CCbyojll.js";/**
import{c as a}from"./index-0Xcu6PnS.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
+1 -1
View File
@@ -7,7 +7,7 @@
<link rel="manifest" href="/manifest.webmanifest" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>Mission Control 2.0</title>
<script type="module" crossorigin src="/assets/index-CCbyojll.js"></script>
<script type="module" crossorigin src="/assets/index-0Xcu6PnS.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Dv1-nhwH.css">
</head>
<body>
@@ -2,7 +2,7 @@ import { Gauge } from "lucide-react"
import { useVoiceTrace } from "@/lib/queries"
import type { VoiceTurn } from "@/lib/api"
// Balken-Stufen in kanonischer Reihenfolge (zeitlich disjunkt). Mem0 ist ein Unter-Detail
// Balken-Stufen in kanonischer Reihenfolge (zeitlich disjunkt). Gedächtnis ist ein Unter-Detail
// INNERHALB von „Hirn" und wird NICHT als eigenes Segment addiert (sonst Doppelzählung).
const SEGMENTS = [
{ key: "stt_ms", label: "STT", color: "#f59e0b" },
@@ -39,7 +39,7 @@ function culprit(t: VoiceTurn): { label: string; color: string; ms: number } | n
function TurnRow({ t, scale }: { t: VoiceTurn; scale: number }) {
const title = SEGMENTS.map((s) => `${s.label} ${fmt(t[s.key] as number | null)}`).join(" · ")
+ (t.mem0_ms != null ? ` (davon Gedächtnis ${fmt(t.mem0_ms)})` : "")
+ (t.memory_ms != null ? ` (davon Gedächtnis ${fmt(t.memory_ms)})` : "")
return (
<div className="flex items-center gap-2.5">
<span className="w-14 shrink-0 text-right font-mono text-[10px] text-muted-foreground/60">{ago(t.ts)}</span>
@@ -103,7 +103,7 @@ export function LatencyCard() {
{latest && !latest.error && (
<div className="mt-0.5 font-mono text-[11px] text-muted-foreground/70">
{SEGMENTS.filter((s) => (latest[s.key] as number | null) != null).map((s) => `${s.label} ${fmt(latest[s.key] as number)}`).join(" · ")}
{latest.mem0_ms != null && <span className="text-muted-foreground/50"> · davon Gedächtnis {fmt(latest.mem0_ms)}</span>}
{latest.memory_ms != null && <span className="text-muted-foreground/50"> · davon Gedächtnis {fmt(latest.memory_ms)}</span>}
</div>
)}
</div>
@@ -128,7 +128,7 @@ export function LatencyCard() {
)}
<div className="mt-3 border-t border-border/30 pt-2 text-[10px] leading-relaxed text-muted-foreground/70">
Hirn" = Zeit bis zum ersten Wort (Agent + Mem0-Suche + Modell); Tool-Runden stecken in „Antwort". Reine
Hirn" = Zeit bis zum ersten Wort (Agent + Gedächtnis-Suche + Modell); Tool-Runden stecken in „Antwort". Reine
Telegram-Turns laufen an MC2 vorbei und erscheinen hier nicht.
</div>
</div>
+3 -3
View File
@@ -443,7 +443,7 @@ export interface TokenStats {
}
// Per-Turn-Latenz-Trace (GET /api/voice/trace). Balken-Stufen (zeitlich disjunkt):
// stt · vision · hirn · gen. mem0 = Unter-Detail INNERHALB von hirn (nicht zum Balken addieren).
// stt · vision · hirn · gen. memory = Unter-Detail INNERHALB von hirn (nicht zum Balken addieren).
export interface VoiceTurn {
id: string
ts: number // Epoch-Sekunden
@@ -452,9 +452,9 @@ export interface VoiceTurn {
had_images: boolean
stt_ms: number | null
vision_ms: number | null
hirn_ms: number | null // Zeit bis erstes Inhalts-Token (Agent + Mem0 + LLM-TTFT)
hirn_ms: number | null // Zeit bis erstes Inhalts-Token (Agent + Gedächtnis + LLM-TTFT)
gen_ms: number | null // Generierung nach dem ersten Token bis Stream-Ende
mem0_ms: number | null // Teil VON hirn (Mem0-Retrieve), best-effort
memory_ms: number | null // Teil VON hirn (Gedächtnis-Retrieve), best-effort
total_ms: number
error: string | null
}
+2 -5
View File
@@ -345,11 +345,8 @@ description: Treibt Entwicklung mit strikter TDD-Praxis
</div>
</div>
<Stack>
Dein Gedächtnis (Tab <strong>Gedächtnis</strong>) ist <strong>Mem0</strong>-basiert: auto-lernend &amp;
semantisch, <strong>graph-first</strong> dargestellt, mit 4-Typen-Taxonomie
(<Pill>Identität</Pill> · <Pill>Wissen</Pill> · <Pill>Regeln</Pill> · <Pill>Ereignisse</Pill>). Der Agent
lernt im Hintergrund nach jedem Turn — egal ob du per Desktop, Konsole, Telegram oder Lucy mit ihm sprichst.
<br /><strong>Gotcha:</strong> ein gelöschter Fakt kann wieder auftauchen, solange die Original-Nachricht noch im Verlauf steht (additive Extraktion).
Dein Gedächtnis ist auto-lernend &amp; semantisch: Der Agent lernt im Hintergrund nach jedem Turn —
egal ob du per Desktop, Konsole, Telegram oder Lucy mit ihm sprichst.
</Stack>
</Card>
+3 -3
View File
@@ -216,7 +216,7 @@ def system_status() -> str:
@mcp.tool()
def list_services() -> str:
"""Erreichbarkeit aller Stack-Dienste (Engine, Gateway, Hermes, Mem0, Voice)."""
"""Erreichbarkeit aller Stack-Dienste (Engine, Gateway, Hermes, Voice)."""
data = _get("/api/system/services")
return "\n".join(
f"{'' if s['ok'] else ''} {s['name']} ({s['url']})"
@@ -227,7 +227,7 @@ def list_services() -> str:
@mcp.tool()
def restart_service(service: str) -> str:
"""Startet einen erlaubten Dienst neu. Erlaubt: llama-swap (Engine) | mission-control-2 |
hermes-gateway | hermes-terminal | mem0-service | voice-service. Der Engine-Neustart
hermes-gateway | hermes-builtin-ui | voice-service. Der Engine-Neustart
(llama-swap = System-Dienst) kann das Box-Passwort verlangen, wenn kein NOPASSWD gesetzt ist."""
res = _post("/api/system/restart", {"service": service})
return f"Restart {service}: {'ok' if res.get('ok') else 'Fehler — ' + res.get('err', '')}"
@@ -235,7 +235,7 @@ def restart_service(service: str) -> str:
@mcp.tool()
def backup_now() -> str:
"""Erstellt SOFORT ein Voll-Backup (mem0 + Configs + Secrets). Nutze dies vor riskanten
"""Erstellt SOFORT ein Voll-Backup (Configs + Secrets). Nutze dies vor riskanten
Änderungen oder wenn der Nutzer ein Backup will."""
r = _post("/api/system/backup")
return f"Backup erstellt: {json.dumps(r, ensure_ascii=False)}"
-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)