wip: add tools to agent control and improve tool class

This commit is contained in:
Shivering Ocelot committed 2026-06-08 05:18:38 -05:00
1 parent 6134c026a6
commit 9585008433
7 files changed
+144 -13

No files matched your search

+20 -2
View File
@@ -21,7 +21,7 @@ class Agent:
# - identifier (for kind in creation)
# - schema: list[AgentField{name, type: str}] (for configuration)
def __init__(self, app, data):
def __init__(self, app, data, resources=[]):
self.app = app
# These variables are set by '_set_basic_info'
@@ -31,6 +31,18 @@ class Agent:
# Auto populated based on the schema fields
agent_class = type(self)
self.config = {field.name: data.get(field.name, field.default) for field in getattr(agent_class, 'schema_fields', [])}
self.resources = resources
self.tools = []
for resource in self.resources:
if resource.kind != 'tool': continue
if resource.value.get('type') == 'native' and 'name' in resource.value:
tool_cls = next((t for t in self.app.tool_classes if t.tool_name() == resource.value['name']), None)
if tool_cls:
tool = tool_cls()
spec = tool.get_spec().clone()
spec.name = resource.key
self.tools.append(spec)
def _set_basic_info(self, id, name, user_id):
self.id = id
@@ -110,7 +122,7 @@ class Agent:
# Returns all resource values matching the kind and key
def get_resources(self, kind, key):
self.app
pass # TODO lol
class DebugAgent(Agent):
@@ -227,6 +239,12 @@ class AgentRun:
if chunk:
self.reasoning.feed(chunk)
def add_tools(self, tools):
if "tools" in self.input:
self.input["tools"] += tools
else:
self.input["tools"] = tools
def add_tool_call(self, tool_name, arguments):
self.emit_event("tool_call", {"name": tool_name, "arguments": arguments})
+6 -2
View File
@@ -16,12 +16,16 @@ class ExpertAgent(Agent):
AgentField('repetition_penalty', 'number', default=None),
]
def __init__(self, app, data):
Agent.__init__(self, app, data)
def __init__(self, *args):
Agent.__init__(self, *args)
async def run(self, run):
opts = {}
agent_tools = self.tools
print('agent tools ' + repr(agent_tools))
run.add_tools(agent_tools)
if self.config.get("temperature") is not None:
opts["temperature"] = self.config["temperature"]
if self.config.get("repetition_penalty") is not None:
+5 -1
View File
@@ -41,6 +41,9 @@ class App(ChatMixin):
agents_mod.ExpertAgent,
]
from features.calc import Calc
self.tool_classes = [Calc]
# --- Connection Management (Synchronous) ---
def on_event(self, callback):
@@ -231,12 +234,13 @@ class App(ChatMixin):
async def get_agent_control(self, agent_id):
agent = await self.get_agent(agent_id)
resources = await self.get_agent_resources(agent_id)
agent_class = next((kind for kind in self.agent_classes if kind.identifier == agent.kind), None)
if agent_class is None:
raise Exception(f"invalid agent kind: {agent.kind}")
control = agent_class(self, agent.data)
control = agent_class(self, agent.data, resources)
control._set_basic_info(agent_id, agent.name, agent.user_id)
return control
View File
Whitespace-only changes.
+38
View File
@@ -0,0 +1,38 @@
import sys
import os
from tool import Tool
class Calc(Tool):
"""
A calculator tool that evaluates mathematical expressions.
This tool can evaluate basic arithmetic operations (+, -, *, /, **, etc.)
and more complex mathematical expressions using Python's eval function.
"""
def run(self, expression: str) -> str:
"""
Evaluate a mathematical expression and return the result.
Args:
expression: A string containing a mathematical expression to evaluate
Returns:
The result of the expression as a string
Raises:
Exception: If the expression cannot be evaluated
"""
try:
# Only allow mathematical operations for safety
allowed_names = {
'abs': abs, 'round': round,
'min': min, 'max': max,
'sum': sum, 'pow': pow,
'int': int, 'float': float,
'__builtins__': {}
}
result = eval(expression, {"__builtins__": None}, allowed_names)
return str(result)
except Exception as e:
return f"Error evaluating expression: {str(e)}"
+74 -8
View File
@@ -1,3 +1,6 @@
import inspect
from typing import get_type_hints
class ToolParameter:
"""Describes a single input parameter for a tool.
@@ -56,6 +59,14 @@ class ToolSpec:
self.parameters = parameters or []
self.func = func
def clone (self):
return ToolSpec(
name = self.name,
description = self.description,
parameters = self.parameters,
func = self.func,
)
# -- serialization helpers -------------------------------------------
def to_openai_dict(self):
@@ -119,26 +130,81 @@ class Tool:
"""Base class for tools that implement their logic in a 'run' method.
Subclasses should override 'run' to provide the tool's functionality.
The Tool class provides a method to_spec() that automatically generates
a ToolSpec from the class name, docstring, and run method signature.
This allows tools to be used with LLM function calling without manually
creating ToolSpec objects. The generated ToolSpec is cached at initialization.
"""
def __init__(self):
pass
def __init__(self, name=None):
# Generate and cache the ToolSpec during initialization
self.name = name or self.__class__.tool_name()
self._spec = self._generate_spec()
def run(self, **kwargs):
"""Execute the tool. To be overridden by subclasses."""
raise NotImplementedError("Tool subclasses must override the 'run' method.")
def to_spec(self):
"""Generate a ToolSpec from this tool instance."""
@classmethod
def tool_name(cls):
return ''.join(['_'+c.lower() if c.isupper() else c for c in cls.__name__]).lstrip('_')
def _generate_spec(self):
"""Generate a ToolSpec from this tool instance.
Automatically creates a ToolSpec by:
- Using the class name (converted to snake_case) as the tool name
- Using the class docstring as the description
- Inspecting the run method signature to create parameters
"""
# Use the class docstring as the description
description = inspect.getdoc(self.__class__) or ""
# Get parameters from the run method signature
parameters = []
run_signature = inspect.signature(self.run)
type_hints = get_type_hints(self.run)
for param_name, param in run_signature.parameters.items():
# Skip 'self' parameter
if param_name == 'self':
continue
# Get type information
param_type = type_hints.get(param_name, "string")
if isinstance(param_type, type):
type_name = param_type.__name__.lower()
else:
type_name = str(param_type).lower()
# Check if parameter is required (no default or default is None)
is_required = param.default is inspect.Parameter.empty or param.default is None
# Create ToolParameter
tool_param = ToolParameter(
name=param_name,
type=type_name,
description="",
required=is_required
)
parameters.append(tool_param)
return ToolSpec(
name=None, # TODO: Computed from class name, or from class attribute
description=None, # TODO: From class doc comment
parameters=None, # TODO: Class attribute
name=self.name,
description=description,
parameters=parameters,
func=self.run,
)
def get_spec(self):
"""Return the cached ToolSpec for this tool."""
return self._spec
def __repr__(self):
return f"Tool(name={self.name!r}, params={[p.name for p in self.parameters]})"
return f"Tool(name={self._spec.name!r}, params={[p.name for p in self._spec.parameters]})"
class ToolCall:
"""Represents a single tool invocation request emitted by the model.
+1
View File
@@ -95,6 +95,7 @@ async function init() {
runs = res.runs || [];
// Listen for run events
if (unsubscribeRunEvents) unsubscribeRunEvents();
unsubscribeRunEvents = backend.onAction('run-event', updateRun);
}