dyadd commited on
Commit
dde2b11
·
verified ·
1 Parent(s): b53230f

Upload folder using huggingface_hub

Browse files
nbs/01_clinical_tutor.ipynb CHANGED
@@ -59,7 +59,7 @@
59
  ],
60
  "source": [
61
  "#| export\n",
62
- "from typing import Dict, List, Optional, Tuple, Any\n",
63
  "import os\n",
64
  "import json\n",
65
  "import logging\n",
@@ -179,17 +179,29 @@
179
  " list: Generated SMART goals\n",
180
  " \"\"\"\n",
181
  " prompt = f\"\"\"Generate {num_goals} SMART learning goals for a medical trainee in {specialty} ({setting}).\n",
182
- "\n",
183
- "For each goal:\n",
184
- "1. Select an appropriate category from: {', '.join(cat.value for cat in LearningCategory)}\n",
185
- "2. Write a specific, measurable goal that builds clinical competence\n",
186
- "\n",
187
- "Format as JSON array with fields:\n",
188
- "- category: Learning category name\n",
189
- "- smart_version: SMART formatted goal text\n",
190
- "\n",
191
- "Goals should be specific to the {setting} setting in {specialty}.\"\"\"\n",
192
- "\n",
 
 
 
 
 
 
 
 
 
 
 
 
193
  " try:\n",
194
  " response = await self._get_completion([{\n",
195
  " \"role\": \"system\",\n",
@@ -213,12 +225,34 @@
213
  " )\n",
214
  " goals.append(goal)\n",
215
  " \n",
 
216
  " return goals\n",
217
  " \n",
218
  " except Exception as e:\n",
219
  " logger.error(f\"Error generating goals: {str(e)}\")\n",
220
- " return []\n",
221
- " \n",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
  " async def generate_smart_goal(\n",
223
  " self,\n",
224
  " user_input: str,\n",
@@ -270,15 +304,15 @@
270
  " logger.error(f\"Error generating SMART goal: {str(e)}\")\n",
271
  " return None\n",
272
  " \n",
273
- " async def discuss_case(self, message: str) -> str:\n",
274
  " \"\"\"\n",
275
- " Process case discussion message.\n",
276
  " \n",
277
  " Args:\n",
278
  " message: User's message\n",
279
- " \n",
280
- " Returns:\n",
281
- " str: Tutor's response\n",
282
  " \"\"\"\n",
283
  " try:\n",
284
  " # Add to discussion history\n",
@@ -289,27 +323,27 @@
289
  " \n",
290
  " # Build conversation prompt\n",
291
  " system_prompt = self._build_discussion_prompt()\n",
292
- " \n",
293
  " messages = [{\n",
294
  " \"role\": \"system\",\n",
295
  " \"content\": system_prompt\n",
296
  " }]\n",
297
  " messages.extend(self.current_discussion)\n",
298
  " \n",
299
- " # Get response\n",
300
- " response = await self._get_completion(messages)\n",
 
 
 
301
  " \n",
302
- " # Add to history\n",
303
  " self.current_discussion.append({\n",
304
  " \"role\": \"assistant\",\n",
305
- " \"content\": response\n",
306
  " })\n",
307
  " \n",
308
- " return response\n",
309
- " \n",
310
  " except Exception as e:\n",
311
  " logger.error(f\"Error in discussion: {str(e)}\")\n",
312
- " return \"I apologize, but I encountered an error. Please try again.\"\n",
313
  " \n",
314
  " def end_discussion(self) -> None:\n",
315
  " \"\"\"End current discussion.\"\"\"\n",
@@ -341,37 +375,35 @@
341
  " Remember: The learner has strong foundational knowledge. Focus on advanced clinical concepts\n",
342
  " rather than basic science.\"\"\"\n",
343
  "\n",
344
- " async def _get_completion(\n",
345
  " self,\n",
346
  " messages: List[Dict],\n",
347
  " temperature: float = 0.7,\n",
348
  " max_retries: int = 3\n",
349
- " ) -> str:\n",
350
  " \"\"\"\n",
351
- " Get completion from OpenRouter API with retry logic.\n",
352
  " \n",
353
  " Args:\n",
354
  " messages: Conversation messages\n",
355
  " temperature: Response temperature\n",
356
  " max_retries: Maximum retry attempts\n",
357
  " \n",
358
- " Returns:\n",
359
- " str: Model response\n",
360
- " \n",
361
- " Raises:\n",
362
- " OpenRouterException: If API calls fail after retries\n",
363
  " \"\"\"\n",
364
  " headers = {\n",
365
  " \"Authorization\": f\"Bearer {self.api_key}\",\n",
366
  " \"Content-Type\": \"application/json\",\n",
367
- " \"HTTP-Referer\": \"http://localhost:7860\" # Or your actual domain\n",
368
  " }\n",
369
  " \n",
370
  " data = {\n",
371
  " \"model\": self.model,\n",
372
  " \"messages\": messages,\n",
373
  " \"temperature\": temperature,\n",
374
- " \"max_tokens\": 2000\n",
 
375
  " }\n",
376
  " \n",
377
  " for attempt in range(max_retries):\n",
@@ -384,15 +416,31 @@
384
  " timeout=30\n",
385
  " ) as response:\n",
386
  " response.raise_for_status()\n",
387
- " result = await response.json()\n",
388
- " return result[\"choices\"][0][\"message\"][\"content\"]\n",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
389
  " \n",
390
  " except Exception as e:\n",
391
  " if attempt == max_retries - 1:\n",
392
  " raise OpenRouterException(f\"API call failed: {str(e)}\")\n",
393
  " logger.warning(f\"Retry {attempt + 1} after error: {str(e)}\")\n",
394
- " await asyncio.sleep(1 * (attempt + 1)) # Exponential backoff\n",
395
- " \n",
396
  " def get_discussion_history(self) -> List[Dict]:\n",
397
  " \"\"\"\n",
398
  " Get current discussion history.\n",
@@ -794,13 +842,14 @@
794
  }
795
  ],
796
  "source": [
797
- "# | hide\n",
798
  "\n",
799
  "from wardbuddy.clinical_tutor import ClinicalTutor\n",
800
  "from wardbuddy.learning_context import LearningCategory, SmartGoal\n",
801
  "\n",
802
  "async def test_clinical_tutor():\n",
803
  " \"\"\"Test basic clinical tutor functionality.\"\"\"\n",
 
804
  " if not os.getenv(\"OPENROUTER_API_KEY\"):\n",
805
  " print(\"Skipping tests: No API key\")\n",
806
  " return\n",
@@ -812,28 +861,50 @@
812
  " test_case = \"28yo M with chest pain, 2 days duration\"\n",
813
  " \n",
814
  " try:\n",
815
- " # Basic discussion test\n",
816
- " response = await tutor.discuss_case(test_case)\n",
817
- " assert isinstance(response, str)\n",
818
- " assert len(response) > 0\n",
 
 
 
 
819
  " \n",
820
  " # Test goal generation\n",
821
  " goals = await tutor.generate_smart_goals(\"Emergency Medicine\", \"ED\")\n",
822
- " assert len(goals) > 0\n",
 
 
 
 
 
 
 
 
 
 
 
823
  " \n",
824
  " # Clear discussion\n",
825
  " tutor.end_discussion()\n",
826
- " assert len(tutor.get_discussion_history()) == 0\n",
 
827
  " \n",
828
- " print(\"All tests passed!\")\n",
829
  " \n",
830
  " except Exception as e:\n",
831
- " print(f\"Test failed: {str(e)}\")\n",
832
  " raise\n",
833
  "\n",
834
  "# Run tests\n",
835
  "if __name__ == \"__main__\":\n",
836
- " asyncio.run(test_clinical_tutor())"
 
 
 
 
 
 
837
  ]
838
  },
839
  {
 
59
  ],
60
  "source": [
61
  "#| export\n",
62
+ "from typing import Dict, List, Optional, Tuple, Any, AsyncGenerator\n",
63
  "import os\n",
64
  "import json\n",
65
  "import logging\n",
 
179
  " list: Generated SMART goals\n",
180
  " \"\"\"\n",
181
  " prompt = f\"\"\"Generate {num_goals} SMART learning goals for a medical trainee in {specialty} ({setting}).\n",
182
+ " \n",
183
+ " For each goal:\n",
184
+ " 1. Select an appropriate category from: {', '.join(cat.value for cat in LearningCategory)}\n",
185
+ " 2. Write a specific, measurable goal that builds clinical competence\n",
186
+ " \n",
187
+ " Format as JSON array with fields:\n",
188
+ " - category: Learning category name\n",
189
+ " - smart_version: SMART formatted goal text\n",
190
+ " \n",
191
+ " For example:\n",
192
+ " [\n",
193
+ " {{\n",
194
+ " \"category\": \"Clinical Reasoning\",\n",
195
+ " \"smart_version\": \"Accurately diagnose acute coronary syndrome in 5 chest pain cases over 2 weeks with 90% agreement with attending assessment\"\n",
196
+ " }},\n",
197
+ " {{\n",
198
+ " \"category\": \"Management\",\n",
199
+ " \"smart_version\": \"Successfully develop and implement management plans for 3 complex cardiac patients with multiple comorbidities by week 4\"\n",
200
+ " }}\n",
201
+ " ]\n",
202
+ " \n",
203
+ " Goals should be specific to the {setting} setting in {specialty}.\"\"\"\n",
204
+ " \n",
205
  " try:\n",
206
  " response = await self._get_completion([{\n",
207
  " \"role\": \"system\",\n",
 
225
  " )\n",
226
  " goals.append(goal)\n",
227
  " \n",
228
+ " logger.info(f\"Generated {len(goals)} SMART goals\")\n",
229
  " return goals\n",
230
  " \n",
231
  " except Exception as e:\n",
232
  " logger.error(f\"Error generating goals: {str(e)}\")\n",
233
+ " # Instead of returning empty list, generate some default goals\n",
234
+ " default_goals = [\n",
235
+ " SmartGoal(\n",
236
+ " id=f\"goal_{uuid.uuid4()}\",\n",
237
+ " category=LearningCategory.CLINICAL_REASONING,\n",
238
+ " original_input=\"\",\n",
239
+ " smart_version=f\"Develop systematic approach to common {specialty} presentations in {setting} setting\",\n",
240
+ " specialty=specialty,\n",
241
+ " setting=setting,\n",
242
+ " created_at=datetime.now()\n",
243
+ " ),\n",
244
+ " SmartGoal(\n",
245
+ " id=f\"goal_{uuid.uuid4()}\",\n",
246
+ " category=LearningCategory.MANAGEMENT,\n",
247
+ " original_input=\"\",\n",
248
+ " smart_version=f\"Create evidence-based management plans for basic {specialty} conditions\",\n",
249
+ " specialty=specialty,\n",
250
+ " setting=setting,\n",
251
+ " created_at=datetime.now()\n",
252
+ " )\n",
253
+ " ]\n",
254
+ " return default_goals\n",
255
+ " \n",
256
  " async def generate_smart_goal(\n",
257
  " self,\n",
258
  " user_input: str,\n",
 
304
  " logger.error(f\"Error generating SMART goal: {str(e)}\")\n",
305
  " return None\n",
306
  " \n",
307
+ " async def discuss_case(self, message: str) -> AsyncGenerator[str, None]:\n",
308
  " \"\"\"\n",
309
+ " Process case discussion message with streaming response.\n",
310
  " \n",
311
  " Args:\n",
312
  " message: User's message\n",
313
+ " \n",
314
+ " Yields:\n",
315
+ " str: Streamed tokens of tutor's response\n",
316
  " \"\"\"\n",
317
  " try:\n",
318
  " # Add to discussion history\n",
 
323
  " \n",
324
  " # Build conversation prompt\n",
325
  " system_prompt = self._build_discussion_prompt()\n",
 
326
  " messages = [{\n",
327
  " \"role\": \"system\",\n",
328
  " \"content\": system_prompt\n",
329
  " }]\n",
330
  " messages.extend(self.current_discussion)\n",
331
  " \n",
332
+ " # Get streaming response\n",
333
+ " full_response = \"\"\n",
334
+ " async for token in self._get_completion_stream(messages):\n",
335
+ " full_response += token\n",
336
+ " yield token\n",
337
  " \n",
338
+ " # Add complete response to history after streaming\n",
339
  " self.current_discussion.append({\n",
340
  " \"role\": \"assistant\",\n",
341
+ " \"content\": full_response\n",
342
  " })\n",
343
  " \n",
 
 
344
  " except Exception as e:\n",
345
  " logger.error(f\"Error in discussion: {str(e)}\")\n",
346
+ " yield \"I apologize, but I encountered an error. Please try again.\"\n",
347
  " \n",
348
  " def end_discussion(self) -> None:\n",
349
  " \"\"\"End current discussion.\"\"\"\n",
 
375
  " Remember: The learner has strong foundational knowledge. Focus on advanced clinical concepts\n",
376
  " rather than basic science.\"\"\"\n",
377
  "\n",
378
+ " async def _get_completion_stream(\n",
379
  " self,\n",
380
  " messages: List[Dict],\n",
381
  " temperature: float = 0.7,\n",
382
  " max_retries: int = 3\n",
383
+ " ) -> AsyncGenerator[str, None]:\n",
384
  " \"\"\"\n",
385
+ " Get streaming completion from OpenRouter API with retry logic.\n",
386
  " \n",
387
  " Args:\n",
388
  " messages: Conversation messages\n",
389
  " temperature: Response temperature\n",
390
  " max_retries: Maximum retry attempts\n",
391
  " \n",
392
+ " Yields:\n",
393
+ " str: Response tokens as they arrive\n",
 
 
 
394
  " \"\"\"\n",
395
  " headers = {\n",
396
  " \"Authorization\": f\"Bearer {self.api_key}\",\n",
397
  " \"Content-Type\": \"application/json\",\n",
398
+ " \"HTTP-Referer\": \"http://localhost:7860\"\n",
399
  " }\n",
400
  " \n",
401
  " data = {\n",
402
  " \"model\": self.model,\n",
403
  " \"messages\": messages,\n",
404
  " \"temperature\": temperature,\n",
405
+ " \"max_tokens\": 2000,\n",
406
+ " \"stream\": True # Enable streaming\n",
407
  " }\n",
408
  " \n",
409
  " for attempt in range(max_retries):\n",
 
416
  " timeout=30\n",
417
  " ) as response:\n",
418
  " response.raise_for_status()\n",
419
+ " \n",
420
+ " # Process streaming response\n",
421
+ " async for line in response.content:\n",
422
+ " text = line.decode('utf-8').strip()\n",
423
+ " if text.startswith('data: '):\n",
424
+ " try:\n",
425
+ " json_str = text[6:] # Remove 'data: ' prefix\n",
426
+ " if json_str == '[DONE]':\n",
427
+ " break\n",
428
+ " \n",
429
+ " chunk = json.loads(json_str)\n",
430
+ " if token := chunk['choices'][0]['delta'].get('content'):\n",
431
+ " yield token\n",
432
+ " \n",
433
+ " except json.JSONDecodeError:\n",
434
+ " continue\n",
435
+ " \n",
436
+ " return\n",
437
  " \n",
438
  " except Exception as e:\n",
439
  " if attempt == max_retries - 1:\n",
440
  " raise OpenRouterException(f\"API call failed: {str(e)}\")\n",
441
  " logger.warning(f\"Retry {attempt + 1} after error: {str(e)}\")\n",
442
+ " await asyncio.sleep(1 * (attempt + 1)) # Exponential backoff \n",
443
+ " \n",
444
  " def get_discussion_history(self) -> List[Dict]:\n",
445
  " \"\"\"\n",
446
  " Get current discussion history.\n",
 
842
  }
843
  ],
844
  "source": [
845
+ "#| hide\n",
846
  "\n",
847
  "from wardbuddy.clinical_tutor import ClinicalTutor\n",
848
  "from wardbuddy.learning_context import LearningCategory, SmartGoal\n",
849
  "\n",
850
  "async def test_clinical_tutor():\n",
851
  " \"\"\"Test basic clinical tutor functionality.\"\"\"\n",
852
+ " # Setup test environment\n",
853
  " if not os.getenv(\"OPENROUTER_API_KEY\"):\n",
854
  " print(\"Skipping tests: No API key\")\n",
855
  " return\n",
 
861
  " test_case = \"28yo M with chest pain, 2 days duration\"\n",
862
  " \n",
863
  " try:\n",
864
+ " # Test streaming discussion\n",
865
+ " full_response = \"\"\n",
866
+ " async for token in tutor.discuss_case(test_case):\n",
867
+ " full_response += token\n",
868
+ " \n",
869
+ " assert isinstance(full_response, str)\n",
870
+ " assert len(full_response) > 0\n",
871
+ " print(\"Discussion test passed\")\n",
872
  " \n",
873
  " # Test goal generation\n",
874
  " goals = await tutor.generate_smart_goals(\"Emergency Medicine\", \"ED\")\n",
875
+ " assert isinstance(goals, list), \"Expected goals to be a list\"\n",
876
+ " assert len(goals) > 0, \"Expected at least one goal\"\n",
877
+ " assert all(isinstance(goal, SmartGoal) for goal in goals), \"Expected all items to be SmartGoal objects\"\n",
878
+ " print(\"Goal generation test passed\")\n",
879
+ " \n",
880
+ " # Test basic goal properties\n",
881
+ " test_goal = goals[0]\n",
882
+ " assert test_goal.specialty == \"Emergency Medicine\", \"Wrong specialty\"\n",
883
+ " assert test_goal.setting == \"ED\", \"Wrong setting\"\n",
884
+ " assert isinstance(test_goal.category, LearningCategory), \"Wrong category type\"\n",
885
+ " assert isinstance(test_goal.smart_version, str), \"Wrong goal text type\"\n",
886
+ " print(\"Goal properties test passed\")\n",
887
  " \n",
888
  " # Clear discussion\n",
889
  " tutor.end_discussion()\n",
890
+ " assert len(tutor.get_discussion_history()) == 0, \"Discussion history not cleared\"\n",
891
+ " print(\"Discussion clearing test passed\")\n",
892
  " \n",
893
+ " print(\"\\nAll tests passed! ✅\")\n",
894
  " \n",
895
  " except Exception as e:\n",
896
+ " print(f\"\\nTest failed ❌: {str(e)}\")\n",
897
  " raise\n",
898
  "\n",
899
  "# Run tests\n",
900
  "if __name__ == \"__main__\":\n",
901
+ " try:\n",
902
+ " asyncio.run(test_clinical_tutor())\n",
903
+ " except RuntimeError:\n",
904
+ " # Handle case where event loop is already running\n",
905
+ " loop = asyncio.get_event_loop()\n",
906
+ " if loop.is_running():\n",
907
+ " loop.create_task(test_clinical_tutor())"
908
  ]
909
  },
910
  {
nbs/02_learning_interface.ipynb CHANGED
@@ -59,7 +59,7 @@
59
  ],
60
  "source": [
61
  "#| export\n",
62
- "from typing import Dict, List, Optional, Tuple, Any\n",
63
  "import gradio as gr\n",
64
  "from pathlib import Path\n",
65
  "import asyncio\n",
@@ -181,6 +181,50 @@
181
  " self.settings = [\"Clinic\", \"Wards\", \"ED\"]\n",
182
  " \n",
183
  " logger.info(\"Learning interface initialized\")\n",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
  " \n",
185
  " def create_interface(self) -> gr.Blocks:\n",
186
  " \"\"\"Create Gradio interface.\"\"\"\n",
@@ -225,24 +269,35 @@
225
  " \n",
226
  " # Chat interface\n",
227
  " chatbot = gr.Chatbot(\n",
228
- " height=400,\n",
229
- " show_label=False\n",
 
 
230
  " )\n",
231
  " \n",
232
  " with gr.Row():\n",
233
- " message = gr.Textbox(\n",
234
  " label=\"Present your case or ask questions\",\n",
235
- " placeholder=\"Present your case as you would to your supervisor...\",\n",
236
- " lines=4\n",
237
  " )\n",
238
  " \n",
239
- " audio_input = gr.Audio(\n",
240
- " sources=\"microphone\",\n",
241
- " type=\"numpy\",\n",
242
  " label=\"Or speak your case\",\n",
 
 
243
  " streaming=True\n",
244
  " )\n",
245
  " \n",
 
 
 
 
 
 
 
 
246
  " with gr.Row():\n",
247
  " clear = gr.Button(\"Clear Discussion\")\n",
248
  " end = gr.Button(\n",
 
59
  ],
60
  "source": [
61
  "#| export\n",
62
+ "from typing import Dict, List, Optional, Tuple, Any, AsyncGenerator\n",
63
  "import gradio as gr\n",
64
  "from pathlib import Path\n",
65
  "import asyncio\n",
 
181
  " self.settings = [\"Clinic\", \"Wards\", \"ED\"]\n",
182
  " \n",
183
  " logger.info(\"Learning interface initialized\")\n",
184
+ "\n",
185
+ " async def process_chat(\n",
186
+ " self,\n",
187
+ " message: str,\n",
188
+ " history: List[List[str]],\n",
189
+ " state: Dict[str, Any]\n",
190
+ " ) -> AsyncGenerator[Tuple[List[List[str]], str, Dict[str, Any]], None]:\n",
191
+ " \"\"\"Process chat messages with streaming.\"\"\"\n",
192
+ " try:\n",
193
+ " if not message.strip():\n",
194
+ " yield history, \"\", state\n",
195
+ " return\n",
196
+ " \n",
197
+ " # Start new discussion if none active\n",
198
+ " if not state.get(\"discussion_active\"):\n",
199
+ " state[\"discussion_active\"] = True\n",
200
+ " state[\"discussion_start\"] = datetime.now().isoformat()\n",
201
+ " \n",
202
+ " # Initialize history if needed\n",
203
+ " if history is None:\n",
204
+ " history = []\n",
205
+ " \n",
206
+ " # Add user message\n",
207
+ " history.append([message, \"\"])\n",
208
+ " \n",
209
+ " # Update display\n",
210
+ " yield history, \"\", state\n",
211
+ " \n",
212
+ " # Process response with streaming\n",
213
+ " current_response = \"\"\n",
214
+ " async for token in self.tutor.discuss_case(message):\n",
215
+ " current_response += token\n",
216
+ " history[-1][1] = current_response\n",
217
+ " yield history, \"\", state\n",
218
+ " \n",
219
+ " state[\"last_message\"] = datetime.now().isoformat()\n",
220
+ " yield history, \"\", state\n",
221
+ " \n",
222
+ " except Exception as e:\n",
223
+ " logger.error(f\"Error in chat: {str(e)}\")\n",
224
+ " if history is None:\n",
225
+ " history = []\n",
226
+ " history.append([message, \"I apologize, but I encountered an error. Please try again.\"])\n",
227
+ " yield history, \"\", state\n",
228
  " \n",
229
  " def create_interface(self) -> gr.Blocks:\n",
230
  " \"\"\"Create Gradio interface.\"\"\"\n",
 
269
  " \n",
270
  " # Chat interface\n",
271
  " chatbot = gr.Chatbot(\n",
272
+ " height=500,\n",
273
+ " label=\"Case Discussion\",\n",
274
+ " show_label=True,\n",
275
+ " elem_classes=[\"dashboard-card\"]\n",
276
  " )\n",
277
  " \n",
278
  " with gr.Row():\n",
279
+ " msg = gr.Textbox(\n",
280
  " label=\"Present your case or ask questions\",\n",
281
+ " placeholder=\"Present your case...\",\n",
282
+ " lines=5\n",
283
  " )\n",
284
  " \n",
285
+ " # Add voice input\n",
286
+ " audio_msg = gr.Audio(\n",
 
287
  " label=\"Or speak your case\",\n",
288
+ " sources=[\"microphone\"],\n",
289
+ " type=\"numpy\",\n",
290
  " streaming=True\n",
291
  " )\n",
292
  " \n",
293
+ " # Wire up chat with streaming\n",
294
+ " msg.submit(\n",
295
+ " self.process_chat,\n",
296
+ " inputs=[msg, chatbot, state],\n",
297
+ " outputs=[chatbot, msg, state],\n",
298
+ " queue=False # Important for streaming\n",
299
+ " ) \n",
300
+ " \n",
301
  " with gr.Row():\n",
302
  " clear = gr.Button(\"Clear Discussion\")\n",
303
  " end = gr.Button(\n",
wardbuddy/_modidx.py CHANGED
@@ -11,8 +11,8 @@ d = { 'settings': { 'branch': 'main',
11
  'wardbuddy/clinical_tutor.py'),
12
  'wardbuddy.clinical_tutor.ClinicalTutor._build_discussion_prompt': ( 'clinical_tutor.html#clinicaltutor._build_discussion_prompt',
13
  'wardbuddy/clinical_tutor.py'),
14
- 'wardbuddy.clinical_tutor.ClinicalTutor._get_completion': ( 'clinical_tutor.html#clinicaltutor._get_completion',
15
- 'wardbuddy/clinical_tutor.py'),
16
  'wardbuddy.clinical_tutor.ClinicalTutor.clear_discussion': ( 'clinical_tutor.html#clinicaltutor.clear_discussion',
17
  'wardbuddy/clinical_tutor.py'),
18
  'wardbuddy.clinical_tutor.ClinicalTutor.discuss_case': ( 'clinical_tutor.html#clinicaltutor.discuss_case',
@@ -64,6 +64,8 @@ d = { 'settings': { 'branch': 'main',
64
  'wardbuddy/learning_interface.py'),
65
  'wardbuddy.learning_interface.LearningInterface.create_interface': ( 'learning_interface.html#learninginterface.create_interface',
66
  'wardbuddy/learning_interface.py'),
 
 
67
  'wardbuddy.learning_interface.create_css': ( 'learning_interface.html#create_css',
68
  'wardbuddy/learning_interface.py')},
69
  'wardbuddy.main': { 'wardbuddy.main.launch': ('main.html#launch', 'wardbuddy/main.py'),
 
11
  'wardbuddy/clinical_tutor.py'),
12
  'wardbuddy.clinical_tutor.ClinicalTutor._build_discussion_prompt': ( 'clinical_tutor.html#clinicaltutor._build_discussion_prompt',
13
  'wardbuddy/clinical_tutor.py'),
14
+ 'wardbuddy.clinical_tutor.ClinicalTutor._get_completion_stream': ( 'clinical_tutor.html#clinicaltutor._get_completion_stream',
15
+ 'wardbuddy/clinical_tutor.py'),
16
  'wardbuddy.clinical_tutor.ClinicalTutor.clear_discussion': ( 'clinical_tutor.html#clinicaltutor.clear_discussion',
17
  'wardbuddy/clinical_tutor.py'),
18
  'wardbuddy.clinical_tutor.ClinicalTutor.discuss_case': ( 'clinical_tutor.html#clinicaltutor.discuss_case',
 
64
  'wardbuddy/learning_interface.py'),
65
  'wardbuddy.learning_interface.LearningInterface.create_interface': ( 'learning_interface.html#learninginterface.create_interface',
66
  'wardbuddy/learning_interface.py'),
67
+ 'wardbuddy.learning_interface.LearningInterface.process_chat': ( 'learning_interface.html#learninginterface.process_chat',
68
+ 'wardbuddy/learning_interface.py'),
69
  'wardbuddy.learning_interface.create_css': ( 'learning_interface.html#create_css',
70
  'wardbuddy/learning_interface.py')},
71
  'wardbuddy.main': { 'wardbuddy.main.launch': ('main.html#launch', 'wardbuddy/main.py'),
wardbuddy/clinical_tutor.py CHANGED
@@ -6,7 +6,7 @@
6
  __all__ = ['logger', 'OpenRouterException', 'ClinicalTutor']
7
 
8
  # %% ../nbs/01_clinical_tutor.ipynb 4
9
- from typing import Dict, List, Optional, Tuple, Any
10
  import os
11
  import json
12
  import logging
@@ -87,17 +87,29 @@ class ClinicalTutor:
87
  list: Generated SMART goals
88
  """
89
  prompt = f"""Generate {num_goals} SMART learning goals for a medical trainee in {specialty} ({setting}).
90
-
91
- For each goal:
92
- 1. Select an appropriate category from: {', '.join(cat.value for cat in LearningCategory)}
93
- 2. Write a specific, measurable goal that builds clinical competence
94
-
95
- Format as JSON array with fields:
96
- - category: Learning category name
97
- - smart_version: SMART formatted goal text
98
-
99
- Goals should be specific to the {setting} setting in {specialty}."""
100
-
 
 
 
 
 
 
 
 
 
 
 
 
101
  try:
102
  response = await self._get_completion([{
103
  "role": "system",
@@ -121,12 +133,34 @@ Goals should be specific to the {setting} setting in {specialty}."""
121
  )
122
  goals.append(goal)
123
 
 
124
  return goals
125
 
126
  except Exception as e:
127
  logger.error(f"Error generating goals: {str(e)}")
128
- return []
129
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  async def generate_smart_goal(
131
  self,
132
  user_input: str,
@@ -178,15 +212,15 @@ Format as JSON with fields:
178
  logger.error(f"Error generating SMART goal: {str(e)}")
179
  return None
180
 
181
- async def discuss_case(self, message: str) -> str:
182
  """
183
- Process case discussion message.
184
 
185
  Args:
186
  message: User's message
187
-
188
- Returns:
189
- str: Tutor's response
190
  """
191
  try:
192
  # Add to discussion history
@@ -197,27 +231,27 @@ Format as JSON with fields:
197
 
198
  # Build conversation prompt
199
  system_prompt = self._build_discussion_prompt()
200
-
201
  messages = [{
202
  "role": "system",
203
  "content": system_prompt
204
  }]
205
  messages.extend(self.current_discussion)
206
 
207
- # Get response
208
- response = await self._get_completion(messages)
 
 
 
209
 
210
- # Add to history
211
  self.current_discussion.append({
212
  "role": "assistant",
213
- "content": response
214
  })
215
 
216
- return response
217
-
218
  except Exception as e:
219
  logger.error(f"Error in discussion: {str(e)}")
220
- return "I apologize, but I encountered an error. Please try again."
221
 
222
  def end_discussion(self) -> None:
223
  """End current discussion."""
@@ -249,37 +283,35 @@ Format as JSON with fields:
249
  Remember: The learner has strong foundational knowledge. Focus on advanced clinical concepts
250
  rather than basic science."""
251
 
252
- async def _get_completion(
253
  self,
254
  messages: List[Dict],
255
  temperature: float = 0.7,
256
  max_retries: int = 3
257
- ) -> str:
258
  """
259
- Get completion from OpenRouter API with retry logic.
260
 
261
  Args:
262
  messages: Conversation messages
263
  temperature: Response temperature
264
  max_retries: Maximum retry attempts
265
 
266
- Returns:
267
- str: Model response
268
-
269
- Raises:
270
- OpenRouterException: If API calls fail after retries
271
  """
272
  headers = {
273
  "Authorization": f"Bearer {self.api_key}",
274
  "Content-Type": "application/json",
275
- "HTTP-Referer": "http://localhost:7860" # Or your actual domain
276
  }
277
 
278
  data = {
279
  "model": self.model,
280
  "messages": messages,
281
  "temperature": temperature,
282
- "max_tokens": 2000
 
283
  }
284
 
285
  for attempt in range(max_retries):
@@ -292,15 +324,31 @@ Format as JSON with fields:
292
  timeout=30
293
  ) as response:
294
  response.raise_for_status()
295
- result = await response.json()
296
- return result["choices"][0]["message"]["content"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
297
 
298
  except Exception as e:
299
  if attempt == max_retries - 1:
300
  raise OpenRouterException(f"API call failed: {str(e)}")
301
  logger.warning(f"Retry {attempt + 1} after error: {str(e)}")
302
- await asyncio.sleep(1 * (attempt + 1)) # Exponential backoff
303
-
304
  def get_discussion_history(self) -> List[Dict]:
305
  """
306
  Get current discussion history.
 
6
  __all__ = ['logger', 'OpenRouterException', 'ClinicalTutor']
7
 
8
  # %% ../nbs/01_clinical_tutor.ipynb 4
9
+ from typing import Dict, List, Optional, Tuple, Any, AsyncGenerator
10
  import os
11
  import json
12
  import logging
 
87
  list: Generated SMART goals
88
  """
89
  prompt = f"""Generate {num_goals} SMART learning goals for a medical trainee in {specialty} ({setting}).
90
+
91
+ For each goal:
92
+ 1. Select an appropriate category from: {', '.join(cat.value for cat in LearningCategory)}
93
+ 2. Write a specific, measurable goal that builds clinical competence
94
+
95
+ Format as JSON array with fields:
96
+ - category: Learning category name
97
+ - smart_version: SMART formatted goal text
98
+
99
+ For example:
100
+ [
101
+ {{
102
+ "category": "Clinical Reasoning",
103
+ "smart_version": "Accurately diagnose acute coronary syndrome in 5 chest pain cases over 2 weeks with 90% agreement with attending assessment"
104
+ }},
105
+ {{
106
+ "category": "Management",
107
+ "smart_version": "Successfully develop and implement management plans for 3 complex cardiac patients with multiple comorbidities by week 4"
108
+ }}
109
+ ]
110
+
111
+ Goals should be specific to the {setting} setting in {specialty}."""
112
+
113
  try:
114
  response = await self._get_completion([{
115
  "role": "system",
 
133
  )
134
  goals.append(goal)
135
 
136
+ logger.info(f"Generated {len(goals)} SMART goals")
137
  return goals
138
 
139
  except Exception as e:
140
  logger.error(f"Error generating goals: {str(e)}")
141
+ # Instead of returning empty list, generate some default goals
142
+ default_goals = [
143
+ SmartGoal(
144
+ id=f"goal_{uuid.uuid4()}",
145
+ category=LearningCategory.CLINICAL_REASONING,
146
+ original_input="",
147
+ smart_version=f"Develop systematic approach to common {specialty} presentations in {setting} setting",
148
+ specialty=specialty,
149
+ setting=setting,
150
+ created_at=datetime.now()
151
+ ),
152
+ SmartGoal(
153
+ id=f"goal_{uuid.uuid4()}",
154
+ category=LearningCategory.MANAGEMENT,
155
+ original_input="",
156
+ smart_version=f"Create evidence-based management plans for basic {specialty} conditions",
157
+ specialty=specialty,
158
+ setting=setting,
159
+ created_at=datetime.now()
160
+ )
161
+ ]
162
+ return default_goals
163
+
164
  async def generate_smart_goal(
165
  self,
166
  user_input: str,
 
212
  logger.error(f"Error generating SMART goal: {str(e)}")
213
  return None
214
 
215
+ async def discuss_case(self, message: str) -> AsyncGenerator[str, None]:
216
  """
217
+ Process case discussion message with streaming response.
218
 
219
  Args:
220
  message: User's message
221
+
222
+ Yields:
223
+ str: Streamed tokens of tutor's response
224
  """
225
  try:
226
  # Add to discussion history
 
231
 
232
  # Build conversation prompt
233
  system_prompt = self._build_discussion_prompt()
 
234
  messages = [{
235
  "role": "system",
236
  "content": system_prompt
237
  }]
238
  messages.extend(self.current_discussion)
239
 
240
+ # Get streaming response
241
+ full_response = ""
242
+ async for token in self._get_completion_stream(messages):
243
+ full_response += token
244
+ yield token
245
 
246
+ # Add complete response to history after streaming
247
  self.current_discussion.append({
248
  "role": "assistant",
249
+ "content": full_response
250
  })
251
 
 
 
252
  except Exception as e:
253
  logger.error(f"Error in discussion: {str(e)}")
254
+ yield "I apologize, but I encountered an error. Please try again."
255
 
256
  def end_discussion(self) -> None:
257
  """End current discussion."""
 
283
  Remember: The learner has strong foundational knowledge. Focus on advanced clinical concepts
284
  rather than basic science."""
285
 
286
+ async def _get_completion_stream(
287
  self,
288
  messages: List[Dict],
289
  temperature: float = 0.7,
290
  max_retries: int = 3
291
+ ) -> AsyncGenerator[str, None]:
292
  """
293
+ Get streaming completion from OpenRouter API with retry logic.
294
 
295
  Args:
296
  messages: Conversation messages
297
  temperature: Response temperature
298
  max_retries: Maximum retry attempts
299
 
300
+ Yields:
301
+ str: Response tokens as they arrive
 
 
 
302
  """
303
  headers = {
304
  "Authorization": f"Bearer {self.api_key}",
305
  "Content-Type": "application/json",
306
+ "HTTP-Referer": "http://localhost:7860"
307
  }
308
 
309
  data = {
310
  "model": self.model,
311
  "messages": messages,
312
  "temperature": temperature,
313
+ "max_tokens": 2000,
314
+ "stream": True # Enable streaming
315
  }
316
 
317
  for attempt in range(max_retries):
 
324
  timeout=30
325
  ) as response:
326
  response.raise_for_status()
327
+
328
+ # Process streaming response
329
+ async for line in response.content:
330
+ text = line.decode('utf-8').strip()
331
+ if text.startswith('data: '):
332
+ try:
333
+ json_str = text[6:] # Remove 'data: ' prefix
334
+ if json_str == '[DONE]':
335
+ break
336
+
337
+ chunk = json.loads(json_str)
338
+ if token := chunk['choices'][0]['delta'].get('content'):
339
+ yield token
340
+
341
+ except json.JSONDecodeError:
342
+ continue
343
+
344
+ return
345
 
346
  except Exception as e:
347
  if attempt == max_retries - 1:
348
  raise OpenRouterException(f"API call failed: {str(e)}")
349
  logger.warning(f"Retry {attempt + 1} after error: {str(e)}")
350
+ await asyncio.sleep(1 * (attempt + 1)) # Exponential backoff
351
+
352
  def get_discussion_history(self) -> List[Dict]:
353
  """
354
  Get current discussion history.
wardbuddy/learning_interface.py CHANGED
@@ -6,7 +6,7 @@
6
  __all__ = ['logger', 'create_css', 'LearningInterface']
7
 
8
  # %% ../nbs/02_learning_interface.ipynb 4
9
- from typing import Dict, List, Optional, Tuple, Any
10
  import gradio as gr
11
  from pathlib import Path
12
  import asyncio
@@ -92,6 +92,50 @@ class LearningInterface:
92
  self.settings = ["Clinic", "Wards", "ED"]
93
 
94
  logger.info("Learning interface initialized")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
 
96
  def create_interface(self) -> gr.Blocks:
97
  """Create Gradio interface."""
@@ -136,24 +180,35 @@ class LearningInterface:
136
 
137
  # Chat interface
138
  chatbot = gr.Chatbot(
139
- height=400,
140
- show_label=False
 
 
141
  )
142
 
143
  with gr.Row():
144
- message = gr.Textbox(
145
  label="Present your case or ask questions",
146
- placeholder="Present your case as you would to your supervisor...",
147
- lines=4
148
  )
149
 
150
- audio_input = gr.Audio(
151
- sources="microphone",
152
- type="numpy",
153
  label="Or speak your case",
 
 
154
  streaming=True
155
  )
156
 
 
 
 
 
 
 
 
 
157
  with gr.Row():
158
  clear = gr.Button("Clear Discussion")
159
  end = gr.Button(
 
6
  __all__ = ['logger', 'create_css', 'LearningInterface']
7
 
8
  # %% ../nbs/02_learning_interface.ipynb 4
9
+ from typing import Dict, List, Optional, Tuple, Any, AsyncGenerator
10
  import gradio as gr
11
  from pathlib import Path
12
  import asyncio
 
92
  self.settings = ["Clinic", "Wards", "ED"]
93
 
94
  logger.info("Learning interface initialized")
95
+
96
+ async def process_chat(
97
+ self,
98
+ message: str,
99
+ history: List[List[str]],
100
+ state: Dict[str, Any]
101
+ ) -> AsyncGenerator[Tuple[List[List[str]], str, Dict[str, Any]], None]:
102
+ """Process chat messages with streaming."""
103
+ try:
104
+ if not message.strip():
105
+ yield history, "", state
106
+ return
107
+
108
+ # Start new discussion if none active
109
+ if not state.get("discussion_active"):
110
+ state["discussion_active"] = True
111
+ state["discussion_start"] = datetime.now().isoformat()
112
+
113
+ # Initialize history if needed
114
+ if history is None:
115
+ history = []
116
+
117
+ # Add user message
118
+ history.append([message, ""])
119
+
120
+ # Update display
121
+ yield history, "", state
122
+
123
+ # Process response with streaming
124
+ current_response = ""
125
+ async for token in self.tutor.discuss_case(message):
126
+ current_response += token
127
+ history[-1][1] = current_response
128
+ yield history, "", state
129
+
130
+ state["last_message"] = datetime.now().isoformat()
131
+ yield history, "", state
132
+
133
+ except Exception as e:
134
+ logger.error(f"Error in chat: {str(e)}")
135
+ if history is None:
136
+ history = []
137
+ history.append([message, "I apologize, but I encountered an error. Please try again."])
138
+ yield history, "", state
139
 
140
  def create_interface(self) -> gr.Blocks:
141
  """Create Gradio interface."""
 
180
 
181
  # Chat interface
182
  chatbot = gr.Chatbot(
183
+ height=500,
184
+ label="Case Discussion",
185
+ show_label=True,
186
+ elem_classes=["dashboard-card"]
187
  )
188
 
189
  with gr.Row():
190
+ msg = gr.Textbox(
191
  label="Present your case or ask questions",
192
+ placeholder="Present your case...",
193
+ lines=5
194
  )
195
 
196
+ # Add voice input
197
+ audio_msg = gr.Audio(
 
198
  label="Or speak your case",
199
+ sources=["microphone"],
200
+ type="numpy",
201
  streaming=True
202
  )
203
 
204
+ # Wire up chat with streaming
205
+ msg.submit(
206
+ self.process_chat,
207
+ inputs=[msg, chatbot, state],
208
+ outputs=[chatbot, msg, state],
209
+ queue=False # Important for streaming
210
+ )
211
+
212
  with gr.Row():
213
  clear = gr.Button("Clear Discussion")
214
  end = gr.Button(