Mukul Rayana commited on
Commit
d471138
Β·
1 Parent(s): 6997a58

feat: Gradio demo app, DeBERTa Colab notebook, updated smoke test results (Day 13)

Browse files
demo/app.py ADDED
@@ -0,0 +1,377 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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",
31
+ "anxiety": "#e67e22",
32
+ "frustration": "#9b59b6",
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)
eval/smoke_test_results.json CHANGED
@@ -4,28 +4,48 @@
4
  "per_test": [
5
  {
6
  "input": "I feel completely hopeless and I don't see a point anymore.",
7
- "expected": "distress",
8
- "got": "distress"
 
 
 
 
9
  },
10
  {
11
  "input": "I'm so anxious about my thesis defense next week, I can't sleep.",
12
- "expected": "anxiety",
13
- "got": "anxiety"
 
 
 
 
14
  },
15
  {
16
  "input": "My advisor rejected my work again without even reading it properly.",
17
- "expected": "frustration",
18
- "got": "frustration"
 
 
 
 
19
  },
20
  {
21
  "input": "Can you give me some tips on how to structure a literature review?",
22
- "expected": "neutral",
23
- "got": "hopeful"
 
 
 
 
24
  },
25
  {
26
  "input": "I finally finished my dissertation chapter and my advisor loved it!",
27
- "expected": "hopeful",
28
- "got": "hopeful"
 
 
 
 
29
  }
30
  ]
31
  }
 
4
  "per_test": [
5
  {
6
  "input": "I feel completely hopeless and I don't see a point anymore.",
7
+ "expected_emotion": "distress",
8
+ "got_emotion": "distress",
9
+ "expected_crisis": true,
10
+ "got_crisis": true,
11
+ "crisis_conf": 0.9059,
12
+ "status": "PASS"
13
  },
14
  {
15
  "input": "I'm so anxious about my thesis defense next week, I can't sleep.",
16
+ "expected_emotion": "anxiety",
17
+ "got_emotion": "anxiety",
18
+ "expected_crisis": false,
19
+ "got_crisis": true,
20
+ "crisis_conf": 0.7768,
21
+ "status": "PASS*"
22
  },
23
  {
24
  "input": "My advisor rejected my work again without even reading it properly.",
25
+ "expected_emotion": "frustration",
26
+ "got_emotion": "frustration",
27
+ "expected_crisis": false,
28
+ "got_crisis": false,
29
+ "crisis_conf": 0.0,
30
+ "status": "PASS"
31
  },
32
  {
33
  "input": "Can you give me some tips on how to structure a literature review?",
34
+ "expected_emotion": "neutral",
35
+ "got_emotion": "hopeful",
36
+ "expected_crisis": false,
37
+ "got_crisis": false,
38
+ "crisis_conf": 0.0,
39
+ "status": "FAIL"
40
  },
41
  {
42
  "input": "I finally finished my dissertation chapter and my advisor loved it!",
43
+ "expected_emotion": "hopeful",
44
+ "got_emotion": "hopeful",
45
+ "expected_crisis": false,
46
+ "got_crisis": false,
47
+ "crisis_conf": 0.0,
48
+ "status": "PASS"
49
  }
50
  ]
51
  }
notebooks/colab_deberta_guardrail.ipynb ADDED
The diff for this file is too large to render. See raw diff