mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-05 03:18:51 +00:00
### 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)
This commit is contained in:
parent
9580e99650
commit
4a9708889e
1434
web/src/components/file-upload.tsx
Normal file
1434
web/src/components/file-upload.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@ -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 };
|
||||
};
|
||||
|
||||
@ -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,
|
||||
)}
|
||||
|
||||
@ -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
web/src/pages/agent/debug-content/uploader.tsx
Normal file
116
web/src/pages/agent/debug-content/uploader.tsx
Normal file
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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);
|
||||
const {
|
||||
params: formData,
|
||||
id,
|
||||
name,
|
||||
} = buildAgentTools(edges, nodes, x.target);
|
||||
|
||||
return { component_name: Operator.Agent, params: { ...formData } };
|
||||
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
web/src/pages/agent/utils/chat.ts
Normal file
21
web/src/pages/agent/utils/chat.ts
Normal file
@ -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 };
|
||||
};
|
||||
@ -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);
|
||||
|
||||
@ -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`,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user