Upload folder using huggingface_hub
Browse files- nbs/02_learning_interface.ipynb +138 -338
- wardbuddy/_modidx.py +8 -0
- wardbuddy/learning_interface.py +137 -337
nbs/02_learning_interface.ipynb
CHANGED
|
@@ -178,7 +178,7 @@
|
|
| 178 |
" Features:\n",
|
| 179 |
" - Context selection\n",
|
| 180 |
" - Goal management\n",
|
| 181 |
-
" - Case discussion\n",
|
| 182 |
" - Progress tracking\n",
|
| 183 |
" \"\"\"\n",
|
| 184 |
" \n",
|
|
@@ -187,13 +187,7 @@
|
|
| 187 |
" context_path: Optional[Path] = None,\n",
|
| 188 |
" model: str = \"anthropic/claude-3.5-sonnet\"\n",
|
| 189 |
" ):\n",
|
| 190 |
-
" \"\"\"\n",
|
| 191 |
-
" Initialize interface.\n",
|
| 192 |
-
" \n",
|
| 193 |
-
" Args:\n",
|
| 194 |
-
" context_path: Path for context persistence\n",
|
| 195 |
-
" model: OpenRouter model identifier\n",
|
| 196 |
-
" \"\"\"\n",
|
| 197 |
" self.tutor = ClinicalTutor(context_path, model)\n",
|
| 198 |
" \n",
|
| 199 |
" # Available options\n",
|
|
@@ -208,7 +202,7 @@
|
|
| 208 |
" self.settings = [\"Clinic\", \"Wards\", \"ED\"]\n",
|
| 209 |
" \n",
|
| 210 |
" logger.info(\"Learning interface initialized\")\n",
|
| 211 |
-
"\n",
|
| 212 |
" async def process_chat(\n",
|
| 213 |
" self,\n",
|
| 214 |
" message: str,\n",
|
|
@@ -252,9 +246,116 @@
|
|
| 252 |
" history = []\n",
|
| 253 |
" history.append([message, \"I apologize, but I encountered an error. Please try again.\"])\n",
|
| 254 |
" yield history, \"\", state\n",
|
| 255 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 256 |
" def create_interface(self) -> gr.Blocks:\n",
|
| 257 |
-
" \"\"\"Create
|
| 258 |
" with gr.Blocks(title=\"Clinical Learning Assistant\", css=create_css()) as interface:\n",
|
| 259 |
" # State management\n",
|
| 260 |
" state = gr.State({\n",
|
|
@@ -263,10 +364,10 @@
|
|
| 263 |
" \"discussion_start\": None,\n",
|
| 264 |
" \"last_message\": None\n",
|
| 265 |
" })\n",
|
| 266 |
-
"
|
| 267 |
" # Header\n",
|
| 268 |
" gr.Markdown(\"# Clinical Learning Assistant\")\n",
|
| 269 |
-
"
|
| 270 |
" with gr.Row():\n",
|
| 271 |
" # Context selection\n",
|
| 272 |
" specialty = gr.Dropdown(\n",
|
|
@@ -279,20 +380,20 @@
|
|
| 279 |
" label=\"Setting\",\n",
|
| 280 |
" value=self.tutor.learning_context.rotation.setting or None\n",
|
| 281 |
" )\n",
|
| 282 |
-
"
|
| 283 |
" # Main content\n",
|
| 284 |
" with gr.Row():\n",
|
| 285 |
" # Left: Discussion interface\n",
|
| 286 |
" with gr.Column(scale=2):\n",
|
| 287 |
" # Active goal display\n",
|
| 288 |
" goal_display = gr.Markdown(value=\"No active learning goal\")\n",
|
| 289 |
-
"
|
| 290 |
" # Discussion status\n",
|
| 291 |
" discussion_status = gr.Markdown(\n",
|
| 292 |
" value=\"Start a new case discussion\",\n",
|
| 293 |
" elem_classes=[\"discussion-status\"]\n",
|
| 294 |
" )\n",
|
| 295 |
-
"
|
| 296 |
" # Chat interface\n",
|
| 297 |
" chatbot = gr.Chatbot(\n",
|
| 298 |
" height=400,\n",
|
|
@@ -300,7 +401,7 @@
|
|
| 300 |
" type=\"messages\", # Use newer message format\n",
|
| 301 |
" elem_classes=[\"chat-window\"]\n",
|
| 302 |
" )\n",
|
| 303 |
-
"
|
| 304 |
" with gr.Row():\n",
|
| 305 |
" msg = gr.Textbox(\n",
|
| 306 |
" label=\"Present your case or ask questions\",\n",
|
|
@@ -312,21 +413,21 @@
|
|
| 312 |
" ),\n",
|
| 313 |
" lines=4\n",
|
| 314 |
" )\n",
|
| 315 |
-
"
|
| 316 |
" audio_input = gr.Audio(\n",
|
| 317 |
" sources=[\"microphone\"],\n",
|
| 318 |
" type=\"numpy\",\n",
|
| 319 |
" label=\"Or speak your case\",\n",
|
| 320 |
" streaming=True\n",
|
| 321 |
" )\n",
|
| 322 |
-
"
|
| 323 |
" with gr.Row():\n",
|
| 324 |
" clear = gr.Button(\"Clear Discussion\")\n",
|
| 325 |
" end = gr.Button(\n",
|
| 326 |
" \"End Discussion & Review\",\n",
|
| 327 |
" variant=\"primary\"\n",
|
| 328 |
" )\n",
|
| 329 |
-
"
|
| 330 |
" # Right: Progress & Goals\n",
|
| 331 |
" with gr.Column(scale=1):\n",
|
| 332 |
" with gr.Tab(\"Learning Goals\"):\n",
|
|
@@ -338,7 +439,7 @@
|
|
| 338 |
" placeholder=\"What do you want to get better at?\"\n",
|
| 339 |
" )\n",
|
| 340 |
" add_goal = gr.Button(\"Add Goal\")\n",
|
| 341 |
-
"
|
| 342 |
" # Goals list\n",
|
| 343 |
" goals_list = gr.DataFrame(\n",
|
| 344 |
" headers=[\"Goal\", \"Category\", \"Status\"],\n",
|
|
@@ -347,7 +448,7 @@
|
|
| 347 |
" interactive=True,\n",
|
| 348 |
" wrap=True\n",
|
| 349 |
" )\n",
|
| 350 |
-
"
|
| 351 |
" with gr.Tab(\"Progress\"):\n",
|
| 352 |
" # Category progress\n",
|
| 353 |
" progress_display = gr.DataFrame(\n",
|
|
@@ -355,14 +456,14 @@
|
|
| 355 |
" value=[],\n",
|
| 356 |
" label=\"Progress by Category\"\n",
|
| 357 |
" )\n",
|
| 358 |
-
"
|
| 359 |
" # Recent completions\n",
|
| 360 |
" recent_display = gr.DataFrame(\n",
|
| 361 |
" headers=[\"Goal\", \"Category\", \"Completed\"],\n",
|
| 362 |
" value=[],\n",
|
| 363 |
" label=\"Recently Completed\"\n",
|
| 364 |
" )\n",
|
| 365 |
-
"
|
| 366 |
" # Event handlers\n",
|
| 367 |
" async def update_context(spec: str, set: str, state: Dict) -> List:\n",
|
| 368 |
" \"\"\"Update rotation context.\"\"\"\n",
|
|
@@ -373,14 +474,13 @@
|
|
| 373 |
" state[\"suggested_goals\"] = goals\n",
|
| 374 |
" return self._update_displays(state)\n",
|
| 375 |
" return [[], [], [], \"No active learning goal\"]\n",
|
| 376 |
-
"
|
| 377 |
" def process_audio(audio: np.ndarray) -> str:\n",
|
| 378 |
" \"\"\"Convert audio to text.\"\"\"\n",
|
| 379 |
" if audio is None:\n",
|
| 380 |
" return \"\"\n",
|
| 381 |
-
" # Would integrate with speech-to-text here\n",
|
| 382 |
" return \"Audio transcription would appear here\"\n",
|
| 383 |
-
"
|
| 384 |
" def clear_discussion() -> Tuple[List, str, Dict]:\n",
|
| 385 |
" \"\"\"Clear chat history.\"\"\"\n",
|
| 386 |
" return [], \"\", {\n",
|
|
@@ -389,20 +489,20 @@
|
|
| 389 |
" \"discussion_start\": None,\n",
|
| 390 |
" \"last_message\": None\n",
|
| 391 |
" }\n",
|
| 392 |
-
"
|
| 393 |
" # Wire up events\n",
|
| 394 |
" specialty.change(\n",
|
| 395 |
" update_context,\n",
|
| 396 |
" inputs=[specialty, setting, state],\n",
|
| 397 |
" outputs=[goals_list, progress_display, recent_display, goal_display]\n",
|
| 398 |
" )\n",
|
| 399 |
-
"
|
| 400 |
" setting.change(\n",
|
| 401 |
" update_context,\n",
|
| 402 |
" inputs=[specialty, setting, state],\n",
|
| 403 |
" outputs=[goals_list, progress_display, recent_display, goal_display]\n",
|
| 404 |
" )\n",
|
| 405 |
-
"
|
| 406 |
" # Chat events with streaming\n",
|
| 407 |
" msg.submit(\n",
|
| 408 |
" self.process_chat,\n",
|
|
@@ -414,7 +514,7 @@
|
|
| 414 |
" inputs=[state],\n",
|
| 415 |
" outputs=[discussion_status]\n",
|
| 416 |
" )\n",
|
| 417 |
-
"
|
| 418 |
" # Voice input handling\n",
|
| 419 |
" audio_input.stream(\n",
|
| 420 |
" process_audio,\n",
|
|
@@ -425,7 +525,7 @@
|
|
| 425 |
" inputs=[msg, chatbot, state],\n",
|
| 426 |
" outputs=[chatbot, msg, state]\n",
|
| 427 |
" )\n",
|
| 428 |
-
"
|
| 429 |
" # Button handlers\n",
|
| 430 |
" clear.click(\n",
|
| 431 |
" clear_discussion,\n",
|
|
@@ -434,333 +534,33 @@
|
|
| 434 |
" lambda: \"Start a new case discussion\",\n",
|
| 435 |
" outputs=[discussion_status]\n",
|
| 436 |
" )\n",
|
| 437 |
-
"
|
| 438 |
" end.click(\n",
|
| 439 |
" self.end_discussion,\n",
|
| 440 |
" inputs=[state],\n",
|
| 441 |
" outputs=[goals_list, progress_display, recent_display, goal_display]\n",
|
| 442 |
" )\n",
|
| 443 |
-
"
|
| 444 |
" generate.click(\n",
|
| 445 |
" self.generate_goals,\n",
|
| 446 |
" inputs=[state],\n",
|
| 447 |
" outputs=[goals_list, progress_display, recent_display, goal_display]\n",
|
| 448 |
" )\n",
|
| 449 |
-
"
|
| 450 |
" # Goal management\n",
|
| 451 |
" add_goal.click(\n",
|
| 452 |
" self.add_user_goal,\n",
|
| 453 |
" inputs=[new_goal, state],\n",
|
| 454 |
" outputs=[goals_list, progress_display, recent_display, goal_display]\n",
|
| 455 |
" )\n",
|
| 456 |
-
"
|
| 457 |
" goals_list.select(\n",
|
| 458 |
" self.select_goal,\n",
|
| 459 |
" inputs=[state],\n",
|
| 460 |
" outputs=[goals_list, progress_display, recent_display, goal_display]\n",
|
| 461 |
" )\n",
|
| 462 |
-
"
|
| 463 |
-
" return interface
|
| 464 |
-
" \n",
|
| 465 |
-
" # Event handlers\n",
|
| 466 |
-
" async def update_context(self, spec: str, set: str, state: Dict) -> List:\n",
|
| 467 |
-
" \"\"\"Update rotation context.\n",
|
| 468 |
-
" \n",
|
| 469 |
-
" Args:\n",
|
| 470 |
-
" spec: Specialty\n",
|
| 471 |
-
" set: Setting\n",
|
| 472 |
-
" state: Interface state\n",
|
| 473 |
-
" \n",
|
| 474 |
-
" Returns:\n",
|
| 475 |
-
" list: Updated display data\n",
|
| 476 |
-
" \"\"\"\n",
|
| 477 |
-
" try:\n",
|
| 478 |
-
" if spec and set:\n",
|
| 479 |
-
" self.tutor.learning_context.update_rotation(spec, set)\n",
|
| 480 |
-
" \n",
|
| 481 |
-
" # Initialize goals list if needed\n",
|
| 482 |
-
" if not state.get(\"suggested_goals\"):\n",
|
| 483 |
-
" goals = await self.tutor.generate_smart_goals(spec, set)\n",
|
| 484 |
-
" state[\"suggested_goals\"] = goals\n",
|
| 485 |
-
" \n",
|
| 486 |
-
" # Return all display updates\n",
|
| 487 |
-
" goals_data = []\n",
|
| 488 |
-
" context = self.tutor.learning_context\n",
|
| 489 |
-
" \n",
|
| 490 |
-
" # Update goals list\n",
|
| 491 |
-
" for goal in state.get(\"suggested_goals\", []):\n",
|
| 492 |
-
" status = \"Active\" if (\n",
|
| 493 |
-
" context.active_goal and \n",
|
| 494 |
-
" context.active_goal.id == goal.id\n",
|
| 495 |
-
" ) else \"Available\"\n",
|
| 496 |
-
" \n",
|
| 497 |
-
" goals_data.append([\n",
|
| 498 |
-
" goal.smart_version,\n",
|
| 499 |
-
" goal.category.value,\n",
|
| 500 |
-
" status\n",
|
| 501 |
-
" ])\n",
|
| 502 |
-
" \n",
|
| 503 |
-
" # Update progress display\n",
|
| 504 |
-
" summary = context.get_category_summary()\n",
|
| 505 |
-
" progress_data = [\n",
|
| 506 |
-
" [cat, data[\"completed\"], data[\"total\"]]\n",
|
| 507 |
-
" for cat, data in summary.items()\n",
|
| 508 |
-
" ]\n",
|
| 509 |
-
" \n",
|
| 510 |
-
" # Update recent completions\n",
|
| 511 |
-
" recent_data = []\n",
|
| 512 |
-
" for cat, data in summary.items():\n",
|
| 513 |
-
" for goal in data[\"recent\"]:\n",
|
| 514 |
-
" recent_data.append([\n",
|
| 515 |
-
" goal[\"smart_version\"],\n",
|
| 516 |
-
" cat,\n",
|
| 517 |
-
" goal[\"completed_at\"]\n",
|
| 518 |
-
" ])\n",
|
| 519 |
-
" \n",
|
| 520 |
-
" # Update active goal display\n",
|
| 521 |
-
" goal_text = (\n",
|
| 522 |
-
" f\"Current Goal: {context.active_goal.smart_version}\"\n",
|
| 523 |
-
" if context.active_goal else\n",
|
| 524 |
-
" \"No active learning goal\"\n",
|
| 525 |
-
" )\n",
|
| 526 |
-
" \n",
|
| 527 |
-
" return [\n",
|
| 528 |
-
" goals_data,\n",
|
| 529 |
-
" progress_data,\n",
|
| 530 |
-
" recent_data,\n",
|
| 531 |
-
" goal_text\n",
|
| 532 |
-
" ]\n",
|
| 533 |
-
" \n",
|
| 534 |
-
" # Return empty displays if no context\n",
|
| 535 |
-
" return [[], [], [], \"No active learning goal\"]\n",
|
| 536 |
-
" \n",
|
| 537 |
-
" except Exception as e:\n",
|
| 538 |
-
" logger.error(f\"Error updating context: {str(e)}\")\n",
|
| 539 |
-
" return [[], [], [], \"Error updating context\"]\n",
|
| 540 |
-
" \n",
|
| 541 |
-
" async def process_message(msg, history):\n",
|
| 542 |
-
" \"\"\"Process chat message.\"\"\"\n",
|
| 543 |
-
" if not msg.strip():\n",
|
| 544 |
-
" return history, \"\"\n",
|
| 545 |
-
" \n",
|
| 546 |
-
" response = await self.tutor.discuss_case(msg)\n",
|
| 547 |
-
" history.append([msg, response])\n",
|
| 548 |
-
" return history, \"\"\n",
|
| 549 |
-
" \n",
|
| 550 |
-
" def process_audio(audio):\n",
|
| 551 |
-
" \"\"\"Convert audio to text.\"\"\"\n",
|
| 552 |
-
" # Would integrate with speech-to-text here\n",
|
| 553 |
-
" return \"Audio transcription would appear here\"\n",
|
| 554 |
-
" \n",
|
| 555 |
-
" async def generate_goals(self, state: Dict[str, Any]) -> List:\n",
|
| 556 |
-
" \"\"\"Generate new goal suggestions.\n",
|
| 557 |
-
" \n",
|
| 558 |
-
" Args:\n",
|
| 559 |
-
" state: Interface state\n",
|
| 560 |
-
" \n",
|
| 561 |
-
" Returns:\n",
|
| 562 |
-
" list: Updated display data\n",
|
| 563 |
-
" \"\"\"\n",
|
| 564 |
-
" rotation = self.tutor.learning_context.rotation\n",
|
| 565 |
-
" if rotation.specialty and rotation.setting:\n",
|
| 566 |
-
" goals = await self.tutor.generate_smart_goals(\n",
|
| 567 |
-
" rotation.specialty,\n",
|
| 568 |
-
" rotation.setting\n",
|
| 569 |
-
" )\n",
|
| 570 |
-
" state[\"suggested_goals\"] = goals\n",
|
| 571 |
-
" return self._update_displays(state)\n",
|
| 572 |
-
" return [[], [], [], \"No active learning goal\"]\n",
|
| 573 |
-
" \n",
|
| 574 |
-
" async def add_user_goal(self, text: str, state: Dict[str, Any]) -> List:\n",
|
| 575 |
-
" \"\"\"Add user-specified goal.\n",
|
| 576 |
-
" \n",
|
| 577 |
-
" Args:\n",
|
| 578 |
-
" text: User's goal text\n",
|
| 579 |
-
" state: Interface state\n",
|
| 580 |
-
" \n",
|
| 581 |
-
" Returns:\n",
|
| 582 |
-
" list: Updated display data\n",
|
| 583 |
-
" \"\"\"\n",
|
| 584 |
-
" if not text.strip():\n",
|
| 585 |
-
" return self._update_displays(state)\n",
|
| 586 |
-
" \n",
|
| 587 |
-
" rotation = self.tutor.learning_context.rotation\n",
|
| 588 |
-
" if rotation.specialty and rotation.setting:\n",
|
| 589 |
-
" goal = await self.tutor.generate_smart_goal(\n",
|
| 590 |
-
" text,\n",
|
| 591 |
-
" rotation.specialty,\n",
|
| 592 |
-
" rotation.setting\n",
|
| 593 |
-
" )\n",
|
| 594 |
-
" if goal:\n",
|
| 595 |
-
" state[\"suggested_goals\"].append(goal)\n",
|
| 596 |
-
" return self._update_displays(state)\n",
|
| 597 |
-
" return self._update_displays(state)\n",
|
| 598 |
-
" \n",
|
| 599 |
-
" def select_goal(self, evt: gr.SelectData, state: Dict[str, Any]) -> List:\n",
|
| 600 |
-
" \"\"\"Set selected goal as active.\n",
|
| 601 |
-
" \n",
|
| 602 |
-
" Args:\n",
|
| 603 |
-
" evt: Selection event data\n",
|
| 604 |
-
" state: Interface state\n",
|
| 605 |
-
" \n",
|
| 606 |
-
" Returns:\n",
|
| 607 |
-
" list: Updated display data\n",
|
| 608 |
-
" \"\"\"\n",
|
| 609 |
-
" if evt.index[0] < len(state[\"suggested_goals\"]):\n",
|
| 610 |
-
" goal = state[\"suggested_goals\"][evt.index[0]]\n",
|
| 611 |
-
" self.tutor.learning_context.add_smart_goal(goal)\n",
|
| 612 |
-
" return self._update_displays(state)\n",
|
| 613 |
-
" return self._update_displays(state)\n",
|
| 614 |
-
" \n",
|
| 615 |
-
" def end_discussion(self, state: Dict[str, Any]) -> List:\n",
|
| 616 |
-
" \"\"\"End current discussion.\n",
|
| 617 |
-
" \n",
|
| 618 |
-
" Args:\n",
|
| 619 |
-
" state: Interface state\n",
|
| 620 |
-
" \n",
|
| 621 |
-
" Returns:\n",
|
| 622 |
-
" list: Updated display data\n",
|
| 623 |
-
" \"\"\"\n",
|
| 624 |
-
" if self.tutor.learning_context.active_goal:\n",
|
| 625 |
-
" self.tutor.learning_context.complete_active_goal()\n",
|
| 626 |
-
" \n",
|
| 627 |
-
" self.tutor.end_discussion()\n",
|
| 628 |
-
" state[\"discussion_active\"] = False\n",
|
| 629 |
-
" state[\"discussion_start\"] = None\n",
|
| 630 |
-
" \n",
|
| 631 |
-
" return self._update_displays(state) \n",
|
| 632 |
-
" \n",
|
| 633 |
-
" def clear_discussion():\n",
|
| 634 |
-
" \"\"\"Clear discussion history.\"\"\"\n",
|
| 635 |
-
" self.tutor.clear_discussion()\n",
|
| 636 |
-
" return [], \"\"\n",
|
| 637 |
-
" \n",
|
| 638 |
-
" # Wire up events\n",
|
| 639 |
-
" specialty.change(\n",
|
| 640 |
-
" update_context,\n",
|
| 641 |
-
" inputs=[specialty, setting, state], # Add state to inputs\n",
|
| 642 |
-
" outputs=[goals_list, progress_display, recent_display, goal_display]\n",
|
| 643 |
-
" )\n",
|
| 644 |
-
" \n",
|
| 645 |
-
" setting.change(\n",
|
| 646 |
-
" update_context,\n",
|
| 647 |
-
" inputs=[specialty, setting, state], # Add state to inputs\n",
|
| 648 |
-
" outputs=[goals_list, progress_display, recent_display, goal_display]\n",
|
| 649 |
-
" )\n",
|
| 650 |
-
" \n",
|
| 651 |
-
" message.submit(\n",
|
| 652 |
-
" process_message,\n",
|
| 653 |
-
" inputs=[message, chatbot],\n",
|
| 654 |
-
" outputs=[chatbot, message]\n",
|
| 655 |
-
" )\n",
|
| 656 |
-
" \n",
|
| 657 |
-
" audio_input.stream(\n",
|
| 658 |
-
" process_audio,\n",
|
| 659 |
-
" inputs=[audio_input],\n",
|
| 660 |
-
" outputs=[message]\n",
|
| 661 |
-
" )\n",
|
| 662 |
-
" \n",
|
| 663 |
-
" clear.click(\n",
|
| 664 |
-
" clear_discussion,\n",
|
| 665 |
-
" outputs=[chatbot, message]\n",
|
| 666 |
-
" )\n",
|
| 667 |
-
" \n",
|
| 668 |
-
" end.click(\n",
|
| 669 |
-
" end_discussion,\n",
|
| 670 |
-
" inputs=[state],\n",
|
| 671 |
-
" outputs=[goals_list, progress_display, recent_display, goal_display]\n",
|
| 672 |
-
" )\n",
|
| 673 |
-
" \n",
|
| 674 |
-
" generate.click(\n",
|
| 675 |
-
" generate_goals,\n",
|
| 676 |
-
" inputs=[state],\n",
|
| 677 |
-
" outputs=[goals_list, progress_display, recent_display, goal_display]\n",
|
| 678 |
-
" )\n",
|
| 679 |
-
" \n",
|
| 680 |
-
" new_goal.submit(\n",
|
| 681 |
-
" add_user_goal,\n",
|
| 682 |
-
" inputs=[new_goal, state],\n",
|
| 683 |
-
" outputs=[goals_list, progress_display, recent_display, goal_display]\n",
|
| 684 |
-
" )\n",
|
| 685 |
-
" \n",
|
| 686 |
-
" goals_list.select(\n",
|
| 687 |
-
" select_goal,\n",
|
| 688 |
-
" inputs=[state],\n",
|
| 689 |
-
" outputs=[goals_list, progress_display, recent_display, goal_display]\n",
|
| 690 |
-
" )\n",
|
| 691 |
-
" \n",
|
| 692 |
-
" return interface\n",
|
| 693 |
-
" \n",
|
| 694 |
-
" def _update_displays(self, state: Dict) -> List:\n",
|
| 695 |
-
" \"\"\"Update all display components.\"\"\"\n",
|
| 696 |
-
" context = self.tutor.learning_context\n",
|
| 697 |
-
" \n",
|
| 698 |
-
" # Update goals list\n",
|
| 699 |
-
" goals_data = []\n",
|
| 700 |
-
" for goal in state[\"suggested_goals\"]:\n",
|
| 701 |
-
" status = \"Active\" if (\n",
|
| 702 |
-
" context.active_goal and \n",
|
| 703 |
-
" context.active_goal.id == goal.id\n",
|
| 704 |
-
" ) else \"Available\"\n",
|
| 705 |
-
" \n",
|
| 706 |
-
" goals_data.append([\n",
|
| 707 |
-
" goal.smart_version,\n",
|
| 708 |
-
" goal.category.value,\n",
|
| 709 |
-
" status\n",
|
| 710 |
-
" ])\n",
|
| 711 |
-
" \n",
|
| 712 |
-
" # Update progress display\n",
|
| 713 |
-
" summary = context.get_category_summary()\n",
|
| 714 |
-
" progress_data = [\n",
|
| 715 |
-
" [cat, data[\"completed\"], data[\"total\"]]\n",
|
| 716 |
-
" for cat, data in summary.items()\n",
|
| 717 |
-
" ]\n",
|
| 718 |
-
" \n",
|
| 719 |
-
" # Update recent completions\n",
|
| 720 |
-
" recent_data = []\n",
|
| 721 |
-
" for cat, data in summary.items():\n",
|
| 722 |
-
" for goal in data[\"recent\"]:\n",
|
| 723 |
-
" recent_data.append([\n",
|
| 724 |
-
" goal[\"smart_version\"],\n",
|
| 725 |
-
" cat,\n",
|
| 726 |
-
" goal[\"completed_at\"]\n",
|
| 727 |
-
" ])\n",
|
| 728 |
-
" \n",
|
| 729 |
-
" # Update active goal display\n",
|
| 730 |
-
" goal_text = (\n",
|
| 731 |
-
" f\"Current Goal: {context.active_goal.smart_version}\"\n",
|
| 732 |
-
" if context.active_goal else\n",
|
| 733 |
-
" \"No active learning goal\"\n",
|
| 734 |
-
" )\n",
|
| 735 |
-
" \n",
|
| 736 |
-
" return [\n",
|
| 737 |
-
" goals_data,\n",
|
| 738 |
-
" progress_data,\n",
|
| 739 |
-
" recent_data,\n",
|
| 740 |
-
" goal_text\n",
|
| 741 |
-
" ]\n",
|
| 742 |
-
" def _update_discussion_status(self, state: Dict[str, Any]) -> str:\n",
|
| 743 |
-
" \"\"\"Update discussion status display based on state.\n",
|
| 744 |
-
" \n",
|
| 745 |
-
" Args:\n",
|
| 746 |
-
" state: Current interface state\n",
|
| 747 |
-
" \n",
|
| 748 |
-
" Returns:\n",
|
| 749 |
-
" str: Status message to display\n",
|
| 750 |
-
" \"\"\"\n",
|
| 751 |
-
" try:\n",
|
| 752 |
-
" if not state.get(\"discussion_active\"):\n",
|
| 753 |
-
" return \"Start a new case discussion\"\n",
|
| 754 |
-
" \n",
|
| 755 |
-
" start = datetime.fromisoformat(state[\"discussion_start\"])\n",
|
| 756 |
-
" duration = datetime.now() - start\n",
|
| 757 |
-
" minutes = int(duration.total_seconds() / 60)\n",
|
| 758 |
-
" \n",
|
| 759 |
-
" return f\"Active discussion ({minutes} minutes)\"\n",
|
| 760 |
-
" \n",
|
| 761 |
-
" except Exception as e:\n",
|
| 762 |
-
" logger.error(f\"Error updating status: {str(e)}\")\n",
|
| 763 |
-
" return \"Discussion status unknown\""
|
| 764 |
]
|
| 765 |
},
|
| 766 |
{
|
|
|
|
| 178 |
" Features:\n",
|
| 179 |
" - Context selection\n",
|
| 180 |
" - Goal management\n",
|
| 181 |
+
" - Case discussion with streaming\n",
|
| 182 |
" - Progress tracking\n",
|
| 183 |
" \"\"\"\n",
|
| 184 |
" \n",
|
|
|
|
| 187 |
" context_path: Optional[Path] = None,\n",
|
| 188 |
" model: str = \"anthropic/claude-3.5-sonnet\"\n",
|
| 189 |
" ):\n",
|
| 190 |
+
" \"\"\"Initialize interface.\"\"\"\n",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
" self.tutor = ClinicalTutor(context_path, model)\n",
|
| 192 |
" \n",
|
| 193 |
" # Available options\n",
|
|
|
|
| 202 |
" self.settings = [\"Clinic\", \"Wards\", \"ED\"]\n",
|
| 203 |
" \n",
|
| 204 |
" logger.info(\"Learning interface initialized\")\n",
|
| 205 |
+
" \n",
|
| 206 |
" async def process_chat(\n",
|
| 207 |
" self,\n",
|
| 208 |
" message: str,\n",
|
|
|
|
| 246 |
" history = []\n",
|
| 247 |
" history.append([message, \"I apologize, but I encountered an error. Please try again.\"])\n",
|
| 248 |
" yield history, \"\", state\n",
|
| 249 |
+
"\n",
|
| 250 |
+
" def _update_discussion_status(self, state: Dict[str, Any]) -> str:\n",
|
| 251 |
+
" \"\"\"Update discussion status display.\"\"\"\n",
|
| 252 |
+
" try:\n",
|
| 253 |
+
" if not state.get(\"discussion_active\"):\n",
|
| 254 |
+
" return \"Start a new case discussion\"\n",
|
| 255 |
+
" \n",
|
| 256 |
+
" start = datetime.fromisoformat(state[\"discussion_start\"])\n",
|
| 257 |
+
" duration = datetime.now() - start\n",
|
| 258 |
+
" minutes = int(duration.total_seconds() / 60)\n",
|
| 259 |
+
" \n",
|
| 260 |
+
" return f\"Active discussion ({minutes} minutes)\"\n",
|
| 261 |
+
" \n",
|
| 262 |
+
" except Exception as e:\n",
|
| 263 |
+
" logger.error(f\"Error updating status: {str(e)}\")\n",
|
| 264 |
+
" return \"Discussion status unknown\"\n",
|
| 265 |
+
"\n",
|
| 266 |
+
" async def generate_goals(self, state: Dict[str, Any]) -> List:\n",
|
| 267 |
+
" \"\"\"Generate new goal suggestions.\"\"\"\n",
|
| 268 |
+
" rotation = self.tutor.learning_context.rotation\n",
|
| 269 |
+
" if rotation.specialty and rotation.setting:\n",
|
| 270 |
+
" goals = await self.tutor.generate_smart_goals(\n",
|
| 271 |
+
" rotation.specialty,\n",
|
| 272 |
+
" rotation.setting\n",
|
| 273 |
+
" )\n",
|
| 274 |
+
" state[\"suggested_goals\"] = goals\n",
|
| 275 |
+
" return self._update_displays(state)\n",
|
| 276 |
+
" return [[], [], [], \"No active learning goal\"]\n",
|
| 277 |
+
"\n",
|
| 278 |
+
" async def add_user_goal(self, text: str, state: Dict[str, Any]) -> List:\n",
|
| 279 |
+
" \"\"\"Add user-specified goal.\"\"\"\n",
|
| 280 |
+
" if not text.strip():\n",
|
| 281 |
+
" return self._update_displays(state)\n",
|
| 282 |
+
" \n",
|
| 283 |
+
" rotation = self.tutor.learning_context.rotation\n",
|
| 284 |
+
" if rotation.specialty and rotation.setting:\n",
|
| 285 |
+
" goal = await self.tutor.generate_smart_goal(\n",
|
| 286 |
+
" text,\n",
|
| 287 |
+
" rotation.specialty,\n",
|
| 288 |
+
" rotation.setting\n",
|
| 289 |
+
" )\n",
|
| 290 |
+
" if goal:\n",
|
| 291 |
+
" if \"suggested_goals\" not in state:\n",
|
| 292 |
+
" state[\"suggested_goals\"] = []\n",
|
| 293 |
+
" state[\"suggested_goals\"].append(goal)\n",
|
| 294 |
+
" return self._update_displays(state)\n",
|
| 295 |
+
" return self._update_displays(state)\n",
|
| 296 |
+
"\n",
|
| 297 |
+
" def select_goal(self, evt: gr.SelectData, state: Dict[str, Any]) -> List:\n",
|
| 298 |
+
" \"\"\"Set selected goal as active.\"\"\"\n",
|
| 299 |
+
" if \"suggested_goals\" in state and evt.index[0] < len(state[\"suggested_goals\"]):\n",
|
| 300 |
+
" goal = state[\"suggested_goals\"][evt.index[0]]\n",
|
| 301 |
+
" self.tutor.learning_context.add_smart_goal(goal)\n",
|
| 302 |
+
" return self._update_displays(state)\n",
|
| 303 |
+
" return self._update_displays(state)\n",
|
| 304 |
+
"\n",
|
| 305 |
+
" def end_discussion(self, state: Dict[str, Any]) -> List:\n",
|
| 306 |
+
" \"\"\"End current discussion.\"\"\"\n",
|
| 307 |
+
" self.tutor.end_discussion()\n",
|
| 308 |
+
" state[\"discussion_active\"] = False\n",
|
| 309 |
+
" state[\"discussion_start\"] = None\n",
|
| 310 |
+
" state[\"last_message\"] = None\n",
|
| 311 |
+
" return self._update_displays(state)\n",
|
| 312 |
+
"\n",
|
| 313 |
+
" def _update_displays(self, state: Dict[str, Any]) -> List:\n",
|
| 314 |
+
" \"\"\"Update all display components.\"\"\"\n",
|
| 315 |
+
" context = self.tutor.learning_context\n",
|
| 316 |
+
" \n",
|
| 317 |
+
" # Update goals list\n",
|
| 318 |
+
" goals_data = []\n",
|
| 319 |
+
" for goal in state.get(\"suggested_goals\", []):\n",
|
| 320 |
+
" status = \"Active\" if (\n",
|
| 321 |
+
" context.active_goal and \n",
|
| 322 |
+
" context.active_goal.id == goal.id\n",
|
| 323 |
+
" ) else \"Available\"\n",
|
| 324 |
+
" \n",
|
| 325 |
+
" goals_data.append([\n",
|
| 326 |
+
" goal.smart_version,\n",
|
| 327 |
+
" goal.category.value,\n",
|
| 328 |
+
" status\n",
|
| 329 |
+
" ])\n",
|
| 330 |
+
" \n",
|
| 331 |
+
" # Update progress display\n",
|
| 332 |
+
" summary = context.get_category_summary()\n",
|
| 333 |
+
" progress_data = [\n",
|
| 334 |
+
" [cat, data[\"completed\"], data[\"total\"]]\n",
|
| 335 |
+
" for cat, data in summary.items()\n",
|
| 336 |
+
" ]\n",
|
| 337 |
+
" \n",
|
| 338 |
+
" # Update recent completions\n",
|
| 339 |
+
" recent_data = []\n",
|
| 340 |
+
" for cat, data in summary.items():\n",
|
| 341 |
+
" for goal in data[\"recent\"]:\n",
|
| 342 |
+
" recent_data.append([\n",
|
| 343 |
+
" goal[\"smart_version\"],\n",
|
| 344 |
+
" cat,\n",
|
| 345 |
+
" goal[\"completed_at\"]\n",
|
| 346 |
+
" ])\n",
|
| 347 |
+
" \n",
|
| 348 |
+
" # Update active goal display\n",
|
| 349 |
+
" goal_text = (\n",
|
| 350 |
+
" f\"Current Goal: {context.active_goal.smart_version}\"\n",
|
| 351 |
+
" if context.active_goal else\n",
|
| 352 |
+
" \"No active learning goal\"\n",
|
| 353 |
+
" )\n",
|
| 354 |
+
" \n",
|
| 355 |
+
" return [goals_data, progress_data, recent_data, goal_text]\n",
|
| 356 |
+
"\n",
|
| 357 |
" def create_interface(self) -> gr.Blocks:\n",
|
| 358 |
+
" \"\"\"Create streaming-enabled interface.\"\"\"\n",
|
| 359 |
" with gr.Blocks(title=\"Clinical Learning Assistant\", css=create_css()) as interface:\n",
|
| 360 |
" # State management\n",
|
| 361 |
" state = gr.State({\n",
|
|
|
|
| 364 |
" \"discussion_start\": None,\n",
|
| 365 |
" \"last_message\": None\n",
|
| 366 |
" })\n",
|
| 367 |
+
"\n",
|
| 368 |
" # Header\n",
|
| 369 |
" gr.Markdown(\"# Clinical Learning Assistant\")\n",
|
| 370 |
+
"\n",
|
| 371 |
" with gr.Row():\n",
|
| 372 |
" # Context selection\n",
|
| 373 |
" specialty = gr.Dropdown(\n",
|
|
|
|
| 380 |
" label=\"Setting\",\n",
|
| 381 |
" value=self.tutor.learning_context.rotation.setting or None\n",
|
| 382 |
" )\n",
|
| 383 |
+
"\n",
|
| 384 |
" # Main content\n",
|
| 385 |
" with gr.Row():\n",
|
| 386 |
" # Left: Discussion interface\n",
|
| 387 |
" with gr.Column(scale=2):\n",
|
| 388 |
" # Active goal display\n",
|
| 389 |
" goal_display = gr.Markdown(value=\"No active learning goal\")\n",
|
| 390 |
+
"\n",
|
| 391 |
" # Discussion status\n",
|
| 392 |
" discussion_status = gr.Markdown(\n",
|
| 393 |
" value=\"Start a new case discussion\",\n",
|
| 394 |
" elem_classes=[\"discussion-status\"]\n",
|
| 395 |
" )\n",
|
| 396 |
+
"\n",
|
| 397 |
" # Chat interface\n",
|
| 398 |
" chatbot = gr.Chatbot(\n",
|
| 399 |
" height=400,\n",
|
|
|
|
| 401 |
" type=\"messages\", # Use newer message format\n",
|
| 402 |
" elem_classes=[\"chat-window\"]\n",
|
| 403 |
" )\n",
|
| 404 |
+
"\n",
|
| 405 |
" with gr.Row():\n",
|
| 406 |
" msg = gr.Textbox(\n",
|
| 407 |
" label=\"Present your case or ask questions\",\n",
|
|
|
|
| 413 |
" ),\n",
|
| 414 |
" lines=4\n",
|
| 415 |
" )\n",
|
| 416 |
+
"\n",
|
| 417 |
" audio_input = gr.Audio(\n",
|
| 418 |
" sources=[\"microphone\"],\n",
|
| 419 |
" type=\"numpy\",\n",
|
| 420 |
" label=\"Or speak your case\",\n",
|
| 421 |
" streaming=True\n",
|
| 422 |
" )\n",
|
| 423 |
+
"\n",
|
| 424 |
" with gr.Row():\n",
|
| 425 |
" clear = gr.Button(\"Clear Discussion\")\n",
|
| 426 |
" end = gr.Button(\n",
|
| 427 |
" \"End Discussion & Review\",\n",
|
| 428 |
" variant=\"primary\"\n",
|
| 429 |
" )\n",
|
| 430 |
+
"\n",
|
| 431 |
" # Right: Progress & Goals\n",
|
| 432 |
" with gr.Column(scale=1):\n",
|
| 433 |
" with gr.Tab(\"Learning Goals\"):\n",
|
|
|
|
| 439 |
" placeholder=\"What do you want to get better at?\"\n",
|
| 440 |
" )\n",
|
| 441 |
" add_goal = gr.Button(\"Add Goal\")\n",
|
| 442 |
+
"\n",
|
| 443 |
" # Goals list\n",
|
| 444 |
" goals_list = gr.DataFrame(\n",
|
| 445 |
" headers=[\"Goal\", \"Category\", \"Status\"],\n",
|
|
|
|
| 448 |
" interactive=True,\n",
|
| 449 |
" wrap=True\n",
|
| 450 |
" )\n",
|
| 451 |
+
"\n",
|
| 452 |
" with gr.Tab(\"Progress\"):\n",
|
| 453 |
" # Category progress\n",
|
| 454 |
" progress_display = gr.DataFrame(\n",
|
|
|
|
| 456 |
" value=[],\n",
|
| 457 |
" label=\"Progress by Category\"\n",
|
| 458 |
" )\n",
|
| 459 |
+
"\n",
|
| 460 |
" # Recent completions\n",
|
| 461 |
" recent_display = gr.DataFrame(\n",
|
| 462 |
" headers=[\"Goal\", \"Category\", \"Completed\"],\n",
|
| 463 |
" value=[],\n",
|
| 464 |
" label=\"Recently Completed\"\n",
|
| 465 |
" )\n",
|
| 466 |
+
"\n",
|
| 467 |
" # Event handlers\n",
|
| 468 |
" async def update_context(spec: str, set: str, state: Dict) -> List:\n",
|
| 469 |
" \"\"\"Update rotation context.\"\"\"\n",
|
|
|
|
| 474 |
" state[\"suggested_goals\"] = goals\n",
|
| 475 |
" return self._update_displays(state)\n",
|
| 476 |
" return [[], [], [], \"No active learning goal\"]\n",
|
| 477 |
+
"\n",
|
| 478 |
" def process_audio(audio: np.ndarray) -> str:\n",
|
| 479 |
" \"\"\"Convert audio to text.\"\"\"\n",
|
| 480 |
" if audio is None:\n",
|
| 481 |
" return \"\"\n",
|
|
|
|
| 482 |
" return \"Audio transcription would appear here\"\n",
|
| 483 |
+
"\n",
|
| 484 |
" def clear_discussion() -> Tuple[List, str, Dict]:\n",
|
| 485 |
" \"\"\"Clear chat history.\"\"\"\n",
|
| 486 |
" return [], \"\", {\n",
|
|
|
|
| 489 |
" \"discussion_start\": None,\n",
|
| 490 |
" \"last_message\": None\n",
|
| 491 |
" }\n",
|
| 492 |
+
"\n",
|
| 493 |
" # Wire up events\n",
|
| 494 |
" specialty.change(\n",
|
| 495 |
" update_context,\n",
|
| 496 |
" inputs=[specialty, setting, state],\n",
|
| 497 |
" outputs=[goals_list, progress_display, recent_display, goal_display]\n",
|
| 498 |
" )\n",
|
| 499 |
+
"\n",
|
| 500 |
" setting.change(\n",
|
| 501 |
" update_context,\n",
|
| 502 |
" inputs=[specialty, setting, state],\n",
|
| 503 |
" outputs=[goals_list, progress_display, recent_display, goal_display]\n",
|
| 504 |
" )\n",
|
| 505 |
+
"\n",
|
| 506 |
" # Chat events with streaming\n",
|
| 507 |
" msg.submit(\n",
|
| 508 |
" self.process_chat,\n",
|
|
|
|
| 514 |
" inputs=[state],\n",
|
| 515 |
" outputs=[discussion_status]\n",
|
| 516 |
" )\n",
|
| 517 |
+
"\n",
|
| 518 |
" # Voice input handling\n",
|
| 519 |
" audio_input.stream(\n",
|
| 520 |
" process_audio,\n",
|
|
|
|
| 525 |
" inputs=[msg, chatbot, state],\n",
|
| 526 |
" outputs=[chatbot, msg, state]\n",
|
| 527 |
" )\n",
|
| 528 |
+
"\n",
|
| 529 |
" # Button handlers\n",
|
| 530 |
" clear.click(\n",
|
| 531 |
" clear_discussion,\n",
|
|
|
|
| 534 |
" lambda: \"Start a new case discussion\",\n",
|
| 535 |
" outputs=[discussion_status]\n",
|
| 536 |
" )\n",
|
| 537 |
+
"\n",
|
| 538 |
" end.click(\n",
|
| 539 |
" self.end_discussion,\n",
|
| 540 |
" inputs=[state],\n",
|
| 541 |
" outputs=[goals_list, progress_display, recent_display, goal_display]\n",
|
| 542 |
" )\n",
|
| 543 |
+
"\n",
|
| 544 |
" generate.click(\n",
|
| 545 |
" self.generate_goals,\n",
|
| 546 |
" inputs=[state],\n",
|
| 547 |
" outputs=[goals_list, progress_display, recent_display, goal_display]\n",
|
| 548 |
" )\n",
|
| 549 |
+
"\n",
|
| 550 |
" # Goal management\n",
|
| 551 |
" add_goal.click(\n",
|
| 552 |
" self.add_user_goal,\n",
|
| 553 |
" inputs=[new_goal, state],\n",
|
| 554 |
" outputs=[goals_list, progress_display, recent_display, goal_display]\n",
|
| 555 |
" )\n",
|
| 556 |
+
"\n",
|
| 557 |
" goals_list.select(\n",
|
| 558 |
" self.select_goal,\n",
|
| 559 |
" inputs=[state],\n",
|
| 560 |
" outputs=[goals_list, progress_display, recent_display, goal_display]\n",
|
| 561 |
" )\n",
|
| 562 |
+
"\n",
|
| 563 |
+
" return interface"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 564 |
]
|
| 565 |
},
|
| 566 |
{
|
wardbuddy/_modidx.py
CHANGED
|
@@ -64,10 +64,18 @@ d = { 'settings': { 'branch': 'main',
|
|
| 64 |
'wardbuddy/learning_interface.py'),
|
| 65 |
'wardbuddy.learning_interface.LearningInterface._update_displays': ( 'learning_interface.html#learninginterface._update_displays',
|
| 66 |
'wardbuddy/learning_interface.py'),
|
|
|
|
|
|
|
| 67 |
'wardbuddy.learning_interface.LearningInterface.create_interface': ( 'learning_interface.html#learninginterface.create_interface',
|
| 68 |
'wardbuddy/learning_interface.py'),
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
'wardbuddy.learning_interface.LearningInterface.process_chat': ( 'learning_interface.html#learninginterface.process_chat',
|
| 70 |
'wardbuddy/learning_interface.py'),
|
|
|
|
|
|
|
| 71 |
'wardbuddy.learning_interface.create_css': ( 'learning_interface.html#create_css',
|
| 72 |
'wardbuddy/learning_interface.py')},
|
| 73 |
'wardbuddy.main': { 'wardbuddy.main.launch': ('main.html#launch', 'wardbuddy/main.py'),
|
|
|
|
| 64 |
'wardbuddy/learning_interface.py'),
|
| 65 |
'wardbuddy.learning_interface.LearningInterface._update_displays': ( 'learning_interface.html#learninginterface._update_displays',
|
| 66 |
'wardbuddy/learning_interface.py'),
|
| 67 |
+
'wardbuddy.learning_interface.LearningInterface.add_user_goal': ( 'learning_interface.html#learninginterface.add_user_goal',
|
| 68 |
+
'wardbuddy/learning_interface.py'),
|
| 69 |
'wardbuddy.learning_interface.LearningInterface.create_interface': ( 'learning_interface.html#learninginterface.create_interface',
|
| 70 |
'wardbuddy/learning_interface.py'),
|
| 71 |
+
'wardbuddy.learning_interface.LearningInterface.end_discussion': ( 'learning_interface.html#learninginterface.end_discussion',
|
| 72 |
+
'wardbuddy/learning_interface.py'),
|
| 73 |
+
'wardbuddy.learning_interface.LearningInterface.generate_goals': ( 'learning_interface.html#learninginterface.generate_goals',
|
| 74 |
+
'wardbuddy/learning_interface.py'),
|
| 75 |
'wardbuddy.learning_interface.LearningInterface.process_chat': ( 'learning_interface.html#learninginterface.process_chat',
|
| 76 |
'wardbuddy/learning_interface.py'),
|
| 77 |
+
'wardbuddy.learning_interface.LearningInterface.select_goal': ( 'learning_interface.html#learninginterface.select_goal',
|
| 78 |
+
'wardbuddy/learning_interface.py'),
|
| 79 |
'wardbuddy.learning_interface.create_css': ( 'learning_interface.html#create_css',
|
| 80 |
'wardbuddy/learning_interface.py')},
|
| 81 |
'wardbuddy.main': { 'wardbuddy.main.launch': ('main.html#launch', 'wardbuddy/main.py'),
|
wardbuddy/learning_interface.py
CHANGED
|
@@ -89,7 +89,7 @@ class LearningInterface:
|
|
| 89 |
Features:
|
| 90 |
- Context selection
|
| 91 |
- Goal management
|
| 92 |
-
- Case discussion
|
| 93 |
- Progress tracking
|
| 94 |
"""
|
| 95 |
|
|
@@ -98,13 +98,7 @@ class LearningInterface:
|
|
| 98 |
context_path: Optional[Path] = None,
|
| 99 |
model: str = "anthropic/claude-3.5-sonnet"
|
| 100 |
):
|
| 101 |
-
"""
|
| 102 |
-
Initialize interface.
|
| 103 |
-
|
| 104 |
-
Args:
|
| 105 |
-
context_path: Path for context persistence
|
| 106 |
-
model: OpenRouter model identifier
|
| 107 |
-
"""
|
| 108 |
self.tutor = ClinicalTutor(context_path, model)
|
| 109 |
|
| 110 |
# Available options
|
|
@@ -119,7 +113,7 @@ class LearningInterface:
|
|
| 119 |
self.settings = ["Clinic", "Wards", "ED"]
|
| 120 |
|
| 121 |
logger.info("Learning interface initialized")
|
| 122 |
-
|
| 123 |
async def process_chat(
|
| 124 |
self,
|
| 125 |
message: str,
|
|
@@ -163,9 +157,116 @@ class LearningInterface:
|
|
| 163 |
history = []
|
| 164 |
history.append([message, "I apologize, but I encountered an error. Please try again."])
|
| 165 |
yield history, "", state
|
| 166 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
def create_interface(self) -> gr.Blocks:
|
| 168 |
-
"""Create
|
| 169 |
with gr.Blocks(title="Clinical Learning Assistant", css=create_css()) as interface:
|
| 170 |
# State management
|
| 171 |
state = gr.State({
|
|
@@ -174,10 +275,10 @@ class LearningInterface:
|
|
| 174 |
"discussion_start": None,
|
| 175 |
"last_message": None
|
| 176 |
})
|
| 177 |
-
|
| 178 |
# Header
|
| 179 |
gr.Markdown("# Clinical Learning Assistant")
|
| 180 |
-
|
| 181 |
with gr.Row():
|
| 182 |
# Context selection
|
| 183 |
specialty = gr.Dropdown(
|
|
@@ -190,20 +291,20 @@ class LearningInterface:
|
|
| 190 |
label="Setting",
|
| 191 |
value=self.tutor.learning_context.rotation.setting or None
|
| 192 |
)
|
| 193 |
-
|
| 194 |
# Main content
|
| 195 |
with gr.Row():
|
| 196 |
# Left: Discussion interface
|
| 197 |
with gr.Column(scale=2):
|
| 198 |
# Active goal display
|
| 199 |
goal_display = gr.Markdown(value="No active learning goal")
|
| 200 |
-
|
| 201 |
# Discussion status
|
| 202 |
discussion_status = gr.Markdown(
|
| 203 |
value="Start a new case discussion",
|
| 204 |
elem_classes=["discussion-status"]
|
| 205 |
)
|
| 206 |
-
|
| 207 |
# Chat interface
|
| 208 |
chatbot = gr.Chatbot(
|
| 209 |
height=400,
|
|
@@ -211,7 +312,7 @@ class LearningInterface:
|
|
| 211 |
type="messages", # Use newer message format
|
| 212 |
elem_classes=["chat-window"]
|
| 213 |
)
|
| 214 |
-
|
| 215 |
with gr.Row():
|
| 216 |
msg = gr.Textbox(
|
| 217 |
label="Present your case or ask questions",
|
|
@@ -223,21 +324,21 @@ class LearningInterface:
|
|
| 223 |
),
|
| 224 |
lines=4
|
| 225 |
)
|
| 226 |
-
|
| 227 |
audio_input = gr.Audio(
|
| 228 |
sources=["microphone"],
|
| 229 |
type="numpy",
|
| 230 |
label="Or speak your case",
|
| 231 |
streaming=True
|
| 232 |
)
|
| 233 |
-
|
| 234 |
with gr.Row():
|
| 235 |
clear = gr.Button("Clear Discussion")
|
| 236 |
end = gr.Button(
|
| 237 |
"End Discussion & Review",
|
| 238 |
variant="primary"
|
| 239 |
)
|
| 240 |
-
|
| 241 |
# Right: Progress & Goals
|
| 242 |
with gr.Column(scale=1):
|
| 243 |
with gr.Tab("Learning Goals"):
|
|
@@ -249,7 +350,7 @@ class LearningInterface:
|
|
| 249 |
placeholder="What do you want to get better at?"
|
| 250 |
)
|
| 251 |
add_goal = gr.Button("Add Goal")
|
| 252 |
-
|
| 253 |
# Goals list
|
| 254 |
goals_list = gr.DataFrame(
|
| 255 |
headers=["Goal", "Category", "Status"],
|
|
@@ -258,7 +359,7 @@ class LearningInterface:
|
|
| 258 |
interactive=True,
|
| 259 |
wrap=True
|
| 260 |
)
|
| 261 |
-
|
| 262 |
with gr.Tab("Progress"):
|
| 263 |
# Category progress
|
| 264 |
progress_display = gr.DataFrame(
|
|
@@ -266,14 +367,14 @@ class LearningInterface:
|
|
| 266 |
value=[],
|
| 267 |
label="Progress by Category"
|
| 268 |
)
|
| 269 |
-
|
| 270 |
# Recent completions
|
| 271 |
recent_display = gr.DataFrame(
|
| 272 |
headers=["Goal", "Category", "Completed"],
|
| 273 |
value=[],
|
| 274 |
label="Recently Completed"
|
| 275 |
)
|
| 276 |
-
|
| 277 |
# Event handlers
|
| 278 |
async def update_context(spec: str, set: str, state: Dict) -> List:
|
| 279 |
"""Update rotation context."""
|
|
@@ -284,14 +385,13 @@ class LearningInterface:
|
|
| 284 |
state["suggested_goals"] = goals
|
| 285 |
return self._update_displays(state)
|
| 286 |
return [[], [], [], "No active learning goal"]
|
| 287 |
-
|
| 288 |
def process_audio(audio: np.ndarray) -> str:
|
| 289 |
"""Convert audio to text."""
|
| 290 |
if audio is None:
|
| 291 |
return ""
|
| 292 |
-
# Would integrate with speech-to-text here
|
| 293 |
return "Audio transcription would appear here"
|
| 294 |
-
|
| 295 |
def clear_discussion() -> Tuple[List, str, Dict]:
|
| 296 |
"""Clear chat history."""
|
| 297 |
return [], "", {
|
|
@@ -300,20 +400,20 @@ class LearningInterface:
|
|
| 300 |
"discussion_start": None,
|
| 301 |
"last_message": None
|
| 302 |
}
|
| 303 |
-
|
| 304 |
# Wire up events
|
| 305 |
specialty.change(
|
| 306 |
update_context,
|
| 307 |
inputs=[specialty, setting, state],
|
| 308 |
outputs=[goals_list, progress_display, recent_display, goal_display]
|
| 309 |
)
|
| 310 |
-
|
| 311 |
setting.change(
|
| 312 |
update_context,
|
| 313 |
inputs=[specialty, setting, state],
|
| 314 |
outputs=[goals_list, progress_display, recent_display, goal_display]
|
| 315 |
)
|
| 316 |
-
|
| 317 |
# Chat events with streaming
|
| 318 |
msg.submit(
|
| 319 |
self.process_chat,
|
|
@@ -325,7 +425,7 @@ class LearningInterface:
|
|
| 325 |
inputs=[state],
|
| 326 |
outputs=[discussion_status]
|
| 327 |
)
|
| 328 |
-
|
| 329 |
# Voice input handling
|
| 330 |
audio_input.stream(
|
| 331 |
process_audio,
|
|
@@ -336,7 +436,7 @@ class LearningInterface:
|
|
| 336 |
inputs=[msg, chatbot, state],
|
| 337 |
outputs=[chatbot, msg, state]
|
| 338 |
)
|
| 339 |
-
|
| 340 |
# Button handlers
|
| 341 |
clear.click(
|
| 342 |
clear_discussion,
|
|
@@ -345,330 +445,30 @@ class LearningInterface:
|
|
| 345 |
lambda: "Start a new case discussion",
|
| 346 |
outputs=[discussion_status]
|
| 347 |
)
|
| 348 |
-
|
| 349 |
end.click(
|
| 350 |
self.end_discussion,
|
| 351 |
inputs=[state],
|
| 352 |
outputs=[goals_list, progress_display, recent_display, goal_display]
|
| 353 |
)
|
| 354 |
-
|
| 355 |
generate.click(
|
| 356 |
self.generate_goals,
|
| 357 |
inputs=[state],
|
| 358 |
outputs=[goals_list, progress_display, recent_display, goal_display]
|
| 359 |
)
|
| 360 |
-
|
| 361 |
# Goal management
|
| 362 |
add_goal.click(
|
| 363 |
self.add_user_goal,
|
| 364 |
inputs=[new_goal, state],
|
| 365 |
outputs=[goals_list, progress_display, recent_display, goal_display]
|
| 366 |
)
|
| 367 |
-
|
| 368 |
goals_list.select(
|
| 369 |
self.select_goal,
|
| 370 |
inputs=[state],
|
| 371 |
outputs=[goals_list, progress_display, recent_display, goal_display]
|
| 372 |
)
|
| 373 |
-
|
| 374 |
-
return interface
|
| 375 |
-
|
| 376 |
-
# Event handlers
|
| 377 |
-
async def update_context(self, spec: str, set: str, state: Dict) -> List:
|
| 378 |
-
"""Update rotation context.
|
| 379 |
-
|
| 380 |
-
Args:
|
| 381 |
-
spec: Specialty
|
| 382 |
-
set: Setting
|
| 383 |
-
state: Interface state
|
| 384 |
-
|
| 385 |
-
Returns:
|
| 386 |
-
list: Updated display data
|
| 387 |
-
"""
|
| 388 |
-
try:
|
| 389 |
-
if spec and set:
|
| 390 |
-
self.tutor.learning_context.update_rotation(spec, set)
|
| 391 |
-
|
| 392 |
-
# Initialize goals list if needed
|
| 393 |
-
if not state.get("suggested_goals"):
|
| 394 |
-
goals = await self.tutor.generate_smart_goals(spec, set)
|
| 395 |
-
state["suggested_goals"] = goals
|
| 396 |
-
|
| 397 |
-
# Return all display updates
|
| 398 |
-
goals_data = []
|
| 399 |
-
context = self.tutor.learning_context
|
| 400 |
-
|
| 401 |
-
# Update goals list
|
| 402 |
-
for goal in state.get("suggested_goals", []):
|
| 403 |
-
status = "Active" if (
|
| 404 |
-
context.active_goal and
|
| 405 |
-
context.active_goal.id == goal.id
|
| 406 |
-
) else "Available"
|
| 407 |
-
|
| 408 |
-
goals_data.append([
|
| 409 |
-
goal.smart_version,
|
| 410 |
-
goal.category.value,
|
| 411 |
-
status
|
| 412 |
-
])
|
| 413 |
-
|
| 414 |
-
# Update progress display
|
| 415 |
-
summary = context.get_category_summary()
|
| 416 |
-
progress_data = [
|
| 417 |
-
[cat, data["completed"], data["total"]]
|
| 418 |
-
for cat, data in summary.items()
|
| 419 |
-
]
|
| 420 |
-
|
| 421 |
-
# Update recent completions
|
| 422 |
-
recent_data = []
|
| 423 |
-
for cat, data in summary.items():
|
| 424 |
-
for goal in data["recent"]:
|
| 425 |
-
recent_data.append([
|
| 426 |
-
goal["smart_version"],
|
| 427 |
-
cat,
|
| 428 |
-
goal["completed_at"]
|
| 429 |
-
])
|
| 430 |
-
|
| 431 |
-
# Update active goal display
|
| 432 |
-
goal_text = (
|
| 433 |
-
f"Current Goal: {context.active_goal.smart_version}"
|
| 434 |
-
if context.active_goal else
|
| 435 |
-
"No active learning goal"
|
| 436 |
-
)
|
| 437 |
-
|
| 438 |
-
return [
|
| 439 |
-
goals_data,
|
| 440 |
-
progress_data,
|
| 441 |
-
recent_data,
|
| 442 |
-
goal_text
|
| 443 |
-
]
|
| 444 |
-
|
| 445 |
-
# Return empty displays if no context
|
| 446 |
-
return [[], [], [], "No active learning goal"]
|
| 447 |
-
|
| 448 |
-
except Exception as e:
|
| 449 |
-
logger.error(f"Error updating context: {str(e)}")
|
| 450 |
-
return [[], [], [], "Error updating context"]
|
| 451 |
-
|
| 452 |
-
async def process_message(msg, history):
|
| 453 |
-
"""Process chat message."""
|
| 454 |
-
if not msg.strip():
|
| 455 |
-
return history, ""
|
| 456 |
-
|
| 457 |
-
response = await self.tutor.discuss_case(msg)
|
| 458 |
-
history.append([msg, response])
|
| 459 |
-
return history, ""
|
| 460 |
-
|
| 461 |
-
def process_audio(audio):
|
| 462 |
-
"""Convert audio to text."""
|
| 463 |
-
# Would integrate with speech-to-text here
|
| 464 |
-
return "Audio transcription would appear here"
|
| 465 |
-
|
| 466 |
-
async def generate_goals(self, state: Dict[str, Any]) -> List:
|
| 467 |
-
"""Generate new goal suggestions.
|
| 468 |
-
|
| 469 |
-
Args:
|
| 470 |
-
state: Interface state
|
| 471 |
-
|
| 472 |
-
Returns:
|
| 473 |
-
list: Updated display data
|
| 474 |
-
"""
|
| 475 |
-
rotation = self.tutor.learning_context.rotation
|
| 476 |
-
if rotation.specialty and rotation.setting:
|
| 477 |
-
goals = await self.tutor.generate_smart_goals(
|
| 478 |
-
rotation.specialty,
|
| 479 |
-
rotation.setting
|
| 480 |
-
)
|
| 481 |
-
state["suggested_goals"] = goals
|
| 482 |
-
return self._update_displays(state)
|
| 483 |
-
return [[], [], [], "No active learning goal"]
|
| 484 |
-
|
| 485 |
-
async def add_user_goal(self, text: str, state: Dict[str, Any]) -> List:
|
| 486 |
-
"""Add user-specified goal.
|
| 487 |
-
|
| 488 |
-
Args:
|
| 489 |
-
text: User's goal text
|
| 490 |
-
state: Interface state
|
| 491 |
-
|
| 492 |
-
Returns:
|
| 493 |
-
list: Updated display data
|
| 494 |
-
"""
|
| 495 |
-
if not text.strip():
|
| 496 |
-
return self._update_displays(state)
|
| 497 |
-
|
| 498 |
-
rotation = self.tutor.learning_context.rotation
|
| 499 |
-
if rotation.specialty and rotation.setting:
|
| 500 |
-
goal = await self.tutor.generate_smart_goal(
|
| 501 |
-
text,
|
| 502 |
-
rotation.specialty,
|
| 503 |
-
rotation.setting
|
| 504 |
-
)
|
| 505 |
-
if goal:
|
| 506 |
-
state["suggested_goals"].append(goal)
|
| 507 |
-
return self._update_displays(state)
|
| 508 |
-
return self._update_displays(state)
|
| 509 |
-
|
| 510 |
-
def select_goal(self, evt: gr.SelectData, state: Dict[str, Any]) -> List:
|
| 511 |
-
"""Set selected goal as active.
|
| 512 |
-
|
| 513 |
-
Args:
|
| 514 |
-
evt: Selection event data
|
| 515 |
-
state: Interface state
|
| 516 |
-
|
| 517 |
-
Returns:
|
| 518 |
-
list: Updated display data
|
| 519 |
-
"""
|
| 520 |
-
if evt.index[0] < len(state["suggested_goals"]):
|
| 521 |
-
goal = state["suggested_goals"][evt.index[0]]
|
| 522 |
-
self.tutor.learning_context.add_smart_goal(goal)
|
| 523 |
-
return self._update_displays(state)
|
| 524 |
-
return self._update_displays(state)
|
| 525 |
-
|
| 526 |
-
def end_discussion(self, state: Dict[str, Any]) -> List:
|
| 527 |
-
"""End current discussion.
|
| 528 |
-
|
| 529 |
-
Args:
|
| 530 |
-
state: Interface state
|
| 531 |
-
|
| 532 |
-
Returns:
|
| 533 |
-
list: Updated display data
|
| 534 |
-
"""
|
| 535 |
-
if self.tutor.learning_context.active_goal:
|
| 536 |
-
self.tutor.learning_context.complete_active_goal()
|
| 537 |
-
|
| 538 |
-
self.tutor.end_discussion()
|
| 539 |
-
state["discussion_active"] = False
|
| 540 |
-
state["discussion_start"] = None
|
| 541 |
-
|
| 542 |
-
return self._update_displays(state)
|
| 543 |
-
|
| 544 |
-
def clear_discussion():
|
| 545 |
-
"""Clear discussion history."""
|
| 546 |
-
self.tutor.clear_discussion()
|
| 547 |
-
return [], ""
|
| 548 |
-
|
| 549 |
-
# Wire up events
|
| 550 |
-
specialty.change(
|
| 551 |
-
update_context,
|
| 552 |
-
inputs=[specialty, setting, state], # Add state to inputs
|
| 553 |
-
outputs=[goals_list, progress_display, recent_display, goal_display]
|
| 554 |
-
)
|
| 555 |
-
|
| 556 |
-
setting.change(
|
| 557 |
-
update_context,
|
| 558 |
-
inputs=[specialty, setting, state], # Add state to inputs
|
| 559 |
-
outputs=[goals_list, progress_display, recent_display, goal_display]
|
| 560 |
-
)
|
| 561 |
-
|
| 562 |
-
message.submit(
|
| 563 |
-
process_message,
|
| 564 |
-
inputs=[message, chatbot],
|
| 565 |
-
outputs=[chatbot, message]
|
| 566 |
-
)
|
| 567 |
-
|
| 568 |
-
audio_input.stream(
|
| 569 |
-
process_audio,
|
| 570 |
-
inputs=[audio_input],
|
| 571 |
-
outputs=[message]
|
| 572 |
-
)
|
| 573 |
-
|
| 574 |
-
clear.click(
|
| 575 |
-
clear_discussion,
|
| 576 |
-
outputs=[chatbot, message]
|
| 577 |
-
)
|
| 578 |
-
|
| 579 |
-
end.click(
|
| 580 |
-
end_discussion,
|
| 581 |
-
inputs=[state],
|
| 582 |
-
outputs=[goals_list, progress_display, recent_display, goal_display]
|
| 583 |
-
)
|
| 584 |
-
|
| 585 |
-
generate.click(
|
| 586 |
-
generate_goals,
|
| 587 |
-
inputs=[state],
|
| 588 |
-
outputs=[goals_list, progress_display, recent_display, goal_display]
|
| 589 |
-
)
|
| 590 |
-
|
| 591 |
-
new_goal.submit(
|
| 592 |
-
add_user_goal,
|
| 593 |
-
inputs=[new_goal, state],
|
| 594 |
-
outputs=[goals_list, progress_display, recent_display, goal_display]
|
| 595 |
-
)
|
| 596 |
-
|
| 597 |
-
goals_list.select(
|
| 598 |
-
select_goal,
|
| 599 |
-
inputs=[state],
|
| 600 |
-
outputs=[goals_list, progress_display, recent_display, goal_display]
|
| 601 |
-
)
|
| 602 |
-
|
| 603 |
return interface
|
| 604 |
-
|
| 605 |
-
def _update_displays(self, state: Dict) -> List:
|
| 606 |
-
"""Update all display components."""
|
| 607 |
-
context = self.tutor.learning_context
|
| 608 |
-
|
| 609 |
-
# Update goals list
|
| 610 |
-
goals_data = []
|
| 611 |
-
for goal in state["suggested_goals"]:
|
| 612 |
-
status = "Active" if (
|
| 613 |
-
context.active_goal and
|
| 614 |
-
context.active_goal.id == goal.id
|
| 615 |
-
) else "Available"
|
| 616 |
-
|
| 617 |
-
goals_data.append([
|
| 618 |
-
goal.smart_version,
|
| 619 |
-
goal.category.value,
|
| 620 |
-
status
|
| 621 |
-
])
|
| 622 |
-
|
| 623 |
-
# Update progress display
|
| 624 |
-
summary = context.get_category_summary()
|
| 625 |
-
progress_data = [
|
| 626 |
-
[cat, data["completed"], data["total"]]
|
| 627 |
-
for cat, data in summary.items()
|
| 628 |
-
]
|
| 629 |
-
|
| 630 |
-
# Update recent completions
|
| 631 |
-
recent_data = []
|
| 632 |
-
for cat, data in summary.items():
|
| 633 |
-
for goal in data["recent"]:
|
| 634 |
-
recent_data.append([
|
| 635 |
-
goal["smart_version"],
|
| 636 |
-
cat,
|
| 637 |
-
goal["completed_at"]
|
| 638 |
-
])
|
| 639 |
-
|
| 640 |
-
# Update active goal display
|
| 641 |
-
goal_text = (
|
| 642 |
-
f"Current Goal: {context.active_goal.smart_version}"
|
| 643 |
-
if context.active_goal else
|
| 644 |
-
"No active learning goal"
|
| 645 |
-
)
|
| 646 |
-
|
| 647 |
-
return [
|
| 648 |
-
goals_data,
|
| 649 |
-
progress_data,
|
| 650 |
-
recent_data,
|
| 651 |
-
goal_text
|
| 652 |
-
]
|
| 653 |
-
def _update_discussion_status(self, state: Dict[str, Any]) -> str:
|
| 654 |
-
"""Update discussion status display based on state.
|
| 655 |
-
|
| 656 |
-
Args:
|
| 657 |
-
state: Current interface state
|
| 658 |
-
|
| 659 |
-
Returns:
|
| 660 |
-
str: Status message to display
|
| 661 |
-
"""
|
| 662 |
-
try:
|
| 663 |
-
if not state.get("discussion_active"):
|
| 664 |
-
return "Start a new case discussion"
|
| 665 |
-
|
| 666 |
-
start = datetime.fromisoformat(state["discussion_start"])
|
| 667 |
-
duration = datetime.now() - start
|
| 668 |
-
minutes = int(duration.total_seconds() / 60)
|
| 669 |
-
|
| 670 |
-
return f"Active discussion ({minutes} minutes)"
|
| 671 |
-
|
| 672 |
-
except Exception as e:
|
| 673 |
-
logger.error(f"Error updating status: {str(e)}")
|
| 674 |
-
return "Discussion status unknown"
|
|
|
|
| 89 |
Features:
|
| 90 |
- Context selection
|
| 91 |
- Goal management
|
| 92 |
+
- Case discussion with streaming
|
| 93 |
- Progress tracking
|
| 94 |
"""
|
| 95 |
|
|
|
|
| 98 |
context_path: Optional[Path] = None,
|
| 99 |
model: str = "anthropic/claude-3.5-sonnet"
|
| 100 |
):
|
| 101 |
+
"""Initialize interface."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
self.tutor = ClinicalTutor(context_path, model)
|
| 103 |
|
| 104 |
# Available options
|
|
|
|
| 113 |
self.settings = ["Clinic", "Wards", "ED"]
|
| 114 |
|
| 115 |
logger.info("Learning interface initialized")
|
| 116 |
+
|
| 117 |
async def process_chat(
|
| 118 |
self,
|
| 119 |
message: str,
|
|
|
|
| 157 |
history = []
|
| 158 |
history.append([message, "I apologize, but I encountered an error. Please try again."])
|
| 159 |
yield history, "", state
|
| 160 |
+
|
| 161 |
+
def _update_discussion_status(self, state: Dict[str, Any]) -> str:
|
| 162 |
+
"""Update discussion status display."""
|
| 163 |
+
try:
|
| 164 |
+
if not state.get("discussion_active"):
|
| 165 |
+
return "Start a new case discussion"
|
| 166 |
+
|
| 167 |
+
start = datetime.fromisoformat(state["discussion_start"])
|
| 168 |
+
duration = datetime.now() - start
|
| 169 |
+
minutes = int(duration.total_seconds() / 60)
|
| 170 |
+
|
| 171 |
+
return f"Active discussion ({minutes} minutes)"
|
| 172 |
+
|
| 173 |
+
except Exception as e:
|
| 174 |
+
logger.error(f"Error updating status: {str(e)}")
|
| 175 |
+
return "Discussion status unknown"
|
| 176 |
+
|
| 177 |
+
async def generate_goals(self, state: Dict[str, Any]) -> List:
|
| 178 |
+
"""Generate new goal suggestions."""
|
| 179 |
+
rotation = self.tutor.learning_context.rotation
|
| 180 |
+
if rotation.specialty and rotation.setting:
|
| 181 |
+
goals = await self.tutor.generate_smart_goals(
|
| 182 |
+
rotation.specialty,
|
| 183 |
+
rotation.setting
|
| 184 |
+
)
|
| 185 |
+
state["suggested_goals"] = goals
|
| 186 |
+
return self._update_displays(state)
|
| 187 |
+
return [[], [], [], "No active learning goal"]
|
| 188 |
+
|
| 189 |
+
async def add_user_goal(self, text: str, state: Dict[str, Any]) -> List:
|
| 190 |
+
"""Add user-specified goal."""
|
| 191 |
+
if not text.strip():
|
| 192 |
+
return self._update_displays(state)
|
| 193 |
+
|
| 194 |
+
rotation = self.tutor.learning_context.rotation
|
| 195 |
+
if rotation.specialty and rotation.setting:
|
| 196 |
+
goal = await self.tutor.generate_smart_goal(
|
| 197 |
+
text,
|
| 198 |
+
rotation.specialty,
|
| 199 |
+
rotation.setting
|
| 200 |
+
)
|
| 201 |
+
if goal:
|
| 202 |
+
if "suggested_goals" not in state:
|
| 203 |
+
state["suggested_goals"] = []
|
| 204 |
+
state["suggested_goals"].append(goal)
|
| 205 |
+
return self._update_displays(state)
|
| 206 |
+
return self._update_displays(state)
|
| 207 |
+
|
| 208 |
+
def select_goal(self, evt: gr.SelectData, state: Dict[str, Any]) -> List:
|
| 209 |
+
"""Set selected goal as active."""
|
| 210 |
+
if "suggested_goals" in state and evt.index[0] < len(state["suggested_goals"]):
|
| 211 |
+
goal = state["suggested_goals"][evt.index[0]]
|
| 212 |
+
self.tutor.learning_context.add_smart_goal(goal)
|
| 213 |
+
return self._update_displays(state)
|
| 214 |
+
return self._update_displays(state)
|
| 215 |
+
|
| 216 |
+
def end_discussion(self, state: Dict[str, Any]) -> List:
|
| 217 |
+
"""End current discussion."""
|
| 218 |
+
self.tutor.end_discussion()
|
| 219 |
+
state["discussion_active"] = False
|
| 220 |
+
state["discussion_start"] = None
|
| 221 |
+
state["last_message"] = None
|
| 222 |
+
return self._update_displays(state)
|
| 223 |
+
|
| 224 |
+
def _update_displays(self, state: Dict[str, Any]) -> List:
|
| 225 |
+
"""Update all display components."""
|
| 226 |
+
context = self.tutor.learning_context
|
| 227 |
+
|
| 228 |
+
# Update goals list
|
| 229 |
+
goals_data = []
|
| 230 |
+
for goal in state.get("suggested_goals", []):
|
| 231 |
+
status = "Active" if (
|
| 232 |
+
context.active_goal and
|
| 233 |
+
context.active_goal.id == goal.id
|
| 234 |
+
) else "Available"
|
| 235 |
+
|
| 236 |
+
goals_data.append([
|
| 237 |
+
goal.smart_version,
|
| 238 |
+
goal.category.value,
|
| 239 |
+
status
|
| 240 |
+
])
|
| 241 |
+
|
| 242 |
+
# Update progress display
|
| 243 |
+
summary = context.get_category_summary()
|
| 244 |
+
progress_data = [
|
| 245 |
+
[cat, data["completed"], data["total"]]
|
| 246 |
+
for cat, data in summary.items()
|
| 247 |
+
]
|
| 248 |
+
|
| 249 |
+
# Update recent completions
|
| 250 |
+
recent_data = []
|
| 251 |
+
for cat, data in summary.items():
|
| 252 |
+
for goal in data["recent"]:
|
| 253 |
+
recent_data.append([
|
| 254 |
+
goal["smart_version"],
|
| 255 |
+
cat,
|
| 256 |
+
goal["completed_at"]
|
| 257 |
+
])
|
| 258 |
+
|
| 259 |
+
# Update active goal display
|
| 260 |
+
goal_text = (
|
| 261 |
+
f"Current Goal: {context.active_goal.smart_version}"
|
| 262 |
+
if context.active_goal else
|
| 263 |
+
"No active learning goal"
|
| 264 |
+
)
|
| 265 |
+
|
| 266 |
+
return [goals_data, progress_data, recent_data, goal_text]
|
| 267 |
+
|
| 268 |
def create_interface(self) -> gr.Blocks:
|
| 269 |
+
"""Create streaming-enabled interface."""
|
| 270 |
with gr.Blocks(title="Clinical Learning Assistant", css=create_css()) as interface:
|
| 271 |
# State management
|
| 272 |
state = gr.State({
|
|
|
|
| 275 |
"discussion_start": None,
|
| 276 |
"last_message": None
|
| 277 |
})
|
| 278 |
+
|
| 279 |
# Header
|
| 280 |
gr.Markdown("# Clinical Learning Assistant")
|
| 281 |
+
|
| 282 |
with gr.Row():
|
| 283 |
# Context selection
|
| 284 |
specialty = gr.Dropdown(
|
|
|
|
| 291 |
label="Setting",
|
| 292 |
value=self.tutor.learning_context.rotation.setting or None
|
| 293 |
)
|
| 294 |
+
|
| 295 |
# Main content
|
| 296 |
with gr.Row():
|
| 297 |
# Left: Discussion interface
|
| 298 |
with gr.Column(scale=2):
|
| 299 |
# Active goal display
|
| 300 |
goal_display = gr.Markdown(value="No active learning goal")
|
| 301 |
+
|
| 302 |
# Discussion status
|
| 303 |
discussion_status = gr.Markdown(
|
| 304 |
value="Start a new case discussion",
|
| 305 |
elem_classes=["discussion-status"]
|
| 306 |
)
|
| 307 |
+
|
| 308 |
# Chat interface
|
| 309 |
chatbot = gr.Chatbot(
|
| 310 |
height=400,
|
|
|
|
| 312 |
type="messages", # Use newer message format
|
| 313 |
elem_classes=["chat-window"]
|
| 314 |
)
|
| 315 |
+
|
| 316 |
with gr.Row():
|
| 317 |
msg = gr.Textbox(
|
| 318 |
label="Present your case or ask questions",
|
|
|
|
| 324 |
),
|
| 325 |
lines=4
|
| 326 |
)
|
| 327 |
+
|
| 328 |
audio_input = gr.Audio(
|
| 329 |
sources=["microphone"],
|
| 330 |
type="numpy",
|
| 331 |
label="Or speak your case",
|
| 332 |
streaming=True
|
| 333 |
)
|
| 334 |
+
|
| 335 |
with gr.Row():
|
| 336 |
clear = gr.Button("Clear Discussion")
|
| 337 |
end = gr.Button(
|
| 338 |
"End Discussion & Review",
|
| 339 |
variant="primary"
|
| 340 |
)
|
| 341 |
+
|
| 342 |
# Right: Progress & Goals
|
| 343 |
with gr.Column(scale=1):
|
| 344 |
with gr.Tab("Learning Goals"):
|
|
|
|
| 350 |
placeholder="What do you want to get better at?"
|
| 351 |
)
|
| 352 |
add_goal = gr.Button("Add Goal")
|
| 353 |
+
|
| 354 |
# Goals list
|
| 355 |
goals_list = gr.DataFrame(
|
| 356 |
headers=["Goal", "Category", "Status"],
|
|
|
|
| 359 |
interactive=True,
|
| 360 |
wrap=True
|
| 361 |
)
|
| 362 |
+
|
| 363 |
with gr.Tab("Progress"):
|
| 364 |
# Category progress
|
| 365 |
progress_display = gr.DataFrame(
|
|
|
|
| 367 |
value=[],
|
| 368 |
label="Progress by Category"
|
| 369 |
)
|
| 370 |
+
|
| 371 |
# Recent completions
|
| 372 |
recent_display = gr.DataFrame(
|
| 373 |
headers=["Goal", "Category", "Completed"],
|
| 374 |
value=[],
|
| 375 |
label="Recently Completed"
|
| 376 |
)
|
| 377 |
+
|
| 378 |
# Event handlers
|
| 379 |
async def update_context(spec: str, set: str, state: Dict) -> List:
|
| 380 |
"""Update rotation context."""
|
|
|
|
| 385 |
state["suggested_goals"] = goals
|
| 386 |
return self._update_displays(state)
|
| 387 |
return [[], [], [], "No active learning goal"]
|
| 388 |
+
|
| 389 |
def process_audio(audio: np.ndarray) -> str:
|
| 390 |
"""Convert audio to text."""
|
| 391 |
if audio is None:
|
| 392 |
return ""
|
|
|
|
| 393 |
return "Audio transcription would appear here"
|
| 394 |
+
|
| 395 |
def clear_discussion() -> Tuple[List, str, Dict]:
|
| 396 |
"""Clear chat history."""
|
| 397 |
return [], "", {
|
|
|
|
| 400 |
"discussion_start": None,
|
| 401 |
"last_message": None
|
| 402 |
}
|
| 403 |
+
|
| 404 |
# Wire up events
|
| 405 |
specialty.change(
|
| 406 |
update_context,
|
| 407 |
inputs=[specialty, setting, state],
|
| 408 |
outputs=[goals_list, progress_display, recent_display, goal_display]
|
| 409 |
)
|
| 410 |
+
|
| 411 |
setting.change(
|
| 412 |
update_context,
|
| 413 |
inputs=[specialty, setting, state],
|
| 414 |
outputs=[goals_list, progress_display, recent_display, goal_display]
|
| 415 |
)
|
| 416 |
+
|
| 417 |
# Chat events with streaming
|
| 418 |
msg.submit(
|
| 419 |
self.process_chat,
|
|
|
|
| 425 |
inputs=[state],
|
| 426 |
outputs=[discussion_status]
|
| 427 |
)
|
| 428 |
+
|
| 429 |
# Voice input handling
|
| 430 |
audio_input.stream(
|
| 431 |
process_audio,
|
|
|
|
| 436 |
inputs=[msg, chatbot, state],
|
| 437 |
outputs=[chatbot, msg, state]
|
| 438 |
)
|
| 439 |
+
|
| 440 |
# Button handlers
|
| 441 |
clear.click(
|
| 442 |
clear_discussion,
|
|
|
|
| 445 |
lambda: "Start a new case discussion",
|
| 446 |
outputs=[discussion_status]
|
| 447 |
)
|
| 448 |
+
|
| 449 |
end.click(
|
| 450 |
self.end_discussion,
|
| 451 |
inputs=[state],
|
| 452 |
outputs=[goals_list, progress_display, recent_display, goal_display]
|
| 453 |
)
|
| 454 |
+
|
| 455 |
generate.click(
|
| 456 |
self.generate_goals,
|
| 457 |
inputs=[state],
|
| 458 |
outputs=[goals_list, progress_display, recent_display, goal_display]
|
| 459 |
)
|
| 460 |
+
|
| 461 |
# Goal management
|
| 462 |
add_goal.click(
|
| 463 |
self.add_user_goal,
|
| 464 |
inputs=[new_goal, state],
|
| 465 |
outputs=[goals_list, progress_display, recent_display, goal_display]
|
| 466 |
)
|
| 467 |
+
|
| 468 |
goals_list.select(
|
| 469 |
self.select_goal,
|
| 470 |
inputs=[state],
|
| 471 |
outputs=[goals_list, progress_display, recent_display, goal_display]
|
| 472 |
)
|
| 473 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 474 |
return interface
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|