Processing Tool Calls in Your Application
Detect finish_reason tool_calls in the API response, extract function names and arguments, execute the corresponding Python function, and send the result back to the model.
Processing Tool Calls in Your Application is a free AI Engineering Academy lesson on CoddyKit — lesson 2 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the AI Engineering Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
The Tool Call Response Object
When the model decides to call a function, the API response contains a tool_calls list on the message object. Each tool call has a unique id, the function.name to call, and function.arguments — a JSON string of the arguments the model wants to pass. Your application code is responsible for parsing this and executing the function.
from openai import OpenAI
import json
client = OpenAI()
response = client.chat.completions.create(
model='gpt-4o',
messages=[{'role': 'user', 'content': 'What is the weather in Paris?'}],
tools=tools # defined previously
)
message = response.choices[0].message
if response.choices[0].finish_reason == 'tool_calls':
for tool_call in message.tool_calls:
print('Call ID:', tool_call.id)
print('Function name:', tool_call.function.name)
print('Arguments (JSON string):', tool_call.function.arguments)Parsing Function Arguments
The function.arguments field is a JSON-encoded string, not a Python dict. You must parse it with json.loads(). Always wrap this in a try/except — the model occasionally produces malformed JSON despite schema guidance, and you need to handle that gracefully.
import json
def parse_tool_call(tool_call) -> dict:
'''Parse a tool call's arguments from JSON string to dict.'''
try:
args = json.loads(tool_call.function.arguments)
return args
except json.JSONDecodeError as e:
print(f'Failed to parse arguments for {tool_call.function.name}: {e}')
print(f'Raw arguments: {tool_call.function.arguments}')
return {}
# Usage
tool_call = message.tool_calls[0]
args = parse_tool_call(tool_call)
print('Parsed args:', args) # {'location': 'Paris', 'unit': 'celsius'}Dispatching to the Right Function
Use the function.name to dispatch to the correct Python function. A clean pattern is to keep your functions in a dictionary mapping name to callable. This avoids brittle if/elif chains and makes it easy to add new tools later.
def get_current_weather(location: str, unit: str = 'celsius') -> str:
# Real implementation calls a weather API
return f'{location}: 18{chr(176)}C, partly cloudy'
def create_calendar_event(title: str, start_time: str, duration_minutes: int, **kwargs) -> str:
return f'Event created: {title} at {start_time} for {duration_minutes} minutes'
# Tool registry: maps function names to callables
TOOL_REGISTRY = {
'get_current_weather': get_current_weather,
'create_calendar_event': create_calendar_event
}
def execute_tool_call(tool_call) -> str:
name = tool_call.function.name
args = parse_tool_call(tool_call)
if name not in TOOL_REGISTRY:
return f'Unknown function: {name}'
try:
result = TOOL_REGISTRY[name](**args)
return str(result)
except Exception as e:
return f'Function {name} raised an error: {str(e)}'Sending Results Back to the Model
After executing the function, you must send the result back to the model in a follow-up API call. Add the assistant's message (which contains the tool_calls) to the conversation, then add a new message with role='tool', the tool_call_id, and the function result as content. Then call the API again.
def run_tool_call_loop(messages: list, tools: list) -> str:
response = client.chat.completions.create(
model='gpt-4o',
messages=messages,
tools=tools
)
message = response.choices[0].message
messages.append(message) # Add assistant's tool_calls message
# Execute all tool calls and collect results
for tool_call in (message.tool_calls or []):
result = execute_tool_call(tool_call)
# Add each tool result as a 'tool' role message
messages.append({
'role': 'tool',
'tool_call_id': tool_call.id,
'content': result
})
# Second API call with results appended
final_response = client.chat.completions.create(
model='gpt-4o',
messages=messages,
tools=tools
)
return final_response.choices[0].message.contentThe Full Conversation Turn
A complete tool call interaction involves four messages in the conversation history: the user's message, the assistant's message requesting a tool call, the tool result message, and the assistant's final response incorporating the result. Understanding this structure is essential for building multi-turn tool-using assistants.
# The full message history for a tool-calling conversation:
conversation = [
{'role': 'user', 'content': 'What is the weather in Tokyo?'},
# Model requests a tool call (added by run_tool_call_loop)
# {'role': 'assistant', 'content': None, 'tool_calls': [...]},
# Application sends tool result back
# {'role': 'tool', 'tool_call_id': 'call_abc123', 'content': 'Tokyo: 22C, sunny'},
# Model produces final human-readable response
# {'role': 'assistant', 'content': 'The weather in Tokyo is 22 degrees Celsius and sunny.'}
]
final_answer = run_tool_call_loop(
[{'role': 'user', 'content': 'What is the weather in Tokyo?'}],
tools
)
print(final_answer)Handling the Case Where No Tool Is Called
Sometimes the model answers directly without calling any tool — finish_reason will be 'stop' rather than 'tool_calls'. Always check for this case before trying to process tool calls. A robust implementation handles both branches cleanly.
def smart_complete(user_message: str) -> str:
messages = [{'role': 'user', 'content': user_message}]
response = client.chat.completions.create(
model='gpt-4o',
messages=messages,
tools=tools
)
choice = response.choices[0]
if choice.finish_reason == 'stop':
# Model answered directly without calling a tool
return choice.message.content
elif choice.finish_reason == 'tool_calls':
# Process tool calls
messages.append(choice.message)
for tc in choice.message.tool_calls:
result = execute_tool_call(tc)
messages.append({'role': 'tool', 'tool_call_id': tc.id, 'content': result})
# Get final answer
final = client.chat.completions.create(model='gpt-4o', messages=messages)
return final.choices[0].message.content
return 'Unexpected finish reason: ' + choice.finish_reasonValidating Arguments Before Execution
The model may occasionally pass arguments that fail business logic validation — a negative duration, an invalid email, or a date in the past. Validate arguments before calling the real function and return a descriptive error string if validation fails. The model can then correct its arguments in the next turn.
from pydantic import BaseModel, ValidationError
from datetime import datetime
class CreateEventArgs(BaseModel):
title: str
start_time: str # ISO 8601
duration_minutes: int
def safe_create_event(tool_call) -> str:
try:
raw_args = json.loads(tool_call.function.arguments)
validated = CreateEventArgs(**raw_args)
# Additional business rule
event_time = datetime.fromisoformat(validated.start_time)
if event_time < datetime.now():
return 'Error: start_time must be in the future.'
return create_calendar_event(**validated.dict())
except ValidationError as e:
return f'Invalid arguments: {e}'Logging Tool Call Interactions
Always log tool call interactions for debugging and analytics. Log the function name, arguments, result, and execution time. This data helps you identify which tools are called most frequently, which fail, and what argument patterns the model produces — invaluable for improving your schemas and function implementations.
import time
import logging
logger = logging.getLogger('tool_calls')
def logged_execute(tool_call) -> str:
name = tool_call.function.name
args_str = tool_call.function.arguments
start = time.time()
result = execute_tool_call(tool_call)
elapsed = time.time() - start
logger.info(
'Tool call executed',
extra={
'function': name,
'arguments': args_str,
'result_length': len(result),
'elapsed_ms': round(elapsed * 1000)
}
)
return resultTool Call Security Considerations
Never execute arbitrary functions based on model output without validation. Always whitelist the exact function names in your TOOL_REGISTRY, validate all arguments, and check authorization before executing actions. The model is an untrusted caller — a malicious prompt could try to invoke destructive functions if your dispatch logic is too permissive.
- Only allow functions explicitly listed in TOOL_REGISTRY
- Validate inputs with Pydantic before execution
- Require authorization for write/delete operations
Returning Rich Structured Results
Tool results don't have to be plain strings. You can return JSON-formatted data, tables, or summaries. Returning structured data as JSON lets the model parse and reference specific fields in its final answer. For large results, return a summary with key facts rather than dumping all raw data into the context.
def get_order_status(order_id: str) -> str:
# Fetch from real database
order = {'id': order_id, 'status': 'shipped', 'estimated_delivery': '2024-03-15', 'carrier': 'FedEx', 'tracking': 'FX123456'}
# Return concise summary, not raw DB record
return (
f'Order {order_id}: Status={order["status"]}, '
f'Estimated delivery: {order["estimated_delivery"]}, '
f'Carrier: {order["carrier"]}, Tracking: {order["tracking"]}'
)Multi-Turn Tool-Using Conversations
A powerful pattern is a multi-turn conversation where the model calls tools across multiple user messages, building up context. Always maintain the full conversation history including past tool calls and results so the model can reference previous answers without re-calling tools unnecessarily.
Quick Check
Test your understanding of processing tool calls in your application.
Lesson Recap
In this lesson you learned: tool call arguments arrive as a JSON string that must be parsed with json.loads(), a TOOL_REGISTRY dictionary maps function names to callables for clean dispatch, and results go back to the model as role='tool' messages with matching tool_call_ids. Next up we handle the case where the model calls multiple functions simultaneously with parallel function calling.
Frequently asked questions
Is the “Processing Tool Calls in Your Application” lesson free?
Yes — the full text of “Processing Tool Calls in Your Application” is free to read here on the web, and the AI Engineering Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the AI Engineering Academy course, upgrade to CoddyKit PRO.
What will I learn in “Processing Tool Calls in Your Application”?
Detect finish_reason tool_calls in the API response, extract function names and arguments, execute the corresponding Python function, and send the result back to the model. You practise AI Engineering Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start AI Engineering Academy?
No prior experience is required. AI Engineering Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 2 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Processing Tool Calls in Your Application” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this AI Engineering Academy lesson?
Yes. Every AI Engineering Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Defining Function Schemas for the API
- Processing Tool Calls in Your Application
- Parallel Function Calling
- Building a Natural Language Database Interface