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 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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+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="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>
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user