jmisak commited on
Commit
0ff005f
·
verified ·
1 Parent(s): 44fd226

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +453 -0
  2. requirements.txt +6 -0
app.py ADDED
@@ -0,0 +1,453 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import yaml
3
+ import json
4
+ import os
5
+ import traceback
6
+ import matplotlib.pyplot as plt
7
+ import numpy as np
8
+
9
+ from engine.loader import load_persona
10
+ from engine.drift import apply_context_shift
11
+ from engine.responder import generate_response
12
+ from engine.utils import safe_log
13
+ from engine.logger import log_interaction
14
+
15
+ # Paths
16
+ persona_dir = "./personas"
17
+ contexts_path = "./contexts/scenarios.json"
18
+ error_log_path = "./ot_simulator_errors.log"
19
+
20
+ # Load available personas
21
+ def get_persona_choices():
22
+ return [f for f in os.listdir(persona_dir) if f.endswith(".yml")]
23
+
24
+ # Load available contextual scenarios
25
+ def get_scenario_choices():
26
+ try:
27
+ with open(contexts_path, "r") as f:
28
+ scenarios = json.load(f)
29
+ return [s["scenario"] for s in scenarios]
30
+ except Exception as e:
31
+ safe_log("Scenarios load error", str(e))
32
+ return []
33
+
34
+ # Generate radar chart for emotional/behavioral states
35
+ def plot_state(state, persona_name):
36
+ if persona_name == "Jack":
37
+ metrics = ["anxiety", "trust", "openness", "physical_discomfort"]
38
+ colors = ["#e74c3c", "#3498db", "#2ecc71", "#f39c12"]
39
+ elif persona_name == "Maya":
40
+ metrics = ["anxiety", "trust", "creative_engagement", "occupational_balance"]
41
+ colors = ["#e74c3c", "#3498db", "#9b59b6", "#1abc9c"]
42
+ else:
43
+ metrics = ["anxiety", "trust", "openness", "engagement"]
44
+ colors = ["#e74c3c", "#3498db", "#2ecc71", "#95a5a6"]
45
+
46
+ values = [state.get(m, 0.0) for m in metrics]
47
+ angles = np.linspace(0, 2 * np.pi, len(metrics), endpoint=False).tolist()
48
+ values += values[:1]
49
+ angles += angles[:1]
50
+
51
+ fig, ax = plt.subplots(figsize=(5, 5), subplot_kw=dict(polar=True))
52
+ ax.plot(angles, values, color=colors[0], linewidth=2)
53
+ ax.fill(angles, values, color=colors[0], alpha=0.25)
54
+ ax.set_xticks(angles[:-1])
55
+ ax.set_xticklabels([m.replace('_', ' ').title() for m in metrics])
56
+ ax.set_ylim(0, 1)
57
+ ax.set_yticklabels(['0.0', '0.2', '0.4', '0.6', '0.8', '1.0'])
58
+ ax.set_title(f"{persona_name}'s Emotional State", fontsize=14, pad=20)
59
+ ax.grid(True)
60
+ fig.tight_layout()
61
+
62
+ chart_path = f"./state_chart_{persona_name}.png"
63
+ fig.savefig(chart_path, dpi=100, bbox_inches='tight')
64
+ plt.close(fig)
65
+ return chart_path
66
+
67
+ # Generate interaction history visualization
68
+ def plot_interaction_history(history):
69
+ if not history or len(history) < 2:
70
+ return None
71
+
72
+ fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(8, 6))
73
+
74
+ interactions = list(range(1, len(history) + 1))
75
+ anxiety_vals = [h.get('anxiety', 0) for h in history]
76
+ trust_vals = [h.get('trust', 0) for h in history]
77
+
78
+ ax1.plot(interactions, anxiety_vals, marker='o', color='#e74c3c', linewidth=2, label='Anxiety')
79
+ ax1.set_ylabel('Anxiety Level', fontsize=10)
80
+ ax1.set_ylim(0, 1)
81
+ ax1.grid(True, alpha=0.3)
82
+ ax1.legend(loc='upper right')
83
+
84
+ ax2.plot(interactions, trust_vals, marker='o', color='#3498db', linewidth=2, label='Trust')
85
+ ax2.set_xlabel('Interaction Number', fontsize=10)
86
+ ax2.set_ylabel('Trust Level', fontsize=10)
87
+ ax2.set_ylim(0, 1)
88
+ ax2.grid(True, alpha=0.3)
89
+ ax2.legend(loc='upper right')
90
+
91
+ fig.suptitle('Therapeutic Relationship Over Time', fontsize=14)
92
+ fig.tight_layout()
93
+
94
+ history_path = "./interaction_history.png"
95
+ fig.savefig(history_path, dpi=100, bbox_inches='tight')
96
+ plt.close(fig)
97
+ return history_path
98
+
99
+ # Download session transcript
100
+ def download_session(conversation_history, state_history, selected_persona_file):
101
+ """Generate downloadable transcript file."""
102
+ if not conversation_history:
103
+ return None
104
+
105
+ try:
106
+ persona_path = os.path.join(persona_dir, selected_persona_file)
107
+ persona = load_persona(persona_path)
108
+
109
+ from datetime import datetime
110
+ timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
111
+ name = persona.get("persona_name", "Client")
112
+
113
+ # Create transcript
114
+ transcript = f"""
115
+ ���������������������������������������������������������������
116
+ OT MENTAL HEALTH SIMULATION - SESSION TRANSCRIPT
117
+ ���������������������������������������������������������������
118
+
119
+ Client: {name}
120
+ Date: {timestamp}
121
+ Number of Interactions: {len(conversation_history)}
122
+
123
+ ���������������������������������������������������������������
124
+ CONVERSATION
125
+ ���������������������������������������������������������������
126
+
127
+ """
128
+
129
+ for i, turn in enumerate(conversation_history, 1):
130
+ transcript += f"\n[Turn {i}]\n"
131
+ if 'scenario' in turn:
132
+ transcript += f"Context: {turn['scenario']}\n\n"
133
+ transcript += f"Student: {turn.get('student', '')}\n\n"
134
+ transcript += f"{name}: {turn.get('client', '')}\n\n"
135
+ transcript += "�" * 63 + "\n"
136
+
137
+ if state_history:
138
+ transcript += f"""
139
+ ���������������������������������������������������������������
140
+ EMOTIONAL STATE PROGRESSION
141
+ ���������������������������������������������������������������
142
+
143
+ Initial State:
144
+ Anxiety: {state_history[0].get('anxiety', 0):.2f}
145
+ Trust: {state_history[0].get('trust', 0):.2f}
146
+ Openness: {state_history[0].get('openness', 0):.2f}
147
+
148
+ Final State:
149
+ Anxiety: {state_history[-1].get('anxiety', 0):.2f}
150
+ Trust: {state_history[-1].get('trust', 0):.2f}
151
+ Openness: {state_history[-1].get('openness', 0):.2f}
152
+
153
+ Change:
154
+ Anxiety: {state_history[-1].get('anxiety', 0) - state_history[0].get('anxiety', 0):+.2f}
155
+ Trust: {state_history[-1].get('trust', 0) - state_history[0].get('trust', 0):+.2f}
156
+ Openness: {state_history[-1].get('openness', 0) - state_history[0].get('openness', 0):+.2f}
157
+
158
+ ���������������������������������������������������������������
159
+ """
160
+
161
+ # Save to temporary file
162
+ filename = f"{name}_{timestamp}.txt"
163
+ filepath = os.path.join("transcripts", filename)
164
+ os.makedirs("transcripts", exist_ok=True)
165
+
166
+ with open(filepath, "w", encoding="utf-8") as f:
167
+ f.write(transcript)
168
+
169
+ return filepath
170
+
171
+ except Exception as e:
172
+ safe_log("Download error", str(e))
173
+ return None
174
+
175
+ # Main simulation function
176
+ def simulate(student_prompt, selected_scenario, selected_persona_file, conversation_history, state_history):
177
+ try:
178
+ persona_path = os.path.join(persona_dir, selected_persona_file)
179
+ persona = load_persona(persona_path)
180
+
181
+ # Parse conversation history
182
+ if conversation_history is None:
183
+ conversation_history = []
184
+ if state_history is None:
185
+ state_history = []
186
+
187
+ # Load and apply contextual scenario
188
+ with open(contexts_path, "r") as f:
189
+ scenarios = json.load(f)
190
+ scenario = next((s for s in scenarios if s["scenario"] == selected_scenario), None)
191
+
192
+ if scenario:
193
+ persona = apply_context_shift(persona, scenario)
194
+ context_note = f"**Context:** {scenario.get('description', selected_scenario)}\n\n"
195
+ else:
196
+ context_note = ""
197
+
198
+ # Generate response
199
+ response, updated_state, teaching_note = generate_response(
200
+ student_prompt,
201
+ persona,
202
+ conversation_history
203
+ )
204
+
205
+ # Update conversation history
206
+ conversation_history.append({
207
+ "student": student_prompt,
208
+ "client": response,
209
+ "scenario": selected_scenario
210
+ })
211
+
212
+ # Track state history
213
+ state_history.append(updated_state.copy())
214
+
215
+ # Format conversation display
216
+ conversation_display = ""
217
+ for i, turn in enumerate(conversation_history, 1):
218
+ conversation_display += f"**Turn {i}**\n"
219
+ if 'scenario' in turn:
220
+ conversation_display += f"*[Context: {turn['scenario']}]*\n"
221
+ conversation_display += f"**Student:** {turn['student']}\n\n"
222
+ conversation_display += f"**{persona['persona_name']}:** {turn['client']}\n\n"
223
+ conversation_display += "---\n\n"
224
+
225
+ # Generate visualizations
226
+ state_yaml = yaml.dump(updated_state, sort_keys=False)
227
+ current_chart = plot_state(updated_state, persona['persona_name'])
228
+ history_chart = plot_interaction_history(state_history)
229
+
230
+ # Format teaching feedback
231
+ teaching_feedback = f"**Teaching Note:**\n{teaching_note}\n\n"
232
+ teaching_feedback += f"**Current State:**\n"
233
+ teaching_feedback += f"- Anxiety: {updated_state.get('anxiety', 0):.2f}\n"
234
+ teaching_feedback += f"- Trust: {updated_state.get('trust', 0):.2f}\n"
235
+ teaching_feedback += f"- Openness: {updated_state.get('openness', 0):.2f}\n"
236
+
237
+ if 'emotional_memory' in updated_state and updated_state['emotional_memory']:
238
+ teaching_feedback += f"\n**Emotional Memory:** {updated_state['emotional_memory'][-1]}\n"
239
+
240
+ # Log interaction
241
+ transcript_path = log_interaction(
242
+ persona,
243
+ student_prompt,
244
+ selected_scenario,
245
+ response,
246
+ updated_state,
247
+ teaching_note
248
+ )
249
+
250
+ return (
251
+ conversation_display,
252
+ teaching_feedback,
253
+ state_yaml,
254
+ current_chart,
255
+ history_chart,
256
+ conversation_history,
257
+ state_history
258
+ )
259
+
260
+ except Exception as e:
261
+ error_msg = traceback.format_exc()
262
+ safe_log("Simulation error", error_msg)
263
+ return (
264
+ "[ERROR] Simulation failed. Check logs.",
265
+ "Error occurred",
266
+ "",
267
+ None,
268
+ None,
269
+ conversation_history,
270
+ state_history
271
+ )
272
+
273
+ # Gradio UI
274
+ with gr.Blocks(title="OT Mental Health Simulator", theme=gr.themes.Soft()) as ui:
275
+ gr.Markdown("""
276
+ # ?? OT Mental Health Training Simulator
277
+ ### Practice therapeutic communication with realistic client personas
278
+
279
+ This simulator helps OT students develop skills in:
280
+ - Therapeutic rapport building
281
+ - Active listening and validation
282
+ - Recognizing emotional states and triggers
283
+ - Adapting communication based on client responses
284
+ - Understanding occupational impacts of mental health
285
+ """)
286
+
287
+ # State management
288
+ conversation_state = gr.State([])
289
+ state_history = gr.State([])
290
+
291
+ with gr.Row():
292
+ with gr.Column(scale=1):
293
+ persona_files = get_persona_choices()
294
+ default_persona = persona_files[0] if persona_files else None
295
+
296
+ persona_selector = gr.Dropdown(
297
+ label="Select Client",
298
+ choices=persona_files,
299
+ value=default_persona,
300
+ allow_custom_value=False,
301
+ info="Choose which client you'll be working with"
302
+ )
303
+
304
+ scenario_selector = gr.Dropdown(
305
+ label="Contextual Scenario",
306
+ choices=get_scenario_choices(),
307
+ value=get_scenario_choices()[0] if get_scenario_choices() else None,
308
+ info="What's happening in the client's life right now?"
309
+ )
310
+
311
+ gr.Markdown("---")
312
+
313
+ with gr.Accordion("Client Information", open=False):
314
+ gr.Markdown("""
315
+ **Jack (22)** - Construction worker navigating chronic pain, family dynamics,
316
+ burnout, and identity questions around gaming vs. career.
317
+
318
+ **Maya (24)** - Graphic designer experiencing creative burnout, work-life imbalance,
319
+ anxiety, and physical symptoms from overwork.
320
+ """)
321
+
322
+ with gr.Column(scale=2):
323
+ conversation_display = gr.Markdown(
324
+ label="Conversation History",
325
+ value="*Conversation will appear here...*"
326
+ )
327
+
328
+ with gr.Row():
329
+ student_prompt = gr.Textbox(
330
+ label="Your Response (as OT student/practitioner)",
331
+ lines=3,
332
+ placeholder="Type your therapeutic response here...\nExample: 'It sounds like work has been really demanding lately. How has that been affecting your daily routine?'"
333
+ )
334
+
335
+ with gr.Row():
336
+ send_btn = gr.Button("Send Response", variant="primary", size="lg")
337
+ download_btn = gr.Button("?? Download Session", size="lg")
338
+ reset_btn = gr.Button("Reset Conversation", size="lg")
339
+
340
+ with gr.Row():
341
+ with gr.Column():
342
+ teaching_output = gr.Markdown(label="Teaching Feedback")
343
+
344
+ with gr.Column():
345
+ state_output = gr.Textbox(
346
+ label="Technical State (for debugging)",
347
+ lines=8,
348
+ visible=False # Hide by default, can be toggled
349
+ )
350
+
351
+ with gr.Row():
352
+ with gr.Column():
353
+ current_state_chart = gr.Image(label="Current Emotional State")
354
+
355
+ with gr.Column():
356
+ history_chart = gr.Image(label="Progress Over Time")
357
+
358
+ # Hidden file output for downloads
359
+ download_file = gr.File(label="Your session transcript (right-click to save)", visible=True)
360
+
361
+ with gr.Accordion("Instructor Guide", open=False):
362
+ gr.Markdown("""
363
+ ### How to Use This Simulator
364
+
365
+ 1. **Select a client** (Jack or Maya)
366
+ 2. **Choose a scenario** to set the context
367
+ 3. **Type your therapeutic response** as if you were the OT practitioner
368
+ 4. **Observe** how the client responds based on:
369
+ - Their emotional state (anxiety, trust, openness)
370
+ - The scenario context
371
+ - Your communication approach
372
+ 5. **Review teaching feedback** to understand what worked or didn't
373
+ 6. **Track progress** through the radar chart and history graph
374
+
375
+ ### Teaching Points
376
+ - Notice how validation vs. advice-giving affects trust
377
+ - Observe how pacing affects client openness
378
+ - See how boundary-setting affects the therapeutic relationship
379
+ - Track how emotional states shift with different approaches
380
+
381
+ ### Sample Scenarios to Explore
382
+ - Building initial rapport
383
+ - Responding to resistance
384
+ - Addressing pain and physical symptoms
385
+ - Exploring occupational balance
386
+ - Managing therapeutic ruptures
387
+ - Crisis response
388
+ """)
389
+
390
+ # Button actions
391
+ send_btn.click(
392
+ fn=simulate,
393
+ inputs=[
394
+ student_prompt,
395
+ scenario_selector,
396
+ persona_selector,
397
+ conversation_state,
398
+ state_history
399
+ ],
400
+ outputs=[
401
+ conversation_display,
402
+ teaching_output,
403
+ state_output,
404
+ current_state_chart,
405
+ history_chart,
406
+ conversation_state,
407
+ state_history
408
+ ]
409
+ )
410
+
411
+ download_btn.click(
412
+ fn=download_session,
413
+ inputs=[
414
+ conversation_state,
415
+ state_history,
416
+ persona_selector
417
+ ],
418
+ outputs=download_file
419
+ )
420
+
421
+ def reset_conversation():
422
+ return (
423
+ "*Conversation will appear here...*",
424
+ "",
425
+ "",
426
+ None,
427
+ None,
428
+ [],
429
+ []
430
+ )
431
+
432
+ reset_btn.click(
433
+ fn=reset_conversation,
434
+ inputs=[],
435
+ outputs=[
436
+ conversation_display,
437
+ teaching_output,
438
+ state_output,
439
+ current_state_chart,
440
+ history_chart,
441
+ conversation_state,
442
+ state_history
443
+ ]
444
+ )
445
+
446
+ if __name__ == "__main__":
447
+ # Create necessary directories
448
+ os.makedirs("personas", exist_ok=True)
449
+ os.makedirs("contexts", exist_ok=True)
450
+ os.makedirs("transcripts", exist_ok=True)
451
+ os.makedirs("engine", exist_ok=True)
452
+
453
+ ui.launch(share=False, server_name="0.0.0.0", server_port=7860)
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ gradio>=4.0.0
2
+ pyyaml>=6.0
3
+ matplotlib>=3.5.0
4
+ numpy>=1.21.0
5
+ huggingface-hub>=0.20.0
6
+ anthropic>=0.18.0