""" Hält das Agent-Hirn (Rolle `hermes`) dauerhaft warm. Hintergrund: llama-swap ist EIN-Gruppen-resident — lädt ein on-demand-Modell außerhalb der `brains`-Gruppe, wird die ganze Gruppe (inkl. Hirn) verdrängt. `persist: true` verhindert nur Idle-Unload, NICHT die Gruppen-Verdrängung; neu vorgewärmt wird sonst erst beim nächsten llama-swap-(Re)Start. Dieser Wächter schließt die Lücke: ist die Box idle (nichts geladen), pingt er das Hirn vor. Während aktiver Last (irgendetwas geladen) hält er sich raus, verdrängt also nie ein gerade genutztes Modell. Abschaltbar/justierbar via Env: MC_REWARM_ENABLED=0, MC_REWARM_INTERVAL, MC_REWARM_MODEL. """ import asyncio import logging import os import httpx from config import LLAMA_SWAP_URL log = logging.getLogger(__name__) ENABLED = os.environ.get("MC_REWARM_ENABLED", "1") != "0" INTERVAL = int(os.environ.get("MC_REWARM_INTERVAL", "90")) # Sekunden zwischen Checks START_DELAY = int(os.environ.get("MC_REWARM_START_DELAY", "25")) NUDGE_GRACE = int(os.environ.get("MC_REWARM_NUDGE_GRACE", "3")) # llama-swap den Reload abschließen lassen # Wecksignal für einen sofortigen Vorwärm-Check (statt bis zum nächsten INTERVAL-Tick zu warten). # Wird von write_config() nach einer Config-Änderung gesetzt: llama-swap (-watch-config) lädt die # neue Config und verwirft dabei ALLE Modelle inkl. Hirn — ohne Nudge bliebe es bis zu INTERVAL # Sekunden kalt liegen, bis der nächste Tick oder eine Anfrage es wieder lädt. _loop: asyncio.AbstractEventLoop | None = None _wake: asyncio.Event | None = None def nudge() -> None: """Threadsicher: bittet den Wächter, nach einem Config-Reload bald vorzuwärmen. No-op, solange der Wächter (noch) nicht läuft.""" if _loop is not None and _wake is not None and not _loop.is_closed(): try: _loop.call_soon_threadsafe(_wake.set) except RuntimeError: pass def _brain_model() -> str: """Aktives Agent-Hirn = Hermes' model.default (sonst model.model). So wärmt der Wächter immer das WIRKLICH genutzte Hirn (passt sich Hirn-Wechseln an). Override: MC_REWARM_MODEL.""" forced = os.environ.get("MC_REWARM_MODEL") if forced: return forced try: from ruamel.yaml import YAML from config import HERMES_HOME p = HERMES_HOME / "config.yaml" if p.exists(): with p.open(encoding="utf-8") as f: cfg = YAML().load(f) or {} m = (cfg.get("model") or {}) if isinstance(cfg, dict) else {} v = m.get("default") or m.get("model") if v: return str(v) except Exception: log.debug("rewarm: Hirn-Lookup fehlgeschlagen", exc_info=True) return "fast" async def _running_empty() -> bool: async with httpx.AsyncClient(timeout=8.0) as c: r = await c.get(f"{LLAMA_SWAP_URL}/running") data = r.json() or {} return not (data.get("running") or []) async def _warm(model: str) -> None: async with httpx.AsyncClient(timeout=180.0) as c: await c.post(f"{LLAMA_SWAP_URL}/v1/chat/completions", json={ "model": model, "max_tokens": 1, "messages": [{"role": "user", "content": "ping"}], }) async def rewarm_loop() -> None: """Endlos-Schleife (Hintergrund-Task): prüft periodisch (und sofort nach einem Config- Reload-Nudge), wärmt bei Idle vor.""" global _loop, _wake _loop = asyncio.get_running_loop() _wake = asyncio.Event() await asyncio.sleep(START_DELAY) # Box/Engine nach MC-Start setzen lassen while True: try: if await _running_empty(): model = _brain_model() log.info("rewarm: Box idle → Hirn '%s' wird vorgewärmt", model) await _warm(model) except Exception: log.debug("rewarm: Tick fehlgeschlagen", exc_info=True) # Bis zum nächsten Tick warten ODER sofort auf einen Config-Reload-Nudge reagieren. try: await asyncio.wait_for(_wake.wait(), timeout=INTERVAL) _wake.clear() await asyncio.sleep(NUDGE_GRACE) # llama-swap den Reload abschließen lassen except asyncio.TimeoutError: pass