DocUA Claude Opus 5 commited on
Commit
09c4875
·
1 Parent(s): e90d802

fix: Or_6 feedback — consent questions, unrecorded verdicts, feedback types

Browse files

Findings from Alex's first reviewed test case (review/Or_6.txt).

Fixed:
- A question about the spiritual care team was scored as a refusal.
"I don't know, who are they" contains "don't"/"not", so _check_consent's
negative keyword scan matched first, _process_consent_declined closed the
topic and moved the session back to GREEN while the patient was still in an
unresolved RED. _is_consent_question now runs before _check_consent, explains
what the chaplains do, re-asks for consent and stays in AWAITING_CONSENT.
- Saving from the always-visible verification panel never set is_correct, so a
fully reviewed session exported as "0 reviewed". Picking a classification is
now the verdict itself (PROVIDER_SUMMARY step excluded, its radio is hidden).
The panel also stays open after Save so a note left on an exchange whose
classification was correct doesn't disappear.

Added:
- "Type of feedback" multi-select (wrong classification / tone / missing
information / conversation flow) in both interfaces, exported as a
feedback_types column with stable codes. An exchange can carry more than one,
which splits reviewer notes into classifier-prompt vs response-prompt fixes
without re-reading free text.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

src/core/simplified_medical_app.py CHANGED
@@ -422,20 +422,27 @@ class SimplifiedMedicalApp:
422
  def _handle_consent_response(self, message: str) -> str:
423
  """
424
  Handle patient's response to consent request.
425
-
 
426
  If patient agrees → generate referral
427
  If patient declines → return to medical dialog without referral
428
-
429
  Requirement: Or_review.md section 4.A.3
430
  """
431
  logger.info(f"Handling consent response: {message[:50]}...")
432
-
433
  # Detect language
434
  patient_language = self._detect_language(message)
435
-
 
 
 
 
 
 
436
  # Check if patient consents
437
  consent_given = self._check_consent(message)
438
-
439
  if consent_given:
440
  logger.info("Patient consented to spiritual care referral")
441
  return self._process_consent_accepted(patient_language)
@@ -443,10 +450,57 @@ class SimplifiedMedicalApp:
443
  logger.info("Patient declined spiritual care referral")
444
  return self._process_consent_declined(patient_language)
445
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
446
  def _check_consent(self, message: str) -> bool:
447
  """
448
  Check if patient's response indicates consent.
449
-
450
  Returns True if patient agrees, False if declines or unclear.
451
  """
452
  message_lower = message.lower()
 
422
  def _handle_consent_response(self, message: str) -> str:
423
  """
424
  Handle patient's response to consent request.
425
+
426
+ If patient asks about the service → explain it and keep awaiting consent
427
  If patient agrees → generate referral
428
  If patient declines → return to medical dialog without referral
429
+
430
  Requirement: Or_review.md section 4.A.3
431
  """
432
  logger.info(f"Handling consent response: {message[:50]}...")
433
+
434
  # Detect language
435
  patient_language = self._detect_language(message)
436
+
437
+ # A question about the spiritual care team is not a refusal - answer it and
438
+ # stay in AWAITING_CONSENT so the patient can still decide (Or_6.txt).
439
+ if self._is_consent_question(message):
440
+ logger.info("Patient asked about the spiritual care team - explaining service")
441
+ return self._explain_spiritual_care_service(patient_language)
442
+
443
  # Check if patient consents
444
  consent_given = self._check_consent(message)
445
+
446
  if consent_given:
447
  logger.info("Patient consented to spiritual care referral")
448
  return self._process_consent_accepted(patient_language)
 
450
  logger.info("Patient declined spiritual care referral")
451
  return self._process_consent_declined(patient_language)
452
 
453
+ def _is_consent_question(self, message: str) -> bool:
454
+ """
455
+ Check whether the patient is asking about the spiritual care team rather
456
+ than accepting or refusing the referral.
457
+
458
+ This must be checked before _check_consent: phrases like "I don't know,
459
+ who are they" contain negative keywords ("don't", "not") and would
460
+ otherwise be scored as a refusal.
461
+ """
462
+ message_lower = message.lower()
463
+
464
+ question_en = [
465
+ "who are they", "who is", "who are", "what is", "what are",
466
+ "what do they", "what would", "what does", "what's that",
467
+ "how does", "how do they", "how would", "tell me more",
468
+ "more information", "more about", "what kind of",
469
+ "i don't know", "i dont know", "not sure", "like what"
470
+ ]
471
+
472
+ question_uk = [
473
+ "хто це", "хто вони", "хто такі", "що це", "що вони",
474
+ "що саме", "як це", "як вони", "розкажіть більше",
475
+ "більше інформації", "не знаю", "не впевнен", "що це означає"
476
+ ]
477
+
478
+ return any(q in message_lower for q in question_en + question_uk)
479
+
480
+ def _explain_spiritual_care_service(self, language: str) -> str:
481
+ """
482
+ Explain what the spiritual care team does and re-ask for consent.
483
+
484
+ The session stays in AWAITING_CONSENT, so the patient's next message is
485
+ routed back through _handle_consent_response.
486
+ """
487
+ if language == "Ukrainian":
488
+ return """Звісно, поясню. Команда духовної підтримки — це капелани нашої лікарні. Вони не є медичним персоналом і не змінюють ваше лікування.
489
+
490
+ Вони просто вислухають вас і побудуть поруч — можна говорити про те, що вас турбує, про сенс, страх чи втрату. Це конфіденційно, безкоштовно, не залежить від вашої релігії чи її відсутності, і ви можете припинити будь-якої миті.
491
+
492
+ Якщо ви погодитесь, я передам їм ваше ім'я та контакт, і хтось із них зв'яжеться з вами. Чи хотіли б ви, щоб я це зробив?"""
493
+ else:
494
+ return """Of course, let me explain. The spiritual care team are the chaplains here at the hospital. They are not medical staff, and they don't change anything about your treatment.
495
+
496
+ They're simply there to listen and sit with you — you can talk about whatever is weighing on you, about meaning, fear, or loss. It's confidential, there's no cost, it doesn't depend on your religion or having one, and you can stop at any point.
497
+
498
+ If you'd like, I can share your name and contact details so someone from the team can reach out to you. Would you like me to do that?"""
499
+
500
  def _check_consent(self, message: str) -> bool:
501
  """
502
  Check if patient's response indicates consent.
503
+
504
  Returns True if patient agrees, False if declines or unclear.
505
  """
506
  message_lower = message.lower()
src/interface/gradio_app.py CHANGED
@@ -260,6 +260,11 @@ def create_simplified_interface():
260
  label="Correct Classification",
261
  interactive=True
262
  )
 
 
 
 
 
263
  conv_incorrect_comment = gr.Textbox(
264
  label="Comment (why incorrect / what to fix)",
265
  placeholder="Add a short note for this exchange...",
@@ -665,6 +670,7 @@ def create_simplified_interface():
665
  conv_verify_status, conv_verify_exchange, conv_position, conv_stats,
666
  # Show the classification/comment panel for the first exchange too
667
  conv_incorrect_comment_row, conv_incorrect_comment, conv_correct_classification,
 
668
  ]
669
  )
670
 
@@ -693,6 +699,7 @@ def create_simplified_interface():
693
  conv_incorrect_comment_row,
694
  conv_incorrect_comment,
695
  conv_correct_classification,
 
696
  ]
697
  )
698
 
@@ -709,12 +716,13 @@ def create_simplified_interface():
709
  conv_incorrect_comment_row,
710
  conv_incorrect_comment,
711
  conv_correct_classification,
 
712
  ]
713
  )
714
 
715
  conv_save_comment_btn.click(
716
  verification_handlers._save_incorrect_comment,
717
- inputs=[conv_verify_records, conv_verify_index, conv_incorrect_comment, conv_correct_classification],
718
  outputs=[
719
  conv_verify_records,
720
  conv_verify_index,
@@ -725,19 +733,20 @@ def create_simplified_interface():
725
  conv_incorrect_comment_row,
726
  conv_incorrect_comment,
727
  conv_correct_classification,
 
728
  ]
729
  )
730
 
731
  conv_prev_btn.click(
732
  lambda records, idx: verification_handlers._nav_conv(records, idx, -1),
733
  inputs=[conv_verify_records, conv_verify_index],
734
- outputs=[conv_verify_index, conv_verify_exchange, conv_position, conv_stats, conv_incorrect_comment_row, conv_incorrect_comment, conv_correct_classification]
735
  )
736
 
737
  conv_next_btn.click(
738
  lambda records, idx: verification_handlers._nav_conv(records, idx, 1),
739
  inputs=[conv_verify_records, conv_verify_index],
740
- outputs=[conv_verify_index, conv_verify_exchange, conv_position, conv_stats, conv_incorrect_comment_row, conv_incorrect_comment, conv_correct_classification]
741
  )
742
 
743
 
 
260
  label="Correct Classification",
261
  interactive=True
262
  )
263
+ conv_feedback_type = gr.CheckboxGroup(
264
+ choices=verification_handlers._FEEDBACK_TYPE_CHOICES,
265
+ label="Type of feedback (select all that apply)",
266
+ interactive=True,
267
+ )
268
  conv_incorrect_comment = gr.Textbox(
269
  label="Comment (why incorrect / what to fix)",
270
  placeholder="Add a short note for this exchange...",
 
670
  conv_verify_status, conv_verify_exchange, conv_position, conv_stats,
671
  # Show the classification/comment panel for the first exchange too
672
  conv_incorrect_comment_row, conv_incorrect_comment, conv_correct_classification,
673
+ conv_feedback_type,
674
  ]
675
  )
676
 
 
699
  conv_incorrect_comment_row,
700
  conv_incorrect_comment,
701
  conv_correct_classification,
702
+ conv_feedback_type,
703
  ]
704
  )
705
 
 
716
  conv_incorrect_comment_row,
717
  conv_incorrect_comment,
718
  conv_correct_classification,
719
+ conv_feedback_type,
720
  ]
721
  )
722
 
723
  conv_save_comment_btn.click(
724
  verification_handlers._save_incorrect_comment,
725
+ inputs=[conv_verify_records, conv_verify_index, conv_incorrect_comment, conv_correct_classification, conv_feedback_type],
726
  outputs=[
727
  conv_verify_records,
728
  conv_verify_index,
 
733
  conv_incorrect_comment_row,
734
  conv_incorrect_comment,
735
  conv_correct_classification,
736
+ conv_feedback_type,
737
  ]
738
  )
739
 
740
  conv_prev_btn.click(
741
  lambda records, idx: verification_handlers._nav_conv(records, idx, -1),
742
  inputs=[conv_verify_records, conv_verify_index],
743
+ outputs=[conv_verify_index, conv_verify_exchange, conv_position, conv_stats, conv_incorrect_comment_row, conv_incorrect_comment, conv_correct_classification, conv_feedback_type]
744
  )
745
 
746
  conv_next_btn.click(
747
  lambda records, idx: verification_handlers._nav_conv(records, idx, 1),
748
  inputs=[conv_verify_records, conv_verify_index],
749
+ outputs=[conv_verify_index, conv_verify_exchange, conv_position, conv_stats, conv_incorrect_comment_row, conv_incorrect_comment, conv_correct_classification, conv_feedback_type]
750
  )
751
 
752
 
src/interface/simplified_gradio_app.py CHANGED
@@ -177,6 +177,11 @@ def create_simplified_interface():
177
  label="Select Correct Classification",
178
  interactive=True
179
  )
 
 
 
 
 
180
  conv_incorrect_comment = gr.Textbox(
181
  label="Comment (why incorrect / what to fix)",
182
  placeholder="Add your feedback here...",
@@ -540,6 +545,7 @@ def create_simplified_interface():
540
  conv_verify_status, conv_verify_exchange, conv_position, conv_stats,
541
  # Show the classification/comment panel for the first exchange too
542
  conv_incorrect_comment_row, conv_incorrect_comment, conv_correct_classification,
 
543
  ]
544
  )
545
 
@@ -562,6 +568,7 @@ def create_simplified_interface():
562
  conv_incorrect_comment_row,
563
  conv_incorrect_comment,
564
  conv_correct_classification,
 
565
  ]
566
  )
567
 
@@ -578,12 +585,13 @@ def create_simplified_interface():
578
  conv_incorrect_comment_row,
579
  conv_incorrect_comment,
580
  conv_correct_classification,
 
581
  ]
582
  )
583
 
584
  conv_save_comment_btn.click(
585
  verification_handlers._save_incorrect_comment,
586
- inputs=[conv_verify_records, conv_verify_index, conv_incorrect_comment, conv_correct_classification],
587
  outputs=[
588
  conv_verify_records,
589
  conv_verify_index,
@@ -594,19 +602,20 @@ def create_simplified_interface():
594
  conv_incorrect_comment_row,
595
  conv_incorrect_comment,
596
  conv_correct_classification,
 
597
  ]
598
  )
599
 
600
  conv_prev_btn.click(
601
  lambda records, idx: verification_handlers._nav_conv(records, idx, -1),
602
  inputs=[conv_verify_records, conv_verify_index],
603
- outputs=[conv_verify_index, conv_verify_exchange, conv_position, conv_stats, conv_incorrect_comment_row, conv_incorrect_comment, conv_correct_classification]
604
  )
605
 
606
  conv_next_btn.click(
607
  lambda records, idx: verification_handlers._nav_conv(records, idx, 1),
608
  inputs=[conv_verify_records, conv_verify_index],
609
- outputs=[conv_verify_index, conv_verify_exchange, conv_position, conv_stats, conv_incorrect_comment_row, conv_incorrect_comment, conv_correct_classification]
610
  )
611
 
612
  # Prompt editing events
 
177
  label="Select Correct Classification",
178
  interactive=True
179
  )
180
+ conv_feedback_type = gr.CheckboxGroup(
181
+ choices=verification_handlers._FEEDBACK_TYPE_CHOICES,
182
+ label="Type of feedback (select all that apply)",
183
+ interactive=True,
184
+ )
185
  conv_incorrect_comment = gr.Textbox(
186
  label="Comment (why incorrect / what to fix)",
187
  placeholder="Add your feedback here...",
 
545
  conv_verify_status, conv_verify_exchange, conv_position, conv_stats,
546
  # Show the classification/comment panel for the first exchange too
547
  conv_incorrect_comment_row, conv_incorrect_comment, conv_correct_classification,
548
+ conv_feedback_type,
549
  ]
550
  )
551
 
 
568
  conv_incorrect_comment_row,
569
  conv_incorrect_comment,
570
  conv_correct_classification,
571
+ conv_feedback_type,
572
  ]
573
  )
574
 
 
585
  conv_incorrect_comment_row,
586
  conv_incorrect_comment,
587
  conv_correct_classification,
588
+ conv_feedback_type,
589
  ]
590
  )
591
 
592
  conv_save_comment_btn.click(
593
  verification_handlers._save_incorrect_comment,
594
+ inputs=[conv_verify_records, conv_verify_index, conv_incorrect_comment, conv_correct_classification, conv_feedback_type],
595
  outputs=[
596
  conv_verify_records,
597
  conv_verify_index,
 
602
  conv_incorrect_comment_row,
603
  conv_incorrect_comment,
604
  conv_correct_classification,
605
+ conv_feedback_type,
606
  ]
607
  )
608
 
609
  conv_prev_btn.click(
610
  lambda records, idx: verification_handlers._nav_conv(records, idx, -1),
611
  inputs=[conv_verify_records, conv_verify_index],
612
+ outputs=[conv_verify_index, conv_verify_exchange, conv_position, conv_stats, conv_incorrect_comment_row, conv_incorrect_comment, conv_correct_classification, conv_feedback_type]
613
  )
614
 
615
  conv_next_btn.click(
616
  lambda records, idx: verification_handlers._nav_conv(records, idx, 1),
617
  inputs=[conv_verify_records, conv_verify_index],
618
+ outputs=[conv_verify_index, conv_verify_exchange, conv_position, conv_stats, conv_incorrect_comment_row, conv_incorrect_comment, conv_correct_classification, conv_feedback_type]
619
  )
620
 
621
  # Prompt editing events
src/interface/verification_handlers.py CHANGED
@@ -924,6 +924,7 @@ def _export_conv_records_to_csv(meta: dict, records: list):
924
  "original_confidence",
925
  "is_correct",
926
  "correct_classification",
 
927
  "verifier_notes",
928
  "user_message",
929
  "assistant_response",
@@ -951,6 +952,7 @@ def _export_conv_records_to_csv(meta: dict, records: list):
951
  "original_confidence": r.get("original_confidence"),
952
  "is_correct": r.get("is_correct"),
953
  "correct_classification": r.get("correct_classification") or "",
 
954
  "verifier_notes": r.get("verifier_notes") or "",
955
  "user_message": r.get("user_message"),
956
  "assistant_response": r.get("assistant_response"),
@@ -961,9 +963,9 @@ def _export_conv_records_to_csv(meta: dict, records: list):
961
 
962
  def _generate_conv_verification(session: SimplifiedSessionData):
963
  if session is None or not hasattr(session.app_instance, "conversation_logger"):
964
- return None, [], 0, "❌ No session/conversation found", "", "", "", gr.update(visible=False), "", gr.update(value=None)
965
  if not session.app_instance.conversation_logger.entries:
966
- return None, [], 0, "⚠️ No exchanges to verify yet", "", "", "", gr.update(visible=False), "", gr.update(value=None)
967
 
968
  manager = ConversationVerificationManager()
969
  vs = manager.create_verification_session(session.app_instance.conversation_logger, "Medical Professional")
@@ -1004,6 +1006,7 @@ def _generate_conv_verification(session: SimplifiedSessionData):
1004
  "correct_classification": r.correct_classification,
1005
  "correction_reason": r.correction_reason,
1006
  "verifier_notes": r.verifier_notes,
 
1007
  "provider_summary": provider_summary_text if r.original_classification.upper() == "RED" else "",
1008
  }
1009
  for r in vs.verification_records
@@ -1011,9 +1014,10 @@ def _generate_conv_verification(session: SimplifiedSessionData):
1011
  html, pos, stats = _render_conv_exchange(records_as_dicts, 0)
1012
  row_upd, note_val = _comment_ui_state(records_as_dicts, 0)
1013
  radio_upd = _classification_update(records_as_dicts, 0)
 
1014
  return (
1015
  meta, records_as_dicts, 0, f"✅ Generated session `{vs.session_id}`",
1016
- html, pos, stats, row_upd, note_val, radio_upd,
1017
  )
1018
 
1019
  # Display text for each classification flag (shared by the verification handlers).
@@ -1023,6 +1027,31 @@ _CLASSIFICATION_DISPLAY = {
1023
  "RED": "🔴 Should be RED - Spiritual distress",
1024
  }
1025
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1026
  def _classification_update(records: list, idx: int):
1027
  """Build the gr.update for the 'Correct Classification' Radio.
1028
 
@@ -1041,38 +1070,42 @@ def _classification_update(records: list, idx: int):
1041
 
1042
  def _mark_conv_correct(records: list, idx: int):
1043
  if not records:
1044
- return records, idx, "", "", "", "", gr.update(visible=False), "", gr.update(value=None)
1045
  idx = max(0, min(idx, len(records) - 1))
1046
  if isinstance(records[idx], dict):
1047
  records[idx]["is_correct"] = True
1048
  # clear comment and correct_classification when marked correct (avoid stale data)
1049
  records[idx]["verifier_notes"] = ""
1050
  records[idx]["correct_classification"] = None
 
1051
  html, pos, stats = _render_conv_exchange(records, idx)
1052
  row_upd, note_val = _comment_ui_state(records, idx)
1053
- return records, idx, "✅ Marked correct", html, pos, stats, row_upd, note_val, _classification_update(records, idx)
1054
 
1055
  def _mark_conv_incorrect(records: list, idx: int):
1056
  if not records:
1057
- return records, idx, "", "", "", "", gr.update(visible=False), "", gr.update(value=None)
1058
  idx = max(0, min(idx, len(records) - 1))
1059
  if isinstance(records[idx], dict):
1060
  records[idx]["is_correct"] = False
1061
  html, pos, stats = _render_conv_exchange(records, idx)
1062
  row_upd, note_val = _comment_ui_state(records, idx)
1063
- return records, idx, "❌ Marked incorrect", html, pos, stats, row_upd, note_val, _classification_update(records, idx)
1064
 
1065
  def _show_incorrect_comment_ui(records: list, idx: int):
1066
  """Mark incorrect and open the comment row, pre-filling any existing note."""
1067
- records, idx, status, html, pos, stats, _row, note, existing_classification = _mark_conv_incorrect(records, idx)
1068
- return records, idx, status, html, pos, stats, gr.update(visible=True), note, existing_classification
1069
 
1070
- def _save_incorrect_comment(records: list, idx: int, note: str, correct_classification: str):
1071
  if not records:
1072
- return records, idx, "", "", "", "", gr.update(visible=False), "", gr.update(value=None)
1073
  idx = max(0, min(idx, len(records) - 1))
1074
  if isinstance(records[idx], dict):
1075
  records[idx]["verifier_notes"] = (note or "").strip()
 
 
 
1076
  # Map display text to classification code
1077
  classification_map = {
1078
  "🟢 Should be GREEN - No distress": "GREEN",
@@ -1080,11 +1113,19 @@ def _save_incorrect_comment(records: list, idx: int, note: str, correct_classifi
1080
  "🔴 Should be RED - Spiritual distress": "RED"
1081
  }
1082
  if correct_classification and correct_classification in classification_map:
1083
- records[idx]["correct_classification"] = classification_map[correct_classification]
 
 
 
 
 
 
 
1084
  html, pos, stats = _render_conv_exchange(records, idx)
1085
- row_upd, note_val = _comment_ui_state(records, idx)
1086
- # keep row visible after save (since still incorrect)
1087
- return records, idx, "💾 Comment saved", html, pos, stats, row_upd, note_val, _classification_update(records, idx)
 
1088
 
1089
  def _download_reviewed_json(meta: dict, records: list):
1090
  return _export_conv_records_to_json(meta, records)
@@ -1094,11 +1135,11 @@ def _download_reviewed_csv(meta: dict, records: list):
1094
 
1095
  def _nav_conv(records: list, idx: int, delta: int):
1096
  if not records:
1097
- return idx, "", "", "", gr.update(visible=False), "", gr.update(value=None)
1098
  idx = max(0, min(idx + delta, len(records) - 1))
1099
  html, pos, stats = _render_conv_exchange(records, idx)
1100
  row_upd, note_val = _comment_ui_state(records, idx)
1101
- return idx, html, pos, stats, row_upd, note_val, _classification_update(records, idx)
1102
 
1103
 
1104
  # ============================================================================
@@ -1113,9 +1154,9 @@ def _generate_conv_verification_with_summary(session: SimplifiedSessionData):
1113
  "Provider Summary to be the final exchange presented in that tab"
1114
  """
1115
  if session is None or not hasattr(session.app_instance, "conversation_logger"):
1116
- return None, [], 0, "❌ No session/conversation found", "", "", "", gr.update(visible=False), "", gr.update(value=None)
1117
  if not session.app_instance.conversation_logger.entries:
1118
- return None, [], 0, "⚠️ No exchanges to verify yet", "", "", "", gr.update(visible=False), "", gr.update(value=None)
1119
 
1120
  manager = ConversationVerificationManager()
1121
  vs = manager.create_verification_session(session.app_instance.conversation_logger, "Medical Professional")
@@ -1182,6 +1223,7 @@ def _generate_conv_verification_with_summary(session: SimplifiedSessionData):
1182
  "correct_classification": r.correct_classification,
1183
  "correction_reason": r.correction_reason,
1184
  "verifier_notes": r.verifier_notes,
 
1185
  "provider_summary": "", # Not shown in regular exchanges
1186
  }
1187
  for r in vs.verification_records
@@ -1204,6 +1246,7 @@ def _generate_conv_verification_with_summary(session: SimplifiedSessionData):
1204
  "correct_classification": None,
1205
  "correction_reason": "",
1206
  "verifier_notes": "",
 
1207
  "provider_summary": provider_summary_text,
1208
  "provider_summary_html": provider_summary_html,
1209
  }
@@ -1212,10 +1255,11 @@ def _generate_conv_verification_with_summary(session: SimplifiedSessionData):
1212
  html, pos, stats = _render_conv_exchange(records_as_dicts, 0)
1213
  row_upd, note_val = _comment_ui_state(records_as_dicts, 0)
1214
  radio_upd = _classification_update(records_as_dicts, 0)
 
1215
  return (
1216
  meta, records_as_dicts, 0,
1217
  f"✅ Generated session with {len(records_as_dicts)} exchanges (Provider Summary as final step)",
1218
- html, pos, stats, row_upd, note_val, radio_upd,
1219
  )
1220
 
1221
 
 
924
  "original_confidence",
925
  "is_correct",
926
  "correct_classification",
927
+ "feedback_types",
928
  "verifier_notes",
929
  "user_message",
930
  "assistant_response",
 
952
  "original_confidence": r.get("original_confidence"),
953
  "is_correct": r.get("is_correct"),
954
  "correct_classification": r.get("correct_classification") or "",
955
+ "feedback_types": "; ".join(r.get("feedback_types") or []),
956
  "verifier_notes": r.get("verifier_notes") or "",
957
  "user_message": r.get("user_message"),
958
  "assistant_response": r.get("assistant_response"),
 
963
 
964
  def _generate_conv_verification(session: SimplifiedSessionData):
965
  if session is None or not hasattr(session.app_instance, "conversation_logger"):
966
+ return None, [], 0, "❌ No session/conversation found", "", "", "", gr.update(visible=False), "", gr.update(value=None), gr.update(value=[])
967
  if not session.app_instance.conversation_logger.entries:
968
+ return None, [], 0, "⚠️ No exchanges to verify yet", "", "", "", gr.update(visible=False), "", gr.update(value=None), gr.update(value=[])
969
 
970
  manager = ConversationVerificationManager()
971
  vs = manager.create_verification_session(session.app_instance.conversation_logger, "Medical Professional")
 
1006
  "correct_classification": r.correct_classification,
1007
  "correction_reason": r.correction_reason,
1008
  "verifier_notes": r.verifier_notes,
1009
+ "feedback_types": [],
1010
  "provider_summary": provider_summary_text if r.original_classification.upper() == "RED" else "",
1011
  }
1012
  for r in vs.verification_records
 
1014
  html, pos, stats = _render_conv_exchange(records_as_dicts, 0)
1015
  row_upd, note_val = _comment_ui_state(records_as_dicts, 0)
1016
  radio_upd = _classification_update(records_as_dicts, 0)
1017
+ types_upd = _feedback_type_update(records_as_dicts, 0)
1018
  return (
1019
  meta, records_as_dicts, 0, f"✅ Generated session `{vs.session_id}`",
1020
+ html, pos, stats, row_upd, note_val, radio_upd, types_upd,
1021
  )
1022
 
1023
  # Display text for each classification flag (shared by the verification handlers).
 
1027
  "RED": "🔴 Should be RED - Spiritual distress",
1028
  }
1029
 
1030
+ # What kind of problem the reviewer is reporting. A single exchange can have more
1031
+ # than one (e.g. wrong flag AND a missing explanation), hence a multi-select.
1032
+ # Stored as stable codes so the export can be split into "fix the classifier
1033
+ # prompt" vs "fix the response prompt" without re-reading free text (Or_6.txt).
1034
+ _FEEDBACK_TYPE_DISPLAY = {
1035
+ "CLASSIFICATION": "🏷 Wrong classification",
1036
+ "TONE": "💬 Tone / wording",
1037
+ "MISSING_CONTENT": "📋 Missing information",
1038
+ "FLOW": "🔀 Conversation flow",
1039
+ }
1040
+ _FEEDBACK_TYPE_CHOICES = list(_FEEDBACK_TYPE_DISPLAY.values())
1041
+ _FEEDBACK_TYPE_CODES = {v: k for k, v in _FEEDBACK_TYPE_DISPLAY.items()}
1042
+
1043
+
1044
+ def _feedback_type_update(records: list, idx: int):
1045
+ """Build the gr.update for the 'Type of feedback' CheckboxGroup."""
1046
+ value = []
1047
+ if records and 0 <= idx < len(records) and isinstance(records[idx], dict):
1048
+ value = [
1049
+ _FEEDBACK_TYPE_DISPLAY[c]
1050
+ for c in (records[idx].get("feedback_types") or [])
1051
+ if c in _FEEDBACK_TYPE_DISPLAY
1052
+ ]
1053
+ return gr.update(value=value)
1054
+
1055
  def _classification_update(records: list, idx: int):
1056
  """Build the gr.update for the 'Correct Classification' Radio.
1057
 
 
1070
 
1071
  def _mark_conv_correct(records: list, idx: int):
1072
  if not records:
1073
+ return records, idx, "", "", "", "", gr.update(visible=False), "", gr.update(value=None), gr.update(value=[])
1074
  idx = max(0, min(idx, len(records) - 1))
1075
  if isinstance(records[idx], dict):
1076
  records[idx]["is_correct"] = True
1077
  # clear comment and correct_classification when marked correct (avoid stale data)
1078
  records[idx]["verifier_notes"] = ""
1079
  records[idx]["correct_classification"] = None
1080
+ records[idx]["feedback_types"] = []
1081
  html, pos, stats = _render_conv_exchange(records, idx)
1082
  row_upd, note_val = _comment_ui_state(records, idx)
1083
+ return records, idx, "✅ Marked correct", html, pos, stats, row_upd, note_val, _classification_update(records, idx), _feedback_type_update(records, idx)
1084
 
1085
  def _mark_conv_incorrect(records: list, idx: int):
1086
  if not records:
1087
+ return records, idx, "", "", "", "", gr.update(visible=False), "", gr.update(value=None), gr.update(value=[])
1088
  idx = max(0, min(idx, len(records) - 1))
1089
  if isinstance(records[idx], dict):
1090
  records[idx]["is_correct"] = False
1091
  html, pos, stats = _render_conv_exchange(records, idx)
1092
  row_upd, note_val = _comment_ui_state(records, idx)
1093
+ return records, idx, "❌ Marked incorrect", html, pos, stats, row_upd, note_val, _classification_update(records, idx), _feedback_type_update(records, idx)
1094
 
1095
  def _show_incorrect_comment_ui(records: list, idx: int):
1096
  """Mark incorrect and open the comment row, pre-filling any existing note."""
1097
+ records, idx, status, html, pos, stats, _row, note, existing_classification, existing_types = _mark_conv_incorrect(records, idx)
1098
+ return records, idx, status, html, pos, stats, gr.update(visible=True), note, existing_classification, existing_types
1099
 
1100
+ def _save_incorrect_comment(records: list, idx: int, note: str, correct_classification: str, feedback_types: list = None):
1101
  if not records:
1102
+ return records, idx, "", "", "", "", gr.update(visible=False), "", gr.update(value=None), gr.update(value=[])
1103
  idx = max(0, min(idx, len(records) - 1))
1104
  if isinstance(records[idx], dict):
1105
  records[idx]["verifier_notes"] = (note or "").strip()
1106
+ records[idx]["feedback_types"] = [
1107
+ _FEEDBACK_TYPE_CODES[t] for t in (feedback_types or []) if t in _FEEDBACK_TYPE_CODES
1108
+ ]
1109
  # Map display text to classification code
1110
  classification_map = {
1111
  "🟢 Should be GREEN - No distress": "GREEN",
 
1113
  "🔴 Should be RED - Spiritual distress": "RED"
1114
  }
1115
  if correct_classification and correct_classification in classification_map:
1116
+ code = classification_map[correct_classification]
1117
+ records[idx]["correct_classification"] = code
1118
+ # Picking a classification IS the review verdict. Without this, a
1119
+ # reviewer who uses the always-visible panel (pick + comment + Save)
1120
+ # never sets is_correct, and the exchange exports as "not reviewed"
1121
+ # even though it was reviewed (Or_6.txt).
1122
+ if records[idx].get("original_classification") != "PROVIDER_SUMMARY":
1123
+ records[idx]["is_correct"] = (code == records[idx].get("original_classification"))
1124
  html, pos, stats = _render_conv_exchange(records, idx)
1125
+ saved_note = records[idx].get("verifier_notes", "") if isinstance(records[idx], dict) else ""
1126
+ # Keep the panel open with the saved note visible, even when the reviewer
1127
+ # agreed with the classification and only commented on the wording.
1128
+ return records, idx, "💾 Comment saved", html, pos, stats, gr.update(visible=True), str(saved_note), _classification_update(records, idx), _feedback_type_update(records, idx)
1129
 
1130
  def _download_reviewed_json(meta: dict, records: list):
1131
  return _export_conv_records_to_json(meta, records)
 
1135
 
1136
  def _nav_conv(records: list, idx: int, delta: int):
1137
  if not records:
1138
+ return idx, "", "", "", gr.update(visible=False), "", gr.update(value=None), gr.update(value=[])
1139
  idx = max(0, min(idx + delta, len(records) - 1))
1140
  html, pos, stats = _render_conv_exchange(records, idx)
1141
  row_upd, note_val = _comment_ui_state(records, idx)
1142
+ return idx, html, pos, stats, row_upd, note_val, _classification_update(records, idx), _feedback_type_update(records, idx)
1143
 
1144
 
1145
  # ============================================================================
 
1154
  "Provider Summary to be the final exchange presented in that tab"
1155
  """
1156
  if session is None or not hasattr(session.app_instance, "conversation_logger"):
1157
+ return None, [], 0, "❌ No session/conversation found", "", "", "", gr.update(visible=False), "", gr.update(value=None), gr.update(value=[])
1158
  if not session.app_instance.conversation_logger.entries:
1159
+ return None, [], 0, "⚠️ No exchanges to verify yet", "", "", "", gr.update(visible=False), "", gr.update(value=None), gr.update(value=[])
1160
 
1161
  manager = ConversationVerificationManager()
1162
  vs = manager.create_verification_session(session.app_instance.conversation_logger, "Medical Professional")
 
1223
  "correct_classification": r.correct_classification,
1224
  "correction_reason": r.correction_reason,
1225
  "verifier_notes": r.verifier_notes,
1226
+ "feedback_types": [],
1227
  "provider_summary": "", # Not shown in regular exchanges
1228
  }
1229
  for r in vs.verification_records
 
1246
  "correct_classification": None,
1247
  "correction_reason": "",
1248
  "verifier_notes": "",
1249
+ "feedback_types": [],
1250
  "provider_summary": provider_summary_text,
1251
  "provider_summary_html": provider_summary_html,
1252
  }
 
1255
  html, pos, stats = _render_conv_exchange(records_as_dicts, 0)
1256
  row_upd, note_val = _comment_ui_state(records_as_dicts, 0)
1257
  radio_upd = _classification_update(records_as_dicts, 0)
1258
+ types_upd = _feedback_type_update(records_as_dicts, 0)
1259
  return (
1260
  meta, records_as_dicts, 0,
1261
  f"✅ Generated session with {len(records_as_dicts)} exchanges (Provider Summary as final step)",
1262
+ html, pos, stats, row_upd, note_val, radio_upd, types_upd,
1263
  )
1264
 
1265