7fac17ed9a
Die Intelligenz sitzt nicht mehr NEBEN dem Coden, sondern DRIN. Alles auf der Box gemessen, nicht angenommen. Stufe 1 — Speicher-Haushalt (llama-swap-Config, nicht im Repo): Coder dauerwarm statt ttl 600 (Kaltstart 23 s -> 491 ms), Devstral als 'kritiker' verdrahtet, Swap 11 GB -> 0. Zwei Befunde: Kontext 131k->65k spart bei Qwen3-Next 0 GB (Hybrid-Attention), und llama-swap haelt nur EINE Gruppe resident -> alles Ko-Residente in dieselbe Gruppe. `persistent: true` verhindert dabei das Freiraeumen vor grossen Modellen -> ausgeloester Kernel-OOM, behoben durch persistent:false + TTLs (Vision laedt jetzt in 10 s statt 117 s + Absturz). Stufe 2 — Governor v2 (deploy/governor/): Steht jetzt IM Pfad (:8100 -> MC2 :9001) statt daneben. Zaehlt den GANZEN Anfragekoerper inkl. tools/tool_calls und kalibriert sich aus den echten usage.prompt_tokens jeder Antwort nach: Schaetzfehler 200 % -> 0,3 %. Soft-Einschub nur noch, wenn die Nachrichtenkette es erlaubt (kein Dazwischen- funken in offene tool_calls). Neuer Status-Endpunkt + systemd-Unit. Lucys Alltagsmodelle sind vom Schnitt ausgenommen. Stufe 3 — OpenCode-Plugin (deploy/opencode/plugin/mc2-governor.ts): Werkzeug-Zaun (git push, rm -rf, sudo, curl|sh — bewiesen), Pruef-Tor auf session.idle mit Selbstreparatur, Savepoint statt Kompression, Meldungen an Lucys Briefkasten mit eigenem Absender 'loop'. Stufe 4 — Mannschaft (opencode.json): plan+build -> coder (51,5 t/s) · explore -> hermes (69,6 t/s, warm, gratis) · review -> Devstral (15,0 t/s, FREMDE Modellfamilie gegen blinde Flecken). Der 63-GB-Planer faellt aus der Tagesrolle raus. Stufe 5 — ein Regelwerk, zwei Ausloeser: deploy/opencode-lauf.sh faehrt dieselbe Bau-Pruef-Schleife unbeaufsichtigt (die Schleife liegt hier UND im Plugin: bei `opencode run` endet der Prozess, bevor session.idle fertig ist — gemessen). Bricht ab, wenn der Governor fehlt. gitea-repo-create.sh saet jetzt VERIFY neben der CI-Ampel: jedes neue Repo wird mit Innen- UND Aussen-Pruefung geboren. Oberflaeche: Token-Waechter-Kachel im Cockpit (Fuellstand, Marken, Ehrlichkeits-Nachweis), /api/governor als gleichursprüngliches Fenster, Devstral im Modellkatalog, Rollen-Texte auf die neue Mannschaft aktualisiert. .gitattributes: deploy/**/*.py auf LF (deploy/*.py greift nur eine Ebene tief). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
182 lines
7.0 KiB
Python
182 lines
7.0 KiB
Python
"""
|
|
Mission Control 2.0 — dünner FastAPI-Einstieg.
|
|
|
|
Hängt die Router ein, liefert (in Prod) das gebaute React-Frontend aus und
|
|
setzt eine no-cache-Middleware. Im Dev läuft das Frontend über den Vite-Dev-
|
|
Server (proxyt /api hierher), daher CORS für localhost offen.
|
|
"""
|
|
|
|
import asyncio
|
|
import logging
|
|
import os
|
|
from collections.abc import AsyncGenerator, Awaitable, Callable
|
|
from contextlib import asynccontextmanager
|
|
from typing import Any
|
|
|
|
import httpx
|
|
from config import FRONTEND_DIST, V1_UPSTREAM, VERSION
|
|
from fastapi import FastAPI
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.responses import FileResponse
|
|
from fastapi.staticfiles import StaticFiles
|
|
from routers import (
|
|
agent,
|
|
auftragsbuch,
|
|
chronik,
|
|
connect,
|
|
console,
|
|
eigenleben,
|
|
events,
|
|
gateway_proxy,
|
|
governor,
|
|
health,
|
|
hermes_ui,
|
|
ideen,
|
|
maintenance,
|
|
memory,
|
|
models,
|
|
routing,
|
|
system,
|
|
voice,
|
|
wissen,
|
|
zeitmaschine,
|
|
)
|
|
from routers import reminders as reminders_router
|
|
from services import ketten_digest, metrics_history, reminders, sentry, warmer
|
|
from services import memory as memory_svc
|
|
from starlette.requests import Request
|
|
|
|
# Zentrales Logging — Level via MC_LOG_LEVEL (INFO default). Eine Konfiguration
|
|
# für alle Module (logging.getLogger(__name__)).
|
|
logging.basicConfig(
|
|
level=os.environ.get("MC_LOG_LEVEL", "INFO").upper(),
|
|
format="%(asctime)s %(levelname)-7s %(name)s: %(message)s",
|
|
)
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
|
"""Hintergrund-Tasks an den App-Lebenszyklus binden: Re-Warm-Wächter fürs Agent-Hirn
|
|
+ Health-Wächter (meldet Ausfälle/Erholung in den Lucy-Briefkasten und auf Telegram)."""
|
|
tasks: list[asyncio.Task[Any]] = []
|
|
if warmer.ENABLED:
|
|
tasks.append(asyncio.create_task(warmer.rewarm_loop()))
|
|
log.info(
|
|
"Hirn-Re-Warm-Wächter aktiv (Intervall %ss, Hirn dynamisch aus Hermes-Config)",
|
|
warmer.INTERVAL,
|
|
)
|
|
if sentry.ENABLED:
|
|
tasks.append(asyncio.create_task(sentry.sentry_loop()))
|
|
tasks.append(asyncio.create_task(reminders.reminders_loop()))
|
|
if ketten_digest.ENABLED:
|
|
# Werkstatt-Status aufs Handy: Fragen sofort, Meilensteine sofort, sonst 60-min-Puls.
|
|
tasks.append(asyncio.create_task(ketten_digest.digest_loop()))
|
|
# 24-h-Metrik-Verlauf (Cockpit-Zeitachse): 10-s-Sampler, Ringpuffer, persistiert.
|
|
tasks.append(asyncio.create_task(metrics_history.sampler_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,
|
|
)
|
|
# Geteilter HTTP-Client zur lokalen Engine: Keep-Alive/Connection-Pooling statt neuer Client
|
|
# pro /v1-Anfrage (spart Sockets/TIME_WAIT unter parallelen Agent-Strömen von Zed/Kilo).
|
|
app.state.gw_client = httpx.AsyncClient(
|
|
timeout=httpx.Timeout(connect=10.0, read=None, write=None, pool=10.0),
|
|
limits=httpx.Limits(max_keepalive_connections=100, max_connections=200),
|
|
)
|
|
try:
|
|
yield
|
|
finally:
|
|
for task in tasks:
|
|
_ = task.cancel()
|
|
await app.state.gw_client.aclose()
|
|
|
|
|
|
app = FastAPI(title="Mission Control 2.0", version=VERSION, lifespan=lifespan)
|
|
|
|
# Dev: Vite-Dev-Server (5173) ruft das Backend per /api auf.
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["http://localhost:5173", "http://127.0.0.1:5173"],
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
|
|
@app.middleware("http")
|
|
async def no_cache(
|
|
request: Request, call_next: Callable[[Request], Awaitable[httpx.Response]]
|
|
) -> httpx.Response:
|
|
resp = await call_next(request)
|
|
if request.url.path.startswith("/api"):
|
|
resp.headers["Cache-Control"] = "no-cache"
|
|
return resp
|
|
|
|
|
|
app.include_router(health.router)
|
|
app.include_router(models.router)
|
|
app.include_router(routing.router)
|
|
app.include_router(system.router)
|
|
app.include_router(connect.router)
|
|
app.include_router(memory.router)
|
|
app.include_router(agent.router)
|
|
app.include_router(
|
|
voice.router
|
|
) # Sprache: STT/TTS-Proxy + Hermes-Agent-Chat (Voice-Tab)
|
|
app.include_router(
|
|
reminders_router.router
|
|
) # Erinnerungen/Routinen (A3) — feuern in den Briefkasten
|
|
# /v1-Datenpfad: Nach dem Gateway-Auszug (UMBAU v3 P1) läuft der eigentliche Gateway als
|
|
# eigener Prozess (mc2-gateway, Loopback :9010) — MC2 reicht /v1 dann nur roh durch, damit
|
|
# LAN-Clients (IDE-Lane) weiter über :9001 kommen. Ohne MC_V1_UPSTREAM (vor dem ersten
|
|
# Deploy der neuen Unit / nach Rollback) bedient MC2 /v1 wie bisher selbst.
|
|
if V1_UPSTREAM:
|
|
from routers import gateway_forward
|
|
app.include_router(gateway_forward.router) # dünner Roh-Weiterleiter → mc2-gateway
|
|
else:
|
|
app.include_router(gateway_proxy.router) # OpenAI-kompatibler /v1-Gateway (model:auto)
|
|
app.include_router(maintenance.router)
|
|
app.include_router(auftragsbuch.router) # Vorschlags-Inbox (Mensch-Gate als Klick)
|
|
app.include_router(ideen.router) # Ideen-Queue (natives Hermes-Kanban) — Tür der Zentrale
|
|
app.include_router(chronik.router) # Timeline der autonomen Taten (Announce-Store)
|
|
app.include_router(governor.router) # Token-Wächter (:8100) — Zählerstand fürs Cockpit
|
|
app.include_router(eigenleben.router) # „Von allein": Skills + Vorschlags-Bilanz der Box
|
|
app.include_router(events.router) # SSE-Eventstrom /api/events (P3a) — Invalidation-Bus
|
|
app.include_router(wissen.router) # Wissens-Vault (Traum-Notizen) read-only
|
|
app.include_router(zeitmaschine.router) # Snapshots ansehen + Ein-Klick-Restore (detached)
|
|
app.include_router(
|
|
console.router
|
|
) # Box-Konsole (ttyd) same-origin durchreichen — VOR dem SPA-Catch-all
|
|
app.include_router(
|
|
hermes_ui.router
|
|
) # Eingebaute Hermes-Web-GUI (hermes serve) same-origin unter /hermes-ui/ — VOR dem SPA-Catch-all
|
|
|
|
|
|
# Prod: gebautes Frontend ausliefern (falls vorhanden). SPA-Fallback auf index.html.
|
|
if FRONTEND_DIST.exists():
|
|
app.mount("/assets", StaticFiles(directory=FRONTEND_DIST / "assets"), name="assets")
|
|
|
|
_DIST_ROOT = FRONTEND_DIST.resolve()
|
|
|
|
@app.get("/{full_path:path}")
|
|
def spa(full_path: str):
|
|
# Datei direkt aus FRONTEND_DIST ausliefern (manifest.webmanifest, favicon.ico, …) — aber
|
|
# NUR innerhalb des dist-Ordners: Pfad auflösen + Traversal (../, absolute Pfade) hart raus.
|
|
try:
|
|
target = (FRONTEND_DIST / full_path).resolve()
|
|
if target.is_relative_to(_DIST_ROOT) and target.is_file():
|
|
return FileResponse(target)
|
|
except (ValueError, OSError):
|
|
pass
|
|
|
|
index = FRONTEND_DIST / "index.html"
|
|
if index.exists():
|
|
# index.html nie cachen → Browser zieht nach jedem Deploy das aktuelle (gehashte) Bundle.
|
|
return FileResponse(
|
|
index, headers={"Cache-Control": "no-cache, must-revalidate"}
|
|
)
|
|
return {"detail": "frontend not built"}
|