Agent Tools
Tools extend agent capabilities with executable functions. There are two types: internal tools and external tools. Internal tools are Python functions the agent can call directly; external tools follow OpenAI's tool-calling protocol and are executed by the client.
Tool Types
Internal Tools
Internal tools are executed within the agent's process. They can be:
- Skill Tools - Defined in skills using
@tooldecorator - Standalone Tools - Decorated functions passed to agent
External Tools
External tools are defined in the request and executed on the client side. The agent will emit OpenAI tool calls; your client is responsible for executing them and returning results in a follow-up message. This keeps server responsibilities minimal while remaining compatible with OpenAI tooling.
HTTP Endpoints
For creating custom HTTP API endpoints, see Agent Endpoints which covers the @http decorator and REST API creation.
Internal Tools
Standalone Tools
from webagents import BaseAgent, tool
# Define standalone tool functions
@tool
def calculate(expression: str) -> str:
"""Calculate mathematical expressions"""
try:
result = eval(expression, {"__builtins__": {}}, {})
return str(result)
except:
return "Invalid expression"
@tool(scope="owner")
def admin_function(action: str) -> str:
"""Owner-only administrative function"""
return f"Admin action: {action}"
# Pass to agent
agent = BaseAgent(
name="my-agent",
model="openai/gpt-4o",
tools=[calculate, admin_function] # Internal tools
)
Capabilities Auto-Registration
Use the capabilities parameter to automatically register decorated functions:
from webagents import tool, hook, handoff
@tool(scope="owner")
def my_tool(message: str) -> str:
return f"Tool: {message}"
@tool
def another_tool(data: str) -> str:
return f"Processed: {data}"
@hook("on_request", priority=10)
def my_hook(context):
return context
# Auto-register all decorated functions
agent = BaseAgent(
name="capable-agent",
model="openai/gpt-4o",
capabilities=[my_tool, another_tool, my_hook]
)
The agent will automatically categorize and register each function based on its decorator type. Tools will be registered as callable functions for the LLM.
Handoff System
Handoffs are handled via skill registration during initialization. Instead of using @handoff decorator on standalone functions, handoffs are registered within skills. See Agent Handoffs for the handoff system.
Skill Tools
from webagents import Skill, tool
class CalculatorSkill(Skill):
@tool
def add(self, a: float, b: float) -> float:
"""Add two numbers"""
return a + b
@tool(scope="owner")
def multiply(self, x: float, y: float) -> float:
"""Multiply two numbers (owner only)"""
return x * y
Tool Parameters
@tool(
name="custom_name", # Override function name
description="Custom", # Override docstring
scope="all", # Access control: all/owner/admin
# For priced tools, prefer the PaymentSkill's @pricing decorator; this field is descriptive only
)
def my_tool(param: str) -> str:
"""Tool implementation"""
return f"Result: {param}"
OpenAI Schema Generation
Tools automatically generate OpenAI-compatible schemas:
@tool
def search_web(query: str, max_results: int = 10) -> List[str]:
"""Search the web for information
Args:
query: Search query string
max_results: Maximum results to return
Returns:
List of search results
"""
return ["result1", "result2"]
# Generates schema:
{
"type": "function",
"function": {
"name": "search_web",
"description": "Search the web for information",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search query string"
},
"max_results": {
"type": "integer",
"description": "Maximum results to return",
"default": 10
}
},
"required": ["query"]
}
}
}
External Tools
External tools are defined in the request's tools parameter and executed on the requester's side. They follow the standard OpenAI tool definition format.
Standard OpenAI Tool Definition Format
External tools use the standard OpenAI format:
{
"tools": [
{
"type": "function",
"function": {
"name": "function_name",
"description": "Function description",
"parameters": {
"type": "object",
"properties": {
"param_name": {
"type": "string",
"description": "Parameter description"
}
},
"required": ["param_name"]
}
}
}
]
}
Using External Tools
# Define external tools in the request
external_tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get current weather for a location",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city and state, e.g. San Francisco, CA"
},
"unit": {
"type": "string",
"description": "Temperature unit (celsius or fahrenheit)",
"enum": ["celsius", "fahrenheit"]
}
},
"required": ["location"]
}
}
},
{
"type": "function",
"function": {
"name": "send_email",
"description": "Send an email to a recipient",
"parameters": {
"type": "object",
"properties": {
"to": {"type": "string", "description": "Recipient email address"},
"subject": {"type": "string", "description": "Email subject"},
"body": {"type": "string", "description": "Email body content"}
},
"required": ["to", "subject", "body"]
}
}
}
]
# Pass tools in the request
messages = [{"role": "user", "content": "What's the weather in Paris?"}]
response = await agent.run(messages=messages, tools=external_tools)
Handling Tool Calls
When the agent makes tool calls, you receive them in the response and execute them client-side:
# Agent response with tool calls
response = await agent.run(messages=messages, tools=external_tools)
assistant_message = response.choices[0].message
if assistant_message.tool_calls:
# Execute each tool call
for tool_call in assistant_message.tool_calls:
function_name = tool_call.function.name
arguments = json.loads(tool_call.function.arguments)
# Execute the tool based on name
if function_name == "get_weather":
result = get_weather_external(arguments["location"])
elif function_name == "send_email":
result = send_email_external(
arguments["to"],
arguments["subject"],
arguments["body"]
)
# Add tool result to conversation
messages.append({
"role": "assistant",
"content": assistant_message.content,
"tool_calls": [tool_call]
})
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result
})
# Get final response
final_response = await agent.run(messages=messages, tools=external_tools)
return final_response.choices[0].message.content
def get_weather_external(location: str) -> str:
"""Your implementation of the external weather tool"""
# Your weather API call here
return f"Sunny in {location}, 22°C"
def send_email_external(to: str, subject: str, body: str) -> str:
"""Your implementation of the external email tool"""
# Your email sending logic here
return f"Email sent to {to}"
Tool Execution
Automatic Tool Calling
# Agent automatically calls tools when needed
user_msg = "What's the weather in Paris?"
response = await agent.run([
{"role": "user", "content": user_msg}
])
# Agent calls get_weather("Paris") automatically
Manual Tool Results
# Include tool results in conversation
messages = [
{"role": "user", "content": "Calculate 42 * 17"},
{"role": "assistant", "content": "I'll calculate that for you.",
"tool_calls": [{
"id": "call_123",
"type": "function",
"function": {"name": "multiply", "arguments": '{"x": 42, "y": 17}'}
}]},
{"role": "tool", "tool_call_id": "call_123", "content": "714"}
]
response = await agent.run(messages)
Advanced Tool Features
Dynamic Tool Registration
class AdaptiveSkill(Skill):
@hook("on_connection")
async def register_dynamic_tools(self, context):
"""Register tools based on context"""
if context.peer_user_id == "admin":
# Register admin tools
self.register_tool(self.admin_tool, scope="admin")
if "math" in str(context.messages):
# Register math tools
self.register_tool(self.advanced_calc)
return context
def admin_tool(self, action: str) -> str:
"""Admin-only tool"""
return f"Admin action: {action}"
Tool Middleware
class ToolMonitor(Skill):
@hook("before_toolcall", priority=1)
async def validate_tool(self, context):
"""Validate before execution"""
tool_name = context["tool_call"]["function"]["name"]
# Rate limiting
if self.is_rate_limited(tool_name):
raise RateLimitError(f"Tool {tool_name} rate limited")
# Parameter validation
args = json.loads(context["tool_call"]["function"]["arguments"])
self.validate_args(tool_name, args)
return context
@hook("after_toolcall", priority=90)
async def log_result(self, context):
"""Log tool execution"""
await self.log_tool_usage(
tool=context["tool_call"]["function"]["name"],
result=context["tool_result"],
duration=context.get("tool_duration")
)
return context
Tool Pricing
from webagents import tool
from webagents.agents.skills.robutler.payments import pricing
class PaidToolsSkill(Skill):
@tool
@pricing(cost=0.10, currency="USD")
def expensive_api_call(self, query: str) -> str:
"""Call expensive external API"""
# Automatically tracks usage for billing
return self.call_paid_api(query)
@tool
@pricing(cost=0.01, per="request")
def database_query(self, sql: str) -> List[Dict]:
"""Execute database query"""
return self.execute_sql(sql)
Tool Patterns
Validation Pattern
@tool
def update_record(self, record_id: str, data: Dict) -> Dict:
"""Update record with validation"""
# Validate inputs
if not self.validate_record_id(record_id):
return {"error": "Invalid record ID"}
if not self.validate_data(data):
return {"error": "Invalid data format"}
# Perform update
try:
result = self.db.update(record_id, data)
return {"success": True, "record": result}
except Exception as e:
return {"error": str(e)}
Async Pattern
@tool
async def fetch_data(self, urls: List[str]) -> List[Dict]:
"""Fetch data from multiple URLs concurrently"""
import aiohttp
async with aiohttp.ClientSession() as session:
tasks = [self.fetch_url(session, url) for url in urls]
results = await asyncio.gather(*tasks)
return results
Caching Pattern
class CachedToolsSkill(Skill):
def __init__(self, config=None):
super().__init__(config)
self.cache = {}
@tool
def expensive_calculation(self, input: str) -> str:
"""Cached expensive calculation"""
if input in self.cache:
return self.cache[input]
result = self.perform_calculation(input)
self.cache[input] = result
return result
Best Practices
- Clear Descriptions - Help LLM understand when to use tools
- Type Hints - Enable automatic schema generation
- Error Handling - Return errors as data, not exceptions
- Scope Control - Use appropriate access levels
- Performance - Consider caching and async execution