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:
Hitonabi
2026-06-27 02:12:56 +02:00
parent c48e583790
commit 65d8ab5fe3
14 changed files with 571 additions and 421 deletions
+4 -3
View File
@@ -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).
+79 -5
View File
@@ -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}