Tools (also called functions) allow your voice assistant to perform actions and retrieve information. When the model determines it needs external data or functionality, it calls your tool functions.
Use the @client.tool decorator to define tools:
from easy_sonic import Sonic
client = Sonic()
@client.tool
def get_weather(city: str) -> str:
"""Get the current weather for a city.
Args:
city: The name of the city.
"""
# Your implementation here
return f"Sunny and 72°F in {city}"
await client.chat("You are a weather assistant.")-
The decorator extracts information from your function:
- Name: From the function name
- Description: From the docstring
- Parameters: From type hints and docstring
-
This generates a JSON schema that tells the model what the tool does
-
When the model needs to use the tool, it calls your function with the appropriate arguments
-
The result is sent back to the model to continue the conversation
Type hints are used to generate the parameter schema:
@client.tool
def search_products(
query: str, # Required string
max_results: int = 10, # Optional integer with default
in_stock: bool = True, # Optional boolean with default
) -> list:
"""Search for products."""
...Supported types:
str→ JSONstringint→ JSONintegerfloat→ JSONnumberbool→ JSONbooleanlist→ JSONarraydict→ JSONobject
Sonic extracts descriptions from docstrings. Both Google and reStructuredText styles work:
Google Style:
@client.tool
def book_flight(origin: str, destination: str, date: str) -> dict:
"""Book a flight between two cities.
Args:
origin: Departure city or airport code.
destination: Arrival city or airport code.
date: Travel date in YYYY-MM-DD format.
"""
...reStructuredText Style:
@client.tool
def book_flight(origin: str, destination: str, date: str) -> dict:
"""Book a flight between two cities.
:param origin: Departure city or airport code.
:param destination: Arrival city or airport code.
:param date: Travel date in YYYY-MM-DD format.
"""
...Tools can be asynchronous for I/O operations:
@client.tool
async def fetch_stock_price(symbol: str) -> dict:
"""Get the current stock price.
Args:
symbol: Stock ticker symbol (e.g., AAPL).
"""
async with aiohttp.ClientSession() as session:
async with session.get(f"https://api.example.com/stocks/{symbol}") as response:
return await response.json()Override the auto-generated name:
@client.tool(name="get_current_time")
def time_now() -> str:
"""Get the current time."""
from datetime import datetime
return datetime.now().strftime("%H:%M:%S")Override the docstring description:
@client.tool(description="Retrieves real-time weather data for any city worldwide")
def get_weather(city: str) -> dict:
"""Old description that will be overridden."""
...Register multiple tools on the same client:
client = Sonic()
@client.tool
def get_weather(city: str) -> str:
"""Get weather for a city."""
...
@client.tool
def get_forecast(city: str, days: int = 3) -> list:
"""Get weather forecast."""
...
@client.tool
def set_reminder(message: str, time: str) -> dict:
"""Set a reminder."""
...
# All tools are available in the conversation
await client.chat("You are a helpful assistant with weather and reminder capabilities.")Monitor tool usage with callbacks:
def on_tool_call(name: str, args: dict) -> None:
print(f"🔧 Calling {name} with {args}")
def on_tool_result(name: str, result) -> None:
print(f"✅ {name} returned: {result}")
await client.chat(
system_prompt="You are helpful.",
on_tool_call=on_tool_call,
on_tool_result=on_tool_result,
)If a tool raises an exception, the error is sent to the model as the result:
@client.tool
def risky_operation(param: str) -> str:
"""A tool that might fail."""
if not param:
raise ValueError("Parameter cannot be empty")
return "Success"The model receives the error and can explain it to the user or try a different approach.
@client.tool
async def query_customers(
name: str = None,
email: str = None,
limit: int = 10
) -> list:
"""Search for customers in the database.
Args:
name: Filter by customer name (partial match).
email: Filter by email address.
limit: Maximum number of results.
"""
async with get_db_connection() as conn:
query = "SELECT * FROM customers WHERE 1=1"
params = []
if name:
query += " AND name ILIKE %s"
params.append(f"%{name}%")
if email:
query += " AND email = %s"
params.append(email)
query += f" LIMIT {limit}"
return await conn.fetch(query, *params)import httpx
@client.tool
async def send_email(to: str, subject: str, body: str) -> dict:
"""Send an email to a recipient.
Args:
to: Recipient email address.
subject: Email subject line.
body: Email body content.
"""
async with httpx.AsyncClient() as client:
response = await client.post(
"https://api.email-service.com/send",
json={"to": to, "subject": subject, "body": body},
headers={"Authorization": f"Bearer {API_KEY}"}
)
return response.json()import math
@client.tool
def calculate(expression: str) -> dict:
"""Evaluate a mathematical expression.
Args:
expression: A math expression like "2 + 2" or "sqrt(16)".
"""
# Safe evaluation with limited functions
safe_dict = {
"abs": abs, "round": round, "min": min, "max": max,
"sqrt": math.sqrt, "sin": math.sin, "cos": math.cos,
"pi": math.pi, "e": math.e,
}
try:
result = eval(expression, {"__builtins__": {}}, safe_dict)
return {"expression": expression, "result": result}
except Exception as e:
return {"expression": expression, "error": str(e)}- Clear descriptions - Write detailed docstrings so the model knows when to use each tool
- Type everything - Use type hints for all parameters and return values
- Handle errors gracefully - Return informative error messages instead of crashing
- Keep tools focused - One tool should do one thing well
- Validate inputs - Check parameters before performing operations
- Use async for I/O - Network calls and database queries should be async
- See the Examples for more tool patterns
- Read the API Reference
- Learn about Custom Audio handling