AI and Machine Learning

Build Custom GPTs with OpenAI Assistants API: A Deep Dive

Master building custom GPTs with OpenAI Assistants API. This guide covers architecture, code examples, tools (Code Interpreter, Retrieval, Function Calling), and real-world scenarios for advanced AI agents.

Khader Vali October 10, 2026 17 min read

The landscape of artificial intelligence is evolving at a breakneck pace, and at the forefront of this evolution are custom AI agents tailored to specific tasks and domains. While OpenAI’s ChatGPT interface allows users to create “Custom GPTs” with natural language instructions, developers often require more programmatic control, statefulness, and integration capabilities. This is where the OpenAI Assistants API shines, offering a robust framework for building sophisticated, persistent, and highly customizable AI experiences.

As a senior software engineer, I’ve seen firsthand how crucial it is to move beyond simple, stateless API calls to build truly intelligent applications. The Assistants API represents a paradigm shift, enabling us to create AI agents that remember context, leverage powerful tools, and interact seamlessly with our existing systems. In this comprehensive guide, we’ll dive deep into the Assistants API, exploring its core concepts, architectural considerations, and practical implementation with detailed code examples. By the end, you’ll have a solid understanding of how to build your own custom GPTs with unprecedented control and flexibility.

Understanding the Paradigm Shift: Custom GPTs (UI) vs. Assistants API

Before we roll up our sleeves and write some code, let’s clarify the distinction between the “Custom GPTs” you might create directly within the ChatGPT user interface and the programmatic power offered by the Assistants API.

Custom GPTs in ChatGPT UI

  • Ease of Use: Designed for non-developers or those needing quick prototypes. You define the GPT’s persona, instructions, and capabilities using natural language prompts within the ChatGPT interface.
  • Limited Control: While you can upload files for retrieval and define custom actions (function calling), the execution flow and integration points are largely managed by OpenAI’s UI. You don’t have direct programmatic access to the conversation state or detailed control over tool usage.
  • UI-Bound: Primarily lives within the ChatGPT ecosystem. Integration into external applications requires workarounds or relying on OpenAI’s limited sharing features.

OpenAI Assistants API

  • Programmatic Control: This is for developers who need to integrate AI agents directly into their applications. You define, manage, and interact with assistants entirely through API calls.
  • Stateful Conversations: The API inherently supports persistent conversation threads, meaning your assistant remembers context across multiple user interactions without you needing to manage it manually.
  • Advanced Tooling: Provides first-class support for Code Interpreter, Retrieval (RAG), and Function Calling, all programmatically controlled and extensible.
  • Custom Workflows: You dictate when the assistant runs, how it uses tools, and how its responses are presented within your application’s specific UI/UX.
  • Scalability & Integration: Built for production environments, allowing you to scale your AI applications and integrate them deeply with your existing backend services, databases, and third-party APIs.

In essence, if you want to build a quick, shareable AI persona within ChatGPT, use the UI. If you want to build a powerful, integrated, and scalable AI agent that is a core part of your application, the Assistants API is your tool of choice.

Core Concepts of the Assistants API

The Assistants API introduces several key abstractions that form the building blocks of your AI agents. Understanding these is crucial for effective implementation.

  1. Assistant

    The Assistant is the blueprint for your AI agent. It defines its core personality, capabilities, and the tools it can use. Think of it as the brain and personality of your custom GPT.

    • Name: A human-readable name for your assistant (e.g., “Customer Support Bot”).
    • Instructions: A natural language prompt describing the assistant’s purpose, behavior, and constraints (e.g., “You are a helpful assistant that answers questions about products. Be concise and polite.”).
    • Model: The underlying OpenAI language model to use (e.g., gpt-4-turbo, gpt-3.5-turbo).
    • Tools: A list of tools the assistant can leverage, such as Code Interpreter, Retrieval, or custom functions.
    • File IDs: For Retrieval, you can attach files directly to the Assistant, making them available for all threads associated with it.
  2. Thread

    A Thread represents a single, ongoing conversation between a user and an Assistant. It’s the mechanism that provides statefulness. All messages and their context are stored within a thread. This means your assistant remembers previous turns in the conversation, allowing for more natural and coherent interactions. You create a new thread for each new conversation or user session.

  3. Message

    A Message is a single unit of communication within a Thread. It can be from the user (input) or from the Assistant (output). Messages have content (text, files) and a role (user or assistant).

  4. Run

    A Run is an execution of an Assistant on a Thread. When you initiate a Run, the Assistant processes the current messages in the thread, decides which tools to use (if any), and generates a response. The Run object tracks the lifecycle of this processing, from initial queuing to completion or requiring action (e.g., for function calling).

  5. Tools

    Tools are capabilities that extend the Assistant’s core language model. They allow the assistant to perform actions beyond generating text, making it incredibly powerful for real-world applications.

    • Code Interpreter: Allows the Assistant to write and execute Python code in a sandboxed environment. Ideal for mathematical calculations, data analysis, and complex logic.
    • Retrieval: Enables the Assistant to augment its knowledge base by searching over provided documents (PDFs, text files, etc.). This is a powerful implementation of Retrieval Augmented Generation (RAG).
    • Function Calling: The most versatile tool, allowing the Assistant to call custom functions defined in your application. This bridges the gap between the AI and external systems (databases, APIs, business logic).
Build Custom GPTs with OpenAI Assistants API: A Deep Dive
Generated Image

Architecture for a Production-Ready Assistant

Building a robust application around the Assistants API requires more than just calling the OpenAI endpoint. Here’s a high-level architectural overview of how you might structure a production-ready system:

Diagram in Words:


+-------------------+     +---------------------+     +--------------------------+     +-------------------+
|                   |     |                     |     |                          |     |                   |
|   User Interface  | <-> |   Backend Service   | <-> |  OpenAI Assistants API   | <-> | External Services |
|   (Web/Mobile)    |     |   (Your API Gateway)|     |  (Assistants, Threads,   |     | (Databases, CRMs, |
|                   |     |                     |     |  Messages, Runs, Tools)  |     | 3rd-Party APIs)   |
+-------------------+     +---------------------+     +--------------------------+     +-------------------+
        |                          ^
        |                          |
        +--------------------------+
          (API Calls: REST/GraphQL)

Components Explained:

  1. User Interface (Frontend Application)

    This is what your end-user interacts with. It could be a web application (React, Vue, Angular), a mobile app (iOS, Android), or even a desktop application. Its primary role is to:

    • Display the conversation history.
    • Capture user input.
    • Send user messages to your Backend Service.
    • Render Assistant responses.
    • Manage UI state (loading indicators, error messages).
  2. Backend Service (Your API Gateway/Application Server)

    This is the heart of your application. It acts as an intermediary between your frontend and the OpenAI Assistants API, as well as any other internal or external services. Written in languages like Python (Flask/Django/FastAPI), Node.js (Express), Go, or Java (Spring Boot), its responsibilities include:

    • API Endpoint Management: Exposing RESTful or GraphQL endpoints for your frontend to interact with (e.g., /api/chat/start, /api/chat/{thread_id}/message).
    • OpenAI API Orchestration:
      • Creating and managing Assistants (often done once during setup or deployment).
      • Creating and retrieving Threads.
      • Adding user messages to Threads.
      • Initiating Runs.
      • Polling Run status and retrieving Assistant responses.
      • Handling Tool Calls (e.g., executing custom functions and submitting their outputs back to OpenAI).
    • State Management (Optional but Recommended): While OpenAI threads store conversation history, you might want to store additional user-specific data, associate threads with user accounts, or manage assistant configurations in your own database.
    • Authentication & Authorization: Securing your API endpoints and ensuring only authorized users can interact with your AI agents.
    • Logging & Monitoring: Tracking interactions, errors, and performance.
    • Error Handling: Gracefully managing failures from the OpenAI API or external services.
    • Rate Limiting & Cost Management: Implementing strategies to control API usage and costs.
  3. OpenAI Assistants API

    This is the external service provided by OpenAI that hosts and executes your Assistants, manages Threads, and processes Runs. Your Backend Service communicates with this API.

  4. External Services

    These are any other systems your Assistant needs to interact with via Function Calling. Examples include:

    • Databases: For retrieving or storing user-specific data, product information, etc.
    • CRMs: To log customer interactions or retrieve customer profiles.
    • Third-Party APIs: Weather APIs, payment gateways, e-commerce platforms, booking systems, etc.
    • Internal Business Logic: Any custom code that needs to be executed based on Assistant’s decisions.

Getting Started: Setting Up Your Environment

Let’s get our hands dirty. We’ll use Python for our examples, as the OpenAI Python client library is robust and widely used.

Prerequisites:

  • Python 3.8+ installed.
  • An OpenAI API key. You can get one from the OpenAI Platform.

Installation:

First, create a virtual environment and install the necessary library:


python -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate
pip install openai python-dotenv

Configuration:

It’s best practice to store your API key securely. Create a .env file in your project root:


# .env
OPENAI_API_KEY="sk-your-super-secret-api-key"

Then, in your Python script, load it:


import os
from openai import OpenAI
from dotenv import load_dotenv

load_dotenv() # Load environment variables from .env

# Initialize the OpenAI client
# It automatically picks up OPENAI_API_KEY from environment variables
client = OpenAI()

print("OpenAI client initialized successfully.")

Step-by-Step Implementation: Building Your First Assistant

Let’s walk through the core API calls to build a simple, stateful AI assistant.

1. Creating an Assistant

An Assistant needs to be created once. You define its persona, model, and initial tools. You’ll typically store its ID for future interactions.


# assistant_manager.py
from openai import OpenAI
import os
from dotenv import load_dotenv

load_dotenv()
client = OpenAI()

def create_or_retrieve_assistant(assistant_name="My Helpful Assistant", model="gpt-4-turbo"):
    """
    Creates an assistant if one doesn't exist, or retrieves an existing one by name.
    """
    # This is a simplified way to manage assistants. 
    # In a real app, you might list all assistants and find by name,
    # or persist the assistant_id in a database.
    
    # For simplicity, we'll try to find an existing one by name in this example.
    # Note: OpenAI API doesn't have a direct "get by name". We'd list and filter.
    # Let's just create one for now and store the ID for demonstration.
    # In a real application, you'd persist this ID.

    # Check if we've already created an assistant and stored its ID
    assistant_id_file = "assistant_id.txt"
    if os.path.exists(assistant_id_file):
        with open(assistant_id_file, "r") as f:
            assistant_id = f.read().strip()
            try:
                existing_assistant = client.beta.assistants.retrieve(assistant_id)
                print(f"Retrieved existing assistant: {existing_assistant.id}")
                return existing_assistant
            except Exception as e:
                print(f"Failed to retrieve existing assistant {assistant_id}: {e}. Creating a new one.")
                # Fall through to create a new one if retrieval fails

    print(f"Creating a new assistant: {assistant_name}...")
    assistant = client.beta.assistants.create(
        name=assistant_name,
        instructions="You are a friendly and knowledgeable assistant. Provide clear, concise, and helpful answers.",
        model=model,
        tools=[{"type": "code_interpreter"}] # Let's add code interpreter as a default tool
    )
    print(f"Created new assistant with ID: {assistant.id}")
    
    # Save the assistant ID for future runs
    with open(assistant_id_file, "w") as f:
        f.write(assistant.id)
        
    return assistant

if __name__ == "__main__":
    assistant = create_or_retrieve_assistant()
    print(f"Assistant ID: {assistant.id}")

    # You can also list existing assistants:
    # my_assistants = client.beta.assistants.list(order="desc", limit="20")
    # print("All assistants:")
    # for ass in my_assistants.data:
    #     print(f"- {ass.name} ({ass.id})")

This script demonstrates how to create an assistant. We’ve added a basic mechanism to store the assistant ID in a file, so we don’t create a new one every time the script runs. In a production system, this ID would be stored in a database or configuration file.

2. Managing Threads

Each conversation happens within a Thread. If a user starts a new chat, you create a new thread. If they continue an existing one, you reuse the thread ID.


# chat_session.py (building on assistant_manager.py)
from openai import OpenAI
import os
from dotenv import load_dotenv
import time

load_dotenv()
client = OpenAI()

# Assume assistant_manager.py has been run and assistant_id.txt exists
def get_assistant_id():
    with open("assistant_id.txt", "r") as f:
        return f.read().strip()

def create_new_thread():
    """Creates a new conversation thread."""
    thread = client.beta.threads.create()
    print(f"Created new thread with ID: {thread.id}")
    return thread

def send_message_and_get_response(assistant_id, thread_id, user_message):
    """
    Sends a user message, runs the assistant, and retrieves its response.
    """
    # 1. Add the user message to the thread
    message = client.beta.threads.messages.create(
        thread_id=thread_id,
        role="user",
        content=user_message
    )
    print(f"Added message to thread {thread_id}: '{user_message}'")

    # 2. Run the assistant on the thread
    run = client.beta.threads.runs.create(
        thread_id=thread_id,
        assistant_id=assistant_id,
        # additional_instructions="Please be very detailed in your response."
    )
    print(f"Started run {run.id} on thread {thread_id}")

    # 3. Poll the run status until it completes
    while run.status in ['queued', 'in_progress', 'cancelling']:
        time.sleep(1) # Wait for 1 second
        run = client.beta.threads.runs.retrieve(
            thread_id=thread_id,
            run_id=run.id
        )
        print(f"Run status: {run.status}")

    # 4. Check for 'requires_action' status (for function calling)
    if run.status == 'requires_action':
        print("Run requires action! (e.g., function calling). This will be handled later.")
        # For now, we'll just acknowledge it and let the loop continue or break
        # In a full implementation, you'd process tool_outputs here and resubmit
        return "Assistant requires action, but no handler implemented yet."
    
    if run.status == 'completed':
        # 5. Retrieve messages added by the assistant
        messages = client.beta.threads.messages.list(
            thread_id=thread_id,
            order="asc", # Get messages in chronological order
            after=message.id # Only get messages after our last user message
        )
        
        assistant_responses = []
        for msg in messages.data:
            if msg.role == "assistant":
                for content_block in msg.content:
                    if content_block.type == 'text':
                        assistant_responses.append(content_block.text.value)
                    elif content_block.type == 'image_file':
                        assistant_responses.append(f"[Image file ID: {content_block.image_file.file_id}]")
        
        return "\n".join(assistant_responses)
    else:
        return f"Run ended with status: {run.status}"

if __name__ == "__main__":
    assistant_id = get_assistant_id()
    
    # Start a new conversation
    thread = create_new_thread()
    thread_id = thread.id

    print("\n--- First Interaction ---")
    response = send_message_and_get_response(assistant_id, thread_id, "Hello, who are you?")
    print(f"Assistant: {response}")

    print("\n--- Second Interaction (continuing thread) ---")
    response = send_message_and_get_response(assistant_id, thread_id, "Can you tell me a fun fact about the number 7?")
    print(f"Assistant: {response}")

    print("\n--- Third Interaction (using code interpreter) ---")
    response = send_message_and_get_response(assistant_id, thread_id, "What is the square root of 123456789?")
    print(f"Assistant: {response}")

This chat_session.py script orchestrates the conversation. It creates a thread, sends user messages, initiates a run, polls for completion, and then retrieves the assistant’s response. Notice how the thread ID maintains the context, allowing subsequent messages to build upon previous ones.

Build Custom GPTs with OpenAI Assistants API: A Deep Dive
Generated Image

3. Implementing Tools: The Power-Ups for Your Assistant

The real magic of the Assistants API lies in its tools. Let’s explore each one.

3.1. Code Interpreter

We already enabled Code Interpreter in our assistant creation. When the assistant detects a need for computation or data manipulation, it will automatically invoke it. The output from the interpreter is then used to formulate the response.

The third interaction in our chat_session.py example (“What is the square root of 123456789?”) demonstrates Code Interpreter in action. The assistant internally writes and executes Python code like import math; math.sqrt(123456789) to get the answer.

3.2. Retrieval

Retrieval allows your assistant to “read” and understand documents you provide. This is essential for building knowledge-based agents, like a chatbot for your company’s internal documentation or product manuals.

Steps for Retrieval:

  1. Upload files to OpenAI.
  2. Attach file IDs to your Assistant.
  3. Enable the Retrieval tool for your Assistant.

Let’s create a simple text file and upload it:


# my_documents/product_faq.txt
Q: What is the main feature of Khadervali.com?
A: Khadervali.com is a developer portfolio and tech blog focused on modern software engineering practices, AI, and cloud technologies.

Q: What kind of content can I find on Khadervali.com?
A: You can find in-depth technical articles, tutorials, code examples, and architectural discussions on topics ranging from API design to machine learning deployment.

Q: Who is Khadervali?
A: Khadervali is a senior software engineer and technical writer, sharing insights and expertise through the platform.

Now, let’s modify our assistant_manager.py to include file upload and retrieval:


# assistant_manager_with_retrieval.py
from openai import OpenAI
import os
from dotenv import load_dotenv
import time

load_dotenv()
client = OpenAI()

def upload_file(file_path, purpose="assistants"):
"""Uploads a file to OpenAI for use with assistants."""
print(f"Uploading file: {file_path}...")
with open(file_path, "rb") as f:
openai_file = client.files.create(
file=f,
purpose=purpose
)
print(f"File uploaded successfully. ID: {openai_file.id}")
return openai_file.id

def create_or_retrieve_assistant_with_retrieval(assistant_name="Knowledge Base Assistant", model="gpt-4-turbo", files_to_upload=None):
assistant_id_file = "assistant_id_kb.txt"
if os.path.exists(assistant_id_file):
with open(assistant_id_file, "r") as f:
assistant_id = f.read().strip()
try:
existing_assistant = client.beta.assistants.retrieve(assistant_id)
print(f"Retrieved existing knowledge base assistant: {existing_assistant.id}")
return existing_assistant
except Exception as e:
print(f"Failed to retrieve existing assistant {assistant_id}: {e}. Creating a new one.")

print(f"Creating a new knowledge base assistant: {assistant_name}...")

uploaded_file_ids = []
if files_to_upload:
for file_path in files_to_upload:
uploaded_file_ids.append(upload_file(file_path))

assistant = client.beta.assistants.create(
name=assistant_name,
instructions="You are a helpful assistant that answers questions based on the provided documents. If the answer is not in the documents, state that you cannot find the information.",
model=model,
tools=[{"type": "retrieval"}],
file_ids=uploaded_file_ids # Attach files directly to the assistant
)
print(f"Created new knowledge base assistant with ID: {assistant.id}")

with open(assistant_id_file, "w") as f:
f.write(assistant.id)

return assistant

if __name__ == "__main__":
# Ensure the directory exists
os.makedirs("my_documents", exist_ok=True)

# Create the dummy file
with open("my_documents/product_faq.txt", "w") as f:
f.write("""Q: What is the main feature of Khadervali.com?
A: Khadervali.com is a developer portfolio and tech blog focused on modern software engineering practices, AI, and cloud technologies.

Q: What kind of content can I find on Khadervali.com?
A: You can find in-depth technical articles, tutorials, code examples, and architectural discussions on topics ranging from API design to machine learning deployment.

Q: Who is Khadervali?
A: Khadervali is a senior software engineer and technical writer, sharing insights and expertise through the platform.
""")

assistant_kb = create_or_retrieve_assistant_with_retrieval(
assistant_name="Khadervali KB Assistant",
files_to_upload=["my_documents/product_faq.txt"]
)
print(f"Knowledge Base Assistant ID: {assistant_kb.id}")

# Now, let's use the chat session with this KB assistant
# We need to adapt the chat_session.py to use the new assistant_id_kb.txt
# For a full example, you'd modify get_assistant_id to accept a filename or parameter.
# For now, let's just use the ID directly for demonstration.

# Simulating chat session with the KB assistant
print("\n--- Knowledge Base Chat Session ---")
thread_kb = client.beta.threads.create()
thread_kb_id = thread_kb.id

questions = [
"What is Khadervali.com?",
"What content is available?",
"Who is Khadervali?",

Written by

Khader Vali

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

Share this article

Related Articles

Real-World MLOps Pipeline: Notebook to Production

Sep 20, 2026 · 15 min read

Evaluating LLM Outputs: Metrics, Benchmarks & Human Feedback

Jul 28, 2026 · 17 min read

Knowledge Graphs: Enhancing LLM Reasoning with Structured Data

Jul 14, 2026 · 17 min read