A RAG-based medicine information assistant combining official Pakistani drug registration data with FDA clinical reference data — built with safety as an architectural constraint, not a disclaimer.
⚠️ This is a reference tool, not a medical advice replacement. It never gives personalized dosing advice or tells someone whether they should take a medicine. That boundary is enforced architecturally — the unsafe path is structurally incapable of reaching generation, not just discouraged by a prompt (see Safety design).
- Plain-language drug lookup — "what is paracetamol used for?" answered from real registration + clinical data, not model guesswork
- Generic equivalent finder — expensive brand names matched to their active ingredient, surfacing cheaper registered alternatives
- Official side-effect / interaction reference — sourced from FDA label data, grounded and cited, not hallucinated
- A hard safety gate — personalized-advice questions never reach the LLM generation step at all; they're intercepted before retrieval even runs
- Why this project
- Architecture
- Data investigation
- A safety bug caught during QA
- API examples
- Known limitations
- Running it locally
- Deployment note
- Tech stack
Built to fill a real gap: prior projects (crop price forecasting, used-car price prediction, AQI forecasting) were all classical ML/data pipelines with no LLM/GenAI component, despite targeting ML/AI Engineer roles. This is the first RAG project — built end-to-end, including a full data-access investigation, a caught-and-fixed safety bug, and a working local web app.
DRAP scraper ──┐
├──> Composition parser ──> openFDA matcher ──> Chroma index
openFDA API ───┘ │
▼
User query ──> Safety classifier ──┬──> [advice-seeking] ──> canned refusal
├──> [out-of-scope] ──> canned refusal
└──> [lookup] ──> retrieve ──> Groq generate ──> answer
Data pipeline (src/scraper, src/matching, src/indexing):
- Scrape DRAP's registered-products database (reverse-engineered two undocumented endpoints via browser DevTools — see Data investigation) for a curated list of ~80 common generic medicines.
- Parse each drug's free-text composition field into structured ingredients.
- Match each active ingredient against openFDA's drug label API to pull indications, warnings, contraindications, and drug interactions.
- Chunk each drug record by content type (overview / warnings / interactions) and embed into a local ChromaDB vector store.
Query pipeline (src/safety, src/rag, src/api):
- Every query first passes through a safety classifier — a rule-based regex pass (high-recall, catches personalized-advice phrasing like "should I take X") followed by an LLM classifier (Groq) for the lookup-vs-out-of-scope distinction.
- Personalized-advice and out-of-scope queries never reach retrieval or generation — they return a fixed canned response. This is a hard architectural gate, not a prompt instruction the model could ignore.
- Lookup queries retrieve relevant chunks from ChromaDB (with keyword pre-filtering to bias toward the actually-named drug) and generate a grounded answer via Groq, constrained to only use retrieved context, with a safety footer appended to every response regardless of content.
Before writing any code, I investigated whether DRAP's data was actually accessible (structured downloads vs. scraping needed) — the same approach used on a prior project (AMIS Punjab mandi price scraping):
- DRAP's registry (
eapp.dra.gov.pk) has no bulk download. Found two undocumented endpoints via DevTools: a GET autocomplete search and a POST detail-by-registration-number endpoint. - DRAP's separate MRP (price) portal turned out to be a broken OData/IIS service on their end (confirmed via a raw ASP.NET config error exposing their server's file path) — still down after multiple days of checking. Pricing/affordability ranking is not implemented in v1 as a result; equivalents are listed unranked. This is an external outage, not a gap in the scraping approach.
- DRAP's data has no indication/side-effect field at all — it's registration metadata only (composition, company, dates). openFDA fills that gap, joined on active ingredient name.
During manual testing, an answer about ibuprofen's side effects included symptoms like "new primary malignancies" and "interstitial lung disease" — warnings that don't belong to ibuprofen at all. Investigation traced this to the openFDA fuzzy-matching fallback: it had matched an Ibuprofen product to Trametinib, an unrelated chemotherapy drug, because openFDA's non-exact substance search returned a loosely-relevant result with no verification step.
A full audit of the dataset found 105 of 564 matches (19%) had the same class of problem — including Cetirizine matched to a homeopathic combination remedy across 4 separate records, and Ranitidine matched to Glimepiride (an unrelated diabetes drug).
Fix: added a plausibility guard rejecting any match with no name overlap between the queried ingredient and the matched substance, then re-validated every existing match against it. Invalidated matches become "no data available" rather than wrong data — a deliberate trade-off, since for a medical-information tool, an honest gap is safe and a wrong answer is not.
# Ask a factual question — retrieves + generates a grounded answer
curl -X POST http://localhost:8000/ask \
-H "Content-Type: application/json" \
-d '{"query": "what are the side effects of ibuprofen?"}'
# Advice-seeking question — intercepted before retrieval/generation runs
curl -X POST http://localhost:8000/ask \
-H "Content-Type: application/json" \
-d '{"query": "should I take ibuprofen for my headache?"}'
# Find generic equivalents by active ingredient
curl http://localhost:8000/equivalents/ACETAMINOPHEN- Curated dataset, not exhaustive: covers ~80 common generic medicines
(~1,270 DRAP records) rather than DRAP's full ~130,000-registration
catalog, due to a hard project deadline. A brute-force full scraper exists
(
drap_scraper.py) and can be run to completion separately (~38 hours at a polite rate limit). - No price-based ranking of generic equivalents (DRAP's MRP portal is currently non-functional server-side — see Data investigation).
- ~19% of ingredient matches were removed after the safety audit above rather than risk serving unverified clinical data; those drugs return DRAP registration info only, no indication/warnings/interactions.
- Retrieval isn't always precise — vector search occasionally ranks unrelated chunks highly for a given query. The generation step is constrained to only answer from retrieved context, so this produces an honest "I don't have that information" rather than a wrong answer, but it does mean some in-scope questions get an unnecessary non-answer.
- No live public deployment — see Deployment note.
If I kept building: full-catalog scraping, price-ranked equivalents once DRAP's MRP service is back up, a second matching pass for the ~8 genuinely unmatched ingredients, and a hosted deployment once a card-free platform with enough RAM for the embedding model is worth the search.
This project runs entirely on your own machine — clone it, follow these steps, and you'll have a working local web app (FastAPI backend + browser frontend).
git clone https://github.com/MuhammadAliEjaz1/rxguard.git
cd rxguard
pip install -r requirements.txt
cp .env.example .env # add your free Groq API key from console.groq.comStart the backend:
cd src/api
uvicorn main:app --reload --port 8000Confirm it's running at http://localhost:8000/docs.
Open the frontend:
Just open frontend/index.html directly in your browser (double-click it, or
right-click → Open with → your browser). It's a single static file with no
build step, already configured to talk to http://localhost:8000. If your
browser has any issue loading it as a local file, alternatively serve it:
cd frontend
python -m http.server 3000
# then open http://localhost:3000(Optional) Rebuild the data pipeline from scratch — the repo already includes the pre-built dataset and vector index, so this is only needed if you want to reproduce the scraping/matching process yourself:
cd src/scraper && python curated_scraper.py
cd ../matching && python match_openfda.py && python revalidate_matches.py
cd ../indexing && python build_index.pyLive hosting was attempted but intentionally not pursued to completion: Hugging Face Spaces moved Docker deployment behind a paywall shortly before this was built, and both Render and Koyeb's free tiers required credit card verification for this account despite documentation stating otherwise (both platforms use risk-based fraud detection that can trigger this regardless of actual usage). Given a hard deadline, the deliberate choice was a robust, fully-documented local setup over gambling further time on unpredictable third-party infrastructure policies — a real engineering trade-off, not an oversight.
Backend: Python, FastAPI, ChromaDB (vector store), sentence-transformers (local embeddings, no API cost), Groq (Llama-based generation, free tier) Data pipeline: BeautifulSoup, requests Frontend: vanilla HTML/CSS/JS (no build tooling)
MIT — see LICENSE.
Built by Muhammad Ali Ejaz · BS Data Science, The Islamia University of Bahawalpur



