Skip to content

Mahad-Ghauri/MedExplain--AI

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

18 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

MedExplain logo

MedExplain AI

Turns a lab report into a plain-English explanation, an abnormality flag, a trend over time and lets you ask follow-up questions about it.

An educational interpretation layer not a diagnostic tool.


Important

MedExplain does not diagnose conditions, does not recommend medication or treatment, and does not claim clinical accuracy. It always defers to "consult a doctor." Every screen carries a persistent disclaimer banner, and every LLM call including multi-turn chat is hard-constrained to explanation only.

What it does

Upload / enter a report (CBC or Lipid Profile)
        │
        ▼
 extract values (OCR or manual)
        │
        ▼
 compare to reference ranges (plain Python rule engine)
        │
        ▼
 flag High / Low / Normal
        │
        ▼
 LLM explains flagged values in plain language
        │
        ▼
 dashboard shows results + trend over time
        │
        ▼
 ask a follow-up question about any flagged value

Screenshots

Dashboard flagged results, plain-language explanation, follow-up chat entry point
Dashboard
Follow-up chat, scoped to one value Second report type Lipid Profile
Chat Lipid Profile
Upload drag & drop Upload OCR reads a real report photo
Upload Upload OCR result
History real trend line with reference-range band
History
Mobile responsive layout
Responsive down to mobile sidebar collapses to a bottom tab bar

Architecture

flowchart TD
    subgraph Client["Frontend: Vite + React (Netlify)"]
        Upload[Upload page]
        Dashboard[Dashboard / manual entry]
        History[History & Trends]
        Chat[Chat modal per flagged value]
    end

    subgraph Server["Backend FastAPI"]
        API[REST API]
        Rules[Rule engine: plain Python, no LLM]
        OCR[OCR pipeline Tesseract + pdfplumber]
        LLMClient[LLM client]
    end

    DB[(Postgres / Neon report_types, parameters, reference_ranges,
saved_reports, saved_report_values, chat_messages)]
    Groq[[Groq API Llama 3.3 70B]]

    Upload -- "PDF / image + report type" --> API
    API --> OCR
    OCR -- "extracted values" --> Upload

    Dashboard -- "sex, age, values, report type" --> API
    API --> Rules
    Rules -- "reads reference ranges" --> DB
    Rules -- "Normal/High/Low + range + saved_value_id" --> Dashboard
    API -- "persists report" --> DB

    Dashboard -- "flagged {name,value,status} only" --> API
    API --> LLMClient
    LLMClient --> Groq
    Groq --> LLMClient --> Dashboard

    Chat -- "saved_value_id + message" --> API
    API -- "rebuilds context from DB row, never trusts client" --> LLMClient
    API -- "stores both sides of the conversation" --> DB

    History -- "device_id" --> API
    API -- "trend series" --> DB
    DB --> History
Loading

Data-minimization boundary: the LLM only ever receives {parameter name, value, status} for values already flagged abnormal by the deterministic rule engine never a raw report, never patient identifiers. This is enforced in code (backend/llm.py), not just by convention. The chat feature keeps this boundary too: every reply is generated from a system prompt rebuilt fresh, server-side, from the database row never from client-supplied context, and never carrying the raw report into the conversation.

Why a rule engine and not an LLM for flagging

Comparing a number to a range is deterministic. Doing it in plain Python means the flag is always reproducible and auditable no hallucination risk on the one thing that must never be wrong. The LLM's only job is turning an already-correct flag into a human-readable sentence, or answering a bounded follow-up question about it.


Tech stack

Layer Choice Why
Frontend Vite + React (JS) + Tailwind v4 Lightweight; Tailwind v4's CSS-based theme kept the "calm, pastel, clinical-but-warm" design system in one file
Routing react-router-dom Sidebar/bottom-tab navigation across Upload / Dashboard / History / Settings
Icons lucide-react Consistent icon set across nav, report-type tabs, and empty states
Charts Recharts Trend line + shaded reference-range band
Backend FastAPI (Python) Async-friendly, typed, minimal boilerplate for a small API surface
ORM SQLAlchemy Models mirror the raw SQL schema exactly no drift
Database PostgreSQL (Neon, free tier) Normalized schema designed to extend to new report types without migrations
OCR Tesseract + pdfplumber Tesseract for images, pdfplumber for text-layer PDFs; per-report-type parser registry (no PaddleOCR too heavy for free-tier hosting)
LLM Groq (Llama 3.3 70B) Free tier with generous rate limits, fast, more than capable for short explanations and bounded follow-up chat
Auth Anonymous device ID (localStorage) No login screen yet; forward-compatible real auth later adds a user_id column without changing the reports/chat schema
Frontend hosting Netlify netlify.toml at repo root drives the build (base=frontend)
Backend hosting Railway / Render (planned) Currently developed against localhost; see Roadmap

Why Groq instead of Claude/OpenAI

The original plan called for a paid cloud LLM API. Budget constraints during development made that impractical, so both the one-shot explanation layer and the follow-up chat run on Groq's free tier instead. The prompt contracts are provider-agnostic swapping back to Claude later is a one-file change in backend/llm.py.


Project structure

MedExplain--AI/
├── frontend/                    # Vite + React app
│   ├── src/
│   │   ├── pages/                # UploadPage, ResultsPage, HistoryPage, SettingsPage
│   │   ├── components/           # Layout, DisclaimerBanner, ParameterCard, StatusPill, ChatModal
│   │   ├── deviceId.js            # anonymous localStorage device identity
│   │   └── reportTypeIcons.jsx    # icon lookup per report type (CBC, Lipid Profile, ...)
│   └── netlify.toml config lives at repo root
├── backend/                     # FastAPI app
│   ├── main.py                    # routes
│   ├── models.py                  # SQLAlchemy models (mirror schema.sql)
│   ├── schemas.py                 # Pydantic request/response models
│   ├── rule_engine.py             # deterministic Normal/High/Low flagging
│   ├── ocr.py                     # Tesseract/pdfplumber extraction, per-report-type parser registry
│   ├── llm.py                     # Groq client one-shot explanation + bounded follow-up chat
│   └── database.py
├── database/
│   ├── schema.sql                 # source-of-truth DDL
│   └── seed/
│       ├── 001_cbc.sql            # CBC reference ranges
│       └── 002_lipid_profile.sql  # Lipid Profile reference ranges
├── docs/
│   ├── implementation_plan.md     # living phase-by-phase status doc
│   └── screenshots/
└── netlify.toml

Database design

Three normalized tables carry the reference knowledge base:

report_types  ──┬──▶  parameters  ──┬──▶  reference_ranges
 (cbc, lipid_    │    (hemoglobin,   │    (sliced by sex + age band)
  profile)       │     ldl, ...)     │

Adding a new report type means inserting rows never a schema change (proven in Phase 9 by adding Lipid Profile with zero backend endpoint changes). Reference ranges vary by sex and age from day one (sex, age_min_years, age_max_years columns), not retrofitted later.

History and chat are stored separately, both keyed off the same saved value:

saved_reports ──▶ saved_report_values ──▶ chat_messages
 (device_id,       (parameter_id, value,    (role, content,
  sex, age,          status, range_low,       created_at)
  created_at)        range_high)

A saved_report_values row is the anchor for a follow-up conversation chat_messages.saved_report_value_id ties every message back to one specific flagged value from one specific report, never a whole report or an open-ended chat.


Getting started

Prerequisites

  • Node.js, Python 3.12 (3.14 currently lacks pydantic-core wheels use 3.12)
  • Tesseract (brew install tesseract)
  • A Neon Postgres project
  • A Groq API key (free)

Database

psql "$DATABASE_URL" -f database/schema.sql
psql "$DATABASE_URL" -f database/seed/001_cbc.sql
psql "$DATABASE_URL" -f database/seed/002_lipid_profile.sql

Backend

cd backend
python3.12 -m venv venv && source venv/bin/activate
pip install -r requirements.txt
cp .env.example .env   # fill in DATABASE_URL, GROQ_API_KEY, JWT_SECRET, CORS_ORIGINS
uvicorn main:app --reload --port 8001

Frontend

cd frontend
npm install
cp .env.example .env.local   # VITE_API_URL=http://localhost:8001
npm run dev

API reference

Method Path Purpose
GET /health Liveness check
GET /report-types List available report types (drives report-type selectors)
GET /report-types/{code}/parameters List parameters for a report type (drives the entry form)
POST /reports Submit values → flagged Normal/High/Low; persists if device_id present, returns a saved_value_id per flagged parameter
POST /ocr/extract Upload PDF/image + report type → extracted {parameter_code, value} matches
POST /explain Flagged values → plain-English explanations (Groq)
GET /history?device_id=...&report_type=... Per-parameter trend series for the History page
POST /chat Send a follow-up message about one flagged value (saved_report_value_id); returns the reply and full thread
GET /chat/{saved_report_value_id} Resume an existing follow-up conversation

Roadmap

Up next

  • Deploy the backend (Railway/Render) currently developed against localhos. Netlify frontend is already live but has nothing to talk to until this lands.
  • Real authentication (JWT signup/login), replacing the anonymous device ID the schema was designed so this only adds a user_id column, no migration of existing data.

Later / v3+

  • PDF export of the results summary
  • Multilingual support (Urdu, Arabic, French)
  • Dark mode
  • "Questions to ask your doctor" generator
  • Additional report types: LFT, KFT, Thyroid, Urine Analysis
  • Rate limiting on /explain, /ocr/extract, and /chat (currently open endpoints fine for a local/portfolio demo, a real gap before a public deploy)

Explicitly avoided (and why)

  • LSTM/transformer trend prediction with fewer than 10 data points per user, this is statistically meaningless. A straight line through actual points, honestly labeled "trend, not prediction," is more truthful than a model dressed up to look predictive.
  • PaddleOCR too heavy for free tier hosting; caused OOM/timeouts in early evaluation.
  • Ollama in production free hosting tiers don't have the RAM to run a local LLM; a cloud API is a hard requirement here.
  • Sending raw reports, patient identifiers, or whole-report context to the LLM both the one-shot explanation and the follow-up chat only ever see {name, value, status} (plus range, for chat) for one parameter at a time.
  • Whole-report or open-ended chat deliberately scoped follow-up chat to one flagged value per conversation; a wider context is harder to keep reliably bounded away from diagnosis/treatment territory across many turns.

Known limitations

  • OCR accuracy depends on image quality. Low resolution photos of real lab reports (~400px wide) originally OCR'd to garbage; the pipeline now upscales and preprocesses, but a genuinely garbled source value is correctly reported as "couldn't read" rather than guessed this is by design, not a gap to silently paper over.
  • Unit normalization is partial. Absolute cell counts (/cumm) and SI-equivalent units (x10⁹/L) are handled for CBC; more exotic report formats will surface new cases as they're tested.
  • No real authentication yet. History and chat are tied to an anonymous per browser device ID, not an account clearing localStorage or switching devices loses access (the underlying data isn't deleted, just unreachable without the ID).
  • No rate limiting. /explain, /ocr/extract, and /chat are open endpoints with no auth and no per device throttling beyond a 40-message cap per chat thread acceptable for local development, a real gap before any public deployment.
  • No output-side safety check on LLM replies. The system prompts hard-constrain the model (no diagnosis, no medication, mandatory disclaimer), verified against direct adversarial prompts during testing but there's no automated check that a given reply actually honored those constraints before it reaches the user.

License

MIT see LICENSE.

Disclaimer

MedExplain AI is an educational project. It is not a medical device, does not provide medical advice, and should never be used as a substitute for consulting a qualified healthcare professional.

About

MedExplain AI: turns CBC lab reports into plain-English, doctor-consult-first health explanations.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages