DEV Community

shashank ms
shashank ms

Posted on

Using LLM for Medical Text Analysis

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

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:

  1. 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.
  2. If you start processing entire patient histories that exceed typical context limits, swap the model to kimi-k2.6 on Oxlo.ai. It handles 131K context and advanced reasoning, still under the same flat per-request pricing.

Top comments (0)