From cf96d6430d32a5ee7df4eca7357fe795430a089a Mon Sep 17 00:00:00 2001 From: balibabu Date: Fri, 21 Aug 2026 13:22:52 +0800 Subject: [PATCH] Feat: Rewrite the Go-based extractor operator form. (#18600) --- web/src/pages/agent/constant/pipeline.tsx | 18 +- .../agent/form/extractor-form/go-form.tsx | 301 ++++++++---------- web/src/pages/agent/hooks/use-add-node.ts | 12 +- web/src/pages/agent/utils.ts | 90 +++--- .../utils/tests/extractor-transform.test.ts | 158 +++++++-- web/src/utils/pipeline-operator.ts | 36 ++- 6 files changed, 345 insertions(+), 270 deletions(-) diff --git a/web/src/pages/agent/constant/pipeline.tsx b/web/src/pages/agent/constant/pipeline.tsx index d27c237ac8..b0f30ccfec 100644 --- a/web/src/pages/agent/constant/pipeline.tsx +++ b/web/src/pages/agent/constant/pipeline.tsx @@ -361,8 +361,8 @@ export const initialExtractorValues = { }, }; -// Defaults for the Go backend extractor (nested per-feature configs plus -// legacy flat fields, which the Go schema still accepts). +// Defaults for the Go backend extractor: the LLM settings plus the nested +// per-feature groups the Go schema reads (schema.ExtractorParam). export const initialGoExtractorValues = { ...initialLlmBaseValues, keywords: { @@ -381,23 +381,11 @@ export const initialGoExtractorValues = { enabled: false, system_prompt: '', }, - metadata_config: { + metadata: { enabled: false, metadata: [], built_in_metadata: [], }, - metadata: [], - built_in_metadata: [], - field_name: '', - auto_keywords: 0, - auto_questions: 0, - auto_tags: 0, - tag_file_id: '', - enable_summary: 0, - enable_metadata: 0, - keywords_sys_prompt: '', - questions_sys_prompt: '', - sys_prompt: '', outputs: { chunks: { type: 'Array', value: [] }, }, diff --git a/web/src/pages/agent/form/extractor-form/go-form.tsx b/web/src/pages/agent/form/extractor-form/go-form.tsx index 4b64c19e9e..86fa14d910 100644 --- a/web/src/pages/agent/form/extractor-form/go-form.tsx +++ b/web/src/pages/agent/form/extractor-form/go-form.tsx @@ -20,13 +20,13 @@ import { } from '@/components/auto-keywords-form-field'; import { LargeModelFormField } from '@/components/large-model-form-field'; import { LlmSettingSchema } from '@/components/llm-setting-items/next'; +import { SelectWithSearch } from '@/components/originui/select-with-search'; import { RAGFlowFormItem } from '@/components/ragflow-form'; import { SliderInputFormField } from '@/components/slider-input-form-field'; import { AsyncTreeSelect } from '@/components/ui/async-tree-select'; import { Button } from '@/components/ui/button'; import { Form } from '@/components/ui/form'; import { Switch } from '@/components/ui/switch'; -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { FormLayout } from '@/constants/form'; import { RAGFlowNodeType } from '@/interfaces/database/agent'; import { PromptEditor } from '@/pages/agent/form/components/prompt-editor'; @@ -82,34 +82,21 @@ export const FormSchema = z.object({ system_prompt: z.string().optional(), }) .optional(), - metadata_config: z + // Matches schema.ExtractorParam.Metadata on the Go side + // (internal/ingestion/component/schema/extractor.go). + metadata: z .object({ enabled: z.union([z.number(), z.boolean()]).optional(), metadata: z.any().optional(), built_in_metadata: z.any().optional(), }) .optional(), - - // Legacy flat fields for backward compatibility - field_name: z.string().optional(), - sys_prompt: z.string().optional(), - prompts: z.string().optional(), - keywords_sys_prompt: z.string().optional(), - questions_sys_prompt: z.string().optional(), - auto_keywords: z.number().optional(), - auto_questions: z.number().optional(), - auto_tags: z.number().optional(), - tag_file_id: z.string().optional(), - enable_summary: z.union([z.number(), z.boolean()]).optional(), - enable_metadata: z.number().optional(), - metadata: z.any().optional(), - built_in_metadata: z.any().optional(), ...LlmSettingSchema, }); export type ExtractorFormSchemaType = z.infer; -enum ExtractorSubTab { +enum ExtractorSection { Keywords = 'keywords', Questions = 'questions', Tags = 'tags', @@ -118,9 +105,9 @@ enum ExtractorSubTab { } // ExtractorAutoMetadata mirrors Python's dataset "Auto metadata" control: an -// enable_metadata switch plus a field-schema editor (custom + built-in). -// Values are stored on the node params (enable_metadata / metadata / -// built_in_metadata) and drive the Go extractor's runEnableMetadata. +// enable switch plus a field-schema editor (custom + built-in). Values are +// stored as the nested `metadata` group ({enabled, metadata, +// built_in_metadata}) that the Go extractor's runEnableMetadata reads. function ExtractorAutoMetadata() { const { t } = useTranslation(); const form = useFormContext(); @@ -135,16 +122,11 @@ function ExtractorAutoMetadata() { const handleOpen = useCallback(() => { showManageMetadataModal({ metadata: util.metaDataSettingJSONToMetaDataTableData( - form.getValues('metadata_config.metadata') || - form.getValues('metadata') || - [], + form.getValues('metadata.metadata') || [], ), isCanAdd: true, type: MetadataType.Setting, - builtInMetadata: - form.getValues('metadata_config.built_in_metadata') || - form.getValues('built_in_metadata') || - [], + builtInMetadata: form.getValues('metadata.built_in_metadata') || [], }); }, [form, showManageMetadataModal]); @@ -153,19 +135,13 @@ function ExtractorAutoMetadata() { metadata?: IMetaDataReturnJSONSettings; builtInMetadata?: IBuiltInMetadataItem[]; }) => { - const metaList = data?.metadata || []; - const builtInList = data?.builtInMetadata || []; - form.setValue('metadata_config.metadata', metaList, { + form.setValue('metadata.metadata', data?.metadata || [], { shouldDirty: true, }); - form.setValue('metadata_config.built_in_metadata', builtInList, { + form.setValue('metadata.built_in_metadata', data?.builtInMetadata || [], { shouldDirty: true, }); - form.setValue('metadata_config.enabled', true, { shouldDirty: true }); - // Also keep flat fields for backward compatibility - form.setValue('metadata', metaList, { shouldDirty: true }); - form.setValue('built_in_metadata', builtInList, { shouldDirty: true }); - form.setValue('enable_metadata', 1, { shouldDirty: true }); + form.setValue('metadata.enabled', true, { shouldDirty: true }); }, [form], ); @@ -174,7 +150,7 @@ function ExtractorAutoMetadata() { <> {(field) => (
@@ -191,12 +167,7 @@ function ExtractorAutoMetadata() { { - field.onChange(checked); - form.setValue('enable_metadata', checked ? 1 : 0, { - shouldDirty: true, - }); - }} + onCheckedChange={field.onChange} data-testid="extractor-metadata-switch" />
@@ -249,12 +220,8 @@ const GoExtractorForm = ({ resolver: zodResolver(FormSchema), }); - useEffect(() => { - form.reset(defaultValues); - }, [defaultValues, form]); - - const [activeTab, setActiveTab] = useState( - ExtractorSubTab.Keywords, + const [activeSection, setActiveSection] = useState( + ExtractorSection.Keywords, ); useWatchFormChange(node?.id, form); @@ -262,8 +229,7 @@ const GoExtractorForm = ({ const ownerTenantId = useOwnerTenantId(); - const tagFileIdWatch = - form.watch('tags.tag_file_id') || form.watch('tag_file_id'); + const tagFileIdWatch = form.watch('tags.tag_file_id'); const { treeData, loadData } = useTagFileTree(tagFileIdWatch); useEffect(() => { @@ -284,8 +250,22 @@ const GoExtractorForm = ({ } }, [form, t]); - const handleTabChange = useCallback((tab: string) => { - setActiveTab(tab as ExtractorSubTab); + const sectionOptions = useMemo( + () => [ + { label: t('flow.keywords'), value: ExtractorSection.Keywords }, + { label: t('flow.questions'), value: ExtractorSection.Questions }, + { + label: t('flow.tags') || t('knowledgeDetails.autoTags'), + value: ExtractorSection.Tags, + }, + { label: t('flow.summary'), value: ExtractorSection.Summary }, + { label: t('flow.metadata'), value: ExtractorSection.Metadata }, + ], + [t], + ); + + const handleSectionChange = useCallback((section: string) => { + setActiveSection(section as ExtractorSection); }, []); return ( @@ -295,130 +275,107 @@ const GoExtractorForm = ({ ownerTenantId={ownerTenantId} > - - - - {t('flow.keywords')} - - - {t('flow.questions')} - - - {t('flow.tags') || t('knowledgeDetails.autoTags')} - - - {t('flow.summary')} - - - {t('flow.metadata')} - - +
+ - - - - - - +
+ {activeSection === ExtractorSection.Keywords && ( + <> + + + + + + )} - - - - - - + {activeSection === ExtractorSection.Questions && ( + <> + + + + + + )} - - - - {(field) => ( - + - )} - - + + {(field) => ( + + )} + + + )} - - - {(field) => ( - { - field.onChange(checked); - form.setValue('field_name', checked ? 'summary' : '', { - shouldDirty: true, - }); - form.setValue('enable_summary', checked ? 1 : 0, { - shouldDirty: true, - }); - }} - data-testid="extractor-summary-switch" - /> - )} - - - - - + {activeSection === ExtractorSection.Summary && ( + <> + + {(field) => ( + + )} + + + + + + )} - - - - + {activeSection === ExtractorSection.Metadata && ( + + )} +
+
{!hideOutputs && } diff --git a/web/src/pages/agent/hooks/use-add-node.ts b/web/src/pages/agent/hooks/use-add-node.ts index 6716af54d8..5d505e40be 100644 --- a/web/src/pages/agent/hooks/use-add-node.ts +++ b/web/src/pages/agent/hooks/use-add-node.ts @@ -1,4 +1,5 @@ import { useFetchDefaultModelDictionary } from '@/hooks/use-llm-request'; +import { isGoBackend } from '@/utils/backend-runtime'; import { Connection, Node, Position, ReactFlowInstance } from '@xyflow/react'; import humanId from 'human-id'; import { t } from 'i18next'; @@ -184,8 +185,15 @@ export const useInitializeOperatorParams = () => { [Operator.Extractor]: { ...getInitialExtractorValues(), llm_id: llmId, - sys_prompt: t('flow.prompts.system.summary'), - prompts: t('flow.prompts.user.summary'), + // sys_prompt/prompts belong to the Python extractor form. The Go + // form seeds summary.system_prompt itself, and the Go extractor + // falls back to a built-in prompt when it is empty. + ...(isGoBackend() + ? {} + : { + sys_prompt: t('flow.prompts.system.summary'), + prompts: t('flow.prompts.user.summary'), + }), }, [Operator.Compiler]: { ...initialCompilationValues, llm_id: llmId }, [Operator.DataOperations]: initialDataOperationsValues, diff --git a/web/src/pages/agent/utils.ts b/web/src/pages/agent/utils.ts index df562376bf..e3c149524c 100644 --- a/web/src/pages/agent/utils.ts +++ b/web/src/pages/agent/utils.ts @@ -20,6 +20,7 @@ import { isEmpty, isEqual, omit, + pick, sample, } from 'lodash'; import isObject from 'lodash/isObject'; @@ -399,82 +400,99 @@ export function transformTitleChunkerParams( }; } +// LLM setting keys the Go extractor DSL keeps besides the nested groups. +// Mirrors LlmSettingSchema (components/llm-setting-items/next) — duplicated +// here as a plain list so this module doesn't import the form components. +const ExtractorLlmSettingKeys = [ + 'llm_id', + 'temperature', + 'top_p', + 'presence_penalty', + 'frequency_penalty', + 'max_tokens', + 'parameter', + 'thinking', + 'temperatureEnabled', + 'topPEnabled', + 'presencePenaltyEnabled', + 'frequencyPenaltyEnabled', + 'maxTokensEnabled', +]; + export function transformExtractorParams( params: ExtractorFormSchemaType, -): Record { +): Record { const raw = params as Record; // The Python extractor only reads the legacy flat fields; the nested // per-feature configs below are Go-only. if (!isGoBackend()) { - return { ...params, prompts: [{ content: params.prompts, role: 'user' }] }; + return { ...params, prompts: [{ content: raw.prompts, role: 'user' }] }; } + // An unopened legacy node can still flow through here with flat keys + // (auto_keywords, keywords_sys_prompt, enable_metadata + metadata[], + // the transitional "metadata_config", ...). Accept them as read + // fallbacks — flat "metadata" is an array, which distinguishes it from + // the group object — but never re-emit them. + const metadataGroup = + raw.metadata !== undefined && !Array.isArray(raw.metadata) + ? raw.metadata + : raw.metadata_config; + const isMetadataEnabled = - params.metadata_config?.enabled !== undefined - ? Boolean(params.metadata_config?.enabled) - : params.enable_metadata === 1 || params.enable_metadata === true; + metadataGroup?.enabled !== undefined + ? Boolean(metadataGroup.enabled) + : raw.enable_metadata === 1 || raw.enable_metadata === true; const isSummaryEnabled = params.summary?.enabled !== undefined ? Boolean(params.summary?.enabled) - : params.enable_summary === 1 || - params.enable_summary === true || - params.field_name === 'summary'; + : raw.enable_summary === 1 || + raw.enable_summary === true || + raw.field_name === 'summary'; const metadataList = - params.metadata_config?.metadata ?? params.metadata ?? []; + metadataGroup?.metadata ?? + (Array.isArray(raw.metadata) ? raw.metadata : []); const builtInMetadataList = - params.metadata_config?.built_in_metadata ?? - params.built_in_metadata ?? - []; + metadataGroup?.built_in_metadata ?? raw.built_in_metadata ?? []; const summarySysPrompt = - params.summary?.system_prompt ?? params.sys_prompt ?? ''; + params.summary?.system_prompt ?? raw.sys_prompt ?? ''; - const keywordsTopN = params.keywords?.top_n ?? params.auto_keywords ?? 0; + const keywordsTopN = params.keywords?.top_n ?? raw.auto_keywords ?? 0; const keywordsSysPrompt = - params.keywords?.system_prompt ?? params.keywords_sys_prompt ?? ''; + params.keywords?.system_prompt ?? raw.keywords_sys_prompt ?? ''; - const questionsTopN = params.questions?.top_n ?? params.auto_questions ?? 0; + const questionsTopN = params.questions?.top_n ?? raw.auto_questions ?? 0; const questionsSysPrompt = - params.questions?.system_prompt ?? params.questions_sys_prompt ?? ''; + params.questions?.system_prompt ?? raw.questions_sys_prompt ?? ''; - const tagsTopN = params.tags?.top_n ?? params.auto_tags ?? 0; - const tagFileId = params.tags?.tag_file_id ?? params.tag_file_id ?? ''; + const tagsTopN = params.tags?.top_n ?? raw.auto_tags ?? 0; + const tagFileId = params.tags?.tag_file_id ?? raw.tag_file_id ?? ''; + // The Go extractor (schema.ExtractorParam) reads only llm_id plus the + // nested per-feature groups, so emit exactly that whitelist along with + // the LLM settings the form defines — no legacy flat mirrors, no + // display-only fields like outputs. return { - ...params, - prompts: [{ content: params.prompts, role: 'user' }], - auto_keywords: keywordsTopN, - keywords_sys_prompt: keywordsSysPrompt, + ...pick(params, ExtractorLlmSettingKeys), keywords: { top_n: keywordsTopN, system_prompt: keywordsSysPrompt, }, - auto_questions: questionsTopN, - questions_sys_prompt: questionsSysPrompt, questions: { top_n: questionsTopN, system_prompt: questionsSysPrompt, }, - auto_tags: tagsTopN, - tag_file_id: tagFileId, tags: { top_n: tagsTopN, tag_file_id: tagFileId, }, - enable_summary: isSummaryEnabled ? 1 : 0, - sys_prompt: summarySysPrompt, - field_name: isSummaryEnabled - ? (params.field_name || 'summary') - : (params.field_name === 'summary' ? '' : (params.field_name || '')), summary: { enabled: isSummaryEnabled, system_prompt: summarySysPrompt, }, - enable_metadata: isMetadataEnabled ? 1 : 0, - metadata: metadataList, - built_in_metadata: builtInMetadataList, - metadata_config: { + metadata: { enabled: isMetadataEnabled, metadata: metadataList, built_in_metadata: builtInMetadataList, diff --git a/web/src/pages/agent/utils/tests/extractor-transform.test.ts b/web/src/pages/agent/utils/tests/extractor-transform.test.ts index 0ea79d055d..8b4a47b6c8 100644 --- a/web/src/pages/agent/utils/tests/extractor-transform.test.ts +++ b/web/src/pages/agent/utils/tests/extractor-transform.test.ts @@ -13,13 +13,13 @@ describe('Extractor parameter transformations & precedence', () => { }); describe('transformExtractorParams', () => { - it('synchronizes nested modular configs to flat fields and preserves nested objects', () => { + it('emits only the LLM settings and the nested groups the Go extractor reads', () => { const input: any = { summary: { enabled: true, system_prompt: 'Custom summary prompt', }, - metadata_config: { + metadata: { enabled: true, metadata: [{ key: 'category', type: 'string' }], built_in_metadata: [{ key: 'update_time', type: 'time' }], @@ -37,22 +37,109 @@ describe('Extractor parameter transformations & precedence', () => { tag_file_id: 'tag-123', }, llm_id: 'gpt-4', + temperature: 0.5, + temperatureEnabled: true, + // Fields that must not leak into the DSL params + outputs: { chunks: { type: 'Array', value: [] } }, + prompts: 'user prompt', + sys_prompt: 'legacy sys', + field_name: 'summary', + auto_keywords: 9, + keywords_sys_prompt: 'legacy kw', }; const result = transformExtractorParams(input); - expect(result.enable_summary).toBe(1); - expect(result.summary).toEqual({ - enabled: true, - system_prompt: 'Custom summary prompt', + expect(result).toEqual({ + llm_id: 'gpt-4', + temperature: 0.5, + temperatureEnabled: true, + keywords: { + top_n: 5, + system_prompt: 'KW prompt', + }, + questions: { + top_n: 3, + system_prompt: 'Q prompt', + }, + tags: { + top_n: 2, + tag_file_id: 'tag-123', + }, + summary: { + enabled: true, + system_prompt: 'Custom summary prompt', + }, + metadata: { + enabled: true, + metadata: [{ key: 'category', type: 'string' }], + built_in_metadata: [{ key: 'update_time', type: 'time' }], + }, }); - expect(result.enable_metadata).toBe(1); - expect(result.metadata_config.enabled).toBe(true); - expect(result.metadata_config.metadata).toHaveLength(1); - expect(result.auto_keywords).toBe(5); - expect(result.auto_questions).toBe(3); - expect(result.auto_tags).toBe(2); - expect(result.tag_file_id).toBe('tag-123'); + }); + + it('maps legacy flat fields into the nested groups without re-emitting them', () => { + const input: any = { + auto_keywords: 4, + keywords_sys_prompt: 'legacy kw prompt', + auto_questions: 2, + questions_sys_prompt: 'legacy q prompt', + auto_tags: 1, + tag_file_id: 'tag-legacy', + enable_summary: 1, + sys_prompt: 'legacy summary prompt', + }; + + const result = transformExtractorParams(input); + + expect(result).toEqual({ + keywords: { top_n: 4, system_prompt: 'legacy kw prompt' }, + questions: { top_n: 2, system_prompt: 'legacy q prompt' }, + tags: { top_n: 1, tag_file_id: 'tag-legacy' }, + summary: { enabled: true, system_prompt: 'legacy summary prompt' }, + metadata: { enabled: false, metadata: [], built_in_metadata: [] }, + }); + }); + + it('accepts legacy flat metadata fields from unopened legacy nodes', () => { + const input: any = { + enable_metadata: 1, + metadata: [{ key: 'author', type: 'string' }], + built_in_metadata: [{ key: 'file_name', type: 'string' }], + }; + + const result = transformExtractorParams(input); + + expect(result).toEqual({ + keywords: { top_n: 0, system_prompt: '' }, + questions: { top_n: 0, system_prompt: '' }, + tags: { top_n: 0, tag_file_id: '' }, + summary: { enabled: false, system_prompt: '' }, + metadata: { + enabled: true, + metadata: [{ key: 'author', type: 'string' }], + built_in_metadata: [{ key: 'file_name', type: 'string' }], + }, + }); + }); + + it('accepts the transitional metadata_config key', () => { + const input: any = { + metadata_config: { + enabled: true, + metadata: [{ key: 'category', type: 'string' }], + built_in_metadata: [], + }, + }; + + const result = transformExtractorParams(input); + + expect(result.metadata).toEqual({ + enabled: true, + metadata: [{ key: 'category', type: 'string' }], + built_in_metadata: [], + }); + expect(result).not.toHaveProperty('metadata_config'); }); it('gives nested modular enabled: false precedence over legacy flat enable_*: 1', () => { @@ -62,7 +149,7 @@ describe('Extractor parameter transformations & precedence', () => { system_prompt: '', }, enable_summary: 1, - metadata_config: { + metadata: { enabled: false, metadata: [], built_in_metadata: [], @@ -73,22 +160,10 @@ describe('Extractor parameter transformations & precedence', () => { const result = transformExtractorParams(input); expect(result.summary.enabled).toBe(false); - expect(result.enable_summary).toBe(0); - expect(result.metadata_config.enabled).toBe(false); - expect(result.enable_metadata).toBe(0); - }); - - it('preserves custom field_name when summary is disabled', () => { - const input: any = { - summary: { - enabled: false, - system_prompt: '', - }, - field_name: 'custom_chunk_field', - }; - - const result = transformExtractorParams(input); - expect(result.field_name).toBe('custom_chunk_field'); + expect(result.metadata.enabled).toBe(false); + expect(result).not.toHaveProperty('enable_summary'); + expect(result).not.toHaveProperty('enable_metadata'); + expect(result).not.toHaveProperty('built_in_metadata'); }); }); @@ -112,15 +187,36 @@ describe('Extractor parameter transformations & precedence', () => { enabled: true, system_prompt: 'Old summary prompt', }); - expect(result.metadata_config).toEqual({ + expect(result.metadata).toEqual({ enabled: true, metadata: [{ key: 'author', type: 'string' }], built_in_metadata: [{ key: 'file_name', type: 'string' }], }); + expect(result).not.toHaveProperty('metadata_config'); + expect(result).not.toHaveProperty('enable_metadata'); + expect(result).not.toHaveProperty('built_in_metadata'); expect(result.keywords.top_n).toBe(4); expect(result.questions.top_n).toBe(2); expect(result.tags.top_n).toBe(1); }); + + it('passes through the metadata group shape unchanged', () => { + const config = { + metadata: { + enabled: true, + metadata: [{ key: 'category', type: 'string', enum: ['科技'] }], + built_in_metadata: [{ key: 'doc_name', type: 'string' }], + }, + }; + + const result = transformExtractorConfigToForm(config); + + expect(result.metadata).toEqual({ + enabled: true, + metadata: [{ key: 'category', type: 'string', enum: ['科技'] }], + built_in_metadata: [{ key: 'doc_name', type: 'string' }], + }); + }); }); describe('python backend keeps the legacy flat shape', () => { diff --git a/web/src/utils/pipeline-operator.ts b/web/src/utils/pipeline-operator.ts index df9023ed37..119dd92697 100644 --- a/web/src/utils/pipeline-operator.ts +++ b/web/src/utils/pipeline-operator.ts @@ -109,9 +109,19 @@ export function transformExtractorConfigToForm( ? Boolean(config.summary?.enabled) : config.enable_summary === 1 || config.enable_summary === true; + // The metadata group is stored under "metadata" — the shape the Go + // Extractor reads (schema.ExtractorParam). Accept two historical shapes: + // the transitional "metadata_config" key, and legacy flat nodes carrying + // enable_metadata + metadata[] + built_in_metadata[] (flat "metadata" is + // an array there, which distinguishes it from the group object). + const metadataGroup = + config.metadata !== undefined && !Array.isArray(config.metadata) + ? config.metadata + : config.metadata_config; + const isMetadataEnabled = - config.metadata_config?.enabled !== undefined - ? Boolean(config.metadata_config?.enabled) + metadataGroup?.enabled !== undefined + ? Boolean(metadataGroup.enabled) : config.enable_metadata === 1 || config.enable_metadata === true; result.keywords = { @@ -141,22 +151,20 @@ export function transformExtractorConfigToForm( enabled: isSummaryEnabled, system_prompt: config.summary?.system_prompt ?? config.sys_prompt ?? '', }; - result.enable_summary = isSummaryEnabled ? 1 : 0; - result.field_name = isSummaryEnabled - ? (config.field_name || 'summary') - : (config.field_name === 'summary' ? '' : (config.field_name || '')); - result.metadata_config = { + result.metadata = { enabled: isMetadataEnabled, - metadata: config.metadata_config?.metadata ?? config.metadata ?? [], + metadata: + metadataGroup?.metadata ?? + (Array.isArray(config.metadata) ? config.metadata : []), built_in_metadata: - config.metadata_config?.built_in_metadata ?? - config.built_in_metadata ?? - [], + metadataGroup?.built_in_metadata ?? config.built_in_metadata ?? [], }; - result.enable_metadata = isMetadataEnabled ? 1 : 0; - result.metadata = result.metadata_config.metadata; - result.built_in_metadata = result.metadata_config.built_in_metadata; + // Drop the historical keys so they don't linger in form state and get + // re-emitted into the DSL on save — the Go extractor only reads the group. + delete result.metadata_config; + delete result.enable_metadata; + delete result.built_in_metadata; return result; }