diff --git a/knowledge-fs/apps/api/src/answer-generation-options.test.ts b/knowledge-fs/apps/api/src/answer-generation-options.test.ts index 0732e2a8414..7d8d1b7d79e 100644 --- a/knowledge-fs/apps/api/src/answer-generation-options.test.ts +++ b/knowledge-fs/apps/api/src/answer-generation-options.test.ts @@ -113,6 +113,42 @@ describe("createApiAnswerGenerationOptions", () => { expect(indexSource).not.toContain("model: answerGenerationOptions.model"); }); + it("keeps interactive retrieval evidence-only and reserves LLM synthesis for Research", async () => { + const { readFile } = await import("node:fs/promises"); + const indexSource = await readFile(new URL("./index.ts", import.meta.url), "utf8"); + const citationOptionsStart = indexSource.indexOf("const multimodalCitationOptions ="); + const researchMultimodalOptionsStart = indexSource.indexOf( + "const researchAnswerMultimodalOptions =", + ); + const retrievalAssemblyStart = indexSource.indexOf( + "const retrievalEvidenceQueryGenerator = retriever", + ); + const researchAnswerAssemblyStart = indexSource.indexOf( + "const profileLlmAnswerQueryGenerator = retriever", + ); + const retrievalAssembly = indexSource.slice( + retrievalAssemblyStart, + researchAnswerAssemblyStart, + ); + const citationOptionsAssembly = indexSource.slice( + citationOptionsStart, + researchMultimodalOptionsStart, + ); + + expect(citationOptionsStart).toBeGreaterThanOrEqual(0); + expect(researchMultimodalOptionsStart).toBeGreaterThan(citationOptionsStart); + expect(retrievalAssemblyStart).toBeGreaterThanOrEqual(0); + expect(researchAnswerAssemblyStart).toBeGreaterThan(retrievalAssemblyStart); + expect(citationOptionsAssembly).toContain("multimodalCandidateResolver"); + expect(citationOptionsAssembly).not.toContain("multimodalAnswerOptions"); + expect(retrievalAssembly).toContain("createHybridQueryGenerator"); + expect(retrievalAssembly).toContain("...multimodalCitationOptions"); + expect(retrievalAssembly).not.toContain("multimodalAnswerOptions"); + expect(indexSource).toContain("generator: researchAnswerQueryGenerator"); + expect(indexSource).toContain("{ queryGenerator: retrievalEvidenceQueryGenerator }"); + expect(indexSource).not.toContain("{ queryGenerator: researchAnswerQueryGenerator }"); + }); + it("honors explicit output-token overrides", () => { const options = createApiAnswerGenerationOptions({ KNOWLEDGE_ANSWER_MAX_OUTPUT_TOKENS: "2048", diff --git a/knowledge-fs/apps/api/src/index.ts b/knowledge-fs/apps/api/src/index.ts index 0483c57d1e6..6ed1e37bf92 100644 --- a/knowledge-fs/apps/api/src/index.ts +++ b/knowledge-fs/apps/api/src/index.ts @@ -558,8 +558,8 @@ const retrievalTestExecutor = retriever retriever, }) : undefined; -// Resolves multimodal citations (manifest item id, asset route, page/bbox) and is shared by both -// query generators. Available in database-repository mode, which is also when the retriever exists. +// Resolves multimodal citations (manifest item id, asset route, page/bbox) without invoking a +// model. Available in database-repository mode, which is also when the retriever exists. const multimodalCandidateResolver = repositoryOptions.documentAssets && repositoryOptions.parseArtifacts ? createDocumentMultimodalCandidateResolver({ @@ -570,21 +570,26 @@ const multimodalCandidateResolver = parseArtifacts: repositoryOptions.parseArtifacts, }) : undefined; -const multimodalGeneratorOptions = { - ...multimodalAnswerOptions, +const multimodalCitationOptions = { ...(multimodalCandidateResolver ? { multimodalCandidateResolver } : {}), }; -const extractiveQueryGenerator = retriever +const researchAnswerMultimodalOptions = { + ...multimodalAnswerOptions, + ...multimodalCitationOptions, +}; +// Interactive query-stream is a retrieval surface: it returns bounded evidence and citations, +// never an LLM/VLM-synthesized answer. Answer synthesis remains a separate Research capability. +const retrievalEvidenceQueryGenerator = retriever ? createHybridQueryGenerator({ limit: 5, maxAnswerChars: 2_000, retriever, topK: 10, ...embeddingGeneratorOptions, - ...multimodalGeneratorOptions, + ...multimodalCitationOptions, }) : undefined; -const llmAnswerQueryGenerator = retriever +const profileLlmAnswerQueryGenerator = retriever ? createLlmAnswerQueryGenerator({ limit: 5, maxAnswerChars: 2_000, @@ -594,14 +599,14 @@ const llmAnswerQueryGenerator = retriever temperature: 0, topK: 10, ...embeddingGeneratorOptions, - ...multimodalGeneratorOptions, + ...researchAnswerMultimodalOptions, }) : undefined; -const queryGenerator = - extractiveQueryGenerator && llmAnswerQueryGenerator +const researchAnswerQueryGenerator = + retrievalEvidenceQueryGenerator && profileLlmAnswerQueryGenerator ? createProfileAwareQueryGenerator({ - extractiveGenerator: extractiveQueryGenerator, - profileLlmGenerator: llmAnswerQueryGenerator, + extractiveGenerator: retrievalEvidenceQueryGenerator, + profileLlmGenerator: profileLlmAnswerQueryGenerator, }) : undefined; const researchProjectionSnapshotResolver = repositoryOptions.projectionSetPublications @@ -646,7 +651,7 @@ const researchTaskRuntime = databaseRepositories.researchTaskPartialResults && databaseRepositories.researchTaskProgressEvents && repositoryOptions.knowledgeSpaceAccess && - queryGenerator + researchAnswerQueryGenerator ? createApiResearchTaskRuntime({ access: repositoryOptions.knowledgeSpaceAccess, adapter, @@ -654,7 +659,7 @@ const researchTaskRuntime = ? { capabilityGrants: databaseRepositories.capabilityGrantProvenance } : {}), ...(deletionLifecycleFence ? { deletionFence: deletionLifecycleFence } : {}), - generator: queryGenerator, + generator: researchAnswerQueryGenerator, manifests: knowledgeSpaceManifests, metrics: operationalMetrics.durableTasks, partials: databaseRepositories.researchTaskPartialResults, @@ -768,7 +773,7 @@ const app = createKnowledgeGateway({ }, } : {}), - ...(queryGenerator ? { queryGenerator } : {}), + ...(retrievalEvidenceQueryGenerator ? { queryGenerator: retrievalEvidenceQueryGenerator } : {}), ...(retrievalTestExecutor ? { retrievalTestExecutor } : {}), ...(publishedGraph ? { publishedGraph } : {}), ...(researchTaskRuntime