""" Zentrale Konfiguration für Mission Control 2.0. Eine Quelle der Wahrheit für Pfade, URLs und Defaults — alles über Env-Vars überschreibbar. Bewusst schlank: MC 2.0 ist ein Glue-Cockpit, das vorhandene Dienste (llama-swap, LiteLLM-Gateway, Hermes) steuert, statt sie nachzubauen. """ import os from pathlib import Path from ruamel.yaml import YAML # --- Engine (llama-swap) ----------------------------------------------------- LLAMA_SWAP_URL = os.environ.get("MC_LLAMA_SWAP_URL", "http://127.0.0.1:8080").rstrip("/") CONFIG_PATH = Path(os.environ.get("MC_CONFIG_PATH", "/etc/llama-swap/config.yaml")) MODELS_DIR = Path(os.environ.get("MC_MODELS_DIR", "/srv/models")) # Cache der Modell-Entdeckung ("aktuell beste Modelle", live von HuggingFace). # 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 # macht llama-server ohnehin automatisch pro Slot (KV-Reuse). _DEFAULT_CMD_TEMPLATE = ( "llama-server -m {model} --host 127.0.0.1 --port ${PORT} " "-c {ctx} -ngl 999 -fa on --no-mmap" ) CMD_TEMPLATE = os.environ.get("MC_CMD_TEMPLATE", _DEFAULT_CMD_TEMPLATE) if "{model}" not in CMD_TEMPLATE: CMD_TEMPLATE = _DEFAULT_CMD_TEMPLATE DEFAULT_TTL = int(os.environ.get("MC_DEFAULT_TTL", "300")) # Verzeichnis mit Draft-Modellen für Speculative Decoding. Beim Hinzufügen eines # fast/coder-Modells wird hieraus automatisch ein **vocab-kompatibler** Draft gewählt # (Vocab-Check via services.gguf_meta; ein inkompatibler Draft lässt llama.cpp scheitern). DRAFTS_DIR = Path(os.environ.get("MC_DRAFTS_DIR", str(MODELS_DIR / "drafts"))) # Optionaler expliziter Default-Draft (leer = Auto-Erkennung aus DRAFTS_DIR). Wird nur # verwendet, wenn er zum Ziel-Modell vocab-kompatibel ist. (Früher fix qwen2.5 → entfernt, # weil das mit neueren Vocabs wie Qwen3.6 inkompatibel ist und Spec stillschweigend brach.) SPEC_DRAFT_MODEL_PATH = os.environ.get("MC_SPEC_DRAFT_MODEL", "") # Speculative-Decoding-Typ (llama.cpp dieser Generation braucht --spec-type zusätzlich # zu --spec-draft-model, sonst ist Spec inaktiv). SPEC_TYPE = os.environ.get("MC_SPEC_TYPE", "draft-simple") # MTP-Speculative-Decoding (Multi-Token-Prediction): manche Modelle bringen einen eigenen # MTP-Kopf mit (z.B. gemma-4 → arch 'gemma4-assistant', Datei 'mtp-*.gguf'). Der wird mit # `--model-draft --spec-type draft-mtp --spec-draft-n-max N` geladen (NICHT # --spec-draft-model/draft-simple). 1,5–2× Durchsatz bei null Qualitätsverlust. SPEC_DRAFT_N_MAX = int(os.environ.get("MC_SPEC_DRAFT_N_MAX", "4")) # Env für HuggingFace-Downloads: XET deaktivieren (Hänger bei ~6 MB, siehe v1-Gotcha). HF_DOWNLOAD_ENV = {"HF_HUB_DISABLE_XET": "1"} # --- Routing-Gateway (builtin in MC2, model: auto) --------------------------- # MC2 IST der Gateway (services/gateway.py + routers/gateway_proxy.py). KEIN externer # LiteLLM-Dienst (scheitert auf Python 3.14). Daher keine Gateway-Config-Datei mehr. GATEWAY_URL = os.environ.get("MC_GATEWAY_URL", f"http://127.0.0.1:{os.environ.get('MC_PORT', '9000')}").rstrip("/") # Gateway-Auszug (UMBAU v3 P1): Ist MC_V1_UPSTREAM gesetzt (Unit-Env, z. B. # http://127.0.0.1:9010), bedient der eigenständige mc2-gateway-Prozess den /v1-Pfad # und MC2 reicht /v1 nur noch roh durch (routers/gateway_forward.py). Leer = altes # Verhalten, MC2 bedient /v1 selbst — die Zeile aus der Unit nehmen ist der Rollback. V1_UPSTREAM = os.environ.get("MC_V1_UPSTREAM", "").rstrip("/") # --- Hermes Agent (eigener Dienst auf der Box) ------------------------------- # Gateway (OpenAI-API des Agenten) + interaktives Web-Terminal (ttyd → `hermes chat`). 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. 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.""" try: env_path = Path(os.path.expanduser(os.environ.get("HERMES_HOME", "~/.hermes"))) / ".env" for line in env_path.read_text(encoding="utf-8").splitlines(): line = line.strip() if line.startswith(f"{key}="): return line.split("=", 1)[1].strip().strip('"').strip("'") except OSError: pass return "" HERMES_API_KEY = ( os.environ.get("HERMES_API_KEY") or os.environ.get("API_SERVER_KEY") or _read_hermes_env("API_SERVER_KEY") ) # Modellfeld im OpenAI-Request; die api_server-Plattform nutzt ihr konfiguriertes Hirn, # das Feld ist i.d.R. kosmetisch. Override via Env, falls die Plattform strikt prüft. 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. 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 # rückwärts geproxyt (routers/console.py → same-origin /console/). So braucht die Konsole # KEINE eigene Firewall-Freigabe (Port 7682 ist von außen dicht) und keinen sudo-Eingriff. # Direkter, SSH-artiger Zugriff, kein Passwort — gleiches LAN-Trust-Modell wie das Dashboard. BOX_CONSOLE_UPSTREAM = os.environ.get("MC_BOX_CONSOLE_UPSTREAM", "http://127.0.0.1:7682").rstrip("/") # Öffentlicher, gleicher-Ursprung-Pfad, unter dem MC2 die Konsole ausliefert (iframe-Ziel). BOX_CONSOLE_PATH = "/console/" # Eingebaute Hermes-Web-GUI (`hermes serve`/`dashboard` — die richtige Agent-Oberfläche mit # Threads/Tool-Calls, die auch die Electron-Desktop-App umhüllt). Bindet NUR an Loopback # (127.0.0.1:9119, kein Login — LAN-Trust wie Terminal/Konsole) und wird von MC2 same-origin unter # /hermes-ui/ durchgereicht (routers/hermes_ui.py). Anders als das ttyd-Terminal ist es eine volle # SPA → das Bundle wird mit Vite-base=/hermes-ui/ gebaut, damit Assets/API/WS unter dem Präfix # liegen; der Proxy streift /hermes-ui ab. Kein eigener Firewall-Port nötig. HERMES_BUILTIN_UI_UPSTREAM = os.environ.get("MC_HERMES_BUILTIN_UI_UPSTREAM", "http://127.0.0.1:9119").rstrip("/") HERMES_BUILTIN_UI_PATH = "/hermes-ui/" # GitHub-Repo für Update-Checks. HERMES_AGENT_REPO = os.environ.get("MC_HERMES_AGENT_REPO", "NousResearch/hermes-agent") HERMES_HOME = Path(os.path.expanduser(os.environ.get("HERMES_HOME", "~/.hermes"))) # PC Executor — läuft auf dem Windows-PC, erreichbar über LAN. PC_EXECUTOR_URL = os.environ.get("MC_PC_EXECUTOR_URL", "http://192.168.178.98:7777").rstrip("/") # --- Server ------------------------------------------------------------------ HOST = os.environ.get("MC_HOST", "0.0.0.0") PORT = int(os.environ.get("MC_PORT", "9000")) # Gebautes React-Frontend (frontend/dist). In Prod liefert FastAPI es statisch aus; # im Dev läuft der Vite-Dev-Server separat und proxyt /api hierher. FRONTEND_DIST = Path(os.environ.get("MC_FRONTEND_DIST", str(Path(__file__).resolve().parent.parent / "frontend" / "dist"))) # Version (Phase 0 — Greenfield-Skeleton). VERSION = "2.0.0-w8" # Gemeinsame YAML-Instanz (preserve_quotes hält Kommentare/Quotes in config.yaml). yaml = YAML() yaml.preserve_quotes = True