Converts the 9 TODO items into an executable phase roadmap (Protocol B, appended after phase 39): - 40 tuning toggle anonymous flash (TODO L3) - 41 sync fail-fast + modal when a model is down (TODO L4) - 42 no reply autoscroll (TODO L5) - 43 thinking scroll back — user scroll + gated autoscroll (TODO L7) - 44 markdown tables (TODO L6) - 45 agent unlimited tool calls behind BOR_AGENT_MAX_ROUNDS (TODO L8) - 46 mobile hamburger nav (TODO L9) - 47 quadlet + jinja import formats, A9 revision (TODO L10–L11) Each phase carries a user story, a dedicated Playwright E2E suite plan, and owner-locked decisions (R1 A9 format extension, R2 phase-37 budget revision, A1–A5 scope decisions) confirmed 2026-08-27. Also records the completed phases 30–39 todo/ -> complete/ moves that were pending in the working tree. TODO.md is cleared (items now live in .agent/phases/todo/).
6.9 KiB
Phase 30 — Document Summaries (lite-model summaries for non-markdown documents)
Source: TODO.md L3 — "One issue I'm having is bad context for the embedder which causes poor retrieval results… we need a small model to analyze non markdown documents and provide a textual summary of those documents with a pointer back to the source… if retrieval == summary, fetch documents referenced by summary… The small model available on aipi.reeseapps.com is 'lite'."
Story: .agent/user_stories/document-summaries.md
Context: The importer (app/rag/importer.py::_index_file — chunk → embed → upsert per file, A9 scope), hybrid retrieval (app/rag/retriever.py — A7: cosine ∪ FTS, RRF, chunk→parent-document mapping, full-document context never truncated, phase 24), the locked persona prompts (app/rag/prompts.py), the aipi client (app/rag/llm.py — A5: turbo chat streaming + embed embeddings), and the E2E mock LLM (tests/e2e/mock_llm.py — deterministic, keys on system-prompt markers like DEFLECT_MODE / <tuning>).
Objective
Give every non-markdown A9 document (txt, yaml, yml, json, py) a natural-language summary generated at import time by the aipi lite model. The summary is stored on the document (documents.summary) and indexed as one extra embedded chunk (chunks.is_summary), so hybrid search has a well-embedding natural-language target to hit instead of the badly-formatted raw text. A summary hit resolves to its parent (the source document) — the existing chunk→document mapping then feeds the full source document to the LLM, implementing the TODO's "if retrieval == summary, fetch the documents referenced by the summary" step. The per-turn log line records how many summary hits landed in the selected context.
Dependencies
29_tuning_nav_link(complete) — the latest finished phase (sequencing only).- Substantively builds on:
02_story_import_documents/24_whole_document_context(import pipeline + full-document context contract),09_story_retrieval_quality(A7 hybrid retrieval the summary chunk flows through unchanged),01_infrastructure(models/alembic, LLM client, E2E mock).
Tasks
01_lite_model_client.md—BOR_LLM_SUMMARY_MODEL(defaultlite) + non-streamingLLMClient.chat()for the lite model.02_migration_summary_columns.md— Alembic 0004:documents.summary TEXT NULL+chunks.is_summary BOOLEAN NOT NULL DEFAULT FALSE.03_summarizer_module.md—app/rag/summarizer.py:SUMMARY_MODEprompt (capped input), lite call, output validation + deterministicSource: <source>/<path>pointer line.04_importer_summary_integration.md— importer generates/stores/indexes summaries for non-md files (best-effort fail-soft) + summary counters.05_pipeline_summary_resolution.md—is_summarythrough the retriever,summary_hitsinTurnPlan+ the per-turn log line; full source document on summary hit (existing mapping, asserted).06_mock_and_e2e.md— deterministicliteinmock_llm.py, sentinel fixture,tests/e2e/test_document_summaries.py, story file, README, commit.
Testing & Quality
- Unit: summarizer (prompt/cap/pointer/errors), importer (summary happy path, md exclusion, fail-soft, replacement on re-import), retriever (
is_summarythrough both candidate lists +fuse), chat gate (TurnPlan.summary_hits), LLM client (chat()). - Integration: migration 0004 up/down.
- Coverage: >90% on
app/(uv run pytest --cov=app --cov-report=term-missing, TOTAL ≥ pre-change number). - E2E (mandatory, A16):
tests/e2e/test_document_summaries.py— one story, run in isolation (uv run pytest tests/e2e/test_document_summaries.py -v --no-cov); proves summary hit → full source document reaches the answer (sentinel in the raw doc, absent from the mock summary). - All existing E2E suites stay green (new columns are defaulted; all existing chunks have
is_summary=false).
Completion Criteria
- After
uv run python -m scripts.import_docs, every non-markdown fixture/doc hasdocuments.summaryset and exactly oneis_summarychunk (position −1, embedded); markdown docs have neither. - A question whose best match is a summary chunk yields an answer grounded in the full source document (E2E sentinel) and the per-turn log line shows
summary_hits>=1. - A lite-model failure during import does not drop the document — it is indexed without a summary, logged, and counted (
summary_errors). uv run pytestgreen;uv run pytest --cov=app --cov-report=term-missingTOTAL ≥ pre-change number (app/ >90%).uv run pytest tests/e2e/test_document_summaries.py -v --no-covgreen in isolation; existing suites (test_chat_rag.py,test_retrieval_quality.py,test_import_documents.py,test_whole_document_context.py) stay green.uv run ruff check . && uv run pyrightclean..agent/user_stories/document-summaries.mdexists..env.example+ README documentBOR_LLM_SUMMARY_MODEL/BOR_SUMMARY_MAX_CHARSand the summary behavior.- One
--no-gpg-signcommit staging only this phase's files (e.g.feat(rag): lite-model document summaries — non-markdown docs summarized at import, summary chunk retrieves and resolves to the full source doc);.agent/phases/todo/30_document_summaries/moved to.agent/phases/complete/.
Locked decisions
- A5 extended, not revised — the
litemodel is served by the same OpenAI-compatible endpoint (https://aipi.reeseapps.com/v1) via a newBOR_LLM_SUMMARY_MODELsetting (defaultlite); no new model management, no new package. - A7 untouched — hybrid retrieval logic is unchanged; the summary is an ordinary chunk, so it flows through the existing cosine ∪ FTS ∪ RRF path and the chunk→document mapping. The "fetch the referenced document" step is the existing full-document context contract (phase 24) — never truncated.
- A9 untouched — "non-markdown" means every already-imported A9 document except
md/markdown. The TODO's quadlet-file example is out of scope:.quadletis not an A9 format andBOR_IMPORT_EXTENSIONSmay only narrow the locked set (flagged at roadmap confirmation; importing quadlet files would require an owner-permission A9 revision). - A13 — migration 0004 adds two columns (
documents.summary,chunks.is_summary); no table rework, both reversible. - Summary generation is best-effort — a lite failure logs + counts (
summary_errors) and the file is still indexed without a summary (same fail-soft spirit as the per-fileEmbeddingErrorhandling, but weaker: the doc is already committed). - Pointer is code-deterministic — the
Source: <source>/<path>line is appended bysummarizer.py, never trusted to the model. - A16 honoured — one dedicated story E2E suite; E2E stays deterministic via the mock LLM's
SUMMARY_MODEmarker. - A17 honoured — one atomic
--no-gpg-signcommit.