An agentic research assistant. Ask a complex question and Docent decomposes it into sub-questions, retrieves from your uploaded documents and the live web, reasons across the sources, and streams back a structured, cited answer you can trace to its origin.
Not a "chat with your PDF" tool. The differentiator is multi-step agentic reasoning over multiple sources (documents + web) with inline citations — built on a hand-rolled RAG pipeline (no LangChain / LlamaIndex) to keep the mechanics explicit.
Live: app · frontend on Vercel, backend on Render (free tier — the first request after idle is slow while it wakes).
Next.js frontend → (HTTP / SSE) → FastAPI backend → Pinecone + Voyage + Claude + Tavily
The frontend and backend are two separate services that talk over HTTP — a deliberate choice (over Next.js API routes) to exercise the real two-service pattern.
Query flow (agentic mode):
- Plan — Claude decomposes the question into 2–4 sub-questions and routes each to
docs,web, orboth(structured outputs). - Retrieve — per sub-question: semantic search in Pinecone (Voyage
voyage-3embeddings, cosine) and/or a Tavily web search. Results are merged, deduped, and ranked. - Synthesize — Claude writes one grounded answer over all context, citing sources
inline as
[n]. - Stream — plan → sources → answer tokens stream to the UI over SSE.
Ingest flow: upload a PDF → extract text → chunk (1000 chars, 200 overlap) → embed → upsert vectors + metadata to Pinecone.
| Layer | Choice |
|---|---|
| Frontend | Next.js (App Router) + TypeScript + Tailwind CSS |
| Backend | Python + FastAPI (async, Pydantic) |
| LLM | Claude (Anthropic API) |
| Embeddings | Voyage AI (voyage-3, 1024-dim) |
| Vector DB | Pinecone (serverless, cosine) |
| Web search | Tavily |
| RAG logic | Plain Python — no LangChain / LlamaIndex |
| Deploy | Vercel (frontend) + Render (backend) |
- 📄 Document library — upload, list, and delete PDFs (deletes also purge the vectors).
- 🔎 Semantic search — inspect the raw retrieval layer: chunks ranked by cosine similarity.
- 🧠 Agentic answers — decompose → route → retrieve → synthesize, streamed token-by-token.
- 🌐 Multi-source — the agent decides per sub-question whether to use documents, the web, or both.
- 🔗 Citations — every claim carries an
[n]marker linked to a numbered source (📄 doc / 🌐 web). - 💬 Chat history — follow-ups reuse recent turns so pronouns/references resolve.
- 🌗 Light / dark theme with no flash on load.
- 🔐 Google sign-in (optional) — Auth.js/NextAuth v5; gates
/appand shows a user menu when configured, fully dormant otherwise.
frontend/ Next.js app (App Router). Landing at "/", the app at "/app".
backend/ FastAPI service: RAG pipeline, endpoints, chunking, vector store, agent.
Key backend modules: main.py (endpoints), chunking.py, embeddings.py (Voyage),
vector_store.py (Pinecone), rag.py (single-step RAG), agent.py (agentic multi-source),
web_search.py (Tavily), config.py (env + readiness checks).
You need two terminals. API keys live only in the backend environment.
Backend (from backend/):
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
cp .env.example .env # then fill in the keys below
uvicorn main:app --reload --port 8000Frontend (from frontend/):
npm install
# frontend/.env.local:
# NEXT_PUBLIC_API_URL=http://localhost:8000
npm run dev # http://localhost:3000| Variable | Purpose |
|---|---|
ANTHROPIC_API_KEY |
Claude (planning + synthesis) |
VOYAGE_API_KEY |
Embeddings |
PINECONE_API_KEY |
Vector store |
TAVILY_API_KEY |
Web search (optional — degrades to docs-only) |
FRONTEND_ORIGINS |
Comma-separated CORS origins (default http://localhost:3000) |
ANSWER_MODEL |
Override the answer model (e.g. claude-haiku-4-5) |
BACKEND_API_SECRET |
Optional. When set, protected endpoints require X-API-Key (see below) |
RATE_LIMIT_* |
Optional per-IP rate limits (RATE_LIMIT_ASK/UPLOAD/SEARCH/DEFAULT) |
The app degrades gracefully without keys: upload + chunking still work, and /status
drives an "add keys" banner while embedding/search/answer endpoints return 503.
Auth is off until you add Google OAuth credentials — the app is open to everyone until then.
To enable it, set these in frontend/.env.local (server-side only):
| Variable | Purpose |
|---|---|
AUTH_SECRET |
Session-cookie encryption key (openssl rand -base64 32) |
AUTH_GOOGLE_ID |
OAuth client ID (Google Cloud Console → Credentials) |
AUTH_GOOGLE_SECRET |
OAuth client secret |
Authorized redirect URI in Google Console: http://localhost:3000/api/auth/callback/google
(and the Vercel URL for prod). Once set, /app requires sign-in and the sidebar shows a
user menu. (Protecting the FastAPI backend with the session token is a planned follow-up.)
| Method | Endpoint | Purpose |
|---|---|---|
| GET | /health |
Liveness |
| GET | /status |
Which capabilities are configured (drives the UI) |
| POST | /upload |
PDF → extract → chunk → embed → upsert |
| GET | /documents |
List uploaded documents |
| DELETE | /documents/{id} |
Remove a document and its vectors |
| POST | /search |
Semantic search over chunks |
| POST | /ask/stream |
Single-step RAG, streamed (SSE) |
| POST | /ask/agentic |
Agentic multi-source answer, streamed (SSE) |
- Rate limiting is always on: per-IP limits (via
slowapi) on the expensive endpoints, so a public URL can't be hammered to burn API keys. Over-limit →429. - Optional API-key gate: set
BACKEND_API_SECRETand the protected endpoints require anX-API-Keyheader. The Next.js proxy injects it server-side, so the secret never reaches the browser. Unset → the backend is open (unchanged). To enable in prod: setBACKEND_API_SECRETon the backend host and route the frontend through the proxy (NEXT_PUBLIC_API_URL=/proxy+BACKEND_PROXY_TARGET).
Backend tests use pytest + FastAPI's TestClient and run without any API keys
(protected endpoints reach their "keys not configured" branch):
cd backend
python -m pytest # chunking, config, health/status, upload validation, gate, rate limitsThe RAG pipeline is built from scratch so every step — chunking, embedding, retrieval, decomposition, source routing, citation numbering, synthesis — is explicit and inspectable, rather than hidden behind a framework abstraction.