From 4942a2329044b387fde963667ea92225237bae83 Mon Sep 17 00:00:00 2001 From: balibabu Date: Tue, 18 Nov 2025 18:58:36 +0800 Subject: [PATCH] Feat: Add a switch to control the display of structured output to the agent form. #10427 (#11344) ### What problem does this PR solve? Feat: Add a switch to control the display of structured output to the agent form. #10427 ### Type of change - [x] New Feature (non-breaking change which adds functionality) --- .../node/dropdown/accordion-operators.tsx | 1 - web/src/pages/agent/constant/index.tsx | 13 +--- web/src/pages/agent/form/agent-form/index.tsx | 65 ++++++++++++++----- .../use-show-structured-output-dialog.ts | 14 ++-- .../agent/form/agent-form/use-watch-change.ts | 17 ++++- .../pages/agent/form/components/output.tsx | 13 +++- .../structured-output-secondary-menu.tsx | 17 +++-- .../pages/agent/form/message-form/index.tsx | 3 +- .../agent/form/message-form/use-values.ts | 3 +- .../pages/agent/form/switch-form/index.tsx | 27 ++------ .../hooks/use-build-structured-output.ts | 5 +- .../utils/filter-agent-structured-output.ts | 16 +++-- web/src/utils/canvas-util.tsx | 14 +++- 13 files changed, 133 insertions(+), 75 deletions(-) diff --git a/web/src/pages/agent/canvas/node/dropdown/accordion-operators.tsx b/web/src/pages/agent/canvas/node/dropdown/accordion-operators.tsx index dd458279c..6021420c5 100644 --- a/web/src/pages/agent/canvas/node/dropdown/accordion-operators.tsx +++ b/web/src/pages/agent/canvas/node/dropdown/accordion-operators.tsx @@ -81,7 +81,6 @@ export function AccordionOperators({ Operator.DataOperations, Operator.VariableAssigner, Operator.ListOperations, - Operator.VariableAssigner, Operator.VariableAggregator, ]} isCustomDropdown={isCustomDropdown} diff --git a/web/src/pages/agent/constant/index.tsx b/web/src/pages/agent/constant/index.tsx index 87217a999..bdd5a0763 100644 --- a/web/src/pages/agent/constant/index.tsx +++ b/web/src/pages/agent/constant/index.tsx @@ -463,21 +463,14 @@ export const initialAgentValues = { tools: [], mcp: [], cite: true, + showStructuredOutput: false, + [AgentStructuredOutputField]: {}, outputs: { - // structured_output: { - // topic: { - // type: 'string', - // description: - // 'default:general. The category of the search.news is useful for retrieving real-time updates, particularly about politics, sports, and major current events covered by mainstream media sources. general is for broader, more general-purpose searches that may include a wide range of sources.', - // enum: ['general', 'news'], - // default: 'general', - // }, - // }, content: { type: 'string', value: '', }, - [AgentStructuredOutputField]: {}, + // [AgentStructuredOutputField]: {}, }, }; diff --git a/web/src/pages/agent/form/agent-form/index.tsx b/web/src/pages/agent/form/agent-form/index.tsx index 12b713f93..b683a281f 100644 --- a/web/src/pages/agent/form/agent-form/index.tsx +++ b/web/src/pages/agent/form/agent-form/index.tsx @@ -6,6 +6,7 @@ import { import { LlmSettingSchema } from '@/components/llm-setting-items/next'; import { MessageHistoryWindowSizeFormField } from '@/components/message-history-window-size-item'; import { SelectWithSearch } from '@/components/originui/select-with-search'; +import { RAGFlowFormItem } from '@/components/ragflow-form'; import { Button } from '@/components/ui/button'; import { Form, @@ -15,6 +16,7 @@ import { FormLabel, } from '@/components/ui/form'; import { Input, NumberInput } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; import { Separator } from '@/components/ui/separator'; import { Switch } from '@/components/ui/switch'; import { LlmModelType } from '@/constants/knowledge'; @@ -26,9 +28,9 @@ import { useTranslation } from 'react-i18next'; import { z } from 'zod'; import { AgentExceptionMethod, + AgentStructuredOutputField, NodeHandleId, VariableType, - initialAgentValues, } from '../../constant'; import { INextOperatorForm } from '../../interface'; import useGraphStore from '../../store'; @@ -71,18 +73,20 @@ const FormSchema = z.object({ exception_default_value: z.string().optional(), ...LargeModelFilterFormSchema, cite: z.boolean().optional(), + showStructuredOutput: z.boolean().optional(), + [AgentStructuredOutputField]: z.record(z.any()), }); export type AgentFormSchemaType = z.infer; -const outputList = buildOutputList(initialAgentValues.outputs); - function AgentForm({ node }: INextOperatorForm) { const { t } = useTranslation(); const { edges, deleteEdgesBySourceAndSourceHandle } = useGraphStore( (state) => state, ); + const outputList = buildOutputList(node?.data.form.outputs); + const defaultValues = useValues(node); const { extraOptions } = useBuildPromptExtraPromptOptions(edges, node?.id); @@ -112,13 +116,18 @@ function AgentForm({ node }: INextOperatorForm) { name: 'exception_method', }); + const showStructuredOutput = useWatch({ + control: form.control, + name: 'showStructuredOutput', + }); + const { initialStructuredOutput, showStructuredOutputDialog, structuredOutputDialogVisible, hideStructuredOutputDialog, handleStructuredOutputDialogOk, - } = useShowStructuredOutputDialog(node?.id); + } = useShowStructuredOutputDialog(form); useEffect(() => { if (exceptionMethod !== AgentExceptionMethod.Goto) { @@ -275,18 +284,42 @@ function AgentForm({ node }: INextOperatorForm) { )} - -
-
- {t('flow.structuredOutput.structuredOutput')} - -
- -
+ + + + + + {(field) => ( +
+ + +
+ )} +
+
+ {showStructuredOutput && ( +
+
+ {t('flow.structuredOutput.structuredOutput')} + +
+ + +
+ )} {structuredOutputDialogVisible && ( diff --git a/web/src/pages/agent/form/agent-form/use-show-structured-output-dialog.ts b/web/src/pages/agent/form/agent-form/use-show-structured-output-dialog.ts index 19e38cefe..3d492f724 100644 --- a/web/src/pages/agent/form/agent-form/use-show-structured-output-dialog.ts +++ b/web/src/pages/agent/form/agent-form/use-show-structured-output-dialog.ts @@ -1,27 +1,25 @@ import { JSONSchema } from '@/components/jsonjoy-builder'; +import { AgentStructuredOutputField } from '@/constants/agent'; import { useSetModalState } from '@/hooks/common-hooks'; import { useCallback } from 'react'; -import useGraphStore from '../../store'; +import { UseFormReturn } from 'react-hook-form'; -export function useShowStructuredOutputDialog(nodeId?: string) { +export function useShowStructuredOutputDialog(form: UseFormReturn) { const { visible: structuredOutputDialogVisible, showModal: showStructuredOutputDialog, hideModal: hideStructuredOutputDialog, } = useSetModalState(); - const { updateNodeForm, getNode } = useGraphStore((state) => state); - const initialStructuredOutput = getNode(nodeId)?.data.form.outputs.structured; + const initialStructuredOutput = form.getValues(AgentStructuredOutputField); const handleStructuredOutputDialogOk = useCallback( (values: JSONSchema) => { // Sync data to canvas - if (nodeId) { - updateNodeForm(nodeId, values, ['outputs', 'structured']); - } + form.setValue(AgentStructuredOutputField, values); hideStructuredOutputDialog(); }, - [hideStructuredOutputDialog, nodeId, updateNodeForm], + [form, hideStructuredOutputDialog], ); return { diff --git a/web/src/pages/agent/form/agent-form/use-watch-change.ts b/web/src/pages/agent/form/agent-form/use-watch-change.ts index 98b0ecf31..08011cc6d 100644 --- a/web/src/pages/agent/form/agent-form/use-watch-change.ts +++ b/web/src/pages/agent/form/agent-form/use-watch-change.ts @@ -1,6 +1,7 @@ +import { omit } from 'lodash'; import { useEffect } from 'react'; import { UseFormReturn, useWatch } from 'react-hook-form'; -import { PromptRole } from '../../constant'; +import { AgentStructuredOutputField, PromptRole } from '../../constant'; import useGraphStore from '../../store'; export function useWatchFormChange(id?: string, form?: UseFormReturn) { @@ -16,6 +17,20 @@ export function useWatchFormChange(id?: string, form?: UseFormReturn) { prompts: [{ role: PromptRole.User, content: values.prompts }], }; + if (values.showStructuredOutput) { + nextValues = { + ...nextValues, + outputs: { + ...values.outputs, + [AgentStructuredOutputField]: values[AgentStructuredOutputField], + }, + }; + } else { + nextValues = { + ...nextValues, + outputs: omit(values.outputs, [AgentStructuredOutputField]), + }; + } updateNodeForm(id, nextValues); } }, [form?.formState.isDirty, id, updateNodeForm, values]); diff --git a/web/src/pages/agent/form/components/output.tsx b/web/src/pages/agent/form/components/output.tsx index 68551ab3c..73058b67b 100644 --- a/web/src/pages/agent/form/components/output.tsx +++ b/web/src/pages/agent/form/components/output.tsx @@ -1,6 +1,7 @@ import { RAGFlowFormItem } from '@/components/ragflow-form'; import { Input } from '@/components/ui/input'; import { t } from 'i18next'; +import { PropsWithChildren } from 'react'; import { z } from 'zod'; export type OutputType = { @@ -11,7 +12,7 @@ export type OutputType = { type OutputProps = { list: Array; isFormRequired?: boolean; -}; +} & PropsWithChildren; export function transferOutputs(outputs: Record) { return Object.entries(outputs).map(([key, value]) => ({ @@ -24,10 +25,16 @@ export const OutputSchema = { outputs: z.record(z.any()), }; -export function Output({ list, isFormRequired = false }: OutputProps) { +export function Output({ + list, + isFormRequired = false, + children, +}: OutputProps) { return (
-
{t('flow.output')}
+
+ {t('flow.output')} {children} +
    {list.map((x, idx) => (
  • x === dataType) || + (types?.some((x) => x === compositeDataType) || hasSpecificTypeChild(value ?? {}, types))) ) { return (
  • {key} - {dataType} + + {compositeDataType} +
    {[JsonSchemaDataType.Object, JsonSchemaDataType.Array].some( (x) => x === dataType, @@ -122,7 +129,7 @@ export function StructuredOutputSecondaryMenu({ side="left" align="start" className={cn( - 'min-w-[140px] border border-border rounded-md shadow-lg p-0', + 'min-w-72 border border-border rounded-md shadow-lg p-0', )} >
    diff --git a/web/src/pages/agent/form/message-form/index.tsx b/web/src/pages/agent/form/message-form/index.tsx index 31b52659e..177fc2f45 100644 --- a/web/src/pages/agent/form/message-form/index.tsx +++ b/web/src/pages/agent/form/message-form/index.tsx @@ -81,7 +81,8 @@ function MessageForm({ node }: INextOperatorForm) { )} {...field} onValueChange={field.onChange} - placeholder={t('flow.messagePlaceholder')} + placeholder={t('common.selectPlaceholder')} + allowClear > diff --git a/web/src/pages/agent/form/message-form/use-values.ts b/web/src/pages/agent/form/message-form/use-values.ts index 0cece91fc..6a90881be 100644 --- a/web/src/pages/agent/form/message-form/use-values.ts +++ b/web/src/pages/agent/form/message-form/use-values.ts @@ -1,7 +1,7 @@ import { RAGFlowNodeType } from '@/interfaces/database/flow'; import { isEmpty } from 'lodash'; import { useMemo } from 'react'; -import { ExportFileType, initialMessageValues } from '../../constant'; +import { initialMessageValues } from '../../constant'; import { convertToObjectArray } from '../../utils'; export function useValues(node?: RAGFlowNodeType) { @@ -15,7 +15,6 @@ export function useValues(node?: RAGFlowNodeType) { return { ...formData, content: convertToObjectArray(formData.content), - output_format: formData.output_format || ExportFileType.PDF, }; }, [node]); diff --git a/web/src/pages/agent/form/switch-form/index.tsx b/web/src/pages/agent/form/switch-form/index.tsx index 6d6849147..f9ccee919 100644 --- a/web/src/pages/agent/form/switch-form/index.tsx +++ b/web/src/pages/agent/form/switch-form/index.tsx @@ -1,5 +1,4 @@ import { FormContainer } from '@/components/form-container'; -import { SelectWithSearch } from '@/components/originui/select-with-search'; import { BlockButton, Button } from '@/components/ui/button'; import { Card, CardContent } from '@/components/ui/card'; import { @@ -16,16 +15,15 @@ import { useBuildSwitchOperatorOptions } from '@/hooks/logic-hooks/use-build-ope import { cn } from '@/lib/utils'; import { zodResolver } from '@hookform/resolvers/zod'; import { t } from 'i18next'; -import { toLower } from 'lodash'; import { X } from 'lucide-react'; import { memo, useCallback, useMemo } from 'react'; import { useFieldArray, useForm, useFormContext } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { z } from 'zod'; -import { SwitchLogicOperatorOptions, VariableType } from '../../constant'; -import { useBuildQueryVariableOptions } from '../../hooks/use-get-begin-query'; +import { SwitchLogicOperatorOptions } from '../../constant'; import { IOperatorForm } from '../../interface'; import { FormWrapper } from '../components/form-wrapper'; +import { QueryVariable } from '../components/query-variable'; import { useValues } from './use-values'; import { useWatchFormChange } from './use-watch-change'; @@ -47,19 +45,6 @@ function ConditionCards({ }: ConditionCardsProps) { const form = useFormContext(); - const nextOptions = useBuildQueryVariableOptions(); - - const finalOptions = useMemo(() => { - return nextOptions.map((x) => { - return { - ...x, - options: x.options.filter( - (y) => !toLower(y.type).includes(VariableType.Array), - ), - }; - }); - }, [nextOptions]); - const switchOperatorOptions = useBuildSwitchOperatorOptions(); const name = `${parentName}.${ItemKey}`; @@ -101,11 +86,11 @@ function ConditionCards({ render={({ field }) => ( - + hideLabel + > diff --git a/web/src/pages/agent/hooks/use-build-structured-output.ts b/web/src/pages/agent/hooks/use-build-structured-output.ts index 891c2dfa9..c9d34bd43 100644 --- a/web/src/pages/agent/hooks/use-build-structured-output.ts +++ b/web/src/pages/agent/hooks/use-build-structured-output.ts @@ -1,3 +1,4 @@ +import { getStructuredDatatype } from '@/utils/canvas-util'; import { get, isPlainObject } from 'lodash'; import { ReactNode, useCallback } from 'react'; import { @@ -106,10 +107,10 @@ export function useFindAgentStructuredOutputTypeByValue() { if (isPlainObject(values) && properties) { for (const [key, value] of Object.entries(properties)) { const nextPath = path ? `${path}.${key}` : key; - const dataType = get(value, 'type'); + const { dataType, compositeDataType } = getStructuredDatatype(value); if (nextPath === target) { - return dataType; + return compositeDataType; } if ( diff --git a/web/src/pages/agent/utils/filter-agent-structured-output.ts b/web/src/pages/agent/utils/filter-agent-structured-output.ts index 40db6b4fe..c20263631 100644 --- a/web/src/pages/agent/utils/filter-agent-structured-output.ts +++ b/web/src/pages/agent/utils/filter-agent-structured-output.ts @@ -1,9 +1,13 @@ import { JSONSchema } from '@/components/jsonjoy-builder'; +import { getStructuredDatatype } from '@/utils/canvas-util'; import { get, isPlainObject, toLower } from 'lodash'; import { JsonSchemaDataType } from '../constant'; -function predicate(types: string[], type: string) { - return types.some((x) => toLower(x) === toLower(type)); +function predicate(types: string[], value: unknown) { + return types.some( + (x) => + toLower(x) === toLower(getStructuredDatatype(value).compositeDataType), + ); } export function hasSpecificTypeChild( @@ -12,7 +16,7 @@ export function hasSpecificTypeChild( ) { if (Array.isArray(data)) { for (const value of data) { - if (isPlainObject(value) && predicate(types, value.type)) { + if (isPlainObject(value) && predicate(types, value)) { return true; } if (hasSpecificTypeChild(value, types)) { @@ -23,7 +27,11 @@ export function hasSpecificTypeChild( if (isPlainObject(data)) { for (const value of Object.values(data)) { - if (isPlainObject(value) && predicate(types, value.type)) { + if ( + isPlainObject(value) && + predicate(types, value) && + get(data, 'type') !== JsonSchemaDataType.Array + ) { return true; } diff --git a/web/src/utils/canvas-util.tsx b/web/src/utils/canvas-util.tsx index f59c4c195..d55862d3e 100644 --- a/web/src/utils/canvas-util.tsx +++ b/web/src/utils/canvas-util.tsx @@ -6,7 +6,7 @@ import { import { BaseNode } from '@/interfaces/database/agent'; import { Edge } from '@xyflow/react'; -import { isEmpty } from 'lodash'; +import { get, isEmpty } from 'lodash'; import { ComponentType, ReactNode } from 'react'; export function filterAllUpstreamNodeIds(edges: Edge[], nodeIds: string[]) { @@ -87,3 +87,15 @@ export function buildNodeOutputOptions({ ), })); } + +export function getStructuredDatatype(value: Record | unknown) { + const dataType = get(value, 'type'); + const arrayItemsType = get(value, 'items.type', JsonSchemaDataType.String); + + const compositeDataType = + dataType === JsonSchemaDataType.Array + ? `${dataType}<${arrayItemsType}>` + : dataType; + + return { dataType, compositeDataType }; +}