Knowledge as of Oct 26, 2026
The Inference TimesReference architectures, tutorials, demos and free skills — by Nitin Goswami
Epoch III · Step 16Ask the AI Nitin →

DESIGN DOCUMENT · rag-demo

RAG Demo: ask real-world documents, see the evidence

A minimal web app that shows semantic Retrieval-Augmented Generation end to end. Pick a knowledge base (HR policies, a lease, a router manual), ask a question, and see the passages, similarity scores and database rows behind every answer. Each stage maps to the CoALA framework.

Stack
FastAPI · LangChain · pgvector · Next.js
Models
gpt-4o-mini · text-embedding-3-small
Date
October 17, 2026
Status
Approved · built
▶ Try it liverag-demo.nitingoswami.io
FollowsCoALA →

Purpose & scope

The app is a learning tool. The ingestion pipeline fills a long-term semantic memory (pgvector). Each chat turn runs a retrieval action that copies the most relevant chunks into the model's working memory (the prompt).

samples.seed()Chunk · embed · storeCoALA learning action
pgvectorVector collectionSemantic memory (long-term)
similarity_searchTop-4 chunks in one packRetrieval action
ChatPromptTemplateContext + historyWorking memory
ChatOpenAIGrounded answerReasoning → external action

In scope

  • Three sample knowledge bases ("packs") of fictional documents, indexed at startup: HR & Policies, Contracts & Legal, and Customer Support.
  • A chat agent that searches only the selected pack, answers only from it, and cites its sources.
  • Evidence views: the pipeline steps with timings and tokens, a sources table (document, page, chunk, similarity, cited), and a read-only view of the pgvector rows.
  • Short chat history per pack, kept on the client and sent with each request.
  • Local mode only (ALLOW_UPLOADS=true): upload .pdf, .txt and .md files, de-duplicate re-uploads, and delete documents.

Demo safety and privacy

Uploads are off by default, so visitors can't store anything and there is no user data to protect. Sample documents are read-only, and uploads can't reuse a sample filename. /chat is rate-limited to 20 requests per minute per IP, which caps OpenAI spend. Request sizes are limited too: a message can be up to 1,000 characters and the history up to 20 turns. Question text and the retrieved passages are sent to OpenAI to generate the answer.

Demo mode (default)Local mode (ALLOW_UPLOADS=true)
Sample knowledge basesRead-onlyRead-only
Upload and delete your own filesDisabled (403)Yes, in a shared "Your documents" set
Limits20 chats per minute per IPAlso 5 files and 5 MB per upload, 10 uploads per minute

Out of scope

Authentication, per-user upload isolation, streaming, reranking, hybrid search and production deployment. Section 10 lists these as follow-ups.

Use cases

Visitor RAG Demo App Pick a knowledge base Ask a question Inspect evidence Upload docs (local mode) OpenAI API embeddings + chat embeddings
Use case diagram: a visitor picks a knowledge base, asks, and inspects the evidence; uploads are local-mode only.
IDUse caseBehaviour
UC1Upload documents (local mode)The user picks files. The system chunks, embeds and stores them, then shows how many chunks were indexed per file. Re-uploading the same file is skipped; a changed file with the same name replaces its old chunks.
UC2Pick a knowledge baseShows each pack's problem, value and documents (chunk and page counts). In local mode, uploaded documents can be deleted, which removes their chunks and embeddings from pgvector.
UC3Ask a questionRetrieves the top-4 chunks from the selected pack. The LLM answers from them only, or replies "I don't know based on the uploaded documents."
UC4Inspect evidenceClicking an answer shows its pipeline timings, a sources table with similarity scores and cited flags, and the matching rows in the pgvector table.

Architecture

The system has three runtime parts: a Next.js client, a FastAPI service that owns all LangChain logic, and Postgres with the pgvector extension. OpenAI provides the embedding and chat models.

Ingestion · startup · samples.seed() Query · POST /chat Sample docs packs.json + files Load + chunk 1000 chars · 150 overlap OpenAI embeddings text-embedding-3-small pgvector chunks + 1536-d vectors text chunks store Question + chosen pack Retrieve cosine top-4 · pack filter Prompt rules + chunks + 6 turns LLM · gpt-4o-mini temperature 0 Answer cites [file p.N] ask embed question top-4 chunks + scores Evidence panels answer sources: rank · page · similarity · cited · pgvector rows · timings answer + sources scores · timings
Pipeline: sample documents are chunked, embedded and stored at startup; each question retrieves the top four chunks, which the LLM answers from with citations.
Browser · Next.js :3000 page.tsx KnowledgeBase.tsx ChatPanel.tsx EvidencePanel.tsx InsightPanel.tsx FastAPI · :8000 main.py ingest.py rag.py samples.py vectorstore.py config.py startup GET /packs · /upload POST /chat GET /packs/{id}/rows OpenAI API Postgres 16 + pgvector SQL + vector search embed chat completion
Component diagram: the panels, the FastAPI modules, and the chat path to pgvector and OpenAI.
Developer machine next dev localhost:3000 uvicorn app.main:app localhost:8000 Docker pgvector:pg16 · :5432 volume pgdata HTTP / JSON psycopg api.openai.com HTTPS
Deployment view for local development.

Key flows

4.1 Ingestion: hash → chunk → embed → store

At startup the backend indexes every sample file; in local mode, uploads take the same path. A file whose hash is already stored in that pack is skipped, so restarts are cheap, and a changed file replaces its old chunks only after the new ones are stored.

seed() or /upload ingest.py OpenAIEmbeddings pgvector 1 each file: ingest_file(name, pack) 2 sha256 of the file 3 stored_versions(name) 4 {(file_hash, pack)} 5 Same hash + pack? Skip 6 Load, split 1000 / 150 7 Tag chunks: pack, chunk, hash 8 embed_documents(chunks) 9 vectors (1536 dims) 10 INSERT rows (stable ids) 11 delete_stale(name, keep=hash) 12 (chunks, added | updated | unchanged)
Sequence: ingestion with de-duplication (CoALA learning action).

4.2 Query: question → retrieve → generate

Visitor ChatPanel FastAPI /chat rag.py OpenAIEmbeddings pgvector ChatOpenAI 1 Ask, or click a suggestion 2 POST /chat {message, pack, history} 3 Rate limit, check pack 4 answer(message, history, pack) 5 embed_query(message) 6 query vector 7 cosine search, k = 4, pack filter 8 top-4 chunks + distances 9 Build prompt: rules + context + 6 turns 10 invoke(messages) 11 answer with [file p.N] + tokens 12 {answer, sources, stats} 13 200 OK 14 Answer; click it for evidence
Sequence: one chat turn within a pack (retrieval → working memory → reasoning).

4.3 Chat request lifecycle

Idle Sending Showing Error user submits 200 OK network / 4xx / 5xx done user retries
State diagram for the chat panel.

Backend design

main +GET /health · /config +GET /packs · /packs/{id}/rows +POST /chat +POST /upload (local mode) +DELETE /documents/{name} samples +packs() list[dict] +seed() +pack_ids() set[str] ingest +ingest_file(path, name, pack) -_load · _split · _file_hash rag +answer(question, history, pack) -_format_context(docs) str -_llm() ChatOpenAI vectorstore +get_vector_store() PGVector +pack_documents · pack_rows +stored_versions · delete_stale +delete_source · count_chunks Settings chat_model · embedding_model chunk_size · chunk_overlap · top_k allow_uploads · rate limits ChatRequest message: str (≤ 1000) history: Message[] (≤ 20) pack: str ChatResponse answer: str sources: Source[] stats: Stats ◆ holds Message {role, content} ◆ Source {rank, source, page, chunk, score} ◆ Stats {retrieve_ms, generate_ms, tokens}
Class diagram: Python modules and Pydantic models.
FileResponsibility
app/config.pyLoads .env and exposes a frozen Settings dataclass: models, chunking, top_k, ALLOW_UPLOADS and the rate and size limits.
app/samples.pyReads samples/packs.json and indexes every sample file at startup (seed()), skipping unchanged files.
app/vectorstore.pyCached PGVector (collection docs, JSONB metadata) using OpenAIEmbeddings, plus SQL for pack documents, rows, de-duplication and delete.
app/ingest.pyHashes the file, skips it if unchanged, picks a loader by extension, splits with RecursiveCharacterTextSplitter, tags source, page, chunk, pack and file_hash, then calls add_documents with stable ids.
app/rag.pyRuns similarity_search_with_score(k=4) filtered to the pack, builds the prompt with ChatPromptTemplate and MessagesPlaceholder, calls ChatOpenAI(temperature=0), and returns the answer, sources with similarity scores, and timings.
app/main.pyFastAPI routes, startup seeding, CORS, request and response models, per-IP rate limits, upload guards and error mapping.

System prompt

API contract

MethodPathRequestResponse
GET/healthn/a{"status":"ok"}
GET/confign/a{"allow_uploads","chat_model","embedding_model","chunk_size","chunk_overlap","top_k",…}
GET/packsn/a[{"id","title","audience","problem","value","questions","readonly","documents":[{"filename","chunks","pages"}]}]
GET/packs/{id}/rowsn/a{"limit":100,"rows":[{"id","source","page","chunk","file_hash","dims","embedding_preview","text"}]}
POST/chat{"message","pack","history":[{"role","content"}]}{"answer","sources":[{"rank","id","source","page","chunk","score",…}],"stats":{"retrieve_ms","generate_ms","chunks_searched","prompt_tokens",…}}
POST/uploadmultipart, field files (1..5), local mode[{"filename","chunks","status"}], status is added, updated or unchanged
DELETE/documents/{filename}n/a, local mode{"filename","chunks_deleted"}

Errors: 400 for an unsupported file type or an empty message, 403 for uploads in demo mode or deleting a sample, 404 for an unknown pack or document, 409 for an upload that reuses a sample filename, 413 for a file that is too large, 429 when a rate limit is hit. 500 with a readable detail for OpenAI or database failures.

Data model

langchain-postgres creates the vector extension and both tables on first use, so no migrations are needed. Each chunk is one row: its text, its 1536-dimension embedding and JSONB metadata. The pack key in that metadata is what keeps each knowledge base separate; the evidence panel reads these rows directly.

langchain_pg_collection uuid uuid PK name varchar "docs" cmetadata jsonb langchain_pg_embedding id varchar PK (stable) collection_id uuid FK embedding vector 1536 dims document varchar chunk text cmetadata jsonb source, page, chunk, pack, file_hash contains
Entity-relationship diagram of the vector store tables.

Frontend design

app/page.tsx 3 columns + bottom panel · chat thread per pack KnowledgeBase 1 · packs, documents ChatPanel 2 · thread, suggestions InsightPanel 3 · steps, timings, tokens EvidencePanel sources table · DB rows UploadPanel local mode only MessageBubble answer · cited sources lib/api.ts fetch wrappers for every route sendChat listRows
Frontend component tree.
  • A full-page layout: knowledge bases on the left, the chat in the centre, "How this answer was built" on the right, and an evidence panel along the bottom. On mobile the panels stack.
  • Each pack keeps its own chat thread, so switching packs never mixes answers.
  • Clicking an answer selects it: the insight panel shows retrieve and generate timings and token counts; the evidence panel shows the Answer sources table (rank, document, page, chunk, similarity, cited) and Database rows from langchain_pg_embedding.
  • UploadPanel appears only when /config reports allow_uploads.
  • Client components ("use client") with Tailwind v4 and plain fetch to NEXT_PUBLIC_API_URL (default http://localhost:8000). No extra packages beyond the create-next-app defaults.

Project layout

rag-demo/
├── docker-compose.yml        pgvector/pgvector:pg16
├── README.md
├── docs/DESIGN.md            this document (Markdown + Mermaid)
├── backend/
│   ├── requirements.txt
│   ├── .env.example          OPENAI_API_KEY, DATABASE_URL
│   ├── samples/              packs.json + fictional docs (hr/, legal/, support/)
│   └── app/  config · samples · vectorstore · ingest · rag · main
└── frontend/
    ├── lib/api.ts
    └── app/  page.tsx · components/{KnowledgeBase, ChatPanel, InsightPanel,
              EvidencePanel, UploadPanel, MessageBubble, Header, Panel}.tsx

Run & verify

No install needed to try it: the live demo runs in demo mode, with the three sample knowledge bases and uploads turned off. To run it yourself:

git clone https://github.com/nitingoswami-io/rag-demo.git && cd rag-demo
docker compose up -d
cd backend && python3.12 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt && cp .env.example .env   # add OPENAI_API_KEY
uvicorn app.main:app --reload                              # :8000, docs at /docs
cd ../frontend && cp .env.local.example .env.local         # NEXT_PUBLIC_API_URL
npm install && npm run dev                                 # :3000
CheckExpected
GET /health{"status":"ok"}
Start the backendLog shows each sample file added (first run) or unchanged.
HR pack: "How many weeks of parental leave do I get?"18 weeks for the primary caregiver and 6 for the secondary, cited as [acme-employee-handbook.md]
Legal pack: "Can I keep a dog in the flat?"One cat or one dog under 15 kg with written consent, plus a 300 EUR pet deposit, cited to the lease.
"Who won the 2022 World Cup?""I don't know based on the uploaded documents."
POST /upload in demo mode403
Upload a .csv (local mode)400 with "only .pdf, .txt and .md are supported."

Next steps

IdeaWhat it teaches
Hybrid search (BM25 + vector) and a rerankerThe largest retrieval-quality gain in real systems
Agentic RAG: let the agent decide when to retrieveCoALA decision procedure instead of a fixed pipeline
Persist chat history in Postgres and retrieve itEpisodic memory alongside semantic memory
Streaming answers over SSEResponsive UX for long answers
Per-user upload collections with authMemory isolation and access control

Contribute

Fork the repo, pick an idea from the table, and open a pull request. Bugs and unclear steps go in issues; the workflow is in CONTRIBUTING.md.

Credits & sources

  1. Theodore R. Sumers, Shunyu Yao, Karthik Narasimhan, Thomas L. Griffiths. Cognitive Architectures for Language Agents (2023)The CoALA vocabulary (semantic and working memory, retrieval and learning actions) used to map each RAG stage.
  2. pgvector — open-source vector similarity search for PostgresThe vector store behind ingestion and similarity search.