DocUA commited on
Commit
32be239
Β·
1 Parent(s): d0d6d94

after comments from Or:

Browse files

feat: 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 ADDED
@@ -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
src/core/provider_summary_generator.py ADDED
@@ -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()
src/core/simplified_medical_app.py CHANGED
@@ -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=["ongoing_triage"],
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=["triage_escalation"],
438
  confidence=0.8,
439
  reasoning=reasoning
440
  )
@@ -479,55 +499,94 @@ class SimplifiedMedicalApp:
479
  language: str,
480
  assessment: SpiritualAssessment
481
  ) -> str:
482
- """Generate compassionate crisis support response."""
 
 
 
 
 
 
 
 
 
 
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
- I'm connecting you with our spiritual care team so someone can reach out to you personally. You're not alone in this."""
499
 
500
  def _generate_referral(self, assessment: SpiritualAssessment) -> str:
501
- """Generate referral message for chaplain team."""
 
 
 
 
502
  # Get triage context if available
503
- triage_context = ""
 
504
  if self.spiritual_state.triage_session:
505
- triage_context = "\n\nTriage exchanges:\n"
506
- for q, r in zip(
507
- self.spiritual_state.triage_session.questions_asked,
508
- self.spiritual_state.triage_session.patient_responses
509
- ):
510
- triage_context += f"Q: {q}\nA: {r}\n"
511
-
512
- referral = f"""
513
- === SPIRITUAL CARE REFERRAL ===
514
- Generated: {datetime.now().isoformat()}
515
- Patient: {self.clinical_background.patient_name}
516
-
517
- DISTRESS INDICATORS:
518
- {chr(10).join(f'β€’ {ind}' for ind in assessment.indicators)}
519
-
520
- CLASSIFICATION: {assessment.state.value.upper()}
521
- CONFIDENCE: {assessment.confidence:.0%}
522
- REASONING: {assessment.reasoning}
523
- {triage_context}
524
- RECENT CONVERSATION:
525
- {self._get_conversation_context_str()}
526
-
527
- RECOMMENDED ACTION: Immediate spiritual care outreach
528
- ================================
529
- """
530
- return referral
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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."""
src/core/soft_triage_manager.py CHANGED
@@ -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 is coping well, has support systems, or the concern was minor</description>
79
  <indicators>
80
- - Mentions coping strategies or support from others
 
81
  - Describes temporary stress that is manageable
82
  - Reports feeling better or having resources
83
  - Shows resilience or positive outlook
 
84
  </indicators>
85
- <example>"I'm just having a bad day, but I have my family to talk to"</example>
86
- <example>"It's been tough, but I'm managing with my therapist's help"</example>
 
 
 
 
 
87
  </outcome>
88
 
89
  <outcome name="ESCALATE_RED" action="generate_referral">
90
- <description>Patient shows significant distress that needs professional support</description>
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
- <example>"I feel completely alone and nothing helps anymore"</example>
99
- <example>"Every day is worse, I can't see a way forward"</example>
 
 
 
 
 
 
 
100
  </outcome>
101
 
102
  <outcome name="CONTINUE" action="ask_another_question">
103
- <description>Need more information to make a determination</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
- <example>"I don't know, it's complicated"</example>
111
- <example>"Maybe, I'm not sure"</example>
 
 
 
112
  </outcome>
113
  </outcome_categories>
114
 
 
 
 
 
 
 
 
 
 
 
115
  <evaluation_process>
116
  <step>Review the patient's response carefully</step>
117
- <step>Identify specific indicators present in their words</step>
118
- <step>Match indicators to outcome categories</step>
119
- <step>Consider the overall tone and context</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
- "reasoning": "Brief explanation of why you chose this outcome",
 
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
 
src/core/spiritual_monitor.py CHANGED
@@ -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="potential_distress">
77
- The message contains indicators of mild to moderate distress, including:
 
 
 
 
 
 
 
 
 
 
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 and it breaks my heart"
129
- "Why is God letting this happen to me?"
130
- "I feel so alone, nobody understands"
131
- "I'm worried about being a burden to my family"
132
- "I don't know who I am anymore without my work"
133
- "I'm exhausted all the time, emotionally drained"
134
- "I can't do the things I used to enjoy"
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:
src/interface/simplified_gradio_app.py CHANGED
@@ -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
- conv_incorrect_comment = gr.Textbox(
219
- label="Comment (why incorrect / what to fix)",
220
- placeholder="Add a short note for this exchange...",
221
- lines=3,
222
- scale=4,
223
- )
224
- conv_save_comment_btn = gr.Button("πŸ’Ύ Save comment", variant="secondary", scale=1)
 
 
 
 
 
 
 
 
 
 
 
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
- f"<div><strong>Reviewed:</strong> {reviewed}/{len(records)}</div>"
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 notes)
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
- return records, idx, "❌ Marked incorrect", html, pos, stats, row_upd, note_val
 
 
 
 
 
 
 
 
 
 
 
 
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
- return idx, html, pos, stats, row_upd, note_val
 
 
 
 
 
 
 
 
 
 
 
 
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