Chat with your documents β get grounded, cited answers.
A production-shaped Retrieval-Augmented Generation (RAG) product: a full SaaS-style web app (landing page, sign-up/login, dashboard with saved conversation history) on top of a FastAPI RAG backend. Upload PDFs, text, or Markdown and query them in natural language β every answer is generated only from the retrieved passages and cites its sources inline.
βΆ Live demo Β· deployed on Vercel
Runs in 60 seconds with zero API keys. The project ships with an offline mode (deterministic embeddings + an extractive answerer) so you can clone, run, and see the full pipeline work immediately β then add one free Gemini key to switch to real semantic retrieval and generation, at no cost.
- Document ingestion β PDF, TXT, and Markdown, with smart overlapping chunking.
- Semantic retrieval β cosine-similarity search over a persisted vector index.
- Grounded answers with citations β the model answers only from retrieved context and
marks every claim with
[n]source markers. - Token streaming β answers stream to the UI over Server-Sent Events.
- Pluggable providers β swap embeddings (Gemini / OpenAI β offline) and the LLM (Gemini / Claude β offline) behind clean interfaces; the vector store is equally swappable (Chroma / pgvector / Pinecone).
- Full product UI ("Peit") β a from-scratch, framework-free single-page app: animated marketing landing page, sign-up with email verification codes (+ password reset), Google/GitHub OAuth, and a dashboard with saved conversation history, per-chat document uploads, chat rename, a collapsible sidebar, a profile menu (manage account + settings), light + dark theme, and a fully responsive/mobile layout. (Password auth and history are client-side β see the note below β so the live demo needs no database; email verification and OAuth are real backend flows.)
- Per-chat retrieval β documents are uploaded into a specific conversation and answers are grounded only in that chat's files (the vector store is partitioned by collection).
- Fully tested & CI-ready β the whole pipeline is exercised offline;
pytestpasses with no keys and no network.
Note on auth & history. To keep the public demo zero-backend, sign-up/login and chat history are implemented client-side (localStorage). The RAG endpoints (
/api/*) are the real backend. For production, swap in a real auth provider (e.g. Clerk/Auth.js) and a database (e.g. Postgres/Neon) β the frontend is structured so this is a drop-in change.
flowchart LR
subgraph Ingestion
U[Upload PDF / TXT / MD] --> P[Extract text]
P --> C[Chunk + overlap]
C --> E1[Embed chunks]
E1 --> V[(Vector index)]
end
subgraph Query
Q[User question] --> E2[Embed query]
E2 --> R[Top-k cosine search]
V --> R
R --> PR[Build grounded prompt]
PR --> L[LLM: Gemini]
L --> A[Streamed answer + citations]
end
Retrieval pipeline: ingest β chunk β embed β index then embed query β retrieve top-k β prompt with numbered context β generate with [n] citations.
git clone https://github.com/tornikepe/rag-knowledge-assistant.git
cd rag-knowledge-assistant
python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
python scripts/ingest_sample.py # optional: seed the bundled sample doc
uvicorn app.main:app --reload # β http://localhost:8000Open http://localhost:8000, upload a document, and start asking questions.
docker compose up --build # β http://localhost:8000The repo ships a vercel.json + serverless entry point (api/index.py). Import the
repo on Vercel and it deploys as-is β the live demo runs in offline mode (no keys) and
auto-seeds the sample document so it's queryable immediately.
On serverless the vector index lives in
/tmp(ephemeral), so uploads persist only within a warm instance β perfect for a demo. For durable storage, run it as a container or swap in a hosted vector DB behind theVectorStoreinterface.
Gemini's free tier runs the whole pipeline at no cost, and one key powers both the
LLM and the embeddings. Grab a free key at
Google AI Studio, copy .env.example to
.env, and set:
EMBEDDING_PROVIDER=gemini
LLM_PROVIDER=gemini
GEMINI_API_KEY=...That's it β real semantic retrieval and grounded, cited generation, for free.
GEMINI_MODEL defaults to gemini-flash-latest (always the current free Flash model).
Prefer other providers? The interfaces are pluggable: set LLM_PROVIDER=anthropic
(+ ANTHROPIC_API_KEY) for Claude, or EMBEDDING_PROVIDER=openai (+ OPENAI_API_KEY)
for OpenAI embeddings β install the optional SDK first with pip install anthropic
or pip install openai.
On Vercel (or any host), set these as Environment Variables in the project settings instead of a
.envfile, then redeploy β env-var changes only take effect on the next deployment. The same applies to the OAuth and SMTP variables below.
| Variable | Default | Description |
|---|---|---|
EMBEDDING_PROVIDER |
hash |
hash (offline), gemini (free), or openai |
LLM_PROVIDER |
echo |
echo (offline), gemini (free), or anthropic |
GEMINI_API_KEY |
β | One free key for the gemini LLM and embeddings (get one) |
GEMINI_MODEL |
gemini-flash-latest |
Gemini model for generation |
GEMINI_EMBEDDING_MODEL |
gemini-embedding-001 |
Gemini embedding model |
GEMINI_EMBEDDING_DIM |
768 |
Gemini embedding output dimensionality |
GEMINI_THINKING_BUDGET |
0 |
0 disables thinking (fast/cheap); -1 lets the model decide |
ANTHROPIC_API_KEY |
β | Required when LLM_PROVIDER=anthropic |
ANTHROPIC_MODEL |
claude-opus-4-8 |
Claude model for generation |
OPENAI_API_KEY |
β | Required when EMBEDDING_PROVIDER=openai |
EMBEDDING_MODEL |
text-embedding-3-small |
OpenAI embedding model |
CHUNK_SIZE |
900 |
Target characters per chunk |
CHUNK_OVERLAP |
150 |
Overlap between adjacent chunks |
TOP_K |
4 |
Chunks retrieved per query |
STORAGE_DIR |
storage |
Where the vector index is persisted |
The login popup's Continue with Google / GitHub buttons use real OAuth 2.0 when
configured, and fall back to a demo login otherwise β so the app works out of the box and
"upgrades" the moment you add credentials. The backend (app/api/auth.py) runs the
Authorization Code flow and issues a signed session cookie; nothing extra is needed on the
client.
To enable it:
- Create the OAuth apps and copy each client id + secret:
- Google β Google Cloud Console β Create Credentials β OAuth client ID β Web application.
- GitHub β Developer settings β OAuth Apps β New OAuth App.
- Set the redirect (callback) URLs to your deployment, exactly:
https://<your-app>.vercel.app/api/auth/google/callbackhttps://<your-app>.vercel.app/api/auth/github/callback
- Set the environment variables (locally in
.env, or in the Vercel project settings):SESSION_SECRET=<a long random string> OAUTH_REDIRECT_BASE=https://<your-app>.vercel.app GOOGLE_CLIENT_ID=... GOOGLE_CLIENT_SECRET=... GITHUB_CLIENT_ID=... GITHUB_CLIENT_SECRET=...
GET /api/auth/providers reports which providers are live. Auth routes: /api/auth/{provider}/login,
/api/auth/{provider}/callback, /api/auth/me, /api/auth/logout.
The vector index is partitioned by collection (one per chat) behind the
VectorStoreinterface, so uploads only affect the conversation they were added to. Extending the same mechanism to per-user scoping is a small change.
Sign-up emails a 6-digit code to the address being registered, and the same flow powers "Forgot password?" on the login screen. Configure SMTP to turn it on; without it, email sign-up and reset are disabled and users sign in with Google/GitHub instead.
Set these (in .env locally, or the Vercel project settings β then redeploy):
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=you@gmail.com # the full address that sends the mail (required)
SMTP_PASSWORD=your-app-password # see the Gmail note below
SMTP_FROM=you@gmail.com # optional; defaults to SMTP_USER
SMTP_STARTTLS=trueGmail: use an App Password, not your account password. Turn on 2-Step Verification, create one at myaccount.google.com/apppasswords, and paste the 16 characters without spaces. All three of
SMTP_HOST,SMTP_USER, andSMTP_PASSWORDmust be set. (Vercel's serverless functions can send over SMTP β the port isn't blocked.)
The code is carried in a short-lived HMAC-signed token, so no database is needed. Endpoints:
POST /api/auth/signup/start, POST /api/auth/signup/verify.
Interactive OpenAPI docs are served at /docs.
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/health |
Status, providers, and index size |
POST |
/api/ingest |
Upload & index a document (multipart; optional collection) |
POST |
/api/query |
Ask a question β answer + citations (JSON; optional collection) |
POST |
/api/query/stream |
Same, streamed as Server-Sent Events |
GET |
/api/documents |
List indexed documents (optional ?collection=) |
DELETE |
/api/documents |
Clear the index |
# Ingest, then ask:
curl -F "file=@data/sample_docs/ai_automation_overview.md" http://localhost:8000/api/ingest
curl -s http://localhost:8000/api/query \
-H "Content-Type: application/json" \
-d '{"question": "What is RAG and why does it matter?"}' | jqpytest # offline: no API keys, no networkCovers chunking, the vector store (ranking + persistence), and the full API flow
(ingest β query β stream β clear) via FastAPI's TestClient.
rag-knowledge-assistant/
βββ app/
β βββ main.py # FastAPI app + static UI mount
β βββ config.py # Settings (pydantic-settings)
β βββ schemas.py # API request/response models
β βββ api/routes.py # HTTP endpoints
β βββ core/
β βββ chunking.py # Overlapping text splitter
β βββ embeddings.py # Gemini + OpenAI + offline hashing providers
β βββ vectorstore.py # From-scratch NumPy cosine index (persisted)
β βββ ingest.py # PDF / text extraction
β βββ llm.py # Gemini + Claude + offline echo providers
β βββ service.py # RAG orchestration (retrieve β prompt β generate)
βββ frontend/ # Dependency-free chat UI (HTML/CSS/JS)
βββ tests/ # Offline pytest suite
βββ data/sample_docs/ # A sample document to try immediately
βββ scripts/ingest_sample.py
βββ Dockerfile Β· docker-compose.yml Β· Makefile
βββ .github/workflows/ci.yml
- Why a hand-written vector store? Retrieval is just normalized dot products over a
matrix β implementing it directly makes the mechanics legible and keeps the dependency
footprint tiny. It sits behind a
VectorStoreinterface, so moving to Chroma, pgvector, or Pinecone is a one-class change. - Why offline providers? So the repo is genuinely runnable and CI-testable without secrets. The provider abstraction is the same one used for the real Gemini/OpenAI/Claude implementations β nothing is faked at the seams.
- Grounding & citations. Retrieved chunks are numbered in the prompt and the model is
instructed to answer only from them and cite with
[n]; the API returns the matching source list so the UI can render verifiable sources.
- Hybrid search (BM25 + dense) and re-ranking
- Per-collection indexes (per-chat document scoping)
- Per-user multi-tenant indexes + durable storage
- Streaming citation highlights in the UI
- Pluggable Chroma / pgvector backends behind the existing interface
MIT β see LICENSE.

