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
+2 -4
View File
@@ -126,9 +126,7 @@ def plan_turn(
best_cosine = max((c.cosine for c in chunks), default=0.0)
fts_hits = sum(1 for c in chunks if c.fts_hit)
if best_cosine >= settings.relevance_threshold or fts_hits > 0:
docs = select_documents(
chunks, n=settings.top_n_docs, max_chars=settings.max_context_chars
)
docs = select_documents(chunks, n=settings.top_n_docs)
return TurnPlan(
best_cosine,
fts_hits,
@@ -144,7 +142,7 @@ def plan_turn(
fts_hits,
True,
build_deflect_prompt(titles, notes=steering),
select_documents(chunks, n=settings.top_n_docs, max_chars=settings.max_context_chars),
select_documents(chunks, n=settings.top_n_docs),
derive_suggestions(titles, settings.suggestions),
len(steering),
)
-1
View File
@@ -55,7 +55,6 @@ class Settings(BaseSettings):
# default never discriminated. LOW only fires when best cosine < this
# AND no candidate chunk matches the question lexically (see A8).
relevance_threshold: float = 0.62
max_context_chars: int = 24_000
#: Maximum output tokens a chat answer may use (owner instruction
#: 2026-08-22: answers must run to their natural end — the old hard
#: 700-token cap cut long answers off mid-sentence).
+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]