06. SOAP Note Generation

Chapter 6 of 18 · 15 min

SOAP (Subjective, Objective, Assessment, Plan) notes provide the standard format for documenting patient encounters in many healthcare settings. Generating SOAP notes from encounter data—either raw transcripts or structured intake forms—offers significant workflow improvement.

The subjective section captures patient-reported symptoms and history. The objective section contains measurable findings—vital signs, exam results, lab values. The assessment synthesizes findings into clinical impressions. The plan outlines treatment approach.

# soap_generator.py
from dataclasses import dataclass
from typing import Optional, List

@dataclass
class SOAPNote:
    subjective: str
    objective: str
    assessment: str
    plan: str

class SOAPNoteGenerator:
    """Generate SOAP-formatted clinical notes from encounter data."""
    
    SOAP_TEMPLATE = """Generate a SOAP note from the following encounter data.
    Use professional clinical language appropriate for medical records.
    
    Patient Information:
    {patient_info}
    
    Chief Complaint:
    {chief_complaint}
    
    HPI (History of Present Illness):
    {hpi}
    
    Review of Systems:
    {ros}
    
    Vital Signs:
    {vitals}
    
    Physical Exam Findings:
    {physical_exam}
    
    Lab Results:
    {lab_results}
    
    Assessment and Plan (if available):
    {assessment_plan}
    
    Format the output as:
    
    S: [Subjective section]
    O: [Objective section]
    A: [Assessment section]
    P: [Plan section]
    """
    
    def __init__(self, ollama_client, model: str = "llama3.2"):
        self.ollama = ollama_client
        self.model = model
        
    def generate_from_encounter(self, encounter_data: dict) -> SOAPNote:
        """Generate SOAP note from structured encounter data."""
        # Validate required fields
        required = ["chief_complaint", "hpi"]
        for field in required:
            if not encounter_data.get(field):
                raise ValueError(f"Missing required field: {field}")
        
        prompt = self.SOAP_TEMPLATE.format(
            patient_info=encounter_data.get("patient_info", "Not provided"),
            chief_complaint=encounter_data.get("chief_complaint"),
            hpi=encounter_data.get("hpi"),
            ros=encounter_data.get("ros", "Not provided"),
            vitals=encounter_data.get("vitals", "Not documented"),
            physical_exam=encounter_data.get("physical_exam", "Not documented"),
            lab_results=encounter_data.get("lab_results", "Results pending"),
            assessment_plan=encounter_data.get("assessment_plan", "")
        )
        
        response = self.ollama.generate(prompt, model=self.model)
        return self._parse_soap_response(response)
    
    def generate_from_transcript(self, transcript: str, 
                                  context: Optional[dict] = None) -> SOAPNote:
        """Generate SOAP note from conversation transcript."""
        prompt = f"""Convert this patient encounter transcript into a SOAP note.
        Extract relevant clinical information from the conversation.
        Use professional clinical language.
        
        Transcript:
        {transcript}
        
        Additional Context (if any):
        {context or 'None provided'}
        
        Format as:
        S: [Subjective]
        O: [Objective]
        A: [Assessment]
        P: [Plan]"""
        
        response = self.ollama.generate(prompt)
        return self._parse_soap_response(response)
    
    def _parse_soap_response(self, response: str) -> SOAPNote:
        """Parse LLM output into structured SOAPNote object."""
        sections = {"S": "", "O": "", "A": "", "P": ""}
        current_section = None
        
        for line in response.split("\n"):
            if line.strip().startswith("S:"):
                current_section = "S"
                line = line[2:]
            elif line.strip().startswith("O:"):
                current_section = "O"
                line = line[2:]
            elif line.strip().startswith("A:"):
                current_section = "A"
                line = line[2:]
            elif line.strip().startswith("P:"):
                current_section = "P"
                line = line[2:]
            
            if current_section:
                sections[current_section] += line + "\n"
        
        return SOAPNote(
            subjective=sections["S"].strip(),
            objective=sections["O"].strip(),
            assessment=sections["A"].strip(),
            plan=sections["P"].strip()
        )

A critical failure mode: the model may generate plausible but clinically inappropriate assessments based on incomplete information. SOAP notes generated from encounter data must clearly indicate uncertainty when diagnoses are implied rather than confirmed. Add system prompts instructing the model to use "consistent with" language when suggesting diagnoses.

EXERCISE

Collect a transcript from a standardized patient encounter. Generate a SOAP note, then compare against a gold-standard note written by a clinician. Identify discrepancies in clinical language and assessment accuracy.