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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user