From e36d9d794fb55f972c2d2782f33cbf95defc64a1 Mon Sep 17 00:00:00 2001 From: Archer <545436317@qq.com> Date: Tue, 6 Aug 2024 10:00:22 +0800 Subject: [PATCH] File input (#2270) * doc * feat: file upload config * perf: chat box file params * feat: markdown show file * feat: chat file store and clear * perf: read file contentType * feat: llm vision config * feat: file url output * perf: plugin error text * perf: image load * feat: ai chat document * perf: file block ui * feat: read file node * feat: file read response field * feat: simple mode support read files * feat: tool call * feat: read file histories * perf: select file * perf: select file config * i18n * i18n * fix: ts; feat: tool response preview result --- .vscode/settings.json | 1 + .../zh-cn/docs/development/upgrading/489.md | 45 ++ packages/global/common/file/constants.ts | 12 +- packages/global/common/file/type.d.ts | 1 + packages/global/common/string/tools.ts | 7 + packages/global/core/ai/prompt/AIChat.ts | 7 + packages/global/core/ai/type.d.ts | 40 +- packages/global/core/app/constants.ts | 8 +- packages/global/core/app/type.d.ts | 10 +- packages/global/core/chat/adapt.ts | 33 +- packages/global/core/chat/type.d.ts | 1 + packages/global/core/chat/utils.ts | 56 +- packages/global/core/workflow/constants.ts | 10 +- .../global/core/workflow/node/constant.ts | 3 +- .../global/core/workflow/runtime/type.d.ts | 9 + .../core/workflow/template/constants.ts | 2 + .../global/core/workflow/template/input.ts | 9 + .../core/workflow/template/system/aiChat.ts | 30 +- .../workflow/template/system/datasetSearch.ts | 6 +- .../template/system/readFiles/index.tsx | 48 ++ .../template/system/readFiles/type.d.ts | 4 + .../workflow/template/system/systemConfig.ts | 5 +- .../core/workflow/template/system/tools.ts | 23 +- .../workflow/template/system/workflowStart.ts | 15 +- packages/global/core/workflow/utils.ts | 2 + .../plugins/src/duckduckgo/search/index.ts | 3 +- .../plugins/src/duckduckgo/searchImg/index.ts | 3 +- .../src/duckduckgo/searchNews/index.ts | 3 +- .../src/duckduckgo/searchVideo/index.ts | 3 +- packages/plugins/src/template/template.json | 2 +- .../service/common/buffer/rawText/schema.ts | 4 +- .../service/common/file/gridfs/controller.ts | 16 +- packages/service/common/file/gridfs/schema.ts | 14 +- packages/service/common/file/read/utils.ts | 44 +- packages/service/common/file/utils.ts | 17 +- .../core/ai/functions/createQuestionGuide.ts | 6 +- packages/service/core/ai/utils.ts | 39 + packages/service/core/app/schema.ts | 3 +- packages/service/core/chat/controller.ts | 40 + packages/service/core/chat/utils.ts | 226 ++++-- .../service/core/dataset/search/controller.ts | 2 +- .../dispatch/agent/classifyQuestion.ts | 7 +- .../core/workflow/dispatch/agent/extract.ts | 14 +- .../dispatch/agent/runTool/constants.ts | 17 + .../dispatch/agent/runTool/functionCall.ts | 84 +- .../workflow/dispatch/agent/runTool/index.ts | 89 ++- .../dispatch/agent/runTool/promptCall.ts | 70 +- .../dispatch/agent/runTool/toolChoice.ts | 96 ++- .../workflow/dispatch/agent/runTool/type.d.ts | 6 +- .../workflow/dispatch/agent/runTool/utils.ts | 22 + .../core/workflow/dispatch/chat/oneapi.ts | 173 ++-- .../service/core/workflow/dispatch/index.ts | 2 + .../workflow/dispatch/init/workflowStart.tsx | 18 +- .../core/workflow/dispatch/tools/readFiles.ts | 196 +++++ .../core/workflow/dispatchV1/chat/oneapi.ts | 24 +- .../service/support/permission/controller.ts | 6 +- .../web/components/common/Icon/constants.ts | 1 + .../Icon/icons/core/app/simpleMode/file.svg | 3 + .../core/workflow/NodeInputSelect.tsx | 10 +- packages/web/hooks/useWidthVariable.ts | 22 + packages/web/i18n/en/app.json | 21 +- packages/web/i18n/en/chat.json | 15 +- packages/web/i18n/en/common.json | 5 +- packages/web/i18n/en/file.json | 2 + packages/web/i18n/en/workflow.json | 14 +- packages/web/i18n/zh/app.json | 23 +- packages/web/i18n/zh/chat.json | 5 +- packages/web/i18n/zh/common.json | 8 +- packages/web/i18n/zh/file.json | 8 +- packages/web/i18n/zh/workflow.json | 14 +- .../public/imgs/app/fileUploadPlaceholder.svg | 167 ++++ .../app/src/components/Markdown/img/Image.tsx | 5 +- .../Textarea/MyTextarea/VariableTip.tsx | 4 +- .../core/ai/AISettingModal/index.tsx | 58 +- .../core/ai/SettingLLMModel/index.tsx | 16 +- .../src/components/core/app/FileSelect.tsx | 147 ++++ projects/app/src/components/core/app/Tip.tsx | 9 +- .../ChatContainer/ChatBox/Input/ChatInput.tsx | 218 +++-- .../chat/ChatContainer/ChatBox/Provider.tsx | 7 +- .../ChatBox/components/ChatItem.tsx | 7 +- .../ChatBox/components/FilesBox.tsx | 93 ++- .../core/chat/ChatContainer/ChatBox/index.tsx | 8 +- .../core/chat/ChatContainer/ChatBox/type.d.ts | 1 + .../core/chat/ChatContainer/ChatBox/utils.ts | 8 +- .../core/chat/components/AIResponseBox.tsx | 16 +- .../chat/components/WholeResponseModal.tsx | 52 +- .../app/src/global/core/workflow/api.d.ts | 1 + .../app/src/pages/api/common/file/read.ts | 2 +- .../app/src/pages/api/common/file/upload.ts | 28 +- projects/app/src/pages/api/core/app/del.ts | 2 + .../app/src/pages/api/core/chat/chatTest.ts | 14 +- .../src/pages/api/core/chat/clearHistories.ts | 108 +-- .../app/src/pages/api/core/chat/delHistory.ts | 6 +- .../app/src/pages/api/core/workflow/debug.ts | 2 + .../app/src/pages/api/v1/chat/completions.ts | 2 + .../detail/components/SimpleApp/ChatTest.tsx | 5 +- .../detail/components/SimpleApp/EditForm.tsx | 26 +- .../detail/components/SimpleApp/Header.tsx | 19 +- .../components/WorkflowComponents/AppCard.tsx | 8 +- .../WorkflowComponents/Flow/index.tsx | 1 + .../Flow/nodes/NodePluginIO/PluginOutput.tsx | 2 +- .../Flow/nodes/NodeSystemConfig.tsx | 34 +- .../Flow/nodes/NodeWorkflowStart.tsx | 28 +- .../Flow/nodes/render/RenderInput/Label.tsx | 2 + .../RenderInput/templates/SettingLLMModel.tsx | 7 +- .../Flow/nodes/render/RenderOutput/Label.tsx | 2 +- .../components/WorkflowComponents/context.tsx | 2 +- .../app/detail/components/useChatTest.tsx | 4 +- projects/app/src/pages/chat/index.tsx | 3 +- projects/app/src/pages/chat/share.tsx | 3 +- projects/app/src/pages/chat/team.tsx | 3 +- .../Import/components/FileSelector.tsx | 4 +- .../login/components/LoginForm/LoginForm.tsx | 2 +- projects/app/src/pages/login/index.tsx | 2 +- .../app/src/service/common/system/cron.ts | 10 +- .../app/src/service/common/system/cronTask.ts | 40 +- projects/app/src/service/core/app/utils.ts | 2 + projects/app/src/service/events/generateQA.ts | 3 +- projects/app/src/web/common/file/api.ts | 5 +- projects/app/src/web/core/app/utils.ts | 745 +++++++----------- projects/app/src/web/core/workflow/utils.ts | 6 +- 121 files changed, 2597 insertions(+), 1139 deletions(-) create mode 100644 docSite/content/zh-cn/docs/development/upgrading/489.md create mode 100644 packages/global/core/workflow/template/system/readFiles/index.tsx create mode 100644 packages/global/core/workflow/template/system/readFiles/type.d.ts create mode 100644 packages/service/core/ai/utils.ts create mode 100644 packages/service/core/workflow/dispatch/tools/readFiles.ts create mode 100644 packages/web/components/common/Icon/icons/core/app/simpleMode/file.svg create mode 100644 packages/web/hooks/useWidthVariable.ts create mode 100644 projects/app/public/imgs/app/fileUploadPlaceholder.svg create mode 100644 projects/app/src/components/core/app/FileSelect.tsx diff --git a/.vscode/settings.json b/.vscode/settings.json index 7f3bde5bc90c..30e2803d7fb9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -21,6 +21,7 @@ "i18n-ally.namespace": true, "i18n-ally.pathMatcher": "{locale}/{namespaces}.json", "i18n-ally.extract.targetPickingStrategy": "most-similar-by-key", + "i18n-ally.translate.engines": ["deepl", "google"], "[typescript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" } diff --git a/docSite/content/zh-cn/docs/development/upgrading/489.md b/docSite/content/zh-cn/docs/development/upgrading/489.md new file mode 100644 index 000000000000..d4d6286ad2a5 --- /dev/null +++ b/docSite/content/zh-cn/docs/development/upgrading/489.md @@ -0,0 +1,45 @@ +--- +title: 'V4.8.9(进行中)' +description: 'FastGPT V4.8.9 更新说明' +icon: 'upgrade' +draft: false +toc: true +weight: 816 +--- + +## 升级指南 + +### 1. 做好数据库备份 + +### 2. 修改镜像 + + +### 3. 执行初始化 + +从任意终端,发起 1 个 HTTP 请求。其中 {{rootkey}} 替换成环境变量里的 `rootkey`;{{host}} 替换成**FastGPT 商业版域名**。 + +```bash +curl --location --request POST 'https://{{host}}/api/admin/init/489' \ +--header 'rootkey: {{rootkey}}' \ +--header 'Content-Type: application/json' +``` + +会初始化多租户的通知方式 + +------- + +## V4.8.9 更新说明 + +1. 新增 - 文件上传配置,不再依赖视觉模型决定是否可上传图片,而是通过系统配置决定。 +2. 新增 - AI 对话节点和工具调用支持选择“是否开启图片识别”,开启后会自动获取对话框上传的图片和“用户问题”中的图片链接。 +3. 新增 - 文档解析节点。 +4. 商业版新增 - 团队通知账号绑定,用于接收重要信息。 +5. 商业版新增 - 知识库集合标签功能,可以对知识库进行标签管理。 +6. 商业版新增 - 知识库搜索节点支持标签过滤和创建时间过滤。 +7. 新增 - 删除所有对话引导内容。 +8. 优化 - 对话框信息懒加载,减少网络传输。 +9. 修复 - 知识库上传文件,网络不稳定或文件较多情况下,进度无法到 100%。 +10. 修复 - 删除应用后回到聊天选择最后一次对话的应用为删除的应用时提示无该应用问题。 +11. 修复 - 插件动态变量配置默认值时,无法正常显示默认值。 +12. 修复 - 工具调用温度和最大回复值未生效。 +13. 修复 - 函数调用模式,assistant role 中,GPT 模型必须传入 content 参数。(不影响大部分模型,目前基本都改用用 ToolChoice 模式,FC 模式已弃用) diff --git a/packages/global/common/file/constants.ts b/packages/global/common/file/constants.ts index d65d14fd8422..e1f7eac1d77e 100644 --- a/packages/global/common/file/constants.ts +++ b/packages/global/common/file/constants.ts @@ -1,11 +1,19 @@ +import { i18nT } from '../../../web/i18n/utils'; + /* mongo fs bucket */ export enum BucketNameEnum { - dataset = 'dataset' + dataset = 'dataset', + chat = 'chat' } export const bucketNameMap = { [BucketNameEnum.dataset]: { - label: 'file.bucket.dataset' + label: i18nT('file:bucket_file') + }, + [BucketNameEnum.chat]: { + label: i18nT('file:bucket_chat') } }; export const ReadFileBaseUrl = '/api/common/file/read'; + +export const documentFileType = '.txt, .docx, .csv, .xlsx, .pdf, .md, .html, .pptx'; diff --git a/packages/global/common/file/type.d.ts b/packages/global/common/file/type.d.ts index dfe8b21c6dfe..56fe0876e541 100644 --- a/packages/global/common/file/type.d.ts +++ b/packages/global/common/file/type.d.ts @@ -5,4 +5,5 @@ export type FileTokenQuery = { teamId: string; tmbId: string; fileId: string; + expiredTime?: number; }; diff --git a/packages/global/common/string/tools.ts b/packages/global/common/string/tools.ts index 8afa1f8eaf02..cee5c800da45 100644 --- a/packages/global/common/string/tools.ts +++ b/packages/global/common/string/tools.ts @@ -91,3 +91,10 @@ export const sliceJsonStr = (str: string) => { return jsonStr; }; + +export const sliceStrStartEnd = (str: string, start: number, end: number) => { + const overSize = str.length > start + end; + const startContent = str.slice(0, start); + const endContent = overSize ? str.slice(-end) : ''; + return startContent + (overSize ? ` ...... ` : '') + endContent; +}; diff --git a/packages/global/core/ai/prompt/AIChat.ts b/packages/global/core/ai/prompt/AIChat.ts index 1c55c371310e..656aa1c1442d 100644 --- a/packages/global/core/ai/prompt/AIChat.ts +++ b/packages/global/core/ai/prompt/AIChat.ts @@ -119,3 +119,10 @@ export const Prompt_QuotePromptList: PromptTemplateItem[] = [ 问题:"""{{question}}"""` } ]; + +// Document quote prompt +export const Prompt_DocumentQuote = `将 中的内容作为你的知识: + +{{quote}} + +`; diff --git a/packages/global/core/ai/type.d.ts b/packages/global/core/ai/type.d.ts index 6fa1051f99ba..52f167cddd16 100644 --- a/packages/global/core/ai/type.d.ts +++ b/packages/global/core/ai/type.d.ts @@ -2,23 +2,46 @@ import openai from 'openai'; import type { ChatCompletionMessageToolCall, ChatCompletionChunk, - ChatCompletionMessageParam, + ChatCompletionMessageParam as SdkChatCompletionMessageParam, ChatCompletionToolMessageParam, - ChatCompletionAssistantMessageParam + ChatCompletionAssistantMessageParam, + ChatCompletionContentPart as SdkChatCompletionContentPart, + ChatCompletionUserMessageParam as SdkChatCompletionUserMessageParam } from 'openai/resources'; import { ChatMessageTypeEnum } from './constants'; export * from 'openai/resources'; -export type ChatCompletionMessageParam = ChatCompletionMessageParam & { +// Extension of ChatCompletionMessageParam, Add file url type +export type ChatCompletionContentPartFile = { + type: 'file_url'; + name: string; + url: string; +}; +// Rewrite ChatCompletionContentPart, Add file type +export type ChatCompletionContentPart = + | SdkChatCompletionContentPart + | ChatCompletionContentPartFile; +type CustomChatCompletionUserMessageParam = { + content: string | Array; + role: 'user'; + name?: string; +}; + +export type ChatCompletionMessageParam = ( + | Exclude + | CustomChatCompletionUserMessageParam +) & { dataId?: string; }; +export type SdkChatCompletionMessageParam = SdkChatCompletionMessageParam; + +/* ToolChoice and functionCall extension */ export type ChatCompletionToolMessageParam = ChatCompletionToolMessageParam & { name: string }; export type ChatCompletionAssistantToolParam = { role: 'assistant'; tool_calls: ChatCompletionMessageToolCall[]; }; - export type ChatCompletionMessageToolCall = ChatCompletionMessageToolCall & { toolName?: string; toolAvatar?: string; @@ -28,13 +51,16 @@ export type ChatCompletionMessageFunctionCall = ChatCompletionAssistantMessagePa toolName?: string; toolAvatar?: string; }; + +// Stream response export type StreamChatType = Stream; +export default openai; +export * from 'openai'; + +// Other export type PromptTemplateItem = { title: string; desc: string; value: string; }; - -export default openai; -export * from 'openai'; diff --git a/packages/global/core/app/constants.ts b/packages/global/core/app/constants.ts index 73c1fa55ca0b..232206a6c662 100644 --- a/packages/global/core/app/constants.ts +++ b/packages/global/core/app/constants.ts @@ -1,4 +1,4 @@ -import { AppTTSConfigType, AppWhisperConfigType } from './type'; +import { AppTTSConfigType, AppFileSelectConfigType, AppWhisperConfigType } from './type'; export enum AppTypeEnum { folder = 'folder', @@ -23,3 +23,9 @@ export const defaultChatInputGuideConfig = { textList: [], customUrl: '' }; + +export const defaultAppSelectFileConfig: AppFileSelectConfigType = { + canSelectFile: false, + canSelectImg: false, + maxFiles: 10 +}; diff --git a/packages/global/core/app/type.d.ts b/packages/global/core/app/type.d.ts index 8080679f9fe0..51093c9f6bbc 100644 --- a/packages/global/core/app/type.d.ts +++ b/packages/global/core/app/type.d.ts @@ -1,7 +1,7 @@ import type { FlowNodeTemplateType, StoreNodeItemType } from '../workflow/type/node'; import { AppTypeEnum } from './constants'; import { PermissionTypeEnum } from '../../support/permission/constant'; -import { VariableInputEnum } from '../workflow/constants'; +import { NodeInputKeyEnum, VariableInputEnum } from '../workflow/constants'; import { SelectedDatasetType } from '../workflow/api'; import { DatasetSearchModeEnum } from '../dataset/constants'; import { TeamTagSchema as TeamTagsSchemaType } from '@fastgpt/global/support/user/team/type.d'; @@ -91,6 +91,7 @@ export type AppChatConfigType = { whisperConfig?: AppWhisperConfigType; scheduledTriggerConfig?: AppScheduledTriggerConfigType; chatInputGuide?: ChatInputGuideConfigType; + fileSelectConfig?: AppFileSelectConfigType; }; export type SettingAIDataType = { model: string; @@ -98,6 +99,7 @@ export type SettingAIDataType = { maxToken: number; isResponseAnswerText?: boolean; maxHistories?: number; + [NodeInputKeyEnum.aiChatVision]?: boolean; // Is open vision mode }; // variable @@ -134,3 +136,9 @@ export type AppScheduledTriggerConfigType = { timezone: string; defaultPrompt: string; }; +// File +export type AppFileSelectConfigType = { + canSelectFile: boolean; + canSelectImg: boolean; + maxFiles: number; +}; diff --git a/packages/global/core/chat/adapt.ts b/packages/global/core/chat/adapt.ts index a685e816f5c5..c88650bcfcfc 100644 --- a/packages/global/core/chat/adapt.ts +++ b/packages/global/core/chat/adapt.ts @@ -56,16 +56,21 @@ export const chats2GPTMessages = ({ text: item.text?.content || '' }; } - if ( - item.type === ChatItemValueTypeEnum.file && - item.file?.type === ChatFileTypeEnum.image - ) { - return { - type: 'image_url', - image_url: { + if (item.type === ChatItemValueTypeEnum.file) { + if (item.file?.type === ChatFileTypeEnum.image) { + return { + type: 'image_url', + image_url: { + url: item.file?.url || '' + } + }; + } else if (item.file?.type === ChatFileTypeEnum.file) { + return { + type: 'file_url', + name: item.file?.name || '', url: item.file?.url || '' - } - }; + }; + } } }) .filter(Boolean) as ChatCompletionContentPart[]; @@ -175,6 +180,16 @@ export const GPTMessages2Chats = ( url: item.image_url.url } }); + } else if (item.type === 'file_url') { + value.push({ + // @ts-ignore + type: ChatItemValueTypeEnum.file, + file: { + type: ChatFileTypeEnum.file, + name: item.name, + url: item.url + } + }); } }); } diff --git a/packages/global/core/chat/type.d.ts b/packages/global/core/chat/type.d.ts index 570486a6a859..4c1545b8ff3d 100644 --- a/packages/global/core/chat/type.d.ts +++ b/packages/global/core/chat/type.d.ts @@ -117,6 +117,7 @@ export type ChatItemType = (UserChatItemType | SystemChatItemType | AIChatItemTy dataId?: string; } & ResponseTagItemType; +// Frontend type export type ChatSiteItemType = (UserChatItemType | SystemChatItemType | AIChatItemType) & { dataId: string; status: `${ChatStatusEnum}`; diff --git a/packages/global/core/chat/utils.ts b/packages/global/core/chat/utils.ts index 8e59948613ea..0b0386fb2c57 100644 --- a/packages/global/core/chat/utils.ts +++ b/packages/global/core/chat/utils.ts @@ -2,6 +2,7 @@ import { DispatchNodeResponseType } from '../workflow/runtime/type'; import { FlowNodeTypeEnum } from '../workflow/node/constant'; import { ChatItemValueTypeEnum, ChatRoleEnum } from './constants'; import { ChatHistoryItemResType, ChatItemType, UserChatItemValueItemType } from './type.d'; +import { sliceStrStartEnd } from '../../common/string/tools'; // Concat 2 -> 1, and sort by role export const concatHistories = (histories1: ChatItemType[], histories2: ChatItemType[]) => { @@ -25,6 +26,7 @@ export const getChatTitleFromChatMessage = (message?: ChatItemType, defaultValue return defaultValue; }; +// Keep the first n and last n characters export const getHistoryPreview = ( completeMessages: ChatItemType[] ): { @@ -32,30 +34,44 @@ export const getHistoryPreview = ( value: string; }[] => { return completeMessages.map((item, i) => { - if (item.obj === ChatRoleEnum.System || i >= completeMessages.length - 2) { - return { - obj: item.obj, - value: item.value?.[0]?.text?.content || '' - }; - } + const n = item.obj === ChatRoleEnum.System || i >= completeMessages.length - 2 ? 80 : 40; - const content = item.value - .map((item) => { - if (item.text?.content) { - const content = - item.text.content.length > 20 - ? `${item.text.content.slice(0, 20)}...` - : item.text.content; - return content; - } - return ''; - }) - .filter(Boolean) - .join('\n'); + // Get message text content + const rawText = (() => { + if (item.obj === ChatRoleEnum.System) { + return item.value?.map((item) => item.text?.content).join('') || ''; + } else if (item.obj === ChatRoleEnum.Human) { + return ( + item.value + ?.map((item) => { + if (item?.text?.content) return item?.text?.content; + if (item.file?.type === 'image') return 'Input an image'; + return ''; + }) + .filter(Boolean) + .join('\n') || '' + ); + } else if (item.obj === ChatRoleEnum.AI) { + return ( + item.value + ?.map((item) => { + return ( + item.text?.content || item?.tools?.map((item) => item.toolName).join(',') || '' + ); + }) + .join('') || '' + ); + } + return ''; + })(); + + const startContent = rawText.slice(0, n); + const endContent = rawText.length > 2 * n ? rawText.slice(-n) : ''; + const content = startContent + (rawText.length > n ? ` ...... ` : '') + endContent; return { obj: item.obj, - value: content + value: sliceStrStartEnd(content, 80, 80) }; }); }; diff --git a/packages/global/core/workflow/constants.ts b/packages/global/core/workflow/constants.ts index c612a829e28c..4e4fc608aa50 100644 --- a/packages/global/core/workflow/constants.ts +++ b/packages/global/core/workflow/constants.ts @@ -75,6 +75,8 @@ export enum NodeInputKeyEnum { aiChatQuoteTemplate = 'quoteTemplate', aiChatQuotePrompt = 'quotePrompt', aiChatDatasetQuote = 'quoteQA', + aiChatVision = 'aiChatVision', + stringQuoteText = 'stringQuoteText', // dataset datasetSelectList = 'datasets', @@ -118,7 +120,10 @@ export enum NodeInputKeyEnum { // code code = 'code', - codeType = 'codeType' // js|py + codeType = 'codeType', // js|py + + // read files + fileUrlList = 'fileUrlList' } export enum NodeOutputKeyEnum { @@ -133,6 +138,9 @@ export enum NodeOutputKeyEnum { addOutputParam = 'system_addOutputParam', rawResponse = 'system_rawResponse', + // start + userFiles = 'userFiles', + // dataset datasetQuoteQA = 'quoteQA', diff --git a/packages/global/core/workflow/node/constant.ts b/packages/global/core/workflow/node/constant.ts index 4dc69dd60117..c373bac57625 100644 --- a/packages/global/core/workflow/node/constant.ts +++ b/packages/global/core/workflow/node/constant.ts @@ -117,7 +117,8 @@ export enum FlowNodeTypeEnum { variableUpdate = 'variableUpdate', code = 'code', textEditor = 'textEditor', - customFeedback = 'customFeedback' + customFeedback = 'customFeedback', + readFiles = 'readFiles' } // node IO value type diff --git a/packages/global/core/workflow/runtime/type.d.ts b/packages/global/core/workflow/runtime/type.d.ts index 6022bbb2bbca..38f4f91ab444 100644 --- a/packages/global/core/workflow/runtime/type.d.ts +++ b/packages/global/core/workflow/runtime/type.d.ts @@ -16,10 +16,12 @@ import { UserModelSchema } from '../../../support/user/type'; import { AppDetailType, AppSchema } from '../../app/type'; import { RuntimeNodeItemType } from '../runtime/type'; import { RuntimeEdgeItemType } from './edge'; +import { ReadFileNodeResponse } from '../template/system/readFiles/type'; /* workflow props */ export type ChatDispatchProps = { res?: NextApiResponse; + requestOrigin?: string; mode: 'test' | 'chat' | 'debug'; teamId: string; tmbId: string; @@ -30,6 +32,7 @@ export type ChatDispatchProps = { histories: ChatItemType[]; variables: Record; // global variable query: UserChatItemValueItemType[]; // trigger query + chatConfig: AppSchema['chatConfig']; stream: boolean; detail: boolean; // response detail maxRunTimes: number; @@ -146,6 +149,10 @@ export type DispatchNodeResponseType = { // plugin pluginOutput?: Record; + + // read files + readFilesResult?: string; + readFiles?: ReadFileNodeResponse; }; export type DispatchNodeResultType = { @@ -166,4 +173,6 @@ export type AIChatNodeProps = { [NodeInputKeyEnum.aiChatIsResponseText]: boolean; [NodeInputKeyEnum.aiChatQuoteTemplate]?: string; [NodeInputKeyEnum.aiChatQuotePrompt]?: string; + [NodeInputKeyEnum.aiChatVision]?: boolean; + [NodeInputKeyEnum.stringQuoteText]?: string; }; diff --git a/packages/global/core/workflow/template/constants.ts b/packages/global/core/workflow/template/constants.ts index e0d6232bd725..8a36355d7e30 100644 --- a/packages/global/core/workflow/template/constants.ts +++ b/packages/global/core/workflow/template/constants.ts @@ -25,6 +25,7 @@ import { VariableUpdateNode } from './system/variableUpdate'; import { CodeNode } from './system/sandbox'; import { TextEditorNode } from './system/textEditor'; import { CustomFeedbackNode } from './system/customFeedback'; +import { ReadFilesNodes } from './system/readFiles'; const systemNodes: FlowNodeTemplateType[] = [ AiChatModule, @@ -36,6 +37,7 @@ const systemNodes: FlowNodeTemplateType[] = [ StopToolNode, ClassifyQuestionModule, ContextExtractModule, + ReadFilesNodes, HttpNode468, AiQueryExtension, LafModule, diff --git a/packages/global/core/workflow/template/input.ts b/packages/global/core/workflow/template/input.ts index 695fec1e22d3..04afa15754ae 100644 --- a/packages/global/core/workflow/template/input.ts +++ b/packages/global/core/workflow/template/input.ts @@ -3,6 +3,7 @@ import { FlowNodeInputTypeEnum } from '../node/constant'; import { WorkflowIOValueTypeEnum } from '../constants'; import { chatNodeSystemPromptTip } from './tip'; import { FlowNodeInputItemType } from '../type/io'; +import { i18nT } from '../../../../web/i18n/utils'; export const Input_Template_History: FlowNodeInputItemType = { key: NodeInputKeyEnum.history, @@ -64,3 +65,11 @@ export const Input_Template_Dataset_Quote: FlowNodeInputItemType = { description: '', valueType: WorkflowIOValueTypeEnum.datasetQuote }; +export const Input_Template_Text_Quote: FlowNodeInputItemType = { + key: NodeInputKeyEnum.stringQuoteText, + renderTypeList: [FlowNodeInputTypeEnum.reference, FlowNodeInputTypeEnum.textarea], + label: i18nT('app:document_quote'), + debugLabel: i18nT('app:document_quote'), + description: i18nT('app:document_quote_tip'), + valueType: WorkflowIOValueTypeEnum.string +}; diff --git a/packages/global/core/workflow/template/system/aiChat.ts b/packages/global/core/workflow/template/system/aiChat.ts index e100e39fd06e..a514d537c231 100644 --- a/packages/global/core/workflow/template/system/aiChat.ts +++ b/packages/global/core/workflow/template/system/aiChat.ts @@ -15,10 +15,12 @@ import { Input_Template_Dataset_Quote, Input_Template_History, Input_Template_System_Prompt, - Input_Template_UserChatInput + Input_Template_UserChatInput, + Input_Template_Text_Quote } from '../input'; import { chatNodeSystemPromptTip } from '../tip'; import { getHandleConfig } from '../utils'; +import { i18nT } from '../../../../../web/i18n/utils'; export const AiChatModule: FlowNodeTemplateType = { id: FlowNodeTypeEnum.chatNode, @@ -27,8 +29,8 @@ export const AiChatModule: FlowNodeTemplateType = { sourceHandle: getHandleConfig(true, true, true, true), targetHandle: getHandleConfig(true, true, true, true), avatar: 'core/workflow/template/aiChat', - name: 'AI 对话', - intro: 'AI 大模型对话', + name: i18nT('workflow:template.ai_chat'), + intro: i18nT('workflow:template.ai_chat_intro'), showStatus: true, isTool: true, version: '481', @@ -40,20 +42,14 @@ export const AiChatModule: FlowNodeTemplateType = { renderTypeList: [FlowNodeInputTypeEnum.hidden], // Set in the pop-up window label: '', value: 0, - valueType: WorkflowIOValueTypeEnum.number, - min: 0, - max: 10, - step: 1 + valueType: WorkflowIOValueTypeEnum.number }, { key: NodeInputKeyEnum.aiChatMaxToken, renderTypeList: [FlowNodeInputTypeEnum.hidden], // Set in the pop-up window label: '', value: 2000, - valueType: WorkflowIOValueTypeEnum.number, - min: 100, - max: 4000, - step: 50 + valueType: WorkflowIOValueTypeEnum.number }, { key: NodeInputKeyEnum.aiChatIsResponseText, @@ -74,6 +70,13 @@ export const AiChatModule: FlowNodeTemplateType = { label: '', valueType: WorkflowIOValueTypeEnum.string }, + { + key: NodeInputKeyEnum.aiChatVision, + renderTypeList: [FlowNodeInputTypeEnum.hidden], + label: '', + valueType: WorkflowIOValueTypeEnum.boolean, + value: true + }, // settings modal --- { ...Input_Template_System_Prompt, @@ -82,8 +85,9 @@ export const AiChatModule: FlowNodeTemplateType = { placeholder: chatNodeSystemPromptTip }, Input_Template_History, - { ...Input_Template_UserChatInput, toolDescription: '用户问题' }, - Input_Template_Dataset_Quote + Input_Template_Dataset_Quote, + Input_Template_Text_Quote, + { ...Input_Template_UserChatInput, toolDescription: '用户问题' } ], outputs: [ { diff --git a/packages/global/core/workflow/template/system/datasetSearch.ts b/packages/global/core/workflow/template/system/datasetSearch.ts index 401c604f389d..7c5afe2ef1da 100644 --- a/packages/global/core/workflow/template/system/datasetSearch.ts +++ b/packages/global/core/workflow/template/system/datasetSearch.ts @@ -13,9 +13,9 @@ import { import { Input_Template_UserChatInput } from '../input'; import { DatasetSearchModeEnum } from '../../../dataset/constants'; import { getHandleConfig } from '../utils'; +import { i18nT } from '../../../../../web/i18n/utils'; -export const Dataset_SEARCH_DESC = - '调用“语义检索”和“全文检索”能力,从“知识库”中查找可能与问题相关的参考内容'; +export const Dataset_SEARCH_DESC = i18nT('workflow:template.dataset_search_intro'); export const DatasetSearchModule: FlowNodeTemplateType = { id: FlowNodeTypeEnum.datasetSearchNode, @@ -24,7 +24,7 @@ export const DatasetSearchModule: FlowNodeTemplateType = { sourceHandle: getHandleConfig(true, true, true, true), targetHandle: getHandleConfig(true, true, true, true), avatar: 'core/workflow/template/datasetSearch', - name: '知识库搜索', + name: i18nT('workflow:template.dataset_search'), intro: Dataset_SEARCH_DESC, showStatus: true, isTool: true, diff --git a/packages/global/core/workflow/template/system/readFiles/index.tsx b/packages/global/core/workflow/template/system/readFiles/index.tsx new file mode 100644 index 000000000000..eb2ea9d767d9 --- /dev/null +++ b/packages/global/core/workflow/template/system/readFiles/index.tsx @@ -0,0 +1,48 @@ +import { i18nT } from '../../../../../../web/i18n/utils'; +import { + FlowNodeTemplateTypeEnum, + NodeInputKeyEnum, + NodeOutputKeyEnum, + WorkflowIOValueTypeEnum +} from '../../../constants'; +import { + FlowNodeInputTypeEnum, + FlowNodeOutputTypeEnum, + FlowNodeTypeEnum +} from '../../../node/constant'; +import { FlowNodeTemplateType } from '../../../type/node'; +import { getHandleConfig } from '../../utils'; + +export const ReadFilesNodes: FlowNodeTemplateType = { + id: FlowNodeTypeEnum.readFiles, + templateType: FlowNodeTemplateTypeEnum.tools, + flowNodeType: FlowNodeTypeEnum.readFiles, + sourceHandle: getHandleConfig(true, true, true, true), + targetHandle: getHandleConfig(true, true, true, true), + avatar: 'core/app/simpleMode/file', + name: i18nT('app:workflow.read_files'), + intro: i18nT('app:workflow.read_files_tip'), + showStatus: true, + version: '489', + isTool: true, + inputs: [ + { + key: NodeInputKeyEnum.fileUrlList, + renderTypeList: [FlowNodeInputTypeEnum.reference], + valueType: WorkflowIOValueTypeEnum.arrayString, + label: i18nT('app:workflow.file_url'), + required: true, + value: [] + } + ], + outputs: [ + { + id: NodeOutputKeyEnum.text, + key: NodeOutputKeyEnum.text, + label: i18nT('app:workflow.read_files_result'), + description: i18nT('app:workflow.read_files_result_desc'), + valueType: WorkflowIOValueTypeEnum.string, + type: FlowNodeOutputTypeEnum.static + } + ] +}; diff --git a/packages/global/core/workflow/template/system/readFiles/type.d.ts b/packages/global/core/workflow/template/system/readFiles/type.d.ts new file mode 100644 index 000000000000..1ed36644e13a --- /dev/null +++ b/packages/global/core/workflow/template/system/readFiles/type.d.ts @@ -0,0 +1,4 @@ +export type ReadFileNodeResponse = { + url: string; + name: string; +}[]; diff --git a/packages/global/core/workflow/template/system/systemConfig.ts b/packages/global/core/workflow/template/system/systemConfig.ts index b037b8f8b1a2..4042425a79e5 100644 --- a/packages/global/core/workflow/template/system/systemConfig.ts +++ b/packages/global/core/workflow/template/system/systemConfig.ts @@ -2,6 +2,7 @@ import { FlowNodeTypeEnum } from '../../node/constant'; import { FlowNodeTemplateType } from '../../type/node.d'; import { FlowNodeTemplateTypeEnum } from '../../constants'; import { getHandleConfig } from '../utils'; +import { i18nT } from '../../../../../web/i18n/utils'; export const SystemConfigNode: FlowNodeTemplateType = { id: FlowNodeTypeEnum.systemConfig, @@ -10,8 +11,8 @@ export const SystemConfigNode: FlowNodeTemplateType = { sourceHandle: getHandleConfig(false, false, false, false), targetHandle: getHandleConfig(false, false, false, false), avatar: 'core/workflow/template/systemConfig', - name: '系统配置', - intro: '可以配置应用的系统参数。', + name: i18nT('workflow:template.system_config'), + intro: '', unique: true, forbidDelete: true, version: '481', diff --git a/packages/global/core/workflow/template/system/tools.ts b/packages/global/core/workflow/template/system/tools.ts index 5ff9f6a4e760..1f3a6ca3ad4d 100644 --- a/packages/global/core/workflow/template/system/tools.ts +++ b/packages/global/core/workflow/template/system/tools.ts @@ -19,6 +19,7 @@ import { import { chatNodeSystemPromptTip } from '../tip'; import { LLMModelTypeEnum } from '../../../ai/constants'; import { getHandleConfig } from '../utils'; +import { i18nT } from '../../../../../web/i18n/utils'; export const ToolModule: FlowNodeTemplateType = { id: FlowNodeTypeEnum.tools, @@ -27,8 +28,8 @@ export const ToolModule: FlowNodeTemplateType = { sourceHandle: getHandleConfig(true, true, false, true), targetHandle: getHandleConfig(true, true, false, true), avatar: 'core/workflow/template/toolCall', - name: '工具调用', - intro: '通过AI模型自动选择一个或多个功能块进行调用,也可以对插件进行调用。', + name: i18nT('workflow:template.tool_call'), + intro: i18nT('workflow:template.tool_call_intro'), showStatus: true, version: '481', inputs: [ @@ -41,21 +42,23 @@ export const ToolModule: FlowNodeTemplateType = { renderTypeList: [FlowNodeInputTypeEnum.hidden], // Set in the pop-up window label: '', value: 0, - valueType: WorkflowIOValueTypeEnum.number, - min: 0, - max: 10, - step: 1 + valueType: WorkflowIOValueTypeEnum.number }, { key: NodeInputKeyEnum.aiChatMaxToken, renderTypeList: [FlowNodeInputTypeEnum.hidden], // Set in the pop-up window label: '', value: 2000, - valueType: WorkflowIOValueTypeEnum.number, - min: 100, - max: 4000, - step: 50 + valueType: WorkflowIOValueTypeEnum.number }, + { + key: NodeInputKeyEnum.aiChatVision, + renderTypeList: [FlowNodeInputTypeEnum.hidden], + label: '', + valueType: WorkflowIOValueTypeEnum.boolean, + value: true + }, + { ...Input_Template_System_Prompt, label: 'core.ai.Prompt', diff --git a/packages/global/core/workflow/template/system/workflowStart.ts b/packages/global/core/workflow/template/system/workflowStart.ts index d07870a734f0..25b12478a7ba 100644 --- a/packages/global/core/workflow/template/system/workflowStart.ts +++ b/packages/global/core/workflow/template/system/workflowStart.ts @@ -7,6 +7,17 @@ import { } from '../../constants'; import { getHandleConfig } from '../utils'; import { Input_Template_UserChatInput } from '../input'; +import { i18nT } from '../../../../../web/i18n/utils'; +import { FlowNodeOutputItemType } from '../../type/io'; + +export const userFilesInput: FlowNodeOutputItemType = { + id: NodeOutputKeyEnum.userFiles, + key: NodeOutputKeyEnum.userFiles, + label: i18nT('app:workflow.user_file_input'), + description: i18nT('app:workflow.user_file_input_desc'), + type: FlowNodeOutputTypeEnum.static, + valueType: WorkflowIOValueTypeEnum.arrayString +}; export const WorkflowStart: FlowNodeTemplateType = { id: FlowNodeTypeEnum.workflowStart, @@ -15,7 +26,7 @@ export const WorkflowStart: FlowNodeTemplateType = { sourceHandle: getHandleConfig(false, true, false, false), targetHandle: getHandleConfig(false, false, false, false), avatar: 'core/workflow/template/workflowStart', - name: '流程开始', + name: i18nT('workflow:template.workflow_start'), intro: '', forbidDelete: true, unique: true, @@ -25,7 +36,7 @@ export const WorkflowStart: FlowNodeTemplateType = { { id: NodeOutputKeyEnum.userChatInput, key: NodeOutputKeyEnum.userChatInput, - label: 'core.module.input.label.user question', + label: i18nT('common:core.module.input.label.user question'), type: FlowNodeOutputTypeEnum.static, valueType: WorkflowIOValueTypeEnum.string } diff --git a/packages/global/core/workflow/utils.ts b/packages/global/core/workflow/utils.ts index e9b4cd92077e..bbc2df6b059a 100644 --- a/packages/global/core/workflow/utils.ts +++ b/packages/global/core/workflow/utils.ts @@ -82,6 +82,8 @@ export const splitGuideModule = (guideModules?: StoreNodeItemType) => { chatInputGuide }; }; + +// Get app chat config: db > nodes export const getAppChatConfig = ({ chatConfig, systemConfigNode, diff --git a/packages/plugins/src/duckduckgo/search/index.ts b/packages/plugins/src/duckduckgo/search/index.ts index 4275909f7e44..8fa0ae4d69ee 100644 --- a/packages/plugins/src/duckduckgo/search/index.ts +++ b/packages/plugins/src/duckduckgo/search/index.ts @@ -1,6 +1,7 @@ import { search, SafeSearchType } from 'duck-duck-scrape'; import { delay } from '@fastgpt/global/common/system/utils'; import { addLog } from '@fastgpt/service/common/system/log'; +import { getErrText } from '@fastgpt/global/common/error/utils'; type Props = { query: string; @@ -35,7 +36,7 @@ const main = async (props: Props, retry = 3): Response => { if (retry <= 0) { addLog.warn('DuckDuckGo error', { error }); return { - result: 'Failed to fetch data' + result: getErrText(error, 'Failed to fetch data from DuckDuckGo') }; } diff --git a/packages/plugins/src/duckduckgo/searchImg/index.ts b/packages/plugins/src/duckduckgo/searchImg/index.ts index 86a45db56fa5..6f5c5867952b 100644 --- a/packages/plugins/src/duckduckgo/searchImg/index.ts +++ b/packages/plugins/src/duckduckgo/searchImg/index.ts @@ -1,6 +1,7 @@ import { searchImages, SafeSearchType } from 'duck-duck-scrape'; import { delay } from '@fastgpt/global/common/system/utils'; import { addLog } from '@fastgpt/service/common/system/log'; +import { getErrText } from '@fastgpt/global/common/error/utils'; type Props = { query: string; @@ -33,7 +34,7 @@ const main = async (props: Props, retry = 3): Response => { if (retry <= 0) { addLog.warn('DuckDuckGo error', { error }); return { - result: 'Failed to fetch data' + result: getErrText(error, 'Failed to fetch data from DuckDuckGo') }; } diff --git a/packages/plugins/src/duckduckgo/searchNews/index.ts b/packages/plugins/src/duckduckgo/searchNews/index.ts index deeae0f6fc05..ac0b84b96ed4 100644 --- a/packages/plugins/src/duckduckgo/searchNews/index.ts +++ b/packages/plugins/src/duckduckgo/searchNews/index.ts @@ -1,6 +1,7 @@ import { searchNews, SafeSearchType } from 'duck-duck-scrape'; import { delay } from '@fastgpt/global/common/system/utils'; import { addLog } from '@fastgpt/service/common/system/log'; +import { getErrText } from '@fastgpt/global/common/error/utils'; type Props = { query: string; @@ -34,7 +35,7 @@ const main = async (props: Props, retry = 3): Response => { if (retry <= 0) { addLog.warn('DuckDuckGo error', { error }); return { - result: 'Failed to fetch data' + result: getErrText(error, 'Failed to fetch data from DuckDuckGo') }; } diff --git a/packages/plugins/src/duckduckgo/searchVideo/index.ts b/packages/plugins/src/duckduckgo/searchVideo/index.ts index 948eb177a46a..c1637d810d20 100644 --- a/packages/plugins/src/duckduckgo/searchVideo/index.ts +++ b/packages/plugins/src/duckduckgo/searchVideo/index.ts @@ -1,6 +1,7 @@ import { searchVideos, SafeSearchType } from 'duck-duck-scrape'; import { delay } from '@fastgpt/global/common/system/utils'; import { addLog } from '@fastgpt/service/common/system/log'; +import { getErrText } from '@fastgpt/global/common/error/utils'; type Props = { query: string; @@ -34,7 +35,7 @@ const main = async (props: Props, retry = 3): Response => { if (retry <= 0) { addLog.warn('DuckDuckGo error', { error }); return { - result: 'Failed to fetch data' + result: getErrText(error, 'Failed to fetch data from DuckDuckGo') }; } diff --git a/packages/plugins/src/template/template.json b/packages/plugins/src/template/template.json index 14d94f9acb5d..e4cdfd8324fa 100644 --- a/packages/plugins/src/template/template.json +++ b/packages/plugins/src/template/template.json @@ -1,6 +1,6 @@ { "author": "", - "version": "486", + "version": "489", "name": "文本加工", "avatar": "/imgs/workflow/textEditor.svg", "intro": "可对固定或传入的文本进行加工后输出,非字符串类型数据最终会转成字符串类型。", diff --git a/packages/service/common/buffer/rawText/schema.ts b/packages/service/common/buffer/rawText/schema.ts index 43c4e4087498..57b9a83099c7 100644 --- a/packages/service/common/buffer/rawText/schema.ts +++ b/packages/service/common/buffer/rawText/schema.ts @@ -1,5 +1,5 @@ -import { connectionMongo, getMongoModel, type Model } from '../../mongo'; -const { Schema, model, models } = connectionMongo; +import { connectionMongo, getMongoModel } from '../../mongo'; +const { Schema } = connectionMongo; import { RawTextBufferSchemaType } from './type'; export const collectionName = 'buffer_rawtexts'; diff --git a/packages/service/common/file/gridfs/controller.ts b/packages/service/common/file/gridfs/controller.ts index 7002fe14b1fa..1345a37b3614 100644 --- a/packages/service/common/file/gridfs/controller.ts +++ b/packages/service/common/file/gridfs/controller.ts @@ -3,16 +3,19 @@ import { BucketNameEnum } from '@fastgpt/global/common/file/constants'; import fsp from 'fs/promises'; import fs from 'fs'; import { DatasetFileSchema } from '@fastgpt/global/core/dataset/type'; -import { MongoFileSchema } from './schema'; +import { MongoChatFileSchema, MongoDatasetFileSchema } from './schema'; import { detectFileEncoding } from '@fastgpt/global/common/file/tools'; import { CommonErrEnum } from '@fastgpt/global/common/error/code/common'; import { MongoRawTextBuffer } from '../../buffer/rawText/schema'; import { readRawContentByFileBuffer } from '../read/utils'; import { gridFsStream2Buffer, stream2Encoding } from './utils'; import { addLog } from '../../system/log'; +import { readFromSecondary } from '../../mongo/utils'; export function getGFSCollection(bucket: `${BucketNameEnum}`) { - MongoFileSchema; + MongoDatasetFileSchema; + MongoChatFileSchema; + return connectionMongo.connection.db.collection(`${bucket}.files`); } export function getGridBucket(bucket: `${BucketNameEnum}`) { @@ -49,6 +52,7 @@ export async function uploadFile({ const { stream: readStream, encoding } = await stream2Encoding(fs.createReadStream(path)); + // Add default metadata metadata.teamId = teamId; metadata.tmbId = tmbId; metadata.encoding = encoding; @@ -103,7 +107,9 @@ export async function delFileByFileIdList({ try { const bucket = getGridBucket(bucketName); - await Promise.all(fileIdList.map((id) => bucket.delete(new Types.ObjectId(id)))); + for await (const fileId of fileIdList) { + await bucket.delete(new Types.ObjectId(fileId)); + } } catch (error) { if (retry > 0) { return delFileByFileIdList({ bucketName, fileIdList, retry: retry - 1 }); @@ -138,7 +144,9 @@ export const readFileContentFromMongo = async ({ filename: string; }> => { // read buffer - const fileBuffer = await MongoRawTextBuffer.findOne({ sourceId: fileId }).lean(); + const fileBuffer = await MongoRawTextBuffer.findOne({ sourceId: fileId }, undefined, { + ...readFromSecondary + }).lean(); if (fileBuffer) { return { rawText: fileBuffer.rawText, diff --git a/packages/service/common/file/gridfs/schema.ts b/packages/service/common/file/gridfs/schema.ts index 0b72a80be068..8d054f49d96d 100644 --- a/packages/service/common/file/gridfs/schema.ts +++ b/packages/service/common/file/gridfs/schema.ts @@ -1,13 +1,17 @@ import { connectionMongo, getMongoModel, type Model } from '../../mongo'; -const { Schema, model, models } = connectionMongo; +const { Schema } = connectionMongo; -const FileSchema = new Schema({}); +const DatasetFileSchema = new Schema({}); +const ChatFileSchema = new Schema({}); try { - FileSchema.index({ 'metadata.teamId': 1 }); - FileSchema.index({ 'metadata.uploadDate': -1 }); + DatasetFileSchema.index({ uploadDate: -1 }); + + ChatFileSchema.index({ uploadDate: -1 }); + ChatFileSchema.index({ 'metadata.chatId': 1 }); } catch (error) { console.log(error); } -export const MongoFileSchema = getMongoModel('dataset.files', FileSchema); +export const MongoDatasetFileSchema = getMongoModel('dataset.files', DatasetFileSchema); +export const MongoChatFileSchema = getMongoModel('chat.files', ChatFileSchema); diff --git a/packages/service/common/file/read/utils.ts b/packages/service/common/file/read/utils.ts index 11360cacf6f0..afd85b0ea345 100644 --- a/packages/service/common/file/read/utils.ts +++ b/packages/service/common/file/read/utils.ts @@ -8,28 +8,6 @@ import fs from 'fs'; import { detectFileEncoding } from '@fastgpt/global/common/file/tools'; import type { ReadFileResponse } from '../../../worker/readFile/type'; -// match md img text and upload to db -export const matchMdImgTextAndUpload = ({ - teamId, - md, - metadata -}: { - md: string; - teamId: string; - metadata?: Record; -}) => - markdownProcess({ - rawText: md, - uploadImgController: (base64Img) => - uploadMongoImg({ - type: MongoImageTypeEnum.collectionImage, - base64Img, - teamId, - metadata, - expiredTime: addHours(new Date(), 2) - }) - }); - export type readRawTextByLocalFileParams = { teamId: string; path: string; @@ -72,6 +50,28 @@ export const readRawContentByFileBuffer = async ({ encoding: string; metadata?: Record; }) => { + // Upload image in markdown + const matchMdImgTextAndUpload = ({ + teamId, + md, + metadata + }: { + md: string; + teamId: string; + metadata?: Record; + }) => + markdownProcess({ + rawText: md, + uploadImgController: (base64Img) => + uploadMongoImg({ + type: MongoImageTypeEnum.collectionImage, + base64Img, + teamId, + metadata, + expiredTime: addHours(new Date(), 1) + }) + }); + let { rawText, formatText } = await runWorker(WorkerNameEnum.readFile, { extension, encoding, diff --git a/packages/service/common/file/utils.ts b/packages/service/common/file/utils.ts index 7a9ef5c0b574..938187f96065 100644 --- a/packages/service/common/file/utils.ts +++ b/packages/service/common/file/utils.ts @@ -18,7 +18,17 @@ export const guessBase64ImageType = (str: string) => { i: 'image/png', R: 'image/gif', U: 'image/webp', - Q: 'image/bmp' + Q: 'image/bmp', + P: 'image/svg+xml', + T: 'image/tiff', + J: 'image/jp2', + S: 'image/x-tga', + I: 'image/ief', + V: 'image/vnd.microsoft.icon', + W: 'image/vnd.wap.wbmp', + X: 'image/x-xbitmap', + Z: 'image/x-xpixmap', + Y: 'image/x-xwindowdump' }; const defaultType = 'image/jpeg'; @@ -30,6 +40,11 @@ export const guessBase64ImageType = (str: string) => { return imageTypeMap[firstChar] || defaultType; }; +export const getFileContentTypeFromHeader = (header: string): string | undefined => { + const contentType = header.split(';')[0]; + return contentType; +}; + export const clearDirFiles = (dirPath: string) => { if (!fs.existsSync(dirPath)) { return; diff --git a/packages/service/core/ai/functions/createQuestionGuide.ts b/packages/service/core/ai/functions/createQuestionGuide.ts index e1ccd344280e..95db092c16f0 100644 --- a/packages/service/core/ai/functions/createQuestionGuide.ts +++ b/packages/service/core/ai/functions/createQuestionGuide.ts @@ -1,6 +1,7 @@ import type { ChatCompletionMessageParam } from '@fastgpt/global/core/ai/type.d'; import { getAIApi } from '../config'; import { countGptMessagesTokens } from '../../../common/string/tiktoken/index'; +import { loadRequestMessages } from '../../chat/utils'; export const Prompt_QuestionGuide = `你是一个AI智能助手,可以回答和解决我的问题。请结合前面的对话记录,帮我生成 3 个问题,引导我继续提问。问题的长度应小于20个字符,按 JSON 格式返回: ["问题1", "问题2", "问题3"]`; @@ -25,7 +26,10 @@ export async function createQuestionGuide({ model: model, temperature: 0.1, max_tokens: 200, - messages: concatMessages, + messages: await loadRequestMessages({ + messages: concatMessages, + useVision: false + }), stream: false }); diff --git a/packages/service/core/ai/utils.ts b/packages/service/core/ai/utils.ts new file mode 100644 index 000000000000..6e685ac750b4 --- /dev/null +++ b/packages/service/core/ai/utils.ts @@ -0,0 +1,39 @@ +import { LLMModelItemType } from '@fastgpt/global/core/ai/model.d'; +import { ChatCompletionMessageParam } from '@fastgpt/global/core/ai/type'; +import { countGptMessagesTokens } from '../../common/string/tiktoken'; + +export const computedMaxToken = async ({ + maxToken, + model, + filterMessages = [] +}: { + maxToken: number; + model: LLMModelItemType; + filterMessages: ChatCompletionMessageParam[]; +}) => { + maxToken = Math.min(maxToken, model.maxResponse); + const tokensLimit = model.maxContext; + + /* count response max token */ + const promptsToken = await countGptMessagesTokens(filterMessages); + maxToken = promptsToken + maxToken > tokensLimit ? tokensLimit - promptsToken : maxToken; + + if (maxToken <= 0) { + maxToken = 200; + } + return maxToken; +}; + +// FastGPT temperature range: [0,10], ai temperature:[0,2],{0,1]…… +export const computedTemperature = ({ + model, + temperature +}: { + model: LLMModelItemType; + temperature: number; +}) => { + temperature = +(model.maxTemperature * (temperature / 10)).toFixed(2); + temperature = Math.max(temperature, 0.01); + + return temperature; +}; diff --git a/packages/service/core/app/schema.ts b/packages/service/core/app/schema.ts index 0dc38bf7ed39..48bd1049c49c 100644 --- a/packages/service/core/app/schema.ts +++ b/packages/service/core/app/schema.ts @@ -17,7 +17,8 @@ export const chatConfigType = { ttsConfig: Object, whisperConfig: Object, scheduledTriggerConfig: Object, - chatInputGuide: Object + chatInputGuide: Object, + fileSelectConfig: Object }; // schema diff --git a/packages/service/core/chat/controller.ts b/packages/service/core/chat/controller.ts index e594fed27721..2c4ccef7a12f 100644 --- a/packages/service/core/chat/controller.ts +++ b/packages/service/core/chat/controller.ts @@ -2,6 +2,9 @@ import type { ChatItemType, ChatItemValueItemType } from '@fastgpt/global/core/c import { MongoChatItem } from './chatItemSchema'; import { addLog } from '../../common/system/log'; import { ChatItemValueTypeEnum } from '@fastgpt/global/core/chat/constants'; +import { delFileByFileIdList, getGFSCollection } from '../../common/file/gridfs/controller'; +import { BucketNameEnum } from '@fastgpt/global/common/file/constants'; +import { MongoChat } from './chatSchema'; export async function getChatItems({ appId, @@ -75,3 +78,40 @@ export const addCustomFeedbacks = async ({ addLog.error('addCustomFeedbacks error', error); } }; + +/* + Delete chat files + 1. ChatId: Delete one chat files + 2. AppId: Delete all the app's chat files +*/ +export const deleteChatFiles = async ({ + chatIdList, + appId +}: { + chatIdList?: string[]; + appId?: string; +}) => { + if (!appId && !chatIdList) return Promise.reject('appId or chatIdList is required'); + + const appChatIdList = await (async () => { + if (appId) { + const appChatIdList = await MongoChat.find({ appId }, { chatId: 1 }); + return appChatIdList.map((item) => String(item.chatId)); + } else if (chatIdList) { + return chatIdList; + } + return []; + })(); + + const collection = getGFSCollection(BucketNameEnum.chat); + const where = { + 'metadata.chatId': { $in: appChatIdList } + }; + + const files = await collection.find(where, { projection: { _id: 1 } }).toArray(); + + await delFileByFileIdList({ + bucketName: BucketNameEnum.chat, + fileIdList: files.map((item) => String(item._id)) + }); +}; diff --git a/packages/service/core/chat/utils.ts b/packages/service/core/chat/utils.ts index 12c5a6fbf958..8795b545e5b4 100644 --- a/packages/service/core/chat/utils.ts +++ b/packages/service/core/chat/utils.ts @@ -1,13 +1,13 @@ import { countGptMessagesTokens } from '../../common/string/tiktoken/index'; import type { ChatCompletionContentPart, - ChatCompletionMessageParam + ChatCompletionMessageParam, + SdkChatCompletionMessageParam } from '@fastgpt/global/core/ai/type.d'; import axios from 'axios'; import { ChatCompletionRequestMessageRoleEnum } from '@fastgpt/global/core/ai/constants'; -import { guessBase64ImageType } from '../../common/file/utils'; +import { getFileContentTypeFromHeader, guessBase64ImageType } from '../../common/file/utils'; import { serverRequestBaseUrl } from '../../common/api/serverRequest'; -import { cloneDeep } from 'lodash'; /* slice chat context by tokens */ const filterEmptyMessages = (messages: ChatCompletionMessageParam[]) => { @@ -96,89 +96,183 @@ export const filterGPTMessageByMaxTokens = async ({ return filterEmptyMessages([...systemPrompts, ...chats]); }; -export const formatGPTMessagesInRequestBefore = (messages: ChatCompletionMessageParam[]) => { - return messages - .map((item) => { - if (!item.content) return; - if (typeof item.content === 'string') { - return { - ...item, - content: item.content.trim() - }; - } +/* + Format requested messages + 1. If not useVision, only retain text. + 2. Remove file_url + 3. If useVision, parse url from question, and load image from url(Local url) +*/ +export const loadRequestMessages = async ({ + messages, + useVision = true, + origin +}: { + messages: ChatCompletionMessageParam[]; + useVision?: boolean; + origin?: string; +}) => { + // Split question text and image + function parseStringWithImages(input: string): ChatCompletionContentPart[] { + if (!useVision) { + return [{ type: 'text', text: input || '' }]; + } - // array - if (item.content.length === 0) return; - if (item.content.length === 1 && item.content[0].type === 'text') { - return { - ...item, - content: item.content[0].text - }; + // 正则表达式匹配图片URL + const imageRegex = /(https?:\/\/.*\.(?:png|jpe?g|gif|webp|bmp|tiff?|svg|ico|heic|avif))/i; + + const result: { type: 'text' | 'image'; value: string }[] = []; + let lastIndex = 0; + let match; + + // 使用正则表达式查找所有匹配项 + while ((match = imageRegex.exec(input.slice(lastIndex))) !== null) { + const textBefore = input.slice(lastIndex, lastIndex + match.index); + + // 如果图片URL前有文本,添加文本部分 + if (textBefore) { + result.push({ type: 'text', value: textBefore }); } - return item; - }) - .filter(Boolean) as ChatCompletionMessageParam[]; -}; + // 添加图片URL + result.push({ type: 'image', value: match[0] }); -/* Load user chat content. - Img: to base 64 -*/ -export const loadChatImgToBase64 = async (content: string | ChatCompletionContentPart[]) => { - if (typeof content === 'string') { - return content; + lastIndex += match.index + match[0].length; + } + + // 添加剩余的文本(如果有的话) + if (lastIndex < input.length) { + result.push({ type: 'text', value: input.slice(lastIndex) }); + } + + return result + .map((item) => { + if (item.type === 'text') { + return { type: 'text', text: item.value }; + } + if (item.type === 'image') { + return { + type: 'image_url', + image_url: { + url: item.value + } + }; + } + return { type: 'text', text: item.value }; + }) + .filter(Boolean) as ChatCompletionContentPart[]; } + // Load image + const parseUserContent = async (content: string | ChatCompletionContentPart[]) => { + if (typeof content === 'string') { + return parseStringWithImages(content); + } + + const result = await Promise.all( + content.map(async (item) => { + if (item.type === 'text') return parseStringWithImages(item.text); + if (item.type === 'file_url') return; - return Promise.all( - content.map(async (item) => { - if (item.type === 'text') return item; - - if (!item.image_url.url) return item; - - /* - 1. From db: Get it from db - 2. From web: Not update - */ - if (item.image_url.url.startsWith('/')) { - const response = await axios.get(item.image_url.url, { - baseURL: serverRequestBaseUrl, - responseType: 'arraybuffer' - }); - const base64 = Buffer.from(response.data).toString('base64'); - let imageType = response.headers['content-type']; - if (imageType === undefined) { - imageType = guessBase64ImageType(base64); + if (!item.image_url.url) return item; + + // Remove url origin + const imgUrl = (() => { + if (origin && item.image_url.url.startsWith(origin)) { + return item.image_url.url.replace(origin, ''); + } + return item.image_url.url; + })(); + + /* Load local image */ + if (imgUrl.startsWith('/')) { + const response = await axios.get(imgUrl, { + baseURL: serverRequestBaseUrl, + responseType: 'arraybuffer' + }); + const base64 = Buffer.from(response.data, 'binary').toString('base64'); + const imageType = + getFileContentTypeFromHeader(response.headers['content-type']) || + guessBase64ImageType(base64); + + return { + ...item, + image_url: { + ...item.image_url, + url: `data:${imageType};base64,${base64}` + } + }; } - return { - ...item, - image_url: { - ...item.image_url, - url: `data:${imageType};base64,${base64}` + + return item; + }) + ); + + return result.flat().filter(Boolean); + }; + // format GPT messages, concat text messages + const clearInvalidMessages = (messages: ChatCompletionMessageParam[]) => { + return messages + .map((item) => { + if (item.role === ChatCompletionRequestMessageRoleEnum.System && !item.content) { + return; + } + if (item.role === ChatCompletionRequestMessageRoleEnum.User) { + if (!item.content) return; + + if (typeof item.content === 'string') { + return { + ...item, + content: item.content.trim() + }; } - }; - } - return item; - }) - ); -}; -export const loadRequestMessages = async (messages: ChatCompletionMessageParam[]) => { + // array + if (item.content.length === 0) return; + if (item.content.length === 1 && item.content[0].type === 'text') { + return { + ...item, + content: item.content[0].text + }; + } + } + + return item; + }) + .filter(Boolean) as ChatCompletionMessageParam[]; + }; + if (messages.length === 0) { return Promise.reject('core.chat.error.Messages empty'); } - const loadMessages = await Promise.all( - messages.map(async (item) => { + // filter messages file + const filterMessages = messages.map((item) => { + // If useVision=false, only retain text. + if ( + item.role === ChatCompletionRequestMessageRoleEnum.User && + Array.isArray(item.content) && + !useVision + ) { + return { + ...item, + content: item.content.filter((item) => item.type === 'text') + }; + } + + return item; + }); + + const loadMessages = (await Promise.all( + filterMessages.map(async (item) => { if (item.role === ChatCompletionRequestMessageRoleEnum.User) { return { ...item, - content: await loadChatImgToBase64(item.content) + content: await parseUserContent(item.content) }; } else { return item; } }) - ); + )) as ChatCompletionMessageParam[]; - return loadMessages; + return clearInvalidMessages(loadMessages) as SdkChatCompletionMessageParam[]; }; diff --git a/packages/service/core/dataset/search/controller.ts b/packages/service/core/dataset/search/controller.ts index 134b6f08b82f..5383d4cb147f 100644 --- a/packages/service/core/dataset/search/controller.ts +++ b/packages/service/core/dataset/search/controller.ts @@ -493,7 +493,7 @@ export async function searchDatasetData(props: SearchDatasetDataProps) { getForbidData(), filterCollectionByMetadata() ]); - console.log(filterCollectionIdList, '==='); + await Promise.all( queries.map(async (query) => { const [{ tokens, embeddingRecallResults }, { fullTextRecallResults }] = await Promise.all([ diff --git a/packages/service/core/workflow/dispatch/agent/classifyQuestion.ts b/packages/service/core/workflow/dispatch/agent/classifyQuestion.ts index 368805e2b1b4..94715db1f9fb 100644 --- a/packages/service/core/workflow/dispatch/agent/classifyQuestion.ts +++ b/packages/service/core/workflow/dispatch/agent/classifyQuestion.ts @@ -16,6 +16,7 @@ import { formatModelChars2Points } from '../../../../support/wallet/usage/utils' import { DispatchNodeResultType } from '@fastgpt/global/core/workflow/runtime/type'; import { chatValue2RuntimePrompt } from '@fastgpt/global/core/chat/adapt'; import { getHandleId } from '@fastgpt/global/core/workflow/utils'; +import { loadRequestMessages } from '../../../chat/utils'; type Props = ModuleDispatchProps<{ [NodeInputKeyEnum.aiModel]: string; @@ -113,6 +114,10 @@ const completions = async ({ ] } ]; + const requestMessages = await loadRequestMessages({ + messages: chats2GPTMessages({ messages, reserveId: false }), + useVision: false + }); const ai = getAIApi({ userKey: user.openaiAccount, @@ -122,7 +127,7 @@ const completions = async ({ const data = await ai.chat.completions.create({ model: cqModel.model, temperature: 0.01, - messages: chats2GPTMessages({ messages, reserveId: false }), + messages: requestMessages, stream: false }); const answer = data.choices?.[0].message?.content || ''; diff --git a/packages/service/core/workflow/dispatch/agent/extract.ts b/packages/service/core/workflow/dispatch/agent/extract.ts index fdfef8637372..e8004f7590bb 100644 --- a/packages/service/core/workflow/dispatch/agent/extract.ts +++ b/packages/service/core/workflow/dispatch/agent/extract.ts @@ -1,5 +1,5 @@ import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt'; -import { filterGPTMessageByMaxTokens } from '../../../chat/utils'; +import { filterGPTMessageByMaxTokens, loadRequestMessages } from '../../../chat/utils'; import type { ChatItemType } from '@fastgpt/global/core/chat/type.d'; import { countMessagesTokens, @@ -173,6 +173,10 @@ ${description ? `- ${description}` : ''} messages: adaptMessages, maxTokens: extractModel.maxContext }); + const requestMessages = await loadRequestMessages({ + messages: filterMessages, + useVision: false + }); const properties: Record< string, @@ -200,7 +204,7 @@ ${description ? `- ${description}` : ''} }; return { - filterMessages, + filterMessages: requestMessages, agentFunction }; }; @@ -338,6 +342,10 @@ Human: ${content}` ] } ]; + const requestMessages = await loadRequestMessages({ + messages: chats2GPTMessages({ messages, reserveId: false }), + useVision: false + }); const ai = getAIApi({ userKey: user.openaiAccount, @@ -346,7 +354,7 @@ Human: ${content}` const data = await ai.chat.completions.create({ model: extractModel.model, temperature: 0.01, - messages: chats2GPTMessages({ messages, reserveId: false }), + messages: requestMessages, stream: false }); const answer = data.choices?.[0].message?.content || ''; diff --git a/packages/service/core/workflow/dispatch/agent/runTool/constants.ts b/packages/service/core/workflow/dispatch/agent/runTool/constants.ts index 95be4ad51d87..03b4aade77e1 100644 --- a/packages/service/core/workflow/dispatch/agent/runTool/constants.ts +++ b/packages/service/core/workflow/dispatch/agent/runTool/constants.ts @@ -1,3 +1,5 @@ +import { replaceVariable } from '@fastgpt/global/common/string/tools'; + export const Prompt_Tool_Call = ` 你是一个智能机器人,除了可以回答用户问题外,你还掌握工具的使用能力。有时候,你可以依赖工具的运行结果,来更准确的回答用户。 @@ -32,6 +34,8 @@ TOOL_RESPONSE: """ ANSWER: 0: 今天杭州是晴天,适合去西湖、灵隐寺、千岛湖等地玩。 +------ + 现在,我们开始吧!下面是你本次可以使用的工具: """ @@ -42,3 +46,16 @@ ANSWER: 0: 今天杭州是晴天,适合去西湖、灵隐寺、千岛湖等地 USER: {{question}} ANSWER: `; + +export const getMultiplePrompt = (obj: { + fileCount: number; + imgCount: number; + question: string; +}) => { + const prompt = `Number of session file inputs: +Document:{{fileCount}} +Image:{{imgCount}} +------ +{{question}}`; + return replaceVariable(prompt, obj); +}; diff --git a/packages/service/core/workflow/dispatch/agent/runTool/functionCall.ts b/packages/service/core/workflow/dispatch/agent/runTool/functionCall.ts index d74b099d8c7c..1f6119f95ae7 100644 --- a/packages/service/core/workflow/dispatch/agent/runTool/functionCall.ts +++ b/packages/service/core/workflow/dispatch/agent/runTool/functionCall.ts @@ -9,7 +9,7 @@ import { ChatCompletionMessageFunctionCall, ChatCompletionFunctionMessageParam, ChatCompletionAssistantMessageParam -} from '@fastgpt/global/core/ai/type'; +} from '@fastgpt/global/core/ai/type.d'; import { NextApiResponse } from 'next'; import { responseWrite, @@ -24,10 +24,11 @@ import { DispatchToolModuleProps, RunToolResponse, ToolNodeItemType } from './ty import json5 from 'json5'; import { DispatchFlowResponse } from '../../type'; import { countGptMessagesTokens } from '../../../../../common/string/tiktoken/index'; -import { getNanoid } from '@fastgpt/global/common/string/tools'; +import { getNanoid, sliceStrStartEnd } from '@fastgpt/global/common/string/tools'; import { AIChatItemType } from '@fastgpt/global/core/chat/type'; import { GPTMessages2Chats } from '@fastgpt/global/core/chat/adapt'; import { updateToolInputValue } from './utils'; +import { computedMaxToken, computedTemperature } from '../../../../ai/utils'; type FunctionRunResponseType = { toolRunResponse: DispatchFlowResponse; @@ -42,7 +43,18 @@ export const runToolWithFunctionCall = async ( }, response?: RunToolResponse ): Promise => { - const { toolModel, toolNodes, messages, res, runtimeNodes, detail = false, node, stream } = props; + const { + toolModel, + toolNodes, + messages, + res, + requestOrigin, + runtimeNodes, + detail = false, + node, + stream, + params: { temperature = 0, maxToken = 4000, aiChatVision } + } = props; const assistantResponses = response?.assistantResponses || []; const functions: ChatCompletionCreateParams.Function[] = toolNodes.map((item) => { @@ -72,44 +84,60 @@ export const runToolWithFunctionCall = async ( }; }); - const filterMessages = await filterGPTMessageByMaxTokens({ - messages, - maxTokens: toolModel.maxContext - 500 // filter token. not response maxToken - }); - const formativeMessages = filterMessages.map((item) => { + const filterMessages = ( + await filterGPTMessageByMaxTokens({ + messages, + maxTokens: toolModel.maxContext - 300 // filter token. not response maxToken + }) + ).map((item) => { if (item.role === ChatCompletionRequestMessageRoleEnum.Assistant && item.function_call) { return { ...item, function_call: { name: item.function_call?.name, arguments: item.function_call?.arguments - } + }, + content: '' }; } return item; }); - const requestMessages = await loadRequestMessages(formativeMessages); - + const [requestMessages, max_tokens] = await Promise.all([ + loadRequestMessages({ + messages: filterMessages, + useVision: toolModel.vision && aiChatVision, + origin: requestOrigin + }), + computedMaxToken({ + model: toolModel, + maxToken, + filterMessages + }) + ]); + const requestBody: any = { + ...toolModel?.defaultConfig, + model: toolModel.model, + temperature: computedTemperature({ + model: toolModel, + temperature + }), + max_tokens, + stream, + messages: requestMessages, + functions, + function_call: 'auto' + }; + + // console.log(JSON.stringify(requestBody, null, 2)); /* Run llm */ const ai = getAIApi({ timeout: 480000 }); - const aiResponse = await ai.chat.completions.create( - { - ...toolModel?.defaultConfig, - model: toolModel.model, - temperature: 0, - stream, - messages: requestMessages, - functions, - function_call: 'auto' - }, - { - headers: { - Accept: 'application/json, text/plain, */*' - } + const aiResponse = await ai.chat.completions.create(requestBody, { + headers: { + Accept: 'application/json, text/plain, */*' } - ); + }); const { answer, functionCalls } = await (async () => { if (res && stream) { @@ -198,7 +226,7 @@ export const runToolWithFunctionCall = async ( toolName: '', toolAvatar: '', params: '', - response: stringToolResponse + response: sliceStrStartEnd(stringToolResponse, 300, 300) } }) }); @@ -222,7 +250,7 @@ export const runToolWithFunctionCall = async ( function_call: functionCall }; const concatToolMessages = [ - ...filterMessages, + ...requestMessages, assistantToolMsgParams ] as ChatCompletionMessageParam[]; const tokens = await countGptMessagesTokens(concatToolMessages, undefined, functions); diff --git a/packages/service/core/workflow/dispatch/agent/runTool/index.ts b/packages/service/core/workflow/dispatch/agent/runTool/index.ts index e8c06aefb7e6..8b6449450249 100644 --- a/packages/service/core/workflow/dispatch/agent/runTool/index.ts +++ b/packages/service/core/workflow/dispatch/agent/runTool/index.ts @@ -8,7 +8,7 @@ import { ModelTypeEnum, getLLMModel } from '../../../../ai/model'; import { filterToolNodeIdByEdges, getHistories } from '../../utils'; import { runToolWithToolChoice } from './toolChoice'; import { DispatchToolModuleProps, ToolNodeItemType } from './type.d'; -import { ChatItemType } from '@fastgpt/global/core/chat/type'; +import { ChatItemType, UserChatItemValueItemType } from '@fastgpt/global/core/chat/type'; import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; import { GPTMessages2Chats, @@ -22,12 +22,46 @@ import { getHistoryPreview } from '@fastgpt/global/core/chat/utils'; import { runToolWithFunctionCall } from './functionCall'; import { runToolWithPromptCall } from './promptCall'; import { replaceVariable } from '@fastgpt/global/common/string/tools'; -import { Prompt_Tool_Call } from './constants'; +import { getMultiplePrompt, Prompt_Tool_Call } from './constants'; +import { filterToolResponseToPreview } from './utils'; type Response = DispatchNodeResultType<{ [NodeOutputKeyEnum.answerText]: string; }>; +/* + Tool call, auth add file prompt to question。 + Guide the LLM to call tool. +*/ +export const toolCallMessagesAdapt = ({ + userInput +}: { + userInput: UserChatItemValueItemType[]; +}) => { + const files = userInput.filter((item) => item.type === 'file'); + + if (files.length > 0) { + return userInput.map((item) => { + if (item.type === 'text') { + const filesCount = files.filter((file) => file.file?.type === 'file').length; + const imgCount = files.filter((file) => file.file?.type === 'image').length; + const text = item.text?.content || ''; + + return { + ...item, + text: { + content: getMultiplePrompt({ fileCount: filesCount, imgCount, question: text }) + } + }; + } + + return item; + }); + } + + return userInput; +}; + export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise => { const { node: { nodeId, name }, @@ -62,16 +96,31 @@ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise< const messages: ChatItemType[] = [ ...getSystemPrompt(systemPrompt), - ...chatHistories, + // Add file input prompt to histories + ...chatHistories.map((item) => { + if (item.obj === ChatRoleEnum.Human) { + return { + ...item, + value: toolCallMessagesAdapt({ + userInput: item.value + }) + }; + } + return item; + }), { obj: ChatRoleEnum.Human, - value: runtimePrompt2ChatsValue({ - text: userChatInput, - files: chatValue2RuntimePrompt(query).files + value: toolCallMessagesAdapt({ + userInput: runtimePrompt2ChatsValue({ + text: userChatInput, + files: chatValue2RuntimePrompt(query).files + }) }) } ]; + // console.log(JSON.stringify(messages, null, 2)); + const { dispatchFlowResponse, // tool flow response totalTokens, @@ -98,14 +147,24 @@ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise< } const lastMessage = adaptMessages[adaptMessages.length - 1]; - if (typeof lastMessage.content !== 'string') { - return Promise.reject('暂时只支持纯文本'); + if (typeof lastMessage.content === 'string') { + lastMessage.content = replaceVariable(Prompt_Tool_Call, { + question: lastMessage.content + }); + } else if (Array.isArray(lastMessage.content)) { + // array, replace last element + const lastText = lastMessage.content[lastMessage.content.length - 1]; + if (lastText.type === 'text') { + lastMessage.content = replaceVariable(Prompt_Tool_Call, { + question: lastText.text + }); + } else { + return Promise.reject('Prompt call invalid input'); + } + } else { + return Promise.reject('Prompt call invalid input'); } - lastMessage.content = replaceVariable(Prompt_Tool_Call, { - question: userChatInput - }); - return runToolWithPromptCall({ ...props, toolNodes, @@ -132,12 +191,14 @@ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise< }, 0); const flatUsages = dispatchFlowResponse.map((item) => item.flowUsages).flat(); + const previewAssistantResponses = filterToolResponseToPreview(assistantResponses); + return { - [NodeOutputKeyEnum.answerText]: assistantResponses + [NodeOutputKeyEnum.answerText]: previewAssistantResponses .filter((item) => item.text?.content) .map((item) => item.text?.content || '') .join(''), - [DispatchNodeResponseKeyEnum.assistantResponses]: assistantResponses, + [DispatchNodeResponseKeyEnum.assistantResponses]: previewAssistantResponses, [DispatchNodeResponseKeyEnum.nodeResponse]: { totalPoints: totalPointsUsage, toolCallTokens: totalTokens, diff --git a/packages/service/core/workflow/dispatch/agent/runTool/promptCall.ts b/packages/service/core/workflow/dispatch/agent/runTool/promptCall.ts index e37561090e81..557078bdcb39 100644 --- a/packages/service/core/workflow/dispatch/agent/runTool/promptCall.ts +++ b/packages/service/core/workflow/dispatch/agent/runTool/promptCall.ts @@ -20,10 +20,16 @@ import { dispatchWorkFlow } from '../../index'; import { DispatchToolModuleProps, RunToolResponse, ToolNodeItemType } from './type.d'; import json5 from 'json5'; import { countGptMessagesTokens } from '../../../../../common/string/tiktoken/index'; -import { getNanoid, replaceVariable, sliceJsonStr } from '@fastgpt/global/common/string/tools'; +import { + getNanoid, + replaceVariable, + sliceJsonStr, + sliceStrStartEnd +} from '@fastgpt/global/common/string/tools'; import { AIChatItemType } from '@fastgpt/global/core/chat/type'; import { GPTMessages2Chats } from '@fastgpt/global/core/chat/adapt'; import { updateToolInputValue } from './utils'; +import { computedMaxToken, computedTemperature } from '../../../../ai/utils'; type FunctionCallCompletion = { id: string; @@ -43,7 +49,18 @@ export const runToolWithPromptCall = async ( }, response?: RunToolResponse ): Promise => { - const { toolModel, toolNodes, messages, res, runtimeNodes, detail = false, node, stream } = props; + const { + toolModel, + toolNodes, + messages, + res, + requestOrigin, + runtimeNodes, + detail = false, + node, + stream, + params: { temperature = 0, maxToken = 4000, aiChatVision } + } = props; const assistantResponses = response?.assistantResponses || []; const toolsPrompt = JSON.stringify( @@ -77,7 +94,7 @@ export const runToolWithPromptCall = async ( const lastMessage = messages[messages.length - 1]; if (typeof lastMessage.content !== 'string') { - return Promise.reject('暂时只支持纯文本'); + return Promise.reject('Prompt call invalid input'); } lastMessage.content = replaceVariable(lastMessage.content, { toolsPrompt @@ -87,27 +104,40 @@ export const runToolWithPromptCall = async ( messages, maxTokens: toolModel.maxContext - 500 // filter token. not response maxToken }); - const requestMessages = await loadRequestMessages(filterMessages); + const [requestMessages, max_tokens] = await Promise.all([ + loadRequestMessages({ + messages: filterMessages, + useVision: toolModel.vision && aiChatVision, + origin: requestOrigin + }), + computedMaxToken({ + model: toolModel, + maxToken, + filterMessages + }) + ]); + const requestBody = { + ...toolModel?.defaultConfig, + model: toolModel.model, + temperature: computedTemperature({ + model: toolModel, + temperature + }), + max_tokens, + stream, + messages: requestMessages + }; - // console.log(JSON.stringify(filterMessages, null, 2)); + // console.log(JSON.stringify(requestBody, null, 2)); /* Run llm */ const ai = getAIApi({ timeout: 480000 }); - const aiResponse = await ai.chat.completions.create( - { - ...toolModel?.defaultConfig, - model: toolModel.model, - temperature: 0, - stream, - messages: requestMessages - }, - { - headers: { - Accept: 'application/json, text/plain, */*' - } + const aiResponse = await ai.chat.completions.create(requestBody, { + headers: { + Accept: 'application/json, text/plain, */*' } - ); + }); const answer = await (async () => { if (res && stream) { @@ -225,7 +255,7 @@ export const runToolWithPromptCall = async ( toolName: '', toolAvatar: '', params: '', - response: stringToolResponse + response: sliceStrStartEnd(stringToolResponse, 300, 300) } }) }); @@ -250,7 +280,7 @@ export const runToolWithPromptCall = async ( function_call: toolJson }; const concatToolMessages = [ - ...filterMessages, + ...requestMessages, assistantToolMsgParams ] as ChatCompletionMessageParam[]; const tokens = await countGptMessagesTokens(concatToolMessages, undefined); diff --git a/packages/service/core/workflow/dispatch/agent/runTool/toolChoice.ts b/packages/service/core/workflow/dispatch/agent/runTool/toolChoice.ts index ebc8645cb4b3..7f34c4646ac9 100644 --- a/packages/service/core/workflow/dispatch/agent/runTool/toolChoice.ts +++ b/packages/service/core/workflow/dispatch/agent/runTool/toolChoice.ts @@ -28,6 +28,8 @@ import { countGptMessagesTokens } from '../../../../../common/string/tiktoken/in import { GPTMessages2Chats } from '@fastgpt/global/core/chat/adapt'; import { AIChatItemType } from '@fastgpt/global/core/chat/type'; import { updateToolInputValue } from './utils'; +import { computedMaxToken, computedTemperature } from '../../../../ai/utils'; +import { sliceStrStartEnd } from '@fastgpt/global/common/string/tools'; type ToolRunResponseType = { toolRunResponse: DispatchFlowResponse; @@ -49,7 +51,18 @@ export const runToolWithToolChoice = async ( }, response?: RunToolResponse ): Promise => { - const { toolModel, toolNodes, messages, res, runtimeNodes, detail = false, node, stream } = props; + const { + toolModel, + toolNodes, + messages, + res, + requestOrigin, + runtimeNodes, + detail = false, + node, + stream, + params: { temperature = 0, maxToken = 4000, aiChatVision } + } = props; const assistantResponses = response?.assistantResponses || []; const tools: ChatCompletionTool[] = toolNodes.map((item) => { @@ -81,12 +94,13 @@ export const runToolWithToolChoice = async ( } }; }); - - const filterMessages = await filterGPTMessageByMaxTokens({ - messages, - maxTokens: toolModel.maxContext - 300 // filter token. not response maxToken - }); - const formativeMessages = filterMessages.map((item) => { + // Filter histories by maxToken + const filterMessages = ( + await filterGPTMessageByMaxTokens({ + messages, + maxTokens: toolModel.maxContext - 300 // filter token. not response maxToken + }) + ).map((item) => { if (item.role === 'assistant' && item.tool_calls) { return { ...item, @@ -99,43 +113,43 @@ export const runToolWithToolChoice = async ( } return item; }); - const requestMessages = await loadRequestMessages(formativeMessages); - - // console.log( - // JSON.stringify( - // { - // ...toolModel?.defaultConfig, - // model: toolModel.model, - // temperature: 0, - // stream, - // messages: requestMessages, - // tools, - // tool_choice: 'auto' - // }, - // null, - // 2 - // ) - // ); + + const [requestMessages, max_tokens] = await Promise.all([ + loadRequestMessages({ + messages: filterMessages, + useVision: toolModel.vision && aiChatVision, + origin: requestOrigin + }), + computedMaxToken({ + model: toolModel, + maxToken, + filterMessages + }) + ]); + const requestBody: any = { + ...toolModel?.defaultConfig, + model: toolModel.model, + temperature: computedTemperature({ + model: toolModel, + temperature + }), + max_tokens, + stream, + messages: requestMessages, + tools, + tool_choice: 'auto' + }; + + // console.log(JSON.stringify(requestBody, null, 2)); /* Run llm */ const ai = getAIApi({ timeout: 480000 }); - const aiResponse = await ai.chat.completions.create( - { - ...toolModel?.defaultConfig, - model: toolModel.model, - temperature: 0, - stream, - messages: requestMessages, - tools, - tool_choice: 'auto' - }, - { - headers: { - Accept: 'application/json, text/plain, */*' - } + const aiResponse = await ai.chat.completions.create(requestBody, { + headers: { + Accept: 'application/json, text/plain, */*' } - ); + }); const { answer, toolCalls } = await (async () => { if (res && stream) { @@ -221,7 +235,7 @@ export const runToolWithToolChoice = async ( toolName: '', toolAvatar: '', params: '', - response: stringToolResponse + response: sliceStrStartEnd(stringToolResponse, 300, 300) } }) }); @@ -243,7 +257,7 @@ export const runToolWithToolChoice = async ( tool_calls: toolCalls }; const concatToolMessages = [ - ...filterMessages, + ...requestMessages, assistantToolMsgParams ] as ChatCompletionMessageParam[]; const tokens = await countGptMessagesTokens(concatToolMessages, tools); diff --git a/packages/service/core/workflow/dispatch/agent/runTool/type.d.ts b/packages/service/core/workflow/dispatch/agent/runTool/type.d.ts index 8f1f7b81fda8..85dac387da9f 100644 --- a/packages/service/core/workflow/dispatch/agent/runTool/type.d.ts +++ b/packages/service/core/workflow/dispatch/agent/runTool/type.d.ts @@ -11,9 +11,13 @@ import { AIChatItemValueItemType, ChatItemValueItemType } from '@fastgpt/global/ export type DispatchToolModuleProps = ModuleDispatchProps<{ [NodeInputKeyEnum.history]?: ChatItemType[]; + [NodeInputKeyEnum.userChatInput]: string; + [NodeInputKeyEnum.aiModel]: string; [NodeInputKeyEnum.aiSystemPrompt]: string; - [NodeInputKeyEnum.userChatInput]: string; + [NodeInputKeyEnum.aiChatTemperature]: number; + [NodeInputKeyEnum.aiChatMaxToken]: number; + [NodeInputKeyEnum.aiChatVision]?: boolean; }>; export type RunToolResponse = { diff --git a/packages/service/core/workflow/dispatch/agent/runTool/utils.ts b/packages/service/core/workflow/dispatch/agent/runTool/utils.ts index 11d25e947929..77bbeeb59bbc 100644 --- a/packages/service/core/workflow/dispatch/agent/runTool/utils.ts +++ b/packages/service/core/workflow/dispatch/agent/runTool/utils.ts @@ -1,3 +1,6 @@ +import { sliceStrStartEnd } from '@fastgpt/global/common/string/tools'; +import { ChatItemValueTypeEnum } from '@fastgpt/global/core/chat/constants'; +import { AIChatItemValueItemType } from '@fastgpt/global/core/chat/type'; import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io'; export const updateToolInputValue = ({ @@ -12,3 +15,22 @@ export const updateToolInputValue = ({ value: params[input.key] ?? input.value })); }; + +export const filterToolResponseToPreview = (response: AIChatItemValueItemType[]) => { + return response.map((item) => { + if (item.type === ChatItemValueTypeEnum.tool) { + const formatTools = item.tools?.map((tool) => { + return { + ...tool, + response: sliceStrStartEnd(tool.response, 500, 500) + }; + }); + return { + ...item, + tools: formatTools + }; + } + + return item; + }); +}; diff --git a/packages/service/core/workflow/dispatch/chat/oneapi.ts b/packages/service/core/workflow/dispatch/chat/oneapi.ts index 299f7683a8f9..fc5e5058595d 100644 --- a/packages/service/core/workflow/dispatch/chat/oneapi.ts +++ b/packages/service/core/workflow/dispatch/chat/oneapi.ts @@ -1,9 +1,5 @@ import type { NextApiResponse } from 'next'; -import { - filterGPTMessageByMaxTokens, - formatGPTMessagesInRequestBefore, - loadRequestMessages -} from '../../../chat/utils'; +import { filterGPTMessageByMaxTokens, loadRequestMessages } from '../../../chat/utils'; import type { ChatItemType, UserChatItemValueItemType } from '@fastgpt/global/core/chat/type.d'; import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants'; @@ -19,10 +15,7 @@ import type { LLMModelItemType } from '@fastgpt/global/core/ai/model.d'; import { postTextCensor } from '../../../../common/api/requestPlusApi'; import { ChatCompletionRequestMessageRoleEnum } from '@fastgpt/global/core/ai/constants'; import type { DispatchNodeResultType } from '@fastgpt/global/core/workflow/runtime/type'; -import { - countGptMessagesTokens, - countMessagesTokens -} from '../../../../common/string/tiktoken/index'; +import { countMessagesTokens } from '../../../../common/string/tiktoken/index'; import { chats2GPTMessages, chatValue2RuntimePrompt, @@ -31,6 +24,7 @@ import { runtimePrompt2ChatsValue } from '@fastgpt/global/core/chat/adapt'; import { + Prompt_DocumentQuote, Prompt_QuotePromptList, Prompt_QuoteTemplateList } from '@fastgpt/global/core/ai/prompt/AIChat'; @@ -46,6 +40,7 @@ import { getHistories } from '../utils'; import { filterSearchResultsByMaxChars } from '../../utils'; import { getHistoryPreview } from '@fastgpt/global/core/chat/utils'; import { addLog } from '../../../../common/system/log'; +import { computedMaxToken, computedTemperature } from '../../../ai/utils'; export type ChatProps = ModuleDispatchProps< AIChatNodeProps & { @@ -63,6 +58,7 @@ export type ChatResponse = DispatchNodeResultType<{ export const dispatchChatCompletion = async (props: ChatProps): Promise => { let { res, + requestOrigin, stream = false, detail = false, user, @@ -79,7 +75,9 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise { + // censor model and system key + if (modelConstantsData.censor && !user.openaiAccount?.key) { + await postTextCensor({ + text: `${systemPrompt} + ${datasetQuoteText} + ${userChatInput} + ` + }); + } + } + ]); + // Get the request messages const concatMessages = [ ...(modelConstantsData.defaultSystemChatPrompt ? [ @@ -148,20 +135,39 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise 0 ? `${filterQuoteQA.map((item, index) => getValue(item, index).trim()).join('\n------\n')}` : ''; return { - quoteText + datasetQuoteText }; } async function getChatMessages({ - quotePrompt, - quoteText, - quoteQA, + datasetQuotePrompt, + datasetQuoteText, + useDatasetQuote, histories = [], systemPrompt, userChatInput, inputFiles, - model + model, + stringQuoteText }: { - quotePrompt?: string; - quoteText: string; - quoteQA: ChatProps['params']['quoteQA']; + datasetQuotePrompt?: string; + datasetQuoteText: string; + useDatasetQuote: boolean; histories: ChatItemType[]; systemPrompt: string; userChatInput: string; inputFiles: UserChatItemValueItemType['file'][]; model: LLMModelItemType; + stringQuoteText?: string; }) { - const replaceInputValue = - quoteQA !== undefined - ? replaceVariable(quotePrompt || Prompt_QuotePromptList[0].value, { - quote: quoteText, - question: userChatInput - }) - : userChatInput; + const replaceInputValue = useDatasetQuote + ? replaceVariable(datasetQuotePrompt || Prompt_QuotePromptList[0].value, { + quote: datasetQuoteText, + question: userChatInput + }) + : userChatInput; const messages: ChatItemType[] = [ ...getSystemPrompt(systemPrompt), + ...(stringQuoteText + ? getSystemPrompt( + replaceVariable(Prompt_DocumentQuote, { + quote: stringQuoteText + }) + ) + : []), ...histories, { obj: ChatRoleEnum.Human, @@ -323,29 +337,6 @@ async function getChatMessages({ filterMessages }; } -async function getMaxTokens({ - maxToken, - model, - filterMessages = [] -}: { - maxToken: number; - model: LLMModelItemType; - filterMessages: ChatCompletionMessageParam[]; -}) { - maxToken = Math.min(maxToken, model.maxResponse); - const tokensLimit = model.maxContext; - - /* count response max token */ - const promptsToken = await countGptMessagesTokens(filterMessages); - maxToken = promptsToken + maxToken > tokensLimit ? tokensLimit - promptsToken : maxToken; - - if (maxToken <= 0) { - maxToken = 200; - } - return { - max_tokens: maxToken - }; -} async function streamResponse({ res, diff --git a/packages/service/core/workflow/dispatch/index.ts b/packages/service/core/workflow/dispatch/index.ts index 8cba5943cba5..56d4e4d56d59 100644 --- a/packages/service/core/workflow/dispatch/index.ts +++ b/packages/service/core/workflow/dispatch/index.ts @@ -55,6 +55,7 @@ import { surrenderProcess } from '../../../common/system/tools'; import { dispatchRunCode } from './code/run'; import { dispatchTextEditor } from './tools/textEditor'; import { dispatchCustomFeedback } from './tools/customFeedback'; +import { dispatchReadFiles } from './tools/readFiles'; const callbackMap: Record = { [FlowNodeTypeEnum.workflowStart]: dispatchWorkflowStart, @@ -78,6 +79,7 @@ const callbackMap: Record = { [FlowNodeTypeEnum.code]: dispatchRunCode, [FlowNodeTypeEnum.textEditor]: dispatchTextEditor, [FlowNodeTypeEnum.customFeedback]: dispatchCustomFeedback, + [FlowNodeTypeEnum.readFiles]: dispatchReadFiles, // none [FlowNodeTypeEnum.systemConfig]: dispatchSystemConfig, diff --git a/packages/service/core/workflow/dispatch/init/workflowStart.tsx b/packages/service/core/workflow/dispatch/init/workflowStart.tsx index ebb43dcf6b06..406c447a7eac 100644 --- a/packages/service/core/workflow/dispatch/init/workflowStart.tsx +++ b/packages/service/core/workflow/dispatch/init/workflowStart.tsx @@ -1,13 +1,16 @@ import { chatValue2RuntimePrompt } from '@fastgpt/global/core/chat/adapt'; -import { UserChatItemValueItemType } from '@fastgpt/global/core/chat/type'; -import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; +import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import type { ModuleDispatchProps } from '@fastgpt/global/core/workflow/runtime/type'; + export type UserChatInputProps = ModuleDispatchProps<{ [NodeInputKeyEnum.userChatInput]: string; - [NodeInputKeyEnum.inputFiles]: UserChatItemValueItemType['file'][]; }>; +type Response = { + [NodeOutputKeyEnum.userChatInput]: string; + [NodeOutputKeyEnum.userFiles]: string[]; +}; -export const dispatchWorkflowStart = (props: Record) => { +export const dispatchWorkflowStart = (props: Record): Response => { const { query, params: { userChatInput } @@ -17,6 +20,11 @@ export const dispatchWorkflowStart = (props: Record) => { return { [NodeInputKeyEnum.userChatInput]: text || userChatInput, - [NodeInputKeyEnum.inputFiles]: files + [NodeOutputKeyEnum.userFiles]: files + .map((item) => { + return item?.url ?? ''; + }) + .filter(Boolean) + // [NodeInputKeyEnum.inputFiles]: files }; }; diff --git a/packages/service/core/workflow/dispatch/tools/readFiles.ts b/packages/service/core/workflow/dispatch/tools/readFiles.ts new file mode 100644 index 000000000000..6f4bffce3fe0 --- /dev/null +++ b/packages/service/core/workflow/dispatch/tools/readFiles.ts @@ -0,0 +1,196 @@ +import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants'; +import type { ModuleDispatchProps } from '@fastgpt/global/core/workflow/runtime/type'; +import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants'; +import { DispatchNodeResultType } from '@fastgpt/global/core/workflow/runtime/type'; +import { documentFileType } from '@fastgpt/global/common/file/constants'; +import axios from 'axios'; +import { serverRequestBaseUrl } from '../../../../common/api/serverRequest'; +import { MongoRawTextBuffer } from '../../../../common/buffer/rawText/schema'; +import { readFromSecondary } from '../../../../common/mongo/utils'; +import { getErrText } from '@fastgpt/global/common/error/utils'; +import { detectFileEncoding } from '@fastgpt/global/common/file/tools'; +import { readRawContentByFileBuffer } from '../../../../common/file/read/utils'; +import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; +import { UserChatItemValueItemType } from '@fastgpt/global/core/chat/type'; + +type Props = ModuleDispatchProps<{ + [NodeInputKeyEnum.fileUrlList]: string[]; +}>; +type Response = DispatchNodeResultType<{ + [NodeOutputKeyEnum.text]: string; +}>; + +const formatResponseObject = ({ + filename, + url, + content +}: { + filename: string; + url: string; + content: string; +}) => ({ + filename, + url, + text: `File: ${filename} + +${content} +`, + nodeResponsePreviewText: `File: ${filename} + +${content.slice(0, 100)}${content.length > 100 ? '......' : ''} +` +}); + +export const dispatchReadFiles = async (props: Props): Promise => { + const { + requestOrigin, + teamId, + histories, + chatConfig, + params: { fileUrlList = [] } + } = props; + const maxFiles = chatConfig?.fileSelectConfig?.maxFiles || 0; + + // Get files from histories + const filesFromHistories = histories + .filter((item) => { + if (item.obj === ChatRoleEnum.Human) { + return item.value.filter((value) => value.type === 'file'); + } + return false; + }) + .map((item) => { + const value = item.value as UserChatItemValueItemType[]; + const files = value + .map((item) => { + return item.file?.url; + }) + .filter(Boolean) as string[]; + return files; + }) + .flat(); + + const parseUrlList = [...fileUrlList, ...filesFromHistories].slice(0, maxFiles); + + const readFilesResult = await Promise.all( + parseUrlList + .map(async (url) => { + // System file + if (url.startsWith('/') || (requestOrigin && url.startsWith(requestOrigin))) { + // Parse url, get filename query. Keep only documents that can be parsed + const parseUrl = new URL(url); + const filenameQuery = parseUrl.searchParams.get('filename'); + if (filenameQuery) { + const extensionQuery = filenameQuery.split('.').pop()?.toLowerCase() || ''; + if (!documentFileType.includes(extensionQuery)) { + return; + } + } + + // Remove the origin(Make intranet requests directly) + if (requestOrigin && url.startsWith(requestOrigin)) { + url = url.replace(requestOrigin, ''); + } + } + + // Get from buffer + const fileBuffer = await MongoRawTextBuffer.findOne({ sourceId: url }, undefined, { + ...readFromSecondary + }).lean(); + if (fileBuffer) { + return formatResponseObject({ + filename: fileBuffer.metadata?.filename || url, + url, + content: fileBuffer.rawText + }); + } + + try { + // Get file buffer + const response = await axios.get(url, { + baseURL: serverRequestBaseUrl, + responseType: 'arraybuffer' + }); + + const buffer = Buffer.from(response.data, 'binary'); + + // Get file name + const filename = (() => { + const contentDisposition = response.headers['content-disposition']; + if (contentDisposition) { + const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/; + const matches = filenameRegex.exec(contentDisposition); + if (matches != null && matches[1]) { + return decodeURIComponent(matches[1].replace(/['"]/g, '')); + } + } + + return url; + })(); + // Extension + const extension = filename.split('.').pop()?.toLowerCase() || ''; + // Get encoding + const encoding = (() => { + const contentType = response.headers['content-type']; + if (contentType) { + const charsetRegex = /charset=([^;]*)/; + const matches = charsetRegex.exec(contentType); + if (matches != null && matches[1]) { + return matches[1]; + } + } + + return detectFileEncoding(buffer); + })(); + + // Read file + const { rawText } = await readRawContentByFileBuffer({ + extension, + isQAImport: false, + teamId, + buffer, + encoding + }); + + // Add to buffer + try { + if (buffer.length < 14 * 1024 * 1024 && rawText.trim()) { + MongoRawTextBuffer.create({ + sourceId: url, + rawText, + metadata: { + filename: filename + } + }); + } + } catch (error) {} + + return formatResponseObject({ filename, url, content: rawText }); + } catch (error) { + return formatResponseObject({ + filename: '', + url, + content: getErrText(error, 'Load file error') + }); + } + }) + .filter(Boolean) + ); + const text = readFilesResult.map((item) => item?.text ?? '').join('\n******\n'); + + return { + [NodeOutputKeyEnum.text]: text, + [DispatchNodeResponseKeyEnum.nodeResponse]: { + readFiles: readFilesResult.map((item) => ({ + name: item?.filename || '', + url: item?.url || '' + })), + readFilesResult: readFilesResult + .map((item) => item?.nodeResponsePreviewText ?? '') + .join('\n******\n') + }, + [DispatchNodeResponseKeyEnum.toolResponses]: { + fileContent: text + } + }; +}; diff --git a/packages/service/core/workflow/dispatchV1/chat/oneapi.ts b/packages/service/core/workflow/dispatchV1/chat/oneapi.ts index 726182b91f0b..c333082c0c51 100644 --- a/packages/service/core/workflow/dispatchV1/chat/oneapi.ts +++ b/packages/service/core/workflow/dispatchV1/chat/oneapi.ts @@ -1,10 +1,6 @@ // @ts-nocheck import type { NextApiResponse } from 'next'; -import { - filterGPTMessageByMaxTokens, - formatGPTMessagesInRequestBefore, - loadChatImgToBase64 -} from '../../../chat/utils'; +import { filterGPTMessageByMaxTokens, loadRequestMessages } from '../../../chat/utils'; import type { ChatItemType, UserChatItemValueItemType } from '@fastgpt/global/core/chat/type.d'; import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants'; @@ -146,25 +142,17 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise { - if (item.role === ChatCompletionRequestMessageRoleEnum.User) { - return { - ...item, - content: await loadChatImgToBase64(item.content) - }; - } else { - return item; - } - }) - ); + const loadMessages = await loadRequestMessages({ + messages: concatMessages, + useVision: false + }); const response = await ai.chat.completions.create( { diff --git a/packages/service/support/permission/controller.ts b/packages/service/support/permission/controller.ts index b0010a9e5d09..9be1f868f6ee 100644 --- a/packages/service/support/permission/controller.ts +++ b/packages/service/support/permission/controller.ts @@ -250,11 +250,13 @@ export const clearCookie = (res: NextApiResponse) => { }; /* file permission */ -export const createFileToken = (data: FileTokenQuery) => { +export const createFileToken = ({ + expiredTime = Math.floor(Date.now() / 1000) + 60 * 30, + ...data +}: FileTokenQuery) => { if (!process.env.FILE_TOKEN_KEY) { return Promise.reject('System unset FILE_TOKEN_KEY'); } - const expiredTime = Math.floor(Date.now() / 1000) + 60 * 30; const key = process.env.FILE_TOKEN_KEY as string; const token = jwt.sign( diff --git a/packages/web/components/common/Icon/constants.ts b/packages/web/components/common/Icon/constants.ts index bb61d857ae9e..8bb7e1f34181 100644 --- a/packages/web/components/common/Icon/constants.ts +++ b/packages/web/components/common/Icon/constants.ts @@ -80,6 +80,7 @@ export const iconPaths = { 'core/app/simpleMode/ai': () => import('./icons/core/app/simpleMode/ai.svg'), 'core/app/simpleMode/chat': () => import('./icons/core/app/simpleMode/chat.svg'), 'core/app/simpleMode/dataset': () => import('./icons/core/app/simpleMode/dataset.svg'), + 'core/app/simpleMode/file': () => import('./icons/core/app/simpleMode/file.svg'), 'core/app/simpleMode/template': () => import('./icons/core/app/simpleMode/template.svg'), 'core/app/simpleMode/tts': () => import('./icons/core/app/simpleMode/tts.svg'), 'core/app/simpleMode/variable': () => import('./icons/core/app/simpleMode/variable.svg'), diff --git a/packages/web/components/common/Icon/icons/core/app/simpleMode/file.svg b/packages/web/components/common/Icon/icons/core/app/simpleMode/file.svg new file mode 100644 index 000000000000..2f77fb12f3a3 --- /dev/null +++ b/packages/web/components/common/Icon/icons/core/app/simpleMode/file.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/packages/web/components/core/workflow/NodeInputSelect.tsx b/packages/web/components/core/workflow/NodeInputSelect.tsx index bf8a7624d7d3..6b1587d2d949 100644 --- a/packages/web/components/core/workflow/NodeInputSelect.tsx +++ b/packages/web/components/core/workflow/NodeInputSelect.tsx @@ -1,10 +1,10 @@ import React, { useMemo, useRef } from 'react'; -import MyMenu, { type Props as MyMenuProps } from '../../common/MyMenu'; +import MyMenu from '../../common/MyMenu'; import { FlowNodeInputMap, FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; -import { Box, Button, Flex, useTheme } from '@chakra-ui/react'; +import { Box, Button, useTheme } from '@chakra-ui/react'; import MyIcon from '../../common/Icon'; import { useTranslation } from 'next-i18next'; import { useConfirm } from '../../../hooks/useConfirm'; @@ -142,11 +142,13 @@ const NodeInputSelect = ({ return ( } + leftIcon={} + rightIcon={} variant={'grayBase'} border={theme.borders.base} borderRadius={'xs'} diff --git a/packages/web/hooks/useWidthVariable.ts b/packages/web/hooks/useWidthVariable.ts new file mode 100644 index 000000000000..77a185f8836a --- /dev/null +++ b/packages/web/hooks/useWidthVariable.ts @@ -0,0 +1,22 @@ +import { useMemo } from 'react'; + +export const useWidthVariable = ({ + width, + widthList = [900, 1200, 1500, 1800, 2100], + list +}: { + width: number; + widthList?: number[]; + list: T[]; +}) => { + const value = useMemo(() => { + // 根据 width 计算,找到第一个大于 width 的值 + const index = widthList.findLastIndex((item) => width > item); + if (index === -1) { + return list[0]; + } + return list[index]; + }, [list, width, widthList]); + + return value; +}; diff --git a/packages/web/i18n/en/app.json b/packages/web/i18n/en/app.json index 4aaa44401563..6e443e5cde4b 100644 --- a/packages/web/i18n/en/app.json +++ b/packages/web/i18n/en/app.json @@ -12,6 +12,7 @@ "chat_debug": "Chat Debug", "chat_logs": "Chat Logs", "chat_logs_tips": "Logs will record online, shared and API (chatId required) conversation records for this app", + "config_file_upload": "Click to configure file upload rules", "confirm_copy_app_tip": "The system will create an application with the same configuration for you, but the permission will not be copied, please confirm!", "confirm_del_app_tip": "Confirm to delete this app and all its chat records?", "confirm_delete_folder_tip": "Are you sure to delete this folder? All the following applications and corresponding chat records will be deleted, please confirm!", @@ -25,14 +26,21 @@ }, "current_settings": "Current settings", "day": "day", + "document_quote": "Document quote", + "document_quote_tip": "It is usually used to accept document content uploaded by users (which requires document parsing), and can also be used to reference other string data.", + "document_upload": "Document upload", "edit_app": "Edit app", "edit_info": "Edit info", "execute_time": "execution time", "export_config_successful": "Config copied, please check for important data", "export_configs": "Export Configs", "feedback_count": "User Feedback", + "file_upload": "file_upload", + "file_upload_tip": "After it is enabled, you can upload documents/pictures. Documents are kept for 7 days and pictures for 15 days. Use of this feature may incur additional charges. To ensure the user experience, select an AI model with a large context length when using this function.", "go_to_chat": "To chat", "go_to_run": "Run", + "image_upload": "Image upload", + "image_upload_tip": "Be sure to select a visual model that can handle the picture", "import_configs": "Import Configs", "import_configs_failed": "Failed to import configs, please ensure configs are valid!", "interval": { @@ -44,6 +52,9 @@ "per_hour": "per hour" }, "intro": "It is a large model application orchestration system that provides out-of-the-box data processing, model calling and other capabilities. It can quickly build a knowledge base and perform workflow orchestration through Flow visualization to realize complex knowledge base scenarios!", + "llm_not_support_vision": "This model does not support image recognition", + "llm_use_vision": "Enable vision", + "llm_use_vision_tip": "When image recognition is enabled, the model automatically receives images from Dialog Uploads, as well as image links from User Questions.", "logs_empty": "No logs yet~", "logs_message_total": "Total Messages", "logs_title": "Title", @@ -92,6 +103,7 @@ "Simple bot": "Simple bot", "Workflow bot": "Workflow" }, + "upload_file_max_amount": "Maximum number of files to be uploaded in a single round", "version": { "Revert success": "Revert success" }, @@ -106,8 +118,15 @@ }, "workflow": { "Input guide": "Input guide", + "file_url": "Url", + "read_files": "Documents parse", + "read_files_result": "Document parsing results", + "read_files_result_desc": "The original text of the document consists of the file name and the document content. Multiple files are separated by horizontal lines.", + "read_files_tip": "Parse the document link passed in the conversation and return the corresponding document content", "template": { "communication": "Communication" - } + }, + "user_file_input": "Files url", + "user_file_input_desc": "Links to documents and images uploaded by users" } } diff --git a/packages/web/i18n/en/chat.json b/packages/web/i18n/en/chat.json index e7af2cdfeaa5..6452c46c26a5 100644 --- a/packages/web/i18n/en/chat.json +++ b/packages/web/i18n/en/chat.json @@ -15,20 +15,23 @@ "custom_input_guide_url": "Custom thesaurus address", "empty_directory": "There is nothing left to choose from in this directory~", "in_progress": "in progress", - "input_guide": "Enter boot", - "input_guide_lexicon": "vocabulary", - "input_guide_tip": "Some preset questions can be configured. \nWhen the user enters a question, relevant questions will be obtained from these preset questions for prompts.", - "insert_input_guide,_some_data_already_exists": "There is duplicate data, which has been automatically filtered. A total of {{len}} pieces of data have been inserted.", "is_chatting": "Chatting...please wait for the end", "items": "strip", "module_runtime_and": "module run time and", "multiple_AI_conversations": "Multiple AI conversations", "new_chat": "new conversation", - "new_input_guide_lexicon": "New vocabulary", "plugins_output": "Plugin output", "question_tip": "From left to right, the response order of each module", "rearrangement": "Search results rearranged", "stream_output": "stream output", "view_citations": "View citations", - "web_site_sync": "Web site synchronization" + "web_site_sync": "Web site synchronization", + "file_amount_over": "Exceed maximum number of files {{max}}", + "input_guide": "Input guide", + "input_guide_lexicon": "Lexicon", + "input_guide_tip": "You can configure some preset questions. When the user enters a question, the relevant question is retrieved from these preset questions for prompt.", + "insert_input_guide,_some_data_already_exists": "Duplicate data, automatically filtered, insert: {{len}} data", + "new_input_guide_lexicon": "New lexicon", + "select_file": "Select file", + "select_img": "Select images" } diff --git a/packages/web/i18n/en/common.json b/packages/web/i18n/en/common.json index e30761170606..fb7065fc702f 100644 --- a/packages/web/i18n/en/common.json +++ b/packages/web/i18n/en/common.json @@ -508,7 +508,7 @@ "module cq result": "Classification result", "module extract description": "Extract requirement description", "module extract result": "Extraction result", - "module historyPreview": "Complete record", + "module historyPreview": "Record preview (only part of the content is displayed)", "module http result": "response body", "module if else Result": "Determinator result", "module limit": "Single search limit", @@ -670,7 +670,7 @@ "Total files": "A total of {{total}} files", "Training mode": "Training mode", "Upload data": "Upload data", - "Upload file progress": "File upload progress", + "Upload file progress": "file_upload progress", "Upload status": "Status", "Web link": "Web link", "Web link desc": "Read static web page content as a dataset" @@ -1330,6 +1330,7 @@ "minute": "minute" }, "unusable_variable": "no usable variable", + "upload_file_error": "Upload file error", "user": { "Account": "Account", "Amount of earnings": "Earnings (¥)", diff --git a/packages/web/i18n/en/file.json b/packages/web/i18n/en/file.json index 6a2ca690707f..50655bb82776 100644 --- a/packages/web/i18n/en/file.json +++ b/packages/web/i18n/en/file.json @@ -1,4 +1,6 @@ { + "bucket_chat": "Chat file", + "bucket_file": "Dataset file", "click_to_view_raw_source": "View source", "file_name": "File Name", "file_size": "File Size", diff --git a/packages/web/i18n/en/workflow.json b/packages/web/i18n/en/workflow.json index 529e6548f7c8..93dec792ddd9 100644 --- a/packages/web/i18n/en/workflow.json +++ b/packages/web/i18n/en/workflow.json @@ -27,7 +27,19 @@ "Code log": "Log", "Custom inputs": "Custom inputs", "Custom outputs": "Custom outputs", - "Error": "Error" + "Error": "Error", + "Read file result": "Document parsing result preview", + "read files": "parsed document" + }, + "template": { + "ai_chat": "LLM chat", + "ai_chat_intro": "Call the AI model for a conversation", + "dataset_search": "Dataset search", + "dataset_search_intro": "Call the \"Semantic Search\" and \"Full-text Search\" capabilities to find reference content that may be related to the problem from the \"Knowledge Base\"", + "system_config": "System Configuration", + "tool_call": "Tool call", + "tool_call_intro": "One or more function blocks are automatically selected for calling through the AI ​​model, and plug-ins can also be called.", + "workflow_start": "Process starts" }, "tool_input": "Tool", "update_link_error": "Update link exception", diff --git a/packages/web/i18n/zh/app.json b/packages/web/i18n/zh/app.json index 12b4157113d3..30cfed1a438b 100644 --- a/packages/web/i18n/zh/app.json +++ b/packages/web/i18n/zh/app.json @@ -13,21 +13,30 @@ "chat_debug": "调试预览", "chat_logs": "对话日志", "chat_logs_tips": "日志会记录该应用的在线、分享和 API(需填写 chatId)对话记录", + "config_file_upload": "点击配置文件上传规则", "confirm_copy_app_tip": "系统将为您创建一个相同配置应用,但权限不会进行复制,请确认!", "confirm_del_app_tip": "确认删除该应用及其所有聊天记录?", "confirm_delete_folder_tip": "确认删除该文件夹?将会删除它下面所有应用及对应的聊天记录,请确认!", "copy_one_app": "创建副本", "create_copy_success": "创建副本成功", "current_settings": "当前配置", + "document_upload": "文档上传", "edit_app": "编辑应用", "edit_info": "编辑信息", "export_config_successful": "已复制配置,自动过滤部分敏感信息,请注意检查是否仍有敏感数据", "export_configs": "导出配置", "feedback_count": "用户反馈", + "file_upload": "文件上传", + "file_upload_tip": "开启后,可以上传文档/图片。文档保留7天,图片保留15天。使用该功能可能产生较多额外费用。为保证使用体验,使用该功能时,请选择上下文长度较大的AI模型。", "go_to_chat": "去对话", "go_to_run": "去运行", + "image_upload": "图片上传", + "image_upload_tip": "请确保选择可处理图片的视觉模型", "import_configs": "导入配置", "import_configs_failed": "导入配置失败,请确保配置正常!", + "llm_not_support_vision": "该模型不支持图片识别", + "llm_use_vision": "启用图片识别", + "llm_use_vision_tip": "启用图片识别后,该模型会自动接收来自“对话框上传”的图片,以及“用户问题”中的图片链接。", "logs_empty": "还没有日志噢~", "logs_message_total": "消息总数", "logs_title": "标题", @@ -72,14 +81,22 @@ "Simple bot": "简易应用", "Workflow bot": "工作流" }, + "upload_file_max_amount": "单轮最大文件上传数量", "version": { "Revert success": "回滚成功" }, "workflow": { "Input guide": "填写说明", + "file_url": "文档链接", + "read_files": "文档解析", + "read_files_result": "文档解析结果", + "read_files_result_desc": "文档原文,由文件名和文档内容组成,多个文件之间通过横线隔开。", + "read_files_tip": "解析对话中上传的文档,返回对应文档内容", "template": { "communication": "通信" - } + }, + "user_file_input": "文件链接", + "user_file_input_desc": "用户上传的文档和图片链接" }, "interval": { "per_hour": "每小时", @@ -109,5 +126,7 @@ }, "day": "日", "execute_time": "执行时间", - "time_zone": "时区" + "time_zone": "时区", + "document_quote": "文档引用", + "document_quote_tip": "通常用于接受用户上传的文档内容(这需要文档解析),也可以用于引用其他字符串数据。" } diff --git a/packages/web/i18n/zh/chat.json b/packages/web/i18n/zh/chat.json index 1c84e8f585f7..bac8ddb67ddd 100644 --- a/packages/web/i18n/zh/chat.json +++ b/packages/web/i18n/zh/chat.json @@ -7,6 +7,7 @@ "csv_input_lexicon_tip": "仅支持 CSV 批量导入,点击下载模板", "custom_input_guide_url": "自定义词库地址", "delete_all_input_guide_confirm": "确定要清空输入引导词库吗?", + "file_amount_over": "超出最大文件数量 {{max}}", "input_guide": "输入引导", "input_guide_lexicon": "词库", "input_guide_tip": "可以配置一些预设的问题。在用户输入问题时,会从这些预设问题中获取相关问题进行提示。", @@ -30,5 +31,7 @@ "question_tip": "从上到下,为各个模块的响应顺序", "rearrangement": "检索结果重排", "web_site_sync": "Web站点同步", - "new_chat": "新对话" + "new_chat": "新对话", + "select_file": "选择文件", + "select_img": "选择图片" } diff --git a/packages/web/i18n/zh/common.json b/packages/web/i18n/zh/common.json index 9d1af2e47308..2f5349ecb774 100644 --- a/packages/web/i18n/zh/common.json +++ b/packages/web/i18n/zh/common.json @@ -559,7 +559,7 @@ "module cq result": "分类结果", "module extract description": "提取背景描述", "module extract result": "提取结果", - "module historyPreview": "完整记录", + "module historyPreview": "记录预览(仅展示部分内容)", "module http result": "响应体", "module if else Result": "判断器结果", "module limit": "单次搜索上限", @@ -646,7 +646,8 @@ "success": "开始同步" } }, - "training": {} + "training": { + } }, "data": { "Auxiliary Data": "辅助数据", @@ -857,7 +858,7 @@ "AppSecret": "AppSecret", "ChatId": "当前对话 ID", "Current time": "当前时间", - "Histories": "最近 10 条聊天记录", + "Histories": "历史记录", "Key already exists": "Key 已经存在", "Key cannot be empty": "参数名不能为空", "Props name": "参数名", @@ -1331,6 +1332,7 @@ }, "textarea_variable_picker_tip": "输入 / 可选择变量", "unusable_variable": "无可用变量", + "upload_file_error": "上传文件失败", "user": { "Account": "账号", "Amount of earnings": "收益(¥)", diff --git a/packages/web/i18n/zh/file.json b/packages/web/i18n/zh/file.json index 09923f9e1458..700856fd3b1d 100644 --- a/packages/web/i18n/zh/file.json +++ b/packages/web/i18n/zh/file.json @@ -1,9 +1,10 @@ { + "bucket_chat": "对话文件", + "bucket_file": "知识库文件", "click_to_view_raw_source": "点击查看来源", - "release_the_mouse_to_upload_the_file": "松开鼠标上传文件", - "upload_error_description": "单次只支持上传多个文件或者一个文件夹", "file_name": "文件名", "file_size": "文件大小", + "release_the_mouse_to_upload_the_file": "松开鼠标上传文件", "select_and_drag_file_tip": "点击或拖动文件到此处上传", "select_file_amount_limit": "最多选择 {{max}} 个文件", "some_file_count_exceeds_limit": "超出 {{maxCount}} 个文件,已自动截取", @@ -12,5 +13,6 @@ "support_max_count": "最多支持 {{maxCount}} 个文件", "support_max_size": "单个文件最大 {{maxSize}}", "upload_failed": "上传异常", - "reached_max_file_count": "已达到最大文件数量" + "reached_max_file_count": "已达到最大文件数量", + "upload_error_description": "单次只支持上传多个文件或者一个文件夹" } diff --git a/packages/web/i18n/zh/workflow.json b/packages/web/i18n/zh/workflow.json index 4db9aabdb6f3..18f1facc22c6 100644 --- a/packages/web/i18n/zh/workflow.json +++ b/packages/web/i18n/zh/workflow.json @@ -25,7 +25,19 @@ "Code log": "Log 日志", "Custom inputs": "自定义输入", "Custom outputs": "自定义输出", - "Error": "错误信息" + "Error": "错误信息", + "Read file result": "文档解析结果预览", + "read files": "解析的文档" + }, + "template": { + "ai_chat": "AI 对话", + "ai_chat_intro": "AI 大模型对话", + "dataset_search": "知识库搜索", + "dataset_search_intro": "调用“语义检索”和“全文检索”能力,从“知识库”中查找可能与问题相关的参考内容", + "system_config": "系统配置", + "tool_call": "工具调用", + "tool_call_intro": "通过AI模型自动选择一个或多个功能块进行调用,也可以对插件进行调用。", + "workflow_start": "流程开始" }, "tool_input": "工具参数", "variable_picker_tips": "可输入节点名或变量名搜索", diff --git a/projects/app/public/imgs/app/fileUploadPlaceholder.svg b/projects/app/public/imgs/app/fileUploadPlaceholder.svg new file mode 100644 index 000000000000..46df70c78aa7 --- /dev/null +++ b/projects/app/public/imgs/app/fileUploadPlaceholder.svg @@ -0,0 +1,167 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/projects/app/src/components/Markdown/img/Image.tsx b/projects/app/src/components/Markdown/img/Image.tsx index 8db8a14a7ec3..7c901f37c122 100644 --- a/projects/app/src/components/Markdown/img/Image.tsx +++ b/projects/app/src/components/Markdown/img/Image.tsx @@ -1,9 +1,9 @@ import React, { useState } from 'react'; -import { Skeleton } from '@chakra-ui/react'; +import { ImageProps, Skeleton } from '@chakra-ui/react'; import MyPhotoView from '@fastgpt/web/components/common/Image/PhotoView'; import { useBoolean } from 'ahooks'; -const MdImage = ({ src }: { src?: string }) => { +const MdImage = ({ src, ...props }: { src?: string } & ImageProps) => { const [isLoaded, { setTrue }] = useBoolean(false); const [renderSrc, setRenderSrc] = useState(src); @@ -31,6 +31,7 @@ const MdImage = ({ src }: { src?: string }) => { setRenderSrc('/imgs/errImg.png'); setTrue(); }} + {...props} /> ); diff --git a/projects/app/src/components/common/Textarea/MyTextarea/VariableTip.tsx b/projects/app/src/components/common/Textarea/MyTextarea/VariableTip.tsx index bd3313c4ccf3..a3179dcbf956 100644 --- a/projects/app/src/components/common/Textarea/MyTextarea/VariableTip.tsx +++ b/projects/app/src/components/common/Textarea/MyTextarea/VariableTip.tsx @@ -6,8 +6,8 @@ import { useTranslation } from 'next-i18next'; const VariableTip = (props: StackProps) => { const { t } = useTranslation(); return ( - - + + {t('common:textarea_variable_picker_tip')} ); diff --git a/projects/app/src/components/core/ai/AISettingModal/index.tsx b/projects/app/src/components/core/ai/AISettingModal/index.tsx index 511008ec8a3f..20cdd4635771 100644 --- a/projects/app/src/components/core/ai/AISettingModal/index.tsx +++ b/projects/app/src/components/core/ai/AISettingModal/index.tsx @@ -41,8 +41,11 @@ const AIChatSettingsModal = ({ }); const model = watch('model'); const showResponseAnswerText = watch(NodeInputKeyEnum.aiChatIsResponseText) !== undefined; + const showVisionSwitch = watch(NodeInputKeyEnum.aiChatVision) !== undefined; const showMaxHistoriesSlider = watch('maxHistories') !== undefined; + const useVision = watch('aiChatVision'); const selectedModel = llmModelList.find((item) => item.model === model) || llmModelList[0]; + const llmSupportVision = !!selectedModel?.vision; const tokenLimit = useMemo(() => { return llmModelList.find((item) => item.model === model)?.maxResponse || 4096; @@ -65,7 +68,7 @@ const AIChatSettingsModal = ({ alignItems: 'center', fontSize: 'sm', color: 'myGray.900', - width: ['80px', '90px'] + width: ['6rem', '8rem'] }; return ( @@ -110,26 +113,24 @@ const AIChatSettingsModal = ({ {feConfigs && ( - + {t('common:core.ai.Ai point price')} - - {t('support.wallet.Ai point every thousand tokens', { + + {t('common:support.wallet.Ai point every thousand tokens', { points: selectedModel?.charsPointsPrice || 0 })} )} - + {t('common:core.ai.Max context')} - - {selectedModel?.maxContext || 4096}Tokens - + {selectedModel?.maxContext || 4096}Tokens - + {t('common:core.ai.Support tool')} @@ -140,11 +141,11 @@ const AIChatSettingsModal = ({ : t('common:common.not_support')} - + {t('common:core.app.Temperature')} - + - + {t('common:core.app.Max tokens')} - + {showMaxHistoriesSlider && ( - + {t('common:core.app.Max histories')} - + )} {showResponseAnswerText && ( - + {t('common:core.app.Ai response')} - + { @@ -227,6 +228,29 @@ const AIChatSettingsModal = ({ )} + {showVisionSwitch && ( + + + {t('app:llm_use_vision')} + + + + {llmSupportVision ? ( + { + const value = e.target.checked; + setValue(NodeInputKeyEnum.aiChatVision, value); + }} + /> + ) : ( + + {t('app:llm_not_support_vision')} + + )} + + + )} {isOpenAIChatSetting && ( diff --git a/projects/app/src/components/core/app/FileSelect.tsx b/projects/app/src/components/core/app/FileSelect.tsx new file mode 100644 index 000000000000..94695c7dd3aa --- /dev/null +++ b/projects/app/src/components/core/app/FileSelect.tsx @@ -0,0 +1,147 @@ +import MyIcon from '@fastgpt/web/components/common/Icon'; +import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; +import { + Box, + Button, + Flex, + ModalBody, + useDisclosure, + Image, + HStack, + Switch, + ModalFooter +} from '@chakra-ui/react'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'next-i18next'; +import type { AppFileSelectConfigType } from '@fastgpt/global/core/app/type.d'; +import MyModal from '@fastgpt/web/components/common/MyModal'; +import MySlider from '@/components/Slider'; +import { defaultAppSelectFileConfig } from '@fastgpt/global/core/app/constants'; +import ChatFunctionTip from './Tip'; +import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel'; +import { useMount } from 'ahooks'; + +const FileSelect = ({ + forbidVision = false, + value = defaultAppSelectFileConfig, + onChange +}: { + forbidVision?: boolean; + value?: AppFileSelectConfigType; + onChange: (e: AppFileSelectConfigType) => void; +}) => { + const { t } = useTranslation(); + const { isOpen, onOpen, onClose } = useDisclosure(); + + const formLabel = useMemo( + () => + value.canSelectFile || value.canSelectImg + ? t('common:core.app.whisper.Open') + : t('common:core.app.whisper.Close'), + [t, value.canSelectFile, value.canSelectImg] + ); + + // Close select img switch when vision is forbidden + useMount(() => { + if (forbidVision) { + onChange({ + ...value, + canSelectImg: false + }); + } + }); + + return ( + + + {t('app:file_upload')} + + + + + + + + + {t('app:document_upload')} + { + onChange({ + ...value, + canSelectFile: e.target.checked + }); + }} + /> + + + {t('app:image_upload')} + {forbidVision ? ( + + {t('app:llm_not_support_vision')} + + ) : ( + { + onChange({ + ...value, + canSelectImg: e.target.checked + }); + }} + /> + )} + + {!forbidVision && ( + + {t('app:image_upload_tip')} + + )} + + + {t('app:upload_file_max_amount')} + + { + onChange({ + ...value, + maxFiles: e + }); + }} + /> + + + + + + + + + ); +}; + +export default FileSelect; diff --git a/projects/app/src/components/core/app/Tip.tsx b/projects/app/src/components/core/app/Tip.tsx index e803a81fa052..68e15535349d 100644 --- a/projects/app/src/components/core/app/Tip.tsx +++ b/projects/app/src/components/core/app/Tip.tsx @@ -9,7 +9,8 @@ enum FnTypeEnum { nextQuestion = 'nextQuestion', tts = 'tts', variable = 'variable', - welcome = 'welcome' + welcome = 'welcome', + file = 'file' } const ChatFunctionTip = ({ type }: { type: `${FnTypeEnum}` }) => { @@ -46,6 +47,12 @@ const ChatFunctionTip = ({ type }: { type: `${FnTypeEnum}` }) => { title: t('common:core.app.Welcome Text'), desc: t('common:core.app.tip.welcomeTextTip'), imgUrl: '/imgs/app/welcome.svg' + }, + [FnTypeEnum.file]: { + icon: '/imgs/app/welcome-icon.svg', + title: t('app:file_upload'), + desc: t('app:file_upload_tip'), + imgUrl: '/imgs/app/fileUploadPlaceholder.svg' } }); const data = map.current[type]; diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/Input/ChatInput.tsx b/projects/app/src/components/core/chat/ChatContainer/ChatBox/Input/ChatInput.tsx index b1c434962aad..1327d92798ec 100644 --- a/projects/app/src/components/core/chat/ChatContainer/ChatBox/Input/ChatInput.tsx +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/Input/ChatInput.tsx @@ -1,16 +1,14 @@ import { useSpeech } from '@/web/common/hooks/useSpeech'; import { useSystemStore } from '@/web/common/system/useSystemStore'; -import { Box, Flex, Image, Spinner, Textarea } from '@chakra-ui/react'; -import React, { useRef, useEffect, useCallback } from 'react'; +import { Box, Flex, HStack, Image, Spinner, Textarea } from '@chakra-ui/react'; +import React, { useRef, useEffect, useCallback, useMemo } from 'react'; import { useTranslation } from 'next-i18next'; import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; import MyIcon from '@fastgpt/web/components/common/Icon'; import { useSelectFile } from '@/web/common/file/hooks/useSelectFile'; -import { compressImgFileAndUpload } from '@/web/common/file/controller'; +import { uploadFile2DB } from '@/web/common/file/controller'; import { ChatFileTypeEnum } from '@fastgpt/global/core/chat/constants'; -import { addDays } from 'date-fns'; -import { useRequest } from '@fastgpt/web/hooks/useRequest'; -import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants'; +import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { ChatBoxInputFormType, ChatBoxInputType, UserInputFileItemType } from '../type'; import { textareaMinH } from '../constants'; import { UseFormReturn, useFieldArray } from 'react-hook-form'; @@ -19,103 +17,167 @@ import dynamic from 'next/dynamic'; import { useContextSelector } from 'use-context-selector'; import { getNanoid } from '@fastgpt/global/common/string/tools'; import { useSystem } from '@fastgpt/web/hooks/useSystem'; +import { documentFileType } from '@fastgpt/global/common/file/constants'; +import { getFileIcon } from '@fastgpt/global/common/file/icon'; +import { useToast } from '@fastgpt/web/hooks/useToast'; +import { clone } from 'lodash'; +import { formatFileSize } from '@fastgpt/global/common/file/tools'; const InputGuideBox = dynamic(() => import('./InputGuideBox')); +const fileTypeFilter = (file: File) => { + return ( + file.type.includes('image') || + documentFileType.split(',').some((type) => file.name.endsWith(type.trim())) + ); +}; + const ChatInput = ({ onSendMessage, onStop, TextareaDom, - showFileSelector = false, resetInputVal, chatForm, appId }: { onSendMessage: (val: ChatBoxInputType & { autoTTSResponse?: boolean }) => void; onStop: () => void; - showFileSelector?: boolean; TextareaDom: React.MutableRefObject; resetInputVal: (val: ChatBoxInputType) => void; chatForm: UseFormReturn; appId: string; }) => { + const { isPc } = useSystem(); + const { toast } = useToast(); + const { t } = useTranslation(); + const { feConfigs } = useSystemStore(); + const { setValue, watch, control } = chatForm; const inputValue = watch('input'); const { - update: updateFile, - remove: removeFile, + update: updateFiles, + remove: removeFiles, fields: fileList, - append: appendFile, - replace: replaceFile + replace: replaceFiles } = useFieldArray({ control, name: 'files' }); - const { isChatting, whisperConfig, autoTTSResponse, chatInputGuide, outLinkAuthData } = - useContextSelector(ChatBoxContext, (v) => v); - const { whisperModel } = useSystemStore(); - const { isPc } = useSystem(); - - const canvasRef = useRef(null); - const { t } = useTranslation(); + const { + chatId, + isChatting, + whisperConfig, + autoTTSResponse, + chatInputGuide, + outLinkAuthData, + fileSelectConfig + } = useContextSelector(ChatBoxContext, (v) => v); const havInput = !!inputValue || fileList.length > 0; const hasFileUploading = fileList.some((item) => !item.url); const canSendMessage = havInput && !hasFileUploading; + const showSelectFile = fileSelectConfig.canSelectFile; + const showSelectImg = fileSelectConfig.canSelectImg; + const maxSelectFiles = fileSelectConfig.maxFiles ?? 10; + const maxSize = (feConfigs?.uploadFileMaxSize || 1024) * 1024 * 1024; // nkb + const { icon: selectFileIcon, tooltip: selectFileTip } = useMemo(() => { + if (showSelectFile) { + return { + icon: 'core/chat/fileSelect', + tooltip: t('chat:select_file') + }; + } else if (showSelectImg) { + return { + icon: 'core/chat/fileSelect', + tooltip: t('chat:select_img') + }; + } + return {}; + }, [showSelectFile, showSelectImg, t]); + /* file selector and upload */ const { File, onOpen: onOpenSelectFile } = useSelectFile({ - fileType: 'image/*', + fileType: `${showSelectImg ? 'image/*,' : ''} ${showSelectFile ? documentFileType : ''}`, multiple: true, - maxCount: 10 + maxCount: maxSelectFiles }); - const { mutate: uploadFile } = useRequest({ - mutationFn: async ({ file, fileIndex }: { file: UserInputFileItemType; fileIndex: number }) => { - if (file.type === ChatFileTypeEnum.image && file.rawFile) { + useRequest2( + async () => { + const filterFiles = fileList.filter((item) => item.status === 0); + + if (filterFiles.length === 0) return; + + replaceFiles(fileList.map((item) => ({ ...item, status: 1 }))); + + for (const file of filterFiles) { + if (!file.rawFile) continue; + try { - const url = await compressImgFileAndUpload({ - type: MongoImageTypeEnum.chatImage, + const { fileId, previewUrl } = await uploadFile2DB({ file: file.rawFile, - maxW: 4320, - maxH: 4320, - maxSize: 1024 * 1024 * 16, - // 7 day expired. - expiredTime: addDays(new Date(), 7), - ...outLinkAuthData + bucketName: 'chat', + metadata: { + chatId + } }); - updateFile(fileIndex, { + + updateFiles(fileList.findIndex((item) => item.id === file.id)!, { ...file, - url + status: 1, + url: `${location.origin}${previewUrl}` }); } catch (error) { - removeFile(fileIndex); + removeFiles(fileList.findIndex((item) => item.id === file.id)!); console.log(error); return Promise.reject(error); } } }, - errorToast: t('common:common.Upload File Failed') - }); + { + manual: false, + errorToast: t('common:upload_file_error'), + refreshDeps: [fileList] + } + ); const onSelectFile = useCallback( async (files: File[]) => { if (!files || files.length === 0) { return; } + // filter max files + if (fileList.length + files.length > maxSelectFiles) { + files = files.slice(0, maxSelectFiles - fileList.length); + toast({ + status: 'warning', + title: t('chat:file_amount_over', { max: maxSelectFiles }) + }); + } + + const filterFilesByMaxSize = files.filter((file) => file.size <= maxSize); + if (filterFilesByMaxSize.length < files.length) { + toast({ + status: 'warning', + title: t('file:some_file_size_exceeds_limit', { maxSize: formatFileSize(maxSize) }) + }); + } + const loadFiles = await Promise.all( - files.map( + filterFilesByMaxSize.map( (file) => new Promise((resolve, reject) => { if (file.type.includes('image')) { const reader = new FileReader(); reader.readAsDataURL(file); reader.onload = () => { - const item = { + const item: UserInputFileItemType = { id: getNanoid(6), rawFile: file, type: ChatFileTypeEnum.image, name: file.name, - icon: reader.result as string + icon: reader.result as string, + status: 0 }; resolve(item); }; @@ -128,22 +190,28 @@ const ChatInput = ({ rawFile: file, type: ChatFileTypeEnum.file, name: file.name, - icon: 'file/pdf' + icon: getFileIcon(file.name), + status: 0 }); } }) ) ); - appendFile(loadFiles); - loadFiles.forEach((file, i) => - uploadFile({ - file, - fileIndex: i + fileList.length + // Document, image + const concatFileList = clone( + fileList.concat(loadFiles).sort((a, b) => { + if (a.type === ChatFileTypeEnum.image && b.type === ChatFileTypeEnum.file) { + return 1; + } else if (a.type === ChatFileTypeEnum.file && b.type === ChatFileTypeEnum.image) { + return -1; + } + return 0; }) ); + replaceFiles(concatFileList); }, - [appendFile, fileList.length, uploadFile] + [fileList, maxSelectFiles, replaceFiles, toast, t] ); /* on send */ @@ -155,10 +223,12 @@ const ChatInput = ({ text: textareaValue.trim(), files: fileList }); - replaceFile([]); + replaceFiles([]); }; /* whisper init */ + const { whisperModel } = useSystemStore(); + const canvasRef = useRef(null); const { isSpeaking, isTransCription, @@ -194,12 +264,12 @@ const ChatInput = ({ files: fileList, autoTTSResponse }); - replaceFile([]); + replaceFiles([]); } else { resetInputVal({ text }); } }, - [autoTTSResponse, fileList, onSendMessage, replaceFile, resetInputVal, whisperConfig?.autoSend] + [autoTTSResponse, fileList, onSendMessage, replaceFiles, resetInputVal, whisperConfig?.autoSend] ); const onWhisperRecord = useCallback(() => { if (isSpeaking) { @@ -261,13 +331,20 @@ const ChatInput = ({ {/* file preview */} - + 0 ? 2 : 0} + > {fileList.map((item, index) => ( { - removeFile(index); + removeFiles(index); }} className="close-icon" display={['', 'none']} @@ -312,19 +389,27 @@ const ChatInput = ({ {'img'} )} + {item.type === ChatFileTypeEnum.file && ( + + + + {item.name} + + + )} ))} 0 ? 1 : 0} pl={[2, 4]}> {/* file selector */} - {showFileSelector && ( + {(showSelectFile || showSelectImg) && ( - - + + @@ -404,12 +489,19 @@ const ChatInput = ({ }} onPaste={(e) => { const clipboardData = e.clipboardData; - if (clipboardData && showFileSelector) { + if (clipboardData && (showSelectFile || showSelectImg)) { const items = clipboardData.items; const files = Array.from(items) .map((item) => (item.kind === 'file' ? item.getAsFile() : undefined)) - .filter(Boolean) as File[]; + .filter((file) => { + console.log(file); + return file && fileTypeFilter(file); + }) as File[]; onSelectFile(files); + + if (files.length > 0) { + e.stopPropagation(); + } } }} /> diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/Provider.tsx b/projects/app/src/components/core/chat/ChatContainer/ChatBox/Provider.tsx index e9dc272c8be6..064813c10232 100644 --- a/projects/app/src/components/core/chat/ChatContainer/ChatBox/Provider.tsx +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/Provider.tsx @@ -3,6 +3,7 @@ import { useAudioPlay } from '@/web/common/utils/voice'; import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat'; import { AppChatConfigType, + AppFileSelectConfigType, AppTTSConfigType, AppWhisperConfigType, ChatInputGuideConfigType, @@ -10,6 +11,7 @@ import { } from '@fastgpt/global/core/app/type'; import { ChatHistoryItemResType, ChatSiteItemType } from '@fastgpt/global/core/chat/type'; import { + defaultAppSelectFileConfig, defaultChatInputGuideConfig, defaultTTSConfig, defaultWhisperConfig @@ -64,6 +66,7 @@ type useChatStoreType = OutLinkChatAuthProps & chatInputGuide: ChatInputGuideConfigType; outLinkAuthData: OutLinkChatAuthProps; getHistoryResponseData: ({ dataId }: { dataId: string }) => Promise; + fileSelectConfig: AppFileSelectConfigType; }; export const ChatBoxContext = createContext({ @@ -146,7 +149,8 @@ const Provider = ({ questionGuide = false, ttsConfig = defaultTTSConfig, whisperConfig = defaultWhisperConfig, - chatInputGuide = defaultChatInputGuideConfig + chatInputGuide = defaultChatInputGuideConfig, + fileSelectConfig = defaultAppSelectFileConfig } = useMemo(() => chatConfig, [chatConfig]); const outLinkAuthData = useMemo( @@ -215,6 +219,7 @@ const Provider = ({ allVariableList: variables, questionGuide, ttsConfig, + fileSelectConfig, whisperConfig, autoTTSResponse, startSegmentedAudio, diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/ChatItem.tsx b/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/ChatItem.tsx index 801c3f290b0c..0a46174c52ce 100644 --- a/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/ChatItem.tsx +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/ChatItem.tsx @@ -73,12 +73,11 @@ const ChatItem = ({ const ContentCard = useMemo(() => { if (type === 'Human') { const { text, files = [] } = formatChatValue2InputType(chat.value); - return ( - <> + {files.length > 0 && } - - + {text && } + ); } diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/FilesBox.tsx b/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/FilesBox.tsx index 29d5c93d2318..8763d29930b5 100644 --- a/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/FilesBox.tsx +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/FilesBox.tsx @@ -1,22 +1,89 @@ -import { Box, Flex, Grid } from '@chakra-ui/react'; +import { Box, Flex, Grid, Text } from '@chakra-ui/react'; import MdImage from '@/components/Markdown/img/Image'; import { UserInputFileItemType } from '@/components/core/chat/ChatContainer/ChatBox/type'; +import MyIcon from '@fastgpt/web/components/common/Icon'; +import React, { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'; +import { clone } from 'lodash'; +import { ChatFileTypeEnum } from '@fastgpt/global/core/chat/constants'; +import { useSystem } from '@fastgpt/web/hooks/useSystem'; +import { useWidthVariable } from '@fastgpt/web/hooks/useWidthVariable'; const FilesBlock = ({ files }: { files: UserInputFileItemType[] }) => { + const chartRef = useRef(null); + const [width, setWidth] = useState(400); + const { isPc } = useSystem(); + const gridColumns = useWidthVariable({ + width, + widthList: [300, 500, 700], + list: ['1fr', 'repeat(2, 1fr)', 'repeat(3, 1fr)'] + }); + + // sort files, file->image + const sortFiles = useMemo(() => { + return clone(files).sort((a, b) => { + if (a.type === ChatFileTypeEnum.image && b.type === ChatFileTypeEnum.file) { + return 1; + } else if (a.type === ChatFileTypeEnum.file && b.type === ChatFileTypeEnum.image) { + return -1; + } + return 0; + }); + }, [files]); + + const computedChatItemWidth = useCallback(() => { + if (!chartRef.current) return; + + // 一直找到 parent = markdown 的元素 + let parent = chartRef.current?.parentElement; + while (parent && !parent.className.includes('chat-box-card')) { + parent = parent.parentElement; + } + + const clientWidth = parent?.clientWidth ?? 400; + setWidth(clientWidth); + return parent; + }, [isPc]); + + useLayoutEffect(() => { + computedChatItemWidth(); + }, [computedChatItemWidth]); + return ( - - {files.map(({ id, type, name, url }, i) => { - if (type === 'image') { - return ( - - - - ); - } - return null; - })} + + {sortFiles.map(({ id, type, name, url, icon }, i) => ( + + {type === 'image' && } + {type === 'file' && ( + { + window.open(url); + }} + > + + + {name || url} + + + )} + + ))} ); }; -export default FilesBlock; +export default React.memo(FilesBlock); diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/index.tsx b/projects/app/src/components/core/chat/ChatContainer/ChatBox/index.tsx index db55d3955293..76d85c80e7ae 100644 --- a/projects/app/src/components/core/chat/ChatContainer/ChatBox/index.tsx +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/index.tsx @@ -75,7 +75,6 @@ type Props = OutLinkChatAuthProps & showVoiceIcon?: boolean; showEmptyIntro?: boolean; userAvatar?: string; - showFileSelector?: boolean; active?: boolean; // can use appId: string; @@ -105,7 +104,6 @@ const ChatBox = ( showEmptyIntro = false, appAvatar, userAvatar, - showFileSelector, active = true, appId, chatId, @@ -378,7 +376,9 @@ const ChatBox = ( return; } + // Abort the previous request abortRequest(); + questionGuideController.current?.abort('stop'); text = text.trim(); @@ -390,14 +390,13 @@ const ChatBox = ( return; } - // delete invalid variables, 只保留在 variableList 中的变量 + // Only declared variables are kept const requestVariables: Record = {}; allVariableList?.forEach((item) => { requestVariables[item.key] = variables[item.key] || ''; }); const responseChatId = getNanoid(24); - questionGuideController.current?.abort('stop'); // set auto audio playing if (autoTTSResponse) { @@ -980,7 +979,6 @@ const ChatBox = ( onStop={() => chatController.current?.abort('stop')} TextareaDom={TextareaDom} resetInputVal={resetInputVal} - showFileSelector={showFileSelector} chatForm={chatForm} appId={appId} /> diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/type.d.ts b/projects/app/src/components/core/chat/ChatContainer/ChatBox/type.d.ts index 489070c55b9b..582d224d38ac 100644 --- a/projects/app/src/components/core/chat/ChatContainer/ChatBox/type.d.ts +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/type.d.ts @@ -13,6 +13,7 @@ export type UserInputFileItemType = { type: `${ChatFileTypeEnum}`; name: string; icon: string; // img is base64 + status: 0 | 1; // 0: uploading, 1: success url?: string; }; diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/utils.ts b/projects/app/src/components/core/chat/ChatContainer/ChatBox/utils.ts index 0b45e1084051..eded8c7e2693 100644 --- a/projects/app/src/components/core/chat/ChatContainer/ChatBox/utils.ts +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/utils.ts @@ -1,6 +1,7 @@ import { ChatItemValueItemType } from '@fastgpt/global/core/chat/type'; import { ChatBoxInputType, UserInputFileItemType } from './type'; import { getNanoid } from '@fastgpt/global/common/string/tools'; +import { getFileIcon } from '@fastgpt/global/common/file/icon'; export const formatChatValue2InputType = (value?: ChatItemValueItemType[]): ChatBoxInputType => { if (!value) { @@ -15,15 +16,16 @@ export const formatChatValue2InputType = (value?: ChatItemValueItemType[]): Chat .filter((item) => item.text?.content) .map((item) => item.text?.content || '') .join(''); + const files = (value - .map((item) => + ?.map((item) => item.type === 'file' && item.file ? { - id: getNanoid(), + id: item.file.url, type: item.file.type, name: item.file.name, - icon: '', + icon: getFileIcon(item.file.name), url: item.file.url } : undefined diff --git a/projects/app/src/components/core/chat/components/AIResponseBox.tsx b/projects/app/src/components/core/chat/components/AIResponseBox.tsx index 3c04c0b15fc9..dbca892b2af8 100644 --- a/projects/app/src/components/core/chat/components/AIResponseBox.tsx +++ b/projects/app/src/components/core/chat/components/AIResponseBox.tsx @@ -105,18 +105,18 @@ ${JSON.stringify(questionGuides)}`; overflowY={'auto'} > {toolParams && toolParams !== '{}' && ( - + + /> + )} {toolResponse && ( - - - + /> )} diff --git a/projects/app/src/components/core/chat/components/WholeResponseModal.tsx b/projects/app/src/components/core/chat/components/WholeResponseModal.tsx index ccfeffa17b05..d989347b3f32 100644 --- a/projects/app/src/components/core/chat/components/WholeResponseModal.tsx +++ b/projects/app/src/components/core/chat/components/WholeResponseModal.tsx @@ -1,5 +1,5 @@ import React, { useMemo, useState } from 'react'; -import { Box, Flex, BoxProps, useDisclosure } from '@chakra-ui/react'; +import { Box, Flex, BoxProps, useDisclosure, HStack } from '@chakra-ui/react'; import type { ChatHistoryItemResType } from '@fastgpt/global/core/chat/type.d'; import { useTranslation } from 'next-i18next'; import { moduleTemplatesFlat } from '@fastgpt/global/core/workflow/template/constants'; @@ -16,6 +16,7 @@ import MyIcon from '@fastgpt/web/components/common/Icon'; import { useContextSelector } from 'use-context-selector'; import { ChatBoxContext } from '../ChatContainer/ChatBox/Provider'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; +import { getFileIcon } from '@fastgpt/global/common/file/icon'; type sideTabItemType = { moduleLogo?: string; @@ -34,7 +35,7 @@ function RowRender({ }: { children: React.ReactNode; label: string } & BoxProps) { return ( - + {label}: @@ -435,9 +436,50 @@ export const WholeResponseContent = ({ value={activeModule?.textOutput} /> {/* code */} - - - + <> + + + + + + {/* read files */} + <> + {activeModule?.readFiles && activeModule?.readFiles.length > 0 && ( + + {activeModule?.readFiles.map((file, i) => ( + window.open(file.url) + } + : {})} + > + + {file.name} + + ))} + + } + /> + )} + + )} diff --git a/projects/app/src/global/core/workflow/api.d.ts b/projects/app/src/global/core/workflow/api.d.ts index d2042047bf9e..dcc892a6db99 100644 --- a/projects/app/src/global/core/workflow/api.d.ts +++ b/projects/app/src/global/core/workflow/api.d.ts @@ -1,3 +1,4 @@ +import { AppSchema } from '@fastgpt/global/core/app/type'; import { ChatHistoryItemResType } from '@fastgpt/global/core/chat/type'; import { RuntimeNodeItemType } from '@fastgpt/global/core/workflow/runtime/type'; import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type'; diff --git a/projects/app/src/pages/api/common/file/read.ts b/projects/app/src/pages/api/common/file/read.ts index 6503a6f4c599..8989139fa688 100644 --- a/projects/app/src/pages/api/common/file/read.ts +++ b/projects/app/src/pages/api/common/file/read.ts @@ -38,7 +38,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< })(); res.setHeader('Content-Type', `${file.contentType}; charset=${encoding}`); - res.setHeader('Cache-Control', 'public, max-age=3600'); + res.setHeader('Cache-Control', 'public, max-age=31536000'); res.setHeader('Content-Disposition', `inline; filename="${encodeURIComponent(file.filename)}"`); stream.pipe(res); diff --git a/projects/app/src/pages/api/common/file/upload.ts b/projects/app/src/pages/api/common/file/upload.ts index 971e9a976394..a26ca552bf42 100644 --- a/projects/app/src/pages/api/common/file/upload.ts +++ b/projects/app/src/pages/api/common/file/upload.ts @@ -1,12 +1,14 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import { jsonRes } from '@fastgpt/service/common/response'; -import { connectToDatabase } from '@/service/mongo'; import { authCert } from '@fastgpt/service/support/permission/auth/common'; import { uploadFile } from '@fastgpt/service/common/file/gridfs/controller'; import { getUploadModel } from '@fastgpt/service/common/file/multer'; import { removeFilesByPaths } from '@fastgpt/service/common/file/utils'; +import { NextAPI } from '@/service/middleware/entry'; +import { createFileToken } from '@fastgpt/service/support/permission/controller'; +import { ReadFileBaseUrl } from '@fastgpt/global/common/file/constants'; -export default async function handler(req: NextApiRequest, res: NextApiResponse) { +async function handler(req: NextApiRequest, res: NextApiResponse) { /* Creates the multer uploader */ const upload = getUploadModel({ maxSize: (global.feConfigs?.uploadFileMaxSize || 500) * 1024 * 1024 @@ -14,11 +16,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< const filePaths: string[] = []; try { - await connectToDatabase(); const { file, bucketName, metadata } = await upload.doUpload(req, res); - filePaths.push(file.path); - const { teamId, tmbId } = await authCert({ req, authToken: true }); if (!bucketName) { @@ -35,8 +34,21 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< metadata: metadata }); - jsonRes(res, { - data: fileId + jsonRes<{ + fileId: string; + previewUrl: string; + }>(res, { + data: { + fileId, + previewUrl: `${ReadFileBaseUrl}?filename=${file.originalname}&token=${await createFileToken( + { + bucketName, + teamId, + tmbId, + fileId + } + )}` + } }); } catch (error) { jsonRes(res, { @@ -48,6 +60,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< removeFilesByPaths(filePaths); } +export default NextAPI(handler); + export const config = { api: { bodyParser: false diff --git a/projects/app/src/pages/api/core/app/del.ts b/projects/app/src/pages/api/core/app/del.ts index 66d2db38dd53..d4386da1bb18 100644 --- a/projects/app/src/pages/api/core/app/del.ts +++ b/projects/app/src/pages/api/core/app/del.ts @@ -15,6 +15,7 @@ import { import { findAppAndAllChildren } from '@fastgpt/service/core/app/controller'; import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema'; import { ClientSession } from '@fastgpt/service/common/mongo'; +import { deleteChatFiles } from '@fastgpt/service/core/chat/controller'; async function handler(req: NextApiRequest, res: NextApiResponse) { const { appId } = req.query as { appId: string }; @@ -53,6 +54,7 @@ export const onDelOneApp = async ({ for await (const app of apps) { const appId = app._id; // Chats + await deleteChatFiles({ appId }); await MongoChatItem.deleteMany( { appId diff --git a/projects/app/src/pages/api/core/chat/chatTest.ts b/projects/app/src/pages/api/core/chat/chatTest.ts index 6556b22b2126..10383f2fa3d1 100644 --- a/projects/app/src/pages/api/core/chat/chatTest.ts +++ b/projects/app/src/pages/api/core/chat/chatTest.ts @@ -21,6 +21,7 @@ import { import { NextAPI } from '@/service/middleware/entry'; import { GPTMessages2Chats } from '@fastgpt/global/core/chat/adapt'; import { ChatCompletionMessageParam } from '@fastgpt/global/core/ai/type'; +import { AppChatConfigType } from '@fastgpt/global/core/app/type'; export type Props = { messages: ChatCompletionMessageParam[]; @@ -29,6 +30,7 @@ export type Props = { variables: Record; appId: string; appName: string; + chatConfig: AppChatConfigType; }; async function handler(req: NextApiRequest, res: NextApiResponse) { @@ -40,7 +42,15 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { res.end(); }); - let { nodes = [], edges = [], messages = [], variables = {}, appName, appId } = req.body as Props; + let { + nodes = [], + edges = [], + messages = [], + variables = {}, + appName, + appId, + chatConfig + } = req.body as Props; try { // [histories, user] const chatMessages = GPTMessages2Chats(messages); @@ -79,6 +89,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { /* start process */ const { flowResponses, flowUsages } = await dispatchWorkFlow({ res, + requestOrigin: req.headers.origin, mode: 'test', teamId, tmbId, @@ -88,6 +99,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { runtimeEdges: edges, variables, query: removeEmptyUserInput(userInput), + chatConfig, histories: chatMessages, stream: true, detail: true, diff --git a/projects/app/src/pages/api/core/chat/clearHistories.ts b/projects/app/src/pages/api/core/chat/clearHistories.ts index fd8ce18fb310..acbc455f450d 100644 --- a/projects/app/src/pages/api/core/chat/clearHistories.ts +++ b/projects/app/src/pages/api/core/chat/clearHistories.ts @@ -1,6 +1,5 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import { jsonRes } from '@fastgpt/service/common/response'; -import { connectToDatabase } from '@/service/mongo'; import { authCert } from '@fastgpt/service/support/permission/auth/common'; import { MongoChat } from '@fastgpt/service/core/chat/chatSchema'; import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema'; @@ -8,64 +7,71 @@ import { ClearHistoriesProps } from '@/global/core/chat/api'; import { authOutLink } from '@/service/support/permission/auth/outLink'; import { ChatSourceEnum } from '@fastgpt/global/core/chat/constants'; import { authTeamSpaceToken } from '@/service/support/permission/auth/team'; +import { NextAPI } from '@/service/middleware/entry'; +import { deleteChatFiles } from '@fastgpt/service/core/chat/controller'; +import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun'; /* clear chat history */ -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - try { - await connectToDatabase(); - const { appId, shareId, outLinkUid, teamId, teamToken } = req.query as ClearHistoriesProps; +async function handler(req: NextApiRequest, res: NextApiResponse) { + const { appId, shareId, outLinkUid, teamId, teamToken } = req.query as ClearHistoriesProps; - let chatAppId = appId; + let chatAppId = appId!; - const match = await (async () => { - if (shareId && outLinkUid) { - const { appId, uid } = await authOutLink({ shareId, outLinkUid }); + const match = await (async () => { + if (shareId && outLinkUid) { + const { appId, uid } = await authOutLink({ shareId, outLinkUid }); - chatAppId = appId; - return { - shareId, - outLinkUid: uid - }; - } - if (teamId && teamToken) { - const { uid } = await authTeamSpaceToken({ teamId, teamToken }); - return { - teamId, - appId, - outLinkUid: uid - }; - } - if (appId) { - const { tmbId } = await authCert({ req, authToken: true }); + chatAppId = appId; + return { + shareId, + outLinkUid: uid + }; + } + if (teamId && teamToken) { + const { uid } = await authTeamSpaceToken({ teamId, teamToken }); + return { + teamId, + appId, + outLinkUid: uid + }; + } + if (appId) { + const { tmbId } = await authCert({ req, authToken: true }); - return { - tmbId, - appId, - source: ChatSourceEnum.online - }; - } + return { + tmbId, + appId, + source: ChatSourceEnum.online + }; + } - return Promise.reject('Param are error'); - })(); + return Promise.reject('Param are error'); + })(); - // find chatIds - const list = await MongoChat.find(match, 'chatId').lean(); - const idList = list.map((item) => item.chatId); + // find chatIds + const list = await MongoChat.find(match, 'chatId').lean(); + const idList = list.map((item) => item.chatId); - await MongoChatItem.deleteMany({ - appId: chatAppId, - chatId: { $in: idList } - }); - await MongoChat.deleteMany({ - appId: chatAppId, - chatId: { $in: idList } - }); + await deleteChatFiles({ chatIdList: idList }); - jsonRes(res); - } catch (err) { - jsonRes(res, { - code: 500, - error: err - }); - } + await mongoSessionRun(async (session) => { + await MongoChatItem.deleteMany( + { + appId: chatAppId, + chatId: { $in: idList } + }, + { session } + ); + await MongoChat.deleteMany( + { + appId: chatAppId, + chatId: { $in: idList } + }, + { session } + ); + }); + + jsonRes(res); } + +export default NextAPI(handler); diff --git a/projects/app/src/pages/api/core/chat/delHistory.ts b/projects/app/src/pages/api/core/chat/delHistory.ts index 4b0d01ffec66..b68a21ba0c2f 100644 --- a/projects/app/src/pages/api/core/chat/delHistory.ts +++ b/projects/app/src/pages/api/core/chat/delHistory.ts @@ -1,4 +1,4 @@ -import type { NextApiRequest, NextApiResponse } from 'next'; +import type { NextApiResponse } from 'next'; import { jsonRes } from '@fastgpt/service/common/response'; import { MongoChat } from '@fastgpt/service/core/chat/chatSchema'; import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema'; @@ -8,6 +8,7 @@ import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun'; import { NextAPI } from '@/service/middleware/entry'; import { ApiRequestProps } from '@fastgpt/service/type/next'; import { WritePermissionVal } from '@fastgpt/global/support/permission/constant'; +import { deleteChatFiles } from '@fastgpt/service/core/chat/controller'; /* clear chat history */ async function handler(req: ApiRequestProps<{}, DelHistoryProps>, res: NextApiResponse) { @@ -20,6 +21,7 @@ async function handler(req: ApiRequestProps<{}, DelHistoryProps>, res: NextApiRe per: WritePermissionVal }); + await deleteChatFiles({ chatIdList: [chatId] }); await mongoSessionRun(async (session) => { await MongoChatItem.deleteMany( { @@ -28,7 +30,7 @@ async function handler(req: ApiRequestProps<{}, DelHistoryProps>, res: NextApiRe }, { session } ); - await MongoChat.findOneAndRemove( + await MongoChat.deleteOne( { appId, chatId diff --git a/projects/app/src/pages/api/core/workflow/debug.ts b/projects/app/src/pages/api/core/workflow/debug.ts index b444b5a93eaa..6ffdc8fcb8e6 100644 --- a/projects/app/src/pages/api/core/workflow/debug.ts +++ b/projects/app/src/pages/api/core/workflow/debug.ts @@ -41,6 +41,7 @@ async function handler( /* start process */ const { flowUsages, flowResponses, debugResponse } = await dispatchWorkFlow({ res, + requestOrigin: req.headers.origin, mode: 'debug', teamId, tmbId, @@ -50,6 +51,7 @@ async function handler( runtimeEdges: edges, variables, query: [], + chatConfig: defaultApp.chatConfig, histories: [], stream: false, detail: true, diff --git a/projects/app/src/pages/api/v1/chat/completions.ts b/projects/app/src/pages/api/v1/chat/completions.ts index 975702de5802..584566719c88 100644 --- a/projects/app/src/pages/api/v1/chat/completions.ts +++ b/projects/app/src/pages/api/v1/chat/completions.ts @@ -249,6 +249,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { if (app.version === 'v2') { return dispatchWorkFlow({ res, + requestOrigin: req.headers.origin, mode: 'chat', user, teamId: String(teamId), @@ -260,6 +261,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { runtimeEdges: initWorkflowEdgeStatus(edges), variables: runtimeVariables, query: removeEmptyUserInput(userQuestion.value), + chatConfig, histories: newHistories, stream, detail, diff --git a/projects/app/src/pages/app/detail/components/SimpleApp/ChatTest.tsx b/projects/app/src/pages/app/detail/components/SimpleApp/ChatTest.tsx index 591f34290aa8..56f88ef5399f 100644 --- a/projects/app/src/pages/app/detail/components/SimpleApp/ChatTest.tsx +++ b/projects/app/src/pages/app/detail/components/SimpleApp/ChatTest.tsx @@ -27,9 +27,10 @@ const ChatTest = ({ appForm }: { appForm: AppSimpleEditFormType }) => { }); useEffect(() => { - const { nodes, edges } = form2AppWorkflow(appForm); + const { nodes, edges } = form2AppWorkflow(appForm, t); + // console.log(form2AppWorkflow(appForm, t)); setWorkflowData({ nodes, edges }); - }, [appForm, setWorkflowData, allDatasets]); + }, [appForm, setWorkflowData, allDatasets, t]); const { restartChat, ChatContainer } = useChatTest({ ...workflowData, diff --git a/projects/app/src/pages/app/detail/components/SimpleApp/EditForm.tsx b/projects/app/src/pages/app/detail/components/SimpleApp/EditForm.tsx index 43358260bc0f..5481d7c61f05 100644 --- a/projects/app/src/pages/app/detail/components/SimpleApp/EditForm.tsx +++ b/projects/app/src/pages/app/detail/components/SimpleApp/EditForm.tsx @@ -47,6 +47,7 @@ const ScheduledTriggerConfig = dynamic( () => import('@/components/core/app/ScheduledTriggerConfig') ); const WelcomeTextConfig = dynamic(() => import('@/components/core/app/WelcomeTextConfig')); +const FileSelectConfig = dynamic(() => import('@/components/core/app/FileSelect')); const BoxStyles: BoxProps = { px: [4, 6], @@ -120,11 +121,11 @@ const EditForm = ({ [appForm.chatConfig.variables, t] ); + const selectedModel = + llmModelList.find((item) => item.model === appForm.aiSettings.model) ?? llmModelList[0]; const tokenLimit = useMemo(() => { - return ( - llmModelList.find((item) => item.model === appForm.aiSettings.model)?.quoteMaxToken || 3000 - ); - }, [llmModelList, appForm.aiSettings.model]); + return selectedModel.quoteMaxToken || 3000; + }, [selectedModel.quoteMaxToken]); return ( <> @@ -338,6 +339,23 @@ const EditForm = ({ + {/* File select */} + + { + setAppForm((state) => ({ + ...state, + chatConfig: { + ...state.chatConfig, + fileSelectConfig: e + } + })); + }} + /> + + {/* variable */} { - const data = form2AppWorkflow(appForm); + const data = form2AppWorkflow(appForm, t); return compareWorkflow( { @@ -66,11 +65,11 @@ const Header = ({ chatConfig: data.chatConfig } ); - }, [appDetail.chatConfig, appDetail.modules, appForm]); + }, [appDetail.chatConfig, appDetail.modules, appForm, t]); const onSubmitPublish = useCallback( async (data: AppSimpleEditFormType) => { - const { nodes, edges } = form2AppWorkflow(data); + const { nodes, edges } = form2AppWorkflow(data, t); await onPublish({ nodes, edges, @@ -78,7 +77,7 @@ const Header = ({ type: AppTypeEnum.simple }); }, - [onPublish] + [onPublish, t] ); const [historiesDefaultData, setHistoriesDefaultData] = useState(); @@ -119,9 +118,11 @@ const Header = ({ : publishStatusStyle.unPublish.colorSchema } > - {isPublished - ? publishStatusStyle.published.text - : publishStatusStyle.unPublish.text} + {t( + isPublished + ? publishStatusStyle.published.text + : publishStatusStyle.unPublish.text + )} )} @@ -133,7 +134,7 @@ const Header = ({ w={'30px'} variant={'whitePrimary'} onClick={() => { - const { nodes, edges } = form2AppWorkflow(appForm); + const { nodes, edges } = form2AppWorkflow(appForm, t); setHistoriesDefaultData({ nodes, edges, diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/AppCard.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/AppCard.tsx index b07d2fdeeb56..8dd537572760 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/AppCard.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/AppCard.tsx @@ -190,9 +190,11 @@ const AppCard = ({ showSaveStatus }: { showSaveStatus: boolean }) => { : publishStatusStyle.unPublish.colorSchema } > - {isPublished - ? publishStatusStyle.published.text - : publishStatusStyle.unPublish.text} + {t( + isPublished + ? publishStatusStyle.published.text + : publishStatusStyle.unPublish.text + )} diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/index.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/index.tsx index 5fd5a8f4f5c1..d52d9f42dca5 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/index.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/index.tsx @@ -36,6 +36,7 @@ const nodeTypes: Record = { [FlowNodeTypeEnum.systemConfig]: dynamic(() => import('./nodes/NodeSystemConfig')), [FlowNodeTypeEnum.workflowStart]: dynamic(() => import('./nodes/NodeWorkflowStart')), [FlowNodeTypeEnum.chatNode]: NodeSimple, + [FlowNodeTypeEnum.readFiles]: NodeSimple, [FlowNodeTypeEnum.datasetSearchNode]: NodeSimple, [FlowNodeTypeEnum.datasetConcatNode]: dynamic(() => import('./nodes/NodeDatasetConcat')), [FlowNodeTypeEnum.answerNode]: dynamic(() => import('./nodes/NodeAnswer')), diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodePluginIO/PluginOutput.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodePluginIO/PluginOutput.tsx index 3c6478ca998e..af7d70b9d257 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodePluginIO/PluginOutput.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodePluginIO/PluginOutput.tsx @@ -174,7 +174,7 @@ function Reference({ <> {input.label} - {input.description && } + {input.description && } {/* value */} diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeSystemConfig.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeSystemConfig.tsx index a9f4c5a6c3d1..f7e00e45e92c 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeSystemConfig.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeSystemConfig.tsx @@ -1,6 +1,6 @@ import React, { Dispatch, useMemo, useTransition } from 'react'; import { NodeProps } from 'reactflow'; -import { Box, useTheme } from '@chakra-ui/react'; +import { Box } from '@chakra-ui/react'; import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node.d'; import QGSwitch from '@/components/core/app/QGSwitch'; @@ -19,6 +19,7 @@ import { useMemoizedFn } from 'ahooks'; import VariableEdit from '@/components/core/app/VariableEdit'; import { AppContext } from '@/pages/app/detail/components/context'; import WelcomeTextConfig from '@/components/core/app/WelcomeTextConfig'; +import FileSelect from '@/components/core/app/FileSelect'; type ComponentProps = { chatConfig: AppChatConfigType; @@ -26,7 +27,6 @@ type ComponentProps = { }; const NodeUserGuide = ({ data, selected }: NodeProps) => { - const theme = useTheme(); const { appDetail, setAppDetail } = useContextSelector(AppContext, (v) => v); const chatConfig = useMemo(() => { @@ -63,19 +63,22 @@ const NodeUserGuide = ({ data, selected }: NodeProps) => { - + + + + - + - + - + - + @@ -219,3 +222,20 @@ function QuestionInputGuide({ chatConfig: { chatInputGuide }, setAppDetail }: Co /> ) : null; } + +function FileSelectConfig({ chatConfig: { fileSelectConfig }, setAppDetail }: ComponentProps) { + return ( + { + setAppDetail((state) => ({ + ...state, + chatConfig: { + ...state.chatConfig, + fileSelectConfig: e + } + })); + }} + /> + ); +} diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeWorkflowStart.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeWorkflowStart.tsx index 2e4c6f1aa278..2e623de50887 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeWorkflowStart.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeWorkflowStart.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react'; +import React, { useEffect, useMemo } from 'react'; import { NodeProps } from 'reactflow'; import NodeCard from './render/NodeCard'; import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node.d'; @@ -14,11 +14,13 @@ import { FlowNodeOutputItemType } from '@fastgpt/global/core/workflow/type/io'; import { FlowNodeOutputTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants'; import { AppContext } from '@/pages/app/detail/components/context'; +import { userFilesInput } from '@fastgpt/global/core/workflow/template/system/workflowStart'; const NodeStart = ({ data, selected }: NodeProps) => { const { t } = useTranslation(); const { nodeId, outputs } = data; const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList); + const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode); const { appDetail } = useContextSelector(AppContext, (v) => v); const variablesOutputs = useCreation(() => { @@ -38,6 +40,30 @@ const NodeStart = ({ data, selected }: NodeProps) => { })); }, [nodeList, t]); + // Dynamic add or delete userFilesInput + useEffect(() => { + const canUploadFiles = + appDetail.chatConfig?.fileSelectConfig?.canSelectFile || + appDetail.chatConfig?.fileSelectConfig?.canSelectImg; + const repeatKey = outputs.find((item) => item.key === userFilesInput.key); + + if (canUploadFiles) { + !repeatKey && + onChangeNode({ + nodeId, + type: 'addOutput', + value: userFilesInput + }); + } else { + repeatKey && + onChangeNode({ + nodeId, + type: 'delOutput', + key: userFilesInput.key + }); + } + }, [appDetail.chatConfig?.fileSelectConfig, nodeId, onChangeNode, outputs]); + return ( { ); }, [ description, + input.renderTypeList, + input.selectedTypeIndex, label, onChangeRenderType, renderTypeList, diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/templates/SettingLLMModel.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/templates/SettingLLMModel.tsx index 24e45fc98f30..97050db490ed 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/templates/SettingLLMModel.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/templates/SettingLLMModel.tsx @@ -36,9 +36,10 @@ const SelectAiModelRender = ({ item, inputs = [], nodeId }: RenderInputProps) => inputs.find((input) => input.key === NodeInputKeyEnum.aiChatMaxToken)?.value ?? 2048, temperature: inputs.find((input) => input.key === NodeInputKeyEnum.aiChatTemperature)?.value ?? 1, - isResponseAnswerText: inputs.find( - (input) => input.key === NodeInputKeyEnum.aiChatIsResponseText - )?.value + isResponseAnswerText: + inputs.find((input) => input.key === NodeInputKeyEnum.aiChatIsResponseText)?.value ?? true, + aiChatVision: + inputs.find((input) => input.key === NodeInputKeyEnum.aiChatVision)?.value ?? true }), [inputs] ); diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderOutput/Label.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderOutput/Label.tsx index d1aa0301346f..512d9994a55d 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderOutput/Label.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderOutput/Label.tsx @@ -35,7 +35,7 @@ const OutputLabel = ({ nodeId, output }: { nodeId: string; output: FlowNodeOutpu > {t(label as any)} - {description && } + {description && } {output.type === FlowNodeOutputTypeEnum.source && ( diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/context.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/context.tsx index 41af938f02e6..0f23a7cb7744 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/context.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/context.tsx @@ -523,7 +523,7 @@ const WorkflowContextProvider = ({ version: 'v2' }); setSaveLabel( - t('core.app.Saved time', { + t('common:core.app.Saved time', { time: formatTime2HM() }) ); diff --git a/projects/app/src/pages/app/detail/components/useChatTest.tsx b/projects/app/src/pages/app/detail/components/useChatTest.tsx index 316340aa051b..a1ac30f2b5a6 100644 --- a/projects/app/src/pages/app/detail/components/useChatTest.tsx +++ b/projects/app/src/pages/app/detail/components/useChatTest.tsx @@ -51,7 +51,8 @@ export const useChatTest = ({ edges: initWorkflowEdgeStatus(edges), variables, appId: appDetail._id, - appName: `调试-${appDetail.name}` + appName: `调试-${appDetail.name}`, + chatConfig }, onMessage: generatingMessage, abortCtrl: controller @@ -99,7 +100,6 @@ export const useChatTest = ({ userAvatar={userInfo?.avatar} showMarkIcon chatConfig={chatConfig} - showFileSelector={checkChatSupportSelectFileByModules(nodes)} onStartChat={startChat} onDelMessage={() => {}} /> diff --git a/projects/app/src/pages/chat/index.tsx b/projects/app/src/pages/chat/index.tsx index 32a1ed7003d7..d588e8772f72 100644 --- a/projects/app/src/pages/chat/index.tsx +++ b/projects/app/src/pages/chat/index.tsx @@ -255,7 +255,6 @@ const Chat = ({ appAvatar={chatData.app.avatar} userAvatar={userInfo?.avatar} chatConfig={chatData.app?.chatConfig} - showFileSelector={checkChatSupportSelectFileByChatModels(chatData.app.chatModels)} feedbackType={'user'} onStartChat={onStartChat} onDelMessage={({ contentId }) => delChatRecordById({ contentId, appId, chatId })} @@ -339,7 +338,7 @@ export async function getServerSideProps(context: any) { props: { appId: context?.query?.appId || '', chatId: context?.query?.chatId || '', - ...(await serviceSideProps(context, ['file', 'app'])) + ...(await serviceSideProps(context, ['file', 'app', 'chat'])) } }; } diff --git a/projects/app/src/pages/chat/share.tsx b/projects/app/src/pages/chat/share.tsx index 559d2362b73b..03deab02bd0b 100644 --- a/projects/app/src/pages/chat/share.tsx +++ b/projects/app/src/pages/chat/share.tsx @@ -318,7 +318,6 @@ const OutLink = ({ appName, appIntro, appAvatar }: Props) => { appAvatar={chatData.app.avatar} userAvatar={chatData.userAvatar} chatConfig={chatData.app?.chatConfig} - showFileSelector={checkChatSupportSelectFileByChatModels(chatData.app.chatModels)} feedbackType={'user'} onStartChat={startChat} onDelMessage={({ contentId }) => @@ -395,7 +394,7 @@ export async function getServerSideProps(context: any) { appIntro: app?.appId?.intro ?? 'intro', shareId: shareId ?? '', authToken: authToken ?? '', - ...(await serviceSideProps(context, ['file', 'app'])) + ...(await serviceSideProps(context, ['file', 'app', 'chat'])) } }; } diff --git a/projects/app/src/pages/chat/team.tsx b/projects/app/src/pages/chat/team.tsx index b8eab200b044..91255c39c9ed 100644 --- a/projects/app/src/pages/chat/team.tsx +++ b/projects/app/src/pages/chat/team.tsx @@ -252,7 +252,6 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => { appAvatar={chatData.app.avatar} userAvatar={chatData.userAvatar} chatConfig={chatData.app?.chatConfig} - showFileSelector={checkChatSupportSelectFileByChatModels(chatData.app.chatModels)} feedbackType={'user'} onStartChat={startChat} onDelMessage={({ contentId }) => @@ -338,7 +337,7 @@ export async function getServerSideProps(context: any) { chatId: context?.query?.chatId || '', teamId: context?.query?.teamId || '', teamToken: context?.query?.teamToken || '', - ...(await serviceSideProps(context, ['file', 'app'])) + ...(await serviceSideProps(context, ['file', 'app', 'chat'])) } }; } diff --git a/projects/app/src/pages/dataset/detail/components/Import/components/FileSelector.tsx b/projects/app/src/pages/dataset/detail/components/Import/components/FileSelector.tsx index 0c54d97f3015..f6d20000a263 100644 --- a/projects/app/src/pages/dataset/detail/components/Import/components/FileSelector.tsx +++ b/projects/app/src/pages/dataset/detail/components/Import/components/FileSelector.tsx @@ -89,7 +89,7 @@ const FileSelector = ({ // upload file await Promise.all( files.map(async ({ fileId, file }) => { - const uploadFileId = await uploadFile2DB({ + const { fileId: uploadFileId } = await uploadFile2DB({ file, bucketName: BucketNameEnum.dataset, percentListen: (e) => { @@ -230,7 +230,7 @@ const FileSelector = ({ let isErr = files.some((item) => item.type === ''); if (isErr) { return toast({ - title: fileT('upload_error_description'), + title: t('file:upload_error_description'), status: 'error' }); } diff --git a/projects/app/src/pages/login/components/LoginForm/LoginForm.tsx b/projects/app/src/pages/login/components/LoginForm/LoginForm.tsx index 7e2795031dae..61d0fefd747d 100644 --- a/projects/app/src/pages/login/components/LoginForm/LoginForm.tsx +++ b/projects/app/src/pages/login/components/LoginForm/LoginForm.tsx @@ -54,7 +54,7 @@ const LoginForm = ({ setPageType, loginSuccess }: Props) => { } setRequesting(false); }, - [loginSuccess, toast] + [loginSuccess, t, toast] ); const isCommunityVersion = feConfigs?.show_register === false && !feConfigs?.isPlus; diff --git a/projects/app/src/pages/login/index.tsx b/projects/app/src/pages/login/index.tsx index b78699e339ab..e04326ffe70c 100644 --- a/projects/app/src/pages/login/index.tsx +++ b/projects/app/src/pages/login/index.tsx @@ -129,7 +129,7 @@ const Login = () => { export async function getServerSideProps(context: any) { return { - props: { ...(await serviceSideProps(context, ['app'])) } + props: { ...(await serviceSideProps(context, ['app', 'user'])) } }; } diff --git a/projects/app/src/service/common/system/cron.ts b/projects/app/src/service/common/system/cron.ts index 0c831b091c85..682691572858 100644 --- a/projects/app/src/service/common/system/cron.ts +++ b/projects/app/src/service/common/system/cron.ts @@ -1,7 +1,12 @@ import { setCron } from '@fastgpt/service/common/system/cron'; import { startTrainingQueue } from '@/service/core/dataset/training/utils'; import { clearTmpUploadFiles } from '@fastgpt/service/common/file/utils'; -import { checkInvalidDatasetFiles, checkInvalidDatasetData, checkInvalidVector } from './cronTask'; +import { + checkInvalidDatasetFiles, + checkInvalidDatasetData, + checkInvalidVector, + removeExpiredChatFiles +} from './cronTask'; import { checkTimerLock } from '@fastgpt/service/common/system/timerLock/utils'; import { TimerIdEnum } from '@fastgpt/service/common/system/timerLock/constants'; import { addHours } from 'date-fns'; @@ -28,7 +33,8 @@ const clearInvalidDataCron = () => { lockMinuted: 59 }) ) { - checkInvalidDatasetFiles(addHours(new Date(), -6), addHours(new Date(), -2)); + await checkInvalidDatasetFiles(addHours(new Date(), -6), addHours(new Date(), -2)); + removeExpiredChatFiles(); } }); diff --git a/projects/app/src/service/common/system/cronTask.ts b/projects/app/src/service/common/system/cronTask.ts index f72578ed4e8e..d12dd1d7b5e1 100644 --- a/projects/app/src/service/common/system/cronTask.ts +++ b/projects/app/src/service/common/system/cronTask.ts @@ -1,3 +1,4 @@ +import { BucketNameEnum } from '@fastgpt/global/common/file/constants'; import { delFileByFileIdList, getGFSCollection @@ -11,15 +12,16 @@ import { import { MongoDatasetCollection } from '@fastgpt/service/core/dataset/collection/schema'; import { MongoDatasetData } from '@fastgpt/service/core/dataset/data/schema'; import { MongoDatasetTraining } from '@fastgpt/service/core/dataset/training/schema'; +import { addDays } from 'date-fns'; /* check dataset.files data. If there is no match in dataset.collections, delete it - 可能异常情况 + 可能异常情况: 1. 上传了文件,未成功创建集合 */ export async function checkInvalidDatasetFiles(start: Date, end: Date) { let deleteFileAmount = 0; - const collection = getGFSCollection('dataset'); + const collection = getGFSCollection(BucketNameEnum.dataset); const where = { uploadDate: { $gte: start, $lte: end } }; @@ -46,7 +48,10 @@ export async function checkInvalidDatasetFiles(start: Date, end: Date) { // 3. if not found, delete file if (hasCollection === 0) { - await delFileByFileIdList({ bucketName: 'dataset', fileIdList: [String(file._id)] }); + await delFileByFileIdList({ + bucketName: BucketNameEnum.dataset, + fileIdList: [String(file._id)] + }); console.log('delete file', file._id); deleteFileAmount++; } @@ -59,6 +64,35 @@ export async function checkInvalidDatasetFiles(start: Date, end: Date) { addLog.info(`Clear invalid dataset files finish, remove ${deleteFileAmount} files`); } +/* + Remove 7 days ago chat files +*/ +export const removeExpiredChatFiles = async () => { + let deleteFileAmount = 0; + const collection = getGFSCollection(BucketNameEnum.chat); + const where = { + uploadDate: { $lte: addDays(new Date(), -7) } + }; + + // get all file _id + const files = await collection.find(where, { projection: { _id: 1 } }).toArray(); + + // Delete file one by one + for await (const file of files) { + try { + await delFileByFileIdList({ + bucketName: BucketNameEnum.chat, + fileIdList: [String(file._id)] + }); + deleteFileAmount++; + } catch (error) { + console.log(error); + } + } + + addLog.info(`Remove expired chat files finish, remove ${deleteFileAmount} files`); +}; + /* 检测无效的 Mongo 数据 异常情况: diff --git a/projects/app/src/service/core/app/utils.ts b/projects/app/src/service/core/app/utils.ts index 5d2ba7ae2908..2f0ffa282894 100644 --- a/projects/app/src/service/core/app/utils.ts +++ b/projects/app/src/service/core/app/utils.ts @@ -1,4 +1,5 @@ import { getUserChatInfoAndAuthTeamPoints } from '@/service/support/permission/auth/team'; +import { defaultApp } from '@/web/core/app/constants'; import { getNextTimeByCronStringAndTimezone } from '@fastgpt/global/common/string/time'; import { getNanoid } from '@fastgpt/global/common/string/tools'; import { delay } from '@fastgpt/global/common/system/utils'; @@ -46,6 +47,7 @@ export const getScheduleTriggerApp = async () => { } } ], + chatConfig: defaultApp.chatConfig, histories: [], stream: false, detail: false, diff --git a/projects/app/src/service/events/generateQA.ts b/projects/app/src/service/events/generateQA.ts index 24893803768c..c68bcbfc3b86 100644 --- a/projects/app/src/service/events/generateQA.ts +++ b/projects/app/src/service/events/generateQA.ts @@ -14,6 +14,7 @@ import { checkInvalidChunkAndLock } from '@fastgpt/service/core/dataset/training import { addMinutes } from 'date-fns'; import { countGptMessagesTokens } from '@fastgpt/service/common/string/tiktoken/index'; import { pushDataListToTrainingQueueByCollectionId } from '@fastgpt/service/core/dataset/training/controller'; +import { loadRequestMessages } from '@fastgpt/service/core/chat/utils'; const reduceQueue = () => { global.qaQueueLen = global.qaQueueLen > 0 ? global.qaQueueLen - 1 : 0; @@ -113,7 +114,7 @@ ${replaceVariable(Prompt_AgentQA.fixedText, { text })}`; const chatResponse = await ai.chat.completions.create({ model, temperature: 0.3, - messages, + messages: await loadRequestMessages({ messages, useVision: false }), stream: false }); const answer = chatResponse.choices?.[0].message?.content || ''; diff --git a/projects/app/src/web/common/file/api.ts b/projects/app/src/web/common/file/api.ts index 460c618ed205..e7607c72f359 100644 --- a/projects/app/src/web/common/file/api.ts +++ b/projects/app/src/web/common/file/api.ts @@ -9,7 +9,10 @@ export const postUploadFiles = ( data: FormData, onUploadProgress: (progressEvent: AxiosProgressEvent) => void ) => - POST('/common/file/upload', data, { + POST<{ + fileId: string; + previewUrl: string; + }>('/common/file/upload', data, { timeout: 600000, onUploadProgress, headers: { diff --git a/projects/app/src/web/core/app/utils.ts b/projects/app/src/web/core/app/utils.ts index 5d3c72d7651e..6b269a9bcd3a 100644 --- a/projects/app/src/web/core/app/utils.ts +++ b/projects/app/src/web/core/app/utils.ts @@ -7,7 +7,6 @@ import { import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node.d'; import { FlowNodeInputTypeEnum, - FlowNodeOutputTypeEnum, FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { NodeInputKeyEnum, WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants'; @@ -18,32 +17,45 @@ import { EditorVariablePickerType } from '@fastgpt/web/components/common/Textare import { TFunction } from 'next-i18next'; import { ToolModule } from '@fastgpt/global/core/workflow/template/system/tools'; import { useDatasetStore } from '../dataset/store/dataset'; +import { + WorkflowStart, + userFilesInput +} from '@fastgpt/global/core/workflow/template/system/workflowStart'; +import { SystemConfigNode } from '@fastgpt/global/core/workflow/template/system/systemConfig'; +import { AiChatModule } from '@fastgpt/global/core/workflow/template/system/aiChat'; +import { DatasetSearchModule } from '@fastgpt/global/core/workflow/template/system/datasetSearch'; +import { ReadFilesNodes } from '@fastgpt/global/core/workflow/template/system/readFiles'; type WorkflowType = { nodes: StoreNodeItemType[]; edges: StoreEdgeItemType[]; }; -export function form2AppWorkflow(data: AppSimpleEditFormType): WorkflowType & { +export function form2AppWorkflow( + data: AppSimpleEditFormType, + t: any // i18nT +): WorkflowType & { chatConfig: AppChatConfigType; } { const workflowStartNodeId = 'workflowStartNodeId'; + const datasetNodeId = 'iKBoX2vIzETU'; + const aiChatNodeId = '7BdojPlukIQw'; const allDatasets = useDatasetStore.getState().allDatasets; const selectedDatasets = data.dataset.datasets.filter((item) => allDatasets.some((ds) => ds._id === item.datasetId) ); - function systemConfigTemplate(formData: AppSimpleEditFormType): StoreNodeItemType { + function systemConfigTemplate(): StoreNodeItemType { return { - nodeId: 'userGuide', - name: '系统配置', - intro: '可以配置应用的系统参数', - flowNodeType: FlowNodeTypeEnum.systemConfig, + nodeId: SystemConfigNode.id, + name: t(SystemConfigNode.name), + intro: '', + flowNodeType: SystemConfigNode.flowNodeType, position: { x: 531.2422736065552, y: -486.7611729549753 }, - version: '481', + version: SystemConfigNode.version, inputs: [], outputs: [] }; @@ -51,509 +63,259 @@ export function form2AppWorkflow(data: AppSimpleEditFormType): WorkflowType & { function workflowStartTemplate(): StoreNodeItemType { return { nodeId: workflowStartNodeId, - name: '流程开始', + name: t(WorkflowStart.name), intro: '', - avatar: '/imgs/workflow/userChatInput.svg', - flowNodeType: FlowNodeTypeEnum.workflowStart, + avatar: WorkflowStart.avatar, + flowNodeType: WorkflowStart.flowNodeType, position: { x: 558.4082376415505, y: 123.72387429194112 }, - version: '481', + version: WorkflowStart.version, + inputs: WorkflowStart.inputs, + outputs: [...WorkflowStart.outputs, userFilesInput] + }; + } + function aiChatTemplate(formData: AppSimpleEditFormType): StoreNodeItemType { + return { + nodeId: aiChatNodeId, + name: t(AiChatModule.name), + intro: t(AiChatModule.intro), + avatar: AiChatModule.avatar, + flowNodeType: AiChatModule.flowNodeType, + showStatus: true, + position: { + x: 1106.3238387960757, + y: -350.6030674683474 + }, + version: AiChatModule.version, inputs: [ + { + key: 'model', + renderTypeList: [FlowNodeInputTypeEnum.settingLLMModel, FlowNodeInputTypeEnum.reference], + label: '', + valueType: WorkflowIOValueTypeEnum.string, + value: formData.aiSettings.model + }, + { + key: 'temperature', + renderTypeList: [FlowNodeInputTypeEnum.hidden], + label: '', + value: formData.aiSettings.temperature, + valueType: WorkflowIOValueTypeEnum.number, + min: 0, + max: 10, + step: 1 + }, + { + key: 'maxToken', + renderTypeList: [FlowNodeInputTypeEnum.hidden], + label: '', + value: formData.aiSettings.maxToken, + valueType: WorkflowIOValueTypeEnum.number, + min: 100, + max: 4000, + step: 50 + }, + { + key: 'isResponseAnswerText', + renderTypeList: [FlowNodeInputTypeEnum.hidden], + label: '', + value: true, + valueType: WorkflowIOValueTypeEnum.boolean + }, + { + key: 'quoteTemplate', + renderTypeList: [FlowNodeInputTypeEnum.hidden], + label: '', + valueType: WorkflowIOValueTypeEnum.string + }, + { + key: 'quotePrompt', + renderTypeList: [FlowNodeInputTypeEnum.hidden], + label: '', + valueType: WorkflowIOValueTypeEnum.string + }, + { + key: 'systemPrompt', + renderTypeList: [FlowNodeInputTypeEnum.textarea, FlowNodeInputTypeEnum.reference], + max: 3000, + valueType: WorkflowIOValueTypeEnum.string, + label: 'core.ai.Prompt', + description: 'core.app.tip.chatNodeSystemPromptTip', + placeholder: 'core.app.tip.chatNodeSystemPromptTip', + value: formData.aiSettings.systemPrompt + }, + { + key: 'history', + renderTypeList: [FlowNodeInputTypeEnum.numberInput, FlowNodeInputTypeEnum.reference], + valueType: WorkflowIOValueTypeEnum.chatHistory, + label: 'core.module.input.label.chat history', + required: true, + min: 0, + max: 30, + value: formData.aiSettings.maxHistories + }, { key: 'userChatInput', renderTypeList: [FlowNodeInputTypeEnum.reference, FlowNodeInputTypeEnum.textarea], valueType: WorkflowIOValueTypeEnum.string, label: '用户问题', required: true, - toolDescription: '用户问题' + toolDescription: '用户问题', + value: [workflowStartNodeId, 'userChatInput'] + }, + { + key: 'quoteQA', + renderTypeList: [FlowNodeInputTypeEnum.settingDatasetQuotePrompt], + label: '', + debugLabel: '知识库引用', + description: '', + valueType: WorkflowIOValueTypeEnum.datasetQuote, + value: selectedDatasets ? [datasetNodeId, 'quoteQA'] : undefined + }, + { + key: NodeInputKeyEnum.aiChatVision, + renderTypeList: [FlowNodeInputTypeEnum.hidden], + label: '', + valueType: WorkflowIOValueTypeEnum.boolean, + value: true } ], - outputs: [ + outputs: AiChatModule.outputs + }; + } + function datasetNodeTemplate(formData: AppSimpleEditFormType, question: any): StoreNodeItemType { + return { + nodeId: datasetNodeId, + name: t(DatasetSearchModule.name), + intro: t(DatasetSearchModule.intro), + avatar: DatasetSearchModule.avatar, + flowNodeType: DatasetSearchModule.flowNodeType, + showStatus: true, + position: { + x: 918.5901682164496, + y: -227.11542247619582 + }, + version: '481', + inputs: [ + { + key: 'datasets', + renderTypeList: [FlowNodeInputTypeEnum.selectDataset, FlowNodeInputTypeEnum.reference], + label: 'core.module.input.label.Select dataset', + value: selectedDatasets, + valueType: WorkflowIOValueTypeEnum.selectDataset, + list: [], + required: true + }, + { + key: 'similarity', + renderTypeList: [FlowNodeInputTypeEnum.selectDatasetParamsModal], + label: '', + value: formData.dataset.similarity, + valueType: WorkflowIOValueTypeEnum.number + }, + { + key: 'limit', + renderTypeList: [FlowNodeInputTypeEnum.hidden], + label: '', + value: formData.dataset.limit, + valueType: WorkflowIOValueTypeEnum.number + }, + { + key: 'searchMode', + renderTypeList: [FlowNodeInputTypeEnum.hidden], + label: '', + valueType: WorkflowIOValueTypeEnum.string, + value: formData.dataset.searchMode + }, + { + key: 'usingReRank', + renderTypeList: [FlowNodeInputTypeEnum.hidden], + label: '', + valueType: WorkflowIOValueTypeEnum.boolean, + value: formData.dataset.usingReRank + }, + { + key: 'datasetSearchUsingExtensionQuery', + renderTypeList: [FlowNodeInputTypeEnum.hidden], + label: '', + valueType: WorkflowIOValueTypeEnum.boolean, + value: formData.dataset.datasetSearchUsingExtensionQuery + }, + { + key: 'datasetSearchExtensionModel', + renderTypeList: [FlowNodeInputTypeEnum.hidden], + label: '', + valueType: WorkflowIOValueTypeEnum.string, + value: formData.dataset.datasetSearchExtensionModel + }, + { + key: 'datasetSearchExtensionBg', + renderTypeList: [FlowNodeInputTypeEnum.hidden], + label: '', + valueType: WorkflowIOValueTypeEnum.string, + value: formData.dataset.datasetSearchExtensionBg + }, { - id: 'userChatInput', key: 'userChatInput', - label: 'core.module.input.label.user question', + renderTypeList: [FlowNodeInputTypeEnum.reference, FlowNodeInputTypeEnum.textarea], valueType: WorkflowIOValueTypeEnum.string, - type: FlowNodeOutputTypeEnum.static + label: '用户问题', + required: true, + toolDescription: '需要检索的内容', + value: question } - ] + ], + outputs: DatasetSearchModule.outputs }; } + // Start, AiChat function simpleChatTemplate(formData: AppSimpleEditFormType): WorkflowType { return { - nodes: [ - { - nodeId: '7BdojPlukIQw', - name: 'AI 对话', - intro: 'AI 大模型对话', - avatar: '/imgs/workflow/AI.png', - flowNodeType: FlowNodeTypeEnum.chatNode, - showStatus: true, - position: { - x: 1106.3238387960757, - y: -350.6030674683474 - }, - version: '481', - inputs: [ - { - key: 'model', - renderTypeList: [ - FlowNodeInputTypeEnum.settingLLMModel, - FlowNodeInputTypeEnum.reference - ], - label: 'core.module.input.label.aiModel', - valueType: WorkflowIOValueTypeEnum.string, - value: formData.aiSettings.model - }, - { - key: 'temperature', - renderTypeList: [FlowNodeInputTypeEnum.hidden], - label: '', - value: formData.aiSettings.temperature, - valueType: WorkflowIOValueTypeEnum.number, - min: 0, - max: 10, - step: 1 - }, - { - key: 'maxToken', - renderTypeList: [FlowNodeInputTypeEnum.hidden], - label: '', - value: formData.aiSettings.maxToken, - valueType: WorkflowIOValueTypeEnum.number, - min: 100, - max: 4000, - step: 50 - }, - { - key: 'isResponseAnswerText', - renderTypeList: [FlowNodeInputTypeEnum.hidden], - label: '', - value: true, - valueType: WorkflowIOValueTypeEnum.boolean - }, - { - key: 'quoteTemplate', - renderTypeList: [FlowNodeInputTypeEnum.hidden], - label: '', - valueType: WorkflowIOValueTypeEnum.string - }, - { - key: 'quotePrompt', - renderTypeList: [FlowNodeInputTypeEnum.hidden], - label: '', - valueType: WorkflowIOValueTypeEnum.string - }, - { - key: 'systemPrompt', - renderTypeList: [FlowNodeInputTypeEnum.textarea, FlowNodeInputTypeEnum.reference], - max: 3000, - valueType: WorkflowIOValueTypeEnum.string, - label: 'core.ai.Prompt', - description: 'core.app.tip.chatNodeSystemPromptTip', - placeholder: 'core.app.tip.chatNodeSystemPromptTip', - value: formData.aiSettings.systemPrompt - }, - { - key: 'history', - renderTypeList: [FlowNodeInputTypeEnum.numberInput, FlowNodeInputTypeEnum.reference], - valueType: WorkflowIOValueTypeEnum.chatHistory, - label: 'core.module.input.label.chat history', - required: true, - min: 0, - max: 30, - value: formData.aiSettings.maxHistories - }, - { - key: 'userChatInput', - renderTypeList: [FlowNodeInputTypeEnum.reference, FlowNodeInputTypeEnum.textarea], - valueType: WorkflowIOValueTypeEnum.string, - label: '用户问题', - required: true, - toolDescription: '用户问题', - value: [workflowStartNodeId, 'userChatInput'] - }, - { - key: 'quoteQA', - renderTypeList: [FlowNodeInputTypeEnum.settingDatasetQuotePrompt], - label: '', - debugLabel: '知识库引用', - description: '', - valueType: WorkflowIOValueTypeEnum.datasetQuote - } - ], - outputs: [ - { - id: 'history', - key: 'history', - label: 'core.module.output.label.New context', - description: 'core.module.output.description.New context', - valueType: WorkflowIOValueTypeEnum.chatHistory, - type: FlowNodeOutputTypeEnum.static - }, - { - id: 'answerText', - key: 'answerText', - label: 'core.module.output.label.Ai response content', - description: 'core.module.output.description.Ai response content', - valueType: WorkflowIOValueTypeEnum.string, - type: FlowNodeOutputTypeEnum.static - } - ] - } - ], + nodes: [aiChatTemplate(formData)], edges: [ { source: workflowStartNodeId, - target: '7BdojPlukIQw', + target: aiChatNodeId, sourceHandle: `${workflowStartNodeId}-source-right`, - targetHandle: '7BdojPlukIQw-target-left' + targetHandle: `${aiChatNodeId}-target-left` } ] }; } + // Start, Dataset search, AiChat function datasetTemplate(formData: AppSimpleEditFormType): WorkflowType { return { nodes: [ - { - nodeId: '7BdojPlukIQw', - name: 'AI 对话', - intro: 'AI 大模型对话', - avatar: '/imgs/workflow/AI.png', - flowNodeType: FlowNodeTypeEnum.chatNode, - showStatus: true, - position: { - x: 1638.509551404687, - y: -341.0428450861567 - }, - version: '481', // [FlowNodeTypeEnum.chatNode] - inputs: [ - { - key: 'model', - renderTypeList: [ - FlowNodeInputTypeEnum.settingLLMModel, - FlowNodeInputTypeEnum.reference - ], - label: 'core.module.input.label.aiModel', - valueType: WorkflowIOValueTypeEnum.string, - value: formData.aiSettings.model - }, - { - key: 'temperature', - renderTypeList: [FlowNodeInputTypeEnum.hidden], - label: '', - value: formData.aiSettings.temperature, - valueType: WorkflowIOValueTypeEnum.number, - min: 0, - max: 10, - step: 1 - }, - { - key: 'maxToken', - renderTypeList: [FlowNodeInputTypeEnum.hidden], - label: '', - value: formData.aiSettings.maxToken, - valueType: WorkflowIOValueTypeEnum.number, - min: 100, - max: 4000, - step: 50 - }, - { - key: 'isResponseAnswerText', - renderTypeList: [FlowNodeInputTypeEnum.hidden], - label: '', - value: true, - valueType: WorkflowIOValueTypeEnum.boolean - }, - { - key: 'quoteTemplate', - renderTypeList: [FlowNodeInputTypeEnum.hidden], - label: '', - valueType: WorkflowIOValueTypeEnum.string - }, - { - key: 'quotePrompt', - renderTypeList: [FlowNodeInputTypeEnum.hidden], - label: '', - valueType: WorkflowIOValueTypeEnum.string - }, - { - key: 'systemPrompt', - renderTypeList: [FlowNodeInputTypeEnum.textarea, FlowNodeInputTypeEnum.reference], - max: 3000, - valueType: WorkflowIOValueTypeEnum.string, - label: 'core.ai.Prompt', - description: 'core.app.tip.chatNodeSystemPromptTip', - placeholder: 'core.app.tip.chatNodeSystemPromptTip', - value: formData.aiSettings.systemPrompt - }, - { - key: 'history', - renderTypeList: [FlowNodeInputTypeEnum.numberInput, FlowNodeInputTypeEnum.reference], - valueType: WorkflowIOValueTypeEnum.chatHistory, - label: 'core.module.input.label.chat history', - required: true, - min: 0, - max: 30, - value: formData.aiSettings.maxHistories - }, - { - key: 'userChatInput', - renderTypeList: [FlowNodeInputTypeEnum.reference, FlowNodeInputTypeEnum.textarea], - valueType: WorkflowIOValueTypeEnum.string, - label: '用户问题', - required: true, - toolDescription: '用户问题', - value: [workflowStartNodeId, 'userChatInput'] - }, - { - key: 'quoteQA', - renderTypeList: [FlowNodeInputTypeEnum.settingDatasetQuotePrompt], - label: '', - debugLabel: '知识库引用', - description: '', - valueType: WorkflowIOValueTypeEnum.datasetQuote, - value: ['iKBoX2vIzETU', 'quoteQA'] - } - ], - outputs: [ - { - id: 'history', - key: 'history', - label: 'core.module.output.label.New context', - description: 'core.module.output.description.New context', - valueType: WorkflowIOValueTypeEnum.chatHistory, - type: FlowNodeOutputTypeEnum.static - }, - { - id: 'answerText', - key: 'answerText', - label: 'core.module.output.label.Ai response content', - description: 'core.module.output.description.Ai response content', - valueType: WorkflowIOValueTypeEnum.string, - type: FlowNodeOutputTypeEnum.static - } - ] - }, - { - nodeId: 'iKBoX2vIzETU', - name: '知识库搜索', - intro: '调用“语义检索”和“全文检索”能力,从“知识库”中查找可能与问题相关的参考内容', - avatar: '/imgs/workflow/db.png', - flowNodeType: FlowNodeTypeEnum.datasetSearchNode, - showStatus: true, - position: { - x: 918.5901682164496, - y: -227.11542247619582 - }, - version: '481', - inputs: [ - { - key: 'datasets', - renderTypeList: [ - FlowNodeInputTypeEnum.selectDataset, - FlowNodeInputTypeEnum.reference - ], - label: 'core.module.input.label.Select dataset', - value: selectedDatasets, - valueType: WorkflowIOValueTypeEnum.selectDataset, - list: [], - required: true - }, - { - key: 'similarity', - renderTypeList: [FlowNodeInputTypeEnum.selectDatasetParamsModal], - label: '', - value: formData.dataset.similarity, - valueType: WorkflowIOValueTypeEnum.number - }, - { - key: 'limit', - renderTypeList: [FlowNodeInputTypeEnum.hidden], - label: '', - value: formData.dataset.limit, - valueType: WorkflowIOValueTypeEnum.number - }, - { - key: 'searchMode', - renderTypeList: [FlowNodeInputTypeEnum.hidden], - label: '', - valueType: WorkflowIOValueTypeEnum.string, - value: formData.dataset.searchMode - }, - { - key: 'usingReRank', - renderTypeList: [FlowNodeInputTypeEnum.hidden], - label: '', - valueType: WorkflowIOValueTypeEnum.boolean, - value: formData.dataset.usingReRank - }, - { - key: 'datasetSearchUsingExtensionQuery', - renderTypeList: [FlowNodeInputTypeEnum.hidden], - label: '', - valueType: WorkflowIOValueTypeEnum.boolean, - value: formData.dataset.datasetSearchUsingExtensionQuery - }, - { - key: 'datasetSearchExtensionModel', - renderTypeList: [FlowNodeInputTypeEnum.hidden], - label: '', - valueType: WorkflowIOValueTypeEnum.string, - value: formData.dataset.datasetSearchExtensionModel - }, - { - key: 'datasetSearchExtensionBg', - renderTypeList: [FlowNodeInputTypeEnum.hidden], - label: '', - valueType: WorkflowIOValueTypeEnum.string, - value: formData.dataset.datasetSearchExtensionBg - }, - { - key: 'userChatInput', - renderTypeList: [FlowNodeInputTypeEnum.reference, FlowNodeInputTypeEnum.textarea], - valueType: WorkflowIOValueTypeEnum.string, - label: '用户问题', - required: true, - toolDescription: '需要检索的内容', - value: [workflowStartNodeId, 'userChatInput'] - } - ], - outputs: [ - { - id: 'quoteQA', - key: 'quoteQA', - label: 'core.module.Dataset quote.label', - type: FlowNodeOutputTypeEnum.static, - valueType: WorkflowIOValueTypeEnum.datasetQuote - } - ] - } + aiChatTemplate(formData), + datasetNodeTemplate(formData, [workflowStartNodeId, 'userChatInput']) ], edges: [ { source: workflowStartNodeId, - target: 'iKBoX2vIzETU', + target: datasetNodeId, sourceHandle: `${workflowStartNodeId}-source-right`, - targetHandle: 'iKBoX2vIzETU-target-left' + targetHandle: `${datasetNodeId}-target-left` }, { - source: 'iKBoX2vIzETU', - target: '7BdojPlukIQw', - sourceHandle: 'iKBoX2vIzETU-source-right', - targetHandle: '7BdojPlukIQw-target-left' + source: datasetNodeId, + target: aiChatNodeId, + sourceHandle: `${datasetNodeId}-source-right`, + targetHandle: `${aiChatNodeId}-target-left` } ] }; } function toolTemplates(formData: AppSimpleEditFormType): WorkflowType { const toolNodeId = getNanoid(6); - const datasetNodeId = getNanoid(6); + // Dataset tool config const datasetTool: WorkflowType | null = selectedDatasets.length > 0 ? { - nodes: [ - { - nodeId: datasetNodeId, - name: '知识库搜索', - intro: '调用“语义检索”和“全文检索”能力,从“知识库”中查找可能与问题相关的参考内容', - avatar: '/imgs/workflow/db.png', - flowNodeType: FlowNodeTypeEnum.datasetSearchNode, - showStatus: true, - position: { - x: 500, - y: 545 - }, - version: '481', - inputs: [ - { - key: 'datasets', - renderTypeList: [ - FlowNodeInputTypeEnum.selectDataset, - FlowNodeInputTypeEnum.reference - ], - label: 'core.module.input.label.Select dataset', - value: selectedDatasets, - valueType: WorkflowIOValueTypeEnum.selectDataset, - list: [], - required: true - }, - { - key: 'similarity', - renderTypeList: [FlowNodeInputTypeEnum.selectDatasetParamsModal], - label: '', - value: formData.dataset.similarity, - valueType: WorkflowIOValueTypeEnum.number - }, - { - key: 'limit', - renderTypeList: [FlowNodeInputTypeEnum.hidden], - label: '', - value: formData.dataset.limit, - valueType: WorkflowIOValueTypeEnum.number - }, - { - key: 'searchMode', - renderTypeList: [FlowNodeInputTypeEnum.hidden], - label: '', - valueType: WorkflowIOValueTypeEnum.string, - value: formData.dataset.searchMode - }, - { - key: 'usingReRank', - renderTypeList: [FlowNodeInputTypeEnum.hidden], - label: '', - valueType: WorkflowIOValueTypeEnum.boolean, - value: formData.dataset.usingReRank - }, - { - key: 'datasetSearchUsingExtensionQuery', - renderTypeList: [FlowNodeInputTypeEnum.hidden], - label: '', - valueType: WorkflowIOValueTypeEnum.boolean, - value: formData.dataset.datasetSearchUsingExtensionQuery - }, - { - key: 'datasetSearchExtensionModel', - renderTypeList: [FlowNodeInputTypeEnum.hidden], - label: '', - valueType: WorkflowIOValueTypeEnum.string, - value: formData.dataset.datasetSearchExtensionModel - }, - { - key: 'datasetSearchExtensionBg', - renderTypeList: [FlowNodeInputTypeEnum.hidden], - label: '', - valueType: WorkflowIOValueTypeEnum.string, - value: formData.dataset.datasetSearchExtensionBg - }, - { - key: 'userChatInput', - renderTypeList: [ - FlowNodeInputTypeEnum.reference, - FlowNodeInputTypeEnum.textarea - ], - valueType: WorkflowIOValueTypeEnum.string, - label: '用户问题', - required: true, - toolDescription: '需要检索的内容' - } - ], - outputs: [ - { - id: 'quoteQA', - key: 'quoteQA', - label: 'core.module.Dataset quote.label', - type: FlowNodeOutputTypeEnum.static, - valueType: WorkflowIOValueTypeEnum.datasetQuote - } - ] - } - ], + nodes: [datasetNodeTemplate(formData, '')], edges: [ { source: toolNodeId, @@ -564,7 +326,46 @@ export function form2AppWorkflow(data: AppSimpleEditFormType): WorkflowType & { ] } : null; + // Read file tool config + const readFileTool: WorkflowType | null = data.chatConfig.fileSelectConfig?.canSelectFile + ? { + nodes: [ + { + nodeId: ReadFilesNodes.id, + name: t(ReadFilesNodes.name), + intro: t(ReadFilesNodes.intro), + avatar: ReadFilesNodes.avatar, + flowNodeType: ReadFilesNodes.flowNodeType, + showStatus: true, + position: { + x: 974.6209854328943, + y: 587.6378828744465 + }, + version: '489', + inputs: [ + { + key: NodeInputKeyEnum.fileUrlList, + renderTypeList: [FlowNodeInputTypeEnum.reference], + valueType: WorkflowIOValueTypeEnum.arrayString, + label: t('app:workflow.file_url'), + value: [workflowStartNodeId, 'userFiles'] + } + ], + outputs: ReadFilesNodes.outputs + } + ], + edges: [ + { + source: toolNodeId, + target: ReadFilesNodes.id, + sourceHandle: 'selectedTools', + targetHandle: 'selectedTools' + } + ] + } + : null; + // Computed tools config const pluginTool: WorkflowType[] = formData.selectedTools.map((tool, i) => { const nodeId = getNanoid(6); return { @@ -602,16 +403,16 @@ export function form2AppWorkflow(data: AppSimpleEditFormType): WorkflowType & { nodes: [ { nodeId: toolNodeId, - name: '工具调用', - intro: '通过AI模型自动选择一个或多个功能块进行调用,也可以对插件进行调用。', - avatar: '/imgs/workflow/tool.svg', - flowNodeType: FlowNodeTypeEnum.tools, + name: ToolModule.name, + intro: ToolModule.intro, + avatar: ToolModule.avatar, + flowNodeType: ToolModule.flowNodeType, showStatus: true, position: { x: 1062.1738942532802, y: -223.65033022650476 }, - version: '481', + version: ToolModule.version, inputs: [ { key: 'model', @@ -671,12 +472,20 @@ export function form2AppWorkflow(data: AppSimpleEditFormType): WorkflowType & { label: '用户问题', required: true, value: [workflowStartNodeId, 'userChatInput'] + }, + { + key: NodeInputKeyEnum.aiChatVision, + renderTypeList: [FlowNodeInputTypeEnum.hidden], + label: '', + valueType: WorkflowIOValueTypeEnum.boolean, + value: true } ], outputs: ToolModule.outputs }, // tool nodes ...(datasetTool ? datasetTool.nodes : []), + ...(readFileTool ? readFileTool.nodes : []), ...pluginTool.map((tool) => tool.nodes).flat() ], edges: [ @@ -688,6 +497,7 @@ export function form2AppWorkflow(data: AppSimpleEditFormType): WorkflowType & { }, // tool edges ...(datasetTool ? datasetTool.edges : []), + ...(readFileTool ? readFileTool.edges : []), ...pluginTool.map((tool) => tool.edges).flat() ] }; @@ -696,13 +506,14 @@ export function form2AppWorkflow(data: AppSimpleEditFormType): WorkflowType & { } const workflow = (() => { - if (data.selectedTools.length > 0) return toolTemplates(data); + if (data.selectedTools.length > 0 || data.chatConfig.fileSelectConfig?.canSelectFile) + return toolTemplates(data); if (selectedDatasets.length > 0) return datasetTemplate(data); return simpleChatTemplate(data); })(); return { - nodes: [systemConfigTemplate(data), workflowStartTemplate(), ...workflow.nodes], + nodes: [systemConfigTemplate(), workflowStartTemplate(), ...workflow.nodes], edges: workflow.edges, chatConfig: data.chatConfig }; diff --git a/projects/app/src/web/core/workflow/utils.ts b/projects/app/src/web/core/workflow/utils.ts index 0a54c6b93d3d..7535fcc4c5de 100644 --- a/projects/app/src/web/core/workflow/utils.ts +++ b/projects/app/src/web/core/workflow/utils.ts @@ -450,7 +450,8 @@ export const compareWorkflow = (workflow1: WorkflowType, workflow2: WorkflowType ttsConfig: clone1.chatConfig?.ttsConfig || undefined, whisperConfig: clone1.chatConfig?.whisperConfig || undefined, scheduledTriggerConfig: clone1.chatConfig?.scheduledTriggerConfig || undefined, - chatInputGuide: clone1.chatConfig?.chatInputGuide || undefined + chatInputGuide: clone1.chatConfig?.chatInputGuide || undefined, + fileSelectConfig: clone1.chatConfig?.fileSelectConfig || undefined }, { welcomeText: clone2.chatConfig?.welcomeText || '', @@ -459,7 +460,8 @@ export const compareWorkflow = (workflow1: WorkflowType, workflow2: WorkflowType ttsConfig: clone2.chatConfig?.ttsConfig || undefined, whisperConfig: clone2.chatConfig?.whisperConfig || undefined, scheduledTriggerConfig: clone2.chatConfig?.scheduledTriggerConfig || undefined, - chatInputGuide: clone2.chatConfig?.chatInputGuide || undefined + chatInputGuide: clone2.chatConfig?.chatInputGuide || undefined, + fileSelectConfig: clone2.chatConfig?.fileSelectConfig || undefined } ) ) {