Spaces:
Sleeping
Sleeping
| """ | |
| Core data models for the Decision Engine. | |
| All models are immutable dataclasses to prevent accidental mutation | |
| during safety-critical evaluation pipelines. | |
| """ | |
| from __future__ import annotations | |
| import enum | |
| from dataclasses import dataclass, field | |
| from typing import Any, Dict, List, Optional | |
| # --------------------------------------------------------------------------- | |
| # Enums | |
| # --------------------------------------------------------------------------- | |
| class ConfidenceTier(enum.Enum): | |
| """Confidence tier for taxonomy trigger matches.""" | |
| HIGH = "high" | |
| MEDIUM = "medium" | |
| LOW = "low" | |
| NONE = "none" | |
| def __ge__(self, other: ConfidenceTier) -> bool: | |
| order = {self.HIGH: 3, self.MEDIUM: 2, self.LOW: 1, self.NONE: 0} | |
| return order[self] >= order[other] | |
| def __gt__(self, other: ConfidenceTier) -> bool: | |
| order = {self.HIGH: 3, self.MEDIUM: 2, self.LOW: 1, self.NONE: 0} | |
| return order[self] > order[other] | |
| def __le__(self, other: ConfidenceTier) -> bool: | |
| order = {self.HIGH: 3, self.MEDIUM: 2, self.LOW: 1, self.NONE: 0} | |
| return order[self] <= order[other] | |
| def __lt__(self, other: ConfidenceTier) -> bool: | |
| order = {self.HIGH: 3, self.MEDIUM: 2, self.LOW: 1, self.NONE: 0} | |
| return order[self] < order[other] | |
| class RiskClass(enum.Enum): | |
| """Clinical risk classification tier.""" | |
| R0 = "R0" # No clinical concern | |
| R1 = "R1" # Low-level concern, routine follow-up | |
| R2 = "R2" # Moderate concern, warm transfer to nurse | |
| R3 = "R3" # Emergent, instruct to call 911 + transfer | |
| def is_emergent(self) -> bool: | |
| return self == RiskClass.R3 | |
| def requires_nurse(self) -> bool: | |
| return self in (RiskClass.R2, RiskClass.R3) | |
| def severity_rank(self) -> int: | |
| return {self.R0: 0, self.R1: 1, self.R2: 2, self.R3: 3}[self] | |
| def __ge__(self, other: RiskClass) -> bool: | |
| return self.severity_rank >= other.severity_rank | |
| def __gt__(self, other: RiskClass) -> bool: | |
| return self.severity_rank > other.severity_rank | |
| def __le__(self, other: RiskClass) -> bool: | |
| return self.severity_rank <= other.severity_rank | |
| def __lt__(self, other: RiskClass) -> bool: | |
| return self.severity_rank < other.severity_rank | |
| class MatchType(enum.Enum): | |
| """Type of lexical match that fired.""" | |
| PHRASE = "phrase" | |
| REGEX = "regex" | |
| PARTIAL = "partial" | |
| class NegationAction(enum.Enum): | |
| """What to do when negation is detected.""" | |
| SUPPRESS = "suppress" # Remove domain nomination entirely | |
| DOWNGRADE_CONFIDENCE = "downgrade_confidence" # Drop confidence one tier | |
| class TurnOutcome(enum.Enum): | |
| """Possible outcomes after a conversational turn.""" | |
| PROCEED = "proceed" | |
| CLARIFY = "clarify" | |
| ESCALATE = "escalate" | |
| HANDOFF = "handoff" | |
| END = "end" | |
| class FlowType(enum.Enum): | |
| """Type of conversation flow.""" | |
| SCREENING = "screening" | |
| ESCALATION = "escalation" | |
| CLARIFICATION = "clarification" | |
| RESOLUTION = "resolution" | |
| HANDOFF = "handoff" | |
| PRIMITIVE = "primitive" | |
| class CallPhase(enum.Enum): | |
| """Phase of the current call.""" | |
| SESSION_START = "session_start" | |
| ACTIVE_CALL = "active_call" | |
| ENDED = "ended" | |
| # --------------------------------------------------------------------------- | |
| # Trigger & Match Models | |
| # --------------------------------------------------------------------------- | |
| class TriggerMatch: | |
| """A single lexical match from a taxonomy trigger.""" | |
| domain: str | |
| confidence_tier: ConfidenceTier | |
| match_type: MatchType | |
| matched_text: str # The phrase/pattern that matched | |
| matched_span: str # The actual text span from patient input | |
| priority: int # Domain priority from triggers.yaml | |
| class NegationResult: | |
| """Result of negation analysis on a domain match.""" | |
| is_negated: bool | |
| action: NegationAction | |
| matched_pattern: str = "" # Which negation pattern matched | |
| original_confidence: ConfidenceTier = ConfidenceTier.NONE | |
| adjusted_confidence: ConfidenceTier = ConfidenceTier.NONE | |
| class DomainNomination: | |
| """A nominated clinical domain with combined confidence from ML + lexical signals.""" | |
| domain: str | |
| display_name: str | |
| confidence_tier: ConfidenceTier | |
| priority: int # From triggers.yaml priority field | |
| target_risk_class: Optional[RiskClass] # Default risk from rules.yaml | |
| trigger_matches: tuple # Tuple of TriggerMatch (frozen) | |
| negation_result: Optional[NegationResult] = None | |
| ml_label: Optional[str] = None # DriveHealthBERT predicted label | |
| ml_confidence: Optional[float] = None # DriveHealthBERT probability | |
| recommended_flow: Optional[str] = None # From rules.yaml decision_rules | |
| def is_negated(self) -> bool: | |
| return self.negation_result is not None and self.negation_result.is_negated | |
| def is_safety_critical(self) -> bool: | |
| """R3 domains or hard-escalate domains.""" | |
| return self.target_risk_class == RiskClass.R3 | |
| # --------------------------------------------------------------------------- | |
| # Risk Assessment Models | |
| # --------------------------------------------------------------------------- | |
| class RiskRuleMatch: | |
| """A risk escalation rule that fired.""" | |
| rule_id: str | |
| domain: str | |
| risk_class: RiskClass | |
| conditions_met: Dict[str, Any] = field(default_factory=dict) | |
| context_conditions: Optional[Dict[str, Any]] = None | |
| class SuppressionResult: | |
| """Result of domain suppression evaluation.""" | |
| suppressed: bool | |
| suppressor_domain: Optional[str] = None | |
| rule_reason: Optional[str] = None | |
| class RiskAssessment: | |
| """Complete risk assessment for a patient utterance.""" | |
| # Primary result | |
| risk_class: RiskClass | |
| primary_domain: Optional[str] | |
| # All nominated domains (ranked by priority) | |
| domain_nominations: tuple # Tuple of DomainNomination | |
| # Which rules fired | |
| matched_rules: tuple # Tuple of RiskRuleMatch | |
| # Suppression results | |
| suppressions: tuple = () # Tuple of SuppressionResult | |
| # Metadata | |
| hard_escalate: bool = False # suicidal_ideation / homicidal_ideation | |
| safety_override: bool = False # Lexical safety signal regardless of ML | |
| ml_label: Optional[str] = None | |
| ml_confidence: Optional[float] = None | |
| recommended_flow: Optional[str] = None | |
| recommended_action: Optional[TurnOutcome] = None | |
| def requires_911(self) -> bool: | |
| return self.risk_class == RiskClass.R3 | |
| def requires_nurse_transfer(self) -> bool: | |
| return self.risk_class >= RiskClass.R2 | |
| def to_dict(self) -> Dict[str, Any]: | |
| """Serialize for API response.""" | |
| return { | |
| "risk_class": self.risk_class.value, | |
| "primary_domain": self.primary_domain, | |
| "requires_911": self.requires_911, | |
| "requires_nurse_transfer": self.requires_nurse_transfer, | |
| "hard_escalate": self.hard_escalate, | |
| "safety_override": self.safety_override, | |
| "ml_label": self.ml_label, | |
| "ml_confidence": self.ml_confidence, | |
| "recommended_flow": self.recommended_flow, | |
| "recommended_action": self.recommended_action.value if self.recommended_action else None, | |
| "domain_nominations": [ | |
| { | |
| "domain": n.domain, | |
| "confidence_tier": n.confidence_tier.value, | |
| "priority": n.priority, | |
| "target_risk_class": n.target_risk_class.value if n.target_risk_class else None, | |
| "is_negated": n.is_negated, | |
| "recommended_flow": n.recommended_flow, | |
| "trigger_match_count": len(n.trigger_matches), | |
| } | |
| for n in self.domain_nominations | |
| ], | |
| "matched_rules": [ | |
| { | |
| "rule_id": r.rule_id, | |
| "domain": r.domain, | |
| "risk_class": r.risk_class.value, | |
| } | |
| for r in self.matched_rules | |
| ], | |
| } | |
| # --------------------------------------------------------------------------- | |
| # Flow Execution Models (Phase 2 — stubs for forward compatibility) | |
| # --------------------------------------------------------------------------- | |
| class ResponseSpec: | |
| """LLM response constraints for a flow step.""" | |
| spec_id: str | |
| goal: str | |
| tone: str | |
| must_include: tuple = () | |
| must_ask: tuple = () | |
| must_not: tuple = () | |
| max_questions: int = 0 | |
| class FlowStep: | |
| """A single step in a conversation flow.""" | |
| step_number: int | |
| step_type: str # "ask", "respond", "handoff" | |
| response_spec: ResponseSpec | |
| collects: tuple = () # Slot names to extract | |
| handoff_target: Optional[str] = None | |
| handoff_urgency: Optional[str] = None | |
| class ConversationFlow: | |
| """A complete conversation flow definition.""" | |
| flow_id: str | |
| flow_type: FlowType | |
| domain: Optional[str] | |
| required_slots: tuple = () | |
| steps: tuple = () # Tuple of FlowStep | |
| exit_rules: Dict[str, str] = field(default_factory=dict) | |
| # --------------------------------------------------------------------------- | |
| # Session State Models (Phase 2 — stubs for forward compatibility) | |
| # --------------------------------------------------------------------------- | |
| class SlotState: | |
| """Mutable slot tracker for an active conversation.""" | |
| slots: Dict[str, Any] = field(default_factory=dict) | |
| def set_slot(self, name: str, value: Any) -> None: | |
| self.slots[name] = value | |
| def get_slot(self, name: str) -> Optional[Any]: | |
| return self.slots.get(name) | |
| def has_slot(self, name: str) -> bool: | |
| return name in self.slots | |
| def has_all(self, slot_names: List[str]) -> bool: | |
| return all(name in self.slots for name in slot_names) | |
| class SessionState: | |
| """Mutable state for an active call session.""" | |
| session_id: str | |
| patient_id: Optional[str] = None | |
| tenant_id: Optional[str] = None | |
| call_phase: CallPhase = CallPhase.SESSION_START | |
| current_flow: Optional[str] = None | |
| current_step: int = 0 | |
| agenda_position: int = 0 | |
| slots: SlotState = field(default_factory=SlotState) | |
| domain_history: List[str] = field(default_factory=list) | |
| risk_history: List[RiskAssessment] = field(default_factory=list) | |
| turn_count: int = 0 | |