Feat: Engine-Update-Mechanismus + Update-Checks (Hermes Agent/AnythingLLM) + WebUI->AnythingLLM
- Engine-Update: deploy/update-engine.sh laedt neuesten ggml-org Vulkan-Build + restart; MC_ENGINE_UPDATE_CMD verdrahtet, engine_update_job nutzt es (Script macht Restart selbst). - Update-Checks: Hermes Agent (NousResearch/hermes-agent, Release-Datum vs. installiertes Commit) + AnythingLLM (Mintplex-Labs/anything-llm, neueste Version + /api/ping-Health), 1h-Cache. Neues Feld /api/maintenance/updates.components; UpdatesCard zeigt beide Zeilen. - WebUI -> AnythingLLM: agent_status.webui_* zeigt jetzt auf ANYTHINGLLM_URL (192.168.178.155:3001, /api/ping); alle "Hermes WebUI"-Buttons/Labels (AgentView, AgentStatusCard, nav, GuideView, Services-Liste) -> "AnythingLLM". Lokaler hermes-webui-Dienst bleibt als Service-Control im SystemDrawer. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+6
-1
@@ -53,9 +53,14 @@ HF_DOWNLOAD_ENV = {"HF_HUB_DISABLE_XET": "1"}
|
||||
GATEWAY_URL = os.environ.get("MC_GATEWAY_URL", f"http://127.0.0.1:{os.environ.get('MC_PORT', '9000')}").rstrip("/")
|
||||
|
||||
# --- Hermes Agent (eigener Dienst auf der Box) -------------------------------
|
||||
# Gateway (OpenAI-API des Agenten) + standalone Web-UI (nesquena/hermes-webui).
|
||||
# Gateway (OpenAI-API des Agenten) + Chat-WebUI = AnythingLLM (eigener Host).
|
||||
HERMES_API_URL = os.environ.get("HERMES_API_URL", "http://127.0.0.1:8642").rstrip("/")
|
||||
HERMES_WEBUI_URL = os.environ.get("HERMES_WEBUI_URL", "http://127.0.0.1:8787").rstrip("/")
|
||||
# AnythingLLM = die Chat-WebUI (löst nesquena/hermes-webui ab). Eigener Host im LAN.
|
||||
ANYTHINGLLM_URL = os.environ.get("MC_ANYTHINGLLM_URL", "http://192.168.178.155:3001").rstrip("/")
|
||||
# GitHub-Repos für Update-Checks.
|
||||
HERMES_AGENT_REPO = os.environ.get("MC_HERMES_AGENT_REPO", "NousResearch/hermes-agent")
|
||||
ANYTHINGLLM_REPO = os.environ.get("MC_ANYTHINGLLM_REPO", "Mintplex-Labs/anything-llm")
|
||||
HERMES_HOME = Path(os.path.expanduser(os.environ.get("HERMES_HOME", "~/.hermes")))
|
||||
# PC Executor — läuft auf dem Windows-PC, erreichbar über LAN.
|
||||
PC_EXECUTOR_URL = os.environ.get("MC_PC_EXECUTOR_URL", "http://192.168.178.98:7777").rstrip("/")
|
||||
|
||||
@@ -12,7 +12,7 @@ import subprocess
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from config import GATEWAY_URL, HERMES_API_URL, HERMES_WEBUI_URL, LLAMA_SWAP_URL
|
||||
from config import GATEWAY_URL, HERMES_API_URL, LLAMA_SWAP_URL
|
||||
from services import backup as backup_svc
|
||||
from services.agent import agent_status
|
||||
from services.gateway import gateway_reachable
|
||||
@@ -46,12 +46,12 @@ def services() -> dict:
|
||||
{"name": "Engine (llama-swap)", "url": LLAMA_SWAP_URL, "ok": engine_reachable()},
|
||||
{"name": "Gateway (integriert)", "url": gw_url, "ok": gateway_reachable()},
|
||||
{"name": "Hermes-Gateway", "url": HERMES_API_URL, "ok": a["gateway_reachable"]},
|
||||
{"name": "Hermes-WebUI", "url": HERMES_WEBUI_URL, "ok": a["webui_reachable"]},
|
||||
{"name": "AnythingLLM", "url": a["webui_url"], "ok": a["webui_reachable"]},
|
||||
],
|
||||
"links": {
|
||||
"engine_ui": f"{LLAMA_SWAP_URL}/ui",
|
||||
"gateway": gw_url,
|
||||
"hermes_webui": HERMES_WEBUI_URL,
|
||||
"hermes_webui": a["webui_url"],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import os
|
||||
|
||||
import httpx
|
||||
|
||||
from config import HERMES_API_URL, HERMES_HOME, HERMES_WEBUI_URL, PC_EXECUTOR_URL
|
||||
from config import ANYTHINGLLM_URL, HERMES_API_URL, HERMES_HOME, PC_EXECUTOR_URL
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -59,9 +59,10 @@ def agent_status() -> dict:
|
||||
|
||||
return {
|
||||
"gateway_url": HERMES_API_URL,
|
||||
"webui_url": HERMES_WEBUI_URL,
|
||||
# Chat-WebUI ist jetzt AnythingLLM (eigener Host), nicht mehr hermes-webui :8787.
|
||||
"webui_url": ANYTHINGLLM_URL,
|
||||
"gateway_reachable": _reach(HERMES_API_URL, "/health"),
|
||||
"webui_reachable": _reach(HERMES_WEBUI_URL),
|
||||
"webui_reachable": _reach(ANYTHINGLLM_URL, "/api/ping"),
|
||||
"home_exists": home.exists(),
|
||||
"brain_model": brain_model,
|
||||
# Best-effort: welche Verdrahtung lokal sichtbar ist (auf der Box aussagekräftig).
|
||||
|
||||
@@ -15,13 +15,17 @@ from datetime import datetime
|
||||
import httpx
|
||||
import psutil
|
||||
|
||||
from services import discover, jobengine, llamaswap
|
||||
from config import ANYTHINGLLM_REPO, ANYTHINGLLM_URL, HERMES_AGENT_REPO
|
||||
from services import discover, jobengine, llamaswap, system
|
||||
|
||||
# System-Dienste (root, via sudo -n NOPASSWD) vs. User-Dienste (systemctl --user).
|
||||
SYSTEM_SERVICES = {"llama-swap"}
|
||||
USER_SERVICES = {"mission-control-2", "hermes-gateway", "hermes-dashboard", "hermes-webui"}
|
||||
|
||||
ENGINE_UPDATE_CMD = os.environ.get("MC_ENGINE_UPDATE_CMD", "")
|
||||
# Engine-Update: lädt den neuesten Vulkan-Build (deploy/update-engine.sh, läuft als root).
|
||||
_REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
ENGINE_UPDATE_CMD = os.environ.get(
|
||||
"MC_ENGINE_UPDATE_CMD", f"sudo bash {_REPO_ROOT}/deploy/update-engine.sh")
|
||||
# Engine = offizieller Vulkan-Build von ggml-org/llama.cpp (RADV auf Strix Halo).
|
||||
ENGINE_PATH = os.environ.get("MC_ENGINE_PATH", "/opt/llamacpp-vulkan")
|
||||
ENGINE_REPO = os.environ.get("MC_ENGINE_REPO", "ggml-org/llama.cpp")
|
||||
@@ -82,6 +86,74 @@ def _engine_update_available() -> bool:
|
||||
return avail
|
||||
|
||||
|
||||
_comp_cache = {"ts": 0.0, "data": []}
|
||||
|
||||
|
||||
def _gh_latest(repo: str) -> dict | None:
|
||||
"""Neuestes GitHub-Release {tag, published} oder None."""
|
||||
try:
|
||||
rel = httpx.get(f"https://api.github.com/repos/{repo}/releases/latest",
|
||||
timeout=6, headers={"User-Agent": "MissionControl2"}).json()
|
||||
return {"tag": str(rel.get("tag_name", "")), "published": rel.get("published_at")}
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _commit_ts(path: str) -> int | None:
|
||||
try:
|
||||
p = subprocess.run(["git", "-C", os.path.expanduser(path), "log", "-1", "--format=%ct"],
|
||||
capture_output=True, text=True, timeout=8)
|
||||
return int(p.stdout.strip()) if p.returncode == 0 and p.stdout.strip() else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _hermes_agent_update() -> dict:
|
||||
"""Hermes-Agent: neuestes Release vs. installiertes git-Commit-Datum."""
|
||||
info = {"key": "hermes_agent", "name": "Hermes Agent", "current": None,
|
||||
"latest": None, "update": False, "reachable": None}
|
||||
git = system.find_hermes_agent_git()
|
||||
if git:
|
||||
info["current"] = git.get("hash")
|
||||
gh = _gh_latest(HERMES_AGENT_REPO)
|
||||
if gh:
|
||||
info["latest"] = gh["tag"]
|
||||
ts = _commit_ts(git["path"]) if (git and git.get("path")) else None
|
||||
if gh["published"] and ts:
|
||||
try:
|
||||
pub = datetime.fromisoformat(gh["published"].replace("Z", "+00:00")).timestamp()
|
||||
info["update"] = pub > ts + 86400 # Release > installiertes Commit (+1 Tag Toleranz)
|
||||
except Exception:
|
||||
pass
|
||||
return info
|
||||
|
||||
|
||||
def _anythingllm_update() -> dict:
|
||||
"""AnythingLLM: erreichbar? + neueste verfügbare Version. Installierte Version ist
|
||||
remote nicht unauth. abfragbar → update=None (nur Info: neueste verfügbar)."""
|
||||
reachable = False
|
||||
try:
|
||||
reachable = httpx.get(f"{ANYTHINGLLM_URL}/api/ping", timeout=4).status_code == 200
|
||||
except Exception:
|
||||
reachable = False
|
||||
info = {"key": "anythingllm", "name": "AnythingLLM", "current": None,
|
||||
"latest": None, "update": None, "reachable": reachable}
|
||||
gh = _gh_latest(ANYTHINGLLM_REPO)
|
||||
if gh:
|
||||
info["latest"] = gh["tag"]
|
||||
return info
|
||||
|
||||
|
||||
def _components_cached() -> list[dict]:
|
||||
"""Update-Status von Hermes-Agent + AnythingLLM (1h-Cache → GitHub schonen)."""
|
||||
now = time.time()
|
||||
if now - _comp_cache["ts"] < 3600 and _comp_cache["data"]:
|
||||
return _comp_cache["data"]
|
||||
data = [_hermes_agent_update(), _anythingllm_update()]
|
||||
_comp_cache.update(ts=now, data=data)
|
||||
return data
|
||||
|
||||
|
||||
def model_upgrades() -> list[dict]:
|
||||
"""Dynamisch: je discover-Kategorie das empfohlene Modell, das NOCH NICHT installiert ist,
|
||||
aber NUR wenn für diese Rolle bereits irgendein Modell konfiguriert ist.
|
||||
@@ -135,7 +207,8 @@ def _last_apt_update() -> float | None:
|
||||
def updates() -> dict:
|
||||
ups = model_upgrades()
|
||||
return {"os": _os_upgradable(), "engine": 1 if _engine_update_available() else 0,
|
||||
"models": len(ups), "model_list": ups, "last_check": _last_apt_update()}
|
||||
"models": len(ups), "model_list": ups, "last_check": _last_apt_update(),
|
||||
"components": _components_cached()}
|
||||
|
||||
|
||||
def _run(cmd: list[str], sudo_password: str | None = None) -> dict:
|
||||
@@ -233,8 +306,9 @@ def engine_update_job(sudo_password: str | None = None) -> dict | None:
|
||||
return None
|
||||
if err := check_sudo_needs_password(sudo_password):
|
||||
return err
|
||||
cmd = f"{ENGINE_UPDATE_CMD} && sudo systemctl restart llama-swap"
|
||||
job_id = jobengine.start_job(["bash", "-c", cmd], "Engine-Update (llama.cpp)", sudo_password=sudo_password)
|
||||
# update-engine.sh läuft via sudo als root und startet llama-swap am Ende selbst neu.
|
||||
job_id = jobengine.start_job(["bash", "-c", ENGINE_UPDATE_CMD],
|
||||
"Engine-Update (llama.cpp Vulkan)", sudo_password=sudo_password)
|
||||
return {"ok": True, "job_id": job_id}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env bash
|
||||
# Aktualisiert den Vulkan-llama.cpp-Build auf den neuesten ggml-org-Release.
|
||||
# Wird vom MC2-„Engine Update"-Button via `sudo bash …` als ROOT aufgerufen
|
||||
# (deshalb KEIN internes sudo). Startet llama-swap am Ende neu.
|
||||
set -euo pipefail
|
||||
|
||||
VULKAN_DIR=/opt/llamacpp-vulkan
|
||||
PIN_BUILD="${MC_ENGINE_BUILD:-}" # leer = neuester Release
|
||||
|
||||
if [ -n "$PIN_BUILD" ]; then
|
||||
TAG="$PIN_BUILD"
|
||||
else
|
||||
TAG="$(curl -s https://api.github.com/repos/ggml-org/llama.cpp/releases/latest | jq -r .tag_name)"
|
||||
fi
|
||||
[ -n "$TAG" ] && [ "$TAG" != "null" ] || { echo "Konnte neuesten Release-Tag nicht ermitteln"; exit 1; }
|
||||
|
||||
ASSET="llama-${TAG}-bin-ubuntu-vulkan-x64.tar.gz"
|
||||
URL="https://github.com/ggml-org/llama.cpp/releases/download/${TAG}/${ASSET}"
|
||||
TMP="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
|
||||
echo "Lade Engine ${TAG} …"
|
||||
curl -fsSL -o "$TMP/v.tgz" "$URL"
|
||||
tar xzf "$TMP/v.tgz" -C "$TMP"
|
||||
mkdir -p "$VULKAN_DIR"
|
||||
cp -rf "$TMP"/llama-*/. "$VULKAN_DIR"/
|
||||
test -x "$VULKAN_DIR/llama-server"
|
||||
ln -sfn "$VULKAN_DIR/llama-server" /usr/local/bin/llama-server
|
||||
|
||||
systemctl restart llama-swap
|
||||
echo "Engine aktualisiert auf ${TAG} und llama-swap neugestartet."
|
||||
+395
File diff suppressed because one or more lines are too long
-395
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -7,7 +7,7 @@
|
||||
<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-DPqptZjm.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-D9tRIUA4.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CjZVMvhL.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -45,7 +45,7 @@ export function AgentStatusCard() {
|
||||
: "border border-border text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
<ExternalLink className="h-3 w-3" /> Hermes öffnen
|
||||
<ExternalLink className="h-3 w-3" /> AnythingLLM öffnen
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
@@ -60,7 +60,7 @@ export function AgentStatusCard() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 bg-background/20 rounded-xl border border-border/40">
|
||||
<div className="text-[10px] text-muted-foreground uppercase font-semibold">WebUI</div>
|
||||
<div className="text-[10px] text-muted-foreground uppercase font-semibold">AnythingLLM</div>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<span className={cn("h-2 w-2 rounded-full", agent.webui_reachable ? "bg-emerald-500 animate-pulse" : "bg-amber-500")} />
|
||||
<span className="text-xs font-medium">{agent.webui_reachable ? "Online" : "Offline"}</span>
|
||||
|
||||
@@ -210,6 +210,35 @@ export function UpdatesCard() {
|
||||
<span>Modell-Upgrades</span>
|
||||
<span className="font-mono">{updates.models > 0 ? `${updates.models} verfügbar` : "aktuell"}</span>
|
||||
</div>
|
||||
|
||||
{/* Komponenten: Hermes Agent + AnythingLLM */}
|
||||
{updates.components?.map((c) => {
|
||||
const isUpdate = c.update === true
|
||||
return (
|
||||
<div key={c.key} className={cn(
|
||||
"flex items-center justify-between px-3 py-1.5 rounded-lg border text-xs",
|
||||
isUpdate
|
||||
? "border-amber-500/30 bg-amber-500/5 text-amber-400 font-semibold"
|
||||
: "border-border/30 bg-background/25 text-muted-foreground"
|
||||
)}>
|
||||
<span className="flex items-center gap-1.5">
|
||||
{c.name}
|
||||
{c.reachable === false && (
|
||||
<span className="text-[8px] font-bold uppercase text-red-400/80">offline</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="font-mono text-[10px]" title={c.current ? `installiert: ${c.current}` : undefined}>
|
||||
{isUpdate
|
||||
? `Update: ${c.latest}`
|
||||
: c.update === false
|
||||
? "aktuell"
|
||||
: c.latest
|
||||
? `neueste: ${c.latest}`
|
||||
: "—"}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2 border-t border-border/20 pt-3">
|
||||
|
||||
@@ -202,12 +202,22 @@ export interface ServicesResp {
|
||||
links: { engine_ui: string; gateway: string; hermes_webui: string }
|
||||
}
|
||||
|
||||
export interface ComponentUpdate {
|
||||
key: string // "hermes_agent" | "anythingllm"
|
||||
name: string
|
||||
current: string | null
|
||||
latest: string | null
|
||||
update: boolean | null // null = unbestimmbar (z.B. AnythingLLM-Version remote nicht abfragbar)
|
||||
reachable: boolean | null
|
||||
}
|
||||
|
||||
export interface UpdatesResp {
|
||||
os: number
|
||||
engine: number
|
||||
models: number
|
||||
model_list: { role: string; title: string; repo: string }[]
|
||||
last_check?: number | null
|
||||
components?: ComponentUpdate[]
|
||||
}
|
||||
|
||||
export interface ConnectTool {
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ export const NAV: NavItem[] = [
|
||||
{ id: "system", label: "Diagnose", hint: "Metriken, Dienste, Logs", icon: Cpu },
|
||||
{ id: "memory", label: "Gedächtnis", hint: "Geteiltes Memory verwalten", icon: Brain },
|
||||
{ id: "connect", label: "Verbinden", hint: "IDE-/Agent-Configs erzeugen", icon: Plug },
|
||||
{ id: "agent", label: "Hermes", hint: "Agent-Status & WebUI öffnen", icon: Bot },
|
||||
{ id: "agent", label: "Hermes", hint: "Agent-Status & AnythingLLM öffnen", icon: Bot },
|
||||
{ id: "guide", label: "Anleitung", hint: "Einrichten & Vibe-Coding", icon: HelpCircle },
|
||||
]
|
||||
|
||||
|
||||
@@ -148,7 +148,7 @@ export function AgentView() {
|
||||
)}
|
||||
>
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
<span>Hermes WebUI öffnen</span>
|
||||
<span>AnythingLLM öffnen</span>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
@@ -170,9 +170,9 @@ export function AgentView() {
|
||||
icon={Bot}
|
||||
/>
|
||||
<Tile
|
||||
label="Agent WebUI"
|
||||
label="AnythingLLM"
|
||||
ok={s.webui_reachable}
|
||||
detail="Port :8787 (Chat UI)"
|
||||
detail="Chat-UI (AnythingLLM)"
|
||||
icon={Activity}
|
||||
/>
|
||||
<Tile
|
||||
@@ -239,10 +239,10 @@ export function AgentView() {
|
||||
onMouseEnter={() => setHoveredNode("webui")}
|
||||
onMouseLeave={() => setHoveredNode(null)}
|
||||
onClick={() => s.webui_reachable && window.open(resolveExternalUrl(s.webui_url), "_blank")}
|
||||
title={s.webui_reachable ? "Klicken um Chat-WebUI zu öffnen" : "WebUI Offline"}
|
||||
title={s.webui_reachable ? "Klicken um AnythingLLM zu öffnen" : "AnythingLLM offline"}
|
||||
>
|
||||
<Activity className={cn("h-3.5 w-3.5", s.webui_reachable ? "text-emerald-400" : "text-amber-500")} />
|
||||
<span>Agent WebUI</span>
|
||||
<span>AnythingLLM</span>
|
||||
<span className={cn("w-1.5 h-1.5 rounded-full animate-pulse ml-auto", s.webui_reachable ? "bg-emerald-500" : "bg-amber-500")} />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -329,7 +329,7 @@ export function GuideView() {
|
||||
<ul className="text-[11px] text-muted-foreground space-y-2 list-disc pl-4 leading-relaxed">
|
||||
<li><strong>Keine Verbindung?</strong> Überprüfe im Live-Verbindungsprüfer oben, ob die Box-IP erreichbar ist. Stelle sicher, dass dein lokaler PC im selben WLAN/LAN-Netzwerk wie die Box eingeloggt ist.</li>
|
||||
<li><strong>Modell antwortet nicht?</strong> Schaue unter <strong>Diagnose</strong>, ob der Dienst `llama-swap` aktiv (grün) is. Wenn nicht, klicke daneben auf <strong>Restart</strong>.</li>
|
||||
<li><strong>Hermes Agent reagiert merkwürdig?</strong> Starte in der Hermes WebUI einfach eine frische Session (neuen Chat). Ältere Chats sammeln Kontext-Müll an.</li>
|
||||
<li><strong>Hermes Agent reagiert merkwürdig?</strong> Starte in AnythingLLM einfach eine frische Session (neuen Chat). Ältere Chats sammeln Kontext-Müll an.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</>
|
||||
@@ -474,7 +474,7 @@ description: Drive development with strict TDD practices
|
||||
<ul className="list-disc pl-4 space-y-1">
|
||||
<li><strong>Chat-Kontext sauber halten:</strong> Starte regelmäßig neue Chats. Zu große Historien verlangsamen den Agenten und führen zu Halluzinationen.</li>
|
||||
<li><strong>Gehirn festlegen:</strong> Konfiguriere im Gateway die Modell-Rolle <code>brain</code> für Hermes, damit er automatisch das passende Modell per Llama Swap lädt.</li>
|
||||
<li><strong>Sandbox umgehen:</strong> Erweitere Hermes' System-Prompt (WebUI-Einstellungen), um Befehle über SSH an deinen Windows-PC zu leiten.</li>
|
||||
<li><strong>Sandbox umgehen:</strong> Erweitere Hermes' System-Prompt (AnythingLLM-Einstellungen), um Befehle über SSH an deinen Windows-PC zu leiten.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user