From 1f0cc60fccb993e2d632e17faa42d579eaba6881 Mon Sep 17 00:00:00 2001 From: automaton82 Date: Fri, 10 Nov 2023 15:31:13 -0500 Subject: [PATCH 1/7] Adding ability to hide node param from UI --- packages/components/src/Interface.ts | 1 + packages/ui/src/views/canvas/CanvasNode.js | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/components/src/Interface.ts b/packages/components/src/Interface.ts index 15b987701..c56d8a5fb 100644 --- a/packages/components/src/Interface.ts +++ b/packages/components/src/Interface.ts @@ -72,6 +72,7 @@ export interface INodeParams { fileType?: string additionalParams?: boolean loadMethod?: string + hidden?: boolean } export interface INodeExecutionData { diff --git a/packages/ui/src/views/canvas/CanvasNode.js b/packages/ui/src/views/canvas/CanvasNode.js index cabe23291..cfc51fe47 100644 --- a/packages/ui/src/views/canvas/CanvasNode.js +++ b/packages/ui/src/views/canvas/CanvasNode.js @@ -207,9 +207,11 @@ const CanvasNode = ({ data }) => { {data.inputAnchors.map((inputAnchor, index) => ( ))} - {data.inputParams.map((inputParam, index) => ( - - ))} + {data.inputParams + .filter((inputParam) => !inputParam.hidden) + .map((inputParam, index) => ( + + ))} {data.inputParams.find((param) => param.additionalParams) && (
Date: Mon, 13 Nov 2023 21:46:36 +0530 Subject: [PATCH 2/7] MongoDB Atlas Integration: Adding MongoDB Memory --- .../credentials/MongoDBUrlApi.credential.ts | 25 ++++ .../memory/MongoDBMemory/MongoDBMemory.ts | 115 ++++++++++++++++++ .../nodes/memory/MongoDBMemory/mongodb.png | Bin 0 -> 3741 bytes packages/components/package.json | 1 + 4 files changed, 141 insertions(+) create mode 100644 packages/components/credentials/MongoDBUrlApi.credential.ts create mode 100644 packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts create mode 100644 packages/components/nodes/memory/MongoDBMemory/mongodb.png diff --git a/packages/components/credentials/MongoDBUrlApi.credential.ts b/packages/components/credentials/MongoDBUrlApi.credential.ts new file mode 100644 index 000000000..2f2cba383 --- /dev/null +++ b/packages/components/credentials/MongoDBUrlApi.credential.ts @@ -0,0 +1,25 @@ +import { INodeParams, INodeCredential } from '../src/Interface' + +class MongoDBUrlApi implements INodeCredential { + label: string + name: string + version: number + description: string + inputs: INodeParams[] + + constructor() { + this.label = 'MongoDB ATLAS' + this.name = 'mongoDBUrlApi' + this.version = 1.0 + this.inputs = [ + { + label: 'ATLAS Connection URL', + name: 'mongoDBConnectUrl', + type: 'string', + placeholder: 'mongodb+srv://myDatabaseUser:D1fficultP%40ssw0rd@cluster0.example.mongodb.net/?retryWrites=true&w=majority' + } + ] + } +} + +module.exports = { credClass: MongoDBUrlApi } diff --git a/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts b/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts new file mode 100644 index 000000000..4c9e8581e --- /dev/null +++ b/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts @@ -0,0 +1,115 @@ +import { getBaseClasses, getCredentialData, getCredentialParam, ICommonObject, INode, INodeData, INodeParams } from '../../../src' +import { MongoDBChatMessageHistory } from 'langchain/stores/message/mongodb' +import { BufferMemory, BufferMemoryInput } from 'langchain/memory' +import { MongoClient } from 'mongodb' + +class MongoDB_Memory implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + baseClasses: string[] + credential: INodeParams + inputs: INodeParams[] + + constructor() { + this.label = 'MongoDB Atlas Chat Memory' + this.name = 'MongoDBAtlasChatMemory' + this.version = 1.0 + this.type = 'MongoDBAtlasChatMemory' + this.icon = 'mongodb.png' + this.category = 'Memory' + this.description = 'Stores the conversation in MongoDB Atlas' + this.baseClasses = [this.type, ...getBaseClasses(BufferMemory)] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['mongoDBUrlApi'] + } + this.inputs = [ + { + label: 'Database', + name: 'databaseName', + placeholder: '', + type: 'string' + }, + { + label: 'Collection Name', + name: 'collectionName', + placeholder: '', + type: 'string' + }, + { + label: 'Session ID', + name: 'sessionId', + type: 'string', + default: '5f9cf7c08d5b1a06b80fae61', + description: 'Must be an Hex String of 24 chars. This will be the objectId of the document in MongoDB Atlas' + }, + { + label: 'Memory Key', + name: 'memoryKey', + type: 'string', + default: 'chat_history', + additionalParams: true + } + ] + } + + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + return initializeMongoDB(nodeData, options) + } + + async clearSessionMemory(nodeData: INodeData, options: ICommonObject): Promise { + const mongodbMemory = await initializeMongoDB(nodeData, options) + const sessionId = nodeData.inputs?.sessionId as string + options.logger.info(`Clearing MongoDB memory session ${sessionId}`) + await mongodbMemory.clear() + options.logger.info(`Successfully cleared MongoDB memory session ${sessionId}`) + } +} + +const initializeMongoDB = async (nodeData: INodeData, options: ICommonObject): Promise => { + const databaseName = nodeData.inputs?.databaseName as string + const collectionName = nodeData.inputs?.collectionName as string + const sessionId = nodeData.inputs?.sessionId as string + const memoryKey = nodeData.inputs?.memoryKey as string + + const credentialData = await getCredentialData(nodeData.credential ?? '', options) + let mongoDBConnectUrl = getCredentialParam('mongoDBConnectUrl', credentialData, nodeData) + + const client = new MongoClient(mongoDBConnectUrl) + await client.connect() + const collection = client.db(databaseName).collection(collectionName) + + const mongoDBChatMessageHistory = new MongoDBChatMessageHistory({ + collection, + sessionId: sessionId + }) + + return new BufferMemoryExtended({ + memoryKey, + chatHistory: mongoDBChatMessageHistory, + returnMessages: true, + isSessionIdUsingChatMessageId: false + }) +} + +interface BufferMemoryExtendedInput { + isSessionIdUsingChatMessageId: boolean +} + +class BufferMemoryExtended extends BufferMemory { + isSessionIdUsingChatMessageId? = false + + constructor(fields: BufferMemoryInput & Partial) { + super(fields) + this.isSessionIdUsingChatMessageId = fields.isSessionIdUsingChatMessageId + } +} + +module.exports = { nodeClass: MongoDB_Memory } diff --git a/packages/components/nodes/memory/MongoDBMemory/mongodb.png b/packages/components/nodes/memory/MongoDBMemory/mongodb.png new file mode 100644 index 0000000000000000000000000000000000000000..5586fe0ac672f7997014d814389c1d6c436d9d0c GIT binary patch literal 3741 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 literal 0 HcmV?d00001 diff --git a/packages/components/package.json b/packages/components/package.json index 996419ca0..ee2adbf9a 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -55,6 +55,7 @@ "llmonitor": "^0.5.5", "mammoth": "^1.5.1", "moment": "^2.29.3", + "mongodb": "^6.2.0", "mysql2": "^3.5.1", "node-fetch": "^2.6.11", "node-html-markdown": "^1.3.0", From 261e45d74a2f912dd0ab6e356fef90c97b420f3f Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Mon, 13 Nov 2023 21:56:18 +0530 Subject: [PATCH 3/7] MongoDB Atlas Integration: Adding MongoDB as a Vector Store --- .../vectorstores/MongoDB/MongoDBSearchBase.ts | 145 ++++++++++++++++++ .../vectorstores/MongoDB/MongoDB_Existing.ts | 41 +++++ .../vectorstores/MongoDB/MongoDB_Upsert.ts | 58 +++++++ .../nodes/vectorstores/MongoDB/mongodb.png | Bin 0 -> 3741 bytes 4 files changed, 244 insertions(+) create mode 100644 packages/components/nodes/vectorstores/MongoDB/MongoDBSearchBase.ts create mode 100644 packages/components/nodes/vectorstores/MongoDB/MongoDB_Existing.ts create mode 100644 packages/components/nodes/vectorstores/MongoDB/MongoDB_Upsert.ts create mode 100644 packages/components/nodes/vectorstores/MongoDB/mongodb.png diff --git a/packages/components/nodes/vectorstores/MongoDB/MongoDBSearchBase.ts b/packages/components/nodes/vectorstores/MongoDB/MongoDBSearchBase.ts new file mode 100644 index 000000000..e9ef8e9a1 --- /dev/null +++ b/packages/components/nodes/vectorstores/MongoDB/MongoDBSearchBase.ts @@ -0,0 +1,145 @@ +import { + getBaseClasses, + getCredentialData, + getCredentialParam, + ICommonObject, + INodeData, + INodeOutputsValue, + INodeParams +} from '../../../src' + +import { Embeddings } from 'langchain/embeddings/base' +import { VectorStore } from 'langchain/vectorstores/base' +import { Document } from 'langchain/document' +import { MongoDBAtlasVectorSearch } from 'langchain/vectorstores/mongodb_atlas' +import { Collection, MongoClient } from 'mongodb' + +export abstract class MongoDBSearchBase { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + baseClasses: string[] + inputs: INodeParams[] + credential: INodeParams + outputs: INodeOutputsValue[] + mongoClient: MongoClient + + protected constructor() { + this.type = 'MongoDB Atlas' + this.icon = 'mongodb.png' + this.category = 'Vector Stores' + this.baseClasses = [this.type, 'VectorStoreRetriever', 'BaseRetriever'] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['mongoDBUrlApi'] + } + this.inputs = [ + { + label: 'Embeddings', + name: 'embeddings', + type: 'Embeddings' + }, + { + label: 'Database', + name: 'databaseName', + placeholder: '', + type: 'string' + }, + { + label: 'Collection Name', + name: 'collectionName', + placeholder: '', + type: 'string' + }, + { + label: 'Index Name', + name: 'indexName', + placeholder: '', + type: 'string' + }, + { + label: 'Content Field', + name: 'textKey', + description: 'Name of the field (column) that contains the actual content', + type: 'string', + default: 'text', + additionalParams: true, + optional: true + }, + { + label: 'Embedded Field', + name: 'embeddingKey', + description: 'Name of the field (column) that contains the Embedding', + type: 'string', + default: 'embedding', + additionalParams: true, + optional: true + }, + { + label: 'Top K', + name: 'topK', + description: 'Number of top results to fetch. Default to 4', + placeholder: '4', + type: 'number', + additionalParams: true, + optional: true + } + ] + this.outputs = [ + { + label: 'MongoDB Retriever', + name: 'retriever', + baseClasses: this.baseClasses + }, + { + label: 'MongoDB Vector Store', + name: 'vectorStore', + baseClasses: [this.type, ...getBaseClasses(MongoDBAtlasVectorSearch)] + } + ] + } + + abstract constructVectorStore( + embeddings: Embeddings, + collection: Collection, + indexName: string, + textKey: string, + embeddingKey: string, + docs: Document>[] | undefined + ): Promise + + async init(nodeData: INodeData, _: string, options: ICommonObject, docs: Document>[] | undefined): Promise { + const credentialData = await getCredentialData(nodeData.credential ?? '', options) + const databaseName = nodeData.inputs?.databaseName as string + const collectionName = nodeData.inputs?.collectionName as string + const indexName = nodeData.inputs?.indexName as string + let textKey = nodeData.inputs?.textKey as string + let embeddingKey = nodeData.inputs?.embeddingKey as string + const embeddings = nodeData.inputs?.embeddings as Embeddings + const topK = nodeData.inputs?.topK as string + const k = topK ? parseFloat(topK) : 4 + const output = nodeData.outputs?.output as string + + let mongoDBConnectUrl = getCredentialParam('mongoDBConnectUrl', credentialData, nodeData) + + this.mongoClient = new MongoClient(mongoDBConnectUrl) + const collection = this.mongoClient.db(databaseName).collection(collectionName) + if (!textKey || textKey === '') textKey = 'text' + if (!embeddingKey || embeddingKey === '') embeddingKey = 'embedding' + const vectorStore = await this.constructVectorStore(embeddings, collection, indexName, textKey, embeddingKey, docs) + + if (output === 'retriever') { + return vectorStore.asRetriever(k) + } else if (output === 'vectorStore') { + ;(vectorStore as any).k = k + return vectorStore + } + return vectorStore + } +} diff --git a/packages/components/nodes/vectorstores/MongoDB/MongoDB_Existing.ts b/packages/components/nodes/vectorstores/MongoDB/MongoDB_Existing.ts new file mode 100644 index 000000000..3cbb36b87 --- /dev/null +++ b/packages/components/nodes/vectorstores/MongoDB/MongoDB_Existing.ts @@ -0,0 +1,41 @@ +import { ICommonObject, INode, INodeData } from '../../../src/Interface' +import { Embeddings } from 'langchain/embeddings/base' +import { VectorStore } from 'langchain/vectorstores/base' +import { Document } from 'langchain/document' + +import { MongoDBSearchBase } from './MongoDBSearchBase' +import { Collection } from 'mongodb' +import { MongoDBAtlasVectorSearch } from 'langchain/vectorstores/mongodb_atlas' + +class MongoDBExisting_VectorStores extends MongoDBSearchBase implements INode { + constructor() { + super() + this.label = 'MongoDB Atlas Load Existing Index' + this.name = 'MongoDBIndex' + this.version = 1.0 + this.description = 'Load existing data from MongoDB Atlas (i.e: Document has been upserted)' + } + + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + return super.init(nodeData, _, options, undefined) + } + + constructVectorStore( + embeddings: Embeddings, + collection: Collection, + indexName: string, + textKey: string, + embeddingKey: string, + _: Document>[] | undefined + ): Promise { + const mongoDBAtlasVectorSearch = new MongoDBAtlasVectorSearch(embeddings, { + collection: collection, + indexName: indexName, + textKey: textKey, + embeddingKey: embeddingKey + }) + return Promise.resolve(mongoDBAtlasVectorSearch) + } +} + +module.exports = { nodeClass: MongoDBExisting_VectorStores } diff --git a/packages/components/nodes/vectorstores/MongoDB/MongoDB_Upsert.ts b/packages/components/nodes/vectorstores/MongoDB/MongoDB_Upsert.ts new file mode 100644 index 000000000..80dfbf195 --- /dev/null +++ b/packages/components/nodes/vectorstores/MongoDB/MongoDB_Upsert.ts @@ -0,0 +1,58 @@ +import { ICommonObject, INode, INodeData } from '../../../src/Interface' +import { Embeddings } from 'langchain/embeddings/base' +import { Document } from 'langchain/document' + +import { flatten } from 'lodash' +import { VectorStore } from 'langchain/vectorstores/base' +import { MongoDBSearchBase } from './MongoDBSearchBase' +import { Collection } from 'mongodb' +import { MongoDBAtlasVectorSearch } from 'langchain/vectorstores/mongodb_atlas' + +class MongoDBUpsert_VectorStores extends MongoDBSearchBase implements INode { + constructor() { + super() + this.label = 'MongoDB Upsert Document' + this.name = 'MongoDBUpsert' + this.version = 1.0 + this.description = 'Upsert documents to MongoDB Atlas' + this.inputs.unshift({ + label: 'Document', + name: 'document', + type: 'Document', + list: true + }) + } + + constructVectorStore( + embeddings: Embeddings, + collection: Collection, + indexName: string, + textKey: string, + embeddingKey: string, + docs: Document>[] + ): Promise { + return MongoDBAtlasVectorSearch.fromDocuments(docs, embeddings, { + collection: collection, + indexName: indexName, + textKey: textKey, + embeddingKey: embeddingKey + }) + } + + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + const docs = nodeData.inputs?.document as Document[] + + const flattenDocs = docs && docs.length ? flatten(docs) : [] + const finalDocs = [] + for (let i = 0; i < flattenDocs.length; i += 1) { + if (flattenDocs[i] && flattenDocs[i].pageContent) { + const document = new Document(flattenDocs[i]) + finalDocs.push(document) + } + } + + return super.init(nodeData, _, options, flattenDocs) + } +} + +module.exports = { nodeClass: MongoDBUpsert_VectorStores } diff --git a/packages/components/nodes/vectorstores/MongoDB/mongodb.png b/packages/components/nodes/vectorstores/MongoDB/mongodb.png new file mode 100644 index 0000000000000000000000000000000000000000..5586fe0ac672f7997014d814389c1d6c436d9d0c GIT binary patch literal 3741 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 literal 0 HcmV?d00001 From f8c4ec28b4aa2284e8878eb324465aa8574c5b45 Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 17 Nov 2023 13:52:41 +0000 Subject: [PATCH 4/7] fix mongodb vector database where no documents are returned --- .../Elasticsearch/Elasticsearch_Upsert.ts | 2 +- .../vectorstores/MongoDB/MongoDB_Existing.ts | 12 +++++------- .../vectorstores/MongoDB/MongoDB_Upsert.ts | 17 +++++++++-------- .../nodes/vectorstores/Redis/Redis_Upsert.ts | 2 +- 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/packages/components/nodes/vectorstores/Elasticsearch/Elasticsearch_Upsert.ts b/packages/components/nodes/vectorstores/Elasticsearch/Elasticsearch_Upsert.ts index d39657865..a8ccd49ac 100644 --- a/packages/components/nodes/vectorstores/Elasticsearch/Elasticsearch_Upsert.ts +++ b/packages/components/nodes/vectorstores/Elasticsearch/Elasticsearch_Upsert.ts @@ -50,7 +50,7 @@ class ElasicsearchUpsert_VectorStores extends ElasticSearchBase implements INode delete d.metadata.loc }) // end of workaround - return super.init(nodeData, _, options, flattenDocs) + return super.init(nodeData, _, options, finalDocs) } } diff --git a/packages/components/nodes/vectorstores/MongoDB/MongoDB_Existing.ts b/packages/components/nodes/vectorstores/MongoDB/MongoDB_Existing.ts index 3cbb36b87..7b06814af 100644 --- a/packages/components/nodes/vectorstores/MongoDB/MongoDB_Existing.ts +++ b/packages/components/nodes/vectorstores/MongoDB/MongoDB_Existing.ts @@ -1,11 +1,10 @@ -import { ICommonObject, INode, INodeData } from '../../../src/Interface' +import { Collection } from 'mongodb' +import { MongoDBAtlasVectorSearch } from 'langchain/vectorstores/mongodb_atlas' import { Embeddings } from 'langchain/embeddings/base' import { VectorStore } from 'langchain/vectorstores/base' import { Document } from 'langchain/document' - import { MongoDBSearchBase } from './MongoDBSearchBase' -import { Collection } from 'mongodb' -import { MongoDBAtlasVectorSearch } from 'langchain/vectorstores/mongodb_atlas' +import { ICommonObject, INode, INodeData } from '../../../src/Interface' class MongoDBExisting_VectorStores extends MongoDBSearchBase implements INode { constructor() { @@ -20,7 +19,7 @@ class MongoDBExisting_VectorStores extends MongoDBSearchBase implements INode { return super.init(nodeData, _, options, undefined) } - constructVectorStore( + async constructVectorStore( embeddings: Embeddings, collection: Collection, indexName: string, @@ -28,13 +27,12 @@ class MongoDBExisting_VectorStores extends MongoDBSearchBase implements INode { embeddingKey: string, _: Document>[] | undefined ): Promise { - const mongoDBAtlasVectorSearch = new MongoDBAtlasVectorSearch(embeddings, { + return new MongoDBAtlasVectorSearch(embeddings, { collection: collection, indexName: indexName, textKey: textKey, embeddingKey: embeddingKey }) - return Promise.resolve(mongoDBAtlasVectorSearch) } } diff --git a/packages/components/nodes/vectorstores/MongoDB/MongoDB_Upsert.ts b/packages/components/nodes/vectorstores/MongoDB/MongoDB_Upsert.ts index 80dfbf195..7d22f0352 100644 --- a/packages/components/nodes/vectorstores/MongoDB/MongoDB_Upsert.ts +++ b/packages/components/nodes/vectorstores/MongoDB/MongoDB_Upsert.ts @@ -1,12 +1,11 @@ -import { ICommonObject, INode, INodeData } from '../../../src/Interface' +import { flatten } from 'lodash' +import { Collection } from 'mongodb' import { Embeddings } from 'langchain/embeddings/base' import { Document } from 'langchain/document' - -import { flatten } from 'lodash' import { VectorStore } from 'langchain/vectorstores/base' -import { MongoDBSearchBase } from './MongoDBSearchBase' -import { Collection } from 'mongodb' import { MongoDBAtlasVectorSearch } from 'langchain/vectorstores/mongodb_atlas' +import { ICommonObject, INode, INodeData } from '../../../src/Interface' +import { MongoDBSearchBase } from './MongoDBSearchBase' class MongoDBUpsert_VectorStores extends MongoDBSearchBase implements INode { constructor() { @@ -23,7 +22,7 @@ class MongoDBUpsert_VectorStores extends MongoDBSearchBase implements INode { }) } - constructVectorStore( + async constructVectorStore( embeddings: Embeddings, collection: Collection, indexName: string, @@ -31,12 +30,14 @@ class MongoDBUpsert_VectorStores extends MongoDBSearchBase implements INode { embeddingKey: string, docs: Document>[] ): Promise { - return MongoDBAtlasVectorSearch.fromDocuments(docs, embeddings, { + const mongoDBAtlasVectorSearch = new MongoDBAtlasVectorSearch(embeddings, { collection: collection, indexName: indexName, textKey: textKey, embeddingKey: embeddingKey }) + await mongoDBAtlasVectorSearch.addDocuments(docs) + return mongoDBAtlasVectorSearch } async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { @@ -51,7 +52,7 @@ class MongoDBUpsert_VectorStores extends MongoDBSearchBase implements INode { } } - return super.init(nodeData, _, options, flattenDocs) + return super.init(nodeData, _, options, finalDocs) } } diff --git a/packages/components/nodes/vectorstores/Redis/Redis_Upsert.ts b/packages/components/nodes/vectorstores/Redis/Redis_Upsert.ts index 9d1a4f453..4da58eaff 100644 --- a/packages/components/nodes/vectorstores/Redis/Redis_Upsert.ts +++ b/packages/components/nodes/vectorstores/Redis/Redis_Upsert.ts @@ -56,7 +56,7 @@ class RedisUpsert_VectorStores extends RedisSearchBase implements INode { } } - return super.init(nodeData, _, options, flattenDocs) + return super.init(nodeData, _, options, finalDocs) } } From e251bd573d50327d1a26dcee9f51f1367fc58f32 Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 17 Nov 2023 14:38:42 +0000 Subject: [PATCH 5/7] changed to enable uuid to be used as sessionId --- .../memory/MongoDBMemory/MongoDBMemory.ts | 45 ++++++++++++++++--- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts b/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts index 4c9e8581e..7de2ec347 100644 --- a/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts +++ b/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts @@ -1,6 +1,7 @@ import { getBaseClasses, getCredentialData, getCredentialParam, ICommonObject, INode, INodeData, INodeParams } from '../../../src' import { MongoDBChatMessageHistory } from 'langchain/stores/message/mongodb' import { BufferMemory, BufferMemoryInput } from 'langchain/memory' +import { BaseMessage, mapStoredMessageToChatMessage } from 'langchain/schema' import { MongoClient } from 'mongodb' class MongoDB_Memory implements INode { @@ -44,11 +45,13 @@ class MongoDB_Memory implements INode { type: 'string' }, { - label: 'Session ID', + label: 'Session Id', name: 'sessionId', type: 'string', - default: '5f9cf7c08d5b1a06b80fae61', - description: 'Must be an Hex String of 24 chars. This will be the objectId of the document in MongoDB Atlas' + description: 'If not specified, the first CHAT_MESSAGE_ID will be used as sessionId', + default: '', + additionalParams: true, + optional: true }, { label: 'Memory Key', @@ -67,9 +70,10 @@ class MongoDB_Memory implements INode { async clearSessionMemory(nodeData: INodeData, options: ICommonObject): Promise { const mongodbMemory = await initializeMongoDB(nodeData, options) const sessionId = nodeData.inputs?.sessionId as string - options.logger.info(`Clearing MongoDB memory session ${sessionId}`) + const chatId = options?.chatId as string + options.logger.info(`Clearing MongoDB memory session ${sessionId ? sessionId : chatId}`) await mongodbMemory.clear() - options.logger.info(`Successfully cleared MongoDB memory session ${sessionId}`) + options.logger.info(`Successfully cleared MongoDB memory session ${sessionId ? sessionId : chatId}`) } } @@ -78,6 +82,10 @@ const initializeMongoDB = async (nodeData: INodeData, options: ICommonObject): P const collectionName = nodeData.inputs?.collectionName as string const sessionId = nodeData.inputs?.sessionId as string const memoryKey = nodeData.inputs?.memoryKey as string + const chatId = options?.chatId as string + + let isSessionIdUsingChatMessageId = false + if (!sessionId && chatId) isSessionIdUsingChatMessageId = true const credentialData = await getCredentialData(nodeData.credential ?? '', options) let mongoDBConnectUrl = getCredentialParam('mongoDBConnectUrl', credentialData, nodeData) @@ -88,14 +96,37 @@ const initializeMongoDB = async (nodeData: INodeData, options: ICommonObject): P const mongoDBChatMessageHistory = new MongoDBChatMessageHistory({ collection, - sessionId: sessionId + sessionId: sessionId ? sessionId : chatId }) + mongoDBChatMessageHistory.getMessages = async (): Promise => { + const document = await collection.findOne({ + sessionId: (mongoDBChatMessageHistory as any).sessionId + }) + const messages = document?.messages || [] + return messages.map(mapStoredMessageToChatMessage) + } + + mongoDBChatMessageHistory.addMessage = async (message: BaseMessage): Promise => { + const messages = [message].map((msg) => msg.toDict()) + await collection.updateOne( + { sessionId: (mongoDBChatMessageHistory as any).sessionId }, + { + $push: { messages: { $each: messages } } + }, + { upsert: true } + ) + } + + mongoDBChatMessageHistory.clear = async (): Promise => { + await collection.deleteOne({ sessionId: (mongoDBChatMessageHistory as any).sessionId }) + } + return new BufferMemoryExtended({ memoryKey, chatHistory: mongoDBChatMessageHistory, returnMessages: true, - isSessionIdUsingChatMessageId: false + isSessionIdUsingChatMessageId }) } From 3bddc95087a81ee2faeea835463841239c8e9b1b Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 17 Nov 2023 16:41:49 +0000 Subject: [PATCH 6/7] add user id --- packages/components/package.json | 2 +- packages/components/src/handler.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/components/package.json b/packages/components/package.json index 996419ca0..0df99a11d 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -49,7 +49,7 @@ "html-to-text": "^9.0.5", "ioredis": "^5.3.2", "langchain": "^0.0.165", - "langfuse-langchain": "^1.0.14-alpha.0", + "langfuse-langchain": "^1.0.31", "langsmith": "^0.0.32", "linkifyjs": "^4.1.1", "llmonitor": "^0.5.5", diff --git a/packages/components/src/handler.ts b/packages/components/src/handler.ts index 37075342f..456cf39c3 100644 --- a/packages/components/src/handler.ts +++ b/packages/components/src/handler.ts @@ -250,6 +250,7 @@ export const additionalCallbacks = async (nodeData: INodeData, options: ICommonO baseUrl: langFuseEndpoint ?? 'https://cloud.langfuse.com' } if (release) langFuseOptions.release = release + if (options.chatId) langFuseOptions.userId = options.chatId const handler = new CallbackHandler(langFuseOptions) callbacks.push(handler) From c7add456479fe0e21d80121de6be33f68b55a960 Mon Sep 17 00:00:00 2001 From: Henry Date: Mon, 20 Nov 2023 00:55:58 +0000 Subject: [PATCH 7/7] add file annotations, sync and delete assistant --- .../agents/OpenAIAssistant/OpenAIAssistant.ts | 125 +++++++++++-- packages/server/src/Interface.ts | 1 + .../src/database/entities/ChatMessage.ts | 3 + ...1021237-AddFileAnnotationsToChatMessage.ts | 12 ++ .../src/database/migrations/mysql/index.ts | 4 +- ...1699481607341-AddUsedToolsToChatMessage.ts | 2 +- ...1021237-AddFileAnnotationsToChatMessage.ts | 11 ++ .../src/database/migrations/postgres/index.ts | 4 +- ...1021237-AddFileAnnotationsToChatMessage.ts | 20 +++ .../src/database/migrations/sqlite/index.ts | 4 +- packages/server/src/index.ts | 16 +- packages/ui/src/api/assistants.js | 3 +- .../ui-component/dialog/ViewMessagesDialog.js | 50 +++++- .../src/views/assistants/AssistantDialog.js | 165 ++++++++++++------ .../views/assistants/DeleteConfirmDialog.js | 47 +++++ .../ui/src/views/chatmessage/ChatMessage.js | 51 +++++- 16 files changed, 436 insertions(+), 82 deletions(-) create mode 100644 packages/server/src/database/migrations/mysql/1700271021237-AddFileAnnotationsToChatMessage.ts create mode 100644 packages/server/src/database/migrations/postgres/1700271021237-AddFileAnnotationsToChatMessage.ts create mode 100644 packages/server/src/database/migrations/sqlite/1700271021237-AddFileAnnotationsToChatMessage.ts create mode 100644 packages/ui/src/views/assistants/DeleteConfirmDialog.js diff --git a/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts b/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts index 56e1b290e..ed7baf7de 100644 --- a/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts +++ b/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts @@ -111,7 +111,7 @@ class OpenAIAssistant_Agents implements INode { const openai = new OpenAI({ apiKey: openAIApiKey }) options.logger.info(`Clearing OpenAI Thread ${sessionId}`) - await openai.beta.threads.del(sessionId) + if (sessionId) await openai.beta.threads.del(sessionId) options.logger.info(`Successfully cleared OpenAI Thread ${sessionId}`) } @@ -135,16 +135,25 @@ class OpenAIAssistant_Agents implements INode { const openai = new OpenAI({ apiKey: openAIApiKey }) - // Retrieve assistant try { const assistantDetails = JSON.parse(assistant.details) const openAIAssistantId = assistantDetails.id + + // Retrieve assistant const retrievedAssistant = await openai.beta.assistants.retrieve(openAIAssistantId) if (formattedTools.length) { - let filteredTools = uniqWith([...retrievedAssistant.tools, ...formattedTools], isEqual) + let filteredTools = [] + for (const tool of retrievedAssistant.tools) { + if (tool.type === 'code_interpreter' || tool.type === 'retrieval') filteredTools.push(tool) + } + filteredTools = uniqWith([...filteredTools, ...formattedTools], isEqual) + // filter out tool with empty function filteredTools = filteredTools.filter((tool) => !(tool.type === 'function' && !(tool as any).function)) await openai.beta.assistants.update(openAIAssistantId, { tools: filteredTools }) + } else { + let filteredTools = retrievedAssistant.tools.filter((tool) => tool.type !== 'function') + await openai.beta.assistants.update(openAIAssistantId, { tools: filteredTools }) } const chatmessage = await appDataSource.getRepository(databaseEntities['ChatMessage']).findOneBy({ @@ -152,14 +161,45 @@ class OpenAIAssistant_Agents implements INode { }) let threadId = '' + let isNewThread = false if (!chatmessage) { const thread = await openai.beta.threads.create({}) threadId = thread.id + isNewThread = true } else { const thread = await openai.beta.threads.retrieve(chatmessage.sessionId) threadId = thread.id } + // List all runs + if (!isNewThread) { + const promise = (threadId: string) => { + return new Promise((resolve) => { + const timeout = setInterval(async () => { + const allRuns = await openai.beta.threads.runs.list(threadId) + if (allRuns.data && allRuns.data.length) { + const firstRunId = allRuns.data[0].id + const runStatus = allRuns.data.find((run) => run.id === firstRunId)?.status + if ( + runStatus && + (runStatus === 'cancelled' || + runStatus === 'completed' || + runStatus === 'expired' || + runStatus === 'failed') + ) { + clearInterval(timeout) + resolve() + } + } else { + clearInterval(timeout) + resolve() + } + }, 500) + }) + } + await promise(threadId) + } + // Add message to thread await openai.beta.threads.messages.create(threadId, { role: 'user', @@ -217,27 +257,41 @@ class OpenAIAssistant_Agents implements INode { }) resolve(state) } else { - reject( - new Error( - `Error processing thread: ${state}, Thread ID: ${threadId}, Run ID: ${runId}. submit_tool_outputs.tool_calls are empty` - ) - ) + await openai.beta.threads.runs.cancel(threadId, runId) + resolve('requires_action_retry') } } } else if (state === 'cancelled' || state === 'expired' || state === 'failed') { clearInterval(timeout) - reject(new Error(`Error processing thread: ${state}, Thread ID: ${threadId}, Run ID: ${runId}`)) + reject( + new Error(`Error processing thread: ${state}, Thread ID: ${threadId}, Run ID: ${runId}, Status: ${state}`) + ) } }, 500) }) } // Polling run status + let runThreadId = runThread.id let state = await promise(threadId, runThread.id) while (state === 'requires_action') { state = await promise(threadId, runThread.id) } + let retries = 3 + while (state === 'requires_action_retry') { + if (retries > 0) { + retries -= 1 + const newRunThread = await openai.beta.threads.runs.create(threadId, { + assistant_id: retrievedAssistant.id + }) + runThreadId = newRunThread.id + state = await promise(threadId, newRunThread.id) + } else { + throw new Error(`Error processing thread: ${state}, Thread ID: ${threadId}`) + } + } + // List messages const messages = await openai.beta.threads.messages.list(threadId) const messageData = messages.data ?? [] @@ -245,12 +299,58 @@ class OpenAIAssistant_Agents implements INode { if (!assistantMessages.length) return '' let returnVal = '' + const fileAnnotations = [] for (let i = 0; i < assistantMessages[0].content.length; i += 1) { if (assistantMessages[0].content[i].type === 'text') { const content = assistantMessages[0].content[i] as MessageContentText - returnVal += content.text.value - //TODO: handle annotations + if (content.text.annotations) { + const message_content = content.text + const annotations = message_content.annotations + + const dirPath = path.join(getUserHome(), '.flowise', 'openai-assistant') + + // Iterate over the annotations and add footnotes + for (let index = 0; index < annotations.length; index++) { + const annotation = annotations[index] + let filePath = '' + + // Gather citations based on annotation attributes + const file_citation = (annotation as OpenAI.Beta.Threads.Messages.MessageContentText.Text.FileCitation) + .file_citation + if (file_citation) { + const cited_file = await openai.files.retrieve(file_citation.file_id) + // eslint-disable-next-line no-useless-escape + const fileName = cited_file.filename.split(/[\/\\]/).pop() ?? cited_file.filename + filePath = path.join(getUserHome(), '.flowise', 'openai-assistant', fileName) + await downloadFile(cited_file, filePath, dirPath, openAIApiKey) + fileAnnotations.push({ + filePath, + fileName + }) + } else { + const file_path = (annotation as OpenAI.Beta.Threads.Messages.MessageContentText.Text.FilePath).file_path + if (file_path) { + const cited_file = await openai.files.retrieve(file_path.file_id) + // eslint-disable-next-line no-useless-escape + const fileName = cited_file.filename.split(/[\/\\]/).pop() ?? cited_file.filename + filePath = path.join(getUserHome(), '.flowise', 'openai-assistant', fileName) + await downloadFile(cited_file, filePath, dirPath, openAIApiKey) + fileAnnotations.push({ + filePath, + fileName + }) + } + } + + // Replace the text with a footnote + message_content.value = message_content.value.replace(`${annotation.text}`, `${filePath}`) + } + + returnVal += message_content.value + } else { + returnVal += content.text.value + } } else { const content = assistantMessages[0].content[i] as MessageContentImageFile const fileId = content.image_file.file_id @@ -271,7 +371,8 @@ class OpenAIAssistant_Agents implements INode { return { text: returnVal, usedTools, - assistant: { assistantId: openAIAssistantId, threadId, runId: runThread.id, messages: messageData } + fileAnnotations, + assistant: { assistantId: openAIAssistantId, threadId, runId: runThreadId, messages: messageData } } } catch (error) { throw new Error(error) diff --git a/packages/server/src/Interface.ts b/packages/server/src/Interface.ts index 8d0965f48..1d2724642 100644 --- a/packages/server/src/Interface.ts +++ b/packages/server/src/Interface.ts @@ -30,6 +30,7 @@ export interface IChatMessage { chatflowid: string sourceDocuments?: string usedTools?: string + fileAnnotations?: string chatType: string chatId: string memoryType?: string diff --git a/packages/server/src/database/entities/ChatMessage.ts b/packages/server/src/database/entities/ChatMessage.ts index b51aa4340..4054a26dd 100644 --- a/packages/server/src/database/entities/ChatMessage.ts +++ b/packages/server/src/database/entities/ChatMessage.ts @@ -23,6 +23,9 @@ export class ChatMessage implements IChatMessage { @Column({ nullable: true, type: 'text' }) usedTools?: string + @Column({ nullable: true, type: 'text' }) + fileAnnotations?: string + @Column() chatType: string diff --git a/packages/server/src/database/migrations/mysql/1700271021237-AddFileAnnotationsToChatMessage.ts b/packages/server/src/database/migrations/mysql/1700271021237-AddFileAnnotationsToChatMessage.ts new file mode 100644 index 000000000..a352cde8c --- /dev/null +++ b/packages/server/src/database/migrations/mysql/1700271021237-AddFileAnnotationsToChatMessage.ts @@ -0,0 +1,12 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddFileAnnotationsToChatMessage1700271021237 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + const columnExists = await queryRunner.hasColumn('chat_message', 'fileAnnotations') + if (!columnExists) queryRunner.query(`ALTER TABLE \`chat_message\` ADD COLUMN \`fileAnnotations\` TEXT;`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`chat_message\` DROP COLUMN \`fileAnnotations\`;`) + } +} diff --git a/packages/server/src/database/migrations/mysql/index.ts b/packages/server/src/database/migrations/mysql/index.ts index 4b7b8a954..eff089cda 100644 --- a/packages/server/src/database/migrations/mysql/index.ts +++ b/packages/server/src/database/migrations/mysql/index.ts @@ -8,6 +8,7 @@ import { AddAnalytic1694432361423 } from './1694432361423-AddAnalytic' import { AddChatHistory1694658767766 } from './1694658767766-AddChatHistory' import { AddAssistantEntity1699325775451 } from './1699325775451-AddAssistantEntity' import { AddUsedToolsToChatMessage1699481607341 } from './1699481607341-AddUsedToolsToChatMessage' +import { AddFileAnnotationsToChatMessage1700271021237 } from './1700271021237-AddFileAnnotationsToChatMessage' export const mysqlMigrations = [ Init1693840429259, @@ -19,5 +20,6 @@ export const mysqlMigrations = [ AddAnalytic1694432361423, AddChatHistory1694658767766, AddAssistantEntity1699325775451, - AddUsedToolsToChatMessage1699481607341 + AddUsedToolsToChatMessage1699481607341, + AddFileAnnotationsToChatMessage1700271021237 ] diff --git a/packages/server/src/database/migrations/postgres/1699481607341-AddUsedToolsToChatMessage.ts b/packages/server/src/database/migrations/postgres/1699481607341-AddUsedToolsToChatMessage.ts index f9f893f82..ae34c8132 100644 --- a/packages/server/src/database/migrations/postgres/1699481607341-AddUsedToolsToChatMessage.ts +++ b/packages/server/src/database/migrations/postgres/1699481607341-AddUsedToolsToChatMessage.ts @@ -6,6 +6,6 @@ export class AddUsedToolsToChatMessage1699481607341 implements MigrationInterfac } public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "chat_flow" DROP COLUMN "usedTools";`) + await queryRunner.query(`ALTER TABLE "chat_message" DROP COLUMN "usedTools";`) } } diff --git a/packages/server/src/database/migrations/postgres/1700271021237-AddFileAnnotationsToChatMessage.ts b/packages/server/src/database/migrations/postgres/1700271021237-AddFileAnnotationsToChatMessage.ts new file mode 100644 index 000000000..8824f57d5 --- /dev/null +++ b/packages/server/src/database/migrations/postgres/1700271021237-AddFileAnnotationsToChatMessage.ts @@ -0,0 +1,11 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddFileAnnotationsToChatMessage1700271021237 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "chat_message" ADD COLUMN IF NOT EXISTS "fileAnnotations" TEXT;`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "chat_message" DROP COLUMN "fileAnnotations";`) + } +} diff --git a/packages/server/src/database/migrations/postgres/index.ts b/packages/server/src/database/migrations/postgres/index.ts index 75562c0b5..93d02f3e0 100644 --- a/packages/server/src/database/migrations/postgres/index.ts +++ b/packages/server/src/database/migrations/postgres/index.ts @@ -8,6 +8,7 @@ import { AddAnalytic1694432361423 } from './1694432361423-AddAnalytic' import { AddChatHistory1694658756136 } from './1694658756136-AddChatHistory' import { AddAssistantEntity1699325775451 } from './1699325775451-AddAssistantEntity' import { AddUsedToolsToChatMessage1699481607341 } from './1699481607341-AddUsedToolsToChatMessage' +import { AddFileAnnotationsToChatMessage1700271021237 } from './1700271021237-AddFileAnnotationsToChatMessage' export const postgresMigrations = [ Init1693891895163, @@ -19,5 +20,6 @@ export const postgresMigrations = [ AddAnalytic1694432361423, AddChatHistory1694658756136, AddAssistantEntity1699325775451, - AddUsedToolsToChatMessage1699481607341 + AddUsedToolsToChatMessage1699481607341, + AddFileAnnotationsToChatMessage1700271021237 ] diff --git a/packages/server/src/database/migrations/sqlite/1700271021237-AddFileAnnotationsToChatMessage.ts b/packages/server/src/database/migrations/sqlite/1700271021237-AddFileAnnotationsToChatMessage.ts new file mode 100644 index 000000000..af29fba4b --- /dev/null +++ b/packages/server/src/database/migrations/sqlite/1700271021237-AddFileAnnotationsToChatMessage.ts @@ -0,0 +1,20 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddFileAnnotationsToChatMessage1700271021237 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "temp_chat_message" ("id" varchar PRIMARY KEY NOT NULL, "role" varchar NOT NULL, "chatflowid" varchar NOT NULL, "content" text NOT NULL, "sourceDocuments" text, "usedTools" text, "fileAnnotations" text, "createdDate" datetime NOT NULL DEFAULT (datetime('now')), "chatType" VARCHAR NOT NULL DEFAULT 'INTERNAL', "chatId" VARCHAR NOT NULL, "memoryType" VARCHAR, "sessionId" VARCHAR);` + ) + await queryRunner.query( + `INSERT INTO "temp_chat_message" ("id", "role", "chatflowid", "content", "sourceDocuments", "usedTools", "createdDate", "chatType", "chatId", "memoryType", "sessionId") SELECT "id", "role", "chatflowid", "content", "sourceDocuments", "usedTools", "createdDate", "chatType", "chatId", "memoryType", "sessionId" FROM "chat_message";` + ) + await queryRunner.query(`DROP TABLE "chat_message";`) + await queryRunner.query(`ALTER TABLE "temp_chat_message" RENAME TO "chat_message";`) + await queryRunner.query(`CREATE INDEX "IDX_e574527322272fd838f4f0f3d3" ON "chat_message" ("chatflowid") ;`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE IF EXISTS "temp_chat_message";`) + await queryRunner.query(`ALTER TABLE "chat_message" DROP COLUMN "fileAnnotations";`) + } +} diff --git a/packages/server/src/database/migrations/sqlite/index.ts b/packages/server/src/database/migrations/sqlite/index.ts index 4a14fc407..edba59305 100644 --- a/packages/server/src/database/migrations/sqlite/index.ts +++ b/packages/server/src/database/migrations/sqlite/index.ts @@ -8,6 +8,7 @@ import { AddAnalytic1694432361423 } from './1694432361423-AddAnalytic' import { AddChatHistory1694657778173 } from './1694657778173-AddChatHistory' import { AddAssistantEntity1699325775451 } from './1699325775451-AddAssistantEntity' import { AddUsedToolsToChatMessage1699481607341 } from './1699481607341-AddUsedToolsToChatMessage' +import { AddFileAnnotationsToChatMessage1700271021237 } from './1700271021237-AddFileAnnotationsToChatMessage' export const sqliteMigrations = [ Init1693835579790, @@ -19,5 +20,6 @@ export const sqliteMigrations = [ AddAnalytic1694432361423, AddChatHistory1694657778173, AddAssistantEntity1699325775451, - AddUsedToolsToChatMessage1699481607341 + AddUsedToolsToChatMessage1699481607341, + AddFileAnnotationsToChatMessage1700271021237 ] diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index ba6c3ce0e..57245f2cd 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -138,6 +138,7 @@ export class App { '/api/v1/node-icon/', '/api/v1/components-credentials-icon/', '/api/v1/chatflows-streaming', + '/api/v1/openai-assistants-file', '/api/v1/ip' ] this.app.use((req, res, next) => { @@ -782,8 +783,8 @@ export class App { await openai.beta.assistants.update(assistantDetails.id, { name: assistantDetails.name, - description: assistantDetails.description, - instructions: assistantDetails.instructions, + description: assistantDetails.description ?? '', + instructions: assistantDetails.instructions ?? '', model: assistantDetails.model, tools: filteredTools, file_ids: uniqWith( @@ -952,7 +953,7 @@ export class App { const results = await this.AppDataSource.getRepository(Assistant).delete({ id: req.params.id }) - await openai.beta.assistants.del(assistantDetails.id) + if (req.query.isDeleteBoth) await openai.beta.assistants.del(assistantDetails.id) return res.json(results) } catch (error: any) { @@ -961,6 +962,14 @@ export class App { } }) + // Download file from assistant + this.app.post('/api/v1/openai-assistants-file', async (req: Request, res: Response) => { + const filePath = path.join(getUserHome(), '.flowise', 'openai-assistant', req.body.fileName) + res.setHeader('Content-Disposition', 'attachment; filename=' + path.basename(filePath)) + const fileStream = fs.createReadStream(filePath) + fileStream.pipe(res) + }) + // ---------------------------------------- // Configuration // ---------------------------------------- @@ -1499,6 +1508,7 @@ export class App { } if (result?.sourceDocuments) apiMessage.sourceDocuments = JSON.stringify(result.sourceDocuments) if (result?.usedTools) apiMessage.usedTools = JSON.stringify(result.usedTools) + if (result?.fileAnnotations) apiMessage.fileAnnotations = JSON.stringify(result.fileAnnotations) await this.addChatMessage(apiMessage) logger.debug(`[server]: Finished running ${nodeToExecuteData.label} (${nodeToExecuteData.id})`) diff --git a/packages/ui/src/api/assistants.js b/packages/ui/src/api/assistants.js index 63dd5e18a..ac941126d 100644 --- a/packages/ui/src/api/assistants.js +++ b/packages/ui/src/api/assistants.js @@ -12,7 +12,8 @@ const createNewAssistant = (body) => client.post(`/assistants`, body) const updateAssistant = (id, body) => client.put(`/assistants/${id}`, body) -const deleteAssistant = (id) => client.delete(`/assistants/${id}`) +const deleteAssistant = (id, isDeleteBoth) => + isDeleteBoth ? client.delete(`/assistants/${id}?isDeleteBoth=true`) : client.delete(`/assistants/${id}`) export default { getAllAssistants, diff --git a/packages/ui/src/ui-component/dialog/ViewMessagesDialog.js b/packages/ui/src/ui-component/dialog/ViewMessagesDialog.js index 2e52d5963..29a64155c 100644 --- a/packages/ui/src/ui-component/dialog/ViewMessagesDialog.js +++ b/packages/ui/src/ui-component/dialog/ViewMessagesDialog.js @@ -7,6 +7,7 @@ import rehypeMathjax from 'rehype-mathjax' import rehypeRaw from 'rehype-raw' import remarkGfm from 'remark-gfm' import remarkMath from 'remark-math' +import axios from 'axios' // material-ui import { @@ -28,7 +29,7 @@ import DatePicker from 'react-datepicker' import robotPNG from 'assets/images/robot.png' import userPNG from 'assets/images/account.png' import msgEmptySVG from 'assets/images/message_empty.svg' -import { IconFileExport, IconEraser, IconX } from '@tabler/icons' +import { IconFileExport, IconEraser, IconX, IconDownload } from '@tabler/icons' // Project import import { MemoizedReactMarkdown } from 'ui-component/markdown/MemoizedReactMarkdown' @@ -48,6 +49,7 @@ import useConfirm from 'hooks/useConfirm' // Utils import { isValidURL, removeDuplicateURL } from 'utils/genericHelper' import useNotifier from 'utils/useNotifier' +import { baseURL } from 'store/constant' import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from 'store/actions' @@ -130,6 +132,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { } if (chatmsg.sourceDocuments) msg.sourceDocuments = JSON.parse(chatmsg.sourceDocuments) if (chatmsg.usedTools) msg.usedTools = JSON.parse(chatmsg.usedTools) + if (chatmsg.fileAnnotations) msg.fileAnnotations = JSON.parse(chatmsg.fileAnnotations) if (!Object.prototype.hasOwnProperty.call(obj, chatPK)) { obj[chatPK] = { @@ -253,6 +256,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { } if (chatmsg.sourceDocuments) obj.sourceDocuments = JSON.parse(chatmsg.sourceDocuments) if (chatmsg.usedTools) obj.usedTools = JSON.parse(chatmsg.usedTools) + if (chatmsg.fileAnnotations) obj.fileAnnotations = JSON.parse(chatmsg.fileAnnotations) loadedMessages.push(obj) } @@ -318,6 +322,26 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { window.open(data, '_blank') } + const downloadFile = async (fileAnnotation) => { + try { + const response = await axios.post( + `${baseURL}/api/v1/openai-assistants-file`, + { fileName: fileAnnotation.fileName }, + { responseType: 'blob' } + ) + const blob = new Blob([response.data], { type: response.headers['content-type'] }) + const downloadUrl = window.URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = downloadUrl + link.download = fileAnnotation.fileName + document.body.appendChild(link) + link.click() + link.remove() + } catch (error) { + console.error('Download failed:', error) + } + } + const onSourceDialogClick = (data, title) => { setSourceDialogProps({ data, title }) setSourceDialogOpen(true) @@ -648,6 +672,30 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { {message.message}
+ {message.fileAnnotations && ( +
+ {message.fileAnnotations.map((fileAnnotation, index) => { + return ( + + ) + })} +
+ )} {message.sourceDocuments && (
{removeDuplicateURL(message).map((source, index) => { diff --git a/packages/ui/src/views/assistants/AssistantDialog.js b/packages/ui/src/views/assistants/AssistantDialog.js index e841fc4fa..30087baed 100644 --- a/packages/ui/src/views/assistants/AssistantDialog.js +++ b/packages/ui/src/views/assistants/AssistantDialog.js @@ -9,12 +9,12 @@ import { Box, Typography, Button, IconButton, Dialog, DialogActions, DialogConte import { StyledButton } from 'ui-component/button/StyledButton' import { TooltipWithParser } from 'ui-component/tooltip/TooltipWithParser' -import ConfirmDialog from 'ui-component/dialog/ConfirmDialog' import { Dropdown } from 'ui-component/dropdown/Dropdown' import { MultiDropdown } from 'ui-component/dropdown/MultiDropdown' import CredentialInputHandler from 'views/canvas/CredentialInputHandler' import { File } from 'ui-component/file/File' import { BackdropLoader } from 'ui-component/loading/BackdropLoader' +import DeleteConfirmDialog from './DeleteConfirmDialog' // Icons import { IconX } from '@tabler/icons' @@ -23,7 +23,6 @@ import { IconX } from '@tabler/icons' import assistantsApi from 'api/assistants' // Hooks -import useConfirm from 'hooks/useConfirm' import useApi from 'hooks/useApi' // utils @@ -71,14 +70,8 @@ const assistantAvailableModels = [ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => { const portalElement = document.getElementById('portal') - - const dispatch = useDispatch() - - // ==============================|| Snackbar ||============================== // - useNotifier() - const { confirm } = useConfirm() - + const dispatch = useDispatch() const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)) const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) @@ -97,6 +90,8 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => { const [assistantFiles, setAssistantFiles] = useState([]) const [uploadAssistantFiles, setUploadAssistantFiles] = useState('') const [loading, setLoading] = useState(false) + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) + const [deleteDialogProps, setDeleteDialogProps] = useState({}) useEffect(() => { if (show) dispatch({ type: SHOW_CANVAS_DIALOG }) @@ -123,20 +118,7 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => { useEffect(() => { if (getAssistantObjApi.data) { - setOpenAIAssistantId(getAssistantObjApi.data.id) - setAssistantName(getAssistantObjApi.data.name) - setAssistantDesc(getAssistantObjApi.data.description) - setAssistantModel(getAssistantObjApi.data.model) - setAssistantInstructions(getAssistantObjApi.data.instructions) - setAssistantFiles(getAssistantObjApi.data.files ?? []) - - let tools = [] - if (getAssistantObjApi.data.tools && getAssistantObjApi.data.tools.length) { - for (const tool of getAssistantObjApi.data.tools) { - tools.push(tool.type) - } - } - setAssistantTools(tools) + syncData(getAssistantObjApi.data) } }, [getAssistantObjApi.data]) @@ -199,6 +181,23 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [dialogProps]) + const syncData = (data) => { + setOpenAIAssistantId(data.id) + setAssistantName(data.name) + setAssistantDesc(data.description) + setAssistantModel(data.model) + setAssistantInstructions(data.instructions) + setAssistantFiles(data.files ?? []) + + let tools = [] + if (data.tools && data.tools.length) { + for (const tool of data.tools) { + tools.push(tool.type) + } + } + setAssistantTools(tools) + } + const addNewAssistant = async () => { setLoading(true) try { @@ -309,41 +308,17 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => { } } - const deleteAssistant = async () => { - const confirmPayload = { - title: `Delete Assistant`, - description: `Delete Assistant ${assistantName}?`, - confirmButtonName: 'Delete', - cancelButtonName: 'Cancel' - } - const isConfirmed = await confirm(confirmPayload) - - if (isConfirmed) { - try { - const delResp = await assistantsApi.deleteAssistant(assistantId) - if (delResp.data) { - enqueueSnackbar({ - message: 'Assistant deleted', - options: { - key: new Date().getTime() + Math.random(), - variant: 'success', - action: (key) => ( - - ) - } - }) - onConfirm() - } - } catch (error) { - const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` + const onSyncClick = async () => { + setLoading(true) + try { + const getResp = await assistantsApi.getAssistantObj(openAIAssistantId, assistantCredential) + if (getResp.data) { + syncData(getResp.data) enqueueSnackbar({ - message: `Failed to delete Assistant: ${errorData}`, + message: 'Assistant successfully synced!', options: { key: new Date().getTime() + Math.random(), - variant: 'error', - persist: true, + variant: 'success', action: (key) => ( + ) + } + }) + setLoading(false) + } + } + + const onDeleteClick = () => { + setDeleteDialogProps({ + title: `Delete Assistant`, + description: `Delete Assistant ${assistantName}?`, + cancelButtonName: 'Cancel' + }) + setDeleteDialogOpen(true) + } + + const deleteAssistant = async (isDeleteBoth) => { + setDeleteDialogOpen(false) + try { + const delResp = await assistantsApi.deleteAssistant(assistantId, isDeleteBoth) + if (delResp.data) { + enqueueSnackbar({ + message: 'Assistant deleted', + options: { + key: new Date().getTime() + Math.random(), + variant: 'success', + action: (key) => ( + + ) + } + }) + onConfirm() + } + } catch (error) { + const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` + enqueueSnackbar({ + message: `Failed to delete Assistant: ${errorData}`, + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + persist: true, + action: (key) => ( + + ) + } + }) + onCancel() } } @@ -578,7 +616,12 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => { {dialogProps.type === 'EDIT' && ( - deleteAssistant()}> + onSyncClick()}> + Sync + + )} + {dialogProps.type === 'EDIT' && ( + onDeleteClick()}> Delete )} @@ -590,7 +633,13 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => { {dialogProps.confirmButtonName} - + setDeleteDialogOpen(false)} + onDelete={() => deleteAssistant()} + onDeleteBoth={() => deleteAssistant(true)} + /> {loading && } ) : null diff --git a/packages/ui/src/views/assistants/DeleteConfirmDialog.js b/packages/ui/src/views/assistants/DeleteConfirmDialog.js new file mode 100644 index 000000000..f4453631b --- /dev/null +++ b/packages/ui/src/views/assistants/DeleteConfirmDialog.js @@ -0,0 +1,47 @@ +import { createPortal } from 'react-dom' +import PropTypes from 'prop-types' +import { Button, Dialog, DialogContent, DialogTitle } from '@mui/material' +import { StyledButton } from 'ui-component/button/StyledButton' + +const DeleteConfirmDialog = ({ show, dialogProps, onCancel, onDelete, onDeleteBoth }) => { + const portalElement = document.getElementById('portal') + + const component = show ? ( + + + {dialogProps.title} + + + {dialogProps.description} +
+ + Delete only from Flowise + + + Delete from both OpenAI and Flowise + + +
+
+
+ ) : null + + return createPortal(component, portalElement) +} + +DeleteConfirmDialog.propTypes = { + show: PropTypes.bool, + dialogProps: PropTypes.object, + onDeleteBoth: PropTypes.func, + onDelete: PropTypes.func, + onCancel: PropTypes.func +} + +export default DeleteConfirmDialog diff --git a/packages/ui/src/views/chatmessage/ChatMessage.js b/packages/ui/src/views/chatmessage/ChatMessage.js index 0cf5695be..7cfd04740 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.js +++ b/packages/ui/src/views/chatmessage/ChatMessage.js @@ -7,10 +7,11 @@ import rehypeMathjax from 'rehype-mathjax' import rehypeRaw from 'rehype-raw' import remarkGfm from 'remark-gfm' import remarkMath from 'remark-math' +import axios from 'axios' -import { CircularProgress, OutlinedInput, Divider, InputAdornment, IconButton, Box, Chip } from '@mui/material' +import { CircularProgress, OutlinedInput, Divider, InputAdornment, IconButton, Box, Chip, Button } from '@mui/material' import { useTheme } from '@mui/material/styles' -import { IconSend } from '@tabler/icons' +import { IconSend, IconDownload } from '@tabler/icons' // project import import { CodeBlock } from 'ui-component/markdown/CodeBlock' @@ -139,7 +140,13 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { setMessages((prevMessages) => [ ...prevMessages, - { message: text, sourceDocuments: data?.sourceDocuments, usedTools: data?.usedTools, type: 'apiMessage' } + { + message: text, + sourceDocuments: data?.sourceDocuments, + usedTools: data?.usedTools, + fileAnnotations: data?.fileAnnotations, + type: 'apiMessage' + } ]) } @@ -170,6 +177,26 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { } } + const downloadFile = async (fileAnnotation) => { + try { + const response = await axios.post( + `${baseURL}/api/v1/openai-assistants-file`, + { fileName: fileAnnotation.fileName }, + { responseType: 'blob' } + ) + const blob = new Blob([response.data], { type: response.headers['content-type'] }) + const downloadUrl = window.URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = downloadUrl + link.download = fileAnnotation.fileName + document.body.appendChild(link) + link.click() + link.remove() + } catch (error) { + console.error('Download failed:', error) + } + } + // Get chatmessages successful useEffect(() => { if (getChatmessageApi.data?.length) { @@ -183,6 +210,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { } if (message.sourceDocuments) obj.sourceDocuments = JSON.parse(message.sourceDocuments) if (message.usedTools) obj.usedTools = JSON.parse(message.usedTools) + if (message.fileAnnotations) obj.fileAnnotations = JSON.parse(message.fileAnnotations) return obj }) setMessages((prevMessages) => [...prevMessages, ...loadedMessages]) @@ -331,6 +359,23 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { {message.message}
+ {message.fileAnnotations && ( +
+ {message.fileAnnotations.map((fileAnnotation, index) => { + return ( + + ) + })} +
+ )} {message.sourceDocuments && (
{removeDuplicateURL(message).map((source, index) => {