dyadd commited on
Commit
38ca196
·
verified ·
1 Parent(s): 0fbbecf

Upload folder using huggingface_hub

Browse files
.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: Wardbuddy
3
- emoji: 🏢
4
- colorFrom: pink
5
- colorTo: yellow
6
- sdk: gradio
7
- sdk_version: 5.14.0
8
- app_file: app.py
9
- pinned: false
10
- ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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)}")