DocUA commited on
Commit
08ac559
·
1 Parent(s): d8cb3fd

Add comments and CSV export to conversation verification

Browse files
src/interface/simplified_gradio_app.py CHANGED
@@ -348,7 +348,8 @@ def create_simplified_interface():
348
 
349
  with gr.Row():
350
  generate_conv_verification_btn = gr.Button("🛠 Generate from current chat", variant="primary")
351
- conv_verify_download_btn = gr.DownloadButton("⬇️ Download session JSON", variant="secondary")
 
352
 
353
  conv_verify_status = gr.Markdown(value="", visible=True)
354
  conv_verify_exchange = gr.HTML(value="", label="Current Exchange")
@@ -359,6 +360,16 @@ def create_simplified_interface():
359
  conv_prev_btn = gr.Button("⬅️ Previous")
360
  conv_next_btn = gr.Button("Next ➡️")
361
 
 
 
 
 
 
 
 
 
 
 
362
  with gr.Row():
363
  with gr.Column(scale=1):
364
  conv_position = gr.Markdown(value="")
@@ -2066,6 +2077,77 @@ To revert, use "Reset to Default" button.
2066
  stats = f"<div><strong>Reviewed:</strong> {checked}/{len(records)}</div>"
2067
  return html, pos, stats
2068
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2069
  def _generate_conv_verification(session: SimplifiedSessionData):
2070
  if session is None or not hasattr(session.app_instance, "conversation_logger"):
2071
  return None, [], 0, "❌ No session/conversation found", "", ""
@@ -2075,9 +2157,13 @@ To revert, use "Reset to Default" button.
2075
  from src.core.conversation_verification import ConversationVerificationManager
2076
  manager = ConversationVerificationManager()
2077
  vs = manager.create_verification_session(session.app_instance.conversation_logger, "Medical Professional")
2078
- # Reuse existing export behavior
2079
- _ = open_verification_window(session) # will export JSON
2080
- export_path = _download_latest_verification_json(session)
 
 
 
 
2081
 
2082
  records_as_dicts = [
2083
  {
@@ -2099,7 +2185,7 @@ To revert, use "Reset to Default" button.
2099
  for r in vs.verification_records
2100
  ]
2101
  html, pos, stats = _render_conv_exchange(records_as_dicts, 0)
2102
- return export_path, records_as_dicts, 0, f"✅ Generated session `{vs.session_id}`", html, pos, stats
2103
 
2104
  def _mark_conv_correct(records: list, idx: int):
2105
  if not records:
@@ -2119,6 +2205,29 @@ To revert, use "Reset to Default" button.
2119
  html, pos, stats = _render_conv_exchange(records, idx)
2120
  return records, idx, "❌ Marked incorrect", html, pos, stats
2121
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2122
  def _nav_conv(records: list, idx: int, delta: int):
2123
  if not records:
2124
  return idx, "", "", ""
@@ -2133,11 +2242,17 @@ To revert, use "Reset to Default" button.
2133
  )
2134
 
2135
  conv_verify_download_btn.click(
2136
- lambda p: p,
2137
- inputs=[conv_verify_state],
2138
  outputs=[conv_verify_download_btn]
2139
  )
2140
 
 
 
 
 
 
 
2141
  conv_correct_btn.click(
2142
  _mark_conv_correct,
2143
  inputs=[conv_verify_records, conv_verify_index],
@@ -2145,9 +2260,32 @@ To revert, use "Reset to Default" button.
2145
  )
2146
 
2147
  conv_incorrect_btn.click(
2148
- _mark_conv_incorrect,
2149
  inputs=[conv_verify_records, conv_verify_index],
2150
- outputs=[conv_verify_records, conv_verify_index, conv_verify_status, conv_verify_exchange, conv_position, conv_stats]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2151
  )
2152
 
2153
  conv_prev_btn.click(
 
348
 
349
  with gr.Row():
350
  generate_conv_verification_btn = gr.Button("🛠 Generate from current chat", variant="primary")
351
+ conv_verify_download_btn = gr.DownloadButton("⬇️ Download reviewed JSON", variant="secondary")
352
+ conv_verify_download_csv_btn = gr.DownloadButton("📄 Download CSV", variant="secondary")
353
 
354
  conv_verify_status = gr.Markdown(value="", visible=True)
355
  conv_verify_exchange = gr.HTML(value="", label="Current Exchange")
 
360
  conv_prev_btn = gr.Button("⬅️ Previous")
361
  conv_next_btn = gr.Button("Next ➡️")
362
 
363
+ # Shown only when marking Incorrect
364
+ with gr.Row(visible=False) as conv_incorrect_comment_row:
365
+ conv_incorrect_comment = gr.Textbox(
366
+ label="Comment (why incorrect / what to fix)",
367
+ placeholder="Add a short note for this exchange...",
368
+ lines=3,
369
+ scale=4,
370
+ )
371
+ conv_save_comment_btn = gr.Button("💾 Save comment", variant="secondary", scale=1)
372
+
373
  with gr.Row():
374
  with gr.Column(scale=1):
375
  conv_position = gr.Markdown(value="")
 
2077
  stats = f"<div><strong>Reviewed:</strong> {checked}/{len(records)}</div>"
2078
  return html, pos, stats
2079
 
2080
+ def _export_conv_records_to_json(meta: dict, records: list):
2081
+ """Write reviewed conversation verification results to a JSON file and return its path."""
2082
+ import json
2083
+ import os
2084
+ from datetime import datetime
2085
+
2086
+ export_dir = os.path.join(os.getcwd(), "verification_sessions")
2087
+ os.makedirs(export_dir, exist_ok=True)
2088
+
2089
+ session_id = (meta or {}).get("session_id") or "conversation_verification"
2090
+ export_filename = f"conversation_verification_reviewed_{datetime.utcnow().strftime('%Y%m%d_%H%M%S')}_{session_id}.json"
2091
+ export_path = os.path.join(export_dir, export_filename)
2092
+
2093
+ payload = {
2094
+ **(meta or {}),
2095
+ "verification_records": records or [],
2096
+ }
2097
+
2098
+ with open(export_path, "w", encoding="utf-8") as f:
2099
+ json.dump(payload, f, ensure_ascii=False, indent=2, default=str)
2100
+ return export_path
2101
+
2102
+ def _export_conv_records_to_csv(meta: dict, records: list):
2103
+ """Write reviewed conversation verification results to a CSV file and return its path."""
2104
+ import csv
2105
+ import os
2106
+ from datetime import datetime
2107
+
2108
+ export_dir = os.path.join(os.getcwd(), "verification_exports")
2109
+ os.makedirs(export_dir, exist_ok=True)
2110
+
2111
+ session_id = (meta or {}).get("session_id") or "conversation_verification"
2112
+ export_filename = f"conversation_verification_reviewed_{datetime.utcnow().strftime('%Y%m%d_%H%M%S')}_{session_id}.csv"
2113
+ export_path = os.path.join(export_dir, export_filename)
2114
+
2115
+ fieldnames = [
2116
+ "session_id",
2117
+ "patient_name",
2118
+ "verifier_name",
2119
+ "start_time",
2120
+ "exchange_number",
2121
+ "exchange_id",
2122
+ "original_classification",
2123
+ "original_confidence",
2124
+ "is_correct",
2125
+ "verifier_notes",
2126
+ "user_message",
2127
+ "assistant_response",
2128
+ ]
2129
+
2130
+ with open(export_path, "w", encoding="utf-8", newline="") as f:
2131
+ w = csv.DictWriter(f, fieldnames=fieldnames)
2132
+ w.writeheader()
2133
+ for r in records or []:
2134
+ row = {
2135
+ "session_id": (meta or {}).get("session_id"),
2136
+ "patient_name": (meta or {}).get("patient_name"),
2137
+ "verifier_name": (meta or {}).get("verifier_name"),
2138
+ "start_time": (meta or {}).get("start_time"),
2139
+ "exchange_number": r.get("exchange_number"),
2140
+ "exchange_id": r.get("exchange_id") or r.get("record_id"),
2141
+ "original_classification": r.get("original_classification"),
2142
+ "original_confidence": r.get("original_confidence"),
2143
+ "is_correct": r.get("is_correct"),
2144
+ "verifier_notes": r.get("verifier_notes") or "",
2145
+ "user_message": r.get("user_message"),
2146
+ "assistant_response": r.get("assistant_response"),
2147
+ }
2148
+ w.writerow(row)
2149
+ return export_path
2150
+
2151
  def _generate_conv_verification(session: SimplifiedSessionData):
2152
  if session is None or not hasattr(session.app_instance, "conversation_logger"):
2153
  return None, [], 0, "❌ No session/conversation found", "", ""
 
2157
  from src.core.conversation_verification import ConversationVerificationManager
2158
  manager = ConversationVerificationManager()
2159
  vs = manager.create_verification_session(session.app_instance.conversation_logger, "Medical Professional")
2160
+
2161
+ meta = {
2162
+ "session_id": vs.session_id,
2163
+ "patient_name": vs.patient_name,
2164
+ "verifier_name": vs.verifier_name,
2165
+ "start_time": vs.start_time.isoformat() if hasattr(vs, "start_time") else None,
2166
+ }
2167
 
2168
  records_as_dicts = [
2169
  {
 
2185
  for r in vs.verification_records
2186
  ]
2187
  html, pos, stats = _render_conv_exchange(records_as_dicts, 0)
2188
+ return meta, records_as_dicts, 0, f"✅ Generated session `{vs.session_id}`", html, pos, stats
2189
 
2190
  def _mark_conv_correct(records: list, idx: int):
2191
  if not records:
 
2205
  html, pos, stats = _render_conv_exchange(records, idx)
2206
  return records, idx, "❌ Marked incorrect", html, pos, stats
2207
 
2208
+ def _show_incorrect_comment_ui(records: list, idx: int):
2209
+ """Mark incorrect and open the comment row, pre-filling any existing note."""
2210
+ records, idx, status, html, pos, stats = _mark_conv_incorrect(records, idx)
2211
+ note = ""
2212
+ if records and isinstance(records[idx], dict):
2213
+ note = records[idx].get("verifier_notes") or ""
2214
+ return records, idx, status, html, pos, stats, gr.update(visible=True), note
2215
+
2216
+ def _save_incorrect_comment(records: list, idx: int, note: str):
2217
+ if not records:
2218
+ return records, idx, "", "", "", gr.update(visible=False), ""
2219
+ idx = max(0, min(idx, len(records) - 1))
2220
+ if isinstance(records[idx], dict):
2221
+ records[idx]["verifier_notes"] = (note or "").strip()
2222
+ html, pos, stats = _render_conv_exchange(records, idx)
2223
+ return records, idx, "💾 Comment saved", html, pos, stats, gr.update(visible=False)
2224
+
2225
+ def _download_reviewed_json(meta: dict, records: list):
2226
+ return _export_conv_records_to_json(meta, records)
2227
+
2228
+ def _download_reviewed_csv(meta: dict, records: list):
2229
+ return _export_conv_records_to_csv(meta, records)
2230
+
2231
  def _nav_conv(records: list, idx: int, delta: int):
2232
  if not records:
2233
  return idx, "", "", ""
 
2242
  )
2243
 
2244
  conv_verify_download_btn.click(
2245
+ _download_reviewed_json,
2246
+ inputs=[conv_verify_state, conv_verify_records],
2247
  outputs=[conv_verify_download_btn]
2248
  )
2249
 
2250
+ conv_verify_download_csv_btn.click(
2251
+ _download_reviewed_csv,
2252
+ inputs=[conv_verify_state, conv_verify_records],
2253
+ outputs=[conv_verify_download_csv_btn]
2254
+ )
2255
+
2256
  conv_correct_btn.click(
2257
  _mark_conv_correct,
2258
  inputs=[conv_verify_records, conv_verify_index],
 
2260
  )
2261
 
2262
  conv_incorrect_btn.click(
2263
+ _show_incorrect_comment_ui,
2264
  inputs=[conv_verify_records, conv_verify_index],
2265
+ outputs=[
2266
+ conv_verify_records,
2267
+ conv_verify_index,
2268
+ conv_verify_status,
2269
+ conv_verify_exchange,
2270
+ conv_position,
2271
+ conv_stats,
2272
+ conv_incorrect_comment_row,
2273
+ conv_incorrect_comment,
2274
+ ]
2275
+ )
2276
+
2277
+ conv_save_comment_btn.click(
2278
+ _save_incorrect_comment,
2279
+ inputs=[conv_verify_records, conv_verify_index, conv_incorrect_comment],
2280
+ outputs=[
2281
+ conv_verify_records,
2282
+ conv_verify_index,
2283
+ conv_verify_status,
2284
+ conv_verify_exchange,
2285
+ conv_position,
2286
+ conv_stats,
2287
+ conv_incorrect_comment_row,
2288
+ ]
2289
  )
2290
 
2291
  conv_prev_btn.click(
tests/test_conversation_verification_export.py CHANGED
@@ -1,5 +1,5 @@
 
1
  import json
2
- import os
3
  from datetime import datetime
4
 
5
 
@@ -65,3 +65,65 @@ def test_conversation_verification_export_serializes_without_record_id(tmp_path,
65
  loaded = json.loads(out.read_text(encoding="utf-8"))
66
  assert loaded["verification_records"][0]["exchange_id"] == "sess_1"
67
  assert loaded["verification_records"][0]["record_id"] == "sess_1"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import csv
2
  import json
 
3
  from datetime import datetime
4
 
5
 
 
65
  loaded = json.loads(out.read_text(encoding="utf-8"))
66
  assert loaded["verification_records"][0]["exchange_id"] == "sess_1"
67
  assert loaded["verification_records"][0]["record_id"] == "sess_1"
68
+
69
+
70
+ def test_conversation_verification_csv_contains_expected_columns(tmp_path):
71
+ meta = {
72
+ "session_id": "verification_test",
73
+ "patient_name": "Test",
74
+ "verifier_name": "Verifier",
75
+ "start_time": "2025-12-12T00:00:00",
76
+ }
77
+ records = [
78
+ {
79
+ "exchange_id": "sess_1",
80
+ "exchange_number": 1,
81
+ "original_classification": "YELLOW",
82
+ "original_confidence": 0.9,
83
+ "is_correct": False,
84
+ "verifier_notes": "Needs follow-up",
85
+ "user_message": "hi",
86
+ "assistant_response": "hello",
87
+ }
88
+ ]
89
+
90
+ out = tmp_path / "export.csv"
91
+
92
+ fieldnames = [
93
+ "session_id",
94
+ "patient_name",
95
+ "verifier_name",
96
+ "start_time",
97
+ "exchange_number",
98
+ "exchange_id",
99
+ "original_classification",
100
+ "original_confidence",
101
+ "is_correct",
102
+ "verifier_notes",
103
+ "user_message",
104
+ "assistant_response",
105
+ ]
106
+
107
+ with out.open("w", encoding="utf-8", newline="") as f:
108
+ w = csv.DictWriter(f, fieldnames=fieldnames)
109
+ w.writeheader()
110
+ for r in records:
111
+ w.writerow(
112
+ {
113
+ "session_id": meta["session_id"],
114
+ "patient_name": meta["patient_name"],
115
+ "verifier_name": meta["verifier_name"],
116
+ "start_time": meta["start_time"],
117
+ "exchange_number": r.get("exchange_number"),
118
+ "exchange_id": r.get("exchange_id"),
119
+ "original_classification": r.get("original_classification"),
120
+ "original_confidence": r.get("original_confidence"),
121
+ "is_correct": r.get("is_correct"),
122
+ "verifier_notes": r.get("verifier_notes"),
123
+ "user_message": r.get("user_message"),
124
+ "assistant_response": r.get("assistant_response"),
125
+ }
126
+ )
127
+
128
+ rows = list(csv.DictReader(out.open("r", encoding="utf-8")))
129
+ assert rows and rows[0]["verifier_notes"] == "Needs follow-up"