From 63cfebbef3f9cf11fe85a181812d0da14ec2e209 Mon Sep 17 00:00:00 2001 From: Jyong Date: Thu, 20 Aug 2026 02:30:59 -0400 Subject: [PATCH] fix(knowledge-fs): constrain research graph planning --- api/knowledge-fs-contract.lock.json | 2 +- .../src/research-evidence-reasoning.test.ts | 12 ++++++++ .../api/src/research-evidence-reasoning.ts | 29 +++++++++++++++++-- 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/api/knowledge-fs-contract.lock.json b/api/knowledge-fs-contract.lock.json index 635e63429b9..1d52148cdfd 100644 --- a/api/knowledge-fs-contract.lock.json +++ b/api/knowledge-fs-contract.lock.json @@ -1,6 +1,6 @@ { "schemaVersion": 5, - "subtreeTree": "0739e3faf3861c45e78beb693edcd7e9d6550c1f", + "subtreeTree": "cd6c4687eaafa5e178c79aaa52c88869666f36b4", "openapiSha256": "2cf348c68bbe65dd51bbde9a0a4f91398beeebd79e89e9288c9386b26ae09796", "capabilityV2AuthManifestSha256": "fc0a47e23cce12544882f0298522b4933002e892b84ce1815df7e81d36a7a0c7", "capabilityV2AuthTestVectorSha256": "ae0de37b1ff05c40f905cf17a7b410d8971acacf64db07d5ee3d6fecfa559ce3", diff --git a/knowledge-fs/packages/api/src/research-evidence-reasoning.test.ts b/knowledge-fs/packages/api/src/research-evidence-reasoning.test.ts index b80cd264a01..260c1e8df69 100644 --- a/knowledge-fs/packages/api/src/research-evidence-reasoning.test.ts +++ b/knowledge-fs/packages/api/src/research-evidence-reasoning.test.ts @@ -83,11 +83,23 @@ describe("Research evidence reasoning", () => { subqueries: ["renewal terms", "termination terms"], }); expect(generate).toHaveBeenCalledTimes(2); + expect(generate.mock.calls[1]?.[0]).toMatchObject({ + structuredOutputSchema: { + properties: { + useGraph: { enum: ["false", "true"], type: "string" }, + }, + }, + }); }); it.each([ { expected: false, providerValue: "false" }, + { + expected: false, + providerValue: "不使用;这是按主题拆分的概览检索,不需要关系图或多跳推理。", + }, { expected: true, providerValue: "true" }, + { expected: true, providerValue: "需要使用关系图完成多跳推理。" }, ])( "normalizes the structured-output string boolean useGraph=$providerValue", async ({ expected, providerValue }) => { diff --git a/knowledge-fs/packages/api/src/research-evidence-reasoning.ts b/knowledge-fs/packages/api/src/research-evidence-reasoning.ts index bfd1eb03535..d74c0d6e6ed 100644 --- a/knowledge-fs/packages/api/src/research-evidence-reasoning.ts +++ b/knowledge-fs/packages/api/src/research-evidence-reasoning.ts @@ -403,7 +403,9 @@ function zodJsonSchema(schema: z.ZodTypeAny): Readonly> evidenceDimensions: { items: { type: "string" }, maxItems: 6, type: "array" }, intent: { enum: ["comparison", "direct", "multi-hop", "overview"], type: "string" }, subqueries: { items: { type: "string" }, maxItems: 3, type: "array" }, - useGraph: { type: "boolean" }, + // Dify's structured-output compatibility layer converts JSON Schema booleans to strings. + // Constrain the string values here so the model cannot replace a boolean with prose. + useGraph: { enum: ["false", "true"], type: "string" }, }, required: ["evidenceDimensions", "intent", "subqueries", "useGraph"], type: "object", @@ -444,7 +446,7 @@ function parseQueryPlan(text: string): z.infer { // Dify's structured-output compatibility layer represents JSON Schema booleans as strings // for providers that do not accept native boolean fields. Normalize that transport detail // before applying the strict domain schema, just as the evidence judge does below. - const useGraph = normalizeBooleanValue(record.useGraph); + const useGraph = normalizeUseGraphValue(record.useGraph); if (useGraph !== undefined) { value = { ...record, useGraph }; } @@ -542,6 +544,29 @@ function normalizeBooleanValue(value: unknown): boolean | undefined { return undefined; } +function normalizeUseGraphValue(value: unknown): boolean | undefined { + const normalizedBoolean = normalizeBooleanValue(value); + if (normalizedBoolean !== undefined || typeof value !== "string") { + return normalizedBoolean; + } + const normalized = value.trim().toLocaleLowerCase(); + if ( + /^(?:do not use|don't use|not needed|unnecessary|不使用|不要使用|无需|不需要|不启用)(?:\s|关系图|图谱|图|[.,,。::;;]|$)/u.test( + normalized, + ) + ) { + return false; + } + if ( + /^(?:use|should use|need to use|enabled|使用|应使用|需要使用|启用|开启)(?:\s|关系图|图谱|图|[.,,。::;;]|$)/u.test( + normalized, + ) + ) { + return true; + } + return undefined; +} + function modelFailureIsRetryable(error: unknown): boolean { if (error && typeof error === "object" && "retryable" in error) { return (error as { readonly retryable?: unknown }).retryable === true;