Spaces:
Sleeping
Sleeping
File size: 15,816 Bytes
d86db02 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 | # 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 |
|