Core Foundations Beginner ⏱️ 16 min

Function Tools

An agent that can only talk is a chatbot. An agent that can act is a tool. You'll build the Lakeside Outfitters Order Operations Agent — it resolves real requests by calling typed Python functions the model invokes on demand.

How tool calling works

You don't call the tools — the model does. You describe each function with type hints and a docstring; the framework advertises them to the model, which decides which to call, with what arguments, and when to chain several.

The tool-calling loop
sequenceDiagram
    participant U as User
    participant A as Agent
    participant M as Model
    participant T as Your Python tools
    U->>A: "Status of LO-10231 and when will it arrive?"
    A->>M: message + tool schemas
    M-->>A: call lookup_order_status("LO-10231")
    A->>T: execute
    T-->>A: "packed, 1x TENT-4P"
    M-->>A: call estimate_delivery("LO-10231")
    A->>T: execute
    T-->>A: "arrives ~3 days"
    M-->>A: final natural-language answer
    A-->>U: response.text
              
The model plans, the framework executes your Python, results feed back into the answer.

Define tools as plain functions

A tool is just a Python function. Three things make it model-ready:

  1. Type hints on every parameter and the return value.
  2. Annotated[type, Field(description=...)] to describe each argument — the model reads these to fill in arguments correctly.
  3. A docstring — it becomes the tool's description, telling the model when to use it.
tools.py
from typing import Annotated
from datetime import date, timedelta
from pydantic import Field

_ORDERS = {
    "LO-10231": {"status": "packed", "sku": "TENT-4P", "qty": 1, "region": "WA"},
    "LO-10477": {"status": "shipped", "sku": "BAG-65L", "qty": 2, "region": "NY"},
}
_TRANSIT_DAYS = {"WA": 2, "NY": 4, "TX": 3}


def lookup_order_status(
    order_id: Annotated[str, Field(description="Order ID, e.g. 'LO-10231'")],
) -> str:
    """Return the current fulfillment status of an order."""
    order = _ORDERS.get(order_id.upper())
    if not order:
        return f"No order found with ID {order_id}."
    return f"Order {order_id.upper()} is '{order['status']}' ({order['qty']}x {order['sku']})."


def estimate_delivery(
    order_id: Annotated[str, Field(description="Order ID to estimate delivery for")],
) -> str:
    """Estimate the delivery date for an order based on its destination region."""
    order = _ORDERS.get(order_id.upper())
    if not order:
        return f"No order found with ID {order_id}."
    days = _TRANSIT_DAYS.get(order["region"], 5)
    eta = date.today() + timedelta(days=days)
    return f"Order {order_id.upper()} should arrive around {eta.isoformat()} ({days} days)."
🏢
Tools are your trust boundary
The model can request a refund, but your function decides whether to grant it. Put validation, authorization, and audit logging inside the tool — never trust the model to enforce business rules. A tool is where "the AI" meets "your policy."

Register tools with the agent

Pass the functions in a list to create_agent. No decorators, no manual schema.

create the agent
agent = client.create_agent(
    name="OrderOpsAgent",
    instructions=(
        "You are Lakeside Outfitters' order operations assistant. "
        "Use the available tools to answer questions about orders, stock, "
        "delivery dates, and refunds. Be concise and confirm actions you take."
    ),
    tools=[lookup_order_status, check_inventory, estimate_delivery, initiate_refund],
)

Run it

bash
cd modules/04_function_tools
python main.py

Watch the agent chain two tools in the first turn — status and delivery — from a single question:

python main.py
User:  What's the status of order LO-10231 and when will it arrive?
Agent: Order LO-10231 is packed (1x TENT-4P) and should arrive around
2025-01-18, about 2 days out.

User:  Is BAG-65L in stock? Order LO-10477 has 2 of them.
Agent: BAG-65L is currently out of stock. Order LO-10477 already shipped its
2 units, so it's unaffected — but new orders for that SKU can't be fulfilled
until it's restocked.

User:  Refund order LO-10588 — the customer changed their mind.
Agent: Done — refund RF-10588 has been initiated for order LO-10588
(reason: customer changed their mind).
💡
Keep tools small and single-purpose
Four focused tools beat one mega-tool with a mode flag. Small tools are easier for the model to choose correctly, easier to test, and easier to authorize independently.

Design checklist

  1. One responsibility per toolinitiate_refund, not manage_order.
  2. Describe every argument with Field(description=...).
  3. Return strings the model can reason over — include IDs and outcomes.
  4. Validate inside the function — never assume the model passed safe input.
  5. Make side-effects auditable — log who/what/when for refunds and writes.