Serve it¶
Two endpoints matter, and they are different shapes on purpose.
POST /score¶
Score a whole stay at once. The batch shape, useful for backfilling and for testing.
curl -s localhost:8300/score -H 'content-type: application/json' -d '{
"patient_id": "demo",
"hours": [
{"HR": 88, "O2Sat": 97, "Temp": 36.8, "SBP": 120, "MAP": 82, "Resp": 17},
{"HR": 96, "O2Sat": 95, "SBP": 112, "MAP": 74, "Resp": 21, "Lactate": 2.4}
]
}'
{
"risk": [0.021, 0.058],
"alert": [false, false],
"alert_transitions": 0,
"threshold": 0.0608,
"alerts_on_nothing": false,
"guarantee": "With probability at least 95% over the calibration draw, ...",
"disclaimer": "Prodrome is a research artefact. It is not a medical device ..."
}
Unknown channel names are rejected rather than ignored, because a caller who misspells
Lactate should be told, not silently scored without it.
WS /stream¶
One hour in, one risk out. This is the shape a real integration has, and the one that demonstrates the causal guarantee end to end: the server holds the stay's history, appends the hour you just sent, rebuilds the features over the history it has, and answers. It has never been sent the next hour, because there is not one yet.
import json, websockets, asyncio
async def main():
async with websockets.connect("ws://localhost:8300/stream") as ws:
await ws.send(json.dumps({"reset": True}))
await ws.recv()
for hour in stay: # one dict per ICU hour
await ws.send(json.dumps(hour))
print(json.loads(await ws.recv()))
asyncio.run(main())
Send {"reset": true} between patients. A stream that runs past a thousand hours
without a reset is a client that forgot, and the server says so rather than growing
forever.
GET /health¶
What is loaded and what it promises:
{
"status": "ok",
"model": "lgbm",
"trained_on": ["A"],
"calibrated_on": ["A"],
"corpus_sha256": "ef46d3e6...",
"threshold": 0.0608,
"alerts_on_nothing": false,
"guarantee": "With probability at least 95% ...",
"disclaimer": "..."
}
GET /metrics¶
Prometheus counters and histograms: requests by endpoint, alert transitions raised, and seconds to score a request.
Every response carries its terms¶
The threshold, the promise and the disclaimer are attached to every answer, not documented elsewhere. A caller cannot receive a risk without receiving the terms it was produced under, because a clinical score arriving over an API with no context is how research code ends up somewhere it should not be.
Operational notes¶
- A model that alerts on nothing reports
alerts_on_nothing: trueand a null threshold, rather than silently never firing. - The service loads one configuration. Run one process per model.
- There is no authentication. Put it behind something that has some.