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 sharedVectorStore(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.
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
.gitignoreREADME.es.mdREADME.mdbackend/Dockerfilebackend/ai_gemini.pybackend/ai_openai.pybackend/main.pybackend/requirements.txtbackend/store.pybootstrap-secrets.shdocker-compose.ymlfrontend/Dockerfilefrontend/app/layout.tsxfrontend/app/page.tsxfrontend/next.config.tsfrontend/package.jsonfrontend/tsconfig.json