File size: 7,354 Bytes
eea689d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 | """The CAP/ICCR endometrium checklist field list: the single source of truth.
Generated by scripts/generate_fields.py from the confirmed field-to-concept mapping
(endopath.mapping.cap_checklist_coverage, #95) and the taxonomy registry (endopath.taxonomy,
#35). Do not edit by hand: run `PYTHONPATH=src python3 scripts/generate_fields.py` and commit
the result. tests/test_field_generation.py fails when this file drifts from a fresh render.
Every consumer derives from `FIELDS` below: schema.py re-exports the enums, llm_extraction reads
FIELD_VALUE_SCHEMAS and ENUM_FIELDS, colpali_retrieval reads FIELD_QUERIES, and the frontend
renders the checklist from GET /api/fields (issue #37).
**Standard library only, deliberately.** `scripts/modal_precompute.py` runs on a Modal image
carrying only colpali-engine/transformers/torch/pillow/numpy, so it cannot import pydantic. Its
`@app.local_entrypoint` imports `FIELD_QUERIES` from here in the local process and passes it to the
GPU function as an argument. A test asserts this module stays import-clean; adding a third-party
import here silently breaks that job. (Note `endopath/__init__.py` imports schema, so the Modal
container must never import `endopath.fields` directly either.)
The value enums live here rather than in schema.py because a field's permitted values are part of
its definition, and schema.py's Pydantic models sit on top of that. schema.py re-exports them, so
`from endopath.schema import Histotype` still resolves.
Field *order* is meaningful: it is the order Case Review renders evidence cards.
Several value sets differ from how docs/taxonomy.md and the registry model the same concept. Those
differences are annotated on the fields below and reconciled separately: #81 (histotype, LVSI,
myometrial, cervical, nodal, and molecular value sets), #62 (pathologic_stage as a reported field
plus per-edition computed views), and #36 (the dedifferentiated/undifferentiated split).
"""
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
from typing import Any, Optional
class Histotype(str, Enum):
ENDOMETRIOID = "endometrioid"
MUCINOUS = "mucinous"
SEROUS = "serous"
CLEAR_CELL = "clear_cell"
CARCINOSARCOMA = "carcinosarcoma"
DEDIFFERENTIATED_UNDIFFERENTIATED = "dedifferentiated_undifferentiated"
MIXED = "mixed"
OTHER_NOS = "other_nos"
class LvsiStatus(str, Enum):
NOT_IDENTIFIED = "not_identified"
FOCAL = "focal"
SUBSTANTIAL = "substantial"
class MolecularClassification(str, Enum):
POLE_ULTRAMUTATED = "pole_ultramutated"
MMR_DEFICIENT = "mmr_deficient"
P53_ABNORMAL = "p53_abnormal"
NO_SPECIFIC_MOLECULAR_PROFILE = "no_specific_molecular_profile"
MULTIPLE_CLASSIFIER = "multiple_classifier"
def _enum_schema(enum_cls: type[Enum]) -> dict[str, Any]:
return {"type": "string", "enum": [member.value for member in enum_cls]}
@dataclass(frozen=True)
class FieldSpec:
"""One checklist field, in every form the codebase needs it.
`query` is the ColPali retrieval phrase, and the key under which that
phrase's precomputed vector is stored in data/embeddings/query_embeddings.npz.
Renaming `name` therefore invalidates that file; `tests/test_fields.py`
asserts the two agree so the break is loud rather than silent.
`value_schema` is JSON Schema, consumed directly as the LLM tool-use
parameter schema (llm_extraction.CHECKLIST_TOOL) and surfaced over
/api/fields so the frontend can render a dropdown instead of a text box.
`python_enum` is set only where the value coerces back to a Python Enum.
"""
name: str
label: str
query: str
value_schema: dict[str, Any]
python_enum: Optional[type[Enum]] = None
@property
def value_type(self) -> str:
return self.value_schema["type"]
@property
def enum_values(self) -> Optional[list[str]]:
return self.value_schema.get("enum")
FIELDS: tuple[FieldSpec, ...] = (
# Registry `histotype` splits dedifferentiated/undifferentiated, drops mucinous, and names
# the residual other_not_listed (not other_nos): #81, #36.
FieldSpec(
name="histologic_type",
label="Histologic type",
query="histologic type",
value_schema=_enum_schema(Histotype),
python_enum=Histotype,
),
# Registry `figo_grade` also carries the off-branch high_grade_non_endometrioid value; the
# CAP field presents only FIGO 1/2/3: #81.
FieldSpec(
name="histologic_grade",
label="Histologic grade (FIGO)",
query="FIGO grade",
value_schema={"type": "string", "enum": ["1", "2", "3"]},
),
# Registry stores invasion_depth and myometrial_thickness in mm and derives the percent
# (numbers stay numbers); the CAP field stores the percent directly: #81, #38.
FieldSpec(
name="myometrial_invasion_percent",
label="Myometrial invasion %",
query="myometrial invasion depth",
value_schema={"type": "number", "minimum": 0, "maximum": 100},
),
# Registry models cervical_stromal_invasion as coded present/absent/cannot_be_assessed; the
# CAP field is a boolean: #81.
FieldSpec(
name="cervical_stromal_invasion",
label="Cervical stromal invasion",
query="cervical stromal invasion",
value_schema={"type": "boolean"},
),
# Registry separates asserted lvsi_status and foci count from derived lvsi_extent
# (negative/focal/substantial); the CAP field stores the category with not_identified for
# negative: #81.
FieldSpec(
name="lymphovascular_space_invasion",
label="LVSI",
query="lymphovascular space invasion",
value_schema=_enum_schema(LvsiStatus),
python_enum=LvsiStatus,
),
# Registry models pn_category as a coded value set (pN0..pN2a); the CAP field is an open
# string: #81.
FieldSpec(
name="regional_lymph_node_status",
label="Regional lymph node status",
query="regional lymph node status",
value_schema={"type": "string"},
),
# Registry derives figo_stage per FIGO edition (derive_stage); the CAP field is one reported
# string. Split into a reported field plus computed views: #62.
FieldSpec(
name="pathologic_stage",
label="Pathologic stage",
query="pathologic stage",
value_schema={"type": "string"},
),
# Registry keeps promise_class and tcga_class as two dimensions; the CAP field mixes TCGA
# and ProMisE naming in one enum: #81.
FieldSpec(
name="molecular_classification",
label="Molecular classification",
query="molecular classification",
value_schema=_enum_schema(MolecularClassification),
python_enum=MolecularClassification,
),
)
BY_NAME: dict[str, FieldSpec] = {spec.name: spec for spec in FIELDS}
FIELD_NAMES: tuple[str, ...] = tuple(spec.name for spec in FIELDS)
FIELD_LABELS: dict[str, str] = {spec.name: spec.label for spec in FIELDS}
FIELD_QUERIES: dict[str, str] = {spec.name: spec.query for spec in FIELDS}
FIELD_VALUE_SCHEMAS: dict[str, dict[str, Any]] = {spec.name: spec.value_schema for spec in FIELDS}
ENUM_FIELDS: dict[str, type[Enum]] = {
spec.name: spec.python_enum for spec in FIELDS if spec.python_enum is not None
}
|