Procházet zdrojové kódy

Feat: Support uploading files when running agent #3221 (#8697)

### 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
balibabu před 3 měsíci
rodič
revize
4a9708889e
Žádný účet není propojen s e-mailovou adresou tvůrce revize

+ 1434
- 0
web/src/components/file-upload.tsx
Diff nebyl zobrazen, protože je příliš veliký
Zobrazit soubor


+ 32
- 0
web/src/hooks/use-agent-request.ts Zobrazit soubor

@@ -26,6 +26,7 @@ export const enum AgentApiAction {
ResetAgent = 'resetAgent',
SetAgent = 'setAgent',
FetchAgentTemplates = 'fetchAgentTemplates',
UploadCanvasFile = 'uploadCanvasFile',
}

export const EmptyDsl = {
@@ -268,3 +269,34 @@ export const useSetAgent = () => {

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 };
};

+ 2
- 2
web/src/pages/agent/chat/box.tsx Zobrazit soubor

@@ -1,6 +1,5 @@
import { MessageType } from '@/constants/chat';
import { useGetFileIcon } from '@/pages/chat/hooks';
import { buildMessageItemReference } from '@/pages/chat/utils';
import { Spin } from 'antd';

import { useSendNextMessage } from './hooks';
@@ -19,6 +18,7 @@ import { useParams } from 'umi';
import DebugContent from '../debug-content';
import { BeginQuery } from '../interface';
import { buildBeginQueryWithObject } from '../utils';
import { buildAgentMessageItemReference } from '../utils/chat';

const AgentChatBox = () => {
const {
@@ -88,7 +88,7 @@ const AgentChatBox = () => {
avatar={userInfo.avatar}
avatarDialog={canvasInfo.avatar}
item={message}
reference={buildMessageItemReference(
reference={buildAgentMessageItemReference(
{ message: derivedMessages, reference },
message,
)}

+ 7
- 20
web/src/pages/agent/debug-content/index.tsx Zobrazit soubor

@@ -1,4 +1,3 @@
import { FileUploader } from '@/components/file-uploader';
import { ButtonLoading } from '@/components/ui/button';
import {
Form,
@@ -19,6 +18,7 @@ import { useTranslation } from 'react-i18next';
import { z } from 'zod';
import { BeginQueryType } from '../constant';
import { BeginQuery } from '../interface';
import { FileUploadDirectUpload } from './uploader';

export const BeginQueryComponentMap = {
[BeginQueryType.Line]: 'string',
@@ -71,7 +71,7 @@ const DebugContent = ({
} else if (type === BeginQueryType.Integer) {
fieldSchema = z.coerce.number();
} else {
fieldSchema = z.instanceof(File);
fieldSchema = z.record(z.any());
}

if (cur.optional) {
@@ -165,18 +165,16 @@ const DebugContent = ({
<React.Fragment key={idx}>
<FormField
control={form.control}
name={'file'}
name={props.name}
render={({ field }) => (
<div className="space-y-6">
<FormItem className="w-full">
<FormLabel>{t('assistantAvatar')}</FormLabel>
<FormControl>
<FileUploader
<FileUploadDirectUpload
value={field.value}
onValueChange={field.onChange}
maxFileCount={1}
maxSize={4 * 1024 * 1024}
/>
onChange={field.onChange}
></FileUploadDirectUpload>
</FormControl>
<FormMessage />
</FormItem>
@@ -232,18 +230,7 @@ const DebugContent = ({
(values: z.infer<typeof formSchemaValues.schema>) => {
const nextValues = Object.entries(values).map(([key, value]) => {
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);

+ 116
- 0
web/src/pages/agent/debug-content/uploader.tsx Zobrazit soubor

@@ -0,0 +1,116 @@
'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>
);
}

+ 17
- 6
web/src/pages/agent/utils.ts Zobrazit soubor

@@ -150,13 +150,22 @@ function buildAgentTools(edges: Edge[], nodes: Node[], nodeId: string) {

(params as IAgentForm).tools = (params as IAgentForm).tools.concat(
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) {
@@ -221,9 +230,11 @@ export const buildDslComponentsByGraph = (
let params = x?.data.form ?? {};

switch (operatorName) {
case Operator.Agent:
params = buildAgentTools(edges, nodes, id);
case Operator.Agent: {
const { params: formData } = buildAgentTools(edges, nodes, id);
params = formData;
break;
}
case Operator.Categorize:
params = buildCategorizeTos(edges, nodes, id);
break;

+ 21
- 0
web/src/pages/agent/utils/chat.ts Zobrazit soubor

@@ -0,0 +1,21 @@
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 };
};

+ 5
- 0
web/src/services/flow-service.ts Zobrazit soubor

@@ -18,6 +18,7 @@ const {
debug,
listCanvasTeam,
settingCanvas,
uploadCanvasFile,
} = api;

const methods = {
@@ -81,6 +82,10 @@ const methods = {
url: settingCanvas,
method: 'post',
},
uploadCanvasFile: {
url: uploadCanvasFile,
method: 'post',
},
} as const;

const flowService = registerServer<keyof typeof methods>(methods, request);

+ 1
- 0
web/src/utils/api.ts Zobrazit soubor

@@ -143,6 +143,7 @@ export default {
testDbConnect: `${api_host}/canvas/test_db_connect`,
getInputElements: `${api_host}/canvas/input_elements`,
debug: `${api_host}/canvas/debug`,
uploadCanvasFile: `${api_host}/canvas/upload`,

// mcp server
getMcpServerList: `${api_host}/mcp_server/list`,

Načítá se…
Zrušit
Uložit