An agentic RAG system for querying FDA drug label information. Ask a question about a medicine and get a cited, self-verified answer pulled directly from official FDA drug labels.
Built as part of an AI engineering internship assignment focused on RAG pipeline design, vector storage, agentic graph architecture, and LLM integration.
| Component | Tool |
|---|---|
| Data | DailyMed FDA drug labels (PDFs) |
| Embeddings | Google Gemini (gemini-embedding-001, 3072d) |
| Vector Store | pgvector + PostgreSQL 16 (Docker) |
| LLM | Ollama llama3.2 (local) |
| Orchestration | LangGraph (Corrective RAG agent) |
| API | FastAPI |
| Frontend | React + Tailwind CSS (Vite) |
| Package Manager | uv |
Most RAG tutorials retrieve once and generate — a fixed pipeline with no error correction. PillWise's pw-agent.py uses a LangGraph state machine that can detect a poor retrieval and self-correct before answering:
question → retrieve → grade ─── relevant? ──→ generate → answer
│
└── not relevant ──→ rewrite → retrieve (retry, capped at 2)
- retrieve — embeds the question, runs pgvector similarity search
- grade — a lightweight LLM call checks whether retrieved chunks actually answer the question
- rewrite — if not, reformulates the question (e.g. brand name → generic name, since DailyMed indexes by generic names) and retries
- generate — produces the final cited answer once context is judged sufficient
The API response includes a retries field, so the number of self-corrections is visible per request — e.g. a query for "Advil" (a brand name absent from the corpus) triggers a rewrite to "ibuprofen" before answering, while an exact match like "ibuprofen" answers on the first pass with zero retries.
| File | Description |
|---|---|
01_scraper.ipynb |
DailyMed automated PDF downloader |
02_load_and_chunk.ipynb |
PDF text extraction and chunking strategy comparison |
03_embeddings.ipynb |
Gemini embedding pipeline |
04_vectorstore.ipynb |
pgvector setup and chunk storage |
05_retrieval.ipynb |
Semantic search and answer generation |
pw-api.py |
Original fixed-chain FastAPI endpoint (retrieve → generate) |
pw-agent.py |
LangGraph Corrective RAG agent + FastAPI endpoint (current primary system) |
insert_remaining_chunks.py |
Production script to bulk-insert remaining medicines, with rate-limit retry handling |
frontend/ |
React + Tailwind UI — search, citation-linked answers, source cards, agent trace |
data/raw_pdfs/ |
Downloaded drug label PDFs organized by medicine |
Prerequisites: Docker Desktop, Ollama with llama3.2, uv, Node.js, Gemini API key (free tier works)
1. Clone and install backend
git clone https://github.com/arinova2701/PillWise
cd PillWise
uv sync2. Create a .env file in the project root
GEMINI_API_KEY=your_key_here
3. Start pgvector in Docker
docker run -d --name pillwise-pgvector -e POSTGRES_USER=pillwise -e POSTGRES_PASSWORD=pillwise123 -e POSTGRES_DB=pillwise -p 5432:5432 pgvector/pgvector:pg16docker exec -it pillwise-pgvector psql -U pillwise -d pillwise -c "CREATE EXTENSION IF NOT EXISTS vector;"4. Pull llama3.2
ollama pull llama3.25. Run notebooks 01 through 05 in order to download PDFs, chunk them, generate embeddings, store in pgvector, and verify retrieval. Use insert_remaining_chunks.py for bulk-inserting additional medicines beyond the initial set.
6. Start the agent API
uv run uvicorn pw-agent:app --reloadAPI runs at http://localhost:8000
7. Run the frontend
cd frontend
npm install
npm run devFrontend runs at http://localhost:5173
POST /ask
Send a question and get a self-verified, cited answer from the drug label corpus.
Request:
{
"question": "what are the warnings for ibuprofen?",
"top_k": 5
}Response:
{
"answer": "According to [3] WARNINGS, the warnings for ibuprofen include cardiovascular thrombotic events [3][1] and severe heart failure precautions [5]...",
"sources": [
{
"drug": "ibuprofen",
"chunk": "Ibuprofen may cause a severe allergic reaction, especially in people allergic to aspirin...",
"similarity": 0.8164
}
],
"retries": 0
}retries reflects how many times the agent rewrote the question and re-retrieved before generating — 0 means the first retrieval was judged sufficient.
GET /health
{"status": "ok"}- PDFs are downloaded from DailyMed using their public API (3 per medicine, 10 medicines, 30 total)
- Text is extracted using pymupdf and split into 500 char chunks with 50 char overlap
- Each chunk is embedded using Gemini producing a 3072 dimensional vector
- Vectors are stored in pgvector alongside chunk text and metadata
- At query time, the LangGraph agent embeds the question and retrieves the top 5 most relevant chunks via cosine similarity
- A grading step checks relevance; if poor, the question is rewritten and retrieval is retried (capped at 2 retries)
- Once context is sufficient, chunks are passed to llama3.2 (local, via Ollama) to generate a cited answer
- The frontend renders the answer with inline citation badges, source cards, and the agent's self-correction trace
Two strategies compared across 30 documents:
| Metric | Fixed Size | Sentence Based |
|---|---|---|
| Total chunks | 3213 | 1626 |
| Avg chunk size | 497 chars | 883 chars |
| Std deviation | 29 | 499 |
| Min | 6 | 46 |
| Max | 500 | 6904 |
Fixed size chunking with overlap performs better for FDA drug labels. Sentence chunking struggles because bullet points lack sentence-ending punctuation, producing chunks up to 6904 characters which are too large for effective retrieval.
Inserted (6 of 10): acetaminophen, ibuprofen, amoxicillin, aspirin, atorvastatin, azithromycin Downloaded, pending insertion: cetirizine, metformin, omeprazole, pantoprazole
- Gemini's free-tier embedding quota (1,000 requests/day) means bulk insertion of all ~3,213 chunks across 10 medicines spans multiple days. Currently 1,609 chunks stored across 6 medicines.
- Embedding models cannot be mixed within one vector store — all chunks and queries must use the same model (
gemini-embedding-001) to remain comparable in the same vector space. - Bullet characters from PDFs not yet stripped before storage.
- LLM runs locally so response time depends on hardware.
- Frontend currently targets
localhost:8000directly; not yet configured for a deployed backend URL.
- Finish inserting remaining 4 medicines (~1,604 chunks)
- Integrate LangSmith for agent tracing and evaluation
- Deploy API to Railway
- Update frontend to point at deployed API URL
- Add Indian medicine data via 1mg scraper (see
1mg-scraper-project-brief.md— separate project, in planning) - Clean bullet characters from chunk text