Chapter 5: Function Calling & Tools

In previous chapters, our AI applications mostly followed this pattern:

User → AI Model → Text Response

But real business applications need to perform actions, not just talk about them. For example:

User: What is the weather in Mumbai?
AI: I need weather data.
AI → Weather API
Weather API: 32°C
AI: The current temperature is 32°C.

The AI model itself has no direct knowledge of how to call your internal business systems. Instead, you give it tools/functions that it can request. This capability is commonly called function calling or tool calling. In the Responses API approach, you define tools, the model requests a tool call, your application executes the function, and the result goes back to the model for the final response.

5.1 What Is Function Calling?

Function calling lets an AI model request that your application execute a specific function.

User: What is the weather in Mumbai?

The AI determines it needs the weather tool and produces a structured tool request:

{
  "name": "get_weather",
  "arguments": {
    "city": "Mumbai"
  }
}

Your application executes:

get_weather("Mumbai")

and returns:

{
  "city": "Mumbai",
  "temperature": 32,
  "condition": "Sunny"
}

The AI finally responds:

The current weather in Mumbai is 32°C and sunny.

Notice: the model never touches the weather API itself — it only asks your application to fetch that data.

5.2 Without Tools vs With Tools

Without Tools:

User → AI → Text
User: What is the current temperature?
AI: I don't have live weather information.

With Tools:

User → AI → Tool Request → Your Application →
External API → Tool Result → AI → Final Response

This is dramatically more powerful — it turns the model from a text generator into an interface over live, real-world systems.

5.3 Real-World Use Cases

  • Customer Support: "Where is my order?" → get_order_status()
  • Banking: "What is my account balance?" → get_account_balance()
  • E-Commerce: "Add a laptop to my cart." → add_to_cart()
  • Calendar: "Schedule a meeting tomorrow." → create_calendar_event()
  • Databases: "Find all overdue invoices." → query_database()
  • Internal Systems: "Create a support ticket." → create_support_ticket()

The AI becomes an intelligent interface layered over your existing systems.

5.4 Important Architecture

The AI model should never directly access your database or business systems.

┌────────────────┐
│    AI Model     │
└───────┬────────┘
        │
    Tool Request
        │
        ▼
┌────────────────┐
│ Your Application│
└───────┬────────┘
        │
        ▼
┌────────────────┐
│ Business Logic  │
└───────┬────────┘
        │
        ▼
┌────────────────┐
│  Database / API │
└────────────────┘

This separation matters for security, validation, authorization, auditing, and error handling. The model requests an action — your application decides whether it’s actually allowed.

5.5 The Tool Calling Lifecycle

1. User sends a request
        ↓
2. AI analyzes the request
        ↓
3. AI decides whether a tool is needed
        ↓
4. AI generates a tool call
        ↓
5. Your application validates the tool call
        ↓
6. Your application executes the function
        ↓
7. Tool result is returned to the AI
        ↓
8. AI generates the final response

Example:

User: What is the status of order 12345?
AI: Call get_order_status(order_id="12345")
Application: Order is shipped.
AI: Your order has been shipped.

5.6 Our Industry Project

We’ll build an AI Customer Support Assistant able to:

  1. Check order status
  2. Search product information
  3. Create a support ticket

Example:

User: Where is order ORD-1001?

The AI requests get_order_status(order_id="ORD-1001"), our application executes it, and the AI responds naturally with the result.

5.7 Project Architecture

ai-support-agent/
│
├── src/
│   └── ai_support_agent/
│       │
│       ├── main.py
│       │
│       ├── config/
│       │   ├── __init__.py
│       │   └── settings.py
│       │
│       ├── clients/
│       │   ├── __init__.py
│       │   └── openai_client.py
│       │
│       ├── tools/
│       │   ├── __init__.py
│       │   ├── definitions.py
│       │   ├── order_tools.py
│       │   ├── product_tools.py
│       │   └── support_tools.py
│       │
│       ├── services/
│       │   ├── __init__.py
│       │   └── assistant_service.py
│       │
│       ├── repositories/
│       │   ├── __init__.py
│       │   └── order_repository.py
│       │
│       ├── models/
│       │   ├── __init__.py
│       │   └── order.py
│       │
│       └── utils/
│           ├── __init__.py
│           └── logging_config.py
│
├── tests/
│   ├── test_order_tools.py
│   ├── test_tool_validation.py
│   └── test_assistant_service.py
│
├── .env
├── .env.example
├── .gitignore
├── requirements.txt
└── README.md

We’re intentionally separating: AI → Tools → Business Logic → Repositories → Data Sources.

5.8 Step 1: Define the Business Problem

Our assistant supports:

  • Order Status: Where is my order ORD-1001?
  • Product Search: Do you have wireless headphones?
  • Support Ticket: I was charged twice. Create a support ticket.

5.9 Step 2: Create an Order Model

Create src/ai_support_agent/models/order.py:

from pydantic import BaseModel

class Order(BaseModel):
    order_id: str
    status: str
    estimated_delivery: str | None = None

Example:

{
  "order_id": "ORD-1001",
  "status": "shipped",
  "estimated_delivery": "2026-07-26"
}

5.10 Step 3: Create an Order Repository

Create src/ai_support_agent/repositories/order_repository.py. For learning purposes, we’ll start with an in-memory data source:

from ai_support_agent.models.order import Order

class OrderRepository:
    def __init__(self) -> None:
        self.orders = {
            "ORD-1001": Order(
                order_id="ORD-1001",
                status="shipped",
                estimated_delivery="2026-07-26",
            ),
            "ORD-1002": Order(
                order_id="ORD-1002",
                status="processing",
                estimated_delivery=None,
            ),
        }

    def get_by_id(
        self,
        order_id: str,
    ) -> Order | None:
        return self.orders.get(order_id)

Later, this repository could connect to PostgreSQL, a REST API, MongoDB, or an ERP system — the AI never needs to know how the data is actually stored.

5.11 Step 4: Create the Business Tool

Create src/ai_support_agent/tools/order_tools.py:

from ai_support_agent.repositories.order_repository import (
    OrderRepository,
)

class OrderTools:
    def __init__(
        self,
        order_repository: OrderRepository,
    ) -> None:
        self.order_repository = order_repository

    def get_order_status(
        self,
        order_id: str,
    ) -> dict:
        order_id = order_id.strip()

        if not order_id:
            raise ValueError(
                "Order ID cannot be empty."
            )

        order = self.order_repository.get_by_id(
            order_id
        )

        if order is None:
            return {
                "found": False,
                "order_id": order_id,
            }

        return {
            "found": True,
            "order_id": order.order_id,
            "status": order.status,
            "estimated_delivery": (
                order.estimated_delivery
            ),
        }

This is the actual business operation — the model never executes this code directly; your application does.

5.12 Step 5: Define the Tool for the AI

The model needs to know the tool name, description, parameters, and required fields:

{
  "type": "function",
  "name": "get_order_status",
  "description": "Retrieve the status of a customer order.",
  "parameters": {
    "type": "object",
    "properties": {
      "order_id": {
        "type": "string"
      }
    },
    "required": [
      "order_id"
    ]
  }
}

This tells the model: you may request this function when the user needs order status information. The tool schema is essentially an interface contract between the model and your application.

5.13 Tool Definition Module

Create src/ai_support_agent/tools/definitions.py:

ORDER_STATUS_TOOL = {
    "type": "function",
    "name": "get_order_status",
    "description": (
        "Retrieve the status of a customer order."
    ),
    "parameters": {
        "type": "object",
        "properties": {
            "order_id": {
                "type": "string",
                "description": (
                    "The unique order identifier."
                ),
            },
        },
        "required": ["order_id"],
        "additionalProperties": False,
    },
}

This is deliberately separate from the actual implementation:

Tool Definition ≠ Tool Implementation

5.14 The Tool Registry

As applications grow, you’ll accumulate many tools: get_order_status, search_products, create_support_ticket, cancel_order, refund_payment. Instead of a growing pile of if statements, create a registry:

TOOL_REGISTRY = {
    "get_order_status": order_tools.get_order_status,
}
Tool Name → Registry → Python Function

This is cleaner and scales gracefully as tools multiply.

5.15 The Tool Calling Loop

User Input
    ↓
Send to AI
    ↓
Does AI request a tool?
        │
   ┌────┴────┐
  No         Yes
   │          │
   │          ▼
   │     Execute Tool
   │          │
   │          ▼
   │     Send Result
   │          │
   └──────────┘
        ↓
Final Response

Pseudo-code:

response = call_openai(user_message)

while response_requires_tool(response):
    tool_call = get_tool_call(response)
    result = execute_tool(tool_call)
    response = send_tool_result_to_openai(
        result
    )

return final_text(response)

This is one of the most important patterns in agentic AI applications — you’ll see it again and again.

5.16 Tool Execution Must Be Controlled

Never blindly execute arbitrary function names.

Dangerous:

function_name = model_output["name"]
globals()[function_name](**arguments)

This is unsafe — it lets anything the model outputs run as code.

Better:

TOOL_REGISTRY = {
    "get_order_status": order_tools.get_order_status,
    "search_products": product_tools.search_products,
}
tool = TOOL_REGISTRY.get(
    function_name
)

if tool is None:
    raise ValueError(
        "Unknown tool requested."
    )

Only explicitly approved tools can ever run.

5.17 Tool Argument Validation

The model may generate {"order_id": 12345} while your application actually requires the format ORD-12345. Always validate tool arguments:

import re

def validate_order_id(
    order_id: str,
) -> str:
    if not re.match(
        r"^ORD-\d+$",
        order_id,
    ):
        raise ValueError(
            "Invalid order ID format."
        )
    return order_id

This protects your business logic from malformed or unexpected input.

5.18 Tool Errors

Suppose the model requests get_order_status("ORD-9999") but the order doesn’t exist. The tool should return a controlled result:

{
  "found": false,
  "order_id": "ORD-9999"
}

The AI can then say:

I couldn't find an order with ID ORD-9999.
Please verify the order number.

Never expose internal stack traces or raw error messages directly to the user.

5.19 Multiple Tools

┌─────────────────────┐
│    AI Assistant       │
└──────────┬──────────┘
           │
   ┌───────┼────────┐
   ▼       ▼        ▼
 Order   Product   Support
  Tool     Tool      Tool

The AI chooses the appropriate tool based on what the user is actually asking for.

5.20 Tool 2: Product Search

Create src/ai_support_agent/tools/product_tools.py:

class ProductTools:
    def search_products(
        self,
        query: str,
    ) -> dict:
        if not query.strip():
            raise ValueError(
                "Search query cannot be empty."
            )

        products = [
            {
                "name": "Wireless Headphones",
                "price": 99.99,
                "available": True,
            },
            {
                "name": "Mechanical Keyboard",
                "price": 129.99,
                "available": True,
            },
        ]

        matches = [
            product
            for product in products
            if query.lower()
            in product["name"].lower()
        ]

        return {
            "query": query,
            "products": matches,
        }

Example:

User: Do you have wireless headphones?
Tool: search_products(query="wireless headphones")
Result:
{
  "products": [
    {
      "name": "Wireless Headphones",
      "price": 99.99,
      "available": true
    }
  ]
}

5.21 Tool 3: Create Support Ticket

Create src/ai_support_agent/tools/support_tools.py:

import uuid

class SupportTools:
    def create_support_ticket(
        self,
        issue: str,
        priority: str,
    ) -> dict:
        if not issue.strip():
            raise ValueError(
                "Issue cannot be empty."
            )

        allowed_priorities = {
            "low",
            "medium",
            "high",
        }

        if priority not in allowed_priorities:
            raise ValueError(
                "Invalid priority."
            )

        ticket_id = (
            f"TICKET-{uuid.uuid4().hex[:8]}"
        )

        return {
            "ticket_id": ticket_id,
            "status": "created",
            "priority": priority,
        }

Example:

User: I was charged twice. Create a high priority ticket.
AI: create_support_ticket(
    issue="Customer was charged twice",
    priority="high"
)

Your application executes the actual function — the AI only requests it.

5.22 Important: Read vs Write Tools

Not all tools carry the same level of risk.

Tool typeExampleRisk
Readget_order_status()Low
Writecreate_support_ticket()Medium
Financialrefund_payment()High

Classify tools by risk:

Read → Automatic Execution
Refund → User Confirmation → Execute

5.23 Human Confirmation

For sensitive operations:

User: Refund my payment.
AI: I found a refund of $250. Would you like me to proceed?
User: Yes.
AI: Execute refund tool.

Architecture:

AI Request
    ↓
Risk Assessment
    ↓
Confirmation Required?
        │
   ┌────┴────┐
  No         Yes
   │          │
Execute    Ask User
              ↓
           Confirm
              ↓
           Execute

This is an essential production pattern for anything with real-world financial or destructive consequences.

5.24 Function Calling Is Not the Same as Trust

A model can make mistakes — it might request refund_payment(amount=1000000). Your application must still enforce maximum refund amounts, authorization, user ownership, and business rules regardless of what the model asks for.

AI Suggestion → Application Validation →
Authorization → Business Rules → Execution

Never treat the model as a trusted backend service — it’s a suggestion generator, not an authority.

5.25 Tool Execution Security

1. Allowlist Tools — only approved functions can execute.

2. Validate Arguments — use Pydantic, type validation, business rules.

3. Check Authorization — is this user allowed to perform this action?

4. Add Audit Logging — record user, tool, arguments, timestamp, result.

5. Add Confirmation for Risky Actions — especially payments, refunds, account deletion, data modification.

5.26 Tool Logging

logger.info(
    "Tool requested",
    extra={
        "tool_name": tool_name,
    },
)

Avoid logging passwords, API keys, credit card numbers, or private secrets. A useful audit record:

{
  "tool": "get_order_status",
  "user_id": "user-123",
  "timestamp": "2026-07-23T12:00:00Z",
  "success": true
}

5.27 Testing Tools

Tools should be tested independently of the AI model itself:

def test_existing_order_is_found():
    repository = OrderRepository()
    tools = OrderTools(
        order_repository=repository
    )
    result = tools.get_order_status(
        "ORD-1001"
    )
    assert result["found"] is True
def test_unknown_order_is_not_found():
    repository = OrderRepository()
    tools = OrderTools(
        order_repository=repository
    )
    result = tools.get_order_status(
        "ORD-9999"
    )
    assert result["found"] is False
import pytest

def test_empty_order_id_is_rejected():
    repository = OrderRepository()
    tools = OrderTools(
        order_repository=repository
    )
    with pytest.raises(ValueError):
        tools.get_order_status("")

5.28 Testing the Tool Registry

def test_registered_tool_exists():
    assert (
        "get_order_status"
        in TOOL_REGISTRY
    )
def test_unknown_tool_is_rejected():
    tool = TOOL_REGISTRY.get(
        "delete_everything"
    )
    assert tool is None

This confirms that arbitrary tool execution is genuinely impossible, not just unlikely.

5.29 The Complete Production Flow

┌─────────────────────────┐
│       User Message        │
└────────────┬────────────┘
             │
             ▼
┌─────────────────────────┐
│       OpenAI Model         │
└────────────┬────────────┘
             │
             ▼
       Tool Call Request
             │
             ▼
┌─────────────────────────┐
│    Tool Name Validation    │
└────────────┬────────────┘
             │
             ▼
┌─────────────────────────┐
│    Argument Validation     │
└────────────┬────────────┘
             │
             ▼
┌─────────────────────────┐
│   Authorization Check      │
└────────────┬────────────┘
             │
             ▼
┌─────────────────────────┐
│    Business Function       │
└────────────┬────────────┘
             │
             ▼
┌─────────────────────────┐
│        Tool Result          │
└────────────┬────────────┘
             │
             ▼
┌─────────────────────────┐
│       OpenAI Model         │
└────────────┬────────────┘
             │
             ▼
┌─────────────────────────┐
│    Final User Response     │
└─────────────────────────┘

5.30 Practical Project: AI Customer Support Assistant

Build an assistant with: order status (get_order_status()), product search (search_products()), and support ticket creation (create_support_ticket()).

5.31 Recommended Development Order

Phase 1 — build the basic project structure (config/, models/, repositories/, tools/, services/, tests/).

Phase 2 — build and test tools without AI involved at all.

Phase 3 — create tool definitions.

Phase 4 — create the AI service.

Phase 5 — implement the tool execution loop (AI → Tool Call → Execute → Tool Result → AI).

Phase 6 — add logging, error handling, validation, authorization, and tests.

This order is deliberately different from “start with the AI API” — building and testing tools in isolation first gives you a much more solid foundation.

5.32 Common Beginner Mistakes

Mistake 1: Letting AI Directly Access the Database. Bad: AI → Database. Better: AI → Approved Tool → Business Logic → Database.

Mistake 2: Executing Any Function Requested by the Model. Bad: globals()[tool_name](**args). Better: validated TOOL_REGISTRY[tool_name].

Mistake 3: No Argument Validation. Never assume the AI will always produce valid values — validate everything.

Mistake 4: No Authorization. The AI should never determine whether a user has permission; your application must.

Mistake 5: No Confirmation for Dangerous Actions. Use confirmation for refund, delete, transfer, cancel, and modify operations.

5.33 Key Concepts to Remember

Function Calling = AI Decides + Application Executes

The model never directly executes your Python function:

Model: "I want to call get_order_status."
Your Application: "Let me validate and execute that request."

This distinction is genuinely one of the most important things to internalize in this entire course.

Chapter 5 Summary

You learned what function calling and tools are, how the AI requests a tool and your application executes it, tool definitions and registries, argument validation, error handling, read vs. write tools, human confirmation, authorization, tool security, logging, testing, and production architecture.

The most important architecture:

User → AI Model → Tool Request → Validation → Authorization →
Business Logic → External System → Tool Result → AI Model → Final Response

Next chapter: Chapter 6 — Building AI Agents, where we’ll combine models, tools, memory, multi-step reasoning, and workflows to build a production-oriented AI agent.




Scroll to Top