From e422ce287b9eba7d4e0ba29d574d925b549a4fe9 Mon Sep 17 00:00:00 2001 From: Henry Heng Date: Tue, 2 Apr 2024 23:47:19 +0100 Subject: [PATCH] Feature/Indexing (#1802) * indexing * fix for multiple files upsert * fix default Postgres port * fix SQLite node description * add MySQLRecordManager node * fix MySQL unique index * add upsert history * update jsx ui * lint-fix * update dialog details * update llamaindex pinecone --------- Co-authored-by: chungyau97 --- .../credentials/MySQLApi.credential.ts | 31 ++ .../MySQLRecordManager/MySQLrecordManager.ts | 361 ++++++++++++++ .../MySQLRecordManager/mysql.png | Bin 0 -> 3750 bytes .../PostgresRecordManager.ts | 332 +++++++++++++ .../PostgresRecordManager/postgres.svg | 1 + .../SQLiteRecordManager.ts | 332 +++++++++++++ .../SQLiteRecordManager/sqlite.png | Bin 0 -> 50383 bytes .../nodes/vectorstores/Astra/Astra.ts | 5 +- .../nodes/vectorstores/Chroma/Chroma.ts | 34 +- .../Elasticsearch/Elasticsearch.ts | 34 +- .../nodes/vectorstores/Faiss/Faiss.ts | 6 +- .../InMemory/InMemoryVectorStore.ts | 5 +- .../nodes/vectorstores/Milvus/Milvus.ts | 6 +- .../vectorstores/MongoDBAtlas/MongoDBAtlas.ts | 5 +- .../vectorstores/OpenSearch/OpenSearch.ts | 5 +- .../nodes/vectorstores/Pinecone/Pinecone.ts | 39 +- .../Pinecone/Pinecone_LlamaIndex.ts | 5 +- .../nodes/vectorstores/Postgres/Postgres.ts | 49 +- .../nodes/vectorstores/Qdrant/Qdrant.ts | 89 +++- .../nodes/vectorstores/Redis/Redis.ts | 6 +- .../vectorstores/SimpleStore/SimpleStore.ts | 5 +- .../vectorstores/Singlestore/Singlestore.ts | 5 +- .../nodes/vectorstores/Supabase/Supabase.ts | 46 +- .../nodes/vectorstores/Vectara/Vectara.ts | 5 +- .../nodes/vectorstores/Weaviate/Weaviate.ts | 38 +- .../components/nodes/vectorstores/Zep/Zep.ts | 5 +- .../nodes/vectorstores/ZepCloud/ZepCloud.ts | 5 +- packages/components/package.json | 1 + packages/components/src/Interface.ts | 12 +- packages/components/src/indexing.ts | 355 ++++++++++++++ packages/server/src/Interface.ts | 8 + .../server/src/controllers/tools/index.ts | 2 +- .../src/controllers/upsert-history/index.ts | 30 ++ .../server/src/database/entities/Assistant.ts | 2 +- .../server/src/database/entities/ChatFlow.ts | 4 +- .../src/database/entities/ChatMessage.ts | 2 +- .../database/entities/ChatMessageFeedback.ts | 2 +- .../src/database/entities/Credential.ts | 4 +- packages/server/src/database/entities/Tool.ts | 4 +- .../src/database/entities/UpsertHistory.ts | 22 + .../server/src/database/entities/Variable.ts | 4 +- .../server/src/database/entities/index.ts | 4 +- .../1709814301358-AddUpsertHistoryEntity.ts | 21 + .../src/database/migrations/mysql/index.ts | 2 + .../1709814301358-AddUpsertHistoryEntity.ts | 20 + .../src/database/migrations/postgres/index.ts | 2 + .../1709814301358-AddUpsertHistoryEntity.ts | 13 + .../src/database/migrations/sqlite/index.ts | 2 + packages/server/src/routes/index.ts | 2 + .../server/src/routes/upsert-history/index.ts | 15 + .../server/src/services/chatflows/index.ts | 14 +- .../src/services/upsert-history/index.ts | 66 +++ .../server/src/services/variables/index.ts | 2 +- packages/server/src/utils/buildChatflow.ts | 1 - packages/server/src/utils/index.ts | 88 +++- packages/server/src/utils/upsertVector.ts | 27 +- packages/ui/src/api/vectorstore.js | 6 +- .../assets/images/upsert_history_empty.svg | 1 + packages/ui/src/menu-items/settings.js | 18 +- packages/ui/src/utils/genericHelper.js | 5 + packages/ui/src/views/canvas/CanvasHeader.jsx | 14 + .../ui/src/views/chatmessage/ChatMessage.jsx | 4 + .../views/vectorstore/UpsertHistoryDialog.jsx | 453 ++++++++++++++++++ .../views/vectorstore/UpsertResultDialog.jsx | 94 ++++ .../views/vectorstore/VectorStoreDialog.jsx | 8 +- .../views/vectorstore/VectorStorePopUp.jsx | 66 +-- pnpm-lock.yaml | 398 +++++++++++---- 67 files changed, 3006 insertions(+), 246 deletions(-) create mode 100644 packages/components/credentials/MySQLApi.credential.ts create mode 100644 packages/components/nodes/recordmanager/MySQLRecordManager/MySQLrecordManager.ts create mode 100644 packages/components/nodes/recordmanager/MySQLRecordManager/mysql.png create mode 100644 packages/components/nodes/recordmanager/PostgresRecordManager/PostgresRecordManager.ts create mode 100644 packages/components/nodes/recordmanager/PostgresRecordManager/postgres.svg create mode 100644 packages/components/nodes/recordmanager/SQLiteRecordManager/SQLiteRecordManager.ts create mode 100644 packages/components/nodes/recordmanager/SQLiteRecordManager/sqlite.png create mode 100644 packages/components/src/indexing.ts create mode 100644 packages/server/src/controllers/upsert-history/index.ts create mode 100644 packages/server/src/database/entities/UpsertHistory.ts create mode 100644 packages/server/src/database/migrations/mysql/1709814301358-AddUpsertHistoryEntity.ts create mode 100644 packages/server/src/database/migrations/postgres/1709814301358-AddUpsertHistoryEntity.ts create mode 100644 packages/server/src/database/migrations/sqlite/1709814301358-AddUpsertHistoryEntity.ts create mode 100644 packages/server/src/routes/upsert-history/index.ts create mode 100644 packages/server/src/services/upsert-history/index.ts create mode 100644 packages/ui/src/assets/images/upsert_history_empty.svg create mode 100644 packages/ui/src/views/vectorstore/UpsertHistoryDialog.jsx create mode 100644 packages/ui/src/views/vectorstore/UpsertResultDialog.jsx diff --git a/packages/components/credentials/MySQLApi.credential.ts b/packages/components/credentials/MySQLApi.credential.ts new file mode 100644 index 00000000..68cb3b0f --- /dev/null +++ b/packages/components/credentials/MySQLApi.credential.ts @@ -0,0 +1,31 @@ +import { INodeParams, INodeCredential } from '../src/Interface' + +class MySQLApi implements INodeCredential { + label: string + name: string + version: number + description: string + inputs: INodeParams[] + + constructor() { + this.label = 'MySQL API' + this.name = 'MySQLApi' + this.version = 1.0 + this.inputs = [ + { + label: 'User', + name: 'user', + type: 'string', + placeholder: '' + }, + { + label: 'Password', + name: 'password', + type: 'password', + placeholder: '' + } + ] + } +} + +module.exports = { credClass: MySQLApi } diff --git a/packages/components/nodes/recordmanager/MySQLRecordManager/MySQLrecordManager.ts b/packages/components/nodes/recordmanager/MySQLRecordManager/MySQLrecordManager.ts new file mode 100644 index 00000000..be06da0e --- /dev/null +++ b/packages/components/nodes/recordmanager/MySQLRecordManager/MySQLrecordManager.ts @@ -0,0 +1,361 @@ +import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' +import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' +import { ListKeyOptions, RecordManagerInterface, UpdateOptions } from '@langchain/community/indexes/base' +import { DataSource, QueryRunner } from 'typeorm' + +class MySQLRecordManager_RecordManager implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + badge: string + baseClasses: string[] + credential: INodeParams + inputs: INodeParams[] + + constructor() { + this.label = 'MySQL Record Manager' + this.name = 'MySQLRecordManager' + this.version = 1.0 + this.type = 'MySQL RecordManager' + this.icon = 'mysql.png' + this.category = 'Record Manager' + this.description = 'Use MySQL to keep track of document writes into the vector databases' + this.baseClasses = [this.type, 'RecordManager', ...getBaseClasses(MySQLRecordManager)] + this.badge = 'NEW' + this.inputs = [ + { + label: 'Host', + name: 'host', + type: 'string' + }, + { + label: 'Database', + name: 'database', + type: 'string' + }, + { + label: 'Port', + name: 'port', + type: 'number', + placeholder: '3306', + optional: true + }, + { + label: 'Additional Connection Configuration', + name: 'additionalConfig', + type: 'json', + additionalParams: true, + optional: true + }, + { + label: 'Table Name', + name: 'tableName', + type: 'string', + placeholder: 'upsertion_records', + additionalParams: true, + optional: true + }, + { + label: 'Namespace', + name: 'namespace', + type: 'string', + description: 'If not specified, chatflowid will be used', + additionalParams: true, + optional: true + }, + { + label: 'Cleanup', + name: 'cleanup', + type: 'options', + description: + 'Read more on the difference between different cleanup methods here', + options: [ + { + label: 'None', + name: 'none', + description: 'No clean up of old content' + }, + { + label: 'Incremental', + name: 'incremental', + description: + 'Delete previous versions of the content if content of the source document has changed. Important!! SourceId Key must be specified and document metadata must contains the specified key' + }, + { + label: 'Full', + name: 'full', + description: + 'Same as incremental, but if the source document has been deleted, it will be deleted from vector store as well, incremental mode will not.' + } + ], + additionalParams: true, + default: 'none' + }, + { + label: 'SourceId Key', + name: 'sourceIdKey', + type: 'string', + description: + 'Key used to get the true source of document, to be compared against the record. Document metadata must contains SourceId Key', + default: 'source', + placeholder: 'source', + additionalParams: true, + optional: true + } + ] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['MySQLApi'] + } + } + + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + const credentialData = await getCredentialData(nodeData.credential ?? '', options) + const user = getCredentialParam('user', credentialData, nodeData) + const password = getCredentialParam('password', credentialData, nodeData) + const _tableName = nodeData.inputs?.tableName as string + const tableName = _tableName ? _tableName : 'upsertion_records' + const additionalConfig = nodeData.inputs?.additionalConfig as string + const _namespace = nodeData.inputs?.namespace as string + const namespace = _namespace ? _namespace : options.chatflowid + const cleanup = nodeData.inputs?.cleanup as string + const _sourceIdKey = nodeData.inputs?.sourceIdKey as string + const sourceIdKey = _sourceIdKey ? _sourceIdKey : 'source' + + let additionalConfiguration = {} + if (additionalConfig) { + try { + additionalConfiguration = typeof additionalConfig === 'object' ? additionalConfig : JSON.parse(additionalConfig) + } catch (exception) { + throw new Error('Invalid JSON in the Additional Configuration: ' + exception) + } + } + + const mysqlOptions = { + ...additionalConfiguration, + type: 'mysql', + host: nodeData.inputs?.host as string, + port: nodeData.inputs?.port as number, + username: user, + password: password, + database: nodeData.inputs?.database as string + } + + const args = { + mysqlOptions, + tableName: tableName + } + + const recordManager = new MySQLRecordManager(namespace, args) + + ;(recordManager as any).cleanup = cleanup + ;(recordManager as any).sourceIdKey = sourceIdKey + + return recordManager + } +} + +type MySQLRecordManagerOptions = { + mysqlOptions: any + tableName?: string +} + +class MySQLRecordManager implements RecordManagerInterface { + lc_namespace = ['langchain', 'recordmanagers', 'mysql'] + + datasource: DataSource + + queryRunner: QueryRunner + + tableName: string + + namespace: string + + constructor(namespace: string, config: MySQLRecordManagerOptions) { + const { mysqlOptions, tableName } = config + this.namespace = namespace + this.tableName = tableName || 'upsertion_records' + this.datasource = new DataSource(mysqlOptions) + } + + async createSchema(): Promise { + try { + const appDataSource = await this.datasource.initialize() + + this.queryRunner = appDataSource.createQueryRunner() + + await this.queryRunner.manager.query(`create table if not exists \`${this.tableName}\` ( + \`uuid\` varchar(36) primary key default (UUID()), + \`key\` varchar(36) not null, + \`namespace\` varchar(36) not null, + \`updated_at\` DOUBLE precision not null, + \`group_id\` varchar(36), + unique key \`unique_key_namespace\` (\`key\`, +\`namespace\`));`) + const columns = [`updated_at`, `key`, `namespace`, `group_id`] + for (const column of columns) { + // MySQL does not support 'IF NOT EXISTS' function for Index + const Check = await this.queryRunner.manager.query( + `SELECT COUNT(1) IndexIsThere FROM INFORMATION_SCHEMA.STATISTICS + WHERE table_schema=DATABASE() AND table_name='${this.tableName}' AND index_name='${column}_index';` + ) + if (Check[0].IndexIsThere === 0) + await this.queryRunner.manager.query(`CREATE INDEX \`${column}_index\` + ON \`${this.tableName}\` (\`${column}\`);`) + } + } catch (e: any) { + // This error indicates that the table already exists + // Due to asynchronous nature of the code, it is possible that + // the table is created between the time we check if it exists + // and the time we try to create it. It can be safely ignored. + if ('code' in e && e.code === '23505') { + return + } + throw e + } + } + + async getTime(): Promise { + try { + const res = await this.queryRunner.manager.query(`SELECT UNIX_TIMESTAMP(NOW()) AS epoch`) + return Number.parseFloat(res[0].epoch) + } catch (error) { + console.error('Error getting time in MySQLRecordManager:') + throw error + } + } + + async update(keys: string[], updateOptions?: UpdateOptions): Promise { + if (keys.length === 0) { + return + } + + const updatedAt = await this.getTime() + const { timeAtLeast, groupIds: _groupIds } = updateOptions ?? {} + + if (timeAtLeast && updatedAt < timeAtLeast) { + throw new Error(`Time sync issue with database ${updatedAt} < ${timeAtLeast}`) + } + + const groupIds = _groupIds ?? keys.map(() => null) + + if (groupIds.length !== keys.length) { + throw new Error(`Number of keys (${keys.length}) does not match number of group_ids (${groupIds.length})`) + } + + const recordsToUpsert = keys.map((key, i) => [ + key, + this.namespace, + updatedAt, + groupIds[i] ?? null // Ensure groupIds[i] is null if undefined + ]) + + const query = ` + INSERT INTO \`${this.tableName}\` (\`key\`, \`namespace\`, \`updated_at\`, \`group_id\`) + VALUES (?, ?, ?, ?) + ON DUPLICATE KEY UPDATE updated_at = updated_at;` + + // To handle multiple files upsert + for (const record of recordsToUpsert) { + // Consider using a transaction for batch operations + await this.queryRunner.manager.query(query, record.flat()) + } + } + + async exists(keys: string[]): Promise { + if (keys.length === 0) { + return [] + } + + // Prepare the placeholders and the query + const placeholders = keys.map(() => `?`).join(', ') + const query = ` + SELECT \`key\` + FROM \`${this.tableName}\` + WHERE \`namespace\` = ? AND \`key\` IN (${placeholders})` + + // Initialize an array to fill with the existence checks + const existsArray = new Array(keys.length).fill(false) + + try { + // Execute the query + const rows = await this.queryRunner.manager.query(query, [this.namespace, ...keys.flat()]) + // Create a set of existing keys for faster lookup + const existingKeysSet = new Set(rows.map((row: { key: string }) => row.key)) + // Map the input keys to booleans indicating if they exist + keys.forEach((key, index) => { + existsArray[index] = existingKeysSet.has(key) + }) + return existsArray + } catch (error) { + console.error('Error checking existence of keys') + throw error // Allow the caller to handle the error + } + } + + async listKeys(options?: ListKeyOptions): Promise { + try { + const { before, after, limit, groupIds } = options ?? {} + let query = `SELECT \`key\` FROM \`${this.tableName}\` WHERE \`namespace\` = ?` + const values: (string | number | string[])[] = [this.namespace] + + if (before) { + query += ` AND \`updated_at\` < ?` + values.push(before) + } + + if (after) { + query += ` AND \`updated_at\` > ?` + values.push(after) + } + + if (limit) { + query += ` LIMIT ?` + values.push(limit) + } + + if (groupIds && Array.isArray(groupIds)) { + query += ` AND \`group_id\` IN (${groupIds + .filter((gid) => gid !== null) + .map(() => '?') + .join(', ')})` + values.push(...groupIds.filter((gid): gid is string => gid !== null)) + } + + query += ';' + + // Directly using try/catch with async/await for cleaner flow + const result = await this.queryRunner.manager.query(query, values) + return result.map((row: { key: string }) => row.key) + } catch (error) { + console.error('MySQLRecordManager listKeys Error: ') + throw error // Re-throw the error to be handled by the caller + } + } + + async deleteKeys(keys: string[]): Promise { + if (keys.length === 0) { + return + } + + const placeholders = keys.map(() => '?').join(', ') + const query = `DELETE FROM \`${this.tableName}\` WHERE \`namespace\` = ? AND \`key\` IN (${placeholders});` + const values = [this.namespace, ...keys].map((v) => (typeof v !== 'string' ? `${v}` : v)) + + // Directly using try/catch with async/await for cleaner flow + try { + await this.queryRunner.manager.query(query, values) + } catch (error) { + console.error('Error deleting keys') + throw error // Re-throw the error to be handled by the caller + } + } +} + +module.exports = { nodeClass: MySQLRecordManager_RecordManager } diff --git a/packages/components/nodes/recordmanager/MySQLRecordManager/mysql.png b/packages/components/nodes/recordmanager/MySQLRecordManager/mysql.png new file mode 100644 index 0000000000000000000000000000000000000000..11c2d8b2e83666b481dbd596a2f1f4109054aeca GIT binary patch literal 3750 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', + options: [ + { + label: 'None', + name: 'none', + description: 'No clean up of old content' + }, + { + label: 'Incremental', + name: 'incremental', + description: + 'Delete previous versions of the content if content of the source document has changed. Important!! SourceId Key must be specified and document metadata must contains the specified key' + }, + { + label: 'Full', + name: 'full', + description: + 'Same as incremental, but if the source document has been deleted, it will be deleted from vector store as well, incremental mode will not.' + } + ], + additionalParams: true, + default: 'none' + }, + { + label: 'SourceId Key', + name: 'sourceIdKey', + type: 'string', + description: + 'Key used to get the true source of document, to be compared against the record. Document metadata must contains SourceId Key', + default: 'source', + placeholder: 'source', + additionalParams: true, + optional: true + } + ] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['PostgresApi'] + } + } + + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + const credentialData = await getCredentialData(nodeData.credential ?? '', options) + const user = getCredentialParam('user', credentialData, nodeData) + const password = getCredentialParam('password', credentialData, nodeData) + const _tableName = nodeData.inputs?.tableName as string + const tableName = _tableName ? _tableName : 'upsertion_records' + const additionalConfig = nodeData.inputs?.additionalConfig as string + const _namespace = nodeData.inputs?.namespace as string + const namespace = _namespace ? _namespace : options.chatflowid + const cleanup = nodeData.inputs?.cleanup as string + const _sourceIdKey = nodeData.inputs?.sourceIdKey as string + const sourceIdKey = _sourceIdKey ? _sourceIdKey : 'source' + + let additionalConfiguration = {} + if (additionalConfig) { + try { + additionalConfiguration = typeof additionalConfig === 'object' ? additionalConfig : JSON.parse(additionalConfig) + } catch (exception) { + throw new Error('Invalid JSON in the Additional Configuration: ' + exception) + } + } + + const postgresConnectionOptions = { + ...additionalConfiguration, + type: 'postgres', + host: nodeData.inputs?.host as string, + port: nodeData.inputs?.port as number, + username: user, + password: password, + database: nodeData.inputs?.database as string + } + + const args = { + postgresConnectionOptions: postgresConnectionOptions, + tableName: tableName + } + + const recordManager = new PostgresRecordManager(namespace, args) + + ;(recordManager as any).cleanup = cleanup + ;(recordManager as any).sourceIdKey = sourceIdKey + + return recordManager + } +} + +type PostgresRecordManagerOptions = { + postgresConnectionOptions: any + tableName?: string +} + +class PostgresRecordManager implements RecordManagerInterface { + lc_namespace = ['langchain', 'recordmanagers', 'postgres'] + + datasource: DataSource + + queryRunner: QueryRunner + + tableName: string + + namespace: string + + constructor(namespace: string, config: PostgresRecordManagerOptions) { + const { postgresConnectionOptions, tableName } = config + this.namespace = namespace + this.datasource = new DataSource(postgresConnectionOptions) + this.tableName = tableName || 'upsertion_records' + } + + async createSchema(): Promise { + try { + const appDataSource = await this.datasource.initialize() + + this.queryRunner = appDataSource.createQueryRunner() + + await this.queryRunner.manager.query(` + CREATE TABLE IF NOT EXISTS "${this.tableName}" ( + uuid UUID PRIMARY KEY DEFAULT gen_random_uuid(), + key TEXT NOT NULL, + namespace TEXT NOT NULL, + updated_at Double PRECISION NOT NULL, + group_id TEXT, + UNIQUE (key, namespace) + ); + CREATE INDEX IF NOT EXISTS updated_at_index ON "${this.tableName}" (updated_at); + CREATE INDEX IF NOT EXISTS key_index ON "${this.tableName}" (key); + CREATE INDEX IF NOT EXISTS namespace_index ON "${this.tableName}" (namespace); + CREATE INDEX IF NOT EXISTS group_id_index ON "${this.tableName}" (group_id);`) + } catch (e: any) { + // This error indicates that the table already exists + // Due to asynchronous nature of the code, it is possible that + // the table is created between the time we check if it exists + // and the time we try to create it. It can be safely ignored. + if ('code' in e && e.code === '23505') { + return + } + throw e + } + } + + async getTime(): Promise { + const res = await this.queryRunner.manager.query('SELECT EXTRACT(EPOCH FROM CURRENT_TIMESTAMP)') + return Number.parseFloat(res[0].extract) + } + + /** + * Generates the SQL placeholders for a specific row at the provided index. + * + * @param index - The index of the row for which placeholders need to be generated. + * @param numOfColumns - The number of columns we are inserting data into. + * @returns The SQL placeholders for the row values. + */ + private generatePlaceholderForRowAt(index: number, numOfColumns: number): string { + const placeholders = [] + for (let i = 0; i < numOfColumns; i += 1) { + placeholders.push(`$${index * numOfColumns + i + 1}`) + } + return `(${placeholders.join(', ')})` + } + + async update(keys: string[], updateOptions?: UpdateOptions): Promise { + if (keys.length === 0) { + return + } + + const updatedAt = await this.getTime() + const { timeAtLeast, groupIds: _groupIds } = updateOptions ?? {} + + if (timeAtLeast && updatedAt < timeAtLeast) { + throw new Error(`Time sync issue with database ${updatedAt} < ${timeAtLeast}`) + } + + const groupIds = _groupIds ?? keys.map(() => null) + + if (groupIds.length !== keys.length) { + throw new Error(`Number of keys (${keys.length}) does not match number of group_ids ${groupIds.length})`) + } + + const recordsToUpsert = keys.map((key, i) => [key, this.namespace, updatedAt, groupIds[i]]) + + const valuesPlaceholders = recordsToUpsert.map((_, j) => this.generatePlaceholderForRowAt(j, recordsToUpsert[0].length)).join(', ') + + const query = `INSERT INTO "${this.tableName}" (key, namespace, updated_at, group_id) VALUES ${valuesPlaceholders} ON CONFLICT (key, namespace) DO UPDATE SET updated_at = EXCLUDED.updated_at;` + await this.queryRunner.manager.query(query, recordsToUpsert.flat()) + } + + async exists(keys: string[]): Promise { + if (keys.length === 0) { + return [] + } + + const startIndex = 2 + const arrayPlaceholders = keys.map((_, i) => `$${i + startIndex}`).join(', ') + + const query = ` + SELECT k, (key is not null) ex from unnest(ARRAY[${arrayPlaceholders}]) k left join "${this.tableName}" on k=key and namespace = $1; + ` + const res = await this.queryRunner.manager.query(query, [this.namespace, ...keys.flat()]) + return res.map((row: { ex: boolean }) => row.ex) + } + + async listKeys(options?: ListKeyOptions): Promise { + const { before, after, limit, groupIds } = options ?? {} + let query = `SELECT key FROM "${this.tableName}" WHERE namespace = $1` + const values: (string | number | (string | null)[])[] = [this.namespace] + + let index = 2 + if (before) { + values.push(before) + query += ` AND updated_at < $${index}` + index += 1 + } + + if (after) { + values.push(after) + query += ` AND updated_at > $${index}` + index += 1 + } + + if (limit) { + values.push(limit) + query += ` LIMIT $${index}` + index += 1 + } + + if (groupIds) { + values.push(groupIds) + query += ` AND group_id = ANY($${index})` + index += 1 + } + + query += ';' + const res = await this.queryRunner.manager.query(query, values) + return res.map((row: { key: string }) => row.key) + } + + async deleteKeys(keys: string[]): Promise { + if (keys.length === 0) { + return + } + + const query = `DELETE FROM "${this.tableName}" WHERE namespace = $1 AND key = ANY($2);` + await this.queryRunner.manager.query(query, [this.namespace, keys]) + } + + /** + * Terminates the connection pool. + * @returns {Promise} + */ + async end(): Promise { + if (this.datasource && this.datasource.isInitialized) await this.datasource.destroy() + } +} + +module.exports = { nodeClass: PostgresRecordManager_RecordManager } diff --git a/packages/components/nodes/recordmanager/PostgresRecordManager/postgres.svg b/packages/components/nodes/recordmanager/PostgresRecordManager/postgres.svg new file mode 100644 index 00000000..f631e7a8 --- /dev/null +++ b/packages/components/nodes/recordmanager/PostgresRecordManager/postgres.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/recordmanager/SQLiteRecordManager/SQLiteRecordManager.ts b/packages/components/nodes/recordmanager/SQLiteRecordManager/SQLiteRecordManager.ts new file mode 100644 index 00000000..3bd95d27 --- /dev/null +++ b/packages/components/nodes/recordmanager/SQLiteRecordManager/SQLiteRecordManager.ts @@ -0,0 +1,332 @@ +import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' +import { getBaseClasses } from '../../../src/utils' +import { ListKeyOptions, RecordManagerInterface, UpdateOptions } from '@langchain/community/indexes/base' +import { DataSource, QueryRunner } from 'typeorm' +import path from 'path' + +class SQLiteRecordManager_RecordManager implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + badge: string + baseClasses: string[] + inputs: INodeParams[] + + constructor() { + this.label = 'SQLite Record Manager' + this.name = 'SQLiteRecordManager' + this.version = 1.0 + this.type = 'SQLite RecordManager' + this.icon = 'sqlite.png' + this.category = 'Record Manager' + this.description = 'Use SQLite to keep track of document writes into the vector databases' + this.baseClasses = [this.type, 'RecordManager', ...getBaseClasses(SQLiteRecordManager)] + this.badge = 'NEW' + this.inputs = [ + { + label: 'Database File Path', + name: 'databaseFilePath', + type: 'string', + placeholder: 'C:\\Users\\User\\.flowise\\database.sqlite' + }, + { + label: 'Additional Connection Configuration', + name: 'additionalConfig', + type: 'json', + additionalParams: true, + optional: true + }, + { + label: 'Table Name', + name: 'tableName', + type: 'string', + placeholder: 'upsertion_records', + additionalParams: true, + optional: true + }, + { + label: 'Namespace', + name: 'namespace', + type: 'string', + description: 'If not specified, chatflowid will be used', + additionalParams: true, + optional: true + }, + { + label: 'Cleanup', + name: 'cleanup', + type: 'options', + description: + 'Read more on the difference between different cleanup methods here', + options: [ + { + label: 'None', + name: 'none', + description: 'No clean up of old content' + }, + { + label: 'Incremental', + name: 'incremental', + description: + 'Delete previous versions of the content if content of the source document has changed. Important!! SourceId Key must be specified and document metadata must contains the specified key' + }, + { + label: 'Full', + name: 'full', + description: + 'Same as incremental, but if the source document has been deleted, it will be deleted from vector store as well, incremental mode will not.' + } + ], + additionalParams: true, + default: 'none' + }, + { + label: 'SourceId Key', + name: 'sourceIdKey', + type: 'string', + description: + 'Key used to get the true source of document, to be compared against the record. Document metadata must contains SourceId Key', + default: 'source', + placeholder: 'source', + additionalParams: true, + optional: true + } + ] + } + + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + const _tableName = nodeData.inputs?.tableName as string + const tableName = _tableName ? _tableName : 'upsertion_records' + const additionalConfig = nodeData.inputs?.additionalConfig as string + const _namespace = nodeData.inputs?.namespace as string + const namespace = _namespace ? _namespace : options.chatflowid + const cleanup = nodeData.inputs?.cleanup as string + const _sourceIdKey = nodeData.inputs?.sourceIdKey as string + const sourceIdKey = _sourceIdKey ? _sourceIdKey : 'source' + const databaseFilePath = nodeData.inputs?.databaseFilePath as string + + let additionalConfiguration = {} + if (additionalConfig) { + try { + additionalConfiguration = typeof additionalConfig === 'object' ? additionalConfig : JSON.parse(additionalConfig) + } catch (exception) { + throw new Error('Invalid JSON in the Additional Configuration: ' + exception) + } + } + + const sqliteOptions = { + ...additionalConfiguration, + type: 'sqlite', + database: path.resolve(databaseFilePath) + } + + const args = { + sqliteOptions, + tableName: tableName + } + + const recordManager = new SQLiteRecordManager(namespace, args) + + ;(recordManager as any).cleanup = cleanup + ;(recordManager as any).sourceIdKey = sourceIdKey + + return recordManager + } +} + +type SQLiteRecordManagerOptions = { + sqliteOptions: any + tableName?: string +} + +class SQLiteRecordManager implements RecordManagerInterface { + lc_namespace = ['langchain', 'recordmanagers', 'sqlite'] + + datasource: DataSource + + queryRunner: QueryRunner + + tableName: string + + namespace: string + + constructor(namespace: string, config: SQLiteRecordManagerOptions) { + const { sqliteOptions, tableName } = config + this.namespace = namespace + this.tableName = tableName || 'upsertion_records' + this.datasource = new DataSource(sqliteOptions) + } + + async createSchema(): Promise { + try { + const appDataSource = await this.datasource.initialize() + + this.queryRunner = appDataSource.createQueryRunner() + + await this.queryRunner.manager.query(` +CREATE TABLE IF NOT EXISTS "${this.tableName}" ( + uuid TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))), + key TEXT NOT NULL, + namespace TEXT NOT NULL, + updated_at REAL NOT NULL, + group_id TEXT, + UNIQUE (key, namespace) +); +CREATE INDEX IF NOT EXISTS updated_at_index ON "${this.tableName}" (updated_at); +CREATE INDEX IF NOT EXISTS key_index ON "${this.tableName}" (key); +CREATE INDEX IF NOT EXISTS namespace_index ON "${this.tableName}" (namespace); +CREATE INDEX IF NOT EXISTS group_id_index ON "${this.tableName}" (group_id);`) + } catch (e: any) { + // This error indicates that the table already exists + // Due to asynchronous nature of the code, it is possible that + // the table is created between the time we check if it exists + // and the time we try to create it. It can be safely ignored. + if ('code' in e && e.code === '23505') { + return + } + throw e + } + } + + async getTime(): Promise { + try { + const res = await this.queryRunner.manager.query(`SELECT strftime('%s', 'now') AS epoch`) + return Number.parseFloat(res[0].epoch) + } catch (error) { + console.error('Error getting time in SQLiteRecordManager:') + throw error + } + } + + async update(keys: string[], updateOptions?: UpdateOptions): Promise { + if (keys.length === 0) { + return + } + + const updatedAt = await this.getTime() + const { timeAtLeast, groupIds: _groupIds } = updateOptions ?? {} + + if (timeAtLeast && updatedAt < timeAtLeast) { + throw new Error(`Time sync issue with database ${updatedAt} < ${timeAtLeast}`) + } + + const groupIds = _groupIds ?? keys.map(() => null) + + if (groupIds.length !== keys.length) { + throw new Error(`Number of keys (${keys.length}) does not match number of group_ids (${groupIds.length})`) + } + + const recordsToUpsert = keys.map((key, i) => [ + key, + this.namespace, + updatedAt, + groupIds[i] ?? null // Ensure groupIds[i] is null if undefined + ]) + + const query = ` + INSERT INTO "${this.tableName}" (key, namespace, updated_at, group_id) + VALUES (?, ?, ?, ?) + ON CONFLICT (key, namespace) DO UPDATE SET updated_at = excluded.updated_at` + + // To handle multiple files upsert + for (const record of recordsToUpsert) { + // Consider using a transaction for batch operations + await this.queryRunner.manager.query(query, record.flat()) + } + } + + async exists(keys: string[]): Promise { + if (keys.length === 0) { + return [] + } + + // Prepare the placeholders and the query + const placeholders = keys.map(() => `?`).join(', ') + const sql = ` + SELECT key + FROM "${this.tableName}" + WHERE namespace = ? AND key IN (${placeholders})` + + // Initialize an array to fill with the existence checks + const existsArray = new Array(keys.length).fill(false) + + try { + // Execute the query + const rows = await this.queryRunner.manager.query(sql, [this.namespace, ...keys.flat()]) + // Create a set of existing keys for faster lookup + const existingKeysSet = new Set(rows.map((row: { key: string }) => row.key)) + // Map the input keys to booleans indicating if they exist + keys.forEach((key, index) => { + existsArray[index] = existingKeysSet.has(key) + }) + return existsArray + } catch (error) { + console.error('Error checking existence of keys') + throw error // Allow the caller to handle the error + } + } + + async listKeys(options?: ListKeyOptions): Promise { + const { before, after, limit, groupIds } = options ?? {} + let query = `SELECT key FROM "${this.tableName}" WHERE namespace = ?` + const values: (string | number | string[])[] = [this.namespace] + + if (before) { + query += ` AND updated_at < ?` + values.push(before) + } + + if (after) { + query += ` AND updated_at > ?` + values.push(after) + } + + if (limit) { + query += ` LIMIT ?` + values.push(limit) + } + + if (groupIds && Array.isArray(groupIds)) { + query += ` AND group_id IN (${groupIds + .filter((gid) => gid !== null) + .map(() => '?') + .join(', ')})` + values.push(...groupIds.filter((gid): gid is string => gid !== null)) + } + + query += ';' + + // Directly using try/catch with async/await for cleaner flow + try { + const result = await this.queryRunner.manager.query(query, values) + return result.map((row: { key: string }) => row.key) + } catch (error) { + console.error('Error listing keys.') + throw error // Re-throw the error to be handled by the caller + } + } + + async deleteKeys(keys: string[]): Promise { + if (keys.length === 0) { + return + } + + const placeholders = keys.map(() => '?').join(', ') + const query = `DELETE FROM "${this.tableName}" WHERE namespace = ? AND key IN (${placeholders});` + const values = [this.namespace, ...keys].map((v) => (typeof v !== 'string' ? `${v}` : v)) + + // Directly using try/catch with async/await for cleaner flow + try { + await this.queryRunner.manager.query(query, values) + } catch (error) { + console.error('Error deleting keys') + throw error // Re-throw the error to be handled by the caller + } + } +} + +module.exports = { nodeClass: SQLiteRecordManager_RecordManager } diff --git a/packages/components/nodes/recordmanager/SQLiteRecordManager/sqlite.png b/packages/components/nodes/recordmanager/SQLiteRecordManager/sqlite.png new file mode 100644 index 0000000000000000000000000000000000000000..9f35f2bdf3c9eb5d8a8fd48dc81627082a1d03fa GIT binary patch literal 50383 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

4hyefmb` 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 Nx{5j`_lnu`{|}An@!kLc literal 0 HcmV?d00001 diff --git a/packages/components/nodes/vectorstores/Astra/Astra.ts b/packages/components/nodes/vectorstores/Astra/Astra.ts index f6a583e0..961722fa 100644 --- a/packages/components/nodes/vectorstores/Astra/Astra.ts +++ b/packages/components/nodes/vectorstores/Astra/Astra.ts @@ -2,7 +2,7 @@ import { flatten } from 'lodash' import { Embeddings } from '@langchain/core/embeddings' import { Document } from '@langchain/core/documents' import { AstraDBVectorStore, AstraLibArgs } from '@langchain/community/vectorstores/astradb' -import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' +import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams, IndexingResult } from '../../../src/Interface' import { getBaseClasses, getCredentialData } from '../../../src/utils' import { addMMRInputParams, resolveVectorStoreOrRetriever } from '../VectorStoreUtils' @@ -101,7 +101,7 @@ class Astra_VectorStores implements INode { //@ts-ignore vectorStoreMethods = { - async upsert(nodeData: INodeData, options: ICommonObject): Promise { + async upsert(nodeData: INodeData, options: ICommonObject): Promise> { const docs = nodeData.inputs?.document as Document[] const embeddings = nodeData.inputs?.embeddings as Embeddings const vectorDimension = nodeData.inputs?.vectorDimension as number @@ -142,6 +142,7 @@ class Astra_VectorStores implements INode { try { await AstraDBVectorStore.fromDocuments(finalDocs, embeddings, astraConfig) + return { numAdded: finalDocs.length, addedDocs: finalDocs } } catch (e) { throw new Error(e) } diff --git a/packages/components/nodes/vectorstores/Chroma/Chroma.ts b/packages/components/nodes/vectorstores/Chroma/Chroma.ts index 70ab2d22..a8722e61 100644 --- a/packages/components/nodes/vectorstores/Chroma/Chroma.ts +++ b/packages/components/nodes/vectorstores/Chroma/Chroma.ts @@ -2,9 +2,10 @@ import { flatten } from 'lodash' import { Chroma } from '@langchain/community/vectorstores/chroma' import { Embeddings } from '@langchain/core/embeddings' import { Document } from '@langchain/core/documents' -import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' +import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams, IndexingResult } from '../../../src/Interface' import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' import { ChromaExtended } from './core' +import { index } from '../../../src/indexing' class Chroma_VectorStores implements INode { label: string @@ -23,7 +24,7 @@ class Chroma_VectorStores implements INode { constructor() { this.label = 'Chroma' this.name = 'chroma' - this.version = 1.0 + this.version = 2.0 this.type = 'Chroma' this.icon = 'chroma.svg' this.category = 'Vector Stores' @@ -51,6 +52,13 @@ class Chroma_VectorStores implements INode { name: 'embeddings', type: 'Embeddings' }, + { + label: 'Record Manager', + name: 'recordManager', + type: 'RecordManager', + description: 'Keep track of the record to prevent duplication', + optional: true + }, { label: 'Collection Name', name: 'collectionName', @@ -95,11 +103,12 @@ class Chroma_VectorStores implements INode { //@ts-ignore vectorStoreMethods = { - async upsert(nodeData: INodeData, options: ICommonObject): Promise { + async upsert(nodeData: INodeData, options: ICommonObject): Promise> { const collectionName = nodeData.inputs?.collectionName as string const docs = nodeData.inputs?.document as Document[] const embeddings = nodeData.inputs?.embeddings as Embeddings const chromaURL = nodeData.inputs?.chromaURL as string + const recordManager = nodeData.inputs?.recordManager const credentialData = await getCredentialData(nodeData.credential ?? '', options) const chromaApiKey = getCredentialParam('chromaApiKey', credentialData, nodeData) @@ -121,7 +130,24 @@ class Chroma_VectorStores implements INode { if (chromaApiKey) obj.chromaApiKey = chromaApiKey try { - await ChromaExtended.fromDocuments(finalDocs, embeddings, obj) + if (recordManager) { + const vectorStore = await ChromaExtended.fromExistingCollection(embeddings, obj) + await recordManager.createSchema() + const res = await index({ + docsSource: finalDocs, + recordManager, + vectorStore, + options: { + cleanup: recordManager?.cleanup, + sourceIdKey: recordManager?.sourceIdKey ?? 'source', + vectorStoreName: collectionName + } + }) + return res + } else { + await ChromaExtended.fromDocuments(finalDocs, embeddings, obj) + return { numAdded: finalDocs.length, addedDocs: finalDocs } + } } catch (e) { throw new Error(e) } diff --git a/packages/components/nodes/vectorstores/Elasticsearch/Elasticsearch.ts b/packages/components/nodes/vectorstores/Elasticsearch/Elasticsearch.ts index cfc721b8..a5069739 100644 --- a/packages/components/nodes/vectorstores/Elasticsearch/Elasticsearch.ts +++ b/packages/components/nodes/vectorstores/Elasticsearch/Elasticsearch.ts @@ -3,8 +3,9 @@ import { Client, ClientOptions } from '@elastic/elasticsearch' import { Document } from '@langchain/core/documents' import { Embeddings } from '@langchain/core/embeddings' import { ElasticClientArgs, ElasticVectorSearch } from '@langchain/community/vectorstores/elasticsearch' -import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' +import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams, IndexingResult } from '../../../src/Interface' import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' +import { index } from '../../../src/indexing' class Elasticsearch_VectorStores implements INode { label: string @@ -23,7 +24,7 @@ class Elasticsearch_VectorStores implements INode { constructor() { this.label = 'Elasticsearch' this.name = 'elasticsearch' - this.version = 1.0 + this.version = 2.0 this.description = 'Upsert embedded data and perform similarity search upon query using Elasticsearch, a distributed search and analytics engine' this.type = 'Elasticsearch' @@ -50,6 +51,13 @@ class Elasticsearch_VectorStores implements INode { name: 'embeddings', type: 'Embeddings' }, + { + label: 'Record Manager', + name: 'recordManager', + type: 'RecordManager', + description: 'Keep track of the record to prevent duplication', + optional: true + }, { label: 'Index Name', name: 'indexName', @@ -105,13 +113,14 @@ class Elasticsearch_VectorStores implements INode { //@ts-ignore vectorStoreMethods = { - async upsert(nodeData: INodeData, options: ICommonObject): Promise { + async upsert(nodeData: INodeData, options: ICommonObject): Promise> { const credentialData = await getCredentialData(nodeData.credential ?? '', options) const endPoint = getCredentialParam('endpoint', credentialData, nodeData) const cloudId = getCredentialParam('cloudId', credentialData, nodeData) const indexName = nodeData.inputs?.indexName as string const embeddings = nodeData.inputs?.embeddings as Embeddings const similarityMeasure = nodeData.inputs?.similarityMeasure as string + const recordManager = nodeData.inputs?.recordManager const docs = nodeData.inputs?.document as Document[] const flattenDocs = docs && docs.length ? flatten(docs) : [] @@ -134,7 +143,24 @@ class Elasticsearch_VectorStores implements INode { const vectorStore = new ElasticVectorSearch(embeddings, elasticSearchClientArgs) try { - await vectorStore.addDocuments(finalDocs) + if (recordManager) { + const vectorStore = await ElasticVectorSearch.fromExistingIndex(embeddings, elasticSearchClientArgs) + await recordManager.createSchema() + const res = await index({ + docsSource: finalDocs, + recordManager, + vectorStore, + options: { + cleanup: recordManager?.cleanup, + sourceIdKey: recordManager?.sourceIdKey ?? 'source', + vectorStoreName: indexName + } + }) + return res + } else { + await vectorStore.addDocuments(finalDocs) + return { numAdded: finalDocs.length, addedDocs: finalDocs } + } } catch (e) { throw new Error(e) } diff --git a/packages/components/nodes/vectorstores/Faiss/Faiss.ts b/packages/components/nodes/vectorstores/Faiss/Faiss.ts index 8a306ed9..774e049e 100644 --- a/packages/components/nodes/vectorstores/Faiss/Faiss.ts +++ b/packages/components/nodes/vectorstores/Faiss/Faiss.ts @@ -2,7 +2,7 @@ import { flatten } from 'lodash' import { Document } from '@langchain/core/documents' import { FaissStore } from '@langchain/community/vectorstores/faiss' import { Embeddings } from '@langchain/core/embeddings' -import { INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' +import { INode, INodeData, INodeOutputsValue, INodeParams, IndexingResult } from '../../../src/Interface' import { getBaseClasses } from '../../../src/utils' class Faiss_VectorStores implements INode { @@ -74,7 +74,7 @@ class Faiss_VectorStores implements INode { //@ts-ignore vectorStoreMethods = { - async upsert(nodeData: INodeData): Promise { + async upsert(nodeData: INodeData): Promise> { const docs = nodeData.inputs?.document as Document[] const embeddings = nodeData.inputs?.embeddings as Embeddings const basePath = nodeData.inputs?.basePath as string @@ -95,6 +95,8 @@ class Faiss_VectorStores implements INode { vectorStore.similaritySearchVectorWithScore = async (query: number[], k: number) => { return await similaritySearchVectorWithScore(query, k, vectorStore) } + + return { numAdded: finalDocs.length, addedDocs: finalDocs } } catch (e) { throw new Error(e) } diff --git a/packages/components/nodes/vectorstores/InMemory/InMemoryVectorStore.ts b/packages/components/nodes/vectorstores/InMemory/InMemoryVectorStore.ts index aff7f637..8c54fe27 100644 --- a/packages/components/nodes/vectorstores/InMemory/InMemoryVectorStore.ts +++ b/packages/components/nodes/vectorstores/InMemory/InMemoryVectorStore.ts @@ -2,7 +2,7 @@ import { flatten } from 'lodash' import { MemoryVectorStore } from 'langchain/vectorstores/memory' import { Embeddings } from '@langchain/core/embeddings' import { Document } from '@langchain/core/documents' -import { INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' +import { INode, INodeData, INodeOutputsValue, INodeParams, IndexingResult } from '../../../src/Interface' import { getBaseClasses } from '../../../src/utils' class InMemoryVectorStore_VectorStores implements INode { @@ -64,7 +64,7 @@ class InMemoryVectorStore_VectorStores implements INode { //@ts-ignore vectorStoreMethods = { - async upsert(nodeData: INodeData): Promise { + async upsert(nodeData: INodeData): Promise> { const docs = nodeData.inputs?.document as Document[] const embeddings = nodeData.inputs?.embeddings as Embeddings @@ -78,6 +78,7 @@ class InMemoryVectorStore_VectorStores implements INode { try { await MemoryVectorStore.fromDocuments(finalDocs, embeddings) + return { numAdded: finalDocs.length, addedDocs: finalDocs } } catch (e) { throw new Error(e) } diff --git a/packages/components/nodes/vectorstores/Milvus/Milvus.ts b/packages/components/nodes/vectorstores/Milvus/Milvus.ts index d6cfaea1..c0818df4 100644 --- a/packages/components/nodes/vectorstores/Milvus/Milvus.ts +++ b/packages/components/nodes/vectorstores/Milvus/Milvus.ts @@ -3,7 +3,7 @@ import { DataType, ErrorCode, MetricType, IndexType } from '@zilliz/milvus2-sdk- import { Document } from '@langchain/core/documents' import { MilvusLibArgs, Milvus } from '@langchain/community/vectorstores/milvus' import { Embeddings } from '@langchain/core/embeddings' -import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' +import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams, IndexingResult } from '../../../src/Interface' import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' interface InsertRow { @@ -109,7 +109,7 @@ class Milvus_VectorStores implements INode { //@ts-ignore vectorStoreMethods = { - async upsert(nodeData: INodeData, options: ICommonObject): Promise { + async upsert(nodeData: INodeData, options: ICommonObject): Promise> { // server setup const address = nodeData.inputs?.milvusServerUrl as string const collectionName = nodeData.inputs?.milvusCollection as string @@ -147,6 +147,8 @@ class Milvus_VectorStores implements INode { vectorStore.similaritySearchVectorWithScore = async (query: number[], k: number, filter?: string) => { return await similaritySearchVectorWithScore(query, k, vectorStore, undefined, filter) } + + return { numAdded: finalDocs.length, addedDocs: finalDocs } } catch (e) { throw new Error(e) } diff --git a/packages/components/nodes/vectorstores/MongoDBAtlas/MongoDBAtlas.ts b/packages/components/nodes/vectorstores/MongoDBAtlas/MongoDBAtlas.ts index e55b84c7..3444d372 100644 --- a/packages/components/nodes/vectorstores/MongoDBAtlas/MongoDBAtlas.ts +++ b/packages/components/nodes/vectorstores/MongoDBAtlas/MongoDBAtlas.ts @@ -3,7 +3,7 @@ import { MongoClient } from 'mongodb' import { MongoDBAtlasVectorSearch } from '@langchain/mongodb' import { Embeddings } from '@langchain/core/embeddings' import { Document } from '@langchain/core/documents' -import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' +import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams, IndexingResult } from '../../../src/Interface' import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' import { addMMRInputParams, resolveVectorStoreOrRetriever } from '../VectorStoreUtils' @@ -113,7 +113,7 @@ class MongoDBAtlas_VectorStores implements INode { //@ts-ignore vectorStoreMethods = { - async upsert(nodeData: INodeData, options: ICommonObject): Promise { + async upsert(nodeData: INodeData, options: ICommonObject): Promise> { const credentialData = await getCredentialData(nodeData.credential ?? '', options) const databaseName = nodeData.inputs?.databaseName as string const collectionName = nodeData.inputs?.collectionName as string @@ -149,6 +149,7 @@ class MongoDBAtlas_VectorStores implements INode { embeddingKey }) await mongoDBAtlasVectorSearch.addDocuments(finalDocs) + return { numAdded: finalDocs.length, addedDocs: finalDocs } } catch (e) { throw new Error(e) } diff --git a/packages/components/nodes/vectorstores/OpenSearch/OpenSearch.ts b/packages/components/nodes/vectorstores/OpenSearch/OpenSearch.ts index b67c8e4f..c60b4de5 100644 --- a/packages/components/nodes/vectorstores/OpenSearch/OpenSearch.ts +++ b/packages/components/nodes/vectorstores/OpenSearch/OpenSearch.ts @@ -3,7 +3,7 @@ import { Client } from '@opensearch-project/opensearch' import { Document } from '@langchain/core/documents' import { OpenSearchVectorStore } from '@langchain/community/vectorstores/opensearch' import { Embeddings } from '@langchain/core/embeddings' -import { INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' +import { INode, INodeData, INodeOutputsValue, INodeParams, IndexingResult } from '../../../src/Interface' import { getBaseClasses } from '../../../src/utils' class OpenSearch_VectorStores implements INode { @@ -79,7 +79,7 @@ class OpenSearch_VectorStores implements INode { //@ts-ignore vectorStoreMethods = { - async upsert(nodeData: INodeData): Promise { + async upsert(nodeData: INodeData): Promise> { const docs = nodeData.inputs?.document as Document[] const embeddings = nodeData.inputs?.embeddings as Embeddings const opensearchURL = nodeData.inputs?.opensearchURL as string @@ -102,6 +102,7 @@ class OpenSearch_VectorStores implements INode { client, indexName: indexName }) + return { numAdded: finalDocs.length, addedDocs: finalDocs } } catch (e) { throw new Error(e) } diff --git a/packages/components/nodes/vectorstores/Pinecone/Pinecone.ts b/packages/components/nodes/vectorstores/Pinecone/Pinecone.ts index 3ef8a8e8..5c4fd084 100644 --- a/packages/components/nodes/vectorstores/Pinecone/Pinecone.ts +++ b/packages/components/nodes/vectorstores/Pinecone/Pinecone.ts @@ -3,9 +3,10 @@ import { Pinecone } from '@pinecone-database/pinecone' import { PineconeStoreParams, PineconeStore } from '@langchain/pinecone' import { Embeddings } from '@langchain/core/embeddings' import { Document } from '@langchain/core/documents' -import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' +import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams, IndexingResult } from '../../../src/Interface' import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' import { addMMRInputParams, resolveVectorStoreOrRetriever } from '../VectorStoreUtils' +import { index } from '../../../src/indexing' class Pinecone_VectorStores implements INode { label: string @@ -24,7 +25,7 @@ class Pinecone_VectorStores implements INode { constructor() { this.label = 'Pinecone' this.name = 'pinecone' - this.version = 2.0 + this.version = 3.0 this.type = 'Pinecone' this.icon = 'pinecone.svg' this.category = 'Vector Stores' @@ -50,6 +51,13 @@ class Pinecone_VectorStores implements INode { name: 'embeddings', type: 'Embeddings' }, + { + label: 'Record Manager', + name: 'recordManager', + type: 'RecordManager', + description: 'Keep track of the record to prevent duplication', + optional: true + }, { label: 'Pinecone Index', name: 'pineconeIndex', @@ -97,11 +105,12 @@ class Pinecone_VectorStores implements INode { //@ts-ignore vectorStoreMethods = { - async upsert(nodeData: INodeData, options: ICommonObject): Promise { - const index = nodeData.inputs?.pineconeIndex as string + async upsert(nodeData: INodeData, options: ICommonObject): Promise> { + const _index = nodeData.inputs?.pineconeIndex as string const pineconeNamespace = nodeData.inputs?.pineconeNamespace as string const docs = nodeData.inputs?.document as Document[] const embeddings = nodeData.inputs?.embeddings as Embeddings + const recordManager = nodeData.inputs?.recordManager const credentialData = await getCredentialData(nodeData.credential ?? '', options) const pineconeApiKey = getCredentialParam('pineconeApiKey', credentialData, nodeData) @@ -110,7 +119,7 @@ class Pinecone_VectorStores implements INode { apiKey: pineconeApiKey }) - const pineconeIndex = client.Index(index) + const pineconeIndex = client.Index(_index) const flattenDocs = docs && docs.length ? flatten(docs) : [] const finalDocs = [] @@ -127,7 +136,25 @@ class Pinecone_VectorStores implements INode { if (pineconeNamespace) obj.namespace = pineconeNamespace try { - await PineconeStore.fromDocuments(finalDocs, embeddings, obj) + if (recordManager) { + const vectorStore = await PineconeStore.fromExistingIndex(embeddings, obj) + await recordManager.createSchema() + const res = await index({ + docsSource: finalDocs, + recordManager, + vectorStore, + options: { + cleanup: recordManager?.cleanup, + sourceIdKey: recordManager?.sourceIdKey ?? 'source', + vectorStoreName: pineconeNamespace + } + }) + + return res + } else { + await PineconeStore.fromDocuments(finalDocs, embeddings, obj) + return { numAdded: finalDocs.length, addedDocs: finalDocs } + } } catch (e) { throw new Error(e) } diff --git a/packages/components/nodes/vectorstores/Pinecone/Pinecone_LlamaIndex.ts b/packages/components/nodes/vectorstores/Pinecone/Pinecone_LlamaIndex.ts index c0b2e5c1..4045012e 100644 --- a/packages/components/nodes/vectorstores/Pinecone/Pinecone_LlamaIndex.ts +++ b/packages/components/nodes/vectorstores/Pinecone/Pinecone_LlamaIndex.ts @@ -13,7 +13,7 @@ import { import { FetchResponse, Index, Pinecone, ScoredPineconeRecord } from '@pinecone-database/pinecone' import { flatten } from 'lodash' import { Document as LCDocument } from 'langchain/document' -import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' +import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams, IndexingResult } from '../../../src/Interface' import { flattenObject, getCredentialData, getCredentialParam } from '../../../src/utils' class PineconeLlamaIndex_VectorStores implements INode { @@ -110,7 +110,7 @@ class PineconeLlamaIndex_VectorStores implements INode { //@ts-ignore vectorStoreMethods = { - async upsert(nodeData: INodeData, options: ICommonObject): Promise { + async upsert(nodeData: INodeData, options: ICommonObject): Promise> { const indexName = nodeData.inputs?.pineconeIndex as string const pineconeNamespace = nodeData.inputs?.pineconeNamespace as string const docs = nodeData.inputs?.document as LCDocument[] @@ -144,6 +144,7 @@ class PineconeLlamaIndex_VectorStores implements INode { try { await VectorStoreIndex.fromDocuments(llamadocs, { serviceContext, storageContext }) + return { numAdded: finalDocs.length, addedDocs: finalDocs } } catch (e) { throw new Error(e) } diff --git a/packages/components/nodes/vectorstores/Postgres/Postgres.ts b/packages/components/nodes/vectorstores/Postgres/Postgres.ts index 67a12a14..090b550d 100644 --- a/packages/components/nodes/vectorstores/Postgres/Postgres.ts +++ b/packages/components/nodes/vectorstores/Postgres/Postgres.ts @@ -4,8 +4,9 @@ import { DataSourceOptions } from 'typeorm' import { Embeddings } from '@langchain/core/embeddings' import { Document } from '@langchain/core/documents' import { TypeORMVectorStore, TypeORMVectorStoreDocument } from '@langchain/community/vectorstores/typeorm' -import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' +import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams, IndexingResult } from '../../../src/Interface' import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' +import { index } from '../../../src/indexing' class Postgres_VectorStores implements INode { label: string @@ -24,7 +25,7 @@ class Postgres_VectorStores implements INode { constructor() { this.label = 'Postgres' this.name = 'postgres' - this.version = 3.0 + this.version = 4.0 this.type = 'Postgres' this.icon = 'postgres.svg' this.category = 'Vector Stores' @@ -50,6 +51,13 @@ class Postgres_VectorStores implements INode { name: 'embeddings', type: 'Embeddings' }, + { + label: 'Record Manager', + name: 'recordManager', + type: 'RecordManager', + description: 'Keep track of the record to prevent duplication', + optional: true + }, { label: 'Host', name: 'host', @@ -108,7 +116,7 @@ class Postgres_VectorStores implements INode { //@ts-ignore vectorStoreMethods = { - async upsert(nodeData: INodeData, options: ICommonObject): Promise { + async upsert(nodeData: INodeData, options: ICommonObject): Promise> { const credentialData = await getCredentialData(nodeData.credential ?? '', options) const user = getCredentialParam('user', credentialData, nodeData) const password = getCredentialParam('password', credentialData, nodeData) @@ -117,6 +125,7 @@ class Postgres_VectorStores implements INode { const docs = nodeData.inputs?.document as Document[] const embeddings = nodeData.inputs?.embeddings as Embeddings const additionalConfig = nodeData.inputs?.additionalConfig as string + const recordManager = nodeData.inputs?.recordManager let additionalConfiguration = {} if (additionalConfig) { @@ -151,11 +160,37 @@ class Postgres_VectorStores implements INode { } try { - const vectorStore = await TypeORMVectorStore.fromDocuments(finalDocs, embeddings, args) + if (recordManager) { + const vectorStore = await TypeORMVectorStore.fromDataSource(embeddings, args) - // Avoid Illegal invocation error - vectorStore.similaritySearchVectorWithScore = async (query: number[], k: number, filter?: any) => { - return await similaritySearchVectorWithScore(query, k, tableName, postgresConnectionOptions, filter) + // Avoid Illegal invocation error + vectorStore.similaritySearchVectorWithScore = async (query: number[], k: number, filter?: any) => { + return await similaritySearchVectorWithScore(query, k, tableName, postgresConnectionOptions, filter) + } + + await recordManager.createSchema() + + const res = await index({ + docsSource: finalDocs, + recordManager, + vectorStore, + options: { + cleanup: recordManager?.cleanup, + sourceIdKey: recordManager?.sourceIdKey ?? 'source', + vectorStoreName: tableName + } + }) + + return res + } else { + const vectorStore = await TypeORMVectorStore.fromDocuments(finalDocs, embeddings, args) + + // Avoid Illegal invocation error + vectorStore.similaritySearchVectorWithScore = async (query: number[], k: number, filter?: any) => { + return await similaritySearchVectorWithScore(query, k, tableName, postgresConnectionOptions, filter) + } + + return { numAdded: finalDocs.length, addedDocs: finalDocs } } } catch (e) { throw new Error(e) diff --git a/packages/components/nodes/vectorstores/Qdrant/Qdrant.ts b/packages/components/nodes/vectorstores/Qdrant/Qdrant.ts index 7fd2559f..c9e77507 100644 --- a/packages/components/nodes/vectorstores/Qdrant/Qdrant.ts +++ b/packages/components/nodes/vectorstores/Qdrant/Qdrant.ts @@ -1,13 +1,19 @@ import { flatten } from 'lodash' +import { v4 as uuid } from 'uuid' import { QdrantClient } from '@qdrant/js-client-rest' import { VectorStoreRetrieverInput } from '@langchain/core/vectorstores' import { Document } from '@langchain/core/documents' import { QdrantVectorStore, QdrantLibArgs } from '@langchain/community/vectorstores/qdrant' import { Embeddings } from '@langchain/core/embeddings' -import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' +import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams, IndexingResult } from '../../../src/Interface' import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' +import { index } from '../../../src/indexing' type RetrieverConfig = Partial> +type QdrantAddDocumentOptions = { + customPayload?: Record[] + ids?: string[] +} class Qdrant_VectorStores implements INode { label: string @@ -26,7 +32,7 @@ class Qdrant_VectorStores implements INode { constructor() { this.label = 'Qdrant' this.name = 'qdrant' - this.version = 1.0 + this.version = 2.0 this.type = 'Qdrant' this.icon = 'qdrant.png' this.category = 'Vector Stores' @@ -55,6 +61,13 @@ class Qdrant_VectorStores implements INode { name: 'embeddings', type: 'Embeddings' }, + { + label: 'Record Manager', + name: 'recordManager', + type: 'RecordManager', + description: 'Keep track of the record to prevent duplication', + optional: true + }, { label: 'Qdrant Server URL', name: 'qdrantServerUrl', @@ -138,13 +151,14 @@ class Qdrant_VectorStores implements INode { //@ts-ignore vectorStoreMethods = { - async upsert(nodeData: INodeData, options: ICommonObject): Promise { + async upsert(nodeData: INodeData, options: ICommonObject): Promise> { const qdrantServerUrl = nodeData.inputs?.qdrantServerUrl as string const collectionName = nodeData.inputs?.qdrantCollection as string const docs = nodeData.inputs?.document as Document[] const embeddings = nodeData.inputs?.embeddings as Embeddings const qdrantSimilarity = nodeData.inputs?.qdrantSimilarity const qdrantVectorDimension = nodeData.inputs?.qdrantVectorDimension + const recordManager = nodeData.inputs?.recordManager const credentialData = await getCredentialData(nodeData.credential ?? '', options) const qdrantApiKey = getCredentialParam('qdrantApiKey', credentialData, nodeData) @@ -178,7 +192,74 @@ class Qdrant_VectorStores implements INode { } try { - await QdrantVectorStore.fromDocuments(finalDocs, embeddings, dbConfig) + if (recordManager) { + const vectorStore = new QdrantVectorStore(embeddings, dbConfig) + await vectorStore.ensureCollection() + + vectorStore.addVectors = async ( + vectors: number[][], + documents: Document[], + documentOptions?: QdrantAddDocumentOptions + ): Promise => { + if (vectors.length === 0) { + return + } + + await vectorStore.ensureCollection() + + const points = vectors.map((embedding, idx) => ({ + id: documentOptions?.ids?.length ? documentOptions?.ids[idx] : uuid(), + vector: embedding, + payload: { + content: documents[idx].pageContent, + metadata: documents[idx].metadata, + customPayload: documentOptions?.customPayload?.length ? documentOptions?.customPayload[idx] : undefined + } + })) + + try { + await client.upsert(collectionName, { + wait: true, + points + }) + } catch (e: any) { + const error = new Error(`${e?.status ?? 'Undefined error code'} ${e?.message}: ${e?.data?.status?.error}`) + throw error + } + } + + vectorStore.delete = async (params: { ids: string[] }): Promise => { + const { ids } = params + + if (ids?.length) { + try { + client.delete(collectionName, { + points: ids + }) + } catch (e) { + console.error('Failed to delete') + } + } + } + + await recordManager.createSchema() + + const res = await index({ + docsSource: finalDocs, + recordManager, + vectorStore, + options: { + cleanup: recordManager?.cleanup, + sourceIdKey: recordManager?.sourceIdKey ?? 'source', + vectorStoreName: collectionName + } + }) + + return res + } else { + await QdrantVectorStore.fromDocuments(finalDocs, embeddings, dbConfig) + return { numAdded: finalDocs.length, addedDocs: finalDocs } + } } catch (e) { throw new Error(e) } diff --git a/packages/components/nodes/vectorstores/Redis/Redis.ts b/packages/components/nodes/vectorstores/Redis/Redis.ts index c1b9c7d4..562e928b 100644 --- a/packages/components/nodes/vectorstores/Redis/Redis.ts +++ b/packages/components/nodes/vectorstores/Redis/Redis.ts @@ -3,7 +3,7 @@ import { createClient, SearchOptions, RedisClientOptions } from 'redis' import { Embeddings } from '@langchain/core/embeddings' import { RedisVectorStore, RedisVectorStoreConfig } from '@langchain/community/vectorstores/redis' import { Document } from '@langchain/core/documents' -import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' +import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams, IndexingResult } from '../../../src/Interface' import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' import { escapeAllStrings, escapeSpecialChars, unEscapeSpecialChars } from './utils' @@ -138,7 +138,7 @@ class Redis_VectorStores implements INode { //@ts-ignore vectorStoreMethods = { - async upsert(nodeData: INodeData, options: ICommonObject): Promise { + async upsert(nodeData: INodeData, options: ICommonObject): Promise> { const credentialData = await getCredentialData(nodeData.credential ?? '', options) const indexName = nodeData.inputs?.indexName as string let contentKey = nodeData.inputs?.contentKey as string @@ -203,6 +203,8 @@ class Redis_VectorStores implements INode { filter ) } + + return { numAdded: finalDocs.length, addedDocs: finalDocs } } catch (e) { throw new Error(e) } diff --git a/packages/components/nodes/vectorstores/SimpleStore/SimpleStore.ts b/packages/components/nodes/vectorstores/SimpleStore/SimpleStore.ts index 36c383e9..09ec2f24 100644 --- a/packages/components/nodes/vectorstores/SimpleStore/SimpleStore.ts +++ b/packages/components/nodes/vectorstores/SimpleStore/SimpleStore.ts @@ -2,7 +2,7 @@ import path from 'path' import { flatten } from 'lodash' import { storageContextFromDefaults, serviceContextFromDefaults, VectorStoreIndex, Document } from 'llamaindex' import { Document as LCDocument } from 'langchain/document' -import { INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' +import { INode, INodeData, INodeOutputsValue, INodeParams, IndexingResult } from '../../../src/Interface' import { getUserHome } from '../../../src' class SimpleStoreUpsert_LlamaIndex_VectorStores implements INode { @@ -79,7 +79,7 @@ class SimpleStoreUpsert_LlamaIndex_VectorStores implements INode { //@ts-ignore vectorStoreMethods = { - async upsert(nodeData: INodeData): Promise { + async upsert(nodeData: INodeData): Promise> { const basePath = nodeData.inputs?.basePath as string const docs = nodeData.inputs?.document as LCDocument[] const embeddings = nodeData.inputs?.embeddings @@ -105,6 +105,7 @@ class SimpleStoreUpsert_LlamaIndex_VectorStores implements INode { try { await VectorStoreIndex.fromDocuments(llamadocs, { serviceContext, storageContext }) + return { numAdded: finalDocs.length, addedDocs: finalDocs } } catch (e) { throw new Error(e) } diff --git a/packages/components/nodes/vectorstores/Singlestore/Singlestore.ts b/packages/components/nodes/vectorstores/Singlestore/Singlestore.ts index 797b7467..6fb55ae7 100644 --- a/packages/components/nodes/vectorstores/Singlestore/Singlestore.ts +++ b/packages/components/nodes/vectorstores/Singlestore/Singlestore.ts @@ -2,7 +2,7 @@ import { flatten } from 'lodash' import { Embeddings } from '@langchain/core/embeddings' import { SingleStoreVectorStore, SingleStoreVectorStoreConfig } from '@langchain/community/vectorstores/singlestore' import { Document } from '@langchain/core/documents' -import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' +import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams, IndexingResult } from '../../../src/Interface' import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' class SingleStore_VectorStores implements INode { @@ -118,7 +118,7 @@ class SingleStore_VectorStores implements INode { //@ts-ignore vectorStoreMethods = { - async upsert(nodeData: INodeData, options: ICommonObject): Promise { + async upsert(nodeData: INodeData, options: ICommonObject): Promise> { const credentialData = await getCredentialData(nodeData.credential ?? '', options) const user = getCredentialParam('user', credentialData, nodeData) const password = getCredentialParam('password', credentialData, nodeData) @@ -151,6 +151,7 @@ class SingleStore_VectorStores implements INode { try { const vectorStore = new SingleStoreVectorStore(embeddings, singleStoreConnectionConfig) vectorStore.addDocuments.bind(vectorStore)(finalDocs) + return { numAdded: finalDocs.length, addedDocs: finalDocs } } catch (e) { throw new Error(e) } diff --git a/packages/components/nodes/vectorstores/Supabase/Supabase.ts b/packages/components/nodes/vectorstores/Supabase/Supabase.ts index 74849911..fd24f373 100644 --- a/packages/components/nodes/vectorstores/Supabase/Supabase.ts +++ b/packages/components/nodes/vectorstores/Supabase/Supabase.ts @@ -3,9 +3,10 @@ import { createClient } from '@supabase/supabase-js' import { Document } from '@langchain/core/documents' import { Embeddings } from '@langchain/core/embeddings' import { SupabaseVectorStore, SupabaseLibArgs } from '@langchain/community/vectorstores/supabase' -import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' +import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams, IndexingResult } from '../../../src/Interface' import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' import { addMMRInputParams, resolveVectorStoreOrRetriever } from '../VectorStoreUtils' +import { index } from '../../../src/indexing' class Supabase_VectorStores implements INode { label: string @@ -24,7 +25,7 @@ class Supabase_VectorStores implements INode { constructor() { this.label = 'Supabase' this.name = 'supabase' - this.version = 2.0 + this.version = 3.0 this.type = 'Supabase' this.icon = 'supabase.svg' this.category = 'Vector Stores' @@ -50,6 +51,13 @@ class Supabase_VectorStores implements INode { name: 'embeddings', type: 'Embeddings' }, + { + label: 'Record Manager', + name: 'recordManager', + type: 'RecordManager', + description: 'Keep track of the record to prevent duplication', + optional: true + }, { label: 'Supabase Project URL', name: 'supabaseProjUrl', @@ -99,12 +107,13 @@ class Supabase_VectorStores implements INode { //@ts-ignore vectorStoreMethods = { - async upsert(nodeData: INodeData, options: ICommonObject): Promise { + async upsert(nodeData: INodeData, options: ICommonObject): Promise> { const supabaseProjUrl = nodeData.inputs?.supabaseProjUrl as string const tableName = nodeData.inputs?.tableName as string const queryName = nodeData.inputs?.queryName as string const docs = nodeData.inputs?.document as Document[] const embeddings = nodeData.inputs?.embeddings as Embeddings + const recordManager = nodeData.inputs?.recordManager const credentialData = await getCredentialData(nodeData.credential ?? '', options) const supabaseApiKey = getCredentialParam('supabaseApiKey', credentialData, nodeData) @@ -120,11 +129,32 @@ class Supabase_VectorStores implements INode { } try { - await SupabaseVectorStore.fromDocuments(finalDocs, embeddings, { - client, - tableName: tableName, - queryName: queryName - }) + if (recordManager) { + const vectorStore = await SupabaseVectorStore.fromExistingIndex(embeddings, { + client, + tableName: tableName, + queryName: queryName + }) + await recordManager.createSchema() + const res = await index({ + docsSource: finalDocs, + recordManager, + vectorStore, + options: { + cleanup: recordManager?.cleanup, + sourceIdKey: recordManager?.sourceIdKey ?? 'source', + vectorStoreName: tableName + '_' + queryName + } + }) + return res + } else { + await SupabaseVectorStore.fromDocuments(finalDocs, embeddings, { + client, + tableName: tableName, + queryName: queryName + }) + return { numAdded: finalDocs.length, addedDocs: finalDocs } + } } catch (e) { throw new Error(e) } diff --git a/packages/components/nodes/vectorstores/Vectara/Vectara.ts b/packages/components/nodes/vectorstores/Vectara/Vectara.ts index a661b18f..db62e85c 100644 --- a/packages/components/nodes/vectorstores/Vectara/Vectara.ts +++ b/packages/components/nodes/vectorstores/Vectara/Vectara.ts @@ -9,7 +9,7 @@ import { } from '@langchain/community/vectorstores/vectara' import { Document } from '@langchain/core/documents' import { Embeddings } from '@langchain/core/embeddings' -import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' +import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams, IndexingResult } from '../../../src/Interface' import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' class Vectara_VectorStores implements INode { @@ -144,7 +144,7 @@ class Vectara_VectorStores implements INode { //@ts-ignore vectorStoreMethods = { - async upsert(nodeData: INodeData, options: ICommonObject): Promise { + async upsert(nodeData: INodeData, options: ICommonObject): Promise> { const credentialData = await getCredentialData(nodeData.credential ?? '', options) const apiKey = getCredentialParam('apiKey', credentialData, nodeData) const customerId = getCredentialParam('customerID', credentialData, nodeData) @@ -204,6 +204,7 @@ class Vectara_VectorStores implements INode { const vectorStore = new VectaraStore(vectaraArgs) await vectorStore.addFiles(vectaraFiles) } + return { numAdded: finalDocs.length, addedDocs: finalDocs } } catch (e) { throw new Error(e) } diff --git a/packages/components/nodes/vectorstores/Weaviate/Weaviate.ts b/packages/components/nodes/vectorstores/Weaviate/Weaviate.ts index e7cd074a..139a02de 100644 --- a/packages/components/nodes/vectorstores/Weaviate/Weaviate.ts +++ b/packages/components/nodes/vectorstores/Weaviate/Weaviate.ts @@ -1,11 +1,12 @@ import { flatten } from 'lodash' import weaviate, { WeaviateClient, ApiKey } from 'weaviate-ts-client' -import { WeaviateLibArgs, WeaviateStore } from '@langchain/community/vectorstores/weaviate' +import { WeaviateLibArgs, WeaviateStore } from '@langchain/weaviate' import { Document } from '@langchain/core/documents' import { Embeddings } from '@langchain/core/embeddings' -import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' +import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams, IndexingResult } from '../../../src/Interface' import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' import { addMMRInputParams, resolveVectorStoreOrRetriever } from '../VectorStoreUtils' +import { index } from '../../../src/indexing' class Weaviate_VectorStores implements INode { label: string @@ -24,7 +25,7 @@ class Weaviate_VectorStores implements INode { constructor() { this.label = 'Weaviate' this.name = 'weaviate' - this.version = 2.0 + this.version = 3.0 this.type = 'Weaviate' this.icon = 'weaviate.png' this.category = 'Vector Stores' @@ -53,6 +54,13 @@ class Weaviate_VectorStores implements INode { name: 'embeddings', type: 'Embeddings' }, + { + label: 'Record Manager', + name: 'recordManager', + type: 'RecordManager', + description: 'Keep track of the record to prevent duplication', + optional: true + }, { label: 'Weaviate Scheme', name: 'weaviateScheme', @@ -125,7 +133,7 @@ class Weaviate_VectorStores implements INode { //@ts-ignore vectorStoreMethods = { - async upsert(nodeData: INodeData, options: ICommonObject): Promise { + async upsert(nodeData: INodeData, options: ICommonObject): Promise> { const weaviateScheme = nodeData.inputs?.weaviateScheme as string const weaviateHost = nodeData.inputs?.weaviateHost as string const weaviateIndex = nodeData.inputs?.weaviateIndex as string @@ -133,6 +141,7 @@ class Weaviate_VectorStores implements INode { const weaviateMetadataKeys = nodeData.inputs?.weaviateMetadataKeys as string const docs = nodeData.inputs?.document as Document[] const embeddings = nodeData.inputs?.embeddings as Embeddings + const recordManager = nodeData.inputs?.recordManager const credentialData = await getCredentialData(nodeData.credential ?? '', options) const weaviateApiKey = getCredentialParam('weaviateApiKey', credentialData, nodeData) @@ -154,6 +163,7 @@ class Weaviate_VectorStores implements INode { } const obj: WeaviateLibArgs = { + //@ts-ignore client, indexName: weaviateIndex } @@ -162,7 +172,24 @@ class Weaviate_VectorStores implements INode { if (weaviateMetadataKeys) obj.metadataKeys = JSON.parse(weaviateMetadataKeys.replace(/\s/g, '')) try { - await WeaviateStore.fromDocuments(finalDocs, embeddings, obj) + if (recordManager) { + const vectorStore = await WeaviateStore.fromExistingIndex(embeddings, obj) + await recordManager.createSchema() + const res = await index({ + docsSource: finalDocs, + recordManager, + vectorStore, + options: { + cleanup: recordManager?.cleanup, + sourceIdKey: recordManager?.sourceIdKey ?? 'source', + vectorStoreName: weaviateTextKey ? weaviateIndex + '_' + weaviateTextKey : weaviateIndex + } + }) + return res + } else { + await WeaviateStore.fromDocuments(finalDocs, embeddings, obj) + return { numAdded: finalDocs.length, addedDocs: finalDocs } + } } catch (e) { throw new Error(e) } @@ -189,6 +216,7 @@ class Weaviate_VectorStores implements INode { const client: WeaviateClient = weaviate.client(clientConfig) const obj: WeaviateLibArgs = { + //@ts-ignore client, indexName: weaviateIndex } diff --git a/packages/components/nodes/vectorstores/Zep/Zep.ts b/packages/components/nodes/vectorstores/Zep/Zep.ts index c8a78eac..0cddf4d1 100644 --- a/packages/components/nodes/vectorstores/Zep/Zep.ts +++ b/packages/components/nodes/vectorstores/Zep/Zep.ts @@ -3,7 +3,7 @@ import { IDocument, ZepClient } from '@getzep/zep-js' import { ZepVectorStore, IZepConfig } from '@langchain/community/vectorstores/zep' import { Embeddings } from '@langchain/core/embeddings' import { Document } from '@langchain/core/documents' -import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' +import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams, IndexingResult } from '../../../src/Interface' import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' import { addMMRInputParams, resolveVectorStoreOrRetriever } from '../VectorStoreUtils' @@ -106,7 +106,7 @@ class Zep_VectorStores implements INode { //@ts-ignore vectorStoreMethods = { - async upsert(nodeData: INodeData, options: ICommonObject): Promise { + async upsert(nodeData: INodeData, options: ICommonObject): Promise> { const baseURL = nodeData.inputs?.baseURL as string const zepCollection = nodeData.inputs?.zepCollection as string const dimension = (nodeData.inputs?.dimension as number) ?? 1536 @@ -134,6 +134,7 @@ class Zep_VectorStores implements INode { try { await ZepVectorStore.fromDocuments(finalDocs, embeddings, zepConfig) + return { numAdded: finalDocs.length, addedDocs: finalDocs } } catch (e) { throw new Error(e) } diff --git a/packages/components/nodes/vectorstores/ZepCloud/ZepCloud.ts b/packages/components/nodes/vectorstores/ZepCloud/ZepCloud.ts index 49da7c0d..85c32ca7 100644 --- a/packages/components/nodes/vectorstores/ZepCloud/ZepCloud.ts +++ b/packages/components/nodes/vectorstores/ZepCloud/ZepCloud.ts @@ -3,7 +3,7 @@ import { IDocument, ZepClient } from '@getzep/zep-cloud' import { IZepConfig, ZepVectorStore } from '@getzep/zep-cloud/langchain' import { Embeddings } from 'langchain/embeddings/base' import { Document } from 'langchain/document' -import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' +import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams, IndexingResult } from '../../../src/Interface' import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' import { addMMRInputParams, resolveVectorStoreOrRetriever } from '../VectorStoreUtils' import { FakeEmbeddings } from 'langchain/embeddings/fake' @@ -89,7 +89,7 @@ class Zep_CloudVectorStores implements INode { //@ts-ignore vectorStoreMethods = { - async upsert(nodeData: INodeData, options: ICommonObject): Promise { + async upsert(nodeData: INodeData, options: ICommonObject): Promise> { const zepCollection = nodeData.inputs?.zepCollection as string const docs = nodeData.inputs?.document as Document[] const credentialData = await getCredentialData(nodeData.credential ?? '', options) @@ -109,6 +109,7 @@ class Zep_CloudVectorStores implements INode { } try { await ZepVectorStore.fromDocuments(finalDocs, new FakeEmbeddings(), zepConfig) + return { numAdded: finalDocs.length, addedDocs: finalDocs } } catch (e) { throw new Error(e) } diff --git a/packages/components/package.json b/packages/components/package.json index e47377a7..7e4f4a89 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -42,6 +42,7 @@ "@langchain/mongodb": "^0.0.1", "@langchain/openai": "^0.0.14", "@langchain/pinecone": "^0.0.3", + "@langchain/weaviate": "^0.0.1", "@mistralai/mistralai": "0.1.3", "@notionhq/client": "^2.2.8", "@opensearch-project/opensearch": "^1.2.0", diff --git a/packages/components/src/Interface.ts b/packages/components/src/Interface.ts index 725f64c6..33f50d4f 100644 --- a/packages/components/src/Interface.ts +++ b/packages/components/src/Interface.ts @@ -113,7 +113,7 @@ export interface INode extends INodeProperties { [key: string]: (nodeData: INodeData, options?: ICommonObject) => Promise } vectorStoreMethods?: { - upsert: (nodeData: INodeData, options?: ICommonObject) => Promise + upsert: (nodeData: INodeData, options?: ICommonObject) => Promise search: (nodeData: INodeData, options?: ICommonObject) => Promise delete: (nodeData: INodeData, options?: ICommonObject) => Promise } @@ -181,6 +181,7 @@ export type MessageContentImageUrl = { import { PromptTemplate as LangchainPromptTemplate, PromptTemplateInput } from '@langchain/core/prompts' import { VectorStore } from '@langchain/core/vectorstores' +import { Document } from '@langchain/core/documents' export class PromptTemplate extends LangchainPromptTemplate { promptValues: ICommonObject @@ -271,6 +272,15 @@ export abstract class FlowiseSummaryMemory extends ConversationSummaryMemory imp abstract clearChatMessages(overrideSessionId?: string): Promise } +export type IndexingResult = { + numAdded: number + numDeleted: number + numUpdated: number + numSkipped: number + totalKeys: number + addedDocs: Document[] +} + export interface IVisionChatModal { id: string configuredModel: string diff --git a/packages/components/src/indexing.ts b/packages/components/src/indexing.ts new file mode 100644 index 00000000..f9cacfc5 --- /dev/null +++ b/packages/components/src/indexing.ts @@ -0,0 +1,355 @@ +import { VectorStore } from '@langchain/core/vectorstores' +import { v5 as uuidv5 } from 'uuid' +import { RecordManagerInterface, UUIDV5_NAMESPACE } from '@langchain/community/indexes/base' +import { insecureHash } from '@langchain/core/utils/hash' +import { Document, DocumentInterface } from '@langchain/core/documents' +import { BaseDocumentLoader } from 'langchain/document_loaders/base.js' +import { IndexingResult } from './Interface' + +type Metadata = Record + +type StringOrDocFunc = string | ((doc: DocumentInterface) => string) + +export interface HashedDocumentInterface extends DocumentInterface { + uid: string + hash_?: string + contentHash?: string + metadataHash?: string + pageContent: string + metadata: Metadata + calculateHashes(): void + toDocument(): DocumentInterface +} + +interface HashedDocumentArgs { + pageContent: string + metadata: Metadata + uid: string +} + +/** + * HashedDocument is a Document with hashes calculated. + * Hashes are calculated based on page content and metadata. + * It is used for indexing. + */ +export class _HashedDocument implements HashedDocumentInterface { + uid: string + + hash_?: string + + contentHash?: string + + metadataHash?: string + + pageContent: string + + metadata: Metadata + + constructor(fields: HashedDocumentArgs) { + this.uid = fields.uid + this.pageContent = fields.pageContent + this.metadata = fields.metadata + } + + calculateHashes(): void { + const forbiddenKeys = ['hash_', 'content_hash', 'metadata_hash'] + + for (const key of forbiddenKeys) { + if (key in this.metadata) { + throw new Error( + `Metadata cannot contain key ${key} as it is reserved for internal use. Restricted keys: [${forbiddenKeys.join(', ')}]` + ) + } + } + + const contentHash = this._hashStringToUUID(this.pageContent) + + try { + const metadataHash = this._hashNestedDictToUUID(this.metadata) + this.contentHash = contentHash + this.metadataHash = metadataHash + } catch (e) { + throw new Error(`Failed to hash metadata: ${e}. Please use a dict that can be serialized using json.`) + } + + this.hash_ = this._hashStringToUUID(this.contentHash + this.metadataHash) + + if (!this.uid) { + this.uid = this.hash_ + } + } + + toDocument(): DocumentInterface { + return new Document({ + pageContent: this.pageContent, + metadata: this.metadata + }) + } + + static fromDocument(document: DocumentInterface, uid?: string): _HashedDocument { + const doc = new this({ + pageContent: document.pageContent, + metadata: document.metadata, + uid: uid || (document as DocumentInterface & { uid: string }).uid + }) + doc.calculateHashes() + return doc + } + + private _hashStringToUUID(inputString: string): string { + const hash_value = insecureHash(inputString) + return uuidv5(hash_value, UUIDV5_NAMESPACE) + } + + private _hashNestedDictToUUID(data: Record): string { + const serialized_data = JSON.stringify(data, Object.keys(data).sort()) + const hash_value = insecureHash(serialized_data) + return uuidv5(hash_value, UUIDV5_NAMESPACE) + } +} + +export type CleanupMode = 'full' | 'incremental' + +export type IndexOptions = { + /** + * The number of documents to index in one batch. + */ + batchSize?: number + /** + * The cleanup mode to use. Can be "full", "incremental" or undefined. + * - **Incremental**: Cleans up all documents that haven't been updated AND + * that are associated with source ids that were seen + * during indexing. + * Clean up is done continuously during indexing helping + * to minimize the probability of users seeing duplicated + * content. + * - **Full**: Delete all documents that haven to been returned by the loader. + * Clean up runs after all documents have been indexed. + * This means that users may see duplicated content during indexing. + * - **undefined**: Do not delete any documents. + */ + cleanup?: CleanupMode + /** + * Optional key that helps identify the original source of the document. + * Must either be a string representing the key of the source in the metadata + * or a function that takes a document and returns a string representing the source. + * **Required when cleanup is incremental**. + */ + sourceIdKey?: StringOrDocFunc + /** + * Batch size to use when cleaning up documents. + */ + cleanupBatchSize?: number + /** + * Force update documents even if they are present in the + * record manager. Useful if you are re-indexing with updated embeddings. + */ + forceUpdate?: boolean + + vectorStoreName?: string +} + +export function _batch(size: number, iterable: T[]): T[][] { + const batches: T[][] = [] + let currentBatch: T[] = [] + + iterable.forEach((item) => { + currentBatch.push(item) + + if (currentBatch.length >= size) { + batches.push(currentBatch) + currentBatch = [] + } + }) + + if (currentBatch.length > 0) { + batches.push(currentBatch) + } + + return batches +} + +export function _deduplicateInOrder(hashedDocuments: HashedDocumentInterface[]): HashedDocumentInterface[] { + const seen = new Set() + const deduplicated: HashedDocumentInterface[] = [] + + for (const hashedDoc of hashedDocuments) { + if (!hashedDoc.hash_) { + throw new Error('Hashed document does not have a hash') + } + + if (!seen.has(hashedDoc.hash_)) { + seen.add(hashedDoc.hash_) + deduplicated.push(hashedDoc) + } + } + return deduplicated +} + +export function _getSourceIdAssigner(sourceIdKey: StringOrDocFunc | null): (doc: DocumentInterface) => string | null { + if (sourceIdKey === null) { + return (_doc: DocumentInterface) => null + } else if (typeof sourceIdKey === 'string') { + return (doc: DocumentInterface) => doc.metadata[sourceIdKey] + } else if (typeof sourceIdKey === 'function') { + return sourceIdKey + } else { + throw new Error(`sourceIdKey should be null, a string or a function, got ${typeof sourceIdKey}`) + } +} + +export const _isBaseDocumentLoader = (arg: any): arg is BaseDocumentLoader => { + if ('load' in arg && typeof arg.load === 'function' && 'loadAndSplit' in arg && typeof arg.loadAndSplit === 'function') { + return true + } + return false +} + +interface IndexArgs { + docsSource: BaseDocumentLoader | DocumentInterface[] + recordManager: RecordManagerInterface + vectorStore: VectorStore + options?: IndexOptions +} + +/** + * Index data from the doc source into the vector store. + * + * Indexing functionality uses a manager to keep track of which documents + * are in the vector store. + * + * This allows us to keep track of which documents were updated, and which + * documents were deleted, which documents should be skipped. + * + * For the time being, documents are indexed using their hashes, and users + * are not able to specify the uid of the document. + * + * @param {IndexArgs} args + * @param {BaseDocumentLoader | DocumentInterface[]} args.docsSource The source of documents to index. Can be a DocumentLoader or a list of Documents. + * @param {RecordManagerInterface} args.recordManager The record manager to use for keeping track of indexed documents. + * @param {VectorStore} args.vectorStore The vector store to use for storing the documents. + * @param {IndexOptions | undefined} args.options Options for indexing. + * @returns {Promise} + */ +export async function index(args: IndexArgs): Promise { + const { docsSource, recordManager, vectorStore, options } = args + const { batchSize = 100, cleanup, sourceIdKey, cleanupBatchSize = 1000, forceUpdate = false, vectorStoreName } = options ?? {} + + if (cleanup === 'incremental' && !sourceIdKey) { + throw new Error("sourceIdKey is required when cleanup mode is incremental. Please provide through 'options.sourceIdKey'.") + } + + if (vectorStoreName) { + ;(recordManager as any).namespace = (recordManager as any).namespace + '_' + vectorStoreName + } + + const docs = _isBaseDocumentLoader(docsSource) ? await docsSource.load() : docsSource + + const sourceIdAssigner = _getSourceIdAssigner(sourceIdKey ?? null) + + const indexStartDt = await recordManager.getTime() + let numAdded = 0 + let addedDocs: Document[] = [] + let numDeleted = 0 + let numUpdated = 0 + let numSkipped = 0 + let totalKeys = 0 + + const batches = _batch(batchSize ?? 100, docs) + + for (const batch of batches) { + const hashedDocs = _deduplicateInOrder(batch.map((doc) => _HashedDocument.fromDocument(doc))) + + const sourceIds = hashedDocs.map((doc) => sourceIdAssigner(doc)) + + if (cleanup === 'incremental') { + hashedDocs.forEach((_hashedDoc, index) => { + const source = sourceIds[index] + if (source === null) { + throw new Error('sourceIdKey must be provided when cleanup is incremental') + } + }) + } + + const batchExists = await recordManager.exists(hashedDocs.map((doc) => doc.uid)) + + const uids: string[] = [] + const docsToIndex: DocumentInterface[] = [] + const docsToUpdate: string[] = [] + const seenDocs = new Set() + hashedDocs.forEach((hashedDoc, i) => { + const docExists = batchExists[i] + if (docExists) { + if (forceUpdate) { + seenDocs.add(hashedDoc.uid) + } else { + docsToUpdate.push(hashedDoc.uid) + return + } + } + uids.push(hashedDoc.uid) + docsToIndex.push(hashedDoc.toDocument()) + }) + + if (docsToUpdate.length > 0) { + await recordManager.update(docsToUpdate, { timeAtLeast: indexStartDt }) + numSkipped += docsToUpdate.length + } + + if (docsToIndex.length > 0) { + await vectorStore.addDocuments(docsToIndex, { ids: uids }) + const newDocs = docsToIndex.map((docs) => ({ + pageContent: docs.pageContent, + metadata: docs.metadata + })) + addedDocs.push(...newDocs) + numAdded += docsToIndex.length - seenDocs.size + numUpdated += seenDocs.size + } + + await recordManager.update( + hashedDocs.map((doc) => doc.uid), + { timeAtLeast: indexStartDt, groupIds: sourceIds } + ) + + if (cleanup === 'incremental') { + sourceIds.forEach((sourceId) => { + if (!sourceId) throw new Error('Source id cannot be null') + }) + const uidsToDelete = await recordManager.listKeys({ + before: indexStartDt, + groupIds: sourceIds + }) + await vectorStore.delete({ ids: uidsToDelete }) + await recordManager.deleteKeys(uidsToDelete) + numDeleted += uidsToDelete.length + } + } + + if (cleanup === 'full') { + let uidsToDelete = await recordManager.listKeys({ + before: indexStartDt, + limit: cleanupBatchSize + }) + while (uidsToDelete.length > 0) { + await vectorStore.delete({ ids: uidsToDelete }) + await recordManager.deleteKeys(uidsToDelete) + numDeleted += uidsToDelete.length + uidsToDelete = await recordManager.listKeys({ + before: indexStartDt, + limit: cleanupBatchSize + }) + } + } + + totalKeys = (await recordManager.listKeys({})).length + + return { + numAdded, + numDeleted, + numUpdated, + numSkipped, + totalKeys, + addedDocs + } +} diff --git a/packages/server/src/Interface.ts b/packages/server/src/Interface.ts index 91fe2a93..7b405a90 100644 --- a/packages/server/src/Interface.ts +++ b/packages/server/src/Interface.ts @@ -93,6 +93,14 @@ export interface IVariable { createdDate: Date } +export interface IUpsertHistory { + id: string + chatflowid: string + result: string + flowData: string + date: Date +} + export interface IComponentNodes { [key: string]: INode } diff --git a/packages/server/src/controllers/tools/index.ts b/packages/server/src/controllers/tools/index.ts index 49cea269..196032f6 100644 --- a/packages/server/src/controllers/tools/index.ts +++ b/packages/server/src/controllers/tools/index.ts @@ -19,7 +19,7 @@ const creatTool = async (req: Request, res: Response, next: NextFunction) => { const deleteTool = async (req: Request, res: Response, next: NextFunction) => { try { if (typeof req.params.id === 'undefined' || req.params.id === '') { - throw new Error(`Error: toolsController.updateTool - id not provided!`) + throw new Error(`Error: toolsController.deleteTool - id not provided!`) } const apiResponse = await toolsService.deleteTool(req.params.id) if (apiResponse.executionError) { diff --git a/packages/server/src/controllers/upsert-history/index.ts b/packages/server/src/controllers/upsert-history/index.ts new file mode 100644 index 00000000..43f36d5b --- /dev/null +++ b/packages/server/src/controllers/upsert-history/index.ts @@ -0,0 +1,30 @@ +import { Request, Response, NextFunction } from 'express' +import upsertHistoryService from '../../services/upsert-history' + +const getAllUpsertHistory = async (req: Request, res: Response, next: NextFunction) => { + try { + const sortOrder = req.query?.order as string | undefined + const chatflowid = req.params?.id as string | undefined + const startDate = req.query?.startDate as string | undefined + const endDate = req.query?.endDate as string | undefined + const apiResponse = await upsertHistoryService.getAllUpsertHistory(sortOrder, chatflowid, startDate, endDate) + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +const patchDeleteUpsertHistory = async (req: Request, res: Response, next: NextFunction) => { + try { + const ids = req.body.ids ?? [] + const apiResponse = await upsertHistoryService.patchDeleteUpsertHistory(ids) + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +export default { + getAllUpsertHistory, + patchDeleteUpsertHistory +} diff --git a/packages/server/src/database/entities/Assistant.ts b/packages/server/src/database/entities/Assistant.ts index cf2f2633..e6be9edf 100644 --- a/packages/server/src/database/entities/Assistant.ts +++ b/packages/server/src/database/entities/Assistant.ts @@ -10,7 +10,7 @@ export class Assistant implements IAssistant { @Column({ type: 'text' }) details: string - @Column({ type: 'uuid'}) + @Column({ type: 'uuid' }) credential: string @Column({ nullable: true }) diff --git a/packages/server/src/database/entities/ChatFlow.ts b/packages/server/src/database/entities/ChatFlow.ts index 92f5a4b6..d654128d 100644 --- a/packages/server/src/database/entities/ChatFlow.ts +++ b/packages/server/src/database/entities/ChatFlow.ts @@ -34,11 +34,11 @@ export class ChatFlow implements IChatFlow { @Column({ nullable: true, type: 'text' }) speechToText?: string - @Column({type:'timestamp'}) + @Column({ type: 'timestamp' }) @CreateDateColumn() createdDate: Date - @Column({type:'timestamp'}) + @Column({ type: 'timestamp' }) @UpdateDateColumn() updatedDate: Date diff --git a/packages/server/src/database/entities/ChatMessage.ts b/packages/server/src/database/entities/ChatMessage.ts index 46cc0dc1..f0dcd0c5 100644 --- a/packages/server/src/database/entities/ChatMessage.ts +++ b/packages/server/src/database/entities/ChatMessage.ts @@ -41,7 +41,7 @@ export class ChatMessage implements IChatMessage { @Column({ type: 'varchar', nullable: true }) sessionId?: string - @Column({type:'timestamp'}) + @Column({ type: 'timestamp' }) @CreateDateColumn() createdDate: Date } diff --git a/packages/server/src/database/entities/ChatMessageFeedback.ts b/packages/server/src/database/entities/ChatMessageFeedback.ts index 7010a794..f7590b29 100644 --- a/packages/server/src/database/entities/ChatMessageFeedback.ts +++ b/packages/server/src/database/entities/ChatMessageFeedback.ts @@ -25,7 +25,7 @@ export class ChatMessageFeedback implements IChatMessageFeedback { @Column({ nullable: true, type: 'text' }) content?: string - @Column({type:'timestamp'}) + @Column({ type: 'timestamp' }) @CreateDateColumn() createdDate: Date } diff --git a/packages/server/src/database/entities/Credential.ts b/packages/server/src/database/entities/Credential.ts index 7b368ef3..daeb0595 100644 --- a/packages/server/src/database/entities/Credential.ts +++ b/packages/server/src/database/entities/Credential.ts @@ -16,11 +16,11 @@ export class Credential implements ICredential { @Column({ type: 'text' }) encryptedData: string - @Column({type:'timestamp'}) + @Column({ type: 'timestamp' }) @CreateDateColumn() createdDate: Date - @Column({type:'timestamp'}) + @Column({ type: 'timestamp' }) @UpdateDateColumn() updatedDate: Date } diff --git a/packages/server/src/database/entities/Tool.ts b/packages/server/src/database/entities/Tool.ts index 38f30480..49f7335e 100644 --- a/packages/server/src/database/entities/Tool.ts +++ b/packages/server/src/database/entities/Tool.ts @@ -25,11 +25,11 @@ export class Tool implements ITool { @Column({ nullable: true, type: 'text' }) func?: string - @Column({type:'timestamp'}) + @Column({ type: 'timestamp' }) @CreateDateColumn() createdDate: Date - @Column({type:'timestamp'}) + @Column({ type: 'timestamp' }) @UpdateDateColumn() updatedDate: Date } diff --git a/packages/server/src/database/entities/UpsertHistory.ts b/packages/server/src/database/entities/UpsertHistory.ts new file mode 100644 index 00000000..0d7674a6 --- /dev/null +++ b/packages/server/src/database/entities/UpsertHistory.ts @@ -0,0 +1,22 @@ +/* eslint-disable */ +import { Entity, Column, PrimaryGeneratedColumn, Index, CreateDateColumn } from 'typeorm' +import { IUpsertHistory } from '../../Interface' + +@Entity() +export class UpsertHistory implements IUpsertHistory { + @PrimaryGeneratedColumn('uuid') + id: string + + @Index() + @Column() + chatflowid: string + + @Column() + result: string + + @Column() + flowData: string + + @CreateDateColumn() + date: Date +} diff --git a/packages/server/src/database/entities/Variable.ts b/packages/server/src/database/entities/Variable.ts index 00df7267..2437e824 100644 --- a/packages/server/src/database/entities/Variable.ts +++ b/packages/server/src/database/entities/Variable.ts @@ -16,11 +16,11 @@ export class Variable implements IVariable { @Column({ default: 'string', type: 'text' }) type: string - @Column({type:'timestamp'}) + @Column({ type: 'timestamp' }) @CreateDateColumn() createdDate: Date - @Column({type:'timestamp'}) + @Column({ type: 'timestamp' }) @UpdateDateColumn() updatedDate: Date } diff --git a/packages/server/src/database/entities/index.ts b/packages/server/src/database/entities/index.ts index 6a0e2d22..d9b9536e 100644 --- a/packages/server/src/database/entities/index.ts +++ b/packages/server/src/database/entities/index.ts @@ -5,6 +5,7 @@ import { Credential } from './Credential' import { Tool } from './Tool' import { Assistant } from './Assistant' import { Variable } from './Variable' +import { UpsertHistory } from './UpsertHistory' export const entities = { ChatFlow, @@ -13,5 +14,6 @@ export const entities = { Credential, Tool, Assistant, - Variable + Variable, + UpsertHistory } diff --git a/packages/server/src/database/migrations/mysql/1709814301358-AddUpsertHistoryEntity.ts b/packages/server/src/database/migrations/mysql/1709814301358-AddUpsertHistoryEntity.ts new file mode 100644 index 00000000..5c114bb1 --- /dev/null +++ b/packages/server/src/database/migrations/mysql/1709814301358-AddUpsertHistoryEntity.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddUpsertHistoryEntity1709814301358 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE IF NOT EXISTS \`upsert_history\` ( + \`id\` varchar(36) NOT NULL, + \`chatflowid\` varchar(255) NOT NULL, + \`result\` text NOT NULL, + \`flowData\` text NOT NULL, + \`date\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + PRIMARY KEY (\`id\`) + KEY \`IDX_a0b59fd66f6e48d2b198123cb6\` (\`chatflowid\`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE upsert_history`) + } +} diff --git a/packages/server/src/database/migrations/mysql/index.ts b/packages/server/src/database/migrations/mysql/index.ts index a4baf424..b0102422 100644 --- a/packages/server/src/database/migrations/mysql/index.ts +++ b/packages/server/src/database/migrations/mysql/index.ts @@ -13,6 +13,7 @@ import { AddFileAnnotationsToChatMessage1700271021237 } from './1700271021237-Ad import { AddFileUploadsToChatMessage1701788586491 } from './1701788586491-AddFileUploadsToChatMessage' import { AddVariableEntity1699325775451 } from './1702200925471-AddVariableEntity' import { AddSpeechToText1706364937060 } from './1706364937060-AddSpeechToText' +import { AddUpsertHistoryEntity1709814301358 } from './1709814301358-AddUpsertHistoryEntity' import { AddFeedback1707213626553 } from './1707213626553-AddFeedback' export const mysqlMigrations = [ @@ -31,5 +32,6 @@ export const mysqlMigrations = [ AddFileUploadsToChatMessage1701788586491, AddVariableEntity1699325775451, AddSpeechToText1706364937060, + AddUpsertHistoryEntity1709814301358, AddFeedback1707213626553 ] diff --git a/packages/server/src/database/migrations/postgres/1709814301358-AddUpsertHistoryEntity.ts b/packages/server/src/database/migrations/postgres/1709814301358-AddUpsertHistoryEntity.ts new file mode 100644 index 00000000..9b93156f --- /dev/null +++ b/packages/server/src/database/migrations/postgres/1709814301358-AddUpsertHistoryEntity.ts @@ -0,0 +1,20 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddUpsertHistoryEntity1709814301358 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE IF NOT EXISTS upsert_history ( + id uuid NOT NULL DEFAULT uuid_generate_v4(), + "chatflowid" varchar NOT NULL, + "result" text NOT NULL, + "flowData" text NOT NULL, + "date" timestamp NOT NULL DEFAULT now(), + CONSTRAINT "PK_37327b22b6e246319bd5eeb0e88" PRIMARY KEY (id) + );` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE upsert_history`) + } +} diff --git a/packages/server/src/database/migrations/postgres/index.ts b/packages/server/src/database/migrations/postgres/index.ts index cec4ccbd..3f37c6e8 100644 --- a/packages/server/src/database/migrations/postgres/index.ts +++ b/packages/server/src/database/migrations/postgres/index.ts @@ -13,6 +13,7 @@ import { AddFileAnnotationsToChatMessage1700271021237 } from './1700271021237-Ad import { AddFileUploadsToChatMessage1701788586491 } from './1701788586491-AddFileUploadsToChatMessage' import { AddVariableEntity1699325775451 } from './1702200925471-AddVariableEntity' import { AddSpeechToText1706364937060 } from './1706364937060-AddSpeechToText' +import { AddUpsertHistoryEntity1709814301358 } from './1709814301358-AddUpsertHistoryEntity' import { AddFeedback1707213601923 } from './1707213601923-AddFeedback' import { FieldTypes1710497452584 } from './1710497452584-FieldTypes' @@ -32,6 +33,7 @@ export const postgresMigrations = [ AddFileUploadsToChatMessage1701788586491, AddVariableEntity1699325775451, AddSpeechToText1706364937060, + AddUpsertHistoryEntity1709814301358, AddFeedback1707213601923, FieldTypes1710497452584 ] diff --git a/packages/server/src/database/migrations/sqlite/1709814301358-AddUpsertHistoryEntity.ts b/packages/server/src/database/migrations/sqlite/1709814301358-AddUpsertHistoryEntity.ts new file mode 100644 index 00000000..c5cfca7d --- /dev/null +++ b/packages/server/src/database/migrations/sqlite/1709814301358-AddUpsertHistoryEntity.ts @@ -0,0 +1,13 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddUpsertHistoryEntity1709814301358 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE IF NOT EXISTS "upsert_history" ("id" varchar PRIMARY KEY NOT NULL, "chatflowid" varchar NOT NULL, "result" text NOT NULL, "flowData" text NOT NULL, "date" datetime NOT NULL DEFAULT (datetime('now')));` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE upsert_history`) + } +} diff --git a/packages/server/src/database/migrations/sqlite/index.ts b/packages/server/src/database/migrations/sqlite/index.ts index 0b4f3e05..0cc692dd 100644 --- a/packages/server/src/database/migrations/sqlite/index.ts +++ b/packages/server/src/database/migrations/sqlite/index.ts @@ -13,6 +13,7 @@ import { AddFileAnnotationsToChatMessage1700271021237 } from './1700271021237-Ad import { AddFileUploadsToChatMessage1701788586491 } from './1701788586491-AddFileUploadsToChatMessage' import { AddVariableEntity1699325775451 } from './1702200925471-AddVariableEntity' import { AddSpeechToText1706364937060 } from './1706364937060-AddSpeechToText' +import { AddUpsertHistoryEntity1709814301358 } from './1709814301358-AddUpsertHistoryEntity' import { AddFeedback1707213619308 } from './1707213619308-AddFeedback' export const sqliteMigrations = [ @@ -31,5 +32,6 @@ export const sqliteMigrations = [ AddFileUploadsToChatMessage1701788586491, AddVariableEntity1699325775451, AddSpeechToText1706364937060, + AddUpsertHistoryEntity1709814301358, AddFeedback1707213619308 ] diff --git a/packages/server/src/routes/index.ts b/packages/server/src/routes/index.ts index c989880b..8d453879 100644 --- a/packages/server/src/routes/index.ts +++ b/packages/server/src/routes/index.ts @@ -35,6 +35,7 @@ import variablesRouter from './variables' import vectorRouter from './vectors' import verifyRouter from './verify' import versionRouter from './versions' +import upsertHistoryRouter from './upsert-history' const router = express.Router() @@ -74,5 +75,6 @@ router.use('/variables', variablesRouter) router.use('/vector', vectorRouter) router.use('/verify', verifyRouter) router.use('/version', versionRouter) +router.use('/upsert-history', upsertHistoryRouter) export default router diff --git a/packages/server/src/routes/upsert-history/index.ts b/packages/server/src/routes/upsert-history/index.ts new file mode 100644 index 00000000..1f4d9d02 --- /dev/null +++ b/packages/server/src/routes/upsert-history/index.ts @@ -0,0 +1,15 @@ +import express from 'express' +import upsertHistoryController from '../../controllers/upsert-history' +const router = express.Router() + +// CREATE + +// READ +router.get('/:id', upsertHistoryController.getAllUpsertHistory) + +// PATCH +router.patch('/', upsertHistoryController.patchDeleteUpsertHistory) + +// DELETE + +export default router diff --git a/packages/server/src/services/chatflows/index.ts b/packages/server/src/services/chatflows/index.ts index c6ee778a..78c5c695 100644 --- a/packages/server/src/services/chatflows/index.ts +++ b/packages/server/src/services/chatflows/index.ts @@ -14,6 +14,9 @@ import logger from '../../utils/logger' import { getStoragePath } from 'flowise-components' import { IReactFlowObject } from '../../Interface' import { utilGetUploadsConfig } from '../../utils/getUploadsConfig' +import { ChatMessage } from '../../database/entities/ChatMessage' +import { ChatMessageFeedback } from '../../database/entities/ChatMessageFeedback' +import { UpsertHistory } from '../../database/entities/UpsertHistory' // Check if chatflow valid for streaming const checkIfChatflowIsValidForStreaming = async (chatflowId: string): Promise => { @@ -105,9 +108,18 @@ const deleteChatflow = async (chatflowId: string): Promise => { const appServer = getRunningExpressApp() const dbResponse = await appServer.AppDataSource.getRepository(ChatFlow).delete({ id: chatflowId }) try { - // Delete all uploads corresponding to this chatflow + // Delete all uploads corresponding to this chatflow const directory = path.join(getStoragePath(), chatflowId) deleteFolderRecursive(directory) + + // Delete all chat messages + await appServer.AppDataSource.getRepository(ChatMessage).delete({ chatflowid: chatflowId }) + + // Delete all chat feedback + await appServer.AppDataSource.getRepository(ChatMessageFeedback).delete({ chatflowid: chatflowId }) + + // Delete all upsert history + await appServer.AppDataSource.getRepository(UpsertHistory).delete({ chatflowid: chatflowId }) } catch (e) { logger.error(`[server]: Error deleting file storage for chatflow ${chatflowId}: ${e}`) } diff --git a/packages/server/src/services/upsert-history/index.ts b/packages/server/src/services/upsert-history/index.ts new file mode 100644 index 00000000..96d9d82d --- /dev/null +++ b/packages/server/src/services/upsert-history/index.ts @@ -0,0 +1,66 @@ +import { MoreThanOrEqual, LessThanOrEqual } from 'typeorm' +import { getRunningExpressApp } from '../../utils/getRunningExpressApp' +import { UpsertHistory } from '../../database/entities/UpsertHistory' + +const getAllUpsertHistory = async ( + sortOrder: string | undefined, + chatflowid: string | undefined, + startDate: string | undefined, + endDate: string | undefined +) => { + try { + 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 + } + + let fromDate + if (startDate) fromDate = setDateToStartOrEndOfDay(startDate, 'start') + + let toDate + if (endDate) toDate = setDateToStartOrEndOfDay(endDate, 'end') + + let upsertHistory = await appServer.AppDataSource.getRepository(UpsertHistory).find({ + where: { + chatflowid, + ...(fromDate && { date: MoreThanOrEqual(fromDate) }), + ...(toDate && { date: LessThanOrEqual(toDate) }) + }, + order: { + date: sortOrder === 'DESC' ? 'DESC' : 'ASC' + } + }) + upsertHistory = upsertHistory.map((hist) => { + return { + ...hist, + result: hist.result ? JSON.parse(hist.result) : {}, + flowData: hist.flowData ? JSON.parse(hist.flowData) : {} + } + }) + + return upsertHistory + } catch (error) { + throw new Error(`Error: upsertHistoryServices.getAllUpsertHistory - ${error}`) + } +} + +const patchDeleteUpsertHistory = async (ids: string[] = []): Promise => { + try { + const appServer = getRunningExpressApp() + const dbResponse = await appServer.AppDataSource.getRepository(UpsertHistory).delete(ids) + return dbResponse + } catch (error) { + throw new Error(`Error: upsertHistoryServices.patchUpsertHistory - ${error}`) + } +} + +export default { + getAllUpsertHistory, + patchDeleteUpsertHistory +} diff --git a/packages/server/src/services/variables/index.ts b/packages/server/src/services/variables/index.ts index 7153c940..37e1c073 100644 --- a/packages/server/src/services/variables/index.ts +++ b/packages/server/src/services/variables/index.ts @@ -18,7 +18,7 @@ const deleteVariable = async (variableId: string): Promise => { const dbResponse = await appServer.AppDataSource.getRepository(Variable).delete({ id: variableId }) return dbResponse } catch (error) { - throw new Error(`Error: variablesServices.createVariable - ${error}`) + throw new Error(`Error: variablesServices.deleteVariable - ${error}`) } } diff --git a/packages/server/src/utils/buildChatflow.ts b/packages/server/src/utils/buildChatflow.ts index 811782bc..b1043cc4 100644 --- a/packages/server/src/utils/buildChatflow.ts +++ b/packages/server/src/utils/buildChatflow.ts @@ -36,7 +36,6 @@ import { utilAddChatMessage } from './addChatMesage' * @param {Request} req * @param {Server} socketIO * @param {boolean} isInternal - * @param {boolean} isUpsert */ export const utilBuildChatflow = async (req: Request, socketIO?: Server, isInternal: boolean = false): Promise => { try { diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index e41e9f64..cfbb2535 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -237,6 +237,84 @@ export const getEndingNodes = (nodeDependencies: INodeDependencies, graph: INode return endingNodeIds } +/** + * Get file name from base64 string + * @param {string} fileBase64 + */ +export const getFileName = (fileBase64: string): string => { + let fileNames = [] + if (fileBase64.startsWith('[') && fileBase64.endsWith(']')) { + const files = JSON.parse(fileBase64) + for (const file of files) { + const splitDataURI = file.split(',') + const filename = splitDataURI[splitDataURI.length - 1].split(':')[1] + fileNames.push(filename) + } + return fileNames.join(', ') + } else { + const splitDataURI = fileBase64.split(',') + const filename = splitDataURI[splitDataURI.length - 1].split(':')[1] + return filename + } +} + +/** + * Save upsert flowData + * @param {INodeData} nodeData + * @param {Record} upsertHistory + */ +export const saveUpsertFlowData = (nodeData: INodeData, upsertHistory: Record): ICommonObject[] => { + const existingUpsertFlowData = upsertHistory['flowData'] ?? [] + const paramValues: ICommonObject[] = [] + + for (const input in nodeData.inputs) { + const inputParam = nodeData.inputParams.find((inp) => inp.name === input) + if (!inputParam) continue + + let paramValue: ICommonObject = {} + + if (!nodeData.inputs[input]) { + continue + } + if ( + typeof nodeData.inputs[input] === 'string' && + nodeData.inputs[input].startsWith('{{') && + nodeData.inputs[input].endsWith('}}') + ) { + continue + } + // Get file name instead of the base64 string + if (nodeData.category === 'Document Loaders' && nodeData.inputParams.find((inp) => inp.name === input)?.type === 'file') { + paramValue = { + label: inputParam?.label, + name: inputParam?.name, + type: inputParam?.type, + value: getFileName(nodeData.inputs[input]) + } + paramValues.push(paramValue) + continue + } + + paramValue = { + label: inputParam?.label, + name: inputParam?.name, + type: inputParam?.type, + value: nodeData.inputs[input] + } + paramValues.push(paramValue) + } + + const newFlowData = { + label: nodeData.label, + name: nodeData.name, + category: nodeData.category, + id: nodeData.id, + paramValues + } + existingUpsertFlowData.push(newFlowData) + return existingUpsertFlowData +} + /** * Build langchain from start to end * @param {string[]} startingNodeIds @@ -272,6 +350,8 @@ export const buildFlow = async ( ) => { const flowNodes = cloneDeep(reactFlowNodes) + let upsertHistory: Record = {} + // Create a Queue and add our initial node in it const nodeQueue = [] as INodeQueue[] const exploredNode = {} as IExploredNode @@ -302,12 +382,15 @@ export const buildFlow = async ( let flowNodeData = cloneDeep(reactFlowNode.data) if (overrideConfig) flowNodeData = replaceInputsWithConfig(flowNodeData, overrideConfig) + + if (isUpsert) upsertHistory['flowData'] = saveUpsertFlowData(flowNodeData, upsertHistory) + const reactFlowNodeData: INodeData = resolveVariables(flowNodeData, flowNodes, question, chatHistory) // TODO: Avoid processing Text Splitter + Doc Loader once Upsert & Load Existing Vector Nodes are deprecated if (isUpsert && stopNodeId && nodeId === stopNodeId) { logger.debug(`[server]: Upserting ${reactFlowNode.data.label} (${reactFlowNode.data.id})`) - await newNodeInstance.vectorStoreMethods!['upsert']!.call(newNodeInstance, reactFlowNodeData, { + const indexResult = await newNodeInstance.vectorStoreMethods!['upsert']!.call(newNodeInstance, reactFlowNodeData, { chatId, sessionId, chatflowid, @@ -319,6 +402,7 @@ export const buildFlow = async ( dynamicVariables, uploads }) + if (indexResult) upsertHistory['result'] = indexResult logger.debug(`[server]: Finished upserting ${reactFlowNode.data.label} (${reactFlowNode.data.id})`) break } else { @@ -422,7 +506,7 @@ export const buildFlow = async ( flowNodes.push(flowNodes.splice(index, 1)[0]) } } - return flowNodes + return isUpsert ? (upsertHistory as any) : flowNodes } /** diff --git a/packages/server/src/utils/upsertVector.ts b/packages/server/src/utils/upsertVector.ts index 0fc90eb1..d8714502 100644 --- a/packages/server/src/utils/upsertVector.ts +++ b/packages/server/src/utils/upsertVector.ts @@ -1,5 +1,6 @@ import { Request, Response } from 'express' import * as fs from 'fs' +import { cloneDeep, omit } from 'lodash' import { ICommonObject } from 'flowise-components' import telemetryService from '../services/telemetry' import logger from '../utils/logger' @@ -18,7 +19,14 @@ import { utilValidateKey } from './validateKey' import { IncomingInput, INodeDirectedGraph, IReactFlowObject, chatType } from '../Interface' import { ChatFlow } from '../database/entities/ChatFlow' import { getRunningExpressApp } from '../utils/getRunningExpressApp' +import { UpsertHistory } from '../database/entities/UpsertHistory' +/** + * Upsert documents + * @param {Request} req + * @param {Response} res + * @param {boolean} isInternal + */ export const upsertVector = async (req: Request, res: Response, isInternal: boolean = false) => { try { const appServer = getRunningExpressApp() @@ -78,6 +86,8 @@ export const upsertVector = async (req: Request, res: Response, isInternal: bool (node) => node.data.category === 'Vector Stores' && !node.data.label.includes('Upsert') && !node.data.label.includes('Load Existing') ) + + // Check if multiple vector store nodes exist, and if stopNodeId is specified if (vsNodes.length > 1 && !stopNodeId) { return res.status(500).send('There are multiple vector nodes, please provide stopNodeId in body request') } else if (vsNodes.length === 1 && !stopNodeId) { @@ -99,7 +109,7 @@ export const upsertVector = async (req: Request, res: Response, isInternal: bool const { startingNodeIds, depthQueue } = getStartingNodes(filteredGraph, stopNodeId) - await buildFlow( + const upsertedResult = await buildFlow( startingNodeIds, nodes, edges, @@ -121,6 +131,19 @@ export const upsertVector = async (req: Request, res: Response, isInternal: bool const startingNodes = nodes.filter((nd) => startingNodeIds.includes(nd.data.id)) await appServer.chatflowPool.add(chatflowid, undefined, startingNodes, incomingInput?.overrideConfig) + + // Save to DB + if (upsertedResult['flowData'] && upsertedResult['result']) { + const result = cloneDeep(upsertedResult) + result['flowData'] = JSON.stringify(result['flowData']) + result['result'] = JSON.stringify(omit(result['result'], ['totalKeys', 'addedDocs'])) + result.chatflowid = chatflowid + const newUpsertHistory = new UpsertHistory() + Object.assign(newUpsertHistory, result) + const upsertHistory = appServer.AppDataSource.getRepository(UpsertHistory).create(newUpsertHistory) + await appServer.AppDataSource.getRepository(UpsertHistory).save(upsertHistory) + } + await telemetryService.createEvent({ name: `vector_upserted`, data: { @@ -131,7 +154,7 @@ export const upsertVector = async (req: Request, res: Response, isInternal: bool stopNodeId } }) - return res.status(201).send('Successfully Upserted') + return res.status(201).json(upsertedResult['result'] ?? { result: 'Successfully Upserted' }) } catch (e: any) { logger.error('[server]: Error:', e) return res.status(500).send(e.message) diff --git a/packages/ui/src/api/vectorstore.js b/packages/ui/src/api/vectorstore.js index 053f5112..64f2ce65 100644 --- a/packages/ui/src/api/vectorstore.js +++ b/packages/ui/src/api/vectorstore.js @@ -1,7 +1,11 @@ import client from './client' const upsertVectorStore = (id, input) => client.post(`/vector/internal-upsert/${id}`, input) +const getUpsertHistory = (id, params = {}) => client.get(`/upsert-history/${id}`, { params: { order: 'DESC', ...params } }) +const deleteUpsertHistory = (ids) => client.patch(`/upsert-history`, { ids }) export default { - upsertVectorStore + getUpsertHistory, + upsertVectorStore, + deleteUpsertHistory } diff --git a/packages/ui/src/assets/images/upsert_history_empty.svg b/packages/ui/src/assets/images/upsert_history_empty.svg new file mode 100644 index 00000000..9f341e2e --- /dev/null +++ b/packages/ui/src/assets/images/upsert_history_empty.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/ui/src/menu-items/settings.js b/packages/ui/src/menu-items/settings.js index 9b83aaa4..c45d7a20 100644 --- a/packages/ui/src/menu-items/settings.js +++ b/packages/ui/src/menu-items/settings.js @@ -1,5 +1,13 @@ // assets -import { IconTrash, IconFileUpload, IconFileExport, IconCopy, IconMessage, IconAdjustmentsHorizontal } from '@tabler/icons' +import { + IconTrash, + IconFileUpload, + IconFileExport, + IconCopy, + IconMessage, + IconDatabaseExport, + IconAdjustmentsHorizontal +} from '@tabler/icons' // constant const icons = { @@ -8,6 +16,7 @@ const icons = { IconFileExport, IconCopy, IconMessage, + IconDatabaseExport, IconAdjustmentsHorizontal } @@ -25,6 +34,13 @@ const settings = { url: '', icon: icons.IconMessage }, + { + id: 'viewUpsertHistory', + title: 'Upsert History', + type: 'item', + url: '', + icon: icons.IconDatabaseExport + }, { id: 'chatflowConfiguration', title: 'Configuration', diff --git a/packages/ui/src/utils/genericHelper.js b/packages/ui/src/utils/genericHelper.js index 645435d2..ed445acd 100644 --- a/packages/ui/src/utils/genericHelper.js +++ b/packages/ui/src/utils/genericHelper.js @@ -355,6 +355,11 @@ export const getUpsertDetails = (nodes, edges) => { innerNodes.push(nodes.find((node) => node.data.id === embeddingsId)) } + if (vsNode.data.inputs.recordManager) { + const recordManagerId = vsNode.data.inputs.recordManager.replace(/{{|}}/g, '').split('.')[0] + innerNodes.push(nodes.find((node) => node.data.id === recordManagerId)) + } + for (const doc of connectedDocs) { const docId = doc.replace(/{{|}}/g, '').split('.')[0] const docNode = nodes.find((node) => node.data.id === docId) diff --git a/packages/ui/src/views/canvas/CanvasHeader.jsx b/packages/ui/src/views/canvas/CanvasHeader.jsx index 4728c6b0..2ec30a16 100644 --- a/packages/ui/src/views/canvas/CanvasHeader.jsx +++ b/packages/ui/src/views/canvas/CanvasHeader.jsx @@ -16,6 +16,7 @@ import SaveChatflowDialog from '@/ui-component/dialog/SaveChatflowDialog' import APICodeDialog from '@/views/chatflows/APICodeDialog' import ViewMessagesDialog from '@/ui-component/dialog/ViewMessagesDialog' import ChatflowConfigurationDialog from '@/ui-component/dialog/ChatflowConfigurationDialog' +import UpsertHistoryDialog from '@/views/vectorstore/UpsertHistoryDialog' // API import chatflowsApi from '@/api/chatflows' @@ -45,6 +46,8 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl const [apiDialogProps, setAPIDialogProps] = useState({}) const [viewMessagesDialogOpen, setViewMessagesDialogOpen] = useState(false) const [viewMessagesDialogProps, setViewMessagesDialogProps] = useState({}) + const [upsertHistoryDialogOpen, setUpsertHistoryDialogOpen] = useState(false) + const [upsertHistoryDialogProps, setUpsertHistoryDialogProps] = useState({}) const [chatflowConfigurationDialogOpen, setChatflowConfigurationDialogOpen] = useState(false) const [chatflowConfigurationDialogProps, setChatflowConfigurationDialogProps] = useState({}) @@ -62,6 +65,12 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl chatflow: chatflow }) setViewMessagesDialogOpen(true) + } else if (setting === 'viewUpsertHistory') { + setUpsertHistoryDialogProps({ + title: 'View Upsert History', + chatflow: chatflow + }) + setUpsertHistoryDialogOpen(true) } else if (setting === 'chatflowConfiguration') { setChatflowConfigurationDialogProps({ title: 'Chatflow Configuration', @@ -387,6 +396,11 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl dialogProps={viewMessagesDialogProps} onCancel={() => setViewMessagesDialogOpen(false)} /> + setUpsertHistoryDialogOpen(false)} + /> { let allMessages = [...cloneDeep(prevMessages)] diff --git a/packages/ui/src/views/vectorstore/UpsertHistoryDialog.jsx b/packages/ui/src/views/vectorstore/UpsertHistoryDialog.jsx new file mode 100644 index 00000000..5c976ece --- /dev/null +++ b/packages/ui/src/views/vectorstore/UpsertHistoryDialog.jsx @@ -0,0 +1,453 @@ +import PropTypes from 'prop-types' +import { createPortal } from 'react-dom' +import { useDispatch, useSelector } from 'react-redux' +import { useEffect, useState, forwardRef } from 'react' +import DatePicker from 'react-datepicker' +import moment from 'moment/moment' + +// MUI +import { + Stack, + Box, + Paper, + Table, + TableBody, + TableContainer, + TableHead, + TableRow, + TableCell, + IconButton, + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + ListItemButton, + Collapse, + Accordion, + AccordionSummary, + Typography, + AccordionDetails, + Checkbox +} from '@mui/material' +import { useTheme } from '@mui/material/styles' +import ExpandMoreIcon from '@mui/icons-material/ExpandMore' +import { IconChevronsUp, IconChevronsDown, IconTrash, IconX } from '@tabler/icons' + +// Project imports +import { TableViewOnly } from '@/ui-component/table/Table' +import { TooltipWithParser } from '@/ui-component/tooltip/TooltipWithParser' +import HistoryEmptySVG from '@/assets/images/upsert_history_empty.svg' + +// Api +import vectorstoreApi from '@/api/vectorstore' +import useApi from '@/hooks/useApi' + +// Store +import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from '@/store/actions' +import { baseURL } from '@/store/constant' +import useNotifier from '@/utils/useNotifier' +import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from '@/store/actions' + +const DatePickerCustomInput = forwardRef(function DatePickerCustomInput({ value, onClick }, ref) { + return ( + + {value} + + ) +}) + +DatePickerCustomInput.propTypes = { + value: PropTypes.string, + onClick: PropTypes.func +} + +function UpsertHistoryRow(props) { + const [open, setOpen] = useState(false) + const [nodeConfigExpanded, setNodeConfigExpanded] = useState({}) + + const handleAccordionChange = (nodeLabel) => (event, isExpanded) => { + const accordianNodes = { ...nodeConfigExpanded } + accordianNodes[nodeLabel] = isExpanded + setNodeConfigExpanded(accordianNodes) + } + + const isItemSelected = props.selected.indexOf(props.upsertHistory.id) !== -1 + + return ( + <> + + + props.handleSelect(event, props.upsertHistory.id)} + inputProps={{ + 'aria-labelledby': props.upsertHistory.id + }} + /> + + {moment(props.upsertHistory.date).format('MMMM Do YYYY, h:mm:ss a')} + {props.upsertHistory.result?.numAdded ?? '0'} + {props.upsertHistory.result?.numUpdated ?? '0'} + {props.upsertHistory.result?.numSkipped ?? '0'} + {props.upsertHistory.result?.numDeleted ?? '0'} + + setOpen(!open)}> + {open ? : } + + + + {open && ( + + + + + {(props.upsertHistory.flowData ?? []).map((node, index) => { + return ( + + } + aria-controls={`nodes-accordian-${node.name}`} + id={`nodes-accordian-header-${node.name}`} + > +

+
+ {node.name} +
+ {node.label} +
+ {node.id} +
+
+ + + + + + ) + })} + + + + + )} + + ) +} + +UpsertHistoryRow.propTypes = { + upsertHistory: PropTypes.object, + theme: PropTypes.any, + isDarkMode: PropTypes.bool, + selected: PropTypes.array, + handleSelect: PropTypes.func +} + +const UpsertHistoryDialog = ({ show, dialogProps, onCancel }) => { + const portalElement = document.getElementById('portal') + const dispatch = useDispatch() + const customization = useSelector((state) => state.customization) + const theme = useTheme() + const getUpsertHistoryApi = useApi(vectorstoreApi.getUpsertHistory) + + useNotifier() + const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)) + const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) + + const [chatflowUpsertHistory, setChatflowUpsertHistory] = useState([]) + const [startDate, setStartDate] = useState(new Date().setMonth(new Date().getMonth() - 1)) + const [endDate, setEndDate] = useState(new Date()) + const [selected, setSelected] = useState([]) + + const onSelectAllClick = (event) => { + if (event.target.checked) { + const newSelected = chatflowUpsertHistory.map((n) => n.id) + setSelected(newSelected) + return + } + setSelected([]) + } + + const onStartDateSelected = (date) => { + setStartDate(date) + getUpsertHistoryApi.request(dialogProps.chatflow.id, { + startDate: date, + endDate: endDate + }) + } + + const onEndDateSelected = (date) => { + setEndDate(date) + getUpsertHistoryApi.request(dialogProps.chatflow.id, { + endDate: date, + startDate: startDate + }) + } + + const handleSelect = (event, id) => { + const selectedIndex = selected.indexOf(id) + let newSelected = [] + + if (selectedIndex === -1) { + newSelected = newSelected.concat(selected, id) + } else if (selectedIndex === 0) { + newSelected = newSelected.concat(selected.slice(1)) + } else if (selectedIndex === selected.length - 1) { + newSelected = newSelected.concat(selected.slice(0, -1)) + } else if (selectedIndex > 0) { + newSelected = newSelected.concat(selected.slice(0, selectedIndex), selected.slice(selectedIndex + 1)) + } + setSelected(newSelected) + } + + const handleRemoveHistory = async () => { + try { + await vectorstoreApi.deleteUpsertHistory(selected) + enqueueSnackbar({ + message: 'Succesfully deleted upsert history', + options: { + key: new Date().getTime() + Math.random(), + variant: 'success', + action: (key) => ( + + ) + } + }) + setChatflowUpsertHistory(chatflowUpsertHistory.filter((hist) => !selected.includes(hist.id))) + setSelected([]) + } catch (error) { + enqueueSnackbar({ + message: 'Error deleting upsert history', + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + persist: true, + action: (key) => ( + + ) + } + }) + } + } + + useEffect(() => { + if (getUpsertHistoryApi.data) { + setChatflowUpsertHistory(getUpsertHistoryApi.data) + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [getUpsertHistoryApi.data]) + + useEffect(() => { + if (dialogProps.chatflow) { + getUpsertHistoryApi.request(dialogProps.chatflow.id) + } + + return () => { + setChatflowUpsertHistory([]) + setStartDate(new Date().setMonth(new Date().getMonth() - 1)) + setEndDate(new Date()) + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dialogProps]) + + useEffect(() => { + if (show) dispatch({ type: SHOW_CANVAS_DIALOG }) + else dispatch({ type: HIDE_CANVAS_DIALOG }) + return () => dispatch({ type: HIDE_CANVAS_DIALOG }) + }, [show, dispatch]) + + const component = show ? ( + + + {dialogProps.title} + + + <> +
+
+ From Date + onStartDateSelected(date)} + selectsStart + startDate={startDate} + endDate={endDate} + customInput={} + /> +
+
+ To Date + onEndDateSelected(date)} + selectsEnd + startDate={startDate} + endDate={endDate} + minDate={startDate} + maxDate={new Date()} + customInput={} + /> +
+
+ {selected.length > 0 && ( + + )} + {chatflowUpsertHistory.length <= 0 && ( + + + HistoryEmptySVG + +
No Upsert History Yet
+
+ )} + {chatflowUpsertHistory.length > 0 && ( + + + + + + + + Date + + Added{' '} + + + + Updated{' '} + + + + Skipped{' '} + + + + Deleted{' '} + + + Details + + + + {chatflowUpsertHistory.map((upsertHistory, index) => ( + + ))} + +
+
+ )} + +
+ + + +
+ ) : null + + return createPortal(component, portalElement) +} + +UpsertHistoryDialog.propTypes = { + show: PropTypes.bool, + dialogProps: PropTypes.object, + onCancel: PropTypes.func +} + +export default UpsertHistoryDialog diff --git a/packages/ui/src/views/vectorstore/UpsertResultDialog.jsx b/packages/ui/src/views/vectorstore/UpsertResultDialog.jsx new file mode 100644 index 00000000..4276bd8a --- /dev/null +++ b/packages/ui/src/views/vectorstore/UpsertResultDialog.jsx @@ -0,0 +1,94 @@ +import PropTypes from 'prop-types' +import { createPortal } from 'react-dom' +import { useDispatch, useSelector } from 'react-redux' +import { useEffect } from 'react' +import ReactJson from 'flowise-react-json-view' +import { Typography, Card, CardContent, Button, Dialog, DialogActions, DialogContent, DialogTitle } from '@mui/material' +import StatsCard from '@/ui-component/cards/StatsCard' +import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from '@/store/actions' + +const UpsertResultDialog = ({ show, dialogProps, onCancel }) => { + const portalElement = document.getElementById('portal') + const dispatch = useDispatch() + const customization = useSelector((state) => state.customization) + + useEffect(() => { + if (show) dispatch({ type: SHOW_CANVAS_DIALOG }) + else dispatch({ type: HIDE_CANVAS_DIALOG }) + return () => dispatch({ type: HIDE_CANVAS_DIALOG }) + }, [show, dispatch]) + + const component = show ? ( + + + Upsert Record + + + <> +
+ + + + +
+ {dialogProps.addedDocs && dialogProps.addedDocs.length > 0 && ( + {dialogProps.numAdded} Added Documents + )} + {dialogProps.addedDocs && + dialogProps.addedDocs.length > 0 && + dialogProps.addedDocs.map((docs, index) => { + return ( + + + + {docs.pageContent} + + + + + ) + })} + +
+ + + +
+ ) : null + + return createPortal(component, portalElement) +} + +UpsertResultDialog.propTypes = { + show: PropTypes.bool, + dialogProps: PropTypes.object, + onCancel: PropTypes.func, + onSave: PropTypes.func +} + +export default UpsertResultDialog diff --git a/packages/ui/src/views/vectorstore/VectorStoreDialog.jsx b/packages/ui/src/views/vectorstore/VectorStoreDialog.jsx index 6426a08f..059f0c6e 100644 --- a/packages/ui/src/views/vectorstore/VectorStoreDialog.jsx +++ b/packages/ui/src/views/vectorstore/VectorStoreDialog.jsx @@ -78,7 +78,7 @@ function a11yProps(index) { } } -const VectorStoreDialog = ({ show, dialogProps, onCancel }) => { +const VectorStoreDialog = ({ show, dialogProps, onCancel, onIndexResult }) => { const portalElement = document.getElementById('portal') const { reactFlowInstance } = useContext(flowContext) const dispatch = useDispatch() @@ -276,7 +276,7 @@ query(formData).then((response) => { const onUpsertClicked = async (vectorStoreNode) => { setLoading(true) try { - await vectorstoreApi.upsertVectorStore(dialogProps.chatflowid, { stopNodeId: vectorStoreNode.data.id }) + const res = await vectorstoreApi.upsertVectorStore(dialogProps.chatflowid, { stopNodeId: vectorStoreNode.data.id }) enqueueSnackbar({ message: 'Succesfully upserted vector store. You can start chatting now!', options: { @@ -290,6 +290,7 @@ query(formData).then((response) => { } }) setLoading(false) + if (res && res.data && typeof res.data === 'object') onIndexResult(res.data) } catch (error) { enqueueSnackbar({ message: error.response.data.message, @@ -549,7 +550,8 @@ query(formData).then((response) => { VectorStoreDialog.propTypes = { show: PropTypes.bool, dialogProps: PropTypes.object, - onCancel: PropTypes.func + onCancel: PropTypes.func, + onIndexResult: PropTypes.func } export default VectorStoreDialog diff --git a/packages/ui/src/views/vectorstore/VectorStorePopUp.jsx b/packages/ui/src/views/vectorstore/VectorStorePopUp.jsx index 13cd9043..657ff3b8 100644 --- a/packages/ui/src/views/vectorstore/VectorStorePopUp.jsx +++ b/packages/ui/src/views/vectorstore/VectorStorePopUp.jsx @@ -1,33 +1,19 @@ import { useState, useRef, useEffect } from 'react' -import { useDispatch } from 'react-redux' import PropTypes from 'prop-types' -import { Button } from '@mui/material' import { IconDatabaseImport, IconX } from '@tabler/icons' // project import import { StyledFab } from '@/ui-component/button/StyledFab' import VectorStoreDialog from './VectorStoreDialog' - -// api -import vectorstoreApi from '@/api/vectorstore' - -// Hooks -import useNotifier from '@/utils/useNotifier' - -// Const -import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from '@/store/actions' +import UpsertResultDialog from './UpsertResultDialog' export const VectorStorePopUp = ({ chatflowid }) => { - const dispatch = useDispatch() - - useNotifier() - const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)) - const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) - const [open, setOpen] = useState(false) const [showExpandDialog, setShowExpandDialog] = useState(false) const [expandDialogProps, setExpandDialogProps] = useState({}) + const [showUpsertResultDialog, setShowUpsertResultDialog] = useState(false) + const [upsertResultDialogProps, setUpsertResultDialogProps] = useState({}) const anchorRef = useRef(null) const prevOpen = useRef(open) @@ -43,38 +29,6 @@ export const VectorStorePopUp = ({ chatflowid }) => { setShowExpandDialog(true) } - const onUpsert = async () => { - try { - await vectorstoreApi.upsertVectorStore(chatflowid, {}) - enqueueSnackbar({ - message: 'Succesfully upserted vector store', - options: { - key: new Date().getTime() + Math.random(), - variant: 'success', - action: (key) => ( - - ) - } - }) - } catch (error) { - enqueueSnackbar({ - message: error.response.data.message, - options: { - key: new Date().getTime() + Math.random(), - variant: 'error', - persist: true, - action: (key) => ( - - ) - } - }) - } - } - useEffect(() => { if (prevOpen.current === true && open === false) { anchorRef.current.focus() @@ -100,12 +54,24 @@ export const VectorStorePopUp = ({ chatflowid }) => { { setShowExpandDialog(false) setOpen((prevopen) => !prevopen) }} + onIndexResult={(indexRes) => { + setShowExpandDialog(false) + setShowUpsertResultDialog(true) + setUpsertResultDialogProps({ ...indexRes }) + }} > + { + setShowUpsertResultDialog(false) + setOpen(false) + }} + > ) } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2f9d6aa3..d0e4ccdb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,9 +1,5 @@ lockfileVersion: '6.0' -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - onlyBuiltDependencies: - faiss-node - sqlite3 @@ -34,7 +30,7 @@ importers: version: 8.10.0(eslint@8.57.0) eslint-config-react-app: specifier: ^7.0.1 - version: 7.0.1(@babel/plugin-syntax-flow@7.23.3)(@babel/plugin-transform-react-jsx@7.23.4)(eslint@8.57.0)(jest@27.5.1)(typescript@4.9.5) + version: 7.0.1(@babel/plugin-syntax-flow@7.23.3)(@babel/plugin-transform-react-jsx@7.23.4)(eslint@8.57.0)(typescript@4.9.5) eslint-plugin-jsx-a11y: specifier: ^6.6.1 version: 6.8.0(eslint@8.57.0) @@ -149,6 +145,9 @@ importers: '@langchain/pinecone': specifier: ^0.0.3 version: 0.0.3 + '@langchain/weaviate': + specifier: ^0.0.1 + version: 0.0.1(graphql@16.8.1) '@mistralai/mistralai': specifier: 0.1.3 version: 0.1.3 @@ -187,7 +186,7 @@ importers: version: 4.3.2 axios: specifier: 1.6.2 - version: 1.6.2(debug@4.3.4) + version: 1.6.2 cheerio: specifier: ^1.0.0-rc.12 version: 1.0.0-rc.12 @@ -278,7 +277,7 @@ importers: version: 3.9.2 node-fetch: specifier: ^2.6.11 - version: 2.7.0(encoding@0.1.13) + version: 2.7.0 node-html-markdown: specifier: ^1.3.0 version: 1.3.0 @@ -411,7 +410,7 @@ importers: version: 0.4.1 axios: specifier: 1.6.2 - version: 1.6.2(debug@4.3.4) + version: 1.6.2 content-disposition: specifier: 0.5.4 version: 0.5.4 @@ -586,7 +585,7 @@ importers: version: 4.21.24(@babel/runtime@7.24.0)(@codemirror/autocomplete@6.14.0)(@codemirror/language@6.10.1)(@codemirror/lint@6.5.0)(@codemirror/search@6.5.6)(@codemirror/state@6.4.1)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.25.1)(codemirror@6.0.1)(react-dom@18.2.0)(react@18.2.0) axios: specifier: 1.6.2 - version: 1.6.2(debug@4.3.4) + version: 1.6.2 clsx: specifier: ^1.1.1 version: 1.2.1 @@ -595,10 +594,10 @@ importers: version: 16.4.5 flowise-embed: specifier: latest - version: 1.2.0 + version: 1.2.1 flowise-embed-react: specifier: latest - version: 1.0.2(flowise-embed@1.2.0)(react-dom@18.2.0)(react@18.2.0)(sass@1.71.1)(typescript@4.9.5) + version: 1.0.2(flowise-embed@1.2.1)(react-dom@18.2.0)(react@18.2.0)(sass@1.71.1)(typescript@4.9.5) flowise-react-json-view: specifier: '*' version: 1.21.7(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) @@ -765,7 +764,7 @@ packages: digest-fetch: 1.3.0 form-data-encoder: 1.7.2 formdata-node: 4.4.1 - node-fetch: 2.7.0(encoding@0.1.13) + node-fetch: 2.7.0 web-streams-polyfill: 3.3.3 transitivePeerDependencies: - encoding @@ -781,7 +780,7 @@ packages: digest-fetch: 1.3.0 form-data-encoder: 1.7.2 formdata-node: 4.4.1 - node-fetch: 2.7.0(encoding@0.1.13) + node-fetch: 2.7.0 web-streams-polyfill: 3.3.3 transitivePeerDependencies: - encoding @@ -797,7 +796,7 @@ packages: digest-fetch: 1.3.0 form-data-encoder: 1.7.2 formdata-node: 4.4.1 - node-fetch: 2.7.0(encoding@0.1.13) + node-fetch: 2.7.0 web-streams-polyfill: 3.3.3 transitivePeerDependencies: - encoding @@ -1944,10 +1943,10 @@ packages: '@babel/helpers': 7.24.0 '@babel/parser': 7.24.0 '@babel/template': 7.24.0 - '@babel/traverse': 7.24.0(supports-color@5.5.0) + '@babel/traverse': 7.24.0 '@babel/types': 7.24.0 convert-source-map: 2.0.0 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -2035,7 +2034,7 @@ packages: '@babel/core': 7.24.0 '@babel/helper-compilation-targets': 7.23.6 '@babel/helper-plugin-utils': 7.24.0 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 lodash.debounce: 4.0.8 resolve: 1.22.8 transitivePeerDependencies: @@ -2049,7 +2048,7 @@ packages: '@babel/core': 7.24.0 '@babel/helper-compilation-targets': 7.23.6 '@babel/helper-plugin-utils': 7.24.0 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 lodash.debounce: 4.0.8 resolve: 1.22.8 transitivePeerDependencies: @@ -2172,7 +2171,7 @@ packages: engines: { node: '>=6.9.0' } dependencies: '@babel/template': 7.24.0 - '@babel/traverse': 7.24.0(supports-color@5.5.0) + '@babel/traverse': 7.24.0 '@babel/types': 7.24.0 transitivePeerDependencies: - supports-color @@ -3249,6 +3248,23 @@ packages: '@babel/parser': 7.24.0 '@babel/types': 7.24.0 + /@babel/traverse@7.24.0: + resolution: { integrity: sha512-HfuJlI8qq3dEDmNU5ChzzpZRWq+oxCZQyMzIMEqLho+AQnhMnKQUzH6ydo3RBl/YjPCuk68Y6s0Gx0AeyULiWw== } + engines: { node: '>=6.9.0' } + dependencies: + '@babel/code-frame': 7.23.5 + '@babel/generator': 7.23.6 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-function-name': 7.23.0 + '@babel/helper-hoist-variables': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/parser': 7.24.0 + '@babel/types': 7.24.0 + debug: 4.3.4 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + /@babel/traverse@7.24.0(supports-color@5.5.0): resolution: { integrity: sha512-HfuJlI8qq3dEDmNU5ChzzpZRWq+oxCZQyMzIMEqLho+AQnhMnKQUzH6ydo3RBl/YjPCuk68Y6s0Gx0AeyULiWw== } engines: { node: '>=6.9.0' } @@ -3265,6 +3281,7 @@ packages: globals: 11.12.0 transitivePeerDependencies: - supports-color + dev: false /@babel/types@7.24.0: resolution: { integrity: sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w== } @@ -3595,7 +3612,7 @@ packages: engines: { node: '>=14.0.0' } hasBin: true dependencies: - axios: 1.6.2(debug@4.3.4) + axios: 1.6.2 bson: 6.4.0 winston: 3.12.0 transitivePeerDependencies: @@ -3620,7 +3637,7 @@ packages: resolution: { integrity: sha512-/SXVuVnuU5b4dq8OFY4izG+dmGla185PcoqgK6+AJMpmOeY1QYVNbWtCwvSvoAANN5D/wV+EBU8+x7Vf9EphbA== } engines: { node: '>=16' } dependencies: - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 hpagent: 1.2.0 ms: 2.1.3 secure-json-parse: 2.7.0 @@ -4146,7 +4163,7 @@ packages: engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } dependencies: ajv: 6.12.6 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 espree: 9.6.1 globals: 13.24.0 ignore: 5.3.1 @@ -4365,7 +4382,7 @@ packages: engines: { node: '>=10.10.0' } dependencies: '@humanwhocodes/object-schema': 2.0.2 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -4758,7 +4775,7 @@ packages: '@babel/preset-typescript': 7.18.6(@babel/core@7.24.0) '@babel/runtime': 7.24.0 '@babel/template': 7.24.0 - '@babel/traverse': 7.24.0(supports-color@5.5.0) + '@babel/traverse': 7.24.0 '@babel/types': 7.24.0 '@ladle/react-context': 1.0.1(react-dom@18.2.0)(react@18.2.0) '@vitejs/plugin-react': 3.1.0(vite@4.5.2) @@ -4768,7 +4785,7 @@ packages: classnames: 2.5.1 commander: 9.5.0 cross-spawn: 7.0.3 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 default-browser: 3.1.0 express: 4.18.3 get-port: 6.1.2 @@ -5225,6 +5242,18 @@ packages: uuid: 9.0.1 dev: false + /@langchain/weaviate@0.0.1(graphql@16.8.1): + resolution: { integrity: sha512-Lf6zgTf6i/fsPNlkDxPRLA3LEz2Wwgk6LNe54dByt0oZM4W+N4n5n/gDwojsXAKNEF5alXUv2N6yAOcUuXSbxg== } + engines: { node: '>=18' } + dependencies: + '@langchain/core': 0.1.44 + uuid: 9.0.1 + weaviate-ts-client: 2.1.1(graphql@16.8.1) + transitivePeerDependencies: + - encoding + - graphql + dev: false + /@leichtgewicht/ip-codec@2.0.4: resolution: { integrity: sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A== } dev: true @@ -5272,7 +5301,7 @@ packages: '@types/qs': 6.9.12 form-data: 4.0.0 js-base64: 3.7.7 - node-fetch: 2.7.0(encoding@0.1.13) + node-fetch: 2.7.0 qs: 6.11.2 dev: false @@ -5296,7 +5325,7 @@ packages: detect-libc: 2.0.2 https-proxy-agent: 5.0.1 make-dir: 3.1.0 - node-fetch: 2.7.0(encoding@0.1.13) + node-fetch: 2.7.0 nopt: 5.0.0 npmlog: 5.0.1 rimraf: 3.0.2 @@ -5310,7 +5339,7 @@ packages: /@mistralai/mistralai@0.0.10: resolution: { integrity: sha512-fZOt7A32DcPSff58wTa44pKUBoJBH5toAuzNI9yoM7s5NjTupa1IYcSqqk2LigO8M5EtOEkFsD/XzdyWPnhaRA== } dependencies: - node-fetch: 2.7.0(encoding@0.1.13) + node-fetch: 2.7.0 transitivePeerDependencies: - encoding dev: false @@ -5318,7 +5347,7 @@ packages: /@mistralai/mistralai@0.1.3: resolution: { integrity: sha512-WUHxC2xdeqX9PTXJEqdiNY54vT2ir72WSJrZTTBKRnkfhX6zIfCYA24faRlWjUB5WTpn+wfdGsTMl3ArijlXFA== } dependencies: - node-fetch: 2.7.0(encoding@0.1.13) + node-fetch: 2.7.0 transitivePeerDependencies: - encoding dev: false @@ -5591,7 +5620,7 @@ packages: engines: { node: '>=12' } dependencies: '@types/node-fetch': 2.6.2 - node-fetch: 2.7.0(encoding@0.1.13) + node-fetch: 2.7.0 transitivePeerDependencies: - encoding dev: false @@ -5915,7 +5944,7 @@ packages: dependencies: '@oclif/core': 2.15.0(@types/node@20.11.26)(typescript@4.9.5) chalk: 4.1.2 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 http-call: 5.3.0 lodash.template: 4.5.0 semver: 7.6.0 @@ -6017,7 +6046,7 @@ packages: '@octokit/request-error': 2.1.0 '@octokit/types': 6.41.0 is-plain-object: 5.0.0 - node-fetch: 2.7.0(encoding@0.1.13) + node-fetch: 2.7.0 universal-user-agent: 6.0.1 transitivePeerDependencies: - encoding @@ -6045,7 +6074,7 @@ packages: engines: { node: '>=10' } dependencies: aws4: 1.12.0 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 hpagent: 0.1.2 ms: 2.1.3 secure-json-parse: 2.7.0 @@ -6165,7 +6194,7 @@ packages: typescript: optional: true dependencies: - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 extract-zip: 2.0.1 progress: 2.0.3 proxy-agent: 6.3.0 @@ -8276,7 +8305,7 @@ packages: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/type-utils': 5.62.0(eslint@8.57.0)(typescript@4.9.5) '@typescript-eslint/utils': 5.62.0(eslint@8.57.0)(typescript@4.9.5) - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 eslint: 8.57.0 graphemer: 1.4.0 ignore: 5.3.1 @@ -8314,7 +8343,7 @@ packages: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/types': 5.62.0 '@typescript-eslint/typescript-estree': 5.62.0(typescript@4.9.5) - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 eslint: 8.57.0 typescript: 4.9.5 transitivePeerDependencies: @@ -8341,7 +8370,7 @@ packages: dependencies: '@typescript-eslint/typescript-estree': 5.62.0(typescript@4.9.5) '@typescript-eslint/utils': 5.62.0(eslint@8.57.0)(typescript@4.9.5) - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 eslint: 8.57.0 tsutils: 3.21.0(typescript@4.9.5) typescript: 4.9.5 @@ -8365,7 +8394,7 @@ packages: dependencies: '@typescript-eslint/types': 5.62.0 '@typescript-eslint/visitor-keys': 5.62.0 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 semver: 7.6.0 @@ -8751,7 +8780,7 @@ packages: resolution: { integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== } engines: { node: '>= 6.0.0' } dependencies: - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 transitivePeerDependencies: - supports-color @@ -8759,7 +8788,7 @@ packages: resolution: { integrity: sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg== } engines: { node: '>= 14' } dependencies: - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 transitivePeerDependencies: - supports-color dev: false @@ -9779,6 +9808,16 @@ packages: engines: { node: '>=4' } dev: false + /axios@1.6.2: + resolution: { integrity: sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A== } + dependencies: + follow-redirects: 1.15.5 + form-data: 4.0.0 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + dev: false + /axios@1.6.2(debug@4.3.4): resolution: { integrity: sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A== } dependencies: @@ -9787,11 +9826,12 @@ packages: proxy-from-env: 1.1.0 transitivePeerDependencies: - debug + dev: true /axios@1.6.7: resolution: { integrity: sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA== } dependencies: - follow-redirects: 1.15.5(debug@4.3.4) + follow-redirects: 1.15.5 form-data: 4.0.0 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -11610,7 +11650,7 @@ packages: dependencies: form-data: 4.0.0 js-base64: 3.7.2 - node-fetch: 2.7.0(encoding@0.1.13) + node-fetch: 2.7.0 qs: 6.11.2 url-join: 4.0.1 transitivePeerDependencies: @@ -12016,6 +12056,14 @@ packages: resolution: { integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g== } dev: false + /cross-fetch@3.1.8: + resolution: { integrity: sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg== } + dependencies: + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + dev: false + /cross-fetch@3.1.8(encoding@0.1.13): resolution: { integrity: sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg== } dependencies: @@ -12027,7 +12075,7 @@ packages: /cross-fetch@4.0.0: resolution: { integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g== } dependencies: - node-fetch: 2.7.0(encoding@0.1.13) + node-fetch: 2.7.0 transitivePeerDependencies: - encoding dev: false @@ -12569,6 +12617,16 @@ packages: dependencies: ms: 2.0.0 + /debug@3.2.7: + resolution: { integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== } + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.3 + /debug@3.2.7(supports-color@5.5.0): resolution: { integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== } peerDependencies: @@ -12579,6 +12637,7 @@ packages: dependencies: ms: 2.1.3 supports-color: 5.5.0 + dev: true /debug@3.2.7(supports-color@8.1.1): resolution: { integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== } @@ -12592,6 +12651,17 @@ packages: supports-color: 8.1.1 dev: true + /debug@4.3.4: + resolution: { integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== } + engines: { node: '>=6.0' } + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + /debug@4.3.4(supports-color@5.5.0): resolution: { integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== } engines: { node: '>=6.0' } @@ -12603,6 +12673,7 @@ packages: dependencies: ms: 2.1.2 supports-color: 5.5.0 + dev: false /debug@4.3.4(supports-color@8.1.1): resolution: { integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== } @@ -13274,7 +13345,7 @@ packages: resolution: { integrity: sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q== } dependencies: '@socket.io/component-emitter': 3.1.0 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 engine.io-parser: 5.2.2 ws: 8.11.0 xmlhttprequest-ssl: 2.0.0 @@ -13300,7 +13371,7 @@ packages: base64id: 2.0.0 cookie: 0.4.2 cors: 2.8.5 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 engine.io-parser: 5.2.2 ws: 8.11.0 transitivePeerDependencies: @@ -13686,10 +13757,45 @@ packages: - supports-color dev: true + /eslint-config-react-app@7.0.1(@babel/plugin-syntax-flow@7.23.3)(@babel/plugin-transform-react-jsx@7.23.4)(eslint@8.57.0)(typescript@4.9.5): + resolution: { integrity: sha512-K6rNzvkIeHaTd8m/QEh1Zko0KI7BACWkkneSs6s9cKZC/J27X3eZR6Upt1jkmZ/4FK+XUOPPxMEN7+lbUXfSlA== } + engines: { node: '>=14.0.0' } + peerDependencies: + eslint: ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@babel/core': 7.24.0 + '@babel/eslint-parser': 7.23.10(@babel/core@7.24.0)(eslint@8.57.0) + '@rushstack/eslint-patch': 1.7.2 + '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.57.0)(typescript@4.9.5) + '@typescript-eslint/parser': 5.62.0(eslint@8.57.0)(typescript@4.9.5) + babel-preset-react-app: 10.0.1 + confusing-browser-globals: 1.0.11 + eslint: 8.57.0 + eslint-plugin-flowtype: 8.0.3(@babel/plugin-syntax-flow@7.23.3)(@babel/plugin-transform-react-jsx@7.23.4)(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.62.0)(eslint@8.57.0) + eslint-plugin-jest: 25.7.0(@typescript-eslint/eslint-plugin@5.62.0)(eslint@8.57.0)(typescript@4.9.5) + eslint-plugin-jsx-a11y: 6.8.0(eslint@8.57.0) + eslint-plugin-react: 7.34.0(eslint@8.57.0) + eslint-plugin-react-hooks: 4.6.0(eslint@8.57.0) + eslint-plugin-testing-library: 5.11.1(eslint@8.57.0)(typescript@4.9.5) + typescript: 4.9.5 + transitivePeerDependencies: + - '@babel/plugin-syntax-flow' + - '@babel/plugin-transform-react-jsx' + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - jest + - supports-color + dev: true + /eslint-import-resolver-node@0.3.9: resolution: { integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g== } dependencies: - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7 is-core-module: 2.13.1 resolve: 1.22.8 transitivePeerDependencies: @@ -13718,7 +13824,7 @@ packages: optional: true dependencies: '@typescript-eslint/parser': 5.62.0(eslint@8.57.0)(typescript@4.9.5) - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 transitivePeerDependencies: @@ -13755,7 +13861,7 @@ packages: array.prototype.findlastindex: 1.2.4 array.prototype.flat: 1.3.2 array.prototype.flatmap: 1.3.2 - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7 doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 @@ -13797,6 +13903,27 @@ packages: - typescript dev: true + /eslint-plugin-jest@25.7.0(@typescript-eslint/eslint-plugin@5.62.0)(eslint@8.57.0)(typescript@4.9.5): + resolution: { integrity: sha512-PWLUEXeeF7C9QGKqvdSbzLOiLTx+bno7/HC9eefePfEb257QFHg7ye3dh80AZVkaa/RQsBB1Q/ORQvg2X7F0NQ== } + engines: { node: ^12.13.0 || ^14.15.0 || >=16.0.0 } + peerDependencies: + '@typescript-eslint/eslint-plugin': ^4.0.0 || ^5.0.0 + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + jest: '*' + peerDependenciesMeta: + '@typescript-eslint/eslint-plugin': + optional: true + jest: + optional: true + dependencies: + '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.57.0)(typescript@4.9.5) + '@typescript-eslint/experimental-utils': 5.62.0(eslint@8.57.0)(typescript@4.9.5) + eslint: 8.57.0 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + /eslint-plugin-jsx-a11y@6.8.0(eslint@8.57.0): resolution: { integrity: sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA== } engines: { node: '>=4.0' } @@ -13976,7 +14103,7 @@ packages: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -14449,7 +14576,7 @@ packages: engines: { node: '>= 10.17.0' } hasBin: true dependencies: - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 get-stream: 5.2.0 yauzl: 2.10.0 optionalDependencies: @@ -14608,7 +14735,7 @@ packages: /fbjs@3.0.5: resolution: { integrity: sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg== } dependencies: - cross-fetch: 3.1.8(encoding@0.1.13) + cross-fetch: 3.1.8 fbjs-css-vars: 1.0.2 loose-envify: 1.4.0 object-assign: 4.1.1 @@ -14920,14 +15047,14 @@ packages: resolution: { integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== } dev: true - /flowise-embed-react@1.0.2(flowise-embed@1.2.0)(react-dom@18.2.0)(react@18.2.0)(sass@1.71.1)(typescript@4.9.5): + /flowise-embed-react@1.0.2(flowise-embed@1.2.1)(react-dom@18.2.0)(react@18.2.0)(sass@1.71.1)(typescript@4.9.5): resolution: { integrity: sha512-M6rDofJWTWI9rtZN7G3oTlqAcQaHoF/IUIoW1YitHsjKly24awq5sky+0Wfkcg4VfoXz3SiLHMZ/XOF4PDuvqA== } peerDependencies: flowise-embed: '*' react: 18.x dependencies: '@ladle/react': 2.5.1(react-dom@18.2.0)(react@18.2.0)(sass@1.71.1)(typescript@4.9.5) - flowise-embed: 1.2.0 + flowise-embed: 1.2.1 react: 18.2.0 transitivePeerDependencies: - '@types/node' @@ -14942,8 +15069,8 @@ packages: - typescript dev: false - /flowise-embed@1.2.0: - resolution: { integrity: sha512-WGVH/ix6fvb+bOvp8ugxgC9raQvF7HuhqTDasFmltkrDhDySXeyOiZOj5eUs5nOkk9WT4ZaDznjSeBGDXOiShQ== } + /flowise-embed@1.2.1: + resolution: { integrity: sha512-sn8An0dseo398RXx/yzQ3lC//+FpY5o/094yaWZs3NmYy6am+duCZJ18lhcYF5A4oCucrIoZCcCz5mxViYtznw== } dependencies: '@babel/core': 7.24.0 '@ts-stack/markdown': 1.5.0 @@ -14998,6 +15125,15 @@ packages: resolution: { integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw== } dev: false + /follow-redirects@1.15.5: + resolution: { integrity: sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw== } + engines: { node: '>=4.0' } + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + /follow-redirects@1.15.5(debug@4.3.4): resolution: { integrity: sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw== } engines: { node: '>=4.0' } @@ -15007,7 +15143,8 @@ packages: debug: optional: true dependencies: - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 + dev: true /for-each@0.3.3: resolution: { integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== } @@ -15350,7 +15487,7 @@ packages: extend: 3.0.2 https-proxy-agent: 5.0.1 is-stream: 2.0.1 - node-fetch: 2.7.0(encoding@0.1.13) + node-fetch: 2.7.0 transitivePeerDependencies: - encoding - supports-color @@ -15363,7 +15500,7 @@ packages: extend: 3.0.2 https-proxy-agent: 7.0.4 is-stream: 2.0.1 - node-fetch: 2.7.0(encoding@0.1.13) + node-fetch: 2.7.0 transitivePeerDependencies: - encoding - supports-color @@ -15471,7 +15608,7 @@ packages: dependencies: basic-ftp: 5.0.5 data-uri-to-buffer: 6.0.2 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 fs-extra: 11.2.0 transitivePeerDependencies: - supports-color @@ -15839,7 +15976,7 @@ packages: fast-text-encoding: 1.0.6 google-auth-library: 8.9.0 is-stream-ended: 0.1.4 - node-fetch: 2.7.0(encoding@0.1.13) + node-fetch: 2.7.0 object-hash: 3.0.0 proto3-json-serializer: 1.1.1 protobufjs: 7.2.4 @@ -15897,7 +16034,7 @@ packages: graphql: 14 - 16 dependencies: '@graphql-typed-document-node/core': 3.2.0(graphql@16.8.1) - cross-fetch: 3.1.8(encoding@0.1.13) + cross-fetch: 3.1.8 extract-files: 9.0.0 form-data: 3.0.1 graphql: 16.8.1 @@ -15930,7 +16067,7 @@ packages: digest-fetch: 1.3.0 form-data-encoder: 1.7.2 formdata-node: 4.4.1 - node-fetch: 2.7.0(encoding@0.1.13) + node-fetch: 2.7.0 web-streams-polyfill: 3.3.3 transitivePeerDependencies: - encoding @@ -16497,7 +16634,7 @@ packages: engines: { node: '>=8.0.0' } dependencies: content-type: 1.0.5 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 is-retry-allowed: 1.2.0 is-stream: 2.0.1 parse-json: 4.0.0 @@ -16540,7 +16677,7 @@ packages: dependencies: '@tootallnate/once': 1.1.2 agent-base: 6.0.2 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 transitivePeerDependencies: - supports-color @@ -16550,7 +16687,7 @@ packages: dependencies: '@tootallnate/once': 2.0.0 agent-base: 6.0.2 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 transitivePeerDependencies: - supports-color @@ -16559,7 +16696,7 @@ packages: engines: { node: '>= 14' } dependencies: agent-base: 7.1.0 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 transitivePeerDependencies: - supports-color dev: false @@ -16588,7 +16725,7 @@ packages: engines: { node: '>=8.0.0' } dependencies: eventemitter3: 4.0.7 - follow-redirects: 1.15.5(debug@4.3.4) + follow-redirects: 1.15.5 requires-port: 1.0.0 transitivePeerDependencies: - debug @@ -16620,7 +16757,7 @@ packages: engines: { node: '>= 6' } dependencies: agent-base: 6.0.2 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 transitivePeerDependencies: - supports-color @@ -16629,7 +16766,7 @@ packages: engines: { node: '>= 14' } dependencies: agent-base: 7.1.0 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 transitivePeerDependencies: - supports-color dev: false @@ -16873,7 +17010,7 @@ packages: dependencies: '@ioredis/commands': 1.2.0 cluster-key-slot: 1.1.2 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 denque: 2.1.0 lodash.defaults: 4.2.0 lodash.isarguments: 3.1.0 @@ -17560,7 +17697,7 @@ packages: /isomorphic-fetch@3.0.0: resolution: { integrity: sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA== } dependencies: - node-fetch: 2.7.0(encoding@0.1.13) + node-fetch: 2.7.0 whatwg-fetch: 3.6.20 transitivePeerDependencies: - encoding @@ -17601,7 +17738,7 @@ packages: resolution: { integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw== } engines: { node: '>=10' } dependencies: - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 istanbul-lib-coverage: 3.2.2 source-map: 0.6.1 transitivePeerDependencies: @@ -18080,7 +18217,7 @@ packages: '@babel/core': 7.24.0 '@babel/generator': 7.23.6 '@babel/plugin-syntax-typescript': 7.23.3(@babel/core@7.24.0) - '@babel/traverse': 7.24.0(supports-color@5.5.0) + '@babel/traverse': 7.24.0 '@babel/types': 7.24.0 '@jest/transform': 27.5.1 '@jest/types': 27.5.1 @@ -18874,7 +19011,7 @@ packages: '@supabase/supabase-js': 2.39.8 apify-client: 2.9.3 assemblyai: 4.3.2 - axios: 1.6.2(debug@4.3.4) + axios: 1.6.2 binary-extensions: 2.2.0 cheerio: 1.0.0-rc.12 chromadb: 1.8.1(@google/generative-ai@0.1.3)(cohere-ai@6.2.2)(openai@4.28.4) @@ -19179,7 +19316,7 @@ packages: dependencies: chalk: 5.3.0 commander: 11.0.0 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 execa: 7.2.0 lilconfig: 2.1.0 listr2: 6.6.1 @@ -20299,6 +20436,27 @@ packages: resolution: { integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== } engines: { node: '>= 0.6' } + /mem-fs-editor@9.7.0: + resolution: { integrity: sha512-ReB3YD24GNykmu4WeUL/FDIQtkoyGB6zfJv60yfCo3QjKeimNcTqv2FT83bP0ccs6uu+sm5zyoBlspAzigmsdg== } + engines: { node: '>=12.10.0' } + peerDependencies: + mem-fs: ^2.1.0 + peerDependenciesMeta: + mem-fs: + optional: true + dependencies: + binaryextensions: 4.19.0 + commondir: 1.0.1 + deep-extend: 0.6.0 + ejs: 3.1.9 + globby: 11.1.0 + isbinaryfile: 5.0.2 + minimatch: 7.4.6 + multimatch: 5.0.0 + normalize-path: 3.0.0 + textextensions: 5.16.0 + dev: true + /mem-fs-editor@9.7.0(mem-fs@2.3.0): resolution: { integrity: sha512-ReB3YD24GNykmu4WeUL/FDIQtkoyGB6zfJv60yfCo3QjKeimNcTqv2FT83bP0ccs6uu+sm5zyoBlspAzigmsdg== } engines: { node: '>=12.10.0' } @@ -20656,7 +20814,7 @@ packages: /micromark@2.11.4: resolution: { integrity: sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA== } dependencies: - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 parse-entities: 2.0.0 transitivePeerDependencies: - supports-color @@ -20666,7 +20824,7 @@ packages: resolution: { integrity: sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA== } dependencies: '@types/debug': 4.1.12 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 decode-named-character-reference: 1.0.2 micromark-core-commonmark: 1.1.0 micromark-factory-space: 1.1.0 @@ -21248,6 +21406,17 @@ packages: resolution: { integrity: sha512-DRI60hzo2oKN1ma0ckc6nQWlHU69RH6xN0sjQTjMpChPfTYvKZdcQFfdYK2RWbJcKyUizSIy/l8OTGxMAM1QDw== } dev: false + /node-fetch@2.7.0: + resolution: { integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== } + engines: { node: 4.x || >=6.0.0 } + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + dependencies: + whatwg-url: 5.0.0 + /node-fetch@2.7.0(encoding@0.1.13): resolution: { integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== } engines: { node: 4.x || >=6.0.0 } @@ -21259,6 +21428,7 @@ packages: dependencies: encoding: 0.1.13 whatwg-url: 5.0.0 + dev: false /node-forge@1.3.1: resolution: { integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== } @@ -21459,7 +21629,7 @@ packages: engines: { node: '>=12' } dependencies: markdown-table: 2.0.0 - node-fetch: 2.7.0(encoding@0.1.13) + node-fetch: 2.7.0 transitivePeerDependencies: - encoding dev: false @@ -21861,7 +22031,7 @@ packages: async-retry: 1.3.3 aws-sdk: 2.1575.0 concurrently: 7.6.0 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 find-yarn-workspace-root: 2.0.0 fs-extra: 8.1.0 github-slugger: 1.5.0 @@ -21997,7 +22167,7 @@ packages: digest-fetch: 1.3.0 form-data-encoder: 1.7.2 formdata-node: 4.4.1 - node-fetch: 2.7.0(encoding@0.1.13) + node-fetch: 2.7.0 web-streams-polyfill: 3.3.3 transitivePeerDependencies: - encoding @@ -22182,7 +22352,7 @@ packages: resolution: { integrity: sha512-UJKdSzgd3KOnXXAtqN5+/eeHcvTn1hBkesEmElVgvO/NAYcxAvmjzIGmnNd3Tb/gRAvMBdNRFD4qAWdHxY6QXg== } engines: { node: '>=12.10.0' } dependencies: - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 p-queue: 6.6.2 transitivePeerDependencies: - supports-color @@ -22199,7 +22369,7 @@ packages: dependencies: '@tootallnate/quickjs-emscripten': 0.23.0 agent-base: 7.1.0 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 get-uri: 6.0.3 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.4 @@ -22586,7 +22756,7 @@ packages: resolution: { integrity: sha512-v6ZJ/efsBpGrGGknjtq9J/oC8tZWq0KWL5vQrk2GlzLEQPUDB1ex+13Rmidl1neNN358Jn9EHZw5y07FFtaC7A== } engines: { node: '>=6.8.1' } dependencies: - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7 node-ensure: 0.0.0 transitivePeerDependencies: - supports-color @@ -22596,6 +22766,8 @@ packages: resolution: { integrity: sha512-Un1yLbSlk/zfwrltgguskExIioXZlFSFwsyXU0cnBorLywbTbcdzmJJEebh+U2cFCtR7y8nDs5lPHAe7ldxjZg== } engines: { node: '>=18.12.1', npm: '>=8.19.2' } hasBin: true + dependencies: + '@xmldom/xmldom': 0.8.10 dev: false bundledDependencies: - '@xmldom/xmldom' @@ -23649,7 +23821,7 @@ packages: resolution: { integrity: sha512-JB+ei0LkwE+rKHyW5z79Nd1jUaGxU6TvkfjFqY9vQaHxU5aU8dRl0UUaEmZdZbHwjp3WmXCBQQRNyimwbNQfCw== } engines: { node: '>=15.0.0' } dependencies: - axios: 1.6.2(debug@4.3.4) + axios: 1.6.2 rusha: 0.8.14 transitivePeerDependencies: - debug @@ -24015,7 +24187,7 @@ packages: engines: { node: '>= 14' } dependencies: agent-base: 7.1.0 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.4 lru-cache: 7.18.3 @@ -24093,7 +24265,7 @@ packages: '@puppeteer/browsers': 1.4.6(typescript@4.9.5) chromium-bidi: 0.4.16(devtools-protocol@0.0.1147663) cross-fetch: 4.0.0 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 devtools-protocol: 0.0.1147663 typescript: 4.9.5 ws: 8.13.0 @@ -25540,7 +25712,7 @@ packages: resolution: { integrity: sha512-wfI3pk7EE80lCIXprqh7ym48IHYdwmAAzESdbU8Q9l7pnRCk9LEhpbOTNKjz6FARLm/Bl5m+4F0ABxOkYUujSQ== } engines: { node: '>=12' } dependencies: - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 extend: 3.0.2 transitivePeerDependencies: - supports-color @@ -26283,7 +26455,7 @@ packages: /socket.io-adapter@2.5.4: resolution: { integrity: sha512-wDNHGXGewWAjQPt3pyeYBtpWSq9cLE5UW1ZUPL/2eGK9jtse/FpXib7epSTsz0Q0m+6sg6Y4KtcFTlah1bdOVg== } dependencies: - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 ws: 8.11.0 transitivePeerDependencies: - bufferutil @@ -26296,7 +26468,7 @@ packages: engines: { node: '>=10.0.0' } dependencies: '@socket.io/component-emitter': 3.1.0 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 engine.io-client: 6.5.3 socket.io-parser: 4.2.4 transitivePeerDependencies: @@ -26310,7 +26482,7 @@ packages: engines: { node: '>=10.0.0' } dependencies: '@socket.io/component-emitter': 3.1.0 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 transitivePeerDependencies: - supports-color dev: false @@ -26322,7 +26494,7 @@ packages: accepts: 1.3.8 base64id: 2.0.0 cors: 2.8.5 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 engine.io: 6.5.4 socket.io-adapter: 2.5.4 socket.io-parser: 4.2.4 @@ -26345,7 +26517,7 @@ packages: engines: { node: '>= 10' } dependencies: agent-base: 6.0.2 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 socks: 2.8.1 transitivePeerDependencies: - supports-color @@ -26355,7 +26527,7 @@ packages: engines: { node: '>= 10' } dependencies: agent-base: 6.0.2 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 socks: 2.8.1 transitivePeerDependencies: - supports-color @@ -26366,7 +26538,7 @@ packages: engines: { node: '>= 14' } dependencies: agent-base: 7.1.0 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 socks: 2.8.1 transitivePeerDependencies: - supports-color @@ -26531,7 +26703,7 @@ packages: /spdy-transport@3.0.0: resolution: { integrity: sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw== } dependencies: - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 detect-node: 2.1.0 hpack.js: 2.1.6 obuf: 1.1.2 @@ -26545,7 +26717,7 @@ packages: resolution: { integrity: sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA== } engines: { node: '>=6.0.0' } dependencies: - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 handle-thing: 2.0.1 http-deceiver: 1.2.7 select-hose: 2.0.0 @@ -26702,7 +26874,7 @@ packages: arg: 5.0.2 bluebird: 3.7.2 check-more-types: 2.24.0 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 execa: 5.1.1 lazy-ass: 1.6.0 ps-tree: 1.2.0 @@ -27831,7 +28003,7 @@ packages: engines: { node: ^14.17.0 || ^16.13.0 || >=18.0.0 } dependencies: '@tufjs/models': 1.0.4 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 make-fetch-happen: 11.1.1 transitivePeerDependencies: - supports-color @@ -28085,7 +28257,7 @@ packages: chalk: 4.1.2 cli-highlight: 2.1.11 dayjs: 1.11.10 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 dotenv: 16.4.5 glob: 10.3.10 ioredis: 5.3.2 @@ -28167,7 +28339,7 @@ packages: chalk: 4.1.2 cli-highlight: 2.1.11 dayjs: 1.11.10 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 dotenv: 16.4.5 glob: 10.3.10 mkdirp: 2.1.6 @@ -28958,7 +29130,7 @@ packages: workbox-build: ^7.0.0 workbox-window: ^7.0.0 dependencies: - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 fast-glob: 3.3.2 pretty-bytes: 6.1.1 vite: 5.1.6(sass@1.71.1) @@ -28985,7 +29157,7 @@ packages: vite: optional: true dependencies: - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 globrex: 0.1.2 tsconfck: 3.0.3(typescript@4.9.5) vite: 4.5.2(sass@1.71.1) @@ -29168,6 +29340,17 @@ packages: - graphql dev: false + /weaviate-ts-client@2.1.1(graphql@16.8.1): + resolution: { integrity: sha512-d8yc2KnIEIV1beHAU8mhrElT3BoROoXGDsLlqFX8QGx3G+gOiPTRMc7SLy4F17+LvaUaTD0XkHvWX++4iehnsg== } + engines: { node: '>=16.0.0' } + dependencies: + graphql-request: 5.2.0(graphql@16.8.1) + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - graphql + dev: false + /web-namespaces@2.0.1: resolution: { integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ== } dev: false @@ -29735,7 +29918,6 @@ packages: /workbox-google-analytics@7.0.0: resolution: { integrity: sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg== } - deprecated: It is not compatible with newer versions of GA starting with v4, as long as you are using GAv3 it should be ok, but the package is not longer being maintained dependencies: workbox-background-sync: 7.0.0 workbox-core: 7.0.0 @@ -30184,7 +30366,7 @@ packages: cli-table: 0.3.11 commander: 7.1.0 dateformat: 4.6.3 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 diff: 5.2.0 error: 10.4.0 escape-string-regexp: 4.0.0 @@ -30229,11 +30411,11 @@ packages: dependencies: chalk: 4.1.2 dargs: 7.0.0 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 execa: 5.1.1 github-username: 6.0.0 lodash: 4.17.21 - mem-fs-editor: 9.7.0(mem-fs@2.3.0) + mem-fs-editor: 9.7.0 minimist: 1.2.8 pacote: 15.2.0 read-pkg-up: 7.0.1 @@ -30307,3 +30489,7 @@ packages: /zwitch@2.0.4: resolution: { integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A== } dev: false + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false