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>
This commit is contained in:
Hitonabi
2026-06-27 02:44:27 +02:00
parent 8bb4e11f31
commit 510de69250
11 changed files with 606 additions and 411 deletions
+7 -1
View File
@@ -3,7 +3,7 @@
from fastapi import APIRouter
from pydantic import BaseModel
from services.agent import agent_status, update_brain_model
from services.agent import agent_status, hermes_brain_info, update_brain_model
router = APIRouter(prefix="/api")
@@ -17,6 +17,12 @@ def status() -> dict:
return agent_status()
@router.get("/agent/brain")
def brain_info() -> dict:
"""Aktuelles Agent-Hirn (hermes) + bestes NousResearch-Hermes-Update."""
return hermes_brain_info()
@router.post("/agent/brain")
def set_brain_model(body: BrainReq) -> dict:
ok = update_brain_model(body.model)
+64
View File
@@ -6,14 +6,78 @@ 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:
+34 -10
View File
@@ -9,8 +9,10 @@ spätere Auto-Setups nutzen ihn, damit sie nie auseinanderlaufen.
"""
import json
import math
import os
import time
from datetime import datetime
import httpx
@@ -46,19 +48,41 @@ def _fetch_author_models(author: str) -> list:
return []
def _age_days(last_modified, now_ts: float) -> float:
"""Alter eines HF-Modells in Tagen (lastModified ISO). Unbekannt → ~1.5 Jahre."""
if not last_modified:
return 540.0
try:
dt = datetime.fromisoformat(str(last_modified).replace("Z", "+00:00"))
return max((now_ts - dt.timestamp()) / 86400.0, 0.0)
except Exception:
return 540.0
def _score(m: dict, now_ts: float) -> float:
"""Zukunftssicherer Rang-Score für DIESE Hardware. Kombiniert:
- Fit: perfect dominiert (Bonus 3.0 > Summe der übrigen Terme → passt-komfortabel zuerst),
- Recency: neuere Generationen bevorzugt (Halbwertszeit ~9 Monate über lastModified),
- Capability: mehr Parameter (log-skaliert),
- Popularity: Downloads (log-skaliert).
So gewinnt bei vergleichbarer Größe die NEUERE Generation (z.B. Qwen3-Coder vor
Qwen2.5-Coder), ohne dass kleine Populär-Modelle große verdrängen."""
fit_bonus = 3.0 if m["fit"]["level"] == "perfect" else 0.0
recency = 0.5 ** (_age_days(m.get("lastModified"), now_ts) / 270.0)
cap = math.log2(max(float(m.get("params_b") or 1.0), 1.0) + 1.0) / 8.0
pop = math.log10(float(m.get("downloads") or 0) + 1.0) / 7.0
return fit_bonus + 1.2 * recency + 1.2 * cap + 0.5 * pop
def rank_runnable(models: list[dict]) -> list[dict]:
"""EINE Quelle der Wahrheit fürs Ranking lauffähiger Modelle für DIESE Hardware:
1) nur was komfortabel passt (perfect vor marginal, too_tight fliegt raus),
2) das FÄHIGSTE zuerst — mehr Parameter = mehr Können (bei MoE bleibt es dank
aktiver-Param-Schätzung schnell),
3) bei Gleichstand das meistgeladene.
So bevorzugt die 128-GB-Box große (MoE-)Modelle statt kleiner Populär-Modelle —
und „Modelle finden" schlägt nie ein Downgrade vor (z.B. 35B-A3B → 4B)."""
"""EINE Quelle der Wahrheit fürs Ranking lauffähiger Modelle für DIESE Hardware.
Nur was passt (too_tight fliegt raus), dann nach `_score` (Fit + Recency + Capability
+ Popularity). Bevorzugt neuere, fähige Modelle → zukunftssicher; „Modelle finden"
schlägt nie ein Downgrade vor (Downgrade-Sperre zusätzlich in maintenance)."""
now_ts = time.time()
return sorted(
[m for m in models if m["fit"]["level"] != "too_tight"],
key=lambda m: (_FIT_ORDER[m["fit"]["level"]],
-float(m.get("params_b") or 0.0),
-int(m.get("downloads") or 0)),
key=lambda m: -_score(m, now_ts),
)