Skip to content
AI Agents

Adding Tools and Function Calling to AI Agents

Design a fictional shipping-quote tool, validate arguments in the executor, and return structured results to a model.

Overview

For learners who want to complete this practical exercise with inspectable sample data and explicit checks.

  • Write a narrow schema instead of letting a model execute arbitrary code.
  • Validate data even with strict=true.
  • Preserve call_id and structured errors in tool-result handling.

You will build: shipping.py containing a schema, an executor, and valid/invalid input checks.

Prerequisites

  • Python 3.10+; understand the Responses loop from the first-agent tutorial.

The executor and tests run offline. Optional model integration uses the openai SDK and billed API access as in the previous tutorial.

Step-by-step tutorial

Conceptual practice workflow; not measured results.

Step 1. Choose a read-only operation

The tool applies a fictional formula: local starts at 3, regional at 6, and each kg adds 2 DEMO units. It creates no shipment, accesses no website, and calculates no real taxes. Specify the unit and the bound 0 < weight_kg <= 100.

Step 2. Write the schema and executor

Save this as shipping.py. Decimal produces a two-decimal result, and validation rejects bool values and extra keys. The model proposes a name and JSON arguments; the application decides whether to execute the function.

PYTHON
import json
from decimal import Decimal

SCHEMA = {"type": "function", "name": "shipping_quote",
    "description": "Fictional training shipping estimate, not a purchase.",
    "strict": True, "parameters": {"type": "object", "properties": {
        "zone": {"type": "string", "enum": ["local", "regional"]},
        "weight_kg": {"type": "number"}},
        "required": ["zone", "weight_kg"], "additionalProperties": False}}

def execute_tool(name, raw):
    if name != "shipping_quote":
        raise ValueError("unknown tool")
    args = json.loads(raw)
    if not isinstance(args, dict) or set(args) != {"zone", "weight_kg"}:
        raise ValueError("unexpected arguments")
    zone, weight = args["zone"], args["weight_kg"]
    if zone not in ("local", "regional"):
        raise ValueError("invalid zone")
    if type(weight) not in (int, float) or not 0 < weight <= 100:
        raise ValueError("invalid weight")
    base = Decimal("3") if zone == "local" else Decimal("6")
    amount = base + Decimal(str(weight)) * Decimal("2")
    return {"amount": str(amount.quantize(Decimal("0.01"))),
            "currency": "DEMO", "fictional": True}

if __name__ == "__main__":
    assert execute_tool("shipping_quote", '{"zone":"local","weight_kg":2}')["amount"] == "7.00"
    for raw in ['{"zone":"other","weight_kg":1}',
                '{"zone":"local","weight_kg":true}',
                '{"zone":"local","weight_kg":-1}']:
        try:
            execute_tool("shipping_quote", raw)
        except ValueError:
            continue
        raise AssertionError("invalid input accepted")
    print("shipping checks passed")

Step 3. Run independent checks

SHELL
python3 shipping.py

Add malformed JSON, missing-field, and weight_kg=0 cases. Unknown tool names must fail before the formula executes. Expand the schema only when needed rather than accepting every dict to avoid model errors.

Step 4. Connect it to the existing model loop

In a copy of the previous agent.py, import SCHEMA and execute_tool from shipping.py. Replace tools=TOOLS with tools=[SCHEMA] and execute(call.name, call.arguments) with execute_tool(call.name, call.arguments). Change instructions to call shipping_quote before quoting and identify DEMO as fictional. Keep history.extend(response.output), function_call_output, and call_id intact.

PROMPT · INSTRUCTIONS
Estimate the DEMO fee for a 2 kg parcel in the local zone using shipping_quote. Ask for missing zone or weight rather than guessing. Do not create an order or request payment.

Step 5. Return errors without losing the trace

When arguments are invalid, the earlier loop returns JSON error with the same call_id so the model can correct the request or explain the limitation. Never turn failure into amount=0, which could be mistaken for free shipping. Keep turn limits and timeouts for the new tool.

Verification

  • local/2 kg returns amount="7.00" and fictional=true.
  • Unknown zones, booleans, negative values, and extra arguments are rejected.

Troubleshooting

Plausible JSON fails in code

Cause: Field names, types, or units violate the contract.

Fix: Check schema and executor together using non-sensitive reproducible samples.

The model proposes a non-allowlisted tool

Cause: The dispatcher is not enforcing allowed tools.

Fix: Reject the name; do not use eval, exec, or arbitrary getattr dispatch.

Exercises & extensions

  • Add a read-only service-area tool with its own validation.
  • Apply the same allowlist principle to research-assistant retrieval.

A strict schema does not establish source accuracy, access permissions, or business safety. DEMO is not a real currency or quote.

References

  1. OpenAI function calling
  2. Python Decimal

Keep practicing