Skip to content

mlei06/Explainable-Medical-Coding

Repository files navigation

Medical Coding Microservice

A deployable HTTP service that turns clinical notes into ICD-9 / ICD-10 / CPT predictions with evidence spans. Pluggable across local PLM models and hosted LLMs, designed to run as a stateless container behind your existing infrastructure.

A small demo UI ships in this repo for local exploration, but it is a sidecar — the service is the product.

What it does

  • Predict medical codes from a clinical note via a versioned HTTP API
  • Return evidence — token-level spans from the note that support each predicted code
  • Pluggable model providers — local PLM (RoBERTa-based, with explainability methods) or hosted LLM (OpenAI)
  • De-identification — replace PHI in a note with MIMIC-style placeholders before downstream use
  • Stateless — no database, no session state; scale horizontally behind a load balancer

API contract

The full OpenAPI spec is served at /docs (Swagger UI) and /openapi.json once the service is running.

Endpoints

Method Path Purpose
POST /v1/predict Predict codes with a local PLM + evidence spans
POST /v1/predict/llm Predict codes with a hosted LLM
POST /v1/deidentify Replace PHI in a note with placeholders
GET /v1/models List configured model logical names
GET /v1/explain-methods List supported PLM explanation methods
GET /healthz Liveness probe
GET /v1/readyz Readiness probe (model loaded)

Unversioned routes (/predict-explain, /predict-explain-llm, /deidentify, /models, /explain-methods) remain available but are deprecated and will be removed in the next release.

Auth

Set API_KEYS=name1:key1,name2:key2 and clients pass X-API-Key: keyN. Set AUTH_REQUIRED=false to disable enforcement in dev. /healthz, /v1/readyz, /docs, and /openapi.json are always public.

Errors

All errors return a uniform envelope:

{ "error": { "code": "not_found", "message": "...", "request_id": "..." } }

Every response includes an X-Request-ID header (echoed from the request, or generated). Each request emits one structured request log line with request_id, tenant_id, method, path, status, duration_ms.

Example

curl -X POST http://localhost:8084/v1/predict \
  -H 'Content-Type: application/json' \
  -d '{
    "note": "Patient presents with chest pain and shortness of breath...",
    "model": "icd10-plm",
    "explain_method": "grad_attention",
    "confidence_threshold": 0.5
  }'

Deploy

The service is a single stateless container. Weights are not baked into the image — mount them at runtime via MODELS_DIR (defaults to /app/models inside the container). The service starts even without local weights; LLM endpoints still work and /v1/readyz reports 503 until a base encoder + DEFAULT_MODEL are available.

Build from source

docker build -t medical-coding-api .
docker run -d \
  --name medical-coding-api \
  -p 8084:8084 \
  --env-file .env \
  -v $(pwd)/models:/app/models:ro \
  -e MODEL_REGISTRY="icd10-plm=icd10-supervised/abc123" \
  -e DEFAULT_MODEL=icd10-plm \
  medical-coding-api

docker-compose (API + demo UI)

cp .env.example .env       # add OPENAI_API_KEY if using LLM
# Optional: place weights under ./models/ first (see "Model providers" below)
docker-compose up -d --build
  • API: http://localhost:8084 (docs at /docs)
  • Demo UI: http://localhost:8090

docker-compose.yml mounts ./models into the API container read-only.

Pre-built image

docker run -d \
  --name medical-coding-api \
  -p 8084:8084 \
  --env-file .env \
  -v $(pwd)/models:/app/models:ro \
  michaeldockerlei/explainable-coding-api:latest

Note: images published before the runtime-mount change still bake weights into the image; rebuild from source for the lean image.

Configuration

All configuration is via environment variables.

Variable Default Purpose
OPENAI_API_KEY Required for any /v1/predict/llm traffic
LLM_CODING_MODEL gpt-5 OpenAI model name
GPT5_DEFAULT_REASONING_EFFORT minimal Reasoning effort for GPT-5 family
MODELS_DIR ./models Filesystem root for PLM weights
MODEL_REGISTRY — (falls back to filesystem scan) Comma-separated name=path entries; paths resolve under MODELS_DIR
DEFAULT_MODEL first registry entry Logical name used when a request omits model
API_KEYS Comma-separated caller_name:key entries (or bare keys)
AUTH_REQUIRED true if API_KEYS set, else false Toggle API-key enforcement
LOG_FORMAT json Set to anything else to fall back to default formatting
LOG_LEVEL INFO Root log level
PORT 8084 HTTP port
UPSTREAM_API_BASE http://localhost:8084 Used by the demo UI to reach the API

Model registry

Clients pass logical names ("icd10-plm"), not filesystem paths. Configure the registry via env:

MODELS_DIR=/var/lib/coding/models
MODEL_REGISTRY=icd9-plm=icd9-supervised/abc123,icd10-plm=icd10-supervised/def456
DEFAULT_MODEL=icd10-plm

If MODEL_REGISTRY is unset, the service falls back to scanning MODELS_DIR for the first non-blacklisted subdirectory — preserved for backwards compatibility.

Health vs readiness

  • GET /healthz — liveness. Returns 200 whenever the process is up.
  • GET /v1/readyz — readiness. Returns 200 only after the default model has been loaded; 503 with a reason otherwise. Use this for load-balancer health checks and orchestrator readiness probes.

Model providers

Local PLM models

The service expects a base encoder (roberta-base-pm-m3-voc-hf) plus one or more fine-tuned ICD/CPT heads under models/. Currently model directories are discovered by filesystem scan; a config-driven registry is on the roadmap.

Manual download:

wget https://dl.fbaipublicfiles.com/biolm/RoBERTa-base-PM-M3-Voc-hf.tar.gz -P models
tar -xvzf models/RoBERTa-base-PM-M3-Voc-hf.tar.gz -C models
mv models/RoBERTa-base-PM-M3-Voc/RoBERTa-base-PM-M3-Voc-hf models/roberta-base-pm-m3-voc-hf
rm -rf models/RoBERTa-base-PM-M3-Voc models/RoBERTa-base-PM-M3-Voc-hf.tar.gz

gdown --id 15ePOrJPS12TbxqRdKmNKH0igZ8k4Mk3k -O models/temp.tar.gz
tar -xvzf models/temp.tar.gz -C models && rm models/temp.tar.gz

Lighter download (2 ICD heads instead of 4): replace the gdown ID with 15ePOrJPS12TbxqRdKmNKH0igZ8k4Mk3k.

LLM provider

Set OPENAI_API_KEY in .env and call /predict-explain-llm. No local weights required.

Local development

Without Docker

python -m venv venv && source venv/bin/activate
pip install -r requirements.txt
cp .env.example .env       # add OPENAI_API_KEY if using LLM
python api.py              # serves on :8084

Demo UI sidecar

The UI in demo-ui/ is a small FastAPI app that proxies the coding service. Useful for visualizing evidence spans and reviewing predictions; not required to use the service.

pip install -r demo-ui/requirements.txt
cd demo-ui && uvicorn main:app --reload --port 8090

Visit http://localhost:8090. Configure UPSTREAM_API_BASE to point at a non-local API.

Evaluation harness

tools/eval.py scores model output against MIMIC-style labelled data. Not part of the service; lives in tools/ for reproducibility and is excluded from the Docker image via .dockerignore.

gdown --id 1xqC10tyviXuU3iLVIjp7oH01RrimHW2- -O data/mimic_data/data.tar.gz
tar -xvzf data/mimic_data/data.tar.gz -C data/mimic_data && rm data/mimic_data/data.tar.gz
python tools/eval.py

tools/curl_api_test.py is an interactive smoke-test that hits a running API instance.

Repo layout

.
├── api.py                # service entry point
├── utils/                # provider implementations (PLM, LLM, deidentify)
├── demo-ui/              # optional UI sidecar
├── tools/                # dev/eval tooling — excluded from the service image
│   ├── eval.py           # evaluation harness against MIMIC-style data
│   └── curl_api_test.py  # interactive smoke test
├── models/               # weights (mounted at runtime; not committed)
├── data/                 # sample notes + code descriptions
├── Dockerfile            # service image (no weights baked in)
└── docker-compose.yml    # service + demo UI

Dependency slimming note: requirements.txt currently includes wandb, textattack, hydra-core, and gdown even though they are only needed by training and manual model-download workflows. They're listed because the runtime PLM modules transitively import explainable_medical_coding.config.factoriestrainer.callbackswandb. Removing them requires gating those imports behind lazy loading — tracked as a future refactor.

Roadmap

  • Slim runtime dependencies: lazy-load trainer modules so the service image can drop wandb, textattack, hydra-core, gdown
  • Move api.py + utils/ into an app/ package for clearer service boundaries
  • Model artifacts from object storage (S3 / HF) instead of host volume mount

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors