Course EN
← back to chapter

Live explainer

Ask any question. The model's answer streams back token by token —

Live explainer

Ask any question. The model's answer streams back token by token — the same typewriter UX that makes chat apps feel responsive even when the total latency is similar to a one-shot call.

Run

bash bootstrap-secrets.sh              # reads ../../../../.env, writes secrets/
docker compose up --build              # default: PROVIDER=openai

Open http://localhost:3000.

To run against Gemini instead:

PROVIDER=gemini docker compose up --build

What's where

  • backend/ai_openai.pyrun_stream yields tokens, run accumulates
  • backend/ai_gemini.py — same shape, other SDK
  • backend/main.py — FastAPI: /api/ai (final) + /api/ai/stream (SSE)
  • frontend/app/page.tsx — reads the SSE stream and types output as it arrives

Stop

docker compose down

Ejecútalo en tu máquina

Descarga el proyecto como ZIP y córrelo con Docker. Levanta un backend FastAPI y un frontend Next.js en localhost:3000.

Descargar live-explainer.zip

unzip live-explainer.zip
cd live-explainer
bash bootstrap-secrets.sh   # one-time: pulls API keys into ./secrets
docker compose up --build   # default provider: openai
# or:  PROVIDER=gemini docker compose up --build

Escribe algo, elige un proveedor y ejecuta el mismo código de Código contra la API real. Requiere iniciar sesión.


  

Los mismos módulos que ejecuta el botón Run. El proyecto completo (frontend, Dockerfile, compose) está en el ZIP, pestaña README.

backend/ai_openai.py

"""Week 3 - Showcase 1 (OpenAI): live answer to a question, streamed.

The streaming pattern itself is the lesson — `run_stream` yields tokens
as they arrive from the server, which the FastAPI app forwards over SSE
to the browser so the reader watches the answer take shape. `run` is a
thin accumulator over `run_stream`, kept so the website's generic Try-it
widget can call a `def run(input: str) -> str` and get the final text.
"""
from typing import Iterator

from openai import OpenAI

_client = OpenAI()  # reads OPENAI_API_KEY from the environment.


def _prompt(question: str) -> str:
    return (
        "Answer the following question in two to four short paragraphs. "
        "Use plain prose only; no headings, no bullet lists, no code blocks."
        f"\n\n---\n{question}\n---"
    )


def run_stream(question: str) -> Iterator[str]:
    # Cap output length so a runaway answer can't burn the bill.
    # 2048 tokens (~6k chars) is well over the prompt's 2-4 paragraphs.
    stream = _client.responses.create(
        model="gpt-5.4-nano", input=_prompt(question), stream=True,
        max_output_tokens=2048,
    )
    for event in stream:
        if event.type == "response.output_text.delta":
            yield event.delta


def run(question: str) -> str:
    return "".join(run_stream(question))

backend/ai_gemini.py

"""Week 3 - Showcase 1 (Gemini): live answer to a question, streamed.

Same lesson as ai_openai.py — `run_stream` yields tokens as they arrive
from the Gemini server, `run` accumulates for the non-streaming contract.
"""
import os
from typing import Iterator

from google import genai
from google.genai import types

_client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])


def _prompt(question: str) -> str:
    return (
        "Answer the following question in two to four short paragraphs. "
        "Use plain prose only; no headings, no bullet lists, no code blocks."
        f"\n\n---\n{question}\n---"
    )


def run_stream(question: str) -> Iterator[str]:
    # Cap output length so a runaway answer can't burn the bill.
    # 2048 tokens (~6k chars) is well over the prompt's 2-4 paragraphs.
    stream = _client.models.generate_content_stream(
        model="gemini-3.1-flash-lite", contents=_prompt(question),
        config=types.GenerateContentConfig(max_output_tokens=2048),
    )
    for chunk in stream:
        if chunk.text:
            yield chunk.text


def run(question: str) -> str:
    return "".join(run_stream(question))

Archivos del proyecto

  • .gitignore
  • README.md
  • backend/Dockerfile
  • backend/ai_gemini.py
  • backend/ai_openai.py
  • backend/main.py
  • backend/requirements.txt
  • bootstrap-secrets.sh
  • docker-compose.yml
  • frontend/Dockerfile
  • frontend/app/layout.tsx
  • frontend/app/page.tsx
  • frontend/next.config.ts
  • frontend/package.json
  • frontend/tsconfig.json