updated with real triage methodology
This commit is contained in:
parent
9a70435636
commit
7cf5510401
BIN
vitallink/backend/__pycache__/triage_engine.cpython-39.pyc
Normal file
BIN
vitallink/backend/__pycache__/triage_engine.cpython-39.pyc
Normal file
Binary file not shown.
@ -12,6 +12,7 @@ from contextlib import asynccontextmanager
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
from triage_engine import TriageEngine, VitalSigns, TriageLevel, triage_from_vitals
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# LIFESPAN MANAGEMENT
|
# LIFESPAN MANAGEMENT
|
||||||
@ -118,41 +119,40 @@ wristband_details_cache = {}
|
|||||||
|
|
||||||
|
|
||||||
def calculate_priority_score(patient: Patient) -> float:
|
def calculate_priority_score(patient: Patient) -> float:
|
||||||
score = 0.0
|
"""
|
||||||
|
Calculate priority score using centralized triage engine
|
||||||
|
"""
|
||||||
|
|
||||||
tier_scores = {"EMERGENCY": 100, "ALERT": 50, "NORMAL": 0}
|
# If no vitals yet, use basic scoring
|
||||||
score += tier_scores.get(patient.current_tier, 0)
|
if not patient.last_vitals:
|
||||||
|
severity_scores = {"severe": 50, "moderate": 30, "mild": 20}
|
||||||
|
return severity_scores.get(patient.severity, 20)
|
||||||
|
|
||||||
wait_minutes = (datetime.now() - patient.check_in_time).total_seconds() / 60
|
# Create VitalSigns object
|
||||||
if wait_minutes > 30:
|
vitals = VitalSigns(
|
||||||
score += (wait_minutes - 30) * 0.5
|
heart_rate=patient.last_vitals.get("hr_bpm", 75),
|
||||||
elif wait_minutes > 60:
|
spo2=patient.last_vitals.get("spo2", 98),
|
||||||
score += (wait_minutes - 60) * 1.0
|
temperature=patient.last_vitals.get("temp_c", 37.0),
|
||||||
|
activity=patient.last_vitals.get("activity", 0.0),
|
||||||
|
)
|
||||||
|
|
||||||
severity_scores = {"severe": 20, "moderate": 10, "mild": 5}
|
# Calculate wait time
|
||||||
score += severity_scores.get(patient.severity, 0)
|
wait_minutes = int((datetime.now() - patient.check_in_time).total_seconds() / 60)
|
||||||
|
|
||||||
if patient.last_vitals:
|
# Use triage engine to assess
|
||||||
hr = patient.last_vitals.get("hr_bpm", 75)
|
assessment = TriageEngine.assess_patient(
|
||||||
spo2 = patient.last_vitals.get("spo2", 98)
|
vitals=vitals,
|
||||||
temp = patient.last_vitals.get("temp_c", 37.0)
|
symptoms=patient.symptoms,
|
||||||
|
severity=patient.severity,
|
||||||
|
age=None, # Add age field to Patient model if needed
|
||||||
|
preexisting=[], # Add preexisting field to Patient model if needed
|
||||||
|
wait_time_minutes=wait_minutes,
|
||||||
|
)
|
||||||
|
|
||||||
if hr > 110 or hr < 50:
|
# Update patient tier based on assessment
|
||||||
score += 10
|
patient.current_tier = assessment["tier_name"]
|
||||||
if hr > 140 or hr < 40:
|
|
||||||
score += 30
|
|
||||||
|
|
||||||
if spo2 < 92:
|
return assessment["priority_score"]
|
||||||
score += 15
|
|
||||||
if spo2 < 88:
|
|
||||||
score += 40
|
|
||||||
|
|
||||||
if temp > 38.5:
|
|
||||||
score += 15
|
|
||||||
if temp > 39.5:
|
|
||||||
score += 25
|
|
||||||
|
|
||||||
return score
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@ -203,24 +203,57 @@ async def check_in_patient(data: PatientCheckIn):
|
|||||||
|
|
||||||
@app.post("/api/vitals")
|
@app.post("/api/vitals")
|
||||||
async def receive_vitals(data: VitalsData):
|
async def receive_vitals(data: VitalsData):
|
||||||
|
"""Receive vitals data from base station"""
|
||||||
|
|
||||||
patient_id = data.patient_id
|
patient_id = data.patient_id
|
||||||
|
|
||||||
if patient_id not in patients_db:
|
if patient_id not in patients_db:
|
||||||
raise HTTPException(status_code=404, detail="Patient not found")
|
raise HTTPException(status_code=404, detail="Patient not found")
|
||||||
|
|
||||||
patient = patients_db[patient_id]
|
patient = patients_db[patient_id]
|
||||||
patient.current_tier = data.tier
|
|
||||||
|
# Use triage engine to determine tier from vitals
|
||||||
|
vitals = VitalSigns(
|
||||||
|
heart_rate=data.hr_bpm,
|
||||||
|
spo2=data.spo2,
|
||||||
|
temperature=data.temp_c,
|
||||||
|
activity=data.activity,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Quick tier assessment
|
||||||
|
tier = triage_from_vitals(data.hr_bpm, data.spo2, data.temp_c)
|
||||||
|
|
||||||
|
patient.current_tier = tier
|
||||||
patient.last_vitals = data.dict()
|
patient.last_vitals = data.dict()
|
||||||
|
|
||||||
|
# Store in history
|
||||||
vitals_history[patient_id].append(data)
|
vitals_history[patient_id].append(data)
|
||||||
if len(vitals_history[patient_id]) > 1000:
|
if len(vitals_history[patient_id]) > 1000:
|
||||||
vitals_history[patient_id] = vitals_history[patient_id][-1000:]
|
vitals_history[patient_id] = vitals_history[patient_id][-1000:]
|
||||||
|
|
||||||
|
# Check for deterioration if we have history
|
||||||
|
if len(vitals_history[patient_id]) >= 3:
|
||||||
|
previous = [
|
||||||
|
VitalSigns(v.hr_bpm, v.spo2, v.temp_c, v.activity)
|
||||||
|
for v in vitals_history[patient_id][-4:-1]
|
||||||
|
]
|
||||||
|
deterioration = TriageEngine.detect_deterioration(vitals, previous)
|
||||||
|
|
||||||
|
if deterioration["deteriorating"]:
|
||||||
|
print(f"⚠️ DETERIORATION DETECTED: {patient_id}")
|
||||||
|
print(f" Concerns: {', '.join(deterioration['concerns'])}")
|
||||||
|
|
||||||
|
# Broadcast update
|
||||||
await broadcast_update(
|
await broadcast_update(
|
||||||
{"type": "vitals_update", "patient_id": patient_id, "vitals": data.dict()}
|
{
|
||||||
|
"type": "vitals_update",
|
||||||
|
"patient_id": patient_id,
|
||||||
|
"vitals": data.dict(),
|
||||||
|
"tier": tier,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
return {"status": "received"}
|
return {"status": "received", "tier": tier}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/queue")
|
@app.get("/api/queue")
|
||||||
|
|||||||
723
vitallink/backend/triage_engine.py
Normal file
723
vitallink/backend/triage_engine.py
Normal file
@ -0,0 +1,723 @@
|
|||||||
|
"""
|
||||||
|
VitalLink Triage Engine
|
||||||
|
Centralized triage assessment algorithm based on hospital emergency standards
|
||||||
|
Combines elements from US ESI, UK Manchester, and Canadian CTAS systems
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from enum import IntEnum
|
||||||
|
from typing import List, Optional, Dict
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# TRIAGE LEVELS (Lower number = Higher priority)
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TriageLevel(IntEnum):
|
||||||
|
"""
|
||||||
|
Four-tier triage system
|
||||||
|
Based on Emergency Severity Index (ESI) and similar systems
|
||||||
|
"""
|
||||||
|
|
||||||
|
LEVEL_1_EMERGENCY = 1 # Immediate life threat - needs resuscitation
|
||||||
|
LEVEL_2_WARNING = 2 # High risk - deteriorating/unstable
|
||||||
|
LEVEL_3_ALERT = 3 # Urgent - abnormal vitals or moderate risk
|
||||||
|
LEVEL_4_NORMAL = 4 # Standard - stable with minor complaints
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# VITAL SIGN THRESHOLDS
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class VitalThresholds:
|
||||||
|
"""Clinical thresholds for vital sign assessment with hysteresis"""
|
||||||
|
|
||||||
|
# Heart Rate (BPM) - Upgrade thresholds
|
||||||
|
HR_CRITICAL_LOW = 40
|
||||||
|
HR_CRITICAL_HIGH = 140
|
||||||
|
HR_ABNORMAL_LOW = 50
|
||||||
|
HR_ABNORMAL_HIGH = 110
|
||||||
|
|
||||||
|
# Heart Rate - Downgrade thresholds (with hysteresis gap)
|
||||||
|
HR_CRITICAL_LOW_RECOVERY = 45 # Must go above 45 to leave critical
|
||||||
|
HR_CRITICAL_HIGH_RECOVERY = 135 # Must go below 135 to leave critical
|
||||||
|
HR_ABNORMAL_LOW_RECOVERY = 55
|
||||||
|
HR_ABNORMAL_HIGH_RECOVERY = 105
|
||||||
|
|
||||||
|
# SpO2 (%) - Upgrade thresholds
|
||||||
|
SPO2_CRITICAL = 88
|
||||||
|
SPO2_ABNORMAL = 92
|
||||||
|
|
||||||
|
# SpO2 - Downgrade thresholds
|
||||||
|
SPO2_CRITICAL_RECOVERY = 90 # Must go above 90 to leave critical
|
||||||
|
SPO2_ABNORMAL_RECOVERY = 94 # Must go above 94 to leave abnormal
|
||||||
|
|
||||||
|
# Temperature (°C) - Upgrade thresholds
|
||||||
|
TEMP_CRITICAL_LOW = 35.0
|
||||||
|
TEMP_CRITICAL_HIGH = 39.5
|
||||||
|
TEMP_ABNORMAL_LOW = 35.5
|
||||||
|
TEMP_ABNORMAL_HIGH = 38.3
|
||||||
|
|
||||||
|
# Temperature - Downgrade thresholds
|
||||||
|
TEMP_CRITICAL_LOW_RECOVERY = 35.5
|
||||||
|
TEMP_CRITICAL_HIGH_RECOVERY = 39.0
|
||||||
|
TEMP_ABNORMAL_LOW_RECOVERY = 36.0
|
||||||
|
TEMP_ABNORMAL_HIGH_RECOVERY = 38.0
|
||||||
|
|
||||||
|
# Respiratory Rate (breaths/min)
|
||||||
|
RR_CRITICAL_LOW = 10
|
||||||
|
RR_CRITICAL_HIGH = 30
|
||||||
|
RR_ABNORMAL_LOW = 12
|
||||||
|
RR_ABNORMAL_HIGH = 20
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TierStabilityTracker:
|
||||||
|
"""Tracks tier changes to prevent oscillation"""
|
||||||
|
|
||||||
|
current_tier: str = "NORMAL"
|
||||||
|
tier_since: float = 0.0 # Timestamp when tier was set
|
||||||
|
consecutive_readings: int = 0
|
||||||
|
tier_change_count: int = 0
|
||||||
|
|
||||||
|
# Confirmation requirements
|
||||||
|
CONFIRMATION_READINGS_UPGRADE = 3 # Need 3 consecutive readings to upgrade
|
||||||
|
CONFIRMATION_READINGS_DOWNGRADE = 5 # Need 5 consecutive readings to downgrade
|
||||||
|
MIN_TIME_IN_TIER = 30.0 # Minimum 30 seconds before tier can change
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# CLINICAL KNOWLEDGE BASE
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
CRITICAL_CONDITIONS = [
|
||||||
|
"cardiac arrest",
|
||||||
|
"respiratory arrest",
|
||||||
|
"major trauma",
|
||||||
|
"severe hemorrhage",
|
||||||
|
"stroke",
|
||||||
|
"seizure ongoing",
|
||||||
|
"anaphylaxis",
|
||||||
|
"severe chest pain",
|
||||||
|
"unresponsive",
|
||||||
|
]
|
||||||
|
|
||||||
|
HIGH_RISK_SYMPTOMS = [
|
||||||
|
"chest pain",
|
||||||
|
"difficulty breathing",
|
||||||
|
"altered mental status",
|
||||||
|
"severe headache",
|
||||||
|
"excessive bleeding",
|
||||||
|
"abdominal pain",
|
||||||
|
"head injury",
|
||||||
|
"suspected sepsis",
|
||||||
|
]
|
||||||
|
|
||||||
|
MODERATE_RISK_SYMPTOMS = [
|
||||||
|
"fever",
|
||||||
|
"nausea/vomiting",
|
||||||
|
"dizziness",
|
||||||
|
"moderate pain",
|
||||||
|
"injury/trauma",
|
||||||
|
"infection signs",
|
||||||
|
]
|
||||||
|
|
||||||
|
PREEXISTING_CONDITIONS = [
|
||||||
|
"diabetes",
|
||||||
|
"copd",
|
||||||
|
"asthma",
|
||||||
|
"cancer",
|
||||||
|
"hypertension",
|
||||||
|
"heart disease",
|
||||||
|
"kidney disease",
|
||||||
|
"immunocompromised",
|
||||||
|
]
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# VITAL SIGNS DATA CLASS
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class VitalSigns:
|
||||||
|
"""Current vital signs for a patient"""
|
||||||
|
|
||||||
|
heart_rate: int
|
||||||
|
spo2: int
|
||||||
|
temperature: float
|
||||||
|
activity: float = 0.0
|
||||||
|
respiratory_rate: Optional[int] = None
|
||||||
|
blood_pressure_systolic: Optional[int] = None
|
||||||
|
consciousness_level: str = "Alert"
|
||||||
|
pain_level: int = 0 # 0-10 scale
|
||||||
|
|
||||||
|
def is_critical(self) -> bool:
|
||||||
|
"""Check if any vital is in critical range"""
|
||||||
|
return (
|
||||||
|
self.heart_rate < VitalThresholds.HR_CRITICAL_LOW
|
||||||
|
or self.heart_rate > VitalThresholds.HR_CRITICAL_HIGH
|
||||||
|
or self.spo2 < VitalThresholds.SPO2_CRITICAL
|
||||||
|
or self.temperature < VitalThresholds.TEMP_CRITICAL_LOW
|
||||||
|
or self.temperature > VitalThresholds.TEMP_CRITICAL_HIGH
|
||||||
|
or (
|
||||||
|
self.respiratory_rate
|
||||||
|
and (
|
||||||
|
self.respiratory_rate < VitalThresholds.RR_CRITICAL_LOW
|
||||||
|
or self.respiratory_rate > VitalThresholds.RR_CRITICAL_HIGH
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def is_abnormal(self) -> bool:
|
||||||
|
"""Check if any vital is abnormal but not critical"""
|
||||||
|
return (
|
||||||
|
self.heart_rate < VitalThresholds.HR_ABNORMAL_LOW
|
||||||
|
or self.heart_rate > VitalThresholds.HR_ABNORMAL_HIGH
|
||||||
|
or self.spo2 < VitalThresholds.SPO2_ABNORMAL
|
||||||
|
or self.temperature < VitalThresholds.TEMP_ABNORMAL_LOW
|
||||||
|
or self.temperature > VitalThresholds.TEMP_ABNORMAL_HIGH
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_abnormalities(self) -> List[str]:
|
||||||
|
"""Get list of abnormal vital signs"""
|
||||||
|
abnormalities = []
|
||||||
|
|
||||||
|
if self.heart_rate < VitalThresholds.HR_CRITICAL_LOW:
|
||||||
|
abnormalities.append(f"Severe Bradycardia ({self.heart_rate} bpm)")
|
||||||
|
elif self.heart_rate > VitalThresholds.HR_CRITICAL_HIGH:
|
||||||
|
abnormalities.append(f"Severe Tachycardia ({self.heart_rate} bpm)")
|
||||||
|
elif self.heart_rate < VitalThresholds.HR_ABNORMAL_LOW:
|
||||||
|
abnormalities.append(f"Bradycardia ({self.heart_rate} bpm)")
|
||||||
|
elif self.heart_rate > VitalThresholds.HR_ABNORMAL_HIGH:
|
||||||
|
abnormalities.append(f"Tachycardia ({self.heart_rate} bpm)")
|
||||||
|
|
||||||
|
if self.spo2 < VitalThresholds.SPO2_CRITICAL:
|
||||||
|
abnormalities.append(f"Critical Hypoxia ({self.spo2}%)")
|
||||||
|
elif self.spo2 < VitalThresholds.SPO2_ABNORMAL:
|
||||||
|
abnormalities.append(f"Hypoxia ({self.spo2}%)")
|
||||||
|
|
||||||
|
if self.temperature > VitalThresholds.TEMP_CRITICAL_HIGH:
|
||||||
|
abnormalities.append(f"High Fever ({self.temperature:.1f}°C)")
|
||||||
|
elif self.temperature > VitalThresholds.TEMP_ABNORMAL_HIGH:
|
||||||
|
abnormalities.append(f"Fever ({self.temperature:.1f}°C)")
|
||||||
|
elif self.temperature < VitalThresholds.TEMP_CRITICAL_LOW:
|
||||||
|
abnormalities.append(f"Hypothermia ({self.temperature:.1f}°C)")
|
||||||
|
|
||||||
|
return abnormalities
|
||||||
|
|
||||||
|
def is_critical_with_hysteresis(self, current_tier: str) -> bool:
|
||||||
|
"""Check if critical, with hysteresis based on current tier"""
|
||||||
|
|
||||||
|
if current_tier == "EMERGENCY":
|
||||||
|
# Already in EMERGENCY - use recovery thresholds (harder to leave)
|
||||||
|
return (
|
||||||
|
self.heart_rate < VitalThresholds.HR_CRITICAL_LOW_RECOVERY
|
||||||
|
or self.heart_rate > VitalThresholds.HR_CRITICAL_HIGH_RECOVERY
|
||||||
|
or self.spo2 < VitalThresholds.SPO2_CRITICAL_RECOVERY
|
||||||
|
or self.temperature < VitalThresholds.TEMP_CRITICAL_LOW_RECOVERY
|
||||||
|
or self.temperature > VitalThresholds.TEMP_CRITICAL_HIGH_RECOVERY
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Not in EMERGENCY - use standard thresholds (easier to enter)
|
||||||
|
return self.is_critical()
|
||||||
|
|
||||||
|
def is_abnormal_with_hysteresis(self, current_tier: str) -> bool:
|
||||||
|
"""Check if abnormal, with hysteresis based on current tier"""
|
||||||
|
|
||||||
|
if current_tier in ["ALERT", "EMERGENCY"]:
|
||||||
|
# Already elevated - use recovery thresholds
|
||||||
|
return (
|
||||||
|
self.heart_rate < VitalThresholds.HR_ABNORMAL_LOW_RECOVERY
|
||||||
|
or self.heart_rate > VitalThresholds.HR_ABNORMAL_HIGH_RECOVERY
|
||||||
|
or self.spo2 < VitalThresholds.SPO2_ABNORMAL_RECOVERY
|
||||||
|
or self.temperature < VitalThresholds.TEMP_ABNORMAL_LOW_RECOVERY
|
||||||
|
or self.temperature > VitalThresholds.TEMP_ABNORMAL_HIGH_RECOVERY
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Normal tier - use standard thresholds
|
||||||
|
return self.is_abnormal()
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# TRIAGE ENGINE
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TriageEngine:
|
||||||
|
"""
|
||||||
|
Central triage assessment engine
|
||||||
|
Implements hospital-standard triage algorithms
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def assess_patient(
|
||||||
|
vitals: VitalSigns,
|
||||||
|
symptoms: List[str],
|
||||||
|
severity: str,
|
||||||
|
age: Optional[int] = None,
|
||||||
|
preexisting: List[str] = None,
|
||||||
|
wait_time_minutes: int = 0,
|
||||||
|
) -> Dict:
|
||||||
|
"""
|
||||||
|
Comprehensive triage assessment
|
||||||
|
Returns triage level, priority score, and reasoning
|
||||||
|
"""
|
||||||
|
|
||||||
|
preexisting = preexisting or []
|
||||||
|
|
||||||
|
# Step 1: Check for immediate life threats
|
||||||
|
if TriageEngine._is_life_threatening(vitals, symptoms):
|
||||||
|
return {
|
||||||
|
"triage_level": TriageLevel.LEVEL_1_EMERGENCY,
|
||||||
|
"tier_name": "EMERGENCY",
|
||||||
|
"priority_score": 200.0,
|
||||||
|
"reasoning": "Life-threatening condition detected",
|
||||||
|
"abnormalities": vitals.get_abnormalities(),
|
||||||
|
"recommended_action": "Immediate intervention required",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Step 2: Assess high-risk situations
|
||||||
|
if TriageEngine._is_high_risk(vitals, symptoms, severity, age, preexisting):
|
||||||
|
base_score = 100.0
|
||||||
|
|
||||||
|
# Add points for vital abnormalities
|
||||||
|
if vitals.is_abnormal():
|
||||||
|
base_score += 30.0
|
||||||
|
|
||||||
|
# Add wait time urgency
|
||||||
|
if wait_time_minutes > 30:
|
||||||
|
base_score += (wait_time_minutes - 30) * 0.5
|
||||||
|
|
||||||
|
return {
|
||||||
|
"triage_level": TriageLevel.LEVEL_2_WARNING,
|
||||||
|
"tier_name": "ALERT",
|
||||||
|
"priority_score": base_score,
|
||||||
|
"reasoning": "High-risk patient with concerning symptoms",
|
||||||
|
"abnormalities": vitals.get_abnormalities(),
|
||||||
|
"recommended_action": "Rapid assessment needed",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Step 3: Evaluate moderate urgency
|
||||||
|
resources_needed = TriageEngine._estimate_resources(symptoms, severity)
|
||||||
|
|
||||||
|
if resources_needed >= 2 or vitals.is_abnormal():
|
||||||
|
base_score = 50.0
|
||||||
|
|
||||||
|
if vitals.is_abnormal():
|
||||||
|
base_score += 20.0
|
||||||
|
|
||||||
|
if wait_time_minutes > 45:
|
||||||
|
base_score += (wait_time_minutes - 45) * 0.8
|
||||||
|
|
||||||
|
# Check for preexisting conditions
|
||||||
|
if any(
|
||||||
|
cond.lower() in [p.lower() for p in preexisting]
|
||||||
|
for cond in PREEXISTING_CONDITIONS
|
||||||
|
):
|
||||||
|
base_score += 15.0
|
||||||
|
|
||||||
|
return {
|
||||||
|
"triage_level": TriageLevel.LEVEL_3_ALERT,
|
||||||
|
"tier_name": "ALERT",
|
||||||
|
"priority_score": base_score,
|
||||||
|
"reasoning": "Urgent care needed - abnormal vitals or complex case",
|
||||||
|
"abnormalities": vitals.get_abnormalities(),
|
||||||
|
"recommended_action": "See within 30 minutes",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Step 4: Standard care
|
||||||
|
base_score = 20.0
|
||||||
|
|
||||||
|
# Severity modifier
|
||||||
|
if severity == "severe":
|
||||||
|
base_score += 15.0
|
||||||
|
elif severity == "moderate":
|
||||||
|
base_score += 10.0
|
||||||
|
else:
|
||||||
|
base_score += 5.0
|
||||||
|
|
||||||
|
# Long wait compensation
|
||||||
|
if wait_time_minutes > 60:
|
||||||
|
base_score += (wait_time_minutes - 60) * 1.0
|
||||||
|
|
||||||
|
return {
|
||||||
|
"triage_level": TriageLevel.LEVEL_4_NORMAL,
|
||||||
|
"tier_name": "NORMAL",
|
||||||
|
"priority_score": base_score,
|
||||||
|
"reasoning": "Stable patient - standard care",
|
||||||
|
"abnormalities": [],
|
||||||
|
"recommended_action": "See within 120 minutes",
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _is_life_threatening(vitals: VitalSigns, symptoms: List[str]) -> bool:
|
||||||
|
"""Detect immediate life threats"""
|
||||||
|
|
||||||
|
# Critical vital signs
|
||||||
|
if vitals.is_critical():
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Unresponsive patient
|
||||||
|
if vitals.consciousness_level.lower() in ["unresponsive", "unconscious"]:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Critical conditions in symptoms
|
||||||
|
return any(
|
||||||
|
cond.lower() in " ".join(symptoms).lower() for cond in CRITICAL_CONDITIONS
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _is_high_risk(
|
||||||
|
vitals: VitalSigns,
|
||||||
|
symptoms: List[str],
|
||||||
|
severity: str,
|
||||||
|
age: Optional[int],
|
||||||
|
preexisting: List[str],
|
||||||
|
) -> bool:
|
||||||
|
"""Detect high-risk patients"""
|
||||||
|
|
||||||
|
# High-risk age groups
|
||||||
|
if age and (age < 1 or age > 65):
|
||||||
|
if vitals.is_abnormal() or severity == "severe":
|
||||||
|
return True
|
||||||
|
|
||||||
|
# High-risk symptoms
|
||||||
|
symptom_text = " ".join(symptoms).lower()
|
||||||
|
if any(symptom.lower() in symptom_text for symptom in HIGH_RISK_SYMPTOMS):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Severe self-reported severity with abnormal vitals
|
||||||
|
if severity == "severe" and vitals.is_abnormal():
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Preexisting conditions with concerning vitals
|
||||||
|
if preexisting and vitals.is_abnormal():
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _estimate_resources(symptoms: List[str], severity: str) -> int:
|
||||||
|
"""
|
||||||
|
Estimate number of resources needed
|
||||||
|
Resources: labs, imaging, IV access, procedures, consultations
|
||||||
|
"""
|
||||||
|
resource_count = 0
|
||||||
|
|
||||||
|
symptom_text = " ".join(symptoms).lower()
|
||||||
|
|
||||||
|
# Likely needs labs
|
||||||
|
if any(s in symptom_text for s in ["fever", "infection", "pain", "bleeding"]):
|
||||||
|
resource_count += 1
|
||||||
|
|
||||||
|
# Likely needs imaging
|
||||||
|
if any(
|
||||||
|
s in symptom_text
|
||||||
|
for s in ["head injury", "chest pain", "abdominal", "trauma"]
|
||||||
|
):
|
||||||
|
resource_count += 1
|
||||||
|
|
||||||
|
# Likely needs IV access
|
||||||
|
if any(
|
||||||
|
s in symptom_text
|
||||||
|
for s in ["difficulty breathing", "severe", "bleeding", "dehydration"]
|
||||||
|
):
|
||||||
|
resource_count += 1
|
||||||
|
|
||||||
|
# Complex intervention likely
|
||||||
|
if severity == "severe":
|
||||||
|
resource_count += 1
|
||||||
|
|
||||||
|
return resource_count
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def calculate_priority_score(
|
||||||
|
triage_level: TriageLevel,
|
||||||
|
vitals: VitalSigns,
|
||||||
|
severity: str,
|
||||||
|
wait_time_minutes: int,
|
||||||
|
preexisting: List[str] = None,
|
||||||
|
) -> float:
|
||||||
|
"""
|
||||||
|
Calculate final priority score for queue ordering
|
||||||
|
Higher score = higher priority
|
||||||
|
"""
|
||||||
|
|
||||||
|
preexisting = preexisting or []
|
||||||
|
|
||||||
|
# Base score from triage level
|
||||||
|
level_scores = {
|
||||||
|
TriageLevel.LEVEL_1_EMERGENCY: 200.0,
|
||||||
|
TriageLevel.LEVEL_2_WARNING: 100.0,
|
||||||
|
TriageLevel.LEVEL_3_ALERT: 50.0,
|
||||||
|
TriageLevel.LEVEL_4_NORMAL: 20.0,
|
||||||
|
}
|
||||||
|
score = level_scores.get(triage_level, 0.0)
|
||||||
|
|
||||||
|
# Vital sign modifiers
|
||||||
|
if vitals.is_critical():
|
||||||
|
score += 50.0
|
||||||
|
elif vitals.is_abnormal():
|
||||||
|
score += 20.0
|
||||||
|
|
||||||
|
# Specific vital concerns
|
||||||
|
if vitals.heart_rate > 140 or vitals.heart_rate < 40:
|
||||||
|
score += 30.0
|
||||||
|
if vitals.spo2 < 88:
|
||||||
|
score += 40.0
|
||||||
|
if vitals.temperature > 39.5:
|
||||||
|
score += 25.0
|
||||||
|
|
||||||
|
# Severity modifier
|
||||||
|
severity_scores = {"severe": 25, "moderate": 15, "mild": 5}
|
||||||
|
score += severity_scores.get(severity, 0)
|
||||||
|
|
||||||
|
# Wait time escalation
|
||||||
|
if wait_time_minutes > 30:
|
||||||
|
score += (wait_time_minutes - 30) * 0.5
|
||||||
|
if wait_time_minutes > 60:
|
||||||
|
score += (wait_time_minutes - 60) * 1.0 # Accelerate
|
||||||
|
if wait_time_minutes > 120:
|
||||||
|
score += (wait_time_minutes - 120) * 2.0 # Major escalation
|
||||||
|
|
||||||
|
# Preexisting conditions
|
||||||
|
if preexisting and triage_level != TriageLevel.LEVEL_1_EMERGENCY:
|
||||||
|
score += 10.0 * len(preexisting)
|
||||||
|
|
||||||
|
return round(score, 1)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def detect_deterioration(
|
||||||
|
current_vitals: VitalSigns,
|
||||||
|
previous_vitals: List[VitalSigns],
|
||||||
|
time_window_minutes: int = 10,
|
||||||
|
) -> Dict:
|
||||||
|
"""
|
||||||
|
Detect if patient is deteriorating based on vital sign trends
|
||||||
|
Returns trend analysis and recommendation
|
||||||
|
"""
|
||||||
|
|
||||||
|
if len(previous_vitals) < 3:
|
||||||
|
return {
|
||||||
|
"deteriorating": False,
|
||||||
|
"reason": "Insufficient data for trend analysis",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Take last 3 readings
|
||||||
|
recent = previous_vitals[-3:]
|
||||||
|
|
||||||
|
# Calculate trends
|
||||||
|
hr_trend = current_vitals.heart_rate - sum(v.heart_rate for v in recent) / len(
|
||||||
|
recent
|
||||||
|
)
|
||||||
|
spo2_trend = sum(v.spo2 for v in recent) / len(recent) - current_vitals.spo2
|
||||||
|
temp_trend = current_vitals.temperature - sum(
|
||||||
|
v.temperature for v in recent
|
||||||
|
) / len(recent)
|
||||||
|
|
||||||
|
# Detect concerning trends
|
||||||
|
concerns = []
|
||||||
|
|
||||||
|
if hr_trend > 15:
|
||||||
|
concerns.append(f"Increasing HR (+{hr_trend:.0f} bpm)")
|
||||||
|
if spo2_trend > 3:
|
||||||
|
concerns.append(f"Decreasing SpO2 (-{spo2_trend:.0f}%)")
|
||||||
|
if temp_trend > 0.5:
|
||||||
|
concerns.append(f"Rising temperature (+{temp_trend:.1f}°C)")
|
||||||
|
|
||||||
|
# Crossed into abnormal range
|
||||||
|
if current_vitals.is_critical() and not any(v.is_critical() for v in recent):
|
||||||
|
concerns.append("Vitals crossed into CRITICAL range")
|
||||||
|
elif current_vitals.is_abnormal() and not any(v.is_abnormal() for v in recent):
|
||||||
|
concerns.append("Vitals crossed into ABNORMAL range")
|
||||||
|
|
||||||
|
deteriorating = len(concerns) > 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
"deteriorating": deteriorating,
|
||||||
|
"concerns": concerns,
|
||||||
|
"hr_trend": hr_trend,
|
||||||
|
"spo2_trend": spo2_trend,
|
||||||
|
"temp_trend": temp_trend,
|
||||||
|
"recommendation": "Escalate care immediately"
|
||||||
|
if deteriorating
|
||||||
|
else "Continue monitoring",
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def should_escalate_tier(
|
||||||
|
current_tier: TriageLevel, vitals: VitalSigns, wait_time_minutes: int
|
||||||
|
) -> tuple[bool, Optional[TriageLevel], str]:
|
||||||
|
"""
|
||||||
|
Determine if patient tier should be escalated
|
||||||
|
Returns (should_escalate, new_tier, reason)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Always escalate if vitals become critical
|
||||||
|
if vitals.is_critical() and current_tier != TriageLevel.LEVEL_1_EMERGENCY:
|
||||||
|
return (
|
||||||
|
True,
|
||||||
|
TriageLevel.LEVEL_1_EMERGENCY,
|
||||||
|
"Vitals entered critical range",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Escalate NORMAL to ALERT if vitals abnormal
|
||||||
|
if current_tier == TriageLevel.LEVEL_4_NORMAL and vitals.is_abnormal():
|
||||||
|
return (True, TriageLevel.LEVEL_3_ALERT, "Vitals became abnormal")
|
||||||
|
|
||||||
|
# Escalate ALERT to WARNING if vitals worsening
|
||||||
|
if current_tier == TriageLevel.LEVEL_3_ALERT and vitals.is_critical():
|
||||||
|
return (
|
||||||
|
True,
|
||||||
|
TriageLevel.LEVEL_2_WARNING,
|
||||||
|
"Vitals worsening toward critical",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Time-based escalation for long waits
|
||||||
|
if wait_time_minutes > 120 and current_tier == TriageLevel.LEVEL_4_NORMAL:
|
||||||
|
return (True, TriageLevel.LEVEL_3_ALERT, "Excessive wait time (>2 hours)")
|
||||||
|
|
||||||
|
if wait_time_minutes > 180 and current_tier == TriageLevel.LEVEL_3_ALERT:
|
||||||
|
return (True, TriageLevel.LEVEL_2_WARNING, "Critical wait time (>3 hours)")
|
||||||
|
|
||||||
|
return (False, None, "No escalation needed")
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# CONVENIENCE FUNCTIONS
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def triage_from_vitals(hr: int, spo2: int, temp: float) -> str:
|
||||||
|
"""
|
||||||
|
Quick triage tier determination from vital signs only
|
||||||
|
Returns: "EMERGENCY", "ALERT", or "NORMAL"
|
||||||
|
"""
|
||||||
|
vitals = VitalSigns(heart_rate=hr, spo2=spo2, temperature=temp)
|
||||||
|
|
||||||
|
if vitals.is_critical():
|
||||||
|
return "EMERGENCY"
|
||||||
|
elif vitals.is_abnormal():
|
||||||
|
return "ALERT"
|
||||||
|
else:
|
||||||
|
return "NORMAL"
|
||||||
|
|
||||||
|
|
||||||
|
def assess_new_patient(
|
||||||
|
hr: int,
|
||||||
|
spo2: int,
|
||||||
|
temp: float,
|
||||||
|
symptoms: List[str],
|
||||||
|
severity: str,
|
||||||
|
age: Optional[int] = None,
|
||||||
|
preexisting: List[str] = None,
|
||||||
|
) -> Dict:
|
||||||
|
"""
|
||||||
|
Complete assessment for new patient
|
||||||
|
Returns full triage result with score and recommendations
|
||||||
|
"""
|
||||||
|
|
||||||
|
vitals = VitalSigns(heart_rate=hr, spo2=spo2, temperature=temp)
|
||||||
|
|
||||||
|
result = TriageEngine.assess_patient(
|
||||||
|
vitals=vitals,
|
||||||
|
symptoms=symptoms,
|
||||||
|
severity=severity,
|
||||||
|
age=age,
|
||||||
|
preexisting=preexisting,
|
||||||
|
wait_time_minutes=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def reassess_patient(
|
||||||
|
patient_data: Dict, current_vitals: Dict, wait_time_minutes: int
|
||||||
|
) -> Dict:
|
||||||
|
"""
|
||||||
|
Reassess existing patient with updated vitals and wait time
|
||||||
|
Returns updated triage assessment
|
||||||
|
"""
|
||||||
|
|
||||||
|
vitals = VitalSigns(
|
||||||
|
heart_rate=current_vitals.get("hr_bpm", 75),
|
||||||
|
spo2=current_vitals.get("spo2", 98),
|
||||||
|
temperature=current_vitals.get("temp_c", 37.0),
|
||||||
|
activity=current_vitals.get("activity", 0.0),
|
||||||
|
)
|
||||||
|
|
||||||
|
result = TriageEngine.assess_patient(
|
||||||
|
vitals=vitals,
|
||||||
|
symptoms=patient_data.get("symptoms", []),
|
||||||
|
severity=patient_data.get("severity", "moderate"),
|
||||||
|
age=patient_data.get("age"),
|
||||||
|
preexisting=patient_data.get("preexisting", []),
|
||||||
|
wait_time_minutes=wait_time_minutes,
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# EXAMPLE USAGE
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("=" * 80)
|
||||||
|
print("VitalLink Triage Engine - Test Cases")
|
||||||
|
print("=" * 80 + "\n")
|
||||||
|
|
||||||
|
# Test Case 1: Critical patient
|
||||||
|
print("Test 1: Critical Emergency")
|
||||||
|
result = assess_new_patient(
|
||||||
|
hr=155,
|
||||||
|
spo2=85,
|
||||||
|
temp=40.2,
|
||||||
|
symptoms=["Severe Chest Pain", "Difficulty Breathing"],
|
||||||
|
severity="severe",
|
||||||
|
)
|
||||||
|
print(f" Level: {result['triage_level'].name}")
|
||||||
|
print(f" Tier: {result['tier_name']}")
|
||||||
|
print(f" Score: {result['priority_score']}")
|
||||||
|
print(f" Reason: {result['reasoning']}")
|
||||||
|
print(f" Abnormalities: {result['abnormalities']}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Test Case 2: Deteriorating patient
|
||||||
|
print("Test 2: Deteriorating Patient")
|
||||||
|
result = assess_new_patient(
|
||||||
|
hr=118,
|
||||||
|
spo2=93,
|
||||||
|
temp=38.5,
|
||||||
|
symptoms=["Fever", "Difficulty Breathing"],
|
||||||
|
severity="moderate",
|
||||||
|
)
|
||||||
|
print(f" Level: {result['triage_level'].name}")
|
||||||
|
print(f" Tier: {result['tier_name']}")
|
||||||
|
print(f" Score: {result['priority_score']}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Test Case 3: Stable patient
|
||||||
|
print("Test 3: Stable Patient")
|
||||||
|
result = assess_new_patient(
|
||||||
|
hr=75, spo2=98, temp=36.9, symptoms=["Minor Pain"], severity="mild"
|
||||||
|
)
|
||||||
|
print(f" Level: {result['triage_level'].name}")
|
||||||
|
print(f" Tier: {result['tier_name']}")
|
||||||
|
print(f" Score: {result['priority_score']}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Test Case 4: Long wait escalation
|
||||||
|
print("Test 4: Long Wait Time Escalation")
|
||||||
|
vitals = VitalSigns(heart_rate=78, spo2=97, temperature=37.1)
|
||||||
|
should_escalate, new_tier, reason = TriageEngine.should_escalate_tier(
|
||||||
|
current_tier=TriageLevel.LEVEL_4_NORMAL, vitals=vitals, wait_time_minutes=130
|
||||||
|
)
|
||||||
|
print(f" Should escalate: {should_escalate}")
|
||||||
|
print(f" New tier: {new_tier.name if new_tier else 'N/A'}")
|
||||||
|
print(f" Reason: {reason}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("=" * 80)
|
||||||
@ -1,4 +0,0 @@
|
|||||||
200440
|
|
||||||
200455
|
|
||||||
200464
|
|
||||||
200523
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1 +0,0 @@
|
|||||||
200440
|
|
||||||
@ -3,7 +3,7 @@
|
|||||||
> vite
|
> vite
|
||||||
|
|
||||||
|
|
||||||
VITE v7.1.10 ready in 116 ms
|
VITE v7.1.10 ready in 107 ms
|
||||||
|
|
||||||
➜ Local: http://localhost:5173/
|
➜ Local: http://localhost:5173/
|
||||||
➜ Network: use --host to expose
|
➜ Network: use --host to expose
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
200464
|
|
||||||
@ -1 +0,0 @@
|
|||||||
200523
|
|
||||||
@ -1 +0,0 @@
|
|||||||
200455
|
|
||||||
Binary file not shown.
@ -181,18 +181,18 @@ class WristbandSimulator:
|
|||||||
return int(hr), int(spo2), temp, self.activity_level
|
return int(hr), int(spo2), temp, self.activity_level
|
||||||
|
|
||||||
def _determine_tier(self, hr: int, spo2: int, temp: float) -> tuple:
|
def _determine_tier(self, hr: int, spo2: int, temp: float) -> tuple:
|
||||||
"""Determine alert tier based on vitals"""
|
"""Determine flags based on vitals - tier determined by backend"""
|
||||||
flags = 0
|
flags = 0
|
||||||
|
|
||||||
# Battery warning
|
# Battery warning
|
||||||
if self.battery < 15:
|
if self.battery < 15:
|
||||||
flags |= VitalFlags.LOW_BATT
|
flags |= VitalFlags.LOW_BATT
|
||||||
|
|
||||||
# Check for critical vitals (EMERGENCY)
|
# Set EMERGENCY flag if critical vitals
|
||||||
if hr > 140 or hr < 45 or spo2 < 88 or temp > 39.5 or temp < 35.0:
|
if hr > 140 or hr < 45 or spo2 < 88 or temp > 39.5 or temp < 35.0:
|
||||||
flags |= VitalFlags.EMERGENCY
|
flags |= VitalFlags.EMERGENCY
|
||||||
tier = "EMERGENCY"
|
tier = "EMERGENCY"
|
||||||
# Check for concerning vitals (ALERT)
|
# Set ALERT flag if concerning vitals
|
||||||
elif hr > 110 or hr < 50 or spo2 < 92 or temp > 38.3 or temp < 35.5:
|
elif hr > 110 or hr < 50 or spo2 < 92 or temp > 38.3 or temp < 35.5:
|
||||||
flags |= VitalFlags.ALERT
|
flags |= VitalFlags.ALERT
|
||||||
tier = "ALERT"
|
tier = "ALERT"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user