What is a Tool
A tool is a function that an agent can call. But more specifically, it is a bridge between the LLM's reasoning and the real world.
The LLM can think, but it cannot act. Tools let it act.
The Tool Schema
When you create a tool, PydanticAI generates a JSON schema that describes it. This schema is sent to the LLM so it knows what tools are available.
The LLM reads this schema to understand:
- What the tool does (from the docstring)
- What parameters it needs (from type hints)
- What each parameter means (from Args section in docstring)
@agent.tool_plain
def get_exchange_rate(from_currency: str, to_currency: str) -> float:
"""Get current exchange rate between two currencies.
Args:
from_currency: Source currency code (e.g., 'USD', 'EUR')
to_currency: Target currency code (e.g., 'AED', 'GBP')
"""
return rates.get((from_currency, to_currency), 0.0)
# Generated JSON schema (simplified):
# {
# "name": "get_exchange_rate",
# "description": "Get current exchange rate between two currencies.",
# "parameters": {
# "properties": {
# "from_currency": {"type": "string", "description": "Source currency code"},
# "to_currency": {"type": "string", "description": "Target currency code"}
# },
# "required": ["from_currency", "to_currency"]
# }
# }Basic Tool - @agent.tool_plain
The simplest way to create a tool. Use when your tool does not need any agent context.
from pydantic_ai import Agent
agent = Agent(
'openai:gpt-4o',
instructions='Help users with currency conversions.'
)
@agent.tool_plain
def get_exchange_rate(from_currency: str, to_currency: str) -> dict:
"""Get the exchange rate between two currencies.
Args:
from_currency: Source currency code (e.g., 'USD', 'EUR')
to_currency: Target currency code (e.g., 'AED', 'GBP')
Returns:
Dictionary with exchange rate information
"""
# Simulated rates (in production, call a real API)
rates = {
('USD', 'AED'): 3.67,
('EUR', 'USD'): 1.08,
('GBP', 'USD'): 1.27,
}
rate = rates.get((from_currency, to_currency))
if rate:
return {'status': 'success', 'from': from_currency, 'to': to_currency, 'rate': rate}
else:
return {'status': 'error', 'message': f'Rate not available for {from_currency} to {to_currency}'}
# Use it
result = agent.run_sync('How much is 100 USD in AED?')
print(result.output)
# Output: 100 USD is 367 AED (at current rate of 3.67)Tool with Context - @agent.tool
Use when your tool needs access to dependencies or agent context.
from pydantic_ai import Agent, RunContext
from dataclasses import dataclass
@dataclass
class ApiConfig:
api_key: str
base_url: str
agent = Agent(
'openai:gpt-4o',
deps_type=ApiConfig # Agent expects this dependency
)
# Tool WITH context (has access to dependencies)
@agent.tool
def fetch_user_data(ctx: RunContext[ApiConfig], user_id: int) -> dict:
"""Fetch user data from API.
Args:
user_id: The user's ID
"""
# Can access ctx.deps.api_key, ctx.deps.base_url
# In real life, you would make an API call here
return {
'user_id': user_id,
'name': 'John Doe',
'api_used': ctx.deps.base_url
}
# When running, provide dependencies
config = ApiConfig(api_key='secret-key-123', base_url='https://api.example.com')
result = agent.run_sync('Get info for user 42', deps=config)Difference Between tool_plain and tool
| Feature | @agent.tool_plain | @agent.tool | |---------|-------------------|-------------| | Context access | No | Yes (via ctx) | | Dependencies | No | Yes (ctx.deps) | | Usage tracking | No | Yes (ctx.usage) | | Use case | Simple, standalone tools | Tools that need config or state |
Using an Agent as a Tool
One of the most powerful patterns: an agent can call another agent as a tool.
Why this is powerful:
- Main agent focuses on coordination
- Specialist agent focuses on one task
- You can use different models (cheap for simple tasks, expensive for complex)
- Easy to test and improve each agent independently
from pydantic_ai import Agent, RunContext
# Specialist agent
translator = Agent(
'openai:gpt-4o',
instructions='You are a translator. Translate text to the requested language. Return only the translation.'
)
# Main agent
assistant = Agent(
'openai:gpt-4o',
instructions='You are a helpful assistant. Use the translator when users need translations.'
)
@assistant.tool
async def translate_text(
ctx: RunContext[None],
text: str,
target_language: str
) -> str:
"""Translate text to another language.
Args:
text: The text to translate
target_language: Target language (e.g., 'Spanish', 'French')
"""
result = await translator.run(
f'Translate to {target_language}: {text}',
usage=ctx.usage # Track combined usage across both agents
)
return result.output
# Use it
result = assistant.run_sync('How do I say "Hello, how are you?" in Spanish?')
print(result.output)
# Output: "Hola, como estas?"Tool Error Handling with ModelRetry
Sometimes the LLM provides bad input to a tool. Instead of failing, you can ask the LLM to try again.
When ModelRetry is raised:
- Error message goes back to the LLM
- LLM sees the error and tries again with (hopefully) better input
- Repeats until success or max retries reached
This is much better than crashing. The LLM learns from the error and self-corrects.
from pydantic_ai import Agent, ModelRetry
agent = Agent('openai:gpt-4o')
@agent.tool_plain(retries=3) # Allow up to 3 retries
def search_database(query: str) -> str:
"""Search our database for information.
Args:
query: Search query, must be at least 3 characters
"""
if len(query) < 3:
raise ModelRetry('Query too short. Please provide at least 3 characters.')
if 'drop table' in query.lower():
raise ModelRetry('Invalid query. Please use different keywords.')
# Do actual search...
return f'Found results for: {query}'Complete Example: Currency Converter Agent
A practical multi-tool agent that shows how tools work together. The agent decided to call both tools and combine the results. You did not tell it which tools to call or in what order.
from pydantic_ai import Agent
currency_agent = Agent(
'openai:gpt-4o',
instructions='''You are a currency conversion assistant.
When users ask about conversions:
1. First get the exchange rate
2. Then calculate the conversion fee
3. Give the final amount including fees
Be precise with numbers. Always show your calculation.'''
)
@currency_agent.tool_plain
def get_exchange_rate(from_currency: str, to_currency: str) -> dict:
"""Get current exchange rate between two currencies.
Args:
from_currency: Source currency code (USD, EUR, GBP, AED, etc.)
to_currency: Target currency code
"""
rates = {
('USD', 'AED'): 3.67,
('EUR', 'AED'): 3.98,
('GBP', 'AED'): 4.65,
('USD', 'EUR'): 0.92,
}
key = (from_currency.upper(), to_currency.upper())
rate = rates.get(key)
if rate:
return {'status': 'success', 'rate': rate}
return {'status': 'error', 'message': 'Rate not found'}
@currency_agent.tool_plain
def calculate_conversion_fee(amount: float, fee_percent: float = 1.5) -> dict:
"""Calculate the conversion fee for a transaction.
Args:
amount: The amount being converted
fee_percent: Fee percentage (default 1.5%)
"""
fee = amount * fee_percent / 100
return {
'original_amount': amount,
'fee_percent': fee_percent,
'fee_amount': round(fee, 2),
'total_with_fee': round(amount + fee, 2)
}
# Use it
result = currency_agent.run_sync(
'I want to convert 500 USD to AED. What will I get after fees?'
)
print(result.output)- 1Tools bridge LLM and reality. The LLM thinks, tools act.
- 2Schemas matter. PydanticAI generates schemas from your function signature and docstring. Write good docstrings.
- 3Two decorator types: @agent.tool_plain for simple tools, @agent.tool when you need dependencies or usage tracking.
- 4Agents as tools. Powerful pattern for building specialist teams.
- 5ModelRetry for self-correction. Let tools ask for better input instead of crashing.