Spaces:
Sleeping
Sleeping
refactor: Modularize core Gradio application logic into dedicated session, chat, model, profile, prompt, stats, and verification handler modules.
Browse files- src/interface/chat_handlers.py +142 -0
- src/interface/model_handlers.py +62 -0
- src/interface/profile_handlers.py +263 -0
- src/interface/prompt_handlers.py +305 -0
- src/interface/session_manager.py +38 -0
- src/interface/simplified_gradio_app.py +0 -0
- src/interface/stats_handlers.py +224 -0
- src/interface/verification_handlers.py +1067 -0
src/interface/chat_handlers.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
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 |
+
|
| 12 |
+
session.update_activity()
|
| 13 |
+
|
| 14 |
+
# Apply per-session model overrides (if configured in Model Settings)
|
| 15 |
+
custom_models = getattr(session, 'custom_models', None)
|
| 16 |
+
if custom_models:
|
| 17 |
+
session.app_instance.set_model_overrides(custom_models)
|
| 18 |
+
else:
|
| 19 |
+
session.app_instance.set_model_overrides({})
|
| 20 |
+
|
| 21 |
+
# Apply per-session prompt overrides (if configured in Edit Prompts)
|
| 22 |
+
custom_prompts = getattr(session, 'custom_prompts', None)
|
| 23 |
+
if custom_prompts:
|
| 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
|
| 30 |
+
stats = get_conversation_stats(session)
|
| 31 |
+
|
| 32 |
+
# Check for provider summary (RED flag case)
|
| 33 |
+
provider_summary_text = ""
|
| 34 |
+
spiritual_care_msg = ""
|
| 35 |
+
show_provider_panel = False
|
| 36 |
+
last_summary = session.app_instance.get_last_provider_summary()
|
| 37 |
+
|
| 38 |
+
# Debug logging
|
| 39 |
+
print(f"DEBUG: last_summary exists: {last_summary is not None}")
|
| 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 |
+
# Generate LLM-based spiritual care message
|
| 46 |
+
try:
|
| 47 |
+
spiritual_care_msg = session.app_instance.generate_spiritual_care_message(
|
| 48 |
+
language="English",
|
| 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 spiritual care message: {e}")
|
| 56 |
+
spiritual_care_msg = ""
|
| 57 |
+
|
| 58 |
+
show_provider_panel = True
|
| 59 |
+
print(f"DEBUG: formatted summary length: {len(provider_summary_text)}")
|
| 60 |
+
print(f"DEBUG: show_provider_panel: {show_provider_panel}")
|
| 61 |
+
else:
|
| 62 |
+
print("DEBUG: No provider summary found")
|
| 63 |
+
|
| 64 |
+
# Debug: print what we're returning
|
| 65 |
+
print(f"DEBUG RETURN: show_panel={show_provider_panel}, text_len={len(provider_summary_text)}")
|
| 66 |
+
if provider_summary_text:
|
| 67 |
+
print(f"DEBUG RETURN: first 100 chars: {provider_summary_text[:100]}")
|
| 68 |
+
|
| 69 |
+
# Format as HTML for display
|
| 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 |
+
status_msg = f"""**π΄ Provider Summary Generated**
|
| 84 |
+
|
| 85 |
+
**Patient:** {session.app_instance.patient_info.get('name', 'Test Patient')}
|
| 86 |
+
**Classification:** RED FLAG
|
| 87 |
+
**Indicators:** {len(session.app_instance.get_last_provider_summary().indicators) if session.app_instance.get_last_provider_summary() else 0} distress indicators detected
|
| 88 |
+
**Summary Length:** {len(provider_summary_text)} characters
|
| 89 |
+
|
| 90 |
+
Use the **Download Summary** button below to access the complete provider summary for the spiritual care team."""
|
| 91 |
+
else:
|
| 92 |
+
status_msg = "No provider summary available"
|
| 93 |
+
|
| 94 |
+
return (
|
| 95 |
+
new_history,
|
| 96 |
+
status,
|
| 97 |
+
session,
|
| 98 |
+
"",
|
| 99 |
+
stats,
|
| 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 |
+
spiritual_care_msg # spiritual_care_message content
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
def handle_clear(session: SimplifiedSessionData):
|
| 107 |
+
"""
|
| 108 |
+
Handle clear chat button.
|
| 109 |
+
|
| 110 |
+
Resets entire session including:
|
| 111 |
+
- Chat history
|
| 112 |
+
- Spiritual monitoring state
|
| 113 |
+
- Provider summary panel (hides and clears content)
|
| 114 |
+
- Conversation statistics
|
| 115 |
+
"""
|
| 116 |
+
if session is None:
|
| 117 |
+
session = SimplifiedSessionData()
|
| 118 |
+
|
| 119 |
+
session.update_activity()
|
| 120 |
+
new_history, status = session.app_instance.reset_session()
|
| 121 |
+
|
| 122 |
+
# Hide and clear provider summary panel
|
| 123 |
+
return (
|
| 124 |
+
new_history, # Clear chat history
|
| 125 |
+
status, # Reset status
|
| 126 |
+
session, # Updated session
|
| 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 spiritual_care_message
|
| 131 |
+
)
|
| 132 |
+
|
| 133 |
+
def send_example(example_text: str, history, session: SimplifiedSessionData):
|
| 134 |
+
"""Send example message."""
|
| 135 |
+
return handle_message(example_text, history, session)
|
| 136 |
+
|
| 137 |
+
def send_example_with_stats(example_text: str, history, session: SimplifiedSessionData):
|
| 138 |
+
"""Send example message and return stats."""
|
| 139 |
+
# Assuming this was supposed to be similar to send_example but returning different things?
|
| 140 |
+
# In the original file, it calls handle_message which returns 9 items.
|
| 141 |
+
# We should match the return signature if this is used by a button.
|
| 142 |
+
return handle_message(example_text, history, session)
|
src/interface/model_handlers.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Optional
|
| 2 |
+
import gradio as gr
|
| 3 |
+
from src.interface.session_manager import SimplifiedSessionData
|
| 4 |
+
|
| 5 |
+
def apply_model_settings(spiritual_model: str, soft_spiritual_triage_model: str, triage_evaluate_model: str, medical_model: str, soft_triage_model: str, spiritual_care_message_model: str, session: SimplifiedSessionData):
|
| 6 |
+
"""Apply custom model settings."""
|
| 7 |
+
if session is None:
|
| 8 |
+
session = SimplifiedSessionData()
|
| 9 |
+
|
| 10 |
+
# Store model settings in session
|
| 11 |
+
if not hasattr(session, 'custom_models'):
|
| 12 |
+
session.custom_models = {}
|
| 13 |
+
|
| 14 |
+
session.custom_models = {
|
| 15 |
+
'SpiritualDistressAnalyzer': spiritual_model,
|
| 16 |
+
'SoftSpiritualTriage': soft_spiritual_triage_model,
|
| 17 |
+
'TriageResponseEvaluator': triage_evaluate_model,
|
| 18 |
+
'MedicalAssistant': medical_model,
|
| 19 |
+
'SoftMedicalTriage': soft_triage_model,
|
| 20 |
+
'SpiritualCareMessage': spiritual_care_message_model
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
status = f"""<div style="padding: 1em; background-color: #ecfdf5; border-left: 4px solid #10b981; border-radius: 4px;">
|
| 24 |
+
<h4 style="color: #059669; margin-top: 0;">β
Model Settings Applied</h4>
|
| 25 |
+
|
| 26 |
+
<p><strong>π Spiritual Monitor:</strong> <code>{spiritual_model}</code></p>
|
| 27 |
+
<p><strong>π‘ Soft Spiritual Triage:</strong> <code>{soft_spiritual_triage_model}</code></p>
|
| 28 |
+
<p><strong>π Triage Response Evaluator:</strong> <code>{triage_evaluate_model}</code></p>
|
| 29 |
+
<p><strong>π₯ Medical Assistant:</strong> <code>{medical_model}</code></p>
|
| 30 |
+
<p><strong>π©Ί Soft Medical Triage:</strong> <code>{soft_triage_model}</code></p>
|
| 31 |
+
<p><strong>π¬ Spiritual Care Message:</strong> <code>{spiritual_care_message_model}</code></p>
|
| 32 |
+
|
| 33 |
+
<p style="color: #d97706; margin-bottom: 0;">
|
| 34 |
+
β οΈ <strong>Note:</strong> Model changes apply to this session only.
|
| 35 |
+
</p>
|
| 36 |
+
</div>"""
|
| 37 |
+
|
| 38 |
+
return status, session
|
| 39 |
+
|
| 40 |
+
def reset_model_settings(session: SimplifiedSessionData):
|
| 41 |
+
"""Reset models to defaults."""
|
| 42 |
+
if session is None:
|
| 43 |
+
session = SimplifiedSessionData()
|
| 44 |
+
|
| 45 |
+
# Clear custom models
|
| 46 |
+
if hasattr(session, 'custom_models'):
|
| 47 |
+
session.custom_models = {}
|
| 48 |
+
|
| 49 |
+
status = """<div style="padding: 1em; background-color: #eff6ff; border-left: 4px solid #3b82f6; border-radius: 4px;">
|
| 50 |
+
<h4 style="color: #2563eb; margin-top: 0;">π Models Reset to Defaults</h4>
|
| 51 |
+
|
| 52 |
+
<p><strong>π Spiritual Monitor:</strong> <code>gemini-2.5-flash</code></p>
|
| 53 |
+
<p><strong>π‘ Soft Spiritual Triage:</strong> <code>claude-sonnet-4-5-20250929</code></p>
|
| 54 |
+
<p><strong>π Triage Response Evaluator:</strong> <code>gemini-2.5-flash</code></p>
|
| 55 |
+
<p><strong>π₯ Medical Assistant:</strong> <code>claude-sonnet-4-5-20250929</code></p>
|
| 56 |
+
<p><strong>π©Ί Soft Medical Triage:</strong> <code>claude-sonnet-4-5-20250929</code></p>
|
| 57 |
+
<p><strong>π¬ Spiritual Care Message:</strong> <code>claude-sonnet-4-5-20250929</code></p>
|
| 58 |
+
|
| 59 |
+
<p style="margin-bottom: 0;">Default models are now active.</p>
|
| 60 |
+
</div>"""
|
| 61 |
+
|
| 62 |
+
return status, session
|
src/interface/profile_handlers.py
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import gradio as gr
|
| 3 |
+
from src.interface.session_manager import SimplifiedSessionData
|
| 4 |
+
from src.core.core_classes import ClinicalBackground
|
| 5 |
+
|
| 6 |
+
def load_profile(profile_name: str, session: SimplifiedSessionData):
|
| 7 |
+
"""Load predefined patient profile and apply it to the session."""
|
| 8 |
+
profiles = {
|
| 9 |
+
"π€ Default Profile (Serhii)": {
|
| 10 |
+
"name": "Serhii",
|
| 11 |
+
"phone": "(555) 123-4567",
|
| 12 |
+
"age": 52,
|
| 13 |
+
"conditions": "Atrial fibrillation, Deep vein thrombosis, Obesity, Hypertension",
|
| 14 |
+
"goal": "Weight reduction and cardiovascular fitness improvement",
|
| 15 |
+
"exercise": "Swimming, Walking, Light cardio",
|
| 16 |
+
"limitations": "Anticoagulation therapy, Post-thrombotic recovery"
|
| 17 |
+
},
|
| 18 |
+
"π’ GREEN - Healthy Coping": {
|
| 19 |
+
"name": "James",
|
| 20 |
+
"phone": "(555) 234-5678",
|
| 21 |
+
"age": 40,
|
| 22 |
+
"conditions": "No chronic conditions, Excellent health",
|
| 23 |
+
"goal": "Maintain fitness and wellness",
|
| 24 |
+
"exercise": "Running, Gym, Sports",
|
| 25 |
+
"limitations": "None"
|
| 26 |
+
},
|
| 27 |
+
"π‘ YELLOW - Mild Distress": {
|
| 28 |
+
"name": "Lisa",
|
| 29 |
+
"phone": "(555) 345-6789",
|
| 30 |
+
"age": 45,
|
| 31 |
+
"conditions": "Hypertension, Mild anxiety, Sleep issues",
|
| 32 |
+
"goal": "Manage stress and improve sleep quality",
|
| 33 |
+
"exercise": "Yoga, Walking, Meditation",
|
| 34 |
+
"limitations": "Stress-related fatigue, Occasional insomnia"
|
| 35 |
+
},
|
| 36 |
+
"π‘ YELLOW - Grief & Loss": {
|
| 37 |
+
"name": "Michael",
|
| 38 |
+
"phone": "(555) 456-7890",
|
| 39 |
+
"age": 58,
|
| 40 |
+
"conditions": "Recent loss of spouse, Mild depression",
|
| 41 |
+
"goal": "Process grief and rebuild routine",
|
| 42 |
+
"exercise": "Gentle walking, Support groups",
|
| 43 |
+
"limitations": "Low motivation, Emotional exhaustion"
|
| 44 |
+
},
|
| 45 |
+
"π‘ YELLOW - Existential Questions": {
|
| 46 |
+
"name": "Patricia",
|
| 47 |
+
"phone": "(555) 567-8901",
|
| 48 |
+
"age": 62,
|
| 49 |
+
"conditions": "Chronic pain, Questioning life purpose",
|
| 50 |
+
"goal": "Find meaning and manage chronic pain",
|
| 51 |
+
"exercise": "Tai Chi, Meditation, Gentle stretching",
|
| 52 |
+
"limitations": "Chronic pain, Existential concerns"
|
| 53 |
+
},
|
| 54 |
+
"π‘ YELLOW - Spiritual Disconnection": {
|
| 55 |
+
"name": "David",
|
| 56 |
+
"phone": "(555) 678-9012",
|
| 57 |
+
"age": 55,
|
| 58 |
+
"conditions": "Loss of faith, Isolation from community",
|
| 59 |
+
"goal": "Reconnect with spiritual community",
|
| 60 |
+
"exercise": "Walking, Community activities",
|
| 61 |
+
"limitations": "Spiritual disconnection, Social isolation"
|
| 62 |
+
},
|
| 63 |
+
"π΄ RED - Crisis (Suicidal Risk)": {
|
| 64 |
+
"name": "Thomas",
|
| 65 |
+
"phone": "(555) 789-0123",
|
| 66 |
+
"age": 35,
|
| 67 |
+
"conditions": "Severe depression, Suicidal ideation",
|
| 68 |
+
"goal": "Immediate crisis intervention and support",
|
| 69 |
+
"exercise": "None - Crisis support priority",
|
| 70 |
+
"limitations": "CRISIS - Suicidal thoughts, Immediate referral needed"
|
| 71 |
+
},
|
| 72 |
+
"π΄ RED - Severe Hopelessness": {
|
| 73 |
+
"name": "Jennifer",
|
| 74 |
+
"phone": "(555) 890-1234",
|
| 75 |
+
"age": 48,
|
| 76 |
+
"conditions": "Major depression, Complete hopelessness",
|
| 77 |
+
"goal": "Crisis stabilization and professional support",
|
| 78 |
+
"exercise": "None - Medical intervention priority",
|
| 79 |
+
"limitations": "CRISIS - Severe hopelessness, Unable to function"
|
| 80 |
+
},
|
| 81 |
+
"π΄ RED - Spiritual Crisis": {
|
| 82 |
+
"name": "Christopher",
|
| 83 |
+
"phone": "(555) 901-2345",
|
| 84 |
+
"age": 52,
|
| 85 |
+
"conditions": "Moral injury, Spiritual crisis, Anger at God",
|
| 86 |
+
"goal": "Spiritual crisis intervention and healing",
|
| 87 |
+
"exercise": "None - Spiritual support priority",
|
| 88 |
+
"limitations": "CRISIS - Spiritual crisis, Rage, Existential despair"
|
| 89 |
+
},
|
| 90 |
+
"π« Cardiac Patient (Rehabilitation)": {
|
| 91 |
+
"name": "John",
|
| 92 |
+
"phone": "(555) 012-3456",
|
| 93 |
+
"age": 65,
|
| 94 |
+
"conditions": "Coronary artery disease, Hypertension, Hyperlipidemia",
|
| 95 |
+
"goal": "Cardiac rehabilitation and risk factor management",
|
| 96 |
+
"exercise": "Supervised walking, Cardiac rehab program",
|
| 97 |
+
"limitations": "Recent MI, Limited exertion tolerance"
|
| 98 |
+
},
|
| 99 |
+
"π©Έ Diabetic Patient (Management)": {
|
| 100 |
+
"name": "Maria",
|
| 101 |
+
"phone": "(555) 111-2222",
|
| 102 |
+
"age": 58,
|
| 103 |
+
"conditions": "Type 2 Diabetes, Obesity, Hypertension",
|
| 104 |
+
"goal": "Blood sugar control and weight management",
|
| 105 |
+
"exercise": "Moderate walking, Resistance training",
|
| 106 |
+
"limitations": "Neuropathy, Retinopathy risk"
|
| 107 |
+
},
|
| 108 |
+
"π₯ Post-Surgery (Recovery)": {
|
| 109 |
+
"name": "Alex",
|
| 110 |
+
"phone": "(555) 222-3333",
|
| 111 |
+
"age": 45,
|
| 112 |
+
"conditions": "Post-surgical recovery, Pain management",
|
| 113 |
+
"goal": "Safe return to normal activities",
|
| 114 |
+
"exercise": "Gentle mobility, Gradual progression",
|
| 115 |
+
"limitations": "Surgical site healing, Limited ROM"
|
| 116 |
+
},
|
| 117 |
+
"π§ Mental Health (Anxiety/Depression)": {
|
| 118 |
+
"name": "Emma",
|
| 119 |
+
"phone": "(555) 333-4444",
|
| 120 |
+
"age": 35,
|
| 121 |
+
"conditions": "Depression, Anxiety, Sedentary lifestyle",
|
| 122 |
+
"goal": "Mood improvement through activity",
|
| 123 |
+
"exercise": "Yoga, Walking, Group activities",
|
| 124 |
+
"limitations": "Low motivation, Energy fluctuations"
|
| 125 |
+
},
|
| 126 |
+
"π΄ Elderly Patient (Chronic Care)": {
|
| 127 |
+
"name": "Robert",
|
| 128 |
+
"phone": "(555) 444-5555",
|
| 129 |
+
"age": 78,
|
| 130 |
+
"conditions": "Arthritis, Osteoporosis, Hypertension",
|
| 131 |
+
"goal": "Maintain independence and mobility",
|
| 132 |
+
"exercise": "Tai Chi, Water aerobics, Balance training",
|
| 133 |
+
"limitations": "Fall risk, Joint pain, Medication interactions"
|
| 134 |
+
},
|
| 135 |
+
"π Athletic Patient (Injury/Training)": {
|
| 136 |
+
"name": "Sarah",
|
| 137 |
+
"phone": "(555) 555-6666",
|
| 138 |
+
"age": 32,
|
| 139 |
+
"conditions": "Mild hypertension, Overtraining syndrome",
|
| 140 |
+
"goal": "Optimize performance and prevent injury",
|
| 141 |
+
"exercise": "Running, Strength training, Cross-training",
|
| 142 |
+
"limitations": "Overuse injuries, Recovery needs"
|
| 143 |
+
}
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
profile = profiles.get(profile_name, profiles["π€ Default Profile (Serhii)"])
|
| 147 |
+
|
| 148 |
+
# Automatically apply the profile to the session
|
| 149 |
+
if session and hasattr(session.app_instance, 'set_patient_info'):
|
| 150 |
+
session.app_instance.set_patient_info(
|
| 151 |
+
name=profile['name'],
|
| 152 |
+
phone=profile.get('phone', '')
|
| 153 |
+
)
|
| 154 |
+
|
| 155 |
+
# Update clinical_background for medical context
|
| 156 |
+
session.app_instance.clinical_background = ClinicalBackground(
|
| 157 |
+
patient_name=profile['name'],
|
| 158 |
+
age=profile['age'],
|
| 159 |
+
conditions=profile['conditions'].split(',') if isinstance(profile['conditions'], str) else profile['conditions'],
|
| 160 |
+
primary_goal=profile['goal'],
|
| 161 |
+
exercise_preferences=profile['exercise'].split(',') if isinstance(profile['exercise'], str) else profile['exercise'],
|
| 162 |
+
exercise_limitations=profile['limitations'].split(',') if isinstance(profile['limitations'], str) else profile['limitations']
|
| 163 |
+
)
|
| 164 |
+
|
| 165 |
+
# Update conversation logger patient name
|
| 166 |
+
if hasattr(session.app_instance, 'conversation_logger'):
|
| 167 |
+
session.app_instance.conversation_logger.patient_name = profile['name']
|
| 168 |
+
|
| 169 |
+
print(f"DEBUG: Auto-applied profile - Name: {profile['name']}, Phone: {profile.get('phone', '')}")
|
| 170 |
+
print(f"DEBUG: Clinical background updated - Age: {profile['age']}, Conditions: {profile['conditions']}")
|
| 171 |
+
|
| 172 |
+
status = f"""<div style="padding: 1em; background-color: #ecfdf5; border-left: 4px solid #10b981; border-radius: 4px;">
|
| 173 |
+
<h4 style="color: #059669; margin-top: 0;">β
Profile Loaded & Applied</h4>
|
| 174 |
+
<p><strong>Patient:</strong> {profile['name']}, {profile['age']} years old</p>
|
| 175 |
+
<p><strong>Phone:</strong> {profile.get('phone', 'Not provided')}</p>
|
| 176 |
+
<p><strong>Profile:</strong> {profile_name}</p>
|
| 177 |
+
<p><strong>Status:</strong> Profile has been automatically applied to this session</p>
|
| 178 |
+
<p style="margin-bottom: 0; color: #059669;">β Ready to use in conversations and reports</p>
|
| 179 |
+
</div>"""
|
| 180 |
+
|
| 181 |
+
return (
|
| 182 |
+
profile['name'],
|
| 183 |
+
profile.get('phone', ''),
|
| 184 |
+
profile['age'],
|
| 185 |
+
profile['conditions'],
|
| 186 |
+
profile['goal'],
|
| 187 |
+
profile['exercise'],
|
| 188 |
+
profile['limitations'],
|
| 189 |
+
status
|
| 190 |
+
)
|
| 191 |
+
|
| 192 |
+
def save_profile(name: str, phone: str, age: float, conditions: str, goal: str, exercise: str, limitations: str, session: SimplifiedSessionData):
|
| 193 |
+
"""Save current profile settings and update app patient info."""
|
| 194 |
+
if not name.strip():
|
| 195 |
+
return """<div style="padding: 1em; background-color: #fef2f2; border-left: 4px solid #dc2626; border-radius: 4px;">
|
| 196 |
+
<h4 style="color: #dc2626; margin-top: 0;">β Error</h4>
|
| 197 |
+
<p style="margin-bottom: 0;">Patient name cannot be empty</p>
|
| 198 |
+
</div>"""
|
| 199 |
+
|
| 200 |
+
# Update session's app instance patient info for provider summaries
|
| 201 |
+
if session and hasattr(session.app_instance, 'set_patient_info'):
|
| 202 |
+
session.app_instance.set_patient_info(name=name.strip(), phone=phone.strip() if phone else None)
|
| 203 |
+
|
| 204 |
+
# Also update clinical_background for medical context
|
| 205 |
+
session.app_instance.clinical_background = ClinicalBackground(
|
| 206 |
+
patient_name=name.strip(),
|
| 207 |
+
age=int(age) if age else None,
|
| 208 |
+
conditions=conditions.strip().split(',') if conditions.strip() else [],
|
| 209 |
+
primary_goal=goal.strip(),
|
| 210 |
+
exercise_preferences=exercise.strip().split(',') if exercise.strip() else [],
|
| 211 |
+
exercise_limitations=limitations.strip().split(',') if limitations.strip() else []
|
| 212 |
+
)
|
| 213 |
+
|
| 214 |
+
# Update conversation logger patient name
|
| 215 |
+
if hasattr(session.app_instance, 'conversation_logger'):
|
| 216 |
+
session.app_instance.conversation_logger.patient_name = name.strip()
|
| 217 |
+
|
| 218 |
+
print(f"DEBUG: Updated patient info - Name: {name.strip()}, Phone: {phone.strip() if phone else None}")
|
| 219 |
+
print(f"DEBUG: Updated clinical_background - Age: {int(age) if age else None}, Conditions: {conditions}")
|
| 220 |
+
|
| 221 |
+
status = f"""<div style="padding: 1em; background-color: #ecfdf5; border-left: 4px solid #10b981; border-radius: 4px;">
|
| 222 |
+
<h4 style="color: #059669; margin-top: 0;">πΎ Profile Saved</h4>
|
| 223 |
+
<p><strong>Patient:</strong> {name}, {int(age)} years old</p>
|
| 224 |
+
<p><strong>Phone:</strong> {phone if phone else 'Not provided'}</p>
|
| 225 |
+
<p><strong>Conditions:</strong> {conditions}</p>
|
| 226 |
+
<p><strong>Primary Goal:</strong> {goal}</p>
|
| 227 |
+
<p style="margin-bottom: 0;">Profile settings have been updated for this session.</p>
|
| 228 |
+
</div>"""
|
| 229 |
+
|
| 230 |
+
return status
|
| 231 |
+
|
| 232 |
+
def reset_profile(session: SimplifiedSessionData):
|
| 233 |
+
"""Reset profile to default."""
|
| 234 |
+
# Reset session's app instance patient info
|
| 235 |
+
if session and hasattr(session.app_instance, 'set_patient_info'):
|
| 236 |
+
session.app_instance.set_patient_info(name="Serhii", phone="(555) 123-4567")
|
| 237 |
+
|
| 238 |
+
# Also reset clinical_background
|
| 239 |
+
session.app_instance.clinical_background = ClinicalBackground(
|
| 240 |
+
patient_name="Serhii",
|
| 241 |
+
age=52,
|
| 242 |
+
conditions=["Atrial fibrillation", "Deep vein thrombosis", "Obesity", "Hypertension"],
|
| 243 |
+
primary_goal="Weight reduction and cardiovascular fitness improvement",
|
| 244 |
+
exercise_preferences=["Swimming", "Walking", "Light cardio"],
|
| 245 |
+
exercise_limitations=["Anticoagulation therapy", "Post-thrombotic recovery"]
|
| 246 |
+
)
|
| 247 |
+
|
| 248 |
+
# Update conversation logger patient name
|
| 249 |
+
if hasattr(session.app_instance, 'conversation_logger'):
|
| 250 |
+
session.app_instance.conversation_logger.patient_name = "Serhii"
|
| 251 |
+
|
| 252 |
+
print("DEBUG: Reset patient info to default (Serhii)")
|
| 253 |
+
|
| 254 |
+
return (
|
| 255 |
+
"Serhii",
|
| 256 |
+
"(555) 123-4567",
|
| 257 |
+
52,
|
| 258 |
+
"Atrial fibrillation, Deep vein thrombosis, Obesity, Hypertension",
|
| 259 |
+
"Weight reduction and cardiovascular fitness improvement",
|
| 260 |
+
"Swimming, Walking, Light cardio",
|
| 261 |
+
"Anticoagulation therapy, Post-thrombotic recovery",
|
| 262 |
+
""
|
| 263 |
+
)
|
src/interface/prompt_handlers.py
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import logging
|
| 3 |
+
import gradio as gr
|
| 4 |
+
from typing import Optional
|
| 5 |
+
from src.interface.session_manager import SimplifiedSessionData
|
| 6 |
+
|
| 7 |
+
logger = logging.getLogger(__name__)
|
| 8 |
+
|
| 9 |
+
def format_prompt_with_html(prompt_text: str) -> str:
|
| 10 |
+
"""Format prompt with HTML tags for better visualization."""
|
| 11 |
+
import re
|
| 12 |
+
import html
|
| 13 |
+
|
| 14 |
+
# Escape HTML first to prevent injection
|
| 15 |
+
formatted = html.escape(prompt_text)
|
| 16 |
+
|
| 17 |
+
# Highlight XML-like opening tags
|
| 18 |
+
formatted = re.sub(
|
| 19 |
+
r'<([a-z_]+)(?:\s+[^>]*)?>',
|
| 20 |
+
r'<span style="color: #2563eb; font-weight: 600;"><\1></span>',
|
| 21 |
+
formatted,
|
| 22 |
+
flags=re.IGNORECASE
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
# Highlight XML-like closing tags
|
| 26 |
+
formatted = re.sub(
|
| 27 |
+
r'</([a-z_]+)>',
|
| 28 |
+
r'<span style="color: #7c3aed; font-weight: 600;"></\1></span>',
|
| 29 |
+
formatted,
|
| 30 |
+
flags=re.IGNORECASE
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
# Highlight XML attributes
|
| 34 |
+
formatted = re.sub(
|
| 35 |
+
r'([a-z_]+)="([^"]+)"',
|
| 36 |
+
r'<span style="color: #059669;">\1</span>=<span style="color: #d97706;">"\2"</span>',
|
| 37 |
+
formatted,
|
| 38 |
+
flags=re.IGNORECASE
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
# Highlight bullet points
|
| 42 |
+
formatted = re.sub(
|
| 43 |
+
r'^([-β’]\s+.+)$',
|
| 44 |
+
r'<div style="margin-left: 1.5em; color: #059669;">\1</div>',
|
| 45 |
+
formatted,
|
| 46 |
+
flags=re.MULTILINE
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
# Highlight numbered lists
|
| 50 |
+
formatted = re.sub(
|
| 51 |
+
r'^(\d+\.\s+.+)$',
|
| 52 |
+
r'<div style="margin-left: 1.5em; color: #dc2626;">\1</div>',
|
| 53 |
+
formatted,
|
| 54 |
+
flags=re.MULTILINE
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
+
# Highlight important keywords
|
| 58 |
+
keywords = [
|
| 59 |
+
'IMPORTANT', 'CRITICAL', 'REQUIRED', 'MUST', 'SHALL',
|
| 60 |
+
'WARNING', 'NOTE', 'TASK', 'GOAL', 'OUTPUT', 'ANY'
|
| 61 |
+
]
|
| 62 |
+
for keyword in keywords:
|
| 63 |
+
formatted = re.sub(
|
| 64 |
+
f'\\b({keyword})\\b',
|
| 65 |
+
r'<strong style="color: #dc2626; background-color: #fef2f2; padding: 2px 4px; border-radius: 3px;">\1</strong>',
|
| 66 |
+
formatted,
|
| 67 |
+
flags=re.IGNORECASE
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
# Highlight JSON/code blocks
|
| 71 |
+
formatted = re.sub(
|
| 72 |
+
r'(\{[^}]+\})',
|
| 73 |
+
r'<code style="background-color: #f3f4f6; padding: 2px 6px; border-radius: 3px; font-family: monospace; font-size: 0.9em;">\1</code>',
|
| 74 |
+
formatted
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
# Convert newlines to <br> for proper display
|
| 78 |
+
formatted = formatted.replace('\n', '<br>')
|
| 79 |
+
|
| 80 |
+
# Wrap in container with monospace font for code-like appearance
|
| 81 |
+
formatted = f'<div style="font-family: \'Courier New\', monospace; line-height: 1.8; padding: 1em; background-color: #f9fafb; border-radius: 8px; overflow-x: auto;">{formatted}</div>'
|
| 82 |
+
|
| 83 |
+
return formatted
|
| 84 |
+
|
| 85 |
+
def _prompt_name_to_agent(prompt_name: str) -> str:
|
| 86 |
+
"""Map UI prompt selection to internal agent/prompt key."""
|
| 87 |
+
mapping = {
|
| 88 |
+
"π Spiritual Monitor (Classifier)": "SpiritualDistressAnalyzer",
|
| 89 |
+
"π‘ Soft Spiritual Triage": "SoftSpiritualTriage",
|
| 90 |
+
"π Triage Response Evaluator": "TriageResponseEvaluator",
|
| 91 |
+
"π₯ Medical Assistant": "MedicalAssistant",
|
| 92 |
+
"π©Ί Soft Medical Triage": "SoftMedicalTriage",
|
| 93 |
+
}
|
| 94 |
+
return mapping.get(prompt_name, prompt_name)
|
| 95 |
+
|
| 96 |
+
def load_prompt(prompt_name: str, session: Optional[SimplifiedSessionData] = None):
|
| 97 |
+
"""Load selected prompt for editing using enhanced prompt editor."""
|
| 98 |
+
try:
|
| 99 |
+
from src.interface.enhanced_prompt_editor import EnhancedPromptEditor
|
| 100 |
+
|
| 101 |
+
# Initialize enhanced editor
|
| 102 |
+
editor = EnhancedPromptEditor()
|
| 103 |
+
|
| 104 |
+
# Get session ID
|
| 105 |
+
session_id = getattr(session, 'session_id', 'default_session') if session else 'default_session'
|
| 106 |
+
|
| 107 |
+
# Use enhanced editor to load prompt
|
| 108 |
+
prompt_content, info_html, status_html = editor.load_prompt_for_editing(prompt_name, session_id)
|
| 109 |
+
|
| 110 |
+
return prompt_content, info_html, status_html
|
| 111 |
+
|
| 112 |
+
except Exception as e:
|
| 113 |
+
# Fallback to old system if enhanced editor fails
|
| 114 |
+
logger.warning(f"Enhanced prompt editor failed, using fallback: {e}")
|
| 115 |
+
|
| 116 |
+
from src.core.spiritual_monitor import SYSTEM_PROMPT_SPIRITUAL_MONITOR
|
| 117 |
+
from src.core.soft_triage_manager import (
|
| 118 |
+
SYSTEM_PROMPT_TRIAGE_QUESTION,
|
| 119 |
+
SYSTEM_PROMPT_TRIAGE_EVALUATE
|
| 120 |
+
)
|
| 121 |
+
from src.config.prompts import (
|
| 122 |
+
SYSTEM_PROMPT_MEDICAL_ASSISTANT,
|
| 123 |
+
SYSTEM_PROMPT_SOFT_MEDICAL_TRIAGE
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
prompts = {
|
| 127 |
+
"π Spiritual Monitor (Classifier)": SYSTEM_PROMPT_SPIRITUAL_MONITOR,
|
| 128 |
+
"π‘ Soft Spiritual Triage": SYSTEM_PROMPT_TRIAGE_QUESTION,
|
| 129 |
+
"π Triage Response Evaluator": SYSTEM_PROMPT_TRIAGE_EVALUATE,
|
| 130 |
+
"π₯ Medical Assistant": SYSTEM_PROMPT_MEDICAL_ASSISTANT,
|
| 131 |
+
"π©Ί Soft Medical Triage": SYSTEM_PROMPT_SOFT_MEDICAL_TRIAGE
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
prompt_text = prompts.get(prompt_name, "")
|
| 135 |
+
|
| 136 |
+
info = f"""**Loaded:** {prompt_name}
|
| 137 |
+
**Length:** {len(prompt_text)} characters
|
| 138 |
+
**Status:** Fallback mode (enhanced editor unavailable)"""
|
| 139 |
+
|
| 140 |
+
status = """<div style="padding: 1em; background-color: #fffbeb; border-left: 4px solid #f59e0b; border-radius: 4px;">
|
| 141 |
+
<h4 style="color: #d97706; margin-top: 0;">β οΈ Fallback Mode</h4>
|
| 142 |
+
<p style="margin-bottom: 0;">Using basic prompt editor. Enhanced features unavailable.</p>
|
| 143 |
+
</div>"""
|
| 144 |
+
|
| 145 |
+
return prompt_text, info, status
|
| 146 |
+
|
| 147 |
+
def apply_prompt_changes(prompt_name: str, prompt_text: str, session: SimplifiedSessionData):
|
| 148 |
+
"""Apply custom prompt changes using enhanced prompt editor."""
|
| 149 |
+
try:
|
| 150 |
+
from src.interface.enhanced_prompt_editor import EnhancedPromptEditor
|
| 151 |
+
|
| 152 |
+
if session is None:
|
| 153 |
+
session = SimplifiedSessionData()
|
| 154 |
+
|
| 155 |
+
# Initialize enhanced editor
|
| 156 |
+
editor = EnhancedPromptEditor()
|
| 157 |
+
|
| 158 |
+
# Get session ID
|
| 159 |
+
session_id = getattr(session, 'session_id', 'default_session')
|
| 160 |
+
|
| 161 |
+
# Use enhanced editor to apply changes
|
| 162 |
+
status_html, success = editor.apply_prompt_changes(prompt_name, prompt_text, session_id)
|
| 163 |
+
|
| 164 |
+
if success:
|
| 165 |
+
# Also store in session for backward compatibility
|
| 166 |
+
if not hasattr(session, 'custom_prompts'):
|
| 167 |
+
session.custom_prompts = {}
|
| 168 |
+
|
| 169 |
+
agent_key = _prompt_name_to_agent(prompt_name)
|
| 170 |
+
session.custom_prompts[agent_key] = prompt_text
|
| 171 |
+
|
| 172 |
+
# Apply to session app instance if available
|
| 173 |
+
if hasattr(session, 'app_instance') and hasattr(session.app_instance, 'set_prompt_overrides'):
|
| 174 |
+
session.app_instance.set_prompt_overrides(session.custom_prompts)
|
| 175 |
+
|
| 176 |
+
return status_html, session
|
| 177 |
+
|
| 178 |
+
except Exception as e:
|
| 179 |
+
# Fallback to old system
|
| 180 |
+
logger.warning(f"Enhanced prompt editor failed, using fallback: {e}")
|
| 181 |
+
|
| 182 |
+
if session is None:
|
| 183 |
+
session = SimplifiedSessionData()
|
| 184 |
+
|
| 185 |
+
if not prompt_text.strip():
|
| 186 |
+
error_html = """<div style="padding: 1em; background-color: #fef2f2; border-left: 4px solid #dc2626; border-radius: 4px;">
|
| 187 |
+
<h4 style="color: #dc2626; margin-top: 0;">β Error</h4>
|
| 188 |
+
<p style="margin-bottom: 0;">Prompt cannot be empty</p>
|
| 189 |
+
</div>"""
|
| 190 |
+
return error_html, session
|
| 191 |
+
|
| 192 |
+
# Store custom prompt in session (session-scoped)
|
| 193 |
+
if not hasattr(session, 'custom_prompts'):
|
| 194 |
+
session.custom_prompts = {}
|
| 195 |
+
|
| 196 |
+
agent_key = _prompt_name_to_agent(prompt_name)
|
| 197 |
+
session.custom_prompts[agent_key] = prompt_text
|
| 198 |
+
|
| 199 |
+
status = f"""<div style="padding: 1em; background-color: #fffbeb; border-left: 4px solid #f59e0b; border-radius: 4px;">
|
| 200 |
+
<h4 style="color: #d97706; margin-top: 0;">β οΈ Fallback Mode - Changes Applied</h4>
|
| 201 |
+
<p><strong>Prompt:</strong> {prompt_name}</p>
|
| 202 |
+
<p><strong>Length:</strong> {len(prompt_text)} characters</p>
|
| 203 |
+
<p style="margin-bottom: 0;">Enhanced features unavailable, using basic session storage.</p>
|
| 204 |
+
</div>"""
|
| 205 |
+
|
| 206 |
+
return status, session
|
| 207 |
+
|
| 208 |
+
def reset_prompt(prompt_name: str, session: SimplifiedSessionData):
|
| 209 |
+
"""Reset prompt to default using enhanced prompt editor."""
|
| 210 |
+
try:
|
| 211 |
+
from src.interface.enhanced_prompt_editor import EnhancedPromptEditor
|
| 212 |
+
|
| 213 |
+
if session is None:
|
| 214 |
+
session = SimplifiedSessionData()
|
| 215 |
+
|
| 216 |
+
# Initialize enhanced editor
|
| 217 |
+
editor = EnhancedPromptEditor()
|
| 218 |
+
|
| 219 |
+
# Get session ID
|
| 220 |
+
session_id = getattr(session, 'session_id', 'default_session')
|
| 221 |
+
|
| 222 |
+
# Use enhanced editor to reset prompt
|
| 223 |
+
prompt_content, info_html, status_html = editor.reset_prompt_to_default(prompt_name, session_id)
|
| 224 |
+
|
| 225 |
+
# Also remove from session for backward compatibility
|
| 226 |
+
agent_key = _prompt_name_to_agent(prompt_name)
|
| 227 |
+
if hasattr(session, 'custom_prompts') and agent_key in session.custom_prompts:
|
| 228 |
+
del session.custom_prompts[agent_key]
|
| 229 |
+
|
| 230 |
+
# Apply to session app instance if available
|
| 231 |
+
if hasattr(session, 'app_instance') and hasattr(session.app_instance, 'set_prompt_overrides'):
|
| 232 |
+
session.app_instance.set_prompt_overrides(getattr(session, 'custom_prompts', {}))
|
| 233 |
+
|
| 234 |
+
return prompt_content, info_html, status_html, session
|
| 235 |
+
|
| 236 |
+
except Exception as e:
|
| 237 |
+
# Fallback to old system
|
| 238 |
+
logger.warning(f"Enhanced prompt editor failed, using fallback: {e}")
|
| 239 |
+
|
| 240 |
+
if session is None:
|
| 241 |
+
session = SimplifiedSessionData()
|
| 242 |
+
|
| 243 |
+
# Remove from custom prompts
|
| 244 |
+
agent_key = _prompt_name_to_agent(prompt_name)
|
| 245 |
+
if hasattr(session, 'custom_prompts') and agent_key in session.custom_prompts:
|
| 246 |
+
del session.custom_prompts[agent_key]
|
| 247 |
+
|
| 248 |
+
# Reload default
|
| 249 |
+
prompt_text, info, status = load_prompt(prompt_name, session)
|
| 250 |
+
|
| 251 |
+
reset_status = """<div style="padding: 1em; background-color: #fffbeb; border-left: 4px solid #f59e0b; border-radius: 4px;">
|
| 252 |
+
<h4 style="color: #d97706; margin-top: 0;">π Fallback Mode - Reset Complete</h4>
|
| 253 |
+
<p style="margin-bottom: 0;">Prompt restored using basic system. Enhanced features unavailable.</p>
|
| 254 |
+
</div>"""
|
| 255 |
+
|
| 256 |
+
return prompt_text, info, reset_status, session
|
| 257 |
+
|
| 258 |
+
def promote_prompt_to_file(prompt_name: str, session: SimplifiedSessionData):
|
| 259 |
+
"""Promote session prompt override to permanent file."""
|
| 260 |
+
try:
|
| 261 |
+
from src.interface.enhanced_prompt_editor import EnhancedPromptEditor
|
| 262 |
+
|
| 263 |
+
if session is None:
|
| 264 |
+
return """<div style="padding: 1em; background-color: #fef2f2; border-left: 4px solid #dc2626; border-radius: 4px;">
|
| 265 |
+
<h4 style="color: #dc2626; margin-top: 0;">β Error</h4>
|
| 266 |
+
<p style="margin-bottom: 0;">No session data available</p>
|
| 267 |
+
</div>""", session
|
| 268 |
+
|
| 269 |
+
# Initialize enhanced editor
|
| 270 |
+
editor = EnhancedPromptEditor()
|
| 271 |
+
|
| 272 |
+
# Get session ID
|
| 273 |
+
session_id = getattr(session, 'session_id', 'default_session')
|
| 274 |
+
|
| 275 |
+
# Use enhanced editor to promote prompt
|
| 276 |
+
status_html, success = editor.promote_session_to_file(prompt_name, session_id)
|
| 277 |
+
|
| 278 |
+
return status_html, session
|
| 279 |
+
|
| 280 |
+
except Exception as e:
|
| 281 |
+
logger.warning(f"Enhanced prompt editor failed: {e}")
|
| 282 |
+
return f"""<div style="padding: 1em; background-color: #fef2f2; border-left: 4px solid #dc2626; border-radius: 4px;">
|
| 283 |
+
<h4 style="color: #dc2626; margin-top: 0;">β Error</h4>
|
| 284 |
+
<p style="margin-bottom: 0;">Failed to promote prompt: {str(e)}</p>
|
| 285 |
+
</div>""", session
|
| 286 |
+
|
| 287 |
+
def validate_prompt_syntax(prompt_text: str):
|
| 288 |
+
"""Validate prompt syntax and structure."""
|
| 289 |
+
try:
|
| 290 |
+
from src.interface.enhanced_prompt_editor import EnhancedPromptEditor
|
| 291 |
+
|
| 292 |
+
# Initialize enhanced editor
|
| 293 |
+
editor = EnhancedPromptEditor()
|
| 294 |
+
|
| 295 |
+
# Use enhanced editor to validate prompt
|
| 296 |
+
validation_html, is_valid = editor.validate_prompt_syntax(prompt_text)
|
| 297 |
+
|
| 298 |
+
return validation_html
|
| 299 |
+
|
| 300 |
+
except Exception as e:
|
| 301 |
+
logger.warning(f"Enhanced prompt editor failed: {e}")
|
| 302 |
+
return f"""<div style="padding: 1em; background-color: #fef2f2; border-left: 4px solid #dc2626; border-radius: 4px;">
|
| 303 |
+
<h4 style="color: #dc2626; margin-top: 0;">β Validation Error</h4>
|
| 304 |
+
<p style="margin-bottom: 0;">Failed to validate prompt: {str(e)}</p>
|
| 305 |
+
</div>"""
|
src/interface/session_manager.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import asyncio
|
| 3 |
+
import uuid
|
| 4 |
+
import os
|
| 5 |
+
import sys
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
from src.core.simplified_medical_app import SimplifiedMedicalApp
|
| 8 |
+
from src.core.core_classes import ClinicalBackground
|
| 9 |
+
|
| 10 |
+
class SimplifiedSessionData:
|
| 11 |
+
"""Container for user session data."""
|
| 12 |
+
|
| 13 |
+
def __init__(self, session_id: str = None):
|
| 14 |
+
self.session_id = session_id or str(uuid.uuid4())
|
| 15 |
+
self.app_instance = SimplifiedMedicalApp()
|
| 16 |
+
self.created_at = datetime.now().isoformat()
|
| 17 |
+
self.last_activity = datetime.now().isoformat()
|
| 18 |
+
|
| 19 |
+
# Set default patient info from profile
|
| 20 |
+
self.app_instance.set_patient_info(name="Serhii", phone="(555) 123-4567")
|
| 21 |
+
|
| 22 |
+
# Update clinical_background to match default profile
|
| 23 |
+
self.app_instance.clinical_background = ClinicalBackground(
|
| 24 |
+
patient_name="Serhii",
|
| 25 |
+
age=52,
|
| 26 |
+
conditions=["Atrial fibrillation", "Deep vein thrombosis", "Obesity", "Hypertension"],
|
| 27 |
+
primary_goal="Weight reduction and cardiovascular fitness improvement",
|
| 28 |
+
exercise_preferences=["Swimming", "Walking", "Light cardio"],
|
| 29 |
+
exercise_limitations=["Anticoagulation therapy", "Post-thrombotic recovery"]
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
# Update conversation logger patient name
|
| 33 |
+
if hasattr(self.app_instance, 'conversation_logger'):
|
| 34 |
+
self.app_instance.conversation_logger.patient_name = "Serhii"
|
| 35 |
+
|
| 36 |
+
def update_activity(self):
|
| 37 |
+
"""Update last activity timestamp."""
|
| 38 |
+
self.last_activity = datetime.now().isoformat()
|
src/interface/simplified_gradio_app.py
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
src/interface/stats_handlers.py
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import gradio as gr
|
| 3 |
+
import json
|
| 4 |
+
import csv
|
| 5 |
+
import os
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
from src.interface.session_manager import SimplifiedSessionData
|
| 8 |
+
|
| 9 |
+
def get_conversation_stats(session: SimplifiedSessionData):
|
| 10 |
+
"""Get conversation statistics."""
|
| 11 |
+
if session is None or not hasattr(session.app_instance, 'conversation_logger'):
|
| 12 |
+
return "No conversation yet"
|
| 13 |
+
|
| 14 |
+
try:
|
| 15 |
+
summary = session.app_instance.get_conversation_summary()
|
| 16 |
+
if not summary or summary.get('total_exchanges', 0) == 0:
|
| 17 |
+
return "No conversation yet"
|
| 18 |
+
|
| 19 |
+
stats_text = f"""**π Conversation Statistics**
|
| 20 |
+
|
| 21 |
+
**Messages:** {summary['total_exchanges']} exchanges
|
| 22 |
+
**Duration:** {summary['session_duration_minutes']} minutes
|
| 23 |
+
|
| 24 |
+
**Classifications:**
|
| 25 |
+
π’ Green: {summary['classification_counts']['green']} ({summary['classification_percentages']['green']}%)
|
| 26 |
+
π‘ Yellow: {summary['classification_counts']['yellow']} ({summary['classification_percentages']['yellow']}%)
|
| 27 |
+
π΄ Red: {summary['classification_counts']['red']} ({summary['classification_percentages']['red']}%)
|
| 28 |
+
|
| 29 |
+
**Average Confidence:** {summary['average_confidence']}
|
| 30 |
+
|
| 31 |
+
**Top Indicators:**"""
|
| 32 |
+
|
| 33 |
+
for indicator, count in list(summary['top_indicators'].items())[:3]:
|
| 34 |
+
stats_text += f"\nβ’ {indicator}: {count}"
|
| 35 |
+
|
| 36 |
+
return stats_text
|
| 37 |
+
|
| 38 |
+
except Exception as e:
|
| 39 |
+
return f"Error getting stats: {str(e)}"
|
| 40 |
+
|
| 41 |
+
def download_conversation_json(session: SimplifiedSessionData):
|
| 42 |
+
"""Download conversation as JSON."""
|
| 43 |
+
if session is None or not hasattr(session.app_instance, 'conversation_logger'):
|
| 44 |
+
return None
|
| 45 |
+
|
| 46 |
+
try:
|
| 47 |
+
log_path = session.app_instance.get_conversation_log_path()
|
| 48 |
+
return log_path
|
| 49 |
+
except Exception as e:
|
| 50 |
+
print(f"Error downloading JSON: {e}")
|
| 51 |
+
return None
|
| 52 |
+
|
| 53 |
+
def download_conversation_csv(session: SimplifiedSessionData):
|
| 54 |
+
"""Download conversation as CSV."""
|
| 55 |
+
if session is None or not hasattr(session.app_instance, 'conversation_logger'):
|
| 56 |
+
return None
|
| 57 |
+
|
| 58 |
+
try:
|
| 59 |
+
csv_path = session.app_instance.export_conversation_csv()
|
| 60 |
+
return csv_path
|
| 61 |
+
except Exception as e:
|
| 62 |
+
print(f"Error downloading CSV: {e}")
|
| 63 |
+
return None
|
| 64 |
+
|
| 65 |
+
def get_status(session: SimplifiedSessionData):
|
| 66 |
+
"""Get current status and stats."""
|
| 67 |
+
if session is None:
|
| 68 |
+
return "β Session not initialized", "No stats", gr.update(visible=False), "No provider summary available", "", ""
|
| 69 |
+
|
| 70 |
+
session.update_activity()
|
| 71 |
+
status_info = session.app_instance._get_status_info()
|
| 72 |
+
|
| 73 |
+
# Get stats
|
| 74 |
+
stats_text = get_conversation_stats(session)
|
| 75 |
+
|
| 76 |
+
# Check for provider summary
|
| 77 |
+
last_summary = session.app_instance.get_last_provider_summary()
|
| 78 |
+
show_provider_panel = last_summary is not None
|
| 79 |
+
|
| 80 |
+
provider_summary_text = ""
|
| 81 |
+
spiritual_care_msg = ""
|
| 82 |
+
|
| 83 |
+
if last_summary:
|
| 84 |
+
provider_summary_text = session.app_instance.provider_summary_generator.format_for_display(last_summary)
|
| 85 |
+
|
| 86 |
+
# Generate spiritual care message
|
| 87 |
+
try:
|
| 88 |
+
spiritual_care_msg = session.app_instance.generate_spiritual_care_message(
|
| 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 spiritual care message in get_status: {e}")
|
| 96 |
+
spiritual_care_msg = ""
|
| 97 |
+
|
| 98 |
+
if provider_summary_text:
|
| 99 |
+
import html
|
| 100 |
+
escaped_text = html.escape(provider_summary_text)
|
| 101 |
+
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>"
|
| 102 |
+
|
| 103 |
+
status_msg = f"""**π΄ Provider Summary Generated**
|
| 104 |
+
|
| 105 |
+
**Patient:** {session.app_instance.patient_info.get('name', 'Test Patient')}
|
| 106 |
+
**Classification:** RED FLAG
|
| 107 |
+
**Indicators:** {len(last_summary.indicators)} distress indicators detected
|
| 108 |
+
**Summary Length:** {len(provider_summary_text)} characters
|
| 109 |
+
|
| 110 |
+
Use the **Download Summary** button below to access the complete provider summary for the spiritual care team."""
|
| 111 |
+
else:
|
| 112 |
+
html_content = ""
|
| 113 |
+
status_msg = "No provider summary available"
|
| 114 |
+
|
| 115 |
+
return (
|
| 116 |
+
status_info,
|
| 117 |
+
stats_text,
|
| 118 |
+
gr.update(visible=show_provider_panel),
|
| 119 |
+
status_msg,
|
| 120 |
+
html_content,
|
| 121 |
+
spiritual_care_msg
|
| 122 |
+
)
|
| 123 |
+
|
| 124 |
+
def download_provider_summary(session: SimplifiedSessionData):
|
| 125 |
+
"""Download provider summary as text file."""
|
| 126 |
+
if session is None:
|
| 127 |
+
return None
|
| 128 |
+
|
| 129 |
+
last_summary = session.app_instance.get_last_provider_summary()
|
| 130 |
+
if not last_summary:
|
| 131 |
+
return None
|
| 132 |
+
|
| 133 |
+
try:
|
| 134 |
+
# Generate filename
|
| 135 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 136 |
+
patient_name = session.app_instance.patient_info.get('name', 'Patient').replace(" ", "_")
|
| 137 |
+
filename = f"provider_summary_{patient_name}_{timestamp}.txt"
|
| 138 |
+
|
| 139 |
+
# Format content
|
| 140 |
+
content = session.app_instance.provider_summary_generator.format_for_display(last_summary)
|
| 141 |
+
|
| 142 |
+
# Save to temp file
|
| 143 |
+
path = os.path.join(os.getcwd(), "exports", filename)
|
| 144 |
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
| 145 |
+
|
| 146 |
+
with open(path, "w", encoding="utf-8") as f:
|
| 147 |
+
f.write(content)
|
| 148 |
+
|
| 149 |
+
return path
|
| 150 |
+
|
| 151 |
+
except Exception as e:
|
| 152 |
+
print(f"Error downloading provider summary: {e}")
|
| 153 |
+
return None
|
| 154 |
+
|
| 155 |
+
def clear_provider_summary():
|
| 156 |
+
"""Clear provider summary panel."""
|
| 157 |
+
return (
|
| 158 |
+
gr.update(visible=False),
|
| 159 |
+
"No provider summary available",
|
| 160 |
+
"",
|
| 161 |
+
""
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
def regenerate_spiritual_care_message(
|
| 165 |
+
session: SimplifiedSessionData,
|
| 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 |
+
|
| 175 |
+
last_summary = session.app_instance.get_last_provider_summary()
|
| 176 |
+
if not last_summary:
|
| 177 |
+
return ""
|
| 178 |
+
|
| 179 |
+
try:
|
| 180 |
+
msg = session.app_instance.generate_spiritual_care_message(
|
| 181 |
+
language="English",
|
| 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 download_spiritual_care_message(session: SimplifiedSessionData):
|
| 193 |
+
"""Download spiritual care message as text file."""
|
| 194 |
+
if session is None:
|
| 195 |
+
return None
|
| 196 |
+
|
| 197 |
+
# We need to get the current message text from the UI state, but typically
|
| 198 |
+
# in Gradio we return the file path. Since we don't have the text passed here,
|
| 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"spiritual_care_message_{patient_name}_{timestamp}.txt"
|
| 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(msg)
|
| 220 |
+
|
| 221 |
+
return path
|
| 222 |
+
except Exception as e:
|
| 223 |
+
print(f"Error downloading spiritual care message: {e}")
|
| 224 |
+
return None
|
src/interface/verification_handlers.py
ADDED
|
@@ -0,0 +1,1067 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import gradio as gr
|
| 3 |
+
import json
|
| 4 |
+
import os
|
| 5 |
+
import uuid
|
| 6 |
+
import csv
|
| 7 |
+
import traceback
|
| 8 |
+
from datetime import datetime
|
| 9 |
+
from typing import List, Optional, Any, Dict
|
| 10 |
+
|
| 11 |
+
from src.interface.session_manager import SimplifiedSessionData
|
| 12 |
+
from src.core.verification_models import VerificationSession, VerificationRecord, TestMessage
|
| 13 |
+
from src.core.verification_store import JSONVerificationStore
|
| 14 |
+
from src.core.chaplain_models import ClassificationFlowResult, DistressIndicator, FollowUpQuestion, TaggingRecord
|
| 15 |
+
from src.core.verification_csv_exporter import VerificationCSVExporter
|
| 16 |
+
from src.core.test_datasets import TestDatasetManager
|
| 17 |
+
from src.interface.verification_ui import VerificationUIComponents
|
| 18 |
+
from src.interface.chaplain_feedback_ui import ChaplainFeedbackUIComponents
|
| 19 |
+
from src.core.conversation_verification import (
|
| 20 |
+
ConversationVerificationManager,
|
| 21 |
+
VerificationRecord as ConvVerificationRecord,
|
| 22 |
+
VerificationSession as ConvVerificationSession
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
def open_verification_window(session: SimplifiedSessionData):
|
| 26 |
+
"""Open verification window for current conversation."""
|
| 27 |
+
if session is None or not hasattr(session.app_instance, 'conversation_logger'):
|
| 28 |
+
return """<div style="background-color: #f8d7da; padding: 0.75em; border-radius: 4px; margin: 0.5em 0;">
|
| 29 |
+
β <strong>No conversation to verify</strong><br>
|
| 30 |
+
<small>Start a conversation first</small>
|
| 31 |
+
</div>"""
|
| 32 |
+
|
| 33 |
+
try:
|
| 34 |
+
# Check if conversation has any entries
|
| 35 |
+
if not session.app_instance.conversation_logger.entries:
|
| 36 |
+
return """<div style="background-color: #fff3cd; padding: 0.75em; border-radius: 4px; margin: 0.5em 0;">
|
| 37 |
+
β οΈ <strong>No conversation exchanges to verify</strong><br>
|
| 38 |
+
<small>Send some messages in the chat first</small>
|
| 39 |
+
</div>"""
|
| 40 |
+
|
| 41 |
+
print(f"π Opening verification for {len(session.app_instance.conversation_logger.entries)} exchanges...")
|
| 42 |
+
|
| 43 |
+
manager = ConversationVerificationManager()
|
| 44 |
+
verification_session = manager.create_verification_session(
|
| 45 |
+
session.app_instance.conversation_logger,
|
| 46 |
+
"Medical Professional"
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
print(f"β
Created verification session: {verification_session.session_id}")
|
| 50 |
+
|
| 51 |
+
# HF Spaces / Gradio limitation:
|
| 52 |
+
# Launching a *second* Gradio server from inside a running Gradio app is unreliable
|
| 53 |
+
# and is currently causing the Button._id error in Spaces.
|
| 54 |
+
# Instead, export the verification session to a JSON file that the user can download.
|
| 55 |
+
|
| 56 |
+
export_dir = os.path.join(os.getcwd(), "verification_sessions")
|
| 57 |
+
os.makedirs(export_dir, exist_ok=True)
|
| 58 |
+
|
| 59 |
+
export_filename = f"verification_session_{datetime.utcnow().strftime('%Y%m%d_%H%M%S')}_{verification_session.session_id}.json"
|
| 60 |
+
export_path = os.path.join(export_dir, export_filename)
|
| 61 |
+
|
| 62 |
+
# Serialize to JSON in a resilient way (dataclasses / pydantic / plain python).
|
| 63 |
+
def _to_dict(obj):
|
| 64 |
+
if hasattr(obj, "model_dump"):
|
| 65 |
+
return obj.model_dump()
|
| 66 |
+
if hasattr(obj, "dict") and callable(getattr(obj, "dict")):
|
| 67 |
+
return obj.dict()
|
| 68 |
+
if hasattr(obj, "__dict__"):
|
| 69 |
+
return obj.__dict__
|
| 70 |
+
return str(obj)
|
| 71 |
+
|
| 72 |
+
payload = {
|
| 73 |
+
"session_id": verification_session.session_id,
|
| 74 |
+
"patient_name": verification_session.patient_name,
|
| 75 |
+
"verifier_name": verification_session.verifier_name,
|
| 76 |
+
"start_time": verification_session.start_time.isoformat() if hasattr(verification_session, "start_time") else None,
|
| 77 |
+
"verification_records": [
|
| 78 |
+
{
|
| 79 |
+
# Conversation verification records use `exchange_id`.
|
| 80 |
+
# Keep a `record_id` alias for backward compatibility with older exports.
|
| 81 |
+
"exchange_id": getattr(r, "exchange_id", None),
|
| 82 |
+
"record_id": getattr(r, "exchange_id", None),
|
| 83 |
+
"timestamp": r.timestamp.isoformat() if hasattr(r, "timestamp") else None,
|
| 84 |
+
"user_message": r.user_message,
|
| 85 |
+
"assistant_response": r.assistant_response,
|
| 86 |
+
"original_classification": r.original_classification,
|
| 87 |
+
"original_confidence": r.original_confidence,
|
| 88 |
+
"original_indicators": r.original_indicators,
|
| 89 |
+
"original_reasoning": r.original_reasoning,
|
| 90 |
+
"is_correct": r.is_correct,
|
| 91 |
+
"correct_classification": r.correct_classification,
|
| 92 |
+
"correction_reason": r.correction_reason,
|
| 93 |
+
"verifier_notes": r.verifier_notes,
|
| 94 |
+
}
|
| 95 |
+
for r in verification_session.verification_records
|
| 96 |
+
],
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
with open(export_path, "w", encoding="utf-8") as f:
|
| 100 |
+
json.dump(payload, f, ensure_ascii=False, indent=2, default=_to_dict)
|
| 101 |
+
|
| 102 |
+
print(f"β
Verification session exported: {export_path}")
|
| 103 |
+
|
| 104 |
+
return f"""<div style="background-color: #d4edda; padding: 0.75em; border-radius: 4px; margin: 0.5em 0;">
|
| 105 |
+
β
<strong>Verification session exported</strong><br>
|
| 106 |
+
<small>Exchanges: {len(verification_session.verification_records)}</small><br>
|
| 107 |
+
<small>Download JSON from the app's files panel (or add a dedicated download button).</small>
|
| 108 |
+
</div>"""
|
| 109 |
+
|
| 110 |
+
except Exception as e:
|
| 111 |
+
print(f"β Error opening verification: {str(e)}")
|
| 112 |
+
traceback.print_exc()
|
| 113 |
+
|
| 114 |
+
return f"""<div style="background-color: #f8d7da; padding: 0.75em; border-radius: 4px; margin: 0.5em 0;">
|
| 115 |
+
β <strong>Error opening verification</strong><br>
|
| 116 |
+
<small>{str(e)}</small>
|
| 117 |
+
</div>"""
|
| 118 |
+
|
| 119 |
+
def load_verification_dataset(dataset_name: str, store: JSONVerificationStore):
|
| 120 |
+
"""Load a verification dataset."""
|
| 121 |
+
try:
|
| 122 |
+
# Find dataset ID from name
|
| 123 |
+
datasets = TestDatasetManager.get_dataset_list()
|
| 124 |
+
dataset_id = None
|
| 125 |
+
for d in datasets:
|
| 126 |
+
if d['name'] in dataset_name:
|
| 127 |
+
dataset_id = d['dataset_id']
|
| 128 |
+
break
|
| 129 |
+
|
| 130 |
+
if not dataset_id:
|
| 131 |
+
return (
|
| 132 |
+
None, # verification_session
|
| 133 |
+
"β Dataset not found", # dataset_info
|
| 134 |
+
"", "", "", "", # message_text, decision_badge, confidence, indicators
|
| 135 |
+
"", # progress_display
|
| 136 |
+
"β Dataset not found", # error_message
|
| 137 |
+
0, # current_message_index
|
| 138 |
+
None, # current_dataset_id
|
| 139 |
+
[], # message_queue
|
| 140 |
+
[], # verification_records
|
| 141 |
+
)
|
| 142 |
+
|
| 143 |
+
# Load dataset
|
| 144 |
+
dataset = TestDatasetManager.load_dataset(dataset_id)
|
| 145 |
+
|
| 146 |
+
# Create new verification session
|
| 147 |
+
new_session = VerificationSession(
|
| 148 |
+
session_id=str(uuid.uuid4()),
|
| 149 |
+
verifier_name="Medical Professional",
|
| 150 |
+
dataset_id=dataset_id,
|
| 151 |
+
dataset_name=dataset.name,
|
| 152 |
+
total_messages=dataset.message_count,
|
| 153 |
+
message_queue=[m.message_id for m in dataset.messages],
|
| 154 |
+
)
|
| 155 |
+
|
| 156 |
+
# Save session
|
| 157 |
+
store.save_session(new_session)
|
| 158 |
+
|
| 159 |
+
# Get first message
|
| 160 |
+
if dataset.messages:
|
| 161 |
+
first_message = dataset.messages[0]
|
| 162 |
+
message_text, decision_badge, confidence, indicators = VerificationUIComponents.render_message_review(
|
| 163 |
+
first_message,
|
| 164 |
+
first_message.pre_classified_label,
|
| 165 |
+
0.85, # Default confidence
|
| 166 |
+
["Distress indicator 1", "Distress indicator 2"]
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
progress = VerificationUIComponents.update_progress_display(0, dataset.message_count)
|
| 170 |
+
|
| 171 |
+
dataset_info_text = f"**{dataset.name}**\n\n{dataset.description}\n\nπ {dataset.message_count} messages to review"
|
| 172 |
+
|
| 173 |
+
return (
|
| 174 |
+
new_session, # verification_session
|
| 175 |
+
dataset_info_text, # dataset_info
|
| 176 |
+
message_text, # message_text
|
| 177 |
+
decision_badge, # decision_badge
|
| 178 |
+
confidence, # confidence
|
| 179 |
+
indicators, # indicators
|
| 180 |
+
progress, # progress_display
|
| 181 |
+
"", # error_message (empty = no error)
|
| 182 |
+
0, # current_message_index
|
| 183 |
+
dataset_id, # current_dataset_id
|
| 184 |
+
[m.message_id for m in dataset.messages], # message_queue
|
| 185 |
+
[], # verification_records
|
| 186 |
+
)
|
| 187 |
+
else:
|
| 188 |
+
return (
|
| 189 |
+
None, # verification_session
|
| 190 |
+
"β Dataset is empty", # dataset_info
|
| 191 |
+
"", "", "", "", # message_text, decision_badge, confidence, indicators
|
| 192 |
+
"", # progress_display
|
| 193 |
+
"β Dataset is empty", # error_message
|
| 194 |
+
0, # current_message_index
|
| 195 |
+
dataset_id, # current_dataset_id
|
| 196 |
+
[], # message_queue
|
| 197 |
+
[], # verification_records
|
| 198 |
+
)
|
| 199 |
+
|
| 200 |
+
except Exception as e:
|
| 201 |
+
return (
|
| 202 |
+
None, # verification_session
|
| 203 |
+
f"β Error loading dataset: {str(e)}", # dataset_info
|
| 204 |
+
"", "", "", "", # message_text, decision_badge, confidence, indicators
|
| 205 |
+
"", # progress_display
|
| 206 |
+
f"β Error: {str(e)}", # error_message
|
| 207 |
+
0, # current_message_index
|
| 208 |
+
None, # current_dataset_id
|
| 209 |
+
[], # message_queue
|
| 210 |
+
[], # verification_records
|
| 211 |
+
)
|
| 212 |
+
|
| 213 |
+
def handle_correct_feedback(session: VerificationSession, current_idx: int, dataset_id: str, message_queue: List[str], records: List[dict], store: JSONVerificationStore):
|
| 214 |
+
"""Handle correct feedback."""
|
| 215 |
+
try:
|
| 216 |
+
if not session or current_idx >= len(message_queue):
|
| 217 |
+
return (
|
| 218 |
+
session,
|
| 219 |
+
"β Error: Invalid session state",
|
| 220 |
+
"", "", "", "",
|
| 221 |
+
"",
|
| 222 |
+
"β Correct: 0",
|
| 223 |
+
"β Incorrect: 0",
|
| 224 |
+
"π Accuracy: 0%",
|
| 225 |
+
current_idx,
|
| 226 |
+
records,
|
| 227 |
+
)
|
| 228 |
+
|
| 229 |
+
# Get current message
|
| 230 |
+
dataset = TestDatasetManager.load_dataset(dataset_id)
|
| 231 |
+
current_message_id = message_queue[current_idx]
|
| 232 |
+
current_message = next((m for m in dataset.messages if m.message_id == current_message_id), None)
|
| 233 |
+
|
| 234 |
+
if not current_message:
|
| 235 |
+
return (
|
| 236 |
+
session,
|
| 237 |
+
"β Error: Message not found",
|
| 238 |
+
"", "", "", "",
|
| 239 |
+
"",
|
| 240 |
+
"β Correct: 0",
|
| 241 |
+
"β Incorrect: 0",
|
| 242 |
+
"π Accuracy: 0%",
|
| 243 |
+
current_idx,
|
| 244 |
+
records,
|
| 245 |
+
)
|
| 246 |
+
|
| 247 |
+
# Create verification record
|
| 248 |
+
record = VerificationRecord(
|
| 249 |
+
message_id=current_message.message_id,
|
| 250 |
+
original_message=current_message.text,
|
| 251 |
+
classifier_decision=current_message.pre_classified_label,
|
| 252 |
+
classifier_confidence=0.85,
|
| 253 |
+
classifier_indicators=["Distress indicator 1", "Distress indicator 2"],
|
| 254 |
+
ground_truth_label=current_message.pre_classified_label,
|
| 255 |
+
verifier_notes="",
|
| 256 |
+
is_correct=True,
|
| 257 |
+
)
|
| 258 |
+
|
| 259 |
+
# Add to session
|
| 260 |
+
session.verifications.append(record)
|
| 261 |
+
session.verified_count += 1
|
| 262 |
+
session.correct_count += 1
|
| 263 |
+
|
| 264 |
+
# Save session
|
| 265 |
+
store.save_session(session)
|
| 266 |
+
|
| 267 |
+
# Move to next message
|
| 268 |
+
next_idx = current_idx + 1
|
| 269 |
+
|
| 270 |
+
if next_idx >= len(message_queue):
|
| 271 |
+
# Session complete
|
| 272 |
+
session.is_complete = True
|
| 273 |
+
session.completed_at = datetime.now()
|
| 274 |
+
store.save_session(session)
|
| 275 |
+
|
| 276 |
+
correct_str, incorrect_str, accuracy_str = VerificationUIComponents.update_statistics_display(
|
| 277 |
+
session.correct_count,
|
| 278 |
+
session.incorrect_count
|
| 279 |
+
)
|
| 280 |
+
|
| 281 |
+
return (
|
| 282 |
+
session,
|
| 283 |
+
"β
Verification complete!",
|
| 284 |
+
"", "", "", "",
|
| 285 |
+
"",
|
| 286 |
+
correct_str,
|
| 287 |
+
incorrect_str,
|
| 288 |
+
accuracy_str,
|
| 289 |
+
next_idx,
|
| 290 |
+
[r.to_dict() for r in session.verifications],
|
| 291 |
+
)
|
| 292 |
+
else:
|
| 293 |
+
# Load next message
|
| 294 |
+
next_message = next((m for m in dataset.messages if m.message_id == message_queue[next_idx]), None)
|
| 295 |
+
if next_message:
|
| 296 |
+
message_text, decision_badge, confidence, indicators = VerificationUIComponents.render_message_review(
|
| 297 |
+
next_message,
|
| 298 |
+
next_message.pre_classified_label,
|
| 299 |
+
0.85,
|
| 300 |
+
["Distress indicator 1", "Distress indicator 2"]
|
| 301 |
+
)
|
| 302 |
+
|
| 303 |
+
progress = VerificationUIComponents.update_progress_display(next_idx, len(message_queue))
|
| 304 |
+
correct_str, incorrect_str, accuracy_str = VerificationUIComponents.update_statistics_display(
|
| 305 |
+
session.correct_count,
|
| 306 |
+
session.incorrect_count
|
| 307 |
+
)
|
| 308 |
+
|
| 309 |
+
return (
|
| 310 |
+
session,
|
| 311 |
+
"",
|
| 312 |
+
message_text,
|
| 313 |
+
decision_badge,
|
| 314 |
+
confidence,
|
| 315 |
+
indicators,
|
| 316 |
+
progress,
|
| 317 |
+
correct_str,
|
| 318 |
+
incorrect_str,
|
| 319 |
+
accuracy_str,
|
| 320 |
+
next_idx,
|
| 321 |
+
[r.to_dict() for r in session.verifications],
|
| 322 |
+
)
|
| 323 |
+
|
| 324 |
+
return (
|
| 325 |
+
session,
|
| 326 |
+
"β Error processing feedback",
|
| 327 |
+
"", "", "", "",
|
| 328 |
+
"",
|
| 329 |
+
"β Correct: 0",
|
| 330 |
+
"β Incorrect: 0",
|
| 331 |
+
"π Accuracy: 0%",
|
| 332 |
+
current_idx,
|
| 333 |
+
records,
|
| 334 |
+
)
|
| 335 |
+
|
| 336 |
+
except Exception as e:
|
| 337 |
+
return (
|
| 338 |
+
session,
|
| 339 |
+
f"β Error: {str(e)}",
|
| 340 |
+
"", "", "", "",
|
| 341 |
+
"",
|
| 342 |
+
"β Correct: 0",
|
| 343 |
+
"β Incorrect: 0",
|
| 344 |
+
"π Accuracy: 0%",
|
| 345 |
+
current_idx,
|
| 346 |
+
records,
|
| 347 |
+
)
|
| 348 |
+
|
| 349 |
+
def handle_incorrect_feedback(session: VerificationSession, current_idx: int, dataset_id: str, message_queue: List[str], records: List[dict]):
|
| 350 |
+
"""Show correction selector."""
|
| 351 |
+
return "β Please select the correct classification below"
|
| 352 |
+
|
| 353 |
+
def handle_submit_correction(session: VerificationSession, current_idx: int, dataset_id: str, message_queue: List[str], records: List[dict], correction: str, notes: str, store: JSONVerificationStore):
|
| 354 |
+
"""Handle correction submission."""
|
| 355 |
+
try:
|
| 356 |
+
if not correction:
|
| 357 |
+
return (
|
| 358 |
+
"β Please select a correction before submitting",
|
| 359 |
+
session,
|
| 360 |
+
current_idx,
|
| 361 |
+
dataset_id,
|
| 362 |
+
message_queue,
|
| 363 |
+
records,
|
| 364 |
+
"", "", "", "",
|
| 365 |
+
"",
|
| 366 |
+
"β Correct: 0",
|
| 367 |
+
"β Incorrect: 0",
|
| 368 |
+
"π Accuracy: 0%",
|
| 369 |
+
"",
|
| 370 |
+
"",
|
| 371 |
+
)
|
| 372 |
+
|
| 373 |
+
# Get current message
|
| 374 |
+
dataset = TestDatasetManager.load_dataset(dataset_id)
|
| 375 |
+
current_message_id = message_queue[current_idx]
|
| 376 |
+
current_message = next((m for m in dataset.messages if m.message_id == current_message_id), None)
|
| 377 |
+
|
| 378 |
+
if not current_message:
|
| 379 |
+
return (
|
| 380 |
+
"β Error: Message not found",
|
| 381 |
+
session,
|
| 382 |
+
current_idx,
|
| 383 |
+
dataset_id,
|
| 384 |
+
message_queue,
|
| 385 |
+
records,
|
| 386 |
+
"", "", "", "",
|
| 387 |
+
"",
|
| 388 |
+
"β Correct: 0",
|
| 389 |
+
"β Incorrect: 0",
|
| 390 |
+
"π Accuracy: 0%",
|
| 391 |
+
"",
|
| 392 |
+
"",
|
| 393 |
+
)
|
| 394 |
+
|
| 395 |
+
# Create verification record
|
| 396 |
+
record = VerificationRecord(
|
| 397 |
+
message_id=current_message.message_id,
|
| 398 |
+
original_message=current_message.text,
|
| 399 |
+
classifier_decision=current_message.pre_classified_label,
|
| 400 |
+
classifier_confidence=0.85,
|
| 401 |
+
classifier_indicators=["Distress indicator 1", "Distress indicator 2"],
|
| 402 |
+
ground_truth_label=correction,
|
| 403 |
+
verifier_notes=notes,
|
| 404 |
+
is_correct=current_message.pre_classified_label == correction,
|
| 405 |
+
)
|
| 406 |
+
|
| 407 |
+
# Add to session
|
| 408 |
+
session.verifications.append(record)
|
| 409 |
+
session.verified_count += 1
|
| 410 |
+
if record.is_correct:
|
| 411 |
+
session.correct_count += 1
|
| 412 |
+
else:
|
| 413 |
+
session.incorrect_count += 1
|
| 414 |
+
|
| 415 |
+
# Save session
|
| 416 |
+
store.save_session(session)
|
| 417 |
+
|
| 418 |
+
# Move to next message
|
| 419 |
+
next_idx = current_idx + 1
|
| 420 |
+
|
| 421 |
+
if next_idx >= len(message_queue):
|
| 422 |
+
# Session complete
|
| 423 |
+
session.is_complete = True
|
| 424 |
+
session.completed_at = datetime.now()
|
| 425 |
+
store.save_session(session)
|
| 426 |
+
|
| 427 |
+
correct_str, incorrect_str, accuracy_str = VerificationUIComponents.update_statistics_display(
|
| 428 |
+
session.correct_count,
|
| 429 |
+
session.incorrect_count
|
| 430 |
+
)
|
| 431 |
+
|
| 432 |
+
summary = VerificationUIComponents.render_summary_card(session, session.verifications)
|
| 433 |
+
|
| 434 |
+
return (
|
| 435 |
+
"β
Verification complete!",
|
| 436 |
+
session,
|
| 437 |
+
next_idx,
|
| 438 |
+
dataset_id,
|
| 439 |
+
message_queue,
|
| 440 |
+
[r.to_dict() for r in session.verifications],
|
| 441 |
+
"", "", "", "",
|
| 442 |
+
"",
|
| 443 |
+
correct_str,
|
| 444 |
+
incorrect_str,
|
| 445 |
+
accuracy_str,
|
| 446 |
+
"",
|
| 447 |
+
summary,
|
| 448 |
+
)
|
| 449 |
+
else:
|
| 450 |
+
# Load next message
|
| 451 |
+
next_message = next((m for m in dataset.messages if m.message_id == message_queue[next_idx]), None)
|
| 452 |
+
if next_message:
|
| 453 |
+
message_text, decision_badge, confidence, indicators = VerificationUIComponents.render_message_review(
|
| 454 |
+
next_message,
|
| 455 |
+
next_message.pre_classified_label,
|
| 456 |
+
0.85,
|
| 457 |
+
["Distress indicator 1", "Distress indicator 2"]
|
| 458 |
+
)
|
| 459 |
+
|
| 460 |
+
progress = VerificationUIComponents.update_progress_display(next_idx, len(message_queue))
|
| 461 |
+
correct_str, incorrect_str, accuracy_str = VerificationUIComponents.update_statistics_display(
|
| 462 |
+
session.correct_count,
|
| 463 |
+
session.incorrect_count
|
| 464 |
+
)
|
| 465 |
+
|
| 466 |
+
return (
|
| 467 |
+
"",
|
| 468 |
+
session,
|
| 469 |
+
next_idx,
|
| 470 |
+
dataset_id,
|
| 471 |
+
message_queue,
|
| 472 |
+
[r.to_dict() for r in session.verifications],
|
| 473 |
+
message_text,
|
| 474 |
+
decision_badge,
|
| 475 |
+
confidence,
|
| 476 |
+
indicators,
|
| 477 |
+
progress,
|
| 478 |
+
correct_str,
|
| 479 |
+
incorrect_str,
|
| 480 |
+
accuracy_str,
|
| 481 |
+
"",
|
| 482 |
+
"",
|
| 483 |
+
)
|
| 484 |
+
|
| 485 |
+
return (
|
| 486 |
+
"β Error processing correction",
|
| 487 |
+
session,
|
| 488 |
+
current_idx,
|
| 489 |
+
dataset_id,
|
| 490 |
+
message_queue,
|
| 491 |
+
records,
|
| 492 |
+
"", "", "", "",
|
| 493 |
+
"",
|
| 494 |
+
"β Correct: 0",
|
| 495 |
+
"β Incorrect: 0",
|
| 496 |
+
"π Accuracy: 0%",
|
| 497 |
+
"",
|
| 498 |
+
"",
|
| 499 |
+
)
|
| 500 |
+
|
| 501 |
+
except Exception as e:
|
| 502 |
+
return (
|
| 503 |
+
f"β Error: {str(e)}",
|
| 504 |
+
session,
|
| 505 |
+
current_idx,
|
| 506 |
+
dataset_id,
|
| 507 |
+
message_queue,
|
| 508 |
+
records,
|
| 509 |
+
"", "", "", "",
|
| 510 |
+
"",
|
| 511 |
+
"β Correct: 0",
|
| 512 |
+
"β Incorrect: 0",
|
| 513 |
+
"π Accuracy: 0%",
|
| 514 |
+
"",
|
| 515 |
+
"",
|
| 516 |
+
)
|
| 517 |
+
|
| 518 |
+
def handle_download_csv(session: VerificationSession, store: JSONVerificationStore):
|
| 519 |
+
"""Handle CSV download - returns file path for DownloadButton."""
|
| 520 |
+
try:
|
| 521 |
+
if not session or session.verified_count == 0:
|
| 522 |
+
return None
|
| 523 |
+
|
| 524 |
+
csv_content = VerificationCSVExporter.generate_csv_content(session)
|
| 525 |
+
filename = VerificationCSVExporter.generate_csv_filename()
|
| 526 |
+
|
| 527 |
+
import tempfile
|
| 528 |
+
|
| 529 |
+
# Use temp directory for Hugging Face compatibility
|
| 530 |
+
temp_dir = tempfile.gettempdir()
|
| 531 |
+
file_path = os.path.join(temp_dir, filename)
|
| 532 |
+
|
| 533 |
+
with open(file_path, 'w', encoding='utf-8') as f:
|
| 534 |
+
f.write(csv_content)
|
| 535 |
+
|
| 536 |
+
return file_path
|
| 537 |
+
|
| 538 |
+
except Exception as e:
|
| 539 |
+
print(f"CSV Export Error: {traceback.format_exc()}")
|
| 540 |
+
return None
|
| 541 |
+
|
| 542 |
+
def handle_next_message(session: VerificationSession, current_idx: int, dataset_id: str, message_queue: List[str], records: List[dict]):
|
| 543 |
+
"""Move to next message."""
|
| 544 |
+
if not session or current_idx >= len(message_queue) - 1:
|
| 545 |
+
return (
|
| 546 |
+
session,
|
| 547 |
+
"β No more messages",
|
| 548 |
+
"", "", "", "",
|
| 549 |
+
"",
|
| 550 |
+
"β Correct: 0",
|
| 551 |
+
"β Incorrect: 0",
|
| 552 |
+
"π Accuracy: 0%",
|
| 553 |
+
current_idx,
|
| 554 |
+
records,
|
| 555 |
+
)
|
| 556 |
+
|
| 557 |
+
next_idx = current_idx + 1
|
| 558 |
+
dataset = TestDatasetManager.load_dataset(dataset_id)
|
| 559 |
+
next_message = next((m for m in dataset.messages if m.message_id == message_queue[next_idx]), None)
|
| 560 |
+
|
| 561 |
+
if next_message:
|
| 562 |
+
message_text, decision_badge, confidence, indicators = VerificationUIComponents.render_message_review(
|
| 563 |
+
next_message,
|
| 564 |
+
next_message.pre_classified_label,
|
| 565 |
+
0.85,
|
| 566 |
+
["Distress indicator 1", "Distress indicator 2"]
|
| 567 |
+
)
|
| 568 |
+
|
| 569 |
+
progress = VerificationUIComponents.update_progress_display(next_idx, len(message_queue))
|
| 570 |
+
correct_str, incorrect_str, accuracy_str = VerificationUIComponents.update_statistics_display(
|
| 571 |
+
session.correct_count,
|
| 572 |
+
session.incorrect_count
|
| 573 |
+
)
|
| 574 |
+
|
| 575 |
+
return (
|
| 576 |
+
session,
|
| 577 |
+
"",
|
| 578 |
+
message_text,
|
| 579 |
+
decision_badge,
|
| 580 |
+
confidence,
|
| 581 |
+
indicators,
|
| 582 |
+
progress,
|
| 583 |
+
correct_str,
|
| 584 |
+
incorrect_str,
|
| 585 |
+
accuracy_str,
|
| 586 |
+
next_idx,
|
| 587 |
+
records,
|
| 588 |
+
)
|
| 589 |
+
|
| 590 |
+
return (
|
| 591 |
+
session,
|
| 592 |
+
"β Error loading next message",
|
| 593 |
+
"", "", "", "",
|
| 594 |
+
"",
|
| 595 |
+
"β Correct: 0",
|
| 596 |
+
"β Incorrect: 0",
|
| 597 |
+
"π Accuracy: 0%",
|
| 598 |
+
current_idx,
|
| 599 |
+
records,
|
| 600 |
+
)
|
| 601 |
+
|
| 602 |
+
def handle_previous_message(session: VerificationSession, current_idx: int, dataset_id: str, message_queue: List[str], records: List[dict]):
|
| 603 |
+
"""Move to previous message."""
|
| 604 |
+
if not session or current_idx <= 0:
|
| 605 |
+
return (
|
| 606 |
+
session,
|
| 607 |
+
"β No previous messages",
|
| 608 |
+
"", "", "", "",
|
| 609 |
+
"",
|
| 610 |
+
"β Correct: 0",
|
| 611 |
+
"β Incorrect: 0",
|
| 612 |
+
"π Accuracy: 0%",
|
| 613 |
+
current_idx,
|
| 614 |
+
records,
|
| 615 |
+
)
|
| 616 |
+
|
| 617 |
+
prev_idx = current_idx - 1
|
| 618 |
+
dataset = TestDatasetManager.load_dataset(dataset_id)
|
| 619 |
+
prev_message = next((m for m in dataset.messages if m.message_id == message_queue[prev_idx]), None)
|
| 620 |
+
|
| 621 |
+
if prev_message:
|
| 622 |
+
message_text, decision_badge, confidence, indicators = VerificationUIComponents.render_message_review(
|
| 623 |
+
prev_message,
|
| 624 |
+
prev_message.pre_classified_label,
|
| 625 |
+
0.85,
|
| 626 |
+
["Distress indicator 1", "Distress indicator 2"]
|
| 627 |
+
)
|
| 628 |
+
|
| 629 |
+
progress = VerificationUIComponents.update_progress_display(prev_idx, len(message_queue))
|
| 630 |
+
correct_str, incorrect_str, accuracy_str = VerificationUIComponents.update_statistics_display(
|
| 631 |
+
session.correct_count,
|
| 632 |
+
session.incorrect_count
|
| 633 |
+
)
|
| 634 |
+
|
| 635 |
+
return (
|
| 636 |
+
session,
|
| 637 |
+
"",
|
| 638 |
+
message_text,
|
| 639 |
+
decision_badge,
|
| 640 |
+
confidence,
|
| 641 |
+
indicators,
|
| 642 |
+
progress,
|
| 643 |
+
correct_str,
|
| 644 |
+
incorrect_str,
|
| 645 |
+
accuracy_str,
|
| 646 |
+
prev_idx,
|
| 647 |
+
records,
|
| 648 |
+
)
|
| 649 |
+
|
| 650 |
+
return (
|
| 651 |
+
session,
|
| 652 |
+
"β Error loading previous message",
|
| 653 |
+
"", "", "", "",
|
| 654 |
+
"",
|
| 655 |
+
"β Correct: 0",
|
| 656 |
+
"β Incorrect: 0",
|
| 657 |
+
"π Accuracy: 0%",
|
| 658 |
+
current_idx,
|
| 659 |
+
records,
|
| 660 |
+
)
|
| 661 |
+
|
| 662 |
+
def handle_skip_message(session: VerificationSession, current_idx: int, dataset_id: str, message_queue: List[str], records: List[dict]):
|
| 663 |
+
"""Skip current message and move to next."""
|
| 664 |
+
return handle_next_message(session, current_idx, dataset_id, message_queue, records)
|
| 665 |
+
|
| 666 |
+
def handle_clear_session():
|
| 667 |
+
"""Clear current verification session."""
|
| 668 |
+
return (
|
| 669 |
+
None, # verification_session
|
| 670 |
+
"β
Session cleared", # error_message
|
| 671 |
+
"", "", "", "", # message components
|
| 672 |
+
"", # progress
|
| 673 |
+
"β Correct: 0", # correct count
|
| 674 |
+
"β Incorrect: 0", # incorrect count
|
| 675 |
+
"π Accuracy: 0%", # accuracy
|
| 676 |
+
0, # current index
|
| 677 |
+
[], # records
|
| 678 |
+
)
|
| 679 |
+
|
| 680 |
+
def show_chaplain_feedback_section():
|
| 681 |
+
"""Show chaplain feedback section after message review."""
|
| 682 |
+
return gr.Row(visible=True)
|
| 683 |
+
|
| 684 |
+
def handle_submit_feedback(
|
| 685 |
+
classification_correct: bool,
|
| 686 |
+
classification_subcategory: Optional[str],
|
| 687 |
+
correct_classification: Optional[str],
|
| 688 |
+
question_issues: List[str],
|
| 689 |
+
question_comments: str,
|
| 690 |
+
referral_issues: List[str],
|
| 691 |
+
referral_comments: str,
|
| 692 |
+
indicator_issues: str,
|
| 693 |
+
indicator_comments: str,
|
| 694 |
+
general_notes: str,
|
| 695 |
+
session: VerificationSession,
|
| 696 |
+
current_idx: int,
|
| 697 |
+
message_queue: List[str],
|
| 698 |
+
):
|
| 699 |
+
"""Handle chaplain feedback submission."""
|
| 700 |
+
try:
|
| 701 |
+
if not session or current_idx >= len(message_queue):
|
| 702 |
+
return "β Error: Invalid session state", session, current_idx
|
| 703 |
+
|
| 704 |
+
current_message_id = message_queue[current_idx]
|
| 705 |
+
|
| 706 |
+
tagging_record = TaggingRecord(
|
| 707 |
+
record_id=str(uuid.uuid4()),
|
| 708 |
+
message_id=current_message_id,
|
| 709 |
+
is_classification_correct=classification_correct,
|
| 710 |
+
classification_subcategory=classification_subcategory,
|
| 711 |
+
correct_classification=correct_classification,
|
| 712 |
+
question_issues=question_issues or [],
|
| 713 |
+
question_comments=question_comments,
|
| 714 |
+
referral_issues=referral_issues or [],
|
| 715 |
+
referral_comments=referral_comments,
|
| 716 |
+
indicator_issues=[i.strip() for i in indicator_issues.split(",") if i.strip()],
|
| 717 |
+
indicator_comments=indicator_comments,
|
| 718 |
+
general_notes=general_notes,
|
| 719 |
+
)
|
| 720 |
+
|
| 721 |
+
# Store tagging record in session (would need to extend VerificationSession)
|
| 722 |
+
# For now, just confirm submission
|
| 723 |
+
success_msg = f"β
Feedback submitted for message {current_idx + 1}"
|
| 724 |
+
|
| 725 |
+
return success_msg, session, current_idx
|
| 726 |
+
|
| 727 |
+
except Exception as e:
|
| 728 |
+
return f"β Error: {str(e)}", session, current_idx
|
| 729 |
+
|
| 730 |
+
def display_classification_flow(flow_result: Optional[ClassificationFlowResult]):
|
| 731 |
+
"""Display classification flow result."""
|
| 732 |
+
if not flow_result:
|
| 733 |
+
return "", "", "", ""
|
| 734 |
+
|
| 735 |
+
badge, explanation, content, indicators = ChaplainFeedbackUIComponents.render_classification_flow(flow_result)
|
| 736 |
+
return badge, explanation, content, indicators
|
| 737 |
+
|
| 738 |
+
def _download_latest_verification_json(session: SimplifiedSessionData):
|
| 739 |
+
"""Return the most recently exported verification session JSON path (if present)."""
|
| 740 |
+
# open_verification_window exports into ./verification_sessions
|
| 741 |
+
import glob
|
| 742 |
+
|
| 743 |
+
export_dir = os.path.join(os.getcwd(), "verification_sessions")
|
| 744 |
+
if not os.path.isdir(export_dir):
|
| 745 |
+
return None
|
| 746 |
+
|
| 747 |
+
candidates = sorted(
|
| 748 |
+
glob.glob(os.path.join(export_dir, "verification_session_*.json")),
|
| 749 |
+
key=lambda p: os.path.getmtime(p),
|
| 750 |
+
reverse=True,
|
| 751 |
+
)
|
| 752 |
+
return candidates[0] if candidates else None
|
| 753 |
+
|
| 754 |
+
def _render_conv_exchange(records: list, index: int):
|
| 755 |
+
if not records:
|
| 756 |
+
return "", "", ""
|
| 757 |
+
index = max(0, min(index, len(records) - 1))
|
| 758 |
+
r = records[index]
|
| 759 |
+
# Reuse renderer from conversation_verification_ui to keep style consistent
|
| 760 |
+
from src.interface.conversation_verification_ui import VerificationInterface
|
| 761 |
+
|
| 762 |
+
vi = VerificationInterface(ConversationVerificationManager())
|
| 763 |
+
# If we already have dicts, build a lightweight VerificationRecord
|
| 764 |
+
if isinstance(r, dict):
|
| 765 |
+
rec = ConvVerificationRecord(
|
| 766 |
+
exchange_id=r.get("exchange_id") or r.get("record_id", ""),
|
| 767 |
+
exchange_number=r.get("exchange_number", 0),
|
| 768 |
+
user_message=r.get("user_message", ""),
|
| 769 |
+
assistant_response=r.get("assistant_response", ""),
|
| 770 |
+
original_classification=r.get("original_classification", ""),
|
| 771 |
+
original_confidence=r.get("original_confidence", 0.0),
|
| 772 |
+
original_indicators=r.get("original_indicators", []),
|
| 773 |
+
original_reasoning=r.get("original_reasoning", ""),
|
| 774 |
+
timestamp=r.get("timestamp"),
|
| 775 |
+
is_correct=r.get("is_correct"),
|
| 776 |
+
correct_classification=r.get("correct_classification"),
|
| 777 |
+
correction_reason=r.get("correction_reason"),
|
| 778 |
+
verifier_notes=r.get("verifier_notes"),
|
| 779 |
+
)
|
| 780 |
+
else:
|
| 781 |
+
rec = r
|
| 782 |
+
html = vi._render_exchange_review(rec)
|
| 783 |
+
# status badge
|
| 784 |
+
cur_is_correct = (r.get("is_correct") if isinstance(r, dict) else getattr(r, "is_correct", None))
|
| 785 |
+
if cur_is_correct is True:
|
| 786 |
+
badge = "β
"
|
| 787 |
+
elif cur_is_correct is False:
|
| 788 |
+
badge = "β"
|
| 789 |
+
else:
|
| 790 |
+
badge = "β³"
|
| 791 |
+
pos = f"### {badge} Exchange {index + 1} of {len(records)}"
|
| 792 |
+
|
| 793 |
+
# richer stats
|
| 794 |
+
reviewed = 0
|
| 795 |
+
correct = 0
|
| 796 |
+
incorrect = 0
|
| 797 |
+
incorrect_with_comment = 0
|
| 798 |
+
corrections = {} # Track classification corrections
|
| 799 |
+
|
| 800 |
+
for x in records:
|
| 801 |
+
v = (x.get("is_correct") if isinstance(x, dict) else getattr(x, "is_correct", None))
|
| 802 |
+
if v is None:
|
| 803 |
+
continue
|
| 804 |
+
reviewed += 1
|
| 805 |
+
if v is True:
|
| 806 |
+
correct += 1
|
| 807 |
+
else:
|
| 808 |
+
incorrect += 1
|
| 809 |
+
note = (x.get("verifier_notes") if isinstance(x, dict) else getattr(x, "verifier_notes", None))
|
| 810 |
+
if note and str(note).strip():
|
| 811 |
+
incorrect_with_comment += 1
|
| 812 |
+
|
| 813 |
+
# Track classification corrections
|
| 814 |
+
original_class = (x.get("original_classification") if isinstance(x, dict) else getattr(x, "original_classification", ""))
|
| 815 |
+
correct_class = (x.get("correct_classification") if isinstance(x, dict) else getattr(x, "correct_classification", None))
|
| 816 |
+
if original_class and correct_class:
|
| 817 |
+
correction_key = f"{original_class}β{correct_class}"
|
| 818 |
+
corrections[correction_key] = corrections.get(correction_key, 0) + 1
|
| 819 |
+
|
| 820 |
+
stats_parts = [
|
| 821 |
+
f"<div><strong>Reviewed:</strong> {reviewed}/{len(records)}</div>",
|
| 822 |
+
f"<div><strong>β
Correct:</strong> {correct}</div>",
|
| 823 |
+
f"<div><strong>β Incorrect:</strong> {incorrect}</div>",
|
| 824 |
+
f"<div><strong>π Incorrect w/ comment:</strong> {incorrect_with_comment}</div>"
|
| 825 |
+
]
|
| 826 |
+
|
| 827 |
+
# Add correction breakdown if any corrections exist
|
| 828 |
+
if corrections:
|
| 829 |
+
correction_text = ", ".join([f"{k}: {v}" for k, v in corrections.items()])
|
| 830 |
+
stats_parts.append(f"<div><strong>π Corrections:</strong> {correction_text}</div>")
|
| 831 |
+
|
| 832 |
+
stats = (
|
| 833 |
+
"<div style='display:flex; gap:12px; flex-wrap:wrap;'>"
|
| 834 |
+
+ "".join(stats_parts) +
|
| 835 |
+
"</div>"
|
| 836 |
+
)
|
| 837 |
+
return html, pos, stats
|
| 838 |
+
|
| 839 |
+
def _comment_ui_state(records: list, idx: int):
|
| 840 |
+
"""Return (row_update, note_value) based on current record state."""
|
| 841 |
+
if not records:
|
| 842 |
+
return gr.update(visible=False), ""
|
| 843 |
+
idx = max(0, min(idx, len(records) - 1))
|
| 844 |
+
r = records[idx]
|
| 845 |
+
is_incorrect = (r.get("is_correct") is False) if isinstance(r, dict) else (getattr(r, "is_correct", None) is False)
|
| 846 |
+
if not is_incorrect:
|
| 847 |
+
return gr.update(visible=False), ""
|
| 848 |
+
note = (r.get("verifier_notes") or "") if isinstance(r, dict) else (getattr(r, "verifier_notes", "") or "")
|
| 849 |
+
return gr.update(visible=True), str(note)
|
| 850 |
+
|
| 851 |
+
def _export_conv_records_to_json(meta: dict, records: list):
|
| 852 |
+
"""Write reviewed conversation verification results to a JSON file and return its path."""
|
| 853 |
+
import json
|
| 854 |
+
import os
|
| 855 |
+
from datetime import datetime
|
| 856 |
+
|
| 857 |
+
export_dir = os.path.join(os.getcwd(), "verification_sessions")
|
| 858 |
+
os.makedirs(export_dir, exist_ok=True)
|
| 859 |
+
|
| 860 |
+
session_id = (meta or {}).get("session_id") or "conversation_verification"
|
| 861 |
+
export_filename = f"conversation_verification_reviewed_{datetime.utcnow().strftime('%Y%m%d_%H%M%S')}_{session_id}.json"
|
| 862 |
+
export_path = os.path.join(export_dir, export_filename)
|
| 863 |
+
|
| 864 |
+
payload = {
|
| 865 |
+
**(meta or {}),
|
| 866 |
+
"verification_records": records or [],
|
| 867 |
+
}
|
| 868 |
+
|
| 869 |
+
with open(export_path, "w", encoding="utf-8") as f:
|
| 870 |
+
json.dump(payload, f, ensure_ascii=False, indent=2, default=str)
|
| 871 |
+
return export_path
|
| 872 |
+
|
| 873 |
+
def _export_conv_records_to_csv(meta: dict, records: list):
|
| 874 |
+
"""Write reviewed conversation verification results to a CSV file and return its path."""
|
| 875 |
+
import csv
|
| 876 |
+
import os
|
| 877 |
+
from datetime import datetime
|
| 878 |
+
|
| 879 |
+
export_dir = os.path.join(os.getcwd(), "verification_exports")
|
| 880 |
+
os.makedirs(export_dir, exist_ok=True)
|
| 881 |
+
|
| 882 |
+
session_id = (meta or {}).get("session_id") or "conversation_verification"
|
| 883 |
+
export_filename = f"conversation_verification_reviewed_{datetime.utcnow().strftime('%Y%m%d_%H%M%S')}_{session_id}.csv"
|
| 884 |
+
export_path = os.path.join(export_dir, export_filename)
|
| 885 |
+
|
| 886 |
+
fieldnames = [
|
| 887 |
+
"session_id",
|
| 888 |
+
"patient_name",
|
| 889 |
+
"patient_phone",
|
| 890 |
+
"verifier_name",
|
| 891 |
+
"start_time",
|
| 892 |
+
"exchange_number",
|
| 893 |
+
"exchange_id",
|
| 894 |
+
"original_classification",
|
| 895 |
+
"original_confidence",
|
| 896 |
+
"is_correct",
|
| 897 |
+
"correct_classification",
|
| 898 |
+
"verifier_notes",
|
| 899 |
+
"user_message",
|
| 900 |
+
"assistant_response",
|
| 901 |
+
"provider_summary",
|
| 902 |
+
]
|
| 903 |
+
|
| 904 |
+
with open(export_path, "w", encoding="utf-8", newline="") as f:
|
| 905 |
+
w = csv.DictWriter(f, fieldnames=fieldnames)
|
| 906 |
+
w.writeheader()
|
| 907 |
+
for r in records or []:
|
| 908 |
+
# Include provider_summary only for RED cases
|
| 909 |
+
provider_summary = ""
|
| 910 |
+
if r.get("original_classification", "").upper() == "RED":
|
| 911 |
+
provider_summary = r.get("provider_summary") or ""
|
| 912 |
+
|
| 913 |
+
row = {
|
| 914 |
+
"session_id": (meta or {}).get("session_id"),
|
| 915 |
+
"patient_name": (meta or {}).get("patient_name"),
|
| 916 |
+
"patient_phone": (meta or {}).get("patient_phone") or "",
|
| 917 |
+
"verifier_name": (meta or {}).get("verifier_name"),
|
| 918 |
+
"start_time": (meta or {}).get("start_time"),
|
| 919 |
+
"exchange_number": r.get("exchange_number"),
|
| 920 |
+
"exchange_id": r.get("exchange_id") or r.get("record_id"),
|
| 921 |
+
"original_classification": r.get("original_classification"),
|
| 922 |
+
"original_confidence": r.get("original_confidence"),
|
| 923 |
+
"is_correct": r.get("is_correct"),
|
| 924 |
+
"correct_classification": r.get("correct_classification") or "",
|
| 925 |
+
"verifier_notes": r.get("verifier_notes") or "",
|
| 926 |
+
"user_message": r.get("user_message"),
|
| 927 |
+
"assistant_response": r.get("assistant_response"),
|
| 928 |
+
"provider_summary": provider_summary,
|
| 929 |
+
}
|
| 930 |
+
w.writerow(row)
|
| 931 |
+
return export_path
|
| 932 |
+
|
| 933 |
+
def _generate_conv_verification(session: SimplifiedSessionData):
|
| 934 |
+
if session is None or not hasattr(session.app_instance, "conversation_logger"):
|
| 935 |
+
return None, [], 0, "β No session/conversation found", "", ""
|
| 936 |
+
if not session.app_instance.conversation_logger.entries:
|
| 937 |
+
return None, [], 0, "β οΈ No exchanges to verify yet", "", ""
|
| 938 |
+
|
| 939 |
+
manager = ConversationVerificationManager()
|
| 940 |
+
vs = manager.create_verification_session(session.app_instance.conversation_logger, "Medical Professional")
|
| 941 |
+
|
| 942 |
+
# Get patient phone from app if available
|
| 943 |
+
patient_phone = ""
|
| 944 |
+
if hasattr(session.app_instance, 'patient_info'):
|
| 945 |
+
patient_phone = session.app_instance.patient_info.get("phone") or ""
|
| 946 |
+
|
| 947 |
+
meta = {
|
| 948 |
+
"session_id": vs.session_id,
|
| 949 |
+
"patient_name": vs.patient_name,
|
| 950 |
+
"patient_phone": patient_phone,
|
| 951 |
+
"verifier_name": vs.verifier_name,
|
| 952 |
+
"start_time": vs.start_time.isoformat() if hasattr(vs, "start_time") else None,
|
| 953 |
+
}
|
| 954 |
+
|
| 955 |
+
# Get provider summary if available (for RED cases)
|
| 956 |
+
provider_summary_text = ""
|
| 957 |
+
if hasattr(session.app_instance, 'get_last_provider_summary'):
|
| 958 |
+
summary = session.app_instance.get_last_provider_summary()
|
| 959 |
+
if summary and hasattr(session.app_instance, 'provider_summary_generator'):
|
| 960 |
+
provider_summary_text = session.app_instance.provider_summary_generator.format_for_export(summary)
|
| 961 |
+
|
| 962 |
+
records_as_dicts = [
|
| 963 |
+
{
|
| 964 |
+
"exchange_id": r.exchange_id,
|
| 965 |
+
"exchange_number": r.exchange_number,
|
| 966 |
+
"record_id": r.exchange_id,
|
| 967 |
+
"timestamp": r.timestamp,
|
| 968 |
+
"user_message": r.user_message,
|
| 969 |
+
"assistant_response": r.assistant_response,
|
| 970 |
+
"original_classification": r.original_classification,
|
| 971 |
+
"original_confidence": r.original_confidence,
|
| 972 |
+
"original_indicators": r.original_indicators,
|
| 973 |
+
"original_reasoning": r.original_reasoning,
|
| 974 |
+
"is_correct": r.is_correct,
|
| 975 |
+
"correct_classification": r.correct_classification,
|
| 976 |
+
"correction_reason": r.correction_reason,
|
| 977 |
+
"verifier_notes": r.verifier_notes,
|
| 978 |
+
"provider_summary": provider_summary_text if r.original_classification.upper() == "RED" else "",
|
| 979 |
+
}
|
| 980 |
+
for r in vs.verification_records
|
| 981 |
+
]
|
| 982 |
+
html, pos, stats = _render_conv_exchange(records_as_dicts, 0)
|
| 983 |
+
return meta, records_as_dicts, 0, f"β
Generated session `{vs.session_id}`", html, pos, stats
|
| 984 |
+
|
| 985 |
+
def _mark_conv_correct(records: list, idx: int):
|
| 986 |
+
if not records:
|
| 987 |
+
return records, idx, "", "", "", gr.update(visible=False), "", ""
|
| 988 |
+
idx = max(0, min(idx, len(records) - 1))
|
| 989 |
+
if isinstance(records[idx], dict):
|
| 990 |
+
records[idx]["is_correct"] = True
|
| 991 |
+
# clear comment and correct_classification when marked correct (avoid stale data)
|
| 992 |
+
records[idx]["verifier_notes"] = ""
|
| 993 |
+
records[idx]["correct_classification"] = None
|
| 994 |
+
html, pos, stats = _render_conv_exchange(records, idx)
|
| 995 |
+
row_upd, note_val = _comment_ui_state(records, idx)
|
| 996 |
+
return records, idx, "β
Marked correct", html, pos, stats, row_upd, note_val, ""
|
| 997 |
+
|
| 998 |
+
def _mark_conv_incorrect(records: list, idx: int):
|
| 999 |
+
if not records:
|
| 1000 |
+
return records, idx, "", "", "", gr.update(visible=False), "", ""
|
| 1001 |
+
idx = max(0, min(idx, len(records) - 1))
|
| 1002 |
+
if isinstance(records[idx], dict):
|
| 1003 |
+
records[idx]["is_correct"] = False
|
| 1004 |
+
html, pos, stats = _render_conv_exchange(records, idx)
|
| 1005 |
+
row_upd, note_val = _comment_ui_state(records, idx)
|
| 1006 |
+
# Get existing correct_classification if any
|
| 1007 |
+
existing_classification = ""
|
| 1008 |
+
if isinstance(records[idx], dict):
|
| 1009 |
+
correct_class = records[idx].get("correct_classification")
|
| 1010 |
+
if correct_class:
|
| 1011 |
+
# Map back to display text
|
| 1012 |
+
reverse_map = {
|
| 1013 |
+
"GREEN": "π’ Should be GREEN - No distress",
|
| 1014 |
+
"YELLOW": "π‘ Should be YELLOW - Needs clarification",
|
| 1015 |
+
"RED": "π΄ Should be RED - Spiritual distress"
|
| 1016 |
+
}
|
| 1017 |
+
existing_classification = reverse_map.get(correct_class, "")
|
| 1018 |
+
return records, idx, "β Marked incorrect", html, pos, stats, row_upd, note_val, existing_classification
|
| 1019 |
+
|
| 1020 |
+
def _show_incorrect_comment_ui(records: list, idx: int):
|
| 1021 |
+
"""Mark incorrect and open the comment row, pre-filling any existing note."""
|
| 1022 |
+
records, idx, status, html, pos, stats, _row, note, existing_classification = _mark_conv_incorrect(records, idx)
|
| 1023 |
+
return records, idx, status, html, pos, stats, gr.update(visible=True), note, existing_classification
|
| 1024 |
+
|
| 1025 |
+
def _save_incorrect_comment(records: list, idx: int, note: str, correct_classification: str):
|
| 1026 |
+
if not records:
|
| 1027 |
+
return records, idx, "", "", "", "", gr.update(visible=False), "", ""
|
| 1028 |
+
idx = max(0, min(idx, len(records) - 1))
|
| 1029 |
+
if isinstance(records[idx], dict):
|
| 1030 |
+
records[idx]["verifier_notes"] = (note or "").strip()
|
| 1031 |
+
# Map display text to classification code
|
| 1032 |
+
classification_map = {
|
| 1033 |
+
"π’ Should be GREEN - No distress": "GREEN",
|
| 1034 |
+
"π‘ Should be YELLOW - Needs clarification": "YELLOW",
|
| 1035 |
+
"π΄ Should be RED - Spiritual distress": "RED"
|
| 1036 |
+
}
|
| 1037 |
+
if correct_classification and correct_classification in classification_map:
|
| 1038 |
+
records[idx]["correct_classification"] = classification_map[correct_classification]
|
| 1039 |
+
html, pos, stats = _render_conv_exchange(records, idx)
|
| 1040 |
+
row_upd, note_val = _comment_ui_state(records, idx)
|
| 1041 |
+
# keep row visible after save (since still incorrect)
|
| 1042 |
+
return records, idx, "πΎ Comment saved", html, pos, stats, row_upd, note_val, ""
|
| 1043 |
+
|
| 1044 |
+
def _download_reviewed_json(meta: dict, records: list):
|
| 1045 |
+
return _export_conv_records_to_json(meta, records)
|
| 1046 |
+
|
| 1047 |
+
def _download_reviewed_csv(meta: dict, records: list):
|
| 1048 |
+
return _export_conv_records_to_csv(meta, records)
|
| 1049 |
+
|
| 1050 |
+
def _nav_conv(records: list, idx: int, delta: int):
|
| 1051 |
+
if not records:
|
| 1052 |
+
return idx, "", "", "", gr.update(visible=False), "", ""
|
| 1053 |
+
idx = max(0, min(idx + delta, len(records) - 1))
|
| 1054 |
+
html, pos, stats = _render_conv_exchange(records, idx)
|
| 1055 |
+
row_upd, note_val = _comment_ui_state(records, idx)
|
| 1056 |
+
# Get existing correct_classification if any
|
| 1057 |
+
existing_classification = ""
|
| 1058 |
+
if isinstance(records[idx], dict):
|
| 1059 |
+
correct_class = records[idx].get("correct_classification")
|
| 1060 |
+
if correct_class:
|
| 1061 |
+
reverse_map = {
|
| 1062 |
+
"GREEN": "π’ Should be GREEN - No distress",
|
| 1063 |
+
"YELLOW": "π‘ Should be YELLOW - Needs clarification",
|
| 1064 |
+
"RED": "π΄ Should be RED - Spiritual distress"
|
| 1065 |
+
}
|
| 1066 |
+
existing_classification = reverse_map.get(correct_class, "")
|
| 1067 |
+
return idx, html, pos, stats, row_upd, note_val, existing_classification
|