Skip to content

Streaming Prescription Extraction

Streaming Prescription Extraction

DELPHOS can listen to a doctor speaking naturally and produce a fully structured, safety-checked prescription in real time. As the doctor dictates, medications appear one by one — each passing through safety gates before the final prescription is assembled.

This is the streaming prescription agent — the most powerful integration point in the DELPHOS platform.


How It Works

Doctor speaks DELPHOS processes Your UI updates
─────────────── ────────► ──────────────────── ────────► ──────────────────
"Amoxicilina 1. Extract items from speech Item 1 appears with
500mg oral 2. Per-item safety gates safety gate results
8/8h por (validation, CMED, posology)
7 dias. 3. Repeat for each medication Item 2 appears...
Dipirona 4. Cross-item safety gates
500mg SOS" (drug interactions, Final prescription
duplicate therapy) with all gates complete

The streaming architecture uses a two-phase gate model:

  • Per-item gates (1, 2, 6) fire immediately as each medication is detected in the speech stream. Results arrive with each item_detected event.
  • Cross-item gates (3, 4) fire after the full input is processed, since they need to analyze interactions between all medications. Results arrive in the gates_complete event.

Sequence Diagram

Client DELPHOS API Safety Gates
│ │ │
│ POST /v1/prescriptions/stream │ │
│ { doctor_input, stream: true } │ │
│ ─────────────────────────────► │ │
│ │ │
│ event: status │ │
│ data: {"type":"analyzing"} │ │
│ ◄───────────────────────────── │ │
│ │ Gate 1 (Validation) │
│ │ Gate 2 (CMED Resolution) │
│ │ Gate 6 (Controlled Subst.)│
│ event: item_detected │ ◄────────────────────────── │
│ data: {index:0, item, gates} │ │
│ ◄───────────────────────────── │ │
│ │ Per-item gates repeat │
│ event: item_detected │ for each medication │
│ data: {index:1, item, gates} │ ◄────────────────────────── │
│ ◄───────────────────────────── │ │
│ │ Gate 3 (Drug Interactions)│
│ │ Gate 4 (Duplicate Therapy)│
│ event: gates_complete │ ◄────────────────────────── │
│ data: {gate3, gate4 results} │ │
│ ◄───────────────────────────── │ │
│ │ │
│ event: prescription │ │
│ data: {items, gates, final} │ │
│ ◄───────────────────────────── │ │

Endpoint Reference

POST /v1/prescriptions/stream

Request Headers

HeaderValueRequired
Content-Typeapplication/jsonYes
x-api-keyYour API keyYes
Accepttext/event-streamRecommended

Request Body

FieldTypeRequiredDescription
consultation_idstringYesExternal consultation identifier (1–255 chars)
patient_idUUIDYesUUID of the patient receiving the prescription
doctor_idUUIDYesUUID of the prescribing physician
doctor_inputstringYesRaw doctor speech or text describing medications (1–10,000 chars)
streambooleanNoIf true (default), return SSE stream. If false, return a single JSON response

Example Request

{
"consultation_id": "ATD-2024-001234",
"patient_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"doctor_id": "f0e1d2c3-b4a5-6789-0abc-def123456789",
"doctor_input": "Amoxicilina 500mg via oral de 8 em 8 horas por 7 dias. Dipirona 500mg via oral se dor, máximo 6 em 6 horas.",
"stream": true
}

SSE Event Types

The stream emits five event types in a defined sequence. Your client receives them in this order and should handle each accordingly.

1. status — Processing Started

Always the first event emitted. Indicates DELPHOS is analyzing the doctor’s input.

event: status
data: {"type": "analyzing"}

2. item_detected — Medication Found

Emitted once per medication as it is parsed from the doctor’s speech. Each event includes the extracted item data and the results of per-item safety gates.

The pending_gates array lists the cross-item gates that will arrive later in the gates_complete event.

event: item_detected
data: {
"index": 0,
"item": {
"medication_name": "Amoxicilina",
"dosage": "500mg",
"route": "oral",
"frequency": "8/8h",
"duration": "7 dias",
"quantity": 21,
"unit": "comprimidos",
"instructions": null
},
"gates": {
"gate1_input_validation": {
"status": "passed",
"message": "Dados do medicamento válidos."
},
"gate2_cmed_resolution": {
"status": "passed",
"message": "Medicamento identificado na base CMED.",
"details": {
"match_tier": "auto",
"cmed_product": "AMOXICILINA 500MG CAP GEL DURA CT BL AL/AL X 21"
}
},
"gate6_controlled_substance": {
"status": "passed",
"message": "Medicamento não é substância controlada."
}
},
"pending_gates": [
"gate3_drug_interactions",
"gate4_duplicate_therapy"
]
}

Item Fields

FieldTypeDescription
medication_namestringName of the medication as spoken by the doctor
dosagestringDosage (e.g., "500mg", "500mg/5ml")
routestringAdministration route ("oral", "IV", "IM", "SC", "sublingual", "topical")
frequencystringDosing frequency (e.g., "8/8h", "1x/dia", "12/12h")
durationstring | nullTreatment duration (e.g., "7 dias", "uso contínuo")
quantityinteger | nullTotal quantity to dispense
unitstring | nullQuantity unit (e.g., "comprimidos", "mL")
instructionsstring | nullAdditional instructions (e.g., "se dor", "em jejum")

3. gates_complete — Cross-Item Analysis Done

Emitted after all items have been detected and the cross-item safety gates finish their analysis across the full prescription.

event: gates_complete
data: {
"gate3_drug_interactions": {
"status": "passed",
"details": {
"pairs_checked": 1,
"interactions_found": 0
}
},
"gate4_duplicate_therapy": {
"status": "passed",
"details": {
"duplicates_found": 0
}
}
}

When interactions are found, the gate returns detailed information:

event: gates_complete
data: {
"gate3_drug_interactions": {
"status": "warning",
"severity": "major",
"message": "Interação medicamentosa detectada entre Warfarina e Amoxicilina.",
"details": {
"pairs_checked": 3,
"interactions_found": 1,
"interactions": [
{
"drug_a": "Warfarina",
"drug_b": "Amoxicilina",
"severity": "major",
"description": "Aumento do risco de sangramento"
}
]
}
},
"gate4_duplicate_therapy": {
"status": "passed",
"details": { "duplicates_found": 0 }
}
}

4. prescription — Final Result

The complete, aggregated prescription with all items and gate results. This is the terminal success event — the stream closes after it.

event: prescription
data: {
"items": [
{
"medication_name": "Amoxicilina",
"dosage": "500mg",
"route": "oral",
"frequency": "8/8h",
"duration": "7 dias",
"quantity": 21,
"unit": "comprimidos",
"instructions": null
},
{
"medication_name": "Dipirona",
"dosage": "500mg",
"route": "oral",
"frequency": "6/6h",
"duration": null,
"quantity": null,
"unit": null,
"instructions": "se dor"
}
],
"gates_per_item": [
{
"gate1_input_validation": { "status": "passed" },
"gate2_cmed_resolution": { "status": "passed", "details": { "match_tier": "auto" } },
"gate6_controlled_substance": { "status": "passed" }
},
{
"gate1_input_validation": { "status": "passed" },
"gate2_cmed_resolution": { "status": "passed", "details": { "match_tier": "auto" } },
"gate6_controlled_substance": { "status": "passed" }
}
],
"gates_cross_item": {
"gate3_drug_interactions": { "status": "passed" },
"gate4_duplicate_therapy": { "status": "passed" }
},
"requires_confirmation": true,
"is_degraded": false
}
FieldTypeDescription
itemsarrayAll extracted medication items
gates_per_itemarrayPer-item gate results, indexed by item position
gates_cross_itemobjectCross-item gate results (gates 3 and 4)
requires_confirmationbooleanAlways true — physician must confirm before finalizing
is_degradedbooleantrue if a safety gate or service encountered an error

5. error — Processing Failed

Emitted when an unrecoverable error occurs during processing. The degraded flag indicates whether partial results may still be usable.

event: error
data: {
"code": "PROCESSING_TIMEOUT",
"message": "Processing time limit exceeded",
"degraded": true
}
Error CodeDescriptionRetry?
PROCESSING_TIMEOUTProcessing exceeded the time limitYes, with backoff
PROCESSING_ERRORInternal processing failureYes, after short delay
PARSE_ERRORCould not extract medications from input — retry once, then rephraseYes, once
INTERNAL_ERRORUnexpected server errorYes, with backoff

Safety Gates

Every prescription passes through six safety gates. DELPHOS follows the physician autonomy principle — all gates are advisory. They inform and warn, but never block the physician’s clinical decision.

GateNameScopeWhat It Checks
1Input ValidationPer-itemRequired fields are present (medication name is mandatory)
2CMED ResolutionPer-itemMatches medication against the ANVISA/CMED national database
3Drug InteractionsCross-itemChecks for drug-drug interactions across all prescription items
4Duplicate TherapyCross-itemDetects overlapping therapeutic classes among medications
6Controlled SubstancePer-itemFlags ANVISA controlled substances (Portaria 344/98, RDC 20/2011)

Gate Severity Levels

When a gate produces a warning, it includes a severity level to help your UI prioritize display:

SeverityMeaningRecommended UI Treatment
infoInformational — no concernsSubtle indicator
warningAdvisory alert — physician should reviewYellow highlight
moderateModerate concern — review recommendedOrange highlight
majorSignificant concern — careful review neededRed highlight with details
criticalSerious safety concern — demands attentionProminent red alert

CMED Resolution Tiers (Gate 2)

When Gate 2 resolves a medication against the CMED database, it reports a match tier:

TierMeaning
autoHigh-confidence match — medication automatically resolved
suggestionModerate confidence — DELPHOS suggests a CMED product for physician confirmation
noneNo match found — medication name could not be resolved in the CMED database

Client Integration

JavaScript — EventSource Pattern

/**
* Stream prescription extraction via SSE.
* Handles all five event types progressively.
*/
async function streamPrescription(consultationId, patientId, doctorId, doctorInput, onEvent) {
const response = await fetch('/v1/prescriptions/stream', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': 'YOUR_API_KEY',
'Accept': 'text/event-stream'
},
body: JSON.stringify({
consultation_id: consultationId,
patient_id: patientId,
doctor_id: doctorId,
doctor_input: doctorInput,
stream: true
})
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const blocks = buffer.split('\n\n');
buffer = blocks.pop(); // keep incomplete block in buffer
for (const block of blocks) {
if (!block.trim()) continue;
const evtMatch = block.match(/^event:\s*(.+)$/m);
const dataMatch = block.match(/^data:\s*(.+)$/m);
if (evtMatch && dataMatch) {
onEvent(evtMatch[1].trim(), JSON.parse(dataMatch[1]));
}
}
}
}
// ── Usage ──────────────────────────────────────────────
streamPrescription(
'ATD-2024-001234',
'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'f0e1d2c3-b4a5-6789-0abc-def123456789',
'Amoxicilina 500mg oral 8/8h por 7 dias. Dipirona 500mg se dor.',
(type, data) => {
switch (type) {
case 'status':
showSpinner(data.type); // "analyzing"
break;
case 'item_detected':
addItemToUI(data.index, data.item, data.gates);
break;
case 'gates_complete':
updateCrossItemGates(data); // drug interactions, duplicates
break;
case 'prescription':
showFinalPrescription(data); // all items + all gates
hideSpinner();
break;
case 'error':
showError(data.code, data.message);
hideSpinner();
break;
}
}
);

Non-Streaming Fallback

Set "stream": false to receive a single JSON response with all items and gate results at once. The response structure is identical to the prescription SSE event payload.

Terminal window
curl -X POST 'https://your-instance.delphos.app/v1/prescriptions/stream' \
-H 'Content-Type: application/json' \
-H 'x-api-key: YOUR_API_KEY' \
-d '{
"consultation_id": "ATD-2024-001234",
"patient_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"doctor_id": "f0e1d2c3-b4a5-6789-0abc-def123456789",
"doctor_input": "Amoxicilina 500mg oral 8/8h por 7 dias",
"stream": false
}'

Error Handling Mid-Stream

When an error occurs during streaming, DELPHOS emits an error event and closes the connection. Your client should implement retry logic with exponential backoff, falling back to the non-streaming endpoint after max retries.

async function streamWithRetry(params, onEvent, maxRetries = 3) {
let attempt = 0;
while (attempt < maxRetries) {
try {
await streamPrescription(
params.consultationId,
params.patientId,
params.doctorId,
params.doctorInput,
(type, data) => {
if (type === 'error' && !['PARSE_ERROR', 'PROCESSING_TIMEOUT', 'PROCESSING_ERROR'].includes(data.code)) {
throw new Error(data.code);
}
onEvent(type, data);
}
);
return; // success — exit retry loop
} catch (err) {
attempt++;
if (attempt >= maxRetries) {
// Fall back to non-streaming endpoint
const response = await fetch('/v1/prescriptions/stream', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': 'YOUR_API_KEY',
},
body: JSON.stringify({
consultation_id: params.consultationId,
patient_id: params.patientId,
doctor_id: params.doctorId,
doctor_input: params.doctorInput,
stream: false
})
});
const data = await response.json();
onEvent('prescription', data);
return;
}
// Exponential backoff: 1s, 2s, 4s
await new Promise(r => setTimeout(r, 1000 * 2 ** (attempt - 1)));
}
}
}