Backend Step 5: OpenAI Integration

Replace the hardcoded bot reply with a real AI response using OpenAI.

What You'll Learn

  • Loading environment variables with python-dotenv
  • Calling the OpenAI Chat Completions API
  • Handling API errors
  • Streaming responses (optional)

Step 1: Get an OpenAI API Key

  1. Go to https://platform.openai.com/api-keys
  2. Sign up or log in
  3. Click "Create new secret key"
  4. Copy the key (starts with sk-...)

Step 2: Set Up the .env File

cd aiproject/backend
cp app/.env.example app/.env

Edit app/.env and replace with your actual key:

OPENAI_API_KEY=sk-proj-your-actual-key-here

If using Docker: The docker-compose.yml automatically loads this .env file into the container. No extra setup needed.

Step 3: Add .env to .gitignore

echo ".env" >> .gitignore

This ensures you never accidentally commit your secret key.

Step 4: Load Environment Variables

Update backend/app/__init__.py:

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from dotenv import load_dotenv

load_dotenv()

db = SQLAlchemy()


def create_app():
    app = Flask(__name__)
    app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///chat.db"
    app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False

    db.init_app(app)

    from .routes import api
    app.register_blueprint(api, url_prefix="/api")

    with app.app_context():
        db.create_all()

    return app


if __name__ == "__main__":
    app = create_app()
    app.run(debug=True, port=5000)

The load_dotenv() call reads the .env file and makes its contents available via os.getenv().

Step 5: Update routes.py with OpenAI

from flask import Blueprint, request, jsonify
from . import db
from .models import Message
from openai import OpenAI
import os

api = Blueprint("api", __name__)

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


@api.route("/status")
def status():
    return jsonify({"status": "ok"})


@api.route("/messages", methods=["GET"])
def get_messages():
    messages = Message.query.order_by(Message.timestamp.asc()).all()
    return jsonify([msg.to_dict() for msg in messages])


@api.route("/chat", methods=["POST"])
def chat():
    data = request.get_json()
    user_message = data.get("message", "")

    if not user_message:
        return jsonify({"error": "Message is required"}), 400

    user_msg = Message(content=user_message, sender="user")
    db.session.add(user_msg)
    db.session.commit()

    try:
        response = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[
                {
                    "role": "system",
                    "content": "You are a helpful assistant.",
                },
                {"role": "user", "content": user_message},
            ],
        )

        ai_reply = response.choices[0].message.content
        ai_msg = Message(content=ai_reply, sender="ai")
        db.session.add(ai_msg)
        db.session.commit()

        return jsonify({
            "user_message": user_msg.to_dict(),
            "ai_message": ai_msg.to_dict(),
        })

    except Exception as e:
        return jsonify({"error": str(e)}), 500

Step 6: Test It

cd aiproject/backend
source venv/bin/activate
python app/__init__.py

In another terminal:

curl -X POST http://127.0.0.1:5000/api/chat \
  -H "Content-Type: application/json" \
  -d '{"message": "What is the capital of France?"}'

Expected response (actual AI reply):

{
  "user_message": {"id": 1, "content": "What is the capital of France?", "sender": "user", ...},
  "ai_message": {"id": 2, "content": "The capital of France is Paris.", "sender": "ai", ...}
}

How the OpenAI Call Works

response = client.chat.completions.create(
    model="gpt-4o-mini",           # Which AI model to use
    messages=[                     # The conversation history
        {"role": "system", "content": "You are a helpful assistant."},
        {"role": "user", "content": user_message},
    ],
)
  • model: Which AI model to use. gpt-4o-mini is fast and cheap.
  • messages: The conversation context. The system message sets the AI's personality. The user message is the question.
  • response.choices[0].message.content: Extracts the AI's reply text.

Error Handling

The try/except block catches any API errors: - Invalid API key → authentication error - Rate limit → too many requests - Network error → can't reach OpenAI

If an error occurs, the API returns {"error": "..."} with status 500 instead of crashing.

Step 7: Mocking OpenAI in Tests

Your tests currently call the real OpenAI API. That wastes money and is slow. Instead, mock the API — replace it with a fake that returns a canned response.

This is a critical skill: never let tests depend on external services.

Update your test to mock OpenAI:

from unittest.mock import patch


@patch("app.routes.client.chat.completions.create")
def test_chat_with_mock_openai(mock_openai, client):
    mock_openai.return_value.choices = [
        MockChoice(MockMessage("Mock AI reply"))
    ]

    response = client.post("/api/chat", json={"message": "Hello"})
    assert response.status_code == 200
    data = response.get_json()
    assert data["ai_message"]["content"] == "Mock AI reply"

Add the helper classes at the top of your test file:

class MockMessage:
    def __init__(self, content):
        self.content = content


class MockChoice:
    def __init__(self, message):
        self.message = message

Now your tests run instantly and free — no API key needed, no network calls.

Verification

  • Send a question and get a real AI response
  • Send an empty message and get a 400 error
  • Check instance/chat.db to confirm messages are saved
  • Run pytest tests/ -v — tests should pass without a real API key