From 32a7ad3cba1178273cafdc735dee73785d6db912 Mon Sep 17 00:00:00 2001 From: balibabu Date: Thu, 26 Jun 2025 16:43:06 +0800 Subject: [PATCH] Feat: Customize the output variable name of the loop operator #3221 (#8514) ### What problem does this PR solve? Feat: Customize the output variable name of the loop operator #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality) --- web/src/pages/agent/constant.tsx | 1 + .../pages/agent/form/components/output.tsx | 9 +- .../agent/form/components/query-variable.tsx | 13 ++- .../form/iteration-form/dynamic-output.tsx | 97 +++++++++++++++++++ .../pages/agent/form/iteration-form/index.tsx | 35 +++---- .../agent/form/iteration-form/interface.ts | 2 + .../form/iteration-form/use-build-options.ts | 31 ++++++ .../agent/form/iteration-form/use-values.ts | 21 ++-- .../iteration-form/use-watch-form-change.ts | 29 ++++++ .../pages/agent/hooks/use-get-begin-query.tsx | 2 +- 10 files changed, 202 insertions(+), 38 deletions(-) create mode 100644 web/src/pages/agent/form/iteration-form/dynamic-output.tsx create mode 100644 web/src/pages/agent/form/iteration-form/interface.ts create mode 100644 web/src/pages/agent/form/iteration-form/use-build-options.ts create mode 100644 web/src/pages/agent/form/iteration-form/use-watch-form-change.ts diff --git a/web/src/pages/agent/constant.tsx b/web/src/pages/agent/constant.tsx index dd8a882d3..d5f27d0d3 100644 --- a/web/src/pages/agent/constant.tsx +++ b/web/src/pages/agent/constant.tsx @@ -646,6 +646,7 @@ export const initialEmailValues = { export const initialIterationValues = { items_ref: '', + outputs: {}, }; export const initialIterationStartValues = { outputs: { diff --git a/web/src/pages/agent/form/components/output.tsx b/web/src/pages/agent/form/components/output.tsx index 3f7e4ae27..eefc3cef5 100644 --- a/web/src/pages/agent/form/components/output.tsx +++ b/web/src/pages/agent/form/components/output.tsx @@ -1,12 +1,19 @@ export type OutputType = { title: string; - type: string; + type?: string; }; type OutputProps = { list: Array; }; +export function transferOutputs(outputs: Record) { + return Object.entries(outputs).map(([key, value]) => ({ + title: key, + type: value?.type, + })); +} + export function Output({ list }: OutputProps) { return (
diff --git a/web/src/pages/agent/form/components/query-variable.tsx b/web/src/pages/agent/form/components/query-variable.tsx index caa85cd7a..a0c498a1d 100644 --- a/web/src/pages/agent/form/components/query-variable.tsx +++ b/web/src/pages/agent/form/components/query-variable.tsx @@ -14,19 +14,18 @@ import { useBuildQueryVariableOptions } from '../../hooks/use-get-begin-query'; type QueryVariableProps = { name?: string; type?: VariableType }; -export function QueryVariable({ - name = 'query', - type = VariableType.String, -}: QueryVariableProps) { +export function QueryVariable({ name = 'query', type }: QueryVariableProps) { const { t } = useTranslation(); const form = useFormContext(); const nextOptions = useBuildQueryVariableOptions(); const finalOptions = useMemo(() => { - return nextOptions.map((x) => { - return { ...x, options: x.options.filter((y) => y.type === type) }; - }); + return type + ? nextOptions.map((x) => { + return { ...x, options: x.options.filter((y) => y.type === type) }; + }) + : nextOptions; }, [nextOptions, type]); return ( diff --git a/web/src/pages/agent/form/iteration-form/dynamic-output.tsx b/web/src/pages/agent/form/iteration-form/dynamic-output.tsx new file mode 100644 index 000000000..6b7e39e33 --- /dev/null +++ b/web/src/pages/agent/form/iteration-form/dynamic-output.tsx @@ -0,0 +1,97 @@ +'use client'; + +import { FormContainer } from '@/components/form-container'; +import { SelectWithSearch } from '@/components/originui/select-with-search'; +import { BlockButton, Button } from '@/components/ui/button'; +import { + FormControl, + FormField, + FormItem, + FormMessage, +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { Separator } from '@/components/ui/separator'; +import { RAGFlowNodeType } from '@/interfaces/database/flow'; +import { X } from 'lucide-react'; +import { ReactNode } from 'react'; +import { useFieldArray, useFormContext } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { useBuildSubNodeOutputOptions } from './use-build-options'; + +interface IProps { + node?: RAGFlowNodeType; +} + +export function DynamicOutputForm({ node }: IProps) { + const { t } = useTranslation(); + const form = useFormContext(); + const options = useBuildSubNodeOutputOptions(node?.id); + const name = 'outputs'; + + const { fields, remove, append } = useFieldArray({ + name: name, + control: form.control, + }); + + return ( +
+ {fields.map((field, index) => { + const typeField = `${name}.${index}.name`; + return ( +
+ ( + + + + + + + )} + /> + + ( + + + + + + + )} + /> + +
+ ); + })} + append({ name: '', ref: undefined })}> + Add + +
+ ); +} + +export function VariableTitle({ title }: { title: ReactNode }) { + return
{title}
; +} + +export function DynamicOutput({ node }: IProps) { + return ( + + + + + ); +} diff --git a/web/src/pages/agent/form/iteration-form/index.tsx b/web/src/pages/agent/form/iteration-form/index.tsx index 7a9d58dc1..7ff631252 100644 --- a/web/src/pages/agent/form/iteration-form/index.tsx +++ b/web/src/pages/agent/form/iteration-form/index.tsx @@ -2,36 +2,23 @@ import { FormContainer } from '@/components/form-container'; import { Form } from '@/components/ui/form'; import { zodResolver } from '@hookform/resolvers/zod'; import { useMemo } from 'react'; -import { useForm } from 'react-hook-form'; +import { useForm, useWatch } from 'react-hook-form'; import { z } from 'zod'; -import { initialRetrievalValues, VariableType } from '../../constant'; -import { useWatchFormChange } from '../../hooks/use-watch-form-change'; +import { VariableType } from '../../constant'; import { INextOperatorForm } from '../../interface'; import { Output } from '../components/output'; import { QueryVariable } from '../components/query-variable'; +import { DynamicOutput } from './dynamic-output'; +import { OutputArray } from './interface'; import { useValues } from './use-values'; +import { useWatchFormChange } from './use-watch-form-change'; const FormSchema = z.object({ query: z.string().optional(), - similarity_threshold: z.coerce.number(), - keywords_similarity_weight: z.coerce.number(), - top_n: z.coerce.number(), - top_k: z.coerce.number(), - kb_ids: z.array(z.string()), - rerank_id: z.string(), - empty_response: z.string(), + outputs: z.array(z.object({ name: z.string(), value: z.any() })).optional(), }); const IterationForm = ({ node }: INextOperatorForm) => { - const outputList = useMemo(() => { - return [ - { - title: 'formalized_content', - type: initialRetrievalValues.outputs.formalized_content.type, - }, - ]; - }, []); - const defaultValues = useValues(node); const form = useForm({ @@ -39,6 +26,15 @@ const IterationForm = ({ node }: INextOperatorForm) => { resolver: zodResolver(FormSchema), }); + const outputs: OutputArray = useWatch({ + control: form?.control, + name: 'outputs', + }); + + const outputList = useMemo(() => { + return outputs.map((x) => ({ title: x.name, type: x?.type })); + }, [outputs]); + useWatchFormChange(node?.id, form); return ( @@ -55,6 +51,7 @@ const IterationForm = ({ node }: INextOperatorForm) => { type={VariableType.Array} > + diff --git a/web/src/pages/agent/form/iteration-form/interface.ts b/web/src/pages/agent/form/iteration-form/interface.ts new file mode 100644 index 000000000..1a23c0a2e --- /dev/null +++ b/web/src/pages/agent/form/iteration-form/interface.ts @@ -0,0 +1,2 @@ +export type OutputArray = Array<{ name: string; ref: string; type?: string }>; +export type OutputObject = Record; diff --git a/web/src/pages/agent/form/iteration-form/use-build-options.ts b/web/src/pages/agent/form/iteration-form/use-build-options.ts new file mode 100644 index 000000000..3439000d4 --- /dev/null +++ b/web/src/pages/agent/form/iteration-form/use-build-options.ts @@ -0,0 +1,31 @@ +import { isEmpty } from 'lodash'; +import { useMemo } from 'react'; +import { Operator } from '../../constant'; +import { buildOutputOptions } from '../../hooks/use-get-begin-query'; +import useGraphStore from '../../store'; + +export function useBuildSubNodeOutputOptions(nodeId?: string) { + const { nodes } = useGraphStore((state) => state); + + const nodeOutputOptions = useMemo(() => { + if (!nodeId) { + return []; + } + + const subNodeWithOutputList = nodes.filter( + (x) => + x.parentId === nodeId && + x.data.label !== Operator.IterationStart && + !isEmpty(x.data?.form?.outputs), + ); + + return subNodeWithOutputList.map((x) => ({ + label: x.data.name, + value: x.id, + title: x.data.name, + options: buildOutputOptions(x.data.form.outputs, x.id), + })); + }, [nodeId, nodes]); + + return nodeOutputOptions; +} diff --git a/web/src/pages/agent/form/iteration-form/use-values.ts b/web/src/pages/agent/form/iteration-form/use-values.ts index 2f8479efe..7c686255d 100644 --- a/web/src/pages/agent/form/iteration-form/use-values.ts +++ b/web/src/pages/agent/form/iteration-form/use-values.ts @@ -2,24 +2,25 @@ import { RAGFlowNodeType } from '@/interfaces/database/flow'; import { isEmpty } from 'lodash'; import { useMemo } from 'react'; import { initialIterationValues } from '../../constant'; +import { OutputObject } from './interface'; + +function convertToArray(outputObject: OutputObject) { + return Object.entries(outputObject).map(([key, value]) => ({ + name: key, + ref: value.ref, + })); +} export function useValues(node?: RAGFlowNodeType) { - const defaultValues = useMemo( - () => ({ - ...initialIterationValues, - }), - [], - ); - const values = useMemo(() => { const formData = node?.data?.form; if (isEmpty(formData)) { - return defaultValues; + return { ...initialIterationValues, outputs: [] }; } - return formData; - }, [defaultValues, node?.data?.form]); + return { ...formData, outputs: convertToArray(formData.outputs) }; + }, [node?.data?.form]); return values; } diff --git a/web/src/pages/agent/form/iteration-form/use-watch-form-change.ts b/web/src/pages/agent/form/iteration-form/use-watch-form-change.ts new file mode 100644 index 000000000..b5d3d6264 --- /dev/null +++ b/web/src/pages/agent/form/iteration-form/use-watch-form-change.ts @@ -0,0 +1,29 @@ +import { useEffect } from 'react'; +import { UseFormReturn, useWatch } from 'react-hook-form'; +import useGraphStore from '../../store'; +import { OutputArray, OutputObject } from './interface'; + +function transferToObject(list: OutputArray) { + return list.reduce((pre, cur) => { + pre[cur.name] = { ref: cur.ref }; + return pre; + }, {}); +} + +export function useWatchFormChange(id?: string, form?: UseFormReturn) { + let values = useWatch({ control: form?.control }); + const updateNodeForm = useGraphStore((state) => state.updateNodeForm); + + useEffect(() => { + // Manually triggered form updates are synchronized to the canvas + if (id && form?.formState.isDirty) { + values = form?.getValues(); + let nextValues: any = { + ...values, + outputs: transferToObject(values.outputs), + }; + + updateNodeForm(id, nextValues); + } + }, [form?.formState.isDirty, id, updateNodeForm, values]); +} diff --git a/web/src/pages/agent/hooks/use-get-begin-query.tsx b/web/src/pages/agent/hooks/use-get-begin-query.tsx index 8ad416453..6468e2675 100644 --- a/web/src/pages/agent/hooks/use-get-begin-query.tsx +++ b/web/src/pages/agent/hooks/use-get-begin-query.tsx @@ -58,7 +58,7 @@ function filterAllUpstreamNodeIds(edges: Edge[], nodeIds: string[]) { }, []); } -function buildOutputOptions( +export function buildOutputOptions( outputs: Record = {}, nodeId?: string, ) {