### What problem does this PR solve? Feat: Add StringTransform operator #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.20.0
| @@ -23,7 +23,7 @@ const HideModalContext = createContext<IModalProps<any>['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 | |||
| </AccordionTrigger> | |||
| <AccordionContent className="flex flex-col gap-4 text-balance"> | |||
| <OperatorItemList operators={[Operator.Code]}></OperatorItemList> | |||
| <OperatorItemList | |||
| operators={[Operator.Code, Operator.StringTransform]} | |||
| ></OperatorItemList> | |||
| </AccordionContent> | |||
| </AccordionItem> | |||
| <AccordionItem value="item-5"> | |||
| @@ -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 { | |||
| @@ -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; | |||
| @@ -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 }) => ( | |||
| <FormItem> | |||
| <FormLabel tooltip={t('chat.modelTip')}>{t('flow.query')}</FormLabel> | |||
| {label || ( | |||
| <FormLabel tooltip={t('chat.modelTip')}> | |||
| {t('flow.query')} | |||
| </FormLabel> | |||
| )} | |||
| <FormControl> | |||
| <SelectWithSearch | |||
| options={finalOptions} | |||
| @@ -0,0 +1,161 @@ | |||
| import { FormContainer } from '@/components/form-container'; | |||
| import { | |||
| Form, | |||
| FormControl, | |||
| FormField, | |||
| FormItem, | |||
| FormLabel, | |||
| FormMessage, | |||
| } from '@/components/ui/form'; | |||
| import { MultiSelect } from '@/components/ui/multi-select'; | |||
| import { RAGFlowSelect } from '@/components/ui/select'; | |||
| import { buildOptions } from '@/utils/form'; | |||
| import { zodResolver } from '@hookform/resolvers/zod'; | |||
| import { useCallback, useMemo } from 'react'; | |||
| import { useForm, useWatch } from 'react-hook-form'; | |||
| import { z } from 'zod'; | |||
| import { | |||
| StringTransformDelimiter, | |||
| StringTransformMethod, | |||
| initialStringTransformValues, | |||
| } from '../../constant'; | |||
| import { INextOperatorForm } from '../../interface'; | |||
| import { Output, transferOutputs } from '../components/output'; | |||
| import { PromptEditor } from '../components/prompt-editor'; | |||
| import { QueryVariable } from '../components/query-variable'; | |||
| import { useValues } from './use-values'; | |||
| import { useWatchFormChange } from './use-watch-form-change'; | |||
| const DelimiterOptions = Object.entries(StringTransformDelimiter).map( | |||
| ([key, val]) => ({ 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<z.infer<typeof FormSchema>>({ | |||
| 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<string>', | |||
| }, | |||
| }; | |||
| form.setValue('outputs', outputs); | |||
| }, | |||
| [form], | |||
| ); | |||
| useWatchFormChange(node?.id, form); | |||
| return ( | |||
| <Form {...form}> | |||
| <form | |||
| className="space-y-5 px-5 " | |||
| autoComplete="off" | |||
| onSubmit={(e) => { | |||
| e.preventDefault(); | |||
| }} | |||
| > | |||
| <FormContainer> | |||
| <FormField | |||
| control={form.control} | |||
| name="method" | |||
| render={({ field }) => ( | |||
| <FormItem> | |||
| <FormLabel>method</FormLabel> | |||
| <FormControl> | |||
| <RAGFlowSelect | |||
| {...field} | |||
| options={buildOptions(StringTransformMethod)} | |||
| onChange={(value) => { | |||
| handleMethodChange(value); | |||
| field.onChange(value); | |||
| }} | |||
| ></RAGFlowSelect> | |||
| </FormControl> | |||
| <FormMessage /> | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| {isSplit && ( | |||
| <QueryVariable | |||
| label={<FormLabel>split_ref</FormLabel>} | |||
| name="split_ref" | |||
| ></QueryVariable> | |||
| )} | |||
| {isSplit || ( | |||
| <FormField | |||
| control={form.control} | |||
| name="script" | |||
| render={({ field }) => ( | |||
| <FormItem> | |||
| <FormLabel>script</FormLabel> | |||
| <FormControl> | |||
| <PromptEditor {...field} showToolbar={false}></PromptEditor> | |||
| </FormControl> | |||
| <FormMessage /> | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| )} | |||
| <FormField | |||
| control={form.control} | |||
| name="delimiters" | |||
| render={({ field }) => ( | |||
| <FormItem> | |||
| <FormLabel>delimiters</FormLabel> | |||
| <FormControl> | |||
| {isSplit ? ( | |||
| <MultiSelect | |||
| options={DelimiterOptions} | |||
| onValueChange={field.onChange} | |||
| variant="inverted" | |||
| {...field} | |||
| /> | |||
| ) : ( | |||
| <RAGFlowSelect | |||
| {...field} | |||
| options={DelimiterOptions} | |||
| ></RAGFlowSelect> | |||
| )} | |||
| </FormControl> | |||
| <FormMessage /> | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| <FormField | |||
| control={form.control} | |||
| name="outputs" | |||
| render={() => <div></div>} | |||
| /> | |||
| </FormContainer> | |||
| </form> | |||
| <div className="p-5"> | |||
| <Output list={outputList}></Output> | |||
| </div> | |||
| </Form> | |||
| ); | |||
| }; | |||
| @@ -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; | |||
| } | |||
| @@ -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<any>) { | |||
| 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]); | |||
| } | |||
| @@ -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]); | |||
| @@ -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, | |||