Tool calling (also known as function calling) allows language models to request execution of predefined functions during a conversation. This enables models to interact with external systems, retrieve real-time data, or perform actions on behalf of users.
Esperanto provides a unified tool calling interface that works consistently across all supported providers. Define your tools once, and use them with OpenAI, Anthropic, Google, Groq, Mistral, and more.
import json
from esperanto import AIFactory
from esperanto.common_types import Tool, ToolFunction
# Define a tool
weather_tool = Tool(
type="function",
function=ToolFunction(
name="get_weather",
description="Get the current weather for a location",
parameters={
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "City name, e.g., 'San Francisco, CA'"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature unit"
}
},
"required": ["location"]
}
)
)
# Create a model with tools
model = AIFactory.create_language(
provider="openai",
model_name="gpt-4o",
config={"tools": [weather_tool]}
)
# Send a message that might trigger tool use
messages = [{"role": "user", "content": "What's the weather in Tokyo?"}]
response = model.chat_complete(messages)
# Check if the model wants to call a tool
if response.choices[0].message.tool_calls:
tool_call = response.choices[0].message.tool_calls[0]
print(f"Tool: {tool_call.function.name}")
print(f"Arguments: {tool_call.function.arguments}")Esperanto provides typed classes for tool definitions:
from esperanto.common_types import Tool, ToolFunction
# ToolFunction defines the function signature
function = ToolFunction(
name="search_database", # Function name (required)
description="Search the database", # What the function does (required)
parameters={ # JSON Schema for parameters
"type": "object",
"properties": {
"query": {"type": "string"},
"limit": {"type": "integer", "default": 10}
},
"required": ["query"]
},
strict=True # OpenAI-only: Enable strict mode for guaranteed schema adherence
)
# Tool wraps the function
tool = Tool(
type="function", # Currently only "function" is supported
function=function
)The parameters field uses JSON Schema to define the function's input:
parameters={
"type": "object",
"properties": {
# String parameter
"name": {
"type": "string",
"description": "The user's name"
},
# Number with constraints
"age": {
"type": "integer",
"minimum": 0,
"maximum": 150
},
# Enum (fixed choices)
"status": {
"type": "string",
"enum": ["active", "inactive", "pending"]
},
# Array
"tags": {
"type": "array",
"items": {"type": "string"}
},
# Nested object
"address": {
"type": "object",
"properties": {
"street": {"type": "string"},
"city": {"type": "string"}
}
}
},
"required": ["name", "status"] # Required fields
}Tools can be configured at two levels:
# 1. Instance level - tools available for all calls
model = AIFactory.create_language(
provider="openai",
model_name="gpt-4o",
config={"tools": [weather_tool, search_tool]}
)
response = model.chat_complete(messages)
# 2. Call level - override for specific request
response = model.chat_complete(
messages,
tools=[different_tool] # Overrides instance tools
)Control how the model uses tools:
# "auto" (default) - Model decides whether to use tools
response = model.chat_complete(messages, tool_choice="auto")
# "required" - Model must call at least one tool
response = model.chat_complete(messages, tool_choice="required")
# "none" - Model cannot use tools (even if provided)
response = model.chat_complete(messages, tool_choice="none")
# Specific tool - Force calling a particular tool
response = model.chat_complete(
messages,
tool_choice={"type": "function", "function": {"name": "get_weather"}}
)Some providers allow models to request multiple tool calls in a single response:
# Allow parallel tool calls (default for most providers)
response = model.chat_complete(messages, parallel_tool_calls=True)
# Force single tool call per response
response = model.chat_complete(messages, parallel_tool_calls=False)When a model decides to call a tool, the response contains tool_calls:
response = model.chat_complete(messages, tools=tools)
message = response.choices[0].message
if message.tool_calls:
for tool_call in message.tool_calls:
# Unique ID for this tool call
print(f"ID: {tool_call.id}")
# Function name
print(f"Function: {tool_call.function.name}")
# Arguments as JSON string
print(f"Arguments (raw): {tool_call.function.arguments}")
# Parse arguments
args = json.loads(tool_call.function.arguments)
print(f"Arguments (parsed): {args}")
else:
# Model responded with text instead
print(f"Response: {message.content}")from esperanto.common_types import ToolCall, FunctionCall
# ToolCall has these fields:
tool_call = ToolCall(
id="call_abc123", # Unique identifier
type="function", # Always "function" currently
function=FunctionCall(
name="get_weather", # Function to call
arguments='{"location": "Tokyo"}' # JSON string of arguments
),
index=0 # Optional: index for streaming
)To continue a conversation after tool calls, send the tool results back to the model:
import json
# Initial request
messages = [{"role": "user", "content": "What's the weather in Tokyo and London?"}]
response = model.chat_complete(messages, tools=tools)
# Model requests tool calls
assistant_message = response.choices[0].message
if assistant_message.tool_calls:
# Add assistant's message (with tool_calls) to history
messages.append({
"role": "assistant",
"tool_calls": [
{
"id": tc.id,
"type": tc.type,
"function": {
"name": tc.function.name,
"arguments": tc.function.arguments
}
}
for tc in assistant_message.tool_calls
]
})
# Execute each tool and add results
for tool_call in assistant_message.tool_calls:
# Execute your tool (implement this based on your needs)
result = execute_tool(
tool_call.function.name,
json.loads(tool_call.function.arguments)
)
# Add tool result to messages
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(result) # Must be a string
})
# Get final response with tool results
final_response = model.chat_complete(messages, tools=tools)
print(final_response.content)import json
from esperanto import AIFactory
from esperanto.common_types import Tool, ToolFunction
# Define tools
tools = [
Tool(
type="function",
function=ToolFunction(
name="get_weather",
description="Get weather for a city",
parameters={
"type": "object",
"properties": {
"city": {"type": "string"}
},
"required": ["city"]
}
)
)
]
# Simulated tool execution
def execute_tool(name: str, args: dict) -> dict:
if name == "get_weather":
# In reality, call a weather API
return {"temperature": 22, "condition": "sunny", "city": args["city"]}
return {"error": f"Unknown tool: {name}"}
# Create model
model = AIFactory.create_language("openai", "gpt-4o")
# Conversation loop
messages = [{"role": "user", "content": "What's the weather in Paris?"}]
while True:
response = model.chat_complete(messages, tools=tools)
message = response.choices[0].message
if not message.tool_calls:
# No more tool calls - print final response
print(f"Assistant: {message.content}")
break
# Add assistant message with tool calls
messages.append({
"role": "assistant",
"tool_calls": [
{
"id": tc.id,
"type": tc.type,
"function": {
"name": tc.function.name,
"arguments": tc.function.arguments
}
}
for tc in message.tool_calls
]
})
# Execute tools and add results
for tool_call in message.tool_calls:
args = json.loads(tool_call.function.arguments)
result = execute_tool(tool_call.function.name, args)
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(result)
})Esperanto can validate tool call arguments against the JSON schema:
from esperanto.common_types import ToolCallValidationError
try:
response = model.chat_complete(
messages,
tools=tools,
validate_tool_calls=True # Enable validation
)
except ToolCallValidationError as e:
print(f"Tool '{e.tool_name}' validation failed:")
for error in e.errors:
print(f" - {error}")Validation requires the jsonschema package:
pip install esperanto[validation]
# or
pip install jsonschemaYou can also validate tool calls manually:
from esperanto.common_types import validate_tool_call, validate_tool_calls, find_tool_by_name
# Validate a single tool call
tool = find_tool_by_name(tools, tool_call.function.name)
if tool:
validate_tool_call(tool_call, tool)
# Validate all tool calls in a response
if message.tool_calls:
validate_tool_calls(message.tool_calls, tools)Tool calls can also be received via streaming:
# Streaming with tools
for chunk in model.chat_complete(messages, tools=tools, stream=True):
delta = chunk.choices[0].delta
if delta.content:
print(delta.content, end="", flush=True)
# Tool calls come in chunks during streaming
if delta.tool_calls:
for tc in delta.tool_calls:
print(f"Tool call chunk: {tc}")Note: When streaming, tool calls arrive incrementally. You'll need to accumulate the chunks to get complete tool call data. The index field on tool calls helps track which chunks belong to which tool call.
Important: The validate_tool_calls parameter is not supported with streaming. Tool call validation requires the complete response to validate arguments against the tool schema. If you pass validate_tool_calls=True with stream=True, a warning will be emitted and validation will be skipped. To validate tool calls when streaming, collect all chunks first, then use the manual validation utilities.
| Provider | Tools | tool_choice | Streaming | Parallel Calls |
|---|---|---|---|---|
| OpenAI | Yes | Yes | Yes | Yes |
| Anthropic | Yes | Yes | Yes | Yes |
| Yes | Yes | Yes | Varies | |
| Groq | Yes | Yes | Yes | Yes |
| Mistral | Yes | Yes | Yes | Yes |
| Azure | Yes | Yes | Yes | Yes |
| Vertex AI | Yes | Yes | Yes | Varies |
| DeepSeek | Yes | Yes | Yes | Varies |
| xAI | Yes | Yes | Yes | Varies |
| OpenRouter | Yes* | Yes* | Yes* | Yes* |
| Ollama | Yes** | Yes** | Yes** | Varies |
* Depends on underlying model ** Model-dependent; not all Ollama models support tools
- Full support for all tool calling features
- Supports
strictmode for guaranteed schema adherence - Reasoning models (o1, o3) have limited tool support
- Tools are converted to Anthropic's
input_schemaformat automatically tool_choice="required"maps to{"type": "any"}parallel_tool_calls=Falsemaps todisable_parallel_tool_use
- Tool IDs are generated by Esperanto (Google doesn't provide them)
tool_choicemaps tofunction_calling_config.mode
- Similar to Google but uses the Vertex AI SDK
- Requires Google Cloud authentication
See the examples/tool_calling/ directory for complete examples:
- basic_tool.py - Simple single-tool example
- multiple_tools.py - Working with multiple tools
- multi_turn.py - Multi-turn conversations with tool results
- streaming_tools.py - Streaming with tool calls
- provider_comparison.py - Same code across providers
Models use descriptions to decide when and how to call tools:
# Good - Clear and specific
ToolFunction(
name="search_products",
description="Search the product catalog by name, category, or price range. "
"Returns up to 10 matching products with name, price, and availability.",
parameters={...}
)
# Bad - Vague
ToolFunction(
name="search",
description="Searches stuff",
parameters={...}
)# Good - Specific types with constraints
parameters={
"type": "object",
"properties": {
"price_min": {"type": "number", "minimum": 0},
"price_max": {"type": "number", "minimum": 0},
"category": {"type": "string", "enum": ["electronics", "clothing", "food"]}
}
}
# Bad - Too permissive
parameters={
"type": "object",
"properties": {
"filters": {"type": "object"} # Too vague
}
}def execute_tool(name: str, args: dict) -> str:
try:
if name == "get_weather":
return json.dumps(fetch_weather(args["city"]))
except Exception as e:
# Return error as tool result - model can handle it
return json.dumps({"error": str(e)})Providing too many tools can confuse the model. Group related functionality:
# Instead of many narrow tools:
# get_user_name, get_user_email, get_user_address, ...
# Use one comprehensive tool:
Tool(
type="function",
function=ToolFunction(
name="get_user_info",
description="Get user information",
parameters={
"type": "object",
"properties": {
"user_id": {"type": "string"},
"fields": {
"type": "array",
"items": {"type": "string", "enum": ["name", "email", "address", "phone"]}
}
}
}
)
)- Language Models (LLM) - Core LLM documentation
- Provider Guides - Provider-specific setup and features
- Examples - Complete working examples