""" 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}