Feat: warm-bewusster Agent-Hirn-Wechsel (beliebiges installiertes Modell)

- agent.set_agent_brain(model_id): vergibt 'hermes'-Alias, tauscht das Modell in die
  residente brains-Gruppe (altes Hirn raus, fast/vision bleiben) und zeigt die Hermes-Config
  darauf (+ Gateway-Restart). Behebt: AgentView-Switch hielt das neue Hirn nicht warm.
- POST /api/agent/brain/set.
- Cockpit Agent-Hirn-Karte: Button "Hirn wechseln" + Modal mit allen installierten Modellen
  (Groesse/Params/Rolle/Tools), aktuelles markiert; Hinweis zugunsten Hermes (Tool-Calling).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-27 12:25:31 +02:00
parent 45e74a97bd
commit 6758bbfbd9
8 changed files with 518 additions and 402 deletions
+16 -1
View File
@@ -3,7 +3,9 @@
from fastapi import APIRouter from fastapi import APIRouter
from pydantic import BaseModel from pydantic import BaseModel
from services.agent import agent_status, hermes_brain_info, update_brain_model from fastapi import HTTPException
from services.agent import agent_status, hermes_brain_info, set_agent_brain, update_brain_model
router = APIRouter(prefix="/api") router = APIRouter(prefix="/api")
@@ -12,6 +14,10 @@ class BrainReq(BaseModel):
model: str model: str
class SetBrainReq(BaseModel):
model_id: str
@router.get("/agent/status") @router.get("/agent/status")
def status() -> dict: def status() -> dict:
return agent_status() return agent_status()
@@ -23,6 +29,15 @@ def brain_info() -> dict:
return hermes_brain_info() return hermes_brain_info()
@router.post("/agent/brain/set")
def set_brain(body: SetBrainReq) -> dict:
"""Setzt ein installiertes Modell als Agent-Hirn (Alias + warm-Gruppe + Config)."""
res = set_agent_brain(body.model_id)
if not res.get("ok"):
raise HTTPException(400, res.get("reason", "Fehler beim Setzen des Agent-Hirns"))
return res
@router.post("/agent/brain") @router.post("/agent/brain")
def set_brain_model(body: BrainReq) -> dict: def set_brain_model(body: BrainReq) -> dict:
ok = update_brain_model(body.model) ok = update_brain_model(body.model)
+24
View File
@@ -207,6 +207,30 @@ def agent_status() -> dict:
} }
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:
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
except PermissionError as exc:
return {"ok": False, "reason": str(exc)}
update_brain_model("hermes") # 3) Config + Restart
return {"ok": True, "old": old, "new": model_id}
def update_brain_model(new_model: str) -> bool: def update_brain_model(new_model: str) -> bool:
from config import HERMES_HOME from config import HERMES_HOME
home = HERMES_HOME home = HERMES_HOME
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -7,8 +7,8 @@
<link rel="manifest" href="/manifest.webmanifest" /> <link rel="manifest" href="/manifest.webmanifest" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>Mission Control 2.0</title> <title>Mission Control 2.0</title>
<script type="module" crossorigin src="/assets/index-lvQamTQ3.js"></script> <script type="module" crossorigin src="/assets/index-Bb8BpQr4.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-D4NZ6il2.css"> <link rel="stylesheet" crossorigin href="/assets/index-bw6S4bF_.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+78 -1
View File
@@ -29,6 +29,7 @@ export function Cockpit() {
const [activeClient, setActiveClient] = useState<"roocode" | "cursor" | "opencode" | "zed" | "continue" | null>(null) const [activeClient, setActiveClient] = useState<"roocode" | "cursor" | "opencode" | "zed" | "continue" | null>(null)
const [activeRoleForAssign, setActiveRoleForAssign] = useState<string | null>(null) const [activeRoleForAssign, setActiveRoleForAssign] = useState<string | null>(null)
const [specModel, setSpecModel] = useState<ModelInfo | null>(null) const [specModel, setSpecModel] = useState<ModelInfo | null>(null)
const [showBrainSwitch, setShowBrainSwitch] = useState(false)
const [copied, setCopied] = useState(false) const [copied, setCopied] = useState(false)
const [hoveredNode, setHoveredNode] = useState<string | null>(null) const [hoveredNode, setHoveredNode] = useState<string | null>(null)
const [viewMode, setViewMode] = useState<"grid" | "list">("grid") const [viewMode, setViewMode] = useState<"grid" | "list">("grid")
@@ -199,6 +200,23 @@ export function Cockpit() {
) )
} }
async function handleSetBrain(modelName: string) {
showConfirm(
"Agent-Hirn wechseln?",
`'${modelName.split("/").pop()}' als Agent-Hirn (Alias hermes) setzen? Es wird warm gehalten (brains-Gruppe); Hermes nutzt es nach einem kurzen Gateway-Restart.`,
async () => {
try {
await api("/api/agent/brain/set", { method: "POST", body: JSON.stringify({ model_id: modelName }) })
showAlert("Erledigt", "Agent-Hirn gewechselt. Gateway wurde neugestartet.")
setShowBrainSwitch(false)
reload()
} catch (e: any) {
showAlert("Fehler", `Wechsel fehlgeschlagen: ${e.message || e}`)
}
}
)
}
async function copySnippet(snippet?: string) { async function copySnippet(snippet?: string) {
if (!snippet) return if (!snippet) return
await navigator.clipboard.writeText(snippet) await navigator.clipboard.writeText(snippet)
@@ -663,7 +681,12 @@ export function Cockpit() {
<span className="text-[9px] font-mono px-1.5 py-0.5 rounded bg-indigo-500/15 text-indigo-300 border border-indigo-500/25">v{brain.current.version}</span> <span className="text-[9px] font-mono px-1.5 py-0.5 rounded bg-indigo-500/15 text-indigo-300 border border-indigo-500/25">v{brain.current.version}</span>
)} )}
</div> </div>
<span className="text-[10px] font-mono text-muted-foreground hidden sm:block">Modell, das der Hermes-Agent als Gehirn nutzt</span> <button
onClick={() => setShowBrainSwitch(true)}
className="h-7 px-3 rounded-lg border border-indigo-500/30 bg-indigo-500/5 text-indigo-300 text-[9px] font-bold uppercase hover:bg-indigo-500/15 transition-all cursor-pointer"
>
Hirn wechseln
</button>
</div> </div>
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 rounded-xl bg-background/30 border border-border/30 p-3.5"> <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 rounded-xl bg-background/30 border border-border/30 p-3.5">
@@ -1145,6 +1168,60 @@ export function Cockpit() {
/> />
)} )}
{/* Agent-Hirn wechseln */}
{showBrainSwitch && (
<div className="fixed inset-0 z-50 bg-black/60 backdrop-blur-sm flex items-center justify-center p-4">
<div className="w-full max-w-md rounded-2xl border border-border/80 bg-card p-6 shadow-2xl space-y-4 animate-in fade-in zoom-in-95 duration-200">
<div className="flex items-center justify-between border-b border-border/20 pb-2">
<h3 className="text-xs font-bold uppercase tracking-wider text-primary flex items-center gap-1.5">
<Bot className="h-4 w-4" /> Agent-Hirn wechseln
</h3>
<button onClick={() => setShowBrainSwitch(false)} className="p-1 rounded hover:bg-accent text-muted-foreground hover:text-foreground cursor-pointer">
<X className="h-4 w-4" />
</button>
</div>
<p className="text-[11px] text-muted-foreground leading-relaxed">
Wähle ein <strong className="text-foreground">installiertes</strong> Modell als Agent-Hirn. Es bekommt den
<code className="text-primary"> hermes</code>-Alias, wird warm gehalten (brains-Gruppe), und Hermes nutzt es nach
einem kurzen Gateway-Restart. Neues Modell (z.B. Hermes-4.3 oder Gemma-4)? Erst über Modelle finden" laden.
</p>
<div className="space-y-1.5 max-h-72 overflow-y-auto pr-1">
{models.map((m) => {
const isBrain = m.role === "hermes"
return (
<button
key={m.name}
onClick={() => !isBrain && handleSetBrain(m.name)}
disabled={isBrain || m.incomplete}
className={cn(
"w-full text-left px-3 py-2.5 rounded-lg text-xs flex items-center justify-between font-mono border transition-all",
isBrain ? "border-indigo-500/40 bg-indigo-500/10 text-indigo-300"
: m.incomplete ? "border-border/20 bg-background/10 text-muted-foreground/50 cursor-not-allowed"
: "border-border/30 bg-background/20 text-foreground hover:bg-accent cursor-pointer"
)}
>
<div className="flex flex-col min-w-0">
<span className="font-semibold truncate max-w-[280px]">{m.name.split("/").pop()?.replace(/\.gguf$/i, "")}</span>
<span className="text-[9px] text-muted-foreground mt-0.5">
{m.capabilities.params_b ? `${m.capabilities.params_b}B` : "?"} · {fmtSize(m.size_bytes)}
{m.role && ` · Rolle: ${m.role}`}{m.capabilities.tools !== "no" ? " · Tools ✓" : " · ohne Tools"}
</span>
</div>
{isBrain
? <span className="text-[9px] font-bold uppercase text-indigo-300 shrink-0 flex items-center gap-1"><Check className="h-3.5 w-3.5" /> Aktiv</span>
: <span className="text-[9px] font-bold uppercase text-primary/70 shrink-0">Als Hirn setzen </span>}
</button>
)
})}
</div>
<div className="text-[10px] text-amber-400/80 flex items-start gap-1.5 border-t border-border/20 pt-2.5">
<span>💡</span>
<span>Für einen <strong>Agenten</strong> sind Hermes-Modelle (natives Tool-Calling) meist die robustere Wahl als allgemeine Modelle.</span>
</div>
</div>
</div>
)}
{dialogElement} {dialogElement}
</div> </div>
) )