Trend-Radar + Pruefstand: Box beobachtet Modell-/Engine-Trends und testet Kandidaten selbst
- trend-radar-feed.sh (Cron Sa 05:15): Live-Puls ALLER Rollen mit Vorwochen-Vergleich, Engine-A/B gegen neuen llama.cpp-Build (Fruehwarnung vor So-Auto-Update), mechanische Watch-Liste (MMQ-Issue, ROCm-Releases, Community-Grid), HF-Kandidaten-Suche mit Zitat-Gate -> max. 1 Pruefstand-Kandidat/Woche - pruefstand.sh + harness.py (Cron So 01:00, no-agent): volles Programm je Kandidat - 6 Coding-Aufgaben mit echten Unit-Tests, 4 Agenten-Aufgaben (Tool-Loop + Endzustand), 4 Recherche-Fragen mit Zitat-Ehrlichkeits-Gate, Deutsch-Richter, Vision-Testbild, Tempo kurz+lang; Baseline der Amtsinhaber als Vergleichsmassstab - Leitplanken (User-Entscheid 17.07.): Download-Deckel 35 GB (drueber -> Karte), 1 Kandidat/Woche, Cache mit Auto-Aufraeumen, RAM-/Platz-Wache, Bench-Port :5899, Radar/Pruefstand EMPFEHLEN nur - Rollen-Wechsel bleibt Commander-Klick - E2E bewiesen (Mock-LLM): coding 6/6, recherche 4/4 inkl. Zitat-Gate, Agent-Roundtrip, Richter-Parsing; Suiten-Selbsttest mit Referenzloesungen 6/6 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+7
-1
@@ -115,6 +115,12 @@ cp "$SRC/deploy/idle-radar-feed.sh" "$HOME/.hermes/scripts/idle-radar-feed.sh"
|
||||
# eigene Aenderungen; der Karten-Gutachter stempelt jede Karte mit einer Empfehlung.
|
||||
cp "$SRC/deploy/selbst-inventur.sh" "$HOME/.hermes/scripts/selbst-inventur.sh"
|
||||
cp "$SRC/deploy/karten-gutachter.sh" "$HOME/.hermes/scripts/karten-gutachter.sh"
|
||||
# Trend-Radar (17.07.2026, Sa 05:15): Live-Puls aller Rollen + Engine-A/B + Watch-Liste +
|
||||
# HF-Kandidaten-Suche → max. 1 Prüfstand-Kandidat/Woche. Prüfstand (So 01:00, --no-agent):
|
||||
# volles Programm (Coding/Agent/Recherche/Deutsch/Vision) gegen die Amtsinhaber-Baseline;
|
||||
# Harness + Suiten liegen im Repo ($SRC/deploy/pruefstand/), nur der Runner wird kopiert.
|
||||
cp "$SRC/deploy/trend-radar-feed.sh" "$HOME/.hermes/scripts/trend-radar-feed.sh"
|
||||
cp "$SRC/deploy/pruefstand/pruefstand.sh" "$HOME/.hermes/scripts/pruefstand.sh"
|
||||
# Werkstatt-Kanban-Profil (Ideen-Queue, 10.07.2026): Leitplanken des Worker-Profils nachziehen
|
||||
# (Profil selbst wurde einmalig per `hermes profile create werkstatt --clone` angelegt).
|
||||
[ -d "$HOME/.hermes/profiles/werkstatt" ] && cp "$SRC/deploy/werkstatt-SOUL.md" "$HOME/.hermes/profiles/werkstatt/SOUL.md"
|
||||
@@ -122,7 +128,7 @@ cp "$SRC/deploy/karten-gutachter.sh" "$HOME/.hermes/scripts/karten-gutachter.sh"
|
||||
# statt Lucy-default. Profil selbst wird einmalig per `hermes profile create betrieb
|
||||
# --clone-from werkstatt` angelegt (model coder→hermes, Description fürs Specifier-Routing).
|
||||
[ -d "$HOME/.hermes/profiles/betrieb" ] && cp "$SRC/deploy/betrieb-SOUL.md" "$HOME/.hermes/profiles/betrieb/SOUL.md"
|
||||
chmod +x "$HOME/.hermes/scripts/fremdblick.sh" "$HOME/.hermes/scripts/dreaming-feed.sh" "$HOME/.hermes/scripts/worker.sh" "$HOME/.hermes/scripts/chef-gutachter-feed.sh" "$HOME/.hermes/scripts/hermes-release-radar-feed.sh" "$HOME/.hermes/scripts/radar-selbstkritik-wrapper.sh" "$HOME/.hermes/scripts/idle-radar-feed.sh" "$HOME/.hermes/scripts/selbst-inventur.sh" "$HOME/.hermes/scripts/karten-gutachter.sh"
|
||||
chmod +x "$HOME/.hermes/scripts/fremdblick.sh" "$HOME/.hermes/scripts/dreaming-feed.sh" "$HOME/.hermes/scripts/worker.sh" "$HOME/.hermes/scripts/chef-gutachter-feed.sh" "$HOME/.hermes/scripts/hermes-release-radar-feed.sh" "$HOME/.hermes/scripts/radar-selbstkritik-wrapper.sh" "$HOME/.hermes/scripts/idle-radar-feed.sh" "$HOME/.hermes/scripts/selbst-inventur.sh" "$HOME/.hermes/scripts/karten-gutachter.sh" "$HOME/.hermes/scripts/trend-radar-feed.sh" "$HOME/.hermes/scripts/pruefstand.sh"
|
||||
# Werkstatt-Skill (Autonomie E6): Selbstwartungs-Kreislauf für Hermes.
|
||||
mkdir -p "$HOME/.hermes/skills/wartung"
|
||||
cp "$SRC/deploy/skills/wartung/SKILL.md" "$HOME/.hermes/skills/wartung/SKILL.md"
|
||||
|
||||
@@ -0,0 +1,571 @@
|
||||
#!/usr/bin/env python3
|
||||
# Prüfstand-Harness (17.07.2026): fährt ECHTE Prüfungen gegen ein OpenAI-kompatibles
|
||||
# Endpoint (llama-swap :8080 für Amtsinhaber, Bench-Port :5899 für Kandidaten) und
|
||||
# bewertet MECHANISCH — bestanden/durchgefallen statt Bauchgefühl.
|
||||
#
|
||||
# Suiten (deploy/pruefstand/suites/):
|
||||
# coding Aufgabe → Modell schreibt Code → echte Unit-Tests laufen im Sandkasten
|
||||
# agent Mehrschritt-Aufgabe mit Werkzeugen (Dateien lesen/schreiben/löschen im
|
||||
# Sandkasten, Tool-Calling-API) → bewertet am ENDZUSTAND + Tool-Fehlerquote
|
||||
# recherche Fragen gegen mitgelieferte Dokumente → Pflicht-Stichworte + wörtliches
|
||||
# Zitat, das mechanisch gegen die Dokumente geprüft wird (Ehrlichkeits-Gate)
|
||||
# deutsch Lucy-Alltags-Turns → Richter-Modell (Zwei-Kritiker-Muster) vergibt Noten
|
||||
# vision generiertes Testbild beschreiben (nur für Vision-Rollen)
|
||||
# speed kurze + lange Probe, pp/tg aus den llama.cpp-Timings
|
||||
#
|
||||
# Nutzung:
|
||||
# python3 harness.py --endpoint http://127.0.0.1:8080/v1 --model hermes \
|
||||
# --suites coding,agent --out ergebnis.json [--judge-model gpt-oss-120b] \
|
||||
# [--deadline-min 90] [--vision-image pfad.png]
|
||||
#
|
||||
# Lehren eingebaut: Reasoning-Modelle brauchen großzügiges max_tokens (Verdikt 14.07.:
|
||||
# knappes Budget = leerer content, nie "kaputt"); Timings stehen im "timings"-Feld der
|
||||
# llama.cpp-Antwort; jeder API-Fehler = Aufgabe durchgefallen mit Grund, nie Abbruch.
|
||||
import argparse
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import unicodedata
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
HIER = os.path.dirname(os.path.abspath(__file__))
|
||||
SUITES_DIR = os.path.join(HIER, "suites")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- HTTP-Helfer
|
||||
def api_chat(endpoint, model, messages, max_tokens=1800, tools=None, timeout=420,
|
||||
temperature=0.1):
|
||||
"""Ein Chat-Call. Gibt (message-dict, timings-dict, fehler-string) zurück."""
|
||||
body = {"model": model, "messages": messages, "max_tokens": max_tokens,
|
||||
"temperature": temperature}
|
||||
if tools:
|
||||
body["tools"] = tools
|
||||
body["tool_choice"] = "auto"
|
||||
req = urllib.request.Request(
|
||||
endpoint.rstrip("/") + "/chat/completions",
|
||||
data=json.dumps(body).encode("utf-8"),
|
||||
headers={"Content-Type": "application/json"})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=timeout) as r:
|
||||
d = json.loads(r.read().decode("utf-8", "replace"))
|
||||
msg = d["choices"][0]["message"]
|
||||
return msg, d.get("timings") or {}, ""
|
||||
except Exception as e: # Timeout, HTTP-Fehler, kaputtes JSON — alles ehrlich melden
|
||||
return None, {}, f"{type(e).__name__}: {e}"
|
||||
|
||||
|
||||
def inhalt_von(msg):
|
||||
"""content zuerst, reasoning_content als Notnagel (Reasoning-Modelle)."""
|
||||
if not msg:
|
||||
return ""
|
||||
return (msg.get("content") or msg.get("reasoning_content") or "").strip()
|
||||
|
||||
|
||||
def norm(s):
|
||||
"""Normalisierung fürs Zitat-Gate (identisch zur idle-radar-Mechanik)."""
|
||||
s = unicodedata.normalize("NFKC", s or "")
|
||||
s = "".join(ch for ch in s if ch.isalnum() or ch.isspace())
|
||||
return " ".join(s.lower().split())
|
||||
|
||||
|
||||
def code_block(text, sprache="python"):
|
||||
"""Letzten passenden Code-Block extrahieren; ohne Zaun: ganzen Text nehmen."""
|
||||
bloecke = re.findall(r"```(?:%s)?\s*\n(.*?)```" % re.escape(sprache),
|
||||
text or "", re.DOTALL | re.IGNORECASE)
|
||||
if bloecke:
|
||||
return bloecke[-1]
|
||||
bloecke = re.findall(r"```\s*\n(.*?)```", text or "", re.DOTALL)
|
||||
return bloecke[-1] if bloecke else (text or "")
|
||||
|
||||
|
||||
def lade_task(pfad):
|
||||
with open(pfad, encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- Suite: coding
|
||||
def suite_coding(cfg, ergebnisse):
|
||||
basis = os.path.join(SUITES_DIR, "coding", "aufgaben")
|
||||
for tid in sorted(os.listdir(basis)):
|
||||
tdir = os.path.join(basis, tid)
|
||||
if not os.path.isfile(os.path.join(tdir, "task.json")):
|
||||
continue
|
||||
if cfg.abgelaufen():
|
||||
ergebnisse.append({"suite": "coding", "id": tid, "pass": False,
|
||||
"grund": "Zeitbudget erschöpft — übersprungen"})
|
||||
continue
|
||||
task = lade_task(os.path.join(tdir, "task.json"))
|
||||
t0 = time.time()
|
||||
prompt = task["prompt"]
|
||||
for name, inhalt in (task.get("dateien") or {}).items():
|
||||
prompt += f"\n\n--- Datei `{name}` (Ist-Stand) ---\n```python\n{inhalt}\n```"
|
||||
prompt += ("\n\nAntworte mit GENAU EINEM ```python-Code-Block, der den "
|
||||
f"vollständigen Inhalt der Datei `{task['antwort_datei']}` enthält. "
|
||||
"Keine Erklärungen außerhalb des Blocks.")
|
||||
msg, tim, err = api_chat(cfg.endpoint, cfg.model,
|
||||
[{"role": "user", "content": prompt}],
|
||||
max_tokens=task.get("max_tokens", 2600))
|
||||
eintrag = {"suite": "coding", "id": tid, "dauer_s": round(time.time() - t0, 1),
|
||||
"tg_tps": tim.get("predicted_per_second"),
|
||||
"pp_tps": tim.get("prompt_per_second")}
|
||||
if err:
|
||||
eintrag.update({"pass": False, "grund": f"API-Fehler: {err}"})
|
||||
ergebnisse.append(eintrag)
|
||||
continue
|
||||
code = code_block(inhalt_von(msg))
|
||||
sbx = tempfile.mkdtemp(prefix="pruefstand-coding-")
|
||||
try:
|
||||
for name, inhalt in (task.get("dateien") or {}).items():
|
||||
with open(os.path.join(sbx, name), "w", encoding="utf-8") as f:
|
||||
f.write(inhalt)
|
||||
with open(os.path.join(sbx, task["antwort_datei"]), "w",
|
||||
encoding="utf-8") as f:
|
||||
f.write(code)
|
||||
shutil.copy(os.path.join(tdir, "verify.py"), os.path.join(sbx, "verify.py"))
|
||||
p = subprocess.run([sys.executable, "verify.py"], cwd=sbx,
|
||||
capture_output=True, text=True,
|
||||
timeout=task.get("timeout_s", 120))
|
||||
eintrag["pass"] = (p.returncode == 0)
|
||||
if p.returncode != 0:
|
||||
eintrag["grund"] = (p.stdout + p.stderr).strip()[-400:]
|
||||
except subprocess.TimeoutExpired:
|
||||
eintrag.update({"pass": False, "grund": "verify-Timeout (Endlosschleife?)"})
|
||||
except Exception as e:
|
||||
eintrag.update({"pass": False, "grund": f"Sandkasten-Fehler: {e}"})
|
||||
finally:
|
||||
shutil.rmtree(sbx, ignore_errors=True)
|
||||
ergebnisse.append(eintrag)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- Suite: agent
|
||||
WERKZEUGE = [
|
||||
{"type": "function", "function": {
|
||||
"name": "liste_dateien",
|
||||
"description": "Listet alle Dateien im Arbeitsverzeichnis (rekursiv).",
|
||||
"parameters": {"type": "object", "properties": {}}}},
|
||||
{"type": "function", "function": {
|
||||
"name": "lies_datei",
|
||||
"description": "Liest eine Datei aus dem Arbeitsverzeichnis.",
|
||||
"parameters": {"type": "object", "properties": {
|
||||
"pfad": {"type": "string"}}, "required": ["pfad"]}}},
|
||||
{"type": "function", "function": {
|
||||
"name": "schreibe_datei",
|
||||
"description": "Schreibt/überschreibt eine Datei im Arbeitsverzeichnis.",
|
||||
"parameters": {"type": "object", "properties": {
|
||||
"pfad": {"type": "string"}, "inhalt": {"type": "string"}},
|
||||
"required": ["pfad", "inhalt"]}}},
|
||||
{"type": "function", "function": {
|
||||
"name": "loesche_datei",
|
||||
"description": "Löscht eine Datei im Arbeitsverzeichnis.",
|
||||
"parameters": {"type": "object", "properties": {
|
||||
"pfad": {"type": "string"}}, "required": ["pfad"]}}},
|
||||
{"type": "function", "function": {
|
||||
"name": "fertig",
|
||||
"description": "Aufgabe abgeschlossen. Kurze Abschluss-Meldung übergeben.",
|
||||
"parameters": {"type": "object", "properties": {
|
||||
"meldung": {"type": "string"}}, "required": ["meldung"]}}},
|
||||
]
|
||||
|
||||
|
||||
def sicherer_pfad(sbx, pfad):
|
||||
"""Pfad-Ausbruch verhindern — Sandkasten bleibt Sandkasten."""
|
||||
ziel = os.path.realpath(os.path.join(sbx, pfad or ""))
|
||||
if not ziel.startswith(os.path.realpath(sbx) + os.sep):
|
||||
raise ValueError(f"Pfad verlässt den Sandkasten: {pfad}")
|
||||
return ziel
|
||||
|
||||
|
||||
def werkzeug_ausfuehren(sbx, name, args):
|
||||
if name == "liste_dateien":
|
||||
alle = []
|
||||
for wurzel, _, dateien in os.walk(sbx):
|
||||
for d in dateien:
|
||||
alle.append(os.path.relpath(os.path.join(wurzel, d), sbx))
|
||||
return "\n".join(sorted(alle)) or "(leer)"
|
||||
if name == "lies_datei":
|
||||
with open(sicherer_pfad(sbx, args["pfad"]), encoding="utf-8",
|
||||
errors="replace") as f:
|
||||
return f.read()[:20000]
|
||||
if name == "schreibe_datei":
|
||||
ziel = sicherer_pfad(sbx, args["pfad"])
|
||||
os.makedirs(os.path.dirname(ziel), exist_ok=True)
|
||||
with open(ziel, "w", encoding="utf-8") as f:
|
||||
f.write(args.get("inhalt", ""))
|
||||
return f"geschrieben: {args['pfad']}"
|
||||
if name == "loesche_datei":
|
||||
os.remove(sicherer_pfad(sbx, args["pfad"]))
|
||||
return f"gelöscht: {args['pfad']}"
|
||||
raise ValueError(f"unbekanntes Werkzeug: {name}")
|
||||
|
||||
|
||||
def suite_agent(cfg, ergebnisse):
|
||||
basis = os.path.join(SUITES_DIR, "agent", "aufgaben")
|
||||
for tid in sorted(os.listdir(basis)):
|
||||
tdir = os.path.join(basis, tid)
|
||||
if not os.path.isfile(os.path.join(tdir, "task.json")):
|
||||
continue
|
||||
if cfg.abgelaufen():
|
||||
ergebnisse.append({"suite": "agent", "id": tid, "pass": False,
|
||||
"grund": "Zeitbudget erschöpft — übersprungen"})
|
||||
continue
|
||||
task = lade_task(os.path.join(tdir, "task.json"))
|
||||
t0 = time.time()
|
||||
sbx = tempfile.mkdtemp(prefix="pruefstand-agent-")
|
||||
tool_fehler = 0
|
||||
schritte = 0
|
||||
try:
|
||||
for name, inhalt in (task.get("sandkasten") or {}).items():
|
||||
ziel = os.path.join(sbx, name)
|
||||
os.makedirs(os.path.dirname(ziel), exist_ok=True)
|
||||
with open(ziel, "w", encoding="utf-8") as f:
|
||||
f.write(inhalt)
|
||||
messages = [
|
||||
{"role": "system", "content":
|
||||
"Du bist ein Arbeits-Agent mit Datei-Werkzeugen in einem "
|
||||
"Sandkasten-Verzeichnis. Erledige die Aufgabe Schritt für Schritt "
|
||||
"mit den Werkzeugen und rufe am Ende `fertig` auf."},
|
||||
{"role": "user", "content": task["prompt"]},
|
||||
]
|
||||
fertig_gemeldet = False
|
||||
for _ in range(task.get("max_schritte", 8)):
|
||||
schritte += 1
|
||||
msg, _tim, err = api_chat(cfg.endpoint, cfg.model, messages,
|
||||
max_tokens=1800, tools=WERKZEUGE)
|
||||
if err:
|
||||
break
|
||||
calls = msg.get("tool_calls") or []
|
||||
if not calls:
|
||||
break # Agent redet statt zu handeln — Endzustand zählt trotzdem
|
||||
messages.append({"role": "assistant",
|
||||
"content": msg.get("content") or "",
|
||||
"tool_calls": calls})
|
||||
for call in calls:
|
||||
fn = (call.get("function") or {}).get("name") or "?"
|
||||
try:
|
||||
args = json.loads((call.get("function") or {})
|
||||
.get("arguments") or "{}")
|
||||
except Exception:
|
||||
args, fn_ok = {}, False
|
||||
tool_fehler += 1
|
||||
antwort = "FEHLER: Argumente waren kein gültiges JSON."
|
||||
else:
|
||||
fn_ok = True
|
||||
if fn_ok:
|
||||
if fn == "fertig":
|
||||
fertig_gemeldet = True
|
||||
antwort = "ok"
|
||||
else:
|
||||
try:
|
||||
antwort = werkzeug_ausfuehren(sbx, fn, args)
|
||||
except Exception as e:
|
||||
tool_fehler += 1
|
||||
antwort = f"FEHLER: {e}"
|
||||
messages.append({"role": "tool",
|
||||
"tool_call_id": call.get("id") or "0",
|
||||
"content": antwort})
|
||||
if fertig_gemeldet:
|
||||
break
|
||||
# -------- Endzustand prüfen (die einzige Wahrheit)
|
||||
fehlschlaege = []
|
||||
for check in task.get("checks", []):
|
||||
if "nicht_vorhanden" in check:
|
||||
if os.path.exists(os.path.join(sbx, check["nicht_vorhanden"])):
|
||||
fehlschlaege.append(
|
||||
f"{check['nicht_vorhanden']} existiert noch")
|
||||
continue
|
||||
pfad = os.path.join(sbx, check["datei"])
|
||||
if not os.path.isfile(pfad):
|
||||
fehlschlaege.append(f"{check['datei']} fehlt")
|
||||
continue
|
||||
with open(pfad, encoding="utf-8", errors="replace") as f:
|
||||
text = f.read()
|
||||
if check.get("regex") and not re.search(check["regex"], text,
|
||||
re.IGNORECASE | re.MULTILINE):
|
||||
fehlschlaege.append(
|
||||
f"{check['datei']}: Muster fehlt ({check['regex']})")
|
||||
ergebnisse.append({
|
||||
"suite": "agent", "id": tid, "pass": not fehlschlaege,
|
||||
"grund": "; ".join(fehlschlaege)[:400] if fehlschlaege else "",
|
||||
"schritte": schritte, "tool_fehler": tool_fehler,
|
||||
"fertig_gemeldet": fertig_gemeldet,
|
||||
"dauer_s": round(time.time() - t0, 1)})
|
||||
except Exception as e:
|
||||
ergebnisse.append({"suite": "agent", "id": tid, "pass": False,
|
||||
"grund": f"Harness-Fehler: {e}",
|
||||
"dauer_s": round(time.time() - t0, 1)})
|
||||
finally:
|
||||
shutil.rmtree(sbx, ignore_errors=True)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- Suite: recherche
|
||||
def suite_recherche(cfg, ergebnisse):
|
||||
basis = os.path.join(SUITES_DIR, "recherche")
|
||||
dok_dir = os.path.join(basis, "dokumente")
|
||||
dokumente = {}
|
||||
for d in sorted(os.listdir(dok_dir)):
|
||||
with open(os.path.join(dok_dir, d), encoding="utf-8") as f:
|
||||
dokumente[d] = f.read()
|
||||
alle_norm = norm("\n".join(dokumente.values()))
|
||||
material = "\n\n".join(f"===== DOKUMENT {n} =====\n{t}"
|
||||
for n, t in dokumente.items())
|
||||
for tid in sorted(os.listdir(os.path.join(basis, "aufgaben"))):
|
||||
tpfad = os.path.join(basis, "aufgaben", tid)
|
||||
if not tpfad.endswith(".json"):
|
||||
continue
|
||||
if cfg.abgelaufen():
|
||||
ergebnisse.append({"suite": "recherche", "id": tid, "pass": False,
|
||||
"grund": "Zeitbudget erschöpft — übersprungen"})
|
||||
continue
|
||||
task = lade_task(tpfad)
|
||||
t0 = time.time()
|
||||
prompt = (f"{material}\n\nFRAGE: {task['frage']}\n\n"
|
||||
"Beantworte die Frage NUR aus den Dokumenten oben, auf Deutsch, "
|
||||
"in höchstens 4 Sätzen. Letzte Zeile deiner Antwort: "
|
||||
'ZITAT: "<eine wörtlich aus einem Dokument übernommene Zeile, '
|
||||
'die deine Antwort belegt>"')
|
||||
msg, tim, err = api_chat(cfg.endpoint, cfg.model,
|
||||
[{"role": "user", "content": prompt}],
|
||||
max_tokens=1200)
|
||||
eintrag = {"suite": "recherche", "id": task.get("id", tid),
|
||||
"dauer_s": round(time.time() - t0, 1),
|
||||
"tg_tps": tim.get("predicted_per_second"),
|
||||
"pp_tps": tim.get("prompt_per_second")}
|
||||
if err:
|
||||
eintrag.update({"pass": False, "grund": f"API-Fehler: {err}"})
|
||||
ergebnisse.append(eintrag)
|
||||
continue
|
||||
antwort = inhalt_von(msg)
|
||||
fehlschlaege = []
|
||||
for gruppe in task.get("muss_gruppen", []):
|
||||
if not any(k.lower() in antwort.lower() for k in gruppe):
|
||||
fehlschlaege.append(f"Pflicht-Stichwort fehlt: {'/'.join(gruppe)}")
|
||||
m = re.search(r'ZITAT:\s*[„"]?(.+?)["“”]?\s*$', antwort,
|
||||
re.MULTILINE | re.DOTALL)
|
||||
zitat = (m.group(1).strip() if m else "")
|
||||
if task.get("zitat_pflicht", True):
|
||||
if not zitat:
|
||||
fehlschlaege.append("kein ZITAT geliefert")
|
||||
elif norm(zitat) not in alle_norm:
|
||||
fehlschlaege.append("ZITAT nicht wörtlich in den Dokumenten "
|
||||
"(Ehrlichkeits-Gate)")
|
||||
eintrag["pass"] = not fehlschlaege
|
||||
if fehlschlaege:
|
||||
eintrag["grund"] = "; ".join(fehlschlaege)[:400]
|
||||
ergebnisse.append(eintrag)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- Suite: deutsch
|
||||
RICHTER_RASTER = (
|
||||
"Du bist ein strenger Deutsch-Richter für eine Sprach-Assistentin (Lucy): "
|
||||
"locker, natürlich, Du-Form, kurze gesprochene Sätze, kein Beamten- oder "
|
||||
"Übersetzungsdeutsch, keine Anglizismen-Ketten, fachlich korrekt. Bewerte die "
|
||||
"ANTWORT unten auf einer Skala 1–10 (10 = klingt wie eine deutsche "
|
||||
"Muttersprachlerin im Alltag). Antworte EXAKT in diesem Format:\n"
|
||||
"NOTE: <Zahl 1-10>\nBEGRUENDUNG: <ein Satz>")
|
||||
|
||||
|
||||
def suite_deutsch(cfg, ergebnisse):
|
||||
aufgaben = lade_task(os.path.join(SUITES_DIR, "deutsch", "aufgaben.json"))
|
||||
for task in aufgaben:
|
||||
if cfg.abgelaufen():
|
||||
ergebnisse.append({"suite": "deutsch", "id": task["id"], "pass": False,
|
||||
"grund": "Zeitbudget erschöpft — übersprungen"})
|
||||
continue
|
||||
t0 = time.time()
|
||||
msg, tim, err = api_chat(cfg.endpoint, cfg.model,
|
||||
[{"role": "user", "content": task["prompt"]}],
|
||||
max_tokens=900)
|
||||
eintrag = {"suite": "deutsch", "id": task["id"],
|
||||
"dauer_s": round(time.time() - t0, 1),
|
||||
"tg_tps": tim.get("predicted_per_second"),
|
||||
"pp_tps": tim.get("prompt_per_second")}
|
||||
if err:
|
||||
eintrag.update({"pass": False, "grund": f"API-Fehler: {err}"})
|
||||
ergebnisse.append(eintrag)
|
||||
continue
|
||||
antwort = inhalt_von(msg)
|
||||
jmsg, _jt, jerr = api_chat(cfg.judge_endpoint, cfg.judge_model, [
|
||||
{"role": "system", "content": RICHTER_RASTER},
|
||||
{"role": "user", "content":
|
||||
f"AUFGABE AN DIE ASSISTENTIN: {task['prompt']}\n\n"
|
||||
f"ANTWORT:\n{antwort}"}],
|
||||
max_tokens=2200, temperature=0.2)
|
||||
note = None
|
||||
if not jerr:
|
||||
m = re.search(r"NOTE:\s*(\d{1,2})", inhalt_von(jmsg))
|
||||
if m:
|
||||
note = max(1, min(10, int(m.group(1))))
|
||||
eintrag["note"] = note
|
||||
eintrag["pass"] = (note is not None and note >= 6)
|
||||
if note is None:
|
||||
eintrag["grund"] = f"Richter ohne Note ({jerr or 'Format'})"
|
||||
ergebnisse.append(eintrag)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- Suite: vision
|
||||
def suite_vision(cfg, ergebnisse):
|
||||
task = lade_task(os.path.join(SUITES_DIR, "vision", "task.json"))
|
||||
bild = cfg.vision_image or os.path.join(SUITES_DIR, "vision", "testbild.png")
|
||||
if not os.path.isfile(bild):
|
||||
# Testbild deterministisch erzeugen (reines Python, kein PIL nötig)
|
||||
subprocess.run([sys.executable,
|
||||
os.path.join(SUITES_DIR, "vision", "gen_testbild.py"), bild],
|
||||
check=False, timeout=30)
|
||||
if not os.path.isfile(bild):
|
||||
ergebnisse.append({"suite": "vision", "id": "testbild", "pass": False,
|
||||
"grund": "Testbild fehlt und konnte nicht erzeugt werden"})
|
||||
return
|
||||
with open(bild, "rb") as f:
|
||||
b64 = base64.b64encode(f.read()).decode("ascii")
|
||||
t0 = time.time()
|
||||
msg, tim, err = api_chat(cfg.endpoint, cfg.model, [
|
||||
{"role": "user", "content": [
|
||||
{"type": "text", "text": task["prompt"]},
|
||||
{"type": "image_url",
|
||||
"image_url": {"url": f"data:image/png;base64,{b64}"}}]}],
|
||||
max_tokens=900, timeout=600)
|
||||
eintrag = {"suite": "vision", "id": "testbild",
|
||||
"dauer_s": round(time.time() - t0, 1),
|
||||
"tg_tps": tim.get("predicted_per_second")}
|
||||
if err:
|
||||
eintrag.update({"pass": False, "grund": f"API-Fehler: {err}"})
|
||||
ergebnisse.append(eintrag)
|
||||
return
|
||||
antwort = inhalt_von(msg)
|
||||
fehlschlaege = [f"nicht erkannt: {'/'.join(g)}"
|
||||
for g in task.get("muss_gruppen", [])
|
||||
if not any(k.lower() in antwort.lower() for k in g)]
|
||||
eintrag["pass"] = not fehlschlaege
|
||||
if fehlschlaege:
|
||||
eintrag["grund"] = "; ".join(fehlschlaege)[:300]
|
||||
eintrag["antwort_auszug"] = antwort[:200]
|
||||
ergebnisse.append(eintrag)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- Suite: speed
|
||||
LANG_BAUSTEIN = ("Die Box protokolliert jeden Dienststart mit Zeitstempel, "
|
||||
"Dienstname und Ergebnis, damit die Morgenlage Abweichungen "
|
||||
"gegen die Vorwoche erkennen und einordnen kann. ")
|
||||
|
||||
|
||||
def suite_speed(cfg, ergebnisse):
|
||||
# Kurz-Probe: misst tg (Alltags-Turn) — 2 Läufe, erster wärmt den Cache an.
|
||||
for lauf in ("warm", "kurz"):
|
||||
t0 = time.time()
|
||||
msg, tim, err = api_chat(cfg.endpoint, cfg.model, [
|
||||
{"role": "user", "content":
|
||||
"Erkläre in drei kurzen Sätzen, was ein Mixture-of-Experts-Modell "
|
||||
"ist."}], max_tokens=200, temperature=0)
|
||||
if lauf == "warm":
|
||||
continue
|
||||
ergebnisse.append({"suite": "speed", "id": "kurz", "pass": not err,
|
||||
"grund": err[:200] if err else "",
|
||||
"dauer_s": round(time.time() - t0, 1),
|
||||
"tg_tps": tim.get("predicted_per_second"),
|
||||
"pp_tps": tim.get("prompt_per_second")})
|
||||
# Lang-Probe: ~12k-Token-Prompt → misst pp bei echtem Kontext (ROCm-Watch-Metrik).
|
||||
if cfg.abgelaufen():
|
||||
ergebnisse.append({"suite": "speed", "id": "lang", "pass": False,
|
||||
"grund": "Zeitbudget erschöpft — übersprungen"})
|
||||
return
|
||||
lang = LANG_BAUSTEIN * 550
|
||||
t0 = time.time()
|
||||
msg, tim, err = api_chat(cfg.endpoint, cfg.model, [
|
||||
{"role": "user", "content":
|
||||
lang + "\n\nWie oft steht sinngemäß derselbe Satz oben? Antworte in "
|
||||
"einem Satz."}], max_tokens=80, temperature=0, timeout=900)
|
||||
ergebnisse.append({"suite": "speed", "id": "lang", "pass": not err,
|
||||
"grund": err[:200] if err else "",
|
||||
"dauer_s": round(time.time() - t0, 1),
|
||||
"tg_tps": tim.get("predicted_per_second"),
|
||||
"pp_tps": tim.get("prompt_per_second"),
|
||||
"prompt_tokens": tim.get("prompt_n")})
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- Hauptlauf
|
||||
SUITE_FUNKS = {"coding": suite_coding, "agent": suite_agent,
|
||||
"recherche": suite_recherche, "deutsch": suite_deutsch,
|
||||
"vision": suite_vision, "speed": suite_speed}
|
||||
|
||||
|
||||
class Konfig:
|
||||
def __init__(self, args):
|
||||
self.endpoint = args.endpoint
|
||||
self.model = args.model
|
||||
self.judge_endpoint = args.judge_endpoint or args.endpoint
|
||||
self.judge_model = args.judge_model
|
||||
self.vision_image = args.vision_image
|
||||
self.deadline = (time.time() + args.deadline_min * 60
|
||||
if args.deadline_min else None)
|
||||
|
||||
def abgelaufen(self):
|
||||
return self.deadline is not None and time.time() > self.deadline
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description="Prüfstand-Harness")
|
||||
ap.add_argument("--endpoint", required=True)
|
||||
ap.add_argument("--model", required=True)
|
||||
ap.add_argument("--suites", required=True,
|
||||
help="kommagetrennt: coding,agent,recherche,deutsch,vision,speed")
|
||||
ap.add_argument("--out", required=True)
|
||||
ap.add_argument("--judge-endpoint", default="")
|
||||
ap.add_argument("--judge-model", default="gpt-oss-120b")
|
||||
ap.add_argument("--vision-image", default="")
|
||||
ap.add_argument("--deadline-min", type=float, default=0)
|
||||
args = ap.parse_args()
|
||||
|
||||
cfg = Konfig(args)
|
||||
ergebnisse = []
|
||||
for name in [s.strip() for s in args.suites.split(",") if s.strip()]:
|
||||
if name not in SUITE_FUNKS:
|
||||
ergebnisse.append({"suite": name, "id": "-", "pass": False,
|
||||
"grund": "unbekannte Suite"})
|
||||
continue
|
||||
try:
|
||||
SUITE_FUNKS[name](cfg, ergebnisse)
|
||||
except Exception as e: # kaputte Suite darf den Gesamtlauf nicht töten
|
||||
ergebnisse.append({"suite": name, "id": "-", "pass": False,
|
||||
"grund": f"Suite-Absturz: {type(e).__name__}: {e}"})
|
||||
|
||||
# -------- Zusammenfassung je Suite (bestanden/gesamt + Tempo-Mittel)
|
||||
zsm = {}
|
||||
for e in ergebnisse:
|
||||
s = zsm.setdefault(e["suite"], {"bestanden": 0, "gesamt": 0,
|
||||
"tg": [], "pp": [], "noten": []})
|
||||
s["gesamt"] += 1
|
||||
s["bestanden"] += 1 if e.get("pass") else 0
|
||||
if e.get("tg_tps"):
|
||||
s["tg"].append(e["tg_tps"])
|
||||
if e.get("pp_tps"):
|
||||
s["pp"].append(e["pp_tps"])
|
||||
if e.get("note") is not None:
|
||||
s["noten"].append(e["note"])
|
||||
for s in zsm.values():
|
||||
s["tg_mittel"] = round(sum(s["tg"]) / len(s["tg"]), 1) if s["tg"] else None
|
||||
s["pp_mittel"] = round(sum(s["pp"]) / len(s["pp"]), 1) if s["pp"] else None
|
||||
s["note_mittel"] = (round(sum(s["noten"]) / len(s["noten"]), 1)
|
||||
if s["noten"] else None)
|
||||
del s["tg"], s["pp"], s["noten"]
|
||||
|
||||
with open(args.out, "w", encoding="utf-8") as f:
|
||||
json.dump({"modell": args.model, "endpoint": args.endpoint,
|
||||
"erhoben": time.strftime("%Y-%m-%d %H:%M"),
|
||||
"zusammenfassung": zsm, "einzeln": ergebnisse},
|
||||
f, ensure_ascii=False, indent=1)
|
||||
# Kurzfassung auf stdout (für Runner-Logs)
|
||||
for name, s in zsm.items():
|
||||
extra = f" · Note {s['note_mittel']}" if s.get("note_mittel") else ""
|
||||
tempo = (f" · tg {s['tg_mittel']} t/s" if s.get("tg_mittel") else "")
|
||||
print(f"{name}: {s['bestanden']}/{s['gesamt']}{extra}{tempo}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,348 @@
|
||||
#!/usr/bin/env bash
|
||||
# Prüfstand (17.07.2026): das "volle Programm" für Modell-Kandidaten — echte Coding-,
|
||||
# Agenten-, Recherche-, Deutsch- und Vision-Prüfungen (harness.py) plus Tempo, verglichen
|
||||
# gegen die BASELINE der Amtsinhaber. Läuft als Cron (So 01:00, --no-agent: stdout wird
|
||||
# wörtlich zugestellt; LEERER stdout = stille Nacht).
|
||||
#
|
||||
# Modi:
|
||||
# (ohne Argument) Cron-Modus: Baseline fehlt → Baseline messen; sonst ältesten
|
||||
# Kandidaten aus der Queue prüfen; nichts zu tun → still.
|
||||
# --baseline Amtsinhaber über llama-swap (:8080) neu vermessen.
|
||||
# --kandidat <spec> einen Kandidaten-Spec (JSON) direkt prüfen.
|
||||
#
|
||||
# Leitplanken (User-Entscheid 17.07.): max 1 Kandidat je Lauf · Download-Deckel 35 GB
|
||||
# (größere Kandidaten → Karte, Download erst nach Klick) · eigener Cache mit
|
||||
# Auto-Aufräumen · Live-Betrieb bleibt unangetastet (eigener Server auf :5899) ·
|
||||
# der Prüfstand EMPFIEHLT NUR — Rollen-Wechsel entscheidet der Commander per Karte.
|
||||
# RAM-Wache vor dem Serverstart; Heredoc+Pipe-Falle vermieden (Temp-Dateien, Lehre 10.07.).
|
||||
set -uo pipefail
|
||||
|
||||
MC="$HOME/mission-control-v2"
|
||||
PSD="$MC/deploy/pruefstand"
|
||||
VAULT="$HOME/wissens-vault"
|
||||
STATE="$HOME/.hermes/state/pruefstand"
|
||||
CACHE="/srv/models/pruefstand-cache"
|
||||
HERMES="$HOME/.local/bin/hermes"
|
||||
BENCH_BIN="${BENCH_BIN:-/opt/llamacpp-vulkan/llama-server}"
|
||||
BENCH_PORT="${BENCH_PORT:-5899}"
|
||||
LIVE="http://127.0.0.1:8080/v1"
|
||||
JUDGE_MODEL="${PRUEFSTAND_JUDGE:-gpt-oss-120b}"
|
||||
DECKEL_GB="${PRUEFSTAND_DECKEL_GB:-35}"
|
||||
DEADLINE_MIN="${PRUEFSTAND_DEADLINE_MIN:-100}"
|
||||
|
||||
mkdir -p "$STATE/queue" "$STATE/done" "$STATE/ergebnisse"
|
||||
|
||||
# Suiten je Rolle (Amtsinhaber-Alias = Rollenname in llama-swap)
|
||||
suiten_fuer() {
|
||||
case "$1" in
|
||||
hermes) echo "speed,agent,recherche,deutsch" ;;
|
||||
coder) echo "speed,coding,agent" ;;
|
||||
heavy) echo "speed,coding,recherche,deutsch" ;;
|
||||
vision) echo "speed,vision" ;;
|
||||
scout) echo "speed,agent,vision" ;;
|
||||
*) echo "speed,recherche,deutsch" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
briefkasten() { # $1=Betreff $2=Text (stiller Chronik-Spiegel, source=pruefstand)
|
||||
curl -sf -m 5 -X POST "${MC_ANNOUNCE_URL:-http://127.0.0.1:9001/api/voice/announce}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data "$(python3 - "$1" "$2" <<'PY'
|
||||
import json, sys
|
||||
print(json.dumps({"text": sys.argv[2], "subject": sys.argv[1],
|
||||
"source": "pruefstand", "priority": "silent"}))
|
||||
PY
|
||||
)" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
zusammenfassung_lesen() { # $1=ergebnis.json → kompakte Zeilen "suite: x/y · Note · tg"
|
||||
python3 - "$1" <<'PY'
|
||||
import json, sys
|
||||
try:
|
||||
z = json.load(open(sys.argv[1])).get("zusammenfassung", {})
|
||||
except Exception:
|
||||
print("(Ergebnis nicht lesbar)"); raise SystemExit
|
||||
for name, s in z.items():
|
||||
teile = [f"{s['bestanden']}/{s['gesamt']}"]
|
||||
if s.get("note_mittel"): teile.append(f"Note {s['note_mittel']}")
|
||||
if s.get("tg_mittel"): teile.append(f"tg {s['tg_mittel']} t/s")
|
||||
if s.get("pp_mittel"): teile.append(f"pp {s['pp_mittel']} t/s")
|
||||
print(f" {name}: " + " · ".join(teile))
|
||||
PY
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------- Baseline
|
||||
baseline_lauf() {
|
||||
echo "## PRÜFSTAND: Basislinie der Amtsinhaber (erhoben am $(date '+%d.%m.%Y %H:%M'))"
|
||||
echo "(Echte Prüfungen: Coding mit Unit-Tests, Agenten-Aufgaben, Recherche mit Zitat-Gate, Deutsch-Richter, Tempo.)"
|
||||
local rollen="hermes coder heavy vision scout"
|
||||
for rolle in $rollen; do
|
||||
local out="$STATE/ergebnisse/baseline-${rolle}.json"
|
||||
echo
|
||||
echo "### Amtsinhaber \`$rolle\`"
|
||||
python3 "$PSD/harness.py" --endpoint "$LIVE" --model "$rolle" \
|
||||
--suites "$(suiten_fuer "$rolle")" --out "$out" \
|
||||
--judge-endpoint "$LIVE" --judge-model "$JUDGE_MODEL" \
|
||||
--deadline-min 30 2>&1 | tail -8
|
||||
done
|
||||
# embed: nur Funktions- und Latenz-Probe (Suiten ergeben hier keinen Sinn)
|
||||
echo
|
||||
echo "### Amtsinhaber \`embed\`"
|
||||
python3 - "$LIVE" <<'PY'
|
||||
import json, sys, time, urllib.request
|
||||
t0 = time.time()
|
||||
req = urllib.request.Request(sys.argv[1].rstrip("/") + "/embeddings",
|
||||
data=json.dumps({"model": "embed", "input": "Prüfstand-Probe"}).encode(),
|
||||
headers={"Content-Type": "application/json"})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
d = json.load(r)
|
||||
dim = len(d["data"][0]["embedding"])
|
||||
print(f" ok · Dimension {dim} · {round((time.time()-t0)*1000)} ms")
|
||||
except Exception as e:
|
||||
print(f" FEHLER: {e}")
|
||||
PY
|
||||
# Sammel-Baseline schreiben (eine Datei, die Kandidaten-Läufe vergleichen)
|
||||
python3 - "$STATE" <<'PY'
|
||||
import glob, json, os, sys, time
|
||||
state = sys.argv[1]
|
||||
basis = {}
|
||||
for f in glob.glob(os.path.join(state, "ergebnisse", "baseline-*.json")):
|
||||
rolle = os.path.basename(f)[len("baseline-"):-len(".json")]
|
||||
try:
|
||||
basis[rolle] = json.load(open(f)).get("zusammenfassung", {})
|
||||
except Exception:
|
||||
pass
|
||||
json.dump({"erhoben": time.strftime("%Y-%m-%d %H:%M"), "rollen": basis},
|
||||
open(os.path.join(state, "baseline.json"), "w"), ensure_ascii=False, indent=1)
|
||||
print(f"\nBasislinie gespeichert ({len(basis)} Rollen).")
|
||||
PY
|
||||
briefkasten "Prüfstand" "Basislinie der Amtsinhaber erhoben — künftige Kandidaten werden dagegen verglichen."
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------- Kandidat
|
||||
kandidat_lauf() { # $1=spec.json
|
||||
local SPEC="$1"
|
||||
local KEY ROLLE HF_ID WARUM
|
||||
KEY="$(jq -r '.key' "$SPEC")"
|
||||
ROLLE="$(jq -r '.rolle' "$SPEC")"
|
||||
HF_ID="$(jq -r '.hf_id' "$SPEC")"
|
||||
WARUM="$(jq -r '.warum // ""' "$SPEC")"
|
||||
echo "## PRÜFSTAND: Kandidat \`$HF_ID\` für Rolle \`$ROLLE\` (am $(date '+%d.%m.%Y %H:%M'))"
|
||||
[ -n "$WARUM" ] && echo "Anlass: $WARUM"
|
||||
|
||||
if [ -f "$STATE/done/${KEY}.json" ]; then
|
||||
echo "Schon geprüft (State done/${KEY}) — nichts zu tun."
|
||||
return 0
|
||||
fi
|
||||
|
||||
# ---- GGUF im HF-Repo finden (bevorzugt ~Q4, ein Stück, Deckel prüfen)
|
||||
local TREE_JSON="$STATE/tmp-tree-${KEY}.json"
|
||||
curl -sf --max-time 60 \
|
||||
"https://huggingface.co/api/models/${HF_ID}/tree/main?recursive=true" \
|
||||
-o "$TREE_JSON" || { echo "HF-API nicht erreichbar — Abbruch, Spec bleibt in der Queue."; return 1; }
|
||||
local AUSWAHL
|
||||
AUSWAHL="$(python3 - "$TREE_JSON" "$DECKEL_GB" <<'PY'
|
||||
import json, re, sys
|
||||
baum = json.load(open(sys.argv[1]))
|
||||
deckel = float(sys.argv[2]) * 2**30
|
||||
dateien = [(e.get("path",""), (e.get("lfs") or {}).get("size") or e.get("size") or 0)
|
||||
for e in baum if isinstance(e, dict)]
|
||||
ggufs = [(p, g) for p, g in dateien if p.lower().endswith(".gguf")]
|
||||
mmproj = next((p for p, g in ggufs if "mmproj" in p.lower()), "")
|
||||
haupt = [(p, g) for p, g in ggufs if "mmproj" not in p.lower()]
|
||||
# Split-GGUFs (…-00001-of-…) klammern wir in v1 aus — sauberer Einzeldatei-Download
|
||||
haupt = [(p, g) for p, g in haupt if not re.search(r"-\d{5}-of-\d{5}", p)]
|
||||
prio = ["q4_k_m", "ud-q4_k_m", "q4_k_s", "iq4", "q5_k_m", "q4", "q8_0"]
|
||||
def rang(p):
|
||||
pl = p.lower()
|
||||
for i, q in enumerate(prio):
|
||||
if q in pl:
|
||||
return i
|
||||
return len(prio)
|
||||
haupt.sort(key=lambda t: (rang(t[0]), t[1]))
|
||||
if not haupt:
|
||||
print("KEINE"); raise SystemExit
|
||||
pfad, groesse = haupt[0]
|
||||
status = "OK" if groesse and groesse <= deckel else "ZU_GROSS"
|
||||
print(status); print(pfad); print(groesse or 0); print(mmproj)
|
||||
PY
|
||||
)"
|
||||
rm -f "$TREE_JSON"
|
||||
local STATUS PFAD GROESSE MMPROJ
|
||||
STATUS="$(printf '%s\n' "$AUSWAHL" | sed -n 1p)"
|
||||
PFAD="$(printf '%s\n' "$AUSWAHL" | sed -n 2p)"
|
||||
GROESSE="$(printf '%s\n' "$AUSWAHL" | sed -n 3p)"
|
||||
MMPROJ="$(printf '%s\n' "$AUSWAHL" | sed -n 4p)"
|
||||
local GB=$(( ${GROESSE:-0} / 1073741824 ))
|
||||
|
||||
if [ "$STATUS" = "KEINE" ]; then
|
||||
echo "Kein brauchbares Einzel-GGUF im Repo (nur Splits oder gar keins) — Karte statt Download."
|
||||
"$HERMES" kanban create "Prüfstand: ${HF_ID} braucht Handarbeit (kein Einzel-GGUF)" \
|
||||
--body "Kandidat für Rolle ${ROLLE}. Im HF-Repo liegt kein Einzeldatei-GGUF ≤ ${DECKEL_GB} GB (vermutlich Split-Dateien). Bei Interesse manuell laden und 'bash ~/mission-control-v2/deploy/pruefstand/pruefstand.sh --kandidat <spec>' fahren. Anlass: ${WARUM}" \
|
||||
--triage --created-by pruefstand --idempotency-key "pruefstand-${KEY}" >/dev/null 2>&1 || true
|
||||
mv "$SPEC" "$STATE/done/${KEY}.spec.json"
|
||||
echo "{\"status\":\"kein_gguf\"}" > "$STATE/done/${KEY}.json"
|
||||
return 0
|
||||
fi
|
||||
if [ "$STATUS" = "ZU_GROSS" ]; then
|
||||
echo "GGUF ${PFAD} ist ${GB} GB > Deckel ${DECKEL_GB} GB — Download wartet auf deinen Klick (Karte liegt bereit)."
|
||||
"$HERMES" kanban create "Prüfstand: ${HF_ID} (${GB} GB) — Download freigeben?" \
|
||||
--body "Kandidat für Rolle ${ROLLE}, Datei ${PFAD} (${GB} GB) liegt ÜBER dem Autonomie-Deckel von ${DECKEL_GB} GB (Entscheid 17.07.). Nach Freigabe: Spec wieder in ~/.hermes/state/pruefstand/queue/ legen und PRUEFSTAND_DECKEL_GB erhöhen oder manuell laden. Anlass: ${WARUM}" \
|
||||
--triage --created-by pruefstand --idempotency-key "pruefstand-${KEY}" >/dev/null 2>&1 || true
|
||||
mv "$SPEC" "$STATE/done/${KEY}.spec.json"
|
||||
echo "{\"status\":\"zu_gross\",\"gb\":${GB}}" > "$STATE/done/${KEY}.json"
|
||||
briefkasten "Prüfstand" "Kandidat ${HF_ID} (${GB} GB) wartet auf Download-Freigabe (Deckel ${DECKEL_GB} GB)."
|
||||
return 0
|
||||
fi
|
||||
|
||||
# ---- Platz- und RAM-Wache
|
||||
local FREI_GB
|
||||
FREI_GB="$(df --output=avail -BG /srv/models | tail -1 | tr -dc '0-9')"
|
||||
if [ "${FREI_GB:-0}" -lt $(( GB + 20 )) ]; then
|
||||
echo "Zu wenig Platz auf /srv/models (${FREI_GB} GB frei, gebraucht ~$((GB+20))) — Lauf verschoben, Spec bleibt in der Queue."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# ---- Download in den Cache (mit Resume, Auto-Aufräumen am Ende)
|
||||
mkdir -p "$CACHE/$KEY"
|
||||
local ZIEL="$CACHE/$KEY/$(basename "$PFAD")"
|
||||
echo "Lade ${PFAD} (${GB} GB) …"
|
||||
curl -fL --retry 3 -C - --max-time 7200 -o "$ZIEL" \
|
||||
"https://huggingface.co/${HF_ID}/resolve/main/${PFAD}" \
|
||||
|| { echo "Download fehlgeschlagen — Spec bleibt in der Queue."; return 1; }
|
||||
local MM_ZIEL=""
|
||||
if [ -n "$MMPROJ" ] && { [ "$ROLLE" = "vision" ] || [ "$ROLLE" = "scout" ]; }; then
|
||||
MM_ZIEL="$CACHE/$KEY/$(basename "$MMPROJ")"
|
||||
curl -fL --retry 3 -C - --max-time 3600 -o "$MM_ZIEL" \
|
||||
"https://huggingface.co/${HF_ID}/resolve/main/${MMPROJ}" || MM_ZIEL=""
|
||||
fi
|
||||
|
||||
local MEM_FREI_GB
|
||||
MEM_FREI_GB="$(awk '/MemAvailable/{printf "%d", $2/1048576}' /proc/meminfo)"
|
||||
if [ "${MEM_FREI_GB:-0}" -lt $(( GB + 8 )) ]; then
|
||||
echo "Zu wenig freier RAM (${MEM_FREI_GB} GB) für ein ${GB}-GB-Modell neben dem Warm-Set — Lauf verschoben."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# ---- Kandidaten-Server auf dem Bench-Port (Live-Betrieb unangetastet)
|
||||
echo "Starte Kandidaten-Server (:${BENCH_PORT}) …"
|
||||
local MMFLAG=""
|
||||
[ -n "$MM_ZIEL" ] && MMFLAG="--mmproj $MM_ZIEL"
|
||||
$BENCH_BIN -m "$ZIEL" --host 127.0.0.1 --port "$BENCH_PORT" \
|
||||
-c 32768 -ngl 999 -fa on --no-mmap --jinja $MMFLAG \
|
||||
>/tmp/pruefstand-server.log 2>&1 &
|
||||
local SPID=$!
|
||||
trap 'kill $SPID 2>/dev/null; wait $SPID 2>/dev/null' RETURN
|
||||
local BEREIT=0
|
||||
for i in $(seq 1 240); do
|
||||
curl -s --max-time 2 "http://127.0.0.1:$BENCH_PORT/health" | grep -q ok && { BEREIT=1; break; }
|
||||
sleep 2
|
||||
kill -0 $SPID 2>/dev/null || break
|
||||
done
|
||||
if [ "$BEREIT" != "1" ]; then
|
||||
echo "LADE-CRASH — Kandidat startet nicht (Architektur zu neu für die Engine?):"
|
||||
tail -3 /tmp/pruefstand-server.log
|
||||
echo "{\"status\":\"lade_crash\"}" > "$STATE/done/${KEY}.json"
|
||||
mv "$SPEC" "$STATE/done/${KEY}.spec.json"
|
||||
rm -rf "${CACHE:?}/$KEY"
|
||||
briefkasten "Prüfstand" "Kandidat ${HF_ID} startet auf unserer Engine nicht (Lade-Crash) — aussortiert."
|
||||
return 0
|
||||
fi
|
||||
|
||||
# ---- Das volle Programm
|
||||
local OUT="$STATE/ergebnisse/kandidat-${KEY}.json"
|
||||
echo "Fahre Suiten: $(suiten_fuer "$ROLLE") (Zeitbudget ${DEADLINE_MIN} min)"
|
||||
python3 "$PSD/harness.py" --endpoint "http://127.0.0.1:$BENCH_PORT/v1" \
|
||||
--model "kandidat" --suites "$(suiten_fuer "$ROLLE")" --out "$OUT" \
|
||||
--judge-endpoint "$LIVE" --judge-model "$JUDGE_MODEL" \
|
||||
--deadline-min "$DEADLINE_MIN" 2>&1 | tail -8
|
||||
kill $SPID 2>/dev/null; wait $SPID 2>/dev/null; trap - RETURN
|
||||
|
||||
# ---- Steckbrief: Kandidat vs. Amtsinhaber
|
||||
local BERICHT="$STATE/ergebnisse/kandidat-${KEY}.md"
|
||||
python3 - "$OUT" "$STATE/baseline.json" "$ROLLE" "$HF_ID" "$WARUM" "$BERICHT" <<'PY'
|
||||
import json, sys, time
|
||||
kand = json.load(open(sys.argv[1]))
|
||||
try:
|
||||
basis = json.load(open(sys.argv[2]))["rollen"].get(sys.argv[3], {})
|
||||
except Exception:
|
||||
basis = {}
|
||||
rolle, hf_id, warum, ziel = sys.argv[3], sys.argv[4], sys.argv[5], sys.argv[6]
|
||||
kz = kand.get("zusammenfassung", {})
|
||||
zeilen = [f"# Prüfstand-Steckbrief: {hf_id}",
|
||||
f"Rolle: **{rolle}** · erhoben {time.strftime('%d.%m.%Y %H:%M')}",
|
||||
f"Anlass: {warum}" if warum else "", "",
|
||||
"| Suite | Kandidat | Amtsinhaber |", "|---|---|---|"]
|
||||
punkte_k = punkte_b = faelle = 0
|
||||
for name in sorted(set(kz) | set(basis)):
|
||||
def fmt(s):
|
||||
if not s: return "—"
|
||||
t = f"{s['bestanden']}/{s['gesamt']}"
|
||||
if s.get("note_mittel"): t += f" · Note {s['note_mittel']}"
|
||||
if s.get("tg_mittel"): t += f" · tg {s['tg_mittel']}"
|
||||
if s.get("pp_mittel"): t += f" · pp {s['pp_mittel']}"
|
||||
return t
|
||||
zeilen.append(f"| {name} | {fmt(kz.get(name))} | {fmt(basis.get(name))} |")
|
||||
if name in kz and name in basis and name != "speed":
|
||||
faelle += 1
|
||||
punkte_k += kz[name]["bestanden"] / max(1, kz[name]["gesamt"])
|
||||
punkte_b += basis[name]["bestanden"] / max(1, basis[name]["gesamt"])
|
||||
tg_k = (kz.get("speed") or {}).get("tg_mittel") or 0
|
||||
tg_b = (basis.get("speed") or {}).get("tg_mittel") or 0
|
||||
zeilen.append("")
|
||||
if faelle:
|
||||
qk, qb = punkte_k / faelle, punkte_b / faelle
|
||||
tempo = (f"Tempo {round(100*tg_k/tg_b-100):+d} % vs. Amtsinhaber" if tg_k and tg_b
|
||||
else "Tempo-Vergleich unvollständig")
|
||||
if qk >= qb and tg_k >= 0.8 * (tg_b or tg_k):
|
||||
urteil = "KANDIDAT SIEHT STARK AUS — Karte prüfen lohnt sich."
|
||||
elif qk >= qb:
|
||||
urteil = "Qualität hält mit, aber deutlich langsamer — vermutlich kein Aufsteiger."
|
||||
else:
|
||||
urteil = "Qualität unter Amtsinhaber — kein Aufsteiger."
|
||||
zeilen.append(f"**Einordnung:** Bestehensquote {qk:.0%} vs. {qb:.0%} · {tempo}. {urteil}")
|
||||
zeilen.append("")
|
||||
zeilen.append("_Der Prüfstand empfiehlt nur — der Rollen-Wechsel bleibt Commander-Entscheid (Karte)._")
|
||||
text = "\n".join(z for z in zeilen if z is not None)
|
||||
open(ziel, "w", encoding="utf-8").write(text + "\n")
|
||||
print(text)
|
||||
PY
|
||||
|
||||
# ---- Vault-Bericht + Karte + stille Chronik, dann Cache aufräumen
|
||||
mkdir -p "$VAULT/pruefstand"
|
||||
cp "$BERICHT" "$VAULT/pruefstand/${KEY}.md" 2>/dev/null || true
|
||||
( cd "$VAULT" 2>/dev/null && git add "pruefstand/${KEY}.md" >/dev/null 2>&1 \
|
||||
&& git commit -q -m "pruefstand: Steckbrief ${KEY}" >/dev/null 2>&1 ) || true
|
||||
"$HERMES" kanban create "Prüfstand-Ergebnis: ${HF_ID} für Rolle ${ROLLE}" \
|
||||
--body "$(cat "$BERICHT")" \
|
||||
--triage --created-by pruefstand --idempotency-key "pruefstand-${KEY}" >/dev/null 2>&1 || true
|
||||
briefkasten "Prüfstand" "Kandidat ${HF_ID} (Rolle ${ROLLE}) geprüft — Steckbrief im Auftrags-Kanban und Vault (pruefstand/${KEY}.md)."
|
||||
mv "$SPEC" "$STATE/done/${KEY}.spec.json" 2>/dev/null || true
|
||||
cp "$OUT" "$STATE/done/${KEY}.json" 2>/dev/null || echo "{\"status\":\"geprueft\"}" > "$STATE/done/${KEY}.json"
|
||||
rm -rf "${CACHE:?}/$KEY"
|
||||
echo
|
||||
echo "(Cache aufgeräumt; Roh-Ergebnisse: $STATE/ergebnisse/kandidat-${KEY}.*)"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------- Einstieg
|
||||
case "${1:-}" in
|
||||
--baseline)
|
||||
baseline_lauf
|
||||
;;
|
||||
--kandidat)
|
||||
[ -n "${2:-}" ] || { echo "Nutzung: pruefstand.sh --kandidat <spec.json>"; exit 1; }
|
||||
kandidat_lauf "$2"
|
||||
;;
|
||||
*)
|
||||
if [ ! -f "$STATE/baseline.json" ]; then
|
||||
baseline_lauf
|
||||
exit 0
|
||||
fi
|
||||
NAECHSTER="$(ls -1tr "$STATE/queue/"*.json 2>/dev/null | head -1 || true)"
|
||||
if [ -n "$NAECHSTER" ]; then
|
||||
kandidat_lauf "$NAECHSTER"
|
||||
fi
|
||||
# keine Queue → LEERER stdout → --no-agent-Cron bleibt still (bewusst)
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"prompt": "Im Arbeitsverzeichnis liegen Notiz-Dateien im Ordner `notizen/`. Sammle ALLE Zeilen, die mit `TODO:` beginnen, und schreibe sie (ohne das `TODO:`-Präfix, eine je Zeile, Reihenfolge egal) in eine neue Datei `offene-punkte.txt` im Wurzelverzeichnis. Rufe danach `fertig` auf.",
|
||||
"max_schritte": 8,
|
||||
"sandkasten": {
|
||||
"notizen/montag.txt": "Besprechung war ok.\nTODO: Backup-Log prüfen\nSonst nichts.\n",
|
||||
"notizen/dienstag.txt": "TODO: Firmware-Update Router\nWetter schlecht.\n",
|
||||
"notizen/mittwoch.txt": "Nichts offen heute.\n"
|
||||
},
|
||||
"checks": [
|
||||
{"datei": "offene-punkte.txt", "regex": "Backup-Log prüfen"},
|
||||
{"datei": "offene-punkte.txt", "regex": "Firmware-Update Router"}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"prompt": "Die Datei `dienst/config.ini` ist kaputt. In `anleitung.txt` steht, welche Werte gelten sollen. Repariere die config.ini entsprechend (nur die falschen Werte ändern, Struktur beibehalten) und rufe danach `fertig` auf.",
|
||||
"max_schritte": 8,
|
||||
"sandkasten": {
|
||||
"anleitung.txt": "Sollwerte für dienst/config.ini:\n- port muss 9001 sein (Zahl, nicht Text!)\n- loglevel muss info sein\n- Der Eintrag host bleibt unverändert.\n",
|
||||
"dienst/config.ini": "[server]\nhost = 127.0.0.1\nport = abc\nloglevel = debug\n"
|
||||
},
|
||||
"checks": [
|
||||
{"datei": "dienst/config.ini", "regex": "^port\\s*=\\s*9001\\s*$"},
|
||||
{"datei": "dienst/config.ini", "regex": "^loglevel\\s*=\\s*info\\s*$"},
|
||||
{"datei": "dienst/config.ini", "regex": "^host\\s*=\\s*127\\.0\\.0\\.1\\s*$"}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"prompt": "Lies die Messwerte in `messwerte.csv` (Spalten: zeit;temperatur_c). Erstelle einen Bericht `bericht.md` mit genau zwei Zeilen: Zeile 1 `Messungen: <Anzahl der Datenzeilen>`, Zeile 2 `Maximum: <höchste Temperatur> °C` (Zahl wie in der Quelldatei geschrieben). Rufe danach `fertig` auf.",
|
||||
"max_schritte": 8,
|
||||
"sandkasten": {
|
||||
"messwerte.csv": "zeit;temperatur_c\n03:00;61,5\n04:00;72,25\n05:00;58,0\n06:00;69,75\n"
|
||||
},
|
||||
"checks": [
|
||||
{"datei": "bericht.md", "regex": "Messungen:\\s*4"},
|
||||
{"datei": "bericht.md", "regex": "Maximum:\\s*72,25\\s*°?C"}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"prompt": "In `regeln.txt` steht, welche Dateien im Arbeitsverzeichnis gelöscht werden dürfen. Lösche GENAU die Dateien, die laut Regeln weg dürfen — nichts anderes. Rufe danach `fertig` auf.",
|
||||
"max_schritte": 10,
|
||||
"sandkasten": {
|
||||
"regeln.txt": "Aufräum-Regeln:\n1. Alle Dateien mit Endung .tmp dürfen gelöscht werden.\n2. Dateien, deren Name mit wichtig beginnt, dürfen NIE gelöscht werden.\n3. Alles andere bleibt liegen.\n",
|
||||
"cache-alt.tmp": "wegwerf-inhalt\n",
|
||||
"sitzung.tmp": "wegwerf-inhalt\n",
|
||||
"wichtig-vertrag.txt": "bleibt!\n",
|
||||
"notiz.md": "bleibt auch.\n"
|
||||
},
|
||||
"checks": [
|
||||
{"nicht_vorhanden": "cache-alt.tmp"},
|
||||
{"nicht_vorhanden": "sitzung.tmp"},
|
||||
{"datei": "wichtig-vertrag.txt", "regex": "bleibt!"},
|
||||
{"datei": "notiz.md", "regex": "bleibt auch"}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"prompt": "Schreibe eine Python-Funktion `summiere_umsaetze(text)` in der Datei `loesung.py`. Eingabe ist ein CSV-Text mit Kopfzeile `kategorie;betrag` — Trennzeichen Semikolon, Beträge mit DEUTSCHEM Dezimalkomma (z. B. `12,50`). Die Funktion gibt ein dict zurück, das je Kategorie die Summe der Beträge als float enthält. Leere Zeilen und Zeilen ohne Semikolon werden ignoriert; unparsebare Beträge lösen einen ValueError mit der Zeilennummer aus.",
|
||||
"antwort_datei": "loesung.py",
|
||||
"timeout_s": 60
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
from loesung import summiere_umsaetze
|
||||
|
||||
|
||||
class Tests(unittest.TestCase):
|
||||
def test_summen(self):
|
||||
text = ("kategorie;betrag\n"
|
||||
"essen;12,50\n"
|
||||
"essen;7,50\n"
|
||||
"\n"
|
||||
"strom;30,00\n")
|
||||
self.assertEqual(summiere_umsaetze(text),
|
||||
{"essen": 20.0, "strom": 30.0})
|
||||
|
||||
def test_ohne_semikolon_ignoriert(self):
|
||||
text = "kategorie;betrag\nnur eine notizzeile\nessen;1,00\n"
|
||||
self.assertEqual(summiere_umsaetze(text), {"essen": 1.0})
|
||||
|
||||
def test_kaputter_betrag(self):
|
||||
with self.assertRaises(ValueError):
|
||||
summiere_umsaetze("kategorie;betrag\nessen;zwoelf\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
r = unittest.main(exit=False).result
|
||||
sys.exit(0 if r.wasSuccessful() else 1)
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"prompt": "In der Datei `zeitfenster.py` steckt mindestens ein Fehler: `im_wartungsfenster(stunde)` soll True liefern, wenn die Stunde im nächtlichen Wartungsfenster von 23 Uhr bis einschließlich 4 Uhr liegt (also 23, 0, 1, 2, 3, 4) — für alle anderen Stunden False. Ungültige Stunden (<0 oder >23) sollen einen ValueError auslösen. Korrigiere die Datei und gib sie VOLLSTÄNDIG zurück, ohne die Funktionssignatur zu ändern.",
|
||||
"antwort_datei": "zeitfenster.py",
|
||||
"dateien": {
|
||||
"zeitfenster.py": "def im_wartungsfenster(stunde):\n \"\"\"True, wenn die Stunde im Wartungsfenster 23-4 Uhr liegt.\"\"\"\n if stunde < 0 or stunde > 24:\n raise ValueError(\"ungueltige Stunde\")\n return 23 <= stunde or stunde < 4\n"
|
||||
},
|
||||
"timeout_s": 60
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
from zeitfenster import im_wartungsfenster
|
||||
|
||||
|
||||
class Tests(unittest.TestCase):
|
||||
def test_im_fenster(self):
|
||||
for h in (23, 0, 1, 2, 3, 4):
|
||||
self.assertTrue(im_wartungsfenster(h), f"Stunde {h}")
|
||||
|
||||
def test_ausserhalb(self):
|
||||
for h in (5, 6, 12, 18, 22):
|
||||
self.assertFalse(im_wartungsfenster(h), f"Stunde {h}")
|
||||
|
||||
def test_ungueltig(self):
|
||||
for h in (-1, 24, 99):
|
||||
with self.assertRaises(ValueError):
|
||||
im_wartungsfenster(h)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
r = unittest.main(exit=False).result
|
||||
sys.exit(0 if r.wasSuccessful() else 1)
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"prompt": "Schreibe in `loesung.py` eine Funktion `fehler_zeilen(log_text)`. Sie bekommt Logtext, in dem relevante Zeilen so aussehen: `2026-07-17T03:15:02 ERROR dienst-name: Meldungstext` (Level kann auch INFO oder WARN sein). Die Funktion gibt eine Liste von Tupeln `(zeitstempel, dienst, meldung)` NUR für ERROR-Zeilen zurück, in Original-Reihenfolge. Zeilen in anderem Format werden still ignoriert. Der Dienstname steht vor dem Doppelpunkt und enthält keine Leerzeichen.",
|
||||
"antwort_datei": "loesung.py",
|
||||
"timeout_s": 60
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
from loesung import fehler_zeilen
|
||||
|
||||
LOG = """2026-07-17T03:15:02 ERROR llama-swap: Modell nicht gefunden
|
||||
2026-07-17T03:15:03 INFO mc2: Start ok
|
||||
kaputte zeile ohne format
|
||||
2026-07-17T03:16:10 WARN voice: Latenz hoch
|
||||
2026-07-17T03:17:00 ERROR mem0-service: Timeout nach 30s
|
||||
"""
|
||||
|
||||
|
||||
class Tests(unittest.TestCase):
|
||||
def test_nur_error(self):
|
||||
erg = fehler_zeilen(LOG)
|
||||
self.assertEqual(len(erg), 2)
|
||||
self.assertEqual(erg[0][0], "2026-07-17T03:15:02")
|
||||
self.assertEqual(erg[0][1], "llama-swap")
|
||||
self.assertEqual(erg[0][2], "Modell nicht gefunden")
|
||||
self.assertEqual(erg[1][1], "mem0-service")
|
||||
|
||||
def test_leer(self):
|
||||
self.assertEqual(fehler_zeilen("nur INFO hier\n"), [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
r = unittest.main(exit=False).result
|
||||
sys.exit(0 if r.wasSuccessful() else 1)
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"prompt": "Schreibe in `loesung.py` eine Klasse `Lager`. Konstruktor ohne Argumente. Methoden: `einlagern(artikel, menge)` (menge > 0, sonst ValueError), `entnehmen(artikel, menge)` (löst ValueError aus, wenn der Bestand nicht reicht oder der Artikel unbekannt ist), `bestand(artikel)` (0 für unbekannte Artikel). Bestände sind ganze Zahlen je Artikelname.",
|
||||
"antwort_datei": "loesung.py",
|
||||
"timeout_s": 60
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
from loesung import Lager
|
||||
|
||||
|
||||
class Tests(unittest.TestCase):
|
||||
def test_ablauf(self):
|
||||
l = Lager()
|
||||
l.einlagern("kabel", 10)
|
||||
l.entnehmen("kabel", 4)
|
||||
self.assertEqual(l.bestand("kabel"), 6)
|
||||
self.assertEqual(l.bestand("unbekannt"), 0)
|
||||
|
||||
def test_unterbestand(self):
|
||||
l = Lager()
|
||||
l.einlagern("kabel", 2)
|
||||
with self.assertRaises(ValueError):
|
||||
l.entnehmen("kabel", 3)
|
||||
with self.assertRaises(ValueError):
|
||||
l.entnehmen("gibtsnicht", 1)
|
||||
|
||||
def test_menge_positiv(self):
|
||||
l = Lager()
|
||||
with self.assertRaises(ValueError):
|
||||
l.einlagern("kabel", 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
r = unittest.main(exit=False).result
|
||||
sys.exit(0 if r.wasSuccessful() else 1)
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"prompt": "Schreibe in `loesung.py` eine Funktion `flach(d, praefix=\"\")`, die ein beliebig tief verschachteltes dict (Werte: dicts oder Skalare) in ein flaches dict verwandelt. Schlüsselpfade werden mit Punkt verbunden, z. B. macht `{\"a\": {\"b\": 1}, \"c\": 2}` daraus `{\"a.b\": 1, \"c\": 2}`. Leere dicts verschwinden ersatzlos. Nicht-dict-Eingaben lösen einen TypeError aus.",
|
||||
"antwort_datei": "loesung.py",
|
||||
"timeout_s": 60
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
from loesung import flach
|
||||
|
||||
|
||||
class Tests(unittest.TestCase):
|
||||
def test_verschachtelt(self):
|
||||
self.assertEqual(flach({"a": {"b": 1, "c": {"d": 2}}, "e": 3}),
|
||||
{"a.b": 1, "a.c.d": 2, "e": 3})
|
||||
|
||||
def test_leere_dicts(self):
|
||||
self.assertEqual(flach({"a": {}, "b": 1}), {"b": 1})
|
||||
|
||||
def test_typfehler(self):
|
||||
with self.assertRaises(TypeError):
|
||||
flach([1, 2])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
r = unittest.main(exit=False).result
|
||||
sys.exit(0 if r.wasSuccessful() else 1)
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"prompt": "Schreibe in `loesung.py` eine Funktion `slug(text)`: wandelt deutschen Text in einen URL-Slug. Regeln: Kleinbuchstaben; ä→ae, ö→oe, ü→ue, ß→ss (auch Großbuchstaben-Varianten); alle anderen Nicht-Alphanumerischen Zeichen werden zu einzelnen Bindestrichen zusammengefasst; keine Bindestriche am Anfang/Ende. Beispiel: `Größte \"Änderung\" 2026!` → `groesste-aenderung-2026`.",
|
||||
"antwort_datei": "loesung.py",
|
||||
"timeout_s": 60
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
from loesung import slug
|
||||
|
||||
|
||||
class Tests(unittest.TestCase):
|
||||
def test_beispiel(self):
|
||||
self.assertEqual(slug('Größte "Änderung" 2026!'),
|
||||
"groesste-aenderung-2026")
|
||||
|
||||
def test_umlaute(self):
|
||||
self.assertEqual(slug("Müßiggänger öfter"), "muessiggaenger-oefter")
|
||||
|
||||
def test_raender(self):
|
||||
self.assertEqual(slug(" --Hallo Welt-- "), "hallo-welt")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
r = unittest.main(exit=False).result
|
||||
sys.exit(0 if r.wasSuccessful() else 1)
|
||||
@@ -0,0 +1,19 @@
|
||||
[
|
||||
{
|
||||
"id": "alltag-erklaerung",
|
||||
"prompt": "Du bist eine deutsche Sprach-Assistentin. Dein Mensch fragt beim Frühstück: »Sag mal, warum wird mein Handy-Akku im Winter eigentlich so schnell leer?« Antworte gesprochen, locker, in 2–3 kurzen Sätzen, Du-Form."
|
||||
},
|
||||
{
|
||||
"id": "schlechte-nachricht",
|
||||
"prompt": "Du bist eine deutsche Sprach-Assistentin. Du musst deinem Menschen sagen, dass das nächtliche Backup fehlgeschlagen ist, aber kein Datenverlust entstanden ist und du es um 9 Uhr erneut versuchst. Sag es gesprochen, ruhig und ohne Technik-Kauderwelsch, in 2–3 Sätzen, Du-Form."
|
||||
},
|
||||
{
|
||||
"id": "terminplanung",
|
||||
"prompt": "Du bist eine deutsche Sprach-Assistentin. Dein Mensch sagt: »Leg mir bitte was mit Zahnarzt nächste Woche Dienstag Nachmittag an, so gegen drei.« Bestätige gesprochen und natürlich in 1–2 Sätzen, Du-Form, und nenne dabei Wochentag und Uhrzeit.",
|
||||
"hinweis_fuer_richter": "Muss Dienstag und 15 Uhr / drei Uhr nachmittags korrekt bestätigen."
|
||||
},
|
||||
{
|
||||
"id": "smalltalk-wetter",
|
||||
"prompt": "Du bist eine deutsche Sprach-Assistentin. Dein Mensch kommt durchnässt rein und sagt nur: »Boah, sag nichts.« Reagiere gesprochen, warm und mit leichtem Humor, in 1–2 Sätzen, Du-Form — ohne aufdringlich zu sein."
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"id": "zeitserver-ausfall",
|
||||
"frage": "Was passiert, wenn der Zeitserver der Werkhalle länger ausfällt, und wie heißt er?",
|
||||
"muss_gruppen": [["chronos-w"], ["Haltemodus"], ["90"]],
|
||||
"zitat_pflicht": true
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"id": "band-schluessel",
|
||||
"frage": "Warum kann ein Dieb mit den Sicherungsbändern aus Nordhof nichts anfangen?",
|
||||
"muss_gruppen": [["AES-256", "AES"], ["Schlüsseltresor", "Verwaltung"], ["verschlüsselt"]],
|
||||
"zitat_pflicht": true
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"id": "pv-anteil",
|
||||
"frage": "Wie viel Strom lieferte die Photovoltaik-Anlage im zweiten Quartal und welchen Anteil am Verbrauch deckte das?",
|
||||
"muss_gruppen": [["96"], ["Viertel", "25", "24", "23"], ["412"]],
|
||||
"zitat_pflicht": true
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"id": "quer-wartungszugriff",
|
||||
"frage": "Ein externer Techniker will nachts um 01:45 Uhr auf eine Maschinensteuerung zugreifen. Über welchen Weg muss er gehen, und warum ist dieser Zeitpunkt zusätzlich ungünstig?",
|
||||
"muss_gruppen": [["falke"], ["Zwei-Faktor", "Zwei Faktor", "2-Faktor", "zweifaktor"], ["01:30", "Vollsicherung", "Sicherung"]],
|
||||
"zitat_pflicht": true
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
# Backup-Konzept Weststadt-Werkhalle (Auszug)
|
||||
|
||||
Gesichert wird dreistufig. Stufe 1: stündliche Schnappschüsse der
|
||||
Leitstand-Datenbank, Aufbewahrung 48 Stunden. Stufe 2: nächtliche
|
||||
Vollsicherung aller Server um 01:30 Uhr auf den Sicherungsknoten arche-3
|
||||
im Keller der Halle. Stufe 3: wöchentliche Auslagerung verschlüsselter
|
||||
Sicherungsbänder in den Standort Nordhof, jeden Donnerstag per Kurier.
|
||||
|
||||
Die Rücksicherung der Leitstand-Datenbank dauert im Normalfall unter
|
||||
20 Minuten. Eine vollständige Wiederherstellung aller Server aus arche-3
|
||||
wurde zuletzt im Frühjahr geprobt und benötigte sechseinhalb Stunden.
|
||||
|
||||
Wichtig: Die Sicherungsbänder für Nordhof werden mit dem Verfahren
|
||||
AES-256 verschlüsselt; die Schlüssel liegen NICHT in der Halle, sondern
|
||||
im Schlüsseltresor der Verwaltung. Ohne Rückholung des Schlüssels aus
|
||||
der Verwaltung ist eine Band-Rücksicherung unmöglich — das ist die
|
||||
bewusste Absicherung gegen Diebstahl der Bänder samt Halle-Infrastruktur.
|
||||
@@ -0,0 +1,16 @@
|
||||
# Energie-Quartalsbericht Weststadt-Werkhalle (Auszug)
|
||||
|
||||
Der Stromverbrauch der Halle lag im zweiten Quartal bei 412 Megawattstunden
|
||||
und damit acht Prozent unter dem Vorjahresquartal. Haupttreiber der
|
||||
Einsparung war die Umrüstung der Hallenbeleuchtung auf gesteuerte
|
||||
LED-Felder, die nur bei Anwesenheit auf voller Stufe laufen.
|
||||
|
||||
Die Photovoltaik-Anlage auf dem Süddach lieferte 96 Megawattstunden und
|
||||
deckte damit knapp ein Viertel des Verbrauchs. An sonnenreichen Wochenenden
|
||||
speist die Halle Überschüsse ins Netz zurück; die Vergütung dafür wird im
|
||||
Jahresabschluss gesondert ausgewiesen.
|
||||
|
||||
Für das dritte Quartal ist die Inbetriebnahme des Batteriespeichers
|
||||
(Kapazität 180 Kilowattstunden) geplant. Er soll Lastspitzen der großen
|
||||
Pressen abfangen, die bisher teure Spitzenlast-Tarife auslösen. Die
|
||||
Wirtschaftlichkeitsrechnung erwartet eine Amortisation nach vier Jahren.
|
||||
@@ -0,0 +1,17 @@
|
||||
# Netzwerk-Handbuch Weststadt-Werkhalle (Auszug)
|
||||
|
||||
Die Werkhalle Weststadt betreibt zwei getrennte Netze: das Betriebsnetz
|
||||
(10.40.0.0/16) für Maschinensteuerungen und das Büronetz (192.168.20.0/24)
|
||||
für Arbeitsplätze. Ein Übergang zwischen beiden Netzen existiert nur über
|
||||
die Koppelstelle KS-2, die jede Verbindung protokolliert.
|
||||
|
||||
Der zentrale Zeitserver der Halle heißt chronos-w und steht im Betriebsnetz.
|
||||
Alle Steuerungen gleichen ihre Uhren viertelstündlich mit chronos-w ab.
|
||||
Fällt der Zeitserver länger als 90 Minuten aus, wechseln die Steuerungen in
|
||||
den Haltemodus und müssen einzeln von Hand quittiert werden.
|
||||
|
||||
Für Wartungszugriffe von außen gilt: Zugang ausschließlich über das
|
||||
Sprungsystem falke, das eine Zwei-Faktor-Anmeldung verlangt. Direkte
|
||||
Zugriffe aus dem Büronetz auf Steuerungen sind technisch gesperrt.
|
||||
Die Sperrliste pflegt das Team Leitstand, Änderungen brauchen zwei
|
||||
Freigaben und werden erst nach dem Wochenwechsel wirksam.
|
||||
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env python3
|
||||
# Erzeugt das Prüfstand-Testbild deterministisch OHNE Zusatz-Pakete (reines
|
||||
# zlib/struct-PNG): weißer Grund 256x256, rotes Rechteck links oben,
|
||||
# blaues Rechteck rechts unten. Die Vision-Suite prüft genau diese Aussagen.
|
||||
import struct
|
||||
import sys
|
||||
import zlib
|
||||
|
||||
B, H = 256, 256
|
||||
|
||||
|
||||
def pixel(x, y):
|
||||
if 24 <= x < 104 and 24 <= y < 104:
|
||||
return (220, 30, 30) # rotes Rechteck links oben
|
||||
if 152 <= x < 232 and 152 <= y < 232:
|
||||
return (30, 60, 220) # blaues Rechteck rechts unten
|
||||
return (255, 255, 255)
|
||||
|
||||
|
||||
def chunk(typ, daten):
|
||||
c = typ + daten
|
||||
return struct.pack(">I", len(daten)) + c + struct.pack(">I", zlib.crc32(c))
|
||||
|
||||
|
||||
roh = b""
|
||||
for y in range(H):
|
||||
roh += b"\x00" + b"".join(bytes(pixel(x, y)) for x in range(B))
|
||||
|
||||
png = (b"\x89PNG\r\n\x1a\n"
|
||||
+ chunk(b"IHDR", struct.pack(">IIBBBBB", B, H, 8, 2, 0, 0, 0))
|
||||
+ chunk(b"IDAT", zlib.compress(roh, 9))
|
||||
+ chunk(b"IEND", b""))
|
||||
|
||||
ziel = sys.argv[1] if len(sys.argv) > 1 else "testbild.png"
|
||||
with open(ziel, "wb") as f:
|
||||
f.write(png)
|
||||
print(f"Testbild geschrieben: {ziel}")
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"prompt": "Beschreibe kurz auf Deutsch, was auf diesem Bild zu sehen ist: welche Formen, welche Farben, und wo sie ungefähr liegen.",
|
||||
"muss_gruppen": [["rot"], ["blau"], ["links oben", "oben links", "obere linke", "links-oben"], ["rechts unten", "unten rechts", "untere rechte", "rechts-unten"]]
|
||||
}
|
||||
@@ -0,0 +1,419 @@
|
||||
#!/usr/bin/env bash
|
||||
# Trend-Radar (wöchentlich Sa 05:15, 17.07.2026): hält das MODELL- UND ENGINE-GEFÜGE
|
||||
# der Box aktuell im Blick — für ALLE Rollen (hermes/coder/heavy/vision/scout/embed),
|
||||
# nicht nur das Hirn. Vier Schritte, alles deterministisch im Skript, der Agent ist
|
||||
# nur der Bote:
|
||||
# 1. LIVE-PULS: echte Tempo-Messung jeder Rolle über llama-swap (:8080), Vergleich
|
||||
# zur Vorwoche (State), Regressionen >10 % werden gemeldet.
|
||||
# 2. ENGINE-A/B: gibt es einen neueren llama.cpp-Vulkan-Build, wird er nach /tmp
|
||||
# geladen und auf dem Bench-Port GEGEN den installierten Build gemessen —
|
||||
# installiert wird weiterhin NICHTS (Update bleibt Button/Entscheid).
|
||||
# 3. WATCH-LISTE: mechanisch prüfbare Bedingungen (Issue zu? neuer ROCm-Release?
|
||||
# Community-Grid-Zahlen) aus deploy/trend-radar-watchlist.json.
|
||||
# 4. KANDIDATEN-SUCHE: HuggingFace-Trending (GGUF) je Rolle; ein Analyst mit
|
||||
# Zitat-Gate destilliert MAX. 1 Prüfstand-Kandidaten pro Woche → Spec in die
|
||||
# Prüfstand-Queue (So 01:00 prüft ihn mit dem vollen Programm).
|
||||
# Der Radar EMPFIEHLT NUR — Rollen-Wechsel, Engine-Updates und Adoptionen entscheidet
|
||||
# der Commander (Karten-Klick). Muster wie idle-radar/release-radar: Temp-Dateien statt
|
||||
# Pipe+Heredoc (Lehre 10.07.), Ein-Schuss-Analyst gegen :8080, stiller Briefkasten.
|
||||
set -uo pipefail
|
||||
|
||||
MC="$HOME/mission-control-v2"
|
||||
STATE_DIR="$HOME/.hermes/state"
|
||||
STATE="$STATE_DIR/trend-radar-state.json"
|
||||
PS_QUEUE="$STATE_DIR/pruefstand/queue"
|
||||
PS_DONE="$STATE_DIR/pruefstand/done"
|
||||
WATCHLIST="$MC/deploy/trend-radar-watchlist.json"
|
||||
ENDPOINT="${RADAR_ENDPOINT:-http://127.0.0.1:8080/v1}"
|
||||
ANALYST="${RADAR_MODEL:-gpt-oss-120b}"
|
||||
VERTRETUNG="${RADAR_FALLBACK:-GLM-4.7-Flash}"
|
||||
BRAIN_GGUF="${BRAIN_GGUF:-/srv/models/Qwen3.6-35B-A3B-MTP-GGUF/Qwen3.6-35B-A3B-UD-Q4_K_M.gguf}"
|
||||
BENCH_PORT="${BENCH_PORT:-5899}"
|
||||
VULKAN_DIR=/opt/llamacpp-vulkan
|
||||
|
||||
mkdir -p "$STATE_DIR" "$PS_QUEUE" "$PS_DONE"
|
||||
[ -f "$STATE" ] || echo '{}' > "$STATE"
|
||||
TMPD="$(mktemp -d /tmp/trend-radar.XXXXXX)"
|
||||
trap 'rm -rf "$TMPD"' EXIT
|
||||
|
||||
briefkasten() { # $1=Betreff $2=Text
|
||||
curl -sf -m 5 -X POST "${MC_ANNOUNCE_URL:-http://127.0.0.1:9001/api/voice/announce}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data "$(python3 - "$1" "$2" <<'PY'
|
||||
import json, sys
|
||||
print(json.dumps({"text": sys.argv[2], "subject": sys.argv[1],
|
||||
"source": "trend-radar", "priority": "silent"}))
|
||||
PY
|
||||
)" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
# ---------- Versionierter Auftrag für den Boten-Agenten ----------
|
||||
cat "$MC/deploy/trend-radar-prompt.md" 2>/dev/null || cat <<'EOF'
|
||||
(Prompt-Datei fehlt noch — Kurzform:) Du bist der Bote des wöchentlichen Trend-Radars.
|
||||
Berichte den Befund unten in Lucys Stimme in höchstens 6 Sätzen: Puls-Auffälligkeiten,
|
||||
Engine-A/B-Ergebnis, Watch-Treffer, ob ein Prüfstand-Kandidat eingereiht wurde.
|
||||
Nichts selbst umsetzen — das Skript hat alles Nötige bereits getan.
|
||||
EOF
|
||||
echo
|
||||
echo "## TREND-RADAR-BEFUND (erhoben am $(date '+%d.%m.%Y %H:%M'))"
|
||||
echo
|
||||
|
||||
# =====================================================================
|
||||
# 1. LIVE-PULS aller Rollen (heavy zuletzt → Analyst findet es noch warm)
|
||||
# =====================================================================
|
||||
echo "### 1. LIVE-PULS (echte Messung über llama-swap, Vergleich zur Vorwoche)"
|
||||
python3 - "$ENDPOINT" "$STATE" "$TMPD/puls.json" <<'PY'
|
||||
import json, sys, time, urllib.request
|
||||
|
||||
endpoint, state_pfad, out_pfad = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||
ROLLEN = ["hermes", "coder", "vision", "scout", "heavy"] # heavy zuletzt (Analyst-Wärme)
|
||||
|
||||
def chat(model, timeout=420):
|
||||
body = {"model": model, "max_tokens": 160, "temperature": 0, "messages": [
|
||||
{"role": "user", "content":
|
||||
"Erkläre in drei kurzen Sätzen, was ein Mixture-of-Experts-Modell ist."}]}
|
||||
req = urllib.request.Request(endpoint.rstrip("/") + "/chat/completions",
|
||||
data=json.dumps(body).encode(), headers={"Content-Type": "application/json"})
|
||||
with urllib.request.urlopen(req, timeout=timeout) as r:
|
||||
return json.load(r).get("timings") or {}
|
||||
|
||||
puls = {}
|
||||
for rolle in ROLLEN:
|
||||
try:
|
||||
chat(rolle) # Lauf 1: lädt/wärmt (on-demand-Modelle brauchen Minuten)
|
||||
t = chat(rolle) # Lauf 2: die eigentliche Messung
|
||||
puls[rolle] = {"tg": round(t.get("predicted_per_second") or 0, 1),
|
||||
"pp": round(t.get("prompt_per_second") or 0, 1)}
|
||||
except Exception as e:
|
||||
puls[rolle] = {"fehler": f"{type(e).__name__}"}
|
||||
# embed: Funktions- und Latenz-Probe
|
||||
try:
|
||||
t0 = time.time()
|
||||
req = urllib.request.Request(endpoint.rstrip("/") + "/embeddings",
|
||||
data=json.dumps({"model": "embed", "input": "Puls-Probe"}).encode(),
|
||||
headers={"Content-Type": "application/json"})
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
json.load(r)
|
||||
puls["embed"] = {"ms": round((time.time() - t0) * 1000)}
|
||||
except Exception as e:
|
||||
puls["embed"] = {"fehler": f"{type(e).__name__}"}
|
||||
|
||||
try:
|
||||
alt = json.load(open(state_pfad)).get("puls") or {}
|
||||
except Exception:
|
||||
alt = {}
|
||||
for rolle, w in puls.items():
|
||||
zeile = f"- {rolle}: "
|
||||
if "fehler" in w:
|
||||
zeile += f"MESSUNG FEHLGESCHLAGEN ({w['fehler']})"
|
||||
elif "ms" in w:
|
||||
zeile += f"{w['ms']} ms"
|
||||
else:
|
||||
zeile += f"tg {w['tg']} t/s · pp {w['pp']} t/s"
|
||||
a = alt.get(rolle) or {}
|
||||
if a.get("tg") and w.get("tg"):
|
||||
d = 100 * w["tg"] / a["tg"] - 100
|
||||
zeile += f" · Vorwoche {a['tg']} ({d:+.0f} %)"
|
||||
if d <= -10:
|
||||
zeile += " ⚠️ REGRESSION"
|
||||
print(zeile)
|
||||
json.dump(puls, open(out_pfad, "w"))
|
||||
PY
|
||||
PULS_BLOCK="$(cat "$TMPD/puls.json" 2>/dev/null || echo '{}')"
|
||||
echo
|
||||
|
||||
# =====================================================================
|
||||
# 2. ENGINE-A/B (nur wenn ein neuerer Vulkan-Build existiert)
|
||||
# =====================================================================
|
||||
echo "### 2. ENGINE-A/B (neuer llama.cpp-Build gegen installierten, Hirn-GGUF, Bench-Port)"
|
||||
ENGINE_BLOCK="kein Vergleich gelaufen"
|
||||
INST_NUM="$(LD_LIBRARY_PATH=$VULKAN_DIR $VULKAN_DIR/llama-server --version 2>&1 \
|
||||
| grep -o 'version: [0-9]*' | grep -o '[0-9]*' || true)"
|
||||
ASSET_RX='ubuntu-vulkan-x64\.tar\.gz$'
|
||||
REL_JSON="$(curl -sf --max-time 30 'https://api.github.com/repos/ggml-org/llama.cpp/releases?per_page=15' || true)"
|
||||
NEU_TAG=""; NEU_URL=""
|
||||
if [ -n "$REL_JSON" ]; then
|
||||
NEU_URL="$(printf '%s' "$REL_JSON" | jq -r --arg rx "$ASSET_RX" \
|
||||
'[.[] | .assets[]? | select(.name|test($rx))][0].browser_download_url // empty' 2>/dev/null || true)"
|
||||
NEU_TAG="$(printf '%s' "$REL_JSON" | jq -r --arg rx "$ASSET_RX" \
|
||||
'[.[] | select(any(.assets[]?; .name|test($rx)))][0].tag_name // empty' 2>/dev/null || true)"
|
||||
fi
|
||||
NEU_NUM="$(printf '%s' "$NEU_TAG" | grep -o '[0-9]*' | head -1 || true)"
|
||||
MEM_FREI_GB="$(awk '/MemAvailable/{printf "%d", $2/1048576}' /proc/meminfo)"
|
||||
|
||||
if [ -z "$INST_NUM" ] || [ -z "$NEU_NUM" ]; then
|
||||
ENGINE_BLOCK="Versionsermittlung unvollständig (installiert: ${INST_NUM:-?}, GitHub: ${NEU_NUM:-?}) — ehrlich: kein Vergleich."
|
||||
elif [ "$NEU_NUM" -le "$INST_NUM" ]; then
|
||||
ENGINE_BLOCK="installiert b${INST_NUM} = aktuellster Build mit Vulkan-Paket (b${NEU_NUM}) — nichts zu messen."
|
||||
elif [ "${MEM_FREI_GB:-0}" -lt 30 ]; then
|
||||
ENGINE_BLOCK="neuer Build b${NEU_NUM} verfügbar, aber nur ${MEM_FREI_GB} GB RAM frei — A/B verschoben."
|
||||
elif [ ! -f "$BRAIN_GGUF" ]; then
|
||||
ENGINE_BLOCK="neuer Build b${NEU_NUM} verfügbar, aber Hirn-GGUF nicht unter ${BRAIN_GGUF} — Pfad prüfen (BRAIN_GGUF)."
|
||||
else
|
||||
mkdir -p "$TMPD/engine"
|
||||
if curl -fsSL --max-time 600 -o "$TMPD/engine/neu.tgz" "$NEU_URL" \
|
||||
&& tar xzf "$TMPD/engine/neu.tgz" -C "$TMPD/engine"; then
|
||||
NEU_DIR="$(echo "$TMPD"/engine/llama-*/ | awk '{print $1}')"
|
||||
ab_bench() { # $1=bin-dir → mittlere tg (2 Messläufe nach Warmlauf) oder "crash"
|
||||
local BD="${1%/}"
|
||||
LD_LIBRARY_PATH="$BD" "$BD/llama-server" -m "$BRAIN_GGUF" --host 127.0.0.1 \
|
||||
--port "$BENCH_PORT" -c 8192 -ngl 999 -fa on --no-mmap --jinja \
|
||||
--spec-type draft-mtp --spec-draft-n-max 3 >/tmp/trend-radar-bench.log 2>&1 &
|
||||
local PID=$!
|
||||
local OK=0
|
||||
for i in $(seq 1 120); do
|
||||
curl -s --max-time 2 "http://127.0.0.1:$BENCH_PORT/health" | grep -q ok && { OK=1; break; }
|
||||
sleep 2
|
||||
kill -0 $PID 2>/dev/null || break
|
||||
done
|
||||
if [ "$OK" != "1" ]; then echo "crash"; kill $PID 2>/dev/null; wait $PID 2>/dev/null; return; fi
|
||||
local SUMME=0 N=0
|
||||
for r in 0 1 2; do
|
||||
curl -s --max-time 300 -X POST "http://127.0.0.1:$BENCH_PORT/completion" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"prompt":"Erklaere in drei kurzen Saetzen, was ein Mixture-of-Experts-Modell ist.","n_predict":150,"temperature":0}' \
|
||||
> "$TMPD/engine/probe.json" 2>/dev/null
|
||||
[ "$r" = "0" ] && continue
|
||||
local TG
|
||||
TG="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get("timings",{}).get("predicted_per_second") or 0)' "$TMPD/engine/probe.json" 2>/dev/null || echo 0)"
|
||||
SUMME="$(python3 -c "print($SUMME + $TG)")"; N=$((N+1))
|
||||
done
|
||||
kill $PID 2>/dev/null; wait $PID 2>/dev/null; sleep 2
|
||||
[ "$N" -gt 0 ] && python3 -c "print(round($SUMME/$N,1))" || echo 0
|
||||
}
|
||||
TG_ALT="$(ab_bench "$VULKAN_DIR")"
|
||||
TG_NEU="$(ab_bench "$NEU_DIR")"
|
||||
if [ "$TG_NEU" = "crash" ]; then
|
||||
ENGINE_BLOCK="Kandidat b${NEU_NUM} STARTET NICHT auf dem Hirn-GGUF (Lade-Crash) — Update wäre riskant, Finger weg bis zum nächsten Build."
|
||||
elif [ "$TG_ALT" = "crash" ] || [ "${TG_ALT%.*}" = "0" ]; then
|
||||
ENGINE_BLOCK="Referenzmessung des installierten Builds fehlgeschlagen — kein belastbares A/B."
|
||||
else
|
||||
DELTA="$(python3 -c "print(round(100*$TG_NEU/$TG_ALT-100,1))" 2>/dev/null || echo '?')"
|
||||
ENGINE_BLOCK="b${NEU_NUM} (neu) tg ${TG_NEU} t/s vs. b${INST_NUM} (installiert) ${TG_ALT} t/s → ${DELTA} %. Anwendung übernimmt das So-04:30-Auto-Update (Rollback-Fangnetz) bzw. der Cockpit-Knopf — bei klar NEGATIVEM Delta vorher pinnen erwägen."
|
||||
fi
|
||||
else
|
||||
ENGINE_BLOCK="Download/Entpacken von b${NEU_NUM} fehlgeschlagen — nächste Woche neuer Versuch."
|
||||
fi
|
||||
fi
|
||||
echo "$ENGINE_BLOCK"
|
||||
echo
|
||||
|
||||
# =====================================================================
|
||||
# 3. WATCH-LISTE (mechanische Bedingungen)
|
||||
# =====================================================================
|
||||
echo "### 3. WATCH-LISTE (mechanisch geprüft)"
|
||||
python3 - "$WATCHLIST" "$STATE" "$TMPD/watch.txt" <<'PY'
|
||||
import json, sys, urllib.request
|
||||
|
||||
def hole(url, timeout=30):
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "mc2-trend-radar"})
|
||||
with urllib.request.urlopen(req, timeout=timeout) as r:
|
||||
return r.read().decode("utf-8", "replace")
|
||||
|
||||
try:
|
||||
eintraege = json.load(open(sys.argv[1], encoding="utf-8")).get("eintraege", [])
|
||||
except Exception as e:
|
||||
print(f"(Watch-Liste nicht lesbar: {e})")
|
||||
open(sys.argv[3], "w").write("")
|
||||
raise SystemExit
|
||||
try:
|
||||
state = json.load(open(sys.argv[2]))
|
||||
except Exception:
|
||||
state = {}
|
||||
gesehen = state.get("watch_gesehen") or {}
|
||||
zeilen = []
|
||||
for e in eintraege:
|
||||
key, typ = e.get("key", "?"), e.get("typ")
|
||||
try:
|
||||
if typ == "github_issue":
|
||||
d = json.loads(hole(f"https://api.github.com/repos/{e['repo']}/issues/{e['nummer']}"))
|
||||
status = d.get("state", "?")
|
||||
treffer = " ⚠️ BEDINGUNG ERFÜLLT!" if status == "closed" else ""
|
||||
zeilen.append(f"- {key}: Issue #{e['nummer']} ist {status}{treffer} ({e['warum']})")
|
||||
elif typ == "github_release":
|
||||
d = json.loads(hole(f"https://api.github.com/repos/{e['repo']}/releases?per_page=1"))
|
||||
tag = (d[0].get("tag_name") if isinstance(d, list) and d else "?") or "?"
|
||||
neu = " ⚠️ NEU seit letzter Woche!" if tag != gesehen.get(key) else ""
|
||||
zeilen.append(f"- {key}: neuester Release {tag}{neu} ({e['warum']})")
|
||||
gesehen[key] = tag
|
||||
elif typ == "url_zeilen":
|
||||
text = hole(e["url"], timeout=45)
|
||||
passend = [z.strip() for z in text.splitlines() if e.get("muster", "") in z]
|
||||
passend = passend[: int(e.get("max_zeilen", 20))]
|
||||
zeilen.append(f"- {key}: {len(passend)} Messwert-Zeilen ({e['warum']}):")
|
||||
zeilen += [f" {z[:180]}" for z in passend]
|
||||
else:
|
||||
zeilen.append(f"- {key}: unbekannter Typ {typ}")
|
||||
except Exception as ex:
|
||||
zeilen.append(f"- {key}: PRÜFUNG AUSGEFALLEN ({type(ex).__name__})")
|
||||
state["watch_gesehen"] = gesehen
|
||||
json.dump(state, open(sys.argv[2], "w"))
|
||||
ausgabe = "\n".join(zeilen) or "(Watch-Liste leer)"
|
||||
print(ausgabe)
|
||||
open(sys.argv[3], "w", encoding="utf-8").write(ausgabe)
|
||||
PY
|
||||
echo
|
||||
|
||||
# =====================================================================
|
||||
# 4. KANDIDATEN-SUCHE (HF-Trending → Analyst mit Zitat-Gate → Prüfstand-Queue)
|
||||
# =====================================================================
|
||||
echo "### 4. KANDIDATEN-SUCHE (HuggingFace-Trending, GGUF)"
|
||||
curl -sf --max-time 45 \
|
||||
'https://huggingface.co/api/models?filter=gguf&sort=trendingScore&direction=-1&limit=40' \
|
||||
-o "$TMPD/hf.json" || echo '[]' > "$TMPD/hf.json"
|
||||
python3 - "$TMPD/hf.json" "$TMPD/hf.txt" <<'PY'
|
||||
import json, sys
|
||||
try:
|
||||
modelle = json.load(open(sys.argv[1]))
|
||||
except Exception:
|
||||
modelle = []
|
||||
zeilen = []
|
||||
for m in modelle if isinstance(modelle, list) else []:
|
||||
mid = m.get("modelId") or m.get("id") or "?"
|
||||
zeilen.append(f"- {mid} · downloads {m.get('downloads', 0)} · likes {m.get('likes', 0)}"
|
||||
f" · angelegt {(m.get('createdAt') or '')[:10]}")
|
||||
ausgabe = "\n".join(zeilen[:40]) or "(HF-API nicht erreichbar oder leer)"
|
||||
print(ausgabe)
|
||||
open(sys.argv[2], "w", encoding="utf-8").write(ausgabe)
|
||||
PY
|
||||
echo
|
||||
|
||||
# Schon bekannt (Dedup): gemeldete Keys + Prüfstand-Queue + erledigte Prüfungen
|
||||
BEKANNT="$(python3 - "$STATE" "$PS_QUEUE" "$PS_DONE" <<'PY'
|
||||
import glob, json, os, sys
|
||||
try:
|
||||
state = json.load(open(sys.argv[1]))
|
||||
except Exception:
|
||||
state = {}
|
||||
zeilen = [f"- {k}" for k in (state.get("kandidaten_gemeldet") or [])]
|
||||
for d in (sys.argv[2], sys.argv[3]):
|
||||
for f in glob.glob(os.path.join(d, "*.json")):
|
||||
zeilen.append(f"- {os.path.splitext(os.path.basename(f))[0]}")
|
||||
print("\n".join(sorted(set(zeilen))) or "(noch keine)")
|
||||
PY
|
||||
)"
|
||||
|
||||
RASTER='Du bist der TREND-RADAR einer lokalen KI-Box (AMD Strix Halo, 128 GB Unified RAM, ~256 GB/s Speicherbandbreite, llama.cpp/Vulkan). Du bekommst: LIVE-PULS aller Modell-Rollen, ein Engine-A/B-Ergebnis, WATCH-LISTEN-Befunde und die HuggingFace-Trending-Liste (GGUF). ROLLEN-STECKBRIEF (Amtsinhaber und harte Anforderungen): hermes = Lucys Sprach-Hirn, Qwen3.6-35B-A3B (MoE, 3B aktiv) — Kandidaten MÜSSEN MoE mit wenigen aktiven Parametern sein, Latenz ist heilig, dichte Modelle sind NIE Hirn-Kandidaten (Bandbreiten-Physik, Verdikt 17.07.); coder = Qwen3-Coder-Next, braucht 128k+ Kontext; heavy = gpt-oss-120b (Denker, MoE); vision = Qwen3-VL-30B-A3B (braucht mmproj); scout = GLM-4.6V-Flash (klein, Vision+Tools); embed = Qwen3-Embedding-0.6B. Deine Aufgabe: destilliere AUS DEM MATERIAL (a) maximal EINEN Prüfstand-Kandidaten (nur wenn ein Trending-Modell eine Rolle PLAUSIBEL verbessern könnte und aufs RAM-Budget passt) und (b) 0-2 HINWEISE (z. B. erfüllte Watch-Bedingung, Engine-Regression, neues Beschleuniger-Verfahren). REGELN: Die hf_id des Kandidaten MUSS WOERTLICH in der HF-Liste stehen (wird mechanisch geprüft, erfundene IDs fliegen raus). Nichts vorschlagen, was unter SCHON BEKANNT steht. Reine Namens-Hypes ohne erkennbare Rollen-Passung sind KEIN Kandidat. KANDIDAT: 0 ist der Normalfall und völlig ok. Antworte auf DEUTSCH, exakt so: erste Zeile "KANDIDAT: <0|1>". Bei 1 folgen vier Zeilen: "HF_ID: <exakte id aus der Liste>" / "ROLLE: <hermes|coder|heavy|vision|scout|embed>" / "BELEG: <die HF-Listenzeile wörtlich>" / "WARUM: <ein Satz mit der erwarteten Verbesserung>". Danach optional: "HINWEISE:" gefolgt von maximal 2 Spiegelstrichen mit je einem Satz + wörtlichem Beleg-Zitat aus dem Material.'
|
||||
|
||||
EVID="$(cat <<EOF
|
||||
LIVE-PULS (JSON): $PULS_BLOCK
|
||||
|
||||
ENGINE-A/B: $ENGINE_BLOCK
|
||||
|
||||
WATCH-LISTE:
|
||||
$(cat "$TMPD/watch.txt" 2>/dev/null || echo '(leer)')
|
||||
|
||||
HF-TRENDING (GGUF, Top 40):
|
||||
$(cat "$TMPD/hf.txt" 2>/dev/null || echo '(leer)')
|
||||
|
||||
SCHON BEKANNT (nicht erneut vorschlagen):
|
||||
$BEKANNT
|
||||
EOF
|
||||
)"
|
||||
|
||||
judge() { # $1=Modell $2=Timeout $3=max_tokens
|
||||
local req
|
||||
req="$(jq -n --arg m "$1" --arg sys "$RASTER" --arg usr "$EVID" --argjson mt "$3" \
|
||||
'{model:$m, messages:[{role:"system",content:$sys},{role:"user",content:$usr}],
|
||||
max_tokens:$mt, temperature:0.2}')"
|
||||
curl -s -m "$2" "$ENDPOINT/chat/completions" -H 'Content-Type: application/json' \
|
||||
--data-binary "$req" | python3 -c '
|
||||
import json, sys
|
||||
try:
|
||||
d = json.load(sys.stdin)
|
||||
msg = d["choices"][0]["message"]
|
||||
print((msg.get("content") or msg.get("reasoning_content") or "").strip())
|
||||
except Exception:
|
||||
pass'
|
||||
}
|
||||
|
||||
RICHTER="$ANALYST"
|
||||
ANALYSE="$(judge "$ANALYST" 420 2600)"
|
||||
if [ -z "${ANALYSE// /}" ]; then
|
||||
RICHTER="$VERTRETUNG (Vertretung — $ANALYST hat nicht geantwortet)"
|
||||
ANALYSE="$(judge "$VERTRETUNG" 240 1800)"
|
||||
fi
|
||||
|
||||
echo "### 5. ANALYSE (Analyst: $RICHTER)"
|
||||
if [ -z "${ANALYSE// /}" ]; then
|
||||
echo "AUSGEFALLEN — beide Analysten haben nicht geantwortet. Puls/Engine/Watch oben gelten trotzdem; dem Commander EINEN ehrlichen Satz melden."
|
||||
briefkasten "Trend-Radar" "Wochenlauf: Puls/Engine/Watch erhoben, Kandidaten-Analyse ausgefallen (kein Analyst erreichbar)."
|
||||
exit 0
|
||||
fi
|
||||
echo "$ANALYSE"
|
||||
echo
|
||||
|
||||
# ---------- Zitat-Gate + Spec in die Prüfstand-Queue (deterministisch) ----------
|
||||
printf '%s' "$ANALYSE" > "$TMPD/analyse.txt"
|
||||
printf '%s' "$EVID" > "$TMPD/material.txt"
|
||||
ERGEBNIS="$(python3 - "$TMPD/analyse.txt" "$TMPD/material.txt" "$STATE" "$PS_QUEUE" <<'PY'
|
||||
import json, os, re, sys, time, unicodedata
|
||||
|
||||
ana = open(sys.argv[1], encoding="utf-8", errors="replace").read()
|
||||
evid = open(sys.argv[2], encoding="utf-8", errors="replace").read()
|
||||
state_pfad, queue = sys.argv[3], sys.argv[4]
|
||||
|
||||
def norm(s):
|
||||
s = unicodedata.normalize("NFKC", s or "")
|
||||
s = "".join(ch for ch in s if ch.isalnum() or ch.isspace())
|
||||
return " ".join(s.lower().split())
|
||||
|
||||
def feld(name):
|
||||
m = re.search(rf"^{name}\s*:\s*(.+)$", ana, re.MULTILINE)
|
||||
return m.group(1).strip() if m else ""
|
||||
|
||||
anzahl = feld("KANDIDAT")
|
||||
if not anzahl.startswith("1"):
|
||||
print("KEIN_KANDIDAT")
|
||||
raise SystemExit
|
||||
hf_id = feld("HF_ID").strip("`").strip()
|
||||
rolle = feld("ROLLE").lower().strip()
|
||||
beleg = feld("BELEG").strip()
|
||||
warum = feld("WARUM").strip()
|
||||
if rolle not in ("hermes", "coder", "heavy", "vision", "scout", "embed"):
|
||||
print(f"VERWORFEN: unbekannte Rolle {rolle!r}")
|
||||
raise SystemExit
|
||||
if hf_id not in evid:
|
||||
print(f"VERWORFEN: HF_ID {hf_id!r} steht nicht wörtlich im Material (Zitat-Gate)")
|
||||
raise SystemExit
|
||||
if beleg and norm(beleg) not in norm(evid):
|
||||
print("VERWORFEN: BELEG nicht wörtlich im Material (Zitat-Gate)")
|
||||
raise SystemExit
|
||||
key = re.sub(r"[^a-z0-9-]+", "-", hf_id.lower())[:60].strip("-")
|
||||
try:
|
||||
state = json.load(open(state_pfad))
|
||||
except Exception:
|
||||
state = {}
|
||||
gemeldet = state.get("kandidaten_gemeldet") or []
|
||||
if key in gemeldet:
|
||||
print(f"VERWORFEN: {key} schon früher gemeldet")
|
||||
raise SystemExit
|
||||
spec = {"key": key, "rolle": rolle, "hf_id": hf_id, "warum": warum,
|
||||
"beleg": beleg, "erstellt": time.strftime("%Y-%m-%d %H:%M"),
|
||||
"quelle": "trend-radar"}
|
||||
with open(os.path.join(queue, key + ".json"), "w", encoding="utf-8") as f:
|
||||
json.dump(spec, f, ensure_ascii=False, indent=1)
|
||||
state["kandidaten_gemeldet"] = gemeldet + [key]
|
||||
json.dump(state, open(state_pfad, "w"))
|
||||
print(f"EINGEREIHT: {hf_id} → Rolle {rolle} (Prüfstand prüft in der Nacht So 01:00)")
|
||||
PY
|
||||
)"
|
||||
echo "### 6. PRÜFSTAND-QUEUE"
|
||||
echo "$ERGEBNIS"
|
||||
|
||||
# ---------- Puls in den State (für den Vorwochen-Vergleich) ----------
|
||||
python3 - "$STATE" "$TMPD/puls.json" <<'PY'
|
||||
import json, sys
|
||||
try:
|
||||
state = json.load(open(sys.argv[1]))
|
||||
except Exception:
|
||||
state = {}
|
||||
try:
|
||||
state["puls"] = json.load(open(sys.argv[2]))
|
||||
except Exception:
|
||||
pass
|
||||
json.dump(state, open(sys.argv[1], "w"))
|
||||
PY
|
||||
|
||||
briefkasten "Trend-Radar" "Wochenlauf fertig. Engine: ${ENGINE_BLOCK}
|
||||
Queue: ${ERGEBNIS}"
|
||||
@@ -0,0 +1,31 @@
|
||||
# Trend-Radar — Auftrag für den Boten-Agenten
|
||||
|
||||
Du bist der Bote des wöchentlichen Trend-Radars. Unten steht ein automatisch
|
||||
erhobener BEFUND in sechs Abschnitten: Live-Puls aller Modell-Rollen, Engine-A/B,
|
||||
Watch-Liste, HuggingFace-Trending, Analysten-Analyse und Prüfstand-Queue. Das
|
||||
Skript hat alles Nötige BEREITS getan (gemessen, geprüft, ggf. einen Kandidaten
|
||||
für den nächtlichen Prüfstand eingereiht).
|
||||
|
||||
Deine Aufgabe — NUR berichten, in Lucys Stimme (Anrede „Commander",
|
||||
Alltagssprache, Fazit zuerst), in höchstens 6 Sätzen:
|
||||
|
||||
1. Puls: nur Auffälligkeiten nennen (⚠️-Zeilen: Regression oder ausgefallene
|
||||
Messung); wenn alles im Rahmen ist, reicht „Tempo aller Rollen stabil".
|
||||
2. Engine-A/B: Ergebnis in einem Satz (Prozent-Delta oder warum kein Vergleich
|
||||
lief). Das ist die FRÜHWARNUNG vor dem So-04:30-Auto-Update: bei Lade-Crash
|
||||
oder klar negativem Delta dem Commander empfehlen, die Engine vorher zu
|
||||
pinnen (Wartungs-Drawer / mc2-pins.json) — sonst reicht der Messwert.
|
||||
3. Watch-Liste: nur Zeilen mit „⚠️" erwähnen — eine erfüllte Bedingung ist die
|
||||
wichtigste Nachricht des Berichts.
|
||||
4. Kandidat: wenn einer eingereiht wurde, Modell + Rolle nennen und sagen, dass
|
||||
der Prüfstand ihn nachts mit dem vollen Programm prüft und das Ergebnis als
|
||||
Karte kommt; bei „KEIN_KANDIDAT" GENAU EIN kurzer Satz dazu.
|
||||
|
||||
Harte Regeln:
|
||||
- NICHTS selbst umsetzen: keine Updates, keine Rollen-Wechsel, keine Downloads,
|
||||
keine Kanban-Aktionen — alles Entscheidende liegt beim Commander bzw. hat das
|
||||
Skript schon erledigt.
|
||||
- Keine Zahlen erfinden oder runden, die nicht im Befund stehen; im Zitat-Gate
|
||||
VERWORFENE Kandidaten nicht als Erfolg verkaufen.
|
||||
- Die stille Briefkasten-Karte hat das Skript bereits geschrieben — du lieferst
|
||||
nur die kurze Telegram-Meldung.
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"_hinweis": "Watch-Liste des Trend-Radars (Sa 05:15) — mechanisch prüfbare Bedingungen statt Prosa-Erinnerungen. Neue Einträge einfach anhängen; der Radar prüft github_issue (offen/zu), github_release (neuer Tag) und url_zeilen (Zeilen mit Suchmuster aus einer Roh-URL).",
|
||||
"eintraege": [
|
||||
{
|
||||
"key": "mmq-prefill-gfx1151",
|
||||
"typ": "github_issue",
|
||||
"repo": "ggml-org/llama.cpp",
|
||||
"nummer": 21284,
|
||||
"bedingung": "Issue geschlossen = MMQ-Tuning für gfx1151 vermutlich gemerged",
|
||||
"warum": "Getunte MMQ-Defaults brachten +60 % ROCm-Prefill auf 35B-MoE (unser Hirn-Typ) und +20 % auf 122B. Wenn gemerged: ROCm-Langkontext-Bench für coder fällig (Vulkan-Verdikt bleibt bis zur Messung)."
|
||||
},
|
||||
{
|
||||
"key": "rocm-releases",
|
||||
"typ": "github_release",
|
||||
"repo": "ROCm/ROCm",
|
||||
"bedingung": "Neuer ROCm-Release seit letzter Meldung",
|
||||
"warum": "Seit 7.14 ist TheRock der Produktions-Stack und AMD optimiert offiziell für die Halo-Familie — ROCm könnte Vulkan bei der Token-Erzeugung einholen. Bei neuem Release: Community-Grid-Zeilen unten gegenlesen."
|
||||
},
|
||||
{
|
||||
"key": "llamacpp-rocm-diskussion",
|
||||
"typ": "url_zeilen",
|
||||
"url": "https://raw.githubusercontent.com/hogeheer499-commits/strix-halo-guide/main/README.md",
|
||||
"muster": "t/s",
|
||||
"max_zeilen": 30,
|
||||
"bedingung": "Community-Messwerte (Vulkan vs. ROCm, MTP, neue Beschleuniger) auf unserer Hardware",
|
||||
"warum": "Der Strix-Halo-Community-Grid ist der ehrlichste externe Messpunkt. Auf tg-Trendwende Vulkan↔ROCm und neue Spec-Decoding-Verfahren achten."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -36,6 +36,9 @@ höchstens „mach". Dieses Blatt ist NUR für den Fall, dass etwas klemmt.
|
||||
manuell geht's jederzeit über den Wartungs-Drawer)
|
||||
- **Mo 05:15** Release-Radar (Hermes-Neuerungen) · **Monatlich 1., 09:00** Monats-Review
|
||||
(Evolution-Radar + Selbstkritik) auf Telegram
|
||||
- **Sa 05:15** Trend-Radar (misst alle Modelle, vergleicht neue Engine-Builds, sucht
|
||||
bessere Modell-Kandidaten) · **So 01:00** Prüfstand (testet gefundene Kandidaten nachts
|
||||
mit echten Aufgaben durch — das Ergebnis kommt als Karte, DU entscheidest über Wechsel)
|
||||
|
||||
## Pinnwand: eine Ebene ist „GEPINNT" — was heißt das?
|
||||
|
||||
|
||||
@@ -86,6 +86,8 @@ per Mini-Request anwärmen (direkt curlen ist zuverlässiger als warmup.sh).
|
||||
| 03:50 | PBS-Host-Backup der Box (`host/aibox`: mem0+hermes+vault+llamaswap.pxar) |
|
||||
| 04:30 | **Chef-Gutachter** (gpt-oss richtet über die autonome Arbeit → Morgenlage) |
|
||||
| 05:15 Mo | **Release-Radar** (hermes-Releases gegen die Eigenbau-Landkarte; erster Lauf 13.07.) |
|
||||
| 05:15 Sa | **Trend-Radar** (Live-Puls ALLER Modell-Rollen + Engine-A/B gegen neuen llama.cpp-Build + Watch-Liste [ROCm/MMQ/Community-Grid] + HF-Kandidaten-Suche → max. 1 Prüfstand-Kandidat/Woche; Frühwarnung vor dem So-Auto-Update) |
|
||||
| 01:00 So | **Prüfstand** (volles Programm für Modell-Kandidaten: echte Coding-/Agenten-/Recherche-/Deutsch-/Vision-Prüfungen + Tempo auf :5899 gegen die Amtsinhaber-Baseline; Download-Deckel 35 GB, Cache räumt sich selbst, Ergebnis = Karte — Rollen-Wechsel bleibt Commander-Klick) |
|
||||
| 07:00 | QNAP-Volume-Snapshot (7 behalten) |
|
||||
| 07:15 | self-smoke (Gateway/Tools/Voice/:9119; Alarm nur bei Rot) |
|
||||
| 08:00 | Daily-Briefing |
|
||||
|
||||
Reference in New Issue
Block a user