Upload folder using huggingface_hub
Browse files- .github/workflows/deploy.yaml +14 -0
- .github/workflows/test.yaml +9 -0
- .gitignore +155 -0
- .gradio/certificate.pem +31 -0
- LICENSE +201 -0
- MANIFEST.in +5 -0
- README.md +73 -12
- app.py +15 -0
- nbs/00_learning_context.ipynb +431 -0
- nbs/01_clinical_tutor.ipynb +857 -0
- nbs/02_learning_interface.ipynb +1325 -0
- nbs/03_utils.ipynb +210 -0
- nbs/_quarto.yml +22 -0
- nbs/index.ipynb +180 -0
- nbs/main.ipynb +87 -0
- nbs/nbdev.yml +9 -0
- nbs/sidebar.yml +5 -0
- nbs/styles.css +25 -0
- pyproject.toml +3 -0
- requirements.txt +19 -0
- settings.ini +45 -0
- setup.py +64 -0
- wardbuddy/__init__.py +1 -0
- wardbuddy/_modidx.py +73 -0
- wardbuddy/clinical_tutor.py +315 -0
- wardbuddy/core.py +9 -0
- wardbuddy/learning_context.py +251 -0
- wardbuddy/learning_interface.py +373 -0
- wardbuddy/main.py +60 -0
- wardbuddy/utils.py +68 -0
.github/workflows/deploy.yaml
ADDED
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
name: Deploy to GitHub Pages
|
2 |
+
|
3 |
+
permissions:
|
4 |
+
contents: write
|
5 |
+
pages: write
|
6 |
+
|
7 |
+
on:
|
8 |
+
push:
|
9 |
+
branches: [ "main", "master" ]
|
10 |
+
workflow_dispatch:
|
11 |
+
jobs:
|
12 |
+
deploy:
|
13 |
+
runs-on: ubuntu-latest
|
14 |
+
steps: [uses: fastai/workflows/quarto-ghp@master]
|
.github/workflows/test.yaml
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
name: CI
|
2 |
+
on: [workflow_dispatch, pull_request, push]
|
3 |
+
|
4 |
+
jobs:
|
5 |
+
test:
|
6 |
+
runs-on: ubuntu-latest
|
7 |
+
env:
|
8 |
+
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
9 |
+
steps: [uses: fastai/workflows/nbdev-ci@master]
|
.gitignore
ADDED
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# API Keys
|
2 |
+
.env
|
3 |
+
|
4 |
+
_docs/
|
5 |
+
_proc/
|
6 |
+
|
7 |
+
*.bak
|
8 |
+
.gitattributes
|
9 |
+
.last_checked
|
10 |
+
.gitconfig
|
11 |
+
*.bak
|
12 |
+
*.log
|
13 |
+
*~
|
14 |
+
~*
|
15 |
+
_tmp*
|
16 |
+
tmp*
|
17 |
+
tags
|
18 |
+
*.pkg
|
19 |
+
|
20 |
+
# Byte-compiled / optimized / DLL files
|
21 |
+
__pycache__/
|
22 |
+
*.py[cod]
|
23 |
+
*$py.class
|
24 |
+
|
25 |
+
# C extensions
|
26 |
+
*.so
|
27 |
+
|
28 |
+
# Distribution / packaging
|
29 |
+
.Python
|
30 |
+
env/
|
31 |
+
build/
|
32 |
+
conda/
|
33 |
+
develop-eggs/
|
34 |
+
dist/
|
35 |
+
downloads/
|
36 |
+
eggs/
|
37 |
+
.eggs/
|
38 |
+
lib/
|
39 |
+
lib64/
|
40 |
+
parts/
|
41 |
+
sdist/
|
42 |
+
var/
|
43 |
+
wheels/
|
44 |
+
*.egg-info/
|
45 |
+
.installed.cfg
|
46 |
+
*.egg
|
47 |
+
|
48 |
+
# PyInstaller
|
49 |
+
# Usually these files are written by a python script from a template
|
50 |
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
51 |
+
*.manifest
|
52 |
+
*.spec
|
53 |
+
|
54 |
+
# Installer logs
|
55 |
+
pip-log.txt
|
56 |
+
pip-delete-this-directory.txt
|
57 |
+
|
58 |
+
# Unit test / coverage reports
|
59 |
+
htmlcov/
|
60 |
+
.tox/
|
61 |
+
.coverage
|
62 |
+
.coverage.*
|
63 |
+
.cache
|
64 |
+
nosetests.xml
|
65 |
+
coverage.xml
|
66 |
+
*.cover
|
67 |
+
.hypothesis/
|
68 |
+
|
69 |
+
# Translations
|
70 |
+
*.mo
|
71 |
+
*.pot
|
72 |
+
|
73 |
+
# Django stuff:
|
74 |
+
*.log
|
75 |
+
local_settings.py
|
76 |
+
|
77 |
+
# Flask stuff:
|
78 |
+
instance/
|
79 |
+
.webassets-cache
|
80 |
+
|
81 |
+
# Scrapy stuff:
|
82 |
+
.scrapy
|
83 |
+
|
84 |
+
# Sphinx documentation
|
85 |
+
docs/_build/
|
86 |
+
|
87 |
+
# PyBuilder
|
88 |
+
target/
|
89 |
+
|
90 |
+
# Jupyter Notebook
|
91 |
+
.ipynb_checkpoints
|
92 |
+
|
93 |
+
# pyenv
|
94 |
+
.python-version
|
95 |
+
|
96 |
+
# celery beat schedule file
|
97 |
+
celerybeat-schedule
|
98 |
+
|
99 |
+
# SageMath parsed files
|
100 |
+
*.sage.py
|
101 |
+
|
102 |
+
# dotenv
|
103 |
+
.env
|
104 |
+
|
105 |
+
# virtualenv
|
106 |
+
.venv
|
107 |
+
venv/
|
108 |
+
ENV/
|
109 |
+
|
110 |
+
# Spyder project settings
|
111 |
+
.spyderproject
|
112 |
+
.spyproject
|
113 |
+
|
114 |
+
# Rope project settings
|
115 |
+
.ropeproject
|
116 |
+
|
117 |
+
# mkdocs documentation
|
118 |
+
/site
|
119 |
+
|
120 |
+
# mypy
|
121 |
+
.mypy_cache/
|
122 |
+
|
123 |
+
.vscode
|
124 |
+
*.swp
|
125 |
+
|
126 |
+
# osx generated files
|
127 |
+
.DS_Store
|
128 |
+
.DS_Store?
|
129 |
+
.Trashes
|
130 |
+
ehthumbs.db
|
131 |
+
Thumbs.db
|
132 |
+
.idea
|
133 |
+
|
134 |
+
# pytest
|
135 |
+
.pytest_cache
|
136 |
+
|
137 |
+
# tools/trust-doc-nbs
|
138 |
+
docs_src/.last_checked
|
139 |
+
|
140 |
+
# symlinks to fastai
|
141 |
+
docs_src/fastai
|
142 |
+
tools/fastai
|
143 |
+
|
144 |
+
# link checker
|
145 |
+
checklink/cookies.txt
|
146 |
+
|
147 |
+
# .gitconfig is now autogenerated
|
148 |
+
.gitconfig
|
149 |
+
|
150 |
+
# Quarto installer
|
151 |
+
.deb
|
152 |
+
.pkg
|
153 |
+
|
154 |
+
# Quarto
|
155 |
+
.quarto
|
.gradio/certificate.pem
ADDED
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
-----BEGIN CERTIFICATE-----
|
2 |
+
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
|
3 |
+
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
|
4 |
+
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
|
5 |
+
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
|
6 |
+
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
|
7 |
+
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
|
8 |
+
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
|
9 |
+
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
|
10 |
+
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
|
11 |
+
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
|
12 |
+
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
|
13 |
+
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
|
14 |
+
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
|
15 |
+
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
|
16 |
+
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
|
17 |
+
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
|
18 |
+
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
|
19 |
+
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
|
20 |
+
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
|
21 |
+
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
|
22 |
+
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
|
23 |
+
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
|
24 |
+
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
|
25 |
+
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
|
26 |
+
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
|
27 |
+
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
|
28 |
+
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
|
29 |
+
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
|
30 |
+
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
|
31 |
+
-----END CERTIFICATE-----
|
LICENSE
ADDED
@@ -0,0 +1,201 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
Apache License
|
2 |
+
Version 2.0, January 2004
|
3 |
+
http://www.apache.org/licenses/
|
4 |
+
|
5 |
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
6 |
+
|
7 |
+
1. Definitions.
|
8 |
+
|
9 |
+
"License" shall mean the terms and conditions for use, reproduction,
|
10 |
+
and distribution as defined by Sections 1 through 9 of this document.
|
11 |
+
|
12 |
+
"Licensor" shall mean the copyright owner or entity authorized by
|
13 |
+
the copyright owner that is granting the License.
|
14 |
+
|
15 |
+
"Legal Entity" shall mean the union of the acting entity and all
|
16 |
+
other entities that control, are controlled by, or are under common
|
17 |
+
control with that entity. For the purposes of this definition,
|
18 |
+
"control" means (i) the power, direct or indirect, to cause the
|
19 |
+
direction or management of such entity, whether by contract or
|
20 |
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
21 |
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
22 |
+
|
23 |
+
"You" (or "Your") shall mean an individual or Legal Entity
|
24 |
+
exercising permissions granted by this License.
|
25 |
+
|
26 |
+
"Source" form shall mean the preferred form for making modifications,
|
27 |
+
including but not limited to software source code, documentation
|
28 |
+
source, and configuration files.
|
29 |
+
|
30 |
+
"Object" form shall mean any form resulting from mechanical
|
31 |
+
transformation or translation of a Source form, including but
|
32 |
+
not limited to compiled object code, generated documentation,
|
33 |
+
and conversions to other media types.
|
34 |
+
|
35 |
+
"Work" shall mean the work of authorship, whether in Source or
|
36 |
+
Object form, made available under the License, as indicated by a
|
37 |
+
copyright notice that is included in or attached to the work
|
38 |
+
(an example is provided in the Appendix below).
|
39 |
+
|
40 |
+
"Derivative Works" shall mean any work, whether in Source or Object
|
41 |
+
form, that is based on (or derived from) the Work and for which the
|
42 |
+
editorial revisions, annotations, elaborations, or other modifications
|
43 |
+
represent, as a whole, an original work of authorship. For the purposes
|
44 |
+
of this License, Derivative Works shall not include works that remain
|
45 |
+
separable from, or merely link (or bind by name) to the interfaces of,
|
46 |
+
the Work and Derivative Works thereof.
|
47 |
+
|
48 |
+
"Contribution" shall mean any work of authorship, including
|
49 |
+
the original version of the Work and any modifications or additions
|
50 |
+
to that Work or Derivative Works thereof, that is intentionally
|
51 |
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
52 |
+
or by an individual or Legal Entity authorized to submit on behalf of
|
53 |
+
the copyright owner. For the purposes of this definition, "submitted"
|
54 |
+
means any form of electronic, verbal, or written communication sent
|
55 |
+
to the Licensor or its representatives, including but not limited to
|
56 |
+
communication on electronic mailing lists, source code control systems,
|
57 |
+
and issue tracking systems that are managed by, or on behalf of, the
|
58 |
+
Licensor for the purpose of discussing and improving the Work, but
|
59 |
+
excluding communication that is conspicuously marked or otherwise
|
60 |
+
designated in writing by the copyright owner as "Not a Contribution."
|
61 |
+
|
62 |
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
63 |
+
on behalf of whom a Contribution has been received by Licensor and
|
64 |
+
subsequently incorporated within the Work.
|
65 |
+
|
66 |
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
67 |
+
this License, each Contributor hereby grants to You a perpetual,
|
68 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
69 |
+
copyright license to reproduce, prepare Derivative Works of,
|
70 |
+
publicly display, publicly perform, sublicense, and distribute the
|
71 |
+
Work and such Derivative Works in Source or Object form.
|
72 |
+
|
73 |
+
3. Grant of Patent License. Subject to the terms and conditions of
|
74 |
+
this License, each Contributor hereby grants to You a perpetual,
|
75 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
76 |
+
(except as stated in this section) patent license to make, have made,
|
77 |
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
78 |
+
where such license applies only to those patent claims licensable
|
79 |
+
by such Contributor that are necessarily infringed by their
|
80 |
+
Contribution(s) alone or by combination of their Contribution(s)
|
81 |
+
with the Work to which such Contribution(s) was submitted. If You
|
82 |
+
institute patent litigation against any entity (including a
|
83 |
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
84 |
+
or a Contribution incorporated within the Work constitutes direct
|
85 |
+
or contributory patent infringement, then any patent licenses
|
86 |
+
granted to You under this License for that Work shall terminate
|
87 |
+
as of the date such litigation is filed.
|
88 |
+
|
89 |
+
4. Redistribution. You may reproduce and distribute copies of the
|
90 |
+
Work or Derivative Works thereof in any medium, with or without
|
91 |
+
modifications, and in Source or Object form, provided that You
|
92 |
+
meet the following conditions:
|
93 |
+
|
94 |
+
(a) You must give any other recipients of the Work or
|
95 |
+
Derivative Works a copy of this License; and
|
96 |
+
|
97 |
+
(b) You must cause any modified files to carry prominent notices
|
98 |
+
stating that You changed the files; and
|
99 |
+
|
100 |
+
(c) You must retain, in the Source form of any Derivative Works
|
101 |
+
that You distribute, all copyright, patent, trademark, and
|
102 |
+
attribution notices from the Source form of the Work,
|
103 |
+
excluding those notices that do not pertain to any part of
|
104 |
+
the Derivative Works; and
|
105 |
+
|
106 |
+
(d) If the Work includes a "NOTICE" text file as part of its
|
107 |
+
distribution, then any Derivative Works that You distribute must
|
108 |
+
include a readable copy of the attribution notices contained
|
109 |
+
within such NOTICE file, excluding those notices that do not
|
110 |
+
pertain to any part of the Derivative Works, in at least one
|
111 |
+
of the following places: within a NOTICE text file distributed
|
112 |
+
as part of the Derivative Works; within the Source form or
|
113 |
+
documentation, if provided along with the Derivative Works; or,
|
114 |
+
within a display generated by the Derivative Works, if and
|
115 |
+
wherever such third-party notices normally appear. The contents
|
116 |
+
of the NOTICE file are for informational purposes only and
|
117 |
+
do not modify the License. You may add Your own attribution
|
118 |
+
notices within Derivative Works that You distribute, alongside
|
119 |
+
or as an addendum to the NOTICE text from the Work, provided
|
120 |
+
that such additional attribution notices cannot be construed
|
121 |
+
as modifying the License.
|
122 |
+
|
123 |
+
You may add Your own copyright statement to Your modifications and
|
124 |
+
may provide additional or different license terms and conditions
|
125 |
+
for use, reproduction, or distribution of Your modifications, or
|
126 |
+
for any such Derivative Works as a whole, provided Your use,
|
127 |
+
reproduction, and distribution of the Work otherwise complies with
|
128 |
+
the conditions stated in this License.
|
129 |
+
|
130 |
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
131 |
+
any Contribution intentionally submitted for inclusion in the Work
|
132 |
+
by You to the Licensor shall be under the terms and conditions of
|
133 |
+
this License, without any additional terms or conditions.
|
134 |
+
Notwithstanding the above, nothing herein shall supersede or modify
|
135 |
+
the terms of any separate license agreement you may have executed
|
136 |
+
with Licensor regarding such Contributions.
|
137 |
+
|
138 |
+
6. Trademarks. This License does not grant permission to use the trade
|
139 |
+
names, trademarks, service marks, or product names of the Licensor,
|
140 |
+
except as required for reasonable and customary use in describing the
|
141 |
+
origin of the Work and reproducing the content of the NOTICE file.
|
142 |
+
|
143 |
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
144 |
+
agreed to in writing, Licensor provides the Work (and each
|
145 |
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
146 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
147 |
+
implied, including, without limitation, any warranties or conditions
|
148 |
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
149 |
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
150 |
+
appropriateness of using or redistributing the Work and assume any
|
151 |
+
risks associated with Your exercise of permissions under this License.
|
152 |
+
|
153 |
+
8. Limitation of Liability. In no event and under no legal theory,
|
154 |
+
whether in tort (including negligence), contract, or otherwise,
|
155 |
+
unless required by applicable law (such as deliberate and grossly
|
156 |
+
negligent acts) or agreed to in writing, shall any Contributor be
|
157 |
+
liable to You for damages, including any direct, indirect, special,
|
158 |
+
incidental, or consequential damages of any character arising as a
|
159 |
+
result of this License or out of the use or inability to use the
|
160 |
+
Work (including but not limited to damages for loss of goodwill,
|
161 |
+
work stoppage, computer failure or malfunction, or any and all
|
162 |
+
other commercial damages or losses), even if such Contributor
|
163 |
+
has been advised of the possibility of such damages.
|
164 |
+
|
165 |
+
9. Accepting Warranty or Additional Liability. While redistributing
|
166 |
+
the Work or Derivative Works thereof, You may choose to offer,
|
167 |
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
168 |
+
or other liability obligations and/or rights consistent with this
|
169 |
+
License. However, in accepting such obligations, You may act only
|
170 |
+
on Your own behalf and on Your sole responsibility, not on behalf
|
171 |
+
of any other Contributor, and only if You agree to indemnify,
|
172 |
+
defend, and hold each Contributor harmless for any liability
|
173 |
+
incurred by, or claims asserted against, such Contributor by reason
|
174 |
+
of your accepting any such warranty or additional liability.
|
175 |
+
|
176 |
+
END OF TERMS AND CONDITIONS
|
177 |
+
|
178 |
+
APPENDIX: How to apply the Apache License to your work.
|
179 |
+
|
180 |
+
To apply the Apache License to your work, attach the following
|
181 |
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
182 |
+
replaced with your own identifying information. (Don't include
|
183 |
+
the brackets!) The text should be enclosed in the appropriate
|
184 |
+
comment syntax for the file format. We also recommend that a
|
185 |
+
file or class name and description of purpose be included on the
|
186 |
+
same "printed page" as the copyright notice for easier
|
187 |
+
identification within third-party archives.
|
188 |
+
|
189 |
+
Copyright 2022, fastai
|
190 |
+
|
191 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
192 |
+
you may not use this file except in compliance with the License.
|
193 |
+
You may obtain a copy of the License at
|
194 |
+
|
195 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
196 |
+
|
197 |
+
Unless required by applicable law or agreed to in writing, software
|
198 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
199 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
200 |
+
See the License for the specific language governing permissions and
|
201 |
+
limitations under the License.
|
MANIFEST.in
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
include settings.ini
|
2 |
+
include LICENSE
|
3 |
+
include CONTRIBUTING.md
|
4 |
+
include README.md
|
5 |
+
recursive-exclude * __pycache__
|
README.md
CHANGED
@@ -1,12 +1,73 @@
|
|
1 |
-
---
|
2 |
-
title:
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
title: wardbuddy
|
3 |
+
app_file: app.py
|
4 |
+
sdk: gradio
|
5 |
+
sdk_version: 5.12.0
|
6 |
+
---
|
7 |
+
# wardbuddy
|
8 |
+
|
9 |
+
|
10 |
+
<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->
|
11 |
+
|
12 |
+
This file will become your README and also the index of your
|
13 |
+
documentation.
|
14 |
+
|
15 |
+
## Developer Guide
|
16 |
+
|
17 |
+
If you are new to using `nbdev` here are some useful pointers to get you
|
18 |
+
started.
|
19 |
+
|
20 |
+
### Install wardbuddy in Development mode
|
21 |
+
|
22 |
+
``` sh
|
23 |
+
# make sure wardbuddy package is installed in development mode
|
24 |
+
$ pip install -e .
|
25 |
+
|
26 |
+
# make changes under nbs/ directory
|
27 |
+
# ...
|
28 |
+
|
29 |
+
# compile to have changes apply to wardbuddy
|
30 |
+
$ nbdev_prepare
|
31 |
+
```
|
32 |
+
|
33 |
+
## Usage
|
34 |
+
|
35 |
+
### Installation
|
36 |
+
|
37 |
+
Install latest from the GitHub
|
38 |
+
[repository](https://github.com/Dyadd/wardbuddy):
|
39 |
+
|
40 |
+
``` sh
|
41 |
+
$ pip install git+https://github.com/Dyadd/wardbuddy.git
|
42 |
+
```
|
43 |
+
|
44 |
+
or from [conda](https://anaconda.org/Dyadd/wardbuddy)
|
45 |
+
|
46 |
+
``` sh
|
47 |
+
$ conda install -c Dyadd wardbuddy
|
48 |
+
```
|
49 |
+
|
50 |
+
or from [pypi](https://pypi.org/project/wardbuddy/)
|
51 |
+
|
52 |
+
``` sh
|
53 |
+
$ pip install wardbuddy
|
54 |
+
```
|
55 |
+
|
56 |
+
### Documentation
|
57 |
+
|
58 |
+
Documentation can be found hosted on this GitHub
|
59 |
+
[repository](https://github.com/Dyadd/wardbuddy)’s
|
60 |
+
[pages](https://Dyadd.github.io/wardbuddy/). Additionally you can find
|
61 |
+
package manager specific guidelines on
|
62 |
+
[conda](https://anaconda.org/Dyadd/wardbuddy) and
|
63 |
+
[pypi](https://pypi.org/project/wardbuddy/) respectively.
|
64 |
+
|
65 |
+
## How to use
|
66 |
+
|
67 |
+
Fill me in please! Don’t forget code examples:
|
68 |
+
|
69 |
+
``` python
|
70 |
+
1+1
|
71 |
+
```
|
72 |
+
|
73 |
+
2
|
app.py
ADDED
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from dotenv import load_dotenv
|
2 |
+
import os
|
3 |
+
from wardbuddy.learning_interface import LearningInterface
|
4 |
+
|
5 |
+
# Load environment variables
|
6 |
+
load_dotenv()
|
7 |
+
|
8 |
+
# Check for API key
|
9 |
+
if not os.getenv("OPENROUTER_API_KEY"):
|
10 |
+
raise ValueError("Please set OPENROUTER_API_KEY in your .env file")
|
11 |
+
|
12 |
+
# Create and launch interface
|
13 |
+
interface = LearningInterface()
|
14 |
+
demo = interface.create_interface()
|
15 |
+
demo.launch(server_name='0.0.0.0', server_port=7860, share=True)
|
nbs/00_learning_context.ipynb
ADDED
@@ -0,0 +1,431 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"cells": [
|
3 |
+
{
|
4 |
+
"cell_type": "code",
|
5 |
+
"execution_count": null,
|
6 |
+
"metadata": {},
|
7 |
+
"outputs": [],
|
8 |
+
"source": [
|
9 |
+
"#| default_exp learning_context"
|
10 |
+
]
|
11 |
+
},
|
12 |
+
{
|
13 |
+
"cell_type": "markdown",
|
14 |
+
"metadata": {},
|
15 |
+
"source": [
|
16 |
+
"# Learning Context\n",
|
17 |
+
"\n",
|
18 |
+
"> Core module for managing learning context (memory -> LOs, prior cases, knowledge gaps, feedback preferences)"
|
19 |
+
]
|
20 |
+
},
|
21 |
+
{
|
22 |
+
"cell_type": "markdown",
|
23 |
+
"metadata": {},
|
24 |
+
"source": [
|
25 |
+
"## Setup"
|
26 |
+
]
|
27 |
+
},
|
28 |
+
{
|
29 |
+
"cell_type": "code",
|
30 |
+
"execution_count": null,
|
31 |
+
"metadata": {},
|
32 |
+
"outputs": [],
|
33 |
+
"source": [
|
34 |
+
"#| hide\n",
|
35 |
+
"from nbdev.showdoc import *"
|
36 |
+
]
|
37 |
+
},
|
38 |
+
{
|
39 |
+
"cell_type": "code",
|
40 |
+
"execution_count": null,
|
41 |
+
"metadata": {},
|
42 |
+
"outputs": [
|
43 |
+
{
|
44 |
+
"name": "stderr",
|
45 |
+
"output_type": "stream",
|
46 |
+
"text": [
|
47 |
+
"C:\\Users\\deepa\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\tqdm\\auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n",
|
48 |
+
" from .autonotebook import tqdm as notebook_tqdm\n"
|
49 |
+
]
|
50 |
+
}
|
51 |
+
],
|
52 |
+
"source": [
|
53 |
+
"#| export\n",
|
54 |
+
"from typing import Dict, List, Optional\n",
|
55 |
+
"from datetime import datetime\n",
|
56 |
+
"from enum import Enum\n",
|
57 |
+
"from pydantic import BaseModel, Field\n",
|
58 |
+
"import json\n",
|
59 |
+
"from pathlib import Path\n",
|
60 |
+
"import logging\n",
|
61 |
+
"from wardbuddy.utils import setup_logger, load_context_safely, save_context_safely"
|
62 |
+
]
|
63 |
+
},
|
64 |
+
{
|
65 |
+
"cell_type": "code",
|
66 |
+
"execution_count": null,
|
67 |
+
"metadata": {},
|
68 |
+
"outputs": [],
|
69 |
+
"source": [
|
70 |
+
"#| export\n",
|
71 |
+
"logger = setup_logger(__name__)"
|
72 |
+
]
|
73 |
+
},
|
74 |
+
{
|
75 |
+
"cell_type": "markdown",
|
76 |
+
"metadata": {},
|
77 |
+
"source": [
|
78 |
+
"## Learning Context Management\n",
|
79 |
+
"\n",
|
80 |
+
"> Core module for managing student learning context and history"
|
81 |
+
]
|
82 |
+
},
|
83 |
+
{
|
84 |
+
"cell_type": "markdown",
|
85 |
+
"metadata": {},
|
86 |
+
"source": [
|
87 |
+
"The system needs to track and handle student information for personalised education. \n",
|
88 |
+
"\n",
|
89 |
+
"This module handles:\n",
|
90 |
+
"* Tracking learning objectives\n",
|
91 |
+
"* Managing case history\n",
|
92 |
+
"* Monitoring knowledge gaps\n",
|
93 |
+
"* Customizing feedback preferences\n",
|
94 |
+
"\n",
|
95 |
+
"and finally:\n",
|
96 |
+
"* Context persistence\n"
|
97 |
+
]
|
98 |
+
},
|
99 |
+
{
|
100 |
+
"cell_type": "code",
|
101 |
+
"execution_count": null,
|
102 |
+
"metadata": {},
|
103 |
+
"outputs": [],
|
104 |
+
"source": [
|
105 |
+
"#| export\n",
|
106 |
+
"class LearningCategory(str, Enum):\n",
|
107 |
+
" \"\"\"Main learning categories\"\"\"\n",
|
108 |
+
" HISTORY_TAKING = \"History Taking\"\n",
|
109 |
+
" PHYSICAL_EXAM = \"Physical Examinations\"\n",
|
110 |
+
" INVESTIGATIONS = \"Investigations\"\n",
|
111 |
+
" MANAGEMENT = \"Management\"\n",
|
112 |
+
" CLINICAL_REASONING = \"Clinical Reasoning\"\n",
|
113 |
+
" COMMUNICATION = \"Communication & Presentation\"\n",
|
114 |
+
" PROCEDURAL = \"Procedural Skills\"\n",
|
115 |
+
" MEDICAL_KNOWLEDGE = \"Medical Knowledge\"\n",
|
116 |
+
"\n",
|
117 |
+
"class RotationContext(BaseModel):\n",
|
118 |
+
" \"\"\"Clinical rotation context\"\"\"\n",
|
119 |
+
" specialty: str = Field(..., description=\"Medical specialty\")\n",
|
120 |
+
" setting: str = Field(..., description=\"Clinical setting (Clinic/Wards/ED)\")\n",
|
121 |
+
"\n",
|
122 |
+
"class SmartGoal(BaseModel):\n",
|
123 |
+
" \"\"\"SMART learning goal\"\"\"\n",
|
124 |
+
" id: str = Field(..., description=\"Unique goal identifier\")\n",
|
125 |
+
" category: LearningCategory\n",
|
126 |
+
" original_input: str = Field(..., description=\"User's original goal input\")\n",
|
127 |
+
" smart_version: str = Field(..., description=\"SMART formatted goal\")\n",
|
128 |
+
" specialty: str\n",
|
129 |
+
" setting: str\n",
|
130 |
+
" created_at: datetime\n",
|
131 |
+
" completed_at: Optional[datetime] = None\n",
|
132 |
+
"\n",
|
133 |
+
" class Config:\n",
|
134 |
+
" json_encoders = {\n",
|
135 |
+
" datetime: lambda v: v.isoformat()\n",
|
136 |
+
" }\n",
|
137 |
+
"\n",
|
138 |
+
"class CategoryProgress(BaseModel):\n",
|
139 |
+
" \"\"\"Progress tracking for a category\"\"\"\n",
|
140 |
+
" category: LearningCategory\n",
|
141 |
+
" completed_goals: List[SmartGoal] = Field(default_factory=list)\n",
|
142 |
+
" total_goals: int = Field(default=0)\n"
|
143 |
+
]
|
144 |
+
},
|
145 |
+
{
|
146 |
+
"cell_type": "code",
|
147 |
+
"execution_count": null,
|
148 |
+
"metadata": {},
|
149 |
+
"outputs": [],
|
150 |
+
"source": [
|
151 |
+
"#| export\n",
|
152 |
+
"class LearningContext:\n",
|
153 |
+
" \"\"\"\n",
|
154 |
+
" Manages learning context and goal tracking.\n",
|
155 |
+
" \n",
|
156 |
+
" This class handles:\n",
|
157 |
+
" - Rotation context management\n",
|
158 |
+
" - SMART goal tracking\n",
|
159 |
+
" - Progress monitoring by category\n",
|
160 |
+
" - Context persistence\n",
|
161 |
+
" \"\"\"\n",
|
162 |
+
" \n",
|
163 |
+
" def __init__(self, context_path: Optional[Path] = None):\n",
|
164 |
+
" \"\"\"\n",
|
165 |
+
" Initialize learning context.\n",
|
166 |
+
" \n",
|
167 |
+
" Args:\n",
|
168 |
+
" context_path: Optional path to load/save context\n",
|
169 |
+
" \"\"\"\n",
|
170 |
+
" self.context_path = context_path\n",
|
171 |
+
" \n",
|
172 |
+
" # Initialize rotation context\n",
|
173 |
+
" self.rotation = RotationContext(\n",
|
174 |
+
" specialty=\"\",\n",
|
175 |
+
" setting=\"\"\n",
|
176 |
+
" )\n",
|
177 |
+
" \n",
|
178 |
+
" # Initialize category tracking\n",
|
179 |
+
" self.category_progress: Dict[LearningCategory, CategoryProgress] = {\n",
|
180 |
+
" cat: CategoryProgress(category=cat)\n",
|
181 |
+
" for cat in LearningCategory\n",
|
182 |
+
" }\n",
|
183 |
+
" \n",
|
184 |
+
" # Current active goal\n",
|
185 |
+
" self.active_goal: Optional[SmartGoal] = None\n",
|
186 |
+
" \n",
|
187 |
+
" # Load existing context if available\n",
|
188 |
+
" if context_path and context_path.exists():\n",
|
189 |
+
" self.load_context()\n",
|
190 |
+
" \n",
|
191 |
+
" def update_rotation(self, specialty: str, setting: str) -> None:\n",
|
192 |
+
" \"\"\"\n",
|
193 |
+
" Update rotation context.\n",
|
194 |
+
" \n",
|
195 |
+
" Args:\n",
|
196 |
+
" specialty: Medical specialty\n",
|
197 |
+
" setting: Clinical setting\n",
|
198 |
+
" \"\"\"\n",
|
199 |
+
" self.rotation = RotationContext(\n",
|
200 |
+
" specialty=specialty,\n",
|
201 |
+
" setting=setting\n",
|
202 |
+
" )\n",
|
203 |
+
" self._save_context()\n",
|
204 |
+
" \n",
|
205 |
+
" def add_smart_goal(self, goal: SmartGoal) -> None:\n",
|
206 |
+
" \"\"\"\n",
|
207 |
+
" Add new SMART goal.\n",
|
208 |
+
" \n",
|
209 |
+
" Args:\n",
|
210 |
+
" goal: SMART goal to add\n",
|
211 |
+
" \"\"\"\n",
|
212 |
+
" self.active_goal = goal\n",
|
213 |
+
" self.category_progress[goal.category].total_goals += 1\n",
|
214 |
+
" self._save_context()\n",
|
215 |
+
" \n",
|
216 |
+
" def complete_active_goal(self) -> None:\n",
|
217 |
+
" \"\"\"Complete current active goal.\"\"\"\n",
|
218 |
+
" if self.active_goal:\n",
|
219 |
+
" self.active_goal.completed_at = datetime.now()\n",
|
220 |
+
" cat = self.active_goal.category\n",
|
221 |
+
" self.category_progress[cat].completed_goals.append(self.active_goal)\n",
|
222 |
+
" self.active_goal = None\n",
|
223 |
+
" self._save_context()\n",
|
224 |
+
" \n",
|
225 |
+
" def get_category_summary(self) -> Dict[str, Dict]:\n",
|
226 |
+
" \"\"\"\n",
|
227 |
+
" Get summary of progress by category.\n",
|
228 |
+
" \n",
|
229 |
+
" Returns:\n",
|
230 |
+
" dict: Category summaries including completed/total goals\n",
|
231 |
+
" and recent completions\n",
|
232 |
+
" \"\"\"\n",
|
233 |
+
" return {\n",
|
234 |
+
" cat.value: {\n",
|
235 |
+
" \"completed\": len(prog.completed_goals),\n",
|
236 |
+
" \"total\": prog.total_goals,\n",
|
237 |
+
" \"recent\": [\n",
|
238 |
+
" {\n",
|
239 |
+
" \"smart_version\": goal.smart_version,\n",
|
240 |
+
" \"completed_at\": goal.completed_at.isoformat()\n",
|
241 |
+
" }\n",
|
242 |
+
" for goal in sorted(\n",
|
243 |
+
" prog.completed_goals,\n",
|
244 |
+
" key=lambda x: x.completed_at or datetime.min,\n",
|
245 |
+
" reverse=True\n",
|
246 |
+
" )[:3] # Last 3 completed\n",
|
247 |
+
" ]\n",
|
248 |
+
" }\n",
|
249 |
+
" for cat, prog in self.category_progress.items()\n",
|
250 |
+
" }\n",
|
251 |
+
" \n",
|
252 |
+
" def get_all_goals(self) -> List[Dict]:\n",
|
253 |
+
" \"\"\"\n",
|
254 |
+
" Get all goals for current rotation.\n",
|
255 |
+
" \n",
|
256 |
+
" Returns:\n",
|
257 |
+
" list: All goals matching current rotation\n",
|
258 |
+
" \"\"\"\n",
|
259 |
+
" all_goals = []\n",
|
260 |
+
" for prog in self.category_progress.values():\n",
|
261 |
+
" # Add completed goals\n",
|
262 |
+
" all_goals.extend([\n",
|
263 |
+
" goal.dict() for goal in prog.completed_goals\n",
|
264 |
+
" if (goal.specialty == self.rotation.specialty and\n",
|
265 |
+
" goal.setting == self.rotation.setting)\n",
|
266 |
+
" ])\n",
|
267 |
+
" \n",
|
268 |
+
" # Add active goal if matches\n",
|
269 |
+
" if self.active_goal and (\n",
|
270 |
+
" self.active_goal.specialty == self.rotation.specialty and\n",
|
271 |
+
" self.active_goal.setting == self.rotation.setting\n",
|
272 |
+
" ):\n",
|
273 |
+
" all_goals.append(self.active_goal.dict())\n",
|
274 |
+
" \n",
|
275 |
+
" return sorted(\n",
|
276 |
+
" all_goals,\n",
|
277 |
+
" key=lambda x: x[\"created_at\"],\n",
|
278 |
+
" reverse=True\n",
|
279 |
+
" )\n",
|
280 |
+
" \n",
|
281 |
+
" def load_context(self) -> None:\n",
|
282 |
+
" \"\"\"Load context from file.\"\"\"\n",
|
283 |
+
" try:\n",
|
284 |
+
" with open(self.context_path, 'r') as f:\n",
|
285 |
+
" data = json.load(f)\n",
|
286 |
+
" \n",
|
287 |
+
" # Load rotation\n",
|
288 |
+
" self.rotation = RotationContext(**data[\"rotation\"])\n",
|
289 |
+
" \n",
|
290 |
+
" # Load progress\n",
|
291 |
+
" for cat_data in data[\"category_progress\"]:\n",
|
292 |
+
" cat = LearningCategory(cat_data[\"category\"])\n",
|
293 |
+
" \n",
|
294 |
+
" # Convert goal dicts back to models\n",
|
295 |
+
" completed_goals = [\n",
|
296 |
+
" SmartGoal(**g) for g in cat_data[\"completed_goals\"]\n",
|
297 |
+
" ]\n",
|
298 |
+
" \n",
|
299 |
+
" self.category_progress[cat] = CategoryProgress(\n",
|
300 |
+
" category=cat,\n",
|
301 |
+
" completed_goals=completed_goals,\n",
|
302 |
+
" total_goals=cat_data[\"total_goals\"]\n",
|
303 |
+
" )\n",
|
304 |
+
" \n",
|
305 |
+
" # Load active goal if exists\n",
|
306 |
+
" if data.get(\"active_goal\"):\n",
|
307 |
+
" self.active_goal = SmartGoal(**data[\"active_goal\"])\n",
|
308 |
+
" \n",
|
309 |
+
" logger.info(f\"Context loaded from {self.context_path}\")\n",
|
310 |
+
" \n",
|
311 |
+
" except Exception as e:\n",
|
312 |
+
" logger.error(f\"Error loading context: {str(e)}\")\n",
|
313 |
+
" \n",
|
314 |
+
" def _save_context(self) -> None:\n",
|
315 |
+
" \"\"\"Save context to file.\"\"\"\n",
|
316 |
+
" if not self.context_path:\n",
|
317 |
+
" return\n",
|
318 |
+
" \n",
|
319 |
+
" try:\n",
|
320 |
+
" data = {\n",
|
321 |
+
" \"rotation\": self.rotation.dict(),\n",
|
322 |
+
" \"category_progress\": [\n",
|
323 |
+
" {\n",
|
324 |
+
" \"category\": prog.category,\n",
|
325 |
+
" \"completed_goals\": [\n",
|
326 |
+
" g.dict() for g in prog.completed_goals\n",
|
327 |
+
" ],\n",
|
328 |
+
" \"total_goals\": prog.total_goals\n",
|
329 |
+
" }\n",
|
330 |
+
" for prog in self.category_progress.values()\n",
|
331 |
+
" ],\n",
|
332 |
+
" \"active_goal\": self.active_goal.dict() if self.active_goal else None\n",
|
333 |
+
" }\n",
|
334 |
+
" \n",
|
335 |
+
" with open(self.context_path, 'w') as f:\n",
|
336 |
+
" json.dump(data, f, indent=2)\n",
|
337 |
+
" \n",
|
338 |
+
" logger.info(f\"Context saved to {self.context_path}\")\n",
|
339 |
+
" \n",
|
340 |
+
" except Exception as e:\n",
|
341 |
+
" logger.error(f\"Error saving context: {str(e)}\")"
|
342 |
+
]
|
343 |
+
},
|
344 |
+
{
|
345 |
+
"cell_type": "markdown",
|
346 |
+
"metadata": {},
|
347 |
+
"source": [
|
348 |
+
"## Tests"
|
349 |
+
]
|
350 |
+
},
|
351 |
+
{
|
352 |
+
"cell_type": "code",
|
353 |
+
"execution_count": null,
|
354 |
+
"metadata": {},
|
355 |
+
"outputs": [
|
356 |
+
{
|
357 |
+
"name": "stderr",
|
358 |
+
"output_type": "stream",
|
359 |
+
"text": [
|
360 |
+
"2025-01-18 23:31:55,450 - __main__ - INFO - Learning context initialized\n",
|
361 |
+
"2025-01-18 23:31:55,457 - __main__ - INFO - Learning context initialized\n"
|
362 |
+
]
|
363 |
+
},
|
364 |
+
{
|
365 |
+
"name": "stdout",
|
366 |
+
"output_type": "stream",
|
367 |
+
"text": [
|
368 |
+
"All learning context tests passed!\n"
|
369 |
+
]
|
370 |
+
}
|
371 |
+
],
|
372 |
+
"source": [
|
373 |
+
"# | hide\n",
|
374 |
+
"def test_learning_context():\n",
|
375 |
+
" \"\"\"Test learning context functionality\"\"\"\n",
|
376 |
+
" # Initialize context\n",
|
377 |
+
" context = LearningContext()\n",
|
378 |
+
" \n",
|
379 |
+
" try:\n",
|
380 |
+
" # Test rotation updates\n",
|
381 |
+
" context.update_rotation(\"Emergency Medicine\", \"ED\")\n",
|
382 |
+
" assert context.rotation.specialty == \"Emergency Medicine\"\n",
|
383 |
+
" assert context.rotation.setting == \"ED\"\n",
|
384 |
+
" \n",
|
385 |
+
" # Test learning objectives\n",
|
386 |
+
" goal = SmartGoal(\n",
|
387 |
+
" id=\"test_goal_1\",\n",
|
388 |
+
" category=LearningCategory.CLINICAL_REASONING,\n",
|
389 |
+
" original_input=\"Get better at chest pain\",\n",
|
390 |
+
" smart_version=\"Demonstrate systematic assessment of chest pain\",\n",
|
391 |
+
" specialty=\"Emergency Medicine\",\n",
|
392 |
+
" setting=\"ED\",\n",
|
393 |
+
" created_at=datetime.now()\n",
|
394 |
+
" )\n",
|
395 |
+
" \n",
|
396 |
+
" context.add_smart_goal(goal)\n",
|
397 |
+
" assert context.active_goal == goal\n",
|
398 |
+
" \n",
|
399 |
+
" # Test completing objectives\n",
|
400 |
+
" context.complete_active_goal()\n",
|
401 |
+
" assert context.active_goal is None\n",
|
402 |
+
" \n",
|
403 |
+
" # Test progress tracking\n",
|
404 |
+
" summary = context.get_category_summary()\n",
|
405 |
+
" cat_data = summary[LearningCategory.CLINICAL_REASONING.value]\n",
|
406 |
+
" assert cat_data[\"completed\"] == 1\n",
|
407 |
+
" assert cat_data[\"total\"] == 1\n",
|
408 |
+
" assert len(cat_data[\"recent\"]) == 1\n",
|
409 |
+
" \n",
|
410 |
+
" finally:\n",
|
411 |
+
" # Cleanup\n",
|
412 |
+
" pass\n",
|
413 |
+
" \n",
|
414 |
+
" print(\"All learning context tests passed!\")\n",
|
415 |
+
"\n",
|
416 |
+
"# Run tests\n",
|
417 |
+
"if __name__ == \"__main__\":\n",
|
418 |
+
" test_learning_context()"
|
419 |
+
]
|
420 |
+
}
|
421 |
+
],
|
422 |
+
"metadata": {
|
423 |
+
"kernelspec": {
|
424 |
+
"display_name": "python3",
|
425 |
+
"language": "python",
|
426 |
+
"name": "python3"
|
427 |
+
}
|
428 |
+
},
|
429 |
+
"nbformat": 4,
|
430 |
+
"nbformat_minor": 4
|
431 |
+
}
|
nbs/01_clinical_tutor.ipynb
ADDED
@@ -0,0 +1,857 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"cells": [
|
3 |
+
{
|
4 |
+
"cell_type": "code",
|
5 |
+
"execution_count": null,
|
6 |
+
"id": "d9a32ad5-9f07-47f2-97ae-15b0646e355b",
|
7 |
+
"metadata": {},
|
8 |
+
"outputs": [],
|
9 |
+
"source": [
|
10 |
+
"#| default_exp clinical_tutor"
|
11 |
+
]
|
12 |
+
},
|
13 |
+
{
|
14 |
+
"cell_type": "markdown",
|
15 |
+
"id": "0db4b759-310c-4e38-9fdc-2efb94b541dd",
|
16 |
+
"metadata": {},
|
17 |
+
"source": [
|
18 |
+
"# Clinical Tutor\n",
|
19 |
+
"\n",
|
20 |
+
"> Core module for using learning context for context-appropriate tutor responses\n"
|
21 |
+
]
|
22 |
+
},
|
23 |
+
{
|
24 |
+
"cell_type": "markdown",
|
25 |
+
"id": "16f93992-88dd-409b-8370-b86302e1ce6a",
|
26 |
+
"metadata": {},
|
27 |
+
"source": [
|
28 |
+
"## Setup"
|
29 |
+
]
|
30 |
+
},
|
31 |
+
{
|
32 |
+
"cell_type": "code",
|
33 |
+
"execution_count": null,
|
34 |
+
"id": "6d2403bb-70a1-4744-be0b-d259234c1b62",
|
35 |
+
"metadata": {},
|
36 |
+
"outputs": [],
|
37 |
+
"source": [
|
38 |
+
"#| hide\n",
|
39 |
+
"from nbdev.showdoc import *"
|
40 |
+
]
|
41 |
+
},
|
42 |
+
{
|
43 |
+
"cell_type": "code",
|
44 |
+
"execution_count": null,
|
45 |
+
"id": "477ba22b-55c1-467e-8206-a92f88a598fd",
|
46 |
+
"metadata": {},
|
47 |
+
"outputs": [
|
48 |
+
{
|
49 |
+
"ename": "ImportError",
|
50 |
+
"evalue": "cannot import name 'LearningCategory' from 'wardbuddy.learning_context' (C:\\Users\\deepa\\OneDrive\\Documents\\StudyBuddy\\wardbuddy\\wardbuddy\\learning_context.py)",
|
51 |
+
"output_type": "error",
|
52 |
+
"traceback": [
|
53 |
+
"\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
|
54 |
+
"\u001b[1;31mImportError\u001b[0m Traceback (most recent call last)",
|
55 |
+
"Cell \u001b[1;32mIn[3], line 12\u001b[0m\n\u001b[0;32m 10\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mpydantic\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m BaseModel\n\u001b[0;32m 11\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mdotenv\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m load_dotenv\n\u001b[1;32m---> 12\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mwardbuddy\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mlearning_context\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m LearningContext, setup_logger, LearningCategory, SmartGoal, RotationContext\n\u001b[0;32m 15\u001b[0m \u001b[38;5;66;03m# Load environment variables\u001b[39;00m\n\u001b[0;32m 16\u001b[0m load_dotenv()\n",
|
56 |
+
"\u001b[1;31mImportError\u001b[0m: cannot import name 'LearningCategory' from 'wardbuddy.learning_context' (C:\\Users\\deepa\\OneDrive\\Documents\\StudyBuddy\\wardbuddy\\wardbuddy\\learning_context.py)"
|
57 |
+
]
|
58 |
+
}
|
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",
|
66 |
+
"import asyncio\n",
|
67 |
+
"import uuid\n",
|
68 |
+
"from datetime import datetime\n",
|
69 |
+
"from pathlib import Path\n",
|
70 |
+
"import aiohttp\n",
|
71 |
+
"from pydantic import BaseModel\n",
|
72 |
+
"from dotenv import load_dotenv\n",
|
73 |
+
"from wardbuddy.learning_context import LearningContext, setup_logger, LearningCategory, SmartGoal, RotationContext\n",
|
74 |
+
"\n",
|
75 |
+
"\n",
|
76 |
+
"# Load environment variables\n",
|
77 |
+
"load_dotenv()\n",
|
78 |
+
"\n",
|
79 |
+
"logger = setup_logger(__name__)"
|
80 |
+
]
|
81 |
+
},
|
82 |
+
{
|
83 |
+
"cell_type": "markdown",
|
84 |
+
"id": "7da445d7-7f51-44d5-b027-e2cf65c79069",
|
85 |
+
"metadata": {},
|
86 |
+
"source": [
|
87 |
+
"## Adaptive Clinical Tutor"
|
88 |
+
]
|
89 |
+
},
|
90 |
+
{
|
91 |
+
"cell_type": "markdown",
|
92 |
+
"id": "58d76615-480c-4fe4-9d4a-e67dd892132a",
|
93 |
+
"metadata": {},
|
94 |
+
"source": [
|
95 |
+
"This module implements:\n",
|
96 |
+
"\n",
|
97 |
+
" - Engages in natural case discussions like a clinical supervisor\n",
|
98 |
+
" - Provides context-aware feedback based on student's rotation and preferences\n",
|
99 |
+
" - Analyzes discussions to track learning progress\n",
|
100 |
+
" - Integrates with the student's learning context\n",
|
101 |
+
"\n",
|
102 |
+
"The tutor aims to mimic real-world clinical teaching interactions where students present cases and receive feedback in a natural conversational style.\n"
|
103 |
+
]
|
104 |
+
},
|
105 |
+
{
|
106 |
+
"cell_type": "code",
|
107 |
+
"execution_count": null,
|
108 |
+
"id": "da7d2115-b30f-40ba-9566-bcbaf155026d",
|
109 |
+
"metadata": {},
|
110 |
+
"outputs": [],
|
111 |
+
"source": [
|
112 |
+
"#| export \n",
|
113 |
+
"class OpenRouterException(Exception):\n",
|
114 |
+
" \"\"\"Custom exception for OpenRouter API errors\"\"\"\n",
|
115 |
+
" pass"
|
116 |
+
]
|
117 |
+
},
|
118 |
+
{
|
119 |
+
"cell_type": "code",
|
120 |
+
"execution_count": null,
|
121 |
+
"id": "c1956b74-a5fe-4b69-9b5c-efe4cc55b97d",
|
122 |
+
"metadata": {},
|
123 |
+
"outputs": [],
|
124 |
+
"source": [
|
125 |
+
"#| export\n",
|
126 |
+
"class ClinicalTutor:\n",
|
127 |
+
" \"\"\"\n",
|
128 |
+
" Clinical teaching system using LLMs for goal setting and case discussion.\n",
|
129 |
+
" \n",
|
130 |
+
" Features:\n",
|
131 |
+
" - SMART goal generation\n",
|
132 |
+
" - Case discussion\n",
|
133 |
+
" - Progress tracking\n",
|
134 |
+
" \"\"\"\n",
|
135 |
+
" \n",
|
136 |
+
" def __init__(\n",
|
137 |
+
" self,\n",
|
138 |
+
" context_path: Optional[Path] = None,\n",
|
139 |
+
" model: str = \"anthropic/claude-3-sonnet\",\n",
|
140 |
+
" api_key: Optional[str] = None\n",
|
141 |
+
" ):\n",
|
142 |
+
" \"\"\"\n",
|
143 |
+
" Initialize clinical tutor.\n",
|
144 |
+
" \n",
|
145 |
+
" Args:\n",
|
146 |
+
" context_path: Path for context persistence\n",
|
147 |
+
" model: OpenRouter model identifier\n",
|
148 |
+
" api_key: OpenRouter API key (falls back to env var)\n",
|
149 |
+
" \"\"\"\n",
|
150 |
+
" self.api_key = api_key or os.getenv(\"OPENROUTER_API_KEY\")\n",
|
151 |
+
" if not self.api_key:\n",
|
152 |
+
" raise ValueError(\"OpenRouter API key required\")\n",
|
153 |
+
" \n",
|
154 |
+
" self.api_url = \"https://openrouter.ai/api/v1/chat/completions\"\n",
|
155 |
+
" self.model = model\n",
|
156 |
+
" \n",
|
157 |
+
" self.learning_context = LearningContext(context_path)\n",
|
158 |
+
" \n",
|
159 |
+
" # Track current discussion\n",
|
160 |
+
" self.current_discussion: List[Dict] = []\n",
|
161 |
+
" \n",
|
162 |
+
" logger.info(\"Clinical tutor initialized\")\n",
|
163 |
+
" \n",
|
164 |
+
" async def generate_smart_goals(\n",
|
165 |
+
" self,\n",
|
166 |
+
" specialty: str,\n",
|
167 |
+
" setting: str,\n",
|
168 |
+
" num_goals: int = 3\n",
|
169 |
+
" ) -> List[SmartGoal]:\n",
|
170 |
+
" \"\"\"\n",
|
171 |
+
" Generate SMART goals for rotation context.\n",
|
172 |
+
" \n",
|
173 |
+
" Args:\n",
|
174 |
+
" specialty: Medical specialty\n",
|
175 |
+
" setting: Clinical setting\n",
|
176 |
+
" num_goals: Number of goals to generate\n",
|
177 |
+
" \n",
|
178 |
+
" Returns:\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 |
+
"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",
|
196 |
+
" \"content\": prompt\n",
|
197 |
+
" }])\n",
|
198 |
+
" \n",
|
199 |
+
" # Parse response to extract goals\n",
|
200 |
+
" goals_data = json.loads(response)\n",
|
201 |
+
" \n",
|
202 |
+
" # Convert to SmartGoal objects\n",
|
203 |
+
" goals = []\n",
|
204 |
+
" for data in goals_data:\n",
|
205 |
+
" goal = SmartGoal(\n",
|
206 |
+
" id=f\"goal_{uuid.uuid4()}\",\n",
|
207 |
+
" category=LearningCategory(data[\"category\"]),\n",
|
208 |
+
" original_input=\"\", # Auto-generated\n",
|
209 |
+
" smart_version=data[\"smart_version\"],\n",
|
210 |
+
" specialty=specialty,\n",
|
211 |
+
" setting=setting,\n",
|
212 |
+
" created_at=datetime.now()\n",
|
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",
|
225 |
+
" specialty: str,\n",
|
226 |
+
" setting: str\n",
|
227 |
+
" ) -> Optional[SmartGoal]:\n",
|
228 |
+
" \"\"\"\n",
|
229 |
+
" Generate SMART goal from user input.\n",
|
230 |
+
" \n",
|
231 |
+
" Args:\n",
|
232 |
+
" user_input: User's goal description\n",
|
233 |
+
" specialty: Current specialty\n",
|
234 |
+
" setting: Current setting\n",
|
235 |
+
" \n",
|
236 |
+
" Returns:\n",
|
237 |
+
" SmartGoal: Generated SMART goal\n",
|
238 |
+
" \"\"\"\n",
|
239 |
+
" prompt = f\"\"\"Convert this learning goal into a SMART goal (Specific, Measurable, Achievable, Relevant, Time-bound) for {specialty} ({setting}):\n",
|
240 |
+
"\n",
|
241 |
+
"\"{user_input}\"\n",
|
242 |
+
"\n",
|
243 |
+
"1. Select the most appropriate category from: {', '.join(cat.value for cat in LearningCategory)}\n",
|
244 |
+
"2. Rewrite as a SMART goal specific to {setting} in {specialty}\n",
|
245 |
+
"\n",
|
246 |
+
"Format as JSON with fields:\n",
|
247 |
+
"- category: Learning category name\n",
|
248 |
+
"- smart_version: SMART formatted goal text\"\"\"\n",
|
249 |
+
"\n",
|
250 |
+
" try:\n",
|
251 |
+
" response = await self._get_completion([{\n",
|
252 |
+
" \"role\": \"system\",\n",
|
253 |
+
" \"content\": prompt\n",
|
254 |
+
" }])\n",
|
255 |
+
" \n",
|
256 |
+
" # Parse response\n",
|
257 |
+
" data = json.loads(response)\n",
|
258 |
+
" \n",
|
259 |
+
" return SmartGoal(\n",
|
260 |
+
" id=f\"goal_{uuid.uuid4()}\",\n",
|
261 |
+
" category=LearningCategory(data[\"category\"]),\n",
|
262 |
+
" original_input=user_input,\n",
|
263 |
+
" smart_version=data[\"smart_version\"],\n",
|
264 |
+
" specialty=specialty,\n",
|
265 |
+
" setting=setting,\n",
|
266 |
+
" created_at=datetime.now()\n",
|
267 |
+
" )\n",
|
268 |
+
" \n",
|
269 |
+
" except Exception as e:\n",
|
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",
|
285 |
+
" self.current_discussion.append({\n",
|
286 |
+
" \"role\": \"user\",\n",
|
287 |
+
" \"content\": message\n",
|
288 |
+
" })\n",
|
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",
|
316 |
+
" if self.learning_context.active_goal:\n",
|
317 |
+
" self.learning_context.complete_active_goal()\n",
|
318 |
+
" \n",
|
319 |
+
" self.current_discussion = []\n",
|
320 |
+
" \n",
|
321 |
+
" def _build_discussion_prompt(self) -> str:\n",
|
322 |
+
" \"\"\"Build context-aware discussion prompt.\"\"\"\n",
|
323 |
+
" context = self.learning_context\n",
|
324 |
+
" rotation = context.rotation\n",
|
325 |
+
" active_goal = context.active_goal\n",
|
326 |
+
" \n",
|
327 |
+
" return f\"\"\"You are an experienced clinical supervisor in {rotation.specialty} \n",
|
328 |
+
" working in a {rotation.setting} setting. Guide the learner through case discussion\n",
|
329 |
+
" using Socratic questioning and targeted feedback.\n",
|
330 |
+
"\n",
|
331 |
+
" Current Learning Goal:\n",
|
332 |
+
" {active_goal.smart_version if active_goal else 'General clinical discussion'}\n",
|
333 |
+
"\n",
|
334 |
+
" Approach:\n",
|
335 |
+
" 1. Focus on clinical reasoning and decision-making\n",
|
336 |
+
" 2. Ask targeted questions to explore understanding\n",
|
337 |
+
" 3. Share relevant clinical pearls\n",
|
338 |
+
" 4. Be conversational and engaging\n",
|
339 |
+
" 5. Relate discussion to current learning goal where relevant\n",
|
340 |
+
"\n",
|
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",
|
378 |
+
" try:\n",
|
379 |
+
" async with aiohttp.ClientSession() as session:\n",
|
380 |
+
" async with session.post(\n",
|
381 |
+
" self.api_url,\n",
|
382 |
+
" headers=headers,\n",
|
383 |
+
" json=data,\n",
|
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",
|
399 |
+
" \n",
|
400 |
+
" Returns:\n",
|
401 |
+
" list: Discussion messages\n",
|
402 |
+
" \"\"\"\n",
|
403 |
+
" return self.current_discussion\n",
|
404 |
+
" \n",
|
405 |
+
" def clear_discussion(self) -> None:\n",
|
406 |
+
" \"\"\"Clear current discussion history.\"\"\"\n",
|
407 |
+
" self.current_discussion = []"
|
408 |
+
]
|
409 |
+
},
|
410 |
+
{
|
411 |
+
"cell_type": "code",
|
412 |
+
"execution_count": null,
|
413 |
+
"id": "75c2cbfc-75e7-45ee-9d86-b931f81a3ad5",
|
414 |
+
"metadata": {},
|
415 |
+
"outputs": [],
|
416 |
+
"source": [
|
417 |
+
"#| hide\n",
|
418 |
+
"class ClinicalTutor:\n",
|
419 |
+
" \"\"\"\n",
|
420 |
+
" Adaptive clinical teaching module that provides context-aware feedback.\n",
|
421 |
+
" \n",
|
422 |
+
" The tutor acts as an experienced clinical supervisor, engaging in natural\n",
|
423 |
+
" case discussions while tracking student progress and adapting feedback\n",
|
424 |
+
" based on learning context.\n",
|
425 |
+
" \n",
|
426 |
+
" Attributes:\n",
|
427 |
+
" learning_context (LearningContext): Student's learning context\n",
|
428 |
+
" model (str): LLM model identifier\n",
|
429 |
+
" api_url (str): OpenRouter API endpoint\n",
|
430 |
+
" \"\"\"\n",
|
431 |
+
" \n",
|
432 |
+
" def __init__(\n",
|
433 |
+
" self,\n",
|
434 |
+
" context_path: Optional[Path] = None,\n",
|
435 |
+
" model: str = \"anthropic/claude-3.5-sonnet\"\n",
|
436 |
+
" ):\n",
|
437 |
+
" \"\"\"\n",
|
438 |
+
" Initialize clinical tutor.\n",
|
439 |
+
" \n",
|
440 |
+
" Args:\n",
|
441 |
+
" context_path: Optional path for context persistence\n",
|
442 |
+
" model: Model identifier for OpenRouter\n",
|
443 |
+
" \"\"\"\n",
|
444 |
+
" self.api_key: str = os.getenv(\"OPENROUTER_API_KEY\")\n",
|
445 |
+
" if not self.api_key:\n",
|
446 |
+
" raise ValueError(\"OpenRouter API key not found\")\n",
|
447 |
+
" \n",
|
448 |
+
" self.api_url: str = \"https://openrouter.ai/api/v1/chat/completions\"\n",
|
449 |
+
" self.model: str = model\n",
|
450 |
+
" \n",
|
451 |
+
" self.learning_context = LearningContext(context_path)\n",
|
452 |
+
" self.context_path = context_path\n",
|
453 |
+
" \n",
|
454 |
+
" # Track conversation state\n",
|
455 |
+
" self.current_case: Dict = {\n",
|
456 |
+
" \"started\": None,\n",
|
457 |
+
" \"chief_complaint\": None,\n",
|
458 |
+
" \"key_findings\": [],\n",
|
459 |
+
" \"assessment\": None,\n",
|
460 |
+
" \"plan\": None\n",
|
461 |
+
" }\n",
|
462 |
+
" \n",
|
463 |
+
" logger.info(f\"Clinical tutor initialized with model: {model}\")\n",
|
464 |
+
" \n",
|
465 |
+
" async def _get_completion(\n",
|
466 |
+
" self,\n",
|
467 |
+
" messages: List[Dict],\n",
|
468 |
+
" temperature: float = 0.7,\n",
|
469 |
+
" max_retries: int = 3\n",
|
470 |
+
" ) -> str:\n",
|
471 |
+
" \"\"\"\n",
|
472 |
+
" Get completion from OpenRouter API with retry logic.\n",
|
473 |
+
" \n",
|
474 |
+
" Args:\n",
|
475 |
+
" messages: List of conversation messages\n",
|
476 |
+
" temperature: Temperature for response generation\n",
|
477 |
+
" max_retries: Maximum retry attempts\n",
|
478 |
+
" \n",
|
479 |
+
" Returns:\n",
|
480 |
+
" str: Model response\n",
|
481 |
+
" \n",
|
482 |
+
" Raises:\n",
|
483 |
+
" OpenRouterException: If API calls fail after retries\n",
|
484 |
+
" \"\"\"\n",
|
485 |
+
" headers = {\n",
|
486 |
+
" \"Authorization\": f\"Bearer {self.api_key}\",\n",
|
487 |
+
" \"Content-Type\": \"application/json\",\n",
|
488 |
+
" \"HTTP-Referer\": \"http://localhost:7860\"\n",
|
489 |
+
" }\n",
|
490 |
+
" \n",
|
491 |
+
" data = {\n",
|
492 |
+
" \"model\": self.model,\n",
|
493 |
+
" \"messages\": messages,\n",
|
494 |
+
" \"temperature\": temperature,\n",
|
495 |
+
" \"max_tokens\": 2000\n",
|
496 |
+
" }\n",
|
497 |
+
" \n",
|
498 |
+
" for attempt in range(max_retries):\n",
|
499 |
+
" try:\n",
|
500 |
+
" async with aiohttp.ClientSession() as session:\n",
|
501 |
+
" async with session.post(\n",
|
502 |
+
" self.api_url,\n",
|
503 |
+
" headers=headers,\n",
|
504 |
+
" json=data,\n",
|
505 |
+
" timeout=30\n",
|
506 |
+
" ) as response:\n",
|
507 |
+
" response.raise_for_status()\n",
|
508 |
+
" result = await response.json()\n",
|
509 |
+
" return result[\"choices\"][0][\"message\"][\"content\"]\n",
|
510 |
+
" \n",
|
511 |
+
" except Exception as e:\n",
|
512 |
+
" if attempt == max_retries - 1:\n",
|
513 |
+
" raise OpenRouterException(f\"API call failed: {str(e)}\")\n",
|
514 |
+
" logger.warning(f\"Retry {attempt + 1} after error: {str(e)}\")\n",
|
515 |
+
" # Could add exponential backoff here if needed\n",
|
516 |
+
" \n",
|
517 |
+
" def _build_discussion_prompt(self) -> str:\n",
|
518 |
+
" \"\"\"Build context-aware prompt for case discussion.\"\"\"\n",
|
519 |
+
" rotation = self.learning_context.current_rotation\n",
|
520 |
+
" active_preferences = [\n",
|
521 |
+
" p[\"focus\"] for p in self.learning_context.feedback_preferences \n",
|
522 |
+
" if p[\"active\"]\n",
|
523 |
+
" ]\n",
|
524 |
+
" \n",
|
525 |
+
" significant_gaps = {\n",
|
526 |
+
" topic: score for topic, score \n",
|
527 |
+
" in self.learning_context.knowledge_profile[\"gaps\"].items()\n",
|
528 |
+
" if score < 0.7 # Only include significant gaps\n",
|
529 |
+
" }\n",
|
530 |
+
" \n",
|
531 |
+
" prompt = f\"\"\"You are an experienced clinical supervisor in {rotation['specialty']}. Act as an engaging and conversational tutor who coaches towards deeper understanding through Socratic dialogue and targeted questions.\n",
|
532 |
+
"\n",
|
533 |
+
" Key Principles:\n",
|
534 |
+
" 1. Assume I have strong foundational knowledge in medicine, clinical reasoning, and pre-medical sciences\n",
|
535 |
+
" 2. Focus on high-level connections and nuanced clinical decision-making\n",
|
536 |
+
" 3. Use targeted questions to explore my thought process and highlight key learning points\n",
|
537 |
+
" 4. Share relevant clinical pearls and real-world applications\n",
|
538 |
+
" 5. Be conversational and engaging, avoiding lecture-style responses\n",
|
539 |
+
" \n",
|
540 |
+
" Current Rotation Focus Areas:\n",
|
541 |
+
" {', '.join(rotation['key_focus_areas'])}\n",
|
542 |
+
"\n",
|
543 |
+
" Areas for Deep Dive:\n",
|
544 |
+
" {', '.join(f'{topic} (confidence: {score:.1f})' for topic, score in significant_gaps.items()) if significant_gaps else 'General clinical reasoning'}\n",
|
545 |
+
"\n",
|
546 |
+
" Student's Interests:\n",
|
547 |
+
" {', '.join(active_preferences) if active_preferences else 'Broad clinical discussion'}\n",
|
548 |
+
"\n",
|
549 |
+
" Ask probing questions that explore clinical reasoning and highlight important connections. I will ask for clarification \n",
|
550 |
+
" if concepts need more explanation.\"\"\"\n",
|
551 |
+
"\n",
|
552 |
+
" return prompt\n",
|
553 |
+
" \n",
|
554 |
+
" def _build_analysis_prompt(self, conversation: List[Dict[str, str]]) -> str:\n",
|
555 |
+
" \"\"\"\n",
|
556 |
+
" Build prompt for post-discussion analysis.\n",
|
557 |
+
" \n",
|
558 |
+
" Args:\n",
|
559 |
+
" conversation: List of message dictionaries with roles and content\n",
|
560 |
+
" \n",
|
561 |
+
" Returns:\n",
|
562 |
+
" str: Analysis prompt\n",
|
563 |
+
" \"\"\"\n",
|
564 |
+
" # Extract case details\n",
|
565 |
+
" case_content = \"\"\n",
|
566 |
+
" for msg in conversation:\n",
|
567 |
+
" if msg[\"role\"] == \"user\":\n",
|
568 |
+
" case_content += msg[\"content\"] + \"\\n\"\n",
|
569 |
+
" \n",
|
570 |
+
" return f\"\"\"Analyze the following case discussion between a medical student and \n",
|
571 |
+
" clinical supervisor. Focus on the student's demonstrated knowledge, skills, \n",
|
572 |
+
" and areas for improvement.\n",
|
573 |
+
"\n",
|
574 |
+
" Case Content:\n",
|
575 |
+
" {case_content}\n",
|
576 |
+
"\n",
|
577 |
+
" Please identify:\n",
|
578 |
+
" 1. Key clinical concepts and learning points demonstrated or discussed\n",
|
579 |
+
" 2. Areas where the student showed uncertainty or knowledge gaps\n",
|
580 |
+
" 3. Strengths demonstrated in clinical reasoning and presentation\n",
|
581 |
+
" 4. Specific learning objectives that would help the student's development\n",
|
582 |
+
"\n",
|
583 |
+
" Frame your response to help with ongoing learning:\n",
|
584 |
+
" - Start with positive observations\n",
|
585 |
+
" - Be specific about knowledge gaps\n",
|
586 |
+
" - Make concrete suggestions for improvement\n",
|
587 |
+
" - Connect to practical clinical scenarios\"\"\"\n",
|
588 |
+
" \n",
|
589 |
+
" async def discuss_case(\n",
|
590 |
+
" self, \n",
|
591 |
+
" message: str,\n",
|
592 |
+
" temperature: float = 0.7\n",
|
593 |
+
" ) -> str:\n",
|
594 |
+
" \"\"\"\n",
|
595 |
+
" Natural case discussion with context-aware responses.\n",
|
596 |
+
" \n",
|
597 |
+
" Args:\n",
|
598 |
+
" message: Student's input message\n",
|
599 |
+
" temperature: Temperature for response generation\n",
|
600 |
+
" \n",
|
601 |
+
" Returns:\n",
|
602 |
+
" str: Clinical supervisor's response\n",
|
603 |
+
" \"\"\"\n",
|
604 |
+
" try:\n",
|
605 |
+
" # Update case tracking\n",
|
606 |
+
" if not self.current_case[\"started\"]:\n",
|
607 |
+
" self.current_case[\"started\"] = datetime.now()\n",
|
608 |
+
" # Try to identify chief complaint from first message\n",
|
609 |
+
" cc_match = re.search(r\"(\\d+)\\s*[yY][oO]\\s*[MmFf]\\s*with\\s*([^.]*)\", message)\n",
|
610 |
+
" if cc_match:\n",
|
611 |
+
" self.current_case[\"chief_complaint\"] = cc_match.group(2).strip()\n",
|
612 |
+
" \n",
|
613 |
+
" # Build system prompt\n",
|
614 |
+
" system_prompt = self._build_discussion_prompt()\n",
|
615 |
+
" \n",
|
616 |
+
" messages = [{\n",
|
617 |
+
" \"role\": \"system\",\n",
|
618 |
+
" \"content\": system_prompt\n",
|
619 |
+
" }, {\n",
|
620 |
+
" \"role\": \"user\",\n",
|
621 |
+
" \"content\": message\n",
|
622 |
+
" }]\n",
|
623 |
+
" \n",
|
624 |
+
" response = await self._get_completion(messages, temperature)\n",
|
625 |
+
" return response\n",
|
626 |
+
" \n",
|
627 |
+
" except Exception as e:\n",
|
628 |
+
" logger.error(f\"Error in case discussion: {str(e)}\")\n",
|
629 |
+
" return \"I apologize, but I encountered an error. Please try presenting your case again.\"\n",
|
630 |
+
" \n",
|
631 |
+
" async def analyze_discussion(\n",
|
632 |
+
" self,\n",
|
633 |
+
" conversation: List[Dict[str, str]]\n",
|
634 |
+
" ) -> Dict[str, Any]:\n",
|
635 |
+
" \"\"\"\n",
|
636 |
+
" Analyze completed case discussion for learning insights.\n",
|
637 |
+
" \n",
|
638 |
+
" Args:\n",
|
639 |
+
" conversation: List of message dictionaries with roles and content\n",
|
640 |
+
" \n",
|
641 |
+
" Returns:\n",
|
642 |
+
" dict: Analysis results containing:\n",
|
643 |
+
" - learning_points: List of key concepts learned\n",
|
644 |
+
" - gaps: Dict of identified knowledge gaps\n",
|
645 |
+
" - strengths: List of demonstrated strengths\n",
|
646 |
+
" - suggested_objectives: List of recommended learning goals\n",
|
647 |
+
" \"\"\"\n",
|
648 |
+
" try:\n",
|
649 |
+
" # Reset case tracking\n",
|
650 |
+
" self.current_case = {\n",
|
651 |
+
" \"started\": None,\n",
|
652 |
+
" \"chief_complaint\": None,\n",
|
653 |
+
" \"key_findings\": [],\n",
|
654 |
+
" \"assessment\": None,\n",
|
655 |
+
" \"plan\": None\n",
|
656 |
+
" }\n",
|
657 |
+
" \n",
|
658 |
+
" # Get analysis from model\n",
|
659 |
+
" analysis_prompt = self._build_analysis_prompt(conversation)\n",
|
660 |
+
" messages = [{\n",
|
661 |
+
" \"role\": \"system\",\n",
|
662 |
+
" \"content\": analysis_prompt\n",
|
663 |
+
" }]\n",
|
664 |
+
" messages.extend(conversation)\n",
|
665 |
+
" \n",
|
666 |
+
" response = await self._get_completion(messages, temperature=0.3)\n",
|
667 |
+
" \n",
|
668 |
+
" # Parse insights\n",
|
669 |
+
" insights = self._parse_analysis(response)\n",
|
670 |
+
" \n",
|
671 |
+
" # Update learning context\n",
|
672 |
+
" self._update_context_from_analysis(insights)\n",
|
673 |
+
" \n",
|
674 |
+
" return insights\n",
|
675 |
+
" \n",
|
676 |
+
" except Exception as e:\n",
|
677 |
+
" logger.error(f\"Error in discussion analysis: {str(e)}\")\n",
|
678 |
+
" return {\n",
|
679 |
+
" \"learning_points\": [],\n",
|
680 |
+
" \"gaps\": {},\n",
|
681 |
+
" \"strengths\": [],\n",
|
682 |
+
" \"suggested_objectives\": []\n",
|
683 |
+
" }\n",
|
684 |
+
" \n",
|
685 |
+
" def _parse_analysis(self, response: str) -> Dict[str, Any]:\n",
|
686 |
+
" \"\"\"\n",
|
687 |
+
" Parse analysis response into structured insights.\n",
|
688 |
+
" \n",
|
689 |
+
" Uses pattern matching and basic NLP to extract:\n",
|
690 |
+
" - Learning points (key concepts discussed)\n",
|
691 |
+
" - Knowledge gaps with confidence estimates\n",
|
692 |
+
" - Demonstrated strengths\n",
|
693 |
+
" - Suggested learning objectives\n",
|
694 |
+
" \n",
|
695 |
+
" Args:\n",
|
696 |
+
" response: Raw analysis response\n",
|
697 |
+
" \n",
|
698 |
+
" Returns:\n",
|
699 |
+
" dict: Structured analysis insights\n",
|
700 |
+
" \"\"\"\n",
|
701 |
+
" insights = {\n",
|
702 |
+
" \"learning_points\": [],\n",
|
703 |
+
" \"gaps\": {},\n",
|
704 |
+
" \"strengths\": [],\n",
|
705 |
+
" \"suggested_objectives\": []\n",
|
706 |
+
" }\n",
|
707 |
+
" \n",
|
708 |
+
" try:\n",
|
709 |
+
" # Split into sections\n",
|
710 |
+
" sections = response.lower().split(\"\\n\\n\")\n",
|
711 |
+
" \n",
|
712 |
+
" for section in sections:\n",
|
713 |
+
" if \"learning point\" in section or \"key concept\" in section:\n",
|
714 |
+
" # Extract bullet points or numbered items\n",
|
715 |
+
" points = re.findall(r\"[-•*]\\s*(.+)$\", section, re.MULTILINE)\n",
|
716 |
+
" insights[\"learning_points\"].extend(points)\n",
|
717 |
+
" \n",
|
718 |
+
" elif \"gap\" in section or \"uncertainty\" in section:\n",
|
719 |
+
" # Look for topic mentions with confidence indicators\n",
|
720 |
+
" gaps = re.findall(\n",
|
721 |
+
" r\"(limited|uncertain|unclear|difficulty with)\\s+([^,.]+)\", \n",
|
722 |
+
" section\n",
|
723 |
+
" )\n",
|
724 |
+
" for indicator, topic in gaps:\n",
|
725 |
+
" # Estimate confidence based on language\n",
|
726 |
+
" confidence = 0.4 if \"limited\" in indicator else 0.6\n",
|
727 |
+
" insights[\"gaps\"][topic.strip()] = confidence\n",
|
728 |
+
" \n",
|
729 |
+
" elif \"strength\" in section or \"demonstrated\" in section:\n",
|
730 |
+
" # Extract positive mentions\n",
|
731 |
+
" strengths = re.findall(r\"[-•*]\\s*(.+)$\", section, re.MULTILINE)\n",
|
732 |
+
" insights[\"strengths\"].extend(strengths)\n",
|
733 |
+
" \n",
|
734 |
+
" elif \"objective\" in section or \"suggest\" in section:\n",
|
735 |
+
" # Extract recommended objectives\n",
|
736 |
+
" objectives = re.findall(r\"[-•*]\\s*(.+)$\", section, re.MULTILINE)\n",
|
737 |
+
" insights[\"suggested_objectives\"].extend(objectives)\n",
|
738 |
+
" \n",
|
739 |
+
" return insights\n",
|
740 |
+
" \n",
|
741 |
+
" except Exception as e:\n",
|
742 |
+
" logger.error(f\"Error parsing analysis: {str(e)}\")\n",
|
743 |
+
" return insights\n",
|
744 |
+
" \n",
|
745 |
+
" def _update_context_from_analysis(self, insights: Dict[str, Any]) -> None:\n",
|
746 |
+
" \"\"\"\n",
|
747 |
+
" Update learning context based on discussion analysis.\n",
|
748 |
+
" \n",
|
749 |
+
" Args:\n",
|
750 |
+
" insights: Dictionary of analysis insights\n",
|
751 |
+
" \"\"\"\n",
|
752 |
+
" try:\n",
|
753 |
+
" # Update knowledge gaps\n",
|
754 |
+
" for topic, confidence in insights[\"gaps\"].items():\n",
|
755 |
+
" self.learning_context.update_knowledge_gap(topic, confidence)\n",
|
756 |
+
" \n",
|
757 |
+
" # Add strengths\n",
|
758 |
+
" for strength in insights[\"strengths\"]:\n",
|
759 |
+
" self.learning_context.add_strength(strength)\n",
|
760 |
+
" \n",
|
761 |
+
" # Save context if path provided\n",
|
762 |
+
" if self.context_path:\n",
|
763 |
+
" self.learning_context.save_context(self.context_path)\n",
|
764 |
+
" \n",
|
765 |
+
" except Exception as e:\n",
|
766 |
+
" logger.error(f\"Error updating context: {str(e)}\")"
|
767 |
+
]
|
768 |
+
},
|
769 |
+
{
|
770 |
+
"cell_type": "markdown",
|
771 |
+
"id": "6a2b15f5-6841-43cb-9b57-c0e3f1a0b0c2",
|
772 |
+
"metadata": {},
|
773 |
+
"source": [
|
774 |
+
"## Tests"
|
775 |
+
]
|
776 |
+
},
|
777 |
+
{
|
778 |
+
"cell_type": "code",
|
779 |
+
"execution_count": null,
|
780 |
+
"id": "67ee6bde-4ade-448e-a831-86f9f7ae82ea",
|
781 |
+
"metadata": {},
|
782 |
+
"outputs": [
|
783 |
+
{
|
784 |
+
"ename": "RuntimeError",
|
785 |
+
"evalue": "asyncio.run() cannot be called from a running event loop",
|
786 |
+
"output_type": "error",
|
787 |
+
"traceback": [
|
788 |
+
"\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
|
789 |
+
"\u001b[1;31mRuntimeError\u001b[0m Traceback (most recent call last)",
|
790 |
+
"Cell \u001b[1;32mIn[7], line 55\u001b[0m\n\u001b[0;32m 53\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;18m__name__\u001b[39m \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m__main__\u001b[39m\u001b[38;5;124m\"\u001b[39m:\n\u001b[0;32m 54\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01masyncio\u001b[39;00m\n\u001b[1;32m---> 55\u001b[0m \u001b[43masyncio\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mrun\u001b[49m\u001b[43m(\u001b[49m\u001b[43mtest_clinical_tutor\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[43m)\u001b[49m\n",
|
791 |
+
"File \u001b[1;32m~\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\asyncio\\runners.py:190\u001b[0m, in \u001b[0;36mrun\u001b[1;34m(main, debug, loop_factory)\u001b[0m\n\u001b[0;32m 161\u001b[0m \u001b[38;5;250m\u001b[39m\u001b[38;5;124;03m\"\"\"Execute the coroutine and return the result.\u001b[39;00m\n\u001b[0;32m 162\u001b[0m \n\u001b[0;32m 163\u001b[0m \u001b[38;5;124;03mThis function runs the passed coroutine, taking care of\u001b[39;00m\n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 186\u001b[0m \u001b[38;5;124;03m asyncio.run(main())\u001b[39;00m\n\u001b[0;32m 187\u001b[0m \u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[0;32m 188\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m events\u001b[38;5;241m.\u001b[39m_get_running_loop() \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[0;32m 189\u001b[0m \u001b[38;5;66;03m# fail fast with short traceback\u001b[39;00m\n\u001b[1;32m--> 190\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mRuntimeError\u001b[39;00m(\n\u001b[0;32m 191\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124masyncio.run() cannot be called from a running event loop\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m 193\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m Runner(debug\u001b[38;5;241m=\u001b[39mdebug, loop_factory\u001b[38;5;241m=\u001b[39mloop_factory) \u001b[38;5;28;01mas\u001b[39;00m runner:\n\u001b[0;32m 194\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m runner\u001b[38;5;241m.\u001b[39mrun(main)\n",
|
792 |
+
"\u001b[1;31mRuntimeError\u001b[0m: asyncio.run() cannot be called from a running event loop"
|
793 |
+
]
|
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",
|
807 |
+
" \n",
|
808 |
+
" # Initialize tutor\n",
|
809 |
+
" tutor = ClinicalTutor()\n",
|
810 |
+
" \n",
|
811 |
+
" # Test case content\n",
|
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 |
+
{
|
840 |
+
"cell_type": "code",
|
841 |
+
"execution_count": null,
|
842 |
+
"id": "3f469c37-afe3-4682-9cc4-40326ac21b74",
|
843 |
+
"metadata": {},
|
844 |
+
"outputs": [],
|
845 |
+
"source": []
|
846 |
+
}
|
847 |
+
],
|
848 |
+
"metadata": {
|
849 |
+
"kernelspec": {
|
850 |
+
"display_name": "python3",
|
851 |
+
"language": "python",
|
852 |
+
"name": "python3"
|
853 |
+
}
|
854 |
+
},
|
855 |
+
"nbformat": 4,
|
856 |
+
"nbformat_minor": 5
|
857 |
+
}
|
nbs/02_learning_interface.ipynb
ADDED
@@ -0,0 +1,1325 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"cells": [
|
3 |
+
{
|
4 |
+
"cell_type": "code",
|
5 |
+
"execution_count": null,
|
6 |
+
"id": "7ce0a47c-1c4f-44a4-a9d8-9ea6399a8f84",
|
7 |
+
"metadata": {},
|
8 |
+
"outputs": [],
|
9 |
+
"source": [
|
10 |
+
"#| default_exp learning_interface"
|
11 |
+
]
|
12 |
+
},
|
13 |
+
{
|
14 |
+
"cell_type": "markdown",
|
15 |
+
"id": "55331735-898e-411b-b751-5b380605be36",
|
16 |
+
"metadata": {},
|
17 |
+
"source": [
|
18 |
+
"# Learning Interface\n",
|
19 |
+
"\n",
|
20 |
+
"> Gradio interface"
|
21 |
+
]
|
22 |
+
},
|
23 |
+
{
|
24 |
+
"cell_type": "markdown",
|
25 |
+
"id": "dd401991-b919-423e-9da7-961387faf11e",
|
26 |
+
"metadata": {},
|
27 |
+
"source": [
|
28 |
+
"## Setup"
|
29 |
+
]
|
30 |
+
},
|
31 |
+
{
|
32 |
+
"cell_type": "code",
|
33 |
+
"execution_count": null,
|
34 |
+
"id": "edc3fbb1-13ef-408a-b5fe-eb7a6821915b",
|
35 |
+
"metadata": {},
|
36 |
+
"outputs": [],
|
37 |
+
"source": [
|
38 |
+
"#| hide\n",
|
39 |
+
"from nbdev.showdoc import *"
|
40 |
+
]
|
41 |
+
},
|
42 |
+
{
|
43 |
+
"cell_type": "code",
|
44 |
+
"execution_count": null,
|
45 |
+
"id": "4be213cf-89b4-48c8-9592-f509332da485",
|
46 |
+
"metadata": {},
|
47 |
+
"outputs": [
|
48 |
+
{
|
49 |
+
"ename": "ImportError",
|
50 |
+
"evalue": "cannot import name 'ClinicalTutor' from 'wardbuddy.clinical_tutor' (C:\\Users\\deepa\\OneDrive\\Documents\\StudyBuddy\\wardbuddy\\wardbuddy\\clinical_tutor.py)",
|
51 |
+
"output_type": "error",
|
52 |
+
"traceback": [
|
53 |
+
"\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
|
54 |
+
"\u001b[1;31mImportError\u001b[0m Traceback (most recent call last)",
|
55 |
+
"Cell \u001b[1;32mIn[4], line 7\u001b[0m\n\u001b[0;32m 5\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mpathlib\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m Path\n\u001b[0;32m 6\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01masyncio\u001b[39;00m\n\u001b[1;32m----> 7\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mwardbuddy\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mclinical_tutor\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m ClinicalTutor\n\u001b[0;32m 8\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mwardbuddy\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mutils\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m format_response\n\u001b[0;32m 9\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mwardbuddy\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mlearning_context\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m setup_logger\n",
|
56 |
+
"\u001b[1;31mImportError\u001b[0m: cannot import name 'ClinicalTutor' from 'wardbuddy.clinical_tutor' (C:\\Users\\deepa\\OneDrive\\Documents\\StudyBuddy\\wardbuddy\\wardbuddy\\clinical_tutor.py)"
|
57 |
+
]
|
58 |
+
}
|
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",
|
66 |
+
"from datetime import datetime\n",
|
67 |
+
"import pandas as pd\n",
|
68 |
+
"from wardbuddy.clinical_tutor import ClinicalTutor\n",
|
69 |
+
"from wardbuddy.learning_context import setup_logger, LearningCategory, SmartGoal\n",
|
70 |
+
"import json\n",
|
71 |
+
"\n",
|
72 |
+
"logger = setup_logger(__name__)"
|
73 |
+
]
|
74 |
+
},
|
75 |
+
{
|
76 |
+
"cell_type": "markdown",
|
77 |
+
"id": "c39da1db-e630-4296-93f6-03b9188320cc",
|
78 |
+
"metadata": {},
|
79 |
+
"source": [
|
80 |
+
"## Learning Interface"
|
81 |
+
]
|
82 |
+
},
|
83 |
+
{
|
84 |
+
"cell_type": "code",
|
85 |
+
"execution_count": null,
|
86 |
+
"id": "aff3d321-2116-475f-b906-f74889e76d66",
|
87 |
+
"metadata": {},
|
88 |
+
"outputs": [],
|
89 |
+
"source": [
|
90 |
+
"#| export\n",
|
91 |
+
"def create_css() -> str:\n",
|
92 |
+
" \"\"\"Create custom CSS for interface styling.\"\"\"\n",
|
93 |
+
" return \"\"\"\n",
|
94 |
+
" .gradio-container {\n",
|
95 |
+
" background-color: #0f172a !important;\n",
|
96 |
+
" }\n",
|
97 |
+
" \n",
|
98 |
+
" .chat-message {\n",
|
99 |
+
" background-color: #1e293b !important;\n",
|
100 |
+
" border: 1px solid #334155 !important;\n",
|
101 |
+
" border-radius: 0.5rem !important;\n",
|
102 |
+
" padding: 1rem !important;\n",
|
103 |
+
" margin: 0.5rem 0 !important;\n",
|
104 |
+
" }\n",
|
105 |
+
" \n",
|
106 |
+
" .user-message {\n",
|
107 |
+
" background-color: #2563eb !important;\n",
|
108 |
+
" }\n",
|
109 |
+
" \n",
|
110 |
+
" textarea, select {\n",
|
111 |
+
" background-color: #1e293b !important;\n",
|
112 |
+
" border: 1px solid #334155 !important;\n",
|
113 |
+
" color: #f1f5f9 !important;\n",
|
114 |
+
" }\n",
|
115 |
+
" \n",
|
116 |
+
" button {\n",
|
117 |
+
" background-color: #2563eb !important;\n",
|
118 |
+
" color: white !important;\n",
|
119 |
+
" }\n",
|
120 |
+
" \n",
|
121 |
+
" button:hover {\n",
|
122 |
+
" background-color: #1d4ed8 !important;\n",
|
123 |
+
" }\n",
|
124 |
+
" \"\"\""
|
125 |
+
]
|
126 |
+
},
|
127 |
+
{
|
128 |
+
"cell_type": "markdown",
|
129 |
+
"id": "de7ace04-4841-461d-89bb-234b5f8b48e1",
|
130 |
+
"metadata": {},
|
131 |
+
"source": [
|
132 |
+
"This module provides the user interface for the clinical learning system, including:\n",
|
133 |
+
" * Case presentation and feedback\n",
|
134 |
+
" * Learning preference configuration\n",
|
135 |
+
" * Session management\n",
|
136 |
+
" * Progress visualization"
|
137 |
+
]
|
138 |
+
},
|
139 |
+
{
|
140 |
+
"cell_type": "code",
|
141 |
+
"execution_count": null,
|
142 |
+
"id": "f6edc027-8625-4856-94ce-8f1a0acd4c8f",
|
143 |
+
"metadata": {},
|
144 |
+
"outputs": [],
|
145 |
+
"source": [
|
146 |
+
"#| export\n",
|
147 |
+
"class LearningInterface:\n",
|
148 |
+
" \"\"\"\n",
|
149 |
+
" Gradio interface for clinical learning system.\n",
|
150 |
+
" \n",
|
151 |
+
" Features:\n",
|
152 |
+
" - Context selection\n",
|
153 |
+
" - Goal management\n",
|
154 |
+
" - Case discussion\n",
|
155 |
+
" - Progress tracking\n",
|
156 |
+
" \"\"\"\n",
|
157 |
+
" \n",
|
158 |
+
" def __init__(\n",
|
159 |
+
" self,\n",
|
160 |
+
" context_path: Optional[Path] = None,\n",
|
161 |
+
" model: str = \"anthropic/claude-3.5-sonnet\"\n",
|
162 |
+
" ):\n",
|
163 |
+
" \"\"\"\n",
|
164 |
+
" Initialize interface.\n",
|
165 |
+
" \n",
|
166 |
+
" Args:\n",
|
167 |
+
" context_path: Path for context persistence\n",
|
168 |
+
" model: OpenRouter model identifier\n",
|
169 |
+
" \"\"\"\n",
|
170 |
+
" self.tutor = ClinicalTutor(context_path, model)\n",
|
171 |
+
" \n",
|
172 |
+
" # Available options\n",
|
173 |
+
" self.specialties = [\n",
|
174 |
+
" \"Internal Medicine\",\n",
|
175 |
+
" \"Emergency Medicine\",\n",
|
176 |
+
" \"Surgery\",\n",
|
177 |
+
" \"Pediatrics\",\n",
|
178 |
+
" \"Family Medicine\"\n",
|
179 |
+
" ]\n",
|
180 |
+
" \n",
|
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",
|
187 |
+
" \n",
|
188 |
+
" with gr.Blocks(\n",
|
189 |
+
" title=\"Clinical Learning Assistant\",\n",
|
190 |
+
" css=create_css()\n",
|
191 |
+
" ) as interface:\n",
|
192 |
+
" # State management\n",
|
193 |
+
" state = gr.State({\n",
|
194 |
+
" \"discussion_active\": False,\n",
|
195 |
+
" \"suggested_goals\": [] # Store generated goals\n",
|
196 |
+
" })\n",
|
197 |
+
" \n",
|
198 |
+
" # Header\n",
|
199 |
+
" gr.Markdown(\"# Clinical Learning Assistant\")\n",
|
200 |
+
" \n",
|
201 |
+
" with gr.Row():\n",
|
202 |
+
" # Context selection\n",
|
203 |
+
" specialty = gr.Dropdown(\n",
|
204 |
+
" choices=self.specialties,\n",
|
205 |
+
" label=\"Specialty\",\n",
|
206 |
+
" value=self.tutor.learning_context.rotation.specialty or None\n",
|
207 |
+
" )\n",
|
208 |
+
" \n",
|
209 |
+
" setting = gr.Dropdown(\n",
|
210 |
+
" choices=self.settings,\n",
|
211 |
+
" label=\"Setting\",\n",
|
212 |
+
" value=self.tutor.learning_context.rotation.setting or None\n",
|
213 |
+
" )\n",
|
214 |
+
" \n",
|
215 |
+
" # Main content\n",
|
216 |
+
" with gr.Row():\n",
|
217 |
+
" # Left: Discussion interface\n",
|
218 |
+
" with gr.Column(scale=2):\n",
|
219 |
+
" # Active goal display\n",
|
220 |
+
" goal_display = gr.Markdown(\n",
|
221 |
+
" value=\"No active learning goal\"\n",
|
222 |
+
" )\n",
|
223 |
+
" \n",
|
224 |
+
" # Chat interface\n",
|
225 |
+
" chatbot = gr.Chatbot(\n",
|
226 |
+
" height=400,\n",
|
227 |
+
" show_label=False\n",
|
228 |
+
" )\n",
|
229 |
+
" \n",
|
230 |
+
" with gr.Row():\n",
|
231 |
+
" message = gr.Textbox(\n",
|
232 |
+
" label=\"Present your case or ask questions\",\n",
|
233 |
+
" placeholder=\"Present your case as you would to your supervisor...\",\n",
|
234 |
+
" lines=4\n",
|
235 |
+
" )\n",
|
236 |
+
" \n",
|
237 |
+
" audio_input = gr.Audio(\n",
|
238 |
+
" source=\"microphone\",\n",
|
239 |
+
" type=\"numpy\",\n",
|
240 |
+
" label=\"Or speak your case\",\n",
|
241 |
+
" streaming=True\n",
|
242 |
+
" )\n",
|
243 |
+
" \n",
|
244 |
+
" with gr.Row():\n",
|
245 |
+
" clear = gr.Button(\"Clear Discussion\")\n",
|
246 |
+
" end = gr.Button(\n",
|
247 |
+
" \"End Discussion & Review\",\n",
|
248 |
+
" variant=\"primary\"\n",
|
249 |
+
" )\n",
|
250 |
+
" \n",
|
251 |
+
" # Right: Progress & Goals\n",
|
252 |
+
" with gr.Column(scale=1):\n",
|
253 |
+
" with gr.Tab(\"Learning Goals\"):\n",
|
254 |
+
" # Goal selection/generation\n",
|
255 |
+
" with gr.Row():\n",
|
256 |
+
" generate = gr.Button(\"Generate New Goals\")\n",
|
257 |
+
" \n",
|
258 |
+
" new_goal = gr.Textbox(\n",
|
259 |
+
" label=\"Or enter your own goal\",\n",
|
260 |
+
" placeholder=\"What do you want to get better at?\"\n",
|
261 |
+
" )\n",
|
262 |
+
" \n",
|
263 |
+
" add_goal = gr.Button(\"Add Goal\")\n",
|
264 |
+
" \n",
|
265 |
+
" # Goals list\n",
|
266 |
+
" goals_list = gr.DataFrame(\n",
|
267 |
+
" headers=[\"Goal\", \"Category\", \"Status\"],\n",
|
268 |
+
" label=\"Available Goals\"\n",
|
269 |
+
" )\n",
|
270 |
+
" \n",
|
271 |
+
" with gr.Tab(\"Progress\"):\n",
|
272 |
+
" # Category progress\n",
|
273 |
+
" progress_display = gr.DataFrame(\n",
|
274 |
+
" headers=[\"Category\", \"Completed\", \"Total\"],\n",
|
275 |
+
" label=\"Progress by Category\"\n",
|
276 |
+
" )\n",
|
277 |
+
" \n",
|
278 |
+
" # Recent completions\n",
|
279 |
+
" recent_display = gr.DataFrame(\n",
|
280 |
+
" headers=[\"Goal\", \"Category\", \"Completed\"],\n",
|
281 |
+
" label=\"Recently Completed\"\n",
|
282 |
+
" )\n",
|
283 |
+
" \n",
|
284 |
+
" # Event handlers\n",
|
285 |
+
" async def update_context(spec, set):\n",
|
286 |
+
" \"\"\"Update rotation context.\"\"\"\n",
|
287 |
+
" if spec and set:\n",
|
288 |
+
" self.tutor.learning_context.update_rotation(spec, set)\n",
|
289 |
+
" \n",
|
290 |
+
" # Generate initial goals if needed\n",
|
291 |
+
" if not state[\"suggested_goals\"]:\n",
|
292 |
+
" goals = await self.tutor.generate_smart_goals(spec, set)\n",
|
293 |
+
" state[\"suggested_goals\"] = goals\n",
|
294 |
+
" \n",
|
295 |
+
" return self._update_displays(state)\n",
|
296 |
+
" return []\n",
|
297 |
+
" \n",
|
298 |
+
" async def process_message(msg, history):\n",
|
299 |
+
" \"\"\"Process chat message.\"\"\"\n",
|
300 |
+
" if not msg.strip():\n",
|
301 |
+
" return history, \"\"\n",
|
302 |
+
" \n",
|
303 |
+
" response = await self.tutor.discuss_case(msg)\n",
|
304 |
+
" history.append([msg, response])\n",
|
305 |
+
" return history, \"\"\n",
|
306 |
+
" \n",
|
307 |
+
" def process_audio(audio):\n",
|
308 |
+
" \"\"\"Convert audio to text.\"\"\"\n",
|
309 |
+
" # Would integrate with speech-to-text here\n",
|
310 |
+
" return \"Audio transcription would appear here\"\n",
|
311 |
+
" \n",
|
312 |
+
" async def generate_goals(state):\n",
|
313 |
+
" \"\"\"Generate new goal suggestions.\"\"\"\n",
|
314 |
+
" rotation = self.tutor.learning_context.rotation\n",
|
315 |
+
" if rotation.specialty and rotation.setting:\n",
|
316 |
+
" goals = await self.tutor.generate_smart_goals(\n",
|
317 |
+
" rotation.specialty,\n",
|
318 |
+
" rotation.setting\n",
|
319 |
+
" )\n",
|
320 |
+
" state[\"suggested_goals\"] = goals\n",
|
321 |
+
" return self._update_displays(state)\n",
|
322 |
+
" return []\n",
|
323 |
+
" \n",
|
324 |
+
" async def add_user_goal(text, state):\n",
|
325 |
+
" \"\"\"Add user-specified goal.\"\"\"\n",
|
326 |
+
" if not text.strip():\n",
|
327 |
+
" return state\n",
|
328 |
+
" \n",
|
329 |
+
" rotation = self.tutor.learning_context.rotation\n",
|
330 |
+
" if rotation.specialty and rotation.setting:\n",
|
331 |
+
" goal = await self.tutor.generate_smart_goal(\n",
|
332 |
+
" text,\n",
|
333 |
+
" rotation.specialty,\n",
|
334 |
+
" rotation.setting\n",
|
335 |
+
" )\n",
|
336 |
+
" if goal:\n",
|
337 |
+
" state[\"suggested_goals\"].append(goal)\n",
|
338 |
+
" return self._update_displays(state)\n",
|
339 |
+
" return state\n",
|
340 |
+
" \n",
|
341 |
+
" def select_goal(evt: gr.SelectData, state):\n",
|
342 |
+
" \"\"\"Set selected goal as active.\"\"\"\n",
|
343 |
+
" if evt.index[0] < len(state[\"suggested_goals\"]):\n",
|
344 |
+
" goal = state[\"suggested_goals\"][evt.index[0]]\n",
|
345 |
+
" self.tutor.learning_context.add_smart_goal(goal)\n",
|
346 |
+
" return self._update_displays(state)\n",
|
347 |
+
" return state\n",
|
348 |
+
" \n",
|
349 |
+
" def end_discussion(state):\n",
|
350 |
+
" \"\"\"End current discussion.\"\"\"\n",
|
351 |
+
" self.tutor.end_discussion()\n",
|
352 |
+
" return self._update_displays(state)\n",
|
353 |
+
" \n",
|
354 |
+
" def clear_discussion():\n",
|
355 |
+
" \"\"\"Clear discussion history.\"\"\"\n",
|
356 |
+
" self.tutor.clear_discussion()\n",
|
357 |
+
" return [], \"\"\n",
|
358 |
+
" \n",
|
359 |
+
" # Wire up events\n",
|
360 |
+
" specialty.change(\n",
|
361 |
+
" update_context,\n",
|
362 |
+
" inputs=[specialty, setting],\n",
|
363 |
+
" outputs=[goals_list, progress_display, recent_display, goal_display]\n",
|
364 |
+
" )\n",
|
365 |
+
" \n",
|
366 |
+
" setting.change(\n",
|
367 |
+
" update_context,\n",
|
368 |
+
" inputs=[specialty, setting],\n",
|
369 |
+
" outputs=[goals_list, progress_display, recent_display, goal_display]\n",
|
370 |
+
" )\n",
|
371 |
+
" \n",
|
372 |
+
" message.submit(\n",
|
373 |
+
" process_message,\n",
|
374 |
+
" inputs=[message, chatbot],\n",
|
375 |
+
" outputs=[chatbot, message]\n",
|
376 |
+
" )\n",
|
377 |
+
" \n",
|
378 |
+
" audio_input.stream(\n",
|
379 |
+
" process_audio,\n",
|
380 |
+
" inputs=[audio_input],\n",
|
381 |
+
" outputs=[message]\n",
|
382 |
+
" )\n",
|
383 |
+
" \n",
|
384 |
+
" clear.click(\n",
|
385 |
+
" clear_discussion,\n",
|
386 |
+
" outputs=[chatbot, message]\n",
|
387 |
+
" )\n",
|
388 |
+
" \n",
|
389 |
+
" end.click(\n",
|
390 |
+
" end_discussion,\n",
|
391 |
+
" inputs=[state],\n",
|
392 |
+
" outputs=[goals_list, progress_display, recent_display, goal_display]\n",
|
393 |
+
" )\n",
|
394 |
+
" \n",
|
395 |
+
" generate.click(\n",
|
396 |
+
" generate_goals,\n",
|
397 |
+
" inputs=[state],\n",
|
398 |
+
" outputs=[goals_list, progress_display, recent_display, goal_display]\n",
|
399 |
+
" )\n",
|
400 |
+
" \n",
|
401 |
+
" new_goal.submit(\n",
|
402 |
+
" add_user_goal,\n",
|
403 |
+
" inputs=[new_goal, state],\n",
|
404 |
+
" outputs=[goals_list, progress_display, recent_display, goal_display]\n",
|
405 |
+
" )\n",
|
406 |
+
" \n",
|
407 |
+
" goals_list.select(\n",
|
408 |
+
" select_goal,\n",
|
409 |
+
" inputs=[state],\n",
|
410 |
+
" outputs=[goals_list, progress_display, recent_display, goal_display]\n",
|
411 |
+
" )\n",
|
412 |
+
" \n",
|
413 |
+
" return interface\n",
|
414 |
+
" \n",
|
415 |
+
" def _update_displays(self, state: Dict) -> List:\n",
|
416 |
+
" \"\"\"Update all display components.\"\"\"\n",
|
417 |
+
" context = self.tutor.learning_context\n",
|
418 |
+
" \n",
|
419 |
+
" # Update goals list\n",
|
420 |
+
" goals_data = []\n",
|
421 |
+
" for goal in state[\"suggested_goals\"]:\n",
|
422 |
+
" status = \"Active\" if (\n",
|
423 |
+
" context.active_goal and \n",
|
424 |
+
" context.active_goal.id == goal.id\n",
|
425 |
+
" ) else \"Available\"\n",
|
426 |
+
" \n",
|
427 |
+
" goals_data.append([\n",
|
428 |
+
" goal.smart_version,\n",
|
429 |
+
" goal.category.value,\n",
|
430 |
+
" status\n",
|
431 |
+
" ])\n",
|
432 |
+
" \n",
|
433 |
+
" # Update progress display\n",
|
434 |
+
" summary = context.get_category_summary()\n",
|
435 |
+
" progress_data = [\n",
|
436 |
+
" [cat, data[\"completed\"], data[\"total\"]]\n",
|
437 |
+
" for cat, data in summary.items()\n",
|
438 |
+
" ]\n",
|
439 |
+
" \n",
|
440 |
+
" # Update recent completions\n",
|
441 |
+
" recent_data = []\n",
|
442 |
+
" for cat, data in summary.items():\n",
|
443 |
+
" for goal in data[\"recent\"]:\n",
|
444 |
+
" recent_data.append([\n",
|
445 |
+
" goal[\"smart_version\"],\n",
|
446 |
+
" cat,\n",
|
447 |
+
" goal[\"completed_at\"]\n",
|
448 |
+
" ])\n",
|
449 |
+
" \n",
|
450 |
+
" # Update active goal display\n",
|
451 |
+
" goal_text = (\n",
|
452 |
+
" f\"Current Goal: {context.active_goal.smart_version}\"\n",
|
453 |
+
" if context.active_goal else\n",
|
454 |
+
" \"No active learning goal\"\n",
|
455 |
+
" )\n",
|
456 |
+
" \n",
|
457 |
+
" return [\n",
|
458 |
+
" goals_data,\n",
|
459 |
+
" progress_data,\n",
|
460 |
+
" recent_data,\n",
|
461 |
+
" goal_text\n",
|
462 |
+
" ]"
|
463 |
+
]
|
464 |
+
},
|
465 |
+
{
|
466 |
+
"cell_type": "code",
|
467 |
+
"execution_count": null,
|
468 |
+
"id": "675c20cc-43aa-4897-89ba-4fa26dd37c20",
|
469 |
+
"metadata": {},
|
470 |
+
"outputs": [],
|
471 |
+
"source": [
|
472 |
+
"#| hide\n",
|
473 |
+
"# old code\n",
|
474 |
+
"\n",
|
475 |
+
"class LearningInterface:\n",
|
476 |
+
" \"\"\"\n",
|
477 |
+
" Gradio interface for clinical learning interactions.\n",
|
478 |
+
" \n",
|
479 |
+
" Features:\n",
|
480 |
+
" - Natural case discussion chat\n",
|
481 |
+
" - Dynamic learning dashboard\n",
|
482 |
+
" - Post-discussion analysis\n",
|
483 |
+
" - Progress tracking\n",
|
484 |
+
" \"\"\"\n",
|
485 |
+
" \n",
|
486 |
+
" def __init__(\n",
|
487 |
+
" self,\n",
|
488 |
+
" context_path: Optional[Path] = None,\n",
|
489 |
+
" theme: str = \"default\"\n",
|
490 |
+
" ):\n",
|
491 |
+
" \"\"\"Initialize learning interface.\"\"\"\n",
|
492 |
+
" self.tutor = ClinicalTutor(context_path)\n",
|
493 |
+
" self.theme = theme\n",
|
494 |
+
" self.context_path = context_path\n",
|
495 |
+
" \n",
|
496 |
+
" # Track current discussion state\n",
|
497 |
+
" self.current_discussion = {\n",
|
498 |
+
" \"started\": None,\n",
|
499 |
+
" \"case_type\": None,\n",
|
500 |
+
" \"messages\": []\n",
|
501 |
+
" }\n",
|
502 |
+
" \n",
|
503 |
+
" logger.info(\"Learning interface initialized\")\n",
|
504 |
+
" \n",
|
505 |
+
" async def process_chat(\n",
|
506 |
+
" self,\n",
|
507 |
+
" message: str,\n",
|
508 |
+
" history: List[List[str]],\n",
|
509 |
+
" state: Dict[str, Any]\n",
|
510 |
+
" ) -> Tuple[List[List[str]], str, Dict[str, Any]]: \n",
|
511 |
+
" \"\"\"\n",
|
512 |
+
" Process chat messages with state management.\n",
|
513 |
+
" \n",
|
514 |
+
" Args:\n",
|
515 |
+
" message: User input message\n",
|
516 |
+
" history: Chat history\n",
|
517 |
+
" state: Current interface state\n",
|
518 |
+
" \n",
|
519 |
+
" Returns:\n",
|
520 |
+
" tuple: (updated history, cleared message, updated state)\n",
|
521 |
+
" \"\"\"\n",
|
522 |
+
" try:\n",
|
523 |
+
" if not message.strip():\n",
|
524 |
+
" return history, \"\", state\n",
|
525 |
+
" \n",
|
526 |
+
" # Start new discussion if none active\n",
|
527 |
+
" if not state.get(\"discussion_active\"):\n",
|
528 |
+
" state[\"discussion_active\"] = True\n",
|
529 |
+
" state[\"discussion_start\"] = datetime.now().isoformat()\n",
|
530 |
+
" \n",
|
531 |
+
" # Get tutor response\n",
|
532 |
+
" response = await self.tutor.discuss_case(message)\n",
|
533 |
+
" \n",
|
534 |
+
" # Update history - now using list pairs instead of dicts\n",
|
535 |
+
" if history is None:\n",
|
536 |
+
" history = []\n",
|
537 |
+
" history.append([message, response]) # Changed from dict format to list pair\n",
|
538 |
+
" \n",
|
539 |
+
" state[\"last_message\"] = datetime.now().isoformat()\n",
|
540 |
+
" \n",
|
541 |
+
" return history, \"\", state\n",
|
542 |
+
" \n",
|
543 |
+
" except Exception as e:\n",
|
544 |
+
" logger.error(f\"Error in chat: {str(e)}\")\n",
|
545 |
+
" return history or [], \"\", state\n",
|
546 |
+
"\n",
|
547 |
+
" async def end_discussion(\n",
|
548 |
+
" self,\n",
|
549 |
+
" history: List[List[str]],\n",
|
550 |
+
" state: Dict[str, Any]\n",
|
551 |
+
" ) -> Tuple[Dict[str, Any], Dict[str, Any]]:\n",
|
552 |
+
" \"\"\"\n",
|
553 |
+
" Analyze completed discussion and prepare summary.\n",
|
554 |
+
" \n",
|
555 |
+
" Args:\n",
|
556 |
+
" history: Chat history as list of [user_message, assistant_message] pairs\n",
|
557 |
+
" state: Current interface state\n",
|
558 |
+
" \n",
|
559 |
+
" Returns:\n",
|
560 |
+
" tuple: (analysis results, updated state)\n",
|
561 |
+
" \"\"\"\n",
|
562 |
+
" try:\n",
|
563 |
+
" if not history:\n",
|
564 |
+
" return {\n",
|
565 |
+
" \"learning_points\": [],\n",
|
566 |
+
" \"gaps\": {},\n",
|
567 |
+
" \"strengths\": [],\n",
|
568 |
+
" \"suggested_objectives\": []\n",
|
569 |
+
" }, state\n",
|
570 |
+
" \n",
|
571 |
+
" # Convert history format for analysis\n",
|
572 |
+
" formatted_history = []\n",
|
573 |
+
" for user_msg, assistant_msg in history:\n",
|
574 |
+
" formatted_history.extend([\n",
|
575 |
+
" {\"role\": \"user\", \"content\": user_msg},\n",
|
576 |
+
" {\"role\": \"assistant\", \"content\": assistant_msg}\n",
|
577 |
+
" ])\n",
|
578 |
+
" \n",
|
579 |
+
" # Get analysis\n",
|
580 |
+
" analysis = await self.tutor.analyze_discussion(formatted_history)\n",
|
581 |
+
" \n",
|
582 |
+
" # Reset discussion state\n",
|
583 |
+
" state[\"discussion_active\"] = False\n",
|
584 |
+
" state[\"discussion_start\"] = None\n",
|
585 |
+
" state[\"last_message\"] = None\n",
|
586 |
+
" \n",
|
587 |
+
" return analysis, state\n",
|
588 |
+
" \n",
|
589 |
+
" except Exception as e:\n",
|
590 |
+
" logger.error(f\"Error analyzing discussion: {str(e)}\")\n",
|
591 |
+
" return {\n",
|
592 |
+
" \"learning_points\": [],\n",
|
593 |
+
" \"gaps\": {},\n",
|
594 |
+
" \"strengths\": [],\n",
|
595 |
+
" \"suggested_objectives\": []\n",
|
596 |
+
" }, state \n",
|
597 |
+
" \n",
|
598 |
+
" def update_rotation(\n",
|
599 |
+
" self,\n",
|
600 |
+
" specialty: str,\n",
|
601 |
+
" start_date: str,\n",
|
602 |
+
" end_date: str,\n",
|
603 |
+
" focus_areas: str\n",
|
604 |
+
" ) -> Tuple[str, str, str, str]:\n",
|
605 |
+
" \"\"\"\n",
|
606 |
+
" Update rotation details and return updated values.\n",
|
607 |
+
" \n",
|
608 |
+
" Args:\n",
|
609 |
+
" specialty: Rotation specialty\n",
|
610 |
+
" start_date: Start date string\n",
|
611 |
+
" end_date: End date string\n",
|
612 |
+
" focus_areas: Comma-separated focus areas\n",
|
613 |
+
" \n",
|
614 |
+
" Returns:\n",
|
615 |
+
" tuple: Updated field values\n",
|
616 |
+
" \"\"\"\n",
|
617 |
+
" try:\n",
|
618 |
+
" # Parse focus areas\n",
|
619 |
+
" focus_list = [\n",
|
620 |
+
" area.strip() \n",
|
621 |
+
" for area in focus_areas.split(\",\") \n",
|
622 |
+
" if area.strip()\n",
|
623 |
+
" ]\n",
|
624 |
+
" \n",
|
625 |
+
" # Update context\n",
|
626 |
+
" rotation = {\n",
|
627 |
+
" \"specialty\": specialty,\n",
|
628 |
+
" \"start_date\": start_date,\n",
|
629 |
+
" \"end_date\": end_date,\n",
|
630 |
+
" \"key_focus_areas\": focus_list\n",
|
631 |
+
" }\n",
|
632 |
+
" self.tutor.learning_context.update_rotation(rotation)\n",
|
633 |
+
" \n",
|
634 |
+
" # Return updated values\n",
|
635 |
+
" return (\n",
|
636 |
+
" specialty,\n",
|
637 |
+
" start_date,\n",
|
638 |
+
" end_date,\n",
|
639 |
+
" \",\".join(focus_list)\n",
|
640 |
+
" )\n",
|
641 |
+
" \n",
|
642 |
+
" except Exception as e:\n",
|
643 |
+
" logger.error(f\"Error updating rotation: {str(e)}\")\n",
|
644 |
+
" current = self.tutor.learning_context.current_rotation\n",
|
645 |
+
" return (\n",
|
646 |
+
" current[\"specialty\"],\n",
|
647 |
+
" current[\"start_date\"] or \"\",\n",
|
648 |
+
" current[\"end_date\"] or \"\",\n",
|
649 |
+
" \",\".join(current[\"key_focus_areas\"])\n",
|
650 |
+
" )\n",
|
651 |
+
"\n",
|
652 |
+
" def add_objective(\n",
|
653 |
+
" self,\n",
|
654 |
+
" objective: str,\n",
|
655 |
+
" objectives_df: pd.DataFrame\n",
|
656 |
+
" ) -> pd.DataFrame:\n",
|
657 |
+
" \"\"\"\n",
|
658 |
+
" Add new learning objective and return updated dataframe.\n",
|
659 |
+
" \n",
|
660 |
+
" Args:\n",
|
661 |
+
" objective: New objective text\n",
|
662 |
+
" objectives_df: Current objectives dataframe\n",
|
663 |
+
" \n",
|
664 |
+
" Returns:\n",
|
665 |
+
" pd.DataFrame: Updated objectives list\n",
|
666 |
+
" \"\"\"\n",
|
667 |
+
" try:\n",
|
668 |
+
" if not objective.strip():\n",
|
669 |
+
" return objectives_df\n",
|
670 |
+
" \n",
|
671 |
+
" # Add to context\n",
|
672 |
+
" self.tutor.learning_context.add_learning_objective(objective)\n",
|
673 |
+
" \n",
|
674 |
+
" # Convert to dataframe\n",
|
675 |
+
" return pd.DataFrame([\n",
|
676 |
+
" [obj[\"objective\"], obj[\"status\"], obj[\"added\"]]\n",
|
677 |
+
" for obj in self.tutor.learning_context.learning_objectives\n",
|
678 |
+
" ], columns=[\"Objective\", \"Status\", \"Date Added\"])\n",
|
679 |
+
" \n",
|
680 |
+
" except Exception as e:\n",
|
681 |
+
" logger.error(f\"Error adding objective: {str(e)}\")\n",
|
682 |
+
" return objectives_df\n",
|
683 |
+
"\n",
|
684 |
+
" def toggle_objective_status(\n",
|
685 |
+
" self,\n",
|
686 |
+
" evt: gr.SelectData, # Updated to use gr.SelectData\n",
|
687 |
+
" objectives_df: pd.DataFrame\n",
|
688 |
+
" ) -> pd.DataFrame:\n",
|
689 |
+
" \"\"\"\n",
|
690 |
+
" Toggle objective status between active and completed.\n",
|
691 |
+
" \n",
|
692 |
+
" Args:\n",
|
693 |
+
" evt: Gradio select event containing row index\n",
|
694 |
+
" objectives_df: Current objectives dataframe\n",
|
695 |
+
" \n",
|
696 |
+
" Returns:\n",
|
697 |
+
" pd.DataFrame: Updated objectives list\n",
|
698 |
+
" \"\"\"\n",
|
699 |
+
" try:\n",
|
700 |
+
" objective_idx = evt.index[0] # Get selected row index\n",
|
701 |
+
" if objective_idx >= len(objectives_df):\n",
|
702 |
+
" return objectives_df\n",
|
703 |
+
" \n",
|
704 |
+
" # Get objective\n",
|
705 |
+
" objective = objectives_df.iloc[objective_idx][\"Objective\"]\n",
|
706 |
+
" current_status = objectives_df.iloc[objective_idx][\"Status\"]\n",
|
707 |
+
" \n",
|
708 |
+
" # Toggle in context\n",
|
709 |
+
" if current_status == \"active\":\n",
|
710 |
+
" self.tutor.learning_context.complete_objective(objective)\n",
|
711 |
+
" else:\n",
|
712 |
+
" self.tutor.learning_context.add_learning_objective(objective)\n",
|
713 |
+
" \n",
|
714 |
+
" # Update dataframe\n",
|
715 |
+
" return pd.DataFrame([\n",
|
716 |
+
" [obj[\"objective\"], obj[\"status\"], obj[\"added\"]]\n",
|
717 |
+
" for obj in self.tutor.learning_context.learning_objectives\n",
|
718 |
+
" ], columns=[\"Objective\", \"Status\", \"Date Added\"])\n",
|
719 |
+
" \n",
|
720 |
+
" except Exception as e:\n",
|
721 |
+
" logger.error(f\"Error toggling objective: {str(e)}\")\n",
|
722 |
+
" return objectives_df\n",
|
723 |
+
"\n",
|
724 |
+
" def add_feedback_focus(\n",
|
725 |
+
" self,\n",
|
726 |
+
" focus: str,\n",
|
727 |
+
" feedback_df: pd.DataFrame\n",
|
728 |
+
" ) -> pd.DataFrame:\n",
|
729 |
+
" \"\"\"Add new feedback focus area.\"\"\"\n",
|
730 |
+
" try:\n",
|
731 |
+
" if not focus.strip():\n",
|
732 |
+
" return feedback_df\n",
|
733 |
+
" \n",
|
734 |
+
" # Add to context\n",
|
735 |
+
" self.tutor.learning_context.toggle_feedback_focus(focus, True)\n",
|
736 |
+
" \n",
|
737 |
+
" # Update dataframe\n",
|
738 |
+
" return pd.DataFrame([\n",
|
739 |
+
" [pref[\"focus\"], pref[\"active\"]]\n",
|
740 |
+
" for pref in self.tutor.learning_context.feedback_preferences\n",
|
741 |
+
" ], columns=[\"Focus Area\", \"Active\"])\n",
|
742 |
+
" \n",
|
743 |
+
" except Exception as e:\n",
|
744 |
+
" logger.error(f\"Error adding feedback focus: {str(e)}\")\n",
|
745 |
+
" return feedback_df\n",
|
746 |
+
"\n",
|
747 |
+
" def toggle_feedback_status(\n",
|
748 |
+
" self,\n",
|
749 |
+
" evt: gr.SelectData, # Updated to use gr.SelectData\n",
|
750 |
+
" feedback_df: pd.DataFrame\n",
|
751 |
+
" ) -> pd.DataFrame:\n",
|
752 |
+
" \"\"\"Toggle feedback focus active status.\"\"\"\n",
|
753 |
+
" try:\n",
|
754 |
+
" focus_idx = evt.index[0] # Get selected row index\n",
|
755 |
+
" if focus_idx >= len(feedback_df):\n",
|
756 |
+
" return feedback_df\n",
|
757 |
+
" \n",
|
758 |
+
" # Get focus area\n",
|
759 |
+
" focus = feedback_df.iloc[focus_idx][\"Focus Area\"]\n",
|
760 |
+
" current_status = feedback_df.iloc[focus_idx][\"Active\"]\n",
|
761 |
+
" \n",
|
762 |
+
" # Toggle in context\n",
|
763 |
+
" self.tutor.learning_context.toggle_feedback_focus(\n",
|
764 |
+
" focus, \n",
|
765 |
+
" not current_status\n",
|
766 |
+
" )\n",
|
767 |
+
" \n",
|
768 |
+
" # Update dataframe\n",
|
769 |
+
" return pd.DataFrame([\n",
|
770 |
+
" [pref[\"focus\"], pref[\"active\"]]\n",
|
771 |
+
" for pref in self.tutor.learning_context.feedback_preferences\n",
|
772 |
+
" ], columns=[\"Focus Area\", \"Active\"])\n",
|
773 |
+
" \n",
|
774 |
+
" except Exception as e:\n",
|
775 |
+
" logger.error(f\"Error toggling feedback: {str(e)}\")\n",
|
776 |
+
" return feedback_df\n",
|
777 |
+
"\n",
|
778 |
+
" def create_interface(self) -> gr.Blocks:\n",
|
779 |
+
" \"\"\"Create and configure the Gradio interface\"\"\"\n",
|
780 |
+
" with gr.Blocks(\n",
|
781 |
+
" title=\"Clinical Learning Assistant\",\n",
|
782 |
+
" theme=self.theme,\n",
|
783 |
+
" css=create_dashboard_css()\n",
|
784 |
+
" ) as interface:\n",
|
785 |
+
" # State management\n",
|
786 |
+
" state = gr.State({\n",
|
787 |
+
" \"discussion_active\": False,\n",
|
788 |
+
" \"discussion_start\": None,\n",
|
789 |
+
" \"last_message\": None\n",
|
790 |
+
" })\n",
|
791 |
+
" \n",
|
792 |
+
" # Header\n",
|
793 |
+
" with gr.Row():\n",
|
794 |
+
" gr.Markdown(\n",
|
795 |
+
" \"# Clinical Learning Assistant\",\n",
|
796 |
+
" elem_classes=[\"dashboard-header\"]\n",
|
797 |
+
" )\n",
|
798 |
+
" \n",
|
799 |
+
" with gr.Row():\n",
|
800 |
+
" # Left column - Chat interface\n",
|
801 |
+
" with gr.Column(scale=2):\n",
|
802 |
+
" # Active discussion indicator\n",
|
803 |
+
" discussion_status = gr.Markdown(\n",
|
804 |
+
" \"Start a new case discussion\",\n",
|
805 |
+
" elem_classes=[\"dashboard-card\"]\n",
|
806 |
+
" )\n",
|
807 |
+
" \n",
|
808 |
+
" # Chat interface\n",
|
809 |
+
" chatbot = gr.Chatbot(\n",
|
810 |
+
" height=500,\n",
|
811 |
+
" label=\"Case Discussion\",\n",
|
812 |
+
" show_label=True,\n",
|
813 |
+
" elem_classes=[\"dashboard-card\"]\n",
|
814 |
+
" )\n",
|
815 |
+
" \n",
|
816 |
+
" with gr.Row():\n",
|
817 |
+
" msg = gr.Textbox(\n",
|
818 |
+
" label=\"Present your case or ask questions\",\n",
|
819 |
+
" placeholder=(\n",
|
820 |
+
" \"Present your case as you would to your supervisor:\\n\"\n",
|
821 |
+
" \"- Start with the chief complaint\\n\"\n",
|
822 |
+
" \"- Include relevant history and findings\\n\"\n",
|
823 |
+
" \"- Share your assessment and plan\"\n",
|
824 |
+
" ),\n",
|
825 |
+
" lines=5\n",
|
826 |
+
" )\n",
|
827 |
+
" \n",
|
828 |
+
" # Add voice input with updated syntax\n",
|
829 |
+
" audio_msg = gr.Audio(\n",
|
830 |
+
" label=\"Or speak your case\",\n",
|
831 |
+
" sources=[\"microphone\"],\n",
|
832 |
+
" type=\"numpy\",\n",
|
833 |
+
" streaming=True\n",
|
834 |
+
" )\n",
|
835 |
+
" \n",
|
836 |
+
" with gr.Row():\n",
|
837 |
+
" clear = gr.Button(\"Clear Discussion\")\n",
|
838 |
+
" end_discussion = gr.Button(\n",
|
839 |
+
" \"End Discussion & Review\",\n",
|
840 |
+
" variant=\"primary\"\n",
|
841 |
+
" )\n",
|
842 |
+
" \n",
|
843 |
+
" # Right column - Learning dashboard\n",
|
844 |
+
" with gr.Column(scale=1):\n",
|
845 |
+
" with gr.Tabs():\n",
|
846 |
+
" # Current Rotation tab\n",
|
847 |
+
" with gr.Tab(\"Current Rotation\"):\n",
|
848 |
+
" with gr.Column(elem_classes=[\"dashboard-card\"]):\n",
|
849 |
+
" specialty = gr.Textbox(\n",
|
850 |
+
" label=\"Specialty\",\n",
|
851 |
+
" value=self.tutor.learning_context.current_rotation[\"specialty\"]\n",
|
852 |
+
" )\n",
|
853 |
+
" start_date = gr.Textbox(\n",
|
854 |
+
" label=\"Start Date (YYYY-MM-DD)\",\n",
|
855 |
+
" value=self.tutor.learning_context.current_rotation[\"start_date\"]\n",
|
856 |
+
" )\n",
|
857 |
+
" end_date = gr.Textbox(\n",
|
858 |
+
" label=\"End Date (YYYY-MM-DD)\",\n",
|
859 |
+
" value=self.tutor.learning_context.current_rotation[\"end_date\"]\n",
|
860 |
+
" )\n",
|
861 |
+
" focus_areas = gr.Textbox(\n",
|
862 |
+
" label=\"Key Focus Areas (comma-separated)\",\n",
|
863 |
+
" value=\",\".join(\n",
|
864 |
+
" self.tutor.learning_context.current_rotation[\"key_focus_areas\"]\n",
|
865 |
+
" )\n",
|
866 |
+
" )\n",
|
867 |
+
" update_rotation_btn = gr.Button(\n",
|
868 |
+
" \"Update Rotation\",\n",
|
869 |
+
" variant=\"secondary\"\n",
|
870 |
+
" )\n",
|
871 |
+
" \n",
|
872 |
+
" # Learning Objectives tab\n",
|
873 |
+
" with gr.Tab(\"Learning Objectives\"):\n",
|
874 |
+
" with gr.Column(elem_classes=[\"dashboard-card\"]):\n",
|
875 |
+
" objectives_df = gr.DataFrame(\n",
|
876 |
+
" headers=[\"Objective\", \"Status\", \"Date Added\"],\n",
|
877 |
+
" value=[[\n",
|
878 |
+
" obj[\"objective\"],\n",
|
879 |
+
" obj[\"status\"],\n",
|
880 |
+
" obj[\"added\"]\n",
|
881 |
+
" ] for obj in self.tutor.learning_context.learning_objectives],\n",
|
882 |
+
" interactive=True,\n",
|
883 |
+
" wrap=True\n",
|
884 |
+
" )\n",
|
885 |
+
" \n",
|
886 |
+
" with gr.Row():\n",
|
887 |
+
" new_objective = gr.Textbox(\n",
|
888 |
+
" label=\"New Learning Objective\",\n",
|
889 |
+
" placeholder=\"Enter objective...\"\n",
|
890 |
+
" )\n",
|
891 |
+
" add_objective_btn = gr.Button(\n",
|
892 |
+
" \"Add\",\n",
|
893 |
+
" variant=\"secondary\"\n",
|
894 |
+
" )\n",
|
895 |
+
" \n",
|
896 |
+
" # Feedback Preferences tab\n",
|
897 |
+
" with gr.Tab(\"Feedback Focus\"):\n",
|
898 |
+
" with gr.Column(elem_classes=[\"dashboard-card\"]):\n",
|
899 |
+
" feedback_df = gr.DataFrame(\n",
|
900 |
+
" headers=[\"Focus Area\", \"Active\"],\n",
|
901 |
+
" value=[[\n",
|
902 |
+
" pref[\"focus\"],\n",
|
903 |
+
" pref[\"active\"]\n",
|
904 |
+
" ] for pref in self.tutor.learning_context.feedback_preferences],\n",
|
905 |
+
" interactive=True,\n",
|
906 |
+
" wrap=True\n",
|
907 |
+
" )\n",
|
908 |
+
" \n",
|
909 |
+
" with gr.Row():\n",
|
910 |
+
" new_feedback = gr.Textbox(\n",
|
911 |
+
" label=\"New Feedback Focus\",\n",
|
912 |
+
" placeholder=\"Enter focus area...\"\n",
|
913 |
+
" )\n",
|
914 |
+
" add_feedback_btn = gr.Button(\n",
|
915 |
+
" \"Add\",\n",
|
916 |
+
" variant=\"secondary\"\n",
|
917 |
+
" )\n",
|
918 |
+
" \n",
|
919 |
+
" # Knowledge Profile tab\n",
|
920 |
+
" with gr.Tab(\"Knowledge Profile\"):\n",
|
921 |
+
" with gr.Column(elem_classes=[\"dashboard-card\"]):\n",
|
922 |
+
" # Knowledge Gaps\n",
|
923 |
+
" gr.Markdown(\"### Knowledge Gaps\")\n",
|
924 |
+
" gaps_display = gr.DataFrame(\n",
|
925 |
+
" headers=[\"Topic\", \"Confidence\"],\n",
|
926 |
+
" value=[[\n",
|
927 |
+
" topic, confidence\n",
|
928 |
+
" ] for topic, confidence in \n",
|
929 |
+
" self.tutor.learning_context.knowledge_profile[\"gaps\"].items()\n",
|
930 |
+
" ],\n",
|
931 |
+
" interactive=False\n",
|
932 |
+
" )\n",
|
933 |
+
" \n",
|
934 |
+
" # Strengths Display\n",
|
935 |
+
" gr.Markdown(\"### Strengths\")\n",
|
936 |
+
" strengths_display = gr.DataFrame(\n",
|
937 |
+
" headers=[\"Area\"],\n",
|
938 |
+
" value=[[strength] for strength in \n",
|
939 |
+
" self.tutor.learning_context.knowledge_profile[\"strengths\"]\n",
|
940 |
+
" ],\n",
|
941 |
+
" interactive=False\n",
|
942 |
+
" )\n",
|
943 |
+
" \n",
|
944 |
+
" # Recent Progress\n",
|
945 |
+
" gr.Markdown(\"### Recent Progress\")\n",
|
946 |
+
" progress_display = gr.DataFrame(\n",
|
947 |
+
" headers=[\"Topic\", \"Improvement\", \"Date\"],\n",
|
948 |
+
" value=[[\n",
|
949 |
+
" prog[\"topic\"],\n",
|
950 |
+
" f\"{prog['improvement']:.2f}\",\n",
|
951 |
+
" prog[\"date\"]\n",
|
952 |
+
" ] for prog in \n",
|
953 |
+
" self.tutor.learning_context.knowledge_profile[\"recent_progress\"]\n",
|
954 |
+
" ],\n",
|
955 |
+
" interactive=False\n",
|
956 |
+
" )\n",
|
957 |
+
" \n",
|
958 |
+
" # Discussion summary section\n",
|
959 |
+
" summary_section = gr.Column(visible=False)\n",
|
960 |
+
" with summary_section:\n",
|
961 |
+
" gr.Markdown(\"## Discussion Summary\")\n",
|
962 |
+
" \n",
|
963 |
+
" # Overview section\n",
|
964 |
+
" with gr.Row():\n",
|
965 |
+
" with gr.Column():\n",
|
966 |
+
" gr.Markdown(\"### Session Overview\")\n",
|
967 |
+
" session_overview = gr.JSON(\n",
|
968 |
+
" label=\"Discussion Details\",\n",
|
969 |
+
" value={\n",
|
970 |
+
" \"duration\": \"0 minutes\",\n",
|
971 |
+
" \"messages\": 0,\n",
|
972 |
+
" \"topics_covered\": []\n",
|
973 |
+
" }\n",
|
974 |
+
" )\n",
|
975 |
+
" \n",
|
976 |
+
" # Learning Points and Gaps\n",
|
977 |
+
" with gr.Row():\n",
|
978 |
+
" with gr.Column():\n",
|
979 |
+
" gr.Markdown(\"### Key Learning Points\")\n",
|
980 |
+
" learning_points = gr.JSON(label=\"Points to Remember\")\n",
|
981 |
+
" \n",
|
982 |
+
" with gr.Column():\n",
|
983 |
+
" gr.Markdown(\"### Knowledge Profile Updates\")\n",
|
984 |
+
" with gr.Row():\n",
|
985 |
+
" gaps = gr.JSON(label=\"Areas for Improvement\")\n",
|
986 |
+
" strengths = gr.JSON(label=\"Demonstrated Strengths\")\n",
|
987 |
+
" \n",
|
988 |
+
" # Future Learning section\n",
|
989 |
+
" gr.Markdown(\"### Planning Ahead\")\n",
|
990 |
+
" with gr.Row():\n",
|
991 |
+
" with gr.Column():\n",
|
992 |
+
" gr.Markdown(\"#### Suggested Learning Objectives\")\n",
|
993 |
+
" objectives = gr.JSON(label=\"Consider Adding\")\n",
|
994 |
+
" \n",
|
995 |
+
" with gr.Column():\n",
|
996 |
+
" gr.Markdown(\"#### Recommended Focus Areas\")\n",
|
997 |
+
" recommendations = gr.JSON(label=\"Next Steps\")\n",
|
998 |
+
" \n",
|
999 |
+
" # Action buttons\n",
|
1000 |
+
" with gr.Row():\n",
|
1001 |
+
" add_selected_objectives = gr.Button(\n",
|
1002 |
+
" \"Add Selected Objectives\",\n",
|
1003 |
+
" variant=\"primary\"\n",
|
1004 |
+
" )\n",
|
1005 |
+
" close_summary = gr.Button(\"Close Summary\")\n",
|
1006 |
+
" \n",
|
1007 |
+
" # Event handlers\n",
|
1008 |
+
" # Add new event handler for voice input\n",
|
1009 |
+
" def process_audio(audio):\n",
|
1010 |
+
" if audio is None:\n",
|
1011 |
+
" return None\n",
|
1012 |
+
" # Convert audio to text using your preferred method\n",
|
1013 |
+
" # For example, you could use transformers pipeline here\n",
|
1014 |
+
" try:\n",
|
1015 |
+
" from transformers import pipeline\n",
|
1016 |
+
" transcriber = pipeline(\"automatic-speech-recognition\", model=\"openai/whisper-small\")\n",
|
1017 |
+
" text = transcriber(audio)[\"text\"]\n",
|
1018 |
+
" return text\n",
|
1019 |
+
" except Exception as e:\n",
|
1020 |
+
" logger.error(f\"Error transcribing audio: {str(e)}\")\n",
|
1021 |
+
" return None\n",
|
1022 |
+
" \n",
|
1023 |
+
" # Update the event handler:\n",
|
1024 |
+
" audio_msg.stop_recording(\n",
|
1025 |
+
" fn=process_audio,\n",
|
1026 |
+
" outputs=[msg]\n",
|
1027 |
+
" ).then(\n",
|
1028 |
+
" fn=self.process_chat,\n",
|
1029 |
+
" inputs=[msg, chatbot, state],\n",
|
1030 |
+
" outputs=[chatbot, msg, state]\n",
|
1031 |
+
" ).then(\n",
|
1032 |
+
" fn=self._update_discussion_status,\n",
|
1033 |
+
" inputs=[state],\n",
|
1034 |
+
" outputs=[discussion_status]\n",
|
1035 |
+
" ) \n",
|
1036 |
+
"\n",
|
1037 |
+
" msg.submit(\n",
|
1038 |
+
" self.process_chat,\n",
|
1039 |
+
" inputs=[msg, chatbot, state],\n",
|
1040 |
+
" outputs=[chatbot, msg, state]\n",
|
1041 |
+
" ).then(\n",
|
1042 |
+
" self._update_discussion_status,\n",
|
1043 |
+
" inputs=[state],\n",
|
1044 |
+
" outputs=[discussion_status]\n",
|
1045 |
+
" )\n",
|
1046 |
+
" \n",
|
1047 |
+
" clear.click(\n",
|
1048 |
+
" lambda: ([], \"\", {\n",
|
1049 |
+
" \"discussion_active\": False,\n",
|
1050 |
+
" \"discussion_start\": None,\n",
|
1051 |
+
" \"last_message\": None\n",
|
1052 |
+
" }),\n",
|
1053 |
+
" outputs=[chatbot, msg, state]\n",
|
1054 |
+
" ).then(\n",
|
1055 |
+
" lambda: \"Start a new case discussion\",\n",
|
1056 |
+
" outputs=[discussion_status]\n",
|
1057 |
+
" )\n",
|
1058 |
+
" \n",
|
1059 |
+
" end_discussion.click(\n",
|
1060 |
+
" self.end_discussion,\n",
|
1061 |
+
" inputs=[chatbot, state],\n",
|
1062 |
+
" outputs=[\n",
|
1063 |
+
" session_overview,\n",
|
1064 |
+
" learning_points,\n",
|
1065 |
+
" gaps,\n",
|
1066 |
+
" strengths,\n",
|
1067 |
+
" objectives,\n",
|
1068 |
+
" recommendations\n",
|
1069 |
+
" ]\n",
|
1070 |
+
" ).then(\n",
|
1071 |
+
" lambda: gr.update(visible=True),\n",
|
1072 |
+
" None,\n",
|
1073 |
+
" summary_section\n",
|
1074 |
+
" ).then(\n",
|
1075 |
+
" self._refresh_knowledge_profile,\n",
|
1076 |
+
" outputs=[gaps_display, strengths_display, progress_display]\n",
|
1077 |
+
" )\n",
|
1078 |
+
" \n",
|
1079 |
+
" close_summary.click(\n",
|
1080 |
+
" lambda: gr.update(visible=False),\n",
|
1081 |
+
" None,\n",
|
1082 |
+
" summary_section\n",
|
1083 |
+
" )\n",
|
1084 |
+
" \n",
|
1085 |
+
" # Rotation management\n",
|
1086 |
+
" update_rotation_btn.click(\n",
|
1087 |
+
" self.update_rotation,\n",
|
1088 |
+
" inputs=[specialty, start_date, end_date, focus_areas],\n",
|
1089 |
+
" outputs=[specialty, start_date, end_date, focus_areas]\n",
|
1090 |
+
" )\n",
|
1091 |
+
" \n",
|
1092 |
+
" # Learning objectives management\n",
|
1093 |
+
" add_objective_btn.click(\n",
|
1094 |
+
" self.add_objective,\n",
|
1095 |
+
" inputs=[new_objective, objectives_df],\n",
|
1096 |
+
" outputs=[objectives_df]\n",
|
1097 |
+
" ).then(\n",
|
1098 |
+
" lambda: \"\",\n",
|
1099 |
+
" None,\n",
|
1100 |
+
" new_objective\n",
|
1101 |
+
" )\n",
|
1102 |
+
" \n",
|
1103 |
+
" objectives_df.select(\n",
|
1104 |
+
" self.toggle_objective_status,\n",
|
1105 |
+
" inputs=[objectives_df],\n",
|
1106 |
+
" outputs=[objectives_df]\n",
|
1107 |
+
" )\n",
|
1108 |
+
" \n",
|
1109 |
+
" # Feedback preferences management\n",
|
1110 |
+
" add_feedback_btn.click(\n",
|
1111 |
+
" self.add_feedback_focus,\n",
|
1112 |
+
" inputs=[new_feedback, feedback_df],\n",
|
1113 |
+
" outputs=[feedback_df]\n",
|
1114 |
+
" ).then(\n",
|
1115 |
+
" lambda: \"\",\n",
|
1116 |
+
" None,\n",
|
1117 |
+
" new_feedback\n",
|
1118 |
+
" )\n",
|
1119 |
+
" \n",
|
1120 |
+
" feedback_df.select(\n",
|
1121 |
+
" self.toggle_feedback_status,\n",
|
1122 |
+
" inputs=[feedback_df],\n",
|
1123 |
+
" outputs=[feedback_df]\n",
|
1124 |
+
" )\n",
|
1125 |
+
" \n",
|
1126 |
+
" # Add selected objectives from summary\n",
|
1127 |
+
" add_selected_objectives.click(\n",
|
1128 |
+
" self._add_suggested_objectives,\n",
|
1129 |
+
" inputs=[objectives],\n",
|
1130 |
+
" outputs=[objectives_df]\n",
|
1131 |
+
" )\n",
|
1132 |
+
" \n",
|
1133 |
+
" return interface\n",
|
1134 |
+
" \n",
|
1135 |
+
" def _update_discussion_status(self, state: Dict[str, Any]) -> str:\n",
|
1136 |
+
" \"\"\"Update discussion status display\"\"\"\n",
|
1137 |
+
" try:\n",
|
1138 |
+
" if not state.get(\"discussion_active\"):\n",
|
1139 |
+
" return \"Start a new case discussion\"\n",
|
1140 |
+
" \n",
|
1141 |
+
" start = datetime.fromisoformat(state[\"discussion_start\"])\n",
|
1142 |
+
" duration = datetime.now() - start\n",
|
1143 |
+
" minutes = int(duration.total_seconds() / 60)\n",
|
1144 |
+
" \n",
|
1145 |
+
" return f\"Active discussion ({minutes} minutes)\"\n",
|
1146 |
+
" \n",
|
1147 |
+
" except Exception as e:\n",
|
1148 |
+
" logger.error(f\"Error updating status: {str(e)}\")\n",
|
1149 |
+
" return \"Discussion status unknown\"\n",
|
1150 |
+
" \n",
|
1151 |
+
" def _refresh_knowledge_profile(\n",
|
1152 |
+
" self\n",
|
1153 |
+
" ) -> Tuple[List[List[str]], List[List[str]], List[List[str]]]:\n",
|
1154 |
+
" \"\"\"Refresh knowledge profile displays\"\"\"\n",
|
1155 |
+
" try:\n",
|
1156 |
+
" # Gaps\n",
|
1157 |
+
" gaps_data = [[\n",
|
1158 |
+
" topic, f\"{confidence:.2f}\"\n",
|
1159 |
+
" ] for topic, confidence in \n",
|
1160 |
+
" self.tutor.learning_context.knowledge_profile[\"gaps\"].items()\n",
|
1161 |
+
" ]\n",
|
1162 |
+
" \n",
|
1163 |
+
" # Strengths\n",
|
1164 |
+
" strengths_data = [[\n",
|
1165 |
+
" strength\n",
|
1166 |
+
" ] for strength in \n",
|
1167 |
+
" self.tutor.learning_context.knowledge_profile[\"strengths\"]\n",
|
1168 |
+
" ]\n",
|
1169 |
+
" \n",
|
1170 |
+
" # Progress\n",
|
1171 |
+
" progress_data = [[\n",
|
1172 |
+
" prog[\"topic\"],\n",
|
1173 |
+
" f\"{prog['improvement']:.2f}\",\n",
|
1174 |
+
" prog[\"date\"]\n",
|
1175 |
+
" ] for prog in \n",
|
1176 |
+
" self.tutor.learning_context.knowledge_profile[\"recent_progress\"]\n",
|
1177 |
+
" ]\n",
|
1178 |
+
" \n",
|
1179 |
+
" return gaps_data, strengths_data, progress_data\n",
|
1180 |
+
" \n",
|
1181 |
+
" except Exception as e:\n",
|
1182 |
+
" logger.error(f\"Error refreshing profile: {str(e)}\")\n",
|
1183 |
+
" return [], [], []\n",
|
1184 |
+
" \n",
|
1185 |
+
" def _add_suggested_objectives(\n",
|
1186 |
+
" self,\n",
|
1187 |
+
" evt: gr.SelectData, # Updated to use gr.SelectData\n",
|
1188 |
+
" suggested_objectives: List[str]\n",
|
1189 |
+
" ) -> pd.DataFrame:\n",
|
1190 |
+
" \"\"\"Add selected suggested objectives to learning objectives\"\"\"\n",
|
1191 |
+
" try:\n",
|
1192 |
+
" selected_indices = [evt.index[0]] # Get selected row index\n",
|
1193 |
+
" \n",
|
1194 |
+
" for idx in selected_indices:\n",
|
1195 |
+
" if idx < len(suggested_objectives):\n",
|
1196 |
+
" objective = suggested_objectives[idx]\n",
|
1197 |
+
" self.tutor.learning_context.add_learning_objective(objective)\n",
|
1198 |
+
" \n",
|
1199 |
+
" return pd.DataFrame([\n",
|
1200 |
+
" [obj[\"objective\"], obj[\"status\"], obj[\"added\"]]\n",
|
1201 |
+
" for obj in self.tutor.learning_context.learning_objectives\n",
|
1202 |
+
" ], columns=[\"Objective\", \"Status\", \"Date Added\"])\n",
|
1203 |
+
" \n",
|
1204 |
+
" except Exception as e:\n",
|
1205 |
+
" logger.error(f\"Error adding objectives: {str(e)}\")\n",
|
1206 |
+
" return pd.DataFrame()"
|
1207 |
+
]
|
1208 |
+
},
|
1209 |
+
{
|
1210 |
+
"cell_type": "markdown",
|
1211 |
+
"id": "30c0f121-5d5f-4dc0-b897-f6e2067a63b2",
|
1212 |
+
"metadata": {},
|
1213 |
+
"source": [
|
1214 |
+
"## Launch Function"
|
1215 |
+
]
|
1216 |
+
},
|
1217 |
+
{
|
1218 |
+
"cell_type": "code",
|
1219 |
+
"execution_count": null,
|
1220 |
+
"id": "65f97529-b221-4a19-9856-fb20d7f7316e",
|
1221 |
+
"metadata": {},
|
1222 |
+
"outputs": [],
|
1223 |
+
"source": [
|
1224 |
+
"#| hide\n",
|
1225 |
+
"# old\n",
|
1226 |
+
"async def launch_learning_interface(\n",
|
1227 |
+
" port: Optional[int] = None,\n",
|
1228 |
+
" context_path: Optional[Path] = None,\n",
|
1229 |
+
" share: bool = False,\n",
|
1230 |
+
" theme: str = \"default\"\n",
|
1231 |
+
") -> None:\n",
|
1232 |
+
" \"\"\"Launch the learning interface application.\"\"\"\n",
|
1233 |
+
" try:\n",
|
1234 |
+
" interface = LearningInterface(context_path, theme)\n",
|
1235 |
+
" app = interface.create_interface()\n",
|
1236 |
+
" app.launch(\n",
|
1237 |
+
" server_port=port,\n",
|
1238 |
+
" share=share\n",
|
1239 |
+
" )\n",
|
1240 |
+
" logger.info(f\"Interface launched on port: {port}\")\n",
|
1241 |
+
" except Exception as e:\n",
|
1242 |
+
" logger.error(f\"Error launching interface: {str(e)}\")\n",
|
1243 |
+
" raise"
|
1244 |
+
]
|
1245 |
+
},
|
1246 |
+
{
|
1247 |
+
"cell_type": "markdown",
|
1248 |
+
"id": "5c75de88-f6d5-4a5d-92b5-1ebe85895a84",
|
1249 |
+
"metadata": {},
|
1250 |
+
"source": [
|
1251 |
+
"## Tests"
|
1252 |
+
]
|
1253 |
+
},
|
1254 |
+
{
|
1255 |
+
"cell_type": "code",
|
1256 |
+
"execution_count": null,
|
1257 |
+
"id": "365bc95a-d189-4ab2-aa30-022d0286b5ba",
|
1258 |
+
"metadata": {},
|
1259 |
+
"outputs": [],
|
1260 |
+
"source": [
|
1261 |
+
"#| hide\n",
|
1262 |
+
"async def test_learning_interface():\n",
|
1263 |
+
" \"\"\"Test learning interface functionality\"\"\"\n",
|
1264 |
+
" interface = LearningInterface()\n",
|
1265 |
+
" \n",
|
1266 |
+
" # Test chat processing\n",
|
1267 |
+
" history = []\n",
|
1268 |
+
" test_input = \"28yo M with chest pain\"\n",
|
1269 |
+
" \n",
|
1270 |
+
" new_history, msg = await interface.process_chat(test_input, history)\n",
|
1271 |
+
" assert isinstance(new_history, list)\n",
|
1272 |
+
" assert len(new_history) == 2 # User message + response\n",
|
1273 |
+
" assert new_history[0][\"role\"] == \"user\"\n",
|
1274 |
+
" assert new_history[0][\"content\"] == test_input\n",
|
1275 |
+
" \n",
|
1276 |
+
" # Test discussion analysis\n",
|
1277 |
+
" analysis = await interface.end_discussion(new_history)\n",
|
1278 |
+
" assert isinstance(analysis, dict)\n",
|
1279 |
+
" assert all(k in analysis for k in [\n",
|
1280 |
+
" 'learning_points', 'gaps', 'strengths', 'suggested_objectives'\n",
|
1281 |
+
" ])\n",
|
1282 |
+
" \n",
|
1283 |
+
" # Test rotation updates\n",
|
1284 |
+
" rotation = interface.update_rotation(\n",
|
1285 |
+
" \"Emergency Medicine\",\n",
|
1286 |
+
" \"2025-01-01\",\n",
|
1287 |
+
" \"2025-03-31\",\n",
|
1288 |
+
" [\"Resuscitation\", \"Procedures\"]\n",
|
1289 |
+
" )\n",
|
1290 |
+
" assert rotation[\"specialty\"] == \"Emergency Medicine\"\n",
|
1291 |
+
" assert \"Resuscitation\" in rotation[\"key_focus_areas\"]\n",
|
1292 |
+
" \n",
|
1293 |
+
" # Test objective management\n",
|
1294 |
+
" objectives = interface.toggle_objective(\"Improve chest pain assessment\", False)\n",
|
1295 |
+
" assert len(objectives) == 1\n",
|
1296 |
+
" assert objectives[0][\"status\"] == \"active\"\n",
|
1297 |
+
" \n",
|
1298 |
+
" objectives = interface.toggle_objective(\"Improve chest pain assessment\", True)\n",
|
1299 |
+
" assert objectives[0][\"status\"] == \"completed\"\n",
|
1300 |
+
" \n",
|
1301 |
+
" # Test feedback preferences\n",
|
1302 |
+
" preferences = interface.toggle_feedback(\"Include more ddx\", True)\n",
|
1303 |
+
" assert len(preferences) == 1\n",
|
1304 |
+
" assert preferences[0][\"active\"] == True\n",
|
1305 |
+
" \n",
|
1306 |
+
" print(\"Interface tests passed!\")\n",
|
1307 |
+
"\n",
|
1308 |
+
"# Run tests\n",
|
1309 |
+
"if __name__ == \"__main__\":\n",
|
1310 |
+
" import asyncio\n",
|
1311 |
+
" if not asyncio.get_event_loop().is_running():\n",
|
1312 |
+
" asyncio.run(test_learning_interface())"
|
1313 |
+
]
|
1314 |
+
}
|
1315 |
+
],
|
1316 |
+
"metadata": {
|
1317 |
+
"kernelspec": {
|
1318 |
+
"display_name": "python3",
|
1319 |
+
"language": "python",
|
1320 |
+
"name": "python3"
|
1321 |
+
}
|
1322 |
+
},
|
1323 |
+
"nbformat": 4,
|
1324 |
+
"nbformat_minor": 5
|
1325 |
+
}
|
nbs/03_utils.ipynb
ADDED
@@ -0,0 +1,210 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"cells": [
|
3 |
+
{
|
4 |
+
"cell_type": "code",
|
5 |
+
"execution_count": null,
|
6 |
+
"id": "ecaeafe7-8cef-4117-8b58-bcf774175b88",
|
7 |
+
"metadata": {},
|
8 |
+
"outputs": [],
|
9 |
+
"source": [
|
10 |
+
"#| default_exp utils"
|
11 |
+
]
|
12 |
+
},
|
13 |
+
{
|
14 |
+
"cell_type": "markdown",
|
15 |
+
"id": "95ff67c2-a1a7-4c35-b704-f0a8beb6822e",
|
16 |
+
"metadata": {},
|
17 |
+
"source": [
|
18 |
+
"# Utils\n",
|
19 |
+
"\n",
|
20 |
+
"> Shared utilities for the entire learning system"
|
21 |
+
]
|
22 |
+
},
|
23 |
+
{
|
24 |
+
"cell_type": "markdown",
|
25 |
+
"id": "1ceb8d6e-dbdf-4d3c-8956-7686284d9b9d",
|
26 |
+
"metadata": {},
|
27 |
+
"source": [
|
28 |
+
"## Setup"
|
29 |
+
]
|
30 |
+
},
|
31 |
+
{
|
32 |
+
"cell_type": "code",
|
33 |
+
"execution_count": null,
|
34 |
+
"id": "25924b5e-01c9-44ca-a519-d399cf44ef07",
|
35 |
+
"metadata": {},
|
36 |
+
"outputs": [],
|
37 |
+
"source": [
|
38 |
+
"#| hide\n",
|
39 |
+
"from nbdev.showdoc import show_doc"
|
40 |
+
]
|
41 |
+
},
|
42 |
+
{
|
43 |
+
"cell_type": "code",
|
44 |
+
"execution_count": null,
|
45 |
+
"id": "5c3c4745-4603-49be-8394-3c16237e18bf",
|
46 |
+
"metadata": {},
|
47 |
+
"outputs": [],
|
48 |
+
"source": [
|
49 |
+
"#| export\n",
|
50 |
+
"from typing import Dict, List, Optional, Any, Tuple\n",
|
51 |
+
"import json\n",
|
52 |
+
"from pathlib import Path\n",
|
53 |
+
"import logging\n",
|
54 |
+
"from datetime import datetime"
|
55 |
+
]
|
56 |
+
},
|
57 |
+
{
|
58 |
+
"cell_type": "markdown",
|
59 |
+
"id": "26603641-e577-4c99-a2b4-34e77a79005b",
|
60 |
+
"metadata": {},
|
61 |
+
"source": [
|
62 |
+
"## Utilities"
|
63 |
+
]
|
64 |
+
},
|
65 |
+
{
|
66 |
+
"cell_type": "code",
|
67 |
+
"execution_count": null,
|
68 |
+
"id": "59cefaf9-f6b2-4617-bd16-c0ed068d400c",
|
69 |
+
"metadata": {},
|
70 |
+
"outputs": [],
|
71 |
+
"source": [
|
72 |
+
"#| export\n",
|
73 |
+
"def setup_logger(name: str) -> logging.Logger:\n",
|
74 |
+
" \"\"\"Set up module logger with consistent formatting\"\"\"\n",
|
75 |
+
" logger = logging.getLogger(name)\n",
|
76 |
+
" if not logger.handlers:\n",
|
77 |
+
" handler = logging.StreamHandler()\n",
|
78 |
+
" handler.setFormatter(\n",
|
79 |
+
" logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')\n",
|
80 |
+
" )\n",
|
81 |
+
" logger.addHandler(handler)\n",
|
82 |
+
" logger.setLevel(logging.INFO)\n",
|
83 |
+
" return logger\n",
|
84 |
+
"\n",
|
85 |
+
"logger = setup_logger(__name__)"
|
86 |
+
]
|
87 |
+
},
|
88 |
+
{
|
89 |
+
"cell_type": "code",
|
90 |
+
"execution_count": null,
|
91 |
+
"id": "86982e56-5813-4684-a557-a6509a3a4de0",
|
92 |
+
"metadata": {},
|
93 |
+
"outputs": [],
|
94 |
+
"source": [
|
95 |
+
"#| export\n",
|
96 |
+
"def load_context_safely(path: Path) -> Dict:\n",
|
97 |
+
" \"\"\"\n",
|
98 |
+
" Safely load learning context from JSON file.\n",
|
99 |
+
" \n",
|
100 |
+
" Args:\n",
|
101 |
+
" path: Path to context file\n",
|
102 |
+
" \n",
|
103 |
+
" Returns:\n",
|
104 |
+
" dict: Loaded context data\n",
|
105 |
+
" \n",
|
106 |
+
" Raises:\n",
|
107 |
+
" ValueError: If file is invalid or inaccessible\n",
|
108 |
+
" \"\"\"\n",
|
109 |
+
" try:\n",
|
110 |
+
" with open(path, 'r') as f:\n",
|
111 |
+
" return json.load(f)\n",
|
112 |
+
" except json.JSONDecodeError as e:\n",
|
113 |
+
" raise ValueError(f\"Invalid context file format: {str(e)}\")\n",
|
114 |
+
" except Exception as e:\n",
|
115 |
+
" raise ValueError(f\"Error loading context file: {str(e)}\")"
|
116 |
+
]
|
117 |
+
},
|
118 |
+
{
|
119 |
+
"cell_type": "code",
|
120 |
+
"execution_count": null,
|
121 |
+
"id": "ae53b367-4ca7-4c00-bed6-a6ff3dcf93cc",
|
122 |
+
"metadata": {},
|
123 |
+
"outputs": [],
|
124 |
+
"source": [
|
125 |
+
"#| export\n",
|
126 |
+
"def save_context_safely(context: Dict, path: Path) -> None:\n",
|
127 |
+
" \"\"\"\n",
|
128 |
+
" Safely save learning context to JSON file.\n",
|
129 |
+
" \n",
|
130 |
+
" Args:\n",
|
131 |
+
" context: Context data to save\n",
|
132 |
+
" path: Path to save file\n",
|
133 |
+
" \n",
|
134 |
+
" Raises:\n",
|
135 |
+
" ValueError: If save operation fails\n",
|
136 |
+
" \"\"\"\n",
|
137 |
+
" try:\n",
|
138 |
+
" with open(path, 'w') as f:\n",
|
139 |
+
" json.dump(context, f, indent=2)\n",
|
140 |
+
" except Exception as e:\n",
|
141 |
+
" raise ValueError(f\"Error saving context: {str(e)}\")"
|
142 |
+
]
|
143 |
+
},
|
144 |
+
{
|
145 |
+
"cell_type": "markdown",
|
146 |
+
"id": "35c98021-745a-441b-8abf-4f4ee5468e1b",
|
147 |
+
"metadata": {},
|
148 |
+
"source": [
|
149 |
+
"## Tests"
|
150 |
+
]
|
151 |
+
},
|
152 |
+
{
|
153 |
+
"cell_type": "code",
|
154 |
+
"execution_count": null,
|
155 |
+
"id": "e09d2392-e5c4-4dbf-b784-be0acb1fb8fc",
|
156 |
+
"metadata": {},
|
157 |
+
"outputs": [],
|
158 |
+
"source": [
|
159 |
+
"def test_utils():\n",
|
160 |
+
" \"\"\"Test utility functions\"\"\"\n",
|
161 |
+
" # Test logger setup\n",
|
162 |
+
" test_logger = setup_logger(\"test\")\n",
|
163 |
+
" assert test_logger.level == logging.INFO\n",
|
164 |
+
" assert len(test_logger.handlers) == 1\n",
|
165 |
+
" \n",
|
166 |
+
" # Test context loading/saving\n",
|
167 |
+
" test_path = Path(\"test_context.json\")\n",
|
168 |
+
" test_data = {\n",
|
169 |
+
" \"current_rotation\": {\"specialty\": \"ED\"},\n",
|
170 |
+
" \"learning_objectives\": [],\n",
|
171 |
+
" \"knowledge_profile\": {\"gaps\": {}, \"strengths\": []}\n",
|
172 |
+
" }\n",
|
173 |
+
" \n",
|
174 |
+
" # Test save\n",
|
175 |
+
" save_context_safely(test_data, test_path)\n",
|
176 |
+
" assert test_path.exists()\n",
|
177 |
+
" \n",
|
178 |
+
" # Test load\n",
|
179 |
+
" loaded = load_context_safely(test_path)\n",
|
180 |
+
" assert loaded == test_data\n",
|
181 |
+
" \n",
|
182 |
+
" # Cleanup\n",
|
183 |
+
" test_path.unlink()\n",
|
184 |
+
" \n",
|
185 |
+
" print(\"Utility tests passed!\")\n",
|
186 |
+
"\n",
|
187 |
+
"# Run tests\n",
|
188 |
+
"if __name__ == \"__main__\":\n",
|
189 |
+
" test_utils()"
|
190 |
+
]
|
191 |
+
},
|
192 |
+
{
|
193 |
+
"cell_type": "code",
|
194 |
+
"execution_count": null,
|
195 |
+
"id": "e91d5138-1ca0-4057-8a13-af0b89dc0a93",
|
196 |
+
"metadata": {},
|
197 |
+
"outputs": [],
|
198 |
+
"source": []
|
199 |
+
}
|
200 |
+
],
|
201 |
+
"metadata": {
|
202 |
+
"kernelspec": {
|
203 |
+
"display_name": "python3",
|
204 |
+
"language": "python",
|
205 |
+
"name": "python3"
|
206 |
+
}
|
207 |
+
},
|
208 |
+
"nbformat": 4,
|
209 |
+
"nbformat_minor": 5
|
210 |
+
}
|
nbs/_quarto.yml
ADDED
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
project:
|
2 |
+
type: website
|
3 |
+
|
4 |
+
format:
|
5 |
+
html:
|
6 |
+
theme: cosmo
|
7 |
+
css: styles.css
|
8 |
+
toc: true
|
9 |
+
keep-md: true
|
10 |
+
commonmark: default
|
11 |
+
|
12 |
+
website:
|
13 |
+
twitter-card: true
|
14 |
+
open-graph: true
|
15 |
+
repo-actions: [issue]
|
16 |
+
navbar:
|
17 |
+
background: primary
|
18 |
+
search: true
|
19 |
+
sidebar:
|
20 |
+
style: floating
|
21 |
+
|
22 |
+
metadata-files: [nbdev.yml, sidebar.yml]
|
nbs/index.ipynb
ADDED
@@ -0,0 +1,180 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"cells": [
|
3 |
+
{
|
4 |
+
"cell_type": "code",
|
5 |
+
"execution_count": null,
|
6 |
+
"metadata": {},
|
7 |
+
"outputs": [],
|
8 |
+
"source": [
|
9 |
+
"#| hide\n",
|
10 |
+
"from wardbuddy.core import *"
|
11 |
+
]
|
12 |
+
},
|
13 |
+
{
|
14 |
+
"cell_type": "markdown",
|
15 |
+
"metadata": {},
|
16 |
+
"source": [
|
17 |
+
"# wardbuddy\n",
|
18 |
+
"\n",
|
19 |
+
"> Making placement learning fun and effective for medical students with a personalised AI system"
|
20 |
+
]
|
21 |
+
},
|
22 |
+
{
|
23 |
+
"cell_type": "markdown",
|
24 |
+
"metadata": {},
|
25 |
+
"source": [
|
26 |
+
"This file will become your README and also the index of your documentation."
|
27 |
+
]
|
28 |
+
},
|
29 |
+
{
|
30 |
+
"cell_type": "markdown",
|
31 |
+
"metadata": {},
|
32 |
+
"source": [
|
33 |
+
"## Developer Guide"
|
34 |
+
]
|
35 |
+
},
|
36 |
+
{
|
37 |
+
"cell_type": "markdown",
|
38 |
+
"metadata": {},
|
39 |
+
"source": [
|
40 |
+
"If you are new to using `nbdev` here are some useful pointers to get you started."
|
41 |
+
]
|
42 |
+
},
|
43 |
+
{
|
44 |
+
"cell_type": "markdown",
|
45 |
+
"metadata": {},
|
46 |
+
"source": [
|
47 |
+
"### Install wardbuddy in Development mode"
|
48 |
+
]
|
49 |
+
},
|
50 |
+
{
|
51 |
+
"cell_type": "markdown",
|
52 |
+
"metadata": {},
|
53 |
+
"source": [
|
54 |
+
"```sh\n",
|
55 |
+
"# make sure wardbuddy package is installed in development mode\n",
|
56 |
+
"$ pip install -e .\n",
|
57 |
+
"\n",
|
58 |
+
"# make changes under nbs/ directory\n",
|
59 |
+
"# ...\n",
|
60 |
+
"\n",
|
61 |
+
"# compile to have changes apply to wardbuddy\n",
|
62 |
+
"$ nbdev_prepare\n",
|
63 |
+
"```"
|
64 |
+
]
|
65 |
+
},
|
66 |
+
{
|
67 |
+
"cell_type": "markdown",
|
68 |
+
"metadata": {},
|
69 |
+
"source": [
|
70 |
+
"## Usage"
|
71 |
+
]
|
72 |
+
},
|
73 |
+
{
|
74 |
+
"cell_type": "markdown",
|
75 |
+
"metadata": {},
|
76 |
+
"source": [
|
77 |
+
"### Installation"
|
78 |
+
]
|
79 |
+
},
|
80 |
+
{
|
81 |
+
"cell_type": "markdown",
|
82 |
+
"metadata": {},
|
83 |
+
"source": [
|
84 |
+
"Install latest from the GitHub [repository][repo]:\n",
|
85 |
+
"\n",
|
86 |
+
"```sh\n",
|
87 |
+
"$ pip install git+https://github.com/Dyadd/wardbuddy.git\n",
|
88 |
+
"```\n",
|
89 |
+
"\n",
|
90 |
+
"or from [conda][conda]\n",
|
91 |
+
"\n",
|
92 |
+
"```sh\n",
|
93 |
+
"$ conda install -c Dyadd wardbuddy\n",
|
94 |
+
"```\n",
|
95 |
+
"\n",
|
96 |
+
"or from [pypi][pypi]\n",
|
97 |
+
"\n",
|
98 |
+
"\n",
|
99 |
+
"```sh\n",
|
100 |
+
"$ pip install wardbuddy\n",
|
101 |
+
"```\n",
|
102 |
+
"\n",
|
103 |
+
"\n",
|
104 |
+
"[repo]: https://github.com/Dyadd/wardbuddy\n",
|
105 |
+
"[docs]: https://Dyadd.github.io/wardbuddy/\n",
|
106 |
+
"[pypi]: https://pypi.org/project/wardbuddy/\n",
|
107 |
+
"[conda]: https://anaconda.org/Dyadd/wardbuddy"
|
108 |
+
]
|
109 |
+
},
|
110 |
+
{
|
111 |
+
"cell_type": "markdown",
|
112 |
+
"metadata": {},
|
113 |
+
"source": [
|
114 |
+
"### Documentation"
|
115 |
+
]
|
116 |
+
},
|
117 |
+
{
|
118 |
+
"cell_type": "markdown",
|
119 |
+
"metadata": {},
|
120 |
+
"source": [
|
121 |
+
"Documentation can be found hosted on this GitHub [repository][repo]'s [pages][docs]. Additionally you can find package manager specific guidelines on [conda][conda] and [pypi][pypi] respectively.\n",
|
122 |
+
"\n",
|
123 |
+
"[repo]: https://github.com/Dyadd/wardbuddy\n",
|
124 |
+
"[docs]: https://Dyadd.github.io/wardbuddy/\n",
|
125 |
+
"[pypi]: https://pypi.org/project/wardbuddy/\n",
|
126 |
+
"[conda]: https://anaconda.org/Dyadd/wardbuddy"
|
127 |
+
]
|
128 |
+
},
|
129 |
+
{
|
130 |
+
"cell_type": "markdown",
|
131 |
+
"metadata": {},
|
132 |
+
"source": [
|
133 |
+
"## How to use"
|
134 |
+
]
|
135 |
+
},
|
136 |
+
{
|
137 |
+
"cell_type": "markdown",
|
138 |
+
"metadata": {},
|
139 |
+
"source": [
|
140 |
+
"Fill me in please! Don't forget code examples:"
|
141 |
+
]
|
142 |
+
},
|
143 |
+
{
|
144 |
+
"cell_type": "code",
|
145 |
+
"execution_count": null,
|
146 |
+
"metadata": {},
|
147 |
+
"outputs": [
|
148 |
+
{
|
149 |
+
"data": {
|
150 |
+
"text/plain": [
|
151 |
+
"2"
|
152 |
+
]
|
153 |
+
},
|
154 |
+
"execution_count": null,
|
155 |
+
"metadata": {},
|
156 |
+
"output_type": "execute_result"
|
157 |
+
}
|
158 |
+
],
|
159 |
+
"source": [
|
160 |
+
"1+1"
|
161 |
+
]
|
162 |
+
},
|
163 |
+
{
|
164 |
+
"cell_type": "code",
|
165 |
+
"execution_count": null,
|
166 |
+
"metadata": {},
|
167 |
+
"outputs": [],
|
168 |
+
"source": []
|
169 |
+
}
|
170 |
+
],
|
171 |
+
"metadata": {
|
172 |
+
"kernelspec": {
|
173 |
+
"display_name": "python3",
|
174 |
+
"language": "python",
|
175 |
+
"name": "python3"
|
176 |
+
}
|
177 |
+
},
|
178 |
+
"nbformat": 4,
|
179 |
+
"nbformat_minor": 4
|
180 |
+
}
|
nbs/main.ipynb
ADDED
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"cells": [
|
3 |
+
{
|
4 |
+
"cell_type": "code",
|
5 |
+
"execution_count": null,
|
6 |
+
"id": "2aea50e4-af9b-419c-afd4-733b8ce5f1be",
|
7 |
+
"metadata": {},
|
8 |
+
"outputs": [],
|
9 |
+
"source": [
|
10 |
+
"#| default_exp main"
|
11 |
+
]
|
12 |
+
},
|
13 |
+
{
|
14 |
+
"cell_type": "code",
|
15 |
+
"execution_count": null,
|
16 |
+
"id": "9eca9cd0-fe04-4c4f-b434-1e9106bff73c",
|
17 |
+
"metadata": {},
|
18 |
+
"outputs": [],
|
19 |
+
"source": [
|
20 |
+
"# | export\n",
|
21 |
+
"\"\"\"Main entry point for the WardBuddy application.\"\"\"\n",
|
22 |
+
"import asyncio\n",
|
23 |
+
"from pathlib import Path\n",
|
24 |
+
"import typer\n",
|
25 |
+
"from typing import Optional\n",
|
26 |
+
"import logging\n",
|
27 |
+
"\n",
|
28 |
+
"from wardbuddy.learning_interface import LearningInterface\n",
|
29 |
+
"\n",
|
30 |
+
"# Configure logging\n",
|
31 |
+
"logging.basicConfig(level=logging.INFO)\n",
|
32 |
+
"logger = logging.getLogger(__name__)\n",
|
33 |
+
"\n",
|
34 |
+
"app = typer.Typer()\n",
|
35 |
+
"\n",
|
36 |
+
"@app.command()\n",
|
37 |
+
"def launch(\n",
|
38 |
+
" port: int = typer.Option(7860, help=\"Port to run on\"),\n",
|
39 |
+
" context_path: Optional[Path] = typer.Option(\n",
|
40 |
+
" None,\n",
|
41 |
+
" help=\"Path to context file\",\n",
|
42 |
+
" dir_okay=False\n",
|
43 |
+
" ),\n",
|
44 |
+
" model: str = typer.Option(\n",
|
45 |
+
" \"anthropic/claude-3-sonnet\",\n",
|
46 |
+
" help=\"OpenRouter model identifier\"\n",
|
47 |
+
" ),\n",
|
48 |
+
" share: bool = typer.Option(\n",
|
49 |
+
" False,\n",
|
50 |
+
" help=\"Create public URL\"\n",
|
51 |
+
" )\n",
|
52 |
+
") -> None:\n",
|
53 |
+
" \"\"\"Launch the clinical learning interface.\"\"\"\n",
|
54 |
+
" try:\n",
|
55 |
+
" interface = LearningInterface(\n",
|
56 |
+
" context_path=context_path,\n",
|
57 |
+
" model=model\n",
|
58 |
+
" )\n",
|
59 |
+
" \n",
|
60 |
+
" app = interface.create_interface()\n",
|
61 |
+
" app.launch(\n",
|
62 |
+
" server_port=port,\n",
|
63 |
+
" share=share\n",
|
64 |
+
" )\n",
|
65 |
+
" \n",
|
66 |
+
" logger.info(f\"Interface launched on port {port}\")\n",
|
67 |
+
" \n",
|
68 |
+
" except Exception as e:\n",
|
69 |
+
" logger.error(f\"Error launching interface: {str(e)}\")\n",
|
70 |
+
" raise typer.Exit(1)\n",
|
71 |
+
"\n",
|
72 |
+
"def run_app():\n",
|
73 |
+
" \"\"\"Entry point for command line.\"\"\"\n",
|
74 |
+
" app()"
|
75 |
+
]
|
76 |
+
}
|
77 |
+
],
|
78 |
+
"metadata": {
|
79 |
+
"kernelspec": {
|
80 |
+
"display_name": "python3",
|
81 |
+
"language": "python",
|
82 |
+
"name": "python3"
|
83 |
+
}
|
84 |
+
},
|
85 |
+
"nbformat": 4,
|
86 |
+
"nbformat_minor": 5
|
87 |
+
}
|
nbs/nbdev.yml
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
project:
|
2 |
+
output-dir: _docs
|
3 |
+
|
4 |
+
website:
|
5 |
+
title: "wardbuddy"
|
6 |
+
site-url: "https://Dyadd.github.io/wardbuddy"
|
7 |
+
description: "Making placement learning fun and effective for medical students with a personalised AI system"
|
8 |
+
repo-branch: main
|
9 |
+
repo-url: "https://github.com/Dyadd/wardbuddy"
|
nbs/sidebar.yml
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
website:
|
2 |
+
sidebar:
|
3 |
+
contents:
|
4 |
+
- index.ipynb
|
5 |
+
- 00_core.ipynb
|
nbs/styles.css
ADDED
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
.gr-button {
|
3 |
+
border-radius: 8px;
|
4 |
+
padding: 10px 20px;
|
5 |
+
}
|
6 |
+
|
7 |
+
.gr-button:hover {
|
8 |
+
background-color: #2c5282;
|
9 |
+
color: white;
|
10 |
+
}
|
11 |
+
|
12 |
+
.message {
|
13 |
+
padding: 15px;
|
14 |
+
border-radius: 10px;
|
15 |
+
margin: 5px 0;
|
16 |
+
}
|
17 |
+
|
18 |
+
.user-message {
|
19 |
+
background-color: #e2e8f0;
|
20 |
+
}
|
21 |
+
|
22 |
+
.assistant-message {
|
23 |
+
background-color: #ebf8ff;
|
24 |
+
}
|
25 |
+
|
pyproject.toml
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
[build-system]
|
2 |
+
requires = ["setuptools>=64.0"]
|
3 |
+
build-backend = "setuptools.build_meta"
|
requirements.txt
ADDED
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Core Python dependencies
|
2 |
+
aiohttp
|
3 |
+
python-dotenv
|
4 |
+
gradio
|
5 |
+
pandas
|
6 |
+
pathlib
|
7 |
+
huggingface_hub
|
8 |
+
|
9 |
+
# Logging and utilities
|
10 |
+
|
11 |
+
jsonschema
|
12 |
+
typing-extensions
|
13 |
+
|
14 |
+
# Development and testing (optional)
|
15 |
+
# pytest
|
16 |
+
# mypy
|
17 |
+
|
18 |
+
# API and LLM interaction
|
19 |
+
# OpenRouter API is used, but no specific library needed beyond aiohttp
|
settings.ini
ADDED
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
[DEFAULT]
|
2 |
+
# All sections below are required unless otherwise specified.
|
3 |
+
# See https://github.com/AnswerDotAI/nbdev/blob/main/settings.ini for examples.
|
4 |
+
|
5 |
+
### Python library ###
|
6 |
+
repo = wardbuddy
|
7 |
+
lib_name = %(repo)s
|
8 |
+
version = 0.0.1
|
9 |
+
min_python = 3.7
|
10 |
+
license = apache2
|
11 |
+
black_formatting = False
|
12 |
+
|
13 |
+
### nbdev ###
|
14 |
+
doc_path = _docs
|
15 |
+
lib_path = wardbuddy
|
16 |
+
nbs_path = nbs
|
17 |
+
recursive = True
|
18 |
+
tst_flags = notest
|
19 |
+
put_version_in_init = True
|
20 |
+
|
21 |
+
### Docs ###
|
22 |
+
branch = main
|
23 |
+
custom_sidebar = False
|
24 |
+
doc_host = https://%(user)s.github.io
|
25 |
+
doc_baseurl = /%(repo)s
|
26 |
+
git_url = https://github.com/%(user)s/%(repo)s
|
27 |
+
title = %(lib_name)s
|
28 |
+
|
29 |
+
### PyPI ###
|
30 |
+
audience = Developers
|
31 |
+
author = Dyadd
|
32 |
+
author_email = [email protected]
|
33 |
+
copyright = 2025 onwards, %(author)s
|
34 |
+
description = Making placement learning fun and effective for medical students with a personalised AI system
|
35 |
+
keywords = nbdev jupyter notebook python
|
36 |
+
language = English
|
37 |
+
status = 3
|
38 |
+
user = Dyadd
|
39 |
+
|
40 |
+
### Optional ###
|
41 |
+
requirements = nbdev>=2.3.0 gradio>=4.0.0 requests>=2.31.0 python-dotenv>=1.0.0 aiohttp>=3.9.0
|
42 |
+
# dev_requirements =
|
43 |
+
# console_scripts =
|
44 |
+
# conda_user =
|
45 |
+
# package_data =
|
setup.py
ADDED
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from pkg_resources import parse_version
|
2 |
+
from configparser import ConfigParser
|
3 |
+
import setuptools, shlex
|
4 |
+
assert parse_version(setuptools.__version__)>=parse_version('36.2')
|
5 |
+
|
6 |
+
# note: all settings are in settings.ini; edit there, not here
|
7 |
+
config = ConfigParser(delimiters=['='])
|
8 |
+
config.read('settings.ini', encoding='utf-8')
|
9 |
+
cfg = config['DEFAULT']
|
10 |
+
|
11 |
+
cfg_keys = 'version description keywords author author_email'.split()
|
12 |
+
expected = cfg_keys + "lib_name user branch license status min_python audience language".split()
|
13 |
+
for o in expected: assert o in cfg, "missing expected setting: {}".format(o)
|
14 |
+
setup_cfg = {o:cfg[o] for o in cfg_keys}
|
15 |
+
|
16 |
+
licenses = {
|
17 |
+
'apache2': ('Apache Software License 2.0','OSI Approved :: Apache Software License'),
|
18 |
+
'mit': ('MIT License', 'OSI Approved :: MIT License'),
|
19 |
+
'gpl2': ('GNU General Public License v2', 'OSI Approved :: GNU General Public License v2 (GPLv2)'),
|
20 |
+
'gpl3': ('GNU General Public License v3', 'OSI Approved :: GNU General Public License v3 (GPLv3)'),
|
21 |
+
'bsd3': ('BSD License', 'OSI Approved :: BSD License'),
|
22 |
+
}
|
23 |
+
statuses = [ '1 - Planning', '2 - Pre-Alpha', '3 - Alpha',
|
24 |
+
'4 - Beta', '5 - Production/Stable', '6 - Mature', '7 - Inactive' ]
|
25 |
+
py_versions = '3.6 3.7 3.8 3.9 3.10 3.11 3.12'.split()
|
26 |
+
|
27 |
+
requirements = shlex.split(cfg.get('requirements', ''))
|
28 |
+
if cfg.get('pip_requirements'): requirements += shlex.split(cfg.get('pip_requirements', ''))
|
29 |
+
min_python = cfg['min_python']
|
30 |
+
lic = licenses.get(cfg['license'].lower(), (cfg['license'], None))
|
31 |
+
dev_requirements = (cfg.get('dev_requirements') or '').split()
|
32 |
+
|
33 |
+
package_data = dict()
|
34 |
+
pkg_data = cfg.get('package_data', None)
|
35 |
+
if pkg_data:
|
36 |
+
package_data[cfg['lib_name']] = pkg_data.split() # split as multiple files might be listed
|
37 |
+
# Add package data to setup_cfg for setuptools.setup(..., **setup_cfg)
|
38 |
+
setup_cfg['package_data'] = package_data
|
39 |
+
|
40 |
+
setuptools.setup(
|
41 |
+
name = cfg['lib_name'],
|
42 |
+
license = lic[0],
|
43 |
+
classifiers = [
|
44 |
+
'Development Status :: ' + statuses[int(cfg['status'])],
|
45 |
+
'Intended Audience :: ' + cfg['audience'].title(),
|
46 |
+
'Natural Language :: ' + cfg['language'].title(),
|
47 |
+
] + ['Programming Language :: Python :: '+o for o in py_versions[py_versions.index(min_python):]] + (['License :: ' + lic[1] ] if lic[1] else []),
|
48 |
+
url = cfg['git_url'],
|
49 |
+
packages = setuptools.find_packages(),
|
50 |
+
include_package_data = True,
|
51 |
+
install_requires = requirements,
|
52 |
+
extras_require={ 'dev': dev_requirements },
|
53 |
+
dependency_links = cfg.get('dep_links','').split(),
|
54 |
+
python_requires = '>=' + cfg['min_python'],
|
55 |
+
long_description = open('README.md', encoding='utf-8').read(),
|
56 |
+
long_description_content_type = 'text/markdown',
|
57 |
+
zip_safe = False,
|
58 |
+
entry_points = {
|
59 |
+
'console_scripts': cfg.get('console_scripts','').split(),
|
60 |
+
'nbdev': [f'{cfg.get("lib_path")}={cfg.get("lib_path")}._modidx:d']
|
61 |
+
},
|
62 |
+
**setup_cfg)
|
63 |
+
|
64 |
+
|
wardbuddy/__init__.py
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
__version__ = "0.0.1"
|
wardbuddy/_modidx.py
ADDED
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Autogenerated by nbdev
|
2 |
+
|
3 |
+
d = { 'settings': { 'branch': 'main',
|
4 |
+
'doc_baseurl': '/wardbuddy',
|
5 |
+
'doc_host': 'https://Dyadd.github.io',
|
6 |
+
'git_url': 'https://github.com/Dyadd/wardbuddy',
|
7 |
+
'lib_path': 'wardbuddy'},
|
8 |
+
'syms': { 'wardbuddy.clinical_tutor': { 'wardbuddy.clinical_tutor.ClinicalTutor': ( 'clinical_tutor.html#clinicaltutor',
|
9 |
+
'wardbuddy/clinical_tutor.py'),
|
10 |
+
'wardbuddy.clinical_tutor.ClinicalTutor.__init__': ( 'clinical_tutor.html#clinicaltutor.__init__',
|
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',
|
19 |
+
'wardbuddy/clinical_tutor.py'),
|
20 |
+
'wardbuddy.clinical_tutor.ClinicalTutor.end_discussion': ( 'clinical_tutor.html#clinicaltutor.end_discussion',
|
21 |
+
'wardbuddy/clinical_tutor.py'),
|
22 |
+
'wardbuddy.clinical_tutor.ClinicalTutor.generate_smart_goal': ( 'clinical_tutor.html#clinicaltutor.generate_smart_goal',
|
23 |
+
'wardbuddy/clinical_tutor.py'),
|
24 |
+
'wardbuddy.clinical_tutor.ClinicalTutor.generate_smart_goals': ( 'clinical_tutor.html#clinicaltutor.generate_smart_goals',
|
25 |
+
'wardbuddy/clinical_tutor.py'),
|
26 |
+
'wardbuddy.clinical_tutor.ClinicalTutor.get_discussion_history': ( 'clinical_tutor.html#clinicaltutor.get_discussion_history',
|
27 |
+
'wardbuddy/clinical_tutor.py'),
|
28 |
+
'wardbuddy.clinical_tutor.OpenRouterException': ( 'clinical_tutor.html#openrouterexception',
|
29 |
+
'wardbuddy/clinical_tutor.py')},
|
30 |
+
'wardbuddy.core': {'wardbuddy.core.foo': ('core.html#foo', 'wardbuddy/core.py')},
|
31 |
+
'wardbuddy.learning_context': { 'wardbuddy.learning_context.CategoryProgress': ( 'learning_context.html#categoryprogress',
|
32 |
+
'wardbuddy/learning_context.py'),
|
33 |
+
'wardbuddy.learning_context.LearningCategory': ( 'learning_context.html#learningcategory',
|
34 |
+
'wardbuddy/learning_context.py'),
|
35 |
+
'wardbuddy.learning_context.LearningContext': ( 'learning_context.html#learningcontext',
|
36 |
+
'wardbuddy/learning_context.py'),
|
37 |
+
'wardbuddy.learning_context.LearningContext.__init__': ( 'learning_context.html#learningcontext.__init__',
|
38 |
+
'wardbuddy/learning_context.py'),
|
39 |
+
'wardbuddy.learning_context.LearningContext._save_context': ( 'learning_context.html#learningcontext._save_context',
|
40 |
+
'wardbuddy/learning_context.py'),
|
41 |
+
'wardbuddy.learning_context.LearningContext.add_smart_goal': ( 'learning_context.html#learningcontext.add_smart_goal',
|
42 |
+
'wardbuddy/learning_context.py'),
|
43 |
+
'wardbuddy.learning_context.LearningContext.complete_active_goal': ( 'learning_context.html#learningcontext.complete_active_goal',
|
44 |
+
'wardbuddy/learning_context.py'),
|
45 |
+
'wardbuddy.learning_context.LearningContext.get_all_goals': ( 'learning_context.html#learningcontext.get_all_goals',
|
46 |
+
'wardbuddy/learning_context.py'),
|
47 |
+
'wardbuddy.learning_context.LearningContext.get_category_summary': ( 'learning_context.html#learningcontext.get_category_summary',
|
48 |
+
'wardbuddy/learning_context.py'),
|
49 |
+
'wardbuddy.learning_context.LearningContext.load_context': ( 'learning_context.html#learningcontext.load_context',
|
50 |
+
'wardbuddy/learning_context.py'),
|
51 |
+
'wardbuddy.learning_context.LearningContext.update_rotation': ( 'learning_context.html#learningcontext.update_rotation',
|
52 |
+
'wardbuddy/learning_context.py'),
|
53 |
+
'wardbuddy.learning_context.RotationContext': ( 'learning_context.html#rotationcontext',
|
54 |
+
'wardbuddy/learning_context.py'),
|
55 |
+
'wardbuddy.learning_context.SmartGoal': ( 'learning_context.html#smartgoal',
|
56 |
+
'wardbuddy/learning_context.py'),
|
57 |
+
'wardbuddy.learning_context.SmartGoal.Config': ( 'learning_context.html#smartgoal.config',
|
58 |
+
'wardbuddy/learning_context.py')},
|
59 |
+
'wardbuddy.learning_interface': { 'wardbuddy.learning_interface.LearningInterface': ( 'learning_interface.html#learninginterface',
|
60 |
+
'wardbuddy/learning_interface.py'),
|
61 |
+
'wardbuddy.learning_interface.LearningInterface.__init__': ( 'learning_interface.html#learninginterface.__init__',
|
62 |
+
'wardbuddy/learning_interface.py'),
|
63 |
+
'wardbuddy.learning_interface.LearningInterface._update_displays': ( 'learning_interface.html#learninginterface._update_displays',
|
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'),
|
70 |
+
'wardbuddy.main.run_app': ('main.html#run_app', 'wardbuddy/main.py')},
|
71 |
+
'wardbuddy.utils': { 'wardbuddy.utils.load_context_safely': ('utils.html#load_context_safely', 'wardbuddy/utils.py'),
|
72 |
+
'wardbuddy.utils.save_context_safely': ('utils.html#save_context_safely', 'wardbuddy/utils.py'),
|
73 |
+
'wardbuddy.utils.setup_logger': ('utils.html#setup_logger', 'wardbuddy/utils.py')}}}
|
wardbuddy/clinical_tutor.py
ADDED
@@ -0,0 +1,315 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""Core module for using learning context for context-appropriate tutor responses"""
|
2 |
+
|
3 |
+
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/01_clinical_tutor.ipynb.
|
4 |
+
|
5 |
+
# %% auto 0
|
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
|
13 |
+
import asyncio
|
14 |
+
import uuid
|
15 |
+
from datetime import datetime
|
16 |
+
from pathlib import Path
|
17 |
+
import aiohttp
|
18 |
+
from pydantic import BaseModel
|
19 |
+
from dotenv import load_dotenv
|
20 |
+
from .learning_context import LearningContext, setup_logger, LearningCategory, SmartGoal, RotationContext
|
21 |
+
|
22 |
+
|
23 |
+
# Load environment variables
|
24 |
+
load_dotenv()
|
25 |
+
|
26 |
+
logger = setup_logger(__name__)
|
27 |
+
|
28 |
+
# %% ../nbs/01_clinical_tutor.ipynb 7
|
29 |
+
class OpenRouterException(Exception):
|
30 |
+
"""Custom exception for OpenRouter API errors"""
|
31 |
+
pass
|
32 |
+
|
33 |
+
# %% ../nbs/01_clinical_tutor.ipynb 8
|
34 |
+
class ClinicalTutor:
|
35 |
+
"""
|
36 |
+
Clinical teaching system using LLMs for goal setting and case discussion.
|
37 |
+
|
38 |
+
Features:
|
39 |
+
- SMART goal generation
|
40 |
+
- Case discussion
|
41 |
+
- Progress tracking
|
42 |
+
"""
|
43 |
+
|
44 |
+
def __init__(
|
45 |
+
self,
|
46 |
+
context_path: Optional[Path] = None,
|
47 |
+
model: str = "anthropic/claude-3-sonnet",
|
48 |
+
api_key: Optional[str] = None
|
49 |
+
):
|
50 |
+
"""
|
51 |
+
Initialize clinical tutor.
|
52 |
+
|
53 |
+
Args:
|
54 |
+
context_path: Path for context persistence
|
55 |
+
model: OpenRouter model identifier
|
56 |
+
api_key: OpenRouter API key (falls back to env var)
|
57 |
+
"""
|
58 |
+
self.api_key = api_key or os.getenv("OPENROUTER_API_KEY")
|
59 |
+
if not self.api_key:
|
60 |
+
raise ValueError("OpenRouter API key required")
|
61 |
+
|
62 |
+
self.api_url = "https://openrouter.ai/api/v1/chat/completions"
|
63 |
+
self.model = model
|
64 |
+
|
65 |
+
self.learning_context = LearningContext(context_path)
|
66 |
+
|
67 |
+
# Track current discussion
|
68 |
+
self.current_discussion: List[Dict] = []
|
69 |
+
|
70 |
+
logger.info("Clinical tutor initialized")
|
71 |
+
|
72 |
+
async def generate_smart_goals(
|
73 |
+
self,
|
74 |
+
specialty: str,
|
75 |
+
setting: str,
|
76 |
+
num_goals: int = 3
|
77 |
+
) -> List[SmartGoal]:
|
78 |
+
"""
|
79 |
+
Generate SMART goals for rotation context.
|
80 |
+
|
81 |
+
Args:
|
82 |
+
specialty: Medical specialty
|
83 |
+
setting: Clinical setting
|
84 |
+
num_goals: Number of goals to generate
|
85 |
+
|
86 |
+
Returns:
|
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",
|
104 |
+
"content": prompt
|
105 |
+
}])
|
106 |
+
|
107 |
+
# Parse response to extract goals
|
108 |
+
goals_data = json.loads(response)
|
109 |
+
|
110 |
+
# Convert to SmartGoal objects
|
111 |
+
goals = []
|
112 |
+
for data in goals_data:
|
113 |
+
goal = SmartGoal(
|
114 |
+
id=f"goal_{uuid.uuid4()}",
|
115 |
+
category=LearningCategory(data["category"]),
|
116 |
+
original_input="", # Auto-generated
|
117 |
+
smart_version=data["smart_version"],
|
118 |
+
specialty=specialty,
|
119 |
+
setting=setting,
|
120 |
+
created_at=datetime.now()
|
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,
|
133 |
+
specialty: str,
|
134 |
+
setting: str
|
135 |
+
) -> Optional[SmartGoal]:
|
136 |
+
"""
|
137 |
+
Generate SMART goal from user input.
|
138 |
+
|
139 |
+
Args:
|
140 |
+
user_input: User's goal description
|
141 |
+
specialty: Current specialty
|
142 |
+
setting: Current setting
|
143 |
+
|
144 |
+
Returns:
|
145 |
+
SmartGoal: Generated SMART goal
|
146 |
+
"""
|
147 |
+
prompt = f"""Convert this learning goal into a SMART goal (Specific, Measurable, Achievable, Relevant, Time-bound) for {specialty} ({setting}):
|
148 |
+
|
149 |
+
"{user_input}"
|
150 |
+
|
151 |
+
1. Select the most appropriate category from: {', '.join(cat.value for cat in LearningCategory)}
|
152 |
+
2. Rewrite as a SMART goal specific to {setting} in {specialty}
|
153 |
+
|
154 |
+
Format as JSON with fields:
|
155 |
+
- category: Learning category name
|
156 |
+
- smart_version: SMART formatted goal text"""
|
157 |
+
|
158 |
+
try:
|
159 |
+
response = await self._get_completion([{
|
160 |
+
"role": "system",
|
161 |
+
"content": prompt
|
162 |
+
}])
|
163 |
+
|
164 |
+
# Parse response
|
165 |
+
data = json.loads(response)
|
166 |
+
|
167 |
+
return SmartGoal(
|
168 |
+
id=f"goal_{uuid.uuid4()}",
|
169 |
+
category=LearningCategory(data["category"]),
|
170 |
+
original_input=user_input,
|
171 |
+
smart_version=data["smart_version"],
|
172 |
+
specialty=specialty,
|
173 |
+
setting=setting,
|
174 |
+
created_at=datetime.now()
|
175 |
+
)
|
176 |
+
|
177 |
+
except Exception as e:
|
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
|
193 |
+
self.current_discussion.append({
|
194 |
+
"role": "user",
|
195 |
+
"content": message
|
196 |
+
})
|
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."""
|
224 |
+
if self.learning_context.active_goal:
|
225 |
+
self.learning_context.complete_active_goal()
|
226 |
+
|
227 |
+
self.current_discussion = []
|
228 |
+
|
229 |
+
def _build_discussion_prompt(self) -> str:
|
230 |
+
"""Build context-aware discussion prompt."""
|
231 |
+
context = self.learning_context
|
232 |
+
rotation = context.rotation
|
233 |
+
active_goal = context.active_goal
|
234 |
+
|
235 |
+
return f"""You are an experienced clinical supervisor in {rotation.specialty}
|
236 |
+
working in a {rotation.setting} setting. Guide the learner through case discussion
|
237 |
+
using Socratic questioning and targeted feedback.
|
238 |
+
|
239 |
+
Current Learning Goal:
|
240 |
+
{active_goal.smart_version if active_goal else 'General clinical discussion'}
|
241 |
+
|
242 |
+
Approach:
|
243 |
+
1. Focus on clinical reasoning and decision-making
|
244 |
+
2. Ask targeted questions to explore understanding
|
245 |
+
3. Share relevant clinical pearls
|
246 |
+
4. Be conversational and engaging
|
247 |
+
5. Relate discussion to current learning goal where relevant
|
248 |
+
|
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):
|
286 |
+
try:
|
287 |
+
async with aiohttp.ClientSession() as session:
|
288 |
+
async with session.post(
|
289 |
+
self.api_url,
|
290 |
+
headers=headers,
|
291 |
+
json=data,
|
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.
|
307 |
+
|
308 |
+
Returns:
|
309 |
+
list: Discussion messages
|
310 |
+
"""
|
311 |
+
return self.current_discussion
|
312 |
+
|
313 |
+
def clear_discussion(self) -> None:
|
314 |
+
"""Clear current discussion history."""
|
315 |
+
self.current_discussion = []
|
wardbuddy/core.py
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""Fill in a module description here"""
|
2 |
+
|
3 |
+
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/00_core.ipynb.
|
4 |
+
|
5 |
+
# %% auto 0
|
6 |
+
__all__ = ['foo']
|
7 |
+
|
8 |
+
# %% ../nbs/00_core.ipynb 3
|
9 |
+
def foo(): pass
|
wardbuddy/learning_context.py
ADDED
@@ -0,0 +1,251 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""Core module for managing learning context (memory -> LOs, prior cases, knowledge gaps, feedback preferences)"""
|
2 |
+
|
3 |
+
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/00_learning_context.ipynb.
|
4 |
+
|
5 |
+
# %% auto 0
|
6 |
+
__all__ = ['logger', 'LearningCategory', 'RotationContext', 'SmartGoal', 'CategoryProgress', 'LearningContext']
|
7 |
+
|
8 |
+
# %% ../nbs/00_learning_context.ipynb 4
|
9 |
+
from typing import Dict, List, Optional
|
10 |
+
from datetime import datetime
|
11 |
+
from enum import Enum
|
12 |
+
from pydantic import BaseModel, Field
|
13 |
+
import json
|
14 |
+
from pathlib import Path
|
15 |
+
import logging
|
16 |
+
from .utils import setup_logger, load_context_safely, save_context_safely
|
17 |
+
|
18 |
+
# %% ../nbs/00_learning_context.ipynb 5
|
19 |
+
logger = setup_logger(__name__)
|
20 |
+
|
21 |
+
# %% ../nbs/00_learning_context.ipynb 8
|
22 |
+
class LearningCategory(str, Enum):
|
23 |
+
"""Main learning categories"""
|
24 |
+
HISTORY_TAKING = "History Taking"
|
25 |
+
PHYSICAL_EXAM = "Physical Examinations"
|
26 |
+
INVESTIGATIONS = "Investigations"
|
27 |
+
MANAGEMENT = "Management"
|
28 |
+
CLINICAL_REASONING = "Clinical Reasoning"
|
29 |
+
COMMUNICATION = "Communication & Presentation"
|
30 |
+
PROCEDURAL = "Procedural Skills"
|
31 |
+
MEDICAL_KNOWLEDGE = "Medical Knowledge"
|
32 |
+
|
33 |
+
class RotationContext(BaseModel):
|
34 |
+
"""Clinical rotation context"""
|
35 |
+
specialty: str = Field(..., description="Medical specialty")
|
36 |
+
setting: str = Field(..., description="Clinical setting (Clinic/Wards/ED)")
|
37 |
+
|
38 |
+
class SmartGoal(BaseModel):
|
39 |
+
"""SMART learning goal"""
|
40 |
+
id: str = Field(..., description="Unique goal identifier")
|
41 |
+
category: LearningCategory
|
42 |
+
original_input: str = Field(..., description="User's original goal input")
|
43 |
+
smart_version: str = Field(..., description="SMART formatted goal")
|
44 |
+
specialty: str
|
45 |
+
setting: str
|
46 |
+
created_at: datetime
|
47 |
+
completed_at: Optional[datetime] = None
|
48 |
+
|
49 |
+
class Config:
|
50 |
+
json_encoders = {
|
51 |
+
datetime: lambda v: v.isoformat()
|
52 |
+
}
|
53 |
+
|
54 |
+
class CategoryProgress(BaseModel):
|
55 |
+
"""Progress tracking for a category"""
|
56 |
+
category: LearningCategory
|
57 |
+
completed_goals: List[SmartGoal] = Field(default_factory=list)
|
58 |
+
total_goals: int = Field(default=0)
|
59 |
+
|
60 |
+
|
61 |
+
# %% ../nbs/00_learning_context.ipynb 9
|
62 |
+
class LearningContext:
|
63 |
+
"""
|
64 |
+
Manages learning context and goal tracking.
|
65 |
+
|
66 |
+
This class handles:
|
67 |
+
- Rotation context management
|
68 |
+
- SMART goal tracking
|
69 |
+
- Progress monitoring by category
|
70 |
+
- Context persistence
|
71 |
+
"""
|
72 |
+
|
73 |
+
def __init__(self, context_path: Optional[Path] = None):
|
74 |
+
"""
|
75 |
+
Initialize learning context.
|
76 |
+
|
77 |
+
Args:
|
78 |
+
context_path: Optional path to load/save context
|
79 |
+
"""
|
80 |
+
self.context_path = context_path
|
81 |
+
|
82 |
+
# Initialize rotation context
|
83 |
+
self.rotation = RotationContext(
|
84 |
+
specialty="",
|
85 |
+
setting=""
|
86 |
+
)
|
87 |
+
|
88 |
+
# Initialize category tracking
|
89 |
+
self.category_progress: Dict[LearningCategory, CategoryProgress] = {
|
90 |
+
cat: CategoryProgress(category=cat)
|
91 |
+
for cat in LearningCategory
|
92 |
+
}
|
93 |
+
|
94 |
+
# Current active goal
|
95 |
+
self.active_goal: Optional[SmartGoal] = None
|
96 |
+
|
97 |
+
# Load existing context if available
|
98 |
+
if context_path and context_path.exists():
|
99 |
+
self.load_context()
|
100 |
+
|
101 |
+
def update_rotation(self, specialty: str, setting: str) -> None:
|
102 |
+
"""
|
103 |
+
Update rotation context.
|
104 |
+
|
105 |
+
Args:
|
106 |
+
specialty: Medical specialty
|
107 |
+
setting: Clinical setting
|
108 |
+
"""
|
109 |
+
self.rotation = RotationContext(
|
110 |
+
specialty=specialty,
|
111 |
+
setting=setting
|
112 |
+
)
|
113 |
+
self._save_context()
|
114 |
+
|
115 |
+
def add_smart_goal(self, goal: SmartGoal) -> None:
|
116 |
+
"""
|
117 |
+
Add new SMART goal.
|
118 |
+
|
119 |
+
Args:
|
120 |
+
goal: SMART goal to add
|
121 |
+
"""
|
122 |
+
self.active_goal = goal
|
123 |
+
self.category_progress[goal.category].total_goals += 1
|
124 |
+
self._save_context()
|
125 |
+
|
126 |
+
def complete_active_goal(self) -> None:
|
127 |
+
"""Complete current active goal."""
|
128 |
+
if self.active_goal:
|
129 |
+
self.active_goal.completed_at = datetime.now()
|
130 |
+
cat = self.active_goal.category
|
131 |
+
self.category_progress[cat].completed_goals.append(self.active_goal)
|
132 |
+
self.active_goal = None
|
133 |
+
self._save_context()
|
134 |
+
|
135 |
+
def get_category_summary(self) -> Dict[str, Dict]:
|
136 |
+
"""
|
137 |
+
Get summary of progress by category.
|
138 |
+
|
139 |
+
Returns:
|
140 |
+
dict: Category summaries including completed/total goals
|
141 |
+
and recent completions
|
142 |
+
"""
|
143 |
+
return {
|
144 |
+
cat.value: {
|
145 |
+
"completed": len(prog.completed_goals),
|
146 |
+
"total": prog.total_goals,
|
147 |
+
"recent": [
|
148 |
+
{
|
149 |
+
"smart_version": goal.smart_version,
|
150 |
+
"completed_at": goal.completed_at.isoformat()
|
151 |
+
}
|
152 |
+
for goal in sorted(
|
153 |
+
prog.completed_goals,
|
154 |
+
key=lambda x: x.completed_at or datetime.min,
|
155 |
+
reverse=True
|
156 |
+
)[:3] # Last 3 completed
|
157 |
+
]
|
158 |
+
}
|
159 |
+
for cat, prog in self.category_progress.items()
|
160 |
+
}
|
161 |
+
|
162 |
+
def get_all_goals(self) -> List[Dict]:
|
163 |
+
"""
|
164 |
+
Get all goals for current rotation.
|
165 |
+
|
166 |
+
Returns:
|
167 |
+
list: All goals matching current rotation
|
168 |
+
"""
|
169 |
+
all_goals = []
|
170 |
+
for prog in self.category_progress.values():
|
171 |
+
# Add completed goals
|
172 |
+
all_goals.extend([
|
173 |
+
goal.dict() for goal in prog.completed_goals
|
174 |
+
if (goal.specialty == self.rotation.specialty and
|
175 |
+
goal.setting == self.rotation.setting)
|
176 |
+
])
|
177 |
+
|
178 |
+
# Add active goal if matches
|
179 |
+
if self.active_goal and (
|
180 |
+
self.active_goal.specialty == self.rotation.specialty and
|
181 |
+
self.active_goal.setting == self.rotation.setting
|
182 |
+
):
|
183 |
+
all_goals.append(self.active_goal.dict())
|
184 |
+
|
185 |
+
return sorted(
|
186 |
+
all_goals,
|
187 |
+
key=lambda x: x["created_at"],
|
188 |
+
reverse=True
|
189 |
+
)
|
190 |
+
|
191 |
+
def load_context(self) -> None:
|
192 |
+
"""Load context from file."""
|
193 |
+
try:
|
194 |
+
with open(self.context_path, 'r') as f:
|
195 |
+
data = json.load(f)
|
196 |
+
|
197 |
+
# Load rotation
|
198 |
+
self.rotation = RotationContext(**data["rotation"])
|
199 |
+
|
200 |
+
# Load progress
|
201 |
+
for cat_data in data["category_progress"]:
|
202 |
+
cat = LearningCategory(cat_data["category"])
|
203 |
+
|
204 |
+
# Convert goal dicts back to models
|
205 |
+
completed_goals = [
|
206 |
+
SmartGoal(**g) for g in cat_data["completed_goals"]
|
207 |
+
]
|
208 |
+
|
209 |
+
self.category_progress[cat] = CategoryProgress(
|
210 |
+
category=cat,
|
211 |
+
completed_goals=completed_goals,
|
212 |
+
total_goals=cat_data["total_goals"]
|
213 |
+
)
|
214 |
+
|
215 |
+
# Load active goal if exists
|
216 |
+
if data.get("active_goal"):
|
217 |
+
self.active_goal = SmartGoal(**data["active_goal"])
|
218 |
+
|
219 |
+
logger.info(f"Context loaded from {self.context_path}")
|
220 |
+
|
221 |
+
except Exception as e:
|
222 |
+
logger.error(f"Error loading context: {str(e)}")
|
223 |
+
|
224 |
+
def _save_context(self) -> None:
|
225 |
+
"""Save context to file."""
|
226 |
+
if not self.context_path:
|
227 |
+
return
|
228 |
+
|
229 |
+
try:
|
230 |
+
data = {
|
231 |
+
"rotation": self.rotation.dict(),
|
232 |
+
"category_progress": [
|
233 |
+
{
|
234 |
+
"category": prog.category,
|
235 |
+
"completed_goals": [
|
236 |
+
g.dict() for g in prog.completed_goals
|
237 |
+
],
|
238 |
+
"total_goals": prog.total_goals
|
239 |
+
}
|
240 |
+
for prog in self.category_progress.values()
|
241 |
+
],
|
242 |
+
"active_goal": self.active_goal.dict() if self.active_goal else None
|
243 |
+
}
|
244 |
+
|
245 |
+
with open(self.context_path, 'w') as f:
|
246 |
+
json.dump(data, f, indent=2)
|
247 |
+
|
248 |
+
logger.info(f"Context saved to {self.context_path}")
|
249 |
+
|
250 |
+
except Exception as e:
|
251 |
+
logger.error(f"Error saving context: {str(e)}")
|
wardbuddy/learning_interface.py
ADDED
@@ -0,0 +1,373 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""Gradio interface"""
|
2 |
+
|
3 |
+
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/02_learning_interface.ipynb.
|
4 |
+
|
5 |
+
# %% auto 0
|
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
|
13 |
+
from datetime import datetime
|
14 |
+
import pandas as pd
|
15 |
+
from .clinical_tutor import ClinicalTutor
|
16 |
+
from .learning_context import setup_logger, LearningCategory, SmartGoal
|
17 |
+
import json
|
18 |
+
|
19 |
+
logger = setup_logger(__name__)
|
20 |
+
|
21 |
+
# %% ../nbs/02_learning_interface.ipynb 6
|
22 |
+
def create_css() -> str:
|
23 |
+
"""Create custom CSS for interface styling."""
|
24 |
+
return """
|
25 |
+
.gradio-container {
|
26 |
+
background-color: #0f172a !important;
|
27 |
+
}
|
28 |
+
|
29 |
+
.chat-message {
|
30 |
+
background-color: #1e293b !important;
|
31 |
+
border: 1px solid #334155 !important;
|
32 |
+
border-radius: 0.5rem !important;
|
33 |
+
padding: 1rem !important;
|
34 |
+
margin: 0.5rem 0 !important;
|
35 |
+
}
|
36 |
+
|
37 |
+
.user-message {
|
38 |
+
background-color: #2563eb !important;
|
39 |
+
}
|
40 |
+
|
41 |
+
textarea, select {
|
42 |
+
background-color: #1e293b !important;
|
43 |
+
border: 1px solid #334155 !important;
|
44 |
+
color: #f1f5f9 !important;
|
45 |
+
}
|
46 |
+
|
47 |
+
button {
|
48 |
+
background-color: #2563eb !important;
|
49 |
+
color: white !important;
|
50 |
+
}
|
51 |
+
|
52 |
+
button:hover {
|
53 |
+
background-color: #1d4ed8 !important;
|
54 |
+
}
|
55 |
+
"""
|
56 |
+
|
57 |
+
# %% ../nbs/02_learning_interface.ipynb 8
|
58 |
+
class LearningInterface:
|
59 |
+
"""
|
60 |
+
Gradio interface for clinical learning system.
|
61 |
+
|
62 |
+
Features:
|
63 |
+
- Context selection
|
64 |
+
- Goal management
|
65 |
+
- Case discussion
|
66 |
+
- Progress tracking
|
67 |
+
"""
|
68 |
+
|
69 |
+
def __init__(
|
70 |
+
self,
|
71 |
+
context_path: Optional[Path] = None,
|
72 |
+
model: str = "anthropic/claude-3.5-sonnet"
|
73 |
+
):
|
74 |
+
"""
|
75 |
+
Initialize interface.
|
76 |
+
|
77 |
+
Args:
|
78 |
+
context_path: Path for context persistence
|
79 |
+
model: OpenRouter model identifier
|
80 |
+
"""
|
81 |
+
self.tutor = ClinicalTutor(context_path, model)
|
82 |
+
|
83 |
+
# Available options
|
84 |
+
self.specialties = [
|
85 |
+
"Internal Medicine",
|
86 |
+
"Emergency Medicine",
|
87 |
+
"Surgery",
|
88 |
+
"Pediatrics",
|
89 |
+
"Family Medicine"
|
90 |
+
]
|
91 |
+
|
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."""
|
98 |
+
|
99 |
+
with gr.Blocks(
|
100 |
+
title="Clinical Learning Assistant",
|
101 |
+
css=create_css()
|
102 |
+
) as interface:
|
103 |
+
# State management
|
104 |
+
state = gr.State({
|
105 |
+
"discussion_active": False,
|
106 |
+
"suggested_goals": [] # Store generated goals
|
107 |
+
})
|
108 |
+
|
109 |
+
# Header
|
110 |
+
gr.Markdown("# Clinical Learning Assistant")
|
111 |
+
|
112 |
+
with gr.Row():
|
113 |
+
# Context selection
|
114 |
+
specialty = gr.Dropdown(
|
115 |
+
choices=self.specialties,
|
116 |
+
label="Specialty",
|
117 |
+
value=self.tutor.learning_context.rotation.specialty or None
|
118 |
+
)
|
119 |
+
|
120 |
+
setting = gr.Dropdown(
|
121 |
+
choices=self.settings,
|
122 |
+
label="Setting",
|
123 |
+
value=self.tutor.learning_context.rotation.setting or None
|
124 |
+
)
|
125 |
+
|
126 |
+
# Main content
|
127 |
+
with gr.Row():
|
128 |
+
# Left: Discussion interface
|
129 |
+
with gr.Column(scale=2):
|
130 |
+
# Active goal display
|
131 |
+
goal_display = gr.Markdown(
|
132 |
+
value="No active learning goal"
|
133 |
+
)
|
134 |
+
|
135 |
+
# Chat interface
|
136 |
+
chatbot = gr.Chatbot(
|
137 |
+
height=400,
|
138 |
+
show_label=False
|
139 |
+
)
|
140 |
+
|
141 |
+
with gr.Row():
|
142 |
+
message = gr.Textbox(
|
143 |
+
label="Present your case or ask questions",
|
144 |
+
placeholder="Present your case as you would to your supervisor...",
|
145 |
+
lines=4
|
146 |
+
)
|
147 |
+
|
148 |
+
audio_input = gr.Audio(
|
149 |
+
source="microphone",
|
150 |
+
type="numpy",
|
151 |
+
label="Or speak your case",
|
152 |
+
streaming=True
|
153 |
+
)
|
154 |
+
|
155 |
+
with gr.Row():
|
156 |
+
clear = gr.Button("Clear Discussion")
|
157 |
+
end = gr.Button(
|
158 |
+
"End Discussion & Review",
|
159 |
+
variant="primary"
|
160 |
+
)
|
161 |
+
|
162 |
+
# Right: Progress & Goals
|
163 |
+
with gr.Column(scale=1):
|
164 |
+
with gr.Tab("Learning Goals"):
|
165 |
+
# Goal selection/generation
|
166 |
+
with gr.Row():
|
167 |
+
generate = gr.Button("Generate New Goals")
|
168 |
+
|
169 |
+
new_goal = gr.Textbox(
|
170 |
+
label="Or enter your own goal",
|
171 |
+
placeholder="What do you want to get better at?"
|
172 |
+
)
|
173 |
+
|
174 |
+
add_goal = gr.Button("Add Goal")
|
175 |
+
|
176 |
+
# Goals list
|
177 |
+
goals_list = gr.DataFrame(
|
178 |
+
headers=["Goal", "Category", "Status"],
|
179 |
+
label="Available Goals"
|
180 |
+
)
|
181 |
+
|
182 |
+
with gr.Tab("Progress"):
|
183 |
+
# Category progress
|
184 |
+
progress_display = gr.DataFrame(
|
185 |
+
headers=["Category", "Completed", "Total"],
|
186 |
+
label="Progress by Category"
|
187 |
+
)
|
188 |
+
|
189 |
+
# Recent completions
|
190 |
+
recent_display = gr.DataFrame(
|
191 |
+
headers=["Goal", "Category", "Completed"],
|
192 |
+
label="Recently Completed"
|
193 |
+
)
|
194 |
+
|
195 |
+
# Event handlers
|
196 |
+
async def update_context(spec, set):
|
197 |
+
"""Update rotation context."""
|
198 |
+
if spec and set:
|
199 |
+
self.tutor.learning_context.update_rotation(spec, set)
|
200 |
+
|
201 |
+
# Generate initial goals if needed
|
202 |
+
if not state["suggested_goals"]:
|
203 |
+
goals = await self.tutor.generate_smart_goals(spec, set)
|
204 |
+
state["suggested_goals"] = goals
|
205 |
+
|
206 |
+
return self._update_displays(state)
|
207 |
+
return []
|
208 |
+
|
209 |
+
async def process_message(msg, history):
|
210 |
+
"""Process chat message."""
|
211 |
+
if not msg.strip():
|
212 |
+
return history, ""
|
213 |
+
|
214 |
+
response = await self.tutor.discuss_case(msg)
|
215 |
+
history.append([msg, response])
|
216 |
+
return history, ""
|
217 |
+
|
218 |
+
def process_audio(audio):
|
219 |
+
"""Convert audio to text."""
|
220 |
+
# Would integrate with speech-to-text here
|
221 |
+
return "Audio transcription would appear here"
|
222 |
+
|
223 |
+
async def generate_goals(state):
|
224 |
+
"""Generate new goal suggestions."""
|
225 |
+
rotation = self.tutor.learning_context.rotation
|
226 |
+
if rotation.specialty and rotation.setting:
|
227 |
+
goals = await self.tutor.generate_smart_goals(
|
228 |
+
rotation.specialty,
|
229 |
+
rotation.setting
|
230 |
+
)
|
231 |
+
state["suggested_goals"] = goals
|
232 |
+
return self._update_displays(state)
|
233 |
+
return []
|
234 |
+
|
235 |
+
async def add_user_goal(text, state):
|
236 |
+
"""Add user-specified goal."""
|
237 |
+
if not text.strip():
|
238 |
+
return state
|
239 |
+
|
240 |
+
rotation = self.tutor.learning_context.rotation
|
241 |
+
if rotation.specialty and rotation.setting:
|
242 |
+
goal = await self.tutor.generate_smart_goal(
|
243 |
+
text,
|
244 |
+
rotation.specialty,
|
245 |
+
rotation.setting
|
246 |
+
)
|
247 |
+
if goal:
|
248 |
+
state["suggested_goals"].append(goal)
|
249 |
+
return self._update_displays(state)
|
250 |
+
return state
|
251 |
+
|
252 |
+
def select_goal(evt: gr.SelectData, state):
|
253 |
+
"""Set selected goal as active."""
|
254 |
+
if evt.index[0] < len(state["suggested_goals"]):
|
255 |
+
goal = state["suggested_goals"][evt.index[0]]
|
256 |
+
self.tutor.learning_context.add_smart_goal(goal)
|
257 |
+
return self._update_displays(state)
|
258 |
+
return state
|
259 |
+
|
260 |
+
def end_discussion(state):
|
261 |
+
"""End current discussion."""
|
262 |
+
self.tutor.end_discussion()
|
263 |
+
return self._update_displays(state)
|
264 |
+
|
265 |
+
def clear_discussion():
|
266 |
+
"""Clear discussion history."""
|
267 |
+
self.tutor.clear_discussion()
|
268 |
+
return [], ""
|
269 |
+
|
270 |
+
# Wire up events
|
271 |
+
specialty.change(
|
272 |
+
update_context,
|
273 |
+
inputs=[specialty, setting],
|
274 |
+
outputs=[goals_list, progress_display, recent_display, goal_display]
|
275 |
+
)
|
276 |
+
|
277 |
+
setting.change(
|
278 |
+
update_context,
|
279 |
+
inputs=[specialty, setting],
|
280 |
+
outputs=[goals_list, progress_display, recent_display, goal_display]
|
281 |
+
)
|
282 |
+
|
283 |
+
message.submit(
|
284 |
+
process_message,
|
285 |
+
inputs=[message, chatbot],
|
286 |
+
outputs=[chatbot, message]
|
287 |
+
)
|
288 |
+
|
289 |
+
audio_input.stream(
|
290 |
+
process_audio,
|
291 |
+
inputs=[audio_input],
|
292 |
+
outputs=[message]
|
293 |
+
)
|
294 |
+
|
295 |
+
clear.click(
|
296 |
+
clear_discussion,
|
297 |
+
outputs=[chatbot, message]
|
298 |
+
)
|
299 |
+
|
300 |
+
end.click(
|
301 |
+
end_discussion,
|
302 |
+
inputs=[state],
|
303 |
+
outputs=[goals_list, progress_display, recent_display, goal_display]
|
304 |
+
)
|
305 |
+
|
306 |
+
generate.click(
|
307 |
+
generate_goals,
|
308 |
+
inputs=[state],
|
309 |
+
outputs=[goals_list, progress_display, recent_display, goal_display]
|
310 |
+
)
|
311 |
+
|
312 |
+
new_goal.submit(
|
313 |
+
add_user_goal,
|
314 |
+
inputs=[new_goal, state],
|
315 |
+
outputs=[goals_list, progress_display, recent_display, goal_display]
|
316 |
+
)
|
317 |
+
|
318 |
+
goals_list.select(
|
319 |
+
select_goal,
|
320 |
+
inputs=[state],
|
321 |
+
outputs=[goals_list, progress_display, recent_display, goal_display]
|
322 |
+
)
|
323 |
+
|
324 |
+
return interface
|
325 |
+
|
326 |
+
def _update_displays(self, state: Dict) -> List:
|
327 |
+
"""Update all display components."""
|
328 |
+
context = self.tutor.learning_context
|
329 |
+
|
330 |
+
# Update goals list
|
331 |
+
goals_data = []
|
332 |
+
for goal in state["suggested_goals"]:
|
333 |
+
status = "Active" if (
|
334 |
+
context.active_goal and
|
335 |
+
context.active_goal.id == goal.id
|
336 |
+
) else "Available"
|
337 |
+
|
338 |
+
goals_data.append([
|
339 |
+
goal.smart_version,
|
340 |
+
goal.category.value,
|
341 |
+
status
|
342 |
+
])
|
343 |
+
|
344 |
+
# Update progress display
|
345 |
+
summary = context.get_category_summary()
|
346 |
+
progress_data = [
|
347 |
+
[cat, data["completed"], data["total"]]
|
348 |
+
for cat, data in summary.items()
|
349 |
+
]
|
350 |
+
|
351 |
+
# Update recent completions
|
352 |
+
recent_data = []
|
353 |
+
for cat, data in summary.items():
|
354 |
+
for goal in data["recent"]:
|
355 |
+
recent_data.append([
|
356 |
+
goal["smart_version"],
|
357 |
+
cat,
|
358 |
+
goal["completed_at"]
|
359 |
+
])
|
360 |
+
|
361 |
+
# Update active goal display
|
362 |
+
goal_text = (
|
363 |
+
f"Current Goal: {context.active_goal.smart_version}"
|
364 |
+
if context.active_goal else
|
365 |
+
"No active learning goal"
|
366 |
+
)
|
367 |
+
|
368 |
+
return [
|
369 |
+
goals_data,
|
370 |
+
progress_data,
|
371 |
+
recent_data,
|
372 |
+
goal_text
|
373 |
+
]
|
wardbuddy/main.py
ADDED
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/main.ipynb.
|
2 |
+
|
3 |
+
# %% auto 0
|
4 |
+
__all__ = ['logger', 'app', 'launch', 'run_app']
|
5 |
+
|
6 |
+
# %% ../nbs/main.ipynb 1
|
7 |
+
"""Main entry point for the WardBuddy application."""
|
8 |
+
import asyncio
|
9 |
+
from pathlib import Path
|
10 |
+
import typer
|
11 |
+
from typing import Optional
|
12 |
+
import logging
|
13 |
+
|
14 |
+
from .learning_interface import LearningInterface
|
15 |
+
|
16 |
+
# Configure logging
|
17 |
+
logging.basicConfig(level=logging.INFO)
|
18 |
+
logger = logging.getLogger(__name__)
|
19 |
+
|
20 |
+
app = typer.Typer()
|
21 |
+
|
22 |
+
@app.command()
|
23 |
+
def launch(
|
24 |
+
port: int = typer.Option(7860, help="Port to run on"),
|
25 |
+
context_path: Optional[Path] = typer.Option(
|
26 |
+
None,
|
27 |
+
help="Path to context file",
|
28 |
+
dir_okay=False
|
29 |
+
),
|
30 |
+
model: str = typer.Option(
|
31 |
+
"anthropic/claude-3-sonnet",
|
32 |
+
help="OpenRouter model identifier"
|
33 |
+
),
|
34 |
+
share: bool = typer.Option(
|
35 |
+
False,
|
36 |
+
help="Create public URL"
|
37 |
+
)
|
38 |
+
) -> None:
|
39 |
+
"""Launch the clinical learning interface."""
|
40 |
+
try:
|
41 |
+
interface = LearningInterface(
|
42 |
+
context_path=context_path,
|
43 |
+
model=model
|
44 |
+
)
|
45 |
+
|
46 |
+
app = interface.create_interface()
|
47 |
+
app.launch(
|
48 |
+
server_port=port,
|
49 |
+
share=share
|
50 |
+
)
|
51 |
+
|
52 |
+
logger.info(f"Interface launched on port {port}")
|
53 |
+
|
54 |
+
except Exception as e:
|
55 |
+
logger.error(f"Error launching interface: {str(e)}")
|
56 |
+
raise typer.Exit(1)
|
57 |
+
|
58 |
+
def run_app():
|
59 |
+
"""Entry point for command line."""
|
60 |
+
app()
|
wardbuddy/utils.py
ADDED
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""Shared utilities for the entire learning system"""
|
2 |
+
|
3 |
+
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/03_utils.ipynb.
|
4 |
+
|
5 |
+
# %% auto 0
|
6 |
+
__all__ = ['logger', 'setup_logger', 'load_context_safely', 'save_context_safely']
|
7 |
+
|
8 |
+
# %% ../nbs/03_utils.ipynb 4
|
9 |
+
from typing import Dict, List, Optional, Any, Tuple
|
10 |
+
import json
|
11 |
+
from pathlib import Path
|
12 |
+
import logging
|
13 |
+
from datetime import datetime
|
14 |
+
|
15 |
+
# %% ../nbs/03_utils.ipynb 6
|
16 |
+
def setup_logger(name: str) -> logging.Logger:
|
17 |
+
"""Set up module logger with consistent formatting"""
|
18 |
+
logger = logging.getLogger(name)
|
19 |
+
if not logger.handlers:
|
20 |
+
handler = logging.StreamHandler()
|
21 |
+
handler.setFormatter(
|
22 |
+
logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
23 |
+
)
|
24 |
+
logger.addHandler(handler)
|
25 |
+
logger.setLevel(logging.INFO)
|
26 |
+
return logger
|
27 |
+
|
28 |
+
logger = setup_logger(__name__)
|
29 |
+
|
30 |
+
# %% ../nbs/03_utils.ipynb 7
|
31 |
+
def load_context_safely(path: Path) -> Dict:
|
32 |
+
"""
|
33 |
+
Safely load learning context from JSON file.
|
34 |
+
|
35 |
+
Args:
|
36 |
+
path: Path to context file
|
37 |
+
|
38 |
+
Returns:
|
39 |
+
dict: Loaded context data
|
40 |
+
|
41 |
+
Raises:
|
42 |
+
ValueError: If file is invalid or inaccessible
|
43 |
+
"""
|
44 |
+
try:
|
45 |
+
with open(path, 'r') as f:
|
46 |
+
return json.load(f)
|
47 |
+
except json.JSONDecodeError as e:
|
48 |
+
raise ValueError(f"Invalid context file format: {str(e)}")
|
49 |
+
except Exception as e:
|
50 |
+
raise ValueError(f"Error loading context file: {str(e)}")
|
51 |
+
|
52 |
+
# %% ../nbs/03_utils.ipynb 8
|
53 |
+
def save_context_safely(context: Dict, path: Path) -> None:
|
54 |
+
"""
|
55 |
+
Safely save learning context to JSON file.
|
56 |
+
|
57 |
+
Args:
|
58 |
+
context: Context data to save
|
59 |
+
path: Path to save file
|
60 |
+
|
61 |
+
Raises:
|
62 |
+
ValueError: If save operation fails
|
63 |
+
"""
|
64 |
+
try:
|
65 |
+
with open(path, 'w') as f:
|
66 |
+
json.dump(context, f, indent=2)
|
67 |
+
except Exception as e:
|
68 |
+
raise ValueError(f"Error saving context: {str(e)}")
|