### What problem does this PR solve? Feat: Support uploading files when running agent #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.20.0
| ResetAgent = 'resetAgent', | ResetAgent = 'resetAgent', | ||||
| SetAgent = 'setAgent', | SetAgent = 'setAgent', | ||||
| FetchAgentTemplates = 'fetchAgentTemplates', | FetchAgentTemplates = 'fetchAgentTemplates', | ||||
| UploadCanvasFile = 'uploadCanvasFile', | |||||
| } | } | ||||
| export const EmptyDsl = { | export const EmptyDsl = { | ||||
| return { data, loading, setAgent: mutateAsync }; | return { data, loading, setAgent: mutateAsync }; | ||||
| }; | }; | ||||
| export const useUploadCanvasFile = () => { | |||||
| const { | |||||
| data, | |||||
| isPending: loading, | |||||
| mutateAsync, | |||||
| } = useMutation({ | |||||
| mutationKey: [AgentApiAction.UploadCanvasFile], | |||||
| mutationFn: async (body: any) => { | |||||
| let nextBody = body; | |||||
| try { | |||||
| if (Array.isArray(body)) { | |||||
| nextBody = new FormData(); | |||||
| body.forEach((file: File) => { | |||||
| nextBody.append('file', file as any); | |||||
| }); | |||||
| } | |||||
| const { data } = await flowService.uploadCanvasFile(nextBody); | |||||
| if (data?.code === 0) { | |||||
| message.success(i18n.t('message.uploaded')); | |||||
| } | |||||
| return data; | |||||
| } catch (error) { | |||||
| message.error('error'); | |||||
| } | |||||
| }, | |||||
| }); | |||||
| return { data, loading, uploadCanvasFile: mutateAsync }; | |||||
| }; |
| import { MessageType } from '@/constants/chat'; | import { MessageType } from '@/constants/chat'; | ||||
| import { useGetFileIcon } from '@/pages/chat/hooks'; | import { useGetFileIcon } from '@/pages/chat/hooks'; | ||||
| import { buildMessageItemReference } from '@/pages/chat/utils'; | |||||
| import { Spin } from 'antd'; | import { Spin } from 'antd'; | ||||
| import { useSendNextMessage } from './hooks'; | import { useSendNextMessage } from './hooks'; | ||||
| import DebugContent from '../debug-content'; | import DebugContent from '../debug-content'; | ||||
| import { BeginQuery } from '../interface'; | import { BeginQuery } from '../interface'; | ||||
| import { buildBeginQueryWithObject } from '../utils'; | import { buildBeginQueryWithObject } from '../utils'; | ||||
| import { buildAgentMessageItemReference } from '../utils/chat'; | |||||
| const AgentChatBox = () => { | const AgentChatBox = () => { | ||||
| const { | const { | ||||
| avatar={userInfo.avatar} | avatar={userInfo.avatar} | ||||
| avatarDialog={canvasInfo.avatar} | avatarDialog={canvasInfo.avatar} | ||||
| item={message} | item={message} | ||||
| reference={buildMessageItemReference( | |||||
| reference={buildAgentMessageItemReference( | |||||
| { message: derivedMessages, reference }, | { message: derivedMessages, reference }, | ||||
| message, | message, | ||||
| )} | )} |
| import { FileUploader } from '@/components/file-uploader'; | |||||
| import { ButtonLoading } from '@/components/ui/button'; | import { ButtonLoading } from '@/components/ui/button'; | ||||
| import { | import { | ||||
| Form, | Form, | ||||
| import { z } from 'zod'; | import { z } from 'zod'; | ||||
| import { BeginQueryType } from '../constant'; | import { BeginQueryType } from '../constant'; | ||||
| import { BeginQuery } from '../interface'; | import { BeginQuery } from '../interface'; | ||||
| import { FileUploadDirectUpload } from './uploader'; | |||||
| export const BeginQueryComponentMap = { | export const BeginQueryComponentMap = { | ||||
| [BeginQueryType.Line]: 'string', | [BeginQueryType.Line]: 'string', | ||||
| } else if (type === BeginQueryType.Integer) { | } else if (type === BeginQueryType.Integer) { | ||||
| fieldSchema = z.coerce.number(); | fieldSchema = z.coerce.number(); | ||||
| } else { | } else { | ||||
| fieldSchema = z.instanceof(File); | |||||
| fieldSchema = z.record(z.any()); | |||||
| } | } | ||||
| if (cur.optional) { | if (cur.optional) { | ||||
| <React.Fragment key={idx}> | <React.Fragment key={idx}> | ||||
| <FormField | <FormField | ||||
| control={form.control} | control={form.control} | ||||
| name={'file'} | |||||
| name={props.name} | |||||
| render={({ field }) => ( | render={({ field }) => ( | ||||
| <div className="space-y-6"> | <div className="space-y-6"> | ||||
| <FormItem className="w-full"> | <FormItem className="w-full"> | ||||
| <FormLabel>{t('assistantAvatar')}</FormLabel> | <FormLabel>{t('assistantAvatar')}</FormLabel> | ||||
| <FormControl> | <FormControl> | ||||
| <FileUploader | |||||
| <FileUploadDirectUpload | |||||
| value={field.value} | value={field.value} | ||||
| onValueChange={field.onChange} | |||||
| maxFileCount={1} | |||||
| maxSize={4 * 1024 * 1024} | |||||
| /> | |||||
| onChange={field.onChange} | |||||
| ></FileUploadDirectUpload> | |||||
| </FormControl> | </FormControl> | ||||
| <FormMessage /> | <FormMessage /> | ||||
| </FormItem> | </FormItem> | ||||
| (values: z.infer<typeof formSchemaValues.schema>) => { | (values: z.infer<typeof formSchemaValues.schema>) => { | ||||
| const nextValues = Object.entries(values).map(([key, value]) => { | const nextValues = Object.entries(values).map(([key, value]) => { | ||||
| const item = parameters[Number(key)]; | const item = parameters[Number(key)]; | ||||
| let nextValue = value; | |||||
| if (Array.isArray(value)) { | |||||
| nextValue = ``; | |||||
| value.forEach((x) => { | |||||
| nextValue += | |||||
| x?.originFileObj instanceof File | |||||
| ? `${x.name}\n${x.response?.data}\n----\n` | |||||
| : `${x.url}\n${x.result}\n----\n`; | |||||
| }); | |||||
| } | |||||
| return { ...item, value: nextValue }; | |||||
| return { ...item, value }; | |||||
| }); | }); | ||||
| ok(nextValues); | ok(nextValues); |
| 'use client'; | |||||
| import { | |||||
| FileUpload, | |||||
| FileUploadDropzone, | |||||
| FileUploadItem, | |||||
| FileUploadItemDelete, | |||||
| FileUploadItemMetadata, | |||||
| FileUploadItemPreview, | |||||
| FileUploadItemProgress, | |||||
| FileUploadList, | |||||
| FileUploadTrigger, | |||||
| type FileUploadProps, | |||||
| } from '@/components/file-upload'; | |||||
| import { Button } from '@/components/ui/button'; | |||||
| import { useUploadCanvasFile } from '@/hooks/use-agent-request'; | |||||
| import { Upload, X } from 'lucide-react'; | |||||
| import * as React from 'react'; | |||||
| import { toast } from 'sonner'; | |||||
| type FileUploadDirectUploadProps = { | |||||
| value: Record<string, any>; | |||||
| onChange(value: Record<string, any>): void; | |||||
| }; | |||||
| export function FileUploadDirectUpload({ | |||||
| onChange, | |||||
| }: FileUploadDirectUploadProps) { | |||||
| const [files, setFiles] = React.useState<File[]>([]); | |||||
| const { uploadCanvasFile } = useUploadCanvasFile(); | |||||
| const onUpload: NonNullable<FileUploadProps['onUpload']> = React.useCallback( | |||||
| async (files, { onSuccess, onError }) => { | |||||
| try { | |||||
| const uploadPromises = files.map(async (file) => { | |||||
| const handleError = (error?: any) => { | |||||
| onError( | |||||
| file, | |||||
| error instanceof Error ? error : new Error('Upload failed'), | |||||
| ); | |||||
| }; | |||||
| try { | |||||
| const ret = await uploadCanvasFile([file]); | |||||
| if (ret.code === 0) { | |||||
| onSuccess(file); | |||||
| onChange(ret.data); | |||||
| } else { | |||||
| handleError(); | |||||
| } | |||||
| } catch (error) { | |||||
| handleError(error); | |||||
| } | |||||
| }); | |||||
| // Wait for all uploads to complete | |||||
| await Promise.all(uploadPromises); | |||||
| } catch (error) { | |||||
| // This handles any error that might occur outside the individual upload processes | |||||
| console.error('Unexpected error during upload:', error); | |||||
| } | |||||
| }, | |||||
| [onChange, uploadCanvasFile], | |||||
| ); | |||||
| const onFileReject = React.useCallback((file: File, message: string) => { | |||||
| toast(message, { | |||||
| description: `"${file.name.length > 20 ? `${file.name.slice(0, 20)}...` : file.name}" has been rejected`, | |||||
| }); | |||||
| }, []); | |||||
| return ( | |||||
| <FileUpload | |||||
| value={files} | |||||
| onValueChange={setFiles} | |||||
| onUpload={onUpload} | |||||
| onFileReject={onFileReject} | |||||
| maxFiles={1} | |||||
| className="w-full max-w-md" | |||||
| multiple={false} | |||||
| > | |||||
| <FileUploadDropzone> | |||||
| <div className="flex flex-col items-center gap-1 text-center"> | |||||
| <div className="flex items-center justify-center rounded-full border p-2.5"> | |||||
| <Upload className="size-6 text-muted-foreground" /> | |||||
| </div> | |||||
| <p className="font-medium text-sm">Drag & drop files here</p> | |||||
| <p className="text-muted-foreground text-xs"> | |||||
| Or click to browse (max 2 files) | |||||
| </p> | |||||
| </div> | |||||
| <FileUploadTrigger asChild> | |||||
| <Button variant="outline" size="sm" className="mt-2 w-fit"> | |||||
| Browse files | |||||
| </Button> | |||||
| </FileUploadTrigger> | |||||
| </FileUploadDropzone> | |||||
| <FileUploadList> | |||||
| {files.map((file, index) => ( | |||||
| <FileUploadItem key={index} value={file} className="flex-col"> | |||||
| <div className="flex w-full items-center gap-2"> | |||||
| <FileUploadItemPreview /> | |||||
| <FileUploadItemMetadata /> | |||||
| <FileUploadItemDelete asChild> | |||||
| <Button variant="ghost" size="icon" className="size-7"> | |||||
| <X /> | |||||
| </Button> | |||||
| </FileUploadItemDelete> | |||||
| </div> | |||||
| <FileUploadItemProgress /> | |||||
| </FileUploadItem> | |||||
| ))} | |||||
| </FileUploadList> | |||||
| </FileUpload> | |||||
| ); | |||||
| } |
| (params as IAgentForm).tools = (params as IAgentForm).tools.concat( | (params as IAgentForm).tools = (params as IAgentForm).tools.concat( | ||||
| bottomSubAgentEdges.map((x) => { | bottomSubAgentEdges.map((x) => { | ||||
| const formData = buildAgentTools(edges, nodes, x.target); | |||||
| return { component_name: Operator.Agent, params: { ...formData } }; | |||||
| const { | |||||
| params: formData, | |||||
| id, | |||||
| name, | |||||
| } = buildAgentTools(edges, nodes, x.target); | |||||
| return { | |||||
| component_name: Operator.Agent, | |||||
| id, | |||||
| name, | |||||
| params: { ...formData }, | |||||
| }; | |||||
| }), | }), | ||||
| ); | ); | ||||
| } | } | ||||
| return params; | |||||
| return { params, name: node?.data.name, id: node?.id }; | |||||
| } | } | ||||
| function filterTargetsBySourceHandleId(edges: Edge[], handleId: string) { | function filterTargetsBySourceHandleId(edges: Edge[], handleId: string) { | ||||
| let params = x?.data.form ?? {}; | let params = x?.data.form ?? {}; | ||||
| switch (operatorName) { | switch (operatorName) { | ||||
| case Operator.Agent: | |||||
| params = buildAgentTools(edges, nodes, id); | |||||
| case Operator.Agent: { | |||||
| const { params: formData } = buildAgentTools(edges, nodes, id); | |||||
| params = formData; | |||||
| break; | break; | ||||
| } | |||||
| case Operator.Categorize: | case Operator.Categorize: | ||||
| params = buildCategorizeTos(edges, nodes, id); | params = buildCategorizeTos(edges, nodes, id); | ||||
| break; | break; |
| import { MessageType } from '@/constants/chat'; | |||||
| import { IReference } from '@/interfaces/database/chat'; | |||||
| import { IMessage } from '@/pages/chat/interface'; | |||||
| import { isEmpty } from 'lodash'; | |||||
| export const buildAgentMessageItemReference = ( | |||||
| conversation: { message: IMessage[]; reference: IReference[] }, | |||||
| message: IMessage, | |||||
| ) => { | |||||
| const assistantMessages = conversation.message?.filter( | |||||
| (x) => x.role === MessageType.Assistant, | |||||
| ); | |||||
| const referenceIndex = assistantMessages.findIndex( | |||||
| (x) => x.id === message.id, | |||||
| ); | |||||
| const reference = !isEmpty(message?.reference) | |||||
| ? message?.reference | |||||
| : (conversation?.reference ?? [])[referenceIndex]; | |||||
| return reference ?? { doc_aggs: [], chunks: [], total: 0 }; | |||||
| }; |
| debug, | debug, | ||||
| listCanvasTeam, | listCanvasTeam, | ||||
| settingCanvas, | settingCanvas, | ||||
| uploadCanvasFile, | |||||
| } = api; | } = api; | ||||
| const methods = { | const methods = { | ||||
| url: settingCanvas, | url: settingCanvas, | ||||
| method: 'post', | method: 'post', | ||||
| }, | }, | ||||
| uploadCanvasFile: { | |||||
| url: uploadCanvasFile, | |||||
| method: 'post', | |||||
| }, | |||||
| } as const; | } as const; | ||||
| const flowService = registerServer<keyof typeof methods>(methods, request); | const flowService = registerServer<keyof typeof methods>(methods, request); |
| testDbConnect: `${api_host}/canvas/test_db_connect`, | testDbConnect: `${api_host}/canvas/test_db_connect`, | ||||
| getInputElements: `${api_host}/canvas/input_elements`, | getInputElements: `${api_host}/canvas/input_elements`, | ||||
| debug: `${api_host}/canvas/debug`, | debug: `${api_host}/canvas/debug`, | ||||
| uploadCanvasFile: `${api_host}/canvas/upload`, | |||||
| // mcp server | // mcp server | ||||
| getMcpServerList: `${api_host}/mcp_server/list`, | getMcpServerList: `${api_host}/mcp_server/list`, |