Week 15 — Real-Time AI Apps with ChatGPT API

🧠 Phase 4 — Deep Learning & GenAI (Weeks 11–15) · Hope AI — ML & DS Course

🎯 TL;DR — Real-time AI apps = streaming API + persistent conversation history + smart context management. Use Streamlit for rapid UI, FastAPI for production backends, WebSockets or SSE for streaming, Whisper for voice, DALL-E for images. Key challenge: managing token limits as history grows.

🧠 Mental Model

Think of a real-time AI app as a live phone call with a goldfish — the model forgets everything between calls, so you must hand it the transcript each time. Streaming is like hearing words as they're spoken (not waiting for a full sentence). Context window management is like summarising old parts of the transcript so the call stays within the phone's memory limit.


📋 Core Concepts — Quick Reference Table

ConceptWhat It IsKey Detail
StreamingTokens returned incrementally as generatedstream=True / .stream() context manager
SSEServer-Sent Events — unidirectional server→client pushtext/event-stream content type
WebSocketBidirectional real-time channelBetter for voice; more complex
Conversation historyList of all messages sent per sessionStored server-side, keyed by session_id
Context windowMax tokens model can process at oncegpt-4o: 128K, claude-opus-4-5: 200K
Sliding windowKeep only last N messages when limit approachesLoses early context
SummarisationCompress old messages into a summaryPreserves key info from early turns
Streamlit chat_messageBuilt-in chat UI componentst.chat_message("user") / "assistant"
Whisper APIOpenAI speech-to-textclient.audio.transcriptions.create()
Vision APISend images alongside textBase64 encode or URL
DALL-EText-to-image generationclient.images.generate()

🔢 Key Steps / Process

  • Choose UI layer — Streamlit (rapid prototype) or FastAPI + frontend (production)
  • Initialise session state — store messages list across rerenders (st.session_state)
  • Capture user input — text via st.chat_input(), voice via Whisper, image via file uploader
  • Append user turn — add {role: "user", content: ...} to history
  • Stream API response — iterate over chunks, display incrementally
  • Append assistant turn — save model reply to history for next turn
  • Manage context — check token count; summarise or slide window when approaching limit
  • Optimise costs — cache repeated queries, compress images, choose smaller model for non-critical paths

💻 Code Cheatsheet

# ============================================================
# 1. STREAMLIT CHAT APP WITH STREAMING (Anthropic — Primary)
# pip install streamlit anthropic
# Run: streamlit run app.py
# ============================================================
import streamlit as st
import anthropic

st.title("💬 Claude Chat")

client = anthropic.Anthropic()           # Reads ANTHROPIC_API_KEY from env

# Persist conversation across rerenders
if "messages" not in st.session_state:
    st.session_state.messages = []

# Display existing conversation
for msg in st.session_state.messages:
    with st.chat_message(msg["role"]):
        st.markdown(msg["content"])

# Get new user input
if prompt := st.chat_input("Message Claude..."):
    # Show user message
    st.chat_message("user").markdown(prompt)
    st.session_state.messages.append({"role": "user", "content": prompt})

    # Stream Claude's response
    with st.chat_message("assistant"):
        response_placeholder = st.empty()
        full_response = ""

        with client.messages.stream(
            model="claude-opus-4-5",
            max_tokens=1024,
            system="You are a helpful AI assistant.",
            messages=st.session_state.messages   # Full history every call
        ) as stream:
            for text in stream.text_stream:
                full_response += text
                response_placeholder.markdown(full_response + "▌")  # Cursor effect

        response_placeholder.markdown(full_response)

    st.session_state.messages.append({"role": "assistant", "content": full_response})


# ============================================================
# 2. CONTEXT WINDOW MANAGEMENT
# ============================================================
import anthropic

def count_tokens_in_history(messages: list, model: str = "claude-opus-4-5") -> int:
    """Approximate token count for conversation history."""
    client = anthropic.Anthropic()
    # Use the token counting API
    response = client.messages.count_tokens(
        model=model,
        messages=messages
    )
    return response.input_tokens

def manage_context(messages: list, max_tokens: int = 150_000) -> list:
    """Sliding window: drop oldest messages when approaching limit."""
    while count_tokens_in_history(messages) > max_tokens and len(messages) > 2:
        messages.pop(0)              # Remove oldest user message
        if messages and messages[0]["role"] == "assistant":
            messages.pop(0)          # Remove its paired assistant reply
    return messages

def summarise_old_context(messages: list, keep_recent: int = 6) -> list:
    """Summarise early messages to preserve context without tokens."""
    client = anthropic.Anthropic()
    if len(messages) <= keep_recent:
        return messages

    old_messages = messages[:-keep_recent]
    recent_messages = messages[-keep_recent:]

    # Ask Claude to summarise the old context
    summary_resp = client.messages.create(
        model="claude-opus-4-5",
        max_tokens=512,
        messages=[
            {"role": "user", "content": f"Summarise this conversation in 3 sentences:\n\n{old_messages}"}
        ]
    )
    summary = summary_resp.content[0].text

    # Inject summary as a system-style user message
    return [
        {"role": "user", "content": f"[Earlier conversation summary]: {summary}"},
        {"role": "assistant", "content": "Understood, I'll keep that context in mind."},
        *recent_messages
    ]


# ============================================================
# 3. FASTAPI STREAMING BACKEND (SSE)
# pip install fastapi uvicorn anthropic
# Run: uvicorn main:app --reload
# ============================================================
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
import anthropic, json
from uuid import uuid4

app = FastAPI(title="Real-Time AI Chat API")
client = anthropic.Anthropic()

# In-memory session store (use Redis in production)
sessions: dict[str, list] = {}

class ChatRequest(BaseModel):
    message: str
    session_id: str | None = None

@app.post("/chat/stream")
async def chat_stream(req: ChatRequest):
    """Streaming SSE endpoint — tokens arrive as they generate."""
    session_id = req.session_id or str(uuid4())

    if session_id not in sessions:
        sessions[session_id] = []

    sessions[session_id].append({"role": "user", "content": req.message})

    def generate():
        full_reply = ""
        # Stream from Anthropic
        with client.messages.stream(
            model="claude-opus-4-5",
            max_tokens=1024,
            system="You are a helpful assistant.",
            messages=sessions[session_id]
        ) as stream:
            for text in stream.text_stream:
                full_reply += text
                # SSE format: "data: <json>\n\n"
                yield f"data: {json.dumps({'text': text})}\n\n"

        # Save assistant reply to history
        sessions[session_id].append({"role": "assistant", "content": full_reply})
        yield f"data: {json.dumps({'done': True, 'session_id': session_id})}\n\n"

    return StreamingResponse(generate(), media_type="text/event-stream")

@app.get("/history/{session_id}")
async def get_history(session_id: str):
    return {"messages": sessions.get(session_id, [])}


# ============================================================
# 4. VOICE INPUT WITH WHISPER + VISION API
# pip install openai streamlit
# ============================================================
import openai
import base64

oai_client = openai.OpenAI()

def transcribe_audio(audio_file_path: str) -> str:
    """Convert speech to text using Whisper."""
    with open(audio_file_path, "rb") as audio:
        transcript = oai_client.audio.transcriptions.create(
            model="whisper-1",
            file=audio,
            language="en"                    # Specify language for accuracy
        )
    return transcript.text

def analyse_image(image_path: str, question: str) -> str:
    """Send image + text question to GPT-4o Vision."""
    with open(image_path, "rb") as f:
        image_data = base64.b64encode(f.read()).decode("utf-8")

    resp = oai_client.chat.completions.create(
        model="gpt-4o",
        messages=[{
            "role": "user",
            "content": [
                {"type": "text", "text": question},
                {"type": "image_url", "image_url": {
                    "url": f"data:image/jpeg;base64,{image_data}",
                    "detail": "high"         # "low" = cheaper, "high" = better for charts/text
                }}
            ]
        }]
    )
    return resp.choices[0].message.content

def generate_image(prompt: str, size: str = "1024x1024") -> str:
    """Generate image with DALL-E 3. Returns URL."""
    response = oai_client.images.generate(
        model="dall-e-3",
        prompt=prompt,
        size=size,                           # "1024x1024" | "1792x1024" | "1024x1792"
        quality="standard",                  # "standard" | "hd"
        n=1                                  # DALL-E 3 only supports n=1
    )
    return response.data[0].url


# ============================================================
# 5. ASYNC BATCH PROCESSING (parallel API calls)
# ============================================================
import asyncio
import anthropic

async def process_single(client: anthropic.AsyncAnthropic, text: str) -> str:
    """Process one document asynchronously."""
    resp = await client.messages.create(
        model="claude-opus-4-5",
        max_tokens=256,
        messages=[{"role": "user", "content": f"Summarise in 2 sentences: {text}"}]
    )
    return resp.content[0].text

async def batch_process(texts: list[str]) -> list[str]:
    """Process all texts in parallel — much faster than sequential."""
    async_client = anthropic.AsyncAnthropic()
    tasks = [process_single(async_client, text) for text in texts]
    return await asyncio.gather(*tasks)      # All API calls fire simultaneously

# Run 10 summarisations in parallel
documents = [f"Document {i}: Long article content here..." for i in range(10)]
summaries = asyncio.run(batch_process(documents))
print(f"Processed {len(summaries)} documents in parallel")

⚙️ Key Parameters / Configuration Table

ParameterTool/APIValueEffect
stream=TrueOpenAIbooleanReturn tokens incrementally
.stream() context managerAnthropicIterator over text_stream
media_type="text/event-stream"FastAPIstringSSE response type
detail="high"/"low"VisionstringImage analysis accuracy vs cost
language="en"WhisperstringForce language for accuracy
quality="hd"DALL-E 3stringHigher quality image
size="1792x1024"DALL-E 3stringLandscape format
k=5 (keep_recent)Context mgmtintNumber of recent turns to preserve
max_tokens=150_000Context limitintWhen to trigger summarisation

🎤 Top Interview Q&A

Q1: How does streaming work with Server-Sent Events?

A: The server keeps an HTTP connection open and pushes chunks prefixed with data: . Each chunk is a JSON object with a text fragment. The client listens with EventSource in JS or reads the stream in Python. FastAPI's StreamingResponse with media_type="text/event-stream" handles this natively.

Q2: How do you maintain conversation history in a stateless HTTP API?

A: Assign each user a session_id (UUID). Store their messages list server-side (Redis or DB in production, dict for development). Every new message appends to the session's list and sends the full list to the API.

Q3: What happens when conversation history exceeds the context window?

A: The API throws a context_length_exceeded error. Solutions: (1) Sliding window — drop oldest messages. (2) Summarisation — ask the model to compress old turns into a paragraph and inject as a system message. (3) Use a model with larger context window (claude-opus-4-5 = 200K tokens).

Q4: When should you use WebSockets vs SSE for streaming?

A: SSE is simpler and sufficient for one-way streaming (server→client) — perfect for chat. Use WebSockets when you need true bidirectional, low-latency communication (e.g., voice calls, collaborative editing, multiplayer apps).

Q5: How do you handle image uploads in a chat app?

A: Accept the file, base64-encode it, and embed it in the messages array as image_url content block. For large images, resize first to reduce tokens and cost. Use detail="low" for visual descriptions, detail="high" for reading text in images.

Q6: What is the cheapest way to scale a streaming chat app?

A: (1) Use gpt-4o-mini or claude-haiku for simple turns. (2) Cache repeated queries with exact-match or semantic cache. (3) Limit max_tokens aggressively — most replies need <300 tokens. (4) Implement async batch processing for bulk operations.

Q7: How does Whisper handle different languages?

A: Whisper is multilingual by default — it auto-detects language. Specify language="ta" for Tamil, language="en" for English etc. to improve accuracy and speed. It supports 100+ languages with varying accuracy.


⚠️ Common Mistakes

  • Re-sending messages without full history — model loses context; always append and send the entire messages list
  • No session_id management — all users share one history; always key sessions by UUID
  • Blocking the event loop — use AsyncAnthropic / AsyncOpenAI for async frameworks, not the sync client
  • Not streaming for long responses — users wait 10+ seconds for a blank box; stream always for chat UIs
  • Ignoring context limits — no sliding window means the app crashes when conversations get long
  • Storing large images in session history — base64 images inflate history; store URL references instead
  • Not compressing audio before Whisper — Whisper has a 25MB file limit; convert to MP3/OPUS first

🚀 Quick Reference — When to Use What

NeedToolNotes
Quick chat prototypeStreamlit + st.chat_input30 lines, instant deploy
Production backendFastAPI + SSEScalable, session management
Bidirectional streamingWebSocketsMore complex, needed for voice
Speech inputOpenAI Whisperaudio.transcriptions.create()
Image analysisGPT-4o VisionBase64 or URL, detail param
Image generationDALL-E 3Best quality; n=1 only
Parallel batch jobsasyncio.gather + AsyncAnthropic10x faster than sequential
Long sessionsSummarisation strategyPreserves context without token bloat

📋 Completion Checklist

  • Build a Streamlit chat app with streaming and st.session_state history
  • Implement SSE streaming endpoint with FastAPI
  • Manage multi-turn conversation history keyed by session_id
  • Handle context window overflow with sliding window or summarisation
  • Add voice input using Whisper API
  • Analyse images using GPT-4o Vision with base64 encoding
  • Generate images with DALL-E 3
  • Use AsyncAnthropic with asyncio.gather for parallel batch processing