Course ES
← back to chapter

Cited summary

RAG beyond question answering. Give it a topic and it retrieves the relevant

Showcase — Cited summary

RAG beyond question answering. Give it a topic and it retrieves the relevant research notes and synthesizes a short summary that pulls from several of them, citing every claim with its [id]. Try "caffeine", "exercise", or "morning light" and watch it weave the matching notes together — with sources.

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.
  • backend/ai_openai.py / backend/ai_gemini.py — the research notes, top-k retrieval, and a summarize-with-citations prompt.
  • frontend/app/page.tsx — topic box + cited summary.

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 cited-summary.zip

unzip cited-summary.zip
cd cited-summary
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 3 (OpenAI): cited multi-passage summaries.

RAG isn't only question answering. Give it a topic, retrieve the several
passages that touch it, and have the model synthesize a short summary that
pulls from all of them — with [id] citations so every claim is traceable. The
corpus is a set of short research notes; ask for "sleep" or "caffeine" and it
weaves the relevant notes together.
"""
from openai import OpenAI

from store import VectorStore

_client = OpenAI()

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

_KB = [
    {"id": "n1", "text": "Adults who sleep 7-9 hours report better mood and sharper attention than short sleepers."},
    {"id": "n2", "text": "Caffeine has a half-life of about 5 hours, so an afternoon coffee can disrupt night sleep."},
    {"id": "n3", "text": "A consistent sleep and wake time strengthens the circadian rhythm more than total hours alone."},
    {"id": "n4", "text": "Bright morning light exposure advances the body clock and helps you fall asleep earlier."},
    {"id": "n5", "text": "Moderate exercise improves sleep quality, but vigorous exercise close to bedtime can delay it."},
    {"id": "n6", "text": "Alcohol shortens the time to fall asleep but fragments REM sleep later in the night."},
]

_SYSTEM = (
    "You are a research assistant. Using ONLY the notes below, write a short "
    "(2-4 sentence) summary answering the user's topic, and cite every claim "
    "with its [id]. If the notes don't cover the topic, say so.\n\nNotes:\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(topic: str) -> str:
    hits = _get_store().search(topic.strip(), k=4)
    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": f"Summarize what the notes say about: {topic.strip()}"}],
    )
    return response.output_text

backend/ai_gemini.py

"""Showcase 3 (Gemini): cited multi-passage summaries.

Same research notes and citation rule, synthesized by Gemini.
"""
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": "n1", "text": "Adults who sleep 7-9 hours report better mood and sharper attention than short sleepers."},
    {"id": "n2", "text": "Caffeine has a half-life of about 5 hours, so an afternoon coffee can disrupt night sleep."},
    {"id": "n3", "text": "A consistent sleep and wake time strengthens the circadian rhythm more than total hours alone."},
    {"id": "n4", "text": "Bright morning light exposure advances the body clock and helps you fall asleep earlier."},
    {"id": "n5", "text": "Moderate exercise improves sleep quality, but vigorous exercise close to bedtime can delay it."},
    {"id": "n6", "text": "Alcohol shortens the time to fall asleep but fragments REM sleep later in the night."},
]

_SYSTEM = (
    "You are a research assistant. Using ONLY the notes below, write a short "
    "(2-4 sentence) summary answering the user's topic, and cite every claim "
    "with its [id]. If the notes don't cover the topic, say so.\n\nNotes:\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(topic: str) -> str:
    hits = _get_store().search(topic.strip(), k=4)
    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=f"Summarize what the notes say about: {topic.strip()}")])],
        config=types.GenerateContentConfig(system_instruction=_SYSTEM.format(context=context)),
    )
    return response.text or ""

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