wip: add tools to agent control and improve tool class
This commit is contained in:
1 parent
6134c026a6
commit
9585008433
7 files changed
+144
-13
No files matched your search
+20
-2
@@ -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):
|
||||
@@ -226,6 +238,12 @@ class AgentRun:
|
||||
self.emit_event('reasoning', self.reasoning)
|
||||
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})
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
Whitespace-only changes.
@@ -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
@@ -1,3 +1,6 @@
|
||||
import inspect
|
||||
from typing import get_type_hints
|
||||
|
||||
class ToolParameter:
|
||||
"""Describes a single input parameter for a tool.
|
||||
|
||||
@@ -55,6 +58,14 @@ class ToolSpec:
|
||||
self.description = description
|
||||
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 -------------------------------------------
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@classmethod
|
||||
def tool_name(cls):
|
||||
return ''.join(['_'+c.lower() if c.isupper() else c for c in cls.__name__]).lstrip('_')
|
||||
|
||||
def to_spec(self):
|
||||
"""Generate a ToolSpec from this tool instance."""
|
||||
|
||||
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.
|
||||
|
||||
Reference in new issue
Block a user