provinans / src /endopath /fields.py
reversely's picture
Upload folder using huggingface_hub
e163a2c verified
Raw
History Blame Contribute Delete
7.35 kB
"""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
}