Compare commits
4 Commits
960bda6021
...
abc8a365d2
| Author | SHA1 | Date | |
|---|---|---|---|
| abc8a365d2 | |||
| 1cd492eb33 | |||
| 3abc133118 | |||
| 0354ce999f |
@@ -5,7 +5,7 @@ from fastapi.responses import JSONResponse, StreamingResponse
|
||||
|
||||
from config import LLAMA_SWAP_URL
|
||||
from services.gateway_stream import record_stream_chunk, record_usage
|
||||
from services.router_logic import VISION_CAPABLE, choose_for_lane, has_image
|
||||
from services.router_logic import IMAGE_PART_TYPES, VISION_CAPABLE, choose_for_lane, has_image
|
||||
from services.routing_policy import load_policy
|
||||
|
||||
router = APIRouter(prefix="/v1")
|
||||
@@ -19,6 +19,61 @@ _LANG_DIRECTIVE = os.environ.get(
|
||||
"Quellcode, Bezeichner und Shell-Befehle bleiben unverändert.")
|
||||
|
||||
|
||||
# Bild-Beschreibung für Coder-Ziele: Prompt bewusst auf wörtliche Wiedergabe von
|
||||
# Code/Fehlermeldungen getrimmt — der Coder arbeitet nur mit diesem Text weiter.
|
||||
_BILD_BESCHREIB_PROMPT = os.environ.get(
|
||||
"MC_CODER_IMAGE_PROMPT",
|
||||
"Beschreibe dieses Bild vollstaendig und praezise auf Deutsch: sichtbarer Text, "
|
||||
"Code, Zahlen/Daten, UI-Elemente, Layout, Farben und alles Auffaellige. "
|
||||
"Sichtbaren Code und Fehlermeldungen gib WOERTLICH wieder.")
|
||||
|
||||
|
||||
def _ist_coder_alias(alias: str) -> bool:
|
||||
"""Coder-Ziele (coder, rohe Qwen-Coder-IDs) bekommen Bild-BESCHREIBUNGEN statt der
|
||||
stumpfen Vision-Umleitung — die Code-Frage bleibt beim Spezialisten."""
|
||||
return "coder" in (alias or "").lower()
|
||||
|
||||
|
||||
async def _bilder_fuer_coder_beschreiben(body: dict, client, vision_alias: str) -> bool:
|
||||
"""Ersetzt jeden Bild-Part durch eine Text-Beschreibung vom Vision-Modell.
|
||||
|
||||
Idee aus einem verwaisten, nie verdrahteten Patch in der Hermes-Quelle
|
||||
(gateway/platforms/api_server.py, 07/2026) — bei der Projekt-Review 15.07.
|
||||
regelkonform hierher umgezogen (Archiv: docs/archiv/). True = alle Bilder
|
||||
ersetzt; False = eine Analyse scheiterte, Aufrufer nutzt die normale
|
||||
Vision-Umleitung als Fallback.
|
||||
"""
|
||||
fundstellen: list[tuple[dict, int, dict]] = []
|
||||
for m in body.get("messages") or []:
|
||||
if not isinstance(m, dict) or not isinstance(m.get("content"), list):
|
||||
continue
|
||||
for i, part in enumerate(m["content"]):
|
||||
if isinstance(part, dict) and part.get("type") in IMAGE_PART_TYPES:
|
||||
fundstellen.append((m, i, part))
|
||||
for nr, (msg, idx, part) in enumerate(fundstellen, start=1):
|
||||
frage = {
|
||||
"model": vision_alias,
|
||||
"stream": False,
|
||||
"max_tokens": 700,
|
||||
"messages": [{"role": "user",
|
||||
"content": [part, {"type": "text", "text": _BILD_BESCHREIB_PROMPT}]}],
|
||||
}
|
||||
try:
|
||||
# Grosszuegiger Timeout: VL-30B ist on-demand (Kaltladen ~30 s) + Beschreibung ~25 s.
|
||||
r = await client.post(f"{LLAMA_SWAP_URL}/v1/chat/completions", json=frage, timeout=240.0)
|
||||
text = ""
|
||||
if r.status_code == 200:
|
||||
text = ((r.json().get("choices") or [{}])[0].get("message") or {}).get("content") or ""
|
||||
if not text.strip():
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
msg["content"][idx] = {"type": "text", "text": (
|
||||
f"[Bild {nr}: dem Request lag ein Bild bei — {vision_alias} beschreibt es so:\n"
|
||||
f"{text.strip()}]")}
|
||||
return True
|
||||
|
||||
|
||||
def _inject_language(body: dict, alias: str) -> None:
|
||||
"""Deutsch-Direktive anhängen. An die ERSTE System-Message (viele Chat-Templates
|
||||
erwarten nur eine), sonst als neue System-Message. `hermes` ausgenommen — Lucys
|
||||
@@ -101,11 +156,19 @@ async def _proxy(path: str, request: Request):
|
||||
routed = {"x-mc-routed-to": requested}
|
||||
|
||||
pol = load_policy()
|
||||
client = request.app.state.gw_client # geteilter Keep-Alive-Client (siehe app.py lifespan)
|
||||
# Bild-Weiche (Faden 11): Requests mit Bild-Anhang automatisch ans Vision-Modell umleiten,
|
||||
# sofern das Ziel nicht ohnehin bildfähig ist. Das MTP-Hirn (fast/hermes) kann keine Bilder —
|
||||
# so bleibt Lucy schnell, ohne dass jemand manuell das Modell wechselt (Entscheid Weg A).
|
||||
# Coder-Sonderweg (15.07.): Coder-Ziele behalten den Request — die Bilder werden vorab vom
|
||||
# Vision-Modell BESCHRIEBEN und als Text injiziert (Screenshot-Debugging bleibt beim Coder).
|
||||
vision_alias = pol.get("vision")
|
||||
if vision_alias and alias not in VISION_CAPABLE and has_image(body):
|
||||
if _ist_coder_alias(alias) and await _bilder_fuer_coder_beschreiben(body, client, vision_alias):
|
||||
routed = {"x-mc-routed-to": alias,
|
||||
"x-mc-route-reason": "Bild beschrieben (Vision) -> bleibt beim Coder",
|
||||
"x-mc-lane": routed.get("x-mc-lane", "-")}
|
||||
else:
|
||||
alias = vision_alias
|
||||
body["model"] = alias
|
||||
# HTTP-Header-Werte muessen latin-1 sein — kein '→' o.ae. (sonst 500, 13.07.).
|
||||
@@ -116,8 +179,6 @@ async def _proxy(path: str, request: Request):
|
||||
body["chat_template_kwargs"] = {"enable_thinking": False}
|
||||
_inject_language(body, alias)
|
||||
url = f"{LLAMA_SWAP_URL}{path}"
|
||||
|
||||
client = request.app.state.gw_client # geteilter Keep-Alive-Client (siehe app.py lifespan)
|
||||
if body.get("stream"):
|
||||
req = client.build_request("POST", url, json=body, timeout=None)
|
||||
r = await client.send(req, stream=True)
|
||||
|
||||
@@ -52,7 +52,7 @@ _CODE_HINT = re.compile(
|
||||
|
||||
# Bild-Weiche (Faden 11): Aliase, die selbst Bilder koennen — die werden NIE umgeroutet.
|
||||
VISION_CAPABLE = {"vision", "scout"}
|
||||
_IMAGE_PART_TYPES = {"image_url", "input_image", "image"}
|
||||
IMAGE_PART_TYPES = {"image_url", "input_image", "image"}
|
||||
|
||||
|
||||
def has_image(body: dict) -> bool:
|
||||
@@ -64,7 +64,7 @@ def has_image(body: dict) -> bool:
|
||||
content = m.get("content")
|
||||
if isinstance(content, list):
|
||||
for part in content:
|
||||
if isinstance(part, dict) and part.get("type") in _IMAGE_PART_TYPES:
|
||||
if isinstance(part, dict) and part.get("type") in IMAGE_PART_TYPES:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ hint=""
|
||||
ctx="$(cat <<EOF
|
||||
[BOX-ORIENTIERUNG — du bist ein Hintergrund-Worker auf der lokalen AI-Box (Linux, Zeit UTC / lokal Europe/Berlin, locale de_DE), NICHT Lucy und kein Dev-Laptop. Du arbeitest EINE Kanban-Aufgabe ab.]
|
||||
Die Modelle / das "Hirn" laufen ausschliesslich ueber den Router llama-swap auf 127.0.0.1:8080 (startet je Modell einen llama-server) bzw. das OpenAI-kompatible Gateway von MC2 auf 127.0.0.1:9001/v1 — es gibt KEINEN anderen Modell-Server, also nicht danach suchen. Weitere Dienste: hermes-gateway :8642, mem0-Gedaechtnis :8765. Der MC2-Live-Checkout ~/mission-control-v2 ist nur zum LESEN da (Schreiben/committen TABU — Code-Arbeit immer im frischen Klon deines Task-Workspaces). Grenzen (was gehoert wohin): MC2 = Steuerpult (verwalten/klicken/zusehen, KEIN Chat-UI), Lucy = Chat & Stimme (eigenes Repo), Hermes-Quelle ist TABU (Agenten-Verhalten nur ueber Config/SOUL/Skill/Hook/MCP aendern). Feste Entscheide/Verbote + volle Grenzen-Landkarte: ~/mission-control-v2/docs/wissen/ (GRENZEN/VERDIKTE/FALLEN/STACK) + ~/wissens-vault/eigenbau-landkarte.md.${hint}
|
||||
PROTOKOLL-PFLICHT (15.07.): Beende deine Aufgabe IMMER mit dem Tool-Aufruf kanban_complete (fertig) oder kanban_block (Frage/Sackgasse). Eine Text-Zusammenfassung OHNE diesen Aufruf zaehlt als Absturz (Protokollverstoss) und blockiert die Karte — genau so ging am 14.07. eine fertige Arbeit verloren.
|
||||
EOF
|
||||
)"
|
||||
|
||||
|
||||
@@ -36,13 +36,18 @@ ti = payload.get("tool_input") or {}
|
||||
home = os.path.realpath(os.path.expanduser("~"))
|
||||
LIVE = os.path.join(home, "mission-control-v2")
|
||||
SRC = os.path.join(home, ".hermes", "hermes-agent")
|
||||
# R2 (Review 15.07.): systemd-User-Units. Die Nacht-Werkstatt hat am 14.07. einen Override
|
||||
# DIREKT scharf geschaltet — ging gut, verletzt aber "jede Code-Zeile ist Klick-pflichtig".
|
||||
# Unit-/Override-Dateien werden nur noch ueber eine angenommene Karte installiert.
|
||||
SYSD = os.path.join(home, ".config", "systemd", "user")
|
||||
|
||||
MSG = (
|
||||
"TABU (Faden 8): Schreiben an dieser Stelle ist fuer Worker gesperrt. Der Live-Checkout "
|
||||
"~/mission-control-v2 und die Hermes-Quelle ~/.hermes/hermes-agent duerfen NIE veraendert "
|
||||
"werden. Arbeite im FRISCHEN Klon deines Task-Workspaces; MC2-Aenderungen kommen als "
|
||||
"Vorschlags-Branch (nie am Live-Checkout), Hermes-Verhalten nur ueber Config/SOUL/Skill/"
|
||||
"Hook/MCP. Siehe docs/wissen/GRENZEN.md."
|
||||
"TABU (Faden 8/R2): Schreiben an dieser Stelle ist fuer Worker gesperrt. Der Live-Checkout "
|
||||
"~/mission-control-v2, die Hermes-Quelle ~/.hermes/hermes-agent und die systemd-User-Units "
|
||||
"~/.config/systemd/user duerfen NIE direkt veraendert werden. Arbeite im FRISCHEN Klon "
|
||||
"deines Task-Workspaces; MC2-Aenderungen kommen als Vorschlags-Branch, systemd-Unit-/"
|
||||
"Override-Dateien werden nur ueber eine angenommene Karte installiert, Hermes-Verhalten "
|
||||
"nur ueber Config/SOUL/Skill/Hook/MCP. Siehe docs/wissen/GRENZEN.md."
|
||||
)
|
||||
|
||||
|
||||
@@ -52,7 +57,7 @@ def block():
|
||||
|
||||
def under_tabu(path):
|
||||
rp = os.path.realpath(path)
|
||||
return rp == LIVE or rp == SRC or rp.startswith(LIVE + os.sep) or rp.startswith(SRC + os.sep)
|
||||
return any(rp == t or rp.startswith(t + os.sep) for t in (LIVE, SRC, SYSD))
|
||||
|
||||
|
||||
# --- Datei-Tools: Zielpfad kanonisch pruefen (zuverlaessig) -------------------------
|
||||
@@ -78,6 +83,7 @@ if tool == "terminal":
|
||||
tabu_forms = [
|
||||
LIVE, "~/mission-control-v2", "$HOME/mission-control-v2", "${HOME}/mission-control-v2",
|
||||
SRC, "~/.hermes/hermes-agent", "$HOME/.hermes/hermes-agent", "${HOME}/.hermes/hermes-agent",
|
||||
SYSD, "~/.config/systemd/user", "$HOME/.config/systemd/user", "${HOME}/.config/systemd/user",
|
||||
]
|
||||
tabu_alt = "(?:" + "|".join(re.escape(t) for t in tabu_forms) + ")"
|
||||
if not re.search(tabu_alt, cmd):
|
||||
|
||||
@@ -83,6 +83,25 @@ else
|
||||
FAILED+=("Voice (Lucys Sprech-Pfad)")
|
||||
fi
|
||||
|
||||
# ── 4) Repo-Wächter: verlorene Merges (Vorfall 12.–15.07.2026) ──────────────
|
||||
# Der Annahme-Runner merged+pusht nach origin/main; klemmt der Push (Gitea-Auth
|
||||
# flattert) oder überschreibt eine fremde Session origin/main, trägt Box-main
|
||||
# Commits, die origin fehlen — der nächste deploy-Reset VERNICHTET sie still
|
||||
# (so verlor die Box die angenommene Karte 'wartung/lucy-annahme-fixes' vom
|
||||
# 12.07., bemerkt erst beim Review am 15.07.). Hier täglich gegenprüfen.
|
||||
LIVE_CO="$HOME/mission-control-v2"
|
||||
if git -C "$LIVE_CO" fetch -q origin 2>/dev/null; then
|
||||
LOST=$(git -C "$LIVE_CO" log --oneline origin/main..main 2>/dev/null | head -3 | tr '\n' ' ')
|
||||
if [ -n "$LOST" ]; then
|
||||
say "FAIL · Box-main trägt ungepushte Commits (Deploy würde sie vernichten): $LOST"
|
||||
FAILED+=("Repo (ungepushte Commits auf Box-main: $LOST— vor dem nächsten Deploy nach origin retten)")
|
||||
else
|
||||
say "PASS · Repo-Wächter (Box-main == origin/main)"
|
||||
fi
|
||||
else
|
||||
say "SKIP · Repo-Wächter (git fetch fehlgeschlagen)"
|
||||
fi
|
||||
|
||||
# ── Meldung nur bei Rot ──────────────────────────────────────────────────────
|
||||
if [ "${#FAILED[@]}" -eq 0 ]; then
|
||||
say "Alle Selbst-Tests grün ($(date '+%F %H:%M'))."
|
||||
|
||||
@@ -86,10 +86,12 @@ Bevor du selbst Tasks aufteilst, befragst du zwingend **`gpt-oss-120b`** nach ei
|
||||
3. Fuer jeden Teil-Task entscheide: (a) Artefakt, (b) Worker (`Qwen3-Coder-Next` + `build`/`refactor`), (c) Kontext.
|
||||
|
||||
### 2. Worktree anlegen (Slug = kurzer Kebab-Case-Name des Auftrags)
|
||||
Worktrees liegen unter `~/.hermes/worktrees/` (NICHT /tmp — ueberlebt keinen Reboot und
|
||||
hinterlaesst kaputte Worktree-Registrierungen, Review-Befund 15.07.):
|
||||
```
|
||||
cd ~/mission-control-v2 && git fetch -q origin \
|
||||
&& git worktree add /tmp/orch-<slug> -b orchestrator/<slug> origin/main
|
||||
mkdir -p /tmp/orch-<slug>-work # Ablage fuer rohe Worker-Ausgaben
|
||||
mkdir -p ~/.hermes/worktrees && cd ~/mission-control-v2 && git fetch -q origin \
|
||||
&& git worktree add ~/.hermes/worktrees/orch-<slug> -b orchestrator/<slug> origin/main
|
||||
mkdir -p ~/.hermes/worktrees/orch-<slug>-work # Ablage fuer rohe Worker-Ausgaben
|
||||
```
|
||||
|
||||
### 3. Routen — je Teil-Task ein Worker-Ein-Schuss (seriell)
|
||||
@@ -98,19 +100,19 @@ Fuer jeden Bau-Teil-Task:
|
||||
fertige Artefakte aus frueheren Schritten als Kontext mitgeben, wenn der Worker sie braucht.
|
||||
2. Ruf den Worker auf und fang die Ausgabe roh ab:
|
||||
```
|
||||
printf '%s' "$eingabe" | WORKER_ROLE=build ~/.hermes/scripts/worker.sh > /tmp/orch-<slug>-work/<n>.out
|
||||
printf '%s' "$eingabe" | WORKER_ROLE=build ~/.hermes/scripts/worker.sh > ~/.hermes/worktrees/orch-<slug>-work/<n>.out
|
||||
```
|
||||
(Default-Modell ist `Qwen3-Coder-Next`. Fuer einen bewusst anderen Worker `WORKER_MODEL=<echte-id>`.)
|
||||
3. **Pruefe die Ausgabe, bevor du sie verwendest:** Beginnt sie mit `FEHLT:`, hast du zu wenig
|
||||
Kontext gegeben → nachliefern und erneut rufen. Hat der Worker versehentlich einen ```-Codezaun
|
||||
oder Prosa drumherum gesetzt, entferne ihn. Dann schreibe das saubere Artefakt mit deinen
|
||||
Datei-Tools an seinen Platz im Worktree (`/tmp/orch-<slug>/...`). Der Worker hat KEINE
|
||||
Datei-Tools an seinen Platz im Worktree (`~/.hermes/worktrees/orch-<slug>/...`). Der Worker hat KEINE
|
||||
Datei-Haende — das Schreiben machst DU.
|
||||
4. **Schreibziel-Pflicht (Vorfall 09.07.2026):** JEDES write_file/patch-Ziel MUSS mit
|
||||
`/tmp/orch-<slug>/` beginnen — vor dem ersten Schreiben einmal laut pruefen. NIEMALS nach
|
||||
`~/.hermes/worktrees/orch-<slug>/` beginnen — vor dem ersten Schreiben einmal laut pruefen. NIEMALS nach
|
||||
`~/mission-control-v2/...` schreiben (Live-Checkout!), NIEMALS Work-Dirs/Worktrees FREMDER
|
||||
Slugs wiederverwenden (beim Doku-Lauf landete ein Artefakt im Live-Checkout, weil das alte
|
||||
`/tmp/orch-hermes-desktop-work` recycelt wurde). Existiert dein `/tmp/orch-<slug>` noch nicht,
|
||||
`/tmp/orch-hermes-desktop-work` recycelt wurde). Existiert dein `~/.hermes/worktrees/orch-<slug>` noch nicht,
|
||||
ist das der Beweis, dass Schritt 2 fehlt — erst Worktree anlegen.
|
||||
5. **Grosse Artefakte NIE im Klartext in deine Antwort** — sie gehoeren in Dateien im Worktree.
|
||||
Wer Dateiinhalte in die Antwort kippt, stirbt am Output-Limit mitten im Lauf (Doku-Lauf
|
||||
@@ -148,7 +150,7 @@ Grund wie in der Werkstatt („wer seine eigenen Hausaufgaben benotet, stimmt si
|
||||
**Zuerst ALLES stagen** — sonst fehlen NEUE Dateien im Diff (haeufigste Falle: `git diff` ignoriert
|
||||
untracked Dateien, die Kritiker bekaemen einen LEEREN Diff und wuerden alles blind ablehnen):
|
||||
```
|
||||
git -C /tmp/orch-<slug> add -A
|
||||
git -C ~/.hermes/worktrees/orch-<slug> add -A
|
||||
```
|
||||
Dann die Eingabe EINMAL bauen (mit `--cached`, damit die neuen Dateien drin sind):
|
||||
```
|
||||
@@ -159,7 +161,7 @@ ZERLEGUNG (deine Teil-Tasks, je 1 Zeile):
|
||||
<1..n>
|
||||
|
||||
GIT-DIFF des Worktrees:
|
||||
$(git -C /tmp/orch-<slug> diff --cached origin/main)"
|
||||
$(git -C ~/.hermes/worktrees/orch-<slug> diff --cached origin/main)"
|
||||
```
|
||||
Ist dieser Diff LEER, hast du nicht gestaged (oder nichts gebaut) → NICHT weiter, erst beheben.
|
||||
**Kritik A — Kompetenz** (`Qwen3-Coder-Next`, starker Coder, vom Bauen noch geladen → KEIN Swap):
|
||||
@@ -212,7 +214,7 @@ Zugriff aufs Repo. Er sieht NUR, was du ihm auf stdin gibst. Deshalb:
|
||||
den Fokus. Ziel: das kleinste, das den Teil-Task eindeutig macht.
|
||||
|
||||
## Nach dem Commander-Entscheid (kommt als neuer Auftrag)
|
||||
- „verwerfen" → `git worktree remove /tmp/orch-<slug> --force && git branch -D orchestrator/<slug>`
|
||||
(+ `rm -rf /tmp/orch-<slug>-work`; Remote-Branch loeschen, falls gepusht).
|
||||
- „verwerfen" → `git worktree remove ~/.hermes/worktrees/orch-<slug> --force && git branch -D orchestrator/<slug>`
|
||||
(+ `rm -rf ~/.hermes/worktrees/orch-<slug>-work`; Remote-Branch loeschen, falls gepusht).
|
||||
- „merge" → das Mergen nach main + Deploy macht der PC/Claude (Frontend-Builds gibt es nur dort).
|
||||
Du pushst NIE nach main.
|
||||
|
||||
@@ -42,7 +42,8 @@ unangenommene Branches aufbauen — erst wenn die Vor-Etappe in main ist, kommt
|
||||
## Ablauf
|
||||
|
||||
1. **Worktree anlegen** (Slug = kurzer Kebab-Case-Name des Auftrags):
|
||||
`cd ~/mission-control-v2 && git fetch origin && git worktree add /tmp/wartung-<slug> -b wartung/<slug> origin/main`
|
||||
`mkdir -p ~/.hermes/worktrees && cd ~/mission-control-v2 && git fetch origin && git worktree add ~/.hermes/worktrees/wartung-<slug> -b wartung/<slug> origin/main`
|
||||
(Worktrees NIE unter /tmp — ueberlebt keinen Reboot, Review-Befund 15.07.)
|
||||
2. **Patch** nur im Worktree. Minimal-invasiv, Stil der umgebenden Datei übernehmen
|
||||
(deutsche Kommentare, bestehende Muster).
|
||||
3. **Selbst-Gate** (was zutrifft):
|
||||
@@ -76,7 +77,7 @@ unangenommene Branches aufbauen — erst wenn die Vor-Etappe in main ist, kommt
|
||||
<der Auftrag>
|
||||
|
||||
GIT-DIFF des Worktrees:
|
||||
<git -C /tmp/wartung-<slug> diff origin/main>" | FREMDBLICK_MODE=code ~/.hermes/scripts/fremdblick.sh
|
||||
<git -C ~/.hermes/worktrees/wartung-<slug> diff origin/main>" | FREMDBLICK_MODE=code ~/.hermes/scripts/fremdblick.sh
|
||||
```
|
||||
|
||||
Das läuft auf `Qwen3-Coder-Next` (128k, code-stark, anderes Modell als der Qwen3.6-Worker,
|
||||
@@ -113,7 +114,7 @@ unangenommene Branches aufbauen — erst wenn die Vor-Etappe in main ist, kommt
|
||||
|
||||
## Nach dem User-Entscheid (kommt als neuer Auftrag)
|
||||
|
||||
- „verwerfen" → `git worktree remove /tmp/wartung-<slug> --force && git branch -D wartung/<slug>`
|
||||
- „verwerfen" → `git worktree remove ~/.hermes/worktrees/wartung-<slug> --force && git branch -D wartung/<slug>`
|
||||
(+ Remote-Branch löschen, falls gepusht: `git push origin --delete wartung/<slug>`)
|
||||
- „merge" → das Mergen nach main + Deploy macht der PC/Claude (Frontend-Builds gibt es
|
||||
nur dort). Du pushst NIE nach main — auch nicht mit Token.
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
diff --git a/gateway/platforms/api_server.py b/gateway/platforms/api_server.py
|
||||
index 628713224..415f3b2dc 100644
|
||||
--- a/gateway/platforms/api_server.py
|
||||
+++ b/gateway/platforms/api_server.py
|
||||
@@ -362,6 +362,159 @@ def _multimodal_validation_error(exc: ValueError, *, param: str) -> "web.Respons
|
||||
)
|
||||
|
||||
|
||||
+def _is_coder_model(model_name: Optional[str]) -> bool:
|
||||
+ """Return True if model_name looks like a coder model (case-insensitive 'coder' in name)."""
|
||||
+ if not model_name:
|
||||
+ return False
|
||||
+ return "coder" in str(model_name).lower()
|
||||
+
|
||||
+
|
||||
+def _extract_image_urls_from_messages(messages: List[Dict[str, Any]]) -> List[str]:
|
||||
+ """Extract all image URLs from OpenAI-style messages payload."""
|
||||
+ image_urls: List[str] = []
|
||||
+ for msg in messages:
|
||||
+ if not isinstance(msg, dict):
|
||||
+ continue
|
||||
+ content = msg.get("content")
|
||||
+ if not content:
|
||||
+ continue
|
||||
+ # Normalize multimodal content to list of parts
|
||||
+ parts: Any = content
|
||||
+ if isinstance(content, str):
|
||||
+ continue # text-only message has no images
|
||||
+ if not isinstance(parts, list):
|
||||
+ continue
|
||||
+ for part in parts:
|
||||
+ if not isinstance(part, dict):
|
||||
+ continue
|
||||
+ part_type = str(part.get("type", "")).strip().lower()
|
||||
+ if part_type in {"image_url", "input_image"}:
|
||||
+ img_ref = part.get("image_url")
|
||||
+ if isinstance(img_ref, dict):
|
||||
+ url = img_ref.get("url")
|
||||
+ else:
|
||||
+ url = img_ref
|
||||
+ if isinstance(url, str) and url.strip():
|
||||
+ image_urls.append(url.strip())
|
||||
+ return image_urls
|
||||
+
|
||||
+
|
||||
+async def _maybe_enrich_with_vision_if_coder(
|
||||
+ body: Dict[str, Any],
|
||||
+) -> Dict[str, Any]:
|
||||
+ """
|
||||
+ For coder model requests with image attachments, pre-analyze images
|
||||
+ with the vision fallback model and replace image parts with descriptive text.
|
||||
+
|
||||
+ Returns a modified copy of body with image URLs replaced by text
|
||||
+ descriptions in the messages array.
|
||||
+ """
|
||||
+ model_name = body.get("model")
|
||||
+ if not _is_coder_model(model_name):
|
||||
+ return body
|
||||
+
|
||||
+ messages = body.get("messages")
|
||||
+ if not messages or not isinstance(messages, list):
|
||||
+ return body
|
||||
+
|
||||
+ image_urls = _extract_image_urls_from_messages(messages)
|
||||
+ if not image_urls:
|
||||
+ return body
|
||||
+
|
||||
+ # Import here to avoid circular dependency issues
|
||||
+ try:
|
||||
+ from tools.vision_tools import vision_analyze_tool
|
||||
+ except ImportError:
|
||||
+ logger.warning(
|
||||
+ "Vision tool not available for coder model enrichment. "
|
||||
+ "Install hermes-agent tools to enable this feature."
|
||||
+ )
|
||||
+ return body
|
||||
+
|
||||
+ # Use the configured vision model or fall back to VL-30B
|
||||
+ # We'll let vision_analyze_tool use its own default (auxiliary.vision)
|
||||
+ # but we can optionally override by passing model parameter
|
||||
+ # For now, let it auto-resolve via config
|
||||
+ analysis_prompt = (
|
||||
+ "Describe everything visible in this image in thorough detail. "
|
||||
+ "Include any text, code, data, objects, people, layout, colors, "
|
||||
+ "and any other notable visual information."
|
||||
+ )
|
||||
+
|
||||
+ # Build a mapping of URL -> description
|
||||
+ url_to_desc: Dict[str, str] = {}
|
||||
+ for img_url in image_urls:
|
||||
+ try:
|
||||
+ result_json = await vision_analyze_tool(
|
||||
+ image_url=img_url,
|
||||
+ user_prompt=analysis_prompt,
|
||||
+ )
|
||||
+ result = json.loads(result_json)
|
||||
+ if result.get("success"):
|
||||
+ desc = result.get("analysis", "")
|
||||
+ url_to_desc[img_url] = (
|
||||
+ f"[The user sent an image~ Here's what I can see:\n{desc}]\n"
|
||||
+ f"[If you need a closer look, use vision_analyze with image_url: {img_url} ~]"
|
||||
+ )
|
||||
+ else:
|
||||
+ url_to_desc[img_url] = (
|
||||
+ "[The user sent an image but I couldn't quite see it this time (>_<) "
|
||||
+ f"You can try looking at it yourself with vision_analyze using image_url: {img_url}]"
|
||||
+ )
|
||||
+ except Exception as e:
|
||||
+ logger.error("Vision auto-analysis error for image %s: %s", img_url, e)
|
||||
+ url_to_desc[img_url] = (
|
||||
+ f"[The user sent an image but something went wrong when I tried to look at it~ "
|
||||
+ f"You can try examining it yourself with vision_analyze using image_url: {img_url}]"
|
||||
+ )
|
||||
+
|
||||
+ # Now replace image parts with text in a copy of messages
|
||||
+ enriched_messages: List[Dict[str, Any]] = []
|
||||
+ for msg in messages:
|
||||
+ msg_copy = dict(msg)
|
||||
+ content = msg_copy.get("content")
|
||||
+ if isinstance(content, str):
|
||||
+ enriched_messages.append(msg_copy)
|
||||
+ continue
|
||||
+
|
||||
+ if not isinstance(content, list):
|
||||
+ enriched_messages.append(msg_copy)
|
||||
+ continue
|
||||
+
|
||||
+ enriched_parts: List[Any] = []
|
||||
+ for part in content:
|
||||
+ if not isinstance(part, dict):
|
||||
+ enriched_parts.append(part)
|
||||
+ continue
|
||||
+
|
||||
+ part_type = str(part.get("type", "")).strip().lower()
|
||||
+ if part_type in {"image_url", "input_image"}:
|
||||
+ img_ref = part.get("image_url")
|
||||
+ if isinstance(img_ref, dict):
|
||||
+ img_url = img_ref.get("url")
|
||||
+ else:
|
||||
+ img_url = img_ref
|
||||
+ if isinstance(img_url, str) and img_url in url_to_desc:
|
||||
+ # Replace image part with text part
|
||||
+ enriched_parts.append({
|
||||
+ "type": "text",
|
||||
+ "text": url_to_desc[img_url],
|
||||
+ })
|
||||
+ else:
|
||||
+ # Keep original if URL not found (shouldn't happen)
|
||||
+ enriched_parts.append(part)
|
||||
+ else:
|
||||
+ enriched_parts.append(part)
|
||||
+
|
||||
+ msg_copy["content"] = enriched_parts
|
||||
+ enriched_messages.append(msg_copy)
|
||||
+
|
||||
+ # Return modified body
|
||||
+ body_copy = dict(body)
|
||||
+ body_copy["messages"] = enriched_messages
|
||||
+ return body_copy
|
||||
+
|
||||
+
|
||||
def _session_chat_user_message(body: Dict[str, Any], *, param: str = "message") -> tuple[Any, Optional["web.Response"]]:
|
||||
"""Parse and normalize session chat ``message`` / ``input`` like chat completions."""
|
||||
user_message = body.get("message") or body.get("input")
|
||||
@@ -0,0 +1,101 @@
|
||||
# UMBAUPLAN — Abschluss-Review 15.07.2026
|
||||
|
||||
**Was das ist:** Ergebnis des kompletten Projekt-Reviews (Repo + Box live + tagesaktuelle
|
||||
Recherche). DIE Arbeitsliste für die Zeit nach Claude. Leser: der User, die Box-Worker
|
||||
(über Queue-Karten) und Gemini/Antigravity (liest F:\-Checkout).
|
||||
**Regel:** Erledigtes hier streichen. Neue Erkenntnisse einarbeiten, nicht daneben legen.
|
||||
|
||||
---
|
||||
|
||||
## 0 · IST-Befund (live verifiziert 15.07., 09:20–11:00)
|
||||
|
||||
**Die Box ist gesund und die Autonomie-Kette ARBEITET nachweislich.**
|
||||
|
||||
- Dienste: alle aktiv (mission-control-2, hermes-gateway, hermes-builtin-ui, mem0, voice, box-console, llama-swap). 8 Tage Uptime, 68 GB frei.
|
||||
- Nacht-Crons 15.07. alle grün: Traum 03:15 · Selbst-Inventur 03:35 · Idle-Radar 03:45 · Karten-Gutachter 04:00 · Bagatell 04:10 · Chef-Gutachter 04:30 · Daily-Briefing 08:00 · Release-Radar Mo 05:15. Backups: mc2-backup 03:32 + PBS 03:50 gelaufen.
|
||||
- Versionen: llama.cpp **b9994** (Vulkan) · llama-swap **v239** · Hermes **0.18.2** (+1 legitimer carried commit) · MC2 2.0.0-w8.
|
||||
- Warm-Set schlank: Hirn 17,5 GB + embed 2 GB + reranker 2,2 GB ≈ 22 GB.
|
||||
- Repo: lokal = Gitea = Box @ main. Live-Checkout wieder sauber auf `main`.
|
||||
- **Autonomie-Beweis der Nacht 14.07.:** Idle-Radar erhob Journal-Befund (stop-sigterm-Timeout) → Decomposer zerlegte → werkstatt baute Restart-Skript+systemd-Override → betrieb testete live (~6 s Recovery) → done. Ohne Menschen, ohne Claude.
|
||||
|
||||
## 1 · Heute im Review erledigt (15.07.)
|
||||
|
||||
- ✅ **Hermes-Quelle wieder jungfräulich:** verwaister ~160-Zeilen-Patch in `gateway/platforms/api_server.py` entfernt (Bild-Beschreibung für Coder — war NIE aufgerufen = toter Code, wurde aber von jedem `hermes update` mitgeschleppt = Konflikt-Risiko fürs kommende v0.19). Original archiviert: `docs/archiv/hermes-api_server-vision-patch-verwaist.diff`.
|
||||
- ✅ **Idee daraus regelkonform portiert → Bild-Weiche v2** (MC2-Gateway): Bild-Request an Coder-Ziel bekommt Vision-BESCHREIBUNG injiziert und bleibt beim Coder (Screenshot-Debugging!); Fallback = alte Umleitung. **Branch `wartung/bild-weiche-v2` → wartet als Karte auf Klick.**
|
||||
- ✅ **VL-8B-Leiche gefixt:** `agents.vision.model: Qwen3-VL-8B-Instruct` (Modell existiert seit 07.07. nicht mehr!) → `vision`-Alias, in Top-Level- UND beiden Profil-Configs (Backups `*.bak-review-20260715`). Gateway neu, `hermes doctor` grün.
|
||||
- ✅ **Live-Checkout aufgeräumt:** zurück auf `main` (stand auf verwaistem Branch), 2 stale Branches gelöscht, leere `state.db` + alter `backend/memory.py` entfernt.
|
||||
- ✅ **2 Mess-Karten in der Queue:** `t_5ada48ea` (Gemma-4-Hirn-Bench) + `t_64a384b9` (heavy-64k-Messung).
|
||||
- ✅ Blockierte Karte `t_8231d6b8` analysiert: **Arbeit ist fertig+live** — Worker vergaß nur den `kanban_complete`-Aufruf. Abschluss = Sofort-Paket a).
|
||||
|
||||
## 2 · Konfig-Bewertung: Das ist GUT so (nicht anfassen)
|
||||
|
||||
| Bereich | Urteil |
|
||||
|---|---|
|
||||
| Modell-Kader (8 Modelle, klare Rollen, schlankes Warm-Set) | ✅ passt zur 122-GB-Box; Recherche 15.07.: **kein** Qwen-Coder-Nachfolger draußen, Coder-Verdikt hält |
|
||||
| Vulkan/RADV statt ROCm | ✅ hält (ROCm auf gfx1151 weiter „Preview", llama.cpp-Issue #21284 offen) |
|
||||
| Fundament-Fixes (concurrency 1, auto_decompose true, Specifier-Pins, 3 Hooks je Profil) | ✅ live korrekt verifiziert |
|
||||
| Verdikte Mem0 / Telegram / Electron / pocket-Default / Hermes-Prompts englisch | ✅ bleiben |
|
||||
| DeepSeek V4-Flash als Kandidat | ❌ zu groß (103–142 GB Quant) — passt nie neben das Warm-Set |
|
||||
| systemd-Override Restart=always für MC2 | ✅ behalten (User-Entscheid 15.07.) — gute Appliance-Praxis |
|
||||
|
||||
## 3 · SOFORT-Paket (User-Klicks + eine sudo-Runde)
|
||||
|
||||
a) **Karte `wartung/bild-weiche-v2` annehmen** (Auftragsbuch). Danach prüft man so:
|
||||
Bild an `model:"coder"` schicken → Antwort-Header `x-mc-route-reason: Bild beschrieben (Vision) -> bleibt beim Coder`.
|
||||
b) **Karte `t_8231d6b8` abschließen** (Arbeit war fertig, nur Protokollverstoß):
|
||||
`ssh hitonabi@192.168.178.151` → `cd ~/.hermes/hermes-agent && venv/bin/hermes kanban complete t_8231d6b8 --comment "war fertig+verifiziert, Worker vergass complete-Call"`
|
||||
c) **sudo-Runde (einmal Passwort):**
|
||||
1. Stale v1-Unit weg: `sudo systemctl disable mission-control.service; sudo rm /etc/systemd/system/mission-control.service; sudo rm -rf /opt/mission-control` (Faden C14)
|
||||
2. Root-Warmup-Kopie aktualisieren: `sudo install -m 0755 ~/mission-control-v2/deploy/warmup.sh /usr/local/bin/llama-swap-warmup.sh` (stale seit 07.07. — sonst wärmt ein llama-swap-Restart falsch)
|
||||
d) **Daily-Briefing-Cron** (dein eigener, 08:00): steht auf `Repeat: 9/41` — **läuft nach 41 Läufen aus**. Wenn dauerhaft gewollt: auf ∞ stellen (`hermes cron edit 095d53c1e99e`).
|
||||
|
||||
## 4 · ROBUSTHEIT (Priorität 1 — Karten für die Queue)
|
||||
|
||||
- **R1 · Worker-Protokoll-Erinnerung** (klein): `box-steckbrief-inject.sh` um einen Satz ergänzen: „Am Ende IMMER kanban_complete oder kanban_block aufrufen — sauberes Text-Ende zählt nicht." Genau daran blockierte die Nacht-Karte.
|
||||
- **R2 · Guard-Erweiterung systemd** (klein, braucht User-Ja): `tabu-pfade-guard.py` um `~/.config/systemd/user/` erweitern. Die Nacht-Werkstatt hat den Override direkt scharf geschaltet — ging gut, widerspricht aber „jede Code-Zeile ist Klick-pflichtig". Installation künftig nur über angenommene Karte.
|
||||
- **R3 · Hermes v0.19 kommt** (Sammel-Release, ~660 PRs seit v0.18.0): Fangnetz existiert (autoupdate → doctor → postcheck → Auto-Rollback+Pin+Telegram). Quelle ist seit heute konfliktfrei. **Nichts zu tun — nur wissen, dass die Meldung kommen wird.**
|
||||
- **R4 · Orchestrator-Worktrees raus aus /tmp** (klein): `/tmp/orch-kanten` überlebt keinen Reboot; Worktree-Basis nach `~/.hermes/orch-worktrees/` o. ä.
|
||||
- **R5 · Runbook C13** (größter offener Robustheits-Batzen): 1-Seiter „Box tot → was drücken" + Bare-Metal-Bootstrap (docs/DISASTER_RECOVERY.md existiert als Konzept). Karten-Serie.
|
||||
- **R6 · Backup-Restposten:** QNAP-/pve-root-Passwörter in Keepass verifizieren; Restore-Probe einmal fahren (PBS läuft, aber ungeprobt zurückgespielt).
|
||||
|
||||
## 5 · AUFRÄUMEN (Priorität 2)
|
||||
|
||||
- **A1 · Alt-Docs** (Bagatell-Klasse, Box darf selbst): README/STATUS/CUTOVER/HERMES_SETUP tragen Vor-Trennung-Stand (Faden C15).
|
||||
- **A2 · Lucy-Repo-Hygiene** (PC-Karte über Lucy-Pipeline): 4 unkommittierte Dateien (Hybrid-TTS-Routing in App.tsx/useVoiceAgent/voice-core/kokoro_server) committen oder verwerfen — vorher prüfen, was live läuft; Saber-Altlasten löschen (XTTS-Datensatz-Verdikt: unbrauchbar); untracked venvs/Datasets ignorieren oder weg.
|
||||
- **A3 · Hermes-Home-Kleinkram:** `hermes_cli/web_dist.bak-rootbuild/` + `.install_method` (harmlos, bei Gelegenheit).
|
||||
- **A4 · D16-Restposten:** c) System-Logs ausbauen (Fehler-Filter) · e) Mem0-Dubletten automatisch statt Knopf · f) Lucy-Reste aus Web-UI + Voice-Sidecar-Diät. Alles einzelne Queue-Karten.
|
||||
|
||||
## 6 · FEATURES (Priorität 3)
|
||||
|
||||
- **F1 · Bild-Weiche v2:** gebaut, wartet auf Klick (Sofort-Paket a).
|
||||
- **F2 · heavy auf 64k** (nach Messkarte `t_64a384b9` + User-Ja): dann `delegation`-Floor erfüllt → heavy als delegate_task-Ziel freischalten → Konzept-Fließband/Lucy nutzen heavy nativ statt worker.sh-Umweg.
|
||||
- **F3 · Mini-Stimme v2 (AKTIV, User-Entscheid 15.07.):**
|
||||
1. User: Colab-Volllauf (~3 h, Notebook `Lucy_Stimme_Training_v2.ipynb` auf Desktop, Volltraining-Modus ist schon eingestellt)
|
||||
2. Danach: Modell-Datei tauschen + Whisper-Rücktranskriptions-Test (der EINZIGE ehrliche Richter) — kann PC-Hermes über die Lucy-Pipeline
|
||||
3. A/B gegen pocket im Live-Gespräch → Default-Entscheid
|
||||
4. **Zukunftsfaden v3:** Qwen3-TTS-**Instruct** kann inzwischen Stil/Emotion bei geklonten Stimmen steuern — genau das „alles klingt gleich"-Problem. Neuer Datensatz mit Emotions-Instruktionen, sonst gleiches Rezept.
|
||||
- **F4 · Gemma 4 26B-A4B als Hirn-Kandidat** (nach Bench-Karte `t_5ada48ea`): Community meldet ~110 t/s mit MTP-Head (vs. ~66 heute), multimodal (könnte Hirn+Augen VEREINEN), 256k Kontext. ABER: gemma-Vorgeschichte (Cache-Bug #21468, „Fix" unbestätigt), KV nur f16 (RAM!), mmproj×MTP ungeklärt. **Nur mit Messbeweis + User-Ja — Hirn-Swap ist ein Groß-Ereignis mit Rollback-Plan.**
|
||||
|
||||
## 7 · TEMPO (Priorität 4)
|
||||
|
||||
- **T1 · ROCm-Recheck 20.–25.07.** (Box-Reminder #3 existiert): Stand 15.07. hält Vulkan (tg-Führung bestätigt, #21284 weiter offen). Falls ROCm 7.13+ liefert: NUR Hybrid für heavy/coder-Prefill erwägen, **Hirn bleibt Vulkan**.
|
||||
- **T2 · hipEngine-Watch** (Reminder #4 existiert): weiterhin praktisch Qwen3.6-only, gfx1151 nur „initial pass" — beobachten, nicht adoptieren.
|
||||
- **T3 · Faden 10** (spec-draft-Sweep): bleibt bewusst übersprungen (User 14.07.).
|
||||
|
||||
## 8 · Übergabe-Logik (wenn Claude weg ist)
|
||||
|
||||
1. **Neue Ideen** → eine der drei Türen (Telegram / Lucy / MC2-Ideenfeld) → Queue → Karte → dein Klick. Die Kette ist E2E bewiesen.
|
||||
2. **Wissen** → `docs/wissen/` (dieser Plan, STACK, FALLEN, GRENZEN, VERDIKTE) — Box liest `~/mission-control-v2`, Gemini liest `F:\Coding Stuff\mission-control-2`. GEMINI_BRIEFING.md existiert.
|
||||
3. **Pflege** → Auto-Update-Timer mit Rollback+Pin; Selfsmoke täglich; Alarmkette Telegram.
|
||||
4. **Evolution** → Release-Radar (Mo) + Chef-Gutachter (täglich) + Traum (täglich) finden Chancen; Bench-Harnesses (brain-bench, model-bench) messen sie; du entscheidest per Klick.
|
||||
5. **Notfall** → Gemini/Antigravity als Review-Partner (RUNBOOK „Wann Gemini rufen").
|
||||
|
||||
## 9 · Recherche-Quellen (15.07.2026)
|
||||
|
||||
- Strix-Halo-Benchmarks/Grid: [strix-halo-guide](https://github.com/hogeheer499-commits/strix-halo-guide) · [llm-tracker Strix Halo](https://llm-tracker.info/_TOORG/Strix-Halo)
|
||||
- ROCm-Prefill-Issue: [llama.cpp #21284](https://github.com/ggml-org/llama.cpp/issues/21284) (offen)
|
||||
- Gemma 4: [26B-A4B auf Strix Halo](https://akehir.com/blog/strix-halo-kubernetes-llm-gemma-4) · [Cache-Bug #21468](https://github.com/ggml-org/llama.cpp/issues/21468) · [unsloth GGUF](https://huggingface.co/unsloth/gemma-4-26B-A4B-it-GGUF) · [AMD Day-0](https://www.amd.com/en/developer/resources/technical-articles/2026/day-0-support-for-gemma-4-on-amd-processors-and-gpus.html)
|
||||
- DeepSeek V4: [unsloth Doku](https://unsloth.ai/docs/models/deepseek-v4) (103–142 GB → zu groß)
|
||||
- Hermes v0.19: [Releases](https://github.com/NousResearch/hermes-agent/releases)
|
||||
- hipEngine: [shisa-ai/hipEngine](https://github.com/shisa-ai/hipEngine)
|
||||
- Qwen3-TTS (Instruct/Emotion): [Qwen-Blog](https://qwen.ai/blog?id=qwen3tts-0115) · [Emotion-Diskussion](https://github.com/QwenLM/Qwen3-TTS/discussions/218)
|
||||
Reference in New Issue
Block a user