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:
- Check order status
- Search product information
- 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 type | Example | Risk |
|---|---|---|
| Read | get_order_status() | Low |
| Write | create_support_ticket() | Medium |
| Financial | refund_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.
