Build a Sales Brain with SQLite

Build a Python sales assistant with validated AI output and SQLite memory.

Introduction

30 Second Summary

A customer asks whether their size is in stock. A confident guess can turn a helpful conversation into a costly mistake.

In this project, you will build a Python terminal sales brain that turns customer messages into validated analyses. It saves every successful analysis beside trusted business records in a local SQLite database.

What You'll Build

You type a customer message to see validated JSON appear with a customer-ready reply before the result becomes a permanent audit record.

By the end of this project, you'll have:

  • A validated sales analysis that classifies each message into predictable fields for intent, sentiment, urgency, missing details, and escalation.
  • A local business memory where you can inspect products, inventory, customers, orders, and conversation audits.
  • A repeatable terminal workflow that processes several customer messages before showing the growing audit count.
  • Secret Mission: Build a safe product search that joins products with inventory by color, size, budget, and stock.

Are there any prerequisites?

You need Python basics, a Mac terminal, VS Code, Python 3.10+, and an OpenAI API key with billing enabled. API billing can feel risky, so this project keeps your working budget at no more than $5.

Before We Start

Before any setup, this step locks in the contract for the sales brain you are about to build. It turns each customer message into a validated analysis plus a draft response while SQLite supplies prices, stock, policies, product details, order status, or customer records.

Set Up and Hear the Brain Speak

The sales brain needs a live OpenAI API connection before it can respond to a customer. A first customer reply gives you proof that the connection works.

The OpenAI Agents SDK handles the model call through Agent plus Runner. You will inspect the actual Python output type before deciding how later code can use it.

In this step, get ready to:
  • Create an isolated project environment with pinned packages.
  • Prepare the current terminal for authenticated API calls.
  • Run a naive sales assistant against one customer message.
Create the project environment

A virtual environment keeps this project's packages separate from the rest of your Mac. The .venv folder becomes the isolated home for both pinned dependencies.

  • Press Cmd+Space to open Spotlight on your Mac.
  • Type Terminal into Spotlight.
  • Press Return to open Terminal.
  • Set up the ai-sales-brain folder on your Desktop by running these commands:
cd ~/Desktop
mkdir ai-sales-brain
cd ai-sales-brain
code .

What do these commands do?

  • The first command moves Terminal to your Desktop.
  • The next command creates the ai-sales-brain folder.
  • The third command makes that folder your current terminal location.
  • The final command opens the current folder in VS Code.

You should see ai-sales-brain as the top-level folder in VS Code's Explorer sidebar.

Did the folder stay closed?

The VS Code terminal command may be unavailable on your Mac. In VS Code, select File followed by Open Folder....

Choose the ai-sales-brain folder on your Desktop. Help me open my project folder in VS Code.

  • Create the virtual environment inside ai-sales-brain by running these commands:
python -m venv .venv
source .venv/bin/activate

What does the virtual environment do?

The first command creates an isolated Python environment in .venv. The second command activates that environment for the current terminal session.

Your terminal prompt should now begin with (.venv). That prefix confirms future package installations stay inside this project.

Don't see the environment prefix?

Confirm that Terminal is still inside the ai-sales-brain folder. Run the activation command again from that location.

If the environment still does not activate, confirm that the .venv folder appears in VS Code's Explorer. Help me activate this Python virtual environment.

  • Click the New File button in VS Code's Explorer sidebar.
  • Enter requirements.txt as the file name.
  • Add the pinned project packages by pasting this content into requirements.txt:
openai-agents==0.23.1
pydantic==2.14.0

Why pin these packages?

  • The openai-agents package provides the agent plus runner used for model calls.
  • Pydantic defines validated data models for the structured contract you will build next.
  • The exact pins make your environment use OpenAI Agents SDK 0.23.1 with Pydantic 2.14.0.
  • Save requirements.txt with Cmd+S.
  • Switch back to the Terminal window from earlier.
  • Install the pinned dependencies into the active environment by running:
python -m pip install -r requirements.txt

What does this install command do?

Python asks pip to read every exact package version from requirements.txt. The active virtual environment keeps those packages inside .venv.

Why the OpenAI Agents SDK?

The SDK manages the agent run through Agent plus Runner. This keeps the project focused on customer-message analysis.

A direct Responses API integration would require more request-loop code. The SDK also provides the path toward structured outputs plus future function tools.

That is your isolated environment ready. Both pinned packages are now available to this project.

Did the package installation fail?

  • Check that your terminal prompt begins with (.venv).
  • Check that requirements.txt contains both package lines exactly as shown.
  • Retry the installation after confirming your internet connection.

Help me diagnose the dependency installation failure.

Export the key and verify imports

The SDK reads your API key from the OPENAI_API_KEY environment variable. The key stays in this terminal session instead of being written into a project file.

Credentials can feel risky to handle. This command does not print your key after you run it.

  • Replace sk-... in the command below with your real OpenAI API key.
  • Export the key into the current Terminal session by running the completed command:
export OPENAI_API_KEY=sk-...

Where does the key go?

The export applies only to this Terminal session. Closing the session removes the exported value from that shell.

Keep the key out of screenshots. Never paste it into requirements.txt or a Python file.

  • Start the Python interpreter inside the active virtual environment by running:
python

What does this command open?

This opens an interactive Python session using the interpreter from .venv. You can test imports before creating the assistant.

  • Verify the installed imports by entering these lines:
from agents import Agent, Runner
from pydantic import BaseModel

Agent
Runner
BaseModel

What do these imports prove?

  • The agents import proves the OpenAI Agents SDK is available inside .venv.
  • The pydantic import proves the validation library is available.
  • The final expressions ask Python to display the imported class descriptions.
  • Press Control+D to leave the Python interpreter.

You should see class descriptions for Agent, Runner, and BaseModel. Your terminal should then return to the (.venv) prompt.

Seeing an import failure?

An import failure usually means the virtual environment is inactive or the package installation did not finish. Confirm the (.venv) prefix before retrying the requirements installation.

Help me fix the failed Python imports.

Build and run the naive assistant

Your environment can now import the SDK. The first assistant uses one prompt plus one model call so you can inspect exactly what comes back.

  • Click the New File button in VS Code's Explorer sidebar.
  • Enter naive_brain.py as the file name.
  • Build the first sales assistant by pasting this code into naive_brain.py:
from agents import Agent, Runner


agent = Agent(
    name="Naive Sales Assistant",
    instructions="Reply helpfully to customer sales messages.",
    model="gpt-6-luna",
)

message = input("Customer: ").strip()

if message:
    result = Runner.run_sync(agent, message)
    print(f"\nOutput type: {type(result.final_output).__name__}")
    print(f"Assistant: {result.final_output}")
else:
    print("Please enter a customer message.")

What does this code do?

  • The imports bring Agent plus Runner into the script.
  • The agent configuration gives the assistant a name plus a short instruction.
  • The message value stores one customer message after trimming surrounding whitespace.
  • Runner.run_sync() sends that message to gpt-6-luna through the agent.
  • The final lines display the reply plus its Python type.
  • Save naive_brain.py with Cmd+S.

This test sends one customer message to the usage-based API. You are making one controlled request.

Before you run it, what Python output type do you think the model reply will use?

  • Start the naive sales assistant by running:
python naive_brain.py

What does this command do?

Python runs naive_brain.py inside the active virtual environment. The program pauses at the Customer: prompt for your message.

  • Enter Do you have black running shoes in size 10? at the customer prompt.

You should see Output type: str followed by an assistant reply. This is the intended shortfall in the first version.

Didn't receive an assistant reply?

  • Confirm that the terminal prompt includes (.venv) before you start the script.
  • Export the API key again if you opened a new Terminal session.
  • Check your internet connection if the request cannot reach the API.

Help me diagnose the failed naive assistant request.

You have your first live sales reply. The run proves that your environment can reach the model through the SDK.

Why does the string fall short?

A str contains free-form text without guaranteed fields. Database logic cannot reliably extract intent or missing information from that shape.

The type check gives you direct evidence of the gap. The next contract gives each required value a predictable place.

✔️ Awesome, I've got everything!

Your packages are installed. Double check that both project files are saved before moving on.

ⓧ I'd like to double check the full code

Compare your two files with these complete references. Each line should match exactly.

openai-agents==0.23.1
pydantic==2.14.0
from agents import Agent, Runner


agent = Agent(
    name="Naive Sales Assistant",
    instructions="Reply helpfully to customer sales messages.",
    model="gpt-6-luna",
)

message = input("Customer: ").strip()

if message:
    result = Runner.run_sync(agent, message)
    print(f"\nOutput type: {type(result.final_output).__name__}")
    print(f"Assistant: {result.final_output}")
else:
    print("Please enter a customer message.")

Your API connection works, but the reply has no dependable structure. Next up, you will define the JSON contract that every analysis must satisfy.

Define the Brain's JSON Contract

Your first sales assistant answered a real customer message. Its reply was a plain str, so later code cannot reliably identify the intent or decide whether business data is required.

The brain now needs a predictable contract with named fields. A Pydantic model validates those fields before later code uses the JSON output to drive tables or tools.

In this step, get ready to:
  • Define the CustomerAnalysis schema for every sales-analysis field.
  • Constrain intent, sentiment, and urgency to approved values.
  • Validate a local example as indented JSON without an API request.
Define the analysis fields

A contract describes the exact shape that every successful analysis must follow. Typed fields preserve customer details while constrained categories prevent unexpected labels.

  • In the file tree on the left side of VS Code, right-click the ai-sales-brain folder.

You'll see a context menu for the project folder.

  • Choose the option that creates a new file.

You'll see a field where you can name the file.

  • Enter analysis_schema.py as the file name.

You'll see a blank analysis_schema.py tab ready for the contract.

  • Define the complete field structure by pasting this code into analysis_schema.py:
from typing import Literal

from pydantic import BaseModel


class CustomerAnalysis(BaseModel):
    intent: Literal[
        "product_search",
        "price_question",
        "stock_question",
        "order_request",
        "complaint",
        "refund_request",
        "general_question",
        "unknown",
    ]
    customer_need: str
    details_mentioned: list[str]
    missing_information: list[str]
    sentiment: Literal["positive", "neutral", "frustrated"]
    urgency: Literal["low", "normal", "high"]
    requires_business_data: bool
    requires_human: bool
    response: str

What does this contract enforce?

  • CustomerAnalysis inherits from BaseModel so Pydantic can validate every field.
  • Literal limits intent, sentiment, and urgency to approved categories.
  • list[str] preserves each mentioned detail or missing detail as a separate value.
  • bool fields capture whether the brain needs stored facts or human support.
  • Save analysis_schema.py in VS Code.
  • Scan the field declarations for red error underlines.

You should see the complete CustomerAnalysis class without red error underlines.

Seeing an error in the model?

  • Check that the closing ] appears after "unknown",.
  • Keep every field declaration indented beneath class CustomerAnalysis(BaseModel):.
  • Help me fix my CustomerAnalysis model.
Test the contract locally

A local example proves that the contract accepts correctly shaped data. This test uses Pydantic without sending an OpenAI API request.

  • In analysis_schema.py, place your cursor below the CustomerAnalysis class.
  • Add the local contract test by pasting this code:
if __name__ == "__main__":
    example = CustomerAnalysis(
        intent="product_search",
        customer_need="Find black running shoes",
        details_mentioned=["black", "running shoes", "size 10"],
        missing_information=["preferred brand", "budget"],
        sentiment="neutral",
        urgency="normal",
        requires_business_data=True,
        requires_human=False,
        response="I can help with that. Do you have a preferred brand or budget?",
    )
    print(example.model_dump_json(indent=2))

How does the local test work?

  • The __main__ guard runs the example when you execute this file directly.
  • CustomerAnalysis(...) checks the example against every declared field and constraint.
  • model_dump_json(indent=2) serializes the validated model as readable JSON.
  • Save analysis_schema.py in VS Code.

Before you run this, do you expect Python-style object text or serialized JSON with named fields?

  • Test the contract in the activated terminal by running:
python analysis_schema.py

What should you see?

The terminal prints indented JSON containing intent, customer_need, requires_business_data, requires_human, and response.

That's the contract working. Your example has passed validation and become machine-readable output.

Contract test not printing JSON?

  • Confirm the terminal is still inside the ai-sales-brain folder.
  • Confirm the terminal prompt still shows the activated .venv environment.
  • Help me debug the local schema test.

✔️ Awesome, I've got everything!

Great. Your validated JSON contract is saved and working locally.

ⓧ I'd like to double check the full code

The complete analysis_schema.py file is shown below for comparison.

from typing import Literal

from pydantic import BaseModel


class CustomerAnalysis(BaseModel):
    intent: Literal[
        "product_search",
        "price_question",
        "stock_question",
        "order_request",
        "complaint",
        "refund_request",
        "general_question",
        "unknown",
    ]
    customer_need: str
    details_mentioned: list[str]
    missing_information: list[str]
    sentiment: Literal["positive", "neutral", "frustrated"]
    urgency: Literal["low", "normal", "high"]
    requires_business_data: bool
    requires_human: bool
    response: str


if __name__ == "__main__":
    example = CustomerAnalysis(
        intent="product_search",
        customer_need="Find black running shoes",
        details_mentioned=["black", "running shoes", "size 10"],
        missing_information=["preferred brand", "budget"],
        sentiment="neutral",
        urgency="normal",
        requires_business_data=True,
        requires_human=False,
        response="I can help with that. Do you have a preferred brand or budget?",
    )
    print(example.model_dump_json(indent=2))

Your sales brain now has a reliable language for intent, missing information, and escalation. Next, you'll connect this contract to the agent so customer messages return validated objects.

Connect Structured AI Analysis

Your CustomerAnalysis schema already gives the sales brain a validated decision shape. Now the OpenAI Agents SDK needs to produce that shape from a live customer message.

The earlier assistant returned a plain str. That free-form output cannot reliably supply the named fields that later database actions need.

In this step, get ready to:
  • Configure the sales agent to return a validated CustomerAnalysis object.
  • Add a function that verifies the agent's final output.
  • Test one customer message through a temporary entry point.
Configure the structured sales agent

A structured output asks the model to fill a defined schema. Setting output_type to CustomerAnalysis connects your Pydantic contract to GPT-6 Luna.

The agent also needs rules for decisions that depend on real business facts. These rules keep prices, stock levels, policies, order statuses, product details, and customer records grounded in future database lookups.

  • Create sales_brain.py inside the ai-sales-brain folder using the file control in VS Code's left sidebar.
  • Add the structured agent configuration to sales_brain.py by pasting this code:
from agents import Agent, Runner

from analysis_schema import CustomerAnalysis


sales_agent = Agent(
    name="Sales Brain",
    instructions=(
        "Analyze one customer message for a retail sales team. "
        "Return every field in the requested structured output. "
        "Never invent prices, stock availability, policies, product specifications, "
        "order status, or customer records. "
        "Set requires_business_data to true when the answer depends on price, stock, "
        "policy, product, order, or customer data that is not present in the message. "
        "Set requires_human to true for complaints, refund requests, payment problems, "
        "legal threats, safety issues, or an explicit request for a person. "
        "List the important details already mentioned and any information still needed. "
        "Write a concise response directly to the customer. If business data is required, "
        "say you need to check it rather than guessing."
    ),
    model="gpt-6-luna",
    output_type=CustomerAnalysis,
)

What does this code do?

  • The imports bring in the agent framework plus your existing CustomerAnalysis contract.
  • The instructions identify cases that require business data.
  • The instructions identify complaints plus other cases that require human help.
  • The output_type setting makes the final result follow the fields defined by CustomerAnalysis.
  • Save sales_brain.py.
  • Confirm that the imports and agent configuration load by running this command:
python sales_brain.py

What does this check prove?

Python loads sales_brain.py plus its imported schema. A clean return to your terminal prompt proves that the agent configuration can initialize.

Good progress. Your typed agent configuration now loads without sending an API request.

Does the configuration fail to load?

Check that sales_brain.py sits beside analysis_schema.py inside ai-sales-brain.

Make sure you are using the terminal from earlier with the activated .venv environment.

Ask for help with the exact terminal output: Help me diagnose why sales_brain.py cannot load its imports or agent configuration.

Add the analysis function

The runner produces a result object whose final_output should now be a CustomerAnalysis instance. A runtime type check protects the rest of the program if that contract is ever broken.

  • Move to the end of sales_brain.py.
  • Add the analysis function by pasting this code below sales_agent.
def analyze_message(message: str) -> CustomerAnalysis:
    result = Runner.run_sync(sales_agent, message)
    analysis = result.final_output

    if not isinstance(analysis, CustomerAnalysis):
        raise TypeError("The agent did not return CustomerAnalysis.")

    return analysis

How does the function protect the contract?

  • Runner.run_sync() sends one customer message through sales_agent.
  • The analysis variable holds the agent's final output.
  • The isinstance() guard stops execution if the result does not match CustomerAnalysis.
  • The return annotation gives later code a predictable object to use.
  • Save sales_brain.py.
  • Confirm that the new function definition loads by running this command:
python sales_brain.py

What does this second check prove?

Python now parses the analyze_message() function as part of the file. Returning to the terminal prompt without a traceback confirms that the function is ready for the temporary entry point.

The boundary is in place. Every successful call through analyze_message() now returns the validated object expected by the rest of the project.

Seeing a Python error after adding the function?

Check that analyze_message() begins at the far-left indentation level below the completed sales_agent configuration.

Compare the spelling of CustomerAnalysis with the imported class name.

Ask for help with the failing line: Help me fix the Python error in my analyze_message function.

Test one structured customer message

A temporary entry point gives the function one message from the terminal. It prints the returned model through model_dump_json(indent=2) so you can inspect every validated field.

  • Move to the end of sales_brain.py.
  • Add the temporary entry point by pasting this code below analyze_message().
def main() -> None:
    message = input("Customer: ").strip()

    if not message:
        print("Please enter a customer message.")
        return

    analysis = analyze_message(message)
    print(analysis.model_dump_json(indent=2))


if __name__ == "__main__":
    main()

What happens when the file runs?

  • main() collects one customer message.
  • The blank-input check prevents an empty request.
  • analyze_message() returns the validated analysis.
  • model_dump_json(indent=2) renders the model as readable JSON.
  • Save sales_brain.py.

This test sends one usage-based API request. Your project budget covers this focused message test.

Prediction time: plain string or validated JSON.

  • Start the structured sales brain by running this command:
python sales_brain.py

What does this command do?

Python executes the temporary main() entry point. The program pauses at the customer prompt until you submit a message.

  • Enter I want black running shoes in size 10 at the customer prompt.

You'll see indented JSON containing intent, customer_need, details_mentioned, missing_information, sentiment, urgency, requires_business_data, requires_human, and response.

The intent value uses one of your constrained labels. The output also marks requires_business_data as true because stock information must come from business data.

That closes the raw-text gap. Your live agent now returns a validated CustomerAnalysis object with a customer-facing response.

Did the structured request fail?

Return to the terminal session from earlier so the current-terminal OPENAI_API_KEY remains available.

Check that analysis_schema.py still defines every field referenced by the agent's output type.

Ask for help without sharing your API key: Help me diagnose why my structured sales agent request failed.

✔️ Awesome, I've got everything!

Your structured sales brain is working. Double check that sales_brain.py is saved before moving on.

ⓧ I'd like to double check the full code

Compare your sales_brain.py file with this complete version.

from agents import Agent, Runner

from analysis_schema import CustomerAnalysis


sales_agent = Agent(
    name="Sales Brain",
    instructions=(
        "Analyze one customer message for a retail sales team. "
        "Return every field in the requested structured output. "
        "Never invent prices, stock availability, policies, product specifications, "
        "order status, or customer records. "
        "Set requires_business_data to true when the answer depends on price, stock, "
        "policy, product, order, or customer data that is not present in the message. "
        "Set requires_human to true for complaints, refund requests, payment problems, "
        "legal threats, safety issues, or an explicit request for a person. "
        "List the important details already mentioned and any information still needed. "
        "Write a concise response directly to the customer. If business data is required, "
        "say you need to check it rather than guessing."
    ),
    model="gpt-6-luna",
    output_type=CustomerAnalysis,
)


def analyze_message(message: str) -> CustomerAnalysis:
    result = Runner.run_sync(sales_agent, message)
    analysis = result.final_output

    if not isinstance(analysis, CustomerAnalysis):
        raise TypeError("The agent did not return CustomerAnalysis.")

    return analysis


def main() -> None:
    message = input("Customer: ").strip()

    if not message:
        print("Please enter a customer message.")
        return

    analysis = analyze_message(message)
    print(analysis.model_dump_json(indent=2))


if __name__ == "__main__":
    main()

How to compare this file

Check the agent configuration first. Then compare the analyze_message() function plus the temporary main() entry point.

Your sales brain can now reason within a validated contract. Next, you'll build the SQLite business memory that supplies the facts it refuses to invent.

Build the SQLite Business Memory

Your structured sales brain can spot when a message needs business facts. It still has no trustworthy place to retrieve those facts.

Now you will give it durable business memory with SQLite. The local database preserves records after the Python process ends.

In this step, get ready to:
  • Define five linked tables with integrity constraints.
  • Seed product, inventory, customer, plus order records.
  • Verify persistent seed data through a five-table summary.
Create the database schema

A relational schema defines the shape of each table. Primary keys identify individual rows. Foreign keys connect related records.

  • In the VS Code Explorer sidebar, click the New File icon.
  • Enter database.py as the file name.

You will see database.py open in a new editor tab inside ai-sales-brain.

  • Add the imports, database path, plus the first table definitions by pasting this into database.py:
import sqlite3
from datetime import datetime, timezone
from pathlib import Path


DB_FILE = Path("sales_brain.db")

SCHEMA = """
CREATE TABLE IF NOT EXISTS products (
    id INTEGER PRIMARY KEY,
    sku TEXT NOT NULL UNIQUE,
    name TEXT NOT NULL,
    color TEXT NOT NULL,
    price_cents INTEGER NOT NULL CHECK (price_cents >= 0)
);

CREATE TABLE IF NOT EXISTS customers (
    id INTEGER PRIMARY KEY,
    name TEXT NOT NULL,
    email TEXT NOT NULL UNIQUE,
    created_at TEXT NOT NULL
);

How Does This Schema Start?

  • The DB_FILE path points every connection at the local sales_brain.db file.
  • The products table gives every product a unique identity, SKU, name, color, plus non-negative price.
  • The customers table prevents duplicate email addresses through its UNIQUE constraint.
  • Continue the SCHEMA string directly below the customers table with these linked tables:


CREATE TABLE IF NOT EXISTS inventory (
    product_id INTEGER NOT NULL,
    size TEXT NOT NULL,
    quantity INTEGER NOT NULL CHECK (quantity >= 0),
    PRIMARY KEY (product_id, size),
    FOREIGN KEY (product_id) REFERENCES products(id)
);

CREATE TABLE IF NOT EXISTS orders (
    id INTEGER PRIMARY KEY,
    external_ref TEXT NOT NULL UNIQUE,
    customer_id INTEGER NOT NULL,
    status TEXT NOT NULL,
    created_at TEXT NOT NULL,
    FOREIGN KEY (customer_id) REFERENCES customers(id)
);

How Are These Tables Connected?

  • The combined product_id plus size key allows one inventory row per product size.
  • The inventory foreign key requires every stock row to reference an existing product.
  • The order foreign key requires every order to reference an existing customer.
  • Finish the SCHEMA string with the conversation audit table:


CREATE TABLE IF NOT EXISTS conversation_audit (
    id INTEGER PRIMARY KEY,
    created_at TEXT NOT NULL,
    message TEXT NOT NULL,
    intent TEXT NOT NULL,
    customer_need TEXT NOT NULL,
    details_mentioned_json TEXT NOT NULL,
    missing_information_json TEXT NOT NULL,
    sentiment TEXT NOT NULL,
    urgency TEXT NOT NULL,
    requires_business_data INTEGER NOT NULL,
    requires_human INTEGER NOT NULL,
    response TEXT NOT NULL
);
"""

Why Keep an Audit Table?

The conversation_audit table reserves one row for every successful analysis. Its columns mirror the structured fields in CustomerAnalysis.

This gives later queries a stable record of the original message, the analysis decision, plus the drafted response.

  • Save database.py.

The SCHEMA value now ends with a closing triple quote. VS Code should show all five table definitions as part of one complete string.

Does the Schema Look Incomplete?

Check that SCHEMA = """ appears before the first table. Confirm that the closing """ appears after the audit table.

Make sure every table definition closes with ); inside the schema string.

Help me compare my database schema with the required five-table structure.

Seed connected business records

The tables need realistic rows before the sales brain can use them. Parameterized SQL binds each runtime value through a ? placeholder.

A transaction groups the seed writes before commit() saves them. Closing the connection releases the local database file.

  • Add the product plus inventory seed values below the SCHEMA string by pasting this code:


PRODUCTS = [
    ("R-100", "Road Runner", "black", 8999),
    ("T-200", "Trail Pro", "blue", 11999),
    ("R-300", "Sprint Lite", "black", 10999),
]

INVENTORY = [
    ("R-100", "9", 2),
    ("R-100", "10", 4),
    ("T-200", "9", 3),
    ("T-200", "10", 0),
    ("R-300", "10", 1),
]

What Do the Seed Values Represent?

Each product tuple contains a SKU, product name, color, plus price in cents. Storing prices as integers avoids floating-point rounding in the database.

Each inventory tuple connects a SKU to one size plus its available quantity. One row deliberately records zero stock.

  • Add the shared connection helper below INVENTORY by pasting this function:


def get_connection() -> sqlite3.Connection:
    connection = sqlite3.connect(DB_FILE)
    connection.row_factory = sqlite3.Row
    connection.execute("PRAGMA foreign_keys = ON")
    return connection

What Does the Connection Helper Do?

  • The sqlite3.connect() call opens sales_brain.db through the shared path.
  • The sqlite3.Row factory lets later code read result columns by name.
  • The PRAGMA foreign_keys = ON statement enables relationship enforcement for every connection returned by this helper.
  • Start initialize_database() below get_connection() with the schema plus product seeding logic:


def initialize_database() -> None:
    connection = get_connection()

    try:
        connection.executescript(SCHEMA)
        connection.executemany(
            """
            INSERT OR IGNORE INTO products (sku, name, color, price_cents)
            VALUES (?, ?, ?, ?)
            """,
            PRODUCTS,
        )

        product_rows = connection.execute(
            "SELECT id, sku FROM products"
        ).fetchall()
        product_ids = {row["sku"]: row["id"] for row in product_rows}
        inventory_rows = [
            (product_ids[sku], size, quantity)
            for sku, size, quantity in INVENTORY
        ]

How Does Product Seeding Work?

The schema runs before any rows are inserted. INSERT OR IGNORE keeps repeated setup runs from duplicating products with existing SKUs.

The product query builds a SKU-to-ID map. That map converts readable inventory SKUs into the product IDs required by the foreign key.

  • Continue initialize_database() with the inventory plus customer writes:
        connection.executemany(
            """
            INSERT OR IGNORE INTO inventory (product_id, size, quantity)
            VALUES (?, ?, ?)
            """,
            inventory_rows,
        )

        created_at = datetime.now(timezone.utc).isoformat()
        connection.execute(
            """
            INSERT OR IGNORE INTO customers (name, email, created_at)
            VALUES (?, ?, ?)
            """,
            ("Alex Morgan", "alex@example.com", created_at),
        )
        customer = connection.execute(
            "SELECT id FROM customers WHERE email = ?",
            ("alex@example.com",),
        ).fetchone()

How Are the Customer Records Prepared?

The inventory rows use the product IDs created by the earlier insert. Every value is bound through placeholders.

The customer receives a UTC timestamp. The following lookup retrieves the generated customer ID needed by the order.

  • Finish initialize_database() with the order write plus transaction cleanup:

        if customer is not None:
            connection.execute(
                """
                INSERT OR IGNORE INTO orders
                    (external_ref, customer_id, status, created_at)
                VALUES (?, ?, ?, ?)
                """,
                ("ORDER-1001", customer["id"], "shipped", created_at),
            )

        connection.commit()
    finally:
        connection.close()

Why Commit and Close?

The order uses the retrieved customer ID to satisfy its foreign key. The conditional protects the write if no customer row was returned.

The commit() call makes every pending seed write persistent. The finally block closes the connection even if setup fails.

  • Save database.py.

The completed initialize_database() function now creates the schema, binds the seed values, commits the transaction, plus closes the connection.

Does the Seed Function Look Misaligned?

Confirm that every statement from connection.executescript(SCHEMA) through connection.commit() stays inside the try block.

Confirm that connection.close() sits inside the matching finally block.

Help me check the indentation and parameter binding in initialize_database().

Summarize the persistent data

A fresh connection proves that the committed rows survived after initialization closed its connection. The summary will count every table without making an API request.

  • Add get_summary() below initialize_database() by pasting this function:


def get_summary() -> dict[str, int]:
    connection = get_connection()

    try:
        return {
            "products": connection.execute(
                "SELECT COUNT(*) AS total FROM products"
            ).fetchone()["total"],
            "inventory": connection.execute(
                "SELECT COUNT(*) AS total FROM inventory"
            ).fetchone()["total"],
            "customers": connection.execute(
                "SELECT COUNT(*) AS total FROM customers"
            ).fetchone()["total"],
            "orders": connection.execute(
                "SELECT COUNT(*) AS total FROM orders"
            ).fetchone()["total"],
            "audit": connection.execute(
                "SELECT COUNT(*) AS total FROM conversation_audit"
            ).fetchone()["total"],
        }
    finally:
        connection.close()

How Does the Summary Prove Persistence?

The function calls get_connection() after initialization has closed its original connection. Each query reads a committed row count from sales_brain.db.

The dictionary keeps the five counts under predictable names. The finally block closes this read connection.

  • Add the program entry point below get_summary() by pasting this code:


def main() -> None:
    initialize_database()
    summary = get_summary()

    print(f"Products: {summary['products']}")
    print(f"Inventory rows: {summary['inventory']}")
    print(f"Customers: {summary['customers']}")
    print(f"Orders: {summary['orders']}")
    print(f"Audit records: {summary['audit']}")


if __name__ == "__main__":
    main()

What Does the Entry Point Do?

The main() function initializes the database before reopening it through get_summary().

The five print calls expose the stored row counts in the terminal. This gives you a visible check for every seeded table plus the empty audit table.

  • Save database.py.

Before you run the database, which five totals do you expect from the seed lists?

  • Create sales_brain.db plus print its persisted counts by running this command:
python database.py

What Should I See?

Your terminal should report these persisted counts:

  • Products: 3.
  • Inventory rows: 5.
  • Customers: 1.
  • Orders: 1.
  • Audit records: 0.

Do the Counts Differ?

Check that every table name in get_summary() matches its name in SCHEMA.

If the script stops before printing counts, compare the indentation inside initialize_database() with the full file below.

Help me diagnose the database.py error or unexpected table counts.

You have the durable memory layer working. Its product, inventory, customer, plus order rows now survive after Python exits.

✔️ Awesome, I've got everything!

Great. Double check that database.py is saved before moving on.

ⓧ I'd like to double check the full code

Compare your database.py file with this complete version.

import sqlite3
from datetime import datetime, timezone
from pathlib import Path


DB_FILE = Path("sales_brain.db")

SCHEMA = """
CREATE TABLE IF NOT EXISTS products (
    id INTEGER PRIMARY KEY,
    sku TEXT NOT NULL UNIQUE,
    name TEXT NOT NULL,
    color TEXT NOT NULL,
    price_cents INTEGER NOT NULL CHECK (price_cents >= 0)
);

CREATE TABLE IF NOT EXISTS customers (
    id INTEGER PRIMARY KEY,
    name TEXT NOT NULL,
    email TEXT NOT NULL UNIQUE,
    created_at TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS inventory (
    product_id INTEGER NOT NULL,
    size TEXT NOT NULL,
    quantity INTEGER NOT NULL CHECK (quantity >= 0),
    PRIMARY KEY (product_id, size),
    FOREIGN KEY (product_id) REFERENCES products(id)
);

CREATE TABLE IF NOT EXISTS orders (
    id INTEGER PRIMARY KEY,
    external_ref TEXT NOT NULL UNIQUE,
    customer_id INTEGER NOT NULL,
    status TEXT NOT NULL,
    created_at TEXT NOT NULL,
    FOREIGN KEY (customer_id) REFERENCES customers(id)
);

CREATE TABLE IF NOT EXISTS conversation_audit (
    id INTEGER PRIMARY KEY,
    created_at TEXT NOT NULL,
    message TEXT NOT NULL,
    intent TEXT NOT NULL,
    customer_need TEXT NOT NULL,
    details_mentioned_json TEXT NOT NULL,
    missing_information_json TEXT NOT NULL,
    sentiment TEXT NOT NULL,
    urgency TEXT NOT NULL,
    requires_business_data INTEGER NOT NULL,
    requires_human INTEGER NOT NULL,
    response TEXT NOT NULL
);
"""

PRODUCTS = [
    ("R-100", "Road Runner", "black", 8999),
    ("T-200", "Trail Pro", "blue", 11999),
    ("R-300", "Sprint Lite", "black", 10999),
]

INVENTORY = [
    ("R-100", "9", 2),
    ("R-100", "10", 4),
    ("T-200", "9", 3),
    ("T-200", "10", 0),
    ("R-300", "10", 1),
]


def get_connection() -> sqlite3.Connection:
    connection = sqlite3.connect(DB_FILE)
    connection.row_factory = sqlite3.Row
    connection.execute("PRAGMA foreign_keys = ON")
    return connection


def initialize_database() -> None:
    connection = get_connection()

    try:
        connection.executescript(SCHEMA)
        connection.executemany(
            """
            INSERT OR IGNORE INTO products (sku, name, color, price_cents)
            VALUES (?, ?, ?, ?)
            """,
            PRODUCTS,
        )

        product_rows = connection.execute(
            "SELECT id, sku FROM products"
        ).fetchall()
        product_ids = {row["sku"]: row["id"] for row in product_rows}
        inventory_rows = [
            (product_ids[sku], size, quantity)
            for sku, size, quantity in INVENTORY
        ]
        connection.executemany(
            """
            INSERT OR IGNORE INTO inventory (product_id, size, quantity)
            VALUES (?, ?, ?)
            """,
            inventory_rows,
        )

        created_at = datetime.now(timezone.utc).isoformat()
        connection.execute(
            """
            INSERT OR IGNORE INTO customers (name, email, created_at)
            VALUES (?, ?, ?)
            """,
            ("Alex Morgan", "alex@example.com", created_at),
        )
        customer = connection.execute(
            "SELECT id FROM customers WHERE email = ?",
            ("alex@example.com",),
        ).fetchone()

        if customer is not None:
            connection.execute(
                """
                INSERT OR IGNORE INTO orders
                    (external_ref, customer_id, status, created_at)
                VALUES (?, ?, ?, ?)
                """,
                ("ORDER-1001", customer["id"], "shipped", created_at),
            )

        connection.commit()
    finally:
        connection.close()


def get_summary() -> dict[str, int]:
    connection = get_connection()

    try:
        return {
            "products": connection.execute(
                "SELECT COUNT(*) AS total FROM products"
            ).fetchone()["total"],
            "inventory": connection.execute(
                "SELECT COUNT(*) AS total FROM inventory"
            ).fetchone()["total"],
            "customers": connection.execute(
                "SELECT COUNT(*) AS total FROM customers"
            ).fetchone()["total"],
            "orders": connection.execute(
                "SELECT COUNT(*) AS total FROM orders"
            ).fetchone()["total"],
            "audit": connection.execute(
                "SELECT COUNT(*) AS total FROM conversation_audit"
            ).fetchone()["total"],
        }
    finally:
        connection.close()


def main() -> None:
    initialize_database()
    summary = get_summary()

    print(f"Products: {summary['products']}")
    print(f"Inventory rows: {summary['inventory']}")
    print(f"Customers: {summary['customers']}")
    print(f"Orders: {summary['orders']}")
    print(f"Audit records: {summary['audit']}")


if __name__ == "__main__":
    main()

Your sales brain now has a trustworthy place for business facts. Next up, you will save every successful customer analysis into its conversation audit.

Save Every Analysis to SQLite

Your sales brain now returns validated JSON. Your SQLite database already preserves the business facts it must trust.

Those layers still operate separately. This step connects them so each successful analysis becomes durable audit data.

Without that write path, later automation has no conversation history to use. You will add safe audit writes before turning the one-message script into a resilient loop.

In this step, get ready to:
  • Add a parameterized database function that saves each validated analysis.
  • Turn the sales brain into a loop that handles repeated customer messages.
  • Submit two messages and confirm both audit records persist.
Add the audit write path

A successful analysis contains Python lists. The audit table stores those values as text. JSON serialization preserves each list so another Python process can read it later.

  • In database.py, replace the import section at the top with this code:
import json
import sqlite3
from datetime import datetime, timezone
from pathlib import Path

from analysis_schema import CustomerAnalysis

Why add these imports?

  • The json module converts the two list fields into text that SQLite can store.
  • The CustomerAnalysis import gives save_analysis() a precise input type.

The new save_analysis() function binds every runtime value through parameterized SQL. Its transaction commits only after the insert succeeds.

  • Add save_analysis() directly below initialize_database() by using the full-file reference below.
  • Add get_recent_audits() directly below get_summary() by using the same reference.
  • Update main() so it prints the latest audit rows after the table counts.

✔️ Awesome, I've got everything!

Save database.py after adding the audit write path and recent-audit query.

ⓧ I'd like to double check the full code

Compare your complete database.py with this reference.

import json
import sqlite3
from datetime import datetime, timezone
from pathlib import Path

from analysis_schema import CustomerAnalysis


DB_FILE = Path("sales_brain.db")

SCHEMA = """
CREATE TABLE IF NOT EXISTS products (
    id INTEGER PRIMARY KEY,
    sku TEXT NOT NULL UNIQUE,
    name TEXT NOT NULL,
    color TEXT NOT NULL,
    price_cents INTEGER NOT NULL CHECK (price_cents >= 0)
);

CREATE TABLE IF NOT EXISTS customers (
    id INTEGER PRIMARY KEY,
    name TEXT NOT NULL,
    email TEXT NOT NULL UNIQUE,
    created_at TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS inventory (
    product_id INTEGER NOT NULL,
    size TEXT NOT NULL,
    quantity INTEGER NOT NULL CHECK (quantity >= 0),
    PRIMARY KEY (product_id, size),
    FOREIGN KEY (product_id) REFERENCES products(id)
);

CREATE TABLE IF NOT EXISTS orders (
    id INTEGER PRIMARY KEY,
    external_ref TEXT NOT NULL UNIQUE,
    customer_id INTEGER NOT NULL,
    status TEXT NOT NULL,
    created_at TEXT NOT NULL,
    FOREIGN KEY (customer_id) REFERENCES customers(id)
);

CREATE TABLE IF NOT EXISTS conversation_audit (
    id INTEGER PRIMARY KEY,
    created_at TEXT NOT NULL,
    message TEXT NOT NULL,
    intent TEXT NOT NULL,
    customer_need TEXT NOT NULL,
    details_mentioned_json TEXT NOT NULL,
    missing_information_json TEXT NOT NULL,
    sentiment TEXT NOT NULL,
    urgency TEXT NOT NULL,
    requires_business_data INTEGER NOT NULL,
    requires_human INTEGER NOT NULL,
    response TEXT NOT NULL
);
"""

PRODUCTS = [
    ("R-100", "Road Runner", "black", 8999),
    ("T-200", "Trail Pro", "blue", 11999),
    ("R-300", "Sprint Lite", "black", 10999),
]

INVENTORY = [
    ("R-100", "9", 2),
    ("R-100", "10", 4),
    ("T-200", "9", 3),
    ("T-200", "10", 0),
    ("R-300", "10", 1),
]


def get_connection() -> sqlite3.Connection:
    connection = sqlite3.connect(DB_FILE)
    connection.row_factory = sqlite3.Row
    connection.execute("PRAGMA foreign_keys = ON")
    return connection


def initialize_database() -> None:
    connection = get_connection()

    try:
        connection.executescript(SCHEMA)
        connection.executemany(
            """
            INSERT OR IGNORE INTO products (sku, name, color, price_cents)
            VALUES (?, ?, ?, ?)
            """,
            PRODUCTS,
        )

        product_rows = connection.execute(
            "SELECT id, sku FROM products"
        ).fetchall()
        product_ids = {row["sku"]: row["id"] for row in product_rows}
        inventory_rows = [
            (product_ids[sku], size, quantity)
            for sku, size, quantity in INVENTORY
        ]
        connection.executemany(
            """
            INSERT OR IGNORE INTO inventory (product_id, size, quantity)
            VALUES (?, ?, ?)
            """,
            inventory_rows,
        )

        created_at = datetime.now(timezone.utc).isoformat()
        connection.execute(
            """
            INSERT OR IGNORE INTO customers (name, email, created_at)
            VALUES (?, ?, ?)
            """,
            ("Alex Morgan", "alex@example.com", created_at),
        )
        customer = connection.execute(
            "SELECT id FROM customers WHERE email = ?",
            ("alex@example.com",),
        ).fetchone()

        if customer is not None:
            connection.execute(
                """
                INSERT OR IGNORE INTO orders
                    (external_ref, customer_id, status, created_at)
                VALUES (?, ?, ?, ?)
                """,
                ("ORDER-1001", customer["id"], "shipped", created_at),
            )

        connection.commit()
    finally:
        connection.close()


def save_analysis(message: str, analysis: CustomerAnalysis) -> None:
    connection = get_connection()

    try:
        connection.execute(
            """
            INSERT INTO conversation_audit (
                created_at,
                message,
                intent,
                customer_need,
                details_mentioned_json,
                missing_information_json,
                sentiment,
                urgency,
                requires_business_data,
                requires_human,
                response
            )
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
            """,
            (
                datetime.now(timezone.utc).isoformat(),
                message,
                analysis.intent,
                analysis.customer_need,
                json.dumps(analysis.details_mentioned, ensure_ascii=False),
                json.dumps(analysis.missing_information, ensure_ascii=False),
                analysis.sentiment,
                analysis.urgency,
                int(analysis.requires_business_data),
                int(analysis.requires_human),
                analysis.response,
            ),
        )
        connection.commit()
    finally:
        connection.close()


def get_summary() -> dict[str, int]:
    connection = get_connection()

    try:
        return {
            "products": connection.execute(
                "SELECT COUNT(*) AS total FROM products"
            ).fetchone()["total"],
            "inventory": connection.execute(
                "SELECT COUNT(*) AS total FROM inventory"
            ).fetchone()["total"],
            "customers": connection.execute(
                "SELECT COUNT(*) AS total FROM customers"
            ).fetchone()["total"],
            "orders": connection.execute(
                "SELECT COUNT(*) AS total FROM orders"
            ).fetchone()["total"],
            "audit": connection.execute(
                "SELECT COUNT(*) AS total FROM conversation_audit"
            ).fetchone()["total"],
        }
    finally:
        connection.close()


def get_recent_audits() -> list[sqlite3.Row]:
    connection = get_connection()

    try:
        return connection.execute(
            """
            SELECT created_at, intent, message
            FROM conversation_audit
            ORDER BY id DESC
            LIMIT 3
            """
        ).fetchall()
    finally:
        connection.close()


def main() -> None:
    initialize_database()
    summary = get_summary()

    print(f"Products: {summary['products']}")
    print(f"Inventory rows: {summary['inventory']}")
    print(f"Customers: {summary['customers']}")
    print(f"Orders: {summary['orders']}")
    print(f"Audit records: {summary['audit']}")

    for row in get_recent_audits():
        print(f"Recent audit: [{row['intent']}] {row['message']}")


if __name__ == "__main__":
    main()

What changed in this file?

  • The save_analysis() function serializes both list fields before inserting one complete audit row.
  • Every changing value uses a ? placeholder. Customer content never becomes part of the SQL statement itself.
  • The get_recent_audits() function returns the latest three messages in reverse insertion order.
  • The updated main() prints those recent rows after the summary.

The database script can now load the new functions without changing any seeded records. A clean run proves that the file remains valid before the sales brain starts writing audits.

  • Save database.py.
  • Check the updated database module by running this command:
python database.py

What does this check prove?

This command initializes the existing tables before reading their counts. It also imports the new audit functions, so a syntax or import problem surfaces immediately.

You should still see three products, five inventory rows, one customer, one order, and zero audit records. No recent audit lines appear because the sales brain has not saved a conversation yet.

Database script not running?

  • Check that import json appears above import sqlite3.
  • Check that the CustomerAnalysis import appears below the standard-library imports.
  • Compare the indentation inside save_analysis() with the full-file reference.

Ask for help with the exact terminal output: Help me debug my updated database.py file.

Turn the sales brain into a resilient loop

The temporary entry point stops after one message. A working terminal assistant needs to accept repeated input without restarting the Python process.

The loop also separates successful analyses from failed requests. Only validated output reaches save_analysis().

  • In sales_brain.py, replace the import section with this code:
import os

from agents import Agent, Runner

from analysis_schema import CustomerAnalysis
from database import initialize_database, save_analysis

What do these imports add?

  • The os module lets the program confirm that the current terminal still has the API key.
  • The database imports prepare the tables before the loop starts. They also expose the function that stores each successful analysis.
  • In sales_brain.py, replace the existing main() function with this version:
def main() -> None:
    if not os.getenv("OPENAI_API_KEY"):
        print("Missing OPENAI_API_KEY. Export it in this terminal and try again.")
        return

    initialize_database()
    print("Sales Brain is ready. Type quit to stop.")

    while True:
        message = input("\nCustomer: ").strip()

        if message.lower() in {"quit", "exit"}:
            print("Sales Brain stopped.")
            break

        if not message:
            print("Please enter a customer message.")
            continue

        try:
            analysis = analyze_message(message)
            print("\nAnalysis:")
            print(analysis.model_dump_json(indent=2))
            save_analysis(message, analysis)
            print("Saved to sales_brain.db.")
        except Exception as error:
            print(f"Request failed: {error}")

How does the loop behave?

  • The API key check stops before any request when the current terminal lacks the required credential.
  • The database initialization makes the tables available before the first customer message.
  • A blank message returns to the prompt without making an API request.
  • The try block prints valid JSON before saving the matching audit row.
  • The exception handler reports a failed request without inserting incomplete data.
  • Save sales_brain.py.
  • Compare the completed file with the full reference below.

✔️ Awesome, I've got everything!

Your loop is ready to accept repeated customer messages and persist each successful analysis.

ⓧ I'd like to double check the full code

Compare your complete sales_brain.py with this reference.

import os

from agents import Agent, Runner

from analysis_schema import CustomerAnalysis
from database import initialize_database, save_analysis


sales_agent = Agent(
    name="Sales Brain",
    instructions=(
        "Analyze one customer message for a retail sales team. "
        "Return every field in the requested structured output. "
        "Never invent prices, stock availability, policies, product specifications, "
        "order status, or customer records. "
        "Set requires_business_data to true when the answer depends on price, stock, "
        "policy, product, order, or customer data that is not present in the message. "
        "Set requires_human to true for complaints, refund requests, payment problems, "
        "legal threats, safety issues, or an explicit request for a person. "
        "List the important details already mentioned and any information still needed. "
        "Write a concise response directly to the customer. If business data is required, "
        "say you need to check it rather than guessing."
    ),
    model="gpt-6-luna",
    output_type=CustomerAnalysis,
)


def analyze_message(message: str) -> CustomerAnalysis:
    result = Runner.run_sync(sales_agent, message)
    analysis = result.final_output

    if not isinstance(analysis, CustomerAnalysis):
        raise TypeError("The agent did not return CustomerAnalysis.")

    return analysis


def main() -> None:
    if not os.getenv("OPENAI_API_KEY"):
        print("Missing OPENAI_API_KEY. Export it in this terminal and try again.")
        return

    initialize_database()
    print("Sales Brain is ready. Type quit to stop.")

    while True:
        message = input("\nCustomer: ").strip()

        if message.lower() in {"quit", "exit"}:
            print("Sales Brain stopped.")
            break

        if not message:
            print("Please enter a customer message.")
            continue

        try:
            analysis = analyze_message(message)
            print("\nAnalysis:")
            print(analysis.model_dump_json(indent=2))
            save_analysis(message, analysis)
            print("Saved to sales_brain.db.")
        except Exception as error:
            print(f"Request failed: {error}")


if __name__ == "__main__":
    main()

What should match?

The existing agent configuration and analyze_message() function stay unchanged. The new imports and replacement main() function create the persistent message loop.

Process two messages and verify persistence

This test uses API credit

Each valid customer message makes one API request. This test sends two messages before stopping the program.

Before you start the loop, do you think both successful analyses will still exist after the Python process stops?

  • Start the completed sales brain by running this command:
python sales_brain.py

What does this command start?

The script checks the current terminal for the API key before initializing the database. It then keeps the customer prompt active until you enter a stop command.

You should see Sales Brain is ready. Type quit to stop. followed by the customer prompt.

  • Press Enter without typing a message.

You should see Please enter a customer message. followed by a fresh prompt. No API request is made for the blank input.

  • Enter I want black running shoes in size 10 at the customer prompt.

You should see a validated analysis in JSON. The output ends with Saved to sales_brain.db..

  • Enter My order arrived damaged and I want a refund at the next customer prompt.

You should see another validated analysis. Its output also ends with Saved to sales_brain.db..

  • Enter quit at the next customer prompt.

You should see Sales Brain stopped. before the process returns to your terminal.

Message not saved?

  • Check that the terminal prints a validated analysis before save_analysis() runs.
  • Check that save_analysis is imported from database.
  • Check that the current terminal still has the API key from the earlier setup.

Share the failure text without sharing your API key: Help me debug why sales_brain.py did not save an analysis.

Before you inspect the database, what audit count do you expect after those two successful messages?

  • Read the persisted database summary by running this command:
python database.py

What does the final check read?

This starts a separate Python process and reopens sales_brain.db. Any audit rows it prints came from committed data on disk.

You should see Audit records: 2. You should also see two recent audit lines containing the submitted messages and their intents.

That completes the connection between reasoning and memory. Your validated customer analyses now survive after the sales brain stops.

Secret mission

Query Product and Inventory Knowledge

Turn your stored business facts into a safe product search. You will join products with inventory to return in-stock matches for a shopper's color, size, and budget without sending an API request.

Clean Up Your Resources

Clean Up Your Resources

Your project files and SQLite database stay on your Mac, so they create no ongoing infrastructure cost. Decide whether to keep them, pause your work, or delete them entirely.

Cost warning

Each new customer message sent through sales_brain.py makes a usage-based OpenAI API request. The database summary and product search run locally without API calls.

Deleting the project folder does not reverse charges for requests already made.

Resources you used:

  • The ai-sales-brain folder containing your Python files and requirements.txt.
  • The .venv virtual environment inside ai-sales-brain.
  • The sales_brain.db database containing seeded business data and saved conversation audits.
  • The current terminal session holding OPENAI_API_KEY.

Keep everything running

No action needed. Choose this if you plan to continue building with the sales brain and its existing business memory.

  • Keep the ai-sales-brain folder in its current location.
  • Retain sales_brain.db so its business memory remains available.
  • Keep the current terminal open only while you are actively testing the sales brain.
  • Type quit at the Customer: prompt when you finish a test session.

Pause - I'll come back to this later

Shut down your active session to free terminal resources. Your complete project remains on disk.

  • Type quit at the Customer: prompt if sales_brain.py is still running.
  • Enter deactivate in the current terminal to leave .venv.
  • Close the current terminal window to clear OPENAI_API_KEY from that session.
  • Leave the ai-sales-brain folder in place for your next session.

Your project remains on disk. You can resume later with the same database history.

Delete - I don't want to use this again

Remove all local project resources to start fresh. Previous API usage remains on your OpenAI account.

Deleting the folder is permanent. Take a moment to confirm that you no longer need the saved conversation history.

  • Type quit at the Customer: prompt if sales_brain.py is still running.
  • Enter deactivate in the current terminal to leave .venv.
  • Close the current terminal window to clear OPENAI_API_KEY from that session.
  • In Finder, locate the existing ai-sales-brain folder from earlier.
  • Delete the ai-sales-brain folder to remove its source code, virtual environment, and database.
  • Empty your Mac's Trash to reclaim the disk space.
  • Search Finder for ai-sales-brain to verify the folder was removed.

You should no longer find ai-sales-brain on your Mac. That confirms the entire local project was removed.

Cleanup complete. The terminal session no longer holds your API key.

Nice Work!

Nice Work!

You did it! Your local sales brain now combines validated AI analysis with durable business data. It can process repeated customer messages while keeping a trustworthy record of every successful analysis.

Here's what you learned:

  • Created a validated analysis contract with Pydantic. The OpenAI Agents SDK now returns predictable JSON fields for every customer message.
  • Built a persistent business memory in SQLite. Its constrained tables store products, inventory, customers, orders, and conversation audits.
  • Connected the analysis loop to sales_brain.db. Each successful analysis now survives restarts and appears in the audit summary.
  • Completed the optional Secret Mission by building a parameterized product search. It filters joined product and inventory rows by color, size, budget, and positive stock.

Ready to quiz yourself?