feat(rag): lite-model document summaries — non-markdown docs summarized at import, summary chunk retrieves and resolves to the full source doc

This commit is contained in:
2026-08-25 17:48:37 -04:00
parent 9809482a4b
commit 572a4190a6
32 changed files with 1806 additions and 26 deletions
+138 -1
View File
@@ -7,6 +7,7 @@ chat integration tests against real Postgres; the pure mapping logic in
from __future__ import annotations
import uuid
from types import SimpleNamespace
import pytest
@@ -97,7 +98,8 @@ from app.rag.retriever import fuse, lexical_tsquery # noqa: E402
def _rc(
doc_path: str, cosine: float = 0.0, fts_hit: bool = False, position: int = 0
doc_path: str, cosine: float = 0.0, fts_hit: bool = False, position: int = 0,
is_summary: bool = False,
) -> RetrievedChunk:
return RetrievedChunk(
chunk_id=uuid.uuid4(),
@@ -107,6 +109,7 @@ def _rc(
document=_doc(doc_path, "x" * 20),
cosine=cosine,
fts_hit=fts_hit,
is_summary=is_summary,
)
@@ -186,3 +189,137 @@ def test_fuse_rejects_nonpositive_k() -> None:
def test_fuse_empty_lists() -> None:
assert fuse([], [], k=60) == []
# ---------------------------------------------------------------------------
# Phase 30: is_summary survives both candidate lists and the fusion
# ---------------------------------------------------------------------------
from app.models import Chunk # noqa: E402
from app.rag.retriever import _lexical_candidates, _vector_candidates # noqa: E402
class _FakeResult:
"""Stands in for SQLAlchemy's RowMapping result (``.all()`` only)."""
def __init__(self, rows: list) -> None:
self._rows = rows
def all(self) -> list:
return self._rows
class _FakeSession:
"""Returns canned rows from ``execute`` without touching Postgres."""
def __init__(self, rows: list) -> None:
self._rows = rows
self.statements: list = []
def execute(self, stmt, params: dict | None = None) -> _FakeResult:
self.statements.append((stmt, params))
return _FakeResult(self._rows)
def _chunk_row(is_summary: bool) -> Chunk:
doc = _doc("summary-src.yaml", "RAW_YAML_CONTENT")
return Chunk(
id=uuid.uuid4(),
document_id=doc.id,
position=-1, # the summary chunk's position (phase 30)
content="Summary text",
is_summary=is_summary,
)
def test_vector_candidates_carry_is_summary_flag() -> None:
"""The vector list copies ``Chunk.is_summary`` onto each candidate."""
doc = _doc("summary-src.yaml", "RAW_YAML_CONTENT")
summary = _chunk_row(is_summary=True)
ordinary = _chunk_row(is_summary=False)
ordinary.position = 0
ordinary.content = "ordinary content"
rows = [
(summary, 0.123456, doc),
(ordinary, 0.2, doc),
]
out = _vector_candidates(_FakeSession(rows), [0.0] * 768, limit=5) # pyright: ignore[reportArgumentType]
assert len(out) == 2
by_pos = {rc.position: rc for rc in out}
assert by_pos[-1].is_summary is True # the summary chunk (position −1)
assert by_pos[0].is_summary is False # ordinary content chunk
assert by_pos[-1].cosine == pytest.approx(0.876544) # 1 − distance, still rounded
def test_vector_candidates_default_is_summary_false_for_legacy_chunks() -> None:
"""Pre-phase-30 rows have ``is_summary=false`` — candidates stay False."""
doc = _doc("legacy.md", "LEGACY")
legacy = Chunk(
id=uuid.uuid4(),
document_id=doc.id,
position=0,
content="legacy content",
is_summary=False,
)
out = _vector_candidates(_FakeSession([(legacy, 0.5, doc)]), [0.0] * 768, limit=5) # pyright: ignore[reportArgumentType]
assert out[0].is_summary is False
def _lexical_row(is_summary: bool, doc_path: str) -> object:
"""One row of ``_LEXICAL_SQL`` (attribute access, as SQLAlchemy returns)."""
doc = _doc(doc_path, "DOC_BODY")
return SimpleNamespace(
chunk_id=uuid.uuid4(),
position=-1 if is_summary else 0,
content="summary chunk text" if is_summary else "content chunk text",
doc_id=doc.id,
source=doc.source,
path=doc.path,
full_path=doc.full_path,
title=doc.title,
doc_content=doc.content,
content_hash=doc.content_hash,
indexed_at=None,
is_summary=is_summary,
rank=0.33,
)
def test_lexical_candidates_carry_is_summary_flag() -> None:
"""The lexical list reads ``c.is_summary`` from the raw row."""
rows = [_lexical_row(True, "summary-src.yaml"), _lexical_row(False, "other.md")]
out = _lexical_candidates(_FakeSession(rows), "how do i configure the thing", limit=10) # pyright: ignore[reportArgumentType]
assert len(out) == 2
by_path = {rc.document.path: rc for rc in out}
assert by_path["summary-src.yaml"].is_summary is True
assert by_path["summary-src.yaml"].position == -1
assert by_path["other.md"].is_summary is False
assert all(rc.fts_hit is True for rc in out)
def test_fuse_keeps_is_summary_on_double_hit() -> None:
"""A summary chunk in both lists keeps the flag after fusion."""
v1 = _rc("s.yaml", cosine=0.9, is_summary=True)
l1 = _rc("s.yaml", cosine=0.9, is_summary=True) # lexical copy of the same chunk
l1.chunk_id = v1.chunk_id
out = fuse([v1], [l1], k=60)
assert len(out) == 1
assert out[0].is_summary is True
assert out[0].fts_hit is True
assert out[0].score == pytest.approx(2 / 61)
def test_fuse_keeps_is_summary_on_lexical_only_hit() -> None:
"""A summary-only lexical hit (no vector rank) keeps the flag."""
out = fuse([], [_rc("s.yaml", is_summary=True)], k=60)
assert len(out) == 1
assert out[0].is_summary is True
assert out[0].fts_hit is True
assert out[0].cosine == 0.0
def test_fuse_default_is_summary_stays_false_for_legacy_chunks() -> None:
"""Neither list flagged ⇒ fusion never invents a summary flag."""
out = fuse([_rc("a.md", cosine=0.8)], [_rc("b.md")], k=60)
assert len(out) == 2
assert all(rc.is_summary is False for rc in out)