Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions medcat-service/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,8 @@ The following environment variables are available for tailoring the MedCAT Servi
- `APP_BULK_NPROC` - the number of threads used in bulk processing (default: `8`),
- `APP_MEDCAT_MODEL_PACK` - MedCAT Model Pack path, if this parameter has a value IT WILL BE LOADED FIRST OVER EVERYTHING ELSE (CDB, Vocab, MetaCATs, etc.) declared above.
- `APP_ENABLE_METRICS` - Enable prometheus metrics collection served on the path /metrics
- `APP_ENABLE_DEMO_UI` - Enable the demo user interface to try models. (Default: `False`)
- `APP_DEMO_UI_PATH` - Customise the path of the demo UI. (Default: `/`)

### Shared Memory (`DOCKER_SHM_SIZE`)

Expand Down
3 changes: 3 additions & 0 deletions medcat-service/medcat_service/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ class Settings(BaseSettings):
description="Enable DEID redaction. Returns text like [***] instead of [ANNOTATION]",
)

enable_demo_ui: bool = Field(default=False, description="Enable the demo app", alias="APP_ENABLE_DEMO_UI")
demo_ui_path: str = Field(default="", description="Path to the demo app", alias="APP_DEMO_UI_PATH")

# Model paths
model_cdb_path: str | None = Field("/cat/models/medmen/cdb.dat", alias="APP_MODEL_CDB_PATH")
model_vocab_path: str | None = Field("/cat/models/medmen/vocab.dat", alias="APP_MODEL_VOCAB_PATH")
Expand Down
15 changes: 11 additions & 4 deletions medcat-service/medcat_service/demo/demo_logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,17 @@
"""

import logging
from typing import Any

from opentelemetry import trace
from pydantic import BaseModel

from medcat_service.dependencies import get_medcat_processor, get_settings
from medcat_service.types import ProcessAPIInputContent, ProcessErrorsResult, ProcessResult
from medcat_service.types_entities import Entity

logger = logging.getLogger(__name__)
tracer = trace.get_tracer("medcat_service")


class EntityAnnotation(BaseModel):
Expand Down Expand Up @@ -108,7 +111,9 @@ def convert_display_model_to_list_of_lists(entity_display_model: list[EntityAnno
]


def perform_named_entity_resolution(input_text: str, redact: bool | None = None):
def perform_named_entity_resolution(
input_text: str, redact: bool | None = None
) -> tuple[dict[str, Any], list[list[str]], str]:
"""
Performs clinical coding by processing the input text with MedCAT to extract and
annotate medical concepts (entities).
Expand All @@ -135,7 +140,7 @@ def perform_named_entity_resolution(input_text: str, redact: bool | None = None)
"""
logger.debug("Performing named entity resolution")
if not input_text or not input_text.strip():
return None, None, None
return {}, [], ""

processor = get_medcat_processor(get_settings())
input = ProcessAPIInputContent(text=input_text)
Expand All @@ -160,15 +165,17 @@ def perform_named_entity_resolution(input_text: str, redact: bool | None = None)
return response_tuple


def medcat_demo_perform_named_entity_resolution(input_text: str):
@tracer.start_as_current_span("medcat_demo_perform_named_entity_resolution")
def medcat_demo_perform_named_entity_resolution(input_text: str) -> tuple[dict[str, Any], list[list[str]]]:
"""
Performs named entity resolution for the MedCAT demo.
"""
result = perform_named_entity_resolution(input_text)
return result[0], result[1]


def anoncat_demo_perform_deidentification(input_text: str, redact: bool):
@tracer.start_as_current_span("anoncat_demo_perform_deidentification")
def anoncat_demo_perform_deidentification(input_text: str, redact: bool) -> tuple[dict[str, Any], list[list[str]], str]:
"""
Performs deidentification for the AnonCAT demo.
"""
Expand Down
141 changes: 79 additions & 62 deletions medcat-service/medcat_service/demo/gradio_demo.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import gradio as gr
import pandas as pd
from fastapi import FastAPI

import medcat_service.demo.demo_content as demo_content
from medcat_service.demo.demo_logic import (
Expand All @@ -22,7 +24,7 @@
annotation_details_placeholder_text = "Click on a highlighted entity to view its details"


def format_annotation_details(row, selected_text: str):
def format_annotation_details(row: pd.Series | None, selected_text: str) -> str:
"""Format a pandas Series row as markdown for display."""
if row is None:
return "**No annotation selected**\n\nClick on a highlighted entity to view its details."
Expand Down Expand Up @@ -52,7 +54,7 @@ def format_annotation_details(row, selected_text: str):
return details


def on_select(value, annotation_details, dataframe, evt: gr.SelectData):
def on_select_annotation(value, annotation_details: str, dataframe: pd.DataFrame, evt: gr.SelectData) -> str:
"""
On select of annotations in the highlighted text component.

Expand All @@ -71,42 +73,54 @@ def on_select(value, annotation_details, dataframe, evt: gr.SelectData):
return annotation_details_placeholder_text


if settings.deid_mode:
with gr.Blocks(title="AnonCAT Demo", fill_width=True) as io:
gr.Markdown("# AnonCAT Demo")
def output_details_interface() -> tuple[gr.HighlightedText, gr.Markdown, gr.Dataframe]:
"""
Output details interface for the demo.
Based on gradio Namd-Entity Recognition Demo
https://www.gradio.app/guides/named-entity-recognition
"""
highlighted = gr.HighlightedText(label="Processed Text", elem_id="highlighted-text-output", interactive=False)
annotation_details = gr.Markdown(label="Annotation Details", value=annotation_details_placeholder_text)
with gr.Accordion(label="All Annotations", open=False):
dataframe = gr.Dataframe(label="All Annotations", headers=headers, interactive=False, max_chars=50)

highlighted.select(on_select_annotation, [highlighted, annotation_details, dataframe], outputs=annotation_details)
return highlighted, annotation_details, dataframe


def anoncat_demo_interface() -> gr.Blocks:
def input_column():
with gr.Tab("Input"):
with gr.Group():
# Using a tab here just to make the input text box align with the output that is also tabbed
input_text = gr.Textbox(
label="Input Text", lines=3, placeholder="Enter some text and click Deidentify..."
)
redact = gr.Checkbox(label="Redact", info="Replace sensitive information with ****")
examples = gr.Examples( # noqa
examples=[demo_content.short_example, demo_content.anoncat_example],
inputs=input_text,
example_labels=["Short Example", "Note with personally identifiable information"],
)
with gr.Row():
clear_btn = gr.Button("Clear", variant="secondary")
annotate_btn = gr.Button("Deidentify", variant="primary")
return input_text, redact, clear_btn, annotate_btn

def output_column():
with gr.Tab("Deidentification"):
deidentified_text = gr.Textbox(label="Deidentified Text", value="", lines=3, interactive=False)
with gr.Tab("Details"):
highlighted, annotation_details, dataframe = output_details_interface()
return highlighted, dataframe, deidentified_text, annotation_details

with gr.Blocks(title="AnonCAT", fill_width=True) as io:
gr.Markdown("# AnonCAT")
with gr.Row():
with gr.Column(): # noqa
with gr.Tab("Input"):
input_text = gr.Textbox(
label="Input Text", lines=3, placeholder="Enter some text and click Deidentify..."
)
examples = gr.Examples(
examples=[demo_content.short_example, demo_content.anoncat_example],
inputs=input_text,
example_labels=["Short Example", "Note with personally identifiable information"],
)
redact = gr.Checkbox(label="Redact")
with gr.Row():
clear_btn = gr.Button("Clear", variant="secondary")
annotate_btn = gr.Button("Deidentify", variant="primary")

input_text, redact, clear_btn, annotate_btn = input_column()
with gr.Column():
with gr.Tab("Deidentification"):
deidentified_text = gr.Textbox(label="Deidentified Text", value="", interactive=False)
with gr.Tab("Details"):
highlighted = gr.HighlightedText(
label="Processed Text", elem_id="highlighted-text-output", interactive=False
)
annotation_details = gr.Markdown(
label="Annotation Details", value=annotation_details_placeholder_text
)
with gr.Accordion(label="All Annotations", open=False):
dataframe = gr.Dataframe(
label="All Annotations", headers=headers, interactive=False, max_chars=50
)

highlighted.select(on_select, [highlighted, annotation_details, dataframe], outputs=annotation_details)

highlighted, dataframe, deidentified_text, annotation_details = output_column()
annotate_btn.click(
anoncat_demo_perform_deidentification,
inputs=[input_text, redact],
Expand All @@ -119,35 +133,34 @@ def on_select(value, annotation_details, dataframe, evt: gr.SelectData):
outputs=[input_text, highlighted, dataframe, annotation_details],
)
gr.Markdown(demo_content.anoncat_help_content)
else:
with gr.Blocks(title="MedCAT Demo", fill_width=True) as io:
gr.Markdown("# MedCAT Demo")
return io


def medcat_demo_interface() -> gr.Blocks:
def input_column():
input_text = gr.Textbox(label="Input Text", lines=6, placeholder="Enter some text and click Annotate...")
with gr.Row():
examples = gr.Examples( # noqa
examples=[demo_content.short_example, demo_content.long_example, demo_content.anoncat_example],
inputs=input_text,
example_labels=[
"Short Example",
"Patient Discharge Summary in Neurology",
"Note with personally identifiable information",
],
)
with gr.Row():
clear_btn = gr.Button("Clear", variant="secondary")
annotate_btn = gr.Button("Annotate", variant="primary")
return input_text, clear_btn, annotate_btn

with gr.Blocks(title="MedCAT", fill_width=True) as io:
gr.Markdown("# MedCAT")
with gr.Row():
with gr.Column():
input_text = gr.Textbox(
label="Input Text", lines=6, placeholder="Enter some text and click Annotate..."
)
with gr.Row():
examples = gr.Examples(
examples=[demo_content.short_example, demo_content.long_example, demo_content.anoncat_example],
inputs=input_text,
example_labels=[
"Short Example",
"Patient Discharge Summary in Neurology",
"Note with personally identifiable information",
],
)
with gr.Row():
clear_btn = gr.Button("Clear", variant="secondary")
annotate_btn = gr.Button("Annotate", variant="primary")
input_text, clear_btn, annotate_btn = input_column()
with gr.Column():
highlighted = gr.HighlightedText(
label="Processed Text", elem_id="highlighted-text-output", interactive=False
)
annotation_details = gr.Markdown(label="Annotation Details", value=annotation_details_placeholder_text)
with gr.Accordion(label="All Annotations", open=False):
dataframe = gr.Dataframe(label="All Annotations", headers=headers, interactive=False, max_chars=50)
highlighted.select(on_select, [highlighted, annotation_details, dataframe], outputs=annotation_details)
highlighted, annotation_details, dataframe = output_details_interface()

annotate_btn.click(lambda: (annotation_details_placeholder_text), outputs=[annotation_details])
annotate_btn.click(
Expand All @@ -159,9 +172,10 @@ def on_select(value, annotation_details, dataframe, evt: gr.SelectData):
outputs=[input_text, highlighted, dataframe, annotation_details],
)
gr.Markdown(demo_content.article_footer)
return io


def mount_gradio_app(app, path: str = "/demo") -> None:
def mount_gradio_app(app: FastAPI, path: str) -> None:
"""
Mount the Gradio interface to the FastAPI app with a custom theme.

Expand All @@ -170,4 +184,7 @@ def mount_gradio_app(app, path: str = "/demo") -> None:
path: The path at which to mount the Gradio app (default: "/demo")
"""
theme = gr.themes.Default(primary_hue="blue", secondary_hue="teal")

io = anoncat_demo_interface() if settings.deid_mode else medcat_demo_interface()

gr.mount_gradio_app(app, io, path=path, theme=theme, css=highlighted_text_css)
4 changes: 3 additions & 1 deletion medcat-service/medcat_service/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@
app.include_router(health.router)
app.include_router(process.router)

mount_gradio_app(app, path="/demo")

if settings.enable_demo_ui:
mount_gradio_app(app, path=settings.demo_ui_path)


def configure_observability(settings: Settings, app: FastAPI):
Expand Down
16 changes: 8 additions & 8 deletions medcat-service/medcat_service/test/demo/test_demo_logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,10 @@ def test_perform_named_entity_resolution_with_empty_string(self, mock_get_proces
# Execute
result_dict, result_table, result_text = perform_named_entity_resolution("")

# Assert
self.assertIsNone(result_dict)
self.assertIsNone(result_table)
self.assertIsNone(result_text)
# Assert - should return empty objects, not None
self.assertEqual(result_dict, {})
self.assertEqual(result_table, [])
self.assertEqual(result_text, "")

@patch("medcat_service.demo.demo_logic.get_settings")
@patch("medcat_service.demo.demo_logic.get_medcat_processor")
Expand All @@ -113,10 +113,10 @@ def test_perform_named_entity_resolution_with_whitespace_only(self, mock_get_pro
# Execute
result_dict, result_table, result_text = perform_named_entity_resolution(" \n\t ")

# Assert
self.assertIsNone(result_dict)
self.assertIsNone(result_table)
self.assertIsNone(result_text)
# Assert - should return empty objects, not None
self.assertEqual(result_dict, {})
self.assertEqual(result_table, [])
self.assertEqual(result_text, "")

@patch("medcat_service.demo.demo_logic.get_settings")
@patch("medcat_service.demo.demo_logic.get_medcat_processor")
Expand Down
1 change: 1 addition & 0 deletions medcat-service/start_service_debug.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ if [ -z "${APP_MODEL_CDB_PATH}" ] && [ -z "${APP_MODEL_VOCAB_PATH}" ] && [ -z "$
fi

export APP_ENABLE_METRICS=${APP_ENABLE_METRICS:-True}
export APP_ENABLE_DEMO_UI=${APP_ENABLE_DEMO_UI:-True}

if [ "${HOT_MODULE_RELOADING}" = "True" ]; then
# Experimental: Hot module reloading. Need to `pip install -r requirements-dev.txt`
Expand Down
Loading