fix(pfs,llm): descriptor stems trimmed to the code's own parenthetical (#700: G0502/G0503 no longer bridge into CCM); excerpts using a family acronym for something else are dropped (CCM = cardiac contractility modulation in the CY2027 NPRM) (refs #700 #691)
Some checks failed
CI / lint (push) Successful in 29s
CI / notebooks-smoke (push) Successful in 1m32s
Deploy / notebooks (push) Has been skipped
Deploy / zotero (push) Has been skipped
Deploy / docs (push) Has been skipped
Deploy / api (push) Has been skipped
Deploy / llm (push) Has been skipped
Deploy / mc (push) Has been skipped
Infra CI / notebooks (push) Successful in 1m2s
Infra CI / zotero (push) Successful in 15s
Infra CI / docs (push) Successful in 1m32s
Infra CI / api (push) Successful in 1m4s
Infra CI / llm (push) Successful in 55s
Infra CI / mc (push) Successful in 14s
Deploy / report (push) Successful in 13s
CI / test (push) Failing after 14m22s

This commit is contained in:
kert
2026-09-10 17:48:30 -04:00
parent 81797a54b5
commit 2de806de32
6 changed files with 254 additions and 7 deletions

View File

@@ -306,6 +306,37 @@ def _source(hit: Hit) -> dict:
return as_source(hit.metadata, hit.text, hit.score)
def _conflict_phrases(families: Sequence[str]) -> tuple[str, ...]:
"""Lower-case phrases that mark an excerpt as using one of
*families*' acronyms for something else (``Family.conflicts``)."""
from pfs.families import FAMILIES
out: list[str] = []
for key in families:
fam = FAMILIES.get(key)
if fam is not None:
out.extend(p.lower() for p in fam.conflicts)
return tuple(dict.fromkeys(out))
def _has_conflict(text: str, phrases: Sequence[str]) -> bool:
if not phrases or not text:
return False
low = text.lower()
return any(p in low for p in phrases)
def _drop_conflicts(
sources: list[dict], phrases: Sequence[str]
) -> tuple[list[dict], int]:
"""*sources* minus the ones whose snippet uses a detected family's
acronym for something else; the count dropped."""
if not phrases:
return sources, 0
kept = [s for s in sources if not _has_conflict(s.get("snippet", ""), phrases)]
return kept, len(sources) - len(kept)
def retrieve(
question: str,
*,
@@ -314,10 +345,15 @@ def retrieve(
since: str = "",
now: date | None = None,
mode: str = "auto",
exclude_phrases: Sequence[str] = (),
) -> list[dict]:
"""Top sources for ``question`` across all collections.
``since`` (ISO date) hard-filters to material dated on/after it.
``exclude_phrases`` (lower-case) drops any hit whose full chunk text
contains one of them before ranking — a detected family's acronym
conflicts (``Family.conflicts``); empty by default, so retrieval is
unchanged for a question that detects nothing.
``mode``: "recent" (default behaviour — similarity × recency blend,
byte-identical to before Task 4) or "timeline" (era-balanced —
@@ -332,9 +368,13 @@ def retrieve(
hits = filter_since(
_hits(vec, cfg=cfg, pool=pool, overfetch=cfg.timeline_overfetch), since
)
ranked = era_balance(hits, per_era=cfg.timeline_per_era, top_n=cfg.top_n)
else:
hits = filter_since(_hits(vec, cfg=cfg, pool=pool), since)
if exclude_phrases:
hits = [h for h in hits if not _has_conflict(h.text, exclude_phrases)]
if resolved == "timeline":
ranked = era_balance(hits, per_era=cfg.timeline_per_era, top_n=cfg.top_n)
else:
ranked = blend(
hits,
weight=cfg.recency_weight,
@@ -592,11 +632,26 @@ def stream_answer(
``{"type":"done"}``.
"""
resolved_mode = _resolve_mode(question, mode)
sources = retrieve(question, cfg=cfg, pool=pool, since=since, mode=mode)
n_retrieved = len(sources)
n_cited = n_lineage_sources = n_manual = 0
# Detection first: a detected family's acronym conflicts (CCM = chronic
# care management here, cardiac contractility modulation in the CY2027
# NPRM) must keep the conflicting excerpts out of retrieval. A codeless
# question detects nothing, so its retrieval is unchanged.
lineage = lineage_evidence(question, cfg)
evidence = valuation_evidence(question, cfg)
conflict_phrases = _conflict_phrases(
(evidence.families if evidence is not None else ())
+ (lineage.families if lineage is not None else ())
)
sources = retrieve(
question,
cfg=cfg,
pool=pool,
since=since,
mode=mode,
exclude_phrases=conflict_phrases,
)
n_retrieved = len(sources)
n_cited = n_lineage_sources = n_manual = 0
codes: tuple[str, ...] = evidence.codes if evidence is not None else ()
families: tuple[str, ...] = evidence.families if evidence is not None else ()
if lineage is not None:
@@ -644,6 +699,15 @@ def stream_answer(
before = len(sources)
sources = merge_sources(sources, _manual_sources(narrow_families, cfg))
n_manual = len(sources) - before
# Cited/lineage/manual rows come from code and family metadata, but a
# rule paragraph can still use the acronym for something else
# (its own text is what the model reads) — drop those too.
sources, n_conflict = _drop_conflicts(sources, conflict_phrases)
if n_conflict:
log.info(
"dropped %d excerpt(s) using a family acronym for something else",
n_conflict,
)
if lineage is not None:
yield lineage.payload()
if evidence is not None:

View File

@@ -69,11 +69,52 @@ def _stem_pattern(code: str) -> re.Pattern[str]:
return re.compile(rf"\b{re.escape(code)}\s*\(\s*(?!codes?\b)[A-Za-z]", re.I)
#: A descriptor stem is the code and the parenthetical that opens right
#: after it — never a whole background paragraph. #700: a 5,543-char
#: paragraph in the CY2027 NPRM recounting G0502/G0503's CY2017 descriptors
#: inline was absorbed wholesale as G0502's descriptor, yielding
#: self-referential addon-of rows and generic activity/population hits
#: that bridged the psychiatric CoCM codes into the CCM family.
MAX_STEM_CHARS = 1500
def descriptor_span(text: str, code: str, *, max_chars: int = MAX_STEM_CHARS) -> str:
"""The slice of *text* that is *code*'s own descriptor: from the code
through the close of the parenthetical that opens after it (nested
parens balanced), capped at *max_chars* — past the cap, cut at the
last sentence end. The stem match itself is required (``_stem_pattern``);
returns *text* unchanged when the code's stem is not in it."""
m = _stem_pattern(code.upper()).search(text)
if not m:
return text
start = m.start()
open_at = text.index("(", m.start())
depth = 0
end = None
for i in range(open_at, min(len(text), start + max_chars)):
ch = text[i]
if ch == "(":
depth += 1
elif ch == ")":
depth -= 1
if depth == 0:
end = i + 1
break
if end is None:
window = text[start : start + max_chars]
cut = max(window.rfind(". "), window.rfind(".\n"))
end = start + (cut + 1 if cut > 0 else len(window))
return text[start:end].strip()
def descriptor_runs(
store: Any, code: str, *, max_elements: int = 40
) -> list[DescriptorRun]:
"""Every FR paragraph that opens *code*'s descriptor, with the element
paragraphs that follow it (stops at the first prose paragraph)."""
paragraphs that follow it (stops at the first prose paragraph). The
stem paragraph is trimmed to the code's own descriptor span
(``descriptor_span``, #700) so a long paragraph that merely quotes
the descriptor inline contributes only the quoted descriptor."""
con = store._con()
pat = _stem_pattern(code.upper())
stems = con.execute(
@@ -86,7 +127,12 @@ def descriptor_runs(
for row in stems:
if not pat.search(row["text"]):
continue
stem = Para(row["item_key"], row["p_id"], row["page"], row["text"])
stem = Para(
row["item_key"],
row["p_id"],
row["page"],
descriptor_span(row["text"], code),
)
following = con.execute(
"SELECT p_id, page, text FROM fr_anchors WHERE item_key = ? AND p_id > ? "
"ORDER BY p_id LIMIT ?",

View File

@@ -73,6 +73,10 @@ class Family:
#: are matched by their curated ``synonyms`` instead and leave this
#: ``False``.
cpt: bool = False
#: Lower-case phrases that mark a text as using this family's acronym
#: for something else ("cardiac contractility modulation" for CCM);
#: the chat drops such excerpts from a question about this family.
conflicts: tuple[str, ...] = ()
HAND_FAMILIES: dict[str, Family] = {
@@ -87,6 +91,11 @@ HAND_FAMILIES: dict[str, Family] = {
"Chronic Care Management",
("99437", "99439", "99487", "99489", "99490", "99491"),
("ccm", "chronic care management"),
# "CCM" also names cardiac contractility modulation — the CY2027
# NPRM (91 FR 43842 ¶330–331) calls its new 33X01–33X11 codes
# "the CCM code family". Excerpts using the acronym that way are
# dropped from a chronic-care-management question (rag._drop_conflicts).
conflicts=("cardiac contractility modulation", "contractility modulation"),
),
"PCM": Family(
"PCM",

View File

@@ -22,6 +22,11 @@
# source's date year; comment: its docket id's year)
- id: ccm-history
# "CCM" in the CY2027 NPRM also names cardiac contractility modulation
# (33X01–33X11); a chronic-care-management answer must never drift there.
forbid:
- pattern: 'contractility'
unless_label: '^$'
question: What is the history of CCM coding and payment?
mode: timeline
expect_anchors:

View File

@@ -764,7 +764,7 @@ class TestStreamAnswer:
# no codes in the question — pgvector is never asked for cited rules
mock_engine.assert_not_called()
mock_retrieve.assert_called_once_with(
"q", cfg=CFG, pool=pool, since="", mode="auto"
"q", cfg=CFG, pool=pool, since="", mode="auto", exclude_phrases=()
)
assert events[0] == {"type": "token", "text": "Doc"}
assert events[1] == {"type": "token", "text": "tors"}
@@ -1244,3 +1244,60 @@ class TestManualSourcesWrapper:
assert rag._manual_sources(("CCM",), CFG) == []
assert "manual sources skipped" in caplog.text
cur.close.assert_called_once()
class TestAcronymConflicts:
"""CCM = chronic care management to the family registry, cardiac
contractility modulation to the CY2027 NPRM's "CCM code family";
excerpts using the acronym the other way are dropped."""
def test_phrases_come_from_the_family_registry(self):
from llm.rag import _conflict_phrases
phrases = _conflict_phrases(["CCM"])
assert "cardiac contractility modulation" in phrases
assert _conflict_phrases(["TCM", "NOPE"]) == ()
def test_drop_conflicts_on_snippets(self):
from llm.rag import _drop_conflicts
srcs = [
{
"label": "a",
"snippet": "the CCM code family for Cardiac Contractility Modulation",
},
{"label": "b", "snippet": "CCM services require a comprehensive care plan"},
]
kept, n = _drop_conflicts(srcs, ("cardiac contractility modulation",))
assert [s["label"] for s in kept] == ["b"] and n == 1
assert _drop_conflicts(srcs, ()) == (srcs, 0)
@patch("llm.rag.PoolEmbeddings")
@patch("llm.index.vectorstore")
def test_retrieve_excludes_conflicting_hits_before_ranking(self, mock_vs, mock_emb):
from llm.rag import retrieve
mock_emb.return_value.embed_query.return_value = [0.1]
hits = {
"rules": [
(
_doc(
"Cardiac contractility modulation (CCM) pulse generator",
item_key="X1",
),
0.1,
),
(_doc("CCM services: comprehensive care plan", item_key="X2"), 0.2),
]
}
mock_vs.side_effect = _stores(hits)
out = retrieve(
"q",
cfg=CFG,
pool=MagicMock(),
mode="recent",
exclude_phrases=("cardiac contractility modulation",),
)
assert [s["label"] for s in out] and all(
"contractility" not in s["snippet"].lower() for s in out
)

View File

@@ -227,3 +227,69 @@ class TestDuckDBSources:
(2015, "A", "Chron care mgmt srvc 20 min"),
(2022, "A", "Chrnc care mgmt staff 1st 20"),
]
class TestDescriptorSpan:
"""#700: a long background paragraph quoting a descriptor inline must
contribute only the quoted descriptor, not the whole paragraph."""
def test_trims_to_the_codes_parenthetical(self):
from pfs.descriptors import descriptor_span
text = (
"In the CY 2017 PFS final rule we created HCPCS code G0502 (Initial "
"psychiatric collaborative care management, first 70 minutes in the "
"first calendar month of behavioral health care manager activities "
"(in consultation with a psychiatric consultant)). "
+ "Background prose about chronic care management services, comprehensive "
"care plans and multiple chronic conditions expected to last 12 months. "
* 60
)
span = descriptor_span(text, "G0502")
assert span.startswith("G0502 (Initial psychiatric")
assert span.endswith("consultant))")
assert "chronic care management" not in span
assert len(span) < 300
def test_no_close_paren_cuts_at_a_sentence_inside_the_cap(self):
from pfs.descriptors import descriptor_span
text = "HCPCS code G0502 (Initial psychiatric care. " + "More words. " * 400
span = descriptor_span(text, "G0502", max_chars=200)
assert len(span) <= 200
assert span.endswith(".")
def test_no_stem_returns_the_text(self):
from pfs.descriptors import descriptor_span
assert descriptor_span("nothing about the code here", "G0502") == (
"nothing about the code here"
)
def test_descriptor_runs_stem_is_the_span(self, store):
from pfs.descriptors import descriptor_runs
con = store._con()
con.execute(
"INSERT INTO fr_anchors VALUES (?,?,?,?,?)",
(
"LONGPARA",
9,
1,
1,
"As background, HCPCS code G0502 (Initial psychiatric collaborative "
"care management, first 70 minutes) was created in CY 2017. "
+ "Chronic care management prose. "
* 100,
),
)
con.execute(
"INSERT INTO items (key, title, date_published) VALUES (?,?,?)",
("LONGPARA", "Medicare Program; CY 2027 PFS Proposed Rule", "2026-07-16"),
)
con.commit()
run = [r for r in descriptor_runs(store, "G0502") if r.item_key == "LONGPARA"][
0
]
assert run.stem.text.startswith("G0502 (Initial psychiatric")
assert "Chronic care management prose" not in run.stem.text