Skip to content
Open
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
16 changes: 16 additions & 0 deletions extensions/team-red-template/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[project]
authors = [
{email = "[email protected]", name = "Canvas Team"}
]
dependencies = [
"canvas[test-utils]"
]
description = "Some description of your project."
license = "MIT"
name = "team-red-template"
readme = "team_red_template/README.md"
requires-python = ">=3.11"
version = "0.1.0"

[tool.pytest.ini_options]
python_files = ["*_tests.py", "test_*.py", "tests.py"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"sdk_version": "0.1.4",
"plugin_version": "0.0.1",
"name": "team_red_template",
"description": "Auto-populates telehealth visit notes with detailed HPI and standard commands when reason for visit is entered",
"components": {
"protocols": [
{
"class": "team_red_template.protocols.telehealth_note_template:TelehealthNoteTemplate",
"description": "Automatically inserts detailed HPI with patient demographics and standard note commands (ROS, PHQ-9, Exam, Instruct) when a reason for visit is originated in a telehealth visit note",
"data_access": {
"event": "",
"read": [],
"write": []
}
}
],
"commands": [],
"content": [],
"effects": [],
"views": []
},
"secrets": [],
"tags": {},
"references": [],
"license": "",
"diagram": false,
"readme": "./README.md"
}
45 changes: 45 additions & 0 deletions extensions/team-red-template/team_red_template/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
team-red-template
=================

## Description

Automatically populates telehealth visit notes with a detailed History of Present Illness (HPI) and standard documentation commands when a structured reason for visit is entered.

This plugin streamlines telehealth documentation by auto-inserting a comprehensive note template that includes:
- **Detailed HPI** with patient demographics (name, age, DOB, sex at birth) and reason for visit with coding
- **Review of Systems** (blank)
- **PHQ-9 Mental Health Questionnaire**
- **Physical Exam** (blank)
- **Patient Instructions** (blank)

## Behavior

The plugin listens for the `REASON_FOR_VISIT_COMMAND__POST_ORIGINATE` event and automatically triggers when:
1. A reason for visit is entered in a note
2. The note type is "Telehealth visit" or "Telemedicine visit"
3. The note doesn't already have an HPI command (prevents duplicates)

### Example HPI Format

```
Jane Doe is a 35 year old female (DOB: 03/15/1989) who presents today for: Hypertension (SNOMED: 38341003)
```

## Installation

```bash
canvas install team-red-template
```

## Customization

To customize this plugin for your organization:

1. **Add/Modify Note Types**: Edit the `TELEHEALTH_NOTE_TYPES` tuple in `telehealth_note_template.py:33` to include additional note type names
2. **Change Commands**: Modify the `compute()` method to add, remove, or reorder commands
3. **Customize HPI Format**: Edit the narrative template in `telehealth_note_template.py:157-160`

### Important Note!

The CANVAS_MANIFEST.json is used when installing your plugin. Please ensure it
gets updated if you add, remove, or rename protocols.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
import arrow

from canvas_sdk.effects import Effect
from canvas_sdk.events import EventType
from canvas_sdk.protocols import BaseProtocol
from canvas_sdk.v1.data.note import Note
from canvas_sdk.v1.data import Command
from canvas_sdk.v1.data.questionnaire import Questionnaire
from canvas_sdk.commands import (
HistoryOfPresentIllnessCommand,
PhysicalExamCommand,
ReviewOfSystemsCommand,
InstructCommand,
QuestionnaireCommand,
)
from canvas_sdk.commands.constants import CodeSystems, Coding

from datetime import datetime
from dateutil.relativedelta import relativedelta
from logger import log


class TelehealthNoteTemplate(BaseProtocol):
"""
Protocol that automatically populates telehealth visit notes with a detailed HPI
and standard commands when a reason for visit is originated.

Triggers on: REASON_FOR_VISIT_COMMAND__POST_ORIGINATE

Inserts:
- History of Present Illness with patient name, age, DOB, sex, and reason for visit
- Review of Systems (blank)
- PHQ-9 Questionnaire
- Physical Exam (blank)
- Instruct (blank)
"""

RESPONDS_TO = EventType.Name(EventType.REASON_FOR_VISIT_COMMAND__POST_ORIGINATE)

# Note types that should trigger this template
TELEHEALTH_NOTE_TYPES = (
'Telehealth visit',
'Telemedicine visit',
)

def calculate_age(self, patient):
"""Calculate the age in years/months of the patient"""
dob = patient.birth_date
difference = relativedelta(arrow.now().date(), dob)
months_old = difference.years * 12 + difference.months

if months_old < 18:
return f"{months_old} month"

return f"{difference.years} year"

def get_sex(self, patient):
"""Map the sex coding value to user friendly string"""
sex = patient.sex_at_birth

_map = {
"F": "female",
"M": "male",
"O": "other",
"UNK": "unknown"
}

return _map.get(sex, "unknown")

def format_date(self, date_obj):
"""Format date as MM/DD/YYYY"""
if date_obj:
return date_obj.strftime("%m/%d/%Y")
return "Unknown"

def get_rfv_display(self):
"""
Extract the reason for visit display text and coding from the event context.
Returns a formatted string with the RFV information.
"""
try:
# Get coding data from the context
coding_data = self.context.get("fields", {}).get("coding", {})
comment = self.context.get("fields", {}).get("comment", "")

# Extract display text and code
display_text = coding_data.get("text", "") or coding_data.get("display", "")
code = coding_data.get("value", "") or coding_data.get("code", "")
system = coding_data.get("system", "")

# Build the RFV string
if display_text:
if code and system:
# Extract just the system name (e.g., "SNOMED" from full URL)
system_name = system.split('/')[-1] if '/' in system else system
return f"{display_text} ({system_name}: {code})"
else:
return display_text
elif comment:
return comment
else:
return "[Reason for visit - please specify]"
except Exception as e:
log.error(f"Error extracting RFV display: {str(e)}")
return "[Reason for visit - please specify]"

def note_has_hpi(self, note):
"""
Check if the note already has an HPI command to avoid duplicates
"""
hpi_commands = note.commands.filter(schema_key="historyOfPresentIllness")
return hpi_commands.exists()

def is_telehealth_note(self, note):
"""
Check if the note type is a telehealth visit
"""
note_type_name = note.note_type_version.name
return note_type_name in self.TELEHEALTH_NOTE_TYPES

def compute(self) -> list[Effect]:
"""
This method gets called when a REASON_FOR_VISIT_COMMAND__POST_ORIGINATE event fires.

It will:
1. Verify this is a telehealth visit note
2. Check if HPI already exists (avoid duplicates)
3. Get patient demographics and calculate age
4. Extract reason for visit information
5. Insert detailed HPI with patient info and RFV
6. Insert blank ROS, PHQ-9, Physical Exam, and Instruct commands
"""

# Get the note from the context
note_uuid = self.context.get("note", {}).get("uuid")
note_id = self.context.get("note", {}).get("id")

if not note_uuid or not note_id:
log.error("No note UUID or ID found in context")
return []

log.info(f"Reason for Visit originated in note {note_id}")

# Get the full note object
note = Note.objects.get(id=note_uuid)

# Check if this is a telehealth visit note
if not self.is_telehealth_note(note):
log.info(f"Note type '{note.note_type_version.name}' is not a telehealth visit. Skipping template.")
return []

# Check if HPI already exists
if self.note_has_hpi(note):
log.info(f"HPI already exists in note {note_id}. Skipping template insertion.")
return []

log.info(f"Inserting telehealth note template for note {note_id}")

# Get patient information
patient = note.patient
patient_name = f"{patient.first_name} {patient.last_name}"
age = self.calculate_age(patient)
sex = self.get_sex(patient)
dob = self.format_date(patient.birth_date)

# Get reason for visit display
rfv_display = self.get_rfv_display()

# Build the HPI narrative
hpi_narrative = (
f"{patient_name} is a {age} year old {sex} "
f"(DOB: {dob}) who presents today for: {rfv_display}"
)

log.info(f"Creating HPI: {hpi_narrative}")

# Create History of Present Illness with detailed narrative
hpi = HistoryOfPresentIllnessCommand(
note_uuid=note_uuid,
narrative=hpi_narrative
)

# Create Review of Systems (blank)
ros = ReviewOfSystemsCommand(note_uuid=note_uuid)

# Create PHQ-9 Questionnaire
effects = []
try:
phq9_questionnaire = Questionnaire.objects.get(
code="44249-1", # PHQ-9 LOINC code
code_system="LOINC",
can_originate_in_charting=True
)
phq9 = QuestionnaireCommand(
note_uuid=note_uuid,
questionnaire_id=str(phq9_questionnaire.id)
)
phq9_effect = phq9.originate()
log.info("PHQ-9 questionnaire command created")
except Exception as e:
log.error(f"Error creating PHQ-9 questionnaire: {str(e)}")
phq9_effect = None

# Create Physical Exam (blank)
exam = PhysicalExamCommand(note_uuid=note_uuid)

# Create Instruct command (blank/unstructured)
instruct = InstructCommand(
note_uuid=note_uuid,
coding=Coding(
system=CodeSystems.UNSTRUCTURED,
code=""
)
)

# Build effects list
effects = [
hpi.originate(),
ros.originate(),
]

if phq9_effect:
effects.append(phq9_effect)

effects.extend([
exam.originate(),
instruct.originate()
])

log.info(f"Inserted {len(effects)} commands into note {note_id}")

return effects