### What problem does this PR solve? Feat: Edit the output data of the code operator #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.20.0
| @@ -135,9 +135,10 @@ export interface IRetrievalForm { | |||
| } | |||
| export interface ICodeForm { | |||
| inputs?: Array<{ name?: string; component_id?: string }>; | |||
| arguments: Record<string, string>; | |||
| lang: string; | |||
| script?: string; | |||
| outputs: Record<string, { value: string; type: string }>; | |||
| } | |||
| export interface IAgentForm { | |||
| @@ -667,7 +667,7 @@ export const initialCodeValues = { | |||
| arg1: '', | |||
| arg2: '', | |||
| }, | |||
| outputs: { result: { value: '', type: 'string' } }, | |||
| outputs: {}, | |||
| }; | |||
| export const initialWaitingDialogueValues = {}; | |||
| @@ -13,16 +13,18 @@ import { | |||
| import { Input } from '@/components/ui/input'; | |||
| import { RAGFlowSelect } from '@/components/ui/select'; | |||
| import { ProgrammingLanguage } from '@/constants/agent'; | |||
| import { ICodeForm } from '@/interfaces/database/flow'; | |||
| import { ICodeForm } from '@/interfaces/database/agent'; | |||
| import { zodResolver } from '@hookform/resolvers/zod'; | |||
| import { useForm } from 'react-hook-form'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { z } from 'zod'; | |||
| import { buildOutputList } from '../../utils/build-output-list'; | |||
| import { Output } from '../components/output'; | |||
| import { | |||
| DynamicInputVariable, | |||
| TypeOptions, | |||
| VariableTitle, | |||
| } from './next-variable'; | |||
| import { FormSchema, FormSchemaType } from './schema'; | |||
| import { useValues } from './use-values'; | |||
| import { | |||
| useHandleLanguageChange, | |||
| @@ -36,26 +38,14 @@ const options = [ | |||
| ProgrammingLanguage.Javascript, | |||
| ].map((x) => ({ value: x, label: x })); | |||
| const DynamicFieldName = 'outputs'; | |||
| const CodeForm = ({ node }: INextOperatorForm) => { | |||
| const formData = node?.data.form as ICodeForm; | |||
| const { t } = useTranslation(); | |||
| const values = useValues(node); | |||
| const FormSchema = z.object({ | |||
| lang: z.string(), | |||
| script: z.string(), | |||
| arguments: z.array( | |||
| z.object({ name: z.string(), component_id: z.string() }), | |||
| ), | |||
| return: z.union([ | |||
| z | |||
| .array(z.object({ name: z.string(), component_id: z.string() })) | |||
| .optional(), | |||
| z.object({ name: z.string(), component_id: z.string() }), | |||
| ]), | |||
| }); | |||
| const form = useForm({ | |||
| const form = useForm<FormSchemaType>({ | |||
| defaultValues: values, | |||
| resolver: zodResolver(FormSchema), | |||
| }); | |||
| @@ -75,6 +65,7 @@ const CodeForm = ({ node }: INextOperatorForm) => { | |||
| <DynamicInputVariable | |||
| node={node} | |||
| title={t('flow.input')} | |||
| isOutputs={false} | |||
| ></DynamicInputVariable> | |||
| <FormField | |||
| control={form.control} | |||
| @@ -124,7 +115,8 @@ const CodeForm = ({ node }: INextOperatorForm) => { | |||
| <DynamicInputVariable | |||
| node={node} | |||
| title={'Return Values'} | |||
| name={'return'} | |||
| name={DynamicFieldName} | |||
| isOutputs | |||
| ></DynamicInputVariable> | |||
| ) : ( | |||
| <div> | |||
| @@ -132,7 +124,7 @@ const CodeForm = ({ node }: INextOperatorForm) => { | |||
| <FormContainer className="space-y-5"> | |||
| <FormField | |||
| control={form.control} | |||
| name={'return.name'} | |||
| name={`${DynamicFieldName}.name`} | |||
| render={({ field }) => ( | |||
| <FormItem> | |||
| <FormLabel>Name</FormLabel> | |||
| @@ -148,7 +140,7 @@ const CodeForm = ({ node }: INextOperatorForm) => { | |||
| /> | |||
| <FormField | |||
| control={form.control} | |||
| name={`return.component_id`} | |||
| name={`${DynamicFieldName}.type`} | |||
| render={({ field }) => ( | |||
| <FormItem className="flex-1"> | |||
| <FormLabel>Type</FormLabel> | |||
| @@ -167,6 +159,9 @@ const CodeForm = ({ node }: INextOperatorForm) => { | |||
| </div> | |||
| )} | |||
| </form> | |||
| <div className="p-5"> | |||
| <Output list={buildOutputList(formData.outputs)}></Output> | |||
| </div> | |||
| </Form> | |||
| ); | |||
| }; | |||
| @@ -10,6 +10,7 @@ import { | |||
| FormMessage, | |||
| } from '@/components/ui/form'; | |||
| import { BlurInput } from '@/components/ui/input'; | |||
| import { RAGFlowSelect } from '@/components/ui/select'; | |||
| import { Separator } from '@/components/ui/separator'; | |||
| import { RAGFlowNodeType } from '@/interfaces/database/flow'; | |||
| import { X } from 'lucide-react'; | |||
| @@ -21,18 +22,19 @@ import { useBuildQueryVariableOptions } from '../../hooks/use-get-begin-query'; | |||
| interface IProps { | |||
| node?: RAGFlowNodeType; | |||
| name?: string; | |||
| isOutputs: boolean; | |||
| } | |||
| export const TypeOptions = [ | |||
| 'String', | |||
| 'Number', | |||
| 'Boolean', | |||
| 'Array[String]', | |||
| 'Array[Number]', | |||
| 'Array<String>', | |||
| 'Array<Number>', | |||
| 'Object', | |||
| ].map((x) => ({ label: x, value: x })); | |||
| export function DynamicVariableForm({ name = 'arguments' }: IProps) { | |||
| export function DynamicVariableForm({ name = 'arguments', isOutputs }: IProps) { | |||
| const { t } = useTranslation(); | |||
| const form = useFormContext(); | |||
| @@ -67,14 +69,22 @@ export function DynamicVariableForm({ name = 'arguments' }: IProps) { | |||
| <Separator className="w-3 text-text-sub-title" /> | |||
| <FormField | |||
| control={form.control} | |||
| name={`${name}.${index}.component_id`} | |||
| name={`${name}.${index}.type`} | |||
| render={({ field }) => ( | |||
| <FormItem className="flex-1 overflow-hidden"> | |||
| <FormControl> | |||
| <SelectWithSearch | |||
| options={nextOptions} | |||
| {...field} | |||
| ></SelectWithSearch> | |||
| {isOutputs ? ( | |||
| <RAGFlowSelect | |||
| placeholder={t('common.pleaseSelect')} | |||
| options={TypeOptions} | |||
| {...field} | |||
| ></RAGFlowSelect> | |||
| ) : ( | |||
| <SelectWithSearch | |||
| options={nextOptions} | |||
| {...field} | |||
| ></SelectWithSearch> | |||
| )} | |||
| </FormControl> | |||
| <FormMessage /> | |||
| </FormItem> | |||
| @@ -86,9 +96,7 @@ export function DynamicVariableForm({ name = 'arguments' }: IProps) { | |||
| </div> | |||
| ); | |||
| })} | |||
| <BlockButton | |||
| onClick={() => append({ name: '', component_id: undefined })} | |||
| > | |||
| <BlockButton onClick={() => append({ name: '', type: undefined })}> | |||
| {t('flow.addVariable')} | |||
| </BlockButton> | |||
| </div> | |||
| @@ -103,12 +111,17 @@ export function DynamicInputVariable({ | |||
| node, | |||
| name, | |||
| title, | |||
| isOutputs = false, | |||
| }: IProps & { title: ReactNode }) { | |||
| return ( | |||
| <section> | |||
| <VariableTitle title={title}></VariableTitle> | |||
| <FormContainer> | |||
| <DynamicVariableForm node={node} name={name}></DynamicVariableForm> | |||
| <DynamicVariableForm | |||
| node={node} | |||
| name={name} | |||
| isOutputs={isOutputs} | |||
| ></DynamicVariableForm> | |||
| </FormContainer> | |||
| </section> | |||
| ); | |||
| @@ -0,0 +1,14 @@ | |||
| import { ProgrammingLanguage } from '@/constants/agent'; | |||
| import { z } from 'zod'; | |||
| export const FormSchema = z.object({ | |||
| lang: z.enum([ProgrammingLanguage.Python, ProgrammingLanguage.Javascript]), | |||
| script: z.string(), | |||
| arguments: z.array(z.object({ name: z.string(), type: z.string() })), | |||
| outputs: z.union([ | |||
| z.array(z.object({ name: z.string(), type: z.string() })).optional(), | |||
| z.object({ name: z.string(), type: z.string() }), | |||
| ]), | |||
| }); | |||
| export type FormSchemaType = z.infer<typeof FormSchema>; | |||
| @@ -1,3 +1,5 @@ | |||
| import { ProgrammingLanguage } from '@/constants/agent'; | |||
| import { ICodeForm } from '@/interfaces/database/agent'; | |||
| import { RAGFlowNodeType } from '@/interfaces/database/flow'; | |||
| import { isEmpty } from 'lodash'; | |||
| import { useMemo } from 'react'; | |||
| @@ -6,10 +8,26 @@ import { initialCodeValues } from '../../constant'; | |||
| function convertToArray(args: Record<string, string>) { | |||
| return Object.entries(args).map(([key, value]) => ({ | |||
| name: key, | |||
| component_id: value, | |||
| type: value, | |||
| })); | |||
| } | |||
| type OutputsFormType = { name: string; type: string }; | |||
| function convertOutputsToArray({ lang, outputs = {} }: ICodeForm) { | |||
| if (lang === ProgrammingLanguage.Python) { | |||
| return Object.entries(outputs).map(([key, val]) => ({ | |||
| name: key, | |||
| type: val.type, | |||
| })); | |||
| } | |||
| return Object.entries(outputs).reduce<OutputsFormType>((pre, [key, val]) => { | |||
| pre.name = key; | |||
| pre.type = val.type; | |||
| return pre; | |||
| }, {} as OutputsFormType); | |||
| } | |||
| export function useValues(node?: RAGFlowNodeType) { | |||
| const values = useMemo(() => { | |||
| const formData = node?.data?.form; | |||
| @@ -18,7 +36,11 @@ export function useValues(node?: RAGFlowNodeType) { | |||
| return initialCodeValues; | |||
| } | |||
| return { ...formData, arguments: convertToArray(formData.arguments) }; | |||
| return { | |||
| ...formData, | |||
| arguments: convertToArray(formData.arguments), | |||
| outputs: convertOutputsToArray(formData), | |||
| }; | |||
| }, [node?.data?.form]); | |||
| return values; | |||
| @@ -1,17 +1,55 @@ | |||
| import { CodeTemplateStrMap, ProgrammingLanguage } from '@/constants/agent'; | |||
| import { ICodeForm } from '@/interfaces/database/agent'; | |||
| import { isEmpty } from 'lodash'; | |||
| import { useCallback, useEffect } from 'react'; | |||
| import { UseFormReturn, useWatch } from 'react-hook-form'; | |||
| import useGraphStore from '../../store'; | |||
| import { FormSchemaType } from './schema'; | |||
| function convertToObject(list: Array<{ name: string; component_id: string }>) { | |||
| function convertToObject(list: FormSchemaType['arguments'] = []) { | |||
| return list.reduce<Record<string, string>>((pre, cur) => { | |||
| pre[cur.name] = cur.component_id; | |||
| if (cur.name && cur.type) { | |||
| pre[cur.name] = cur.type; | |||
| } | |||
| return pre; | |||
| }, {}); | |||
| } | |||
| export function useWatchFormChange(id?: string, form?: UseFormReturn) { | |||
| type ArrayOutputs = Extract<FormSchemaType['outputs'], Array<any>>; | |||
| type ObjectOutputs = Exclude<FormSchemaType['outputs'], Array<any>>; | |||
| function convertOutputsToObject({ lang, outputs }: FormSchemaType) { | |||
| if (lang === ProgrammingLanguage.Python) { | |||
| return (outputs as ArrayOutputs).reduce<ICodeForm['outputs']>( | |||
| (pre, cur) => { | |||
| pre[cur.name] = { | |||
| value: '', | |||
| type: cur.type, | |||
| }; | |||
| return pre; | |||
| }, | |||
| {}, | |||
| ); | |||
| } | |||
| const outputsObject = outputs as ObjectOutputs; | |||
| if (isEmpty(outputsObject)) { | |||
| return {}; | |||
| } | |||
| return { | |||
| [outputsObject.name]: { | |||
| value: '', | |||
| type: outputsObject.type, | |||
| }, | |||
| }; | |||
| } | |||
| export function useWatchFormChange( | |||
| id?: string, | |||
| form?: UseFormReturn<FormSchemaType>, | |||
| ) { | |||
| let values = useWatch({ control: form?.control }); | |||
| const updateNodeForm = useGraphStore((state) => state.updateNodeForm); | |||
| @@ -21,7 +59,10 @@ export function useWatchFormChange(id?: string, form?: UseFormReturn) { | |||
| values = form?.getValues() || {}; | |||
| let nextValues: any = { | |||
| ...values, | |||
| arguments: convertToObject(values.arguments), | |||
| arguments: convertToObject( | |||
| values?.arguments as FormSchemaType['arguments'], | |||
| ), | |||
| outputs: convertOutputsToObject(values as FormSchemaType), | |||
| }; | |||
| updateNodeForm(id, nextValues); | |||
| @@ -29,7 +70,10 @@ export function useWatchFormChange(id?: string, form?: UseFormReturn) { | |||
| }, [form?.formState.isDirty, id, updateNodeForm, values]); | |||
| } | |||
| export function useHandleLanguageChange(id?: string, form?: UseFormReturn) { | |||
| export function useHandleLanguageChange( | |||
| id?: string, | |||
| form?: UseFormReturn<FormSchemaType>, | |||
| ) { | |||
| const updateNodeForm = useGraphStore((state) => state.updateNodeForm); | |||
| const handleLanguageChange = useCallback( | |||
| @@ -37,6 +81,12 @@ export function useHandleLanguageChange(id?: string, form?: UseFormReturn) { | |||
| if (id) { | |||
| const script = CodeTemplateStrMap[lang as ProgrammingLanguage]; | |||
| form?.setValue('script', script); | |||
| form?.setValue( | |||
| 'outputs', | |||
| (lang === ProgrammingLanguage.Python | |||
| ? [] | |||
| : {}) as FormSchemaType['outputs'], | |||
| ); | |||
| updateNodeForm(id, script, ['script']); | |||
| } | |||
| }, | |||
| @@ -0,0 +1,8 @@ | |||
| import { OutputType } from '../form/components/output'; | |||
| export function buildOutputList(outputs: Record<string, Record<string, any>>) { | |||
| return Object.entries(outputs).reduce<OutputType[]>((pre, [key, val]) => { | |||
| pre.push({ title: key, type: val.type }); | |||
| return pre; | |||
| }, []); | |||
| } | |||