Upload any PDF → ask questions → get answers powered by Gemini 2.0 Flash.
Embeddings via Google text-embedding-004. Vector store via ChromaDB (local).
Zero OpenAI. Zero Anthropic. One API key.
PDF Upload
└─► pypdf (text extraction)
└─► RecursiveCharacterTextSplitter (chunks, overlap=150)
└─► text-embedding-004 (Google Gemini API)
└─► ChromaDB (cosine similarity, persistent)
└─► User Question ──► text-embedding-004 (query embed)
└─► top-5 chunks retrieved
└─► gemini-2.0-flash (strict RAG)
└─► Answer
pdf-chat/
├── backend/
│ ├── main.py # FastAPI: /upload /ask /session /health
│ ├── config.py # All settings via .env
│ ├── requirements.txt # google-generativeai, chromadb, pypdf, fastapi
│ ├── .env.example
│ └── services/
│ ├── pdf_processor.py # pypdf + RecursiveCharacterTextSplitter
│ ├── vector_store.py # text-embedding-004 + ChromaDB
│ └── llm_service.py # gemini-2.0-flash, strict RAG prompt
└── frontend/
├── index.html
├── package.json
├── vite.config.js # Proxies /upload /ask → localhost:8000
├── tailwind.config.js
└── src/
├── App.jsx # Root layout
├── main.jsx
├── index.css
├── hooks/useChat.js # All state management
├── utils/api.js # Backend API client (XHR for progress)
└── components/
├── Sidebar.jsx
├── UploadZone.jsx
├── ChatWindow.jsx
├── ChatInput.jsx
├── MessageBubble.jsx
├── TypingIndicator.jsx
└── ErrorBanner.jsx
| Tool | Version |
|---|---|
| Python | 3.10+ |
| Node.js | 18+ |
| Gemini API key | Free at aistudio.google.com/apikey |
Go to https://aistudio.google.com/apikey → Create API key → Copy it.
cd pdf-chat/backend
# Create virtual environment
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
# Install dependencies
pip install -r requirements.txt
# Configure
cp .env.example .env
# Open .env and set: GEMINI_API_KEY=AIza_your_key_here
# Start server
uvicorn main:app --reload --host 0.0.0.0 --port 8000You should see:
INFO: Uvicorn running on http://0.0.0.0:8000
INFO: ChromaDB initialised.
cd pdf-chat/frontend
npm install
npm run dev
# → Open http://localhost:5173Request: multipart/form-data, field file (PDF only, max 50 MB)
Response:
{
"session_id": "3f2a...",
"filename": "report.pdf",
"chunks": 134,
"message": "Processed successfully — 134 chunks indexed."
}Request:
{ "session_id": "3f2a...", "question": "What is the conclusion?" }Response:
{ "answer": "The conclusion states that..." }Removes the ChromaDB collection for that session.
{ "status": "ok", "llm": "gemini-2.0-flash" }| Variable | Default | Description |
|---|---|---|
GEMINI_API_KEY |
(required) | From Google AI Studio |
GEMINI_LLM_MODEL |
gemini-2.0-flash |
For answering questions |
GEMINI_EMBEDDING_MODEL |
text-embedding-004 |
For chunk + query embeddings |
CHUNK_SIZE |
800 |
Characters per chunk |
CHUNK_OVERLAP |
150 |
Overlap between adjacent chunks |
TOP_K_CHUNKS |
5 |
Chunks retrieved per query |
MAX_FILE_SIZE_MB |
50 |
Upload size limit |
MAX_CONTEXT_CHARS |
12000 |
Max chars passed to LLM per query |
CHROMA_PERSIST_DIR |
./chroma_db |
ChromaDB storage path |
| Component | Choice | Reason |
|---|---|---|
| LLM | gemini-2.0-flash |
Fast, cheap, excellent instruction following |
| Embeddings | text-embedding-004 |
Google's best embedding model, 768-dim, same API key |
| Vector DB | ChromaDB | Local, persistent, no extra service needed |
| Chunking | RecursiveCharacterTextSplitter | Respects sentence/paragraph boundaries |
| Overlap | 150 chars | Prevents answers split across chunk boundaries |
- Image-only / scanned PDFs — pypdf cannot extract text from scanned images. The PDF must have embedded text.
- Gemini free tier rate limits — if you upload a very large PDF (500+ chunks), the embedding step may hit rate limits. Add a short
time.sleep(1)between batches invector_store.pyif needed. - Session memory — sessions are tied to the FastAPI process. Restarting the server loses the in-memory session mapping, but ChromaDB data persists on disk.