dyadd commited on
Commit
5ef39ed
·
verified ·
1 Parent(s): 124b7ba

Upload folder using huggingface_hub

Browse files
nbs/01_clinical_tutor.ipynb CHANGED
@@ -1,588 +1,575 @@
1
- {
2
- "cells": [
3
- {
4
- "cell_type": "code",
5
- "execution_count": null,
6
- "id": "d9a32ad5-9f07-47f2-97ae-15b0646e355b",
7
- "metadata": {},
8
- "outputs": [],
9
- "source": [
10
- "#| default_exp clinical_tutor"
11
- ]
12
- },
13
- {
14
- "cell_type": "markdown",
15
- "id": "0db4b759-310c-4e38-9fdc-2efb94b541dd",
16
- "metadata": {},
17
- "source": [
18
- "# Clinical Tutor\n",
19
- "\n",
20
- "> Core module for using learning context for context-appropriate tutor responses\n"
21
- ]
22
- },
23
- {
24
- "cell_type": "markdown",
25
- "id": "16f93992-88dd-409b-8370-b86302e1ce6a",
26
- "metadata": {},
27
- "source": [
28
- "## Setup"
29
- ]
30
- },
31
- {
32
- "cell_type": "code",
33
- "execution_count": null,
34
- "id": "6d2403bb-70a1-4744-be0b-d259234c1b62",
35
- "metadata": {},
36
- "outputs": [],
37
- "source": [
38
- "#| hide\n",
39
- "from nbdev.showdoc import *"
40
- ]
41
- },
42
- {
43
- "cell_type": "code",
44
- "execution_count": null,
45
- "id": "477ba22b-55c1-467e-8206-a92f88a598fd",
46
- "metadata": {},
47
- "outputs": [
48
- {
49
- "name": "stderr",
50
- "output_type": "stream",
51
- "text": [
52
- "C:\\Users\\deepa\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\tqdm\\auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n",
53
- " from .autonotebook import tqdm as notebook_tqdm\n"
54
- ]
55
- }
56
- ],
57
- "source": [
58
- "#| export\n",
59
- "from typing import Dict, List, Optional, Any, Tuple\n",
60
- "import os\n",
61
- "import json\n",
62
- "import logging\n",
63
- "from pathlib import Path\n",
64
- "from datetime import datetime\n",
65
- "from dotenv import load_dotenv\n",
66
- "import aiohttp\n",
67
- "import re\n",
68
- "from collections import defaultdict\n",
69
- "from wardbuddy.learning_context import LearningContext, setup_logger\n",
70
- "\n",
71
- "# Load environment variables\n",
72
- "load_dotenv()\n",
73
- "\n",
74
- "logger = setup_logger(__name__)"
75
- ]
76
- },
77
- {
78
- "cell_type": "markdown",
79
- "id": "7da445d7-7f51-44d5-b027-e2cf65c79069",
80
- "metadata": {},
81
- "source": [
82
- "## Adaptive Clinical Tutor"
83
- ]
84
- },
85
- {
86
- "cell_type": "markdown",
87
- "id": "58d76615-480c-4fe4-9d4a-e67dd892132a",
88
- "metadata": {},
89
- "source": [
90
- "This module implements:\n",
91
- "\n",
92
- " - Engages in natural case discussions like a clinical supervisor\n",
93
- " - Provides context-aware feedback based on student's rotation and preferences\n",
94
- " - Analyzes discussions to track learning progress\n",
95
- " - Integrates with the student's learning context\n",
96
- "\n",
97
- "The tutor aims to mimic real-world clinical teaching interactions where students present cases and receive feedback in a natural conversational style.\n"
98
- ]
99
- },
100
- {
101
- "cell_type": "code",
102
- "execution_count": null,
103
- "id": "da7d2115-b30f-40ba-9566-bcbaf155026d",
104
- "metadata": {},
105
- "outputs": [],
106
- "source": [
107
- "#| export \n",
108
- "class OpenRouterException(Exception):\n",
109
- " \"\"\"Custom exception for OpenRouter API errors\"\"\"\n",
110
- " pass"
111
- ]
112
- },
113
- {
114
- "cell_type": "code",
115
- "execution_count": null,
116
- "id": "75c2cbfc-75e7-45ee-9d86-b931f81a3ad5",
117
- "metadata": {},
118
- "outputs": [],
119
- "source": [
120
- "#| export\n",
121
- "class ClinicalTutor:\n",
122
- " \"\"\"\n",
123
- " Adaptive clinical teaching module that provides context-aware feedback.\n",
124
- " \n",
125
- " The tutor acts as an experienced clinical supervisor, engaging in natural\n",
126
- " case discussions while tracking student progress and adapting feedback\n",
127
- " based on learning context.\n",
128
- " \n",
129
- " Attributes:\n",
130
- " learning_context (LearningContext): Student's learning context\n",
131
- " model (str): LLM model identifier\n",
132
- " api_url (str): OpenRouter API endpoint\n",
133
- " \"\"\"\n",
134
- " \n",
135
- " def __init__(\n",
136
- " self,\n",
137
- " context_path: Optional[Path] = None,\n",
138
- " model: str = \"anthropic/claude-3.5-sonnet\"\n",
139
- " ):\n",
140
- " \"\"\"\n",
141
- " Initialize clinical tutor.\n",
142
- " \n",
143
- " Args:\n",
144
- " context_path: Optional path for context persistence\n",
145
- " model: Model identifier for OpenRouter\n",
146
- " \"\"\"\n",
147
- " self.api_key: str = os.getenv(\"OPENROUTER_API_KEY\")\n",
148
- " if not self.api_key:\n",
149
- " raise ValueError(\"OpenRouter API key not found\")\n",
150
- " \n",
151
- " self.api_url: str = \"https://openrouter.ai/api/v1/chat/completions\"\n",
152
- " self.model: str = model\n",
153
- " \n",
154
- " self.learning_context = LearningContext(context_path)\n",
155
- " self.context_path = context_path\n",
156
- " \n",
157
- " # Track conversation state\n",
158
- " self.current_case: Dict = {\n",
159
- " \"started\": None,\n",
160
- " \"chief_complaint\": None,\n",
161
- " \"key_findings\": [],\n",
162
- " \"assessment\": None,\n",
163
- " \"plan\": None\n",
164
- " }\n",
165
- " \n",
166
- " logger.info(f\"Clinical tutor initialized with model: {model}\")\n",
167
- " \n",
168
- " async def _get_completion(\n",
169
- " self,\n",
170
- " messages: List[Dict],\n",
171
- " temperature: float = 0.7,\n",
172
- " max_retries: int = 3\n",
173
- " ) -> str:\n",
174
- " \"\"\"\n",
175
- " Get completion from OpenRouter API with retry logic.\n",
176
- " \n",
177
- " Args:\n",
178
- " messages: List of conversation messages\n",
179
- " temperature: Temperature for response generation\n",
180
- " max_retries: Maximum retry attempts\n",
181
- " \n",
182
- " Returns:\n",
183
- " str: Model response\n",
184
- " \n",
185
- " Raises:\n",
186
- " OpenRouterException: If API calls fail after retries\n",
187
- " \"\"\"\n",
188
- " headers = {\n",
189
- " \"Authorization\": f\"Bearer {self.api_key}\",\n",
190
- " \"Content-Type\": \"application/json\",\n",
191
- " \"HTTP-Referer\": \"http://localhost:7860\"\n",
192
- " }\n",
193
- " \n",
194
- " data = {\n",
195
- " \"model\": self.model,\n",
196
- " \"messages\": messages,\n",
197
- " \"temperature\": temperature,\n",
198
- " \"max_tokens\": 2000\n",
199
- " }\n",
200
- " \n",
201
- " for attempt in range(max_retries):\n",
202
- " try:\n",
203
- " async with aiohttp.ClientSession() as session:\n",
204
- " async with session.post(\n",
205
- " self.api_url,\n",
206
- " headers=headers,\n",
207
- " json=data,\n",
208
- " timeout=30\n",
209
- " ) as response:\n",
210
- " response.raise_for_status()\n",
211
- " result = await response.json()\n",
212
- " return result[\"choices\"][0][\"message\"][\"content\"]\n",
213
- " \n",
214
- " except Exception as e:\n",
215
- " if attempt == max_retries - 1:\n",
216
- " raise OpenRouterException(f\"API call failed: {str(e)}\")\n",
217
- " logger.warning(f\"Retry {attempt + 1} after error: {str(e)}\")\n",
218
- " # Could add exponential backoff here if needed\n",
219
- " \n",
220
- " def _build_discussion_prompt(self) -> str:\n",
221
- " \"\"\"Build context-aware prompt for case discussion.\"\"\"\n",
222
- " rotation = self.learning_context.current_rotation\n",
223
- " active_preferences = [\n",
224
- " p[\"focus\"] for p in self.learning_context.feedback_preferences \n",
225
- " if p[\"active\"]\n",
226
- " ]\n",
227
- " \n",
228
- " significant_gaps = {\n",
229
- " topic: score for topic, score \n",
230
- " in self.learning_context.knowledge_profile[\"gaps\"].items()\n",
231
- " if score < 0.7 # Only include significant gaps\n",
232
- " }\n",
233
- " \n",
234
- " prompt = f\"\"\"You are an experienced clinical supervisor in {rotation['specialty']}. Act as an engaging and conversational tutor who coaches towards deeper understanding through Socratic dialogue and targeted questions.\n",
235
- "\n",
236
- " Key Principles:\n",
237
- " 1. Assume I have strong foundational knowledge in medicine, clinical reasoning, and pre-medical sciences\n",
238
- " 2. Focus on high-level connections and nuanced clinical decision-making\n",
239
- " 3. Use targeted questions to explore my thought process and highlight key learning points\n",
240
- " 4. Share relevant clinical pearls and real-world applications\n",
241
- " 5. Be conversational and engaging, avoiding lecture-style responses\n",
242
- " \n",
243
- " Current Rotation Focus Areas:\n",
244
- " {', '.join(rotation['key_focus_areas'])}\n",
245
- "\n",
246
- " Areas for Deep Dive:\n",
247
- " {', '.join(f'{topic} (confidence: {score:.1f})' for topic, score in significant_gaps.items()) if significant_gaps else 'General clinical reasoning'}\n",
248
- "\n",
249
- " Student's Interests:\n",
250
- " {', '.join(active_preferences) if active_preferences else 'Broad clinical discussion'}\n",
251
- "\n",
252
- " Engage as a supportive colleague who challenges thinking through conversation. Ask probing questions \n",
253
- " that explore clinical reasoning and highlight important connections. I will ask for clarification \n",
254
- " if concepts need more explanation.\"\"\"\n",
255
- "\n",
256
- " return prompt\n",
257
- " \n",
258
- " def _build_analysis_prompt(self, conversation: List[Dict[str, str]]) -> str:\n",
259
- " \"\"\"\n",
260
- " Build prompt for post-discussion analysis.\n",
261
- " \n",
262
- " Args:\n",
263
- " conversation: List of message dictionaries with roles and content\n",
264
- " \n",
265
- " Returns:\n",
266
- " str: Analysis prompt\n",
267
- " \"\"\"\n",
268
- " # Extract case details\n",
269
- " case_content = \"\"\n",
270
- " for msg in conversation:\n",
271
- " if msg[\"role\"] == \"user\":\n",
272
- " case_content += msg[\"content\"] + \"\\n\"\n",
273
- " \n",
274
- " return f\"\"\"Analyze the following case discussion between a medical student and \n",
275
- " clinical supervisor. Focus on the student's demonstrated knowledge, skills, \n",
276
- " and areas for improvement.\n",
277
- "\n",
278
- " Case Content:\n",
279
- " {case_content}\n",
280
- "\n",
281
- " Please identify:\n",
282
- " 1. Key clinical concepts and learning points demonstrated or discussed\n",
283
- " 2. Areas where the student showed uncertainty or knowledge gaps\n",
284
- " 3. Strengths demonstrated in clinical reasoning and presentation\n",
285
- " 4. Specific learning objectives that would help the student's development\n",
286
- "\n",
287
- " Frame your response to help with ongoing learning:\n",
288
- " - Start with positive observations\n",
289
- " - Be specific about knowledge gaps\n",
290
- " - Make concrete suggestions for improvement\n",
291
- " - Connect to practical clinical scenarios\"\"\"\n",
292
- " \n",
293
- " async def discuss_case(\n",
294
- " self, \n",
295
- " message: str,\n",
296
- " temperature: float = 0.7\n",
297
- " ) -> str:\n",
298
- " \"\"\"\n",
299
- " Natural case discussion with context-aware responses.\n",
300
- " \n",
301
- " Args:\n",
302
- " message: Student's input message\n",
303
- " temperature: Temperature for response generation\n",
304
- " \n",
305
- " Returns:\n",
306
- " str: Clinical supervisor's response\n",
307
- " \"\"\"\n",
308
- " try:\n",
309
- " # Update case tracking\n",
310
- " if not self.current_case[\"started\"]:\n",
311
- " self.current_case[\"started\"] = datetime.now()\n",
312
- " # Try to identify chief complaint from first message\n",
313
- " cc_match = re.search(r\"(\\d+)\\s*[yY][oO]\\s*[MmFf]\\s*with\\s*([^.]*)\", message)\n",
314
- " if cc_match:\n",
315
- " self.current_case[\"chief_complaint\"] = cc_match.group(2).strip()\n",
316
- " \n",
317
- " # Build system prompt\n",
318
- " system_prompt = self._build_discussion_prompt()\n",
319
- " \n",
320
- " messages = [{\n",
321
- " \"role\": \"system\",\n",
322
- " \"content\": system_prompt\n",
323
- " }, {\n",
324
- " \"role\": \"user\",\n",
325
- " \"content\": message\n",
326
- " }]\n",
327
- " \n",
328
- " response = await self._get_completion(messages, temperature)\n",
329
- " return response\n",
330
- " \n",
331
- " except Exception as e:\n",
332
- " logger.error(f\"Error in case discussion: {str(e)}\")\n",
333
- " return \"I apologize, but I encountered an error. Please try presenting your case again.\"\n",
334
- " \n",
335
- " async def analyze_discussion(\n",
336
- " self,\n",
337
- " conversation: List[Dict[str, str]]\n",
338
- " ) -> Dict[str, Any]:\n",
339
- " \"\"\"\n",
340
- " Analyze completed case discussion for learning insights.\n",
341
- " \n",
342
- " Args:\n",
343
- " conversation: List of message dictionaries with roles and content\n",
344
- " \n",
345
- " Returns:\n",
346
- " dict: Analysis results containing:\n",
347
- " - learning_points: List of key concepts learned\n",
348
- " - gaps: Dict of identified knowledge gaps\n",
349
- " - strengths: List of demonstrated strengths\n",
350
- " - suggested_objectives: List of recommended learning goals\n",
351
- " \"\"\"\n",
352
- " try:\n",
353
- " # Reset case tracking\n",
354
- " self.current_case = {\n",
355
- " \"started\": None,\n",
356
- " \"chief_complaint\": None,\n",
357
- " \"key_findings\": [],\n",
358
- " \"assessment\": None,\n",
359
- " \"plan\": None\n",
360
- " }\n",
361
- " \n",
362
- " # Get analysis from model\n",
363
- " analysis_prompt = self._build_analysis_prompt(conversation)\n",
364
- " messages = [{\n",
365
- " \"role\": \"system\",\n",
366
- " \"content\": analysis_prompt\n",
367
- " }]\n",
368
- " messages.extend(conversation)\n",
369
- " \n",
370
- " response = await self._get_completion(messages, temperature=0.3)\n",
371
- " \n",
372
- " # Parse insights\n",
373
- " insights = self._parse_analysis(response)\n",
374
- " \n",
375
- " # Update learning context\n",
376
- " self._update_context_from_analysis(insights)\n",
377
- " \n",
378
- " return insights\n",
379
- " \n",
380
- " except Exception as e:\n",
381
- " logger.error(f\"Error in discussion analysis: {str(e)}\")\n",
382
- " return {\n",
383
- " \"learning_points\": [],\n",
384
- " \"gaps\": {},\n",
385
- " \"strengths\": [],\n",
386
- " \"suggested_objectives\": []\n",
387
- " }\n",
388
- " \n",
389
- " def _parse_analysis(self, response: str) -> Dict[str, Any]:\n",
390
- " \"\"\"\n",
391
- " Parse analysis response into structured insights.\n",
392
- " \n",
393
- " Uses pattern matching and basic NLP to extract:\n",
394
- " - Learning points (key concepts discussed)\n",
395
- " - Knowledge gaps with confidence estimates\n",
396
- " - Demonstrated strengths\n",
397
- " - Suggested learning objectives\n",
398
- " \n",
399
- " Args:\n",
400
- " response: Raw analysis response\n",
401
- " \n",
402
- " Returns:\n",
403
- " dict: Structured analysis insights\n",
404
- " \"\"\"\n",
405
- " insights = {\n",
406
- " \"learning_points\": [],\n",
407
- " \"gaps\": {},\n",
408
- " \"strengths\": [],\n",
409
- " \"suggested_objectives\": []\n",
410
- " }\n",
411
- " \n",
412
- " try:\n",
413
- " # Split into sections\n",
414
- " sections = response.lower().split(\"\\n\\n\")\n",
415
- " \n",
416
- " for section in sections:\n",
417
- " if \"learning point\" in section or \"key concept\" in section:\n",
418
- " # Extract bullet points or numbered items\n",
419
- " points = re.findall(r\"[-•*]\\s*(.+)$\", section, re.MULTILINE)\n",
420
- " insights[\"learning_points\"].extend(points)\n",
421
- " \n",
422
- " elif \"gap\" in section or \"uncertainty\" in section:\n",
423
- " # Look for topic mentions with confidence indicators\n",
424
- " gaps = re.findall(\n",
425
- " r\"(limited|uncertain|unclear|difficulty with)\\s+([^,.]+)\", \n",
426
- " section\n",
427
- " )\n",
428
- " for indicator, topic in gaps:\n",
429
- " # Estimate confidence based on language\n",
430
- " confidence = 0.4 if \"limited\" in indicator else 0.6\n",
431
- " insights[\"gaps\"][topic.strip()] = confidence\n",
432
- " \n",
433
- " elif \"strength\" in section or \"demonstrated\" in section:\n",
434
- " # Extract positive mentions\n",
435
- " strengths = re.findall(r\"[-•*]\\s*(.+)$\", section, re.MULTILINE)\n",
436
- " insights[\"strengths\"].extend(strengths)\n",
437
- " \n",
438
- " elif \"objective\" in section or \"suggest\" in section:\n",
439
- " # Extract recommended objectives\n",
440
- " objectives = re.findall(r\"[-•*]\\s*(.+)$\", section, re.MULTILINE)\n",
441
- " insights[\"suggested_objectives\"].extend(objectives)\n",
442
- " \n",
443
- " return insights\n",
444
- " \n",
445
- " except Exception as e:\n",
446
- " logger.error(f\"Error parsing analysis: {str(e)}\")\n",
447
- " return insights\n",
448
- " \n",
449
- " def _update_context_from_analysis(self, insights: Dict[str, Any]) -> None:\n",
450
- " \"\"\"\n",
451
- " Update learning context based on discussion analysis.\n",
452
- " \n",
453
- " Args:\n",
454
- " insights: Dictionary of analysis insights\n",
455
- " \"\"\"\n",
456
- " try:\n",
457
- " # Update knowledge gaps\n",
458
- " for topic, confidence in insights[\"gaps\"].items():\n",
459
- " self.learning_context.update_knowledge_gap(topic, confidence)\n",
460
- " \n",
461
- " # Add strengths\n",
462
- " for strength in insights[\"strengths\"]:\n",
463
- " self.learning_context.add_strength(strength)\n",
464
- " \n",
465
- " # Save context if path provided\n",
466
- " if self.context_path:\n",
467
- " self.learning_context.save_context(self.context_path)\n",
468
- " \n",
469
- " except Exception as e:\n",
470
- " logger.error(f\"Error updating context: {str(e)}\")"
471
- ]
472
- },
473
- {
474
- "cell_type": "markdown",
475
- "id": "6a2b15f5-6841-43cb-9b57-c0e3f1a0b0c2",
476
- "metadata": {},
477
- "source": [
478
- "## Tests"
479
- ]
480
- },
481
- {
482
- "cell_type": "code",
483
- "execution_count": null,
484
- "id": "67ee6bde-4ade-448e-a831-86f9f7ae82ea",
485
- "metadata": {},
486
- "outputs": [
487
- {
488
- "ename": "RuntimeError",
489
- "evalue": "asyncio.run() cannot be called from a running event loop",
490
- "output_type": "error",
491
- "traceback": [
492
- "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
493
- "\u001b[1;31mRuntimeError\u001b[0m Traceback (most recent call last)",
494
- "Cell \u001b[1;32mIn[7], line 55\u001b[0m\n\u001b[0;32m 53\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;18m__name__\u001b[39m \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m__main__\u001b[39m\u001b[38;5;124m\"\u001b[39m:\n\u001b[0;32m 54\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01masyncio\u001b[39;00m\n\u001b[1;32m---> 55\u001b[0m \u001b[43masyncio\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mrun\u001b[49m\u001b[43m(\u001b[49m\u001b[43mtest_clinical_tutor\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[43m)\u001b[49m\n",
495
- "File \u001b[1;32m~\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\asyncio\\runners.py:190\u001b[0m, in \u001b[0;36mrun\u001b[1;34m(main, debug, loop_factory)\u001b[0m\n\u001b[0;32m 161\u001b[0m \u001b[38;5;250m\u001b[39m\u001b[38;5;124;03m\"\"\"Execute the coroutine and return the result.\u001b[39;00m\n\u001b[0;32m 162\u001b[0m \n\u001b[0;32m 163\u001b[0m \u001b[38;5;124;03mThis function runs the passed coroutine, taking care of\u001b[39;00m\n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 186\u001b[0m \u001b[38;5;124;03m asyncio.run(main())\u001b[39;00m\n\u001b[0;32m 187\u001b[0m \u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[0;32m 188\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m events\u001b[38;5;241m.\u001b[39m_get_running_loop() \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[0;32m 189\u001b[0m \u001b[38;5;66;03m# fail fast with short traceback\u001b[39;00m\n\u001b[1;32m--> 190\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mRuntimeError\u001b[39;00m(\n\u001b[0;32m 191\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124masyncio.run() cannot be called from a running event loop\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m 193\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m Runner(debug\u001b[38;5;241m=\u001b[39mdebug, loop_factory\u001b[38;5;241m=\u001b[39mloop_factory) \u001b[38;5;28;01mas\u001b[39;00m runner:\n\u001b[0;32m 194\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m runner\u001b[38;5;241m.\u001b[39mrun(main)\n",
496
- "\u001b[1;31mRuntimeError\u001b[0m: asyncio.run() cannot be called from a running event loop"
497
- ]
498
- }
499
- ],
500
- "source": [
501
- "async def test_clinical_tutor():\n",
502
- " \"\"\"Test ClinicalTutor functionality\"\"\"\n",
503
- " if not os.getenv(\"OPENROUTER_API_KEY\"):\n",
504
- " print(\"Skipping tests: No API key\")\n",
505
- " return\n",
506
- " \n",
507
- " tutor = ClinicalTutor()\n",
508
- " \n",
509
- " # Test case discussion\n",
510
- " test_case = \"\"\"\n",
511
- " 28yo M with chest pain\n",
512
- " - 2 days duration\n",
513
- " - Sharp, pleuritic\n",
514
- " - No fever or cough\n",
515
- " - Vitals stable\n",
516
- " - Clear exam\n",
517
- " A/P: Likely MSK pain\n",
518
- " \"\"\"\n",
519
- " \n",
520
- " try:\n",
521
- " # Test basic discussion\n",
522
- " response = await tutor.discuss_case(test_case)\n",
523
- " assert isinstance(response, str)\n",
524
- " assert len(response) > 0\n",
525
- " \n",
526
- " # Only assert case tracking if chief complaint was detected\n",
527
- " if tutor.current_case[\"chief_complaint\"]:\n",
528
- " assert \"chest pain\" in tutor.current_case[\"chief_complaint\"].lower()\n",
529
- " \n",
530
- " print(\"Discussion test passed\")\n",
531
- " \n",
532
- " # Test discussion analysis\n",
533
- " conversation = [\n",
534
- " {\"role\": \"user\", \"content\": test_case},\n",
535
- " {\"role\": \"assistant\", \"content\": response}\n",
536
- " ]\n",
537
- " \n",
538
- " analysis = await tutor.analyze_discussion(conversation)\n",
539
- " assert isinstance(analysis, dict)\n",
540
- " assert all(k in analysis for k in [\n",
541
- " 'learning_points', 'gaps', 'strengths', 'suggested_objectives'\n",
542
- " ])\n",
543
- " print(\"Analysis test passed\")\n",
544
- " \n",
545
- " except Exception as e:\n",
546
- " print(f\"Test failed: {str(e)}\")\n",
547
- " raise\n",
548
- " \n",
549
- " print(\"All clinical tutor tests passed!\")\n",
550
- "\n",
551
- "# Run tests\n",
552
- "if __name__ == \"__main__\":\n",
553
- " import asyncio\n",
554
- " if not asyncio.get_event_loop().is_running():\n",
555
- " asyncio.run(test_clinical_tutor())"
556
- ]
557
- },
558
- {
559
- "cell_type": "code",
560
- "execution_count": null,
561
- "id": "3f469c37-afe3-4682-9cc4-40326ac21b74",
562
- "metadata": {},
563
- "outputs": [],
564
- "source": []
565
- }
566
- ],
567
- "metadata": {
568
- "kernelspec": {
569
- "display_name": "Python 3 (ipykernel)",
570
- "language": "python",
571
- "name": "python3"
572
- },
573
- "language_info": {
574
- "codemirror_mode": {
575
- "name": "ipython",
576
- "version": 3
577
- },
578
- "file_extension": ".py",
579
- "mimetype": "text/x-python",
580
- "name": "python",
581
- "nbconvert_exporter": "python",
582
- "pygments_lexer": "ipython3",
583
- "version": "3.12.7"
584
- }
585
- },
586
- "nbformat": 4,
587
- "nbformat_minor": 5
588
- }
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "code",
5
+ "execution_count": null,
6
+ "id": "d9a32ad5-9f07-47f2-97ae-15b0646e355b",
7
+ "metadata": {},
8
+ "outputs": [],
9
+ "source": [
10
+ "#| default_exp clinical_tutor"
11
+ ]
12
+ },
13
+ {
14
+ "cell_type": "markdown",
15
+ "id": "0db4b759-310c-4e38-9fdc-2efb94b541dd",
16
+ "metadata": {},
17
+ "source": [
18
+ "# Clinical Tutor\n",
19
+ "\n",
20
+ "> Core module for using learning context for context-appropriate tutor responses\n"
21
+ ]
22
+ },
23
+ {
24
+ "cell_type": "markdown",
25
+ "id": "16f93992-88dd-409b-8370-b86302e1ce6a",
26
+ "metadata": {},
27
+ "source": [
28
+ "## Setup"
29
+ ]
30
+ },
31
+ {
32
+ "cell_type": "code",
33
+ "execution_count": null,
34
+ "id": "6d2403bb-70a1-4744-be0b-d259234c1b62",
35
+ "metadata": {},
36
+ "outputs": [],
37
+ "source": [
38
+ "#| hide\n",
39
+ "from nbdev.showdoc import *"
40
+ ]
41
+ },
42
+ {
43
+ "cell_type": "code",
44
+ "execution_count": null,
45
+ "id": "477ba22b-55c1-467e-8206-a92f88a598fd",
46
+ "metadata": {},
47
+ "outputs": [
48
+ {
49
+ "name": "stderr",
50
+ "output_type": "stream",
51
+ "text": [
52
+ "C:\\Users\\deepa\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\tqdm\\auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n",
53
+ " from .autonotebook import tqdm as notebook_tqdm\n"
54
+ ]
55
+ }
56
+ ],
57
+ "source": [
58
+ "#| export\n",
59
+ "from typing import Dict, List, Optional, Any, Tuple\n",
60
+ "import os\n",
61
+ "import json\n",
62
+ "import logging\n",
63
+ "from pathlib import Path\n",
64
+ "from datetime import datetime\n",
65
+ "from dotenv import load_dotenv\n",
66
+ "import aiohttp\n",
67
+ "import re\n",
68
+ "from collections import defaultdict\n",
69
+ "from wardbuddy.learning_context import LearningContext, setup_logger\n",
70
+ "\n",
71
+ "# Load environment variables\n",
72
+ "load_dotenv()\n",
73
+ "\n",
74
+ "logger = setup_logger(__name__)"
75
+ ]
76
+ },
77
+ {
78
+ "cell_type": "markdown",
79
+ "id": "7da445d7-7f51-44d5-b027-e2cf65c79069",
80
+ "metadata": {},
81
+ "source": [
82
+ "## Adaptive Clinical Tutor"
83
+ ]
84
+ },
85
+ {
86
+ "cell_type": "markdown",
87
+ "id": "58d76615-480c-4fe4-9d4a-e67dd892132a",
88
+ "metadata": {},
89
+ "source": [
90
+ "This module implements:\n",
91
+ "\n",
92
+ " - Engages in natural case discussions like a clinical supervisor\n",
93
+ " - Provides context-aware feedback based on student's rotation and preferences\n",
94
+ " - Analyzes discussions to track learning progress\n",
95
+ " - Integrates with the student's learning context\n",
96
+ "\n",
97
+ "The tutor aims to mimic real-world clinical teaching interactions where students present cases and receive feedback in a natural conversational style.\n"
98
+ ]
99
+ },
100
+ {
101
+ "cell_type": "code",
102
+ "execution_count": null,
103
+ "id": "da7d2115-b30f-40ba-9566-bcbaf155026d",
104
+ "metadata": {},
105
+ "outputs": [],
106
+ "source": [
107
+ "#| export \n",
108
+ "class OpenRouterException(Exception):\n",
109
+ " \"\"\"Custom exception for OpenRouter API errors\"\"\"\n",
110
+ " pass"
111
+ ]
112
+ },
113
+ {
114
+ "cell_type": "code",
115
+ "execution_count": null,
116
+ "id": "75c2cbfc-75e7-45ee-9d86-b931f81a3ad5",
117
+ "metadata": {},
118
+ "outputs": [],
119
+ "source": [
120
+ "#| export\n",
121
+ "class ClinicalTutor:\n",
122
+ " \"\"\"\n",
123
+ " Adaptive clinical teaching module that provides context-aware feedback.\n",
124
+ " \n",
125
+ " The tutor acts as an experienced clinical supervisor, engaging in natural\n",
126
+ " case discussions while tracking student progress and adapting feedback\n",
127
+ " based on learning context.\n",
128
+ " \n",
129
+ " Attributes:\n",
130
+ " learning_context (LearningContext): Student's learning context\n",
131
+ " model (str): LLM model identifier\n",
132
+ " api_url (str): OpenRouter API endpoint\n",
133
+ " \"\"\"\n",
134
+ " \n",
135
+ " def __init__(\n",
136
+ " self,\n",
137
+ " context_path: Optional[Path] = None,\n",
138
+ " model: str = \"anthropic/claude-3.5-sonnet\"\n",
139
+ " ):\n",
140
+ " \"\"\"\n",
141
+ " Initialize clinical tutor.\n",
142
+ " \n",
143
+ " Args:\n",
144
+ " context_path: Optional path for context persistence\n",
145
+ " model: Model identifier for OpenRouter\n",
146
+ " \"\"\"\n",
147
+ " self.api_key: str = os.getenv(\"OPENROUTER_API_KEY\")\n",
148
+ " if not self.api_key:\n",
149
+ " raise ValueError(\"OpenRouter API key not found\")\n",
150
+ " \n",
151
+ " self.api_url: str = \"https://openrouter.ai/api/v1/chat/completions\"\n",
152
+ " self.model: str = model\n",
153
+ " \n",
154
+ " self.learning_context = LearningContext(context_path)\n",
155
+ " self.context_path = context_path\n",
156
+ " \n",
157
+ " # Track conversation state\n",
158
+ " self.current_case: Dict = {\n",
159
+ " \"started\": None,\n",
160
+ " \"chief_complaint\": None,\n",
161
+ " \"key_findings\": [],\n",
162
+ " \"assessment\": None,\n",
163
+ " \"plan\": None\n",
164
+ " }\n",
165
+ " \n",
166
+ " logger.info(f\"Clinical tutor initialized with model: {model}\")\n",
167
+ " \n",
168
+ " async def _get_completion(\n",
169
+ " self,\n",
170
+ " messages: List[Dict],\n",
171
+ " temperature: float = 0.7,\n",
172
+ " max_retries: int = 3\n",
173
+ " ) -> str:\n",
174
+ " \"\"\"\n",
175
+ " Get completion from OpenRouter API with retry logic.\n",
176
+ " \n",
177
+ " Args:\n",
178
+ " messages: List of conversation messages\n",
179
+ " temperature: Temperature for response generation\n",
180
+ " max_retries: Maximum retry attempts\n",
181
+ " \n",
182
+ " Returns:\n",
183
+ " str: Model response\n",
184
+ " \n",
185
+ " Raises:\n",
186
+ " OpenRouterException: If API calls fail after retries\n",
187
+ " \"\"\"\n",
188
+ " headers = {\n",
189
+ " \"Authorization\": f\"Bearer {self.api_key}\",\n",
190
+ " \"Content-Type\": \"application/json\",\n",
191
+ " \"HTTP-Referer\": \"http://localhost:7860\"\n",
192
+ " }\n",
193
+ " \n",
194
+ " data = {\n",
195
+ " \"model\": self.model,\n",
196
+ " \"messages\": messages,\n",
197
+ " \"temperature\": temperature,\n",
198
+ " \"max_tokens\": 2000\n",
199
+ " }\n",
200
+ " \n",
201
+ " for attempt in range(max_retries):\n",
202
+ " try:\n",
203
+ " async with aiohttp.ClientSession() as session:\n",
204
+ " async with session.post(\n",
205
+ " self.api_url,\n",
206
+ " headers=headers,\n",
207
+ " json=data,\n",
208
+ " timeout=30\n",
209
+ " ) as response:\n",
210
+ " response.raise_for_status()\n",
211
+ " result = await response.json()\n",
212
+ " return result[\"choices\"][0][\"message\"][\"content\"]\n",
213
+ " \n",
214
+ " except Exception as e:\n",
215
+ " if attempt == max_retries - 1:\n",
216
+ " raise OpenRouterException(f\"API call failed: {str(e)}\")\n",
217
+ " logger.warning(f\"Retry {attempt + 1} after error: {str(e)}\")\n",
218
+ " # Could add exponential backoff here if needed\n",
219
+ " \n",
220
+ " def _build_discussion_prompt(self) -> str:\n",
221
+ " \"\"\"Build context-aware prompt for case discussion.\"\"\"\n",
222
+ " rotation = self.learning_context.current_rotation\n",
223
+ " active_preferences = [\n",
224
+ " p[\"focus\"] for p in self.learning_context.feedback_preferences \n",
225
+ " if p[\"active\"]\n",
226
+ " ]\n",
227
+ " \n",
228
+ " significant_gaps = {\n",
229
+ " topic: score for topic, score \n",
230
+ " in self.learning_context.knowledge_profile[\"gaps\"].items()\n",
231
+ " if score < 0.7 # Only include significant gaps\n",
232
+ " }\n",
233
+ " \n",
234
+ " prompt = f\"\"\"You are an experienced clinical supervisor in {rotation['specialty']}. Act as an engaging and conversational tutor who coaches towards deeper understanding through Socratic dialogue and targeted questions.\n",
235
+ "\n",
236
+ " Key Principles:\n",
237
+ " 1. Assume I have strong foundational knowledge in medicine, clinical reasoning, and pre-medical sciences\n",
238
+ " 2. Focus on high-level connections and nuanced clinical decision-making\n",
239
+ " 3. Use targeted questions to explore my thought process and highlight key learning points\n",
240
+ " 4. Share relevant clinical pearls and real-world applications\n",
241
+ " 5. Be conversational and engaging, avoiding lecture-style responses\n",
242
+ " \n",
243
+ " Current Rotation Focus Areas:\n",
244
+ " {', '.join(rotation['key_focus_areas'])}\n",
245
+ "\n",
246
+ " Areas for Deep Dive:\n",
247
+ " {', '.join(f'{topic} (confidence: {score:.1f})' for topic, score in significant_gaps.items()) if significant_gaps else 'General clinical reasoning'}\n",
248
+ "\n",
249
+ " Student's Interests:\n",
250
+ " {', '.join(active_preferences) if active_preferences else 'Broad clinical discussion'}\n",
251
+ "\n",
252
+ " Ask probing questions that explore clinical reasoning and highlight important connections. I will ask for clarification \n",
253
+ " if concepts need more explanation.\"\"\"\n",
254
+ "\n",
255
+ " return prompt\n",
256
+ " \n",
257
+ " def _build_analysis_prompt(self, conversation: List[Dict[str, str]]) -> str:\n",
258
+ " \"\"\"\n",
259
+ " Build prompt for post-discussion analysis.\n",
260
+ " \n",
261
+ " Args:\n",
262
+ " conversation: List of message dictionaries with roles and content\n",
263
+ " \n",
264
+ " Returns:\n",
265
+ " str: Analysis prompt\n",
266
+ " \"\"\"\n",
267
+ " # Extract case details\n",
268
+ " case_content = \"\"\n",
269
+ " for msg in conversation:\n",
270
+ " if msg[\"role\"] == \"user\":\n",
271
+ " case_content += msg[\"content\"] + \"\\n\"\n",
272
+ " \n",
273
+ " return f\"\"\"Analyze the following case discussion between a medical student and \n",
274
+ " clinical supervisor. Focus on the student's demonstrated knowledge, skills, \n",
275
+ " and areas for improvement.\n",
276
+ "\n",
277
+ " Case Content:\n",
278
+ " {case_content}\n",
279
+ "\n",
280
+ " Please identify:\n",
281
+ " 1. Key clinical concepts and learning points demonstrated or discussed\n",
282
+ " 2. Areas where the student showed uncertainty or knowledge gaps\n",
283
+ " 3. Strengths demonstrated in clinical reasoning and presentation\n",
284
+ " 4. Specific learning objectives that would help the student's development\n",
285
+ "\n",
286
+ " Frame your response to help with ongoing learning:\n",
287
+ " - Start with positive observations\n",
288
+ " - Be specific about knowledge gaps\n",
289
+ " - Make concrete suggestions for improvement\n",
290
+ " - Connect to practical clinical scenarios\"\"\"\n",
291
+ " \n",
292
+ " async def discuss_case(\n",
293
+ " self, \n",
294
+ " message: str,\n",
295
+ " temperature: float = 0.7\n",
296
+ " ) -> str:\n",
297
+ " \"\"\"\n",
298
+ " Natural case discussion with context-aware responses.\n",
299
+ " \n",
300
+ " Args:\n",
301
+ " message: Student's input message\n",
302
+ " temperature: Temperature for response generation\n",
303
+ " \n",
304
+ " Returns:\n",
305
+ " str: Clinical supervisor's response\n",
306
+ " \"\"\"\n",
307
+ " try:\n",
308
+ " # Update case tracking\n",
309
+ " if not self.current_case[\"started\"]:\n",
310
+ " self.current_case[\"started\"] = datetime.now()\n",
311
+ " # Try to identify chief complaint from first message\n",
312
+ " cc_match = re.search(r\"(\\d+)\\s*[yY][oO]\\s*[MmFf]\\s*with\\s*([^.]*)\", message)\n",
313
+ " if cc_match:\n",
314
+ " self.current_case[\"chief_complaint\"] = cc_match.group(2).strip()\n",
315
+ " \n",
316
+ " # Build system prompt\n",
317
+ " system_prompt = self._build_discussion_prompt()\n",
318
+ " \n",
319
+ " messages = [{\n",
320
+ " \"role\": \"system\",\n",
321
+ " \"content\": system_prompt\n",
322
+ " }, {\n",
323
+ " \"role\": \"user\",\n",
324
+ " \"content\": message\n",
325
+ " }]\n",
326
+ " \n",
327
+ " response = await self._get_completion(messages, temperature)\n",
328
+ " return response\n",
329
+ " \n",
330
+ " except Exception as e:\n",
331
+ " logger.error(f\"Error in case discussion: {str(e)}\")\n",
332
+ " return \"I apologize, but I encountered an error. Please try presenting your case again.\"\n",
333
+ " \n",
334
+ " async def analyze_discussion(\n",
335
+ " self,\n",
336
+ " conversation: List[Dict[str, str]]\n",
337
+ " ) -> Dict[str, Any]:\n",
338
+ " \"\"\"\n",
339
+ " Analyze completed case discussion for learning insights.\n",
340
+ " \n",
341
+ " Args:\n",
342
+ " conversation: List of message dictionaries with roles and content\n",
343
+ " \n",
344
+ " Returns:\n",
345
+ " dict: Analysis results containing:\n",
346
+ " - learning_points: List of key concepts learned\n",
347
+ " - gaps: Dict of identified knowledge gaps\n",
348
+ " - strengths: List of demonstrated strengths\n",
349
+ " - suggested_objectives: List of recommended learning goals\n",
350
+ " \"\"\"\n",
351
+ " try:\n",
352
+ " # Reset case tracking\n",
353
+ " self.current_case = {\n",
354
+ " \"started\": None,\n",
355
+ " \"chief_complaint\": None,\n",
356
+ " \"key_findings\": [],\n",
357
+ " \"assessment\": None,\n",
358
+ " \"plan\": None\n",
359
+ " }\n",
360
+ " \n",
361
+ " # Get analysis from model\n",
362
+ " analysis_prompt = self._build_analysis_prompt(conversation)\n",
363
+ " messages = [{\n",
364
+ " \"role\": \"system\",\n",
365
+ " \"content\": analysis_prompt\n",
366
+ " }]\n",
367
+ " messages.extend(conversation)\n",
368
+ " \n",
369
+ " response = await self._get_completion(messages, temperature=0.3)\n",
370
+ " \n",
371
+ " # Parse insights\n",
372
+ " insights = self._parse_analysis(response)\n",
373
+ " \n",
374
+ " # Update learning context\n",
375
+ " self._update_context_from_analysis(insights)\n",
376
+ " \n",
377
+ " return insights\n",
378
+ " \n",
379
+ " except Exception as e:\n",
380
+ " logger.error(f\"Error in discussion analysis: {str(e)}\")\n",
381
+ " return {\n",
382
+ " \"learning_points\": [],\n",
383
+ " \"gaps\": {},\n",
384
+ " \"strengths\": [],\n",
385
+ " \"suggested_objectives\": []\n",
386
+ " }\n",
387
+ " \n",
388
+ " def _parse_analysis(self, response: str) -> Dict[str, Any]:\n",
389
+ " \"\"\"\n",
390
+ " Parse analysis response into structured insights.\n",
391
+ " \n",
392
+ " Uses pattern matching and basic NLP to extract:\n",
393
+ " - Learning points (key concepts discussed)\n",
394
+ " - Knowledge gaps with confidence estimates\n",
395
+ " - Demonstrated strengths\n",
396
+ " - Suggested learning objectives\n",
397
+ " \n",
398
+ " Args:\n",
399
+ " response: Raw analysis response\n",
400
+ " \n",
401
+ " Returns:\n",
402
+ " dict: Structured analysis insights\n",
403
+ " \"\"\"\n",
404
+ " insights = {\n",
405
+ " \"learning_points\": [],\n",
406
+ " \"gaps\": {},\n",
407
+ " \"strengths\": [],\n",
408
+ " \"suggested_objectives\": []\n",
409
+ " }\n",
410
+ " \n",
411
+ " try:\n",
412
+ " # Split into sections\n",
413
+ " sections = response.lower().split(\"\\n\\n\")\n",
414
+ " \n",
415
+ " for section in sections:\n",
416
+ " if \"learning point\" in section or \"key concept\" in section:\n",
417
+ " # Extract bullet points or numbered items\n",
418
+ " points = re.findall(r\"[-•*]\\s*(.+)$\", section, re.MULTILINE)\n",
419
+ " insights[\"learning_points\"].extend(points)\n",
420
+ " \n",
421
+ " elif \"gap\" in section or \"uncertainty\" in section:\n",
422
+ " # Look for topic mentions with confidence indicators\n",
423
+ " gaps = re.findall(\n",
424
+ " r\"(limited|uncertain|unclear|difficulty with)\\s+([^,.]+)\", \n",
425
+ " section\n",
426
+ " )\n",
427
+ " for indicator, topic in gaps:\n",
428
+ " # Estimate confidence based on language\n",
429
+ " confidence = 0.4 if \"limited\" in indicator else 0.6\n",
430
+ " insights[\"gaps\"][topic.strip()] = confidence\n",
431
+ " \n",
432
+ " elif \"strength\" in section or \"demonstrated\" in section:\n",
433
+ " # Extract positive mentions\n",
434
+ " strengths = re.findall(r\"[-•*]\\s*(.+)$\", section, re.MULTILINE)\n",
435
+ " insights[\"strengths\"].extend(strengths)\n",
436
+ " \n",
437
+ " elif \"objective\" in section or \"suggest\" in section:\n",
438
+ " # Extract recommended objectives\n",
439
+ " objectives = re.findall(r\"[-•*]\\s*(.+)$\", section, re.MULTILINE)\n",
440
+ " insights[\"suggested_objectives\"].extend(objectives)\n",
441
+ " \n",
442
+ " return insights\n",
443
+ " \n",
444
+ " except Exception as e:\n",
445
+ " logger.error(f\"Error parsing analysis: {str(e)}\")\n",
446
+ " return insights\n",
447
+ " \n",
448
+ " def _update_context_from_analysis(self, insights: Dict[str, Any]) -> None:\n",
449
+ " \"\"\"\n",
450
+ " Update learning context based on discussion analysis.\n",
451
+ " \n",
452
+ " Args:\n",
453
+ " insights: Dictionary of analysis insights\n",
454
+ " \"\"\"\n",
455
+ " try:\n",
456
+ " # Update knowledge gaps\n",
457
+ " for topic, confidence in insights[\"gaps\"].items():\n",
458
+ " self.learning_context.update_knowledge_gap(topic, confidence)\n",
459
+ " \n",
460
+ " # Add strengths\n",
461
+ " for strength in insights[\"strengths\"]:\n",
462
+ " self.learning_context.add_strength(strength)\n",
463
+ " \n",
464
+ " # Save context if path provided\n",
465
+ " if self.context_path:\n",
466
+ " self.learning_context.save_context(self.context_path)\n",
467
+ " \n",
468
+ " except Exception as e:\n",
469
+ " logger.error(f\"Error updating context: {str(e)}\")"
470
+ ]
471
+ },
472
+ {
473
+ "cell_type": "markdown",
474
+ "id": "6a2b15f5-6841-43cb-9b57-c0e3f1a0b0c2",
475
+ "metadata": {},
476
+ "source": [
477
+ "## Tests"
478
+ ]
479
+ },
480
+ {
481
+ "cell_type": "code",
482
+ "execution_count": null,
483
+ "id": "67ee6bde-4ade-448e-a831-86f9f7ae82ea",
484
+ "metadata": {},
485
+ "outputs": [
486
+ {
487
+ "ename": "RuntimeError",
488
+ "evalue": "asyncio.run() cannot be called from a running event loop",
489
+ "output_type": "error",
490
+ "traceback": [
491
+ "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
492
+ "\u001b[1;31mRuntimeError\u001b[0m Traceback (most recent call last)",
493
+ "Cell \u001b[1;32mIn[7], line 55\u001b[0m\n\u001b[0;32m 53\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;18m__name__\u001b[39m \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m__main__\u001b[39m\u001b[38;5;124m\"\u001b[39m:\n\u001b[0;32m 54\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01masyncio\u001b[39;00m\n\u001b[1;32m---> 55\u001b[0m \u001b[43masyncio\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mrun\u001b[49m\u001b[43m(\u001b[49m\u001b[43mtest_clinical_tutor\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[43m)\u001b[49m\n",
494
+ "File \u001b[1;32m~\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\asyncio\\runners.py:190\u001b[0m, in \u001b[0;36mrun\u001b[1;34m(main, debug, loop_factory)\u001b[0m\n\u001b[0;32m 161\u001b[0m \u001b[38;5;250m\u001b[39m\u001b[38;5;124;03m\"\"\"Execute the coroutine and return the result.\u001b[39;00m\n\u001b[0;32m 162\u001b[0m \n\u001b[0;32m 163\u001b[0m \u001b[38;5;124;03mThis function runs the passed coroutine, taking care of\u001b[39;00m\n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 186\u001b[0m \u001b[38;5;124;03m asyncio.run(main())\u001b[39;00m\n\u001b[0;32m 187\u001b[0m \u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[0;32m 188\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m events\u001b[38;5;241m.\u001b[39m_get_running_loop() \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[0;32m 189\u001b[0m \u001b[38;5;66;03m# fail fast with short traceback\u001b[39;00m\n\u001b[1;32m--> 190\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mRuntimeError\u001b[39;00m(\n\u001b[0;32m 191\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124masyncio.run() cannot be called from a running event loop\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m 193\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m Runner(debug\u001b[38;5;241m=\u001b[39mdebug, loop_factory\u001b[38;5;241m=\u001b[39mloop_factory) \u001b[38;5;28;01mas\u001b[39;00m runner:\n\u001b[0;32m 194\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m runner\u001b[38;5;241m.\u001b[39mrun(main)\n",
495
+ "\u001b[1;31mRuntimeError\u001b[0m: asyncio.run() cannot be called from a running event loop"
496
+ ]
497
+ }
498
+ ],
499
+ "source": [
500
+ "async def test_clinical_tutor():\n",
501
+ " \"\"\"Test ClinicalTutor functionality\"\"\"\n",
502
+ " if not os.getenv(\"OPENROUTER_API_KEY\"):\n",
503
+ " print(\"Skipping tests: No API key\")\n",
504
+ " return\n",
505
+ " \n",
506
+ " tutor = ClinicalTutor()\n",
507
+ " \n",
508
+ " # Test case discussion\n",
509
+ " test_case = \"\"\"\n",
510
+ " 28yo M with chest pain\n",
511
+ " - 2 days duration\n",
512
+ " - Sharp, pleuritic\n",
513
+ " - No fever or cough\n",
514
+ " - Vitals stable\n",
515
+ " - Clear exam\n",
516
+ " A/P: Likely MSK pain\n",
517
+ " \"\"\"\n",
518
+ " \n",
519
+ " try:\n",
520
+ " # Test basic discussion\n",
521
+ " response = await tutor.discuss_case(test_case)\n",
522
+ " assert isinstance(response, str)\n",
523
+ " assert len(response) > 0\n",
524
+ " \n",
525
+ " # Only assert case tracking if chief complaint was detected\n",
526
+ " if tutor.current_case[\"chief_complaint\"]:\n",
527
+ " assert \"chest pain\" in tutor.current_case[\"chief_complaint\"].lower()\n",
528
+ " \n",
529
+ " print(\"Discussion test passed\")\n",
530
+ " \n",
531
+ " # Test discussion analysis\n",
532
+ " conversation = [\n",
533
+ " {\"role\": \"user\", \"content\": test_case},\n",
534
+ " {\"role\": \"assistant\", \"content\": response}\n",
535
+ " ]\n",
536
+ " \n",
537
+ " analysis = await tutor.analyze_discussion(conversation)\n",
538
+ " assert isinstance(analysis, dict)\n",
539
+ " assert all(k in analysis for k in [\n",
540
+ " 'learning_points', 'gaps', 'strengths', 'suggested_objectives'\n",
541
+ " ])\n",
542
+ " print(\"Analysis test passed\")\n",
543
+ " \n",
544
+ " except Exception as e:\n",
545
+ " print(f\"Test failed: {str(e)}\")\n",
546
+ " raise\n",
547
+ " \n",
548
+ " print(\"All clinical tutor tests passed!\")\n",
549
+ "\n",
550
+ "# Run tests\n",
551
+ "if __name__ == \"__main__\":\n",
552
+ " import asyncio\n",
553
+ " if not asyncio.get_event_loop().is_running():\n",
554
+ " asyncio.run(test_clinical_tutor())"
555
+ ]
556
+ },
557
+ {
558
+ "cell_type": "code",
559
+ "execution_count": null,
560
+ "id": "3f469c37-afe3-4682-9cc4-40326ac21b74",
561
+ "metadata": {},
562
+ "outputs": [],
563
+ "source": []
564
+ }
565
+ ],
566
+ "metadata": {
567
+ "kernelspec": {
568
+ "display_name": "python3",
569
+ "language": "python",
570
+ "name": "python3"
571
+ }
572
+ },
573
+ "nbformat": 4,
574
+ "nbformat_minor": 5
575
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
nbs/02_learning_interface.ipynb CHANGED
@@ -90,49 +90,111 @@
90
  "def create_dashboard_css() -> str:\n",
91
  " \"\"\"Create custom CSS for dashboard styling\"\"\"\n",
92
  " return \"\"\"\n",
 
 
 
 
 
 
93
  " .dashboard-card {\n",
94
- " border: 1px solid #e2e8f0;\n",
95
- " border-radius: 8px;\n",
96
- " padding: 16px;\n",
97
- " margin: 8px 0;\n",
98
- " background: white;\n",
 
99
  " }\n",
100
  " \n",
101
- " .status-active {\n",
102
- " color: #48bb78;\n",
103
- " font-weight: 500;\n",
 
104
  " }\n",
105
  " \n",
106
- " .status-completed {\n",
107
- " color: #718096;\n",
 
 
 
108
  " }\n",
109
  " \n",
110
- " .dashboard-header {\n",
111
- " font-size: 1.25rem;\n",
112
- " font-weight: 600;\n",
113
- " margin-bottom: 1rem;\n",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  " }\n",
115
  " \n",
116
- " .summary-modal {\n",
117
- " max-width: 800px !important;\n",
118
  " }\n",
119
  " \n",
120
- " .feedback-tag {\n",
121
- " display: inline-block;\n",
122
- " padding: 4px 8px;\n",
123
- " border-radius: 4px;\n",
124
- " margin: 4px;\n",
125
- " font-size: 0.875rem;\n",
 
 
 
 
 
 
 
 
 
 
126
  " }\n",
127
  " \n",
128
- " .feedback-active {\n",
129
- " background: #ebf8ff;\n",
130
- " color: #2b6cb0;\n",
 
131
  " }\n",
132
  " \n",
133
- " .feedback-inactive {\n",
134
- " background: #f7fafc;\n",
135
- " color: #718096;\n",
136
  " }\n",
137
  " \"\"\""
138
  ]
 
90
  "def create_dashboard_css() -> str:\n",
91
  " \"\"\"Create custom CSS for dashboard styling\"\"\"\n",
92
  " return \"\"\"\n",
93
+ " /* Global styles */\n",
94
+ " .gradio-container {\n",
95
+ " background-color: #0f172a !important; /* slate-900 */\n",
96
+ " }\n",
97
+ " \n",
98
+ " /* Card styling */\n",
99
  " .dashboard-card {\n",
100
+ " background-color: #1e293b !important; /* slate-800 */\n",
101
+ " border: 1px solid #334155 !important; /* slate-700 */\n",
102
+ " border-radius: 0.5rem !important;\n",
103
+ " padding: 1rem !important;\n",
104
+ " margin: 0.5rem 0 !important;\n",
105
+ " color: #f1f5f9 !important; /* slate-100 */\n",
106
  " }\n",
107
  " \n",
108
+ " /* Chat container */\n",
109
+ " .chatbot {\n",
110
+ " background-color: #1e293b !important; /* slate-800 */\n",
111
+ " border-color: #334155 !important; /* slate-700 */\n",
112
  " }\n",
113
  " \n",
114
+ " /* Message bubbles */\n",
115
+ " .chatbot .message.user {\n",
116
+ " background-color: #334155 !important; /* slate-700 */\n",
117
+ " border: 1px solid #475569 !important; /* slate-600 */\n",
118
+ " color: #f1f5f9 !important; /* slate-100 */\n",
119
  " }\n",
120
  " \n",
121
+ " .chatbot .message.bot {\n",
122
+ " background-color: #1e40af !important; /* blue-800 */\n",
123
+ " border: 1px solid #1e3a8a !important; /* blue-900 */\n",
124
+ " color: #f1f5f9 !important; /* slate-100 */\n",
125
+ " }\n",
126
+ " \n",
127
+ " /* Input fields */\n",
128
+ " textarea, input[type=\"text\"] {\n",
129
+ " background-color: #334155 !important; /* slate-700 */\n",
130
+ " color: #f1f5f9 !important; /* slate-100 */\n",
131
+ " border: 1px solid #475569 !important; /* slate-600 */\n",
132
+ " }\n",
133
+ " \n",
134
+ " textarea:focus, input[type=\"text\"]:focus {\n",
135
+ " border-color: #3b82f6 !important; /* blue-500 */\n",
136
+ " box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2) !important;\n",
137
+ " }\n",
138
+ " \n",
139
+ " /* Buttons */\n",
140
+ " button.primary {\n",
141
+ " background-color: #2563eb !important; /* blue-600 */\n",
142
+ " color: white !important;\n",
143
+ " }\n",
144
+ " \n",
145
+ " button.primary:hover {\n",
146
+ " background-color: #3b82f6 !important; /* blue-500 */\n",
147
+ " }\n",
148
+ " \n",
149
+ " button.secondary {\n",
150
+ " background-color: #475569 !important; /* slate-600 */\n",
151
+ " color: white !important;\n",
152
+ " }\n",
153
+ " \n",
154
+ " button.secondary:hover {\n",
155
+ " background-color: #64748b !important; /* slate-500 */\n",
156
+ " }\n",
157
+ " \n",
158
+ " /* Tabs */\n",
159
+ " .tab-nav {\n",
160
+ " background-color: #1e293b !important; /* slate-800 */\n",
161
+ " border-bottom: 1px solid #334155 !important; /* slate-700 */\n",
162
+ " }\n",
163
+ " \n",
164
+ " .tab-nav button {\n",
165
+ " color: #f1f5f9 !important; /* slate-100 */\n",
166
  " }\n",
167
  " \n",
168
+ " .tab-nav button.selected {\n",
169
+ " border-bottom-color: #3b82f6 !important; /* blue-500 */\n",
170
  " }\n",
171
  " \n",
172
+ " /* Status indicators */\n",
173
+ " .status-active {\n",
174
+ " color: #22c55e !important; /* green-500 */\n",
175
+ " font-weight: 500 !important;\n",
176
+ " }\n",
177
+ " \n",
178
+ " .status-completed {\n",
179
+ " color: #94a3b8 !important; /* slate-400 */\n",
180
+ " }\n",
181
+ " \n",
182
+ " /* Headers */\n",
183
+ " .dashboard-header {\n",
184
+ " color: #f1f5f9 !important; /* slate-100 */\n",
185
+ " font-size: 1.5rem !important;\n",
186
+ " font-weight: 600 !important;\n",
187
+ " margin-bottom: 1rem !important;\n",
188
  " }\n",
189
  " \n",
190
+ " /* Tables */\n",
191
+ " table {\n",
192
+ " background-color: #1e293b !important; /* slate-800 */\n",
193
+ " color: #f1f5f9 !important; /* slate-100 */\n",
194
  " }\n",
195
  " \n",
196
+ " th, td {\n",
197
+ " border-color: #334155 !important; /* slate-700 */\n",
 
198
  " }\n",
199
  " \"\"\""
200
  ]
wardbuddy/clinical_tutor.py CHANGED
@@ -129,52 +129,40 @@ class ClinicalTutor:
129
  # Could add exponential backoff here if needed
130
 
131
  def _build_discussion_prompt(self) -> str:
132
- """
133
- Build context-aware prompt for case discussion.
134
-
135
- Incorporates:
136
- - Current rotation details
137
- - Active feedback preferences
138
- - Recent learning points
139
- - Knowledge gaps needing attention
140
-
141
- Returns:
142
- str: Contextualized system prompt
143
- """
144
  rotation = self.learning_context.current_rotation
145
  active_preferences = [
146
  p["focus"] for p in self.learning_context.feedback_preferences
147
  if p["active"]
148
  ]
149
 
150
- # Get relevant knowledge gaps
151
  significant_gaps = {
152
  topic: score for topic, score
153
  in self.learning_context.knowledge_profile["gaps"].items()
154
  if score < 0.7 # Only include significant gaps
155
  }
156
 
157
- prompt = f"""You are an experienced clinical supervisor in {rotation['specialty']}
158
- providing teaching and feedback. You aim to:
159
-
160
- 1. Help students develop strong clinical reasoning
161
- 2. Connect theory to practical applications
162
- 3. Build diagnostic confidence
163
- 4. Improve presentation skills
164
 
 
 
 
 
 
 
 
165
  Current Rotation Focus Areas:
166
  {', '.join(rotation['key_focus_areas'])}
167
 
168
- Areas Needing Attention:
169
- {', '.join(f'{topic} (confidence: {score:.1f})' for topic, score in significant_gaps.items()) if significant_gaps else 'No specific gaps identified'}
170
 
171
- Student's Requested Focus:
172
- {', '.join(active_preferences) if active_preferences else 'General clinical feedback'}
 
 
 
173
 
174
- Engage naturally as a supportive but challenging supervisor would during case
175
- presentations. Ask probing questions when appropriate, share relevant clinical
176
- pearls, and help the student build their clinical reasoning skills."""
177
-
178
  return prompt
179
 
180
  def _build_analysis_prompt(self, conversation: List[Dict[str, str]]) -> str:
 
129
  # Could add exponential backoff here if needed
130
 
131
  def _build_discussion_prompt(self) -> str:
132
+ """Build context-aware prompt for case discussion."""
 
 
 
 
 
 
 
 
 
 
 
133
  rotation = self.learning_context.current_rotation
134
  active_preferences = [
135
  p["focus"] for p in self.learning_context.feedback_preferences
136
  if p["active"]
137
  ]
138
 
 
139
  significant_gaps = {
140
  topic: score for topic, score
141
  in self.learning_context.knowledge_profile["gaps"].items()
142
  if score < 0.7 # Only include significant gaps
143
  }
144
 
145
+ prompt = f"""You are an experienced clinical supervisor in {rotation['specialty']}. Act as an engaging and conversational tutor who coaches towards deeper understanding through Socratic dialogue and targeted questions.
 
 
 
 
 
 
146
 
147
+ Key Principles:
148
+ 1. Assume I have strong foundational knowledge in medicine, clinical reasoning, and pre-medical sciences
149
+ 2. Focus on high-level connections and nuanced clinical decision-making
150
+ 3. Use targeted questions to explore my thought process and highlight key learning points
151
+ 4. Share relevant clinical pearls and real-world applications
152
+ 5. Be conversational and engaging, avoiding lecture-style responses
153
+
154
  Current Rotation Focus Areas:
155
  {', '.join(rotation['key_focus_areas'])}
156
 
157
+ Areas for Deep Dive:
158
+ {', '.join(f'{topic} (confidence: {score:.1f})' for topic, score in significant_gaps.items()) if significant_gaps else 'General clinical reasoning'}
159
 
160
+ Student's Interests:
161
+ {', '.join(active_preferences) if active_preferences else 'Broad clinical discussion'}
162
+
163
+ Ask probing questions that explore clinical reasoning and highlight important connections. I will ask for clarification
164
+ if concepts need more explanation."""
165
 
 
 
 
 
166
  return prompt
167
 
168
  def _build_analysis_prompt(self, conversation: List[Dict[str, str]]) -> str:
wardbuddy/learning_interface.py CHANGED
@@ -21,49 +21,111 @@ logger = setup_logger(__name__)
21
  def create_dashboard_css() -> str:
22
  """Create custom CSS for dashboard styling"""
23
  return """
 
 
 
 
 
 
24
  .dashboard-card {
25
- border: 1px solid #e2e8f0;
26
- border-radius: 8px;
27
- padding: 16px;
28
- margin: 8px 0;
29
- background: white;
 
30
  }
31
 
32
- .status-active {
33
- color: #48bb78;
34
- font-weight: 500;
 
35
  }
36
 
37
- .status-completed {
38
- color: #718096;
 
 
 
39
  }
40
 
41
- .dashboard-header {
42
- font-size: 1.25rem;
43
- font-weight: 600;
44
- margin-bottom: 1rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  }
46
 
47
- .summary-modal {
48
- max-width: 800px !important;
49
  }
50
 
51
- .feedback-tag {
52
- display: inline-block;
53
- padding: 4px 8px;
54
- border-radius: 4px;
55
- margin: 4px;
56
- font-size: 0.875rem;
 
 
 
 
 
 
 
 
 
 
57
  }
58
 
59
- .feedback-active {
60
- background: #ebf8ff;
61
- color: #2b6cb0;
 
62
  }
63
 
64
- .feedback-inactive {
65
- background: #f7fafc;
66
- color: #718096;
67
  }
68
  """
69
 
 
21
  def create_dashboard_css() -> str:
22
  """Create custom CSS for dashboard styling"""
23
  return """
24
+ /* Global styles */
25
+ .gradio-container {
26
+ background-color: #0f172a !important; /* slate-900 */
27
+ }
28
+
29
+ /* Card styling */
30
  .dashboard-card {
31
+ background-color: #1e293b !important; /* slate-800 */
32
+ border: 1px solid #334155 !important; /* slate-700 */
33
+ border-radius: 0.5rem !important;
34
+ padding: 1rem !important;
35
+ margin: 0.5rem 0 !important;
36
+ color: #f1f5f9 !important; /* slate-100 */
37
  }
38
 
39
+ /* Chat container */
40
+ .chatbot {
41
+ background-color: #1e293b !important; /* slate-800 */
42
+ border-color: #334155 !important; /* slate-700 */
43
  }
44
 
45
+ /* Message bubbles */
46
+ .chatbot .message.user {
47
+ background-color: #334155 !important; /* slate-700 */
48
+ border: 1px solid #475569 !important; /* slate-600 */
49
+ color: #f1f5f9 !important; /* slate-100 */
50
  }
51
 
52
+ .chatbot .message.bot {
53
+ background-color: #1e40af !important; /* blue-800 */
54
+ border: 1px solid #1e3a8a !important; /* blue-900 */
55
+ color: #f1f5f9 !important; /* slate-100 */
56
+ }
57
+
58
+ /* Input fields */
59
+ textarea, input[type="text"] {
60
+ background-color: #334155 !important; /* slate-700 */
61
+ color: #f1f5f9 !important; /* slate-100 */
62
+ border: 1px solid #475569 !important; /* slate-600 */
63
+ }
64
+
65
+ textarea:focus, input[type="text"]:focus {
66
+ border-color: #3b82f6 !important; /* blue-500 */
67
+ box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2) !important;
68
+ }
69
+
70
+ /* Buttons */
71
+ button.primary {
72
+ background-color: #2563eb !important; /* blue-600 */
73
+ color: white !important;
74
+ }
75
+
76
+ button.primary:hover {
77
+ background-color: #3b82f6 !important; /* blue-500 */
78
+ }
79
+
80
+ button.secondary {
81
+ background-color: #475569 !important; /* slate-600 */
82
+ color: white !important;
83
+ }
84
+
85
+ button.secondary:hover {
86
+ background-color: #64748b !important; /* slate-500 */
87
+ }
88
+
89
+ /* Tabs */
90
+ .tab-nav {
91
+ background-color: #1e293b !important; /* slate-800 */
92
+ border-bottom: 1px solid #334155 !important; /* slate-700 */
93
+ }
94
+
95
+ .tab-nav button {
96
+ color: #f1f5f9 !important; /* slate-100 */
97
  }
98
 
99
+ .tab-nav button.selected {
100
+ border-bottom-color: #3b82f6 !important; /* blue-500 */
101
  }
102
 
103
+ /* Status indicators */
104
+ .status-active {
105
+ color: #22c55e !important; /* green-500 */
106
+ font-weight: 500 !important;
107
+ }
108
+
109
+ .status-completed {
110
+ color: #94a3b8 !important; /* slate-400 */
111
+ }
112
+
113
+ /* Headers */
114
+ .dashboard-header {
115
+ color: #f1f5f9 !important; /* slate-100 */
116
+ font-size: 1.5rem !important;
117
+ font-weight: 600 !important;
118
+ margin-bottom: 1rem !important;
119
  }
120
 
121
+ /* Tables */
122
+ table {
123
+ background-color: #1e293b !important; /* slate-800 */
124
+ color: #f1f5f9 !important; /* slate-100 */
125
  }
126
 
127
+ th, td {
128
+ border-color: #334155 !important; /* slate-700 */
 
129
  }
130
  """
131