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_calls in 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?":

  1. The AI receives the message
  2. It decides: "I can't calculate this precisely, but I have a calculate tool"
  3. It returns a tool_calls response:
{
  "tool_calls": [{
    "id": "call_abc123",
    "function": {
      "name": "calculate",
      "arguments": "{\"expression\": \"234 * 567\"}"
    }
  }]
}
  1. Your code calls calculate("234 * 567") which returns "132678"
  2. You send the result back to the AI with role: "tool"
  3. 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.