Introduction & Overview

In the previous lessons, you learned how to create tool schemas and understand GPT-5's responses when it wants to use tools. You can now recognize when GPT-5 requests tool execution by checking for type: "function_call" items in the output array and extracting the necessary details from function call items. However, knowing what GPT-5 wants to do is only half the story — you still need to actually execute those tools and complete the conversation cycle.

In this lesson, you'll learn how to bridge that gap by executing the functions GPT-5 requests, capturing their results, and sending those results back to GPT-5 in the proper format. By the end of this lesson, you'll have a complete tool execution pipeline that can handle GPT-5's tool requests from start to finish, maintaining proper conversation flow throughout the entire process.

The Complete Tool Execution Flow

Before we dive into the implementation, let's understand the complete workflow we'll be building in this lesson. Here's the step-by-step process that transforms GPT-5 from a simple chatbot into a capable agent:

  1. Set up the foundation - Create function mappings, tool schemas, and initial conversation messages
  2. Send the initial request - Make the first API call to GPT-5 using responses.create with the user's question and available tools
  3. Detect tool use requests - Iterate through the output array to find items with type: "function_call"
  4. Extract tool information - Pull out the function name, arguments, and call ID from each function call item
  5. Execute the requested functions - Parse the JSON arguments and use our function mapping to call the actual Python functions with GPT-5's parameters
  6. Collect and format tool results - Structure all function outputs in the function_call_output format with matching call_id values
  7. Send results back to GPT-5 - Make a second API call with the complete conversation, including both the original function_call items AND their outputs
  8. Display the final response - Show GPT-5's natural language answer that incorporates the tool outputs

This complete cycle enables GPT-5 to seamlessly use tools as part of its reasoning process, transforming raw function outputs into conversational responses that directly answer user questions.

Setting Up the Foundation

Before diving into tool execution, we need to establish the foundation that connects GPT-5's tool requests to our actual Python functions. As we covered in previous lessons, this involves creating a mapping dictionary and preparing our tool schemas and initial messages. The critical component here is the function mapping dictionary — this serves as the bridge between the tool names GPT-5 uses and our actual Python functions.

import json
from openai import OpenAI
from functions import sum_numbers, multiply_numbers

# Initialize the OpenAI client
client = OpenAI()

# Create a dictionary mapping tool names to functions
# This is crucial - the keys must match our tool schema names exactly
tools = {
    "sum_numbers": sum_numbers,
    "multiply_numbers": multiply_numbers
}

# Choose a model to use
model = "gpt-5"

# System instructions for the assistant
# Note: Instructions are passed as a parameter, not as a system message
system_prompt = (
    "You are a helpful math assistant. "
    "Always use the available tools to perform calculations accurately."
)

# Load the schemas from JSON file
with open('schemas.json', 'r') as f:
    tool_schemas = json.load(f)

# Create a list of messages to send to GPT-5
messages = [
    {"role": "user", "content": "Please calculate 15 + 27"}
]

This setup creates everything we need for tool execution: the tools dictionary enables dynamic function lookup, the system_prompt guides GPT-5's behavior, the tool_schemas provide technical specifications, and the messages array starts the conversation. The function mapping is particularly important because it allows our code to execute the correct function based on GPT-5's string-based tool requests.

Sending the Initial Request

With our foundation in place, we can now send the initial request to GPT-5. The Responses API uses different parameter names than traditional chat completions, so pay attention to the specific parameters we're using here.

# Send the messages to GPT-5 with all necessary components
response = client.responses.create(
    model=model,
    instructions=system_prompt,  # Instructions for GPT-5's behavior
    input=messages,              # The initial user message (not 'messages')
    tools=tool_schemas,          # Available tools and their specifications
    reasoning={"effort": "low"}, # Reasoning configuration
    store=False                  # Don't store this conversation
)

This initial API call includes all the components necessary for GPT-5 to potentially request tool execution. The tools parameter gives GPT-5 the specifications it needs to decide whether to call functions, while the input contains the user's question that will drive that decision.

Detecting Tool Use Requests

After receiving GPT-5's response, we need to examine the output array to determine if tool execution is needed. Unlike checking a single stop_reason field, we iterate through the output items and look for function call requests.

# STEP 2: Look for function calls in the response
# Function calls appear as items with type "function_call" in the output
function_calls = []

for item in response.output:
    # Check if this item is a function_call
    if item.type == "function_call":
        # Add the function call to the list
        function_calls.append(item)

The response.output array contains all output items from GPT-5, which can include text messages, function calls, and other content types. By iterating through this array and checking each item's type field for "function_call", we identify all the tools GPT-5 wants to execute. We collect these in a list because GPT-5 might request multiple function calls in a single response.

Once we've collected all function calls, we check if any were found and proceed accordingly:

# Check if GPT-5 wants to call any functions
if function_calls:
    # STEP 3: Execute each function call that the model requested
    print("Executing function calls...")
    
    # Initialize a list to store the function outputs
    function_outputs = []
    
    for function in function_calls:
        # Get the function name, arguments, and call ID
        name = function.name
        call_id = function.call_id
        
        # Parse arguments from JSON string to Python dict
        args = json.loads(function.arguments)

When we find function call items, we extract the essential information from each one: the function name (which function to call), the call_id (a unique identifier we'll need to match results back to requests), and the arguments (provided as a JSON string). The json.loads() call is essential because the arguments come as a JSON string, not a Python dictionary, so we must parse them before we can use them to call our functions.

Executing the Requested Functions

With the function information extracted and arguments parsed, we can now execute the actual Python functions using our mapping dictionary. This is where the tool names from GPT-5's requests get translated into actual function calls.

if function_calls:
    # We have function calls to execute
    print("Executing function calls...")
    
    # Initialize a list to store the function outputs
    function_outputs = []
    
    for function in function_calls:
        # Extract the function name, arguments, and call ID
        name = function.name
        call_id = function.call_id
        
        # Parse arguments from JSON string to Python dict
        args = json.loads(function.arguments)
        
        # Use try-except to handle both missing tools and execution errors
        try:
            # Check if the requested tool exists and execute it
            if name in tools:
                result = tools[name](**args)
            else:
                result = f"Error: Function '{name}' not found"
        except Exception as e:
            # Handle any errors that occur during function execution
            result = f"Error executing {name}: {e}"
        
        # Print the function call and result for debugging
        print(f"{name}({args}) = {result}")

The critical step here is parsing the arguments with json.loads(function.arguments) before unpacking them with **args. Without this parsing step, we'd be trying to unpack a JSON string instead of a dictionary, which would cause an error. Using a try-except block provides comprehensive error handling that catches not only missing tools but also any runtime errors that might occur during function execution. For example, if a function receives invalid parameters (like trying to divide by zero), the exception will be caught and converted into an error message that can be sent back to GPT-5.

Formatting Tool Results

After executing each tool (or encountering an error), we must format the results in the specific structure that GPT-5 expects. The Responses API requires a particular format for function outputs that properly links each result back to its original request.

if function_calls:
    # We have function calls to execute
    print("Executing function calls...")
    
    # Initialize a list to store the function outputs
    function_outputs = []
    
    for function in function_calls:
        # Extract the function name, arguments, and call ID
        name = function.name
        call_id = function.call_id
        
        # Parse arguments from JSON string to Python dict
        args = json.loads(function.arguments)
        
        try:
            # Execute the tool if it exists...
            if name in tools:
                result = tools[name](**args)
            else:
                result = f"Error: Function '{name}' not found"
        except Exception as e:
            result = f"Error executing {name}: {e}"
        
        # Print the function call and result
        print(f"{name}({args}) = {result}")
        
        # Package the result in the format the Responses API expects
        function_outputs.append({
            "type": "function_call_output",  # Special content type for function results
            "call_id": call_id,              # Links result to original request
            "output": json.dumps({"result": result})  # The actual function output as JSON
        })

Each function output must have the type "function_call_output", the call_id must exactly match the call_id from the original function call item, and the output must be a JSON string (not a plain string or dictionary). We use json.dumps() to convert the result into proper JSON format.

It's critical that you provide a function output for every function call that GPT-5 makes. If you skip providing an output for any call_id, your next API call to GPT-5 will fail with an error because GPT-5 expects to receive results for all the functions it requested. This is why we handle both successful executions and errors in the same way — GPT-5 needs to know what happened with each function request, whether it succeeded or failed.

Reconstructing Function Call Items

However, formatting the outputs is only half of what we need to send back to GPT-5. The Responses API requires both the original function calls AND their outputs to be present in the conversation. This means we need to reconstruct the function call items in the proper format before adding them to our messages array.

    # Prepare function call items to send back to the model
    # We need to include both the original function calls AND their outputs
    function_call_items = []
    for function in function_calls:
        function_call_items.append({
            "type": "function_call",
            "name": function.name,
            "arguments": function.arguments,
            "call_id": function.call_id
        })
    
    # Add the function calls and their results to our conversation
    messages.extend(function_call_items)
    messages.extend(function_outputs)

You might wonder why we can't simply use messages.extend(function_calls) instead of reconstructing each item. The reason is that the items in function_calls are SDK model objects, not plain JSON dictionaries. The Responses API requires a strictly shaped, JSON-serializable payload with specific fields. By reconstructing each item, we ensure we send only the allowed fields (type, name, arguments, call_id) in the exact wire format the API expects, avoiding serialization or validation errors that could occur from SDK-internal fields.

This two-step process is essential: first, we reconstruct the original function call items with all their details, then we add both the function calls and their outputs to the messages array using extend(). This maintains the proper conversation structure that GPT-5 expects, showing both what was requested and what the results were.

Getting GPT-5's Final Response

Once we've executed all requested functions and added both the function calls and their results to the messages array, we can send the updated conversation back to GPT-5 to get the final response that incorporates the tool results.

    # Send the complete conversation with tool results back to GPT-5
    # Now the model can use the function results to formulate its response
    final_response = client.responses.create(
        model=model,
        instructions=system_prompt,
        input=messages,  # Now includes both function calls and their outputs
        tools=tool_schemas,
        reasoning={"effort": "low"},
        store=False
    )
    
    # Display GPT-5's final response to the user
    print("\nFinal answer:")
    print(final_response.output_text)

This second API call uses the same parameters as the first, but now the input array contains the complete conversation history, including both the original function call items and their corresponding outputs. GPT-5 can now provide a comprehensive answer that incorporates the tool execution results, transforming raw calculation outputs into natural, conversational responses.

The output_text property provides a convenient way to extract just the text content from GPT-5's response, filtering out any non-text items in the output array.

The complete execution flow produces the following output, showing both the tool execution and GPT-5's final response:

Executing function calls...
sum_numbers({'a': 15, 'b': 27}) = 42

Final answer:
15 + 27 = 42

This output demonstrates the complete tool execution cycle: our code executes the requested function with the provided parameters, captures the numerical result, and then GPT-5 transforms that raw output into a natural, conversational response that directly answers the user's original question.

Handling Non-Tool Responses

Not every user request will require tool usage. When GPT-5 can answer directly without needing to execute functions, the function_calls list will be empty after we iterate through the output array.

# Check if GPT-5 wants to call any functions
if function_calls:
    # STEP 3: Execute each function call that the model requested
    print("Executing function calls...")
    # ... execute tools and get final response
else:
    # GPT-5 responded directly without calling functions
    print("GPT-5 reply (no tools):")
    print(response.output_text)

By checking if the function_calls list contains any items, we can determine whether GPT-5 needs tools or can answer directly. When the list is empty (meaning no function call items were found in the output array), we simply display GPT-5's response using response.output_text, which extracts the text content from the output array. For example, if a user asks, "What is the capital of France?", GPT-5 would respond directly without requesting any function calls, since no mathematical calculations are needed. This ensures your application handles both tool-requiring and non-tool scenarios gracefully, maintaining proper conversation flow in all cases.

Visualizing the Complete Conversation History

Throughout the tool execution cycle, maintaining proper conversation history enables GPT-5 to understand context and provide coherent responses. Let's examine how the complete conversation flow develops through each step of the process.

# Print the complete messages history to understand the conversation flow
print("\nMessages history:")
# Loop through each message to see the conversation development
for i, message in enumerate(messages):
    print(f"\nMessage {i+1}:")
    print(f"Type: {message.get('type', 'user message')}")
    
    # Display the content based on message structure
    if 'role' in message:
        print(f"Role: {message['role']}")
        print(f"Content: {message['content']}")
    elif 'name' in message:
        print(f"Function: {message['name']}")
        print(f"Arguments: {message['arguments']}")
        print(f"Call ID: {message['call_id']}")
    elif 'call_id' in message:
        print(f"Call ID: {message['call_id']}")
        print(f"Output: {message['output']}")

GPT-5's message structure is simpler than some other systems — each item in the messages array has a type field that indicates whether it's a regular message, a function_call, or a function_call_output. Regular user and assistant messages have role and content fields, function calls have name, arguments, and call_id fields, and function outputs have call_id and output fields.

This debugging output reveals the complete conversation structure that enables GPT-5's tool-using capabilities:

Messages history:

Message 1:
Type: user message
Role: user
Content: Please calculate 15 + 27

Message 2:
Type: function_call
Function: sum_numbers
Arguments: {"a": 15, "b": 27}
Call ID: call_abc123

Message 3:
Type: function_call_output
Call ID: call_abc123
Output: {"result": 42}

This conversation history shows the complete tool execution cycle: the user's initial request, the function call that GPT-5 requested with its arguments and unique ID, and the function output with the matching call ID. After this, GPT-5 would receive these messages and generate its final response incorporating the tool output. This structure demonstrates how proper message management creates seamless conversation flow that transforms GPT-5 into a capable agent.

Summary & Practice Preparation

You now understand the complete tool execution workflow that transforms GPT-5 from a text-only assistant into an agent capable of performing actions and calculations. The process involves iterating through the output array to find function calls, parsing JSON arguments before execution, collecting and formatting all results with matching call IDs, reconstructing original function call items, and maintaining conversation history throughout the entire cycle.

The key components work together to create a robust system: function mapping enables dynamic execution, JSON parsing ensures arguments are properly formatted, proper result collection ensures all tools are handled, correct message formatting with both function calls and outputs allows GPT-5 to understand results, and conversation history maintains context across the entire interaction. This foundation allows GPT-5 to act as an intelligent agent that can reason about when to use tools and incorporate their results into natural responses.

In the upcoming practice exercises, you'll implement this complete workflow yourself, working with different types of tools and handling various scenarios, including multiple tool uses and error handling. Happy coding!

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal