dyadd commited on
Commit
5400c2e
·
verified ·
1 Parent(s): 692d305

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 Test
3
- emoji: 📈
4
- colorFrom: red
5
- colorTo: yellow
6
- sdk: gradio
7
- sdk_version: 5.13.1
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-test
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,367 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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, Any, Tuple\n",
55
+ "from datetime import datetime\n",
56
+ "import json\n",
57
+ "from pathlib import Path\n",
58
+ "from wardbuddy.utils import setup_logger, load_context_safely, save_context_safely"
59
+ ]
60
+ },
61
+ {
62
+ "cell_type": "code",
63
+ "execution_count": null,
64
+ "metadata": {},
65
+ "outputs": [],
66
+ "source": [
67
+ "#| export\n",
68
+ "logger = setup_logger(__name__)"
69
+ ]
70
+ },
71
+ {
72
+ "cell_type": "markdown",
73
+ "metadata": {},
74
+ "source": [
75
+ "## Learning Context Management\n",
76
+ "\n",
77
+ "> Core module for managing student learning context and history"
78
+ ]
79
+ },
80
+ {
81
+ "cell_type": "markdown",
82
+ "metadata": {},
83
+ "source": [
84
+ "The system needs to track and handle student information for personalised education. \n",
85
+ "\n",
86
+ "This module handles:\n",
87
+ "* Tracking learning objectives\n",
88
+ "* Managing case history\n",
89
+ "* Monitoring knowledge gaps\n",
90
+ "* Customizing feedback preferences\n",
91
+ "\n",
92
+ "and finally:\n",
93
+ "* Context persistence\n"
94
+ ]
95
+ },
96
+ {
97
+ "cell_type": "code",
98
+ "execution_count": null,
99
+ "metadata": {},
100
+ "outputs": [],
101
+ "source": [
102
+ "#| export\n",
103
+ "class LearningContext:\n",
104
+ " \"\"\"\n",
105
+ " Manages the dynamic learning context for each student.\n",
106
+ " \n",
107
+ " Tracks:\n",
108
+ " - Current rotation details\n",
109
+ " - Learning objectives and progress\n",
110
+ " - Knowledge gaps and strengths\n",
111
+ " - Custom feedback preferences\n",
112
+ " \"\"\"\n",
113
+ " \n",
114
+ " def __init__(self, context_file: Optional[Path] = None):\n",
115
+ " \"\"\"\n",
116
+ " Initialize learning context, optionally loading from file.\n",
117
+ " \n",
118
+ " Args:\n",
119
+ " context_file: Optional path to saved context\n",
120
+ " \"\"\"\n",
121
+ " # Initialize current rotation\n",
122
+ " self.current_rotation = {\n",
123
+ " \"specialty\": \"\",\n",
124
+ " \"start_date\": None,\n",
125
+ " \"end_date\": None,\n",
126
+ " \"key_focus_areas\": []\n",
127
+ " }\n",
128
+ " \n",
129
+ " # Initialize learning objectives list\n",
130
+ " self.learning_objectives = [] # [{\"objective\": str, \"status\": \"active\"|\"completed\", \"added\": datetime}]\n",
131
+ " \n",
132
+ " # Initialize knowledge profile\n",
133
+ " self.knowledge_profile = {\n",
134
+ " \"gaps\": {}, # topic -> confidence level\n",
135
+ " \"strengths\": [],\n",
136
+ " \"recent_progress\": [] # [{topic, improvement, date}]\n",
137
+ " }\n",
138
+ " \n",
139
+ " # Initialize feedback preferences\n",
140
+ " self.feedback_preferences = [] # [{\"focus\": str, \"active\": bool}]\n",
141
+ " \n",
142
+ " if context_file and context_file.exists():\n",
143
+ " self.load_context(context_file)\n",
144
+ " \n",
145
+ " logger.info(\"Learning context initialized\")\n",
146
+ " \n",
147
+ " def update_rotation(self, rotation_details: Dict[str, Any]) -> None:\n",
148
+ " \"\"\"Update current rotation details\"\"\"\n",
149
+ " self.current_rotation.update(rotation_details)\n",
150
+ " \n",
151
+ " def add_learning_objective(self, objective: str) -> None:\n",
152
+ " \"\"\"Add new learning objective\"\"\"\n",
153
+ " self.learning_objectives.append({\n",
154
+ " \"objective\": objective,\n",
155
+ " \"status\": \"active\",\n",
156
+ " \"added\": datetime.now().isoformat()\n",
157
+ " })\n",
158
+ " \n",
159
+ " def complete_objective(self, objective: str) -> None:\n",
160
+ " \"\"\"Mark learning objective as completed\"\"\"\n",
161
+ " for obj in self.learning_objectives:\n",
162
+ " if obj[\"objective\"] == objective and obj[\"status\"] == \"active\":\n",
163
+ " obj[\"status\"] = \"completed\"\n",
164
+ " obj[\"completed\"] = datetime.now().isoformat()\n",
165
+ " break\n",
166
+ " \n",
167
+ " def update_knowledge_gap(self, topic: str, confidence: float) -> None:\n",
168
+ " \"\"\"Update knowledge gap confidence level\"\"\"\n",
169
+ " old_confidence = self.knowledge_profile[\"gaps\"].get(topic)\n",
170
+ " self.knowledge_profile[\"gaps\"][topic] = confidence\n",
171
+ " \n",
172
+ " # Track progress if confidence improved\n",
173
+ " if old_confidence and confidence > old_confidence:\n",
174
+ " self.knowledge_profile[\"recent_progress\"].append({\n",
175
+ " \"topic\": topic,\n",
176
+ " \"improvement\": confidence - old_confidence,\n",
177
+ " \"date\": datetime.now().isoformat()\n",
178
+ " })\n",
179
+ " \n",
180
+ " # Keep only recent progress\n",
181
+ " self.knowledge_profile[\"recent_progress\"] = \\\n",
182
+ " self.knowledge_profile[\"recent_progress\"][-5:]\n",
183
+ " \n",
184
+ " def add_strength(self, topic: str) -> None:\n",
185
+ " \"\"\"Add identified strength\"\"\"\n",
186
+ " if topic not in self.knowledge_profile[\"strengths\"]:\n",
187
+ " self.knowledge_profile[\"strengths\"].append(topic)\n",
188
+ " \n",
189
+ " def toggle_feedback_focus(self, focus: str, active: bool) -> None:\n",
190
+ " \"\"\"Toggle feedback focus area\"\"\"\n",
191
+ " # Update if exists\n",
192
+ " for pref in self.feedback_preferences:\n",
193
+ " if pref[\"focus\"] == focus:\n",
194
+ " pref[\"active\"] = active\n",
195
+ " return\n",
196
+ " \n",
197
+ " # Add if new\n",
198
+ " self.feedback_preferences.append({\n",
199
+ " \"focus\": focus,\n",
200
+ " \"active\": active\n",
201
+ " })\n",
202
+ " \n",
203
+ " def save_context(self, file_path: Path) -> None:\n",
204
+ " \"\"\"Save context to file\"\"\"\n",
205
+ " context_data = {\n",
206
+ " \"current_rotation\": self.current_rotation,\n",
207
+ " \"learning_objectives\": self.learning_objectives,\n",
208
+ " \"knowledge_profile\": self.knowledge_profile,\n",
209
+ " \"feedback_preferences\": self.feedback_preferences\n",
210
+ " }\n",
211
+ " save_context_safely(context_data, file_path)\n",
212
+ " \n",
213
+ " def load_context(self, file_path: Path) -> None:\n",
214
+ " \"\"\"Load context from file\"\"\"\n",
215
+ " try:\n",
216
+ " context_data = load_context_safely(file_path)\n",
217
+ " \n",
218
+ " # Use .get() with default values to handle missing keys\n",
219
+ " self.current_rotation = context_data.get(\"current_rotation\", {\n",
220
+ " \"specialty\": \"\",\n",
221
+ " \"start_date\": None,\n",
222
+ " \"end_date\": None,\n",
223
+ " \"key_focus_areas\": []\n",
224
+ " })\n",
225
+ " \n",
226
+ " # Ensure required keys exist\n",
227
+ " for key in [\"specialty\", \"start_date\", \"end_date\", \"key_focus_areas\"]:\n",
228
+ " if key not in self.current_rotation:\n",
229
+ " self.current_rotation[key] = None if key != \"key_focus_areas\" else []\n",
230
+ " \n",
231
+ " self.learning_objectives = context_data.get(\"learning_objectives\", [])\n",
232
+ " self.knowledge_profile = context_data.get(\"knowledge_profile\", {\n",
233
+ " \"gaps\": {},\n",
234
+ " \"strengths\": [],\n",
235
+ " \"recent_progress\": []\n",
236
+ " })\n",
237
+ " self.feedback_preferences = context_data.get(\"feedback_preferences\", [])\n",
238
+ " \n",
239
+ " logger.info(f\"Context loaded successfully from {file_path}\")\n",
240
+ " \n",
241
+ " except Exception as e:\n",
242
+ " logger.error(f\"Error loading context: {str(e)}\")\n",
243
+ " # Initialize with defaults on error\n",
244
+ " self.__init__()"
245
+ ]
246
+ },
247
+ {
248
+ "cell_type": "markdown",
249
+ "metadata": {},
250
+ "source": [
251
+ "## Tests"
252
+ ]
253
+ },
254
+ {
255
+ "cell_type": "code",
256
+ "execution_count": null,
257
+ "metadata": {},
258
+ "outputs": [
259
+ {
260
+ "name": "stderr",
261
+ "output_type": "stream",
262
+ "text": [
263
+ "2025-01-18 23:31:55,450 - __main__ - INFO - Learning context initialized\n",
264
+ "2025-01-18 23:31:55,457 - __main__ - INFO - Learning context initialized\n"
265
+ ]
266
+ },
267
+ {
268
+ "name": "stdout",
269
+ "output_type": "stream",
270
+ "text": [
271
+ "All learning context tests passed!\n"
272
+ ]
273
+ }
274
+ ],
275
+ "source": [
276
+ "def test_learning_context():\n",
277
+ " \"\"\"Test LearningContext functionality\"\"\"\n",
278
+ " # Create a temporary directory for tests\n",
279
+ " import tempfile\n",
280
+ " temp_dir = tempfile.mkdtemp()\n",
281
+ " temp_path = Path(temp_dir) / \"test_context.json\"\n",
282
+ " \n",
283
+ " try:\n",
284
+ " # Initialize context\n",
285
+ " context = LearningContext()\n",
286
+ " \n",
287
+ " # Test rotation updates\n",
288
+ " rotation_details = {\n",
289
+ " \"specialty\": \"Emergency Medicine\",\n",
290
+ " \"start_date\": \"2025-01-01\",\n",
291
+ " \"end_date\": \"2025-03-31\",\n",
292
+ " \"key_focus_areas\": [\"Resuscitation\", \"Procedures\"]\n",
293
+ " }\n",
294
+ " context.update_rotation(rotation_details)\n",
295
+ " assert context.current_rotation[\"specialty\"] == \"Emergency Medicine\"\n",
296
+ " \n",
297
+ " # Test learning objectives\n",
298
+ " context.add_learning_objective(\"Improve chest pain assessment\")\n",
299
+ " assert len(context.learning_objectives) == 1\n",
300
+ " assert context.learning_objectives[0][\"status\"] == \"active\"\n",
301
+ " \n",
302
+ " # Test completing objectives\n",
303
+ " context.complete_objective(\"Improve chest pain assessment\")\n",
304
+ " assert context.learning_objectives[0][\"status\"] == \"completed\"\n",
305
+ " \n",
306
+ " # Test knowledge gaps\n",
307
+ " context.update_knowledge_gap(\"ECG interpretation\", 0.6)\n",
308
+ " assert context.knowledge_profile[\"gaps\"][\"ECG interpretation\"] == 0.6\n",
309
+ " \n",
310
+ " # Test progress tracking\n",
311
+ " context.update_knowledge_gap(\"ECG interpretation\", 0.8)\n",
312
+ " assert len(context.knowledge_profile[\"recent_progress\"]) == 1\n",
313
+ " \n",
314
+ " # Test strengths\n",
315
+ " context.add_strength(\"History taking\")\n",
316
+ " assert \"History taking\" in context.knowledge_profile[\"strengths\"]\n",
317
+ " \n",
318
+ " # Test feedback preferences\n",
319
+ " context.toggle_feedback_focus(\"Include more ddx\", True)\n",
320
+ " assert len(context.feedback_preferences) == 1\n",
321
+ " assert context.feedback_preferences[0][\"active\"] == True\n",
322
+ " \n",
323
+ " # Test persistence\n",
324
+ " context.save_context(temp_path)\n",
325
+ " assert temp_path.exists()\n",
326
+ " \n",
327
+ " # Test loading in new context\n",
328
+ " new_context = LearningContext(temp_path)\n",
329
+ " assert new_context.current_rotation[\"specialty\"] == \"Emergency Medicine\"\n",
330
+ " assert len(new_context.learning_objectives) == 1\n",
331
+ " assert \"ECG interpretation\" in new_context.knowledge_profile[\"gaps\"]\n",
332
+ " \n",
333
+ " finally:\n",
334
+ " # Cleanup\n",
335
+ " import shutil\n",
336
+ " shutil.rmtree(temp_dir)\n",
337
+ " \n",
338
+ " print(\"All learning context tests passed!\")\n",
339
+ "\n",
340
+ "# Run tests\n",
341
+ "if __name__ == \"__main__\":\n",
342
+ " test_learning_context()"
343
+ ]
344
+ }
345
+ ],
346
+ "metadata": {
347
+ "kernelspec": {
348
+ "display_name": "Python 3 (ipykernel)",
349
+ "language": "python",
350
+ "name": "python3"
351
+ },
352
+ "language_info": {
353
+ "codemirror_mode": {
354
+ "name": "ipython",
355
+ "version": 3
356
+ },
357
+ "file_extension": ".py",
358
+ "mimetype": "text/x-python",
359
+ "name": "python",
360
+ "nbconvert_exporter": "python",
361
+ "pygments_lexer": "ipython3",
362
+ "version": "3.12.7"
363
+ }
364
+ },
365
+ "nbformat": 4,
366
+ "nbformat_minor": 4
367
+ }
nbs/01_clinical_tutor.ipynb ADDED
@@ -0,0 +1,599 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "code",
5
+ "execution_count": null,
6
+ "id": "d9a32ad5-9f07-47f2-97ae-15b0646e355b",
7
+ "metadata": {},
8
+ "outputs": [],
9
+ "source": [
10
+ "#| default_exp clinical_tutor"
11
+ ]
12
+ },
13
+ {
14
+ "cell_type": "markdown",
15
+ "id": "0db4b759-310c-4e38-9fdc-2efb94b541dd",
16
+ "metadata": {},
17
+ "source": [
18
+ "# Clinical Tutor\n",
19
+ "\n",
20
+ "> Core module for using learning context for context-appropriate tutor responses\n"
21
+ ]
22
+ },
23
+ {
24
+ "cell_type": "markdown",
25
+ "id": "16f93992-88dd-409b-8370-b86302e1ce6a",
26
+ "metadata": {},
27
+ "source": [
28
+ "## Setup"
29
+ ]
30
+ },
31
+ {
32
+ "cell_type": "code",
33
+ "execution_count": null,
34
+ "id": "6d2403bb-70a1-4744-be0b-d259234c1b62",
35
+ "metadata": {},
36
+ "outputs": [],
37
+ "source": [
38
+ "#| hide\n",
39
+ "from nbdev.showdoc import *"
40
+ ]
41
+ },
42
+ {
43
+ "cell_type": "code",
44
+ "execution_count": null,
45
+ "id": "477ba22b-55c1-467e-8206-a92f88a598fd",
46
+ "metadata": {},
47
+ "outputs": [
48
+ {
49
+ "name": "stderr",
50
+ "output_type": "stream",
51
+ "text": [
52
+ "C:\\Users\\deepa\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\tqdm\\auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n",
53
+ " from .autonotebook import tqdm as notebook_tqdm\n"
54
+ ]
55
+ }
56
+ ],
57
+ "source": [
58
+ "#| export\n",
59
+ "from typing import Dict, List, Optional, Any, Tuple\n",
60
+ "import os\n",
61
+ "import json\n",
62
+ "import logging\n",
63
+ "from pathlib import Path\n",
64
+ "from datetime import datetime\n",
65
+ "from dotenv import load_dotenv\n",
66
+ "import aiohttp\n",
67
+ "import re\n",
68
+ "from collections import defaultdict\n",
69
+ "from wardbuddy.learning_context import LearningContext, setup_logger\n",
70
+ "\n",
71
+ "# Load environment variables\n",
72
+ "load_dotenv()\n",
73
+ "\n",
74
+ "logger = setup_logger(__name__)"
75
+ ]
76
+ },
77
+ {
78
+ "cell_type": "markdown",
79
+ "id": "7da445d7-7f51-44d5-b027-e2cf65c79069",
80
+ "metadata": {},
81
+ "source": [
82
+ "## Adaptive Clinical Tutor"
83
+ ]
84
+ },
85
+ {
86
+ "cell_type": "markdown",
87
+ "id": "58d76615-480c-4fe4-9d4a-e67dd892132a",
88
+ "metadata": {},
89
+ "source": [
90
+ "This module implements:\n",
91
+ "\n",
92
+ " - Engages in natural case discussions like a clinical supervisor\n",
93
+ " - Provides context-aware feedback based on student's rotation and preferences\n",
94
+ " - Analyzes discussions to track learning progress\n",
95
+ " - Integrates with the student's learning context\n",
96
+ "\n",
97
+ "The tutor aims to mimic real-world clinical teaching interactions where students present cases and receive feedback in a natural conversational style.\n"
98
+ ]
99
+ },
100
+ {
101
+ "cell_type": "code",
102
+ "execution_count": null,
103
+ "id": "da7d2115-b30f-40ba-9566-bcbaf155026d",
104
+ "metadata": {},
105
+ "outputs": [],
106
+ "source": [
107
+ "#| export \n",
108
+ "class OpenRouterException(Exception):\n",
109
+ " \"\"\"Custom exception for OpenRouter API errors\"\"\"\n",
110
+ " pass"
111
+ ]
112
+ },
113
+ {
114
+ "cell_type": "code",
115
+ "execution_count": null,
116
+ "id": "75c2cbfc-75e7-45ee-9d86-b931f81a3ad5",
117
+ "metadata": {},
118
+ "outputs": [],
119
+ "source": [
120
+ "#| export\n",
121
+ "class ClinicalTutor:\n",
122
+ " \"\"\"\n",
123
+ " Adaptive clinical teaching module that provides context-aware feedback.\n",
124
+ " \n",
125
+ " The tutor acts as an experienced clinical supervisor, engaging in natural\n",
126
+ " case discussions while tracking student progress and adapting feedback\n",
127
+ " based on learning context.\n",
128
+ " \n",
129
+ " Attributes:\n",
130
+ " learning_context (LearningContext): Student's learning context\n",
131
+ " model (str): LLM model identifier\n",
132
+ " api_url (str): OpenRouter API endpoint\n",
133
+ " \"\"\"\n",
134
+ " \n",
135
+ " def __init__(\n",
136
+ " self,\n",
137
+ " context_path: Optional[Path] = None,\n",
138
+ " model: str = \"anthropic/claude-3.5-sonnet\"\n",
139
+ " ):\n",
140
+ " \"\"\"\n",
141
+ " Initialize clinical tutor.\n",
142
+ " \n",
143
+ " Args:\n",
144
+ " context_path: Optional path for context persistence\n",
145
+ " model: Model identifier for OpenRouter\n",
146
+ " \"\"\"\n",
147
+ " self.api_key: str = os.getenv(\"OPENROUTER_API_KEY\")\n",
148
+ " if not self.api_key:\n",
149
+ " raise ValueError(\"OpenRouter API key not found\")\n",
150
+ " \n",
151
+ " self.api_url: str = \"https://openrouter.ai/api/v1/chat/completions\"\n",
152
+ " self.model: str = model\n",
153
+ " \n",
154
+ " self.learning_context = LearningContext(context_path)\n",
155
+ " self.context_path = context_path\n",
156
+ " \n",
157
+ " # Track conversation state\n",
158
+ " self.current_case: Dict = {\n",
159
+ " \"started\": None,\n",
160
+ " \"chief_complaint\": None,\n",
161
+ " \"key_findings\": [],\n",
162
+ " \"assessment\": None,\n",
163
+ " \"plan\": None\n",
164
+ " }\n",
165
+ " \n",
166
+ " logger.info(f\"Clinical tutor initialized with model: {model}\")\n",
167
+ " \n",
168
+ " async def _get_completion(\n",
169
+ " self,\n",
170
+ " messages: List[Dict],\n",
171
+ " temperature: float = 0.7,\n",
172
+ " max_retries: int = 3\n",
173
+ " ) -> str:\n",
174
+ " \"\"\"\n",
175
+ " Get completion from OpenRouter API with retry logic.\n",
176
+ " \n",
177
+ " Args:\n",
178
+ " messages: List of conversation messages\n",
179
+ " temperature: Temperature for response generation\n",
180
+ " max_retries: Maximum retry attempts\n",
181
+ " \n",
182
+ " Returns:\n",
183
+ " str: Model response\n",
184
+ " \n",
185
+ " Raises:\n",
186
+ " OpenRouterException: If API calls fail after retries\n",
187
+ " \"\"\"\n",
188
+ " headers = {\n",
189
+ " \"Authorization\": f\"Bearer {self.api_key}\",\n",
190
+ " \"Content-Type\": \"application/json\",\n",
191
+ " \"HTTP-Referer\": \"http://localhost:7860\"\n",
192
+ " }\n",
193
+ " \n",
194
+ " data = {\n",
195
+ " \"model\": self.model,\n",
196
+ " \"messages\": messages,\n",
197
+ " \"temperature\": temperature,\n",
198
+ " \"max_tokens\": 2000\n",
199
+ " }\n",
200
+ " \n",
201
+ " for attempt in range(max_retries):\n",
202
+ " try:\n",
203
+ " async with aiohttp.ClientSession() as session:\n",
204
+ " async with session.post(\n",
205
+ " self.api_url,\n",
206
+ " headers=headers,\n",
207
+ " json=data,\n",
208
+ " timeout=30\n",
209
+ " ) as response:\n",
210
+ " response.raise_for_status()\n",
211
+ " result = await response.json()\n",
212
+ " return result[\"choices\"][0][\"message\"][\"content\"]\n",
213
+ " \n",
214
+ " except Exception as e:\n",
215
+ " if attempt == max_retries - 1:\n",
216
+ " raise OpenRouterException(f\"API call failed: {str(e)}\")\n",
217
+ " logger.warning(f\"Retry {attempt + 1} after error: {str(e)}\")\n",
218
+ " # Could add exponential backoff here if needed\n",
219
+ " \n",
220
+ " def _build_discussion_prompt(self) -> str:\n",
221
+ " \"\"\"\n",
222
+ " Build context-aware prompt for case discussion.\n",
223
+ " \n",
224
+ " Incorporates:\n",
225
+ " - Current rotation details\n",
226
+ " - Active feedback preferences\n",
227
+ " - Recent learning points\n",
228
+ " - Knowledge gaps needing attention\n",
229
+ " \n",
230
+ " Returns:\n",
231
+ " str: Contextualized system prompt\n",
232
+ " \"\"\"\n",
233
+ " rotation = self.learning_context.current_rotation\n",
234
+ " active_preferences = [\n",
235
+ " p[\"focus\"] for p in self.learning_context.feedback_preferences \n",
236
+ " if p[\"active\"]\n",
237
+ " ]\n",
238
+ " \n",
239
+ " # Get relevant knowledge gaps\n",
240
+ " significant_gaps = {\n",
241
+ " topic: score for topic, score \n",
242
+ " in self.learning_context.knowledge_profile[\"gaps\"].items()\n",
243
+ " if score < 0.7 # Only include significant gaps\n",
244
+ " }\n",
245
+ " \n",
246
+ " prompt = f\"\"\"You are an experienced clinical supervisor in {rotation['specialty']} \n",
247
+ " providing teaching and feedback. You aim to:\n",
248
+ "\n",
249
+ " 1. Help students develop strong clinical reasoning\n",
250
+ " 2. Connect theory to practical applications\n",
251
+ " 3. Build diagnostic confidence\n",
252
+ " 4. Improve presentation skills\n",
253
+ "\n",
254
+ " Current Rotation Focus Areas:\n",
255
+ " {', '.join(rotation['key_focus_areas'])}\n",
256
+ "\n",
257
+ " Areas Needing Attention:\n",
258
+ " {', '.join(f'{topic} (confidence: {score:.1f})' for topic, score in significant_gaps.items()) if significant_gaps else 'No specific gaps identified'}\n",
259
+ "\n",
260
+ " Student's Requested Focus:\n",
261
+ " {', '.join(active_preferences) if active_preferences else 'General clinical feedback'}\n",
262
+ "\n",
263
+ " Engage naturally as a supportive but challenging supervisor would during case \n",
264
+ " presentations. Ask probing questions when appropriate, share relevant clinical \n",
265
+ " pearls, and help the student build their clinical reasoning skills.\"\"\"\n",
266
+ " \n",
267
+ " return prompt\n",
268
+ " \n",
269
+ " def _build_analysis_prompt(self, conversation: List[Dict[str, str]]) -> str:\n",
270
+ " \"\"\"\n",
271
+ " Build prompt for post-discussion analysis.\n",
272
+ " \n",
273
+ " Args:\n",
274
+ " conversation: List of message dictionaries with roles and content\n",
275
+ " \n",
276
+ " Returns:\n",
277
+ " str: Analysis prompt\n",
278
+ " \"\"\"\n",
279
+ " # Extract case details\n",
280
+ " case_content = \"\"\n",
281
+ " for msg in conversation:\n",
282
+ " if msg[\"role\"] == \"user\":\n",
283
+ " case_content += msg[\"content\"] + \"\\n\"\n",
284
+ " \n",
285
+ " return f\"\"\"Analyze the following case discussion between a medical student and \n",
286
+ " clinical supervisor. Focus on the student's demonstrated knowledge, skills, \n",
287
+ " and areas for improvement.\n",
288
+ "\n",
289
+ " Case Content:\n",
290
+ " {case_content}\n",
291
+ "\n",
292
+ " Please identify:\n",
293
+ " 1. Key clinical concepts and learning points demonstrated or discussed\n",
294
+ " 2. Areas where the student showed uncertainty or knowledge gaps\n",
295
+ " 3. Strengths demonstrated in clinical reasoning and presentation\n",
296
+ " 4. Specific learning objectives that would help the student's development\n",
297
+ "\n",
298
+ " Frame your response to help with ongoing learning:\n",
299
+ " - Start with positive observations\n",
300
+ " - Be specific about knowledge gaps\n",
301
+ " - Make concrete suggestions for improvement\n",
302
+ " - Connect to practical clinical scenarios\"\"\"\n",
303
+ " \n",
304
+ " async def discuss_case(\n",
305
+ " self, \n",
306
+ " message: str,\n",
307
+ " temperature: float = 0.7\n",
308
+ " ) -> str:\n",
309
+ " \"\"\"\n",
310
+ " Natural case discussion with context-aware responses.\n",
311
+ " \n",
312
+ " Args:\n",
313
+ " message: Student's input message\n",
314
+ " temperature: Temperature for response generation\n",
315
+ " \n",
316
+ " Returns:\n",
317
+ " str: Clinical supervisor's response\n",
318
+ " \"\"\"\n",
319
+ " try:\n",
320
+ " # Update case tracking\n",
321
+ " if not self.current_case[\"started\"]:\n",
322
+ " self.current_case[\"started\"] = datetime.now()\n",
323
+ " # Try to identify chief complaint from first message\n",
324
+ " cc_match = re.search(r\"(\\d+)\\s*[yY][oO]\\s*[MmFf]\\s*with\\s*([^.]*)\", message)\n",
325
+ " if cc_match:\n",
326
+ " self.current_case[\"chief_complaint\"] = cc_match.group(2).strip()\n",
327
+ " \n",
328
+ " # Build system prompt\n",
329
+ " system_prompt = self._build_discussion_prompt()\n",
330
+ " \n",
331
+ " messages = [{\n",
332
+ " \"role\": \"system\",\n",
333
+ " \"content\": system_prompt\n",
334
+ " }, {\n",
335
+ " \"role\": \"user\",\n",
336
+ " \"content\": message\n",
337
+ " }]\n",
338
+ " \n",
339
+ " response = await self._get_completion(messages, temperature)\n",
340
+ " return response\n",
341
+ " \n",
342
+ " except Exception as e:\n",
343
+ " logger.error(f\"Error in case discussion: {str(e)}\")\n",
344
+ " return \"I apologize, but I encountered an error. Please try presenting your case again.\"\n",
345
+ " \n",
346
+ " async def analyze_discussion(\n",
347
+ " self,\n",
348
+ " conversation: List[Dict[str, str]]\n",
349
+ " ) -> Dict[str, Any]:\n",
350
+ " \"\"\"\n",
351
+ " Analyze completed case discussion for learning insights.\n",
352
+ " \n",
353
+ " Args:\n",
354
+ " conversation: List of message dictionaries with roles and content\n",
355
+ " \n",
356
+ " Returns:\n",
357
+ " dict: Analysis results containing:\n",
358
+ " - learning_points: List of key concepts learned\n",
359
+ " - gaps: Dict of identified knowledge gaps\n",
360
+ " - strengths: List of demonstrated strengths\n",
361
+ " - suggested_objectives: List of recommended learning goals\n",
362
+ " \"\"\"\n",
363
+ " try:\n",
364
+ " # Reset case tracking\n",
365
+ " self.current_case = {\n",
366
+ " \"started\": None,\n",
367
+ " \"chief_complaint\": None,\n",
368
+ " \"key_findings\": [],\n",
369
+ " \"assessment\": None,\n",
370
+ " \"plan\": None\n",
371
+ " }\n",
372
+ " \n",
373
+ " # Get analysis from model\n",
374
+ " analysis_prompt = self._build_analysis_prompt(conversation)\n",
375
+ " messages = [{\n",
376
+ " \"role\": \"system\",\n",
377
+ " \"content\": analysis_prompt\n",
378
+ " }]\n",
379
+ " messages.extend(conversation)\n",
380
+ " \n",
381
+ " response = await self._get_completion(messages, temperature=0.3)\n",
382
+ " \n",
383
+ " # Parse insights\n",
384
+ " insights = self._parse_analysis(response)\n",
385
+ " \n",
386
+ " # Update learning context\n",
387
+ " self._update_context_from_analysis(insights)\n",
388
+ " \n",
389
+ " return insights\n",
390
+ " \n",
391
+ " except Exception as e:\n",
392
+ " logger.error(f\"Error in discussion analysis: {str(e)}\")\n",
393
+ " return {\n",
394
+ " \"learning_points\": [],\n",
395
+ " \"gaps\": {},\n",
396
+ " \"strengths\": [],\n",
397
+ " \"suggested_objectives\": []\n",
398
+ " }\n",
399
+ " \n",
400
+ " def _parse_analysis(self, response: str) -> Dict[str, Any]:\n",
401
+ " \"\"\"\n",
402
+ " Parse analysis response into structured insights.\n",
403
+ " \n",
404
+ " Uses pattern matching and basic NLP to extract:\n",
405
+ " - Learning points (key concepts discussed)\n",
406
+ " - Knowledge gaps with confidence estimates\n",
407
+ " - Demonstrated strengths\n",
408
+ " - Suggested learning objectives\n",
409
+ " \n",
410
+ " Args:\n",
411
+ " response: Raw analysis response\n",
412
+ " \n",
413
+ " Returns:\n",
414
+ " dict: Structured analysis insights\n",
415
+ " \"\"\"\n",
416
+ " insights = {\n",
417
+ " \"learning_points\": [],\n",
418
+ " \"gaps\": {},\n",
419
+ " \"strengths\": [],\n",
420
+ " \"suggested_objectives\": []\n",
421
+ " }\n",
422
+ " \n",
423
+ " try:\n",
424
+ " # Split into sections\n",
425
+ " sections = response.lower().split(\"\\n\\n\")\n",
426
+ " \n",
427
+ " for section in sections:\n",
428
+ " if \"learning point\" in section or \"key concept\" in section:\n",
429
+ " # Extract bullet points or numbered items\n",
430
+ " points = re.findall(r\"[-•*]\\s*(.+)$\", section, re.MULTILINE)\n",
431
+ " insights[\"learning_points\"].extend(points)\n",
432
+ " \n",
433
+ " elif \"gap\" in section or \"uncertainty\" in section:\n",
434
+ " # Look for topic mentions with confidence indicators\n",
435
+ " gaps = re.findall(\n",
436
+ " r\"(limited|uncertain|unclear|difficulty with)\\s+([^,.]+)\", \n",
437
+ " section\n",
438
+ " )\n",
439
+ " for indicator, topic in gaps:\n",
440
+ " # Estimate confidence based on language\n",
441
+ " confidence = 0.4 if \"limited\" in indicator else 0.6\n",
442
+ " insights[\"gaps\"][topic.strip()] = confidence\n",
443
+ " \n",
444
+ " elif \"strength\" in section or \"demonstrated\" in section:\n",
445
+ " # Extract positive mentions\n",
446
+ " strengths = re.findall(r\"[-•*]\\s*(.+)$\", section, re.MULTILINE)\n",
447
+ " insights[\"strengths\"].extend(strengths)\n",
448
+ " \n",
449
+ " elif \"objective\" in section or \"suggest\" in section:\n",
450
+ " # Extract recommended objectives\n",
451
+ " objectives = re.findall(r\"[-•*]\\s*(.+)$\", section, re.MULTILINE)\n",
452
+ " insights[\"suggested_objectives\"].extend(objectives)\n",
453
+ " \n",
454
+ " return insights\n",
455
+ " \n",
456
+ " except Exception as e:\n",
457
+ " logger.error(f\"Error parsing analysis: {str(e)}\")\n",
458
+ " return insights\n",
459
+ " \n",
460
+ " def _update_context_from_analysis(self, insights: Dict[str, Any]) -> None:\n",
461
+ " \"\"\"\n",
462
+ " Update learning context based on discussion analysis.\n",
463
+ " \n",
464
+ " Args:\n",
465
+ " insights: Dictionary of analysis insights\n",
466
+ " \"\"\"\n",
467
+ " try:\n",
468
+ " # Update knowledge gaps\n",
469
+ " for topic, confidence in insights[\"gaps\"].items():\n",
470
+ " self.learning_context.update_knowledge_gap(topic, confidence)\n",
471
+ " \n",
472
+ " # Add strengths\n",
473
+ " for strength in insights[\"strengths\"]:\n",
474
+ " self.learning_context.add_strength(strength)\n",
475
+ " \n",
476
+ " # Save context if path provided\n",
477
+ " if self.context_path:\n",
478
+ " self.learning_context.save_context(self.context_path)\n",
479
+ " \n",
480
+ " except Exception as e:\n",
481
+ " logger.error(f\"Error updating context: {str(e)}\")"
482
+ ]
483
+ },
484
+ {
485
+ "cell_type": "markdown",
486
+ "id": "6a2b15f5-6841-43cb-9b57-c0e3f1a0b0c2",
487
+ "metadata": {},
488
+ "source": [
489
+ "## Tests"
490
+ ]
491
+ },
492
+ {
493
+ "cell_type": "code",
494
+ "execution_count": null,
495
+ "id": "67ee6bde-4ade-448e-a831-86f9f7ae82ea",
496
+ "metadata": {},
497
+ "outputs": [
498
+ {
499
+ "ename": "RuntimeError",
500
+ "evalue": "asyncio.run() cannot be called from a running event loop",
501
+ "output_type": "error",
502
+ "traceback": [
503
+ "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
504
+ "\u001b[1;31mRuntimeError\u001b[0m Traceback (most recent call last)",
505
+ "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",
506
+ "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",
507
+ "\u001b[1;31mRuntimeError\u001b[0m: asyncio.run() cannot be called from a running event loop"
508
+ ]
509
+ }
510
+ ],
511
+ "source": [
512
+ "async def test_clinical_tutor():\n",
513
+ " \"\"\"Test ClinicalTutor functionality\"\"\"\n",
514
+ " if not os.getenv(\"OPENROUTER_API_KEY\"):\n",
515
+ " print(\"Skipping tests: No API key\")\n",
516
+ " return\n",
517
+ " \n",
518
+ " tutor = ClinicalTutor()\n",
519
+ " \n",
520
+ " # Test case discussion\n",
521
+ " test_case = \"\"\"\n",
522
+ " 28yo M with chest pain\n",
523
+ " - 2 days duration\n",
524
+ " - Sharp, pleuritic\n",
525
+ " - No fever or cough\n",
526
+ " - Vitals stable\n",
527
+ " - Clear exam\n",
528
+ " A/P: Likely MSK pain\n",
529
+ " \"\"\"\n",
530
+ " \n",
531
+ " try:\n",
532
+ " # Test basic discussion\n",
533
+ " response = await tutor.discuss_case(test_case)\n",
534
+ " assert isinstance(response, str)\n",
535
+ " assert len(response) > 0\n",
536
+ " \n",
537
+ " # Only assert case tracking if chief complaint was detected\n",
538
+ " if tutor.current_case[\"chief_complaint\"]:\n",
539
+ " assert \"chest pain\" in tutor.current_case[\"chief_complaint\"].lower()\n",
540
+ " \n",
541
+ " print(\"Discussion test passed\")\n",
542
+ " \n",
543
+ " # Test discussion analysis\n",
544
+ " conversation = [\n",
545
+ " {\"role\": \"user\", \"content\": test_case},\n",
546
+ " {\"role\": \"assistant\", \"content\": response}\n",
547
+ " ]\n",
548
+ " \n",
549
+ " analysis = await tutor.analyze_discussion(conversation)\n",
550
+ " assert isinstance(analysis, dict)\n",
551
+ " assert all(k in analysis for k in [\n",
552
+ " 'learning_points', 'gaps', 'strengths', 'suggested_objectives'\n",
553
+ " ])\n",
554
+ " print(\"Analysis test passed\")\n",
555
+ " \n",
556
+ " except Exception as e:\n",
557
+ " print(f\"Test failed: {str(e)}\")\n",
558
+ " raise\n",
559
+ " \n",
560
+ " print(\"All clinical tutor tests passed!\")\n",
561
+ "\n",
562
+ "# Run tests\n",
563
+ "if __name__ == \"__main__\":\n",
564
+ " import asyncio\n",
565
+ " if not asyncio.get_event_loop().is_running():\n",
566
+ " asyncio.run(test_clinical_tutor())"
567
+ ]
568
+ },
569
+ {
570
+ "cell_type": "code",
571
+ "execution_count": null,
572
+ "id": "3f469c37-afe3-4682-9cc4-40326ac21b74",
573
+ "metadata": {},
574
+ "outputs": [],
575
+ "source": []
576
+ }
577
+ ],
578
+ "metadata": {
579
+ "kernelspec": {
580
+ "display_name": "Python 3 (ipykernel)",
581
+ "language": "python",
582
+ "name": "python3"
583
+ },
584
+ "language_info": {
585
+ "codemirror_mode": {
586
+ "name": "ipython",
587
+ "version": 3
588
+ },
589
+ "file_extension": ".py",
590
+ "mimetype": "text/x-python",
591
+ "name": "python",
592
+ "nbconvert_exporter": "python",
593
+ "pygments_lexer": "ipython3",
594
+ "version": "3.12.7"
595
+ }
596
+ },
597
+ "nbformat": 4,
598
+ "nbformat_minor": 5
599
+ }
nbs/02_learning_interface.ipynb ADDED
@@ -0,0 +1,979 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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\n",
70
+ "\n",
71
+ "logger = setup_logger(__name__)"
72
+ ]
73
+ },
74
+ {
75
+ "cell_type": "markdown",
76
+ "id": "c39da1db-e630-4296-93f6-03b9188320cc",
77
+ "metadata": {},
78
+ "source": [
79
+ "## Learning Interface"
80
+ ]
81
+ },
82
+ {
83
+ "cell_type": "code",
84
+ "execution_count": null,
85
+ "id": "aff3d321-2116-475f-b906-f74889e76d66",
86
+ "metadata": {},
87
+ "outputs": [],
88
+ "source": [
89
+ "#| export\n",
90
+ "def create_dashboard_css() -> str:\n",
91
+ " \"\"\"Create custom CSS for dashboard styling\"\"\"\n",
92
+ " return \"\"\"\n",
93
+ " .dashboard-card {\n",
94
+ " border: 1px solid #e2e8f0;\n",
95
+ " border-radius: 8px;\n",
96
+ " padding: 16px;\n",
97
+ " margin: 8px 0;\n",
98
+ " background: white;\n",
99
+ " }\n",
100
+ " \n",
101
+ " .status-active {\n",
102
+ " color: #48bb78;\n",
103
+ " font-weight: 500;\n",
104
+ " }\n",
105
+ " \n",
106
+ " .status-completed {\n",
107
+ " color: #718096;\n",
108
+ " }\n",
109
+ " \n",
110
+ " .dashboard-header {\n",
111
+ " font-size: 1.25rem;\n",
112
+ " font-weight: 600;\n",
113
+ " margin-bottom: 1rem;\n",
114
+ " }\n",
115
+ " \n",
116
+ " .summary-modal {\n",
117
+ " max-width: 800px !important;\n",
118
+ " }\n",
119
+ " \n",
120
+ " .feedback-tag {\n",
121
+ " display: inline-block;\n",
122
+ " padding: 4px 8px;\n",
123
+ " border-radius: 4px;\n",
124
+ " margin: 4px;\n",
125
+ " font-size: 0.875rem;\n",
126
+ " }\n",
127
+ " \n",
128
+ " .feedback-active {\n",
129
+ " background: #ebf8ff;\n",
130
+ " color: #2b6cb0;\n",
131
+ " }\n",
132
+ " \n",
133
+ " .feedback-inactive {\n",
134
+ " background: #f7fafc;\n",
135
+ " color: #718096;\n",
136
+ " }\n",
137
+ " \"\"\""
138
+ ]
139
+ },
140
+ {
141
+ "cell_type": "markdown",
142
+ "id": "de7ace04-4841-461d-89bb-234b5f8b48e1",
143
+ "metadata": {},
144
+ "source": [
145
+ "This module provides the user interface for the clinical learning system, including:\n",
146
+ " * Case presentation and feedback\n",
147
+ " * Learning preference configuration\n",
148
+ " * Session management\n",
149
+ " * Progress visualization"
150
+ ]
151
+ },
152
+ {
153
+ "cell_type": "code",
154
+ "execution_count": null,
155
+ "id": "675c20cc-43aa-4897-89ba-4fa26dd37c20",
156
+ "metadata": {},
157
+ "outputs": [],
158
+ "source": [
159
+ "#| export\n",
160
+ "\n",
161
+ "class LearningInterface:\n",
162
+ " \"\"\"\n",
163
+ " Gradio interface for clinical learning interactions.\n",
164
+ " \n",
165
+ " Features:\n",
166
+ " - Natural case discussion chat\n",
167
+ " - Dynamic learning dashboard\n",
168
+ " - Post-discussion analysis\n",
169
+ " - Progress tracking\n",
170
+ " \"\"\"\n",
171
+ " \n",
172
+ " def __init__(\n",
173
+ " self,\n",
174
+ " context_path: Optional[Path] = None,\n",
175
+ " theme: str = \"default\"\n",
176
+ " ):\n",
177
+ " \"\"\"Initialize learning interface.\"\"\"\n",
178
+ " self.tutor = ClinicalTutor(context_path)\n",
179
+ " self.theme = theme\n",
180
+ " self.context_path = context_path\n",
181
+ " \n",
182
+ " # Track current discussion state\n",
183
+ " self.current_discussion = {\n",
184
+ " \"started\": None,\n",
185
+ " \"case_type\": None,\n",
186
+ " \"messages\": []\n",
187
+ " }\n",
188
+ " \n",
189
+ " logger.info(\"Learning interface initialized\")\n",
190
+ " \n",
191
+ " async def process_chat(\n",
192
+ " self,\n",
193
+ " message: str,\n",
194
+ " history: List[Dict[str, str]],\n",
195
+ " state: Dict[str, Any]\n",
196
+ " ) -> Tuple[List[Dict[str, str]], str, Dict[str, Any]]:\n",
197
+ " \"\"\"\n",
198
+ " Process chat messages with state management.\n",
199
+ " \n",
200
+ " Args:\n",
201
+ " message: User input message\n",
202
+ " history: Chat history\n",
203
+ " state: Current interface state\n",
204
+ " \n",
205
+ " Returns:\n",
206
+ " tuple: (updated history, cleared message, updated state)\n",
207
+ " \"\"\"\n",
208
+ " try:\n",
209
+ " if not message.strip():\n",
210
+ " return history, \"\", state\n",
211
+ " \n",
212
+ " # Start new discussion if none active\n",
213
+ " if not state.get(\"discussion_active\"):\n",
214
+ " state[\"discussion_active\"] = True\n",
215
+ " state[\"discussion_start\"] = datetime.now().isoformat()\n",
216
+ " \n",
217
+ " # Get tutor response\n",
218
+ " response = await self.tutor.discuss_case(message)\n",
219
+ " \n",
220
+ " # Update history and state\n",
221
+ " if history is None:\n",
222
+ " history = []\n",
223
+ " history.extend([\n",
224
+ " {\"role\": \"user\", \"content\": message},\n",
225
+ " {\"role\": \"assistant\", \"content\": response}\n",
226
+ " ])\n",
227
+ " \n",
228
+ " state[\"last_message\"] = datetime.now().isoformat()\n",
229
+ " \n",
230
+ " return history, \"\", state\n",
231
+ " \n",
232
+ " except Exception as e:\n",
233
+ " logger.error(f\"Error in chat: {str(e)}\")\n",
234
+ " return history or [], \"\", state\n",
235
+ "\n",
236
+ " async def end_discussion(\n",
237
+ " self,\n",
238
+ " history: List[Dict[str, str]],\n",
239
+ " state: Dict[str, Any]\n",
240
+ " ) -> Tuple[Dict[str, Any], Dict[str, Any]]:\n",
241
+ " \"\"\"\n",
242
+ " Analyze completed discussion and prepare summary.\n",
243
+ " \n",
244
+ " Args:\n",
245
+ " history: Chat history\n",
246
+ " state: Current interface state\n",
247
+ " \n",
248
+ " Returns:\n",
249
+ " tuple: (analysis results, updated state)\n",
250
+ " \"\"\"\n",
251
+ " try:\n",
252
+ " if not history:\n",
253
+ " return {\n",
254
+ " \"learning_points\": [],\n",
255
+ " \"gaps\": {},\n",
256
+ " \"strengths\": [],\n",
257
+ " \"suggested_objectives\": []\n",
258
+ " }, state\n",
259
+ " \n",
260
+ " # Get analysis\n",
261
+ " analysis = await self.tutor.analyze_discussion(history)\n",
262
+ " \n",
263
+ " # Reset discussion state\n",
264
+ " state[\"discussion_active\"] = False\n",
265
+ " state[\"discussion_start\"] = None\n",
266
+ " state[\"last_message\"] = None\n",
267
+ " \n",
268
+ " return analysis, state\n",
269
+ " \n",
270
+ " except Exception as e:\n",
271
+ " logger.error(f\"Error analyzing discussion: {str(e)}\")\n",
272
+ " return {\n",
273
+ " \"learning_points\": [],\n",
274
+ " \"gaps\": {},\n",
275
+ " \"strengths\": [],\n",
276
+ " \"suggested_objectives\": []\n",
277
+ " }, state\n",
278
+ " \n",
279
+ " def update_rotation(\n",
280
+ " self,\n",
281
+ " specialty: str,\n",
282
+ " start_date: str,\n",
283
+ " end_date: str,\n",
284
+ " focus_areas: str\n",
285
+ " ) -> Tuple[str, str, str, str]:\n",
286
+ " \"\"\"\n",
287
+ " Update rotation details and return updated values.\n",
288
+ " \n",
289
+ " Args:\n",
290
+ " specialty: Rotation specialty\n",
291
+ " start_date: Start date string\n",
292
+ " end_date: End date string\n",
293
+ " focus_areas: Comma-separated focus areas\n",
294
+ " \n",
295
+ " Returns:\n",
296
+ " tuple: Updated field values\n",
297
+ " \"\"\"\n",
298
+ " try:\n",
299
+ " # Parse focus areas\n",
300
+ " focus_list = [\n",
301
+ " area.strip() \n",
302
+ " for area in focus_areas.split(\",\") \n",
303
+ " if area.strip()\n",
304
+ " ]\n",
305
+ " \n",
306
+ " # Update context\n",
307
+ " rotation = {\n",
308
+ " \"specialty\": specialty,\n",
309
+ " \"start_date\": start_date,\n",
310
+ " \"end_date\": end_date,\n",
311
+ " \"key_focus_areas\": focus_list\n",
312
+ " }\n",
313
+ " self.tutor.learning_context.update_rotation(rotation)\n",
314
+ " \n",
315
+ " # Return updated values\n",
316
+ " return (\n",
317
+ " specialty,\n",
318
+ " start_date,\n",
319
+ " end_date,\n",
320
+ " \",\".join(focus_list)\n",
321
+ " )\n",
322
+ " \n",
323
+ " except Exception as e:\n",
324
+ " logger.error(f\"Error updating rotation: {str(e)}\")\n",
325
+ " current = self.tutor.learning_context.current_rotation\n",
326
+ " return (\n",
327
+ " current[\"specialty\"],\n",
328
+ " current[\"start_date\"] or \"\",\n",
329
+ " current[\"end_date\"] or \"\",\n",
330
+ " \",\".join(current[\"key_focus_areas\"])\n",
331
+ " )\n",
332
+ "\n",
333
+ " def add_objective(\n",
334
+ " self,\n",
335
+ " objective: str,\n",
336
+ " objectives_df: pd.DataFrame\n",
337
+ " ) -> pd.DataFrame:\n",
338
+ " \"\"\"\n",
339
+ " Add new learning objective and return updated dataframe.\n",
340
+ " \n",
341
+ " Args:\n",
342
+ " objective: New objective text\n",
343
+ " objectives_df: Current objectives dataframe\n",
344
+ " \n",
345
+ " Returns:\n",
346
+ " pd.DataFrame: Updated objectives list\n",
347
+ " \"\"\"\n",
348
+ " try:\n",
349
+ " if not objective.strip():\n",
350
+ " return objectives_df\n",
351
+ " \n",
352
+ " # Add to context\n",
353
+ " self.tutor.learning_context.add_learning_objective(objective)\n",
354
+ " \n",
355
+ " # Convert to dataframe\n",
356
+ " return pd.DataFrame([\n",
357
+ " [obj[\"objective\"], obj[\"status\"], obj[\"added\"]]\n",
358
+ " for obj in self.tutor.learning_context.learning_objectives\n",
359
+ " ], columns=[\"Objective\", \"Status\", \"Date Added\"])\n",
360
+ " \n",
361
+ " except Exception as e:\n",
362
+ " logger.error(f\"Error adding objective: {str(e)}\")\n",
363
+ " return objectives_df\n",
364
+ "\n",
365
+ " def toggle_objective_status(\n",
366
+ " self,\n",
367
+ " evt: gr.SelectData, # Updated to use gr.SelectData\n",
368
+ " objectives_df: pd.DataFrame\n",
369
+ " ) -> pd.DataFrame:\n",
370
+ " \"\"\"\n",
371
+ " Toggle objective status between active and completed.\n",
372
+ " \n",
373
+ " Args:\n",
374
+ " evt: Gradio select event containing row index\n",
375
+ " objectives_df: Current objectives dataframe\n",
376
+ " \n",
377
+ " Returns:\n",
378
+ " pd.DataFrame: Updated objectives list\n",
379
+ " \"\"\"\n",
380
+ " try:\n",
381
+ " objective_idx = evt.index[0] # Get selected row index\n",
382
+ " if objective_idx >= len(objectives_df):\n",
383
+ " return objectives_df\n",
384
+ " \n",
385
+ " # Get objective\n",
386
+ " objective = objectives_df.iloc[objective_idx][\"Objective\"]\n",
387
+ " current_status = objectives_df.iloc[objective_idx][\"Status\"]\n",
388
+ " \n",
389
+ " # Toggle in context\n",
390
+ " if current_status == \"active\":\n",
391
+ " self.tutor.learning_context.complete_objective(objective)\n",
392
+ " else:\n",
393
+ " self.tutor.learning_context.add_learning_objective(objective)\n",
394
+ " \n",
395
+ " # Update dataframe\n",
396
+ " return pd.DataFrame([\n",
397
+ " [obj[\"objective\"], obj[\"status\"], obj[\"added\"]]\n",
398
+ " for obj in self.tutor.learning_context.learning_objectives\n",
399
+ " ], columns=[\"Objective\", \"Status\", \"Date Added\"])\n",
400
+ " \n",
401
+ " except Exception as e:\n",
402
+ " logger.error(f\"Error toggling objective: {str(e)}\")\n",
403
+ " return objectives_df\n",
404
+ "\n",
405
+ " def add_feedback_focus(\n",
406
+ " self,\n",
407
+ " focus: str,\n",
408
+ " feedback_df: pd.DataFrame\n",
409
+ " ) -> pd.DataFrame:\n",
410
+ " \"\"\"Add new feedback focus area.\"\"\"\n",
411
+ " try:\n",
412
+ " if not focus.strip():\n",
413
+ " return feedback_df\n",
414
+ " \n",
415
+ " # Add to context\n",
416
+ " self.tutor.learning_context.toggle_feedback_focus(focus, True)\n",
417
+ " \n",
418
+ " # Update dataframe\n",
419
+ " return pd.DataFrame([\n",
420
+ " [pref[\"focus\"], pref[\"active\"]]\n",
421
+ " for pref in self.tutor.learning_context.feedback_preferences\n",
422
+ " ], columns=[\"Focus Area\", \"Active\"])\n",
423
+ " \n",
424
+ " except Exception as e:\n",
425
+ " logger.error(f\"Error adding feedback focus: {str(e)}\")\n",
426
+ " return feedback_df\n",
427
+ "\n",
428
+ " def toggle_feedback_status(\n",
429
+ " self,\n",
430
+ " evt: gr.SelectData, # Updated to use gr.SelectData\n",
431
+ " feedback_df: pd.DataFrame\n",
432
+ " ) -> pd.DataFrame:\n",
433
+ " \"\"\"Toggle feedback focus active status.\"\"\"\n",
434
+ " try:\n",
435
+ " focus_idx = evt.index[0] # Get selected row index\n",
436
+ " if focus_idx >= len(feedback_df):\n",
437
+ " return feedback_df\n",
438
+ " \n",
439
+ " # Get focus area\n",
440
+ " focus = feedback_df.iloc[focus_idx][\"Focus Area\"]\n",
441
+ " current_status = feedback_df.iloc[focus_idx][\"Active\"]\n",
442
+ " \n",
443
+ " # Toggle in context\n",
444
+ " self.tutor.learning_context.toggle_feedback_focus(\n",
445
+ " focus, \n",
446
+ " not current_status\n",
447
+ " )\n",
448
+ " \n",
449
+ " # Update dataframe\n",
450
+ " return pd.DataFrame([\n",
451
+ " [pref[\"focus\"], pref[\"active\"]]\n",
452
+ " for pref in self.tutor.learning_context.feedback_preferences\n",
453
+ " ], columns=[\"Focus Area\", \"Active\"])\n",
454
+ " \n",
455
+ " except Exception as e:\n",
456
+ " logger.error(f\"Error toggling feedback: {str(e)}\")\n",
457
+ " return feedback_df\n",
458
+ "\n",
459
+ " def create_interface(self) -> gr.Blocks:\n",
460
+ " \"\"\"Create and configure the Gradio interface\"\"\"\n",
461
+ " with gr.Blocks(\n",
462
+ " title=\"Clinical Learning Assistant\",\n",
463
+ " theme=self.theme,\n",
464
+ " css=create_dashboard_css()\n",
465
+ " ) as interface:\n",
466
+ " # State management\n",
467
+ " state = gr.State({\n",
468
+ " \"discussion_active\": False,\n",
469
+ " \"discussion_start\": None,\n",
470
+ " \"last_message\": None\n",
471
+ " })\n",
472
+ " \n",
473
+ " # Header\n",
474
+ " with gr.Row():\n",
475
+ " gr.Markdown(\n",
476
+ " \"# Clinical Learning Assistant\",\n",
477
+ " elem_classes=[\"dashboard-header\"]\n",
478
+ " )\n",
479
+ " \n",
480
+ " with gr.Row():\n",
481
+ " # Left column - Chat interface\n",
482
+ " with gr.Column(scale=2):\n",
483
+ " # Active discussion indicator\n",
484
+ " discussion_status = gr.Markdown(\n",
485
+ " \"Start a new case discussion\",\n",
486
+ " elem_classes=[\"dashboard-card\"]\n",
487
+ " )\n",
488
+ " \n",
489
+ " # Chat interface\n",
490
+ " chatbot = gr.Chatbot(\n",
491
+ " height=500,\n",
492
+ " label=\"Case Discussion\",\n",
493
+ " show_label=True,\n",
494
+ " elem_classes=[\"dashboard-card\"]\n",
495
+ " )\n",
496
+ " \n",
497
+ " with gr.Row():\n",
498
+ " msg = gr.Textbox(\n",
499
+ " label=\"Present your case or ask questions\",\n",
500
+ " placeholder=(\n",
501
+ " \"Present your case as you would to your supervisor:\\n\"\n",
502
+ " \"- Start with the chief complaint\\n\"\n",
503
+ " \"- Include relevant history and findings\\n\"\n",
504
+ " \"- Share your assessment and plan\"\n",
505
+ " ),\n",
506
+ " lines=5\n",
507
+ " )\n",
508
+ " \n",
509
+ " with gr.Row():\n",
510
+ " clear = gr.Button(\"Clear Discussion\")\n",
511
+ " end_discussion = gr.Button(\n",
512
+ " \"End Discussion & Review\",\n",
513
+ " variant=\"primary\"\n",
514
+ " )\n",
515
+ " \n",
516
+ " # Right column - Learning dashboard\n",
517
+ " with gr.Column(scale=1):\n",
518
+ " with gr.Tabs():\n",
519
+ " # Current Rotation tab\n",
520
+ " with gr.Tab(\"Current Rotation\"):\n",
521
+ " with gr.Column(elem_classes=[\"dashboard-card\"]):\n",
522
+ " specialty = gr.Textbox(\n",
523
+ " label=\"Specialty\",\n",
524
+ " value=self.tutor.learning_context.current_rotation[\"specialty\"]\n",
525
+ " )\n",
526
+ " start_date = gr.Textbox(\n",
527
+ " label=\"Start Date (YYYY-MM-DD)\",\n",
528
+ " value=self.tutor.learning_context.current_rotation[\"start_date\"]\n",
529
+ " )\n",
530
+ " end_date = gr.Textbox(\n",
531
+ " label=\"End Date (YYYY-MM-DD)\",\n",
532
+ " value=self.tutor.learning_context.current_rotation[\"end_date\"]\n",
533
+ " )\n",
534
+ " focus_areas = gr.Textbox(\n",
535
+ " label=\"Key Focus Areas (comma-separated)\",\n",
536
+ " value=\",\".join(\n",
537
+ " self.tutor.learning_context.current_rotation[\"key_focus_areas\"]\n",
538
+ " )\n",
539
+ " )\n",
540
+ " update_rotation_btn = gr.Button(\n",
541
+ " \"Update Rotation\",\n",
542
+ " variant=\"secondary\"\n",
543
+ " )\n",
544
+ " \n",
545
+ " # Learning Objectives tab\n",
546
+ " with gr.Tab(\"Learning Objectives\"):\n",
547
+ " with gr.Column(elem_classes=[\"dashboard-card\"]):\n",
548
+ " objectives_df = gr.DataFrame(\n",
549
+ " headers=[\"Objective\", \"Status\", \"Date Added\"],\n",
550
+ " value=[[\n",
551
+ " obj[\"objective\"],\n",
552
+ " obj[\"status\"],\n",
553
+ " obj[\"added\"]\n",
554
+ " ] for obj in self.tutor.learning_context.learning_objectives],\n",
555
+ " interactive=True,\n",
556
+ " wrap=True\n",
557
+ " )\n",
558
+ " \n",
559
+ " with gr.Row():\n",
560
+ " new_objective = gr.Textbox(\n",
561
+ " label=\"New Learning Objective\",\n",
562
+ " placeholder=\"Enter objective...\"\n",
563
+ " )\n",
564
+ " add_objective_btn = gr.Button(\n",
565
+ " \"Add\",\n",
566
+ " variant=\"secondary\"\n",
567
+ " )\n",
568
+ " \n",
569
+ " # Feedback Preferences tab\n",
570
+ " with gr.Tab(\"Feedback Focus\"):\n",
571
+ " with gr.Column(elem_classes=[\"dashboard-card\"]):\n",
572
+ " feedback_df = gr.DataFrame(\n",
573
+ " headers=[\"Focus Area\", \"Active\"],\n",
574
+ " value=[[\n",
575
+ " pref[\"focus\"],\n",
576
+ " pref[\"active\"]\n",
577
+ " ] for pref in self.tutor.learning_context.feedback_preferences],\n",
578
+ " interactive=True,\n",
579
+ " wrap=True\n",
580
+ " )\n",
581
+ " \n",
582
+ " with gr.Row():\n",
583
+ " new_feedback = gr.Textbox(\n",
584
+ " label=\"New Feedback Focus\",\n",
585
+ " placeholder=\"Enter focus area...\"\n",
586
+ " )\n",
587
+ " add_feedback_btn = gr.Button(\n",
588
+ " \"Add\",\n",
589
+ " variant=\"secondary\"\n",
590
+ " )\n",
591
+ " \n",
592
+ " # Knowledge Profile tab\n",
593
+ " with gr.Tab(\"Knowledge Profile\"):\n",
594
+ " with gr.Column(elem_classes=[\"dashboard-card\"]):\n",
595
+ " # Knowledge Gaps\n",
596
+ " gr.Markdown(\"### Knowledge Gaps\")\n",
597
+ " gaps_display = gr.DataFrame(\n",
598
+ " headers=[\"Topic\", \"Confidence\"],\n",
599
+ " value=[[\n",
600
+ " topic, confidence\n",
601
+ " ] for topic, confidence in \n",
602
+ " self.tutor.learning_context.knowledge_profile[\"gaps\"].items()\n",
603
+ " ],\n",
604
+ " interactive=False\n",
605
+ " )\n",
606
+ " \n",
607
+ " # Strengths Display\n",
608
+ " gr.Markdown(\"### Strengths\")\n",
609
+ " strengths_display = gr.DataFrame(\n",
610
+ " headers=[\"Area\"],\n",
611
+ " value=[[strength] for strength in \n",
612
+ " self.tutor.learning_context.knowledge_profile[\"strengths\"]\n",
613
+ " ],\n",
614
+ " interactive=False\n",
615
+ " )\n",
616
+ " \n",
617
+ " # Recent Progress\n",
618
+ " gr.Markdown(\"### Recent Progress\")\n",
619
+ " progress_display = gr.DataFrame(\n",
620
+ " headers=[\"Topic\", \"Improvement\", \"Date\"],\n",
621
+ " value=[[\n",
622
+ " prog[\"topic\"],\n",
623
+ " f\"{prog['improvement']:.2f}\",\n",
624
+ " prog[\"date\"]\n",
625
+ " ] for prog in \n",
626
+ " self.tutor.learning_context.knowledge_profile[\"recent_progress\"]\n",
627
+ " ],\n",
628
+ " interactive=False\n",
629
+ " )\n",
630
+ " \n",
631
+ " # Discussion summary section\n",
632
+ " summary_section = gr.Column(visible=False)\n",
633
+ " with summary_section:\n",
634
+ " gr.Markdown(\"## Discussion Summary\")\n",
635
+ " \n",
636
+ " # Overview section\n",
637
+ " with gr.Row():\n",
638
+ " with gr.Column():\n",
639
+ " gr.Markdown(\"### Session Overview\")\n",
640
+ " session_overview = gr.JSON(\n",
641
+ " label=\"Discussion Details\",\n",
642
+ " value={\n",
643
+ " \"duration\": \"0 minutes\",\n",
644
+ " \"messages\": 0,\n",
645
+ " \"topics_covered\": []\n",
646
+ " }\n",
647
+ " )\n",
648
+ " \n",
649
+ " # Learning Points and Gaps\n",
650
+ " with gr.Row():\n",
651
+ " with gr.Column():\n",
652
+ " gr.Markdown(\"### Key Learning Points\")\n",
653
+ " learning_points = gr.JSON(label=\"Points to Remember\")\n",
654
+ " \n",
655
+ " with gr.Column():\n",
656
+ " gr.Markdown(\"### Knowledge Profile Updates\")\n",
657
+ " with gr.Row():\n",
658
+ " gaps = gr.JSON(label=\"Areas for Improvement\")\n",
659
+ " strengths = gr.JSON(label=\"Demonstrated Strengths\")\n",
660
+ " \n",
661
+ " # Future Learning section\n",
662
+ " gr.Markdown(\"### Planning Ahead\")\n",
663
+ " with gr.Row():\n",
664
+ " with gr.Column():\n",
665
+ " gr.Markdown(\"#### Suggested Learning Objectives\")\n",
666
+ " objectives = gr.JSON(label=\"Consider Adding\")\n",
667
+ " \n",
668
+ " with gr.Column():\n",
669
+ " gr.Markdown(\"#### Recommended Focus Areas\")\n",
670
+ " recommendations = gr.JSON(label=\"Next Steps\")\n",
671
+ " \n",
672
+ " # Action buttons\n",
673
+ " with gr.Row():\n",
674
+ " add_selected_objectives = gr.Button(\n",
675
+ " \"Add Selected Objectives\",\n",
676
+ " variant=\"primary\"\n",
677
+ " )\n",
678
+ " close_summary = gr.Button(\"Close Summary\")\n",
679
+ " \n",
680
+ " # Event handlers\n",
681
+ " msg.submit(\n",
682
+ " self.process_chat,\n",
683
+ " inputs=[msg, chatbot, state],\n",
684
+ " outputs=[chatbot, msg, state]\n",
685
+ " ).then(\n",
686
+ " self._update_discussion_status,\n",
687
+ " inputs=[state],\n",
688
+ " outputs=[discussion_status]\n",
689
+ " )\n",
690
+ " \n",
691
+ " clear.click(\n",
692
+ " lambda: ([], \"\", {\n",
693
+ " \"discussion_active\": False,\n",
694
+ " \"discussion_start\": None,\n",
695
+ " \"last_message\": None\n",
696
+ " }),\n",
697
+ " outputs=[chatbot, msg, state]\n",
698
+ " ).then(\n",
699
+ " lambda: \"Start a new case discussion\",\n",
700
+ " outputs=[discussion_status]\n",
701
+ " )\n",
702
+ " \n",
703
+ " end_discussion.click(\n",
704
+ " self.end_discussion,\n",
705
+ " inputs=[chatbot, state],\n",
706
+ " outputs=[\n",
707
+ " session_overview,\n",
708
+ " learning_points,\n",
709
+ " gaps,\n",
710
+ " strengths,\n",
711
+ " objectives,\n",
712
+ " recommendations\n",
713
+ " ]\n",
714
+ " ).then(\n",
715
+ " lambda: gr.update(visible=True),\n",
716
+ " None,\n",
717
+ " summary_section\n",
718
+ " ).then(\n",
719
+ " self._refresh_knowledge_profile,\n",
720
+ " outputs=[gaps_display, strengths_display, progress_display]\n",
721
+ " )\n",
722
+ " \n",
723
+ " close_summary.click(\n",
724
+ " lambda: gr.update(visible=False),\n",
725
+ " None,\n",
726
+ " summary_section\n",
727
+ " )\n",
728
+ " \n",
729
+ " # Rotation management\n",
730
+ " update_rotation_btn.click(\n",
731
+ " self.update_rotation,\n",
732
+ " inputs=[specialty, start_date, end_date, focus_areas],\n",
733
+ " outputs=[specialty, start_date, end_date, focus_areas]\n",
734
+ " )\n",
735
+ " \n",
736
+ " # Learning objectives management\n",
737
+ " add_objective_btn.click(\n",
738
+ " self.add_objective,\n",
739
+ " inputs=[new_objective, objectives_df],\n",
740
+ " outputs=[objectives_df]\n",
741
+ " ).then(\n",
742
+ " lambda: \"\",\n",
743
+ " None,\n",
744
+ " new_objective\n",
745
+ " )\n",
746
+ " \n",
747
+ " objectives_df.select(\n",
748
+ " self.toggle_objective_status,\n",
749
+ " inputs=[objectives_df],\n",
750
+ " outputs=[objectives_df]\n",
751
+ " )\n",
752
+ " \n",
753
+ " # Feedback preferences management\n",
754
+ " add_feedback_btn.click(\n",
755
+ " self.add_feedback_focus,\n",
756
+ " inputs=[new_feedback, feedback_df],\n",
757
+ " outputs=[feedback_df]\n",
758
+ " ).then(\n",
759
+ " lambda: \"\",\n",
760
+ " None,\n",
761
+ " new_feedback\n",
762
+ " )\n",
763
+ " \n",
764
+ " feedback_df.select(\n",
765
+ " self.toggle_feedback_status,\n",
766
+ " inputs=[feedback_df],\n",
767
+ " outputs=[feedback_df]\n",
768
+ " )\n",
769
+ " \n",
770
+ " # Add selected objectives from summary\n",
771
+ " add_selected_objectives.click(\n",
772
+ " self._add_suggested_objectives,\n",
773
+ " inputs=[objectives],\n",
774
+ " outputs=[objectives_df]\n",
775
+ " )\n",
776
+ " \n",
777
+ " return interface\n",
778
+ " \n",
779
+ " def _update_discussion_status(self, state: Dict[str, Any]) -> str:\n",
780
+ " \"\"\"Update discussion status display\"\"\"\n",
781
+ " try:\n",
782
+ " if not state.get(\"discussion_active\"):\n",
783
+ " return \"Start a new case discussion\"\n",
784
+ " \n",
785
+ " start = datetime.fromisoformat(state[\"discussion_start\"])\n",
786
+ " duration = datetime.now() - start\n",
787
+ " minutes = int(duration.total_seconds() / 60)\n",
788
+ " \n",
789
+ " return f\"Active discussion ({minutes} minutes)\"\n",
790
+ " \n",
791
+ " except Exception as e:\n",
792
+ " logger.error(f\"Error updating status: {str(e)}\")\n",
793
+ " return \"Discussion status unknown\"\n",
794
+ " \n",
795
+ " def _refresh_knowledge_profile(\n",
796
+ " self\n",
797
+ " ) -> Tuple[List[List[str]], List[List[str]], List[List[str]]]:\n",
798
+ " \"\"\"Refresh knowledge profile displays\"\"\"\n",
799
+ " try:\n",
800
+ " # Gaps\n",
801
+ " gaps_data = [[\n",
802
+ " topic, f\"{confidence:.2f}\"\n",
803
+ " ] for topic, confidence in \n",
804
+ " self.tutor.learning_context.knowledge_profile[\"gaps\"].items()\n",
805
+ " ]\n",
806
+ " \n",
807
+ " # Strengths\n",
808
+ " strengths_data = [[\n",
809
+ " strength\n",
810
+ " ] for strength in \n",
811
+ " self.tutor.learning_context.knowledge_profile[\"strengths\"]\n",
812
+ " ]\n",
813
+ " \n",
814
+ " # Progress\n",
815
+ " progress_data = [[\n",
816
+ " prog[\"topic\"],\n",
817
+ " f\"{prog['improvement']:.2f}\",\n",
818
+ " prog[\"date\"]\n",
819
+ " ] for prog in \n",
820
+ " self.tutor.learning_context.knowledge_profile[\"recent_progress\"]\n",
821
+ " ]\n",
822
+ " \n",
823
+ " return gaps_data, strengths_data, progress_data\n",
824
+ " \n",
825
+ " except Exception as e:\n",
826
+ " logger.error(f\"Error refreshing profile: {str(e)}\")\n",
827
+ " return [], [], []\n",
828
+ " \n",
829
+ " def _add_suggested_objectives(\n",
830
+ " self,\n",
831
+ " evt: gr.SelectData, # Updated to use gr.SelectData\n",
832
+ " suggested_objectives: List[str]\n",
833
+ " ) -> pd.DataFrame:\n",
834
+ " \"\"\"Add selected suggested objectives to learning objectives\"\"\"\n",
835
+ " try:\n",
836
+ " selected_indices = [evt.index[0]] # Get selected row index\n",
837
+ " \n",
838
+ " for idx in selected_indices:\n",
839
+ " if idx < len(suggested_objectives):\n",
840
+ " objective = suggested_objectives[idx]\n",
841
+ " self.tutor.learning_context.add_learning_objective(objective)\n",
842
+ " \n",
843
+ " return pd.DataFrame([\n",
844
+ " [obj[\"objective\"], obj[\"status\"], obj[\"added\"]]\n",
845
+ " for obj in self.tutor.learning_context.learning_objectives\n",
846
+ " ], columns=[\"Objective\", \"Status\", \"Date Added\"])\n",
847
+ " \n",
848
+ " except Exception as e:\n",
849
+ " logger.error(f\"Error adding objectives: {str(e)}\")\n",
850
+ " return pd.DataFrame()"
851
+ ]
852
+ },
853
+ {
854
+ "cell_type": "markdown",
855
+ "id": "30c0f121-5d5f-4dc0-b897-f6e2067a63b2",
856
+ "metadata": {},
857
+ "source": [
858
+ "## Launch Function"
859
+ ]
860
+ },
861
+ {
862
+ "cell_type": "code",
863
+ "execution_count": null,
864
+ "id": "65f97529-b221-4a19-9856-fb20d7f7316e",
865
+ "metadata": {},
866
+ "outputs": [],
867
+ "source": [
868
+ "#| export\n",
869
+ "async def launch_learning_interface(\n",
870
+ " port: Optional[int] = None,\n",
871
+ " context_path: Optional[Path] = None,\n",
872
+ " share: bool = False,\n",
873
+ " theme: str = \"default\"\n",
874
+ ") -> None:\n",
875
+ " \"\"\"Launch the learning interface application.\"\"\"\n",
876
+ " try:\n",
877
+ " interface = LearningInterface(context_path, theme)\n",
878
+ " app = interface.create_interface()\n",
879
+ " app.launch(\n",
880
+ " server_port=port,\n",
881
+ " share=share\n",
882
+ " )\n",
883
+ " logger.info(f\"Interface launched on port: {port}\")\n",
884
+ " except Exception as e:\n",
885
+ " logger.error(f\"Error launching interface: {str(e)}\")\n",
886
+ " raise"
887
+ ]
888
+ },
889
+ {
890
+ "cell_type": "markdown",
891
+ "id": "5c75de88-f6d5-4a5d-92b5-1ebe85895a84",
892
+ "metadata": {},
893
+ "source": [
894
+ "## Tests"
895
+ ]
896
+ },
897
+ {
898
+ "cell_type": "code",
899
+ "execution_count": null,
900
+ "id": "365bc95a-d189-4ab2-aa30-022d0286b5ba",
901
+ "metadata": {},
902
+ "outputs": [],
903
+ "source": [
904
+ "async def test_learning_interface():\n",
905
+ " \"\"\"Test learning interface functionality\"\"\"\n",
906
+ " interface = LearningInterface()\n",
907
+ " \n",
908
+ " # Test chat processing\n",
909
+ " history = []\n",
910
+ " test_input = \"28yo M with chest pain\"\n",
911
+ " \n",
912
+ " new_history, msg = await interface.process_chat(test_input, history)\n",
913
+ " assert isinstance(new_history, list)\n",
914
+ " assert len(new_history) == 2 # User message + response\n",
915
+ " assert new_history[0][\"role\"] == \"user\"\n",
916
+ " assert new_history[0][\"content\"] == test_input\n",
917
+ " \n",
918
+ " # Test discussion analysis\n",
919
+ " analysis = await interface.end_discussion(new_history)\n",
920
+ " assert isinstance(analysis, dict)\n",
921
+ " assert all(k in analysis for k in [\n",
922
+ " 'learning_points', 'gaps', 'strengths', 'suggested_objectives'\n",
923
+ " ])\n",
924
+ " \n",
925
+ " # Test rotation updates\n",
926
+ " rotation = interface.update_rotation(\n",
927
+ " \"Emergency Medicine\",\n",
928
+ " \"2025-01-01\",\n",
929
+ " \"2025-03-31\",\n",
930
+ " [\"Resuscitation\", \"Procedures\"]\n",
931
+ " )\n",
932
+ " assert rotation[\"specialty\"] == \"Emergency Medicine\"\n",
933
+ " assert \"Resuscitation\" in rotation[\"key_focus_areas\"]\n",
934
+ " \n",
935
+ " # Test objective management\n",
936
+ " objectives = interface.toggle_objective(\"Improve chest pain assessment\", False)\n",
937
+ " assert len(objectives) == 1\n",
938
+ " assert objectives[0][\"status\"] == \"active\"\n",
939
+ " \n",
940
+ " objectives = interface.toggle_objective(\"Improve chest pain assessment\", True)\n",
941
+ " assert objectives[0][\"status\"] == \"completed\"\n",
942
+ " \n",
943
+ " # Test feedback preferences\n",
944
+ " preferences = interface.toggle_feedback(\"Include more ddx\", True)\n",
945
+ " assert len(preferences) == 1\n",
946
+ " assert preferences[0][\"active\"] == True\n",
947
+ " \n",
948
+ " print(\"Interface tests passed!\")\n",
949
+ "\n",
950
+ "# Run tests\n",
951
+ "if __name__ == \"__main__\":\n",
952
+ " import asyncio\n",
953
+ " if not asyncio.get_event_loop().is_running():\n",
954
+ " asyncio.run(test_learning_interface())"
955
+ ]
956
+ }
957
+ ],
958
+ "metadata": {
959
+ "kernelspec": {
960
+ "display_name": "Python 3 (ipykernel)",
961
+ "language": "python",
962
+ "name": "python3"
963
+ },
964
+ "language_info": {
965
+ "codemirror_mode": {
966
+ "name": "ipython",
967
+ "version": 3
968
+ },
969
+ "file_extension": ".py",
970
+ "mimetype": "text/x-python",
971
+ "name": "python",
972
+ "nbconvert_exporter": "python",
973
+ "pygments_lexer": "ipython3",
974
+ "version": "3.12.7"
975
+ }
976
+ },
977
+ "nbformat": 4,
978
+ "nbformat_minor": 5
979
+ }
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/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"
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 = djey0003@student.monash.edu
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,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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_analysis_prompt': ( 'clinical_tutor.html#clinicaltutor._build_analysis_prompt',
13
+ 'wardbuddy/clinical_tutor.py'),
14
+ 'wardbuddy.clinical_tutor.ClinicalTutor._build_discussion_prompt': ( 'clinical_tutor.html#clinicaltutor._build_discussion_prompt',
15
+ 'wardbuddy/clinical_tutor.py'),
16
+ 'wardbuddy.clinical_tutor.ClinicalTutor._get_completion': ( 'clinical_tutor.html#clinicaltutor._get_completion',
17
+ 'wardbuddy/clinical_tutor.py'),
18
+ 'wardbuddy.clinical_tutor.ClinicalTutor._parse_analysis': ( 'clinical_tutor.html#clinicaltutor._parse_analysis',
19
+ 'wardbuddy/clinical_tutor.py'),
20
+ 'wardbuddy.clinical_tutor.ClinicalTutor._update_context_from_analysis': ( 'clinical_tutor.html#clinicaltutor._update_context_from_analysis',
21
+ 'wardbuddy/clinical_tutor.py'),
22
+ 'wardbuddy.clinical_tutor.ClinicalTutor.analyze_discussion': ( 'clinical_tutor.html#clinicaltutor.analyze_discussion',
23
+ 'wardbuddy/clinical_tutor.py'),
24
+ 'wardbuddy.clinical_tutor.ClinicalTutor.discuss_case': ( 'clinical_tutor.html#clinicaltutor.discuss_case',
25
+ 'wardbuddy/clinical_tutor.py'),
26
+ 'wardbuddy.clinical_tutor.OpenRouterException': ( 'clinical_tutor.html#openrouterexception',
27
+ 'wardbuddy/clinical_tutor.py')},
28
+ 'wardbuddy.core': {'wardbuddy.core.foo': ('core.html#foo', 'wardbuddy/core.py')},
29
+ 'wardbuddy.learning_context': { 'wardbuddy.learning_context.LearningContext': ( 'learning_context.html#learningcontext',
30
+ 'wardbuddy/learning_context.py'),
31
+ 'wardbuddy.learning_context.LearningContext.__init__': ( 'learning_context.html#learningcontext.__init__',
32
+ 'wardbuddy/learning_context.py'),
33
+ 'wardbuddy.learning_context.LearningContext.add_learning_objective': ( 'learning_context.html#learningcontext.add_learning_objective',
34
+ 'wardbuddy/learning_context.py'),
35
+ 'wardbuddy.learning_context.LearningContext.add_strength': ( 'learning_context.html#learningcontext.add_strength',
36
+ 'wardbuddy/learning_context.py'),
37
+ 'wardbuddy.learning_context.LearningContext.complete_objective': ( 'learning_context.html#learningcontext.complete_objective',
38
+ 'wardbuddy/learning_context.py'),
39
+ 'wardbuddy.learning_context.LearningContext.load_context': ( 'learning_context.html#learningcontext.load_context',
40
+ 'wardbuddy/learning_context.py'),
41
+ 'wardbuddy.learning_context.LearningContext.save_context': ( 'learning_context.html#learningcontext.save_context',
42
+ 'wardbuddy/learning_context.py'),
43
+ 'wardbuddy.learning_context.LearningContext.toggle_feedback_focus': ( 'learning_context.html#learningcontext.toggle_feedback_focus',
44
+ 'wardbuddy/learning_context.py'),
45
+ 'wardbuddy.learning_context.LearningContext.update_knowledge_gap': ( 'learning_context.html#learningcontext.update_knowledge_gap',
46
+ 'wardbuddy/learning_context.py'),
47
+ 'wardbuddy.learning_context.LearningContext.update_rotation': ( 'learning_context.html#learningcontext.update_rotation',
48
+ 'wardbuddy/learning_context.py')},
49
+ 'wardbuddy.learning_interface': { 'wardbuddy.learning_interface.LearningInterface': ( 'learning_interface.html#learninginterface',
50
+ 'wardbuddy/learning_interface.py'),
51
+ 'wardbuddy.learning_interface.LearningInterface.__init__': ( 'learning_interface.html#learninginterface.__init__',
52
+ 'wardbuddy/learning_interface.py'),
53
+ 'wardbuddy.learning_interface.LearningInterface._add_suggested_objectives': ( 'learning_interface.html#learninginterface._add_suggested_objectives',
54
+ 'wardbuddy/learning_interface.py'),
55
+ 'wardbuddy.learning_interface.LearningInterface._refresh_knowledge_profile': ( 'learning_interface.html#learninginterface._refresh_knowledge_profile',
56
+ 'wardbuddy/learning_interface.py'),
57
+ 'wardbuddy.learning_interface.LearningInterface._update_discussion_status': ( 'learning_interface.html#learninginterface._update_discussion_status',
58
+ 'wardbuddy/learning_interface.py'),
59
+ 'wardbuddy.learning_interface.LearningInterface.add_feedback_focus': ( 'learning_interface.html#learninginterface.add_feedback_focus',
60
+ 'wardbuddy/learning_interface.py'),
61
+ 'wardbuddy.learning_interface.LearningInterface.add_objective': ( 'learning_interface.html#learninginterface.add_objective',
62
+ 'wardbuddy/learning_interface.py'),
63
+ 'wardbuddy.learning_interface.LearningInterface.create_interface': ( 'learning_interface.html#learninginterface.create_interface',
64
+ 'wardbuddy/learning_interface.py'),
65
+ 'wardbuddy.learning_interface.LearningInterface.end_discussion': ( 'learning_interface.html#learninginterface.end_discussion',
66
+ 'wardbuddy/learning_interface.py'),
67
+ 'wardbuddy.learning_interface.LearningInterface.process_chat': ( 'learning_interface.html#learninginterface.process_chat',
68
+ 'wardbuddy/learning_interface.py'),
69
+ 'wardbuddy.learning_interface.LearningInterface.toggle_feedback_status': ( 'learning_interface.html#learninginterface.toggle_feedback_status',
70
+ 'wardbuddy/learning_interface.py'),
71
+ 'wardbuddy.learning_interface.LearningInterface.toggle_objective_status': ( 'learning_interface.html#learninginterface.toggle_objective_status',
72
+ 'wardbuddy/learning_interface.py'),
73
+ 'wardbuddy.learning_interface.LearningInterface.update_rotation': ( 'learning_interface.html#learninginterface.update_rotation',
74
+ 'wardbuddy/learning_interface.py'),
75
+ 'wardbuddy.learning_interface.create_dashboard_css': ( 'learning_interface.html#create_dashboard_css',
76
+ 'wardbuddy/learning_interface.py'),
77
+ 'wardbuddy.learning_interface.launch_learning_interface': ( 'learning_interface.html#launch_learning_interface',
78
+ 'wardbuddy/learning_interface.py')},
79
+ 'wardbuddy.utils': { 'wardbuddy.utils.load_context_safely': ('utils.html#load_context_safely', 'wardbuddy/utils.py'),
80
+ 'wardbuddy.utils.save_context_safely': ('utils.html#save_context_safely', 'wardbuddy/utils.py'),
81
+ 'wardbuddy.utils.setup_logger': ('utils.html#setup_logger', 'wardbuddy/utils.py')}}}
wardbuddy/clinical_tutor.py ADDED
@@ -0,0 +1,392 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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, Any, Tuple
10
+ import os
11
+ import json
12
+ import logging
13
+ from pathlib import Path
14
+ from datetime import datetime
15
+ from dotenv import load_dotenv
16
+ import aiohttp
17
+ import re
18
+ from collections import defaultdict
19
+ from .learning_context import LearningContext, setup_logger
20
+
21
+ # Load environment variables
22
+ load_dotenv()
23
+
24
+ logger = setup_logger(__name__)
25
+
26
+ # %% ../nbs/01_clinical_tutor.ipynb 7
27
+ class OpenRouterException(Exception):
28
+ """Custom exception for OpenRouter API errors"""
29
+ pass
30
+
31
+ # %% ../nbs/01_clinical_tutor.ipynb 8
32
+ class ClinicalTutor:
33
+ """
34
+ Adaptive clinical teaching module that provides context-aware feedback.
35
+
36
+ The tutor acts as an experienced clinical supervisor, engaging in natural
37
+ case discussions while tracking student progress and adapting feedback
38
+ based on learning context.
39
+
40
+ Attributes:
41
+ learning_context (LearningContext): Student's learning context
42
+ model (str): LLM model identifier
43
+ api_url (str): OpenRouter API endpoint
44
+ """
45
+
46
+ def __init__(
47
+ self,
48
+ context_path: Optional[Path] = None,
49
+ model: str = "anthropic/claude-3.5-sonnet"
50
+ ):
51
+ """
52
+ Initialize clinical tutor.
53
+
54
+ Args:
55
+ context_path: Optional path for context persistence
56
+ model: Model identifier for OpenRouter
57
+ """
58
+ self.api_key: str = os.getenv("OPENROUTER_API_KEY")
59
+ if not self.api_key:
60
+ raise ValueError("OpenRouter API key not found")
61
+
62
+ self.api_url: str = "https://openrouter.ai/api/v1/chat/completions"
63
+ self.model: str = model
64
+
65
+ self.learning_context = LearningContext(context_path)
66
+ self.context_path = context_path
67
+
68
+ # Track conversation state
69
+ self.current_case: Dict = {
70
+ "started": None,
71
+ "chief_complaint": None,
72
+ "key_findings": [],
73
+ "assessment": None,
74
+ "plan": None
75
+ }
76
+
77
+ logger.info(f"Clinical tutor initialized with model: {model}")
78
+
79
+ async def _get_completion(
80
+ self,
81
+ messages: List[Dict],
82
+ temperature: float = 0.7,
83
+ max_retries: int = 3
84
+ ) -> str:
85
+ """
86
+ Get completion from OpenRouter API with retry logic.
87
+
88
+ Args:
89
+ messages: List of conversation messages
90
+ temperature: Temperature for response generation
91
+ max_retries: Maximum retry attempts
92
+
93
+ Returns:
94
+ str: Model response
95
+
96
+ Raises:
97
+ OpenRouterException: If API calls fail after retries
98
+ """
99
+ headers = {
100
+ "Authorization": f"Bearer {self.api_key}",
101
+ "Content-Type": "application/json",
102
+ "HTTP-Referer": "http://localhost:7860"
103
+ }
104
+
105
+ data = {
106
+ "model": self.model,
107
+ "messages": messages,
108
+ "temperature": temperature,
109
+ "max_tokens": 2000
110
+ }
111
+
112
+ for attempt in range(max_retries):
113
+ try:
114
+ async with aiohttp.ClientSession() as session:
115
+ async with session.post(
116
+ self.api_url,
117
+ headers=headers,
118
+ json=data,
119
+ timeout=30
120
+ ) as response:
121
+ response.raise_for_status()
122
+ result = await response.json()
123
+ return result["choices"][0]["message"]["content"]
124
+
125
+ except Exception as e:
126
+ if attempt == max_retries - 1:
127
+ raise OpenRouterException(f"API call failed: {str(e)}")
128
+ logger.warning(f"Retry {attempt + 1} after error: {str(e)}")
129
+ # Could add exponential backoff here if needed
130
+
131
+ def _build_discussion_prompt(self) -> str:
132
+ """
133
+ Build context-aware prompt for case discussion.
134
+
135
+ Incorporates:
136
+ - Current rotation details
137
+ - Active feedback preferences
138
+ - Recent learning points
139
+ - Knowledge gaps needing attention
140
+
141
+ Returns:
142
+ str: Contextualized system prompt
143
+ """
144
+ rotation = self.learning_context.current_rotation
145
+ active_preferences = [
146
+ p["focus"] for p in self.learning_context.feedback_preferences
147
+ if p["active"]
148
+ ]
149
+
150
+ # Get relevant knowledge gaps
151
+ significant_gaps = {
152
+ topic: score for topic, score
153
+ in self.learning_context.knowledge_profile["gaps"].items()
154
+ if score < 0.7 # Only include significant gaps
155
+ }
156
+
157
+ prompt = f"""You are an experienced clinical supervisor in {rotation['specialty']}
158
+ providing teaching and feedback. You aim to:
159
+
160
+ 1. Help students develop strong clinical reasoning
161
+ 2. Connect theory to practical applications
162
+ 3. Build diagnostic confidence
163
+ 4. Improve presentation skills
164
+
165
+ Current Rotation Focus Areas:
166
+ {', '.join(rotation['key_focus_areas'])}
167
+
168
+ Areas Needing Attention:
169
+ {', '.join(f'{topic} (confidence: {score:.1f})' for topic, score in significant_gaps.items()) if significant_gaps else 'No specific gaps identified'}
170
+
171
+ Student's Requested Focus:
172
+ {', '.join(active_preferences) if active_preferences else 'General clinical feedback'}
173
+
174
+ Engage naturally as a supportive but challenging supervisor would during case
175
+ presentations. Ask probing questions when appropriate, share relevant clinical
176
+ pearls, and help the student build their clinical reasoning skills."""
177
+
178
+ return prompt
179
+
180
+ def _build_analysis_prompt(self, conversation: List[Dict[str, str]]) -> str:
181
+ """
182
+ Build prompt for post-discussion analysis.
183
+
184
+ Args:
185
+ conversation: List of message dictionaries with roles and content
186
+
187
+ Returns:
188
+ str: Analysis prompt
189
+ """
190
+ # Extract case details
191
+ case_content = ""
192
+ for msg in conversation:
193
+ if msg["role"] == "user":
194
+ case_content += msg["content"] + "\n"
195
+
196
+ return f"""Analyze the following case discussion between a medical student and
197
+ clinical supervisor. Focus on the student's demonstrated knowledge, skills,
198
+ and areas for improvement.
199
+
200
+ Case Content:
201
+ {case_content}
202
+
203
+ Please identify:
204
+ 1. Key clinical concepts and learning points demonstrated or discussed
205
+ 2. Areas where the student showed uncertainty or knowledge gaps
206
+ 3. Strengths demonstrated in clinical reasoning and presentation
207
+ 4. Specific learning objectives that would help the student's development
208
+
209
+ Frame your response to help with ongoing learning:
210
+ - Start with positive observations
211
+ - Be specific about knowledge gaps
212
+ - Make concrete suggestions for improvement
213
+ - Connect to practical clinical scenarios"""
214
+
215
+ async def discuss_case(
216
+ self,
217
+ message: str,
218
+ temperature: float = 0.7
219
+ ) -> str:
220
+ """
221
+ Natural case discussion with context-aware responses.
222
+
223
+ Args:
224
+ message: Student's input message
225
+ temperature: Temperature for response generation
226
+
227
+ Returns:
228
+ str: Clinical supervisor's response
229
+ """
230
+ try:
231
+ # Update case tracking
232
+ if not self.current_case["started"]:
233
+ self.current_case["started"] = datetime.now()
234
+ # Try to identify chief complaint from first message
235
+ cc_match = re.search(r"(\d+)\s*[yY][oO]\s*[MmFf]\s*with\s*([^.]*)", message)
236
+ if cc_match:
237
+ self.current_case["chief_complaint"] = cc_match.group(2).strip()
238
+
239
+ # Build system prompt
240
+ system_prompt = self._build_discussion_prompt()
241
+
242
+ messages = [{
243
+ "role": "system",
244
+ "content": system_prompt
245
+ }, {
246
+ "role": "user",
247
+ "content": message
248
+ }]
249
+
250
+ response = await self._get_completion(messages, temperature)
251
+ return response
252
+
253
+ except Exception as e:
254
+ logger.error(f"Error in case discussion: {str(e)}")
255
+ return "I apologize, but I encountered an error. Please try presenting your case again."
256
+
257
+ async def analyze_discussion(
258
+ self,
259
+ conversation: List[Dict[str, str]]
260
+ ) -> Dict[str, Any]:
261
+ """
262
+ Analyze completed case discussion for learning insights.
263
+
264
+ Args:
265
+ conversation: List of message dictionaries with roles and content
266
+
267
+ Returns:
268
+ dict: Analysis results containing:
269
+ - learning_points: List of key concepts learned
270
+ - gaps: Dict of identified knowledge gaps
271
+ - strengths: List of demonstrated strengths
272
+ - suggested_objectives: List of recommended learning goals
273
+ """
274
+ try:
275
+ # Reset case tracking
276
+ self.current_case = {
277
+ "started": None,
278
+ "chief_complaint": None,
279
+ "key_findings": [],
280
+ "assessment": None,
281
+ "plan": None
282
+ }
283
+
284
+ # Get analysis from model
285
+ analysis_prompt = self._build_analysis_prompt(conversation)
286
+ messages = [{
287
+ "role": "system",
288
+ "content": analysis_prompt
289
+ }]
290
+ messages.extend(conversation)
291
+
292
+ response = await self._get_completion(messages, temperature=0.3)
293
+
294
+ # Parse insights
295
+ insights = self._parse_analysis(response)
296
+
297
+ # Update learning context
298
+ self._update_context_from_analysis(insights)
299
+
300
+ return insights
301
+
302
+ except Exception as e:
303
+ logger.error(f"Error in discussion analysis: {str(e)}")
304
+ return {
305
+ "learning_points": [],
306
+ "gaps": {},
307
+ "strengths": [],
308
+ "suggested_objectives": []
309
+ }
310
+
311
+ def _parse_analysis(self, response: str) -> Dict[str, Any]:
312
+ """
313
+ Parse analysis response into structured insights.
314
+
315
+ Uses pattern matching and basic NLP to extract:
316
+ - Learning points (key concepts discussed)
317
+ - Knowledge gaps with confidence estimates
318
+ - Demonstrated strengths
319
+ - Suggested learning objectives
320
+
321
+ Args:
322
+ response: Raw analysis response
323
+
324
+ Returns:
325
+ dict: Structured analysis insights
326
+ """
327
+ insights = {
328
+ "learning_points": [],
329
+ "gaps": {},
330
+ "strengths": [],
331
+ "suggested_objectives": []
332
+ }
333
+
334
+ try:
335
+ # Split into sections
336
+ sections = response.lower().split("\n\n")
337
+
338
+ for section in sections:
339
+ if "learning point" in section or "key concept" in section:
340
+ # Extract bullet points or numbered items
341
+ points = re.findall(r"[-•*]\s*(.+)$", section, re.MULTILINE)
342
+ insights["learning_points"].extend(points)
343
+
344
+ elif "gap" in section or "uncertainty" in section:
345
+ # Look for topic mentions with confidence indicators
346
+ gaps = re.findall(
347
+ r"(limited|uncertain|unclear|difficulty with)\s+([^,.]+)",
348
+ section
349
+ )
350
+ for indicator, topic in gaps:
351
+ # Estimate confidence based on language
352
+ confidence = 0.4 if "limited" in indicator else 0.6
353
+ insights["gaps"][topic.strip()] = confidence
354
+
355
+ elif "strength" in section or "demonstrated" in section:
356
+ # Extract positive mentions
357
+ strengths = re.findall(r"[-•*]\s*(.+)$", section, re.MULTILINE)
358
+ insights["strengths"].extend(strengths)
359
+
360
+ elif "objective" in section or "suggest" in section:
361
+ # Extract recommended objectives
362
+ objectives = re.findall(r"[-•*]\s*(.+)$", section, re.MULTILINE)
363
+ insights["suggested_objectives"].extend(objectives)
364
+
365
+ return insights
366
+
367
+ except Exception as e:
368
+ logger.error(f"Error parsing analysis: {str(e)}")
369
+ return insights
370
+
371
+ def _update_context_from_analysis(self, insights: Dict[str, Any]) -> None:
372
+ """
373
+ Update learning context based on discussion analysis.
374
+
375
+ Args:
376
+ insights: Dictionary of analysis insights
377
+ """
378
+ try:
379
+ # Update knowledge gaps
380
+ for topic, confidence in insights["gaps"].items():
381
+ self.learning_context.update_knowledge_gap(topic, confidence)
382
+
383
+ # Add strengths
384
+ for strength in insights["strengths"]:
385
+ self.learning_context.add_strength(strength)
386
+
387
+ # Save context if path provided
388
+ if self.context_path:
389
+ self.learning_context.save_context(self.context_path)
390
+
391
+ except Exception as e:
392
+ logger.error(f"Error updating context: {str(e)}")
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,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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', 'LearningContext']
7
+
8
+ # %% ../nbs/00_learning_context.ipynb 4
9
+ from typing import Dict, List, Optional, Any, Tuple
10
+ from datetime import datetime
11
+ import json
12
+ from pathlib import Path
13
+ from .utils import setup_logger, load_context_safely, save_context_safely
14
+
15
+ # %% ../nbs/00_learning_context.ipynb 5
16
+ logger = setup_logger(__name__)
17
+
18
+ # %% ../nbs/00_learning_context.ipynb 8
19
+ class LearningContext:
20
+ """
21
+ Manages the dynamic learning context for each student.
22
+
23
+ Tracks:
24
+ - Current rotation details
25
+ - Learning objectives and progress
26
+ - Knowledge gaps and strengths
27
+ - Custom feedback preferences
28
+ """
29
+
30
+ def __init__(self, context_file: Optional[Path] = None):
31
+ """
32
+ Initialize learning context, optionally loading from file.
33
+
34
+ Args:
35
+ context_file: Optional path to saved context
36
+ """
37
+ # Initialize current rotation
38
+ self.current_rotation = {
39
+ "specialty": "",
40
+ "start_date": None,
41
+ "end_date": None,
42
+ "key_focus_areas": []
43
+ }
44
+
45
+ # Initialize learning objectives list
46
+ self.learning_objectives = [] # [{"objective": str, "status": "active"|"completed", "added": datetime}]
47
+
48
+ # Initialize knowledge profile
49
+ self.knowledge_profile = {
50
+ "gaps": {}, # topic -> confidence level
51
+ "strengths": [],
52
+ "recent_progress": [] # [{topic, improvement, date}]
53
+ }
54
+
55
+ # Initialize feedback preferences
56
+ self.feedback_preferences = [] # [{"focus": str, "active": bool}]
57
+
58
+ if context_file and context_file.exists():
59
+ self.load_context(context_file)
60
+
61
+ logger.info("Learning context initialized")
62
+
63
+ def update_rotation(self, rotation_details: Dict[str, Any]) -> None:
64
+ """Update current rotation details"""
65
+ self.current_rotation.update(rotation_details)
66
+
67
+ def add_learning_objective(self, objective: str) -> None:
68
+ """Add new learning objective"""
69
+ self.learning_objectives.append({
70
+ "objective": objective,
71
+ "status": "active",
72
+ "added": datetime.now().isoformat()
73
+ })
74
+
75
+ def complete_objective(self, objective: str) -> None:
76
+ """Mark learning objective as completed"""
77
+ for obj in self.learning_objectives:
78
+ if obj["objective"] == objective and obj["status"] == "active":
79
+ obj["status"] = "completed"
80
+ obj["completed"] = datetime.now().isoformat()
81
+ break
82
+
83
+ def update_knowledge_gap(self, topic: str, confidence: float) -> None:
84
+ """Update knowledge gap confidence level"""
85
+ old_confidence = self.knowledge_profile["gaps"].get(topic)
86
+ self.knowledge_profile["gaps"][topic] = confidence
87
+
88
+ # Track progress if confidence improved
89
+ if old_confidence and confidence > old_confidence:
90
+ self.knowledge_profile["recent_progress"].append({
91
+ "topic": topic,
92
+ "improvement": confidence - old_confidence,
93
+ "date": datetime.now().isoformat()
94
+ })
95
+
96
+ # Keep only recent progress
97
+ self.knowledge_profile["recent_progress"] = \
98
+ self.knowledge_profile["recent_progress"][-5:]
99
+
100
+ def add_strength(self, topic: str) -> None:
101
+ """Add identified strength"""
102
+ if topic not in self.knowledge_profile["strengths"]:
103
+ self.knowledge_profile["strengths"].append(topic)
104
+
105
+ def toggle_feedback_focus(self, focus: str, active: bool) -> None:
106
+ """Toggle feedback focus area"""
107
+ # Update if exists
108
+ for pref in self.feedback_preferences:
109
+ if pref["focus"] == focus:
110
+ pref["active"] = active
111
+ return
112
+
113
+ # Add if new
114
+ self.feedback_preferences.append({
115
+ "focus": focus,
116
+ "active": active
117
+ })
118
+
119
+ def save_context(self, file_path: Path) -> None:
120
+ """Save context to file"""
121
+ context_data = {
122
+ "current_rotation": self.current_rotation,
123
+ "learning_objectives": self.learning_objectives,
124
+ "knowledge_profile": self.knowledge_profile,
125
+ "feedback_preferences": self.feedback_preferences
126
+ }
127
+ save_context_safely(context_data, file_path)
128
+
129
+ def load_context(self, file_path: Path) -> None:
130
+ """Load context from file"""
131
+ try:
132
+ context_data = load_context_safely(file_path)
133
+
134
+ # Use .get() with default values to handle missing keys
135
+ self.current_rotation = context_data.get("current_rotation", {
136
+ "specialty": "",
137
+ "start_date": None,
138
+ "end_date": None,
139
+ "key_focus_areas": []
140
+ })
141
+
142
+ # Ensure required keys exist
143
+ for key in ["specialty", "start_date", "end_date", "key_focus_areas"]:
144
+ if key not in self.current_rotation:
145
+ self.current_rotation[key] = None if key != "key_focus_areas" else []
146
+
147
+ self.learning_objectives = context_data.get("learning_objectives", [])
148
+ self.knowledge_profile = context_data.get("knowledge_profile", {
149
+ "gaps": {},
150
+ "strengths": [],
151
+ "recent_progress": []
152
+ })
153
+ self.feedback_preferences = context_data.get("feedback_preferences", [])
154
+
155
+ logger.info(f"Context loaded successfully from {file_path}")
156
+
157
+ except Exception as e:
158
+ logger.error(f"Error loading context: {str(e)}")
159
+ # Initialize with defaults on error
160
+ self.__init__()
wardbuddy/learning_interface.py ADDED
@@ -0,0 +1,780 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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_dashboard_css', 'LearningInterface', 'launch_learning_interface']
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
17
+
18
+ logger = setup_logger(__name__)
19
+
20
+ # %% ../nbs/02_learning_interface.ipynb 6
21
+ def create_dashboard_css() -> str:
22
+ """Create custom CSS for dashboard styling"""
23
+ return """
24
+ .dashboard-card {
25
+ border: 1px solid #e2e8f0;
26
+ border-radius: 8px;
27
+ padding: 16px;
28
+ margin: 8px 0;
29
+ background: white;
30
+ }
31
+
32
+ .status-active {
33
+ color: #48bb78;
34
+ font-weight: 500;
35
+ }
36
+
37
+ .status-completed {
38
+ color: #718096;
39
+ }
40
+
41
+ .dashboard-header {
42
+ font-size: 1.25rem;
43
+ font-weight: 600;
44
+ margin-bottom: 1rem;
45
+ }
46
+
47
+ .summary-modal {
48
+ max-width: 800px !important;
49
+ }
50
+
51
+ .feedback-tag {
52
+ display: inline-block;
53
+ padding: 4px 8px;
54
+ border-radius: 4px;
55
+ margin: 4px;
56
+ font-size: 0.875rem;
57
+ }
58
+
59
+ .feedback-active {
60
+ background: #ebf8ff;
61
+ color: #2b6cb0;
62
+ }
63
+
64
+ .feedback-inactive {
65
+ background: #f7fafc;
66
+ color: #718096;
67
+ }
68
+ """
69
+
70
+ # %% ../nbs/02_learning_interface.ipynb 8
71
+ class LearningInterface:
72
+ """
73
+ Gradio interface for clinical learning interactions.
74
+
75
+ Features:
76
+ - Natural case discussion chat
77
+ - Dynamic learning dashboard
78
+ - Post-discussion analysis
79
+ - Progress tracking
80
+ """
81
+
82
+ def __init__(
83
+ self,
84
+ context_path: Optional[Path] = None,
85
+ theme: str = "default"
86
+ ):
87
+ """Initialize learning interface."""
88
+ self.tutor = ClinicalTutor(context_path)
89
+ self.theme = theme
90
+ self.context_path = context_path
91
+
92
+ # Track current discussion state
93
+ self.current_discussion = {
94
+ "started": None,
95
+ "case_type": None,
96
+ "messages": []
97
+ }
98
+
99
+ logger.info("Learning interface initialized")
100
+
101
+ async def process_chat(
102
+ self,
103
+ message: str,
104
+ history: List[Dict[str, str]],
105
+ state: Dict[str, Any]
106
+ ) -> Tuple[List[Dict[str, str]], str, Dict[str, Any]]:
107
+ """
108
+ Process chat messages with state management.
109
+
110
+ Args:
111
+ message: User input message
112
+ history: Chat history
113
+ state: Current interface state
114
+
115
+ Returns:
116
+ tuple: (updated history, cleared message, updated state)
117
+ """
118
+ try:
119
+ if not message.strip():
120
+ return history, "", state
121
+
122
+ # Start new discussion if none active
123
+ if not state.get("discussion_active"):
124
+ state["discussion_active"] = True
125
+ state["discussion_start"] = datetime.now().isoformat()
126
+
127
+ # Get tutor response
128
+ response = await self.tutor.discuss_case(message)
129
+
130
+ # Update history and state
131
+ if history is None:
132
+ history = []
133
+ history.extend([
134
+ {"role": "user", "content": message},
135
+ {"role": "assistant", "content": response}
136
+ ])
137
+
138
+ state["last_message"] = datetime.now().isoformat()
139
+
140
+ return history, "", state
141
+
142
+ except Exception as e:
143
+ logger.error(f"Error in chat: {str(e)}")
144
+ return history or [], "", state
145
+
146
+ async def end_discussion(
147
+ self,
148
+ history: List[Dict[str, str]],
149
+ state: Dict[str, Any]
150
+ ) -> Tuple[Dict[str, Any], Dict[str, Any]]:
151
+ """
152
+ Analyze completed discussion and prepare summary.
153
+
154
+ Args:
155
+ history: Chat history
156
+ state: Current interface state
157
+
158
+ Returns:
159
+ tuple: (analysis results, updated state)
160
+ """
161
+ try:
162
+ if not history:
163
+ return {
164
+ "learning_points": [],
165
+ "gaps": {},
166
+ "strengths": [],
167
+ "suggested_objectives": []
168
+ }, state
169
+
170
+ # Get analysis
171
+ analysis = await self.tutor.analyze_discussion(history)
172
+
173
+ # Reset discussion state
174
+ state["discussion_active"] = False
175
+ state["discussion_start"] = None
176
+ state["last_message"] = None
177
+
178
+ return analysis, state
179
+
180
+ except Exception as e:
181
+ logger.error(f"Error analyzing discussion: {str(e)}")
182
+ return {
183
+ "learning_points": [],
184
+ "gaps": {},
185
+ "strengths": [],
186
+ "suggested_objectives": []
187
+ }, state
188
+
189
+ def update_rotation(
190
+ self,
191
+ specialty: str,
192
+ start_date: str,
193
+ end_date: str,
194
+ focus_areas: str
195
+ ) -> Tuple[str, str, str, str]:
196
+ """
197
+ Update rotation details and return updated values.
198
+
199
+ Args:
200
+ specialty: Rotation specialty
201
+ start_date: Start date string
202
+ end_date: End date string
203
+ focus_areas: Comma-separated focus areas
204
+
205
+ Returns:
206
+ tuple: Updated field values
207
+ """
208
+ try:
209
+ # Parse focus areas
210
+ focus_list = [
211
+ area.strip()
212
+ for area in focus_areas.split(",")
213
+ if area.strip()
214
+ ]
215
+
216
+ # Update context
217
+ rotation = {
218
+ "specialty": specialty,
219
+ "start_date": start_date,
220
+ "end_date": end_date,
221
+ "key_focus_areas": focus_list
222
+ }
223
+ self.tutor.learning_context.update_rotation(rotation)
224
+
225
+ # Return updated values
226
+ return (
227
+ specialty,
228
+ start_date,
229
+ end_date,
230
+ ",".join(focus_list)
231
+ )
232
+
233
+ except Exception as e:
234
+ logger.error(f"Error updating rotation: {str(e)}")
235
+ current = self.tutor.learning_context.current_rotation
236
+ return (
237
+ current["specialty"],
238
+ current["start_date"] or "",
239
+ current["end_date"] or "",
240
+ ",".join(current["key_focus_areas"])
241
+ )
242
+
243
+ def add_objective(
244
+ self,
245
+ objective: str,
246
+ objectives_df: pd.DataFrame
247
+ ) -> pd.DataFrame:
248
+ """
249
+ Add new learning objective and return updated dataframe.
250
+
251
+ Args:
252
+ objective: New objective text
253
+ objectives_df: Current objectives dataframe
254
+
255
+ Returns:
256
+ pd.DataFrame: Updated objectives list
257
+ """
258
+ try:
259
+ if not objective.strip():
260
+ return objectives_df
261
+
262
+ # Add to context
263
+ self.tutor.learning_context.add_learning_objective(objective)
264
+
265
+ # Convert to dataframe
266
+ return pd.DataFrame([
267
+ [obj["objective"], obj["status"], obj["added"]]
268
+ for obj in self.tutor.learning_context.learning_objectives
269
+ ], columns=["Objective", "Status", "Date Added"])
270
+
271
+ except Exception as e:
272
+ logger.error(f"Error adding objective: {str(e)}")
273
+ return objectives_df
274
+
275
+ def toggle_objective_status(
276
+ self,
277
+ evt: gr.SelectData, # Updated to use gr.SelectData
278
+ objectives_df: pd.DataFrame
279
+ ) -> pd.DataFrame:
280
+ """
281
+ Toggle objective status between active and completed.
282
+
283
+ Args:
284
+ evt: Gradio select event containing row index
285
+ objectives_df: Current objectives dataframe
286
+
287
+ Returns:
288
+ pd.DataFrame: Updated objectives list
289
+ """
290
+ try:
291
+ objective_idx = evt.index[0] # Get selected row index
292
+ if objective_idx >= len(objectives_df):
293
+ return objectives_df
294
+
295
+ # Get objective
296
+ objective = objectives_df.iloc[objective_idx]["Objective"]
297
+ current_status = objectives_df.iloc[objective_idx]["Status"]
298
+
299
+ # Toggle in context
300
+ if current_status == "active":
301
+ self.tutor.learning_context.complete_objective(objective)
302
+ else:
303
+ self.tutor.learning_context.add_learning_objective(objective)
304
+
305
+ # Update dataframe
306
+ return pd.DataFrame([
307
+ [obj["objective"], obj["status"], obj["added"]]
308
+ for obj in self.tutor.learning_context.learning_objectives
309
+ ], columns=["Objective", "Status", "Date Added"])
310
+
311
+ except Exception as e:
312
+ logger.error(f"Error toggling objective: {str(e)}")
313
+ return objectives_df
314
+
315
+ def add_feedback_focus(
316
+ self,
317
+ focus: str,
318
+ feedback_df: pd.DataFrame
319
+ ) -> pd.DataFrame:
320
+ """Add new feedback focus area."""
321
+ try:
322
+ if not focus.strip():
323
+ return feedback_df
324
+
325
+ # Add to context
326
+ self.tutor.learning_context.toggle_feedback_focus(focus, True)
327
+
328
+ # Update dataframe
329
+ return pd.DataFrame([
330
+ [pref["focus"], pref["active"]]
331
+ for pref in self.tutor.learning_context.feedback_preferences
332
+ ], columns=["Focus Area", "Active"])
333
+
334
+ except Exception as e:
335
+ logger.error(f"Error adding feedback focus: {str(e)}")
336
+ return feedback_df
337
+
338
+ def toggle_feedback_status(
339
+ self,
340
+ evt: gr.SelectData, # Updated to use gr.SelectData
341
+ feedback_df: pd.DataFrame
342
+ ) -> pd.DataFrame:
343
+ """Toggle feedback focus active status."""
344
+ try:
345
+ focus_idx = evt.index[0] # Get selected row index
346
+ if focus_idx >= len(feedback_df):
347
+ return feedback_df
348
+
349
+ # Get focus area
350
+ focus = feedback_df.iloc[focus_idx]["Focus Area"]
351
+ current_status = feedback_df.iloc[focus_idx]["Active"]
352
+
353
+ # Toggle in context
354
+ self.tutor.learning_context.toggle_feedback_focus(
355
+ focus,
356
+ not current_status
357
+ )
358
+
359
+ # Update dataframe
360
+ return pd.DataFrame([
361
+ [pref["focus"], pref["active"]]
362
+ for pref in self.tutor.learning_context.feedback_preferences
363
+ ], columns=["Focus Area", "Active"])
364
+
365
+ except Exception as e:
366
+ logger.error(f"Error toggling feedback: {str(e)}")
367
+ return feedback_df
368
+
369
+ def create_interface(self) -> gr.Blocks:
370
+ """Create and configure the Gradio interface"""
371
+ with gr.Blocks(
372
+ title="Clinical Learning Assistant",
373
+ theme=self.theme,
374
+ css=create_dashboard_css()
375
+ ) as interface:
376
+ # State management
377
+ state = gr.State({
378
+ "discussion_active": False,
379
+ "discussion_start": None,
380
+ "last_message": None
381
+ })
382
+
383
+ # Header
384
+ with gr.Row():
385
+ gr.Markdown(
386
+ "# Clinical Learning Assistant",
387
+ elem_classes=["dashboard-header"]
388
+ )
389
+
390
+ with gr.Row():
391
+ # Left column - Chat interface
392
+ with gr.Column(scale=2):
393
+ # Active discussion indicator
394
+ discussion_status = gr.Markdown(
395
+ "Start a new case discussion",
396
+ elem_classes=["dashboard-card"]
397
+ )
398
+
399
+ # Chat interface
400
+ chatbot = gr.Chatbot(
401
+ height=500,
402
+ label="Case Discussion",
403
+ show_label=True,
404
+ elem_classes=["dashboard-card"]
405
+ )
406
+
407
+ with gr.Row():
408
+ msg = gr.Textbox(
409
+ label="Present your case or ask questions",
410
+ placeholder=(
411
+ "Present your case as you would to your supervisor:\n"
412
+ "- Start with the chief complaint\n"
413
+ "- Include relevant history and findings\n"
414
+ "- Share your assessment and plan"
415
+ ),
416
+ lines=5
417
+ )
418
+
419
+ with gr.Row():
420
+ clear = gr.Button("Clear Discussion")
421
+ end_discussion = gr.Button(
422
+ "End Discussion & Review",
423
+ variant="primary"
424
+ )
425
+
426
+ # Right column - Learning dashboard
427
+ with gr.Column(scale=1):
428
+ with gr.Tabs():
429
+ # Current Rotation tab
430
+ with gr.Tab("Current Rotation"):
431
+ with gr.Column(elem_classes=["dashboard-card"]):
432
+ specialty = gr.Textbox(
433
+ label="Specialty",
434
+ value=self.tutor.learning_context.current_rotation["specialty"]
435
+ )
436
+ start_date = gr.Textbox(
437
+ label="Start Date (YYYY-MM-DD)",
438
+ value=self.tutor.learning_context.current_rotation["start_date"]
439
+ )
440
+ end_date = gr.Textbox(
441
+ label="End Date (YYYY-MM-DD)",
442
+ value=self.tutor.learning_context.current_rotation["end_date"]
443
+ )
444
+ focus_areas = gr.Textbox(
445
+ label="Key Focus Areas (comma-separated)",
446
+ value=",".join(
447
+ self.tutor.learning_context.current_rotation["key_focus_areas"]
448
+ )
449
+ )
450
+ update_rotation_btn = gr.Button(
451
+ "Update Rotation",
452
+ variant="secondary"
453
+ )
454
+
455
+ # Learning Objectives tab
456
+ with gr.Tab("Learning Objectives"):
457
+ with gr.Column(elem_classes=["dashboard-card"]):
458
+ objectives_df = gr.DataFrame(
459
+ headers=["Objective", "Status", "Date Added"],
460
+ value=[[
461
+ obj["objective"],
462
+ obj["status"],
463
+ obj["added"]
464
+ ] for obj in self.tutor.learning_context.learning_objectives],
465
+ interactive=True,
466
+ wrap=True
467
+ )
468
+
469
+ with gr.Row():
470
+ new_objective = gr.Textbox(
471
+ label="New Learning Objective",
472
+ placeholder="Enter objective..."
473
+ )
474
+ add_objective_btn = gr.Button(
475
+ "Add",
476
+ variant="secondary"
477
+ )
478
+
479
+ # Feedback Preferences tab
480
+ with gr.Tab("Feedback Focus"):
481
+ with gr.Column(elem_classes=["dashboard-card"]):
482
+ feedback_df = gr.DataFrame(
483
+ headers=["Focus Area", "Active"],
484
+ value=[[
485
+ pref["focus"],
486
+ pref["active"]
487
+ ] for pref in self.tutor.learning_context.feedback_preferences],
488
+ interactive=True,
489
+ wrap=True
490
+ )
491
+
492
+ with gr.Row():
493
+ new_feedback = gr.Textbox(
494
+ label="New Feedback Focus",
495
+ placeholder="Enter focus area..."
496
+ )
497
+ add_feedback_btn = gr.Button(
498
+ "Add",
499
+ variant="secondary"
500
+ )
501
+
502
+ # Knowledge Profile tab
503
+ with gr.Tab("Knowledge Profile"):
504
+ with gr.Column(elem_classes=["dashboard-card"]):
505
+ # Knowledge Gaps
506
+ gr.Markdown("### Knowledge Gaps")
507
+ gaps_display = gr.DataFrame(
508
+ headers=["Topic", "Confidence"],
509
+ value=[[
510
+ topic, confidence
511
+ ] for topic, confidence in
512
+ self.tutor.learning_context.knowledge_profile["gaps"].items()
513
+ ],
514
+ interactive=False
515
+ )
516
+
517
+ # Strengths Display
518
+ gr.Markdown("### Strengths")
519
+ strengths_display = gr.DataFrame(
520
+ headers=["Area"],
521
+ value=[[strength] for strength in
522
+ self.tutor.learning_context.knowledge_profile["strengths"]
523
+ ],
524
+ interactive=False
525
+ )
526
+
527
+ # Recent Progress
528
+ gr.Markdown("### Recent Progress")
529
+ progress_display = gr.DataFrame(
530
+ headers=["Topic", "Improvement", "Date"],
531
+ value=[[
532
+ prog["topic"],
533
+ f"{prog['improvement']:.2f}",
534
+ prog["date"]
535
+ ] for prog in
536
+ self.tutor.learning_context.knowledge_profile["recent_progress"]
537
+ ],
538
+ interactive=False
539
+ )
540
+
541
+ # Discussion summary section
542
+ summary_section = gr.Column(visible=False)
543
+ with summary_section:
544
+ gr.Markdown("## Discussion Summary")
545
+
546
+ # Overview section
547
+ with gr.Row():
548
+ with gr.Column():
549
+ gr.Markdown("### Session Overview")
550
+ session_overview = gr.JSON(
551
+ label="Discussion Details",
552
+ value={
553
+ "duration": "0 minutes",
554
+ "messages": 0,
555
+ "topics_covered": []
556
+ }
557
+ )
558
+
559
+ # Learning Points and Gaps
560
+ with gr.Row():
561
+ with gr.Column():
562
+ gr.Markdown("### Key Learning Points")
563
+ learning_points = gr.JSON(label="Points to Remember")
564
+
565
+ with gr.Column():
566
+ gr.Markdown("### Knowledge Profile Updates")
567
+ with gr.Row():
568
+ gaps = gr.JSON(label="Areas for Improvement")
569
+ strengths = gr.JSON(label="Demonstrated Strengths")
570
+
571
+ # Future Learning section
572
+ gr.Markdown("### Planning Ahead")
573
+ with gr.Row():
574
+ with gr.Column():
575
+ gr.Markdown("#### Suggested Learning Objectives")
576
+ objectives = gr.JSON(label="Consider Adding")
577
+
578
+ with gr.Column():
579
+ gr.Markdown("#### Recommended Focus Areas")
580
+ recommendations = gr.JSON(label="Next Steps")
581
+
582
+ # Action buttons
583
+ with gr.Row():
584
+ add_selected_objectives = gr.Button(
585
+ "Add Selected Objectives",
586
+ variant="primary"
587
+ )
588
+ close_summary = gr.Button("Close Summary")
589
+
590
+ # Event handlers
591
+ msg.submit(
592
+ self.process_chat,
593
+ inputs=[msg, chatbot, state],
594
+ outputs=[chatbot, msg, state]
595
+ ).then(
596
+ self._update_discussion_status,
597
+ inputs=[state],
598
+ outputs=[discussion_status]
599
+ )
600
+
601
+ clear.click(
602
+ lambda: ([], "", {
603
+ "discussion_active": False,
604
+ "discussion_start": None,
605
+ "last_message": None
606
+ }),
607
+ outputs=[chatbot, msg, state]
608
+ ).then(
609
+ lambda: "Start a new case discussion",
610
+ outputs=[discussion_status]
611
+ )
612
+
613
+ end_discussion.click(
614
+ self.end_discussion,
615
+ inputs=[chatbot, state],
616
+ outputs=[
617
+ session_overview,
618
+ learning_points,
619
+ gaps,
620
+ strengths,
621
+ objectives,
622
+ recommendations
623
+ ]
624
+ ).then(
625
+ lambda: gr.update(visible=True),
626
+ None,
627
+ summary_section
628
+ ).then(
629
+ self._refresh_knowledge_profile,
630
+ outputs=[gaps_display, strengths_display, progress_display]
631
+ )
632
+
633
+ close_summary.click(
634
+ lambda: gr.update(visible=False),
635
+ None,
636
+ summary_section
637
+ )
638
+
639
+ # Rotation management
640
+ update_rotation_btn.click(
641
+ self.update_rotation,
642
+ inputs=[specialty, start_date, end_date, focus_areas],
643
+ outputs=[specialty, start_date, end_date, focus_areas]
644
+ )
645
+
646
+ # Learning objectives management
647
+ add_objective_btn.click(
648
+ self.add_objective,
649
+ inputs=[new_objective, objectives_df],
650
+ outputs=[objectives_df]
651
+ ).then(
652
+ lambda: "",
653
+ None,
654
+ new_objective
655
+ )
656
+
657
+ objectives_df.select(
658
+ self.toggle_objective_status,
659
+ inputs=[objectives_df],
660
+ outputs=[objectives_df]
661
+ )
662
+
663
+ # Feedback preferences management
664
+ add_feedback_btn.click(
665
+ self.add_feedback_focus,
666
+ inputs=[new_feedback, feedback_df],
667
+ outputs=[feedback_df]
668
+ ).then(
669
+ lambda: "",
670
+ None,
671
+ new_feedback
672
+ )
673
+
674
+ feedback_df.select(
675
+ self.toggle_feedback_status,
676
+ inputs=[feedback_df],
677
+ outputs=[feedback_df]
678
+ )
679
+
680
+ # Add selected objectives from summary
681
+ add_selected_objectives.click(
682
+ self._add_suggested_objectives,
683
+ inputs=[objectives],
684
+ outputs=[objectives_df]
685
+ )
686
+
687
+ return interface
688
+
689
+ def _update_discussion_status(self, state: Dict[str, Any]) -> str:
690
+ """Update discussion status display"""
691
+ try:
692
+ if not state.get("discussion_active"):
693
+ return "Start a new case discussion"
694
+
695
+ start = datetime.fromisoformat(state["discussion_start"])
696
+ duration = datetime.now() - start
697
+ minutes = int(duration.total_seconds() / 60)
698
+
699
+ return f"Active discussion ({minutes} minutes)"
700
+
701
+ except Exception as e:
702
+ logger.error(f"Error updating status: {str(e)}")
703
+ return "Discussion status unknown"
704
+
705
+ def _refresh_knowledge_profile(
706
+ self
707
+ ) -> Tuple[List[List[str]], List[List[str]], List[List[str]]]:
708
+ """Refresh knowledge profile displays"""
709
+ try:
710
+ # Gaps
711
+ gaps_data = [[
712
+ topic, f"{confidence:.2f}"
713
+ ] for topic, confidence in
714
+ self.tutor.learning_context.knowledge_profile["gaps"].items()
715
+ ]
716
+
717
+ # Strengths
718
+ strengths_data = [[
719
+ strength
720
+ ] for strength in
721
+ self.tutor.learning_context.knowledge_profile["strengths"]
722
+ ]
723
+
724
+ # Progress
725
+ progress_data = [[
726
+ prog["topic"],
727
+ f"{prog['improvement']:.2f}",
728
+ prog["date"]
729
+ ] for prog in
730
+ self.tutor.learning_context.knowledge_profile["recent_progress"]
731
+ ]
732
+
733
+ return gaps_data, strengths_data, progress_data
734
+
735
+ except Exception as e:
736
+ logger.error(f"Error refreshing profile: {str(e)}")
737
+ return [], [], []
738
+
739
+ def _add_suggested_objectives(
740
+ self,
741
+ evt: gr.SelectData, # Updated to use gr.SelectData
742
+ suggested_objectives: List[str]
743
+ ) -> pd.DataFrame:
744
+ """Add selected suggested objectives to learning objectives"""
745
+ try:
746
+ selected_indices = [evt.index[0]] # Get selected row index
747
+
748
+ for idx in selected_indices:
749
+ if idx < len(suggested_objectives):
750
+ objective = suggested_objectives[idx]
751
+ self.tutor.learning_context.add_learning_objective(objective)
752
+
753
+ return pd.DataFrame([
754
+ [obj["objective"], obj["status"], obj["added"]]
755
+ for obj in self.tutor.learning_context.learning_objectives
756
+ ], columns=["Objective", "Status", "Date Added"])
757
+
758
+ except Exception as e:
759
+ logger.error(f"Error adding objectives: {str(e)}")
760
+ return pd.DataFrame()
761
+
762
+ # %% ../nbs/02_learning_interface.ipynb 10
763
+ async def launch_learning_interface(
764
+ port: Optional[int] = None,
765
+ context_path: Optional[Path] = None,
766
+ share: bool = False,
767
+ theme: str = "default"
768
+ ) -> None:
769
+ """Launch the learning interface application."""
770
+ try:
771
+ interface = LearningInterface(context_path, theme)
772
+ app = interface.create_interface()
773
+ app.launch(
774
+ server_port=port,
775
+ share=share
776
+ )
777
+ logger.info(f"Interface launched on port: {port}")
778
+ except Exception as e:
779
+ logger.error(f"Error launching interface: {str(e)}")
780
+ raise
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)}")