Skip to content

Store answer: reason over structure+summaries, then full section content#58

Merged
hallelx2 merged 2 commits into
mainfrom
feat/store-answer-treewalk-map
Jul 4, 2026
Merged

Store answer: reason over structure+summaries, then full section content#58
hallelx2 merged 2 commits into
mainfrom
feat/store-answer-treewalk-map

Conversation

@hallelx2

@hallelx2 hallelx2 commented Jul 4, 2026

Copy link
Copy Markdown
Owner

What

Makes the store answer (/v1/answer/store) use the true Vectorless retrieval primitive over a collection: reason over structure + summaries → select relevant sections → pull their full content → generate. No chunking, no embeddings, no page-walking, no truncated snippets.

Flow

  1. Per document (parallel): load the tree, render its llms.txt-style outline (each section's title + one-line summary), and ask the LLM which sections' full text are relevant — reasoning over summaries, not raw pages, not chunks.
  2. Fetch the FULL content of every selected section (a selected heading pulls its subsection's content leaves).
  3. One generation call over the full content of all relevant sections → a single answer citing sections + documents with [n] markers.

That's two cheap selection calls over compact summaries + one generation call — far cheaper than N page-based tree-walks, and faithful to "reason over structure, read the relevant sections in full."

Controls

  • max_docs (0 = all documents)
  • max_sections (0 = all relevant sections)
  • total content budget so a large selection still fits the model context

Citations

Per section: {index, document_id, document_title, section_title, section_ids, start_page, end_page, quote} — the dashboard groups them by document. Returns 501 without an LLM.

Tests

TestParseStoreAnswer, TestParseRelevant, TestBuildStoreCitations, TestFormatPageRange. go build ./... + go test ./... green.

History

Supersedes the earlier tree-walk-per-document map on this branch — same endpoint, the reasoning substrate moved from raw pages to structure+summaries per review discussion.

Replace the store answer's map stage: instead of fanning the
section-selection strategy across documents (retrieve-then-generate,
chunk-shaped), run the FULL agentic tree-walk on EACH document — read its
structure, navigate hop by hop to a grounded per-document answer + page
citations — then synthesise the per-document answers into one. Every
document is now reasoned over exactly like /v1/answer/treewalk; no
chunking, no embeddings anywhere in the collection path.

- max_depth controls the per-document tree-walk hop budget (0 = the
  engine's full/default depth), as requested.
- max_docs bounds how many documents are tree-walked (0 = all) — a cost
  valve for large collections; the per-doc walks run with bounded
  concurrency.
- Citations are one-per-contributing-document, carrying the document's
  cited page span + overlapping section ids (from the tree-walk's
  CitedPages) and a short quote from its answer. Refusals/empty per-doc
  answers are dropped before synthesis.

Returns 501 unless the tree-walk strategy + an LLM are configured.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @hallelx2, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The /v1/answer/store handler is refactored from a multi-document evidence gatherer/reducer to a per-document tree-walk map step followed by a synthesis reduce step. Dependencies switch from storage/MultiDoc to storeTreeLoader and TreeWalkStrategy, citations are rebuilt per-document, and router wiring and tests are updated accordingly.

Changes

Answer Store Tree-Walk Refactor

Layer / File(s) Summary
Handler dependencies and request validation
internal/handler/answer_store.go
Constructor and struct switch to storeTreeLoader, retrieval.TreeWalkStrategy, and enginecfg.TreeWalkBlock; requests return 501 when unconfigured and max_docs truncates document lists.
Concurrent map step
internal/handler/answer_store.go
New mapTreeWalk loads document trees concurrently via errgroup, applies per-request strategy overrides, converts pages to section ids, and aggregates deterministic per-document results.
Filtering and reduce/synthesise flow
internal/handler/answer_store.go
Non-answers are filtered via isNonAnswer, citation indices assigned, synthesise builds numbered per-document blocks with an updated reducer prompt, and parseStoreAnswer parses the reducer output.
Citation building and tests
internal/handler/answer_store.go, internal/handler/answer_store_test.go
buildStoreCitations is rewritten to use docAnswer values (document id, section ids, confidence, snippet); tests updated from evidence blocks to docAnswer inputs.
Router wiring
internal/handler/router.go
answerStore handler construction uses DB, TreeWalkStrategy, and TreeWalk instead of Storage and MultiDoc.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Handler as HandleAnswerStore
  participant Map as mapTreeWalk
  participant Loader as storeTreeLoader
  participant LLM as llmgate.Client

  Client->>Handler: POST /v1/answer/store
  Handler->>Handler: validate config, truncate max_docs
  Handler->>Map: mapTreeWalk(docIDs)
  loop per document
    Map->>Loader: LoadTree(docID)
    Loader-->>Map: tree
    Map->>Map: walk tree, collect docAnswer
  end
  Map-->>Handler: docAnswers
  Handler->>Handler: filter non-answers, assign citation indices
  Handler->>LLM: synthesise(contributing docAnswers)
  LLM-->>Handler: reducer JSON response
  Handler->>Handler: parseStoreAnswer, buildStoreCitations
  Handler-->>Client: answer + citations
Loading

Possibly related PRs

  • hallelx2/vectorless-engine#35: Introduces TreeWalkStrategy/TreeWalk dependency injection into router.go's Deps that this PR consumes for the /v1/answer/store handler.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the shift to reasoning over structure and summaries before using full section content.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/store-answer-treewalk-map

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/handler/answer_store.go`:
- Around line 145-150: The no-match response in answer_store’s map-stage flow is
masking complete traversal failures from LoadTree/SelectWithCost. Update the
logic around the contributing-documents path in answer_store so failures are
tracked separately from true no-match results, and if every document walk fails,
return an error instead of the 200 “does not address this query” response. Use
the existing answer assembly path and the contributing/docIDs handling to
distinguish partial skips from total failure, and apply the same fix
consistently in the related answer paths mentioned by the review.
- Around line 426-429: The negative-findings filter in isIrrelevant-like string
matching is too broad and is excluding legitimate answers that state a document
does not contain something relevant. Tighten the logic in the answer filtering
helper that checks phrases like “does not address” and “does not contain” so it
only rejects clear non-answers, not substantive negative findings. Update the
matching in the relevant string-checking function to distinguish true
irrelevance from useful statements such as absence of an arbitration clause.
- Around line 251-256: The sort in answer ordering only breaks ties by
confidence and docTitle, so equal values can still produce nondeterministic
results. Update the comparator in the stable sort used by the answer_store
ordering logic to add a final tie-breaker on docID after docTitle, keeping the
existing confidence-first and title-second behavior in the same function.
- Around line 389-392: The fallback in parseStoreAnswer is too aggressive
because it expands to every document whenever cited is empty, even when the
answer text already contains usable [n] markers. Update parseStoreAnswer to
first extract citation markers from the answer field itself, then use those
parsed indices before falling back to all docs; keep the existing docs loop only
for cases where no markers can be found at all. Use the parseStoreAnswer logic
and its cited/answer handling to locate the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e63b45c7-b9d6-46a3-ac81-e5295afc0ce6

📥 Commits

Reviewing files that changed from the base of the PR and between db6888d and e43c16d.

📒 Files selected for processing (3)
  • internal/handler/answer_store.go
  • internal/handler/answer_store_test.go
  • internal/handler/router.go

Comment thread internal/handler/answer_store.go Outdated
Comment on lines +145 to +150
if len(contributing) == 0 {
writeJSON(w, http.StatusOK, map[string]any{
"query": body.Query,
"answer": "The documents in this store do not appear to address this query.",
"answer": "The documents in this collection do not appear to address this query.",
"citations": []any{},
"documents_searched": len(body.DocumentIDs),
"documents_searched": len(docIDs),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Don’t convert map-stage failures into a no-match answer.

LoadTree/SelectWithCost errors are logged and skipped, so if every document fails, Line 145 returns a 200 saying the collection does not address the query. Track failures separately and return an error when no document was successfully walked.

Proposed direction
- perDoc, mapUsage := h.mapTreeWalk(r.Context(), orgID, storeID(r), docIDs, body.Query, model, body.MaxDepth)
+ perDoc, mapUsage, mapFailures := h.mapTreeWalk(r.Context(), orgID, storeID(r), docIDs, body.Query, model, body.MaxDepth)
+ if len(perDoc) == 0 && mapFailures > 0 {
+ 	writeErr(w, http.StatusBadGateway, "answer map failed for all documents")
+ 	return
+ }
-func (h *AnswerStoreHandler) mapTreeWalk(ctx context.Context, orgID, storeID string, docIDs []tree.DocumentID, query, model string, maxDepth int) ([]docAnswer, retrieval.Usage) {
+func (h *AnswerStoreHandler) mapTreeWalk(ctx context.Context, orgID, storeID string, docIDs []tree.DocumentID, query, model string, maxDepth int) ([]docAnswer, retrieval.Usage, int) {
 	var (
-		mu    sync.Mutex
-		out   = make([]docAnswer, 0, len(docIDs))
-		usage retrieval.Usage
+		mu       sync.Mutex
+		out      = make([]docAnswer, 0, len(docIDs))
+		usage    retrieval.Usage
+		failures int
 	)
+	recordFailure := func() {
+		mu.Lock()
+		failures++
+		mu.Unlock()
+	}
 ...
 			if err != nil || t == nil {
 				h.logger.Warn("answer/store: load tree", "doc", docID, "err", err)
+				recordFailure()
 				return nil
 			}
 ...
 			if err != nil {
 				h.logger.Warn("answer/store: treewalk", "doc", docID, "err", err)
+				recordFailure()
 				return nil
 			}
 ...
-	return out, usage
+	return out, usage, failures
 }

Also applies to: 195-210, 249-249

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/handler/answer_store.go` around lines 145 - 150, The no-match
response in answer_store’s map-stage flow is masking complete traversal failures
from LoadTree/SelectWithCost. Update the logic around the contributing-documents
path in answer_store so failures are tracked separately from true no-match
results, and if every document walk fails, return an error instead of the 200
“does not address this query” response. Use the existing answer assembly path
and the contributing/docIDs handling to distinguish partial skips from total
failure, and apply the same fix consistently in the related answer paths
mentioned by the review.

Comment thread internal/handler/answer_store.go Outdated
Comment on lines +251 to +256
// Deterministic order: highest confidence first, then title.
sort.SliceStable(out, func(i, j int) bool {
if out[i].confidence != out[j].confidence {
return out[i].confidence > out[j].confidence
}
return out[i].docTitle < out[j].docTitle

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add a stable final tie-breaker.

Concurrent append order is nondeterministic, so equal confidence and equal title still produce nondeterministic citation indices. Tie-break by docID after title.

Proposed fix
 	sort.SliceStable(out, func(i, j int) bool {
 		if out[i].confidence != out[j].confidence {
 			return out[i].confidence > out[j].confidence
 		}
-		return out[i].docTitle < out[j].docTitle
+		if out[i].docTitle != out[j].docTitle {
+			return out[i].docTitle < out[j].docTitle
+		}
+		return string(out[i].docID) < string(out[j].docID)
 	})
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Deterministic order: highest confidence first, then title.
sort.SliceStable(out, func(i, j int) bool {
if out[i].confidence != out[j].confidence {
return out[i].confidence > out[j].confidence
}
return out[i].docTitle < out[j].docTitle
sort.SliceStable(out, func(i, j int) bool {
if out[i].confidence != out[j].confidence {
return out[i].confidence > out[j].confidence
}
if out[i].docTitle != out[j].docTitle {
return out[i].docTitle < out[j].docTitle
}
return string(out[i].docID) < string(out[j].docID)
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/handler/answer_store.go` around lines 251 - 256, The sort in answer
ordering only breaks ties by confidence and docTitle, so equal values can still
produce nondeterministic results. Update the comparator in the stable sort used
by the answer_store ordering logic to add a final tie-breaker on docID after
docTitle, keeping the existing confidence-first and title-second behavior in the
same function.

Comment thread internal/handler/answer_store.go Outdated
Comment on lines 389 to 392
if len(cited) == 0 { // model cited nothing parseable → show all sources
for _, d := range docs {
cited = append(cited, d.index)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Scrape answer markers before falling back to every document.

If the reducer returns JSON like {"answer":"... [2]","cited":[]}, this fallback emits citations for all contributing docs instead of the marker actually used. Prefer [n] markers from the answer, then fall back to all only when no markers are present.

Proposed fix in parseStoreAnswer
 			var parsed storeAnswerJSON
 			if err := json.Unmarshal([]byte(s[i:j+1]), &parsed); err == nil && strings.TrimSpace(parsed.Answer) != "" {
-				return strings.TrimSpace(parsed.Answer), dedupeInRange(parsed.Cited, maxIdx)
+				answer := strings.TrimSpace(parsed.Answer)
+				cited := dedupeInRange(parsed.Cited, maxIdx)
+				if len(cited) == 0 {
+					cited = scrapeMarkers(answer, maxIdx)
+				}
+				return answer, cited
 			}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if len(cited) == 0 { // model cited nothing parseable → show all sources
for _, d := range docs {
cited = append(cited, d.index)
}
var parsed storeAnswerJSON
if err := json.Unmarshal([]byte(s[i:j+1]), &parsed); err == nil && strings.TrimSpace(parsed.Answer) != "" {
answer := strings.TrimSpace(parsed.Answer)
cited := dedupeInRange(parsed.Cited, maxIdx)
if len(cited) == 0 {
cited = scrapeMarkers(answer, maxIdx)
}
return answer, cited
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/handler/answer_store.go` around lines 389 - 392, The fallback in
parseStoreAnswer is too aggressive because it expands to every document whenever
cited is empty, even when the answer text already contains usable [n] markers.
Update parseStoreAnswer to first extract citation markers from the answer field
itself, then use those parsed indices before falling back to all docs; keep the
existing docs loop only for cases where no markers can be found at all. Use the
parseStoreAnswer logic and its cited/answer handling to locate the change.

Comment thread internal/handler/answer_store.go Outdated
Rework the store map to the Vectorless primitive the collection query
should use — reason over each document's llms.txt-style structure +
summaries, select the relevant sections, pull their FULL content, then
generate:

  1. per document (parallel): load the tree, render its section outline
     (title + one-line summary per section), and ask the LLM which
     sections' full text are relevant — reasoning over summaries, not raw
     pages, and not chunks.
  2. fetch the FULL content of every selected section (a selected heading
     pulls its subsection's leaves).
  3. one generation call over the full content of ALL relevant sections
     → a single answer citing sections + documents with [n] markers.

Two cheap selection calls over compact summaries + one generation call,
instead of N page-based tree-walks. No chunking, no embeddings, no
truncated snippets — full section content drives the answer.

Controls: max_docs (0 = all), max_sections (0 = all relevant), plus a
total content budget so a large selection still fits the model context.
Citations are per section, carrying document + section title + page span
+ a quote. Returns 501 without an LLM.
@hallelx2 hallelx2 changed the title Store answer: reasoning-heavy map (full tree-walk per document) Store answer: reason over structure+summaries, then full section content Jul 4, 2026
@hallelx2 hallelx2 merged commit ec119c9 into main Jul 4, 2026
8 checks passed
@hallelx2 hallelx2 deleted the feat/store-answer-treewalk-map branch July 4, 2026 09:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant