Learn function calls in OpenAI Assistants API (2024)

Learn function calls in OpenAI Assistants API (2)

OpenAI Assistants come with three tools in their belt; code interpreter, knowledge retrieval, and function calls. And although the former two are so far the most used tools, in part because they’re easy to get started with, function calls is an untapped power we’re likely to see grow in popularity as people learn how to implement. And in this tutorial, you’ll learn how to master them within a few minutes using the video and code below.

Function calls is essentially Assistant’s calling APIs, allowing it to perform specialized tasks beyond its core capability. It means you can give assistants very specific and powerful capabilities, that return very precise and expected results.

In this tutorial we’ll build a function that returns information about a specific country, but it can be changed to essentially any functionality you wish. Let’s dive in!

Before you start, you need these things set up on your

  • NodeJS*
  • OpenAI npm (yarn add openai)
  • Readline npm (yarn add readline)
  • fs npm (yarn add fs)
  • dotenv npm (yarn add dotenv)
  • Axios npm (yarn add axios)
  • An OpenAPI key

First, create a folder where you want to store the project. Open up the folder in your terminal, and initialize your project with yarn. The default values will be fine, but change them if you want to.

yarn init

Next you’ll want to install all of the dependencies

yarn add openai readline fs dotenv axios

Lastly, let’s create three files that our project will use. One is the main JS file I named index.js, next is the file countryInformation.js that will contain the function we’ll run, and lastly .env which holds our OpenAI API key.

Let’s walk them through in the reverse order. Create the .env file and add the following line to it. Don’t forget to replace the text with your OpenAI API key which you can get from your OpenAI developer account.

OPENAI_API_KEY="REPLACE WITH YOUR OPENAI API KEY HERE"

Next, create another file and name it countryInformation.js. You can change this later when you build your own functions, it’s just the name I use in this tutorial.

// countryInformation.js: Contain the function we'll run for our assistant.
const axios = require("axios");

async function getCountryInformation(params) {
const country = params.country;

try {
const url = `https://restcountries.com/v3.1/name/${encodeURIComponent(
country
)}`;
const response = await axios.get(url);
// console.log(response.data); if you want to inspect the output
return JSON.stringify(response.data);
} catch (error) {
console.error(error);
return null;
}
}

module.exports = getCountryInformation;

This function uses the free restcountries.com API, and will return a JSON object that contain information about a country. If you prefer, you could just include the async function getCountryInformation() in your main index.js file, which we’ll create next.

// import the required dependencies
require("dotenv").config();
const OpenAI = require("openai");
const fsPromises = require("fs").promises;
const fs = require("fs");
const readline = require("readline").createInterface({
input: process.stdin,
output: process.stdout,
});
const getCountryInformation = require("./countryInformation");
global.getCountryInformation = getCountryInformation;
// Create a OpenAI connection
const secretKey = process.env.OPENAI_API_KEY;
const openai = new OpenAI({
apiKey: secretKey,
});

async function askQuestion(question) {
return new Promise((resolve, reject) => {
readline.question(question, (answer) => {
resolve(answer);
});
});
}

async function main() {
try {
let assistantId;
const assistantFilePath = "./assistant.json";

// Check if the assistant.json file exists
try {
const assistantData = await fsPromises.readFile(
assistantFilePath,
"utf8"
);
assistantDetails = JSON.parse(assistantData);
assistantId = assistantDetails.assistantId;
console.log("\nExisting assistant detected.\n");
} catch (error) {
// If file does not exist or there is an error in reading it, create a new assistant
console.log("No existing assistant detected, creating new.\n");
const assistantConfig = {
name: "Country helper",
instructions:
"You're a travelling assistant, helping with information about destination countries.",
model: "gpt-4-1106-preview",
tools: [
{
type: "function",
function: {
name: "getCountryInformation",
parameters: {
type: "object",
properties: {
country: {
type: "string",
description: "Country name, e.g. Sweden",
},
},
required: ["country"],
},
description: "Determine information about a country",
},
},
],
};

const assistant = await openai.beta.assistants.create(assistantConfig);
assistantDetails = { assistantId: assistant.id, ...assistantConfig };

// Save the assistant details to assistant.json
await fsPromises.writeFile(
assistantFilePath,
JSON.stringify(assistantDetails, null, 2)
);
assistantId = assistantDetails.assistantId;
}

// Log the first greeting
console.log(
`Hello there, I'm your personal assistant. You gave me these instructions:\n${assistantDetails.instructions}\n`
);

// Create a thread using the assistantId
const thread = await openai.beta.threads.create();
// Use keepAsking as state for keep asking questions
let keepAsking = true;
while (keepAsking) {
const action = await askQuestion(
"What do you want to do?\n1. Chat with assistant\n2. Upload file to assistant\nEnter your choice (1 or 2): "
);

if (action === "2") {
const fileName = await askQuestion("Enter the filename to upload: ");

// Upload the file
const file = await openai.files.create({
file: fs.createReadStream(fileName),
purpose: "assistants",
});

// Retrieve existing file IDs from assistant.json to not overwrite
let existingFileIds = assistantDetails.file_ids || [];

// Update the assistant with the new file ID
await openai.beta.assistants.update(assistantId, {
file_ids: [...existingFileIds, file.id],
});

// Update local assistantDetails and save to assistant.json
assistantDetails.file_ids = [...existingFileIds, file.id];
await fsPromises.writeFile(
assistantFilePath,
JSON.stringify(assistantDetails, null, 2)
);

console.log("File uploaded and successfully added to assistant\n");
}

if (action === "1") {
let continueAskingQuestion = true;

while (continueAskingQuestion) {
const userQuestion = await askQuestion("\nWhat is your question? ");

// Pass in the user question into the existing thread
await openai.beta.threads.messages.create(thread.id, {
role: "user",
content: userQuestion,
});

// Create a run
const run = await openai.beta.threads.runs.create(thread.id, {
assistant_id: assistantId,
});

// Imediately fetch run-status, which will be "in_progress"
let runStatus = await openai.beta.threads.runs.retrieve(
thread.id,
run.id
);

// Polling mechanism to see if runStatus is completed
while (runStatus.status !== "completed") {
await new Promise((resolve) => setTimeout(resolve, 1000));
runStatus = await openai.beta.threads.runs.retrieve(
thread.id,
run.id
);

if (runStatus.status === "requires_action") {
// console.log(
// runStatus.required_action.submit_tool_outputs.tool_calls
// );
const toolCalls =
runStatus.required_action.submit_tool_outputs.tool_calls;
const toolOutputs = [];

for (const toolCall of toolCalls) {
const functionName = toolCall.function.name;

console.log(
`This question requires us to call a function: ${functionName}`
);

const args = JSON.parse(toolCall.function.arguments);

const argsArray = Object.keys(args).map((key) => args[key]);

// Dynamically call the function with arguments
const output = await global[functionName].apply(null, [args]);

toolOutputs.push({
tool_call_id: toolCall.id,
output: output,
});
}
// Submit tool outputs
await openai.beta.threads.runs.submitToolOutputs(
thread.id,
run.id,
{ tool_outputs: toolOutputs }
);
continue; // Continue polling for the final response
}

// Check for failed, cancelled, or expired status
if (["failed", "cancelled", "expired"].includes(runStatus.status)) {
console.log(
`Run status is '${runStatus.status}'. Unable to complete the request.`
);
break; // Exit the loop if the status indicates a failure or cancellation
}
}

// Get the last assistant message from the messages array
const messages = await openai.beta.threads.messages.list(thread.id);

// Find the last message for the current run
const lastMessageForRun = messages.data
.filter(
(message) =>
message.run_id === run.id && message.role === "assistant"
)
.pop();

// If an assistant message is found, console.log() it
if (lastMessageForRun) {
console.log(`${lastMessageForRun.content[0].text.value} \n`);
} else if (
!["failed", "cancelled", "expired"].includes(runStatus.status)
) {
console.log("No response received from the assistant.");
}

// Ask if the user wants to ask another question
const continueAsking = await askQuestion(
"Do you want to ask another question? (yes/no) "
);
continueAskingQuestion =
continueAsking.toLowerCase() === "yes" ||
continueAsking.toLowerCase() === "y";
}
}

// Outside of action "1", ask if the user wants to continue with any action
const continueOverall = await askQuestion(
"Do you want to perform another action? (yes/no) "
);
keepAsking =
continueOverall.toLowerCase() === "yes" ||
continueOverall.toLowerCase() === "y";

// If the keepAsking state is falsy show an ending message
if (!keepAsking) {
console.log("Alrighty then, see you next time!\n");
}
}
// close the readline
readline.close();
} catch (error) {
console.error(error);
}
}

// Call the main function
main();

In this tutorial I’ll focus on the Functions part of this code, if you want to understand Assistants work in general I suggest you watch this tutorial where I break down the non-Functions part of this code.

If you don’t want to learn more about how Functions work and just want to run this code, skip forward to further below. Next I’ll explain the three three Functions-related things our code do.

  1. Creates an assistant that has a function
  2. Orchestration logic for knowing that the assistant wants to call a function, and executes it.
  3. The function itself

You see, when you create a functions in OpenAI it’s not OpenAI that executes them — that’s your responsibility. And I’ll show you how below, but let’s first start with looking at how you create an assistant with functions.

Creating an assistant and a function

 const assistantConfig = {
name: "Country helper",
instructions:
"You're a travelling assistant, helping with information about destination countries.",
model: "gpt-4-1106-preview",
tools: [
{
type: "function",
function: {
name: "getCountryInformation",
parameters: {
type: "object",
properties: {
country: {
type: "string",
description: "Country name, e.g. Sweden",
},
},
required: ["country"],
},
description: "Determine information about a country",
},
},
],
};

In the tools portion of this code we define that the assistant should have access to a function, and define the function. In this tutorial the function is lookining up information about a country, so I’ve named the function getCountryInformation. It takes on parameter named country which is a string and that is required. You can tack on more properties, but we have only one. It’s important to give your parameters a good description so the assistant understand what they are. Similarly, give your function a good description so the assistant knows what the function is for. These descriptions will be used by the assistant to determine when to call a function, and what input to provide.

Next, let’s look at the function we created in countryInformation.js.

async function getCountryInformation(params) {
const country = params.country;
...
}

Note that the function name (getCountryInformation) and the params (country) are identical to what we defined in the Assistant function? That’s by design, as it allows us to call functions dynamically in our code.

while (runStatus.status !== "completed") {

...

if (runStatus.status === "requires_action") {
const toolCalls =
runStatus.required_action.submit_tool_outputs.tool_calls;
const toolOutputs = [];

for (const toolCall of toolCalls) {
const functionName = toolCall.function.name;

console.log(
`This question requires us to call a function: ${functionName}`
);

const args = JSON.parse(toolCall.function.arguments);

const argsArray = Object.keys(args).map((key) => args[key]);

// Dynamically call the function with arguments
const output = await global[functionName].apply(null, [args]);

toolOutputs.push({
tool_call_id: toolCall.id,
output: output,
});
}
// Submit tool outputs
await openai.beta.threads.runs.submitToolOutputs(
thread.id,
run.id,
{ tool_outputs: toolOutputs }
);
continue; // Continue polling for the final response
}

...
}

When an assistant identifies that it needs to run a function, the status of the ongoing run is set to requires_action. And once it is, there’a information about what the action is inrunStatus.required_action.submit_tool_outputs.tool_calls. Here’s an example of what that could look like

[
{
id: 'call_5aYhdXyZgiQi5W3LILGczB9l',
type: 'function',
function: {
name: 'getCountryInformation',
arguments: '{"country":"Sweden"}'
}
}
]

Because an assistant may ask to execute n>1 functions, our code handles the execution of several functions. For each function the assistant want to call, we take the function name and arguments and execute them.

As mentioned earlier, I’ve set up the code to execute functions dynamically. The lines below take the function name passed to us and execute a local function with the same name by passing in the parameters to it. This is why it’s important that the function in our code is named the same as in the assistant’s function.

const functionName = toolCall.function.name;
const args = JSON.parse(toolCall.function.arguments);
const argsArray = Object.keys(args).map((key) => args[key]);
const output = await global[functionName].apply(null, [args]);

For global[functionName] to work, we defined getCountryInformation in the global scope like this in index.js:

const getCountryInformation = require("./countryInformation");
global.getCountryInformation = getCountryInformation;

So long as you name your function(s) its input parameters the same in your assistant and your JS file, and define this global scope, the code will dynamically trigger whatever function the assistant wants you to run.

Returning the function result to the assistant

Once we have the result from our function(s), we need to return it to the assistant. We’ve save the result of each function we’ve run to toolOutputs, and then call openai.beta.threads.runs.submitToolOutputs with them along with the thread and run ids.


for (const toolCall of toolCalls) {
...

const output = await global[functionName].apply(null, [args]);

toolOutputs.push({
tool_call_id: toolCall.id,
output: output,
});
}
// Submit tool outputs
await openai.beta.threads.runs.submitToolOutputs(
thread.id,
run.id,
{ tool_outputs: toolOutputs }
);
continue; // Continue polling for the final response
}

...
}

The assistant now has the information it needs to continue processing the original user question. The runStatus.status is changed from requires_action to pending until the assistant decides what’s next. If it wants to return a response, it’ll set the status to completed or if you have complex multi-function runs it may decide that additional functions are needed and set it to requires_action again. The code I’ve provided in this tutorial should handle it all.

When you’re ready, you can run the code simply by typing this in your terminal:

node index.js

The script is set up to recognize if you haven’t already created an assistant with it, and automatically create one. Once the new assistant is created, it will store information about it to an assistant.json file in your folder. This is how it knows if you already have an assistant. It will look something like this:

{
"assistantId": "asst_IkYQ9mzm1QapDMb1XnyFJH5j",
"name": "Country helper",
"instructions": "You're a travelling assistant, helping with information about destination countries.",
"model": "gpt-4-1106-preview",
"tools": [
{
"type": "function",
"function": {
"name": "getCountryInformation",
"parameters": {
"type": "object",
"properties": {
"country": {
"type": "string",
"description": "Country name, e.g. Sweden"
}
},
"required": [
"country"
]
},
"description": "Determine information about a country"
}
}
]
}

This file allows you to re-use the assistant next time you run the script, and avoid creating a new one every time. If you ever want to create a new assistant, simply delete the file. But remember that you’d have to manually remove it from the OpenAI platform UI.

Learn function calls in OpenAI Assistants API (3)

To use the function you’ve created simply select 1. Ask a question such as “I’m travelling to country X, what currency should I bring?” that the assistant understand it should use the function for.

It’s really this simple. You can change the function in the assistant definition and countryInformation.js with your own logic, and go from there.

I hope this tutorial was useful. If it was, please consider liking it on YouTube, subscribing to my channel where I publish OpenAI tutorials, or following me on Twitter or LinkedIn.

Maybe you’d like to learn how to build a voice assistant with OpenAI Whisper and TTS?

Learn function calls in OpenAI Assistants API (2024)

References

Top Articles
Latest Posts
Article information

Author: Nathanial Hackett

Last Updated:

Views: 6305

Rating: 4.1 / 5 (72 voted)

Reviews: 87% of readers found this page helpful

Author information

Name: Nathanial Hackett

Birthday: 1997-10-09

Address: Apt. 935 264 Abshire Canyon, South Nerissachester, NM 01800

Phone: +9752624861224

Job: Forward Technology Assistant

Hobby: Listening to music, Shopping, Vacation, Baton twirling, Flower arranging, Blacksmithing, Do it yourself

Introduction: My name is Nathanial Hackett, I am a lovely, curious, smiling, lively, thoughtful, courageous, lively person who loves writing and wants to share my knowledge and understanding with you.