Spaces:
Running
Running
File size: 9,735 Bytes
1ddeb51 | 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 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 | """Align optional time-spent-with-patient to specialty duration rules.
Paediatrics (26) has hard Schedule minima for special / extended /
neurodevelopmental consults and developmental K-units. Family Practice (00)
assessments are content-defined (not hard minute floors); time spent only
steers BM25 toward the matching office ladder rung.
When time spent is omitted, no duration filtering is applied.
"""
from __future__ import annotations
import re
from dataclasses import dataclass
from .specialty_registry import normalize_specialty_code
# Hard minimum direct-contact minutes from PAEDIATRICS (26) / GENERAL LISTINGS.
_MIN_MINUTES_BY_CODE: dict[str, int] = {
# Special paediatric consultation
"A260": 75,
"C260": 75,
"W260": 75,
# Extended special paediatric consultation
"A662": 90,
"C662": 90,
"W662": 90,
# Neurodevelopmental consultation
"A667": 90,
"A695": 90,
"C667": 90,
"C695": 90,
"W667": 90,
"W695": 90,
# Developmental / behavioural care β first unit (β₯ 20 min of a 30-min unit)
"K122": 20,
"K123": 20,
}
# Description cues for codes whose indexed text mentions a minute floor but
# may not be in the allowlist above (future SoB additions).
_DESC_MIN_PATTERNS: list[tuple[re.Pattern[str], int]] = [
(
re.compile(
r"minimum\s+of\s+ninety\s+minutes|minimum\s+of\s+90\s+minutes|"
r"spends?\s+a\s+minimum\s+of\s+90\s+minutes",
re.I,
),
90,
),
(
re.compile(
r"minimum\s+of\s+seventy[- ]?five\s+minutes|"
r"minimum\s+of\s+75\s+minutes|"
r"spends?\s+a\s+minimum\s+of\s+75\s+minutes",
re.I,
),
75,
),
(
re.compile(
r"minimum\s+of\s+fifty\s+minutes|minimum\s+of\s+50\s+minutes",
re.I,
),
50,
),
(
re.compile(r"\bper\s+unit\b", re.I),
20, # GP Β½-hour unit services: first unit needs β₯ 20 minutes
),
]
@dataclass(frozen=True)
class DurationAlignment:
minutes: int
label: str
keywords: str
def align_time_spent(
minutes: int | None,
specialty_code: str | None = None,
) -> DurationAlignment | None:
"""Build steering keywords / label for clinician-reported time with patient."""
if minutes is None or minutes < 0:
return None
specialty = normalize_specialty_code(specialty_code)
if specialty == "00":
return _align_time_spent_family_practice(minutes)
return _align_time_spent_paediatrics(minutes)
def _align_time_spent_family_practice(minutes: int) -> DurationAlignment:
"""Soft ladder steer for FP β no invented hard minute floors."""
if minutes < 15:
label = f"{minutes} minutes with patient (brief)"
keywords = (
f"brief minor assessment A001 mini assessment A008 "
f"Family Practice {minutes} minutes"
)
elif minutes < 35:
label = f"{minutes} minutes with patient"
keywords = (
f"intermediate assessment A007 minor assessment A001 "
f"Family Practice {minutes} minutes"
)
else:
label = f"{minutes} minutes with patient"
keywords = (
f"general assessment A003 intermediate assessment A007 "
f"periodic health K017 Family Practice {minutes} minutes"
)
return DurationAlignment(minutes=minutes, label=label, keywords=keywords)
def _align_time_spent_paediatrics(minutes: int) -> DurationAlignment:
if minutes < 20:
label = f"{minutes} minutes with patient (brief)"
keywords = (
"brief level 1 paediatric assessment short visit "
f"{minutes} minutes"
)
elif minutes < 75:
label = f"{minutes} minutes with patient"
keywords = (
"level 1 level 2 paediatric assessment medical specific "
f"consultation {minutes} minutes direct contact"
)
elif minutes < 90:
label = f"{minutes} minutes with patient (special consult eligible)"
keywords = (
"special paediatric consultation minimum 75 minutes "
f"level 2 medical specific {minutes} minutes direct contact"
)
else:
label = f"{minutes} minutes with patient (extended/neurodevelopmental eligible)"
keywords = (
"extended special paediatric consultation neurodevelopmental "
"consultation minimum 90 minutes developmental behavioural care "
f"{minutes} minutes direct contact"
)
return DurationAlignment(minutes=minutes, label=label, keywords=keywords)
def code_min_minutes(doc: dict) -> int | None:
"""Return the Schedule minimum direct-contact minutes for this code, if any."""
code = (doc.get("billing_code") or "").upper()
if code in _MIN_MINUTES_BY_CODE:
return _MIN_MINUTES_BY_CODE[code]
text = " ".join(
[
doc.get("description_text") or "",
doc.get("description") or "",
doc.get("rules_and_constraints") or "",
]
)
if not text.strip():
return None
for pattern, mins in _DESC_MIN_PATTERNS:
if pattern.search(text):
return mins
return None
def code_applies_to_duration(doc: dict, minutes: int) -> bool:
"""True if this code is eligible given time spent with the patient.
Codes without a Schedule minimum always apply. Minimum-time services are
eligible only when ``minutes`` meets or exceeds their floor.
"""
required = code_min_minutes(doc)
if required is None:
return True
return minutes >= required
def filter_docs_for_duration(docs: list[dict], minutes: int) -> list[dict]:
"""Drop minimum-time codes the reported duration cannot support."""
return [d for d in docs if code_applies_to_duration(d, minutes)]
def k_units_for_minutes(minutes: int) -> int:
"""How many Β½-hour developmental/behavioural units the time supports.
General Preamble table (30-minute units, major part thereof):
1β20, 2β46, 3β76, 4β106, 5β136, 6β166, 7β196, 8β226 minutes.
"""
thresholds = (20, 46, 76, 106, 136, 166, 196, 226)
units = 0
for i, need in enumerate(thresholds, start=1):
if minutes >= need:
units = i
else:
break
return units
def duration_hint_for_llm(
align: DurationAlignment,
specialty_code: str | None = None,
) -> str:
"""Clinician-facing / LLM hint describing duration eligibility."""
specialty = normalize_specialty_code(specialty_code)
if specialty == "00":
return _duration_hint_family_practice(align)
return _duration_hint_paediatrics(align)
def _duration_hint_family_practice(align: DurationAlignment) -> str:
mins = align.minutes
lines = [
f"{align.label}.",
"Family Practice office assessments are content-defined (not hard "
"minute floors). Prefer the ladder rung that matches the documented "
"service: mini A008 β minor A001 β intermediate A007 β general A003 "
"β re-assessment A004 / periodic health K017.",
]
if mins < 15:
lines.append(
"Brief contact β prefer minor A001 or mini A008 unless the note "
"documents a fuller intermediate or general assessment."
)
elif mins < 35:
lines.append(
"Prefer intermediate A007 or minor A001; do not recommend general "
"assessment A003 / periodic K017 unless the note documents those "
"elements."
)
else:
lines.append(
"Longer visit β general assessment A003 or periodic health K017 "
"may apply when the note documents those elements."
)
return " ".join(lines)
def _duration_hint_paediatrics(align: DurationAlignment) -> str:
mins = align.minutes
lines = [
f"{align.label}.",
"Only recommend Paediatrics codes whose Schedule minimum direct-contact "
"time is met (or that have no minimum-time requirement).",
"Hard floors from the current Schedule:",
" β’ Special paediatric consultation (A260/C260/W260): β₯ 75 minutes",
" β’ Extended special paediatric consultation (A662/C662/W662): β₯ 90 minutes",
" β’ Neurodevelopmental consultation (A667/A695 and setting mirrors): β₯ 90 minutes",
" β’ Developmental/behavioural care (K122/K123): β₯ 20 minutes for 1 unit",
]
units = k_units_for_minutes(mins)
if units:
lines.append(
f"Reported time supports up to {units} K122/K123 unit(s) "
"(Β½ hour or major part thereof)."
)
else:
lines.append(
"Reported time is below 20 minutes β do not recommend K122/K123 "
"unit services."
)
if mins < 75:
lines.append(
"Do not recommend special / extended / neurodevelopmental "
"consultations β time spent is below their Schedule minima."
)
elif mins < 90:
lines.append(
"Special paediatric consultation (β₯ 75 min) may apply; extended "
"special and neurodevelopmental consultations still require β₯ 90 min."
)
else:
lines.append(
"Time spent meets minima for special, extended special, and "
"neurodevelopmental consultations when other elements are met."
)
if mins < 20:
lines.append(
"Prefer Level 1 paediatric assessment (A261) for brief encounters "
"unless the note clearly documents a more extensive Level 2 exam."
)
return " ".join(lines)
|