wip(backend/tool): tool collection and execution is now agents job

This commit is contained in:
Shivering Ocelot committed 2026-09-30 08:20:44 -05:00
1 parent f3a8376df6
commit 2ba8096a3c
2 files changed
+100 -84

No files matched your search

+12
View File
@@ -183,6 +183,18 @@ class Agent:
return [r for r in self.resources return [r for r in self.resources
if r.kind == kind and (key is None or r.key == key)] if r.kind == kind and (key is None or r.key == key)]
def get_tools(self, run=None):
"""Collect the tools available to this agent.
Returns the agent-configured tools (derived from its resources) plus
any remote tools referenced by the run input. When a run is given,
remote names in ``run.input['remotes']`` are resolved and appended.
"""
tools = list(self.tools)
if run is not None and 'remotes' in run.input:
tools += self.app.getRemoteTools(self.user_id, run.input['remotes'])
return tools
class DebugAgent(Agent): class DebugAgent(Agent):
identifier = "debug" identifier = "debug"
+88 -84
View File
@@ -8,6 +8,77 @@ Expert AI with customizable system prompt.
Will get guidelines, documents, and other advanced features in the future. Will get guidelines, documents, and other advanced features in the future.
""".strip() """.strip()
# This needs to move to inference, just like build_messages
async def chat_completion_loop(app, run, messages, model_id=None, tools=None):
"""Drive ``inference.chat_completion`` until the model is finished.
Wires the run's output / reasoning / tool-call events, executes requested
tools when ``tool_policy`` is 'auto', and feeds tool results back into
``messages`` (mutated in place) so the model can continue. Returns once the
model produces a final answer with no further tool calls.
"""
tools = list(tools or [])
execute_tools = run.input.get('tool_policy', 'auto') == 'auto'
tool_calls = []
output_stream = None
reasoning_stream = None
inference_end = asyncio.Event()
def on_output(stream):
nonlocal output_stream
output_stream = stream
def on_reasoning(stream):
nonlocal reasoning_stream
reasoning_stream = stream
run.on('output', on_output)
run.on('reasoning', on_reasoning)
run.on('tool-call', tool_calls.append)
run.on('inference-end', lambda _: inference_end.set())
while True:
# Reset per-iteration state
output_stream = None
reasoning_stream = None
inference_end.clear()
await inference.chat_completion(
app,
model_id=model_id,
messages=messages,
tools=tools,
agent_run=run,
)
await inference_end.wait()
content = output_stream and await output_stream.content_wait()
reasoning = reasoning_stream and await reasoning_stream.content_wait()
if content or reasoning:
msg = inference.LlamaMessage('assistant', content or "")
if reasoning:
msg.reasoning = reasoning
messages.append(msg)
if execute_tools and tool_calls:
for tc in tool_calls:
tool = next((t for t in tools if t.name == tc.tool_name), None)
if tool is None:
print(f"[agent] unknown tool name: {tc.tool_name}")
continue
tc.result = await tool.execute_async(**tc.arguments)
messages.append(inference.LlamaMessage('tool', tc))
tool_calls.clear()
# Buffer time to let the provider rest before the next call
await asyncio.sleep(2)
elif content or not reasoning:
break
run.close()
class ExpertAgent(Agent): class ExpertAgent(Agent):
identifier = "expert" identifier = "expert"
description = CLASS_DESCRIPTION description = CLASS_DESCRIPTION
@@ -26,36 +97,22 @@ class ExpertAgent(Agent):
Agent.__init__(self, *args) Agent.__init__(self, *args)
async def run(self, run): async def run(self, run):
opts = {} # Apply agent configuration overrides to the run input
for key in ("temperature", "repetition_penalty", "reasoning_effort"):
if self.config.get(key) is not None:
run.input[key] = self.config[key]
# Is this roon using *Agent's* tools, as opposed to run tools # Provide the agent's default tools unless the run supplies its own.
using_agent_tools = False # Only agent-provided tools are executable within the loop.
tools = run.input.get('tools', []) tools = run.input.get("tools", [])
if tools:
run.input['tool_policy'] = 'ignore'
else:
tools += self.get_tools(run)
if not tools: print("Final tool object: " + repr(run.input.get('tools', [])))
# Agent tools will fully replace run tools if none are present,
# otherwise they won't be used
if self.tools:
tools = self.tools
using_agent_tools = True
# Inject remotes as tools # Build the initial message list
if 'remotes' in run.input:
tools += self.app.getRemoteTools(self.user_id, run.input['remotes'])
using_agent_tools = True
print("Final tool object: " + repr(tools))
if tools: run.input['tools'] = tools
if self.config.get("temperature") is not None:
run.input["temperature"] = self.config["temperature"]
if self.config.get("repetition_penalty") is not None:
run.input["repetition_penalty"] = self.config["repetition_penalty"]
if self.config.get("reasoning_effort") is not None:
run.input["reasoning_effort"] = self.config["reasoning_effort"]
# Build message list based on input
messages = inference.build_messages( messages = inference.build_messages(
system_prompt=self.config.get("system_prompt", ""), system_prompt=self.config.get("system_prompt", ""),
instruction=run.input.get("instruction"), instruction=run.input.get("instruction"),
@@ -63,63 +120,10 @@ class ExpertAgent(Agent):
messages=run.input.get("messages"), messages=run.input.get("messages"),
) )
# Collect inference output. These are resused across calls await chat_completion_loop(
tool_calls = []
output_stream = None
reasoning_stream = None
inference_end = asyncio.Event()
def on_output(stream):
nonlocal output_stream
output_stream = stream
def on_reasoning(stream):
nonlocal reasoning_stream
reasoning_stream = stream
run.on('output', on_output)
run.on('reasoning', on_reasoning)
run.on('inference-end', lambda event: inference_end.set())
run.on('tool-call', lambda tc: print(f"[expert] tool call: {tc}") or tool_calls.append(tc))
while True:
# Reset data
inference_end.clear()
output_stream = None
# Call inference, passing the run to get events out
await inference.chat_completion(
self.app, self.app,
run,
messages,
model_id=self.config.get("model"), model_id=self.config.get("model"),
messages=messages, tools=tools,
tools=run.input.get("tools", []),
opts=opts,
agent_run=run,
) )
await inference_end.wait()
content = output_stream and await output_stream.content_wait();
reasoning = reasoning_stream and await reasoning_stream.content_wait();
if content or reasoning:
msg = inference.LlamaMessage('assistant', content or "")
if reasoning: msg.reasoning = reasoning
messages.append(msg)
print(f"[expert] collected tool calls: {len(tool_calls)}, using? {using_agent_tools}")
if using_agent_tools and tool_calls:
print("Will execute tool calls")
for tc in tool_calls:
try:
tool = next(t for t in tools if t.name == tc.tool_name)
if not tool: raise StopIteration()
tc.result = await tool.execute_async(**tc.arguments)
messages.append(inference.LlamaMessage('tool', tc))
except StopIteration:
print(f"Bad agent tool name: {tc.name}")
tool_calls = []
# Buffer time to let openrouter rest?
await asyncio.sleep(2)
elif content or not reasoning:
print("No tool calls to execute, finish")
break
run.close()