Maternal Health Multi-Task Intelligence β Inference Service
FastAPI service that wraps the joint multi-task PyTorch network behind a typed, explainable HTTP contract. It is Part 1 of a three-tier platform:
Next.js dashboard -> Spring Boot API -> this FastAPI service -> PyTorch model
(Client/) (Server/) (Model/)
One shared-backbone network predicts three targets at once:
| Task | Head | Output |
|---|---|---|
| Gestational Diabetes | head_gdm |
probability + binary decision |
| Hypertensive Disorders | head_hdp |
probability + binary decision |
| Composite Urgency Risk | head_composite |
Low / Mid / High + full class breakdown |
Architecture
The package is layered so that each concern is testable in isolation and no layer depends on one above it.
Model/
βββ app.py entry point; imports the factory and nothing else
βββ maternal_ai/
β βββ config.py typed settings (env driven, MATERNAL_* prefix)
β βββ exceptions.py error hierarchy -> HTTP status + JSON envelope
β βββ factory.py composition root: create_app()
β βββ domain/ framework-free value objects
β β βββ enums.py ClinicalTask, RiskLevel, FindingSeverity, PredictorKind
β β βββ vitals.py PatientVitals aggregate (validation + MAP/PP behaviour)
β β βββ assessment.py RawPrediction, BinaryTaskOutcome, CompositeRiskOutcome,
β β ClinicalFinding, RiskAssessment
β βββ features/
β β βββ engineering.py FeatureEngineer (Template Method) + ClinicalFeatureEngineer
β β βββ scaling.py FeatureScaler (Adapter) + StandardScalerAdapter, IdentityScaler
β βββ clinical/
β β βββ rules.py ClinicalRule (Strategy) + ClinicalRuleEngine (Composite)
β β βββ narrative.py NarrativeComposer -> summary text and action list
β βββ inference/
β β βββ architecture.py JointMultiTaskNet (serving copy of the trained net)
β β βββ artifacts.py ArtifactLoader + ArtifactBundle (with SHA-256 provenance)
β β βββ assembler.py RiskAssessmentAssembler (raw numbers -> interpreted result)
β β βββ predictor.py RiskPredictor ABC + ClinicalRuleBasedPredictor fallback
β β βββ torch_predictor.py TorchMultiTaskPredictor
β βββ api/
β βββ schemas.py Pydantic DTOs (camelCase wire format)
β βββ dependencies.py DI providers: settings, predictor, API key guard
β βββ middleware.py correlation id + server timing
β βββ errors.py one exception handler for every failure mode
β βββ routes.py thin handlers
βββ tests/test_inference_service.py
OOP principles in practice
- Encapsulation β
PatientVitalsis a frozen dataclass whose constructor enforces every clinical invariant. There is no way to hold an invalid instance, so no downstream code needs defensive checks. - Abstraction β routes depend on
RiskPredictor, not on PyTorch. Swapping the engine (neural, rule-based, a future ensemble) changes one factory line. - Polymorphism β
TorchMultiTaskPredictorandClinicalRuleBasedPredictorare interchangeable;predict_batchin the base class works with either. - Inheritance used for behaviour reuse, not taxonomy β
RiskPredictorimplements timing, batching and assembly once; subclasses supply_infer. - Open/Closed β a new clinical criterion is a new
ClinicalRulesubclass registered with the engine; no existing class is edited. - Separation of concerns β DTOs never appear below
api/, and domain objects never know about HTTP.
Feature contract
The scaler and the first nn.Linear layer are position-sensitive, so the
serving code reproduces the training column order exactly. ArtifactLoader
verifies this at start-up against both scaler.n_features_in_ and the
checkpoint's first layer width, and refuses to start on a mismatch.
| # | Feature | Unit | Source |
|---|---|---|---|
| 0 | Age | years | submitted |
| 1 | Systolic BP | mmHg | submitted |
| 2 | Diastolic | mmHg | submitted |
| 3 | BS | mmol/L | submitted |
| 4 | Body Temp | Β°F | submitted |
| 5 | BMI | kg/mΒ² | submitted |
| 6 | Previous Complications | 0/1 | submitted |
| 7 | Preexisting Diabetes | 0/1 | submitted |
| 8 | Mental Health | 0/1 | submitted |
| 9 | Heart Rate | bpm | submitted |
| 10 | MAP | mmHg | engineered: (SBP + 2Β·DBP) / 3 |
| 11 | PP | mmHg | engineered: SBP β DBP |
Celsius temperatures and mg/dL glucose values are detected and converted to the trained units before scaling; the response echoes the canonicalised values.
Endpoints
| Method | Path | Purpose |
|---|---|---|
GET |
/health |
liveness/readiness, active engine, uptime |
GET |
/metadata |
model card: features, thresholds, checkpoint SHA-256 |
POST |
/predict |
assess one patient (?includeFeatures=true adds the feature vector) |
POST |
/predict/batch |
assess a cohort in one forward pass, with summary counts |
GET |
/docs |
interactive OpenAPI documentation |
Request
curl -X POST http://localhost:7860/predict \
-H "Content-Type: application/json" \
-d '{
"age": 34, "systolicBp": 148, "diastolicBp": 96, "bloodSugar": 8.4,
"bodyTemp": 99.1, "bmi": 31.2, "previousComplications": 1,
"preexistingDiabetes": 0, "mentalHealth": 1, "heartRate": 96,
"patientReference": "ANC-2026-0142"
}'
Response (abridged)
{
"assessmentId": "08ae58fe-94ab-4cdf-a08a-7899d4092439",
"modelVersion": "joint-multitask-v1",
"engine": "neural_multitask",
"riskScore": 47.48,
"requiresEscalation": true,
"gestationalDiabetes": { "prediction": 1, "label": "Positive", "probability": 0.503830 },
"hypertensiveDisorders": { "prediction": 0, "label": "Negative", "probability": 0.244116 },
"compositeRisk": {
"label": "High Risk",
"confidence": 0.446449,
"reviewWindow": "24 hours",
"probabilities": { "lowRisk": 0.362502, "midRisk": 0.191050, "highRisk": 0.446449 }
},
"findings": [
{
"code": "BP_ELEVATED",
"severity": "warning",
"detail": "Blood pressure 148/96 mmHg meets the gestational hypertension threshold (>= 140/90).",
"recommendation": "Confirm with a repeat reading after 4 hours of rest and test for proteinuria."
}
],
"recommendedActions": ["Escalate to an obstetric clinician for same-day assessment."],
"narrative": "A 34-year-old patient presenting with blood pressure 148/96 mmHg ..."
}
Errors always use the same envelope:
{
"errorCode": "invalid_vitals",
"message": "Systolic blood pressure must be greater than diastolic blood pressure.",
"details": { "systolicBp": 70.0, "diastolicBp": 80.0 },
"requestId": "69f7ce98-9d6f-4991-a76e-ce991143ff5e"
}
Configuration
Every setting is an environment variable prefixed with MATERNAL_.
| Variable | Default | Purpose |
|---|---|---|
MATERNAL_MODEL_PATH |
./joint_multitask_model.pth |
checkpoint location |
MATERNAL_SCALER_PATH |
./joint_scaler.pkl |
fitted scaler location |
MATERNAL_DEVICE |
auto |
auto, cpu or cuda |
MATERNAL_GDM_THRESHOLD |
0.5 |
gestational diabetes decision boundary |
MATERNAL_HDP_THRESHOLD |
0.5 |
hypertensive disorders decision boundary |
MATERNAL_MAX_BATCH_SIZE |
256 |
rejects larger batches with HTTP 413 |
MATERNAL_ALLOW_RULE_BASED_FALLBACK |
true |
serve rule-based results if artifacts fail to load |
MATERNAL_API_KEY |
unset | when set, /predict* requires the X-API-Key header |
MATERNAL_CORS_ORIGINS |
* |
comma separated allowed origins |
MATERNAL_PORT |
7860 |
listen port |
Running
# local
pip install torch==2.13.0 --index-url https://download.pytorch.org/whl/cpu
pip install -r requirements.txt
python app.py # http://localhost:7860/docs
# tests
pip install -r requirements-dev.txt
python -m pytest tests -q
# docker
docker build -t maternal-ai .
docker run --rm -p 7860:7860 maternal-ai
Hugging Face Spaces
Create a Space with the Docker SDK and push app.py, maternal_ai/,
Dockerfile, requirements.txt and both artifact files. The YAML header at the
top of this README is the Space configuration; app_port: 7860 matches the
EXPOSE/CMD in the Dockerfile.
Known limitation of the shipped checkpoint
joint_scaler.pkl was fitted on three training rows, so the bundled weights are
a smoke-test artifact rather than a trained model β for example it returns a low
hypertensive-disorder probability for a clearly hypertensive 148/96 reading.
Re-run maternal_pipeline.py against the full Maternal Health Risk Data Set
and replace both artifacts; the service picks up the new files with no code
changes, because the loader validates the contract dynamically.