From a701fdde7ba258fbcb1a7c104a45d84aba1ecb33 Mon Sep 17 00:00:00 2001
From: alhendrickson <159636032+alhendrickson@users.noreply.github.com.>
Date: Wed, 13 Aug 2025 14:43:53 +0000
Subject: [PATCH 1/2] chore(medcat-trainer): CU-869a4br6j Create a copy of the
v1 medcat-trainer in the /v1 folder
---
v1/medcat-trainer/.env-example | 6 +
v1/medcat-trainer/.gitignore | 51 +
v1/medcat-trainer/.readthedocs.yaml | 17 +
v1/medcat-trainer/.vscode/launch.json | 91 +
v1/medcat-trainer/CODE_OF_CONDUCT.md | 128 +
v1/medcat-trainer/LICENSE | 202 +
v1/medcat-trainer/README.md | 18 +
v1/medcat-trainer/client/README.md | 88 +
v1/medcat-trainer/client/__init__.py | 0
v1/medcat-trainer/client/mctclient.py | 547 ++
v1/medcat-trainer/client/pyproject.toml | 18 +
.../client/tests/test_mctclient.py | 119 +
v1/medcat-trainer/configs/base.txt | 14 +
v1/medcat-trainer/docker-compose-dev.yml | 61 +
v1/medcat-trainer/docker-compose-mc0x.yml | 35 +
v1/medcat-trainer/docker-compose-prod.yml | 57 +
v1/medcat-trainer/docker-compose.yml | 56 +
v1/medcat-trainer/docs/Makefile | 20 +
.../docs/_static/css/overrides.css | 24 +
.../img/add-annotation-concept-pick.png | Bin 0 -> 10221 bytes
.../_static/img/add-annotation-concept.png | Bin 0 -> 32984 bytes
.../docs/_static/img/add-annotation-menu.png | Bin 0 -> 6698 bytes
.../img/add-annotation-select-concept.png | Bin 0 -> 35589 bytes
.../docs/_static/img/add-annotation-text.png | Bin 0 -> 4173 bytes
.../docs/_static/img/add-new-meta-task.png | Bin 0 -> 7556 bytes
.../docs/_static/img/add-new-users.png | Bin 0 -> 62344 bytes
.../img/add_project_annotate_entities.png | Bin 0 -> 60800 bytes
.../docs/_static/img/admin_page.png | Bin 0 -> 71979 bytes
.../docs/_static/img/available-projects.png | Bin 0 -> 33240 bytes
.../docs/_static/img/cat-logo.png | Bin 0 -> 105558 bytes
.../docs/_static/img/cat-logo.svg | 73 +
.../docs/_static/img/clone-projects.png | Bin 0 -> 100038 bytes
.../_static/img/concepts-imported-status.png | Bin 0 -> 49788 bytes
.../_static/img/delete-indexed-concepts.png | Bin 0 -> 37645 bytes
.../docs/_static/img/demo_interface.png | Bin 0 -> 415013 bytes
.../docs/_static/img/demo_tab.png | Bin 0 -> 75272 bytes
.../docs/_static/img/download-annos.png | Bin 0 -> 100836 bytes
.../docs/_static/img/help-button.png | Bin 0 -> 1539 bytes
.../_static/img/home-screen-admin-options.png | Bin 0 -> 10941 bytes
.../docs/_static/img/import-concepts.png | Bin 0 -> 97005 bytes
v1/medcat-trainer/docs/_static/img/login.png | Bin 0 -> 10833 bytes
.../_static/img/main-annotation-interface.png | Bin 0 -> 192062 bytes
.../docs/_static/img/meta-task-form.png | Bin 0 -> 44346 bytes
.../docs/_static/img/meta-tasks-interface.png | Bin 0 -> 283188 bytes
.../docs/_static/img/new_projects.png | Bin 0 -> 92896 bytes
.../_static/img/pick-alternative-concept.png | Bin 0 -> 131193 bytes
.../img/project-groups-view-available.png | Bin 0 -> 322000 bytes
.../docs/_static/img/project-groups-view.png | Bin 0 -> 165723 bytes
.../_static/img/project_annotate_entities.png | Bin 0 -> 73753 bytes
.../docs/_static/img/projects-in-group.png | Bin 0 -> 312811 bytes
.../docs/_static/img/remove-default-user.png | Bin 0 -> 30582 bytes
.../docs/_static/img/reset-button.png | Bin 0 -> 1592 bytes
.../docs/_static/img/reset-projects.png | Bin 0 -> 99127 bytes
.../docs/_static/img/save_cdb.png | Bin 0 -> 75937 bytes
.../docs/_static/img/select-concept-dbs.png | Bin 0 -> 46261 bytes
.../_static/img/select-existing-project.png | Bin 0 -> 45413 bytes
.../docs/_static/img/select-tasks.png | Bin 0 -> 19276 bytes
.../docs/_static/img/summary-button.png | Bin 0 -> 1401 bytes
.../docs/_static/img/tick_mark.png | Bin 0 -> 1239 bytes
.../docs/_static/img/users-select.png | Bin 0 -> 33183 bytes
v1/medcat-trainer/docs/admin_setup.md | 25 +
v1/medcat-trainer/docs/advanced_usage.md | 3 +
v1/medcat-trainer/docs/annotator_guide.md | 72 +
v1/medcat-trainer/docs/client.md | 88 +
v1/medcat-trainer/docs/conf.py | 88 +
v1/medcat-trainer/docs/demo_page.md | 23 +
v1/medcat-trainer/docs/index.rst | 31 +
v1/medcat-trainer/docs/installation.md | 74 +
v1/medcat-trainer/docs/main.md | 7 +
v1/medcat-trainer/docs/maintenance.md | 61 +
v1/medcat-trainer/docs/make.bat | 35 +
v1/medcat-trainer/docs/meta_annotations.md | 42 +
v1/medcat-trainer/docs/project_admin.md | 205 +
v1/medcat-trainer/docs/project_group_admin.md | 50 +
v1/medcat-trainer/docs/requirements.txt | 4 +
v1/medcat-trainer/envs/.keep | 0
v1/medcat-trainer/envs/env | 46 +
v1/medcat-trainer/envs/env-mc0x | 27 +
v1/medcat-trainer/envs/env-prod | 47 +
v1/medcat-trainer/install.sh | 31 +
v1/medcat-trainer/nginx/nginx.conf | 135 +
.../nginx/sites-enabled/medcattrainer | 30 +
.../notebook_docs/API_Examples.ipynb | 573 ++
.../notebook_docs/Client_API_Tutorials.ipynb | 485 ++
.../notebook_docs/Generate_CUI_Filters.ipynb | 267 +
.../Processing_Annotations.ipynb | 808 ++
.../notebook_docs/Train_MedCAT_Models.ipynb | 1724 ++++
..._Export_With_Text_2020-05-22_10_34_09.json | 1 +
.../notebook_docs/example_data/README.md | 4 +
.../notebook_docs/example_data/cardio.csv | 21 +
.../notebook_docs/example_data/neuro.csv | 21 +
.../notebook_docs/example_data/ortho.csv | 21 +
.../notebook_docs/example_data/psych.csv | 95 +
v1/medcat-trainer/supervisord.conf | 29 +
v1/medcat-trainer/webapp/Dockerfile | 44 +
v1/medcat-trainer/webapp/api/api/__init__.py | 2 +
.../webapp/api/api/admin/__init__.py | 21 +
.../webapp/api/api/admin/actions.py | 393 +
.../webapp/api/api/admin/models.py | 214 +
v1/medcat-trainer/webapp/api/api/apps.py | 35 +
.../webapp/api/api/data_utils.py | 209 +
.../api/api/fixtures/text_class_values.json | 23 +
.../webapp/api/api/medcat_utils.py | 68 +
v1/medcat-trainer/webapp/api/api/metrics.py | 441 +
.../webapp/api/api/migrations/0001_initial.py | 198 +
.../api/migrations/0002_auto_20190923_2342.py | 33 +
.../api/migrations/0003_auto_20190924_1934.py | 22 +
.../api/migrations/0004_auto_20190925_1450.py | 18 +
.../0005_annotatedentity_alternative.py | 18 +
.../api/api/migrations/0006_concept_icd10.py | 18 +
.../api/api/migrations/0007_concept_cdb.py | 19 +
.../0008_annotatedentity_manually_created.py | 18 +
.../migrations/0009_document_last_modified.py | 18 +
.../0010_annotatedentity_last_modified.py | 18 +
...ojectannotateentities_cdb_search_filter.py | 18 +
.../0012_conceptdb_use_for_training.py | 18 +
.../api/migrations/0013_auto_20191001_2248.py | 18 +
.../api/api/migrations/0014_conceptdb_name.py | 18 +
.../api/migrations/0015_auto_20191002_1615.py | 28 +
.../api/migrations/0016_auto_20191002_2306.py | 33 +
.../api/api/migrations/0017_delete_link.py | 16 +
.../migrations/0018_annotatedentity_killed.py | 18 +
.../0019_annotatedentity_correct.py | 18 +
...tannotateentities_train_model_on_submit.py | 18 +
.../api/migrations/0021_auto_20200124_1108.py | 18 +
.../api/migrations/0022_metatask_default.py | 19 +
.../api/api/migrations/0023_concept_opcs4.py | 18 +
.../api/migrations/0024_auto_20200131_2339.py | 47 +
.../api/migrations/0025_auto_20200131_2353.py | 24 +
.../api/migrations/0026_auto_20200204_1517.py | 19 +
.../api/migrations/0027_auto_20200208_1443.py | 32 +
.../api/migrations/0028_auto_20200208_1559.py | 18 +
.../api/migrations/0029_auto_20200208_1609.py | 32 +
.../api/migrations/0030_auto_20200208_1858.py | 32 +
.../0031_annotatedentity_create_time.py | 19 +
.../api/migrations/0032_auto_20200212_0017.py | 19 +
.../api/migrations/0033_auto_20200212_0021.py | 19 +
.../api/migrations/0034_auto_20200212_0022.py | 18 +
...rojectannotateentities_add_new_entities.py | 18 +
.../api/migrations/0036_auto_20200320_0105.py | 19 +
.../api/migrations/0037_auto_20200320_0107.py | 19 +
.../api/migrations/0038_auto_20200407_1418.py | 18 +
.../api/migrations/0039_project_cuis_file.py | 18 +
...nnotateentities_restrict_concept_lookup.py | 18 +
.../api/migrations/0041_auto_20200519_1544.py | 18 +
.../api/migrations/0042_auto_20200520_0945.py | 38 +
.../api/migrations/0043_auto_20200520_1018.py | 18 +
...ectannotateentities_terminate_available.py | 18 +
.../api/migrations/0045_auto_20200603_0934.py | 18 +
.../api/migrations/0046_auto_20200802_1006.py | 33 +
.../api/migrations/0047_auto_20210112_0355.py | 23 +
.../api/migrations/0048_projectcuicounter.py | 23 +
.../api/migrations/0049_auto_20210202_1539.py | 23 +
.../migrations/0050_remove_project_tuis.py | 17 +
.../api/migrations/0051_auto_20210211_1812.py | 19 +
.../api/migrations/0052_auto_20210216_1130.py | 18 +
...nnotateentities_clinical_coding_project.py | 17 +
.../migrations/0054_remove_concept_vocab.py | 17 +
.../api/migrations/0055_auto_20210417_0902.py | 18 +
.../api/migrations/0056_auto_20210417_0926.py | 18 +
.../api/migrations/0057_auto_20210705_0954.py | 43 +
.../api/migrations/0058_auto_20210921_0022.py | 23 +
.../api/migrations/0059_auto_20211008_2038.py | 29 +
.../api/migrations/0060_auto_20211022_0940.py | 18 +
.../0061_project_annotation_guideline_link.py | 18 +
.../api/migrations/0062_auto_20220426_1558.py | 18 +
.../api/migrations/0063_auto_20220606_1107.py | 19 +
.../api/migrations/0064_auto_20220606_1253.py | 19 +
.../api/migrations/0065_auto_20221129_1102.py | 18 +
.../api/migrations/0066_exportedproject.py | 20 +
.../api/api/migrations/0067_delete_concept.py | 16 +
.../api/migrations/0068_auto_20230203_1715.py | 18 +
.../api/migrations/0069_auto_20230711_0938.py | 28 +
.../api/migrations/0070_auto_20230711_0951.py | 18 +
.../api/migrations/0071_auto_20230711_1654.py | 18 +
.../0072_delete_projectcuicounter.py | 16 +
.../api/migrations/0073_auto_20231022_0028.py | 26 +
.../api/migrations/0074_auto_20240215_1024.py | 36 +
.../api/migrations/0075_auto_20240429_1350.py | 23 +
.../api/migrations/0076_auto_20240510_1451.py | 115 +
...projectgroup_create_associated_projects.py | 18 +
.../0078_alter_project_polymorphic_ctype.py | 20 +
.../migrations/0078_metacatmodel_modelpack.py | 34 +
.../migrations/0079_merge_20240701_2259.py | 14 +
...ns_alter_metataskvalue_options_and_more.py | 31 +
.../migrations/0081_alter_metatask_name.py | 18 +
..._remove_metacatmodel_meta_task_and_more.py | 98 +
...083_project_prepared_documents_and_more.py | 23 +
...e_time_conceptdb_last_modified_and_more.py | 33 +
...ceptdb_name_alter_dataset_name_and_more.py | 49 +
.../api/migrations/0086_vocabulary_name.py | 18 +
...e_time_modelpack_last_modified_and_more.py | 55 +
.../migrations/0088_alter_conceptdb_name.py | 19 +
...entities_deid_model_annotation_and_more.py | 23 +
.../webapp/api/api/migrations/__init__.py | 0
.../webapp/api/api/model_cache.py | 157 +
v1/medcat-trainer/webapp/api/api/models.py | 556 ++
.../webapp/api/api/permissions.py | 11 +
.../webapp/api/api/serializers.py | 133 +
v1/medcat-trainer/webapp/api/api/signals.py | 85 +
.../webapp/api/api/solr_utils.py | 214 +
v1/medcat-trainer/webapp/api/api/tests.py | 3 +
v1/medcat-trainer/webapp/api/api/urls.py | 5 +
v1/medcat-trainer/webapp/api/api/utils.py | 293 +
v1/medcat-trainer/webapp/api/api/views.py | 945 +++
v1/medcat-trainer/webapp/api/core/__init__.py | 0
v1/medcat-trainer/webapp/api/core/settings.py | 218 +
v1/medcat-trainer/webapp/api/core/urls.py | 67 +
v1/medcat-trainer/webapp/api/core/wsgi.py | 16 +
v1/medcat-trainer/webapp/api/db-backup/.keep | 0
v1/medcat-trainer/webapp/api/db/.keep | 0
v1/medcat-trainer/webapp/api/manage.py | 21 +
v1/medcat-trainer/webapp/api/media/.keep | 0
v1/medcat-trainer/webapp/api/static/.keep | 0
.../webapp/frontend/.editorconfig | 6 +
v1/medcat-trainer/webapp/frontend/.gitignore | 30 +
.../webapp/frontend/.prettierrc.json | 7 +
.../webapp/frontend/.vscode/extensions.json | 9 +
v1/medcat-trainer/webapp/frontend/README.md | 45 +
v1/medcat-trainer/webapp/frontend/env.d.ts | 1 +
.../webapp/frontend/eslint.config.js | 25 +
v1/medcat-trainer/webapp/frontend/index.html | 16 +
.../webapp/frontend/package-lock.json | 7165 +++++++++++++++++
.../webapp/frontend/package.json | 60 +
.../webapp/frontend/public/favicon.ico | Bin 0 -> 4286 bytes
v1/medcat-trainer/webapp/frontend/src/App.vue | 149 +
.../webapp/frontend/src/assets/base.css | 97 +
.../webapp/frontend/src/assets/cat-logo.png | Bin 0 -> 105558 bytes
.../webapp/frontend/src/assets/main.css | 32 +
.../src/components/anns/AddAnnotation.vue | 245 +
.../src/components/anns/AnnoResult.vue | 67 +
.../frontend/src/components/anns/TaskBar.vue | 147 +
.../src/components/common/AddNewConcept.vue | 171 +
.../components/common/AnnotationSummary.vue | 73 +
.../src/components/common/ClinicalText.vue | 283 +
.../src/components/common/ConceptFilter.vue | 176 +
.../src/components/common/ConceptPicker.vue | 135 +
.../src/components/common/ConceptSummary.vue | 276 +
.../src/components/common/DocumentSummary.vue | 247 +
.../frontend/src/components/common/Login.vue | 114 +
.../frontend/src/components/common/Modal.vue | 132 +
.../frontend/src/components/common/NavBar.vue | 74 +
.../src/components/common/ProjectList.vue | 580 ++
.../components/metrics/AnnotationsTable.vue | 62 +
.../src/components/metrics/ConceptSummary.vue | 331 +
.../src/components/metrics/MetricCell.vue | 56 +
.../components/models/ConceptDatabaseViz.vue | 179 +
.../src/components/models/VueTree.vue | 576 ++
.../src/components/usecases/HelpContent.vue | 29 +
.../usecases/MetaAnnotationTask.vue | 88 +
.../usecases/MetaAnnotationTaskContainer.vue | 97 +
.../usecases/RelationAnnotation.vue | 144 +
.../RelationAnnotationTaskContainer.vue | 144 +
.../webapp/frontend/src/event-bus.ts | 8 +
v1/medcat-trainer/webapp/frontend/src/main.ts | 69 +
.../src/mixins/ConceptDetailService.js | 72 +
.../src/mixins/MetaAnnotationService.js | 93 +
.../frontend/src/mixins/SummaryMixin.js | 86 +
.../frontend/src/plugins/fontawesome.js | 8 +
.../webapp/frontend/src/router/index.ts | 51 +
.../webapp/frontend/src/styles/_common.scss | 76 +
.../frontend/src/styles/_functions.scss | 8 +
.../webapp/frontend/src/styles/_tabs.scss | 67 +
.../frontend/src/styles/_variables.scss | 68 +
.../frontend/src/views/ConceptDatabase.vue | 273 +
.../webapp/frontend/src/views/Demo.vue | 156 +
.../webapp/frontend/src/views/Home.vue | 210 +
.../webapp/frontend/src/views/Metrics.vue | 771 ++
.../webapp/frontend/src/views/MetricsHome.vue | 178 +
.../frontend/src/views/TrainAnnotations.vue | 986 +++
.../webapp/frontend/tsconfig.app.json | 14 +
.../webapp/frontend/tsconfig.json | 14 +
.../webapp/frontend/tsconfig.node.json | 19 +
.../webapp/frontend/tsconfig.vitest.json | 11 +
.../webapp/frontend/vite.config.ts | 48 +
.../webapp/frontend/vitest.config.ts | 14 +
v1/medcat-trainer/webapp/requirements.txt | 9 +
v1/medcat-trainer/webapp/scripts/backup_db.sh | 27 +
.../webapp/scripts/create_group.py | 14 +
v1/medcat-trainer/webapp/scripts/crontab | 3 +
v1/medcat-trainer/webapp/scripts/entry.sh | 6 +
.../webapp/scripts/load_examples.py | 137 +
.../webapp/scripts/restore_db.sh | 26 +
.../webapp/scripts/run-bg-process.sh | 13 +
v1/medcat-trainer/webapp/scripts/run.sh | 36 +
.../registration/password_reset_email.html | 17 +
.../registration/password_reset_subject.txt | 1 +
287 files changed, 28984 insertions(+)
create mode 100644 v1/medcat-trainer/.env-example
create mode 100644 v1/medcat-trainer/.gitignore
create mode 100644 v1/medcat-trainer/.readthedocs.yaml
create mode 100644 v1/medcat-trainer/.vscode/launch.json
create mode 100644 v1/medcat-trainer/CODE_OF_CONDUCT.md
create mode 100644 v1/medcat-trainer/LICENSE
create mode 100644 v1/medcat-trainer/README.md
create mode 100644 v1/medcat-trainer/client/README.md
create mode 100644 v1/medcat-trainer/client/__init__.py
create mode 100644 v1/medcat-trainer/client/mctclient.py
create mode 100644 v1/medcat-trainer/client/pyproject.toml
create mode 100644 v1/medcat-trainer/client/tests/test_mctclient.py
create mode 100644 v1/medcat-trainer/configs/base.txt
create mode 100644 v1/medcat-trainer/docker-compose-dev.yml
create mode 100644 v1/medcat-trainer/docker-compose-mc0x.yml
create mode 100644 v1/medcat-trainer/docker-compose-prod.yml
create mode 100644 v1/medcat-trainer/docker-compose.yml
create mode 100644 v1/medcat-trainer/docs/Makefile
create mode 100644 v1/medcat-trainer/docs/_static/css/overrides.css
create mode 100644 v1/medcat-trainer/docs/_static/img/add-annotation-concept-pick.png
create mode 100644 v1/medcat-trainer/docs/_static/img/add-annotation-concept.png
create mode 100644 v1/medcat-trainer/docs/_static/img/add-annotation-menu.png
create mode 100644 v1/medcat-trainer/docs/_static/img/add-annotation-select-concept.png
create mode 100644 v1/medcat-trainer/docs/_static/img/add-annotation-text.png
create mode 100644 v1/medcat-trainer/docs/_static/img/add-new-meta-task.png
create mode 100644 v1/medcat-trainer/docs/_static/img/add-new-users.png
create mode 100644 v1/medcat-trainer/docs/_static/img/add_project_annotate_entities.png
create mode 100644 v1/medcat-trainer/docs/_static/img/admin_page.png
create mode 100644 v1/medcat-trainer/docs/_static/img/available-projects.png
create mode 100644 v1/medcat-trainer/docs/_static/img/cat-logo.png
create mode 100644 v1/medcat-trainer/docs/_static/img/cat-logo.svg
create mode 100644 v1/medcat-trainer/docs/_static/img/clone-projects.png
create mode 100644 v1/medcat-trainer/docs/_static/img/concepts-imported-status.png
create mode 100644 v1/medcat-trainer/docs/_static/img/delete-indexed-concepts.png
create mode 100644 v1/medcat-trainer/docs/_static/img/demo_interface.png
create mode 100644 v1/medcat-trainer/docs/_static/img/demo_tab.png
create mode 100644 v1/medcat-trainer/docs/_static/img/download-annos.png
create mode 100644 v1/medcat-trainer/docs/_static/img/help-button.png
create mode 100644 v1/medcat-trainer/docs/_static/img/home-screen-admin-options.png
create mode 100644 v1/medcat-trainer/docs/_static/img/import-concepts.png
create mode 100644 v1/medcat-trainer/docs/_static/img/login.png
create mode 100644 v1/medcat-trainer/docs/_static/img/main-annotation-interface.png
create mode 100644 v1/medcat-trainer/docs/_static/img/meta-task-form.png
create mode 100644 v1/medcat-trainer/docs/_static/img/meta-tasks-interface.png
create mode 100644 v1/medcat-trainer/docs/_static/img/new_projects.png
create mode 100644 v1/medcat-trainer/docs/_static/img/pick-alternative-concept.png
create mode 100644 v1/medcat-trainer/docs/_static/img/project-groups-view-available.png
create mode 100644 v1/medcat-trainer/docs/_static/img/project-groups-view.png
create mode 100644 v1/medcat-trainer/docs/_static/img/project_annotate_entities.png
create mode 100644 v1/medcat-trainer/docs/_static/img/projects-in-group.png
create mode 100644 v1/medcat-trainer/docs/_static/img/remove-default-user.png
create mode 100644 v1/medcat-trainer/docs/_static/img/reset-button.png
create mode 100644 v1/medcat-trainer/docs/_static/img/reset-projects.png
create mode 100644 v1/medcat-trainer/docs/_static/img/save_cdb.png
create mode 100644 v1/medcat-trainer/docs/_static/img/select-concept-dbs.png
create mode 100644 v1/medcat-trainer/docs/_static/img/select-existing-project.png
create mode 100644 v1/medcat-trainer/docs/_static/img/select-tasks.png
create mode 100644 v1/medcat-trainer/docs/_static/img/summary-button.png
create mode 100644 v1/medcat-trainer/docs/_static/img/tick_mark.png
create mode 100644 v1/medcat-trainer/docs/_static/img/users-select.png
create mode 100644 v1/medcat-trainer/docs/admin_setup.md
create mode 100644 v1/medcat-trainer/docs/advanced_usage.md
create mode 100644 v1/medcat-trainer/docs/annotator_guide.md
create mode 100644 v1/medcat-trainer/docs/client.md
create mode 100644 v1/medcat-trainer/docs/conf.py
create mode 100644 v1/medcat-trainer/docs/demo_page.md
create mode 100644 v1/medcat-trainer/docs/index.rst
create mode 100644 v1/medcat-trainer/docs/installation.md
create mode 100644 v1/medcat-trainer/docs/main.md
create mode 100644 v1/medcat-trainer/docs/maintenance.md
create mode 100644 v1/medcat-trainer/docs/make.bat
create mode 100644 v1/medcat-trainer/docs/meta_annotations.md
create mode 100644 v1/medcat-trainer/docs/project_admin.md
create mode 100644 v1/medcat-trainer/docs/project_group_admin.md
create mode 100644 v1/medcat-trainer/docs/requirements.txt
create mode 100644 v1/medcat-trainer/envs/.keep
create mode 100644 v1/medcat-trainer/envs/env
create mode 100644 v1/medcat-trainer/envs/env-mc0x
create mode 100644 v1/medcat-trainer/envs/env-prod
create mode 100644 v1/medcat-trainer/install.sh
create mode 100644 v1/medcat-trainer/nginx/nginx.conf
create mode 100644 v1/medcat-trainer/nginx/sites-enabled/medcattrainer
create mode 100644 v1/medcat-trainer/notebook_docs/API_Examples.ipynb
create mode 100644 v1/medcat-trainer/notebook_docs/Client_API_Tutorials.ipynb
create mode 100644 v1/medcat-trainer/notebook_docs/Generate_CUI_Filters.ipynb
create mode 100644 v1/medcat-trainer/notebook_docs/Processing_Annotations.ipynb
create mode 100644 v1/medcat-trainer/notebook_docs/Train_MedCAT_Models.ipynb
create mode 100644 v1/medcat-trainer/notebook_docs/example_data/MedCAT_Export_With_Text_2020-05-22_10_34_09.json
create mode 100644 v1/medcat-trainer/notebook_docs/example_data/README.md
create mode 100644 v1/medcat-trainer/notebook_docs/example_data/cardio.csv
create mode 100644 v1/medcat-trainer/notebook_docs/example_data/neuro.csv
create mode 100644 v1/medcat-trainer/notebook_docs/example_data/ortho.csv
create mode 100644 v1/medcat-trainer/notebook_docs/example_data/psych.csv
create mode 100644 v1/medcat-trainer/supervisord.conf
create mode 100644 v1/medcat-trainer/webapp/Dockerfile
create mode 100644 v1/medcat-trainer/webapp/api/api/__init__.py
create mode 100644 v1/medcat-trainer/webapp/api/api/admin/__init__.py
create mode 100644 v1/medcat-trainer/webapp/api/api/admin/actions.py
create mode 100644 v1/medcat-trainer/webapp/api/api/admin/models.py
create mode 100644 v1/medcat-trainer/webapp/api/api/apps.py
create mode 100644 v1/medcat-trainer/webapp/api/api/data_utils.py
create mode 100644 v1/medcat-trainer/webapp/api/api/fixtures/text_class_values.json
create mode 100644 v1/medcat-trainer/webapp/api/api/medcat_utils.py
create mode 100644 v1/medcat-trainer/webapp/api/api/metrics.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0001_initial.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0002_auto_20190923_2342.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0003_auto_20190924_1934.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0004_auto_20190925_1450.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0005_annotatedentity_alternative.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0006_concept_icd10.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0007_concept_cdb.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0008_annotatedentity_manually_created.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0009_document_last_modified.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0010_annotatedentity_last_modified.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0011_projectannotateentities_cdb_search_filter.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0012_conceptdb_use_for_training.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0013_auto_20191001_2248.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0014_conceptdb_name.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0015_auto_20191002_1615.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0016_auto_20191002_2306.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0017_delete_link.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0018_annotatedentity_killed.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0019_annotatedentity_correct.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0020_projectannotateentities_train_model_on_submit.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0021_auto_20200124_1108.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0022_metatask_default.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0023_concept_opcs4.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0024_auto_20200131_2339.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0025_auto_20200131_2353.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0026_auto_20200204_1517.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0027_auto_20200208_1443.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0028_auto_20200208_1559.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0029_auto_20200208_1609.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0030_auto_20200208_1858.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0031_annotatedentity_create_time.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0032_auto_20200212_0017.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0033_auto_20200212_0021.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0034_auto_20200212_0022.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0035_projectannotateentities_add_new_entities.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0036_auto_20200320_0105.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0037_auto_20200320_0107.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0038_auto_20200407_1418.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0039_project_cuis_file.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0040_projectannotateentities_restrict_concept_lookup.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0041_auto_20200519_1544.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0042_auto_20200520_0945.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0043_auto_20200520_1018.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0044_projectannotateentities_terminate_available.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0045_auto_20200603_0934.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0046_auto_20200802_1006.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0047_auto_20210112_0355.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0048_projectcuicounter.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0049_auto_20210202_1539.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0050_remove_project_tuis.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0051_auto_20210211_1812.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0052_auto_20210216_1130.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0053_remove_projectannotateentities_clinical_coding_project.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0054_remove_concept_vocab.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0055_auto_20210417_0902.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0056_auto_20210417_0926.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0057_auto_20210705_0954.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0058_auto_20210921_0022.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0059_auto_20211008_2038.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0060_auto_20211022_0940.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0061_project_annotation_guideline_link.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0062_auto_20220426_1558.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0063_auto_20220606_1107.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0064_auto_20220606_1253.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0065_auto_20221129_1102.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0066_exportedproject.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0067_delete_concept.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0068_auto_20230203_1715.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0069_auto_20230711_0938.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0070_auto_20230711_0951.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0071_auto_20230711_1654.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0072_delete_projectcuicounter.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0073_auto_20231022_0028.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0074_auto_20240215_1024.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0075_auto_20240429_1350.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0076_auto_20240510_1451.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0077_projectgroup_create_associated_projects.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0078_alter_project_polymorphic_ctype.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0078_metacatmodel_modelpack.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0079_merge_20240701_2259.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0080_alter_metatask_options_alter_metataskvalue_options_and_more.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0081_alter_metatask_name.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0082_remove_metacatmodel_meta_task_and_more.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0083_project_prepared_documents_and_more.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0084_conceptdb_create_time_conceptdb_last_modified_and_more.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0085_alter_conceptdb_name_alter_dataset_name_and_more.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0086_vocabulary_name.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0087_modelpack_create_time_modelpack_last_modified_and_more.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0088_alter_conceptdb_name.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/0089_projectannotateentities_deid_model_annotation_and_more.py
create mode 100644 v1/medcat-trainer/webapp/api/api/migrations/__init__.py
create mode 100644 v1/medcat-trainer/webapp/api/api/model_cache.py
create mode 100644 v1/medcat-trainer/webapp/api/api/models.py
create mode 100644 v1/medcat-trainer/webapp/api/api/permissions.py
create mode 100644 v1/medcat-trainer/webapp/api/api/serializers.py
create mode 100644 v1/medcat-trainer/webapp/api/api/signals.py
create mode 100644 v1/medcat-trainer/webapp/api/api/solr_utils.py
create mode 100644 v1/medcat-trainer/webapp/api/api/tests.py
create mode 100644 v1/medcat-trainer/webapp/api/api/urls.py
create mode 100644 v1/medcat-trainer/webapp/api/api/utils.py
create mode 100644 v1/medcat-trainer/webapp/api/api/views.py
create mode 100644 v1/medcat-trainer/webapp/api/core/__init__.py
create mode 100644 v1/medcat-trainer/webapp/api/core/settings.py
create mode 100644 v1/medcat-trainer/webapp/api/core/urls.py
create mode 100644 v1/medcat-trainer/webapp/api/core/wsgi.py
create mode 100644 v1/medcat-trainer/webapp/api/db-backup/.keep
create mode 100755 v1/medcat-trainer/webapp/api/db/.keep
create mode 100755 v1/medcat-trainer/webapp/api/manage.py
create mode 100644 v1/medcat-trainer/webapp/api/media/.keep
create mode 100644 v1/medcat-trainer/webapp/api/static/.keep
create mode 100644 v1/medcat-trainer/webapp/frontend/.editorconfig
create mode 100644 v1/medcat-trainer/webapp/frontend/.gitignore
create mode 100644 v1/medcat-trainer/webapp/frontend/.prettierrc.json
create mode 100644 v1/medcat-trainer/webapp/frontend/.vscode/extensions.json
create mode 100644 v1/medcat-trainer/webapp/frontend/README.md
create mode 100644 v1/medcat-trainer/webapp/frontend/env.d.ts
create mode 100644 v1/medcat-trainer/webapp/frontend/eslint.config.js
create mode 100644 v1/medcat-trainer/webapp/frontend/index.html
create mode 100644 v1/medcat-trainer/webapp/frontend/package-lock.json
create mode 100644 v1/medcat-trainer/webapp/frontend/package.json
create mode 100644 v1/medcat-trainer/webapp/frontend/public/favicon.ico
create mode 100644 v1/medcat-trainer/webapp/frontend/src/App.vue
create mode 100644 v1/medcat-trainer/webapp/frontend/src/assets/base.css
create mode 100644 v1/medcat-trainer/webapp/frontend/src/assets/cat-logo.png
create mode 100644 v1/medcat-trainer/webapp/frontend/src/assets/main.css
create mode 100644 v1/medcat-trainer/webapp/frontend/src/components/anns/AddAnnotation.vue
create mode 100644 v1/medcat-trainer/webapp/frontend/src/components/anns/AnnoResult.vue
create mode 100644 v1/medcat-trainer/webapp/frontend/src/components/anns/TaskBar.vue
create mode 100644 v1/medcat-trainer/webapp/frontend/src/components/common/AddNewConcept.vue
create mode 100644 v1/medcat-trainer/webapp/frontend/src/components/common/AnnotationSummary.vue
create mode 100644 v1/medcat-trainer/webapp/frontend/src/components/common/ClinicalText.vue
create mode 100644 v1/medcat-trainer/webapp/frontend/src/components/common/ConceptFilter.vue
create mode 100644 v1/medcat-trainer/webapp/frontend/src/components/common/ConceptPicker.vue
create mode 100644 v1/medcat-trainer/webapp/frontend/src/components/common/ConceptSummary.vue
create mode 100644 v1/medcat-trainer/webapp/frontend/src/components/common/DocumentSummary.vue
create mode 100644 v1/medcat-trainer/webapp/frontend/src/components/common/Login.vue
create mode 100644 v1/medcat-trainer/webapp/frontend/src/components/common/Modal.vue
create mode 100644 v1/medcat-trainer/webapp/frontend/src/components/common/NavBar.vue
create mode 100644 v1/medcat-trainer/webapp/frontend/src/components/common/ProjectList.vue
create mode 100644 v1/medcat-trainer/webapp/frontend/src/components/metrics/AnnotationsTable.vue
create mode 100644 v1/medcat-trainer/webapp/frontend/src/components/metrics/ConceptSummary.vue
create mode 100644 v1/medcat-trainer/webapp/frontend/src/components/metrics/MetricCell.vue
create mode 100644 v1/medcat-trainer/webapp/frontend/src/components/models/ConceptDatabaseViz.vue
create mode 100644 v1/medcat-trainer/webapp/frontend/src/components/models/VueTree.vue
create mode 100644 v1/medcat-trainer/webapp/frontend/src/components/usecases/HelpContent.vue
create mode 100644 v1/medcat-trainer/webapp/frontend/src/components/usecases/MetaAnnotationTask.vue
create mode 100644 v1/medcat-trainer/webapp/frontend/src/components/usecases/MetaAnnotationTaskContainer.vue
create mode 100644 v1/medcat-trainer/webapp/frontend/src/components/usecases/RelationAnnotation.vue
create mode 100644 v1/medcat-trainer/webapp/frontend/src/components/usecases/RelationAnnotationTaskContainer.vue
create mode 100644 v1/medcat-trainer/webapp/frontend/src/event-bus.ts
create mode 100644 v1/medcat-trainer/webapp/frontend/src/main.ts
create mode 100644 v1/medcat-trainer/webapp/frontend/src/mixins/ConceptDetailService.js
create mode 100644 v1/medcat-trainer/webapp/frontend/src/mixins/MetaAnnotationService.js
create mode 100644 v1/medcat-trainer/webapp/frontend/src/mixins/SummaryMixin.js
create mode 100644 v1/medcat-trainer/webapp/frontend/src/plugins/fontawesome.js
create mode 100644 v1/medcat-trainer/webapp/frontend/src/router/index.ts
create mode 100644 v1/medcat-trainer/webapp/frontend/src/styles/_common.scss
create mode 100644 v1/medcat-trainer/webapp/frontend/src/styles/_functions.scss
create mode 100644 v1/medcat-trainer/webapp/frontend/src/styles/_tabs.scss
create mode 100644 v1/medcat-trainer/webapp/frontend/src/styles/_variables.scss
create mode 100644 v1/medcat-trainer/webapp/frontend/src/views/ConceptDatabase.vue
create mode 100644 v1/medcat-trainer/webapp/frontend/src/views/Demo.vue
create mode 100644 v1/medcat-trainer/webapp/frontend/src/views/Home.vue
create mode 100644 v1/medcat-trainer/webapp/frontend/src/views/Metrics.vue
create mode 100644 v1/medcat-trainer/webapp/frontend/src/views/MetricsHome.vue
create mode 100644 v1/medcat-trainer/webapp/frontend/src/views/TrainAnnotations.vue
create mode 100644 v1/medcat-trainer/webapp/frontend/tsconfig.app.json
create mode 100644 v1/medcat-trainer/webapp/frontend/tsconfig.json
create mode 100644 v1/medcat-trainer/webapp/frontend/tsconfig.node.json
create mode 100644 v1/medcat-trainer/webapp/frontend/tsconfig.vitest.json
create mode 100644 v1/medcat-trainer/webapp/frontend/vite.config.ts
create mode 100644 v1/medcat-trainer/webapp/frontend/vitest.config.ts
create mode 100644 v1/medcat-trainer/webapp/requirements.txt
create mode 100755 v1/medcat-trainer/webapp/scripts/backup_db.sh
create mode 100644 v1/medcat-trainer/webapp/scripts/create_group.py
create mode 100644 v1/medcat-trainer/webapp/scripts/crontab
create mode 100644 v1/medcat-trainer/webapp/scripts/entry.sh
create mode 100644 v1/medcat-trainer/webapp/scripts/load_examples.py
create mode 100755 v1/medcat-trainer/webapp/scripts/restore_db.sh
create mode 100755 v1/medcat-trainer/webapp/scripts/run-bg-process.sh
create mode 100755 v1/medcat-trainer/webapp/scripts/run.sh
create mode 100644 v1/medcat-trainer/webapp/templates/registration/password_reset_email.html
create mode 100644 v1/medcat-trainer/webapp/templates/registration/password_reset_subject.txt
diff --git a/v1/medcat-trainer/.env-example b/v1/medcat-trainer/.env-example
new file mode 100644
index 000000000..40b942556
--- /dev/null
+++ b/v1/medcat-trainer/.env-example
@@ -0,0 +1,6 @@
+# MedCAT
+SPACY_MODELS="en_core_web_sm en_core_web_md en_core_web_lg"
+
+# Ports
+MCTRAINER_PORT=8001
+SOLR_PORT=8983
diff --git a/v1/medcat-trainer/.gitignore b/v1/medcat-trainer/.gitignore
new file mode 100644
index 000000000..c87aa6e98
--- /dev/null
+++ b/v1/medcat-trainer/.gitignore
@@ -0,0 +1,51 @@
+#Directories to be ignored fully
+/books/
+/articles/
+/other/
+/output/
+/graphics/
+/webapp/models/*
+data/
+tmp/
+*_tmp/
+.idea
+
+/webapp/frontend/dist/*
+/webapp/api/media/*
+/webapp/api/static/*
+
+# Configuration
+.env
+
+# Keep folders with this
+!.keep
+
+#tmp and similar files
+.nfs*
+*.pyc
+*.out
+*.swp
+*.swn
+tmp_*
+t_*
+tmp_*
+*_tmp
+*.swo
+*.lyx.emergency
+*.lyx#
+*~
+*.log
+*hidden*
+db.sqlite3
+nohup.out
+tmp.py
+
+# docs outputs
+docs/_build
+
+# macOS system files
+.DS_Store
+*/.DS_Store
+
+# Jupyter Notebook checkpoints
+*/.ipynb_checkpoints/*
diff --git a/v1/medcat-trainer/.readthedocs.yaml b/v1/medcat-trainer/.readthedocs.yaml
new file mode 100644
index 000000000..4bea1918f
--- /dev/null
+++ b/v1/medcat-trainer/.readthedocs.yaml
@@ -0,0 +1,17 @@
+# .readthedocs.yaml
+# Read the Docs configuration file
+# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
+
+version: 2
+
+build:
+ os: ubuntu-20.04
+ tools:
+ python: "3.9"
+
+sphinx:
+ configuration: medcat-trainer/docs/conf.py
+
+python:
+ install:
+ - requirements: medcat-trainer/docs/requirements.txt
diff --git a/v1/medcat-trainer/.vscode/launch.json b/v1/medcat-trainer/.vscode/launch.json
new file mode 100644
index 000000000..66557aad5
--- /dev/null
+++ b/v1/medcat-trainer/.vscode/launch.json
@@ -0,0 +1,91 @@
+{
+ // Use IntelliSense to learn about possible attributes.
+ // Hover to view descriptions of existing attributes.
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": [
+
+
+ {
+ "name": "frontend",
+ "type": "node",
+ "request": "launch",
+ "cwd": "${workspaceFolder}/webapp/frontend",
+ "runtimeExecutable": "npm",
+ "runtimeArgs": [
+ "run",
+ "dev"
+ ],
+ "console": "integratedTerminal"
+ },
+ {
+ "name": "frontend build",
+ "type": "node",
+ "request": "launch",
+ "cwd": "${workspaceFolder}/webapp/frontend",
+ "runtimeExecutable": "npm",
+ "runtimeArgs": [
+ "run",
+ "build"
+ ],
+ "console": "integratedTerminal"
+ },
+ {
+ "name": "django shell",
+ "type": "debugpy",
+ "request": "launch",
+ "program": "${workspaceFolder}/webapp/api/manage.py",
+ "args": [
+ "shell"
+ ],
+ "django": true
+ },
+ {
+ "name": "make migrations",
+ "type": "debugpy",
+ "request": "launch",
+ "program": "${workspaceFolder}/webapp/api/manage.py",
+ "args": [
+ "makemigrations"
+ ],
+ "django": true
+ },
+ {
+ "name": "migrate",
+ "type": "debugpy",
+ "request": "launch",
+ "program": "${workspaceFolder}/webapp/api/manage.py",
+ "args": [
+ "migrate"
+ ],
+ "django": true
+ },
+ {
+ "name": "run server",
+ "type": "debugpy",
+ "request": "launch",
+ "args": [
+ "runserver",
+ "0.0.0.0:8001"
+ ],
+ "env": {
+ "CONCEPT_SEARCH_SERVICE_HOST": "localhost"
+ },
+ "django": true,
+ "program": "${workspaceFolder}/webapp/api/manage.py"
+ },
+ {
+ "name": "process tasks",
+ "type": "debugpy",
+ "request": "launch",
+ "args": [
+ "process_tasks"
+ ],
+ "env": {
+ "CONCEPT_SEARCH_SERVICE_HOST": "localhost"
+ },
+ "django": true,
+ "program": "${workspaceFolder}/webapp/api/manage.py"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/v1/medcat-trainer/CODE_OF_CONDUCT.md b/v1/medcat-trainer/CODE_OF_CONDUCT.md
new file mode 100644
index 000000000..18c914718
--- /dev/null
+++ b/v1/medcat-trainer/CODE_OF_CONDUCT.md
@@ -0,0 +1,128 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+We as members, contributors, and leaders pledge to make participation in our
+community a harassment-free experience for everyone, regardless of age, body
+size, visible or invisible disability, ethnicity, sex characteristics, gender
+identity and expression, level of experience, education, socio-economic status,
+nationality, personal appearance, race, religion, or sexual identity
+and orientation.
+
+We pledge to act and interact in ways that contribute to an open, welcoming,
+diverse, inclusive, and healthy community.
+
+## Our Standards
+
+Examples of behavior that contributes to a positive environment for our
+community include:
+
+* Demonstrating empathy and kindness toward other people
+* Being respectful of differing opinions, viewpoints, and experiences
+* Giving and gracefully accepting constructive feedback
+* Accepting responsibility and apologizing to those affected by our mistakes,
+ and learning from the experience
+* Focusing on what is best not just for us as individuals, but for the
+ overall community
+
+Examples of unacceptable behavior include:
+
+* The use of sexualized language or imagery, and sexual attention or
+ advances of any kind
+* Trolling, insulting or derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or email
+ address, without their explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Enforcement Responsibilities
+
+Community leaders are responsible for clarifying and enforcing our standards of
+acceptable behavior and will take appropriate and fair corrective action in
+response to any behavior that they deem inappropriate, threatening, offensive,
+or harmful.
+
+Community leaders have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, and will communicate reasons for moderation
+decisions when appropriate.
+
+## Scope
+
+This Code of Conduct applies within all community spaces, and also applies when
+an individual is officially representing the community in public spaces.
+Examples of representing our community include using an official e-mail address,
+posting via an official social media account, or acting as an appointed
+representative at an online or offline event.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported to the community leaders responsible for enforcement at
+.
+All complaints will be reviewed and investigated promptly and fairly.
+
+All community leaders are obligated to respect the privacy and security of the
+reporter of any incident.
+
+## Enforcement Guidelines
+
+Community leaders will follow these Community Impact Guidelines in determining
+the consequences for any action they deem in violation of this Code of Conduct:
+
+### 1. Correction
+
+**Community Impact**: Use of inappropriate language or other behavior deemed
+unprofessional or unwelcome in the community.
+
+**Consequence**: A private, written warning from community leaders, providing
+clarity around the nature of the violation and an explanation of why the
+behavior was inappropriate. A public apology may be requested.
+
+### 2. Warning
+
+**Community Impact**: A violation through a single incident or series
+of actions.
+
+**Consequence**: A warning with consequences for continued behavior. No
+interaction with the people involved, including unsolicited interaction with
+those enforcing the Code of Conduct, for a specified period of time. This
+includes avoiding interactions in community spaces as well as external channels
+like social media. Violating these terms may lead to a temporary or
+permanent ban.
+
+### 3. Temporary Ban
+
+**Community Impact**: A serious violation of community standards, including
+sustained inappropriate behavior.
+
+**Consequence**: A temporary ban from any sort of interaction or public
+communication with the community for a specified period of time. No public or
+private interaction with the people involved, including unsolicited interaction
+with those enforcing the Code of Conduct, is allowed during this period.
+Violating these terms may lead to a permanent ban.
+
+### 4. Permanent Ban
+
+**Community Impact**: Demonstrating a pattern of violation of community
+standards, including sustained inappropriate behavior, harassment of an
+individual, or aggression toward or disparagement of classes of individuals.
+
+**Consequence**: A permanent ban from any sort of public interaction within
+the community.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage],
+version 2.0, available at
+https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
+
+Community Impact Guidelines were inspired by [Mozilla's code of conduct
+enforcement ladder](https://github.com/mozilla/diversity).
+
+[homepage]: https://www.contributor-covenant.org
+
+For answers to common questions about this code of conduct, see the FAQ at
+https://www.contributor-covenant.org/faq. Translations are available at
+https://www.contributor-covenant.org/translations.
diff --git a/v1/medcat-trainer/LICENSE b/v1/medcat-trainer/LICENSE
new file mode 100644
index 000000000..7a4a3ea24
--- /dev/null
+++ b/v1/medcat-trainer/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
\ No newline at end of file
diff --git a/v1/medcat-trainer/README.md b/v1/medcat-trainer/README.md
new file mode 100644
index 000000000..13b7ef260
--- /dev/null
+++ b/v1/medcat-trainer/README.md
@@ -0,0 +1,18 @@
+ # Medical
oncept Annotation Tool Trainer
+
+[](https://github.com/CogStack/cogstack-nlp/actions/workflows/medcat-trainer_qa.yml?query=branch%3Amain)
+[](https://github.com/CogStack/cogstack-nlp/actions/workflows/medcat-trainer_release.yml)
+[](https://readthedocs.org/projects/cogstack-nlp-medcat-trainer/badge/?version=latest)
+[](https://github.com/CogStack/cogstack-nlp/releases/latest)
+
+MedCATTrainer is an interface for building, improving and customising a given Named Entity Recognition
+and Linking (NER+L) model (MedCAT) for biomedical domain text.
+
+MedCATTrainer was presented at EMNLP/IJCNLP 2019 :tada:
+[here](https://www.aclweb.org/anthology/D19-3024.pdf)
+
+# Documentation and Discussion
+
+Official docs available [here](https://docs.cogstack.org/projects/medcat-trainer)
+
+If you have any questions why not reach out to the community [discourse forum here](https://discourse.cogstack.org/)
diff --git a/v1/medcat-trainer/client/README.md b/v1/medcat-trainer/client/README.md
new file mode 100644
index 000000000..d5d131325
--- /dev/null
+++ b/v1/medcat-trainer/client/README.md
@@ -0,0 +1,88 @@
+
+---
+
+# MedCATtrainer Client
+
+A Python client for interacting with a MedCATTrainer web application instance. This package allows you to manage datasets, concept databases, vocabularies, model packs, users, projects, and more via Python code or the command line.
+
+## Features
+
+- Manage datasets, concept databases, vocabularies, and model packs
+- Create and manage users and projects
+- Retrieve and upload project annotations
+- Command-line interface (CLI) for automation
+
+## Installation
+
+```sh
+pip install mctclient
+```
+
+Or, if installing from source:
+
+```sh
+cd client
+python -m build
+pip install dist/*.whl
+```
+
+## Python Usage
+
+```sh
+export MCTRAINER_USERNAME=
+export MCTRAINER_PASSWORD=
+```
+
+```python
+from mctclient import MedCATTrainerSession, MCTDataset, MCTConceptDB, MCTVocab, MCTModelPack, MCTMetaTask, MCTRelTask, MCTUser, MCTProject
+
+# Connect to your MedCATTrainer instance
+session = MedCATTrainerSession(server="http://localhost:8001")
+
+# List all projects
+projects = session.get_projects()
+for project in projects:
+ print(project)
+
+# Create a new dataset
+dataset = session.create_dataset(name="My Dataset", dataset_file="path/to/data.csv")
+
+# Create a new user
+user = session.create_user(username="newuser", password="password123")
+
+# Create a new project
+project = session.create_project(
+ name="My Project",
+ description="A new annotation project",
+ members=[user],
+ dataset=dataset
+)
+```
+
+### MedCATTrainerSession Methods
+
+- `create_project(name, description, members, dataset, cuis=[], cuis_file=None, concept_db=None, vocab=None, cdb_search_filter=None, modelpack=None, meta_tasks=[], rel_tasks=[])`
+- `create_dataset(name, dataset_file)`
+- `create_user(username, password)`
+- `create_medcat_model(cdb, vocab)`
+- `create_medcat_model_pack(model_pack)`
+- `get_users()`
+- `get_models()`
+- `get_model_packs()`
+- `get_meta_tasks()`
+- `get_rel_tasks()`
+- `get_projects()`
+- `get_datasets()`
+- `get_project_annos(projects)`
+
+Each method returns the corresponding object or a list of objects.
+
+## License
+
+This project is licensed under the Apache 2.0 License.
+
+## Contributing
+
+Pull requests are welcome! For major changes, please open an issue first to discuss what you would like to change.
+
+
diff --git a/v1/medcat-trainer/client/__init__.py b/v1/medcat-trainer/client/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/v1/medcat-trainer/client/mctclient.py b/v1/medcat-trainer/client/mctclient.py
new file mode 100644
index 000000000..2d4b0370b
--- /dev/null
+++ b/v1/medcat-trainer/client/mctclient.py
@@ -0,0 +1,547 @@
+from dataclasses import dataclass
+import json
+import os
+from abc import ABC
+from typing import List, Tuple, Union
+
+import requests
+
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass
+class MCTObj(ABC):
+ id: str=None
+
+ def valid(self):
+ return self.id is not None
+
+
+@dataclass
+class MCTDataset(MCTObj):
+ """A dataset in the MedCATTrainer instance.
+
+ Attributes:
+ name (str): The name of the dataset.
+ dataset_file (str): The path to the dataset file, can be a csv, or excel file, with at
+ least 2 columns: 'name': unique identifier for each text, and 'text': the text to be annotated.
+ """
+ name: str=None
+ dataset_file: str=None
+
+ def __str__(self):
+ return f'{self.id} : {self.name} \t {self.dataset_file}'
+
+
+@dataclass
+class MCTConceptDB(MCTObj):
+ """A concept database in the MedCATTrainer instance.
+
+ Attributes:
+ name (str): The name of the concept database. Name must start with a lowercase letter and contain only alphanumeric characters and underscores.
+ conceptdb_file (str): The path to the concept database file, should be a .dat file.
+ use_for_training (bool): Whether to use the concept database for training. Defaults to True as most uploaded CDBs will be used for training, unless they are used for the concept search lookup.
+ """
+ name: str=None
+ conceptdb_file: str=None
+ use_for_training: bool=True
+
+ def __post_init__(self):
+ if self.name is not None:
+ if not self.name[0].islower():
+ raise ValueError("Name must start with a lowercase letter")
+ if not self.name.replace('_', '').replace('-', '').isalnum():
+ raise ValueError("Name must contain only alphanumeric characters and underscores")
+
+ def __str__(self):
+ return f'{getattr(self, "id", "N/A")} : {self.name} \t {self.conceptdb_file}'
+
+
+@dataclass
+class MCTVocab(MCTObj):
+ """A vocabulary in the MedCATTrainer instance.
+
+ Attributes:
+ name (str): The name of the vocabulary.
+ vocab_file (str): The path to the vocabulary file, should be a .dat file.
+ """
+ name: str=None
+ vocab_file: str=None
+
+ def __str__(self):
+ return f'{self.id} : {self.vocab_file}'
+
+
+@dataclass
+class MCTModelPack(MCTObj):
+ """A model pack in the MedCATTrainer instance.
+
+ Attributes:
+ name (str): The name of the model pack.
+ model_pack_zip (str): The path to the model pack zip file, should be a .zip file.
+ """
+ name: str=None
+ model_pack_zip: str=None
+
+ def __str__(self):
+ return f'{self.id} : {self.name} \t {self. model_pack_zip}'
+
+
+@dataclass
+class MCTMetaTask(MCTObj):
+ """A meta task in the MedCATTrainer instance.
+
+ Attributes:
+ name (str): The name of the meta task.
+ """
+ name: str=None
+
+ def __str__(self):
+ return f'{self.id} : {self.name}'
+
+
+@dataclass
+class MCTRelTask(MCTObj):
+ """A relation extraction task in the MedCATTrainer instance.
+
+ Attributes:
+ name (str): The name of the relation extraction task.
+ """
+ name: str=None
+
+ def __str__(self):
+ return f'{self.id} : {self.name}'
+
+
+@dataclass
+class MCTUser(MCTObj):
+ """A user in the MedCATTrainer instance.
+
+ Attributes:
+ username (str): The username of the user.
+ """
+ username: str=None
+
+ def __str__(self):
+ return f'{self.id} : {self.username}'
+
+
+@dataclass
+class MCTProject(MCTObj):
+ """A project in the MedCATTrainer instance.
+
+ Attributes:
+ name (str): The name of the project.
+ description (str): The description of the project.
+ cuis (str): The CUIs to be used in the project filter.
+ dataset (MCTDataset): The dataset to be used in the project.
+ concept_db (MCTConceptDB): The concept database to be used in the project.
+ vocab (MCTVocab): The vocabulary to be used in the project.
+ members (List[MCTUser]): The annotators for the project.
+ meta_tasks (List[MCTMetaTask]): The meta tasks for the project.
+ rel_tasks (List[MCTRelTask]): The relation extraction tasks for the project.
+ """
+ name: str=None
+ description: str=None
+ cuis: str=None
+ dataset: MCTDataset=None
+ concept_db: MCTConceptDB=None
+ vocab: MCTVocab=None
+ members: List[MCTUser]=None
+ meta_tasks: List[MCTMetaTask]=None
+ rel_tasks: List[MCTRelTask]=None
+
+ def __str__(self):
+ return f'{self.id} : {self.name} \t {self.description} \t {self.dataset}'
+
+
+
+class MedCATTrainerSession:
+ """Wrapper for the MedCATTrainer API.
+ This class provides a wrapper around the MedCATTrainer API, allowing for easy creation of projects, datasets, users, and models.
+
+ Attributes:
+ server (str): The server to connect to can also be set by an ENVVAR MCTRAINER_SERVER. Defaults to http://localhost:8001.
+ username (str): The username to connect to can also be set by an ENVVAR MCTRAINER_USERNAME.
+ password (str): The password to connect to can also be set by an ENVVAR MCTRAINER_PASSWORD.
+
+ Example:
+ Create a project with a concept database, vocabulary, dataset, and user.
+
+ >>> session = MedCATTrainerSession()
+ >>> ds = session.create_dataset(name='Test DS', dataset_file='.csv')
+ >>> cdb_file = '/cdb.dat'
+ >>> vocab_file = '/vocab.dat'
+ >>> model_pack_zip = '.zip'
+ >>> # Create a concept database and vocabulary in the MCTrainer instance. This is the NER+L model only.
+ >>> cdb, vocab = session.create_medcat_model(MCTConceptDB(name='test_cdb', conceptdb_file=cdb_file),
+ MCTVocab(name='test_vocab', vocab_file=vocab_file))
+ >>> # OR Create a model pack in the MCTrainer instance, NER+L, plus any MetaCAT or RelCAT models packaged together.
+ >>> session.create_medcat_model_pack(MCTModelPack(name='test_model_pack', model_pack_zip=model_pack_zip))
+ >>> session.create_project(name='test-project', description='test-description', members=[MCTUser(username='test-user')], dataset=ds, concept_db=cdb, vocab=vocab)
+
+ A common interaction would be to create a project with a new dataset but existing concept database and vocabulary or Modelpack.
+ >>> projects = session.get_projects()
+ >>> ds = session.create_dataset(name='New Test DS', dataset_file='/Users/tom/phd/MedCATtrainer/notebook_docs/example_data/cardio.csv')
+ >>> # MCTObjects can be referenced by name or by the wrapper object.
+ >>> session.create_project(name='test-project', description='test-description', members=[MCTUser(username='test-user')], dataset=ds,
+ concept_db=MCTConceptDB(name='test_cdb'), vocab=MCTVocab(name='test_vocab'))
+
+ To download annotations for a project:
+ >>> projects = session.get_projects()
+ >>> annotations = session.get_project_annos(projects[0])
+ """
+
+ def __init__(self, server=None, username=None, password=None):
+ """Initialize the MedCATTrainerSession.
+
+ Args:
+ server (_type_, optional): _description_. Defaults to None.
+
+ Raises:
+ MCTUtilsException: _description_
+ """
+ self.username = username or os.getenv("MCTRAINER_USERNAME")
+ self.password = password or os.getenv("MCTRAINER_PASSWORD")
+ self.server = server or 'http://localhost:8001'
+
+ payload = {"username": self.username, "password": self.password}
+ resp = requests.post(f"{self.server}/api/api-token-auth/", json=payload)
+ if 200 <= resp.status_code < 300:
+ token = json.loads(resp.text)["token"]
+ self.headers = {
+ 'Authorization': f'Token {token}',
+ }
+ else:
+ raise MCTUtilsException(f'Failed to login to MedCATtrainer instance running at: {self.server}')
+
+ def create_project(self, name: str,
+ description: str,
+ members: Union[List[MCTUser], List[str]],
+ dataset: Union[MCTDataset, str],
+ cuis: List[str]=[],
+ cuis_file: str=None,
+ concept_db: Union[MCTConceptDB, str]=None,
+ vocab: Union[MCTVocab, str]=None,
+ cdb_search_filter: Union[MCTConceptDB, str]=None,
+ modelpack: Union[MCTModelPack, str]=None,
+ meta_tasks: Union[List[MCTMetaTask], List[str]]=[],
+ rel_tasks: Union[List[MCTRelTask], List[str]]=[]):
+ """Create a new project in the MedCATTrainer session.
+ Users, models, datasets etc. can be referred to by either their client wrapper object or their name, and the ID will be retrieved
+ then used to create the project. Most names have a unique constraint on them so for the majority of cases will not results in an error.
+
+ Only a concept_db and vocab pair, or a modelpack needs to be specified.
+
+ Setting a modelpack will also eventually automatically select meta tasks and rel tasks.
+
+ Args:
+ name (str): The name of the project.
+ description (str): The description of the project.
+ members (Union[List[MCTUser], List[str]]): The annotators for the project.
+ dataset (Union[MCTDataset, str]): The dataset to be used in the project.
+ cuis (List[str]): The CUIs to be used in the project filter.
+ cuis_file (str): The file containing the CUIs to be used in the project filter, will be appended to the cuis list.
+ concept_db (Union[MCTConceptDB, str], optional): The concept database to be used in the project. Defaults to None.
+ vocab (Union[MCTVocab, str], optional): The vocabulary to be used in the project. Defaults to None.
+ cdb_search_filter (Union[MCTConceptDB, str], optional): _description_. Defaults to None.
+ modelpack (Union[MCTModelPack, str], optional): _description_. Defaults to None.
+ meta_tasks (Union[List[MCTMetaTask], List[str]], optional): _description_. Defaults to None.
+ rel_tasks (Union[List[MCTRelTask], List[str]], optional): _description_. Defaults to None.
+
+ Raises:
+ MCTUtilsException: If the project creation fails
+
+ Returns:
+ MCTProject: The created project
+ """
+
+ if all(isinstance(m, str) for m in members):
+ mct_members = [u for u in self.get_users() if u.username in members]
+ if len(mct_members) != len(members):
+ raise MCTUtilsException(f'Not all users found in MedCATTrainer instance: {members} requested, trainer members found: {mct_members}')
+ else:
+ members = mct_members
+
+ if isinstance(dataset, str):
+ try:
+ dataset = [d for d in self.get_datasets() if d.name == dataset].pop()
+ except IndexError:
+ raise MCTUtilsException(f'Dataset not found in MedCATTrainer instance: {dataset}')
+
+ if isinstance(concept_db, str):
+ try:
+ concept_db = [c for c in self.get_models()[0] if c.name == concept_db].pop()
+ except IndexError:
+ raise MCTUtilsException(f'Concept DB not found in MedCATTrainer instance: {concept_db}')
+
+ if isinstance(vocab, str):
+ try:
+ vocab = [v for v in self.get_models()[1] if v.name == vocab].pop()
+ except IndexError:
+ raise MCTUtilsException(f'Vocab not found in MedCATTrainer instance: {vocab}')
+
+ if isinstance(cdb_search_filter, str):
+ try:
+ cdb_search_filter = [c for c in self.get_concept_dbs() if c.name == cdb_search_filter].pop()
+ except IndexError:
+ raise MCTUtilsException(f'Concept DB not found in MedCATTrainer instance: {cdb_search_filter}')
+
+ if isinstance(modelpack, str):
+ try:
+ modelpack = [m for m in self.get_model_packs() if m.name == modelpack].pop()
+ except IndexError:
+ raise MCTUtilsException(f'Model pack not found in MedCATTrainer instance: {modelpack}')
+
+ if all(isinstance(m, str) for m in meta_tasks):
+ mct_meta_tasks = [m for m in self.get_meta_tasks() if m.name in meta_tasks]
+ if len(mct_meta_tasks) != len(meta_tasks):
+ raise MCTUtilsException(f'Not all meta tasks found in MedCATTrainer instance: {meta_tasks} requested, trainer meta tasks found: {mct_meta_tasks}')
+ else:
+ meta_tasks = mct_meta_tasks
+
+ if all(isinstance(r, str) for r in rel_tasks):
+ mct_rel_tasks = [r for r in self.get_rel_tasks() if r.name in rel_tasks]
+ if len(mct_rel_tasks) != len(rel_tasks):
+ raise MCTUtilsException(f'Not all rel tasks found in MedCATTrainer instance: {rel_tasks} requested, trainer rel tasks found: {mct_rel_tasks}')
+ else:
+ rel_tasks = mct_rel_tasks
+
+ if (concept_db or vocab) and modelpack:
+ raise MCTUtilsException('Cannot specify both concept_db/vocab and modelpack')
+
+ payload = {
+ 'name': name,
+ 'description': description,
+ 'cuis': ','.join(cuis),
+ 'dataset': dataset.id,
+ 'members': [m.id for m in members],
+ 'tasks': [mt.id for mt in meta_tasks],
+ 'relations': [rt.id for rt in rel_tasks]
+ }
+
+ if concept_db and vocab:
+ payload['concept_db'] = concept_db.id
+ payload['vocab'] = vocab.id
+ elif modelpack:
+ payload['model_pack'] = modelpack.id
+
+ if cdb_search_filter:
+ payload['cdb_search_filter'] = [cdb_search_filter.id]
+
+ if cuis_file:
+ with open(cuis_file, 'rb') as f:
+ resp = requests.post(f'{self.server}/api/project-annotate-entities/', data=payload, files={'cuis_file': f}, headers=self.headers)
+ else:
+ resp = requests.post(f'{self.server}/api/project-annotate-entities/', data=payload, headers=self.headers)
+ if 200 <= resp.status_code < 300:
+ resp_json = json.loads(resp.text)
+ return MCTProject(id=resp_json['id'], name=name, description=description, cuis=cuis,
+ dataset=dataset, concept_db=concept_db, vocab=vocab, members=members,
+ meta_tasks=meta_tasks, rel_tasks=rel_tasks)
+ else:
+ raise MCTUtilsException(f'Failed to create project with name: {name}', resp.text)
+
+ def create_dataset(self, name: str, dataset_file: str):
+ """Create a new dataset in the MedCATTrainer session.
+
+ Args:
+ name (str): The name of the dataset.
+ dataset_file (str): The path to the dataset file.
+
+ Raises:
+ MCTUtilsException: If the dataset creation fails
+
+ Returns:
+ MCTDataset: The created dataset
+ """
+ resp = requests.post(f'{self.server}/api/datasets/', headers=self.headers,
+ data={'name': name},
+ files={'original_file': open(dataset_file, 'rb')})
+ if 200 <= resp.status_code < 300:
+ resp_json = json.loads(resp.text)
+ return MCTDataset(name=name, id=resp_json['id'])
+ else:
+ raise MCTUtilsException(f'Failed to create dataset with name: {name}', resp.text)
+
+ def create_user(self, username: str, password):
+ """Create a new user in the MedCATTrainer session.
+
+ Args:
+ username (str): The username of the new user.
+ password (str): The password of the new user.
+
+ Raises:
+ MCTUtilsException: If the user creation fails
+
+ Returns:
+ MCTUser: The created user
+ """
+ payload = {
+ 'username': username,
+ 'password': password
+ }
+ resp = requests.post(f'{self.server}/api/users/', json=payload, headers=self.headers)
+ if 200 <= resp.status_code < 300:
+ resp_json = json.loads(resp.text)
+ return MCTUser(username=username, id=resp_json['id'])
+ else:
+ raise MCTUtilsException(f'Failed to create new user with username: {username}', resp.text)
+
+ def create_medcat_model(self, cdb:MCTConceptDB, vocab: MCTVocab):
+ """Create a new MedCAT cdb and vocab model in the MedCATTrainer session.
+
+ Args:
+ cdb (MCTConceptDB): The concept database to be created.
+ vocab (MCTVocab): The vocabulary to be created.
+
+ Raises:
+ MCTUtilsException: If the model creation fails
+ """
+ resp = requests.post(f'{self.server}/api/concept-dbs/', headers=self.headers,
+ data={'name': cdb.name, 'use_for_training': cdb.use_for_training},
+ files={'cdb_file': open(cdb.conceptdb_file, 'rb')})
+ if 200 <= resp.status_code < 300:
+ resp_json = json.loads(resp.text)
+ cdb.id = resp_json['id']
+ else:
+ raise MCTUtilsException(f'Failed uploading MedCAT cdb model: {cdb}', resp.text)
+
+ resp = requests.post(f'{self.server}/api/vocabs/', headers=self.headers,
+ data={'name': vocab.name},
+ files={'vocab_file': open(vocab.vocab_file, 'rb')})
+ if 200 <= resp.status_code < 300:
+ resp_json = json.loads(resp.text)
+ vocab.id = resp_json['id']
+ else:
+ raise MCTUtilsException(f'Failed uploading MedCAT vocab model: {vocab}', resp.text)
+
+ return cdb, vocab
+
+ def create_medcat_model_pack(self, model_pack: MCTModelPack):
+ """Create a new MedCAT model pack in the MedCATTrainer session.
+
+ Args:
+ model_pack (MCTModelPack): The model pack to be created.
+
+ Raises:
+ MCTUtilsException: If the model pack creation fails
+ """
+ resp = requests.post(f'{self.server}/api/modelpacks/', headers=self.headers,
+ data={'name': model_pack.name},
+ files={'model_pack': open(model_pack.model_pack_zip, 'rb')})
+ if 200 <= resp.status_code < 300:
+ resp_json = json.loads(resp.text)
+ model_pack.id = resp_json['id']
+ else:
+ raise MCTUtilsException(f'Failed uploading model pack: {model_pack.model_pack_zip}', resp.text)
+
+ def get_users(self) -> List[MCTUser]:
+ """Get all users in the MedCATTrainer instance.
+
+ Returns:
+ List[MCTUser]: A list of all users in the MedCATTrainer instance
+ """
+ users = json.loads(requests.get(f'{self.server}/api/users/', headers=self.headers).text)['results']
+ return [MCTUser(id=u['id'], username=u['username']) for u in users]
+
+ def get_models(self) -> Tuple[List[str], List[str]]:
+ """Get all MedCAT cdb and vocab models in the MedCATTrainer instance.
+
+ Returns:
+ Tuple[List[MCTConceptDB], List[MCTVocab]]: A tuple of lists of all MedCAT cdb and vocab models in the MedCATTrainer instance
+ """
+ cdbs = json.loads(requests.get(f'{self.server}/api/concept-dbs/', headers=self.headers).text)['results']
+ vocabs = json.loads(requests.get(f'{self.server}/api/vocabs/', headers=self.headers).text)['results']
+ mct_cdbs = [MCTConceptDB(id=cdb['id'], name=cdb['name'], conceptdb_file=cdb['cdb_file']) for cdb in cdbs]
+ mct_vocabs = [MCTVocab(id=v['id'], name=v['name'], vocab_file=v['vocab_file']) for v in vocabs]
+ return mct_cdbs, mct_vocabs
+
+ def get_model_packs(self) -> List[MCTModelPack]:
+ """Get all MedCAT model packs in the MedCATTrainer instance.
+
+ Returns:
+ List[MCTModelPack]: A list of all MedCAT model packs in the MedCATTrainer instance
+ """
+ resp = json.loads(requests.get(f'{self.server}/api/modelpacks/', headers=self.headers).text)['results']
+ mct_model_packs = [MCTModelPack(id=mp['id'], name=mp['name'], model_pack_zip=mp['model_pack']) for mp in resp]
+ return mct_model_packs
+
+ def get_meta_tasks(self) -> List[MCTMetaTask]:
+ """Get all MedCAT meta tasks that have been created in the MedCATTrainer instance.
+
+ Returns:
+ List[MCTMetaTask]: A list of all MedCAT meta tasks in the MedCATTrainer instance
+ """
+ resp = json.loads(requests.get(f'{self.server}/api/meta-tasks/', headers=self.headers).text)['results']
+ mct_meta_tasks = [MCTMetaTask(name=mt['name'], id=mt['id']) for mt in resp]
+ return mct_meta_tasks
+
+ def get_rel_tasks(self) -> List[MCTRelTask]:
+ """Get all MedCAT relation tasks that have been created in the MedCATTrainer instance.
+
+ Returns:
+ List[MCTRelTask]: A list of all MedCAT relation tasks in the MedCATTrainer instance
+ """
+ resp = json.loads(requests.get(f'{self.server}/api/relations/', headers=self.headers).text)['results']
+ mct_rel_tasks = [MCTRelTask(name=rt['label'], id=rt['id']) for rt in resp]
+ return mct_rel_tasks
+
+ def get_projects(self) -> List[MCTProject]:
+ """Get all MedCAT annotation projects that have been created in the MedCATTrainer instance.
+
+ Returns:
+ List[MCTProject]: A list of all MedCAT annotation projects in the MedCATTrainer instance
+ """
+ resp = json.loads(requests.get(f'{self.server}/api/project-annotate-entities/', headers=self.headers).text)['results']
+ mct_projects = [MCTProject(id=p['id'], name=p['name'], description=p['description'], cuis=p['cuis'],
+ dataset=MCTDataset(id=p['id']),
+ concept_db=MCTConceptDB(id=p['concept_db']),
+ vocab=MCTVocab(id=p['vocab']),
+ members=[MCTUser(id=u) for u in p['members']],
+ meta_tasks=[MCTMetaTask(id=mt) for mt in p['tasks']],
+ rel_tasks=[MCTRelTask(id=rt) for rt in p['relations']]) for p in resp]
+ return mct_projects
+
+ def get_datasets(self) -> List[MCTDataset]:
+ """Get all datasets that have been created in the MedCATTrainer instance.
+
+ Returns:
+ List[MCTDataset]: A list of all datasets in the MedCATTrainer instance
+ """
+ resp = json.loads(requests.get(f'{self.server}/api/datasets/', headers=self.headers).text)['results']
+ mct_datasets = [MCTDataset(name=d['name'], dataset_file=d['original_file'], id=d['id']) for d in resp]
+ return mct_datasets
+
+ def get_project_annos(self, projects: List[MCTProject]):
+ """Get the annotations for a list of projects. Schema is documented here: https://github.com/medcat/MedCATtrainer/blob/main/docs/api.md#download-annotations
+
+ Args:
+ projects (List[MCTProject]): A list of projects to get annotations for
+
+ Returns:
+ List[MCTProject]: A list of all projects with annotations
+ """
+ if any(p.id is None for p in projects):
+ raise MCTUtilsException('One or more project.id are None and all are required to download annotations')
+
+ resp = json.loads(requests.get(f'{self.server}/api/download-annos/?project_ids={",".join([str(p.id) for p in projects])}&with_text=1',
+ headers=self.headers).text)
+ return resp
+
+ def __str__(self) -> str:
+ return f'{self.server} \t {self.username} \t {self.password}'
+
+
+class MCTUtilsException(Exception):
+ """Base exception for MedCAT Trainer API errors"""
+ def __init__(self, message, original_exception=None):
+ self.message = message
+ self.original_exception = original_exception
+ super().__init__(self.message)
+
+ def __str__(self):
+ return f'{self.message} \n {self.original_exception}'
+
diff --git a/v1/medcat-trainer/client/pyproject.toml b/v1/medcat-trainer/client/pyproject.toml
new file mode 100644
index 000000000..05e562f9e
--- /dev/null
+++ b/v1/medcat-trainer/client/pyproject.toml
@@ -0,0 +1,18 @@
+[build-system]
+requires = ["setuptools>=61.0", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "medcattrainer-client"
+version = "1.0.0"
+description = "Python client for interacting with a MedCATTrainer instance"
+readme = "client/README.md"
+requires-python = ">=3.10"
+license = { file = "LICENSE" }
+authors = [{ name = "Tom Searle", email = "tom@cogstack.org" }]
+dependencies = ["requests"]
+
+[project.urls]
+Homepage = "https://github.com/CogStack/cogstack-nlp/"
+Documentation = "https://docs.cogstack.org/projects/medcat-trainer"
+Source = "https://github.com/CogStack/cogstack-nlp/"
diff --git a/v1/medcat-trainer/client/tests/test_mctclient.py b/v1/medcat-trainer/client/tests/test_mctclient.py
new file mode 100644
index 000000000..c06b8ae64
--- /dev/null
+++ b/v1/medcat-trainer/client/tests/test_mctclient.py
@@ -0,0 +1,119 @@
+import json
+import unittest
+from unittest.mock import patch, MagicMock
+from mctclient import (
+ MedCATTrainerSession, MCTDataset, MCTConceptDB, MCTVocab, MCTModelPack, MCTMetaTask, MCTRelTask, MCTUser, MCTProject
+)
+
+class TestMCTClient(unittest.TestCase):
+
+ @patch('mctclient.requests.post')
+ @patch('mctclient.requests.get')
+ def test_session_get_projects(self, mock_get, mock_post):
+ # Mock authentication
+ mock_post.return_value = MagicMock(status_code=200, text='{"token": "abc"}')
+ # Mock get_projects with a real project structure
+ mock_project = {
+ "id": 1,
+ "name": "Test Project",
+ "description": "A test project",
+ "cuis": "C001,C002",
+ "dataset": 10,
+ "concept_db": 20,
+ "vocab": 30,
+ "members": [100, 101],
+ "tasks": [200],
+ "relations": [300]
+ }
+ mock_get.return_value = MagicMock(
+ status_code=200,
+ text=json.dumps({"results": [mock_project]})
+ )
+ session = MedCATTrainerSession(server='http://localhost', username='u', password='p')
+ projects = session.get_projects()
+ self.assertIsInstance(projects, list)
+ self.assertEqual(len(projects), 1)
+ project = projects[0]
+ self.assertIsInstance(project, MCTProject)
+ self.assertEqual(project.name, "Test Project")
+ self.assertEqual(project.description, "A test project")
+ self.assertEqual(project.cuis, "C001,C002")
+ self.assertIsInstance(project.dataset, MCTDataset)
+ self.assertIsInstance(project.concept_db, MCTConceptDB)
+ self.assertIsInstance(project.vocab, MCTVocab)
+ self.assertTrue(all(isinstance(m, MCTUser) for m in project.members))
+ self.assertTrue(all(isinstance(mt, MCTMetaTask) for mt in project.meta_tasks))
+ self.assertTrue(all(isinstance(rt, MCTRelTask) for rt in project.rel_tasks))
+
+ @patch('mctclient.requests.post')
+ def test_create_project(self, mock_post):
+ # Mock authentication
+ def post_side_effect(url, *args, **kwargs):
+ if url.endswith('/api/api-token-auth/'):
+ return MagicMock(status_code=200, text='{"token": "abc"}')
+ elif url.endswith('/api/project-annotate-entities/'):
+ # Return a response with all fields needed for MCTProject
+ return MagicMock(
+ status_code=200,
+ text=json.dumps({
+ 'id': '3',
+ 'name': 'My Project',
+ 'description': 'desc',
+ 'cuis': 'C001,C002',
+ 'dataset': '2',
+ 'concept_db': '20',
+ 'vocab': '30',
+ 'members': ['1'],
+ 'tasks': ['200'],
+ 'relations': ['300']
+ }),
+ json=lambda: {
+ 'id': '3',
+ 'name': 'My Project',
+ 'description': 'desc',
+ 'cuis': 'C001,C002',
+ 'dataset': '2',
+ 'concept_db': '20',
+ 'vocab': '30',
+ 'members': ['1'],
+ 'tasks': ['200'],
+ 'relations': ['300']
+ }
+ )
+ else:
+ return MagicMock(status_code=404, text='')
+
+ mock_post.side_effect = post_side_effect
+
+ session = MedCATTrainerSession(server='http://localhost', username='u', password='p')
+ user = MCTUser(id='1', username='testuser')
+ dataset = MCTDataset(id='2', name='TestDS', dataset_file='file.csv')
+ concept_db = MCTConceptDB(id='20', name='testCDB', conceptdb_file='cdb.dat')
+ vocab = MCTVocab(id='30', name='testVocab', vocab_file='vocab.dat')
+ meta_task = MCTMetaTask(id='200', name='TestMetaTask')
+ rel_task = MCTRelTask(id='300', name='TestRelTask')
+
+ project = session.create_project(
+ name='My Project',
+ description='desc',
+ cuis='C001,C002',
+ members=[user],
+ dataset=dataset,
+ concept_db=concept_db,
+ vocab=vocab,
+ meta_tasks=[meta_task],
+ rel_tasks=[rel_task]
+ )
+ self.assertIsInstance(project, MCTProject)
+ self.assertEqual(project.name, 'My Project')
+ self.assertEqual(project.description, 'desc')
+ self.assertEqual(project.cuis, 'C001,C002')
+ self.assertIsInstance(project.dataset, MCTDataset)
+ self.assertIsInstance(project.concept_db, MCTConceptDB)
+ self.assertIsInstance(project.vocab, MCTVocab)
+ self.assertEqual(project.members, [user])
+ self.assertEqual(project.meta_tasks, [meta_task])
+ self.assertEqual(project.rel_tasks, [rel_task])
+
+if __name__ == '__main__':
+ unittest.main()
\ No newline at end of file
diff --git a/v1/medcat-trainer/configs/base.txt b/v1/medcat-trainer/configs/base.txt
new file mode 100644
index 000000000..418f28af8
--- /dev/null
+++ b/v1/medcat-trainer/configs/base.txt
@@ -0,0 +1,14 @@
+cat.linking.optim = {'type': 'standard', 'lr': 0.1}
+cat.linking.filter_before_disamb = True
+# 20 - INFO; 10 - DEBUG
+cat.general.log_level = 20
+# Recommended is to have this one negative
+cat.linking.similarity_threshold = -5
+# And this one to be used as the real th
+cat.linking.similarity_threshold_trainer = -5
+# Used for limiting the number of occ of a concept in a project
+cat.general.cui_count_limit = 100000000
+# Is unlink full
+cat.general.full_unlink = False
+# use this spacy model
+cat.general.spacy_model = 'en_core_web_md'
diff --git a/v1/medcat-trainer/docker-compose-dev.yml b/v1/medcat-trainer/docker-compose-dev.yml
new file mode 100644
index 000000000..7117ab2ce
--- /dev/null
+++ b/v1/medcat-trainer/docker-compose-dev.yml
@@ -0,0 +1,61 @@
+# Dev compose yml file for building, running and mounting local git cloned repo.
+
+services:
+ medcattrainer:
+ build:
+ network: host
+ context: ./webapp
+ args:
+ SPACY_MODELS: ${SPACY_MODELS:-en_core_web_md}
+ restart: always
+ volumes:
+ - api-media:/home/api/media
+ - api-static:/home/api/static
+ - api-db:/home/api/db
+ - api-db-backup:/home/api/db-backup
+ - ./webapp/api/core:/home/api/core
+ - ./webapp/api/api:/home/api/api
+ - ./webapp/scripts:/home/scripts
+ - ./configs:/home/configs
+ - ./supervisord.conf:/etc/supervisord.conf
+ env_file:
+ - ./envs/env
+ environment:
+ - MCT_VERSION=latest
+ command: /usr/bin/supervisord -c /etc/supervisord.conf
+
+ nginx:
+ image: nginx
+ restart: always
+ volumes:
+ - api-media:/home/api/media
+ - api-static:/home/api/static
+ - ./nginx/nginx.conf:/etc/nginx/nginx.conf
+ - ./nginx/sites-enabled/:/etc/nginx/sites-enabled
+ env_file:
+ - ./envs/env
+ ports:
+ - ${MCTRAINER_PORT:-8001}:8000
+ depends_on:
+ - medcattrainer
+ - solr
+
+ solr:
+ container_name: mct_solr
+ image: solr:8
+ restart: always
+ env_file:
+ - ./envs/env
+ ports:
+ - ${SOLR_PORT:-8983}:8983
+ volumes:
+ - solr-data:/var/solr
+ command:
+ - -cloud
+
+volumes:
+ api-media:
+ api-static:
+ api-db:
+ api-db-backup:
+ solr-data:
diff --git a/v1/medcat-trainer/docker-compose-mc0x.yml b/v1/medcat-trainer/docker-compose-mc0x.yml
new file mode 100644
index 000000000..14e8fa5f9
--- /dev/null
+++ b/v1/medcat-trainer/docker-compose-mc0x.yml
@@ -0,0 +1,35 @@
+version: '3.4'
+
+# Compose file yml for MedCAT v0.x models. This is a legacy compose services config for older MedCAT models.
+
+services:
+ medcattrainer:
+ container_name: medcattrainer
+ hostname: medcat
+ restart: always
+ image: cogstacksystems/medcat-trainer:mc-v0.x-latest
+ volumes:
+ - api-media:/home/api/media
+ - api-static:/home/api/static
+ - api-db:/home/api/db
+ expose:
+ - "8000"
+ env_file:
+ - ./envs/env-mc0x
+ command: /home/run.sh
+ nginx:
+ image: nginx
+ restart: always
+ volumes:
+ - api-media:/home/api/media
+ - api-static:/home/api/static
+ - ./nginx/nginx.conf:/etc/nginx/nginx.conf
+ - ./nginx/sites-enabled/:/etc/nginx/sites-enabled
+ ports:
+ - "${MCTRAINER_PORT:-8001}:8000"
+ depends_on:
+ - medcattrainer
+volumes:
+ api-media:
+ api-static:
+ api-db:
diff --git a/v1/medcat-trainer/docker-compose-prod.yml b/v1/medcat-trainer/docker-compose-prod.yml
new file mode 100644
index 000000000..ed60c7fcb
--- /dev/null
+++ b/v1/medcat-trainer/docker-compose-prod.yml
@@ -0,0 +1,57 @@
+# Prod compose file -
+
+services:
+ medcattrainer:
+ container_name: medcattrainer
+ hostname: medcat
+ restart: always
+ image: cogstacksystems/medcat-trainer:v2.22.1
+ volumes:
+ - api-media:/home/api/media
+ - api-static:/home/api/static
+ - api-db:/home/api/db
+ - api-db-backup:/home/api/db-backup
+ - ./supervisord.conf:/etc/supervisord.conf
+ - ./configs:/home/configs
+ env_file:
+ - ./envs/env-prod
+ environment:
+ - MCT_VERSION=v2.22.1
+ command: /usr/bin/supervisord -c /etc/supervisord.conf
+
+ nginx:
+ container_name: medcattrainer_nginx
+ image: nginx
+ restart: always
+ volumes:
+ - api-media:/home/api/media
+ - api-static:/home/api/static
+ - ./nginx/nginx.conf:/etc/nginx/nginx.conf
+ - ./nginx/sites-enabled/:/etc/nginx/sites-enabled
+ env_file:
+ - ./envs/env-prod
+ ports:
+ - "${MCTRAINER_PORT:-8001}:8000"
+ depends_on:
+ - medcattrainer
+ - solr
+
+ solr:
+ container_name: mct_solr
+ image: solr:8
+ restart: always
+ env_file:
+ - ./envs/env-prod
+ expose:
+ - "8983"
+ volumes:
+ - solr-data:/var/solr
+ command:
+ - -cloud # solr cloud launches
+
+volumes:
+ api-media:
+ api-static:
+ api-db:
+ api-db-backup:
+ solr-data:
diff --git a/v1/medcat-trainer/docker-compose.yml b/v1/medcat-trainer/docker-compose.yml
new file mode 100644
index 000000000..7fa61a286
--- /dev/null
+++ b/v1/medcat-trainer/docker-compose.yml
@@ -0,0 +1,56 @@
+# Default compose yml file - uses latest build of MedCATtrainer services. Default passwords and example
+# projects are not used.
+
+services:
+ # medattrainer services
+ medcattrainer:
+ image: cogstacksystems/medcat-trainer:v2.22.1
+ restart: always
+ volumes:
+ - api-media:/home/api/media
+ - api-static:/home/api/static
+ - api-db:/home/api/db
+ - api-db-backup:/home/api/db-backup
+ - ./configs:/home/configs
+ - ./supervisord.conf:/etc/supervisord.conf
+ env_file:
+ - ./envs/env
+ environment:
+ - MCT_VERSION=v2.22.1
+ command: /usr/bin/supervisord -c /etc/supervisord.conf
+
+ nginx:
+ image: nginx
+ restart: always
+ volumes:
+ - api-media:/home/api/media
+ - api-static:/home/api/static
+ - ./nginx/nginx.conf:/etc/nginx/nginx.conf
+ - ./nginx/sites-enabled/:/etc/nginx/sites-enabled
+ env_file:
+ - ./envs/env
+ ports:
+ - ${MCTRAINER_PORT:-8001}:8000
+ depends_on:
+ - medcattrainer
+ - solr
+
+ solr:
+ container_name: mct_solr
+ image: solr:8
+ restart: always
+ env_file:
+ - ./envs/env
+ ports:
+ - ${SOLR_PORT:-8983}:8983
+ volumes:
+ - solr-data:/var/solr
+ command:
+ - -cloud
+
+volumes:
+ api-media:
+ api-static:
+ api-db:
+ api-db-backup:
+ solr-data:
diff --git a/v1/medcat-trainer/docs/Makefile b/v1/medcat-trainer/docs/Makefile
new file mode 100644
index 000000000..d4bb2cbb9
--- /dev/null
+++ b/v1/medcat-trainer/docs/Makefile
@@ -0,0 +1,20 @@
+# Minimal makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line, and also
+# from the environment for the first two.
+SPHINXOPTS ?=
+SPHINXBUILD ?= sphinx-build
+SOURCEDIR = .
+BUILDDIR = _build
+
+# Put it first so that "make" without argument is like "make help".
+help:
+ @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+.PHONY: help Makefile
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+ @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/v1/medcat-trainer/docs/_static/css/overrides.css b/v1/medcat-trainer/docs/_static/css/overrides.css
new file mode 100644
index 000000000..efe2995d9
--- /dev/null
+++ b/v1/medcat-trainer/docs/_static/css/overrides.css
@@ -0,0 +1,24 @@
+
+.wy-side-nav-search {
+ background: #0072CE; /* NHS Blue */
+}
+
+.wy-side-scroll {
+ background: #E8EDEE; /* NHS Pale Grey */
+}
+
+.wy-menu-vertical a {
+ color: #212B32; /* primary text: NHS Dark Grey */
+}
+
+.wy-menu-vertical a:hover {
+ color: white;
+}
+
+.wy-side-nav-search .wy-dropdown>a img.logo, .wy-side-nav-search>a img.logo {
+ max-width: 50px;
+}
+
+.wy-nav-content {
+ max-width: 1200px !important;
+}
diff --git a/v1/medcat-trainer/docs/_static/img/add-annotation-concept-pick.png b/v1/medcat-trainer/docs/_static/img/add-annotation-concept-pick.png
new file mode 100644
index 0000000000000000000000000000000000000000..6fcacebf742e1db66ed525c18bc7b302d407dca5
GIT binary patch
literal 10221
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
literal 0
HcmV?d00001
diff --git a/v1/medcat-trainer/docs/_static/img/add-annotation-concept.png b/v1/medcat-trainer/docs/_static/img/add-annotation-concept.png
new file mode 100644
index 0000000000000000000000000000000000000000..64dedabb29c1a3d4d2fe3871a9bcfad525ace85d
GIT binary patch
literal 32984
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