Appearance
Building Tools
A practical guide to building effective tools for your Elsai agents.
Anatomy of a good tool
python
from elsai import tool
@tool
def search_products(
query: str,
category: str = "all",
max_price: float | None = None,
in_stock_only: bool = True,
) -> list[dict]:
"""Search the product catalog for items matching the query.
Use this when the user asks about specific products, wants to find items
by name or description, or asks what we carry.
Args:
query: Natural language search query (e.g. "red sneakers size 10").
category: Product category to filter by. Use "all" to search everything.
Valid values: "all", "shoes", "clothing", "accessories".
max_price: Maximum price in USD. Omit to show all prices.
in_stock_only: When True, only return currently available items.
Returns:
A list of product dicts with keys: id, name, price, category, in_stock.
"""
results = catalog.search(
query=query,
category=category,
max_price=max_price,
in_stock_only=in_stock_only,
)
return [
{"id": p.id, "name": p.name, "price": p.price, "category": p.category, "in_stock": p.in_stock}
for p in results
]Key principles:
- The docstring description explains when to call the tool (the model uses this to decide)
Argssection documents each parameter the model must fill in- Return value is clear and machine-readable
Error signalling
Raise exceptions to tell the model something went wrong:
python
@tool
def get_order(order_id: str) -> dict:
"""Retrieve order details by order ID.
Args:
order_id: The order ID (format: ORD-XXXXXX).
"""
if not order_id.startswith("ORD-"):
raise ValueError(f"Invalid order ID format: '{order_id}'. Must start with 'ORD-'.")
order = db.get_order(order_id)
if not order:
raise LookupError(f"Order '{order_id}' not found.")
return {"id": order.id, "status": order.status, "total": order.total}Returning image / file content
Return binary content as base64:
python
import base64
from elsai import tool
@tool
def get_chart(metric: str, period: str = "7d") -> dict:
"""Generate a chart image for a metric.
Args:
metric: The metric to chart (e.g. "revenue", "dau").
period: Time period — "7d", "30d", or "90d".
"""
image_bytes = chart_generator.render(metric, period)
return {
"content": [
{
"image": {
"format": "png",
"source": {"bytes": base64.b64encode(image_bytes).decode()},
}
},
{"text": f"Chart for {metric} over {period}"},
]
}Tools that need credentials
Inject secrets via closure instead of hard-coding:
python
import os
from elsai import tool
def make_email_tool(api_key: str):
@tool
def send_email(to: str, subject: str, body: str) -> str:
"""Send an email.
Args:
to: Recipient email address.
subject: Email subject line.
body: Email body text.
"""
email_service.send(api_key=api_key, to=to, subject=subject, body=body)
return f"Email sent to {to}."
return send_email
email_tool = make_email_tool(os.environ["EMAIL_API_KEY"])
agent = Agent(tools=[email_tool])Database tools
python
import sqlite3
from elsai import tool
DB_PATH = "app.db"
@tool
def query_database(sql: str) -> list[dict]:
"""Run a read-only SQL query against the application database.
Only SELECT statements are allowed. Do not use INSERT, UPDATE, DELETE, or DROP.
Args:
sql: A valid SELECT SQL statement.
"""
if not sql.strip().upper().startswith("SELECT"):
raise ValueError("Only SELECT queries are allowed.")
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
cursor = conn.execute(sql)
rows = [dict(row) for row in cursor.fetchall()]
conn.close()
return rowsHTTP API tools
python
import httpx
from elsai import tool
@tool
async def call_weather_api(city: str, units: str = "metric") -> dict:
"""Get current weather conditions for a city.
Args:
city: City name (e.g. "London", "Tokyo").
units: Temperature units — "metric" (Celsius) or "imperial" (Fahrenheit).
"""
async with httpx.AsyncClient() as client:
response = await client.get(
"https://api.openweathermap.org/data/2.5/weather",
params={"q": city, "units": units, "appid": WEATHER_API_KEY},
)
response.raise_for_status()
data = response.json()
return {
"city": city,
"temperature": data["main"]["temp"],
"feels_like": data["main"]["feels_like"],
"conditions": data["weather"][0]["description"],
"humidity": data["main"]["humidity"],
}Tool naming
Tool names must match [a-zA-Z0-9_-]{1,64}. Prefer snake_case:
python
@tool
def get_user_profile(user_id: str) -> dict: # ✅ good
...
@tool
def GetUserProfile(user_id: str) -> dict: # ⚠️ works but not idiomatic
...Testing tools in isolation
python
def test_search_products():
result = search_products("red shoes", category="shoes", max_price=100.0)
assert isinstance(result, list)
assert all(p["category"] == "shoes" for p in result)
assert all(p["price"] <= 100.0 for p in result)Then test the full agent:
python
agent = Agent(tools=[search_products])
result = agent("Find red sneakers under $100")
assert "shoes" in str(result).lower() or "sneakers" in str(result).lower()