mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-06 03:47:41 +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',
|
ResetAgent = 'resetAgent',
|
||||||
SetAgent = 'setAgent',
|
SetAgent = 'setAgent',
|
||||||
FetchAgentTemplates = 'fetchAgentTemplates',
|
FetchAgentTemplates = 'fetchAgentTemplates',
|
||||||
|
UploadCanvasFile = 'uploadCanvasFile',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EmptyDsl = {
|
export const EmptyDsl = {
|
||||||
@ -268,3 +269,34 @@ export const useSetAgent = () => {
|
|||||||
|
|
||||||
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 };
|
||||||
|
};
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
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';
|
||||||
@ -19,6 +18,7 @@ import { useParams } from 'umi';
|
|||||||
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 {
|
||||||
@ -88,7 +88,7 @@ const AgentChatBox = () => {
|
|||||||
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,
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { FileUploader } from '@/components/file-uploader';
|
|
||||||
import { ButtonLoading } from '@/components/ui/button';
|
import { ButtonLoading } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
@ -19,6 +18,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
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',
|
||||||
@ -71,7 +71,7 @@ const DebugContent = ({
|
|||||||
} 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) {
|
||||||
@ -165,18 +165,16 @@ const DebugContent = ({
|
|||||||
<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}
|
onChange={field.onChange}
|
||||||
maxFileCount={1}
|
></FileUploadDirectUpload>
|
||||||
maxSize={4 * 1024 * 1024}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@ -232,18 +230,7 @@ const DebugContent = ({
|
|||||||
(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;
|
return { ...item, 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 };
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ok(nextValues);
|
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(
|
(params as IAgentForm).tools = (params as IAgentForm).tools.concat(
|
||||||
bottomSubAgentEdges.map((x) => {
|
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) {
|
function filterTargetsBySourceHandleId(edges: Edge[], handleId: string) {
|
||||||
@ -221,9 +230,11 @@ export const buildDslComponentsByGraph = (
|
|||||||
let params = x?.data.form ?? {};
|
let params = x?.data.form ?? {};
|
||||||
|
|
||||||
switch (operatorName) {
|
switch (operatorName) {
|
||||||
case Operator.Agent:
|
case Operator.Agent: {
|
||||||
params = buildAgentTools(edges, nodes, id);
|
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;
|
||||||
|
|||||||
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,
|
debug,
|
||||||
listCanvasTeam,
|
listCanvasTeam,
|
||||||
settingCanvas,
|
settingCanvas,
|
||||||
|
uploadCanvasFile,
|
||||||
} = api;
|
} = api;
|
||||||
|
|
||||||
const methods = {
|
const methods = {
|
||||||
@ -81,6 +82,10 @@ 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);
|
||||||
|
|||||||
@ -143,6 +143,7 @@ export default {
|
|||||||
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`,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user