203 lines
4.9 KiB
Python
203 lines
4.9 KiB
Python
|
|
"""Tool system for agent capabilities."""
|
||
|
|
|
||
|
|
import asyncio
|
||
|
|
import logging
|
||
|
|
from abc import ABC, abstractmethod
|
||
|
|
from typing import Any
|
||
|
|
|
||
|
|
logger = logging.getLogger(__name__)
|
||
|
|
|
||
|
|
|
||
|
|
class Tool(ABC):
|
||
|
|
"""Abstract base class for agent tools."""
|
||
|
|
|
||
|
|
@property
|
||
|
|
@abstractmethod
|
||
|
|
def name(self) -> str:
|
||
|
|
"""Tool name used in function calls."""
|
||
|
|
pass
|
||
|
|
|
||
|
|
@property
|
||
|
|
@abstractmethod
|
||
|
|
def description(self) -> str:
|
||
|
|
"""Description of what the tool does."""
|
||
|
|
pass
|
||
|
|
|
||
|
|
@property
|
||
|
|
@abstractmethod
|
||
|
|
def parameters(self) -> dict[str, Any]:
|
||
|
|
"""JSON Schema for tool parameters."""
|
||
|
|
pass
|
||
|
|
|
||
|
|
@abstractmethod
|
||
|
|
async def execute(self, **kwargs: Any) -> str:
|
||
|
|
"""Execute the tool with given parameters.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
String result of the tool execution.
|
||
|
|
"""
|
||
|
|
pass
|
||
|
|
|
||
|
|
def to_schema(self) -> dict[str, Any]:
|
||
|
|
"""Convert tool to function schema format."""
|
||
|
|
return {
|
||
|
|
"type": "function",
|
||
|
|
"function": {
|
||
|
|
"name": self.name,
|
||
|
|
"description": self.description,
|
||
|
|
"parameters": self.parameters,
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
class ToolRegistry:
|
||
|
|
"""Registry for managing agent tools."""
|
||
|
|
|
||
|
|
def __init__(self):
|
||
|
|
self._tools: dict[str, Tool] = {}
|
||
|
|
|
||
|
|
def register(self, tool: Tool) -> None:
|
||
|
|
"""Register a tool.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
tool: Tool instance to register
|
||
|
|
"""
|
||
|
|
self._tools[tool.name] = tool
|
||
|
|
logger.info(f"Registered tool: {tool.name}")
|
||
|
|
|
||
|
|
def unregister(self, name: str) -> None:
|
||
|
|
"""Unregister a tool.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
name: Tool name to unregister
|
||
|
|
"""
|
||
|
|
if name in self._tools:
|
||
|
|
del self._tools[name]
|
||
|
|
logger.info(f"Unregistered tool: {name}")
|
||
|
|
|
||
|
|
def get(self, name: str) -> Tool | None:
|
||
|
|
"""Get a tool by name.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
name: Tool name
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Tool instance or None
|
||
|
|
"""
|
||
|
|
return self._tools.get(name)
|
||
|
|
|
||
|
|
def get_definitions(self) -> list[dict[str, Any]]:
|
||
|
|
"""Get all tool definitions for LLM.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
List of tool schemas
|
||
|
|
"""
|
||
|
|
return [tool.to_schema() for tool in self._tools.values()]
|
||
|
|
|
||
|
|
async def execute(self, name: str, arguments: dict[str, Any]) -> str:
|
||
|
|
"""Execute a tool.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
name: Tool name
|
||
|
|
arguments: Tool arguments
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Tool execution result
|
||
|
|
"""
|
||
|
|
tool = self.get(name)
|
||
|
|
if not tool:
|
||
|
|
return f'{{"error": "Unknown tool: {name}"}}'
|
||
|
|
|
||
|
|
try:
|
||
|
|
# Validate parameters
|
||
|
|
validated = tool.cast_params(arguments)
|
||
|
|
errors = tool.validate_params(validated)
|
||
|
|
if errors:
|
||
|
|
return f'{{"error": "Parameter validation failed: {errors}"}}'
|
||
|
|
|
||
|
|
# Execute with timeout
|
||
|
|
result = await asyncio.wait_for(
|
||
|
|
tool.execute(**validated),
|
||
|
|
timeout=60.0,
|
||
|
|
)
|
||
|
|
return result
|
||
|
|
except asyncio.TimeoutError:
|
||
|
|
return f'{{"error": "Tool execution timed out: {name}"}}'
|
||
|
|
except Exception as exc:
|
||
|
|
logger.exception(f"Tool execution error: {name}")
|
||
|
|
return f'{{"error": "Tool execution failed: {exc}"}}'
|
||
|
|
|
||
|
|
def list_tools(self) -> list[str]:
|
||
|
|
"""List all registered tool names.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
List of tool names
|
||
|
|
"""
|
||
|
|
return list(self._tools.keys())
|
||
|
|
|
||
|
|
|
||
|
|
# Built-in placeholder tools
|
||
|
|
class EchoTool(Tool):
|
||
|
|
"""Echo tool for testing."""
|
||
|
|
|
||
|
|
@property
|
||
|
|
def name(self) -> str:
|
||
|
|
return "echo"
|
||
|
|
|
||
|
|
@property
|
||
|
|
def description(self) -> str:
|
||
|
|
return "Echo back the input text. Useful for testing."
|
||
|
|
|
||
|
|
@property
|
||
|
|
def parameters(self) -> dict[str, Any]:
|
||
|
|
return {
|
||
|
|
"type": "object",
|
||
|
|
"properties": {
|
||
|
|
"text": {
|
||
|
|
"type": "string",
|
||
|
|
"description": "Text to echo back",
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"required": ["text"],
|
||
|
|
}
|
||
|
|
|
||
|
|
async def execute(self, **kwargs: Any) -> str:
|
||
|
|
text = kwargs.get("text", "")
|
||
|
|
return f'{{"echo": "{text}"}}'
|
||
|
|
|
||
|
|
|
||
|
|
class TimeTool(Tool):
|
||
|
|
"""Get current time tool."""
|
||
|
|
|
||
|
|
@property
|
||
|
|
def name(self) -> str:
|
||
|
|
return "get_time"
|
||
|
|
|
||
|
|
@property
|
||
|
|
def description(self) -> str:
|
||
|
|
return "Get the current date and time."
|
||
|
|
|
||
|
|
@property
|
||
|
|
def parameters(self) -> dict[str, Any]:
|
||
|
|
return {
|
||
|
|
"type": "object",
|
||
|
|
"properties": {},
|
||
|
|
}
|
||
|
|
|
||
|
|
async def execute(self, **kwargs: Any) -> str:
|
||
|
|
from datetime import datetime
|
||
|
|
now = datetime.now()
|
||
|
|
return f'{{"time": "{now.isoformat()}"}}'
|
||
|
|
|
||
|
|
|
||
|
|
def create_default_registry() -> ToolRegistry:
|
||
|
|
"""Create a tool registry with default tools.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Tool registry with built-in tools
|
||
|
|
"""
|
||
|
|
registry = ToolRegistry()
|
||
|
|
registry.register(EchoTool())
|
||
|
|
registry.register(TimeTool())
|
||
|
|
return registry
|