Mehrere lokale Modelle, on-demand geladen/geswappt, in Diagnostic waehlbar. Design: das Brain schickt den Modellnamen (aus local_llm.json) im llm_request mit -> Adapter -> llama-swap laedt/swappt. Keine separate Gamebox-Config noetig. - xtts: `llama`-Container -> `llama-swap` (unified-cuda), config.yaml mit qwen3-8b (Standard) + qwen3-4b; Auto-Download via -hf, Cache /models geteilt (qwen3-8b schon da). Adapter -> llama-swap:8080, Timeout 600s (Erst-Download). - adapter: `model` aus dem Request an llama-swap durchreichen (Fallback env). - brain: router.load_config liest localLlmModel; local_llm_chat(model=...); agent gibt cfg-Modell mit; bridge reicht model durch (_local_llm + Route). - diagnostic: /api/local-models-list (aus /shared/config/local_models.json, seeded), local-llm-config um localLlmModel erweitert; Dropdown "Lokales Modell" im Settings-Block + Erst-Download-Hinweis. BLIND gebaut (Gamebox nicht testbar hier): llama-swap CLI/Config-Pfad beim ersten Start via `docker logs aria-llama-swap` pruefen. Live-Lade-Status (Adapter->Diagnostic) ist B0.5-2 (Folgeschritt). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
69 lines
2.8 KiB
Python
69 lines
2.8 KiB
Python
"""
|
|
Local-LLM-Client (Plan B) — Brain-Seite.
|
|
|
|
Ruft das schnelle lokale LLM (Qwen3 auf der Gamebox) ueber die Bridge:
|
|
Brain → HTTP /internal/local-llm → Bridge → RVS → llm-adapter → llama.cpp
|
|
|
|
Analog zum Claude-`proxy_client`, nur ueber die Bridge (die ist der RVS-Client;
|
|
das Brain bleibt HTTP-only). Der Router im Brain (B1) entscheidet, welche Turns
|
|
hierher gehen (einfach) und welche an Claude (schwer / Tool-Bedarf).
|
|
|
|
Rueckgabe von local_llm_chat: {ok, content, model?, elapsedMs?} oder {ok:False, error}.
|
|
Nie werfen — der Aufrufer entscheidet bei ok=False, ob er auf Claude eskaliert.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
import urllib.error
|
|
import urllib.request
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
BRIDGE_URL = os.environ.get("BRIDGE_URL", "http://aria-bridge:8090")
|
|
# Etwas ueber dem Bridge-seitigen _LLM_TIMEOUT_S (30s), damit der HTTP-Call nicht
|
|
# vor dem eigentlichen LLM-Timeout abbricht.
|
|
LOCAL_LLM_HTTP_TIMEOUT_SEC = float(os.environ.get("LOCAL_LLM_HTTP_TIMEOUT_SEC", "35"))
|
|
|
|
|
|
def local_llm_chat(messages: list, *, max_tokens: int = 512,
|
|
temperature: float = 0.7, stop=None, tools=None,
|
|
model=None) -> dict:
|
|
"""Ein Chat-Call ans lokale LLM. messages = [{role, content}, ...].
|
|
model (B0.5): welches Modell llama-swap laden soll. tools (B1b): optionale
|
|
OpenAI-Tool-Defs; das Ergebnis kann dann result['tool_calls'] enthalten.
|
|
Blockierend (urllib) — chat() laeuft ohnehin im Executor-Thread."""
|
|
if not isinstance(messages, list) or not messages:
|
|
return {"ok": False, "error": "messages leer/ungueltig"}
|
|
req = {"messages": messages, "max_tokens": max_tokens, "temperature": temperature}
|
|
if stop:
|
|
req["stop"] = stop
|
|
if tools:
|
|
req["tools"] = tools
|
|
if model:
|
|
req["model"] = model
|
|
try:
|
|
body = json.dumps(req).encode("utf-8")
|
|
http_req = urllib.request.Request(
|
|
f"{BRIDGE_URL}/internal/local-llm", data=body, method="POST",
|
|
headers={"Content-Type": "application/json"},
|
|
)
|
|
with urllib.request.urlopen(http_req, timeout=LOCAL_LLM_HTTP_TIMEOUT_SEC) as resp:
|
|
result = json.loads(resp.read().decode("utf-8", "ignore"))
|
|
except urllib.error.HTTPError as exc:
|
|
try:
|
|
err_data = json.loads(exc.read().decode("utf-8", "ignore"))
|
|
err = err_data.get("error") or str(exc)
|
|
except Exception:
|
|
err = str(exc)
|
|
return {"ok": False, "error": f"local-llm: {err}"}
|
|
except Exception as exc:
|
|
logger.warning("local_llm_chat HTTP-Call fehlgeschlagen: %s", exc)
|
|
return {"ok": False, "error": f"local-llm nicht erreichbar ({exc})"}
|
|
|
|
if not isinstance(result, dict) or not result.get("ok"):
|
|
return {"ok": False, "error": (result or {}).get("error", "unbekannt")}
|
|
return result
|