medishield / IMPLEMENTATION_PLAN.md
sriny2131's picture
deploy: sync from local repo
d86db02 verified
|
Raw
History Blame Contribute Delete
15.8 kB
# MediShield AI Document Classification β€” Implementation Plan
## Workflow Diagram
```
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Uploaded Image β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Stage 1: Rules Engine β”‚
β”‚ regex: ^bill_ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
bill_ match? no match
β”‚ β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ doc_type = "bill" β”‚ β”‚ Stage 2: KYC OCR β”‚
β”‚ method = "rules" β”‚ β”‚ easyocr β†’ keyword regex β”‚
β”‚ βœ“ DONE β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
KYC match? no match
β”‚ β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ doc_type = "kyc" β”‚ β”‚ Stage 3: Gemini LLM β”‚
β”‚ method = "ocr" β”‚ β”‚ gemma-4-31b-it β”‚
β”‚ βœ“ DONE β”‚ β”‚ β†’ Patient Bills β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β†’ Claim Forms β”‚
β”‚ β†’ Medical Reports β”‚
β”‚ β†’ Prescriptions β”‚
β”‚ β†’ Unknown β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ doc_type = "image" β”‚
β”‚ sub_type = <category> β”‚
β”‚ method = "llm" β”‚
β”‚ βœ“ DONE β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
All stages emit @traceable spans β†’ LangSmith (traces Β· tokens Β· latency)
All results served via FastAPI β†’ Drag & Drop UI
Container deployed on Azure Container Apps via GitHub Actions CI/CD
```
## Architecture Overview
```
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Frontend UI β”‚
β”‚ Drag & Drop Β· frontend/index.html β”‚
β”‚ - Batch upload (all files in one POST) β”‚
β”‚ - Concurrent server processing (asyncio.gather) β”‚
β”‚ - Live progress bar + per-file status rows β”‚
β”‚ - Color-coded badges: bill/kyc/image β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ POST /classify (multipart)
β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ FastAPI Β· src/api.py Β· Port 8000 β”‚
β”‚ POST /classify Β· GET /health Β· GET /metrics β”‚
β”‚ asyncio.gather + run_in_executor (concurrent files) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ src/classifier.py β”‚ (orchestrator)
β””β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜
β”‚ β”‚ β”‚
rules β”‚ ocr β”‚ llm β”‚
engine β”‚ β”‚ β”‚
β–Ό β–Ό β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ src/monitoring.py β€” LangSmith @traceable spans β”‚
β”‚ trace_rules_engine Β· trace_kyc_ocr β”‚
β”‚ trace_llm_classify Β· trace_classify β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β–Ό β–Ό
LangSmith Azure Monitor
(traces/tokens) (container logs)
```
## Decision Rules
| Condition | doc_type | method | Sent to LLM? |
|---|---|---|---|
| filename matches `^bill_` (regex) | `bill` | `rules` | No |
| OCR text contains KYC keywords | `kyc` | `ocr` | No |
| Everything else | `image` | `llm` | Yes |
## Final File Layout
```
multimodal-ai/
β”œβ”€β”€ src/
β”‚ β”œβ”€β”€ rules_engine.py # Step 1 βœ…
β”‚ β”œβ”€β”€ kyc_detector.py # Step 2 βœ…
β”‚ β”œβ”€β”€ llm_classifier.py # Step 3 βœ…
β”‚ β”œβ”€β”€ classifier.py # Step 4 βœ…
β”‚ β”œβ”€β”€ api.py # Step 5 βœ…
β”‚ └── monitoring.py # Step 7 βœ…
β”œβ”€β”€ frontend/
β”‚ └── index.html # Step 6 βœ…
β”œβ”€β”€ tests/
β”‚ β”œβ”€β”€ test_rules_engine.py # 11 tests βœ…
β”‚ β”œβ”€β”€ test_kyc_detector.py # 21 tests βœ…
β”‚ β”œβ”€β”€ test_llm_classifier.py # 32 tests βœ…
β”‚ β”œβ”€β”€ test_classifier.py # 29 tests βœ…
β”‚ β”œβ”€β”€ test_api.py # 11 tests βœ…
β”‚ └── test_monitoring.py # 13 tests βœ… (117 total)
β”œβ”€β”€ infra/
β”‚ β”œβ”€β”€ deploy.sh # Step 9 βœ… Azure Container Apps
β”‚ └── teardown.sh # βœ…
β”œβ”€β”€ .github/workflows/
β”‚ β”œβ”€β”€ ci.yml # Step 10 βœ… test on every push
β”‚ └── deploy.yml # Step 10 βœ… deploy on merge to main
β”œβ”€β”€ Dockerfile # Step 8 βœ… two-stage build
β”œβ”€β”€ .dockerignore # Step 8 βœ…
β”œβ”€β”€ README.md # Step 10 βœ…
β”œβ”€β”€ pyproject.toml
└── .env.example
```
---
## Steps
### Phase 1 β€” Core Classification Engine
- [x] **Step 1 β€” Rules Engine** (`src/rules_engine.py`)
- Compiled regex `re.compile(r"^bill_")` β€” case-sensitive, anchored to start of filename
- Strips directory prefix so full paths work (`dataset/bill_x.png`)
- Returns `RulesResult(filename, doc_type, send_to_llm)`
- **Changed from plan:** Used `re.compile` regex instead of `str.startswith()` as requested
- βœ… **11/11 tests passing**
- [x] **Step 2 β€” KYC Detector** (`src/kyc_detector.py`)
- 11 compiled regex patterns covering Aadhaar, PAN, Passport, Govt of India, DOB, 12-digit Aadhaar number, PAN card format
- easyocr `Reader` is a lazy singleton β€” loaded once on first use, not at import time
- `reader` is injectable (passed as parameter) so tests never load the real model
- Returns `KYCResult(filename, doc_type, send_to_llm, ocr_text)`
- βœ… **21/21 tests passing**
- [x] **Step 3 β€” LLM Classifier** (`src/llm_classifier.py`)
- Sends image bytes + structured prompt to `gemma-4-31b-it` via `google-genai`
- Prompt instructs model to return exactly one category name
- `_parse_category()` does case-insensitive match + strips whitespace, falls back to `"Unknown"`
- Captures `input_tokens` and `output_tokens` from `response.usage_metadata`
- `client` is injectable for testing β€” zero live API calls in test suite
- Returns `LLMResult(filename, doc_type, sub_type, method, input_tokens, output_tokens, raw_response)`
- βœ… **32/32 tests passing**
- [x] **Step 4 β€” Pipeline Orchestrator** (`src/classifier.py`)
- `classify(filename, image_bytes, ocr_reader, llm_client)` β€” single document
- `classify_dataset(dataset_dir, ...)` β€” scans all PNGs in a directory
- Returns `ClassificationResult(filename, doc_type, sub_type, method, latency_ms, input_tokens, output_tokens)`
- Each stage emits a LangSmith trace span (added in Step 7)
- βœ… **29/29 tests passing**
---
### Phase 2 β€” FastAPI Server
- [x] **Step 5 β€” API Server** (`src/api.py`)
- `POST /classify` β€” multipart file upload, returns JSON array
- `GET /health` β€” liveness probe
- `GET /metrics` β€” in-memory counters per method/doc_type/token usage
- `GET /docs` β€” auto Swagger UI
- **Changed from plan:** `asyncio.gather` + `run_in_executor` runs all uploaded files concurrently β€” `bill_` files return in < 10 ms without waiting behind OCR/LLM calls
- easyocr `Reader` and Gemini `Client` loaded once at startup via FastAPI `lifespan`
- CORS middleware enabled for browser UI
- βœ… **11/11 tests passing** (patched at `src.api.classify`)
---
### Phase 3 β€” Frontend UI
- [x] **Step 6 β€” Drag & Drop UI** (`frontend/index.html`)
- Self-contained single HTML file, no external dependencies
- Drag & drop + click-to-browse, deduplicates files by name
- **Changed from plan (sequential β†’ batch):** Sends all files in ONE `POST /classify` β€” server processes concurrently so `bill_` files don't wait behind slow OCR/LLM calls
- Results table appears immediately with `queued…` rows; fills in as server responds
- Live progress bar + `Processing file N of M` text
- Color-coded badges: bill=blue, kyc=orange, image=green, rules=purple, ocr=red, llm=teal
- All controls (classify, clear, remove buttons, drop zone) disabled during processing
- Summary bar: counts per type + average latency
- Error banner for API failures and unsupported file types
---
### Phase 4 β€” Monitoring (LangSmith)
- [x] **Step 7 β€” LangSmith Integration** (`src/monitoring.py`)
- Four `@traceable` functions forming a parent/child span tree:
- `trace_classify` β€” top-level `chain` span per document
- `trace_rules_engine` β€” `tool` span for Stage 1
- `trace_kyc_ocr` β€” `tool` span for Stage 2; records `ocr_text_length` not raw text (PII safety)
- `trace_llm_classify` β€” `llm` span for Stage 3; records token breakdown
- `record_token_usage()` extracts `input/output/total_tokens` from Gemini `usage_metadata`
- Tracing is a **no-op** when `LANGCHAIN_TRACING_V2` is not set β€” CI safe
- **Required env vars:**
```
LANGCHAIN_TRACING_V2=true
LANGCHAIN_API_KEY=<key>
LANGCHAIN_PROJECT=medishield-classification
```
- βœ… **13/13 tests passing**
---
### Phase 5 β€” Docker
- [x] **Step 8 β€” Dockerfile**
- Two-stage build: `uv` builder β†’ `python:3.12-slim` runtime
- Installs OS libs for easyocr/opencv/weasyprint in runtime stage
- **Pre-downloads easyocr models at build time** as `appuser` β€” container starts in ~10s not 60s
- Runs as non-root `appuser` (with home dir so easyocr can write model cache)
- `HEALTHCHECK` polls `/health` every 30s, 60s start period
- 2 uvicorn workers for concurrency
- **Fix applied during build:** Created home dir for `appuser` and set `EASYOCR_MODULE_PATH` to fix permission error on model cache write
- βœ… **Build verified, `/classify` tested inside container**
---
### Phase 6 β€” Azure Deployment
- [x] **Step 9 β€” Azure Container Apps** (`infra/deploy.sh`)
- **Changed from plan:** Azure instead of AWS (simpler setup, no separate load balancer, built-in HTTPS)
- Provisions: Resource Group β†’ ACR β†’ Log Analytics β†’ Container Apps Environment β†’ Container App
- Container App: 0.5 vCPU / 2 GB RAM, min 1 replica, max 3, public HTTPS ingress
- Secrets (`GOOGLE_API_KEY`, `LANGCHAIN_API_KEY`) injected via Container Apps secret references
- `infra/teardown.sh` for full cleanup
- **CI/CD via `.github/workflows/deploy.yml`:**
- Tests gate deploy (deploy only runs if tests pass)
- `az acr build` builds in Azure cloud (no local Docker in CI)
- `az containerapp update` rolling deploy
- Smoke tests live `/health` endpoint post-deploy
- OIDC login (no long-lived secrets in GitHub)
---
### Phase 7 β€” Documentation
- [x] **Step 10 β€” README + CI** (`README.md`, `.github/workflows/ci.yml`)
- Professional README with ASCII architecture diagram, workflow diagram, full API reference, setup guide, deployment guide, test matrix, environment variable table
- `ci.yml` runs all 117 tests on every push/PR β€” no real API keys needed
---
## Build Order Summary
| # | Deliverable | Test Gate | Status |
|---|---|---|---|
| 1 | Rules Engine | `pytest tests/test_rules_engine.py` β€” 11 passed | βœ… |
| 2 | KYC Detector | `pytest tests/test_kyc_detector.py` β€” 21 passed | βœ… |
| 3 | LLM Classifier | `pytest tests/test_llm_classifier.py` β€” 32 passed | βœ… |
| 4 | Orchestrator | `pytest tests/test_classifier.py` β€” 29 passed | βœ… |
| 5 | FastAPI Server | `pytest tests/test_api.py` β€” 11 passed + Swagger check | βœ… |
| 6 | Frontend UI | Batch POST, live progress, controls locked during processing | βœ… |
| 7 | LangSmith Monitoring | `pytest tests/test_monitoring.py` β€” 13 passed | βœ… |
| 8 | Docker | `docker build` + `/classify` tested inside container | βœ… |
| 9 | Azure Deploy | `infra/deploy.sh` + GitHub Actions CI/CD pipeline | βœ… |
| 10 | README + CI | `ci.yml` + `deploy.yml` + `README.md` | βœ… |
**Total: 117 tests Β· 10 steps Β· all complete βœ…**
## Key Changes vs Original Plan
| Area | Original Plan | What We Actually Built |
|---|---|---|
| Rules matching | `str.startswith("bill_")` | `re.compile(r"^bill_")` regex |
| API concurrency | Sequential file loop | `asyncio.gather` + `run_in_executor` |
| UI upload strategy | One request per file (sequential) | One batch request, server concurrent |
| Cloud provider | AWS ECS Fargate | Azure Container Apps |
| Metrics | OpenTelemetry + CloudWatch | LangSmith + Azure Monitor |
| Docker user | Root | Non-root `appuser` with home dir |
| easyocr models | Downloaded at runtime | Pre-baked into image at build time |