feat(rag): feed whole matched documents to the LLM — no context truncation (A7 revised)

This commit is contained in:
2026-08-24 23:37:44 -04:00
parent d7a4064616
commit 1e6ae360e0
16 changed files with 923 additions and 60 deletions
+15 -23
View File
@@ -10,10 +10,12 @@
fused score ranks; :meth:`select_documents` and :func:`weak_hit_titles`
keep working off ``score``.
The product requirement is unchanged (LOCKED A7): the LLM receives the
**entire relevant document**, not just the chunk — chunk hits map back to
their parents, dedupe, rank by best fused score, and the combined context
is capped at ``BOR_MAX_CONTEXT_CHARS``.
The product requirement (LOCKED A7, revised 2026-08-24): the LLM receives
the **entire relevant document**, not just the chunk — chunk hits map back
to their parents, dedupe, rank by best fused score, and the full text of
the top-N documents is always fed through, never truncated. If a future KB
ever makes the prompt too large for the model, the ``LLMError`` → SSE
``error`` path surfaces it loudly — no silent partial context.
Deterministic tie-break for equal fused scores:
``(−fused, −cosine, document.path, chunk.position)``.
@@ -31,7 +33,9 @@ from sqlalchemy.orm import Session
from app.config import get_settings
from app.models import Chunk, Document
#: Marker appended when the context budget is exceeded (PLAN §6).
#: Shared overflow marker — now used by the steering (<tuning>) section
#: only (phase 15; imported by ``app.rag.prompts``). The document context
#: path never truncates (A7 revised, owner permission 2026-08-24).
TRUNCATION_MARKER = "[…truncated…]"
#: Alphanumeric tokens of a question (``to_tsquery`` input, OR-joined).
@@ -254,19 +258,17 @@ def weak_hit_titles(chunks: Sequence[RetrievedChunk]) -> list[str]:
def select_documents(
chunks: Sequence[RetrievedChunk],
n: int | None = None,
max_chars: int | None = None,
) -> list[Document]:
"""Map chunk hits to distinct parent documents, ranked by best fused score.
At most *n* documents are returned (default ``BOR_TOP_N_DOCS``). The
returned rows carry the full document content; if the combined content
would exceed *max_chars* (default ``BOR_MAX_CONTEXT_CHARS``), the
lowest-ranked overflowing document is truncated in place with the
``[…truncated…]`` marker so the assembled context never exceeds the
budget (PLAN §6).
returned rows carry the full document content, byte-identical — a
matched parent document is **never truncated** (A7 revised, owner
permission 2026-08-24). There is deliberately no context budget: an
oversized prompt must fail loudly through the ``LLMError`` → SSE
``error`` path, never arrive as silent partial context.
"""
top_n = n if n is not None else get_settings().top_n_docs
budget = max_chars if max_chars is not None else get_settings().max_context_chars
docs: list[Document] = []
seen: set[uuid.UUID] = set()
@@ -275,14 +277,4 @@ def select_documents(
continue
seen.add(rc.document.id)
docs.append(rc.document)
docs = docs[:top_n]
remaining = budget
for doc in docs:
if len(doc.content) <= remaining:
remaining -= len(doc.content)
else:
keep = max(0, remaining - len(TRUNCATION_MARKER))
doc.content = doc.content[:keep] + TRUNCATION_MARKER
remaining = 0
return docs
return docs[:top_n]