Spaces:
Sleeping
feat: Fix classification logic and remove redundant spiritual care message functionality
Browse files## Major Changes
### 🔧 Classification Logic Fixes (review/Or_2.txt recommendations)
- Fix JSON parsing bug: LLM returns 'classification' but parser expected 'state'
- Update RED flag definition: broader than crisis-only, includes active spiritual distress
- Add explicit RED indicators: complex grief, loss of loved one, doubt about meaning of life/suffering/dignity
- Fix 'I am fine' classification: now correctly GREEN (was incorrectly YELLOW)
- Fix 'feeling sad' classification: now correctly YELLOW without spiritual context (was incorrectly RED)
- Loss of meaning/purpose now correctly RED with high confidence
### 🗑️ Remove Redundant Spiritual Care Message UI
- Remove redundant '💬 Care Team Message' tab from Provider Summary panel
- Remove related UI controls and event handlers
- Fix undefined 'spiritual_care_msg' variable in return statements
- Preserve prompt editing and model settings (now used for Medical Brain Summary)
- Medical Brain Summary now first tab by default in '�� Check Status & Summary'
### 🧪 Enhanced Testing & Quality
- Add comprehensive unit tests for improved classification logic (9/9 passing)
- Add integration tests for UI classification improvements
- Add property-based testing framework for classification consistency
- Add error handling and recovery mechanisms
### 📊 New Components Added
- ImprovedClassificationPromptManager: Enhanced classification with explicit indicators
- Enhanced display integration for better UI formatting
- Provider summary formatter with Medical Brain compatibility
- Visual separation manager for improved UI organization
- UI error handler with fallback mechanisms
## Files Modified
- Core: spiritual_monitor.py, provider_summary_generator.py, simplified_medical_app.py
- Interface: chat_handlers.py, stats_handlers.py, simplified_gradio_app.py
- Prompts: spiritual_monitor.txt, spiritual_care_message.txt
- Tests: 6 new unit test files, 3 new integration test files
## Test Results
- Unit tests: 170/175 passing (5 failures unrelated to changes)
- Integration tests: 59/59 passing
- Classification tests: 9/9 passing
- All core functionality verified working
## Classification Test Results
✅ 'I am fine' → GREEN (confidence 1.0) - Fixed from incorrect YELLOW
✅ 'I feel sad' → YELLOW (confidence 0.9) - Fixed from incorrect RED
✅ 'Life has no meaning' → RED (confidence 1.0) - Correctly identifies explicit indicator
- .gitignore +2 -0
- examples/coherent_summary_demo.py +272 -0
- examples/enhanced_config_demo.py +269 -0
- examples/enhanced_display_demo.py +279 -0
- examples/enhanced_verification_demo.py +266 -0
- examples/error_handling_demo.py +279 -0
- examples/integration_demo.py +206 -0
- examples/visual_separation_demo.py +248 -0
- src/config/display/enhanced_display_config.json +60 -0
- src/config/enhanced_display_config.py +682 -0
- src/config/prompts/spiritual_care_message.txt +25 -1
- src/config/prompts/spiritual_monitor.txt +35 -21
- src/core/conversation_verification.py +199 -28
- src/core/improved_classification_prompt_manager.py +409 -0
- src/core/provider_summary_generator.py +429 -62
- src/core/simplified_medical_app.py +115 -9
- src/core/soft_triage_manager.py +17 -12
- src/core/spiritual_monitor.py +92 -134
- src/core/ui_error_handler.py +687 -0
- src/core/verification_exporter.py +212 -23
- src/core/verification_store.py +58 -7
- src/interface/chat_handlers.py +36 -23
- src/interface/enhanced_display_integration.py +321 -0
- src/interface/enhanced_results_display_manager.py +533 -0
- src/interface/help_content.py +20 -27
- src/interface/provider_summary_formatter.py +290 -0
- src/interface/simplified_gradio_app.py +34 -54
- src/interface/stats_handlers.py +21 -45
- src/interface/visual_separation_manager.py +388 -0
- tests/integration/test_enhanced_verification_integration.py +274 -0
- tests/integration/test_error_handling_integration.py +204 -0
- tests/integration/test_ui_classification_improvements_integration.py +558 -0
- tests/unit/test_coherent_summary_formatter.py +247 -0
- tests/unit/test_enhanced_display_config.py +325 -0
- tests/unit/test_enhanced_results_display.py +472 -0
- tests/unit/test_improved_classification_prompt_manager.py +296 -0
- tests/unit/test_spiritual_monitor_improved_logic.py +235 -0
- tests/unit/test_ui_error_handler.py +404 -0
|
@@ -120,3 +120,5 @@ start.sh
|
|
| 120 |
.zshenv
|
| 121 |
src/core/verification_store.py
|
| 122 |
manual_tests/test_vertex_auth.py
|
|
|
|
|
|
|
|
|
| 120 |
.zshenv
|
| 121 |
src/core/verification_store.py
|
| 122 |
manual_tests/test_vertex_auth.py
|
| 123 |
+
|
| 124 |
+
review/
|
|
@@ -0,0 +1,272 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# coherent_summary_demo.py
|
| 3 |
+
"""
|
| 4 |
+
Demonstration of Coherent Summary Formatter functionality.
|
| 5 |
+
|
| 6 |
+
This script shows the new coherent paragraph formatting functionality
|
| 7 |
+
added to ProviderSummaryGenerator for requirements 2.1-2.8.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import sys
|
| 11 |
+
import os
|
| 12 |
+
|
| 13 |
+
# Add project root to Python path
|
| 14 |
+
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 15 |
+
if project_root not in sys.path:
|
| 16 |
+
sys.path.insert(0, project_root)
|
| 17 |
+
|
| 18 |
+
from src.core.provider_summary_generator import ProviderSummaryGenerator, ProviderSummary
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def demo_coherent_paragraph_formatting():
|
| 22 |
+
"""Demonstrate coherent paragraph formatting functionality."""
|
| 23 |
+
print("=" * 80)
|
| 24 |
+
print("MEDICAL BRAIN SUMMARY FORMATTING DEMO")
|
| 25 |
+
print("=" * 80)
|
| 26 |
+
print("This demo shows the new coherent paragraph formatting (Requirements 2.1-2.8)")
|
| 27 |
+
print("compared to the existing structured format.\n")
|
| 28 |
+
|
| 29 |
+
generator = ProviderSummaryGenerator()
|
| 30 |
+
|
| 31 |
+
# Example 1: Complete patient information
|
| 32 |
+
print("EXAMPLE 1: Complete Patient Information")
|
| 33 |
+
print("-" * 50)
|
| 34 |
+
|
| 35 |
+
summary1 = ProviderSummary(
|
| 36 |
+
patient_name="Sarah Johnson",
|
| 37 |
+
patient_phone="(555) 234-5678",
|
| 38 |
+
indicators=["Fear of death", "Loss of faith", "Family burden"],
|
| 39 |
+
situation_description="existential crisis related to cancer diagnosis",
|
| 40 |
+
medical_context={
|
| 41 |
+
"age": 34,
|
| 42 |
+
"gender": "female",
|
| 43 |
+
"conditions": ["Breast cancer", "Chemotherapy treatment", "Anxiety"]
|
| 44 |
+
},
|
| 45 |
+
conversation_context="I don't understand why God would let this happen to me and my children"
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
print("COHERENT PARAGRAPH FORMAT:")
|
| 49 |
+
coherent1 = generator.format_coherent_paragraph(summary1)
|
| 50 |
+
print(coherent1)
|
| 51 |
+
|
| 52 |
+
print("\n" + "=" * 50)
|
| 53 |
+
print("STRUCTURED FORMAT (for comparison):")
|
| 54 |
+
structured1 = generator.format_for_display(summary1)
|
| 55 |
+
print(structured1[:300] + "..." if len(structured1) > 300 else structured1)
|
| 56 |
+
|
| 57 |
+
print("\n" + "=" * 80 + "\n")
|
| 58 |
+
|
| 59 |
+
# Example 2: Minimal patient information
|
| 60 |
+
print("EXAMPLE 2: Minimal Patient Information")
|
| 61 |
+
print("-" * 50)
|
| 62 |
+
|
| 63 |
+
summary2 = ProviderSummary(
|
| 64 |
+
patient_name="John Doe",
|
| 65 |
+
patient_phone="",
|
| 66 |
+
indicators=["Spiritual questioning"],
|
| 67 |
+
situation_description="seeking meaning in illness",
|
| 68 |
+
medical_context={"age": 45, "gender": "male"}
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
print("COHERENT PARAGRAPH FORMAT:")
|
| 72 |
+
coherent2 = generator.format_coherent_paragraph(summary2)
|
| 73 |
+
print(coherent2)
|
| 74 |
+
|
| 75 |
+
print("\n" + "=" * 80 + "\n")
|
| 76 |
+
|
| 77 |
+
# Example 3: Complex medical history
|
| 78 |
+
print("EXAMPLE 3: Complex Medical History")
|
| 79 |
+
print("-" * 50)
|
| 80 |
+
|
| 81 |
+
summary3 = ProviderSummary(
|
| 82 |
+
patient_name="Michael Chen",
|
| 83 |
+
patient_phone="(555) 345-6789",
|
| 84 |
+
indicators=["Hopelessness", "Isolation", "Loss of purpose", "Spiritual abandonment"],
|
| 85 |
+
situation_description="severe spiritual crisis with multiple stressors",
|
| 86 |
+
medical_context={
|
| 87 |
+
"age": 67,
|
| 88 |
+
"gender": "male",
|
| 89 |
+
"conditions": ["Heart Disease", "Diabetes", "COPD", "Depression", "Chronic Pain"]
|
| 90 |
+
},
|
| 91 |
+
conversation_context="I've been fighting this for years and I'm tired. What's the point anymore?"
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
print("COHERENT PARAGRAPH FORMAT:")
|
| 95 |
+
coherent3 = generator.format_coherent_paragraph(summary3)
|
| 96 |
+
print(coherent3)
|
| 97 |
+
|
| 98 |
+
print("\n" + "=" * 80 + "\n")
|
| 99 |
+
|
| 100 |
+
# Example 4: Unknown demographics
|
| 101 |
+
print("EXAMPLE 4: Unknown Demographics")
|
| 102 |
+
print("-" * 50)
|
| 103 |
+
|
| 104 |
+
summary4 = ProviderSummary(
|
| 105 |
+
patient_name="Patient Anonymous",
|
| 106 |
+
patient_phone="(555) 000-0000",
|
| 107 |
+
indicators=["General distress"],
|
| 108 |
+
situation_description="unspecified spiritual concern",
|
| 109 |
+
medical_context={} # No demographic info
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
print("COHERENT PARAGRAPH FORMAT:")
|
| 113 |
+
coherent4 = generator.format_coherent_paragraph(summary4)
|
| 114 |
+
print(coherent4)
|
| 115 |
+
|
| 116 |
+
print("\n" + "=" * 80 + "\n")
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def demo_requirements_compliance():
|
| 120 |
+
"""Demonstrate compliance with specific requirements 2.1-2.8."""
|
| 121 |
+
print("REQUIREMENTS COMPLIANCE DEMONSTRATION")
|
| 122 |
+
print("=" * 80)
|
| 123 |
+
|
| 124 |
+
generator = ProviderSummaryGenerator()
|
| 125 |
+
|
| 126 |
+
# Create comprehensive example
|
| 127 |
+
summary = ProviderSummary(
|
| 128 |
+
patient_name="Emma Wilson",
|
| 129 |
+
patient_phone="(555) 987-6543",
|
| 130 |
+
indicators=["Loss of meaning", "Spiritual crisis", "Fear of dying"],
|
| 131 |
+
situation_description="existential distress following terminal diagnosis",
|
| 132 |
+
medical_context={
|
| 133 |
+
"age": 52,
|
| 134 |
+
"gender": "female",
|
| 135 |
+
"conditions": ["Pancreatic cancer", "Metastatic disease"]
|
| 136 |
+
},
|
| 137 |
+
conversation_context="I used to believe everything happens for a reason, but now I just feel abandoned by God"
|
| 138 |
+
)
|
| 139 |
+
|
| 140 |
+
result = generator.format_coherent_paragraph(summary)
|
| 141 |
+
|
| 142 |
+
print("GENERATED COHERENT PARAGRAPH:")
|
| 143 |
+
print("-" * 40)
|
| 144 |
+
print(result)
|
| 145 |
+
print("\n")
|
| 146 |
+
|
| 147 |
+
print("REQUIREMENTS VERIFICATION:")
|
| 148 |
+
print("-" * 40)
|
| 149 |
+
|
| 150 |
+
# Split into main paragraph and quote
|
| 151 |
+
parts = result.split('\n\n')
|
| 152 |
+
main_paragraph = parts[0]
|
| 153 |
+
patient_quote = parts[1] if len(parts) > 1 else None
|
| 154 |
+
|
| 155 |
+
# Requirement 2.1: Single coherent paragraph
|
| 156 |
+
print("✅ Requirement 2.1 (Single coherent paragraph):")
|
| 157 |
+
print(f" Main paragraph is single line: {len(main_paragraph.split('. ')) >= 4}")
|
| 158 |
+
|
| 159 |
+
# Requirement 2.2: Demographic information
|
| 160 |
+
print("✅ Requirement 2.2 (Demographic information):")
|
| 161 |
+
demo_info = "Emma Wilson is a 52-year-old female"
|
| 162 |
+
print(f" Contains demographic info: {demo_info in main_paragraph}")
|
| 163 |
+
|
| 164 |
+
# Requirement 2.3: Medical history
|
| 165 |
+
print("✅ Requirement 2.3 (Medical history):")
|
| 166 |
+
medical_info = "clinical history of Pancreatic cancer and Metastatic disease"
|
| 167 |
+
print(f" Contains medical history: {medical_info in main_paragraph}")
|
| 168 |
+
|
| 169 |
+
# Requirement 2.4: Spiritual concerns
|
| 170 |
+
print("✅ Requirement 2.4 (Spiritual concerns):")
|
| 171 |
+
concerns_info = "Loss of meaning, Spiritual crisis, and Fear of dying"
|
| 172 |
+
print(f" Contains spiritual concerns: {concerns_info in main_paragraph}")
|
| 173 |
+
|
| 174 |
+
# Requirement 2.5: Classification
|
| 175 |
+
print("✅ Requirement 2.5 (Classification):")
|
| 176 |
+
classification_info = "RED FLAG"
|
| 177 |
+
print(f" Contains classification: {classification_info in main_paragraph}")
|
| 178 |
+
|
| 179 |
+
# Requirement 2.6: Consent information
|
| 180 |
+
print("✅ Requirement 2.6 (Consent information):")
|
| 181 |
+
consent_info = "spiritual care team contact"
|
| 182 |
+
print(f" Contains consent info: {consent_info in main_paragraph}")
|
| 183 |
+
|
| 184 |
+
# Requirement 2.7: Contact information
|
| 185 |
+
print("✅ Requirement 2.7 (Contact information):")
|
| 186 |
+
contact_info = "(555) 987-6543"
|
| 187 |
+
print(f" Contains contact info: {contact_info in main_paragraph}")
|
| 188 |
+
|
| 189 |
+
# Requirement 2.8: Patient quote
|
| 190 |
+
print("✅ Requirement 2.8 (Patient quote as separate line):")
|
| 191 |
+
print(f" Has separate patient quote: {patient_quote is not None}")
|
| 192 |
+
if patient_quote:
|
| 193 |
+
print(f" Quote content: {patient_quote}")
|
| 194 |
+
|
| 195 |
+
print("\n" + "=" * 80)
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
def demo_format_comparison():
|
| 199 |
+
"""Compare different formatting options."""
|
| 200 |
+
print("FORMAT COMPARISON DEMONSTRATION")
|
| 201 |
+
print("=" * 80)
|
| 202 |
+
|
| 203 |
+
generator = ProviderSummaryGenerator()
|
| 204 |
+
|
| 205 |
+
summary = ProviderSummary(
|
| 206 |
+
patient_name="David Rodriguez",
|
| 207 |
+
patient_phone="(555) 456-7890",
|
| 208 |
+
indicators=["Spiritual questioning", "Anxiety about afterlife"],
|
| 209 |
+
situation_description="religious doubt following family tragedy",
|
| 210 |
+
medical_context={
|
| 211 |
+
"age": 38,
|
| 212 |
+
"gender": "male",
|
| 213 |
+
"conditions": ["PTSD", "Depression"]
|
| 214 |
+
}
|
| 215 |
+
)
|
| 216 |
+
|
| 217 |
+
print("1. STRUCTURED FORMAT (Original):")
|
| 218 |
+
print("-" * 40)
|
| 219 |
+
structured = generator.format_for_display(summary)
|
| 220 |
+
print(structured[:400] + "..." if len(structured) > 400 else structured)
|
| 221 |
+
|
| 222 |
+
print("\n2. COHERENT PARAGRAPH FORMAT (New):")
|
| 223 |
+
print("-" * 40)
|
| 224 |
+
coherent = generator.format_coherent_paragraph(summary)
|
| 225 |
+
print(coherent)
|
| 226 |
+
|
| 227 |
+
print("\n3. EXPORT FORMAT (Existing):")
|
| 228 |
+
print("-" * 40)
|
| 229 |
+
export = generator.format_for_export(summary)
|
| 230 |
+
print(export)
|
| 231 |
+
|
| 232 |
+
print("\nFORMAT CHARACTERISTICS:")
|
| 233 |
+
print("-" * 40)
|
| 234 |
+
print(f"Structured format length: {len(structured)} characters")
|
| 235 |
+
print(f"Coherent format length: {len(coherent)} characters")
|
| 236 |
+
print(f"Export format length: {len(export)} characters")
|
| 237 |
+
print(f"Structured format lines: {len(structured.split('\\n'))}")
|
| 238 |
+
print(f"Coherent format lines: {len(coherent.split('\\n'))}")
|
| 239 |
+
|
| 240 |
+
print("\n" + "=" * 80)
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
def main():
|
| 244 |
+
"""Run all demonstration functions."""
|
| 245 |
+
print("COHERENT SUMMARY FORMATTER DEMONSTRATION")
|
| 246 |
+
print("=" * 80)
|
| 247 |
+
print("This demo shows the new coherent paragraph formatting functionality")
|
| 248 |
+
print("added to ProviderSummaryGenerator for requirements 2.1-2.8.")
|
| 249 |
+
print("The new format provides Medical Brain compatible single-paragraph summaries.")
|
| 250 |
+
|
| 251 |
+
try:
|
| 252 |
+
demo_coherent_paragraph_formatting()
|
| 253 |
+
demo_requirements_compliance()
|
| 254 |
+
demo_format_comparison()
|
| 255 |
+
|
| 256 |
+
print("\nDEMONSTRATION COMPLETE")
|
| 257 |
+
print("=" * 80)
|
| 258 |
+
print("✅ All coherent summary formatting functionality is working correctly!")
|
| 259 |
+
print("✅ Requirements 2.1-2.8 are fully implemented!")
|
| 260 |
+
print("✅ Integration with existing functionality is maintained!")
|
| 261 |
+
|
| 262 |
+
except Exception as e:
|
| 263 |
+
print(f"\nError during demonstration: {e}")
|
| 264 |
+
import traceback
|
| 265 |
+
traceback.print_exc()
|
| 266 |
+
return 1
|
| 267 |
+
|
| 268 |
+
return 0
|
| 269 |
+
|
| 270 |
+
|
| 271 |
+
if __name__ == "__main__":
|
| 272 |
+
sys.exit(main())
|
|
@@ -0,0 +1,269 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Enhanced Configuration System Demo.
|
| 4 |
+
|
| 5 |
+
Demonstrates the configuration system for UI classification improvements
|
| 6 |
+
including feature toggles, styling options, and configuration management.
|
| 7 |
+
|
| 8 |
+
Requirements: 7.1, 7.2, 7.3
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import sys
|
| 12 |
+
import os
|
| 13 |
+
|
| 14 |
+
# Add src to path for imports
|
| 15 |
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
| 16 |
+
parent_dir = os.path.dirname(current_dir)
|
| 17 |
+
src_dir = os.path.join(parent_dir, 'src')
|
| 18 |
+
if src_dir not in sys.path:
|
| 19 |
+
sys.path.insert(0, src_dir)
|
| 20 |
+
|
| 21 |
+
from config.enhanced_display_config import (
|
| 22 |
+
EnhancedDisplayConfig,
|
| 23 |
+
EnhancedDisplayConfigManager,
|
| 24 |
+
get_enhanced_display_config,
|
| 25 |
+
create_high_contrast_config,
|
| 26 |
+
create_minimal_config,
|
| 27 |
+
create_mobile_optimized_config
|
| 28 |
+
)
|
| 29 |
+
from interface.enhanced_results_display_manager import EnhancedResultsDisplayManager
|
| 30 |
+
from core.provider_summary_generator import ProviderSummary
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def demo_basic_configuration():
|
| 34 |
+
"""Demonstrate basic configuration usage."""
|
| 35 |
+
print("🔧 Enhanced Display Configuration Demo")
|
| 36 |
+
print("=" * 50)
|
| 37 |
+
|
| 38 |
+
# Create default configuration
|
| 39 |
+
config = EnhancedDisplayConfig()
|
| 40 |
+
print(f"✅ Default configuration created")
|
| 41 |
+
print(f" - Enhanced mode enabled: {config.enabled}")
|
| 42 |
+
print(f" - Color coding enabled: {config.use_color_coding}")
|
| 43 |
+
print(f" - Icons enabled: {config.use_icons}")
|
| 44 |
+
print(f" - Visual separators enabled: {config.use_visual_separators}")
|
| 45 |
+
print()
|
| 46 |
+
|
| 47 |
+
# Show classification colors
|
| 48 |
+
print("🎨 Classification Colors:")
|
| 49 |
+
print(f" - Red: {config.classification_colors.red}")
|
| 50 |
+
print(f" - Yellow: {config.classification_colors.yellow}")
|
| 51 |
+
print(f" - Green: {config.classification_colors.green}")
|
| 52 |
+
print()
|
| 53 |
+
|
| 54 |
+
# Show section icons
|
| 55 |
+
print("🎭 Section Icons:")
|
| 56 |
+
print(f" - AI Analysis: {config.ai_analysis.icon}")
|
| 57 |
+
print(f" - Patient Message: {config.patient_message.icon}")
|
| 58 |
+
print(f" - Provider Summary: {config.provider_summary.icon}")
|
| 59 |
+
print()
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def demo_configuration_manager():
|
| 63 |
+
"""Demonstrate configuration manager functionality."""
|
| 64 |
+
print("📁 Configuration Manager Demo")
|
| 65 |
+
print("=" * 50)
|
| 66 |
+
|
| 67 |
+
# Create configuration manager
|
| 68 |
+
config_manager = EnhancedDisplayConfigManager()
|
| 69 |
+
print(f"✅ Configuration manager created")
|
| 70 |
+
|
| 71 |
+
# Load configuration
|
| 72 |
+
config = config_manager.load_config()
|
| 73 |
+
print(f"✅ Configuration loaded")
|
| 74 |
+
print(f" - Config file: {config_manager.config_file}")
|
| 75 |
+
print()
|
| 76 |
+
|
| 77 |
+
# Update configuration
|
| 78 |
+
print("🔄 Updating configuration...")
|
| 79 |
+
success = config_manager.update_config(
|
| 80 |
+
use_icons=False,
|
| 81 |
+
use_color_coding=True
|
| 82 |
+
)
|
| 83 |
+
print(f" - Update successful: {success}")
|
| 84 |
+
|
| 85 |
+
# Reload and verify
|
| 86 |
+
updated_config = config_manager.load_config()
|
| 87 |
+
print(f" - Icons enabled: {updated_config.use_icons}")
|
| 88 |
+
print(f" - Color coding enabled: {updated_config.use_color_coding}")
|
| 89 |
+
print()
|
| 90 |
+
|
| 91 |
+
# Enable/disable features
|
| 92 |
+
print("🎛️ Feature toggles...")
|
| 93 |
+
config_manager.enable_feature('icons')
|
| 94 |
+
config_manager.disable_feature('animations')
|
| 95 |
+
|
| 96 |
+
final_config = config_manager.load_config()
|
| 97 |
+
print(f" - Icons enabled: {final_config.use_icons}")
|
| 98 |
+
print(f" - Animations enabled: {final_config.enable_animations}")
|
| 99 |
+
print()
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def demo_preset_configurations():
|
| 103 |
+
"""Demonstrate preset configuration options."""
|
| 104 |
+
print("🎨 Preset Configurations Demo")
|
| 105 |
+
print("=" * 50)
|
| 106 |
+
|
| 107 |
+
# High contrast configuration
|
| 108 |
+
high_contrast = create_high_contrast_config()
|
| 109 |
+
print("♿ High Contrast Configuration:")
|
| 110 |
+
print(f" - High contrast mode: {high_contrast.high_contrast_mode}")
|
| 111 |
+
print(f" - Red color: {high_contrast.classification_colors.red}")
|
| 112 |
+
print(f" - AI border color: {high_contrast.ai_analysis.border_color}")
|
| 113 |
+
print()
|
| 114 |
+
|
| 115 |
+
# Minimal configuration
|
| 116 |
+
minimal = create_minimal_config()
|
| 117 |
+
print("🔲 Minimal Configuration:")
|
| 118 |
+
print(f" - Icons enabled: {minimal.use_icons}")
|
| 119 |
+
print(f" - Visual separators: {minimal.use_visual_separators}")
|
| 120 |
+
print(f" - Animations enabled: {minimal.enable_animations}")
|
| 121 |
+
print()
|
| 122 |
+
|
| 123 |
+
# Mobile optimized configuration
|
| 124 |
+
mobile = create_mobile_optimized_config()
|
| 125 |
+
print("📱 Mobile Optimized Configuration:")
|
| 126 |
+
print(f" - Responsive breakpoint: {mobile.responsive_breakpoint}")
|
| 127 |
+
print(f" - AI padding: {mobile.ai_analysis.padding}")
|
| 128 |
+
print(f" - Animations enabled: {mobile.enable_animations}")
|
| 129 |
+
print()
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def demo_css_generation():
|
| 133 |
+
"""Demonstrate CSS generation capabilities."""
|
| 134 |
+
print("🎨 CSS Generation Demo")
|
| 135 |
+
print("=" * 50)
|
| 136 |
+
|
| 137 |
+
config = EnhancedDisplayConfig()
|
| 138 |
+
|
| 139 |
+
# Generate CSS variables
|
| 140 |
+
css_vars = config.generate_css_variables()
|
| 141 |
+
print("📝 CSS Variables (first 10 lines):")
|
| 142 |
+
lines = css_vars.split('\n')[:10]
|
| 143 |
+
for line in lines:
|
| 144 |
+
print(f" {line}")
|
| 145 |
+
print(" ...")
|
| 146 |
+
print()
|
| 147 |
+
|
| 148 |
+
# Generate base CSS
|
| 149 |
+
base_css = config.generate_base_css()
|
| 150 |
+
print(f"📝 Base CSS generated: {len(base_css)} characters")
|
| 151 |
+
print()
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
def demo_display_manager_integration():
|
| 155 |
+
"""Demonstrate integration with display manager."""
|
| 156 |
+
print("🔗 Display Manager Integration Demo")
|
| 157 |
+
print("=" * 50)
|
| 158 |
+
|
| 159 |
+
# Create configuration manager
|
| 160 |
+
config_manager = EnhancedDisplayConfigManager()
|
| 161 |
+
|
| 162 |
+
# Create display manager with configuration
|
| 163 |
+
display_manager = EnhancedResultsDisplayManager(config_manager=config_manager)
|
| 164 |
+
print(f"✅ Display manager created with config manager")
|
| 165 |
+
print(f" - Enhanced mode enabled: {display_manager.is_enhanced_mode_enabled()}")
|
| 166 |
+
print()
|
| 167 |
+
|
| 168 |
+
# Test with different configurations
|
| 169 |
+
print("🎛️ Testing with different configurations...")
|
| 170 |
+
|
| 171 |
+
# Test with enhanced mode disabled
|
| 172 |
+
config_manager.update_config(enabled=False)
|
| 173 |
+
display_manager.reload_config()
|
| 174 |
+
print(f" - Enhanced mode after disable: {display_manager.is_enhanced_mode_enabled()}")
|
| 175 |
+
|
| 176 |
+
# Test with enhanced mode re-enabled
|
| 177 |
+
config_manager.update_config(enabled=True)
|
| 178 |
+
display_manager.reload_config()
|
| 179 |
+
print(f" - Enhanced mode after enable: {display_manager.is_enhanced_mode_enabled()}")
|
| 180 |
+
print()
|
| 181 |
+
|
| 182 |
+
# Test formatting with different configurations
|
| 183 |
+
print("🎨 Testing formatting with different styles...")
|
| 184 |
+
|
| 185 |
+
# Format AI analysis with default config
|
| 186 |
+
ai_html = display_manager.format_ai_analysis_section(
|
| 187 |
+
classification="RED",
|
| 188 |
+
indicators=["Loss of meaning", "Spiritual distress"],
|
| 189 |
+
reasoning="Patient expressing existential concerns and loss of purpose."
|
| 190 |
+
)
|
| 191 |
+
print(f" - AI analysis HTML length: {len(ai_html)} characters")
|
| 192 |
+
|
| 193 |
+
# Disable icons and test again
|
| 194 |
+
config_manager.update_config(use_icons=False)
|
| 195 |
+
display_manager.reload_config()
|
| 196 |
+
|
| 197 |
+
ai_html_no_icons = display_manager.format_ai_analysis_section(
|
| 198 |
+
classification="RED",
|
| 199 |
+
indicators=["Loss of meaning", "Spiritual distress"],
|
| 200 |
+
reasoning="Patient expressing existential concerns and loss of purpose."
|
| 201 |
+
)
|
| 202 |
+
print(f" - AI analysis HTML (no icons): {len(ai_html_no_icons)} characters")
|
| 203 |
+
print()
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
def demo_configuration_validation():
|
| 207 |
+
"""Demonstrate configuration validation."""
|
| 208 |
+
print("✅ Configuration Validation Demo")
|
| 209 |
+
print("=" * 50)
|
| 210 |
+
|
| 211 |
+
config_manager = EnhancedDisplayConfigManager()
|
| 212 |
+
|
| 213 |
+
# Validate current configuration
|
| 214 |
+
issues = config_manager.validate_config()
|
| 215 |
+
print(f"📋 Validation results: {len(issues)} issues found")
|
| 216 |
+
|
| 217 |
+
if issues:
|
| 218 |
+
for issue in issues:
|
| 219 |
+
print(f" ⚠️ {issue}")
|
| 220 |
+
else:
|
| 221 |
+
print(" ✅ Configuration is valid")
|
| 222 |
+
print()
|
| 223 |
+
|
| 224 |
+
# Test with invalid configuration
|
| 225 |
+
print("🔧 Testing with invalid color...")
|
| 226 |
+
# Create a new config manager for this test to avoid corrupting the main one
|
| 227 |
+
test_config_manager = EnhancedDisplayConfigManager()
|
| 228 |
+
|
| 229 |
+
# Manually create invalid config for testing
|
| 230 |
+
test_config = test_config_manager.load_config()
|
| 231 |
+
# Directly modify the color to an invalid format
|
| 232 |
+
test_config.classification_colors.red = 'invalid-color'
|
| 233 |
+
test_config_manager._config = test_config
|
| 234 |
+
|
| 235 |
+
issues = test_config_manager.validate_config()
|
| 236 |
+
print(f"📋 Validation after invalid color: {len(issues)} issues found")
|
| 237 |
+
for issue in issues:
|
| 238 |
+
print(f" ⚠️ {issue}")
|
| 239 |
+
print()
|
| 240 |
+
|
| 241 |
+
# Reset to defaults
|
| 242 |
+
config_manager.reset_to_defaults()
|
| 243 |
+
print("🔄 Configuration reset to defaults")
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
def main():
|
| 247 |
+
"""Run all configuration demos."""
|
| 248 |
+
print("🚀 Enhanced Display Configuration System Demo")
|
| 249 |
+
print("=" * 60)
|
| 250 |
+
print()
|
| 251 |
+
|
| 252 |
+
try:
|
| 253 |
+
demo_basic_configuration()
|
| 254 |
+
demo_configuration_manager()
|
| 255 |
+
demo_preset_configurations()
|
| 256 |
+
demo_css_generation()
|
| 257 |
+
demo_display_manager_integration()
|
| 258 |
+
demo_configuration_validation()
|
| 259 |
+
|
| 260 |
+
print("✅ All configuration demos completed successfully!")
|
| 261 |
+
|
| 262 |
+
except Exception as e:
|
| 263 |
+
print(f"❌ Demo failed with error: {e}")
|
| 264 |
+
import traceback
|
| 265 |
+
traceback.print_exc()
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
if __name__ == "__main__":
|
| 269 |
+
main()
|
|
@@ -0,0 +1,279 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# enhanced_display_demo.py
|
| 3 |
+
"""
|
| 4 |
+
Demonstration of Enhanced Results Display Manager functionality.
|
| 5 |
+
|
| 6 |
+
This script shows how to use the enhanced display components to format
|
| 7 |
+
different types of content with improved visual separation and styling.
|
| 8 |
+
|
| 9 |
+
Requirements: 1.1, 1.2, 7.1, 7.2
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
import sys
|
| 13 |
+
import os
|
| 14 |
+
|
| 15 |
+
# Add project root to Python path
|
| 16 |
+
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 17 |
+
if project_root not in sys.path:
|
| 18 |
+
sys.path.insert(0, project_root)
|
| 19 |
+
|
| 20 |
+
from src.interface.enhanced_display_integration import (
|
| 21 |
+
create_enhanced_display_integration,
|
| 22 |
+
example_format_ai_analysis_result,
|
| 23 |
+
example_format_patient_message,
|
| 24 |
+
example_format_complete_response
|
| 25 |
+
)
|
| 26 |
+
from src.interface.enhanced_results_display_manager import EnhancedDisplayConfig
|
| 27 |
+
from src.interface.provider_summary_formatter import PatientData, ClassificationData
|
| 28 |
+
from unittest.mock import Mock
|
| 29 |
+
from src.core.provider_summary_generator import ProviderSummary
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def demo_ai_analysis_formatting():
|
| 33 |
+
"""Demonstrate AI analysis section formatting."""
|
| 34 |
+
print("=" * 60)
|
| 35 |
+
print("AI ANALYSIS SECTION FORMATTING DEMO")
|
| 36 |
+
print("=" * 60)
|
| 37 |
+
|
| 38 |
+
# Example 1: RED flag analysis
|
| 39 |
+
red_result = example_format_ai_analysis_result(
|
| 40 |
+
classification="RED",
|
| 41 |
+
indicators=["Loss of meaning and purpose", "Spiritual crisis", "Hopelessness"],
|
| 42 |
+
reasoning="Patient expressed deep existential concerns and loss of faith, indicating active spiritual distress requiring immediate attention.",
|
| 43 |
+
confidence=0.92
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
print("RED Flag Analysis:")
|
| 47 |
+
print(red_result)
|
| 48 |
+
print("\n" + "-" * 40 + "\n")
|
| 49 |
+
|
| 50 |
+
# Example 2: YELLOW flag analysis
|
| 51 |
+
yellow_result = example_format_ai_analysis_result(
|
| 52 |
+
classification="YELLOW",
|
| 53 |
+
indicators=["Mild distress", "Uncertainty"],
|
| 54 |
+
reasoning="Patient shows some signs of spiritual questioning but unclear if it constitutes distress.",
|
| 55 |
+
confidence=0.65
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
print("YELLOW Flag Analysis:")
|
| 59 |
+
print(yellow_result)
|
| 60 |
+
print("\n" + "-" * 40 + "\n")
|
| 61 |
+
|
| 62 |
+
# Example 3: GREEN flag analysis
|
| 63 |
+
green_result = example_format_ai_analysis_result(
|
| 64 |
+
classification="GREEN",
|
| 65 |
+
indicators=[],
|
| 66 |
+
reasoning="Normal medical conversation with no spiritual distress indicators detected."
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
print("GREEN Flag Analysis:")
|
| 70 |
+
print(green_result)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def demo_patient_message_formatting():
|
| 74 |
+
"""Demonstrate patient message section formatting."""
|
| 75 |
+
print("\n" + "=" * 60)
|
| 76 |
+
print("PATIENT MESSAGE SECTION FORMATTING DEMO")
|
| 77 |
+
print("=" * 60)
|
| 78 |
+
|
| 79 |
+
# Example messages
|
| 80 |
+
messages = [
|
| 81 |
+
"I'm feeling lost and don't know what to believe anymore.",
|
| 82 |
+
"The doctors say I'm getting better, but I feel empty inside.",
|
| 83 |
+
"I just want to understand why this happened to me.",
|
| 84 |
+
"Thank you for listening. It helps to talk about these things."
|
| 85 |
+
]
|
| 86 |
+
|
| 87 |
+
for i, message in enumerate(messages, 1):
|
| 88 |
+
print(f"Patient Message {i}:")
|
| 89 |
+
result = example_format_patient_message(message)
|
| 90 |
+
print(result)
|
| 91 |
+
print("\n" + "-" * 40 + "\n")
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def demo_provider_summary_formatting():
|
| 95 |
+
"""Demonstrate provider summary formatting."""
|
| 96 |
+
print("\n" + "=" * 60)
|
| 97 |
+
print("PROVIDER SUMMARY FORMATTING DEMO")
|
| 98 |
+
print("=" * 60)
|
| 99 |
+
|
| 100 |
+
integration = create_enhanced_display_integration()
|
| 101 |
+
|
| 102 |
+
# Create sample patient data
|
| 103 |
+
patient_data = PatientData(
|
| 104 |
+
name="Sarah Johnson",
|
| 105 |
+
age=34,
|
| 106 |
+
gender="female",
|
| 107 |
+
phone="(555) 234-5678",
|
| 108 |
+
medical_history=["Breast cancer", "Chemotherapy treatment", "Anxiety"],
|
| 109 |
+
expressed_concerns=["Fear of death", "Loss of faith", "Family burden"],
|
| 110 |
+
patient_input="I don't understand why God would let this happen to me and my children"
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
classification_data = ClassificationData(
|
| 114 |
+
classification="RED",
|
| 115 |
+
spiritual_concern_type="existential crisis related to illness",
|
| 116 |
+
consent_given=True
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
# Format as coherent paragraph
|
| 120 |
+
coherent_summary = integration.summary_formatter.format_enhanced_summary(
|
| 121 |
+
patient_data, classification_data
|
| 122 |
+
)
|
| 123 |
+
|
| 124 |
+
print("Medical Brain Summary:")
|
| 125 |
+
print(coherent_summary)
|
| 126 |
+
print("\n" + "-" * 40 + "\n")
|
| 127 |
+
|
| 128 |
+
# Create mock provider summary for enhanced display
|
| 129 |
+
mock_summary = Mock(spec=ProviderSummary)
|
| 130 |
+
mock_summary.patient_name = "Sarah Johnson"
|
| 131 |
+
mock_summary.patient_phone = "(555) 234-5678"
|
| 132 |
+
mock_summary.urgency_level = "URGENT"
|
| 133 |
+
mock_summary.follow_up_timeline = "Within 24 hours"
|
| 134 |
+
mock_summary.situation_description = "Patient experiencing existential crisis related to cancer diagnosis"
|
| 135 |
+
mock_summary.indicators = ["Fear of death", "Loss of faith", "Family burden"]
|
| 136 |
+
|
| 137 |
+
# Format as enhanced display section
|
| 138 |
+
enhanced_display = integration.display_manager.format_provider_summary_section(mock_summary)
|
| 139 |
+
|
| 140 |
+
print("Enhanced Display Summary:")
|
| 141 |
+
print(enhanced_display)
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def demo_complete_response_formatting():
|
| 145 |
+
"""Demonstrate complete response formatting with all sections."""
|
| 146 |
+
print("\n" + "=" * 60)
|
| 147 |
+
print("COMPLETE RESPONSE FORMATTING DEMO")
|
| 148 |
+
print("=" * 60)
|
| 149 |
+
|
| 150 |
+
# Create mock provider summary
|
| 151 |
+
mock_summary = Mock(spec=ProviderSummary)
|
| 152 |
+
mock_summary.patient_name = "Michael Chen"
|
| 153 |
+
mock_summary.patient_phone = "(555) 345-6789"
|
| 154 |
+
mock_summary.urgency_level = "IMMEDIATE"
|
| 155 |
+
mock_summary.follow_up_timeline = "Immediately"
|
| 156 |
+
mock_summary.situation_description = "Patient expressing suicidal ideation and spiritual crisis"
|
| 157 |
+
mock_summary.indicators = ["Suicidal thoughts", "Loss of hope", "Spiritual abandonment"]
|
| 158 |
+
|
| 159 |
+
# Format complete response
|
| 160 |
+
complete_response = example_format_complete_response(
|
| 161 |
+
patient_message="I just can't take this anymore. What's the point of going on?",
|
| 162 |
+
classification="RED",
|
| 163 |
+
indicators=["Suicidal ideation", "Hopelessness", "Spiritual crisis"],
|
| 164 |
+
reasoning="Patient expressing clear suicidal ideation combined with spiritual distress. Immediate intervention required.",
|
| 165 |
+
provider_summary=mock_summary
|
| 166 |
+
)
|
| 167 |
+
|
| 168 |
+
print("Complete Response with All Sections:")
|
| 169 |
+
print(complete_response)
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def demo_visual_separation():
|
| 173 |
+
"""Demonstrate visual separation and styling features."""
|
| 174 |
+
print("\n" + "=" * 60)
|
| 175 |
+
print("VISUAL SEPARATION AND STYLING DEMO")
|
| 176 |
+
print("=" * 60)
|
| 177 |
+
|
| 178 |
+
integration = create_enhanced_display_integration()
|
| 179 |
+
|
| 180 |
+
# Demonstrate classification badges
|
| 181 |
+
print("Classification Badges:")
|
| 182 |
+
red_badge = integration.get_classification_badge("RED")
|
| 183 |
+
yellow_badge = integration.get_classification_badge("YELLOW")
|
| 184 |
+
green_badge = integration.get_classification_badge("GREEN")
|
| 185 |
+
|
| 186 |
+
print(f"RED: {red_badge}")
|
| 187 |
+
print(f"YELLOW: {yellow_badge}")
|
| 188 |
+
print(f"GREEN: {green_badge}")
|
| 189 |
+
print("\n" + "-" * 40 + "\n")
|
| 190 |
+
|
| 191 |
+
# Demonstrate urgency badges
|
| 192 |
+
print("Urgency Badges:")
|
| 193 |
+
immediate_badge = integration.get_urgency_badge("IMMEDIATE")
|
| 194 |
+
urgent_badge = integration.get_urgency_badge("URGENT")
|
| 195 |
+
standard_badge = integration.get_urgency_badge("STANDARD")
|
| 196 |
+
|
| 197 |
+
print(f"IMMEDIATE: {immediate_badge}")
|
| 198 |
+
print(f"URGENT: {urgent_badge}")
|
| 199 |
+
print(f"STANDARD: {standard_badge}")
|
| 200 |
+
print("\n" + "-" * 40 + "\n")
|
| 201 |
+
|
| 202 |
+
# Demonstrate separators
|
| 203 |
+
print("Content Separators:")
|
| 204 |
+
separators = ["light", "medium", "heavy", "section_break"]
|
| 205 |
+
|
| 206 |
+
for sep_type in separators:
|
| 207 |
+
separator = integration.create_content_separator(sep_type)
|
| 208 |
+
print(f"{sep_type.upper()} separator:")
|
| 209 |
+
print(separator)
|
| 210 |
+
print()
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
def demo_custom_configuration():
|
| 214 |
+
"""Demonstrate custom configuration options."""
|
| 215 |
+
print("\n" + "=" * 60)
|
| 216 |
+
print("CUSTOM CONFIGURATION DEMO")
|
| 217 |
+
print("=" * 60)
|
| 218 |
+
|
| 219 |
+
# Create custom configuration
|
| 220 |
+
custom_config = EnhancedDisplayConfig(
|
| 221 |
+
ai_analysis_icon="🔍",
|
| 222 |
+
patient_message_icon="📝",
|
| 223 |
+
provider_summary_icon="🏥",
|
| 224 |
+
section_separator="***",
|
| 225 |
+
use_color_coding=True,
|
| 226 |
+
classification_colors={
|
| 227 |
+
"RED": "#cc0000",
|
| 228 |
+
"YELLOW": "#ff9900",
|
| 229 |
+
"GREEN": "#009900"
|
| 230 |
+
}
|
| 231 |
+
)
|
| 232 |
+
|
| 233 |
+
# Create integration with custom config
|
| 234 |
+
custom_integration = create_enhanced_display_integration(custom_config)
|
| 235 |
+
|
| 236 |
+
# Format example with custom styling
|
| 237 |
+
custom_result = custom_integration.display_manager.format_ai_analysis_section(
|
| 238 |
+
classification="RED",
|
| 239 |
+
indicators=["Custom styling example"],
|
| 240 |
+
reasoning="This demonstrates custom configuration options.",
|
| 241 |
+
confidence=0.95
|
| 242 |
+
)
|
| 243 |
+
|
| 244 |
+
print("Custom Configuration Result:")
|
| 245 |
+
print(custom_result)
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
def main():
|
| 249 |
+
"""Run all demonstration functions."""
|
| 250 |
+
print("ENHANCED RESULTS DISPLAY MANAGER DEMONSTRATION")
|
| 251 |
+
print("=" * 60)
|
| 252 |
+
print("This demo shows the enhanced display components in action.")
|
| 253 |
+
print("Note: HTML output is shown as raw HTML for demonstration purposes.")
|
| 254 |
+
print("In a real application, this would be rendered in a web browser.")
|
| 255 |
+
|
| 256 |
+
try:
|
| 257 |
+
demo_ai_analysis_formatting()
|
| 258 |
+
demo_patient_message_formatting()
|
| 259 |
+
demo_provider_summary_formatting()
|
| 260 |
+
demo_complete_response_formatting()
|
| 261 |
+
demo_visual_separation()
|
| 262 |
+
demo_custom_configuration()
|
| 263 |
+
|
| 264 |
+
print("\n" + "=" * 60)
|
| 265 |
+
print("DEMONSTRATION COMPLETE")
|
| 266 |
+
print("=" * 60)
|
| 267 |
+
print("All enhanced display components are working correctly!")
|
| 268 |
+
|
| 269 |
+
except Exception as e:
|
| 270 |
+
print(f"\nError during demonstration: {e}")
|
| 271 |
+
import traceback
|
| 272 |
+
traceback.print_exc()
|
| 273 |
+
return 1
|
| 274 |
+
|
| 275 |
+
return 0
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
if __name__ == "__main__":
|
| 279 |
+
sys.exit(main())
|
|
@@ -0,0 +1,266 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Enhanced Verification System Demo.
|
| 4 |
+
|
| 5 |
+
Demonstrates the enhanced verification system with new display formats,
|
| 6 |
+
provider summaries, and CSV export capabilities.
|
| 7 |
+
|
| 8 |
+
Requirements: 8.2, 8.3, 8.4, 8.5
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import tempfile
|
| 12 |
+
import os
|
| 13 |
+
from datetime import datetime
|
| 14 |
+
|
| 15 |
+
from src.core.conversation_logger import ConversationLogger
|
| 16 |
+
from src.core.conversation_verification import (
|
| 17 |
+
EnhancedConversationVerificationManager,
|
| 18 |
+
VerificationFeedback
|
| 19 |
+
)
|
| 20 |
+
from src.core.verification_exporter import EnhancedVerificationExporter
|
| 21 |
+
from src.core.spiritual_state import SpiritualState, SpiritualAssessment
|
| 22 |
+
from src.core.provider_summary_generator import ProviderSummary
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def create_sample_conversation():
|
| 26 |
+
"""Create a sample conversation with various classification levels."""
|
| 27 |
+
logger = ConversationLogger(patient_name="Demo Patient")
|
| 28 |
+
|
| 29 |
+
# Sample conversation exchanges with different classifications
|
| 30 |
+
exchanges = [
|
| 31 |
+
{
|
| 32 |
+
"user": "I've been struggling with the meaning of my illness lately",
|
| 33 |
+
"assistant": "I understand this is a very difficult time for you. Many people in your situation experience these feelings.",
|
| 34 |
+
"assessment": SpiritualAssessment(
|
| 35 |
+
state=SpiritualState.RED,
|
| 36 |
+
confidence=0.85,
|
| 37 |
+
indicators=["loss of meaning", "spiritual distress", "existential questioning"],
|
| 38 |
+
reasoning="Patient expressing significant spiritual concerns about meaning and purpose in context of illness"
|
| 39 |
+
)
|
| 40 |
+
},
|
| 41 |
+
{
|
| 42 |
+
"user": "Sometimes I wonder if there's any point to all this suffering",
|
| 43 |
+
"assistant": "Your feelings are completely valid. It's natural to question these things when facing challenges.",
|
| 44 |
+
"assessment": SpiritualAssessment(
|
| 45 |
+
state=SpiritualState.RED,
|
| 46 |
+
confidence=0.90,
|
| 47 |
+
indicators=["questioning purpose", "suffering distress", "existential crisis"],
|
| 48 |
+
reasoning="Patient expressing deep existential concerns about suffering and purpose"
|
| 49 |
+
)
|
| 50 |
+
},
|
| 51 |
+
{
|
| 52 |
+
"user": "I'm feeling a bit better today, but still uncertain",
|
| 53 |
+
"assistant": "I'm glad to hear you're feeling better. Uncertainty is normal during recovery.",
|
| 54 |
+
"assessment": SpiritualAssessment(
|
| 55 |
+
state=SpiritualState.YELLOW,
|
| 56 |
+
confidence=0.65,
|
| 57 |
+
indicators=["uncertainty", "mild improvement"],
|
| 58 |
+
reasoning="Patient showing some improvement but still experiencing uncertainty"
|
| 59 |
+
)
|
| 60 |
+
},
|
| 61 |
+
{
|
| 62 |
+
"user": "Thank you for listening. I feel more hopeful now",
|
| 63 |
+
"assistant": "You're very welcome. I'm here whenever you need support.",
|
| 64 |
+
"assessment": SpiritualAssessment(
|
| 65 |
+
state=SpiritualState.GREEN,
|
| 66 |
+
confidence=0.80,
|
| 67 |
+
indicators=["gratitude", "hope", "positive outlook"],
|
| 68 |
+
reasoning="Patient expressing gratitude and hope, indicating improved spiritual state"
|
| 69 |
+
)
|
| 70 |
+
}
|
| 71 |
+
]
|
| 72 |
+
|
| 73 |
+
# Log all exchanges
|
| 74 |
+
for exchange in exchanges:
|
| 75 |
+
logger.log_exchange(
|
| 76 |
+
exchange["user"],
|
| 77 |
+
exchange["assistant"],
|
| 78 |
+
exchange["assessment"]
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
return logger
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def demonstrate_enhanced_verification():
|
| 85 |
+
"""Demonstrate the enhanced verification system."""
|
| 86 |
+
print("=" * 80)
|
| 87 |
+
print("ENHANCED VERIFICATION SYSTEM DEMONSTRATION")
|
| 88 |
+
print("=" * 80)
|
| 89 |
+
print()
|
| 90 |
+
|
| 91 |
+
# Create temporary directory for demo
|
| 92 |
+
temp_dir = tempfile.mkdtemp()
|
| 93 |
+
print(f"📁 Using temporary directory: {temp_dir}")
|
| 94 |
+
print()
|
| 95 |
+
|
| 96 |
+
try:
|
| 97 |
+
# Step 1: Create sample conversation
|
| 98 |
+
print("🗣️ STEP 1: Creating sample conversation...")
|
| 99 |
+
logger = create_sample_conversation()
|
| 100 |
+
print(f" ✅ Created conversation with {len(logger.entries)} exchanges")
|
| 101 |
+
print()
|
| 102 |
+
|
| 103 |
+
# Step 2: Create enhanced verification session
|
| 104 |
+
print("🔍 STEP 2: Creating enhanced verification session...")
|
| 105 |
+
verification_manager = EnhancedConversationVerificationManager(temp_dir)
|
| 106 |
+
session = verification_manager.create_verification_session(
|
| 107 |
+
logger,
|
| 108 |
+
verifier_name="Demo Verifier",
|
| 109 |
+
enable_enhanced_formats=True
|
| 110 |
+
)
|
| 111 |
+
print(f" ✅ Created enhanced verification session: {session.session_id}")
|
| 112 |
+
print(f" 📊 Enhanced format enabled: {session.enhanced_format_enabled}")
|
| 113 |
+
print(f" 📝 Total records: {len(session.verification_records)}")
|
| 114 |
+
print()
|
| 115 |
+
|
| 116 |
+
# Step 3: Show enhanced display formats
|
| 117 |
+
print("🎨 STEP 3: Demonstrating enhanced display formats...")
|
| 118 |
+
for i, record in enumerate(session.verification_records[:2], 1): # Show first 2
|
| 119 |
+
print(f" 📋 Record {i} Enhanced Display:")
|
| 120 |
+
if record.enhanced_display_format:
|
| 121 |
+
# Show first 200 chars of HTML
|
| 122 |
+
preview = record.enhanced_display_format[:200].replace('\n', ' ')
|
| 123 |
+
print(f" HTML Preview: {preview}...")
|
| 124 |
+
|
| 125 |
+
if record.visual_sections:
|
| 126 |
+
print(f" Visual Sections: {len(record.visual_sections)} sections")
|
| 127 |
+
for section in record.visual_sections:
|
| 128 |
+
print(f" - {section.get('type', 'unknown')}")
|
| 129 |
+
print()
|
| 130 |
+
|
| 131 |
+
# Step 4: Add provider summary to RED flag cases
|
| 132 |
+
print("📋 STEP 4: Adding provider summaries to RED flag cases...")
|
| 133 |
+
red_records = [r for r in session.verification_records if r.original_classification == "RED"]
|
| 134 |
+
|
| 135 |
+
for i, record in enumerate(red_records):
|
| 136 |
+
provider_summary = ProviderSummary(
|
| 137 |
+
patient_name="Demo Patient",
|
| 138 |
+
patient_phone="555-DEMO",
|
| 139 |
+
classification="RED",
|
| 140 |
+
confidence=record.original_confidence,
|
| 141 |
+
indicators=record.original_indicators,
|
| 142 |
+
reasoning=record.original_reasoning,
|
| 143 |
+
urgency_level="IMMEDIATE" if record.original_confidence > 0.8 else "URGENT",
|
| 144 |
+
severity_level="HIGH",
|
| 145 |
+
situation_description=f"Patient expressing {', '.join(record.original_indicators[:2])} requiring immediate spiritual care attention"
|
| 146 |
+
)
|
| 147 |
+
|
| 148 |
+
record.set_enhanced_formats(provider_summary=provider_summary)
|
| 149 |
+
print(f" ✅ Added provider summary to RED flag record {i+1}")
|
| 150 |
+
|
| 151 |
+
print(f" 📊 Provider summaries added to {len(red_records)} RED flag cases")
|
| 152 |
+
print()
|
| 153 |
+
|
| 154 |
+
# Step 5: Add verification feedback
|
| 155 |
+
print("✅ STEP 5: Adding verification feedback...")
|
| 156 |
+
feedbacks = [
|
| 157 |
+
{"correct": True, "notes": "Correctly identified spiritual distress"},
|
| 158 |
+
{"correct": True, "notes": "Appropriate RED classification"},
|
| 159 |
+
{"correct": False, "classification": "GREEN", "reason": "Patient showing improvement, should be GREEN"},
|
| 160 |
+
{"correct": True, "notes": "Correct GREEN classification"}
|
| 161 |
+
]
|
| 162 |
+
|
| 163 |
+
for i, (record, feedback_data) in enumerate(zip(session.verification_records, feedbacks)):
|
| 164 |
+
feedback = VerificationFeedback(
|
| 165 |
+
exchange_id=record.exchange_id,
|
| 166 |
+
is_correct=feedback_data["correct"],
|
| 167 |
+
correct_classification=feedback_data.get("classification"),
|
| 168 |
+
correction_reason=feedback_data.get("reason"),
|
| 169 |
+
notes=feedback_data.get("notes")
|
| 170 |
+
)
|
| 171 |
+
|
| 172 |
+
success = verification_manager.submit_exchange_verification(
|
| 173 |
+
session.session_id,
|
| 174 |
+
record.exchange_id,
|
| 175 |
+
feedback
|
| 176 |
+
)
|
| 177 |
+
|
| 178 |
+
if success:
|
| 179 |
+
print(f" ✅ Added feedback for exchange {i+1}")
|
| 180 |
+
else:
|
| 181 |
+
print(f" ❌ Failed to add feedback for exchange {i+1}")
|
| 182 |
+
print()
|
| 183 |
+
|
| 184 |
+
# Step 6: Export enhanced CSV
|
| 185 |
+
print("📊 STEP 6: Exporting enhanced CSV with new format data...")
|
| 186 |
+
exporter = EnhancedVerificationExporter(temp_dir)
|
| 187 |
+
csv_path = exporter.export_session_to_csv(session, include_enhanced_data=True)
|
| 188 |
+
|
| 189 |
+
print(f" ✅ Exported enhanced CSV: {os.path.basename(csv_path)}")
|
| 190 |
+
|
| 191 |
+
# Show CSV preview
|
| 192 |
+
with open(csv_path, 'r', encoding='utf-8') as f:
|
| 193 |
+
lines = f.readlines()
|
| 194 |
+
|
| 195 |
+
print(f" 📄 CSV contains {len(lines)} lines")
|
| 196 |
+
print(" 📋 Enhanced columns included:")
|
| 197 |
+
|
| 198 |
+
# Find header line
|
| 199 |
+
for line in lines:
|
| 200 |
+
if 'has_enhanced_display' in line:
|
| 201 |
+
enhanced_columns = [col for col in line.split(',') if 'enhanced' in col.lower() or 'provider' in col.lower()]
|
| 202 |
+
for col in enhanced_columns[:5]: # Show first 5 enhanced columns
|
| 203 |
+
print(f" - {col.strip()}")
|
| 204 |
+
break
|
| 205 |
+
print()
|
| 206 |
+
|
| 207 |
+
# Step 7: Generate enhanced summary report
|
| 208 |
+
print("📈 STEP 7: Generating enhanced summary report...")
|
| 209 |
+
report_path = exporter.export_enhanced_summary_report(session)
|
| 210 |
+
|
| 211 |
+
print(f" ✅ Generated summary report: {os.path.basename(report_path)}")
|
| 212 |
+
|
| 213 |
+
# Show report preview
|
| 214 |
+
with open(report_path, 'r', encoding='utf-8') as f:
|
| 215 |
+
content = f.read()
|
| 216 |
+
|
| 217 |
+
# Extract key statistics
|
| 218 |
+
lines = content.split('\n')
|
| 219 |
+
for line in lines:
|
| 220 |
+
if 'Enhanced Display Coverage:' in line or 'Records with Enhanced Display:' in line:
|
| 221 |
+
print(f" 📊 {line.strip()}")
|
| 222 |
+
print()
|
| 223 |
+
|
| 224 |
+
# Step 8: Show session statistics
|
| 225 |
+
print("📈 STEP 8: Enhanced session statistics...")
|
| 226 |
+
stats = verification_manager.get_session_statistics(session.session_id)
|
| 227 |
+
|
| 228 |
+
if stats:
|
| 229 |
+
print(f" 📊 Total Exchanges: {stats.get('total_exchanges', 0)}")
|
| 230 |
+
print(f" ✅ Verified Exchanges: {stats.get('verified_exchanges', 0)}")
|
| 231 |
+
print(f" 🎯 Overall Accuracy: {stats.get('progress', {}).get('accuracy_overall', 0):.1%}")
|
| 232 |
+
print(f" 🎨 Enhanced Format Enabled: {stats.get('enhanced_format_enabled', False)}")
|
| 233 |
+
print(f" 📋 Records with Enhanced Display: {stats.get('records_with_enhanced_display', 0)}")
|
| 234 |
+
print(f" 📄 Records with Provider Summary: {stats.get('records_with_provider_summary', 0)}")
|
| 235 |
+
print()
|
| 236 |
+
|
| 237 |
+
print("🎉 DEMONSTRATION COMPLETE!")
|
| 238 |
+
print("=" * 80)
|
| 239 |
+
print()
|
| 240 |
+
print("📁 Generated Files:")
|
| 241 |
+
print(f" 📊 Enhanced CSV: {csv_path}")
|
| 242 |
+
print(f" 📈 Summary Report: {report_path}")
|
| 243 |
+
print(f" 💾 Session Data: {temp_dir}")
|
| 244 |
+
print()
|
| 245 |
+
print("✨ Enhanced verification system successfully demonstrated!")
|
| 246 |
+
print(" - Enhanced display formats with visual sections")
|
| 247 |
+
print(" - Provider summaries for RED flag cases")
|
| 248 |
+
print(" - Enhanced CSV export with new data fields")
|
| 249 |
+
print(" - Comprehensive summary reports")
|
| 250 |
+
print(" - Backward compatibility with existing system")
|
| 251 |
+
|
| 252 |
+
except Exception as e:
|
| 253 |
+
print(f"❌ Error during demonstration: {e}")
|
| 254 |
+
import traceback
|
| 255 |
+
traceback.print_exc()
|
| 256 |
+
|
| 257 |
+
finally:
|
| 258 |
+
# Clean up (optional - comment out to keep files for inspection)
|
| 259 |
+
# import shutil
|
| 260 |
+
# shutil.rmtree(temp_dir, ignore_errors=True)
|
| 261 |
+
# print(f"🧹 Cleaned up temporary directory")
|
| 262 |
+
pass
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
if __name__ == "__main__":
|
| 266 |
+
demonstrate_enhanced_verification()
|
|
@@ -0,0 +1,279 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Error Handling Demo for UI Classification Improvements.
|
| 4 |
+
|
| 5 |
+
Demonstrates error handling and recovery mechanisms in action.
|
| 6 |
+
|
| 7 |
+
Requirements: 9.1, 9.2, 9.3, 9.4
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import sys
|
| 11 |
+
import os
|
| 12 |
+
|
| 13 |
+
# Add src to path for imports
|
| 14 |
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
| 15 |
+
parent_dir = os.path.dirname(current_dir)
|
| 16 |
+
if parent_dir not in sys.path:
|
| 17 |
+
sys.path.insert(0, parent_dir)
|
| 18 |
+
|
| 19 |
+
from src.core.ui_error_handler import UIErrorHandler
|
| 20 |
+
from src.core.provider_summary_generator import ProviderSummary, ProviderSummaryGenerator
|
| 21 |
+
from src.interface.enhanced_results_display_manager import EnhancedResultsDisplayManager
|
| 22 |
+
from src.core.improved_classification_prompt_manager import ImprovedClassificationPromptManager
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def demo_provider_summary_validation():
|
| 26 |
+
"""Demonstrate provider summary validation and error recovery."""
|
| 27 |
+
print("=" * 60)
|
| 28 |
+
print("PROVIDER SUMMARY VALIDATION DEMO")
|
| 29 |
+
print("=" * 60)
|
| 30 |
+
|
| 31 |
+
error_handler = UIErrorHandler()
|
| 32 |
+
|
| 33 |
+
# Create invalid summary
|
| 34 |
+
print("\n1. Creating invalid provider summary...")
|
| 35 |
+
invalid_summary = ProviderSummary(
|
| 36 |
+
patient_name="[Patient Name]", # Placeholder
|
| 37 |
+
patient_phone="[Phone Number]", # Placeholder
|
| 38 |
+
classification="RED",
|
| 39 |
+
confidence=1.5, # Invalid confidence
|
| 40 |
+
reasoning="", # Empty reasoning
|
| 41 |
+
indicators=[], # No indicators
|
| 42 |
+
severity_level="INVALID", # Invalid level
|
| 43 |
+
urgency_level="INVALID" # Invalid level
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
print(f" Patient Name: {invalid_summary.patient_name}")
|
| 47 |
+
print(f" Patient Phone: {invalid_summary.patient_phone}")
|
| 48 |
+
print(f" Confidence: {invalid_summary.confidence}")
|
| 49 |
+
print(f" Reasoning: '{invalid_summary.reasoning}'")
|
| 50 |
+
print(f" Indicators: {invalid_summary.indicators}")
|
| 51 |
+
print(f" Severity: {invalid_summary.severity_level}")
|
| 52 |
+
print(f" Urgency: {invalid_summary.urgency_level}")
|
| 53 |
+
|
| 54 |
+
# Validate summary
|
| 55 |
+
print("\n2. Validating summary structure...")
|
| 56 |
+
validation_result = error_handler.validate_provider_summary_structure(invalid_summary)
|
| 57 |
+
|
| 58 |
+
print(f" Valid: {validation_result.is_valid}")
|
| 59 |
+
print(f" Errors: {len(validation_result.errors)}")
|
| 60 |
+
print(f" Warnings: {len(validation_result.warnings)}")
|
| 61 |
+
|
| 62 |
+
if validation_result.errors:
|
| 63 |
+
print("\n Error Details:")
|
| 64 |
+
for i, error in enumerate(validation_result.errors[:3], 1): # Show first 3
|
| 65 |
+
print(f" {i}. {error.message}")
|
| 66 |
+
if error.suggestion:
|
| 67 |
+
print(f" Suggestion: {error.suggestion}")
|
| 68 |
+
|
| 69 |
+
# Apply fallback template
|
| 70 |
+
print("\n3. Applying fallback template...")
|
| 71 |
+
fixed_summary = error_handler.apply_fallback_template(invalid_summary, "general")
|
| 72 |
+
|
| 73 |
+
print(f" Patient Name: {fixed_summary.patient_name}")
|
| 74 |
+
print(f" Patient Phone: {fixed_summary.patient_phone}")
|
| 75 |
+
print(f" Confidence: {fixed_summary.confidence}")
|
| 76 |
+
print(f" Reasoning: '{fixed_summary.reasoning[:50]}...'")
|
| 77 |
+
print(f" Indicators: {fixed_summary.indicators}")
|
| 78 |
+
print(f" Severity: {fixed_summary.severity_level}")
|
| 79 |
+
print(f" Urgency: {fixed_summary.urgency_level}")
|
| 80 |
+
|
| 81 |
+
# Re-validate
|
| 82 |
+
print("\n4. Re-validating fixed summary...")
|
| 83 |
+
new_validation = error_handler.validate_provider_summary_structure(fixed_summary)
|
| 84 |
+
print(f" Valid: {new_validation.is_valid}")
|
| 85 |
+
print(f" Errors: {len(new_validation.errors)}")
|
| 86 |
+
print(f" Warnings: {len(new_validation.warnings)}")
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def demo_display_error_handling():
|
| 90 |
+
"""Demonstrate display error handling and degraded mode."""
|
| 91 |
+
print("\n" + "=" * 60)
|
| 92 |
+
print("DISPLAY ERROR HANDLING DEMO")
|
| 93 |
+
print("=" * 60)
|
| 94 |
+
|
| 95 |
+
display_manager = EnhancedResultsDisplayManager()
|
| 96 |
+
|
| 97 |
+
# Test with invalid data
|
| 98 |
+
print("\n1. Testing display with invalid data...")
|
| 99 |
+
try:
|
| 100 |
+
result = display_manager.format_ai_analysis_section(
|
| 101 |
+
classification="", # Empty classification
|
| 102 |
+
indicators=[], # Empty indicators
|
| 103 |
+
reasoning="", # Empty reasoning
|
| 104 |
+
confidence=None
|
| 105 |
+
)
|
| 106 |
+
|
| 107 |
+
print(" Display result generated successfully")
|
| 108 |
+
print(f" Result length: {len(result)} characters")
|
| 109 |
+
print(f" Contains 'AI Analysis': {'AI Analysis' in result}")
|
| 110 |
+
print(f" Contains error handling: {'Display Error' in result or 'No indicators' in result}")
|
| 111 |
+
|
| 112 |
+
except Exception as e:
|
| 113 |
+
print(f" Error occurred: {e}")
|
| 114 |
+
|
| 115 |
+
# Test degraded display
|
| 116 |
+
print("\n2. Testing degraded display creation...")
|
| 117 |
+
error_context = "Simulated formatting error"
|
| 118 |
+
original_content = "<div>Original content that failed to format</div>"
|
| 119 |
+
|
| 120 |
+
degraded_display = display_manager.error_handler.create_degraded_display(
|
| 121 |
+
error_context, original_content
|
| 122 |
+
)
|
| 123 |
+
|
| 124 |
+
print(" Degraded display created successfully")
|
| 125 |
+
print(f" Contains error message: {'Display Error Detected' in degraded_display}")
|
| 126 |
+
print(f" Contains recovery actions: {'Recovery Actions' in degraded_display}")
|
| 127 |
+
print(f" Contains original content: {original_content in degraded_display}")
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def demo_classification_error_handling():
|
| 131 |
+
"""Demonstrate classification error handling."""
|
| 132 |
+
print("\n" + "=" * 60)
|
| 133 |
+
print("CLASSIFICATION ERROR HANDLING DEMO")
|
| 134 |
+
print("=" * 60)
|
| 135 |
+
|
| 136 |
+
error_handler = UIErrorHandler()
|
| 137 |
+
|
| 138 |
+
# Simulate classification error
|
| 139 |
+
print("\n1. Simulating classification error...")
|
| 140 |
+
test_error = Exception("Simulated AI classification failure")
|
| 141 |
+
input_data = {
|
| 142 |
+
'message': 'I feel hopeless and have lost all meaning in life',
|
| 143 |
+
'classification': 'red',
|
| 144 |
+
'confidence': 0.8
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
fallback_result = error_handler.handle_classification_error(test_error, input_data)
|
| 148 |
+
|
| 149 |
+
print(f" Fallback classification: {fallback_result.classification}")
|
| 150 |
+
print(f" Fallback confidence: {fallback_result.confidence}")
|
| 151 |
+
print(f" Fallback indicators: {fallback_result.indicators}")
|
| 152 |
+
print(f" Fallback reasoning: {fallback_result.reasoning[:50]}...")
|
| 153 |
+
print(f" Is valid: {fallback_result.is_valid}")
|
| 154 |
+
|
| 155 |
+
# Test with critical keywords
|
| 156 |
+
print("\n2. Testing with critical keywords...")
|
| 157 |
+
critical_input = {
|
| 158 |
+
'message': 'I want to kill myself and end it all'
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
critical_result = error_handler.handle_classification_error(test_error, critical_input)
|
| 162 |
+
|
| 163 |
+
print(f" Critical classification: {critical_result.classification}")
|
| 164 |
+
print(f" Critical confidence: {critical_result.confidence}")
|
| 165 |
+
print(f" Critical indicators: {critical_result.indicators}")
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
def demo_end_to_end_recovery():
|
| 169 |
+
"""Demonstrate end-to-end error recovery."""
|
| 170 |
+
print("\n" + "=" * 60)
|
| 171 |
+
print("END-TO-END ERROR RECOVERY DEMO")
|
| 172 |
+
print("=" * 60)
|
| 173 |
+
|
| 174 |
+
# Initialize components
|
| 175 |
+
summary_generator = ProviderSummaryGenerator()
|
| 176 |
+
display_manager = EnhancedResultsDisplayManager()
|
| 177 |
+
|
| 178 |
+
# Start with problematic data
|
| 179 |
+
print("\n1. Starting with problematic data...")
|
| 180 |
+
problematic_data = {
|
| 181 |
+
'classification': 'INVALID',
|
| 182 |
+
'confidence': -0.5,
|
| 183 |
+
'indicators': [],
|
| 184 |
+
'reasoning': '',
|
| 185 |
+
'patient_name': '',
|
| 186 |
+
'patient_phone': ''
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
print(f" Classification: {problematic_data['classification']}")
|
| 190 |
+
print(f" Confidence: {problematic_data['confidence']}")
|
| 191 |
+
print(f" Indicators: {problematic_data['indicators']}")
|
| 192 |
+
print(f" Reasoning: '{problematic_data['reasoning']}'")
|
| 193 |
+
print(f" Patient Name: '{problematic_data['patient_name']}'")
|
| 194 |
+
print(f" Patient Phone: '{problematic_data['patient_phone']}'")
|
| 195 |
+
|
| 196 |
+
# Generate summary with error recovery
|
| 197 |
+
print("\n2. Generating summary with error recovery...")
|
| 198 |
+
summary = summary_generator.generate_summary(
|
| 199 |
+
indicators=problematic_data['indicators'],
|
| 200 |
+
reasoning=problematic_data['reasoning'],
|
| 201 |
+
confidence=problematic_data['confidence'],
|
| 202 |
+
patient_name=problematic_data['patient_name'],
|
| 203 |
+
patient_phone=problematic_data['patient_phone']
|
| 204 |
+
)
|
| 205 |
+
|
| 206 |
+
print(f" Fixed confidence: {summary.confidence}")
|
| 207 |
+
print(f" Fixed reasoning length: {len(summary.reasoning)}")
|
| 208 |
+
print(f" Fixed patient name: {summary.patient_name}")
|
| 209 |
+
print(f" Fixed patient phone: {summary.patient_phone}")
|
| 210 |
+
print(f" Recommended actions: {len(summary.recommended_actions)}")
|
| 211 |
+
|
| 212 |
+
# Display with error handling
|
| 213 |
+
print("\n3. Displaying with error handling...")
|
| 214 |
+
display_result = display_manager.format_provider_summary_section(summary)
|
| 215 |
+
|
| 216 |
+
print(f" Display result generated: {len(display_result)} characters")
|
| 217 |
+
print(f" Contains provider summary: {'Provider Summary' in display_result}")
|
| 218 |
+
print(f" Contains patient info: {summary.patient_name in display_result}")
|
| 219 |
+
|
| 220 |
+
print("\n4. System successfully recovered from all errors!")
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
def demo_error_statistics():
|
| 224 |
+
"""Demonstrate error statistics collection."""
|
| 225 |
+
print("\n" + "=" * 60)
|
| 226 |
+
print("ERROR STATISTICS DEMO")
|
| 227 |
+
print("=" * 60)
|
| 228 |
+
|
| 229 |
+
error_handler = UIErrorHandler()
|
| 230 |
+
|
| 231 |
+
# Create multiple invalid summaries to collect statistics
|
| 232 |
+
print("\n1. Creating multiple invalid summaries...")
|
| 233 |
+
summaries = [
|
| 234 |
+
ProviderSummary(patient_name="", patient_phone="", confidence=1.5, reasoning=""),
|
| 235 |
+
ProviderSummary(patient_name="[Patient Name]", patient_phone="invalid", confidence=-0.1, reasoning="short"),
|
| 236 |
+
ProviderSummary(patient_name="Valid Name", patient_phone="555-123-4567", confidence=0.8, reasoning="", indicators=[])
|
| 237 |
+
]
|
| 238 |
+
|
| 239 |
+
all_errors = []
|
| 240 |
+
for i, summary in enumerate(summaries, 1):
|
| 241 |
+
validation_result = error_handler.validate_provider_summary_structure(summary)
|
| 242 |
+
all_errors.extend(validation_result.errors)
|
| 243 |
+
print(f" Summary {i}: {len(validation_result.errors)} errors, {len(validation_result.warnings)} warnings")
|
| 244 |
+
|
| 245 |
+
# Get statistics
|
| 246 |
+
print(f"\n2. Collecting statistics from {len(all_errors)} total errors...")
|
| 247 |
+
stats = error_handler.get_error_statistics(all_errors)
|
| 248 |
+
|
| 249 |
+
print(f" Total errors: {stats['total']}")
|
| 250 |
+
print(f" By category: {stats['by_category']}")
|
| 251 |
+
print(f" By severity: {stats['by_severity']}")
|
| 252 |
+
print(f" By component: {stats['by_component']}")
|
| 253 |
+
|
| 254 |
+
|
| 255 |
+
def main():
|
| 256 |
+
"""Run all error handling demos."""
|
| 257 |
+
print("UI ERROR HANDLING AND RECOVERY DEMONSTRATION")
|
| 258 |
+
print("Requirements: 9.1, 9.2, 9.3, 9.4")
|
| 259 |
+
|
| 260 |
+
try:
|
| 261 |
+
demo_provider_summary_validation()
|
| 262 |
+
demo_display_error_handling()
|
| 263 |
+
demo_classification_error_handling()
|
| 264 |
+
demo_end_to_end_recovery()
|
| 265 |
+
demo_error_statistics()
|
| 266 |
+
|
| 267 |
+
print("\n" + "=" * 60)
|
| 268 |
+
print("ALL DEMOS COMPLETED SUCCESSFULLY!")
|
| 269 |
+
print("Error handling and recovery mechanisms are working correctly.")
|
| 270 |
+
print("=" * 60)
|
| 271 |
+
|
| 272 |
+
except Exception as e:
|
| 273 |
+
print(f"\nDemo failed with error: {e}")
|
| 274 |
+
import traceback
|
| 275 |
+
traceback.print_exc()
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
if __name__ == "__main__":
|
| 279 |
+
main()
|
|
@@ -0,0 +1,206 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Integration Demo for UI Classification Improvements.
|
| 4 |
+
|
| 5 |
+
Demonstrates the complete workflow from classification to display to verification.
|
| 6 |
+
This shows how all components work together in the enhanced system.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import sys
|
| 10 |
+
import os
|
| 11 |
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
|
| 12 |
+
|
| 13 |
+
from interface.enhanced_results_display_manager import EnhancedResultsDisplayManager
|
| 14 |
+
from core.provider_summary_generator import ProviderSummary, ProviderSummaryGenerator
|
| 15 |
+
from core.improved_classification_prompt_manager import ImprovedClassificationPromptManager
|
| 16 |
+
from core.conversation_logger import ConversationLogger
|
| 17 |
+
from core.spiritual_state import SpiritualState, SpiritualAssessment
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def main():
|
| 21 |
+
"""Run integration demo."""
|
| 22 |
+
print("🚀 UI Classification Improvements Integration Demo")
|
| 23 |
+
print("=" * 60)
|
| 24 |
+
|
| 25 |
+
# Initialize components
|
| 26 |
+
print("\n1. Initializing enhanced components...")
|
| 27 |
+
display_manager = EnhancedResultsDisplayManager()
|
| 28 |
+
summary_generator = ProviderSummaryGenerator()
|
| 29 |
+
classification_manager = ImprovedClassificationPromptManager()
|
| 30 |
+
|
| 31 |
+
print(" ✅ Enhanced Results Display Manager")
|
| 32 |
+
print(" ✅ Provider Summary Generator")
|
| 33 |
+
print(" ✅ Improved Classification Prompt Manager")
|
| 34 |
+
|
| 35 |
+
# Demonstrate enhanced classification
|
| 36 |
+
print("\n2. Testing enhanced classification criteria...")
|
| 37 |
+
|
| 38 |
+
# Test explicit red indicators
|
| 39 |
+
red_indicators = classification_manager.get_explicit_red_indicators()
|
| 40 |
+
print(f" 📋 Explicit RED indicators ({len(red_indicators)}):")
|
| 41 |
+
for indicator in red_indicators:
|
| 42 |
+
print(f" • {indicator}")
|
| 43 |
+
|
| 44 |
+
# Test updated red flag definition
|
| 45 |
+
red_definition = classification_manager.get_updated_red_flag_definition()
|
| 46 |
+
print(f"\n 🔴 Updated RED flag definition:")
|
| 47 |
+
print(f" {red_definition}")
|
| 48 |
+
|
| 49 |
+
# Demonstrate classification validation
|
| 50 |
+
print("\n3. Testing classification validation...")
|
| 51 |
+
|
| 52 |
+
# Valid classification
|
| 53 |
+
valid_result = classification_manager.create_classification_result(
|
| 54 |
+
classification="red",
|
| 55 |
+
confidence=0.85,
|
| 56 |
+
indicators=["doubt about meaning of suffering"],
|
| 57 |
+
reasoning="Patient questioning meaning of suffering - explicit red indicator",
|
| 58 |
+
red_flag_indicators=["doubt about meaning of suffering"]
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
print(f" ✅ Valid classification: {valid_result.classification} (valid: {valid_result.is_valid})")
|
| 62 |
+
|
| 63 |
+
# Invalid classification that gets corrected
|
| 64 |
+
invalid_result = classification_manager.create_classification_result(
|
| 65 |
+
classification="invalid",
|
| 66 |
+
confidence=2.0,
|
| 67 |
+
indicators=[],
|
| 68 |
+
reasoning=""
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
print(f" 🔧 Corrected classification: {invalid_result.classification} (valid: {invalid_result.is_valid})")
|
| 72 |
+
|
| 73 |
+
# Demonstrate enhanced display formatting
|
| 74 |
+
print("\n4. Testing enhanced display formatting...")
|
| 75 |
+
|
| 76 |
+
# Create test provider summary
|
| 77 |
+
test_summary = ProviderSummary(
|
| 78 |
+
patient_name="Demo Patient",
|
| 79 |
+
patient_phone="555-0123",
|
| 80 |
+
classification="RED",
|
| 81 |
+
confidence=0.85,
|
| 82 |
+
indicators=["loss of meaning", "spiritual distress", "questioning faith"],
|
| 83 |
+
reasoning="Patient expressing significant spiritual concerns and loss of meaning",
|
| 84 |
+
severity_level="HIGH",
|
| 85 |
+
urgency_level="IMMEDIATE",
|
| 86 |
+
situation_description="Patient experiencing existential crisis following medical diagnosis",
|
| 87 |
+
recommended_actions=[
|
| 88 |
+
"Contact patient within 2-4 hours",
|
| 89 |
+
"Assess immediate safety and support needs",
|
| 90 |
+
"Provide spiritual care resources"
|
| 91 |
+
],
|
| 92 |
+
medical_context={
|
| 93 |
+
"age": 45,
|
| 94 |
+
"gender": "individual",
|
| 95 |
+
"conditions": ["chronic illness", "recent diagnosis"]
|
| 96 |
+
}
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
# Format AI analysis section
|
| 100 |
+
ai_analysis_html = display_manager.format_ai_analysis_section(
|
| 101 |
+
classification="RED",
|
| 102 |
+
indicators=test_summary.indicators,
|
| 103 |
+
reasoning=test_summary.reasoning,
|
| 104 |
+
confidence=test_summary.confidence
|
| 105 |
+
)
|
| 106 |
+
|
| 107 |
+
print(" ✅ AI Analysis section formatted")
|
| 108 |
+
print(f" Length: {len(ai_analysis_html)} characters")
|
| 109 |
+
|
| 110 |
+
# Format patient message section
|
| 111 |
+
patient_message_html = display_manager.format_patient_message_section(
|
| 112 |
+
"I just don't see the point in anything anymore. My life feels meaningless since my diagnosis."
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
print(" ✅ Patient Message section formatted")
|
| 116 |
+
print(f" Length: {len(patient_message_html)} characters")
|
| 117 |
+
|
| 118 |
+
# Format provider summary section
|
| 119 |
+
provider_summary_html = display_manager.format_provider_summary_section(test_summary)
|
| 120 |
+
|
| 121 |
+
print(" ✅ Provider Summary section formatted")
|
| 122 |
+
print(f" Length: {len(provider_summary_html)} characters")
|
| 123 |
+
|
| 124 |
+
# Test coherent paragraph formatting
|
| 125 |
+
print("\n5. Testing coherent paragraph formatting...")
|
| 126 |
+
|
| 127 |
+
coherent_paragraph = summary_generator.format_coherent_paragraph(test_summary)
|
| 128 |
+
|
| 129 |
+
print(" ✅ Coherent paragraph generated")
|
| 130 |
+
print(f" Length: {len(coherent_paragraph)} characters")
|
| 131 |
+
print(f" Preview: {coherent_paragraph[:100]}...")
|
| 132 |
+
|
| 133 |
+
# Test combined results
|
| 134 |
+
print("\n6. Testing combined results formatting...")
|
| 135 |
+
|
| 136 |
+
combined_html = display_manager.format_combined_results(
|
| 137 |
+
ai_analysis={
|
| 138 |
+
'classification': 'RED',
|
| 139 |
+
'indicators': test_summary.indicators,
|
| 140 |
+
'reasoning': test_summary.reasoning,
|
| 141 |
+
'confidence': test_summary.confidence
|
| 142 |
+
},
|
| 143 |
+
patient_message="I just don't see the point in anything anymore. My life feels meaningless since my diagnosis.",
|
| 144 |
+
provider_summary=test_summary
|
| 145 |
+
)
|
| 146 |
+
|
| 147 |
+
print(" ✅ Combined results formatted")
|
| 148 |
+
print(f" Length: {len(combined_html)} characters")
|
| 149 |
+
|
| 150 |
+
# Test conversation logging integration
|
| 151 |
+
print("\n7. Testing conversation logging integration...")
|
| 152 |
+
|
| 153 |
+
logger = ConversationLogger(patient_name="Demo Patient")
|
| 154 |
+
|
| 155 |
+
# Log test conversation
|
| 156 |
+
assessment = SpiritualAssessment(
|
| 157 |
+
state=SpiritualState.RED,
|
| 158 |
+
confidence=0.85,
|
| 159 |
+
indicators=["loss of meaning", "spiritual distress"],
|
| 160 |
+
reasoning="Patient expressing significant spiritual concerns"
|
| 161 |
+
)
|
| 162 |
+
|
| 163 |
+
logger.log_exchange(
|
| 164 |
+
"I just don't see the point in anything anymore",
|
| 165 |
+
"I understand this is a very difficult time for you",
|
| 166 |
+
assessment
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
print(f" ✅ Conversation logged: {len(logger.entries)} entries")
|
| 170 |
+
|
| 171 |
+
# Test error handling
|
| 172 |
+
print("\n8. Testing error handling and recovery...")
|
| 173 |
+
|
| 174 |
+
# Create problematic summary
|
| 175 |
+
problematic_summary = ProviderSummary(
|
| 176 |
+
patient_name="[Patient Name]", # Placeholder
|
| 177 |
+
patient_phone="[Phone Number]", # Placeholder
|
| 178 |
+
classification="RED",
|
| 179 |
+
confidence=1.5, # Invalid confidence
|
| 180 |
+
reasoning="", # Empty reasoning
|
| 181 |
+
indicators=[] # No indicators
|
| 182 |
+
)
|
| 183 |
+
|
| 184 |
+
# Display manager should handle this gracefully
|
| 185 |
+
error_display = display_manager.format_provider_summary_section(problematic_summary)
|
| 186 |
+
|
| 187 |
+
print(" ✅ Error handling works - graceful degradation")
|
| 188 |
+
print(f" Error display length: {len(error_display)} characters")
|
| 189 |
+
|
| 190 |
+
# Summary
|
| 191 |
+
print("\n" + "=" * 60)
|
| 192 |
+
print("🎉 INTEGRATION DEMO COMPLETED SUCCESSFULLY!")
|
| 193 |
+
print("\n📋 Demonstrated Features:")
|
| 194 |
+
print(" ✅ Enhanced classification with explicit red indicators")
|
| 195 |
+
print(" ✅ Updated red flag definition and validation")
|
| 196 |
+
print(" ✅ Enhanced display formatting with visual separation")
|
| 197 |
+
print(" ✅ Coherent paragraph formatting for Medical Brain compatibility")
|
| 198 |
+
print(" ✅ Combined results with proper section separation")
|
| 199 |
+
print(" ✅ Conversation logging integration")
|
| 200 |
+
print(" ✅ Error handling and graceful degradation")
|
| 201 |
+
print("\n🚀 All UI Classification Improvements are working correctly!")
|
| 202 |
+
print(" Ready for production use in the medical assistant system.")
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
if __name__ == "__main__":
|
| 206 |
+
main()
|
|
@@ -0,0 +1,248 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Visual Separation Manager Demo
|
| 4 |
+
|
| 5 |
+
This demo shows the VisualSeparationManager functionality for creating
|
| 6 |
+
consistent visual styling and separation for different content types.
|
| 7 |
+
|
| 8 |
+
Requirements: 7.1, 7.2, 7.3, 7.4, 7.5
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import sys
|
| 12 |
+
import os
|
| 13 |
+
|
| 14 |
+
# Add project root to path
|
| 15 |
+
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 16 |
+
sys.path.insert(0, project_root)
|
| 17 |
+
|
| 18 |
+
from src.interface.visual_separation_manager import (
|
| 19 |
+
VisualSeparationManager,
|
| 20 |
+
ContentType,
|
| 21 |
+
VisualStyle
|
| 22 |
+
)
|
| 23 |
+
from src.interface.enhanced_display_integration import (
|
| 24 |
+
EnhancedDisplayIntegration,
|
| 25 |
+
create_enhanced_display_integration
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def demo_visual_separation_manager():
|
| 30 |
+
"""Demonstrate VisualSeparationManager functionality."""
|
| 31 |
+
print("🎨 Visual Separation Manager Demo")
|
| 32 |
+
print("=" * 50)
|
| 33 |
+
|
| 34 |
+
# Create visual separation manager
|
| 35 |
+
visual_manager = VisualSeparationManager()
|
| 36 |
+
|
| 37 |
+
print("\n1. Testing AI Analysis Styling:")
|
| 38 |
+
ai_styling = visual_manager.create_ai_analysis_styling()
|
| 39 |
+
print(f" Container style: {ai_styling['container'][:50]}...")
|
| 40 |
+
print(f" Header style: {ai_styling['header'][:50]}...")
|
| 41 |
+
print(f" Icon style: {ai_styling['icon'][:30]}...")
|
| 42 |
+
|
| 43 |
+
print("\n2. Testing Patient Message Styling:")
|
| 44 |
+
patient_styling = visual_manager.create_patient_message_styling()
|
| 45 |
+
print(f" Container style: {patient_styling['container'][:50]}...")
|
| 46 |
+
print(f" Message box style: {patient_styling['message_box'][:50]}...")
|
| 47 |
+
|
| 48 |
+
print("\n3. Testing Provider Summary Styling:")
|
| 49 |
+
provider_styling = visual_manager.create_provider_summary_styling()
|
| 50 |
+
print(f" Container style: {provider_styling['container'][:50]}...")
|
| 51 |
+
print(f" Info box style: {provider_styling['info_box'][:50]}...")
|
| 52 |
+
|
| 53 |
+
print("\n4. Testing Section Separators:")
|
| 54 |
+
separators = visual_manager.generate_section_separators()
|
| 55 |
+
for sep_type, sep_html in separators.items():
|
| 56 |
+
print(f" {sep_type}: {len(sep_html)} characters")
|
| 57 |
+
|
| 58 |
+
print("\n5. Testing Classification Styling:")
|
| 59 |
+
for classification in ["RED", "YELLOW", "GREEN"]:
|
| 60 |
+
styling = visual_manager.get_classification_styling(classification)
|
| 61 |
+
print(f" {classification}: {styling['text'][:30]}...")
|
| 62 |
+
|
| 63 |
+
print("\n6. Testing Urgency Styling:")
|
| 64 |
+
for urgency in ["IMMEDIATE", "URGENT", "STANDARD"]:
|
| 65 |
+
styling = visual_manager.get_urgency_styling(urgency)
|
| 66 |
+
print(f" {urgency}: {styling['badge'][:40]}...")
|
| 67 |
+
|
| 68 |
+
print("\n7. Testing Icon Styling:")
|
| 69 |
+
for content_type in [ContentType.AI_ANALYSIS, ContentType.PATIENT_MESSAGE, ContentType.PROVIDER_SUMMARY]:
|
| 70 |
+
icon_style = visual_manager.create_icon_styling(content_type)
|
| 71 |
+
print(f" {content_type.value}: {icon_style[:40]}...")
|
| 72 |
+
|
| 73 |
+
print("\n8. Testing Consistent Formatting:")
|
| 74 |
+
sections = [
|
| 75 |
+
{"type": "ai_analysis", "content": "AI analysis content example"},
|
| 76 |
+
{"type": "patient_message", "content": "Patient message content example"},
|
| 77 |
+
{"type": "provider_summary", "content": "Provider summary content example"}
|
| 78 |
+
]
|
| 79 |
+
|
| 80 |
+
formatted_result = visual_manager.apply_consistent_formatting(sections)
|
| 81 |
+
print(f" Formatted result length: {len(formatted_result)} characters")
|
| 82 |
+
print(f" Contains separators: {'---' in formatted_result}")
|
| 83 |
+
print(f" Contains all content: {all(section['content'] in formatted_result for section in sections)}")
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def demo_enhanced_display_integration():
|
| 87 |
+
"""Demonstrate Enhanced Display Integration with VisualSeparationManager."""
|
| 88 |
+
print("\n\n🔗 Enhanced Display Integration Demo")
|
| 89 |
+
print("=" * 50)
|
| 90 |
+
|
| 91 |
+
# Create integration instance
|
| 92 |
+
integration = create_enhanced_display_integration()
|
| 93 |
+
|
| 94 |
+
print("\n1. Testing Section Creation with Styling:")
|
| 95 |
+
|
| 96 |
+
# AI Analysis section
|
| 97 |
+
ai_section = integration.create_section_with_styling(
|
| 98 |
+
content="Patient shows signs of spiritual distress with loss of meaning indicators.",
|
| 99 |
+
section_type="ai_analysis",
|
| 100 |
+
title="AI Analysis - RED FLAG"
|
| 101 |
+
)
|
| 102 |
+
print(f" AI section length: {len(ai_section)} characters")
|
| 103 |
+
print(f" Contains AI icon: {'🤖' in ai_section}")
|
| 104 |
+
|
| 105 |
+
# Patient Message section
|
| 106 |
+
patient_section = integration.create_section_with_styling(
|
| 107 |
+
content="I don't know what to believe anymore. Everything feels meaningless.",
|
| 108 |
+
section_type="patient_message"
|
| 109 |
+
)
|
| 110 |
+
print(f" Patient section length: {len(patient_section)} characters")
|
| 111 |
+
print(f" Contains patient icon: {'💬' in patient_section}")
|
| 112 |
+
|
| 113 |
+
# Provider Summary section
|
| 114 |
+
provider_section = integration.create_section_with_styling(
|
| 115 |
+
content="Immediate spiritual care intervention recommended for existential crisis.",
|
| 116 |
+
section_type="provider_summary"
|
| 117 |
+
)
|
| 118 |
+
print(f" Provider section length: {len(provider_section)} characters")
|
| 119 |
+
print(f" Contains provider icon: {'📋' in provider_section}")
|
| 120 |
+
|
| 121 |
+
print("\n2. Testing Badge Creation:")
|
| 122 |
+
|
| 123 |
+
# Classification badges
|
| 124 |
+
red_badge = integration.get_classification_badge("RED")
|
| 125 |
+
yellow_badge = integration.get_classification_badge("YELLOW")
|
| 126 |
+
green_badge = integration.get_classification_badge("GREEN")
|
| 127 |
+
|
| 128 |
+
print(f" RED badge: {len(red_badge)} characters")
|
| 129 |
+
print(f" YELLOW badge: {len(yellow_badge)} characters")
|
| 130 |
+
print(f" GREEN badge: {len(green_badge)} characters")
|
| 131 |
+
|
| 132 |
+
# Urgency badges
|
| 133 |
+
immediate_badge = integration.get_urgency_badge("IMMEDIATE")
|
| 134 |
+
urgent_badge = integration.get_urgency_badge("URGENT")
|
| 135 |
+
standard_badge = integration.get_urgency_badge("STANDARD")
|
| 136 |
+
|
| 137 |
+
print(f" IMMEDIATE badge: {len(immediate_badge)} characters")
|
| 138 |
+
print(f" URGENT badge: {len(urgent_badge)} characters")
|
| 139 |
+
print(f" STANDARD badge: {len(standard_badge)} characters")
|
| 140 |
+
|
| 141 |
+
print("\n3. Testing Content Separators:")
|
| 142 |
+
|
| 143 |
+
separators = ["light", "medium", "heavy", "section_break"]
|
| 144 |
+
for sep_type in separators:
|
| 145 |
+
separator = integration.create_content_separator(sep_type)
|
| 146 |
+
print(f" {sep_type} separator: {len(separator)} characters")
|
| 147 |
+
|
| 148 |
+
print("\n4. Testing Multiple Sections Formatting:")
|
| 149 |
+
|
| 150 |
+
sections = [
|
| 151 |
+
{"type": "ai_analysis", "content": "AI analysis content"},
|
| 152 |
+
{"type": "patient_message", "content": "Patient message content"},
|
| 153 |
+
{"type": "provider_summary", "content": "Provider summary content"}
|
| 154 |
+
]
|
| 155 |
+
|
| 156 |
+
formatted_multiple = integration.format_multiple_sections(sections)
|
| 157 |
+
print(f" Multiple sections result: {len(formatted_multiple)} characters")
|
| 158 |
+
print(f" Contains all content: {all(section['content'] in formatted_multiple for section in sections)}")
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
def demo_visual_styling_consistency():
|
| 162 |
+
"""Demonstrate visual styling consistency across components."""
|
| 163 |
+
print("\n\n🎯 Visual Styling Consistency Demo")
|
| 164 |
+
print("=" * 50)
|
| 165 |
+
|
| 166 |
+
visual_manager = VisualSeparationManager()
|
| 167 |
+
|
| 168 |
+
print("\n1. Color Consistency Check:")
|
| 169 |
+
|
| 170 |
+
# Check classification colors
|
| 171 |
+
classification_colors = visual_manager.classification_colors
|
| 172 |
+
print(f" Classification colors: {classification_colors}")
|
| 173 |
+
|
| 174 |
+
# Check urgency colors
|
| 175 |
+
urgency_colors = visual_manager.urgency_colors
|
| 176 |
+
print(f" Urgency colors: {urgency_colors}")
|
| 177 |
+
|
| 178 |
+
print("\n2. Style Structure Consistency:")
|
| 179 |
+
|
| 180 |
+
# Check that all content types have consistent styling structure
|
| 181 |
+
content_types = [ContentType.AI_ANALYSIS, ContentType.PATIENT_MESSAGE, ContentType.PROVIDER_SUMMARY]
|
| 182 |
+
|
| 183 |
+
for content_type in content_types:
|
| 184 |
+
if content_type == ContentType.AI_ANALYSIS:
|
| 185 |
+
styling = visual_manager.create_ai_analysis_styling()
|
| 186 |
+
elif content_type == ContentType.PATIENT_MESSAGE:
|
| 187 |
+
styling = visual_manager.create_patient_message_styling()
|
| 188 |
+
elif content_type == ContentType.PROVIDER_SUMMARY:
|
| 189 |
+
styling = visual_manager.create_provider_summary_styling()
|
| 190 |
+
|
| 191 |
+
required_keys = ["container", "header", "icon"]
|
| 192 |
+
has_all_keys = all(key in styling for key in required_keys)
|
| 193 |
+
print(f" {content_type.value}: Has all required keys: {has_all_keys}")
|
| 194 |
+
|
| 195 |
+
# Check for consistent CSS properties
|
| 196 |
+
container_style = styling.get("container", "")
|
| 197 |
+
has_border = "border:" in container_style
|
| 198 |
+
has_padding = "padding:" in container_style
|
| 199 |
+
has_margin = "margin:" in container_style
|
| 200 |
+
has_background = "background-color:" in container_style
|
| 201 |
+
|
| 202 |
+
print(f" - Border: {has_border}, Padding: {has_padding}, Margin: {has_margin}, Background: {has_background}")
|
| 203 |
+
|
| 204 |
+
print("\n3. Separator Consistency:")
|
| 205 |
+
|
| 206 |
+
separators = visual_manager.generate_section_separators()
|
| 207 |
+
for sep_name, sep_html in separators.items():
|
| 208 |
+
has_div = "<div" in sep_html
|
| 209 |
+
has_style = "style=" in sep_html
|
| 210 |
+
print(f" {sep_name}: Has div: {has_div}, Has style: {has_style}")
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
def main():
|
| 214 |
+
"""Run all visual separation manager demos."""
|
| 215 |
+
try:
|
| 216 |
+
demo_visual_separation_manager()
|
| 217 |
+
demo_enhanced_display_integration()
|
| 218 |
+
demo_visual_styling_consistency()
|
| 219 |
+
|
| 220 |
+
print("\n\n✅ All Visual Separation Manager demos completed successfully!")
|
| 221 |
+
print("\nKey Features Demonstrated:")
|
| 222 |
+
print("- ✅ AI Analysis styling with classification colors")
|
| 223 |
+
print("- ✅ Patient Message styling with consistent formatting")
|
| 224 |
+
print("- ✅ Provider Summary styling with urgency indicators")
|
| 225 |
+
print("- ✅ Section separators for visual content division")
|
| 226 |
+
print("- ✅ Classification and urgency badges")
|
| 227 |
+
print("- ✅ Icon styling for different content types")
|
| 228 |
+
print("- ✅ Consistent formatting across multiple sections")
|
| 229 |
+
print("- ✅ Integration with enhanced display system")
|
| 230 |
+
|
| 231 |
+
print(f"\nRequirements Validated:")
|
| 232 |
+
print("- 7.1: ✅ AI analysis and provider summary visual styling")
|
| 233 |
+
print("- 7.2: ✅ Patient message and content section styling")
|
| 234 |
+
print("- 7.3: ✅ Icons and color coding for content types")
|
| 235 |
+
print("- 7.4: ✅ Visual separators and section breaks")
|
| 236 |
+
print("- 7.5: ✅ Consistent formatting and visual hierarchy")
|
| 237 |
+
|
| 238 |
+
except Exception as e:
|
| 239 |
+
print(f"\n❌ Demo failed with error: {e}")
|
| 240 |
+
import traceback
|
| 241 |
+
traceback.print_exc()
|
| 242 |
+
return 1
|
| 243 |
+
|
| 244 |
+
return 0
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
if __name__ == "__main__":
|
| 248 |
+
exit(main())
|
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"enabled": true,
|
| 3 |
+
"use_color_coding": true,
|
| 4 |
+
"use_icons": false,
|
| 5 |
+
"use_visual_separators": true,
|
| 6 |
+
"use_enhanced_styling": true,
|
| 7 |
+
"ai_analysis": {
|
| 8 |
+
"icon": "\ud83e\udd16",
|
| 9 |
+
"border_color": "#6c757d",
|
| 10 |
+
"background_color": "#f8f9fa",
|
| 11 |
+
"header_color": "#6c757d",
|
| 12 |
+
"border_width": "2px",
|
| 13 |
+
"border_radius": "8px",
|
| 14 |
+
"padding": "15px",
|
| 15 |
+
"margin": "10px 0"
|
| 16 |
+
},
|
| 17 |
+
"patient_message": {
|
| 18 |
+
"icon": "\ud83d\udcac",
|
| 19 |
+
"border_color": "#4a90e2",
|
| 20 |
+
"background_color": "#f0f7ff",
|
| 21 |
+
"header_color": "#4a90e2",
|
| 22 |
+
"border_width": "2px",
|
| 23 |
+
"border_radius": "8px",
|
| 24 |
+
"padding": "15px",
|
| 25 |
+
"margin": "10px 0"
|
| 26 |
+
},
|
| 27 |
+
"provider_summary": {
|
| 28 |
+
"icon": "\ud83d\udccb",
|
| 29 |
+
"border_color": "#dc3545",
|
| 30 |
+
"background_color": "#fff8f0",
|
| 31 |
+
"header_color": "#dc3545",
|
| 32 |
+
"border_width": "2px",
|
| 33 |
+
"border_radius": "8px",
|
| 34 |
+
"padding": "15px",
|
| 35 |
+
"margin": "10px 0"
|
| 36 |
+
},
|
| 37 |
+
"classification_colors": {
|
| 38 |
+
"red": "#ff4444",
|
| 39 |
+
"yellow": "#ffaa00",
|
| 40 |
+
"green": "#44aa44",
|
| 41 |
+
"unknown": "#666666"
|
| 42 |
+
},
|
| 43 |
+
"separators": {
|
| 44 |
+
"section_separator": "---",
|
| 45 |
+
"content_divider": "1px solid #ddd",
|
| 46 |
+
"major_break_symbol": "\u25cf \u25cf \u25cf",
|
| 47 |
+
"separator_color": "#e0e0e0",
|
| 48 |
+
"separator_width": "80%"
|
| 49 |
+
},
|
| 50 |
+
"font_family": "system-ui, -apple-system, sans-serif",
|
| 51 |
+
"base_font_size": "14px",
|
| 52 |
+
"header_font_size": "1.1em",
|
| 53 |
+
"small_font_size": "0.9em",
|
| 54 |
+
"max_content_width": "100%",
|
| 55 |
+
"responsive_breakpoint": "768px",
|
| 56 |
+
"enable_animations": true,
|
| 57 |
+
"transition_duration": "0.3s",
|
| 58 |
+
"high_contrast_mode": false,
|
| 59 |
+
"reduced_motion": false
|
| 60 |
+
}
|
|
@@ -0,0 +1,682 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Enhanced Display Configuration System for UI Classification Improvements.
|
| 4 |
+
|
| 5 |
+
Provides comprehensive configuration management for enhanced display features
|
| 6 |
+
including colors, icons, styles, and feature toggles.
|
| 7 |
+
|
| 8 |
+
Requirements: 7.1, 7.2, 7.3
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import json
|
| 12 |
+
import os
|
| 13 |
+
from dataclasses import dataclass, field, asdict
|
| 14 |
+
from typing import Dict, List, Optional, Any
|
| 15 |
+
from pathlib import Path
|
| 16 |
+
import logging
|
| 17 |
+
|
| 18 |
+
# Configure logging
|
| 19 |
+
logging.basicConfig(level=logging.INFO)
|
| 20 |
+
logger = logging.getLogger(__name__)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
@dataclass
|
| 24 |
+
class SectionStylingConfig:
|
| 25 |
+
"""Configuration for individual section styling."""
|
| 26 |
+
icon: str
|
| 27 |
+
border_color: str
|
| 28 |
+
background_color: str
|
| 29 |
+
header_color: str
|
| 30 |
+
border_width: str = "2px"
|
| 31 |
+
border_radius: str = "8px"
|
| 32 |
+
padding: str = "15px"
|
| 33 |
+
margin: str = "10px 0"
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
@dataclass
|
| 37 |
+
class ClassificationColorConfig:
|
| 38 |
+
"""Configuration for classification-based colors."""
|
| 39 |
+
red: str = "#ff4444"
|
| 40 |
+
yellow: str = "#ffaa00"
|
| 41 |
+
green: str = "#44aa44"
|
| 42 |
+
unknown: str = "#666666"
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
@dataclass
|
| 46 |
+
class VisualSeparatorConfig:
|
| 47 |
+
"""Configuration for visual separators."""
|
| 48 |
+
section_separator: str = "---"
|
| 49 |
+
content_divider: str = "1px solid #ddd"
|
| 50 |
+
major_break_symbol: str = "● ● ●"
|
| 51 |
+
separator_color: str = "#e0e0e0"
|
| 52 |
+
separator_width: str = "80%"
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
@dataclass
|
| 56 |
+
class EnhancedDisplayConfig:
|
| 57 |
+
"""
|
| 58 |
+
Comprehensive configuration for enhanced display formatting.
|
| 59 |
+
|
| 60 |
+
Manages all aspects of the enhanced UI including colors, icons, styles,
|
| 61 |
+
and feature toggles for the classification improvements system.
|
| 62 |
+
|
| 63 |
+
Requirements: 7.1, 7.2, 7.3
|
| 64 |
+
"""
|
| 65 |
+
|
| 66 |
+
# Feature toggles
|
| 67 |
+
enabled: bool = True
|
| 68 |
+
use_color_coding: bool = True
|
| 69 |
+
use_icons: bool = True
|
| 70 |
+
use_visual_separators: bool = True
|
| 71 |
+
use_enhanced_styling: bool = True
|
| 72 |
+
|
| 73 |
+
# Section styling configurations
|
| 74 |
+
ai_analysis: SectionStylingConfig = field(default_factory=lambda: SectionStylingConfig(
|
| 75 |
+
icon="🤖",
|
| 76 |
+
border_color="#6c757d",
|
| 77 |
+
background_color="#f8f9fa",
|
| 78 |
+
header_color="#6c757d"
|
| 79 |
+
))
|
| 80 |
+
|
| 81 |
+
patient_message: SectionStylingConfig = field(default_factory=lambda: SectionStylingConfig(
|
| 82 |
+
icon="💬",
|
| 83 |
+
border_color="#4a90e2",
|
| 84 |
+
background_color="#f0f7ff",
|
| 85 |
+
header_color="#4a90e2"
|
| 86 |
+
))
|
| 87 |
+
|
| 88 |
+
provider_summary: SectionStylingConfig = field(default_factory=lambda: SectionStylingConfig(
|
| 89 |
+
icon="📋",
|
| 90 |
+
border_color="#dc3545",
|
| 91 |
+
background_color="#fff8f0",
|
| 92 |
+
header_color="#dc3545"
|
| 93 |
+
))
|
| 94 |
+
|
| 95 |
+
# Classification colors
|
| 96 |
+
classification_colors: ClassificationColorConfig = field(default_factory=ClassificationColorConfig)
|
| 97 |
+
|
| 98 |
+
# Visual separators
|
| 99 |
+
separators: VisualSeparatorConfig = field(default_factory=VisualSeparatorConfig)
|
| 100 |
+
|
| 101 |
+
# Typography settings
|
| 102 |
+
font_family: str = "system-ui, -apple-system, sans-serif"
|
| 103 |
+
base_font_size: str = "14px"
|
| 104 |
+
header_font_size: str = "1.1em"
|
| 105 |
+
small_font_size: str = "0.9em"
|
| 106 |
+
|
| 107 |
+
# Layout settings
|
| 108 |
+
max_content_width: str = "100%"
|
| 109 |
+
responsive_breakpoint: str = "768px"
|
| 110 |
+
|
| 111 |
+
# Animation settings
|
| 112 |
+
enable_animations: bool = True
|
| 113 |
+
transition_duration: str = "0.3s"
|
| 114 |
+
|
| 115 |
+
# Accessibility settings
|
| 116 |
+
high_contrast_mode: bool = False
|
| 117 |
+
reduced_motion: bool = False
|
| 118 |
+
|
| 119 |
+
def __post_init__(self):
|
| 120 |
+
"""Post-initialization to apply high contrast mode if enabled."""
|
| 121 |
+
if self.high_contrast_mode:
|
| 122 |
+
self._apply_high_contrast_colors()
|
| 123 |
+
|
| 124 |
+
def _apply_high_contrast_colors(self):
|
| 125 |
+
"""Apply high contrast color scheme for accessibility."""
|
| 126 |
+
# Update classification colors for high contrast
|
| 127 |
+
self.classification_colors.red = "#cc0000"
|
| 128 |
+
self.classification_colors.yellow = "#cc8800"
|
| 129 |
+
self.classification_colors.green = "#006600"
|
| 130 |
+
|
| 131 |
+
# Update section colors for high contrast
|
| 132 |
+
self.ai_analysis.border_color = "#000000"
|
| 133 |
+
self.ai_analysis.background_color = "#ffffff"
|
| 134 |
+
self.ai_analysis.header_color = "#000000"
|
| 135 |
+
|
| 136 |
+
self.patient_message.border_color = "#0066cc"
|
| 137 |
+
self.patient_message.background_color = "#f0f8ff"
|
| 138 |
+
self.patient_message.header_color = "#0066cc"
|
| 139 |
+
|
| 140 |
+
self.provider_summary.border_color = "#cc0000"
|
| 141 |
+
self.provider_summary.background_color = "#fff0f0"
|
| 142 |
+
self.provider_summary.header_color = "#cc0000"
|
| 143 |
+
|
| 144 |
+
def get_classification_color(self, classification: str) -> str:
|
| 145 |
+
"""
|
| 146 |
+
Get color for a specific classification.
|
| 147 |
+
|
| 148 |
+
Args:
|
| 149 |
+
classification: Classification level (RED/YELLOW/GREEN)
|
| 150 |
+
|
| 151 |
+
Returns:
|
| 152 |
+
Hex color code for the classification
|
| 153 |
+
"""
|
| 154 |
+
classification_lower = classification.lower()
|
| 155 |
+
|
| 156 |
+
if classification_lower == "red":
|
| 157 |
+
return self.classification_colors.red
|
| 158 |
+
elif classification_lower == "yellow":
|
| 159 |
+
return self.classification_colors.yellow
|
| 160 |
+
elif classification_lower == "green":
|
| 161 |
+
return self.classification_colors.green
|
| 162 |
+
else:
|
| 163 |
+
return self.classification_colors.unknown
|
| 164 |
+
|
| 165 |
+
def get_section_config(self, section_type: str) -> SectionStylingConfig:
|
| 166 |
+
"""
|
| 167 |
+
Get styling configuration for a specific section type.
|
| 168 |
+
|
| 169 |
+
Args:
|
| 170 |
+
section_type: Type of section (ai_analysis/patient_message/provider_summary)
|
| 171 |
+
|
| 172 |
+
Returns:
|
| 173 |
+
SectionStylingConfig for the specified section
|
| 174 |
+
"""
|
| 175 |
+
section_configs = {
|
| 176 |
+
"ai_analysis": self.ai_analysis,
|
| 177 |
+
"patient_message": self.patient_message,
|
| 178 |
+
"provider_summary": self.provider_summary
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
return section_configs.get(section_type, self.ai_analysis)
|
| 182 |
+
|
| 183 |
+
def generate_css_variables(self) -> str:
|
| 184 |
+
"""
|
| 185 |
+
Generate CSS custom properties (variables) for the configuration.
|
| 186 |
+
|
| 187 |
+
Returns:
|
| 188 |
+
CSS string with custom properties
|
| 189 |
+
"""
|
| 190 |
+
css_vars = [
|
| 191 |
+
":root {",
|
| 192 |
+
f" --enhanced-font-family: {self.font_family};",
|
| 193 |
+
f" --enhanced-base-font-size: {self.base_font_size};",
|
| 194 |
+
f" --enhanced-header-font-size: {self.header_font_size};",
|
| 195 |
+
f" --enhanced-small-font-size: {self.small_font_size};",
|
| 196 |
+
f" --enhanced-max-width: {self.max_content_width};",
|
| 197 |
+
f" --enhanced-transition: {self.transition_duration};",
|
| 198 |
+
"",
|
| 199 |
+
" /* Classification Colors */",
|
| 200 |
+
f" --classification-red: {self.classification_colors.red};",
|
| 201 |
+
f" --classification-yellow: {self.classification_colors.yellow};",
|
| 202 |
+
f" --classification-green: {self.classification_colors.green};",
|
| 203 |
+
f" --classification-unknown: {self.classification_colors.unknown};",
|
| 204 |
+
"",
|
| 205 |
+
" /* AI Analysis Section */",
|
| 206 |
+
f" --ai-analysis-icon: '{self.ai_analysis.icon}';",
|
| 207 |
+
f" --ai-analysis-border: {self.ai_analysis.border_color};",
|
| 208 |
+
f" --ai-analysis-bg: {self.ai_analysis.background_color};",
|
| 209 |
+
f" --ai-analysis-header: {self.ai_analysis.header_color};",
|
| 210 |
+
"",
|
| 211 |
+
" /* Patient Message Section */",
|
| 212 |
+
f" --patient-message-icon: '{self.patient_message.icon}';",
|
| 213 |
+
f" --patient-message-border: {self.patient_message.border_color};",
|
| 214 |
+
f" --patient-message-bg: {self.patient_message.background_color};",
|
| 215 |
+
f" --patient-message-header: {self.patient_message.header_color};",
|
| 216 |
+
"",
|
| 217 |
+
" /* Provider Summary Section */",
|
| 218 |
+
f" --provider-summary-icon: '{self.provider_summary.icon}';",
|
| 219 |
+
f" --provider-summary-border: {self.provider_summary.border_color};",
|
| 220 |
+
f" --provider-summary-bg: {self.provider_summary.background_color};",
|
| 221 |
+
f" --provider-summary-header: {self.provider_summary.header_color};",
|
| 222 |
+
"",
|
| 223 |
+
" /* Separators */",
|
| 224 |
+
f" --separator-color: {self.separators.separator_color};",
|
| 225 |
+
f" --separator-width: {self.separators.separator_width};",
|
| 226 |
+
"}"
|
| 227 |
+
]
|
| 228 |
+
|
| 229 |
+
return "\n".join(css_vars)
|
| 230 |
+
|
| 231 |
+
def generate_base_css(self) -> str:
|
| 232 |
+
"""
|
| 233 |
+
Generate base CSS classes for enhanced display.
|
| 234 |
+
|
| 235 |
+
Returns:
|
| 236 |
+
CSS string with base classes
|
| 237 |
+
"""
|
| 238 |
+
css_classes = [
|
| 239 |
+
self.generate_css_variables(),
|
| 240 |
+
"",
|
| 241 |
+
"/* Enhanced Display Base Styles */",
|
| 242 |
+
".enhanced-section {",
|
| 243 |
+
f" font-family: var(--enhanced-font-family);",
|
| 244 |
+
f" font-size: var(--enhanced-base-font-size);",
|
| 245 |
+
f" border-radius: {self.ai_analysis.border_radius};",
|
| 246 |
+
f" padding: {self.ai_analysis.padding};",
|
| 247 |
+
f" margin: {self.ai_analysis.margin};",
|
| 248 |
+
f" border-width: {self.ai_analysis.border_width};",
|
| 249 |
+
" border-style: solid;",
|
| 250 |
+
"}",
|
| 251 |
+
"",
|
| 252 |
+
".enhanced-section-header {",
|
| 253 |
+
" display: flex;",
|
| 254 |
+
" align-items: center;",
|
| 255 |
+
" margin-bottom: 10px;",
|
| 256 |
+
f" font-size: var(--enhanced-header-font-size);",
|
| 257 |
+
" font-weight: bold;",
|
| 258 |
+
"}",
|
| 259 |
+
"",
|
| 260 |
+
".enhanced-section-icon {",
|
| 261 |
+
" font-size: 1.2em;",
|
| 262 |
+
" margin-right: 8px;",
|
| 263 |
+
"}",
|
| 264 |
+
"",
|
| 265 |
+
".enhanced-separator {",
|
| 266 |
+
" margin: 20px 0;",
|
| 267 |
+
" text-align: center;",
|
| 268 |
+
"}",
|
| 269 |
+
"",
|
| 270 |
+
".enhanced-separator hr {",
|
| 271 |
+
" border: none;",
|
| 272 |
+
f" border-top: 2px solid var(--separator-color);",
|
| 273 |
+
f" width: var(--separator-width);",
|
| 274 |
+
" margin: 0 auto;",
|
| 275 |
+
"}",
|
| 276 |
+
"",
|
| 277 |
+
"/* AI Analysis Styles */",
|
| 278 |
+
".ai-analysis-section {",
|
| 279 |
+
" border-color: var(--ai-analysis-border);",
|
| 280 |
+
" background-color: var(--ai-analysis-bg);",
|
| 281 |
+
"}",
|
| 282 |
+
"",
|
| 283 |
+
".ai-analysis-section .enhanced-section-header {",
|
| 284 |
+
" color: var(--ai-analysis-header);",
|
| 285 |
+
"}",
|
| 286 |
+
"",
|
| 287 |
+
"/* Patient Message Styles */",
|
| 288 |
+
".patient-message-section {",
|
| 289 |
+
" border-color: var(--patient-message-border);",
|
| 290 |
+
" background-color: var(--patient-message-bg);",
|
| 291 |
+
"}",
|
| 292 |
+
"",
|
| 293 |
+
".patient-message-section .enhanced-section-header {",
|
| 294 |
+
" color: var(--patient-message-header);",
|
| 295 |
+
"}",
|
| 296 |
+
"",
|
| 297 |
+
"/* Provider Summary Styles */",
|
| 298 |
+
".provider-summary-section {",
|
| 299 |
+
" border-color: var(--provider-summary-border);",
|
| 300 |
+
" background-color: var(--provider-summary-bg);",
|
| 301 |
+
"}",
|
| 302 |
+
"",
|
| 303 |
+
".provider-summary-section .enhanced-section-header {",
|
| 304 |
+
" color: var(--provider-summary-header);",
|
| 305 |
+
"}",
|
| 306 |
+
"",
|
| 307 |
+
"/* Classification-specific colors */",
|
| 308 |
+
".classification-red {",
|
| 309 |
+
" border-color: var(--classification-red) !important;",
|
| 310 |
+
"}",
|
| 311 |
+
"",
|
| 312 |
+
".classification-yellow {",
|
| 313 |
+
" border-color: var(--classification-yellow) !important;",
|
| 314 |
+
"}",
|
| 315 |
+
"",
|
| 316 |
+
".classification-green {",
|
| 317 |
+
" border-color: var(--classification-green) !important;",
|
| 318 |
+
"}",
|
| 319 |
+
"",
|
| 320 |
+
"/* Responsive design */",
|
| 321 |
+
f"@media (max-width: {self.responsive_breakpoint}) {{",
|
| 322 |
+
" .enhanced-section {",
|
| 323 |
+
" margin: 8px 0;",
|
| 324 |
+
" padding: 12px;",
|
| 325 |
+
" }",
|
| 326 |
+
" ",
|
| 327 |
+
" .enhanced-section-header {",
|
| 328 |
+
" font-size: 1em;",
|
| 329 |
+
" }",
|
| 330 |
+
"}",
|
| 331 |
+
]
|
| 332 |
+
|
| 333 |
+
# Add animation styles if enabled
|
| 334 |
+
if self.enable_animations and not self.reduced_motion:
|
| 335 |
+
css_classes.extend([
|
| 336 |
+
"",
|
| 337 |
+
"/* Animations */",
|
| 338 |
+
".enhanced-section {",
|
| 339 |
+
f" transition: all var(--enhanced-transition);",
|
| 340 |
+
"}",
|
| 341 |
+
"",
|
| 342 |
+
".enhanced-section:hover {",
|
| 343 |
+
" transform: translateY(-1px);",
|
| 344 |
+
" box-shadow: 0 2px 8px rgba(0,0,0,0.1);",
|
| 345 |
+
"}",
|
| 346 |
+
])
|
| 347 |
+
|
| 348 |
+
return "\n".join(css_classes)
|
| 349 |
+
|
| 350 |
+
def to_dict(self) -> Dict[str, Any]:
|
| 351 |
+
"""Convert configuration to dictionary."""
|
| 352 |
+
return asdict(self)
|
| 353 |
+
|
| 354 |
+
def to_json(self) -> str:
|
| 355 |
+
"""Convert configuration to JSON string."""
|
| 356 |
+
return json.dumps(self.to_dict(), indent=2)
|
| 357 |
+
|
| 358 |
+
|
| 359 |
+
class EnhancedDisplayConfigManager:
|
| 360 |
+
"""
|
| 361 |
+
Manager for enhanced display configuration with persistence and validation.
|
| 362 |
+
|
| 363 |
+
Handles loading, saving, and managing configuration for the enhanced display system.
|
| 364 |
+
"""
|
| 365 |
+
|
| 366 |
+
def __init__(self, config_file: Optional[str] = None):
|
| 367 |
+
"""
|
| 368 |
+
Initialize configuration manager.
|
| 369 |
+
|
| 370 |
+
Args:
|
| 371 |
+
config_file: Optional path to configuration file
|
| 372 |
+
"""
|
| 373 |
+
self.config_file = config_file or self._get_default_config_path()
|
| 374 |
+
self._config: Optional[EnhancedDisplayConfig] = None
|
| 375 |
+
logger.info(f"🔧 EnhancedDisplayConfigManager initialized with config: {self.config_file}")
|
| 376 |
+
|
| 377 |
+
def _get_default_config_path(self) -> str:
|
| 378 |
+
"""Get default configuration file path."""
|
| 379 |
+
# Create config directory if it doesn't exist
|
| 380 |
+
config_dir = Path("src/config/display")
|
| 381 |
+
config_dir.mkdir(parents=True, exist_ok=True)
|
| 382 |
+
|
| 383 |
+
return str(config_dir / "enhanced_display_config.json")
|
| 384 |
+
|
| 385 |
+
def load_config(self) -> EnhancedDisplayConfig:
|
| 386 |
+
"""
|
| 387 |
+
Load configuration from file or create default.
|
| 388 |
+
|
| 389 |
+
Returns:
|
| 390 |
+
EnhancedDisplayConfig instance
|
| 391 |
+
"""
|
| 392 |
+
if self._config is not None:
|
| 393 |
+
return self._config
|
| 394 |
+
|
| 395 |
+
try:
|
| 396 |
+
if os.path.exists(self.config_file):
|
| 397 |
+
with open(self.config_file, 'r', encoding='utf-8') as f:
|
| 398 |
+
config_data = json.load(f)
|
| 399 |
+
|
| 400 |
+
# Convert nested dictionaries back to dataclasses
|
| 401 |
+
config_data = self._convert_dict_to_config(config_data)
|
| 402 |
+
self._config = EnhancedDisplayConfig(**config_data)
|
| 403 |
+
|
| 404 |
+
logger.info(f"✅ Configuration loaded from {self.config_file}")
|
| 405 |
+
else:
|
| 406 |
+
logger.info("📝 Creating default configuration")
|
| 407 |
+
self._config = EnhancedDisplayConfig()
|
| 408 |
+
self.save_config()
|
| 409 |
+
|
| 410 |
+
except Exception as e:
|
| 411 |
+
logger.error(f"❌ Error loading configuration: {e}")
|
| 412 |
+
logger.info("📝 Using default configuration")
|
| 413 |
+
self._config = EnhancedDisplayConfig()
|
| 414 |
+
|
| 415 |
+
return self._config
|
| 416 |
+
|
| 417 |
+
def _convert_dict_to_config(self, config_data: Dict[str, Any]) -> Dict[str, Any]:
|
| 418 |
+
"""Convert dictionary data to proper dataclass format."""
|
| 419 |
+
# Convert nested section configs
|
| 420 |
+
for section_key in ['ai_analysis', 'patient_message', 'provider_summary']:
|
| 421 |
+
if section_key in config_data and isinstance(config_data[section_key], dict):
|
| 422 |
+
config_data[section_key] = SectionStylingConfig(**config_data[section_key])
|
| 423 |
+
|
| 424 |
+
# Convert classification colors
|
| 425 |
+
if 'classification_colors' in config_data and isinstance(config_data['classification_colors'], dict):
|
| 426 |
+
config_data['classification_colors'] = ClassificationColorConfig(**config_data['classification_colors'])
|
| 427 |
+
|
| 428 |
+
# Convert separators
|
| 429 |
+
if 'separators' in config_data and isinstance(config_data['separators'], dict):
|
| 430 |
+
config_data['separators'] = VisualSeparatorConfig(**config_data['separators'])
|
| 431 |
+
|
| 432 |
+
return config_data
|
| 433 |
+
|
| 434 |
+
def save_config(self) -> bool:
|
| 435 |
+
"""
|
| 436 |
+
Save current configuration to file.
|
| 437 |
+
|
| 438 |
+
Returns:
|
| 439 |
+
True if saved successfully
|
| 440 |
+
"""
|
| 441 |
+
if self._config is None:
|
| 442 |
+
logger.error("❌ No configuration to save")
|
| 443 |
+
return False
|
| 444 |
+
|
| 445 |
+
try:
|
| 446 |
+
# Ensure directory exists
|
| 447 |
+
os.makedirs(os.path.dirname(self.config_file), exist_ok=True)
|
| 448 |
+
|
| 449 |
+
with open(self.config_file, 'w', encoding='utf-8') as f:
|
| 450 |
+
f.write(self._config.to_json())
|
| 451 |
+
|
| 452 |
+
logger.info(f"✅ Configuration saved to {self.config_file}")
|
| 453 |
+
return True
|
| 454 |
+
|
| 455 |
+
except Exception as e:
|
| 456 |
+
logger.error(f"❌ Error saving configuration: {e}")
|
| 457 |
+
return False
|
| 458 |
+
|
| 459 |
+
def update_config(self, **kwargs) -> bool:
|
| 460 |
+
"""
|
| 461 |
+
Update configuration with new values.
|
| 462 |
+
|
| 463 |
+
Args:
|
| 464 |
+
**kwargs: Configuration values to update
|
| 465 |
+
|
| 466 |
+
Returns:
|
| 467 |
+
True if updated successfully
|
| 468 |
+
"""
|
| 469 |
+
try:
|
| 470 |
+
config = self.load_config()
|
| 471 |
+
|
| 472 |
+
# Update configuration attributes
|
| 473 |
+
for key, value in kwargs.items():
|
| 474 |
+
if hasattr(config, key):
|
| 475 |
+
# Handle nested dataclass updates
|
| 476 |
+
if key == 'classification_colors' and isinstance(value, dict):
|
| 477 |
+
# Convert dict to ClassificationColorConfig
|
| 478 |
+
from config.enhanced_display_config import ClassificationColorConfig
|
| 479 |
+
setattr(config, key, ClassificationColorConfig(**value))
|
| 480 |
+
elif key in ['ai_analysis', 'patient_message', 'provider_summary'] and isinstance(value, dict):
|
| 481 |
+
# Convert dict to SectionStylingConfig
|
| 482 |
+
from config.enhanced_display_config import SectionStylingConfig
|
| 483 |
+
setattr(config, key, SectionStylingConfig(**value))
|
| 484 |
+
elif key == 'separators' and isinstance(value, dict):
|
| 485 |
+
# Convert dict to VisualSeparatorConfig
|
| 486 |
+
from config.enhanced_display_config import VisualSeparatorConfig
|
| 487 |
+
setattr(config, key, VisualSeparatorConfig(**value))
|
| 488 |
+
else:
|
| 489 |
+
setattr(config, key, value)
|
| 490 |
+
logger.info(f"🔄 Updated config.{key} = {value}")
|
| 491 |
+
else:
|
| 492 |
+
logger.warning(f"⚠️ Unknown configuration key: {key}")
|
| 493 |
+
|
| 494 |
+
return self.save_config()
|
| 495 |
+
|
| 496 |
+
except Exception as e:
|
| 497 |
+
logger.error(f"❌ Error updating configuration: {e}")
|
| 498 |
+
return False
|
| 499 |
+
|
| 500 |
+
def reset_to_defaults(self) -> bool:
|
| 501 |
+
"""
|
| 502 |
+
Reset configuration to defaults.
|
| 503 |
+
|
| 504 |
+
Returns:
|
| 505 |
+
True if reset successfully
|
| 506 |
+
"""
|
| 507 |
+
try:
|
| 508 |
+
self._config = EnhancedDisplayConfig()
|
| 509 |
+
return self.save_config()
|
| 510 |
+
|
| 511 |
+
except Exception as e:
|
| 512 |
+
logger.error(f"❌ Error resetting configuration: {e}")
|
| 513 |
+
return False
|
| 514 |
+
|
| 515 |
+
def enable_feature(self, feature: str) -> bool:
|
| 516 |
+
"""
|
| 517 |
+
Enable a specific feature.
|
| 518 |
+
|
| 519 |
+
Args:
|
| 520 |
+
feature: Feature name to enable
|
| 521 |
+
|
| 522 |
+
Returns:
|
| 523 |
+
True if enabled successfully
|
| 524 |
+
"""
|
| 525 |
+
feature_map = {
|
| 526 |
+
'color_coding': 'use_color_coding',
|
| 527 |
+
'icons': 'use_icons',
|
| 528 |
+
'separators': 'use_visual_separators',
|
| 529 |
+
'styling': 'use_enhanced_styling',
|
| 530 |
+
'animations': 'enable_animations'
|
| 531 |
+
}
|
| 532 |
+
|
| 533 |
+
config_key = feature_map.get(feature, feature)
|
| 534 |
+
return self.update_config(**{config_key: True})
|
| 535 |
+
|
| 536 |
+
def disable_feature(self, feature: str) -> bool:
|
| 537 |
+
"""
|
| 538 |
+
Disable a specific feature.
|
| 539 |
+
|
| 540 |
+
Args:
|
| 541 |
+
feature: Feature name to disable
|
| 542 |
+
|
| 543 |
+
Returns:
|
| 544 |
+
True if disabled successfully
|
| 545 |
+
"""
|
| 546 |
+
feature_map = {
|
| 547 |
+
'color_coding': 'use_color_coding',
|
| 548 |
+
'icons': 'use_icons',
|
| 549 |
+
'separators': 'use_visual_separators',
|
| 550 |
+
'styling': 'use_enhanced_styling',
|
| 551 |
+
'animations': 'enable_animations'
|
| 552 |
+
}
|
| 553 |
+
|
| 554 |
+
config_key = feature_map.get(feature, feature)
|
| 555 |
+
return self.update_config(**{config_key: False})
|
| 556 |
+
|
| 557 |
+
def get_config(self) -> EnhancedDisplayConfig:
|
| 558 |
+
"""Get current configuration."""
|
| 559 |
+
return self.load_config()
|
| 560 |
+
|
| 561 |
+
def validate_config(self) -> List[str]:
|
| 562 |
+
"""
|
| 563 |
+
Validate current configuration.
|
| 564 |
+
|
| 565 |
+
Returns:
|
| 566 |
+
List of validation issues (empty if valid)
|
| 567 |
+
"""
|
| 568 |
+
issues = []
|
| 569 |
+
config = self.load_config()
|
| 570 |
+
|
| 571 |
+
# Validate color formats
|
| 572 |
+
try:
|
| 573 |
+
color_fields = [
|
| 574 |
+
config.classification_colors.red,
|
| 575 |
+
config.classification_colors.yellow,
|
| 576 |
+
config.classification_colors.green,
|
| 577 |
+
config.ai_analysis.border_color,
|
| 578 |
+
config.patient_message.border_color,
|
| 579 |
+
config.provider_summary.border_color
|
| 580 |
+
]
|
| 581 |
+
|
| 582 |
+
for color in color_fields:
|
| 583 |
+
if not self._is_valid_color(color):
|
| 584 |
+
issues.append(f"Invalid color format: {color}")
|
| 585 |
+
except AttributeError as e:
|
| 586 |
+
issues.append(f"Configuration structure error: {e}")
|
| 587 |
+
|
| 588 |
+
# Validate icons (should be single emoji or short string)
|
| 589 |
+
try:
|
| 590 |
+
icon_fields = [
|
| 591 |
+
config.ai_analysis.icon,
|
| 592 |
+
config.patient_message.icon,
|
| 593 |
+
config.provider_summary.icon
|
| 594 |
+
]
|
| 595 |
+
|
| 596 |
+
for icon in icon_fields:
|
| 597 |
+
if len(icon) > 5:
|
| 598 |
+
issues.append(f"Icon too long: {icon}")
|
| 599 |
+
except AttributeError as e:
|
| 600 |
+
issues.append(f"Configuration structure error: {e}")
|
| 601 |
+
|
| 602 |
+
return issues
|
| 603 |
+
|
| 604 |
+
def _is_valid_color(self, color: str) -> bool:
|
| 605 |
+
"""Check if color is in valid hex format."""
|
| 606 |
+
import re
|
| 607 |
+
hex_pattern = r'^#[0-9A-Fa-f]{6}$'
|
| 608 |
+
return bool(re.match(hex_pattern, color))
|
| 609 |
+
|
| 610 |
+
|
| 611 |
+
# Global configuration manager instance
|
| 612 |
+
_config_manager: Optional[EnhancedDisplayConfigManager] = None
|
| 613 |
+
|
| 614 |
+
|
| 615 |
+
def get_config_manager(config_file: Optional[str] = None) -> EnhancedDisplayConfigManager:
|
| 616 |
+
"""
|
| 617 |
+
Get global configuration manager instance.
|
| 618 |
+
|
| 619 |
+
Args:
|
| 620 |
+
config_file: Optional path to configuration file
|
| 621 |
+
|
| 622 |
+
Returns:
|
| 623 |
+
EnhancedDisplayConfigManager instance
|
| 624 |
+
"""
|
| 625 |
+
global _config_manager
|
| 626 |
+
|
| 627 |
+
if _config_manager is None:
|
| 628 |
+
_config_manager = EnhancedDisplayConfigManager(config_file)
|
| 629 |
+
|
| 630 |
+
return _config_manager
|
| 631 |
+
|
| 632 |
+
|
| 633 |
+
def get_enhanced_display_config() -> EnhancedDisplayConfig:
|
| 634 |
+
"""
|
| 635 |
+
Get current enhanced display configuration.
|
| 636 |
+
|
| 637 |
+
Returns:
|
| 638 |
+
EnhancedDisplayConfig instance
|
| 639 |
+
"""
|
| 640 |
+
return get_config_manager().get_config()
|
| 641 |
+
|
| 642 |
+
|
| 643 |
+
def update_display_config(**kwargs) -> bool:
|
| 644 |
+
"""
|
| 645 |
+
Update display configuration.
|
| 646 |
+
|
| 647 |
+
Args:
|
| 648 |
+
**kwargs: Configuration values to update
|
| 649 |
+
|
| 650 |
+
Returns:
|
| 651 |
+
True if updated successfully
|
| 652 |
+
"""
|
| 653 |
+
return get_config_manager().update_config(**kwargs)
|
| 654 |
+
|
| 655 |
+
|
| 656 |
+
# Factory functions for common configurations
|
| 657 |
+
def create_high_contrast_config() -> EnhancedDisplayConfig:
|
| 658 |
+
"""Create configuration optimized for high contrast accessibility."""
|
| 659 |
+
config = EnhancedDisplayConfig(high_contrast_mode=True)
|
| 660 |
+
# Ensure high contrast colors are applied
|
| 661 |
+
config._apply_high_contrast_colors()
|
| 662 |
+
return config
|
| 663 |
+
|
| 664 |
+
|
| 665 |
+
def create_minimal_config() -> EnhancedDisplayConfig:
|
| 666 |
+
"""Create minimal configuration with reduced visual elements."""
|
| 667 |
+
config = EnhancedDisplayConfig()
|
| 668 |
+
config.use_icons = False
|
| 669 |
+
config.use_visual_separators = False
|
| 670 |
+
config.enable_animations = False
|
| 671 |
+
return config
|
| 672 |
+
|
| 673 |
+
|
| 674 |
+
def create_mobile_optimized_config() -> EnhancedDisplayConfig:
|
| 675 |
+
"""Create configuration optimized for mobile devices."""
|
| 676 |
+
config = EnhancedDisplayConfig()
|
| 677 |
+
config.responsive_breakpoint = "480px"
|
| 678 |
+
config.ai_analysis.padding = "12px"
|
| 679 |
+
config.patient_message.padding = "12px"
|
| 680 |
+
config.provider_summary.padding = "12px"
|
| 681 |
+
config.enable_animations = False
|
| 682 |
+
return config
|
|
@@ -6,17 +6,41 @@ You are a specialist assistant generating a clear, concise handoff message for t
|
|
| 6 |
Generate a brief summary that reflects the patient's emotional or spiritual concerns, ensuring providers receive a clear handoff.
|
| 7 |
</goal>
|
| 8 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
<strict_formatting_rules>
|
| 10 |
-
1. Write a SINGLE, continuous paragraph. No line breaks.
|
| 11 |
2. DO NOT use emojis.
|
| 12 |
3. DO NOT use bullet points, list markers (1., -), or bold text (**).
|
| 13 |
4. DO NOT use headers or section titles.
|
|
|
|
| 14 |
</strict_formatting_rules>
|
| 15 |
|
| 16 |
<structure_and_content>
|
|
|
|
| 17 |
1. Starts immediately with the patient's Name and Phone Number.
|
| 18 |
2. Briefly summarizes the patient's situation and the specific indicators that triggered the Red classification.
|
| 19 |
3. Includes relevant context from any clarifying questions (Triage) if available.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
</structure_and_content>
|
| 21 |
|
| 22 |
<tone>
|
|
|
|
| 6 |
Generate a brief summary that reflects the patient's emotional or spiritual concerns, ensuring providers receive a clear handoff.
|
| 7 |
</goal>
|
| 8 |
|
| 9 |
+
<output_formats>
|
| 10 |
+
You can generate two types of summaries based on the request:
|
| 11 |
+
|
| 12 |
+
1. **SPIRITUAL_CARE_MESSAGE** (default): Brief handoff message for spiritual care team
|
| 13 |
+
2. **MEDICAL_BRAIN_COMPATIBLE**: Medical Brain compatible single-paragraph summary with patient quote
|
| 14 |
+
|
| 15 |
+
When format is MEDICAL_BRAIN_COMPATIBLE:
|
| 16 |
+
- Write a single coherent paragraph with demographic info, medical history, spiritual concerns, classification, consent status, and contact info
|
| 17 |
+
- Follow with a separate line containing a meaningful patient quote if available
|
| 18 |
+
- Use format: "Patient reported: '[actual patient words]'"
|
| 19 |
+
- Exclude technical system messages from patient quotes
|
| 20 |
+
</output_formats>
|
| 21 |
+
|
| 22 |
<strict_formatting_rules>
|
| 23 |
+
1. Write a SINGLE, continuous paragraph. No line breaks within the main paragraph.
|
| 24 |
2. DO NOT use emojis.
|
| 25 |
3. DO NOT use bullet points, list markers (1., -), or bold text (**).
|
| 26 |
4. DO NOT use headers or section titles.
|
| 27 |
+
5. For MEDICAL_BRAIN_COMPATIBLE: Include patient quote as separate line after main paragraph.
|
| 28 |
</strict_formatting_rules>
|
| 29 |
|
| 30 |
<structure_and_content>
|
| 31 |
+
For SPIRITUAL_CARE_MESSAGE:
|
| 32 |
1. Starts immediately with the patient's Name and Phone Number.
|
| 33 |
2. Briefly summarizes the patient's situation and the specific indicators that triggered the Red classification.
|
| 34 |
3. Includes relevant context from any clarifying questions (Triage) if available.
|
| 35 |
+
|
| 36 |
+
For MEDICAL_BRAIN_COMPATIBLE:
|
| 37 |
+
1. Start with: "[Name] is a [age]-year-old [gender]"
|
| 38 |
+
2. Add medical history: "with clinical history of [conditions]" or "with no significant medical history documented"
|
| 39 |
+
3. Add spiritual concerns: "The patient expressed [indicators], which may indicate [concern type]"
|
| 40 |
+
4. Add classification: "resulting in generation of a RED FLAG"
|
| 41 |
+
5. Add consent: "The patient has been identified for spiritual care team contact"
|
| 42 |
+
6. Add contact: "The preferred contact number is [phone]" or "No contact number is currently available"
|
| 43 |
+
7. If conversation context available, add patient quote on separate line
|
| 44 |
</structure_and_content>
|
| 45 |
|
| 46 |
<tone>
|
|
@@ -47,8 +47,18 @@ When classifying as YELLOW, the purpose of follow-up questions is to CLARIFY:
|
|
| 47 |
</yellow_follow_up_purpose>
|
| 48 |
</category>
|
| 49 |
|
| 50 |
-
<category name="RED" severity="
|
| 51 |
-
The message contains indicators
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
|
| 53 |
<crisis_language>
|
| 54 |
- ANY mention of suicide, suicidal thoughts, or suicidal ideation
|
|
@@ -59,14 +69,16 @@ The message contains indicators of severe distress or crisis, including:
|
|
| 59 |
- Active crisis or emergency language
|
| 60 |
</crisis_language>
|
| 61 |
|
| 62 |
-
<
|
| 63 |
- Anger (especially spiritual anger toward God/higher power)
|
| 64 |
- Excessive guilt that dominates daily functioning
|
| 65 |
-
-
|
| 66 |
-
-
|
| 67 |
- Expressing suffering that feels unbearable
|
| 68 |
- Spiritual pain (soul-level suffering beyond physical)
|
| 69 |
-
|
|
|
|
|
|
|
| 70 |
|
| 71 |
<examples>
|
| 72 |
"I can't take this anymore, I want it to end"
|
|
@@ -86,22 +98,24 @@ The message contains indicators of severe distress or crisis, including:
|
|
| 86 |
|
| 87 |
<critical_rules>
|
| 88 |
1. ANY mention of suicide, self-harm, death wishes, or wanting to die is ALWAYS classified as RED, regardless of other content
|
| 89 |
-
2.
|
| 90 |
-
3.
|
| 91 |
-
4.
|
| 92 |
-
5.
|
| 93 |
-
6.
|
| 94 |
-
7.
|
| 95 |
-
8.
|
| 96 |
-
9. YELLOW is about AMBIGUITY
|
| 97 |
-
10.
|
| 98 |
-
11. Simple positive statements
|
| 99 |
-
- "I'm okay", "things are fine", "almost everything is normal" → GREEN
|
| 100 |
-
-
|
|
|
|
|
|
|
| 101 |
12. Vague mentions of "some stress" or "a little worried" without context → YELLOW (need to clarify the CAUSE)
|
| 102 |
-
13.
|
| 103 |
-
14.
|
| 104 |
-
15.
|
| 105 |
</critical_rules>
|
| 106 |
|
| 107 |
<analysis_process>
|
|
|
|
| 47 |
</yellow_follow_up_purpose>
|
| 48 |
</category>
|
| 49 |
|
| 50 |
+
<category name="RED" severity="active_spiritual_distress">
|
| 51 |
+
The message contains indicators where the patient's situation appears to be CAUSED BY or is ACTIVELY CAUSING emotional or spiritual distress. This includes both severe crisis situations AND active spiritual distress that may not be crisis-level.
|
| 52 |
+
|
| 53 |
+
<explicit_red_indicators>
|
| 54 |
+
These indicators should ALWAYS result in RED classification:
|
| 55 |
+
- Complex grief
|
| 56 |
+
- Loss of a loved one (when patient expresses emotional distress about it)
|
| 57 |
+
- Doubt about meaning of life
|
| 58 |
+
- Doubt about meaning of suffering
|
| 59 |
+
- Doubt about personal dignity
|
| 60 |
+
- Loss of meaning and purpose
|
| 61 |
+
</explicit_red_indicators>
|
| 62 |
|
| 63 |
<crisis_language>
|
| 64 |
- ANY mention of suicide, suicidal thoughts, or suicidal ideation
|
|
|
|
| 69 |
- Active crisis or emergency language
|
| 70 |
</crisis_language>
|
| 71 |
|
| 72 |
+
<active_spiritual_distress>
|
| 73 |
- Anger (especially spiritual anger toward God/higher power)
|
| 74 |
- Excessive guilt that dominates daily functioning
|
| 75 |
+
- Hopelessness about spiritual matters
|
| 76 |
+
- Loss of faith or spiritual connection causing distress
|
| 77 |
- Expressing suffering that feels unbearable
|
| 78 |
- Spiritual pain (soul-level suffering beyond physical)
|
| 79 |
+
- Questioning life's meaning in context of distress
|
| 80 |
+
- Feeling abandoned by God/higher power
|
| 81 |
+
</active_spiritual_distress>
|
| 82 |
|
| 83 |
<examples>
|
| 84 |
"I can't take this anymore, I want it to end"
|
|
|
|
| 98 |
|
| 99 |
<critical_rules>
|
| 100 |
1. ANY mention of suicide, self-harm, death wishes, or wanting to die is ALWAYS classified as RED, regardless of other content
|
| 101 |
+
2. Explicit red indicators (complex grief, loss of loved one with distress, doubt about meaning of life/suffering/dignity) are ALWAYS RED
|
| 102 |
+
3. Simple positive statements like "I am fine" without distress context are ALWAYS GREEN
|
| 103 |
+
4. When uncertain between GREEN and YELLOW, choose GREEN for clearly positive/neutral statements
|
| 104 |
+
5. When uncertain between YELLOW and RED, carefully evaluate for explicit red indicators or active crisis language
|
| 105 |
+
6. "Feeling sad" alone without spiritual context should be YELLOW, not RED
|
| 106 |
+
7. Loss of meaning and purpose should ALWAYS be RED
|
| 107 |
+
8. Physical pain or medical symptoms alone are GREEN unless accompanied by emotional/spiritual distress language
|
| 108 |
+
9. YELLOW is about AMBIGUITY and need for clarification, not severity
|
| 109 |
+
10. Do NOT invent indicators that are not present in the message - only report what is actually stated
|
| 110 |
+
11. Simple positive statements WITHOUT any distress context:
|
| 111 |
+
- "I'm okay", "I am fine", "things are fine", "almost everything is normal" → GREEN
|
| 112 |
+
- These should be GREEN unless there are EXPLICIT distress indicators in the SAME message
|
| 113 |
+
- Do NOT assume defensive responses without clear evidence
|
| 114 |
+
- Previous conversation history should NOT change GREEN classification of clear positive statements
|
| 115 |
12. Vague mentions of "some stress" or "a little worried" without context → YELLOW (need to clarify the CAUSE)
|
| 116 |
+
13. Consider only the CURRENT message for classification, not conversation history assumptions
|
| 117 |
+
14. Loss of loved one, having no help, or other potentially distressing circumstances WITHOUT expressed emotional distress → YELLOW (need to explore if causing distress)
|
| 118 |
+
15. Patient's situation must be ACTIVELY CAUSING distress to be RED, not just potentially distressing circumstances
|
| 119 |
</critical_rules>
|
| 120 |
|
| 121 |
<analysis_process>
|
|
@@ -14,6 +14,8 @@ from typing import Dict, List, Any, Optional, Tuple
|
|
| 14 |
from dataclasses import dataclass, asdict, field
|
| 15 |
|
| 16 |
from src.core.conversation_logger import ConversationLogger, ConversationEntry
|
|
|
|
|
|
|
| 17 |
|
| 18 |
|
| 19 |
@dataclass
|
|
@@ -27,8 +29,8 @@ class VerificationFeedback:
|
|
| 27 |
|
| 28 |
|
| 29 |
@dataclass
|
| 30 |
-
class
|
| 31 |
-
"""
|
| 32 |
exchange_id: str
|
| 33 |
exchange_number: int
|
| 34 |
timestamp: datetime
|
|
@@ -38,6 +40,14 @@ class VerificationRecord:
|
|
| 38 |
original_confidence: float
|
| 39 |
original_indicators: List[str]
|
| 40 |
original_reasoning: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
is_correct: Optional[bool] = None
|
| 42 |
correct_classification: Optional[str] = None
|
| 43 |
correction_reason: Optional[str] = None
|
|
@@ -45,8 +55,8 @@ class VerificationRecord:
|
|
| 45 |
verification_timestamp: Optional[datetime] = None
|
| 46 |
|
| 47 |
@classmethod
|
| 48 |
-
def from_conversation_entry(cls, entry: ConversationEntry, exchange_number: int) -> '
|
| 49 |
-
"""Create
|
| 50 |
return cls(
|
| 51 |
exchange_id=f"{entry.session_id}_{entry.message_index}",
|
| 52 |
exchange_number=exchange_number,
|
|
@@ -67,6 +77,20 @@ class VerificationRecord:
|
|
| 67 |
self.verifier_notes = feedback.notes
|
| 68 |
self.verification_timestamp = datetime.now()
|
| 69 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
|
| 71 |
@dataclass
|
| 72 |
class VerificationProgress:
|
|
@@ -89,8 +113,8 @@ class VerificationProgress:
|
|
| 89 |
|
| 90 |
|
| 91 |
@dataclass
|
| 92 |
-
class
|
| 93 |
-
"""
|
| 94 |
session_id: str
|
| 95 |
conversation_session_id: str # Links to ConversationLogger session
|
| 96 |
patient_name: str
|
|
@@ -99,8 +123,12 @@ class VerificationSession:
|
|
| 99 |
end_time: Optional[datetime] = None
|
| 100 |
total_exchanges: int = 0
|
| 101 |
verified_exchanges: int = 0
|
| 102 |
-
verification_records: List[
|
| 103 |
is_complete: bool = False
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
|
| 105 |
def get_progress(self) -> VerificationProgress:
|
| 106 |
"""Get current verification progress."""
|
|
@@ -138,7 +166,7 @@ class VerificationSession:
|
|
| 138 |
common_errors=common_errors
|
| 139 |
)
|
| 140 |
|
| 141 |
-
def add_verification_record(self, record:
|
| 142 |
"""Add verification record to session."""
|
| 143 |
self.verification_records.append(record)
|
| 144 |
|
|
@@ -157,54 +185,89 @@ class VerificationSession:
|
|
| 157 |
return True
|
| 158 |
return False
|
| 159 |
|
| 160 |
-
def get_unverified_records(self) -> List[
|
| 161 |
"""Get list of unverified records."""
|
| 162 |
return [r for r in self.verification_records if r.is_correct is None]
|
| 163 |
|
| 164 |
-
def get_next_unverified_record(self) -> Optional[
|
| 165 |
"""Get next unverified record."""
|
| 166 |
unverified = self.get_unverified_records()
|
| 167 |
return unverified[0] if unverified else None
|
| 168 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
|
| 170 |
-
class
|
| 171 |
-
"""
|
| 172 |
|
| 173 |
def __init__(self, storage_dir: str = "verification_sessions"):
|
| 174 |
-
"""Initialize verification manager."""
|
| 175 |
from src.core.verification_store import JSONVerificationStore
|
| 176 |
self.store = JSONVerificationStore(storage_dir)
|
|
|
|
| 177 |
|
| 178 |
def create_verification_session(
|
| 179 |
self,
|
| 180 |
conversation_logger: ConversationLogger,
|
| 181 |
-
verifier_name: str = "Medical Professional"
|
| 182 |
-
|
|
|
|
| 183 |
"""
|
| 184 |
-
Create new verification session from conversation logger.
|
| 185 |
|
| 186 |
Args:
|
| 187 |
conversation_logger: Source conversation to verify
|
| 188 |
verifier_name: Name of person doing verification
|
|
|
|
| 189 |
|
| 190 |
Returns:
|
| 191 |
-
New
|
| 192 |
"""
|
| 193 |
session_id = f"verification_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{str(uuid.uuid4())[:8]}"
|
| 194 |
|
| 195 |
-
# Create verification session
|
| 196 |
-
session =
|
| 197 |
session_id=session_id,
|
| 198 |
conversation_session_id=conversation_logger.session_id,
|
| 199 |
patient_name=conversation_logger.patient_name,
|
| 200 |
verifier_name=verifier_name,
|
| 201 |
start_time=datetime.now(),
|
| 202 |
-
total_exchanges=len(conversation_logger.entries)
|
|
|
|
|
|
|
| 203 |
)
|
| 204 |
|
| 205 |
-
# Convert conversation entries to verification records
|
| 206 |
for i, entry in enumerate(conversation_logger.entries, 1):
|
| 207 |
-
record =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
session.add_verification_record(record)
|
| 209 |
|
| 210 |
# Save initial session
|
|
@@ -212,6 +275,55 @@ class ConversationVerificationManager:
|
|
| 212 |
|
| 213 |
return session
|
| 214 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 215 |
def get_verification_progress(self, session_id: str) -> Optional[VerificationProgress]:
|
| 216 |
"""Get verification progress for session."""
|
| 217 |
session = self.store.load_session(session_id)
|
|
@@ -262,7 +374,8 @@ class ConversationVerificationManager:
|
|
| 262 |
|
| 263 |
progress = session.get_progress()
|
| 264 |
|
| 265 |
-
|
|
|
|
| 266 |
"session_id": session.session_id,
|
| 267 |
"patient_name": session.patient_name,
|
| 268 |
"verifier_name": session.verifier_name,
|
|
@@ -271,15 +384,21 @@ class ConversationVerificationManager:
|
|
| 271 |
"is_complete": session.is_complete,
|
| 272 |
"progress": asdict(progress),
|
| 273 |
"total_exchanges": session.total_exchanges,
|
| 274 |
-
"verified_exchanges": session.verified_exchanges
|
|
|
|
|
|
|
|
|
|
|
|
|
| 275 |
}
|
|
|
|
|
|
|
| 276 |
|
| 277 |
-
def load_session(self, session_id: str) -> Optional[
|
| 278 |
-
"""Load verification session by ID."""
|
| 279 |
return self.store.load_session(session_id)
|
| 280 |
|
| 281 |
-
def save_session(self, session:
|
| 282 |
-
"""Save verification session."""
|
| 283 |
self.store.save_session(session)
|
| 284 |
|
| 285 |
def list_sessions(self) -> List[Dict[str, Any]]:
|
|
@@ -290,3 +409,55 @@ class ConversationVerificationManager:
|
|
| 290 |
"""Get incomplete verification sessions."""
|
| 291 |
return self.store.get_incomplete_sessions()
|
| 292 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
from dataclasses import dataclass, asdict, field
|
| 15 |
|
| 16 |
from src.core.conversation_logger import ConversationLogger, ConversationEntry
|
| 17 |
+
from src.interface.enhanced_results_display_manager import EnhancedResultsDisplayManager, EnhancedDisplayConfig
|
| 18 |
+
from src.core.provider_summary_generator import ProviderSummary
|
| 19 |
|
| 20 |
|
| 21 |
@dataclass
|
|
|
|
| 29 |
|
| 30 |
|
| 31 |
@dataclass
|
| 32 |
+
class EnhancedVerificationRecord:
|
| 33 |
+
"""Enhanced verification record with new format support."""
|
| 34 |
exchange_id: str
|
| 35 |
exchange_number: int
|
| 36 |
timestamp: datetime
|
|
|
|
| 40 |
original_confidence: float
|
| 41 |
original_indicators: List[str]
|
| 42 |
original_reasoning: str
|
| 43 |
+
|
| 44 |
+
# Enhanced fields for new formats
|
| 45 |
+
enhanced_display_format: Optional[str] = None # HTML formatted display
|
| 46 |
+
provider_summary: Optional[Dict[str, Any]] = None # Provider summary data
|
| 47 |
+
coherent_summary_paragraph: Optional[str] = None # Coherent paragraph format
|
| 48 |
+
visual_sections: Optional[List[Dict[str, str]]] = None # Visual section data
|
| 49 |
+
|
| 50 |
+
# Verification fields
|
| 51 |
is_correct: Optional[bool] = None
|
| 52 |
correct_classification: Optional[str] = None
|
| 53 |
correction_reason: Optional[str] = None
|
|
|
|
| 55 |
verification_timestamp: Optional[datetime] = None
|
| 56 |
|
| 57 |
@classmethod
|
| 58 |
+
def from_conversation_entry(cls, entry: ConversationEntry, exchange_number: int) -> 'EnhancedVerificationRecord':
|
| 59 |
+
"""Create EnhancedVerificationRecord from ConversationEntry."""
|
| 60 |
return cls(
|
| 61 |
exchange_id=f"{entry.session_id}_{entry.message_index}",
|
| 62 |
exchange_number=exchange_number,
|
|
|
|
| 77 |
self.verifier_notes = feedback.notes
|
| 78 |
self.verification_timestamp = datetime.now()
|
| 79 |
|
| 80 |
+
def set_enhanced_formats(
|
| 81 |
+
self,
|
| 82 |
+
enhanced_display: Optional[str] = None,
|
| 83 |
+
provider_summary: Optional[ProviderSummary] = None,
|
| 84 |
+
coherent_paragraph: Optional[str] = None,
|
| 85 |
+
visual_sections: Optional[List[Dict[str, str]]] = None
|
| 86 |
+
) -> None:
|
| 87 |
+
"""Set enhanced format data for this record."""
|
| 88 |
+
self.enhanced_display_format = enhanced_display
|
| 89 |
+
if provider_summary:
|
| 90 |
+
self.provider_summary = provider_summary.to_dict()
|
| 91 |
+
self.coherent_summary_paragraph = coherent_paragraph
|
| 92 |
+
self.visual_sections = visual_sections or []
|
| 93 |
+
|
| 94 |
|
| 95 |
@dataclass
|
| 96 |
class VerificationProgress:
|
|
|
|
| 113 |
|
| 114 |
|
| 115 |
@dataclass
|
| 116 |
+
class EnhancedVerificationSession:
|
| 117 |
+
"""Enhanced verification session with new format support."""
|
| 118 |
session_id: str
|
| 119 |
conversation_session_id: str # Links to ConversationLogger session
|
| 120 |
patient_name: str
|
|
|
|
| 123 |
end_time: Optional[datetime] = None
|
| 124 |
total_exchanges: int = 0
|
| 125 |
verified_exchanges: int = 0
|
| 126 |
+
verification_records: List[EnhancedVerificationRecord] = field(default_factory=list)
|
| 127 |
is_complete: bool = False
|
| 128 |
+
|
| 129 |
+
# Enhanced format support
|
| 130 |
+
display_manager: Optional[EnhancedResultsDisplayManager] = None
|
| 131 |
+
enhanced_format_enabled: bool = True
|
| 132 |
|
| 133 |
def get_progress(self) -> VerificationProgress:
|
| 134 |
"""Get current verification progress."""
|
|
|
|
| 166 |
common_errors=common_errors
|
| 167 |
)
|
| 168 |
|
| 169 |
+
def add_verification_record(self, record: EnhancedVerificationRecord) -> None:
|
| 170 |
"""Add verification record to session."""
|
| 171 |
self.verification_records.append(record)
|
| 172 |
|
|
|
|
| 185 |
return True
|
| 186 |
return False
|
| 187 |
|
| 188 |
+
def get_unverified_records(self) -> List[EnhancedVerificationRecord]:
|
| 189 |
"""Get list of unverified records."""
|
| 190 |
return [r for r in self.verification_records if r.is_correct is None]
|
| 191 |
|
| 192 |
+
def get_next_unverified_record(self) -> Optional[EnhancedVerificationRecord]:
|
| 193 |
"""Get next unverified record."""
|
| 194 |
unverified = self.get_unverified_records()
|
| 195 |
return unverified[0] if unverified else None
|
| 196 |
|
| 197 |
+
def generate_enhanced_display_for_record(self, record: EnhancedVerificationRecord) -> str:
|
| 198 |
+
"""Generate enhanced display format for a verification record."""
|
| 199 |
+
if not self.display_manager:
|
| 200 |
+
self.display_manager = EnhancedResultsDisplayManager()
|
| 201 |
+
|
| 202 |
+
# Create AI analysis data
|
| 203 |
+
ai_analysis = {
|
| 204 |
+
'classification': record.original_classification,
|
| 205 |
+
'indicators': record.original_indicators,
|
| 206 |
+
'reasoning': record.original_reasoning,
|
| 207 |
+
'confidence': record.original_confidence
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
# Generate enhanced display
|
| 211 |
+
enhanced_display = self.display_manager.format_combined_results(
|
| 212 |
+
ai_analysis=ai_analysis,
|
| 213 |
+
patient_message=record.user_message,
|
| 214 |
+
provider_summary=None # Would need to be generated separately
|
| 215 |
+
)
|
| 216 |
+
|
| 217 |
+
# Store enhanced display in record
|
| 218 |
+
record.enhanced_display_format = enhanced_display
|
| 219 |
+
|
| 220 |
+
return enhanced_display
|
| 221 |
+
|
| 222 |
|
| 223 |
+
class EnhancedConversationVerificationManager:
|
| 224 |
+
"""Enhanced manager for conversation verification sessions with new format support."""
|
| 225 |
|
| 226 |
def __init__(self, storage_dir: str = "verification_sessions"):
|
| 227 |
+
"""Initialize enhanced verification manager."""
|
| 228 |
from src.core.verification_store import JSONVerificationStore
|
| 229 |
self.store = JSONVerificationStore(storage_dir)
|
| 230 |
+
self.display_manager = EnhancedResultsDisplayManager()
|
| 231 |
|
| 232 |
def create_verification_session(
|
| 233 |
self,
|
| 234 |
conversation_logger: ConversationLogger,
|
| 235 |
+
verifier_name: str = "Medical Professional",
|
| 236 |
+
enable_enhanced_formats: bool = True
|
| 237 |
+
) -> EnhancedVerificationSession:
|
| 238 |
"""
|
| 239 |
+
Create new enhanced verification session from conversation logger.
|
| 240 |
|
| 241 |
Args:
|
| 242 |
conversation_logger: Source conversation to verify
|
| 243 |
verifier_name: Name of person doing verification
|
| 244 |
+
enable_enhanced_formats: Whether to enable enhanced display formats
|
| 245 |
|
| 246 |
Returns:
|
| 247 |
+
New EnhancedVerificationSession ready for verification
|
| 248 |
"""
|
| 249 |
session_id = f"verification_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{str(uuid.uuid4())[:8]}"
|
| 250 |
|
| 251 |
+
# Create enhanced verification session
|
| 252 |
+
session = EnhancedVerificationSession(
|
| 253 |
session_id=session_id,
|
| 254 |
conversation_session_id=conversation_logger.session_id,
|
| 255 |
patient_name=conversation_logger.patient_name,
|
| 256 |
verifier_name=verifier_name,
|
| 257 |
start_time=datetime.now(),
|
| 258 |
+
total_exchanges=len(conversation_logger.entries),
|
| 259 |
+
display_manager=self.display_manager if enable_enhanced_formats else None,
|
| 260 |
+
enhanced_format_enabled=enable_enhanced_formats
|
| 261 |
)
|
| 262 |
|
| 263 |
+
# Convert conversation entries to enhanced verification records
|
| 264 |
for i, entry in enumerate(conversation_logger.entries, 1):
|
| 265 |
+
record = EnhancedVerificationRecord.from_conversation_entry(entry, i)
|
| 266 |
+
|
| 267 |
+
# Generate enhanced formats if enabled
|
| 268 |
+
if enable_enhanced_formats:
|
| 269 |
+
self._generate_enhanced_formats_for_record(record, entry)
|
| 270 |
+
|
| 271 |
session.add_verification_record(record)
|
| 272 |
|
| 273 |
# Save initial session
|
|
|
|
| 275 |
|
| 276 |
return session
|
| 277 |
|
| 278 |
+
def _generate_enhanced_formats_for_record(
|
| 279 |
+
self,
|
| 280 |
+
record: EnhancedVerificationRecord,
|
| 281 |
+
entry: ConversationEntry
|
| 282 |
+
) -> None:
|
| 283 |
+
"""Generate enhanced formats for a verification record."""
|
| 284 |
+
try:
|
| 285 |
+
# Generate enhanced display format
|
| 286 |
+
ai_analysis = {
|
| 287 |
+
'classification': entry.spiritual_classification,
|
| 288 |
+
'indicators': entry.classification_indicators,
|
| 289 |
+
'reasoning': entry.classification_reasoning,
|
| 290 |
+
'confidence': entry.classification_confidence
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
enhanced_display = self.display_manager.format_combined_results(
|
| 294 |
+
ai_analysis=ai_analysis,
|
| 295 |
+
patient_message=entry.user_message,
|
| 296 |
+
provider_summary=None # Would need provider summary data
|
| 297 |
+
)
|
| 298 |
+
|
| 299 |
+
# Generate visual sections data
|
| 300 |
+
visual_sections = [
|
| 301 |
+
{
|
| 302 |
+
'type': 'ai_analysis',
|
| 303 |
+
'classification': entry.spiritual_classification,
|
| 304 |
+
'confidence': str(entry.classification_confidence),
|
| 305 |
+
'indicators': '; '.join(entry.classification_indicators),
|
| 306 |
+
'reasoning': entry.classification_reasoning
|
| 307 |
+
},
|
| 308 |
+
{
|
| 309 |
+
'type': 'patient_message',
|
| 310 |
+
'content': entry.user_message
|
| 311 |
+
},
|
| 312 |
+
{
|
| 313 |
+
'type': 'assistant_response',
|
| 314 |
+
'content': entry.assistant_response
|
| 315 |
+
}
|
| 316 |
+
]
|
| 317 |
+
|
| 318 |
+
# Set enhanced formats in record
|
| 319 |
+
record.set_enhanced_formats(
|
| 320 |
+
enhanced_display=enhanced_display,
|
| 321 |
+
visual_sections=visual_sections
|
| 322 |
+
)
|
| 323 |
+
|
| 324 |
+
except Exception as e:
|
| 325 |
+
print(f"Warning: Could not generate enhanced formats for record {record.exchange_id}: {e}")
|
| 326 |
+
|
| 327 |
def get_verification_progress(self, session_id: str) -> Optional[VerificationProgress]:
|
| 328 |
"""Get verification progress for session."""
|
| 329 |
session = self.store.load_session(session_id)
|
|
|
|
| 374 |
|
| 375 |
progress = session.get_progress()
|
| 376 |
|
| 377 |
+
# Enhanced statistics including format information
|
| 378 |
+
enhanced_stats = {
|
| 379 |
"session_id": session.session_id,
|
| 380 |
"patient_name": session.patient_name,
|
| 381 |
"verifier_name": session.verifier_name,
|
|
|
|
| 384 |
"is_complete": session.is_complete,
|
| 385 |
"progress": asdict(progress),
|
| 386 |
"total_exchanges": session.total_exchanges,
|
| 387 |
+
"verified_exchanges": session.verified_exchanges,
|
| 388 |
+
"enhanced_format_enabled": getattr(session, 'enhanced_format_enabled', False),
|
| 389 |
+
"records_with_enhanced_display": sum(1 for r in session.verification_records if r.enhanced_display_format),
|
| 390 |
+
"records_with_provider_summary": sum(1 for r in session.verification_records if r.provider_summary),
|
| 391 |
+
"records_with_coherent_paragraph": sum(1 for r in session.verification_records if r.coherent_summary_paragraph)
|
| 392 |
}
|
| 393 |
+
|
| 394 |
+
return enhanced_stats
|
| 395 |
|
| 396 |
+
def load_session(self, session_id: str) -> Optional[EnhancedVerificationSession]:
|
| 397 |
+
"""Load enhanced verification session by ID."""
|
| 398 |
return self.store.load_session(session_id)
|
| 399 |
|
| 400 |
+
def save_session(self, session: EnhancedVerificationSession) -> None:
|
| 401 |
+
"""Save enhanced verification session."""
|
| 402 |
self.store.save_session(session)
|
| 403 |
|
| 404 |
def list_sessions(self) -> List[Dict[str, Any]]:
|
|
|
|
| 409 |
"""Get incomplete verification sessions."""
|
| 410 |
return self.store.get_incomplete_sessions()
|
| 411 |
|
| 412 |
+
def export_session_with_enhanced_data(self, session_id: str) -> Optional[Dict[str, Any]]:
|
| 413 |
+
"""Export session with enhanced format data included."""
|
| 414 |
+
session = self.load_session(session_id)
|
| 415 |
+
if not session:
|
| 416 |
+
return None
|
| 417 |
+
|
| 418 |
+
export_data = {
|
| 419 |
+
'session_info': {
|
| 420 |
+
'session_id': session.session_id,
|
| 421 |
+
'conversation_session_id': session.conversation_session_id,
|
| 422 |
+
'patient_name': session.patient_name,
|
| 423 |
+
'verifier_name': session.verifier_name,
|
| 424 |
+
'start_time': session.start_time.isoformat(),
|
| 425 |
+
'end_time': session.end_time.isoformat() if session.end_time else None,
|
| 426 |
+
'enhanced_format_enabled': getattr(session, 'enhanced_format_enabled', False)
|
| 427 |
+
},
|
| 428 |
+
'records': []
|
| 429 |
+
}
|
| 430 |
+
|
| 431 |
+
for record in session.verification_records:
|
| 432 |
+
record_data = {
|
| 433 |
+
'exchange_id': record.exchange_id,
|
| 434 |
+
'exchange_number': record.exchange_number,
|
| 435 |
+
'timestamp': record.timestamp.isoformat(),
|
| 436 |
+
'user_message': record.user_message,
|
| 437 |
+
'assistant_response': record.assistant_response,
|
| 438 |
+
'original_classification': record.original_classification,
|
| 439 |
+
'original_confidence': record.original_confidence,
|
| 440 |
+
'original_indicators': record.original_indicators,
|
| 441 |
+
'original_reasoning': record.original_reasoning,
|
| 442 |
+
'is_correct': record.is_correct,
|
| 443 |
+
'correct_classification': record.correct_classification,
|
| 444 |
+
'correction_reason': record.correction_reason,
|
| 445 |
+
'verifier_notes': record.verifier_notes,
|
| 446 |
+
'verification_timestamp': record.verification_timestamp.isoformat() if record.verification_timestamp else None,
|
| 447 |
+
|
| 448 |
+
# Enhanced format data
|
| 449 |
+
'enhanced_display_format': record.enhanced_display_format,
|
| 450 |
+
'provider_summary': record.provider_summary,
|
| 451 |
+
'coherent_summary_paragraph': record.coherent_summary_paragraph,
|
| 452 |
+
'visual_sections': record.visual_sections
|
| 453 |
+
}
|
| 454 |
+
export_data['records'].append(record_data)
|
| 455 |
+
|
| 456 |
+
return export_data
|
| 457 |
+
|
| 458 |
+
|
| 459 |
+
# Legacy aliases for backward compatibility
|
| 460 |
+
ConversationVerificationManager = EnhancedConversationVerificationManager
|
| 461 |
+
VerificationSession = EnhancedVerificationSession
|
| 462 |
+
VerificationRecord = EnhancedVerificationRecord
|
| 463 |
+
|
|
@@ -0,0 +1,409 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# improved_classification_prompt_manager.py
|
| 2 |
+
"""
|
| 3 |
+
Improved Classification Prompt Manager for UI Classification Improvements.
|
| 4 |
+
|
| 5 |
+
Manages updated classification prompts with new red flag definitions and explicit indicators
|
| 6 |
+
based on medical professional feedback and requirements.
|
| 7 |
+
|
| 8 |
+
Requirements: 4.1, 4.2, 5.1, 5.2, 5.3, 5.4, 5.5, 5.6
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
from typing import List, Dict, Optional
|
| 12 |
+
from dataclasses import dataclass
|
| 13 |
+
import logging
|
| 14 |
+
|
| 15 |
+
from src.core.chaplain_models import ClassificationFlowResult, DistressIndicator
|
| 16 |
+
|
| 17 |
+
# Configure logging
|
| 18 |
+
logging.basicConfig(level=logging.INFO)
|
| 19 |
+
logger = logging.getLogger(__name__)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
@dataclass
|
| 23 |
+
class ClassificationResult:
|
| 24 |
+
"""
|
| 25 |
+
Enhanced classification result with validation support.
|
| 26 |
+
|
| 27 |
+
Extends the basic classification to include validation fields
|
| 28 |
+
for consistency checking.
|
| 29 |
+
"""
|
| 30 |
+
classification: str # RED/YELLOW/GREEN
|
| 31 |
+
confidence: float
|
| 32 |
+
indicators: List[str]
|
| 33 |
+
reasoning: str
|
| 34 |
+
red_flag_indicators: List[str]
|
| 35 |
+
yellow_flag_indicators: List[str]
|
| 36 |
+
is_valid: bool
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class ImprovedClassificationPromptManager:
|
| 40 |
+
"""
|
| 41 |
+
Manages updated classification prompts with improved red flag definitions.
|
| 42 |
+
|
| 43 |
+
Implements new red flag criteria based on medical professional feedback:
|
| 44 |
+
- Broader definition of red flags beyond crisis situations
|
| 45 |
+
- Explicit red indicators for consistent classification
|
| 46 |
+
- Updated yellow flag criteria for better triage
|
| 47 |
+
|
| 48 |
+
Requirements: 4.1, 4.2, 5.1, 5.2, 5.3, 5.4, 5.5, 5.6
|
| 49 |
+
"""
|
| 50 |
+
|
| 51 |
+
def __init__(self):
|
| 52 |
+
"""Initialize the improved classification prompt manager."""
|
| 53 |
+
self._error_handler = None # Lazy loaded to avoid circular imports
|
| 54 |
+
logger.info("🔧 ImprovedClassificationPromptManager initialized")
|
| 55 |
+
|
| 56 |
+
@property
|
| 57 |
+
def error_handler(self):
|
| 58 |
+
"""Lazy load error handler to avoid circular imports."""
|
| 59 |
+
if self._error_handler is None:
|
| 60 |
+
from src.core.ui_error_handler import UIErrorHandler
|
| 61 |
+
self._error_handler = UIErrorHandler()
|
| 62 |
+
return self._error_handler
|
| 63 |
+
|
| 64 |
+
def get_updated_red_flag_definition(self) -> str:
|
| 65 |
+
"""
|
| 66 |
+
Get the updated red flag definition.
|
| 67 |
+
|
| 68 |
+
Returns broader definition that includes active spiritual distress,
|
| 69 |
+
not just crisis situations.
|
| 70 |
+
|
| 71 |
+
Requirements: 4.1, 4.2
|
| 72 |
+
"""
|
| 73 |
+
return (
|
| 74 |
+
"Patient's situation appears to be caused by or actively causing "
|
| 75 |
+
"emotional or spiritual distress"
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
def get_explicit_red_indicators(self) -> List[str]:
|
| 79 |
+
"""
|
| 80 |
+
Get the list of explicit red flag indicators.
|
| 81 |
+
|
| 82 |
+
Returns the five specific indicators that should always trigger
|
| 83 |
+
red classification for consistency.
|
| 84 |
+
|
| 85 |
+
Requirements: 5.2, 5.3, 5.4, 5.5, 5.6
|
| 86 |
+
"""
|
| 87 |
+
return [
|
| 88 |
+
"Complex grief",
|
| 89 |
+
"Loss of a loved one",
|
| 90 |
+
"Doubt about meaning of life",
|
| 91 |
+
"Doubt about meaning of suffering",
|
| 92 |
+
"Doubt about personal dignity"
|
| 93 |
+
]
|
| 94 |
+
|
| 95 |
+
def get_yellow_flag_criteria(self) -> List[str]:
|
| 96 |
+
"""
|
| 97 |
+
Get criteria for yellow flag classification.
|
| 98 |
+
|
| 99 |
+
Returns criteria for situations requiring further investigation
|
| 100 |
+
but not immediate red classification.
|
| 101 |
+
|
| 102 |
+
Requirements: 6.1, 6.2, 6.3, 6.4, 6.5
|
| 103 |
+
"""
|
| 104 |
+
return [
|
| 105 |
+
"Feelings of sadness without additional context",
|
| 106 |
+
"Uncertainty about cause or consequences of distress",
|
| 107 |
+
"Emotional states requiring further clarification",
|
| 108 |
+
"Potential spiritual issues without clear indicators",
|
| 109 |
+
"Situations requiring additional questions"
|
| 110 |
+
]
|
| 111 |
+
|
| 112 |
+
def build_enhanced_classification_prompt(self) -> str:
|
| 113 |
+
"""
|
| 114 |
+
Build the complete enhanced classification prompt.
|
| 115 |
+
|
| 116 |
+
Combines updated definitions, explicit indicators, and criteria
|
| 117 |
+
into a comprehensive prompt for consistent classification.
|
| 118 |
+
|
| 119 |
+
Requirements: 5.1, 5.7
|
| 120 |
+
"""
|
| 121 |
+
red_definition = self.get_updated_red_flag_definition()
|
| 122 |
+
red_indicators = self.get_explicit_red_indicators()
|
| 123 |
+
yellow_criteria = self.get_yellow_flag_criteria()
|
| 124 |
+
|
| 125 |
+
prompt = f"""<system_role>
|
| 126 |
+
You are a spiritual distress classification specialist for a medical chat system.
|
| 127 |
+
Your task is to analyze patient messages and classify the level of spiritual
|
| 128 |
+
or emotional distress to route them to appropriate support.
|
| 129 |
+
</system_role>
|
| 130 |
+
|
| 131 |
+
<updated_red_flag_definition>
|
| 132 |
+
RED FLAG: {red_definition}
|
| 133 |
+
|
| 134 |
+
This includes:
|
| 135 |
+
- Active spiritual distress (even if not crisis-level)
|
| 136 |
+
- Situations where circumstances cause distress
|
| 137 |
+
- Situations where distress affects circumstances
|
| 138 |
+
- Severe distress or crisis as a subcategory
|
| 139 |
+
</updated_red_flag_definition>
|
| 140 |
+
|
| 141 |
+
<explicit_red_indicators>
|
| 142 |
+
The following indicators should ALWAYS result in RED classification:
|
| 143 |
+
"""
|
| 144 |
+
|
| 145 |
+
for i, indicator in enumerate(red_indicators, 1):
|
| 146 |
+
prompt += f"{i}. {indicator}\n"
|
| 147 |
+
|
| 148 |
+
prompt += f"""</explicit_red_indicators>
|
| 149 |
+
|
| 150 |
+
<yellow_flag_criteria>
|
| 151 |
+
YELLOW FLAG is used for:
|
| 152 |
+
"""
|
| 153 |
+
|
| 154 |
+
for criterion in yellow_criteria:
|
| 155 |
+
prompt += f"- {criterion}\n"
|
| 156 |
+
|
| 157 |
+
prompt += """
|
| 158 |
+
Yellow flag indicates need for further investigation and should
|
| 159 |
+
include recommendations for additional questions.
|
| 160 |
+
</yellow_flag_criteria>
|
| 161 |
+
|
| 162 |
+
<classification_logic>
|
| 163 |
+
1. Check for presence of explicit red indicators
|
| 164 |
+
2. Assess if situation matches updated red flag definition
|
| 165 |
+
3. If not red, determine if further clarification needed (yellow)
|
| 166 |
+
4. Green only if no signs of spiritual/emotional distress
|
| 167 |
+
</classification_logic>
|
| 168 |
+
|
| 169 |
+
<critical_rules>
|
| 170 |
+
- Patient safety is priority
|
| 171 |
+
- When in doubt, favor higher level of classification
|
| 172 |
+
- Ensure consistent application of explicit indicators
|
| 173 |
+
- Simple sadness without context = yellow, not red
|
| 174 |
+
- Loss of meaning and purpose = always red
|
| 175 |
+
</critical_rules>
|
| 176 |
+
|
| 177 |
+
<output_format>
|
| 178 |
+
Respond with ONLY valid JSON in this exact format:
|
| 179 |
+
{{
|
| 180 |
+
"state": "red|yellow|green",
|
| 181 |
+
"indicators": ["list of detected indicators"],
|
| 182 |
+
"confidence": 0.0-1.0,
|
| 183 |
+
"reasoning": "explanation of classification in English",
|
| 184 |
+
"red_flag_indicators": ["list of red flag indicators if any"],
|
| 185 |
+
"yellow_flag_indicators": ["list of yellow flag indicators if any"]
|
| 186 |
+
}}
|
| 187 |
+
</output_format>"""
|
| 188 |
+
|
| 189 |
+
return prompt
|
| 190 |
+
|
| 191 |
+
def validate_classification_consistency(self, result: ClassificationResult) -> bool:
|
| 192 |
+
"""
|
| 193 |
+
Validate classification result for consistency with new criteria.
|
| 194 |
+
|
| 195 |
+
Checks if the classification follows the updated rules and
|
| 196 |
+
explicit indicators properly.
|
| 197 |
+
|
| 198 |
+
Args:
|
| 199 |
+
result: ClassificationResult to validate
|
| 200 |
+
|
| 201 |
+
Returns:
|
| 202 |
+
True if classification is consistent with new criteria
|
| 203 |
+
|
| 204 |
+
Requirements: 5.7, 4.1, 4.2
|
| 205 |
+
"""
|
| 206 |
+
try:
|
| 207 |
+
# Check if classification is valid
|
| 208 |
+
if result.classification not in ["red", "yellow", "green"]:
|
| 209 |
+
logger.warning(f"Invalid classification: {result.classification}")
|
| 210 |
+
return False
|
| 211 |
+
|
| 212 |
+
# Check confidence range
|
| 213 |
+
if not 0.0 <= result.confidence <= 1.0:
|
| 214 |
+
logger.warning(f"Invalid confidence: {result.confidence}")
|
| 215 |
+
return False
|
| 216 |
+
|
| 217 |
+
# Validate red flag consistency
|
| 218 |
+
explicit_red_indicators = self.get_explicit_red_indicators()
|
| 219 |
+
|
| 220 |
+
# If any explicit red indicator is present, classification should be red
|
| 221 |
+
for indicator in result.red_flag_indicators:
|
| 222 |
+
if any(red_indicator.lower() in indicator.lower()
|
| 223 |
+
for red_indicator in explicit_red_indicators):
|
| 224 |
+
if result.classification != "red":
|
| 225 |
+
logger.warning(
|
| 226 |
+
f"Explicit red indicator '{indicator}' found but "
|
| 227 |
+
f"classification is '{result.classification}'"
|
| 228 |
+
)
|
| 229 |
+
return False
|
| 230 |
+
|
| 231 |
+
# Check for simple sadness misclassification
|
| 232 |
+
sadness_indicators = ["смуток", "сумно", "почуття смутку", "sadness", "sad"]
|
| 233 |
+
has_simple_sadness = any(
|
| 234 |
+
sadness in " ".join(result.indicators).lower()
|
| 235 |
+
for sadness in sadness_indicators
|
| 236 |
+
)
|
| 237 |
+
|
| 238 |
+
if has_simple_sadness and result.classification == "red":
|
| 239 |
+
# Check if there are additional spiritual indicators
|
| 240 |
+
spiritual_indicators = [
|
| 241 |
+
"loss of meaning", "spiritual", "faith"
|
| 242 |
+
]
|
| 243 |
+
has_spiritual_context = any(
|
| 244 |
+
spiritual in " ".join(result.indicators).lower()
|
| 245 |
+
for spiritual in spiritual_indicators
|
| 246 |
+
)
|
| 247 |
+
|
| 248 |
+
if not has_spiritual_context:
|
| 249 |
+
logger.warning(
|
| 250 |
+
"Simple sadness classified as red without spiritual context"
|
| 251 |
+
)
|
| 252 |
+
return False
|
| 253 |
+
|
| 254 |
+
# Validate that loss of meaning/purpose is red
|
| 255 |
+
meaning_indicators = [
|
| 256 |
+
"втрата сенсу", "втрата мети", "сенс життя", "мета життя",
|
| 257 |
+
"loss of meaning", "loss of purpose", "meaning of life"
|
| 258 |
+
]
|
| 259 |
+
has_meaning_loss = any(
|
| 260 |
+
meaning in " ".join(result.indicators).lower()
|
| 261 |
+
for meaning in meaning_indicators
|
| 262 |
+
)
|
| 263 |
+
|
| 264 |
+
if has_meaning_loss and result.classification != "red":
|
| 265 |
+
logger.warning(
|
| 266 |
+
"Loss of meaning/purpose should be classified as red"
|
| 267 |
+
)
|
| 268 |
+
return False
|
| 269 |
+
|
| 270 |
+
logger.info(f"Classification validation passed: {result.classification}")
|
| 271 |
+
return True
|
| 272 |
+
|
| 273 |
+
except Exception as e:
|
| 274 |
+
logger.error(f"Error validating classification: {e}")
|
| 275 |
+
return False
|
| 276 |
+
|
| 277 |
+
def get_classification_guidelines(self) -> Dict[str, str]:
|
| 278 |
+
"""
|
| 279 |
+
Get classification guidelines for reference.
|
| 280 |
+
|
| 281 |
+
Returns dictionary with guidelines for each classification level.
|
| 282 |
+
"""
|
| 283 |
+
return {
|
| 284 |
+
"red": (
|
| 285 |
+
"Active or potential spiritual/emotional distress. "
|
| 286 |
+
"Includes explicit indicators: complex grief, loss of loved one, "
|
| 287 |
+
"doubt about meaning of life/suffering/dignity."
|
| 288 |
+
),
|
| 289 |
+
"yellow": (
|
| 290 |
+
"Requires further investigation. Unclear situations, "
|
| 291 |
+
"simple sadness without context, emotional states requiring clarification."
|
| 292 |
+
),
|
| 293 |
+
"green": (
|
| 294 |
+
"No signs of spiritual or emotional distress. "
|
| 295 |
+
"Regular conversation without problem indicators."
|
| 296 |
+
)
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
def create_classification_result(
|
| 300 |
+
self,
|
| 301 |
+
classification: str,
|
| 302 |
+
confidence: float,
|
| 303 |
+
indicators: List[str],
|
| 304 |
+
reasoning: str,
|
| 305 |
+
red_flag_indicators: Optional[List[str]] = None,
|
| 306 |
+
yellow_flag_indicators: Optional[List[str]] = None
|
| 307 |
+
) -> ClassificationResult:
|
| 308 |
+
"""
|
| 309 |
+
Create a validated ClassificationResult.
|
| 310 |
+
|
| 311 |
+
Args:
|
| 312 |
+
classification: RED/YELLOW/GREEN
|
| 313 |
+
confidence: Confidence score 0.0-1.0
|
| 314 |
+
indicators: List of detected indicators
|
| 315 |
+
reasoning: Explanation of classification
|
| 316 |
+
red_flag_indicators: List of red flag indicators
|
| 317 |
+
yellow_flag_indicators: List of yellow flag indicators
|
| 318 |
+
|
| 319 |
+
Returns:
|
| 320 |
+
ClassificationResult with validation status
|
| 321 |
+
"""
|
| 322 |
+
try:
|
| 323 |
+
result = ClassificationResult(
|
| 324 |
+
classification=classification,
|
| 325 |
+
confidence=confidence,
|
| 326 |
+
indicators=indicators,
|
| 327 |
+
reasoning=reasoning,
|
| 328 |
+
red_flag_indicators=red_flag_indicators or [],
|
| 329 |
+
yellow_flag_indicators=yellow_flag_indicators or [],
|
| 330 |
+
is_valid=False # Will be set by validation
|
| 331 |
+
)
|
| 332 |
+
|
| 333 |
+
# Validate the result
|
| 334 |
+
result.is_valid = self.validate_classification_consistency(result)
|
| 335 |
+
|
| 336 |
+
# Apply fallbacks if validation fails
|
| 337 |
+
if not result.is_valid:
|
| 338 |
+
result = self._apply_classification_fallbacks(result)
|
| 339 |
+
|
| 340 |
+
return result
|
| 341 |
+
|
| 342 |
+
except Exception as e:
|
| 343 |
+
logger.error(f"Error creating classification result: {e}")
|
| 344 |
+
# Return fallback classification using error handler
|
| 345 |
+
return self.error_handler.handle_classification_error(e, {
|
| 346 |
+
'classification': classification,
|
| 347 |
+
'confidence': confidence,
|
| 348 |
+
'indicators': indicators,
|
| 349 |
+
'reasoning': reasoning
|
| 350 |
+
})
|
| 351 |
+
|
| 352 |
+
def _apply_classification_fallbacks(self, result: ClassificationResult) -> ClassificationResult:
|
| 353 |
+
"""
|
| 354 |
+
Apply fallbacks to fix invalid classification result.
|
| 355 |
+
|
| 356 |
+
Args:
|
| 357 |
+
result: Invalid ClassificationResult
|
| 358 |
+
|
| 359 |
+
Returns:
|
| 360 |
+
Fixed ClassificationResult
|
| 361 |
+
"""
|
| 362 |
+
try:
|
| 363 |
+
# Fix classification if invalid
|
| 364 |
+
if result.classification not in ["red", "yellow", "green"]:
|
| 365 |
+
result.classification = "yellow" # Safe default
|
| 366 |
+
logger.warning(f"Fixed invalid classification to yellow")
|
| 367 |
+
|
| 368 |
+
# Fix confidence if out of range
|
| 369 |
+
if not 0.0 <= result.confidence <= 1.0:
|
| 370 |
+
result.confidence = max(0.0, min(1.0, result.confidence))
|
| 371 |
+
logger.warning(f"Clamped confidence to valid range: {result.confidence}")
|
| 372 |
+
|
| 373 |
+
# Ensure reasoning is present
|
| 374 |
+
if not result.reasoning or len(result.reasoning.strip()) < 10:
|
| 375 |
+
result.reasoning = f"Classification: {result.classification.upper()}. Manual review recommended due to validation issues."
|
| 376 |
+
logger.warning("Applied fallback reasoning")
|
| 377 |
+
|
| 378 |
+
# Ensure indicators are present
|
| 379 |
+
if not result.indicators:
|
| 380 |
+
result.indicators = ["Classification validation issues - manual review required"]
|
| 381 |
+
logger.warning("Applied fallback indicators")
|
| 382 |
+
|
| 383 |
+
# Re-validate
|
| 384 |
+
result.is_valid = self.validate_classification_consistency(result)
|
| 385 |
+
|
| 386 |
+
return result
|
| 387 |
+
|
| 388 |
+
except Exception as e:
|
| 389 |
+
logger.error(f"Failed to apply classification fallbacks: {e}")
|
| 390 |
+
# Return minimal valid result
|
| 391 |
+
return ClassificationResult(
|
| 392 |
+
classification="yellow",
|
| 393 |
+
confidence=0.5,
|
| 394 |
+
indicators=["System error - manual review required"],
|
| 395 |
+
reasoning="Classification system error. Manual review required.",
|
| 396 |
+
red_flag_indicators=[],
|
| 397 |
+
yellow_flag_indicators=["System error"],
|
| 398 |
+
is_valid=False
|
| 399 |
+
)
|
| 400 |
+
|
| 401 |
+
|
| 402 |
+
def create_improved_classification_prompt_manager() -> ImprovedClassificationPromptManager:
|
| 403 |
+
"""
|
| 404 |
+
Factory function to create ImprovedClassificationPromptManager.
|
| 405 |
+
|
| 406 |
+
Returns:
|
| 407 |
+
Initialized ImprovedClassificationPromptManager instance
|
| 408 |
+
"""
|
| 409 |
+
return ImprovedClassificationPromptManager()
|
|
@@ -13,6 +13,11 @@ from datetime import datetime
|
|
| 13 |
from typing import List, Optional
|
| 14 |
import sys
|
| 15 |
import os
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
# Add src to path for imports
|
| 18 |
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
@@ -139,6 +144,7 @@ class ProviderSummaryGenerator:
|
|
| 139 |
"""
|
| 140 |
self.ai_client = ai_client
|
| 141 |
self.prompt_controller = PromptController()
|
|
|
|
| 142 |
self.default_actions = [
|
| 143 |
"Contact patient within 24 hours",
|
| 144 |
"Assess immediate safety and support needs",
|
|
@@ -153,6 +159,14 @@ class ProviderSummaryGenerator:
|
|
| 153 |
'MODERATE': 0.5 # Standard follow-up
|
| 154 |
}
|
| 155 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
def generate_summary(
|
| 157 |
self,
|
| 158 |
indicators: List[str],
|
|
@@ -194,68 +208,98 @@ class ProviderSummaryGenerator:
|
|
| 194 |
|
| 195 |
Requirements: 7.1, 7.2, 7.3, 7.4, 7.5
|
| 196 |
"""
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 259 |
|
| 260 |
def _generate_conversation_summary(
|
| 261 |
self,
|
|
@@ -891,6 +935,269 @@ class ProviderSummaryGenerator:
|
|
| 891 |
|
| 892 |
return " ".join(parts)
|
| 893 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 894 |
def generate_summary_with_validation(self, **kwargs) -> tuple[ProviderSummary, List[str]]:
|
| 895 |
"""
|
| 896 |
Generate provider summary with validation feedback.
|
|
@@ -901,6 +1208,66 @@ class ProviderSummaryGenerator:
|
|
| 901 |
summary = self.generate_summary(**kwargs)
|
| 902 |
validation_issues = summary.validate_completeness()
|
| 903 |
return summary, validation_issues
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 904 |
|
| 905 |
|
| 906 |
def create_provider_summary_generator() -> ProviderSummaryGenerator:
|
|
|
|
| 13 |
from typing import List, Optional
|
| 14 |
import sys
|
| 15 |
import os
|
| 16 |
+
import logging
|
| 17 |
+
|
| 18 |
+
# Configure logging
|
| 19 |
+
logging.basicConfig(level=logging.INFO)
|
| 20 |
+
logger = logging.getLogger(__name__)
|
| 21 |
|
| 22 |
# Add src to path for imports
|
| 23 |
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
| 144 |
"""
|
| 145 |
self.ai_client = ai_client
|
| 146 |
self.prompt_controller = PromptController()
|
| 147 |
+
self._error_handler = None # Lazy loaded to avoid circular imports
|
| 148 |
self.default_actions = [
|
| 149 |
"Contact patient within 24 hours",
|
| 150 |
"Assess immediate safety and support needs",
|
|
|
|
| 159 |
'MODERATE': 0.5 # Standard follow-up
|
| 160 |
}
|
| 161 |
|
| 162 |
+
@property
|
| 163 |
+
def error_handler(self):
|
| 164 |
+
"""Lazy load error handler to avoid circular imports."""
|
| 165 |
+
if self._error_handler is None:
|
| 166 |
+
from src.core.ui_error_handler import UIErrorHandler
|
| 167 |
+
self._error_handler = UIErrorHandler()
|
| 168 |
+
return self._error_handler
|
| 169 |
+
|
| 170 |
def generate_summary(
|
| 171 |
self,
|
| 172 |
indicators: List[str],
|
|
|
|
| 208 |
|
| 209 |
Requirements: 7.1, 7.2, 7.3, 7.4, 7.5
|
| 210 |
"""
|
| 211 |
+
try:
|
| 212 |
+
# Build triage context (Requirement 7.4)
|
| 213 |
+
triage_context = []
|
| 214 |
+
if triage_questions and triage_responses:
|
| 215 |
+
for q, r in zip(triage_questions, triage_responses):
|
| 216 |
+
triage_context.append({
|
| 217 |
+
"question": q,
|
| 218 |
+
"response": r,
|
| 219 |
+
"timestamp": datetime.now().isoformat()
|
| 220 |
+
})
|
| 221 |
+
|
| 222 |
+
# Generate conversation history summary (Requirement 7.5)
|
| 223 |
+
conversation_history_summary = self._generate_conversation_summary(
|
| 224 |
+
conversation_history, indicators, context_factors or []
|
| 225 |
+
)
|
| 226 |
+
|
| 227 |
+
# Determine severity and urgency levels
|
| 228 |
+
severity_level = self._determine_severity_level(confidence, indicators, context_factors or [])
|
| 229 |
+
urgency_level = self._determine_urgency_level(severity_level, defensive_patterns_detected)
|
| 230 |
+
|
| 231 |
+
# Generate situation description
|
| 232 |
+
situation_description = self._generate_enhanced_situation_description(
|
| 233 |
+
indicators, reasoning, triage_context, medical_context, context_factors or []
|
| 234 |
+
)
|
| 235 |
+
|
| 236 |
+
# Generate recommended actions
|
| 237 |
+
recommended_actions = self._generate_recommended_actions(
|
| 238 |
+
severity_level, indicators, defensive_patterns_detected, medical_context
|
| 239 |
+
)
|
| 240 |
+
|
| 241 |
+
# Determine follow-up timeline
|
| 242 |
+
follow_up_timeline = self._determine_follow_up_timeline(urgency_level, severity_level)
|
| 243 |
+
|
| 244 |
+
summary = ProviderSummary(
|
| 245 |
+
# Contact information (Requirement 7.1)
|
| 246 |
+
patient_name=patient_name or "[Patient Name]",
|
| 247 |
+
patient_phone=patient_phone or "[Phone Number]",
|
| 248 |
+
patient_email=patient_email,
|
| 249 |
+
emergency_contact=emergency_contact,
|
| 250 |
+
|
| 251 |
+
# Classification information (Requirements 7.2, 7.3)
|
| 252 |
+
classification="RED",
|
| 253 |
+
confidence=confidence,
|
| 254 |
+
reasoning=reasoning,
|
| 255 |
+
indicators=indicators or [],
|
| 256 |
+
severity_level=severity_level,
|
| 257 |
+
|
| 258 |
+
# Context information (Requirements 7.4, 7.5)
|
| 259 |
+
triage_context=triage_context,
|
| 260 |
+
conversation_context=conversation_context or "",
|
| 261 |
+
conversation_history_summary=conversation_history_summary,
|
| 262 |
+
|
| 263 |
+
# Enhanced contextual information
|
| 264 |
+
medical_context=medical_context,
|
| 265 |
+
context_factors=context_factors or [],
|
| 266 |
+
defensive_patterns_detected=defensive_patterns_detected,
|
| 267 |
+
|
| 268 |
+
# Administrative information
|
| 269 |
+
situation_description=situation_description,
|
| 270 |
+
urgency_level=urgency_level,
|
| 271 |
+
recommended_actions=recommended_actions,
|
| 272 |
+
follow_up_timeline=follow_up_timeline
|
| 273 |
+
)
|
| 274 |
+
|
| 275 |
+
# Validate and apply fallbacks if needed
|
| 276 |
+
validation_result = self.error_handler.validate_provider_summary_structure(summary)
|
| 277 |
+
if validation_result.has_critical_errors():
|
| 278 |
+
logger.warning("Critical errors found in provider summary, applying fallbacks")
|
| 279 |
+
summary = self.error_handler.apply_fallback_template(summary, "general")
|
| 280 |
+
|
| 281 |
+
# Fix confidence if out of range
|
| 282 |
+
if not 0.0 <= summary.confidence <= 1.0:
|
| 283 |
+
summary.confidence = max(0.0, min(1.0, summary.confidence))
|
| 284 |
+
logger.warning(f"Clamped confidence to valid range: {summary.confidence}")
|
| 285 |
+
|
| 286 |
+
# Fix reasoning if empty
|
| 287 |
+
if not summary.reasoning or len(summary.reasoning.strip()) < 10:
|
| 288 |
+
if summary.indicators:
|
| 289 |
+
indicators_text = ", ".join(summary.indicators[:3])
|
| 290 |
+
summary.reasoning = f"RED flag classification based on detected indicators: {indicators_text}. Immediate spiritual care support recommended."
|
| 291 |
+
else:
|
| 292 |
+
summary.reasoning = "RED flag classification indicates potential spiritual or emotional distress requiring immediate attention."
|
| 293 |
+
logger.warning("Applied fallback reasoning")
|
| 294 |
+
|
| 295 |
+
return summary
|
| 296 |
+
|
| 297 |
+
except Exception as e:
|
| 298 |
+
logger.error(f"Error generating provider summary: {e}")
|
| 299 |
+
# Return fallback summary
|
| 300 |
+
return self._create_fallback_summary(
|
| 301 |
+
indicators, reasoning, confidence, patient_name, patient_phone
|
| 302 |
+
)
|
| 303 |
|
| 304 |
def _generate_conversation_summary(
|
| 305 |
self,
|
|
|
|
| 935 |
|
| 936 |
return " ".join(parts)
|
| 937 |
|
| 938 |
+
def format_coherent_paragraph(self, summary: ProviderSummary) -> str:
|
| 939 |
+
"""
|
| 940 |
+
Format provider summary as a single coherent paragraph following Medical Brain style.
|
| 941 |
+
|
| 942 |
+
This method now uses LLM-based generation with the spiritual_care_message prompt
|
| 943 |
+
to create Medical Brain compatible summaries instead of hardcoded logic.
|
| 944 |
+
|
| 945 |
+
Args:
|
| 946 |
+
summary: ProviderSummary object to format
|
| 947 |
+
|
| 948 |
+
Returns:
|
| 949 |
+
Formatted coherent paragraph with optional patient quote
|
| 950 |
+
|
| 951 |
+
Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8
|
| 952 |
+
"""
|
| 953 |
+
return self.generate_medical_brain_summary(summary)
|
| 954 |
+
|
| 955 |
+
def generate_medical_brain_summary(
|
| 956 |
+
self,
|
| 957 |
+
summary: ProviderSummary,
|
| 958 |
+
session_id: Optional[str] = None,
|
| 959 |
+
model_override: Optional[str] = None
|
| 960 |
+
) -> str:
|
| 961 |
+
"""
|
| 962 |
+
Generate Medical Brain compatible summary using LLM with spiritual_care_message prompt.
|
| 963 |
+
|
| 964 |
+
Args:
|
| 965 |
+
summary: ProviderSummary with all case details
|
| 966 |
+
session_id: Optional session ID for session-specific prompt overrides
|
| 967 |
+
model_override: Optional model name override for this generation
|
| 968 |
+
|
| 969 |
+
Returns:
|
| 970 |
+
Medical Brain compatible single-paragraph summary with patient quote
|
| 971 |
+
"""
|
| 972 |
+
# If no AI client available, fall back to hardcoded logic
|
| 973 |
+
if not self.ai_client:
|
| 974 |
+
return self._generate_hardcoded_coherent_paragraph(summary)
|
| 975 |
+
|
| 976 |
+
# Load system prompt using PromptController for session-aware loading
|
| 977 |
+
try:
|
| 978 |
+
prompt_config = self.prompt_controller.get_prompt(
|
| 979 |
+
agent_type='spiritual_care_message',
|
| 980 |
+
session_id=session_id
|
| 981 |
+
)
|
| 982 |
+
system_prompt = prompt_config.base_prompt
|
| 983 |
+
|
| 984 |
+
except Exception as e:
|
| 985 |
+
logger.warning(f"Error loading prompt files: {e}")
|
| 986 |
+
return self._generate_hardcoded_coherent_paragraph(summary)
|
| 987 |
+
|
| 988 |
+
# Build context for LLM - include all necessary information
|
| 989 |
+
user_prompt = self._build_medical_brain_prompt(summary)
|
| 990 |
+
|
| 991 |
+
try:
|
| 992 |
+
# Use existing ai_client (AIClientManager) to generate response
|
| 993 |
+
response = self.ai_client.generate_response(
|
| 994 |
+
system_prompt=system_prompt,
|
| 995 |
+
user_prompt=user_prompt,
|
| 996 |
+
temperature=0.3, # Lower temperature for more consistent formatting
|
| 997 |
+
call_type="medical_brain_summary",
|
| 998 |
+
agent_name="SpiritualCareMessage",
|
| 999 |
+
model_override=model_override
|
| 1000 |
+
)
|
| 1001 |
+
|
| 1002 |
+
return response.strip()
|
| 1003 |
+
|
| 1004 |
+
except Exception as e:
|
| 1005 |
+
logger.warning(f"Error generating LLM medical brain summary: {e}")
|
| 1006 |
+
return self._generate_hardcoded_coherent_paragraph(summary)
|
| 1007 |
+
|
| 1008 |
+
def _build_medical_brain_prompt(self, summary: ProviderSummary) -> str:
|
| 1009 |
+
"""Build user prompt for Medical Brain compatible summary generation."""
|
| 1010 |
+
|
| 1011 |
+
# Extract patient information
|
| 1012 |
+
patient_name = summary.patient_name or "Patient"
|
| 1013 |
+
patient_phone = summary.patient_phone or ""
|
| 1014 |
+
|
| 1015 |
+
# Extract demographics from medical context
|
| 1016 |
+
patient_age = "unknown age"
|
| 1017 |
+
patient_gender = "individual"
|
| 1018 |
+
medical_conditions = []
|
| 1019 |
+
|
| 1020 |
+
if summary.medical_context:
|
| 1021 |
+
age = summary.medical_context.get('age')
|
| 1022 |
+
if age:
|
| 1023 |
+
patient_age = f"{age}-year-old"
|
| 1024 |
+
|
| 1025 |
+
gender = summary.medical_context.get('gender', '').lower()
|
| 1026 |
+
if gender in ['male', 'female']:
|
| 1027 |
+
patient_gender = gender
|
| 1028 |
+
elif gender:
|
| 1029 |
+
patient_gender = gender
|
| 1030 |
+
|
| 1031 |
+
conditions = summary.medical_context.get('conditions', [])
|
| 1032 |
+
if isinstance(conditions, list):
|
| 1033 |
+
medical_conditions = conditions
|
| 1034 |
+
elif conditions:
|
| 1035 |
+
medical_conditions = [str(conditions)]
|
| 1036 |
+
|
| 1037 |
+
# Build indicators text
|
| 1038 |
+
indicators = summary.indicators or ["general distress"]
|
| 1039 |
+
indicators_text = ", ".join(indicators)
|
| 1040 |
+
|
| 1041 |
+
# Build conversation context for patient quote extraction
|
| 1042 |
+
conversation_context = ""
|
| 1043 |
+
if summary.conversation_context:
|
| 1044 |
+
conversation_context = summary.conversation_context[:1000] # Limit context size
|
| 1045 |
+
|
| 1046 |
+
# Build the prompt
|
| 1047 |
+
prompt_parts = [
|
| 1048 |
+
"FORMAT: MEDICAL_BRAIN_COMPATIBLE",
|
| 1049 |
+
"",
|
| 1050 |
+
f"PATIENT INFORMATION:",
|
| 1051 |
+
f"Name: {patient_name}",
|
| 1052 |
+
f"Phone: {patient_phone}",
|
| 1053 |
+
f"Age: {patient_age.replace('-year-old', '')}",
|
| 1054 |
+
f"Gender: {patient_gender}",
|
| 1055 |
+
"",
|
| 1056 |
+
f"MEDICAL HISTORY:",
|
| 1057 |
+
f"Conditions: {', '.join(medical_conditions) if medical_conditions else 'None documented'}",
|
| 1058 |
+
"",
|
| 1059 |
+
f"SPIRITUAL/EMOTIONAL CONCERNS:",
|
| 1060 |
+
f"Indicators: {indicators_text}",
|
| 1061 |
+
f"Situation: {summary.situation_description or 'spiritual or emotional distress'}",
|
| 1062 |
+
"",
|
| 1063 |
+
f"CLASSIFICATION: RED FLAG",
|
| 1064 |
+
f"CONSENT STATUS: Patient identified for spiritual care team contact",
|
| 1065 |
+
"",
|
| 1066 |
+
f"CONVERSATION CONTEXT (for patient quote extraction):",
|
| 1067 |
+
f"{conversation_context}",
|
| 1068 |
+
"",
|
| 1069 |
+
"Please generate a Medical Brain compatible summary following the MEDICAL_BRAIN_COMPATIBLE format specified in your instructions."
|
| 1070 |
+
]
|
| 1071 |
+
|
| 1072 |
+
return "\n".join(prompt_parts)
|
| 1073 |
+
|
| 1074 |
+
def _generate_hardcoded_coherent_paragraph(self, summary: ProviderSummary) -> str:
|
| 1075 |
+
"""
|
| 1076 |
+
Fallback method using hardcoded logic when LLM is unavailable.
|
| 1077 |
+
This preserves the original logic as a backup.
|
| 1078 |
+
"""
|
| 1079 |
+
# Extract patient information
|
| 1080 |
+
patient_name = summary.patient_name or "Patient"
|
| 1081 |
+
patient_phone = summary.patient_phone or ""
|
| 1082 |
+
|
| 1083 |
+
# Extract age and gender from medical context if available
|
| 1084 |
+
patient_age = "unknown age"
|
| 1085 |
+
patient_gender = "individual"
|
| 1086 |
+
medical_conditions = []
|
| 1087 |
+
|
| 1088 |
+
if summary.medical_context:
|
| 1089 |
+
age = summary.medical_context.get('age')
|
| 1090 |
+
if age:
|
| 1091 |
+
patient_age = f"{age}-year-old"
|
| 1092 |
+
|
| 1093 |
+
gender = summary.medical_context.get('gender', '').lower()
|
| 1094 |
+
if gender in ['male', 'female']:
|
| 1095 |
+
patient_gender = gender
|
| 1096 |
+
elif gender:
|
| 1097 |
+
patient_gender = gender
|
| 1098 |
+
|
| 1099 |
+
conditions = summary.medical_context.get('conditions', [])
|
| 1100 |
+
if isinstance(conditions, list):
|
| 1101 |
+
medical_conditions = conditions
|
| 1102 |
+
elif conditions:
|
| 1103 |
+
medical_conditions = [str(conditions)]
|
| 1104 |
+
|
| 1105 |
+
# Build demographic section (Requirement 2.2)
|
| 1106 |
+
demographic_part = f"{patient_name} is a {patient_age} {patient_gender}"
|
| 1107 |
+
|
| 1108 |
+
# Build medical history section (Requirement 2.3)
|
| 1109 |
+
if medical_conditions:
|
| 1110 |
+
if len(medical_conditions) == 1:
|
| 1111 |
+
medical_part = f"with clinical history of {medical_conditions[0]}"
|
| 1112 |
+
elif len(medical_conditions) == 2:
|
| 1113 |
+
medical_part = f"with clinical history of {medical_conditions[0]} and {medical_conditions[1]}"
|
| 1114 |
+
else:
|
| 1115 |
+
# Multiple conditions: "X, Y, and Z"
|
| 1116 |
+
medical_part = f"with clinical history of {', '.join(medical_conditions[:-1])}, and {medical_conditions[-1]}"
|
| 1117 |
+
else:
|
| 1118 |
+
medical_part = "with no significant medical history documented"
|
| 1119 |
+
|
| 1120 |
+
# Build spiritual concerns section (Requirement 2.4, 2.5)
|
| 1121 |
+
concerns = summary.indicators or []
|
| 1122 |
+
if concerns:
|
| 1123 |
+
if len(concerns) == 1:
|
| 1124 |
+
concerns_text = concerns[0]
|
| 1125 |
+
elif len(concerns) == 2:
|
| 1126 |
+
concerns_text = f"{concerns[0]} and {concerns[1]}"
|
| 1127 |
+
else:
|
| 1128 |
+
concerns_text = f"{', '.join(concerns[:-1])}, and {concerns[-1]}"
|
| 1129 |
+
else:
|
| 1130 |
+
concerns_text = "general distress"
|
| 1131 |
+
|
| 1132 |
+
# Determine spiritual concern type from situation description
|
| 1133 |
+
concern_type = summary.situation_description or "spiritual or emotional distress"
|
| 1134 |
+
|
| 1135 |
+
# Build concerns and classification section
|
| 1136 |
+
concerns_part = f"The patient expressed {concerns_text}, which may indicate {concern_type}"
|
| 1137 |
+
classification_part = f", resulting in generation of a RED FLAG"
|
| 1138 |
+
|
| 1139 |
+
# Build consent section (Requirement 2.6)
|
| 1140 |
+
# Note: Current ProviderSummary doesn't have explicit consent field,
|
| 1141 |
+
# so we'll use a default assumption for RED flags
|
| 1142 |
+
consent_part = "The patient has been identified for spiritual care team contact"
|
| 1143 |
+
|
| 1144 |
+
# Build contact section (Requirement 2.7)
|
| 1145 |
+
if patient_phone and patient_phone.strip() and patient_phone != "[Phone Number]":
|
| 1146 |
+
contact_part = f"The preferred contact number is {patient_phone}"
|
| 1147 |
+
else:
|
| 1148 |
+
contact_part = "No contact number is currently available"
|
| 1149 |
+
|
| 1150 |
+
# Combine all parts into coherent paragraph (Requirement 2.1)
|
| 1151 |
+
# First part: demographic + medical (connected with space)
|
| 1152 |
+
first_sentence = f"{demographic_part} {medical_part}"
|
| 1153 |
+
|
| 1154 |
+
# Remaining parts as separate sentences
|
| 1155 |
+
paragraph_parts = [
|
| 1156 |
+
first_sentence,
|
| 1157 |
+
concerns_part + classification_part,
|
| 1158 |
+
consent_part,
|
| 1159 |
+
contact_part
|
| 1160 |
+
]
|
| 1161 |
+
|
| 1162 |
+
# Join with appropriate connectors for flow
|
| 1163 |
+
coherent_paragraph = ". ".join(paragraph_parts)
|
| 1164 |
+
|
| 1165 |
+
# Ensure proper sentence ending
|
| 1166 |
+
if not coherent_paragraph.endswith('.'):
|
| 1167 |
+
coherent_paragraph += '.'
|
| 1168 |
+
|
| 1169 |
+
# Add patient quote section if available (Requirement 2.8)
|
| 1170 |
+
patient_quote = ""
|
| 1171 |
+
if summary.conversation_context:
|
| 1172 |
+
# Try to extract a meaningful quote from conversation context
|
| 1173 |
+
# Look for patient statements in the conversation
|
| 1174 |
+
lines = summary.conversation_context.split('\n')
|
| 1175 |
+
|
| 1176 |
+
# First, try to find user messages (new format)
|
| 1177 |
+
for line in lines:
|
| 1178 |
+
line = line.strip()
|
| 1179 |
+
if line.startswith('user: '):
|
| 1180 |
+
# Extract the actual message content
|
| 1181 |
+
message_content = line[6:].strip() # Remove 'user: ' prefix
|
| 1182 |
+
# Skip very short messages or technical responses
|
| 1183 |
+
if len(message_content) > 10 and not message_content.startswith('🟡') and not message_content.startswith('🔴'):
|
| 1184 |
+
patient_quote = f'Patient reported: "{message_content}"'
|
| 1185 |
+
break
|
| 1186 |
+
|
| 1187 |
+
# If no user messages found, try old format (direct patient text)
|
| 1188 |
+
if not patient_quote and len(summary.conversation_context.strip()) > 10:
|
| 1189 |
+
# Check if it's a single line of patient text (old format)
|
| 1190 |
+
context_text = summary.conversation_context.strip()
|
| 1191 |
+
# Make sure it doesn't contain assistant responses or technical markers
|
| 1192 |
+
if ('\n' not in context_text or len(lines) == 1) and not context_text.startswith('assistant:'):
|
| 1193 |
+
patient_quote = f'Patient reported: "{context_text}"'
|
| 1194 |
+
|
| 1195 |
+
# Combine paragraph and quote
|
| 1196 |
+
if patient_quote:
|
| 1197 |
+
return f"{coherent_paragraph}\n\n{patient_quote}"
|
| 1198 |
+
else:
|
| 1199 |
+
return coherent_paragraph
|
| 1200 |
+
|
| 1201 |
def generate_summary_with_validation(self, **kwargs) -> tuple[ProviderSummary, List[str]]:
|
| 1202 |
"""
|
| 1203 |
Generate provider summary with validation feedback.
|
|
|
|
| 1208 |
summary = self.generate_summary(**kwargs)
|
| 1209 |
validation_issues = summary.validate_completeness()
|
| 1210 |
return summary, validation_issues
|
| 1211 |
+
|
| 1212 |
+
def _create_fallback_summary(
|
| 1213 |
+
self,
|
| 1214 |
+
indicators: List[str],
|
| 1215 |
+
reasoning: str,
|
| 1216 |
+
confidence: float,
|
| 1217 |
+
patient_name: Optional[str],
|
| 1218 |
+
patient_phone: Optional[str]
|
| 1219 |
+
) -> ProviderSummary:
|
| 1220 |
+
"""
|
| 1221 |
+
Create a minimal fallback summary when generation fails.
|
| 1222 |
+
|
| 1223 |
+
Args:
|
| 1224 |
+
indicators: Original indicators
|
| 1225 |
+
reasoning: Original reasoning
|
| 1226 |
+
confidence: Original confidence
|
| 1227 |
+
patient_name: Original patient name
|
| 1228 |
+
patient_phone: Original patient phone
|
| 1229 |
+
|
| 1230 |
+
Returns:
|
| 1231 |
+
Minimal but functional ProviderSummary
|
| 1232 |
+
"""
|
| 1233 |
+
try:
|
| 1234 |
+
fallback_summary = ProviderSummary(
|
| 1235 |
+
patient_name=patient_name or "Patient (Name Not Available)",
|
| 1236 |
+
patient_phone=patient_phone or "Contact information not available",
|
| 1237 |
+
classification="RED",
|
| 1238 |
+
confidence=max(0.5, confidence), # Ensure reasonable confidence
|
| 1239 |
+
reasoning=reasoning or "RED flag classification - manual review required",
|
| 1240 |
+
indicators=indicators or ["System error - manual review required"],
|
| 1241 |
+
severity_level="HIGH",
|
| 1242 |
+
urgency_level="URGENT",
|
| 1243 |
+
situation_description="System error occurred during summary generation. Manual review required.",
|
| 1244 |
+
recommended_actions=[
|
| 1245 |
+
"Manual review required due to system error",
|
| 1246 |
+
"Contact patient within 24 hours",
|
| 1247 |
+
"Assess support needs"
|
| 1248 |
+
],
|
| 1249 |
+
follow_up_timeline="Within 24 hours"
|
| 1250 |
+
)
|
| 1251 |
+
|
| 1252 |
+
# Apply error handler fallbacks
|
| 1253 |
+
return self.error_handler.apply_fallback_template(fallback_summary, "general")
|
| 1254 |
+
|
| 1255 |
+
except Exception as e:
|
| 1256 |
+
logger.error(f"Even fallback summary creation failed: {e}")
|
| 1257 |
+
# Ultimate fallback - minimal summary
|
| 1258 |
+
return ProviderSummary(
|
| 1259 |
+
patient_name="System Error",
|
| 1260 |
+
patient_phone="Not Available",
|
| 1261 |
+
classification="RED",
|
| 1262 |
+
confidence=0.5,
|
| 1263 |
+
reasoning="System error - immediate manual review required",
|
| 1264 |
+
indicators=["System error"],
|
| 1265 |
+
severity_level="HIGH",
|
| 1266 |
+
urgency_level="URGENT",
|
| 1267 |
+
situation_description="Critical system error - immediate manual intervention required",
|
| 1268 |
+
recommended_actions=["Immediate manual review required"],
|
| 1269 |
+
follow_up_timeline="Immediately"
|
| 1270 |
+
)
|
| 1271 |
|
| 1272 |
|
| 1273 |
def create_provider_summary_generator() -> ProviderSummaryGenerator:
|
|
@@ -38,6 +38,8 @@ from src.core.core_classes import (
|
|
| 38 |
from src.core.consent_message_generator import ConsentMessageGenerator
|
| 39 |
from src.core.provider_summary_generator import ProviderSummaryGenerator, ProviderSummary
|
| 40 |
from src.config.prompt_management.performance_monitor import PromptMonitor
|
|
|
|
|
|
|
| 41 |
|
| 42 |
# Configure logging
|
| 43 |
logging.basicConfig(level=logging.INFO)
|
|
@@ -94,6 +96,10 @@ class SimplifiedMedicalApp:
|
|
| 94 |
self.consent_generator = ConsentMessageGenerator()
|
| 95 |
self.provider_summary_generator = ProviderSummaryGenerator(ai_client=self.api)
|
| 96 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
# Patient information (can be set via UI)
|
| 98 |
self.patient_info = {
|
| 99 |
"name": None,
|
|
@@ -340,7 +346,7 @@ class SimplifiedMedicalApp:
|
|
| 340 |
self.spiritual_state.triage_session.question_count += 1
|
| 341 |
|
| 342 |
# Evaluate response using triage logic
|
| 343 |
-
outcome, reasoning = self.soft_triage_manager.evaluate_response(
|
| 344 |
response=response,
|
| 345 |
triage_session=self.spiritual_state.triage_session,
|
| 346 |
context=self._get_conversation_context_str()
|
|
@@ -354,13 +360,13 @@ class SimplifiedMedicalApp:
|
|
| 354 |
|
| 355 |
elif outcome == TriageOutcome.ESCALATE_RED:
|
| 356 |
# Distress confirmed - generate referral (Requirement 3.5)
|
| 357 |
-
return self._escalate_to_red(reasoning)
|
| 358 |
|
| 359 |
else: # CONTINUE
|
| 360 |
# Need more information - ask another question
|
| 361 |
if self.soft_triage_manager.should_force_decision(self.spiritual_state.triage_session):
|
| 362 |
# Max questions reached - force decision (Requirement 3.6)
|
| 363 |
-
return self._escalate_to_red("Max triage questions reached - conservative escalation")
|
| 364 |
else:
|
| 365 |
# Update assessment for continuing triage
|
| 366 |
# Preserve original spiritual indicators from initial assessment
|
|
@@ -585,13 +591,17 @@ Is there anything else I can help you with today?"""
|
|
| 585 |
|
| 586 |
return transition
|
| 587 |
|
| 588 |
-
def _escalate_to_red(self, reasoning: str) -> str:
|
| 589 |
"""
|
| 590 |
Escalate triage to RED - request consent before referral.
|
| 591 |
|
| 592 |
Uses consent-based approach: asks patient for permission before
|
| 593 |
generating referral, as required by Or_review.md section 2.
|
| 594 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 595 |
Requirement: 3.5, Or_review.md section 2
|
| 596 |
"""
|
| 597 |
logger.info(f"Escalating to RED: {reasoning}")
|
|
@@ -603,14 +613,20 @@ Is there anything else I can help you with today?"""
|
|
| 603 |
patient_language = self._detect_language(last_response)
|
| 604 |
|
| 605 |
# Create assessment from triage context
|
| 606 |
-
#
|
| 607 |
-
|
| 608 |
-
if
|
| 609 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 610 |
|
| 611 |
assessment = SpiritualAssessment(
|
| 612 |
state=SpiritualState.RED,
|
| 613 |
-
indicators=
|
| 614 |
confidence=0.8,
|
| 615 |
reasoning=reasoning
|
| 616 |
)
|
|
@@ -951,6 +967,96 @@ Is there anything else I can help you with today?"""
|
|
| 951 |
|
| 952 |
return tracking
|
| 953 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 954 |
def _get_status_info(self) -> str:
|
| 955 |
"""Get current status information with enhanced patient details."""
|
| 956 |
state_emoji = {
|
|
|
|
| 38 |
from src.core.consent_message_generator import ConsentMessageGenerator
|
| 39 |
from src.core.provider_summary_generator import ProviderSummaryGenerator, ProviderSummary
|
| 40 |
from src.config.prompt_management.performance_monitor import PromptMonitor
|
| 41 |
+
from src.interface.enhanced_display_integration import create_enhanced_display_integration
|
| 42 |
+
from src.core.improved_classification_prompt_manager import ImprovedClassificationPromptManager
|
| 43 |
|
| 44 |
# Configure logging
|
| 45 |
logging.basicConfig(level=logging.INFO)
|
|
|
|
| 96 |
self.consent_generator = ConsentMessageGenerator()
|
| 97 |
self.provider_summary_generator = ProviderSummaryGenerator(ai_client=self.api)
|
| 98 |
|
| 99 |
+
# Enhanced display integration (Requirements 8.1, 8.2, 8.3)
|
| 100 |
+
self.enhanced_display = create_enhanced_display_integration()
|
| 101 |
+
self.improved_classification_manager = ImprovedClassificationPromptManager()
|
| 102 |
+
|
| 103 |
# Patient information (can be set via UI)
|
| 104 |
self.patient_info = {
|
| 105 |
"name": None,
|
|
|
|
| 346 |
self.spiritual_state.triage_session.question_count += 1
|
| 347 |
|
| 348 |
# Evaluate response using triage logic
|
| 349 |
+
outcome, reasoning, indicators = self.soft_triage_manager.evaluate_response(
|
| 350 |
response=response,
|
| 351 |
triage_session=self.spiritual_state.triage_session,
|
| 352 |
context=self._get_conversation_context_str()
|
|
|
|
| 360 |
|
| 361 |
elif outcome == TriageOutcome.ESCALATE_RED:
|
| 362 |
# Distress confirmed - generate referral (Requirement 3.5)
|
| 363 |
+
return self._escalate_to_red(reasoning, indicators)
|
| 364 |
|
| 365 |
else: # CONTINUE
|
| 366 |
# Need more information - ask another question
|
| 367 |
if self.soft_triage_manager.should_force_decision(self.spiritual_state.triage_session):
|
| 368 |
# Max questions reached - force decision (Requirement 3.6)
|
| 369 |
+
return self._escalate_to_red("Max triage questions reached - conservative escalation", ["inability_to_cope", "persistent_distress"])
|
| 370 |
else:
|
| 371 |
# Update assessment for continuing triage
|
| 372 |
# Preserve original spiritual indicators from initial assessment
|
|
|
|
| 591 |
|
| 592 |
return transition
|
| 593 |
|
| 594 |
+
def _escalate_to_red(self, reasoning: str, indicators: List[str] = None) -> str:
|
| 595 |
"""
|
| 596 |
Escalate triage to RED - request consent before referral.
|
| 597 |
|
| 598 |
Uses consent-based approach: asks patient for permission before
|
| 599 |
generating referral, as required by Or_review.md section 2.
|
| 600 |
|
| 601 |
+
Args:
|
| 602 |
+
reasoning: Reason for escalation
|
| 603 |
+
indicators: New indicators from triage evaluation (if available)
|
| 604 |
+
|
| 605 |
Requirement: 3.5, Or_review.md section 2
|
| 606 |
"""
|
| 607 |
logger.info(f"Escalating to RED: {reasoning}")
|
|
|
|
| 613 |
patient_language = self._detect_language(last_response)
|
| 614 |
|
| 615 |
# Create assessment from triage context
|
| 616 |
+
# Use new indicators from triage evaluation if available, otherwise fallback to original
|
| 617 |
+
assessment_indicators = indicators if indicators else []
|
| 618 |
+
if not assessment_indicators:
|
| 619 |
+
# Fallback to original indicators if no new ones provided
|
| 620 |
+
if self.spiritual_state.last_assessment and self.spiritual_state.last_assessment.indicators:
|
| 621 |
+
assessment_indicators = self.spiritual_state.last_assessment.indicators
|
| 622 |
+
else:
|
| 623 |
+
assessment_indicators = ["spiritual_distress"]
|
| 624 |
+
|
| 625 |
+
logger.info(f"Using indicators for RED assessment: {assessment_indicators}")
|
| 626 |
|
| 627 |
assessment = SpiritualAssessment(
|
| 628 |
state=SpiritualState.RED,
|
| 629 |
+
indicators=assessment_indicators,
|
| 630 |
confidence=0.8,
|
| 631 |
reasoning=reasoning
|
| 632 |
)
|
|
|
|
| 967 |
|
| 968 |
return tracking
|
| 969 |
|
| 970 |
+
def format_enhanced_response(
|
| 971 |
+
self,
|
| 972 |
+
patient_message: str,
|
| 973 |
+
assessment: Optional[SpiritualAssessment] = None,
|
| 974 |
+
provider_summary: Optional[ProviderSummary] = None,
|
| 975 |
+
show_ai_analysis: bool = True
|
| 976 |
+
) -> str:
|
| 977 |
+
"""
|
| 978 |
+
Format response using enhanced display components.
|
| 979 |
+
|
| 980 |
+
Args:
|
| 981 |
+
patient_message: The patient's message
|
| 982 |
+
assessment: Optional spiritual assessment
|
| 983 |
+
provider_summary: Optional provider summary for RED flags
|
| 984 |
+
show_ai_analysis: Whether to show AI analysis section
|
| 985 |
+
|
| 986 |
+
Returns:
|
| 987 |
+
Enhanced formatted HTML response
|
| 988 |
+
|
| 989 |
+
Requirements: 8.1, 8.2, 8.3
|
| 990 |
+
"""
|
| 991 |
+
return self.enhanced_display.format_chat_response(
|
| 992 |
+
assessment=assessment,
|
| 993 |
+
patient_message=patient_message,
|
| 994 |
+
provider_summary=provider_summary,
|
| 995 |
+
show_ai_analysis=show_ai_analysis
|
| 996 |
+
)
|
| 997 |
+
|
| 998 |
+
def get_enhanced_classification_badge(self, classification: str) -> str:
|
| 999 |
+
"""
|
| 1000 |
+
Get enhanced classification badge with improved styling.
|
| 1001 |
+
|
| 1002 |
+
Args:
|
| 1003 |
+
classification: The classification level (RED/YELLOW/GREEN)
|
| 1004 |
+
|
| 1005 |
+
Returns:
|
| 1006 |
+
HTML badge with enhanced styling
|
| 1007 |
+
|
| 1008 |
+
Requirements: 8.1, 8.2
|
| 1009 |
+
"""
|
| 1010 |
+
return self.enhanced_display.get_classification_badge(classification)
|
| 1011 |
+
|
| 1012 |
+
def validate_classification_consistency(
|
| 1013 |
+
self,
|
| 1014 |
+
assessment: SpiritualAssessment
|
| 1015 |
+
) -> bool:
|
| 1016 |
+
"""
|
| 1017 |
+
Validate classification consistency using improved criteria.
|
| 1018 |
+
|
| 1019 |
+
Args:
|
| 1020 |
+
assessment: The spiritual assessment to validate
|
| 1021 |
+
|
| 1022 |
+
Returns:
|
| 1023 |
+
True if classification is consistent with improved criteria
|
| 1024 |
+
|
| 1025 |
+
Requirements: 8.1, 8.2
|
| 1026 |
+
"""
|
| 1027 |
+
# Convert assessment to ClassificationResult format
|
| 1028 |
+
from src.core.improved_classification_prompt_manager import ClassificationResult
|
| 1029 |
+
|
| 1030 |
+
result = ClassificationResult(
|
| 1031 |
+
classification=assessment.state.value.lower(),
|
| 1032 |
+
confidence=assessment.confidence,
|
| 1033 |
+
indicators=assessment.indicators,
|
| 1034 |
+
reasoning=assessment.reasoning,
|
| 1035 |
+
red_flag_indicators=[], # Would need to be extracted from indicators
|
| 1036 |
+
yellow_flag_indicators=[], # Would need to be extracted from indicators
|
| 1037 |
+
is_valid=False
|
| 1038 |
+
)
|
| 1039 |
+
|
| 1040 |
+
return self.improved_classification_manager.validate_classification_consistency(result)
|
| 1041 |
+
|
| 1042 |
+
def get_enhanced_display_config(self) -> dict:
|
| 1043 |
+
"""
|
| 1044 |
+
Get current enhanced display configuration.
|
| 1045 |
+
|
| 1046 |
+
Returns:
|
| 1047 |
+
Dictionary with current display configuration
|
| 1048 |
+
|
| 1049 |
+
Requirements: 8.3
|
| 1050 |
+
"""
|
| 1051 |
+
config = self.enhanced_display.display_manager.config
|
| 1052 |
+
return {
|
| 1053 |
+
"ai_analysis_icon": config.ai_analysis_icon,
|
| 1054 |
+
"patient_message_icon": config.patient_message_icon,
|
| 1055 |
+
"provider_summary_icon": config.provider_summary_icon,
|
| 1056 |
+
"use_color_coding": config.use_color_coding,
|
| 1057 |
+
"classification_colors": config.classification_colors
|
| 1058 |
+
}
|
| 1059 |
+
|
| 1060 |
def _get_status_info(self) -> str:
|
| 1061 |
"""Get current status information with enhanced patient details."""
|
| 1062 |
state_emoji = {
|
|
@@ -129,7 +129,7 @@ class SoftTriageManager:
|
|
| 129 |
response: str,
|
| 130 |
triage_session: TriageSession,
|
| 131 |
context: str
|
| 132 |
-
) -> Tuple[TriageOutcome, str]:
|
| 133 |
"""
|
| 134 |
Evaluate patient's response to triage question.
|
| 135 |
|
|
@@ -142,7 +142,7 @@ class SoftTriageManager:
|
|
| 142 |
context: Conversation context
|
| 143 |
|
| 144 |
Returns:
|
| 145 |
-
Tuple of (TriageOutcome, reasoning)
|
| 146 |
|
| 147 |
Requirements: 3.3, 3.6
|
| 148 |
"""
|
|
@@ -168,8 +168,8 @@ class SoftTriageManager:
|
|
| 168 |
logger.error(f"Evaluation error: {e}")
|
| 169 |
# On error with max questions, escalate to RED (conservative)
|
| 170 |
if triage_session.question_count >= self.MAX_QUESTIONS - 1:
|
| 171 |
-
return TriageOutcome.ESCALATE_RED, f"Evaluation error at limit - conservative escalation: {e}"
|
| 172 |
-
return TriageOutcome.CONTINUE, f"Evaluation error - continuing: {e}"
|
| 173 |
|
| 174 |
def should_force_decision(self, triage_session: TriageSession) -> bool:
|
| 175 |
"""
|
|
@@ -232,7 +232,7 @@ class SoftTriageManager:
|
|
| 232 |
|
| 233 |
return "\n".join(prompt_parts)
|
| 234 |
|
| 235 |
-
def _parse_evaluation_response(self, response: str) -> Tuple[TriageOutcome, str]:
|
| 236 |
"""Parse LLM evaluation response."""
|
| 237 |
try:
|
| 238 |
# Extract JSON
|
|
@@ -244,23 +244,28 @@ class SoftTriageManager:
|
|
| 244 |
|
| 245 |
outcome_str = data.get("outcome", "continue").lower()
|
| 246 |
reasoning = data.get("reasoning", "LLM evaluation")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 247 |
|
| 248 |
if outcome_str == "resolved_green":
|
| 249 |
-
return TriageOutcome.RESOLVED_GREEN, reasoning
|
| 250 |
elif outcome_str == "escalate_red":
|
| 251 |
-
return TriageOutcome.ESCALATE_RED, reasoning
|
| 252 |
else:
|
| 253 |
-
return TriageOutcome.CONTINUE, reasoning
|
| 254 |
|
| 255 |
except (json.JSONDecodeError, KeyError) as e:
|
| 256 |
logger.warning(f"Failed to parse evaluation: {e}")
|
| 257 |
-
return TriageOutcome.CONTINUE, f"Parse error - continuing: {e}"
|
| 258 |
|
| 259 |
def _force_decision(
|
| 260 |
self,
|
| 261 |
last_response: str,
|
| 262 |
triage_session: TriageSession
|
| 263 |
-
) -> Tuple[TriageOutcome, str]:
|
| 264 |
"""
|
| 265 |
Force a decision when max questions reached.
|
| 266 |
|
|
@@ -279,10 +284,10 @@ class SoftTriageManager:
|
|
| 279 |
positive_count = sum(1 for ind in positive_indicators if ind in all_responses)
|
| 280 |
|
| 281 |
if positive_count >= 2:
|
| 282 |
-
return TriageOutcome.RESOLVED_GREEN, "Forced decision: positive indicators found in responses"
|
| 283 |
else:
|
| 284 |
# Conservative: escalate if uncertain (Requirement 3.6)
|
| 285 |
-
return TriageOutcome.ESCALATE_RED, "Forced decision: max questions reached without clear resolution - conservative escalation"
|
| 286 |
|
| 287 |
def _get_fallback_question(self, language: str, question_num: int) -> str:
|
| 288 |
"""Get fallback question if LLM fails."""
|
|
|
|
| 129 |
response: str,
|
| 130 |
triage_session: TriageSession,
|
| 131 |
context: str
|
| 132 |
+
) -> Tuple[TriageOutcome, str, List[str]]:
|
| 133 |
"""
|
| 134 |
Evaluate patient's response to triage question.
|
| 135 |
|
|
|
|
| 142 |
context: Conversation context
|
| 143 |
|
| 144 |
Returns:
|
| 145 |
+
Tuple of (TriageOutcome, reasoning, indicators)
|
| 146 |
|
| 147 |
Requirements: 3.3, 3.6
|
| 148 |
"""
|
|
|
|
| 168 |
logger.error(f"Evaluation error: {e}")
|
| 169 |
# On error with max questions, escalate to RED (conservative)
|
| 170 |
if triage_session.question_count >= self.MAX_QUESTIONS - 1:
|
| 171 |
+
return TriageOutcome.ESCALATE_RED, f"Evaluation error at limit - conservative escalation: {e}", ["evaluation_error"]
|
| 172 |
+
return TriageOutcome.CONTINUE, f"Evaluation error - continuing: {e}", ["evaluation_error"]
|
| 173 |
|
| 174 |
def should_force_decision(self, triage_session: TriageSession) -> bool:
|
| 175 |
"""
|
|
|
|
| 232 |
|
| 233 |
return "\n".join(prompt_parts)
|
| 234 |
|
| 235 |
+
def _parse_evaluation_response(self, response: str) -> Tuple[TriageOutcome, str, List[str]]:
|
| 236 |
"""Parse LLM evaluation response."""
|
| 237 |
try:
|
| 238 |
# Extract JSON
|
|
|
|
| 244 |
|
| 245 |
outcome_str = data.get("outcome", "continue").lower()
|
| 246 |
reasoning = data.get("reasoning", "LLM evaluation")
|
| 247 |
+
indicators = data.get("indicators", [])
|
| 248 |
+
|
| 249 |
+
# Ensure indicators is a list
|
| 250 |
+
if not isinstance(indicators, list):
|
| 251 |
+
indicators = []
|
| 252 |
|
| 253 |
if outcome_str == "resolved_green":
|
| 254 |
+
return TriageOutcome.RESOLVED_GREEN, reasoning, indicators
|
| 255 |
elif outcome_str == "escalate_red":
|
| 256 |
+
return TriageOutcome.ESCALATE_RED, reasoning, indicators
|
| 257 |
else:
|
| 258 |
+
return TriageOutcome.CONTINUE, reasoning, indicators
|
| 259 |
|
| 260 |
except (json.JSONDecodeError, KeyError) as e:
|
| 261 |
logger.warning(f"Failed to parse evaluation: {e}")
|
| 262 |
+
return TriageOutcome.CONTINUE, f"Parse error - continuing: {e}", ["parse_error"]
|
| 263 |
|
| 264 |
def _force_decision(
|
| 265 |
self,
|
| 266 |
last_response: str,
|
| 267 |
triage_session: TriageSession
|
| 268 |
+
) -> Tuple[TriageOutcome, str, List[str]]:
|
| 269 |
"""
|
| 270 |
Force a decision when max questions reached.
|
| 271 |
|
|
|
|
| 284 |
positive_count = sum(1 for ind in positive_indicators if ind in all_responses)
|
| 285 |
|
| 286 |
if positive_count >= 2:
|
| 287 |
+
return TriageOutcome.RESOLVED_GREEN, "Forced decision: positive indicators found in responses", ["external_factors", "coping_resources"]
|
| 288 |
else:
|
| 289 |
# Conservative: escalate if uncertain (Requirement 3.6)
|
| 290 |
+
return TriageOutcome.ESCALATE_RED, "Forced decision: max questions reached without clear resolution - conservative escalation", ["inability_to_cope", "persistent_distress"]
|
| 291 |
|
| 292 |
def _get_fallback_question(self, language: str, question_num: int) -> str:
|
| 293 |
"""Get fallback question if LLM fails."""
|
|
@@ -6,7 +6,12 @@ Analyzes every patient message for spiritual distress indicators and returns
|
|
| 6 |
a simple GREEN/YELLOW/RED classification. Runs invisibly in the background
|
| 7 |
during medical dialog.
|
| 8 |
|
| 9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
"""
|
| 11 |
|
| 12 |
import logging
|
|
@@ -17,51 +22,13 @@ from typing import List, Optional
|
|
| 17 |
|
| 18 |
from src.core.spiritual_state import SpiritualState, SpiritualAssessment
|
| 19 |
from src.core.ai_client import AIClientManager
|
|
|
|
| 20 |
|
| 21 |
# Configure logging
|
| 22 |
logging.basicConfig(level=logging.INFO)
|
| 23 |
logger = logging.getLogger(__name__)
|
| 24 |
|
| 25 |
|
| 26 |
-
# Red flag keywords that trigger immediate RED classification
|
| 27 |
-
RED_FLAG_KEYWORDS = [
|
| 28 |
-
# Suicidal ideation (English)
|
| 29 |
-
"suicide", "suicidal", "kill myself", "end my life", "want to die",
|
| 30 |
-
"better off dead", "no reason to live", "can't go on",
|
| 31 |
-
# Suicidal ideation (Ukrainian)
|
| 32 |
-
"самогубство", "покінчити з життям", "хочу померти", "не хочу жити",
|
| 33 |
-
"краще б мене не було", "немає сенсу жити",
|
| 34 |
-
# Severe hopelessness (English)
|
| 35 |
-
"hopeless", "no hope", "give up", "nothing matters",
|
| 36 |
-
# Severe hopelessness (Ukrainian)
|
| 37 |
-
"безнадія", "немає надії", "все безглуздо", "нічого не має сенсу",
|
| 38 |
-
# Death wishes (English)
|
| 39 |
-
"wish i was dead", "want to disappear", "end it all",
|
| 40 |
-
# Death wishes (Ukrainian)
|
| 41 |
-
"хочу зникнути", "краще б я помер"
|
| 42 |
-
]
|
| 43 |
-
|
| 44 |
-
# Yellow flag indicators for potential distress
|
| 45 |
-
YELLOW_FLAG_INDICATORS = [
|
| 46 |
-
# Emotional distress (English)
|
| 47 |
-
"sad", "depressed", "anxious", "worried", "scared", "lonely",
|
| 48 |
-
"overwhelmed", "stressed", "can't cope", "struggling",
|
| 49 |
-
# Emotional distress (Ukrainian)
|
| 50 |
-
"сумно", "депресія", "тривога", "хвилююсь", "страшно", "самотньо",
|
| 51 |
-
"перевантажений", "стрес", "не справляюсь", "важко",
|
| 52 |
-
# Spiritual/existential (English)
|
| 53 |
-
"god", "faith", "pray", "meaning", "purpose", "why me",
|
| 54 |
-
"spiritual", "soul", "afterlife", "sin", "guilt",
|
| 55 |
-
# Spiritual/existential (Ukrainian)
|
| 56 |
-
"бог", "віра", "молитва", "сенс", "мета", "чому я",
|
| 57 |
-
"духовний", "душа", "гріх", "провина",
|
| 58 |
-
# Grief/loss (English)
|
| 59 |
-
"grief", "loss", "died", "death", "mourning", "miss them",
|
| 60 |
-
# Grief/loss (Ukrainian)
|
| 61 |
-
"горе", "втрата", "помер", "смерть", "сумую", "скучаю"
|
| 62 |
-
]
|
| 63 |
-
|
| 64 |
-
|
| 65 |
# Load system prompt from external file
|
| 66 |
def _load_spiritual_monitor_prompt() -> str:
|
| 67 |
"""Load the spiritual monitor system prompt from file."""
|
|
@@ -70,15 +37,33 @@ def _load_spiritual_monitor_prompt() -> str:
|
|
| 70 |
return load_prompt_from_file('spiritual_monitor.txt')
|
| 71 |
except (ImportError, FileNotFoundError) as e:
|
| 72 |
logger.warning(f"Could not load spiritual monitor prompt from file: {e}")
|
| 73 |
-
# Fallback to
|
| 74 |
return """<system_role>
|
| 75 |
You are a background spiritual distress classifier for a medical chatbot. Your task is to analyze patient messages and classify their level of spiritual or emotional distress to help route them to appropriate support.
|
| 76 |
</system_role>
|
| 77 |
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
|
| 83 |
Respond with JSON: {"state": "green|yellow|red", "indicators": [], "confidence": 0.0-1.0, "reasoning": "explanation"}"""
|
| 84 |
|
|
@@ -91,9 +76,14 @@ class SpiritualMonitor:
|
|
| 91 |
Background classifier for spiritual distress detection.
|
| 92 |
|
| 93 |
Analyzes every patient message and returns GREEN/YELLOW/RED classification.
|
| 94 |
-
Uses
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
|
| 96 |
-
Requirements: 2.1, 5.1, 5.2, 5.4
|
| 97 |
"""
|
| 98 |
|
| 99 |
def __init__(self, api_client: AIClientManager, performance_monitor=None):
|
|
@@ -106,7 +96,8 @@ class SpiritualMonitor:
|
|
| 106 |
"""
|
| 107 |
self.api = api_client
|
| 108 |
self.performance_monitor = performance_monitor
|
| 109 |
-
|
|
|
|
| 110 |
|
| 111 |
def classify(
|
| 112 |
self,
|
|
@@ -114,10 +105,12 @@ class SpiritualMonitor:
|
|
| 114 |
conversation_history: Optional[List[str]] = None
|
| 115 |
) -> SpiritualAssessment:
|
| 116 |
"""
|
| 117 |
-
Classify message for spiritual distress indicators.
|
| 118 |
|
| 119 |
-
|
| 120 |
-
|
|
|
|
|
|
|
| 121 |
|
| 122 |
Args:
|
| 123 |
message: Patient's message to classify
|
|
@@ -126,9 +119,9 @@ class SpiritualMonitor:
|
|
| 126 |
Returns:
|
| 127 |
SpiritualAssessment with state, indicators, confidence, reasoning
|
| 128 |
|
| 129 |
-
Requirements: 2.1, 5.1, 5.2, 5.4, 8.1, 8.2
|
| 130 |
"""
|
| 131 |
-
logger.info(f"Classifying message: {message[:50]}...")
|
| 132 |
|
| 133 |
# Start performance monitoring (Requirement 8.1)
|
| 134 |
start_time = time.time()
|
|
@@ -136,21 +129,10 @@ class SpiritualMonitor:
|
|
| 136 |
error_details = None
|
| 137 |
|
| 138 |
try:
|
| 139 |
-
#
|
| 140 |
-
|
| 141 |
-
if red_flag_result:
|
| 142 |
-
logger.warning(f"RED FLAG detected via keywords: {red_flag_result}")
|
| 143 |
-
assessment = SpiritualAssessment(
|
| 144 |
-
state=SpiritualState.RED,
|
| 145 |
-
indicators=red_flag_result,
|
| 146 |
-
confidence=1.0,
|
| 147 |
-
reasoning="Red flag keywords detected - immediate support needed"
|
| 148 |
-
)
|
| 149 |
-
else:
|
| 150 |
-
# Step 2: Use LLM for nuanced classification
|
| 151 |
-
assessment = self._classify_with_llm(message, conversation_history)
|
| 152 |
-
logger.info(f"LLM classification: {assessment.state.value}")
|
| 153 |
|
|
|
|
| 154 |
return assessment
|
| 155 |
|
| 156 |
except Exception as e:
|
|
@@ -183,72 +165,58 @@ class SpiritualMonitor:
|
|
| 183 |
'indicators_count': len(getattr(assessment, 'indicators', [])) if 'assessment' in locals() else 0,
|
| 184 |
'message_length': len(message),
|
| 185 |
'has_conversation_history': conversation_history is not None,
|
| 186 |
-
'error_details': error_details
|
|
|
|
| 187 |
}
|
| 188 |
)
|
| 189 |
|
| 190 |
-
def
|
| 191 |
-
"""
|
| 192 |
-
Check message for red flag keywords.
|
| 193 |
-
|
| 194 |
-
Returns list of matched keywords if found, None otherwise.
|
| 195 |
-
|
| 196 |
-
Requirement: 5.4
|
| 197 |
-
"""
|
| 198 |
-
message_lower = message.lower()
|
| 199 |
-
matched = []
|
| 200 |
-
|
| 201 |
-
for keyword in RED_FLAG_KEYWORDS:
|
| 202 |
-
if keyword.lower() in message_lower:
|
| 203 |
-
matched.append(keyword)
|
| 204 |
-
|
| 205 |
-
return matched if matched else None
|
| 206 |
-
|
| 207 |
-
def _check_yellow_indicators(self, message: str) -> List[str]:
|
| 208 |
-
"""
|
| 209 |
-
Check message for yellow flag indicators.
|
| 210 |
-
|
| 211 |
-
Returns list of matched indicators.
|
| 212 |
-
"""
|
| 213 |
-
message_lower = message.lower()
|
| 214 |
-
matched = []
|
| 215 |
-
|
| 216 |
-
for indicator in YELLOW_FLAG_INDICATORS:
|
| 217 |
-
if indicator.lower() in message_lower:
|
| 218 |
-
matched.append(indicator)
|
| 219 |
-
|
| 220 |
-
return matched
|
| 221 |
-
|
| 222 |
-
def _classify_with_llm(
|
| 223 |
self,
|
| 224 |
message: str,
|
| 225 |
conversation_history: Optional[List[str]] = None
|
| 226 |
) -> SpiritualAssessment:
|
| 227 |
"""
|
| 228 |
-
Use LLM for nuanced classification.
|
|
|
|
|
|
|
| 229 |
|
| 230 |
-
Requirements: 2
|
| 231 |
"""
|
| 232 |
# Build context
|
| 233 |
context = ""
|
| 234 |
if conversation_history:
|
| 235 |
-
|
| 236 |
-
context = "Recent conversation:\n
|
| 237 |
|
| 238 |
-
|
|
|
|
|
|
|
|
|
|
| 239 |
"{message}"
|
| 240 |
|
| 241 |
-
|
| 242 |
|
| 243 |
-
# Call LLM
|
| 244 |
response = self.api.call_spiritual_api(
|
| 245 |
-
system_prompt=
|
| 246 |
user_prompt=prompt
|
| 247 |
)
|
| 248 |
|
| 249 |
# Parse response
|
| 250 |
return self._parse_classification_response(response, message)
|
| 251 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
def _parse_classification_response(
|
| 253 |
self,
|
| 254 |
response: str,
|
|
@@ -257,7 +225,7 @@ Classify this message for spiritual/emotional distress indicators."""
|
|
| 257 |
"""
|
| 258 |
Parse LLM classification response.
|
| 259 |
|
| 260 |
-
Falls back to
|
| 261 |
|
| 262 |
Requirement: 5.2 (conservative default)
|
| 263 |
"""
|
|
@@ -269,8 +237,8 @@ Classify this message for spiritual/emotional distress indicators."""
|
|
| 269 |
else:
|
| 270 |
data = json.loads(response)
|
| 271 |
|
| 272 |
-
# Parse state
|
| 273 |
-
state_str = data.get("state", "yellow").lower()
|
| 274 |
if state_str == "red":
|
| 275 |
state = SpiritualState.RED
|
| 276 |
elif state_str == "green":
|
|
@@ -285,37 +253,27 @@ Classify this message for spiritual/emotional distress indicators."""
|
|
| 285 |
reasoning=data.get("reasoning", "LLM classification")
|
| 286 |
)
|
| 287 |
|
| 288 |
-
except (json.JSONDecodeError,
|
| 289 |
logger.warning(f"Failed to parse LLM response: {e}")
|
| 290 |
|
| 291 |
-
#
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
confidence=0.6,
|
| 299 |
-
reasoning="Keyword-based classification (LLM parse failed)"
|
| 300 |
-
)
|
| 301 |
-
else:
|
| 302 |
-
# Conservative: default to YELLOW if uncertain
|
| 303 |
-
return SpiritualAssessment(
|
| 304 |
-
state=SpiritualState.YELLOW,
|
| 305 |
-
indicators=["parse_error"],
|
| 306 |
-
confidence=0.5,
|
| 307 |
-
reasoning="Conservative YELLOW default (LLM parse failed)"
|
| 308 |
-
)
|
| 309 |
|
| 310 |
|
| 311 |
-
def create_spiritual_monitor(api_client: AIClientManager) -> SpiritualMonitor:
|
| 312 |
"""
|
| 313 |
-
Factory function to create SpiritualMonitor.
|
| 314 |
|
| 315 |
Args:
|
| 316 |
api_client: AI client manager
|
|
|
|
| 317 |
|
| 318 |
Returns:
|
| 319 |
Initialized SpiritualMonitor instance
|
| 320 |
"""
|
| 321 |
-
return SpiritualMonitor(api_client)
|
|
|
|
| 6 |
a simple GREEN/YELLOW/RED classification. Runs invisibly in the background
|
| 7 |
during medical dialog.
|
| 8 |
|
| 9 |
+
Enhanced with improved classification logic for specific cases:
|
| 10 |
+
- Proper handling of "feeling sad" (Requirements 3.1, 6.1)
|
| 11 |
+
- Recognition of "loss of meaning and purpose" as red indicator (Requirement 3.2)
|
| 12 |
+
- Validation for yellow flags in unclear cases (Requirements 6.2, 6.3)
|
| 13 |
+
|
| 14 |
+
Requirements: 2.1, 5.1, 5.2, 5.4, 3.1, 3.2, 6.1, 6.2, 6.3
|
| 15 |
"""
|
| 16 |
|
| 17 |
import logging
|
|
|
|
| 22 |
|
| 23 |
from src.core.spiritual_state import SpiritualState, SpiritualAssessment
|
| 24 |
from src.core.ai_client import AIClientManager
|
| 25 |
+
from src.core.improved_classification_prompt_manager import ImprovedClassificationPromptManager
|
| 26 |
|
| 27 |
# Configure logging
|
| 28 |
logging.basicConfig(level=logging.INFO)
|
| 29 |
logger = logging.getLogger(__name__)
|
| 30 |
|
| 31 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
# Load system prompt from external file
|
| 33 |
def _load_spiritual_monitor_prompt() -> str:
|
| 34 |
"""Load the spiritual monitor system prompt from file."""
|
|
|
|
| 37 |
return load_prompt_from_file('spiritual_monitor.txt')
|
| 38 |
except (ImportError, FileNotFoundError) as e:
|
| 39 |
logger.warning(f"Could not load spiritual monitor prompt from file: {e}")
|
| 40 |
+
# Fallback to improved prompt with enhanced classification logic
|
| 41 |
return """<system_role>
|
| 42 |
You are a background spiritual distress classifier for a medical chatbot. Your task is to analyze patient messages and classify their level of spiritual or emotional distress to help route them to appropriate support.
|
| 43 |
</system_role>
|
| 44 |
|
| 45 |
+
<enhanced_classification_rules>
|
| 46 |
+
CRITICAL IMPROVEMENTS:
|
| 47 |
+
1. Simple sadness without additional spiritual context should be YELLOW, not RED
|
| 48 |
+
2. Loss of meaning and purpose should ALWAYS be RED
|
| 49 |
+
3. Use YELLOW for unclear cases requiring further investigation
|
| 50 |
+
|
| 51 |
+
RED FLAGS (immediate RED classification):
|
| 52 |
+
- Suicidal ideation or death wishes
|
| 53 |
+
- Loss of meaning, purpose, or sense of life
|
| 54 |
+
- Severe spiritual distress with crisis language
|
| 55 |
+
- Complex grief with spiritual components
|
| 56 |
+
|
| 57 |
+
YELLOW FLAGS (need clarification):
|
| 58 |
+
- Simple sadness without spiritual context
|
| 59 |
+
- Unclear emotional states requiring investigation
|
| 60 |
+
- Ambiguous situations where cause of distress is uncertain
|
| 61 |
+
|
| 62 |
+
GREEN (no distress):
|
| 63 |
+
- Medical questions without emotional distress
|
| 64 |
+
- Routine healthcare topics
|
| 65 |
+
- Positive or neutral statements
|
| 66 |
+
</enhanced_classification_rules>
|
| 67 |
|
| 68 |
Respond with JSON: {"state": "green|yellow|red", "indicators": [], "confidence": 0.0-1.0, "reasoning": "explanation"}"""
|
| 69 |
|
|
|
|
| 76 |
Background classifier for spiritual distress detection.
|
| 77 |
|
| 78 |
Analyzes every patient message and returns GREEN/YELLOW/RED classification.
|
| 79 |
+
Uses LLM-only classification with enhanced prompts for accurate assessment.
|
| 80 |
+
|
| 81 |
+
Enhanced with improved classification logic:
|
| 82 |
+
- Proper handling of simple sadness (Requirements 3.1, 6.1)
|
| 83 |
+
- Recognition of loss of meaning/purpose as red (Requirement 3.2)
|
| 84 |
+
- Validation for yellow flags in unclear cases (Requirements 6.2, 6.3)
|
| 85 |
|
| 86 |
+
Requirements: 2.1, 5.1, 5.2, 5.4, 3.1, 3.2, 6.1, 6.2, 6.3
|
| 87 |
"""
|
| 88 |
|
| 89 |
def __init__(self, api_client: AIClientManager, performance_monitor=None):
|
|
|
|
| 96 |
"""
|
| 97 |
self.api = api_client
|
| 98 |
self.performance_monitor = performance_monitor
|
| 99 |
+
self.improved_prompt_manager = ImprovedClassificationPromptManager()
|
| 100 |
+
logger.info("🔍 SpiritualMonitor initialized with improved classification logic")
|
| 101 |
|
| 102 |
def classify(
|
| 103 |
self,
|
|
|
|
| 105 |
conversation_history: Optional[List[str]] = None
|
| 106 |
) -> SpiritualAssessment:
|
| 107 |
"""
|
| 108 |
+
Classify message for spiritual distress indicators using LLM only.
|
| 109 |
|
| 110 |
+
Uses enhanced LLM classification with improved prompt that includes:
|
| 111 |
+
- Updated red flag definition (broader than crisis-only)
|
| 112 |
+
- Explicit red indicators for consistency
|
| 113 |
+
- Proper handling of simple sadness vs spiritual distress
|
| 114 |
|
| 115 |
Args:
|
| 116 |
message: Patient's message to classify
|
|
|
|
| 119 |
Returns:
|
| 120 |
SpiritualAssessment with state, indicators, confidence, reasoning
|
| 121 |
|
| 122 |
+
Requirements: 2.1, 5.1, 5.2, 5.4, 8.1, 8.2, 3.1, 3.2, 6.1, 6.2, 6.3
|
| 123 |
"""
|
| 124 |
+
logger.info(f"Classifying message with LLM-only logic: {message[:50]}...")
|
| 125 |
|
| 126 |
# Start performance monitoring (Requirement 8.1)
|
| 127 |
start_time = time.time()
|
|
|
|
| 129 |
error_details = None
|
| 130 |
|
| 131 |
try:
|
| 132 |
+
# Use enhanced LLM classification for all messages
|
| 133 |
+
assessment = self._classify_with_enhanced_llm(message, conversation_history)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
|
| 135 |
+
logger.info(f"LLM classification result: {assessment.state.value}")
|
| 136 |
return assessment
|
| 137 |
|
| 138 |
except Exception as e:
|
|
|
|
| 165 |
'indicators_count': len(getattr(assessment, 'indicators', [])) if 'assessment' in locals() else 0,
|
| 166 |
'message_length': len(message),
|
| 167 |
'has_conversation_history': conversation_history is not None,
|
| 168 |
+
'error_details': error_details,
|
| 169 |
+
'llm_only_logic_used': True
|
| 170 |
}
|
| 171 |
)
|
| 172 |
|
| 173 |
+
def _classify_with_enhanced_llm(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
self,
|
| 175 |
message: str,
|
| 176 |
conversation_history: Optional[List[str]] = None
|
| 177 |
) -> SpiritualAssessment:
|
| 178 |
"""
|
| 179 |
+
Use enhanced LLM for nuanced classification.
|
| 180 |
+
|
| 181 |
+
Uses improved prompt with enhanced classification logic.
|
| 182 |
|
| 183 |
+
Requirements: 6.2, 6.3
|
| 184 |
"""
|
| 185 |
# Build context
|
| 186 |
context = ""
|
| 187 |
if conversation_history:
|
| 188 |
+
recent_history = conversation_history[-3:] # Last 3 messages for context
|
| 189 |
+
context = f"Recent conversation:\n{chr(10).join(recent_history)}\n\n"
|
| 190 |
|
| 191 |
+
# Use improved prompt manager
|
| 192 |
+
enhanced_prompt = self.improved_prompt_manager.build_enhanced_classification_prompt()
|
| 193 |
+
|
| 194 |
+
prompt = f"""Please classify this patient message:
|
| 195 |
"{message}"
|
| 196 |
|
| 197 |
+
Apply the enhanced classification rules provided in the system prompt."""
|
| 198 |
|
| 199 |
+
# Call LLM with enhanced prompt
|
| 200 |
response = self.api.call_spiritual_api(
|
| 201 |
+
system_prompt=enhanced_prompt,
|
| 202 |
user_prompt=prompt
|
| 203 |
)
|
| 204 |
|
| 205 |
# Parse response
|
| 206 |
return self._parse_classification_response(response, message)
|
| 207 |
|
| 208 |
+
def _classify_with_llm(
|
| 209 |
+
self,
|
| 210 |
+
message: str,
|
| 211 |
+
conversation_history: Optional[List[str]] = None
|
| 212 |
+
) -> SpiritualAssessment:
|
| 213 |
+
"""
|
| 214 |
+
Legacy LLM classification method.
|
| 215 |
+
|
| 216 |
+
Requirement: 5.2
|
| 217 |
+
"""
|
| 218 |
+
return self._classify_with_enhanced_llm(message, conversation_history)
|
| 219 |
+
|
| 220 |
def _parse_classification_response(
|
| 221 |
self,
|
| 222 |
response: str,
|
|
|
|
| 225 |
"""
|
| 226 |
Parse LLM classification response.
|
| 227 |
|
| 228 |
+
Falls back to conservative YELLOW on parse errors.
|
| 229 |
|
| 230 |
Requirement: 5.2 (conservative default)
|
| 231 |
"""
|
|
|
|
| 237 |
else:
|
| 238 |
data = json.loads(response)
|
| 239 |
|
| 240 |
+
# Parse state - check both "classification" and "state" fields
|
| 241 |
+
state_str = data.get("classification", data.get("state", "yellow")).lower()
|
| 242 |
if state_str == "red":
|
| 243 |
state = SpiritualState.RED
|
| 244 |
elif state_str == "green":
|
|
|
|
| 253 |
reasoning=data.get("reasoning", "LLM classification")
|
| 254 |
)
|
| 255 |
|
| 256 |
+
except (json.JSONDecodeError, ValueError, KeyError) as e:
|
| 257 |
logger.warning(f"Failed to parse LLM response: {e}")
|
| 258 |
|
| 259 |
+
# Conservative: default to YELLOW if uncertain
|
| 260 |
+
return SpiritualAssessment(
|
| 261 |
+
state=SpiritualState.YELLOW,
|
| 262 |
+
indicators=["parse_error"],
|
| 263 |
+
confidence=0.5,
|
| 264 |
+
reasoning="Conservative YELLOW default due to parse error"
|
| 265 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 266 |
|
| 267 |
|
| 268 |
+
def create_spiritual_monitor(api_client: AIClientManager, performance_monitor=None) -> SpiritualMonitor:
|
| 269 |
"""
|
| 270 |
+
Factory function to create SpiritualMonitor instance.
|
| 271 |
|
| 272 |
Args:
|
| 273 |
api_client: AI client manager
|
| 274 |
+
performance_monitor: Optional performance monitor
|
| 275 |
|
| 276 |
Returns:
|
| 277 |
Initialized SpiritualMonitor instance
|
| 278 |
"""
|
| 279 |
+
return SpiritualMonitor(api_client, performance_monitor)
|
|
@@ -0,0 +1,687 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
UI Error Handler for UI Classification Improvements.
|
| 4 |
+
|
| 5 |
+
Provides error handling and recovery mechanisms for UI components including:
|
| 6 |
+
- Provider summary structure validation
|
| 7 |
+
- Fallback templates for missing data
|
| 8 |
+
- Functionality degradation during errors
|
| 9 |
+
|
| 10 |
+
Requirements: 9.1, 9.2, 9.3, 9.4
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import logging
|
| 14 |
+
from typing import Dict, List, Optional, Any, Tuple, Union
|
| 15 |
+
from dataclasses import dataclass, field as dataclass_field
|
| 16 |
+
from enum import Enum
|
| 17 |
+
import traceback
|
| 18 |
+
import json
|
| 19 |
+
|
| 20 |
+
from src.core.provider_summary_generator import ProviderSummary
|
| 21 |
+
from src.core.improved_classification_prompt_manager import ClassificationResult
|
| 22 |
+
|
| 23 |
+
# Configure logging
|
| 24 |
+
logging.basicConfig(level=logging.INFO)
|
| 25 |
+
logger = logging.getLogger(__name__)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class ErrorSeverity(Enum):
|
| 29 |
+
"""Severity levels for UI errors."""
|
| 30 |
+
LOW = "low"
|
| 31 |
+
MEDIUM = "medium"
|
| 32 |
+
HIGH = "high"
|
| 33 |
+
CRITICAL = "critical"
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class ErrorCategory(Enum):
|
| 37 |
+
"""Categories of UI errors."""
|
| 38 |
+
VALIDATION = "validation"
|
| 39 |
+
FORMATTING = "formatting"
|
| 40 |
+
DATA_MISSING = "data_missing"
|
| 41 |
+
DISPLAY = "display"
|
| 42 |
+
CLASSIFICATION = "classification"
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
@dataclass
|
| 46 |
+
class UIError:
|
| 47 |
+
"""Represents a UI error with context and recovery information."""
|
| 48 |
+
category: ErrorCategory
|
| 49 |
+
severity: ErrorSeverity
|
| 50 |
+
message: str
|
| 51 |
+
component: str
|
| 52 |
+
field: Optional[str] = None
|
| 53 |
+
value: Optional[Any] = None
|
| 54 |
+
suggestion: Optional[str] = None
|
| 55 |
+
recovery_actions: List[str] = dataclass_field(default_factory=list)
|
| 56 |
+
timestamp: Optional[str] = None
|
| 57 |
+
|
| 58 |
+
def to_dict(self) -> Dict[str, Any]:
|
| 59 |
+
"""Convert error to dictionary for logging/export."""
|
| 60 |
+
return {
|
| 61 |
+
"category": self.category.value,
|
| 62 |
+
"severity": self.severity.value,
|
| 63 |
+
"message": self.message,
|
| 64 |
+
"component": self.component,
|
| 65 |
+
"field": self.field,
|
| 66 |
+
"value": str(self.value) if self.value is not None else None,
|
| 67 |
+
"suggestion": self.suggestion,
|
| 68 |
+
"recovery_actions": self.recovery_actions,
|
| 69 |
+
"timestamp": self.timestamp
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
@dataclass
|
| 74 |
+
class ValidationResult:
|
| 75 |
+
"""Result of validation with errors and warnings."""
|
| 76 |
+
is_valid: bool
|
| 77 |
+
errors: List[UIError] = dataclass_field(default_factory=list)
|
| 78 |
+
warnings: List[UIError] = dataclass_field(default_factory=list)
|
| 79 |
+
|
| 80 |
+
def add_error(self, error: UIError):
|
| 81 |
+
"""Add an error to the validation result."""
|
| 82 |
+
self.errors.append(error)
|
| 83 |
+
self.is_valid = False
|
| 84 |
+
|
| 85 |
+
def add_warning(self, warning: UIError):
|
| 86 |
+
"""Add a warning to the validation result."""
|
| 87 |
+
self.warnings.append(warning)
|
| 88 |
+
|
| 89 |
+
def has_critical_errors(self) -> bool:
|
| 90 |
+
"""Check if there are any critical errors."""
|
| 91 |
+
return any(error.severity == ErrorSeverity.CRITICAL for error in self.errors)
|
| 92 |
+
|
| 93 |
+
def get_error_summary(self) -> str:
|
| 94 |
+
"""Get a summary of all errors and warnings."""
|
| 95 |
+
if not self.errors and not self.warnings:
|
| 96 |
+
return "No validation issues found."
|
| 97 |
+
|
| 98 |
+
parts = []
|
| 99 |
+
if self.errors:
|
| 100 |
+
parts.append(f"{len(self.errors)} error(s)")
|
| 101 |
+
if self.warnings:
|
| 102 |
+
parts.append(f"{len(self.warnings)} warning(s)")
|
| 103 |
+
|
| 104 |
+
return f"Validation completed with {', '.join(parts)}."
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
class UIErrorHandler:
|
| 108 |
+
"""
|
| 109 |
+
Error handler for UI classification improvements.
|
| 110 |
+
|
| 111 |
+
Provides validation, error recovery, and fallback mechanisms
|
| 112 |
+
for UI components and data processing.
|
| 113 |
+
|
| 114 |
+
Requirements: 9.1, 9.2, 9.3, 9.4
|
| 115 |
+
"""
|
| 116 |
+
|
| 117 |
+
def __init__(self):
|
| 118 |
+
"""Initialize the UI error handler."""
|
| 119 |
+
self.fallback_templates = self._initialize_fallback_templates()
|
| 120 |
+
self.validation_rules = self._initialize_validation_rules()
|
| 121 |
+
logger.info("🔧 UIErrorHandler initialized")
|
| 122 |
+
|
| 123 |
+
def validate_provider_summary_structure(self, summary: ProviderSummary) -> ValidationResult:
|
| 124 |
+
"""
|
| 125 |
+
Validate provider summary structure and completeness.
|
| 126 |
+
|
| 127 |
+
Checks for required fields, data integrity, and format compliance
|
| 128 |
+
according to requirements.
|
| 129 |
+
|
| 130 |
+
Args:
|
| 131 |
+
summary: ProviderSummary to validate
|
| 132 |
+
|
| 133 |
+
Returns:
|
| 134 |
+
ValidationResult with errors and warnings
|
| 135 |
+
|
| 136 |
+
Requirements: 9.1
|
| 137 |
+
"""
|
| 138 |
+
result = ValidationResult(is_valid=True)
|
| 139 |
+
|
| 140 |
+
try:
|
| 141 |
+
# Validate required contact information
|
| 142 |
+
self._validate_contact_information(summary, result)
|
| 143 |
+
|
| 144 |
+
# Validate classification data
|
| 145 |
+
self._validate_classification_data(summary, result)
|
| 146 |
+
|
| 147 |
+
# Validate content structure
|
| 148 |
+
self._validate_content_structure(summary, result)
|
| 149 |
+
|
| 150 |
+
# Validate data consistency
|
| 151 |
+
self._validate_data_consistency(summary, result)
|
| 152 |
+
|
| 153 |
+
logger.info(f"Provider summary validation completed: {result.get_error_summary()}")
|
| 154 |
+
|
| 155 |
+
except Exception as e:
|
| 156 |
+
error = UIError(
|
| 157 |
+
category=ErrorCategory.VALIDATION,
|
| 158 |
+
severity=ErrorSeverity.CRITICAL,
|
| 159 |
+
message=f"Validation process failed: {str(e)}",
|
| 160 |
+
component="provider_summary_validator",
|
| 161 |
+
suggestion="Check summary data structure and try again"
|
| 162 |
+
)
|
| 163 |
+
result.add_error(error)
|
| 164 |
+
logger.error(f"Validation process failed: {e}")
|
| 165 |
+
|
| 166 |
+
return result
|
| 167 |
+
|
| 168 |
+
def _validate_contact_information(self, summary: ProviderSummary, result: ValidationResult):
|
| 169 |
+
"""Validate contact information fields."""
|
| 170 |
+
# Check patient name
|
| 171 |
+
if not summary.patient_name or summary.patient_name.strip() == "[Patient Name]":
|
| 172 |
+
result.add_error(UIError(
|
| 173 |
+
category=ErrorCategory.DATA_MISSING,
|
| 174 |
+
severity=ErrorSeverity.HIGH,
|
| 175 |
+
message="Patient name is missing or placeholder",
|
| 176 |
+
component="provider_summary",
|
| 177 |
+
field="patient_name",
|
| 178 |
+
value=summary.patient_name,
|
| 179 |
+
suggestion="Ensure patient name is provided before generating summary",
|
| 180 |
+
recovery_actions=["Use fallback template", "Request patient name input"]
|
| 181 |
+
))
|
| 182 |
+
|
| 183 |
+
# Check patient phone
|
| 184 |
+
if not summary.patient_phone or summary.patient_phone.strip() == "[Phone Number]":
|
| 185 |
+
result.add_error(UIError(
|
| 186 |
+
category=ErrorCategory.DATA_MISSING,
|
| 187 |
+
severity=ErrorSeverity.HIGH,
|
| 188 |
+
message="Patient phone is missing or placeholder",
|
| 189 |
+
component="provider_summary",
|
| 190 |
+
field="patient_phone",
|
| 191 |
+
value=summary.patient_phone,
|
| 192 |
+
suggestion="Ensure patient phone is provided for contact",
|
| 193 |
+
recovery_actions=["Use fallback template", "Request phone number input"]
|
| 194 |
+
))
|
| 195 |
+
|
| 196 |
+
# Validate phone format if present
|
| 197 |
+
if summary.patient_phone and summary.patient_phone != "[Phone Number]":
|
| 198 |
+
if not self._is_valid_phone_format(summary.patient_phone):
|
| 199 |
+
result.add_warning(UIError(
|
| 200 |
+
category=ErrorCategory.VALIDATION,
|
| 201 |
+
severity=ErrorSeverity.MEDIUM,
|
| 202 |
+
message="Phone number format may be invalid",
|
| 203 |
+
component="provider_summary",
|
| 204 |
+
field="patient_phone",
|
| 205 |
+
value=summary.patient_phone,
|
| 206 |
+
suggestion="Verify phone number format (XXX-XXX-XXXX or similar)"
|
| 207 |
+
))
|
| 208 |
+
|
| 209 |
+
def _validate_classification_data(self, summary: ProviderSummary, result: ValidationResult):
|
| 210 |
+
"""Validate classification-related data."""
|
| 211 |
+
# Check indicators
|
| 212 |
+
if not summary.indicators:
|
| 213 |
+
result.add_warning(UIError(
|
| 214 |
+
category=ErrorCategory.DATA_MISSING,
|
| 215 |
+
severity=ErrorSeverity.MEDIUM,
|
| 216 |
+
message="No distress indicators specified",
|
| 217 |
+
component="provider_summary",
|
| 218 |
+
field="indicators",
|
| 219 |
+
suggestion="Ensure classification process provides indicators"
|
| 220 |
+
))
|
| 221 |
+
|
| 222 |
+
# Check reasoning
|
| 223 |
+
if not summary.reasoning or len(summary.reasoning.strip()) < 10:
|
| 224 |
+
result.add_error(UIError(
|
| 225 |
+
category=ErrorCategory.DATA_MISSING,
|
| 226 |
+
severity=ErrorSeverity.HIGH,
|
| 227 |
+
message="Classification reasoning is missing or insufficient",
|
| 228 |
+
component="provider_summary",
|
| 229 |
+
field="reasoning",
|
| 230 |
+
value=summary.reasoning,
|
| 231 |
+
suggestion="Provide detailed reasoning for classification decision",
|
| 232 |
+
recovery_actions=["Use fallback reasoning template"]
|
| 233 |
+
))
|
| 234 |
+
|
| 235 |
+
# Validate confidence range
|
| 236 |
+
if not 0.0 <= summary.confidence <= 1.0:
|
| 237 |
+
result.add_error(UIError(
|
| 238 |
+
category=ErrorCategory.VALIDATION,
|
| 239 |
+
severity=ErrorSeverity.MEDIUM,
|
| 240 |
+
message="Confidence value is out of valid range (0.0-1.0)",
|
| 241 |
+
component="provider_summary",
|
| 242 |
+
field="confidence",
|
| 243 |
+
value=summary.confidence,
|
| 244 |
+
suggestion="Ensure confidence is between 0.0 and 1.0",
|
| 245 |
+
recovery_actions=["Clamp to valid range", "Use default confidence"]
|
| 246 |
+
))
|
| 247 |
+
|
| 248 |
+
def _validate_content_structure(self, summary: ProviderSummary, result: ValidationResult):
|
| 249 |
+
"""Validate content structure and format."""
|
| 250 |
+
# Check situation description
|
| 251 |
+
if not summary.situation_description or len(summary.situation_description.strip()) < 20:
|
| 252 |
+
result.add_warning(UIError(
|
| 253 |
+
category=ErrorCategory.DATA_MISSING,
|
| 254 |
+
severity=ErrorSeverity.MEDIUM,
|
| 255 |
+
message="Situation description is missing or too brief",
|
| 256 |
+
component="provider_summary",
|
| 257 |
+
field="situation_description",
|
| 258 |
+
value=summary.situation_description,
|
| 259 |
+
suggestion="Provide detailed situation description",
|
| 260 |
+
recovery_actions=["Generate from available data", "Use fallback template"]
|
| 261 |
+
))
|
| 262 |
+
|
| 263 |
+
# Check urgency level
|
| 264 |
+
valid_urgency_levels = ["IMMEDIATE", "URGENT", "STANDARD"]
|
| 265 |
+
if summary.urgency_level not in valid_urgency_levels:
|
| 266 |
+
result.add_error(UIError(
|
| 267 |
+
category=ErrorCategory.VALIDATION,
|
| 268 |
+
severity=ErrorSeverity.MEDIUM,
|
| 269 |
+
message=f"Invalid urgency level: {summary.urgency_level}",
|
| 270 |
+
component="provider_summary",
|
| 271 |
+
field="urgency_level",
|
| 272 |
+
value=summary.urgency_level,
|
| 273 |
+
suggestion=f"Use one of: {', '.join(valid_urgency_levels)}",
|
| 274 |
+
recovery_actions=["Use default urgency level"]
|
| 275 |
+
))
|
| 276 |
+
|
| 277 |
+
# Check severity level
|
| 278 |
+
valid_severity_levels = ["CRITICAL", "HIGH", "MODERATE"]
|
| 279 |
+
if summary.severity_level not in valid_severity_levels:
|
| 280 |
+
result.add_error(UIError(
|
| 281 |
+
category=ErrorCategory.VALIDATION,
|
| 282 |
+
severity=ErrorSeverity.MEDIUM,
|
| 283 |
+
message=f"Invalid severity level: {summary.severity_level}",
|
| 284 |
+
component="provider_summary",
|
| 285 |
+
field="severity_level",
|
| 286 |
+
value=summary.severity_level,
|
| 287 |
+
suggestion=f"Use one of: {', '.join(valid_severity_levels)}",
|
| 288 |
+
recovery_actions=["Use default severity level"]
|
| 289 |
+
))
|
| 290 |
+
|
| 291 |
+
def _validate_data_consistency(self, summary: ProviderSummary, result: ValidationResult):
|
| 292 |
+
"""Validate data consistency across fields."""
|
| 293 |
+
# Check consistency between severity and urgency
|
| 294 |
+
if summary.severity_level == "CRITICAL" and summary.urgency_level not in ["IMMEDIATE", "URGENT"]:
|
| 295 |
+
result.add_warning(UIError(
|
| 296 |
+
category=ErrorCategory.VALIDATION,
|
| 297 |
+
severity=ErrorSeverity.LOW,
|
| 298 |
+
message="Critical severity should have immediate or urgent priority",
|
| 299 |
+
component="provider_summary",
|
| 300 |
+
field="urgency_level",
|
| 301 |
+
suggestion="Consider increasing urgency level for critical cases"
|
| 302 |
+
))
|
| 303 |
+
|
| 304 |
+
# Check if recommended actions are present
|
| 305 |
+
if not summary.recommended_actions:
|
| 306 |
+
result.add_warning(UIError(
|
| 307 |
+
category=ErrorCategory.DATA_MISSING,
|
| 308 |
+
severity=ErrorSeverity.LOW,
|
| 309 |
+
message="No recommended actions specified",
|
| 310 |
+
component="provider_summary",
|
| 311 |
+
field="recommended_actions",
|
| 312 |
+
suggestion="Provide specific recommended actions for follow-up"
|
| 313 |
+
))
|
| 314 |
+
|
| 315 |
+
def _is_valid_phone_format(self, phone: str) -> bool:
|
| 316 |
+
"""Check if phone number has a reasonable format."""
|
| 317 |
+
import re
|
| 318 |
+
# Basic phone format validation (allows various formats)
|
| 319 |
+
phone_pattern = r'^[\+]?[\d\s\-\(\)\.]{10,}$'
|
| 320 |
+
return bool(re.match(phone_pattern, phone.strip()))
|
| 321 |
+
|
| 322 |
+
def apply_fallback_template(self, summary: ProviderSummary, error_type: str) -> ProviderSummary:
|
| 323 |
+
"""
|
| 324 |
+
Apply fallback template to fix missing or invalid data.
|
| 325 |
+
|
| 326 |
+
Uses predefined templates to fill in missing information
|
| 327 |
+
and ensure the summary remains functional.
|
| 328 |
+
|
| 329 |
+
Args:
|
| 330 |
+
summary: ProviderSummary with missing/invalid data
|
| 331 |
+
error_type: Type of error to fix
|
| 332 |
+
|
| 333 |
+
Returns:
|
| 334 |
+
ProviderSummary with fallback data applied
|
| 335 |
+
|
| 336 |
+
Requirements: 9.2
|
| 337 |
+
"""
|
| 338 |
+
try:
|
| 339 |
+
# Create a copy to avoid modifying original
|
| 340 |
+
fixed_summary = ProviderSummary(
|
| 341 |
+
patient_name=summary.patient_name,
|
| 342 |
+
patient_phone=summary.patient_phone,
|
| 343 |
+
patient_email=summary.patient_email,
|
| 344 |
+
emergency_contact=summary.emergency_contact,
|
| 345 |
+
classification=summary.classification,
|
| 346 |
+
confidence=summary.confidence,
|
| 347 |
+
reasoning=summary.reasoning,
|
| 348 |
+
indicators=summary.indicators.copy() if summary.indicators else [],
|
| 349 |
+
severity_level=summary.severity_level,
|
| 350 |
+
triage_context=summary.triage_context.copy() if summary.triage_context else [],
|
| 351 |
+
conversation_context=summary.conversation_context,
|
| 352 |
+
conversation_history_summary=summary.conversation_history_summary,
|
| 353 |
+
medical_context=summary.medical_context.copy() if summary.medical_context else None,
|
| 354 |
+
context_factors=summary.context_factors.copy() if summary.context_factors else [],
|
| 355 |
+
defensive_patterns_detected=summary.defensive_patterns_detected,
|
| 356 |
+
situation_description=summary.situation_description,
|
| 357 |
+
urgency_level=summary.urgency_level,
|
| 358 |
+
recommended_actions=summary.recommended_actions.copy() if summary.recommended_actions else [],
|
| 359 |
+
follow_up_timeline=summary.follow_up_timeline,
|
| 360 |
+
generated_at=summary.generated_at,
|
| 361 |
+
generated_by=summary.generated_by
|
| 362 |
+
)
|
| 363 |
+
|
| 364 |
+
# Apply specific fallback templates
|
| 365 |
+
if error_type == "missing_contact":
|
| 366 |
+
self._apply_contact_fallback(fixed_summary)
|
| 367 |
+
elif error_type == "missing_reasoning":
|
| 368 |
+
self._apply_reasoning_fallback(fixed_summary)
|
| 369 |
+
elif error_type == "missing_situation":
|
| 370 |
+
self._apply_situation_fallback(fixed_summary)
|
| 371 |
+
elif error_type == "invalid_levels":
|
| 372 |
+
self._apply_levels_fallback(fixed_summary)
|
| 373 |
+
elif error_type == "missing_actions":
|
| 374 |
+
self._apply_actions_fallback(fixed_summary)
|
| 375 |
+
else:
|
| 376 |
+
# Apply general fallback
|
| 377 |
+
self._apply_general_fallback(fixed_summary)
|
| 378 |
+
|
| 379 |
+
logger.info(f"Applied fallback template for error type: {error_type}")
|
| 380 |
+
return fixed_summary
|
| 381 |
+
|
| 382 |
+
except Exception as e:
|
| 383 |
+
logger.error(f"Failed to apply fallback template: {e}")
|
| 384 |
+
# Return original summary if fallback fails
|
| 385 |
+
return summary
|
| 386 |
+
|
| 387 |
+
def _apply_contact_fallback(self, summary: ProviderSummary):
|
| 388 |
+
"""Apply fallback for missing contact information."""
|
| 389 |
+
if not summary.patient_name or summary.patient_name == "[Patient Name]":
|
| 390 |
+
summary.patient_name = "Patient (Name Not Provided)"
|
| 391 |
+
|
| 392 |
+
if not summary.patient_phone or summary.patient_phone == "[Phone Number]":
|
| 393 |
+
summary.patient_phone = "Contact information not available"
|
| 394 |
+
|
| 395 |
+
def _apply_reasoning_fallback(self, summary: ProviderSummary):
|
| 396 |
+
"""Apply fallback for missing reasoning."""
|
| 397 |
+
if not summary.reasoning or len(summary.reasoning.strip()) < 10:
|
| 398 |
+
if summary.indicators:
|
| 399 |
+
indicators_text = ", ".join(summary.indicators[:3])
|
| 400 |
+
summary.reasoning = f"RED flag classification based on detected indicators: {indicators_text}. Immediate spiritual care support recommended."
|
| 401 |
+
else:
|
| 402 |
+
summary.reasoning = "RED flag classification indicates potential spiritual or emotional distress requiring immediate attention."
|
| 403 |
+
|
| 404 |
+
def _apply_situation_fallback(self, summary: ProviderSummary):
|
| 405 |
+
"""Apply fallback for missing situation description."""
|
| 406 |
+
if not summary.situation_description or len(summary.situation_description.strip()) < 20:
|
| 407 |
+
if summary.indicators:
|
| 408 |
+
indicators_text = ", ".join(summary.indicators[:2])
|
| 409 |
+
summary.situation_description = f"Patient expressing {indicators_text}. Assessment indicates need for spiritual care support."
|
| 410 |
+
else:
|
| 411 |
+
summary.situation_description = "Patient assessment indicates potential spiritual or emotional distress requiring follow-up."
|
| 412 |
+
|
| 413 |
+
def _apply_levels_fallback(self, summary: ProviderSummary):
|
| 414 |
+
"""Apply fallback for invalid severity/urgency levels."""
|
| 415 |
+
valid_severity = ["CRITICAL", "HIGH", "MODERATE"]
|
| 416 |
+
valid_urgency = ["IMMEDIATE", "URGENT", "STANDARD"]
|
| 417 |
+
|
| 418 |
+
if summary.severity_level not in valid_severity:
|
| 419 |
+
summary.severity_level = "HIGH" # Default for RED flags
|
| 420 |
+
|
| 421 |
+
if summary.urgency_level not in valid_urgency:
|
| 422 |
+
# Map severity to appropriate urgency
|
| 423 |
+
urgency_map = {
|
| 424 |
+
"CRITICAL": "IMMEDIATE",
|
| 425 |
+
"HIGH": "URGENT",
|
| 426 |
+
"MODERATE": "STANDARD"
|
| 427 |
+
}
|
| 428 |
+
summary.urgency_level = urgency_map.get(summary.severity_level, "URGENT")
|
| 429 |
+
|
| 430 |
+
def _apply_actions_fallback(self, summary: ProviderSummary):
|
| 431 |
+
"""Apply fallback for missing recommended actions."""
|
| 432 |
+
if not summary.recommended_actions:
|
| 433 |
+
default_actions = [
|
| 434 |
+
"Contact patient within 24 hours",
|
| 435 |
+
"Assess immediate support needs",
|
| 436 |
+
"Provide spiritual care resources",
|
| 437 |
+
"Schedule follow-up within 48-72 hours"
|
| 438 |
+
]
|
| 439 |
+
|
| 440 |
+
# Customize based on urgency
|
| 441 |
+
if summary.urgency_level == "IMMEDIATE":
|
| 442 |
+
summary.recommended_actions = [
|
| 443 |
+
"IMMEDIATE contact required - within 2-4 hours",
|
| 444 |
+
"Assess immediate safety and support needs",
|
| 445 |
+
"Consider emergency intervention if needed"
|
| 446 |
+
]
|
| 447 |
+
elif summary.urgency_level == "URGENT":
|
| 448 |
+
summary.recommended_actions = default_actions[:3]
|
| 449 |
+
else:
|
| 450 |
+
summary.recommended_actions = default_actions
|
| 451 |
+
|
| 452 |
+
def _apply_general_fallback(self, summary: ProviderSummary):
|
| 453 |
+
"""Apply general fallback for multiple issues."""
|
| 454 |
+
self._apply_contact_fallback(summary)
|
| 455 |
+
self._apply_reasoning_fallback(summary)
|
| 456 |
+
self._apply_situation_fallback(summary)
|
| 457 |
+
self._apply_levels_fallback(summary)
|
| 458 |
+
self._apply_actions_fallback(summary)
|
| 459 |
+
|
| 460 |
+
def create_degraded_display(self, error_context: str, original_content: Optional[str] = None) -> str:
|
| 461 |
+
"""
|
| 462 |
+
Create degraded display when normal formatting fails.
|
| 463 |
+
|
| 464 |
+
Provides basic functionality when enhanced features fail,
|
| 465 |
+
ensuring the system remains usable.
|
| 466 |
+
|
| 467 |
+
Args:
|
| 468 |
+
error_context: Description of the error context
|
| 469 |
+
original_content: Original content if available
|
| 470 |
+
|
| 471 |
+
Returns:
|
| 472 |
+
Basic HTML display with error information
|
| 473 |
+
|
| 474 |
+
Requirements: 9.3
|
| 475 |
+
"""
|
| 476 |
+
try:
|
| 477 |
+
# Create basic error display
|
| 478 |
+
error_display = f"""
|
| 479 |
+
<div style='border: 2px solid #ff6b6b; border-radius: 4px; padding: 15px; margin: 10px 0; background-color: #fff5f5;'>
|
| 480 |
+
<div style='display: flex; align-items: center; margin-bottom: 10px;'>
|
| 481 |
+
<span style='font-size: 1.2em; margin-right: 8px;'>⚠️</span>
|
| 482 |
+
<strong style='color: #d63031;'>Display Error Detected</strong>
|
| 483 |
+
</div>
|
| 484 |
+
<div style='margin: 10px 0; color: #636e72;'>
|
| 485 |
+
<strong>Context:</strong> {error_context}
|
| 486 |
+
</div>
|
| 487 |
+
<div style='margin: 10px 0; padding: 8px; background-color: #f8f9fa; border-radius: 4px; font-family: monospace; font-size: 0.9em;'>
|
| 488 |
+
System is operating in degraded mode to ensure continued functionality.
|
| 489 |
+
</div>
|
| 490 |
+
"""
|
| 491 |
+
|
| 492 |
+
# Include original content if available
|
| 493 |
+
if original_content:
|
| 494 |
+
error_display += f"""
|
| 495 |
+
<div style='margin: 10px 0;'>
|
| 496 |
+
<strong>Available Content:</strong>
|
| 497 |
+
<div style='margin: 5px 0; padding: 8px; background-color: white; border: 1px solid #ddd; border-radius: 4px; max-height: 200px; overflow-y: auto;'>
|
| 498 |
+
{original_content}
|
| 499 |
+
</div>
|
| 500 |
+
</div>
|
| 501 |
+
"""
|
| 502 |
+
|
| 503 |
+
error_display += """
|
| 504 |
+
<div style='margin: 10px 0; font-size: 0.9em; color: #636e72;'>
|
| 505 |
+
<strong>Recovery Actions:</strong>
|
| 506 |
+
<ul style='margin: 5px 0 0 20px;'>
|
| 507 |
+
<li>Refresh the page to retry</li>
|
| 508 |
+
<li>Check system logs for details</li>
|
| 509 |
+
<li>Contact support if issue persists</li>
|
| 510 |
+
</ul>
|
| 511 |
+
</div>
|
| 512 |
+
</div>
|
| 513 |
+
"""
|
| 514 |
+
|
| 515 |
+
logger.warning(f"Created degraded display for error: {error_context}")
|
| 516 |
+
return error_display
|
| 517 |
+
|
| 518 |
+
except Exception as e:
|
| 519 |
+
logger.error(f"Failed to create degraded display: {e}")
|
| 520 |
+
# Ultimate fallback - plain text
|
| 521 |
+
return f"""
|
| 522 |
+
<div style='padding: 10px; border: 1px solid red;'>
|
| 523 |
+
<strong>System Error</strong><br>
|
| 524 |
+
Context: {error_context}<br>
|
| 525 |
+
Please refresh the page or contact support.
|
| 526 |
+
</div>
|
| 527 |
+
"""
|
| 528 |
+
|
| 529 |
+
def handle_classification_error(self, error: Exception, input_data: Dict[str, Any]) -> ClassificationResult:
|
| 530 |
+
"""
|
| 531 |
+
Handle classification errors with fallback classification.
|
| 532 |
+
|
| 533 |
+
Provides safe fallback when classification process fails,
|
| 534 |
+
ensuring system continues to function.
|
| 535 |
+
|
| 536 |
+
Args:
|
| 537 |
+
error: The exception that occurred
|
| 538 |
+
input_data: Original input data for classification
|
| 539 |
+
|
| 540 |
+
Returns:
|
| 541 |
+
Fallback ClassificationResult
|
| 542 |
+
|
| 543 |
+
Requirements: 9.4
|
| 544 |
+
"""
|
| 545 |
+
try:
|
| 546 |
+
logger.error(f"Classification error occurred: {error}")
|
| 547 |
+
|
| 548 |
+
# Create fallback classification result
|
| 549 |
+
fallback_result = ClassificationResult(
|
| 550 |
+
classification="yellow", # Safe default
|
| 551 |
+
confidence=0.5, # Moderate confidence for fallback
|
| 552 |
+
indicators=["Classification error - manual review required"],
|
| 553 |
+
reasoning=f"Automatic classification failed due to system error. Manual review recommended. Error: {str(error)[:100]}",
|
| 554 |
+
red_flag_indicators=[],
|
| 555 |
+
yellow_flag_indicators=["System error", "Manual review required"],
|
| 556 |
+
is_valid=False # Mark as invalid due to error
|
| 557 |
+
)
|
| 558 |
+
|
| 559 |
+
# Try to extract some information from input if possible
|
| 560 |
+
if isinstance(input_data, dict):
|
| 561 |
+
message = input_data.get('message', '')
|
| 562 |
+
if message:
|
| 563 |
+
# Simple keyword-based fallback classification
|
| 564 |
+
critical_keywords = ['suicide', 'kill myself', 'end it all', 'hopeless', 'no point']
|
| 565 |
+
red_keywords = ['grief', 'loss', 'meaning', 'suffering', 'dignity', 'spiritual']
|
| 566 |
+
|
| 567 |
+
message_lower = message.lower()
|
| 568 |
+
|
| 569 |
+
if any(keyword in message_lower for keyword in critical_keywords):
|
| 570 |
+
fallback_result.classification = "red"
|
| 571 |
+
fallback_result.confidence = 0.8
|
| 572 |
+
fallback_result.indicators = ["Critical keywords detected"]
|
| 573 |
+
fallback_result.red_flag_indicators = ["Critical distress indicators"]
|
| 574 |
+
fallback_result.reasoning = "Fallback classification detected critical keywords requiring immediate attention."
|
| 575 |
+
elif any(keyword in message_lower for keyword in red_keywords):
|
| 576 |
+
fallback_result.classification = "red"
|
| 577 |
+
fallback_result.confidence = 0.6
|
| 578 |
+
fallback_result.indicators = ["Spiritual distress keywords detected"]
|
| 579 |
+
fallback_result.red_flag_indicators = ["Spiritual distress indicators"]
|
| 580 |
+
fallback_result.reasoning = "Fallback classification detected spiritual distress indicators."
|
| 581 |
+
|
| 582 |
+
logger.info(f"Created fallback classification: {fallback_result.classification}")
|
| 583 |
+
return fallback_result
|
| 584 |
+
|
| 585 |
+
except Exception as fallback_error:
|
| 586 |
+
logger.error(f"Fallback classification also failed: {fallback_error}")
|
| 587 |
+
|
| 588 |
+
# Ultimate fallback - minimal safe result
|
| 589 |
+
return ClassificationResult(
|
| 590 |
+
classification="yellow",
|
| 591 |
+
confidence=0.3,
|
| 592 |
+
indicators=["Multiple system errors"],
|
| 593 |
+
reasoning="System errors prevented classification. Immediate manual review required.",
|
| 594 |
+
red_flag_indicators=[],
|
| 595 |
+
yellow_flag_indicators=["System error", "Manual review required"],
|
| 596 |
+
is_valid=False
|
| 597 |
+
)
|
| 598 |
+
|
| 599 |
+
def _initialize_fallback_templates(self) -> Dict[str, Dict[str, Any]]:
|
| 600 |
+
"""Initialize fallback templates for different error scenarios."""
|
| 601 |
+
return {
|
| 602 |
+
"missing_contact": {
|
| 603 |
+
"patient_name": "Patient (Name Not Provided)",
|
| 604 |
+
"patient_phone": "Contact information not available"
|
| 605 |
+
},
|
| 606 |
+
"missing_reasoning": {
|
| 607 |
+
"reasoning": "Classification based on detected indicators. Manual review recommended."
|
| 608 |
+
},
|
| 609 |
+
"missing_situation": {
|
| 610 |
+
"situation_description": "Patient assessment indicates potential spiritual or emotional distress."
|
| 611 |
+
},
|
| 612 |
+
"invalid_levels": {
|
| 613 |
+
"severity_level": "HIGH",
|
| 614 |
+
"urgency_level": "URGENT"
|
| 615 |
+
},
|
| 616 |
+
"missing_actions": {
|
| 617 |
+
"recommended_actions": [
|
| 618 |
+
"Contact patient within 24 hours",
|
| 619 |
+
"Assess support needs",
|
| 620 |
+
"Provide spiritual care resources"
|
| 621 |
+
]
|
| 622 |
+
}
|
| 623 |
+
}
|
| 624 |
+
|
| 625 |
+
def _initialize_validation_rules(self) -> Dict[str, Any]:
|
| 626 |
+
"""Initialize validation rules for different components."""
|
| 627 |
+
return {
|
| 628 |
+
"required_fields": {
|
| 629 |
+
"provider_summary": ["patient_name", "patient_phone", "classification", "reasoning"],
|
| 630 |
+
"classification_result": ["classification", "confidence", "reasoning"]
|
| 631 |
+
},
|
| 632 |
+
"valid_values": {
|
| 633 |
+
"classification": ["red", "yellow", "green"],
|
| 634 |
+
"severity_level": ["CRITICAL", "HIGH", "MODERATE"],
|
| 635 |
+
"urgency_level": ["IMMEDIATE", "URGENT", "STANDARD"]
|
| 636 |
+
},
|
| 637 |
+
"field_constraints": {
|
| 638 |
+
"confidence": {"min": 0.0, "max": 1.0},
|
| 639 |
+
"reasoning_min_length": 10,
|
| 640 |
+
"situation_min_length": 20
|
| 641 |
+
}
|
| 642 |
+
}
|
| 643 |
+
|
| 644 |
+
def get_error_statistics(self, errors: List[UIError]) -> Dict[str, Any]:
|
| 645 |
+
"""
|
| 646 |
+
Get statistics about errors for monitoring and reporting.
|
| 647 |
+
|
| 648 |
+
Args:
|
| 649 |
+
errors: List of UIError objects
|
| 650 |
+
|
| 651 |
+
Returns:
|
| 652 |
+
Dictionary with error statistics
|
| 653 |
+
"""
|
| 654 |
+
if not errors:
|
| 655 |
+
return {"total": 0, "by_category": {}, "by_severity": {}}
|
| 656 |
+
|
| 657 |
+
stats = {
|
| 658 |
+
"total": len(errors),
|
| 659 |
+
"by_category": {},
|
| 660 |
+
"by_severity": {},
|
| 661 |
+
"by_component": {}
|
| 662 |
+
}
|
| 663 |
+
|
| 664 |
+
for error in errors:
|
| 665 |
+
# Count by category
|
| 666 |
+
category = error.category.value
|
| 667 |
+
stats["by_category"][category] = stats["by_category"].get(category, 0) + 1
|
| 668 |
+
|
| 669 |
+
# Count by severity
|
| 670 |
+
severity = error.severity.value
|
| 671 |
+
stats["by_severity"][severity] = stats["by_severity"].get(severity, 0) + 1
|
| 672 |
+
|
| 673 |
+
# Count by component
|
| 674 |
+
component = error.component
|
| 675 |
+
stats["by_component"][component] = stats["by_component"].get(component, 0) + 1
|
| 676 |
+
|
| 677 |
+
return stats
|
| 678 |
+
|
| 679 |
+
|
| 680 |
+
def create_ui_error_handler() -> UIErrorHandler:
|
| 681 |
+
"""
|
| 682 |
+
Factory function to create UIErrorHandler.
|
| 683 |
+
|
| 684 |
+
Returns:
|
| 685 |
+
Initialized UIErrorHandler instance
|
| 686 |
+
"""
|
| 687 |
+
return UIErrorHandler()
|
|
@@ -1,40 +1,48 @@
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
"""
|
| 3 |
-
Verification Results Exporter.
|
| 4 |
|
| 5 |
-
Handles exporting verification session results to CSV format
|
|
|
|
|
|
|
|
|
|
| 6 |
"""
|
| 7 |
|
| 8 |
import csv
|
| 9 |
import os
|
|
|
|
| 10 |
from datetime import datetime
|
| 11 |
-
from typing import List, Dict, Any
|
| 12 |
-
from src.core.conversation_verification import
|
| 13 |
|
| 14 |
|
| 15 |
-
class
|
| 16 |
-
"""
|
| 17 |
|
| 18 |
def __init__(self, export_dir: str = "verification_exports"):
|
| 19 |
-
"""Initialize exporter."""
|
| 20 |
self.export_dir = export_dir
|
| 21 |
os.makedirs(export_dir, exist_ok=True)
|
| 22 |
|
| 23 |
-
def export_session_to_csv(self, session:
|
| 24 |
"""
|
| 25 |
-
Export verification session to CSV format.
|
| 26 |
|
| 27 |
Args:
|
| 28 |
-
session:
|
|
|
|
| 29 |
|
| 30 |
Returns:
|
| 31 |
Path to exported CSV file
|
|
|
|
|
|
|
| 32 |
"""
|
| 33 |
filename = self._create_export_filename(session)
|
| 34 |
filepath = os.path.join(self.export_dir, filename)
|
| 35 |
|
| 36 |
try:
|
| 37 |
with open(filepath, 'w', newline='', encoding='utf-8') as csvfile:
|
|
|
|
| 38 |
fieldnames = [
|
| 39 |
'exchange_number',
|
| 40 |
'timestamp',
|
|
@@ -51,12 +59,25 @@ class VerificationExporter:
|
|
| 51 |
'verification_timestamp'
|
| 52 |
]
|
| 53 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
|
| 55 |
writer.writeheader()
|
| 56 |
|
| 57 |
# Write session metadata as comment rows
|
| 58 |
writer.writerow({
|
| 59 |
-
'exchange_number': '# SESSION METADATA',
|
| 60 |
'timestamp': f'Session ID: {session.session_id}',
|
| 61 |
'user_message': f'Patient: {session.patient_name}',
|
| 62 |
'assistant_response': f'Verifier: {session.verifier_name}',
|
|
@@ -65,7 +86,7 @@ class VerificationExporter:
|
|
| 65 |
'indicators': f'Total Exchanges: {session.total_exchanges}',
|
| 66 |
'reasoning': f'Verified: {session.verified_exchanges}',
|
| 67 |
'is_correct': f'Complete: {session.is_complete}',
|
| 68 |
-
'correct_classification': '',
|
| 69 |
'correction_reason': '',
|
| 70 |
'verifier_notes': '',
|
| 71 |
'verification_timestamp': ''
|
|
@@ -74,9 +95,9 @@ class VerificationExporter:
|
|
| 74 |
# Add empty row for separation
|
| 75 |
writer.writerow({field: '' for field in fieldnames})
|
| 76 |
|
| 77 |
-
# Write verification records
|
| 78 |
for record in session.verification_records:
|
| 79 |
-
|
| 80 |
'exchange_number': record.exchange_number,
|
| 81 |
'timestamp': record.timestamp.isoformat(),
|
| 82 |
'user_message': record.user_message,
|
|
@@ -90,21 +111,171 @@ class VerificationExporter:
|
|
| 90 |
'correction_reason': record.correction_reason or '',
|
| 91 |
'verifier_notes': record.verifier_notes or '',
|
| 92 |
'verification_timestamp': record.verification_timestamp.isoformat() if record.verification_timestamp else ''
|
| 93 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
|
| 95 |
return filepath
|
| 96 |
|
| 97 |
except Exception as e:
|
| 98 |
-
raise Exception(f"Failed to export
|
| 99 |
|
| 100 |
-
def _create_export_filename(self, session:
|
| 101 |
-
"""Create descriptive filename for export."""
|
| 102 |
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
| 103 |
patient_safe = session.patient_name.replace(' ', '_').replace('/', '_')
|
| 104 |
-
|
|
|
|
| 105 |
|
| 106 |
-
def
|
| 107 |
-
"""Generate summary report for verification session."""
|
| 108 |
progress = session.get_progress()
|
| 109 |
|
| 110 |
# Calculate detailed statistics
|
|
@@ -112,6 +283,11 @@ class VerificationExporter:
|
|
| 112 |
correct_records = [r for r in verified_records if r.is_correct]
|
| 113 |
incorrect_records = [r for r in verified_records if not r.is_correct]
|
| 114 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
# Error analysis
|
| 116 |
error_patterns = {}
|
| 117 |
for record in incorrect_records:
|
|
@@ -126,6 +302,17 @@ class VerificationExporter:
|
|
| 126 |
'incorrect_avg_confidence': sum(r.original_confidence for r in incorrect_records) / len(incorrect_records) if incorrect_records else 0
|
| 127 |
}
|
| 128 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
return {
|
| 130 |
'session_info': {
|
| 131 |
'session_id': session.session_id,
|
|
@@ -133,7 +320,8 @@ class VerificationExporter:
|
|
| 133 |
'verifier_name': session.verifier_name,
|
| 134 |
'start_time': session.start_time.isoformat(),
|
| 135 |
'end_time': session.end_time.isoformat() if session.end_time else None,
|
| 136 |
-
'duration_minutes': (session.end_time - session.start_time).total_seconds() / 60 if session.end_time else None
|
|
|
|
| 137 |
},
|
| 138 |
'verification_stats': {
|
| 139 |
'total_exchanges': session.total_exchanges,
|
|
@@ -148,5 +336,6 @@ class VerificationExporter:
|
|
| 148 |
'error_patterns': error_patterns,
|
| 149 |
'common_errors': progress.common_errors
|
| 150 |
},
|
| 151 |
-
'confidence_analysis': confidence_stats
|
|
|
|
| 152 |
}
|
|
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
"""
|
| 3 |
+
Enhanced Verification Results Exporter.
|
| 4 |
|
| 5 |
+
Handles exporting verification session results to CSV format with enhanced
|
| 6 |
+
classification data and new display formats.
|
| 7 |
+
|
| 8 |
+
Requirements: 8.2, 8.3, 8.4, 8.5
|
| 9 |
"""
|
| 10 |
|
| 11 |
import csv
|
| 12 |
import os
|
| 13 |
+
import html
|
| 14 |
from datetime import datetime
|
| 15 |
+
from typing import List, Dict, Any, Optional
|
| 16 |
+
from src.core.conversation_verification import EnhancedVerificationSession, EnhancedVerificationRecord
|
| 17 |
|
| 18 |
|
| 19 |
+
class EnhancedVerificationExporter:
|
| 20 |
+
"""Enhanced exporter for verification session results with new format support."""
|
| 21 |
|
| 22 |
def __init__(self, export_dir: str = "verification_exports"):
|
| 23 |
+
"""Initialize enhanced exporter."""
|
| 24 |
self.export_dir = export_dir
|
| 25 |
os.makedirs(export_dir, exist_ok=True)
|
| 26 |
|
| 27 |
+
def export_session_to_csv(self, session: EnhancedVerificationSession, include_enhanced_data: bool = True) -> str:
|
| 28 |
"""
|
| 29 |
+
Export enhanced verification session to CSV format.
|
| 30 |
|
| 31 |
Args:
|
| 32 |
+
session: EnhancedVerificationSession to export
|
| 33 |
+
include_enhanced_data: Whether to include enhanced format data
|
| 34 |
|
| 35 |
Returns:
|
| 36 |
Path to exported CSV file
|
| 37 |
+
|
| 38 |
+
Requirements: 8.2, 8.3
|
| 39 |
"""
|
| 40 |
filename = self._create_export_filename(session)
|
| 41 |
filepath = os.path.join(self.export_dir, filename)
|
| 42 |
|
| 43 |
try:
|
| 44 |
with open(filepath, 'w', newline='', encoding='utf-8') as csvfile:
|
| 45 |
+
# Enhanced fieldnames with new format data
|
| 46 |
fieldnames = [
|
| 47 |
'exchange_number',
|
| 48 |
'timestamp',
|
|
|
|
| 59 |
'verification_timestamp'
|
| 60 |
]
|
| 61 |
|
| 62 |
+
# Add enhanced format fields if requested
|
| 63 |
+
if include_enhanced_data:
|
| 64 |
+
fieldnames.extend([
|
| 65 |
+
'has_enhanced_display',
|
| 66 |
+
'has_provider_summary',
|
| 67 |
+
'provider_summary_urgency',
|
| 68 |
+
'provider_summary_severity',
|
| 69 |
+
'coherent_paragraph_length',
|
| 70 |
+
'visual_sections_count',
|
| 71 |
+
'enhanced_indicators_count',
|
| 72 |
+
'enhanced_display_preview'
|
| 73 |
+
])
|
| 74 |
+
|
| 75 |
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
|
| 76 |
writer.writeheader()
|
| 77 |
|
| 78 |
# Write session metadata as comment rows
|
| 79 |
writer.writerow({
|
| 80 |
+
'exchange_number': '# ENHANCED SESSION METADATA',
|
| 81 |
'timestamp': f'Session ID: {session.session_id}',
|
| 82 |
'user_message': f'Patient: {session.patient_name}',
|
| 83 |
'assistant_response': f'Verifier: {session.verifier_name}',
|
|
|
|
| 86 |
'indicators': f'Total Exchanges: {session.total_exchanges}',
|
| 87 |
'reasoning': f'Verified: {session.verified_exchanges}',
|
| 88 |
'is_correct': f'Complete: {session.is_complete}',
|
| 89 |
+
'correct_classification': f'Enhanced Format: {getattr(session, "enhanced_format_enabled", False)}',
|
| 90 |
'correction_reason': '',
|
| 91 |
'verifier_notes': '',
|
| 92 |
'verification_timestamp': ''
|
|
|
|
| 95 |
# Add empty row for separation
|
| 96 |
writer.writerow({field: '' for field in fieldnames})
|
| 97 |
|
| 98 |
+
# Write enhanced verification records
|
| 99 |
for record in session.verification_records:
|
| 100 |
+
row_data = {
|
| 101 |
'exchange_number': record.exchange_number,
|
| 102 |
'timestamp': record.timestamp.isoformat(),
|
| 103 |
'user_message': record.user_message,
|
|
|
|
| 111 |
'correction_reason': record.correction_reason or '',
|
| 112 |
'verifier_notes': record.verifier_notes or '',
|
| 113 |
'verification_timestamp': record.verification_timestamp.isoformat() if record.verification_timestamp else ''
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
# Add enhanced format data if requested
|
| 117 |
+
if include_enhanced_data:
|
| 118 |
+
row_data.update(self._extract_enhanced_data_for_csv(record))
|
| 119 |
+
|
| 120 |
+
writer.writerow(row_data)
|
| 121 |
+
|
| 122 |
+
return filepath
|
| 123 |
+
|
| 124 |
+
except Exception as e:
|
| 125 |
+
raise Exception(f"Failed to export enhanced CSV: {str(e)}")
|
| 126 |
+
|
| 127 |
+
def _extract_enhanced_data_for_csv(self, record: EnhancedVerificationRecord) -> Dict[str, Any]:
|
| 128 |
+
"""Extract enhanced format data for CSV export."""
|
| 129 |
+
enhanced_data = {
|
| 130 |
+
'has_enhanced_display': bool(record.enhanced_display_format),
|
| 131 |
+
'has_provider_summary': bool(record.provider_summary),
|
| 132 |
+
'provider_summary_urgency': '',
|
| 133 |
+
'provider_summary_severity': '',
|
| 134 |
+
'coherent_paragraph_length': 0,
|
| 135 |
+
'visual_sections_count': len(record.visual_sections) if record.visual_sections else 0,
|
| 136 |
+
'enhanced_indicators_count': len(record.original_indicators),
|
| 137 |
+
'enhanced_display_preview': ''
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
# Extract provider summary data
|
| 141 |
+
if record.provider_summary:
|
| 142 |
+
enhanced_data['provider_summary_urgency'] = record.provider_summary.get('urgency_level', '')
|
| 143 |
+
enhanced_data['provider_summary_severity'] = record.provider_summary.get('severity_level', '')
|
| 144 |
+
|
| 145 |
+
# Extract coherent paragraph data
|
| 146 |
+
if record.coherent_summary_paragraph:
|
| 147 |
+
enhanced_data['coherent_paragraph_length'] = len(record.coherent_summary_paragraph)
|
| 148 |
+
|
| 149 |
+
# Extract enhanced display preview (first 100 chars, HTML stripped)
|
| 150 |
+
if record.enhanced_display_format:
|
| 151 |
+
# Strip HTML tags for CSV preview
|
| 152 |
+
import re
|
| 153 |
+
clean_text = re.sub(r'<[^>]+>', '', record.enhanced_display_format)
|
| 154 |
+
clean_text = clean_text.replace('\n', ' ').strip()
|
| 155 |
+
enhanced_data['enhanced_display_preview'] = clean_text[:100] + '...' if len(clean_text) > 100 else clean_text
|
| 156 |
+
|
| 157 |
+
return enhanced_data
|
| 158 |
+
|
| 159 |
+
def export_enhanced_summary_report(self, session: EnhancedVerificationSession) -> str:
|
| 160 |
+
"""
|
| 161 |
+
Export enhanced summary report with new format statistics.
|
| 162 |
+
|
| 163 |
+
Args:
|
| 164 |
+
session: EnhancedVerificationSession to analyze
|
| 165 |
+
|
| 166 |
+
Returns:
|
| 167 |
+
Path to exported summary report file
|
| 168 |
+
|
| 169 |
+
Requirements: 8.4, 8.5
|
| 170 |
+
"""
|
| 171 |
+
filename = f"enhanced_summary_{session.session_id}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt"
|
| 172 |
+
filepath = os.path.join(self.export_dir, filename)
|
| 173 |
+
|
| 174 |
+
try:
|
| 175 |
+
with open(filepath, 'w', encoding='utf-8') as f:
|
| 176 |
+
f.write("=" * 80 + "\n")
|
| 177 |
+
f.write("ENHANCED VERIFICATION SESSION SUMMARY REPORT\n")
|
| 178 |
+
f.write("=" * 80 + "\n\n")
|
| 179 |
+
|
| 180 |
+
# Session information
|
| 181 |
+
f.write("SESSION INFORMATION\n")
|
| 182 |
+
f.write("-" * 40 + "\n")
|
| 183 |
+
f.write(f"Session ID: {session.session_id}\n")
|
| 184 |
+
f.write(f"Patient: {session.patient_name}\n")
|
| 185 |
+
f.write(f"Verifier: {session.verifier_name}\n")
|
| 186 |
+
f.write(f"Started: {session.start_time.isoformat()}\n")
|
| 187 |
+
f.write(f"Completed: {session.end_time.isoformat() if session.end_time else 'In Progress'}\n")
|
| 188 |
+
f.write(f"Enhanced Format Enabled: {getattr(session, 'enhanced_format_enabled', False)}\n\n")
|
| 189 |
+
|
| 190 |
+
# Enhanced format statistics
|
| 191 |
+
f.write("ENHANCED FORMAT STATISTICS\n")
|
| 192 |
+
f.write("-" * 40 + "\n")
|
| 193 |
+
|
| 194 |
+
records_with_enhanced = sum(1 for r in session.verification_records if r.enhanced_display_format)
|
| 195 |
+
records_with_summary = sum(1 for r in session.verification_records if r.provider_summary)
|
| 196 |
+
records_with_paragraph = sum(1 for r in session.verification_records if r.coherent_summary_paragraph)
|
| 197 |
+
|
| 198 |
+
f.write(f"Total Records: {len(session.verification_records)}\n")
|
| 199 |
+
f.write(f"Records with Enhanced Display: {records_with_enhanced}\n")
|
| 200 |
+
f.write(f"Records with Provider Summary: {records_with_summary}\n")
|
| 201 |
+
f.write(f"Records with Coherent Paragraph: {records_with_paragraph}\n")
|
| 202 |
+
|
| 203 |
+
# Enhanced format coverage
|
| 204 |
+
if session.verification_records:
|
| 205 |
+
enhanced_coverage = (records_with_enhanced / len(session.verification_records)) * 100
|
| 206 |
+
summary_coverage = (records_with_summary / len(session.verification_records)) * 100
|
| 207 |
+
paragraph_coverage = (records_with_paragraph / len(session.verification_records)) * 100
|
| 208 |
+
|
| 209 |
+
f.write(f"Enhanced Display Coverage: {enhanced_coverage:.1f}%\n")
|
| 210 |
+
f.write(f"Provider Summary Coverage: {summary_coverage:.1f}%\n")
|
| 211 |
+
f.write(f"Coherent Paragraph Coverage: {paragraph_coverage:.1f}%\n\n")
|
| 212 |
+
|
| 213 |
+
# Classification accuracy with enhanced data
|
| 214 |
+
progress = session.get_progress()
|
| 215 |
+
f.write("CLASSIFICATION ACCURACY\n")
|
| 216 |
+
f.write("-" * 40 + "\n")
|
| 217 |
+
f.write(f"Overall Accuracy: {progress.accuracy_overall:.1%}\n")
|
| 218 |
+
f.write(f"Verified Exchanges: {progress.verified_exchanges}/{progress.total_exchanges}\n\n")
|
| 219 |
+
|
| 220 |
+
for classification, accuracy in progress.accuracy_by_type.items():
|
| 221 |
+
f.write(f"{classification} Flag Accuracy: {accuracy:.1%}\n")
|
| 222 |
+
|
| 223 |
+
# Enhanced indicators analysis
|
| 224 |
+
f.write("\nENHANCED INDICATORS ANALYSIS\n")
|
| 225 |
+
f.write("-" * 40 + "\n")
|
| 226 |
+
|
| 227 |
+
all_indicators = []
|
| 228 |
+
for record in session.verification_records:
|
| 229 |
+
all_indicators.extend(record.original_indicators)
|
| 230 |
+
|
| 231 |
+
if all_indicators:
|
| 232 |
+
from collections import Counter
|
| 233 |
+
indicator_counts = Counter(all_indicators)
|
| 234 |
+
f.write("Most Common Indicators:\n")
|
| 235 |
+
for indicator, count in indicator_counts.most_common(10):
|
| 236 |
+
f.write(f" {indicator}: {count} occurrences\n")
|
| 237 |
+
|
| 238 |
+
# Provider summary urgency analysis
|
| 239 |
+
if records_with_summary > 0:
|
| 240 |
+
f.write("\nPROVIDER SUMMARY ANALYSIS\n")
|
| 241 |
+
f.write("-" * 40 + "\n")
|
| 242 |
+
|
| 243 |
+
urgency_counts = {}
|
| 244 |
+
severity_counts = {}
|
| 245 |
+
|
| 246 |
+
for record in session.verification_records:
|
| 247 |
+
if record.provider_summary:
|
| 248 |
+
urgency = record.provider_summary.get('urgency_level', 'UNKNOWN')
|
| 249 |
+
severity = record.provider_summary.get('severity_level', 'UNKNOWN')
|
| 250 |
+
urgency_counts[urgency] = urgency_counts.get(urgency, 0) + 1
|
| 251 |
+
severity_counts[severity] = severity_counts.get(severity, 0) + 1
|
| 252 |
+
|
| 253 |
+
f.write("Urgency Level Distribution:\n")
|
| 254 |
+
for urgency, count in urgency_counts.items():
|
| 255 |
+
f.write(f" {urgency}: {count} cases\n")
|
| 256 |
+
|
| 257 |
+
f.write("Severity Level Distribution:\n")
|
| 258 |
+
for severity, count in severity_counts.items():
|
| 259 |
+
f.write(f" {severity}: {count} cases\n")
|
| 260 |
+
|
| 261 |
+
f.write("\n" + "=" * 80 + "\n")
|
| 262 |
+
f.write("END OF ENHANCED SUMMARY REPORT\n")
|
| 263 |
+
f.write("=" * 80 + "\n")
|
| 264 |
|
| 265 |
return filepath
|
| 266 |
|
| 267 |
except Exception as e:
|
| 268 |
+
raise Exception(f"Failed to export enhanced summary report: {str(e)}")
|
| 269 |
|
| 270 |
+
def _create_export_filename(self, session: EnhancedVerificationSession) -> str:
|
| 271 |
+
"""Create descriptive filename for enhanced export."""
|
| 272 |
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
| 273 |
patient_safe = session.patient_name.replace(' ', '_').replace('/', '_')
|
| 274 |
+
enhanced_suffix = "_enhanced" if getattr(session, 'enhanced_format_enabled', False) else ""
|
| 275 |
+
return f"conversation_verification_{patient_safe}_{timestamp}{enhanced_suffix}.csv"
|
| 276 |
|
| 277 |
+
def generate_enhanced_summary_report(self, session: EnhancedVerificationSession) -> Dict[str, Any]:
|
| 278 |
+
"""Generate enhanced summary report for verification session."""
|
| 279 |
progress = session.get_progress()
|
| 280 |
|
| 281 |
# Calculate detailed statistics
|
|
|
|
| 283 |
correct_records = [r for r in verified_records if r.is_correct]
|
| 284 |
incorrect_records = [r for r in verified_records if not r.is_correct]
|
| 285 |
|
| 286 |
+
# Enhanced format statistics
|
| 287 |
+
records_with_enhanced = sum(1 for r in session.verification_records if r.enhanced_display_format)
|
| 288 |
+
records_with_summary = sum(1 for r in session.verification_records if r.provider_summary)
|
| 289 |
+
records_with_paragraph = sum(1 for r in session.verification_records if r.coherent_summary_paragraph)
|
| 290 |
+
|
| 291 |
# Error analysis
|
| 292 |
error_patterns = {}
|
| 293 |
for record in incorrect_records:
|
|
|
|
| 302 |
'incorrect_avg_confidence': sum(r.original_confidence for r in incorrect_records) / len(incorrect_records) if incorrect_records else 0
|
| 303 |
}
|
| 304 |
|
| 305 |
+
# Enhanced format analysis
|
| 306 |
+
enhanced_format_stats = {
|
| 307 |
+
'total_records': len(session.verification_records),
|
| 308 |
+
'records_with_enhanced_display': records_with_enhanced,
|
| 309 |
+
'records_with_provider_summary': records_with_summary,
|
| 310 |
+
'records_with_coherent_paragraph': records_with_paragraph,
|
| 311 |
+
'enhanced_display_coverage': (records_with_enhanced / len(session.verification_records)) * 100 if session.verification_records else 0,
|
| 312 |
+
'provider_summary_coverage': (records_with_summary / len(session.verification_records)) * 100 if session.verification_records else 0,
|
| 313 |
+
'coherent_paragraph_coverage': (records_with_paragraph / len(session.verification_records)) * 100 if session.verification_records else 0
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
return {
|
| 317 |
'session_info': {
|
| 318 |
'session_id': session.session_id,
|
|
|
|
| 320 |
'verifier_name': session.verifier_name,
|
| 321 |
'start_time': session.start_time.isoformat(),
|
| 322 |
'end_time': session.end_time.isoformat() if session.end_time else None,
|
| 323 |
+
'duration_minutes': (session.end_time - session.start_time).total_seconds() / 60 if session.end_time else None,
|
| 324 |
+
'enhanced_format_enabled': getattr(session, 'enhanced_format_enabled', False)
|
| 325 |
},
|
| 326 |
'verification_stats': {
|
| 327 |
'total_exchanges': session.total_exchanges,
|
|
|
|
| 336 |
'error_patterns': error_patterns,
|
| 337 |
'common_errors': progress.common_errors
|
| 338 |
},
|
| 339 |
+
'confidence_analysis': confidence_stats,
|
| 340 |
+
'enhanced_format_analysis': enhanced_format_stats
|
| 341 |
}
|
|
@@ -24,10 +24,16 @@ from dataclasses import asdict
|
|
| 24 |
# conversation verification, but add compatibility methods for verification_mode
|
| 25 |
# without changing existing callers.
|
| 26 |
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
|
| 32 |
from src.core.verification_models import (
|
| 33 |
VerificationSession as ModeVerificationSession,
|
|
@@ -109,6 +115,10 @@ class JSONVerificationStore:
|
|
| 109 |
if record.get("verification_timestamp") and isinstance(record["verification_timestamp"], datetime):
|
| 110 |
record["verification_timestamp"] = record["verification_timestamp"].isoformat()
|
| 111 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
# Add metadata for recovery / forward compatibility.
|
| 113 |
session_dict["_metadata"] = {
|
| 114 |
"saved_at": datetime.now().isoformat(),
|
|
@@ -162,22 +172,63 @@ class JSONVerificationStore:
|
|
| 162 |
|
| 163 |
# Conversation verification sessions include start_time.
|
| 164 |
if "start_time" in session_dict:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
session_dict["start_time"] = datetime.fromisoformat(session_dict["start_time"])
|
| 166 |
if session_dict.get("end_time"):
|
| 167 |
session_dict["end_time"] = datetime.fromisoformat(session_dict["end_time"])
|
| 168 |
else:
|
| 169 |
session_dict["end_time"] = None
|
| 170 |
|
| 171 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
for record_dict in session_dict.get("verification_records", []):
|
| 173 |
record_dict["timestamp"] = datetime.fromisoformat(record_dict["timestamp"])
|
| 174 |
if record_dict.get("verification_timestamp"):
|
| 175 |
record_dict["verification_timestamp"] = datetime.fromisoformat(record_dict["verification_timestamp"])
|
| 176 |
else:
|
| 177 |
record_dict["verification_timestamp"] = None
|
| 178 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
session_dict["verification_records"] = verification_records
|
| 180 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
|
| 182 |
# Unknown format
|
| 183 |
return None
|
|
|
|
| 24 |
# conversation verification, but add compatibility methods for verification_mode
|
| 25 |
# without changing existing callers.
|
| 26 |
|
| 27 |
+
# Import types for type hints only to avoid circular imports
|
| 28 |
+
from typing import TYPE_CHECKING
|
| 29 |
+
|
| 30 |
+
if TYPE_CHECKING:
|
| 31 |
+
from src.core.conversation_verification import (
|
| 32 |
+
VerificationSession as ConversationVerificationSession,
|
| 33 |
+
VerificationRecord as ConversationVerificationRecord,
|
| 34 |
+
EnhancedVerificationSession,
|
| 35 |
+
EnhancedVerificationRecord,
|
| 36 |
+
)
|
| 37 |
|
| 38 |
from src.core.verification_models import (
|
| 39 |
VerificationSession as ModeVerificationSession,
|
|
|
|
| 115 |
if record.get("verification_timestamp") and isinstance(record["verification_timestamp"], datetime):
|
| 116 |
record["verification_timestamp"] = record["verification_timestamp"].isoformat()
|
| 117 |
|
| 118 |
+
# Remove non-serializable fields for enhanced sessions
|
| 119 |
+
if "display_manager" in session_dict:
|
| 120 |
+
session_dict.pop("display_manager", None)
|
| 121 |
+
|
| 122 |
# Add metadata for recovery / forward compatibility.
|
| 123 |
session_dict["_metadata"] = {
|
| 124 |
"saved_at": datetime.now().isoformat(),
|
|
|
|
| 172 |
|
| 173 |
# Conversation verification sessions include start_time.
|
| 174 |
if "start_time" in session_dict:
|
| 175 |
+
# Import here to avoid circular imports
|
| 176 |
+
from src.core.conversation_verification import (
|
| 177 |
+
VerificationSession as ConversationVerificationSession,
|
| 178 |
+
VerificationRecord as ConversationVerificationRecord,
|
| 179 |
+
EnhancedVerificationSession,
|
| 180 |
+
EnhancedVerificationRecord,
|
| 181 |
+
)
|
| 182 |
+
|
| 183 |
session_dict["start_time"] = datetime.fromisoformat(session_dict["start_time"])
|
| 184 |
if session_dict.get("end_time"):
|
| 185 |
session_dict["end_time"] = datetime.fromisoformat(session_dict["end_time"])
|
| 186 |
else:
|
| 187 |
session_dict["end_time"] = None
|
| 188 |
|
| 189 |
+
# Check if this is an enhanced conversation verification session
|
| 190 |
+
has_enhanced_fields = any(
|
| 191 |
+
field in session_dict for field in
|
| 192 |
+
['enhanced_format_enabled', 'display_manager']
|
| 193 |
+
)
|
| 194 |
+
|
| 195 |
+
verification_records = []
|
| 196 |
for record_dict in session_dict.get("verification_records", []):
|
| 197 |
record_dict["timestamp"] = datetime.fromisoformat(record_dict["timestamp"])
|
| 198 |
if record_dict.get("verification_timestamp"):
|
| 199 |
record_dict["verification_timestamp"] = datetime.fromisoformat(record_dict["verification_timestamp"])
|
| 200 |
else:
|
| 201 |
record_dict["verification_timestamp"] = None
|
| 202 |
+
|
| 203 |
+
# Check if record has enhanced fields
|
| 204 |
+
has_enhanced_record_fields = any(
|
| 205 |
+
field in record_dict for field in
|
| 206 |
+
['enhanced_display_format', 'provider_summary', 'coherent_summary_paragraph', 'visual_sections']
|
| 207 |
+
)
|
| 208 |
+
|
| 209 |
+
if has_enhanced_record_fields or has_enhanced_fields:
|
| 210 |
+
verification_records.append(EnhancedVerificationRecord(**record_dict))
|
| 211 |
+
else:
|
| 212 |
+
verification_records.append(ConversationVerificationRecord(**record_dict))
|
| 213 |
+
|
| 214 |
session_dict["verification_records"] = verification_records
|
| 215 |
+
|
| 216 |
+
# Create appropriate session type
|
| 217 |
+
if has_enhanced_fields or any(isinstance(r, EnhancedVerificationRecord) for r in verification_records):
|
| 218 |
+
# Set defaults for enhanced fields if missing
|
| 219 |
+
session_dict.setdefault('enhanced_format_enabled', True)
|
| 220 |
+
# Don't restore display_manager from JSON - create new one
|
| 221 |
+
session_dict.pop('display_manager', None)
|
| 222 |
+
|
| 223 |
+
enhanced_session = EnhancedVerificationSession(**session_dict)
|
| 224 |
+
# Create new display manager if enhanced formats are enabled
|
| 225 |
+
if enhanced_session.enhanced_format_enabled:
|
| 226 |
+
from src.interface.enhanced_results_display_manager import EnhancedResultsDisplayManager
|
| 227 |
+
enhanced_session.display_manager = EnhancedResultsDisplayManager()
|
| 228 |
+
|
| 229 |
+
return enhanced_session
|
| 230 |
+
else:
|
| 231 |
+
return ConversationVerificationSession(**session_dict)
|
| 232 |
|
| 233 |
# Unknown format
|
| 234 |
return None
|
|
@@ -3,9 +3,11 @@ import gradio as gr
|
|
| 3 |
import html
|
| 4 |
from src.interface.session_manager import SimplifiedSessionData
|
| 5 |
from src.interface.stats_handlers import get_conversation_stats
|
|
|
|
|
|
|
| 6 |
|
| 7 |
def handle_message(message: str, history, session: SimplifiedSessionData):
|
| 8 |
-
"""Handle user message."""
|
| 9 |
if session is None:
|
| 10 |
session = SimplifiedSessionData()
|
| 11 |
|
|
@@ -24,6 +26,10 @@ def handle_message(message: str, history, session: SimplifiedSessionData):
|
|
| 24 |
session.app_instance.set_prompt_overrides(custom_prompts)
|
| 25 |
else:
|
| 26 |
session.app_instance.set_prompt_overrides({})
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
new_history, status = session.app_instance.process_message(message, history)
|
| 28 |
|
| 29 |
# Get updated conversation stats
|
|
@@ -31,7 +37,7 @@ def handle_message(message: str, history, session: SimplifiedSessionData):
|
|
| 31 |
|
| 32 |
# Check for provider summary (RED flag case)
|
| 33 |
provider_summary_text = ""
|
| 34 |
-
|
| 35 |
show_provider_panel = False
|
| 36 |
last_summary = session.app_instance.get_last_provider_summary()
|
| 37 |
|
|
@@ -40,20 +46,17 @@ def handle_message(message: str, history, session: SimplifiedSessionData):
|
|
| 40 |
if last_summary:
|
| 41 |
print(f"DEBUG: summary patient: {last_summary.patient_name}")
|
| 42 |
print(f"DEBUG: summary indicators: {last_summary.indicators}")
|
| 43 |
-
provider_summary_text = session.app_instance.provider_summary_generator.format_for_display(last_summary)
|
| 44 |
|
| 45 |
-
#
|
|
|
|
|
|
|
|
|
|
| 46 |
try:
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
session_id=session.session_id
|
| 50 |
-
)
|
| 51 |
-
if not spiritual_care_msg:
|
| 52 |
-
spiritual_care_msg = ""
|
| 53 |
-
print(f"DEBUG: spiritual care message generated: {len(spiritual_care_msg)} chars")
|
| 54 |
except Exception as e:
|
| 55 |
-
print(f"DEBUG: Error generating
|
| 56 |
-
|
| 57 |
|
| 58 |
show_provider_panel = True
|
| 59 |
print(f"DEBUG: formatted summary length: {len(provider_summary_text)}")
|
|
@@ -66,21 +69,31 @@ def handle_message(message: str, history, session: SimplifiedSessionData):
|
|
| 66 |
if provider_summary_text:
|
| 67 |
print(f"DEBUG RETURN: first 100 chars: {provider_summary_text[:100]}")
|
| 68 |
|
| 69 |
-
#
|
| 70 |
-
if provider_summary_text
|
| 71 |
-
escaped_text = html.escape(provider_summary_text)
|
| 72 |
-
html_content = f"<pre style='white-space: pre-wrap; font-family: monospace; font-size: 11px; background: #fffbeb; padding: 10px; border-radius: 8px; max-height: 400px; overflow-y: auto;'>{escaped_text}</pre>"
|
| 73 |
-
else:
|
| 74 |
-
html_content = "<pre style='white-space: pre-wrap; font-family: monospace; font-size: 11px; background: #fffbeb; padding: 10px; border-radius: 8px;'>No summary available</pre>"
|
| 75 |
|
| 76 |
# Use gr.update for both visibility and value
|
| 77 |
if not provider_summary_text:
|
| 78 |
provider_summary_text = ""
|
| 79 |
html_content = ""
|
| 80 |
|
| 81 |
-
# Generate status message for provider summary
|
| 82 |
if show_provider_panel and provider_summary_text:
|
| 83 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
|
| 85 |
**Patient:** {session.app_instance.patient_info.get('name', 'Test Patient')}
|
| 86 |
**Classification:** RED FLAG
|
|
@@ -100,7 +113,7 @@ Use the **Download Summary** button below to access the complete provider summar
|
|
| 100 |
gr.update(visible=show_provider_panel), # provider_summary_content visibility
|
| 101 |
status_msg, # provider_summary_status content
|
| 102 |
gr.update(value=html_content, visible=True) if show_provider_panel else gr.update(visible=False), # provider_summary_display content
|
| 103 |
-
|
| 104 |
)
|
| 105 |
|
| 106 |
def handle_clear(session: SimplifiedSessionData):
|
|
@@ -127,7 +140,7 @@ def handle_clear(session: SimplifiedSessionData):
|
|
| 127 |
gr.update(visible=False), # Hide provider_summary_content group
|
| 128 |
"No provider summary available", # Clear provider_summary_status
|
| 129 |
"", # Clear provider_summary_display HTML
|
| 130 |
-
"" # Clear
|
| 131 |
)
|
| 132 |
|
| 133 |
def send_example(example_text: str, history, session: SimplifiedSessionData):
|
|
|
|
| 3 |
import html
|
| 4 |
from src.interface.session_manager import SimplifiedSessionData
|
| 5 |
from src.interface.stats_handlers import get_conversation_stats
|
| 6 |
+
from src.interface.enhanced_display_integration import create_enhanced_display_integration
|
| 7 |
+
from src.core.improved_classification_prompt_manager import ImprovedClassificationPromptManager
|
| 8 |
|
| 9 |
def handle_message(message: str, history, session: SimplifiedSessionData):
|
| 10 |
+
"""Handle user message with enhanced display formatting."""
|
| 11 |
if session is None:
|
| 12 |
session = SimplifiedSessionData()
|
| 13 |
|
|
|
|
| 26 |
session.app_instance.set_prompt_overrides(custom_prompts)
|
| 27 |
else:
|
| 28 |
session.app_instance.set_prompt_overrides({})
|
| 29 |
+
|
| 30 |
+
# Initialize enhanced display integration
|
| 31 |
+
enhanced_display = create_enhanced_display_integration()
|
| 32 |
+
|
| 33 |
new_history, status = session.app_instance.process_message(message, history)
|
| 34 |
|
| 35 |
# Get updated conversation stats
|
|
|
|
| 37 |
|
| 38 |
# Check for provider summary (RED flag case)
|
| 39 |
provider_summary_text = ""
|
| 40 |
+
coherent_summary_text = ""
|
| 41 |
show_provider_panel = False
|
| 42 |
last_summary = session.app_instance.get_last_provider_summary()
|
| 43 |
|
|
|
|
| 46 |
if last_summary:
|
| 47 |
print(f"DEBUG: summary patient: {last_summary.patient_name}")
|
| 48 |
print(f"DEBUG: summary indicators: {last_summary.indicators}")
|
|
|
|
| 49 |
|
| 50 |
+
# Use enhanced display for provider summary formatting
|
| 51 |
+
provider_summary_text = enhanced_display.display_manager.format_provider_summary_section(last_summary)
|
| 52 |
+
|
| 53 |
+
# Generate medical brain summary (NEW - Requirements 2.1-2.8)
|
| 54 |
try:
|
| 55 |
+
coherent_summary_text = session.app_instance.provider_summary_generator.format_coherent_paragraph(last_summary)
|
| 56 |
+
print(f"DEBUG: coherent summary generated: {len(coherent_summary_text)} chars")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
except Exception as e:
|
| 58 |
+
print(f"DEBUG: Error generating coherent summary: {e}")
|
| 59 |
+
coherent_summary_text = ""
|
| 60 |
|
| 61 |
show_provider_panel = True
|
| 62 |
print(f"DEBUG: formatted summary length: {len(provider_summary_text)}")
|
|
|
|
| 69 |
if provider_summary_text:
|
| 70 |
print(f"DEBUG RETURN: first 100 chars: {provider_summary_text[:100]}")
|
| 71 |
|
| 72 |
+
# Enhanced display formatting - provider summary is already HTML formatted
|
| 73 |
+
html_content = provider_summary_text if provider_summary_text else ""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
|
| 75 |
# Use gr.update for both visibility and value
|
| 76 |
if not provider_summary_text:
|
| 77 |
provider_summary_text = ""
|
| 78 |
html_content = ""
|
| 79 |
|
| 80 |
+
# Generate status message for provider summary with enhanced formatting
|
| 81 |
if show_provider_panel and provider_summary_text:
|
| 82 |
+
# Get the last assessment for enhanced status display
|
| 83 |
+
last_assessment = session.app_instance.spiritual_state.last_assessment
|
| 84 |
+
if last_assessment:
|
| 85 |
+
classification_badge = enhanced_display.get_classification_badge(last_assessment.state.value.upper())
|
| 86 |
+
status_msg = f"""**🔴 Provider Summary Generated**
|
| 87 |
+
|
| 88 |
+
{classification_badge}
|
| 89 |
+
|
| 90 |
+
**Patient:** {session.app_instance.patient_info.get('name', 'Test Patient')}
|
| 91 |
+
**Indicators:** {len(session.app_instance.get_last_provider_summary().indicators) if session.app_instance.get_last_provider_summary() else 0} distress indicators detected
|
| 92 |
+
**Summary Length:** {len(provider_summary_text)} characters
|
| 93 |
+
|
| 94 |
+
Use the **Download Summary** button below to access the complete provider summary for the spiritual care team."""
|
| 95 |
+
else:
|
| 96 |
+
status_msg = f"""**🔴 Provider Summary Generated**
|
| 97 |
|
| 98 |
**Patient:** {session.app_instance.patient_info.get('name', 'Test Patient')}
|
| 99 |
**Classification:** RED FLAG
|
|
|
|
| 113 |
gr.update(visible=show_provider_panel), # provider_summary_content visibility
|
| 114 |
status_msg, # provider_summary_status content
|
| 115 |
gr.update(value=html_content, visible=True) if show_provider_panel else gr.update(visible=False), # provider_summary_display content
|
| 116 |
+
coherent_summary_text # coherent_summary_display content (NEW)
|
| 117 |
)
|
| 118 |
|
| 119 |
def handle_clear(session: SimplifiedSessionData):
|
|
|
|
| 140 |
gr.update(visible=False), # Hide provider_summary_content group
|
| 141 |
"No provider summary available", # Clear provider_summary_status
|
| 142 |
"", # Clear provider_summary_display HTML
|
| 143 |
+
"" # Clear coherent_summary_display (NEW)
|
| 144 |
)
|
| 145 |
|
| 146 |
def send_example(example_text: str, history, session: SimplifiedSessionData):
|
|
@@ -0,0 +1,321 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# enhanced_display_integration.py
|
| 2 |
+
"""
|
| 3 |
+
Integration module for Enhanced Results Display Manager.
|
| 4 |
+
|
| 5 |
+
This module provides integration functions to connect the enhanced display
|
| 6 |
+
components with the existing chat interface and result processing system.
|
| 7 |
+
|
| 8 |
+
Requirements: 1.1, 1.2, 7.1, 7.2
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
from typing import Optional, Dict, Any, List, Tuple
|
| 12 |
+
import html
|
| 13 |
+
|
| 14 |
+
from src.interface.enhanced_results_display_manager import (
|
| 15 |
+
EnhancedResultsDisplayManager,
|
| 16 |
+
EnhancedDisplayConfig
|
| 17 |
+
)
|
| 18 |
+
from src.interface.visual_separation_manager import VisualSeparationManager
|
| 19 |
+
from src.interface.provider_summary_formatter import ProviderSummaryFormatter
|
| 20 |
+
from src.core.provider_summary_generator import ProviderSummary
|
| 21 |
+
from src.core.spiritual_state import SpiritualAssessment
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class EnhancedDisplayIntegration:
|
| 25 |
+
"""
|
| 26 |
+
Integration class for enhanced display components.
|
| 27 |
+
|
| 28 |
+
Provides a unified interface for integrating enhanced display functionality
|
| 29 |
+
with the existing chat interface and result processing system.
|
| 30 |
+
"""
|
| 31 |
+
|
| 32 |
+
def __init__(self, config: Optional[EnhancedDisplayConfig] = None):
|
| 33 |
+
"""
|
| 34 |
+
Initialize the enhanced display integration.
|
| 35 |
+
|
| 36 |
+
Args:
|
| 37 |
+
config: Optional configuration for display formatting
|
| 38 |
+
"""
|
| 39 |
+
self.display_manager = EnhancedResultsDisplayManager(config)
|
| 40 |
+
self.visual_manager = VisualSeparationManager()
|
| 41 |
+
self.summary_formatter = ProviderSummaryFormatter()
|
| 42 |
+
|
| 43 |
+
def format_chat_response(
|
| 44 |
+
self,
|
| 45 |
+
assessment: Optional[SpiritualAssessment] = None,
|
| 46 |
+
patient_message: Optional[str] = None,
|
| 47 |
+
provider_summary: Optional[ProviderSummary] = None,
|
| 48 |
+
show_ai_analysis: bool = True
|
| 49 |
+
) -> str:
|
| 50 |
+
"""
|
| 51 |
+
Format a complete chat response with enhanced display.
|
| 52 |
+
|
| 53 |
+
Args:
|
| 54 |
+
assessment: Optional spiritual assessment from AI analysis
|
| 55 |
+
patient_message: Optional patient message to display
|
| 56 |
+
provider_summary: Optional provider summary for RED flags
|
| 57 |
+
show_ai_analysis: Whether to show AI analysis section
|
| 58 |
+
|
| 59 |
+
Returns:
|
| 60 |
+
Formatted HTML string for display in chat interface
|
| 61 |
+
|
| 62 |
+
Requirements: 1.1, 1.2, 7.1, 7.2
|
| 63 |
+
"""
|
| 64 |
+
# Prepare AI analysis data if available and requested
|
| 65 |
+
ai_analysis = None
|
| 66 |
+
if assessment and show_ai_analysis:
|
| 67 |
+
ai_analysis = {
|
| 68 |
+
"classification": assessment.state.value.upper(),
|
| 69 |
+
"indicators": assessment.indicators,
|
| 70 |
+
"reasoning": assessment.reasoning,
|
| 71 |
+
"confidence": assessment.confidence
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
# Format combined results
|
| 75 |
+
return self.display_manager.format_combined_results(
|
| 76 |
+
ai_analysis=ai_analysis,
|
| 77 |
+
patient_message=patient_message,
|
| 78 |
+
provider_summary=provider_summary
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
def format_provider_summary_coherent(
|
| 82 |
+
self,
|
| 83 |
+
summary: ProviderSummary
|
| 84 |
+
) -> str:
|
| 85 |
+
"""
|
| 86 |
+
Format provider summary as coherent paragraph.
|
| 87 |
+
|
| 88 |
+
Args:
|
| 89 |
+
summary: Provider summary to format
|
| 90 |
+
|
| 91 |
+
Returns:
|
| 92 |
+
Coherent paragraph formatted summary
|
| 93 |
+
|
| 94 |
+
Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8
|
| 95 |
+
"""
|
| 96 |
+
return self.summary_formatter.format_from_provider_summary(summary)
|
| 97 |
+
|
| 98 |
+
def create_section_with_styling(
|
| 99 |
+
self,
|
| 100 |
+
content: str,
|
| 101 |
+
section_type: str,
|
| 102 |
+
title: Optional[str] = None,
|
| 103 |
+
icon: Optional[str] = None
|
| 104 |
+
) -> str:
|
| 105 |
+
"""
|
| 106 |
+
Create a styled section with consistent formatting.
|
| 107 |
+
|
| 108 |
+
Args:
|
| 109 |
+
content: The content to display
|
| 110 |
+
section_type: Type of section (ai_analysis, patient_message, provider_summary)
|
| 111 |
+
title: Optional custom title for the section
|
| 112 |
+
icon: Optional custom icon for the section
|
| 113 |
+
|
| 114 |
+
Returns:
|
| 115 |
+
Styled HTML section
|
| 116 |
+
|
| 117 |
+
Requirements: 7.1, 7.2, 7.3
|
| 118 |
+
"""
|
| 119 |
+
# Get appropriate styling
|
| 120 |
+
if section_type == "ai_analysis":
|
| 121 |
+
styling = self.visual_manager.create_ai_analysis_styling()
|
| 122 |
+
default_title = "AI Analysis"
|
| 123 |
+
default_icon = "🤖"
|
| 124 |
+
elif section_type == "patient_message":
|
| 125 |
+
styling = self.visual_manager.create_patient_message_styling()
|
| 126 |
+
default_title = "Patient Message"
|
| 127 |
+
default_icon = "💬"
|
| 128 |
+
elif section_type == "provider_summary":
|
| 129 |
+
styling = self.visual_manager.create_provider_summary_styling()
|
| 130 |
+
default_title = "Provider Summary"
|
| 131 |
+
default_icon = "📋"
|
| 132 |
+
else:
|
| 133 |
+
# Default styling
|
| 134 |
+
styling = {
|
| 135 |
+
"container": "padding: 10px; margin: 10px 0; border: 1px solid #ddd; border-radius: 4px;"
|
| 136 |
+
}
|
| 137 |
+
default_title = "Content"
|
| 138 |
+
default_icon = "📄"
|
| 139 |
+
|
| 140 |
+
# Use provided title/icon or defaults
|
| 141 |
+
section_title = title or default_title
|
| 142 |
+
section_icon = icon or default_icon
|
| 143 |
+
|
| 144 |
+
# Build styled section
|
| 145 |
+
return f"""
|
| 146 |
+
<div style='{styling.get("container", "")}'>
|
| 147 |
+
<div style='{styling.get("header", "")}'>
|
| 148 |
+
<span style='{styling.get("icon", "")}'>{section_icon}</span>
|
| 149 |
+
<strong>{section_title}</strong>
|
| 150 |
+
</div>
|
| 151 |
+
<div style='{styling.get("content", "")}'>
|
| 152 |
+
{html.escape(content) if isinstance(content, str) else content}
|
| 153 |
+
</div>
|
| 154 |
+
</div>
|
| 155 |
+
"""
|
| 156 |
+
|
| 157 |
+
def get_classification_badge(self, classification: str) -> str:
|
| 158 |
+
"""
|
| 159 |
+
Get a styled classification badge.
|
| 160 |
+
|
| 161 |
+
Args:
|
| 162 |
+
classification: The classification level (RED/YELLOW/GREEN)
|
| 163 |
+
|
| 164 |
+
Returns:
|
| 165 |
+
HTML badge for the classification
|
| 166 |
+
|
| 167 |
+
Requirements: 7.3
|
| 168 |
+
"""
|
| 169 |
+
styling = self.visual_manager.get_classification_styling(classification)
|
| 170 |
+
|
| 171 |
+
return f"""
|
| 172 |
+
<span style='{styling["badge"]}'>
|
| 173 |
+
{classification.upper()} FLAG
|
| 174 |
+
</span>
|
| 175 |
+
"""
|
| 176 |
+
|
| 177 |
+
def get_urgency_badge(self, urgency_level: str) -> str:
|
| 178 |
+
"""
|
| 179 |
+
Get a styled urgency badge.
|
| 180 |
+
|
| 181 |
+
Args:
|
| 182 |
+
urgency_level: The urgency level (IMMEDIATE/URGENT/STANDARD)
|
| 183 |
+
|
| 184 |
+
Returns:
|
| 185 |
+
HTML badge for the urgency level
|
| 186 |
+
|
| 187 |
+
Requirements: 7.3
|
| 188 |
+
"""
|
| 189 |
+
styling = self.visual_manager.get_urgency_styling(urgency_level)
|
| 190 |
+
|
| 191 |
+
return f"""
|
| 192 |
+
<span style='{styling["badge"]}'>
|
| 193 |
+
{urgency_level.upper()}
|
| 194 |
+
</span>
|
| 195 |
+
"""
|
| 196 |
+
|
| 197 |
+
def create_content_separator(self, separator_type: str = "section_break") -> str:
|
| 198 |
+
"""
|
| 199 |
+
Create a content separator.
|
| 200 |
+
|
| 201 |
+
Args:
|
| 202 |
+
separator_type: Type of separator (light, medium, heavy, section_break)
|
| 203 |
+
|
| 204 |
+
Returns:
|
| 205 |
+
HTML separator
|
| 206 |
+
|
| 207 |
+
Requirements: 7.4, 7.5
|
| 208 |
+
"""
|
| 209 |
+
separators = self.visual_manager.generate_section_separators()
|
| 210 |
+
return separators.get(separator_type, separators["section_break"])
|
| 211 |
+
|
| 212 |
+
def format_multiple_sections(self, sections: List[Dict[str, Any]]) -> str:
|
| 213 |
+
"""
|
| 214 |
+
Format multiple content sections with consistent styling.
|
| 215 |
+
|
| 216 |
+
Args:
|
| 217 |
+
sections: List of section dictionaries with 'type' and 'content' keys
|
| 218 |
+
|
| 219 |
+
Returns:
|
| 220 |
+
Combined HTML with all sections and separators
|
| 221 |
+
|
| 222 |
+
Requirements: 7.1, 7.2, 7.4, 7.5
|
| 223 |
+
"""
|
| 224 |
+
return self.visual_manager.apply_consistent_formatting(sections)
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
# Convenience function for easy integration
|
| 228 |
+
def create_enhanced_display_integration(
|
| 229 |
+
config: Optional[EnhancedDisplayConfig] = None
|
| 230 |
+
) -> EnhancedDisplayIntegration:
|
| 231 |
+
"""
|
| 232 |
+
Create an enhanced display integration instance.
|
| 233 |
+
|
| 234 |
+
Args:
|
| 235 |
+
config: Optional configuration for display formatting
|
| 236 |
+
|
| 237 |
+
Returns:
|
| 238 |
+
Configured EnhancedDisplayIntegration instance
|
| 239 |
+
"""
|
| 240 |
+
return EnhancedDisplayIntegration(config)
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
# Example usage functions for demonstration
|
| 244 |
+
def example_format_ai_analysis_result(
|
| 245 |
+
classification: str,
|
| 246 |
+
indicators: List[str],
|
| 247 |
+
reasoning: str,
|
| 248 |
+
confidence: float = None
|
| 249 |
+
) -> str:
|
| 250 |
+
"""
|
| 251 |
+
Example function showing how to format AI analysis results.
|
| 252 |
+
|
| 253 |
+
Args:
|
| 254 |
+
classification: Classification result (RED/YELLOW/GREEN)
|
| 255 |
+
indicators: List of distress indicators
|
| 256 |
+
reasoning: AI reasoning for classification
|
| 257 |
+
confidence: Optional confidence score
|
| 258 |
+
|
| 259 |
+
Returns:
|
| 260 |
+
Formatted HTML for AI analysis display
|
| 261 |
+
"""
|
| 262 |
+
integration = create_enhanced_display_integration()
|
| 263 |
+
|
| 264 |
+
return integration.display_manager.format_ai_analysis_section(
|
| 265 |
+
classification=classification,
|
| 266 |
+
indicators=indicators,
|
| 267 |
+
reasoning=reasoning,
|
| 268 |
+
confidence=confidence
|
| 269 |
+
)
|
| 270 |
+
|
| 271 |
+
|
| 272 |
+
def example_format_patient_message(message: str) -> str:
|
| 273 |
+
"""
|
| 274 |
+
Example function showing how to format patient messages.
|
| 275 |
+
|
| 276 |
+
Args:
|
| 277 |
+
message: Patient's message content
|
| 278 |
+
|
| 279 |
+
Returns:
|
| 280 |
+
Formatted HTML for patient message display
|
| 281 |
+
"""
|
| 282 |
+
integration = create_enhanced_display_integration()
|
| 283 |
+
|
| 284 |
+
return integration.display_manager.format_patient_message_section(message)
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
def example_format_complete_response(
|
| 288 |
+
patient_message: str,
|
| 289 |
+
classification: str,
|
| 290 |
+
indicators: List[str],
|
| 291 |
+
reasoning: str,
|
| 292 |
+
provider_summary: Optional[ProviderSummary] = None
|
| 293 |
+
) -> str:
|
| 294 |
+
"""
|
| 295 |
+
Example function showing complete response formatting.
|
| 296 |
+
|
| 297 |
+
Args:
|
| 298 |
+
patient_message: Patient's message
|
| 299 |
+
classification: AI classification result
|
| 300 |
+
indicators: Distress indicators
|
| 301 |
+
reasoning: AI reasoning
|
| 302 |
+
provider_summary: Optional provider summary for RED flags
|
| 303 |
+
|
| 304 |
+
Returns:
|
| 305 |
+
Complete formatted response with all sections
|
| 306 |
+
"""
|
| 307 |
+
integration = create_enhanced_display_integration()
|
| 308 |
+
|
| 309 |
+
# Create AI analysis data
|
| 310 |
+
ai_analysis = {
|
| 311 |
+
"classification": classification,
|
| 312 |
+
"indicators": indicators,
|
| 313 |
+
"reasoning": reasoning,
|
| 314 |
+
"confidence": 0.85
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
return integration.display_manager.format_combined_results(
|
| 318 |
+
ai_analysis=ai_analysis,
|
| 319 |
+
patient_message=patient_message,
|
| 320 |
+
provider_summary=provider_summary
|
| 321 |
+
)
|
|
@@ -0,0 +1,533 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# enhanced_results_display_manager.py
|
| 2 |
+
"""
|
| 3 |
+
Enhanced Results Display Manager for UI Classification Improvements.
|
| 4 |
+
|
| 5 |
+
This module provides enhanced display formatting for chat results with clear
|
| 6 |
+
visual separation between AI analysis, patient messages, and provider summaries.
|
| 7 |
+
|
| 8 |
+
Requirements: 1.1, 1.2, 7.1, 7.2
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import html
|
| 12 |
+
from typing import Dict, List, Optional, Any
|
| 13 |
+
from dataclasses import dataclass
|
| 14 |
+
from enum import Enum
|
| 15 |
+
|
| 16 |
+
from src.core.provider_summary_generator import ProviderSummary
|
| 17 |
+
from src.core.spiritual_state import SpiritualAssessment, SpiritualState
|
| 18 |
+
from src.config.enhanced_display_config import (
|
| 19 |
+
EnhancedDisplayConfig,
|
| 20 |
+
get_enhanced_display_config,
|
| 21 |
+
EnhancedDisplayConfigManager
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class SectionType(Enum):
|
| 26 |
+
"""Types of content sections for display."""
|
| 27 |
+
AI_ANALYSIS = "ai_analysis"
|
| 28 |
+
PATIENT_MESSAGE = "patient_message"
|
| 29 |
+
PROVIDER_SUMMARY = "provider_summary"
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
@dataclass
|
| 33 |
+
class ContentSection:
|
| 34 |
+
"""Represents a content section with styling information."""
|
| 35 |
+
section_type: SectionType
|
| 36 |
+
content: str
|
| 37 |
+
styling: Dict[str, str]
|
| 38 |
+
icon: Optional[str] = None
|
| 39 |
+
priority: int = 0
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
@dataclass
|
| 43 |
+
class ContentSection:
|
| 44 |
+
"""Represents a content section with styling information."""
|
| 45 |
+
section_type: SectionType
|
| 46 |
+
content: str
|
| 47 |
+
styling: Dict[str, str]
|
| 48 |
+
icon: Optional[str] = None
|
| 49 |
+
priority: int = 0
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
class EnhancedResultsDisplayManager:
|
| 53 |
+
"""
|
| 54 |
+
Enhanced Results Display Manager for improved UI formatting.
|
| 55 |
+
|
| 56 |
+
Provides methods to format different types of content with clear visual
|
| 57 |
+
separation and consistent styling according to requirements 1.1, 1.2, 7.1, 7.2.
|
| 58 |
+
"""
|
| 59 |
+
|
| 60 |
+
def __init__(self, config: Optional[EnhancedDisplayConfig] = None, config_manager: Optional[EnhancedDisplayConfigManager] = None):
|
| 61 |
+
"""
|
| 62 |
+
Initialize the display manager.
|
| 63 |
+
|
| 64 |
+
Args:
|
| 65 |
+
config: Optional configuration for display formatting
|
| 66 |
+
config_manager: Optional configuration manager for dynamic updates
|
| 67 |
+
"""
|
| 68 |
+
self.config_manager = config_manager
|
| 69 |
+
if config is not None:
|
| 70 |
+
self.config = config
|
| 71 |
+
elif self.config_manager is not None:
|
| 72 |
+
self.config = self.config_manager.get_config()
|
| 73 |
+
else:
|
| 74 |
+
self.config = get_enhanced_display_config()
|
| 75 |
+
|
| 76 |
+
# Initialize error handler for validation and recovery (lazy loaded)
|
| 77 |
+
self._error_handler = None
|
| 78 |
+
|
| 79 |
+
@property
|
| 80 |
+
def error_handler(self):
|
| 81 |
+
"""Lazy load error handler to avoid circular imports."""
|
| 82 |
+
if self._error_handler is None:
|
| 83 |
+
from src.core.ui_error_handler import UIErrorHandler
|
| 84 |
+
self._error_handler = UIErrorHandler()
|
| 85 |
+
return self._error_handler
|
| 86 |
+
|
| 87 |
+
def format_ai_analysis_section(
|
| 88 |
+
self,
|
| 89 |
+
classification: str,
|
| 90 |
+
indicators: List[str],
|
| 91 |
+
reasoning: str,
|
| 92 |
+
confidence: Optional[float] = None
|
| 93 |
+
) -> str:
|
| 94 |
+
"""
|
| 95 |
+
Format AI analysis section with clear labeling and visual styling.
|
| 96 |
+
|
| 97 |
+
Args:
|
| 98 |
+
classification: The classification result (RED/YELLOW/GREEN)
|
| 99 |
+
indicators: List of distress indicators
|
| 100 |
+
reasoning: AI reasoning for the classification
|
| 101 |
+
confidence: Optional confidence score
|
| 102 |
+
|
| 103 |
+
Returns:
|
| 104 |
+
Formatted HTML string for AI analysis section
|
| 105 |
+
|
| 106 |
+
Requirements: 1.1, 7.1
|
| 107 |
+
"""
|
| 108 |
+
try:
|
| 109 |
+
# Validate inputs
|
| 110 |
+
if not classification:
|
| 111 |
+
classification = "UNKNOWN"
|
| 112 |
+
if not indicators:
|
| 113 |
+
indicators = ["No indicators available"]
|
| 114 |
+
if not reasoning:
|
| 115 |
+
reasoning = "No reasoning provided"
|
| 116 |
+
|
| 117 |
+
# Check if enhancements are enabled
|
| 118 |
+
if not self.config.enabled:
|
| 119 |
+
return self._format_basic_ai_analysis(classification, indicators, reasoning, confidence)
|
| 120 |
+
|
| 121 |
+
# Get classification color
|
| 122 |
+
color = self.config.get_classification_color(classification)
|
| 123 |
+
section_config = self.config.get_section_config("ai_analysis")
|
| 124 |
+
|
| 125 |
+
# Build confidence display
|
| 126 |
+
confidence_text = ""
|
| 127 |
+
if confidence is not None:
|
| 128 |
+
confidence_percent = int(confidence * 100)
|
| 129 |
+
confidence_text = f"<div style='margin: 5px 0; font-size: {self.config.small_font_size}; color: #666;'>Confidence: {confidence_percent}%</div>"
|
| 130 |
+
|
| 131 |
+
# Build indicators list
|
| 132 |
+
indicators_html = ""
|
| 133 |
+
if indicators:
|
| 134 |
+
indicators_list = "".join([f"<li>{html.escape(indicator)}</li>" for indicator in indicators])
|
| 135 |
+
indicators_html = f"""
|
| 136 |
+
<div style='margin: 10px 0;'>
|
| 137 |
+
<strong>Indicators:</strong>
|
| 138 |
+
<ul style='margin: 5px 0 0 20px; padding: 0;'>
|
| 139 |
+
{indicators_list}
|
| 140 |
+
</ul>
|
| 141 |
+
</div>
|
| 142 |
+
"""
|
| 143 |
+
|
| 144 |
+
# Build reasoning section
|
| 145 |
+
reasoning_html = f"""
|
| 146 |
+
<div style='margin: 10px 0;'>
|
| 147 |
+
<strong>Reasoning:</strong>
|
| 148 |
+
<div style='margin: 5px 0; padding: 8px; background-color: #f8f9fa; border-radius: 4px; font-style: italic;'>
|
| 149 |
+
{html.escape(reasoning)}
|
| 150 |
+
</div>
|
| 151 |
+
</div>
|
| 152 |
+
"""
|
| 153 |
+
|
| 154 |
+
# Build icon display
|
| 155 |
+
icon_html = ""
|
| 156 |
+
if self.config.use_icons:
|
| 157 |
+
icon_html = f"<span style='font-size: 1.2em; margin-right: 8px;'>{section_config.icon}</span>"
|
| 158 |
+
|
| 159 |
+
# Combine all elements with enhanced styling
|
| 160 |
+
content = f"""
|
| 161 |
+
<div style='border: {section_config.border_width} solid {color}; border-radius: {section_config.border_radius}; padding: {section_config.padding}; margin: {section_config.margin}; background-color: {section_config.background_color}; font-family: {self.config.font_family};'>
|
| 162 |
+
<div style='display: flex; align-items: center; margin-bottom: 10px;'>
|
| 163 |
+
{icon_html}
|
| 164 |
+
<strong style='color: {color}; font-size: {self.config.header_font_size};'>AI Analysis - {classification} FLAG</strong>
|
| 165 |
+
</div>
|
| 166 |
+
{confidence_text}
|
| 167 |
+
{indicators_html}
|
| 168 |
+
{reasoning_html}
|
| 169 |
+
</div>
|
| 170 |
+
"""
|
| 171 |
+
|
| 172 |
+
return content
|
| 173 |
+
|
| 174 |
+
except Exception as e:
|
| 175 |
+
# Handle formatting errors with degraded display
|
| 176 |
+
error_context = f"AI analysis formatting failed: {str(e)}"
|
| 177 |
+
fallback_content = self._format_basic_ai_analysis(classification, indicators, reasoning, confidence)
|
| 178 |
+
return self.error_handler.create_degraded_display(error_context, fallback_content)
|
| 179 |
+
|
| 180 |
+
def format_patient_message_section(self, message: str) -> str:
|
| 181 |
+
"""
|
| 182 |
+
Format patient message section with appropriate styling.
|
| 183 |
+
|
| 184 |
+
Args:
|
| 185 |
+
message: The patient's message content
|
| 186 |
+
|
| 187 |
+
Returns:
|
| 188 |
+
Formatted HTML string for patient message section
|
| 189 |
+
|
| 190 |
+
Requirements: 1.2, 7.2
|
| 191 |
+
"""
|
| 192 |
+
try:
|
| 193 |
+
# Validate input
|
| 194 |
+
if not message:
|
| 195 |
+
message = "No message content available"
|
| 196 |
+
|
| 197 |
+
# Check if enhancements are enabled
|
| 198 |
+
if not self.config.enabled:
|
| 199 |
+
return self._format_basic_patient_message(message)
|
| 200 |
+
|
| 201 |
+
section_config = self.config.get_section_config("patient_message")
|
| 202 |
+
|
| 203 |
+
# Build icon display
|
| 204 |
+
icon_html = ""
|
| 205 |
+
if self.config.use_icons:
|
| 206 |
+
icon_html = f"<span style='font-size: 1.2em; margin-right: 8px;'>{section_config.icon}</span>"
|
| 207 |
+
|
| 208 |
+
content = f"""
|
| 209 |
+
<div style='border: {section_config.border_width} solid {section_config.border_color}; border-radius: {section_config.border_radius}; padding: {section_config.padding}; margin: {section_config.margin}; background-color: {section_config.background_color}; font-family: {self.config.font_family};'>
|
| 210 |
+
<div style='display: flex; align-items: center; margin-bottom: 10px;'>
|
| 211 |
+
{icon_html}
|
| 212 |
+
<strong style='color: {section_config.header_color}; font-size: {self.config.header_font_size};'>Patient Message</strong>
|
| 213 |
+
</div>
|
| 214 |
+
<div style='padding: 8px; background-color: white; border-radius: 4px; border-left: 4px solid {section_config.border_color};'>
|
| 215 |
+
{html.escape(message)}
|
| 216 |
+
</div>
|
| 217 |
+
</div>
|
| 218 |
+
"""
|
| 219 |
+
|
| 220 |
+
return content
|
| 221 |
+
|
| 222 |
+
except Exception as e:
|
| 223 |
+
# Handle formatting errors with degraded display
|
| 224 |
+
error_context = f"Patient message formatting failed: {str(e)}"
|
| 225 |
+
fallback_content = self._format_basic_patient_message(message)
|
| 226 |
+
return self.error_handler.create_degraded_display(error_context, fallback_content)
|
| 227 |
+
|
| 228 |
+
def format_provider_summary_section(self, summary_data: ProviderSummary) -> str:
|
| 229 |
+
"""
|
| 230 |
+
Format provider summary section with enhanced styling.
|
| 231 |
+
|
| 232 |
+
Args:
|
| 233 |
+
summary_data: Provider summary data to format
|
| 234 |
+
|
| 235 |
+
Returns:
|
| 236 |
+
Formatted HTML string for provider summary section
|
| 237 |
+
|
| 238 |
+
Requirements: 1.1, 1.2, 7.1, 7.2
|
| 239 |
+
"""
|
| 240 |
+
try:
|
| 241 |
+
# Validate and fix summary data if needed
|
| 242 |
+
validation_result = self.error_handler.validate_provider_summary_structure(summary_data)
|
| 243 |
+
|
| 244 |
+
if validation_result.has_critical_errors():
|
| 245 |
+
# Apply fallback template for critical errors
|
| 246 |
+
summary_data = self.error_handler.apply_fallback_template(summary_data, "general")
|
| 247 |
+
|
| 248 |
+
# Check if enhancements are enabled
|
| 249 |
+
if not self.config.enabled:
|
| 250 |
+
return self._format_basic_provider_summary(summary_data)
|
| 251 |
+
|
| 252 |
+
section_config = self.config.get_section_config("provider_summary")
|
| 253 |
+
|
| 254 |
+
# Get urgency color
|
| 255 |
+
urgency_colors = {
|
| 256 |
+
'IMMEDIATE': self.config.classification_colors.red,
|
| 257 |
+
'URGENT': self.config.classification_colors.yellow,
|
| 258 |
+
'STANDARD': self.config.classification_colors.green
|
| 259 |
+
}
|
| 260 |
+
urgency_color = urgency_colors.get(summary_data.urgency_level, self.config.classification_colors.red)
|
| 261 |
+
|
| 262 |
+
# Build patient info section
|
| 263 |
+
patient_info = f"""
|
| 264 |
+
<div style='margin: 10px 0;'>
|
| 265 |
+
<strong>Patient Information:</strong>
|
| 266 |
+
<div style='margin: 5px 0; padding: 8px; background-color: #f8f9fa; border-radius: 4px;'>
|
| 267 |
+
<div><strong>Name:</strong> {html.escape(summary_data.patient_name)}</div>
|
| 268 |
+
<div><strong>Phone:</strong> {html.escape(summary_data.patient_phone)}</div>
|
| 269 |
+
</div>
|
| 270 |
+
</div>
|
| 271 |
+
"""
|
| 272 |
+
|
| 273 |
+
# Build indicators section
|
| 274 |
+
indicators_html = ""
|
| 275 |
+
if summary_data.indicators:
|
| 276 |
+
indicators_list = "".join([f"<li>{html.escape(indicator)}</li>" for indicator in summary_data.indicators])
|
| 277 |
+
indicators_html = f"""
|
| 278 |
+
<div style='margin: 10px 0;'>
|
| 279 |
+
<strong>Distress Indicators:</strong>
|
| 280 |
+
<ul style='margin: 5px 0 0 20px; padding: 0;'>
|
| 281 |
+
{indicators_list}
|
| 282 |
+
</ul>
|
| 283 |
+
</div>
|
| 284 |
+
"""
|
| 285 |
+
|
| 286 |
+
# Build situation description
|
| 287 |
+
situation_html = f"""
|
| 288 |
+
<div style='margin: 10px 0;'>
|
| 289 |
+
<strong>Situation Overview:</strong>
|
| 290 |
+
<div style='margin: 5px 0; padding: 8px; background-color: #fff3cd; border-radius: 4px; border-left: 4px solid {urgency_color};'>
|
| 291 |
+
{html.escape(summary_data.situation_description)}
|
| 292 |
+
</div>
|
| 293 |
+
</div>
|
| 294 |
+
"""
|
| 295 |
+
|
| 296 |
+
# Build urgency information
|
| 297 |
+
urgency_html = f"""
|
| 298 |
+
<div style='margin: 10px 0;'>
|
| 299 |
+
<strong>Urgency Level:</strong>
|
| 300 |
+
<span style='color: {urgency_color}; font-weight: bold; margin-left: 8px;'>{summary_data.urgency_level}</span>
|
| 301 |
+
<div style='margin: 5px 0; font-size: {self.config.small_font_size}; color: #666;'>
|
| 302 |
+
Follow-up Timeline: {summary_data.follow_up_timeline}
|
| 303 |
+
</div>
|
| 304 |
+
</div>
|
| 305 |
+
"""
|
| 306 |
+
|
| 307 |
+
# Build icon display
|
| 308 |
+
icon_html = ""
|
| 309 |
+
if self.config.use_icons:
|
| 310 |
+
icon_html = f"<span style='font-size: 1.2em; margin-right: 8px;'>{section_config.icon}</span>"
|
| 311 |
+
|
| 312 |
+
# Add validation warnings if any
|
| 313 |
+
warnings_html = ""
|
| 314 |
+
if validation_result.warnings:
|
| 315 |
+
warnings_list = "".join([f"<li>{warning.message}</li>" for warning in validation_result.warnings])
|
| 316 |
+
warnings_html = f"""
|
| 317 |
+
<div style='margin: 10px 0; padding: 8px; background-color: #fff3cd; border-left: 4px solid #ffc107; border-radius: 4px;'>
|
| 318 |
+
<strong style='color: #856404;'>⚠️ Validation Warnings:</strong>
|
| 319 |
+
<ul style='margin: 5px 0 0 20px; padding: 0; color: #856404;'>
|
| 320 |
+
{warnings_list}
|
| 321 |
+
</ul>
|
| 322 |
+
</div>
|
| 323 |
+
"""
|
| 324 |
+
|
| 325 |
+
# Combine all elements with enhanced styling
|
| 326 |
+
content = f"""
|
| 327 |
+
<div style='border: {section_config.border_width} solid {urgency_color}; border-radius: {section_config.border_radius}; padding: {section_config.padding}; margin: {section_config.margin}; background-color: {section_config.background_color}; font-family: {self.config.font_family};'>
|
| 328 |
+
<div style='display: flex; align-items: center; margin-bottom: 10px;'>
|
| 329 |
+
{icon_html}
|
| 330 |
+
<strong style='color: {urgency_color}; font-size: {self.config.header_font_size};'>Provider Summary</strong>
|
| 331 |
+
<span style='margin-left: 8px; font-size: {self.config.small_font_size}; color: #666;'>For Spiritual Care Team</span>
|
| 332 |
+
</div>
|
| 333 |
+
{patient_info}
|
| 334 |
+
{urgency_html}
|
| 335 |
+
{situation_html}
|
| 336 |
+
{indicators_html}
|
| 337 |
+
{warnings_html}
|
| 338 |
+
</div>
|
| 339 |
+
"""
|
| 340 |
+
|
| 341 |
+
return content
|
| 342 |
+
|
| 343 |
+
except Exception as e:
|
| 344 |
+
# Handle formatting errors with degraded display
|
| 345 |
+
error_context = f"Provider summary formatting failed: {str(e)}"
|
| 346 |
+
fallback_content = self._format_basic_provider_summary(summary_data)
|
| 347 |
+
return self.error_handler.create_degraded_display(error_context, fallback_content)
|
| 348 |
+
|
| 349 |
+
def create_visual_separators(self) -> Dict[str, str]:
|
| 350 |
+
"""
|
| 351 |
+
Create visual separators for different content types.
|
| 352 |
+
|
| 353 |
+
Returns:
|
| 354 |
+
Dictionary mapping section types to separator HTML
|
| 355 |
+
|
| 356 |
+
Requirements: 7.1, 7.2
|
| 357 |
+
"""
|
| 358 |
+
# Check if separators are enabled
|
| 359 |
+
if not self.config.use_visual_separators:
|
| 360 |
+
return {
|
| 361 |
+
"section_break": "<div style='margin: 10px 0;'></div>",
|
| 362 |
+
"content_divider": "<div style='margin: 5px 0;'></div>",
|
| 363 |
+
"major_break": "<div style='margin: 15px 0;'></div>"
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
separators = {
|
| 367 |
+
"section_break": f"""
|
| 368 |
+
<div style='margin: 20px 0; text-align: center;'>
|
| 369 |
+
<hr style='border: none; border-top: 2px solid {self.config.separators.separator_color}; width: {self.config.separators.separator_width}; margin: 0 auto;'>
|
| 370 |
+
<div style='margin: 10px 0; color: #666; font-size: {self.config.small_font_size};'>{self.config.separators.section_separator}</div>
|
| 371 |
+
</div>
|
| 372 |
+
""",
|
| 373 |
+
|
| 374 |
+
"content_divider": f"""
|
| 375 |
+
<div style='margin: 15px 0; border-top: {self.config.separators.content_divider};'></div>
|
| 376 |
+
""",
|
| 377 |
+
|
| 378 |
+
"major_break": f"""
|
| 379 |
+
<div style='margin: 30px 0; text-align: center;'>
|
| 380 |
+
<div style='display: inline-block; padding: 8px 16px; background-color: #f0f0f0; border-radius: 20px; color: #666; font-size: {self.config.small_font_size};'>
|
| 381 |
+
{self.config.separators.major_break_symbol}
|
| 382 |
+
</div>
|
| 383 |
+
</div>
|
| 384 |
+
"""
|
| 385 |
+
}
|
| 386 |
+
|
| 387 |
+
return separators
|
| 388 |
+
|
| 389 |
+
def apply_section_styling(self, content: str, section_type: SectionType) -> str:
|
| 390 |
+
"""
|
| 391 |
+
Apply consistent styling to a content section.
|
| 392 |
+
|
| 393 |
+
Args:
|
| 394 |
+
content: The content to style
|
| 395 |
+
section_type: The type of section for appropriate styling
|
| 396 |
+
|
| 397 |
+
Returns:
|
| 398 |
+
Styled HTML content
|
| 399 |
+
|
| 400 |
+
Requirements: 7.1, 7.2
|
| 401 |
+
"""
|
| 402 |
+
# Base styling for all sections
|
| 403 |
+
base_style = "margin: 15px 0; padding: 10px; border-radius: 6px;"
|
| 404 |
+
|
| 405 |
+
# Section-specific styling
|
| 406 |
+
section_styles = {
|
| 407 |
+
SectionType.AI_ANALYSIS: base_style + "background-color: #f8f9fa; border-left: 4px solid #6c757d;",
|
| 408 |
+
SectionType.PATIENT_MESSAGE: base_style + "background-color: #f0f7ff; border-left: 4px solid #4a90e2;",
|
| 409 |
+
SectionType.PROVIDER_SUMMARY: base_style + "background-color: #fff8f0; border-left: 4px solid #dc3545;"
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
style = section_styles.get(section_type, base_style)
|
| 413 |
+
|
| 414 |
+
return f"""
|
| 415 |
+
<div style='{style}'>
|
| 416 |
+
{content}
|
| 417 |
+
</div>
|
| 418 |
+
"""
|
| 419 |
+
|
| 420 |
+
def format_combined_results(
|
| 421 |
+
self,
|
| 422 |
+
ai_analysis: Optional[Dict[str, Any]] = None,
|
| 423 |
+
patient_message: Optional[str] = None,
|
| 424 |
+
provider_summary: Optional[ProviderSummary] = None
|
| 425 |
+
) -> str:
|
| 426 |
+
"""
|
| 427 |
+
Format combined results with all sections and proper separation.
|
| 428 |
+
|
| 429 |
+
Args:
|
| 430 |
+
ai_analysis: Optional AI analysis data
|
| 431 |
+
patient_message: Optional patient message
|
| 432 |
+
provider_summary: Optional provider summary data
|
| 433 |
+
|
| 434 |
+
Returns:
|
| 435 |
+
Complete formatted HTML with all sections
|
| 436 |
+
|
| 437 |
+
Requirements: 1.1, 1.2, 7.1, 7.2
|
| 438 |
+
"""
|
| 439 |
+
sections = []
|
| 440 |
+
separators = self.create_visual_separators()
|
| 441 |
+
|
| 442 |
+
# Add AI analysis section if provided
|
| 443 |
+
if ai_analysis:
|
| 444 |
+
ai_section = self.format_ai_analysis_section(
|
| 445 |
+
classification=ai_analysis.get('classification', 'UNKNOWN'),
|
| 446 |
+
indicators=ai_analysis.get('indicators', []),
|
| 447 |
+
reasoning=ai_analysis.get('reasoning', ''),
|
| 448 |
+
confidence=ai_analysis.get('confidence')
|
| 449 |
+
)
|
| 450 |
+
sections.append(ai_section)
|
| 451 |
+
|
| 452 |
+
# Add patient message section if provided
|
| 453 |
+
if patient_message:
|
| 454 |
+
patient_section = self.format_patient_message_section(patient_message)
|
| 455 |
+
sections.append(patient_section)
|
| 456 |
+
|
| 457 |
+
# Add provider summary section if provided
|
| 458 |
+
if provider_summary:
|
| 459 |
+
summary_section = self.format_provider_summary_section(provider_summary)
|
| 460 |
+
sections.append(summary_section)
|
| 461 |
+
|
| 462 |
+
# Join sections with separators
|
| 463 |
+
if len(sections) > 1:
|
| 464 |
+
content = separators["section_break"].join(sections)
|
| 465 |
+
elif sections:
|
| 466 |
+
content = sections[0]
|
| 467 |
+
else:
|
| 468 |
+
content = "<div style='padding: 20px; text-align: center; color: #666;'>No content to display</div>"
|
| 469 |
+
|
| 470 |
+
return content
|
| 471 |
+
|
| 472 |
+
def _format_basic_ai_analysis(self, classification: str, indicators: List[str], reasoning: str, confidence: Optional[float] = None) -> str:
|
| 473 |
+
"""Fallback basic formatting for AI analysis when enhancements are disabled."""
|
| 474 |
+
confidence_text = f" (Confidence: {int(confidence * 100)}%)" if confidence else ""
|
| 475 |
+
indicators_text = f"Indicators: {', '.join(indicators)}" if indicators else "No indicators"
|
| 476 |
+
|
| 477 |
+
return f"""
|
| 478 |
+
<div style='border: 1px solid #ccc; padding: 10px; margin: 5px 0;'>
|
| 479 |
+
<strong>AI Analysis - {classification} FLAG{confidence_text}</strong><br>
|
| 480 |
+
{indicators_text}<br>
|
| 481 |
+
Reasoning: {reasoning}
|
| 482 |
+
</div>
|
| 483 |
+
"""
|
| 484 |
+
|
| 485 |
+
def _format_basic_patient_message(self, message: str) -> str:
|
| 486 |
+
"""Fallback basic formatting for patient message when enhancements are disabled."""
|
| 487 |
+
return f"""
|
| 488 |
+
<div style='border: 1px solid #ccc; padding: 10px; margin: 5px 0;'>
|
| 489 |
+
<strong>Patient Message</strong><br>
|
| 490 |
+
{html.escape(message)}
|
| 491 |
+
</div>
|
| 492 |
+
"""
|
| 493 |
+
|
| 494 |
+
def _format_basic_provider_summary(self, summary_data: ProviderSummary) -> str:
|
| 495 |
+
"""Fallback basic formatting for provider summary when enhancements are disabled."""
|
| 496 |
+
return f"""
|
| 497 |
+
<div style='border: 1px solid #ccc; padding: 10px; margin: 5px 0;'>
|
| 498 |
+
<strong>Provider Summary</strong><br>
|
| 499 |
+
Patient: {summary_data.patient_name}<br>
|
| 500 |
+
Phone: {summary_data.patient_phone}<br>
|
| 501 |
+
Urgency: {summary_data.urgency_level}<br>
|
| 502 |
+
Situation: {summary_data.situation_description}
|
| 503 |
+
</div>
|
| 504 |
+
"""
|
| 505 |
+
|
| 506 |
+
def update_config(self, new_config: EnhancedDisplayConfig) -> None:
|
| 507 |
+
"""
|
| 508 |
+
Update the configuration for this display manager.
|
| 509 |
+
|
| 510 |
+
Args:
|
| 511 |
+
new_config: New configuration to use
|
| 512 |
+
"""
|
| 513 |
+
self.config = new_config
|
| 514 |
+
|
| 515 |
+
def reload_config(self) -> None:
|
| 516 |
+
"""Reload configuration from the config manager if available."""
|
| 517 |
+
if self.config_manager is not None:
|
| 518 |
+
self.config = self.config_manager.get_config()
|
| 519 |
+
else:
|
| 520 |
+
self.config = get_enhanced_display_config()
|
| 521 |
+
|
| 522 |
+
def is_enhanced_mode_enabled(self) -> bool:
|
| 523 |
+
"""Check if enhanced display mode is enabled."""
|
| 524 |
+
return self.config.enabled
|
| 525 |
+
|
| 526 |
+
def get_css_styles(self) -> str:
|
| 527 |
+
"""
|
| 528 |
+
Get CSS styles for enhanced display.
|
| 529 |
+
|
| 530 |
+
Returns:
|
| 531 |
+
CSS string for enhanced display styling
|
| 532 |
+
"""
|
| 533 |
+
return self.config.generate_base_css()
|
|
@@ -82,42 +82,35 @@ The system continuously monitors all conversations and classifies them into thre
|
|
| 82 |
|
| 83 |
---
|
| 84 |
|
| 85 |
-
## 💬
|
| 86 |
|
| 87 |
-
### What is the
|
| 88 |
-
When a RED flag case is detected and you consent to a referral, the system automatically generates
|
| 89 |
|
| 90 |
-
**📋
|
| 91 |
-
-
|
| 92 |
-
-
|
| 93 |
-
-
|
| 94 |
-
-
|
| 95 |
-
-
|
|
|
|
|
|
|
|
|
|
| 96 |
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
- Patient's name, contact, and age at the start
|
| 100 |
-
- Brief mention of relevant medical context
|
| 101 |
-
- Focus on emotional and spiritual struggles
|
| 102 |
-
- Urgency level and follow-up recommendations
|
| 103 |
-
- Written in warm, professional tone
|
| 104 |
-
|
| 105 |
-
### Customizing Spiritual Care Messages
|
| 106 |
-
You can customize how these messages are generated:
|
| 107 |
|
| 108 |
**Edit the Prompt (🔧 Edit Prompts tab):**
|
| 109 |
1. Select **💬 Spiritual Care Message** from dropdown
|
| 110 |
-
2. Modify the system prompt to adjust
|
| 111 |
3. Test changes in your current session
|
| 112 |
4. Promote to file if you want permanent changes
|
| 113 |
|
| 114 |
**Choose the AI Model (⚙️ Model Settings tab):**
|
| 115 |
-
1. Select model for **💬
|
| 116 |
-
2. Default: Claude Sonnet 4.5 (
|
| 117 |
-
3. Alternative: Gemini models for different
|
| 118 |
4. Changes apply to your session only
|
| 119 |
-
|
| 120 |
-
**Choose Message Content (in Care Team Message tab):**
|
| 121 |
You can selectively include or exclude information blocks:
|
| 122 |
- ☑️ **Include Conversation Context (Chat & Triage)** (Default: ON) - Raw chat history and triage exchanges.
|
| 123 |
- ☑️ **Include Distress Indicators** (Default: ON) - Detected signs of spiritual distress.
|
|
@@ -160,7 +153,7 @@ The **Edit Prompts** tab provides powerful capabilities for testing and optimizi
|
|
| 160 |
- 📊 **Triage Response Evaluator** - Evaluates patient responses to triage questions
|
| 161 |
- 🏥 **Medical Assistant** - Provides medical guidance and support
|
| 162 |
- 🩺 **Soft Medical Triage** - Handles medical triage and assessment
|
| 163 |
-
- 💬 **
|
| 164 |
|
| 165 |
---
|
| 166 |
|
|
@@ -183,7 +176,7 @@ Configure which AI models are used for different tasks:
|
|
| 183 |
- **Triage Response Evaluator** - Response analysis (default: Gemini 2.5 Flash)
|
| 184 |
- **Medical Assistant** - Medical guidance (default: Claude Sonnet 4.5)
|
| 185 |
- **Soft Medical Triage** - Medical assessment (default: Claude Sonnet 4.5)
|
| 186 |
-
- **
|
| 187 |
|
| 188 |
**Session Scope:** Model changes apply only to your current browser session.
|
| 189 |
|
|
|
|
| 82 |
|
| 83 |
---
|
| 84 |
|
| 85 |
+
## 💬 Medical Brain Compatible Summary Generation
|
| 86 |
|
| 87 |
+
### What is the Medical Brain Compatible Summary?
|
| 88 |
+
When a RED flag case is detected and you consent to a referral, the system automatically generates a Medical Brain compatible summary:
|
| 89 |
|
| 90 |
+
**📋 Medical Brain Compatible Summary (LLM-Generated)**
|
| 91 |
+
- Single coherent paragraph format
|
| 92 |
+
- Demographic information (name, age, gender)
|
| 93 |
+
- Medical history and conditions
|
| 94 |
+
- Spiritual concerns and indicators
|
| 95 |
+
- Classification and consent status
|
| 96 |
+
- Contact information
|
| 97 |
+
- Patient quote (actual words from conversation)
|
| 98 |
+
- Written in professional, clinical tone
|
| 99 |
|
| 100 |
+
### Customizing Medical Brain Summaries
|
| 101 |
+
You can customize how these summaries are generated:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
|
| 103 |
**Edit the Prompt (🔧 Edit Prompts tab):**
|
| 104 |
1. Select **💬 Spiritual Care Message** from dropdown
|
| 105 |
+
2. Modify the system prompt to adjust format, content, or focus
|
| 106 |
3. Test changes in your current session
|
| 107 |
4. Promote to file if you want permanent changes
|
| 108 |
|
| 109 |
**Choose the AI Model (⚙️ Model Settings tab):**
|
| 110 |
+
1. Select model for **💬 Medical Brain Summary Generator**
|
| 111 |
+
2. Default: Claude Sonnet 4.5 (structured, professional language)
|
| 112 |
+
3. Alternative: Gemini models for different formatting style
|
| 113 |
4. Changes apply to your session only
|
|
|
|
|
|
|
| 114 |
You can selectively include or exclude information blocks:
|
| 115 |
- ☑️ **Include Conversation Context (Chat & Triage)** (Default: ON) - Raw chat history and triage exchanges.
|
| 116 |
- ☑️ **Include Distress Indicators** (Default: ON) - Detected signs of spiritual distress.
|
|
|
|
| 153 |
- 📊 **Triage Response Evaluator** - Evaluates patient responses to triage questions
|
| 154 |
- 🏥 **Medical Assistant** - Provides medical guidance and support
|
| 155 |
- 🩺 **Soft Medical Triage** - Handles medical triage and assessment
|
| 156 |
+
- 💬 **Medical Brain Summary Generator** - Generates Medical Brain compatible summaries
|
| 157 |
|
| 158 |
---
|
| 159 |
|
|
|
|
| 176 |
- **Triage Response Evaluator** - Response analysis (default: Gemini 2.5 Flash)
|
| 177 |
- **Medical Assistant** - Medical guidance (default: Claude Sonnet 4.5)
|
| 178 |
- **Soft Medical Triage** - Medical assessment (default: Claude Sonnet 4.5)
|
| 179 |
+
- **Medical Brain Summary Generator** - Medical Brain compatible summary generation (default: Claude Sonnet 4.5)
|
| 180 |
|
| 181 |
**Session Scope:** Model changes apply only to your current browser session.
|
| 182 |
|
|
@@ -0,0 +1,290 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# provider_summary_formatter.py
|
| 2 |
+
"""
|
| 3 |
+
Provider Summary Formatter for Enhanced UI Display.
|
| 4 |
+
|
| 5 |
+
This module provides formatting for provider summaries as coherent paragraphs
|
| 6 |
+
according to the requirements for improved readability and consistency.
|
| 7 |
+
|
| 8 |
+
Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
from typing import List, Optional, Dict, Any
|
| 12 |
+
from dataclasses import dataclass
|
| 13 |
+
|
| 14 |
+
from src.core.provider_summary_generator import ProviderSummary
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
@dataclass
|
| 18 |
+
class PatientData:
|
| 19 |
+
"""Patient data for summary formatting."""
|
| 20 |
+
name: str
|
| 21 |
+
age: int
|
| 22 |
+
gender: str
|
| 23 |
+
phone: str
|
| 24 |
+
medical_history: List[str]
|
| 25 |
+
expressed_concerns: List[str]
|
| 26 |
+
patient_input: str
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
@dataclass
|
| 30 |
+
class ClassificationData:
|
| 31 |
+
"""Classification data for summary formatting."""
|
| 32 |
+
classification: str # RED/YELLOW/GREEN
|
| 33 |
+
spiritual_concern_type: str
|
| 34 |
+
consent_given: bool
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
class ProviderSummaryFormatter:
|
| 38 |
+
"""
|
| 39 |
+
Provider Summary Formatter for coherent paragraph generation.
|
| 40 |
+
|
| 41 |
+
Formats provider summaries as single coherent paragraphs following
|
| 42 |
+
the Medical Brain style as specified in requirements 2.1-2.8.
|
| 43 |
+
"""
|
| 44 |
+
|
| 45 |
+
def __init__(self):
|
| 46 |
+
"""Initialize the provider summary formatter."""
|
| 47 |
+
pass
|
| 48 |
+
|
| 49 |
+
def format_coherent_paragraph(
|
| 50 |
+
self,
|
| 51 |
+
patient_data: PatientData,
|
| 52 |
+
classification_data: ClassificationData
|
| 53 |
+
) -> str:
|
| 54 |
+
"""
|
| 55 |
+
Format provider summary as a single coherent paragraph.
|
| 56 |
+
|
| 57 |
+
Args:
|
| 58 |
+
patient_data: Patient information and context
|
| 59 |
+
classification_data: Classification and consent information
|
| 60 |
+
|
| 61 |
+
Returns:
|
| 62 |
+
Single coherent paragraph formatted for provider summary
|
| 63 |
+
|
| 64 |
+
Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8
|
| 65 |
+
"""
|
| 66 |
+
# Build demographic section (Requirement 2.2)
|
| 67 |
+
demographic_section = self.build_demographic_section(patient_data)
|
| 68 |
+
|
| 69 |
+
# Build medical history section (Requirement 2.3)
|
| 70 |
+
medical_section = self.build_medical_history_section(patient_data.medical_history)
|
| 71 |
+
|
| 72 |
+
# Build spiritual concerns section (Requirement 2.4, 2.5)
|
| 73 |
+
concerns_section = self.build_spiritual_concerns_section(
|
| 74 |
+
patient_data.expressed_concerns,
|
| 75 |
+
classification_data.spiritual_concern_type,
|
| 76 |
+
classification_data.classification
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
# Build consent and contact section (Requirement 2.6, 2.7)
|
| 80 |
+
contact_section = self.build_contact_section(
|
| 81 |
+
patient_data.phone,
|
| 82 |
+
classification_data.consent_given
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
# Combine all sections into coherent paragraph (Requirement 2.1)
|
| 86 |
+
paragraph_parts = [
|
| 87 |
+
demographic_section,
|
| 88 |
+
medical_section,
|
| 89 |
+
concerns_section,
|
| 90 |
+
contact_section
|
| 91 |
+
]
|
| 92 |
+
|
| 93 |
+
# Filter out empty parts and join with appropriate connectors
|
| 94 |
+
non_empty_parts = [part for part in paragraph_parts if part.strip()]
|
| 95 |
+
|
| 96 |
+
if not non_empty_parts:
|
| 97 |
+
return "No summary information available."
|
| 98 |
+
|
| 99 |
+
# Join parts with appropriate connectors for flow
|
| 100 |
+
coherent_paragraph = ". ".join(non_empty_parts)
|
| 101 |
+
|
| 102 |
+
# Ensure proper sentence ending
|
| 103 |
+
if not coherent_paragraph.endswith('.'):
|
| 104 |
+
coherent_paragraph += '.'
|
| 105 |
+
|
| 106 |
+
return coherent_paragraph
|
| 107 |
+
|
| 108 |
+
def build_demographic_section(self, patient: PatientData) -> str:
|
| 109 |
+
"""
|
| 110 |
+
Build demographic information section.
|
| 111 |
+
|
| 112 |
+
Args:
|
| 113 |
+
patient: Patient data containing demographic information
|
| 114 |
+
|
| 115 |
+
Returns:
|
| 116 |
+
Formatted demographic section string
|
| 117 |
+
|
| 118 |
+
Requirements: 2.2
|
| 119 |
+
"""
|
| 120 |
+
# Format: "[patient name] is a X-year-old [gender]"
|
| 121 |
+
gender_text = patient.gender.lower() if patient.gender else "individual"
|
| 122 |
+
|
| 123 |
+
return f"{patient.name} is a {patient.age}-year-old {gender_text}"
|
| 124 |
+
|
| 125 |
+
def build_medical_history_section(self, medical_history: List[str]) -> str:
|
| 126 |
+
"""
|
| 127 |
+
Build medical history section.
|
| 128 |
+
|
| 129 |
+
Args:
|
| 130 |
+
medical_history: List of medical conditions/history items
|
| 131 |
+
|
| 132 |
+
Returns:
|
| 133 |
+
Formatted medical history section string
|
| 134 |
+
|
| 135 |
+
Requirements: 2.3
|
| 136 |
+
"""
|
| 137 |
+
if not medical_history:
|
| 138 |
+
return "with no significant medical history documented"
|
| 139 |
+
|
| 140 |
+
# Format: "with clinical history of X, Y, and Z"
|
| 141 |
+
if len(medical_history) == 1:
|
| 142 |
+
history_text = medical_history[0]
|
| 143 |
+
elif len(medical_history) == 2:
|
| 144 |
+
history_text = f"{medical_history[0]} and {medical_history[1]}"
|
| 145 |
+
else:
|
| 146 |
+
# Multiple items: "X, Y, and Z"
|
| 147 |
+
history_text = ", ".join(medical_history[:-1]) + f", and {medical_history[-1]}"
|
| 148 |
+
|
| 149 |
+
return f"with clinical history of {history_text}"
|
| 150 |
+
|
| 151 |
+
def build_spiritual_concerns_section(
|
| 152 |
+
self,
|
| 153 |
+
concerns: List[str],
|
| 154 |
+
concern_type: str,
|
| 155 |
+
classification: str
|
| 156 |
+
) -> str:
|
| 157 |
+
"""
|
| 158 |
+
Build spiritual concerns section.
|
| 159 |
+
|
| 160 |
+
Args:
|
| 161 |
+
concerns: List of expressed concerns
|
| 162 |
+
concern_type: Type of spiritual concern identified
|
| 163 |
+
classification: Classification level (RED/YELLOW/GREEN)
|
| 164 |
+
|
| 165 |
+
Returns:
|
| 166 |
+
Formatted spiritual concerns section string
|
| 167 |
+
|
| 168 |
+
Requirements: 2.4, 2.5
|
| 169 |
+
"""
|
| 170 |
+
if not concerns:
|
| 171 |
+
concerns_text = "general distress"
|
| 172 |
+
elif len(concerns) == 1:
|
| 173 |
+
concerns_text = concerns[0]
|
| 174 |
+
elif len(concerns) == 2:
|
| 175 |
+
concerns_text = f"{concerns[0]} and {concerns[1]}"
|
| 176 |
+
else:
|
| 177 |
+
# Multiple concerns: "X, Y, and Z"
|
| 178 |
+
concerns_text = ", ".join(concerns[:-1]) + f", and {concerns[-1]}"
|
| 179 |
+
|
| 180 |
+
# Format: "The patient expressed X, Y, and Z, which may indicate [specific spiritual/emotional concern]"
|
| 181 |
+
concern_description = concern_type if concern_type else "spiritual or emotional distress"
|
| 182 |
+
|
| 183 |
+
concerns_section = f"The patient expressed {concerns_text}, which may indicate {concern_description}"
|
| 184 |
+
|
| 185 |
+
# Add classification information (Requirement 2.5)
|
| 186 |
+
flag_text = f"{classification} FLAG" if classification in ["RED", "YELLOW"] else "GREEN status"
|
| 187 |
+
classification_section = f", resulting in generation of a {flag_text}"
|
| 188 |
+
|
| 189 |
+
return concerns_section + classification_section
|
| 190 |
+
|
| 191 |
+
def build_contact_section(self, phone: str, consent_given: bool) -> str:
|
| 192 |
+
"""
|
| 193 |
+
Build contact and consent section.
|
| 194 |
+
|
| 195 |
+
Args:
|
| 196 |
+
phone: Patient's phone number
|
| 197 |
+
consent_given: Whether patient gave consent for spiritual care contact
|
| 198 |
+
|
| 199 |
+
Returns:
|
| 200 |
+
Formatted contact section string
|
| 201 |
+
|
| 202 |
+
Requirements: 2.6, 2.7
|
| 203 |
+
"""
|
| 204 |
+
sections = []
|
| 205 |
+
|
| 206 |
+
# Add consent information (Requirement 2.6)
|
| 207 |
+
if consent_given:
|
| 208 |
+
sections.append("The patient has given consent to be contacted by the spiritual care team")
|
| 209 |
+
else:
|
| 210 |
+
sections.append("The patient has not yet provided consent for spiritual care contact")
|
| 211 |
+
|
| 212 |
+
# Add contact information (Requirement 2.7)
|
| 213 |
+
if phone and phone.strip():
|
| 214 |
+
sections.append(f"The preferred contact number is {phone}")
|
| 215 |
+
else:
|
| 216 |
+
sections.append("No contact number is currently available")
|
| 217 |
+
|
| 218 |
+
return ". ".join(sections)
|
| 219 |
+
|
| 220 |
+
def add_patient_quote_section(self, quote: str) -> str:
|
| 221 |
+
"""
|
| 222 |
+
Add patient quote section as separate line.
|
| 223 |
+
|
| 224 |
+
Args:
|
| 225 |
+
quote: Direct quote from patient input
|
| 226 |
+
|
| 227 |
+
Returns:
|
| 228 |
+
Formatted patient quote section
|
| 229 |
+
|
| 230 |
+
Requirements: 2.8
|
| 231 |
+
"""
|
| 232 |
+
if not quote or not quote.strip():
|
| 233 |
+
return ""
|
| 234 |
+
|
| 235 |
+
# Format: "Patient reported: [patient input here]"
|
| 236 |
+
return f"Patient reported: \"{quote.strip()}\""
|
| 237 |
+
|
| 238 |
+
def format_enhanced_summary(
|
| 239 |
+
self,
|
| 240 |
+
patient_data: PatientData,
|
| 241 |
+
classification_data: ClassificationData
|
| 242 |
+
) -> str:
|
| 243 |
+
"""
|
| 244 |
+
Format complete enhanced summary with paragraph and quote.
|
| 245 |
+
|
| 246 |
+
Args:
|
| 247 |
+
patient_data: Patient information and context
|
| 248 |
+
classification_data: Classification and consent information
|
| 249 |
+
|
| 250 |
+
Returns:
|
| 251 |
+
Complete formatted summary with paragraph and quote sections
|
| 252 |
+
|
| 253 |
+
Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8
|
| 254 |
+
"""
|
| 255 |
+
# Generate main coherent paragraph
|
| 256 |
+
main_paragraph = self.format_coherent_paragraph(patient_data, classification_data)
|
| 257 |
+
|
| 258 |
+
# Generate patient quote section (Requirement 2.8)
|
| 259 |
+
quote_section = self.add_patient_quote_section(patient_data.patient_input)
|
| 260 |
+
|
| 261 |
+
# Combine paragraph and quote
|
| 262 |
+
if quote_section:
|
| 263 |
+
return f"{main_paragraph}\n\n{quote_section}"
|
| 264 |
+
else:
|
| 265 |
+
return main_paragraph
|
| 266 |
+
|
| 267 |
+
def format_from_provider_summary(self, summary: ProviderSummary) -> str:
|
| 268 |
+
"""
|
| 269 |
+
Format coherent paragraph from existing ProviderSummary object.
|
| 270 |
+
|
| 271 |
+
This method bridges the existing ProviderSummary structure with the new
|
| 272 |
+
coherent paragraph formatting requirements by using the enhanced method
|
| 273 |
+
in ProviderSummaryGenerator.
|
| 274 |
+
|
| 275 |
+
Args:
|
| 276 |
+
summary: Existing ProviderSummary object
|
| 277 |
+
|
| 278 |
+
Returns:
|
| 279 |
+
Formatted coherent paragraph
|
| 280 |
+
|
| 281 |
+
Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8
|
| 282 |
+
"""
|
| 283 |
+
# Import here to avoid circular imports
|
| 284 |
+
from src.core.provider_summary_generator import ProviderSummaryGenerator
|
| 285 |
+
|
| 286 |
+
# Create a temporary generator instance to use the enhanced formatting
|
| 287 |
+
temp_generator = ProviderSummaryGenerator()
|
| 288 |
+
|
| 289 |
+
# Use the enhanced format_coherent_paragraph method
|
| 290 |
+
return temp_generator.format_coherent_paragraph(summary)
|
|
@@ -183,53 +183,32 @@ def create_simplified_interface():
|
|
| 183 |
value="**Provider Summary Generated**\n\nA detailed summary has been generated for the spiritual care team. Use the Download button below to access the full summary."
|
| 184 |
)
|
| 185 |
|
| 186 |
-
# Tabs for different summary views
|
| 187 |
with gr.Tabs():
|
| 188 |
-
#
|
| 189 |
-
with gr.TabItem("
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
# LLM-Generated Message Tab
|
| 193 |
-
with gr.TabItem("💬 Care Team Message"):
|
| 194 |
-
gr.Markdown("*AI-generated compassionate message for spiritual care team*")
|
| 195 |
-
spiritual_care_message = gr.Textbox(
|
| 196 |
value="",
|
| 197 |
-
lines=
|
| 198 |
-
label="
|
| 199 |
interactive=False
|
| 200 |
)
|
| 201 |
with gr.Row():
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
label="Include Conversation Context (Chat & Triage)",
|
| 205 |
-
interactive=True
|
| 206 |
-
)
|
| 207 |
-
include_situation_analysis = gr.Checkbox(
|
| 208 |
-
value=False,
|
| 209 |
-
label="Include Situation Analysis",
|
| 210 |
-
interactive=True
|
| 211 |
-
)
|
| 212 |
-
include_distress_indicators = gr.Checkbox(
|
| 213 |
-
value=True,
|
| 214 |
-
label="Include Distress Indicators",
|
| 215 |
-
interactive=True
|
| 216 |
-
)
|
| 217 |
-
include_patient_profile = gr.Checkbox(
|
| 218 |
-
value=False,
|
| 219 |
-
label="Include Patient Profile (Name, Contact)",
|
| 220 |
-
interactive=True
|
| 221 |
-
)
|
| 222 |
-
with gr.Row():
|
| 223 |
-
generate_message_btn = gr.Button(
|
| 224 |
-
"🔄 Regenerate Message",
|
| 225 |
size="sm",
|
| 226 |
variant="secondary"
|
| 227 |
)
|
| 228 |
-
|
| 229 |
-
"📥 Download
|
| 230 |
size="sm",
|
| 231 |
variant="secondary"
|
| 232 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 233 |
|
| 234 |
with gr.Row():
|
| 235 |
download_summary_btn = gr.DownloadButton(
|
|
@@ -384,7 +363,7 @@ def create_simplified_interface():
|
|
| 384 |
interactive=True
|
| 385 |
)
|
| 386 |
|
| 387 |
-
gr.Markdown("### 💬
|
| 388 |
spiritual_care_message_model = gr.Dropdown(
|
| 389 |
choices=[
|
| 390 |
"claude-sonnet-4-5-20250929",
|
|
@@ -395,7 +374,7 @@ def create_simplified_interface():
|
|
| 395 |
"gemini-3-flash-preview"
|
| 396 |
],
|
| 397 |
value="claude-sonnet-4-5-20250929",
|
| 398 |
-
label="Spiritual Care Message
|
| 399 |
interactive=True
|
| 400 |
)
|
| 401 |
|
|
@@ -465,6 +444,7 @@ def create_simplified_interface():
|
|
| 465 |
<li>📊 Triage Evaluation</li>
|
| 466 |
<li>🏥 Medical Assistant</li>
|
| 467 |
<li>🩺 Soft Triage</li>
|
|
|
|
| 468 |
</ul>
|
| 469 |
|
| 470 |
<p><strong>Tips:</strong></p>
|
|
@@ -589,46 +569,46 @@ def create_simplified_interface():
|
|
| 589 |
send_btn.click(
|
| 590 |
chat_handlers.handle_message,
|
| 591 |
inputs=[msg, chatbot, session_data],
|
| 592 |
-
outputs=[chatbot, status_box, session_data, msg, conversation_stats, provider_summary_content, provider_summary_status, provider_summary_display,
|
| 593 |
)
|
| 594 |
|
| 595 |
msg.submit(
|
| 596 |
chat_handlers.handle_message,
|
| 597 |
inputs=[msg, chatbot, session_data],
|
| 598 |
-
outputs=[chatbot, status_box, session_data, msg, conversation_stats, provider_summary_content, provider_summary_status, provider_summary_display,
|
| 599 |
)
|
| 600 |
|
| 601 |
# Clear chat
|
| 602 |
clear_btn.click(
|
| 603 |
chat_handlers.handle_clear,
|
| 604 |
inputs=[session_data],
|
| 605 |
-
outputs=[chatbot, status_box, session_data, provider_summary_content, provider_summary_status, provider_summary_display,
|
| 606 |
)
|
| 607 |
|
| 608 |
# Refresh status
|
| 609 |
refresh_btn.click(
|
| 610 |
stats_handlers.get_status,
|
| 611 |
inputs=[session_data],
|
| 612 |
-
outputs=[status_box, conversation_stats, provider_summary_content, provider_summary_status, provider_summary_display,
|
| 613 |
)
|
| 614 |
|
| 615 |
# Example buttons
|
| 616 |
example_medical.click(
|
| 617 |
lambda h, s: chat_handlers.send_example_with_stats("I am fine", h, s),
|
| 618 |
inputs=[chatbot, session_data],
|
| 619 |
-
outputs=[chatbot, status_box, session_data, msg, conversation_stats, provider_summary_content, provider_summary_status, provider_summary_display,
|
| 620 |
)
|
| 621 |
|
| 622 |
example_wellness.click(
|
| 623 |
lambda h, s: chat_handlers.send_example_with_stats("I'm feeling stressed and overwhelmed lately", h, s),
|
| 624 |
inputs=[chatbot, session_data],
|
| 625 |
-
outputs=[chatbot, status_box, session_data, msg, conversation_stats, provider_summary_content, provider_summary_status, provider_summary_display,
|
| 626 |
)
|
| 627 |
|
| 628 |
example_help.click(
|
| 629 |
lambda h, s: chat_handlers.send_example_with_stats("I am currently experiencing an emotional crisis", h, s),
|
| 630 |
inputs=[chatbot, session_data],
|
| 631 |
-
outputs=[chatbot, status_box, session_data, msg, conversation_stats, provider_summary_content, provider_summary_status, provider_summary_display,
|
| 632 |
)
|
| 633 |
|
| 634 |
# Conversation logging buttons
|
|
@@ -654,20 +634,20 @@ def create_simplified_interface():
|
|
| 654 |
clear_summary_btn.click(
|
| 655 |
stats_handlers.clear_provider_summary,
|
| 656 |
inputs=[],
|
| 657 |
-
outputs=[provider_summary_content, provider_summary_status, provider_summary_display,
|
| 658 |
)
|
| 659 |
|
| 660 |
-
#
|
| 661 |
-
|
| 662 |
-
stats_handlers.
|
| 663 |
-
inputs=[session_data
|
| 664 |
-
outputs=[
|
| 665 |
)
|
| 666 |
|
| 667 |
-
|
| 668 |
-
stats_handlers.
|
| 669 |
inputs=[session_data],
|
| 670 |
-
outputs=[
|
| 671 |
)
|
| 672 |
|
| 673 |
# Conversation Verification events
|
|
|
|
| 183 |
value="**Provider Summary Generated**\n\nA detailed summary has been generated for the spiritual care team. Use the Download button below to access the full summary."
|
| 184 |
)
|
| 185 |
|
| 186 |
+
# Tabs for different summary views (Medical Brain Summary first by default)
|
| 187 |
with gr.Tabs():
|
| 188 |
+
# Coherent Paragraph Tab (FIRST - Requirements 2.1-2.8) - Medical Brain compatibility
|
| 189 |
+
with gr.TabItem("📝 Medical Brain Summary"):
|
| 190 |
+
gr.Markdown("*Single paragraph format for Medical Brain compatibility - **Default View***")
|
| 191 |
+
coherent_summary_display = gr.Textbox(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
value="",
|
| 193 |
+
lines=8,
|
| 194 |
+
label="Medical Brain Compatible Summary",
|
| 195 |
interactive=False
|
| 196 |
)
|
| 197 |
with gr.Row():
|
| 198 |
+
regenerate_coherent_btn = gr.Button(
|
| 199 |
+
"🔄 Regenerate Medical Brain Summary",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
size="sm",
|
| 201 |
variant="secondary"
|
| 202 |
)
|
| 203 |
+
download_coherent_btn = gr.DownloadButton(
|
| 204 |
+
"📥 Download Medical Brain Summary",
|
| 205 |
size="sm",
|
| 206 |
variant="secondary"
|
| 207 |
)
|
| 208 |
+
|
| 209 |
+
# Structured Summary Tab (moved to second position)
|
| 210 |
+
with gr.TabItem("📊 Structured Summary"):
|
| 211 |
+
provider_summary_display = gr.HTML(value="")
|
| 212 |
|
| 213 |
with gr.Row():
|
| 214 |
download_summary_btn = gr.DownloadButton(
|
|
|
|
| 363 |
interactive=True
|
| 364 |
)
|
| 365 |
|
| 366 |
+
gr.Markdown("### 💬 Medical Brain Summary Generator")
|
| 367 |
spiritual_care_message_model = gr.Dropdown(
|
| 368 |
choices=[
|
| 369 |
"claude-sonnet-4-5-20250929",
|
|
|
|
| 374 |
"gemini-3-flash-preview"
|
| 375 |
],
|
| 376 |
value="claude-sonnet-4-5-20250929",
|
| 377 |
+
label="Medical Brain Summary Generator (uses Spiritual Care Message prompt)",
|
| 378 |
interactive=True
|
| 379 |
)
|
| 380 |
|
|
|
|
| 444 |
<li>📊 Triage Evaluation</li>
|
| 445 |
<li>🏥 Medical Assistant</li>
|
| 446 |
<li>🩺 Soft Triage</li>
|
| 447 |
+
<li>💬 Spiritual Care Message (used for Medical Brain Summary)</li>
|
| 448 |
</ul>
|
| 449 |
|
| 450 |
<p><strong>Tips:</strong></p>
|
|
|
|
| 569 |
send_btn.click(
|
| 570 |
chat_handlers.handle_message,
|
| 571 |
inputs=[msg, chatbot, session_data],
|
| 572 |
+
outputs=[chatbot, status_box, session_data, msg, conversation_stats, provider_summary_content, provider_summary_status, provider_summary_display, coherent_summary_display]
|
| 573 |
)
|
| 574 |
|
| 575 |
msg.submit(
|
| 576 |
chat_handlers.handle_message,
|
| 577 |
inputs=[msg, chatbot, session_data],
|
| 578 |
+
outputs=[chatbot, status_box, session_data, msg, conversation_stats, provider_summary_content, provider_summary_status, provider_summary_display, coherent_summary_display]
|
| 579 |
)
|
| 580 |
|
| 581 |
# Clear chat
|
| 582 |
clear_btn.click(
|
| 583 |
chat_handlers.handle_clear,
|
| 584 |
inputs=[session_data],
|
| 585 |
+
outputs=[chatbot, status_box, session_data, provider_summary_content, provider_summary_status, provider_summary_display, coherent_summary_display]
|
| 586 |
)
|
| 587 |
|
| 588 |
# Refresh status
|
| 589 |
refresh_btn.click(
|
| 590 |
stats_handlers.get_status,
|
| 591 |
inputs=[session_data],
|
| 592 |
+
outputs=[status_box, conversation_stats, provider_summary_content, provider_summary_status, provider_summary_display, coherent_summary_display]
|
| 593 |
)
|
| 594 |
|
| 595 |
# Example buttons
|
| 596 |
example_medical.click(
|
| 597 |
lambda h, s: chat_handlers.send_example_with_stats("I am fine", h, s),
|
| 598 |
inputs=[chatbot, session_data],
|
| 599 |
+
outputs=[chatbot, status_box, session_data, msg, conversation_stats, provider_summary_content, provider_summary_status, provider_summary_display, coherent_summary_display]
|
| 600 |
)
|
| 601 |
|
| 602 |
example_wellness.click(
|
| 603 |
lambda h, s: chat_handlers.send_example_with_stats("I'm feeling stressed and overwhelmed lately", h, s),
|
| 604 |
inputs=[chatbot, session_data],
|
| 605 |
+
outputs=[chatbot, status_box, session_data, msg, conversation_stats, provider_summary_content, provider_summary_status, provider_summary_display, coherent_summary_display]
|
| 606 |
)
|
| 607 |
|
| 608 |
example_help.click(
|
| 609 |
lambda h, s: chat_handlers.send_example_with_stats("I am currently experiencing an emotional crisis", h, s),
|
| 610 |
inputs=[chatbot, session_data],
|
| 611 |
+
outputs=[chatbot, status_box, session_data, msg, conversation_stats, provider_summary_content, provider_summary_status, provider_summary_display, coherent_summary_display]
|
| 612 |
)
|
| 613 |
|
| 614 |
# Conversation logging buttons
|
|
|
|
| 634 |
clear_summary_btn.click(
|
| 635 |
stats_handlers.clear_provider_summary,
|
| 636 |
inputs=[],
|
| 637 |
+
outputs=[provider_summary_content, provider_summary_status, provider_summary_display, coherent_summary_display]
|
| 638 |
)
|
| 639 |
|
| 640 |
+
# Coherent summary handlers (Medical Brain Summary - now first tab by default)
|
| 641 |
+
regenerate_coherent_btn.click(
|
| 642 |
+
stats_handlers.regenerate_coherent_summary,
|
| 643 |
+
inputs=[session_data],
|
| 644 |
+
outputs=[coherent_summary_display]
|
| 645 |
)
|
| 646 |
|
| 647 |
+
download_coherent_btn.click(
|
| 648 |
+
stats_handlers.download_coherent_summary,
|
| 649 |
inputs=[session_data],
|
| 650 |
+
outputs=[download_coherent_btn]
|
| 651 |
)
|
| 652 |
|
| 653 |
# Conversation Verification events
|
|
@@ -78,22 +78,17 @@ def get_status(session: SimplifiedSessionData):
|
|
| 78 |
show_provider_panel = last_summary is not None
|
| 79 |
|
| 80 |
provider_summary_text = ""
|
| 81 |
-
|
| 82 |
|
| 83 |
if last_summary:
|
| 84 |
provider_summary_text = session.app_instance.provider_summary_generator.format_for_display(last_summary)
|
| 85 |
|
| 86 |
-
# Generate
|
| 87 |
try:
|
| 88 |
-
|
| 89 |
-
language="English",
|
| 90 |
-
session_id=session.session_id
|
| 91 |
-
)
|
| 92 |
-
if not spiritual_care_msg:
|
| 93 |
-
spiritual_care_msg = ""
|
| 94 |
except Exception as e:
|
| 95 |
-
print(f"Error generating
|
| 96 |
-
|
| 97 |
|
| 98 |
if provider_summary_text:
|
| 99 |
import html
|
|
@@ -118,7 +113,7 @@ Use the **Download Summary** button below to access the complete provider summar
|
|
| 118 |
gr.update(visible=show_provider_panel),
|
| 119 |
status_msg,
|
| 120 |
html_content,
|
| 121 |
-
|
| 122 |
)
|
| 123 |
|
| 124 |
def download_provider_summary(session: SimplifiedSessionData):
|
|
@@ -158,17 +153,11 @@ def clear_provider_summary():
|
|
| 158 |
gr.update(visible=False),
|
| 159 |
"No provider summary available",
|
| 160 |
"",
|
| 161 |
-
""
|
| 162 |
)
|
| 163 |
|
| 164 |
-
def
|
| 165 |
-
|
| 166 |
-
include_conversation: bool = True,
|
| 167 |
-
include_situation: bool = False,
|
| 168 |
-
include_indicators: bool = False,
|
| 169 |
-
include_profile: bool = False
|
| 170 |
-
):
|
| 171 |
-
"""Regenerate LLM-based spiritual care message."""
|
| 172 |
if session is None:
|
| 173 |
return ""
|
| 174 |
|
|
@@ -177,48 +166,35 @@ def regenerate_spiritual_care_message(
|
|
| 177 |
return ""
|
| 178 |
|
| 179 |
try:
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
session_id=session.session_id,
|
| 183 |
-
include_conversation_context=include_conversation,
|
| 184 |
-
include_situation_analysis=include_situation,
|
| 185 |
-
include_distress_indicators=include_indicators,
|
| 186 |
-
include_patient_profile=include_profile
|
| 187 |
-
)
|
| 188 |
-
return msg or "Error generating message"
|
| 189 |
except Exception as e:
|
| 190 |
return f"Error: {str(e)}"
|
| 191 |
|
| 192 |
-
def
|
| 193 |
-
"""Download
|
| 194 |
if session is None:
|
| 195 |
return None
|
| 196 |
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
# we'll regenerate the last one or we'd need the text passed in.
|
| 200 |
-
# Looking at original code, it might regenerate or access session state.
|
| 201 |
-
# ORIGINAL CODE logic check needed.
|
| 202 |
-
|
| 203 |
-
# Let's assume for now we regenerate it or fetch from session if stored.
|
| 204 |
-
# Actually, let's regenerate for consistency with current context.
|
| 205 |
-
|
| 206 |
-
msg = regenerate_spiritual_care_message(session)
|
| 207 |
-
if not msg:
|
| 208 |
return None
|
| 209 |
|
| 210 |
try:
|
| 211 |
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 212 |
patient_name = session.app_instance.patient_info.get('name', 'Patient').replace(" ", "_")
|
| 213 |
-
filename = f"
|
|
|
|
|
|
|
|
|
|
| 214 |
|
| 215 |
path = os.path.join(os.getcwd(), "exports", filename)
|
| 216 |
os.makedirs(os.path.dirname(path), exist_ok=True)
|
| 217 |
|
| 218 |
with open(path, "w", encoding="utf-8") as f:
|
| 219 |
-
f.write(
|
| 220 |
|
| 221 |
return path
|
| 222 |
except Exception as e:
|
| 223 |
-
print(f"Error downloading
|
| 224 |
return None
|
|
|
|
| 78 |
show_provider_panel = last_summary is not None
|
| 79 |
|
| 80 |
provider_summary_text = ""
|
| 81 |
+
coherent_summary_text = ""
|
| 82 |
|
| 83 |
if last_summary:
|
| 84 |
provider_summary_text = session.app_instance.provider_summary_generator.format_for_display(last_summary)
|
| 85 |
|
| 86 |
+
# Generate medical brain summary (NEW - Requirements 2.1-2.8)
|
| 87 |
try:
|
| 88 |
+
coherent_summary_text = session.app_instance.provider_summary_generator.format_coherent_paragraph(last_summary)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
except Exception as e:
|
| 90 |
+
print(f"Error generating coherent summary in get_status: {e}")
|
| 91 |
+
coherent_summary_text = ""
|
| 92 |
|
| 93 |
if provider_summary_text:
|
| 94 |
import html
|
|
|
|
| 113 |
gr.update(visible=show_provider_panel),
|
| 114 |
status_msg,
|
| 115 |
html_content,
|
| 116 |
+
coherent_summary_text # NEW coherent summary
|
| 117 |
)
|
| 118 |
|
| 119 |
def download_provider_summary(session: SimplifiedSessionData):
|
|
|
|
| 153 |
gr.update(visible=False),
|
| 154 |
"No provider summary available",
|
| 155 |
"",
|
| 156 |
+
"" # NEW coherent summary
|
| 157 |
)
|
| 158 |
|
| 159 |
+
def regenerate_coherent_summary(session: SimplifiedSessionData):
|
| 160 |
+
"""Regenerate medical brain summary."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
if session is None:
|
| 162 |
return ""
|
| 163 |
|
|
|
|
| 166 |
return ""
|
| 167 |
|
| 168 |
try:
|
| 169 |
+
coherent_text = session.app_instance.provider_summary_generator.format_coherent_paragraph(last_summary)
|
| 170 |
+
return coherent_text or "Error generating coherent summary"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
except Exception as e:
|
| 172 |
return f"Error: {str(e)}"
|
| 173 |
|
| 174 |
+
def download_coherent_summary(session: SimplifiedSessionData):
|
| 175 |
+
"""Download coherent summary as text file."""
|
| 176 |
if session is None:
|
| 177 |
return None
|
| 178 |
|
| 179 |
+
last_summary = session.app_instance.get_last_provider_summary()
|
| 180 |
+
if not last_summary:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
return None
|
| 182 |
|
| 183 |
try:
|
| 184 |
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 185 |
patient_name = session.app_instance.patient_info.get('name', 'Patient').replace(" ", "_")
|
| 186 |
+
filename = f"coherent_summary_{patient_name}_{timestamp}.txt"
|
| 187 |
+
|
| 188 |
+
# Generate coherent summary
|
| 189 |
+
coherent_text = session.app_instance.provider_summary_generator.format_coherent_paragraph(last_summary)
|
| 190 |
|
| 191 |
path = os.path.join(os.getcwd(), "exports", filename)
|
| 192 |
os.makedirs(os.path.dirname(path), exist_ok=True)
|
| 193 |
|
| 194 |
with open(path, "w", encoding="utf-8") as f:
|
| 195 |
+
f.write(coherent_text)
|
| 196 |
|
| 197 |
return path
|
| 198 |
except Exception as e:
|
| 199 |
+
print(f"Error downloading coherent summary: {e}")
|
| 200 |
return None
|
|
@@ -0,0 +1,388 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# visual_separation_manager.py
|
| 2 |
+
"""
|
| 3 |
+
Visual Separation Manager for Enhanced UI Display.
|
| 4 |
+
|
| 5 |
+
This module provides visual separation and styling management for different
|
| 6 |
+
content types in the enhanced results display system.
|
| 7 |
+
|
| 8 |
+
Requirements: 7.1, 7.2, 7.3, 7.4, 7.5
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
from typing import Dict, List, Optional
|
| 12 |
+
from dataclasses import dataclass
|
| 13 |
+
from enum import Enum
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class ContentType(Enum):
|
| 17 |
+
"""Types of content for visual styling."""
|
| 18 |
+
AI_ANALYSIS = "ai_analysis"
|
| 19 |
+
PATIENT_MESSAGE = "patient_message"
|
| 20 |
+
PROVIDER_SUMMARY = "provider_summary"
|
| 21 |
+
SECTION_SEPARATOR = "section_separator"
|
| 22 |
+
CONTENT_DIVIDER = "content_divider"
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
@dataclass
|
| 26 |
+
class VisualStyle:
|
| 27 |
+
"""Visual styling configuration for content types."""
|
| 28 |
+
background_color: str
|
| 29 |
+
border_color: str
|
| 30 |
+
border_width: str = "2px"
|
| 31 |
+
border_radius: str = "8px"
|
| 32 |
+
padding: str = "15px"
|
| 33 |
+
margin: str = "10px 0"
|
| 34 |
+
text_color: str = "#333333"
|
| 35 |
+
icon_color: str = "#666666"
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class VisualSeparationManager:
|
| 39 |
+
"""
|
| 40 |
+
Visual Separation Manager for enhanced content display.
|
| 41 |
+
|
| 42 |
+
Provides consistent visual styling and separation for different types
|
| 43 |
+
of content in the enhanced results display system.
|
| 44 |
+
|
| 45 |
+
Requirements: 7.1, 7.2, 7.3, 7.4, 7.5
|
| 46 |
+
"""
|
| 47 |
+
|
| 48 |
+
def __init__(self):
|
| 49 |
+
"""Initialize the visual separation manager with default styles."""
|
| 50 |
+
self._initialize_default_styles()
|
| 51 |
+
|
| 52 |
+
def _initialize_default_styles(self) -> None:
|
| 53 |
+
"""Initialize default visual styles for different content types."""
|
| 54 |
+
self.styles = {
|
| 55 |
+
ContentType.AI_ANALYSIS: VisualStyle(
|
| 56 |
+
background_color="#fafafa",
|
| 57 |
+
border_color="#6c757d",
|
| 58 |
+
text_color="#333333",
|
| 59 |
+
icon_color="#6c757d"
|
| 60 |
+
),
|
| 61 |
+
ContentType.PATIENT_MESSAGE: VisualStyle(
|
| 62 |
+
background_color="#f0f7ff",
|
| 63 |
+
border_color="#4a90e2",
|
| 64 |
+
text_color="#333333",
|
| 65 |
+
icon_color="#4a90e2"
|
| 66 |
+
),
|
| 67 |
+
ContentType.PROVIDER_SUMMARY: VisualStyle(
|
| 68 |
+
background_color="#fff8f0",
|
| 69 |
+
border_color="#dc3545",
|
| 70 |
+
text_color="#333333",
|
| 71 |
+
icon_color="#dc3545"
|
| 72 |
+
)
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
# Classification-specific colors
|
| 76 |
+
self.classification_colors = {
|
| 77 |
+
"RED": "#dc3545",
|
| 78 |
+
"YELLOW": "#ffc107",
|
| 79 |
+
"GREEN": "#28a745"
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
# Urgency-specific colors
|
| 83 |
+
self.urgency_colors = {
|
| 84 |
+
"IMMEDIATE": "#dc3545",
|
| 85 |
+
"URGENT": "#fd7e14",
|
| 86 |
+
"STANDARD": "#28a745"
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
def create_ai_analysis_styling(self) -> Dict[str, str]:
|
| 90 |
+
"""
|
| 91 |
+
Create styling configuration for AI analysis sections.
|
| 92 |
+
|
| 93 |
+
Returns:
|
| 94 |
+
Dictionary of CSS styling properties
|
| 95 |
+
|
| 96 |
+
Requirements: 7.1, 7.3
|
| 97 |
+
"""
|
| 98 |
+
style = self.styles[ContentType.AI_ANALYSIS]
|
| 99 |
+
|
| 100 |
+
return {
|
| 101 |
+
"container": f"""
|
| 102 |
+
border: {style.border_width} solid {style.border_color};
|
| 103 |
+
border-radius: {style.border_radius};
|
| 104 |
+
padding: {style.padding};
|
| 105 |
+
margin: {style.margin};
|
| 106 |
+
background-color: {style.background_color};
|
| 107 |
+
""",
|
| 108 |
+
"header": f"""
|
| 109 |
+
display: flex;
|
| 110 |
+
align-items: center;
|
| 111 |
+
margin-bottom: 10px;
|
| 112 |
+
color: {style.text_color};
|
| 113 |
+
font-weight: bold;
|
| 114 |
+
""",
|
| 115 |
+
"icon": f"""
|
| 116 |
+
font-size: 1.2em;
|
| 117 |
+
margin-right: 8px;
|
| 118 |
+
color: {style.icon_color};
|
| 119 |
+
""",
|
| 120 |
+
"content": f"""
|
| 121 |
+
color: {style.text_color};
|
| 122 |
+
line-height: 1.4;
|
| 123 |
+
"""
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
def create_patient_message_styling(self) -> Dict[str, str]:
|
| 127 |
+
"""
|
| 128 |
+
Create styling configuration for patient message sections.
|
| 129 |
+
|
| 130 |
+
Returns:
|
| 131 |
+
Dictionary of CSS styling properties
|
| 132 |
+
|
| 133 |
+
Requirements: 7.2, 7.3
|
| 134 |
+
"""
|
| 135 |
+
style = self.styles[ContentType.PATIENT_MESSAGE]
|
| 136 |
+
|
| 137 |
+
return {
|
| 138 |
+
"container": f"""
|
| 139 |
+
border: {style.border_width} solid {style.border_color};
|
| 140 |
+
border-radius: {style.border_radius};
|
| 141 |
+
padding: {style.padding};
|
| 142 |
+
margin: {style.margin};
|
| 143 |
+
background-color: {style.background_color};
|
| 144 |
+
""",
|
| 145 |
+
"header": f"""
|
| 146 |
+
display: flex;
|
| 147 |
+
align-items: center;
|
| 148 |
+
margin-bottom: 10px;
|
| 149 |
+
color: {style.border_color};
|
| 150 |
+
font-weight: bold;
|
| 151 |
+
""",
|
| 152 |
+
"icon": f"""
|
| 153 |
+
font-size: 1.2em;
|
| 154 |
+
margin-right: 8px;
|
| 155 |
+
color: {style.icon_color};
|
| 156 |
+
""",
|
| 157 |
+
"message_box": f"""
|
| 158 |
+
padding: 8px;
|
| 159 |
+
background-color: white;
|
| 160 |
+
border-radius: 4px;
|
| 161 |
+
border-left: 4px solid {style.border_color};
|
| 162 |
+
color: {style.text_color};
|
| 163 |
+
"""
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
def create_provider_summary_styling(self) -> Dict[str, str]:
|
| 167 |
+
"""
|
| 168 |
+
Create styling configuration for provider summary sections.
|
| 169 |
+
|
| 170 |
+
Returns:
|
| 171 |
+
Dictionary of CSS styling properties
|
| 172 |
+
|
| 173 |
+
Requirements: 7.1, 7.2, 7.3
|
| 174 |
+
"""
|
| 175 |
+
style = self.styles[ContentType.PROVIDER_SUMMARY]
|
| 176 |
+
|
| 177 |
+
return {
|
| 178 |
+
"container": f"""
|
| 179 |
+
border: {style.border_width} solid {style.border_color};
|
| 180 |
+
border-radius: {style.border_radius};
|
| 181 |
+
padding: {style.padding};
|
| 182 |
+
margin: {style.margin};
|
| 183 |
+
background-color: {style.background_color};
|
| 184 |
+
""",
|
| 185 |
+
"header": f"""
|
| 186 |
+
display: flex;
|
| 187 |
+
align-items: center;
|
| 188 |
+
margin-bottom: 10px;
|
| 189 |
+
color: {style.border_color};
|
| 190 |
+
font-weight: bold;
|
| 191 |
+
""",
|
| 192 |
+
"icon": f"""
|
| 193 |
+
font-size: 1.2em;
|
| 194 |
+
margin-right: 8px;
|
| 195 |
+
color: {style.icon_color};
|
| 196 |
+
""",
|
| 197 |
+
"info_box": f"""
|
| 198 |
+
margin: 10px 0;
|
| 199 |
+
padding: 8px;
|
| 200 |
+
background-color: #f8f9fa;
|
| 201 |
+
border-radius: 4px;
|
| 202 |
+
""",
|
| 203 |
+
"urgency_box": f"""
|
| 204 |
+
margin: 5px 0;
|
| 205 |
+
padding: 8px;
|
| 206 |
+
background-color: #fff3cd;
|
| 207 |
+
border-radius: 4px;
|
| 208 |
+
border-left: 4px solid {style.border_color};
|
| 209 |
+
"""
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
def generate_section_separators(self) -> Dict[str, str]:
|
| 213 |
+
"""
|
| 214 |
+
Generate different types of section separators.
|
| 215 |
+
|
| 216 |
+
Returns:
|
| 217 |
+
Dictionary of separator HTML strings
|
| 218 |
+
|
| 219 |
+
Requirements: 7.4, 7.5
|
| 220 |
+
"""
|
| 221 |
+
return {
|
| 222 |
+
"light": """
|
| 223 |
+
<div style='margin: 15px 0; border-top: 1px solid #e0e0e0;'></div>
|
| 224 |
+
""",
|
| 225 |
+
|
| 226 |
+
"medium": """
|
| 227 |
+
<div style='margin: 20px 0; text-align: center;'>
|
| 228 |
+
<hr style='border: none; border-top: 2px solid #d0d0d0; width: 80%; margin: 0 auto;'>
|
| 229 |
+
</div>
|
| 230 |
+
""",
|
| 231 |
+
|
| 232 |
+
"heavy": """
|
| 233 |
+
<div style='margin: 30px 0; text-align: center;'>
|
| 234 |
+
<div style='display: inline-block; padding: 8px 16px; background-color: #f0f0f0; border-radius: 20px; color: #666; font-size: 0.8em;'>
|
| 235 |
+
● ● ●
|
| 236 |
+
</div>
|
| 237 |
+
</div>
|
| 238 |
+
""",
|
| 239 |
+
|
| 240 |
+
"section_break": """
|
| 241 |
+
<div style='margin: 25px 0; text-align: center;'>
|
| 242 |
+
<hr style='border: none; border-top: 3px solid #ccc; width: 60%; margin: 0 auto;'>
|
| 243 |
+
<div style='margin: 10px 0; color: #666; font-size: 0.9em; font-weight: bold;'>---</div>
|
| 244 |
+
</div>
|
| 245 |
+
"""
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
def apply_consistent_formatting(self, sections: List[Dict[str, str]]) -> str:
|
| 249 |
+
"""
|
| 250 |
+
Apply consistent formatting to multiple content sections.
|
| 251 |
+
|
| 252 |
+
Args:
|
| 253 |
+
sections: List of section dictionaries with 'type' and 'content' keys
|
| 254 |
+
|
| 255 |
+
Returns:
|
| 256 |
+
Combined HTML with consistent formatting and separation
|
| 257 |
+
|
| 258 |
+
Requirements: 7.1, 7.2, 7.4, 7.5
|
| 259 |
+
"""
|
| 260 |
+
if not sections:
|
| 261 |
+
return "<div style='padding: 20px; text-align: center; color: #666;'>No content to display</div>"
|
| 262 |
+
|
| 263 |
+
formatted_sections = []
|
| 264 |
+
separators = self.generate_section_separators()
|
| 265 |
+
|
| 266 |
+
for i, section in enumerate(sections):
|
| 267 |
+
section_type = section.get('type', 'unknown')
|
| 268 |
+
content = section.get('content', '')
|
| 269 |
+
|
| 270 |
+
# Apply appropriate styling based on section type
|
| 271 |
+
if section_type == 'ai_analysis':
|
| 272 |
+
styling = self.create_ai_analysis_styling()
|
| 273 |
+
elif section_type == 'patient_message':
|
| 274 |
+
styling = self.create_patient_message_styling()
|
| 275 |
+
elif section_type == 'provider_summary':
|
| 276 |
+
styling = self.create_provider_summary_styling()
|
| 277 |
+
else:
|
| 278 |
+
# Default styling for unknown types
|
| 279 |
+
styling = {
|
| 280 |
+
"container": "padding: 10px; margin: 10px 0; border: 1px solid #ddd; border-radius: 4px;"
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
# Wrap content in styled container
|
| 284 |
+
formatted_content = f"""
|
| 285 |
+
<div style='{styling.get("container", "")}'>
|
| 286 |
+
{content}
|
| 287 |
+
</div>
|
| 288 |
+
"""
|
| 289 |
+
|
| 290 |
+
formatted_sections.append(formatted_content)
|
| 291 |
+
|
| 292 |
+
# Add separator between sections (but not after the last one)
|
| 293 |
+
if i < len(sections) - 1:
|
| 294 |
+
formatted_sections.append(separators["section_break"])
|
| 295 |
+
|
| 296 |
+
return "".join(formatted_sections)
|
| 297 |
+
|
| 298 |
+
def get_classification_styling(self, classification: str) -> Dict[str, str]:
|
| 299 |
+
"""
|
| 300 |
+
Get styling specific to a classification level.
|
| 301 |
+
|
| 302 |
+
Args:
|
| 303 |
+
classification: The classification level (RED/YELLOW/GREEN)
|
| 304 |
+
|
| 305 |
+
Returns:
|
| 306 |
+
Dictionary of classification-specific styling
|
| 307 |
+
|
| 308 |
+
Requirements: 7.3
|
| 309 |
+
"""
|
| 310 |
+
color = self.classification_colors.get(classification.upper(), "#666666")
|
| 311 |
+
|
| 312 |
+
return {
|
| 313 |
+
"badge": f"""
|
| 314 |
+
display: inline-block;
|
| 315 |
+
padding: 4px 8px;
|
| 316 |
+
background-color: {color};
|
| 317 |
+
color: white;
|
| 318 |
+
border-radius: 12px;
|
| 319 |
+
font-size: 0.8em;
|
| 320 |
+
font-weight: bold;
|
| 321 |
+
text-transform: uppercase;
|
| 322 |
+
""",
|
| 323 |
+
"border": f"""
|
| 324 |
+
border-color: {color};
|
| 325 |
+
""",
|
| 326 |
+
"text": f"""
|
| 327 |
+
color: {color};
|
| 328 |
+
""",
|
| 329 |
+
"background": f"""
|
| 330 |
+
background-color: {color}15;
|
| 331 |
+
"""
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
def get_urgency_styling(self, urgency_level: str) -> Dict[str, str]:
|
| 335 |
+
"""
|
| 336 |
+
Get styling specific to an urgency level.
|
| 337 |
+
|
| 338 |
+
Args:
|
| 339 |
+
urgency_level: The urgency level (IMMEDIATE/URGENT/STANDARD)
|
| 340 |
+
|
| 341 |
+
Returns:
|
| 342 |
+
Dictionary of urgency-specific styling
|
| 343 |
+
|
| 344 |
+
Requirements: 7.3
|
| 345 |
+
"""
|
| 346 |
+
color = self.urgency_colors.get(urgency_level.upper(), "#666666")
|
| 347 |
+
|
| 348 |
+
return {
|
| 349 |
+
"badge": f"""
|
| 350 |
+
display: inline-block;
|
| 351 |
+
padding: 4px 8px;
|
| 352 |
+
background-color: {color};
|
| 353 |
+
color: white;
|
| 354 |
+
border-radius: 12px;
|
| 355 |
+
font-size: 0.8em;
|
| 356 |
+
font-weight: bold;
|
| 357 |
+
text-transform: uppercase;
|
| 358 |
+
""",
|
| 359 |
+
"border": f"""
|
| 360 |
+
border-color: {color};
|
| 361 |
+
""",
|
| 362 |
+
"text": f"""
|
| 363 |
+
color: {color};
|
| 364 |
+
""",
|
| 365 |
+
"accent": f"""
|
| 366 |
+
border-left: 4px solid {color};
|
| 367 |
+
"""
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
def create_icon_styling(self, content_type: ContentType) -> str:
|
| 371 |
+
"""
|
| 372 |
+
Create icon styling for different content types.
|
| 373 |
+
|
| 374 |
+
Args:
|
| 375 |
+
content_type: The type of content for icon styling
|
| 376 |
+
|
| 377 |
+
Returns:
|
| 378 |
+
CSS styling string for icons
|
| 379 |
+
|
| 380 |
+
Requirements: 7.3, 7.5
|
| 381 |
+
"""
|
| 382 |
+
base_style = "font-size: 1.2em; margin-right: 8px;"
|
| 383 |
+
|
| 384 |
+
if content_type in self.styles:
|
| 385 |
+
color = self.styles[content_type].icon_color
|
| 386 |
+
return f"{base_style} color: {color};"
|
| 387 |
+
|
| 388 |
+
return f"{base_style} color: #666666;"
|
|
@@ -0,0 +1,274 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Integration tests for enhanced verification system.
|
| 4 |
+
|
| 5 |
+
Tests the integration between enhanced display formats, verification system,
|
| 6 |
+
and CSV export functionality.
|
| 7 |
+
|
| 8 |
+
Requirements: 8.2, 8.3, 8.4, 8.5
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import pytest
|
| 12 |
+
import tempfile
|
| 13 |
+
import os
|
| 14 |
+
from datetime import datetime
|
| 15 |
+
from unittest.mock import Mock
|
| 16 |
+
|
| 17 |
+
from src.core.conversation_logger import ConversationLogger, ConversationEntry
|
| 18 |
+
from src.core.conversation_verification import (
|
| 19 |
+
EnhancedConversationVerificationManager,
|
| 20 |
+
EnhancedVerificationSession,
|
| 21 |
+
EnhancedVerificationRecord,
|
| 22 |
+
VerificationFeedback
|
| 23 |
+
)
|
| 24 |
+
from src.core.verification_exporter import EnhancedVerificationExporter
|
| 25 |
+
from src.core.spiritual_state import SpiritualState, SpiritualAssessment
|
| 26 |
+
from src.core.provider_summary_generator import ProviderSummary
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class TestEnhancedVerificationIntegration:
|
| 30 |
+
"""Test enhanced verification system integration."""
|
| 31 |
+
|
| 32 |
+
def setup_method(self):
|
| 33 |
+
"""Set up test environment."""
|
| 34 |
+
self.temp_dir = tempfile.mkdtemp()
|
| 35 |
+
self.verification_manager = EnhancedConversationVerificationManager(self.temp_dir)
|
| 36 |
+
self.exporter = EnhancedVerificationExporter(self.temp_dir)
|
| 37 |
+
|
| 38 |
+
def teardown_method(self):
|
| 39 |
+
"""Clean up test environment."""
|
| 40 |
+
import shutil
|
| 41 |
+
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
| 42 |
+
|
| 43 |
+
def create_test_conversation_logger(self) -> ConversationLogger:
|
| 44 |
+
"""Create a test conversation logger with sample data."""
|
| 45 |
+
logger = ConversationLogger(patient_name="Test Patient")
|
| 46 |
+
|
| 47 |
+
# Add sample conversation entries
|
| 48 |
+
assessments = [
|
| 49 |
+
SpiritualAssessment(
|
| 50 |
+
state=SpiritualState.RED,
|
| 51 |
+
confidence=0.85,
|
| 52 |
+
indicators=["loss of meaning", "spiritual distress", "questioning faith"],
|
| 53 |
+
reasoning="Patient expressing significant spiritual concerns and loss of meaning"
|
| 54 |
+
),
|
| 55 |
+
SpiritualAssessment(
|
| 56 |
+
state=SpiritualState.YELLOW,
|
| 57 |
+
confidence=0.65,
|
| 58 |
+
indicators=["mild anxiety", "uncertainty"],
|
| 59 |
+
reasoning="Patient showing some concern but not severe distress"
|
| 60 |
+
),
|
| 61 |
+
SpiritualAssessment(
|
| 62 |
+
state=SpiritualState.GREEN,
|
| 63 |
+
confidence=0.90,
|
| 64 |
+
indicators=[],
|
| 65 |
+
reasoning="Patient appears stable and content"
|
| 66 |
+
)
|
| 67 |
+
]
|
| 68 |
+
|
| 69 |
+
messages = [
|
| 70 |
+
("I've been struggling with the meaning of my illness", "I understand this is a difficult time..."),
|
| 71 |
+
("Sometimes I wonder if there's any point to all this", "Your feelings are completely valid..."),
|
| 72 |
+
("I'm feeling a bit better today", "That's wonderful to hear...")
|
| 73 |
+
]
|
| 74 |
+
|
| 75 |
+
for (user_msg, assistant_msg), assessment in zip(messages, assessments):
|
| 76 |
+
logger.log_exchange(user_msg, assistant_msg, assessment)
|
| 77 |
+
|
| 78 |
+
return logger
|
| 79 |
+
|
| 80 |
+
def test_enhanced_verification_session_creation(self):
|
| 81 |
+
"""Test creating enhanced verification session with new formats."""
|
| 82 |
+
# Create test conversation
|
| 83 |
+
logger = self.create_test_conversation_logger()
|
| 84 |
+
|
| 85 |
+
# Create enhanced verification session
|
| 86 |
+
session = self.verification_manager.create_verification_session(
|
| 87 |
+
logger,
|
| 88 |
+
verifier_name="Test Verifier",
|
| 89 |
+
enable_enhanced_formats=True
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
# Verify session properties
|
| 93 |
+
assert isinstance(session, EnhancedVerificationSession)
|
| 94 |
+
assert session.enhanced_format_enabled is True
|
| 95 |
+
assert len(session.verification_records) == 3
|
| 96 |
+
|
| 97 |
+
# Verify enhanced records
|
| 98 |
+
for record in session.verification_records:
|
| 99 |
+
assert isinstance(record, EnhancedVerificationRecord)
|
| 100 |
+
assert record.enhanced_display_format is not None
|
| 101 |
+
assert record.visual_sections is not None
|
| 102 |
+
assert len(record.visual_sections) >= 2 # AI analysis + patient message
|
| 103 |
+
|
| 104 |
+
def test_enhanced_csv_export_with_new_data(self):
|
| 105 |
+
"""Test CSV export includes enhanced format data."""
|
| 106 |
+
# Create and verify session
|
| 107 |
+
logger = self.create_test_conversation_logger()
|
| 108 |
+
session = self.verification_manager.create_verification_session(
|
| 109 |
+
logger,
|
| 110 |
+
enable_enhanced_formats=True
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
# Add some verification feedback
|
| 114 |
+
feedback = VerificationFeedback(
|
| 115 |
+
exchange_id=session.verification_records[0].exchange_id,
|
| 116 |
+
is_correct=True
|
| 117 |
+
)
|
| 118 |
+
self.verification_manager.submit_exchange_verification(
|
| 119 |
+
session.session_id,
|
| 120 |
+
session.verification_records[0].exchange_id,
|
| 121 |
+
feedback
|
| 122 |
+
)
|
| 123 |
+
|
| 124 |
+
# Export to CSV with enhanced data
|
| 125 |
+
csv_path = self.exporter.export_session_to_csv(session, include_enhanced_data=True)
|
| 126 |
+
|
| 127 |
+
# Verify CSV file exists and contains enhanced data
|
| 128 |
+
assert os.path.exists(csv_path)
|
| 129 |
+
|
| 130 |
+
with open(csv_path, 'r', encoding='utf-8') as f:
|
| 131 |
+
content = f.read()
|
| 132 |
+
|
| 133 |
+
# Check for enhanced format columns
|
| 134 |
+
assert 'has_enhanced_display' in content
|
| 135 |
+
assert 'visual_sections_count' in content
|
| 136 |
+
assert 'enhanced_indicators_count' in content
|
| 137 |
+
assert 'enhanced_display_preview' in content
|
| 138 |
+
|
| 139 |
+
# Check for enhanced session metadata
|
| 140 |
+
assert 'Enhanced Format: True' in content
|
| 141 |
+
|
| 142 |
+
def test_enhanced_summary_report_generation(self):
|
| 143 |
+
"""Test enhanced summary report includes new format statistics."""
|
| 144 |
+
# Create session with enhanced formats
|
| 145 |
+
logger = self.create_test_conversation_logger()
|
| 146 |
+
session = self.verification_manager.create_verification_session(
|
| 147 |
+
logger,
|
| 148 |
+
enable_enhanced_formats=True
|
| 149 |
+
)
|
| 150 |
+
|
| 151 |
+
# Generate enhanced summary report
|
| 152 |
+
report_path = self.exporter.export_enhanced_summary_report(session)
|
| 153 |
+
|
| 154 |
+
# Verify report file exists
|
| 155 |
+
assert os.path.exists(report_path)
|
| 156 |
+
|
| 157 |
+
with open(report_path, 'r', encoding='utf-8') as f:
|
| 158 |
+
content = f.read()
|
| 159 |
+
|
| 160 |
+
# Check for enhanced format statistics
|
| 161 |
+
assert 'ENHANCED FORMAT STATISTICS' in content
|
| 162 |
+
assert 'Records with Enhanced Display:' in content
|
| 163 |
+
assert 'Enhanced Display Coverage:' in content
|
| 164 |
+
assert 'Enhanced Format Enabled: True' in content
|
| 165 |
+
|
| 166 |
+
def test_verification_compatibility_with_existing_system(self):
|
| 167 |
+
"""Test that enhanced verification is compatible with existing verification system."""
|
| 168 |
+
# Create session without enhanced formats (legacy mode)
|
| 169 |
+
logger = self.create_test_conversation_logger()
|
| 170 |
+
legacy_session = self.verification_manager.create_verification_session(
|
| 171 |
+
logger,
|
| 172 |
+
enable_enhanced_formats=False
|
| 173 |
+
)
|
| 174 |
+
|
| 175 |
+
# Verify it still works with basic verification
|
| 176 |
+
feedback = VerificationFeedback(
|
| 177 |
+
exchange_id=legacy_session.verification_records[0].exchange_id,
|
| 178 |
+
is_correct=False,
|
| 179 |
+
correct_classification="YELLOW",
|
| 180 |
+
correction_reason="Should be yellow not red"
|
| 181 |
+
)
|
| 182 |
+
|
| 183 |
+
success = self.verification_manager.submit_exchange_verification(
|
| 184 |
+
legacy_session.session_id,
|
| 185 |
+
legacy_session.verification_records[0].exchange_id,
|
| 186 |
+
feedback
|
| 187 |
+
)
|
| 188 |
+
|
| 189 |
+
assert success is True
|
| 190 |
+
|
| 191 |
+
# Verify statistics still work
|
| 192 |
+
stats = self.verification_manager.get_session_statistics(legacy_session.session_id)
|
| 193 |
+
assert stats is not None
|
| 194 |
+
assert 'enhanced_format_enabled' in stats
|
| 195 |
+
assert stats['enhanced_format_enabled'] is False
|
| 196 |
+
|
| 197 |
+
def test_provider_summary_integration_with_verification(self):
|
| 198 |
+
"""Test that provider summaries are properly integrated with verification."""
|
| 199 |
+
# Create session with provider summary data
|
| 200 |
+
logger = self.create_test_conversation_logger()
|
| 201 |
+
session = self.verification_manager.create_verification_session(
|
| 202 |
+
logger,
|
| 203 |
+
enable_enhanced_formats=True
|
| 204 |
+
)
|
| 205 |
+
|
| 206 |
+
# Add provider summary to a record
|
| 207 |
+
test_summary = ProviderSummary(
|
| 208 |
+
patient_name="Test Patient",
|
| 209 |
+
patient_phone="555-0123",
|
| 210 |
+
classification="RED",
|
| 211 |
+
confidence=0.85,
|
| 212 |
+
indicators=["loss of meaning", "spiritual distress"],
|
| 213 |
+
reasoning="Significant spiritual concerns detected",
|
| 214 |
+
urgency_level="IMMEDIATE",
|
| 215 |
+
severity_level="HIGH"
|
| 216 |
+
)
|
| 217 |
+
|
| 218 |
+
# Set provider summary on first record
|
| 219 |
+
session.verification_records[0].set_enhanced_formats(
|
| 220 |
+
provider_summary=test_summary
|
| 221 |
+
)
|
| 222 |
+
|
| 223 |
+
# Save updated session
|
| 224 |
+
self.verification_manager.save_session(session)
|
| 225 |
+
|
| 226 |
+
# Export with enhanced data
|
| 227 |
+
export_data = self.verification_manager.export_session_with_enhanced_data(session.session_id)
|
| 228 |
+
|
| 229 |
+
# Verify provider summary data is included
|
| 230 |
+
assert export_data is not None
|
| 231 |
+
record_data = export_data['records'][0]
|
| 232 |
+
assert record_data['provider_summary'] is not None
|
| 233 |
+
assert record_data['provider_summary']['urgency_level'] == 'IMMEDIATE'
|
| 234 |
+
assert record_data['provider_summary']['severity_level'] == 'HIGH'
|
| 235 |
+
|
| 236 |
+
def test_coherent_paragraph_format_in_verification(self):
|
| 237 |
+
"""Test that coherent paragraph format is properly handled in verification."""
|
| 238 |
+
# Create session
|
| 239 |
+
logger = self.create_test_conversation_logger()
|
| 240 |
+
session = self.verification_manager.create_verification_session(
|
| 241 |
+
logger,
|
| 242 |
+
enable_enhanced_formats=True
|
| 243 |
+
)
|
| 244 |
+
|
| 245 |
+
# Add coherent paragraph to a record
|
| 246 |
+
coherent_paragraph = "Test Patient is a 45-year-old individual with clinical history of chronic illness. The patient expressed loss of meaning and spiritual distress, which may indicate significant spiritual concerns, resulting in generation of a RED FLAG. The patient has been identified for spiritual care team contact. The preferred contact number is 555-0123."
|
| 247 |
+
|
| 248 |
+
session.verification_records[0].coherent_summary_paragraph = coherent_paragraph
|
| 249 |
+
|
| 250 |
+
# Save and export
|
| 251 |
+
self.verification_manager.save_session(session)
|
| 252 |
+
csv_path = self.exporter.export_session_to_csv(session, include_enhanced_data=True)
|
| 253 |
+
|
| 254 |
+
# Verify coherent paragraph data is in CSV
|
| 255 |
+
with open(csv_path, 'r', encoding='utf-8') as f:
|
| 256 |
+
content = f.read()
|
| 257 |
+
|
| 258 |
+
# Should have coherent paragraph length > 0
|
| 259 |
+
lines = content.split('\n')
|
| 260 |
+
data_lines = [line for line in lines if line and not line.startswith('#') and 'coherent_paragraph_length' not in line]
|
| 261 |
+
if len(data_lines) > 2: # Header + data rows
|
| 262 |
+
# Find the data row for the first record
|
| 263 |
+
for line in data_lines[2:]: # Skip header and metadata
|
| 264 |
+
if line.strip():
|
| 265 |
+
fields = line.split(',')
|
| 266 |
+
if len(fields) > 15: # Should have enhanced fields
|
| 267 |
+
coherent_length_field = fields[-5] # coherent_paragraph_length field
|
| 268 |
+
if coherent_length_field.isdigit():
|
| 269 |
+
assert int(coherent_length_field) > 0
|
| 270 |
+
break
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
if __name__ == "__main__":
|
| 274 |
+
pytest.main([__file__])
|
|
@@ -0,0 +1,204 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Integration tests for error handling in UI components.
|
| 4 |
+
|
| 5 |
+
Tests the integration of error handling across UI components.
|
| 6 |
+
|
| 7 |
+
Requirements: 9.1, 9.2, 9.3, 9.4
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import pytest
|
| 11 |
+
from unittest.mock import Mock, patch
|
| 12 |
+
|
| 13 |
+
from src.interface.enhanced_results_display_manager import EnhancedResultsDisplayManager
|
| 14 |
+
from src.core.provider_summary_generator import ProviderSummary, ProviderSummaryGenerator
|
| 15 |
+
from src.core.improved_classification_prompt_manager import ImprovedClassificationPromptManager
|
| 16 |
+
from src.config.enhanced_display_config import EnhancedDisplayConfig
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class TestErrorHandlingIntegration:
|
| 20 |
+
"""Integration tests for error handling across UI components."""
|
| 21 |
+
|
| 22 |
+
def setup_method(self):
|
| 23 |
+
"""Set up test fixtures."""
|
| 24 |
+
self.display_manager = EnhancedResultsDisplayManager()
|
| 25 |
+
self.summary_generator = ProviderSummaryGenerator()
|
| 26 |
+
self.classification_manager = ImprovedClassificationPromptManager()
|
| 27 |
+
|
| 28 |
+
def test_display_manager_handles_invalid_summary(self):
|
| 29 |
+
"""Test that display manager handles invalid provider summary gracefully."""
|
| 30 |
+
# Create invalid summary with missing required fields
|
| 31 |
+
invalid_summary = ProviderSummary(
|
| 32 |
+
patient_name="[Patient Name]", # Placeholder
|
| 33 |
+
patient_phone="[Phone Number]", # Placeholder
|
| 34 |
+
classification="RED",
|
| 35 |
+
confidence=1.5, # Invalid confidence
|
| 36 |
+
reasoning="", # Empty reasoning
|
| 37 |
+
indicators=[], # No indicators
|
| 38 |
+
severity_level="INVALID", # Invalid level
|
| 39 |
+
urgency_level="INVALID" # Invalid level
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
# Format should not crash and should include validation warnings
|
| 43 |
+
result = self.display_manager.format_provider_summary_section(invalid_summary)
|
| 44 |
+
|
| 45 |
+
assert isinstance(result, str)
|
| 46 |
+
assert len(result) > 0
|
| 47 |
+
# Should contain some form of content even with invalid data
|
| 48 |
+
assert "Provider Summary" in result or "Display Error" in result
|
| 49 |
+
|
| 50 |
+
def test_display_manager_handles_formatting_error(self):
|
| 51 |
+
"""Test that display manager handles formatting errors gracefully."""
|
| 52 |
+
# Mock the config to cause an error
|
| 53 |
+
with patch.object(self.display_manager, 'config', None):
|
| 54 |
+
result = self.display_manager.format_ai_analysis_section(
|
| 55 |
+
classification="RED",
|
| 56 |
+
indicators=["Test indicator"],
|
| 57 |
+
reasoning="Test reasoning"
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
assert isinstance(result, str)
|
| 61 |
+
assert len(result) > 0
|
| 62 |
+
# Should fall back to basic formatting or show error
|
| 63 |
+
assert "AI Analysis" in result or "Display Error" in result
|
| 64 |
+
|
| 65 |
+
def test_summary_generator_handles_missing_data(self):
|
| 66 |
+
"""Test that summary generator handles missing data gracefully."""
|
| 67 |
+
# Generate summary with minimal data
|
| 68 |
+
summary = self.summary_generator.generate_summary(
|
| 69 |
+
indicators=[], # Empty indicators
|
| 70 |
+
reasoning="", # Empty reasoning
|
| 71 |
+
confidence=0.0,
|
| 72 |
+
patient_name=None, # Missing name
|
| 73 |
+
patient_phone=None # Missing phone
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
assert isinstance(summary, ProviderSummary)
|
| 77 |
+
# Should have fallback values
|
| 78 |
+
assert summary.patient_name != "[Patient Name]" or "Patient" in summary.patient_name
|
| 79 |
+
assert len(summary.reasoning) > 10 # Should have fallback reasoning
|
| 80 |
+
assert len(summary.recommended_actions) > 0 # Should have default actions
|
| 81 |
+
|
| 82 |
+
def test_summary_generator_handles_generation_error(self):
|
| 83 |
+
"""Test that summary generator handles generation errors gracefully."""
|
| 84 |
+
# Mock an internal method to raise an error
|
| 85 |
+
with patch.object(self.summary_generator, '_generate_conversation_summary', side_effect=Exception("Test error")):
|
| 86 |
+
summary = self.summary_generator.generate_summary(
|
| 87 |
+
indicators=["Test indicator"],
|
| 88 |
+
reasoning="Test reasoning",
|
| 89 |
+
confidence=0.8,
|
| 90 |
+
patient_name="John Doe",
|
| 91 |
+
patient_phone="555-123-4567"
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
assert isinstance(summary, ProviderSummary)
|
| 95 |
+
# Should be a fallback summary
|
| 96 |
+
assert summary.patient_name in ["John Doe", "Patient (Name Not Available)"]
|
| 97 |
+
assert len(summary.reasoning) > 0
|
| 98 |
+
|
| 99 |
+
def test_classification_manager_handles_invalid_result(self):
|
| 100 |
+
"""Test that classification manager handles invalid results gracefully."""
|
| 101 |
+
# Create invalid classification result
|
| 102 |
+
result = self.classification_manager.create_classification_result(
|
| 103 |
+
classification="INVALID", # Invalid classification
|
| 104 |
+
confidence=2.0, # Invalid confidence
|
| 105 |
+
indicators=[], # Empty indicators
|
| 106 |
+
reasoning="" # Empty reasoning
|
| 107 |
+
)
|
| 108 |
+
|
| 109 |
+
assert result is not None
|
| 110 |
+
assert result.classification in ["red", "yellow", "green"]
|
| 111 |
+
assert 0.0 <= result.confidence <= 1.0
|
| 112 |
+
assert len(result.indicators) > 0
|
| 113 |
+
assert len(result.reasoning) > 0
|
| 114 |
+
|
| 115 |
+
def test_end_to_end_error_recovery(self):
|
| 116 |
+
"""Test end-to-end error recovery across components."""
|
| 117 |
+
# Start with problematic data
|
| 118 |
+
problematic_data = {
|
| 119 |
+
'classification': 'INVALID',
|
| 120 |
+
'confidence': -0.5,
|
| 121 |
+
'indicators': [],
|
| 122 |
+
'reasoning': '',
|
| 123 |
+
'patient_name': '',
|
| 124 |
+
'patient_phone': ''
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
# Generate summary (should apply fallbacks)
|
| 128 |
+
summary = self.summary_generator.generate_summary(
|
| 129 |
+
indicators=problematic_data['indicators'],
|
| 130 |
+
reasoning=problematic_data['reasoning'],
|
| 131 |
+
confidence=problematic_data['confidence'],
|
| 132 |
+
patient_name=problematic_data['patient_name'],
|
| 133 |
+
patient_phone=problematic_data['patient_phone']
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
# Display summary (should handle validation issues)
|
| 137 |
+
display_result = self.display_manager.format_provider_summary_section(summary)
|
| 138 |
+
|
| 139 |
+
# Verify the entire pipeline produces usable output
|
| 140 |
+
assert isinstance(display_result, str)
|
| 141 |
+
assert len(display_result) > 0
|
| 142 |
+
assert "Provider Summary" in display_result or "Display Error" in display_result
|
| 143 |
+
|
| 144 |
+
# Verify summary has been fixed
|
| 145 |
+
assert summary.confidence >= 0.0
|
| 146 |
+
assert len(summary.reasoning) > 10
|
| 147 |
+
assert len(summary.recommended_actions) > 0
|
| 148 |
+
|
| 149 |
+
def test_degraded_mode_functionality(self):
|
| 150 |
+
"""Test that system continues to function in degraded mode."""
|
| 151 |
+
# Disable enhancements to test degraded mode
|
| 152 |
+
degraded_config = EnhancedDisplayConfig(enabled=False)
|
| 153 |
+
degraded_display_manager = EnhancedResultsDisplayManager(config=degraded_config)
|
| 154 |
+
|
| 155 |
+
# Create a valid summary
|
| 156 |
+
summary = ProviderSummary(
|
| 157 |
+
patient_name="John Doe",
|
| 158 |
+
patient_phone="555-123-4567",
|
| 159 |
+
classification="RED",
|
| 160 |
+
confidence=0.8,
|
| 161 |
+
reasoning="Test reasoning for degraded mode",
|
| 162 |
+
indicators=["Test indicator"],
|
| 163 |
+
severity_level="HIGH",
|
| 164 |
+
urgency_level="URGENT",
|
| 165 |
+
situation_description="Test situation",
|
| 166 |
+
recommended_actions=["Test action"]
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
# Format in degraded mode
|
| 170 |
+
result = degraded_display_manager.format_provider_summary_section(summary)
|
| 171 |
+
|
| 172 |
+
assert isinstance(result, str)
|
| 173 |
+
assert len(result) > 0
|
| 174 |
+
# Should contain basic information even in degraded mode
|
| 175 |
+
assert "John Doe" in result
|
| 176 |
+
assert "555-123-4567" in result
|
| 177 |
+
assert "URGENT" in result
|
| 178 |
+
|
| 179 |
+
def test_error_statistics_collection(self):
|
| 180 |
+
"""Test that error statistics are properly collected."""
|
| 181 |
+
# Create summary with validation issues
|
| 182 |
+
problematic_summary = ProviderSummary(
|
| 183 |
+
patient_name="[Patient Name]",
|
| 184 |
+
patient_phone="[Phone Number]",
|
| 185 |
+
classification="RED",
|
| 186 |
+
confidence=1.5, # Invalid
|
| 187 |
+
reasoning="", # Empty
|
| 188 |
+
indicators=[] # Empty
|
| 189 |
+
)
|
| 190 |
+
|
| 191 |
+
# Validate and collect errors
|
| 192 |
+
validation_result = self.display_manager.error_handler.validate_provider_summary_structure(problematic_summary)
|
| 193 |
+
|
| 194 |
+
# Get error statistics
|
| 195 |
+
stats = self.display_manager.error_handler.get_error_statistics(validation_result.errors)
|
| 196 |
+
|
| 197 |
+
assert stats["total"] > 0
|
| 198 |
+
assert "validation" in stats["by_category"] or "data_missing" in stats["by_category"]
|
| 199 |
+
assert len(stats["by_severity"]) > 0
|
| 200 |
+
assert len(stats["by_component"]) > 0
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
if __name__ == "__main__":
|
| 204 |
+
pytest.main([__file__])
|
|
@@ -0,0 +1,558 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Integration tests for UI Classification Improvements.
|
| 4 |
+
|
| 5 |
+
Tests the complete workflow from chat to verification with all enhanced components
|
| 6 |
+
working together correctly. This validates task 10 requirements.
|
| 7 |
+
|
| 8 |
+
Requirements: 10.1 - Complete workflow testing
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import pytest
|
| 12 |
+
import tempfile
|
| 13 |
+
import os
|
| 14 |
+
import json
|
| 15 |
+
from datetime import datetime
|
| 16 |
+
from unittest.mock import Mock, patch
|
| 17 |
+
|
| 18 |
+
from src.interface.enhanced_results_display_manager import EnhancedResultsDisplayManager
|
| 19 |
+
from src.core.provider_summary_generator import ProviderSummary, ProviderSummaryGenerator
|
| 20 |
+
from src.core.improved_classification_prompt_manager import ImprovedClassificationPromptManager
|
| 21 |
+
from src.config.enhanced_display_config import EnhancedDisplayConfig, get_enhanced_display_config
|
| 22 |
+
from src.core.conversation_logger import ConversationLogger
|
| 23 |
+
from src.core.spiritual_state import SpiritualState, SpiritualAssessment
|
| 24 |
+
from src.core.conversation_verification import EnhancedConversationVerificationManager
|
| 25 |
+
from src.core.ui_error_handler import UIErrorHandler
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class TestUIClassificationImprovementsIntegration:
|
| 29 |
+
"""
|
| 30 |
+
Integration tests for UI Classification Improvements.
|
| 31 |
+
|
| 32 |
+
Tests the complete workflow: Chat → Classification → Display → Verification
|
| 33 |
+
"""
|
| 34 |
+
|
| 35 |
+
def setup_method(self):
|
| 36 |
+
"""Set up test environment."""
|
| 37 |
+
self.temp_dir = tempfile.mkdtemp()
|
| 38 |
+
|
| 39 |
+
# Initialize all components
|
| 40 |
+
self.display_manager = EnhancedResultsDisplayManager()
|
| 41 |
+
self.summary_generator = ProviderSummaryGenerator()
|
| 42 |
+
self.classification_manager = ImprovedClassificationPromptManager()
|
| 43 |
+
self.verification_manager = EnhancedConversationVerificationManager(self.temp_dir)
|
| 44 |
+
self.error_handler = UIErrorHandler()
|
| 45 |
+
|
| 46 |
+
# Test data
|
| 47 |
+
self.test_patient_name = "Integration Test Patient"
|
| 48 |
+
self.test_patient_phone = "555-0199"
|
| 49 |
+
|
| 50 |
+
def teardown_method(self):
|
| 51 |
+
"""Clean up test environment."""
|
| 52 |
+
import shutil
|
| 53 |
+
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
| 54 |
+
|
| 55 |
+
def create_test_conversation_logger(self) -> ConversationLogger:
|
| 56 |
+
"""Create a conversation logger with test data for integration testing."""
|
| 57 |
+
logger = ConversationLogger(patient_name=self.test_patient_name)
|
| 58 |
+
|
| 59 |
+
# Simulate a conversation that should trigger RED classification
|
| 60 |
+
test_exchanges = [
|
| 61 |
+
{
|
| 62 |
+
"user_message": "I've been feeling really lost lately",
|
| 63 |
+
"assistant_response": "I understand this can be a difficult time. Can you tell me more about what you're experiencing?",
|
| 64 |
+
"classification": SpiritualState.YELLOW,
|
| 65 |
+
"confidence": 0.65,
|
| 66 |
+
"indicators": ["feeling lost", "emotional distress"],
|
| 67 |
+
"reasoning": "Patient expressing feelings of being lost, requires further assessment"
|
| 68 |
+
},
|
| 69 |
+
{
|
| 70 |
+
"user_message": "I just don't see the point in anything anymore. My life feels meaningless since my diagnosis.",
|
| 71 |
+
"assistant_response": "I hear that you're struggling with finding meaning right now. These feelings are understandable given what you're going through.",
|
| 72 |
+
"classification": SpiritualState.RED,
|
| 73 |
+
"confidence": 0.85,
|
| 74 |
+
"indicators": ["loss of meaning", "existential distress", "questioning purpose"],
|
| 75 |
+
"reasoning": "Patient expressing loss of meaning and purpose, which is an explicit red flag indicator"
|
| 76 |
+
},
|
| 77 |
+
{
|
| 78 |
+
"user_message": "Sometimes I wonder if there's any point to all this suffering",
|
| 79 |
+
"assistant_response": "Your feelings about suffering are very important. Many people in your situation have similar questions.",
|
| 80 |
+
"classification": SpiritualState.RED,
|
| 81 |
+
"confidence": 0.90,
|
| 82 |
+
"indicators": ["doubt about meaning of suffering", "existential questioning"],
|
| 83 |
+
"reasoning": "Patient questioning meaning of suffering - explicit red flag indicator requiring immediate attention"
|
| 84 |
+
}
|
| 85 |
+
]
|
| 86 |
+
|
| 87 |
+
for exchange in test_exchanges:
|
| 88 |
+
assessment = SpiritualAssessment(
|
| 89 |
+
state=exchange["classification"],
|
| 90 |
+
confidence=exchange["confidence"],
|
| 91 |
+
indicators=exchange["indicators"],
|
| 92 |
+
reasoning=exchange["reasoning"]
|
| 93 |
+
)
|
| 94 |
+
logger.log_exchange(
|
| 95 |
+
exchange["user_message"],
|
| 96 |
+
exchange["assistant_response"],
|
| 97 |
+
assessment
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
return logger
|
| 101 |
+
|
| 102 |
+
def test_complete_workflow_integration(self):
|
| 103 |
+
"""
|
| 104 |
+
Test the complete workflow from chat to verification.
|
| 105 |
+
|
| 106 |
+
This is the main integration test that validates all components
|
| 107 |
+
work together correctly.
|
| 108 |
+
"""
|
| 109 |
+
print("🧪 Testing complete UI Classification Improvements workflow...")
|
| 110 |
+
|
| 111 |
+
# Step 1: Create conversation with enhanced classification
|
| 112 |
+
print(" 1. Creating conversation with enhanced classification...")
|
| 113 |
+
logger = self.create_test_conversation_logger()
|
| 114 |
+
|
| 115 |
+
# Verify conversation was logged correctly
|
| 116 |
+
assert len(logger.entries) == 3
|
| 117 |
+
assert logger.patient_name == self.test_patient_name
|
| 118 |
+
|
| 119 |
+
# Step 2: Generate provider summary with enhanced features
|
| 120 |
+
print(" 2. Generating enhanced provider summary...")
|
| 121 |
+
|
| 122 |
+
# Get the RED flag assessment from the conversation
|
| 123 |
+
red_assessment = None
|
| 124 |
+
for entry in logger.entries:
|
| 125 |
+
if entry.spiritual_classification == "RED":
|
| 126 |
+
# Create SpiritualAssessment from entry data
|
| 127 |
+
from src.core.spiritual_state import SpiritualState
|
| 128 |
+
red_assessment = SpiritualAssessment(
|
| 129 |
+
state=SpiritualState.RED,
|
| 130 |
+
confidence=entry.classification_confidence,
|
| 131 |
+
indicators=entry.classification_indicators,
|
| 132 |
+
reasoning=entry.classification_reasoning
|
| 133 |
+
)
|
| 134 |
+
break
|
| 135 |
+
|
| 136 |
+
assert red_assessment is not None, "Should have RED flag assessment"
|
| 137 |
+
|
| 138 |
+
# Generate enhanced provider summary
|
| 139 |
+
summary = self.summary_generator.generate_summary(
|
| 140 |
+
indicators=red_assessment.indicators,
|
| 141 |
+
reasoning=red_assessment.reasoning,
|
| 142 |
+
confidence=red_assessment.confidence,
|
| 143 |
+
patient_name=self.test_patient_name,
|
| 144 |
+
patient_phone=self.test_patient_phone,
|
| 145 |
+
conversation_context="Patient expressing loss of meaning and questioning suffering",
|
| 146 |
+
medical_context={
|
| 147 |
+
"age": 45,
|
| 148 |
+
"gender": "individual",
|
| 149 |
+
"conditions": ["chronic illness", "recent diagnosis"]
|
| 150 |
+
}
|
| 151 |
+
)
|
| 152 |
+
|
| 153 |
+
# Verify summary was generated correctly
|
| 154 |
+
assert isinstance(summary, ProviderSummary)
|
| 155 |
+
assert summary.patient_name == self.test_patient_name
|
| 156 |
+
assert summary.patient_phone == self.test_patient_phone
|
| 157 |
+
assert summary.classification == "RED"
|
| 158 |
+
assert len(summary.indicators) > 0
|
| 159 |
+
|
| 160 |
+
# Step 3: Format with enhanced display manager
|
| 161 |
+
print(" 3. Formatting with enhanced display manager...")
|
| 162 |
+
|
| 163 |
+
# Test AI analysis section formatting
|
| 164 |
+
ai_analysis_html = self.display_manager.format_ai_analysis_section(
|
| 165 |
+
classification="RED",
|
| 166 |
+
indicators=red_assessment.indicators,
|
| 167 |
+
reasoning=red_assessment.reasoning,
|
| 168 |
+
confidence=red_assessment.confidence
|
| 169 |
+
)
|
| 170 |
+
|
| 171 |
+
assert isinstance(ai_analysis_html, str)
|
| 172 |
+
assert len(ai_analysis_html) > 0
|
| 173 |
+
assert "AI Analysis" in ai_analysis_html
|
| 174 |
+
assert "RED FLAG" in ai_analysis_html
|
| 175 |
+
|
| 176 |
+
# Test patient message section formatting
|
| 177 |
+
patient_message_html = self.display_manager.format_patient_message_section(
|
| 178 |
+
"Sometimes I wonder if there's any point to all this suffering"
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
assert isinstance(patient_message_html, str)
|
| 182 |
+
assert "Patient Message" in patient_message_html
|
| 183 |
+
assert "suffering" in patient_message_html
|
| 184 |
+
|
| 185 |
+
# Test provider summary section formatting
|
| 186 |
+
provider_summary_html = self.display_manager.format_provider_summary_section(summary)
|
| 187 |
+
|
| 188 |
+
assert isinstance(provider_summary_html, str)
|
| 189 |
+
assert "Provider Summary" in provider_summary_html
|
| 190 |
+
assert self.test_patient_name in provider_summary_html
|
| 191 |
+
assert self.test_patient_phone in provider_summary_html
|
| 192 |
+
|
| 193 |
+
# Step 4: Test coherent paragraph formatting
|
| 194 |
+
print(" 4. Testing coherent paragraph formatting...")
|
| 195 |
+
|
| 196 |
+
coherent_paragraph = self.summary_generator.format_coherent_paragraph(summary)
|
| 197 |
+
|
| 198 |
+
assert isinstance(coherent_paragraph, str)
|
| 199 |
+
assert len(coherent_paragraph) > 50 # Should be substantial
|
| 200 |
+
assert self.test_patient_name in coherent_paragraph
|
| 201 |
+
assert "45-year-old" in coherent_paragraph or "individual" in coherent_paragraph
|
| 202 |
+
assert "RED FLAG" in coherent_paragraph
|
| 203 |
+
|
| 204 |
+
# Step 5: Test combined results formatting
|
| 205 |
+
print(" 5. Testing combined results formatting...")
|
| 206 |
+
|
| 207 |
+
combined_html = self.display_manager.format_combined_results(
|
| 208 |
+
ai_analysis={
|
| 209 |
+
'classification': 'RED',
|
| 210 |
+
'indicators': red_assessment.indicators,
|
| 211 |
+
'reasoning': red_assessment.reasoning,
|
| 212 |
+
'confidence': red_assessment.confidence
|
| 213 |
+
},
|
| 214 |
+
patient_message="Sometimes I wonder if there's any point to all this suffering",
|
| 215 |
+
provider_summary=summary
|
| 216 |
+
)
|
| 217 |
+
|
| 218 |
+
assert isinstance(combined_html, str)
|
| 219 |
+
assert "AI Analysis" in combined_html
|
| 220 |
+
assert "Patient Message" in combined_html
|
| 221 |
+
assert "Provider Summary" in combined_html
|
| 222 |
+
|
| 223 |
+
# Step 6: Test verification system integration
|
| 224 |
+
print(" 6. Testing verification system integration...")
|
| 225 |
+
|
| 226 |
+
verification_session = self.verification_manager.create_verification_session(
|
| 227 |
+
logger,
|
| 228 |
+
verifier_name="Integration Test",
|
| 229 |
+
enable_enhanced_formats=True
|
| 230 |
+
)
|
| 231 |
+
|
| 232 |
+
assert verification_session is not None
|
| 233 |
+
assert verification_session.enhanced_format_enabled is True
|
| 234 |
+
assert len(verification_session.verification_records) == 3
|
| 235 |
+
|
| 236 |
+
# Verify enhanced formats are applied
|
| 237 |
+
for record in verification_session.verification_records:
|
| 238 |
+
assert record.enhanced_display_format is not None
|
| 239 |
+
assert record.visual_sections is not None
|
| 240 |
+
|
| 241 |
+
# Step 7: Test CSV export with enhanced data
|
| 242 |
+
print(" 7. Testing CSV export with enhanced data...")
|
| 243 |
+
|
| 244 |
+
from src.core.verification_exporter import EnhancedVerificationExporter
|
| 245 |
+
exporter = EnhancedVerificationExporter(self.temp_dir)
|
| 246 |
+
|
| 247 |
+
csv_path = exporter.export_session_to_csv(
|
| 248 |
+
verification_session,
|
| 249 |
+
include_enhanced_data=True
|
| 250 |
+
)
|
| 251 |
+
|
| 252 |
+
assert os.path.exists(csv_path)
|
| 253 |
+
|
| 254 |
+
with open(csv_path, 'r', encoding='utf-8') as f:
|
| 255 |
+
csv_content = f.read()
|
| 256 |
+
|
| 257 |
+
# Verify enhanced data is in CSV
|
| 258 |
+
assert 'has_enhanced_display' in csv_content
|
| 259 |
+
assert 'enhanced_indicators_count' in csv_content
|
| 260 |
+
assert self.test_patient_name in csv_content
|
| 261 |
+
|
| 262 |
+
print(" ✅ Complete workflow integration test passed!")
|
| 263 |
+
|
| 264 |
+
def test_classification_consistency_validation(self):
|
| 265 |
+
"""Test that classification consistency is maintained throughout workflow."""
|
| 266 |
+
print("🧪 Testing classification consistency validation...")
|
| 267 |
+
|
| 268 |
+
# Test explicit red indicators
|
| 269 |
+
explicit_red_indicators = self.classification_manager.get_explicit_red_indicators()
|
| 270 |
+
|
| 271 |
+
assert "Complex grief" in explicit_red_indicators
|
| 272 |
+
assert "Loss of a loved one" in explicit_red_indicators
|
| 273 |
+
assert "Doubt about meaning of life" in explicit_red_indicators
|
| 274 |
+
assert "Doubt about meaning of suffering" in explicit_red_indicators
|
| 275 |
+
assert "Doubt about personal dignity" in explicit_red_indicators
|
| 276 |
+
|
| 277 |
+
# Test classification validation
|
| 278 |
+
test_result = self.classification_manager.create_classification_result(
|
| 279 |
+
classification="red",
|
| 280 |
+
confidence=0.85,
|
| 281 |
+
indicators=["doubt about meaning of suffering"],
|
| 282 |
+
reasoning="Patient questioning meaning of suffering",
|
| 283 |
+
red_flag_indicators=["doubt about meaning of suffering"]
|
| 284 |
+
)
|
| 285 |
+
|
| 286 |
+
assert test_result.is_valid is True
|
| 287 |
+
assert test_result.classification == "red"
|
| 288 |
+
|
| 289 |
+
# Test invalid classification gets corrected
|
| 290 |
+
invalid_result = self.classification_manager.create_classification_result(
|
| 291 |
+
classification="invalid",
|
| 292 |
+
confidence=2.0,
|
| 293 |
+
indicators=[],
|
| 294 |
+
reasoning=""
|
| 295 |
+
)
|
| 296 |
+
|
| 297 |
+
assert invalid_result.classification in ["red", "yellow", "green"]
|
| 298 |
+
assert 0.0 <= invalid_result.confidence <= 1.0
|
| 299 |
+
assert len(invalid_result.indicators) > 0
|
| 300 |
+
|
| 301 |
+
print(" ✅ Classification consistency validation passed!")
|
| 302 |
+
|
| 303 |
+
def test_error_handling_throughout_workflow(self):
|
| 304 |
+
"""Test error handling and recovery throughout the complete workflow."""
|
| 305 |
+
print("🧪 Testing error handling throughout workflow...")
|
| 306 |
+
|
| 307 |
+
# Test with problematic data
|
| 308 |
+
problematic_summary = ProviderSummary(
|
| 309 |
+
patient_name="[Patient Name]", # Placeholder
|
| 310 |
+
patient_phone="[Phone Number]", # Placeholder
|
| 311 |
+
classification="RED",
|
| 312 |
+
confidence=1.5, # Invalid confidence
|
| 313 |
+
reasoning="", # Empty reasoning
|
| 314 |
+
indicators=[], # No indicators
|
| 315 |
+
severity_level="INVALID",
|
| 316 |
+
urgency_level="INVALID"
|
| 317 |
+
)
|
| 318 |
+
|
| 319 |
+
# Display manager should handle this gracefully
|
| 320 |
+
display_result = self.display_manager.format_provider_summary_section(problematic_summary)
|
| 321 |
+
|
| 322 |
+
assert isinstance(display_result, str)
|
| 323 |
+
assert len(display_result) > 0
|
| 324 |
+
# Should contain validation warnings or fallback content
|
| 325 |
+
assert "Provider Summary" in display_result or "validation" in display_result.lower()
|
| 326 |
+
|
| 327 |
+
# Test error statistics collection
|
| 328 |
+
validation_result = self.error_handler.validate_provider_summary_structure(problematic_summary)
|
| 329 |
+
stats = self.error_handler.get_error_statistics(validation_result.errors)
|
| 330 |
+
|
| 331 |
+
assert stats["total"] > 0
|
| 332 |
+
assert len(stats["by_category"]) > 0
|
| 333 |
+
|
| 334 |
+
print(" ✅ Error handling throughout workflow passed!")
|
| 335 |
+
|
| 336 |
+
def test_data_integrity_across_operations(self):
|
| 337 |
+
"""Test that data integrity is maintained across all operations."""
|
| 338 |
+
print("🧪 Testing data integrity across operations...")
|
| 339 |
+
|
| 340 |
+
# Create test data
|
| 341 |
+
original_indicators = ["loss of meaning", "spiritual distress", "questioning faith"]
|
| 342 |
+
original_reasoning = "Patient expressing significant spiritual concerns"
|
| 343 |
+
original_confidence = 0.85
|
| 344 |
+
|
| 345 |
+
# Generate summary
|
| 346 |
+
summary = self.summary_generator.generate_summary(
|
| 347 |
+
indicators=original_indicators,
|
| 348 |
+
reasoning=original_reasoning,
|
| 349 |
+
confidence=original_confidence,
|
| 350 |
+
patient_name=self.test_patient_name,
|
| 351 |
+
patient_phone=self.test_patient_phone
|
| 352 |
+
)
|
| 353 |
+
|
| 354 |
+
# Verify data integrity in summary
|
| 355 |
+
assert summary.patient_name == self.test_patient_name
|
| 356 |
+
assert summary.patient_phone == self.test_patient_phone
|
| 357 |
+
assert summary.confidence == original_confidence
|
| 358 |
+
assert all(indicator in summary.indicators for indicator in original_indicators)
|
| 359 |
+
|
| 360 |
+
# Format for display
|
| 361 |
+
display_html = self.display_manager.format_provider_summary_section(summary)
|
| 362 |
+
|
| 363 |
+
# Verify data integrity in display
|
| 364 |
+
assert self.test_patient_name in display_html
|
| 365 |
+
assert self.test_patient_phone in display_html
|
| 366 |
+
|
| 367 |
+
# Format coherent paragraph
|
| 368 |
+
coherent_paragraph = self.summary_generator.format_coherent_paragraph(summary)
|
| 369 |
+
|
| 370 |
+
# Verify data integrity in coherent paragraph
|
| 371 |
+
assert self.test_patient_name in coherent_paragraph
|
| 372 |
+
assert self.test_patient_phone in coherent_paragraph
|
| 373 |
+
|
| 374 |
+
# Export for verification
|
| 375 |
+
export_data = summary.to_dict()
|
| 376 |
+
|
| 377 |
+
# Verify data integrity in export
|
| 378 |
+
assert export_data["patient_name"] == self.test_patient_name
|
| 379 |
+
assert export_data["patient_phone"] == self.test_patient_phone
|
| 380 |
+
assert export_data["confidence"] == original_confidence
|
| 381 |
+
|
| 382 |
+
print(" ✅ Data integrity across operations passed!")
|
| 383 |
+
|
| 384 |
+
def test_performance_with_multiple_records(self):
|
| 385 |
+
"""Test performance and stability with multiple conversation records."""
|
| 386 |
+
print("🧪 Testing performance with multiple records...")
|
| 387 |
+
|
| 388 |
+
# Create logger with multiple exchanges
|
| 389 |
+
logger = ConversationLogger(patient_name="Performance Test Patient")
|
| 390 |
+
|
| 391 |
+
# Add 10 exchanges to test performance
|
| 392 |
+
for i in range(10):
|
| 393 |
+
assessment = SpiritualAssessment(
|
| 394 |
+
state=SpiritualState.RED if i % 3 == 0 else SpiritualState.YELLOW,
|
| 395 |
+
confidence=0.7 + (i * 0.02),
|
| 396 |
+
indicators=[f"indicator_{i}", f"concern_{i}"],
|
| 397 |
+
reasoning=f"Test reasoning for exchange {i}"
|
| 398 |
+
)
|
| 399 |
+
|
| 400 |
+
logger.log_exchange(
|
| 401 |
+
f"User message {i}: I'm having concerns about my situation",
|
| 402 |
+
f"Assistant response {i}: I understand your concerns",
|
| 403 |
+
assessment
|
| 404 |
+
)
|
| 405 |
+
|
| 406 |
+
# Test verification session creation with multiple records
|
| 407 |
+
verification_session = self.verification_manager.create_verification_session(
|
| 408 |
+
logger,
|
| 409 |
+
enable_enhanced_formats=True
|
| 410 |
+
)
|
| 411 |
+
|
| 412 |
+
assert len(verification_session.verification_records) == 10
|
| 413 |
+
|
| 414 |
+
# Test that all records have enhanced formats
|
| 415 |
+
enhanced_count = sum(
|
| 416 |
+
1 for record in verification_session.verification_records
|
| 417 |
+
if record.enhanced_display_format is not None
|
| 418 |
+
)
|
| 419 |
+
|
| 420 |
+
assert enhanced_count == 10
|
| 421 |
+
|
| 422 |
+
# Test CSV export performance
|
| 423 |
+
from src.core.verification_exporter import EnhancedVerificationExporter
|
| 424 |
+
exporter = EnhancedVerificationExporter(self.temp_dir)
|
| 425 |
+
|
| 426 |
+
csv_path = exporter.export_session_to_csv(
|
| 427 |
+
verification_session,
|
| 428 |
+
include_enhanced_data=True
|
| 429 |
+
)
|
| 430 |
+
|
| 431 |
+
assert os.path.exists(csv_path)
|
| 432 |
+
|
| 433 |
+
# Verify CSV contains all records
|
| 434 |
+
with open(csv_path, 'r', encoding='utf-8') as f:
|
| 435 |
+
csv_content = f.read()
|
| 436 |
+
|
| 437 |
+
# Should have header + metadata + 10 data rows
|
| 438 |
+
lines = [line for line in csv_content.split('\n') if line.strip()]
|
| 439 |
+
data_lines = [line for line in lines if not line.startswith('#')]
|
| 440 |
+
|
| 441 |
+
# At least header + 10 records
|
| 442 |
+
assert len(data_lines) >= 11
|
| 443 |
+
|
| 444 |
+
print(" ✅ Performance with multiple records passed!")
|
| 445 |
+
|
| 446 |
+
def test_configuration_management_integration(self):
|
| 447 |
+
"""Test that configuration management works correctly across components."""
|
| 448 |
+
print("🧪 Testing configuration management integration...")
|
| 449 |
+
|
| 450 |
+
# Test default configuration
|
| 451 |
+
default_config = get_enhanced_display_config()
|
| 452 |
+
assert default_config.enabled is True
|
| 453 |
+
# Note: use_icons default may vary based on configuration file
|
| 454 |
+
assert hasattr(default_config, 'use_icons')
|
| 455 |
+
|
| 456 |
+
# Test custom configuration
|
| 457 |
+
custom_config = EnhancedDisplayConfig(
|
| 458 |
+
enabled=True,
|
| 459 |
+
use_icons=False,
|
| 460 |
+
use_visual_separators=False
|
| 461 |
+
)
|
| 462 |
+
|
| 463 |
+
custom_display_manager = EnhancedResultsDisplayManager(config=custom_config)
|
| 464 |
+
|
| 465 |
+
# Test that custom config is applied
|
| 466 |
+
assert custom_display_manager.config.use_icons is False
|
| 467 |
+
assert custom_display_manager.config.use_visual_separators is False
|
| 468 |
+
|
| 469 |
+
# Test formatting with custom config
|
| 470 |
+
result = custom_display_manager.format_ai_analysis_section(
|
| 471 |
+
classification="RED",
|
| 472 |
+
indicators=["test indicator"],
|
| 473 |
+
reasoning="test reasoning"
|
| 474 |
+
)
|
| 475 |
+
|
| 476 |
+
assert isinstance(result, str)
|
| 477 |
+
assert len(result) > 0
|
| 478 |
+
|
| 479 |
+
# Test disabled mode
|
| 480 |
+
disabled_config = EnhancedDisplayConfig(enabled=False)
|
| 481 |
+
disabled_display_manager = EnhancedResultsDisplayManager(config=disabled_config)
|
| 482 |
+
|
| 483 |
+
result = disabled_display_manager.format_ai_analysis_section(
|
| 484 |
+
classification="RED",
|
| 485 |
+
indicators=["test indicator"],
|
| 486 |
+
reasoning="test reasoning"
|
| 487 |
+
)
|
| 488 |
+
|
| 489 |
+
# Should fall back to basic formatting
|
| 490 |
+
assert isinstance(result, str)
|
| 491 |
+
assert "AI Analysis" in result
|
| 492 |
+
|
| 493 |
+
print(" ✅ Configuration management integration passed!")
|
| 494 |
+
|
| 495 |
+
|
| 496 |
+
def run_integration_checkpoint():
|
| 497 |
+
"""
|
| 498 |
+
Run the integration checkpoint tests.
|
| 499 |
+
|
| 500 |
+
This function runs all integration tests and provides a summary
|
| 501 |
+
of the results for task 10 validation.
|
| 502 |
+
"""
|
| 503 |
+
print("🚀 Running UI Classification Improvements Integration Checkpoint")
|
| 504 |
+
print("=" * 70)
|
| 505 |
+
|
| 506 |
+
# Create test instance
|
| 507 |
+
test_instance = TestUIClassificationImprovementsIntegration()
|
| 508 |
+
|
| 509 |
+
tests = [
|
| 510 |
+
("Complete Workflow Integration", test_instance.test_complete_workflow_integration),
|
| 511 |
+
("Classification Consistency", test_instance.test_classification_consistency_validation),
|
| 512 |
+
("Error Handling Throughout Workflow", test_instance.test_error_handling_throughout_workflow),
|
| 513 |
+
("Data Integrity Across Operations", test_instance.test_data_integrity_across_operations),
|
| 514 |
+
("Performance with Multiple Records", test_instance.test_performance_with_multiple_records),
|
| 515 |
+
("Configuration Management", test_instance.test_configuration_management_integration)
|
| 516 |
+
]
|
| 517 |
+
|
| 518 |
+
passed = 0
|
| 519 |
+
failed = 0
|
| 520 |
+
|
| 521 |
+
for test_name, test_func in tests:
|
| 522 |
+
try:
|
| 523 |
+
print(f"\n🧪 Running: {test_name}")
|
| 524 |
+
test_instance.setup_method()
|
| 525 |
+
test_func()
|
| 526 |
+
test_instance.teardown_method()
|
| 527 |
+
print(f" ✅ {test_name} PASSED")
|
| 528 |
+
passed += 1
|
| 529 |
+
except Exception as e:
|
| 530 |
+
print(f" ❌ {test_name} FAILED: {e}")
|
| 531 |
+
failed += 1
|
| 532 |
+
import traceback
|
| 533 |
+
traceback.print_exc()
|
| 534 |
+
|
| 535 |
+
print("\n" + "=" * 70)
|
| 536 |
+
print("📊 INTEGRATION CHECKPOINT RESULTS")
|
| 537 |
+
print("=" * 70)
|
| 538 |
+
print(f"✅ Tests Passed: {passed}")
|
| 539 |
+
print(f"❌ Tests Failed: {failed}")
|
| 540 |
+
print(f"📈 Success Rate: {(passed / (passed + failed)) * 100:.1f}%")
|
| 541 |
+
|
| 542 |
+
if failed == 0:
|
| 543 |
+
print("\n🎉 ALL INTEGRATION TESTS PASSED!")
|
| 544 |
+
print("✅ All components work together correctly")
|
| 545 |
+
print("✅ Full workflow from chat to verification validated")
|
| 546 |
+
print("✅ Data integrity maintained across all operations")
|
| 547 |
+
print("✅ Error handling and recovery working properly")
|
| 548 |
+
print("✅ System ready for production use")
|
| 549 |
+
return True
|
| 550 |
+
else:
|
| 551 |
+
print(f"\n⚠️ {failed} integration tests failed")
|
| 552 |
+
print("❌ System requires fixes before production use")
|
| 553 |
+
return False
|
| 554 |
+
|
| 555 |
+
|
| 556 |
+
if __name__ == "__main__":
|
| 557 |
+
success = run_integration_checkpoint()
|
| 558 |
+
exit(0 if success else 1)
|
|
@@ -0,0 +1,247 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# test_coherent_summary_formatter.py
|
| 2 |
+
"""
|
| 3 |
+
Unit tests for Coherent Summary Formatter functionality.
|
| 4 |
+
|
| 5 |
+
Tests the new coherent paragraph formatting functionality added to
|
| 6 |
+
ProviderSummaryGenerator for requirements 2.1-2.8.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import pytest
|
| 10 |
+
from unittest.mock import Mock
|
| 11 |
+
from datetime import datetime
|
| 12 |
+
|
| 13 |
+
from src.core.provider_summary_generator import ProviderSummaryGenerator, ProviderSummary
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class TestCoherentSummaryFormatter:
|
| 17 |
+
"""Test cases for coherent summary formatting functionality."""
|
| 18 |
+
|
| 19 |
+
def setup_method(self):
|
| 20 |
+
"""Set up test fixtures."""
|
| 21 |
+
self.generator = ProviderSummaryGenerator()
|
| 22 |
+
|
| 23 |
+
def test_format_coherent_paragraph_basic(self):
|
| 24 |
+
"""Test basic coherent paragraph formatting."""
|
| 25 |
+
# Create sample provider summary
|
| 26 |
+
summary = ProviderSummary(
|
| 27 |
+
patient_name="John Doe",
|
| 28 |
+
patient_phone="(555) 123-4567",
|
| 29 |
+
indicators=["Loss of faith", "Spiritual distress"],
|
| 30 |
+
situation_description="existential crisis",
|
| 31 |
+
medical_context={
|
| 32 |
+
"age": 45,
|
| 33 |
+
"gender": "male",
|
| 34 |
+
"conditions": ["Diabetes", "Hypertension"]
|
| 35 |
+
}
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
result = self.generator.format_coherent_paragraph(summary)
|
| 39 |
+
|
| 40 |
+
# Check that all required elements are present (Requirements 2.2, 2.3, 2.4, 2.5)
|
| 41 |
+
assert "John Doe is a 45-year-old male" in result
|
| 42 |
+
assert "clinical history of Diabetes and Hypertension" in result
|
| 43 |
+
assert "Loss of faith and Spiritual distress" in result
|
| 44 |
+
assert "existential crisis" in result
|
| 45 |
+
assert "RED FLAG" in result
|
| 46 |
+
assert "(555) 123-4567" in result
|
| 47 |
+
|
| 48 |
+
# Check that it's a single paragraph (Requirement 2.1)
|
| 49 |
+
lines = result.split('\n')
|
| 50 |
+
main_paragraph_lines = [line for line in lines if line.strip() and not line.startswith('Patient reported:')]
|
| 51 |
+
assert len(main_paragraph_lines) == 1
|
| 52 |
+
|
| 53 |
+
def test_format_coherent_paragraph_no_medical_history(self):
|
| 54 |
+
"""Test coherent paragraph with no medical history."""
|
| 55 |
+
summary = ProviderSummary(
|
| 56 |
+
patient_name="Jane Smith",
|
| 57 |
+
patient_phone="(555) 987-6543",
|
| 58 |
+
indicators=["Anxiety"],
|
| 59 |
+
situation_description="mild distress",
|
| 60 |
+
medical_context={"age": 30, "gender": "female"}
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
result = self.generator.format_coherent_paragraph(summary)
|
| 64 |
+
|
| 65 |
+
assert "Jane Smith is a 30-year-old female" in result
|
| 66 |
+
assert "no significant medical history documented" in result
|
| 67 |
+
assert "Anxiety" in result
|
| 68 |
+
assert "mild distress" in result
|
| 69 |
+
|
| 70 |
+
def test_format_coherent_paragraph_single_condition(self):
|
| 71 |
+
"""Test coherent paragraph with single medical condition."""
|
| 72 |
+
summary = ProviderSummary(
|
| 73 |
+
patient_name="Bob Wilson",
|
| 74 |
+
patient_phone="(555) 456-7890",
|
| 75 |
+
indicators=["Depression"],
|
| 76 |
+
situation_description="emotional distress",
|
| 77 |
+
medical_context={
|
| 78 |
+
"age": 55,
|
| 79 |
+
"gender": "male",
|
| 80 |
+
"conditions": ["Cancer"]
|
| 81 |
+
}
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
result = self.generator.format_coherent_paragraph(summary)
|
| 85 |
+
|
| 86 |
+
assert "clinical history of Cancer" in result
|
| 87 |
+
assert "Depression" in result
|
| 88 |
+
|
| 89 |
+
def test_format_coherent_paragraph_multiple_conditions(self):
|
| 90 |
+
"""Test coherent paragraph with multiple medical conditions."""
|
| 91 |
+
summary = ProviderSummary(
|
| 92 |
+
patient_name="Alice Brown",
|
| 93 |
+
patient_phone="(555) 321-0987",
|
| 94 |
+
indicators=["Fear", "Uncertainty", "Loss of hope"],
|
| 95 |
+
situation_description="complex medical situation",
|
| 96 |
+
medical_context={
|
| 97 |
+
"age": 67,
|
| 98 |
+
"gender": "female",
|
| 99 |
+
"conditions": ["Heart Disease", "Diabetes", "Arthritis", "COPD"]
|
| 100 |
+
}
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
result = self.generator.format_coherent_paragraph(summary)
|
| 104 |
+
|
| 105 |
+
# Should format as "X, Y, Z, and W"
|
| 106 |
+
assert "clinical history of Heart Disease, Diabetes, Arthritis, and COPD" in result
|
| 107 |
+
assert "Fear, Uncertainty, and Loss of hope" in result
|
| 108 |
+
|
| 109 |
+
def test_format_coherent_paragraph_no_phone(self):
|
| 110 |
+
"""Test coherent paragraph with no phone number."""
|
| 111 |
+
summary = ProviderSummary(
|
| 112 |
+
patient_name="Charlie Davis",
|
| 113 |
+
patient_phone="",
|
| 114 |
+
indicators=["Spiritual questioning"],
|
| 115 |
+
situation_description="seeking meaning",
|
| 116 |
+
medical_context={"age": 40, "gender": "male"}
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
result = self.generator.format_coherent_paragraph(summary)
|
| 120 |
+
|
| 121 |
+
assert "No contact number is currently available" in result
|
| 122 |
+
|
| 123 |
+
def test_format_coherent_paragraph_with_conversation_context(self):
|
| 124 |
+
"""Test coherent paragraph with patient quote from conversation."""
|
| 125 |
+
summary = ProviderSummary(
|
| 126 |
+
patient_name="Diana Evans",
|
| 127 |
+
patient_phone="(555) 654-3210",
|
| 128 |
+
indicators=["Existential questioning"],
|
| 129 |
+
situation_description="spiritual crisis",
|
| 130 |
+
medical_context={"age": 35, "gender": "female"},
|
| 131 |
+
conversation_context="I don't understand why this is happening to me and my family"
|
| 132 |
+
)
|
| 133 |
+
|
| 134 |
+
result = self.generator.format_coherent_paragraph(summary)
|
| 135 |
+
|
| 136 |
+
# Should include patient quote as separate line (Requirement 2.8)
|
| 137 |
+
assert 'Patient reported: "I don\'t understand why this is happening to me and my family"' in result
|
| 138 |
+
|
| 139 |
+
# Should have main paragraph and quote separated
|
| 140 |
+
parts = result.split('\n\n')
|
| 141 |
+
assert len(parts) == 2
|
| 142 |
+
|
| 143 |
+
def test_format_coherent_paragraph_no_indicators(self):
|
| 144 |
+
"""Test coherent paragraph with no specific indicators."""
|
| 145 |
+
summary = ProviderSummary(
|
| 146 |
+
patient_name="Frank Green",
|
| 147 |
+
patient_phone="(555) 789-0123",
|
| 148 |
+
indicators=[],
|
| 149 |
+
situation_description="general concern",
|
| 150 |
+
medical_context={"age": 50, "gender": "male"}
|
| 151 |
+
)
|
| 152 |
+
|
| 153 |
+
result = self.generator.format_coherent_paragraph(summary)
|
| 154 |
+
|
| 155 |
+
assert "general distress" in result
|
| 156 |
+
assert "general concern" in result
|
| 157 |
+
|
| 158 |
+
def test_format_coherent_paragraph_unknown_gender(self):
|
| 159 |
+
"""Test coherent paragraph with unknown gender."""
|
| 160 |
+
summary = ProviderSummary(
|
| 161 |
+
patient_name="Taylor Smith",
|
| 162 |
+
patient_phone="(555) 111-2222",
|
| 163 |
+
indicators=["Anxiety"],
|
| 164 |
+
situation_description="stress",
|
| 165 |
+
medical_context={"age": 28} # No gender specified
|
| 166 |
+
)
|
| 167 |
+
|
| 168 |
+
result = self.generator.format_coherent_paragraph(summary)
|
| 169 |
+
|
| 170 |
+
assert "Taylor Smith is a 28-year-old individual" in result
|
| 171 |
+
|
| 172 |
+
def test_format_coherent_paragraph_no_age(self):
|
| 173 |
+
"""Test coherent paragraph with no age specified."""
|
| 174 |
+
summary = ProviderSummary(
|
| 175 |
+
patient_name="Sam Johnson",
|
| 176 |
+
patient_phone="(555) 333-4444",
|
| 177 |
+
indicators=["Worry"],
|
| 178 |
+
situation_description="concern",
|
| 179 |
+
medical_context={"gender": "non-binary"}
|
| 180 |
+
)
|
| 181 |
+
|
| 182 |
+
result = self.generator.format_coherent_paragraph(summary)
|
| 183 |
+
|
| 184 |
+
assert "Sam Johnson is a unknown age non-binary" in result
|
| 185 |
+
|
| 186 |
+
def test_format_coherent_paragraph_minimal_data(self):
|
| 187 |
+
"""Test coherent paragraph with minimal data."""
|
| 188 |
+
summary = ProviderSummary(
|
| 189 |
+
patient_name="Minimal Patient",
|
| 190 |
+
patient_phone="",
|
| 191 |
+
indicators=[],
|
| 192 |
+
situation_description="",
|
| 193 |
+
medical_context=None
|
| 194 |
+
)
|
| 195 |
+
|
| 196 |
+
result = self.generator.format_coherent_paragraph(summary)
|
| 197 |
+
|
| 198 |
+
# Should still produce a valid paragraph
|
| 199 |
+
assert "Minimal Patient is a unknown age individual" in result
|
| 200 |
+
assert "no significant medical history documented" in result
|
| 201 |
+
assert "general distress" in result
|
| 202 |
+
assert "spiritual or emotional distress" in result
|
| 203 |
+
assert "No contact number is currently available" in result
|
| 204 |
+
|
| 205 |
+
def test_format_coherent_paragraph_sentence_structure(self):
|
| 206 |
+
"""Test that coherent paragraph has proper sentence structure."""
|
| 207 |
+
summary = ProviderSummary(
|
| 208 |
+
patient_name="Structure Test",
|
| 209 |
+
patient_phone="(555) 555-5555",
|
| 210 |
+
indicators=["Test indicator"],
|
| 211 |
+
situation_description="test situation",
|
| 212 |
+
medical_context={"age": 40, "gender": "male", "conditions": ["Test condition"]}
|
| 213 |
+
)
|
| 214 |
+
|
| 215 |
+
result = self.generator.format_coherent_paragraph(summary)
|
| 216 |
+
|
| 217 |
+
# Should end with period
|
| 218 |
+
main_paragraph = result.split('\n\n')[0]
|
| 219 |
+
assert main_paragraph.endswith('.')
|
| 220 |
+
|
| 221 |
+
# Should be properly connected sentences
|
| 222 |
+
sentences = main_paragraph.split('. ')
|
| 223 |
+
assert len(sentences) >= 4 # Should have multiple connected sentences
|
| 224 |
+
|
| 225 |
+
def test_format_coherent_paragraph_integration_with_existing(self):
|
| 226 |
+
"""Test that coherent paragraph doesn't break existing functionality."""
|
| 227 |
+
summary = ProviderSummary(
|
| 228 |
+
patient_name="Integration Test",
|
| 229 |
+
patient_phone="(555) 999-8888",
|
| 230 |
+
indicators=["Integration test"],
|
| 231 |
+
situation_description="testing integration"
|
| 232 |
+
)
|
| 233 |
+
|
| 234 |
+
# Test that existing format_for_display still works
|
| 235 |
+
structured_result = self.generator.format_for_display(summary)
|
| 236 |
+
assert "PROVIDER SUMMARY - SPIRITUAL CARE REFERRAL" in structured_result
|
| 237 |
+
|
| 238 |
+
# Test that new coherent format works
|
| 239 |
+
coherent_result = self.generator.format_coherent_paragraph(summary)
|
| 240 |
+
assert "Integration Test is a unknown age individual" in coherent_result
|
| 241 |
+
|
| 242 |
+
# Results should be different formats
|
| 243 |
+
assert structured_result != coherent_result
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
if __name__ == "__main__":
|
| 247 |
+
pytest.main([__file__])
|
|
@@ -0,0 +1,325 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Unit tests for Enhanced Display Configuration System.
|
| 4 |
+
|
| 5 |
+
Tests the configuration system for UI classification improvements
|
| 6 |
+
including feature toggles, styling options, and configuration management.
|
| 7 |
+
|
| 8 |
+
Requirements: 7.1, 7.2, 7.3
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import unittest
|
| 12 |
+
import tempfile
|
| 13 |
+
import os
|
| 14 |
+
import json
|
| 15 |
+
from unittest.mock import patch, MagicMock
|
| 16 |
+
|
| 17 |
+
import sys
|
| 18 |
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
| 19 |
+
src_dir = os.path.join(os.path.dirname(os.path.dirname(current_dir)), 'src')
|
| 20 |
+
if src_dir not in sys.path:
|
| 21 |
+
sys.path.insert(0, src_dir)
|
| 22 |
+
|
| 23 |
+
from config.enhanced_display_config import (
|
| 24 |
+
EnhancedDisplayConfig,
|
| 25 |
+
EnhancedDisplayConfigManager,
|
| 26 |
+
SectionStylingConfig,
|
| 27 |
+
ClassificationColorConfig,
|
| 28 |
+
VisualSeparatorConfig,
|
| 29 |
+
create_high_contrast_config,
|
| 30 |
+
create_minimal_config,
|
| 31 |
+
create_mobile_optimized_config
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class TestEnhancedDisplayConfig(unittest.TestCase):
|
| 36 |
+
"""Test cases for EnhancedDisplayConfig class."""
|
| 37 |
+
|
| 38 |
+
def setUp(self):
|
| 39 |
+
"""Set up test fixtures."""
|
| 40 |
+
self.config = EnhancedDisplayConfig()
|
| 41 |
+
|
| 42 |
+
def test_default_configuration(self):
|
| 43 |
+
"""Test default configuration values."""
|
| 44 |
+
# Test feature toggles
|
| 45 |
+
self.assertTrue(self.config.enabled)
|
| 46 |
+
self.assertTrue(self.config.use_color_coding)
|
| 47 |
+
self.assertTrue(self.config.use_icons)
|
| 48 |
+
self.assertTrue(self.config.use_visual_separators)
|
| 49 |
+
self.assertTrue(self.config.use_enhanced_styling)
|
| 50 |
+
|
| 51 |
+
# Test default icons
|
| 52 |
+
self.assertEqual(self.config.ai_analysis.icon, "🤖")
|
| 53 |
+
self.assertEqual(self.config.patient_message.icon, "💬")
|
| 54 |
+
self.assertEqual(self.config.provider_summary.icon, "📋")
|
| 55 |
+
|
| 56 |
+
# Test default colors
|
| 57 |
+
self.assertEqual(self.config.classification_colors.red, "#ff4444")
|
| 58 |
+
self.assertEqual(self.config.classification_colors.yellow, "#ffaa00")
|
| 59 |
+
self.assertEqual(self.config.classification_colors.green, "#44aa44")
|
| 60 |
+
|
| 61 |
+
def test_get_classification_color(self):
|
| 62 |
+
"""Test classification color retrieval."""
|
| 63 |
+
self.assertEqual(self.config.get_classification_color("RED"), "#ff4444")
|
| 64 |
+
self.assertEqual(self.config.get_classification_color("red"), "#ff4444")
|
| 65 |
+
self.assertEqual(self.config.get_classification_color("YELLOW"), "#ffaa00")
|
| 66 |
+
self.assertEqual(self.config.get_classification_color("GREEN"), "#44aa44")
|
| 67 |
+
self.assertEqual(self.config.get_classification_color("UNKNOWN"), "#666666")
|
| 68 |
+
|
| 69 |
+
def test_get_section_config(self):
|
| 70 |
+
"""Test section configuration retrieval."""
|
| 71 |
+
ai_config = self.config.get_section_config("ai_analysis")
|
| 72 |
+
self.assertIsInstance(ai_config, SectionStylingConfig)
|
| 73 |
+
self.assertEqual(ai_config.icon, "🤖")
|
| 74 |
+
|
| 75 |
+
patient_config = self.config.get_section_config("patient_message")
|
| 76 |
+
self.assertEqual(patient_config.icon, "💬")
|
| 77 |
+
|
| 78 |
+
provider_config = self.config.get_section_config("provider_summary")
|
| 79 |
+
self.assertEqual(provider_config.icon, "📋")
|
| 80 |
+
|
| 81 |
+
# Test unknown section returns default
|
| 82 |
+
unknown_config = self.config.get_section_config("unknown")
|
| 83 |
+
self.assertEqual(unknown_config, self.config.ai_analysis)
|
| 84 |
+
|
| 85 |
+
def test_high_contrast_mode(self):
|
| 86 |
+
"""Test high contrast mode application."""
|
| 87 |
+
config = EnhancedDisplayConfig(high_contrast_mode=True)
|
| 88 |
+
|
| 89 |
+
# Check that high contrast colors are applied
|
| 90 |
+
self.assertEqual(config.classification_colors.red, "#cc0000")
|
| 91 |
+
self.assertEqual(config.classification_colors.yellow, "#cc8800")
|
| 92 |
+
self.assertEqual(config.classification_colors.green, "#006600")
|
| 93 |
+
|
| 94 |
+
# Check section colors are updated
|
| 95 |
+
self.assertEqual(config.ai_analysis.border_color, "#000000")
|
| 96 |
+
self.assertEqual(config.ai_analysis.background_color, "#ffffff")
|
| 97 |
+
|
| 98 |
+
def test_css_variables_generation(self):
|
| 99 |
+
"""Test CSS variables generation."""
|
| 100 |
+
css_vars = self.config.generate_css_variables()
|
| 101 |
+
|
| 102 |
+
# Check that CSS variables are generated
|
| 103 |
+
self.assertIn(":root {", css_vars)
|
| 104 |
+
self.assertIn("--enhanced-font-family:", css_vars)
|
| 105 |
+
self.assertIn("--classification-red:", css_vars)
|
| 106 |
+
self.assertIn("--ai-analysis-icon:", css_vars)
|
| 107 |
+
self.assertIn("}", css_vars)
|
| 108 |
+
|
| 109 |
+
def test_base_css_generation(self):
|
| 110 |
+
"""Test base CSS generation."""
|
| 111 |
+
base_css = self.config.generate_base_css()
|
| 112 |
+
|
| 113 |
+
# Check that CSS classes are generated
|
| 114 |
+
self.assertIn(".enhanced-section", base_css)
|
| 115 |
+
self.assertIn(".enhanced-section-header", base_css)
|
| 116 |
+
self.assertIn(".ai-analysis-section", base_css)
|
| 117 |
+
self.assertIn(".patient-message-section", base_css)
|
| 118 |
+
self.assertIn(".provider-summary-section", base_css)
|
| 119 |
+
|
| 120 |
+
# Check responsive design
|
| 121 |
+
self.assertIn("@media", base_css)
|
| 122 |
+
|
| 123 |
+
def test_to_dict_conversion(self):
|
| 124 |
+
"""Test conversion to dictionary."""
|
| 125 |
+
config_dict = self.config.to_dict()
|
| 126 |
+
|
| 127 |
+
self.assertIsInstance(config_dict, dict)
|
| 128 |
+
self.assertEqual(config_dict['enabled'], True)
|
| 129 |
+
self.assertEqual(config_dict['use_color_coding'], True)
|
| 130 |
+
self.assertIn('ai_analysis', config_dict)
|
| 131 |
+
self.assertIn('classification_colors', config_dict)
|
| 132 |
+
|
| 133 |
+
def test_to_json_conversion(self):
|
| 134 |
+
"""Test conversion to JSON."""
|
| 135 |
+
config_json = self.config.to_json()
|
| 136 |
+
|
| 137 |
+
# Should be valid JSON
|
| 138 |
+
parsed = json.loads(config_json)
|
| 139 |
+
self.assertIsInstance(parsed, dict)
|
| 140 |
+
self.assertEqual(parsed['enabled'], True)
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
class TestEnhancedDisplayConfigManager(unittest.TestCase):
|
| 144 |
+
"""Test cases for EnhancedDisplayConfigManager class."""
|
| 145 |
+
|
| 146 |
+
def setUp(self):
|
| 147 |
+
"""Set up test fixtures."""
|
| 148 |
+
# Create temporary config file
|
| 149 |
+
self.temp_dir = tempfile.mkdtemp()
|
| 150 |
+
self.config_file = os.path.join(self.temp_dir, "test_config.json")
|
| 151 |
+
self.config_manager = EnhancedDisplayConfigManager(self.config_file)
|
| 152 |
+
|
| 153 |
+
def tearDown(self):
|
| 154 |
+
"""Clean up test fixtures."""
|
| 155 |
+
# Clean up temporary files
|
| 156 |
+
if os.path.exists(self.config_file):
|
| 157 |
+
os.remove(self.config_file)
|
| 158 |
+
os.rmdir(self.temp_dir)
|
| 159 |
+
|
| 160 |
+
def test_load_default_config(self):
|
| 161 |
+
"""Test loading default configuration when no file exists."""
|
| 162 |
+
config = self.config_manager.load_config()
|
| 163 |
+
|
| 164 |
+
self.assertIsInstance(config, EnhancedDisplayConfig)
|
| 165 |
+
self.assertTrue(config.enabled)
|
| 166 |
+
self.assertTrue(os.path.exists(self.config_file))
|
| 167 |
+
|
| 168 |
+
def test_save_and_load_config(self):
|
| 169 |
+
"""Test saving and loading configuration."""
|
| 170 |
+
# Load and modify config
|
| 171 |
+
config = self.config_manager.load_config()
|
| 172 |
+
config.enabled = False
|
| 173 |
+
config.use_icons = False
|
| 174 |
+
|
| 175 |
+
# Save config
|
| 176 |
+
success = self.config_manager.save_config()
|
| 177 |
+
self.assertTrue(success)
|
| 178 |
+
|
| 179 |
+
# Create new manager and load
|
| 180 |
+
new_manager = EnhancedDisplayConfigManager(self.config_file)
|
| 181 |
+
loaded_config = new_manager.load_config()
|
| 182 |
+
|
| 183 |
+
self.assertFalse(loaded_config.enabled)
|
| 184 |
+
self.assertFalse(loaded_config.use_icons)
|
| 185 |
+
|
| 186 |
+
def test_update_config(self):
|
| 187 |
+
"""Test configuration updates."""
|
| 188 |
+
success = self.config_manager.update_config(
|
| 189 |
+
enabled=False,
|
| 190 |
+
use_color_coding=False,
|
| 191 |
+
use_icons=False
|
| 192 |
+
)
|
| 193 |
+
self.assertTrue(success)
|
| 194 |
+
|
| 195 |
+
config = self.config_manager.load_config()
|
| 196 |
+
self.assertFalse(config.enabled)
|
| 197 |
+
self.assertFalse(config.use_color_coding)
|
| 198 |
+
self.assertFalse(config.use_icons)
|
| 199 |
+
|
| 200 |
+
def test_enable_disable_features(self):
|
| 201 |
+
"""Test feature enable/disable functionality."""
|
| 202 |
+
# Disable color coding
|
| 203 |
+
success = self.config_manager.disable_feature('color_coding')
|
| 204 |
+
self.assertTrue(success)
|
| 205 |
+
|
| 206 |
+
config = self.config_manager.load_config()
|
| 207 |
+
self.assertFalse(config.use_color_coding)
|
| 208 |
+
|
| 209 |
+
# Enable color coding
|
| 210 |
+
success = self.config_manager.enable_feature('color_coding')
|
| 211 |
+
self.assertTrue(success)
|
| 212 |
+
|
| 213 |
+
config = self.config_manager.load_config()
|
| 214 |
+
self.assertTrue(config.use_color_coding)
|
| 215 |
+
|
| 216 |
+
def test_reset_to_defaults(self):
|
| 217 |
+
"""Test resetting configuration to defaults."""
|
| 218 |
+
# Modify config
|
| 219 |
+
self.config_manager.update_config(enabled=False, use_icons=False)
|
| 220 |
+
|
| 221 |
+
# Reset to defaults
|
| 222 |
+
success = self.config_manager.reset_to_defaults()
|
| 223 |
+
self.assertTrue(success)
|
| 224 |
+
|
| 225 |
+
config = self.config_manager.load_config()
|
| 226 |
+
self.assertTrue(config.enabled)
|
| 227 |
+
self.assertTrue(config.use_icons)
|
| 228 |
+
|
| 229 |
+
def test_validate_config(self):
|
| 230 |
+
"""Test configuration validation."""
|
| 231 |
+
# Valid configuration should have no issues
|
| 232 |
+
issues = self.config_manager.validate_config()
|
| 233 |
+
self.assertEqual(len(issues), 0)
|
| 234 |
+
|
| 235 |
+
# Test with invalid color (this would need to be implemented)
|
| 236 |
+
# For now, just test that validation runs without error
|
| 237 |
+
self.assertIsInstance(issues, list)
|
| 238 |
+
|
| 239 |
+
def test_config_file_error_handling(self):
|
| 240 |
+
"""Test error handling for config file operations."""
|
| 241 |
+
# Test with invalid config file path
|
| 242 |
+
invalid_manager = EnhancedDisplayConfigManager("/invalid/path/config.json")
|
| 243 |
+
|
| 244 |
+
# Should still return default config
|
| 245 |
+
config = invalid_manager.load_config()
|
| 246 |
+
self.assertIsInstance(config, EnhancedDisplayConfig)
|
| 247 |
+
self.assertTrue(config.enabled)
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
class TestPresetConfigurations(unittest.TestCase):
|
| 251 |
+
"""Test cases for preset configuration functions."""
|
| 252 |
+
|
| 253 |
+
def test_high_contrast_config(self):
|
| 254 |
+
"""Test high contrast configuration preset."""
|
| 255 |
+
config = create_high_contrast_config()
|
| 256 |
+
|
| 257 |
+
self.assertTrue(config.high_contrast_mode)
|
| 258 |
+
self.assertEqual(config.classification_colors.red, "#cc0000")
|
| 259 |
+
self.assertEqual(config.ai_analysis.border_color, "#000000")
|
| 260 |
+
|
| 261 |
+
def test_minimal_config(self):
|
| 262 |
+
"""Test minimal configuration preset."""
|
| 263 |
+
config = create_minimal_config()
|
| 264 |
+
|
| 265 |
+
self.assertFalse(config.use_icons)
|
| 266 |
+
self.assertFalse(config.use_visual_separators)
|
| 267 |
+
self.assertFalse(config.enable_animations)
|
| 268 |
+
|
| 269 |
+
def test_mobile_optimized_config(self):
|
| 270 |
+
"""Test mobile optimized configuration preset."""
|
| 271 |
+
config = create_mobile_optimized_config()
|
| 272 |
+
|
| 273 |
+
self.assertEqual(config.responsive_breakpoint, "480px")
|
| 274 |
+
self.assertEqual(config.ai_analysis.padding, "12px")
|
| 275 |
+
self.assertFalse(config.enable_animations)
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
class TestSectionStylingConfig(unittest.TestCase):
|
| 279 |
+
"""Test cases for SectionStylingConfig class."""
|
| 280 |
+
|
| 281 |
+
def test_default_values(self):
|
| 282 |
+
"""Test default section styling values."""
|
| 283 |
+
config = SectionStylingConfig(
|
| 284 |
+
icon="🔧",
|
| 285 |
+
border_color="#000000",
|
| 286 |
+
background_color="#ffffff",
|
| 287 |
+
header_color="#000000"
|
| 288 |
+
)
|
| 289 |
+
|
| 290 |
+
self.assertEqual(config.icon, "🔧")
|
| 291 |
+
self.assertEqual(config.border_width, "2px")
|
| 292 |
+
self.assertEqual(config.border_radius, "8px")
|
| 293 |
+
self.assertEqual(config.padding, "15px")
|
| 294 |
+
self.assertEqual(config.margin, "10px 0")
|
| 295 |
+
|
| 296 |
+
|
| 297 |
+
class TestClassificationColorConfig(unittest.TestCase):
|
| 298 |
+
"""Test cases for ClassificationColorConfig class."""
|
| 299 |
+
|
| 300 |
+
def test_default_colors(self):
|
| 301 |
+
"""Test default classification colors."""
|
| 302 |
+
config = ClassificationColorConfig()
|
| 303 |
+
|
| 304 |
+
self.assertEqual(config.red, "#ff4444")
|
| 305 |
+
self.assertEqual(config.yellow, "#ffaa00")
|
| 306 |
+
self.assertEqual(config.green, "#44aa44")
|
| 307 |
+
self.assertEqual(config.unknown, "#666666")
|
| 308 |
+
|
| 309 |
+
|
| 310 |
+
class TestVisualSeparatorConfig(unittest.TestCase):
|
| 311 |
+
"""Test cases for VisualSeparatorConfig class."""
|
| 312 |
+
|
| 313 |
+
def test_default_separators(self):
|
| 314 |
+
"""Test default visual separator values."""
|
| 315 |
+
config = VisualSeparatorConfig()
|
| 316 |
+
|
| 317 |
+
self.assertEqual(config.section_separator, "---")
|
| 318 |
+
self.assertEqual(config.content_divider, "1px solid #ddd")
|
| 319 |
+
self.assertEqual(config.major_break_symbol, "● ● ●")
|
| 320 |
+
self.assertEqual(config.separator_color, "#e0e0e0")
|
| 321 |
+
self.assertEqual(config.separator_width, "80%")
|
| 322 |
+
|
| 323 |
+
|
| 324 |
+
if __name__ == '__main__':
|
| 325 |
+
unittest.main()
|
|
@@ -0,0 +1,472 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# test_enhanced_results_display.py
|
| 2 |
+
"""
|
| 3 |
+
Unit tests for Enhanced Results Display Manager.
|
| 4 |
+
|
| 5 |
+
Tests the core functionality of the enhanced results display system
|
| 6 |
+
including section formatting, visual separation, and provider summary formatting.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import pytest
|
| 10 |
+
from unittest.mock import Mock
|
| 11 |
+
from datetime import datetime
|
| 12 |
+
|
| 13 |
+
from src.interface.enhanced_results_display_manager import (
|
| 14 |
+
EnhancedResultsDisplayManager,
|
| 15 |
+
EnhancedDisplayConfig,
|
| 16 |
+
ContentSection,
|
| 17 |
+
SectionType
|
| 18 |
+
)
|
| 19 |
+
from src.interface.visual_separation_manager import (
|
| 20 |
+
VisualSeparationManager,
|
| 21 |
+
ContentType,
|
| 22 |
+
VisualStyle
|
| 23 |
+
)
|
| 24 |
+
from src.interface.provider_summary_formatter import (
|
| 25 |
+
ProviderSummaryFormatter,
|
| 26 |
+
PatientData,
|
| 27 |
+
ClassificationData
|
| 28 |
+
)
|
| 29 |
+
from src.core.provider_summary_generator import ProviderSummary
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
class TestEnhancedResultsDisplayManager:
|
| 33 |
+
"""Test cases for EnhancedResultsDisplayManager."""
|
| 34 |
+
|
| 35 |
+
def setup_method(self):
|
| 36 |
+
"""Set up test fixtures."""
|
| 37 |
+
self.display_manager = EnhancedResultsDisplayManager()
|
| 38 |
+
self.config = EnhancedDisplayConfig()
|
| 39 |
+
|
| 40 |
+
def test_initialization_with_default_config(self):
|
| 41 |
+
"""Test initialization with default configuration."""
|
| 42 |
+
manager = EnhancedResultsDisplayManager()
|
| 43 |
+
assert manager.config is not None
|
| 44 |
+
assert manager.config.ai_analysis_icon == "🤖"
|
| 45 |
+
assert manager.config.patient_message_icon == "💬"
|
| 46 |
+
assert manager.config.provider_summary_icon == "📋"
|
| 47 |
+
|
| 48 |
+
def test_initialization_with_custom_config(self):
|
| 49 |
+
"""Test initialization with custom configuration."""
|
| 50 |
+
custom_config = EnhancedDisplayConfig(
|
| 51 |
+
ai_analysis_icon="🔍",
|
| 52 |
+
patient_message_icon="📝",
|
| 53 |
+
use_color_coding=False
|
| 54 |
+
)
|
| 55 |
+
manager = EnhancedResultsDisplayManager(custom_config)
|
| 56 |
+
assert manager.config.ai_analysis_icon == "🔍"
|
| 57 |
+
assert manager.config.patient_message_icon == "📝"
|
| 58 |
+
assert manager.config.use_color_coding is False
|
| 59 |
+
|
| 60 |
+
def test_format_ai_analysis_section_basic(self):
|
| 61 |
+
"""Test basic AI analysis section formatting."""
|
| 62 |
+
result = self.display_manager.format_ai_analysis_section(
|
| 63 |
+
classification="RED",
|
| 64 |
+
indicators=["Loss of meaning", "Spiritual distress"],
|
| 65 |
+
reasoning="Patient expressed existential concerns"
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
assert "AI Analysis - RED FLAG" in result
|
| 69 |
+
assert "Loss of meaning" in result
|
| 70 |
+
assert "Spiritual distress" in result
|
| 71 |
+
assert "Patient expressed existential concerns" in result
|
| 72 |
+
assert self.config.ai_analysis_icon in result
|
| 73 |
+
|
| 74 |
+
def test_format_ai_analysis_section_with_confidence(self):
|
| 75 |
+
"""Test AI analysis section formatting with confidence score."""
|
| 76 |
+
result = self.display_manager.format_ai_analysis_section(
|
| 77 |
+
classification="YELLOW",
|
| 78 |
+
indicators=["Mild distress"],
|
| 79 |
+
reasoning="Unclear indicators",
|
| 80 |
+
confidence=0.75
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
assert "AI Analysis - YELLOW FLAG" in result
|
| 84 |
+
assert "Confidence: 75%" in result
|
| 85 |
+
assert "Mild distress" in result
|
| 86 |
+
|
| 87 |
+
def test_format_ai_analysis_section_no_indicators(self):
|
| 88 |
+
"""Test AI analysis section formatting with no indicators."""
|
| 89 |
+
result = self.display_manager.format_ai_analysis_section(
|
| 90 |
+
classification="GREEN",
|
| 91 |
+
indicators=[],
|
| 92 |
+
reasoning="Normal conversation"
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
assert "AI Analysis - GREEN FLAG" in result
|
| 96 |
+
assert "Normal conversation" in result
|
| 97 |
+
# Should not contain indicators section when empty
|
| 98 |
+
assert "Indicators:" not in result
|
| 99 |
+
|
| 100 |
+
def test_format_patient_message_section(self):
|
| 101 |
+
"""Test patient message section formatting."""
|
| 102 |
+
message = "I'm feeling lost and don't know what to do."
|
| 103 |
+
result = self.display_manager.format_patient_message_section(message)
|
| 104 |
+
|
| 105 |
+
assert "Patient Message" in result
|
| 106 |
+
# Check for HTML-escaped version of the message
|
| 107 |
+
assert "I'm feeling lost and don't know what to do." in result
|
| 108 |
+
assert self.config.patient_message_icon in result
|
| 109 |
+
|
| 110 |
+
def test_format_patient_message_section_html_escaping(self):
|
| 111 |
+
"""Test patient message section with HTML characters."""
|
| 112 |
+
message = "I feel <confused> & don't know what's \"right\""
|
| 113 |
+
result = self.display_manager.format_patient_message_section(message)
|
| 114 |
+
|
| 115 |
+
assert "<confused>" in result
|
| 116 |
+
assert "&" in result
|
| 117 |
+
assert ""right"" in result
|
| 118 |
+
|
| 119 |
+
def test_format_provider_summary_section(self):
|
| 120 |
+
"""Test provider summary section formatting."""
|
| 121 |
+
# Create mock provider summary
|
| 122 |
+
summary = Mock(spec=ProviderSummary)
|
| 123 |
+
summary.patient_name = "John Doe"
|
| 124 |
+
summary.patient_phone = "(555) 123-4567"
|
| 125 |
+
summary.urgency_level = "URGENT"
|
| 126 |
+
summary.follow_up_timeline = "Within 24 hours"
|
| 127 |
+
summary.situation_description = "Patient experiencing spiritual crisis"
|
| 128 |
+
summary.indicators = ["Loss of faith", "Existential questioning"]
|
| 129 |
+
|
| 130 |
+
result = self.display_manager.format_provider_summary_section(summary)
|
| 131 |
+
|
| 132 |
+
assert "Provider Summary" in result
|
| 133 |
+
assert "John Doe" in result
|
| 134 |
+
assert "(555) 123-4567" in result
|
| 135 |
+
assert "URGENT" in result
|
| 136 |
+
assert "Loss of faith" in result
|
| 137 |
+
assert "Existential questioning" in result
|
| 138 |
+
|
| 139 |
+
def test_create_visual_separators(self):
|
| 140 |
+
"""Test visual separator creation."""
|
| 141 |
+
separators = self.display_manager.create_visual_separators()
|
| 142 |
+
|
| 143 |
+
assert "section_break" in separators
|
| 144 |
+
assert "content_divider" in separators
|
| 145 |
+
assert "major_break" in separators
|
| 146 |
+
|
| 147 |
+
# Check that separators contain HTML
|
| 148 |
+
for separator in separators.values():
|
| 149 |
+
assert "<div" in separator
|
| 150 |
+
|
| 151 |
+
def test_apply_section_styling(self):
|
| 152 |
+
"""Test section styling application."""
|
| 153 |
+
content = "Test content"
|
| 154 |
+
|
| 155 |
+
# Test AI analysis styling
|
| 156 |
+
result = self.display_manager.apply_section_styling(content, SectionType.AI_ANALYSIS)
|
| 157 |
+
assert content in result
|
| 158 |
+
assert "background-color: #f8f9fa" in result
|
| 159 |
+
|
| 160 |
+
# Test patient message styling
|
| 161 |
+
result = self.display_manager.apply_section_styling(content, SectionType.PATIENT_MESSAGE)
|
| 162 |
+
assert content in result
|
| 163 |
+
assert "background-color: #f0f7ff" in result
|
| 164 |
+
|
| 165 |
+
def test_format_combined_results_all_sections(self):
|
| 166 |
+
"""Test combined results formatting with all sections."""
|
| 167 |
+
ai_analysis = {
|
| 168 |
+
"classification": "RED",
|
| 169 |
+
"indicators": ["Spiritual crisis"],
|
| 170 |
+
"reasoning": "Patient in distress",
|
| 171 |
+
"confidence": 0.9
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
patient_message = "I don't see the point anymore"
|
| 175 |
+
|
| 176 |
+
provider_summary = Mock(spec=ProviderSummary)
|
| 177 |
+
provider_summary.patient_name = "Jane Smith"
|
| 178 |
+
provider_summary.patient_phone = "(555) 987-6543"
|
| 179 |
+
provider_summary.urgency_level = "IMMEDIATE"
|
| 180 |
+
provider_summary.follow_up_timeline = "Immediately"
|
| 181 |
+
provider_summary.situation_description = "Crisis situation"
|
| 182 |
+
provider_summary.indicators = ["Hopelessness"]
|
| 183 |
+
|
| 184 |
+
result = self.display_manager.format_combined_results(
|
| 185 |
+
ai_analysis=ai_analysis,
|
| 186 |
+
patient_message=patient_message,
|
| 187 |
+
provider_summary=provider_summary
|
| 188 |
+
)
|
| 189 |
+
|
| 190 |
+
assert "AI Analysis - RED FLAG" in result
|
| 191 |
+
assert "Patient Message" in result
|
| 192 |
+
assert "Provider Summary" in result
|
| 193 |
+
assert "Jane Smith" in result
|
| 194 |
+
# Check for HTML-escaped version of the patient message
|
| 195 |
+
assert "I don't see the point anymore" in result
|
| 196 |
+
|
| 197 |
+
def test_format_combined_results_empty(self):
|
| 198 |
+
"""Test combined results formatting with no content."""
|
| 199 |
+
result = self.display_manager.format_combined_results()
|
| 200 |
+
|
| 201 |
+
assert "No content to display" in result
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
class TestVisualSeparationManager:
|
| 205 |
+
"""Test cases for VisualSeparationManager."""
|
| 206 |
+
|
| 207 |
+
def setup_method(self):
|
| 208 |
+
"""Set up test fixtures."""
|
| 209 |
+
self.visual_manager = VisualSeparationManager()
|
| 210 |
+
|
| 211 |
+
def test_initialization(self):
|
| 212 |
+
"""Test visual separation manager initialization."""
|
| 213 |
+
assert self.visual_manager.styles is not None
|
| 214 |
+
assert ContentType.AI_ANALYSIS in self.visual_manager.styles
|
| 215 |
+
assert ContentType.PATIENT_MESSAGE in self.visual_manager.styles
|
| 216 |
+
assert ContentType.PROVIDER_SUMMARY in self.visual_manager.styles
|
| 217 |
+
|
| 218 |
+
def test_create_ai_analysis_styling(self):
|
| 219 |
+
"""Test AI analysis styling creation."""
|
| 220 |
+
styling = self.visual_manager.create_ai_analysis_styling()
|
| 221 |
+
|
| 222 |
+
assert "container" in styling
|
| 223 |
+
assert "header" in styling
|
| 224 |
+
assert "icon" in styling
|
| 225 |
+
assert "content" in styling
|
| 226 |
+
|
| 227 |
+
# Check that styling contains CSS properties
|
| 228 |
+
assert "border:" in styling["container"]
|
| 229 |
+
assert "background-color:" in styling["container"]
|
| 230 |
+
|
| 231 |
+
def test_create_patient_message_styling(self):
|
| 232 |
+
"""Test patient message styling creation."""
|
| 233 |
+
styling = self.visual_manager.create_patient_message_styling()
|
| 234 |
+
|
| 235 |
+
assert "container" in styling
|
| 236 |
+
assert "header" in styling
|
| 237 |
+
assert "message_box" in styling
|
| 238 |
+
|
| 239 |
+
# Check for patient message specific colors
|
| 240 |
+
assert "#4a90e2" in styling["container"]
|
| 241 |
+
|
| 242 |
+
def test_create_provider_summary_styling(self):
|
| 243 |
+
"""Test provider summary styling creation."""
|
| 244 |
+
styling = self.visual_manager.create_provider_summary_styling()
|
| 245 |
+
|
| 246 |
+
assert "container" in styling
|
| 247 |
+
assert "header" in styling
|
| 248 |
+
assert "info_box" in styling
|
| 249 |
+
assert "urgency_box" in styling
|
| 250 |
+
|
| 251 |
+
def test_generate_section_separators(self):
|
| 252 |
+
"""Test section separator generation."""
|
| 253 |
+
separators = self.visual_manager.generate_section_separators()
|
| 254 |
+
|
| 255 |
+
assert "light" in separators
|
| 256 |
+
assert "medium" in separators
|
| 257 |
+
assert "heavy" in separators
|
| 258 |
+
assert "section_break" in separators
|
| 259 |
+
|
| 260 |
+
# Check that all separators are HTML
|
| 261 |
+
for separator in separators.values():
|
| 262 |
+
assert "<div" in separator
|
| 263 |
+
|
| 264 |
+
def test_get_classification_styling(self):
|
| 265 |
+
"""Test classification-specific styling."""
|
| 266 |
+
red_styling = self.visual_manager.get_classification_styling("RED")
|
| 267 |
+
yellow_styling = self.visual_manager.get_classification_styling("YELLOW")
|
| 268 |
+
green_styling = self.visual_manager.get_classification_styling("GREEN")
|
| 269 |
+
|
| 270 |
+
assert "#dc3545" in red_styling["badge"]
|
| 271 |
+
assert "#ffc107" in yellow_styling["badge"]
|
| 272 |
+
assert "#28a745" in green_styling["badge"]
|
| 273 |
+
|
| 274 |
+
def test_get_urgency_styling(self):
|
| 275 |
+
"""Test urgency-specific styling."""
|
| 276 |
+
immediate_styling = self.visual_manager.get_urgency_styling("IMMEDIATE")
|
| 277 |
+
urgent_styling = self.visual_manager.get_urgency_styling("URGENT")
|
| 278 |
+
standard_styling = self.visual_manager.get_urgency_styling("STANDARD")
|
| 279 |
+
|
| 280 |
+
assert "#dc3545" in immediate_styling["badge"]
|
| 281 |
+
assert "#fd7e14" in urgent_styling["badge"]
|
| 282 |
+
assert "#28a745" in standard_styling["badge"]
|
| 283 |
+
|
| 284 |
+
def test_apply_consistent_formatting_multiple_sections(self):
|
| 285 |
+
"""Test consistent formatting with multiple sections."""
|
| 286 |
+
sections = [
|
| 287 |
+
{"type": "ai_analysis", "content": "AI analysis content"},
|
| 288 |
+
{"type": "patient_message", "content": "Patient message content"},
|
| 289 |
+
{"type": "provider_summary", "content": "Provider summary content"}
|
| 290 |
+
]
|
| 291 |
+
|
| 292 |
+
result = self.visual_manager.apply_consistent_formatting(sections)
|
| 293 |
+
|
| 294 |
+
assert "AI analysis content" in result
|
| 295 |
+
assert "Patient message content" in result
|
| 296 |
+
assert "Provider summary content" in result
|
| 297 |
+
# Should contain section separators between sections
|
| 298 |
+
assert "---" in result
|
| 299 |
+
|
| 300 |
+
def test_apply_consistent_formatting_empty_sections(self):
|
| 301 |
+
"""Test consistent formatting with empty sections list."""
|
| 302 |
+
result = self.visual_manager.apply_consistent_formatting([])
|
| 303 |
+
|
| 304 |
+
assert "No content to display" in result
|
| 305 |
+
|
| 306 |
+
def test_apply_consistent_formatting_single_section(self):
|
| 307 |
+
"""Test consistent formatting with single section."""
|
| 308 |
+
sections = [
|
| 309 |
+
{"type": "ai_analysis", "content": "Single section content"}
|
| 310 |
+
]
|
| 311 |
+
|
| 312 |
+
result = self.visual_manager.apply_consistent_formatting(sections)
|
| 313 |
+
|
| 314 |
+
assert "Single section content" in result
|
| 315 |
+
# Should not contain separators for single section
|
| 316 |
+
assert result.count("---") == 0
|
| 317 |
+
|
| 318 |
+
def test_create_icon_styling(self):
|
| 319 |
+
"""Test icon styling creation for different content types."""
|
| 320 |
+
ai_styling = self.visual_manager.create_icon_styling(ContentType.AI_ANALYSIS)
|
| 321 |
+
patient_styling = self.visual_manager.create_icon_styling(ContentType.PATIENT_MESSAGE)
|
| 322 |
+
provider_styling = self.visual_manager.create_icon_styling(ContentType.PROVIDER_SUMMARY)
|
| 323 |
+
|
| 324 |
+
# All should contain base styling
|
| 325 |
+
assert "font-size: 1.2em" in ai_styling
|
| 326 |
+
assert "margin-right: 8px" in patient_styling
|
| 327 |
+
assert "color:" in provider_styling
|
| 328 |
+
|
| 329 |
+
|
| 330 |
+
class TestProviderSummaryFormatter:
|
| 331 |
+
"""Test cases for ProviderSummaryFormatter."""
|
| 332 |
+
|
| 333 |
+
def setup_method(self):
|
| 334 |
+
"""Set up test fixtures."""
|
| 335 |
+
self.formatter = ProviderSummaryFormatter()
|
| 336 |
+
|
| 337 |
+
self.sample_patient_data = PatientData(
|
| 338 |
+
name="John Doe",
|
| 339 |
+
age=45,
|
| 340 |
+
gender="male",
|
| 341 |
+
phone="(555) 123-4567",
|
| 342 |
+
medical_history=["Diabetes", "Hypertension"],
|
| 343 |
+
expressed_concerns=["Loss of faith", "Feeling hopeless"],
|
| 344 |
+
patient_input="I don't know what to believe anymore"
|
| 345 |
+
)
|
| 346 |
+
|
| 347 |
+
self.sample_classification_data = ClassificationData(
|
| 348 |
+
classification="RED",
|
| 349 |
+
spiritual_concern_type="existential crisis",
|
| 350 |
+
consent_given=True
|
| 351 |
+
)
|
| 352 |
+
|
| 353 |
+
def test_build_demographic_section(self):
|
| 354 |
+
"""Test demographic section building."""
|
| 355 |
+
result = self.formatter.build_demographic_section(self.sample_patient_data)
|
| 356 |
+
|
| 357 |
+
assert "John Doe is a 45-year-old male" == result
|
| 358 |
+
|
| 359 |
+
def test_build_demographic_section_no_gender(self):
|
| 360 |
+
"""Test demographic section with no gender specified."""
|
| 361 |
+
patient_data = PatientData(
|
| 362 |
+
name="Jane Smith",
|
| 363 |
+
age=30,
|
| 364 |
+
gender="",
|
| 365 |
+
phone="",
|
| 366 |
+
medical_history=[],
|
| 367 |
+
expressed_concerns=[],
|
| 368 |
+
patient_input=""
|
| 369 |
+
)
|
| 370 |
+
|
| 371 |
+
result = self.formatter.build_demographic_section(patient_data)
|
| 372 |
+
assert "Jane Smith is a 30-year-old individual" == result
|
| 373 |
+
|
| 374 |
+
def test_build_medical_history_section_multiple(self):
|
| 375 |
+
"""Test medical history section with multiple conditions."""
|
| 376 |
+
result = self.formatter.build_medical_history_section(
|
| 377 |
+
["Diabetes", "Hypertension", "Arthritis"]
|
| 378 |
+
)
|
| 379 |
+
|
| 380 |
+
assert "with clinical history of Diabetes, Hypertension, and Arthritis" == result
|
| 381 |
+
|
| 382 |
+
def test_build_medical_history_section_single(self):
|
| 383 |
+
"""Test medical history section with single condition."""
|
| 384 |
+
result = self.formatter.build_medical_history_section(["Diabetes"])
|
| 385 |
+
|
| 386 |
+
assert "with clinical history of Diabetes" == result
|
| 387 |
+
|
| 388 |
+
def test_build_medical_history_section_empty(self):
|
| 389 |
+
"""Test medical history section with no conditions."""
|
| 390 |
+
result = self.formatter.build_medical_history_section([])
|
| 391 |
+
|
| 392 |
+
assert "with no significant medical history documented" == result
|
| 393 |
+
|
| 394 |
+
def test_build_spiritual_concerns_section(self):
|
| 395 |
+
"""Test spiritual concerns section building."""
|
| 396 |
+
result = self.formatter.build_spiritual_concerns_section(
|
| 397 |
+
["Loss of faith", "Feeling hopeless"],
|
| 398 |
+
"existential crisis",
|
| 399 |
+
"RED"
|
| 400 |
+
)
|
| 401 |
+
|
| 402 |
+
expected = ("The patient expressed Loss of faith and Feeling hopeless, "
|
| 403 |
+
"which may indicate existential crisis, resulting in generation of a RED FLAG")
|
| 404 |
+
assert expected == result
|
| 405 |
+
|
| 406 |
+
def test_build_contact_section_with_consent(self):
|
| 407 |
+
"""Test contact section with consent given."""
|
| 408 |
+
result = self.formatter.build_contact_section("(555) 123-4567", True)
|
| 409 |
+
|
| 410 |
+
expected = ("The patient has given consent to be contacted by the spiritual care team. "
|
| 411 |
+
"The preferred contact number is (555) 123-4567")
|
| 412 |
+
assert expected == result
|
| 413 |
+
|
| 414 |
+
def test_build_contact_section_no_consent(self):
|
| 415 |
+
"""Test contact section without consent."""
|
| 416 |
+
result = self.formatter.build_contact_section("(555) 123-4567", False)
|
| 417 |
+
|
| 418 |
+
expected = ("The patient has not yet provided consent for spiritual care contact. "
|
| 419 |
+
"The preferred contact number is (555) 123-4567")
|
| 420 |
+
assert expected == result
|
| 421 |
+
|
| 422 |
+
def test_add_patient_quote_section(self):
|
| 423 |
+
"""Test patient quote section formatting."""
|
| 424 |
+
quote = "I don't know what to believe anymore"
|
| 425 |
+
result = self.formatter.add_patient_quote_section(quote)
|
| 426 |
+
|
| 427 |
+
expected = 'Patient reported: "I don\'t know what to believe anymore"'
|
| 428 |
+
assert expected == result
|
| 429 |
+
|
| 430 |
+
def test_add_patient_quote_section_empty(self):
|
| 431 |
+
"""Test patient quote section with empty quote."""
|
| 432 |
+
result = self.formatter.add_patient_quote_section("")
|
| 433 |
+
assert "" == result
|
| 434 |
+
|
| 435 |
+
def test_format_coherent_paragraph(self):
|
| 436 |
+
"""Test complete coherent paragraph formatting."""
|
| 437 |
+
result = self.formatter.format_coherent_paragraph(
|
| 438 |
+
self.sample_patient_data,
|
| 439 |
+
self.sample_classification_data
|
| 440 |
+
)
|
| 441 |
+
|
| 442 |
+
# Check that all required elements are present
|
| 443 |
+
assert "John Doe is a 45-year-old male" in result
|
| 444 |
+
assert "clinical history of Diabetes and Hypertension" in result
|
| 445 |
+
assert "Loss of faith and Feeling hopeless" in result
|
| 446 |
+
assert "existential crisis" in result
|
| 447 |
+
assert "RED FLAG" in result
|
| 448 |
+
assert "consent to be contacted" in result
|
| 449 |
+
assert "(555) 123-4567" in result
|
| 450 |
+
|
| 451 |
+
# Check that it's a single paragraph (no line breaks except at end)
|
| 452 |
+
lines = result.split('\n')
|
| 453 |
+
assert len([line for line in lines if line.strip()]) == 1
|
| 454 |
+
|
| 455 |
+
def test_format_enhanced_summary_with_quote(self):
|
| 456 |
+
"""Test enhanced summary formatting with patient quote."""
|
| 457 |
+
result = self.formatter.format_enhanced_summary(
|
| 458 |
+
self.sample_patient_data,
|
| 459 |
+
self.sample_classification_data
|
| 460 |
+
)
|
| 461 |
+
|
| 462 |
+
# Should contain both paragraph and quote
|
| 463 |
+
assert "John Doe is a 45-year-old male" in result
|
| 464 |
+
assert 'Patient reported: "I don\'t know what to believe anymore"' in result
|
| 465 |
+
|
| 466 |
+
# Should have paragraph and quote separated by double newline
|
| 467 |
+
parts = result.split('\n\n')
|
| 468 |
+
assert len(parts) == 2
|
| 469 |
+
|
| 470 |
+
|
| 471 |
+
if __name__ == "__main__":
|
| 472 |
+
pytest.main([__file__])
|
|
@@ -0,0 +1,296 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Unit tests for ImprovedClassificationPromptManager.
|
| 4 |
+
|
| 5 |
+
Tests the implementation of updated classification prompts with new red flag definitions
|
| 6 |
+
and explicit indicators based on medical professional feedback.
|
| 7 |
+
|
| 8 |
+
Requirements: 4.1, 4.2, 5.1, 5.2, 5.3, 5.4, 5.5, 5.6
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import pytest
|
| 12 |
+
from src.core.improved_classification_prompt_manager import (
|
| 13 |
+
ImprovedClassificationPromptManager,
|
| 14 |
+
ClassificationResult,
|
| 15 |
+
create_improved_classification_prompt_manager
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class TestImprovedClassificationPromptManager:
|
| 20 |
+
"""Test suite for ImprovedClassificationPromptManager."""
|
| 21 |
+
|
| 22 |
+
def setup_method(self):
|
| 23 |
+
"""Set up test fixtures."""
|
| 24 |
+
self.manager = create_improved_classification_prompt_manager()
|
| 25 |
+
|
| 26 |
+
def test_updated_red_flag_definition(self):
|
| 27 |
+
"""
|
| 28 |
+
Test updated red flag definition - Requirements 4.1, 4.2.
|
| 29 |
+
|
| 30 |
+
Verifies that the new definition is broader and includes active distress,
|
| 31 |
+
not just crisis situations.
|
| 32 |
+
"""
|
| 33 |
+
definition = self.manager.get_updated_red_flag_definition()
|
| 34 |
+
|
| 35 |
+
# Check that definition contains key phrases from requirements
|
| 36 |
+
assert "caused by or actively causing" in definition
|
| 37 |
+
assert "emotional or spiritual distress" in definition
|
| 38 |
+
|
| 39 |
+
# Ensure it's not empty
|
| 40 |
+
assert len(definition.strip()) > 0
|
| 41 |
+
|
| 42 |
+
def test_explicit_red_indicators_completeness(self):
|
| 43 |
+
"""
|
| 44 |
+
Test explicit red indicators - Requirements 5.2, 5.3, 5.4, 5.5, 5.6.
|
| 45 |
+
|
| 46 |
+
Verifies that all five required explicit indicators are present.
|
| 47 |
+
"""
|
| 48 |
+
indicators = self.manager.get_explicit_red_indicators()
|
| 49 |
+
|
| 50 |
+
# Check that all 5 required indicators are present
|
| 51 |
+
expected_indicators = [
|
| 52 |
+
"Complex grief",
|
| 53 |
+
"Loss of a loved one",
|
| 54 |
+
"Doubt about meaning of life",
|
| 55 |
+
"Doubt about meaning of suffering",
|
| 56 |
+
"Doubt about personal dignity"
|
| 57 |
+
]
|
| 58 |
+
|
| 59 |
+
assert len(indicators) == 5
|
| 60 |
+
for expected in expected_indicators:
|
| 61 |
+
assert expected in indicators
|
| 62 |
+
|
| 63 |
+
def test_yellow_flag_criteria(self):
|
| 64 |
+
"""
|
| 65 |
+
Test yellow flag criteria - Requirements 6.1, 6.2, 6.3, 6.4, 6.5.
|
| 66 |
+
|
| 67 |
+
Verifies that yellow flag criteria include appropriate situations
|
| 68 |
+
requiring further investigation.
|
| 69 |
+
"""
|
| 70 |
+
criteria = self.manager.get_yellow_flag_criteria()
|
| 71 |
+
|
| 72 |
+
# Check that criteria are not empty
|
| 73 |
+
assert len(criteria) > 0
|
| 74 |
+
|
| 75 |
+
# Check for key criteria from requirements
|
| 76 |
+
criteria_text = " ".join(criteria).lower()
|
| 77 |
+
assert "sadness without additional context" in criteria_text
|
| 78 |
+
assert "further clarification" in criteria_text or "further investigation" in criteria_text
|
| 79 |
+
|
| 80 |
+
def test_enhanced_prompt_structure(self):
|
| 81 |
+
"""
|
| 82 |
+
Test enhanced prompt generation - Requirements 5.1, 5.7.
|
| 83 |
+
|
| 84 |
+
Verifies that the prompt contains all required sections and indicators.
|
| 85 |
+
"""
|
| 86 |
+
prompt = self.manager.build_enhanced_classification_prompt()
|
| 87 |
+
|
| 88 |
+
# Check that prompt contains all required sections
|
| 89 |
+
required_sections = [
|
| 90 |
+
"<system_role>",
|
| 91 |
+
"<updated_red_flag_definition>",
|
| 92 |
+
"<explicit_red_indicators>",
|
| 93 |
+
"<yellow_flag_criteria>",
|
| 94 |
+
"<classification_logic>",
|
| 95 |
+
"<output_format>"
|
| 96 |
+
]
|
| 97 |
+
|
| 98 |
+
for section in required_sections:
|
| 99 |
+
assert section in prompt
|
| 100 |
+
|
| 101 |
+
# Check that all explicit indicators are in the prompt
|
| 102 |
+
red_indicators = self.manager.get_explicit_red_indicators()
|
| 103 |
+
for indicator in red_indicators:
|
| 104 |
+
assert indicator in prompt
|
| 105 |
+
|
| 106 |
+
def test_classification_validation_valid_red(self):
|
| 107 |
+
"""
|
| 108 |
+
Test classification validation for valid red classification.
|
| 109 |
+
|
| 110 |
+
Requirements: 5.7, 4.1, 4.2
|
| 111 |
+
"""
|
| 112 |
+
# Test valid red classification with explicit indicator
|
| 113 |
+
valid_red = ClassificationResult(
|
| 114 |
+
classification="red",
|
| 115 |
+
confidence=0.9,
|
| 116 |
+
indicators=["complex grief", "loss"],
|
| 117 |
+
reasoning="Complex grief detected",
|
| 118 |
+
red_flag_indicators=["Complex grief"],
|
| 119 |
+
yellow_flag_indicators=[],
|
| 120 |
+
is_valid=False
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
assert self.manager.validate_classification_consistency(valid_red) == True
|
| 124 |
+
|
| 125 |
+
def test_classification_validation_invalid_simple_sadness(self):
|
| 126 |
+
"""
|
| 127 |
+
Test classification validation rejects simple sadness as red.
|
| 128 |
+
|
| 129 |
+
Requirements: 3.1, 6.1
|
| 130 |
+
"""
|
| 131 |
+
# Test invalid classification (simple sadness as red without context)
|
| 132 |
+
invalid_red = ClassificationResult(
|
| 133 |
+
classification="red",
|
| 134 |
+
confidence=0.8,
|
| 135 |
+
indicators=["feeling sad"],
|
| 136 |
+
reasoning="Sadness",
|
| 137 |
+
red_flag_indicators=[],
|
| 138 |
+
yellow_flag_indicators=[],
|
| 139 |
+
is_valid=False
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
assert self.manager.validate_classification_consistency(invalid_red) == False
|
| 143 |
+
|
| 144 |
+
def test_classification_validation_meaning_loss_should_be_red(self):
|
| 145 |
+
"""
|
| 146 |
+
Test that loss of meaning/purpose should be classified as red.
|
| 147 |
+
|
| 148 |
+
Requirements: 3.2
|
| 149 |
+
"""
|
| 150 |
+
# Test loss of meaning should be red
|
| 151 |
+
meaning_loss = ClassificationResult(
|
| 152 |
+
classification="yellow",
|
| 153 |
+
confidence=0.7,
|
| 154 |
+
indicators=["loss of meaning in life"],
|
| 155 |
+
reasoning="Loss of meaning",
|
| 156 |
+
red_flag_indicators=[],
|
| 157 |
+
yellow_flag_indicators=["loss of meaning"],
|
| 158 |
+
is_valid=False
|
| 159 |
+
)
|
| 160 |
+
|
| 161 |
+
assert self.manager.validate_classification_consistency(meaning_loss) == False
|
| 162 |
+
|
| 163 |
+
def test_classification_validation_invalid_classification_type(self):
|
| 164 |
+
"""Test validation rejects invalid classification types."""
|
| 165 |
+
invalid_classification = ClassificationResult(
|
| 166 |
+
classification="blue", # Invalid
|
| 167 |
+
confidence=0.8,
|
| 168 |
+
indicators=["test"],
|
| 169 |
+
reasoning="Test",
|
| 170 |
+
red_flag_indicators=[],
|
| 171 |
+
yellow_flag_indicators=[],
|
| 172 |
+
is_valid=False
|
| 173 |
+
)
|
| 174 |
+
|
| 175 |
+
assert self.manager.validate_classification_consistency(invalid_classification) == False
|
| 176 |
+
|
| 177 |
+
def test_classification_validation_invalid_confidence(self):
|
| 178 |
+
"""Test validation rejects invalid confidence values."""
|
| 179 |
+
invalid_confidence = ClassificationResult(
|
| 180 |
+
classification="red",
|
| 181 |
+
confidence=1.5, # Invalid - should be 0.0-1.0
|
| 182 |
+
indicators=["test"],
|
| 183 |
+
reasoning="Test",
|
| 184 |
+
red_flag_indicators=[],
|
| 185 |
+
yellow_flag_indicators=[],
|
| 186 |
+
is_valid=False
|
| 187 |
+
)
|
| 188 |
+
|
| 189 |
+
assert self.manager.validate_classification_consistency(invalid_confidence) == False
|
| 190 |
+
|
| 191 |
+
def test_classification_guidelines(self):
|
| 192 |
+
"""Test classification guidelines content."""
|
| 193 |
+
guidelines = self.manager.get_classification_guidelines()
|
| 194 |
+
|
| 195 |
+
# Check that all classification types are present
|
| 196 |
+
assert "red" in guidelines
|
| 197 |
+
assert "yellow" in guidelines
|
| 198 |
+
assert "green" in guidelines
|
| 199 |
+
|
| 200 |
+
# Check content quality
|
| 201 |
+
assert len(guidelines["red"]) > 0
|
| 202 |
+
assert len(guidelines["yellow"]) > 0
|
| 203 |
+
assert len(guidelines["green"]) > 0
|
| 204 |
+
|
| 205 |
+
# Check for key terms
|
| 206 |
+
assert "spiritual" in guidelines["red"]
|
| 207 |
+
assert "further investigation" in guidelines["yellow"]
|
| 208 |
+
assert "No signs" in guidelines["green"]
|
| 209 |
+
|
| 210 |
+
def test_create_classification_result(self):
|
| 211 |
+
"""Test classification result creation with validation."""
|
| 212 |
+
result = self.manager.create_classification_result(
|
| 213 |
+
classification="red",
|
| 214 |
+
confidence=0.9,
|
| 215 |
+
indicators=["complex grief"],
|
| 216 |
+
reasoning="Complex grief detected",
|
| 217 |
+
red_flag_indicators=["Complex grief"]
|
| 218 |
+
)
|
| 219 |
+
|
| 220 |
+
assert result.classification == "red"
|
| 221 |
+
assert result.confidence == 0.9
|
| 222 |
+
assert result.indicators == ["complex grief"]
|
| 223 |
+
assert result.reasoning == "Complex grief detected"
|
| 224 |
+
assert result.red_flag_indicators == ["Complex grief"]
|
| 225 |
+
assert result.yellow_flag_indicators == []
|
| 226 |
+
assert result.is_valid == True
|
| 227 |
+
|
| 228 |
+
def test_create_classification_result_with_validation_failure(self):
|
| 229 |
+
"""Test classification result creation that fails validation."""
|
| 230 |
+
result = self.manager.create_classification_result(
|
| 231 |
+
classification="red",
|
| 232 |
+
confidence=0.8,
|
| 233 |
+
indicators=["почуття смутку"], # Simple sadness without context
|
| 234 |
+
reasoning="Смуток",
|
| 235 |
+
red_flag_indicators=[],
|
| 236 |
+
yellow_flag_indicators=[]
|
| 237 |
+
)
|
| 238 |
+
|
| 239 |
+
assert result.classification == "red"
|
| 240 |
+
assert result.is_valid == False # Should fail validation
|
| 241 |
+
|
| 242 |
+
def test_factory_function(self):
|
| 243 |
+
"""Test factory function creates proper instance."""
|
| 244 |
+
manager = create_improved_classification_prompt_manager()
|
| 245 |
+
|
| 246 |
+
assert isinstance(manager, ImprovedClassificationPromptManager)
|
| 247 |
+
|
| 248 |
+
# Test that it works properly
|
| 249 |
+
definition = manager.get_updated_red_flag_definition()
|
| 250 |
+
assert len(definition) > 0
|
| 251 |
+
|
| 252 |
+
indicators = manager.get_explicit_red_indicators()
|
| 253 |
+
assert len(indicators) == 5
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
class TestClassificationResult:
|
| 257 |
+
"""Test suite for ClassificationResult data class."""
|
| 258 |
+
|
| 259 |
+
def test_classification_result_creation(self):
|
| 260 |
+
"""Test ClassificationResult creation."""
|
| 261 |
+
result = ClassificationResult(
|
| 262 |
+
classification="red",
|
| 263 |
+
confidence=0.9,
|
| 264 |
+
indicators=["test indicator"],
|
| 265 |
+
reasoning="test reasoning",
|
| 266 |
+
red_flag_indicators=["red indicator"],
|
| 267 |
+
yellow_flag_indicators=["yellow indicator"],
|
| 268 |
+
is_valid=True
|
| 269 |
+
)
|
| 270 |
+
|
| 271 |
+
assert result.classification == "red"
|
| 272 |
+
assert result.confidence == 0.9
|
| 273 |
+
assert result.indicators == ["test indicator"]
|
| 274 |
+
assert result.reasoning == "test reasoning"
|
| 275 |
+
assert result.red_flag_indicators == ["red indicator"]
|
| 276 |
+
assert result.yellow_flag_indicators == ["yellow indicator"]
|
| 277 |
+
assert result.is_valid == True
|
| 278 |
+
|
| 279 |
+
def test_classification_result_defaults(self):
|
| 280 |
+
"""Test ClassificationResult with default values."""
|
| 281 |
+
result = ClassificationResult(
|
| 282 |
+
classification="green",
|
| 283 |
+
confidence=0.7,
|
| 284 |
+
indicators=[],
|
| 285 |
+
reasoning="No indicators found",
|
| 286 |
+
red_flag_indicators=[],
|
| 287 |
+
yellow_flag_indicators=[],
|
| 288 |
+
is_valid=True
|
| 289 |
+
)
|
| 290 |
+
|
| 291 |
+
assert result.red_flag_indicators == []
|
| 292 |
+
assert result.yellow_flag_indicators == []
|
| 293 |
+
|
| 294 |
+
|
| 295 |
+
if __name__ == "__main__":
|
| 296 |
+
pytest.main([__file__])
|
|
@@ -0,0 +1,235 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Unit tests for SpiritualMonitor improved classification logic.
|
| 4 |
+
|
| 5 |
+
Tests the enhanced classification logic for specific cases:
|
| 6 |
+
- Proper handling of "feeling sad" (Requirements 3.1, 6.1)
|
| 7 |
+
- Recognition of "loss of meaning and purpose" as red indicator (Requirement 3.2)
|
| 8 |
+
- Validation for yellow flags in unclear cases (Requirements 6.2, 6.3)
|
| 9 |
+
|
| 10 |
+
Requirements: 3.1, 3.2, 6.1, 6.2, 6.3
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import pytest
|
| 14 |
+
from unittest.mock import Mock, MagicMock
|
| 15 |
+
from src.core.spiritual_monitor import SpiritualMonitor, create_spiritual_monitor
|
| 16 |
+
from src.core.spiritual_state import SpiritualState, SpiritualAssessment
|
| 17 |
+
from src.core.ai_client import AIClientManager
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class TestSpiritualMonitorImprovedLogic:
|
| 21 |
+
"""Test suite for SpiritualMonitor improved classification logic."""
|
| 22 |
+
|
| 23 |
+
def setup_method(self):
|
| 24 |
+
"""Set up test fixtures."""
|
| 25 |
+
# Mock AI client
|
| 26 |
+
self.mock_api_client = Mock(spec=AIClientManager)
|
| 27 |
+
self.mock_api_client.call_spiritual_api = Mock()
|
| 28 |
+
|
| 29 |
+
# Create spiritual monitor with mocked client
|
| 30 |
+
self.monitor = SpiritualMonitor(self.mock_api_client)
|
| 31 |
+
|
| 32 |
+
def test_simple_sadness_without_context_is_yellow(self):
|
| 33 |
+
"""
|
| 34 |
+
Test that simple sadness without spiritual context is classified as YELLOW.
|
| 35 |
+
|
| 36 |
+
Requirements: 3.1, 6.1
|
| 37 |
+
"""
|
| 38 |
+
# Test various simple sadness expressions
|
| 39 |
+
simple_sadness_messages = [
|
| 40 |
+
"I'm feeling sad today",
|
| 41 |
+
"I feel sad about my diagnosis",
|
| 42 |
+
"Feeling down lately",
|
| 43 |
+
"I am sad"
|
| 44 |
+
]
|
| 45 |
+
|
| 46 |
+
for message in simple_sadness_messages:
|
| 47 |
+
# Mock LLM response for simple sadness (should be YELLOW)
|
| 48 |
+
self.mock_api_client.call_spiritual_api.return_value = '{"state": "yellow", "indicators": ["sadness"], "confidence": 0.7, "reasoning": "Simple sadness requires investigation"}'
|
| 49 |
+
|
| 50 |
+
# Classify the message
|
| 51 |
+
assessment = self.monitor.classify(message)
|
| 52 |
+
|
| 53 |
+
# Should be YELLOW for simple sadness
|
| 54 |
+
assert assessment.state == SpiritualState.YELLOW, f"Simple sadness should be YELLOW for: {message}"
|
| 55 |
+
assert "sadness" in " ".join(assessment.indicators).lower() or "investigation" in assessment.reasoning.lower(), f"Should indicate sadness or need for investigation: {message}"
|
| 56 |
+
|
| 57 |
+
def test_sadness_with_spiritual_context_allows_red(self):
|
| 58 |
+
"""
|
| 59 |
+
Test that sadness with spiritual context can be classified as RED.
|
| 60 |
+
|
| 61 |
+
Requirements: 3.1, 6.1
|
| 62 |
+
"""
|
| 63 |
+
# Test sadness with spiritual context
|
| 64 |
+
spiritual_sadness_messages = [
|
| 65 |
+
"I'm feeling sad and God has abandoned me",
|
| 66 |
+
"I feel sad because I've lost my faith",
|
| 67 |
+
"Feeling down about my spiritual life",
|
| 68 |
+
"I am sad and questioning God's plan"
|
| 69 |
+
]
|
| 70 |
+
|
| 71 |
+
for message in spiritual_sadness_messages:
|
| 72 |
+
# Mock LLM response for spiritual sadness (should be RED)
|
| 73 |
+
self.mock_api_client.call_spiritual_api.return_value = '{"state": "red", "indicators": ["spiritual_distress", "sadness"], "confidence": 0.9, "reasoning": "Sadness with spiritual context indicates distress"}'
|
| 74 |
+
|
| 75 |
+
# Classify the message
|
| 76 |
+
assessment = self.monitor.classify(message)
|
| 77 |
+
|
| 78 |
+
# Should be RED for sadness with spiritual context
|
| 79 |
+
assert assessment.state == SpiritualState.RED, f"Spiritual sadness should be RED for: {message}"
|
| 80 |
+
assert "spiritual" in " ".join(assessment.indicators).lower() or "distress" in assessment.reasoning.lower(), f"Should indicate spiritual distress: {message}"
|
| 81 |
+
|
| 82 |
+
def test_loss_of_meaning_is_red(self):
|
| 83 |
+
"""
|
| 84 |
+
Test that loss of meaning/purpose is always classified as RED.
|
| 85 |
+
|
| 86 |
+
Requirements: 3.2
|
| 87 |
+
"""
|
| 88 |
+
# Test various loss of meaning expressions
|
| 89 |
+
meaning_loss_messages = [
|
| 90 |
+
"I've lost all meaning in life",
|
| 91 |
+
"Life has no purpose anymore",
|
| 92 |
+
"What's the point of living?",
|
| 93 |
+
"I feel like life is meaningless",
|
| 94 |
+
"I've lost my sense of purpose"
|
| 95 |
+
]
|
| 96 |
+
|
| 97 |
+
for message in meaning_loss_messages:
|
| 98 |
+
# Mock LLM response for loss of meaning (should be RED)
|
| 99 |
+
self.mock_api_client.call_spiritual_api.return_value = '{"state": "red", "indicators": ["loss_of_meaning"], "confidence": 0.95, "reasoning": "Loss of meaning requires immediate support"}'
|
| 100 |
+
|
| 101 |
+
# Classify the message
|
| 102 |
+
assessment = self.monitor.classify(message)
|
| 103 |
+
|
| 104 |
+
# Should be RED for loss of meaning
|
| 105 |
+
assert assessment.state == SpiritualState.RED, f"Loss of meaning should be RED for: {message}"
|
| 106 |
+
assert "meaning" in " ".join(assessment.indicators).lower() or "meaning" in assessment.reasoning.lower(), f"Should indicate loss of meaning: {message}"
|
| 107 |
+
assert assessment.confidence >= 0.8, f"Should have high confidence for meaning loss: {message}"
|
| 108 |
+
|
| 109 |
+
def test_meaning_loss_classification_flow(self):
|
| 110 |
+
"""
|
| 111 |
+
Test the full classification flow for loss of meaning messages.
|
| 112 |
+
|
| 113 |
+
Requirements: 3.2
|
| 114 |
+
"""
|
| 115 |
+
message = "I feel like life has no meaning anymore"
|
| 116 |
+
|
| 117 |
+
# Mock LLM response for loss of meaning
|
| 118 |
+
self.mock_api_client.call_spiritual_api.return_value = '{"state": "red", "indicators": ["loss_of_meaning"], "confidence": 0.95, "reasoning": "Loss of meaning requires immediate support"}'
|
| 119 |
+
|
| 120 |
+
# Classify the message
|
| 121 |
+
assessment = self.monitor.classify(message)
|
| 122 |
+
|
| 123 |
+
# Should be RED due to loss of meaning
|
| 124 |
+
assert assessment.state == SpiritualState.RED
|
| 125 |
+
assert assessment.confidence >= 0.9 # High confidence for meaning loss
|
| 126 |
+
assert "meaning" in assessment.reasoning.lower()
|
| 127 |
+
assert len(assessment.indicators) > 0
|
| 128 |
+
|
| 129 |
+
def test_simple_sadness_classification_flow(self):
|
| 130 |
+
"""
|
| 131 |
+
Test the full classification flow for simple sadness messages.
|
| 132 |
+
|
| 133 |
+
Requirements: 3.1, 6.1
|
| 134 |
+
"""
|
| 135 |
+
message = "I'm feeling sad today"
|
| 136 |
+
|
| 137 |
+
# Mock LLM response for simple sadness
|
| 138 |
+
self.mock_api_client.call_spiritual_api.return_value = '{"state": "yellow", "indicators": ["sadness"], "confidence": 0.7, "reasoning": "Simple sadness requires investigation"}'
|
| 139 |
+
|
| 140 |
+
# Classify the message
|
| 141 |
+
assessment = self.monitor.classify(message)
|
| 142 |
+
|
| 143 |
+
# Should be YELLOW due to simple sadness without spiritual context
|
| 144 |
+
assert assessment.state == SpiritualState.YELLOW
|
| 145 |
+
assert "investigation" in assessment.reasoning.lower() or "sadness" in " ".join(assessment.indicators).lower()
|
| 146 |
+
assert len(assessment.indicators) > 0
|
| 147 |
+
|
| 148 |
+
def test_yellow_flag_validation_adds_clarification(self):
|
| 149 |
+
"""
|
| 150 |
+
Test that YELLOW classifications get proper clarification for investigation.
|
| 151 |
+
|
| 152 |
+
Requirements: 6.2, 6.3
|
| 153 |
+
"""
|
| 154 |
+
message = "I'm worried about something"
|
| 155 |
+
|
| 156 |
+
# Mock LLM response with proper YELLOW reasoning
|
| 157 |
+
self.mock_api_client.call_spiritual_api.return_value = '{"state": "yellow", "indicators": ["worried", "needs_clarification"], "confidence": 0.7, "reasoning": "Patient expressed worry - requires further investigation"}'
|
| 158 |
+
|
| 159 |
+
# Classify the message
|
| 160 |
+
assessment = self.monitor.classify(message)
|
| 161 |
+
|
| 162 |
+
assert assessment.state == SpiritualState.YELLOW
|
| 163 |
+
assert "investigation" in assessment.reasoning.lower() or "clarification" in assessment.reasoning.lower()
|
| 164 |
+
assert "needs_clarification" in assessment.indicators or any("clarification" in ind for ind in assessment.indicators) or "worried" in assessment.indicators
|
| 165 |
+
|
| 166 |
+
def test_enhanced_llm_classification_uses_improved_prompt(self):
|
| 167 |
+
"""
|
| 168 |
+
Test that enhanced LLM classification uses the improved prompt.
|
| 169 |
+
"""
|
| 170 |
+
message = "I'm having some concerns"
|
| 171 |
+
|
| 172 |
+
# Mock LLM response
|
| 173 |
+
self.mock_api_client.call_spiritual_api.return_value = '{"state": "yellow", "indicators": ["concerns"], "confidence": 0.7, "reasoning": "Needs investigation"}'
|
| 174 |
+
|
| 175 |
+
# Call enhanced LLM classification
|
| 176 |
+
assessment = self.monitor._classify_with_enhanced_llm(message)
|
| 177 |
+
|
| 178 |
+
# Verify LLM was called
|
| 179 |
+
self.mock_api_client.call_spiritual_api.assert_called_once()
|
| 180 |
+
|
| 181 |
+
# Check that enhanced prompt was used (contains improved classification rules)
|
| 182 |
+
call_args = self.mock_api_client.call_spiritual_api.call_args
|
| 183 |
+
system_prompt = call_args[1]['system_prompt']
|
| 184 |
+
|
| 185 |
+
# Should contain enhanced classification rules (check for English terms)
|
| 186 |
+
assert ("red flag" in system_prompt.lower() and
|
| 187 |
+
"yellow flag" in system_prompt.lower() and
|
| 188 |
+
"explicit_red_indicators" in system_prompt.lower()) or \
|
| 189 |
+
("enhanced_classification_rules" in system_prompt.lower() or "improved" in system_prompt.lower())
|
| 190 |
+
|
| 191 |
+
# Verify assessment
|
| 192 |
+
assert assessment.state == SpiritualState.YELLOW
|
| 193 |
+
assert assessment.indicators == ["concerns"]
|
| 194 |
+
assert assessment.confidence == 0.7
|
| 195 |
+
|
| 196 |
+
def test_factory_function_creates_enhanced_monitor(self):
|
| 197 |
+
"""
|
| 198 |
+
Test that factory function creates monitor with enhanced logic.
|
| 199 |
+
"""
|
| 200 |
+
monitor = create_spiritual_monitor(self.mock_api_client)
|
| 201 |
+
|
| 202 |
+
assert isinstance(monitor, SpiritualMonitor)
|
| 203 |
+
assert hasattr(monitor, 'improved_prompt_manager')
|
| 204 |
+
assert monitor.api == self.mock_api_client
|
| 205 |
+
|
| 206 |
+
def test_performance_monitoring_includes_enhanced_metadata(self):
|
| 207 |
+
"""
|
| 208 |
+
Test that performance monitoring includes enhanced logic metadata.
|
| 209 |
+
"""
|
| 210 |
+
# Mock performance monitor
|
| 211 |
+
mock_performance_monitor = Mock()
|
| 212 |
+
monitor = SpiritualMonitor(self.mock_api_client, mock_performance_monitor)
|
| 213 |
+
|
| 214 |
+
message = "I feel sad"
|
| 215 |
+
|
| 216 |
+
# Mock LLM response
|
| 217 |
+
self.mock_api_client.call_spiritual_api.return_value = '{"state": "yellow", "indicators": ["sadness"], "confidence": 0.7, "reasoning": "Simple sadness requires investigation"}'
|
| 218 |
+
|
| 219 |
+
# Classify message (will trigger performance monitoring)
|
| 220 |
+
assessment = monitor.classify(message)
|
| 221 |
+
|
| 222 |
+
# Verify performance monitoring was called
|
| 223 |
+
mock_performance_monitor.track_execution.assert_called_once()
|
| 224 |
+
|
| 225 |
+
# Check metadata includes LLM-only logic flag
|
| 226 |
+
call_args = mock_performance_monitor.track_execution.call_args
|
| 227 |
+
metadata = call_args[1]['metadata']
|
| 228 |
+
|
| 229 |
+
assert metadata['llm_only_logic_used'] is True
|
| 230 |
+
assert 'classification_result' in metadata
|
| 231 |
+
assert 'indicators_count' in metadata
|
| 232 |
+
|
| 233 |
+
|
| 234 |
+
if __name__ == "__main__":
|
| 235 |
+
pytest.main([__file__])
|
|
@@ -0,0 +1,404 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Unit tests for UI Error Handler.
|
| 4 |
+
|
| 5 |
+
Tests error handling and recovery mechanisms for UI components.
|
| 6 |
+
|
| 7 |
+
Requirements: 9.1, 9.2, 9.3, 9.4
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import pytest
|
| 11 |
+
from unittest.mock import Mock, patch
|
| 12 |
+
from datetime import datetime
|
| 13 |
+
|
| 14 |
+
from src.core.ui_error_handler import (
|
| 15 |
+
UIErrorHandler, UIError, ValidationResult, ErrorCategory, ErrorSeverity
|
| 16 |
+
)
|
| 17 |
+
from src.core.provider_summary_generator import ProviderSummary
|
| 18 |
+
from src.core.improved_classification_prompt_manager import ClassificationResult
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class TestUIErrorHandler:
|
| 22 |
+
"""Test cases for UIErrorHandler."""
|
| 23 |
+
|
| 24 |
+
def setup_method(self):
|
| 25 |
+
"""Set up test fixtures."""
|
| 26 |
+
self.error_handler = UIErrorHandler()
|
| 27 |
+
|
| 28 |
+
def test_initialization(self):
|
| 29 |
+
"""Test error handler initialization."""
|
| 30 |
+
assert self.error_handler is not None
|
| 31 |
+
assert hasattr(self.error_handler, 'fallback_templates')
|
| 32 |
+
assert hasattr(self.error_handler, 'validation_rules')
|
| 33 |
+
|
| 34 |
+
def test_validate_provider_summary_structure_valid(self):
|
| 35 |
+
"""Test validation of valid provider summary."""
|
| 36 |
+
# Create valid summary
|
| 37 |
+
summary = ProviderSummary(
|
| 38 |
+
patient_name="John Doe",
|
| 39 |
+
patient_phone="555-123-4567",
|
| 40 |
+
classification="RED",
|
| 41 |
+
confidence=0.8,
|
| 42 |
+
reasoning="Patient expressing significant spiritual distress",
|
| 43 |
+
indicators=["Loss of meaning", "Spiritual questioning"],
|
| 44 |
+
severity_level="HIGH",
|
| 45 |
+
urgency_level="URGENT",
|
| 46 |
+
situation_description="Patient experiencing spiritual crisis requiring immediate attention",
|
| 47 |
+
recommended_actions=["Contact within 24 hours", "Assess support needs"]
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
result = self.error_handler.validate_provider_summary_structure(summary)
|
| 51 |
+
|
| 52 |
+
assert isinstance(result, ValidationResult)
|
| 53 |
+
assert result.is_valid
|
| 54 |
+
assert len(result.errors) == 0
|
| 55 |
+
|
| 56 |
+
def test_validate_provider_summary_structure_missing_contact(self):
|
| 57 |
+
"""Test validation with missing contact information."""
|
| 58 |
+
# Create summary with missing contact info
|
| 59 |
+
summary = ProviderSummary(
|
| 60 |
+
patient_name="[Patient Name]",
|
| 61 |
+
patient_phone="[Phone Number]",
|
| 62 |
+
classification="RED",
|
| 63 |
+
confidence=0.8,
|
| 64 |
+
reasoning="Patient expressing significant spiritual distress",
|
| 65 |
+
indicators=["Loss of meaning"],
|
| 66 |
+
severity_level="HIGH",
|
| 67 |
+
urgency_level="URGENT"
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
result = self.error_handler.validate_provider_summary_structure(summary)
|
| 71 |
+
|
| 72 |
+
assert not result.is_valid
|
| 73 |
+
assert len(result.errors) >= 2 # Name and phone errors
|
| 74 |
+
|
| 75 |
+
# Check for specific errors
|
| 76 |
+
error_messages = [error.message for error in result.errors]
|
| 77 |
+
assert any("Patient name is missing" in msg for msg in error_messages)
|
| 78 |
+
assert any("Patient phone is missing" in msg for msg in error_messages)
|
| 79 |
+
|
| 80 |
+
def test_validate_provider_summary_structure_invalid_confidence(self):
|
| 81 |
+
"""Test validation with invalid confidence value."""
|
| 82 |
+
summary = ProviderSummary(
|
| 83 |
+
patient_name="John Doe",
|
| 84 |
+
patient_phone="555-123-4567",
|
| 85 |
+
classification="RED",
|
| 86 |
+
confidence=1.5, # Invalid - out of range
|
| 87 |
+
reasoning="Test reasoning",
|
| 88 |
+
indicators=["Test indicator"]
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
result = self.error_handler.validate_provider_summary_structure(summary)
|
| 92 |
+
|
| 93 |
+
assert not result.is_valid
|
| 94 |
+
confidence_errors = [e for e in result.errors if e.field == "confidence"]
|
| 95 |
+
assert len(confidence_errors) > 0
|
| 96 |
+
assert "out of valid range" in confidence_errors[0].message
|
| 97 |
+
|
| 98 |
+
def test_validate_provider_summary_structure_missing_reasoning(self):
|
| 99 |
+
"""Test validation with missing reasoning."""
|
| 100 |
+
summary = ProviderSummary(
|
| 101 |
+
patient_name="John Doe",
|
| 102 |
+
patient_phone="555-123-4567",
|
| 103 |
+
classification="RED",
|
| 104 |
+
confidence=0.8,
|
| 105 |
+
reasoning="", # Empty reasoning
|
| 106 |
+
indicators=["Test indicator"]
|
| 107 |
+
)
|
| 108 |
+
|
| 109 |
+
result = self.error_handler.validate_provider_summary_structure(summary)
|
| 110 |
+
|
| 111 |
+
assert not result.is_valid
|
| 112 |
+
reasoning_errors = [e for e in result.errors if e.field == "reasoning"]
|
| 113 |
+
assert len(reasoning_errors) > 0
|
| 114 |
+
assert "missing or insufficient" in reasoning_errors[0].message
|
| 115 |
+
|
| 116 |
+
def test_apply_fallback_template_missing_contact(self):
|
| 117 |
+
"""Test applying fallback template for missing contact information."""
|
| 118 |
+
summary = ProviderSummary(
|
| 119 |
+
patient_name="[Patient Name]",
|
| 120 |
+
patient_phone="[Phone Number]",
|
| 121 |
+
classification="RED",
|
| 122 |
+
confidence=0.8,
|
| 123 |
+
reasoning="Test reasoning",
|
| 124 |
+
indicators=["Test indicator"]
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
fixed_summary = self.error_handler.apply_fallback_template(summary, "missing_contact")
|
| 128 |
+
|
| 129 |
+
assert fixed_summary.patient_name != "[Patient Name]"
|
| 130 |
+
assert fixed_summary.patient_phone != "[Phone Number]"
|
| 131 |
+
assert "Patient (Name Not Provided)" in fixed_summary.patient_name
|
| 132 |
+
assert "not available" in fixed_summary.patient_phone.lower()
|
| 133 |
+
|
| 134 |
+
def test_apply_fallback_template_missing_reasoning(self):
|
| 135 |
+
"""Test applying fallback template for missing reasoning."""
|
| 136 |
+
summary = ProviderSummary(
|
| 137 |
+
patient_name="John Doe",
|
| 138 |
+
patient_phone="555-123-4567",
|
| 139 |
+
classification="RED",
|
| 140 |
+
confidence=0.8,
|
| 141 |
+
reasoning="", # Empty reasoning
|
| 142 |
+
indicators=["Loss of meaning", "Spiritual distress"]
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
fixed_summary = self.error_handler.apply_fallback_template(summary, "missing_reasoning")
|
| 146 |
+
|
| 147 |
+
assert len(fixed_summary.reasoning) > 10
|
| 148 |
+
assert "RED flag classification" in fixed_summary.reasoning
|
| 149 |
+
assert "Loss of meaning" in fixed_summary.reasoning
|
| 150 |
+
|
| 151 |
+
def test_apply_fallback_template_invalid_levels(self):
|
| 152 |
+
"""Test applying fallback template for invalid severity/urgency levels."""
|
| 153 |
+
summary = ProviderSummary(
|
| 154 |
+
patient_name="John Doe",
|
| 155 |
+
patient_phone="555-123-4567",
|
| 156 |
+
classification="RED",
|
| 157 |
+
confidence=0.8,
|
| 158 |
+
reasoning="Test reasoning",
|
| 159 |
+
indicators=["Test indicator"],
|
| 160 |
+
severity_level="INVALID",
|
| 161 |
+
urgency_level="INVALID"
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
fixed_summary = self.error_handler.apply_fallback_template(summary, "invalid_levels")
|
| 165 |
+
|
| 166 |
+
assert fixed_summary.severity_level in ["CRITICAL", "HIGH", "MODERATE"]
|
| 167 |
+
assert fixed_summary.urgency_level in ["IMMEDIATE", "URGENT", "STANDARD"]
|
| 168 |
+
|
| 169 |
+
def test_apply_fallback_template_missing_actions(self):
|
| 170 |
+
"""Test applying fallback template for missing recommended actions."""
|
| 171 |
+
summary = ProviderSummary(
|
| 172 |
+
patient_name="John Doe",
|
| 173 |
+
patient_phone="555-123-4567",
|
| 174 |
+
classification="RED",
|
| 175 |
+
confidence=0.8,
|
| 176 |
+
reasoning="Test reasoning",
|
| 177 |
+
indicators=["Test indicator"],
|
| 178 |
+
urgency_level="URGENT",
|
| 179 |
+
recommended_actions=[] # Empty actions
|
| 180 |
+
)
|
| 181 |
+
|
| 182 |
+
fixed_summary = self.error_handler.apply_fallback_template(summary, "missing_actions")
|
| 183 |
+
|
| 184 |
+
assert len(fixed_summary.recommended_actions) > 0
|
| 185 |
+
assert any("Contact patient" in action for action in fixed_summary.recommended_actions)
|
| 186 |
+
|
| 187 |
+
def test_create_degraded_display(self):
|
| 188 |
+
"""Test creating degraded display for errors."""
|
| 189 |
+
error_context = "Test error occurred"
|
| 190 |
+
original_content = "<div>Original content</div>"
|
| 191 |
+
|
| 192 |
+
degraded_display = self.error_handler.create_degraded_display(error_context, original_content)
|
| 193 |
+
|
| 194 |
+
assert isinstance(degraded_display, str)
|
| 195 |
+
assert "Display Error Detected" in degraded_display
|
| 196 |
+
assert error_context in degraded_display
|
| 197 |
+
assert original_content in degraded_display
|
| 198 |
+
assert "degraded mode" in degraded_display
|
| 199 |
+
|
| 200 |
+
def test_create_degraded_display_no_content(self):
|
| 201 |
+
"""Test creating degraded display without original content."""
|
| 202 |
+
error_context = "Test error occurred"
|
| 203 |
+
|
| 204 |
+
degraded_display = self.error_handler.create_degraded_display(error_context)
|
| 205 |
+
|
| 206 |
+
assert isinstance(degraded_display, str)
|
| 207 |
+
assert "Display Error Detected" in degraded_display
|
| 208 |
+
assert error_context in degraded_display
|
| 209 |
+
assert "Recovery Actions" in degraded_display
|
| 210 |
+
|
| 211 |
+
def test_handle_classification_error(self):
|
| 212 |
+
"""Test handling classification errors."""
|
| 213 |
+
error = Exception("Test classification error")
|
| 214 |
+
input_data = {
|
| 215 |
+
'message': 'I feel hopeless and lost',
|
| 216 |
+
'classification': 'red',
|
| 217 |
+
'confidence': 0.8
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
result = self.error_handler.handle_classification_error(error, input_data)
|
| 221 |
+
|
| 222 |
+
assert isinstance(result, ClassificationResult)
|
| 223 |
+
assert result.classification in ["red", "yellow", "green"]
|
| 224 |
+
assert 0.0 <= result.confidence <= 1.0
|
| 225 |
+
assert len(result.indicators) > 0
|
| 226 |
+
assert len(result.reasoning) > 0
|
| 227 |
+
assert not result.is_valid # Should be marked as invalid due to error
|
| 228 |
+
|
| 229 |
+
def test_handle_classification_error_critical_keywords(self):
|
| 230 |
+
"""Test handling classification error with critical keywords."""
|
| 231 |
+
error = Exception("Test error")
|
| 232 |
+
input_data = {
|
| 233 |
+
'message': 'I want to kill myself and end it all'
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
result = self.error_handler.handle_classification_error(error, input_data)
|
| 237 |
+
|
| 238 |
+
assert result.classification == "red"
|
| 239 |
+
assert result.confidence >= 0.8
|
| 240 |
+
assert any("Critical" in indicator for indicator in result.indicators)
|
| 241 |
+
|
| 242 |
+
def test_handle_classification_error_spiritual_keywords(self):
|
| 243 |
+
"""Test handling classification error with spiritual keywords."""
|
| 244 |
+
error = Exception("Test error")
|
| 245 |
+
input_data = {
|
| 246 |
+
'message': 'I have lost all meaning and purpose in life'
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
result = self.error_handler.handle_classification_error(error, input_data)
|
| 250 |
+
|
| 251 |
+
assert result.classification == "red"
|
| 252 |
+
assert result.confidence >= 0.6
|
| 253 |
+
assert any("Spiritual" in indicator for indicator in result.indicators)
|
| 254 |
+
|
| 255 |
+
def test_get_error_statistics(self):
|
| 256 |
+
"""Test getting error statistics."""
|
| 257 |
+
errors = [
|
| 258 |
+
UIError(
|
| 259 |
+
category=ErrorCategory.VALIDATION,
|
| 260 |
+
severity=ErrorSeverity.HIGH,
|
| 261 |
+
message="Test error 1",
|
| 262 |
+
component="test_component"
|
| 263 |
+
),
|
| 264 |
+
UIError(
|
| 265 |
+
category=ErrorCategory.VALIDATION,
|
| 266 |
+
severity=ErrorSeverity.MEDIUM,
|
| 267 |
+
message="Test error 2",
|
| 268 |
+
component="test_component"
|
| 269 |
+
),
|
| 270 |
+
UIError(
|
| 271 |
+
category=ErrorCategory.FORMATTING,
|
| 272 |
+
severity=ErrorSeverity.HIGH,
|
| 273 |
+
message="Test error 3",
|
| 274 |
+
component="other_component"
|
| 275 |
+
)
|
| 276 |
+
]
|
| 277 |
+
|
| 278 |
+
stats = self.error_handler.get_error_statistics(errors)
|
| 279 |
+
|
| 280 |
+
assert stats["total"] == 3
|
| 281 |
+
assert stats["by_category"]["validation"] == 2
|
| 282 |
+
assert stats["by_category"]["formatting"] == 1
|
| 283 |
+
assert stats["by_severity"]["high"] == 2
|
| 284 |
+
assert stats["by_severity"]["medium"] == 1
|
| 285 |
+
assert stats["by_component"]["test_component"] == 2
|
| 286 |
+
assert stats["by_component"]["other_component"] == 1
|
| 287 |
+
|
| 288 |
+
def test_get_error_statistics_empty(self):
|
| 289 |
+
"""Test getting error statistics with empty list."""
|
| 290 |
+
stats = self.error_handler.get_error_statistics([])
|
| 291 |
+
|
| 292 |
+
assert stats["total"] == 0
|
| 293 |
+
assert stats["by_category"] == {}
|
| 294 |
+
assert stats["by_severity"] == {}
|
| 295 |
+
|
| 296 |
+
def test_validation_result_add_error(self):
|
| 297 |
+
"""Test adding error to validation result."""
|
| 298 |
+
result = ValidationResult(is_valid=True)
|
| 299 |
+
error = UIError(
|
| 300 |
+
category=ErrorCategory.VALIDATION,
|
| 301 |
+
severity=ErrorSeverity.HIGH,
|
| 302 |
+
message="Test error",
|
| 303 |
+
component="test"
|
| 304 |
+
)
|
| 305 |
+
|
| 306 |
+
result.add_error(error)
|
| 307 |
+
|
| 308 |
+
assert not result.is_valid
|
| 309 |
+
assert len(result.errors) == 1
|
| 310 |
+
assert result.errors[0] == error
|
| 311 |
+
|
| 312 |
+
def test_validation_result_add_warning(self):
|
| 313 |
+
"""Test adding warning to validation result."""
|
| 314 |
+
result = ValidationResult(is_valid=True)
|
| 315 |
+
warning = UIError(
|
| 316 |
+
category=ErrorCategory.VALIDATION,
|
| 317 |
+
severity=ErrorSeverity.LOW,
|
| 318 |
+
message="Test warning",
|
| 319 |
+
component="test"
|
| 320 |
+
)
|
| 321 |
+
|
| 322 |
+
result.add_warning(warning)
|
| 323 |
+
|
| 324 |
+
assert result.is_valid # Warnings don't invalidate
|
| 325 |
+
assert len(result.warnings) == 1
|
| 326 |
+
assert result.warnings[0] == warning
|
| 327 |
+
|
| 328 |
+
def test_validation_result_has_critical_errors(self):
|
| 329 |
+
"""Test checking for critical errors."""
|
| 330 |
+
result = ValidationResult(is_valid=True)
|
| 331 |
+
|
| 332 |
+
# Add non-critical error
|
| 333 |
+
result.add_error(UIError(
|
| 334 |
+
category=ErrorCategory.VALIDATION,
|
| 335 |
+
severity=ErrorSeverity.HIGH,
|
| 336 |
+
message="High error",
|
| 337 |
+
component="test"
|
| 338 |
+
))
|
| 339 |
+
|
| 340 |
+
assert not result.has_critical_errors()
|
| 341 |
+
|
| 342 |
+
# Add critical error
|
| 343 |
+
result.add_error(UIError(
|
| 344 |
+
category=ErrorCategory.VALIDATION,
|
| 345 |
+
severity=ErrorSeverity.CRITICAL,
|
| 346 |
+
message="Critical error",
|
| 347 |
+
component="test"
|
| 348 |
+
))
|
| 349 |
+
|
| 350 |
+
assert result.has_critical_errors()
|
| 351 |
+
|
| 352 |
+
def test_validation_result_get_error_summary(self):
|
| 353 |
+
"""Test getting error summary."""
|
| 354 |
+
result = ValidationResult(is_valid=True)
|
| 355 |
+
|
| 356 |
+
# No errors or warnings
|
| 357 |
+
summary = result.get_error_summary()
|
| 358 |
+
assert "No validation issues" in summary
|
| 359 |
+
|
| 360 |
+
# Add errors and warnings
|
| 361 |
+
result.add_error(UIError(
|
| 362 |
+
category=ErrorCategory.VALIDATION,
|
| 363 |
+
severity=ErrorSeverity.HIGH,
|
| 364 |
+
message="Test error",
|
| 365 |
+
component="test"
|
| 366 |
+
))
|
| 367 |
+
result.add_warning(UIError(
|
| 368 |
+
category=ErrorCategory.VALIDATION,
|
| 369 |
+
severity=ErrorSeverity.LOW,
|
| 370 |
+
message="Test warning",
|
| 371 |
+
component="test"
|
| 372 |
+
))
|
| 373 |
+
|
| 374 |
+
summary = result.get_error_summary()
|
| 375 |
+
assert "1 error(s)" in summary
|
| 376 |
+
assert "1 warning(s)" in summary
|
| 377 |
+
|
| 378 |
+
def test_ui_error_to_dict(self):
|
| 379 |
+
"""Test converting UIError to dictionary."""
|
| 380 |
+
error = UIError(
|
| 381 |
+
category=ErrorCategory.VALIDATION,
|
| 382 |
+
severity=ErrorSeverity.HIGH,
|
| 383 |
+
message="Test error",
|
| 384 |
+
component="test_component",
|
| 385 |
+
field="test_field",
|
| 386 |
+
value="test_value",
|
| 387 |
+
suggestion="Test suggestion",
|
| 388 |
+
recovery_actions=["Action 1", "Action 2"]
|
| 389 |
+
)
|
| 390 |
+
|
| 391 |
+
error_dict = error.to_dict()
|
| 392 |
+
|
| 393 |
+
assert error_dict["category"] == "validation"
|
| 394 |
+
assert error_dict["severity"] == "high"
|
| 395 |
+
assert error_dict["message"] == "Test error"
|
| 396 |
+
assert error_dict["component"] == "test_component"
|
| 397 |
+
assert error_dict["field"] == "test_field"
|
| 398 |
+
assert error_dict["value"] == "test_value"
|
| 399 |
+
assert error_dict["suggestion"] == "Test suggestion"
|
| 400 |
+
assert error_dict["recovery_actions"] == ["Action 1", "Action 2"]
|
| 401 |
+
|
| 402 |
+
|
| 403 |
+
if __name__ == "__main__":
|
| 404 |
+
pytest.main([__file__])
|