""" 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