@@ -196,24 +196,41 @@ func run() error {
196196 }
197197 q .Register (queue .KindIngestDocument , pipeline .Handler ())
198198
199+ // /v1/answer/pageindex gets its OWN PageIndexStrategy instance,
200+ // independent of whatever selection strategy is configured in
201+ // retrieval.strategy. This way the endpoint is always available
202+ // (gated by retrieval.pageindex.enabled), even on a deployment
203+ // using chunked-tree as its default selection path.
204+ var pageIndexStrategy * retrieval.PageIndexStrategy
205+ if cfg .Retrieval .PageIndex .Enabled && llmClient != nil {
206+ pageIndexStrategy = buildPageIndexStrategy (cfg .Retrieval , llmClient , store )
207+ logger .Info ("retrieval: pageindex answer endpoint enabled" ,
208+ "max_hops" , pageIndexStrategy .MaxHops ,
209+ "page_content_limit" , pageIndexStrategy .PageContentLimit ,
210+ "model_override" , cfg .Retrieval .PageIndex .Model ,
211+ )
212+ }
213+
199214 deps := api.Deps {
200- Logger : logger ,
201- DB : pool ,
202- Storage : store ,
203- Queue : q ,
204- Strategy : strategy ,
205- Version : version ,
206- MultiDoc : multiDoc ,
207- LLM : llmClient ,
208- LLMModel : modelFor (cfg .LLM ),
209- AnswerSpan : cfg .Retrieval .AnswerSpan ,
210- Answer : cfg .Retrieval .Answer ,
211- Planner : planner ,
212- Planning : cfg .Retrieval .Planning ,
213- ReRanker : reRanker ,
214- ReRank : cfg .Retrieval .ReRank ,
215- Replay : replayStore ,
216- Abstain : cfg .Retrieval .Abstain ,
215+ Logger : logger ,
216+ DB : pool ,
217+ Storage : store ,
218+ Queue : q ,
219+ Strategy : strategy ,
220+ Version : version ,
221+ MultiDoc : multiDoc ,
222+ LLM : llmClient ,
223+ LLMModel : modelFor (cfg .LLM ),
224+ AnswerSpan : cfg .Retrieval .AnswerSpan ,
225+ Answer : cfg .Retrieval .Answer ,
226+ Planner : planner ,
227+ Planning : cfg .Retrieval .Planning ,
228+ ReRanker : reRanker ,
229+ ReRank : cfg .Retrieval .ReRank ,
230+ Replay : replayStore ,
231+ Abstain : cfg .Retrieval .Abstain ,
232+ PageIndexStrategy : pageIndexStrategy ,
233+ PageIndex : cfg .Retrieval .PageIndex ,
217234 }
218235
219236 srv := & http.Server {
@@ -365,11 +382,36 @@ func buildStrategy(c config.RetrievalConfig, client llmgate.Client, store storag
365382 }
366383 a .ModelOverride = c .Agentic .Model
367384 return a
385+ case "pageindex" :
386+ return buildPageIndexStrategy (c , client , store )
368387 default :
369388 return retrieval .NewChunkedTree (client )
370389 }
371390}
372391
392+ // buildPageIndexStrategy constructs the page-based agentic
393+ // strategy with the storage-backed PageLoader and the configured
394+ // caps. Used by buildStrategy when retrieval.strategy=pageindex AND
395+ // by the /v1/answer/pageindex endpoint setup (which wires its own
396+ // instance regardless of the selection strategy).
397+ //
398+ // The TOCProvider is left nil here. PR-A (toc-tree-builder) adds
399+ // documents.toc_tree + a DB-backed provider; until it lands the
400+ // strategy degrades to its synthesised view, which is the
401+ // documented fallback path.
402+ func buildPageIndexStrategy (c config.RetrievalConfig , client llmgate.Client , store storage.Storage ) * retrieval.PageIndexStrategy {
403+ p := retrieval .NewPageIndexStrategy (client )
404+ p .PageLoader = storagePageLoader {s : store }
405+ if c .PageIndex .MaxHops > 0 {
406+ p .MaxHops = c .PageIndex .MaxHops
407+ }
408+ if c .PageIndex .PageContentLimit > 0 {
409+ p .PageContentLimit = c .PageIndex .PageContentLimit
410+ }
411+ p .ModelOverride = c .PageIndex .Model
412+ return p
413+ }
414+
373415// storageFetcher adapts a storage.Storage to retrieval.ContentFetcher.
374416// The agentic strategy reads section bodies one at a time, so we
375417// materialize the full reader contents into a []byte here rather than
@@ -385,6 +427,23 @@ func (sf storageFetcher) Get(ctx context.Context, ref string) ([]byte, error) {
385427 return io .ReadAll (rc )
386428}
387429
430+ // storagePageLoader adapts a storage.Storage to
431+ // retrieval.PageContentLoader. Mirrors storageFetcher but lives
432+ // behind a separate interface so the two callers (agentic /
433+ // pageindex) can be wired independently. The PageIndex strategy
434+ // materialises section bodies once per get_pages observation, so
435+ // reading the full reader into a []byte is the right shape.
436+ type storagePageLoader struct { s storage.Storage }
437+
438+ func (l storagePageLoader ) Load (ctx context.Context , ref string ) ([]byte , error ) {
439+ rc , _ , err := l .s .Get (ctx , ref )
440+ if err != nil {
441+ return nil , err
442+ }
443+ defer rc .Close ()
444+ return io .ReadAll (rc )
445+ }
446+
388447// buildTLSConfig returns a *tls.Config when direct TLS is enabled, or nil
389448// when the engine should serve plaintext (behind a proxy). Returning nil
390449// leaves http.Server's TLSConfig unset, which is exactly what ListenAndServe
0 commit comments