Detailed Implementation: OpenAIAssistantApi
In this expanded guide, we delve more deeply into each function of OpenAIAssistantApi
—including the official Beta Assistants API with kindly-labeled endpoints: Assistants, Threads, Messages, and Runs. These are documented in the following sections of the official references:
Each segment below references the relevant endpoints and object structures, embedding details from the official docs to provide a thorough step-by-step approach.
1. Listing All Assistants
Retrieving a list of existing Assistants is straightforward thanks to the List Assistants API, which supports limit
and order
parameters for paging and sorting. According to the docs, you can do something like:
async list(query: { limit?: number; order?: 'asc' | 'desc' } = { limit: 100, order: 'desc' }): Promise<AssistantDto[]> {
// 1) Call openai.beta.assistants.list(...) with pagination
// 2) Collect all assistants
// 3) Return them as a typed array of AssistantDto
let assistants: AssistantDto[] = [];
let response = await this.openai.beta.assistants.list({ ...query });
assistants = assistants.concat(
response.data.map((assistant) => ({
id: assistant.id,
name: assistant.name,
instructions: assistant.instructions,
tools: assistant.tools,
}))
);
// If there's more data, keep fetching
while (response.hasNextPage()) {
response = await response.getNextPage();
assistants = assistants.concat(
response.data.map((assistant) => ({
id: assistant.id,
name: assistant.name,
instructions: assistant.instructions,
tools: assistant.tools,
}))
);
}
return assistants;
}
In the docs, seeList Assistantsfor query parameters and how pagination cursors (after
/ before
) work.
The returned data matchesthe assistant object. Knowing how each property is shaped (e.g. id
, name
,description
, instructions
, tools
, etc) is crucial for your typed responses.
2. Creating & Updating Assistants
According to the docs, you cancreate an assistantby specifying at least the model
and optionally adescription
,instructions
, up to 128 tools
, and any tool_resources
they might need. You can alsomodify an assistantat any time to change them:
async create(
name: string,
instructions: string,
tools: { type: string }[] = [{ type: 'code_interpreter' }],
responseFormat = { type: 'json_object' },
model = 'gpt-4',
temperature = 0.1
): Promise<AssistantDto> {
const assistant = await this.openai.beta.assistants.create({
name,
instructions,
tools,
model,
response_format: responseFormat,
temperature,
});
return {
id: assistant.id,
name: assistant.name,
instructions: assistant.instructions,
tools: assistant.tools,
};
}
async update(
assistantId: string,
name: string,
instructions: string,
tools: { type: string }[] = [{ type: 'code_interpreter' }],
responseFormat = { type: 'json_object' },
model = 'gpt-4',
temperature = 0.1
): Promise<AssistantDto> {
const assistant = await this.openai.beta.assistants.update(assistantId, {
name,
instructions,
tools,
model,
response_format: responseFormat,
temperature,
});
return {
id: assistant.id,
name: assistant.name,
instructions: assistant.instructions,
tools: assistant.tools,
};
}
Refer tothe Assistants Overviewfor deeper detail on tool_resources
and advanced usage like attaching file_ids
to a code_interpreter
or file_search
tool. Each file can be up to 512 MB with a maximum of 5,000,000 tokens.
3. Creating Threads & Adding Messages
A conversation is logically represented by aThread object. According to theCreate Threadendpoint, you can either start an empty conversation or pre-populate initial messages. Each user or assistant message is aMessage object
.
For instance, you might init a new thread:
async createThread(): Promise<ThreadDto> {
// Creates a fresh conversation
const thread = await this.openai.beta.threads.create();
return {
id: thread.id,
object: thread.object,
created_at: thread.created_at,
};
}
async addMessage(
threadId: string,
content: string,
metadata?: Record<string, string>,
role: 'user' | 'assistant' = 'user'
): Promise<MessageDto> {
const message = await this.openai.beta.threads.messages.create(threadId, {
role,
content,
metadata,
});
return {
id: message.id,
content: message.content,
role: message.role,
};
}
You can pass attachments (files, images) in the messages
call if your model can handle them (e.g. Vision support). TheMessages API Docsdetail how to structure content
with image_url
,image_file
, or multiple text blocks.
4. Running & Polling the Assistant
After you have an Assistant object (with a model, instructions, and optional tools) and a Thread with user messages, you cancreate a Runto get completions. As the doc states, a run has statuses likequeued
, in_progress
, and so on. You can also poll for completion or stream.
async runAndPoll(
threadId: string,
assistantId: string,
toolChoice?: { type: string },
additionalInstructions?: string
): Promise<RunDto> {
// Creates a run, then uses createAndPoll for convenience
const run = await this.openai.beta.threads.runs.createAndPoll(
threadId,
{
assistant_id: assistantId,
parallel_tool_calls: false,
tool_choice: toolChoice,
additional_instructions: additionalInstructions,
},
{ pollIntervalMs: 500 }
);
return run;
}
Notice the docs let you override things like model
,instructions
, tool_choice
,max_prompt_tokens
, max_completion_tokens
, or truncation_strategy
when creating a run. Refer tothe Runs docsfor advanced usage (streaming or partial updates).
5. Retrieving Runs & Messages
In many cases, you may want to fetch the conversation or any in-progress runs. Example:
async getActiveRun(threadId: string): Promise<RunDto | undefined> {
const runsList = await this.openai.beta.threads.runs.list(threadId);
const active = runsList.data
.sort((a, b) => (b.created_at > a.created_at ? 1 : -1))
.find(
(r) =>
!['completed','failed','cancelled','incomplete','expired'].includes(r.status)
);
return active; // or undefined if none
}
async getMessages(threadId: string): Promise<MessageDto[]> {
let messages: MessageDto[] = [];
let response = await this.openai.beta.threads.messages.list(threadId);
messages = messages.concat(
response.data.map((m) => ({
id: m.id,
content: m.content,
role: m.role,
metadata: m.metadata as Record<string, string>,
}))
);
// paginate
while (response.hasNextPage()) {
response = await response.getNextPage();
messages = messages.concat(
response.data.map((m) => ({
id: m.id,
content: m.content,
role: m.role,
metadata: m.metadata as Record<string, string>,
}))
);
}
return messages;
}
SeeRetrieve Runandlisting Messagesfor further details on these object shapes.
6. Submitting Tool Outputs
Tools such as code_interpreter
or function
calls can request data that must be returned to the run withsubmitToolOutputs. This typically happens when the run status is requires_action
. If you wait too long (expires_at
timeframe), the run becomes expired
.
async submitToolOutput(
threadId: string,
runId: string,
payload: { tool_outputs: Array<{ tool_call_id: string; output: string }> }
): Promise<RunDto> {
const updatedRun =
await this.openai.beta.threads.runs.submitToolOutputsAndPoll(
threadId,
runId,
payload
);
return updatedRun;
}
In theofficial docs, you’ll see how the run transitions from requires_action
back to queued
or in_progress
after submission, eventually concluding with completed
or failed
.
7. Cloning Threads
Sometimes, you’ll want a “branch” in your conversation. For example, if you’re building a RAG-based system with different user paths, you can replicate a portion of the conversation as follows:
async cloneThread(
originalThreadId: string,
upToMessageId: string,
exclude: string[] = []
): Promise<string> {
const allMessages = await this.getMessages(originalThreadId);
const reversed = allMessages.reverse();
const toClone: MessageDto[] = [];
for (const message of reversed) {
if (exclude.includes(message.id)) continue;
toClone.push(message);
if (message.id === upToMessageId) break;
}
const newThread = await this.createThread();
for (const msg of toClone) {
await this.addMessage(
newThread.id,
msg.content,
msg.metadata,
msg.role as 'user' | 'assistant'
);
}
return newThread.id;
}
This approach is helpful if you want to preserve conversation history but let the user “choose their own adventure” from a mid-point.
With these methods, you have a powerful set of utilities to create, list, update, and manage AI Assistants—then build Threads and Messages for user interactions, run the Assistant with your chosen tools, handle function calls, and clone conversation states at will.
For more advanced scenarios (e.g., large quantity of files via the file_search
tool or a custom chunking strategy in vector_stores
), be sure to consult the official docs. ThecreateThreadAndRunendpoint also supports streaming responses if you’d like to show partial results to your end user.
Keep Going!
Go to Part 4