Files
ducoterra dbf2af26c6 refactor(agents): migrate .agent/ planning tree to .agents/
Standardize on the .agents/ directory (shared with project skills):
phases/, user_stories/, reports/, screenshots/, validate.sh, and
phase-sessions/ + pipeline.log all move to .agents/ (git mv preserves
history; runtime artifacts move alongside).

Updates every reference in AGENTS.md, README.md, .gitignore, app
docstrings, and test story headers. Historical KB content in data/
and the runtime pipeline.log transcript are left untouched.
2026-09-05 10:57:07 -04:00

79 lines
4.3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Story: Retrieval Quality — Multi-Format Ingestion + Hybrid Search
**Phase:** `09_story_retrieval_quality.md` · **E2E:** `tests/e2e/test_retrieval_quality.py`
## Narrative
As **a user**, I want to ask questions that name my tools ("How did I
install gitlab?") and get a grounded answer citing the right document —
even when the answer lives in a `.yaml`, `.py`, `.json` or `.txt` file —
so Brain actually covers my notes instead of deflecting around them.
- **Given** my notes are a mix of markdown write-ups and config/code
files, and today's index is polluted with vendored-cache docs
(`.esphome/.espressif/**`, merge-request templates, …) that outrank
real content, while retrieval is pure cosine top-4
- **When** the importer covers the full text-format set, skips hidden
directories, and retrieval fuses vector similarity with Postgres
full-text search
- **Then** "How did I install gitlab?" surfaces `active/container_gitlab/
gitlab.md` as the top document, the junk docs are pruned from the
index, and genuinely off-topic questions still get the honest
deflection.
## Acceptance criteria
1. **Ingestion scope (A9, revised with owner permission 2026-08-21):**
default extensions `md, markdown, txt, yaml, yml, json, py`,
overridable via `BOR_IMPORT_EXTENSIONS`; **hidden (dot) directories
skipped by default**; the A9 exclusion list stays.
2. **Format-aware chunking:** md unchanged (heading-aware); yaml/yml
split on top-level keys and `---` document separators (key kept as
chunk anchor); json pretty-printed and split on top-level keys; py
split on top-level defs/classes (stdlib `ast`); txt on paragraphs.
Every format honors the 1200-char hard cap (aipi ~1024-token request
cap).
3. **Hybrid retrieval (A7, revised with owner permission 2026-08-21):**
cosine top-30 + FTS top-30 (OR tsquery over the question's tokens,
`ts_rank`-ordered) fused with Reciprocal Rank Fusion (k=60);
document-level ranking by best fused chunk score; the LLM still
receives the **full top-2 documents** capped at 24k chars (contract
preserved).
4. **Honesty gate (A8, revised with owner permission 2026-08-21):**
deflect when `best cosine < BOR_RELEVANCE_THRESHOLD` **and** no
candidate chunk FTS-matches the question; threshold re-tuned for the
`embed` model's measured 0.41–0.84 score compression (default 0.62,
documented tuning procedure); `fts_hits` logged per turn and stored in
`query_log`.
5. **Live verification:** `scripts/eval_retrieval.py` run against the
imported KB shows `gitlab.md` as the top document for "How did I
install gitlab?"; a known off-topic question gates LOW.
6. **Index hygiene:** after a `--prune` re-import, **zero** documents
with a dot-prefixed path component exist in `documents`; prune also
drops docs that no longer match the format filter.
7. **No regressions:** off-topic deflection (story 04) and grounded
answers (story 03) still pass their E2E suites in isolation; the E2E
mock keeps its own threshold calibration via the app fixture.
8. Unit + integration green, `app/` coverage >90%, story E2E green in
isolation, one `--no-gpg-sign` commit.
## UI Visualization & Structure
No UI change. The Sources page document count drops after the junk prune
(intended cleanup — noted in the README).
## Playwright Mapping Rule
**Test Scenario → `tests/e2e/test_retrieval_quality.py`** (mock LLM, seeded KB):
1. `test_multiformat_docs_indexed` — fixture tree with `.md`, `.yaml`,
`.py`, `.json`, `.txt` plus `.hidden/junk.md`: import in-process →
`/api/docs` lists every non-hidden doc and none of the hidden ones.
2. `test_named_tool_question_retrieves_right_doc` — "How did I install
gitlab?" → not deflected, streamed answer, a `.source-chip` containing
the gitlab doc, `query_log` row `deflected=false` with the gitlab doc
in sources.
3. `test_keyword_only_question_beats_vector_ranking` — a question whose
rare token exists in exactly one fixture doc (weak mock-embedding
overlap, e.g. "kafkabridge") → grounded answer citing that doc
(FTS-OR gate + lexical RRF lift, end to end).
4. `test_off_topic_still_deflects` — "sourdough starter" →
`.is-deflected` bubble matching /haven't done anything like that/i +
≥2 "Maybe try:" chips (story 04 behavior under the new gate).