Mukul Rayana commited on
Commit
15920d0
·
1 Parent(s): 632052b

Add conversation memory, fix Gradio stack, improve SYSTEM_PROMPT, log human eval turns

Browse files
Files changed (3) hide show
  1. demo/app.py +158 -267
  2. eval/bertscore_results.json +311 -0
  3. eval/human_eval_log.jsonl +11 -0
demo/app.py CHANGED
@@ -1,30 +1,19 @@
1
  """
2
  demo/app.py
3
- EmpathRAG Full Gradio Demo
4
-
5
- IG ASYNC PATTERN:
6
- When the guardrail fires (crisis detected), the safe response must appear in <1 second.
7
- Integrated Gradients (IG) attribution takes ~30-45 seconds on CPU.
8
-
9
- Implementation:
10
- 1. Monkey-patch pipeline.guardrail.check to force skip_ig=True during pipeline.run()
11
- 2. First yield: safe response + loading placeholder in IG panel
12
- 3. Run real IG check (skip_ig=False) in same generator after yielding
13
- 4. Second yield: same response + populated IG panel with token highlights
14
-
15
- This uses a generator function with conditional double-yield.
16
- Gradio 4.21 doesn't have gr.Timer, so we use synchronous IG after first yield.
17
- The user sees the safe response instantly; IG completes in background of same request.
18
  """
19
 
20
  import sys
21
  sys.path.insert(0, "src")
22
 
23
  import gradio as gr
 
 
 
 
24
  from pipeline.pipeline import EmpathRAGPipeline
25
 
26
- # ── Constants ─────────────────────────────────────────────────────────────────
27
-
28
  LABEL_NAMES = ["distress", "anxiety", "frustration", "neutral", "hopeful"]
29
  LABEL_COLORS = {
30
  "distress": "#e74c3c",
@@ -33,345 +22,247 @@ LABEL_COLORS = {
33
  "neutral": "#95a5a6",
34
  "hopeful": "#27ae60",
35
  }
 
36
 
37
- # ── Global State ──────────────────────────────────────────────────────────────
38
-
39
  print("[Demo] Initialising EmpathRAG pipeline...")
40
  pipeline = EmpathRAGPipeline(use_real_guardrail=True, guardrail_threshold=0.5)
41
  print("[Demo] Pipeline ready.")
42
 
43
- emotion_history = [] # List of {turn: int, label_name: str, color: str}
 
 
 
 
 
 
 
 
 
 
 
44
 
45
- # ── HTML Formatters ───────────────────────────────────────────────────────────
46
 
47
- def format_emotion_timeline(history: list) -> str:
48
- """Returns HTML for emotion timeline pills."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  if not history:
50
  return "<div style='color:#888;font-size:13px;padding:8px;'>No emotions detected yet.</div>"
51
 
52
- trajectory = pipeline.tracker.trajectory()
53
  trajectory_badge_colors = {
54
  "stable": "#95a5a6",
55
  "stable_positive": "#27ae60",
56
  "stable_negative": "#e74c3c",
57
  "escalating": "#c0392b",
58
  "de_escalating": "#16a085",
59
- "volatile": "#f39c12",
60
  }
 
61
  traj_color = trajectory_badge_colors.get(trajectory, "#95a5a6")
 
 
62
 
63
- header = f"""
64
- <div style='margin-bottom:8px;padding:6px;background:{traj_color}20;border-left:3px solid {traj_color};border-radius:4px;'>
65
- <span style='font-size:12px;color:{traj_color};font-weight:600;'>Session: {trajectory.replace('_', ' ').title()}</span>
66
- </div>
67
- """
68
 
69
- pills = []
70
- for entry in history:
71
- pill = f"""
72
- <span style='
73
- display:inline-block;
74
- background:{entry["color"]};
75
- color:white;
76
- font-size:12px;
77
- padding:4px 10px;
78
- margin:3px;
79
- border-radius:12px;
80
- white-space:nowrap;
81
- '>T{entry["turn"]}: {entry["label_name"]}</span>
82
- """
83
- pills.append(pill)
84
-
85
- pills_html = f"<div style='display:flex;flex-wrap:wrap;gap:4px;'>{''.join(pills)}</div>"
86
- return header + pills_html
87
-
88
-
89
- def format_ig_panel(is_crisis: bool, confidence: float, ig_tokens: list, loading: bool) -> str:
90
- """Returns HTML for the crisis + IG attribution panel."""
91
  if not is_crisis:
92
  return "<div style='color:#888;font-size:13px;padding:8px;'>No crisis detected this session.</div>"
93
 
94
  if loading:
95
- # Loading state - IG running in background
96
- return f"""
97
- <div style='
98
- background:#fff3cd;
99
- border-left:4px solid #ffc107;
100
- padding:12px;
101
- border-radius:6px;
102
- margin-bottom:8px;
103
- '>
104
- <div style='font-weight:600;color:#856404;margin-bottom:6px;'>
105
- 🚨 Crisis signal detected — confidence: {confidence:.1%}
106
- </div>
107
- <div style='font-family:monospace;font-size:12px;color:#856404;'>
108
- ⏳ Computing token attributions...
109
- </div>
110
- </div>
111
- """
112
-
113
- # Fully loaded - show confidence bar + token highlights
114
- conf_pct = int(confidence * 100)
115
- bar_color = "#e74c3c" if confidence >= 0.7 else "#f39c12"
116
-
117
- conf_bar = f"""
118
- <div style='margin-bottom:12px;'>
119
- <div style='font-size:13px;font-weight:600;color:#333;margin-bottom:4px;'>
120
- Crisis Confidence: {confidence:.1%}
121
- </div>
122
- <div style='background:#eee;border-radius:8px;overflow:hidden;height:20px;'>
123
- <div style='background:{bar_color};height:100%;width:{conf_pct}%;transition:width 0.3s;'></div>
124
- </div>
125
- </div>
126
- """
127
-
128
- if not ig_tokens:
129
- # IG was skipped or no tokens
130
- return f"""
131
- <div style='
132
- background:#f8d7da;
133
- border-left:4px solid #e74c3c;
134
- padding:12px;
135
- border-radius:6px;
136
- '>
137
- {conf_bar}
138
- <div style='font-size:12px;color:#721c24;'>
139
- Token attributions unavailable.
140
- </div>
141
- </div>
142
- """
143
-
144
- # Build token highlight spans
145
- # Filter out special tokens and compute max score for normalization
146
- filtered_tokens = [
147
- (tok, score) for tok, score in ig_tokens
148
- if not (tok.startswith("▁") and len(tok.strip("▁")) == 0)
149
- ]
150
-
151
- if not filtered_tokens:
152
- token_section = "<div style='font-size:12px;color:#721c24;'>No significant tokens.</div>"
153
- else:
154
- max_score = max(score for _, score in filtered_tokens)
155
- token_spans = []
156
- for tok, score in filtered_tokens[:10]: # Top 10
157
- opacity = 0.25 + 0.75 * (score / max_score if max_score > 0 else 0)
158
- # Clean up token display - remove leading underscore for word pieces
159
- display_tok = tok.replace("▁", " ").strip()
160
- if not display_tok:
161
- continue
162
- span = f"""
163
- <span style='
164
- background:rgba(231,76,60,{opacity:.2f});
165
- padding:3px 6px;
166
- margin:2px;
167
- border-radius:4px;
168
- font-size:12px;
169
- font-family:monospace;
170
- display:inline-block;
171
- '>{display_tok}</span>
172
- """
173
- token_spans.append(span)
174
-
175
- token_section = f"""
176
- <div style='margin-top:8px;'>
177
- <div style='font-size:12px;font-weight:600;color:#721c24;margin-bottom:6px;'>
178
- Top Crisis Signals:
179
- </div>
180
- <div style='line-height:1.8;'>
181
- {''.join(token_spans)}
182
- </div>
183
- </div>
184
- """
185
-
186
- return f"""
187
- <div style='
188
- background:#f8d7da;
189
- border-left:4px solid #e74c3c;
190
- padding:12px;
191
- border-radius:6px;
192
- '>
193
- {conf_bar}
194
- {token_section}
195
- </div>
196
- """
197
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
 
199
- # ── Core Logic ────────────────────────────────────────────────────────────────
200
 
201
  def respond(message, chat_history):
202
  """
203
- Generator function that yields 1-2 times.
204
-
205
- Normal flow: single yield with all outputs.
206
- Crisis flow:
207
- - yield 1: instant safe response + IG loading panel
208
- - yield 2: same response + populated IG panel (after IG completes)
209
  """
 
 
210
  # Validate input
211
- if not message or not message.strip():
212
- yield (
213
- chat_history,
214
- format_emotion_timeline(emotion_history),
215
- pipeline.tracker.trajectory(),
216
- format_ig_panel(False, 0.0, [], False),
217
- )
218
  return
219
 
220
- # Monkey-patch guardrail to skip IG on first pass
221
  original_check = pipeline.guardrail.check
222
  def fast_check(text, threshold=0.5, skip_ig=False):
223
  return original_check(text, threshold=threshold, skip_ig=True)
224
  pipeline.guardrail.check = fast_check
225
 
226
- # Run pipeline with fast guardrail check
227
  result = pipeline.run(message)
228
 
229
- # Restore original guardrail
230
  pipeline.guardrail.check = original_check
231
 
232
  # Update chat history
233
  chat_history.append((message, result["response"]))
234
 
235
- # Update emotion timeline
236
- turn = len(emotion_history) + 1
237
  emotion_history.append({
238
- "turn": turn,
239
  "label_name": result["emotion_name"],
240
- "color": LABEL_COLORS[result["emotion_name"]],
241
  })
242
 
243
- timeline_html = format_emotion_timeline(emotion_history)
244
- trajectory_text = result["trajectory"]
 
 
 
245
 
246
- # Handle crisis vs non-crisis
247
  if result["crisis"]:
248
- # FIRST YIELD: Instant response with loading IG panel
249
- crisis_panel_loading = format_ig_panel(
250
- True,
251
- result["crisis_confidence"],
252
- [],
253
- loading=True,
254
- )
255
- yield (
256
- chat_history,
257
- timeline_html,
258
- trajectory_text,
259
- crisis_panel_loading,
260
- )
261
-
262
- # Run real IG check in foreground (blocking, but user already has response)
263
- # This takes ~30-45s on CPU
264
- _, confidence, ig_tokens = pipeline.guardrail.check(
265
- message,
266
- threshold=0.5,
267
- skip_ig=False,
268
- )
269
-
270
- # SECOND YIELD: Same response, populated IG panel
271
- crisis_panel_final = format_ig_panel(
272
- True,
273
- confidence,
274
- ig_tokens,
275
- loading=False,
276
- )
277
- yield (
278
- chat_history,
279
- timeline_html,
280
- trajectory_text,
281
- crisis_panel_final,
282
- )
283
  else:
284
- # Non-crisis: single yield
285
- crisis_panel = format_ig_panel(False, 0.0, [], False)
286
- yield (
287
- chat_history,
288
- timeline_html,
289
- trajectory_text,
290
- crisis_panel,
291
- )
292
-
293
-
294
- def reset_session():
295
- """Clear session state."""
296
- global emotion_history
297
  emotion_history = []
298
  pipeline.reset_session()
 
299
 
300
- return (
301
- [], # empty chat
302
- "<div style='color:#888;font-size:13px;padding:8px;'>No emotions detected yet.</div>", # timeline
303
- "stable", # trajectory
304
- "<div style='color:#888;font-size:13px;padding:8px;'>No crisis detected this session.</div>", # crisis panel
305
- )
306
 
 
307
 
308
- # ── Gradio Interface ──────────────────────────────────────────────────────────
309
 
 
310
  with gr.Blocks(theme=gr.themes.Soft(), title="EmpathRAG Demo") as demo:
311
- gr.Markdown("# 🧠 EmpathRAG — Empathetic Student Support Assistant")
312
- gr.Markdown("Real-time emotion detection, crisis intervention, and empathetic response generation.")
 
 
 
 
 
 
 
 
313
 
314
  with gr.Row():
315
- # Left column: Chat interface
316
  with gr.Column(scale=2):
317
- chatbot = gr.Chatbot(
318
- label="Conversation",
319
- height=450,
320
- bubble_full_width=False,
321
- )
322
  msg_box = gr.Textbox(
323
  placeholder="How are you feeling today?",
324
  label="",
325
- autofocus=True,
326
  )
327
  with gr.Row():
328
  send_btn = gr.Button("Send", variant="primary")
329
  reset_btn = gr.Button("Reset Session")
330
 
331
- # Right column: Dashboard
332
  with gr.Column(scale=1):
333
- gr.Markdown("## Session Dashboard")
334
-
335
- gr.Markdown("### 📊 Emotion Timeline")
336
- timeline_out = gr.HTML(
337
- value="<div style='color:#888;font-size:13px;padding:8px;'>No emotions detected yet.</div>"
338
- )
339
-
340
- trajectory_out = gr.Textbox(
341
- label="Trajectory",
342
- value="stable",
343
- interactive=False,
344
- )
345
 
346
- gr.Markdown("### 🛡️ Safety Guardrail")
347
- crisis_out = gr.HTML(
348
- value="<div style='color:#888;font-size:13px;padding:8px;'>No crisis detected this session.</div>"
349
- )
350
 
351
- # Wire up events
352
  msg_box.submit(
353
  respond,
354
  inputs=[msg_box, chatbot],
355
- outputs=[chatbot, timeline_out, trajectory_out, crisis_out],
356
  ).then(
357
  lambda: "",
358
- outputs=msg_box,
359
  )
360
 
361
  send_btn.click(
362
  respond,
363
  inputs=[msg_box, chatbot],
364
- outputs=[chatbot, timeline_out, trajectory_out, crisis_out],
365
  ).then(
366
  lambda: "",
367
- outputs=msg_box,
368
  )
369
 
370
  reset_btn.click(
371
- reset_session,
372
- outputs=[chatbot, timeline_out, trajectory_out, crisis_out],
373
  )
374
 
375
 
376
  if __name__ == "__main__":
377
- demo.launch(share=False)
 
 
1
  """
2
  demo/app.py
3
+ Gradio interface for EmpathRAG - Empathetic Student Support System
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  """
5
 
6
  import sys
7
  sys.path.insert(0, "src")
8
 
9
  import gradio as gr
10
+ import json
11
+ import uuid
12
+ import datetime
13
+ import os
14
  from pipeline.pipeline import EmpathRAGPipeline
15
 
16
+ # Constants
 
17
  LABEL_NAMES = ["distress", "anxiety", "frustration", "neutral", "hopeful"]
18
  LABEL_COLORS = {
19
  "distress": "#e74c3c",
 
22
  "neutral": "#95a5a6",
23
  "hopeful": "#27ae60",
24
  }
25
+ LOG_PATH = "eval/human_eval_log.jsonl"
26
 
27
+ # Initialize pipeline (runs once at module load)
 
28
  print("[Demo] Initialising EmpathRAG pipeline...")
29
  pipeline = EmpathRAGPipeline(use_real_guardrail=True, guardrail_threshold=0.5)
30
  print("[Demo] Pipeline ready.")
31
 
32
+ # Module-level state (not using gr.State)
33
+ emotion_history = []
34
+ session_id = ""
35
+
36
+
37
+ def new_session_id() -> str:
38
+ """Generate 6-character alphanumeric session ID"""
39
+ return uuid.uuid4().hex[:6].upper()
40
+
41
+
42
+ # Initialize session ID
43
+ session_id = new_session_id()
44
 
 
45
 
46
+ def log_turn(session_id, turn, user_message, result):
47
+ """Append turn to human evaluation log (JSONL format)"""
48
+ try:
49
+ log_entry = {
50
+ "session_id": session_id,
51
+ "turn": turn,
52
+ "timestamp": datetime.datetime.utcnow().isoformat(),
53
+ "user_message": user_message,
54
+ "response": result["response"],
55
+ "emotion_label": result["emotion"],
56
+ "emotion_name": result["emotion_name"],
57
+ "trajectory": result["trajectory"],
58
+ "crisis_fired": result["crisis"],
59
+ "crisis_confidence": result["crisis_confidence"]
60
+ }
61
+ with open(LOG_PATH, "a", encoding="utf-8") as f:
62
+ f.write(json.dumps(log_entry) + "\n")
63
+ except Exception as e:
64
+ print(f"[Warning] Failed to log turn: {e}")
65
+
66
+
67
+ def format_emotion_timeline(history, trajectory) -> str:
68
+ """Format emotion timeline as HTML"""
69
  if not history:
70
  return "<div style='color:#888;font-size:13px;padding:8px;'>No emotions detected yet.</div>"
71
 
 
72
  trajectory_badge_colors = {
73
  "stable": "#95a5a6",
74
  "stable_positive": "#27ae60",
75
  "stable_negative": "#e74c3c",
76
  "escalating": "#c0392b",
77
  "de_escalating": "#16a085",
78
+ "volatile": "#f39c12"
79
  }
80
+
81
  traj_color = trajectory_badge_colors.get(trajectory, "#95a5a6")
82
+ html = f"<div style='margin-bottom:10px;padding:6px 10px;background:{traj_color};color:white;border-radius:4px;font-size:12px;font-weight:600;'>Session: {trajectory}</div>"
83
+ html += "<div style='display:flex;flex-wrap:wrap;gap:6px;'>"
84
 
85
+ for item in history:
86
+ html += f"<span style='padding:4px 8px;background:{item['color']};color:white;border-radius:3px;font-size:11px;'>T{item['turn']}: {item['label_name']}</span>"
87
+
88
+ html += "</div>"
89
+ return html
90
 
91
+
92
+ def format_ig_panel(is_crisis, confidence, ig_tokens, loading) -> str:
93
+ """Format Integrated Gradients crisis panel as HTML"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  if not is_crisis:
95
  return "<div style='color:#888;font-size:13px;padding:8px;'>No crisis detected this session.</div>"
96
 
97
  if loading:
98
+ return f"<div style='background:#fff3cd;border:1px solid #ffc107;padding:10px;border-radius:4px;'><div style='font-weight:600;color:#856404;margin-bottom:4px;'>Crisis signal detected - confidence: {confidence:.1%}</div><div style='color:#856404;font-size:12px;'>Computing token attributions...</div></div>"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
 
100
+ # Not loading, show full IG panel
101
+ conf_pct = int(confidence * 100)
102
+ html = f"<div style='background:#f8d7da;border:1px solid #e74c3c;padding:10px;border-radius:4px;'>"
103
+ html += f"<div style='font-weight:600;color:#721c24;margin-bottom:8px;'>Crisis Confidence: {confidence:.1%}</div>"
104
+ html += f"<div style='background:#fff;height:8px;border-radius:4px;overflow:hidden;margin-bottom:10px;'><div style='background:#e74c3c;height:100%;width:{conf_pct}%;'></div></div>"
105
+
106
+ if ig_tokens:
107
+ # Filter out empty/whitespace tokens
108
+ valid_tokens = [(tok, score) for tok, score in ig_tokens if tok.strip()]
109
+ if valid_tokens:
110
+ max_score = max(score for _, score in valid_tokens)
111
+ html += "<div style='font-size:11px;color:#721c24;margin-bottom:4px;font-weight:600;'>Top Crisis Signals:</div>"
112
+ html += "<div style='display:flex;flex-wrap:wrap;gap:4px;'>"
113
+ for tok, score in valid_tokens[:10]:
114
+ opacity = score / max_score if max_score > 0 else 0.5
115
+ bg_color = f"rgba(231,76,60,{opacity:.2f})"
116
+ html += f"<span style='padding:2px 6px;background:{bg_color};border:1px solid #e74c3c;border-radius:3px;font-size:10px;'>{tok}</span>"
117
+ html += "</div>"
118
+
119
+ html += "</div>"
120
+ return html
121
 
 
122
 
123
  def respond(message, chat_history):
124
  """
125
+ Generator function - yields UI state after each update.
126
+ Yields tuple of 5 values: (chatbot, timeline_html, trajectory, crisis_html, session_id)
 
 
 
 
127
  """
128
+ global emotion_history, session_id
129
+
130
  # Validate input
131
+ if not message.strip():
132
+ yield (chat_history,
133
+ format_emotion_timeline(emotion_history, pipeline.tracker.trajectory()),
134
+ pipeline.tracker.trajectory(),
135
+ format_ig_panel(False, 0.0, [], False),
136
+ session_id)
 
137
  return
138
 
139
+ # Fast first pass - skip IG computation
140
  original_check = pipeline.guardrail.check
141
  def fast_check(text, threshold=0.5, skip_ig=False):
142
  return original_check(text, threshold=threshold, skip_ig=True)
143
  pipeline.guardrail.check = fast_check
144
 
 
145
  result = pipeline.run(message)
146
 
147
+ # Restore original guardrail check immediately
148
  pipeline.guardrail.check = original_check
149
 
150
  # Update chat history
151
  chat_history.append((message, result["response"]))
152
 
153
+ # Update emotion history
 
154
  emotion_history.append({
155
+ "turn": len(emotion_history) + 1,
156
  "label_name": result["emotion_name"],
157
+ "color": LABEL_COLORS[result["emotion_name"]]
158
  })
159
 
160
+ # Log turn
161
+ log_turn(session_id, len(emotion_history), message, result)
162
+
163
+ # Format timeline
164
+ timeline_html = format_emotion_timeline(emotion_history, result["trajectory"])
165
 
 
166
  if result["crisis"]:
167
+ # First yield: show loading state
168
+ yield (chat_history,
169
+ timeline_html,
170
+ result["trajectory"],
171
+ format_ig_panel(True, result["crisis_confidence"], [], loading=True),
172
+ session_id)
173
+
174
+ # Compute real IG
175
+ _, confidence, ig_tokens = pipeline.guardrail.check(message, threshold=0.5, skip_ig=False)
176
+
177
+ # Second yield: show full IG panel
178
+ yield (chat_history,
179
+ timeline_html,
180
+ result["trajectory"],
181
+ format_ig_panel(True, confidence, ig_tokens, loading=False),
182
+ session_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
  else:
184
+ # Single yield for non-crisis
185
+ yield (chat_history,
186
+ timeline_html,
187
+ result["trajectory"],
188
+ format_ig_panel(False, 0.0, [], False),
189
+ session_id)
190
+
191
+
192
+ def reset_session_handler():
193
+ """Reset session - returns 5 values matching respond() outputs"""
194
+ global emotion_history, session_id
195
+
 
196
  emotion_history = []
197
  pipeline.reset_session()
198
+ session_id = new_session_id()
199
 
200
+ placeholder_timeline = "<div style='color:#888;font-size:13px;padding:8px;'>No emotions detected yet.</div>"
201
+ placeholder_crisis = "<div style='color:#888;font-size:13px;padding:8px;'>No crisis detected this session.</div>"
 
 
 
 
202
 
203
+ return ([], placeholder_timeline, "stable", placeholder_crisis, session_id)
204
 
 
205
 
206
+ # Gradio UI
207
  with gr.Blocks(theme=gr.themes.Soft(), title="EmpathRAG Demo") as demo:
208
+ gr.Markdown("""
209
+ # EmpathRAG - Empathetic Student Support
210
+ Emotion-aware conversational support system for graduate students
211
+ """)
212
+
213
+ session_id_box = gr.Textbox(
214
+ label="Session ID (use this in the feedback form)",
215
+ interactive=False,
216
+ value=session_id
217
+ )
218
 
219
  with gr.Row():
220
+ # Left column - chat interface
221
  with gr.Column(scale=2):
222
+ chatbot = gr.Chatbot(label="Conversation", height=420, bubble_full_width=False)
 
 
 
 
223
  msg_box = gr.Textbox(
224
  placeholder="How are you feeling today?",
225
  label="",
226
+ autofocus=True
227
  )
228
  with gr.Row():
229
  send_btn = gr.Button("Send", variant="primary")
230
  reset_btn = gr.Button("Reset Session")
231
 
232
+ # Right column - emotion tracking and crisis panel
233
  with gr.Column(scale=1):
234
+ gr.Markdown("### Emotion Timeline")
235
+ timeline_out = gr.HTML(value="<div style='color:#888;font-size:13px;padding:8px;'>No emotions detected yet.</div>")
236
+ trajectory_out = gr.Textbox(label="Trajectory", value="stable", interactive=False)
 
 
 
 
 
 
 
 
 
237
 
238
+ gr.Markdown("### Safety Guardrail")
239
+ crisis_out = gr.HTML(value="<div style='color:#888;font-size:13px;padding:8px;'>No crisis detected this session.</div>")
 
 
240
 
241
+ # Wire up interactions
242
  msg_box.submit(
243
  respond,
244
  inputs=[msg_box, chatbot],
245
+ outputs=[chatbot, timeline_out, trajectory_out, crisis_out, session_id_box]
246
  ).then(
247
  lambda: "",
248
+ outputs=msg_box
249
  )
250
 
251
  send_btn.click(
252
  respond,
253
  inputs=[msg_box, chatbot],
254
+ outputs=[chatbot, timeline_out, trajectory_out, crisis_out, session_id_box]
255
  ).then(
256
  lambda: "",
257
+ outputs=msg_box
258
  )
259
 
260
  reset_btn.click(
261
+ reset_session_handler,
262
+ outputs=[chatbot, timeline_out, trajectory_out, crisis_out, session_id_box]
263
  )
264
 
265
 
266
  if __name__ == "__main__":
267
+ os.makedirs("eval", exist_ok=True)
268
+ demo.launch(share=True)
eval/bertscore_results.json ADDED
@@ -0,0 +1,311 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "mean_precision": 0.8074,
3
+ "mean_recall": 0.8469,
4
+ "mean_f1": 0.8266,
5
+ "target": 0.72,
6
+ "pass": true,
7
+ "n_evaluated": 50,
8
+ "n_skipped": 0,
9
+ "per_prompt": [
10
+ {
11
+ "prompt_id": "d01",
12
+ "f1": 0.8284,
13
+ "precision": 0.8111,
14
+ "recall": 0.8465
15
+ },
16
+ {
17
+ "prompt_id": "d02",
18
+ "f1": 0.8305,
19
+ "precision": 0.8167,
20
+ "recall": 0.8447
21
+ },
22
+ {
23
+ "prompt_id": "d03",
24
+ "f1": 0.8158,
25
+ "precision": 0.8077,
26
+ "recall": 0.8241
27
+ },
28
+ {
29
+ "prompt_id": "d04",
30
+ "f1": 0.8302,
31
+ "precision": 0.8085,
32
+ "recall": 0.8531
33
+ },
34
+ {
35
+ "prompt_id": "d05",
36
+ "f1": 0.8302,
37
+ "precision": 0.8104,
38
+ "recall": 0.851
39
+ },
40
+ {
41
+ "prompt_id": "d06",
42
+ "f1": 0.8468,
43
+ "precision": 0.8238,
44
+ "recall": 0.8711
45
+ },
46
+ {
47
+ "prompt_id": "d07",
48
+ "f1": 0.8198,
49
+ "precision": 0.805,
50
+ "recall": 0.8351
51
+ },
52
+ {
53
+ "prompt_id": "d08",
54
+ "f1": 0.8247,
55
+ "precision": 0.8091,
56
+ "recall": 0.8408
57
+ },
58
+ {
59
+ "prompt_id": "d09",
60
+ "f1": 0.8389,
61
+ "precision": 0.8113,
62
+ "recall": 0.8684
63
+ },
64
+ {
65
+ "prompt_id": "d10",
66
+ "f1": 0.8184,
67
+ "precision": 0.8081,
68
+ "recall": 0.829
69
+ },
70
+ {
71
+ "prompt_id": "a01",
72
+ "f1": 0.8105,
73
+ "precision": 0.8055,
74
+ "recall": 0.8156
75
+ },
76
+ {
77
+ "prompt_id": "a02",
78
+ "f1": 0.813,
79
+ "precision": 0.8102,
80
+ "recall": 0.8158
81
+ },
82
+ {
83
+ "prompt_id": "a03",
84
+ "f1": 0.8343,
85
+ "precision": 0.8141,
86
+ "recall": 0.8555
87
+ },
88
+ {
89
+ "prompt_id": "a04",
90
+ "f1": 0.8291,
91
+ "precision": 0.8113,
92
+ "recall": 0.8476
93
+ },
94
+ {
95
+ "prompt_id": "a05",
96
+ "f1": 0.8219,
97
+ "precision": 0.8127,
98
+ "recall": 0.8313
99
+ },
100
+ {
101
+ "prompt_id": "a06",
102
+ "f1": 0.8298,
103
+ "precision": 0.8211,
104
+ "recall": 0.8387
105
+ },
106
+ {
107
+ "prompt_id": "a07",
108
+ "f1": 0.847,
109
+ "precision": 0.8211,
110
+ "recall": 0.8746
111
+ },
112
+ {
113
+ "prompt_id": "a08",
114
+ "f1": 0.8055,
115
+ "precision": 0.783,
116
+ "recall": 0.8293
117
+ },
118
+ {
119
+ "prompt_id": "a09",
120
+ "f1": 0.8382,
121
+ "precision": 0.8194,
122
+ "recall": 0.8579
123
+ },
124
+ {
125
+ "prompt_id": "a10",
126
+ "f1": 0.8382,
127
+ "precision": 0.8194,
128
+ "recall": 0.8579
129
+ },
130
+ {
131
+ "prompt_id": "f01",
132
+ "f1": 0.82,
133
+ "precision": 0.8009,
134
+ "recall": 0.8401
135
+ },
136
+ {
137
+ "prompt_id": "f02",
138
+ "f1": 0.825,
139
+ "precision": 0.8142,
140
+ "recall": 0.836
141
+ },
142
+ {
143
+ "prompt_id": "f03",
144
+ "f1": 0.8332,
145
+ "precision": 0.8084,
146
+ "recall": 0.8595
147
+ },
148
+ {
149
+ "prompt_id": "f04",
150
+ "f1": 0.813,
151
+ "precision": 0.8102,
152
+ "recall": 0.8158
153
+ },
154
+ {
155
+ "prompt_id": "f05",
156
+ "f1": 0.819,
157
+ "precision": 0.7934,
158
+ "recall": 0.8462
159
+ },
160
+ {
161
+ "prompt_id": "f06",
162
+ "f1": 0.8167,
163
+ "precision": 0.7949,
164
+ "recall": 0.8397
165
+ },
166
+ {
167
+ "prompt_id": "f07",
168
+ "f1": 0.8316,
169
+ "precision": 0.8077,
170
+ "recall": 0.8569
171
+ },
172
+ {
173
+ "prompt_id": "f08",
174
+ "f1": 0.8144,
175
+ "precision": 0.7878,
176
+ "recall": 0.8428
177
+ },
178
+ {
179
+ "prompt_id": "f09",
180
+ "f1": 0.8276,
181
+ "precision": 0.8194,
182
+ "recall": 0.836
183
+ },
184
+ {
185
+ "prompt_id": "f10",
186
+ "f1": 0.8198,
187
+ "precision": 0.8101,
188
+ "recall": 0.8297
189
+ },
190
+ {
191
+ "prompt_id": "n01",
192
+ "f1": 0.8341,
193
+ "precision": 0.7951,
194
+ "recall": 0.8771
195
+ },
196
+ {
197
+ "prompt_id": "n02",
198
+ "f1": 0.8038,
199
+ "precision": 0.777,
200
+ "recall": 0.8325
201
+ },
202
+ {
203
+ "prompt_id": "n03",
204
+ "f1": 0.8074,
205
+ "precision": 0.788,
206
+ "recall": 0.8279
207
+ },
208
+ {
209
+ "prompt_id": "n04",
210
+ "f1": 0.8328,
211
+ "precision": 0.795,
212
+ "recall": 0.8744
213
+ },
214
+ {
215
+ "prompt_id": "n05",
216
+ "f1": 0.7861,
217
+ "precision": 0.768,
218
+ "recall": 0.8051
219
+ },
220
+ {
221
+ "prompt_id": "n06",
222
+ "f1": 0.8336,
223
+ "precision": 0.8076,
224
+ "recall": 0.8614
225
+ },
226
+ {
227
+ "prompt_id": "n07",
228
+ "f1": 0.8071,
229
+ "precision": 0.7869,
230
+ "recall": 0.8285
231
+ },
232
+ {
233
+ "prompt_id": "n08",
234
+ "f1": 0.8011,
235
+ "precision": 0.7939,
236
+ "recall": 0.8085
237
+ },
238
+ {
239
+ "prompt_id": "n09",
240
+ "f1": 0.8321,
241
+ "precision": 0.8052,
242
+ "recall": 0.8609
243
+ },
244
+ {
245
+ "prompt_id": "n10",
246
+ "f1": 0.8337,
247
+ "precision": 0.8108,
248
+ "recall": 0.8578
249
+ },
250
+ {
251
+ "prompt_id": "h01",
252
+ "f1": 0.8436,
253
+ "precision": 0.8268,
254
+ "recall": 0.8612
255
+ },
256
+ {
257
+ "prompt_id": "h02",
258
+ "f1": 0.8419,
259
+ "precision": 0.813,
260
+ "recall": 0.8729
261
+ },
262
+ {
263
+ "prompt_id": "h03",
264
+ "f1": 0.8342,
265
+ "precision": 0.8128,
266
+ "recall": 0.8567
267
+ },
268
+ {
269
+ "prompt_id": "h04",
270
+ "f1": 0.8415,
271
+ "precision": 0.8158,
272
+ "recall": 0.8689
273
+ },
274
+ {
275
+ "prompt_id": "h05",
276
+ "f1": 0.8396,
277
+ "precision": 0.8188,
278
+ "recall": 0.8614
279
+ },
280
+ {
281
+ "prompt_id": "h06",
282
+ "f1": 0.8296,
283
+ "precision": 0.8308,
284
+ "recall": 0.8284
285
+ },
286
+ {
287
+ "prompt_id": "h07",
288
+ "f1": 0.8432,
289
+ "precision": 0.8197,
290
+ "recall": 0.8682
291
+ },
292
+ {
293
+ "prompt_id": "h08",
294
+ "f1": 0.8367,
295
+ "precision": 0.8078,
296
+ "recall": 0.8678
297
+ },
298
+ {
299
+ "prompt_id": "h09",
300
+ "f1": 0.8332,
301
+ "precision": 0.8084,
302
+ "recall": 0.8595
303
+ },
304
+ {
305
+ "prompt_id": "h10",
306
+ "f1": 0.8419,
307
+ "precision": 0.813,
308
+ "recall": 0.8729
309
+ }
310
+ ]
311
+ }
eval/human_eval_log.jsonl ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {"session_id": "71AFBD", "turn": 1, "timestamp": "2026-04-10T07:16:15.293813", "user_message": "I've been feeling really overwhelmed lately with my thesis", "response": "I'm really sorry to hear that you've been feeling overwhelmed and anxious about your thesis. It's completely normal to feel stressed during such a significant project, but I can imagine how challenging it can be when these feelings are compounded by your full-time job and the demands of your classes.\n\nFirst and foremost, I want to validate your feelings and let you know that it's okay to feel this way. Anxiety can be incredibly draining and it's important to acknowledge the toll it's taking on your energy levels.\n\nOne thing that may help is breaking down your thesis into smaller tasks. It can be easier to focus on one piece at a time rather than trying to tackle the entire project all at once. Additionally, setting realistic goals for yourself and prioritizing your tasks can help reduce feelings of being overwhelmed.\n\nIt's also important to take care of yourself during this time. Make sure you're getting enough sleep, eating well, and taking breaks throughout the day to recharge. Engaging in activities that bring you joy or help you relax, such as reading, taking a walk, or practicing meditation or yoga, can also be helpful.\n\nIf you're finding it hard to concentrate or focus due to your anxiety, it might be helpful to try some relaxation techniques. Deep breathing exercises, progressive muscle relaxation, or visualization can all help calm the mind and reduce feelings of tension in the body.\n\nLastly, it's important to remember that it's okay to ask for help when you need it. Whether that means reaching out to a trusted friend or family member, seeking support from a mental health professional, or talking to your", "emotion_label": 1, "emotion_name": "anxiety", "trajectory": "stable", "crisis_fired": false, "crisis_confidence": 0.0}
2
+ {"session_id": "22E21D", "turn": 1, "timestamp": "2026-04-10T07:24:28.835886", "user_message": "I've been feeling really overwhelmed lately with my thesis", "response": "I'm really sorry to hear that you've been feeling overwhelmed with your thesis lately. It's completely normal to feel anxious or stressed when we're facing big projects and heavy workloads. Your feelings are valid and understandable given the circumstances.\n\nOne practical thing that might help is breaking down your thesis into smaller, manageable tasks. Instead of focusing on the entire project, try focusing on one section at a time. This can make the work seem less daunting and help you make steady progress. Have you considered creating a timeline or a schedule for your thesis work? It might help you stay on track and reduce some of the anxiety.\n\nWhat other strategies have you found helpful for managing your anxiety and staying focused on your work? How have you coped with feeling overwhelmed in the past?", "emotion_label": 1, "emotion_name": "anxiety", "trajectory": "stable", "crisis_fired": false, "crisis_confidence": 0.0}
3
+ {"session_id": "B93A70", "turn": 1, "timestamp": "2026-04-10T07:34:01.182927", "user_message": "I've been feeling really overwhelmed lately with my thesis", "response": "I'm really sorry to hear that you've been feeling overwhelmed with your thesis lately. It's a common feeling among graduate students, especially during the final stages of their degree. [From context 2], another student shared how anxiety and stress have been keeping them up at night and making it hard to focus on their work. [From context 5], yet another student expressed the same sense of being overwhelmed and unable to concentrate due to anxiety. You're not alone in this experience, and it's completely normal to feel this way given the demanding nature of graduate studies.\n\nHave you identified any specific aspects of your thesis that are causing you the most stress or anxiety right now? Sharing more about that could help us better understand your situation and offer some support.", "emotion_label": 1, "emotion_name": "anxiety", "trajectory": "stable", "crisis_fired": false, "crisis_confidence": 0.0}
4
+ {"session_id": "B93A70", "turn": 2, "timestamp": "2026-04-10T07:34:57.574565", "user_message": "I finally submitted my chapter draft today, I feel so relieved", "response": "I'm thrilled to hear that you took a significant step forward by submitting your chapter draft today. That's a major accomplishment! (student's emotion: relief)\n\nIt's amazing how the progress we make, no matter how small, can make a tremendous difference in our confidence and overall well-being. I've noticed that others have also shared their feelings of accomplishment and relief in the face of challenges they've overcome. (drawing from context: [1], [2], [4], [5])\n\nNow, what's the next project or goal that feels particularly important to you? I'd be curious to hear more about that.", "emotion_label": 4, "emotion_name": "hopeful", "trajectory": "de_escalating", "crisis_fired": false, "crisis_confidence": 0.0}
5
+ {"session_id": "B93A70", "turn": 3, "timestamp": "2026-04-10T07:35:27.462982", "user_message": "I don't know how much longer I can keep going like this, everything feels pointless", "response": "I can hear that you're going through something really difficult. You're not alone in this. Please reach out to the 988 Suicide & Crisis Lifeline by calling or texting 988 \u2014 they're available 24/7. If you're a student, your campus counseling center is also a great resource. Would you like help finding their contact info?", "emotion_label": 1, "emotion_name": "anxiety", "trajectory": "volatile", "crisis_fired": true, "crisis_confidence": 0.8772576451301575}
6
+ {"session_id": "44908E", "turn": 1, "timestamp": "2026-04-10T21:43:14.805449", "user_message": "I've been feeling really overwhelmed lately with my thesis", "response": "I'm really sorry to hear that you've been feeling overwhelmed with your thesis. It's a common feeling among graduate students, and the pressure of completing a significant project while balancing other responsibilities can be challenging. Have you tried reaching out to your professors or peers for support or discussing your concerns with them? It might help to lighten the load or provide some perspective on your situation. How about sharing more about what specifically is causing you to feel overwhelmed?", "emotion_label": 1, "emotion_name": "anxiety", "trajectory": "stable", "crisis_fired": false, "crisis_confidence": 0.0}
7
+ {"session_id": "8727D9", "turn": 1, "timestamp": "2026-04-11T04:22:51.670438", "user_message": "I've been feeling really overwhelmed lately with my thesis", "response": "Thesis work can truly be an emotional rollercoaster, and the feeling of being overwhelmed is something many grad students have experienced.\n\nWhat's been specifically challenging for you about your thesis lately?", "emotion_label": 1, "emotion_name": "anxiety", "trajectory": "stable", "crisis_fired": false, "crisis_confidence": 0.0}
8
+ {"session_id": "8727D9", "turn": 2, "timestamp": "2026-04-11T04:23:33.658171", "user_message": "It's the deadline and the ideas I get are not that great at all, So i'm very frustrated!", "response": "Deadlines can be a powerful motivator, but when the ideas just don't seem to flow, it can be a frustrating experience.\n\nWhat specifically about the ideas for your assignment is causing you frustration? Is there a particular aspect that you're struggling with?", "emotion_label": 2, "emotion_name": "frustration", "trajectory": "de_escalating", "crisis_fired": false, "crisis_confidence": 0.0}
9
+ {"session_id": "7CF2E4", "turn": 1, "timestamp": "2026-04-11T07:44:55.653777", "user_message": "I've been feeling really overwhelmed lately with my thesis", "response": "Thesis work can truly be an emotional rollercoaster, and the feeling of being overwhelmed is something many grad students have experienced.\n\nWhat's been specifically challenging for you about your thesis lately?", "emotion_label": 1, "emotion_name": "anxiety", "trajectory": "stable", "crisis_fired": false, "crisis_confidence": 0.0}
10
+ {"session_id": "7CF2E4", "turn": 2, "timestamp": "2026-04-11T07:45:11.989209", "user_message": "It's the deadlines mostly, I have three chapters due next week", "response": "Deadlines can add a lot of pressure, especially when you have multiple chapters due in a short amount of time.\n\nHow have you been managing your time for these chapters so far? What's been working, and what areas could use some improvement?", "emotion_label": 3, "emotion_name": "neutral", "trajectory": "de_escalating", "crisis_fired": false, "crisis_confidence": 0.0}
11
+ {"session_id": "7CF2E4", "turn": 3, "timestamp": "2026-04-11T07:45:46.676207", "user_message": "My advisor keeps changing requirements and it's driving me crazy", "response": "It's understandable to feel frustrated when your advisor changes requirements, as it can make it difficult to focus on your work and meet deadlines.\n\nHave you tried having an open and honest conversation with your advisor about your concerns? They may not realize the impact of their changes on you, and discussing your situation could help bring clarity and find a solution.", "emotion_label": 2, "emotion_name": "frustration", "trajectory": "de_escalating", "crisis_fired": false, "crisis_confidence": 0.0}