Tool API Reference
API documentation for creating and using tools with native OpenAI function calling.
Module: udspy.tool
@tool Decorator
@tool(
name: str | None = None,
description: str | None = None,
*,
require_confirmation: bool = False,
) -> Callable[[Callable], Tool]
Decorator to mark a function as a tool for use with Predict and ReAct modules.
Parameters:
name(str | None, default:None): Tool name visible to the LLM- If not provided, uses the function name
description(str | None, default:None): Tool description visible to the LLM- If not provided, uses the function's docstring
- This helps the LLM decide when to use the tool
require_confirmation(bool, default:False): Whether to require user confirmation before execution- If
True, wraps the tool with@confirm_firstdecorator - Raises
ConfirmationRequiredon first call, executes after approval - Useful for destructive or sensitive operations
Returns:
Tool: A wrapped tool object with metadata
Example:
from pydantic import Field
from udspy import tool
@tool(name="calculator", description="Perform arithmetic operations")
def calculator(
operation: str = Field(description="add, subtract, multiply, or divide"),
a: float = Field(description="First number"),
b: float = Field(description="Second number"),
) -> float:
"""Execute arithmetic operation."""
ops = {
"add": a + b,
"subtract": a - b,
"multiply": a * b,
"divide": a / b if b != 0 else float("inf"),
}
return ops[operation]
Tool Confirmation Example
import os
from pydantic import Field
from udspy import tool
@tool(
name="delete_file",
description="Delete a file from the filesystem",
require_confirmation=True # Requires user confirmation
)
def delete_file(path: str = Field(description="File path to delete")) -> str:
"""Delete a file (requires confirmation)."""
os.remove(path)
return f"Deleted {path}"
Tool Class
The Tool class wraps a function and adds metadata for OpenAI function calling. You typically don't instantiate this directly; use the @tool decorator instead.
Constructor
def __init__(
self,
func: Callable,
name: str | None = None,
description: str | None = None,
*,
require_confirmation: bool = False,
desc: str | None = None,
args: dict[str, str] | None = None,
)
Parameters:
func(Callable): The function to wrapname(str | None): Tool name (defaults to function name)description(str | None): Tool description (defaults to docstring)require_confirmation(bool, default:False): Whether to require confirmation before executiondesc(str | None): Alias fordescription(DSPy compatibility)args(dict[str, str] | None): Manual argument specification (DSPy compatibility)
Example:
from udspy import Tool
def my_function(x: int, y: int) -> int:
"""Add two numbers."""
return x + y
tool = Tool(my_function, name="adder", description="Adds numbers")
However, using the @tool decorator is preferred:
@tool(name="adder", description="Adds numbers")
def my_function(x: int, y: int) -> int:
"""Add two numbers."""
return x + y
Attributes
func
The underlying function that this tool wraps.
name
The tool's name as seen by the LLM.
description
The tool's description as seen by the LLM.
require_confirmation
Whether this tool requires user confirmation before execution.
parameters
Dictionary mapping parameter names to their metadata:
{
"param_name": {
"type": str, # Python type
"description": "Parameter description",
"required": True # Whether parameter is required
},
# ...
}
desc (DSPy compatibility)
Alias for description. Provided for DSPy compatibility.
args (DSPy compatibility)
Dictionary mapping parameter names to type + description strings. Provided for DSPy compatibility.
Example:
{
"operation": "str - add, subtract, multiply, or divide",
"a": "float - First number",
"b": "float - Second number"
}
Methods
__call__(*args, **kwargs) -> Any
Call the wrapped function synchronously.
Parameters:
*args: Positional arguments**kwargs: Keyword arguments
Returns:
- Function result
Example:
@tool(name="add", description="Add numbers")
def add(a: int, b: int) -> int:
return a + b
result = add(2, 3) # Returns 5
acall(**kwargs) -> Any
Call the wrapped function asynchronously.
- If the function is async, awaits it directly
- If the function is sync, runs it in an executor to avoid blocking (unless
require_confirmation=True) - If
require_confirmation=True, may raiseConfirmationRequiredbefore execution
Parameters:
**kwargs: Keyword arguments
Returns:
- Awaitable that resolves to the function result
Raises:
ConfirmationRequired: Ifrequire_confirmation=Trueand not yet approved
Example:
import asyncio
@tool(name="fetch", description="Fetch data")
async def fetch_data(url: str) -> str:
# Async operation
return f"Data from {url}"
# Call async
result = await fetch_data.acall(url="https://example.com")
Sync function example:
@tool(name="compute", description="Compute value")
def compute(x: int) -> int:
return x * 2
# Still works with acall - runs in executor
result = await compute.acall(x=5)
parameters Property
Get the JSON schema for OpenAI function calling.
Returns:
dict: Complete JSON schema with type, properties, and required fields
Example:
@tool(name="calculator", description="Do math")
def calculator(
operation: str = Field(description="Operation type"),
a: float = Field(description="First number"),
b: float = Field(description="Second number"),
) -> float:
return eval(f"{a} {operation} {b}")
# Get the parameters schema for OpenAI
params = calculator.parameters
# {
# "type": "object",
# "properties": {
# "operation": {
# "type": "string",
# "description": "Operation type"
# },
# "a": {
# "type": "number",
# "description": "First number"
# },
# "b": {
# "type": "number",
# "description": "Second number"
# }
# },
# "required": ["operation", "a", "b"]
# }
# The adapter uses this to build OpenAI function schemas:
from udspy.adapter import ChatAdapter
adapter = ChatAdapter()
openai_schema = adapter.format_tool_schema(calculator)
# {
# "type": "function",
# "function": {
# "name": "calculator",
# "description": "Do math",
# "parameters": calculator.parameters # ← Uses this property
# }
# }
format() -> str
Format the tool as a human-readable string for LLM prompts.
Returns:
str: Human-readable description with name, description, and parameters
Example:
@tool(name="calculator", description="Perform arithmetic operations")
def calculator(
operation: str = Field(description="Operation type"),
a: float = Field(description="First number"),
) -> float:
return a * 2
# Get human-readable format for module prompts
description = calculator.format()
# "calculator, whose description is <desc>Perform arithmetic operations</desc>. It takes arguments {<properties>}."
Parameter Type Annotations
Tools use Python type hints to generate OpenAI schemas. Supported types:
| Python Type | JSON Schema Type |
|---|---|
str |
"string" |
int |
"integer" |
float |
"number" |
bool |
"boolean" |
list |
"array" |
dict |
"object" |
Optional[T] |
Type of T (nullable) |
Example:
from typing import Optional
from pydantic import Field
from udspy import tool
@tool(name="search", description="Search with filters")
def search(
query: str = Field(description="Search query"),
max_results: int = Field(description="Maximum results", default=10),
include_archived: bool = Field(description="Include archived", default=False),
tags: Optional[list] = Field(description="Filter by tags", default=None),
) -> str:
return f"Searching for: {query}"
Using Pydantic Fields
Use pydantic.Field() to add parameter descriptions and defaults:
from pydantic import Field
@tool(name="example", description="Example tool")
def example_tool(
# Required parameter with description
query: str = Field(description="The search query"),
# Optional parameter with default
limit: int = Field(description="Result limit", default=10),
# Optional parameter that can be None
filter: Optional[str] = Field(description="Optional filter", default=None),
) -> str:
return f"Query: {query}, Limit: {limit}"
Important:
- Always provide descriptions for parameters
- Use
Field(...)orField()for required parameters (no default) - Use
Field(default=value)for optional parameters - Descriptions help the LLM understand when and how to use the tool
Tool Confirmation
For destructive or sensitive operations, use require_confirmation=True:
import os
from pydantic import Field
from udspy import tool, ConfirmationRequired
@tool(
name="delete_all_files",
description="Delete all files in a directory",
require_confirmation=True # Requires confirmation
)
def delete_all_files(
directory: str = Field(description="Directory path")
) -> str:
"""Delete all files in directory (requires confirmation)."""
for file in os.listdir(directory):
os.remove(os.path.join(directory, file))
return f"Deleted all files in {directory}"
# When ReAct tries to call this tool, it raises ConfirmationRequired on first call
# After user approves, the tool executes normally
How it works:
- LLM decides to call the tool
- Tool function (wrapped with
@confirm_first) raisesConfirmationRequiredon first call - User sees confirmation prompt:
"Confirm execution of delete_all_files with args: {...}? (yes/no)" - User responds with
"yes","no", or modified arguments - ReAct resumes execution based on user's response
- If approved, subsequent calls to the same tool with same args execute normally
Usage with ReAct
from pydantic import Field
from udspy import InputField, OutputField, ReAct, Signature, tool
# Define tools
@tool(name="search", description="Search for information")
def search(query: str = Field(description="Search query")) -> str:
return f"Results for: {query}"
@tool(name="calculate", description="Perform calculations")
def calculate(expression: str = Field(description="Math expression")) -> str:
return str(eval(expression))
# Define signature
class QA(Signature):
"""Answer questions using tools."""
question: str = InputField()
answer: str = OutputField()
# Create agent with tools
agent = ReAct(QA, tools=[search, calculate])
# Execute
result = agent(question="What is the population of Tokyo times 2?")
# Agent will:
# 1. Call search("Tokyo population")
# 2. Call calculate("population * 2")
# 3. Synthesize final answer
Usage with Predict
from udspy import Predict, Signature, InputField, OutputField, tool
from pydantic import Field
@tool(name="get_weather", description="Get current weather")
def get_weather(city: str = Field(description="City name")) -> str:
return f"Weather in {city}: Sunny, 72°F"
class WeatherQuery(Signature):
"""Get weather information."""
city: str = InputField()
weather: str = OutputField()
predictor = Predict(WeatherQuery, tools=[get_weather])
result = predictor(city="San Francisco")
DSPy Compatibility
The Tool class includes DSPy-compatible attributes:
@tool(name="search", description="Search tool")
def search(query: str = Field(description="Search query")) -> str:
return "results"
# DSPy-style access
print(search.desc) # Same as search.description: "Search tool"
print(search.args) # Properties dict: {"query": {"type": "string", "description": "Search query", ...}}
Best Practices
-
Clear descriptions: Write clear, concise tool and parameter descriptions
-
Use Field() for all parameters: Always use
Field()with descriptions -
Require confirmation for destructive ops: Use
require_confirmation=True -
Handle errors gracefully: Return error messages as strings
-
Keep tools focused: Each tool should do one thing well
# Good: Focused tool @tool(name="search_users", description="Search for users") def search_users(query: str = Field(...)) -> str: ... # Bad: Too many responsibilities @tool(name="user_management", description="Manage all user operations") def user_management(action: str, query: str, data: dict) -> str: ...
Type Annotations
from typing import Callable, Any
from udspy import Tool
# Decorator signature
def tool(
name: str | None = None,
description: str | None = None,
*,
require_confirmation: bool = False,
) -> Callable[[Callable[..., Any]], Tool]: ...
# Tool class
class Tool:
func: Callable[..., Any]
name: str
description: str
require_confirmation: bool
parameters: dict[str, dict[str, Any]]
desc: str # Alias for description
args: dict[str, str] # DSPy compatibility
def __call__(self, *args: Any, **kwargs: Any) -> Any: ...
async def acall(self, **kwargs: Any) -> Any: ...
def to_openai_schema(self) -> dict[str, Any]: ...
Dynamic Tool Management
Tools can return module callbacks to dynamically add other tools during execution. This is useful for:
- Loading specialized tools on demand
- Adding tools based on user permissions
- Progressive tool discovery
Basic Example
from udspy import tool, module_callback
from pydantic import Field
# The tool that will be loaded dynamically
@tool(name="calculator", description="Perform calculations")
def calculator(expression: str = Field(...)) -> str:
return str(eval(expression, {"__builtins__": {}}, {}))
# Meta-tool that loads the calculator
@tool(name="load_calculator", description="Load calculator tool")
def load_calculator() -> callable:
"""Load calculator dynamically."""
@module_callback
def add_calculator(context):
# Get current tools (excluding built-ins)
current = [
t for t in context.module.tools.values()
if t.name not in ("finish", "user_clarification")
]
# Add calculator
context.module.init_module(tools=current + [calculator])
return "Calculator loaded successfully"
return add_calculator
# Use with ReAct
from udspy import ReAct, Signature, InputField, OutputField
class Question(Signature):
"""Answer questions. Load tools if needed."""
question: str = InputField()
answer: str = OutputField()
agent = ReAct(Question, tools=[load_calculator])
result = agent(question="What is 157 * 834?")
# Agent will:
# 1. Call load_calculator() to get the calculator tool
# 2. Use calculator(expression="157 * 834")
# 3. Return the answer
How It Works
- Tool returns callable: Instead of a string/value, return a function decorated with
@module_callback - Callback receives context: Context has the module instance and execution state
- Callback modifies tools: Call
context.module.init_module(tools=[...])to add/remove tools - Callback returns observation: Return a string that appears in the trajectory
- Tools persist: Newly added tools remain available for the rest of execution
Category-Based Loading
@tool(name="load_tools", description="Load tools by category")
def load_tools(category: str = Field(...)) -> callable:
@module_callback
def add_tools(context):
current = list(context.module.tools.values())
if category == "math":
new_tools = [calculator, statistics]
elif category == "web":
new_tools = [search, scrape]
else:
return f"Unknown category: {category}"
context.module.init_module(tools=current + new_tools)
return f"Loaded {len(new_tools)} {category} tools"
return add_tools
Important Notes
- Must return string: Module callbacks must return a string (the observation)
- Async by default: Callbacks are called during tool execution
- Built-ins preserved:
finishand user clarification are automatically kept - Persistence: Tools remain available for the entire execution
For complete documentation and examples, see: - Dynamic Tools Guide - Complete guide with examples
Example Code: See examples/dynamic_calculator.py and examples/dynamic_tools.py in the GitHub repository
See Also
- Dynamic Tools Guide - Dynamic tool management
- Confirmation API - Confirmation system and
@confirm_firstdecorator - ReAct API - Using tools with ReAct agents
- ReAct Examples - Tool usage examples
- Module API - Base module documentation