
Building a Fully Functional AI Assistant with OpenAI (Series)
Implementation Steps
In this post, I’ll walk you through a robust, step-by-step guide to setting up a fully functional OpenAI Assistant using the new Assistants API (currently in beta). I’m particularly excited to share practical code snippets, covering everything from the simplest quickstart—perfect for new enthusiasts—on up to more advanced usage like real-time streaming with tools such as the Code Interpreter. Let’s get started!
Quickstart: Spin Up Your First Assistant
If you’re brand new to Assistants, here’s a minimal Node.js example. It demonstrates how to create an assistant, a thread, add messages, then produce a quick response. Once you get the hang of it, you can blend in advanced features such as custom tools, code execution, and more.
npm install openai
import OpenAI from 'openai';
const openai = new OpenAI();
async function createAssistant() {
const assistant = await openai.beta.assistants.create({
name: 'MyFirstAssistant',
instructions: 'You are a helpful assistant. Answer succinctly.',
tools: [{ type: 'code_interpreter' }],
model: 'gpt-4',
});
const thread = await openai.beta.threads.create();
await openai.beta.threads.messages.create(thread.id, {
role: 'user',
content: 'Hello, assistant!',
});
const run = await openai.beta.threads.runs.createAndPoll(thread.id, {
assistant_id: assistant.id,
});
console.log(run.status);
}
createAssistant();
import OpenAI from "openai";
const openai = new OpenAI();
async function main() {
const assistant = await openai.beta.assistants.create({
name: "MyFirstAssistant",
instructions: "You are a helpful assistant. Answer succinctly.",
tools: [{ type: "code_interpreter" }],
model: "gpt-4",
});
const thread = await openai.beta.threads.create();
await openai.beta.threads.messages.create(thread.id, {
role: "user",
content: "Hello, assistant!",
});
const run = await openai.beta.threads.runs.createAndPoll(thread.id, {
assistant_id: assistant.id,
});
console.log(run.status);
}
main();
In just a few lines of code, you’ve created a brand-new AI assistant conversation, added a message to that conversation, and polled for a completed response. Next up, let’s dive deeper by breaking down the Assistants API step by step. This expanded flow is a key blueprint for building out more complex workflows, including advanced instructions, streaming responses, and a variety of integrated tool calls.
Step 1: Create an Assistant
An Assistant is at the heart of your AI functionality. Here, we’ll define core parameters: the model we’re using, sets of instructions that shape the assistant’s personality or role, and whether we want to enable specialized tools (like the Code Interpreter). For instance, consider building a math tutor that uses code execution to solve equations and display the results.
import OpenAI from "openai";
const openai = new OpenAI();
async function createMathTutorAssistant() {
const assistant = await openai.beta.assistants.create({
name: "Math Tutor",
instructions:
"You are a personal math tutor. Write and run code to answer math questions.",
tools: [{ type: "code_interpreter" }],
model: "gpt-4o",
});
console.log("Assistant created:", assistant.id);
}
createMathTutorAssistant();
import OpenAI from "openai";
const openai = new OpenAI();
async function createMathTutorAssistant() {
const assistant = await openai.beta.assistants.create({
name: "Math Tutor",
instructions:
"You are a personal math tutor. Write and run code to answer math questions.",
tools: [{ type: "code_interpreter" }],
model: "gpt-4o"
});
console.log("Assistant created:", assistant.id);
}
createMathTutorAssistant();
curl "https://api.openai.com/v1/assistants" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "OpenAI-Beta: assistants=v2" \
-d '{
"instructions": "You are a personal math tutor. Write and run code to answer math questions.",
"name": "Math Tutor",
"tools": [{"type": "code_interpreter"}],
"model": "gpt-4o"
}'
Once your Assistant is created, you’re off to the races. The next steps revolve around building a conversational Thread and populating it with user messages.
Step 2: Create a Thread
Threads act as the container for your entire conversation. Creating one is straightforward—think of it as opening up a new session or chat where your user and the assistant can communicate.
const thread = await openai.beta.threads.create();
console.log("Thread created:", thread.id);
curl https://api.openai.com/v1/threads \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "OpenAI-Beta: assistants=v2" \
-d ''
Step 3: Add a Message to the Thread
With your Thread in place, you can start passing user queries or prompts to the Assistant. Each snippet of conversation is saved as a Message object so the Assistant can keep track of context. Here’s how to add a user’s math question:
const message = await openai.beta.threads.messages.create(
thread.id,
{
role: "user",
content: "I need to solve the equation 3x + 11 = 14. Can you help me?"
}
);
console.log("Message created:", message.id);
curl https://api.openai.com/v1/threads/<THREAD_ID>/messages \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "OpenAI-Beta: assistants=v2" \
-d '{
"role": "user",
"content": "I need to solve the equation 3x + 11 = 14. Can you help me?"
}'
You can continually add more user messages, each building upon the context established so far.
Step 4: Create a Run
Once you’ve populated your conversation with messages, it’s time to run the thread. Creating a Run triggers the model to process everything in the thread, optionally engaging the assigned tools, and produce a response. Depending on your preference, you can run the Assistant in two ways: offering a standard, non-streaming approach, or streaming responses in real time.
4a: Non-Streaming
Non-streaming calls handle the entire request-response cycle in one go. You can periodically poll the server until the run is completed. This is useful if you’re dealing with shorter tasks where immediate results are enough.
const run = await openai.beta.threads.runs.createAndPoll(
thread.id,
{
assistant_id: assistant.id,
instructions:
"Please address the user as Jane Doe. The user has a premium account."
}
);
console.log("Run status:", run.status);
if (run.status === 'completed') {
const messages = await openai.beta.threads.messages.list(thread.id);
for (const msg of messages.data) {
console.log(`${msg.role} > ${msg.content[0].text.value}`);
}
} else {
console.log(run.status);
}
let run = await openai.beta.threads.runs.createAndPoll(
thread.id,
{
assistant_id: assistant.id,
instructions: "Please address the user as Jane Doe. The user has a premium account."
}
);
if (run.status === 'completed') {
const messages = await openai.beta.threads.messages.list(run.thread_id);
for (const message of messages.data.reverse()) {
console.log(`${message.role} > ${message.content[0].text.value}`);
}
} else {
console.log(run.status);
}
4b: Streaming
For large or time-consuming tasks (like running code interpretations or step-by-step debugging) you’ll likely want streaming. This approach delivers tokens (and tool call events) in real time, allowing the user to watch the answer unfold.
import OpenAI from "openai";
const openai = new OpenAI();
class EventHandler {
onTextCreated(text: string): void {
process.stdout.write("\nassistant > ");
}
onTextDelta(delta: { value: string }, snapshot: any): void {
process.stdout.write(delta.value);
}
onToolCallCreated(toolCall: { type: string }): void {
process.stdout.write(`\nassistant > ${toolCall.type}\n`);
}
onToolCallDelta(delta: any, snapshot: any): void {
if (delta.type === 'code_interpreter') {
if (delta.code_interpreter.input) {
process.stdout.write(delta.code_interpreter.input);
}
if (delta.code_interpreter.outputs) {
process.stdout.write("\noutput >\n");
delta.code_interpreter.outputs.forEach((output: any) => {
if (output.type === "logs") {
process.stdout.write(`\n${output.logs}\n`);
}
});
}
}
}
}
async function runStream() {
const stream = openai.beta.threads.runs.stream(thread.id, {
assistant_id: assistant.id,
instructions:
"Please address the user as Jane Doe. The user has a premium account.",
eventHandler: new EventHandler(),
});
for await (const event of stream) {
// Optionally process stream events here
}
}
runStream();
By incorporating streaming, you’ll see partial responses, real-time code execution, and more. This is immensely powerful for complicated tutoring sessions or any scenario that benefits from incremental feedback.
That’s it! You can freely mix and match threads and messages, either streaming or non-streaming, to tailor how your AI assistant serves content.
Next Steps
Congrats on getting an end-to-end glimpse of how everything fits together. From a single Node script to advanced streaming sessions, the Assistants API provides a flexible framework to build anything from a quick helper bot to a full-blown study companion. If you’re looking for a more polished reference project that uses these same approaches, I recommend checking out:
In the next installment, we’ll go even deeper, exploring how to incorporate more advanced tool usage, handle file search, and build custom flows for specialized tasks like debugging code.
Ready to dive deeper into building an assistant?
Go to Part 3