Completing the Tool Use Cycle
Introduction & Overview
In the previous lessons, you learned how to create tool schemas and how to understand GPT-5's responses when it wants to use tools. You can now recognize when GPT-5 requests function execution through function_call items in response.output, and how to extract the name, arguments, and call_id from each. 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 will learn how to bridge that gap by executing the methods 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 will have a complete tool execution pipeline that can handle GPT-5's tool requests from start to finish, maintaining a 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 will be building in this lesson. Here is the step-by-step process that transforms GPT-5 from a simple chatbot into a capable agent:
- Set up the foundation — Create a tool registry hash mapping tool names to Ruby
Methodobjects, load tool schemas fromJSON, and prepare initial conversationmessages. - Send the initial request — Make the first API call to GPT-5 with the user's question and available tools using
client.responses.create. - Detect function calls — Filter
response.outputfor items whosetypeequals"function_call". - Append function call items to messages — Add each function call back into your conversation array so GPT-5 has a complete view of what it asked for.
- Execute the requested methods — Parse the JSON arguments, use the tool registry to retrieve the right
Methodobject, and call it with keyword arguments. - Collect and format outputs — For each call, build a
function_call_outputitem that includes the matchingcall_idand the result encoded as a JSON string. - Send results back to GPT-5 — Make a second
client.responses.createcall with the complete conversation, including the function call items and their outputs. - Display the final response — Use
final_response.output_textto 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 method 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 Ruby methods. As we covered in previous lessons, this involves creating a tool registry hash and preparing our tool schemas and initial messages. The critical component here is the tool registry hash — this serves as the bridge between the tool names GPT-5 uses and our actual Ruby Method objects.
This setup creates everything we need for tool execution: the tools hash enables dynamic method lookup using Method objects, the developer_prompt guides GPT-5's behavior, the tool_schemas provide technical specifications, and the messages array starts the conversation. The tool registry is particularly important because it allows our code to execute the correct method 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. We use client.responses.create, passing the developer message and our user messages in the input array along with the available tools:
A couple of new details here:
- We pass
reasoning: { effort: "low" }. GPT-5 is a reasoning model, and this option controls how much thinking it does before responding."low"is a good default for fast, cost-effective interactions;"high"is appropriate for more complex problems. store: falsetells OpenAI not to persist the response on its server. We'll be managing the conversation state ourselves on the client side.
Detecting Function Calls
After receiving GPT-5's response, we filter for function_call items in the output. If there's at least one, we know GPT-5 wants us to execute tools.
We use .to_s when comparing item.type because the OpenAI Ruby SDK returns enum-like values that need conversion to strings for robust comparison. The function_calls array contains zero or more ResponseFunctionToolCall objects, each with a name, arguments, and call_id.
If function_calls is empty, GPT-5 has answered directly — we use response.output_text to print the answer and we're done. Otherwise, we proceed to execute the tools.
Appending Function Call Items to the Conversation
For GPT-5 to understand the conversation history correctly, we need to add each function_call item it produced back into our messages array. These are added as plain item hashes (not wrapped in a role/content message):
This is a key conceptual difference from a typical chat-style API: in the Responses API, function_call and function_call_output items live alongside regular role-based messages as separate entries in the input array. They are not nested under an "assistant" message.
Executing the Requested Methods
Now we execute each function call. The arguments come as a JSON string, so we parse them into a Ruby hash and then call our method with keyword arguments.
A few important things happen here:
JSON.parse(function_call.arguments || "{}")converts the JSON-encoded arguments string into a Ruby hash with string keys.args.transform_keys(&:to_sym)converts those string keys to symbols so we can use them as keyword arguments. The&symbol in&:to_symis a Ruby shorthand that applies theto_symmethod to each key.tools.fetch(name)looks up the correspondingMethodobject. We use.fetch(instead oftools[name]) so that a missing tool raises aKeyError, which we can rescue cleanly. If we used the bracket syntax, a missing tool would returnniland cause a confusingNoMethodErrorlater.- The
begin...rescueblock catches both missing-tool errors and any runtime exceptions during execution, converting them into error strings so the model can see what went wrong. - For each call, we build a
function_call_outputitem containing the matchingcall_idand the result encoded as a JSON string viaJSON.generate(result: result). Theoutputfield must be a string — wrapping the result in a JSON object is a clean convention.
It's critical to provide a function_call_output for every function_call GPT-5 produced. If you skip one, your next API call will fail because GPT-5 expects to see results for all the calls it made.
Getting GPT-5's Final Response
Once we've executed all the tools and collected their outputs, we add them to messages and send everything back to GPT-5 for the final answer:
This second API call uses the same parameters as the first, but now the input array contains the full history: the developer prompt, the user's original message, the function call items, and the function output items. GPT-5 has everything it needs to produce a natural, conversational answer that incorporates the tool results.
We use final_response.output_text to get the model's text answer. This SDK helper conveniently concatenates the text from all message items in the output, saving us from manually iterating through content blocks.
The complete execution flow produces output similar to this:
This output demonstrates the complete tool execution cycle: our code executes the requested method with the provided arguments and 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 methods, it will simply produce a message item (and maybe reasoning) — but no function_call. Our code already handles this in the else branch:
This ensures your application handles both tool-requiring and direct-answer scenarios gracefully.
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 detecting function_call items, executing methods through a tool registry hash of Method objects, formatting results as function_call_output items with matching call_ids, and sending the complete conversation back for a final answer.
The key Ruby techniques to remember:
- Use
method(:function_name)to createMethodobjects and store them in a Hash registry. - Use
.to_sfor safe comparison against API enum values. - Parse JSON-encoded
argumentsstrings withJSON.parse. - Convert hash keys with
transform_keys(&:to_sym)before calling Ruby keyword-argument methods. - Wrap tool execution in
begin...rescueblocks to handle missing tools and runtime errors gracefully. - Use
JSON.generate(result: result)to encode tool outputs as JSON strings for the API.
In the upcoming practice exercises, you will implement this complete workflow yourself, working with different scenarios including multiple function calls and error handling. Happy coding!
