Skip to content

Latest commit

 

History

History
562 lines (453 loc) · 15.8 KB

File metadata and controls

562 lines (453 loc) · 15.8 KB

Tool/Function Calling

Overview

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.

Quick Start

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}")

Tool Definition

Tool Types

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
)

Parameters Schema

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
}

Using Tools

Passing Tools

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
)

Tool Choice

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"}}
)

Parallel Tool Calls

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)

Processing Tool Calls

Reading Tool Call Responses

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}")

ToolCall Structure

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
)

Multi-Turn Conversations

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)

Complete Multi-Turn Example

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)
        })

Validation

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 jsonschema

Manual Validation

You 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)

Streaming with 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 Support

Provider Tools tool_choice Streaming Parallel Calls
OpenAI Yes Yes Yes Yes
Anthropic Yes Yes Yes Yes
Google 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

Provider-Specific Notes

OpenAI

  • Full support for all tool calling features
  • Supports strict mode for guaranteed schema adherence
  • Reasoning models (o1, o3) have limited tool support

Anthropic

  • Tools are converted to Anthropic's input_schema format automatically
  • tool_choice="required" maps to {"type": "any"}
  • parallel_tool_calls=False maps to disable_parallel_tool_use

Google (Gemini)

  • Tool IDs are generated by Esperanto (Google doesn't provide them)
  • tool_choice maps to function_calling_config.mode

Vertex AI

  • Similar to Google but uses the Vertex AI SDK
  • Requires Google Cloud authentication

Examples

See the examples/tool_calling/ directory for complete examples:

Best Practices

1. Write Clear Descriptions

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={...}
)

2. Use Specific Parameter Types

# 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
    }
}

3. Handle Errors Gracefully

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)})

4. Limit Tool Count

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"]}
                }
            }
        }
    )
)

See Also