We are going to build a clinical note analyzer that turns unstructured medical text into structured JSON. It is useful for developers automating EHR data entry or researchers normalizing free-form records. Because clinical notes can run long, I run this on Oxlo.ai, where the flat per-request pricing means a discharge summary costs the same as a single sentence.
What you'll need
- Python 3.10 or newer
- The OpenAI SDK:
pip install openai - An Oxlo.ai API key from https://portal.oxlo.ai
Oxlo.ai is fully OpenAI SDK compatible, so the client code below drops in without changes.
Step 1: Connect to Oxlo.ai
First, import the SDK and point the client at Oxlo.ai. I use llama-3.3-70b as the workhorse model. There are no cold starts, so the first request returns immediately.
from openai import OpenAI
client = OpenAI(base_url="https://api.oxlo.ai/v1", api_key="YOUR_OXLO_API_KEY")
response = client.chat.completions.create(
model="llama-3.3-70b",
messages=[
{"role": "system", "content": "You are a concise medical data extraction assistant."},
{"role": "user", "content": "Reply with 'connection ok' and nothing else."},
],
)
print(response.choices[0].message.content)
Step 2: Define the extraction system prompt
The system prompt is the only part the end user never sees. It locks the model into a strict JSON schema so downstream code can rely on the shape of the output.
SYSTEM_PROMPT = """
You are a clinical data extraction engine.
Read the unstructured medical note provided by the user.
Extract the following fields and return ONLY a JSON object with no markdown formatting:
- diagnoses: list of confirmed diagnoses
- symptoms: list of symptoms mentioned
- medications: list of current medications with dosage if stated
- follow_up: list of recommended follow-up actions
- severity: one of [low, moderate, high, critical] based on the overall note
If a field is not present in the text, return an empty list for that field, except severity which should be 'low'.
"""
Step 3: Build the analyzer function
Wrap the API call in a function that accepts raw text, injects the system prompt, and parses the returned JSON. I keep the client call identical to the pattern above so it is easy to audit.
import json
def analyze_clinical_note(text: str) -> dict:
response = client.chat.completions.create(
model="llama-3.3-70b",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": text},
],
)
raw = response.choices[0].message.content.strip()
# Remove accidental markdown code fences if the model emits them
if raw.startswith("
```"):
raw = raw.split("```
")[1].replace("json", "").strip()
return json.loads(raw)
Step 4: Process a long clinical note
Real discharge summaries are verbose. With token-based providers, long inputs inflate cost linearly. On Oxlo.ai, the price stays flat per request, so passing a 2,000 word note costs the same as a tweet. Here is a realistic note.
DISCHARGE_NOTE = """
Patient: Jane Doe, 68F
Admission Date: 2024-03-10
Discharge Date: 2024-03-14
Chief Complaint: Shortness of breath and bilateral lower extremity edema.
History of Present Illness: Patient presented to the ED with progressive dyspnea on exertion over the past week. She reports sleeping on three pillows and waking breathless at night. No chest pain. She has a known history of congestive heart failure with reduced ejection fraction, last documented at 35 percent.
Physical Exam: BP 142/88, HR 96, RR 22, SpO2 91 percent on room air. Jugular venous distension noted. Bilateral pitting edema to the knees. Crackles heard in bilateral lung bases.
Assessment: Acute on chronic systolic congestive heart failure exacerbation, likely triggered by dietary sodium noncompliance. Secondary hypertension, uncontrolled.
Plan:
- Restart Lisinopril 10 mg PO daily
- Increase Furosemide to 40 mg PO BID for 7 days, then reassess
- Low sodium diet counseling provided
- Follow up with cardiology within 1 week
- Daily weights recorded, call if weight increases by more than 3 pounds in 24 hours
"""
result = analyze_clinical_note(DISCHARGE_NOTE)
print(json.dumps(result, indent=2))
Step 5: Batch process multiple records
In production you will process more than one note. A simple loop keeps the code transparent. If you need higher throughput, Oxlo.ai supports streaming responses, but for extraction I prefer synchronous calls so I can validate JSON before moving to the next record.
notes = [
"Patient reports mild headache and takes acetaminophen 500 mg PRN. No follow-up needed.",
DISCHARGE_NOTE,
]
for idx, note in enumerate(notes):
try:
parsed = analyze_clinical_note(note)
print(f"Record {idx}: {json.dumps(parsed)}")
except Exception as e:
print(f"Record {idx} failed: {e}")
Run it
Save the script as medical_analyzer.py, export your key, and execute it. The output for the long discharge note should look like this.
# terminal
export OXLO_API_KEY="YOUR_OXLO_API_KEY"
python medical_analyzer.py
# example output for the discharge note
{
"diagnoses": [
"Acute on chronic systolic congestive heart failure exacerbation",
"Secondary hypertension, uncontrolled"
],
"symptoms": [
"Shortness of breath",
"bilateral lower extremity edema",
"progressive dyspnea on exertion",
"orthopnea",
"jugular venous distension",
"bilateral pitting edema",
"crackles in bilateral lung bases"
],
"medications": [
"Lisinopril 10 mg PO daily",
"Furosemide 40 mg PO BID"
],
"follow_up": [
"Follow up with cardiology within 1 week",
"Daily weights recorded, call if weight increases by more than 3 pounds in 24 hours"
],
"severity": "high"
}
Wrap-up and next steps
The extractor is now a clean function you can drop into a FastAPI endpoint or an Airflow DAG. Two concrete moves from here:
- Map the output JSON to FHIR resources, such as DocumentReference for the note and MedicationRequest for each drug, so the data feeds directly into an EHR pipeline.
- If you start processing entire patient histories that exceed typical context limits, swap the model to
kimi-k2.6on Oxlo.ai. It handles 131K context and advanced reasoning, still under the same flat per-request pricing.
Top comments (0)