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)
|
# - identifier (for kind in creation)
|
||||||
# - schema: list[AgentField{name, type: str}] (for configuration)
|
# - schema: list[AgentField{name, type: str}] (for configuration)
|
||||||
|
|
||||||
def __init__(self, app, data):
|
def __init__(self, app, data, resources=[]):
|
||||||
self.app = app
|
self.app = app
|
||||||
|
|
||||||
# These variables are set by '_set_basic_info'
|
# These variables are set by '_set_basic_info'
|
||||||
@@ -31,6 +31,18 @@ class Agent:
|
|||||||
# Auto populated based on the schema fields
|
# Auto populated based on the schema fields
|
||||||
agent_class = type(self)
|
agent_class = type(self)
|
||||||
self.config = {field.name: data.get(field.name, field.default) for field in getattr(agent_class, 'schema_fields', [])}
|
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):
|
def _set_basic_info(self, id, name, user_id):
|
||||||
self.id = id
|
self.id = id
|
||||||
@@ -110,7 +122,7 @@ class Agent:
|
|||||||
|
|
||||||
# Returns all resource values matching the kind and key
|
# Returns all resource values matching the kind and key
|
||||||
def get_resources(self, kind, key):
|
def get_resources(self, kind, key):
|
||||||
self.app
|
pass # TODO lol
|
||||||
|
|
||||||
|
|
||||||
class DebugAgent(Agent):
|
class DebugAgent(Agent):
|
||||||
@@ -226,6 +238,12 @@ class AgentRun:
|
|||||||
self.emit_event('reasoning', self.reasoning)
|
self.emit_event('reasoning', self.reasoning)
|
||||||
if chunk:
|
if chunk:
|
||||||
self.reasoning.feed(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):
|
def add_tool_call(self, tool_name, arguments):
|
||||||
self.emit_event("tool_call", {"name": tool_name, "arguments": arguments})
|
self.emit_event("tool_call", {"name": tool_name, "arguments": arguments})
|
||||||
|
|||||||
@@ -16,12 +16,16 @@ class ExpertAgent(Agent):
|
|||||||
AgentField('repetition_penalty', 'number', default=None),
|
AgentField('repetition_penalty', 'number', default=None),
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, app, data):
|
def __init__(self, *args):
|
||||||
Agent.__init__(self, app, data)
|
Agent.__init__(self, *args)
|
||||||
|
|
||||||
async def run(self, run):
|
async def run(self, run):
|
||||||
opts = {}
|
opts = {}
|
||||||
|
|
||||||
|
agent_tools = self.tools
|
||||||
|
print('agent tools ' + repr(agent_tools))
|
||||||
|
run.add_tools(agent_tools)
|
||||||
|
|
||||||
if self.config.get("temperature") is not None:
|
if self.config.get("temperature") is not None:
|
||||||
opts["temperature"] = self.config["temperature"]
|
opts["temperature"] = self.config["temperature"]
|
||||||
if self.config.get("repetition_penalty") is not None:
|
if self.config.get("repetition_penalty") is not None:
|
||||||
|
|||||||
+5
-1
@@ -41,6 +41,9 @@ class App(ChatMixin):
|
|||||||
agents_mod.ExpertAgent,
|
agents_mod.ExpertAgent,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
from features.calc import Calc
|
||||||
|
self.tool_classes = [Calc]
|
||||||
|
|
||||||
# --- Connection Management (Synchronous) ---
|
# --- Connection Management (Synchronous) ---
|
||||||
|
|
||||||
def on_event(self, callback):
|
def on_event(self, callback):
|
||||||
@@ -231,12 +234,13 @@ class App(ChatMixin):
|
|||||||
|
|
||||||
async def get_agent_control(self, agent_id):
|
async def get_agent_control(self, agent_id):
|
||||||
agent = await self.get_agent(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)
|
agent_class = next((kind for kind in self.agent_classes if kind.identifier == agent.kind), None)
|
||||||
if agent_class is None:
|
if agent_class is None:
|
||||||
raise Exception(f"invalid agent kind: {agent.kind}")
|
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)
|
control._set_basic_info(agent_id, agent.name, agent.user_id)
|
||||||
return control
|
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:
|
class ToolParameter:
|
||||||
"""Describes a single input parameter for a tool.
|
"""Describes a single input parameter for a tool.
|
||||||
|
|
||||||
@@ -55,6 +58,14 @@ class ToolSpec:
|
|||||||
self.description = description
|
self.description = description
|
||||||
self.parameters = parameters or []
|
self.parameters = parameters or []
|
||||||
self.func = func
|
self.func = func
|
||||||
|
|
||||||
|
def clone (self):
|
||||||
|
return ToolSpec(
|
||||||
|
name = self.name,
|
||||||
|
description = self.description,
|
||||||
|
parameters = self.parameters,
|
||||||
|
func = self.func,
|
||||||
|
)
|
||||||
|
|
||||||
# -- serialization helpers -------------------------------------------
|
# -- serialization helpers -------------------------------------------
|
||||||
|
|
||||||
@@ -119,26 +130,81 @@ class Tool:
|
|||||||
"""Base class for tools that implement their logic in a 'run' method.
|
"""Base class for tools that implement their logic in a 'run' method.
|
||||||
|
|
||||||
Subclasses should override 'run' to provide the tool's functionality.
|
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):
|
def __init__(self, name=None):
|
||||||
pass
|
# Generate and cache the ToolSpec during initialization
|
||||||
|
self.name = name or self.__class__.tool_name()
|
||||||
|
self._spec = self._generate_spec()
|
||||||
|
|
||||||
def run(self, **kwargs):
|
def run(self, **kwargs):
|
||||||
"""Execute the tool. To be overridden by subclasses."""
|
"""Execute the tool. To be overridden by subclasses."""
|
||||||
raise NotImplementedError("Tool subclasses must override the 'run' method.")
|
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(
|
return ToolSpec(
|
||||||
name=None, # TODO: Computed from class name, or from class attribute
|
name=self.name,
|
||||||
description=None, # TODO: From class doc comment
|
description=description,
|
||||||
parameters=None, # TODO: Class attribute
|
parameters=parameters,
|
||||||
func=self.run,
|
func=self.run,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_spec(self):
|
||||||
|
"""Return the cached ToolSpec for this tool."""
|
||||||
|
return self._spec
|
||||||
|
|
||||||
def __repr__(self):
|
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:
|
class ToolCall:
|
||||||
"""Represents a single tool invocation request emitted by the model.
|
"""Represents a single tool invocation request emitted by the model.
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ async function init() {
|
|||||||
runs = res.runs || [];
|
runs = res.runs || [];
|
||||||
|
|
||||||
// Listen for run events
|
// Listen for run events
|
||||||
|
if (unsubscribeRunEvents) unsubscribeRunEvents();
|
||||||
unsubscribeRunEvents = backend.onAction('run-event', updateRun);
|
unsubscribeRunEvents = backend.onAction('run-event', updateRun);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user