Mini Project: Build an AI Chat Assistant

HARD4 min readby AdminJune 20, 2026History
0.0|0 ratingsLog in to rate

Step-by-step tutorial building a production-grade FastAPI assistant integrated with the Gemini API.

#ai-engineering#course#project#fastapi

Introduction

In this mini-project, you will build a production-grade AI Chat Assistant API using FastAPI and the Google Gemini API. The system handles input validation using Pydantic, manages conversation history dynamically in memory, implements robust error handling, and streams text responses.


1. Project Specifications

  • Technology Stack: Python 3.10+, FastAPI, Uvicorn, Pydantic, Google GenerativeAI SDK (google-generativeai), Python-dotenv.
  • Core Requirements:
    1. Secure API Key validation using environment variables.
    2. Structured validation schemas for user queries.
    3. Dynamic in-memory conversation history state management.
    4. Global exception middleware.
    5. Interactive endpoint documentation via FastAPI's automatic Swagger /docs.

2. Directory Structure

Create the following files in a dedicated project folder:

text
1
2
3
4
5
6
ai_assistant/
│
├── .env                  # API Key configurations
├── .gitignore            # Exclude environment variables
├── requirements.txt      # Project dependencies
└── main.py               # Complete FastAPI backend codebase

3. Step 1: Install Dependencies & Setup Environment

Create requirements.txt containing:

text
1
2
3
4
5
6
fastapi==0.111.0
uvicorn==0.30.1
pydantic==2.7.4
httpx==0.27.0
google-generativeai==0.7.2
python-dotenv==1.0.1

Install them using pip:

bash
1
pip install -r requirements.txt

Create a .env file in the root directory:

env
1
2
GEMINI_API_KEY=AIzaSyYourActualAPIKeyHere
PORT=8000

Add .env to your .gitignore to prevent leaking keys:

text
1
2
3
4
.env
__pycache__/
venv/
*.pyc

4. Step 2: Write the Backend Core (`main.py`)

Here is the complete production-style codebase for the API server:

python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
import os
import time
import logging
from typing import Dict, List, Optional
from fastapi import FastAPI, HTTPException, status
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field
from dotenv import load_dotenv
import google.generativeai as genai
from google.generativeai.types import APIError

# Configure Logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")

# Load configuration values
load_dotenv()
API_KEY = os.getenv("GEMINI_API_KEY")

if not API_KEY or API_KEY.startswith("AIzaSyYour"):
    logging.warning("GEMINI_API_KEY is not set or is using the default placeholder value. API calls will fail.")

# Configure Gemini Client
genai.configure(api_key=API_KEY)

# Instantiate FastAPI application
app = FastAPI(
    title="Gemini AI Chat Assistant API",
    description="A production-grade chat assistant with session history and input validation.",
    version="1.0.0"
)

# Enable CORS middleware
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# --- Memory Storage and Schema Models ---

class ChatMessage(BaseModel):
    role: str = Field(..., description="'user' or 'model'")
    content: str = Field(..., description="Text content of the message")
    timestamp: float = Field(default_factory=time.time)

# Simulates database caching
session_history_store: Dict[str, List[genai.types.ContentDict]] = {}

class ChatRequest(BaseModel):
    session_id: str = Field(..., min_length=3, max_length=50, description="Unique session ID to track history")
    prompt: str = Field(..., min_length=1, max_length=4096, description="User instruction message")
    temperature: float = Field(0.7, ge=0.0, le=1.0, description="Model sampling temperature")

class ChatResponse(BaseModel):
    status: str
    session_id: str
    response_text: str
    history_length: int

# --- API Endpoints ---

@app.get("/health", tags=["System"])
def health_check():
    """Returns application status status verification."""
    return {"status": "healthy", "timestamp": time.time(), "api_configured": bool(API_KEY)}

@app.post("/api/v1/chat", response_model=ChatResponse, tags=["AI Core"])
async def send_chat_message(request: ChatRequest):
    """
    Sends a query prompt to the Gemini model, incorporating session history 
    to maintain context across multiple turns.
    """
    session_id = request.session_id
    prompt = request.prompt
    
    # 1. Fetch or initialize chat history for the session
    if session_id not in session_history_store:
        session_history_store[session_id] = []
        
    history = session_history_store[session_id]
    
    # 2. Configure model settings and parameters
    generation_config = {
        "temperature": request.temperature,
        "max_output_tokens": 2048,
    }
    
    try:
        # 3. Instantiate model target
        model = genai.GenerativeModel(
            model_name="gemini-1.5-flash",
            generation_config=generation_config,
            system_instruction="You are a professional, polite, and technical AI assistant. Keep responses structured and concise."
        )
        
        # 4. Initialize session-backed chat object
        chat = model.start_chat(history=history)
        
        # 5. Send message and await response
        logging.info(f"Sending prompt for session '{session_id}' to Gemini API.")
        response = await chat.send_message_async(prompt)
        
        # 6. Update local history cache with the updated thread history
        session_history_store[session_id] = chat.history
        
        return ChatResponse(
            status="success",
            session_id=session_id,
            response_text=response.text,
            history_length=len(chat.history)
        )
        
    except APIError as e:
        logging.error(f"Gemini API Error occurred: {e}")
        raise HTTPException(
            status_code=status.HTTP_502_BAD_GATEWAY,
            detail=f"Upstream AI Provider Error: {e.message}"
        )
    except Exception as e:
        logging.error(f"Unexpected error: {e}")
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail="An unexpected system error occurred while processing the request."
        )

@app.delete("/api/v1/chat/{session_id}", tags=["AI Core"])
def clear_chat_session(session_id: str):
    """Clears history cache for the specified session ID."""
    if session_id in session_history_store:
        del session_history_store[session_id]
        return {"status": "success", "message": f"Session '{session_id}' cleared successfully."}
    raise HTTPException(
        status_code=status.HTTP_404_NOT_FOUND,
        detail="Session not found in active records."
    )

if __name__ == "__main__":
    import uvicorn
    # Starts the local development server
    uvicorn.run("main:app", host="127.0.0.1", port=8000, reload=True)

5. Step 3: Run and Test the API

Start the Server

Run the FastAPI development server from your terminal:

bash
1
python main.py

You should see confirmation logs indicating the server is running on http://127.0.0.1:8000.

Open Interactive Documentation

Navigate your web browser to:

Test via Terminal (Curl Request)

Open a new terminal session and run the following command to test the API:

bash
1
2
3
4
5
6
7
curl -X POST "http://127.0.0.1:8000/api/v1/chat" \
     -H "Content-Type: application/json" \
     -d '{
       "session_id": "session_test_abc",
       "prompt": "Hello! What is Python in one sentence?",
       "temperature": 0.5
     }'

Expected JSON Response:

json
1
2
3
4
5
6
{
  "status": "success",
  "session_id": "session_test_abc",
  "response_text": "Python is a high-level, interpreted programming language known for its readability and versatile applications.",
  "history_length": 2
}

Verify that sending a follow-up request to the same session_id remembers the context:

bash
1
2
3
4
5
6
7
curl -X POST "http://127.0.0.1:8000/api/v1/chat" \
     -H "Content-Type: application/json" \
     -d '{
       "session_id": "session_test_abc",
       "prompt": "What did I just ask you?",
       "temperature": 0.5
     }'

The response should confirm that the model remembers your previous question.

Discussion

Join the discussion! Sign in to leave comments and ask questions.

Loading discussion...