MC2 Final (D16 a/b/d): verständliche Updates + ehrliche Speicher-Zahlen
a) Update-Meldungen: generischer Release-Summarizer in Lucys Stimme mit
strukturiertem Aktions-Verdikt ("Musst du etwas tun? NEIN/JA") für
Hermes, Engine (llama.cpp) und llama-swap; Fangnetz-Hinweis verheiratet
Breaking-Change-Sorge mit dem Postcheck.
b) OS ehrlich: zurückgestellte Pakete (Phasen-Rollout / kept back) werden
ausgewiesen statt scheinbar zu hängen.
d) Ehrliche Speicher-Zahlen: KV-Cache aus echten GGUF-Architektur-Daten
(Layer × KV-Köpfe × head_dim) + KV-Quant aus dem cmd statt params-blinder
Schätzung — footprint_gb als eine Zahlensprache (Zentrale, Modell-Manager,
fits-Check auf warmset+largest). Auto-Rewarm-Nudge nach Config-Reload.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+115
-29
@@ -298,8 +298,42 @@ def updates() -> dict:
|
||||
|
||||
# ── Update-Details (was genau wird aktualisiert) — on-demand beim Öffnen des Fensters ──
|
||||
|
||||
def _os_held_back() -> list[dict]:
|
||||
"""Pakete, die apt aktuell NICHT einspielt, obwohl es sie gäbe — mit ehrlichem Grund.
|
||||
Zwei Fälle, aus `apt-get -s upgrade` (Simulation) gelesen:
|
||||
• Phasen-Rollout (Ubuntu staffelt Updates prozentual aus) → 'deferred due to phasing'.
|
||||
• zurückgehalten wegen neuer Abhängigkeiten → 'kept back'.
|
||||
Beides ist normal und löst sich von selbst — verhindert nur das 'hängt fest'-Gefühl,
|
||||
wenn nach 'Fertig' noch aktualisierbare Pakete übrig scheinen."""
|
||||
held: list[dict] = []
|
||||
sections = {
|
||||
"The following upgrades have been deferred due to phasing:": "phasing",
|
||||
"The following packages have been kept back:": "kept_back",
|
||||
}
|
||||
try:
|
||||
out = subprocess.run(
|
||||
["bash", "-c", "LC_ALL=C apt-get -s upgrade 2>/dev/null"],
|
||||
capture_output=True, text=True, timeout=25)
|
||||
reason: str | None = None
|
||||
for line in (out.stdout or "").splitlines():
|
||||
hdr = sections.get(line.strip())
|
||||
if hdr: # Abschnitts-Kopf → folgende Zeilen sammeln
|
||||
reason = hdr
|
||||
continue
|
||||
if reason and line.startswith((" ", "\t")): # eingerückt = Paketnamen des Abschnitts
|
||||
for name in line.split():
|
||||
held.append({"name": name, "reason": reason})
|
||||
elif reason: # nicht eingerückt → Abschnitt zu Ende
|
||||
reason = None
|
||||
except Exception: # noqa: BLE001 — nur Zusatzinfo, nie ein Blocker
|
||||
pass
|
||||
held.sort(key=lambda p: p["name"])
|
||||
return held
|
||||
|
||||
|
||||
def os_update_details() -> dict:
|
||||
"""Liste der aktualisierbaren apt-Pakete (Name, installiert → Kandidat)."""
|
||||
"""Liste der aktualisierbaren apt-Pakete (Name, installiert → Kandidat) + ehrliche
|
||||
Anzeige der vom System zurückgestellten Pakete (Phasen-Rollout / kept back)."""
|
||||
out_pkgs: list[dict] = []
|
||||
try:
|
||||
# LC_ALL=C → englische Ausgabe, damit der Regex "[upgradable from: ...]" greift
|
||||
@@ -314,7 +348,8 @@ def os_update_details() -> dict:
|
||||
out_pkgs.append({"name": m.group(1), "candidate": m.group(2),
|
||||
"current": m.group(3).strip()})
|
||||
out_pkgs.sort(key=lambda p: p["name"])
|
||||
return {"kind": "os", "count": len(out_pkgs), "packages": out_pkgs}
|
||||
return {"kind": "os", "count": len(out_pkgs), "packages": out_pkgs,
|
||||
"held_back": _os_held_back()}
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return {"kind": "os", "count": len(out_pkgs), "packages": out_pkgs, "error": str(exc)}
|
||||
|
||||
@@ -334,6 +369,13 @@ def engine_update_details() -> dict:
|
||||
info["url"] = rel.get("html_url")
|
||||
body = (rel.get("body") or "").strip()
|
||||
info["body"] = body[:2000] if body else None
|
||||
# Nur zusammenfassen, wenn wirklich ein neuerer Build ansteht (spart einen LLM-Call,
|
||||
# wenn das Modal bei aktuellem Stand geöffnet wird).
|
||||
if body and info["latest_build"] and info["installed_build"] \
|
||||
and info["latest_build"] > info["installed_build"]:
|
||||
ctx = ("Es geht um ein Update der Inferenz-Engine llama.cpp (Vulkan-Build, treibt alle "
|
||||
"Sprachmodelle der Box auf der AMD-Strix-Halo-GPU).")
|
||||
info.update(_summarize_release("engine", tag, ctx, body[:6000]))
|
||||
except Exception as exc: # noqa: BLE001
|
||||
info["error"] = str(exc)
|
||||
return info
|
||||
@@ -354,31 +396,54 @@ def swap_update_details() -> dict:
|
||||
info["url"] = rel.get("html_url")
|
||||
body = (rel.get("body") or "").strip()
|
||||
info["body"] = body[:2000] if body else None
|
||||
if body and info["latest_build"] and info["installed_build"] \
|
||||
and info["latest_build"] > info["installed_build"]:
|
||||
ctx = ("Es geht um ein Update von llama-swap (der Router, der Anfragen an die Box "
|
||||
"verteilt und Sprachmodelle heiß nachlädt).")
|
||||
info.update(_summarize_release("swap", tag, ctx, body[:6000]))
|
||||
except Exception as exc: # noqa: BLE001
|
||||
info["error"] = str(exc)
|
||||
return info
|
||||
|
||||
|
||||
# LLM-Zusammenfassung der anstehenden Hermes-Commits (die Box liest ihre Release-Notes selbst).
|
||||
# Gecacht auf den neuesten Commit-Hash — das Modal darf beliebig oft geöffnet werden.
|
||||
_relnotes_cache: dict = {"key": "", "summary": ""}
|
||||
# LLM-Zusammenfassung anstehender Updates in Lucys Stimme (die Box liest ihre Release-Notes
|
||||
# selbst). Jede Zusammenfassung beginnt mit einem klaren Aktions-Verdikt für den Besitzer
|
||||
# (kein Entwickler): "Musst du etwas tun? NEIN — das Fangnetz regelt das / JA: …".
|
||||
# Gecacht je Komponente auf den neuesten Commit-Hash/Release-Tag — das Modal darf beliebig
|
||||
# oft geöffnet werden, ohne das Modell jedes Mal neu zu befragen.
|
||||
_relnotes_caches: dict = {"hermes": {}, "engine": {}, "swap": {}}
|
||||
|
||||
# Erste Zeile jeder Modell-Antwort: "AKTION: NEIN" oder "AKTION: JA — <grund>".
|
||||
# Toleriert Markdown-Deko (**fett**, Bullet, Überschrift), die 'fast' gern einstreut.
|
||||
_ACTION_RX = re.compile(
|
||||
r"^[\s*_>#\-]*AKTION:?\s*\**\s*(JA|NEIN)\b[\s—:,.\-*_]*(.*?)[\s*_]*$", re.IGNORECASE)
|
||||
|
||||
|
||||
def _summarize_hermes_commits(commits: list[dict]) -> str:
|
||||
key = commits[0]["hash"] if commits else ""
|
||||
def _summarize_release(kind: str, key: str, context: str, changes: str) -> dict:
|
||||
"""Fasst Release-Notes/Commits in Lucys Stimme zusammen und trennt das Aktions-Verdikt
|
||||
ab. Rückgabe: {summary, action_needed, action_text}. Cache je Komponente auf `key`."""
|
||||
empty = {"summary": "", "action_needed": None, "action_text": ""}
|
||||
if not key:
|
||||
return ""
|
||||
if _relnotes_cache["key"] == key and _relnotes_cache["summary"]:
|
||||
return _relnotes_cache["summary"]
|
||||
return empty
|
||||
cache = _relnotes_caches.setdefault(kind, {})
|
||||
if cache.get("key") == key and cache.get("data"):
|
||||
return cache["data"]
|
||||
from config import LLAMA_SWAP_URL
|
||||
subjects = "\n".join(f"- {c['subject']}" for c in commits[:100])
|
||||
prompt = (
|
||||
"Du bist der Update-Berater einer lokalen AI-Box (Hermes-Agent auf Strix Halo; genutzt werden: "
|
||||
"api_server/Gateway, memory-provider-Plugin 'mc2-memory', terminal-/web-Tools, approvals, cron). "
|
||||
"Hier die Commit-Titel des anstehenden Hermes-Updates:\n\n" + subjects + "\n\n"
|
||||
"Fasse auf DEUTSCH in maximal 6 Stichpunkten zusammen: (1) Breaking Changes oder geänderte/"
|
||||
"entfernte Config-Schlüssel (WICHTIGSTES zuerst, explizit warnen), (2) was unser Setup betrifft, "
|
||||
"(3) welche neuen Features sich für uns lohnen. Keine Einleitung, nur die Punkte."
|
||||
"Du bist Lucy, die Stimme einer lokalen AI-Box, und erklärst dem Besitzer (KEIN Entwickler, "
|
||||
"fasst nie eine Konsole an) ein anstehendes Update ruhig und verständlich.\n"
|
||||
f"{context}\n\n"
|
||||
"Anstehende Änderungen:\n" + changes + "\n\n"
|
||||
"Antworte auf DEUTSCH, knapp und klar. HALTE DICH GENAU an dieses Format:\n"
|
||||
"Zeile 1 ist das Aktions-Verdikt und beginnt mit 'AKTION: ':\n"
|
||||
" • 'AKTION: NEIN' — wenn der Besitzer nichts tun muss (die Box spielt es selbst ein, das "
|
||||
"Fangnetz prüft danach automatisch und rollt bei Problemen von allein zurück). Das ist der "
|
||||
"Normalfall.\n"
|
||||
" • 'AKTION: JA — <was er konkret tun/entscheiden muss>' — NUR wenn er wirklich selbst "
|
||||
"handeln muss.\n"
|
||||
"Danach maximal 5 kurze Stichpunkte (je mit '- '), Wichtigstes zuerst: Breaking Changes oder "
|
||||
"geänderte/entfernte Config-Schlüssel ZUERST und mit '⚠️' markiert, dann was unser Setup "
|
||||
"betrifft, dann lohnende neue Features. Keine Einleitung, keine Überschrift."
|
||||
)
|
||||
try:
|
||||
r = httpx.post(f"{LLAMA_SWAP_URL}/v1/chat/completions", timeout=90.0, json={
|
||||
@@ -389,21 +454,42 @@ def _summarize_hermes_commits(commits: list[dict]) -> str:
|
||||
r.raise_for_status()
|
||||
resp = r.json()
|
||||
choice = (resp.get("choices") or [{}])[0]
|
||||
summary = (choice.get("message") or {}).get("content") or ""
|
||||
text = ((choice.get("message") or {}).get("content") or "").strip()
|
||||
finish_reason = choice.get("finish_reason") or ""
|
||||
summary = summary.strip()
|
||||
|
||||
# finish_reason == "length" → Antwort wurde wegen Token-Limits abgeschnitten →
|
||||
# letzten (unvollständigen) Stichpunkt entfernen.
|
||||
# Bei "stop" oder None → Antwort ist vollständig → unverändert lassen.
|
||||
if finish_reason == "length" and summary:
|
||||
lines = summary.rsplit("\n", 1)
|
||||
summary = lines[0] if len(lines) > 1 else ""
|
||||
if summary:
|
||||
_relnotes_cache.update(key=key, summary=summary)
|
||||
return summary
|
||||
# letzten (unvollständigen) Stichpunkt entfernen. Bei "stop"/None → vollständig.
|
||||
if finish_reason == "length" and text:
|
||||
head, _, _tail = text.rpartition("\n")
|
||||
text = head if head else ""
|
||||
|
||||
# Aktions-Verdikt herauslösen (strukturiertes Feld fürs UI). Normalerweise Zeile 1,
|
||||
# aber tolerant: erste passende Zeile suchen (Modell startet manchmal mit Leerzeile).
|
||||
action_needed: bool | None = None
|
||||
action_text = ""
|
||||
lines = text.splitlines()
|
||||
for idx, ln in enumerate(lines):
|
||||
if m := _ACTION_RX.match(ln):
|
||||
action_needed = m.group(1).upper() == "JA"
|
||||
action_text = (m.group(2) or "").strip()
|
||||
text = "\n".join(lines[:idx] + lines[idx + 1:]).strip()
|
||||
break
|
||||
|
||||
data = {"summary": text, "action_needed": action_needed, "action_text": action_text}
|
||||
if text:
|
||||
cache.update(key=key, data=data)
|
||||
return data
|
||||
except Exception as exc: # noqa: BLE001 — Zusammenfassung ist Komfort, nie Blocker
|
||||
return f"(Zusammenfassung nicht verfügbar: {exc})"
|
||||
return {"summary": f"(Zusammenfassung nicht verfügbar: {exc})",
|
||||
"action_needed": None, "action_text": ""}
|
||||
|
||||
|
||||
def _summarize_hermes_commits(commits: list[dict]) -> dict:
|
||||
subjects = "\n".join(f"- {c['subject']}" for c in commits[:100])
|
||||
context = ("Es geht um ein Update des Hermes-Agenten (das Gehirn/Werkzeug-System der Box auf "
|
||||
"Strix Halo; genutzt werden: api_server/Gateway, memory-provider-Plugin 'mc2-memory', "
|
||||
"terminal-/web-Tools, approvals, cron).")
|
||||
return _summarize_release("hermes", commits[0]["hash"] if commits else "", context, subjects)
|
||||
|
||||
|
||||
def hermes_update_details() -> dict:
|
||||
@@ -430,7 +516,7 @@ def hermes_update_details() -> dict:
|
||||
info["commits"] = commits
|
||||
info["behind"] = len(commits)
|
||||
if commits:
|
||||
info["summary"] = _summarize_hermes_commits(commits)
|
||||
info.update(_summarize_hermes_commits(commits))
|
||||
except Exception as exc: # noqa: BLE001
|
||||
info["error"] = str(exc)
|
||||
return info
|
||||
|
||||
Reference in New Issue
Block a user