3.8 KiB
Story: Document Summaries (lite-model summaries for non-markdown docs)
Phase: 30_document_summaries.md · E2E: tests/e2e/test_document_summaries.py
Narrative
As any user of Reese, I ask questions about notes that live in machine-formatted files (yaml, json, py, txt). Raw flags and keys embed badly, so retrieval misses those documents. I want a small model to analyze every non-markdown document at import time and store a natural-language summary with a pointer back to the source. When retrieval hits a summary, Reese follows the pointer and answers from the full source document — the summary is a retrieval target, never the answer.
- Given a knowledge base containing non-markdown A9 documents (yaml, yml, json, py, txt)
- When I run
import_docsand then ask a question those documents answer - Then each non-markdown document carries a
lite-model summary (documents.summaryplus one embeddedis_summarychunk at position −1), markdown documents carry neither, and a question whose best match is a summary chunk gets an answer grounded in the full source document, with the hit counted in the per-turn log line (summary_hits=N).
Acceptance criteria
BOR_LLM_SUMMARY_MODEL(defaultlite) + non-streamingLLMClient.chat()— A5 extended (same OpenAI-compatible endpoint), empty/missing content raisesLLMError(no silent summaries).- Migration 0004:
documents.summary TEXT NULL+chunks.is_summary BOOLEAN NOT NULL DEFAULT FALSE(reversible, integration-tested up/down). app/rag/summarizer.py: theSUMMARY_MODEprompt (document content capped atBOR_SUMMARY_MAX_CHARS, overflow marked with the shared truncation marker), the lite call, output validation, and the code-deterministicSource: <source>/<path>pointer line (never model-generated).- Importer: every non-markdown file gets its summary stored + indexed
(best-effort — a lite failure logs, counts in
summary_errors, and still leaves the document fully indexed); markdown files get neither; re-import replaces the old summary chunk (exactly one at a time). is_summaryflows through the retriever's vector and lexical candidate lists andfuse;TurnPlan.summary_hitscounts hit chunks withis_summarywhose parent document made the selected top-N context; the per-turn log line recordssummary_hits=Nafterfts_hits=N(PLAN §9 extension).- A summary hit resolves to its parent through the unchanged chunk→document mapping — the LLM receives the full source document (A7 revised: never truncated).
.env.example+ README documentBOR_LLM_SUMMARY_MODEL/BOR_SUMMARY_MAX_CHARSand the fail-soft behavior; the deterministic mock LLM answersSUMMARY_MODEwith a byte-stable 24-token digest.- Unit + integration green,
app/coverage >90%, story E2E green in isolation,ruff+pyrightclean, one--no-gpg-signcommit.
Playwright Mapping Rule
tests/e2e/test_document_summaries.py — one story, one file, run in
isolation. It imports the story-dedicated fixture KB
(tests/fixtures/summary_kb/: one yaml document with a tail sentinel +
one markdown control document) through the real importer against the
deterministic mock LLM, asserts the yaml document's import state (one
embedded is_summary chunk at position −1 whose text is the byte-stable
mock digest + the deterministic Source: pointer), asserts the summary
chunk is the yaml document's best fused chunk, then asks the question
whose best yaml match is that summary chunk and asserts the rendered
answer quotes the document's tail sentinel RESE-SUMMARY-SENTINEL-7f3a
(only possible if the full source document — not the summary digest —
reached the LLM), the source chip cites the yaml path, the turn is not
deflected, and the markdown control document carries no summary chunk.