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:
@@ -3,7 +3,9 @@
|
||||
from fastapi import APIRouter
|
||||
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")
|
||||
|
||||
@@ -12,6 +14,10 @@ class BrainReq(BaseModel):
|
||||
model: str
|
||||
|
||||
|
||||
class SetBrainReq(BaseModel):
|
||||
model_id: str
|
||||
|
||||
|
||||
@router.get("/agent/status")
|
||||
def status() -> dict:
|
||||
return agent_status()
|
||||
@@ -23,6 +29,15 @@ def brain_info() -> dict:
|
||||
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")
|
||||
def set_brain_model(body: BrainReq) -> dict:
|
||||
ok = update_brain_model(body.model)
|
||||
|
||||
@@ -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:
|
||||
from config import HERMES_HOME
|
||||
home = HERMES_HOME
|
||||
|
||||
+397
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
-397
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -7,8 +7,8 @@
|
||||
<link rel="manifest" href="/manifest.webmanifest" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<title>Mission Control 2.0</title>
|
||||
<script type="module" crossorigin src="/assets/index-lvQamTQ3.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-D4NZ6il2.css">
|
||||
<script type="module" crossorigin src="/assets/index-Bb8BpQr4.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-bw6S4bF_.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -29,6 +29,7 @@ export function Cockpit() {
|
||||
const [activeClient, setActiveClient] = useState<"roocode" | "cursor" | "opencode" | "zed" | "continue" | null>(null)
|
||||
const [activeRoleForAssign, setActiveRoleForAssign] = useState<string | null>(null)
|
||||
const [specModel, setSpecModel] = useState<ModelInfo | null>(null)
|
||||
const [showBrainSwitch, setShowBrainSwitch] = useState(false)
|
||||
const [copied, setCopied] = useState(false)
|
||||
const [hoveredNode, setHoveredNode] = useState<string | null>(null)
|
||||
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) {
|
||||
if (!snippet) return
|
||||
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>
|
||||
)}
|
||||
</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 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}
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user