Back to roadmaps openai Course

Handling Tool Calls and Returning Execution Results

Once the model requests a function call, execute the function on your server, append the execution results to your message history, and send them back to the API.


1. Multi-Step Execution Flow

graph TD
    A[User query] --> B[OpenAI requests tool: getUserSubscription]
    B --> C[Server executes getUserSubscription locally]
    C --> D[Server sends function result to OpenAI]
    D --> E[OpenAI generates final text answer for User]

2. Implementing the Tool Execution Loop

Write a script that processes tool requests dynamically:

import { openai } from "../lib/openai";
import { chatTools } from "../config/tools";
import { prisma } from "../lib/prisma";

// Local helper matching the schema tool definition
async function getUserSubscription(email: string) {
  const user = await prisma.user.findUnique({
    where: { email },
  });

  return JSON.stringify({
    found: !!user,
    tier: user?.pricingTier || "free",
    active: user?.subscriptionActive || false,
  });
}

export async function runAgentConversation(userQuery: string) {
  const messages: any[] = [
    { role: "user", content: userQuery }
  ];

  // 1. Initial request with tools registered
  const firstResponse = await openai.chat.completions.create({
    model: "gpt-4o",
    messages,
    tools: chatTools,
  });

  const message = firstResponse.choices[0].message;
  const toolCalls = message.tool_calls;

  // Check if the model requested any tool calls
  if (toolCalls) {
    // Append the assistant tool request to maintain message sequence
    messages.push(message);

    for (const toolCall of toolCalls) {
      const functionName = toolCall.function.name;
      const functionArgs = JSON.parse(toolCall.function.arguments);

      let resultText = "";

      if (functionName === "getUserSubscription") {
        // Execute the local server function
        resultText = await getUserSubscription(functionArgs.email);
      }

      // Append the tool execution output message to history
      messages.push({
        role: "tool",
        tool_call_id: toolCall.id,
        name: functionName,
        content: resultText,
      });
    }

    // 2. Submit execution outputs back to OpenAI to get final reply text
    const secondResponse = await openai.chat.completions.create({
      model: "gpt-4o",
      messages,
    });

    return secondResponse.choices[0].message.content;
  }

  return message.content;
}

3. Crucial Rules for Tool Cycles

  • Preserve Message Sequence: Always append the initial model reply containing tool_calls before appending role: "tool" execution output messages. Failure to preserve this ordering results in API validation errors.
  • Handle Exceptions: Wrap local function calls inside try-catch blocks. If a lookup fails, return the error message as the tool content so the model can report the issue to the user.
Published on Last updated: