Files
ARIA-AGENT/aria-brain/local_llm.py
T
duffyduckandClaude Opus 4.8 8ae20a9bd8 feat(local-llm): B0.5 — llama-swap + lokale Modellauswahl in Diagnostic
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>
2026-07-11 14:11:59 +02:00

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