RAG Agent V2 is a Docker-first Retrieval-Augmented Generation demo application for asking questions over uploaded PDF documents. It combines a React frontend, FastAPI backend, PostgreSQL metadata storage, Qdrant vector search, Docling document parsing, OpenAI embeddings, and OpenAI chat completion into one end-to-end portfolio project.
The project is built as a practical RAG system rather than a notebook-only prototype. Admin users upload and manage documents, regular users ask questions through a chat interface, answers stream back to the browser, and citations show which document chunks supported the response.
- Full-stack RAG application with React, FastAPI, PostgreSQL, and Qdrant.
- PDF ingestion pipeline using Docling hybrid chunking.
- OpenAI
text-embedding-3-smallembeddings stored in Qdrant. - Optional dense + BM25 hybrid search with Qdrant named vectors.
- Optional local cross-encoder reranking before answer generation.
- Streaming chat answers through Server-Sent Events.
- Citation previews with document name, page, section/header, chunk ID, score, and retrieved text.
- JWT access tokens plus rotating refresh tokens stored in PostgreSQL.
- Admin document upload/delete, user creation, password reset, and audit log views.
- Standalone evaluation runner with JSONL datasets and saved result artifacts.
- Docker Compose setup for frontend, backend, PostgreSQL, and Qdrant.
| Area | Technology |
|---|---|
| Frontend | React, Vite, React Router |
| Backend | FastAPI, SQLAlchemy, Alembic |
| Database | PostgreSQL |
| Vector database | Qdrant |
| Parsing | Docling, pdfium fallback |
| Embeddings | OpenAI embeddings API |
| Answer generation | OpenAI chat completions |
| Reranking | sentence-transformers cross-encoder |
| Auth | Password hashing, JWT access tokens, rotating refresh tokens |
| Tooling | Docker Compose, uv, pytest, ESLint |
Browser
|
| React UI
v
FastAPI backend
|
|-- PostgreSQL
| users, refresh tokens, documents, chunks, chat history, audit logs
|
|-- Qdrant
| dense OpenAI vectors and optional BM25 sparse vectors
|
|-- Docling / pdfium
| PDF parsing and chunking
|
|-- OpenAI
embeddings and grounded answer generation
The app stores uploaded files in Documents/, parsed chunk metadata in PostgreSQL, and searchable vectors in Qdrant. During a question, the backend embeds the query, retrieves candidate chunks from Qdrant, optionally reranks them, builds a grounded prompt, streams the answer to the frontend, and saves the final answer to chat history.
- Admin-only PDF upload.
- Background ingestion after upload.
- Document status tracking:
uploaded,processing,ready,failed. - Shared document library for all logged-in users.
- Document download for all logged-in users.
- Admin-only delete that removes the raw file, PostgreSQL rows, and Qdrant vectors.
- Authenticated chat UI.
- Streaming answers with progress events.
- Saved chat history per user.
- Fallback answer when retrieved context is not enough.
- Citations attached to each answer.
- Citation preview text for inspecting retrieved context.
Runtime settings are controlled through environment variables:
RAG_HYBRID_SEARCH_ENABLEDRAG_RERANKER_ENABLEDRAG_RETRIEVAL_CANDIDATE_LIMITRAG_FINAL_CONTEXT_LIMITRAG_MAX_CHUNKS_PER_DOCUMENTRAG_ADJACENT_CHUNK_COUNTRAG_MIN_SIMILARITY_SCORE
- Login with username and password.
- Password hashes stored in PostgreSQL.
- JWT access tokens for protected routes.
- Rotating refresh tokens with database-backed revocation.
- Logout revokes the current refresh-token session.
- Admin user creation and password reset.
- Audit log page for important user/admin actions.
backend/
app/
api/ FastAPI route modules
core/ config, logging, middleware, error handling
db/ SQLAlchemy models, sessions, Alembic integration
ingestion/ PDF parsing, chunking, embedding, ingestion flow
rag/ retrieval, reranking, prompt building, answer generation
security/ password hashing, JWTs, refresh tokens, dependencies
vector/ Qdrant collection and vector helpers
evaluation/ standalone RAG quality evaluation runner
docs/ public roadmap and portfolio notes
frontend/
src/ React app, pages, auth helpers, API client
postgres/ PostgreSQL Docker image config
qdrant/ Qdrant Docker image config
deploy/ local/private deployment notes
Documents/ local document storage, ignored by git
docker-data/ local database/vector storage, ignored by git
See docs/roadmap.md for the portfolio-safe roadmap and completed milestone summary.
- Docker and Docker Compose.
- Python 3.12.
- uv.
- Node.js 22 or compatible npm environment if running the frontend outside Docker.
- OpenAI API key.
Create your local environment file:
cp .env.example .envUpdate at least these values:
OPENAI_API_KEY=your-openai-api-key
OPENAI_CHAT_MODEL=your-chat-model
JWT_SECRET_KEY=replace-with-a-long-random-secret
ADMIN_USERNAME=admin
ADMIN_PASSWORD=change-this-admin-passwordFor Docker Compose, keep service URLs using Compose hostnames:
POSTGRES_PASSWORD=change-me
DATABASE_URL=postgresql+psycopg://rag_user:change-me@postgres:5432/rag_agent_v2
QDRANT_URL=http://qdrant:6333The password inside DATABASE_URL must match POSTGRES_PASSWORD.
Start the full application:
docker compose up -d --buildInitialize the database and seed the admin user:
docker compose exec backend uv run python -m backend.app.scripts.init_dbOpen the app:
Frontend: http://localhost:5173
Backend API: http://localhost:8000
FastAPI docs: http://localhost:8000/docs
Qdrant: http://localhost:6334
PostgreSQL host port: 5433
Sign in with the admin credentials from .env.
You can upload PDFs from the admin document page in the frontend. The backend saves the file and runs ingestion in the background.
You can also ingest PDFs already placed in Documents/:
docker compose exec backend uv run python -m backend.app.scripts.ingest_demo_documentsThe ingestion flow:
- Reads PDF files from
Documents/. - Parses and chunks them with Docling.
- Stores document and chunk rows in PostgreSQL.
- Generates OpenAI embeddings.
- Stores dense vectors and BM25 sparse vectors in Qdrant.
| Area | Endpoints |
|---|---|
| Health | GET /health, GET /health/db, GET /health/qdrant |
| Auth | POST /auth/login, POST /auth/refresh, POST /auth/logout, GET /auth/me, POST /auth/change-password |
| Admin users | GET /admin/users, POST /admin/users, POST /admin/users/{user_id}/reset-password |
| Documents | GET /documents, POST /documents, GET /documents/{document_id}/download, DELETE /documents/{document_id} |
| RAG | POST /rag/ask, POST /rag/ask/stream, GET /rag/history |
| Audit logs | GET /admin/audit-logs |
Run only PostgreSQL and Qdrant in Docker:
docker compose up -d postgres qdrantRun backend commands from the host with host-facing service URLs:
POSTGRES_USER=$(grep '^POSTGRES_USER=' .env | cut -d= -f2 | tr -d '"')
POSTGRES_PASSWORD=$(grep '^POSTGRES_PASSWORD=' .env | cut -d= -f2 | tr -d '"')
POSTGRES_DB=$(grep '^POSTGRES_DB=' .env | cut -d= -f2 | tr -d '"')
HOST_DATABASE_URL="postgresql+psycopg://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5433/${POSTGRES_DB}"
DATABASE_URL="$HOST_DATABASE_URL" \
QDRANT_URL='http://localhost:6334' \
uv run python -m backend.app.scripts.init_dbStart the backend locally:
POSTGRES_USER=$(grep '^POSTGRES_USER=' .env | cut -d= -f2 | tr -d '"')
POSTGRES_PASSWORD=$(grep '^POSTGRES_PASSWORD=' .env | cut -d= -f2 | tr -d '"')
POSTGRES_DB=$(grep '^POSTGRES_DB=' .env | cut -d= -f2 | tr -d '"')
HOST_DATABASE_URL="postgresql+psycopg://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5433/${POSTGRES_DB}"
DATABASE_URL="$HOST_DATABASE_URL" \
QDRANT_URL='http://localhost:6334' \
uv run uvicorn backend.app.main:app --reloadStart the frontend locally:
cd frontend
npm install
npm run devThe evaluation runner imports the backend RAG pipeline directly. It does not require the FastAPI server to be running, but PostgreSQL and Qdrant must be available.
Host-run Python commands should use localhost:5433 for PostgreSQL and localhost:6334 for Qdrant. Docker-container commands should use postgres:5432 and qdrant:6333.
Start the required services:
docker compose up -d postgres qdrantCheck that documents and vectors already exist:
POSTGRES_USER=$(grep '^POSTGRES_USER=' .env | cut -d= -f2 | tr -d '"')
POSTGRES_PASSWORD=$(grep '^POSTGRES_PASSWORD=' .env | cut -d= -f2 | tr -d '"')
POSTGRES_DB=$(grep '^POSTGRES_DB=' .env | cut -d= -f2 | tr -d '"')
HOST_DATABASE_URL="postgresql+psycopg://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5433/${POSTGRES_DB}"
DATABASE_URL="$HOST_DATABASE_URL" \
QDRANT_URL='http://localhost:6334' \
uv run python - <<'PY'
from sqlalchemy import func, select
from backend.app.core.config import get_settings
from backend.app.db import models
from backend.app.db.session import SessionLocal
from backend.app.vector.qdrant import get_qdrant_client
settings = get_settings()
with SessionLocal() as db:
document_count = db.scalar(select(func.count(models.Document.id)))
chunk_count = db.scalar(select(func.count(models.DocumentChunk.id)))
qdrant_count = get_qdrant_client().count(
collection_name=settings.qdrant_collection_name,
exact=True,
).count
print(f"documents={document_count}")
print(f"postgres_chunks={chunk_count}")
print(f"qdrant_points={qdrant_count}")
PYRun a small smoke evaluation without judge calls:
POSTGRES_USER=$(grep '^POSTGRES_USER=' .env | cut -d= -f2 | tr -d '"')
POSTGRES_PASSWORD=$(grep '^POSTGRES_PASSWORD=' .env | cut -d= -f2 | tr -d '"')
POSTGRES_DB=$(grep '^POSTGRES_DB=' .env | cut -d= -f2 | tr -d '"')
HOST_DATABASE_URL="postgresql+psycopg://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5433/${POSTGRES_DB}"
DATABASE_URL="$HOST_DATABASE_URL" \
QDRANT_URL='http://localhost:6334' \
uv run python -m evaluation.evaluate_rag \
--limit 3 \
--no-judgeRun the full judged evaluation:
POSTGRES_USER=$(grep '^POSTGRES_USER=' .env | cut -d= -f2 | tr -d '"')
POSTGRES_PASSWORD=$(grep '^POSTGRES_PASSWORD=' .env | cut -d= -f2 | tr -d '"')
POSTGRES_DB=$(grep '^POSTGRES_DB=' .env | cut -d= -f2 | tr -d '"')
HOST_DATABASE_URL="postgresql+psycopg://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5433/${POSTGRES_DB}"
DATABASE_URL="$HOST_DATABASE_URL" \
QDRANT_URL='http://localhost:6334' \
uv run python -m evaluation.evaluate_rag \
--dataset evaluation/datasets/rag_quality_world_history.jsonl \
--run-name baseline \
--judge-model gpt-5Evaluation results are written to:
evaluation/results/<timestamp>-<run-name>/
run_config.json
results.jsonl
results.pretty.json
results.<question_type>.pretty.json
summary.json
Generated evaluation results are ignored by default. The repository keeps one curated example result at evaluation/results/20260702T044801Z-hybrid_rerank_ctx8_doc3_adj2_min02/, and evaluation/results/latest.json points to that example.
Backend tests:
uv run pytestFrontend lint:
cd frontend
npm run lintFrontend production build:
cd frontend
npm run buildDocker Compose config check:
docker compose config --quiet- This project is scoped as a local or private-network demo, not a public SaaS deployment.
Documents/anddocker-data/are runtime data folders and should not be committed.- New
evaluation/results/runs are ignored except for the curated portfolio example result. - Host-run Python commands should use
localhost:5433for PostgreSQL andlocalhost:6334for Qdrant. - Docker-container commands should use
postgres:5432andqdrant:6333. - If an old Qdrant collection uses the previous unnamed-vector schema, rebuild or recreate the collection before enabling hybrid search.
- An OpenAI API key and access to the configured embedding, answer, and judge models are required.
- Source documents are not bundled in this repository; upload or place PDFs in
Documents/before ingestion. - The app uses one shared document library for all users and does not include self-registration or advanced permission groups.
- The deployment files are for local or private-network Docker use. Public TLS, public domain setup, and cloud object storage are out of scope.
- Runtime state is stored locally in
Documents/anddocker-data/. - Evaluation scores depend on the local documents, model settings, retrieval configuration, and judge model used for that run.
This project demonstrates:
- Designing a full RAG workflow from document ingestion to grounded answer generation.
- Building a user-facing web app around retrieval and citations.
- Managing auth, audit logs, background ingestion, and operational error handling.
- Evaluating RAG quality with reproducible result artifacts.
- Keeping local development, Docker runtime, and host-run evaluation workflows separate and documented.