Skip to content

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)
  • Args section 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 rows

HTTP 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()

Copyright © 2026 Elsai Foundry.