From a58aa5594d561eb0a8dd3bd0c2d29fc42556fa01 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Sat, 11 Jul 2026 12:36:09 +0200 Subject: [PATCH] =?UTF-8?q?feat(local-llm):=20B1b-Plumbing=20=E2=80=94=20t?= =?UTF-8?q?ools/tool=5Fcalls=20durch=20Adapter/Bridge/Brain-Client?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Traegt OpenAI-Tool-Definitionen (tools) durch den ganzen lokalen Pfad und gibt tool_calls zurueck: - adapter.py: tools -> llama.cpp /v1/chat/completions (tool_choice=auto), message.tool_calls zurueck in llm_response. - aria_bridge.py: _local_llm + /internal/local-llm reichen tools durch, geben tool_calls zurueck. - local_llm.py: local_llm_chat akzeptiert tools, result enthaelt tool_calls. Inert bis der Brain-Tool-Loop (naechster Schritt) tools uebergibt — Verhalten unveraendert. Tool-Set + lokale Tool-Loop + Router-Anpassung folgen. Co-Authored-By: Claude Opus 4.8 --- aria-brain/local_llm.py | 8 ++++++-- bridge/aria_bridge.py | 14 ++++++++++---- xtts/llm-adapter/adapter.py | 28 +++++++++++++++++++++------- 3 files changed, 37 insertions(+), 13 deletions(-) diff --git a/aria-brain/local_llm.py b/aria-brain/local_llm.py index a8b9353..205a514 100644 --- a/aria-brain/local_llm.py +++ b/aria-brain/local_llm.py @@ -29,14 +29,18 @@ LOCAL_LLM_HTTP_TIMEOUT_SEC = float(os.environ.get("LOCAL_LLM_HTTP_TIMEOUT_SEC", def local_llm_chat(messages: list, *, max_tokens: int = 512, - temperature: float = 0.7, stop=None) -> dict: + temperature: float = 0.7, stop=None, tools=None) -> dict: """Ein Chat-Call ans lokale LLM. messages = [{role, content}, ...]. - Blockierend (urllib) — im Brain laeuft chat() ohnehin im Executor-Thread.""" + 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 try: body = json.dumps(req).encode("utf-8") http_req = urllib.request.Request( diff --git a/bridge/aria_bridge.py b/bridge/aria_bridge.py index ef2cfd3..3831938 100644 --- a/bridge/aria_bridge.py +++ b/bridge/aria_bridge.py @@ -3367,9 +3367,10 @@ class ARIABridge: _LLM_TIMEOUT_S = 30.0 async def _local_llm(self, messages: list, max_tokens: int = 512, - temperature: float = 0.7, stop=None) -> dict: + temperature: float = 0.7, stop=None, tools=None) -> dict: """Schickt einen llm_request an den llm-adapter (Gamebox), wartet auf - llm_response. Rueckgabe: {ok, content, model, elapsedMs} oder {ok:False, error}.""" + llm_response. tools (B1b) werden durchgereicht; tool_calls kommen zurueck. + Rueckgabe: {ok, content, tool_calls, model, elapsedMs} oder {ok:False, error}.""" if self.ws_rvs is None: return {"ok": False, "error": "RVS-Verbindung nicht aktiv"} if not isinstance(messages, list) or not messages: @@ -3388,8 +3389,10 @@ class ARIABridge: } if stop: req_payload["stop"] = stop - logger.info("[rvs] llm_request → llm-adapter (id=%s, msgs=%d, max_tokens=%d)", - request_id[:8], len(messages), max_tokens) + if tools: + req_payload["tools"] = tools + logger.info("[rvs] llm_request → llm-adapter (id=%s, msgs=%d, max_tokens=%d, tools=%d)", + request_id[:8], len(messages), max_tokens, len(tools) if tools else 0) ok = await self._send_to_rvs({ "type": "llm_request", "payload": req_payload, @@ -3407,6 +3410,7 @@ class ARIABridge: return { "ok": True, "content": result.get("content", ""), + "tool_calls": result.get("tool_calls"), "model": result.get("model"), "elapsedMs": result.get("elapsedMs"), } @@ -3885,9 +3889,11 @@ class ARIABridge: temperature = float(data.get("temperature")) except (TypeError, ValueError): temperature = 0.7 + _tools = data.get("tools") if isinstance(data.get("tools"), list) else None result = await self._local_llm( messages=messages, max_tokens=max_tokens, temperature=temperature, stop=data.get("stop"), + tools=_tools, ) status = 200 if result.get("ok") else 502 await _send_response(writer, status, result) diff --git a/xtts/llm-adapter/adapter.py b/xtts/llm-adapter/adapter.py index d9f9854..e69f5e6 100644 --- a/xtts/llm-adapter/adapter.py +++ b/xtts/llm-adapter/adapter.py @@ -69,9 +69,12 @@ async def _send(ws, mtype: str, payload: dict) -> None: async def _call_llama(messages: list, *, max_tokens: int, temperature: float, - stop) -> dict: + stop, tools=None) -> dict: """Ruft llama.cpp /v1/chat/completions (OpenAI-Format). Gibt - {ok, content, error} zurueck — wirft nie.""" + {ok, content, tool_calls, error} zurueck — wirft nie. + + tools: optionale OpenAI-Tool-Definitionen (B1b). llama.cpp (--jinja) mit + Qwen3 kann natives Tool-Calling und liefert dann message.tool_calls.""" body = { "model": LLM_MODEL, "messages": messages, @@ -81,6 +84,9 @@ async def _call_llama(messages: list, *, max_tokens: int, temperature: float, } if stop: body["stop"] = stop + if tools: + body["tools"] = tools + body["tool_choice"] = "auto" if LLM_DISABLE_THINKING: # llama.cpp (--jinja) reicht chat_template_kwargs an die Chat-Vorlage # weiter. Qwen3 unterdrueckt damit den -Block. @@ -90,8 +96,13 @@ async def _call_llama(messages: list, *, max_tokens: int, temperature: float, r = await client.post(f"{LLAMA_URL}/v1/chat/completions", json=body) r.raise_for_status() data = r.json() - content = (data.get("choices") or [{}])[0].get("message", {}).get("content", "") - return {"ok": True, "content": content or "", "usage": data.get("usage")} + msg = (data.get("choices") or [{}])[0].get("message", {}) or {} + return { + "ok": True, + "content": msg.get("content") or "", + "tool_calls": msg.get("tool_calls") or None, + "usage": data.get("usage"), + } except Exception as e: logger.warning("llama.cpp-Call fehlgeschlagen: %s", e) return {"ok": False, "content": "", "error": str(e)[:300]} @@ -108,17 +119,20 @@ async def _handle_llm_request(ws, payload: dict) -> None: max_tokens = int(payload.get("max_tokens", 512) or 512) temperature = float(payload.get("temperature", 0.7) or 0.7) stop = payload.get("stop") + tools = payload.get("tools") or None t0 = time.time() res = await _call_llama(messages, max_tokens=max_tokens, - temperature=temperature, stop=stop) + temperature=temperature, stop=stop, tools=tools) dt = time.time() - t0 - logger.info("llm_request id=%s -> ok=%s %.2fs content_len=%d", + tc = res.get("tool_calls") + logger.info("llm_request id=%s -> ok=%s %.2fs content_len=%d tool_calls=%d", (req_id[:8] if req_id else "?"), res.get("ok"), dt, - len(res.get("content") or "")) + len(res.get("content") or ""), len(tc) if tc else 0) await _send(ws, "llm_response", { "requestId": req_id, "ok": res.get("ok", False), "content": res.get("content", ""), + "tool_calls": tc, "error": res.get("error"), "model": LLM_MODEL, "elapsedMs": int(dt * 1000),