Skip to main content

Tools

Tools extend agent capabilities by allowing them to perform actions beyond conversation. This guide covers creating, registering, and executing tools.

Overview

Tools are async Python functions that:

  • Have typed parameters with descriptions
  • Return structured results
  • Are automatically converted to OpenAI tool schemas

Creating Tools

Basic Tool

from stella_sdk import tool


@tool
async def get_weather(city: str) -> dict:
"""Get the current weather for a city.

Args:
city: The city name to get weather for

Returns:
Weather data including temperature and conditions
"""
# Call weather API
response = await weather_api.get(city)

return {
"temperature": response.temp,
"conditions": response.conditions,
"humidity": response.humidity
}

Tool with Multiple Parameters

@tool
async def search_products(
query: str,
category: str = None,
max_results: int = 10,
min_price: float = None,
max_price: float = None
) -> dict:
"""Search for products in the catalog.

Args:
query: Search query
category: Optional category filter
max_results: Maximum number of results (default: 10)
min_price: Minimum price filter
max_price: Maximum price filter

Returns:
List of matching products
"""
products = await catalog.search(
query=query,
category=category,
limit=max_results,
price_range=(min_price, max_price)
)

return {
"count": len(products),
"products": [
{"id": p.id, "name": p.name, "price": p.price}
for p in products
]
}

Registering Tools

In Agent Constructor

class MyAgent(BaseAgent):
def __init__(self):
super().__init__()

# Register tools
self.register_tool(get_weather)
self.register_tool(search_products)
self.register_tool(create_order)

From a Module

# tools/__init__.py
from .weather import get_weather
from .products import search_products, create_order

ALL_TOOLS = [get_weather, search_products, create_order]

# agent.py
from tools import ALL_TOOLS

class MyAgent(BaseAgent):
def __init__(self):
super().__init__()
for tool in ALL_TOOLS:
self.register_tool(tool)

Executing Tools

With OpenAI

async def generate_response(self, user_input: str) -> str:
messages = [
{"role": "system", "content": self.system_prompt},
*self.history,
{"role": "user", "content": user_input}
]

# Get tool definitions
tools = self.get_tool_definitions()

# First LLM call
response = await self.openai.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=tools
)

# Handle tool calls
while response.choices[0].message.tool_calls:
tool_calls = response.choices[0].message.tool_calls
messages.append(response.choices[0].message)

for tool_call in tool_calls:
name = tool_call.function.name
args = json.loads(tool_call.function.arguments)

# Show status
await self.send_status("thinking", f"Running {name}...")

# Execute
result = await self.execute_tool(name, args)

# Add result to messages
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(result)
})

# Next LLM call with tool results
response = await self.openai.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=tools
)

return response.choices[0].message.content

Advanced Patterns

Tool with Side Effects

@tool
async def send_email(
to: str,
subject: str,
body: str,
cc: str = None
) -> dict:
"""Send an email to the specified recipient.

Args:
to: Recipient email address
subject: Email subject
body: Email body content
cc: Optional CC recipient

Returns:
Confirmation with message ID
"""
try:
result = await email_service.send(
to=to,
subject=subject,
body=body,
cc=cc
)
return {
"success": True,
"message_id": result.id,
"message": f"Email sent to {to}"
}
except EmailError as e:
return {
"success": False,
"error": str(e)
}

Tool with Database Access

@tool
async def create_ticket(
title: str,
description: str,
priority: str = "medium",
assignee: str = None
) -> dict:
"""Create a support ticket in the system.

Args:
title: Brief ticket title
description: Detailed description
priority: low, medium, or high
assignee: Optional assignee email

Returns:
Created ticket details
"""
ticket = await db.tickets.create(
title=title,
description=description,
priority=priority,
assignee=assignee,
status="open",
created_at=datetime.utcnow()
)

# Notify assignee if specified
if assignee:
await notify_assignee(ticket, assignee)

return {
"ticket_id": ticket.id,
"url": f"https://support.example.com/tickets/{ticket.id}",
"status": "created"
}

Tool with Validation

from pydantic import BaseModel, validator

class OrderInput(BaseModel):
product_id: str
quantity: int
shipping_address: str

@validator('quantity')
def quantity_positive(cls, v):
if v <= 0:
raise ValueError('Quantity must be positive')
return v


@tool
async def place_order(
product_id: str,
quantity: int,
shipping_address: str
) -> dict:
"""Place an order for a product.

Args:
product_id: Product identifier
quantity: Number of items (must be positive)
shipping_address: Delivery address

Returns:
Order confirmation
"""
# Validate input
try:
validated = OrderInput(
product_id=product_id,
quantity=quantity,
shipping_address=shipping_address
)
except ValueError as e:
return {"success": False, "error": str(e)}

# Process order
order = await orders.create(
product_id=validated.product_id,
quantity=validated.quantity,
address=validated.shipping_address
)

return {
"success": True,
"order_id": order.id,
"estimated_delivery": order.estimated_delivery.isoformat()
}

Parallel Tool Execution

When the LLM requests multiple tools, execute them in parallel:

async def execute_tools_parallel(self, tool_calls):
tasks = []
for tool_call in tool_calls:
name = tool_call.function.name
args = json.loads(tool_call.function.arguments)
tasks.append(self.execute_tool(name, args))

results = await asyncio.gather(*tasks)

return [
{
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(result)
}
for tool_call, result in zip(tool_calls, results)
]

Error Handling

Graceful Error Returns

@tool
async def risky_operation(param: str) -> dict:
"""Perform an operation that might fail."""
try:
result = await external_service.call(param)
return {"success": True, "data": result}
except ServiceError as e:
# Return error for LLM to handle
return {
"success": False,
"error": str(e),
"suggestion": "Try again with different parameters"
}

Timeout Handling

import asyncio

@tool
async def slow_operation(query: str) -> dict:
"""Operation with timeout."""
try:
result = await asyncio.wait_for(
external_api.call(query),
timeout=30.0
)
return {"success": True, "data": result}
except asyncio.TimeoutError:
return {
"success": False,
"error": "Operation timed out",
"suggestion": "Try a simpler query"
}

Testing Tools

import pytest
from unittest.mock import AsyncMock

@pytest.mark.asyncio
async def test_get_weather():
# Mock the API
with patch('tools.weather_api') as mock_api:
mock_api.get = AsyncMock(return_value=MockResponse(
temp=72,
conditions="sunny",
humidity=45
))

result = await get_weather("San Francisco")

assert result["temperature"] == 72
assert result["conditions"] == "sunny"
mock_api.get.assert_called_once_with("San Francisco")

Next Steps