What We're Building

Ma Mahalakshmi V Updated 19 Sep 2026
19 min read ·Lesson 188 of 224

What We're Building

Time to put Lessons 1 through 4 into practice with a real, working project: a command-line chatbot that genuinely remembers a conversation across turns, saves that conversation to disk so it survives between runs of the program, and lets you list and resume past conversations by name. This isn't a toy — the patterns here (persisting conversation state, handling multiple named conversations, graceful error handling) are the same patterns a real production chatbot backend uses, just without the web framework wrapped around them.

We'll build this incrementally, in three stages: a minimal single-conversation version, then adding persistence to disk, then adding support for multiple named, resumable conversations. Each stage is fully working code you can run and test before moving to the next.

Project Structure

cli-chatbot/
├── .env
├── requirements.txt
├── chatbot.py          # the main program
└── conversations/      # created automatically — stores saved conversation JSON files

Setup

mkdir cli-chatbot && cd cli-chatbot
pip install openai python-dotenv

requirements.txt

openai>=1.50.0
python-dotenv>=1.0.0

.env

OPENAI_API_KEY=your_api_key_here

Remember: never commit .env to version control. Add it to a .gitignore file if you're using Git for this project.

Stage 1: A Minimal Remembering Chatbot

Let's start with the core loop, applying Lesson 2's manual history management directly — no persistence yet, just proving the conversation memory works within a single run.

chatbot.py (Stage 1)

import os
from dotenv import load_dotenv
from openai import OpenAI

load_dotenv()
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])

SYSTEM_PROMPT = "You are a helpful, friendly command-line assistant. Keep responses concise."


def main():
    history = [{"role": "developer", "content": SYSTEM_PROMPT}]

    print("Chatbot ready. Type 'quit' to exit.\n")

    while True:
        user_text = input("You: ").strip()
        if user_text.lower() in ("quit", "exit"):
            print("Goodbye!")
            break
        if not user_text:
            continue

        history.append({"role": "user", "content": user_text})

        response = client.responses.create(
            model="gpt-5.6-luna",
            input=history,
        )

        assistant_text = response.output_text
        history.append({"role": "assistant", "content": assistant_text})

        print(f"Bot: {assistant_text}\n")


if __name__ == "__main__":
    main()

Running It

python chatbot.py

Expected Output

Chatbot ready. Type 'quit' to exit.

You: My name is Diego and I'm learning Python.
Bot: Nice to meet you, Diego! Python is a great language to learn —
happy to help with anything you run into.

You: What's my name?
Bot: Your name is Diego.

You: quit
Goodbye!

This is Lesson 2's technique, applied directly: history grows with every turn, and the entire list is sent as input on each call. If you restart the script, though, history resets to empty — the conversation is only remembered within a single run. That's the gap Stage 2 fixes.

Code Walkthrough

The main() function runs a standard read-eval-print loop: read a line of input, check for the exit command, append it to history, call the API with the full accumulated history, print the response, and append the response back to history before looping again. This is exactly the pattern from Lesson 2's Conversation class, just inlined into a script rather than wrapped in a class — we'll bring the class structure back in Stage 3, once we need to manage more than one conversation at a time.

Stage 2: Persisting Conversations to Disk

A chatbot that forgets everything the moment you close the terminal isn't very useful. Let's add persistence, so a conversation survives between runs of the program.

chatbot.py (Stage 2)

import json
import os
from pathlib import Path
from dotenv import load_dotenv
from openai import OpenAI

load_dotenv()
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])

SYSTEM_PROMPT = "You are a helpful, friendly command-line assistant. Keep responses concise."
CONVERSATIONS_DIR = Path("conversations")
DEFAULT_FILE = CONVERSATIONS_DIR / "default.json"


def load_history() -> list[dict]:
    if DEFAULT_FILE.exists():
        with open(DEFAULT_FILE, "r") as f:
            return json.load(f)
    return [{"role": "developer", "content": SYSTEM_PROMPT}]


def save_history(history: list[dict]) -> None:
    CONVERSATIONS_DIR.mkdir(exist_ok=True)
    with open(DEFAULT_FILE, "w") as f:
        json.dump(history, f, indent=2)


def main():
    history = load_history()
    turn_count = sum(1 for m in history if m["role"] == "user")
    print(f"Chatbot ready. Resuming with {turn_count} prior message(s). Type 'quit' to exit.\n")

    while True:
        user_text = input("You: ").strip()
        if user_text.lower() in ("quit", "exit"):
            print("Goodbye!")
            break
        if not user_text:
            continue

        history.append({"role": "user", "content": user_text})

        response = client.responses.create(
            model="gpt-5.6-luna",
            input=history,
        )

        assistant_text = response.output_text
        history.append({"role": "assistant", "content": assistant_text})
        save_history(history)  # persist after every turn, not just at exit

        print(f"Bot: {assistant_text}\n")


if __name__ == "__main__":
    main()

Running It Across Two Sessions

$ python chatbot.py
Chatbot ready. Resuming with 0 prior message(s). Type 'quit' to exit.

You: My favorite color is teal.
Bot: Got it — teal is a great choice, kind of a mix between blue and green.

You: quit
Goodbye!

$ python chatbot.py
Chatbot ready. Resuming with 1 prior message(s). Type 'quit' to exit.

You: What's my favorite color?
Bot: Your favorite color is teal.

You: quit
Goodbye!

The second invocation of the script — a completely separate process, started fresh — correctly recalls information from the first session, because load_history() reads the saved JSON file back in before the loop starts. This is the practical, hands-on proof of everything Lesson 1 taught: nothing is remembered by the API itself, but our own application-level persistence (a JSON file on disk, in this simple case) gives the illusion of memory across separate runs.

Code Walkthrough

load_history() checks whether a saved conversation file already exists. If it does, it loads and returns the saved message list; if not, it returns a fresh list containing just the system prompt — exactly the same fallback pattern used for a brand-new conversation. save_history() writes the current history list to disk as JSON after every single turn, not just when the program exits — this matters because if the program crashes or is killed unexpectedly mid-conversation, you still keep everything up through the last completed turn, rather than losing the whole session.

Notice this is precisely the "database table" tier from Lesson 2's storage-options discussion, just using the filesystem instead of a real database — appropriate for a single-user command-line tool, though a real multi-user web application would use an actual database table instead, exactly as Lesson 2 described.

Stage 3: Multiple Named, Resumable Conversations

A single default conversation is useful, but a real chatbot tool should support multiple separate conversations — one for work questions, one for a personal project, and so on — each independently resumable by name. Let's refactor into a proper class and add conversation naming.

chatbot.py (Stage 3, Final Version)

import json
import sys
from pathlib import Path
import os
from dotenv import load_dotenv
from openai import OpenAI

load_dotenv()
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])

SYSTEM_PROMPT = "You are a helpful, friendly command-line assistant. Keep responses concise."
CONVERSATIONS_DIR = Path("conversations")


class PersistentChatbot:
    def __init__(self, conversation_name: str):
        self.name = conversation_name
        self.file_path = CONVERSATIONS_DIR / f"{conversation_name}.json"
        self.history = self._load()

    def _load(self) -> list[dict]:
        if self.file_path.exists():
            with open(self.file_path, "r") as f:
                return json.load(f)
        return [{"role": "developer", "content": SYSTEM_PROMPT}]

    def _save(self) -> None:
        CONVERSATIONS_DIR.mkdir(exist_ok=True)
        with open(self.file_path, "w") as f:
            json.dump(self.history, f, indent=2)

    def send(self, user_text: str) -> str:
        # Build a candidate list first, so a failed API call doesn't
        # leave a dangling, unanswered user message in saved history.
        candidate = self.history + [{"role": "user", "content": user_text}]

        response = client.responses.create(
            model="gpt-5.6-luna",
            input=candidate,
        )

        self.history = candidate + [
            {"role": "assistant", "content": response.output_text}
        ]
        self._save()
        return response.output_text

    def turn_count(self) -> int:
        return sum(1 for m in self.history if m["role"] == "user")


def list_conversations() -> list[str]:
    if not CONVERSATIONS_DIR.exists():
        return []
    return sorted(p.stem for p in CONVERSATIONS_DIR.glob("*.json"))


def main():
    existing = list_conversations()
    if existing:
        print("Existing conversations:", ", ".join(existing))

    name = input("Conversation name (existing or new): ").strip() or "default"
    bot = PersistentChatbot(name)
    print(f"\nUsing conversation '{name}' ({bot.turn_count()} prior message(s)). Type 'quit' to exit.\n")

    while True:
        user_text = input("You: ").strip()
        if user_text.lower() in ("quit", "exit"):
            print("Goodbye!")
            break
        if not user_text:
            continue

        try:
            reply = bot.send(user_text)
        except Exception as e:
            print(f"[Error contacting the model: {e}. Your message wasn't saved — try again.]\n")
            continue

        print(f"Bot: {reply}\n")


if __name__ == "__main__":
    main()

Running It

$ python chatbot.py
Conversation name (existing or new): work

Using conversation 'work' (0 prior message(s)). Type 'quit' to exit.

You: Let's brainstorm names for my new API client library.
Bot: Sure! What language is it for, and what's the core thing it does?

You: quit
Goodbye!

$ python chatbot.py
Existing conversations: work
Conversation name (existing or new): personal

Using conversation 'personal' (0 prior message(s)). Type 'quit' to exit.

You: quit
Goodbye!

$ python chatbot.py
Existing conversations: personal, work
Conversation name (existing or new): work

Using conversation 'work' (1 prior message(s)). Type 'quit' to exit.

You: It's a Python client for a weather API.
Bot: Got it. A few ideas: WeatherKit, SkyClient, or Forecastly...

Notice the third session correctly resumes the "work" conversation exactly where it left off, while "personal" remains a completely separate, independent conversation file — this is the multi-conversation equivalent of the session-keyed dictionary pattern from Lesson 2's multi-user widget example, just applied to named conversations instead of user sessions.

Code Walkthrough

PersistentChatbot bundles the loading, saving, and sending logic into a reusable class, keyed by a conversation name that maps directly to a JSON file on disk. This mirrors the Conversation class from Lesson 2, with persistence layered on top — each instance owns exactly one conversation's history and knows how to load and save it.

The send() method uses the candidate-history pattern from Lesson 2's error-handling section: it builds a candidate list including the new user message, only commits it to self.history (and saves it to disk) once the API call succeeds. Combined with the try/except around bot.send() in main(), this means a network failure or API error doesn't corrupt the saved conversation — the user's unanswered message is simply not persisted, and they can retry cleanly.

list_conversations() scans the conversations/ directory for existing .json files and reports their names, giving the user visibility into what conversations already exist before they choose to resume one or start fresh — a small but genuinely useful piece of UX that a real chatbot interface would also need to provide.

An Optional Extension: Switching to the Conversations API

Everything above uses Lesson 2's manual, file-based persistence. As a worthwhile exercise, try rewriting PersistentChatbot using Lesson 4's Conversations API instead — storing just the OpenAI conversation_id in a small local mapping file (conversation name → conversation_id) rather than the full message history yourself:

# Sketch of the alternative approach — left as an exercise to complete
class ConversationsAPIChatbot:
    def __init__(self, conversation_name: str):
        self.name = conversation_name
        self.mapping_file = CONVERSATIONS_DIR / "name_to_id.json"
        self.conversation_id = self._load_or_create_id()

    def send(self, user_text: str) -> str:
        response = client.responses.create(
            model="gpt-5.6-luna",
            conversation=self.conversation_id,
            input=user_text,
        )
        return response.output_text

This version trades local visibility (you can no longer just open a JSON file and read the full conversation) for reduced local storage responsibility (OpenAI holds the actual message content; your local file only maps a name to an ID). Building both versions and comparing them directly is one of the best ways to internalize the tradeoffs Lesson 4 described in the abstract — you'll feel the difference in how much code each version needs, and how much visibility each one gives you into what's actually being stored.

Adding Useful Commands: History, Reset, and Delete

A real command-line tool benefits from a few more commands beyond just chatting and quitting. Let's extend Stage 3 with /history (print the full conversation), /reset (clear the current conversation but keep its name), and /delete (remove a conversation entirely).

def print_history(bot: "PersistentChatbot") -> None:
    for message in bot.history:
        role = message["role"]
        content = message["content"]
        if role == "developer":
            continue  # skip printing the system prompt in the transcript view
        label = "You" if role == "user" else "Bot"
        print(f"{label}: {content}")


def main():
    existing = list_conversations()
    if existing:
        print("Existing conversations:", ", ".join(existing))

    name = input("Conversation name (existing or new): ").strip() or "default"
    bot = PersistentChatbot(name)
    print(f"\nUsing conversation '{name}' ({bot.turn_count()} prior message(s)).")
    print("Commands: /history, /reset, /delete, quit\n")

    while True:
        user_text = input("You: ").strip()

        if user_text.lower() in ("quit", "exit"):
            print("Goodbye!")
            break

        if user_text == "/history":
            print_history(bot)
            continue

        if user_text == "/reset":
            bot.history = [{"role": "developer", "content": SYSTEM_PROMPT}]
            bot._save()
            print("Conversation reset.\n")
            continue

        if user_text == "/delete":
            if bot.file_path.exists():
                bot.file_path.unlink()
            print(f"Conversation '{bot.name}' deleted. Exiting.")
            break

        if not user_text:
            continue

        try:
            reply = bot.send(user_text)
        except Exception as e:
            print(f"[Error contacting the model: {e}. Your message wasn't saved — try again.]\n")
            continue

        print(f"Bot: {reply}\n")

Expected Output

Using conversation 'work' (3 prior message(s)).
Commands: /history, /reset, /delete, quit

You: /history
You: Let's brainstorm names for my new API client library.
Bot: Sure! What language is it for, and what's the core thing it does?
You: It's a Python client for a weather API.
Bot: Got it. A few ideas: WeatherKit, SkyClient, or Forecastly...

You: /reset
Conversation reset.

You: quit
Goodbye!

/history gives a human-readable transcript without needing to open the raw JSON file directly — genuinely useful once a conversation has grown long enough that scrolling back through the terminal itself isn't practical. /reset demonstrates something worth noticing: it deliberately keeps the conversation's name (and its file) while clearing its content back to just the system prompt, which is a different, more surgical operation than /delete, which removes the conversation entirely. Distinguishing "clear this conversation's content" from "remove this conversation as a concept" is a small design decision, but it's exactly the kind of distinction a real chatbot product needs to get right — users expect "clear this chat" and "delete this chat" to be different actions with different consequences.

Exporting a Conversation

Another genuinely useful extension: exporting a saved conversation to a readable Markdown file, useful for sharing a conversation's content outside the tool entirely, or archiving it before deleting it from the active conversations/ directory.

def export_to_markdown(bot: "PersistentChatbot") -> Path:
    export_path = CONVERSATIONS_DIR / f"{bot.name}_export.md"
    lines = [f"# Conversation: {bot.name}\n"]

    for message in bot.history:
        if message["role"] == "developer":
            continue
        label = "**You**" if message["role"] == "user" else "**Bot**"
        lines.append(f"{label}: {message['content']}\n")

    export_path.write_text("\n".join(lines))
    return export_path

Wiring this in as an /export command (following the same pattern as /history, /reset, and /delete above) is a natural next exercise — and it's a good illustration of a broader point worth internalizing from this whole unit: once you own the conversation data yourself (as Lesson 2's manual approach does, and as this project does by extension), you can do essentially anything with it — export it, search it, analyze it, feed a summary of it into an entirely different feature — because it's just data sitting in a file or a database you control. This flexibility is part of the real, ongoing tradeoff against the Conversations API's convenience, and it's worth having actually felt both sides of that tradeoff by building this project both ways.

Testing the Persistence Logic

Following Lesson 2's guidance on testing conversation logic without burning API calls, the parts of this project most worth unit testing are the pure, non-network pieces — loading, saving, and the reset/delete commands — since these are exactly the pieces most likely to have a genuine bug that has nothing to do with the model's actual responses.

import json
import tempfile
from pathlib import Path

def test_load_creates_fresh_history_when_no_file_exists(tmp_path, monkeypatch):
    monkeypatch.setattr("chatbot.CONVERSATIONS_DIR", tmp_path)
    bot = PersistentChatbot("test_conversation")
    assert len(bot.history) == 1
    assert bot.history[0]["role"] == "developer"


def test_save_and_reload_roundtrips_correctly(tmp_path, monkeypatch):
    monkeypatch.setattr("chatbot.CONVERSATIONS_DIR", tmp_path)
    bot = PersistentChatbot("test_conversation")
    bot.history.append({"role": "user", "content": "hello"})
    bot._save()

    # Load it again as a fresh instance, simulating a new process
    reloaded = PersistentChatbot("test_conversation")
    assert reloaded.history[-1] == {"role": "user", "content": "hello"}

These tests use pytest's built-in tmp_path fixture to create a temporary directory for each test run, so tests never touch your real conversations/ directory and never depend on or interfere with each other. Combined with the mocked-API testing pattern from Lesson 2, this gives you solid coverage over the entire project without a single real network call — reserve actual API-hitting tests for a small number of manual or integration checks, run deliberately rather than on every test run.

Frequently Asked Questions

Why store conversations as JSON files instead of a lightweight database like SQLite, even for this simple project? JSON files keep the project dependency-free and easy to inspect directly (you can cat a conversation file and read it), which is ideal for a learning project. For anything beyond a single-user command-line tool, though, a real database — even a simple SQLite file — becomes the better choice quickly, since it handles concurrent access safely and makes querying across conversations (search, filtering, analytics) far more practical than scanning a directory of JSON files.

Could this same structure work with streaming responses instead of waiting for the full reply? Yes, and it's a natural extension — the Unit 5: Streaming and Responsiveness material in this course covers exactly how to convert a client.responses.create() call into a streamed one, printing tokens as they arrive rather than waiting for the complete response. The conversation-memory logic in this lesson is unaffected either way; streaming changes how you display the response, not how you manage conversation state.

What would need to change to turn this into a real web application instead of a command-line tool? The core PersistentChatbot class barely changes — you'd swap file-based persistence for a proper database (as discussed throughout this lesson and Lesson 2), and wrap send() in an API endpoint (using a framework like FastAPI, covered in this course's capstone unit) instead of a terminal input loop. The conversation-management concepts transfer directly; only the storage layer and the input/output mechanism change.

Is it safe to commit the conversations/ directory to version control for a personal project? Generally no, unless you're deliberately fine with your conversation history being stored in Git history — treat it the same way you'd treat any file containing personal data or potentially sensitive content, and add conversations/ to your .gitignore alongside .env.

Handling Interruption Gracefully

One more real-world detail worth building in: what happens if the user presses Ctrl+C mid-conversation, or closes the terminal unexpectedly? Python raises a KeyboardInterrupt in the first case, and without handling it, the program exits with a somewhat alarming traceback rather than a clean goodbye.

def main():
    existing = list_conversations()
    if existing:
        print("Existing conversations:", ", ".join(existing))

    name = input("Conversation name (existing or new): ").strip() or "default"
    bot = PersistentChatbot(name)
    print(f"\nUsing conversation '{name}' ({bot.turn_count()} prior message(s)).")
    print("Commands: /history, /reset, /delete, quit\n")

    try:
        while True:
            user_text = input("You: ").strip()
            # ... rest of the loop body as before ...
    except KeyboardInterrupt:
        print("\n\nInterrupted — your conversation has already been saved through the last completed turn. Goodbye!")

Because _save() is called after every successfully completed turn (not just at the end of the session), the KeyboardInterrupt handler doesn't even need to do any special saving work — the conversation is already safely persisted up through the last full exchange, and the handler's only job is to exit cleanly instead of printing a raw traceback. This is a direct payoff of the "commit only after success" design from Stage 3: by the time an interruption happens, there's never a question of whether unsaved state might be lost, because there never was any unsaved state sitting around waiting to be lost.

Project Checklist: What You've Actually Built

Before moving on, it's worth explicitly naming everything this project demonstrates, since it's easy to have followed along step by step without stepping back to see the full picture: manual conversation history management applied to a real, runnable program (Lesson 2's core technique); persistence across process restarts, proving conversation "memory" is an application-level responsibility rather than something the API provides (the central lesson of Lesson 1); a multi-conversation design keyed by name, mirroring the session-keyed pattern needed for any multi-user or multi-thread production system; defensive error handling that keeps persisted state consistent even when an API call fails; and a set of genuinely useful auxiliary commands (/history, /reset, /delete) that mirror the kind of conversation-management features a real chat product needs to offer its users.

If any part of this project felt unclear while building it, it's worth going back to the specific earlier lesson that introduced that concept — Lesson 1 for why persistence is needed at all, Lesson 2 for the mechanics of building and growing a history list, and this lesson for how those mechanics translate into an actual, runnable tool. Everything here is deliberately built from first principles you've already learned, rather than introducing anything new — the goal of this project is consolidation, not new material.

Common Mistakes

Saving history before confirming the API call succeeded. As covered in the Stage 3 walkthrough, saving too early can leave a corrupted, dangling conversation on disk if a call fails partway through. Always confirm success before committing state to persistent storage — this applies whether that storage is a JSON file, as here, or a real database in a production system.

Not handling the case where the saved file exists but contains invalid JSON. A conversation file that gets corrupted (a crash mid-write, manual editing gone wrong) will cause json.load() to raise an exception on the next run. A more robust _load() would catch json.JSONDecodeError and fall back to a fresh conversation rather than crashing the entire program on startup.

Forgetting that this simple file-based approach doesn't scale to concurrent access. This project is built for a single user running the script interactively — it doesn't handle two processes trying to write to the same conversation file simultaneously. A real multi-user application needs a proper database with appropriate transaction handling, exactly as Lesson 2 discussed.

Troubleshooting

The script can't find the conversations/ directory. Both save_history() in Stage 2 and _save() in Stage 3 call .mkdir(exist_ok=True) before writing, which should create the directory automatically on first save — if you're still seeing a missing-directory error, confirm the script has write permissions in its current working directory.

A resumed conversation doesn't seem to include everything from the previous session. Check the saved JSON file directly (cat conversations/<name>.json) to confirm what's actually stored — this is the same debugging technique Lesson 2 recommended: when in doubt, look directly at the exact data being sent, rather than guessing.

The OPENAI_API_KEY isn't being found when running the script. Confirm .env is in the same directory you're running python chatbot.py from, and that load_dotenv() is called before OpenAI(api_key=...) — a .env file in the wrong directory is a very common cause of this specific error.

Best Practices

Always separate "build the candidate next state" from "commit it," as Stage 3's send() method does — this one pattern eliminates an entire class of bugs where a failed network call leaves your persisted state inconsistent. Persist after every turn, not just at the end of a session, so a crash or forced exit never loses more than the single in-flight message. And treat this project as a genuine template: the same three-stage progression — get it working in memory, add persistence, add support for multiple independent conversations — applies directly to building a real production chatbot backend, just swapping a JSON file for a proper database once you move beyond a single-user command-line tool.

What's Next

You've now built a complete, working example that applies every technique from Lessons 1 through 4. The one thing this project hasn't had to deal with yet is a conversation growing long enough to hit real context-window or cost problems — something that will eventually happen to any of these conversations if you keep chatting in the same named thread for long enough. Lesson 6 tackles that problem directly: counting tokens before you hit a limit, and applying trimming and compaction strategies so a long-running conversation degrades gracefully instead of failing outright.

0 Comments

Reviewed before they appear

No comments yet.

OpenAI SDK
Introduction to the OpenAI SDK Setting Up Python Creating an API Key Your First Call — client.responses.create() and response.output_text Understanding Billing, Credits, and What a Request Costs Why Responses Replaced Chat Completions Anatomy of a Request: model, input, and instructions Anatomy of a Response: The Typed output Array, Not Just Text Roles: User, Assistant, and Developer/System Choosing a Model, and Reading the Models Page Instead of Memorizing Names Instructions vs. Input Writing Prompts That Get Consistent Results Few-Shot Examples Reasoning Models and the reasoning Parameter Debugging a Prompt That Misbehaves Why Streaming Matters for User Experience stream=True and Iterating Over Events Handling the Event Types You Actually Care About Background Mode for Long-Running Jobs Project — Add Live Streaming to Your Chatbot The Problem With Parsing Free Text JSON Schema and Strict Mode Pydantic Models With the SDK's Parse Helpers Handling Refusals and Validation Failures Project — A Resume-to-JSON Extractor Working With input_image input_file, PDFs, and the Files API Image Generation Speech-to-Text and Text-to-Speech Project: A PDF Question-Answering Script What Function Calling Is Defining a Tool Schema The Full Loop Multiple Tools Errors, Timeouts, and Untrusted Arguments Project: A Weather Assistant Web Search File Search and Vector Stores Code Interpreter Remote MCP Servers and Connectors Project: A Research Assistant What an Embedding Is, Without the Maths Generating and Storing Embeddings Similarity Search From Scratch Hosted Vector Stores vs. Rolling Your Own A Small RAG App Over a Folder of Notes Agents vs. a Single API Call — When You Need One pip install openai Giving Agents Tools Handoffs and Multi-Agent Triage Guardrails and Approvals Tracing and Observing What Your Agent Did A Multi-Agent Support Desk Error Codes and What Each One Means Retries, Timeouts, and Backoff Rate Limits and Spend Limits Prompt Caching and Cost Optimisation The Batch API for Bulk Work Async Clients and Concurrency Moderation and Safety Best Practices Designing the App Backend With FastAPI Streaming to a Simple Frontend Deploying and a Cost/Safety Checklist Why Web Search Is Useful for Current Information Using the Web Search Tool with the Responses API Configuring Search Behavior for Application Use Cases Understanding Citations and Source Attribution Where to Go Next Building a Research Assistant with Web Search Combining Web Search with Structured Outputs Handling Conflicting or Low-Quality Web Sources Reducing Unsupported Claims with Grounded Generation Testing Freshness-Sensitive AI Answers Production Considerations for Web-Grounded Applications Understanding File Search and Retrieval-Augmented Generation Creating and Organizing Vector Stores Uploading Documents for Retrieval Connecting Vector Stores to Responses API Requests Designing Document Metadata and Filtering Strategies Building a PDF Question-Answering Application Improving Retrieval Quality With Better Document Preparation Handling Missing Evidence and Retrieval Failures Combining File Search With Web Search Building a Production Knowledge-Base Assistant What the Code Interpreter Tool Is Designed For Running Python-Based Analysis Through the OpenAI SDK Uploading Datasets for Analysis Analyzing CSV and Spreadsheet Data Generating Charts and Data Summaries Handling Generated Files and Downloadable Artifacts Building a Data-Analysis Assistant Combining Code Execution with Structured Outputs Validating Generated Calculations and Results Security and Sandbox Considerations for Code Execution Understanding Multimodal Input with the OpenAI SDK Sending Images to a Model Image Analysis from URLs and Uploaded Files Extracting Text and Information from Screenshots Building an Image-Question-Answering Application Combining Image Input with Structured Output Analyzing Multiple Images in One Request Handling Image Quality and Input Limitations Designing Multimodal Prompts for Reliable Results Building a Practical Vision-Powered Python Application Understanding Speech-to-Text and Text-to-Speech Workflows Transcribing Audio with the OpenAI SDK Working with Uploaded Audio Files Handling Timestamps and Transcription Metadata Building a Meeting Transcription Workflow Generating Spoken Responses from Text Handling Long Audio and Processing Failures Combining Audio with Text and Tool Calling Building an End-to-End Python Voice Application What Embeddings Are and When to Use Them Generating Embeddings With the OpenAI API Preparing Text for Embedding Comparing Vectors With Cosine Similarity Building a Simple Semantic Search Engine in Python Storing Embeddings in a Database Metadata Filtering for Semantic Search Chunking Strategies for Better Retrieval Evaluating Semantic Search Quality Building a Document Similarity Application When Batch Processing Makes Sense Designing Large-Volume AI Processing Pipelines Using Asynchronous Python with the OpenAI SDK Running Concurrent Requests Safely Controlling Concurrency and Avoiding Rate Limits Tracking Batch Job Progress Handling Partial Failures in Bulk Workloads Retrying Failed Items Without Duplicating Successful Work Designing Resumable AI Processing Jobs Building a Production Batch-Processing Pipeline Batch Processing Makes Sense Large-Scale AI Processing Pipelines Async Python with OpenAI SDK Safe Concurrent Requests Concurrency & Rate Limits Batch Progress Tracking Partial Failure Handling Safe Retry Handling Resumable AI Jobs Production Batch Pipeline System–User Data Separation Reusable App Instructions Prompt Templates & Variables Extraction & Classification Prompts Summarization & Transformation Prompts Explicit Output Requirements Prompt Version Management Prompt Testing & Evaluation Reusable Python Prompt Library API Key Security Secure API Key Storage Secure Secret Management Prompt Injection Prevention Trusted vs. Untrusted Content Tool Argument Validation Sensitive Data Handling Secure Logging AI Action Authorization Production AI Security Checklist Why AI Applications Need Evaluation Beyond Unit Tests Unit Testing OpenAI SDK Integration Code Mocking API Responses in Python Tests Testing Structured Outputs Against Schemas Testing Tool-Calling Workflows Building a Small Evaluation Dataset Measuring Accuracy, Consistency, and Failure Rates Regression Testing Prompts and Model Changes Human Evaluation Versus Automated Evaluation Creating a Repeatable Evaluation Pipeline AI Request Monitoring Token Cost Management Usage Metrics Design Reducing Model Calls Prompt & Context Optimization Model Selection & Optimization AI Caching Strategies Interactive Latency Optimization Usage Dashboards & Budget Alerts Performance & Cost Checklist Every API Call Starts Fresh Fixing API Statelessness Server-Side Conversation Memory Limits of Response Chaining What We're Building Conversation Memory Challenges Preparing an OpenAI SDK Application for Deployment Environment-Specific Configuration for Development and Production Deploying a Python AI Service with Docker Container Health Checks and Startup Configuration Managing Secrets in Cloud Deployments Background Workers for Long-Running AI Tasks Queues and Asynchronous Job Architectures Scaling AI Workloads Horizontally Monitoring Production Incidents and Failures Production Deployment Checklist for OpenAI SDK Applications Reusable OpenAI Service Classes AI Client Dependency Injection Typed AI Responses Python Configuration Management AI Request Decorators Centralized AI Error Handling Clean SDK Abstractions Reusable OpenAI Utilities Internal AI Python Libraries SDK Integration Maintenance Production AI Chatbot Document Q&A System Web Research Assistant Customer Support Agent AI Data Analysis Assistant Image Analysis App Meeting Transcription & Summary Semantic Document Search Multi-Tool AI Agent Production OpenAI SDK App Why "It Looked Fine When I Tested It" Isn't Enough Timing Note Status Note Pre-Decision Status Note Current Availability Note
Ask about this post
AI Ask about this post

Ask questions about What We're Building and get answers drawn from it.

Signed-in readers only.