fix(llm): chat table caption, status/CF notes, empty-url guard; heading codes (refs P48)

The valuation table now captions itself as national unadjusted amounts,
titles the status and CF cells with their notes, lists the unpaid-status
reasons under the table, and renders a citation with no URL as a span
instead of an <a href=""> that reloads the chat. The prompt block carries
the same two notes. Markdown chunks scan their heading, so a section
titled with a code stays findable by it past the first chunk.
This commit is contained in:
kert
2026-09-08 23:26:04 -04:00
parent 0bc53ccb7d
commit 6a6396b929
7 changed files with 92 additions and 9 deletions

View File

@@ -145,7 +145,10 @@ def chunk_doc(
"item_key": doc.key, "item_key": doc.key,
"seq": str(seq), "seq": str(seq),
"section": heading, "section": heading,
"codes": " ".join(find_codes(piece)), # The heading is scanned with the piece: a section titled
# "## G0556 — APCM" is about G0556 all the way down, but
# only its first chunk repeats the code.
"codes": " ".join(find_codes(f"{heading} {piece}")),
}, },
) )
for seq, (heading, piece) in enumerate(pieces) for seq, (heading, piece) in enumerate(pieces)

View File

@@ -49,11 +49,17 @@ class ValuationEvidence:
def prompt_block(self) -> str: def prompt_block(self) -> str:
lines = [_HEADER] lines = [_HEADER]
for r in self.rows: for r in self.rows:
# The status note says why an unpaid code shows no dollars, and
# the CF note says which of the year's CFs the money came from.
status = f"{r.status or '?'}" + (
f"; {r.status_note}" if r.status_note else ""
)
cf = _n(r.cf, 4, money=True) + (f" [{r.cf_note}]" if r.cf_note else "")
lines.append( lines.append(
f'{r.label} {r.code} "{r.description}" — status {r.status or "?"}; ' f'{r.label} {r.code} "{r.description}" — status {status}; '
f"work {_n(r.work)}; PE {_n(r.pe_nf)} non-fac / {_n(r.pe_f)} fac; " 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"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"CF {cf} → payment {_n(r.pay_nf, money=True)} non-fac / "
f"{_n(r.pay_f, money=True)} fac" f"{_n(r.pay_f, money=True)} fac"
) )
if self.unpriced: if self.unpriced:

View File

@@ -77,6 +77,10 @@
text-transform: uppercase; color: var(--muted-fg); } 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 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; } table.valuation tr.proposed td { font-style: italic; opacity: .85; }
table.valuation caption {
caption-side: bottom; text-align: left; font-size: 11px;
color: var(--muted-fg); padding: 4px 2px 0;
}
.valuation-wrap { overflow-x: auto; } .valuation-wrap { overflow-x: auto; }
.provenance { font-size: 12px; color: var(--muted-fg); margin-top: 4px; } .provenance { font-size: 12px; color: var(--muted-fg); margin-top: 4px; }
.provenance .cite { margin-right: 4px; } .provenance .cite { margin-right: 4px; }
@@ -194,7 +198,8 @@
const VAL_COLS = [ const VAL_COLS = [
['code', 'Code'], ['description', 'Description'], ['vintage', 'Vintage'], ['status', 'St'], ['code', 'Code'], ['description', 'Description'], ['vintage', 'Vintage'], ['status', 'St'],
['work', 'Work'], ['pe_nf', 'PE NF'], ['pe_f', 'PE F'], ['mp', 'MP'], ['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'], ['total_nf', 'Total NF'], ['total_f', 'Total F'], ['cf', 'CF (non-APM)'],
['pay_nf', 'Pay NF'], ['pay_f', 'Pay F'],
]; ];
function fmt(key, v) { function fmt(key, v) {
if (v === null || v === undefined) return '—'; if (v === null || v === undefined) return '—';
@@ -210,13 +215,21 @@
if (rows.length) { if (rows.length) {
const t = document.createElement('table'); const t = document.createElement('table');
t.className = 'valuation'; t.className = 'valuation';
t.createCaption().textContent =
'National unadjusted amounts (total RVUs \u00d7 CF); no geographic (GPCI) adjustment.';
const thead = t.createTHead().insertRow(); const thead = t.createTHead().insertRow();
for (const [, name] of VAL_COLS) { const th = document.createElement('th'); th.textContent = name; thead.appendChild(th); } for (const [, name] of VAL_COLS) { const th = document.createElement('th'); th.textContent = name; thead.appendChild(th); }
const tb = t.createTBody(); const tb = t.createTBody();
for (const r of rows) { for (const r of rows) {
const tr = tb.insertRow(); const tr = tb.insertRow();
if (r.proposed) tr.className = 'proposed'; if (r.proposed) tr.className = 'proposed';
for (const [key] of VAL_COLS) tr.insertCell().textContent = fmt(key, r[key]); for (const [key] of VAL_COLS) {
const td = tr.insertCell();
td.textContent = fmt(key, r[key]);
// why a status is not paid / which of the year's CFs this is
if (key === 'status' && r.status_note) td.title = r.status_note;
if (key === 'cf' && r.cf_note) td.title = r.cf_note;
}
} }
box.appendChild(t); box.appendChild(t);
} }
@@ -225,14 +238,31 @@
for (const p of ev.provenance || []) { for (const p of ev.provenance || []) {
const c = document.createElement('span'); c.className = 'cite'; c.textContent = p.label; const c = document.createElement('span'); c.className = 'cite'; c.textContent = p.label;
prov.appendChild(c); prov.appendChild(c);
const a = document.createElement('a'); a.href = p.url; a.target = '_blank'; a.rel = 'noopener'; let cite;
a.textContent = p.citation || p.vintage; if (p.url) {
prov.appendChild(a); cite = document.createElement('a');
cite.href = p.url; cite.target = '_blank'; cite.rel = 'noopener';
} else {
cite = document.createElement('span'); // an empty href reloads the chat
}
cite.textContent = p.citation || p.vintage;
prov.appendChild(cite);
prov.appendChild(document.createTextNode(' ')); prov.appendChild(document.createTextNode(' '));
} }
if ((ev.unpriced || []).length) { if ((ev.unpriced || []).length) {
prov.appendChild(document.createTextNode('not priced: ' + ev.unpriced.join(', '))); prov.appendChild(document.createTextNode('not priced: ' + ev.unpriced.join(', ')));
} }
const notes = [];
for (const r of rows) {
if (!r.status_note) continue;
const line = r.code + ' (' + (r.status || '?') + '): ' + r.status_note;
if (!notes.includes(line)) notes.push(line);
}
for (const line of notes) {
const d = document.createElement('div');
d.textContent = line;
prov.appendChild(d);
}
box.appendChild(prov); box.appendChild(prov);
wrap.appendChild(box); wrap.appendChild(box);
log.scrollTop = log.scrollHeight; log.scrollTop = log.scrollHeight;

View File

@@ -31,6 +31,14 @@ class TestIndex:
assert "function renderValuation" in html assert "function renderValuation" in html
assert "ev.type === 'valuation'" in html assert "ev.type === 'valuation'" in html
assert "table.valuation" in html assert "table.valuation" in html
# the money on screen is national and unadjusted — say so
assert "no geographic (GPCI) adjustment" in html
assert "createCaption" in html
# notes explain an unpaid status and which CF is shown
assert "r.status_note" in html and "r.cf_note" in html
assert "'cf', 'CF (non-APM)'" in html
# a citation without a URL is not an empty <a href="">
assert "if (p.url)" in html
class TestWhoami: class TestWhoami:

View File

@@ -162,6 +162,15 @@ class TestCodesMetadata:
assert "99490" in found assert "99490" in found
assert "" in found assert "" in found
def test_heading_codes_stamped_on_every_piece_of_the_section(self):
"""A section headed by the code it discusses ("## G0556 — APCM")
splits into several chunks; the ones past the first never repeat
the code, and used to be unfindable by it."""
text = "## G0556 — APCM\n\n" + "We propose to value the service. " * 20
chunks = chunk_doc(_doc(text), target_chars=120, overlap_chars=10)
assert len(chunks) > 1
assert all(c.metadata["codes"] == "G0556" for c in chunks)
def test_rule_paragraph_chunks_get_codes(self): def test_rule_paragraph_chunks_get_codes(self):
from llm.chunk import Paragraph from llm.chunk import Paragraph

View File

@@ -71,6 +71,27 @@ class TestEvidenceText:
"Not priced in the indexed fee schedule: Z9999" "Not priced in the indexed fee schedule: Z9999"
) )
def test_prompt_block_labels_the_cf_and_an_unpaid_status(self):
ev = ValuationEvidence(
("G0559",),
(),
(
_row(
code="G0559",
status="B",
status_note="bundled — no separate PFS payment",
cf_note="non-APM standard CF; QP CF $33.5675",
pay_nf=None,
pay_f=None,
),
),
(),
)
line = ev.prompt_block().splitlines()[1]
assert "— status B; bundled — no separate PFS payment; work" in line
assert "CF $33.4009 [non-APM standard CF; QP CF $33.5675] → payment" in line
assert "payment n/a non-fac / n/a fac" in line
def test_prompt_block_null_components(self): def test_prompt_block_null_components(self):
ev = ValuationEvidence( ev = ValuationEvidence(
("G9999",), ("G9999",),
@@ -121,6 +142,7 @@ class TestEvidenceText:
and p["families"] == ["APCM"] and p["families"] == ["APCM"]
) )
assert p["rows"][0]["pay_nf"] == 16.37 and p["rows"][1]["proposed"] is True assert p["rows"][0]["pay_nf"] == 16.37 and p["rows"][1]["proposed"] is True
assert p["rows"][0]["status_note"] == "" and p["rows"][0]["cf_note"] == ""
assert p["provenance"] == [ assert p["provenance"] == [
{ {
"label": "[PFS CY2026 Addendum B]", "label": "[PFS CY2026 Addendum B]",

View File

@@ -296,10 +296,13 @@ class TestStreamAnswer:
pool.serves.return_value = serves_big pool.serves.return_value = serves_big
return pool return pool
@patch("llm.rag._engine")
@patch("llm.rag.valuation_evidence", return_value=None) @patch("llm.rag.valuation_evidence", return_value=None)
@patch("llm.rag.httpx.Client") @patch("llm.rag.httpx.Client")
@patch("llm.rag.retrieve") @patch("llm.rag.retrieve")
def test_yields_tokens_then_sources_then_done(self, mock_retrieve, MockClient, _ev): def test_yields_tokens_then_sources_then_done(
self, mock_retrieve, MockClient, _ev, mock_engine
):
src = { src = {
"id": "C1", "id": "C1",
"label": "C1", "label": "C1",
@@ -322,6 +325,8 @@ class TestStreamAnswer:
events = list(stream_answer("q", cfg=CFG, pool=pool)) events = list(stream_answer("q", cfg=CFG, pool=pool))
pool.check.assert_called_once_with("chat") pool.check.assert_called_once_with("chat")
# 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="") mock_retrieve.assert_called_once_with("q", cfg=CFG, pool=pool, since="")
assert events[0] == {"type": "token", "text": "Doc"} assert events[0] == {"type": "token", "text": "Doc"}
assert events[1] == {"type": "token", "text": "tors"} assert events[1] == {"type": "token", "text": "tors"}