wip(backend/tool): tool collection and execution is now agents job
This commit is contained in:
1 parent
f3a8376df6
commit
2ba8096a3c
2 files changed
+103
-87
No files matched your search
@@ -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"
|
||||||
|
|||||||
@@ -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 = []
|
self.app,
|
||||||
output_stream = None
|
run,
|
||||||
reasoning_stream = None
|
messages,
|
||||||
inference_end = asyncio.Event()
|
model_id=self.config.get("model"),
|
||||||
def on_output(stream):
|
tools=tools,
|
||||||
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,
|
|
||||||
model_id=self.config.get("model"),
|
|
||||||
messages=messages,
|
|
||||||
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()
|
|
||||||
Reference in new issue
Block a user