From f7b6c4ca99ffdc2256f034aa417855a8d531550e Mon Sep 17 00:00:00 2001 From: balibabu Date: Fri, 27 Jun 2025 09:27:28 +0800 Subject: [PATCH] Feat: Add StringTransform operator #3221 (#8520) ### What problem does this PR solve? Feat: Add StringTransform operator #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality) --- .../node/dropdown/next-step-dropdown.tsx | 6 +- web/src/pages/agent/constant.tsx | 28 +++ .../agent/form-sheet/use-form-config-map.tsx | 6 + .../agent/form/components/query-variable.tsx | 20 ++- .../form/string-transform-form/index.tsx | 161 ++++++++++++++++++ .../form/string-transform-form/use-values.ts | 27 +++ .../use-watch-form-change.ts | 26 +++ web/src/pages/agent/hooks/use-add-node.ts | 2 + web/src/pages/agent/operator-icon.tsx | 5 +- 9 files changed, 273 insertions(+), 8 deletions(-) create mode 100644 web/src/pages/agent/form/string-transform-form/index.tsx create mode 100644 web/src/pages/agent/form/string-transform-form/use-values.ts create mode 100644 web/src/pages/agent/form/string-transform-form/use-watch-form-change.ts diff --git a/web/src/pages/agent/canvas/node/dropdown/next-step-dropdown.tsx b/web/src/pages/agent/canvas/node/dropdown/next-step-dropdown.tsx index dfb6b12f1..4acc9c746 100644 --- a/web/src/pages/agent/canvas/node/dropdown/next-step-dropdown.tsx +++ b/web/src/pages/agent/canvas/node/dropdown/next-step-dropdown.tsx @@ -23,7 +23,7 @@ const HideModalContext = createContext['showModal']>(() => {}); function OperatorItemList({ operators }: OperatorItemProps) { const { addCanvasNode } = useContext(AgentInstanceContext); - const { nodeId, id, type, position } = useContext(HandleContext); + const { nodeId, id, position } = useContext(HandleContext); const hideModal = useContext(HideModalContext); return ( @@ -89,7 +89,9 @@ function AccordionOperators() { Data Manipulation - + diff --git a/web/src/pages/agent/constant.tsx b/web/src/pages/agent/constant.tsx index d5f27d0d3..ea78c9765 100644 --- a/web/src/pages/agent/constant.tsx +++ b/web/src/pages/agent/constant.tsx @@ -87,6 +87,7 @@ export enum Operator { Tool = 'Tool', TavilySearch = 'TavilySearch', UserFillUp = 'UserFillUp', + StringTransform = 'StringTransform', } export const SwitchLogicOperatorOptions = ['and', 'or']; @@ -704,6 +705,32 @@ export const initialUserFillUpValues = { inputs: [], }; +export enum StringTransformMethod { + Merge = 'merge', + Split = 'split', +} + +export enum StringTransformDelimiter { + Comma = ',', + Semicolon = ';', + Period = '.', + LineBreak = '\n', + Tab = '\t', + Space = ' ', +} + +export const initialStringTransformValues = { + method: StringTransformMethod.Merge, + split_ref: '', + script: '', + delimiters: [], + outputs: { + result: { + type: 'string', + }, + }, +}; + export enum TavilySearchDepth { Basic = 'basic', Advanced = 'advanced', @@ -869,6 +896,7 @@ export const NodeMap = { [Operator.Tool]: 'toolNode', [Operator.TavilySearch]: 'ragNode', [Operator.UserFillUp]: 'ragNode', + [Operator.StringTransform]: 'ragNode', }; export enum BeginQueryType { diff --git a/web/src/pages/agent/form-sheet/use-form-config-map.tsx b/web/src/pages/agent/form-sheet/use-form-config-map.tsx index 175205f64..71ac344e5 100644 --- a/web/src/pages/agent/form-sheet/use-form-config-map.tsx +++ b/web/src/pages/agent/form-sheet/use-form-config-map.tsx @@ -33,6 +33,7 @@ import QWeatherForm from '../form/qweather-form'; import RelevantForm from '../form/relevant-form'; import RetrievalForm from '../form/retrieval-form/next'; import RewriteQuestionForm from '../form/rewrite-question-form'; +import { StringTransformForm } from '../form/string-transform-form'; import SwitchForm from '../form/switch-form'; import TavilyForm from '../form/tavily-form'; import TemplateForm from '../form/template-form'; @@ -388,6 +389,11 @@ export function useFormConfigMap() { defaultValues: {}, schema: z.object({}), }, + [Operator.StringTransform]: { + component: StringTransformForm, + defaultValues: {}, + schema: z.object({}), + }, }; return FormConfigMap; diff --git a/web/src/pages/agent/form/components/query-variable.tsx b/web/src/pages/agent/form/components/query-variable.tsx index a0c498a1d..9b33a87ae 100644 --- a/web/src/pages/agent/form/components/query-variable.tsx +++ b/web/src/pages/agent/form/components/query-variable.tsx @@ -6,15 +6,23 @@ import { FormLabel, FormMessage, } from '@/components/ui/form'; -import { useMemo } from 'react'; +import { ReactNode, useMemo } from 'react'; import { useFormContext } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { VariableType } from '../../constant'; import { useBuildQueryVariableOptions } from '../../hooks/use-get-begin-query'; -type QueryVariableProps = { name?: string; type?: VariableType }; +type QueryVariableProps = { + name?: string; + type?: VariableType; + label?: ReactNode; +}; -export function QueryVariable({ name = 'query', type }: QueryVariableProps) { +export function QueryVariable({ + name = 'query', + type, + label, +}: QueryVariableProps) { const { t } = useTranslation(); const form = useFormContext(); @@ -34,7 +42,11 @@ export function QueryVariable({ name = 'query', type }: QueryVariableProps) { name={name} render={({ field }) => ( - {t('flow.query')} + {label || ( + + {t('flow.query')} + + )} ({ label: key, value: val }), +); + +export const StringTransformForm = ({ node }: INextOperatorForm) => { + const values = useValues(node); + + const FormSchema = z.object({ + method: z.string(), + split_ref: z.string().optional(), + script: z.string().optional(), + delimiters: z.array(z.string()), + outputs: z.object({ result: z.object({ type: z.string() }) }).optional(), + }); + + const form = useForm>({ + defaultValues: values, + resolver: zodResolver(FormSchema), + }); + + const method = useWatch({ control: form.control, name: 'method' }); + + const isSplit = method === StringTransformMethod.Split; + + const outputList = useMemo(() => { + return transferOutputs(values.outputs); + }, [values.outputs]); + + const handleMethodChange = useCallback( + (value: StringTransformMethod) => { + const outputs = { + ...initialStringTransformValues.outputs, + result: { + type: + value === StringTransformMethod.Merge ? 'string' : 'Array', + }, + }; + form.setValue('outputs', outputs); + }, + [form], + ); + + useWatchFormChange(node?.id, form); + + return ( +
+ { + e.preventDefault(); + }} + > + + ( + + method + + { + handleMethodChange(value); + field.onChange(value); + }} + > + + + + )} + /> + {isSplit && ( + split_ref} + name="split_ref" + > + )} + {isSplit || ( + ( + + script + + + + + + )} + /> + )} + ( + + delimiters + + {isSplit ? ( + + ) : ( + + )} + + + + )} + /> +
} + /> +
+
+
+ +
+ + ); +}; diff --git a/web/src/pages/agent/form/string-transform-form/use-values.ts b/web/src/pages/agent/form/string-transform-form/use-values.ts new file mode 100644 index 000000000..363f1567d --- /dev/null +++ b/web/src/pages/agent/form/string-transform-form/use-values.ts @@ -0,0 +1,27 @@ +import { RAGFlowNodeType } from '@/interfaces/database/flow'; +import { isEmpty } from 'lodash'; +import { useMemo } from 'react'; +import { + initialStringTransformValues, + StringTransformMethod, +} from '../../constant'; + +export function useValues(node?: RAGFlowNodeType) { + const values = useMemo(() => { + const formData = node?.data?.form; + + if (isEmpty(formData)) { + return initialStringTransformValues; + } + + return { + ...formData, + delimiters: + formData.method === StringTransformMethod.Merge + ? formData.delimiters[0] + : formData.delimiters, + }; + }, [node?.data?.form]); + + return values; +} diff --git a/web/src/pages/agent/form/string-transform-form/use-watch-form-change.ts b/web/src/pages/agent/form/string-transform-form/use-watch-form-change.ts new file mode 100644 index 000000000..c5b7841f2 --- /dev/null +++ b/web/src/pages/agent/form/string-transform-form/use-watch-form-change.ts @@ -0,0 +1,26 @@ +import { useEffect } from 'react'; +import { UseFormReturn, useWatch } from 'react-hook-form'; +import { StringTransformMethod } from '../../constant'; +import useGraphStore from '../../store'; + +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; + + if ( + values.delimiters !== undefined && + values.method === StringTransformMethod.Merge + ) { + nextValues.delimiters = [values.delimiters]; + } + + updateNodeForm(id, nextValues); + } + }, [form?.formState.isDirty, id, updateNodeForm, values]); +} diff --git a/web/src/pages/agent/hooks/use-add-node.ts b/web/src/pages/agent/hooks/use-add-node.ts index c243f9773..023028b1a 100644 --- a/web/src/pages/agent/hooks/use-add-node.ts +++ b/web/src/pages/agent/hooks/use-add-node.ts @@ -39,6 +39,7 @@ import { initialRelevantValues, initialRetrievalValues, initialRewriteQuestionValues, + initialStringTransformValues, initialSwitchValues, initialTavilyValues, initialTemplateValues, @@ -108,6 +109,7 @@ export const useInitializeOperatorParams = () => { [Operator.Tool]: {}, [Operator.TavilySearch]: initialTavilyValues, [Operator.UserFillUp]: initialUserFillUpValues, + [Operator.StringTransform]: initialStringTransformValues, }; }, [llmId]); diff --git a/web/src/pages/agent/operator-icon.tsx b/web/src/pages/agent/operator-icon.tsx index 1547b6ca7..f7d787b72 100644 --- a/web/src/pages/agent/operator-icon.tsx +++ b/web/src/pages/agent/operator-icon.tsx @@ -1,6 +1,6 @@ import { IconFont } from '@/components/icon-font'; import { cn } from '@/lib/utils'; -import { CirclePlay, MessageSquareMore } from 'lucide-react'; +import { CirclePlay } from 'lucide-react'; import { Operator } from './constant'; interface IProps { @@ -19,7 +19,8 @@ export const OperatorIconMap = { [Operator.Switch]: 'condition', [Operator.Code]: 'code-set', [Operator.Agent]: 'agent-ai', - [Operator.UserFillUp]: MessageSquareMore, + [Operator.UserFillUp]: 'await', + [Operator.StringTransform]: 'a-textprocessing', // [Operator.Relevant]: BranchesOutlined, // [Operator.RewriteQuestion]: FormOutlined, // [Operator.KeywordExtract]: KeywordIcon,