Skip to main content
Tools allow agents to extend their capabilities beyond text generation by calling functions, accessing databases, making API requests, or performing computations. The framework automatically handles function calling, parameter validation, and result formatting.

What are Tools?

Tools (also called “functions” or “function tools”) are Python/C# functions that agents can invoke to:
  • Retrieve real-time data (weather, stock prices, news)
  • Perform calculations or data processing
  • Execute actions (send emails, create tickets)
  • Access databases or search engines
  • Interact with external APIs
The framework uses JSON Schema to describe function parameters to the model, validates arguments, executes functions, and returns results.

Defining a Tool

Use the @tool decorator to convert any function into a tool:
from agent_framework import tool
from typing import Annotated
from pydantic import Field

@tool(approval_mode="never_require")  # Use "always_require" in production!
def get_weather(
    location: Annotated[str, Field(description="The city name")],
    unit: Annotated[str, Field(description="Temperature unit")] = "celsius",
) -> str:
    """Get the current weather for a location."""
    # Simulate API call
    return f"The weather in {location} is sunny and 22°{unit[0].upper()}."
Key Points:
  • The docstring becomes the tool description shown to the model
  • Use Annotated[type, Field(description="...")] for parameter descriptions
  • Set approval_mode="always_require" in production to require user confirmation
  • Return type should be JSON-serializable or a string

Using Tools with Agents

Pass tools to the agent when creating it:
from agent_framework.azure import AzureOpenAIResponsesClient

agent = client.as_agent(
    name="WeatherAgent",
    instructions="You are a weather assistant. Use the get_weather tool to answer questions.",
    tools=get_weather,  # Single tool
)

# Or multiple tools
agent = client.as_agent(
    name="Assistant",
    instructions="You are a helpful assistant.",
    tools=[get_weather, calculate_tip, search_web],  # Multiple tools
)

# Use the agent
result = await agent.run("What's the weather in Seattle?")
print(result.text)
# Output: "The weather in Seattle is sunny and 22°C."

Tool Schemas

The framework automatically generates JSON Schema from function signatures:
@tool
def search_database(
    query: Annotated[str, Field(description="Search query")],
    filters: Annotated[dict[str, str], Field(description="Filters")] = None,
    limit: Annotated[int, Field(description="Max results", ge=1, le=100)] = 10,
) -> list[dict]:
    """Search the product database."""
    ...

# Generated schema (accessible via tool.parameters())
{
    "type": "object",
    "properties": {
        "query": {
            "type": "string",
            "description": "Search query"
        },
        "filters": {
            "type": "object",
            "description": "Filters"
        },
        "limit": {
            "type": "integer",
            "description": "Max results",
            "minimum": 1,
            "maximum": 100,
            "default": 10
        }
    },
    "required": ["query"]
}
You can also provide an explicit schema:
from pydantic import BaseModel

class WeatherInput(BaseModel):
    location: str = Field(description="City name")
    unit: str = Field(default="celsius", description="Temperature unit")

@tool(schema=WeatherInput)
def get_weather(location: str, unit: str = "celsius") -> str:
    """Get the weather."""
    ...

Async Tools

Both sync and async functions are supported:
import httpx

@tool(approval_mode="never_require")
async def fetch_news(topic: Annotated[str, Field(description="News topic")]) -> str:
    """Fetch latest news about a topic."""
    async with httpx.AsyncClient() as client:
        response = await client.get(f"https://api.news.com/search?q={topic}")
        return response.json()["articles"][0]["title"]

# Framework automatically awaits async functions
agent = client.as_agent(
    name="NewsAgent",
    tools=fetch_news,
)

Tool Approval

Require user approval before executing sensitive tools:
@tool(approval_mode="always_require")
def send_email(
    to: Annotated[str, Field(description="Recipient email")],
    subject: Annotated[str, Field(description="Email subject")],
    body: Annotated[str, Field(description="Email body")],
) -> str:
    """Send an email."""
    # Send email
    return f"Email sent to {to}"

agent = client.as_agent(
    name="EmailAgent",
    tools=send_email,
)

# When the model requests to call this tool, you'll get an approval request
async for update in agent.run("Send an email to [email protected]", stream=True):
    if update.approval_required:
        print(f"Tool call requires approval: {update.function_calls}")
        # Display to user and get approval
        approved = get_user_approval()
        if approved:
            # Continue with approved=True
            ...

Advanced Tool Features

Tool Invocation Limits

Control how many times a tool can be called:
@tool(
    approval_mode="never_require",
    max_invocations=3,  # Limit to 3 calls per tool instance lifetime
    max_invocation_exceptions=2,  # Stop after 2 errors
)
def expensive_api_call(query: str) -> str:
    """Call an expensive external API."""
    ...

# Use function invocation config for per-request limits
from agent_framework.openai import OpenAIChatClient

client = OpenAIChatClient()
client.function_invocation_configuration["max_iterations"] = 5  # LLM roundtrips
client.function_invocation_configuration["max_function_calls"] = 20  # Total calls

Custom Result Parsing

Override how tool results are serialized:
def custom_parser(result: Any) -> str:
    """Custom result serializer."""
    if isinstance(result, MyCustomClass):
        return result.to_json()
    return str(result)

@tool(result_parser=custom_parser)
def get_data() -> MyCustomClass:
    """Get custom data."""
    return MyCustomClass(...)

Declaration-Only Tools

Create tools that agents can reason about without executing:
from agent_framework import FunctionTool

# Tool declaration without implementation
time_tool = FunctionTool(
    name="get_current_time",
    description="Get the current time in ISO 8601 format.",
    func=None,  # No implementation
)

# Agent can see and request the tool, but it won't execute
# Useful for testing agent reasoning or client-side implementations

Tool Execution Flow

Best Practices

Tool Design Tips
  1. Clear Descriptions: Write detailed docstrings and parameter descriptions
  2. Input Validation: Use Pydantic Field constraints for validation
  3. Error Handling: Return helpful error messages, don’t raise exceptions
  4. Idempotency: Tools should be safe to retry
  5. Performance: Keep tools fast; use async for I/O operations
  6. Security: Always use approval_mode="always_require" for sensitive operations
Production Considerations
  • Rate Limiting: Protect external APIs from abuse
  • Timeout Handling: Set timeouts for external calls
  • Secret Management: Never hardcode API keys in tool code
  • Logging: Log tool executions for debugging and auditing
  • Cost Control: Monitor and limit expensive API calls
  • Testing: Test tools independently before agent integration

Example: Complete Weather Agent

import httpx
from agent_framework import tool
from agent_framework.azure import AzureOpenAIResponsesClient
from typing import Annotated
from pydantic import Field
import os

@tool(approval_mode="never_require")
async def get_weather(
    location: Annotated[str, Field(description="City name")],
    unit: Annotated[str, Field(description="celsius or fahrenheit")] = "celsius",
) -> str:
    """Get current weather for a location."""
    api_key = os.environ["WEATHER_API_KEY"]
    async with httpx.AsyncClient() as client:
        response = await client.get(
            f"https://api.weather.com/current",
            params={"location": location, "unit": unit, "key": api_key},
        )
        data = response.json()
        return f"The weather in {location} is {data['condition']} with a temperature of {data['temp']}°{unit[0].upper()}."

@tool(approval_mode="never_require")
async def get_forecast(
    location: Annotated[str, Field(description="City name")],
    days: Annotated[int, Field(description="Number of days", ge=1, le=7)] = 3,
) -> str:
    """Get weather forecast for a location."""
    # Implementation
    ...

async def main():
    client = AzureOpenAIResponsesClient(
        project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
        deployment_name=os.environ["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"],
    )

    agent = client.as_agent(
        name="WeatherAgent",
        instructions="You are a weather assistant. Use the tools to provide accurate weather information.",
        tools=[get_weather, get_forecast],
    )

    session = agent.create_session()

    # Multi-turn conversation
    questions = [
        "What's the weather in Seattle?",
        "How about the 5-day forecast?",
        "Compare it to San Francisco",
    ]

    for question in questions:
        print(f"\nUser: {question}")
        result = await agent.run(question, session=session)
        print(f"Agent: {result.text}")

Next Steps

Middleware

Intercept and modify tool invocations with middleware

Sessions

Manage conversation state across tool calls

Observability

Monitor tool performance and usage

Agents

Learn more about agent capabilities

Build docs developers (and LLMs) love