52 KiB
LLM Chat Code Valuation Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: When a chat question names a HCPCS/CPT code or a registered code family, the answer keeps its retrieved citations and additionally presents each code's RVUs, conversion factor and national payment by vintage, as a cited prompt block, a valuation SSE event, and a rendered table.
Architecture: Deterministic detection (pfs/families.py) runs before generation; pfs/valuation.py reads the DuckDB read replica and attaches conversion factors from pfs.rules; llm/evidence.py turns rows into a prompt block, a UI payload, and pulls "code-cited" rule chunks from pgvector via a codes metadata list stamped at index time; llm/rag.py merges those sources, adds the block to the prompt and emits the event; chat.html renders the table. The llm container gets a read-only ./data mount.
Tech Stack: Python 3.13, uv run --no-sync, pytest, DuckDB (in-memory fixtures in tests), SQLAlchemy text() against pgvector's langchain_pg_embedding, FastAPI SSE, vanilla JS in chat.html.
Spec: docs/superpowers/specs/2026-09-08-llm-chat-code-valuation-design.md
Global Constraints
- Tests:
uv run --no-sync pytest <path> -q -p no:cacheprovider; output pristine. The pre-commit hook runs ruff + the cli suite. - Commit messages: conventional prefix,
(refs P48); noCo-Authored-Bytrailer.git status --shortbefore every commit; stage only your own files (shared worktree). - Nothing under
src/pfs/families.py,src/pfs/valuation.py,src/llm/evidence.pymay importnarwhals,pfs.calcs,pfs.pipe,aco, orconf.connect.ducklake— the chat image lacks narwhals.pfs/__init__.pyhas no imports, sopfs.rules/pfs.nprm/pfs.families/pfs.valuationare safe. - Chunk metadata stays
dict[str, str]: the newcodesvalue is a space-joined string ("G0556 G0557"),""when none. - Citation labels are square-bracketed: final
[PFS CY2026 Addendum B], proposed[CY2027 NPRM Addendum B]. FR citation URL:https://www.federalregister.gov/citation/<vol>-FR-<page>built from a"90 FR 49266"string. - Conversion factors come only from
pfs.rules.RULES[year].conversion_factor(final) andpfs.rules.proposed_for(year).conversion_factor(proposed); never frompfs.rvu.conv_factor. - National unadjusted payment =
round(total_rvu * cf, 2); no GPCI, no OPPS. - Event order:
valuation(only when codes detected) →token* →sources→done. With no codes detected, the prompt and event sequence are byte-identical to today. - Failures in evidence (replica missing, pgvector error) log a warning and drop the evidence; the chat never fails because of it.
File Structure
| File | Responsibility |
|---|---|
src/pfs/families.py (new) |
Family, FAMILIES, CODE_RE, find_codes(text), detect_codes(text) -> Detection — pure |
src/pfs/valuation.py (new) |
ValuationRow, valuation(con, codes, *, years), vintage_label/citation/url helpers — DuckDB SQL + pfs.rules |
src/llm/links.py |
as_source(md, text, score) -> dict (moved from rag._source) |
src/llm/chunk.py |
stamp codes metadata on every chunk |
src/llm/config.py + stack.toml |
duckdb_replica, valuation_years, code_cited_per_code |
src/llm/evidence.py (new) |
ValuationEvidence, valuation_evidence(question, cfg), code_cited_sources(engine, codes, *, per_code, collection) |
src/llm/rag.py |
system prompt addendum, build_messages(..., evidence=), stream_answer merge + event |
src/llm/web/chat.html |
renderValuation + styles |
compose.yml |
llm mount + env |
tests/pfs/test_families.py, tests/pfs/test_valuation.py, tests/llm/test_evidence.py (new); tests/llm/test_chunk.py, test_config.py, test_rag.py, test_api.py, test_links.py (extend) |
Task 1: pfs/families.py — registry and detection
Files:
- Create:
src/pfs/families.py - Test:
tests/pfs/test_families.py
Interfaces:
-
Produces:
CODE_RE(compiled),find_codes(text) -> tuple[str, ...](sorted unique codes literally present),Family(key, name, codes, synonyms),FAMILIES: dict[str, Family],Detection(codes, families, explicit),detect_codes(text) -> Detection,family_of(code) -> Family | None. -
Step 1: Write the failing tests
# tests/pfs/test_families.py
"""pfs.families — code-family registry + deterministic code detection."""
from __future__ import annotations
from pfs.families import FAMILIES, Detection, detect_codes, family_of, find_codes
class TestFindCodes:
def test_hcpcs_and_cpt(self):
assert find_codes("Codes G0556 and 99490 apply; see g0557.") == ("99490", "G0556")
def test_no_false_hits_on_years_and_fr_pages(self):
# 2026 (4 digits) and 43842 (5 digits) — the CPT regex will match 43842;
# validation against the fee schedule happens at lookup, not here.
assert find_codes("91 FR 43842, CY2026") == ("43842",)
def test_dedupes_and_sorts(self):
assert find_codes("G0558 G0556 G0558") == ("G0556", "G0558")
def test_empty(self):
assert find_codes("") == ()
class TestRegistry:
def test_apcm_family(self):
f = FAMILIES["APCM"]
assert f.codes == ("G0556", "G0557", "G0558")
assert "advanced primary care management" in f.synonyms
assert family_of("G0557") is f
assert family_of("00000") is None
def test_all_families_present(self):
assert set(FAMILIES) == {"ACP", "CCM", "PCM", "TCM", "APCM"}
class TestDetectCodes:
def test_family_name_expands_to_codes(self):
d = detect_codes("What is APCM and how is it valued?")
assert d == Detection(codes=("G0556", "G0557", "G0558"), families=("APCM",), explicit=())
def test_synonym_case_insensitive_word_boundary(self):
d = detect_codes("Tell me about Advanced Primary Care Management.")
assert d.families == ("APCM",)
assert detect_codes("the apcmx code").families == ()
def test_single_member_code_expands_family(self):
d = detect_codes("How much does G0557 pay?")
assert d.explicit == ("G0557",)
assert d.families == ("APCM",)
assert d.codes == ("G0556", "G0557", "G0558")
def test_unregistered_code_stays_alone(self):
d = detect_codes("value of 99213")
assert d == Detection(codes=("99213",), families=(), explicit=("99213",))
def test_multiple_families(self):
d = detect_codes("compare CCM and TCM")
assert d.families == ("CCM", "TCM")
assert d.codes == tuple(sorted(FAMILIES["CCM"].codes + FAMILIES["TCM"].codes))
def test_nothing(self):
assert detect_codes("what did commenters say about telehealth?") == Detection((), (), ())
- Step 2: Run to verify failure
Run: uv run --no-sync pytest tests/pfs/test_families.py -q -p no:cacheprovider
Expected: FAIL — ModuleNotFoundError: No module named 'pfs.families'
- Step 3: Implement
# src/pfs/families.py
"""Code families and deterministic code detection for chat questions.
The registry used to live in a notebook cell
(``notebooks/palliative_care_rfi.py``); this is its home in ``src`` so
the chat, notebooks and scripts share one list. Pure: no I/O, no
DuckDB, no narwhals — it must import inside the ``llm`` container.
"""
from __future__ import annotations
import re
from dataclasses import dataclass
# HCPCS level II (letter + 4 digits) or CPT (5 digits). Validation against
# the fee schedule happens at lookup time (pfs.valuation), not here.
CODE_RE = re.compile(r"\b(?:[A-Z]\d{4}|\d{5})\b")
def find_codes(text: str) -> tuple[str, ...]:
"""Sorted unique codes literally present in *text* (case-normalised)."""
return tuple(sorted({m.group(0).upper() for m in CODE_RE.finditer(text.upper())}))
@dataclass(frozen=True)
class Family:
key: str
name: str
codes: tuple[str, ...]
synonyms: tuple[str, ...] # lower-case; matched on word boundaries
FAMILIES: dict[str, Family] = {
"ACP": Family(
"ACP", "Advance Care Planning", ("99497", "99498"),
("acp", "advance care planning"),
),
"CCM": Family(
"CCM", "Chronic Care Management",
("99437", "99439", "99487", "99489", "99490", "99491"),
("ccm", "chronic care management"),
),
"PCM": Family(
"PCM", "Principal Care Management", ("99424", "99425", "99426", "99427"),
("pcm", "principal care management"),
),
"TCM": Family(
"TCM", "Transitional Care Management", ("99495", "99496"),
("tcm", "transitional care management"),
),
"APCM": Family(
"APCM", "Advanced Primary Care Management", ("G0556", "G0557", "G0558"),
("apcm", "advanced primary care management"),
),
}
_CODE_TO_FAMILY: dict[str, Family] = {c: f for f in FAMILIES.values() for c in f.codes}
def family_of(code: str) -> Family | None:
return _CODE_TO_FAMILY.get(code.upper())
@dataclass(frozen=True)
class Detection:
codes: tuple[str, ...] # sorted unique: explicit + family-expanded
families: tuple[str, ...] # family keys, sorted
explicit: tuple[str, ...] # codes literally present in the text
def detect_codes(text: str) -> Detection:
"""Codes a question is about: explicit codes plus every code of any
family named (by synonym) or touched (by one member code)."""
explicit = find_codes(text)
lowered = text.lower()
families: set[str] = set()
for fam in FAMILIES.values():
if any(re.search(rf"\b{re.escape(s)}\b", lowered) for s in fam.synonyms):
families.add(fam.key)
for code in explicit:
fam = family_of(code)
if fam is not None:
families.add(fam.key)
codes = set(explicit)
for key in families:
codes.update(FAMILIES[key].codes)
return Detection(
codes=tuple(sorted(codes)),
families=tuple(sorted(families)),
explicit=explicit,
)
- Step 4: Run to verify pass
Run: uv run --no-sync pytest tests/pfs/test_families.py -q -p no:cacheprovider
Expected: 12 passed
- Step 5: Commit
git add src/pfs/families.py tests/pfs/test_families.py
git commit -m "feat(pfs): code-family registry + deterministic code detection (refs P48)"
Task 2: pfs/valuation.py — rows by vintage from the replica
Files:
- Create:
src/pfs/valuation.py - Test:
tests/pfs/test_valuation.py
Interfaces:
-
Consumes:
pfs.rules.RULES,pfs.rules.proposed_for,pfs.nprm.NPRM_SOURCES. -
Produces:
ValuationRow(frozen dataclass; fields below),valuation(con, codes, *, years=4) -> tuple[list[ValuationRow], list[str]](rows ordered by code then year, proposed last per code; second element = codes with no rows),fr_citation_url(cite) -> str,vintage_label(year, proposed) -> str. -
Step 1: Write the failing tests
# tests/pfs/test_valuation.py
"""pfs.valuation — RVUs + national payment by vintage from a DuckDB replica."""
from __future__ import annotations
import duckdb
import pytest
from pfs.rules import RULES, proposed_for
from pfs.valuation import ValuationRow, fr_citation_url, valuation, vintage_label
RVU_COLS = (
"hcpcs VARCHAR, mod VARCHAR, description VARCHAR, status_code VARCHAR, "
"work_rvu DOUBLE, non_fac_pe_rvu DOUBLE, fac_pe_rvu DOUBLE, mp_rvu DOUBLE, "
"non_fac_total DOUBLE, fac_total DOUBLE, conv_factor DOUBLE, year INTEGER"
)
PROPOSED_COLS = (
"hcpcs VARCHAR, mod VARCHAR, description VARCHAR, status_code VARCHAR, "
"work_rvu DOUBLE, non_fac_pe_rvu DOUBLE, fac_pe_rvu DOUBLE, mp_rvu DOUBLE, cms_rule_id VARCHAR"
)
@pytest.fixture
def con():
c = duckdb.connect(":memory:")
c.execute("CREATE SCHEMA pfs")
c.execute(f"CREATE TABLE pfs.rvu ({RVU_COLS})")
c.execute(f"CREATE TABLE pfs.rvu_proposed ({PROPOSED_COLS})")
rows = [
# G0556: 2025 + 2026 final, with a modifier row that must be ignored
("G0556", None, "Adv prim care mgmt lvl 1", "A", 0.25, 0.20, 0.10, 0.02, 0.47, 0.37, 32.3465, 2025),
("G0556", None, "Adv prim care mgmt lvl 1", "A", 0.25, 0.22, 0.06, 0.02, 0.49, 0.33, 33.4009, 2026),
("G0556", "26", "Adv prim care mgmt lvl 1", "A", 9.0, 9.0, 9.0, 9.0, 27.0, 27.0, 33.4009, 2026),
# 99490: six years so the last-N cut applies (2021..2026)
*[("99490", "", "Chrnc care mgmt srvc 20 min", "A", 1.0, 1.0, 0.5, 0.05, 2.05, 1.55, 30.0, y) for y in range(2021, 2027)],
# I-status code with NULL components
("G9999", None, "Not priced", "I", None, None, None, None, None, None, 33.4009, 2026),
]
c.executemany("INSERT INTO pfs.rvu VALUES (?,?,?,?,?,?,?,?,?,?,?,?)", rows)
c.executemany(
"INSERT INTO pfs.rvu_proposed VALUES (?,?,?,?,?,?,?,?,?)",
[
("G0556", None, "Adv prim care mgmt lvl 1", "A", 0.25, 0.23, 0.07, 0.02, "CMS-1848-P"),
("G0556", "26", "Adv prim care mgmt lvl 1", "A", 9.0, 9.0, 9.0, 9.0, "CMS-1848-P"),
("G0556", None, "older nprm", "A", 0.1, 0.1, 0.1, 0.1, "CMS-1832-P"),
],
)
yield c
c.close()
class TestHelpers:
def test_vintage_label(self):
assert vintage_label(2026, False) == "[PFS CY2026 Addendum B]"
assert vintage_label(2027, True) == "[CY2027 NPRM Addendum B]"
def test_fr_citation_url(self):
assert fr_citation_url("90 FR 49266") == "https://www.federalregister.gov/citation/90-FR-49266"
assert fr_citation_url("") == ""
class TestValuation:
def test_final_and_proposed_rows_with_cf_and_payment(self, con):
rows, unpriced = valuation(con, ["G0556"], years=4)
assert unpriced == []
assert [(r.year, r.proposed) for r in rows] == [(2025, False), (2026, False), (2027, True)]
r26 = rows[1]
assert isinstance(r26, ValuationRow)
assert (r26.code, r26.status, r26.work, r26.pe_nf, r26.pe_f, r26.mp) == ("G0556", "A", 0.25, 0.22, 0.06, 0.02)
assert (r26.total_nf, r26.total_f) == (0.49, 0.33)
assert r26.cf == RULES[2026].conversion_factor
assert r26.pay_nf == round(0.49 * RULES[2026].conversion_factor, 2)
assert r26.pay_f == round(0.33 * RULES[2026].conversion_factor, 2)
assert r26.label == "[PFS CY2026 Addendum B]"
assert r26.citation == RULES[2026].federal_register_citation
assert r26.url == fr_citation_url(RULES[2026].federal_register_citation)
assert r26.vintage == "CY2026 final"
p = rows[2]
assert p.label == "[CY2027 NPRM Addendum B]" and p.vintage == "CY2027 proposed"
assert p.cf == proposed_for(2027).conversion_factor
assert p.total_nf == round(0.25 + 0.23 + 0.02, 4) and p.total_f == round(0.25 + 0.07 + 0.02, 4)
assert p.pay_nf == round(p.total_nf * p.cf, 2)
assert p.citation == proposed_for(2027).federal_register_citation
def test_modifier_rows_are_ignored(self, con):
rows, _ = valuation(con, ["G0556"])
assert all(r.work == 0.25 for r in rows)
def test_last_n_years_only(self, con):
rows, _ = valuation(con, ["99490"], years=4)
assert [r.year for r in rows if not r.proposed] == [2023, 2024, 2025, 2026]
def test_unpriced_codes_reported(self, con):
rows, unpriced = valuation(con, ["G0556", "Z9999"])
assert unpriced == ["Z9999"]
assert {r.code for r in rows} == {"G0556"}
def test_null_components_pass_through(self, con):
rows, unpriced = valuation(con, ["G9999"])
assert unpriced == []
(r,) = rows
assert r.status == "I" and r.work is None and r.pay_nf is None
def test_order_is_code_then_year(self, con):
rows, _ = valuation(con, ["G0556", "99490"], years=2)
assert [(r.code, r.year) for r in rows] == [
("99490", 2025), ("99490", 2026), ("G0556", 2025), ("G0556", 2026), ("G0556", 2027)
]
- Step 2: Run to verify failure
Run: uv run --no-sync pytest tests/pfs/test_valuation.py -q -p no:cacheprovider
Expected: FAIL — ModuleNotFoundError: No module named 'pfs.valuation'
- Step 3: Implement
# src/pfs/valuation.py
"""RVUs, conversion factor and national unadjusted payment per code and
vintage, read from the DuckDB replica (``pfs.rvu`` final years,
``pfs.rvu_proposed`` for the newest NPRM). Conversion factors come from
``pfs.rules`` — never from ``pfs.rvu.conv_factor``. Pure DuckDB SQL; no
narwhals (this module runs inside the ``llm`` container).
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Sequence
from pfs.nprm import NPRM_SOURCES
from pfs.rules import RULES, proposed_for
@dataclass(frozen=True)
class ValuationRow:
code: str
description: str
vintage: str # "CY2026 final" | "CY2027 proposed"
year: int
proposed: bool
status: str
work: float | None
pe_nf: float | None
pe_f: float | None
mp: float | None
total_nf: float | None
total_f: float | None
cf: float
pay_nf: float | None
pay_f: float | None
label: str # "[PFS CY2026 Addendum B]"
citation: str # "90 FR 49266"
url: str
def vintage_label(year: int, proposed: bool) -> str:
return f"[CY{year} NPRM Addendum B]" if proposed else f"[PFS CY{year} Addendum B]"
def fr_citation_url(cite: str) -> str:
parts = cite.split()
if len(parts) != 3 or parts[1].upper() != "FR":
return ""
return f"https://www.federalregister.gov/citation/{parts[0]}-FR-{parts[2]}"
def _pay(total: float | None, cf: float) -> float | None:
return None if total is None else round(total * cf, 2)
_FINAL_SQL = """
WITH base AS (
SELECT hcpcs, year, description, status_code, work_rvu, non_fac_pe_rvu,
fac_pe_rvu, mp_rvu, non_fac_total, fac_total
FROM pfs.rvu
WHERE hcpcs IN (SELECT UNNEST(?::VARCHAR[]))
QUALIFY row_number() OVER (
PARTITION BY hcpcs, year ORDER BY (mod IS NULL OR mod = '') DESC, mod
) = 1
)
SELECT * FROM base
QUALIFY dense_rank() OVER (PARTITION BY hcpcs ORDER BY year DESC) <= ?
ORDER BY hcpcs, year
"""
_PROPOSED_SQL = """
SELECT hcpcs, description, status_code, work_rvu, non_fac_pe_rvu, fac_pe_rvu, mp_rvu
FROM pfs.rvu_proposed
WHERE cms_rule_id = ? AND hcpcs IN (SELECT UNNEST(?::VARCHAR[]))
QUALIFY row_number() OVER (
PARTITION BY hcpcs ORDER BY (mod IS NULL OR mod = '') DESC, mod
) = 1
ORDER BY hcpcs
"""
def _sum(*vals: float | None) -> float | None:
if any(v is None for v in vals):
return None
return round(sum(vals), 4) # type: ignore[arg-type]
def valuation(
con: Any, codes: Sequence[str], *, years: int = 4
) -> tuple[list[ValuationRow], list[str]]:
"""Rows for *codes* (last *years* final vintages + the newest NPRM),
ordered by code then year with the proposed row last per code, and the
codes that produced no rows at all."""
wanted = sorted({c.upper() for c in codes})
if not wanted:
return [], []
by_code: dict[str, list[ValuationRow]] = {c: [] for c in wanted}
for hcpcs, year, desc, status, work, pe_nf, pe_f, mp, tot_nf, tot_f in con.execute(
_FINAL_SQL, [wanted, years]
).fetchall():
rule = RULES.get(int(year))
if rule is None:
continue
cf = rule.conversion_factor
cite = rule.federal_register_citation
by_code[hcpcs].append(
ValuationRow(
code=hcpcs, description=desc or "", vintage=f"CY{year} final",
year=int(year), proposed=False, status=status or "",
work=work, pe_nf=pe_nf, pe_f=pe_f, mp=mp,
total_nf=tot_nf, total_f=tot_f, cf=cf,
pay_nf=_pay(tot_nf, cf), pay_f=_pay(tot_f, cf),
label=vintage_label(int(year), False), citation=cite,
url=fr_citation_url(cite),
)
)
nprm_year, _tag, rule_id, _pin = max(NPRM_SOURCES, key=lambda s: s[0])
prop = proposed_for(nprm_year)
if prop is not None:
cf = prop.conversion_factor
cite = prop.federal_register_citation
for hcpcs, desc, status, work, pe_nf, pe_f, mp in con.execute(
_PROPOSED_SQL, [rule_id, wanted]
).fetchall():
tot_nf, tot_f = _sum(work, pe_nf, mp), _sum(work, pe_f, mp)
by_code[hcpcs].append(
ValuationRow(
code=hcpcs, description=desc or "", vintage=f"CY{nprm_year} proposed",
year=nprm_year, proposed=True, status=status or "",
work=work, pe_nf=pe_nf, pe_f=pe_f, mp=mp,
total_nf=tot_nf, total_f=tot_f, cf=cf,
pay_nf=_pay(tot_nf, cf), pay_f=_pay(tot_f, cf),
label=vintage_label(nprm_year, True), citation=cite,
url=fr_citation_url(cite),
)
)
rows = [r for c in wanted for r in sorted(by_code[c], key=lambda r: (r.year, r.proposed))]
unpriced = [c for c in wanted if not by_code[c]]
return rows, unpriced
Note: DuckDB accepts a Python list for a ?::VARCHAR[] parameter; UNNEST turns it into rows for IN.
- Step 4: Run to verify pass
Run: uv run --no-sync pytest tests/pfs/test_valuation.py -q -p no:cacheprovider
Expected: 8 passed
- Step 5: Commit
git add src/pfs/valuation.py tests/pfs/test_valuation.py
git commit -m "feat(pfs): valuation rows by vintage from the DuckDB replica with pfs.rules conversion factors (refs P48)"
Task 3: codes chunk metadata + links.as_source
Files:
- Modify:
src/llm/chunk.py(chunk_doc,_chunk_paragraphs) - Modify:
src/llm/links.py(addas_source),src/llm/rag.py(_sourcedelegates) - Test:
tests/llm/test_chunk.py,tests/llm/test_links.py
Interfaces:
-
Consumes:
pfs.families.find_codes. -
Produces: every
Chunk.metadata["codes"]= space-joined sorted codes in that chunk's text (""when none);llm.links.as_source(md: dict[str,str], text: str, score: float) -> dictreturning the same dict shaperag._sourcereturns today (id,label,kind,url,title,date,docket,comment_id,snippet,score). -
Step 1: Write the failing tests
Append to tests/llm/test_chunk.py:
class TestCodesMetadata:
def test_codes_stamped_per_chunk(self):
text = "We propose G0556 and G0557.\n\n## Other\n\nNo codes here.\n\n## More\n\n99490 applies."
chunks = chunk_doc(_doc(text), target_chars=60, overlap_chars=10)
found = {c.metadata["codes"] for c in chunks}
assert "G0556 G0557" in found
assert "99490" in found
assert "" in found
def test_rule_paragraph_chunks_get_codes(self):
from llm.chunk import Paragraph
doc = Doc(
key="R1", text="x",
metadata={"kind": "rule"},
paragraphs=(Paragraph(1, 100, 1, "APCM code G0556 is valued at 0.25 work RVUs."),),
)
(chunk,) = chunk_doc(doc)
assert chunk.metadata["codes"] == "G0556"
Append to tests/llm/test_links.py:
class TestAsSource:
def test_shape_matches_rag_source(self):
from llm.links import as_source
s = as_source({"kind": "comment", "comment_id": "CMS-2026-2377-1", "date": "2026-08-19", "title": "t", "docket": "CMS-2026-2377"}, "body " * 200, 0.123456)
assert set(s) == {"id", "label", "kind", "url", "title", "date", "docket", "comment_id", "snippet", "score"}
assert s["label"] == "CMS-2026-2377-1" and s["kind"] == "comment"
assert len(s["snippet"]) <= 500 and s["score"] == 0.1235
- Step 2: Run to verify failure
Run: uv run --no-sync pytest tests/llm/test_chunk.py tests/llm/test_links.py -q -p no:cacheprovider
Expected: FAIL — KeyError: 'codes' and ImportError: cannot import name 'as_source'.
- Step 3: Implement
In src/llm/chunk.py: add from pfs.families import find_codes at the top; in both chunk builders add "codes": " ".join(find_codes(piece)) (comment path, using piece) and "codes": " ".join(find_codes(text)) (paragraph path) to the metadata dicts after "section".
In src/llm/links.py add:
_SNIPPET_CHARS = 500
def as_source(md: dict[str, str], text: str, score: float) -> dict:
"""The source dict the chat sends to the prompt and the UI."""
snippet = text[:_SNIPPET_CHARS].strip()
url, label = for_source(md, snippet)
return {
"id": label,
"label": label,
"kind": md.get("kind", ""),
"url": url,
"title": md.get("title", ""),
"date": md.get("date", ""),
"docket": md.get("docket", ""),
"comment_id": md.get("comment_id", ""),
"snippet": snippet,
"score": round(score, 4),
}
In src/llm/rag.py: _source(hit) becomes return as_source(hit.metadata, hit.text, hit.score) (import as_source from llm.links; delete the local _SNIPPET_CHARS if unused).
- Step 4: Run to verify pass
Run: uv run --no-sync pytest tests/llm -q -p no:cacheprovider
Expected: pass (existing test_rag retrieve tests still assert the same source shape).
- Step 5: Commit
git add src/llm/chunk.py src/llm/links.py src/llm/rag.py tests/llm/test_chunk.py tests/llm/test_links.py
git commit -m "feat(llm): stamp codes metadata on every chunk; links.as_source shared source dict (refs P48)"
Task 4: Config knobs
Files:
- Modify:
src/llm/config.py(LlmConfig,load),stack.toml([llm]) - Test:
tests/llm/test_config.py
Interfaces:
-
Produces:
LlmConfig.duckdb_replica: str(default"data/aco.ro.duckdb", envLLM_DUCKDB_REPLICAoverrides),valuation_years: int(default 4),code_cited_per_code: int(default 3). -
Step 1: Write the failing tests (append to
TestLoad)
def test_valuation_knobs_defaults(self, monkeypatch):
monkeypatch.delenv("LLM_DUCKDB_REPLICA", raising=False)
cfg = llm_config.load()
assert cfg.duckdb_replica == "data/aco.ro.duckdb"
assert cfg.valuation_years == 4
assert cfg.code_cited_per_code == 3
def test_duckdb_replica_env_override(self, monkeypatch):
monkeypatch.setenv("LLM_DUCKDB_REPLICA", "/app/data/aco.ro.duckdb")
assert llm_config.load().duckdb_replica == "/app/data/aco.ro.duckdb"
-
Step 2: Run to verify failure —
uv run --no-sync pytest tests/llm/test_config.py -q -p no:cacheprovider→ FAIL (AttributeError: duckdb_replica). -
Step 3: Implement
LlmConfig gains (after top_n):
duckdb_replica: str = "data/aco.ro.duckdb"
valuation_years: int = 4
code_cited_per_code: int = 3
load() passes:
duckdb_replica=os.environ.get("LLM_DUCKDB_REPLICA") or str(_opt(section, "duckdb_replica", "data/aco.ro.duckdb")),
valuation_years=int(_opt(section, "valuation_years", 4)),
code_cited_per_code=int(_opt(section, "code_cited_per_code", 3)),
Add to the module docstring env contract: LLM_DUCKDB_REPLICA beats [llm].duckdb_replica (containers set /app/data/aco.ro.duckdb). In stack.toml [llm] add:
duckdb_replica = "data/aco.ro.duckdb" # read-only DuckDB replica the chat reads valuations from
valuation_years = 4 # final-rule vintages shown per code (plus the newest NPRM)
code_cited_per_code = 3 # rule paragraphs literally citing each detected code
Also update tests/llm/test_rag.py's CFG and any other LlmConfig(...) literal in tests only if they break (they use defaults, so they should not).
-
Step 4: Run to verify pass —
uv run --no-sync pytest tests/llm/test_config.py tests/llm/test_rag.py -q -p no:cacheprovider→ pass. -
Step 5: Commit
git add src/llm/config.py stack.toml tests/llm/test_config.py
git commit -m "feat(llm): duckdb_replica / valuation_years / code_cited_per_code config (refs P48)"
Task 5: llm/evidence.py — evidence block, payload, code-cited sources
Files:
- Create:
src/llm/evidence.py - Test:
tests/llm/test_evidence.py
Interfaces:
-
Consumes:
pfs.families.detect_codes,pfs.valuation.valuation/ValuationRow,llm.links.as_source,llm.index._engine,LlmConfig.duckdb_replica/valuation_years. -
Produces:
ValuationEvidence(codes, families, rows, unpriced)withprompt_block() -> strandpayload() -> dict.valuation_evidence(question: str, cfg: LlmConfig) -> ValuationEvidence | None(None when no codes detected or the replica cannot be opened).code_cited_sources(engine, codes: Sequence[str], *, per_code: int, collection: str = "rules") -> list[dict].merge_sources(retrieved: list[dict], extra: list[dict]) -> list[dict](retrieved order kept; extras appended unless theirlabelalready present).
-
Step 1: Write the failing tests
# tests/llm/test_evidence.py
"""llm.evidence — valuation prompt block, SSE payload, code-cited rule sources."""
from __future__ import annotations
from unittest.mock import MagicMock, patch
from llm.config import LlmConfig
from llm.evidence import ValuationEvidence, code_cited_sources, merge_sources, valuation_evidence
from pfs.valuation import ValuationRow
CFG = LlmConfig(
ollama_hosts=("http://h1:11434",), embed_model="e", instruct_model="c", embed_dim=768,
build_ann_index=False, pg_host="x", pg_port=5432, pg_db="llm", pg_user="llm",
duckdb_replica="/nonexistent/aco.ro.duckdb", valuation_years=4, code_cited_per_code=2,
)
def _row(**kw) -> ValuationRow:
base = dict(
code="G0556", description="Adv prim care mgmt lvl 1", vintage="CY2026 final", year=2026,
proposed=False, status="A", work=0.25, pe_nf=0.22, pe_f=0.06, mp=0.02, total_nf=0.49,
total_f=0.33, cf=33.4009, pay_nf=16.37, pay_f=11.02, label="[PFS CY2026 Addendum B]",
citation="90 FR 49266", url="https://www.federalregister.gov/citation/90-FR-49266",
)
base.update(kw)
return ValuationRow(**base)
class TestEvidenceText:
def test_prompt_block_exact(self):
ev = ValuationEvidence(codes=("G0556",), families=("APCM",), rows=(_row(),), unpriced=("Z9999",))
assert ev.prompt_block() == (
"Valuation (authoritative for RVUs, conversion factors and payments; "
"national unadjusted, no GPCI):\n"
'[PFS CY2026 Addendum B] G0556 "Adv prim care mgmt lvl 1" — status A; work 0.25; '
"PE 0.22 non-fac / 0.06 fac; MP 0.02; total 0.49 non-fac / 0.33 fac; "
"CF $33.4009 → payment $16.37 non-fac / $11.02 fac\n"
"Not priced in the indexed fee schedule: Z9999"
)
def test_prompt_block_null_components(self):
ev = ValuationEvidence(("G9999",), (), (_row(code="G9999", status="I", work=None, pe_nf=None, pe_f=None, mp=None, total_nf=None, total_f=None, pay_nf=None, pay_f=None),), ())
line = ev.prompt_block().splitlines()[1]
assert "status I; work n/a; PE n/a non-fac / n/a fac" in line and "payment n/a" in line
def test_payload_shape(self):
ev = ValuationEvidence(("G0556",), ("APCM",), (_row(), _row(year=2027, proposed=True, vintage="CY2027 proposed", label="[CY2027 NPRM Addendum B]", citation="91 FR 43842", url="https://www.federalregister.gov/citation/91-FR-43842")), ())
p = ev.payload()
assert p["type"] == "valuation" and p["codes"] == ["G0556"] and p["families"] == ["APCM"]
assert p["rows"][0]["pay_nf"] == 16.37 and p["rows"][1]["proposed"] is True
assert p["provenance"] == [
{"label": "[PFS CY2026 Addendum B]", "vintage": "CY2026 final", "citation": "90 FR 49266", "url": "https://www.federalregister.gov/citation/90-FR-49266"},
{"label": "[CY2027 NPRM Addendum B]", "vintage": "CY2027 proposed", "citation": "91 FR 43842", "url": "https://www.federalregister.gov/citation/91-FR-43842"},
]
assert p["unpriced"] == []
class TestValuationEvidence:
def test_none_when_no_codes(self):
assert valuation_evidence("what did commenters say about telehealth?", CFG) is None
@patch("llm.evidence.valuation", return_value=([_row()], []))
@patch("llm.evidence.duckdb.connect")
def test_detects_and_queries_replica_read_only(self, mock_connect, mock_val):
ev = valuation_evidence("How is APCM valued?", CFG)
mock_connect.assert_called_once_with("/nonexistent/aco.ro.duckdb", read_only=True)
assert mock_val.call_args.args[1] == ["G0556", "G0557", "G0558"]
assert mock_val.call_args.kwargs == {"years": 4}
assert ev is not None and ev.codes == ("G0556", "G0557", "G0558") and ev.families == ("APCM",)
mock_connect.return_value.close.assert_called_once()
@patch("llm.evidence.duckdb.connect", side_effect=OSError("no replica"))
def test_unopenable_replica_yields_none(self, _c, caplog):
assert valuation_evidence("G0556?", CFG) is None
assert "valuation evidence skipped" in caplog.text
def _engine(rows):
engine = MagicMock()
conn = engine.begin.return_value.__enter__.return_value
conn.execute.return_value.fetchall.return_value = rows
return engine, conn
def _rule_md(p_id, codes, date="2024-12-09"):
return {"kind": "rule", "item_key": "R1", "p_id": str(p_id), "page": "97710", "ordinal": "1",
"html_url": "https://www.federalregister.gov/d/2024-25382", "fr_volume": "89",
"date": date, "title": "CY2025 PFS final rule", "codes": codes}
class TestCodeCitedSources:
def test_sql_and_per_code_cap_and_dedupe(self):
engine, conn = _engine([
("para one G0556", _rule_md(1, "G0556")),
("para two G0556", _rule_md(2, "G0556")),
("para three G0556", _rule_md(3, "G0556")), # over the cap for G0556
("para four G0557", _rule_md(4, "G0557 G0556")), # G0557 still under cap
("para one again", _rule_md(1, "G0556")), # duplicate (item_key, p_id)
])
out = code_cited_sources(engine, ["G0556", "G0557"], per_code=2)
sql = str(conn.execute.call_args.args[0])
assert "string_to_array" in sql and "langchain_pg_collection" in sql
assert conn.execute.call_args.args[1]["collection"] == "rules"
assert conn.execute.call_args.args[1]["codes"] == ["G0556", "G0557"]
assert [s["snippet"] for s in out] == ["para one G0556", "para two G0556", "para four G0557"]
assert all(s["kind"] == "rule" and s["score"] == 0.0 for s in out)
def test_engine_error_yields_empty(self, caplog):
engine = MagicMock()
engine.begin.side_effect = RuntimeError("pg down")
assert code_cited_sources(engine, ["G0556"], per_code=2) == []
assert "code-cited sources skipped" in caplog.text
class TestMergeSources:
def test_keeps_order_and_dedupes_by_label(self):
a = [{"label": "X", "score": 0.9}, {"label": "Y", "score": 0.8}]
b = [{"label": "Y", "score": 0.0}, {"label": "Z", "score": 0.0}]
assert [s["label"] for s in merge_sources(a, b)] == ["X", "Y", "Z"]
assert merge_sources(a, b)[1]["score"] == 0.8
- Step 2: Run to verify failure
Run: uv run --no-sync pytest tests/llm/test_evidence.py -q -p no:cacheprovider
Expected: FAIL — ModuleNotFoundError: No module named 'llm.evidence'
- Step 3: Implement
# src/llm/evidence.py
"""Structured evidence for the chat: code valuations beside the excerpts.
``valuation_evidence`` detects HCPCS/CPT codes (and registered families)
in the question, reads their RVUs from the DuckDB read replica and
attaches conversion factors from ``pfs.rules``. ``code_cited_sources``
pulls rule chunks whose ``codes`` metadata mention those codes straight
from pgvector so the answer can cite where CMS valued them. Both degrade
to "no evidence" with a warning — the chat never fails because of them.
"""
from __future__ import annotations
import logging
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Any, Sequence
import duckdb
from sqlalchemy import text
from llm.config import LlmConfig
from llm.links import as_source
from pfs.families import detect_codes
from pfs.valuation import ValuationRow, valuation
log = logging.getLogger(__name__)
_HEADER = (
"Valuation (authoritative for RVUs, conversion factors and payments; "
"national unadjusted, no GPCI):"
)
def _n(v: float | None, digits: int = 2, money: bool = False) -> str:
if v is None:
return "n/a"
s = f"{v:.{digits}f}"
return f"${s}" if money else s
@dataclass(frozen=True)
class ValuationEvidence:
codes: tuple[str, ...]
families: tuple[str, ...]
rows: tuple[ValuationRow, ...]
unpriced: tuple[str, ...]
def prompt_block(self) -> str:
lines = [_HEADER]
for r in self.rows:
lines.append(
f'{r.label} {r.code} "{r.description}" — status {r.status or "?"}; '
f"work {_n(r.work)}; PE {_n(r.pe_nf)} non-fac / {_n(r.pe_f)} fac; "
f"MP {_n(r.mp)}; total {_n(r.total_nf)} non-fac / {_n(r.total_f)} fac; "
f"CF {_n(r.cf, 4, money=True)} → payment {_n(r.pay_nf, money=True)} non-fac / "
f"{_n(r.pay_f, money=True)} fac"
)
if self.unpriced:
lines.append("Not priced in the indexed fee schedule: " + ", ".join(self.unpriced))
return "\n".join(lines)
def payload(self) -> dict[str, Any]:
provenance: list[dict[str, str]] = []
seen: set[str] = set()
for r in self.rows:
if r.label not in seen:
seen.add(r.label)
provenance.append({"label": r.label, "vintage": r.vintage, "citation": r.citation, "url": r.url})
return {
"type": "valuation",
"codes": list(self.codes),
"families": list(self.families),
"rows": [asdict(r) for r in self.rows],
"provenance": provenance,
"unpriced": list(self.unpriced),
}
def _replica_path(cfg: LlmConfig) -> str:
p = Path(cfg.duckdb_replica)
if p.is_absolute():
return str(p)
from conf import ROOT
return str(ROOT / p)
def valuation_evidence(question: str, cfg: LlmConfig) -> ValuationEvidence | None:
det = detect_codes(question)
if not det.codes:
return None
try:
con = duckdb.connect(_replica_path(cfg), read_only=True)
except Exception as e: # noqa: BLE001 — duckdb raises several types
log.warning("valuation evidence skipped (%s): %s", cfg.duckdb_replica, e)
return None
try:
rows, unpriced = valuation(con, list(det.codes), years=cfg.valuation_years)
except Exception as e: # noqa: BLE001
log.warning("valuation evidence skipped (query): %s", e)
return None
finally:
con.close()
return ValuationEvidence(det.codes, det.families, tuple(rows), tuple(unpriced))
_CITED_SQL = text(
"SELECT e.document, e.cmetadata FROM langchain_pg_embedding e "
"JOIN langchain_pg_collection c ON c.uuid = e.collection_id "
"WHERE c.name = :collection "
"AND string_to_array(COALESCE(e.cmetadata->>'codes', ''), ' ') && CAST(:codes AS text[]) "
"ORDER BY e.cmetadata->>'date' DESC NULLS LAST, e.cmetadata->>'item_key', e.cmetadata->>'p_id' "
"LIMIT :lim"
)
def code_cited_sources(
engine: Any, codes: Sequence[str], *, per_code: int, collection: str = "rules"
) -> list[dict]:
"""Chunks in *collection* whose ``codes`` metadata mention any of
*codes* — at most *per_code* per code, newest rule first, deduped on
(item_key, p_id). Scores are 0.0: these are additive, not ranked."""
wanted = [c.upper() for c in codes]
try:
with engine.begin() as conn:
rows = conn.execute(
_CITED_SQL,
{"collection": collection, "codes": wanted, "lim": per_code * len(wanted) * 4},
).fetchall()
except Exception as e: # noqa: BLE001
log.warning("code-cited sources skipped: %s", e)
return []
out: list[dict] = []
seen: set[tuple[str, str]] = set()
count: dict[str, int] = {c: 0 for c in wanted}
for document, md in rows:
md = {k: str(v) for k, v in (md or {}).items()}
key = (md.get("item_key", ""), md.get("p_id", ""))
if key in seen:
continue
hit_codes = [c for c in md.get("codes", "").split() if c in count and count[c] < per_code]
if not hit_codes:
continue
seen.add(key)
for c in hit_codes:
count[c] += 1
out.append(as_source(md, document, 0.0))
return out
def merge_sources(retrieved: list[dict], extra: list[dict]) -> list[dict]:
labels = {s["label"] for s in retrieved}
return retrieved + [s for s in extra if s["label"] not in labels]
-
Step 4: Run to verify pass —
uv run --no-sync pytest tests/llm/test_evidence.py -q -p no:cacheprovider→ 9 passed. -
Step 5: Commit
git add src/llm/evidence.py tests/llm/test_evidence.py
git commit -m "feat(llm): valuation evidence block, SSE payload and code-cited rule sources (refs P48)"
Task 6: rag.py — prompt block, merged sources, valuation event
Files:
- Modify:
src/llm/rag.py(_SYSTEM,build_messages,stream_answer) - Test:
tests/llm/test_rag.py
Interfaces:
-
Consumes:
llm.evidence.valuation_evidence/code_cited_sources/merge_sources/ValuationEvidence,llm.index._engine. -
Produces:
build_messages(question, sources, evidence: ValuationEvidence | None = None);stream_answeryieldsvaluationfirst when evidence exists. -
Step 1: Write the failing tests
In tests/llm/test_rag.py add to TestBuildMessages:
def test_evidence_block_between_excerpts_and_question(self):
from llm.evidence import ValuationEvidence
from pfs.valuation import ValuationRow
row = ValuationRow(code="G0556", description="d", vintage="CY2026 final", year=2026, proposed=False,
status="A", work=0.25, pe_nf=0.22, pe_f=0.06, mp=0.02, total_nf=0.49, total_f=0.33,
cf=33.4009, pay_nf=16.37, pay_f=11.02, label="[PFS CY2026 Addendum B]",
citation="90 FR 49266", url="u")
ev = ValuationEvidence(("G0556",), ("APCM",), (row,), ())
msgs = build_messages("how much?", [], evidence=ev)
user = msgs[1]["content"]
assert user.index("Excerpts:") < user.index("Valuation (authoritative") < user.index("Question: how much?")
assert "[PFS CY2026 Addendum B] G0556" in user
assert "valuation" in msgs[0]["content"].lower()
assert "do not compute" in msgs[0]["content"].lower()
def test_no_evidence_prompt_unchanged(self):
assert build_messages("q", []) == build_messages("q", [], evidence=None)
assert "Valuation" not in build_messages("q", [])[1]["content"]
In TestStreamAnswer, patch llm.rag.valuation_evidence to return None in the existing four tests (add @patch("llm.rag.valuation_evidence", return_value=None) as the outermost decorator and a leading _ev parameter) so they stay deterministic, and add:
@patch("llm.rag._engine")
@patch("llm.rag.code_cited_sources")
@patch("llm.rag.valuation_evidence")
@patch("llm.rag.httpx.Client")
@patch("llm.rag.retrieve")
def test_valuation_event_first_and_sources_merged(self, mock_retrieve, MockClient, mock_ev, mock_cited, mock_engine):
from llm.evidence import ValuationEvidence
src = {"id": "C1", "label": "C1", "kind": "comment", "snippet": "s", "score": 0.1}
cited = {"id": "89 FR 97710 ¶3", "label": "89 FR 97710 ¶3", "kind": "rule", "snippet": "G0556", "score": 0.0}
mock_retrieve.return_value = [src]
mock_cited.return_value = [cited]
mock_ev.return_value = ValuationEvidence(("G0556",), ("APCM",), (), ())
client = MockClient.return_value.__enter__.return_value
resp = client.stream.return_value.__enter__.return_value
resp.iter_lines.return_value = iter(['{"message":{"content":"x"},"done":true}'])
events = list(stream_answer("APCM?", cfg=CFG, pool=self._pool()))
assert events[0]["type"] == "valuation" and events[0]["codes"] == ["G0556"]
assert events[1] == {"type": "token", "text": "x"}
assert events[-2]["sources"] == [src, cited]
mock_cited.assert_called_once_with(mock_engine.return_value, ("G0556",), per_code=CFG.code_cited_per_code)
body = client.stream.call_args.kwargs["json"]
assert "Valuation (authoritative" in body["messages"][1]["content"]
-
Step 2: Run to verify failure —
uv run --no-sync pytest tests/llm/test_rag.py -q -p no:cacheprovider→ FAIL (TypeError: build_messages() got an unexpected keyword argument 'evidence',AttributeError: llm.rag has no attribute valuation_evidence). -
Step 3: Implement
_SYSTEM +=
" A Valuation section may follow the excerpts. Its rows are "
"authoritative for RVUs, conversion factors and payment amounts; when "
"you state any of those numbers, cite the row's bracketed label exactly, "
"e.g. [PFS CY2026 Addendum B]. Do not compute, extrapolate or convert "
"numbers beyond what the rows show; if a code is listed as not priced, "
"say so."
Imports: from llm.evidence import ValuationEvidence, code_cited_sources, merge_sources, valuation_evidence and from llm.index import _engine.
def build_messages(
question: str, sources: list[dict], evidence: ValuationEvidence | None = None
) -> list[dict]:
... # context as today
parts = [f"Excerpts:\n\n{context}"]
if evidence is not None:
parts.append(evidence.prompt_block())
parts.append(f"Question: {question}")
user = "\n\n".join(parts)
...
def stream_answer(question, *, cfg, pool, since=""):
sources = retrieve(question, cfg=cfg, pool=pool, since=since)
evidence = valuation_evidence(question, cfg)
if evidence is not None:
cited = code_cited_sources(_engine(cfg), evidence.codes, per_code=cfg.code_cited_per_code)
sources = merge_sources(sources, cited)
yield evidence.payload()
pool.check(cfg.instruct_model)
messages = build_messages(question, sources, evidence)
... # unchanged
Update the module docstring's event list.
-
Step 4: Run to verify pass —
uv run --no-sync pytest tests/llm -q -p no:cacheprovider→ pass. -
Step 5: Commit
git add src/llm/rag.py tests/llm/test_rag.py
git commit -m "feat(llm): chat cites code valuations — Valuation prompt block, merged code-cited sources, valuation SSE event (refs P48)"
Task 7: chat.html — render the valuation table
Files:
-
Modify:
src/llm/web/chat.html -
Test:
tests/llm/test_api.py -
Step 1: Write the failing test (extend
test_serves_chat_page)
def test_serves_chat_page_with_valuation_renderer(self):
r = client.get("/")
assert r.status_code == 200
html = r.text
assert "function renderValuation" in html
assert "ev.type === 'valuation'" in html
assert "table.valuation" in html
-
Step 2: Run to verify failure —
uv run --no-sync pytest tests/llm/test_api.py -q -p no:cacheprovider→ FAIL on the new assertions. -
Step 3: Implement
CSS (next to details.sources):
table.valuation {
border-collapse: collapse; width: 100%; font-size: 13px; margin-top: 4px;
font-variant-numeric: tabular-nums;
}
table.valuation th, table.valuation td {
border: 1px solid var(--border); padding: 4px 6px; text-align: right; white-space: nowrap;
}
table.valuation th { font-family: var(--font-mono); font-size: 10px; letter-spacing: .08em;
text-transform: uppercase; color: var(--muted-fg); }
table.valuation td:nth-child(-n+3), table.valuation th:nth-child(-n+3) { text-align: left; }
table.valuation tr.proposed td { font-style: italic; opacity: .85; }
.valuation-wrap { overflow-x: auto; }
.provenance { font-size: 12px; color: var(--muted-fg); margin-top: 4px; }
.provenance .cite { margin-right: 4px; }
JS (next to renderSources):
const VAL_COLS = [
['code', 'Code'], ['description', 'Description'], ['vintage', 'Vintage'], ['status', 'St'],
['work', 'Work'], ['pe_nf', 'PE NF'], ['pe_f', 'PE F'], ['mp', 'MP'],
['total_nf', 'Total NF'], ['total_f', 'Total F'], ['cf', 'CF'], ['pay_nf', 'Pay NF'], ['pay_f', 'Pay F'],
];
function fmt(key, v) {
if (v === null || v === undefined) return '—';
if (key === 'cf') return '$' + Number(v).toFixed(4);
if (key === 'pay_nf' || key === 'pay_f') return '$' + Number(v).toFixed(2);
if (typeof v === 'number') return v.toFixed(2);
return String(v);
}
function renderValuation(wrap, ev) {
const rows = ev.rows || [];
const box = document.createElement('div');
box.className = 'valuation-wrap';
if (rows.length) {
const t = document.createElement('table');
t.className = 'valuation';
const thead = t.createTHead().insertRow();
for (const [, name] of VAL_COLS) { const th = document.createElement('th'); th.textContent = name; thead.appendChild(th); }
const tb = t.createTBody();
for (const r of rows) {
const tr = tb.insertRow();
if (r.proposed) tr.className = 'proposed';
for (const [key] of VAL_COLS) tr.insertCell().textContent = fmt(key, r[key]);
}
box.appendChild(t);
}
const prov = document.createElement('div');
prov.className = 'provenance';
for (const p of ev.provenance || []) {
const c = document.createElement('span'); c.className = 'cite'; c.textContent = p.label;
prov.appendChild(c);
const a = document.createElement('a'); a.href = p.url; a.target = '_blank'; a.rel = 'noopener';
a.textContent = p.citation || p.vintage;
prov.appendChild(a);
prov.appendChild(document.createTextNode(' '));
}
if ((ev.unpriced || []).length) {
prov.appendChild(document.createTextNode('not priced: ' + ev.unpriced.join(', ')));
}
box.appendChild(prov);
wrap.appendChild(box);
log.scrollTop = log.scrollHeight;
}
Dispatch: add else if (ev.type === 'valuation') renderValuation(wrap, ev); after the token branch. The table lands under the bubble (before the sources drawer that arrives later).
-
Step 4: Run to verify pass —
uv run --no-sync pytest tests/llm/test_api.py -q -p no:cacheprovider→ pass. Also open the page locally (uv run stack llm serve --port 8010, thencurl -s localhost:8010/ | grep -c renderValuation) to be sure the HTML is well-formed (no JS syntax error in the browser console if you can open it). -
Step 5: Commit
git add src/llm/web/chat.html tests/llm/test_api.py
git commit -m "feat(llm): chat UI renders the valuation table with FR provenance links (refs P48)"
Task 8: compose mount, rollout, live probes
Files:
-
Modify:
compose.yml(llmservice) -
Step 1: compose
Add to the llm service:
volumes:
- ./data:/app/data:ro # DuckDB read replica for code valuations (directory mount survives replica re-publish)
environment:
...
- LLM_DUCKDB_REPLICA=/app/data/aco.ro.duckdb
Run docker compose config --services >/dev/null && echo ok. Commit: chore(compose): llm reads the DuckDB replica read-only (refs P48).
-
Step 2: Full suite —
uv run --no-sync pytest tests -q -p no:cacheprovider -n auto→ all pass. -
Step 3: Rollout (from /home/kert/stack on main, after merge)
set -a; . ./.env; set +a
uv run stack llm index --collection rules --force # stamp codes metadata on the 80 rules (~15 min)
# roll the container: COMMIT_SHA in .env → HEAD, then
docker compose build llm && docker compose up -d llm && docker compose ps llm
docker exec llm curl -s http://localhost:8000/health
# probes
docker exec llm curl -s -N -X POST http://localhost:8000/chat -H 'Content-Type: application/json' \
-d '{"question":"What is APCM and how is it valued?"}' | tee .state/llm/probe-apcm.log | grep -c '"type": "valuation"'
grep -o '"code": "G055[678]"' .state/llm/probe-apcm.log | sort | uniq -c # 3 codes × 3 vintages
grep -o '\[PFS CY2026 Addendum B\]\|\[CY2027 NPRM Addendum B\]' .state/llm/probe-apcm.log | sort | uniq -c # cited in prose
grep -c '"kind": "rule"' .state/llm/probe-apcm.log # code-cited paragraphs present
docker exec llm curl -s -N -X POST http://localhost:8000/chat -H 'Content-Type: application/json' \
-d '{"question":"What did commenters say about telehealth?"}' | grep -c '"type": "valuation"' # must be 0
Record the probe results (counts + the first prose paragraph) on the tracker issue for P48 and update the memory note llm_service_whole_library.md with the valuation feature and the LLM_DUCKDB_REPLICA contract.