Spaces:
Sleeping
after comments from Or:
Browse filesfeat: Enhance spiritual care classification and consent messaging
- Updated the YELLOW category definition in spiritual_monitor.py to clarify ambiguity in distress indicators.
- Added follow-up purpose guidelines for YELLOW classifications.
- Introduced consent-based messaging requirements for RED classifications, ensuring patient autonomy.
- Created a new consent_message_generator.py to handle consent message generation and validation.
- Developed a provider_summary_generator.py to generate structured summaries for RED flag cases, including patient information and distress indicators.
- Modified the simplified_gradio_app.py to include patient phone number and correct classification options during conversation verification.
- Enhanced UI elements to support new classification and consent features.
- src/core/consent_message_generator.py +184 -0
- src/core/provider_summary_generator.py +242 -0
- src/core/simplified_medical_app.py +94 -35
- src/core/soft_triage_manager.py +80 -17
- src/core/spiritual_monitor.py +53 -12
- src/interface/simplified_gradio_app.py +159 -31
|
@@ -0,0 +1,184 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Consent Message Generator.
|
| 4 |
+
|
| 5 |
+
Generates consent-based messages for RED category cases, ensuring patient
|
| 6 |
+
autonomy and avoiding assumptive language.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import re
|
| 10 |
+
from typing import List, Optional
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class ConsentMessageGenerator:
|
| 14 |
+
"""Generator for consent-based messages for RED category cases."""
|
| 15 |
+
|
| 16 |
+
# Approved consent-based template
|
| 17 |
+
CONSENT_TEMPLATE = (
|
| 18 |
+
"Some patients who feel this way find it helpful to talk with "
|
| 19 |
+
"someone from our spiritual care team. Would you be open to me "
|
| 20 |
+
"sharing your information so they can reach out to you?"
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
# Alternative consent templates for variety
|
| 24 |
+
CONSENT_TEMPLATES = [
|
| 25 |
+
"Some patients who feel this way find it helpful to talk with someone from our spiritual care team. Would you be open to me sharing your information so they can reach out to you?",
|
| 26 |
+
"Many people in similar situations have found it beneficial to speak with someone from our spiritual care team. Would you be comfortable with me sharing your information so they can contact you?",
|
| 27 |
+
"Our spiritual care team has experience supporting patients who are going through similar experiences. Would you like me to share your information so someone can reach out to offer support?",
|
| 28 |
+
"Some patients find it meaningful to connect with our spiritual care team during challenging times like this. Would you be interested in having someone reach out to you?",
|
| 29 |
+
]
|
| 30 |
+
|
| 31 |
+
# Forbidden assumptive phrases that must not appear in messages
|
| 32 |
+
FORBIDDEN_PHRASES = [
|
| 33 |
+
"I'm connecting you with",
|
| 34 |
+
"You're not alone in this",
|
| 35 |
+
"I'm referring you to",
|
| 36 |
+
"Someone will reach out",
|
| 37 |
+
"I will contact",
|
| 38 |
+
"I'm arranging",
|
| 39 |
+
"I'll make sure",
|
| 40 |
+
"I'll have someone",
|
| 41 |
+
"I'm scheduling",
|
| 42 |
+
"I'm setting up",
|
| 43 |
+
"You will receive",
|
| 44 |
+
"Someone is coming",
|
| 45 |
+
"Help is on the way",
|
| 46 |
+
"I've notified",
|
| 47 |
+
"I've contacted",
|
| 48 |
+
"I've arranged",
|
| 49 |
+
"We're sending",
|
| 50 |
+
"We will contact",
|
| 51 |
+
"We're connecting",
|
| 52 |
+
"We're referring",
|
| 53 |
+
]
|
| 54 |
+
|
| 55 |
+
# Required consent language patterns
|
| 56 |
+
REQUIRED_CONSENT_PATTERNS = [
|
| 57 |
+
r"would you be (?:open to|comfortable with|interested in)",
|
| 58 |
+
r"would you like",
|
| 59 |
+
r"are you (?:open to|comfortable with|interested in)",
|
| 60 |
+
r"do you want",
|
| 61 |
+
r"would it be (?:okay|alright|helpful)",
|
| 62 |
+
]
|
| 63 |
+
|
| 64 |
+
def generate_consent_request(self, context: str = "") -> str:
|
| 65 |
+
"""
|
| 66 |
+
Generate consent request message for RED case.
|
| 67 |
+
|
| 68 |
+
Args:
|
| 69 |
+
context: Optional context about the patient's situation
|
| 70 |
+
|
| 71 |
+
Returns:
|
| 72 |
+
Consent-based message asking for patient permission
|
| 73 |
+
"""
|
| 74 |
+
# For now, use the primary template
|
| 75 |
+
# In the future, this could be enhanced to select based on context
|
| 76 |
+
return self.CONSENT_TEMPLATE
|
| 77 |
+
|
| 78 |
+
def validate_message(self, message: str) -> bool:
|
| 79 |
+
"""
|
| 80 |
+
Ensure message doesn't contain forbidden assumptive phrases.
|
| 81 |
+
|
| 82 |
+
Args:
|
| 83 |
+
message: Message to validate
|
| 84 |
+
|
| 85 |
+
Returns:
|
| 86 |
+
True if message is valid (no forbidden phrases), False otherwise
|
| 87 |
+
"""
|
| 88 |
+
if not message or not isinstance(message, str):
|
| 89 |
+
return False
|
| 90 |
+
|
| 91 |
+
message_lower = message.lower()
|
| 92 |
+
|
| 93 |
+
# Check for forbidden phrases
|
| 94 |
+
for forbidden_phrase in self.FORBIDDEN_PHRASES:
|
| 95 |
+
if forbidden_phrase.lower() in message_lower:
|
| 96 |
+
return False
|
| 97 |
+
|
| 98 |
+
return True
|
| 99 |
+
|
| 100 |
+
def contains_consent_language(self, message: str) -> bool:
|
| 101 |
+
"""
|
| 102 |
+
Check if message contains proper consent-asking language.
|
| 103 |
+
|
| 104 |
+
Args:
|
| 105 |
+
message: Message to check
|
| 106 |
+
|
| 107 |
+
Returns:
|
| 108 |
+
True if message contains consent language, False otherwise
|
| 109 |
+
"""
|
| 110 |
+
if not message or not isinstance(message, str):
|
| 111 |
+
return False
|
| 112 |
+
|
| 113 |
+
message_lower = message.lower()
|
| 114 |
+
|
| 115 |
+
# Check for required consent patterns
|
| 116 |
+
for pattern in self.REQUIRED_CONSENT_PATTERNS:
|
| 117 |
+
if re.search(pattern, message_lower):
|
| 118 |
+
return True
|
| 119 |
+
|
| 120 |
+
return False
|
| 121 |
+
|
| 122 |
+
def get_validation_errors(self, message: str) -> List[str]:
|
| 123 |
+
"""
|
| 124 |
+
Get list of validation errors for a message.
|
| 125 |
+
|
| 126 |
+
Args:
|
| 127 |
+
message: Message to validate
|
| 128 |
+
|
| 129 |
+
Returns:
|
| 130 |
+
List of validation error messages
|
| 131 |
+
"""
|
| 132 |
+
errors = []
|
| 133 |
+
|
| 134 |
+
if not message or not isinstance(message, str):
|
| 135 |
+
errors.append("Message is empty or not a string")
|
| 136 |
+
return errors
|
| 137 |
+
|
| 138 |
+
# Check for forbidden phrases
|
| 139 |
+
message_lower = message.lower()
|
| 140 |
+
for forbidden_phrase in self.FORBIDDEN_PHRASES:
|
| 141 |
+
if forbidden_phrase.lower() in message_lower:
|
| 142 |
+
errors.append(f"Contains forbidden assumptive phrase: '{forbidden_phrase}'")
|
| 143 |
+
|
| 144 |
+
# Check for consent language
|
| 145 |
+
if not self.contains_consent_language(message):
|
| 146 |
+
errors.append("Missing consent-asking language (e.g., 'Would you be open to...', 'Would you like...')")
|
| 147 |
+
|
| 148 |
+
return errors
|
| 149 |
+
|
| 150 |
+
def fix_message(self, message: str) -> str:
|
| 151 |
+
"""
|
| 152 |
+
Attempt to fix a message by replacing forbidden phrases.
|
| 153 |
+
|
| 154 |
+
Args:
|
| 155 |
+
message: Message to fix
|
| 156 |
+
|
| 157 |
+
Returns:
|
| 158 |
+
Fixed message (or original if no fixes needed)
|
| 159 |
+
"""
|
| 160 |
+
if not message or not isinstance(message, str):
|
| 161 |
+
return self.CONSENT_TEMPLATE
|
| 162 |
+
|
| 163 |
+
fixed_message = message
|
| 164 |
+
|
| 165 |
+
# Replace common assumptive phrases with consent-based alternatives
|
| 166 |
+
replacements = {
|
| 167 |
+
"I'm connecting you with": "Would you be open to connecting with",
|
| 168 |
+
"You're not alone in this": "Some patients find it helpful to know they're not alone. Would you like to connect with",
|
| 169 |
+
"I'm referring you to": "Would you be interested in speaking with",
|
| 170 |
+
"Someone will reach out": "Would you like someone to reach out",
|
| 171 |
+
"I will contact": "Would you be comfortable if I contact",
|
| 172 |
+
"I'm arranging": "Would you like me to arrange",
|
| 173 |
+
"I'll make sure": "Would you like me to make sure",
|
| 174 |
+
"I'll have someone": "Would you be open to having someone",
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
for forbidden, replacement in replacements.items():
|
| 178 |
+
fixed_message = re.sub(re.escape(forbidden), replacement, fixed_message, flags=re.IGNORECASE)
|
| 179 |
+
|
| 180 |
+
# If still not valid, return the template
|
| 181 |
+
if not self.validate_message(fixed_message) or not self.contains_consent_language(fixed_message):
|
| 182 |
+
return self.CONSENT_TEMPLATE
|
| 183 |
+
|
| 184 |
+
return fixed_message
|
|
@@ -0,0 +1,242 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Provider Summary Generator.
|
| 4 |
+
|
| 5 |
+
Generates provider-facing summaries for RED flag cases, including patient
|
| 6 |
+
situation, indicators, context from clarifying questions, and contact info.
|
| 7 |
+
|
| 8 |
+
Requirements: 6.1, 6.2, 6.3, 6.4, 6.5
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
from dataclasses import dataclass, field
|
| 12 |
+
from datetime import datetime
|
| 13 |
+
from typing import List, Optional
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
@dataclass
|
| 17 |
+
class ProviderSummary:
|
| 18 |
+
"""
|
| 19 |
+
Provider-facing summary for RED flag cases.
|
| 20 |
+
|
| 21 |
+
Contains all information needed for spiritual care team follow-up.
|
| 22 |
+
"""
|
| 23 |
+
patient_name: str = "[Patient Name]"
|
| 24 |
+
patient_phone: str = "[Phone Number]"
|
| 25 |
+
situation_description: str = ""
|
| 26 |
+
indicators: List[str] = field(default_factory=list)
|
| 27 |
+
classification: str = "RED"
|
| 28 |
+
confidence: float = 0.0
|
| 29 |
+
reasoning: str = ""
|
| 30 |
+
triage_context: List[dict] = field(default_factory=list)
|
| 31 |
+
conversation_context: str = ""
|
| 32 |
+
generated_at: str = field(default_factory=lambda: datetime.now().isoformat())
|
| 33 |
+
|
| 34 |
+
def to_dict(self) -> dict:
|
| 35 |
+
"""Convert to dictionary for export."""
|
| 36 |
+
return {
|
| 37 |
+
"patient_name": self.patient_name,
|
| 38 |
+
"patient_phone": self.patient_phone,
|
| 39 |
+
"situation_description": self.situation_description,
|
| 40 |
+
"indicators": self.indicators,
|
| 41 |
+
"classification": self.classification,
|
| 42 |
+
"confidence": self.confidence,
|
| 43 |
+
"reasoning": self.reasoning,
|
| 44 |
+
"triage_context": self.triage_context,
|
| 45 |
+
"conversation_context": self.conversation_context,
|
| 46 |
+
"generated_at": self.generated_at
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class ProviderSummaryGenerator:
|
| 51 |
+
"""
|
| 52 |
+
Generator for provider-facing summaries in RED flag cases.
|
| 53 |
+
|
| 54 |
+
Creates structured summaries for spiritual care team with patient
|
| 55 |
+
information, distress indicators, and relevant context.
|
| 56 |
+
|
| 57 |
+
Requirements: 6.1, 6.2, 6.3, 6.4
|
| 58 |
+
"""
|
| 59 |
+
|
| 60 |
+
def generate_summary(
|
| 61 |
+
self,
|
| 62 |
+
indicators: List[str],
|
| 63 |
+
reasoning: str,
|
| 64 |
+
confidence: float = 0.0,
|
| 65 |
+
patient_name: Optional[str] = None,
|
| 66 |
+
patient_phone: Optional[str] = None,
|
| 67 |
+
triage_questions: Optional[List[str]] = None,
|
| 68 |
+
triage_responses: Optional[List[str]] = None,
|
| 69 |
+
conversation_context: Optional[str] = None
|
| 70 |
+
) -> ProviderSummary:
|
| 71 |
+
"""
|
| 72 |
+
Generate provider-facing summary for RED flag case.
|
| 73 |
+
|
| 74 |
+
Args:
|
| 75 |
+
indicators: List of distress indicators detected
|
| 76 |
+
reasoning: Reasoning for RED classification
|
| 77 |
+
confidence: Confidence level (0.0-1.0)
|
| 78 |
+
patient_name: Patient name (optional, uses placeholder if not provided)
|
| 79 |
+
patient_phone: Patient phone (optional, uses placeholder if not provided)
|
| 80 |
+
triage_questions: List of triage questions asked (if any)
|
| 81 |
+
triage_responses: List of patient responses to triage (if any)
|
| 82 |
+
conversation_context: Recent conversation context
|
| 83 |
+
|
| 84 |
+
Returns:
|
| 85 |
+
ProviderSummary with all relevant information
|
| 86 |
+
|
| 87 |
+
Requirements: 6.1, 6.2, 6.4
|
| 88 |
+
"""
|
| 89 |
+
# Build triage context
|
| 90 |
+
triage_context = []
|
| 91 |
+
if triage_questions and triage_responses:
|
| 92 |
+
for q, r in zip(triage_questions, triage_responses):
|
| 93 |
+
triage_context.append({
|
| 94 |
+
"question": q,
|
| 95 |
+
"response": r
|
| 96 |
+
})
|
| 97 |
+
|
| 98 |
+
# Generate situation description from indicators and reasoning
|
| 99 |
+
situation_description = self._generate_situation_description(
|
| 100 |
+
indicators, reasoning, triage_context
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
return ProviderSummary(
|
| 104 |
+
patient_name=patient_name or "[Patient Name]",
|
| 105 |
+
patient_phone=patient_phone or "[Phone Number]",
|
| 106 |
+
situation_description=situation_description,
|
| 107 |
+
indicators=indicators,
|
| 108 |
+
classification="RED",
|
| 109 |
+
confidence=confidence,
|
| 110 |
+
reasoning=reasoning,
|
| 111 |
+
triage_context=triage_context,
|
| 112 |
+
conversation_context=conversation_context or ""
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
def _generate_situation_description(
|
| 116 |
+
self,
|
| 117 |
+
indicators: List[str],
|
| 118 |
+
reasoning: str,
|
| 119 |
+
triage_context: List[dict]
|
| 120 |
+
) -> str:
|
| 121 |
+
"""Generate brief description of patient's situation."""
|
| 122 |
+
parts = []
|
| 123 |
+
|
| 124 |
+
# Add indicator summary
|
| 125 |
+
if indicators:
|
| 126 |
+
indicator_text = ", ".join(indicators)
|
| 127 |
+
parts.append(f"Patient showing signs of: {indicator_text}.")
|
| 128 |
+
|
| 129 |
+
# Add reasoning
|
| 130 |
+
if reasoning:
|
| 131 |
+
parts.append(f"Assessment: {reasoning}")
|
| 132 |
+
|
| 133 |
+
# Add triage summary if available
|
| 134 |
+
if triage_context:
|
| 135 |
+
parts.append(f"Clarifying questions asked: {len(triage_context)}")
|
| 136 |
+
|
| 137 |
+
return " ".join(parts) if parts else "RED flag detected - spiritual care support recommended."
|
| 138 |
+
|
| 139 |
+
def format_for_display(self, summary: ProviderSummary) -> str:
|
| 140 |
+
"""
|
| 141 |
+
Format provider summary for display in UI.
|
| 142 |
+
|
| 143 |
+
Args:
|
| 144 |
+
summary: ProviderSummary to format
|
| 145 |
+
|
| 146 |
+
Returns:
|
| 147 |
+
Formatted string for display
|
| 148 |
+
|
| 149 |
+
Requirements: 6.3
|
| 150 |
+
"""
|
| 151 |
+
lines = [
|
| 152 |
+
"β" * 50,
|
| 153 |
+
"π PROVIDER SUMMARY - SPIRITUAL CARE REFERRAL",
|
| 154 |
+
"β" * 50,
|
| 155 |
+
"",
|
| 156 |
+
f"π
Generated: {summary.generated_at}",
|
| 157 |
+
"",
|
| 158 |
+
"π€ PATIENT INFORMATION",
|
| 159 |
+
"β" * 30,
|
| 160 |
+
f" Name: {summary.patient_name}",
|
| 161 |
+
f" Phone: {summary.patient_phone}",
|
| 162 |
+
"",
|
| 163 |
+
"π΄ CLASSIFICATION: RED FLAG",
|
| 164 |
+
f" Confidence: {summary.confidence:.0%}",
|
| 165 |
+
"",
|
| 166 |
+
"π SITUATION",
|
| 167 |
+
"β" * 30,
|
| 168 |
+
f" {summary.situation_description}",
|
| 169 |
+
"",
|
| 170 |
+
"β οΈ DISTRESS INDICATORS",
|
| 171 |
+
"β" * 30,
|
| 172 |
+
]
|
| 173 |
+
|
| 174 |
+
if summary.indicators:
|
| 175 |
+
for indicator in summary.indicators:
|
| 176 |
+
lines.append(f" β’ {indicator}")
|
| 177 |
+
else:
|
| 178 |
+
lines.append(" β’ No specific indicators recorded")
|
| 179 |
+
|
| 180 |
+
lines.append("")
|
| 181 |
+
lines.append("π REASONING")
|
| 182 |
+
lines.append("β" * 30)
|
| 183 |
+
lines.append(f" {summary.reasoning}")
|
| 184 |
+
|
| 185 |
+
if summary.triage_context:
|
| 186 |
+
lines.append("")
|
| 187 |
+
lines.append("π TRIAGE EXCHANGES")
|
| 188 |
+
lines.append("β" * 30)
|
| 189 |
+
for i, exchange in enumerate(summary.triage_context, 1):
|
| 190 |
+
lines.append(f" Q{i}: {exchange.get('question', 'N/A')}")
|
| 191 |
+
lines.append(f" A{i}: {exchange.get('response', 'N/A')}")
|
| 192 |
+
lines.append("")
|
| 193 |
+
|
| 194 |
+
if summary.conversation_context:
|
| 195 |
+
lines.append("")
|
| 196 |
+
lines.append("π¬ RECENT CONVERSATION")
|
| 197 |
+
lines.append("β" * 30)
|
| 198 |
+
# Truncate if too long
|
| 199 |
+
context = summary.conversation_context
|
| 200 |
+
if len(context) > 500:
|
| 201 |
+
context = context[:500] + "..."
|
| 202 |
+
lines.append(f" {context}")
|
| 203 |
+
|
| 204 |
+
lines.append("")
|
| 205 |
+
lines.append("β" * 50)
|
| 206 |
+
lines.append("RECOMMENDED ACTION: Immediate spiritual care outreach")
|
| 207 |
+
lines.append("β" * 50)
|
| 208 |
+
|
| 209 |
+
return "\n".join(lines)
|
| 210 |
+
|
| 211 |
+
def format_for_export(self, summary: ProviderSummary) -> str:
|
| 212 |
+
"""
|
| 213 |
+
Format provider summary for export (CSV/JSON).
|
| 214 |
+
|
| 215 |
+
Args:
|
| 216 |
+
summary: ProviderSummary to format
|
| 217 |
+
|
| 218 |
+
Returns:
|
| 219 |
+
Compact string suitable for export
|
| 220 |
+
|
| 221 |
+
Requirements: 6.5
|
| 222 |
+
"""
|
| 223 |
+
parts = [
|
| 224 |
+
f"Patient: {summary.patient_name} ({summary.patient_phone})",
|
| 225 |
+
f"Classification: RED ({summary.confidence:.0%})",
|
| 226 |
+
f"Indicators: {', '.join(summary.indicators) if summary.indicators else 'None'}",
|
| 227 |
+
f"Reasoning: {summary.reasoning}",
|
| 228 |
+
]
|
| 229 |
+
|
| 230 |
+
if summary.triage_context:
|
| 231 |
+
triage_summary = "; ".join([
|
| 232 |
+
f"Q: {ex.get('question', '')} A: {ex.get('response', '')}"
|
| 233 |
+
for ex in summary.triage_context
|
| 234 |
+
])
|
| 235 |
+
parts.append(f"Triage: {triage_summary}")
|
| 236 |
+
|
| 237 |
+
return " | ".join(parts)
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
def create_provider_summary_generator() -> ProviderSummaryGenerator:
|
| 241 |
+
"""Factory function to create ProviderSummaryGenerator."""
|
| 242 |
+
return ProviderSummaryGenerator()
|
|
@@ -28,6 +28,8 @@ from src.core.core_classes import (
|
|
| 28 |
ClinicalBackground, ChatMessage, SessionState,
|
| 29 |
PatientDataLoader, MedicalAssistant, SoftMedicalTriage
|
| 30 |
)
|
|
|
|
|
|
|
| 31 |
|
| 32 |
# Configure logging
|
| 33 |
logging.basicConfig(level=logging.INFO)
|
|
@@ -78,6 +80,14 @@ class SimplifiedMedicalApp:
|
|
| 78 |
# Spiritual monitoring components
|
| 79 |
self.spiritual_monitor = SpiritualMonitor(self.api)
|
| 80 |
self.soft_triage_manager = SoftTriageManager(self.api)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
|
| 82 |
# Load patient data
|
| 83 |
logger.info("π Loading patient data...")
|
|
@@ -339,9 +349,14 @@ class SimplifiedMedicalApp:
|
|
| 339 |
return self._escalate_to_red("Max triage questions reached - conservative escalation")
|
| 340 |
else:
|
| 341 |
# Update assessment for continuing triage
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 342 |
continue_assessment = SpiritualAssessment(
|
| 343 |
state=SpiritualState.YELLOW,
|
| 344 |
-
indicators=["
|
| 345 |
confidence=0.7,
|
| 346 |
reasoning=f"Continuing triage assessment - {reasoning}"
|
| 347 |
)
|
|
@@ -432,9 +447,14 @@ class SimplifiedMedicalApp:
|
|
| 432 |
patient_language = self._detect_language(last_response)
|
| 433 |
|
| 434 |
# Create assessment from triage context
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 435 |
assessment = SpiritualAssessment(
|
| 436 |
state=SpiritualState.RED,
|
| 437 |
-
indicators=["
|
| 438 |
confidence=0.8,
|
| 439 |
reasoning=reasoning
|
| 440 |
)
|
|
@@ -479,55 +499,94 @@ class SimplifiedMedicalApp:
|
|
| 479 |
language: str,
|
| 480 |
assessment: SpiritualAssessment
|
| 481 |
) -> str:
|
| 482 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 483 |
if language == "Ukrainian":
|
| 484 |
-
return """Π― ΡΡΡ Π²Π°Ρ, Ρ ΠΌΠ΅Π½Ρ Π²Π°ΠΆΠ»ΠΈΠ²ΠΎ, ΡΠΎ Π²ΠΈ ΠΏΠΎΠ΄ΡΠ»ΠΈΠ»ΠΈΡΡ ΡΠΈΠΌ Π·Ρ ΠΌΠ½ΠΎΡ. Π’Π΅, ΡΠΎ Π²ΠΈ Π²ΡΠ΄ΡΡΠ²Π°ΡΡΠ΅, ΡΠ΅ΡΠΉΠΎΠ·Π½Π΅, Ρ Π²ΠΈ Π·Π°ΡΠ»ΡΠ³ΠΎΠ²ΡΡΡΠ΅ Π½Π° ΠΏΡΠ΄ΡΡΠΈΠΌΠΊΡ.
|
| 485 |
|
| 486 |
π **ΠΠ°ΠΆΠ»ΠΈΠ²ΠΎ:** Π―ΠΊΡΠΎ Π²ΠΈ ΠΌΠ°ΡΡΠ΅ Π΄ΡΠΌΠΊΠΈ ΠΏΡΠΎ ΡΠ°ΠΌΠΎΡΡΠΊΠΎΠ΄ΠΆΠ΅Π½Π½Ρ, Π±ΡΠ΄Ρ Π»Π°ΡΠΊΠ°, Π·Π²Π΅ΡΠ½ΡΡΡΡΡ Π·Π° Π΄ΠΎΠΏΠΎΠΌΠΎΠ³ΠΎΡ:
|
| 487 |
β’ ΠΡΠ½ΡΡ Π΄ΠΎΠ²ΡΡΠΈ: 7333 (Π±Π΅Π·ΠΊΠΎΡΡΠΎΠ²Π½ΠΎ Π· ΠΌΠΎΠ±ΡΠ»ΡΠ½ΠΎΠ³ΠΎ)
|
| 488 |
β’ ΠΠ°ΠΉΡΠ»Π°ΠΉΠ½ Π£ΠΊΡΠ°ΡΠ½Π°: 0 800 500 335
|
| 489 |
|
| 490 |
-
|
| 491 |
else:
|
| 492 |
-
return """I hear you, and I'm glad you shared this with me. What you're feeling is serious, and you deserve support.
|
| 493 |
|
| 494 |
π **Important:** If you're having thoughts of self-harm, please reach out for help:
|
| 495 |
β’ National Suicide Prevention Lifeline: 988
|
| 496 |
β’ Crisis Text Line: Text HOME to 741741
|
| 497 |
|
| 498 |
-
|
| 499 |
|
| 500 |
def _generate_referral(self, assessment: SpiritualAssessment) -> str:
|
| 501 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 502 |
# Get triage context if available
|
| 503 |
-
|
|
|
|
| 504 |
if self.spiritual_state.triage_session:
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 531 |
|
| 532 |
def _detect_language(self, text: str) -> str:
|
| 533 |
"""Simple language detection based on character analysis."""
|
|
|
|
| 28 |
ClinicalBackground, ChatMessage, SessionState,
|
| 29 |
PatientDataLoader, MedicalAssistant, SoftMedicalTriage
|
| 30 |
)
|
| 31 |
+
from src.core.consent_message_generator import ConsentMessageGenerator
|
| 32 |
+
from src.core.provider_summary_generator import ProviderSummaryGenerator, ProviderSummary
|
| 33 |
|
| 34 |
# Configure logging
|
| 35 |
logging.basicConfig(level=logging.INFO)
|
|
|
|
| 80 |
# Spiritual monitoring components
|
| 81 |
self.spiritual_monitor = SpiritualMonitor(self.api)
|
| 82 |
self.soft_triage_manager = SoftTriageManager(self.api)
|
| 83 |
+
self.consent_generator = ConsentMessageGenerator()
|
| 84 |
+
self.provider_summary_generator = ProviderSummaryGenerator()
|
| 85 |
+
|
| 86 |
+
# Patient information (can be set via UI)
|
| 87 |
+
self.patient_info = {
|
| 88 |
+
"name": None,
|
| 89 |
+
"phone": None
|
| 90 |
+
}
|
| 91 |
|
| 92 |
# Load patient data
|
| 93 |
logger.info("π Loading patient data...")
|
|
|
|
| 349 |
return self._escalate_to_red("Max triage questions reached - conservative escalation")
|
| 350 |
else:
|
| 351 |
# Update assessment for continuing triage
|
| 352 |
+
# Preserve original spiritual indicators from initial assessment
|
| 353 |
+
original_indicators = []
|
| 354 |
+
if self.spiritual_state.last_assessment and self.spiritual_state.last_assessment.indicators:
|
| 355 |
+
original_indicators = self.spiritual_state.last_assessment.indicators
|
| 356 |
+
|
| 357 |
continue_assessment = SpiritualAssessment(
|
| 358 |
state=SpiritualState.YELLOW,
|
| 359 |
+
indicators=original_indicators if original_indicators else ["potential_distress"],
|
| 360 |
confidence=0.7,
|
| 361 |
reasoning=f"Continuing triage assessment - {reasoning}"
|
| 362 |
)
|
|
|
|
| 447 |
patient_language = self._detect_language(last_response)
|
| 448 |
|
| 449 |
# Create assessment from triage context
|
| 450 |
+
# Preserve original spiritual indicators from triage session
|
| 451 |
+
original_indicators = []
|
| 452 |
+
if self.spiritual_state.last_assessment and self.spiritual_state.last_assessment.indicators:
|
| 453 |
+
original_indicators = self.spiritual_state.last_assessment.indicators
|
| 454 |
+
|
| 455 |
assessment = SpiritualAssessment(
|
| 456 |
state=SpiritualState.RED,
|
| 457 |
+
indicators=original_indicators if original_indicators else ["spiritual_distress"],
|
| 458 |
confidence=0.8,
|
| 459 |
reasoning=reasoning
|
| 460 |
)
|
|
|
|
| 499 |
language: str,
|
| 500 |
assessment: SpiritualAssessment
|
| 501 |
) -> str:
|
| 502 |
+
"""
|
| 503 |
+
Generate compassionate crisis support response with consent-based messaging.
|
| 504 |
+
|
| 505 |
+
Uses ConsentMessageGenerator to ensure patient autonomy - asks for permission
|
| 506 |
+
before connecting with spiritual care team instead of assuming consent.
|
| 507 |
+
|
| 508 |
+
Requirements: 2.1, 2.2, 2.3, 2.5
|
| 509 |
+
"""
|
| 510 |
+
# Get consent request from generator
|
| 511 |
+
consent_request = self.consent_generator.generate_consent_request()
|
| 512 |
+
|
| 513 |
if language == "Ukrainian":
|
| 514 |
+
return f"""Π― ΡΡΡ Π²Π°Ρ, Ρ ΠΌΠ΅Π½Ρ Π²Π°ΠΆΠ»ΠΈΠ²ΠΎ, ΡΠΎ Π²ΠΈ ΠΏΠΎΠ΄ΡΠ»ΠΈΠ»ΠΈΡΡ ΡΠΈΠΌ Π·Ρ ΠΌΠ½ΠΎΡ. Π’Π΅, ΡΠΎ Π²ΠΈ Π²ΡΠ΄ΡΡΠ²Π°ΡΡΠ΅, ΡΠ΅ΡΠΉΠΎΠ·Π½Π΅, Ρ Π²ΠΈ Π·Π°ΡΠ»ΡΠ³ΠΎΠ²ΡΡΡΠ΅ Π½Π° ΠΏΡΠ΄ΡΡΠΈΠΌΠΊΡ.
|
| 515 |
|
| 516 |
π **ΠΠ°ΠΆΠ»ΠΈΠ²ΠΎ:** Π―ΠΊΡΠΎ Π²ΠΈ ΠΌΠ°ΡΡΠ΅ Π΄ΡΠΌΠΊΠΈ ΠΏΡΠΎ ΡΠ°ΠΌΠΎΡΡΠΊΠΎΠ΄ΠΆΠ΅Π½Π½Ρ, Π±ΡΠ΄Ρ Π»Π°ΡΠΊΠ°, Π·Π²Π΅ΡΠ½ΡΡΡΡΡ Π·Π° Π΄ΠΎΠΏΠΎΠΌΠΎΠ³ΠΎΡ:
|
| 517 |
β’ ΠΡΠ½ΡΡ Π΄ΠΎΠ²ΡΡΠΈ: 7333 (Π±Π΅Π·ΠΊΠΎΡΡΠΎΠ²Π½ΠΎ Π· ΠΌΠΎΠ±ΡΠ»ΡΠ½ΠΎΠ³ΠΎ)
|
| 518 |
β’ ΠΠ°ΠΉΡΠ»Π°ΠΉΠ½ Π£ΠΊΡΠ°ΡΠ½Π°: 0 800 500 335
|
| 519 |
|
| 520 |
+
ΠΠ΅ΡΠΊΡ ΠΏΠ°ΡΡΡΠ½ΡΠΈ, ΡΠΊΡ Π²ΡΠ΄ΡΡΠ²Π°ΡΡΡ ΠΏΠΎΠ΄ΡΠ±Π½Π΅, Π²Π²Π°ΠΆΠ°ΡΡΡ ΠΊΠΎΡΠΈΡΠ½ΠΈΠΌ ΠΏΠΎΠ³ΠΎΠ²ΠΎΡΠΈΡΠΈ Π· ΠΊΠΈΠΌΠΎΡΡ ΡΠ· Π½Π°ΡΠΎΡ ΠΊΠΎΠΌΠ°Π½Π΄ΠΈ Π΄ΡΡ
ΠΎΠ²Π½ΠΎΡ ΠΏΡΠ΄ΡΡΠΈΠΌΠΊΠΈ. Π§ΠΈ Π±ΡΠ»ΠΈ Π± Π²ΠΈ Π²ΡΠ΄ΠΊΡΠΈΡΡ Π΄ΠΎ ΡΠΎΠ³ΠΎ, ΡΠΎΠ± Ρ ΠΏΠΎΠ΄ΡΠ»ΠΈΠ²ΡΡ Π²Π°ΡΠΎΡ ΡΠ½ΡΠΎΡΠΌΠ°ΡΡΡΡ, ΡΠΎΠ± Π²ΠΎΠ½ΠΈ ΠΌΠΎΠ³Π»ΠΈ Π·Π²'ΡΠ·Π°ΡΠΈΡΡ Π· Π²Π°ΠΌΠΈ?"""
|
| 521 |
else:
|
| 522 |
+
return f"""I hear you, and I'm glad you shared this with me. What you're feeling is serious, and you deserve support.
|
| 523 |
|
| 524 |
π **Important:** If you're having thoughts of self-harm, please reach out for help:
|
| 525 |
β’ National Suicide Prevention Lifeline: 988
|
| 526 |
β’ Crisis Text Line: Text HOME to 741741
|
| 527 |
|
| 528 |
+
{consent_request}"""
|
| 529 |
|
| 530 |
def _generate_referral(self, assessment: SpiritualAssessment) -> str:
|
| 531 |
+
"""
|
| 532 |
+
Generate referral message for chaplain team using ProviderSummaryGenerator.
|
| 533 |
+
|
| 534 |
+
Requirements: 6.1, 6.2, 6.3
|
| 535 |
+
"""
|
| 536 |
# Get triage context if available
|
| 537 |
+
triage_questions = []
|
| 538 |
+
triage_responses = []
|
| 539 |
if self.spiritual_state.triage_session:
|
| 540 |
+
triage_questions = self.spiritual_state.triage_session.questions_asked
|
| 541 |
+
triage_responses = self.spiritual_state.triage_session.patient_responses
|
| 542 |
+
|
| 543 |
+
# Get patient name - prefer patient_info, fall back to clinical_background
|
| 544 |
+
patient_name = self.patient_info.get("name") or self.clinical_background.patient_name
|
| 545 |
+
patient_phone = self.patient_info.get("phone")
|
| 546 |
+
|
| 547 |
+
# Generate provider summary
|
| 548 |
+
summary = self.provider_summary_generator.generate_summary(
|
| 549 |
+
indicators=assessment.indicators,
|
| 550 |
+
reasoning=assessment.reasoning,
|
| 551 |
+
confidence=assessment.confidence,
|
| 552 |
+
patient_name=patient_name,
|
| 553 |
+
patient_phone=patient_phone,
|
| 554 |
+
triage_questions=triage_questions,
|
| 555 |
+
triage_responses=triage_responses,
|
| 556 |
+
conversation_context=self._get_conversation_context_str()
|
| 557 |
+
)
|
| 558 |
+
|
| 559 |
+
# Store the summary for later access
|
| 560 |
+
self._last_provider_summary = summary
|
| 561 |
+
|
| 562 |
+
# Return formatted summary for display
|
| 563 |
+
return self.provider_summary_generator.format_for_display(summary)
|
| 564 |
+
|
| 565 |
+
def set_patient_info(self, name: Optional[str] = None, phone: Optional[str] = None) -> None:
|
| 566 |
+
"""
|
| 567 |
+
Set patient information for provider summaries.
|
| 568 |
+
|
| 569 |
+
Args:
|
| 570 |
+
name: Patient name (optional)
|
| 571 |
+
phone: Patient phone number (optional)
|
| 572 |
+
|
| 573 |
+
Requirements: 7.1, 7.2
|
| 574 |
+
"""
|
| 575 |
+
if name:
|
| 576 |
+
self.patient_info["name"] = name
|
| 577 |
+
if phone:
|
| 578 |
+
self.patient_info["phone"] = phone
|
| 579 |
+
|
| 580 |
+
def get_last_provider_summary(self) -> Optional[ProviderSummary]:
|
| 581 |
+
"""
|
| 582 |
+
Get the last generated provider summary.
|
| 583 |
+
|
| 584 |
+
Returns:
|
| 585 |
+
ProviderSummary if one was generated, None otherwise
|
| 586 |
+
|
| 587 |
+
Requirements: 6.3
|
| 588 |
+
"""
|
| 589 |
+
return getattr(self, '_last_provider_summary', None)
|
| 590 |
|
| 591 |
def _detect_language(self, text: str) -> str:
|
| 592 |
"""Simple language detection based on character analysis."""
|
|
@@ -71,52 +71,114 @@ Respond with ONLY the question text, no JSON or formatting. Match the patient's
|
|
| 71 |
# System prompt for evaluating triage responses
|
| 72 |
SYSTEM_PROMPT_TRIAGE_EVALUATE = """<system_role>
|
| 73 |
You are evaluating a patient's response during a gentle wellness check. Based on the patient's response, determine the appropriate outcome to guide next steps.
|
|
|
|
|
|
|
| 74 |
</system_role>
|
| 75 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
<outcome_categories>
|
| 77 |
<outcome name="RESOLVED_GREEN" action="return_to_medical">
|
| 78 |
-
<description>Patient
|
| 79 |
<indicators>
|
| 80 |
-
-
|
|
|
|
| 81 |
- Describes temporary stress that is manageable
|
| 82 |
- Reports feeling better or having resources
|
| 83 |
- Shows resilience or positive outlook
|
|
|
|
| 84 |
</indicators>
|
| 85 |
-
<
|
| 86 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
</outcome>
|
| 88 |
|
| 89 |
<outcome name="ESCALATE_RED" action="generate_referral">
|
| 90 |
-
<description>Patient
|
| 91 |
<indicators>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
- Persistent hopelessness without relief
|
| 93 |
- Complete isolation with no support system
|
| 94 |
- Inability to cope or function
|
| 95 |
- Worsening symptoms or deterioration
|
| 96 |
- Continued crisis language
|
|
|
|
|
|
|
| 97 |
</indicators>
|
| 98 |
-
<
|
| 99 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
</outcome>
|
| 101 |
|
| 102 |
<outcome name="CONTINUE" action="ask_another_question">
|
| 103 |
-
<description>
|
| 104 |
<indicators>
|
| 105 |
-
- Vague or unclear response
|
| 106 |
-
- Patient deflecting or avoiding
|
| 107 |
-
- Ambiguous situation requiring clarification
|
| 108 |
- Mixed signals that need exploration
|
|
|
|
| 109 |
</indicators>
|
| 110 |
-
<
|
| 111 |
-
|
|
|
|
|
|
|
|
|
|
| 112 |
</outcome>
|
| 113 |
</outcome_categories>
|
| 114 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
<evaluation_process>
|
| 116 |
<step>Review the patient's response carefully</step>
|
| 117 |
-
<step>Identify
|
| 118 |
-
<step>
|
| 119 |
-
<step>
|
| 120 |
<step>Assess confidence in your determination</step>
|
| 121 |
</evaluation_process>
|
| 122 |
|
|
@@ -124,7 +186,8 @@ You are evaluating a patient's response during a gentle wellness check. Based on
|
|
| 124 |
Respond ONLY with valid JSON in this exact format:
|
| 125 |
{
|
| 126 |
"outcome": "resolved_green" | "escalate_red" | "continue",
|
| 127 |
-
"
|
|
|
|
| 128 |
"confidence": 0.0-1.0
|
| 129 |
}
|
| 130 |
|
|
|
|
| 71 |
# System prompt for evaluating triage responses
|
| 72 |
SYSTEM_PROMPT_TRIAGE_EVALUATE = """<system_role>
|
| 73 |
You are evaluating a patient's response during a gentle wellness check. Based on the patient's response, determine the appropriate outcome to guide next steps.
|
| 74 |
+
|
| 75 |
+
IMPORTANT: You have access to the full classification definitions to make accurate decisions.
|
| 76 |
</system_role>
|
| 77 |
|
| 78 |
+
<classification_definitions>
|
| 79 |
+
<category name="GREEN" description="No spiritual/emotional distress">
|
| 80 |
+
The patient's situation is NOT caused by and is NOT causing emotional/spiritual distress. The concern is:
|
| 81 |
+
- Due to external factors (time constraints, routine changes, logistics)
|
| 82 |
+
- Medical symptoms without emotional distress component
|
| 83 |
+
- Temporary circumstances that patient is managing well
|
| 84 |
+
- Patient has adequate support and coping mechanisms
|
| 85 |
+
</category>
|
| 86 |
+
|
| 87 |
+
<category name="RED" description="Clear spiritual/emotional distress requiring support">
|
| 88 |
+
The patient shows clear indicators of emotional or spiritual distress:
|
| 89 |
+
- Loss of meaning, purpose, or hope
|
| 90 |
+
- Sadness, despair, grief that is affecting functioning
|
| 91 |
+
- Spiritual questioning with distress (anger at God, loss of faith)
|
| 92 |
+
- Identity disruption ("I don't know who I am anymore")
|
| 93 |
+
- Isolation combined with distress
|
| 94 |
+
- Guilt, shame, or remorse causing suffering
|
| 95 |
+
- Crisis language (hopelessness, wanting to give up)
|
| 96 |
+
- Patient with mental health condition expressing distress
|
| 97 |
+
- Anticipatory emotional response causing distress
|
| 98 |
+
</category>
|
| 99 |
+
|
| 100 |
+
<category name="YELLOW" description="Ambiguous - need more information">
|
| 101 |
+
It remains UNCLEAR whether the patient's situation is caused by or is causing emotional/spiritual distress. Use this only when you genuinely cannot determine if distress is present.
|
| 102 |
+
</category>
|
| 103 |
+
</classification_definitions>
|
| 104 |
+
|
| 105 |
<outcome_categories>
|
| 106 |
<outcome name="RESOLVED_GREEN" action="return_to_medical">
|
| 107 |
+
<description>Patient's response indicates NO spiritual/emotional distress - situation is due to external factors</description>
|
| 108 |
<indicators>
|
| 109 |
+
- External causes identified: time constraints, routine changes, medical symptoms without emotional component
|
| 110 |
+
- Patient mentions coping strategies or support from others
|
| 111 |
- Describes temporary stress that is manageable
|
| 112 |
- Reports feeling better or having resources
|
| 113 |
- Shows resilience or positive outlook
|
| 114 |
+
- Concern is logistical/practical, not emotional/spiritual
|
| 115 |
</indicators>
|
| 116 |
+
<examples>
|
| 117 |
+
"I'm just having a bad day, but I have my family to talk to"
|
| 118 |
+
"It's been tough, but I'm managing with my therapist's help"
|
| 119 |
+
"I haven't been sleeping well because of my medication schedule"
|
| 120 |
+
"I'm just busy with appointments, that's why I'm stressed"
|
| 121 |
+
"My routine changed because of the treatment, but I'm adjusting"
|
| 122 |
+
</examples>
|
| 123 |
</outcome>
|
| 124 |
|
| 125 |
<outcome name="ESCALATE_RED" action="generate_referral">
|
| 126 |
+
<description>Patient's response indicates clear emotional/spiritual distress requiring support</description>
|
| 127 |
<indicators>
|
| 128 |
+
- Loss of meaning, purpose, or hope expressed
|
| 129 |
+
- Sadness, despair, grief affecting daily life
|
| 130 |
+
- Spiritual distress (anger at God, questioning faith with pain)
|
| 131 |
+
- Identity disruption or loss of self
|
| 132 |
- Persistent hopelessness without relief
|
| 133 |
- Complete isolation with no support system
|
| 134 |
- Inability to cope or function
|
| 135 |
- Worsening symptoms or deterioration
|
| 136 |
- Continued crisis language
|
| 137 |
+
- Patient with mental health condition expressing distress
|
| 138 |
+
- Anticipatory emotional response causing suffering
|
| 139 |
</indicators>
|
| 140 |
+
<examples>
|
| 141 |
+
"I feel completely alone and nothing helps anymore"
|
| 142 |
+
"Every day is worse, I can't see a way forward"
|
| 143 |
+
"I don't know who I am anymore since the diagnosis"
|
| 144 |
+
"What's the point of any of this?"
|
| 145 |
+
"I feel like God has abandoned me"
|
| 146 |
+
"I'm so sad all the time, I can't enjoy anything"
|
| 147 |
+
"I'm terrified about what's going to happen and can't stop thinking about it"
|
| 148 |
+
</examples>
|
| 149 |
</outcome>
|
| 150 |
|
| 151 |
<outcome name="CONTINUE" action="ask_another_question">
|
| 152 |
+
<description>Response is still ambiguous - need more information to determine if distress is present</description>
|
| 153 |
<indicators>
|
| 154 |
+
- Vague or unclear response that doesn't clarify cause
|
| 155 |
+
- Patient deflecting or avoiding the question
|
|
|
|
| 156 |
- Mixed signals that need exploration
|
| 157 |
+
- Cannot determine if external factors or emotional distress
|
| 158 |
</indicators>
|
| 159 |
+
<examples>
|
| 160 |
+
"I don't know, it's complicated"
|
| 161 |
+
"Maybe, I'm not sure"
|
| 162 |
+
"Things are just different now"
|
| 163 |
+
</examples>
|
| 164 |
</outcome>
|
| 165 |
</outcome_categories>
|
| 166 |
|
| 167 |
+
<yellow_flow_logic>
|
| 168 |
+
CRITICAL: The purpose of triage is to CLARIFY ambiguity. Apply these rules:
|
| 169 |
+
|
| 170 |
+
1. If patient's response indicates EXTERNAL CAUSES (time, routine, medical symptoms, logistics) β RESOLVED_GREEN
|
| 171 |
+
2. If patient's response indicates EMOTIONAL/SPIRITUAL DISTRESS (loss of meaning, sadness, despair, grief, spiritual pain) β ESCALATE_RED
|
| 172 |
+
3. If patient with known mental health condition expresses emotional distress β ESCALATE_RED
|
| 173 |
+
4. If patient expresses anticipatory emotional response causing distress β ESCALATE_RED
|
| 174 |
+
5. If response is still ambiguous and you cannot determine cause β CONTINUE (if questions remain)
|
| 175 |
+
</yellow_flow_logic>
|
| 176 |
+
|
| 177 |
<evaluation_process>
|
| 178 |
<step>Review the patient's response carefully</step>
|
| 179 |
+
<step>Identify if response indicates EXTERNAL causes (β GREEN) or EMOTIONAL/SPIRITUAL distress (β RED)</step>
|
| 180 |
+
<step>Apply the yellow_flow_logic rules</step>
|
| 181 |
+
<step>If still ambiguous and questions remain, choose CONTINUE</step>
|
| 182 |
<step>Assess confidence in your determination</step>
|
| 183 |
</evaluation_process>
|
| 184 |
|
|
|
|
| 186 |
Respond ONLY with valid JSON in this exact format:
|
| 187 |
{
|
| 188 |
"outcome": "resolved_green" | "escalate_red" | "continue",
|
| 189 |
+
"indicators": ["indicator1", "indicator2"],
|
| 190 |
+
"reasoning": "Brief explanation of why you chose this outcome based on the classification definitions",
|
| 191 |
"confidence": 0.0-1.0
|
| 192 |
}
|
| 193 |
|
|
@@ -73,8 +73,18 @@ You must classify this message into exactly ONE of the following three categorie
|
|
| 73 |
The message contains only medical symptoms, routine questions, appointment scheduling, medication inquiries, or other standard healthcare topics. There are no indicators of emotional or spiritual distress.
|
| 74 |
</category>
|
| 75 |
|
| 76 |
-
<category name="YELLOW" severity="
|
| 77 |
-
The message contains indicators
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
|
| 79 |
<emotional_expressions>
|
| 80 |
- Sleep difficulties, insomnia (Dysomnias/Difficulty sleeping)
|
|
@@ -124,16 +134,22 @@ The message contains indicators of mild to moderate distress, including:
|
|
| 124 |
</spiritual_practices>
|
| 125 |
|
| 126 |
<examples>
|
| 127 |
-
"I can't sleep at night, my mind won't stop racing"
|
| 128 |
-
"I used to love gardening, but now I can't
|
| 129 |
-
"
|
| 130 |
-
"I
|
| 131 |
-
"I'
|
| 132 |
-
"
|
| 133 |
-
"I'm
|
| 134 |
-
"I
|
| 135 |
-
"What's the meaning of all this suffering?"
|
| 136 |
</examples>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
</category>
|
| 138 |
|
| 139 |
<category name="RED" severity="severe_distress">
|
|
@@ -208,6 +224,10 @@ The message contains indicators of severe distress or crisis, including:
|
|
| 208 |
4. Spiritual questions alone (without crisis indicators) are YELLOW, not RED
|
| 209 |
5. Multiple YELLOW indicators together still remain YELLOW unless crisis language is present
|
| 210 |
6. Physical pain or medical symptoms alone are GREEN unless accompanied by emotional/spiritual distress language
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
</critical_rules>
|
| 212 |
|
| 213 |
<analysis_process>
|
|
@@ -238,7 +258,28 @@ Your response must be ONLY valid JSON in this exact format:
|
|
| 238 |
}
|
| 239 |
|
| 240 |
Do not include any text before or after the JSON object.
|
| 241 |
-
</output_format>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
|
| 243 |
|
| 244 |
class SpiritualMonitor:
|
|
|
|
| 73 |
The message contains only medical symptoms, routine questions, appointment scheduling, medication inquiries, or other standard healthcare topics. There are no indicators of emotional or spiritual distress.
|
| 74 |
</category>
|
| 75 |
|
| 76 |
+
<category name="YELLOW" severity="ambiguous_distress">
|
| 77 |
+
The message contains indicators where it is UNCLEAR whether the patient's situation is caused by or is causing emotional/spiritual distress, or if it is due to something else (medical symptoms, pain, temporary circumstances, external factors).
|
| 78 |
+
|
| 79 |
+
YELLOW is NOT about severity level - it is about AMBIGUITY. Use YELLOW when you need more information to determine if the situation warrants spiritual care support.
|
| 80 |
+
|
| 81 |
+
Common YELLOW scenarios:
|
| 82 |
+
- Patient mentions potentially distressing circumstances without expressing emotional distress
|
| 83 |
+
- Patient reports loss of loved one but hasn't expressed how they're coping emotionally
|
| 84 |
+
- Patient mentions having no help but hasn't indicated if this is causing distress
|
| 85 |
+
- Patient describes difficult situation but cause of any distress is unclear
|
| 86 |
+
|
| 87 |
+
Indicators that may warrant YELLOW classification:
|
| 88 |
|
| 89 |
<emotional_expressions>
|
| 90 |
- Sleep difficulties, insomnia (Dysomnias/Difficulty sleeping)
|
|
|
|
| 134 |
</spiritual_practices>
|
| 135 |
|
| 136 |
<examples>
|
| 137 |
+
"I can't sleep at night, my mind won't stop racing" (unclear if medical or emotional cause)
|
| 138 |
+
"I used to love gardening, but now I can't" (unclear if causing distress or just factual)
|
| 139 |
+
"My mother passed away last month" (unclear how patient is coping emotionally)
|
| 140 |
+
"I don't have anyone to help me at home" (unclear if this is causing distress)
|
| 141 |
+
"I've been feeling tired lately" (could be medical or emotional)
|
| 142 |
+
"Things have been difficult since my diagnosis" (unclear extent of emotional impact)
|
| 143 |
+
"I'm worried about my upcoming surgery" (normal concern vs spiritual distress unclear)
|
| 144 |
+
"I haven't been able to go to church lately" (unclear if causing spiritual distress)
|
|
|
|
| 145 |
</examples>
|
| 146 |
+
|
| 147 |
+
<yellow_follow_up_purpose>
|
| 148 |
+
When classifying as YELLOW, the purpose of follow-up questions is to CLARIFY:
|
| 149 |
+
- Is the situation CAUSING emotional/spiritual distress? β Escalate to RED
|
| 150 |
+
- Is the distress due to external factors (time, routine, medical symptoms)? β Downgrade to GREEN
|
| 151 |
+
- Does the patient express loss of meaning, sadness, despair, grief? β Escalate to RED
|
| 152 |
+
</yellow_follow_up_purpose>
|
| 153 |
</category>
|
| 154 |
|
| 155 |
<category name="RED" severity="severe_distress">
|
|
|
|
| 224 |
4. Spiritual questions alone (without crisis indicators) are YELLOW, not RED
|
| 225 |
5. Multiple YELLOW indicators together still remain YELLOW unless crisis language is present
|
| 226 |
6. Physical pain or medical symptoms alone are GREEN unless accompanied by emotional/spiritual distress language
|
| 227 |
+
7. Patient with known mental health condition who expresses emotional or spiritual distress β RED (regardless of diagnosis)
|
| 228 |
+
8. Patient expressing anticipatory emotional response causing distress β RED
|
| 229 |
+
9. YELLOW is about AMBIGUITY, not severity - use YELLOW when you need clarification about whether distress is present
|
| 230 |
+
10. If patient clearly expresses emotional/spiritual distress (loss of meaning, sadness, despair, grief) β RED, not YELLOW
|
| 231 |
</critical_rules>
|
| 232 |
|
| 233 |
<analysis_process>
|
|
|
|
| 258 |
}
|
| 259 |
|
| 260 |
Do not include any text before or after the JSON object.
|
| 261 |
+
</output_format>
|
| 262 |
+
|
| 263 |
+
<consent_based_messaging>
|
| 264 |
+
CRITICAL FOR RED CLASSIFICATIONS:
|
| 265 |
+
When a message is classified as RED, the system will generate a response that asks for patient CONSENT before connecting them with spiritual care support. This is essential for patient autonomy.
|
| 266 |
+
|
| 267 |
+
The response MUST:
|
| 268 |
+
- Ask for permission before sharing patient information
|
| 269 |
+
- Use phrases like "Would you be open to..." or "Would you like..."
|
| 270 |
+
- Respect patient's right to decline
|
| 271 |
+
|
| 272 |
+
The response MUST NOT:
|
| 273 |
+
- Assume the patient wants to be connected with support
|
| 274 |
+
- Use assumptive language like "I'm connecting you with..." or "Someone will reach out..."
|
| 275 |
+
- Make decisions on behalf of the patient
|
| 276 |
+
|
| 277 |
+
Example of CORRECT consent-based language:
|
| 278 |
+
"Some patients who feel this way find it helpful to talk with someone from our spiritual care team. Would you be open to me sharing your information so they can reach out to you?"
|
| 279 |
+
|
| 280 |
+
Example of INCORRECT assumptive language (DO NOT USE):
|
| 281 |
+
"I'm connecting you with our spiritual care team so someone can reach out to you personally."
|
| 282 |
+
</consent_based_messaging>"""
|
| 283 |
|
| 284 |
|
| 285 |
class SpiritualMonitor:
|
|
@@ -215,13 +215,24 @@ def create_simplified_interface():
|
|
| 215 |
|
| 216 |
# Shown only when marking Incorrect
|
| 217 |
with gr.Row(visible=False) as conv_incorrect_comment_row:
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 225 |
|
| 226 |
with gr.Row():
|
| 227 |
with gr.Column(scale=1):
|
|
@@ -430,6 +441,12 @@ def create_simplified_interface():
|
|
| 430 |
value="Serhii",
|
| 431 |
interactive=True
|
| 432 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 433 |
patient_age = gr.Number(
|
| 434 |
label="Age",
|
| 435 |
value=52,
|
|
@@ -1934,6 +1951,10 @@ To revert, use "Reset to Default" button.
|
|
| 1934 |
original_indicators=r.get("original_indicators", []),
|
| 1935 |
original_reasoning=r.get("original_reasoning", ""),
|
| 1936 |
timestamp=r.get("timestamp"),
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1937 |
)
|
| 1938 |
else:
|
| 1939 |
rec = r
|
|
@@ -1953,6 +1974,8 @@ To revert, use "Reset to Default" button.
|
|
| 1953 |
correct = 0
|
| 1954 |
incorrect = 0
|
| 1955 |
incorrect_with_comment = 0
|
|
|
|
|
|
|
| 1956 |
for x in records:
|
| 1957 |
v = (x.get("is_correct") if isinstance(x, dict) else getattr(x, "is_correct", None))
|
| 1958 |
if v is None:
|
|
@@ -1965,13 +1988,29 @@ To revert, use "Reset to Default" button.
|
|
| 1965 |
note = (x.get("verifier_notes") if isinstance(x, dict) else getattr(x, "verifier_notes", None))
|
| 1966 |
if note and str(note).strip():
|
| 1967 |
incorrect_with_comment += 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1968 |
|
| 1969 |
stats = (
|
| 1970 |
"<div style='display:flex; gap:12px; flex-wrap:wrap;'>"
|
| 1971 |
-
|
| 1972 |
-
f"<div><strong>β
Correct:</strong> {correct}</div>"
|
| 1973 |
-
f"<div><strong>β Incorrect:</strong> {incorrect}</div>"
|
| 1974 |
-
f"<div><strong>π Incorrect w/ comment:</strong> {incorrect_with_comment}</div>"
|
| 1975 |
"</div>"
|
| 1976 |
)
|
| 1977 |
return html, pos, stats
|
|
@@ -2026,6 +2065,7 @@ To revert, use "Reset to Default" button.
|
|
| 2026 |
fieldnames = [
|
| 2027 |
"session_id",
|
| 2028 |
"patient_name",
|
|
|
|
| 2029 |
"verifier_name",
|
| 2030 |
"start_time",
|
| 2031 |
"exchange_number",
|
|
@@ -2033,18 +2073,26 @@ To revert, use "Reset to Default" button.
|
|
| 2033 |
"original_classification",
|
| 2034 |
"original_confidence",
|
| 2035 |
"is_correct",
|
|
|
|
| 2036 |
"verifier_notes",
|
| 2037 |
"user_message",
|
| 2038 |
"assistant_response",
|
|
|
|
| 2039 |
]
|
| 2040 |
|
| 2041 |
with open(export_path, "w", encoding="utf-8", newline="") as f:
|
| 2042 |
w = csv.DictWriter(f, fieldnames=fieldnames)
|
| 2043 |
w.writeheader()
|
| 2044 |
for r in records or []:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2045 |
row = {
|
| 2046 |
"session_id": (meta or {}).get("session_id"),
|
| 2047 |
"patient_name": (meta or {}).get("patient_name"),
|
|
|
|
| 2048 |
"verifier_name": (meta or {}).get("verifier_name"),
|
| 2049 |
"start_time": (meta or {}).get("start_time"),
|
| 2050 |
"exchange_number": r.get("exchange_number"),
|
|
@@ -2052,9 +2100,11 @@ To revert, use "Reset to Default" button.
|
|
| 2052 |
"original_classification": r.get("original_classification"),
|
| 2053 |
"original_confidence": r.get("original_confidence"),
|
| 2054 |
"is_correct": r.get("is_correct"),
|
|
|
|
| 2055 |
"verifier_notes": r.get("verifier_notes") or "",
|
| 2056 |
"user_message": r.get("user_message"),
|
| 2057 |
"assistant_response": r.get("assistant_response"),
|
|
|
|
| 2058 |
}
|
| 2059 |
w.writerow(row)
|
| 2060 |
return export_path
|
|
@@ -2069,13 +2119,26 @@ To revert, use "Reset to Default" button.
|
|
| 2069 |
manager = ConversationVerificationManager()
|
| 2070 |
vs = manager.create_verification_session(session.app_instance.conversation_logger, "Medical Professional")
|
| 2071 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2072 |
meta = {
|
| 2073 |
"session_id": vs.session_id,
|
| 2074 |
"patient_name": vs.patient_name,
|
|
|
|
| 2075 |
"verifier_name": vs.verifier_name,
|
| 2076 |
"start_time": vs.start_time.isoformat() if hasattr(vs, "start_time") else None,
|
| 2077 |
}
|
| 2078 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2079 |
records_as_dicts = [
|
| 2080 |
{
|
| 2081 |
"exchange_id": r.exchange_id,
|
|
@@ -2092,6 +2155,7 @@ To revert, use "Reset to Default" button.
|
|
| 2092 |
"correct_classification": r.correct_classification,
|
| 2093 |
"correction_reason": r.correction_reason,
|
| 2094 |
"verifier_notes": r.verifier_notes,
|
|
|
|
| 2095 |
}
|
| 2096 |
for r in vs.verification_records
|
| 2097 |
]
|
|
@@ -2100,41 +2164,62 @@ To revert, use "Reset to Default" button.
|
|
| 2100 |
|
| 2101 |
def _mark_conv_correct(records: list, idx: int):
|
| 2102 |
if not records:
|
| 2103 |
-
return records, idx, "", "", "", gr.update(visible=False), ""
|
| 2104 |
idx = max(0, min(idx, len(records) - 1))
|
| 2105 |
if isinstance(records[idx], dict):
|
| 2106 |
records[idx]["is_correct"] = True
|
| 2107 |
-
# clear comment when marked correct (avoid stale
|
| 2108 |
records[idx]["verifier_notes"] = ""
|
|
|
|
| 2109 |
html, pos, stats = _render_conv_exchange(records, idx)
|
| 2110 |
row_upd, note_val = _comment_ui_state(records, idx)
|
| 2111 |
-
return records, idx, "β
Marked correct", html, pos, stats, row_upd, note_val
|
| 2112 |
|
| 2113 |
def _mark_conv_incorrect(records: list, idx: int):
|
| 2114 |
if not records:
|
| 2115 |
-
return records, idx, "", "", "", gr.update(visible=False), ""
|
| 2116 |
idx = max(0, min(idx, len(records) - 1))
|
| 2117 |
if isinstance(records[idx], dict):
|
| 2118 |
records[idx]["is_correct"] = False
|
| 2119 |
html, pos, stats = _render_conv_exchange(records, idx)
|
| 2120 |
row_upd, note_val = _comment_ui_state(records, idx)
|
| 2121 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2122 |
|
| 2123 |
def _show_incorrect_comment_ui(records: list, idx: int):
|
| 2124 |
"""Mark incorrect and open the comment row, pre-filling any existing note."""
|
| 2125 |
-
records, idx, status, html, pos, stats, _row, note = _mark_conv_incorrect(records, idx)
|
| 2126 |
-
return records, idx, status, html, pos, stats, gr.update(visible=True), note
|
| 2127 |
|
| 2128 |
-
def _save_incorrect_comment(records: list, idx: int, note: str):
|
| 2129 |
if not records:
|
| 2130 |
-
return records, idx, "", "", "", "", gr.update(visible=False), ""
|
| 2131 |
idx = max(0, min(idx, len(records) - 1))
|
| 2132 |
if isinstance(records[idx], dict):
|
| 2133 |
records[idx]["verifier_notes"] = (note or "").strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2134 |
html, pos, stats = _render_conv_exchange(records, idx)
|
| 2135 |
row_upd, note_val = _comment_ui_state(records, idx)
|
| 2136 |
# keep row visible after save (since still incorrect)
|
| 2137 |
-
return records, idx, "πΎ Comment saved", html, pos, stats, row_upd, note_val
|
| 2138 |
|
| 2139 |
def _download_reviewed_json(meta: dict, records: list):
|
| 2140 |
return _export_conv_records_to_json(meta, records)
|
|
@@ -2144,11 +2229,23 @@ To revert, use "Reset to Default" button.
|
|
| 2144 |
|
| 2145 |
def _nav_conv(records: list, idx: int, delta: int):
|
| 2146 |
if not records:
|
| 2147 |
-
return idx, "", "", "", gr.update(visible=False), ""
|
| 2148 |
idx = max(0, min(idx + delta, len(records) - 1))
|
| 2149 |
html, pos, stats = _render_conv_exchange(records, idx)
|
| 2150 |
row_upd, note_val = _comment_ui_state(records, idx)
|
| 2151 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2152 |
|
| 2153 |
generate_conv_verification_btn.click(
|
| 2154 |
_generate_conv_verification,
|
|
@@ -2180,6 +2277,7 @@ To revert, use "Reset to Default" button.
|
|
| 2180 |
conv_stats,
|
| 2181 |
conv_incorrect_comment_row,
|
| 2182 |
conv_incorrect_comment,
|
|
|
|
| 2183 |
]
|
| 2184 |
)
|
| 2185 |
|
|
@@ -2195,12 +2293,13 @@ To revert, use "Reset to Default" button.
|
|
| 2195 |
conv_stats,
|
| 2196 |
conv_incorrect_comment_row,
|
| 2197 |
conv_incorrect_comment,
|
|
|
|
| 2198 |
]
|
| 2199 |
)
|
| 2200 |
|
| 2201 |
conv_save_comment_btn.click(
|
| 2202 |
_save_incorrect_comment,
|
| 2203 |
-
inputs=[conv_verify_records, conv_verify_index, conv_incorrect_comment],
|
| 2204 |
outputs=[
|
| 2205 |
conv_verify_records,
|
| 2206 |
conv_verify_index,
|
|
@@ -2210,19 +2309,20 @@ To revert, use "Reset to Default" button.
|
|
| 2210 |
conv_stats,
|
| 2211 |
conv_incorrect_comment_row,
|
| 2212 |
conv_incorrect_comment,
|
|
|
|
| 2213 |
]
|
| 2214 |
)
|
| 2215 |
|
| 2216 |
conv_prev_btn.click(
|
| 2217 |
lambda records, idx: _nav_conv(records, idx, -1),
|
| 2218 |
inputs=[conv_verify_records, conv_verify_index],
|
| 2219 |
-
outputs=[conv_verify_index, conv_verify_exchange, conv_position, conv_stats, conv_incorrect_comment_row, conv_incorrect_comment]
|
| 2220 |
)
|
| 2221 |
|
| 2222 |
conv_next_btn.click(
|
| 2223 |
lambda records, idx: _nav_conv(records, idx, 1),
|
| 2224 |
inputs=[conv_verify_records, conv_verify_index],
|
| 2225 |
-
outputs=[conv_verify_index, conv_verify_exchange, conv_position, conv_stats, conv_incorrect_comment_row, conv_incorrect_comment]
|
| 2226 |
)
|
| 2227 |
|
| 2228 |
# Refresh conversation stats
|
|
@@ -2334,6 +2434,7 @@ To revert, use "Reset to Default" button.
|
|
| 2334 |
profiles = {
|
| 2335 |
"Default (Serhii)": {
|
| 2336 |
"name": "Serhii",
|
|
|
|
| 2337 |
"age": 52,
|
| 2338 |
"conditions": "Atrial fibrillation, Deep vein thrombosis, Obesity, Hypertension",
|
| 2339 |
"goal": "Weight reduction and cardiovascular fitness improvement",
|
|
@@ -2342,6 +2443,7 @@ To revert, use "Reset to Default" button.
|
|
| 2342 |
},
|
| 2343 |
"π’ GREEN - Healthy": {
|
| 2344 |
"name": "James",
|
|
|
|
| 2345 |
"age": 40,
|
| 2346 |
"conditions": "No chronic conditions, Excellent health",
|
| 2347 |
"goal": "Maintain fitness and wellness",
|
|
@@ -2350,6 +2452,7 @@ To revert, use "Reset to Default" button.
|
|
| 2350 |
},
|
| 2351 |
"π‘ YELLOW - Mild Distress": {
|
| 2352 |
"name": "Lisa",
|
|
|
|
| 2353 |
"age": 45,
|
| 2354 |
"conditions": "Hypertension, Mild anxiety, Sleep issues",
|
| 2355 |
"goal": "Manage stress and improve sleep quality",
|
|
@@ -2358,6 +2461,7 @@ To revert, use "Reset to Default" button.
|
|
| 2358 |
},
|
| 2359 |
"π‘ YELLOW - Grief & Loss": {
|
| 2360 |
"name": "Michael",
|
|
|
|
| 2361 |
"age": 58,
|
| 2362 |
"conditions": "Recent loss of spouse, Mild depression",
|
| 2363 |
"goal": "Process grief and rebuild routine",
|
|
@@ -2366,6 +2470,7 @@ To revert, use "Reset to Default" button.
|
|
| 2366 |
},
|
| 2367 |
"π‘ YELLOW - Existential Questions": {
|
| 2368 |
"name": "Patricia",
|
|
|
|
| 2369 |
"age": 62,
|
| 2370 |
"conditions": "Chronic pain, Questioning life purpose",
|
| 2371 |
"goal": "Find meaning and manage chronic pain",
|
|
@@ -2374,6 +2479,7 @@ To revert, use "Reset to Default" button.
|
|
| 2374 |
},
|
| 2375 |
"π‘ YELLOW - Spiritual Disconnection": {
|
| 2376 |
"name": "David",
|
|
|
|
| 2377 |
"age": 55,
|
| 2378 |
"conditions": "Loss of faith, Isolation from community",
|
| 2379 |
"goal": "Reconnect with spiritual community",
|
|
@@ -2382,6 +2488,7 @@ To revert, use "Reset to Default" button.
|
|
| 2382 |
},
|
| 2383 |
"π΄ RED - Crisis (Suicidal)": {
|
| 2384 |
"name": "Thomas",
|
|
|
|
| 2385 |
"age": 35,
|
| 2386 |
"conditions": "Severe depression, Suicidal ideation",
|
| 2387 |
"goal": "Immediate crisis intervention and support",
|
|
@@ -2390,6 +2497,7 @@ To revert, use "Reset to Default" button.
|
|
| 2390 |
},
|
| 2391 |
"π΄ RED - Severe Hopelessness": {
|
| 2392 |
"name": "Jennifer",
|
|
|
|
| 2393 |
"age": 48,
|
| 2394 |
"conditions": "Major depression, Complete hopelessness",
|
| 2395 |
"goal": "Crisis stabilization and professional support",
|
|
@@ -2398,6 +2506,7 @@ To revert, use "Reset to Default" button.
|
|
| 2398 |
},
|
| 2399 |
"π΄ RED - Spiritual Crisis": {
|
| 2400 |
"name": "Christopher",
|
|
|
|
| 2401 |
"age": 52,
|
| 2402 |
"conditions": "Moral injury, Spiritual crisis, Anger at God",
|
| 2403 |
"goal": "Spiritual crisis intervention and healing",
|
|
@@ -2406,6 +2515,7 @@ To revert, use "Reset to Default" button.
|
|
| 2406 |
},
|
| 2407 |
"Cardiac Patient": {
|
| 2408 |
"name": "John",
|
|
|
|
| 2409 |
"age": 65,
|
| 2410 |
"conditions": "Coronary artery disease, Hypertension, Hyperlipidemia",
|
| 2411 |
"goal": "Cardiac rehabilitation and risk factor management",
|
|
@@ -2414,6 +2524,7 @@ To revert, use "Reset to Default" button.
|
|
| 2414 |
},
|
| 2415 |
"Diabetic Patient": {
|
| 2416 |
"name": "Maria",
|
|
|
|
| 2417 |
"age": 58,
|
| 2418 |
"conditions": "Type 2 Diabetes, Obesity, Hypertension",
|
| 2419 |
"goal": "Blood sugar control and weight management",
|
|
@@ -2422,6 +2533,7 @@ To revert, use "Reset to Default" button.
|
|
| 2422 |
},
|
| 2423 |
"Post-Surgery Recovery": {
|
| 2424 |
"name": "Alex",
|
|
|
|
| 2425 |
"age": 45,
|
| 2426 |
"conditions": "Post-surgical recovery, Pain management",
|
| 2427 |
"goal": "Safe return to normal activities",
|
|
@@ -2430,6 +2542,7 @@ To revert, use "Reset to Default" button.
|
|
| 2430 |
},
|
| 2431 |
"Mental Health Focus": {
|
| 2432 |
"name": "Emma",
|
|
|
|
| 2433 |
"age": 35,
|
| 2434 |
"conditions": "Depression, Anxiety, Sedentary lifestyle",
|
| 2435 |
"goal": "Mood improvement through activity",
|
|
@@ -2438,6 +2551,7 @@ To revert, use "Reset to Default" button.
|
|
| 2438 |
},
|
| 2439 |
"Elderly Patient": {
|
| 2440 |
"name": "Robert",
|
|
|
|
| 2441 |
"age": 78,
|
| 2442 |
"conditions": "Arthritis, Osteoporosis, Hypertension",
|
| 2443 |
"goal": "Maintain independence and mobility",
|
|
@@ -2446,6 +2560,7 @@ To revert, use "Reset to Default" button.
|
|
| 2446 |
},
|
| 2447 |
"Athletic Patient": {
|
| 2448 |
"name": "Sarah",
|
|
|
|
| 2449 |
"age": 32,
|
| 2450 |
"conditions": "Mild hypertension, Overtraining syndrome",
|
| 2451 |
"goal": "Optimize performance and prevent injury",
|
|
@@ -2459,12 +2574,14 @@ To revert, use "Reset to Default" button.
|
|
| 2459 |
status = f"""<div style="padding: 1em; background-color: #ecfdf5; border-left: 4px solid #10b981; border-radius: 4px;">
|
| 2460 |
<h4 style="color: #059669; margin-top: 0;">β
Profile Loaded</h4>
|
| 2461 |
<p><strong>Patient:</strong> {profile['name']}, {profile['age']} years old</p>
|
|
|
|
| 2462 |
<p><strong>Profile:</strong> {profile_name}</p>
|
| 2463 |
<p style="margin-bottom: 0;">Profile data loaded into settings. Review and save if needed.</p>
|
| 2464 |
</div>"""
|
| 2465 |
|
| 2466 |
return (
|
| 2467 |
profile['name'],
|
|
|
|
| 2468 |
profile['age'],
|
| 2469 |
profile['conditions'],
|
| 2470 |
profile['goal'],
|
|
@@ -2473,17 +2590,22 @@ To revert, use "Reset to Default" button.
|
|
| 2473 |
status
|
| 2474 |
)
|
| 2475 |
|
| 2476 |
-
def save_profile(name: str, age: float, conditions: str, goal: str, exercise: str, limitations: str):
|
| 2477 |
-
"""Save current profile settings."""
|
| 2478 |
if not name.strip():
|
| 2479 |
return """<div style="padding: 1em; background-color: #fef2f2; border-left: 4px solid #dc2626; border-radius: 4px;">
|
| 2480 |
<h4 style="color: #dc2626; margin-top: 0;">β Error</h4>
|
| 2481 |
<p style="margin-bottom: 0;">Patient name cannot be empty</p>
|
| 2482 |
</div>"""
|
| 2483 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2484 |
status = f"""<div style="padding: 1em; background-color: #ecfdf5; border-left: 4px solid #10b981; border-radius: 4px;">
|
| 2485 |
<h4 style="color: #059669; margin-top: 0;">πΎ Profile Saved</h4>
|
| 2486 |
<p><strong>Patient:</strong> {name}, {int(age)} years old</p>
|
|
|
|
| 2487 |
<p><strong>Conditions:</strong> {conditions}</p>
|
| 2488 |
<p><strong>Primary Goal:</strong> {goal}</p>
|
| 2489 |
<p style="margin-bottom: 0;">Profile settings have been updated for this session.</p>
|
|
@@ -2493,14 +2615,20 @@ To revert, use "Reset to Default" button.
|
|
| 2493 |
|
| 2494 |
def reset_profile():
|
| 2495 |
"""Reset profile to default."""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2496 |
status = """<div style="padding: 1em; background-color: #eff6ff; border-left: 4px solid #3b82f6; border-radius: 4px;">
|
| 2497 |
<h4 style="color: #2563eb; margin-top: 0;">π Profile Reset</h4>
|
| 2498 |
<p><strong>Patient:</strong> Serhii, 52 years old</p>
|
|
|
|
| 2499 |
<p style="margin-bottom: 0;">Default profile has been restored.</p>
|
| 2500 |
</div>"""
|
| 2501 |
|
| 2502 |
return (
|
| 2503 |
"Serhii",
|
|
|
|
| 2504 |
52,
|
| 2505 |
"Atrial fibrillation, Deep vein thrombosis, Obesity, Hypertension",
|
| 2506 |
"Weight reduction and cardiovascular fitness improvement",
|
|
@@ -2513,18 +2641,18 @@ To revert, use "Reset to Default" button.
|
|
| 2513 |
load_profile_btn.click(
|
| 2514 |
load_profile,
|
| 2515 |
inputs=[profile_selector],
|
| 2516 |
-
outputs=[patient_name, patient_age, conditions, primary_goal, exercise_prefs, exercise_limits, profile_status]
|
| 2517 |
)
|
| 2518 |
|
| 2519 |
save_profile_btn.click(
|
| 2520 |
save_profile,
|
| 2521 |
-
inputs=[patient_name, patient_age, conditions, primary_goal, exercise_prefs, exercise_limits],
|
| 2522 |
outputs=[profile_save_status]
|
| 2523 |
)
|
| 2524 |
|
| 2525 |
reset_profile_btn.click(
|
| 2526 |
reset_profile,
|
| 2527 |
-
outputs=[patient_name, patient_age, conditions, primary_goal, exercise_prefs, exercise_limits, profile_save_status]
|
| 2528 |
)
|
| 2529 |
|
| 2530 |
return demo
|
|
|
|
| 215 |
|
| 216 |
# Shown only when marking Incorrect
|
| 217 |
with gr.Row(visible=False) as conv_incorrect_comment_row:
|
| 218 |
+
with gr.Column(scale=3):
|
| 219 |
+
gr.Markdown("### Select Correct Classification:")
|
| 220 |
+
conv_correct_classification = gr.Radio(
|
| 221 |
+
choices=[
|
| 222 |
+
"π’ Should be GREEN - No distress",
|
| 223 |
+
"π‘ Should be YELLOW - Needs clarification",
|
| 224 |
+
"π΄ Should be RED - Spiritual distress"
|
| 225 |
+
],
|
| 226 |
+
label="Correct Classification",
|
| 227 |
+
interactive=True
|
| 228 |
+
)
|
| 229 |
+
conv_incorrect_comment = gr.Textbox(
|
| 230 |
+
label="Comment (why incorrect / what to fix)",
|
| 231 |
+
placeholder="Add a short note for this exchange...",
|
| 232 |
+
lines=3,
|
| 233 |
+
)
|
| 234 |
+
with gr.Column(scale=1):
|
| 235 |
+
conv_save_comment_btn = gr.Button("πΎ Save comment", variant="secondary")
|
| 236 |
|
| 237 |
with gr.Row():
|
| 238 |
with gr.Column(scale=1):
|
|
|
|
| 441 |
value="Serhii",
|
| 442 |
interactive=True
|
| 443 |
)
|
| 444 |
+
patient_phone = gr.Textbox(
|
| 445 |
+
label="Phone Number",
|
| 446 |
+
value="",
|
| 447 |
+
placeholder="(555) 123-4567",
|
| 448 |
+
interactive=True
|
| 449 |
+
)
|
| 450 |
patient_age = gr.Number(
|
| 451 |
label="Age",
|
| 452 |
value=52,
|
|
|
|
| 1951 |
original_indicators=r.get("original_indicators", []),
|
| 1952 |
original_reasoning=r.get("original_reasoning", ""),
|
| 1953 |
timestamp=r.get("timestamp"),
|
| 1954 |
+
is_correct=r.get("is_correct"),
|
| 1955 |
+
correct_classification=r.get("correct_classification"),
|
| 1956 |
+
correction_reason=r.get("correction_reason"),
|
| 1957 |
+
verifier_notes=r.get("verifier_notes"),
|
| 1958 |
)
|
| 1959 |
else:
|
| 1960 |
rec = r
|
|
|
|
| 1974 |
correct = 0
|
| 1975 |
incorrect = 0
|
| 1976 |
incorrect_with_comment = 0
|
| 1977 |
+
corrections = {} # Track classification corrections
|
| 1978 |
+
|
| 1979 |
for x in records:
|
| 1980 |
v = (x.get("is_correct") if isinstance(x, dict) else getattr(x, "is_correct", None))
|
| 1981 |
if v is None:
|
|
|
|
| 1988 |
note = (x.get("verifier_notes") if isinstance(x, dict) else getattr(x, "verifier_notes", None))
|
| 1989 |
if note and str(note).strip():
|
| 1990 |
incorrect_with_comment += 1
|
| 1991 |
+
|
| 1992 |
+
# Track classification corrections
|
| 1993 |
+
original_class = (x.get("original_classification") if isinstance(x, dict) else getattr(x, "original_classification", ""))
|
| 1994 |
+
correct_class = (x.get("correct_classification") if isinstance(x, dict) else getattr(x, "correct_classification", None))
|
| 1995 |
+
if original_class and correct_class:
|
| 1996 |
+
correction_key = f"{original_class}β{correct_class}"
|
| 1997 |
+
corrections[correction_key] = corrections.get(correction_key, 0) + 1
|
| 1998 |
+
|
| 1999 |
+
stats_parts = [
|
| 2000 |
+
f"<div><strong>Reviewed:</strong> {reviewed}/{len(records)}</div>",
|
| 2001 |
+
f"<div><strong>β
Correct:</strong> {correct}</div>",
|
| 2002 |
+
f"<div><strong>β Incorrect:</strong> {incorrect}</div>",
|
| 2003 |
+
f"<div><strong>π Incorrect w/ comment:</strong> {incorrect_with_comment}</div>"
|
| 2004 |
+
]
|
| 2005 |
+
|
| 2006 |
+
# Add correction breakdown if any corrections exist
|
| 2007 |
+
if corrections:
|
| 2008 |
+
correction_text = ", ".join([f"{k}: {v}" for k, v in corrections.items()])
|
| 2009 |
+
stats_parts.append(f"<div><strong>π Corrections:</strong> {correction_text}</div>")
|
| 2010 |
|
| 2011 |
stats = (
|
| 2012 |
"<div style='display:flex; gap:12px; flex-wrap:wrap;'>"
|
| 2013 |
+
+ "".join(stats_parts) +
|
|
|
|
|
|
|
|
|
|
| 2014 |
"</div>"
|
| 2015 |
)
|
| 2016 |
return html, pos, stats
|
|
|
|
| 2065 |
fieldnames = [
|
| 2066 |
"session_id",
|
| 2067 |
"patient_name",
|
| 2068 |
+
"patient_phone",
|
| 2069 |
"verifier_name",
|
| 2070 |
"start_time",
|
| 2071 |
"exchange_number",
|
|
|
|
| 2073 |
"original_classification",
|
| 2074 |
"original_confidence",
|
| 2075 |
"is_correct",
|
| 2076 |
+
"correct_classification",
|
| 2077 |
"verifier_notes",
|
| 2078 |
"user_message",
|
| 2079 |
"assistant_response",
|
| 2080 |
+
"provider_summary",
|
| 2081 |
]
|
| 2082 |
|
| 2083 |
with open(export_path, "w", encoding="utf-8", newline="") as f:
|
| 2084 |
w = csv.DictWriter(f, fieldnames=fieldnames)
|
| 2085 |
w.writeheader()
|
| 2086 |
for r in records or []:
|
| 2087 |
+
# Include provider_summary only for RED cases
|
| 2088 |
+
provider_summary = ""
|
| 2089 |
+
if r.get("original_classification", "").upper() == "RED":
|
| 2090 |
+
provider_summary = r.get("provider_summary") or ""
|
| 2091 |
+
|
| 2092 |
row = {
|
| 2093 |
"session_id": (meta or {}).get("session_id"),
|
| 2094 |
"patient_name": (meta or {}).get("patient_name"),
|
| 2095 |
+
"patient_phone": (meta or {}).get("patient_phone") or "",
|
| 2096 |
"verifier_name": (meta or {}).get("verifier_name"),
|
| 2097 |
"start_time": (meta or {}).get("start_time"),
|
| 2098 |
"exchange_number": r.get("exchange_number"),
|
|
|
|
| 2100 |
"original_classification": r.get("original_classification"),
|
| 2101 |
"original_confidence": r.get("original_confidence"),
|
| 2102 |
"is_correct": r.get("is_correct"),
|
| 2103 |
+
"correct_classification": r.get("correct_classification") or "",
|
| 2104 |
"verifier_notes": r.get("verifier_notes") or "",
|
| 2105 |
"user_message": r.get("user_message"),
|
| 2106 |
"assistant_response": r.get("assistant_response"),
|
| 2107 |
+
"provider_summary": provider_summary,
|
| 2108 |
}
|
| 2109 |
w.writerow(row)
|
| 2110 |
return export_path
|
|
|
|
| 2119 |
manager = ConversationVerificationManager()
|
| 2120 |
vs = manager.create_verification_session(session.app_instance.conversation_logger, "Medical Professional")
|
| 2121 |
|
| 2122 |
+
# Get patient phone from app if available
|
| 2123 |
+
patient_phone = ""
|
| 2124 |
+
if hasattr(session.app_instance, 'patient_info'):
|
| 2125 |
+
patient_phone = session.app_instance.patient_info.get("phone") or ""
|
| 2126 |
+
|
| 2127 |
meta = {
|
| 2128 |
"session_id": vs.session_id,
|
| 2129 |
"patient_name": vs.patient_name,
|
| 2130 |
+
"patient_phone": patient_phone,
|
| 2131 |
"verifier_name": vs.verifier_name,
|
| 2132 |
"start_time": vs.start_time.isoformat() if hasattr(vs, "start_time") else None,
|
| 2133 |
}
|
| 2134 |
|
| 2135 |
+
# Get provider summary if available (for RED cases)
|
| 2136 |
+
provider_summary_text = ""
|
| 2137 |
+
if hasattr(session.app_instance, 'get_last_provider_summary'):
|
| 2138 |
+
summary = session.app_instance.get_last_provider_summary()
|
| 2139 |
+
if summary and hasattr(session.app_instance, 'provider_summary_generator'):
|
| 2140 |
+
provider_summary_text = session.app_instance.provider_summary_generator.format_for_export(summary)
|
| 2141 |
+
|
| 2142 |
records_as_dicts = [
|
| 2143 |
{
|
| 2144 |
"exchange_id": r.exchange_id,
|
|
|
|
| 2155 |
"correct_classification": r.correct_classification,
|
| 2156 |
"correction_reason": r.correction_reason,
|
| 2157 |
"verifier_notes": r.verifier_notes,
|
| 2158 |
+
"provider_summary": provider_summary_text if r.original_classification.upper() == "RED" else "",
|
| 2159 |
}
|
| 2160 |
for r in vs.verification_records
|
| 2161 |
]
|
|
|
|
| 2164 |
|
| 2165 |
def _mark_conv_correct(records: list, idx: int):
|
| 2166 |
if not records:
|
| 2167 |
+
return records, idx, "", "", "", gr.update(visible=False), "", ""
|
| 2168 |
idx = max(0, min(idx, len(records) - 1))
|
| 2169 |
if isinstance(records[idx], dict):
|
| 2170 |
records[idx]["is_correct"] = True
|
| 2171 |
+
# clear comment and correct_classification when marked correct (avoid stale data)
|
| 2172 |
records[idx]["verifier_notes"] = ""
|
| 2173 |
+
records[idx]["correct_classification"] = None
|
| 2174 |
html, pos, stats = _render_conv_exchange(records, idx)
|
| 2175 |
row_upd, note_val = _comment_ui_state(records, idx)
|
| 2176 |
+
return records, idx, "β
Marked correct", html, pos, stats, row_upd, note_val, ""
|
| 2177 |
|
| 2178 |
def _mark_conv_incorrect(records: list, idx: int):
|
| 2179 |
if not records:
|
| 2180 |
+
return records, idx, "", "", "", gr.update(visible=False), "", ""
|
| 2181 |
idx = max(0, min(idx, len(records) - 1))
|
| 2182 |
if isinstance(records[idx], dict):
|
| 2183 |
records[idx]["is_correct"] = False
|
| 2184 |
html, pos, stats = _render_conv_exchange(records, idx)
|
| 2185 |
row_upd, note_val = _comment_ui_state(records, idx)
|
| 2186 |
+
# Get existing correct_classification if any
|
| 2187 |
+
existing_classification = ""
|
| 2188 |
+
if isinstance(records[idx], dict):
|
| 2189 |
+
correct_class = records[idx].get("correct_classification")
|
| 2190 |
+
if correct_class:
|
| 2191 |
+
# Map back to display text
|
| 2192 |
+
reverse_map = {
|
| 2193 |
+
"GREEN": "π’ Should be GREEN - No distress",
|
| 2194 |
+
"YELLOW": "π‘ Should be YELLOW - Needs clarification",
|
| 2195 |
+
"RED": "π΄ Should be RED - Spiritual distress"
|
| 2196 |
+
}
|
| 2197 |
+
existing_classification = reverse_map.get(correct_class, "")
|
| 2198 |
+
return records, idx, "β Marked incorrect", html, pos, stats, row_upd, note_val, existing_classification
|
| 2199 |
|
| 2200 |
def _show_incorrect_comment_ui(records: list, idx: int):
|
| 2201 |
"""Mark incorrect and open the comment row, pre-filling any existing note."""
|
| 2202 |
+
records, idx, status, html, pos, stats, _row, note, existing_classification = _mark_conv_incorrect(records, idx)
|
| 2203 |
+
return records, idx, status, html, pos, stats, gr.update(visible=True), note, existing_classification
|
| 2204 |
|
| 2205 |
+
def _save_incorrect_comment(records: list, idx: int, note: str, correct_classification: str):
|
| 2206 |
if not records:
|
| 2207 |
+
return records, idx, "", "", "", "", gr.update(visible=False), "", ""
|
| 2208 |
idx = max(0, min(idx, len(records) - 1))
|
| 2209 |
if isinstance(records[idx], dict):
|
| 2210 |
records[idx]["verifier_notes"] = (note or "").strip()
|
| 2211 |
+
# Map display text to classification code
|
| 2212 |
+
classification_map = {
|
| 2213 |
+
"π’ Should be GREEN - No distress": "GREEN",
|
| 2214 |
+
"π‘ Should be YELLOW - Needs clarification": "YELLOW",
|
| 2215 |
+
"π΄ Should be RED - Spiritual distress": "RED"
|
| 2216 |
+
}
|
| 2217 |
+
if correct_classification and correct_classification in classification_map:
|
| 2218 |
+
records[idx]["correct_classification"] = classification_map[correct_classification]
|
| 2219 |
html, pos, stats = _render_conv_exchange(records, idx)
|
| 2220 |
row_upd, note_val = _comment_ui_state(records, idx)
|
| 2221 |
# keep row visible after save (since still incorrect)
|
| 2222 |
+
return records, idx, "πΎ Comment saved", html, pos, stats, row_upd, note_val, ""
|
| 2223 |
|
| 2224 |
def _download_reviewed_json(meta: dict, records: list):
|
| 2225 |
return _export_conv_records_to_json(meta, records)
|
|
|
|
| 2229 |
|
| 2230 |
def _nav_conv(records: list, idx: int, delta: int):
|
| 2231 |
if not records:
|
| 2232 |
+
return idx, "", "", "", gr.update(visible=False), "", ""
|
| 2233 |
idx = max(0, min(idx + delta, len(records) - 1))
|
| 2234 |
html, pos, stats = _render_conv_exchange(records, idx)
|
| 2235 |
row_upd, note_val = _comment_ui_state(records, idx)
|
| 2236 |
+
# Get existing correct_classification if any
|
| 2237 |
+
existing_classification = ""
|
| 2238 |
+
if isinstance(records[idx], dict):
|
| 2239 |
+
correct_class = records[idx].get("correct_classification")
|
| 2240 |
+
if correct_class:
|
| 2241 |
+
# Map back to display text
|
| 2242 |
+
reverse_map = {
|
| 2243 |
+
"GREEN": "π’ Should be GREEN - No distress",
|
| 2244 |
+
"YELLOW": "π‘ Should be YELLOW - Needs clarification",
|
| 2245 |
+
"RED": "π΄ Should be RED - Spiritual distress"
|
| 2246 |
+
}
|
| 2247 |
+
existing_classification = reverse_map.get(correct_class, "")
|
| 2248 |
+
return idx, html, pos, stats, row_upd, note_val, existing_classification
|
| 2249 |
|
| 2250 |
generate_conv_verification_btn.click(
|
| 2251 |
_generate_conv_verification,
|
|
|
|
| 2277 |
conv_stats,
|
| 2278 |
conv_incorrect_comment_row,
|
| 2279 |
conv_incorrect_comment,
|
| 2280 |
+
conv_correct_classification,
|
| 2281 |
]
|
| 2282 |
)
|
| 2283 |
|
|
|
|
| 2293 |
conv_stats,
|
| 2294 |
conv_incorrect_comment_row,
|
| 2295 |
conv_incorrect_comment,
|
| 2296 |
+
conv_correct_classification,
|
| 2297 |
]
|
| 2298 |
)
|
| 2299 |
|
| 2300 |
conv_save_comment_btn.click(
|
| 2301 |
_save_incorrect_comment,
|
| 2302 |
+
inputs=[conv_verify_records, conv_verify_index, conv_incorrect_comment, conv_correct_classification],
|
| 2303 |
outputs=[
|
| 2304 |
conv_verify_records,
|
| 2305 |
conv_verify_index,
|
|
|
|
| 2309 |
conv_stats,
|
| 2310 |
conv_incorrect_comment_row,
|
| 2311 |
conv_incorrect_comment,
|
| 2312 |
+
conv_correct_classification,
|
| 2313 |
]
|
| 2314 |
)
|
| 2315 |
|
| 2316 |
conv_prev_btn.click(
|
| 2317 |
lambda records, idx: _nav_conv(records, idx, -1),
|
| 2318 |
inputs=[conv_verify_records, conv_verify_index],
|
| 2319 |
+
outputs=[conv_verify_index, conv_verify_exchange, conv_position, conv_stats, conv_incorrect_comment_row, conv_incorrect_comment, conv_correct_classification]
|
| 2320 |
)
|
| 2321 |
|
| 2322 |
conv_next_btn.click(
|
| 2323 |
lambda records, idx: _nav_conv(records, idx, 1),
|
| 2324 |
inputs=[conv_verify_records, conv_verify_index],
|
| 2325 |
+
outputs=[conv_verify_index, conv_verify_exchange, conv_position, conv_stats, conv_incorrect_comment_row, conv_incorrect_comment, conv_correct_classification]
|
| 2326 |
)
|
| 2327 |
|
| 2328 |
# Refresh conversation stats
|
|
|
|
| 2434 |
profiles = {
|
| 2435 |
"Default (Serhii)": {
|
| 2436 |
"name": "Serhii",
|
| 2437 |
+
"phone": "(555) 123-4567",
|
| 2438 |
"age": 52,
|
| 2439 |
"conditions": "Atrial fibrillation, Deep vein thrombosis, Obesity, Hypertension",
|
| 2440 |
"goal": "Weight reduction and cardiovascular fitness improvement",
|
|
|
|
| 2443 |
},
|
| 2444 |
"π’ GREEN - Healthy": {
|
| 2445 |
"name": "James",
|
| 2446 |
+
"phone": "(555) 234-5678",
|
| 2447 |
"age": 40,
|
| 2448 |
"conditions": "No chronic conditions, Excellent health",
|
| 2449 |
"goal": "Maintain fitness and wellness",
|
|
|
|
| 2452 |
},
|
| 2453 |
"π‘ YELLOW - Mild Distress": {
|
| 2454 |
"name": "Lisa",
|
| 2455 |
+
"phone": "(555) 345-6789",
|
| 2456 |
"age": 45,
|
| 2457 |
"conditions": "Hypertension, Mild anxiety, Sleep issues",
|
| 2458 |
"goal": "Manage stress and improve sleep quality",
|
|
|
|
| 2461 |
},
|
| 2462 |
"π‘ YELLOW - Grief & Loss": {
|
| 2463 |
"name": "Michael",
|
| 2464 |
+
"phone": "(555) 456-7890",
|
| 2465 |
"age": 58,
|
| 2466 |
"conditions": "Recent loss of spouse, Mild depression",
|
| 2467 |
"goal": "Process grief and rebuild routine",
|
|
|
|
| 2470 |
},
|
| 2471 |
"π‘ YELLOW - Existential Questions": {
|
| 2472 |
"name": "Patricia",
|
| 2473 |
+
"phone": "(555) 567-8901",
|
| 2474 |
"age": 62,
|
| 2475 |
"conditions": "Chronic pain, Questioning life purpose",
|
| 2476 |
"goal": "Find meaning and manage chronic pain",
|
|
|
|
| 2479 |
},
|
| 2480 |
"π‘ YELLOW - Spiritual Disconnection": {
|
| 2481 |
"name": "David",
|
| 2482 |
+
"phone": "(555) 678-9012",
|
| 2483 |
"age": 55,
|
| 2484 |
"conditions": "Loss of faith, Isolation from community",
|
| 2485 |
"goal": "Reconnect with spiritual community",
|
|
|
|
| 2488 |
},
|
| 2489 |
"π΄ RED - Crisis (Suicidal)": {
|
| 2490 |
"name": "Thomas",
|
| 2491 |
+
"phone": "(555) 789-0123",
|
| 2492 |
"age": 35,
|
| 2493 |
"conditions": "Severe depression, Suicidal ideation",
|
| 2494 |
"goal": "Immediate crisis intervention and support",
|
|
|
|
| 2497 |
},
|
| 2498 |
"π΄ RED - Severe Hopelessness": {
|
| 2499 |
"name": "Jennifer",
|
| 2500 |
+
"phone": "(555) 890-1234",
|
| 2501 |
"age": 48,
|
| 2502 |
"conditions": "Major depression, Complete hopelessness",
|
| 2503 |
"goal": "Crisis stabilization and professional support",
|
|
|
|
| 2506 |
},
|
| 2507 |
"π΄ RED - Spiritual Crisis": {
|
| 2508 |
"name": "Christopher",
|
| 2509 |
+
"phone": "(555) 901-2345",
|
| 2510 |
"age": 52,
|
| 2511 |
"conditions": "Moral injury, Spiritual crisis, Anger at God",
|
| 2512 |
"goal": "Spiritual crisis intervention and healing",
|
|
|
|
| 2515 |
},
|
| 2516 |
"Cardiac Patient": {
|
| 2517 |
"name": "John",
|
| 2518 |
+
"phone": "(555) 012-3456",
|
| 2519 |
"age": 65,
|
| 2520 |
"conditions": "Coronary artery disease, Hypertension, Hyperlipidemia",
|
| 2521 |
"goal": "Cardiac rehabilitation and risk factor management",
|
|
|
|
| 2524 |
},
|
| 2525 |
"Diabetic Patient": {
|
| 2526 |
"name": "Maria",
|
| 2527 |
+
"phone": "(555) 111-2222",
|
| 2528 |
"age": 58,
|
| 2529 |
"conditions": "Type 2 Diabetes, Obesity, Hypertension",
|
| 2530 |
"goal": "Blood sugar control and weight management",
|
|
|
|
| 2533 |
},
|
| 2534 |
"Post-Surgery Recovery": {
|
| 2535 |
"name": "Alex",
|
| 2536 |
+
"phone": "(555) 222-3333",
|
| 2537 |
"age": 45,
|
| 2538 |
"conditions": "Post-surgical recovery, Pain management",
|
| 2539 |
"goal": "Safe return to normal activities",
|
|
|
|
| 2542 |
},
|
| 2543 |
"Mental Health Focus": {
|
| 2544 |
"name": "Emma",
|
| 2545 |
+
"phone": "(555) 333-4444",
|
| 2546 |
"age": 35,
|
| 2547 |
"conditions": "Depression, Anxiety, Sedentary lifestyle",
|
| 2548 |
"goal": "Mood improvement through activity",
|
|
|
|
| 2551 |
},
|
| 2552 |
"Elderly Patient": {
|
| 2553 |
"name": "Robert",
|
| 2554 |
+
"phone": "(555) 444-5555",
|
| 2555 |
"age": 78,
|
| 2556 |
"conditions": "Arthritis, Osteoporosis, Hypertension",
|
| 2557 |
"goal": "Maintain independence and mobility",
|
|
|
|
| 2560 |
},
|
| 2561 |
"Athletic Patient": {
|
| 2562 |
"name": "Sarah",
|
| 2563 |
+
"phone": "(555) 555-6666",
|
| 2564 |
"age": 32,
|
| 2565 |
"conditions": "Mild hypertension, Overtraining syndrome",
|
| 2566 |
"goal": "Optimize performance and prevent injury",
|
|
|
|
| 2574 |
status = f"""<div style="padding: 1em; background-color: #ecfdf5; border-left: 4px solid #10b981; border-radius: 4px;">
|
| 2575 |
<h4 style="color: #059669; margin-top: 0;">β
Profile Loaded</h4>
|
| 2576 |
<p><strong>Patient:</strong> {profile['name']}, {profile['age']} years old</p>
|
| 2577 |
+
<p><strong>Phone:</strong> {profile.get('phone', 'Not provided')}</p>
|
| 2578 |
<p><strong>Profile:</strong> {profile_name}</p>
|
| 2579 |
<p style="margin-bottom: 0;">Profile data loaded into settings. Review and save if needed.</p>
|
| 2580 |
</div>"""
|
| 2581 |
|
| 2582 |
return (
|
| 2583 |
profile['name'],
|
| 2584 |
+
profile.get('phone', ''),
|
| 2585 |
profile['age'],
|
| 2586 |
profile['conditions'],
|
| 2587 |
profile['goal'],
|
|
|
|
| 2590 |
status
|
| 2591 |
)
|
| 2592 |
|
| 2593 |
+
def save_profile(name: str, phone: str, age: float, conditions: str, goal: str, exercise: str, limitations: str):
|
| 2594 |
+
"""Save current profile settings and update app patient info."""
|
| 2595 |
if not name.strip():
|
| 2596 |
return """<div style="padding: 1em; background-color: #fef2f2; border-left: 4px solid #dc2626; border-radius: 4px;">
|
| 2597 |
<h4 style="color: #dc2626; margin-top: 0;">β Error</h4>
|
| 2598 |
<p style="margin-bottom: 0;">Patient name cannot be empty</p>
|
| 2599 |
</div>"""
|
| 2600 |
|
| 2601 |
+
# Update app's patient info for provider summaries
|
| 2602 |
+
if hasattr(app, 'set_patient_info'):
|
| 2603 |
+
app.set_patient_info(name=name.strip(), phone=phone.strip() if phone else None)
|
| 2604 |
+
|
| 2605 |
status = f"""<div style="padding: 1em; background-color: #ecfdf5; border-left: 4px solid #10b981; border-radius: 4px;">
|
| 2606 |
<h4 style="color: #059669; margin-top: 0;">πΎ Profile Saved</h4>
|
| 2607 |
<p><strong>Patient:</strong> {name}, {int(age)} years old</p>
|
| 2608 |
+
<p><strong>Phone:</strong> {phone if phone else 'Not provided'}</p>
|
| 2609 |
<p><strong>Conditions:</strong> {conditions}</p>
|
| 2610 |
<p><strong>Primary Goal:</strong> {goal}</p>
|
| 2611 |
<p style="margin-bottom: 0;">Profile settings have been updated for this session.</p>
|
|
|
|
| 2615 |
|
| 2616 |
def reset_profile():
|
| 2617 |
"""Reset profile to default."""
|
| 2618 |
+
# Reset app's patient info
|
| 2619 |
+
if hasattr(app, 'set_patient_info'):
|
| 2620 |
+
app.set_patient_info(name="Serhii", phone="(555) 123-4567")
|
| 2621 |
+
|
| 2622 |
status = """<div style="padding: 1em; background-color: #eff6ff; border-left: 4px solid #3b82f6; border-radius: 4px;">
|
| 2623 |
<h4 style="color: #2563eb; margin-top: 0;">π Profile Reset</h4>
|
| 2624 |
<p><strong>Patient:</strong> Serhii, 52 years old</p>
|
| 2625 |
+
<p><strong>Phone:</strong> (555) 123-4567</p>
|
| 2626 |
<p style="margin-bottom: 0;">Default profile has been restored.</p>
|
| 2627 |
</div>"""
|
| 2628 |
|
| 2629 |
return (
|
| 2630 |
"Serhii",
|
| 2631 |
+
"(555) 123-4567",
|
| 2632 |
52,
|
| 2633 |
"Atrial fibrillation, Deep vein thrombosis, Obesity, Hypertension",
|
| 2634 |
"Weight reduction and cardiovascular fitness improvement",
|
|
|
|
| 2641 |
load_profile_btn.click(
|
| 2642 |
load_profile,
|
| 2643 |
inputs=[profile_selector],
|
| 2644 |
+
outputs=[patient_name, patient_phone, patient_age, conditions, primary_goal, exercise_prefs, exercise_limits, profile_status]
|
| 2645 |
)
|
| 2646 |
|
| 2647 |
save_profile_btn.click(
|
| 2648 |
save_profile,
|
| 2649 |
+
inputs=[patient_name, patient_phone, patient_age, conditions, primary_goal, exercise_prefs, exercise_limits],
|
| 2650 |
outputs=[profile_save_status]
|
| 2651 |
)
|
| 2652 |
|
| 2653 |
reset_profile_btn.click(
|
| 2654 |
reset_profile,
|
| 2655 |
+
outputs=[patient_name, patient_phone, patient_age, conditions, primary_goal, exercise_prefs, exercise_limits, profile_save_status]
|
| 2656 |
)
|
| 2657 |
|
| 2658 |
return demo
|