Skip to content

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:

tools.py
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

  1. Always include docstrings - The LLM uses them to understand when to call the tool
  2. Use type hints - Required for proper schema generation
  3. Document parameters - Use the Args: section in Google-style docstrings
  4. 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:

  1. Agent receives user message
  2. Model decides to call a tool
  3. ToolCallEvent is emitted
  4. Tool function is executed
  5. ToolResultEvent is emitted with result
  6. Model continues with tool result

Handling Tool Events

tool_events.py
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:

async_tools.py
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:

error_handling.py
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:

agent = Agent(
    provider=provider,
    session_manager=session_manager,
    tools=[get_weather],  # Only get_weather is available
)
# If LLM tries to call "search_database", it receives:
# "Unknown tool: search_database. Available tools: get_weather"
# The LLM can then retry with a valid tool

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:

complex_params.py
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

  1. Single Responsibility - Each tool should do one thing well
  2. Clear Names - Use descriptive function names like get_weather, not gw
  3. Detailed Docstrings - The LLM relies on these to understand the tool
  4. Return Strings - Return human-readable strings for best LLM comprehension
  5. Handle Errors Gracefully - Raise meaningful exceptions with clear messages
  6. 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)