From 6c5140fb92f0da3de65a4bc016881c61468c5e57 Mon Sep 17 00:00:00 2001 From: Shivering Ocelot Date: Sun, 23 Aug 2026 00:36:37 -0500 Subject: [PATCH] Fix agent tool support, fix many other bugs --- backend/src/agent.py | 8 +-- backend/src/agents/expert.py | 18 +++++-- backend/src/inference.py | 98 +++++++++++++++++++----------------- backend/src/server.py | 2 + frontend/src/router/index.js | 9 ++++ 5 files changed, 79 insertions(+), 56 deletions(-) diff --git a/backend/src/agent.py b/backend/src/agent.py index 19137a7..befe887 100644 --- a/backend/src/agent.py +++ b/backend/src/agent.py @@ -34,6 +34,7 @@ class Agent: self.config = {field.name: data.get(field.name, field.default) for field in getattr(agent_class, 'schema_fields', [])} self.resources = resources + # should be in expert init? self.tools = [] for resource in self.resources: if resource.kind != 'tool': continue @@ -66,7 +67,6 @@ class Agent: **kwargs, }, ) - await self.run(run) out_stream = Stream(self.app) def on_output_event(stream): @@ -77,6 +77,7 @@ class Agent: out_stream.close() run.on('output', on_output_event) run.on('done', on_done) + await self.run(run) return out_stream @@ -95,14 +96,11 @@ class Agent: user_id=self.user_id, input={ "messages": messages, - "tools": getattr(self, "tools", []), "chat_id": message.chat_id, "thread_id": message.thread_id, }, ) - await self.run(run) - async def on_output_event(stream): print("[agent] run output " + repr(stream)) new_message = await self.app.send_chat_message( @@ -118,6 +116,8 @@ class Agent: }) await run.on_async('output', on_output_event) + + await self.run(run) return run def _role_for_message(self, message): diff --git a/backend/src/agents/expert.py b/backend/src/agents/expert.py index 76ed296..71de467 100644 --- a/backend/src/agents/expert.py +++ b/backend/src/agents/expert.py @@ -24,11 +24,14 @@ class ExpertAgent(Agent): async def run(self, run): opts = {} - using_tools = False + # Is this roon using *Agent's* tools, as opposed to run tools + using_agent_tools = False if 'tools' not in run.input: + # Agent tools will fully replace run tools if none are present, + # otherwise they won't be used if self.tools: run.input['tools'] = self.tools - using_tools = True + using_agent_tools = True if self.config.get("temperature") is not None: opts["temperature"] = self.config["temperature"] @@ -52,12 +55,11 @@ class ExpertAgent(Agent): output_stream = stream run.on('output', on_output) run.on('inference-end', lambda event: inference_end.set()) - run.on('tool-call', lambda tc: tool_calls.append(tc)) + run.on('tool-call', lambda tc: print(f"[expert] tool call: {tc}") or tool_calls.append(tc)) while True: # Reset data inference_end.clear() - tool_calls = [] output_stream = None # Call inference, passing the run to get events out @@ -73,8 +75,10 @@ class ExpertAgent(Agent): await inference_end.wait() messages.append(inference.LlamaMessage('assistant', output_stream and await output_stream.content_wait() or "")) + print(f"[expert] collected tool calls: {len(tool_calls)}, using? {using_agent_tools}") - if using_tools and tool_calls: + if using_agent_tools and tool_calls: + print("Will execute tool calls") for tc in tool_calls: tool = next(t for t in self.tools if t.name == tc.tool_name) if not tool: @@ -82,7 +86,11 @@ class ExpertAgent(Agent): continue tc.result = await tool.execute_async(**tc.arguments) messages.append(inference.LlamaMessage('tool', tc)) + tool_calls = [] + # Buffer time to let openrouter rest? + await asyncio.sleep(2) else: + print("No tool calls to execute, finish") break run.close() diff --git a/backend/src/inference.py b/backend/src/inference.py index 5860fff..e548f19 100644 --- a/backend/src/inference.py +++ b/backend/src/inference.py @@ -57,7 +57,7 @@ def format_openai(messages): formatted.append({ 'role': 'tool', 'tool_call_id': msg.content.id, - 'content': json.dumps(msg.content.output), + 'content': json.dumps(msg.content.result), }) else: fmsg = {'role': msg.role, 'content': msg.content} @@ -337,6 +337,9 @@ async def openrouter_chat_completion(app, messages, model, tools=[], opts={}, ag 'model': model, 'messages': format_openai(messages), 'stream': True, + # Options + 'timeout_ms': 45_000, + 'retries': 3, } # Pass tools in OpenAI-compatible format if provided @@ -344,63 +347,64 @@ async def openrouter_chat_completion(app, messages, model, tools=[], opts={}, ag if formatted_tools: kwargs['tools'] = formatted_tools - try: - stream_response = await app.openrouter_client.chat.send_async(**kwargs) + stream_response = await app.openrouter_client.chat.send_async(**kwargs) - # OpenAI format streams tool calls in parts - # We could, but I won't assume any tool call is complete until the message is over - tool_calls = [] + # OpenAI format streams tool calls in parts + # We could, but I won't assume any tool call is complete until the message is over + tool_calls = [] - async for event in stream_response: - if agent_run and agent_run.cancelled: - print(f"Cancelling openrouter inference for run {agent_run.id}") - # TODO: Need to cancell? - break - - if event.choices and len(event.choices) > 0: - delta = event.choices[0].delta + async for event in stream_response: + if agent_run and agent_run.cancelled: + print(f"Cancelling openrouter inference for run {agent_run.id}") + # TODO: Need to cancell? + break + + if event.choices and len(event.choices) > 0: + delta = event.choices[0].delta - if delta.content: - emit('output', delta.content) - if delta.reasoning: - emit('reasoning', delta.reasoning) - if delta.tool_calls: - print("[openrouter] tool call:" + repr(delta.tool_calls)) - for tc in delta.tool_calls: - if tc.index >= len(tool_calls): - tool_calls.append(ToolCall(tool_name="", arguments="", id="")) - tool_call = tool_calls[tc.index] + if delta.content: + emit('output', delta.content) + if delta.reasoning: + emit('reasoning', delta.reasoning) + if delta.tool_calls: + print("[openrouter] tool call:" + repr(delta.tool_calls)) + for tc in delta.tool_calls: + if tc.index >= len(tool_calls): + tool_calls.append(ToolCall(tool_name="", arguments="", id="")) + tool_call = tool_calls[tc.index] - if tc.id: - tool_call.id += tc.id + if tc.id: + tool_call.id += tc.id - if tc.function: - if tc.function.name: - tool_call.tool_name += tc.function.name - if tc.function.arguments: - tool_call.arguments += tc.function.arguments - else: - print("[openrouter] no event choices: " + repr(event)) + if tc.function: + if tc.function.name: + tool_call.tool_name += tc.function.name + if tc.function.arguments: + tool_call.arguments += tc.function.arguments + else: + print("[openrouter] no event choices: " + repr(event)) - for tc in tool_calls: - try: - arguments = json.loads(tc.arguments) - tc.arguments = arguments - emit('tool-call', tc) - except Exception: - print("[openrouter] Bad tool call arguments for " + repr(tc)) - print(traceback.format_exc()) - except asyncio.CancelledError: - print(f"[openrouter] task cancelled for run {agent_run.id}") - raise - except Exception: - print(f"[openrouter] exception during inference") - print(traceback.format_exc()) + for tc in tool_calls: + try: + arguments = json.loads(tc.arguments) + tc.arguments = arguments + emit('tool-call', tc) + except Exception: + print("[openrouter] Bad tool call arguments for " + repr(tc)) + print(traceback.format_exc()) emit('close') print("END OPENROUTER INFERENCE") + def handle_done(task): + try: + task.result() + except Exception as e: + print(f"[openrouter] Worker error: {e}") + print(traceback.format_exc()) + task = asyncio.create_task(worker()) + task.add_done_callback(handle_done) agent_run.task = task diff --git a/backend/src/server.py b/backend/src/server.py index 7005e8b..aaf86e4 100644 --- a/backend/src/server.py +++ b/backend/src/server.py @@ -111,6 +111,7 @@ async def start_server(app, shutdown_event): try: user_id = auth_manager.load_token(auth_req['token']) if not await app.verify_user_id(user_id): + print("Invalid Token"); await websocket.send(json.dumps({'error': 'invalid credentials'})) return except Exception as err: @@ -125,6 +126,7 @@ async def start_server(app, shutdown_event): user_id = app.verify_user_credentials(auth_req['username'], auth_req['password']) if user_id is None: + print("Invalid Credentials"); await websocket.send(json.dumps({'error': 'invalid credentials'})) return diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index 7066b0e..e3f21d0 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -10,6 +10,14 @@ function objectView (name, mod) { } } +function staticView (name, mod) { + return { + name, + path: `/${name}`, + component: mod + } +} + // These routers must be handled by the server by rendering the index page // file: backend/server.py const router = createRouter({ @@ -50,6 +58,7 @@ const router = createRouter({ }, objectView('chat', () => import('../views/Chat.vue')), objectView('agent', () => import('../views/Agent.vue')), + staticView('user', () => import('../views/User.vue')), ], })