Course EN
← back to chapter

Q&A de documentos

El loop canónico de RAG. Tu pregunta recupera los mejores chunks de los docs de

Showcase — Q&A de documentos

El loop canónico de RAG. Tu pregunta recupera los mejores chunks de los docs de un producto; el modelo responde solo a partir de ellos, cita los [id] que usó, y los ids de los chunks recuperados se muestran de vuelta para que puedas verificar las citas contra lo que se trajo. Pregunta "does the free plan include automations?" y responde a partir de los chunks de plans y automations.

Córrelo

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

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

Qué hay aquí

  • backend/store.py — el VectorStore compartido (retrieval).
  • backend/ai_openai.py / backend/ai_gemini.py — la KB, el retrieval, el prompt con fundamento y la llamada de generación.
  • frontend/app/page.tsx — caja de pregunta + respuesta con citas.

Detenlo

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 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

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

"""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}"

Archivos del proyecto

  • .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