From 4a9ffe7b99ef31804d12ab9431ad98f8483118ef Mon Sep 17 00:00:00 2001 From: Vincelwt Date: Mon, 21 Oct 2024 18:32:51 +0800 Subject: [PATCH 01/20] Lunary: feedback tracking (#3332) * Lunary: feedback tracking * fix incorrect param order --- packages/components/src/handler.ts | 19 ++++--- .../src/controllers/openai-realtime/index.ts | 3 +- .../src/services/openai-realtime/index.ts | 14 +++-- packages/server/src/utils/buildAgentGraph.ts | 2 + packages/server/src/utils/buildChatflow.ts | 53 ++++++++++--------- packages/server/src/utils/index.ts | 3 ++ .../src/utils/updateChatMessageFeedback.ts | 17 ++++++ packages/server/src/utils/upsertVector.ts | 5 +- 8 files changed, 80 insertions(+), 36 deletions(-) diff --git a/packages/components/src/handler.ts b/packages/components/src/handler.ts index e6959d5511f..b97648dac59 100644 --- a/packages/components/src/handler.ts +++ b/packages/components/src/handler.ts @@ -243,12 +243,14 @@ class ExtendedLunaryHandler extends LunaryHandler { databaseEntities: IDatabaseEntity currentRunId: string | null thread: any + apiMessageId: string constructor({ flowiseOptions, ...options }: any) { super(options) this.appDataSource = flowiseOptions.appDataSource this.databaseEntities = flowiseOptions.databaseEntities this.chatId = flowiseOptions.chatId + this.apiMessageId = flowiseOptions.apiMessageId } async initThread() { @@ -258,14 +260,18 @@ class ExtendedLunaryHandler extends LunaryHandler { } }) + const userId = entity?.email ?? entity?.id + this.thread = lunary.openThread({ id: this.chatId, - userId: entity?.email ?? entity?.id, - userProps: { - name: entity?.name ?? undefined, - email: entity?.email ?? undefined, - phone: entity?.phone ?? undefined - } + userId, + userProps: userId + ? { + name: entity?.name ?? undefined, + email: entity?.email ?? undefined, + phone: entity?.phone ?? undefined + } + : undefined }) } @@ -298,6 +304,7 @@ class ExtendedLunaryHandler extends LunaryHandler { const answer = outputs.output this.thread.trackMessage({ + id: this.apiMessageId, content: answer, role: 'assistant' }) diff --git a/packages/server/src/controllers/openai-realtime/index.ts b/packages/server/src/controllers/openai-realtime/index.ts index b5a504ed2c4..a16ea3db50c 100644 --- a/packages/server/src/controllers/openai-realtime/index.ts +++ b/packages/server/src/controllers/openai-realtime/index.ts @@ -54,7 +54,8 @@ const executeAgentTool = async (req: Request, res: Response, next: NextFunction) req.params.id, req.body.chatId, req.body.toolName, - req.body.inputArgs + req.body.inputArgs, + req.body.apiMessageId ) return res.json(apiResponse) } catch (error) { diff --git a/packages/server/src/services/openai-realtime/index.ts b/packages/server/src/services/openai-realtime/index.ts index 3a9249be329..cdc02a11e05 100644 --- a/packages/server/src/services/openai-realtime/index.ts +++ b/packages/server/src/services/openai-realtime/index.ts @@ -12,7 +12,7 @@ import { v4 as uuidv4 } from 'uuid' const SOURCE_DOCUMENTS_PREFIX = '\n\n----FLOWISE_SOURCE_DOCUMENTS----\n\n' const ARTIFACTS_PREFIX = '\n\n----FLOWISE_ARTIFACTS----\n\n' -const buildAndInitTool = async (chatflowid: string, _chatId?: string) => { +const buildAndInitTool = async (chatflowid: string, _chatId?: string, _apiMessageId?: string) => { const appServer = getRunningExpressApp() const chatflow = await appServer.AppDataSource.getRepository(ChatFlow).findOneBy({ id: chatflowid @@ -22,6 +22,7 @@ const buildAndInitTool = async (chatflowid: string, _chatId?: string) => { } const chatId = _chatId || uuidv4() + const apiMessageId = _apiMessageId || uuidv4() const flowData = JSON.parse(chatflow.flowData) const nodes = flowData.nodes const edges = flowData.edges @@ -62,6 +63,7 @@ const buildAndInitTool = async (chatflowid: string, _chatId?: string) => { chatId: chatId, sessionId: chatId, chatflowid, + apiMessageId, appDataSource: appServer.AppDataSource }) @@ -113,9 +115,15 @@ const getAgentTools = async (chatflowid: string): Promise => { } } -const executeAgentTool = async (chatflowid: string, chatId: string, toolName: string, inputArgs: string): Promise => { +const executeAgentTool = async ( + chatflowid: string, + chatId: string, + toolName: string, + inputArgs: string, + apiMessageId?: string +): Promise => { try { - const agent = await buildAndInitTool(chatflowid, chatId) + const agent = await buildAndInitTool(chatflowid, chatId, apiMessageId) const tools = agent.tools const tool = tools.find((tool: any) => tool.name === toolName) diff --git a/packages/server/src/utils/buildAgentGraph.ts b/packages/server/src/utils/buildAgentGraph.ts index ea44382bb30..3104737117d 100644 --- a/packages/server/src/utils/buildAgentGraph.ts +++ b/packages/server/src/utils/buildAgentGraph.ts @@ -57,6 +57,7 @@ import logger from './logger' export const buildAgentGraph = async ( chatflow: IChatFlow, chatId: string, + apiMessageId: string, sessionId: string, incomingInput: IncomingInput, isInternal: boolean, @@ -114,6 +115,7 @@ export const buildAgentGraph = async ( startingNodeIds, reactFlowNodes: nodes, reactFlowEdges: edges, + apiMessageId, graph, depthQueue, componentNodes: appServer.nodesPool.componentNodes, diff --git a/packages/server/src/utils/buildChatflow.ts b/packages/server/src/utils/buildChatflow.ts index 3257f0a8a9e..2c625ab8681 100644 --- a/packages/server/src/utils/buildChatflow.ts +++ b/packages/server/src/utils/buildChatflow.ts @@ -202,6 +202,8 @@ export const utilBuildChatflow = async (req: Request, isInternal: boolean = fals const nodes = parsedFlowData.nodes const edges = parsedFlowData.edges + const apiMessageId = uuidv4() + /*** Get session ID ***/ const memoryNode = findMemoryNode(nodes, edges) const memoryType = memoryNode?.data.label @@ -217,6 +219,7 @@ export const utilBuildChatflow = async (req: Request, isInternal: boolean = fals chatflow, isInternal, chatId, + apiMessageId, memoryType ?? '', sessionId, userMessageDateTime, @@ -339,6 +342,7 @@ export const utilBuildChatflow = async (req: Request, isInternal: boolean = fals reactFlowEdges: edges, graph, depthQueue, + apiMessageId, componentNodes: appServer.nodesPool.componentNodes, question: incomingInput.question, chatHistory, @@ -369,6 +373,7 @@ export const utilBuildChatflow = async (req: Request, isInternal: boolean = fals chatflowid, chatId, sessionId, + apiMessageId, chatHistory, ...incomingInput.overrideConfig } @@ -394,29 +399,23 @@ export const utilBuildChatflow = async (req: Request, isInternal: boolean = fals isStreamValid = (req.body.streaming === 'true' || req.body.streaming === true) && isStreamValid - let result = isStreamValid - ? await nodeInstance.run(nodeToExecuteData, incomingInput.question, { - chatId, - chatflowid, - logger, - appDataSource: appServer.AppDataSource, - databaseEntities, - analytic: chatflow.analytic, - uploads: incomingInput.uploads, - prependMessages, - sseStreamer: appServer.sseStreamer, - shouldStreamResponse: isStreamValid - }) - : await nodeInstance.run(nodeToExecuteData, incomingInput.question, { - chatId, - chatflowid, - logger, - appDataSource: appServer.AppDataSource, - databaseEntities, - analytic: chatflow.analytic, - uploads: incomingInput.uploads, - prependMessages - }) + const runParams = { + chatId, + chatflowid, + apiMessageId, + logger, + appDataSource: appServer.AppDataSource, + databaseEntities, + analytic: chatflow.analytic, + uploads: incomingInput.uploads, + prependMessages + } + + let result = await nodeInstance.run(nodeToExecuteData, incomingInput.question, { + ...runParams, + ...(isStreamValid && { sseStreamer: appServer.sseStreamer, shouldStreamResponse: true }) + }) + result = typeof result === 'string' ? { text: result } : result // Retrieve threadId from assistant if exists @@ -443,7 +442,8 @@ export const utilBuildChatflow = async (req: Request, isInternal: boolean = fals else if (result.json) resultText = '```json\n' + JSON.stringify(result.json, null, 2) else resultText = JSON.stringify(result, null, 2) - const apiMessage: Omit = { + const apiMessage: Omit = { + id: apiMessageId, role: 'apiMessage', content: resultText, chatflowid, @@ -507,6 +507,7 @@ const utilBuildAgentResponse = async ( agentflow: IChatFlow, isInternal: boolean, chatId: string, + apiMessageId: string, memoryType: string, sessionId: string, userMessageDateTime: Date, @@ -523,6 +524,7 @@ const utilBuildAgentResponse = async ( const streamResults = await buildAgentGraph( agentflow, chatId, + apiMessageId, sessionId, incomingInput, isInternal, @@ -546,7 +548,8 @@ const utilBuildAgentResponse = async ( } await utilAddChatMessage(userMessage) - const apiMessage: Omit = { + const apiMessage: Omit = { + id: apiMessageId, role: 'apiMessage', content: finalResult, chatflowid: agentflow.id, diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index f2fb5f2467b..2a8cf2b6292 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -430,6 +430,7 @@ type BuildFlowParams = { chatId: string sessionId: string chatflowid: string + apiMessageId: string appDataSource: DataSource overrideConfig?: ICommonObject cachePool?: CachePool @@ -452,6 +453,7 @@ export const buildFlow = async ({ componentNodes, question, chatHistory, + apiMessageId, chatId, sessionId, chatflowid, @@ -524,6 +526,7 @@ export const buildFlow = async ({ sessionId, chatflowid, chatHistory, + apiMessageId, logger, appDataSource, databaseEntities, diff --git a/packages/server/src/utils/updateChatMessageFeedback.ts b/packages/server/src/utils/updateChatMessageFeedback.ts index ef327fa78f3..69cdffd8b4a 100644 --- a/packages/server/src/utils/updateChatMessageFeedback.ts +++ b/packages/server/src/utils/updateChatMessageFeedback.ts @@ -1,6 +1,8 @@ import { IChatMessageFeedback } from '../Interface' import { getRunningExpressApp } from '../utils/getRunningExpressApp' import { ChatMessageFeedback } from '../database/entities/ChatMessageFeedback' +import { ChatFlow } from '../database/entities/ChatFlow' +import lunary from 'lunary' /** * Method that updates chat message feedback. @@ -11,6 +13,21 @@ export const utilUpdateChatMessageFeedback = async (id: string, chatMessageFeedb const appServer = getRunningExpressApp() const newChatMessageFeedback = new ChatMessageFeedback() Object.assign(newChatMessageFeedback, chatMessageFeedback) + await appServer.AppDataSource.getRepository(ChatMessageFeedback).update({ id }, chatMessageFeedback) + + // Fetch the updated entity + const updatedFeedback = await appServer.AppDataSource.getRepository(ChatMessageFeedback).findOne({ where: { id } }) + + const chatflow = await appServer.AppDataSource.getRepository(ChatFlow).findOne({ where: { id: updatedFeedback?.chatflowid } }) + const analytic = JSON.parse(chatflow?.analytic ?? '{}') + + if (analytic?.lunary?.status === true && updatedFeedback?.rating) { + lunary.trackFeedback(updatedFeedback.messageId, { + comment: updatedFeedback?.content, + thumb: updatedFeedback?.rating === 'THUMBS_UP' ? 'up' : 'down' + }) + } + return { status: 'OK' } } diff --git a/packages/server/src/utils/upsertVector.ts b/packages/server/src/utils/upsertVector.ts index c0e5e4d98f3..bfed225f17c 100644 --- a/packages/server/src/utils/upsertVector.ts +++ b/packages/server/src/utils/upsertVector.ts @@ -23,7 +23,7 @@ import { UpsertHistory } from '../database/entities/UpsertHistory' import { InternalFlowiseError } from '../errors/internalFlowiseError' import { StatusCodes } from 'http-status-codes' import { getErrorMessage } from '../errors/utils' - +import { v4 as uuidv4 } from 'uuid' /** * Upsert documents * @param {Request} req @@ -108,6 +108,8 @@ export const upsertVector = async (req: Request, isInternal: boolean = false) => const nodes = parsedFlowData.nodes const edges = parsedFlowData.edges + const apiMessageId = req.body.apiMessageId ?? uuidv4() + let stopNodeId = incomingInput?.stopNodeId ?? '' let chatHistory: IMessage[] = [] let chatId = incomingInput.chatId ?? '' @@ -162,6 +164,7 @@ export const upsertVector = async (req: Request, isInternal: boolean = false) => question: incomingInput.question, chatHistory, chatId, + apiMessageId, sessionId: sessionId ?? '', chatflowid, appDataSource: appServer.AppDataSource, From a0d93f910f7e28b7d4703f7d09470e80f83380a0 Mon Sep 17 00:00:00 2001 From: Henry Heng Date: Mon, 21 Oct 2024 11:37:13 +0100 Subject: [PATCH 02/20] Feature/add rendering html tags for share chatbot (#3343) add rendering html tags for share chatbot --- .../ui/src/views/chatflows/ShareChatbot.jsx | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/ui/src/views/chatflows/ShareChatbot.jsx b/packages/ui/src/views/chatflows/ShareChatbot.jsx index 9c5de4c36ae..7b09b0ff396 100644 --- a/packages/ui/src/views/chatflows/ShareChatbot.jsx +++ b/packages/ui/src/views/chatflows/ShareChatbot.jsx @@ -56,6 +56,7 @@ const ShareChatbot = ({ isSessionMemory, isAgentCanvas }) => { const [isPublicChatflow, setChatflowIsPublic] = useState(chatflow.isPublic ?? false) const [generateNewSession, setGenerateNewSession] = useState(chatbotConfig?.generateNewSession ?? false) + const [renderHTML, setRenderHTML] = useState(chatbotConfig?.renderHTML ?? false) const [title, setTitle] = useState(chatbotConfig?.title ?? '') const [titleAvatarSrc, setTitleAvatarSrc] = useState(chatbotConfig?.titleAvatarSrc ?? '') @@ -138,6 +139,12 @@ const ShareChatbot = ({ isSessionMemory, isAgentCanvas }) => { if (isSessionMemory) obj.overrideConfig.generateNewSession = generateNewSession + if (renderHTML) { + obj.overrideConfig.renderHTML = true + } else { + obj.overrideConfig.renderHTML = false + } + if (chatbotConfig?.starterPrompts) obj.starterPrompts = chatbotConfig.starterPrompts if (isAgentCanvas) { @@ -312,6 +319,9 @@ const ShareChatbot = ({ isSessionMemory, isAgentCanvas }) => { case 'showAgentMessages': setShowAgentMessages(value) break + case 'renderHTML': + setRenderHTML(value) + break } } @@ -480,6 +490,13 @@ const ShareChatbot = ({ isSessionMemory, isAgentCanvas }) => { {textField(textInputPlaceholder, 'textInputPlaceholder', 'TextInput Placeholder', 'string', `Type question..`)} {colorField(textInputSendButtonColor, 'textInputSendButtonColor', 'TextIntput Send Button Color')} + <> + + Render HTML + + {booleanField(renderHTML, 'renderHTML', 'Render HTML on the chat')} + + {/*Session Memory Input*/} {isSessionMemory && ( <> From 35ce647a38130e8556f0b25d404cd008e2b7ca6e Mon Sep 17 00:00:00 2001 From: Chirag Date: Mon, 21 Oct 2024 20:32:31 +0530 Subject: [PATCH 03/20] Refactor ChatOpenAI_ChatModels to include stopSequence parameter (#3388) * Refactor ChatOpenAI_ChatModels to include stopSequence parameter * lint fix * Stop Sequence String will now be split by comma * Update ChatOpenAI.ts --------- Co-authored-by: Henry Heng --- .../nodes/chatmodels/ChatOpenAI/ChatOpenAI.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI.ts b/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI.ts index 5f3e1542904..e12524ad022 100644 --- a/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI.ts +++ b/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI.ts @@ -111,6 +111,15 @@ class ChatOpenAI_ChatModels implements INode { optional: true, additionalParams: true }, + { + label: 'Stop Sequence', + name: 'stopSequence', + type: 'string', + rows: 4, + optional: true, + description: 'List of stop words to use when generating. Use comma to separate multiple stop words.', + additionalParams: true + }, { label: 'BaseOptions', name: 'baseOptions', @@ -168,6 +177,7 @@ class ChatOpenAI_ChatModels implements INode { const frequencyPenalty = nodeData.inputs?.frequencyPenalty as string const presencePenalty = nodeData.inputs?.presencePenalty as string const timeout = nodeData.inputs?.timeout as string + const stopSequence = nodeData.inputs?.stopSequence as string const streaming = nodeData.inputs?.streaming as boolean const basePath = nodeData.inputs?.basepath as string const proxyUrl = nodeData.inputs?.proxyUrl as string @@ -199,6 +209,10 @@ class ChatOpenAI_ChatModels implements INode { if (presencePenalty) obj.presencePenalty = parseFloat(presencePenalty) if (timeout) obj.timeout = parseInt(timeout, 10) if (cache) obj.cache = cache + if (stopSequence) { + const stopSequenceArray = stopSequence.split(',').map((item) => item.trim()) + obj.stop = stopSequenceArray + } let parsedBaseOptions: any | undefined = undefined From 116d02d0bc871759ffab1646f514b56648d74a65 Mon Sep 17 00:00:00 2001 From: Henry Heng Date: Tue, 22 Oct 2024 00:49:20 +0100 Subject: [PATCH 04/20] Bugfix/Remove in-mem vector store from document store (#3395) remove in mem vector store from document store --- packages/server/src/services/documentstore/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/server/src/services/documentstore/index.ts b/packages/server/src/services/documentstore/index.ts index 53b3cefb263..0e4e1094031 100644 --- a/packages/server/src/services/documentstore/index.ts +++ b/packages/server/src/services/documentstore/index.ts @@ -1029,7 +1029,9 @@ const getEmbeddingProviders = async () => { const getVectorStoreProviders = async () => { try { const dbResponse = await nodesService.getAllNodesForCategory('Vector Stores') - return dbResponse.filter((node) => !node.tags?.includes('LlamaIndex') && node.name !== 'documentStoreVS') + return dbResponse.filter( + (node) => !node.tags?.includes('LlamaIndex') && node.name !== 'documentStoreVS' && node.name !== 'memoryVectorStore' + ) } catch (error) { throw new InternalFlowiseError( StatusCodes.INTERNAL_SERVER_ERROR, From 53e504c32fd6637696e7f30d871fdd2fca448c45 Mon Sep 17 00:00:00 2001 From: Henry Heng Date: Wed, 23 Oct 2024 11:00:46 +0100 Subject: [PATCH 05/20] Feature/Full File Uploads & Message Delete API (#3314) * add functionality for full file uploads, add remove messages from view dialog and API * add attachments swagger * update question to include uploadedFilesContent * make config dialog modal lg size --- .../api-documentation/src/yml/swagger.yml | 75 +++++++- .../nodes/documentloaders/File/File.ts | 28 ++- .../nodes/sequentialagents/Agent/Agent.ts | 14 +- .../nodes/sequentialagents/LLMNode/LLMNode.ts | 2 +- .../sequentialagents/ToolNode/ToolNode.ts | 20 +- packages/components/src/utils.ts | 25 ++- packages/server/src/Interface.ts | 2 +- .../src/controllers/attachments/index.ts | 15 ++ .../src/controllers/chat-messages/index.ts | 182 +++++++++++++----- .../server/src/controllers/stats/index.ts | 14 +- packages/server/src/index.ts | 3 +- .../server/src/routes/attachments/index.ts | 13 ++ packages/server/src/routes/index.ts | 2 + .../server/src/services/attachments/index.ts | 20 ++ .../src/services/chat-messages/index.ts | 36 +++- .../src/services/documentstore/index.ts | 4 +- packages/server/src/services/stats/index.ts | 4 +- packages/server/src/utils/buildAgentGraph.ts | 56 ++++-- packages/server/src/utils/buildChatflow.ts | 41 ++-- packages/server/src/utils/createAttachment.ts | 84 ++++++++ packages/server/src/utils/getChatMessage.ts | 21 +- packages/server/src/utils/getUploadsConfig.ts | 10 +- packages/server/src/utils/index.ts | 40 +++- packages/server/src/utils/upsertVector.ts | 4 +- packages/ui/src/api/attachments.js | 10 + .../ui/src/assets/images/fileAttachment.png | Bin 0 -> 14884 bytes .../dialog/ChatflowConfigurationDialog.jsx | 8 +- .../dialog/ViewMessagesDialog.jsx | 143 +++++++++++++- .../src/ui-component/extended/FileUpload.jsx | 122 ++++++++++++ .../src/ui-component/json/SelectVariable.jsx | 40 ++++ .../ui/src/views/chatmessage/ChatMessage.jsx | 165 ++++++++++++---- 31 files changed, 1011 insertions(+), 192 deletions(-) create mode 100644 packages/server/src/controllers/attachments/index.ts create mode 100644 packages/server/src/routes/attachments/index.ts create mode 100644 packages/server/src/services/attachments/index.ts create mode 100644 packages/server/src/utils/createAttachment.ts create mode 100644 packages/ui/src/api/attachments.js create mode 100644 packages/ui/src/assets/images/fileAttachment.png create mode 100644 packages/ui/src/ui-component/extended/FileUpload.jsx diff --git a/packages/api-documentation/src/yml/swagger.yml b/packages/api-documentation/src/yml/swagger.yml index 66531111d5e..da52b2f05c1 100644 --- a/packages/api-documentation/src/yml/swagger.yml +++ b/packages/api-documentation/src/yml/swagger.yml @@ -1,5 +1,6 @@ tags: - name: assistants + - name: attachments - name: chatmessage - name: chatflows - name: document-store @@ -270,6 +271,61 @@ paths: '500': description: Internal error + /attachments/{chatflowId}/{chatId}: + post: + tags: + - attachments + security: + - bearerAuth: [] + operationId: createAttachment + summary: Create attachments array + description: Return contents of the files in plain string format + parameters: + - in: path + name: chatflowId + required: true + schema: + type: string + description: Chatflow ID + - in: path + name: chatId + required: true + schema: + type: string + description: Chat ID + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + files: + type: array + items: + type: string + format: binary + description: Files to be uploaded + required: + - files + required: true + responses: + '200': + description: Attachments created successfully + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/CreateAttachmentResponse' + '400': + description: Invalid input provided + '404': + description: Chatflow or ChatId not found + '422': + description: Validation error + '500': + description: Internal server error + /chatflows: post: tags: @@ -1825,7 +1881,8 @@ components: properties: type: type: string - description: The type of file upload (e.g., 'file', 'audio', 'url') + enum: [audio, url, file, file:rag, file:full] + description: The type of file upload example: file name: type: string @@ -2193,6 +2250,22 @@ components: format: date-time description: Date and time when the feedback was created + CreateAttachmentResponse: + type: object + properties: + name: + type: string + description: Name of the file + mimeType: + type: string + description: Mime type of the file + size: + type: string + description: Size of the file + content: + type: string + description: Content of the file in string format + securitySchemes: bearerAuth: type: http diff --git a/packages/components/nodes/documentloaders/File/File.ts b/packages/components/nodes/documentloaders/File/File.ts index 0369787155f..da7a9a4106c 100644 --- a/packages/components/nodes/documentloaders/File/File.ts +++ b/packages/components/nodes/documentloaders/File/File.ts @@ -121,11 +121,22 @@ class File_DocumentLoaders implements INode { } const chatflowid = options.chatflowid - for (const file of files) { - if (!file) continue - const fileData = await getFileFromStorage(file, chatflowid) - const blob = new Blob([fileData]) - fileBlobs.push({ blob, ext: file.split('.').pop() || '' }) + // specific to createAttachment to get files from chatId + const retrieveAttachmentChatId = options.retrieveAttachmentChatId + if (retrieveAttachmentChatId) { + for (const file of files) { + if (!file) continue + const fileData = await getFileFromStorage(file, chatflowid, options.chatId) + const blob = new Blob([fileData]) + fileBlobs.push({ blob, ext: file.split('.').pop() || '' }) + } + } else { + for (const file of files) { + if (!file) continue + const fileData = await getFileFromStorage(file, chatflowid) + const blob = new Blob([fileData]) + fileBlobs.push({ blob, ext: file.split('.').pop() || '' }) + } } } else { if (totalFiles.startsWith('[') && totalFiles.endsWith(']')) { @@ -288,7 +299,12 @@ class MultiFileLoader extends BaseDocumentLoader { const loader = loaderFactory(fileBlob.blob) documents.push(...(await loader.load())) } else { - throw new Error(`Error loading file`) + const loader = new TextLoader(fileBlob.blob) + try { + documents.push(...(await loader.load())) + } catch (error) { + throw new Error(`Error loading file`) + } } } diff --git a/packages/components/nodes/sequentialagents/Agent/Agent.ts b/packages/components/nodes/sequentialagents/Agent/Agent.ts index 8153595cdbd..a5517095543 100644 --- a/packages/components/nodes/sequentialagents/Agent/Agent.ts +++ b/packages/components/nodes/sequentialagents/Agent/Agent.ts @@ -68,9 +68,9 @@ const howToUseCode = ` "sourceDocuments": [ { "pageContent": "This is the page content", - "metadata": "{foo: var}", + "metadata": "{foo: var}" } - ], + ] } \`\`\` @@ -102,10 +102,10 @@ const howToUse = ` |-----------|-----------| | user | john doe | -2. If you want to use the agent's output as the value to update state, it is available as available as \`$flow.output\` with the following structure: +2. If you want to use the Agent's output as the value to update state, it is available as available as \`$flow.output\` with the following structure: \`\`\`json { - "output": "Hello! How can I assist you today?", + "content": "Hello! How can I assist you today?", "usedTools": [ { "tool": "tool-name", @@ -116,9 +116,9 @@ const howToUse = ` "sourceDocuments": [ { "pageContent": "This is the page content", - "metadata": "{foo: var}", + "metadata": "{foo: var}" } - ], + ] } \`\`\` @@ -195,7 +195,7 @@ class Agent_SeqAgents implements INode { constructor() { this.label = 'Agent' this.name = 'seqAgent' - this.version = 3.0 + this.version = 3.1 this.type = 'Agent' this.icon = 'seqAgent.png' this.category = 'Sequential Agents' diff --git a/packages/components/nodes/sequentialagents/LLMNode/LLMNode.ts b/packages/components/nodes/sequentialagents/LLMNode/LLMNode.ts index a5a570064a1..0fcb7eb20e0 100644 --- a/packages/components/nodes/sequentialagents/LLMNode/LLMNode.ts +++ b/packages/components/nodes/sequentialagents/LLMNode/LLMNode.ts @@ -88,7 +88,7 @@ const howToUse = ` |-----------|-----------| | user | john doe | -2. If you want to use the agent's output as the value to update state, it is available as available as \`$flow.output\` with the following structure: +2. If you want to use the LLM Node's output as the value to update state, it is available as available as \`$flow.output\` with the following structure: \`\`\`json { "content": 'Hello! How can I assist you today?', diff --git a/packages/components/nodes/sequentialagents/ToolNode/ToolNode.ts b/packages/components/nodes/sequentialagents/ToolNode/ToolNode.ts index 2c4daee48e0..fda82aec95b 100644 --- a/packages/components/nodes/sequentialagents/ToolNode/ToolNode.ts +++ b/packages/components/nodes/sequentialagents/ToolNode/ToolNode.ts @@ -48,9 +48,9 @@ const howToUseCode = ` "sourceDocuments": [ { "pageContent": "This is the page content", - "metadata": "{foo: var}", + "metadata": "{foo: var}" } - ], + ] } ] \`\`\` @@ -64,7 +64,7 @@ const howToUseCode = ` */ return { - "sources": $flow.output[0].sourceDocuments + "sources": $flow.output[0].toolOutput } \`\`\` @@ -89,17 +89,19 @@ const howToUse = ` |-----------|-----------| | user | john doe | -2. If you want to use the agent's output as the value to update state, it is available as available as \`$flow.output\` with the following structure (array): +2. If you want to use the Tool Node's output as the value to update state, it is available as available as \`$flow.output\` with the following structure (array): \`\`\`json [ { - "content": "Hello! How can I assist you today?", + "tool": "tool's name", + "toolInput": {}, + "toolOutput": "tool's output content", "sourceDocuments": [ { "pageContent": "This is the page content", - "metadata": "{foo: var}", + "metadata": "{foo: var}" } - ], + ] } ] \`\`\` @@ -107,7 +109,7 @@ const howToUse = ` For example: | Key | Value | |--------------|-------------------------------------------| - | sources | \`$flow.output[0].sourceDocuments\` | + | sources | \`$flow.output[0].toolOutput\` | 3. You can get default flow config, including the current "state": - \`$flow.sessionId\` @@ -152,7 +154,7 @@ class ToolNode_SeqAgents implements INode { constructor() { this.label = 'Tool Node' this.name = 'seqToolNode' - this.version = 2.0 + this.version = 2.1 this.type = 'ToolNode' this.icon = 'toolNode.svg' this.category = 'Sequential Agents' diff --git a/packages/components/src/utils.ts b/packages/components/src/utils.ts index 387cca64f1c..b57191abbe2 100644 --- a/packages/components/src/utils.ts +++ b/packages/components/src/utils.ts @@ -5,7 +5,7 @@ import * as path from 'path' import { JSDOM } from 'jsdom' import { z } from 'zod' import { DataSource } from 'typeorm' -import { ICommonObject, IDatabaseEntity, IMessage, INodeData, IVariable, MessageContentImageUrl } from './Interface' +import { ICommonObject, IDatabaseEntity, IDocument, IMessage, INodeData, IVariable, MessageContentImageUrl } from './Interface' import { AES, enc } from 'crypto-js' import { AIMessage, HumanMessage, BaseMessage } from '@langchain/core/messages' import { getFileFromStorage } from './storageUtils' @@ -609,10 +609,11 @@ export const mapChatMessageToBaseMessage = async (chatmessages: any[] = []): Pro if (message.role === 'apiMessage' || message.type === 'apiMessage') { chatHistory.push(new AIMessage(message.content || '')) } else if (message.role === 'userMessage' || message.role === 'userMessage') { - // check for image uploads + // check for image/files uploads if (message.fileUploads) { // example: [{"type":"stored-file","name":"0_DiXc4ZklSTo3M8J4.jpg","mime":"image/jpeg"}] try { + let messageWithFileUploads = '' const uploads = JSON.parse(message.fileUploads) const imageContents: MessageContentImageUrl[] = [] for (const upload of uploads) { @@ -634,14 +635,32 @@ export const mapChatMessageToBaseMessage = async (chatmessages: any[] = []): Pro url: upload.data } }) + } else if (upload.type === 'stored-file:full') { + const fileLoaderNodeModule = await import('../nodes/documentloaders/File/File') + // @ts-ignore + const fileLoaderNodeInstance = new fileLoaderNodeModule.nodeClass() + const options = { + retrieveAttachmentChatId: true, + chatflowid: message.chatflowid, + chatId: message.chatId + } + const nodeData = { + inputs: { + txtFile: `FILE-STORAGE::${JSON.stringify([upload.name])}` + } + } + const documents: IDocument[] = await fileLoaderNodeInstance.init(nodeData, '', options) + const pageContents = documents.map((doc) => doc.pageContent).join('\n') + messageWithFileUploads += `${pageContents}\n\n` } } + const messageContent = messageWithFileUploads ? `${messageWithFileUploads}\n\n${message.content}` : message.content chatHistory.push( new HumanMessage({ content: [ { type: 'text', - text: message.content + text: messageContent }, ...imageContents ] diff --git a/packages/server/src/Interface.ts b/packages/server/src/Interface.ts index 04ac2519d05..c8b981d5015 100644 --- a/packages/server/src/Interface.ts +++ b/packages/server/src/Interface.ts @@ -4,7 +4,7 @@ export type MessageType = 'apiMessage' | 'userMessage' export type ChatflowType = 'CHATFLOW' | 'MULTIAGENT' -export enum chatType { +export enum ChatType { INTERNAL = 'INTERNAL', EXTERNAL = 'EXTERNAL' } diff --git a/packages/server/src/controllers/attachments/index.ts b/packages/server/src/controllers/attachments/index.ts new file mode 100644 index 00000000000..9f87f5eaf48 --- /dev/null +++ b/packages/server/src/controllers/attachments/index.ts @@ -0,0 +1,15 @@ +import { Request, Response, NextFunction } from 'express' +import attachmentsService from '../../services/attachments' + +const createAttachment = async (req: Request, res: Response, next: NextFunction) => { + try { + const apiResponse = await attachmentsService.createAttachment(req) + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +export default { + createAttachment +} diff --git a/packages/server/src/controllers/chat-messages/index.ts b/packages/server/src/controllers/chat-messages/index.ts index 903a313bc7b..0e914a474c7 100644 --- a/packages/server/src/controllers/chat-messages/index.ts +++ b/packages/server/src/controllers/chat-messages/index.ts @@ -1,13 +1,36 @@ import { Request, Response, NextFunction } from 'express' -import { ChatMessageRatingType, chatType, IReactFlowObject } from '../../Interface' +import { ChatMessageRatingType, ChatType, IReactFlowObject } from '../../Interface' import chatflowsService from '../../services/chatflows' import chatMessagesService from '../../services/chat-messages' -import { clearSessionMemory } from '../../utils' +import { aMonthAgo, clearSessionMemory, setDateToStartOrEndOfDay } from '../../utils' import { getRunningExpressApp } from '../../utils/getRunningExpressApp' -import { FindOptionsWhere } from 'typeorm' +import { Between, FindOptionsWhere } from 'typeorm' import { ChatMessage } from '../../database/entities/ChatMessage' import { InternalFlowiseError } from '../../errors/internalFlowiseError' import { StatusCodes } from 'http-status-codes' +import { utilGetChatMessage } from '../../utils/getChatMessage' + +const getFeedbackTypeFilters = (_feedbackTypeFilters: ChatMessageRatingType[]): ChatMessageRatingType[] | undefined => { + try { + let feedbackTypeFilters + const feedbackTypeFilterArray = JSON.parse(JSON.stringify(_feedbackTypeFilters)) + if ( + feedbackTypeFilterArray.includes(ChatMessageRatingType.THUMBS_UP) && + feedbackTypeFilterArray.includes(ChatMessageRatingType.THUMBS_DOWN) + ) { + feedbackTypeFilters = [ChatMessageRatingType.THUMBS_UP, ChatMessageRatingType.THUMBS_DOWN] + } else if (feedbackTypeFilterArray.includes(ChatMessageRatingType.THUMBS_UP)) { + feedbackTypeFilters = [ChatMessageRatingType.THUMBS_UP] + } else if (feedbackTypeFilterArray.includes(ChatMessageRatingType.THUMBS_DOWN)) { + feedbackTypeFilters = [ChatMessageRatingType.THUMBS_DOWN] + } else { + feedbackTypeFilters = undefined + } + return feedbackTypeFilters + } catch (e) { + return _feedbackTypeFilters + } +} const createChatMessage = async (req: Request, res: Response, next: NextFunction) => { try { @@ -26,16 +49,16 @@ const createChatMessage = async (req: Request, res: Response, next: NextFunction const getAllChatMessages = async (req: Request, res: Response, next: NextFunction) => { try { - let chatTypeFilter = req.query?.chatType as chatType | undefined + let chatTypeFilter = req.query?.chatType as ChatType | undefined if (chatTypeFilter) { try { const chatTypeFilterArray = JSON.parse(chatTypeFilter) - if (chatTypeFilterArray.includes(chatType.EXTERNAL) && chatTypeFilterArray.includes(chatType.INTERNAL)) { + if (chatTypeFilterArray.includes(ChatType.EXTERNAL) && chatTypeFilterArray.includes(ChatType.INTERNAL)) { chatTypeFilter = undefined - } else if (chatTypeFilterArray.includes(chatType.EXTERNAL)) { - chatTypeFilter = chatType.EXTERNAL - } else if (chatTypeFilterArray.includes(chatType.INTERNAL)) { - chatTypeFilter = chatType.INTERNAL + } else if (chatTypeFilterArray.includes(ChatType.EXTERNAL)) { + chatTypeFilter = ChatType.EXTERNAL + } else if (chatTypeFilterArray.includes(ChatType.INTERNAL)) { + chatTypeFilter = ChatType.INTERNAL } } catch (e) { return res.status(500).send(e) @@ -51,23 +74,7 @@ const getAllChatMessages = async (req: Request, res: Response, next: NextFunctio const feedback = req.query?.feedback as boolean | undefined let feedbackTypeFilters = req.query?.feedbackType as ChatMessageRatingType[] | undefined if (feedbackTypeFilters) { - try { - const feedbackTypeFilterArray = JSON.parse(JSON.stringify(feedbackTypeFilters)) - if ( - feedbackTypeFilterArray.includes(ChatMessageRatingType.THUMBS_UP) && - feedbackTypeFilterArray.includes(ChatMessageRatingType.THUMBS_DOWN) - ) { - feedbackTypeFilters = [ChatMessageRatingType.THUMBS_UP, ChatMessageRatingType.THUMBS_DOWN] - } else if (feedbackTypeFilterArray.includes(ChatMessageRatingType.THUMBS_UP)) { - feedbackTypeFilters = [ChatMessageRatingType.THUMBS_UP] - } else if (feedbackTypeFilterArray.includes(ChatMessageRatingType.THUMBS_DOWN)) { - feedbackTypeFilters = [ChatMessageRatingType.THUMBS_DOWN] - } else { - feedbackTypeFilters = undefined - } - } catch (e) { - return res.status(500).send(e) - } + feedbackTypeFilters = getFeedbackTypeFilters(feedbackTypeFilters) } if (typeof req.params === 'undefined' || !req.params.id) { throw new InternalFlowiseError( @@ -105,9 +112,13 @@ const getAllInternalChatMessages = async (req: Request, res: Response, next: Nex const startDate = req.query?.startDate as string | undefined const endDate = req.query?.endDate as string | undefined const feedback = req.query?.feedback as boolean | undefined + let feedbackTypeFilters = req.query?.feedbackType as ChatMessageRatingType[] | undefined + if (feedbackTypeFilters) { + feedbackTypeFilters = getFeedbackTypeFilters(feedbackTypeFilters) + } const apiResponse = await chatMessagesService.getAllInternalChatMessages( req.params.id, - chatType.INTERNAL, + ChatType.INTERNAL, sortOrder, chatId, memoryType, @@ -115,7 +126,8 @@ const getAllInternalChatMessages = async (req: Request, res: Response, next: Nex startDate, endDate, messageId, - feedback + feedback, + feedbackTypeFilters ) return res.json(parseAPIResponse(apiResponse)) } catch (error) { @@ -123,7 +135,6 @@ const getAllInternalChatMessages = async (req: Request, res: Response, next: Nex } } -//Delete all chatmessages from chatId const removeAllChatMessages = async (req: Request, res: Response, next: NextFunction) => { try { const appServer = getRunningExpressApp() @@ -138,35 +149,102 @@ const removeAllChatMessages = async (req: Request, res: Response, next: NextFunc if (!chatflow) { return res.status(404).send(`Chatflow ${req.params.id} not found`) } + const flowData = chatflow.flowData + const parsedFlowData: IReactFlowObject = JSON.parse(flowData) + const nodes = parsedFlowData.nodes const chatId = req.query?.chatId as string const memoryType = req.query?.memoryType as string | undefined const sessionId = req.query?.sessionId as string | undefined - const chatType = req.query?.chatType as string | undefined + const _chatType = req.query?.chatType as string | undefined + const startDate = req.query?.startDate as string | undefined + const endDate = req.query?.endDate as string | undefined const isClearFromViewMessageDialog = req.query?.isClearFromViewMessageDialog as string | undefined - const flowData = chatflow.flowData - const parsedFlowData: IReactFlowObject = JSON.parse(flowData) - const nodes = parsedFlowData.nodes - try { - await clearSessionMemory( - nodes, - appServer.nodesPool.componentNodes, - chatId, - appServer.AppDataSource, - sessionId, - memoryType, - isClearFromViewMessageDialog - ) - } catch (e) { - return res.status(500).send('Error clearing chat messages') + let feedbackTypeFilters = req.query?.feedbackType as ChatMessageRatingType[] | undefined + if (feedbackTypeFilters) { + feedbackTypeFilters = getFeedbackTypeFilters(feedbackTypeFilters) } - const deleteOptions: FindOptionsWhere = { chatflowid } - if (chatId) deleteOptions.chatId = chatId - if (memoryType) deleteOptions.memoryType = memoryType - if (sessionId) deleteOptions.sessionId = sessionId - if (chatType) deleteOptions.chatType = chatType - const apiResponse = await chatMessagesService.removeAllChatMessages(chatId, chatflowid, deleteOptions) - return res.json(apiResponse) + if (!chatId) { + const isFeedback = feedbackTypeFilters?.length ? true : false + const hardDelete = req.query?.hardDelete as boolean | undefined + const messages = await utilGetChatMessage( + chatflowid, + _chatType as ChatType | undefined, + undefined, + undefined, + undefined, + undefined, + startDate, + endDate, + undefined, + isFeedback, + feedbackTypeFilters + ) + const messageIds = messages.map((message) => message.id) + + // Categorize by chatId_memoryType_sessionId + const chatIdMap = new Map() + messages.forEach((message) => { + const chatId = message.chatId + const memoryType = message.memoryType + const sessionId = message.sessionId + const composite_key = `${chatId}_${memoryType}_${sessionId}` + if (!chatIdMap.has(composite_key)) { + chatIdMap.set(composite_key, []) + } + chatIdMap.get(composite_key)?.push(message) + }) + + // If hardDelete is ON, we clearSessionMemory from third party integrations + if (hardDelete) { + for (const [composite_key] of chatIdMap) { + const [chatId, memoryType, sessionId] = composite_key.split('_') + try { + await clearSessionMemory( + nodes, + appServer.nodesPool.componentNodes, + chatId, + appServer.AppDataSource, + sessionId, + memoryType, + isClearFromViewMessageDialog + ) + } catch (e) { + console.error('Error clearing chat messages') + } + } + } + + const apiResponse = await chatMessagesService.removeChatMessagesByMessageIds(chatflowid, chatIdMap, messageIds) + return res.json(apiResponse) + } else { + try { + await clearSessionMemory( + nodes, + appServer.nodesPool.componentNodes, + chatId, + appServer.AppDataSource, + sessionId, + memoryType, + isClearFromViewMessageDialog + ) + } catch (e) { + return res.status(500).send('Error clearing chat messages') + } + + const deleteOptions: FindOptionsWhere = { chatflowid } + if (chatId) deleteOptions.chatId = chatId + if (memoryType) deleteOptions.memoryType = memoryType + if (sessionId) deleteOptions.sessionId = sessionId + if (_chatType) deleteOptions.chatType = _chatType + if (startDate && endDate) { + const fromDate = setDateToStartOrEndOfDay(startDate, 'start') + const toDate = setDateToStartOrEndOfDay(endDate, 'end') + deleteOptions.createdDate = Between(fromDate ?? aMonthAgo(), toDate ?? new Date()) + } + const apiResponse = await chatMessagesService.removeAllChatMessages(chatId, chatflowid, deleteOptions) + return res.json(apiResponse) + } } catch (error) { next(error) } diff --git a/packages/server/src/controllers/stats/index.ts b/packages/server/src/controllers/stats/index.ts index a96464514f5..20a0bd7fa82 100644 --- a/packages/server/src/controllers/stats/index.ts +++ b/packages/server/src/controllers/stats/index.ts @@ -1,7 +1,7 @@ import { StatusCodes } from 'http-status-codes' import { Request, Response, NextFunction } from 'express' import statsService from '../../services/stats' -import { ChatMessageRatingType, chatType } from '../../Interface' +import { ChatMessageRatingType, ChatType } from '../../Interface' import { InternalFlowiseError } from '../../errors/internalFlowiseError' import { getErrorMessage } from '../../errors/utils' @@ -11,19 +11,19 @@ const getChatflowStats = async (req: Request, res: Response, next: NextFunction) throw new InternalFlowiseError(StatusCodes.PRECONDITION_FAILED, `Error: statsController.getChatflowStats - id not provided!`) } const chatflowid = req.params.id - let chatTypeFilter = req.query?.chatType as chatType | undefined + let chatTypeFilter = req.query?.chatType as ChatType | undefined const startDate = req.query?.startDate as string | undefined const endDate = req.query?.endDate as string | undefined let feedbackTypeFilters = req.query?.feedbackType as ChatMessageRatingType[] | undefined if (chatTypeFilter) { try { const chatTypeFilterArray = JSON.parse(chatTypeFilter) - if (chatTypeFilterArray.includes(chatType.EXTERNAL) && chatTypeFilterArray.includes(chatType.INTERNAL)) { + if (chatTypeFilterArray.includes(ChatType.EXTERNAL) && chatTypeFilterArray.includes(ChatType.INTERNAL)) { chatTypeFilter = undefined - } else if (chatTypeFilterArray.includes(chatType.EXTERNAL)) { - chatTypeFilter = chatType.EXTERNAL - } else if (chatTypeFilterArray.includes(chatType.INTERNAL)) { - chatTypeFilter = chatType.INTERNAL + } else if (chatTypeFilterArray.includes(ChatType.EXTERNAL)) { + chatTypeFilter = ChatType.EXTERNAL + } else if (chatTypeFilterArray.includes(ChatType.INTERNAL)) { + chatTypeFilter = ChatType.INTERNAL } } catch (e) { throw new InternalFlowiseError( diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 5285e5d28f2..1bdde15a174 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -136,7 +136,8 @@ export class App { '/api/v1/get-upload-file', '/api/v1/ip', '/api/v1/ping', - '/api/v1/version' + '/api/v1/version', + '/api/v1/attachments' ] const URL_CASE_INSENSITIVE_REGEX: RegExp = /\/api\/v1\//i const URL_CASE_SENSITIVE_REGEX: RegExp = /\/api\/v1\// diff --git a/packages/server/src/routes/attachments/index.ts b/packages/server/src/routes/attachments/index.ts new file mode 100644 index 00000000000..abe09a6c89c --- /dev/null +++ b/packages/server/src/routes/attachments/index.ts @@ -0,0 +1,13 @@ +import express from 'express' +import multer from 'multer' +import path from 'path' +import attachmentsController from '../../controllers/attachments' + +const router = express.Router() + +const upload = multer({ dest: `${path.join(__dirname, '..', '..', '..', 'uploads')}/` }) + +// CREATE +router.post('/:chatflowId/:chatId', upload.array('files'), attachmentsController.createAttachment) + +export default router diff --git a/packages/server/src/routes/index.ts b/packages/server/src/routes/index.ts index 3df0dd30438..89fb7350a87 100644 --- a/packages/server/src/routes/index.ts +++ b/packages/server/src/routes/index.ts @@ -1,6 +1,7 @@ import express from 'express' import apikeyRouter from './apikey' import assistantsRouter from './assistants' +import attachmentsRouter from './attachments' import chatMessageRouter from './chat-messages' import chatflowsRouter from './chatflows' import chatflowsStreamingRouter from './chatflows-streaming' @@ -47,6 +48,7 @@ const router = express.Router() router.use('/ping', pingRouter) router.use('/apikey', apikeyRouter) router.use('/assistants', assistantsRouter) +router.use('/attachments', attachmentsRouter) router.use('/chatflows', chatflowsRouter) router.use('/chatflows-streaming', chatflowsStreamingRouter) router.use('/chatmessage', chatMessageRouter) diff --git a/packages/server/src/services/attachments/index.ts b/packages/server/src/services/attachments/index.ts new file mode 100644 index 00000000000..ea9814da534 --- /dev/null +++ b/packages/server/src/services/attachments/index.ts @@ -0,0 +1,20 @@ +import { Request } from 'express' +import { StatusCodes } from 'http-status-codes' +import { createFileAttachment } from '../../utils/createAttachment' +import { InternalFlowiseError } from '../../errors/internalFlowiseError' +import { getErrorMessage } from '../../errors/utils' + +const createAttachment = async (req: Request) => { + try { + return await createFileAttachment(req) + } catch (error) { + throw new InternalFlowiseError( + StatusCodes.INTERNAL_SERVER_ERROR, + `Error: attachmentService.createAttachment - ${getErrorMessage(error)}` + ) + } +} + +export default { + createAttachment +} diff --git a/packages/server/src/services/chat-messages/index.ts b/packages/server/src/services/chat-messages/index.ts index 9f2d53f7e03..621b37d1a72 100644 --- a/packages/server/src/services/chat-messages/index.ts +++ b/packages/server/src/services/chat-messages/index.ts @@ -1,6 +1,6 @@ import { DeleteResult, FindOptionsWhere } from 'typeorm' import { StatusCodes } from 'http-status-codes' -import { ChatMessageRatingType, chatType, IChatMessage } from '../../Interface' +import { ChatMessageRatingType, ChatType, IChatMessage } from '../../Interface' import { utilGetChatMessage } from '../../utils/getChatMessage' import { utilAddChatMessage } from '../../utils/addChatMesage' import { getRunningExpressApp } from '../../utils/getRunningExpressApp' @@ -27,7 +27,7 @@ const createChatMessage = async (chatMessage: Partial) => { // Get all chatmessages from chatflowid const getAllChatMessages = async ( chatflowId: string, - chatTypeFilter: chatType | undefined, + chatTypeFilter: ChatType | undefined, sortOrder: string = 'ASC', chatId?: string, memoryType?: string, @@ -64,7 +64,7 @@ const getAllChatMessages = async ( // Get internal chatmessages from chatflowid const getAllInternalChatMessages = async ( chatflowId: string, - chatTypeFilter: chatType | undefined, + chatTypeFilter: ChatType | undefined, sortOrder: string = 'ASC', chatId?: string, memoryType?: string, @@ -128,6 +128,35 @@ const removeAllChatMessages = async ( } } +const removeChatMessagesByMessageIds = async ( + chatflowid: string, + chatIdMap: Map, + messageIds: string[] +): Promise => { + try { + const appServer = getRunningExpressApp() + + for (const [composite_key] of chatIdMap) { + const [chatId] = composite_key.split('_') + + // Remove all related feedback records + const feedbackDeleteOptions: FindOptionsWhere = { chatId } + await appServer.AppDataSource.getRepository(ChatMessageFeedback).delete(feedbackDeleteOptions) + + // Delete all uploads corresponding to this chatflow/chatId + await removeFilesFromStorage(chatflowid, chatId) + } + + const dbResponse = await appServer.AppDataSource.getRepository(ChatMessage).delete(messageIds) + return dbResponse + } catch (error) { + throw new InternalFlowiseError( + StatusCodes.INTERNAL_SERVER_ERROR, + `Error: chatMessagesService.removeAllChatMessages - ${getErrorMessage(error)}` + ) + } +} + const abortChatMessage = async (chatId: string, chatflowid: string) => { try { const appServer = getRunningExpressApp() @@ -155,5 +184,6 @@ export default { getAllChatMessages, getAllInternalChatMessages, removeAllChatMessages, + removeChatMessagesByMessageIds, abortChatMessage } diff --git a/packages/server/src/services/documentstore/index.ts b/packages/server/src/services/documentstore/index.ts index 0e4e1094031..d5ef3dd9ea1 100644 --- a/packages/server/src/services/documentstore/index.ts +++ b/packages/server/src/services/documentstore/index.ts @@ -9,7 +9,7 @@ import { removeSpecificFileFromStorage } from 'flowise-components' import { - chatType, + ChatType, DocumentStoreStatus, IDocumentStoreFileChunkPagedResponse, IDocumentStoreLoader, @@ -995,7 +995,7 @@ const _insertIntoVectorStoreWorkerThread = async (data: ICommonObject) => { data: { version: await getAppVersion(), chatlowId: chatflowid, - type: chatType.INTERNAL, + type: ChatType.INTERNAL, flowGraph: omit(indexResult['result'], ['totalKeys', 'addedDocs']) } }) diff --git a/packages/server/src/services/stats/index.ts b/packages/server/src/services/stats/index.ts index 8d9b99da524..c4c0e52887c 100644 --- a/packages/server/src/services/stats/index.ts +++ b/packages/server/src/services/stats/index.ts @@ -1,5 +1,5 @@ import { StatusCodes } from 'http-status-codes' -import { ChatMessageRatingType, chatType } from '../../Interface' +import { ChatMessageRatingType, ChatType } from '../../Interface' import { ChatMessage } from '../../database/entities/ChatMessage' import { utilGetChatMessage } from '../../utils/getChatMessage' import { ChatMessageFeedback } from '../../database/entities/ChatMessageFeedback' @@ -9,7 +9,7 @@ import { getErrorMessage } from '../../errors/utils' // get stats for showing in chatflow const getChatflowStats = async ( chatflowid: string, - chatTypeFilter: chatType | undefined, + chatTypeFilter: ChatType | undefined, startDate?: string, endDate?: string, messageId?: string, diff --git a/packages/server/src/utils/buildAgentGraph.ts b/packages/server/src/utils/buildAgentGraph.ts index 3104737117d..6acf641bd7f 100644 --- a/packages/server/src/utils/buildAgentGraph.ts +++ b/packages/server/src/utils/buildAgentGraph.ts @@ -63,7 +63,8 @@ export const buildAgentGraph = async ( isInternal: boolean, baseURL?: string, sseStreamer?: IServerSideEventStreamer, - shouldStreamResponse?: boolean + shouldStreamResponse?: boolean, + uploadedFilesContent?: string ): Promise => { try { const appServer = getRunningExpressApp() @@ -129,7 +130,8 @@ export const buildAgentGraph = async ( cachePool: appServer.cachePool, isUpsert: false, uploads: incomingInput.uploads, - baseURL + baseURL, + uploadedFilesContent }) const options = { @@ -188,7 +190,8 @@ export const buildAgentGraph = async ( chatHistory, incomingInput?.overrideConfig, sessionId || chatId, - seqAgentNodes.some((node) => node.data.inputs?.summarization) + seqAgentNodes.some((node) => node.data.inputs?.summarization), + uploadedFilesContent ) } else { isSequential = true @@ -204,7 +207,8 @@ export const buildAgentGraph = async ( chatHistory, incomingInput?.overrideConfig, sessionId || chatId, - incomingInput.action + incomingInput.action, + uploadedFilesContent ) } @@ -348,7 +352,6 @@ export const buildAgentGraph = async ( if (isSequential && !finalResult && agentReasoning.length) { const lastMessages = agentReasoning[agentReasoning.length - 1].messages const lastAgentReasoningMessage = lastMessages[lastMessages.length - 1] - // If last message is an AI Message with tool calls, that means the last node was interrupted if (lastMessageRaw.tool_calls && lastMessageRaw.tool_calls.length > 0) { // The last node that got interrupted @@ -456,6 +459,7 @@ export const buildAgentGraph = async ( * @param {ICommonObject} overrideConfig * @param {string} threadId * @param {boolean} summarization + * @param {string} uploadedFilesContent, */ const compileMultiAgentsGraph = async ( chatflow: IChatFlow, @@ -470,7 +474,8 @@ const compileMultiAgentsGraph = async ( chatHistory: IMessage[] = [], overrideConfig?: ICommonObject, threadId?: string, - summarization?: boolean + summarization?: boolean, + uploadedFilesContent?: string ) => { const appServer = getRunningExpressApp() const channels: ITeamState = { @@ -502,7 +507,15 @@ const compileMultiAgentsGraph = async ( let flowNodeData = cloneDeep(workerNode.data) if (overrideConfig) flowNodeData = replaceInputsWithConfig(flowNodeData, overrideConfig) - flowNodeData = await resolveVariables(appServer.AppDataSource, flowNodeData, reactflowNodes, question, chatHistory, overrideConfig) + flowNodeData = await resolveVariables( + appServer.AppDataSource, + flowNodeData, + reactflowNodes, + question, + chatHistory, + overrideConfig, + uploadedFilesContent + ) try { const workerResult: IMultiAgentNode = await newNodeInstance.init(flowNodeData, question, options) @@ -533,7 +546,15 @@ const compileMultiAgentsGraph = async ( let flowNodeData = cloneDeep(supervisorNode.data) if (overrideConfig) flowNodeData = replaceInputsWithConfig(flowNodeData, overrideConfig) - flowNodeData = await resolveVariables(appServer.AppDataSource, flowNodeData, reactflowNodes, question, chatHistory, overrideConfig) + flowNodeData = await resolveVariables( + appServer.AppDataSource, + flowNodeData, + reactflowNodes, + question, + chatHistory, + overrideConfig, + uploadedFilesContent + ) if (flowNodeData.inputs) flowNodeData.inputs.workerNodes = supervisorWorkers[supervisor] @@ -603,9 +624,10 @@ const compileMultiAgentsGraph = async ( } // Return stream result as we should only have 1 supervisor + const finalQuestion = uploadedFilesContent ? `${uploadedFilesContent}\n\n${question}` : question return await graph.stream( { - messages: [...prependMessages, new HumanMessage({ content: question })] + messages: [...prependMessages, new HumanMessage({ content: finalQuestion })] }, { recursionLimit: supervisorResult?.recursionLimit ?? 100, callbacks: [loggerHandler, ...callbacks], configurable: config } ) @@ -641,7 +663,8 @@ const compileSeqAgentsGraph = async ( chatHistory: IMessage[] = [], overrideConfig?: ICommonObject, threadId?: string, - action?: IAction + action?: IAction, + uploadedFilesContent?: string ) => { const appServer = getRunningExpressApp() @@ -693,7 +716,15 @@ const compileSeqAgentsGraph = async ( flowNodeData = cloneDeep(node.data) if (overrideConfig) flowNodeData = replaceInputsWithConfig(flowNodeData, overrideConfig) - flowNodeData = await resolveVariables(appServer.AppDataSource, flowNodeData, reactflowNodes, question, chatHistory, overrideConfig) + flowNodeData = await resolveVariables( + appServer.AppDataSource, + flowNodeData, + reactflowNodes, + question, + chatHistory, + overrideConfig, + uploadedFilesContent + ) const seqAgentNode: ISeqAgentNode = await newNodeInstance.init(flowNodeData, question, options) return seqAgentNode @@ -997,8 +1028,9 @@ const compileSeqAgentsGraph = async ( } } + const finalQuestion = uploadedFilesContent ? `${uploadedFilesContent}\n\n${question}` : question let humanMsg: { messages: HumanMessage[] | ToolMessage[] } | null = { - messages: [...prependMessages, new HumanMessage({ content: question })] + messages: [...prependMessages, new HumanMessage({ content: finalQuestion })] } if (action && action.mapping && question === action.mapping.approve) { diff --git a/packages/server/src/utils/buildChatflow.ts b/packages/server/src/utils/buildChatflow.ts index 2c625ab8681..e2d2db12b7e 100644 --- a/packages/server/src/utils/buildChatflow.ts +++ b/packages/server/src/utils/buildChatflow.ts @@ -19,7 +19,7 @@ import { IReactFlowObject, IReactFlowNode, IDepthQueue, - chatType, + ChatType, IChatMessage, IChatFlow, IReactFlowEdge @@ -88,12 +88,14 @@ export const utilBuildChatflow = async (req: Request, isInternal: boolean = fals } let fileUploads: IFileUpload[] = [] + let uploadedFilesContent = '' if (incomingInput.uploads) { fileUploads = incomingInput.uploads for (let i = 0; i < fileUploads.length; i += 1) { const upload = fileUploads[i] - if ((upload.type === 'file' || upload.type === 'audio') && upload.data) { + // if upload in an image, a rag file, or audio + if ((upload.type === 'file' || upload.type === 'file:rag' || upload.type === 'audio') && upload.data) { const filename = upload.name const splitDataURI = upload.data.split(',') const bf = Buffer.from(splitDataURI.pop() || '', 'base64') @@ -139,6 +141,13 @@ export const utilBuildChatflow = async (req: Request, isInternal: boolean = fals } } } + + if (upload.type === 'file:full' && upload.data) { + upload.type = 'stored-file:full' + // Omit upload.data since we don't store the content in database + uploadedFilesContent += `${upload.data}\n\n` + fileUploads[i] = omit(upload, ['data']) + } } } @@ -229,7 +238,8 @@ export const utilBuildChatflow = async (req: Request, isInternal: boolean = fals edges, baseURL, appServer.sseStreamer, - true + true, + uploadedFilesContent ) } @@ -345,6 +355,7 @@ export const utilBuildChatflow = async (req: Request, isInternal: boolean = fals apiMessageId, componentNodes: appServer.nodesPool.componentNodes, question: incomingInput.question, + uploadedFilesContent, chatHistory, chatId, sessionId: sessionId ?? '', @@ -384,7 +395,8 @@ export const utilBuildChatflow = async (req: Request, isInternal: boolean = fals reactFlowNodes, incomingInput.question, chatHistory, - flowData + flowData, + uploadedFilesContent ) nodeToExecuteData = reactFlowNodeData @@ -398,6 +410,7 @@ export const utilBuildChatflow = async (req: Request, isInternal: boolean = fals const nodeInstance = new nodeModule.nodeClass({ sessionId }) isStreamValid = (req.body.streaming === 'true' || req.body.streaming === true) && isStreamValid + const finalQuestion = uploadedFilesContent ? `${uploadedFilesContent}\n\n${incomingInput.question}` : incomingInput.question const runParams = { chatId, @@ -411,7 +424,7 @@ export const utilBuildChatflow = async (req: Request, isInternal: boolean = fals prependMessages } - let result = await nodeInstance.run(nodeToExecuteData, incomingInput.question, { + let result = await nodeInstance.run(nodeToExecuteData, finalQuestion, { ...runParams, ...(isStreamValid && { sseStreamer: appServer.sseStreamer, shouldStreamResponse: true }) }) @@ -427,7 +440,7 @@ export const utilBuildChatflow = async (req: Request, isInternal: boolean = fals role: 'userMessage', content: incomingInput.question, chatflowid, - chatType: isInternal ? chatType.INTERNAL : chatType.EXTERNAL, + chatType: isInternal ? ChatType.INTERNAL : ChatType.EXTERNAL, chatId, memoryType, sessionId, @@ -447,7 +460,7 @@ export const utilBuildChatflow = async (req: Request, isInternal: boolean = fals role: 'apiMessage', content: resultText, chatflowid, - chatType: isInternal ? chatType.INTERNAL : chatType.EXTERNAL, + chatType: isInternal ? ChatType.INTERNAL : ChatType.EXTERNAL, chatId, memoryType, sessionId @@ -476,7 +489,7 @@ export const utilBuildChatflow = async (req: Request, isInternal: boolean = fals version: await getAppVersion(), chatflowId: chatflowid, chatId, - type: isInternal ? chatType.INTERNAL : chatType.EXTERNAL, + type: isInternal ? ChatType.INTERNAL : ChatType.EXTERNAL, flowGraph: getTelemetryFlowObj(nodes, edges) }) @@ -517,7 +530,8 @@ const utilBuildAgentResponse = async ( edges: IReactFlowEdge[], baseURL?: string, sseStreamer?: IServerSideEventStreamer, - shouldStreamResponse?: boolean + shouldStreamResponse?: boolean, + uploadedFilesContent?: string ) => { try { const appServer = getRunningExpressApp() @@ -530,7 +544,8 @@ const utilBuildAgentResponse = async ( isInternal, baseURL, sseStreamer, - shouldStreamResponse + shouldStreamResponse, + uploadedFilesContent ) if (streamResults) { const { finalResult, finalAction, sourceDocuments, artifacts, usedTools, agentReasoning } = streamResults @@ -538,7 +553,7 @@ const utilBuildAgentResponse = async ( role: 'userMessage', content: incomingInput.question, chatflowid: agentflow.id, - chatType: isInternal ? chatType.INTERNAL : chatType.EXTERNAL, + chatType: isInternal ? ChatType.INTERNAL : ChatType.EXTERNAL, chatId, memoryType, sessionId, @@ -553,7 +568,7 @@ const utilBuildAgentResponse = async ( role: 'apiMessage', content: finalResult, chatflowid: agentflow.id, - chatType: isInternal ? chatType.INTERNAL : chatType.EXTERNAL, + chatType: isInternal ? ChatType.INTERNAL : ChatType.EXTERNAL, chatId, memoryType, sessionId @@ -581,7 +596,7 @@ const utilBuildAgentResponse = async ( version: await getAppVersion(), agentflowId: agentflow.id, chatId, - type: isInternal ? chatType.INTERNAL : chatType.EXTERNAL, + type: isInternal ? ChatType.INTERNAL : ChatType.EXTERNAL, flowGraph: getTelemetryFlowObj(nodes, edges) }) diff --git a/packages/server/src/utils/createAttachment.ts b/packages/server/src/utils/createAttachment.ts new file mode 100644 index 00000000000..544a08aa70d --- /dev/null +++ b/packages/server/src/utils/createAttachment.ts @@ -0,0 +1,84 @@ +import { Request } from 'express' +import * as path from 'path' +import * as fs from 'fs' +import { addArrayFilesToStorage, IDocument, mapExtToInputField, mapMimeTypeToInputField } from 'flowise-components' +import { getRunningExpressApp } from './getRunningExpressApp' +import { getErrorMessage } from '../errors/utils' + +/** + * Create attachment + * @param {Request} req + */ +export const createFileAttachment = async (req: Request) => { + const appServer = getRunningExpressApp() + + const chatflowid = req.params.chatflowId + if (!chatflowid) { + throw new Error( + 'Params chatflowId is required! Please provide chatflowId and chatId in the URL: /api/v1/attachments/:chatflowId/:chatId' + ) + } + + const chatId = req.params.chatId + if (!chatId) { + throw new Error( + 'Params chatId is required! Please provide chatflowId and chatId in the URL: /api/v1/attachments/:chatflowId/:chatId' + ) + } + + // Find FileLoader node + const fileLoaderComponent = appServer.nodesPool.componentNodes['fileLoader'] + const fileLoaderNodeInstanceFilePath = fileLoaderComponent.filePath as string + const fileLoaderNodeModule = await import(fileLoaderNodeInstanceFilePath) + const fileLoaderNodeInstance = new fileLoaderNodeModule.nodeClass() + const options = { + retrieveAttachmentChatId: true, + chatflowid, + chatId + } + const files = (req.files as Express.Multer.File[]) || [] + const fileAttachments = [] + if (files.length) { + for (const file of files) { + const fileBuffer = fs.readFileSync(file.path) + const fileNames: string[] = [] + const storagePath = await addArrayFilesToStorage(file.mimetype, fileBuffer, file.originalname, fileNames, chatflowid, chatId) + + const fileInputFieldFromMimeType = mapMimeTypeToInputField(file.mimetype) + + const fileExtension = path.extname(file.originalname) + + const fileInputFieldFromExt = mapExtToInputField(fileExtension) + + let fileInputField = 'txtFile' + + if (fileInputFieldFromExt !== 'txtFile') { + fileInputField = fileInputFieldFromExt + } else if (fileInputFieldFromMimeType !== 'txtFile') { + fileInputField = fileInputFieldFromExt + } + + fs.unlinkSync(file.path) + + try { + const nodeData = { + inputs: { + [fileInputField]: storagePath + } + } + const documents: IDocument[] = await fileLoaderNodeInstance.init(nodeData, '', options) + const pageContents = documents.map((doc) => doc.pageContent).join('\n') + fileAttachments.push({ + name: file.originalname, + mimeType: file.mimetype, + size: file.size, + content: pageContents + }) + } catch (error) { + throw new Error(`Failed operation: createFileAttachment - ${getErrorMessage(error)}`) + } + } + } + + return fileAttachments +} diff --git a/packages/server/src/utils/getChatMessage.ts b/packages/server/src/utils/getChatMessage.ts index 459d62f73a7..7ffde0eb8cf 100644 --- a/packages/server/src/utils/getChatMessage.ts +++ b/packages/server/src/utils/getChatMessage.ts @@ -1,13 +1,14 @@ import { MoreThanOrEqual, LessThanOrEqual } from 'typeorm' -import { ChatMessageRatingType, chatType } from '../Interface' +import { ChatMessageRatingType, ChatType } from '../Interface' import { ChatMessage } from '../database/entities/ChatMessage' import { ChatMessageFeedback } from '../database/entities/ChatMessageFeedback' import { getRunningExpressApp } from '../utils/getRunningExpressApp' +import { aMonthAgo, setDateToStartOrEndOfDay } from '.' /** * Method that get chat messages. * @param {string} chatflowid - * @param {chatType} chatType + * @param {ChatType} chatType * @param {string} sortOrder * @param {string} chatId * @param {string} memoryType @@ -19,7 +20,7 @@ import { getRunningExpressApp } from '../utils/getRunningExpressApp' */ export const utilGetChatMessage = async ( chatflowid: string, - chatType: chatType | undefined, + chatType: ChatType | undefined, sortOrder: string = 'ASC', chatId?: string, memoryType?: string, @@ -31,20 +32,6 @@ export const utilGetChatMessage = async ( feedbackTypes?: ChatMessageRatingType[] ): Promise => { const appServer = getRunningExpressApp() - const setDateToStartOrEndOfDay = (dateTimeStr: string, setHours: 'start' | 'end') => { - const date = new Date(dateTimeStr) - if (isNaN(date.getTime())) { - return undefined - } - setHours === 'start' ? date.setHours(0, 0, 0, 0) : date.setHours(23, 59, 59, 999) - return date - } - - const aMonthAgo = () => { - const date = new Date() - date.setMonth(new Date().getMonth() - 1) - return date - } let fromDate if (startDate) fromDate = setDateToStartOrEndOfDay(startDate, 'start') diff --git a/packages/server/src/utils/getUploadsConfig.ts b/packages/server/src/utils/getUploadsConfig.ts index 4688d680ed5..1bd65bb81ba 100644 --- a/packages/server/src/utils/getUploadsConfig.ts +++ b/packages/server/src/utils/getUploadsConfig.ts @@ -8,7 +8,7 @@ import { InternalFlowiseError } from '../errors/internalFlowiseError' type IUploadConfig = { isSpeechToTextEnabled: boolean isImageUploadAllowed: boolean - isFileUploadAllowed: boolean + isRAGFileUploadAllowed: boolean imgUploadSizeAndTypes: IUploadFileSizeAndTypes[] fileUploadSizeAndTypes: IUploadFileSizeAndTypes[] } @@ -32,7 +32,7 @@ export const utilGetUploadsConfig = async (chatflowid: string): Promise { const isObject = typeof paramValue === 'object' const initialValue = (isObject ? JSON.stringify(paramValue) : paramValue) ?? '' @@ -803,6 +809,10 @@ export const getVariableValue = async ( variableDict[`{{${variableFullPath}}}`] = handleEscapeCharacters(question, false) } + if (isAcceptVariable && variableFullPath === FILE_ATTACHMENT_PREFIX) { + variableDict[`{{${variableFullPath}}}`] = handleEscapeCharacters(uploadedFilesContent, false) + } + if (isAcceptVariable && variableFullPath === CHAT_HISTORY_VAR_PREFIX) { variableDict[`{{${variableFullPath}}}`] = handleEscapeCharacters(convertChatHistoryToText(chatHistory), false) } @@ -916,7 +926,8 @@ export const resolveVariables = async ( reactFlowNodes: IReactFlowNode[], question: string, chatHistory: IMessage[], - flowData?: ICommonObject + flowData?: ICommonObject, + uploadedFilesContent?: string ): Promise => { let flowNodeData = cloneDeep(reactFlowNodeData) const types = 'inputs' @@ -934,7 +945,8 @@ export const resolveVariables = async ( question, chatHistory, undefined, - flowData + flowData, + uploadedFilesContent ) resolvedInstances.push(resolvedInstance) } @@ -948,7 +960,8 @@ export const resolveVariables = async ( question, chatHistory, isAcceptVariable, - flowData + flowData, + uploadedFilesContent ) paramsObj[key] = resolvedInstance } @@ -1572,3 +1585,18 @@ export const convertToValidFilename = (word: string) => { .replace(' ', '') .toLowerCase() } + +export const setDateToStartOrEndOfDay = (dateTimeStr: string, setHours: 'start' | 'end') => { + const date = new Date(dateTimeStr) + if (isNaN(date.getTime())) { + return undefined + } + setHours === 'start' ? date.setHours(0, 0, 0, 0) : date.setHours(23, 59, 59, 999) + return date +} + +export const aMonthAgo = () => { + const date = new Date() + date.setMonth(new Date().getMonth() - 1) + return date +} diff --git a/packages/server/src/utils/upsertVector.ts b/packages/server/src/utils/upsertVector.ts index bfed225f17c..7d7fd70302a 100644 --- a/packages/server/src/utils/upsertVector.ts +++ b/packages/server/src/utils/upsertVector.ts @@ -16,7 +16,7 @@ import { getStartingNodes } from '../utils' import { validateChatflowAPIKey } from './validateKey' -import { IncomingInput, INodeDirectedGraph, IReactFlowObject, chatType } from '../Interface' +import { IncomingInput, INodeDirectedGraph, IReactFlowObject, ChatType } from '../Interface' import { ChatFlow } from '../database/entities/ChatFlow' import { getRunningExpressApp } from '../utils/getRunningExpressApp' import { UpsertHistory } from '../database/entities/UpsertHistory' @@ -195,7 +195,7 @@ export const upsertVector = async (req: Request, isInternal: boolean = false) => data: { version: await getAppVersion(), chatlowId: chatflowid, - type: isInternal ? chatType.INTERNAL : chatType.EXTERNAL, + type: isInternal ? ChatType.INTERNAL : ChatType.EXTERNAL, flowGraph: getTelemetryFlowObj(nodes, edges), stopNodeId } diff --git a/packages/ui/src/api/attachments.js b/packages/ui/src/api/attachments.js new file mode 100644 index 00000000000..7e9ef372f9d --- /dev/null +++ b/packages/ui/src/api/attachments.js @@ -0,0 +1,10 @@ +import client from './client' + +const createAttachment = (chatflowid, chatid, formData) => + client.post(`/attachments/${chatflowid}/${chatid}`, formData, { + headers: { 'Content-Type': 'multipart/form-data' } + }) + +export default { + createAttachment +} diff --git a/packages/ui/src/assets/images/fileAttachment.png b/packages/ui/src/assets/images/fileAttachment.png new file mode 100644 index 0000000000000000000000000000000000000000..8fb385c8c1b2940883a8da1b9bee1cf40b79a428 GIT binary patch literal 14884 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 literal 0 HcmV?d00001 diff --git a/packages/ui/src/ui-component/dialog/ChatflowConfigurationDialog.jsx b/packages/ui/src/ui-component/dialog/ChatflowConfigurationDialog.jsx index 1155eec56ef..48d6f3700a7 100644 --- a/packages/ui/src/ui-component/dialog/ChatflowConfigurationDialog.jsx +++ b/packages/ui/src/ui-component/dialog/ChatflowConfigurationDialog.jsx @@ -11,6 +11,7 @@ import AnalyseFlow from '@/ui-component/extended/AnalyseFlow' import StarterPrompts from '@/ui-component/extended/StarterPrompts' import Leads from '@/ui-component/extended/Leads' import FollowUpPrompts from '@/ui-component/extended/FollowUpPrompts' +import FileUpload from '@/ui-component/extended/FileUpload' const CHATFLOW_CONFIGURATION_TABS = [ { @@ -44,6 +45,10 @@ const CHATFLOW_CONFIGURATION_TABS = [ { label: 'Leads', id: 'leads' + }, + { + label: 'File Upload', + id: 'fileUpload' } ] @@ -85,7 +90,7 @@ const ChatflowConfigurationDialog = ({ show, dialogProps, onCancel }) => { onClose={onCancel} open={show} fullWidth - maxWidth={'md'} + maxWidth={'lg'} aria-labelledby='alert-dialog-title' aria-describedby='alert-dialog-description' > @@ -127,6 +132,7 @@ const ChatflowConfigurationDialog = ({ show, dialogProps, onCancel }) => { {item.id === 'allowedDomains' ? : null} {item.id === 'analyseChatflow' ? : null} {item.id === 'leads' ? : null} + {item.id === 'fileUpload' ? : null} ))} diff --git a/packages/ui/src/ui-component/dialog/ViewMessagesDialog.jsx b/packages/ui/src/ui-component/dialog/ViewMessagesDialog.jsx index 5b40d1e7c21..1b803826b46 100644 --- a/packages/ui/src/ui-component/dialog/ViewMessagesDialog.jsx +++ b/packages/ui/src/ui-component/dialog/ViewMessagesDialog.jsx @@ -25,7 +25,10 @@ import { Chip, Card, CardMedia, - CardContent + CardContent, + FormControlLabel, + Checkbox, + DialogActions } from '@mui/material' import { useTheme } from '@mui/material/styles' import DatePicker from 'react-datepicker' @@ -84,6 +87,52 @@ const messageImageStyle = { objectFit: 'cover' } +const ConfirmDeleteMessageDialog = ({ show, dialogProps, onCancel, onConfirm }) => { + const portalElement = document.getElementById('portal') + const [hardDelete, setHardDelete] = useState(false) + + const onSubmit = () => { + onConfirm(hardDelete) + } + + const component = show ? ( + + + {dialogProps.title} + + + {dialogProps.description} + setHardDelete(event.target.checked)} />} + label='Remove messages from 3rd party Memory Node' + /> + + + + + {dialogProps.confirmButtonName} + + + + ) : null + + return createPortal(component, portalElement) +} + +ConfirmDeleteMessageDialog.propTypes = { + show: PropTypes.bool, + dialogProps: PropTypes.object, + onCancel: PropTypes.func, + onConfirm: PropTypes.func +} + const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { const portalElement = document.getElementById('portal') const dispatch = useDispatch() @@ -103,6 +152,8 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { const [selectedChatId, setSelectedChatId] = useState('') const [sourceDialogOpen, setSourceDialogOpen] = useState(false) const [sourceDialogProps, setSourceDialogProps] = useState({}) + const [hardDeleteDialogOpen, setHardDeleteDialogOpen] = useState(false) + const [hardDeleteDialogProps, setHardDeleteDialogProps] = useState({}) const [chatTypeFilter, setChatTypeFilter] = useState([]) const [feedbackTypeFilter, setFeedbackTypeFilter] = useState([]) const [startDate, setStartDate] = useState(new Date().setMonth(new Date().getMonth() - 1)) @@ -175,6 +226,83 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { }) } + const onDeleteMessages = () => { + setHardDeleteDialogProps({ + title: 'Delete Messages', + description: 'Are you sure you want to delete messages? This action cannot be undone.', + confirmButtonName: 'Delete', + cancelButtonName: 'Cancel' + }) + setHardDeleteDialogOpen(true) + } + + const deleteMessages = async (hardDelete) => { + setHardDeleteDialogOpen(false) + const chatflowid = dialogProps.chatflow.id + try { + const obj = { chatflowid, isClearFromViewMessageDialog: true } + + let _chatTypeFilter = chatTypeFilter + if (typeof chatTypeFilter === 'string') { + _chatTypeFilter = JSON.parse(chatTypeFilter) + } + if (_chatTypeFilter.length === 1) { + obj.chatType = _chatTypeFilter[0] + } + + let _feedbackTypeFilter = feedbackTypeFilter + if (typeof feedbackTypeFilter === 'string') { + _feedbackTypeFilter = JSON.parse(feedbackTypeFilter) + } + if (_feedbackTypeFilter.length === 1) { + obj.feedbackType = _feedbackTypeFilter[0] + } + + if (startDate) obj.startDate = startDate + if (endDate) obj.endDate = endDate + if (hardDelete) obj.hardDelete = true + + await chatmessageApi.deleteChatmessage(chatflowid, obj) + enqueueSnackbar({ + message: 'Succesfully deleted messages', + options: { + key: new Date().getTime() + Math.random(), + variant: 'success', + action: (key) => ( + + ) + } + }) + getChatmessageApi.request(chatflowid, { + chatType: chatTypeFilter.length ? chatTypeFilter : undefined, + startDate: startDate, + endDate: endDate + }) + getStatsApi.request(chatflowid, { + chatType: chatTypeFilter.length ? chatTypeFilter : undefined, + startDate: startDate, + endDate: endDate + }) + } catch (error) { + console.error(error) + enqueueSnackbar({ + message: typeof error.response.data === 'object' ? error.response.data.message : error.response.data, + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + persist: true, + action: (key) => ( + + ) + } + }) + } + } + const exportMessages = async () => { if (!storagePath && getStoragePathFromServer.data) { storagePath = getStoragePathFromServer.data.storagePath @@ -675,7 +803,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { onClose={onCancel} open={show} fullWidth - maxWidth={chatlogs && chatlogs.length == 0 ? 'md' : 'lg'} + maxWidth={'lg'} aria-labelledby='alert-dialog-title' aria-describedby='alert-dialog-description' > @@ -781,6 +909,11 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { />
+ {stats.totalMessages > 0 && ( + + )}
{ )}
setSourceDialogOpen(false)} /> + setHardDeleteDialogOpen(false)} + onConfirm={(hardDelete) => deleteMessages(hardDelete)} + /> diff --git a/packages/ui/src/ui-component/extended/FileUpload.jsx b/packages/ui/src/ui-component/extended/FileUpload.jsx new file mode 100644 index 00000000000..b1ccb08ca4b --- /dev/null +++ b/packages/ui/src/ui-component/extended/FileUpload.jsx @@ -0,0 +1,122 @@ +import { useDispatch } from 'react-redux' +import { useState, useEffect } from 'react' +import PropTypes from 'prop-types' +import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction, SET_CHATFLOW } from '@/store/actions' + +// material-ui +import { Button, Box, Typography } from '@mui/material' +import { IconX } from '@tabler/icons-react' + +// Project import +import { StyledButton } from '@/ui-component/button/StyledButton' +import { TooltipWithParser } from '@/ui-component/tooltip/TooltipWithParser' +import { SwitchInput } from '@/ui-component/switch/Switch' + +// store +import useNotifier from '@/utils/useNotifier' + +// API +import chatflowsApi from '@/api/chatflows' + +const message = `Allow files to be uploaded from the chat. Uploaded files will be parsed as string and sent to LLM. If File Upload is enabled on Vector Store as well, this will override and takes precedence.` + +const FileUpload = ({ dialogProps }) => { + const dispatch = useDispatch() + + useNotifier() + + const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)) + const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) + + const [fullFileUpload, setFullFileUpload] = useState(false) + const [chatbotConfig, setChatbotConfig] = useState({}) + + const handleChange = (value) => { + setFullFileUpload(value) + } + + const onSave = async () => { + try { + const value = { + status: fullFileUpload + } + chatbotConfig.fullFileUpload = value + + const saveResp = await chatflowsApi.updateChatflow(dialogProps.chatflow.id, { + chatbotConfig: JSON.stringify(chatbotConfig) + }) + if (saveResp.data) { + enqueueSnackbar({ + message: 'File Upload Configuration Saved', + options: { + key: new Date().getTime() + Math.random(), + variant: 'success', + action: (key) => ( + + ) + } + }) + dispatch({ type: SET_CHATFLOW, chatflow: saveResp.data }) + } + } catch (error) { + enqueueSnackbar({ + message: `Failed to save File Upload Configuration: ${ + typeof error.response.data === 'object' ? error.response.data.message : error.response.data + }`, + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + persist: true, + action: (key) => ( + + ) + } + }) + } + } + + useEffect(() => { + if (dialogProps.chatflow) { + if (dialogProps.chatflow.chatbotConfig) { + try { + let chatbotConfig = JSON.parse(dialogProps.chatflow.chatbotConfig) + setChatbotConfig(chatbotConfig || {}) + if (chatbotConfig.fullFileUpload) { + setFullFileUpload(chatbotConfig.fullFileUpload.status) + } + } catch (e) { + setChatbotConfig({}) + } + } + } + + return () => {} + }, [dialogProps]) + + return ( + <> + +
+ + Enable Full File Upload + + +
+ +
+ + Save + + + ) +} + +FileUpload.propTypes = { + dialogProps: PropTypes.object +} + +export default FileUpload diff --git a/packages/ui/src/ui-component/json/SelectVariable.jsx b/packages/ui/src/ui-component/json/SelectVariable.jsx index 9f4e2f737ad..55fdce061d6 100644 --- a/packages/ui/src/ui-component/json/SelectVariable.jsx +++ b/packages/ui/src/ui-component/json/SelectVariable.jsx @@ -5,6 +5,7 @@ import PerfectScrollbar from 'react-perfect-scrollbar' import robotPNG from '@/assets/images/robot.png' import chatPNG from '@/assets/images/chathistory.png' import diskPNG from '@/assets/images/floppy-disc.png' +import fileAttachmentPNG from '@/assets/images/fileAttachment.png' import { baseURL } from '@/store/constant' const sequentialStateMessagesSelection = [ @@ -119,6 +120,45 @@ const SelectVariable = ({ availableNodesForVariable, disabled = false, onSelectA /> + onSelectOutputResponseClick(null, 'file_attachment')} + > + + +
+ fileAttachment +
+
+ +
+
{availableNodesForVariable && availableNodesForVariable.length > 0 && availableNodesForVariable.map((node, index) => { diff --git a/packages/ui/src/views/chatmessage/ChatMessage.jsx b/packages/ui/src/views/chatmessage/ChatMessage.jsx index c2faa51a2f9..96d722b3b1f 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.jsx +++ b/packages/ui/src/views/chatmessage/ChatMessage.jsx @@ -67,6 +67,7 @@ import chatmessageApi from '@/api/chatmessage' import chatflowsApi from '@/api/chatflows' import predictionApi from '@/api/prediction' import vectorstoreApi from '@/api/vectorstore' +import attachmentsApi from '@/api/attachments' import chatmessagefeedbackApi from '@/api/chatmessagefeedback' import leadsApi from '@/api/lead' @@ -88,7 +89,7 @@ const messageImageStyle = { objectFit: 'cover' } -const CardWithDeleteOverlay = ({ item, customization, onDelete }) => { +const CardWithDeleteOverlay = ({ item, disabled, customization, onDelete }) => { const [isHovered, setIsHovered] = useState(false) const defaultBackgroundColor = customization.isDarkMode ? 'rgba(0, 0, 0, 0.3)' : 'transparent' @@ -125,8 +126,9 @@ const CardWithDeleteOverlay = ({ item, customization, onDelete }) => { {item.name} - {isHovered && ( + {isHovered && !disabled && (