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:
@@ -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)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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]",
|
||||||
|
|||||||
@@ -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"}
|
||||||
|
|||||||
Reference in New Issue
Block a user