""" Hermes-Agent: Status und Hirn-Umstellung. MC betreibt Hermes NICHT — Werkzeuge und MCP werden in Hermes' eigener Config verdrahtet (siehe docs/ARCHITEKTUR.md, „Wer schreibt was"). """ import logging import os import httpx from config import ( HERMES_API_URL, HERMES_BUILTIN_UI_PATH, HERMES_BUILTIN_UI_UPSTREAM, HERMES_HOME, ) log = logging.getLogger(__name__) def _active_brain_name() -> str: """Aktives Agent-Hirn aus Hermes' Config: model.default (sonst model.model).""" try: from ruamel.yaml import YAML 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 {} return str(m.get("default") or m.get("model") or "auto") except Exception: log.debug("_active_brain_name: Lesefehler", exc_info=True) return "auto" def hermes_brain_info() -> dict: """Aktuelles Agent-Hirn = Modell/Alias, das Hermes laut Config nutzt (model.default), plus Budget-Check. Zeigt das REAL genutzte Hirn — unabhängig von einer 'hermes'-Rolle.""" from services import llamaswap models = llamaswap.list_models() brain = _active_brain_name() # z.B. "fast" (Alias) oder ein Modellname bl = brain.lower() cur = next((m for m in models if (m.get("role") or "").lower() == bl), None) \ or next((m for m in models if bl in (m["name"] or "").lower()), None) cur_params = (cur.get("capabilities") or {}).get("params_b") if cur else None current = None if cur: current = {"name": cur["name"], "alias": brain, "filename": cur.get("filename"), "params_b": cur_params, "quant": cur.get("quant"), "size_bytes": cur.get("size_bytes"), "gguf_path": cur.get("gguf_path"), "incomplete": cur.get("incomplete")} # Fit-Check: passt das (immer warme) Hirn + das größte on-demand-Modell zusammen ins Budget? budget = None try: from services.budget import footprint_gb, gtt_budget_gb groups = llamaswap.list_groups() persist = set() for g in groups.values(): if isinstance(g, dict) and (g.get("persist") or g.get("persistent")): persist.update(g.get("members") or []) cur_name = cur["name"] if cur else None brain_gb = footprint_gb(cur) if cur else 0.0 # voller Always-Warm-Footprint (alle persist, Brain=Empfehlung) — nur Info warm = brain_gb + sum(footprint_gb(m) for m in models if m["name"] in persist and m["name"] != cur_name) largest_od = max((footprint_gb(m) for m in models if m["name"] not in persist), default=0.0) gtt = gtt_budget_gb() # Seit dem `persistent`-Fix (03.07.) bleibt das GANZE Warmset (Hirn+embed+vision) # resident, wenn ein on-demand-Modell DANEBEN lädt → der reale Peak ist Warmset + # größtes on-demand, nicht nur Hirn + größtes. Genau daran wird `fits` gemessen. budget = { "gtt_gb": gtt, "brain_gb": round(brain_gb, 1), "warm_projected_gb": round(warm, 1), "largest_ondemand_gb": round(largest_od, 1), "fits": (warm + largest_od) <= gtt, "free_after_gb": round(gtt - warm - largest_od, 1), } except Exception: log.debug("hermes_brain_info: Budget-Berechnung fehlgeschlagen", exc_info=True) return {"current": current, "recommended": None, "update_available": False, "budget": budget} def _reach(url: str, path: str = "") -> bool: try: with httpx.Client(timeout=3.0) as c: return c.get(f"{url}{path}").status_code < 500 except httpx.HTTPError: return False def agent_status() -> dict: """Erreichbarkeit des Hermes-Gateways (:8642) und des Hermes-Dashboards — für die Dienste-Liste. Bis 24.09.2026 fragte die Funktion bei jedem Aufruf auch die abgebaute Konsole und den PC-Ausführer ab (3 s Zeitlimit) und las die Hermes-Config, obwohl nur diese Felder gebraucht werden. Seit der PC-Ausführer schläft, hätte das jede Dienste-Abfrage um 3 s verzögert.""" return { "gateway_url": HERMES_API_URL, # Eingebaute Hermes-Web-GUI (hermes serve): same-origin über MC2 geproxyt (/hermes-ui/). "hermes_ui_url": HERMES_BUILTIN_UI_PATH, "gateway_reachable": _reach(HERMES_API_URL, "/health"), "hermes_ui_reachable": _reach(HERMES_BUILTIN_UI_UPSTREAM, "/"), } def set_agent_brain(model_id: str) -> dict: """Setzt ein (bereits installiertes) Modell als Agent-Hirn — WARM-bewusst: 1) vergibt den 'hermes'-Alias (das Agent-Hirn-Slot), 2) tauscht es in die residente brains-Gruppe (altes Hirn raus, fast/vision bleiben), 3) zeigt die Hermes-Config auf den 'hermes'-Alias + Gateway-Restart. So bleibt das neue Hirn warm und der Agent nutzt es sofort.""" from services import llamaswap models = {m["name"]: m for m in llamaswap.list_models()} if model_id not in models: return {"ok": False, "reason": "Modell nicht installiert — erst über Modelle-finden laden."} old = next((m["name"] for m in models.values() if m.get("role") == "hermes"), None) if model_id == old: # Idempotent härten: auch wenn schon Hirn, warm (brains) + ttl 0 sicherstellen. try: from services.llamaswap import set_ttl brains = (llamaswap.list_groups().get("brains") or {}).get("members") or [] if model_id not in brains: llamaswap.set_group("brains", brains + [model_id], swap=False, persist=True) set_ttl(model_id, 0) except PermissionError as exc: return {"ok": False, "reason": str(exc)} return {"ok": True, "old": old, "new": model_id, "note": "ist bereits das Agent-Hirn"} try: llamaswap.set_role(model_id, "hermes") # 1) Alias brains = (llamaswap.list_groups().get("brains") or {}).get("members") or [] new_members = [x for x in brains if x not in (old, model_id)] + [model_id] llamaswap.set_group("brains", new_members, swap=False, persist=True) # 2) warm # 2b) TTL härten: neues Hirn nie auto-entladen; altes Hirn auf Default entspannen. from services.llamaswap import DEFAULT_TTL, set_ttl set_ttl(model_id, 0) if old: set_ttl(old, DEFAULT_TTL) except PermissionError as exc: return {"ok": False, "reason": str(exc)} update_brain_model("hermes") # 3) Config + Restart # Weiche Budget-Warnung (kein Hard-Block): passt Hirn + größtes on-demand zusammen ins GTT? warning = None try: b = hermes_brain_info().get("budget") or {} if b and not b.get("fits", True): warning = (f"Speicher-Warnung: Hirn (~{b.get('brain_gb')} GB) + größtes on-demand-" f"Modell (~{b.get('largest_ondemand_gb')} GB) übersteigen das GTT-Budget " f"(~{b.get('gtt_gb')} GB) — das Hirn ist persistent, heavy/coder laden " f"DANEBEN: Überlauf droht (Lade-Crash/Swapping statt Verdrängung).") except Exception: log.debug("set_agent_brain: Budget-Check fehlgeschlagen", exc_info=True) return {"ok": True, "old": old, "new": model_id, "warning": warning} def update_brain_model(new_model: str) -> bool: """Setzt Lucys Hirn in Hermes' config.yaml (model.default + model.model) und startet den Hermes-Gateway neu. Seit 24.09.2026 vorsichtig: Ist die Datei nicht lesbar (halb geschrieben, Syntaxfehler), wird NICHTS geschrieben — früher entstand dann eine neue Datei nur mit dem model-Block, und der Rest von Hermes' Konfiguration wäre weg gewesen. Geschrieben wird atomar (tmp + os.replace), vorher liegt eine Sicherung config.yaml.bak-hirn- daneben.""" import shutil import time as _time from config import HERMES_HOME from ruamel.yaml import YAML config_path = HERMES_HOME / "config.yaml" if not config_path.exists(): log.warning("update_brain_model: %s fehlt — Hirn wird nicht umgestellt", config_path) return False r_yaml = YAML() r_yaml.preserve_quotes = True r_yaml.width = 100 r_yaml.indent(mapping=2, sequence=4, offset=2) try: with config_path.open("r", encoding="utf-8") as f: cfg = r_yaml.load(f) except Exception: log.warning("update_brain_model: config.yaml nicht lesbar — nichts geschrieben", exc_info=True) return False if not isinstance(cfg, dict): log.warning("update_brain_model: config.yaml hat unerwarteten Inhalt — nichts geschrieben") return False if "model" not in cfg or not isinstance(cfg["model"], dict): cfg["model"] = {} # Hermes liest model.default als aktives Modell; model.model ist der Provider-Param. # Beide setzen, sonst greift die Umschaltung nicht. cfg["model"]["default"] = new_model cfg["model"]["model"] = new_model tmp = config_path.with_suffix(".yaml.neu") try: shutil.copy2(config_path, config_path.with_name(f"config.yaml.bak-hirn-{_time.strftime('%Y%m%d-%H%M%S')}")) with tmp.open("w", encoding="utf-8") as f: r_yaml.dump(cfg, f) YAML(typ="safe").load(tmp.read_text(encoding="utf-8")) # muss wieder lesbar sein os.replace(tmp, config_path) except Exception: log.warning("update_brain_model: Schreiben der config.yaml fehlgeschlagen", exc_info=True) tmp.unlink(missing_ok=True) return False try: from services import maintenance maintenance.restart_service("hermes-gateway") except Exception: log.warning("update_brain_model: hermes-gateway-Restart fehlgeschlagen", exc_info=True) return True