diff --git a/src/llm/config.py b/src/llm/config.py
index 180ee10..bf12db4 100644
--- a/src/llm/config.py
+++ b/src/llm/config.py
@@ -57,6 +57,9 @@ class LlmConfig:
lineage_on_demand_max: int = 3
lineage_sources_max: int = 8
chat_codes_max: int = 24
+ #: IOM sections cited by retrieved rule paragraphs pulled in as
+ #: companion sources per turn (llm.iomlink)
+ iom_companion_max: int = 4
valuation_rows_max: int = 24
@@ -133,6 +136,7 @@ def load() -> LlmConfig:
_opt(section, "code_cited_collections", ["rules", "comments", "corpus"])
),
code_cited_max=int(_opt(section, "code_cited_max", 12)),
+ iom_companion_max=int(_opt(section, "iom_companion_max", 4)),
lineage_max_rows=int(_opt(section, "lineage_max_rows", 25)),
lineage_on_demand_max=int(_opt(section, "lineage_on_demand_max", 3)),
lineage_sources_max=int(_opt(section, "lineage_sources_max", 8)),
diff --git a/src/llm/iomlink.py b/src/llm/iomlink.py
new file mode 100644
index 0000000..da0c948
--- /dev/null
+++ b/src/llm/iomlink.py
@@ -0,0 +1,121 @@
+"""Rule discussions ↔ manual sections: the IOM sections a retrieved rule
+paragraph cites, pulled in as companion sources.
+
+A Federal Register paragraph that says "see Pub. 100-04, Chapter 12,
+Section 30.6.4" is pointing the reader at the operational rule; the
+manual chapters are indexed by section (``llm.manuals``), so the cited
+section's own text can sit beside the paragraph in the chat's sources —
+labelled ``IOM 100-04 ch.12 §30.6.4``, linked to the chapter PDF page,
+and marked with the FR paragraph(s) that cite it. Chapter-only citations
+("Chapter 12") resolve to the chapter's first section; a section the
+manual splits finer than the citation ("30.6" → 30.6.1) resolves to the
+first section under it.
+
+ companions(engine, sources, max_total=…) → companion source dicts
+"""
+
+from __future__ import annotations
+
+import logging
+from typing import Any, Sequence
+
+from sqlalchemy import text
+
+from llm.links import as_source
+
+log = logging.getLogger(__name__)
+
+_EXACT = """
+SELECT e.document, e.cmetadata FROM langchain_pg_embedding e
+WHERE e.cmetadata->>'doctype' = 'manual' AND e.cmetadata->>'manual' = :pub
+ AND e.cmetadata->>'chapter' = :chapter AND e.cmetadata->>'iom_section' = :section
+ORDER BY (e.cmetadata->>'seq')::int LIMIT 1
+"""
+_PREFIX = """
+SELECT e.document, e.cmetadata FROM langchain_pg_embedding e
+WHERE e.cmetadata->>'doctype' = 'manual' AND e.cmetadata->>'manual' = :pub
+ AND e.cmetadata->>'chapter' = :chapter AND e.cmetadata->>'iom_section' LIKE :prefix
+ORDER BY (e.cmetadata->>'seq')::int LIMIT 1
+"""
+_CHAPTER = """
+SELECT e.document, e.cmetadata FROM langchain_pg_embedding e
+WHERE e.cmetadata->>'doctype' = 'manual' AND e.cmetadata->>'manual' = :pub
+ AND e.cmetadata->>'chapter' = :chapter AND coalesce(e.cmetadata->>'iom_section', '') <> ''
+ORDER BY (e.cmetadata->>'seq')::int LIMIT 1
+"""
+
+
+def lookup(conn: Any, pub: str, chapter: str, section: str) -> tuple[str, dict] | None:
+ """The first chunk of the cited section (exact, then finer sections
+ under it, then the chapter's first section for a chapter-only cite)."""
+ if section:
+ row = conn.execute(
+ text(_EXACT), {"pub": pub, "chapter": chapter, "section": section}
+ ).fetchone()
+ if row is None:
+ row = conn.execute(
+ text(_PREFIX),
+ {"pub": pub, "chapter": chapter, "prefix": section + ".%"},
+ ).fetchone()
+ else:
+ row = conn.execute(text(_CHAPTER), {"pub": pub, "chapter": chapter}).fetchone()
+ if row is None:
+ return None
+ return row[0] or "", dict(row[1] or {})
+
+
+def companions(
+ engine: Any, sources: Sequence[dict], *, max_total: int = 4
+) -> list[dict]:
+ """Manual-section sources for the IOM citations carried by the rule
+ sources in *sources* (``as_source``'s ``iom_refs``), most-cited
+ first, each with ``cited_by`` (the citing FR labels) and
+ ``companion = "iom"``; a section already present in *sources* is not
+ repeated. Never raises — a lookup failure logs and yields nothing."""
+ order: list[tuple[str, str, str]] = []
+ cited_by: dict[tuple[str, str, str], list[str]] = {}
+ for s in sources:
+ if s.get("kind") != "rule":
+ continue
+ for ref in s.get("iom_refs") or []:
+ key = (ref.get("pub", ""), ref.get("chapter", ""), ref.get("section", ""))
+ if not key[0] or not key[1]:
+ continue
+ if key not in cited_by:
+ cited_by[key] = []
+ order.append(key)
+ if s.get("label") and s["label"] not in cited_by[key]:
+ cited_by[key].append(s["label"])
+ if not order:
+ return []
+ # most-cited first, ties in order of appearance; sectioned cites
+ # before chapter-only ones
+ ranked = sorted(
+ order, key=lambda k: (-len(cited_by[k]), 0 if k[2] else 1, order.index(k))
+ )
+ present = {
+ (s.get("manual"), s.get("chapter"), s.get("iom_section")) for s in sources
+ }
+ out: list[dict] = []
+ seen: set[tuple[str, str, str]] = set()
+ try:
+ with engine.begin() as conn:
+ for pub, chapter, section in ranked:
+ if len(out) >= max_total:
+ break
+ hit = lookup(conn, pub, chapter, section)
+ if hit is None:
+ continue
+ doc, md = hit
+ found = (md.get("manual"), md.get("chapter"), md.get("iom_section"))
+ if found in seen or found in present:
+ continue
+ seen.add(found)
+ src = as_source({k: str(v) for k, v in md.items()}, doc, 0.0)
+ src["cited_by"] = list(cited_by[(pub, chapter, section)])
+ src["companion"] = "iom"
+ out.append(src)
+ except Exception as exc: # noqa: BLE001 — a companion lookup must never break the chat
+ log.warning("iom companions skipped: %s", exc)
+ return out
+ return out
diff --git a/src/llm/links.py b/src/llm/links.py
index 8073cd3..9a25d2b 100644
--- a/src/llm/links.py
+++ b/src/llm/links.py
@@ -95,6 +95,33 @@ def for_source(md: dict[str, str], snippet: str) -> tuple[str, str]:
return _corpus(md)
+def iom_refs_of(text: str) -> list[dict[str, str]]:
+ """``[{"pub", "chapter", "section", "label"}]`` for every CMS manual
+ citation in *text* ("Pub. 100-04, Chapter 12, Section 30.6.4"),
+ de-duplicated in order of appearance."""
+ from llm.manuals import label as manual_label
+ from pfs.guidance import iom_refs
+
+ out: list[dict[str, str]] = []
+ seen: set[tuple[str, str, str]] = set()
+ for pub, chapter, section in iom_refs(text or ""):
+ key = (pub, chapter, section)
+ if key in seen:
+ continue
+ seen.add(key)
+ out.append(
+ {
+ "pub": pub,
+ "chapter": chapter,
+ "section": section,
+ "label": manual_label(
+ {"manual": pub, "chapter": chapter, "iom_section": section}
+ ),
+ }
+ )
+ return out
+
+
def as_source(md: dict[str, str], text: str, score: float) -> dict:
"""The source dict the chat sends to the prompt and the UI.
@@ -121,6 +148,9 @@ def as_source(md: dict[str, str], text: str, score: float) -> dict:
"iom_section": md.get("iom_section", ""),
# theme slugs stamped by llm.themes (comma-joined), for filters and badges
"themes": md.get("themes", ""),
+ # IOM sections a rule paragraph cites (llm.iomlink pulls them in as
+ # companion sources; the search page links them)
+ "iom_refs": iom_refs_of(text) if kind == "rule" else [],
"url": url,
"title": md.get("title", ""),
"date": md.get("date", ""),
diff --git a/src/llm/rag.py b/src/llm/rag.py
index 9b58e6a..e13be63 100644
--- a/src/llm/rag.py
+++ b/src/llm/rag.py
@@ -417,8 +417,13 @@ def _user_message(
sources = retrieved + cited + lineage_src + manual + rest
if sources:
context = "\n\n".join(
- f"[{s['label']}] ({source_kind(s)}, {s.get('date', '') or 'undated'}) "
- f"{s['snippet']}"
+ f"[{s['label']}] ({source_kind(s)}, {s.get('date', '') or 'undated'}"
+ + (
+ f"; cited by {', '.join('[' + c + ']' for c in s['cited_by'])}"
+ if s.get("cited_by")
+ else ""
+ )
+ + f") {s['snippet']}"
for s in sources
)
else:
@@ -591,6 +596,21 @@ def build_messages(
]
+def _iom_companions(
+ sources: list[dict], cfg: LlmConfig, *, max_total: int
+) -> list[dict]:
+ """``llm.iomlink.companions`` over the current sources; never raises."""
+ if not any(s.get("kind") == "rule" and s.get("iom_refs") for s in sources):
+ return [] # nothing cites a manual: no engine, no query
+ try:
+ from llm.iomlink import companions
+
+ return companions(_engine(cfg), sources, max_total=max_total)
+ except Exception as e: # noqa: BLE001 — a companion bug must not break the chat
+ log.warning("iom companions skipped: %s", e)
+ return []
+
+
def _manual_sources(families: Sequence[str], cfg: LlmConfig) -> list[dict]:
"""Cursor-per-call wrapper around ``evidence.manual_sources`` — the
same cached-replica-cursor pattern ``valuation_evidence`` uses
@@ -747,6 +767,15 @@ def _stream_answer(
"dropped %d excerpt(s) using a family acronym for something else",
n_conflict,
)
+ # Rule discussions ↔ manual sections: the IOM sections the retrieved
+ # and cited FR paragraphs point at, as companion sources right after
+ # the CPT manual block (they share its budget slot, llm.iomlink).
+ if cfg.iom_companion_max > 0:
+ before = len(sources)
+ sources = merge_sources(
+ sources, _iom_companions(sources, cfg, max_total=cfg.iom_companion_max)
+ )
+ n_manual += len(sources) - before
if lineage is not None:
yield lineage.payload()
if evidence is not None:
diff --git a/src/llm/web/chat.html b/src/llm/web/chat.html
index 1fa068c..858d9f6 100644
--- a/src/llm/web/chat.html
+++ b/src/llm/web/chat.html
@@ -205,6 +205,7 @@
background: color-mix(in srgb, var(--primary) 12%, transparent); margin-right: 6px;
}
.src .date { color: var(--muted-fg); margin-left: 6px; }
+ .src .citedby { font-family: var(--font-mono); font-size: 11px; color: var(--accent); margin-left: 4px; }
.src a.viewer {
font-family: var(--font-mono); font-size: 11px; padding: 1px 6px; border-radius: 3px;
border: 1px solid var(--accent); color: var(--accent); text-decoration: none; margin-left: 6px;
@@ -660,6 +661,13 @@
if (src.title && src.kind !== 'comment') {
el.appendChild(document.createTextNode(' — ' + src.title));
}
+ // an IOM section pulled in because a retrieved rule paragraph cites it
+ if (src.cited_by && src.cited_by.length) {
+ const c = document.createElement('span');
+ c.className = 'citedby';
+ c.textContent = ' cited by ' + src.cited_by.map(l => '[' + l + ']').join(', ');
+ el.appendChild(c);
+ }
// A comment or article attachment on file opens in the PDF viewer
// on the page the passage was located on (rules link to the FR).
if (src.attachment && src.item_key && src.kind !== 'rule') {
diff --git a/src/llm/web/search.html b/src/llm/web/search.html
index dd77815..4cf9004 100644
--- a/src/llm/web/search.html
+++ b/src/llm/web/search.html
@@ -56,6 +56,8 @@
}
form#search input#q { flex: 1 1 320px; font-size: 16px; }
form#search input#docket, form#search input#year { width: 150px; }
+ form#search input#manual { width: 90px; } form#search input#chapter { width: 56px; }
+ form#search input#section { width: 90px; } form#search input#theme { width: 150px; }
form#search label {
display: flex; align-items: center; gap: 6px; font-size: 13px;
color: var(--muted-fg); white-space: nowrap;
@@ -95,6 +97,9 @@
text-decoration: underline dotted;
}
.hit .date, .hit .meta { color: var(--muted-fg); font-size: 12px; font-family: var(--font-mono); }
+ .hit a.iom {
+ font-family: var(--font-mono); font-size: 11px; color: var(--accent); text-decoration: underline dotted;
+ }
.hit a.viewer {
font-family: var(--font-mono); font-size: 11px; padding: 1px 6px; border-radius: 3px;
border: 1px solid var(--accent); color: var(--accent); text-decoration: none; white-space: nowrap;
@@ -144,6 +149,10 @@
+
+
+
+
@@ -190,6 +199,11 @@
if (val('kind')) p.set('kind', val('kind'));
if (val('docket')) p.set('docket', val('docket'));
if (val('year')) p.set('year', val('year'));
+ if (val('manual')) p.set('manual', val('manual'));
+ if (val('chapter')) p.set('chapter', val('chapter'));
+ if (val('section')) p.set('section', val('section'));
+ if (val('manual') || val('chapter') || val('section')) p.set('doctype', 'manual');
+ if (val('theme')) p.set('theme', val('theme'));
if (offset) p.set('offset', String(offset));
p.set('limit', String(LIMIT));
return p;
@@ -202,6 +216,10 @@
document.getElementById('kind').value = p.get('kind') || '';
document.getElementById('docket').value = p.get('docket') || '';
document.getElementById('year').value = p.get('year') || '';
+ document.getElementById('manual').value = p.get('manual') || '';
+ document.getElementById('chapter').value = p.get('chapter') || '';
+ document.getElementById('section').value = p.get('section') || '';
+ document.getElementById('theme').value = p.get('theme') || '';
offset = Math.max(0, parseInt(p.get('offset') || '0', 10) || 0);
return p.get('q') || '';
}
@@ -249,6 +267,18 @@
line.appendChild(view);
}
+ // a rule paragraph's CMS manual citations → the cited section, filtered
+ (r.iom_refs || []).forEach(function (ref) {
+ if (!ref.label) return;
+ var a = document.createElement('a');
+ a.className = 'iom';
+ var p = new URLSearchParams({ q: (document.getElementById('q') || {}).value || ref.label, doctype: 'manual', manual: ref.pub, chapter: ref.chapter });
+ if (ref.section) p.set('section', ref.section);
+ a.href = '/ui/search?' + p.toString();
+ a.textContent = 'cites ' + ref.label;
+ line.appendChild(a);
+ });
+
if (r.date) {
var date = document.createElement('span');
date.className = 'date';
diff --git a/src/pfs/guidance.py b/src/pfs/guidance.py
index a9ac53c..6e24024 100644
--- a/src/pfs/guidance.py
+++ b/src/pfs/guidance.py
@@ -296,6 +296,12 @@ def _cfr_refs(text: str) -> list[tuple[str, str]]:
return out
+def iom_refs(text: str) -> list[tuple[str, str, str]]:
+ """Public name for :func:`_iom_refs` — ``llm.links`` uses it to carry a
+ rule paragraph's IOM citations on its source dict."""
+ return _iom_refs(text)
+
+
def _iom_refs(text: str) -> list[tuple[str, str, str]]:
"""(pub, chapter, section) for every IOM reference in *text* —
*section* is ``""`` when the sentence names a chapter but no
diff --git a/tests/llm/test_config.py b/tests/llm/test_config.py
index 6b6b305..970e1fc 100644
--- a/tests/llm/test_config.py
+++ b/tests/llm/test_config.py
@@ -125,3 +125,20 @@ class TestStrTuple:
raw["llm"]["code_cited_collections"] = "rules"
monkeypatch.setattr(conf, "cfg", _Cfg(raw))
assert llm_config.load().code_cited_collections == ("rules",)
+
+
+def test_iom_companion_max_defaults_to_four():
+ from llm.config import LlmConfig
+
+ cfg = LlmConfig(
+ ollama_hosts=("http://h",),
+ embed_model="e",
+ instruct_model="i",
+ embed_dim=1,
+ build_ann_index=False,
+ pg_host="h",
+ pg_port=1,
+ pg_db="d",
+ pg_user="u",
+ )
+ assert cfg.iom_companion_max == 4
diff --git a/tests/llm/test_iomlink.py b/tests/llm/test_iomlink.py
new file mode 100644
index 0000000..99570ad
--- /dev/null
+++ b/tests/llm/test_iomlink.py
@@ -0,0 +1,169 @@
+"""llm.iomlink — cited IOM sections as companion sources."""
+
+from __future__ import annotations
+
+from unittest.mock import MagicMock
+
+from llm.iomlink import companions, lookup
+from llm.links import as_source, iom_refs_of
+
+RULE_TEXT = (
+ "We refer readers to Pub. 100-04, Chapter 12, Section 30.6.4 for incident-to "
+ "billing, and to the Medicare Benefit Policy Manual, Chapter 15, for coverage. "
+ "See also Pub. 100-04, Chapter 12, Section 30.6.4 again."
+)
+
+
+class TestIomRefsOnSources:
+ def test_rule_source_carries_deduplicated_refs(self):
+ refs = iom_refs_of(RULE_TEXT)
+ assert refs == [
+ {
+ "pub": "100-04",
+ "chapter": "12",
+ "section": "30.6.4",
+ "label": "IOM 100-04 ch.12 §30.6.4",
+ },
+ {
+ "pub": "100-02",
+ "chapter": "15",
+ "section": "",
+ "label": "IOM 100-02 ch.15",
+ },
+ ]
+ s = as_source({"kind": "rule", "item_key": "R", "title": "t"}, RULE_TEXT, 0.4)
+ assert [r["label"] for r in s["iom_refs"]] == [
+ "IOM 100-04 ch.12 §30.6.4",
+ "IOM 100-02 ch.15",
+ ]
+ assert (
+ as_source({"kind": "comment", "comment_id": "C-1"}, RULE_TEXT, 0.4)[
+ "iom_refs"
+ ]
+ == []
+ )
+
+
+def _engine(rows_by_sql):
+ """rows_by_sql: substring of SQL → row (document, cmetadata) or None."""
+ engine = MagicMock()
+ conn = engine.begin.return_value.__enter__.return_value
+ calls = []
+
+ def execute(sql, params=None):
+ calls.append((str(sql), params))
+ r = MagicMock()
+ for needle, row in rows_by_sql.items():
+ if needle in str(sql):
+ r.fetchone.return_value = row
+ return r
+ r.fetchone.return_value = None
+ return r
+
+ conn.execute.side_effect = execute
+ return engine, conn, calls
+
+
+MD = {
+ "doctype": "manual",
+ "kind": "corpus",
+ "manual": "100-04",
+ "chapter": "12",
+ "iom_section": "30.6.4",
+ "iom_title": "E/M Services Furnished Incident to",
+ "page": "39",
+ "item_key": "SPJB4FX2",
+ "seq": "73",
+ "url": "https://www.cms.gov/x/clm104c12.pdf",
+ "title": "Medicare Claims Processing Manual — Chapter 12",
+}
+
+
+class TestCompanions:
+ def _rule(self, label, text=RULE_TEXT):
+ return as_source(
+ {
+ "kind": "rule",
+ "item_key": "R",
+ "p_id": "5",
+ "page": "43949",
+ "ordinal": "4",
+ "fr_volume": "91",
+ "title": "t",
+ "html_url": "https://fr/x",
+ },
+ text,
+ 0.5,
+ ) | {"label": label}
+
+ def test_cited_section_becomes_a_companion_with_cited_by(self):
+ engine, conn, calls = _engine(
+ {"iom_section' = :section": ("## 30.6.4 - E/M ... incident to", MD)}
+ )
+ out = companions(
+ engine,
+ [self._rule("91 FR 43949 ¶4"), self._rule("91 FR 43950 ¶2")],
+ max_total=4,
+ )
+ assert len(out) == 1
+ c = out[0]
+ assert c["label"] == "IOM 100-04 ch.12 §30.6.4" and c["companion"] == "iom"
+ assert c["cited_by"] == ["91 FR 43949 ¶4", "91 FR 43950 ¶2"]
+ assert (
+ c["url"] == "https://www.cms.gov/x/clm104c12.pdf#page=39"
+ and c["doctype"] == "manual"
+ )
+ assert c["snippet"].startswith("## 30.6.4")
+
+ def test_prefix_and_chapter_fallbacks(self):
+ finer = dict(MD, iom_section="30.6.4.1")
+ engine, conn, calls = _engine({"LIKE :prefix": ("finer text", finer)})
+ assert lookup(conn, "100-04", "12", "30.6.4") == ("finer text", finer)
+ assert [p["prefix"] for _, p in calls if p and "prefix" in p] == ["30.6.4.%"]
+ first = dict(MD, iom_section="10")
+ engine, conn, calls = _engine({"<> ''": ("chapter first", first)})
+ assert lookup(conn, "100-02", "15", "") == ("chapter first", first)
+ engine, conn, calls = _engine({})
+ assert lookup(conn, "100-04", "99", "1") is None
+
+ def test_ranking_dedupe_cap_and_present_sections(self):
+ by = {
+ "iom_section' = :section": ("sec text", MD),
+ "<> ''": (
+ "chapter text",
+ dict(MD, chapter="15", manual="100-02", iom_section="10"),
+ ),
+ }
+ engine, conn, calls = _engine(by)
+ rules = [self._rule("A"), self._rule("B")]
+ out = companions(engine, rules, max_total=1)
+ assert [c["label"] for c in out] == [
+ "IOM 100-04 ch.12 §30.6.4"
+ ] # cited twice, sectioned → first; cap 1
+ # already among the sources → not repeated
+ present = as_source({k: str(v) for k, v in MD.items()}, "sec text", 0.9)
+ assert companions(engine, rules + [present], max_total=4) == [
+ c
+ for c in companions(engine, rules + [present], max_total=4)
+ if c["label"] != "IOM 100-04 ch.12 §30.6.4"
+ ]
+
+ def test_no_rule_sources_no_queries_and_failures_swallowed(self):
+ engine, conn, calls = _engine({})
+ assert (
+ companions(
+ engine,
+ [
+ as_source(
+ {"kind": "comment", "comment_id": "C"},
+ "Pub. 100-04, Chapter 12",
+ 0.1,
+ )
+ ],
+ )
+ == []
+ )
+ assert calls == []
+ engine = MagicMock()
+ engine.begin.side_effect = RuntimeError("db down")
+ assert companions(engine, [self._rule("A")]) == []
diff --git a/tests/llm/test_links.py b/tests/llm/test_links.py
index 7399c59..0fe89c8 100644
--- a/tests/llm/test_links.py
+++ b/tests/llm/test_links.py
@@ -156,6 +156,7 @@ class TestAsSource:
"chapter",
"iom_section",
"themes",
+ "iom_refs",
"id",
"label",
"kind",