Spaces:
Sleeping
title: MediShield Document Classifier
emoji: π₯
colorFrom: blue
colorTo: green
sdk: docker
app_port: 8000
pinned: false
license: mit
MediShield AI Document Classifier
Automatically classify scanned insurance documents β bills, KYC identity proofs, prescriptions, lab reports and more β using a three-stage pipeline: filename rules β OCR β Gemini LLM.
π Live demo: https://sriny2131-medishield.hf.space (Hugging Face Spaces, free CPU)
Table of Contents
- Background
- How It Works
- Architecture
- Classification Pipeline
- Project Structure
- Quick Start (Local)
- Running with Docker
- API Reference
- Frontend UI
- Monitoring with LangSmith
- Deploying to Azure
- Running Tests
- Environment Variables
Background
MediShield Insurance processes thousands of scanned health insurance claims every month. Previously, a team of 12 operators manually reviewed every document β classifying prescriptions, bills, KYC identity proofs, lab reports and claim forms by hand. This caused:
- 48-hour backlogs during peak periods
- 6β8% misclassification rate causing downstream rework
- Skilled operators doing mechanical sorting work
This system automates the classification step, targeting β₯ 95% accuracy and < 5 second processing time per document.
How It Works
Every uploaded image passes through a three-stage pipeline. Each stage can short-circuit so only documents that truly need AI processing reach the LLM:
| Stage | Trigger | Result | LLM used? |
|---|---|---|---|
| Rules Engine | Filename starts with bill_ |
doc_type = bill |
β No |
| KYC OCR | easyocr finds Aadhaar/PAN/Passport keywords | doc_type = kyc |
β No |
| Gemini LLM | Everything else | doc_type = image + sub-type |
β Yes |
Architecture
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Browser / Client β
β Drag & Drop UI Β· frontend/index.html β
ββββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββ
β POST /classify (multipart)
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β FastAPI Β· src/api.py Β· Port 8000 β
β β
β asyncio.gather β runs all files concurrently in thread pool β
β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Classification Pipeline (src/classifier.py) β β
β β β β
β β ββββββββββββββββ bill_* ββββββββββββββββββββββββ β β
β β β Rules Engine ββββββββββββΆβ doc_type = "bill" β β β
β β β(src/rules_ β β method = "rules" β β β
β β β engine.py) β ββββββββββββββββββββββββ β β
β β ββββββββ¬ββββββββ β β
β β β others β β
β β βΌ β β
β β ββββββββββββββββ KYC kw ββββββββββββββββββββββββ β β
β β β KYC OCR ββββββββββββΆβ doc_type = "kyc" β β β
β β β(easyocr) β β method = "ocr" β β β
β β β(src/kyc_ β ββββββββββββββββββββββββ β β
β β β detector.py) β β β
β β ββββββββ¬ββββββββ β β
β β β no KYC match β β
β β βΌ β β
β β ββββββββββββββββ ββββββββββββββββββββββββ β β
β β β Gemini LLM ββββββββββββΆβ doc_type = "image" β β β
β β β gemma-4-31b β β sub_type = category β β β
β β β(src/llm_ β β method = "llm" β β β
β β β classifier β ββββββββββββββββββββββββ β β
β β β .py) β β β
β β ββββββββββββββββ β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Monitoring Β· src/monitoring.py β β
β β @traceable spans Β· token counts Β· structured logs β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
ββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββ΄βββββββββββ
βΌ βΌ
LangSmith Azure Monitor
(traces Β· tokens (container logs
Β· latency) Β· metrics)
Classification Pipeline
Stage 1 β Rules Engine (src/rules_engine.py)
Uses a compiled regex ^bill_ against the filename. Zero ML cost β runs in < 1 ms.
bill_innovh_01.png β doc_type=bill method=rules β done
Stage 2 β KYC OCR Detector (src/kyc_detector.py)
Runs easyocr on the image bytes and matches against 11 regex patterns:
| Pattern | Matches |
|---|---|
\baadhaar\b |
Aadhaar cards |
\buidai\b |
UIDAI-issued docs |
\bpan\b |
PAN cards |
\bpassport\b |
Passports |
\bgovt\.?\s+of\s+india\b |
Govt. of India docs |
\bdate\s+of\s+birth\b |
DOB field |
\bincome\s+tax\b |
Income tax dept |
\b[2-9]\d{3}\s?\d{4}\s?\d{4}\b |
12-digit Aadhaar number |
\b[A-Z]{5}[0-9]{4}[A-Z]\b |
PAN card format |
03ac1d4117.png (Aadhaar scan) β doc_type=kyc method=ocr β done
Stage 3 β Gemini LLM Classifier (src/llm_classifier.py)
Sends image bytes + a structured prompt to gemma-4-31b-it. Returns one of:
Patient BillsClaim FormsKYC DocumentsMedical ReportsPrescriptionsUnknown
05a5de87a2.png β doc_type=image sub_type=Medical Reports method=llm
Project Structure
multimodal-ai/
βββ src/
β βββ rules_engine.py # Stage 1: filename regex rules
β βββ kyc_detector.py # Stage 2: easyocr + KYC keyword matching
β βββ llm_classifier.py # Stage 3: Gemini multimodal classification
β βββ classifier.py # Pipeline orchestrator (wires all 3 stages)
β βββ api.py # FastAPI server (POST /classify, GET /health)
β βββ monitoring.py # LangSmith @traceable wrappers + token logging
βββ 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)
βββ frontend/
β βββ index.html # Self-contained drag & drop UI
βββ infra/
β βββ deploy.sh # Azure Container Apps one-shot deploy
β βββ teardown.sh # Delete all Azure resources
βββ .github/
β βββ workflows/
β βββ ci.yml # Run tests on every push / PR
β βββ deploy.yml # Build β push β deploy on merge to main
βββ Dockerfile # Two-stage build (uv builder + slim runtime)
βββ .dockerignore
βββ pyproject.toml
βββ .env.example
βββ IMPLEMENTATION_PLAN.md
Quick Start (Local)
Prerequisites
- Python 3.12+
- uv package manager
- A Google Gemini API key
- (Optional) A LangSmith API key for tracing
1. Clone and install
git clone https://github.com/sriny3/medishield-document-classifier.git
cd multimodal-ai
uv sync
2. Configure environment
cp .env.example .env
# Edit .env and set GOOGLE_API_KEY
3. Start the API server
.venv/Scripts/python -m uvicorn src.api:app --reload --port 8000
# Windows: .venv\Scripts\python -m uvicorn src.api:app --reload
First startup takes ~30 seconds while easyocr loads its OCR models into memory.
4. Open the UI
Serve the frontend (any static server works):
cd frontend
python -m http.server 3000
Open http://localhost:3000 and drag images from the dataset/ folder.
Running with Docker
# Build (downloads easyocr models into the image β takes ~5 min first time)
docker build -t medishield-classifier:latest .
# Run
docker run -p 8000:8000 \
-e GOOGLE_API_KEY=your-key \
-e LANGCHAIN_TRACING_V2=true \
-e LANGCHAIN_API_KEY=your-langsmith-key \
-e LANGCHAIN_PROJECT=medishield-classification \
medishield-classifier:latest
# Verify
curl http://localhost:8000/health
API Reference
POST /classify
Classify one or more scanned document images.
Request: multipart/form-data
| Field | Type | Description |
|---|---|---|
files |
File[] |
One or more PNG / JPEG / WebP images |
Response: 200 OK β JSON array, one object per file
[
{
"filename": "bill_innovh_01.png",
"doc_type": "bill",
"sub_type": null,
"method": "rules",
"latency_ms": 7,
"input_tokens": 0,
"output_tokens": 0
},
{
"filename": "03ac1d4117.png",
"doc_type": "kyc",
"sub_type": null,
"method": "ocr",
"latency_ms": 1240,
"input_tokens": 0,
"output_tokens": 0
},
{
"filename": "05a5de87a2.png",
"doc_type": "image",
"sub_type": "Medical Reports",
"method": "llm",
"latency_ms": 3210,
"input_tokens": 312,
"output_tokens": 4
}
]
| Field | Values |
|---|---|
doc_type |
bill Β· kyc Β· image |
sub_type |
Patient Bills Β· Claim Forms Β· KYC Documents Β· Medical Reports Β· Prescriptions Β· Unknown Β· null |
method |
rules Β· ocr Β· llm |
Error responses:
| Code | Reason |
|---|---|
422 |
Unsupported MIME type (only PNG / JPEG / WebP accepted) |
500 |
Gemini API error or internal failure |
GET /health
Liveness probe.
{ "status": "ok" }
GET /metrics
In-memory counters since last restart.
{
"total_requests": 42,
"total_documents": 189,
"total_input_tokens": 58320,
"total_output_tokens": 756,
"by_method": { "rules": 80, "ocr": 35, "llm": 74 },
"by_doc_type": { "bill": 80, "kyc": 35, "image": 74 }
}
Note: With multiple uvicorn workers each worker has its own counter. Use a single worker locally (
--workers 1) to see accurate totals, or wire up Prometheus/Redis for production aggregation.
GET /docs
Interactive Swagger UI β auto-generated by FastAPI.
Frontend UI
A self-contained single-file drag & drop interface at frontend/index.html.
Features:
- Drag & drop zone or click-to-browse for PNG / JPEG / WebP
- Thumbnail preview grid with per-file remove button
- Sends all files in one batch β server classifies concurrently so
bill_files return in < 10 ms without waiting for OCR/LLM calls on other files - Live progress bar and per-file status rows (queued β classifying β done)
- Color-coded result badges: bill (blue) Β· kyc (orange) Β· image (green)
- Summary bar: counts per type + average latency
- All controls disabled during processing (no accidental re-submit)
Monitoring with LangSmith
Every classification run emits a nested trace to LangSmith:
classify-document (chain)
βββ rules-engine (tool) filename, doc_type, send_to_llm
βββ kyc-ocr (tool) ocr_text_length, doc_type
βββ llm-classify (llm) sub_type, input_tokens, output_tokens
Token usage, latency, and inputs/outputs are visible per run in the LangSmith dashboard. Tracing is a no-op when LANGCHAIN_TRACING_V2 is not set β safe for local dev and CI.
Enable tracing:
# .env
LANGCHAIN_TRACING_V2=true
LANGCHAIN_API_KEY=ls__your_key
LANGCHAIN_PROJECT=medishield-classification
Deploying to Azure
One-shot deploy (first time)
az login
chmod +x infra/deploy.sh infra/teardown.sh
./infra/deploy.sh
The script provisions:
| Resource | Purpose |
|---|---|
Resource Group medishield-rg |
Container for all resources |
| Azure Container Registry | Private Docker registry |
| Log Analytics Workspace | Container log ingestion |
| Container Apps Environment | Serverless container runtime |
| Container App | The running classifier (0.5 vCPU / 2 GB RAM, min 1 replica) |
Secrets (GOOGLE_API_KEY, LANGCHAIN_API_KEY) are injected via Container Apps secret references β never stored as plain env vars.
CI/CD (automated on every merge to main)
.github/workflows/deploy.yml runs on every push to main:
- β Full test suite must pass
az acr buildβ builds image in Azure (no local Docker needed)az containerapp updateβ rolling deploy, zero downtime- Smoke tests
/healthon the live URL
GitHub secrets required:
| Secret | How to get it |
|---|---|
AZURE_CLIENT_ID |
az ad sp create-for-rbac --role contributor |
AZURE_TENANT_ID |
Same command output |
AZURE_SUBSCRIPTION_ID |
az account show --query id |
Teardown
./infra/teardown.sh
Running Tests
# All 117 tests
.venv/Scripts/python -m pytest tests/ -v
# Single module
.venv/Scripts/python -m pytest tests/test_rules_engine.py -v
# With coverage
.venv/Scripts/python -m pytest tests/ --cov=src --cov-report=term-missing
| Module | Tests | What's covered |
|---|---|---|
test_rules_engine |
11 | Regex matching, case sensitivity, full paths |
test_kyc_detector |
21 | 8 KYC patterns, 5 non-KYC, mocked easyocr |
test_llm_classifier |
32 | All 6 categories, token capture, prompt structure |
test_classifier |
29 | Pipeline short-circuits, stage ordering, dataset scan |
test_api |
11 | Endpoints, MIME validation, metrics, multi-file |
test_monitoring |
13 | Trace output, token extraction, pipeline instrumentation |
Environment Variables
| Variable | Required | Description |
|---|---|---|
GOOGLE_API_KEY |
β Yes | Gemini API key from Google AI Studio |
LANGCHAIN_TRACING_V2 |
Optional | Set to true to enable LangSmith tracing |
LANGCHAIN_API_KEY |
Optional | LangSmith API key |
LANGCHAIN_PROJECT |
Optional | LangSmith project name (default: medishield-classification) |
Tech Stack
| Layer | Technology |
|---|---|
| API framework | FastAPI + uvicorn |
| OCR | easyocr (CPU, English) |
| LLM | Google Gemini gemma-4-31b-it via google-genai |
| Monitoring | LangSmith (@traceable) |
| Packaging | uv + pyproject.toml |
| Container | Docker (two-stage, python:3.12-slim) |
| Cloud | Azure Container Apps + ACR + Log Analytics |
| CI/CD | GitHub Actions |
| Tests | pytest (117 tests, 0 live API calls) |