Files
mission-control-v2/backend/services/agent.py
T
Hitonabi 510de69250 Feat: Discover zukunftssicher (Recency-Score) + Agent-Hirn (Hermes) im Modell-Manager
- discover.rank_runnable: Score aus Fit + Recency (lastModified, Halbwertszeit ~9 Mon)
  + Capability (params, log) + Popularity (downloads, log) -> neuere Generationen bevorzugt.
- Agent-Hirn: neuer GET /api/agent/brain (aktuelles hermes-Modell + bestes NousResearch-
  Hermes-Update, versions-aware via Hermes-X.Y-Parsing). Cockpit zeigt "Agent-Hirn (Hermes)"-Karte
  mit aktuellem Brain + Aktualisieren-Button, wenn NousResearch eine neuere Generation hat
  (z.B. Hermes-4-14B -> Hermes-4.3-36B). Update installiert mit Rolle hermes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 02:44:27 +02:00

187 lines
7.0 KiB
Python

"""
Hermes-Agent-Status (Control-Plane-Read). MC betreibt Hermes NICHT — es zeigt nur
Status + verlinkt das standalone hermes-webui. Voller Zugriff + Tools/MCP werden in
Hermes' eigener Config verdrahtet (siehe docs/HERMES_SETUP.md).
"""
import logging
import os
import re
import httpx
import psutil
from config import ANYTHINGLLM_URL, HERMES_API_URL, HERMES_HOME, PC_EXECUTOR_URL
log = logging.getLogger(__name__)
def _hermes_version(name: str) -> float | None:
"""Versionszahl aus 'Hermes-4.3', 'Hermes-4', 'Nous-Hermes-2' → 4.3/4.0/2.0."""
low = (name or "").lower()
if "hermes" not in low:
return None
m = re.search(r"hermes[-_ ]?(\d+(?:\.\d+)?)", low)
return float(m.group(1)) if m else None
def hermes_brain_info() -> dict:
"""Aktuelles Agent-Hirn (hermes-Rolle) + bestes verfügbares NousResearch-Hermes-Modell,
das auf diese Hardware passt. Für den Modell-Manager: Brain sichtbar + updatebar,
sobald NousResearch eine neuere Hermes-Generation veröffentlicht."""
from services import discover, llamaswap
from services.fit import evaluate_fit, extract_params_b
models = llamaswap.list_models()
cur = next((m for m in models if m.get("role") == "hermes"), None)
cur_ver = _hermes_version(cur["name"]) if cur else None
cur_params = (cur.get("capabilities") or {}).get("params_b") if cur else None
current = None
if cur:
current = {"name": cur["name"], "filename": cur.get("filename"),
"params_b": cur_params, "quant": cur.get("quant"),
"size_bytes": cur.get("size_bytes"), "version": cur_ver,
"gguf_path": cur.get("gguf_path"), "incomplete": cur.get("incomplete")}
ram = psutil.virtual_memory().total / (1024 ** 3)
best = None
try:
cands = []
for r in discover._fetch_author_models("NousResearch"):
rid = r.get("id", "")
if "hermes" not in rid.lower():
continue
pb = extract_params_b(rid)
fit = evaluate_fit(pb, "Q4_K_M", 8192, ram, name=rid)
if fit["level"] == "too_tight":
continue
cands.append({"repo": rid, "name": rid.split("/")[-1],
"version": _hermes_version(rid) or 0.0, "params_b": pb,
"downloads": int(r.get("downloads") or 0), "fit": fit})
# neueste Hermes-Version zuerst, dann größer/fähiger, dann beliebter
cands.sort(key=lambda c: (c["version"], c["params_b"], c["downloads"]), reverse=True)
best = cands[0] if cands else None
except Exception:
log.debug("hermes_brain_info: HF-Abfrage fehlgeschlagen", exc_info=True)
update = False
if best is not None:
if cur_ver is None:
update = True
elif best["version"] > cur_ver:
update = True
elif best["version"] == cur_ver and best["params_b"] > (cur_params or 0) * 1.05:
update = True
# gleiche Datei schon installiert? dann kein Update
if current and best["repo"].split("/")[-1].lower() in (current["name"] or "").lower():
update = False
return {"current": current, "recommended": best, "update_available": update}
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 _count_enabled_mcp_servers() -> int:
config_path = HERMES_HOME / "config.yaml"
if not config_path.exists():
return 0
try:
from ruamel.yaml import YAML
r_yaml = YAML()
with config_path.open("r", encoding="utf-8") as f:
cfg = r_yaml.load(f) or {}
mcp_servers = cfg.get("mcp_servers", {}) if isinstance(cfg, dict) else {}
if not isinstance(mcp_servers, dict):
return 0
return sum(1 for v in mcp_servers.values() if isinstance(v, dict) and v.get("enabled", True))
except Exception:
log.debug("_count_enabled_mcp_servers: Fehler", exc_info=True)
return 0
def agent_status() -> dict:
"""Erreichbarkeit von Gateway (:8642) + WebUI (:8787) + lokale Hinweise."""
home = HERMES_HOME
brain_model = "auto"
config_path = home / "config.yaml"
if config_path.exists():
try:
from ruamel.yaml import YAML
r_yaml = YAML()
with config_path.open("r", encoding="utf-8") as f:
cfg = r_yaml.load(f) or {}
if isinstance(cfg, dict):
brain_model = cfg.get("model", {}).get("model", "auto")
except Exception:
log.debug("agent_status: Hermes-config.yaml nicht lesbar", exc_info=True)
return {
"gateway_url": HERMES_API_URL,
# Chat-WebUI ist jetzt AnythingLLM (eigener Host), nicht mehr hermes-webui :8787.
"webui_url": ANYTHINGLLM_URL,
"gateway_reachable": _reach(HERMES_API_URL, "/health"),
"webui_reachable": _reach(ANYTHINGLLM_URL, "/api/ping"),
"home_exists": home.exists(),
"brain_model": brain_model,
# Best-effort: welche Verdrahtung lokal sichtbar ist (auf der Box aussagekräftig).
"has_config": (home / "config.yaml").exists() or (home / "config.json").exists(),
"has_skills": (home / "skills").exists(),
"has_memories": (home / "memories").exists(),
# Neue Felder: Telegram, MCP-Server-Anzahl, PC-Executor-Erreichbarkeit.
"telegram_enabled": bool(os.environ.get("TELEGRAM_BOT_TOKEN", "")),
"mcp_server_count": _count_enabled_mcp_servers(),
"pc_executor_reachable": _reach(PC_EXECUTOR_URL, "/health"),
}
def update_brain_model(new_model: str) -> bool:
from config import HERMES_HOME
home = HERMES_HOME
config_path = home / "config.yaml"
# Ensure home directory exists
home.mkdir(parents=True, exist_ok=True)
cfg = {}
if config_path.exists():
try:
from ruamel.yaml import YAML
r_yaml = YAML()
with config_path.open("r", encoding="utf-8") as f:
cfg = r_yaml.load(f) or {}
except Exception:
log.debug("update_brain_model: bestehende config.yaml nicht lesbar", exc_info=True)
cfg = {}
if not isinstance(cfg, dict):
cfg = {}
if "model" not in cfg or not isinstance(cfg["model"], dict):
cfg["model"] = {}
cfg["model"]["model"] = new_model
try:
from ruamel.yaml import YAML
r_yaml = YAML()
with config_path.open("w", encoding="utf-8") as f:
r_yaml.dump(cfg, f)
# Restart the user-space service to apply changes
try:
import services.maintenance as maintenance
maintenance.restart_service("hermes-gateway")
except Exception:
log.warning("update_brain_model: hermes-gateway-Restart fehlgeschlagen", exc_info=True)
return True
except Exception:
log.warning("update_brain_model: Schreiben der config.yaml fehlgeschlagen", exc_info=True)
return False