Skip to content

Commit f30f9f9

Browse files
committed
feat(server): port /v1/answer + /v1/answer/pageindex into the deployed router
Both answer endpoints existed only on the standalone cmd/engine binary (internal/api), so they were unreachable on the deployed cmd/server binary (internal/handler) that production runs. Port them, adapting the standalone single-tenant org to the deployed server's multi-tenant model (org + store resolved from the X-Vectorless-Org / X-Vectorless-Store headers). - /v1/answer runs retrieval + per-section answer-span extraction + a synthesis LLM call, returning a quote-grounded answer with citations. - /v1/answer/pageindex runs the PageIndex agentic loop end-to-end (structure -> get_pages -> done), with an opt-in reasoning trace and an SSE streaming variant. Its per-request strategy copy gets an org-scoped TOC provider so get_document_structure reads only the requesting tenant's documents.toc_tree. Extend handler.Deps with the LLM client, default model, answer-span / answer config, the replay store, and the dedicated PageIndex strategy, and wire them at boot in cmd/server. Replay capture is shared with the existing /v1/replay endpoint via byte-identical response storage.
1 parent e78d5ea commit f30f9f9

5 files changed

Lines changed: 1006 additions & 9 deletions

File tree

cmd/server/main.go

Lines changed: 61 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,36 @@ func run() error {
163163
// as the default (no shared cache wrapper across overrides).
164164
strategies := buildStrategySet(cfg.Engine.Retrieval, llmClient, store, pool)
165165

166+
// Replay store: every /v1/answer and /v1/answer/pageindex response
167+
// is stamped with a deterministic trace_token and its body bytes
168+
// persisted here so /v1/replay can return them verbatim. On by
169+
// default; operators opt out via retrieval.replay.enabled=false.
170+
var replayStore retrieval.ReplayStore
171+
if cfg.Engine.Retrieval.Replay.Enabled {
172+
replayStore = retrieval.NewLRUReplayStore(retrieval.LRUReplayConfig{
173+
MaxEntries: cfg.Engine.Retrieval.Replay.MaxEntries,
174+
TTL: time.Duration(cfg.Engine.Retrieval.Replay.TTLSeconds) * time.Second,
175+
})
176+
logger.Info("retrieval: replay store enabled",
177+
"max_entries", cfg.Engine.Retrieval.Replay.MaxEntries,
178+
"ttl_seconds", cfg.Engine.Retrieval.Replay.TTLSeconds,
179+
)
180+
}
181+
182+
// /v1/answer/pageindex gets its OWN PageIndexStrategy instance,
183+
// independent of whatever selection strategy retrieval.strategy
184+
// chose, so the endpoint is always available (gated by
185+
// retrieval.pageindex.enabled) even on a chunked-tree deployment.
186+
var pageIndexStrategy *retrieval.PageIndexStrategy
187+
if cfg.Engine.Retrieval.PageIndex.Enabled && llmClient != nil {
188+
pageIndexStrategy = buildPageIndexStrategy(cfg.Engine.Retrieval, llmClient, store, pool)
189+
logger.Info("retrieval: pageindex answer endpoint enabled",
190+
"max_hops", pageIndexStrategy.MaxHops,
191+
"page_content_limit", pageIndexStrategy.PageContentLimit,
192+
"model_override", cfg.Engine.Retrieval.PageIndex.Model,
193+
)
194+
}
195+
166196
// ── Ingest pipeline ───────────────────────────────────────────
167197
pipeline := ingest.NewPipeline(ingest.Pipeline{
168198
DB: pool,
@@ -210,15 +240,22 @@ func run() error {
210240
// Only start the HTTP server in "server" role.
211241
if *role == "server" {
212242
deps := handler.Deps{
213-
Logger: logger,
214-
DB: pool,
215-
Storage: store,
216-
Queue: q,
217-
Strategy: strategy,
218-
MultiDoc: multiDoc,
219-
Version: version,
220-
Config: cfg,
221-
Strategies: strategies,
243+
Logger: logger,
244+
DB: pool,
245+
Storage: store,
246+
Queue: q,
247+
Strategy: strategy,
248+
MultiDoc: multiDoc,
249+
Version: version,
250+
Config: cfg,
251+
Strategies: strategies,
252+
LLM: llmClient,
253+
LLMModel: modelFor(cfg.Engine.LLM),
254+
AnswerSpan: cfg.Engine.Retrieval.AnswerSpan,
255+
Answer: cfg.Engine.Retrieval.Answer,
256+
Replay: replayStore,
257+
PageIndexStrategy: pageIndexStrategy,
258+
PageIndex: cfg.Engine.Retrieval.PageIndex,
222259
}
223260

224261
srv := &http.Server{
@@ -334,6 +371,21 @@ func buildQueue(c enginecfg.QueueConfig, dbURL string) (queue.Queue, error) {
334371
}
335372
}
336373

374+
// modelFor returns the configured chat/general-purpose model name for
375+
// the selected LLM driver. Used as the engine-default fallback when an
376+
// API request omits an explicit model (answer + answer/pageindex).
377+
func modelFor(c enginecfg.LLMConfig) string {
378+
switch c.Driver {
379+
case "anthropic":
380+
return c.Anthropic.Model
381+
case "openai":
382+
return c.OpenAI.Model
383+
case "gemini":
384+
return c.Gemini.Model
385+
}
386+
return ""
387+
}
388+
337389
func buildLLM(c enginecfg.LLMConfig) (llmgate.Client, error) {
338390
switch c.Driver {
339391
case "anthropic":

0 commit comments

Comments
 (0)