If you have been working with AI for a while, function calling is something that should come quite natural to you by now. If not, this tutorial will explain how to make it work simply but effectively using OpenAI’s Responses API. As a reminder, function calling is the feature that bridges the gap between AI language models and your code’s capabilities. In other words, how you can combine AI development with traditional programming. In this post, we’ll explore how OpenAI’s function calling works and build a simple timezone converter to demonstrate its potential.
What is Function Calling?
Function calling allows large language models (LLMs) like GPT-4o to:
- Recognise when a user’s request requires a specific function
- Generate a properly formatted JSON object with the necessary parameters
- Call that function and use its returned result in the conversation
Building a Timezone Converter Assistant
Let’s break down a practical example — a timezone converter that tells users the current time anywhere in the world. This demonstrates the core concepts while being immediately useful.
The Code Explained
First, let’s define our function, the function that will be called by the LLM:
def get_current_time(timezone: str) -> str:
# Get the timezone object
timezone = pytz.timezone(timezone)
# Get current time in UTC
utc_time = datetime.now(pytz.UTC)
# Convert to desired timezone
local_time = utc_time.astimezone(timezone)
# Format the time
formatted_time = local_time.strftime("%I:%M:%S %p %Z")
return formatted_time
Next, we need to describe this function to the OpenAI model:
tools = [
{
"type": "function",
"name": "get_current_time",
"description": "Get the current time in the specified timezone",
"parameters": {
"type": "object",
"properties": {
"timezone": {
"type": "string",
"description": "The timezone to get the current time for"
}
},
"required": ["timezone"],
"additionalProperties": False
},
"strict": True
}
]
This JSON schema tells the model:
- What the function does
- What parameters it expects
- Which parameters are required
- Set
stricttoTrueto ensure the function schema is followed. The alternative means that best effort is made to call the function with the right parameters. Why would you not always setstricttoTrue, you may wonder? It is because not all JSON schemas are supported. That’s the primary reason, although there are other reasons related to performance — if the schema dynamically changes, then this could introduce additional latency, as there is an initial processing cost when the first request is processed. So, unless you do something quite complex, set thestrictparameter toTrue.
The Flow of Execution
When a user asks something like What time is it in Seville?, here’s what happens:
- The user’s question is sent to OpenAI’s API along with our function definition
- The model recognises this requires the
get_current_timefunction and generates a function call - Our code extracts the function name and arguments from the response
- We execute the real Python function and get the result
- We send the function result back to the API for a final response
- The model crafts a natural language response incorporating the function’s output
Handling the Function Call
if response.output[0].type == "function_call":
tool_call = response.output[0]
args = json.loads(tool_call.arguments)
name = tool_call.name
# Call the function by name
result = globals()[name](**args)
# Send the result back to the API
input_messages.append(tool_call)
input_messages.append({
"type": "function_call_output",
"call_id": tool_call.call_id,
"output": str(result)
})
response = client.responses.create(
model="gpt-4o",
input=input_messages,
tools=tools,
)
This pattern is powerful — the model decides when to call functions based on the user’s intent, not through explicit programming logic.
I’ve seen some people doing if‘s against the tool_call‘s name, so they know which function to call. In my opinion, this is not the best way to handle it, as it is not easily extensible. Every time you add a new function you have to remember to change this code so the function can be recognised and called.
Best Practices
- Clear descriptions: Make function and parameter descriptions clear and specific. Avoid defining parameters that can lead to contradictions (for example, parameters that should be called with mutual exclusivity)
- Appropriate tooling: Only expose functions that make sense for your use case. Don’t expose too many (soft rule, around 20)
- Error handling: Gracefully handle cases where functions fail or return unexpected results
Getting Started
To try this example yourself:
- Create a virtual environment and activate it:
python -m venv .venv && source .venv/bin/activate - Install the required packages:
openai,python-dotenv, andpytz - Set up your OpenAI API key in a
.envfile - Run the code and ask about the time in different cities:
python openai_function_calling.py
In future articles I will explore how to do this using other APIs and AI frameworks, so stay tuned!
Full Code
from openai import OpenAI
import dotenv
import json
import pytz
from datetime import datetime
dotenv.load_dotenv()
model = "gpt-4o"
def get_current_time(timezone: str) -> str:
timezone = pytz.timezone(timezone)
utc_time = datetime.now(pytz.UTC)
local_time = utc_time.astimezone(timezone)
formatted_time = local_time.strftime("%I:%M:%S %p %Z")
return formatted_time
def main():
client = OpenAI()
tools = [
{
"type": "function",
"name": "get_current_time",
"description": "Get the current time in the specified timezone",
"parameters": {
"type": "object",
"properties": {
"timezone": {
"type": "string",
"description": "The timezone to get the current time for"
}
},
"required": ["timezone"],
"additionalProperties": False
},
"strict": True
}
]
question = input("Ask me a question: ")
input_messages = [
{
"role": "user",
"content": question
}
]
response = client.responses.create(
model=model,
input=input_messages,
tools=tools,
)
if response.output[0].type == "function_call":
tool_call = response.output[0]
args = json.loads(tool_call.arguments)
name = tool_call.name
# Call the function called 'name'
result = globals()[name](**args)
input_messages.append(tool_call)
input_messages.append({
"type": "function_call_output",
"call_id": tool_call.call_id,
"output": str(result)
})
response = client.responses.create(
model=model,
input=input_messages,
tools=tools,
)
print(response.output_text)
if __name__ == "__main__":
main()
