"""Locations, identifiers, models, question counts, progress reporting and the term glossary shared by the stages. build_patient.sh sets MEMEBENCH_WORK (the patient's working directory), MEMEBENCH_PATIENT (the patient's number in the visit table), the per-patient facts and the model names. The visit table (the synthetic timeline) and the term glossary are not distributed (MEMEBENCH_VISIT_TABLE, MEMEBENCH_GLOSSARY). """ from __future__ import annotations import io import json import os import re import time from functools import lru_cache from pathlib import Path VISIT_TABLE = Path(os.environ.get("MEMEBENCH_VISIT_TABLE", "")) GLOSSARY = Path(os.environ.get("MEMEBENCH_GLOSSARY", "")) SEX_MAP = {"0": "female", "1": "male"} DEFAULT_PDESC = "longitudinal multi-department history" FIELDS = ["diagnosis", "patient_self_description", "tests_and_medication", "doctor_notes", "doctor_plans", "health_advice", "doctor_extra_notes"] # Question counts: the minimum of record questions on the axes that get synthetic questions (3, 5, 6, 7), and the # sizes asked of steps axis1/axis2. PER_AXIS_MINIMUM = {3: 3, 5: 10, 6: 10, 7: 10} AXIS1_TARGET = "8-10" AXIS1_FORM_MIX = "5 form=range + 3 form=never_again" AXIS2_TARGET = "4" def _required(name: str) -> str: value = os.environ.get(name, "") if not value or value.startswith("<"): raise SystemExit(f"{name} is not set; run the stages through build_patient.sh") return value BASE = Path(_required("MEMEBENCH_WORK")) def patient_id() -> int: """The patient's number in the visit table.""" return int(_required("MEMEBENCH_PATIENT")) def model(role: str) -> str: """The model configured for a role: extraction, generation or dialogue.""" return _required(f"MEMEBENCH_{role.upper()}_MODEL") def no_memory_exempt_axes() -> set: """Axis numbers whose questions are kept regardless of the no-memory probe (MEMEBENCH_NO_MEMORY_EXEMPT_AXES).""" if "MEMEBENCH_NO_MEMORY_EXEMPT_AXES" not in os.environ: raise SystemExit("MEMEBENCH_NO_MEMORY_EXEMPT_AXES is not set; run the stages through build_patient.sh") raw = os.environ["MEMEBENCH_NO_MEMORY_EXEMPT_AXES"] return {int(x) for x in raw.split(",") if x.strip()} def _sex_str() -> str: """The patient's sex from the visit table (0 = female, 1 = male), for the synthetic-atom prompts.""" patient = patient_id() try: import pandas as pd df = pd.read_parquet(VISIT_TABLE, columns=["record_id", "sex"]) v = str(df[df["record_id"] == patient]["sex"].dropna().iloc[0]).strip() except Exception as e: raise RuntimeError(f"cannot read the patient's sex from {VISIT_TABLE}: {e}") from e if v not in SEX_MAP: raise RuntimeError(f"sex value {v!r} of the patient in {VISIT_TABLE} is not one of {sorted(SEX_MAP)}") return SEX_MAP[v] @lru_cache(maxsize=None) def patient_line() -> str: """The one-line patient description given to the synthetic-atom prompts.""" pdesc = os.environ.get("MEMEBENCH_PDESC", DEFAULT_PDESC) return f"Patient ({_sex_str()}): {pdesc}." def norm_value(v) -> str: """A timeline value for comparison: lower case, parentheses and a leading 'history of' removed.""" v = re.sub(r"\s*\([^)]*\)", "", str(v or "")).strip().lower() v = re.sub(r"^(?:history of|hx of|h/o|known)\s+", "", v) return re.sub(r"\s+", " ", v).strip(" .;,") def other_department_segments(timelines, entity, department): """Segments of the timelines that other departments keep for the same entity (name compared ignoring case and spacing).""" key = re.sub(r"\s+", " ", str(entity or "").strip().lower()) return [s for t in timelines if t.get("department") != department and re.sub(r"\s+", " ", str(t.get("entity") or "").strip().lower()) == key for s in t.get("segments") or []] def has_text(q, key="question") -> bool: """Whether a model reply carries a non-empty string under `key`.""" return isinstance(q, dict) and isinstance(q.get(key), str) and bool(q[key].strip()) def lists_options(question, options) -> bool: """Whether a question text already lists its options: every option label appears as an item marker, '