fix(llm,pfs): I5/M1/M2 — probe the model pool once per classifier, strip FR citations before lineage pattern matching, drop unused extract_text param
I5: `closed_vocab_classifier` called `pool.check(cfg.instruct_model)` on every `classify()` call — a host-liveness probe, not per-request work. A code with hundreds of element lines re-probed the pool hundreds of times. Move the check to the closure's construction. M1: `fr_events` matched event patterns (crosswalk, replaces, …) against the raw FR paragraph text, so a citation like "91 FR 99490" inside a "crosswalk ... 91 FR 99490" sentence read as a real crosswalk event for 99490 — the citation's page number is not a mention of the code. Strip FR citations with the now-public `pfs.families.FR_CITE_RE` before both the presence check and every event pattern. M2: `extract_text`'s `classify` parameter was unused; no call site passed it. Drop it. Claude-Session: https://claude.ai/code/session_01Aum3pEMAM3yQVdFSdVe6Gc
This commit is contained in:
@@ -56,11 +56,15 @@ def _default_post(url: str, json: dict) -> dict:
|
|||||||
def closed_vocab_classifier(
|
def closed_vocab_classifier(
|
||||||
cfg: Any, pool: Any, *, post: Callable[..., dict] | None = None
|
cfg: Any, pool: Any, *, post: Callable[..., dict] | None = None
|
||||||
) -> Classifier:
|
) -> Classifier:
|
||||||
"""Return ``classify(text, choices) -> slug | None`` bound to *pool*."""
|
"""Return ``classify(text, choices) -> slug | None`` bound to *pool*.
|
||||||
|
|
||||||
|
``pool.check`` runs once here, not per call (I5) — it's a host-liveness
|
||||||
|
probe, not per-request work, and a code with hundreds of element lines
|
||||||
|
used to re-probe the pool hundreds of times."""
|
||||||
send = post or _default_post
|
send = post or _default_post
|
||||||
|
pool.check(cfg.instruct_model)
|
||||||
|
|
||||||
def classify(text: str, choices: Sequence[str]) -> str | None:
|
def classify(text: str, choices: Sequence[str]) -> str | None:
|
||||||
pool.check(cfg.instruct_model)
|
|
||||||
with pool.acquire_generation() as host:
|
with pool.acquire_generation() as host:
|
||||||
model = pick_model(cfg, pool, host)
|
model = pick_model(cfg, pool, host)
|
||||||
data = send(
|
data = send(
|
||||||
|
|||||||
@@ -80,9 +80,7 @@ def extract_run(
|
|||||||
return Extraction(run.code, tuple(ordered), tuple(reviews))
|
return Extraction(run.code, tuple(ordered), tuple(reviews))
|
||||||
|
|
||||||
|
|
||||||
def extract_text(
|
def extract_text(code: str, text: str, *, year: int, source: str) -> Extraction:
|
||||||
code: str, text: str, *, year: int, source: str, classify: Classifier | None = None
|
|
||||||
) -> Extraction:
|
|
||||||
rows = [
|
rows = [
|
||||||
ElementRow(code, year, e.type.value, e.value, e.detail, text, "", 0, 0, source)
|
ElementRow(code, year, e.type.value, e.value, e.detail, text, "", 0, 0, source)
|
||||||
for e in parse_descriptor(text)
|
for e in parse_descriptor(text)
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ from typing import Any
|
|||||||
|
|
||||||
from pfs.codetables import EventRow
|
from pfs.codetables import EventRow
|
||||||
from pfs.descriptors import rule_year_of
|
from pfs.descriptors import rule_year_of
|
||||||
from pfs.families import find_codes
|
from pfs.families import FR_CITE_RE, find_codes
|
||||||
|
|
||||||
RVU_KINDS = (
|
RVU_KINDS = (
|
||||||
"appeared",
|
"appeared",
|
||||||
@@ -199,6 +199,11 @@ def fr_events(store: Any, code: str) -> list[EventRow]:
|
|||||||
out: list[EventRow] = []
|
out: list[EventRow] = []
|
||||||
for r in rows:
|
for r in rows:
|
||||||
text = _expand_ranges(r["text"])
|
text = _expand_ranges(r["text"])
|
||||||
|
# An FR page citation's number ("91 FR 99490") is not a mention of
|
||||||
|
# the code — strip it before both the presence check and every
|
||||||
|
# event pattern below (M1: a "crosswalk ... 91 FR 99490" sentence
|
||||||
|
# used to read as a crosswalk event for 99490).
|
||||||
|
text = FR_CITE_RE.sub(" ", text)
|
||||||
if code not in text.upper():
|
if code not in text.upper():
|
||||||
continue
|
continue
|
||||||
year = rule_year_of(r["title"], r["date_published"])
|
year = rule_year_of(r["title"], r["date_published"])
|
||||||
|
|||||||
@@ -93,3 +93,14 @@ class TestClassifier:
|
|||||||
post=lambda url, json: {"message": {"content": "maybe consent?"}},
|
post=lambda url, json: {"message": {"content": "maybe consent?"}},
|
||||||
)
|
)
|
||||||
assert classify("x", ["consent"]) is None
|
assert classify("x", ["consent"]) is None
|
||||||
|
|
||||||
|
def test_pool_check_runs_once_not_per_call(self):
|
||||||
|
# I5: pool.check is a host-liveness probe, done once when the
|
||||||
|
# classifier closure is built — not per classify() call.
|
||||||
|
pool = _Pool()
|
||||||
|
classify = closed_vocab_classifier(
|
||||||
|
_Cfg(), pool, post=lambda url, json: {"message": {"content": "consent"}}
|
||||||
|
)
|
||||||
|
classify("a", ["consent"])
|
||||||
|
classify("b", ["consent"])
|
||||||
|
assert pool.checked == ["qwen2.5:14b"]
|
||||||
|
|||||||
@@ -189,6 +189,23 @@ class TestFrEvents:
|
|||||||
kinds = {e.kind for e in fr_events(store, "99441")}
|
kinds = {e.kind for e in fr_events(store, "99441")}
|
||||||
assert {"deleted", "bundled", "telehealth_list"} <= kinds
|
assert {"deleted", "bundled", "telehealth_list"} <= kinds
|
||||||
|
|
||||||
|
def test_fr_citation_page_number_is_not_a_crosswalk_mention(self, store):
|
||||||
|
# M1: an FR page citation ("91 FR 99490") is not a real mention of
|
||||||
|
# 99490 — it must be stripped before pattern matching, or a
|
||||||
|
# "crosswalk ... 91 FR 99490" sentence reads as a crosswalk event.
|
||||||
|
store.con.execute(
|
||||||
|
"INSERT INTO fr_anchors VALUES (?,?,?,?,?)",
|
||||||
|
(
|
||||||
|
"DE2VH9PD",
|
||||||
|
1500,
|
||||||
|
67999,
|
||||||
|
1500,
|
||||||
|
"We note the crosswalk methodology discussed at 91 FR 99490 for related codes.",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
ev = fr_events(store, "99490")
|
||||||
|
assert not any(e.kind == "crosswalk" for e in ev)
|
||||||
|
|
||||||
def test_deleted_via_range_prefilter_middle_member(self, store):
|
def test_deleted_via_range_prefilter_middle_member(self, store):
|
||||||
# 90868 is a middle member of a "90867-90869" range and appears
|
# 90868 is a middle member of a "90867-90869" range and appears
|
||||||
# nowhere else in the paragraph literally — the SQL prefilter must
|
# nowhere else in the paragraph literally — the SQL prefilter must
|
||||||
|
|||||||
Reference in New Issue
Block a user