763e634dfd
Analog zum Auto-ctx-Button: das Rollen-Zuweisungs-Modal empfiehlt jetzt, welches
INSTALLIERTE Modell am besten auf die Rolle passt - capability-getrieben (Vision/Coder/
Tools/MoE aus services.caps) + setup-bewusster Fit (services.budget, gleiche Mathematik
wie Install-Automatik & Auto-ctx).
- services/roles.py: recommend_for_role() rankt installierte Modelle (Eignung + Fit + Tempo
+ Wissen); harte Anforderungen (Vision braucht Vision, Hirn braucht Tools) schliessen aus.
- GET /api/roles/{role}/recommend
- Cockpit-Modal: »Auto: <Modell>«-Button im Header, »Empfohlen«-Badge, Sortierung nach Score,
pro Zeile Fit + Begruendung (~t/s); ungeeignete gedimmt mit Klartext-Grund.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
110 lines
4.3 KiB
Python
110 lines
4.3 KiB
Python
"""
|
|
Rollen-Empfehlung: welches INSTALLIERTE Modell passt am besten auf eine Serving-Rolle?
|
|
Capability-getrieben (Vision/Coder/Tools/MoE aus services.caps) + setup-bewusster Fit
|
|
(services.budget). Speist den 'Empfohlen'-Hinweis + Auto-Pick im Rollen-Zuweisungs-Modal.
|
|
|
|
EINE Quelle der Wahrheit mit der ctx-/Fit-Logik: nutzt budget.setup_aware_ctx_for_model
|
|
und fit.evaluate_fit — dieselbe Mathematik wie Install-Automatik und Auto-ctx-Button.
|
|
"""
|
|
|
|
import psutil
|
|
|
|
from services import budget, llamaswap
|
|
from services.fit import evaluate_fit
|
|
|
|
|
|
def _ram_gb() -> float:
|
|
return psutil.virtual_memory().total / (1024 ** 3)
|
|
|
|
|
|
def _suitability(role: str, caps: dict, params: float, name: str) -> float:
|
|
"""0..1 — wie gut passt die Capability eines Modells zur Rolle. Harte Anforderungen
|
|
(Vision braucht Vision) geben 0 bei Nichterfüllung; weiche Präferenzen skalieren."""
|
|
role = (role or "").lower()
|
|
low = (name or "").lower()
|
|
vision = bool(caps.get("vision"))
|
|
coder = bool(caps.get("coder"))
|
|
tools = caps.get("tools") != "no"
|
|
moe = bool(caps.get("moe"))
|
|
|
|
if role == "vision":
|
|
return 1.0 if vision else 0.0 # harte Anforderung
|
|
if role == "coder":
|
|
return 1.0 if coder else 0.45 # Coder bevorzugt, andere notfalls
|
|
if role == "hermes":
|
|
# Agent-Hirn: natives Tool-Calling Pflicht; Hermes-Familie am robustesten.
|
|
if "hermes" in low:
|
|
return 1.0
|
|
return 0.85 if tools else 0.15
|
|
if role == "fast":
|
|
# schnelles Alltags-Hirn: klein/MoE bevorzugt (niedrige aktive Params = Tempo).
|
|
return 1.0 if (moe or params <= 40) else 0.5
|
|
if role == "heavy":
|
|
# schweres Reasoning: Wissen = Gesamt-Params (groß bevorzugt).
|
|
return min(params / 70.0, 1.0)
|
|
if role == "scout":
|
|
# Multimodal-Allrounder: Vision ein Plus, sonst solide Basis.
|
|
return 0.9 if vision else 0.7
|
|
return 0.5
|
|
|
|
|
|
def _reason(role: str, caps: dict, fit: dict, suit: float, fits: bool, incomplete: bool) -> str:
|
|
if incomplete:
|
|
return "Download unvollständig"
|
|
if role == "vision" and not caps.get("vision"):
|
|
return "keine Vision-Fähigkeit"
|
|
if role == "hermes" and caps.get("tools") == "no":
|
|
return "kein natives Tool-Calling"
|
|
if not fits:
|
|
return "passt nicht ins Budget (OOM)"
|
|
bits = []
|
|
if role == "vision":
|
|
bits.append("Vision ✓")
|
|
if role == "coder" and caps.get("coder"):
|
|
bits.append("Coder ✓")
|
|
if role == "hermes":
|
|
bits.append("Tools ✓" if caps.get("tools") != "no" else "ohne Tools")
|
|
if caps.get("moe"):
|
|
bits.append("MoE")
|
|
bits.append(f"{fit['text']}, ~{fit['tps']:.0f} t/s")
|
|
return " · ".join(bits)
|
|
|
|
|
|
def recommend_for_role(role: str) -> dict:
|
|
"""Rankt alle installierten Modelle für eine Rolle. Empfohlen = bester geeigneter,
|
|
passender Eintrag. Liefert pro Modell Fit/Eignung/Begründung fürs UI."""
|
|
role = (role or "").strip().lower()
|
|
ram = _ram_gb()
|
|
out = []
|
|
for m in llamaswap.list_models():
|
|
caps = m.get("capabilities") or {}
|
|
params = budget.params_of_model(m)
|
|
quant = m.get("quant") or "Q4_K_M"
|
|
ctx = budget.setup_aware_ctx_for_model(m)["ctx"]
|
|
fit = evaluate_fit(params, quant, ctx, ram, name=m["name"])
|
|
incomplete = bool(m.get("incomplete"))
|
|
fits = (fit["level"] != "too_tight") and not incomplete
|
|
suit = _suitability(role, caps, params, m["name"])
|
|
suitable = suit >= 0.5 and fits
|
|
|
|
fit_term = {"perfect": 1.0, "marginal": 0.3}.get(fit["level"], -2.0)
|
|
score = (2.0 * suit) + fit_term \
|
|
+ min((fit["tps"] or 0) / 80.0, 1.0) * 0.5 \
|
|
+ min(params / 120.0, 1.0) * 0.5
|
|
if not fits:
|
|
score -= 5.0
|
|
|
|
out.append({
|
|
"name": m["name"], "current_role": m.get("role"),
|
|
"params_b": round(params, 1), "quant": quant,
|
|
"fit": fit, "suitable": suitable, "incomplete": incomplete,
|
|
"score": round(score, 3),
|
|
"reason": _reason(role, caps, fit, suit, fits, incomplete),
|
|
})
|
|
|
|
out.sort(key=lambda x: -x["score"])
|
|
rec = next((o["name"] for o in out if o["suitable"]), None)
|
|
for o in out:
|
|
o["recommended"] = (o["name"] == rec)
|
|
return {"role": role, "recommended": rec, "models": out}
|