AI Data Analysis Assistant

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

Project 5: Build an AI Data-Analysis Assistant

This project builds an assistant that answers questions about a tabular dataset by writing and running its own analysis code, using the Code Interpreter tool from Unit 17. The core value of this approach over asking the model to reason about data in plain text is that the model computes exact answers — real aggregations, real statistics, real chart data — rather than approximating them from a text description of the data.

Scope and Design Decisions

The assistant accepts a CSV file and a natural-language question, and returns a text answer plus, when relevant, a generated chart. It is scoped to single-dataset analysis in one session rather than a multi-dataset warehouse — joining across files or connecting to a live database is a natural extension, not part of the base project.

Two decisions shape the implementation:

  1. The dataset is uploaded once and reused across a session's questions. Re-uploading the file on every question wastes both time and tokens; the container that Code Interpreter runs in persists for the lifetime of the conversation thread, so the file is attached once.
  2. Generated code and outputs are surfaced to the caller, not hidden. For a data-analysis tool, showing what computation actually produced an answer is a trust and debugging requirement, not an optional nicety — a user needs to be able to check that the model filtered the right column before trusting a number.

Uploading the Dataset and Running the First Analysis

from openai import OpenAI

client = OpenAI()

def create_analysis_session(csv_path: str) -> str:
    with open(csv_path, "rb") as f:
        uploaded_file = client.files.create(file=f, purpose="assistants")

    response = client.responses.create(
        model="gpt-5.6-terra",
        tools=[{"type": "code_interpreter", "container": {"type": "auto", "file_ids": [uploaded_file.id]}}],
        input=[{
            "role": "user",
            "content": "A CSV file has been attached. Load it and report the column "
                        "names, row count, and data types, without further analysis yet.",
        }],
    )
    container_id = _extract_container_id(response)
    return container_id

def _extract_container_id(response) -> str:
    for item in response.output:
        if item.type == "code_interpreter_call":
            return item.container_id
    raise RuntimeError("No code interpreter call found in the response.")

container.type: "auto" tells the API to provision a sandboxed execution container automatically and load the attached file into it; the returned container_id is what makes the container reusable across subsequent calls instead of starting a fresh, file-less container on every question. The first call intentionally asks only for a structural summary — column names, row count, dtypes — rather than jumping straight into analysis, which both confirms the file loaded correctly and gives the calling application useful metadata (for example, to populate a UI showing available columns) before any real analysis is requested.

Note: Code Interpreter container lifetime and expiration policy are managed by OpenAI and can change; a long-running analysis session should handle the case where a container has expired by re-uploading the file and creating a new one, rather than assuming a container ID remains valid indefinitely.

Asking Analytical Questions Against a Persistent Container

def ask_data_question(container_id: str, question: str) -> dict:
    response = client.responses.create(
        model="gpt-5.6-terra",
        tools=[{"type": "code_interpreter", "container": {"type": "auto", "id": container_id}}],
        input=[{"role": "user", "content": question}],
    )

    generated_code = []
    chart_file_ids = []
    for item in response.output:
        if item.type == "code_interpreter_call":
            generated_code.append(item.code)
            for output in getattr(item, "outputs", []) or []:
                if output.get("type") == "image":
                    chart_file_ids.append(output["file_id"])

    return {
        "answer": response.output_text,
        "code_run": generated_code,
        "chart_file_ids": chart_file_ids,
    }

Reusing container.id (rather than container.type: "auto" with a fresh file_ids list) is what keeps this a persistent analysis session — the dataset and any intermediate variables the model created in earlier turns remain available, so a follow-up question like "now break that down by region" does not need to restate what "that" refers to from scratch. The function collects both the generated code and any chart file IDs the model produced, because a data-analysis assistant that hides its computation is far less trustworthy than one that shows its work — the returned code_run list lets the caller display exactly what pandas or matplotlib code the model executed to arrive at its answer.

Downloading Generated Charts

def save_charts(chart_file_ids: list[str], output_dir: str) -> list[str]:
    import os
    os.makedirs(output_dir, exist_ok=True)
    saved_paths = []
    for i, file_id in enumerate(chart_file_ids):
        content = client.files.content(file_id)
        path = os.path.join(output_dir, f"chart_{i}.png")
        with open(path, "wb") as f:
            f.write(content.read())
        saved_paths.append(path)
    return saved_paths

Charts the model generates inside the sandbox exist only as files within that container until explicitly retrieved through the Files API — client.files.content downloads the raw image bytes. This two-step retrieval (get the file ID from the response, then separately download its content) is a pattern worth internalizing: several OpenAI tools produce artifacts by reference rather than embedding raw bytes directly in the response payload, keeping response objects lightweight even when the underlying artifact is large.

Guarding Against Unbounded Analysis Requests

FORBIDDEN_PATTERNS = ["requests.", "urllib", "socket.", "subprocess", "os.system"]

def validate_question_scope(question: str) -> None:
    lowered = question.lower()
    if any(pattern in lowered for pattern in FORBIDDEN_PATTERNS):
        raise ValueError(
            "This question appears to request network or system access, which "
            "is outside the scope of this data-analysis assistant."
        )

def ask_data_question_safely(container_id: str, question: str) -> dict:
    validate_question_scope(question)
    return ask_data_question(container_id, question)

The Code Interpreter sandbox already isolates code execution from the host system, so this check is not a security boundary in itself — it is a scope guard that catches obviously out-of-domain requests before spending a model call on them. Real security for the execution environment comes from the sandbox itself (Unit 17 and Unit 23 cover this in more depth); validate_question_scope exists purely to keep the assistant focused on data analysis and to fail fast on requests that are clearly not what the tool is for.

Testing the Extraction Logic

class FakeCodeCall:
    type = "code_interpreter_call"
    def __init__(self, code, outputs):
        self.code = code
        self.outputs = outputs

class FakeResponse:
    def __init__(self, output, text):
        self.output = output
        self.output_text = text

def test_extracts_code_and_chart_ids(monkeypatch_create):
    fake_call = FakeCodeCall(
        code="df.groupby('region')['sales'].sum().plot(kind='bar')",
        outputs=[{"type": "image", "file_id": "file-abc123"}],
    )
    fake_response = FakeResponse(output=[fake_call], text="Sales by region are shown above.")

    monkeypatch_create(lambda **kwargs: fake_response)
    result = ask_data_question("container-123", "Show sales by region")

    assert result["answer"] == "Sales by region are shown above."
    assert result["chart_file_ids"] == ["file-abc123"]
    assert "groupby" in result["code_run"][0]
    print("PASS: code and chart file IDs are correctly extracted from the response")

def test_validate_question_scope_blocks_network_access():
    try:
        validate_question_scope("Use requests.get to fetch external data and merge it in")
        assert False, "expected ValueError"
    except ValueError:
        print("PASS: out-of-scope network request is rejected before a model call")

def _make_monkeypatch():
    original = client.responses.create
    def apply(fn):
        client.responses.create = fn
    def restore():
        client.responses.create = original
    return apply, restore

monkeypatch_create, restore_create = _make_monkeypatch()
test_extracts_code_and_chart_ids(monkeypatch_create)
restore_create()
test_validate_question_scope_blocks_network_access()

The fake FakeCodeCall and FakeResponse classes mimic just enough of the real response shape — a type attribute, code, and outputs — to exercise ask_data_question's extraction logic without a real sandbox execution. This is valuable specifically because Code Interpreter calls are slow and cost real compute; a test suite that could only validate this parsing logic against live calls would be far more expensive to run on every change.

Extending This Project

Add multi-file support so the assistant can join two related CSVs (for example, orders and customers) inside the same container, and add a result-caching layer keyed on a hash of the question and container ID so repeated identical questions do not re-run the same analysis.

Common Mistakes

  • Re-uploading the dataset on every question. This discards the persistent container's state and is unnecessary — reuse the container ID from the first call for the rest of the session.
  • Hiding the generated code from the end user. Data-analysis answers are only as trustworthy as the computation behind them; surfacing the code the model ran is what lets a user catch a wrong filter or a misinterpreted column.
  • Treating scope-guard string matching as a security control. validate_question_scope narrows what the assistant attempts; it is not a substitute for the sandbox's own isolation, and should never be relied on as the only defense against unsafe code execution.

Best Practices

  • Reuse the sandbox container across a session's questions. It preserves loaded data and intermediate state, and avoids redundant re-uploads.
  • Always retrieve and expose both the answer and the code that produced it. This is what separates a data-analysis assistant from a black box that produces numbers no one can verify.
  • Download generated artifacts (charts, exported files) explicitly rather than assuming they persist indefinitely. Container-scoped files should be retrieved and stored durably as soon as they are needed beyond the current session.

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 AI Data Analysis Assistant and get answers drawn from it.

Signed-in readers only.