de21e1ac68
Drei Luecken aus der Selbst-Review 17.07.: 1. MTP-Fairness: Kandidaten mit MTP-Kopf (Dateiname oder nextn-Tensoren im GGUF-Kopf) bekommen eine Zusatz-Tempomessung mit draft-mtp; Steckbrief rechnet best-of speed/speed_mtp - das Live-Hirn laeuft schliesslich MIT MTP. 2. Baseline-Refresh: baseline.json traegt jetzt den Engine-Build; weicht die installierte Engine ab (So-Auto-Update), misst der Pruefstand-Cron nur den Tempo-Teil aller Rollen neu (Geister-Zahlen-Schutz, Qualitaet bleibt gueltig). 3. Langzeit-Historie: Puls wird als JSONL angehaengt statt ueberschrieben; Radar vergleicht zusaetzlich gegen ~8 Wochen zurueck und meldet SCHLEICHENDE REGRESSION, die woechentlich unter der 10-%-Schwelle bleibt. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
470 lines
22 KiB
Bash
470 lines
22 KiB
Bash
#!/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"
|
|
HIST="$STATE_DIR/trend-radar-puls-history.jsonl" # Langzeit-Verlauf (Lücke 3, 17.07.)
|
|
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 + Langzeit)"
|
|
python3 - "$ENDPOINT" "$STATE" "$TMPD/puls.json" "$HIST" <<'PY'
|
|
import json, sys, time, urllib.request
|
|
|
|
endpoint, state_pfad, out_pfad = sys.argv[1], sys.argv[2], sys.argv[3]
|
|
hist_pfad = sys.argv[4]
|
|
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)
|
|
|
|
# Langzeit-Vergleich (~8 Wochen): fängt SCHLEICHENDE Regression, die jede Woche
|
|
# einzeln unter der 10-%-Schwelle bleibt (Lücke 3, 17.07.).
|
|
eintraege = []
|
|
try:
|
|
with open(hist_pfad, encoding="utf-8") as f:
|
|
for l in f:
|
|
l = l.strip()
|
|
if l:
|
|
try:
|
|
eintraege.append(json.loads(l))
|
|
except Exception:
|
|
pass
|
|
except FileNotFoundError:
|
|
pass
|
|
grenze = time.time() - 49 * 86400
|
|
alte = [z for z in eintraege if (z.get("ts") or 0) <= grenze]
|
|
ref = alte[-1] if alte else None
|
|
if ref:
|
|
treffer = []
|
|
for rolle, w in puls.items():
|
|
a = (ref.get("puls") or {}).get(rolle) or {}
|
|
if a.get("tg") and w.get("tg"):
|
|
d = 100 * w["tg"] / a["tg"] - 100
|
|
if d <= -10:
|
|
treffer.append(f"- LANGZEIT {rolle}: tg {w['tg']} vs. {a['tg']} "
|
|
f"({ref.get('datum','?')}, Engine b{ref.get('engine','?')}) "
|
|
f"= {d:+.0f} % ⚠️ SCHLEICHENDE REGRESSION")
|
|
if treffer:
|
|
print(f"Langzeit-Vergleich (vs. {ref.get('datum','?')}):")
|
|
for t in treffer:
|
|
print(t)
|
|
else:
|
|
print(f"Langzeit-Vergleich vs. {ref.get('datum','?')}: unauffällig.")
|
|
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). NIE einen Amtsinhaber selbst oder ein Repackaging/eine Quantisierung desselben Modells vorschlagen (gleicher Modellname = KEIN Kandidat; wird mechanisch geblockt). 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
|
|
# Amtsinhaber-Sperre (Testlauf-Befund 17.07.: Radar schlug das eigene Hirn-Repo vor):
|
|
# gleicher Modellname = Repackaging, kein Kandidat.
|
|
AMTSINHABER = ("qwen3.6-35b-a3b", "qwen3-vl-30b", "gpt-oss-120b",
|
|
"qwen3-coder-next", "glm-4.6v-flash", "qwen3-embedding")
|
|
if any(t in hf_id.lower() for t in AMTSINHABER):
|
|
print(f"VERWORFEN: {hf_id} ist ein Amtsinhaber-Repackaging (mechanische Sperre)")
|
|
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 (Vorwoche) + an die Langzeit-Historie anhängen ----------
|
|
python3 - "$STATE" "$TMPD/puls.json" "$HIST" "${INST_NUM:-}" <<'PY'
|
|
import json, sys, time
|
|
try:
|
|
state = json.load(open(sys.argv[1]))
|
|
except Exception:
|
|
state = {}
|
|
try:
|
|
puls = json.load(open(sys.argv[2]))
|
|
except Exception:
|
|
puls = None
|
|
if puls:
|
|
state["puls"] = puls
|
|
with open(sys.argv[3], "a", encoding="utf-8") as f:
|
|
f.write(json.dumps({"ts": int(time.time()),
|
|
"datum": time.strftime("%Y-%m-%d"),
|
|
"engine": sys.argv[4], "puls": puls},
|
|
ensure_ascii=False) + "\n")
|
|
json.dump(state, open(sys.argv[1], "w"))
|
|
PY
|
|
|
|
briefkasten "Trend-Radar" "Wochenlauf fertig. Engine: ${ENGINE_BLOCK}
|
|
Queue: ${ERGEBNIS}"
|