Skip to content
Draft
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
4 changes: 2 additions & 2 deletions canvas_generated/messages/effects_pb2.py

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions canvas_generated/messages/effects_pb2.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,12 @@ class EffectType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
UPDATE_PATIENT_FACILITY_ADDRESS: _ClassVar[EffectType]
DELETE_PATIENT_FACILITY_ADDRESS: _ClassVar[EffectType]
HOMEPAGE_CONFIGURATION: _ClassVar[EffectType]
LINK_DOCUMENT_TO_PATIENT: _ClassVar[EffectType]
CATEGORIZE_DOCUMENT: _ClassVar[EffectType]
ASSIGN_DOCUMENT_REVIEWER: _ClassVar[EffectType]
UPDATE_DOCUMENT_FIELDS: _ClassVar[EffectType]
JUNK_DOCUMENT: _ClassVar[EffectType]
REMOVE_DOCUMENT_FROM_PATIENT: _ClassVar[EffectType]
UNKNOWN_EFFECT: EffectType
LOG: EffectType
ADD_PLAN_COMMAND: EffectType
Expand Down Expand Up @@ -655,6 +661,12 @@ CREATE_PATIENT_FACILITY_ADDRESS: EffectType
UPDATE_PATIENT_FACILITY_ADDRESS: EffectType
DELETE_PATIENT_FACILITY_ADDRESS: EffectType
HOMEPAGE_CONFIGURATION: EffectType
LINK_DOCUMENT_TO_PATIENT: EffectType
CATEGORIZE_DOCUMENT: EffectType
ASSIGN_DOCUMENT_REVIEWER: EffectType
UPDATE_DOCUMENT_FIELDS: EffectType
JUNK_DOCUMENT: EffectType
REMOVE_DOCUMENT_FROM_PATIENT: EffectType

class Effect(_message.Message):
__slots__ = ("type", "payload", "plugin_name", "classname", "handler_name", "actor", "source")
Expand Down
4 changes: 2 additions & 2 deletions canvas_generated/messages/events_pb2.py

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions canvas_generated/messages/events_pb2.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -1059,6 +1059,13 @@ class EventType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
DOCUMENT_REFERENCE_CREATED: _ClassVar[EventType]
DOCUMENT_REFERENCE_UPDATED: _ClassVar[EventType]
DOCUMENT_REFERENCE_DELETED: _ClassVar[EventType]
DOCUMENT_RECEIVED: _ClassVar[EventType]
DOCUMENT_LINKED_TO_PATIENT: _ClassVar[EventType]
DOCUMENT_CATEGORIZED: _ClassVar[EventType]
DOCUMENT_REVIEWER_ASSIGNED: _ClassVar[EventType]
DOCUMENT_FIELDS_UPDATED: _ClassVar[EventType]
DOCUMENT_REVIEWED: _ClassVar[EventType]
DOCUMENT_DELETED: _ClassVar[EventType]
PANEL_SECTIONS_CONFIGURATION: _ClassVar[EventType]
REVENUE__PAYMENT_PROCESSOR__LIST: _ClassVar[EventType]
REVENUE__PAYMENT_PROCESSOR__CHARGE: _ClassVar[EventType]
Expand Down Expand Up @@ -2121,6 +2128,13 @@ LETTER_UPDATED: EventType
DOCUMENT_REFERENCE_CREATED: EventType
DOCUMENT_REFERENCE_UPDATED: EventType
DOCUMENT_REFERENCE_DELETED: EventType
DOCUMENT_RECEIVED: EventType
DOCUMENT_LINKED_TO_PATIENT: EventType
DOCUMENT_CATEGORIZED: EventType
DOCUMENT_REVIEWER_ASSIGNED: EventType
DOCUMENT_FIELDS_UPDATED: EventType
DOCUMENT_REVIEWED: EventType
DOCUMENT_DELETED: EventType
PANEL_SECTIONS_CONFIGURATION: EventType
REVENUE__PAYMENT_PROCESSOR__LIST: EventType
REVENUE__PAYMENT_PROCESSOR__CHARGE: EventType
Expand Down
27 changes: 27 additions & 0 deletions canvas_sdk/effects/data_integration/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from .assign_document_reviewer import AssignDocumentReviewer as AssignDocumentReviewer
from .assign_document_reviewer import Priority as Priority
from .assign_document_reviewer import ReviewMode as ReviewMode
from .categorize_document import CategorizeDocument as CategorizeDocument
from .junk_document import JunkDocument as JunkDocument
from .link_document_to_patient import LinkDocumentToPatient as LinkDocumentToPatient
from .prefill_document_fields import PrefillDocumentFieldData as PrefillDocumentFieldData
from .prefill_document_fields import PrefillDocumentFields as PrefillDocumentFields
from .prefill_document_fields import TemplateFields as TemplateFields
from .remove_document_from_patient import RemoveDocumentFromPatient as RemoveDocumentFromPatient
from .types import AnnotationItem as AnnotationItem
from .types import DocumentType as DocumentType

__all__ = __exports__ = (
"AnnotationItem",
"AssignDocumentReviewer",
"CategorizeDocument",
"DocumentType",
"PrefillDocumentFieldData",
"JunkDocument",
"LinkDocumentToPatient",
"PrefillDocumentFields",
"Priority",
"RemoveDocumentFromPatient",
"ReviewMode",
"TemplateFields",
)
121 changes: 121 additions & 0 deletions canvas_sdk/effects/data_integration/assign_document_reviewer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
from enum import StrEnum
from typing import Any

from canvas_sdk.effects.base import EffectType
from canvas_sdk.effects.data_integration.base import _BaseDocumentEffect


class Priority(StrEnum):
"""Priority levels for document review.

Maps to Boolean in database: HIGH=True, NORMAL=False.

Attributes:
NORMAL: Standard priority (default) - maps to False in database
HIGH: Elevated priority for time-sensitive documents - maps to True in database
"""

NORMAL = "normal"
HIGH = "high"


class ReviewMode(StrEnum):
"""Review mode for document review.

Maps to short codes in database: RR, AR, RN.

Attributes:
REVIEW_REQUIRED: Document requires active review and action (default) - maps to "RR"
ALREADY_REVIEWED: Document was already reviewed offline - maps to "AR"
REVIEW_NOT_REQUIRED: Document does not require review - maps to "RN"
"""

REVIEW_REQUIRED = "review_required"
ALREADY_REVIEWED = "already_reviewed"
REVIEW_NOT_REQUIRED = "review_not_required"

@property
def db_code(self) -> str:
"""Get database short code for this review mode."""
_DB_CODES = {
ReviewMode.REVIEW_REQUIRED: "RR",
ReviewMode.ALREADY_REVIEWED: "AR",
ReviewMode.REVIEW_NOT_REQUIRED: "RN",
}
return _DB_CODES[self]


class AssignDocumentReviewer(_BaseDocumentEffect):
"""
An Effect that assigns a staff member or team as reviewer to a document
in the Data Integration queue.

When processed by the home-app interpreter, this effect will:
- Validate the document (IntegrationTask) exists
- Validate the Staff exists if reviewer_id is provided
- Validate the Team exists if team_id is provided
- Assign the reviewer and/or team to the document with the specified priority and review_mode
- If both reviewer_id and team_id are provided, both will be assigned
- Create/update an IntegrationTaskPrefill record with field_type="reviewer"

This effect is typically emitted by LLM-based document processing plugins
and supports both initial reviewer assignment and reassignment.

Attributes:
document_id: The ID of the IntegrationTask document to assign a reviewer to (required).
Accepts str or int; always serialized as string in the payload.
Leading/trailing whitespace is stripped during serialization.
reviewer_id: Optional Staff key of the reviewer to assign.
Leading/trailing whitespace is stripped during serialization.
team_id: Optional Team UUID to assign.
Leading/trailing whitespace is stripped during serialization.
priority: Priority level for the review (normal, high). Defaults to normal.
review_mode: Review mode (review_required, already_reviewed, review_not_required).
Defaults to review_required.
annotations: Optional list of AnnotationItem objects to display with the reviewer prefill.
Each annotation has "text" and "color" attributes (e.g., [AnnotationItem(text="Team lead", color="#FF0000")]).
source_protocol: Optional identifier for the protocol/plugin that generated this effect.
Used for tracking and debugging (e.g., "llm_v1").
"""

class Meta:
effect_type = EffectType.ASSIGN_DOCUMENT_REVIEWER
apply_required_fields = ("document_id",)

reviewer_id: str | int | None = None
team_id: str | int | None = None
priority: Priority = Priority.NORMAL
review_mode: ReviewMode = ReviewMode.REVIEW_REQUIRED

@property
def values(self) -> dict[str, Any]:
"""The effect's values to be sent in the payload.

Strings are stripped of leading/trailing whitespace during serialization.
document_id is always converted to string.
priority is converted to boolean (HIGH=True, NORMAL=False).
review_mode is converted to database short codes (RR, AR, RN).
annotations is serialized as list of dicts with text and color keys.
source_protocol is stripped of leading/trailing whitespace.
"""
result: dict[str, Any] = {
"document_id": self._serialize_document_id(),
"priority": self.priority == Priority.HIGH,
"review_mode": self.review_mode.db_code,
}
if self.reviewer_id is not None:
result["reviewer_id"] = str(self.reviewer_id).strip()
if self.team_id is not None:
result["team_id"] = str(self.team_id).strip()
if self.annotations is not None:
result["annotations"] = self.annotations
if self.source_protocol is not None:
result["source_protocol"] = self._serialize_source_protocol()
return result


__exports__ = (
"AssignDocumentReviewer",
"Priority",
"ReviewMode",
)
74 changes: 74 additions & 0 deletions canvas_sdk/effects/data_integration/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
from typing import Any

from pydantic_core import InitErrorDetails

from canvas_sdk.effects.base import _BaseEffect
from canvas_sdk.effects.data_integration.types import AnnotationItem


class _BaseDocumentEffect(_BaseEffect):
"""
Base class for data integration effects that operate on documents.

Provides common functionality for:
- document_id field (required, str|int, serialized as stripped string)
- Optional source_protocol field
- Optional annotations field
- Validation for document_id non-empty
- Serialization helpers

Subclasses should:
- Define their own Meta.effect_type
- Define Meta.apply_required_fields (typically includes "document_id")
- Override values() to add additional fields
- Override _get_error_details() to add field-specific validations
"""

document_id: str | int | None = None
source_protocol: str | None = None
annotations: list[AnnotationItem] | None = None

def _serialize_document_id(self) -> str | None:
"""Serialize document_id to string with whitespace stripped."""
return str(self.document_id).strip() if self.document_id is not None else None

def _serialize_source_protocol(self) -> str | None:
"""Serialize source_protocol with whitespace stripped."""
return self.source_protocol.strip() if self.source_protocol is not None else None

def _validate_document_id(self) -> list[InitErrorDetails]:
"""Validate document_id is non-empty if provided as string."""
errors: list[InitErrorDetails] = []
if isinstance(self.document_id, str) and not self.document_id.strip():
errors.append(
self._create_error_detail(
"value_error",
"document_id must be a non-empty string",
self.document_id,
)
)
return errors

def _validate_non_empty_string(
self, field_name: str, field_value: str | None
) -> list[InitErrorDetails]:
"""Validate a string field is non-empty if provided."""
errors: list[InitErrorDetails] = []
if field_value is not None and not field_value.strip():
errors.append(
self._create_error_detail(
"value_error",
f"{field_name} must be a non-empty string",
field_value,
)
)
return errors

def _get_error_details(self, method: Any) -> list[InitErrorDetails]:
"""Validate common fields. Subclasses should extend this."""
errors = super()._get_error_details(method)
errors.extend(self._validate_document_id())
return errors


__exports__ = ()
Loading