AI and Machine Learning

Building Custom GPTs: OpenAI Assistants API for AI Agents

Unlock advanced AI agent capabilities. Learn to build custom GPTs with OpenAI Assistants API, integrating tools, retrieval, and state management. Comprehensive guide.

Khader Vali August 7, 2026 16 min read

Building Custom GPTs with the OpenAI Assistants API: Your Gateway to Advanced AI Agents

The landscape of artificial intelligence is evolving at a breathtaking pace, moving beyond simple question-answering systems to sophisticated, autonomous agents capable of complex reasoning, tool usage, and persistent memory. OpenAI’s Custom GPTs, accessible via their consumer-facing chat interface, have democratized access to personalized AI. However, for developers seeking deeper integration, programmatic control, and the ability to embed these intelligent agents directly into their applications, the OpenAI Assistants API is the true powerhouse.

As a senior engineer and technical writer here at Khadervali.com, I’ve had my hands deep in various OpenAI integrations. I’m excited to guide you through the intricacies of building robust, custom AI agents using the Assistants API. This isn’t just about recreating a chat interface; it’s about crafting intelligent backend systems that can power a new generation of applications, from smart customer support bots to personalized learning tutors, and automated data analysts.

In this comprehensive guide, we’ll peel back the layers of the Assistants API, understand its core components, explore its capabilities like function calling and retrieval, and walk through practical code examples. We’ll even sketch out a full-fledged architectural pattern for deploying your custom AI assistant in a real-world scenario. By the end, you’ll have a solid foundation to start building your own advanced AI agents, ready to integrate seamlessly into your projects.

Understanding the OpenAI Assistants API: A Paradigm Shift for AI Development

Before the Assistants API, building conversational AI often involved a lot of manual state management. You’d have to store chat history, decide when to call external tools, and manage retrieval of documents yourself. The Assistants API abstracts away much of this complexity, offering a higher-level API that’s purpose-built for creating AI agents with persistent capabilities.

Core Components of the Assistants API

Think of the Assistants API as an orchestration layer for your AI agent. It manages several key entities:

  1. Assistant: This is the blueprint for your AI agent. It defines its personality (instructions), the underlying language model (e.g., gpt-4o, gpt-3.5-turbo), and the tools it has access to (Code Interpreter, Retrieval, Function Calling).

  2. Thread: A thread represents a single conversation between a user and an Assistant. It automatically stores message history, so you don’t have to pass the entire conversation back and forth with each API call. This is crucial for maintaining context.

  3. Message: Individual messages within a thread, typically from the user or the Assistant. Messages can contain text, images, or even file annotations.

  4. Run: A run is a single invocation of an Assistant on a thread. When you create a run, the Assistant processes the messages in the thread, executes tools if necessary, and generates a response. The run object has a status field that you’ll poll to track its progress.

  5. Run Step: For complex operations involving tool calls or code interpreter usage, a run might go through multiple steps. Run steps provide granular details about what the Assistant did during a run (e.g., calling a function, using the code interpreter, generating a response).

Why Choose the Assistants API Over Raw Completions?

While the Chat Completions API is excellent for single-turn interactions or simpler conversational flows, the Assistants API excels when you need:

  • Persistent Conversation History: No more manually managing message arrays. Threads handle it automatically.
  • Built-in Tool Orchestration: The API intelligently decides when and how to use Code Interpreter, Retrieval, and Function Calling based on the conversation context and your defined tools.
  • Automated Retrieval-Augmented Generation (RAG): Upload documents, and the Assistant will automatically use them for context, without you managing vector databases or embeddings explicitly.
  • Code Interpreter: Empower your agent to write and execute Python code, useful for data analysis, math, and complex logic.
  • State Management: The API handles the lifecycle of an interaction, including tool calls and their results, simplifying your application logic.

Architecting an Assistants API Application: A Conceptual Overview

Integrating the Assistants API into your application typically involves a backend service that acts as an intermediary between your user interface and OpenAI. This architecture provides flexibility, security, and scalability.

Conceptual Diagram:


+-------------------+      +---------------------------------+      +---------------------------+
|                   |      |                                 |      |                           |
|  User Interface   |----->|     Your Backend Service        |----->|     OpenAI Assistants API |
| (Web, Mobile App) |      | (e.g., FastAPI, Node.js Express)|      |                           |
|                   |      |                                 |      |  - Assistant definition   |
+-------------------+      |  1. Authenticates User          |      |  - Thread management      |
          ^                |  2. Manages User Sessions/Threads |      |  - Message processing     |
          |                |  3. Calls OpenAI Assistants API |      |  - Run orchestration      |
          |                |  4. Handles Tool Calls          |      |                           |
          |                |     (invokes external services) |      +---------------------------+
          |                |  5. Streams/Returns Responses   |                   |
          |                +---------------------------------+                   |
          |                                  ^                                   |
          |                                  |                                   |
          |                                  | Tool Call Invocation              |
          |                                  |                                   |
          |                                  +-----------------------------------+
          |                                  |
          |                                  v
          |                         +-----------------------+
          |                         |                       |
          +-------------------------|   External Services   |
                                    | (e.g., Database, CRM, |
                                    |   Payment Gateway,    |
                                    |   Custom APIs)        |
                                    +-----------------------+
    

Explanation of the Flow:

  1. User Interaction: A user interacts with your application’s frontend (web, mobile, desktop). They type a query, click a button, or upload a file.
  2. Request to Backend: The frontend sends this user input to your custom backend service. This service is responsible for user authentication, authorization, and managing the application’s state.
  3. Backend to OpenAI: Your backend uses the OpenAI Python/Node.js client library to interact with the Assistants API.

    • It first identifies or creates a Thread for the current user session.
    • It adds the user’s Message to that thread.
    • It then creates a Run for the specified Assistant and Thread.
  4. OpenAI Processing & Tool Calls: The OpenAI Assistant processes the thread.

    • If the Assistant determines it needs to use a tool (e.g., to fetch data from your database or an external API), the Run‘s status will transition to requires_action.
    • The Assistant will provide details about the function it wants to call, including its name and arguments.
  5. Backend Handles Tool Calls: Your backend service, upon detecting a requires_action status, will parse the tool call instructions from the OpenAI API. It then invokes the actual external service or internal function (e.g., querying your CRM, fetching weather data).
  6. Backend Submits Tool Outputs: Once your backend receives the result from the external service, it sends this output back to the Assistants API as part of the Run. This allows the Assistant to continue processing with the newly provided information.
  7. OpenAI Generates Response: After all necessary tool calls are resolved, or if no tools were needed, the Assistant generates a final response based on the updated thread context. The Run status will eventually become completed.
  8. Backend Returns Response: Your backend retrieves the Assistant’s response messages from the thread and sends them back to the frontend.
  9. User Receives Response: The frontend displays the Assistant’s response to the user.

This architecture decouples the AI logic from your core business logic, allows for robust error handling, and enables you to integrate with any number of internal or external systems.

Building Custom GPTs: OpenAI Assistants API for AI Agents
Generated Image

Getting Started: Setting Up Your Environment

To follow along with the code examples, you’ll need a few things:

  • Python 3.8+: The OpenAI client library is best used with modern Python versions.
  • OpenAI API Key: Get one from your OpenAI dashboard. Make sure to keep it secure!
  • OpenAI Python Client Library: Install it via pip.

pip install openai python-dotenv
    

It’s good practice to store your API key in an environment variable. Create a .env file in your project root:


OPENAI_API_KEY="sk-YOUR_SUPER_SECRET_API_KEY"
    

Then, in your Python script, load it:


import openai
import os
from dotenv import load_dotenv

load_dotenv()
client = openai.OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
    

Core Concepts in Depth with Code Examples

Let’s dive into the practical implementation of each core component.

1. Creating and Managing an Assistant

The Assistant is the brain of your operation. You define its purpose, the model it uses, and the tools it can access.


# Define a tool (function) schema for our assistant
# This function could, for example, fetch current stock price from an external API
stock_price_tool = {
    "type": "function",
    "function": {
        "name": "get_stock_price",
        "description": "Get the current stock price for a given ticker symbol.",
        "parameters": {
            "type": "object",
            "properties": {
                "ticker_symbol": {
                    "type": "string",
                    "description": "The stock ticker symbol (e.g., 'AAPL' for Apple)."
                }
            },
            "required": ["ticker_symbol"]
        }
    }
}

# Create an Assistant
# You typically create an assistant once and then retrieve it by ID.
# For demonstration, we'll create it here.
# In a real app, you might check if an assistant with a specific name exists first.
try:
    my_assistant = client.beta.assistants.create(
        name="Financial Advisor Assistant",
        instructions="You are a helpful financial advisor. Use the available tools to answer questions about stock prices.",
        model="gpt-4o", # Or "gpt-3.5-turbo", etc.
        tools=[
            stock_price_tool,
            {"type": "code_interpreter"}, # Enable code interpreter
            {"type": "retrieval"} # Enable retrieval for knowledge base
        ]
    )
    print(f"Assistant created with ID: {my_assistant.id}")
except openai.APIStatusError as e:
    print(f"Error creating assistant: {e}")
    # Handle error, perhaps retrieve an existing assistant if creation failed due to conflict
    # In a production setup, you'd manage assistant IDs carefully.

# To retrieve an existing assistant:
# assistant_id = "asst_YOUR_ASSISTANT_ID" # Replace with your actual assistant ID
# my_assistant = client.beta.assistants.retrieve(assistant_id)
# print(f"Retrieved assistant: {my_assistant.name}")
    

Note: In a production environment, you’d usually create an Assistant once and store its ID in your configuration or database. Then, you’d retrieve it by ID rather than creating a new one every time.

2. Managing Threads for Conversations

A thread is the container for a single conversation. It automatically stores messages, maintaining context.


# Create a new thread
try:
    thread = client.beta.threads.create()
    print(f"Thread created with ID: {thread.id}")
except openai.APIStatusError as e:
    print(f"Error creating thread: {e}")

# To retrieve an existing thread:
# thread_id = "thread_YOUR_THREAD_ID" # Replace with your actual thread ID
# thread = client.beta.threads.retrieve(thread_id)
# print(f"Retrieved thread with ID: {thread.id}")
    

Each user of your application should ideally have their own thread, or a new thread can be created for distinct conversational sessions.

3. Adding Messages to a Thread

User input is added to the thread as messages. You can also add messages from the Assistant for manual responses or system messages.


# Add a user message to the thread
user_message_content = "What is the stock price of TSLA?"
try:
    message = client.beta.threads.messages.create(
        thread_id=thread.id,
        role="user",
        content=user_message_content,
    )
    print(f"Message added to thread: {message.id}")
except openai.APIStatusError as e:
    print(f"Error adding message: {e}")
    

4. Running the Assistant and Polling for Status

After adding a message, you initiate a “run” to make the Assistant process the thread and generate a response. This is an asynchronous process, so you’ll typically poll its status.


import time

# Create a run
try:
    run = client.beta.threads.runs.create(
        thread_id=thread.id,
        assistant_id=my_assistant.id,
        # You can override assistant instructions or model for this specific run
        # instructions="You are a helpful assistant, prioritize brevity.",
        # model="gpt-4o-mini"
    )
    print(f"Run created with ID: {run.id}")
except openai.APIStatusError as e:
    print(f"Error creating run: {e}")

# Poll the run status until it completes or requires action
while run.status == "queued" or run.status == "in_progress":
    run = client.beta.threads.runs.retrieve(
        thread_id=thread.id,
        run_id=run.id
    )
    print(f"Run status: {run.status}")
    if run.status == "completed":
        print("Run completed!")
        break
    elif run.status == "requires_action":
        print("Run requires action (tool call)!")
        break
    elif run.status in ["failed", "cancelled", "expired"]:
        print(f"Run finished with status: {run.status}")
        break
    time.sleep(1) # Wait for 1 second before polling again
    

The polling loop is critical. In a real-world application, you might use webhooks for long-running processes to avoid constant polling, but for many interactive scenarios, polling is sufficient.

5. Handling Tool Calls (Function Calling)

This is where the Assistants API truly shines. When the Assistant needs external information or to perform an action, it will tell you which tool function to call and with what arguments.


# Mock function for demonstration
def get_stock_price(ticker_symbol: str) -> str:
    """Fetches a mock stock price for a given ticker symbol."""
    mock_prices = {
        "TSLA": "250.75",
        "AAPL": "175.20",
        "MSFT": "420.10",
        "GOOG": "150.50"
    }
    price = mock_prices.get(ticker_symbol.upper())
    if price:
        return f"The current price of {ticker_symbol.upper()} is ${price}."
    else:
        return f"Could not find stock price for {ticker_symbol.upper()}. Please check the symbol."

# Mapping of tool names to actual Python functions
available_functions = {
    "get_stock_price": get_stock_price,
}

if run.status == "requires_action":
    tool_outputs = []
    for tool_call in run.required_action.submit_tool_outputs.tool_calls:
        function_name = tool_call.function.name
        function_args = json.loads(tool_call.function.arguments)

        if function_name in available_functions:
            print(f"Assistant requested to call function: {function_name} with args: {function_args}")
            # Execute the function
            function_to_call = available_functions[function_name]
            output = function_to_call(**function_args)
            print(f"Function output: {output}")
            tool_outputs.append({
                "tool_call_id": tool_call.id,
                "output": output,
            })
        else:
            print(f"Error: Function {function_name} not found.")
            tool_outputs.append({
                "tool_call_id": tool_call.id,
                "output": "Error: Function not implemented.",
            })

    # Submit the tool outputs back to the Assistant
    try:
        run = client.beta.threads.runs.submit_tool_outputs(
            thread_id=thread.id,
            run_id=run.id,
            tool_outputs=tool_outputs
        )
        print("Tool outputs submitted. Continuing run...")
    except openai.APIStatusError as e:
        print(f"Error submitting tool outputs: {e}")

    # Continue polling after submitting tool outputs
    while run.status == "queued" or run.status == "in_progress":
        run = client.beta.threads.runs.retrieve(
            thread_id=thread.id,
            run_id=run.id
        )
        print(f"Run status (after tool output): {run.status}")
        if run.status == "completed":
            print("Run completed after tool execution!")
            break
        elif run.status in ["failed", "cancelled", "expired"]:
            print(f"Run finished with status: {run.status}")
            break
        time.sleep(1)
    

This `requires_action` loop is the heart of making your AI agent dynamic and integrated with your services. It’s a powerful pattern for extending the AI’s capabilities beyond its training data.

6. Retrieving and Displaying Assistant’s Response

Once the run is `completed`, you can fetch all messages from the thread to see the Assistant’s responses.


# Retrieve messages from the thread
messages = client.beta.threads.messages.list(
    thread_id=thread.id,
    order="asc" # Get messages in chronological order
)

print("\n--- Conversation History ---")
for msg in messages.data:
    if msg.role == "user":
        print(f"User: {msg.content[0].text.value}")
    elif msg.role == "assistant":
        # Assistant messages might have tool_calls or annotations
        for content_block in msg.content:
            if content_block.type == "text":
                print(f"Assistant: {content_block.text.value}")
            elif content_block.type == "image_file":
                print(f"Assistant: Sent an image with file_id: {content_block.image_file.file_id}")
            # Add more handling for other content types if needed

print("--- End Conversation History ---\n")
    

7. Retrieval: Giving Your Assistant a Knowledge Base

Retrieval allows your Assistant to access external documents (PDFs, text files, etc.) to answer questions. It’s OpenAI’s built-in RAG (Retrieval Augmented Generation) system.


# 1. Upload a file
# For this example, let's create a dummy file
with open("company_policy.txt", "w") as f:
    f.write("Our company policy states that working hours are 9 AM to 5 PM, Monday to Friday. Overtime must be approved by a manager. All employees are eligible for 20 days of paid time off per year.")

try:
    file = client.files.create(
        file=open("company_policy.txt", "rb"),
        purpose="assistants"
    )
    print(f"File uploaded with ID: {file.id}")
except openai.APIStatusError as e:
    print(f"Error uploading file: {e}")

# 2. Update the Assistant to use the file for retrieval
# You can add files to an Assistant or to a specific Thread/Message
updated_assistant = client.beta.assistants.update(
    my_assistant.id,
    tools=[
        stock_price_tool,
        {"type": "code_interpreter"},
        {"type": "retrieval"}
    ],
    file_ids=[file.id] # Attach the uploaded file
)
print(f"Assistant updated with retrieval file: {updated_assistant.id}")

# Now, if we ask a question about company policy, the assistant should retrieve from the file.
# We'd add a new message to the thread and run the assistant again.
# user_message_content_2 = "What are the company's working hours and PTO policy?"
# ... (add message, create run, poll, retrieve messages)
    

8. Code Interpreter: The AI’s Data Scientist

The Code Interpreter tool allows the Assistant to write and execute Python code in a sandboxed environment. This is incredibly powerful for mathematical calculations, data analysis, and complex logic that’s hard to encapsulate in a simple function call.

You don’t write code for the Code Interpreter; you simply enable it for your Assistant, and the Assistant decides when to use it based on the conversation and instructions.

For example, if you ask:


"What is the average of 123, 456, and 789? Also, what's 15% of that average?"
    

The Assistant with Code Interpreter enabled would likely perform these calculations using Python code behind the scenes and then provide you with the accurate answer. You enabled it when creating the Assistant with {"type": "code_interpreter"}.

Building a Full-Fledged Application: A Smart Support Agent Scenario

Let’s tie everything together with a common real-world use case: an intelligent customer support agent for an e-commerce platform. This agent can answer FAQs, check order statuses, and even recommend products.

Building Custom GPTs: OpenAI Assistants API for AI Agents
Generated Image

Scenario Capabilities:

  • Answer FAQs: Using Retrieval, the agent can access product manuals, return policies, and general company information.
  • Check Order Status: Using Function Calling, the agent can query an internal order management system.
  • Recommend Products: Using Function Calling, the agent can query a product catalog or recommendation engine.

Architectural Walkthrough for the Smart Support Agent:

  1. Frontend (Web/Mobile App): A user types a query into a chat interface. This query is sent to a backend API endpoint (e.g., /chat).
  2. Backend (FastAPI/Flask/Node.js Express):

    • Session Management: The backend maintains a mapping between the user’s session ID (e.g., from a cookie or JWT) and an OpenAI Thread ID. If a user is new or starts a new session, a new thread is created.
    • Assistant Initialization: The backend uses a pre-configured Assistant ID (our “Smart Support Agent”).
    • Process User Input:

      1. Receives user’s query.
      2. Adds the query as a new Message to the user’s Thread.
      3. Creates a Run for the Assistant on that Thread.
    • Run Polling Loop:

      1. Continuously polls the Run status.
      2. If requires_action:

        • Parses the tool calls requested by the Assistant (e.g., getOrderStatus, getProductRecommendations).
        • Calls internal microservices or databases to execute these functions.
        • Submits the results back to the OpenAI API using submit_tool_outputs.
      3. If completed:

        • Retrieves the latest messages from the Thread.
        • Extracts the Assistant’s response.
Written by

Khader Vali

Senior Software Engineer specializing in cloud architecture, real-time systems, and enterprise-scale applications.

Share this article

Related Articles

Knowledge Graphs: Enhancing LLM Reasoning with Structured Data

Aug 14, 2026 · 19 min read

Fine-Tuning LLMs with LoRA: A Practical Guide

Jul 07, 2026 · 13 min read

Building AI Agents with Function Calling in Python hero image showcasing LLM tool use architecture

Building AI Agents with Function Calling in Python

Jun 01, 2026 · 16 min read