Agentic Step 2: Tools and Function Calling
Build your first tool — a calculator that the AI can use.
What You'll Learn
- Defining tool schemas for OpenAI
- Handling
tool_callsin the API response - Executing functions based on AI decisions
- The tool call loop
Step 1: Define the Calculator Tool
Create agentic/agent.py:
import json
from openai import OpenAI
import os
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
def calculate(expression: str) -> str:
"""Evaluate a math expression safely."""
try:
# Safe eval — only allow basic math operations
allowed = set("0123456789+-*/.() ")
if not all(c in allowed for c in expression):
return "Error: Invalid characters in expression"
result = eval(expression)
return str(result)
except Exception as e:
return f"Error: {str(e)}"
tools = [
{
"type": "function",
"function": {
"name": "calculate",
"description": "Evaluate a mathematical expression",
"parameters": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "Math expression like '2 + 2' or '3 * (4 + 5)'"
}
},
"required": ["expression"]
}
}
}
]
Step 2: The Agent Loop
def run_agent(user_message: str) -> str:
messages = [
{"role": "system", "content": "You are a helpful assistant with access to a calculator."},
{"role": "user", "content": user_message},
]
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
tools=tools,
tool_choice="auto",
)
message = response.choices[0].message
# If the AI wants to call a tool
if message.tool_calls:
messages.append(message)
for tool_call in message.tool_calls:
if tool_call.function.name == "calculate":
args = json.loads(tool_call.function.arguments)
result = calculate(args["expression"])
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result,
})
# Get final response after tool call
final = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
tools=tools,
)
return final.choices[0].message.content
# Normal text response (no tool needed)
return message.content
Step 3: Test It
if __name__ == "__main__":
test_queries = [
"What is 234 * 567?",
"Calculate (15 + 27) * 3",
"Hello, how are you?",
"What's the square root of 144? (hint: use 144 ** 0.5)",
]
for query in test_queries:
print(f"\nUser: {query}")
result = run_agent(query)
print(f"AI: {result}")
Run it:
cd aiproject
source backend/venv/bin/activate
python advanced/agentic/agent.py
Step 4: Lint and Test Your Tool
Lint the Agent
cd aiproject
ruff check advanced/agentic/
This catches issues: unused imports, undefined variables, naming problems.
Test the Calculator Function
Create advanced/agentic/tests/test_tools.py:
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from agent import calculate
def test_calculate_adds_numbers():
assert calculate("2 + 2") == "4"
def test_calculate_multiplies():
assert calculate("3 * 4") == "12"
def test_calculate_rejects_invalid_chars():
result = calculate("import os")
assert result.startswith("Error")
Run it:
cd aiproject
python -m pytest advanced/agentic/tests/ -v
Expected:
test_tools.py::test_calculate_adds_numbers PASSED
test_tools.py::test_calculate_multiplies PASSED
test_tools.py::test_calculate_rejects_invalid_chars PASSED
Why test tools separately? Tool functions are pure logic — no API calls, no network. Testing them is fast and catches bugs before they reach the agent loop.
Step 5: Understanding the Tool Call
When you ask "What is 234 * 567?":
- The AI receives the message
- It decides: "I can't calculate this precisely, but I have a calculate tool"
- It returns a
tool_callsresponse:
{
"tool_calls": [{
"id": "call_abc123",
"function": {
"name": "calculate",
"arguments": "{\"expression\": \"234 * 567\"}"
}
}]
}
- Your code calls
calculate("234 * 567")which returns "132678" - You send the result back to the AI with
role: "tool" - The AI responds: "234 × 567 = 132,678"
The Tool Call Flow
User: "What's 234 * 567?"
1. POST /v1/chat/completions (messages=[user_question], tools=[calculate])
← Response: tool_calls → calculate(234 * 567)
2. Execute calculate("234 * 567") → "132678"
3. POST /v1/chat/completions (messages=[user_question, ai_tool_call, tool_result])
← Response: "234 × 567 = 132,678"
Safety Note
The calculate function uses eval(), which is dangerous if the user sends malicious code. We added a character whitelist to block anything except numbers and operators. In production, use a proper math parser like numexpr.