Tools¶
Tools allow your agent to call Python functions during conversations, enabling it to perform actions, fetch data, and interact with external systems.
Defining Tools¶
Any Python function can be used as a tool. The function's docstring and type hints are used to generate the tool schema automatically:
def get_weather(city: str) -> str:
"""Get the current weather for a city.
Args:
city: The name of the city to get weather for.
"""
# Your implementation
return f"Weather in {city}: Sunny, 22°C"
def calculate(expression: str) -> str:
"""Evaluate a mathematical expression.
Args:
expression: A mathematical expression like "2 + 2" or "10 * 5"
"""
return str(eval(expression))
Tool Documentation Best Practices
- Always include docstrings - The LLM uses them to understand when to call the tool
- Use type hints - Required for proper schema generation
- Document parameters - Use the
Args:section in Google-style docstrings - Be descriptive - Explain what the tool does and when to use it
Registering Tools¶
Pass tools when creating the agent:
from pathlib import Path
from nagents import Agent, SessionManager
agent = Agent(
provider=provider,
session_manager=SessionManager(Path("sessions.db")),
tools=[get_weather, calculate],
)
Tool Execution Flow¶
sequenceDiagram
participant User
participant Agent
participant LLM
participant Tool
User->>Agent: "What's 15 * 7?"
Agent->>LLM: Send message
LLM->>Agent: ToolCall(calculate, {"expression": "15 * 7"})
Agent->>Tool: Execute calculate("15 * 7")
Tool->>Agent: "105"
Agent->>LLM: Tool result: "105"
LLM->>Agent: "15 * 7 equals 105"
Agent->>User: TextChunkEvent("15 * 7 equals 105")
The execution flow:
- Agent receives user message
- Model decides to call a tool
ToolCallEventis emitted- Tool function is executed
ToolResultEventis emitted with result- Model continues with tool result
Handling Tool Events¶
from nagents import ToolCallEvent, ToolResultEvent, TextChunkEvent
async for event in agent.run("What's 15 * 7?"):
match event:
case ToolCallEvent(name=name, arguments=args):
print(f":material-tools: Calling: {name}({args})")
case ToolResultEvent(result=result, duration_ms=ms):
print(f":material-check: Result: {result} ({ms:.0f}ms)")
case TextChunkEvent(chunk=chunk):
print(chunk, end="")
Async Tools¶
Async functions are fully supported:
import aiohttp
async def fetch_data(url: str) -> str:
"""Fetch data from a URL.
Args:
url: The URL to fetch data from.
"""
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
return await response.text()
async def query_database(query: str) -> str:
"""Execute a database query.
Args:
query: The SQL query to execute.
"""
# Async database operations
result = await db.execute(query)
return str(result)
agent = Agent(
provider=provider,
session_manager=session_manager,
tools=[fetch_data, query_database],
)
Sync vs Async
Both sync and async functions work seamlessly. nagents automatically handles the execution context.
Tool Errors¶
If a tool raises an exception, it's captured and sent to the model:
def risky_operation(param: str) -> str:
"""A tool that might fail.
Args:
param: A required parameter.
"""
if not param:
raise ValueError("param cannot be empty")
return f"Success: {param}"
The ToolResultEvent will have error set instead of result:
async for event in agent.run("Call risky_operation"):
if isinstance(event, ToolResultEvent):
if event.error:
print(f":material-alert: Tool failed: {event.error}")
else:
print(f":material-check: Result: {event.result}")
Handling Tool Hallucinations¶
Sometimes LLMs may attempt to call tools that don't exist ("hallucinated" tools).
By default, nagents returns an error message to the LLM listing available tools:
For stricter control, raise an exception:
from nagents import Agent, ToolHallucinationError
agent = Agent(
provider=provider,
session_manager=session_manager,
tools=[get_weather],
fail_on_invalid_tool=True,
)
try:
async for event in agent.run("Search for restaurants"):
...
except ToolHallucinationError as e:
print(f"LLM tried to call: {e.tool_name}")
print(f"Available tools: {e.available_tools}")
When to Use Strict Mode
Use fail_on_invalid_tool=True when:
- You need deterministic behavior
- Tool hallucinations indicate a problem with your prompts
- You want to catch and handle these cases explicitly
Complex Parameters¶
Tools can have complex parameter types:
from typing import Optional
def search(
query: str,
limit: int = 10,
filters: Optional[dict] = None,
) -> str:
"""Search for items.
Args:
query: Search query string.
limit: Maximum results to return (default: 10).
filters: Optional filters to apply.
"""
results = perform_search(query, limit, filters)
return str(results)
def create_event(
title: str,
date: str,
attendees: list[str],
location: Optional[str] = None,
) -> str:
"""Create a calendar event.
Args:
title: Event title.
date: Event date in YYYY-MM-DD format.
attendees: List of attendee email addresses.
location: Optional event location.
"""
...
Supported Parameter Types
| Type | Description |
|---|---|
str |
String values |
int |
Integer values |
float |
Floating point values |
bool |
Boolean values |
list[T] |
Arrays of type T |
dict |
JSON objects |
Optional[T] |
Optional parameters |
| Default values | Parameters with defaults are optional |
Best Practices¶
Tool Design Guidelines
- Single Responsibility - Each tool should do one thing well
- Clear Names - Use descriptive function names like
get_weather, notgw - Detailed Docstrings - The LLM relies on these to understand the tool
- Return Strings - Return human-readable strings for best LLM comprehension
- Handle Errors Gracefully - Raise meaningful exceptions with clear messages
- Keep It Simple - Avoid overly complex parameter structures
Well-Designed Tool Example
def search_products(
query: str,
category: Optional[str] = None,
max_price: Optional[float] = None,
in_stock_only: bool = True,
) -> str:
"""Search for products in the catalog.
Use this tool when the user wants to find products by name,
category, or other criteria.
Args:
query: Search terms (e.g., "red shoes", "wireless headphones").
category: Optional category filter (e.g., "electronics", "clothing").
max_price: Maximum price in USD. Omit for no price limit.
in_stock_only: Only return products currently in stock (default: True).
Returns:
A formatted list of matching products with names, prices, and availability.
"""
products = catalog.search(
query=query,
category=category,
max_price=max_price,
in_stock=in_stock_only,
)
if not products:
return f"No products found matching '{query}'."
lines = [f"Found {len(products)} products:"]
for p in products[:10]: # Limit results
status = "In Stock" if p.in_stock else "Out of Stock"
lines.append(f"- {p.name}: ${p.price:.2f} ({status})")
return "\n".join(lines)