# GeminiRAG — Codebase Reference **Every file, what it does, and how they connect.** --- ## Directory Tree ``` geminirag/ ├── app/ │ ├── main.py │ ├── config.py │ ├── deps.py │ ├── security.py │ ├── limiter.py │ ├── api/ │ │ ├── auth.py │ │ ├── files.py │ │ ├── jobs.py │ │ ├── documents.py │ │ ├── query.py │ │ ├── admin.py │ │ └── agent.py │ ├── models/ │ │ └── db.py │ ├── processors/ │ │ ├── base.py │ │ ├── pdf.py │ │ ├── docx_proc.py │ │ ├── xlsx_proc.py │ │ ├── image.py │ │ └── video.py │ ├── rag/ │ │ ├── engine.py │ │ ├── chunker.py │ │ ├── embedder.py │ │ └── vectorstore.py │ ├── workers/ │ │ ├── celery_app.py │ │ └── tasks.py │ ├── agent/ │ │ ├── agent.py │ │ └── tools.py │ ├── evaluation/ │ │ └── ragas_eval.py │ └── observability/ │ ├── logging.py │ └── tracing.py ├── frontend/ │ ├── src/ │ │ ├── main.tsx │ │ ├── App.tsx │ │ ├── index.css │ │ ├── vite-env.d.ts │ │ ├── api/ │ │ │ └── client.ts │ │ ├── context/ │ │ │ ├── AuthContext.tsx │ │ │ └── ToastContext.tsx │ │ ├── components/ │ │ │ ├── NavBar.tsx │ │ │ └── PrivateRoute.tsx │ │ ├── hooks/ │ │ │ └── useToast.ts │ │ └── pages/ │ │ ├── LoginPage.tsx │ │ ├── RegisterPage.tsx │ │ ├── UploadPage.tsx │ │ ├── QueryPage.tsx │ │ ├── JobsPage.tsx │ │ ├── AdminPage.tsx │ │ └── AgentPage.tsx │ ├── package.json │ ├── vite.config.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ └── index.html ├── scripts/ │ ├── seed_admin.py │ ├── ragas_baseline.py │ └── download_ragas_datasets.py ├── tests/ │ ├── conftest.py │ ├── test_api.py │ ├── test_processors.py │ ├── test_rag.py │ ├── test_query.py │ └── test_agent.py ├── migrations/ ← Alembic migration versions ├── Data set/ │ └── ragas_eval/ │ ├── ms_marco_samples.json │ └── natural_questions_samples.json ├── .env ← gitignored ├── .env.example ├── pyproject.toml ├── docker-compose.yml ├── docker-compose.prod.yml ├── Dockerfile ├── alembic.ini ├── README.md ├── HANDOVER.md ├── DEMO_SCRIPT.md ├── context.md ← this project's session context └── codebase.md ← this file ``` --- ## Backend Files (`app/`) --- ### `app/main.py` — App Factory **What it does:** Creates the FastAPI application, wires up all middleware and routers, exposes `/health`. **Key contents:** - `create_app() → FastAPI` — the factory function - CORS middleware using `settings.allowed_origins_list` (env-configurable) - slowapi rate limiter exception handler - HTTP request logging middleware — logs `request_id`, `user_id`, `endpoint`, `method`, `status_code`, `latency_ms` for every request - `/health` GET — pings PostgreSQL (`SELECT 1`) and ChromaDB (`heartbeat()`); returns `{"status":"ok","database":"ok","chromadb":"ok"}` or 503 if either is down - Registers 7 routers: `auth`, `files`, `jobs`, `documents`, `query`, `admin`, `agent` - `app = create_app()` — singleton used by uvicorn **Connects to:** `config.py`, `limiter.py`, `observability/logging.py`, `observability/tracing.py`, all `api/` modules, `models/db.py`, `rag/vectorstore.py` --- ### `app/config.py` — Settings **What it does:** Loads and validates all environment variables. The app crashes on startup if P0 vars are missing or still set to placeholder values. **Key contents:** - `class Settings(BaseSettings)` — Pydantic settings model - P0 fields (required, app exits if missing): `GEMINI_API_KEY`, `DATABASE_URL`, `REDIS_URL`, `SECRET_KEY` - P1 fields (have defaults): `CHROMA_HOST/PORT/COLLECTION`, `ACCESS_TOKEN_EXPIRE_MINUTES`, `ALGORITHM`, `UPLOAD_DIR`, `GEMINI_MODEL`, `GEMINI_EMBEDDING_MODEL`, `CHUNK_SIZE` (800), `CHUNK_OVERLAP` (100), `RAG_TOP_K` (5), `CONFIDENCE_THRESHOLD` (0.65), `CELERY_MAX_RETRIES`, `CELERY_RETRY_BACKOFF`, `OTEL_*`, `ALLOWED_ORIGINS` - `allowed_origins_list` property — splits `ALLOWED_ORIGINS` by comma for CORS - `model_post_init()` — validates no placeholder values remain - `settings` singleton — imported everywhere via `from app.config import settings` **Connects to:** imported by virtually every other module --- ### `app/deps.py` — FastAPI Dependencies **What it does:** Provides reusable dependency-injected objects for route handlers. **Key contents:** - `oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login")` - `get_db()` — yields a `Session(get_engine())`, used as `Depends(get_db)` in routes - `get_current_user(token, db)` — decodes JWT, loads `User` from DB, checks `is_active`, updates `last_active_at`, raises 401 if anything fails - `require_admin(current_user)` — checks `current_user.role == UserRole.admin`, raises 403 if not **Connects to:** `security.py` (decode_token), `models/db.py` (User, get_engine) --- ### `app/security.py` — Auth Utilities **What it does:** Password hashing and JWT encoding/decoding. No FastAPI dependencies — pure utility functions. **Key contents:** - `hash_password(password) → str` — bcrypt hash via passlib - `verify_password(plain, hashed) → bool` — bcrypt comparison - `create_access_token(data, expires_minutes) → str` — JWT encode with `exp` claim, signed with `settings.SECRET_KEY` - `decode_token(token) → dict` — JWT decode, raises HTTP 401 on JWTError or expired token **Connects to:** `config.py` (SECRET_KEY, ALGORITHM), used by `api/auth.py` and `deps.py` --- ### `app/limiter.py` — Rate Limiter **What it does:** Single module that instantiates the slowapi `Limiter` so it can be imported without circular dependencies. **Key contents:** - `limiter = Limiter(key_func=get_remote_address)` **Connects to:** `main.py` (registers exception handler), `api/auth.py` (decorates /login) --- ## API Route Handlers (`app/api/`) --- ### `app/api/auth.py` — Authentication **Routes:** - `POST /auth/register` — creates User record with hashed password - `POST /auth/login` (rate limit: 10/min) — verifies credentials, returns JWT `access_token` **Connects to:** `security.py`, `limiter.py`, `models/db.py` (User), `deps.py` (get_db) --- ### `app/api/files.py` — File Upload **Routes:** - `POST /v1/files/upload` — multipart upload, validates type/size, creates Job (PENDING), saves file to disk, enqueues Celery task **Key logic:** - `EXTENSION_MAP` — maps file extensions to internal type strings (pdf, docx, xlsx, csv, image, video, audio) - `MAX_FILE_SIZE_BYTES = 500 * 1024 * 1024` (500 MB) - Saves file to `UPLOAD_DIR/{job_id}/{original_filename}` - Creates `Job` in `PENDING` state - Calls `process_file.delay(str(job.id))` - Returns 202 immediately with `{job_id, filename, file_type, status: "PENDING"}` **Connects to:** `models/db.py` (Job, JobStatus), `workers/tasks.py` (process_file), `deps.py`, `observability/logging.py` --- ### `app/api/jobs.py` — Job Management **Routes:** - `GET /v1/jobs/{job_id}` — fetch single job (owner or admin) - `GET /v1/jobs` — list jobs (user sees own, admin sees all) - `POST /v1/jobs/{job_id}/reprocess` — reset error fields, re-queue via `process_file.delay()` **Connects to:** `models/db.py` (Job, JobStatus, UserRole), `workers/tasks.py` (process_file), `deps.py` --- ### `app/api/documents.py` — Document Retrieval **Routes:** - `GET /v1/documents` — list COMPLETED jobs (these are "documents") - `GET /v1/documents/{job_id}/summary` — return `job.result` parsed as JSON **Connects to:** `models/db.py` (Job, JobStatus, UserRole), `deps.py` --- ### `app/api/query.py` — RAG Queries **Routes:** - `POST /v1/query` — standard RAG query, waits for full answer - `POST /v1/query/stream` — same RAG retrieval, but streams answer token-by-token via SSE **Key logic:** - `_resolve_chunks_and_context()` — shared helper: embeds question, searches ChromaDB, applies confidence gate, returns `{early_return: bool, payload: ..., chunks: [...], user_prompt: str}` - Streaming uses `StreamingResponse(event_stream(), media_type="text/event-stream")` — yields `data: {json}\n\n` events of type `chunk` (text fragment) and `done` (final answer + citations) - Frontend uses `fetch` + `ReadableStream` (not `EventSource`) to handle auth headers **Connects to:** `rag/engine.py`, `rag/embedder.py`, `rag/vectorstore.py`, `models/db.py`, `deps.py`, `google-genai` SDK --- ### `app/api/admin.py` — Admin Analytics **Routes:** - `GET /v1/admin/usage` — token counts, latency trends, per-user breakdown - `GET /v1/admin/ragas` — RAGAS averages + low-scoring queries - `GET /v1/admin/users` — user list with stats; `PATCH` to toggle `is_active` - `GET /v1/admin/logs` — paginated raw UsageLog entries **Connects to:** `models/db.py` (UsageLog, QueryHistory, User, UserRole), `deps.py` (require_admin) --- ### `app/api/agent.py` — Agent Chat **Routes:** - `POST /v1/agent/chat` — sends message to ADK agent, returns `{response, tool_calls_made, session_id, prompt_tokens, completion_tokens}` **Connects to:** `agent/agent.py` (run_agent), `deps.py` --- ## Database Models (`app/models/`) --- ### `app/models/db.py` — ORM Tables **What it does:** Defines all four database tables using SQLModel (SQLAlchemy under the hood). **Tables:** | Table | Primary Fields | |---|---| | `users` | id (UUID), email (unique), hashed_password, role (admin/user), is_active, created_at, last_active_at | | `jobs` | id (UUID), user_id (FK), filename, file_type, file_path, status, step, retry_count, error_type, error_message, result (JSON str), chunk_count, created_at, updated_at | | `usage_logs` | id (UUID), user_id, job_id, endpoint, model, prompt/completion/total_tokens, latency_ms, query_text, llm_response_preview, created_at | | `query_history` | id (UUID), user_id, question, answer, citations (JSON), job_ids_queried (JSON), chunk_count_retrieved, avg_similarity_score, confidence_gate_passed, prompt/completion_tokens, latency_ms, ragas_scores (JSON), ragas_computed_at, created_at | **Key functions:** - `get_engine()` — lazy singleton with `pool_size=10, max_overflow=20, pool_pre_ping=True` - `create_db_and_tables()` — creates all tables (used in startup or tests) **Connects to:** everything — all API handlers, tasks, and scripts import from here --- ## File Processors (`app/processors/`) All processors follow the same pattern: extend `BaseProcessor`, implement `extract()` and `summarise()`, call via `processor.run(db)`. --- ### `app/processors/base.py` — Abstract Base **What it does:** Defines the interface all processors must implement, and provides the Gemini API call wrappers. **Key contents:** - `RateLimitError`, `InvalidInputError` — custom exceptions for error classification - `BaseProcessor(ABC)` abstract class: - `extract() → str` — abstract, extract raw text from file - `summarise(text, db) → dict` — abstract, call Gemini and return JSON summary - `run(db) → (str, dict)` — template method: calls extract(), summarise(), stores JSON in `job.result`, returns both - `_call_gemini_json(prompt, db)` — calls Gemini with `response_mime_type="application/json"`, handles 429/400/503, logs to UsageLog - `_call_gemini_vision_json(prompt, image_data, mime_type, db)` — multimodal Gemini call (image + text) **Connects to:** `observability/logging.py` (log_llm_call), `config.py` (settings), `google-genai` SDK --- ### `app/processors/pdf.py` — PDF Processor - **extract():** pdfplumber → page text + tables → `[Page N]` prefixed concatenation - **summarise():** Gemini JSON → `{title, document_type, summary, key_points, risks, entities, tables_found}` - **Library:** pdfplumber --- ### `app/processors/docx_proc.py` — DOCX Processor - **extract():** python-docx → paragraphs + tables → markdown - **summarise():** Gemini JSON → `{title, document_type, summary, key_points, risks, sections, entities}` - **Library:** python-docx --- ### `app/processors/xlsx_proc.py` — XLSX/CSV Processor - **extract():** openpyxl (XLSX) or csv.reader (CSV) → markdown tables, `[Sheet: name]` prefixed, capped at 500 rows - **summarise():** Gemini JSON → `{title, summary, sheets, column_descriptions, key_insights, row_count}` - **Libraries:** openpyxl, csv --- ### `app/processors/image.py` — Image Processor - **extract():** returns `""` — no text extraction step - **summarise():** reads file as bytes → `_call_gemini_vision_json()` → `{image_type, ocr_text, language, business_card: {name, title, company, email, phone, address, website}, summary}` - **Supported MIME types:** image/png, image/jpeg, image/webp --- ### `app/processors/video.py` — Audio/Video Processor - **extract():** uploads file to Gemini Files API, polls until ACTIVE (300s timeout), stores `uploaded_file` reference - **summarise():** multimodal Gemini call with diarization prompt → `{duration_seconds, speaker_count, speakers, segments: [{speaker, timestamp, text}], full_transcript, summary, action_items, key_decisions, topics_discussed}` - **Note:** Handles both audio (.mp3/.wav/.m4a) and video (.mp4/.mov). Diarization accuracy depends on audio quality. --- ## RAG Layer (`app/rag/`) --- ### `app/rag/engine.py` — RAG Orchestration **What it does:** The core query brain. Connects all RAG components together. **Key contents:** - `RAG_SYSTEM_PROMPT` — instructs Gemini to only answer from context, cite sources as [1][2], and refuse out-of-scope questions - `_resolve_chunks_and_context(question, job_ids, settings)` — shared by both `/query` and `/query/stream`: embed question → search ChromaDB → confidence gate → format user prompt. Returns `{early_return, payload}` or `{chunks, user_prompt}` - `query(question, job_ids, user_id, db, settings)` — full pipeline: call `_resolve_chunks_and_context()`, call Gemini for answer, parse citations, log to UsageLog + QueryHistory, enqueue `compute_ragas.delay()`, return result dict **Confidence gate:** If `avg_similarity_score < CONFIDENCE_THRESHOLD (0.65)` → returns canned "I don't have enough information" answer without calling Gemini. **Connects to:** `rag/embedder.py` (embed_query), `rag/vectorstore.py` (search), `observability/logging.py` (log_llm_call), `models/db.py` (QueryHistory), `workers/tasks.py` (compute_ragas.delay), `google-genai` SDK --- ### `app/rag/chunker.py` — Text Chunking **What it does:** Splits extracted text into overlapping chunks suitable for embedding. **Key functions:** - `chunk_text(text, job_id, filename, file_type, chunk_size=800, overlap=100)` — splits on whitespace, sliding window (800 words, 100-word overlap), extracts `[Page N]` markers, skips chunks < 50 words. Returns list of `{text, job_id, filename, file_type, chunk_index, metadata: {page_or_segment}}` - `chunk_video_segments(segments, job_id, filename)` — converts `[{speaker, timestamp, text}]` from Gemini diarization output into chunks with speaker/timestamp metadata **Connects to:** `workers/tasks.py` (called during CHUNKING step) --- ### `app/rag/embedder.py` — Embedding Generation **What it does:** Converts text chunks and queries into 768-dimensional vectors using Gemini. **Key functions:** - `embed_chunks(chunks, user_id, job_id, settings, db)` — batches 100 chunks at a time, calls `genai.embed_content()` with `task_type="RETRIEVAL_DOCUMENT"`, retries on 429 with delays [60, 120, 240]s, logs each batch to UsageLog - `embed_query(question, settings)` — embeds single query with `task_type="RETRIEVAL_QUERY"`, returns 768-dim vector **Connects to:** `observability/logging.py` (log_llm_call), `config.py` (GEMINI_EMBEDDING_MODEL), `google-genai` SDK --- ### `app/rag/vectorstore.py` — ChromaDB Operations **What it does:** All interactions with ChromaDB vector database. **Key functions:** - `get_chroma_client(settings)` → `chromadb.HttpClient(host, port)` - `get_or_create_collection(client, settings)` → cosine-distance collection named `settings.CHROMA_COLLECTION` - `add_chunks(collection, chunks, embeddings)` — upserts with 3x retry + 5s backoff. IDs: `{job_id}_{chunk_index}`. Stores text, embeddings, metadata (job_id, filename, file_type, chunk_index, page_or_segment, speaker, timestamp) - `search(collection, query_embedding, top_k=5, job_ids=None)` → list of `{text, score, filename, page_or_segment, job_id}`, similarity = 1 - cosine_distance - `delete_job_chunks(collection, job_id)` — removes all chunks for a job (called on reprocess) **Connects to:** `config.py` (CHROMA_HOST/PORT/COLLECTION), ChromaDB client library --- ## Celery Workers (`app/workers/`) --- ### `app/workers/celery_app.py` — Celery Configuration **What it does:** Creates and configures the Celery application instance. **Key configuration:** - broker: `settings.REDIS_URL` (Redis) - backend: `settings.DATABASE_URL` (PostgreSQL) - `task_serializer / result_serializer`: `"json"` - `worker_prefetch_multiplier: 1` — process one task at a time - beat_schedule: `cleanup_old_uploads` runs every 86400 seconds (daily) **Connects to:** `config.py` (REDIS_URL, DATABASE_URL) --- ### `app/workers/tasks.py` — Task Definitions **What it does:** The three Celery task functions that do the actual background work. **Key functions:** `process_file(self, job_id)` — max_retries=3, bound task: 1. Dispatches to correct processor by `job.file_type` 2. Calls `processor.run(db)` → `(extracted_text, summary)` 3. `chunk_text()` or `chunk_video_segments()` 4. `embed_chunks()` → vectors 5. `add_chunks()` to ChromaDB 6. Updates job to COMPLETED 7. On error: `classify_error()` → retry if retryable (max 3 times, exponential backoff), else FAILED_PERMANENT + push to Redis `geminirag:dead_letter` list `compute_ragas(query_history_id)` — max_retries=2: - Re-embeds question, re-searches ChromaDB - Calls `compute_ragas_scores()` - Saves scores to `QueryHistory.ragas_scores` `cleanup_old_uploads()` — scheduled daily: - Finds COMPLETED/FAILED_PERMANENT jobs older than 7 days - Deletes upload directories from `UPLOAD_DIR` **Helper functions:** - `update_job_state(db, job_id, status, step, ...)` — atomic DB update with logging - `classify_error(exc) → (error_type_str, is_retryable)` — "429"/"quota"/"rate" → RATE_LIMIT (retryable), "400"/"invalid" → INVALID_INPUT (not retryable), else UNKNOWN (retryable) **Connects to:** all processors, all rag modules, `models/db.py`, `celery_app.py`, `observability/logging.py`, `observability/tracing.py` --- ## ADK Agent (`app/agent/`) --- ### `app/agent/agent.py` — Agent Runner **What it does:** Creates and runs the Google ADK conversational agent. **Key contents:** - `AGENT_SYSTEM_PROMPT` — instructs agent on capabilities (process files, check status, query RAG, cite sources) - `_agent = Agent(model="gemini-2.0-flash", tools=[ingest_file, get_job_status, query_rag, list_documents, summarize_document])` - `_session_service = InMemorySessionService()` — conversation history (resets on restart) - `_runner = Runner(app_name="geminirag", agent=_agent, session_service=_session_service)` - `run_agent(message, user_id, session_id?) → dict` — sets user context, calls runner, collects tool_calls and final text, returns `{response, tool_calls_made, session_id, prompt_tokens, completion_tokens}` **Connects to:** `agent/tools.py` (5 tools), `google.adk` library, `observability/logging.py` --- ### `app/agent/tools.py` — MCP Tools **What it does:** Implements the 5 tools the agent can call. **Context:** `_current_user_id: ContextVar[str]` — set by `run_agent()` so tools know which user is calling. **Tools:** | Tool | Input | What it does | Returns | |---|---|---|---| | `ingest_file` | file_path: str | Creates Job, copies file to upload dir, queues process_file | {job_id, status, message} | | `get_job_status` | job_id: str | Looks up Job in DB | {job_id, status, step, chunk_count, error_message} | | `query_rag` | question, job_ids?, use_job_context | Calls engine.query() | {answer, citations, confidence_gate_passed, scores} | | `list_documents` | — | Returns all COMPLETED jobs | {documents: [{job_id, filename, file_type, chunk_count}]} | | `summarize_document` | job_id: str | Returns job.result JSON | {job_id, filename, summary: {...}} | **Connects to:** `rag/engine.py` (query_rag), `workers/tasks.py` (ingest_file queues process_file), `models/db.py` (Job, User), `observability/logging.py` --- ## Evaluation (`app/evaluation/`) --- ### `app/evaluation/ragas_eval.py` — RAGAS Metrics **What it does:** Computes 5 RAG quality metrics using the RAGAS library. **Key functions:** - `get_ragas_llm(settings)` → `ChatGoogleGenerativeAI` — LangChain wrapper for Gemini - `get_ragas_embeddings(settings)` → `GoogleGenerativeAIEmbeddings` - `compute_ragas_scores(question, answer, contexts, ground_truth, settings) → dict` — runs RAGAS evaluation: - Always: Faithfulness, AnswerRelevancy, ContextPrecision - If ground_truth provided: ContextRecall, AnswerCorrectness - Returns `{faithfulness, answer_relevancy, context_precision, context_recall, answer_correctness}` or `{error: str}` **RAGAS target scores:** Faithfulness ≥ 0.80, Context Precision ≥ 0.60 **Connects to:** `config.py` (GEMINI_MODEL, GEMINI_EMBEDDING_MODEL), ragas library, langchain-google-genai --- ## Observability (`app/observability/`) --- ### `app/observability/logging.py` — Structured Logging **What it does:** Configures structlog and provides the `log_llm_call()` helper that writes to both structlog and the `usage_logs` database table. **Key functions:** - `configure_logging()` — structlog setup with JSONRenderer, TimeStamper, StackInfoRenderer - `get_logger()` → structlog bound logger - `log_llm_call(user_id, job_id, endpoint, model, prompt_tokens, completion_tokens, latency_ms, query_text, llm_response_preview, db)` — creates `UsageLog` record in DB + logs to structlog **Used by:** processors (every Gemini call), embedder, rag/engine, agent/tools --- ### `app/observability/tracing.py` — OpenTelemetry **What it does:** Configures OpenTelemetry distributed tracing. **Usage:** `from app.observability.tracing import tracer` then `with tracer.start_as_current_span("process_file") as span: span.set_attribute("job_id", ...)` **Exporter:** stdout (configurable via `OTEL_EXPORTER` env var) **Connected to:** `workers/tasks.py` (`process_file` task uses spans with job_id, file_type, user_id attributes), `main.py` (configures on startup) --- ## Frontend (`frontend/src/`) --- ### `frontend/src/main.tsx` — Entry Point Renders `` into `#root` DOM element. No logic here. --- ### `frontend/src/App.tsx` — Router **What it does:** Sets up React Router with 7 lazy-loaded pages, wrapped in providers. **Structure:** ```tsx }> ``` **Pages (all lazy-loaded):** LoginPage, RegisterPage, UploadPage, QueryPage, AgentPage, JobsPage, AdminPage **Guards:** `` wraps authenticated routes; `` for /admin **Connects to:** All page components, `context/AuthContext.tsx`, `context/ToastContext.tsx`, `components/PrivateRoute.tsx` --- ### `frontend/src/api/client.ts` — HTTP Client **What it does:** Axios instance pre-configured for the API with JWT auth injection. **Key exports:** - `api` — default Axios instance with `baseURL = VITE_API_URL || "http://localhost:8000"` - Request interceptor: attaches `Authorization: Bearer ` from `_getToken()` - Response interceptor: catches 401 → calls `_onUnauthorized()` (logout + redirect) - `setTokenGetter(fn)` / `setUnauthorizedHandler(fn)` — called by AuthContext to inject token source and logout callback **Connects to:** `context/AuthContext.tsx` (calls setters on login/logout), used by every page --- ### `frontend/src/context/AuthContext.tsx` — Auth State **What it does:** Global JWT authentication context. Manages logged-in user state. **Key exports:** - `AuthProvider` — wraps app, persists token in localStorage - `useAuth()` → `{user: AuthUser | null, login(email, password), logout}` - `AuthUser` = `{id, email, role, token}` **Login flow:** POST /auth/login → decode JWT payload (base64 split on ".") → extract {sub, role} → store `AuthUser` in state + localStorage **Connects to:** `api/client.ts` (injects token getter + unauthorized handler), `components/NavBar.tsx`, `components/PrivateRoute.tsx`, all pages --- ### `frontend/src/context/ToastContext.tsx` — Toast Notifications **What it does:** App-wide toast notification system (no external library). **Key exports:** - `ToastProvider` — renders toast container fixed bottom-right, manages queue - `useToastContext()` → `{addToast(message, type: "success"|"error"|"info"|"warning")}` **Connects to:** `hooks/useToast.ts` (uses internal hook), used by all pages for success/error feedback --- ### `frontend/src/hooks/useToast.ts` — Toast Hook **What it does:** Manages the toast array state, auto-removes after 4000ms. **Key exports:** - `useToast()` → `{toasts, addToast(message, type), removeToast(id)}` - Each toast: `{id: number, message: string, type: ToastType}` **Connects to:** `context/ToastContext.tsx` (used internally) --- ### `frontend/src/components/NavBar.tsx` — Navigation Bar **What it does:** Top navigation bar with links to all pages and logout button. **Active link:** Uses `useLocation()` to highlight current page (`border-b-2 border-white`) **Connects to:** `context/AuthContext.tsx` (logout), React Router (useLocation, Link) --- ### `frontend/src/components/PrivateRoute.tsx` — Route Guard **What it does:** Wraps routes that require authentication (or admin role). **Logic:** If `!user` → redirect to `/login`. If `requireAdmin && user.role !== "admin"` → redirect to `/upload`. **Connects to:** `context/AuthContext.tsx` (useAuth) --- ### `frontend/src/pages/LoginPage.tsx` Email + password form → `useAuth().login()` → redirect to `/upload` on success. --- ### `frontend/src/pages/RegisterPage.tsx` Email + password form → `POST /auth/register` → redirect to `/login`. --- ### `frontend/src/pages/UploadPage.tsx` — Upload & Job Management **What it does:** The main file upload page and job status dashboard. **Features:** - Drag-and-drop zone + file input button - `EXT_TO_TYPE` map for client-side validation before upload - Polls `GET /v1/jobs/{job_id}` every 3 seconds until COMPLETED or FAILED - Job cards with color-coded status badges - Expandable card shows document summary (from `GET /v1/documents/{id}/summary`) - Retry button re-uploads file via `POST /v1/files/upload` with stored File reference - Empty state when no jobs exist - Toast notifications on upload success/failure **Connects to:** `api/client.ts`, `context/ToastContext.tsx` --- ### `frontend/src/pages/QueryPage.tsx` — RAG Query Interface **What it does:** The primary user interface for asking questions against uploaded documents. **Features:** - Loads document list from `GET /v1/documents` for document selector - Multi-select documents to scope query (or query all) - Toggle between standard and streaming mode - Standard: `POST /v1/query` → displays full answer when ready - Streaming: `POST /v1/query/stream` via Fetch API + ReadableStream → streams tokens as they arrive - Citation rendering: `[1]`, `[2]` superscripts in answer text → clickable → highlights citation card - RAGAS score badges (green ≥ 0.8, amber 0.6–0.8, red < 0.6) - Copy-to-clipboard button on answer - Query history sidebar **Why Fetch not EventSource:** EventSource API doesn't support POST or custom headers. The streaming endpoint needs both (POST body + JWT Bearer token). **Connects to:** `api/client.ts`, `context/ToastContext.tsx` --- ### `frontend/src/pages/JobsPage.tsx` — Jobs Table **What it does:** Full table view of all jobs with detailed status and re-process capability. **Features:** - `GET /v1/jobs` → table with all columns (filename, type, status, step, chunks, timestamps) - Expandable rows with error details - Re-process button → `POST /v1/jobs/{id}/reprocess` for failed jobs - Horizontal scroll for wide table on mobile **Connects to:** `api/client.ts`, `context/ToastContext.tsx` --- ### `frontend/src/pages/AdminPage.tsx` — Admin Dashboard **What it does:** Three-tab analytics dashboard for administrators only. **Tabs:** 1. **Usage** — today's tokens, avg latency, 7-day token trend chart (Recharts LineChart), endpoint breakdown, per-user table 2. **RAGAS** — metric averages with pass/fail indicators, 7-day RAGAS trend chart, low-scoring queries table (faith < 0.8 or relevance < 0.7) 3. **Users** — all users with query/token/job counts, last_active_at, toggle is_active button (guards self-deactivation) **Connects to:** `api/client.ts` (admin endpoints), `context/AuthContext.tsx` (useAuth for self-deactivation guard), Recharts --- ### `frontend/src/pages/AgentPage.tsx` — Agent Chat **What it does:** Conversational UI for the ADK agent with tool call visibility. **Features:** - Chat messages (user on right, agent on left with avatar) - `POST /v1/agent/chat` with `{message, session_id}` for multi-turn conversation - Left sidebar shows tool calls made in each response (name + icon from TOOL_ICONS map) - Shift+Enter = newline, Enter = submit - Markdown rendering in responses (bold, inline code, citation links) - Token count footer - Clear conversation button (resets session_id) **Tool icons:** ingest_file 📎, get_job_status 🔍, query_rag 💬, list_documents 📋, summarize_document 📄 **Connects to:** `api/client.ts`, `context/ToastContext.tsx` --- ## Scripts (`scripts/`) --- ### `scripts/seed_admin.py` ```bash py scripts/seed_admin.py --email admin@test.com --password Admin1234! ``` Creates an admin user in PostgreSQL. Exits gracefully if email already exists. **Connects to:** `app/config.py`, `app/models/db.py` (User, UserRole), `app/security.py` (hash_password) --- ### `scripts/ragas_baseline.py` ```bash py scripts/ragas_baseline.py --test-set C:/tmp/ragas_test_set.json ``` **Input:** JSON array of `{question, ground_truth, job_id}` (default: `C:/tmp/ragas_test_set.json`) **Process:** Runs RAG engine for each Q&A pair → computes RAGAS scores → prints table → saves to `C:/tmp/ragas_baseline.json` **Output:** Table with columns: Question | Faith | AnswRel | CtxPrec | CtxRec | AnsCorr; plus averages vs targets. **Connects to:** `app/config.py`, `app/rag/engine.py`, `app/evaluation/ragas_eval.py`, `app/models/db.py` --- ### `scripts/download_ragas_datasets.py` ```bash py scripts/download_ragas_datasets.py ``` Downloads 50 Q&A pairs from MS MARCO v1.1 validation and Natural Questions dev via HuggingFace `datasets` (streaming mode — does not download full dataset). **Output:** - `Data set/ragas_eval/ms_marco_samples.json` — 50 × `{question, ground_truth}` - `Data set/ragas_eval/natural_questions_samples.json` — 50 × `{question, ground_truth}` **Next step after downloading:** Add `job_id` to entries matching uploaded documents, then run `ragas_baseline.py`. --- ## Tests (`tests/`) --- ### `tests/conftest.py` **Fixtures:** - `engine` — SQLite `test_geminirag.db`, creates all tables, drops after session - `db` — `Session(engine)` per test - `client` — `TestClient(app)` with dependency override to inject test DB engine, rate limiter reset --- ### `tests/test_api.py` Tests for authentication and file upload routes. Includes: - `test_register_and_login` — register user, login, get token - `test_upload_unsupported_type` — upload .xyz file → 415 error - `test_upload_file_too_large` — upload >500MB → 413 error - `test_get_job_wrong_user_403` — user A cannot see user B's job - `test_login_inactive_user` — deactivated user gets 401 - `test_health` — accepts 200 or 503 (depends on ChromaDB/DB availability in test env) --- ### `tests/test_processors.py` Tests for all 5 processor classes with mocked Gemini API calls. --- ### `tests/test_rag.py` Tests for `chunk_text()`, `embed_chunks()` (mocked), and `vectorstore` operations. --- ### `tests/test_query.py` Tests for `/v1/query` endpoint including confidence gate behavior and citation format. --- ### `tests/test_agent.py` Tests for ADK agent tool invocations and response format. --- ## Top-Level Config Files | File | Purpose | |---|---| | `.env` | Secrets — gitignored. Contains GEMINI_API_KEY, DB/Redis/Secret credentials | | `.env.example` | Template for .env — committed to repo | | `pyproject.toml` | Python 3.11+ project metadata and all dependencies | | `alembic.ini` | Alembic migration config — points to `DATABASE_URL` env var | | `docker-compose.yml` | Dev orchestration — 5 services (api, worker, postgres, redis, chromadb) | | `docker-compose.prod.yml` | Production variant — no --reload, resource limits, ALLOWED_ORIGINS from env | | `Dockerfile` | python:3.11-slim, installs deps, exposes 8000 | | `README.md` | Full project documentation — architecture, setup, API reference, observability | | `HANDOVER.md` | Client handover doc — setup, admin seed, RAGAS baseline, limitations, key files | | `DEMO_SCRIPT.md` | 10-min demo guide with timing, talking points, and "if something goes wrong" table | | `context.md` | Session context for next Claude conversation | | `codebase.md` | This file | --- ## Data Flow Summary ``` Browser (localhost:5173) │ ├── POST /auth/login ──────────────────→ api/auth.py → security.py → DB (users) │ ← JWT token │ ├── POST /v1/files/upload ─────────────→ api/files.py → DB (jobs) → Redis → Celery │ (multipart, JWT) ← {job_id, status: "PENDING"} │ │ Celery worker: process_file() │ → processors/*.py → Gemini API │ → rag/chunker.py │ → rag/embedder.py → Gemini embeddings │ → rag/vectorstore.py → ChromaDB │ → DB (jobs.status = COMPLETED) │ ├── GET /v1/jobs/{id} (poll) ──────────→ api/jobs.py → DB (jobs) │ ← {status, step, chunk_count} │ ├── POST /v1/query ────────────────────→ api/query.py → rag/engine.py │ (JWT, {question, job_ids?}) → embedder.py → Gemini │ → vectorstore.py → ChromaDB │ → Gemini RAG call │ → DB (query_history) │ → Redis → Celery (compute_ragas) │ ← {answer, citations, scores} │ └── POST /v1/agent/chat ─────────────→ api/agent.py → agent/agent.py (ADK) (JWT, {message, session_id}) → agent/tools.py (5 tools) ← {response, tool_calls_made} ``` --- ## Import Graph (simplified) ``` config.py ←────────────── everything models/db.py ←─────────── api/, workers/, scripts/, agent/tools.py security.py ←──────────── api/auth.py, deps.py deps.py ←──────────────── all api/ route handlers observability/logging.py ← processors/, rag/embedder.py, rag/engine.py, agent/tools.py rag/engine.py ←────────── api/query.py, agent/tools.py, scripts/ragas_baseline.py rag/vectorstore.py ←───── rag/engine.py, workers/tasks.py rag/embedder.py ←──────── workers/tasks.py, rag/engine.py (via compute_ragas) processors/base.py ←───── processors/pdf.py, docx_proc.py, xlsx_proc.py, image.py, video.py workers/tasks.py ←─────── api/files.py (.delay()), api/jobs.py (.delay()), rag/engine.py (.delay()) evaluation/ragas_eval.py ← workers/tasks.py (compute_ragas), scripts/ragas_baseline.py agent/tools.py ←───────── agent/agent.py ```