Course ES
← back to chapter

Document Q&A

The canonical RAG loop. Your question retrieves the top chunks from a product's

Showcase — Document Q&A

The canonical RAG loop. Your question retrieves the top chunks from a product's docs; the model answers only from them, cites the [id]s it used, and the retrieved chunk ids are echoed so you can check the citations against what was fetched. Ask "does the free plan include automations?" and it answers from the plans and automations chunks.

Run

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

Open http://localhost:3000. Gemini: PROVIDER=gemini docker compose up --build.

What's where

  • backend/store.py — the shared VectorStore (retrieval).
  • backend/ai_openai.py / backend/ai_gemini.py — the KB, the retrieval, the grounded prompt, and the generation call.
  • frontend/app/page.tsx — question box + cited answer.

Stop

docker compose down

Run locally

Download the project as a ZIP and run it with Docker. Brings up a FastAPI backend + Next.js frontend on localhost:3000.

Download doc-qa.zip

unzip doc-qa.zip
cd doc-qa
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

Type some input, pick a provider, and run the same code shown in Source against the live API. Sign-in required.


  

The same modules the Run button hits. The whole project (frontend, Dockerfile, compose) is in the ZIP under README.

backend/ai_openai.py

"""Showcase 1 (OpenAI): document Q&A — the canonical RAG loop.

Retrieve the top chunks for a question, paste them in as numbered context, and
make the model answer only from them, citing the [id]s it used. The retrieved
ids are echoed at the bottom so you can check the citations against what was
actually fetched.
"""
from openai import OpenAI

from store import VectorStore

_client = OpenAI()

_EMBED_MODEL = "text-embedding-3-small"
_CHAT_MODEL = "gpt-5.4-nano"

_KB = [
    {"id": "plans", "text": "TaskFlow Free includes 3 boards. Pro is $10/month for unlimited boards and Gantt charts."},
    {"id": "invite", "text": "Invite members from Team > Invite; they join as Editors and can be changed to Admin."},
    {"id": "automations", "text": "Automations (e.g. 'when a card moves to Done, notify the owner') are a Pro feature."},
    {"id": "export", "text": "Export any board to CSV from Board > Export. Attachments are not included in exports."},
    {"id": "api", "text": "The API is available on Pro, rate-limited to 120 requests/minute, keyed under Settings > API."},
    {"id": "mobile", "text": "The mobile app supports viewing and moving cards; automations must be edited on the web."},
]

_SYSTEM = (
    "You are TaskFlow's support assistant. Answer using ONLY the context below. "
    "Cite the sources you use by their [id]. If the context does not contain the "
    "answer, say 'I don't have that in the docs.' Never use outside knowledge.\n\n"
    "Context:\n{context}"
)

_store: VectorStore | None = None


def _embed(texts):
    return [item.embedding for item in _client.embeddings.create(model=_EMBED_MODEL, input=texts).data]


def _get_store() -> VectorStore:
    global _store
    if _store is None:
        _store = VectorStore(_embed)
        _store.add(_KB)
    return _store


def run(question: str) -> str:
    hits = _get_store().search(question.strip(), k=3)
    context = "\n".join(f"[{item['id']}] {item['text']}" for _, item in hits)
    response = _client.responses.create(
        model=_CHAT_MODEL,
        instructions=_SYSTEM.format(context=context),
        input=[{"role": "user", "content": question.strip()}],
    )
    used = ", ".join(item["id"] for _, item in hits)
    return f"{response.output_text}\n\n— retrieved chunks: {used}"

backend/ai_gemini.py

"""Showcase 1 (Gemini): document Q&A — the canonical RAG loop.

Same retrieval, same grounding rules, Gemini's embedding + chat models.
"""
import os

from google import genai
from google.genai import types

from store import VectorStore

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

_EMBED_MODEL = "gemini-embedding-001"
_CHAT_MODEL = "gemini-3.1-flash-lite"

_KB = [
    {"id": "plans", "text": "TaskFlow Free includes 3 boards. Pro is $10/month for unlimited boards and Gantt charts."},
    {"id": "invite", "text": "Invite members from Team > Invite; they join as Editors and can be changed to Admin."},
    {"id": "automations", "text": "Automations (e.g. 'when a card moves to Done, notify the owner') are a Pro feature."},
    {"id": "export", "text": "Export any board to CSV from Board > Export. Attachments are not included in exports."},
    {"id": "api", "text": "The API is available on Pro, rate-limited to 120 requests/minute, keyed under Settings > API."},
    {"id": "mobile", "text": "The mobile app supports viewing and moving cards; automations must be edited on the web."},
]

_SYSTEM = (
    "You are TaskFlow's support assistant. Answer using ONLY the context below. "
    "Cite the sources you use by their [id]. If the context does not contain the "
    "answer, say 'I don't have that in the docs.' Never use outside knowledge.\n\n"
    "Context:\n{context}"
)

_store: VectorStore | None = None


def _embed(texts):
    return [e.values for e in _client.models.embed_content(model=_EMBED_MODEL, contents=texts).embeddings]


def _get_store() -> VectorStore:
    global _store
    if _store is None:
        _store = VectorStore(_embed)
        _store.add(_KB)
    return _store


def run(question: str) -> str:
    hits = _get_store().search(question.strip(), k=3)
    context = "\n".join(f"[{item['id']}] {item['text']}" for _, item in hits)
    response = _client.models.generate_content(
        model=_CHAT_MODEL,
        contents=[types.Content(role="user", parts=[types.Part(text=question.strip())])],
        config=types.GenerateContentConfig(system_instruction=_SYSTEM.format(context=context)),
    )
    used = ", ".join(item["id"] for _, item in hits)
    return f"{response.text or ''}\n\n— retrieved chunks: {used}"

Project files

  • .gitignore
  • README.es.md
  • README.md
  • backend/Dockerfile
  • backend/ai_gemini.py
  • backend/ai_openai.py
  • backend/main.py
  • backend/requirements.txt
  • backend/store.py
  • 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