Tool Interfaces¶
Overview¶
A good tool interface is like a well-designed API—clear, predictable, and impossible to misuse.
LLMs need tools with explicit contracts they can understand and follow reliably.
The Problem¶
Without Good Design¶
# BAD
def tool_1(x):
"""Do something with x"""
return result
# LLM doesn't know:
# - What type of x?
# - What does "something" mean?
# - What type is result?
# - When does it fail?
# - What are side effects?
With Good Design¶
# GOOD
@agent_tool
def search_database(
query: str, # What we're searching for (1-500 chars)
table: str, # Which table ("users", "products", "orders")
limit: int = 10 # Max results (1-100, default 10)
) -> dict:
"""
Search database table for matching records.
Returns:
{
"success": bool,
"results": List[dict], # Matching records
"count": int, # Number of results
"query_time_ms": float
}
"""
5 Principles of Good Tool Design¶
1. Explicit Contracts¶
# Specify everything
@tool(
name="calculate",
description="Perform arithmetic on two numbers",
required_params=["a", "b", "operation"],
optional_params=["precision"]
)
def calculate(
a: float, # First number
b: float, # Second number
operation: Literal["add", "subtract", "multiply", "divide"], # Operation type
precision: int = 2 # Decimal places (0-10)
) -> dict:
"""Calculate result with specified precision"""
...
2. Clear Error Handling¶
def tool_with_errors():
"""
Raises:
- ValueError: If arguments are invalid
- TimeoutError: If operation takes > 30 seconds
- PermissionError: If user lacks access
Returns: {...} on success
"""
...
3. Meaningful Defaults¶
# GOOD
def search(
query: str,
max_results: int = 10, # Reasonable default
timeout_sec: int = 30
):
...
# BAD
def search(query: str, max_results: int = 999999):
...
4. Consistent Responses¶
# GOOD
{
"success": bool,
"data": Any, # Actual result
"error": Optional[str], # Error message if failed
"metadata": {
"execution_time_ms": float,
"timestamp": str
}
}
# BAD
# Sometimes returns string, sometimes dict, sometimes list
5. Comprehensive Examples¶
@tool
def send_email(
to: str, # Email address
subject: str, # Email subject
body: str, # Email body (markdown)
cc: Optional[List[str]] = None # CC recipients
) -> dict:
"""
Send an email message.
Examples:
Example 1: Simple email
send_email(
to="user@example.com",
subject="Hello",
body="Hi there!"
)
Example 2: With CC
send_email(
to="manager@example.com",
subject="Report",
body="Q3 Results:\n- Revenue: $50K\n- Growth: 15%",
cc=["cto@example.com"]
)
"""
Tool Interface Patterns¶
Pattern 1: Dictionary-Based Tools¶
tools_registry = {
"calculate": {
"description": "Perform arithmetic",
"function": calculate_func,
"params": {
"a": {"type": "float", "required": True},
"b": {"type": "float", "required": True},
"op": {"type": "string", "enum": ["add", "subtract", "multiply"]}
},
"returns": {"type": "dict", "properties": {...}}
},
"search": {
"description": "Search documents",
"function": search_func,
"params": {...}
}
}
Pattern 2: Class-Based Tools¶
class Tool(ABC):
@property
def name(self) -> str:
"""Tool name"""
@property
def description(self) -> str:
"""What it does"""
@property
def parameters(self) -> dict:
"""Parameter schema"""
@abstractmethod
def call(self, **kwargs) -> dict:
"""Execute the tool"""
class CalculatorTool(Tool):
@property
def name(self):
return "calculator"
@property
def description(self):
return "Perform arithmetic operations"
@property
def parameters(self):
return {
"a": {"type": "number"},
"b": {"type": "number"},
"operation": {"type": "string", "enum": ["add", "subtract"]}
}
def call(self, a: float, b: float, operation: str) -> dict:
if operation == "add":
result = a + b
elif operation == "subtract":
result = a - b
else:
return {"error": f"Unknown operation: {operation}"}
return {"success": True, "result": result}
Pattern 3: Decorator-Based Tools¶
@register_tool(
description="Send email notifications",
examples=[
{"input": {"to": "user@example.com", "message": "Hello"},
"output": {"success": True}}
]
)
def send_notification(
to: str, # Recipient email
message: str, # Message to send (max 1000 chars)
priority: str = "normal" # "low", "normal", "high"
) -> dict:
"""Send email notification to user"""
...
-
Tool Metadata Specification¶
TOOL_SPEC = {
# Identification
"name": "tool_name", # Unique identifier
"version": "1.0.0", # Semantic versioning
# Description
"description": "What this tool does", # Brief (1 sentence)
"long_description": "...", # Detailed (2-3 sentences)
"category": "communication", # categorization
# Parameters
"parameters": {
"type": "object",
"properties": {
"param_name": {
"type": "string", # string, number, boolean, array, object
"description": "What this parameter means",
"examples": ["example1", "example2"],
"constraints": {
"minLength": 1,
"maxLength": 500,
"pattern": "^[a-z]+$"
}
}
},
"required": ["required_param"],
"optional": ["optional_param"]
},
# Return value
"returns": {
"type": "object",
"description": "What the tool returns on success",
"properties": {
"success": {"type": "boolean"},
"data": {"type": "object"},
"error": {"type": "string"}
}
},
# Error handling
"errors": [
{"code": "INVALID_INPUT", "description": "Input validation failed"},
{"code": "TIMEOUT", "description": "Operation took too long"},
{"code": "PERMISSION_DENIED", "description": "User lacks permission"}
],
# Usage
"examples": [
{
"description": "Simple example",
"input": {"param1": "value1"},
"output": {"success": True, "data": {...}}
}
],
# Constraints
"constraints": {
"rate_limit": "100 calls/minute",
"timeout_sec": 30,
"requires_auth": True,
"costs_tokens": True
},
# Integration
"dependencies": ["database", "auth"],
"side_effects": ["Sends email", "Updates database"],
"idempotent": False
}
Tool Abstraction Layer¶
class ToolInterface:
"""Unified interface for all tools"""
def validate_inputs(self, inputs: dict) -> tuple[bool, str]:
"""Validate inputs match schema"""
try:
jsonschema.validate(inputs, self.schema["parameters"])
return True, ""
except jsonschema.ValidationError as e:
return False, f"Invalid input: {e.message}"
def call(self, **inputs) -> dict:
"""Execute tool with validation"""
valid, error = self.validate_inputs(inputs)
if not valid:
return {"success": False, "error": error}
try:
result = self._execute(**inputs)
return {"success": True, "data": result}
except Exception as e:
return {"success": False, "error": str(e)}
def to_openai_schema(self) -> dict:
"""Convert to OpenAI function calling format"""
return {
"type": "function",
"function": {
"name": self.name,
"description": self.description,
"parameters": self.schema["parameters"]
}
}
def to_claude_schema(self) -> dict:
"""Convert to Claude tool_use format"""
return {
"name": self.name,
"description": self.description,
"input_schema": self.schema["parameters"]
}
Tool Registry¶
class ToolRegistry:
"""Manage collection of tools"""
def __init__(self):
self.tools = {}
self.categories = {}
def register(self, tool: Tool) -> None:
"""Register a new tool"""
if tool.name in self.tools:
raise ValueError(f"Tool {tool.name} already registered")
self.tools[tool.name] = tool
# Index by category
category = tool.metadata.get("category")
if category not in self.categories:
self.categories[category] = []
self.categories[category].append(tool.name)
def get_tool(self, name: str) -> Tool:
"""Get tool by name"""
if name not in self.tools:
raise ValueError(f"Tool {name} not found")
return self.tools[name]
def get_tools_by_category(self, category: str) -> List[Tool]:
"""Get all tools in category"""
names = self.categories.get(category, [])
return [self.tools[name] for name in names]
def list_tools(self) -> dict:
"""List all available tools with descriptions"""
return {
name: tool.description
for name, tool in self.tools.items()
}
3 Tool Interface Warnings¶
Warning 1: Ambiguous Descriptions¶
# WRONG
@tool(description="Process data")
def process_data(x):
...
# LLM doesn't know:
# - What type of data?
# - How is it processed?
# - What's returned?
# RIGHT
@tool(
description="Clean and validate user input data (names, emails, etc) "
"by removing whitespace, checking format, and returning "
"standardized version"
)
def validate_user_data(
data: str, # User input (name or email)
data_type: Literal["name", "email"] # What kind of data
) -> dict:
"""Returns {'valid': bool, 'cleaned': str, 'error': str}"""
...
Key Lesson: Be specific. LLMs work better with detailed descriptions.
Warning 2: Missing Examples¶
# WRONG
@tool(description="Send message")
def send_message(to, message):
...
# LLM doesn't know:
# - Email or phone number?
# - How long can message be?
# - What format for recipient?
# RIGHT
@tool(
description="Send message via email or SMS",
examples=[
{
"description": "Send email",
"input": {"to": "user@example.com", "message": "Hi!"},
"output": {"success": True, "message_id": "msg_123"}
},
{
"description": "Send SMS",
"input": {"to": "+1234567890", "message": "Hello"},
"output": {"success": True, "message_id": "msg_124"}
}
]
)
def send_message(
to: str, # Email address or phone number
message: str # Message content (max 500 chars)
) -> dict:
...
Key Lesson: Examples prevent misuse and improve accuracy.
Warning 3: Leaky Abstractions¶
# WRONG
@tool
def query_database(
sql: str, # Raw SQL query (DANGER!)
connection_pool_size: int = 10
) -> List[dict]:
...
# Problems:
# - LLM can write malicious SQL
# - Expose internal connection details
# - Breaks if schema changes
# RIGHT
@tool
def search_users(
search_term: str, # Name or email to search
limit: int = 10 # Max results
) -> dict:
"""
Search for users by name or email.
Returns: {'users': [{'id': str, 'name': str, 'email': str}]}
"""
# Internal: construct safe SQL
# SELECT * FROM users WHERE name LIKE ? OR email LIKE ? LIMIT ?
...
Key Lesson: Hide implementation details. Only expose what's necessary.
Best Practices¶
1. Version Tools¶
@tool(version="1.2.0")
def my_tool():
"""Track breaking changes"""
# Deprecation path:
# v1.0
# v1.1
# v1.2
# v2.0
2. Document Rate Limits¶
@tool(
constraints={
"rate_limit": "100 calls per minute",
"quota_per_day": "10,000 calls"
}
)
def expensive_tool():
"""Tool with usage constraints"""
...
3. Enable Tool Discovery¶
def list_available_tools() -> dict:
"""Return tools agent can use"""
return {
tool.name: {
"description": tool.description,
"parameters": tool.parameters,
"category": tool.category,
"rate_limit": tool.rate_limit
}
for tool in registry.list_all()
}
Key Takeaways¶
- Good interfaces are explicit - LLMs need clear contracts
- Examples matter - Show, don't just tell
- Hide complexity - Expose only what's necessary
- Validate inputs - Catch errors early
- Consistent responses - Predictable format
- Document thoroughly - Every parameter, return value, error
- Version your tools - Track changes over time
-
Next Steps¶
- Read Tool Composition - Chain tools together
- Read Error Handling - Handle failures gracefully
-
Last Updated: August 9, 2026