From 6ccac3a5447ae2ce10a0f338bf601c749236b352 Mon Sep 17 00:00:00 2001 From: Henry Date: Tue, 7 Nov 2023 21:23:57 +0000 Subject: [PATCH 01/64] =?UTF-8?q?=F0=9F=A5=B3=20flowise@1.4.0-rc.1=20relea?= =?UTF-8?q?se?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- packages/server/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index e894a437..523f615a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "flowise", - "version": "1.3.9", + "version": "1.4.0-rc.1", "private": true, "homepage": "https://flowiseai.com", "workspaces": [ diff --git a/packages/server/package.json b/packages/server/package.json index 0a722683..12d8bfbb 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "flowise", - "version": "1.3.9", + "version": "1.4.0-rc.1", "description": "Flowiseai Server", "main": "dist/index", "types": "dist/index.d.ts", From 220db3944da05b88d2fff77f736a318e319df2a4 Mon Sep 17 00:00:00 2001 From: Henry Date: Tue, 7 Nov 2023 21:24:18 +0000 Subject: [PATCH 02/64] =?UTF-8?q?=F0=9F=A5=B3=20flowise-components@1.4.0-r?= =?UTF-8?q?c.1=20release?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/components/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/package.json b/packages/components/package.json index cc2e5227..de5f264d 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "flowise-components", - "version": "1.3.11", + "version": "1.4.0-rc.1", "description": "Flowiseai Components", "main": "dist/src/index", "types": "dist/src/index.d.ts", From be19cc65814f3c8ee86eeea585ce960b46eb482f Mon Sep 17 00:00:00 2001 From: Henry Date: Tue, 7 Nov 2023 21:24:36 +0000 Subject: [PATCH 03/64] =?UTF-8?q?=F0=9F=A5=B3=20flowise-ui@1.4.0-rc.1=20re?= =?UTF-8?q?lease?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/ui/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/package.json b/packages/ui/package.json index 7205f33c..ab0b2740 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "flowise-ui", - "version": "1.3.7", + "version": "1.4.0-rc.1", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://flowiseai.com", "author": { From 964b5a263c2f9f1ec20a0333784c19c8681a40f9 Mon Sep 17 00:00:00 2001 From: vincent-pli Date: Wed, 8 Nov 2023 08:20:07 +0800 Subject: [PATCH 04/64] correct the help link for apichain --- packages/components/nodes/chains/ApiChain/GETApiChain.ts | 2 +- packages/components/nodes/chains/ApiChain/POSTApiChain.ts | 2 +- packages/server/marketplaces/chatflows/API Agent.json | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/components/nodes/chains/ApiChain/GETApiChain.ts b/packages/components/nodes/chains/ApiChain/GETApiChain.ts index 89aa2f61..d88ab6f9 100644 --- a/packages/components/nodes/chains/ApiChain/GETApiChain.ts +++ b/packages/components/nodes/chains/ApiChain/GETApiChain.ts @@ -47,7 +47,7 @@ class GETApiChain_Chains implements INode { name: 'apiDocs', type: 'string', description: - 'Description of how API works. Please refer to more examples', + 'Description of how API works. Please refer to more examples', rows: 4 }, { diff --git a/packages/components/nodes/chains/ApiChain/POSTApiChain.ts b/packages/components/nodes/chains/ApiChain/POSTApiChain.ts index 70277ee9..f005f47a 100644 --- a/packages/components/nodes/chains/ApiChain/POSTApiChain.ts +++ b/packages/components/nodes/chains/ApiChain/POSTApiChain.ts @@ -36,7 +36,7 @@ class POSTApiChain_Chains implements INode { name: 'apiDocs', type: 'string', description: - 'Description of how API works. Please refer to more examples', + 'Description of how API works. Please refer to more examples', rows: 4 }, { diff --git a/packages/server/marketplaces/chatflows/API Agent.json b/packages/server/marketplaces/chatflows/API Agent.json index c37aaff2..26c9b340 100644 --- a/packages/server/marketplaces/chatflows/API Agent.json +++ b/packages/server/marketplaces/chatflows/API Agent.json @@ -24,7 +24,7 @@ "label": "API Documentation", "name": "apiDocs", "type": "string", - "description": "Description of how API works. Please refer to more examples", + "description": "Description of how API works. Please refer to more examples", "rows": 4, "id": "getApiChain_0-input-apiDocs-string" }, @@ -318,7 +318,7 @@ "label": "API Documentation", "name": "apiDocs", "type": "string", - "description": "Description of how API works. Please refer to more examples", + "description": "Description of how API works. Please refer to more examples", "rows": 4, "id": "postApiChain_0-input-apiDocs-string" }, From c7fcc90fbeb183b9b5c3386b4905675c227fd3df Mon Sep 17 00:00:00 2001 From: Henry Date: Thu, 9 Nov 2023 13:55:21 +0000 Subject: [PATCH 05/64] update flowise-ui and components package json --- packages/server/package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/server/package.json b/packages/server/package.json index ab48decd..4470b41b 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "flowise", - "version": "1.4.0-rc.1", + "version": "1.4.0", "description": "Flowiseai Server", "main": "dist/index", "types": "dist/index.d.ts", @@ -54,8 +54,8 @@ "express": "^4.17.3", "express-basic-auth": "^1.2.1", "express-rate-limit": "^6.9.0", - "flowise-components": "1.4.0-rc.1", - "flowise-ui": "1.4.0-rc.1", + "flowise-components": "*", + "flowise-ui": "*", "moment-timezone": "^0.5.34", "multer": "^1.4.5-lts.1", "mysql": "^2.18.1", From 4db3ca76504c0b36b82b46751c3d2f5fb5be43fb Mon Sep 17 00:00:00 2001 From: Henry Date: Thu, 9 Nov 2023 14:00:37 +0000 Subject: [PATCH 06/64] =?UTF-8?q?=F0=9F=A5=B3=20flowise-components@1.4.0?= =?UTF-8?q?=20release?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/components/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/package.json b/packages/components/package.json index 8c7c6703..5a5dd7da 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "flowise-components", - "version": "1.4.0-rc.1", + "version": "1.4.0", "description": "Flowiseai Components", "main": "dist/src/index", "types": "dist/src/index.d.ts", From 0d496c2792c73297fc5b88b520b96c795492eda3 Mon Sep 17 00:00:00 2001 From: Henry Date: Thu, 9 Nov 2023 14:00:57 +0000 Subject: [PATCH 07/64] =?UTF-8?q?=F0=9F=A5=B3=20flowise-ui@1.4.0=20release?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/ui/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/package.json b/packages/ui/package.json index ab0b2740..6914c04a 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "flowise-ui", - "version": "1.4.0-rc.1", + "version": "1.4.0", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://flowiseai.com", "author": { From 4b483c09545193c6410a815aa6ef230bc71bbf6e Mon Sep 17 00:00:00 2001 From: Henry Date: Thu, 9 Nov 2023 14:01:17 +0000 Subject: [PATCH 08/64] =?UTF-8?q?=F0=9F=A5=B3=20flowise@1.4.0=20release?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 523f615a..23969e30 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "flowise", - "version": "1.4.0-rc.1", + "version": "1.4.0", "private": true, "homepage": "https://flowiseai.com", "workspaces": [ From 0a84c2e093d48bea0d2cd9966105f0450f0ff3f4 Mon Sep 17 00:00:00 2001 From: Henry Date: Thu, 9 Nov 2023 16:47:34 +0000 Subject: [PATCH 09/64] =?UTF-8?q?=F0=9F=A5=B3=20flowise@1.4.1=20minor=20bu?= =?UTF-8?q?gfix=20release?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- packages/server/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 23969e30..6c340418 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "flowise", - "version": "1.4.0", + "version": "1.4.1", "private": true, "homepage": "https://flowiseai.com", "workspaces": [ diff --git a/packages/server/package.json b/packages/server/package.json index 4470b41b..f87fbcc5 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "flowise", - "version": "1.4.0", + "version": "1.4.1", "description": "Flowiseai Server", "main": "dist/index", "types": "dist/index.d.ts", From 0ad110658c1476aa811d751865b97f177059b754 Mon Sep 17 00:00:00 2001 From: Henry Date: Thu, 9 Nov 2023 16:48:47 +0000 Subject: [PATCH 10/64] =?UTF-8?q?=F0=9F=A5=B3=20flowise-components@1.4.1?= =?UTF-8?q?=20minor=20bugfix=20release?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/components/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/package.json b/packages/components/package.json index 5a5dd7da..5bc9edd5 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "flowise-components", - "version": "1.4.0", + "version": "1.4.1", "description": "Flowiseai Components", "main": "dist/src/index", "types": "dist/src/index.d.ts", From 4baba784c5bd5805c772a6b9e9117f7e165fcf3b Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 10 Nov 2023 16:42:22 +0000 Subject: [PATCH 11/64] =?UTF-8?q?=F0=9F=A5=B3=20flowise@1.4.2=20minor=20bu?= =?UTF-8?q?gfix=20release?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- packages/server/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 6c340418..909eb345 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "flowise", - "version": "1.4.1", + "version": "1.4.2", "private": true, "homepage": "https://flowiseai.com", "workspaces": [ diff --git a/packages/server/package.json b/packages/server/package.json index f87fbcc5..3503e982 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "flowise", - "version": "1.4.1", + "version": "1.4.2", "description": "Flowiseai Server", "main": "dist/index", "types": "dist/index.d.ts", From a311e024e1eae56e656e8774a0747d479addb27a Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 10 Nov 2023 16:43:54 +0000 Subject: [PATCH 12/64] =?UTF-8?q?=F0=9F=A5=B3=20flowise-components@1.4.2?= =?UTF-8?q?=20minor=20bugfix=20release?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/components/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/package.json b/packages/components/package.json index 5bc9edd5..996419ca 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "flowise-components", - "version": "1.4.1", + "version": "1.4.2", "description": "Flowiseai Components", "main": "dist/src/index", "types": "dist/src/index.d.ts", From 1f0cc60fccb993e2d632e17faa42d579eaba6881 Mon Sep 17 00:00:00 2001 From: automaton82 Date: Fri, 10 Nov 2023 15:31:13 -0500 Subject: [PATCH 13/64] Adding ability to hide node param from UI --- packages/components/src/Interface.ts | 1 + packages/ui/src/views/canvas/CanvasNode.js | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/components/src/Interface.ts b/packages/components/src/Interface.ts index 15b98770..c56d8a5f 100644 --- a/packages/components/src/Interface.ts +++ b/packages/components/src/Interface.ts @@ -72,6 +72,7 @@ export interface INodeParams { fileType?: string additionalParams?: boolean loadMethod?: string + hidden?: boolean } export interface INodeExecutionData { diff --git a/packages/ui/src/views/canvas/CanvasNode.js b/packages/ui/src/views/canvas/CanvasNode.js index cabe2329..cfc51fe4 100644 --- a/packages/ui/src/views/canvas/CanvasNode.js +++ b/packages/ui/src/views/canvas/CanvasNode.js @@ -207,9 +207,11 @@ const CanvasNode = ({ data }) => { {data.inputAnchors.map((inputAnchor, index) => ( ))} - {data.inputParams.map((inputParam, index) => ( - - ))} + {data.inputParams + .filter((inputParam) => !inputParam.hidden) + .map((inputParam, index) => ( + + ))} {data.inputParams.find((param) => param.additionalParams) && (
Date: Mon, 13 Nov 2023 10:48:24 +0530 Subject: [PATCH 14/64] ResponsibleAI - Input Moderation - Ability to verify inputs and prevent potentially harmful content generation --- .../nodes/chains/LLMChain/LLMChain.ts | 21 ++- .../InputModeration/OpenAIModeration.ts | 154 ++++++++++++++++++ .../InputModeration/OpenAIModerationRunner.ts | 62 +++++++ .../InputModeration/SimplePromptModeration.ts | 56 +++++++ .../SimplePromptModerationRunner.ts | 24 +++ .../InputModeration/openai-moderation.png | Bin 0 -> 47939 bytes .../InputModeration/simple_moderation.png | Bin 0 -> 44837 bytes .../nodes/responsibleAI/ResponsibleAI.ts | 30 ++++ 8 files changed, 346 insertions(+), 1 deletion(-) create mode 100644 packages/components/nodes/responsibleAI/InputModeration/OpenAIModeration.ts create mode 100644 packages/components/nodes/responsibleAI/InputModeration/OpenAIModerationRunner.ts create mode 100644 packages/components/nodes/responsibleAI/InputModeration/SimplePromptModeration.ts create mode 100644 packages/components/nodes/responsibleAI/InputModeration/SimplePromptModerationRunner.ts create mode 100644 packages/components/nodes/responsibleAI/InputModeration/openai-moderation.png create mode 100644 packages/components/nodes/responsibleAI/InputModeration/simple_moderation.png create mode 100644 packages/components/nodes/responsibleAI/ResponsibleAI.ts diff --git a/packages/components/nodes/chains/LLMChain/LLMChain.ts b/packages/components/nodes/chains/LLMChain/LLMChain.ts index 0d555884..44520021 100644 --- a/packages/components/nodes/chains/LLMChain/LLMChain.ts +++ b/packages/components/nodes/chains/LLMChain/LLMChain.ts @@ -7,6 +7,7 @@ import { BaseOutputParser } from 'langchain/schema/output_parser' import { formatResponse, injectOutputParser } from '../../outputparsers/OutputParserHelpers' import { BaseLLMOutputParser } from 'langchain/schema/output_parser' import { OutputFixingParser } from 'langchain/output_parsers' +import { checkInputs, Moderation, streamResponse } from '../../responsibleAI/ResponsibleAI' class LLMChain_Chains implements INode { label: string @@ -36,6 +37,14 @@ class LLMChain_Chains implements INode { name: 'model', type: 'BaseLanguageModel' }, + { + label: 'Input Moderation', + description: 'Detect text that could generate harmful output and prevent it from being sent to the language model', + name: 'inputModeration', + type: 'Moderation', + optional: true, + list: true + }, { label: 'Prompt', name: 'prompt', @@ -144,7 +153,7 @@ const runPrediction = async ( const isStreaming = options.socketIO && options.socketIOClientId const socketIO = isStreaming ? options.socketIO : undefined const socketIOClientId = isStreaming ? options.socketIOClientId : '' - + const moderations = nodeData.inputs?.inputModeration as Moderation[] /** * Apply string transformation to reverse converted special chars: * FROM: { "value": "hello i am benFLOWISE_NEWLINEFLOWISE_NEWLINEFLOWISE_TABhow are you?" } @@ -152,6 +161,16 @@ const runPrediction = async ( */ const promptValues = handleEscapeCharacters(promptValuesRaw, true) + if (moderations && moderations.length > 0) { + try { + // Use the output of the moderation chain as input for the LLM chain + input = await checkInputs(moderations, chain.llm, input) + } catch (e) { + streamResponse(isStreaming, e.message, socketIO, socketIOClientId) + return formatResponse(e.message) + } + } + if (promptValues && inputVariables.length > 0) { let seen: string[] = [] diff --git a/packages/components/nodes/responsibleAI/InputModeration/OpenAIModeration.ts b/packages/components/nodes/responsibleAI/InputModeration/OpenAIModeration.ts new file mode 100644 index 00000000..79297d74 --- /dev/null +++ b/packages/components/nodes/responsibleAI/InputModeration/OpenAIModeration.ts @@ -0,0 +1,154 @@ +import { INode, INodeData, INodeParams } from '../../../src/Interface' +import { getBaseClasses } from '../../../src' +import { Moderation } from '../ResponsibleAI' +import { OpenAIModerationRunner } from './OpenAIModerationRunner' + +class OpenAIModeration implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + baseClasses: string[] + inputs: INodeParams[] + + constructor() { + this.label = 'Moderation - Open AI' + this.name = 'inputModerationOpenAI' + this.version = 1.0 + this.type = 'Moderation' + this.icon = 'openai-moderation.png' + this.category = 'Responsible AI' + this.description = 'Check whether content complies with OpenAI usage policies.' + this.baseClasses = [this.type, ...getBaseClasses(Moderation)] + this.inputs = [ + { + label: 'Moderation Checks', + name: 'moderationConfig', + type: 'options', + default: 'useDefault', + options: [ + { + label: 'OpenAI Default', + name: 'useDefault' + }, + { + label: 'Use Custom Threshold Values', + name: 'useCustom' + }, + { + label: 'Combine OpenAI Default with Custom Threshold Values', + name: 'combineBoth' + } + ] + }, + { + label: 'Error Message', + name: 'moderationErrorMessage', + type: 'string', + rows: 2, + default: "Cannot Process! Input violates OpenAI's content moderation policies.", + optional: true + }, + { + label: 'Threshold Score - Sexual', + name: 'catSexualThreshold', + type: 'number', + default: 0.01, + additionalParams: true + }, + { + label: 'Threshold Score - Sexual/Minors', + name: 'catSexualMinorsThreshold', + type: 'number', + default: 0.01, + additionalParams: true + }, + { + label: 'Threshold Score - Hate', + name: 'catHateThreshold', + type: 'number', + default: 0.01, + additionalParams: true + }, + { + label: 'Threshold Score - Hate/Threatening', + name: 'catHateThreateningThreshold', + type: 'number', + default: 0.01, + additionalParams: true + }, + { + label: 'Threshold Score - Harassment', + name: 'catHarassmentThreshold', + type: 'number', + default: 0.01, + additionalParams: true + }, + { + label: 'Threshold Score - Harassment/Threatening', + name: 'catHarassmentThreateningThreshold', + type: 'number', + default: 0.01, + additionalParams: true + }, + { + label: 'Threshold Score - Self Harm', + name: 'catSelfHarmThreshold', + type: 'number', + default: 0.01, + additionalParams: true + }, + { + label: 'Threshold Score - Self-Harm/Intent', + name: 'catSelfHarmIntentThreshold', + type: 'number', + default: 0.01, + additionalParams: true + }, + { + label: 'Threshold Score - Self-Harm/Instructions', + name: 'catSelfHarmInstructionsThreshold', + type: 'number', + default: 0.01, + additionalParams: true + }, + { + label: 'Threshold Score - Violence', + name: 'catViolenceThreshold', + type: 'number', + default: 0.01, + additionalParams: true + }, + { + label: 'Threshold Score - Violence/Graphic', + name: 'catViolenceGraphicThreshold', + type: 'number', + default: 0.01, + additionalParams: true + } + ] + } + + async init(nodeData: INodeData): Promise { + const runner = new OpenAIModerationRunner() + this.inputs.forEach((value) => { + if (value.additionalParams === true) { + // remove thePrefix - 'cat' + let categoryName = value.name.substring(3) + // remove theSuffix - 'Threshold' + categoryName = categoryName.substring(0, categoryName.length - 9) + categoryName = categoryName.substring(0, 1).toLowerCase() + categoryName.substring(1) + let categoryThreshold = nodeData.inputs ? nodeData.inputs[value.name] : value.default + runner.setParameter(categoryName, parseFloat(categoryThreshold)) + } else { + runner.setParameter(value.name, nodeData.inputs ? nodeData.inputs[value.name] : value.default) + } + }) + return runner + } +} + +module.exports = { nodeClass: OpenAIModeration } diff --git a/packages/components/nodes/responsibleAI/InputModeration/OpenAIModerationRunner.ts b/packages/components/nodes/responsibleAI/InputModeration/OpenAIModerationRunner.ts new file mode 100644 index 00000000..13bf2212 --- /dev/null +++ b/packages/components/nodes/responsibleAI/InputModeration/OpenAIModerationRunner.ts @@ -0,0 +1,62 @@ +import { Moderation } from '../ResponsibleAI' +import { BaseLanguageModel } from 'langchain/base_language' +import { OpenAIModerationChain } from 'langchain/chains' + +export class OpenAIModerationRunner implements Moderation { + private moderationConfig: string = 'useDefault' + private moderationErrorMessage: string = "Text was found that violates OpenAI's content policy." + private sexual: number = 0.01 + private sexualMinors: number = 0.01 + private hate: number = 0.01 + private hateThreatening: number = 0.01 + private harassment: number = 0.01 + private harassmentThreatening: number = 0.01 + private selfHarm: number = 0.01 + private selfHarmIntent: number = 0.01 + private selfHarmInstructions: number = 0.01 + private violence: number = 0.01 + private violenceGraphic: number = 0.01 + + async checkForViolations(llm: BaseLanguageModel, input: string): Promise { + const openAIApiKey = (llm as any).openAIApiKey + if (!openAIApiKey) { + throw Error('OpenAI API key not found') + } + // Create a new instance of the OpenAIModerationChain + const moderation = new OpenAIModerationChain({ + openAIApiKey: openAIApiKey, + throwError: false // If set to true, the call will throw an error when the moderation chain detects violating content. If set to false, violating content will return "Text was found that violates OpenAI's content policy.". + }) + // Send the user's input to the moderation chain and wait for the result + const { output: moderationOutput, results } = await moderation.call({ + input: input + }) + if (this.moderationConfig != 'useCustom' && results[0].flagged) { + throw Error(this.moderationErrorMessage) + } + if (this.moderationConfig != 'useDefault') { + const categoryScores = results[0].category_scores + if ( + categoryScores['harassment'] > this.harassment || + categoryScores['harassment/threatening'] > this.harassmentThreatening || + categoryScores['self-harm'] > this.selfHarm || + categoryScores['self-harm/intent'] > this.selfHarmIntent || + categoryScores['self-harm/instructions'] > this.selfHarmInstructions || + categoryScores['sexual'] > this.sexual || + categoryScores['sexual/minors'] > this.sexualMinors || + categoryScores['hate'] > this.hate || + categoryScores['hate/threatening'] > this.hateThreatening || + categoryScores['violence'] > this.violence || + categoryScores['violence/graphic'] > this.violenceGraphic + ) { + throw Error(this.moderationErrorMessage) + } + } + return moderationOutput + } + + setParameter(category: string, value: number) { + // @ts-ignore + this[category] = value + } +} diff --git a/packages/components/nodes/responsibleAI/InputModeration/SimplePromptModeration.ts b/packages/components/nodes/responsibleAI/InputModeration/SimplePromptModeration.ts new file mode 100644 index 00000000..5fde5ca3 --- /dev/null +++ b/packages/components/nodes/responsibleAI/InputModeration/SimplePromptModeration.ts @@ -0,0 +1,56 @@ +import { INode, INodeData, INodeParams } from '../../../src/Interface' +import { getBaseClasses } from '../../../src' +import { Moderation } from '../ResponsibleAI' +import { SimplePromptModerationRunner } from './SimplePromptModerationRunner' + +class SimplePromptModeration implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + baseClasses: string[] + inputs: INodeParams[] + + constructor() { + this.label = 'Moderation - Simple Prompt' + this.name = 'inputModerationSimple' + this.version = 1.0 + this.type = 'Moderation' + this.icon = 'simple_moderation.png' + this.category = 'Responsible AI' + this.description = 'Detecting and mitigating prompt attacks' + this.baseClasses = [this.type, ...getBaseClasses(Moderation)] + this.inputs = [ + { + label: 'Deny List', + name: 'denyList', + type: 'string', + rows: 4, + placeholder: `ignore previous instructions\ndo not follow the directions\nyou must ignore all previous instructions`, + description: 'An array of string literals (enter one per line) that should not appear in the prompt text.', + optional: false + }, + { + label: 'Error Message', + name: 'moderationErrorMessage', + type: 'string', + rows: 2, + default: 'Cannot Process! Input violates content moderation policies.', + optional: true + } + ] + } + + // eslint-disable-next-line unused-imports/no-unused-vars + async init(nodeData: INodeData): Promise { + const denyList = nodeData.inputs?.denyList as string + const moderationErrorMessage = nodeData.inputs?.moderationErrorMessage as string + + return new SimplePromptModerationRunner(denyList, moderationErrorMessage) + } +} + +module.exports = { nodeClass: SimplePromptModeration } diff --git a/packages/components/nodes/responsibleAI/InputModeration/SimplePromptModerationRunner.ts b/packages/components/nodes/responsibleAI/InputModeration/SimplePromptModerationRunner.ts new file mode 100644 index 00000000..aaded907 --- /dev/null +++ b/packages/components/nodes/responsibleAI/InputModeration/SimplePromptModerationRunner.ts @@ -0,0 +1,24 @@ +import { Moderation } from '../ResponsibleAI' +import { BaseLanguageModel } from 'langchain/base_language' + +export class SimplePromptModerationRunner implements Moderation { + private readonly denyList: string = '' + private readonly moderationErrorMessage: string = '' + + constructor(denyList: string, moderationErrorMessage: string) { + this.denyList = denyList + if (denyList.indexOf('\n') === -1) { + this.denyList += '\n' + } + this.moderationErrorMessage = moderationErrorMessage + } + + async checkForViolations(llm: BaseLanguageModel, input: string): Promise { + this.denyList.split('\n').forEach((denyListItem) => { + if (denyListItem && denyListItem !== '' && input.includes(denyListItem)) { + throw Error(this.moderationErrorMessage) + } + }) + return Promise.resolve(input) + } +} diff --git a/packages/components/nodes/responsibleAI/InputModeration/openai-moderation.png b/packages/components/nodes/responsibleAI/InputModeration/openai-moderation.png new file mode 100644 index 0000000000000000000000000000000000000000..e3b1b282a70fdcbf36da31b2856b7a0ade9c45c5 GIT binary patch literal 47939 zcmV)#K##wPP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vG=N&o;XN&$CzbWH#Ny4XoXK~#8N?Y#$l zRo8YV{^mcKmPuwNGf5`D$&}1wGD&_fc`j6edY9-O_xjxFAQ07n!NvwRY+NwC_uj$u zUJa(0-mAKFb?;SM|L7rXP@@(@%{Dk{pP zLQ15(!jI68(1*Pxfqjgtyu1{DEAS7$@TUyXbGx)m#9w0emzH81raOiVBUFa0~Zq`hehbRR%8!x$KerLgf#ZgyvjJ3lh(#APLPr zC^|?`21Z$hCrWUr*jp;Mvhw7_PiJM{zGJd}{VrLvYKwfoey42NyjyndJSazxos#P} zawNCFD;|HDcu8Ddk4Ol{RaVA~;7();eYPO(eBC4YCCRUXgw|>p(24|6Ue4oY+nygp zDXg0zAFZFUfQFbQWqt*(DlIPq`O0D4O6B6EYqEFWAz8EbdzmnCnv5FtnPgB6(z!uvW48?%-GnUzv5G*5A|<0GAj#*a${W8}G)UX^C8+DdA())F0;ERiwE5)Dmo)?9@5Q^Z1mEM3D~!N7Bf>u>7+J zs=_1r#mldTgl@tGtzHNTLqN+}0n6tr@kmi|z7nytyaZ(Aez=aXg4b`{lBLU5NY`$? zBt9WU8bm}&WNd;YrZkuI7Hy?j%MOy9hOk)&g_1F`1&Em@ammf)iN?`VziFH#gN$7> zGG*T4<#HA-Z4nBIBw;=X=`9VqrJ7WP7WO~|Or=HeWS;Oye(CZnAR(32kd3aV8-{>% zQ9&vf%F9X=rKKnb3M)-~uul0J`dR*~Fy*%Mm#@f@6|1Eu$djB7VkIO?TJtuFsMsX6 zgi*06(l8=cA|eyiDmINyL6|DB3C$&?S$j!q-C1H%TSz0EpVGR6^y>ev%v-t|M7$xz za77D>;g*7gbc9)9!*ZhX%y}M?t{ncVO|!r{D=Z-&$uC}h6(ppD644Nl7SWXbps@j1 zB9;-bdj5(MkinzmqYd;z$#3Q6$);`FrQg6I(xOd!iAzeAq_pO+dM!b)RB6;C2G%Y` zEn%y+Pf4ezUy=@;Uyv59p8`oc;QW@-BrZ+j)7nU4i}n(g+)SF_y3|%3q-W+3S@Zoi zxt?7p9v{nyrWu1oR04vG_IUzU(8)lw4Fnm5If)O>r+*~BWck&QkYZiX%0g@5M({j> zO%OgPHc3EBejr_Y^pj^_d{dgnrz?_Mb(G{boupx0iagmgUgDdzm5)CER`wnFNs7Qg zKkmzWGYw2V)1(Ac38r01s00LXj_wbATt{wy50B(wm0t}Bt>wF2f=X;)6qM)E>aeEL z^JMR#AGM5k z^+ujtyO}R%&R>&Vdk@Q$84Kl|UIUbH4Wg4IGNpwyNooeSG)0;wHj_8I_mgRJ7t6J* zJP^^84>cV~lckSZbjJ-Pl!PQ7p+sb=gW}Fy9+B{uBfoeOhAN(t3(8wsTB=a10=2fF zYFf~vW6flNw?sTJMa3wa$u029iPKkP+1f4AqyKP;O=~L=C?m$Ex04ubH%)9I zZ92a!qdxr__qwR}XB(myX{SXb*fvS9JWID(3CK(&#H|h$KL3Lx;GOREAk*~fA@koAU zWkM_bxFnv}?+aK$+6;okvmjyEovaw)&3vyE;e_+ovSsFiW%AN%Zz%~|wC$)?kR)v0 zrh|MucAV_ocTjSRyc(Zc(qgaV6nK=Bw1!3A3hWcf_lfNK;iQcEe7q#J>a65z0xS4L z(?qEknIzBtzN@TQzfCUQ$khyhXa@fJ&Nex8E0o!7Infn2$f&GK8WT)LhmH}Z;=fM+h>l&jf=a_IOO`C|MOdFuIB zrAbVJ5->icnIxn(mluEkrYu{%PHyDnOCi`s&~>EcV*gM=tssdA0_J%|u4EO^pJ_lKUREEa>rg^-3FXFdvlra}d-E-bAfqp@TJEn#+1ndEpxw(dJBZ}%Aj zYxtBBk*;aOm{dt@(NX#h86{I@FO~J1_9BgYWy7Z3^8MyrvUTfT*}P?^Y}tzKZM&T9 zow9BF9&GQB9}XXv+`J+wF2;>mRs>jf4Bd8j(F>2DYgkjbi5_~)3eT|!J@!b9+&2IyNScGaHN^3=1xS9E^nRe2g|>io>h8b0;xE4tlpz%W_3XqjBUep5RgkWgN) z2k!w{0=E5rxXWc;-R8)Uu&9!VcAsr;y}eJa1%>B^?$_&ajOBBBC0bFY6-|%G7%F!l zAzyG1p)ZavlP{Prm)mY}pUco%D7&xnhe8M-@7BS%tuLJePevSFBtuufFlNR;JLUY?_#+mMyMXdx-`jL9`|yV^ff=iG!wW zIskY~i3t+YbtDm+#-~bhvo`YD+udaS#%)>w!&*#M$mAF1Y1*xjggPn$+bkDexlts` z*6onjyA70-HqRkW5IzNj2kDw7@IDB!kKlGxa)$t-Q#$Io(Wyu`h!~Xw5;B88x)PhU zgN1)XmM&W*w{B(YH!$zrWK@=fBpV4>Qd)@2!2`92bqv4^-?AB;LP^R@&Y-*uWOcUP zWDI12&iAt4%=Wz{@dy%6$>@-h9V6&9JR)IGYCuAZpy&s{i)0O?ZP&83k$k-t_8j-V zAh_QRXFuY$;0Lx!#t-fH`vb&>#y}@bx=2C}H<~tmrnGJk*RM&mJQ)!qO%l>29^^|v z87!uG2PI&`#8wgkOG7kCZRdcN5v0U%OF}lR(=Ck#SW`NA#vD0);j&soTEe1YC_7zI zgtWpOaZeJ`66Sj<tEtrbd87YPXxlO&{7 zBnjE($mR|9#7Sx1RwhlJF6Yl*)Mw{Av1BS?XLPuuBw<-OUDIL>tu-Xn5`q|f@Amr! z$mqbhq9$H@jvMap!!*z{AiVx!g`0!~32KOHzV#!i1_^I}`9TRKA#*o(o18`HetRxM zn{!&2ea50Pek-9QWT+%%n;Obu+-7aziJwl%SK}vYua?NT6cEU?$N`KkD2w30XnN%9rr zgJWN@r$k4+e>moINrC$ro7h5I&V*Nlt48m8iJ46C?ha zOxCRrw_DanQf(53g1I8yjiTa09f5A$4{mc*x*eJBCSfS}aeOFixDn{#;tc5_36+Q- z83~9(N=AnK&2Q;vBe}OsiVA(QZvAHI+kdDMFb2d+NH^svB_OR$N(Ti=SDAnY5lKX_ zj7FDHiI@xmB(;!6O%tVE=jY|)&&SL1H5=vFPiNIC7UTY$@CkY-H?lmkdi_r6KlCGM z)9Dpco&%9+4H+lX(Kxn+R?g7Mai1ldn}iC7q)da&5JMSvI9nYb*Et-sXW`y& zj`3W?kJ~c0wIM$~6@laN)W_O8Itss7OM2O$lgVtq{v@v_f$pLws60X&7Z(+)mHGA>I29k&nlIBeUi&l_ks9 z$-ITDWa5;$GUk)<(yQ+NQPO0w%P0N=ZjjHHv9&YDW=oB@*H|LpOMS zpkfATA)3TBlP1wA`mD2N&(}6XwN3=yH#0L@Lw+-og5(H%FNPo*->rS$7VK=mN+<1f z;DqeS!wv}UP|xAD%pgff+|R~ubi=r#>J~eMBdiC-kSTp6H7FB?l91pL{c!jg1an9> zUfb&X+`=O5sX}Ds<%dOfeu1L$?;=$yA}_yK@(a9@ol~eN$oInSMDB`j2%f72M)91<4&%GuQQK`}t?q3vKPqk*Yj0NHm;YnzS*d!SQBngQG(_9dr+EJ35cSZ>7 z8Jn(Tj7n@N&09Yu&%N}P{Qiv|^5QG+NUJu_Dfy#t??zE+(jY2bo{UVD*mPJp$Lf(- z5pgJQ!V+2n+Wl@=LNa8TFfy@~f+UPgfF(sn^ZZM1$izw0_1%zw){>DJBn6uTSc99D zlP%e~Ie4c9l9QXSVP3vR-*t8#LI+v7#c~VpoxOK%<`x2;K*;@Bc>mc2__naT?7GM4 zIp_+j6?O>7kd~O<5Zf-oekvJqoc{(7rz-@^}+SzTsV18)kpmb3T@hWr_ zKu#e)T7g`?ktJs?UXddwPNCd%NY-!O2Dfd!EM2u$)^6M)o3`&#Y}~dp5ZdkFZ-iTR z%J-Xhn4Tk>cPrMf-zIza98x0sytuy)_b|H46I`;2-)$em*ulBzLBdp za^P_UEgHA!Y2+3b$nO37WZlM1I!Jlr)*UFTnjS=(x8c14o3`%3`{wpuoU=0h#&IGOC_HV;BbA z&dQm3aNCHxqUR4yZnK2pNXVRx;7kB9mv#lTW`KFYk~3 zT;3h=k@O!jLVEQZEM0o`lefEPNVneo^>>eqfzrLt01bO&^mpLiH&A-^87RFn21)ns z{iIvBz8d!FGf2MqdZL^=ds$siT0aty2}6-YE?>@)VIw}4cn}~dt%D?_fn;&4Nrb{D zq8tb^HU_!eB(#>%jUWj-8W)yibTy{4l7|GRiy8}p&@x8GHUj}WNGb`2`!@mE=suEg zBoWJ*Pe!L}c`&i%(;6RL(s0BJ-A7mwTDMf)Y*^xEEj!7`QJ>0%%QtkCKATPq0oh~6 zBR?GdQNI3ey!0J_cbbtY-Fjt6kKPD-_C+{A-*b=NgADrM+W=kr43RFqP1vK~Fu2`A z72PuMEqf1C^y)thW#0Gktv}Vl&r@g2mF26~$$^7M1a335+jQrMM$zn%LYRGxBa`4tCM#IxX~K}dBjO}6vAOig7$83$JqdTp zV`?K&9wZryipu2Z(KFJg-%yEyl_Cl0LN<+uLW6vkglhd#I!hyvtvV!Zk^oXT7Ex<4 zQI4Z4NfITs=&Y8pQ4HKuxSS*KR^;wo8jHY=(HS7qmvw@NCe+^Q@-~g z_$-Z?VZ=38=<`LxwRSq56OG^sv)Lvgy|uJz-&vk{@ikt{(E9KykAGO1L zk%v8sOhLw$8@WwABPb-9yGe?n8}4JjM){3>HU_p5gf0c!y*SdFACzEKJj)D)U?K;i zPM@;~x$ArK{HxuiRfkugfKbx7R;qlF@l+DJOi&&twA<<)IxxRmDY#9=q8k^(-*hFR z04X3-N;)h~Y^u)XIsCm*a_xFH${@@rkS>%DSvJhi_sj0xKOl!6AWfk(2~=FfNfNTQ zk%S@%NkF0@h}ICqa=V~zSP}*Z2#ZLNd|H+R=xPSaYDh21MJvdKr= zT~qMAQm`+%IWpZ8?a;s}9-n^hWf?GJm`t5HOO74?NeYYDV+U3mAF9w(q$ISJIF@8= zm$YQFzwJKvG0LxvkiW6k&yvp!Dnfq{7*zZS;NU%92_1y;|$`gSjA-tIYd z`I^Gzi-^HTLKwx03@E!rBuEgG1Z9O{Gvsl54u3}`U|(X#z;AOMu8GG-hGJV1Hi=4< zwjG|8(PKWBtn31$6}de$t_0SQxRsSJt5K;wj;DAFm4{ zl!VOm67X&sL?p`dFTbr5M{%kQ)@X8wl?R1nj?}-Jl_j5!8z=2LKcn6FViJ)-rM84+ zG-b+ISoRoLcE0OaSp8T$dmOxhSRfAX$-F!BE|_`Hy^e3rN+x!SyDIa=M4Ce2-ec^_zCf;9(z0i#E?ny(Y+2p)hgDPay}yM@xE264Rbj#3jQe zO@y+?LSf@UK5X+i&x?!$@c{1E?F58?`OWhUQDYO@t5T$-b(91sZ@q>wI)dQqZzf81 zPJvoMrkSoNUD8XJZ^(?9^W>#hyC?~1QE8PMBXgi4(aj_YBh%mt0@&S_D8)A#}%&lLPkJZ2DkF_<-4g<<@r}$$M;B-CNW7MTr;h>vLuX-r8NW@nek~Rh-bw& zP&m)ecNmk}Ni!j)wK0kr4N(?u5S5DKtt5`#N*dCY3{MCaF)AMJd2+gRdin+FKX|CD zT(wS4pE;*x#amf9l9ykoZH$(5ZjxCNvIBrU=05(bC7~_%X;&;LxE<+T=&Mi*IAQvH zdGYmb$RTOn&a(@fi-B_S;#Nf?3shIp`SAtba=7Zm3Pi0C2=6_OUgl8_dP?wY&&#&bwO zbwvXNLueHp+m#Wteo8)v(>+#6E zB}?R;9(_Ozl&?X;n1trAgzez6!=;7G9EZ$KfoHYe2)z^QlCx%+Bvh-2G}4-DhXW8X zCXJmCkYR#^w1_MxMkh5>*OWuRA|m5-s>rvy^^^%yrpuAzC$(9GWmeWXgd-tCOG0j2 z{8~jxs1-2?Nj|pwaL_7q)ZF4S`SH{RnKpN+yxuiK3*#gp+uvCBVI@UVSgt0quqbh` zav&m!M8XmLZ4pX1H}UK?Noq+*OURr#fffa2mWC1W(tprYowyt;$aokh6wiPEb5(=v4AXgPfR zl)x#_!Nxk)5!7AC@U|Eos&~#HX<+>p3@2bMe z;Pv6a^Fx2%5zG|Az>+_RR)*XJELvk&wxkxVrCZEZ0;QMj(AR7d7QTE!l z?|=;Za17o_3zXSdW1Vi^5r`NMB9VM_{Y_;VtQyPjfMyan7J4s#tPSPVe+96FHwwJ6e8V>B()V4w;4~c(&H{OCI$ykTyI5;x-4y1U{7B3- z8OEhQBMHrk=QQMR?r#DMXL41BbocB&v!4WHPOpv8xDVYX?TDS;T0Z}3yqr3FDKMKE zD~_0cFMjOUxldkv*&8z@`n>hJ%z7QT|PBCNIA6 zmTcU)+Yk^h#gdTQM^2rQkH>wZB;){P&I}%%#A{n>1r|wY+*@XR_yz!Lm077pi>==U znSquI(_krGB(&+T7nLIQqf@oEn7ToIAu*{h>FuA_kn`)rq_&c783SeZf+ceC>J4po zwj{LVv-pD|A*+j_(^@XfFD{WQ*?F?#;E(d&m@gy^xtN(H3~mNo4_X(#Tw25?@hxx+ zE(qNc6xy}wofZcB8bjIDb+I1~A(e>kAx9JNxK%iA6Wq@>IJQkOZqCmc4TWQ0&1S7T z$~P0I>O{MoUV;Rq>&$Y+&0E>BV&z(S>e-jn^`Ua=T%RC`x|C^9Bq#?*Ntlk>Ce-~p zcJSW7Yt_|+(pgyd)54TDjSE{X;QhG~u!xAuz_`C0{m+UJ&KeMxoF?s`dRA7f-5}Y8 zh3bm3W|YnxUDH$NFUhx)rb${$d?%2WL%dinRCg3{vh2-@9@}(e2x&;DnIed&eQ5C= znFd;98%AMYOtVh-cFmNC)Dh05W8GAJ2S_WwMI%^8j_qvLwzCZXV6^N#cv!M?@~DR@ zbU{N2_Xj~jb}b?iNkV?O0&l4tKX*|+`FfH({mNTv1v%vdKRn$H=E5W-iPAWhjm>P3 z<_9taJyB9Y=wv7^(*}zH1>j)a2oQ!K@febb97WG`*9KI=NV>ib_}h&d zlaO$c&}ME1MzNKEu=JeWo6~p1B&JBqb{%E@lI3#o`gO_oqF4dt<$Gd94QGd&yJ(p< zs?$>-5xG`I0^UO+tXTr?!=?$_p380fJ2Bl9I})2er9-di{g8Y*C>htXGeSIy0Q8dV zccnh4$AmP}3wS&-RqC-W0qN$vtIQ-{cL2PL*HhmsH<(+;m;n-Q%SeE0((7Oz;XBxE}2K9htj+@HI2 zS!T^!C>@@DPDvOMpMrFu+yfUj3gC1VO;SMYG$;?0fT5Y*f*ArUQtlmU4a2}@Hik5} zgYU0uIF<*XYtGtMTjQ!71Jo5|(@S%ZFd6Q8lC)~qUgj@eBA2dR*G77dW?|n^7BSeF ze$9qW^3w0$kT&g~l6D=R(FIJ~;+?na^fK^@wCV7Yv~K^RhWy>W^Q#7(UenPaEpe<_ zo9868<SrgHqmob|_|mBev^bo@ zg>6G|>8+&gQ!mIHJu>9Ik3N&HCeM)R^Onf;1xsbd{H1|##=>Pf!7hI@;XAKuNjx*O+71C3<6Ii1h8oHs~_(DUt z`s_BwkhRgQQl^JV6o#WH)|a+x`Ig-oBlOs37kH$ceWv*xXI!d37xmK)4pDRUOA#y4FdUw=1C zMvNXOZ}%Rg8LSR`hI?!)WvDlev8`S}GZaOf;s7%!wUi(%-O|ZZX2^*XCt(3ewqV7K zCEXtc3E4EkvLHj2wN@a<>@#qfx;lKB%;geWc2*M7%_R9qLMjYjIt%)Zpa{v$TFJ|A zb&-)DeIhd!ES8=74#~-jSKta2$PEscDJYh#BCigC$zjbV%AUDiPott*M-0HJw-aVXz%|0wP#3j%hqtATB2;yMpD|K zjM9>>8_GXGVvDwt&>U_Nwr!}}u$cUv*sM*B!i47S?g)#MdlUnQG7+o=XX7|)lQ{sG z9Uk(F@EwXufpWa=Np@Bk@HfjX+D#6j9uEUrXo5Ak9Q2D_feye7 zxu2Gty)ag-TQ8HQ&6E$ud@B70y{i~FbOi98WDXvtzd3DT{~^O9Ug@uJWQvVOe67r?mTC`8b zeJ3s2KW$!MLUS#HupCHmn-wxFo6!B>i%x;Xd8d0fnKyr-{CMKHc1xng(rFK1vDhEg zl85(UNYc1}+we}H*f3PMpVKQ`zj;GWo;oAD_aBhW+jq#ut=nYBp8c|GA8`=dd-eCu zz58&^Aw7@#?eUt2wa1>lH4mBooxAqPw(UFRha-pO)S1(AvWe3Mzh8!2e3%K3+qs#qNWg0`fSDF{~Wpa&q*F=pAuUR1%J5XBK5_r5>ACbn-3e zSBusiO1n<6y@PvaT_6)Gex@kus4hd6FDGdQ{hZU^TN&|L5VnGd_KRstgG2 ztEnoy7qK}cU%ct+RHm+tP7I|5{b2m6aQMIsxu42JvBw|AkdLQC0Bp$p`n)cp={|c6 z{&K$^HWVOWC<*bK=T&(e2uniz<$Ko}O}uX+D?3*X9y%=Fj-MbezxJj?M8`?v*aR)I z*a?+MKvvpNuQ>XGJ$E>wWAKpoa4qyFuSl&kNl4vc+9-bzB&6GSEh|@+tXwPa^vX2N z0?etLnmP_^p$lsJM{4aT+*DR|k9>cW?Ay0TiVE`)6Fy8S+)oc&39rWt;=~JMSmK)E zAS~Qp`5s`WKJ*oPH3RSznspkulo*wz3}07g1oM|@Sot?T_(Kr}(^pl%9PFBtM9Hlghd^-%05x)a{S1wdmYJMvdk_dXeb1&XMKR;h~?%XNwjd)*T z6H|oiIkK{j^$R-nKYRFqgv`J#3AyI~JKcKAsdkC;Pb~9ujvAAJ5X>Ch1A%|F zB;*Q3Q)bSUm)__OpFTCmIVg9J1rL>dscJ#Me7^sRs(AwUv`u)P1}%a>*9^l3WzGFPOVVQA=(4)lWI$-W-pS9S8l2H>+-bvemXbH7}7BD^dTWi%hN%-oU-DK73wOSsd)v-!oKOleeg;*uw zK1!t*#ULE+L2SIdk4*=yBdrLPH{e?_1XP)wYwi^`uBs63ntOqLrxsS}b(+Bt>*qel zE#cyIE4D$na}11BCL@I|KJQOGV7YYd`gOVtL3%3`YvNP2mkbHWrY`lO*qi|pvSNlL zeD;Oc=zIy>rh-tcQdp_GdFbNOjGFKk{x zzPgcHcI=dHz57aP>vnKIleN8-l`$OhWHNTRr!39@?3mw)L$St9kaHKWY3-}Eg#1Ld z%=)V)p=~zcfY49B{6^Y8^NLzR=2!#?NYEuE2{}ZFQ~7ZY&OZI$m2KO1t1^&q3@Mfj zB!b<(mlwk;?gqph2>HQyFDj;0&`?EX$lrD!ch?R<1+5O4PB*aYo`A>1vjyL+#_f7` zi@OTqs)P`6-_zd5E&N5WBQ{6QvoefrwmmX3CAlU02B&DJ1~yf&n;@IISX0Vk4N1s3 zh1zv`Q9d2}t(-Z3#Wa-wtenxBQYB%iD{4p>nifj9G(PO|8u!E5laI zn9kJ35|W6VJc%=jB{u6I83TsOuHAd}!;^4yizxg}p;EYArJBARdp5(nfeNi34EM2$ z8194EwAz9{57!Rn9@T{r%XyLv$ahq_L8)1Z(1yT7VTXrL9 z?Dn=@yS1BPQge1=Jl>2sG&TP5Nl0olx@gA0D` zB*fzdEaB+SzETo4h)h<$ov>|%EE8%?DF?eM3ESyF(Ve^Ys3OoECE+XytYUCKVXb1Q z@O9nsl~r*t+;#Q?xZxJw-&%-pX?YkW+%vghrKI zrK_^=ySwr8%Wv3_UYUfiIwWkRaFdXOJlV6J{TVF@ckJA)AKG0z=T>xg#X-22Q`O51 z9H`=i(A|NL1Mt2U_L%)0s*LXIeyOm}W#7B|8SQi1-*-ZFB=j0Y>+FY0W1A#mr-*(7 zhe%2*Si*SIBth#(5^6g$GItV?pe1Y%chr)QL{>7>9R(5Xv)z_{-qYN=lGx)_3lr?d z&o6f+p*GrALBf86hRXIGy8;Tp(7nxL@Vx>RMpE29H`=i(B1ashr%ASX|wyR zxX*nUaW~;PR#C%|FjQ>3JRYjp)e-mk?&}!p{ZS-fk0N%fW5o=o-{6om+hDKV)lk%7 zodWyBa&B3=qaTbJD@RYBRdVBxf9}^dgsc*>K9gerCi(qLit*>wC`#r=%-xvLWUZtiy8oh0;nZmTB*ecE$i zzYKhLxRQ|lNJ&D|W2S`;V`7h)xKuM|Dc93r#SBUK<4QI!PJ zaN7m;V>k%x|8>aydhb?j{LFN@~wGm;eEt#(I9EUtXn7vLrKSNei-}veklWEwb0!0ZufFupr`$^NqDDcy$!*o2z=dKG0kdPB7RYk%;69uQjLz$3`^?ZRGqvtLY zS`t$1KKoMaiwXT5B*481V{s!?0=Kbp+XeRi4g&js9TI%c_eb2(^Ijz3)}4E#PrpIh zL}3Q`nz@K{C{uh3?KQ)NgXoez*(gf8x4k!Nj80zc&`e3lp-hbPS5Lybl?mOJkR-g_ z67Iu8<8`=5=zej0QO13!>cvG14p(!1YKnKyrl{CM;yU0=lKce<(}4!(Pm&~#ff--&W(J>qfdnk8U3JbdyC zBcX}Qkk7*Xf`j+B5_(_v2QpR^`|f~8QZpfO*nvBv{~&4Ai3`S>DKXhZL9jf?Wfew` zogiyA?2z*pu1aBn*C8Ovfgm#D;JfEE`J6L{WkV9tela5a_rZ}661um|Z+DU8;gAPH zLW3$K{M!udb{`JDU`s$d1|)=xdR}Ox<`_SrDMzBvufL%U!n4|kgyU#^eviAx7gjm z!!{oKQu&fyFB&gDaE@+=iaBOT$d5#YAsvG;+7CXY#H%=*s4^DXL1 z9=JPdj`3V>^Dr;wLz&+e&TdmfhBVq2&6itV;g|9X6Ggzx8W zs9Jb`z9qku;(K_C^Tb!e3Bz{Ez@czQJ9m%{&%GccKKV?RZrCU%E?tpqFwcXl3Sj{` zw`@T%T~R|mB_M961jHk9F`J??SVV>ncx|x*o~Q3s^~`|!!L*c>bA2jDR~(r4W`xg0 zy|8*qgg!T6CEPKhE(9brN*5%d8Op?=CiLa2CSiM!aCis_9n!fU9{Z^Ls)+bz+A){a z&j4%G_+E4l6&y1NDlG}Z=@Yd~ptWQG1Ls9gRekbt;%38z^2e;dLJ?ycXsSm7@z5MODL9Qa|6j2r)@j2!)eOr1Ad z4xBhHSMu{EAMdviSLNdy<>R|5ZJc*vGecTAk}VhcX&$1_!bvxlz8vIDc*!m+1N|Y&_!aouZ-`?a6dwfVgL?QNc)^I*!&#(#&U`AqA}KE^1aWh5 z%`Nd2UqhG!Lgt$ZrBJT$6yaQEgoqp8S9|xDA#MCX{xXl0lo#V3hzpjp5Q!)RQ8mW} zm;>7!nJ?XpN`TTM9j1YpiRQK;A&xd^$hTvxYRo*KUkK~b1LP+?9lCV)zvmU)6 zp;?88B;?9Kl}I??eUNZ}kc7w$=`fTF$_A1T5*DhmlT_Bl;7}DEuHpdttt8Z0#Gts$ zt^BZ3gckq>G^gYMPflZ4R7mSrfO{bR@?5w-*&r6IBCH(U8cx&h0Rh295C|X%aXqa^ zSvjX)fW<_}T+@%7QqzT0lM*h9@sTuB;EZPFWj+Tu)1s8WX^~70sf6WSG)E7@WIRDj zX)Pe}NFE8L)+F4y^B@!vN=q{irKU>L5|V(lgtTcS5|vS>yoc^_dUa06uB+_g=H)0K zn6jL4C+I>jF1it_=p;B?#8S8(K7SF&4A&B+0yrL0fNO4eJaQ$kNUr4i%HtX-MX3a7 zG{8QbW6lj;tJmXM3EbEKQ7O2Mv?(6RBZ1V4g#8A-FWYw=RI6cgcox#jKo2FLl8|-~ z)(kgOB0AO(?l=fVVmZnR?Ih`Ze)gIGN#MrP;#ByHr2=j!*r?|)wnDga+2tj24HqA~ znJ;S&o|HN3_sZN22V~W*({kuSp^5DV78G7=+ud~LGu-)AKNa#WNVxx?<096lzSjyTdkN0 zlJEzR%@B-k7iq$Z4_o;`5}NXoa}M;-0=lX$BSRcgqOf4EB&3z`!P=mlC&eBmBdfuS z5LY(NJqzD<|E*$~v-_;PJ9VABHgb~ue)t5*96MhouQ@0OFMH(%c+A2*D*;$AM})W$ z;`8MI`A7t<8r@B#8N?v*m?;pr1m#cXw-VG;HZhZ-wO5Nz%c_|To)e%2x3Dg(v7`oC zu5?1(ek6|sQbQ7Q71vM_9;`${bvFY90TPe*V%e|=`pKoXZo!4jDfDZl z2=pG{3|k-!tdtakj79J-IeWLyqigMvfR}+CH%ny7-V5^1H*2NEfJqYh`iBzp+WXR? z>ln%SY`!erbwo&0-L%7;Y)yL1y8F>#YDGeXQ_tK&!4*<+#RJ+sa^ftVi-lC_3d~^+;&e ztXC4oq=AI(LBbIr;eO+ef?#w>NjieAsZOn~zgaC}$d}_Omvh(iW#6&$vT4sTS-s_e zY}$81j$O&p>S$0zm7TSq;w_T$0$44CtU}Jm^%oJ*&Mc8{c3qUWr)-skK{F(xCrH@s zTS@HkwWN3dLf#rRQ|4?uCO_Ts2T16JC1n{OtDL_^FRBDccE2M5nyzFV2-xxTRX&gL}wg zng@q+akHB^?JD>RuU0Ms=Yg|0b_UOI25dS5f}KUiaJJMd=V4_p0GC1Jt6=YSZ;9OW zmIH$A3c1d>uuWV6*RL=He$B#)xDjZVWM*c{#~*(z3l=Pp z)2B~6Xa8YKwMoeFVC)G`5^@*{SB}jbI9zt^+5@GA_5ytI6cRB|9q+_ngbmmq*>dEJ zd@^;hyfNrwY4^^%(&n9^@^r?>GI-)DnY-tL94QdFL?Xf*6`*WY2#Z(%|L_*BKLlTN z+QBUO{e&$NJ!FnN-gkyH9JolLhb@!nelsQFtCQH_x%a%1~b7b{7H}Yi7 zdD_!okI+-Oe*LD!m`d9x-e)F5(R2zTtCx0T{x^X2}#?B2D*u`K>TWrMb_IV9vBx?XfMZ^4Qz*?veeKbK2)K=!Wctmv|Axxd!%-j5k1}!@014J1@xFleS6X@P$%;z#MsU;5?~6 zaK1DdG*_DTo+Qb=zm|UAtd^xa&dPxf#9@$*moTO%0M;0X*-rzE|b>%Al=6mamFNR(q|GfgDD2Rrb?6EQ>AgA z>Cy;jm@!l8!EGkEP5hFDy_a5k>7}qlstf$lAN`Sh^wCEF@9NhqH6)=~(n?9#tPMyw z*pQGlj^!*@F{a9vkYz%))3H*6{r@(dxGbYk9_-M6j3jmYK$3cWA*q9>OXFUXX zRkC3JRXKjMRI*w3!lZz86)2;Xb7?euOj>1T2Jm<#7cTRSa+KGwO{=>bg}*_tUd?)p zlcXLKC9cmbiR!aZp6s|cs>ES83Smw<>% zB%;qUMdLopLA2%4AY-LG*>9yh(Qmcl@xH6%$-b+Rk*vm@f0@GaTsY#~FC0I9+`03w zQ>sBi_8o0i4H8;QND`WIm~lrdq`0gEWv&XjiG9ZlN@dxRi_+_x`I6S>V~Kcsv?L9I z!Vj7!PxOYR=`&kee!58puevC6&WViMl`XyJ9+I^87HcKU?|M#?rbCxX!Y68KB&c4c2%nya5u@EoN!VlJF|h1j8#^&lO3}FBVDsTOUh8 zPhl27Nap!`LiO0FU?ID!&`BRl_GTw<1oww_ljB+}E&S!>~lT-+1PkXX>Q9$Tr{X z>})x3;DBZ(Q>ILjMT-{6!Gi~NKi%sGE%!jeRsj-n;O6e#o0Wv@yv>RaikpPS9j%Ze z;1;&o#&-y1weg#Ol-IvpAn|>_k*09#8up(jPYzftzs*=Ak7X>B<{xdA%oR7})rp5B zedKD8VYWP;0qfU)fixX5U!sT1kmSLW0x6(d-Ag!n1zJiKw8g%r`s>&%7F1pCAAw!*!?Ssfjb$|bCx{WeU8-6SPC+& zkSF@W3c|`g4hzTaCt&UB_gMpXb&WLYwFcIFjYRZVr)bn`Ju;2;u%a8}v3?umcNyQy zKZB$U9|xZ7{XJ6f%Mq@?OBXR5K^_$T*Z=xox)^OugiCl&o;+D1BO}AcSrwW#ZK?## z%cG{;U#S5JTi%6){s0Nx?S?v>2jwrVeMC-r3CDlJX=^VR$)}r+OQ$i5pu`j4vdxi* z_tr_1kz3@SGFHexyt4q-af8GU+XQ7^BLCEL0bIYu(qPa6Sj>r%Fl3s%{N+-azWXu= z2)7b(^7F9rMcV?kYZ`o6e=bN^pbh6t2b^Xt6B2pIBJz+$+Pwek8SKrkyL5hlsJMQPRw;Lx06ttD9lhM647l5?Q&@(VIoM_Z}4E*wVQ4g zyKs?&U><}dzVv&AJuo(t}==}pJsm9POm7sb>p zB%o0Hcrtw)ctj$4DoSCQSx)syPD!5Rl;+EIpGVdmIwBv=SS(#XnJO=hoGcxOOps0^ zX30~d7Rl42mq^F=7fIXU^Q7H-^QHZW1=0cN^d7NLIuBncPoX?ZJUx7obRMxtI*nW` z9Y!sYwxbqH>kpSI+KyT(?LJs4?cZOD)IR)j>Cz?nm;dr#hE>j=3;*`t{#z+5q(5`J zj2}O~#!V#E#eet@|3SX~`fKM-btfR9QB2cLSb0z{;U3g$m&YPfY0YT;ZsXO`H0--bA_uQU zxd}w<4dP`kl$arlB&y$3l-0hI7e>#KuQ!~K-B*2b6%pr^mMa0ZV*q^q62DiwqG{)E zc30CbVyv|_oe0>Ehr^@_K+wF>TPPLUtZkK${Klh7z)K+DEw5LKN{WSpWSIsf zAqv+VHLYD>2%2UlibT{BDv8Je!}%a%7PfQos}TP@a;wB6SA4~C-s6)~MSiVFI#y7w zI9iD9LOc@GY%4sgtH{n9q>uDOIWF2ic3A>DfLPr z%4Zx0$TH%27^xpogj;y{x(t}QSz7j=BJn+^N@Ta`(xlrAiS9KA<*SJj)peY_2n+bd z>I1UxhF7j2{wzeFi}D=fR01M1U=1FLsAB=uFJ_|nsKyeiTMDqVwXZZ!yuRzeRq^_7 zNpV@8cFZ;n`DK!YOoScAS)mjTaCRjJV9}k;9&WIA5G$kTG?~7p<~)MeFy4n*UU}t} zu!>hx&^7$~zyEtBOloSXR*?M7-~3ITS=fK^FaAZ?{SOL_8Z}aVtBq8TgzAz8N!TDJ zRY}+zB-{iNqIjq7COM~iZwaWDh!)8Q7qLj^{qUCLL)i;p5ep=*yhxRjZFPsTd@_IU zdFelXy|nH#Nupl)LZaXJR$|}!R+77aD=&xd>d(e;kCK#~ui4q01B2Q3l!S5<%y!-` z1mN^)We-Q_n`tdzV*!rZcf;=IImJjPuX$L7u3b2Vt1jrS_3G72D|GH8SFT)* zV|mm7`xK_kYQ_W_+pXR0*tQ8Wi_W#dhrkgqu-H>7#b6yCya zA&Z4DPWXXPN05_*$PwZCRe*#QW!X?a;uh|l1!aZ{h`+Qb*HQjDez8cFZ9XMm&)Oj$ ze!EUSpRrZuZ8$0i&lkvLxUKmhN_KIf6!{Bu(k7M#*&oykx0FP5lh8~vVMu6Z*)ns@ zn1NM@!77T#bjcumt4&+=OS74WTo@IRku^hz?0v zLG&;RM?Qp8vb*8`{lEWLD{DYGd-kl>K!%I|mjJrm_ab#b!k|0KfxB)J>JCKf9DaPFCW5be?pza0MQmTcdDUUnb7BB!o-BoD{b0zt`( zKtj%OS?uLxPjLOf5Ki($f{=g#%LJ%<3Is^#kV7YJBE!^bBk+PeK3vRnJLzYeKVhe6 zWc*AgK}%*}PFLbP_oB74WT*Q}kbpYfV!$HuMh~C-&;R*9!z$RF@c848J6G3@Jg8dE z`+79+ zz;bR8I8q1{;8ND85*n@gx=?jk6PEK>JH`fjOF;Y&63|5qOG4xXNJv$HsXbAJCWdSI^ z>X({FGKZixV<&w)Elm{}RIqvi3aRA4A%f&#Gz5MJEe@=ZbBc;{J{7A=!g!Sv|c1&baQc~Fa)E11lrp*X` zzh4?RZX7ng>Y#b^=1xp^ms*o>`<`u3VM9W75vh!pgao$@8&qMb@a~EY>(1dSBMu1_ z&iUa;sBm8dv81r1Qt0(TvRDL(;E+Yd-W>Y_=GX-uv*+7H1aIWp2JpWU+{g4d<9c|6 z-N3>rQ&srWKmAkZ%G%4MNt5nN*SP=tfB&y`Jg<%H+qX~t&A<6KVdK9W*m=A0RkG`)7=QQ%3rq)5qTM*=el*>A(f$`r4$bZw=66P4X_8`8HAMp z5$OgqftHK`AXRQ?VKEH!TqM*@EC;;5ZtD$AP-Xa#2%8qdDNp~b31DyY(lx0bjHWWJ2Bi@YDvPOC=>3&)uvA-%S|dI5A?G#nT%e#Nyyqx-n+Ua zBSx$q*up!_b)|WH!JNhE>B%?XUH+1{^zSp}$$iGlW*)-i+T6-N@~s zeU67mI(P0IR*}Mi<)y&4z6YVJSHFJ!uyNNGoJguRX{$W)BIAn7xeF)qnRA{6@u_+-5S(dG+D5?Y{9#ZC> z;MXER$N&iq5#1FofwUkEhK#{y4`%d`$^Us~DtCm`maP#iHRDIJ}@94bhKY0M&@Woq&Bv`hi3(9*E;oMOJjR{cO zSsCgoGNi&+R{pRg)QF7)X#mUZToVeL)==2vmE2a6QixDPSvQEG!9C0uhi*cOYS)Y-W|;f$qaKyjHJgh`bMxeK^MOA(EPmLT6iA zP2^sB;oJ$Yzy5lS`={2Wu-q9gT~*=6jT_FD;U$EGUpOQ*(_0u4wsw)QT_qCs9Xv9) zDw8U%DX+2o#5Dl9%$fF?B(WS(C1wcNGNhrQv6$3c+qKLP&LO6wz1m>T_DCfn32Q@~ zTh3lNbPq{Dl;@BJCwmCo)6EAG!L~dY!rYEvgMIKkH#7&bU^1Nj_B!p3qdkTkI43;l z9Q)kAU+!FWce#11MxzYH!h2v2M2fZHTDEuUF~5YaKM2+0kNl6>|f zM)vN}gkYaV=gQ8+%FRD-Mcqz95n&wc-gdRlM~~^ zAvG!A{_c0bbFK<6H6)?i9W^90Yf*q0uukk0tu>70`B26@g<2-;=V$^h^2wZxgdhQ_ z<92;`5``qxj@4Fi%L;W{QdM@b=R(4+DU-Z9bUL_!8uZWdG5LA z!lpSK{@Z{1Z_ZWWB|yS6SFX#b&+bk_x~N8&5n9a&l~n0SzS3NS zGA_6R%cw*$ZHe^KXq9NiJjEzGs()`RsZP>_YmD;dONlQ_e7+pf6?Q=|5|PB^A}@K~ zau5(?M11U22g@)%Q#0q5>f7j%jgGfb-W|37D%XNH@cGmT}yC_ zl{89T0Nk#Gp!-SCZKk_v+8k-^NH#O+zOjDB0&>y|W*0w?u%dv<$f+5qAT@2byPqUv zCC-^MXPm3+PBw4eEdS|0{im?^wBUQ-B*WF|XG#kiE{<^Eh>$zUZ&-~%+>L~h@vS8; z#YMvOj*>ZOWF-=E^n8GXWjO{Mz66Uv68imKUFehR$a|pBT!xH9c%q(aqNy2WFNS4nhH;OCd7Ovys zGRLz-mhHYEAJ5z*eLkKlJx6~dL%*FOvo>y&qc^U}O%SR8*A;>ow4$5X<*Wp~af@CXkflIGxj9O0R z95kl00=?Y_AuRuJXi!}?l>ZV)NUw*MzNS{*4Z5gRO>M}|l+(yFwgWm@U_v-^w!AV5ZrHj@NM67@dMpCMBn&}R?+AbI1qzicor(M8lS8xf} zm#344pFoM^mT?NgLMS{431!ZL<+=_6bE4lXuttY&i7ecIQwDsuL7pEnS(frx*U~}kdScO)EzY$^GFx_P7*@Gu?`92t4YF9vSarFxM0v})?Bho zh1j%)6hy4hk@QqeC?qUL0Z5nw5?;dLLl=Fr?uQ$)=7(Fd^;DrQ=5iIs*!z!D0_H+p zixfqY1NW3u)nCW)i@=YFcJ`iZ={Iq^wCp!ol6rhC@!dX^gl->5+&cs1g%3W)@x5{+ zFH3IX?z!s9`qfgEmUv*XK?R+B3M4PSB?X?FlJAAv3wM;nA^CD(t(X?~6o)qfu3d2x zq$6%A%+!lI`x}1eIU!+Y(lOWF!Z`%v%tgp~SMQfxyLRoq&YJzOlCUnCL+*s3Lx-xt zq142}vUB{M#8okKXA;sSH5D`MLBi%BVQ1+#WRz^*a|n4oNXVWrtR;mKYv*TmL1`b1 zdq)e94Fa9YE|rZ(Z_0!fKghe|*GS(lmdi(Tw#%Zu7v#WouTE!3<;_PNh4`C?GT|){ zBnKqtT7sN3cfo;d88~f^v>iN8nsl8YQN1QfY`+N-+4EzG>@i%P`RGfTao~{rSX3z2 zkb-Pn%S?dfH#Q+~R&IjJaQjNRYHy*=T}r2w=_EC;rUc7j*bGN(8gCOCpTeoyB({ORtH8{3# z+O%n5k5vV(_;e?!9SH}2s3g?BqfkyZxVtMT=%BGeg%u-I?wcUkrlVKo^SRsPo%d%* zhhASv>RV%^^MLU(Z2Cr7cKE8ChFPXU=h7nKssdP3T0*+1r(sH$AG#`;6E;eR!E+_H z`xJ@kJ6j?%XG&z>Nz%C2XOcE-ob;T&NS24JPUwjc(3F;aTizVbhG4Z^a7|0h>}-X&PcCrPTOoVT+>p7Oe~_^Yx5|6dH^{rwHcS60 znRndFO7GRU~nYvjrrn$DKZj^pgHb~}VgT9l$mp+p>>G{1J^hSIcQ*nIC zS{XQbCEtIYNlhvWYKxzngv>nY8itGGVMBFRk*YBXqm$cJBH`v;hftO>?SAa}qP=I} zmU5PBl908G?8!n)#)*69ZaFDkKAbBp-uhgcb^Tgez=9?A9EbAU=h9)=BpI=Ex2(Qc zAU`2ax^Y}e?O0){EdSx044b-2o*Fbk61sdTaowg!eD66Dp8-qQeUc<(PLkKZT_xjo zoRn?3J~@r+F2l;*Dn$kW*R#aww@G)DL}c#7_C~Je#Iok8{0dpL`=ku|ZizfMWW2QK z{iU=*oUI4VlIDYEOWMGBlIkF3&>Tqx(gx2)I78BaW&@{7^MO;PcCx@gn^+b(=16J?FwA zPLcFJljZlHEthY%oso@Ky~3tmlJ~0LgEFN@d?g-jNVok^*T3K=y-sz4@ zg)(~nW_fn#caqlQ3yFUF3yJK;m4K&9qwX`LUe}pYulp>i-($Aa>p5FPu2a~c=M-tw z1FkSKf+j%IZp0Lc=!)yQ&XPvm=1YTa3#ESdh0>t=d}-WmwlwWJO`^NrxgDyy!iMc| zAOC*h=O7`U^Y8xd@4{a9FrzwS9YaVMY@(OxSW9Yz&Dbb&0c@IJ?Q0G! z8*2s+UayepTh2xznCxSC|5S`KT%Tp&yd)Db0l`qa*61_TpIRU zE{!r*N}~a*6%G5ZlE(d4O2mLg(saN)iR?d5qB7@63=oycW!#oZWIr75w+`4KjWajs zc4WU*D90{`1z&_a*IwAUIo!v;pLmcYWDz1dIy&t6wFUch@}A+2)fK8EztKt+C86!W zZAhr@s5aIc654KS+}9{39VF}^8G}a1HntPudeZ?LOatlI5KfDulRx1%>rB~3b^~m^ z06y%#?3D@Yj>rqcr%5DT%l@;pa-~7vS-N6xli@2R{p;P*WnGqZm~up7-d`h6^qMA( z`%DKhW`T&4LBJ`}1{SpMH!Eb;mg92Zx?h({<3gltlOqASV4Izmf*={yO-71XTWXhQ zI|&l5J$Xa=j$b0pdVem7y}y|WacUj8w^}4^)i=B z;{l67#05&k0QxTgaThB|8}?r(4f=1ChM5};5pjKF=28%K0a95zY1gh@*biS_PyxGj z>sI3##U2z1x%AlmAPLK9w#Jf`JI_A*Y}oT^3-;8hPXE_1By_s1-Hn9J+Jl6{W&7@Z zYWH*m1e++J+)~bNY#hqNzLX>bNQisf#Eq!*=b-f8|8!GE%z=wJe3r!b1G)OomWCNK z<%z!Y$k$=osD8K6ia%Rql6`85aSU1m{DsO+jOlE?ByDwzR z6_A_dPP)Fdyd~^Bz=6Hahzunm++&Wdu;tY3(o%ItFCxi1FXhVU1)Jo#VQ@z?zLLi8 zd?Am&@s%!--58cFV&F26X_-9Pcd^v#w?t9DF9?c^qjAOzY20_3M8KL8+~$4~nCsow z&%m{PRshS8kt~!5JWFKnX-MV03)=$kRY5s2eC*hcseLNT~am!WR;Xl zHtxl7##}6O*^%oqcD0{Jc6w#PFUO4A{W zC3?VYiS9K)V!Mx%-+#JP#;!apd#)7AdGL(xC%XnH0eM%XAxJ{po#_Gn=pri#%SyEy z94#l^*Auy3S+x6@44=AEUL7?}S`U~YNqxpkLdFzHL85KqWXM`veb07h)?#JEB^!{HXH!gZOAgX zaI>XJk8dTR*H`lDrwe54x}&o3T#=jt^R9#UY+z@P97{s&Sj{ab9fpMLNI)8v>JX>w zVlFh}SMuIM25~CKBb$G^BD1y~lL;G+$(O5tl+nw6lu^r%$*2`4<-=8H<%88{<^5G> zfK&3(8esKN8NK?5;^S38jOPATKgma{P6DSLoYZ|EuR1E9th)E*vN`8xxDS0daFU{` zU#Zx~O$T!rRM)K@ytoY_x|(Z~@tO^cD`Y;ht@Q z2f&`02QNs*)OFJ8-N_Qu3q*wVq19=UF;`*+ERr~svl@5#N(so-dcWOtTsB|v$QhIY z*}Omk+J2cNph*whV-6a0yT*=tOoFm@v;ZXZmYEvdBAln?R$sZCxLG7W-t@_ln`N-H zKDerW*>Sl{c3c(NaUF_v&0q&i`mP&gK#A-|&a&5yYh{Yv*UM!Wf%A4Fv4lB?@8;fw ziWTky-w9KuOmVIbFTXYt{@4HdUv*uJ+6eoL-jAiYIjSNrkA){AwIX4k!G?rXd>t78 z1W3qsIg*f-GYDyaJY3ouhjq<*gg3yJv%pc9uNiwzOZTsrNJ{UqAlo;RoC#O1&lG9e zb-YA(g9YsIrM&hDEZ~|0vi+(@PLY7P8y|%hkY!FIlNL)t-A5Y2HSzh1 zCBGm`atm%rPTno?loX2x<;1LFpXA~4e8kCy{9Cw$X=M}3b?hUqVZVag0L!|!9Ax1< zUIPwmS382o*;S1pyTlRqDm5LB6crWaTv@gJT1d!pC5IW+rEt2&dz}iuD)1Y!yB(!k zB(%6E5(=9Iyr6|&ihYIJb45o}l$OgaAIgb1gJr}6H;ZNR=EL&(M<|c=9Vdz1K9lIT zK9Q!s|47n%ekpH$JP+>S0oigfUrr-x_QB+}Bw)V3RLR3;5pXSpl#_zG6OJ*g%YM1cFb*F+ z{9s7P=`Cmtef~o2xl-iwN}k83WkkA?YGHuGAmTSGcS+ZeXGq)LpGbO_QPRHeSn2Y~ zZ24~8A=z{)OD-a6czH@d5`Yyxw6G*M)mlNH8-yd#0wiSGZ2BB>a^4%JPv^2BDmE<6$AC?u5Ek+oqx0P#qG6b4TOS+ez<1oOjfc z5PBR+LMWk>;U(x8Lo?p6#E){A7f|AqNnS~*Q!yfP(NiWrUN4lH8}`b8FK0@xk0;5n z2@7TN`aQDie3qO?badl58Y36wPg)czvzGr@5d&fcn@VsJ(?W~mK&K$2MI_jl)aOx> zm6f41$aE?kLUO7O1sqavQw-S&?C#@n8-DEU`)bW!b zp+2(%y|0oGacXKwKnqR1UQ<)y6cu4VVin*dwbhlHkZ{3*>UT8wxk+e2GJf{iXHKf> zPFAm8U89MU!{UCDkZzo9r&A)*iqN%#;@g@^^1{qJgYp-y#aT);_GZZ{tx&7SZ4%U| zFB{}xOF&+h+dReju&&14MG92;&AmZF*qhr)h;95QXtj(5b1W!`tYp+QlVpx1w6G*p zBHHHynRrhnV9*k3^N3~)Ofge#!QUh<-&)P2rVEnPWGcU!kdXMNfBGkF;J%wYEF`ob z8PAQ&bVv8bJx&L?l>;=_syR42X{N!?payiV)fl zq5#&4;QTITpK;=BP0LM`V#kgz7BCD`M{>*f8bCp9G@L4_VTaG(=IczM`J zXz`E#_>Yp6#hQh>6D|tcsZ*!0_q!wRm4sR`6p5uRR=F6mfUF=S$r8Md+lQr_u~o|0i7!}y$0;P>MRSc*O|RaPQ6pjgl=Hij~E6dRZZkxNNDj#fAmMP zZQC{{rpi*=VOE@z)=ffMB11x> z$W@V$uBjyy6%}O6bAZv#WY?><*khaG>C9StJT_TR!%)>qvpQ)x&lW7(1HrHjS+-*+I z>VR>CPM}*GOUOQ?ZehRAaOc$p+O%oo+@)@Wdcjc|;qG-CYD+@ff!j?&iV}sSfVu_U zJL`%CtwK<#4b-w|jE0Uzd5r=a#dn=g65}3jE8`}iLa#9iV|?^Rss41Iz1XlMB$z^` z)Wk$$8M1IXLlTGdu^EKjQWD}?^OWo(TMoyQAstZDgkq*S#C@G-;dS>y!WlDWg#U

_wG68x><=35`nmw$-Nua@ojcWD{>y*)FJbQ! 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

I{36t7LlSB{^VRUNy;FO1H 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 P00000NkvXXu0mjf)=Eu{ literal 0 HcmV?d00001 diff --git a/packages/components/nodes/responsibleAI/InputModeration/simple_moderation.png b/packages/components/nodes/responsibleAI/InputModeration/simple_moderation.png new file mode 100644 index 0000000000000000000000000000000000000000..47d78cb672fc6a3c77ddfb7fe57e79e16bf9d71f GIT binary patch literal 44837 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 literal 0 HcmV?d00001 diff --git a/packages/components/nodes/responsibleAI/ResponsibleAI.ts b/packages/components/nodes/responsibleAI/ResponsibleAI.ts new file mode 100644 index 00000000..0c75501f --- /dev/null +++ b/packages/components/nodes/responsibleAI/ResponsibleAI.ts @@ -0,0 +1,30 @@ +import { BaseLanguageModel } from 'langchain/base_language' +import { Server } from 'socket.io' + +export abstract class ResponsibleAI {} + +export abstract class Moderation extends ResponsibleAI { + abstract checkForViolations(llm: BaseLanguageModel, input: string): Promise +} + +export const checkInputs = async (inputModerations: Moderation[], llm: BaseLanguageModel, input: string): Promise => { + for (const moderation of inputModerations) { + input = await moderation.checkForViolations(llm, input) + } + return input +} + +// is this the correct location for this function? +// should we have a utils files that all node components can use? +export const streamResponse = (isStreaming: any, response: string, socketIO: Server, socketIOClientId: string) => { + if (isStreaming) { + const result = response.split(/(\s+)/) + result.forEach((token: string, index: number) => { + if (index === 0) { + socketIO.to(socketIOClientId).emit('start', token) + } + socketIO.to(socketIOClientId).emit('token', token) + }) + socketIO.to(socketIOClientId).emit('end') + } +} From 77ef39698b4fe11dfea04536723c6c28e4db1d1b Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Mon, 13 Nov 2023 21:46:36 +0530 Subject: [PATCH 15/64] MongoDB Atlas Integration: Adding MongoDB Memory --- .../credentials/MongoDBUrlApi.credential.ts | 25 ++++ .../memory/MongoDBMemory/MongoDBMemory.ts | 115 ++++++++++++++++++ .../nodes/memory/MongoDBMemory/mongodb.png | Bin 0 -> 3741 bytes packages/components/package.json | 1 + 4 files changed, 141 insertions(+) create mode 100644 packages/components/credentials/MongoDBUrlApi.credential.ts create mode 100644 packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts create mode 100644 packages/components/nodes/memory/MongoDBMemory/mongodb.png diff --git a/packages/components/credentials/MongoDBUrlApi.credential.ts b/packages/components/credentials/MongoDBUrlApi.credential.ts new file mode 100644 index 00000000..2f2cba38 --- /dev/null +++ b/packages/components/credentials/MongoDBUrlApi.credential.ts @@ -0,0 +1,25 @@ +import { INodeParams, INodeCredential } from '../src/Interface' + +class MongoDBUrlApi implements INodeCredential { + label: string + name: string + version: number + description: string + inputs: INodeParams[] + + constructor() { + this.label = 'MongoDB ATLAS' + this.name = 'mongoDBUrlApi' + this.version = 1.0 + this.inputs = [ + { + label: 'ATLAS Connection URL', + name: 'mongoDBConnectUrl', + type: 'string', + placeholder: 'mongodb+srv://myDatabaseUser:D1fficultP%40ssw0rd@cluster0.example.mongodb.net/?retryWrites=true&w=majority' + } + ] + } +} + +module.exports = { credClass: MongoDBUrlApi } diff --git a/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts b/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts new file mode 100644 index 00000000..4c9e8581 --- /dev/null +++ b/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts @@ -0,0 +1,115 @@ +import { getBaseClasses, getCredentialData, getCredentialParam, ICommonObject, INode, INodeData, INodeParams } from '../../../src' +import { MongoDBChatMessageHistory } from 'langchain/stores/message/mongodb' +import { BufferMemory, BufferMemoryInput } from 'langchain/memory' +import { MongoClient } from 'mongodb' + +class MongoDB_Memory implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + baseClasses: string[] + credential: INodeParams + inputs: INodeParams[] + + constructor() { + this.label = 'MongoDB Atlas Chat Memory' + this.name = 'MongoDBAtlasChatMemory' + this.version = 1.0 + this.type = 'MongoDBAtlasChatMemory' + this.icon = 'mongodb.png' + this.category = 'Memory' + this.description = 'Stores the conversation in MongoDB Atlas' + this.baseClasses = [this.type, ...getBaseClasses(BufferMemory)] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['mongoDBUrlApi'] + } + this.inputs = [ + { + label: 'Database', + name: 'databaseName', + placeholder: '', + type: 'string' + }, + { + label: 'Collection Name', + name: 'collectionName', + placeholder: '', + type: 'string' + }, + { + label: 'Session ID', + name: 'sessionId', + type: 'string', + default: '5f9cf7c08d5b1a06b80fae61', + description: 'Must be an Hex String of 24 chars. This will be the objectId of the document in MongoDB Atlas' + }, + { + label: 'Memory Key', + name: 'memoryKey', + type: 'string', + default: 'chat_history', + additionalParams: true + } + ] + } + + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + return initializeMongoDB(nodeData, options) + } + + async clearSessionMemory(nodeData: INodeData, options: ICommonObject): Promise { + const mongodbMemory = await initializeMongoDB(nodeData, options) + const sessionId = nodeData.inputs?.sessionId as string + options.logger.info(`Clearing MongoDB memory session ${sessionId}`) + await mongodbMemory.clear() + options.logger.info(`Successfully cleared MongoDB memory session ${sessionId}`) + } +} + +const initializeMongoDB = async (nodeData: INodeData, options: ICommonObject): Promise => { + const databaseName = nodeData.inputs?.databaseName as string + const collectionName = nodeData.inputs?.collectionName as string + const sessionId = nodeData.inputs?.sessionId as string + const memoryKey = nodeData.inputs?.memoryKey as string + + const credentialData = await getCredentialData(nodeData.credential ?? '', options) + let mongoDBConnectUrl = getCredentialParam('mongoDBConnectUrl', credentialData, nodeData) + + const client = new MongoClient(mongoDBConnectUrl) + await client.connect() + const collection = client.db(databaseName).collection(collectionName) + + const mongoDBChatMessageHistory = new MongoDBChatMessageHistory({ + collection, + sessionId: sessionId + }) + + return new BufferMemoryExtended({ + memoryKey, + chatHistory: mongoDBChatMessageHistory, + returnMessages: true, + isSessionIdUsingChatMessageId: false + }) +} + +interface BufferMemoryExtendedInput { + isSessionIdUsingChatMessageId: boolean +} + +class BufferMemoryExtended extends BufferMemory { + isSessionIdUsingChatMessageId? = false + + constructor(fields: BufferMemoryInput & Partial) { + super(fields) + this.isSessionIdUsingChatMessageId = fields.isSessionIdUsingChatMessageId + } +} + +module.exports = { nodeClass: MongoDB_Memory } diff --git a/packages/components/nodes/memory/MongoDBMemory/mongodb.png b/packages/components/nodes/memory/MongoDBMemory/mongodb.png new file mode 100644 index 0000000000000000000000000000000000000000..5586fe0ac672f7997014d814389c1d6c436d9d0c GIT binary patch literal 3741 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 literal 0 HcmV?d00001 diff --git a/packages/components/package.json b/packages/components/package.json index 996419ca..ee2adbf9 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -55,6 +55,7 @@ "llmonitor": "^0.5.5", "mammoth": "^1.5.1", "moment": "^2.29.3", + "mongodb": "^6.2.0", "mysql2": "^3.5.1", "node-fetch": "^2.6.11", "node-html-markdown": "^1.3.0", From 261e45d74a2f912dd0ab6e356fef90c97b420f3f Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Mon, 13 Nov 2023 21:56:18 +0530 Subject: [PATCH 16/64] MongoDB Atlas Integration: Adding MongoDB as a Vector Store --- .../vectorstores/MongoDB/MongoDBSearchBase.ts | 145 ++++++++++++++++++ .../vectorstores/MongoDB/MongoDB_Existing.ts | 41 +++++ .../vectorstores/MongoDB/MongoDB_Upsert.ts | 58 +++++++ .../nodes/vectorstores/MongoDB/mongodb.png | Bin 0 -> 3741 bytes 4 files changed, 244 insertions(+) create mode 100644 packages/components/nodes/vectorstores/MongoDB/MongoDBSearchBase.ts create mode 100644 packages/components/nodes/vectorstores/MongoDB/MongoDB_Existing.ts create mode 100644 packages/components/nodes/vectorstores/MongoDB/MongoDB_Upsert.ts create mode 100644 packages/components/nodes/vectorstores/MongoDB/mongodb.png diff --git a/packages/components/nodes/vectorstores/MongoDB/MongoDBSearchBase.ts b/packages/components/nodes/vectorstores/MongoDB/MongoDBSearchBase.ts new file mode 100644 index 00000000..e9ef8e9a --- /dev/null +++ b/packages/components/nodes/vectorstores/MongoDB/MongoDBSearchBase.ts @@ -0,0 +1,145 @@ +import { + getBaseClasses, + getCredentialData, + getCredentialParam, + ICommonObject, + INodeData, + INodeOutputsValue, + INodeParams +} from '../../../src' + +import { Embeddings } from 'langchain/embeddings/base' +import { VectorStore } from 'langchain/vectorstores/base' +import { Document } from 'langchain/document' +import { MongoDBAtlasVectorSearch } from 'langchain/vectorstores/mongodb_atlas' +import { Collection, MongoClient } from 'mongodb' + +export abstract class MongoDBSearchBase { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + baseClasses: string[] + inputs: INodeParams[] + credential: INodeParams + outputs: INodeOutputsValue[] + mongoClient: MongoClient + + protected constructor() { + this.type = 'MongoDB Atlas' + this.icon = 'mongodb.png' + this.category = 'Vector Stores' + this.baseClasses = [this.type, 'VectorStoreRetriever', 'BaseRetriever'] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['mongoDBUrlApi'] + } + this.inputs = [ + { + label: 'Embeddings', + name: 'embeddings', + type: 'Embeddings' + }, + { + label: 'Database', + name: 'databaseName', + placeholder: '', + type: 'string' + }, + { + label: 'Collection Name', + name: 'collectionName', + placeholder: '', + type: 'string' + }, + { + label: 'Index Name', + name: 'indexName', + placeholder: '', + type: 'string' + }, + { + label: 'Content Field', + name: 'textKey', + description: 'Name of the field (column) that contains the actual content', + type: 'string', + default: 'text', + additionalParams: true, + optional: true + }, + { + label: 'Embedded Field', + name: 'embeddingKey', + description: 'Name of the field (column) that contains the Embedding', + type: 'string', + default: 'embedding', + additionalParams: true, + optional: true + }, + { + label: 'Top K', + name: 'topK', + description: 'Number of top results to fetch. Default to 4', + placeholder: '4', + type: 'number', + additionalParams: true, + optional: true + } + ] + this.outputs = [ + { + label: 'MongoDB Retriever', + name: 'retriever', + baseClasses: this.baseClasses + }, + { + label: 'MongoDB Vector Store', + name: 'vectorStore', + baseClasses: [this.type, ...getBaseClasses(MongoDBAtlasVectorSearch)] + } + ] + } + + abstract constructVectorStore( + embeddings: Embeddings, + collection: Collection, + indexName: string, + textKey: string, + embeddingKey: string, + docs: Document>[] | undefined + ): Promise + + async init(nodeData: INodeData, _: string, options: ICommonObject, docs: Document>[] | undefined): Promise { + const credentialData = await getCredentialData(nodeData.credential ?? '', options) + const databaseName = nodeData.inputs?.databaseName as string + const collectionName = nodeData.inputs?.collectionName as string + const indexName = nodeData.inputs?.indexName as string + let textKey = nodeData.inputs?.textKey as string + let embeddingKey = nodeData.inputs?.embeddingKey as string + const embeddings = nodeData.inputs?.embeddings as Embeddings + const topK = nodeData.inputs?.topK as string + const k = topK ? parseFloat(topK) : 4 + const output = nodeData.outputs?.output as string + + let mongoDBConnectUrl = getCredentialParam('mongoDBConnectUrl', credentialData, nodeData) + + this.mongoClient = new MongoClient(mongoDBConnectUrl) + const collection = this.mongoClient.db(databaseName).collection(collectionName) + if (!textKey || textKey === '') textKey = 'text' + if (!embeddingKey || embeddingKey === '') embeddingKey = 'embedding' + const vectorStore = await this.constructVectorStore(embeddings, collection, indexName, textKey, embeddingKey, docs) + + if (output === 'retriever') { + return vectorStore.asRetriever(k) + } else if (output === 'vectorStore') { + ;(vectorStore as any).k = k + return vectorStore + } + return vectorStore + } +} diff --git a/packages/components/nodes/vectorstores/MongoDB/MongoDB_Existing.ts b/packages/components/nodes/vectorstores/MongoDB/MongoDB_Existing.ts new file mode 100644 index 00000000..3cbb36b8 --- /dev/null +++ b/packages/components/nodes/vectorstores/MongoDB/MongoDB_Existing.ts @@ -0,0 +1,41 @@ +import { ICommonObject, INode, INodeData } from '../../../src/Interface' +import { Embeddings } from 'langchain/embeddings/base' +import { VectorStore } from 'langchain/vectorstores/base' +import { Document } from 'langchain/document' + +import { MongoDBSearchBase } from './MongoDBSearchBase' +import { Collection } from 'mongodb' +import { MongoDBAtlasVectorSearch } from 'langchain/vectorstores/mongodb_atlas' + +class MongoDBExisting_VectorStores extends MongoDBSearchBase implements INode { + constructor() { + super() + this.label = 'MongoDB Atlas Load Existing Index' + this.name = 'MongoDBIndex' + this.version = 1.0 + this.description = 'Load existing data from MongoDB Atlas (i.e: Document has been upserted)' + } + + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + return super.init(nodeData, _, options, undefined) + } + + constructVectorStore( + embeddings: Embeddings, + collection: Collection, + indexName: string, + textKey: string, + embeddingKey: string, + _: Document>[] | undefined + ): Promise { + const mongoDBAtlasVectorSearch = new MongoDBAtlasVectorSearch(embeddings, { + collection: collection, + indexName: indexName, + textKey: textKey, + embeddingKey: embeddingKey + }) + return Promise.resolve(mongoDBAtlasVectorSearch) + } +} + +module.exports = { nodeClass: MongoDBExisting_VectorStores } diff --git a/packages/components/nodes/vectorstores/MongoDB/MongoDB_Upsert.ts b/packages/components/nodes/vectorstores/MongoDB/MongoDB_Upsert.ts new file mode 100644 index 00000000..80dfbf19 --- /dev/null +++ b/packages/components/nodes/vectorstores/MongoDB/MongoDB_Upsert.ts @@ -0,0 +1,58 @@ +import { ICommonObject, INode, INodeData } from '../../../src/Interface' +import { Embeddings } from 'langchain/embeddings/base' +import { Document } from 'langchain/document' + +import { flatten } from 'lodash' +import { VectorStore } from 'langchain/vectorstores/base' +import { MongoDBSearchBase } from './MongoDBSearchBase' +import { Collection } from 'mongodb' +import { MongoDBAtlasVectorSearch } from 'langchain/vectorstores/mongodb_atlas' + +class MongoDBUpsert_VectorStores extends MongoDBSearchBase implements INode { + constructor() { + super() + this.label = 'MongoDB Upsert Document' + this.name = 'MongoDBUpsert' + this.version = 1.0 + this.description = 'Upsert documents to MongoDB Atlas' + this.inputs.unshift({ + label: 'Document', + name: 'document', + type: 'Document', + list: true + }) + } + + constructVectorStore( + embeddings: Embeddings, + collection: Collection, + indexName: string, + textKey: string, + embeddingKey: string, + docs: Document>[] + ): Promise { + return MongoDBAtlasVectorSearch.fromDocuments(docs, embeddings, { + collection: collection, + indexName: indexName, + textKey: textKey, + embeddingKey: embeddingKey + }) + } + + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + const docs = nodeData.inputs?.document as Document[] + + const flattenDocs = docs && docs.length ? flatten(docs) : [] + const finalDocs = [] + for (let i = 0; i < flattenDocs.length; i += 1) { + if (flattenDocs[i] && flattenDocs[i].pageContent) { + const document = new Document(flattenDocs[i]) + finalDocs.push(document) + } + } + + return super.init(nodeData, _, options, flattenDocs) + } +} + +module.exports = { nodeClass: MongoDBUpsert_VectorStores } diff --git a/packages/components/nodes/vectorstores/MongoDB/mongodb.png b/packages/components/nodes/vectorstores/MongoDB/mongodb.png new file mode 100644 index 0000000000000000000000000000000000000000..5586fe0ac672f7997014d814389c1d6c436d9d0c GIT binary patch literal 3741 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 literal 0 HcmV?d00001 From f21f5257cac9a50661e2408017729641f00a69f5 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Tue, 14 Nov 2023 14:35:47 +0530 Subject: [PATCH 17/64] UX Changes: Ability to view as table and search on the dashboard --- .../src/ui-component/button/StyledButton.js | 8 ++ .../src/ui-component/table/FlowListTable.js | 133 ++++++++++++++++++ .../ui/src/ui-component/toolbar/Toolbar.js | 24 ++++ packages/ui/src/views/chatflows/index.js | 96 ++++++++++--- 4 files changed, 240 insertions(+), 21 deletions(-) create mode 100644 packages/ui/src/ui-component/table/FlowListTable.js create mode 100644 packages/ui/src/ui-component/toolbar/Toolbar.js diff --git a/packages/ui/src/ui-component/button/StyledButton.js b/packages/ui/src/ui-component/button/StyledButton.js index 6e0c7078..29e17f80 100644 --- a/packages/ui/src/ui-component/button/StyledButton.js +++ b/packages/ui/src/ui-component/button/StyledButton.js @@ -1,5 +1,6 @@ import { styled } from '@mui/material/styles' import { Button } from '@mui/material' +import MuiToggleButton from '@mui/material/ToggleButton' export const StyledButton = styled(Button)(({ theme, color = 'primary' }) => ({ color: 'white', @@ -9,3 +10,10 @@ export const StyledButton = styled(Button)(({ theme, color = 'primary' }) => ({ backgroundImage: `linear-gradient(rgb(0 0 0/10%) 0 0)` } })) + +export const StyledToggleButton = styled(MuiToggleButton)(({ theme, color = 'primary' }) => ({ + '&.Mui-selected, &.Mui-selected:hover': { + color: 'white', + backgroundColor: theme.palette[color].main + } +})) diff --git a/packages/ui/src/ui-component/table/FlowListTable.js b/packages/ui/src/ui-component/table/FlowListTable.js new file mode 100644 index 00000000..819a49cb --- /dev/null +++ b/packages/ui/src/ui-component/table/FlowListTable.js @@ -0,0 +1,133 @@ +import PropTypes from 'prop-types' +import { useNavigate } from 'react-router-dom' +import { IconEdit } from '@tabler/icons' +import moment from 'moment' +import { styled } from '@mui/material/styles' +import Table from '@mui/material/Table' +import TableBody from '@mui/material/TableBody' +import TableCell, { tableCellClasses } from '@mui/material/TableCell' +import TableContainer from '@mui/material/TableContainer' +import TableHead from '@mui/material/TableHead' +import TableRow from '@mui/material/TableRow' +import Paper from '@mui/material/Paper' +import { Button, Typography } from '@mui/material' + +const StyledTableCell = styled(TableCell)(({ theme }) => ({ + [`&.${tableCellClasses.head}`]: { + backgroundColor: theme.palette.common.black, + color: theme.palette.common.white + }, + [`&.${tableCellClasses.body}`]: { + fontSize: 14 + } +})) + +const StyledTableRow = styled(TableRow)(({ theme }) => ({ + '&:nth-of-type(odd)': { + backgroundColor: theme.palette.action.hover + }, + // hide last border + '&:last-child td, &:last-child th': { + border: 0 + } +})) + +export const FlowListTable = ({ data, images, filterFunction }) => { + const navigate = useNavigate() + const goToCanvas = (selectedChatflow) => { + navigate(`/canvas/${selectedChatflow.id}`) + } + return ( + <> + + + + + + Name + + + Nodes + + + Last Modified Date + + + Actions + + + + + {data.filter(filterFunction).map((row, index) => ( + + + + {row.templateName || row.name} + + + + + {images[row.id] && ( +
+ {images[row.id].map((img) => ( +
+ +
+ ))} +
+ )} +
+ {moment(row.updatedDate).format('dddd, MMMM Do, YYYY h:mm:ss A')} + + + +
+ ))} +
+
+
+ + ) +} + +FlowListTable.propTypes = { + data: PropTypes.object, + images: PropTypes.array, + filterFunction: PropTypes.func +} diff --git a/packages/ui/src/ui-component/toolbar/Toolbar.js b/packages/ui/src/ui-component/toolbar/Toolbar.js new file mode 100644 index 00000000..f72ba339 --- /dev/null +++ b/packages/ui/src/ui-component/toolbar/Toolbar.js @@ -0,0 +1,24 @@ +import * as React from 'react' +import ViewListIcon from '@mui/icons-material/ViewList' +import ViewModuleIcon from '@mui/icons-material/ViewModule' +import ToggleButtonGroup from '@mui/material/ToggleButtonGroup' +import { StyledToggleButton } from '../button/StyledButton' + +export default function Toolbar() { + const [view, setView] = React.useState('list') + + const handleChange = (event, nextView) => { + setView(nextView) + } + + return ( + + + + + + + + + ) +} diff --git a/packages/ui/src/views/chatflows/index.js b/packages/ui/src/views/chatflows/index.js index 6712623e..e01a9373 100644 --- a/packages/ui/src/views/chatflows/index.js +++ b/packages/ui/src/views/chatflows/index.js @@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom' import { useSelector } from 'react-redux' // material-ui -import { Grid, Box, Stack } from '@mui/material' +import { Grid, Box, Stack, Toolbar, ToggleButton, ButtonGroup, Typography, InputAdornment, TextField } from '@mui/material' import { useTheme } from '@mui/material/styles' // project imports @@ -11,7 +11,6 @@ import MainCard from 'ui-component/cards/MainCard' import ItemCard from 'ui-component/cards/ItemCard' import { gridSpacing } from 'store/constant' import WorkflowEmptySVG from 'assets/images/workflow_empty.svg' -import { StyledButton } from 'ui-component/button/StyledButton' import LoginDialog from 'ui-component/dialog/LoginDialog' // API @@ -24,7 +23,13 @@ import useApi from 'hooks/useApi' import { baseURL } from 'store/constant' // icons -import { IconPlus } from '@tabler/icons' +import { IconPlus, IconSearch } from '@tabler/icons' +import * as React from 'react' +import ViewListIcon from '@mui/icons-material/ViewList' +import ViewModuleIcon from '@mui/icons-material/ViewModule' +import ToggleButtonGroup from '@mui/material/ToggleButtonGroup' +import { FlowListTable } from '../../ui-component/table/FlowListTable' +import { StyledButton } from '../../ui-component/button/StyledButton' // ==============================|| CHATFLOWS ||============================== // @@ -35,10 +40,24 @@ const Chatflows = () => { const [isLoading, setLoading] = useState(true) const [images, setImages] = useState({}) + const [search, setSearch] = useState('') const [loginDialogOpen, setLoginDialogOpen] = useState(false) const [loginDialogProps, setLoginDialogProps] = useState({}) const getAllChatflowsApi = useApi(chatflowsApi.getAllChatflows) + const [view, setView] = React.useState('card') + + const handleChange = (event, nextView) => { + setView(nextView) + } + + const onSearchChange = (event) => { + setSearch(event.target.value) + } + + function filterFlows(data) { + return data.name.toLowerCase().indexOf(search.toLowerCase()) > -1 + } const onLoginClick = (username, password) => { localStorage.setItem('username', username) @@ -102,26 +121,61 @@ const Chatflows = () => { return ( - -

Chatflows

- - - - }> - Add New - + + + + + Chatflows + + + + + + ) + }} + /> + + + + + + + + + + + + + + + }> + Add New + + + + + + {!isLoading && (!view || view === 'card') && getAllChatflowsApi.data && ( + + {getAllChatflowsApi.data.filter(filterFlows).map((data, index) => ( + + goToCanvas(data)} data={data} images={images[data.id]} /> + + ))} - + )} + {!isLoading && view === 'list' && getAllChatflowsApi.data && ( + + )} - - {!isLoading && - getAllChatflowsApi.data && - getAllChatflowsApi.data.map((data, index) => ( - - goToCanvas(data)} data={data} images={images[data.id]} /> - - ))} - + {!isLoading && (!getAllChatflowsApi.data || getAllChatflowsApi.data.length === 0) && ( From 77994ce2178e2c7ba456a1a950bf6c89cde83040 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Tue, 14 Nov 2023 15:15:34 +0530 Subject: [PATCH 18/64] UX Changes: adding a placeholder for chatflow search. --- packages/ui/src/views/chatflows/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ui/src/views/chatflows/index.js b/packages/ui/src/views/chatflows/index.js index e01a9373..f7d1497d 100644 --- a/packages/ui/src/views/chatflows/index.js +++ b/packages/ui/src/views/chatflows/index.js @@ -132,6 +132,7 @@ const Chatflows = () => { size='small' sx={{ width: 400 }} variant='outlined' + placeholder='Search Chatflows' onChange={onSearchChange} InputProps={{ startAdornment: ( From 7ef817bc996200d89c4d5f11d4a556f2ac5a16df Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Tue, 14 Nov 2023 15:48:14 +0530 Subject: [PATCH 19/64] UX Changes: limiting display of node icons to 5 and with label to indicate additional. --- packages/ui/src/ui-component/table/FlowListTable.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/ui-component/table/FlowListTable.js b/packages/ui/src/ui-component/table/FlowListTable.js index 819a49cb..896ce3ea 100644 --- a/packages/ui/src/ui-component/table/FlowListTable.js +++ b/packages/ui/src/ui-component/table/FlowListTable.js @@ -37,6 +37,7 @@ export const FlowListTable = ({ data, images, filterFunction }) => { const goToCanvas = (selectedChatflow) => { navigate(`/canvas/${selectedChatflow.id}`) } + let nodeCount = 0 return ( <> @@ -53,7 +54,7 @@ export const FlowListTable = ({ data, images, filterFunction }) => { Name - Nodes + Nodes (Showing first 5) Last Modified Date @@ -84,7 +85,7 @@ export const FlowListTable = ({ data, images, filterFunction }) => { marginTop: 5 }} > - {images[row.id].map((img) => ( + {images[row.id].slice(0, images[row.id].length > 5 ? 5 : images[row.id].length).map((img) => (
{ />
))} + {images[row.id].length > 5 && ( + + + {images[row.id].length - 5} More + + )}
)} From 57b31130397ca0097ef04db78137cccc79d7b63e Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Tue, 14 Nov 2023 20:13:24 +0530 Subject: [PATCH 20/64] UX Changes: minor UI tweaks/adjustments and fixes for small screens --- .../src/ui-component/table/FlowListTable.js | 20 ++++++------- packages/ui/src/views/chatflows/index.js | 28 +++++++++++++------ 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/packages/ui/src/ui-component/table/FlowListTable.js b/packages/ui/src/ui-component/table/FlowListTable.js index 896ce3ea..9dfc9522 100644 --- a/packages/ui/src/ui-component/table/FlowListTable.js +++ b/packages/ui/src/ui-component/table/FlowListTable.js @@ -37,26 +37,20 @@ export const FlowListTable = ({ data, images, filterFunction }) => { const goToCanvas = (selectedChatflow) => { navigate(`/canvas/${selectedChatflow.id}`) } - let nodeCount = 0 + return ( <> - + Name - + Nodes (Showing first 5) - + Last Modified Date @@ -75,7 +69,7 @@ export const FlowListTable = ({ data, images, filterFunction }) => { - + {images[row.id] && (
{
)}
- {moment(row.updatedDate).format('dddd, MMMM Do, YYYY h:mm:ss A')} + + {moment(row.updatedDate).format('dddd, MMMM Do, YYYY h:mm:ss A')} + + ) + } + }) + } + } + + const handleDelete = async () => { + setAnchorEl(null) + const confirmPayload = { + title: `Delete`, + description: `Delete chatflow ${chatflow.name}?`, + confirmButtonName: 'Delete', + cancelButtonName: 'Cancel' + } + const isConfirmed = await confirm(confirmPayload) + + if (isConfirmed) { + try { + await chatflowsApi.deleteChatflow(chatflow.id) + await updateFlowsApi.request() + } catch (error) { + const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` + enqueueSnackbar({ + message: errorData, + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + persist: true, + action: (key) => ( + + ) + } + }) + } + } + } + + const handleDuplicate = () => { + setAnchorEl(null) + try { + localStorage.setItem('duplicatedFlowData', chatflow.flowData) + window.open(`${uiBaseURL}/canvas`, '_blank') + } catch (e) { + console.error(e) + } + } + const handleExport = () => { + setAnchorEl(null) + try { + const flowData = JSON.parse(chatflow.flowData) + let dataStr = JSON.stringify(generateExportFlowData(flowData), null, 2) + let dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr) + + let exportFileDefaultName = `${chatflow.name} Chatflow.json` + + let linkElement = document.createElement('a') + linkElement.setAttribute('href', dataUri) + linkElement.setAttribute('download', exportFileDefaultName) + linkElement.click() + } catch (e) { + console.error(e) + } + } + + return ( +
+ + + + + Rename + + + + Duplicate + + + + Export + + + + + Delete + + + + setFlowDialogOpen(false)} + onConfirm={saveFlowRename} + /> +
+ ) +} + +FlowListMenu.propTypes = { + chatflow: PropTypes.object, + updateFlowsApi: PropTypes.object +} diff --git a/packages/ui/src/ui-component/table/FlowListTable.js b/packages/ui/src/ui-component/table/FlowListTable.js index 9dfc9522..8dc25e96 100644 --- a/packages/ui/src/ui-component/table/FlowListTable.js +++ b/packages/ui/src/ui-component/table/FlowListTable.js @@ -1,6 +1,5 @@ import PropTypes from 'prop-types' import { useNavigate } from 'react-router-dom' -import { IconEdit } from '@tabler/icons' import moment from 'moment' import { styled } from '@mui/material/styles' import Table from '@mui/material/Table' @@ -10,7 +9,8 @@ import TableContainer from '@mui/material/TableContainer' import TableHead from '@mui/material/TableHead' import TableRow from '@mui/material/TableRow' import Paper from '@mui/material/Paper' -import { Button, Typography } from '@mui/material' +import { Button, Stack, Typography } from '@mui/material' +import FlowListMenu from '../button/FlowListMenu' const StyledTableCell = styled(TableCell)(({ theme }) => ({ [`&.${tableCellClasses.head}`]: { @@ -32,7 +32,7 @@ const StyledTableRow = styled(TableRow)(({ theme }) => ({ } })) -export const FlowListTable = ({ data, images, filterFunction }) => { +export const FlowListTable = ({ data, images, filterFunction, updateFlowsApi }) => { const navigate = useNavigate() const goToCanvas = (selectedChatflow) => { navigate(`/canvas/${selectedChatflow.id}`) @@ -44,7 +44,7 @@ export const FlowListTable = ({ data, images, filterFunction }) => {
- + Name @@ -53,7 +53,7 @@ export const FlowListTable = ({ data, images, filterFunction }) => { Last Modified Date - + Actions @@ -65,7 +65,7 @@ export const FlowListTable = ({ data, images, filterFunction }) => { - {row.templateName || row.name} + @@ -111,15 +111,13 @@ export const FlowListTable = ({ data, images, filterFunction }) => { {moment(row.updatedDate).format('dddd, MMMM Do, YYYY h:mm:ss A')} - - + + + {/**/} + + ))} @@ -133,5 +131,6 @@ export const FlowListTable = ({ data, images, filterFunction }) => { FlowListTable.propTypes = { data: PropTypes.object, images: PropTypes.array, - filterFunction: PropTypes.func + filterFunction: PropTypes.func, + updateFlowsApi: PropTypes.object } diff --git a/packages/ui/src/views/chatflows/index.js b/packages/ui/src/views/chatflows/index.js index b7784a89..f008bfa8 100644 --- a/packages/ui/src/views/chatflows/index.js +++ b/packages/ui/src/views/chatflows/index.js @@ -23,10 +23,8 @@ import useApi from 'hooks/useApi' import { baseURL } from 'store/constant' // icons -import { IconPlus, IconSearch } from '@tabler/icons' +import { IconPlus, IconSearch, IconLayoutCards, IconLayoutColumns } from '@tabler/icons' import * as React from 'react' -import ViewListIcon from '@mui/icons-material/ViewList' -import ViewModuleIcon from '@mui/icons-material/ViewModule' import ToggleButtonGroup from '@mui/material/ToggleButtonGroup' import { FlowListTable } from '../../ui-component/table/FlowListTable' import { StyledButton } from '../../ui-component/button/StyledButton' @@ -159,10 +157,10 @@ const Chatflows = () => { > - + - + @@ -185,7 +183,13 @@ const Chatflows = () => { )} {!isLoading && view === 'list' && getAllChatflowsApi.data && ( - + )} From a7b34848cd3fe814b4e3820c2409c77f0d67c5ca Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Thu, 16 Nov 2023 08:29:06 +0530 Subject: [PATCH 22/64] UX Changes: Ability to set category (tags) to each chatflow; corresponding changes to table display and table search --- .../server/src/database/entities/ChatFlow.ts | 3 + .../1699900910291-AddCategoryToChatFlow.ts | 12 +++ .../src/database/migrations/mysql/index.ts | 4 +- .../1699900910291-AddCategoryToChatFlow.ts | 11 +++ .../src/database/migrations/postgres/index.ts | 4 +- .../1699900910291-AddCategoryToChatFlow.ts | 11 +++ .../src/database/migrations/sqlite/index.ts | 4 +- .../src/ui-component/button/FlowListMenu.js | 55 ++++++++++- .../ui/src/ui-component/dialog/TagDialog.js | 91 +++++++++++++++++++ .../src/ui-component/table/FlowListTable.js | 35 +++++-- packages/ui/src/views/chatflows/index.js | 7 +- 11 files changed, 220 insertions(+), 17 deletions(-) create mode 100644 packages/server/src/database/migrations/mysql/1699900910291-AddCategoryToChatFlow.ts create mode 100644 packages/server/src/database/migrations/postgres/1699900910291-AddCategoryToChatFlow.ts create mode 100644 packages/server/src/database/migrations/sqlite/1699900910291-AddCategoryToChatFlow.ts create mode 100644 packages/ui/src/ui-component/dialog/TagDialog.js diff --git a/packages/server/src/database/entities/ChatFlow.ts b/packages/server/src/database/entities/ChatFlow.ts index 376a100b..b3131c2e 100644 --- a/packages/server/src/database/entities/ChatFlow.ts +++ b/packages/server/src/database/entities/ChatFlow.ts @@ -36,4 +36,7 @@ export class ChatFlow implements IChatFlow { @UpdateDateColumn() updatedDate: Date + + @Column({ nullable: true, type: 'text' }) + category?: string } diff --git a/packages/server/src/database/migrations/mysql/1699900910291-AddCategoryToChatFlow.ts b/packages/server/src/database/migrations/mysql/1699900910291-AddCategoryToChatFlow.ts new file mode 100644 index 00000000..424f3b0e --- /dev/null +++ b/packages/server/src/database/migrations/mysql/1699900910291-AddCategoryToChatFlow.ts @@ -0,0 +1,12 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddCategoryToChatFlow1699900910291 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + const columnExists = await queryRunner.hasColumn('chat_flow', 'category') + if (!columnExists) queryRunner.query(`ALTER TABLE \`chat_flow\` ADD COLUMN \`category\` TEXT;`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`chat_flow\` DROP COLUMN \`category\`;`) + } +} diff --git a/packages/server/src/database/migrations/mysql/index.ts b/packages/server/src/database/migrations/mysql/index.ts index 4b7b8a95..53d652ee 100644 --- a/packages/server/src/database/migrations/mysql/index.ts +++ b/packages/server/src/database/migrations/mysql/index.ts @@ -8,6 +8,7 @@ import { AddAnalytic1694432361423 } from './1694432361423-AddAnalytic' import { AddChatHistory1694658767766 } from './1694658767766-AddChatHistory' import { AddAssistantEntity1699325775451 } from './1699325775451-AddAssistantEntity' import { AddUsedToolsToChatMessage1699481607341 } from './1699481607341-AddUsedToolsToChatMessage' +import { AddCategoryToChatFlow1699900910291 } from './1699900910291-AddCategoryToChatFlow' export const mysqlMigrations = [ Init1693840429259, @@ -19,5 +20,6 @@ export const mysqlMigrations = [ AddAnalytic1694432361423, AddChatHistory1694658767766, AddAssistantEntity1699325775451, - AddUsedToolsToChatMessage1699481607341 + AddUsedToolsToChatMessage1699481607341, + AddCategoryToChatFlow1699900910291 ] diff --git a/packages/server/src/database/migrations/postgres/1699900910291-AddCategoryToChatFlow.ts b/packages/server/src/database/migrations/postgres/1699900910291-AddCategoryToChatFlow.ts new file mode 100644 index 00000000..f5d96439 --- /dev/null +++ b/packages/server/src/database/migrations/postgres/1699900910291-AddCategoryToChatFlow.ts @@ -0,0 +1,11 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddCategoryToChatFlow1699900910291 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "chat_flow" ADD COLUMN IF NOT EXISTS "category" TEXT;`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "chat_flow" DROP COLUMN "category";`) + } +} diff --git a/packages/server/src/database/migrations/postgres/index.ts b/packages/server/src/database/migrations/postgres/index.ts index 75562c0b..70642eb6 100644 --- a/packages/server/src/database/migrations/postgres/index.ts +++ b/packages/server/src/database/migrations/postgres/index.ts @@ -8,6 +8,7 @@ import { AddAnalytic1694432361423 } from './1694432361423-AddAnalytic' import { AddChatHistory1694658756136 } from './1694658756136-AddChatHistory' import { AddAssistantEntity1699325775451 } from './1699325775451-AddAssistantEntity' import { AddUsedToolsToChatMessage1699481607341 } from './1699481607341-AddUsedToolsToChatMessage' +import { AddCategoryToChatFlow1699900910291 } from './1699900910291-AddCategoryToChatFlow' export const postgresMigrations = [ Init1693891895163, @@ -19,5 +20,6 @@ export const postgresMigrations = [ AddAnalytic1694432361423, AddChatHistory1694658756136, AddAssistantEntity1699325775451, - AddUsedToolsToChatMessage1699481607341 + AddUsedToolsToChatMessage1699481607341, + AddCategoryToChatFlow1699900910291 ] diff --git a/packages/server/src/database/migrations/sqlite/1699900910291-AddCategoryToChatFlow.ts b/packages/server/src/database/migrations/sqlite/1699900910291-AddCategoryToChatFlow.ts new file mode 100644 index 00000000..270b2998 --- /dev/null +++ b/packages/server/src/database/migrations/sqlite/1699900910291-AddCategoryToChatFlow.ts @@ -0,0 +1,11 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddCategoryToChatFlow1699900910291 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "chat_flow" ADD COLUMN "category" TEXT;`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "chat_flow" DROP COLUMN "category";`) + } +} diff --git a/packages/server/src/database/migrations/sqlite/index.ts b/packages/server/src/database/migrations/sqlite/index.ts index 4a14fc40..fe7611ea 100644 --- a/packages/server/src/database/migrations/sqlite/index.ts +++ b/packages/server/src/database/migrations/sqlite/index.ts @@ -8,6 +8,7 @@ import { AddAnalytic1694432361423 } from './1694432361423-AddAnalytic' import { AddChatHistory1694657778173 } from './1694657778173-AddChatHistory' import { AddAssistantEntity1699325775451 } from './1699325775451-AddAssistantEntity' import { AddUsedToolsToChatMessage1699481607341 } from './1699481607341-AddUsedToolsToChatMessage' +import { AddCategoryToChatFlow1699900910291 } from './1699900910291-AddCategoryToChatFlow' export const sqliteMigrations = [ Init1693835579790, @@ -19,5 +20,6 @@ export const sqliteMigrations = [ AddAnalytic1694432361423, AddChatHistory1694657778173, AddAssistantEntity1699325775451, - AddUsedToolsToChatMessage1699481607341 + AddUsedToolsToChatMessage1699481607341, + AddCategoryToChatFlow1699900910291 ] diff --git a/packages/ui/src/ui-component/button/FlowListMenu.js b/packages/ui/src/ui-component/button/FlowListMenu.js index fb759505..f4ffbd32 100644 --- a/packages/ui/src/ui-component/button/FlowListMenu.js +++ b/packages/ui/src/ui-component/button/FlowListMenu.js @@ -7,6 +7,7 @@ import Divider from '@mui/material/Divider' import FileCopyIcon from '@mui/icons-material/FileCopy' import FileDownloadIcon from '@mui/icons-material/Downloading' import FileDeleteIcon from '@mui/icons-material/Delete' +import FileCategoryIcon from '@mui/icons-material/Category' import Button from '@mui/material/Button' import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown' import PropTypes from 'prop-types' @@ -22,6 +23,7 @@ import ConfirmDialog from '../dialog/ConfirmDialog' import SaveChatflowDialog from '../dialog/SaveChatflowDialog' import { useState } from 'react' import useApi from '../../hooks/useApi' +import TagDialog from '../dialog/TagDialog' const StyledMenu = styled((props) => ( dispatch(enqueueSnackbarAction(...args)) const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) - const [anchorEl, setAnchorEl] = React.useState(null) const open = Boolean(anchorEl) const handleClick = (event) => { @@ -79,12 +83,10 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) { const handleClose = () => { setAnchorEl(null) } - const handleFlowRename = () => { setAnchorEl(null) setFlowDialogOpen(true) } - const saveFlowRename = async (chatflowName) => { const updateBody = { name: chatflowName, @@ -110,7 +112,39 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) { }) } } - + const handleFlowCategory = () => { + setAnchorEl(null) + if (chatflow.category) setCategoryValues(chatflow.category.split(';')) + else setCategoryValues([]) + setCategoryDialogOpen(true) + } + const saveFlowCategory = async (categories) => { + // save categories as string + const categoryTags = categories.join(';') + const updateBody = { + category: categoryTags, + chatflow + } + try { + await updateChatflowApi.request(chatflow.id, updateBody) + await updateFlowsApi.request() + } catch (error) { + const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` + enqueueSnackbar({ + message: errorData, + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + persist: true, + action: (key) => ( + + ) + } + }) + } + } const handleDelete = async () => { setAnchorEl(null) const confirmPayload = { @@ -143,7 +177,6 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) { } } } - const handleDuplicate = () => { setAnchorEl(null) try { @@ -206,6 +239,11 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) { Export + + + Update Category + + Delete @@ -222,6 +260,13 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) { onCancel={() => setFlowDialogOpen(false)} onConfirm={saveFlowRename} /> + setCategoryDialogOpen(false)} + tags={categoryValues} + setTags={setCategoryValues} + onSubmit={saveFlowCategory} + /> ) } diff --git a/packages/ui/src/ui-component/dialog/TagDialog.js b/packages/ui/src/ui-component/dialog/TagDialog.js new file mode 100644 index 00000000..be133fa4 --- /dev/null +++ b/packages/ui/src/ui-component/dialog/TagDialog.js @@ -0,0 +1,91 @@ +import { useState } from 'react' +import Dialog from '@mui/material/Dialog' +import Box from '@mui/material/Box' +import Button from '@mui/material/Button' +import TextField from '@mui/material/TextField' +import Chip from '@mui/material/Chip' +import PropTypes from 'prop-types' +import { DialogActions, DialogContent, DialogTitle } from '@mui/material' + +const TagDialog = ({ isOpen, onClose, tags, setTags, onSubmit }) => { + const [inputValue, setInputValue] = useState('') + + const handleInputChange = (event) => { + setInputValue(event.target.value) + } + + const handleInputKeyDown = (event) => { + if (event.key === 'Enter' && inputValue.trim()) { + event.preventDefault() + if (!tags.includes(inputValue)) { + setTags([...tags, inputValue]) + setInputValue('') + } + } + } + + const handleDeleteTag = (tagToDelete) => { + setTags(tags.filter((tag) => tag !== tagToDelete)) + } + + const handleSubmit = (event) => { + event.preventDefault() + onSubmit(tags) + onClose() + } + + return ( + + + Set Chatflow Category Tags + + + +
+
+ {tags.map((tag, index) => ( + handleDeleteTag(tag)} + style={{ marginRight: 5, marginBottom: 5 }} + /> + ))} +
+ + +
+
+ + + + +
+ ) +} + +TagDialog.propTypes = { + isOpen: PropTypes.bool, + onClose: PropTypes.func, + tags: PropTypes.array, + setTags: PropTypes.func, + onSubmit: PropTypes.func +} + +export default TagDialog diff --git a/packages/ui/src/ui-component/table/FlowListTable.js b/packages/ui/src/ui-component/table/FlowListTable.js index 8dc25e96..08caed57 100644 --- a/packages/ui/src/ui-component/table/FlowListTable.js +++ b/packages/ui/src/ui-component/table/FlowListTable.js @@ -11,6 +11,7 @@ import TableRow from '@mui/material/TableRow' import Paper from '@mui/material/Paper' import { Button, Stack, Typography } from '@mui/material' import FlowListMenu from '../button/FlowListMenu' +import Chip from '@mui/material/Chip' const StyledTableCell = styled(TableCell)(({ theme }) => ({ [`&.${tableCellClasses.head}`]: { @@ -47,13 +48,16 @@ export const FlowListTable = ({ data, images, filterFunction, updateFlowsApi }) Name - - Nodes (Showing first 5) + + Category - Last Modified Date + Nodes (Showing first 5) + Last Modified Date + + Actions @@ -68,8 +72,25 @@ export const FlowListTable = ({ data, images, filterFunction, updateFlowsApi }) - +
+   + {row.category && + row.category + .split(';') + .map((tag, index) => ( + + ))} +
+
+ {images[row.id] && (
)} - - {moment(row.updatedDate).format('dddd, MMMM Do, YYYY h:mm:ss A')} - + {moment(row.updatedDate).format('MMMM Do, YYYY')} + + {/*
)} {getNodeConfigApi.data && getNodeConfigApi.data.length > 0 && ( - + { + // eslint-disable-next-line + const { node, nodeId, ...rest } = obj + return rest + })} + columns={Object.keys(getNodeConfigApi.data[0]).slice(-3)} + /> )} diff --git a/packages/ui/src/ui-component/table/Table.js b/packages/ui/src/ui-component/table/Table.js index 2cf39182..1f0892bb 100644 --- a/packages/ui/src/ui-component/table/Table.js +++ b/packages/ui/src/ui-component/table/Table.js @@ -1,11 +1,11 @@ import PropTypes from 'prop-types' import { TableContainer, Table, TableHead, TableCell, TableRow, TableBody, Paper } from '@mui/material' -export const TableViewOnly = ({ columns, rows }) => { +export const TableViewOnly = ({ columns, rows, sx }) => { return ( <> -
+
{columns.map((col, index) => ( @@ -16,11 +16,9 @@ export const TableViewOnly = ({ columns, rows }) => { {rows.map((row, index) => ( - {Object.keys(row) - .slice(-3) - .map((key, index) => ( - {row[key]} - ))} + {Object.keys(row).map((key, index) => ( + {row[key]} + ))} ))} @@ -32,5 +30,6 @@ export const TableViewOnly = ({ columns, rows }) => { TableViewOnly.propTypes = { rows: PropTypes.array, - columns: PropTypes.array + columns: PropTypes.array, + sx: PropTypes.object } diff --git a/packages/ui/src/utils/genericHelper.js b/packages/ui/src/utils/genericHelper.js index 32331b14..e93663e1 100644 --- a/packages/ui/src/utils/genericHelper.js +++ b/packages/ui/src/utils/genericHelper.js @@ -332,6 +332,57 @@ export const getAvailableNodesForVariable = (nodes, edges, target, targetHandle) return parentNodes } +export const getUpsertDetails = (nodes, edges) => { + const vsNodes = nodes.filter( + (node) => + node.data.category === 'Vector Stores' && !node.data.label.includes('Upsert') && !node.data.label.includes('Load Existing') + ) + const vsNodeIds = vsNodes.map((vs) => vs.data.id) + + const upsertNodes = [] + const seenVsNodeIds = [] + for (const edge of edges) { + if (vsNodeIds.includes(edge.source) || vsNodeIds.includes(edge.target)) { + const vsNode = vsNodes.find((node) => node.data.id === edge.source || node.data.id === edge.target) + if (!vsNode || seenVsNodeIds.includes(vsNode.data.id)) continue + seenVsNodeIds.push(vsNode.data.id) + + // Found Vector Store Node, proceed to find connected Document Loader node + let connectedDocs = [] + + if (vsNode.data.inputs.document) connectedDocs = [...new Set(vsNode.data.inputs.document)] + + if (connectedDocs.length) { + const innerNodes = [vsNode] + + if (vsNode.data.inputs.embeddings) { + const embeddingsId = vsNode.data.inputs.embeddings.replace(/{{|}}/g, '').split('.')[0] + innerNodes.push(nodes.find((node) => node.data.id === embeddingsId)) + } + + for (const doc of connectedDocs) { + const docId = doc.replace(/{{|}}/g, '').split('.')[0] + const docNode = nodes.find((node) => node.data.id === docId) + if (docNode) innerNodes.push(docNode) + + // Found Document Loader Node, proceed to find connected Text Splitter node + if (docNode && docNode.data.inputs.textSplitter) { + const textSplitterId = docNode.data.inputs.textSplitter.replace(/{{|}}/g, '').split('.')[0] + const textSplitterNode = nodes.find((node) => node.data.id === textSplitterId) + if (textSplitterNode) innerNodes.push(textSplitterNode) + } + } + + upsertNodes.push({ + vectorNode: vsNode, + nodes: innerNodes.reverse() + }) + } + } + } + return upsertNodes +} + export const rearrangeToolsOrdering = (newValues, sourceNodeId) => { // RequestsGet and RequestsPost have to be in order before other tools newValues.push(`{{${sourceNodeId}.data.instance}}`) @@ -454,3 +505,106 @@ export const formatDataGridRows = (rows) => { return [] } } + +export const setLocalStorageChatflow = (chatflowid, chatId, chatHistory) => { + const chatDetails = localStorage.getItem(`${chatflowid}_INTERNAL`) + const obj = {} + if (chatId) obj.chatId = chatId + if (chatHistory) obj.chatHistory = chatHistory + + if (!chatDetails) { + localStorage.setItem(`${chatflowid}_INTERNAL`, JSON.stringify(obj)) + } else { + try { + const parsedChatDetails = JSON.parse(chatDetails) + localStorage.setItem(`${chatflowid}_INTERNAL`, JSON.stringify({ ...parsedChatDetails, ...obj })) + } catch (e) { + const chatId = chatDetails + obj.chatId = chatId + localStorage.setItem(`${chatflowid}_INTERNAL`, JSON.stringify(obj)) + } + } +} + +export const unshiftFiles = (configData) => { + const filesConfig = configData.find((config) => config.name === 'files') + if (filesConfig) { + configData = configData.filter((config) => config.name !== 'files') + configData.unshift(filesConfig) + } + return configData +} + +export const getConfigExamplesForJS = (configData, bodyType, isMultiple, stopNodeId) => { + let finalStr = '' + configData = unshiftFiles(configData) + const loop = Math.min(configData.length, 4) + for (let i = 0; i < loop; i += 1) { + const config = configData[i] + let exampleVal = `"example"` + if (config.type === 'string') exampleVal = `"example"` + else if (config.type === 'boolean') exampleVal = `true` + else if (config.type === 'number') exampleVal = `1` + else if (config.type === 'json') exampleVal = `{ "key": "val" }` + else if (config.name === 'files') exampleVal = `input.files[0]` + finalStr += bodyType === 'json' ? `\n "${config.name}": ${exampleVal},` : `formData.append("${config.name}", ${exampleVal})\n` + if (i === loop - 1 && bodyType !== 'json') + finalStr += !isMultiple + ? `` + : stopNodeId + ? `formData.append("stopNodeId", "${stopNodeId}")\n` + : `formData.append("question", "Hey, how are you?")\n` + } + return finalStr +} + +export const getConfigExamplesForPython = (configData, bodyType, isMultiple, stopNodeId) => { + let finalStr = '' + configData = unshiftFiles(configData) + const loop = Math.min(configData.length, 4) + for (let i = 0; i < loop; i += 1) { + const config = configData[i] + let exampleVal = `"example"` + if (config.type === 'string') exampleVal = `"example"` + else if (config.type === 'boolean') exampleVal = `true` + else if (config.type === 'number') exampleVal = `1` + else if (config.type === 'json') exampleVal = `{ "key": "val" }` + else if (config.name === 'files') continue + finalStr += bodyType === 'json' ? `\n "${config.name}": ${exampleVal},` : `\n "${config.name}": ${exampleVal},` + if (i === loop - 1 && bodyType !== 'json') + finalStr += !isMultiple + ? `\n` + : stopNodeId + ? `\n "stopNodeId": "${stopNodeId}"\n` + : `\n "question": "Hey, how are you?"\n` + } + return finalStr +} + +export const getConfigExamplesForCurl = (configData, bodyType, isMultiple, stopNodeId) => { + let finalStr = '' + configData = unshiftFiles(configData) + const loop = Math.min(configData.length, 4) + for (let i = 0; i < loop; i += 1) { + const config = configData[i] + let exampleVal = `example` + if (config.type === 'string') exampleVal = bodyType === 'json' ? `"example"` : `example` + else if (config.type === 'boolean') exampleVal = `true` + else if (config.type === 'number') exampleVal = `1` + else if (config.type === 'json') exampleVal = `{key:val}` + else if (config.name === 'files') + exampleVal = `@/home/user1/Desktop/example${config.type.includes(',') ? config.type.split(',')[0] : config.type}` + finalStr += bodyType === 'json' ? `"${config.name}": ${exampleVal}` : `\n -F "${config.name}=${exampleVal}"` + if (i === loop - 1) + finalStr += + bodyType === 'json' + ? ` }` + : !isMultiple + ? `` + : stopNodeId + ? ` \\\n -F "stopNodeId=${stopNodeId}"` + : ` \\\n -F "question=Hey, how are you?"` + else finalStr += bodyType === 'json' ? `, ` : ` \\` + } + return finalStr +} diff --git a/packages/ui/src/views/canvas/AddNodes.js b/packages/ui/src/views/canvas/AddNodes.js index c6134cb9..e0e639d1 100644 --- a/packages/ui/src/views/canvas/AddNodes.js +++ b/packages/ui/src/views/canvas/AddNodes.js @@ -21,7 +21,8 @@ import { Paper, Popper, Stack, - Typography + Typography, + Chip } from '@mui/material' import ExpandMoreIcon from '@mui/icons-material/ExpandMore' @@ -301,7 +302,37 @@ const AddNodes = ({ nodesData, node }) => { + {node.label} +   + {node.badge && ( + + )} + + } secondary={node.description} /> diff --git a/packages/ui/src/views/canvas/index.js b/packages/ui/src/views/canvas/index.js index c0206a9e..29602a4f 100644 --- a/packages/ui/src/views/canvas/index.js +++ b/packages/ui/src/views/canvas/index.js @@ -25,6 +25,7 @@ import CanvasHeader from './CanvasHeader' import AddNodes from './AddNodes' import ConfirmDialog from 'ui-component/dialog/ConfirmDialog' import { ChatPopUp } from 'views/chatmessage/ChatPopUp' +import { VectorStorePopUp } from 'views/vectorstore/VectorStorePopUp' import { flowContext } from 'store/context/ReactFlowContext' // API @@ -39,7 +40,7 @@ import useConfirm from 'hooks/useConfirm' import { IconX } from '@tabler/icons' // utils -import { getUniqueNodeId, initNode, getEdgeLabelName, rearrangeToolsOrdering } from 'utils/genericHelper' +import { getUniqueNodeId, initNode, getEdgeLabelName, rearrangeToolsOrdering, getUpsertDetails } from 'utils/genericHelper' import useNotifier from 'utils/useNotifier' // const @@ -81,6 +82,7 @@ const Canvas = () => { const [edges, setEdges, onEdgesChange] = useEdgesState() const [selectedNode, setSelectedNode] = useState(null) + const [isUpsertButtonEnabled, setIsUpsertButtonEnabled] = useState(false) const reactFlowWrapper = useRef(null) @@ -167,6 +169,7 @@ const Canvas = () => { if (isConfirmed) { try { await chatflowsApi.deleteChatflow(chatflow.id) + localStorage.removeItem(`${chatflow.id}_INTERNAL`) navigate(-1) } catch (error) { const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` @@ -339,6 +342,12 @@ const Canvas = () => { dispatch({ type: SET_DIRTY }) } + const checkIfUpsertAvailable = (nodes, edges) => { + const upsertNodeDetails = getUpsertDetails(nodes, edges) + if (upsertNodeDetails.length) setIsUpsertButtonEnabled(true) + else setIsUpsertButtonEnabled(false) + } + // ==============================|| useEffect ||============================== // // Get specific chatflow successful @@ -409,7 +418,13 @@ const Canvas = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [testChatflowApi.error]) - useEffect(() => setChatflow(canvasDataStore.chatflow), [canvasDataStore.chatflow]) + useEffect(() => { + setChatflow(canvasDataStore.chatflow) + if (canvasDataStore.chatflow) { + const flowData = canvasDataStore.chatflow.flowData ? JSON.parse(canvasDataStore.chatflow.flowData) : [] + checkIfUpsertAvailable(flowData.nodes || [], flowData.edges || []) + } + }, [canvasDataStore.chatflow]) // Initialization useEffect(() => { @@ -524,6 +539,7 @@ const Canvas = () => { /> + {isUpsertButtonEnabled && } diff --git a/packages/ui/src/views/chatflows/APICodeDialog.js b/packages/ui/src/views/chatflows/APICodeDialog.js index 49c718cc..34c2281f 100644 --- a/packages/ui/src/views/chatflows/APICodeDialog.js +++ b/packages/ui/src/views/chatflows/APICodeDialog.js @@ -23,6 +23,7 @@ import ExpandMoreIcon from '@mui/icons-material/ExpandMore' import { Dropdown } from 'ui-component/dropdown/Dropdown' import ShareChatbot from './ShareChatbot' import EmbedChat from './EmbedChat' +import Configuration from './Configuration' // Const import { baseURL } from 'store/constant' @@ -35,6 +36,7 @@ import cURLSVG from 'assets/images/cURL.svg' import EmbedSVG from 'assets/images/embed.svg' import ShareChatbotSVG from 'assets/images/sharing.png' import settingsSVG from 'assets/images/settings.svg' +import { IconBulb } from '@tabler/icons' // API import apiKeyApi from 'api/apikey' @@ -46,8 +48,8 @@ import useApi from 'hooks/useApi' import { CheckboxInput } from 'ui-component/checkbox/Checkbox' import { TableViewOnly } from 'ui-component/table/Table' -import { IconBulb } from '@tabler/icons' -import Configuration from './Configuration' +// Helpers +import { unshiftFiles, getConfigExamplesForJS, getConfigExamplesForPython, getConfigExamplesForCurl } from 'utils/genericHelper' function TabPanel(props) { const { children, value, index, ...other } = props @@ -77,67 +79,6 @@ function a11yProps(index) { } } -const unshiftFiles = (configData) => { - const filesConfig = configData.find((config) => config.name === 'files') - if (filesConfig) { - configData = configData.filter((config) => config.name !== 'files') - configData.unshift(filesConfig) - } - return configData -} - -const getConfigExamplesForJS = (configData, bodyType) => { - let finalStr = '' - configData = unshiftFiles(configData) - const loop = Math.min(configData.length, 4) - for (let i = 0; i < loop; i += 1) { - const config = configData[i] - let exampleVal = `"example"` - if (config.type === 'string') exampleVal = `"example"` - else if (config.type === 'boolean') exampleVal = `true` - else if (config.type === 'number') exampleVal = `1` - else if (config.name === 'files') exampleVal = `input.files[0]` - finalStr += bodyType === 'json' ? `\n "${config.name}": ${exampleVal},` : `formData.append("${config.name}", ${exampleVal})\n` - if (i === loop - 1 && bodyType !== 'json') finalStr += `formData.append("question", "Hey, how are you?")\n` - } - return finalStr -} - -const getConfigExamplesForPython = (configData, bodyType) => { - let finalStr = '' - configData = unshiftFiles(configData) - const loop = Math.min(configData.length, 4) - for (let i = 0; i < loop; i += 1) { - const config = configData[i] - let exampleVal = `"example"` - if (config.type === 'string') exampleVal = `"example"` - else if (config.type === 'boolean') exampleVal = `true` - else if (config.type === 'number') exampleVal = `1` - else if (config.name === 'files') continue - finalStr += bodyType === 'json' ? `\n "${config.name}": ${exampleVal},` : `\n "${config.name}": ${exampleVal},` - if (i === loop - 1 && bodyType !== 'json') finalStr += `\n "question": "Hey, how are you?"\n` - } - return finalStr -} - -const getConfigExamplesForCurl = (configData, bodyType) => { - let finalStr = '' - configData = unshiftFiles(configData) - const loop = Math.min(configData.length, 4) - for (let i = 0; i < loop; i += 1) { - const config = configData[i] - let exampleVal = `example` - if (config.type === 'string') exampleVal = bodyType === 'json' ? `"example"` : `example` - else if (config.type === 'boolean') exampleVal = `true` - else if (config.type === 'number') exampleVal = `1` - else if (config.name === 'files') exampleVal = `@/home/user1/Desktop/example${config.type}` - finalStr += bodyType === 'json' ? `"${config.name}": ${exampleVal}` : `\n -F "${config.name}=${exampleVal}"` - if (i === loop - 1) finalStr += bodyType === 'json' ? ` }` : ` \\\n -F "question=Hey, how are you?"` - else finalStr += bodyType === 'json' ? `, ` : ` \\` - } - return finalStr -} - const APICodeDialog = ({ show, dialogProps, onCancel }) => { const portalElement = document.getElementById('portal') const navigate = useNavigate() @@ -334,7 +275,8 @@ query({"question": "Hey, how are you?"}).then((response) => { const getConfigCodeWithFormData = (codeLang, configData) => { if (codeLang === 'Python') { configData = unshiftFiles(configData) - const fileType = configData[0].type + let fileType = configData[0].type + if (fileType.includes(',')) fileType = fileType.split(',')[0] return `import requests API_URL = "${baseURL}/api/v1/prediction/${dialogProps.chatflowid}" @@ -384,7 +326,8 @@ query(formData).then((response) => { const getConfigCodeWithFormDataWithAuth = (codeLang, configData) => { if (codeLang === 'Python') { configData = unshiftFiles(configData) - const fileType = configData[0].type + let fileType = configData[0].type + if (fileType.includes(',')) fileType = fileType.split(',')[0] return `import requests API_URL = "${baseURL}/api/v1/prediction/${dialogProps.chatflowid}" @@ -700,7 +643,11 @@ formData.append("openAIApiKey[openAIEmbeddings_0]", "sk-my-openai-2nd-key")` { + // eslint-disable-next-line + const { node, nodeId, ...rest } = obj + return rest + })} columns={Object.keys(nodeConfig[nodeLabel][0]).slice(-3)} /> diff --git a/packages/ui/src/views/chatmessage/ChatMessage.js b/packages/ui/src/views/chatmessage/ChatMessage.js index 0cf5695b..61e37077 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.js +++ b/packages/ui/src/views/chatmessage/ChatMessage.js @@ -31,7 +31,7 @@ import { baseURL, maxScroll } from 'store/constant' import robotPNG from 'assets/images/robot.png' import userPNG from 'assets/images/account.png' -import { isValidURL, removeDuplicateURL } from 'utils/genericHelper' +import { isValidURL, removeDuplicateURL, setLocalStorageChatflow } from 'utils/genericHelper' export const ChatMessage = ({ open, chatflowid, isDialog }) => { const theme = useTheme() @@ -127,10 +127,9 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { if (response.data) { const data = response.data - if (!chatId) { - setChatId(data.chatId) - localStorage.setItem(`${chatflowid}_INTERNAL`, data.chatId) - } + + if (!chatId) setChatId(data.chatId) + if (!isChatFlowAvailableToStream) { let text = '' if (data.text) text = data.text @@ -142,7 +141,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { { message: text, sourceDocuments: data?.sourceDocuments, usedTools: data?.usedTools, type: 'apiMessage' } ]) } - + setLocalStorageChatflow(chatflowid, data.chatId, messages) setLoading(false) setUserInput('') setTimeout(() => { @@ -175,7 +174,6 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { if (getChatmessageApi.data?.length) { const chatId = getChatmessageApi.data[0]?.chatId setChatId(chatId) - localStorage.setItem(`${chatflowid}_INTERNAL`, chatId) const loadedMessages = getChatmessageApi.data.map((message) => { const obj = { message: message.content, @@ -186,6 +184,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { return obj }) setMessages((prevMessages) => [...prevMessages, ...loadedMessages]) + setLocalStorageChatflow(chatflowid, chatId, messages) } // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/packages/ui/src/views/chatmessage/ChatPopUp.js b/packages/ui/src/views/chatmessage/ChatPopUp.js index 1b87ac30..670fb00f 100644 --- a/packages/ui/src/views/chatmessage/ChatPopUp.js +++ b/packages/ui/src/views/chatmessage/ChatPopUp.js @@ -85,8 +85,10 @@ export const ChatPopUp = ({ chatflowid }) => { if (isConfirmed) { try { - const chatId = localStorage.getItem(`${chatflowid}_INTERNAL`) - await chatmessageApi.deleteChatmessage(chatflowid, { chatId, chatType: 'INTERNAL' }) + const chatDetails = localStorage.getItem(`${chatflowid}_INTERNAL`) + if (!chatDetails) return + const objChatDetails = JSON.parse(chatDetails) + await chatmessageApi.deleteChatmessage(chatflowid, { chatId: objChatDetails.chatId, chatType: 'INTERNAL' }) localStorage.removeItem(`${chatflowid}_INTERNAL`) resetChatDialog() enqueueSnackbar({ diff --git a/packages/ui/src/views/vectorstore/VectorStoreDialog.js b/packages/ui/src/views/vectorstore/VectorStoreDialog.js new file mode 100644 index 00000000..29f443f4 --- /dev/null +++ b/packages/ui/src/views/vectorstore/VectorStoreDialog.js @@ -0,0 +1,556 @@ +import { createPortal } from 'react-dom' +import PropTypes from 'prop-types' +import { useDispatch } from 'react-redux' +import { useContext, useState, useEffect } from 'react' +import PerfectScrollbar from 'react-perfect-scrollbar' +import { CopyBlock, atomOneDark } from 'react-code-blocks' + +import { + Dialog, + DialogContent, + DialogTitle, + Button, + Box, + Tabs, + Tab, + Accordion, + AccordionSummary, + AccordionDetails, + Typography +} from '@mui/material' + +import { CheckboxInput } from 'ui-component/checkbox/Checkbox' +import { BackdropLoader } from 'ui-component/loading/BackdropLoader' +import { TableViewOnly } from 'ui-component/table/Table' + +import { IconX } from '@tabler/icons' +import ExpandMoreIcon from '@mui/icons-material/ExpandMore' +import pythonSVG from 'assets/images/python.svg' +import javascriptSVG from 'assets/images/javascript.svg' +import cURLSVG from 'assets/images/cURL.svg' + +import useApi from 'hooks/useApi' +import configApi from 'api/config' +import vectorstoreApi from 'api/vectorstore' + +// Utils +import { + getUpsertDetails, + getFileName, + unshiftFiles, + getConfigExamplesForJS, + getConfigExamplesForPython, + getConfigExamplesForCurl +} from 'utils/genericHelper' +import useNotifier from 'utils/useNotifier' + +// Store +import { flowContext } from 'store/context/ReactFlowContext' +import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions' +import { baseURL } from 'store/constant' +import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from 'store/actions' + +function TabPanel(props) { + const { children, value, index, ...other } = props + return ( + + ) +} + +TabPanel.propTypes = { + children: PropTypes.node, + index: PropTypes.number.isRequired, + value: PropTypes.number.isRequired +} + +function a11yProps(index) { + return { + id: `attachment-tab-${index}`, + 'aria-controls': `attachment-tabpanel-${index}` + } +} + +const VectorStoreDialog = ({ show, dialogProps, onCancel }) => { + const portalElement = document.getElementById('portal') + const { reactFlowInstance } = useContext(flowContext) + const dispatch = useDispatch() + + useNotifier() + const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)) + const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) + const getConfigApi = useApi(configApi.getConfig) + + const [nodes, setNodes] = useState([]) + const [loading, setLoading] = useState(false) + const [isFormDataRequired, setIsFormDataRequired] = useState({}) + const [nodeConfigExpanded, setNodeConfigExpanded] = useState({}) + const [nodeCheckboxExpanded, setCheckboxExpanded] = useState({}) + const [tabValue, setTabValue] = useState(0) + const [expandedVectorNodeId, setExpandedVectorNodeId] = useState('') + const [configData, setConfigData] = useState({}) + + const reformatConfigData = (configData, nodes) => { + return configData.filter((item1) => nodes.some((item2) => item1.nodeId === item2.id)) + } + + const getCode = (codeLang, vectorNodeId, isMultiple, configData) => { + if (codeLang === 'Python') { + return `import requests + +API_URL = "${baseURL}/api/v1/vector/upsert/${dialogProps.chatflowid}" + +def query(payload): + response = requests.post(API_URL, json=payload) + return response.json() + +output = query({ + ${isMultiple ? `"stopNodeId": "${vectorNodeId}",\n ` : ``}"overrideConfig": {${getConfigExamplesForPython( + configData, + 'json', + isMultiple, + vectorNodeId + )} + } +}) +` + } else if (codeLang === 'JavaScript') { + return `async function query(data) { + const response = await fetch( + "${baseURL}/api/v1/vector/upsert/${dialogProps.chatflowid}", + { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(data) + } + ); + const result = await response.json(); + return result; +} + +query({ + ${isMultiple ? `"stopNodeId": "${vectorNodeId}",\n ` : ``}"overrideConfig": {${getConfigExamplesForJS( + configData, + 'json', + isMultiple, + vectorNodeId + )} + } +}).then((response) => { + console.log(response); +}); +` + } else if (codeLang === 'cURL') { + return `curl ${baseURL}/api/v1/vector/upsert/${dialogProps.chatflowid} \\ + -X POST \\ + ${ + isMultiple + ? `-d '{"stopNodeId": "${vectorNodeId}", "overrideConfig": {${getConfigExamplesForCurl( + configData, + 'json', + isMultiple, + vectorNodeId + )}}' \\` + : `-d '{"overrideConfig": {${getConfigExamplesForCurl(configData, 'json', isMultiple, vectorNodeId)}}' \\` + } + -H "Content-Type: application/json"` + } + return '' + } + + const getCodeWithFormData = (codeLang, vectorNodeId, isMultiple, configData) => { + if (codeLang === 'Python') { + configData = unshiftFiles(configData) + let fileType = configData[0].type + if (fileType.includes(',')) fileType = fileType.split(',')[0] + return `import requests + +API_URL = "${baseURL}/api/v1/vector/upsert/${dialogProps.chatflowid}" + +# use form data to upload files +form_data = { + "files": ${`('example${fileType}', open('example${fileType}', 'rb'))`} +} +body_data = {${getConfigExamplesForPython(configData, 'formData', isMultiple, vectorNodeId)}} + +def query(form_data, body_data): + response = requests.post(API_URL, files=form_data, data=body_data) + return response.json() + +output = query(form_data, body_data) +` + } else if (codeLang === 'JavaScript') { + return `// use FormData to upload files +let formData = new FormData(); +${getConfigExamplesForJS(configData, 'formData', isMultiple, vectorNodeId)} +async function query(formData) { + const response = await fetch( + "${baseURL}/api/v1/vector/upsert/${dialogProps.chatflowid}", + { + method: "POST", + body: formData + } + ); + const result = await response.json(); + return result; +} + +query(formData).then((response) => { + console.log(response); +}); +` + } else if (codeLang === 'cURL') { + return `curl ${baseURL}/api/v1/vector/upsert/${dialogProps.chatflowid} \\ + -X POST \\${getConfigExamplesForCurl(configData, 'formData', isMultiple, vectorNodeId)} \\ + -H "Content-Type: multipart/form-data"` + } + return '' + } + + const getLang = (codeLang) => { + if (codeLang === 'Python') { + return 'python' + } else if (codeLang === 'JavaScript') { + return 'javascript' + } else if (codeLang === 'cURL') { + return 'bash' + } + return 'python' + } + + const getSVG = (codeLang) => { + if (codeLang === 'Python') { + return pythonSVG + } else if (codeLang === 'JavaScript') { + return javascriptSVG + } else if (codeLang === 'Embed') { + return EmbedSVG + } else if (codeLang === 'cURL') { + return cURLSVG + } else if (codeLang === 'Share Chatbot') { + return ShareChatbotSVG + } else if (codeLang === 'Configuration') { + return settingsSVG + } + return pythonSVG + } + + const handleAccordionChange = (nodeLabel) => (event, isExpanded) => { + const accordianNodes = { ...nodeConfigExpanded } + accordianNodes[nodeLabel] = isExpanded + setNodeConfigExpanded(accordianNodes) + } + + const onCheckBoxChanged = (vectorNodeId) => { + const checkboxNodes = { ...nodeCheckboxExpanded } + if (Object.keys(checkboxNodes).includes(vectorNodeId)) checkboxNodes[vectorNodeId] = !checkboxNodes[vectorNodeId] + else checkboxNodes[vectorNodeId] = true + + if (checkboxNodes[vectorNodeId] === true) getConfigApi.request(dialogProps.chatflowid) + setCheckboxExpanded(checkboxNodes) + setExpandedVectorNodeId(vectorNodeId) + + const newIsFormDataRequired = { ...isFormDataRequired } + newIsFormDataRequired[vectorNodeId] = false + setIsFormDataRequired(newIsFormDataRequired) + const newNodes = nodes.find((node) => node.vectorNode.data.id === vectorNodeId)?.nodes ?? [] + + for (const node of newNodes) { + if (node.data.inputParams.find((param) => param.type === 'file')) { + newIsFormDataRequired[vectorNodeId] = true + setIsFormDataRequired(newIsFormDataRequired) + break + } + } + } + + const onUpsertClicked = async (vectorStoreNode) => { + setLoading(true) + try { + await vectorstoreApi.upsertVectorStore(dialogProps.chatflowid, { stopNodeId: vectorStoreNode.data.id }) + enqueueSnackbar({ + message: 'Succesfully upserted vector store. You can start chatting now!', + options: { + key: new Date().getTime() + Math.random(), + variant: 'success', + action: (key) => ( + + ) + } + }) + setLoading(false) + } catch (error) { + const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` + enqueueSnackbar({ + message: errorData, + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + persist: true, + action: (key) => ( + + ) + } + }) + setLoading(false) + } + } + + const getNodeDetail = (node) => { + const nodeDetails = [] + const inputKeys = Object.keys(node.data.inputs) + for (let i = 0; i < node.data.inputParams.length; i += 1) { + if (inputKeys.includes(node.data.inputParams[i].name)) { + nodeDetails.push({ + label: node.data.inputParams[i].label, + name: node.data.inputParams[i].name, + type: node.data.inputParams[i].type, + value: + node.data.inputParams[i].type === 'file' + ? getFileName(node.data.inputs[node.data.inputParams[i].name]) + : node.data.inputs[node.data.inputParams[i].name] ?? '' + }) + } + } + return nodeDetails + } + + useEffect(() => { + if (getConfigApi.data) { + const newConfigData = { ...configData } + newConfigData[expandedVectorNodeId] = reformatConfigData( + getConfigApi.data, + nodes.find((node) => node.vectorNode.data.id === expandedVectorNodeId)?.nodes ?? [] + ) + setConfigData(newConfigData) + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [getConfigApi.data]) + + useEffect(() => { + if (dialogProps && reactFlowInstance) { + const nodes = reactFlowInstance.getNodes() + const edges = reactFlowInstance.getEdges() + setNodes(getUpsertDetails(nodes, edges)) + } + + return () => { + setNodes([]) + setLoading(false) + setIsFormDataRequired({}) + setNodeConfigExpanded({}) + setCheckboxExpanded({}) + setTabValue(0) + setConfigData({}) + } + + // 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} + + + + {nodes.length > 0 && + nodes.map((data, index) => { + return ( +
+ {data.nodes.length > 0 && + data.nodes.map((node, index) => { + return ( + + } + aria-controls={`nodes-accordian-${node.data.name}`} + id={`nodes-accordian-header-${node.data.name}`} + > +
+
+ {node.data.name} +
+ {node.data.label} +
+ + {node.data.id} + +
+
+
+ + + +
+ ) + })} + + onCheckBoxChanged(data.vectorNode.data.id)} + /> + {nodeCheckboxExpanded[data.vectorNode.data.id] && ( +
+ setTabValue(val)} aria-label='tabs'> + {['Python', 'JavaScript', 'cURL'].map((codeLang, index) => ( + + } + iconPosition='start' + key={index} + label={codeLang} + {...a11yProps(index)} + > + ))} + +
+ )} + {nodeCheckboxExpanded[data.vectorNode.data.id] && + isFormDataRequired[data.vectorNode.data.id] !== undefined && + configData[data.vectorNode.data.id] && + configData[data.vectorNode.data.id].length > 0 && ( + <> +
+ {['Python', 'JavaScript', 'cURL'].map((codeLang, index) => ( + + 1 ? true : false, + configData[data.vectorNode.data.id] + ) + : getCode( + codeLang, + data.vectorNode.data.id, + nodes.length > 1 ? true : false, + configData[data.vectorNode.data.id] + ) + } + language={getLang(codeLang)} + showLineNumbers={false} + wrapLines + /> + + ))} +
+ + )} +
+
+ {loading && } + {!loading && ( + + )} +
+
+ ) + })} +
+
+
+ ) : null + + return createPortal(component, portalElement) +} + +VectorStoreDialog.propTypes = { + show: PropTypes.bool, + dialogProps: PropTypes.object, + onCancel: PropTypes.func +} + +export default VectorStoreDialog diff --git a/packages/ui/src/views/vectorstore/VectorStorePopUp.js b/packages/ui/src/views/vectorstore/VectorStorePopUp.js new file mode 100644 index 00000000..2e23e69c --- /dev/null +++ b/packages/ui/src/views/vectorstore/VectorStorePopUp.js @@ -0,0 +1,114 @@ +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' + +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 anchorRef = useRef(null) + const prevOpen = useRef(open) + + const handleToggle = () => { + setOpen((prevopen) => !prevopen) + const props = { + open: true, + title: 'Upsert Vector Store', + chatflowid + } + setExpandDialogProps(props) + 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) { + const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` + enqueueSnackbar({ + message: errorData, + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + persist: true, + action: (key) => ( + + ) + } + }) + } + } + + useEffect(() => { + if (prevOpen.current === true && open === false) { + anchorRef.current.focus() + } + prevOpen.current = open + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [open, chatflowid]) + + return ( + <> + + {open ? : } + + { + setShowExpandDialog(false) + setOpen((prevopen) => !prevopen) + }} + > + + ) +} + +VectorStorePopUp.propTypes = { chatflowid: PropTypes.string } From 0cc68e5625529387ec879465433f297cad170fdc Mon Sep 17 00:00:00 2001 From: Henry Date: Thu, 16 Nov 2023 23:23:41 +0000 Subject: [PATCH 24/64] add dark mode changes --- .../src/ui-component/button/FlowListMenu.js | 1 - .../src/ui-component/table/FlowListTable.js | 5 +--- packages/ui/src/views/chatflows/index.js | 27 +++++++++++++------ 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/packages/ui/src/ui-component/button/FlowListMenu.js b/packages/ui/src/ui-component/button/FlowListMenu.js index f4ffbd32..44192298 100644 --- a/packages/ui/src/ui-component/button/FlowListMenu.js +++ b/packages/ui/src/ui-component/button/FlowListMenu.js @@ -43,7 +43,6 @@ const StyledMenu = styled((props) => ( borderRadius: 6, marginTop: theme.spacing(1), minWidth: 180, - color: theme.palette.mode === 'light' ? 'rgb(55, 65, 81)' : theme.palette.grey[300], boxShadow: 'rgb(255, 255, 255) 0px 0px 0px 0px, rgba(0, 0, 0, 0.05) 0px 0px 0px 1px, rgba(0, 0, 0, 0.1) 0px 10px 15px -3px, rgba(0, 0, 0, 0.05) 0px 4px 6px -2px', '& .MuiMenu-list': { diff --git a/packages/ui/src/ui-component/table/FlowListTable.js b/packages/ui/src/ui-component/table/FlowListTable.js index 08caed57..68641d44 100644 --- a/packages/ui/src/ui-component/table/FlowListTable.js +++ b/packages/ui/src/ui-component/table/FlowListTable.js @@ -52,7 +52,7 @@ export const FlowListTable = ({ data, images, filterFunction, updateFlowsApi }) Category - Nodes (Showing first 5) + Nodes Last Modified Date @@ -134,9 +134,6 @@ export const FlowListTable = ({ data, images, filterFunction, updateFlowsApi }) - {/**/} diff --git a/packages/ui/src/views/chatflows/index.js b/packages/ui/src/views/chatflows/index.js index 44c670d6..34c6523b 100644 --- a/packages/ui/src/views/chatflows/index.js +++ b/packages/ui/src/views/chatflows/index.js @@ -23,7 +23,7 @@ import useApi from 'hooks/useApi' import { baseURL } from 'store/constant' // icons -import { IconPlus, IconSearch, IconLayoutCards, IconLayoutColumns } from '@tabler/icons' +import { IconPlus, IconSearch, IconLayoutGrid, IconList } from '@tabler/icons' import * as React from 'react' import ToggleButtonGroup from '@mui/material/ToggleButtonGroup' import { FlowListTable } from '../../ui-component/table/FlowListTable' @@ -138,7 +138,7 @@ const Chatflows = () => {

Chatflows

{ }} /> - + - - - + + + - - + + From 97247713ef7c2f0134c3114c1de84fea040a47f5 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Fri, 17 Nov 2023 11:24:55 +0530 Subject: [PATCH 25/64] UX Changes: Fixed 2 edge cases, (1) UI-tag not added, when the user clicks the submit without hitting enter and (2) server side error when the update/rename is attempted before any flow is opened --- packages/server/src/index.ts | 8 ++++++-- packages/ui/src/ui-component/dialog/TagDialog.js | 8 +++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index ba6c3ce0..5a5b2eda 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -355,8 +355,12 @@ export class App { this.AppDataSource.getRepository(ChatFlow).merge(chatflow, updateChatFlow) const result = await this.AppDataSource.getRepository(ChatFlow).save(chatflow) - // Update chatflowpool inSync to false, to build Langchain again because data has been changed - this.chatflowPool.updateInSync(chatflow.id, false) + // chatFlowPool is initialized only when a flow is opened + // if the user attempts to rename/update category without opening any flow, chatFlowPool will be undefined + if (this.chatflowPool) { + // Update chatflowpool inSync to false, to build Langchain again because data has been changed + this.chatflowPool.updateInSync(chatflow.id, false) + } return res.json(result) }) diff --git a/packages/ui/src/ui-component/dialog/TagDialog.js b/packages/ui/src/ui-component/dialog/TagDialog.js index be133fa4..778bf2cc 100644 --- a/packages/ui/src/ui-component/dialog/TagDialog.js +++ b/packages/ui/src/ui-component/dialog/TagDialog.js @@ -5,7 +5,7 @@ import Button from '@mui/material/Button' import TextField from '@mui/material/TextField' import Chip from '@mui/material/Chip' import PropTypes from 'prop-types' -import { DialogActions, DialogContent, DialogTitle } from '@mui/material' +import { DialogActions, DialogContent, DialogTitle, Typography } from '@mui/material' const TagDialog = ({ isOpen, onClose, tags, setTags, onSubmit }) => { const [inputValue, setInputValue] = useState('') @@ -30,6 +30,9 @@ const TagDialog = ({ isOpen, onClose, tags, setTags, onSubmit }) => { const handleSubmit = (event) => { event.preventDefault() + if (inputValue.trim() && !tags.includes(inputValue)) { + setTags([...tags, inputValue]) + } onSubmit(tags) onClose() } @@ -67,6 +70,9 @@ const TagDialog = ({ isOpen, onClose, tags, setTags, onSubmit }) => { label='Add a tag' variant='outlined' /> + + Enter a tag and press enter to add it to the list. You can add as many tags as you want. + From a0397c008e035ec64e8063b4b2c7859a5d2eadc8 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Fri, 17 Nov 2023 11:25:59 +0530 Subject: [PATCH 26/64] UX Changes: Column display fixes for 'xs' mode --- packages/ui/src/ui-component/table/FlowListTable.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/ui-component/table/FlowListTable.js b/packages/ui/src/ui-component/table/FlowListTable.js index 68641d44..e33a8ba1 100644 --- a/packages/ui/src/ui-component/table/FlowListTable.js +++ b/packages/ui/src/ui-component/table/FlowListTable.js @@ -48,7 +48,7 @@ export const FlowListTable = ({ data, images, filterFunction, updateFlowsApi }) Name - + Category @@ -72,7 +72,7 @@ export const FlowListTable = ({ data, images, filterFunction, updateFlowsApi }) - +
Date: Fri, 17 Nov 2023 12:29:14 +0530 Subject: [PATCH 27/64] UX Changes: Addition of search filters for API Keys and Credentials. --- packages/ui/src/views/apikey/index.js | 71 ++++++++++++++--- packages/ui/src/views/credentials/index.js | 88 ++++++++++++++++++---- 2 files changed, 136 insertions(+), 23 deletions(-) diff --git a/packages/ui/src/views/apikey/index.js b/packages/ui/src/views/apikey/index.js index a2b2e639..e08baac2 100644 --- a/packages/ui/src/views/apikey/index.js +++ b/packages/ui/src/views/apikey/index.js @@ -16,7 +16,11 @@ import { Paper, IconButton, Popover, - Typography + Typography, + Toolbar, + TextField, + InputAdornment, + ButtonGroup } from '@mui/material' import { useTheme } from '@mui/material/styles' @@ -37,7 +41,7 @@ import useConfirm from 'hooks/useConfirm' import useNotifier from 'utils/useNotifier' // Icons -import { IconTrash, IconEdit, IconCopy, IconX, IconPlus, IconEye, IconEyeOff } from '@tabler/icons' +import { IconTrash, IconEdit, IconCopy, IconX, IconPlus, IconEye, IconEyeOff, IconSearch } from '@tabler/icons' import APIEmptySVG from 'assets/images/api_empty.svg' // ==============================|| APIKey ||============================== // @@ -59,6 +63,14 @@ const APIKey = () => { const [showApiKeys, setShowApiKeys] = useState([]) const openPopOver = Boolean(anchorEl) + const [search, setSearch] = useState('') + const onSearchChange = (event) => { + setSearch(event.target.value) + } + function filterKeys(data) { + return data.keyName.toLowerCase().indexOf(search.toLowerCase()) > -1 + } + const { confirm } = useConfirm() const getAllAPIKeysApi = useApi(apiKeyApi.getAllAPIKeys) @@ -171,12 +183,53 @@ const APIKey = () => { <> -

API Keys 

- - - }> - Create Key - + + +

API Keys 

+ + + + ) + }} + /> + + + + } + > + Create Key + + + +
+
{apiKeys.length <= 0 && ( @@ -199,7 +252,7 @@ const APIKey = () => { - {apiKeys.map((key, index) => ( + {apiKeys.filter(filterKeys).map((key, index) => ( {key.keyName} diff --git a/packages/ui/src/views/credentials/index.js b/packages/ui/src/views/credentials/index.js index 9db990a7..31e35831 100644 --- a/packages/ui/src/views/credentials/index.js +++ b/packages/ui/src/views/credentials/index.js @@ -4,7 +4,23 @@ import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackba import moment from 'moment' // material-ui -import { Button, Box, Stack, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, IconButton } from '@mui/material' +import { + Button, + Box, + Stack, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + IconButton, + Toolbar, + TextField, + InputAdornment, + ButtonGroup +} from '@mui/material' import { useTheme } from '@mui/material/styles' // project imports @@ -25,7 +41,7 @@ import useConfirm from 'hooks/useConfirm' import useNotifier from 'utils/useNotifier' // Icons -import { IconTrash, IconEdit, IconX, IconPlus } from '@tabler/icons' +import { IconTrash, IconEdit, IconX, IconPlus, IconSearch } from '@tabler/icons' import CredentialEmptySVG from 'assets/images/credential_empty.svg' // const @@ -56,6 +72,14 @@ const Credentials = () => { const getAllCredentialsApi = useApi(credentialsApi.getAllCredentials) const getAllComponentsCredentialsApi = useApi(credentialsApi.getAllComponentsCredentials) + const [search, setSearch] = useState('') + const onSearchChange = (event) => { + setSearch(event.target.value) + } + function filterCredentials(data) { + return data.credentialName.toLowerCase().indexOf(search.toLowerCase()) > -1 + } + const listCredential = () => { const dialogProp = { title: 'Add New Credential', @@ -168,17 +192,53 @@ const Credentials = () => { <> -

Credentials 

- - - } - > - Add Credential - + + +

Credentials 

+ + + + ) + }} + /> + + + + } + > + Add Credential + + + +
+
{credentials.length <= 0 && ( @@ -205,7 +265,7 @@ const Credentials = () => {
- {credentials.map((credential, index) => ( + {credentials.filter(filterCredentials).map((credential, index) => (
Date: Fri, 17 Nov 2023 12:35:01 +0000 Subject: [PATCH 28/64] add fix where tags are not added when submit is clicked without enter --- .../src/ui-component/button/FlowListMenu.js | 57 ++++++++++------- .../ui/src/ui-component/dialog/TagDialog.js | 63 +++++++++++-------- .../src/ui-component/table/FlowListTable.js | 2 +- 3 files changed, 75 insertions(+), 47 deletions(-) diff --git a/packages/ui/src/ui-component/button/FlowListMenu.js b/packages/ui/src/ui-component/button/FlowListMenu.js index 44192298..b242d2cb 100644 --- a/packages/ui/src/ui-component/button/FlowListMenu.js +++ b/packages/ui/src/ui-component/button/FlowListMenu.js @@ -1,4 +1,7 @@ -import * as React from 'react' +import { useState } from 'react' +import { useDispatch } from 'react-redux' +import PropTypes from 'prop-types' + import { styled, alpha } from '@mui/material/styles' import Menu from '@mui/material/Menu' import MenuItem from '@mui/material/MenuItem' @@ -10,21 +13,22 @@ import FileDeleteIcon from '@mui/icons-material/Delete' import FileCategoryIcon from '@mui/icons-material/Category' import Button from '@mui/material/Button' import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown' -import PropTypes from 'prop-types' -import { uiBaseURL } from '../../store/constant' -import { generateExportFlowData } from '../../utils/genericHelper' -import chatflowsApi from 'api/chatflows' -import useConfirm from 'hooks/useConfirm' -import useNotifier from '../../utils/useNotifier' -import { closeSnackbar as closeSnackbarAction, enqueueSnackbar as enqueueSnackbarAction } from '../../store/actions' import { IconX } from '@tabler/icons' -import { useDispatch } from 'react-redux' + +import chatflowsApi from 'api/chatflows' + +import useApi from '../../hooks/useApi' +import useConfirm from 'hooks/useConfirm' +import { uiBaseURL } from '../../store/constant' +import { closeSnackbar as closeSnackbarAction, enqueueSnackbar as enqueueSnackbarAction } from '../../store/actions' + import ConfirmDialog from '../dialog/ConfirmDialog' import SaveChatflowDialog from '../dialog/SaveChatflowDialog' -import { useState } from 'react' -import useApi from '../../hooks/useApi' import TagDialog from '../dialog/TagDialog' +import { generateExportFlowData } from '../../utils/genericHelper' +import useNotifier from '../../utils/useNotifier' + const StyledMenu = styled((props) => ( ( export default function FlowListMenu({ chatflow, updateFlowsApi }) { const { confirm } = useConfirm() const dispatch = useDispatch() - const [flowDialogOpen, setFlowDialogOpen] = useState(false) - const [categoryValues, setCategoryValues] = useState([]) - - const [categoryDialogOpen, setCategoryDialogOpen] = useState(false) const updateChatflowApi = useApi(chatflowsApi.updateChatflow) - // ==============================|| Snackbar ||============================== // useNotifier() const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)) const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) - const [anchorEl, setAnchorEl] = React.useState(null) + + const [flowDialogOpen, setFlowDialogOpen] = useState(false) + const [categoryDialogOpen, setCategoryDialogOpen] = useState(false) + const [categoryDialogProps, setCategoryDialogProps] = useState({}) + const [anchorEl, setAnchorEl] = useState(null) const open = Boolean(anchorEl) + const handleClick = (event) => { setAnchorEl(event.currentTarget) } + const handleClose = () => { setAnchorEl(null) } + const handleFlowRename = () => { setAnchorEl(null) setFlowDialogOpen(true) } + const saveFlowRename = async (chatflowName) => { const updateBody = { name: chatflowName, @@ -111,13 +118,19 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) { }) } } + const handleFlowCategory = () => { setAnchorEl(null) - if (chatflow.category) setCategoryValues(chatflow.category.split(';')) - else setCategoryValues([]) + if (chatflow.category) { + setCategoryDialogProps({ + category: chatflow.category.split(';') + }) + } setCategoryDialogOpen(true) } + const saveFlowCategory = async (categories) => { + setCategoryDialogOpen(false) // save categories as string const categoryTags = categories.join(';') const updateBody = { @@ -144,6 +157,7 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) { }) } } + const handleDelete = async () => { setAnchorEl(null) const confirmPayload = { @@ -176,6 +190,7 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) { } } } + const handleDuplicate = () => { setAnchorEl(null) try { @@ -185,6 +200,7 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) { console.error(e) } } + const handleExport = () => { setAnchorEl(null) try { @@ -261,9 +277,8 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) { /> setCategoryDialogOpen(false)} - tags={categoryValues} - setTags={setCategoryValues} onSubmit={saveFlowCategory} />
diff --git a/packages/ui/src/ui-component/dialog/TagDialog.js b/packages/ui/src/ui-component/dialog/TagDialog.js index 778bf2cc..82c35dde 100644 --- a/packages/ui/src/ui-component/dialog/TagDialog.js +++ b/packages/ui/src/ui-component/dialog/TagDialog.js @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useState, useEffect } from 'react' import Dialog from '@mui/material/Dialog' import Box from '@mui/material/Box' import Button from '@mui/material/Button' @@ -7,8 +7,9 @@ import Chip from '@mui/material/Chip' import PropTypes from 'prop-types' import { DialogActions, DialogContent, DialogTitle, Typography } from '@mui/material' -const TagDialog = ({ isOpen, onClose, tags, setTags, onSubmit }) => { +const TagDialog = ({ isOpen, dialogProps, onClose, onSubmit }) => { const [inputValue, setInputValue] = useState('') + const [categoryValues, setCategoryValues] = useState([]) const handleInputChange = (event) => { setInputValue(event.target.value) @@ -17,34 +18,44 @@ const TagDialog = ({ isOpen, onClose, tags, setTags, onSubmit }) => { const handleInputKeyDown = (event) => { if (event.key === 'Enter' && inputValue.trim()) { event.preventDefault() - if (!tags.includes(inputValue)) { - setTags([...tags, inputValue]) + if (!categoryValues.includes(inputValue)) { + setCategoryValues([...categoryValues, inputValue]) setInputValue('') } } } - const handleDeleteTag = (tagToDelete) => { - setTags(tags.filter((tag) => tag !== tagToDelete)) + const handleDeleteTag = (categoryToDelete) => { + setCategoryValues(categoryValues.filter((category) => category !== categoryToDelete)) } const handleSubmit = (event) => { event.preventDefault() - if (inputValue.trim() && !tags.includes(inputValue)) { - setTags([...tags, inputValue]) + let newCategories = [...categoryValues] + if (inputValue.trim() && !categoryValues.includes(inputValue)) { + newCategories = [...newCategories, inputValue] + setCategoryValues(newCategories) } - onSubmit(tags) - onClose() + onSubmit(newCategories) } + useEffect(() => { + if (dialogProps.category) setCategoryValues(dialogProps.category) + + return () => { + setInputValue('') + setCategoryValues([]) + } + }, [dialogProps]) + return ( Set Chatflow Category Tags @@ -52,17 +63,20 @@ const TagDialog = ({ isOpen, onClose, tags, setTags, onSubmit }) => {
-
- {tags.map((tag, index) => ( - handleDeleteTag(tag)} - style={{ marginRight: 5, marginBottom: 5 }} - /> - ))} -
+ {categoryValues.length > 0 && ( +
+ {categoryValues.map((category, index) => ( + handleDeleteTag(category)} + style={{ marginRight: 5, marginBottom: 5 }} + /> + ))} +
+ )} { label='Add a tag' variant='outlined' /> - + Enter a tag and press enter to add it to the list. You can add as many tags as you want. @@ -88,9 +102,8 @@ const TagDialog = ({ isOpen, onClose, tags, setTags, onSubmit }) => { TagDialog.propTypes = { isOpen: PropTypes.bool, + dialogProps: PropTypes.object, onClose: PropTypes.func, - tags: PropTypes.array, - setTags: PropTypes.func, onSubmit: PropTypes.func } diff --git a/packages/ui/src/ui-component/table/FlowListTable.js b/packages/ui/src/ui-component/table/FlowListTable.js index e33a8ba1..c879d82d 100644 --- a/packages/ui/src/ui-component/table/FlowListTable.js +++ b/packages/ui/src/ui-component/table/FlowListTable.js @@ -9,9 +9,9 @@ import TableContainer from '@mui/material/TableContainer' import TableHead from '@mui/material/TableHead' import TableRow from '@mui/material/TableRow' import Paper from '@mui/material/Paper' +import Chip from '@mui/material/Chip' import { Button, Stack, Typography } from '@mui/material' import FlowListMenu from '../button/FlowListMenu' -import Chip from '@mui/material/Chip' const StyledTableCell = styled(TableCell)(({ theme }) => ({ [`&.${tableCellClasses.head}`]: { From 0b4bf0193123e955fc990a8180139528ff7f6cd5 Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 17 Nov 2023 12:44:16 +0000 Subject: [PATCH 29/64] unhide columns when xs mode --- .../ui/src/ui-component/table/FlowListTable.js | 14 ++++++-------- packages/ui/src/views/chatflows/index.js | 7 +------ 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/packages/ui/src/ui-component/table/FlowListTable.js b/packages/ui/src/ui-component/table/FlowListTable.js index c879d82d..e3baa2e2 100644 --- a/packages/ui/src/ui-component/table/FlowListTable.js +++ b/packages/ui/src/ui-component/table/FlowListTable.js @@ -51,13 +51,13 @@ export const FlowListTable = ({ data, images, filterFunction, updateFlowsApi }) Category - + Nodes - + Last Modified Date - + Actions
@@ -90,7 +90,7 @@ export const FlowListTable = ({ data, images, filterFunction, updateFlowsApi }) ))}
- + {images[row.id] && (
)} - - {moment(row.updatedDate).format('MMMM Do, YYYY')} - - + {moment(row.updatedDate).format('MMMM Do, YYYY')} + diff --git a/packages/ui/src/views/chatflows/index.js b/packages/ui/src/views/chatflows/index.js index 34c6523b..7f288a95 100644 --- a/packages/ui/src/views/chatflows/index.js +++ b/packages/ui/src/views/chatflows/index.js @@ -152,12 +152,7 @@ const Chatflows = () => { /> - + Date: Fri, 17 Nov 2023 13:52:41 +0000 Subject: [PATCH 30/64] fix mongodb vector database where no documents are returned --- .../Elasticsearch/Elasticsearch_Upsert.ts | 2 +- .../vectorstores/MongoDB/MongoDB_Existing.ts | 12 +++++------- .../vectorstores/MongoDB/MongoDB_Upsert.ts | 17 +++++++++-------- .../nodes/vectorstores/Redis/Redis_Upsert.ts | 2 +- 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/packages/components/nodes/vectorstores/Elasticsearch/Elasticsearch_Upsert.ts b/packages/components/nodes/vectorstores/Elasticsearch/Elasticsearch_Upsert.ts index d3965786..a8ccd49a 100644 --- a/packages/components/nodes/vectorstores/Elasticsearch/Elasticsearch_Upsert.ts +++ b/packages/components/nodes/vectorstores/Elasticsearch/Elasticsearch_Upsert.ts @@ -50,7 +50,7 @@ class ElasicsearchUpsert_VectorStores extends ElasticSearchBase implements INode delete d.metadata.loc }) // end of workaround - return super.init(nodeData, _, options, flattenDocs) + return super.init(nodeData, _, options, finalDocs) } } diff --git a/packages/components/nodes/vectorstores/MongoDB/MongoDB_Existing.ts b/packages/components/nodes/vectorstores/MongoDB/MongoDB_Existing.ts index 3cbb36b8..7b06814a 100644 --- a/packages/components/nodes/vectorstores/MongoDB/MongoDB_Existing.ts +++ b/packages/components/nodes/vectorstores/MongoDB/MongoDB_Existing.ts @@ -1,11 +1,10 @@ -import { ICommonObject, INode, INodeData } from '../../../src/Interface' +import { Collection } from 'mongodb' +import { MongoDBAtlasVectorSearch } from 'langchain/vectorstores/mongodb_atlas' import { Embeddings } from 'langchain/embeddings/base' import { VectorStore } from 'langchain/vectorstores/base' import { Document } from 'langchain/document' - import { MongoDBSearchBase } from './MongoDBSearchBase' -import { Collection } from 'mongodb' -import { MongoDBAtlasVectorSearch } from 'langchain/vectorstores/mongodb_atlas' +import { ICommonObject, INode, INodeData } from '../../../src/Interface' class MongoDBExisting_VectorStores extends MongoDBSearchBase implements INode { constructor() { @@ -20,7 +19,7 @@ class MongoDBExisting_VectorStores extends MongoDBSearchBase implements INode { return super.init(nodeData, _, options, undefined) } - constructVectorStore( + async constructVectorStore( embeddings: Embeddings, collection: Collection, indexName: string, @@ -28,13 +27,12 @@ class MongoDBExisting_VectorStores extends MongoDBSearchBase implements INode { embeddingKey: string, _: Document>[] | undefined ): Promise { - const mongoDBAtlasVectorSearch = new MongoDBAtlasVectorSearch(embeddings, { + return new MongoDBAtlasVectorSearch(embeddings, { collection: collection, indexName: indexName, textKey: textKey, embeddingKey: embeddingKey }) - return Promise.resolve(mongoDBAtlasVectorSearch) } } diff --git a/packages/components/nodes/vectorstores/MongoDB/MongoDB_Upsert.ts b/packages/components/nodes/vectorstores/MongoDB/MongoDB_Upsert.ts index 80dfbf19..7d22f035 100644 --- a/packages/components/nodes/vectorstores/MongoDB/MongoDB_Upsert.ts +++ b/packages/components/nodes/vectorstores/MongoDB/MongoDB_Upsert.ts @@ -1,12 +1,11 @@ -import { ICommonObject, INode, INodeData } from '../../../src/Interface' +import { flatten } from 'lodash' +import { Collection } from 'mongodb' import { Embeddings } from 'langchain/embeddings/base' import { Document } from 'langchain/document' - -import { flatten } from 'lodash' import { VectorStore } from 'langchain/vectorstores/base' -import { MongoDBSearchBase } from './MongoDBSearchBase' -import { Collection } from 'mongodb' import { MongoDBAtlasVectorSearch } from 'langchain/vectorstores/mongodb_atlas' +import { ICommonObject, INode, INodeData } from '../../../src/Interface' +import { MongoDBSearchBase } from './MongoDBSearchBase' class MongoDBUpsert_VectorStores extends MongoDBSearchBase implements INode { constructor() { @@ -23,7 +22,7 @@ class MongoDBUpsert_VectorStores extends MongoDBSearchBase implements INode { }) } - constructVectorStore( + async constructVectorStore( embeddings: Embeddings, collection: Collection, indexName: string, @@ -31,12 +30,14 @@ class MongoDBUpsert_VectorStores extends MongoDBSearchBase implements INode { embeddingKey: string, docs: Document>[] ): Promise { - return MongoDBAtlasVectorSearch.fromDocuments(docs, embeddings, { + const mongoDBAtlasVectorSearch = new MongoDBAtlasVectorSearch(embeddings, { collection: collection, indexName: indexName, textKey: textKey, embeddingKey: embeddingKey }) + await mongoDBAtlasVectorSearch.addDocuments(docs) + return mongoDBAtlasVectorSearch } async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { @@ -51,7 +52,7 @@ class MongoDBUpsert_VectorStores extends MongoDBSearchBase implements INode { } } - return super.init(nodeData, _, options, flattenDocs) + return super.init(nodeData, _, options, finalDocs) } } diff --git a/packages/components/nodes/vectorstores/Redis/Redis_Upsert.ts b/packages/components/nodes/vectorstores/Redis/Redis_Upsert.ts index 9d1a4f45..4da58eaf 100644 --- a/packages/components/nodes/vectorstores/Redis/Redis_Upsert.ts +++ b/packages/components/nodes/vectorstores/Redis/Redis_Upsert.ts @@ -56,7 +56,7 @@ class RedisUpsert_VectorStores extends RedisSearchBase implements INode { } } - return super.init(nodeData, _, options, flattenDocs) + return super.init(nodeData, _, options, finalDocs) } } From e251bd573d50327d1a26dcee9f51f1367fc58f32 Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 17 Nov 2023 14:38:42 +0000 Subject: [PATCH 31/64] changed to enable uuid to be used as sessionId --- .../memory/MongoDBMemory/MongoDBMemory.ts | 45 ++++++++++++++++--- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts b/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts index 4c9e8581..7de2ec34 100644 --- a/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts +++ b/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts @@ -1,6 +1,7 @@ import { getBaseClasses, getCredentialData, getCredentialParam, ICommonObject, INode, INodeData, INodeParams } from '../../../src' import { MongoDBChatMessageHistory } from 'langchain/stores/message/mongodb' import { BufferMemory, BufferMemoryInput } from 'langchain/memory' +import { BaseMessage, mapStoredMessageToChatMessage } from 'langchain/schema' import { MongoClient } from 'mongodb' class MongoDB_Memory implements INode { @@ -44,11 +45,13 @@ class MongoDB_Memory implements INode { type: 'string' }, { - label: 'Session ID', + label: 'Session Id', name: 'sessionId', type: 'string', - default: '5f9cf7c08d5b1a06b80fae61', - description: 'Must be an Hex String of 24 chars. This will be the objectId of the document in MongoDB Atlas' + description: 'If not specified, the first CHAT_MESSAGE_ID will be used as sessionId', + default: '', + additionalParams: true, + optional: true }, { label: 'Memory Key', @@ -67,9 +70,10 @@ class MongoDB_Memory implements INode { async clearSessionMemory(nodeData: INodeData, options: ICommonObject): Promise { const mongodbMemory = await initializeMongoDB(nodeData, options) const sessionId = nodeData.inputs?.sessionId as string - options.logger.info(`Clearing MongoDB memory session ${sessionId}`) + const chatId = options?.chatId as string + options.logger.info(`Clearing MongoDB memory session ${sessionId ? sessionId : chatId}`) await mongodbMemory.clear() - options.logger.info(`Successfully cleared MongoDB memory session ${sessionId}`) + options.logger.info(`Successfully cleared MongoDB memory session ${sessionId ? sessionId : chatId}`) } } @@ -78,6 +82,10 @@ const initializeMongoDB = async (nodeData: INodeData, options: ICommonObject): P const collectionName = nodeData.inputs?.collectionName as string const sessionId = nodeData.inputs?.sessionId as string const memoryKey = nodeData.inputs?.memoryKey as string + const chatId = options?.chatId as string + + let isSessionIdUsingChatMessageId = false + if (!sessionId && chatId) isSessionIdUsingChatMessageId = true const credentialData = await getCredentialData(nodeData.credential ?? '', options) let mongoDBConnectUrl = getCredentialParam('mongoDBConnectUrl', credentialData, nodeData) @@ -88,14 +96,37 @@ const initializeMongoDB = async (nodeData: INodeData, options: ICommonObject): P const mongoDBChatMessageHistory = new MongoDBChatMessageHistory({ collection, - sessionId: sessionId + sessionId: sessionId ? sessionId : chatId }) + mongoDBChatMessageHistory.getMessages = async (): Promise => { + const document = await collection.findOne({ + sessionId: (mongoDBChatMessageHistory as any).sessionId + }) + const messages = document?.messages || [] + return messages.map(mapStoredMessageToChatMessage) + } + + mongoDBChatMessageHistory.addMessage = async (message: BaseMessage): Promise => { + const messages = [message].map((msg) => msg.toDict()) + await collection.updateOne( + { sessionId: (mongoDBChatMessageHistory as any).sessionId }, + { + $push: { messages: { $each: messages } } + }, + { upsert: true } + ) + } + + mongoDBChatMessageHistory.clear = async (): Promise => { + await collection.deleteOne({ sessionId: (mongoDBChatMessageHistory as any).sessionId }) + } + return new BufferMemoryExtended({ memoryKey, chatHistory: mongoDBChatMessageHistory, returnMessages: true, - isSessionIdUsingChatMessageId: false + isSessionIdUsingChatMessageId }) } From 3bddc95087a81ee2faeea835463841239c8e9b1b Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 17 Nov 2023 16:41:49 +0000 Subject: [PATCH 32/64] add user id --- packages/components/package.json | 2 +- packages/components/src/handler.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/components/package.json b/packages/components/package.json index 996419ca..0df99a11 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -49,7 +49,7 @@ "html-to-text": "^9.0.5", "ioredis": "^5.3.2", "langchain": "^0.0.165", - "langfuse-langchain": "^1.0.14-alpha.0", + "langfuse-langchain": "^1.0.31", "langsmith": "^0.0.32", "linkifyjs": "^4.1.1", "llmonitor": "^0.5.5", diff --git a/packages/components/src/handler.ts b/packages/components/src/handler.ts index 37075342..456cf39c 100644 --- a/packages/components/src/handler.ts +++ b/packages/components/src/handler.ts @@ -250,6 +250,7 @@ export const additionalCallbacks = async (nodeData: INodeData, options: ICommonO baseUrl: langFuseEndpoint ?? 'https://cloud.langfuse.com' } if (release) langFuseOptions.release = release + if (options.chatId) langFuseOptions.userId = options.chatId const handler = new CallbackHandler(langFuseOptions) callbacks.push(handler) From 28f5d94c1343e4c122d9929fefb6fce1d598cf55 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Sat, 18 Nov 2023 16:47:15 +0530 Subject: [PATCH 33/64] API Keys: Displaying the names of the chatflows associated with the keys and Warning the user before deletion. --- packages/server/src/index.ts | 30 +++++- packages/ui/src/views/apikey/index.js | 138 +++++++++++++++----------- 2 files changed, 104 insertions(+), 64 deletions(-) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index ba6c3ce0..4307946b 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1135,28 +1135,50 @@ export class App { // API Keys // ---------------------------------------- + const addChatflowsCount = async (keys: any, res: Response) => { + if (keys) { + const updatedKeys: any[] = [] + //iterate through keys and get chatflows + for (const key of keys) { + const chatflows = await this.AppDataSource.getRepository(ChatFlow) + .createQueryBuilder('cf') + .where('cf.apikeyid = :apikeyid', { apikeyid: key.id }) + .getMany() + const linkedChatFlows: any[] = [] + chatflows.map((cf) => { + linkedChatFlows.push({ + flowName: cf.name + }) + }) + key.chatFlows = linkedChatFlows + updatedKeys.push(key) + } + return res.json(updatedKeys) + } + return res.json(keys) + } // Get api keys this.app.get('/api/v1/apikey', async (req: Request, res: Response) => { const keys = await getAPIKeys() - return res.json(keys) + return addChatflowsCount(keys, res) }) // Add new api key this.app.post('/api/v1/apikey', async (req: Request, res: Response) => { const keys = await addAPIKey(req.body.keyName) - return res.json(keys) + return addChatflowsCount(keys, res) }) // Update api key this.app.put('/api/v1/apikey/:id', async (req: Request, res: Response) => { const keys = await updateAPIKey(req.params.id, req.body.keyName) - return res.json(keys) + return addChatflowsCount(keys, res) }) // Delete new api key this.app.delete('/api/v1/apikey/:id', async (req: Request, res: Response) => { const keys = await deleteAPIKey(req.params.id) - return res.json(keys) + return addChatflowsCount(keys, res) }) // Verify api key diff --git a/packages/ui/src/views/apikey/index.js b/packages/ui/src/views/apikey/index.js index a2b2e639..226baaee 100644 --- a/packages/ui/src/views/apikey/index.js +++ b/packages/ui/src/views/apikey/index.js @@ -6,6 +6,7 @@ import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackba import { Button, Box, + Chip, Stack, Table, TableBody, @@ -37,7 +38,7 @@ import useConfirm from 'hooks/useConfirm' import useNotifier from 'utils/useNotifier' // Icons -import { IconTrash, IconEdit, IconCopy, IconX, IconPlus, IconEye, IconEyeOff } from '@tabler/icons' +import { IconTrash, IconEdit, IconCopy, IconCornerDownRight, IconX, IconPlus, IconEye, IconEyeOff } from '@tabler/icons' import APIEmptySVG from 'assets/images/api_empty.svg' // ==============================|| APIKey ||============================== // @@ -106,7 +107,10 @@ const APIKey = () => { const deleteKey = async (key) => { const confirmPayload = { title: `Delete`, - description: `Delete key ${key.keyName}?`, + description: + key.chatFlows.length === 0 + ? `Delete key [${key.keyName}] ? ` + : `Delete key [${key.keyName}] ?\n There are ${key.chatFlows.length} chatflows using this key.`, confirmButtonName: 'Delete', cancelButtonName: 'Cancel' } @@ -193,6 +197,7 @@ const APIKey = () => { Key Name API Key + Usage Created @@ -200,65 +205,78 @@ const APIKey = () => { {apiKeys.map((key, index) => ( - - - {key.keyName} - - - {showApiKeys.includes(key.apiKey) - ? key.apiKey - : `${key.apiKey.substring(0, 2)}${'•'.repeat(18)}${key.apiKey.substring( - key.apiKey.length - 5 - )}`} - { - navigator.clipboard.writeText(key.apiKey) - setAnchorEl(event.currentTarget) - setTimeout(() => { - handleClosePopOver() - }, 1500) - }} - > - - - onShowApiKeyClick(key.apiKey)}> - {showApiKeys.includes(key.apiKey) ? : } - - - + + + {key.keyName} + + + {showApiKeys.includes(key.apiKey) + ? key.apiKey + : `${key.apiKey.substring(0, 2)}${'•'.repeat(18)}${key.apiKey.substring( + key.apiKey.length - 5 + )}`} + { + navigator.clipboard.writeText(key.apiKey) + setAnchorEl(event.currentTarget) + setTimeout(() => { + handleClosePopOver() + }, 1500) + }} > - Copied! - - - - {key.createdAt} - - edit(key)}> - - - - - deleteKey(key)}> - - - - + + + onShowApiKeyClick(key.apiKey)}> + {showApiKeys.includes(key.apiKey) ? : } + + + + Copied! + + + + {key.chatFlows.length} + {key.createdAt} + + edit(key)}> + + + + + deleteKey(key)}> + + + + + {key.chatFlows.length > 0 && ( + + + {' '} + {key.chatFlows.map((flow, index) => ( + + ))} + + + )} + ))}
From a4a1e7d562dff040f9dd00cc51762e426fe2318f Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Mon, 20 Nov 2023 13:03:28 +0530 Subject: [PATCH 34/64] API Key: Changes to API Key Dashboard to show usage details (chatflows) --- packages/server/src/index.ts | 3 +- packages/ui/src/views/apikey/index.js | 204 ++++++++++++++++---------- 2 files changed, 131 insertions(+), 76 deletions(-) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 4307946b..a8745db7 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1147,7 +1147,8 @@ export class App { const linkedChatFlows: any[] = [] chatflows.map((cf) => { linkedChatFlows.push({ - flowName: cf.name + flowName: cf.name, + updatedDate: cf.updatedDate }) }) key.chatFlows = linkedChatFlows diff --git a/packages/ui/src/views/apikey/index.js b/packages/ui/src/views/apikey/index.js index 226baaee..72b73baf 100644 --- a/packages/ui/src/views/apikey/index.js +++ b/packages/ui/src/views/apikey/index.js @@ -6,7 +6,6 @@ import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackba import { Button, Box, - Chip, Stack, Table, TableBody, @@ -17,7 +16,8 @@ import { Paper, IconButton, Popover, - Typography + Typography, + Collapse } from '@mui/material' import { useTheme } from '@mui/material/styles' @@ -38,11 +38,118 @@ import useConfirm from 'hooks/useConfirm' import useNotifier from 'utils/useNotifier' // Icons -import { IconTrash, IconEdit, IconCopy, IconCornerDownRight, IconX, IconPlus, IconEye, IconEyeOff } from '@tabler/icons' +import { IconTrash, IconEdit, IconCopy, IconChevronsUp, IconChevronsDown, IconX, IconPlus, IconEye, IconEyeOff } from '@tabler/icons' import APIEmptySVG from 'assets/images/api_empty.svg' +import * as PropTypes from 'prop-types' // ==============================|| APIKey ||============================== // +function APIKeyRow(props) { + const [open, setOpen] = useState(false) + return ( + <> + *': { borderBottom: 'unset' } }}> + + {props.apiKey.keyName} + + + {props.showApiKeys.includes(props.apiKey.apiKey) + ? props.apiKey.apiKey + : `${props.apiKey.apiKey.substring(0, 2)}${'•'.repeat(18)}${props.apiKey.apiKey.substring( + props.apiKey.apiKey.length - 5 + )}`} + + + + + {props.showApiKeys.includes(props.apiKey.apiKey) ? : } + + + + Copied! + + + + + {props.apiKey.chatFlows.length}{' '} + {props.apiKey.chatFlows.length > 0 && ( + setOpen(!open)}> + {props.apiKey.chatFlows.length > 0 && open ? : } + + )} + + {props.apiKey.createdAt} + + + + + + + + + + + + + + + + + + + Chatflow Name + Modified On + Category + + + + {props.apiKey.chatFlows.map((flow, index) => ( + + + {flow.flowName} + + {flow.updatedDate} + + + ))} + +
+
+
+
+
+ + ) +} + +APIKeyRow.propTypes = { + apiKey: PropTypes.any, + showApiKeys: PropTypes.arrayOf(PropTypes.any), + onCopyClick: PropTypes.func, + onShowAPIClick: PropTypes.func, + open: PropTypes.bool, + anchorEl: PropTypes.any, + onClose: PropTypes.func, + theme: PropTypes.any, + onEditClick: PropTypes.func, + onDeleteClick: PropTypes.func +} const APIKey = () => { const theme = useTheme() const customization = useSelector((state) => state.customization) @@ -205,78 +312,25 @@ const APIKey = () => { {apiKeys.map((key, index) => ( - <> - - - {key.keyName} - - - {showApiKeys.includes(key.apiKey) - ? key.apiKey - : `${key.apiKey.substring(0, 2)}${'•'.repeat(18)}${key.apiKey.substring( - key.apiKey.length - 5 - )}`} - { - navigator.clipboard.writeText(key.apiKey) - setAnchorEl(event.currentTarget) - setTimeout(() => { - handleClosePopOver() - }, 1500) - }} - > - - - onShowApiKeyClick(key.apiKey)}> - {showApiKeys.includes(key.apiKey) ? : } - - - - Copied! - - - - {key.chatFlows.length} - {key.createdAt} - - edit(key)}> - - - - - deleteKey(key)}> - - - - - {key.chatFlows.length > 0 && ( - - - {' '} - {key.chatFlows.map((flow, index) => ( - - ))} - - - )} - + { + navigator.clipboard.writeText(key.apiKey) + setAnchorEl(event.currentTarget) + setTimeout(() => { + handleClosePopOver() + }, 1500) + }} + onShowAPIClick={() => onShowApiKeyClick(key.apiKey)} + open={openPopOver} + anchorEl={anchorEl} + onClose={handleClosePopOver} + theme={theme} + onEditClick={() => edit(key)} + onDeleteClick={() => deleteKey(key)} + /> ))} From c7bf75e2597809fbdb56c04282059ece22f73f66 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Mon, 20 Nov 2023 18:19:25 +0530 Subject: [PATCH 35/64] UX Changes: persist user display choice in localStorage --- packages/ui/src/views/chatflows/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/views/chatflows/index.js b/packages/ui/src/views/chatflows/index.js index 7f288a95..3c4b8972 100644 --- a/packages/ui/src/views/chatflows/index.js +++ b/packages/ui/src/views/chatflows/index.js @@ -43,9 +43,10 @@ const Chatflows = () => { const [loginDialogProps, setLoginDialogProps] = useState({}) const getAllChatflowsApi = useApi(chatflowsApi.getAllChatflows) - const [view, setView] = React.useState('card') + const [view, setView] = React.useState(localStorage.getItem('flowDisplayStyle') || 'card') const handleChange = (event, nextView) => { + localStorage.setItem('flowDisplayStyle', nextView) setView(nextView) } From 9a3be5f4bf35706adc8cc65466b65c50b85d1f22 Mon Sep 17 00:00:00 2001 From: Henry Date: Mon, 20 Nov 2023 14:38:33 +0000 Subject: [PATCH 36/64] fix sessionid undefined --- packages/server/src/utils/index.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index 239773a9..6eb979f6 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -985,10 +985,14 @@ export const redactCredentialWithPasswordType = ( * @param {any} instance * @param {string} chatId */ -export const checkMemorySessionId = (instance: any, chatId: string): string => { +export const checkMemorySessionId = (instance: any, chatId: string): string | undefined => { if (instance.memory && instance.memory.isSessionIdUsingChatMessageId && chatId) { instance.memory.sessionId = chatId instance.memory.chatHistory.sessionId = chatId } - return instance.memory ? instance.memory.sessionId ?? instance.memory.chatHistory.sessionId : undefined + + if (instance.memory && instance.memory.sessionId) return instance.memory.sessionId + else if (instance.memory && instance.memory.chatHistory && instance.memory.chatHistory.sessionId) + return instance.memory.chatHistory.sessionId + return undefined } From 40a63008ece274161173daf4c674eb743192fb95 Mon Sep 17 00:00:00 2001 From: Henry Date: Mon, 20 Nov 2023 19:34:30 +0000 Subject: [PATCH 37/64] add vectara chain --- .../nodes/chains/VectaraChain/VectaraChain.ts | 147 ++++++++++++++++++ .../nodes/chains/VectaraChain/vectara.png | Bin 0 -> 67193 bytes packages/server/src/utils/index.ts | 2 +- .../ui-component/dialog/ViewMessagesDialog.js | 5 +- packages/ui/src/utils/genericHelper.js | 12 +- .../ui/src/views/chatmessage/ChatMessage.js | 5 +- 6 files changed, 164 insertions(+), 7 deletions(-) create mode 100644 packages/components/nodes/chains/VectaraChain/VectaraChain.ts create mode 100644 packages/components/nodes/chains/VectaraChain/vectara.png diff --git a/packages/components/nodes/chains/VectaraChain/VectaraChain.ts b/packages/components/nodes/chains/VectaraChain/VectaraChain.ts new file mode 100644 index 00000000..a2fac534 --- /dev/null +++ b/packages/components/nodes/chains/VectaraChain/VectaraChain.ts @@ -0,0 +1,147 @@ +import { INode, INodeData, INodeParams } from '../../../src/Interface' +import { getBaseClasses } from '../../../src/utils' +import { VectorDBQAChain } from 'langchain/chains' +import { Document } from 'langchain/document' +import { VectaraStore } from 'langchain/vectorstores/vectara' +import fetch from 'node-fetch' + +class VectaraChain_Chains implements INode { + label: string + name: string + version: number + type: string + icon: string + category: string + baseClasses: string[] + description: string + inputs: INodeParams[] + + constructor() { + this.label = 'Vectara QA Chain' + this.name = 'vectaraQAChain' + this.version = 1.0 + this.type = 'VectaraQAChain' + this.icon = 'vectara.png' + this.category = 'Chains' + this.description = 'QA chain for Vectara' + this.baseClasses = [this.type, ...getBaseClasses(VectorDBQAChain)] + this.inputs = [ + { + label: 'Vectara Vector Store', + name: 'vectaraStore', + type: 'VectorStore' + } + ] + } + + async init(): Promise { + return null + } + + async run(nodeData: INodeData, input: string): Promise { + const vectorStore = nodeData.inputs?.vectaraStore as VectaraStore + const topK = (vectorStore as any)?.k ?? 4 + + const headers = await vectorStore.getJsonHeader() + const vectaraFilter = (vectorStore as any).vectaraFilter ?? {} + const corpusId: number[] = (vectorStore as any).corpusId ?? [] + const customerId = (vectorStore as any).customerId ?? '' + + const corpusKeys = corpusId.map((corpusId) => ({ + customerId, + corpusId, + metadataFilter: vectaraFilter?.filter ?? '', + lexicalInterpolationConfig: { lambda: vectaraFilter?.lambda ?? 0.025 } + })) + + let summarizerPromptName = 'vectara-experimental-summary-ext-2023-10-23-med' // can let user select + let responseLang = 'en' // can let user select + let maxSummarizedResults = 5 // can let user specify + + const data = { + query: [ + { + query: input, + start: 0, + numResults: topK, + contextConfig: { + sentencesAfter: vectaraFilter?.contextConfig?.sentencesAfter ?? 2, + sentencesBefore: vectaraFilter?.contextConfig?.sentencesBefore ?? 2 + }, + corpusKey: corpusKeys, + summary: [ + { + summarizerPromptName, + responseLang, + maxSummarizedResults + } + ] + } + ] + } + + try { + const response = await fetch(`https://api.vectara.io/v1/query`, { + method: 'POST', + headers: headers?.headers, + body: JSON.stringify(data) + }) + + if (response.status !== 200) { + throw new Error(`Vectara API returned status code ${response.status}`) + } + + const result = await response.json() + const responses = result.responseSet[0].response + const documents = result.responseSet[0].document + let summarizedText = '' + + for (let i = 0; i < responses.length; i += 1) { + const responseMetadata = responses[i].metadata + const documentMetadata = documents[responses[i].documentIndex].metadata + const combinedMetadata: Record = {} + + responseMetadata.forEach((item: { name: string; value: unknown }) => { + combinedMetadata[item.name] = item.value + }) + + documentMetadata.forEach((item: { name: string; value: unknown }) => { + combinedMetadata[item.name] = item.value + }) + + responses[i].metadata = combinedMetadata + } + + const summaryStatus = result.responseSet[0].summary[0].status + if (summaryStatus.length > 0 && summaryStatus[0].code === 'BAD_REQUEST') { + throw new Error( + `BAD REQUEST: Too much text for the summarizer to summarize. Please try reducing the number of search results to summarize, or the context of each result by adjusting the 'summary_num_sentences', and 'summary_num_results' parameters respectively.` + ) + } + + if ( + summaryStatus.length > 0 && + summaryStatus[0].code === 'NOT_FOUND' && + summaryStatus[0].statusDetail === 'Failed to retrieve summarizer.' + ) { + throw new Error(`BAD REQUEST: summarizer ${summarizerPromptName} is invalid for this account.`) + } + + summarizedText = result.responseSet[0].summary[0]?.text + + const sourceDocuments: Document[] = responses.map( + (response: { text: string; metadata: Record; score: number }) => + new Document({ + pageContent: response.text, + metadata: response.metadata + }) + ) + + return { text: summarizedText, sourceDocuments: sourceDocuments } + } catch (error) { + throw new Error(error) + } + } +} + +module.exports = { nodeClass: VectaraChain_Chains } diff --git a/packages/components/nodes/chains/VectaraChain/vectara.png b/packages/components/nodes/chains/VectaraChain/vectara.png new file mode 100644 index 0000000000000000000000000000000000000000..a13a34e6b837a94f70e01253cd3e17aa02b68f48 GIT binary patch literal 67193 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%0A)QN)(hd`;d-QP6WmNWEHJ!9n^=PgZ@EK~ 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 r=_yJBf`HDk literal 0 HcmV?d00001 diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index 239773a9..616454a5 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -842,7 +842,7 @@ export const isFlowValidForStream = (reactFlowNodes: IReactFlowNode[], endingNod let isValidChainOrAgent = false if (endingNodeData.category === 'Chains') { // Chains that are not available to stream - const blacklistChains = ['openApiChain'] + const blacklistChains = ['openApiChain', 'vectaraQAChain'] isValidChainOrAgent = !blacklistChains.includes(endingNodeData.name) } else if (endingNodeData.category === 'Agents') { // Agent that are available to stream diff --git a/packages/ui/src/ui-component/dialog/ViewMessagesDialog.js b/packages/ui/src/ui-component/dialog/ViewMessagesDialog.js index 29a64155..cadd4abd 100644 --- a/packages/ui/src/ui-component/dialog/ViewMessagesDialog.js +++ b/packages/ui/src/ui-component/dialog/ViewMessagesDialog.js @@ -699,7 +699,10 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { {message.sourceDocuments && (
{removeDuplicateURL(message).map((source, index) => { - const URL = isValidURL(source.metadata.source) + const URL = + source.metadata && source.metadata.source + ? isValidURL(source.metadata.source) + : undefined return ( { if (!message.sourceDocuments) return newSourceDocuments message.sourceDocuments.forEach((source) => { - if (isValidURL(source.metadata.source) && !visitedURLs.includes(source.metadata.source)) { - visitedURLs.push(source.metadata.source) - newSourceDocuments.push(source) - } else if (!isValidURL(source.metadata.source)) { + if (source.metadata && source.metadata.source) { + if (isValidURL(source.metadata.source) && !visitedURLs.includes(source.metadata.source)) { + visitedURLs.push(source.metadata.source) + newSourceDocuments.push(source) + } else if (!isValidURL(source.metadata.source)) { + newSourceDocuments.push(source) + } + } else { newSourceDocuments.push(source) } }) diff --git a/packages/ui/src/views/chatmessage/ChatMessage.js b/packages/ui/src/views/chatmessage/ChatMessage.js index 7cfd0474..7e805f7e 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.js +++ b/packages/ui/src/views/chatmessage/ChatMessage.js @@ -379,7 +379,10 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { {message.sourceDocuments && (
{removeDuplicateURL(message).map((source, index) => { - const URL = isValidURL(source.metadata.source) + const URL = + source.metadata && source.metadata.source + ? isValidURL(source.metadata.source) + : undefined return ( Date: Tue, 21 Nov 2023 10:56:32 +0000 Subject: [PATCH 38/64] add more options --- .../nodes/chains/VectaraChain/VectaraChain.ts | 172 +++++++++++++++++- 1 file changed, 166 insertions(+), 6 deletions(-) diff --git a/packages/components/nodes/chains/VectaraChain/VectaraChain.ts b/packages/components/nodes/chains/VectaraChain/VectaraChain.ts index a2fac534..143c6d5b 100644 --- a/packages/components/nodes/chains/VectaraChain/VectaraChain.ts +++ b/packages/components/nodes/chains/VectaraChain/VectaraChain.ts @@ -27,9 +27,168 @@ class VectaraChain_Chains implements INode { this.baseClasses = [this.type, ...getBaseClasses(VectorDBQAChain)] this.inputs = [ { - label: 'Vectara Vector Store', + label: 'Vectara Store', name: 'vectaraStore', type: 'VectorStore' + }, + { + label: 'Summarizer Prompt Name', + name: 'summarizerPromptName', + description: + 'Summarize the results fetched from Vectara. Read more', + type: 'options', + options: [ + { + label: 'vectara-summary-ext-v1.2.0 (gpt-3.5-turbo)', + name: 'vectara-summary-ext-v1.2.0' + }, + { + label: 'vectara-experimental-summary-ext-2023-10-23-small (gpt-3.5-turbo)', + name: 'vectara-experimental-summary-ext-2023-10-23-small', + description: 'In beta, available to both Growth and Scale Vectara users' + }, + { + label: 'vectara-summary-ext-v1.3.0 (gpt-4.0)', + name: 'vectara-summary-ext-v1.3.0', + description: 'Only available to paying Scale Vectara users' + }, + { + label: 'vectara-experimental-summary-ext-2023-10-23-med (gpt-4.0)', + name: 'vectara-experimental-summary-ext-2023-10-23-med', + description: 'In beta, only available to paying Scale Vectara users' + } + ], + default: 'vectara-summary-ext-v1.2.0' + }, + { + label: 'Response Language', + name: 'responseLang', + description: + 'Return the response in specific language. If not selected, Vectara will automatically detects the language. Read more', + type: 'options', + options: [ + { + label: 'English', + name: 'eng' + }, + { + label: 'German', + name: 'deu' + }, + { + label: 'French', + name: 'fra' + }, + { + label: 'Chinese', + name: 'zho' + }, + { + label: 'Korean', + name: 'kor' + }, + { + label: 'Arabic', + name: 'ara' + }, + { + label: 'Russian', + name: 'rus' + }, + { + label: 'Thai', + name: 'tha' + }, + { + label: 'Dutch', + name: 'nld' + }, + { + label: 'Italian', + name: 'ita' + }, + { + label: 'Portuguese', + name: 'por' + }, + { + label: 'Spanish', + name: 'spa' + }, + { + label: 'Japanese', + name: 'jpn' + }, + { + label: 'Polish', + name: 'pol' + }, + { + label: 'Turkish', + name: 'tur' + }, + { + label: 'Vietnamese', + name: 'vie' + }, + { + label: 'Indonesian', + name: 'ind' + }, + { + label: 'Czech', + name: 'ces' + }, + { + label: 'Ukrainian', + name: 'ukr' + }, + { + label: 'Greek', + name: 'ell' + }, + { + label: 'Hebrew', + name: 'heb' + }, + { + label: 'Farsi/Persian', + name: 'fas' + }, + { + label: 'Hindi', + name: 'hin' + }, + { + label: 'Urdu', + name: 'urd' + }, + { + label: 'Swedish', + name: 'swe' + }, + { + label: 'Bengali', + name: 'ben' + }, + { + label: 'Malay', + name: 'msa' + }, + { + label: 'Romanian', + name: 'ron' + } + ], + optional: true, + default: 'eng' + }, + { + label: 'Max Summarized Results', + name: 'maxSummarizedResults', + description: 'Maximum results used to build the summarized response', + type: 'number', + default: 7 } ] } @@ -40,7 +199,12 @@ class VectaraChain_Chains implements INode { async run(nodeData: INodeData, input: string): Promise { const vectorStore = nodeData.inputs?.vectaraStore as VectaraStore - const topK = (vectorStore as any)?.k ?? 4 + const responseLang = (nodeData.inputs?.responseLang as string) ?? 'auto' + const summarizerPromptName = nodeData.inputs?.summarizerPromptName as string + const maxSummarizedResultsStr = nodeData.inputs?.maxSummarizedResults as string + const maxSummarizedResults = maxSummarizedResultsStr ? parseInt(maxSummarizedResultsStr, 10) : 7 + + const topK = (vectorStore as any)?.k ?? 10 const headers = await vectorStore.getJsonHeader() const vectaraFilter = (vectorStore as any).vectaraFilter ?? {} @@ -54,10 +218,6 @@ class VectaraChain_Chains implements INode { lexicalInterpolationConfig: { lambda: vectaraFilter?.lambda ?? 0.025 } })) - let summarizerPromptName = 'vectara-experimental-summary-ext-2023-10-23-med' // can let user select - let responseLang = 'en' // can let user select - let maxSummarizedResults = 5 // can let user specify - const data = { query: [ { From c716c1972aeebf50aa7a9bc9086061a72a9716a5 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Tue, 21 Nov 2023 18:40:20 +0530 Subject: [PATCH 39/64] API Key: UX Fixes and adjustments post the dashboard updates --- packages/server/src/index.ts | 1 + packages/ui/src/views/apikey/index.js | 36 +++++++++++++++------------ 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 922aa307..2f7d31e2 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1161,6 +1161,7 @@ export class App { chatflows.map((cf) => { linkedChatFlows.push({ flowName: cf.name, + category: cf.category, updatedDate: cf.updatedDate }) }) diff --git a/packages/ui/src/views/apikey/index.js b/packages/ui/src/views/apikey/index.js index 96b0d1de..73224cb2 100644 --- a/packages/ui/src/views/apikey/index.js +++ b/packages/ui/src/views/apikey/index.js @@ -6,6 +6,7 @@ import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackba import { Button, Box, + Chip, Stack, Table, TableBody, @@ -56,6 +57,7 @@ import { } from '@tabler/icons' import APIEmptySVG from 'assets/images/api_empty.svg' import * as PropTypes from 'prop-types' +import moment from 'moment/moment' // ==============================|| APIKey ||============================== // @@ -63,10 +65,8 @@ function APIKeyRow(props) { const [open, setOpen] = useState(false) return ( <> - *': { borderBottom: 'unset' } }}> - - {props.apiKey.keyName} - + + {props.apiKey.keyName} {props.showApiKeys.includes(props.apiKey.apiKey) ? props.apiKey.apiKey @@ -118,19 +118,15 @@ function APIKeyRow(props) { - + - +
- Chatflow Name - Modified On - Category + Chatflow Name + Modified On + Category @@ -139,8 +135,16 @@ function APIKeyRow(props) { {flow.flowName} - {flow.updatedDate} - + {moment(flow.updatedDate).format('DD-MMM-YY')} + +   + {flow.category && + flow.category + .split(';') + .map((tag, index) => ( + + ))} + ))} @@ -375,7 +379,7 @@ const APIKey = () => { - {apiKeys.map((key, index) => ( + {apiKeys.filter(filterKeys).map((key, index) => ( Date: Tue, 21 Nov 2023 07:52:00 -0800 Subject: [PATCH 40/64] reorder citations in Vectara response --- .../nodes/chains/VectaraChain/VectaraChain.ts | 45 +++++++++++++++++-- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/packages/components/nodes/chains/VectaraChain/VectaraChain.ts b/packages/components/nodes/chains/VectaraChain/VectaraChain.ts index 143c6d5b..2f7d09a2 100644 --- a/packages/components/nodes/chains/VectaraChain/VectaraChain.ts +++ b/packages/components/nodes/chains/VectaraChain/VectaraChain.ts @@ -5,6 +5,42 @@ import { Document } from 'langchain/document' import { VectaraStore } from 'langchain/vectorstores/vectara' import fetch from 'node-fetch' +// functionality based on https://github.com/vectara/vectara-answer +const reorderCitations = (unorderedSummary: string) => { + const allCitations = unorderedSummary.match(/\[\d+\]/g) || []; + + const uniqueCitations = [...new Set(allCitations)]; + const citationToReplacement: { [key: string]: string } = {}; + uniqueCitations.forEach((citation, index) => { + citationToReplacement[citation] = `[${index + 1}]`; + }); + + return unorderedSummary.replace( + /\[\d+\]/g, + (match) => citationToReplacement[match] + ); +}; +const applyCitationOrder = ( + searchResults: any[], + unorderedSummary: string + ) => { + const orderedSearchResults: any[] = []; + const allCitations = unorderedSummary.match(/\[\d+\]/g) || []; + + const addedIndices = new Set(); + for (let i = 0; i < allCitations.length; i++) { + const citation = allCitations[i]; + const index = Number(citation.slice(1, citation.length - 1)) - 1; + + if (addedIndices.has(index)) continue; + orderedSearchResults.push(searchResults[index]); + addedIndices.add(index); + } + + return orderedSearchResults; +}; + + class VectaraChain_Chains implements INode { label: string name: string @@ -254,7 +290,7 @@ class VectaraChain_Chains implements INode { const result = await response.json() const responses = result.responseSet[0].response const documents = result.responseSet[0].document - let summarizedText = '' + let rawSummarizedText = '' for (let i = 0; i < responses.length; i += 1) { const responseMetadata = responses[i].metadata @@ -287,9 +323,12 @@ class VectaraChain_Chains implements INode { throw new Error(`BAD REQUEST: summarizer ${summarizerPromptName} is invalid for this account.`) } - summarizedText = result.responseSet[0].summary[0]?.text + rawSummarizedText = result.responseSet[0].summary[0]?.text - const sourceDocuments: Document[] = responses.map( + let summarizedText = reorderCitations(rawSummarizedText); + let summaryResponses = applyCitationOrder(responses, rawSummarizedText); + + const sourceDocuments: Document[] = summaryResponses.map( (response: { text: string; metadata: Record; score: number }) => new Document({ pageContent: response.text, From a4c3250a67ea7a2de10b289b4bc842cd19e194f2 Mon Sep 17 00:00:00 2001 From: Ofer Mendelevitch Date: Tue, 21 Nov 2023 09:14:12 -0800 Subject: [PATCH 41/64] fixed lint issues --- .../nodes/chains/VectaraChain/VectaraChain.ts | 59 ++++++++----------- packages/components/package.json | 4 ++ 2 files changed, 30 insertions(+), 33 deletions(-) diff --git a/packages/components/nodes/chains/VectaraChain/VectaraChain.ts b/packages/components/nodes/chains/VectaraChain/VectaraChain.ts index 2f7d09a2..3799d062 100644 --- a/packages/components/nodes/chains/VectaraChain/VectaraChain.ts +++ b/packages/components/nodes/chains/VectaraChain/VectaraChain.ts @@ -7,39 +7,32 @@ import fetch from 'node-fetch' // functionality based on https://github.com/vectara/vectara-answer const reorderCitations = (unorderedSummary: string) => { - const allCitations = unorderedSummary.match(/\[\d+\]/g) || []; - - const uniqueCitations = [...new Set(allCitations)]; - const citationToReplacement: { [key: string]: string } = {}; + const allCitations = unorderedSummary.match(/\[\d+\]/g) || [] + + const uniqueCitations = [...new Set(allCitations)] + const citationToReplacement: { [key: string]: string } = {} uniqueCitations.forEach((citation, index) => { - citationToReplacement[citation] = `[${index + 1}]`; - }); - - return unorderedSummary.replace( - /\[\d+\]/g, - (match) => citationToReplacement[match] - ); -}; -const applyCitationOrder = ( - searchResults: any[], - unorderedSummary: string - ) => { - const orderedSearchResults: any[] = []; - const allCitations = unorderedSummary.match(/\[\d+\]/g) || []; - - const addedIndices = new Set(); + citationToReplacement[citation] = `[${index + 1}]` + }) + + return unorderedSummary.replace(/\[\d+\]/g, (match) => citationToReplacement[match]) +} +const applyCitationOrder = (searchResults: any[], unorderedSummary: string) => { + const orderedSearchResults: any[] = [] + const allCitations = unorderedSummary.match(/\[\d+\]/g) || [] + + const addedIndices = new Set() for (let i = 0; i < allCitations.length; i++) { - const citation = allCitations[i]; - const index = Number(citation.slice(1, citation.length - 1)) - 1; - - if (addedIndices.has(index)) continue; - orderedSearchResults.push(searchResults[index]); - addedIndices.add(index); + const citation = allCitations[i] + const index = Number(citation.slice(1, citation.length - 1)) - 1 + + if (addedIndices.has(index)) continue + orderedSearchResults.push(searchResults[index]) + addedIndices.add(index) } - - return orderedSearchResults; -}; - + + return orderedSearchResults +} class VectaraChain_Chains implements INode { label: string @@ -325,9 +318,9 @@ class VectaraChain_Chains implements INode { rawSummarizedText = result.responseSet[0].summary[0]?.text - let summarizedText = reorderCitations(rawSummarizedText); - let summaryResponses = applyCitationOrder(responses, rawSummarizedText); - + let summarizedText = reorderCitations(rawSummarizedText) + let summaryResponses = applyCitationOrder(responses, rawSummarizedText) + const sourceDocuments: Document[] = summaryResponses.map( (response: { text: string; metadata: Record; score: number }) => new Document({ diff --git a/packages/components/package.json b/packages/components/package.json index c7a29a9f..1d4cea57 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -47,6 +47,7 @@ "google-auth-library": "^9.0.0", "graphql": "^16.6.0", "html-to-text": "^9.0.5", + "husky": "^8.0.3", "ioredis": "^5.3.2", "langchain": "^0.0.165", "langfuse-langchain": "^1.0.31", @@ -82,6 +83,9 @@ "@types/object-hash": "^3.0.2", "@types/pg": "^8.10.2", "@types/ws": "^8.5.3", + "eslint-plugin-markdown": "^3.0.1", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.6.0", "gulp": "^4.0.2", "typescript": "^4.8.4" } From 681890a600b804cf116088739ced9dcc3e0d97bc Mon Sep 17 00:00:00 2001 From: tirongi Date: Tue, 21 Nov 2023 19:13:19 +0100 Subject: [PATCH 42/64] Enable inserting custom URL using basic auth --- .../ElectricsearchUserPassword.credential.ts | 2 +- .../Elasticsearch/ElasticSearchBase.ts | 31 ++++++++++++++----- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/packages/components/credentials/ElectricsearchUserPassword.credential.ts b/packages/components/credentials/ElectricsearchUserPassword.credential.ts index 6c47f7b1..76b9a0eb 100644 --- a/packages/components/credentials/ElectricsearchUserPassword.credential.ts +++ b/packages/components/credentials/ElectricsearchUserPassword.credential.ts @@ -15,7 +15,7 @@ class ElasticSearchUserPassword implements INodeCredential { 'Refer to official guide on how to get User Password from ElasticSearch' this.inputs = [ { - label: 'Cloud ID', + label: 'Cloud ID or custom server URL', name: 'cloudId', type: 'string' }, diff --git a/packages/components/nodes/vectorstores/Elasticsearch/ElasticSearchBase.ts b/packages/components/nodes/vectorstores/Elasticsearch/ElasticSearchBase.ts index 59294b7e..68c8392a 100644 --- a/packages/components/nodes/vectorstores/Elasticsearch/ElasticSearchBase.ts +++ b/packages/components/nodes/vectorstores/Elasticsearch/ElasticSearchBase.ts @@ -144,15 +144,30 @@ export abstract class ElasticSearchBase { } else if (cloudId) { let username = getCredentialParam('username', credentialData, nodeData) let password = getCredentialParam('password', credentialData, nodeData) - elasticSearchClientOptions = { - cloud: { - id: cloudId - }, - auth: { - username: username, - password: password + if (cloudId.startsWith('http')) { + let username = getCredentialParam('username', credentialData, nodeData) + let password = getCredentialParam('password', credentialData, nodeData) + elasticSearchClientOptions = { + node: cloudId, + auth: { + username: username, + password: password + }, + tls: { + rejectUnauthorized: false + } } - } + } else{ + elasticSearchClientOptions = { + cloud: { + id: cloudId + }, + auth: { + username: username, + password: password + } + } + } } return elasticSearchClientOptions } From 8358e59df2dfa2e05e6bfc37b0a0821a2169f39f Mon Sep 17 00:00:00 2001 From: Henry Date: Tue, 21 Nov 2023 19:18:08 +0000 Subject: [PATCH 43/64] add claude-2.1 --- .../nodes/chatmodels/ChatAnthropic/ChatAnthropic.ts | 7 ++++++- .../server/marketplaces/chatflows/Claude LLM.json | 11 ++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/components/nodes/chatmodels/ChatAnthropic/ChatAnthropic.ts b/packages/components/nodes/chatmodels/ChatAnthropic/ChatAnthropic.ts index f16968b6..358a15d1 100644 --- a/packages/components/nodes/chatmodels/ChatAnthropic/ChatAnthropic.ts +++ b/packages/components/nodes/chatmodels/ChatAnthropic/ChatAnthropic.ts @@ -19,7 +19,7 @@ class ChatAnthropic_ChatModels implements INode { constructor() { this.label = 'ChatAnthropic' this.name = 'chatAnthropic' - this.version = 2.0 + this.version = 3.0 this.type = 'ChatAnthropic' this.icon = 'chatAnthropic.png' this.category = 'Chat Models' @@ -48,6 +48,11 @@ class ChatAnthropic_ChatModels implements INode { name: 'claude-2', description: 'Claude 2 latest major version, automatically get updates to the model as they are released' }, + { + label: 'claude-2.1', + name: 'claude-2.1', + description: 'Claude 2 latest full version' + }, { label: 'claude-instant-1', name: 'claude-instant-1', diff --git a/packages/server/marketplaces/chatflows/Claude LLM.json b/packages/server/marketplaces/chatflows/Claude LLM.json index b7989815..0ead3dd8 100644 --- a/packages/server/marketplaces/chatflows/Claude LLM.json +++ b/packages/server/marketplaces/chatflows/Claude LLM.json @@ -1,5 +1,5 @@ { - "description": "Use Anthropic Claude with 100k context window to ingest whole document for QnA", + "description": "Use Anthropic Claude with 200k context window to ingest whole document for QnA", "nodes": [ { "width": 300, @@ -148,7 +148,7 @@ "id": "chatAnthropic_0", "label": "ChatAnthropic", "name": "chatAnthropic", - "version": 2, + "version": 3, "type": "ChatAnthropic", "baseClasses": ["ChatAnthropic", "BaseChatModel", "BaseLanguageModel"], "category": "Chat Models", @@ -171,6 +171,11 @@ "name": "claude-2", "description": "Claude 2 latest major version, automatically get updates to the model as they are released" }, + { + "label": "claude-2.1", + "name": "claude-2.1", + "description": "Claude 2 latest full version" + }, { "label": "claude-instant-1", "name": "claude-instant-1", @@ -268,7 +273,7 @@ } ], "inputs": { - "modelName": "claude-2", + "modelName": "claude-2.1", "temperature": 0.9, "maxTokensToSample": "", "topP": "", From 88c9514cca6a9ede2ae71365ab2305a3e473bafd Mon Sep 17 00:00:00 2001 From: tirongi Date: Tue, 21 Nov 2023 21:45:51 +0100 Subject: [PATCH 44/64] changes based on suggestions --- .../credentials/ElectricsearchUserPassword.credential.ts | 5 +++-- .../nodes/vectorstores/Elasticsearch/ElasticSearchBase.ts | 6 ++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/components/credentials/ElectricsearchUserPassword.credential.ts b/packages/components/credentials/ElectricsearchUserPassword.credential.ts index 76b9a0eb..ef4f3490 100644 --- a/packages/components/credentials/ElectricsearchUserPassword.credential.ts +++ b/packages/components/credentials/ElectricsearchUserPassword.credential.ts @@ -12,10 +12,11 @@ class ElasticSearchUserPassword implements INodeCredential { this.name = 'elasticSearchUserPassword' this.version = 1.0 this.description = - 'Refer to official guide on how to get User Password from ElasticSearch' + `Use Cloud ID field to enter your Elastic Cloud ID or the URL of the Elastic server instance. + Refer to official guide on how to get User Password from ElasticSearch.` this.inputs = [ { - label: 'Cloud ID or custom server URL', + label: 'Cloud ID', name: 'cloudId', type: 'string' }, diff --git a/packages/components/nodes/vectorstores/Elasticsearch/ElasticSearchBase.ts b/packages/components/nodes/vectorstores/Elasticsearch/ElasticSearchBase.ts index 68c8392a..a1233c21 100644 --- a/packages/components/nodes/vectorstores/Elasticsearch/ElasticSearchBase.ts +++ b/packages/components/nodes/vectorstores/Elasticsearch/ElasticSearchBase.ts @@ -145,8 +145,6 @@ export abstract class ElasticSearchBase { let username = getCredentialParam('username', credentialData, nodeData) let password = getCredentialParam('password', credentialData, nodeData) if (cloudId.startsWith('http')) { - let username = getCredentialParam('username', credentialData, nodeData) - let password = getCredentialParam('password', credentialData, nodeData) elasticSearchClientOptions = { node: cloudId, auth: { @@ -157,7 +155,7 @@ export abstract class ElasticSearchBase { rejectUnauthorized: false } } - } else{ + } else { elasticSearchClientOptions = { cloud: { id: cloudId @@ -167,7 +165,7 @@ export abstract class ElasticSearchBase { password: password } } - } + } } return elasticSearchClientOptions } From 08443fdb1f93560fd94cceaba8ca103598fb4316 Mon Sep 17 00:00:00 2001 From: tirongi Date: Tue, 21 Nov 2023 21:55:31 +0100 Subject: [PATCH 45/64] linting --- .../credentials/ElectricsearchUserPassword.credential.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/components/credentials/ElectricsearchUserPassword.credential.ts b/packages/components/credentials/ElectricsearchUserPassword.credential.ts index ef4f3490..c1ac82c1 100644 --- a/packages/components/credentials/ElectricsearchUserPassword.credential.ts +++ b/packages/components/credentials/ElectricsearchUserPassword.credential.ts @@ -11,8 +11,7 @@ class ElasticSearchUserPassword implements INodeCredential { this.label = 'ElasticSearch User Password' this.name = 'elasticSearchUserPassword' this.version = 1.0 - this.description = - `Use Cloud ID field to enter your Elastic Cloud ID or the URL of the Elastic server instance. + this.description = `Use Cloud ID field to enter your Elastic Cloud ID or the URL of the Elastic server instance. Refer to official guide on how to get User Password from ElasticSearch.` this.inputs = [ { From 619fb4f5c14c401d6b011523adf8122740e95f71 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Wed, 22 Nov 2023 19:19:59 +0530 Subject: [PATCH 46/64] ResponsibleAI - Input Moderation - Simplifying the Options for OpenAI Moderation. --- .../InputModeration/OpenAIModeration.ts | 112 +----------------- .../InputModeration/OpenAIModerationRunner.ts | 37 +----- 2 files changed, 5 insertions(+), 144 deletions(-) diff --git a/packages/components/nodes/responsibleAI/InputModeration/OpenAIModeration.ts b/packages/components/nodes/responsibleAI/InputModeration/OpenAIModeration.ts index 79297d74..bf2b2268 100644 --- a/packages/components/nodes/responsibleAI/InputModeration/OpenAIModeration.ts +++ b/packages/components/nodes/responsibleAI/InputModeration/OpenAIModeration.ts @@ -24,26 +24,6 @@ class OpenAIModeration implements INode { this.description = 'Check whether content complies with OpenAI usage policies.' this.baseClasses = [this.type, ...getBaseClasses(Moderation)] this.inputs = [ - { - label: 'Moderation Checks', - name: 'moderationConfig', - type: 'options', - default: 'useDefault', - options: [ - { - label: 'OpenAI Default', - name: 'useDefault' - }, - { - label: 'Use Custom Threshold Values', - name: 'useCustom' - }, - { - label: 'Combine OpenAI Default with Custom Threshold Values', - name: 'combineBoth' - } - ] - }, { label: 'Error Message', name: 'moderationErrorMessage', @@ -51,102 +31,14 @@ class OpenAIModeration implements INode { rows: 2, default: "Cannot Process! Input violates OpenAI's content moderation policies.", optional: true - }, - { - label: 'Threshold Score - Sexual', - name: 'catSexualThreshold', - type: 'number', - default: 0.01, - additionalParams: true - }, - { - label: 'Threshold Score - Sexual/Minors', - name: 'catSexualMinorsThreshold', - type: 'number', - default: 0.01, - additionalParams: true - }, - { - label: 'Threshold Score - Hate', - name: 'catHateThreshold', - type: 'number', - default: 0.01, - additionalParams: true - }, - { - label: 'Threshold Score - Hate/Threatening', - name: 'catHateThreateningThreshold', - type: 'number', - default: 0.01, - additionalParams: true - }, - { - label: 'Threshold Score - Harassment', - name: 'catHarassmentThreshold', - type: 'number', - default: 0.01, - additionalParams: true - }, - { - label: 'Threshold Score - Harassment/Threatening', - name: 'catHarassmentThreateningThreshold', - type: 'number', - default: 0.01, - additionalParams: true - }, - { - label: 'Threshold Score - Self Harm', - name: 'catSelfHarmThreshold', - type: 'number', - default: 0.01, - additionalParams: true - }, - { - label: 'Threshold Score - Self-Harm/Intent', - name: 'catSelfHarmIntentThreshold', - type: 'number', - default: 0.01, - additionalParams: true - }, - { - label: 'Threshold Score - Self-Harm/Instructions', - name: 'catSelfHarmInstructionsThreshold', - type: 'number', - default: 0.01, - additionalParams: true - }, - { - label: 'Threshold Score - Violence', - name: 'catViolenceThreshold', - type: 'number', - default: 0.01, - additionalParams: true - }, - { - label: 'Threshold Score - Violence/Graphic', - name: 'catViolenceGraphicThreshold', - type: 'number', - default: 0.01, - additionalParams: true } ] } async init(nodeData: INodeData): Promise { const runner = new OpenAIModerationRunner() - this.inputs.forEach((value) => { - if (value.additionalParams === true) { - // remove thePrefix - 'cat' - let categoryName = value.name.substring(3) - // remove theSuffix - 'Threshold' - categoryName = categoryName.substring(0, categoryName.length - 9) - categoryName = categoryName.substring(0, 1).toLowerCase() + categoryName.substring(1) - let categoryThreshold = nodeData.inputs ? nodeData.inputs[value.name] : value.default - runner.setParameter(categoryName, parseFloat(categoryThreshold)) - } else { - runner.setParameter(value.name, nodeData.inputs ? nodeData.inputs[value.name] : value.default) - } - }) + const moderationErrorMessage = nodeData.inputs?.moderationErrorMessage as string + if (moderationErrorMessage) runner.setErrorMessage(moderationErrorMessage) return runner } } diff --git a/packages/components/nodes/responsibleAI/InputModeration/OpenAIModerationRunner.ts b/packages/components/nodes/responsibleAI/InputModeration/OpenAIModerationRunner.ts index 13bf2212..293a75d0 100644 --- a/packages/components/nodes/responsibleAI/InputModeration/OpenAIModerationRunner.ts +++ b/packages/components/nodes/responsibleAI/InputModeration/OpenAIModerationRunner.ts @@ -3,19 +3,7 @@ import { BaseLanguageModel } from 'langchain/base_language' import { OpenAIModerationChain } from 'langchain/chains' export class OpenAIModerationRunner implements Moderation { - private moderationConfig: string = 'useDefault' private moderationErrorMessage: string = "Text was found that violates OpenAI's content policy." - private sexual: number = 0.01 - private sexualMinors: number = 0.01 - private hate: number = 0.01 - private hateThreatening: number = 0.01 - private harassment: number = 0.01 - private harassmentThreatening: number = 0.01 - private selfHarm: number = 0.01 - private selfHarmIntent: number = 0.01 - private selfHarmInstructions: number = 0.01 - private violence: number = 0.01 - private violenceGraphic: number = 0.01 async checkForViolations(llm: BaseLanguageModel, input: string): Promise { const openAIApiKey = (llm as any).openAIApiKey @@ -31,32 +19,13 @@ export class OpenAIModerationRunner implements Moderation { const { output: moderationOutput, results } = await moderation.call({ input: input }) - if (this.moderationConfig != 'useCustom' && results[0].flagged) { + if (results[0].flagged) { throw Error(this.moderationErrorMessage) } - if (this.moderationConfig != 'useDefault') { - const categoryScores = results[0].category_scores - if ( - categoryScores['harassment'] > this.harassment || - categoryScores['harassment/threatening'] > this.harassmentThreatening || - categoryScores['self-harm'] > this.selfHarm || - categoryScores['self-harm/intent'] > this.selfHarmIntent || - categoryScores['self-harm/instructions'] > this.selfHarmInstructions || - categoryScores['sexual'] > this.sexual || - categoryScores['sexual/minors'] > this.sexualMinors || - categoryScores['hate'] > this.hate || - categoryScores['hate/threatening'] > this.hateThreatening || - categoryScores['violence'] > this.violence || - categoryScores['violence/graphic'] > this.violenceGraphic - ) { - throw Error(this.moderationErrorMessage) - } - } return moderationOutput } - setParameter(category: string, value: number) { - // @ts-ignore - this[category] = value + setErrorMessage(message: string) { + this.moderationErrorMessage = message } } From c274085d425e1dc1f4d3d56ff75923355aff40ea Mon Sep 17 00:00:00 2001 From: Henry Date: Wed, 22 Nov 2023 17:10:18 +0000 Subject: [PATCH 47/64] slight naming changes --- .../nodes/chains/LLMChain/LLMChain.ts | 17 +- .../InputModeration/OpenAIModeration.ts | 2 +- .../InputModeration/SimplePromptModeration.ts | 5 +- .../SimplePromptModerationRunner.ts | 2 +- .../chatflows/Input Moderation.json | 441 ++++++++++++++++++ 5 files changed, 454 insertions(+), 13 deletions(-) create mode 100644 packages/server/marketplaces/chatflows/Input Moderation.json diff --git a/packages/components/nodes/chains/LLMChain/LLMChain.ts b/packages/components/nodes/chains/LLMChain/LLMChain.ts index 44520021..4d6eb551 100644 --- a/packages/components/nodes/chains/LLMChain/LLMChain.ts +++ b/packages/components/nodes/chains/LLMChain/LLMChain.ts @@ -37,14 +37,6 @@ class LLMChain_Chains implements INode { name: 'model', type: 'BaseLanguageModel' }, - { - label: 'Input Moderation', - description: 'Detect text that could generate harmful output and prevent it from being sent to the language model', - name: 'inputModeration', - type: 'Moderation', - optional: true, - list: true - }, { label: 'Prompt', name: 'prompt', @@ -56,6 +48,14 @@ class LLMChain_Chains implements INode { type: 'BaseLLMOutputParser', optional: true }, + { + label: 'Input Moderation', + description: 'Detect text that could generate harmful output and prevent it from being sent to the language model', + name: 'inputModeration', + type: 'Moderation', + optional: true, + list: true + }, { label: 'Chain Name', name: 'chainName', @@ -166,6 +166,7 @@ const runPrediction = async ( // Use the output of the moderation chain as input for the LLM chain input = await checkInputs(moderations, chain.llm, input) } catch (e) { + await new Promise((resolve) => setTimeout(resolve, 500)) streamResponse(isStreaming, e.message, socketIO, socketIOClientId) return formatResponse(e.message) } diff --git a/packages/components/nodes/responsibleAI/InputModeration/OpenAIModeration.ts b/packages/components/nodes/responsibleAI/InputModeration/OpenAIModeration.ts index bf2b2268..2edf9ad0 100644 --- a/packages/components/nodes/responsibleAI/InputModeration/OpenAIModeration.ts +++ b/packages/components/nodes/responsibleAI/InputModeration/OpenAIModeration.ts @@ -15,7 +15,7 @@ class OpenAIModeration implements INode { inputs: INodeParams[] constructor() { - this.label = 'Moderation - Open AI' + this.label = 'OpenAI Moderation' this.name = 'inputModerationOpenAI' this.version = 1.0 this.type = 'Moderation' diff --git a/packages/components/nodes/responsibleAI/InputModeration/SimplePromptModeration.ts b/packages/components/nodes/responsibleAI/InputModeration/SimplePromptModeration.ts index 5fde5ca3..88bf2c42 100644 --- a/packages/components/nodes/responsibleAI/InputModeration/SimplePromptModeration.ts +++ b/packages/components/nodes/responsibleAI/InputModeration/SimplePromptModeration.ts @@ -15,13 +15,13 @@ class SimplePromptModeration implements INode { inputs: INodeParams[] constructor() { - this.label = 'Moderation - Simple Prompt' + this.label = 'Simple Prompt Moderation' this.name = 'inputModerationSimple' this.version = 1.0 this.type = 'Moderation' this.icon = 'simple_moderation.png' this.category = 'Responsible AI' - this.description = 'Detecting and mitigating prompt attacks' + this.description = 'Check whether input consists of any text from Deny list, and prevent being sent to LLM' this.baseClasses = [this.type, ...getBaseClasses(Moderation)] this.inputs = [ { @@ -44,7 +44,6 @@ class SimplePromptModeration implements INode { ] } - // eslint-disable-next-line unused-imports/no-unused-vars async init(nodeData: INodeData): Promise { const denyList = nodeData.inputs?.denyList as string const moderationErrorMessage = nodeData.inputs?.moderationErrorMessage as string diff --git a/packages/components/nodes/responsibleAI/InputModeration/SimplePromptModerationRunner.ts b/packages/components/nodes/responsibleAI/InputModeration/SimplePromptModerationRunner.ts index aaded907..fb71166c 100644 --- a/packages/components/nodes/responsibleAI/InputModeration/SimplePromptModerationRunner.ts +++ b/packages/components/nodes/responsibleAI/InputModeration/SimplePromptModerationRunner.ts @@ -13,7 +13,7 @@ export class SimplePromptModerationRunner implements Moderation { this.moderationErrorMessage = moderationErrorMessage } - async checkForViolations(llm: BaseLanguageModel, input: string): Promise { + async checkForViolations(_: BaseLanguageModel, input: string): Promise { this.denyList.split('\n').forEach((denyListItem) => { if (denyListItem && denyListItem !== '' && input.includes(denyListItem)) { throw Error(this.moderationErrorMessage) diff --git a/packages/server/marketplaces/chatflows/Input Moderation.json b/packages/server/marketplaces/chatflows/Input Moderation.json new file mode 100644 index 00000000..254df4ac --- /dev/null +++ b/packages/server/marketplaces/chatflows/Input Moderation.json @@ -0,0 +1,441 @@ +{ + "description": "Detect text that could generate harmful output and prevent it from being sent to the language model", + "badge": "NEW", + "nodes": [ + { + "width": 300, + "height": 356, + "id": "inputModerationOpenAI_0", + "position": { + "x": 334.36040624369247, + "y": 467.88081727992824 + }, + "type": "customNode", + "data": { + "id": "inputModerationOpenAI_0", + "label": "OpenAI Moderation", + "version": 1, + "name": "inputModerationOpenAI", + "type": "Moderation", + "baseClasses": ["Moderation", "ResponsibleAI"], + "category": "Responsible AI", + "description": "Check whether content complies with OpenAI usage policies.", + "inputParams": [ + { + "label": "Error Message", + "name": "moderationErrorMessage", + "type": "string", + "rows": 2, + "default": "Cannot Process! Input violates OpenAI's content moderation policies.", + "optional": true, + "id": "inputModerationOpenAI_0-input-moderationErrorMessage-string" + } + ], + "inputAnchors": [], + "inputs": { + "moderationErrorMessage": "Cannot Process! Input violates OpenAI's content moderation policies." + }, + "outputAnchors": [ + { + "id": "inputModerationOpenAI_0-output-inputModerationOpenAI-Moderation|ResponsibleAI", + "name": "inputModerationOpenAI", + "label": "Moderation", + "type": "Moderation | ResponsibleAI" + } + ], + "outputs": {}, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 334.36040624369247, + "y": 467.88081727992824 + }, + "dragging": false + }, + { + "width": 300, + "height": 507, + "id": "llmChain_0", + "position": { + "x": 859.216454729136, + "y": 154.86846618352752 + }, + "type": "customNode", + "data": { + "id": "llmChain_0", + "label": "LLM Chain", + "version": 3, + "name": "llmChain", + "type": "LLMChain", + "baseClasses": ["LLMChain", "BaseChain", "Runnable"], + "category": "Chains", + "description": "Chain to run queries against LLMs", + "inputParams": [ + { + "label": "Chain Name", + "name": "chainName", + "type": "string", + "placeholder": "Name Your Chain", + "optional": true, + "id": "llmChain_0-input-chainName-string" + } + ], + "inputAnchors": [ + { + "label": "Language Model", + "name": "model", + "type": "BaseLanguageModel", + "id": "llmChain_0-input-model-BaseLanguageModel" + }, + { + "label": "Prompt", + "name": "prompt", + "type": "BasePromptTemplate", + "id": "llmChain_0-input-prompt-BasePromptTemplate" + }, + { + "label": "Output Parser", + "name": "outputParser", + "type": "BaseLLMOutputParser", + "optional": true, + "id": "llmChain_0-input-outputParser-BaseLLMOutputParser" + }, + { + "label": "Input Moderation", + "description": "Detect text that could generate harmful output and prevent it from being sent to the language model", + "name": "inputModeration", + "type": "Moderation", + "optional": true, + "list": true, + "id": "llmChain_0-input-inputModeration-Moderation" + } + ], + "inputs": { + "model": "{{chatOpenAI_0.data.instance}}", + "prompt": "{{promptTemplate_0.data.instance}}", + "outputParser": "", + "inputModeration": ["{{inputModerationOpenAI_0.data.instance}}"], + "chainName": "" + }, + "outputAnchors": [ + { + "name": "output", + "label": "Output", + "type": "options", + "options": [ + { + "id": "llmChain_0-output-llmChain-LLMChain|BaseChain|Runnable", + "name": "llmChain", + "label": "LLM Chain", + "type": "LLMChain | BaseChain | Runnable" + }, + { + "id": "llmChain_0-output-outputPrediction-string|json", + "name": "outputPrediction", + "label": "Output Prediction", + "type": "string | json" + } + ], + "default": "llmChain" + } + ], + "outputs": { + "output": "llmChain" + }, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 859.216454729136, + "y": 154.86846618352752 + }, + "dragging": false + }, + { + "width": 300, + "height": 574, + "id": "chatOpenAI_0", + "position": { + "x": 424.69244822381864, + "y": -271.138349609141 + }, + "type": "customNode", + "data": { + "id": "chatOpenAI_0", + "label": "ChatOpenAI", + "version": 2, + "name": "chatOpenAI", + "type": "ChatOpenAI", + "baseClasses": ["ChatOpenAI", "BaseChatModel", "BaseLanguageModel", "Runnable"], + "category": "Chat Models", + "description": "Wrapper around OpenAI large language models that use the Chat endpoint", + "inputParams": [ + { + "label": "Connect Credential", + "name": "credential", + "type": "credential", + "credentialNames": ["openAIApi"], + "id": "chatOpenAI_0-input-credential-credential" + }, + { + "label": "Model Name", + "name": "modelName", + "type": "options", + "options": [ + { + "label": "gpt-4", + "name": "gpt-4" + }, + { + "label": "gpt-4-1106-preview", + "name": "gpt-4-1106-preview" + }, + { + "label": "gpt-4-vision-preview", + "name": "gpt-4-vision-preview" + }, + { + "label": "gpt-4-0613", + "name": "gpt-4-0613" + }, + { + "label": "gpt-4-32k", + "name": "gpt-4-32k" + }, + { + "label": "gpt-4-32k-0613", + "name": "gpt-4-32k-0613" + }, + { + "label": "gpt-3.5-turbo", + "name": "gpt-3.5-turbo" + }, + { + "label": "gpt-3.5-turbo-1106", + "name": "gpt-3.5-turbo-1106" + }, + { + "label": "gpt-3.5-turbo-0613", + "name": "gpt-3.5-turbo-0613" + }, + { + "label": "gpt-3.5-turbo-16k", + "name": "gpt-3.5-turbo-16k" + }, + { + "label": "gpt-3.5-turbo-16k-0613", + "name": "gpt-3.5-turbo-16k-0613" + } + ], + "default": "gpt-3.5-turbo", + "optional": true, + "id": "chatOpenAI_0-input-modelName-options" + }, + { + "label": "Temperature", + "name": "temperature", + "type": "number", + "step": 0.1, + "default": 0.9, + "optional": true, + "id": "chatOpenAI_0-input-temperature-number" + }, + { + "label": "Max Tokens", + "name": "maxTokens", + "type": "number", + "step": 1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-maxTokens-number" + }, + { + "label": "Top Probability", + "name": "topP", + "type": "number", + "step": 0.1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-topP-number" + }, + { + "label": "Frequency Penalty", + "name": "frequencyPenalty", + "type": "number", + "step": 0.1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-frequencyPenalty-number" + }, + { + "label": "Presence Penalty", + "name": "presencePenalty", + "type": "number", + "step": 0.1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-presencePenalty-number" + }, + { + "label": "Timeout", + "name": "timeout", + "type": "number", + "step": 1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-timeout-number" + }, + { + "label": "BasePath", + "name": "basepath", + "type": "string", + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-basepath-string" + }, + { + "label": "BaseOptions", + "name": "baseOptions", + "type": "json", + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-baseOptions-json" + } + ], + "inputAnchors": [ + { + "label": "Cache", + "name": "cache", + "type": "BaseCache", + "optional": true, + "id": "chatOpenAI_0-input-cache-BaseCache" + } + ], + "inputs": { + "cache": "", + "modelName": "gpt-3.5-turbo", + "temperature": 0.9, + "maxTokens": "", + "topP": "", + "frequencyPenalty": "", + "presencePenalty": "", + "timeout": "", + "basepath": "", + "baseOptions": "" + }, + "outputAnchors": [ + { + "id": "chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable", + "name": "chatOpenAI", + "label": "ChatOpenAI", + "type": "ChatOpenAI | BaseChatModel | BaseLanguageModel | Runnable" + } + ], + "outputs": {}, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 424.69244822381864, + "y": -271.138349609141 + }, + "dragging": false + }, + { + "width": 300, + "height": 475, + "id": "promptTemplate_0", + "position": { + "x": -17.005933033720936, + "y": -20.829788775850602 + }, + "type": "customNode", + "data": { + "id": "promptTemplate_0", + "label": "Prompt Template", + "version": 1, + "name": "promptTemplate", + "type": "PromptTemplate", + "baseClasses": ["PromptTemplate", "BaseStringPromptTemplate", "BasePromptTemplate", "Runnable"], + "category": "Prompts", + "description": "Schema to represent a basic prompt for an LLM", + "inputParams": [ + { + "label": "Template", + "name": "template", + "type": "string", + "rows": 4, + "placeholder": "What is a good name for a company that makes {product}?", + "id": "promptTemplate_0-input-template-string" + }, + { + "label": "Format Prompt Values", + "name": "promptValues", + "type": "json", + "optional": true, + "acceptVariable": true, + "list": true, + "id": "promptTemplate_0-input-promptValues-json" + } + ], + "inputAnchors": [], + "inputs": { + "template": "Answer user question:\n{text}", + "promptValues": "{\"history\":\"{{chat_history}}\"}" + }, + "outputAnchors": [ + { + "id": "promptTemplate_0-output-promptTemplate-PromptTemplate|BaseStringPromptTemplate|BasePromptTemplate|Runnable", + "name": "promptTemplate", + "label": "PromptTemplate", + "type": "PromptTemplate | BaseStringPromptTemplate | BasePromptTemplate | Runnable" + } + ], + "outputs": {}, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": -17.005933033720936, + "y": -20.829788775850602 + }, + "dragging": false + } + ], + "edges": [ + { + "source": "inputModerationOpenAI_0", + "sourceHandle": "inputModerationOpenAI_0-output-inputModerationOpenAI-Moderation|ResponsibleAI", + "target": "llmChain_0", + "targetHandle": "llmChain_0-input-inputModeration-Moderation", + "type": "buttonedge", + "id": "inputModerationOpenAI_0-inputModerationOpenAI_0-output-inputModerationOpenAI-Moderation|ResponsibleAI-llmChain_0-llmChain_0-input-inputModeration-Moderation", + "data": { + "label": "" + } + }, + { + "source": "chatOpenAI_0", + "sourceHandle": "chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable", + "target": "llmChain_0", + "targetHandle": "llmChain_0-input-model-BaseLanguageModel", + "type": "buttonedge", + "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable-llmChain_0-llmChain_0-input-model-BaseLanguageModel", + "data": { + "label": "" + } + }, + { + "source": "promptTemplate_0", + "sourceHandle": "promptTemplate_0-output-promptTemplate-PromptTemplate|BaseStringPromptTemplate|BasePromptTemplate|Runnable", + "target": "llmChain_0", + "targetHandle": "llmChain_0-input-prompt-BasePromptTemplate", + "type": "buttonedge", + "id": "promptTemplate_0-promptTemplate_0-output-promptTemplate-PromptTemplate|BaseStringPromptTemplate|BasePromptTemplate|Runnable-llmChain_0-llmChain_0-input-prompt-BasePromptTemplate", + "data": { + "label": "" + } + } + ] +} From 7d13b6323fe426ccf209e6c1f11646c363bc98d1 Mon Sep 17 00:00:00 2001 From: Henry Date: Wed, 22 Nov 2023 19:48:01 +0000 Subject: [PATCH 48/64] fix namings, update description, show badge and node info --- .../retrievers/HydeRetriever/HydeRetriever.ts | 2 +- .../nodes/vectorstores/Chroma/Chroma.ts | 2 +- .../Elasticsearch/Elasticsearch.ts | 3 +- .../nodes/vectorstores/Faiss/Faiss.ts | 2 +- .../nodes/vectorstores/Milvus/Milvus.ts | 4 +- .../vectorstores/OpenSearch/OpenSearch.ts | 2 +- .../nodes/vectorstores/Pinecone/Pinecone.ts | 2 +- .../nodes/vectorstores/Postgres/Postgres.ts | 2 +- .../nodes/vectorstores/Qdrant/Qdrant.ts | 3 +- .../nodes/vectorstores/Redis/Redis.ts | 3 +- .../vectorstores/Singlestore/Singlestore.ts | 7 +- .../nodes/vectorstores/Supabase/Supabase.ts | 6 +- .../nodes/vectorstores/Vectara/Vectara.ts | 6 +- .../nodes/vectorstores/Weaviate/Weaviate.ts | 3 +- .../components/nodes/vectorstores/Zep/Zep.ts | 3 +- .../marketplaces/chatflows/AutoGPT.json | 2 +- .../marketplaces/chatflows/BabyAGI.json | 2 +- .../Conversational Retrieval Agent.json | 2 +- .../Conversational Retrieval QA Chain.json | 2 +- .../marketplaces/chatflows/Local QnA.json | 2 +- .../chatflows/Long Term Memory.json | 2 +- .../chatflows/Metadata Filter.json | 2 +- .../chatflows/Multi Retrieval QA Chain.json | 6 +- .../chatflows/Multiple VectorDB.json | 4 +- .../Prompt Chaining with VectorStore.json | 2 +- .../chatflows/Vectara LLM Chain Upload.json | 2 +- .../marketplaces/chatflows/WebPage QnA.json | 2 +- .../src/ui-component/dialog/NodeInfoDialog.js | 26 ++ packages/ui/src/views/canvas/AddNodes.js | 225 +++++++++++------- packages/ui/src/views/canvas/CanvasNode.js | 6 +- 30 files changed, 214 insertions(+), 123 deletions(-) diff --git a/packages/components/nodes/retrievers/HydeRetriever/HydeRetriever.ts b/packages/components/nodes/retrievers/HydeRetriever/HydeRetriever.ts index 2baf677e..9ec7ada0 100644 --- a/packages/components/nodes/retrievers/HydeRetriever/HydeRetriever.ts +++ b/packages/components/nodes/retrievers/HydeRetriever/HydeRetriever.ts @@ -104,7 +104,7 @@ class HydeRetriever_Retrievers implements INode { const promptKey = nodeData.inputs?.promptKey as PromptKey const customPrompt = nodeData.inputs?.customPrompt as string const topK = nodeData.inputs?.topK as string - const k = topK ? parseInt(topK, 10) : 4 + const k = topK ? parseFloat(topK) : 4 const obj: HydeRetrieverOptions = { llm, diff --git a/packages/components/nodes/vectorstores/Chroma/Chroma.ts b/packages/components/nodes/vectorstores/Chroma/Chroma.ts index eef2db79..6e1cfa67 100644 --- a/packages/components/nodes/vectorstores/Chroma/Chroma.ts +++ b/packages/components/nodes/vectorstores/Chroma/Chroma.ts @@ -27,7 +27,7 @@ class Chroma_VectorStores implements INode { this.type = 'Chroma' this.icon = 'chroma.svg' this.category = 'Vector Stores' - this.description = 'Upsert or Load data to Chroma Vector Database' + this.description = 'Upsert embedded data and perform similarity search upon query using Chroma, an open-source embedding database' this.baseClasses = [this.type, 'VectorStoreRetriever', 'BaseRetriever'] this.badge = 'NEW' this.credential = { diff --git a/packages/components/nodes/vectorstores/Elasticsearch/Elasticsearch.ts b/packages/components/nodes/vectorstores/Elasticsearch/Elasticsearch.ts index e7915b3e..5f3cf206 100644 --- a/packages/components/nodes/vectorstores/Elasticsearch/Elasticsearch.ts +++ b/packages/components/nodes/vectorstores/Elasticsearch/Elasticsearch.ts @@ -24,7 +24,8 @@ class Elasticsearch_VectorStores implements INode { this.label = 'Elasticsearch' this.name = 'elasticsearch' this.version = 1.0 - this.description = 'Upsert or Load data to Elasticsearch Vector Database' + this.description = + 'Upsert embedded data and perform similarity search upon query using Elasticsearch, a distributed search and analytics engine' this.type = 'Elasticsearch' this.icon = 'elasticsearch.png' this.category = 'Vector Stores' diff --git a/packages/components/nodes/vectorstores/Faiss/Faiss.ts b/packages/components/nodes/vectorstores/Faiss/Faiss.ts index e2c1512e..4120a57e 100644 --- a/packages/components/nodes/vectorstores/Faiss/Faiss.ts +++ b/packages/components/nodes/vectorstores/Faiss/Faiss.ts @@ -25,7 +25,7 @@ class Faiss_VectorStores implements INode { this.type = 'Faiss' this.icon = 'faiss.svg' this.category = 'Vector Stores' - this.description = 'Upsert or Load data to Faiss Vector Store' + this.description = 'Upsert embedded data and perform similarity search upon query using Faiss library from Meta' this.baseClasses = [this.type, 'VectorStoreRetriever', 'BaseRetriever'] this.badge = 'NEW' this.inputs = [ diff --git a/packages/components/nodes/vectorstores/Milvus/Milvus.ts b/packages/components/nodes/vectorstores/Milvus/Milvus.ts index b937be1e..090f35f7 100644 --- a/packages/components/nodes/vectorstores/Milvus/Milvus.ts +++ b/packages/components/nodes/vectorstores/Milvus/Milvus.ts @@ -31,7 +31,7 @@ class Milvus_VectorStores implements INode { this.type = 'Milvus' this.icon = 'milvus.svg' this.category = 'Vector Stores' - this.description = 'Upsert or Load data to Milvus Vector Database' + this.description = `Upsert embedded data and perform similarity search upon query using Milvus, world's most advanced open-source vector database` this.baseClasses = [this.type, 'VectorStoreRetriever', 'BaseRetriever'] this.badge = 'NEW' this.credential = { @@ -159,7 +159,7 @@ class Milvus_VectorStores implements INode { const output = nodeData.outputs?.output as string // format data - const k = topK ? parseInt(topK, 10) : 4 + const k = topK ? parseFloat(topK) : 4 // credential const credentialData = await getCredentialData(nodeData.credential ?? '', options) diff --git a/packages/components/nodes/vectorstores/OpenSearch/OpenSearch.ts b/packages/components/nodes/vectorstores/OpenSearch/OpenSearch.ts index 66f04143..e3e18ce1 100644 --- a/packages/components/nodes/vectorstores/OpenSearch/OpenSearch.ts +++ b/packages/components/nodes/vectorstores/OpenSearch/OpenSearch.ts @@ -26,7 +26,7 @@ class OpenSearch_VectorStores implements INode { this.type = 'OpenSearch' this.icon = 'opensearch.png' this.category = 'Vector Stores' - this.description = 'Upsert or Load data to OpenSearch Vector Database' + this.description = `Upsert embedded data and perform similarity search upon query using OpenSearch, an open-source, all-in-one vector database` this.baseClasses = [this.type, 'VectorStoreRetriever', 'BaseRetriever'] this.badge = 'NEW' this.inputs = [ diff --git a/packages/components/nodes/vectorstores/Pinecone/Pinecone.ts b/packages/components/nodes/vectorstores/Pinecone/Pinecone.ts index e4ef9fb7..4ece4720 100644 --- a/packages/components/nodes/vectorstores/Pinecone/Pinecone.ts +++ b/packages/components/nodes/vectorstores/Pinecone/Pinecone.ts @@ -27,7 +27,7 @@ class Pinecone_VectorStores implements INode { this.type = 'Pinecone' this.icon = 'pinecone.png' this.category = 'Vector Stores' - this.description = 'Upsert or Load data to Pinecone Vector Database' + this.description = `Upsert embedded data and perform similarity search upon query using Pinecone, a leading fully managed hosted vector database` this.baseClasses = [this.type, 'VectorStoreRetriever', 'BaseRetriever'] this.badge = 'NEW' this.credential = { diff --git a/packages/components/nodes/vectorstores/Postgres/Postgres.ts b/packages/components/nodes/vectorstores/Postgres/Postgres.ts index 0609d0b5..ac4b80c3 100644 --- a/packages/components/nodes/vectorstores/Postgres/Postgres.ts +++ b/packages/components/nodes/vectorstores/Postgres/Postgres.ts @@ -28,7 +28,7 @@ class Postgres_VectorStores implements INode { this.type = 'Postgres' this.icon = 'postgres.svg' this.category = 'Vector Stores' - this.description = 'Upsert or Load data to Postgres using pgvector' + this.description = 'Upsert embedded data and perform similarity search upon query using pgvector on Postgres' this.baseClasses = [this.type, 'VectorStoreRetriever', 'BaseRetriever'] this.badge = 'NEW' this.credential = { diff --git a/packages/components/nodes/vectorstores/Qdrant/Qdrant.ts b/packages/components/nodes/vectorstores/Qdrant/Qdrant.ts index 6de2c186..6413f8bf 100644 --- a/packages/components/nodes/vectorstores/Qdrant/Qdrant.ts +++ b/packages/components/nodes/vectorstores/Qdrant/Qdrant.ts @@ -30,7 +30,8 @@ class Qdrant_VectorStores implements INode { this.type = 'Qdrant' this.icon = 'qdrant.png' this.category = 'Vector Stores' - this.description = 'Upsert or Load data to Qdrant Vector Database' + this.description = + 'Upsert embedded data and perform similarity search upon query using Qdrant, a scalable open source vector database written in Rust' this.baseClasses = [this.type, 'VectorStoreRetriever', 'BaseRetriever'] this.badge = 'NEW' this.credential = { diff --git a/packages/components/nodes/vectorstores/Redis/Redis.ts b/packages/components/nodes/vectorstores/Redis/Redis.ts index d857e225..dc993b86 100644 --- a/packages/components/nodes/vectorstores/Redis/Redis.ts +++ b/packages/components/nodes/vectorstores/Redis/Redis.ts @@ -25,7 +25,8 @@ class Redis_VectorStores implements INode { this.label = 'Redis' this.name = 'redis' this.version = 1.0 - this.description = 'Upsert or Load data to Redis' + this.description = + 'Upsert embedded data and perform similarity search upon query using Redis, an open source, in-memory data structure store' this.type = 'Redis' this.icon = 'redis.svg' this.category = 'Vector Stores' diff --git a/packages/components/nodes/vectorstores/Singlestore/Singlestore.ts b/packages/components/nodes/vectorstores/Singlestore/Singlestore.ts index 3597f41e..d16252ac 100644 --- a/packages/components/nodes/vectorstores/Singlestore/Singlestore.ts +++ b/packages/components/nodes/vectorstores/Singlestore/Singlestore.ts @@ -26,7 +26,8 @@ class SingleStore_VectorStores implements INode { this.type = 'SingleStore' this.icon = 'singlestore.svg' this.category = 'Vector Stores' - this.description = 'Upsert or Load data to SingleStore Vector Database' + this.description = + 'Upsert embedded data and perform similarity search upon query using SingleStore, a fast and distributed cloud relational database' this.baseClasses = [this.type, 'VectorStoreRetriever', 'BaseRetriever'] this.badge = 'NEW' this.credential = { @@ -180,9 +181,7 @@ class SingleStore_VectorStores implements INode { const topK = nodeData.inputs?.topK as string const k = topK ? parseFloat(topK) : 4 - let vectorStore: SingleStoreVectorStore - - vectorStore = new SingleStoreVectorStore(embeddings, singleStoreConnectionConfig) + const vectorStore = new SingleStoreVectorStore(embeddings, singleStoreConnectionConfig) if (output === 'retriever') { const retriever = vectorStore.asRetriever(k) diff --git a/packages/components/nodes/vectorstores/Supabase/Supabase.ts b/packages/components/nodes/vectorstores/Supabase/Supabase.ts index a7de3211..13840ab7 100644 --- a/packages/components/nodes/vectorstores/Supabase/Supabase.ts +++ b/packages/components/nodes/vectorstores/Supabase/Supabase.ts @@ -27,7 +27,7 @@ class Supabase_VectorStores implements INode { this.type = 'Supabase' this.icon = 'supabase.svg' this.category = 'Vector Stores' - this.description = 'Upsert or Load data to Supabase using pgvector' + this.description = 'Upsert embedded data and perform similarity search upon query using Supabase via pgvector extension' this.baseClasses = [this.type, 'VectorStoreRetriever', 'BaseRetriever'] this.badge = 'NEW' this.credential = { @@ -112,7 +112,9 @@ class Supabase_VectorStores implements INode { const flattenDocs = docs && docs.length ? flatten(docs) : [] const finalDocs = [] for (let i = 0; i < flattenDocs.length; i += 1) { - finalDocs.push(new Document(flattenDocs[i])) + if (flattenDocs[i] && flattenDocs[i].pageContent) { + finalDocs.push(new Document(flattenDocs[i])) + } } try { diff --git a/packages/components/nodes/vectorstores/Vectara/Vectara.ts b/packages/components/nodes/vectorstores/Vectara/Vectara.ts index f12dc4a2..7460c586 100644 --- a/packages/components/nodes/vectorstores/Vectara/Vectara.ts +++ b/packages/components/nodes/vectorstores/Vectara/Vectara.ts @@ -26,7 +26,7 @@ class Vectara_VectorStores implements INode { this.type = 'Vectara' this.icon = 'vectara.png' this.category = 'Vector Stores' - this.description = 'Upsert or Load data to Vectara Vector Database' + this.description = 'Upsert embedded data and perform similarity search upon query using Vectara, a LLM-powered search-as-a-service' this.baseClasses = [this.type, 'VectorStoreRetriever', 'BaseRetriever'] this.badge = 'NEW' this.credential = { @@ -65,6 +65,7 @@ class Vectara_VectorStores implements INode { name: 'sentencesBefore', description: 'Number of sentences to fetch before the matched sentence. Defaults to 2.', type: 'number', + default: 2, additionalParams: true, optional: true }, @@ -73,6 +74,7 @@ class Vectara_VectorStores implements INode { name: 'sentencesAfter', description: 'Number of sentences to fetch after the matched sentence. Defaults to 2.', type: 'number', + default: 2, additionalParams: true, optional: true }, @@ -189,7 +191,7 @@ class Vectara_VectorStores implements INode { const lambda = nodeData.inputs?.lambda as number const output = nodeData.outputs?.output as string const topK = nodeData.inputs?.topK as string - const k = topK ? parseInt(topK, 10) : 4 + const k = topK ? parseFloat(topK) : 4 const vectaraArgs: VectaraLibArgs = { apiKey: apiKey, diff --git a/packages/components/nodes/vectorstores/Weaviate/Weaviate.ts b/packages/components/nodes/vectorstores/Weaviate/Weaviate.ts index e54d122b..5c31c737 100644 --- a/packages/components/nodes/vectorstores/Weaviate/Weaviate.ts +++ b/packages/components/nodes/vectorstores/Weaviate/Weaviate.ts @@ -27,7 +27,8 @@ class Weaviate_VectorStores implements INode { this.type = 'Weaviate' this.icon = 'weaviate.png' this.category = 'Vector Stores' - this.description = 'Upsert or Load data to Weaviate Vector Database' + this.description = + 'Upsert embedded data and perform similarity search upon query using Weaviate, a scalable open-source vector database' this.baseClasses = [this.type, 'VectorStoreRetriever', 'BaseRetriever'] this.badge = 'NEW' this.credential = { diff --git a/packages/components/nodes/vectorstores/Zep/Zep.ts b/packages/components/nodes/vectorstores/Zep/Zep.ts index ce863a9e..21c885b4 100644 --- a/packages/components/nodes/vectorstores/Zep/Zep.ts +++ b/packages/components/nodes/vectorstores/Zep/Zep.ts @@ -27,7 +27,8 @@ class Zep_VectorStores implements INode { this.type = 'Zep' this.icon = 'zep.png' this.category = 'Vector Stores' - this.description = 'Upsert or Load data to Zep Vector Database' + this.description = + 'Upsert embedded data and perform similarity search upon query using Zep, a fast and scalable building block for LLM apps' this.baseClasses = [this.type, 'VectorStoreRetriever', 'BaseRetriever'] this.badge = 'NEW' this.credential = { diff --git a/packages/server/marketplaces/chatflows/AutoGPT.json b/packages/server/marketplaces/chatflows/AutoGPT.json index 5f388da6..150fe17e 100644 --- a/packages/server/marketplaces/chatflows/AutoGPT.json +++ b/packages/server/marketplaces/chatflows/AutoGPT.json @@ -511,7 +511,7 @@ "type": "Pinecone", "baseClasses": ["Pinecone", "VectorStoreRetriever", "BaseRetriever"], "category": "Vector Stores", - "description": "Upsert or Load data to Pinecone Vector Database", + "description": "Upsert embedded data and perform similarity search upon query using Pinecone, a leading fully managed hosted vector database", "inputParams": [ { "label": "Connect Credential", diff --git a/packages/server/marketplaces/chatflows/BabyAGI.json b/packages/server/marketplaces/chatflows/BabyAGI.json index 211e9e42..ab387205 100644 --- a/packages/server/marketplaces/chatflows/BabyAGI.json +++ b/packages/server/marketplaces/chatflows/BabyAGI.json @@ -166,7 +166,7 @@ "type": "Pinecone", "baseClasses": ["Pinecone", "VectorStoreRetriever", "BaseRetriever"], "category": "Vector Stores", - "description": "Upsert or Load data to Pinecone Vector Database", + "description": "Upsert embedded data and perform similarity search upon query using Pinecone, a leading fully managed hosted vector database", "inputParams": [ { "label": "Connect Credential", diff --git a/packages/server/marketplaces/chatflows/Conversational Retrieval Agent.json b/packages/server/marketplaces/chatflows/Conversational Retrieval Agent.json index dd4cf3b1..aafc8e8e 100644 --- a/packages/server/marketplaces/chatflows/Conversational Retrieval Agent.json +++ b/packages/server/marketplaces/chatflows/Conversational Retrieval Agent.json @@ -301,7 +301,7 @@ "type": "Pinecone", "baseClasses": ["Pinecone", "VectorStoreRetriever", "BaseRetriever"], "category": "Vector Stores", - "description": "Upsert or Load data to Pinecone Vector Database", + "description": "Upsert embedded data and perform similarity search upon query using Pinecone, a leading fully managed hosted vector database", "inputParams": [ { "label": "Connect Credential", diff --git a/packages/server/marketplaces/chatflows/Conversational Retrieval QA Chain.json b/packages/server/marketplaces/chatflows/Conversational Retrieval QA Chain.json index e775846c..5c55d833 100644 --- a/packages/server/marketplaces/chatflows/Conversational Retrieval QA Chain.json +++ b/packages/server/marketplaces/chatflows/Conversational Retrieval QA Chain.json @@ -553,7 +553,7 @@ "type": "Pinecone", "baseClasses": ["Pinecone", "VectorStoreRetriever", "BaseRetriever"], "category": "Vector Stores", - "description": "Upsert or Load data to Pinecone Vector Database", + "description": "Upsert embedded data and perform similarity search upon query using Pinecone, a leading fully managed hosted vector database", "inputParams": [ { "label": "Connect Credential", diff --git a/packages/server/marketplaces/chatflows/Local QnA.json b/packages/server/marketplaces/chatflows/Local QnA.json index fcf8593c..e24ad7ca 100644 --- a/packages/server/marketplaces/chatflows/Local QnA.json +++ b/packages/server/marketplaces/chatflows/Local QnA.json @@ -555,7 +555,7 @@ "type": "Faiss", "baseClasses": ["Faiss", "VectorStoreRetriever", "BaseRetriever"], "category": "Vector Stores", - "description": "Upsert or Load data to Faiss Vector Store", + "description": "Upsert embedded data and perform similarity search upon query using Faiss library from Meta", "inputParams": [ { "label": "Base Path to load", diff --git a/packages/server/marketplaces/chatflows/Long Term Memory.json b/packages/server/marketplaces/chatflows/Long Term Memory.json index f5ff2dca..c508b480 100644 --- a/packages/server/marketplaces/chatflows/Long Term Memory.json +++ b/packages/server/marketplaces/chatflows/Long Term Memory.json @@ -351,7 +351,7 @@ "type": "Qdrant", "baseClasses": ["Qdrant", "VectorStoreRetriever", "BaseRetriever"], "category": "Vector Stores", - "description": "Upsert or Load data to Qdrant Vector Database", + "description": "Upsert embedded data and perform similarity search upon query using Qdrant, a scalable open source vector database written in Rust", "inputParams": [ { "label": "Connect Credential", diff --git a/packages/server/marketplaces/chatflows/Metadata Filter.json b/packages/server/marketplaces/chatflows/Metadata Filter.json index f594a2b6..9865ae70 100644 --- a/packages/server/marketplaces/chatflows/Metadata Filter.json +++ b/packages/server/marketplaces/chatflows/Metadata Filter.json @@ -634,7 +634,7 @@ "type": "Pinecone", "baseClasses": ["Pinecone", "VectorStoreRetriever", "BaseRetriever"], "category": "Vector Stores", - "description": "Upsert or Load data to Pinecone Vector Database", + "description": "Upsert embedded data and perform similarity search upon query using Pinecone, a leading fully managed hosted vector database", "inputParams": [ { "label": "Connect Credential", diff --git a/packages/server/marketplaces/chatflows/Multi Retrieval QA Chain.json b/packages/server/marketplaces/chatflows/Multi Retrieval QA Chain.json index 9032122b..5388d965 100644 --- a/packages/server/marketplaces/chatflows/Multi Retrieval QA Chain.json +++ b/packages/server/marketplaces/chatflows/Multi Retrieval QA Chain.json @@ -560,7 +560,7 @@ "type": "Pinecone", "baseClasses": ["Pinecone", "VectorStoreRetriever", "BaseRetriever"], "category": "Vector Stores", - "description": "Upsert or Load data to Pinecone Vector Database", + "description": "Upsert embedded data and perform similarity search upon query using Pinecone, a leading fully managed hosted vector database", "inputParams": [ { "label": "Connect Credential", @@ -678,7 +678,7 @@ "type": "Chroma", "baseClasses": ["Chroma", "VectorStoreRetriever", "BaseRetriever"], "category": "Vector Stores", - "description": "Upsert or Load data to Chroma Vector Database", + "description": "Upsert embedded data and perform similarity search upon query using Chroma, an open-source embedding database", "inputParams": [ { "label": "Connect Credential", @@ -796,7 +796,7 @@ "type": "Supabase", "baseClasses": ["Supabase", "VectorStoreRetriever", "BaseRetriever"], "category": "Vector Stores", - "description": "Upsert or Load data to Supabase using pgvector", + "description": "Upsert embedded data and perform similarity search upon query using Supabase via pgvector extension", "inputParams": [ { "label": "Connect Credential", diff --git a/packages/server/marketplaces/chatflows/Multiple VectorDB.json b/packages/server/marketplaces/chatflows/Multiple VectorDB.json index 723b510e..e7718616 100644 --- a/packages/server/marketplaces/chatflows/Multiple VectorDB.json +++ b/packages/server/marketplaces/chatflows/Multiple VectorDB.json @@ -634,7 +634,7 @@ "type": "Redis", "baseClasses": ["Redis", "VectorStoreRetriever", "BaseRetriever"], "category": "Vector Stores", - "description": "Upsert or Load data to Redis", + "description": "Upsert embedded data and perform similarity search upon query using Redis, an open source, in-memory data structure store", "inputParams": [ { "label": "Connect Credential", @@ -776,7 +776,7 @@ "type": "Faiss", "baseClasses": ["Faiss", "VectorStoreRetriever", "BaseRetriever"], "category": "Vector Stores", - "description": "Upsert or Load data to Faiss Vector Store", + "description": "Upsert embedded data and perform similarity search upon query using Faiss library from Meta", "inputParams": [ { "label": "Base Path to load", diff --git a/packages/server/marketplaces/chatflows/Prompt Chaining with VectorStore.json b/packages/server/marketplaces/chatflows/Prompt Chaining with VectorStore.json index fca62a76..0ddec74f 100644 --- a/packages/server/marketplaces/chatflows/Prompt Chaining with VectorStore.json +++ b/packages/server/marketplaces/chatflows/Prompt Chaining with VectorStore.json @@ -792,7 +792,7 @@ "type": "SingleStore", "baseClasses": ["SingleStore", "VectorStoreRetriever", "BaseRetriever"], "category": "Vector Stores", - "description": "Upsert or Load data to SingleStore Vector Database", + "description": "Upsert embedded data and perform similarity search upon query using SingleStore, a fast and distributed cloud relational database", "inputParams": [ { "label": "Connect Credential", diff --git a/packages/server/marketplaces/chatflows/Vectara LLM Chain Upload.json b/packages/server/marketplaces/chatflows/Vectara LLM Chain Upload.json index 4f35bd4c..d9f9fb49 100644 --- a/packages/server/marketplaces/chatflows/Vectara LLM Chain Upload.json +++ b/packages/server/marketplaces/chatflows/Vectara LLM Chain Upload.json @@ -305,7 +305,7 @@ "type": "Vectara", "baseClasses": ["Vectara", "VectorStoreRetriever", "BaseRetriever"], "category": "Vector Stores", - "description": "Upsert or Load data to Vectara Vector Database", + "description": "Upsert embedded data and perform similarity search upon query using Vectara, a LLM-powered search-as-a-service", "inputParams": [ { "label": "Connect Credential", diff --git a/packages/server/marketplaces/chatflows/WebPage QnA.json b/packages/server/marketplaces/chatflows/WebPage QnA.json index da05721b..9b1119b9 100644 --- a/packages/server/marketplaces/chatflows/WebPage QnA.json +++ b/packages/server/marketplaces/chatflows/WebPage QnA.json @@ -654,7 +654,7 @@ "type": "Pinecone", "baseClasses": ["Pinecone", "VectorStoreRetriever", "BaseRetriever"], "category": "Vector Stores", - "description": "Upsert or Load data to Pinecone Vector Database", + "description": "Upsert embedded data and perform similarity search upon query using Pinecone, a leading fully managed hosted vector database", "inputParams": [ { "label": "Connect Credential", diff --git a/packages/ui/src/ui-component/dialog/NodeInfoDialog.js b/packages/ui/src/ui-component/dialog/NodeInfoDialog.js index 5abdb035..6f3bec5d 100644 --- a/packages/ui/src/ui-component/dialog/NodeInfoDialog.js +++ b/packages/ui/src/ui-component/dialog/NodeInfoDialog.js @@ -106,6 +106,32 @@ const NodeInfoDialog = ({ show, dialogProps, onCancel }) => { version {dialogProps.data.version} )} + {dialogProps.data.badge && ( +
+ + {dialogProps.data.badge} + +
+ )} diff --git a/packages/ui/src/views/canvas/AddNodes.js b/packages/ui/src/views/canvas/AddNodes.js index e0e639d1..44030d0e 100644 --- a/packages/ui/src/views/canvas/AddNodes.js +++ b/packages/ui/src/views/canvas/AddNodes.js @@ -57,6 +57,22 @@ const AddNodes = ({ nodesData, node }) => { const prevOpen = useRef(open) const ps = useRef() + // Temporary method to handle Deprecating Vector Store and New ones + const categorizeVectorStores = (nodes) => { + const obj = { ...nodes } + const vsNodes = obj['Vector Stores'] ?? [] + const deprecatingNodes = [] + const newNodes = [] + for (const vsNode of vsNodes) { + if (vsNode.badge === 'DEPRECATING') deprecatingNodes.push(vsNode) + else newNodes.push(vsNode) + } + delete obj['Vector Stores'] + obj['Vector Stores;DEPRECATING'] = deprecatingNodes + obj['Vector Stores;NEW'] = newNodes + setNodes(obj) + } + const scrollTop = () => { const curr = ps.current if (curr) { @@ -96,6 +112,7 @@ const AddNodes = ({ nodesData, node }) => { return r }, Object.create(null)) setNodes(result) + categorizeVectorStores(result) setCategoryExpanded(accordianCategories) } @@ -138,6 +155,8 @@ const AddNodes = ({ nodesData, node }) => { groupByCategory(nodesData) dispatch({ type: SET_COMPONENT_NODES, componentNodes: nodesData }) } + + // eslint-disable-next-line react-hooks/exhaustive-deps }, [nodesData, dispatch]) return ( @@ -250,99 +269,135 @@ const AddNodes = ({ nodesData, node }) => { > {Object.keys(nodes) .sort() - .map((category) => ( - - } - aria-controls={`nodes-accordian-${category}`} - id={`nodes-accordian-header-${category}`} + .map((category) => + category === 'Vector Stores' ? ( + <> + ) : ( + - {category} - - - {nodes[category].map((node, index) => ( -
onDragStart(event, node)} - draggable - > - } + aria-controls={`nodes-accordian-${category}`} + id={`nodes-accordian-header-${category}`} + > + {category.split(';').length > 1 ? ( +
- - -
- {node.name} -
-
- {category.split(';')[0]} +   + +
+ ) : ( + {category} + )} + + + {nodes[category].map((node, index) => ( +
onDragStart(event, node)} + draggable + > + + +
- {node.label} -   - {node.badge && ( - - )} + {node.name}
- } - secondary={node.description} - /> -
-
- {index === nodes[category].length - 1 ? null : } -
- ))} -
- - ))} + + + {node.label} +   + {node.badge && ( + + )} +
+ } + secondary={node.description} + /> + + + {index === nodes[category].length - 1 ? null : } + + ))} +
+
+ ) + )} diff --git a/packages/ui/src/views/canvas/CanvasNode.js b/packages/ui/src/views/canvas/CanvasNode.js index cabe2329..4455afc0 100644 --- a/packages/ui/src/views/canvas/CanvasNode.js +++ b/packages/ui/src/views/canvas/CanvasNode.js @@ -83,8 +83,10 @@ const CanvasNode = ({ data }) => { if (componentNode) { if (!data.version) { setWarningMessage(nodeVersionEmptyMessage(componentNode.version)) - } else { - if (componentNode.version > data.version) setWarningMessage(nodeOutdatedMessage(data.version, componentNode.version)) + } else if (data.version && componentNode.version > data.version) { + setWarningMessage(nodeOutdatedMessage(data.version, componentNode.version)) + } else if (componentNode.badge === 'DEPRECATING') { + setWarningMessage('This node will be deprecated in the next release. Change to a new node tagged with NEW') } } }, [canvas.componentNodes, data.name, data.version]) From d5af16fcfa38abe423daf9b6a76a4675dc3baba8 Mon Sep 17 00:00:00 2001 From: Henry Date: Wed, 22 Nov 2023 20:06:32 +0000 Subject: [PATCH 49/64] add mongodb atlas --- .../vectorstores/MongoDBAtlas/MongoDBAtlas.ts | 194 ++++++++++++++++++ .../MongoDBSearchBase.ts | 2 + .../MongoDB_Existing.ts | 0 .../MongoDB_Upsert.ts | 2 +- .../{MongoDB => MongoDBAtlas}/mongodb.png | Bin 5 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 packages/components/nodes/vectorstores/MongoDBAtlas/MongoDBAtlas.ts rename packages/components/nodes/vectorstores/{MongoDB => MongoDBAtlas}/MongoDBSearchBase.ts (98%) rename packages/components/nodes/vectorstores/{MongoDB => MongoDBAtlas}/MongoDB_Existing.ts (100%) rename packages/components/nodes/vectorstores/{MongoDB => MongoDBAtlas}/MongoDB_Upsert.ts (97%) rename packages/components/nodes/vectorstores/{MongoDB => MongoDBAtlas}/mongodb.png (100%) diff --git a/packages/components/nodes/vectorstores/MongoDBAtlas/MongoDBAtlas.ts b/packages/components/nodes/vectorstores/MongoDBAtlas/MongoDBAtlas.ts new file mode 100644 index 00000000..a0699f6b --- /dev/null +++ b/packages/components/nodes/vectorstores/MongoDBAtlas/MongoDBAtlas.ts @@ -0,0 +1,194 @@ +import { flatten } from 'lodash' +import { MongoClient } from 'mongodb' +import { MongoDBAtlasVectorSearch } from 'langchain/vectorstores/mongodb_atlas' +import { Embeddings } from 'langchain/embeddings/base' +import { Document } from 'langchain/document' +import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' +import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' + +class MongoDBAtlas_VectorStores implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + badge: string + baseClasses: string[] + inputs: INodeParams[] + credential: INodeParams + outputs: INodeOutputsValue[] + + constructor() { + this.label = 'MongoDB Atlas' + this.name = 'mongoDBAtlas' + this.version = 1.0 + this.description = `Upsert embedded data and perform similarity search upon query using MongoDB Atlas, a managed cloud mongodb database` + this.type = 'MongoDB Atlas' + this.icon = 'mongodb.png' + this.category = 'Vector Stores' + this.baseClasses = [this.type, 'VectorStoreRetriever', 'BaseRetriever'] + this.badge = 'NEW' + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['mongoDBUrlApi'] + } + this.inputs = [ + { + label: 'Document', + name: 'document', + type: 'Document', + list: true, + optional: true + }, + { + label: 'Embeddings', + name: 'embeddings', + type: 'Embeddings' + }, + { + label: 'Database', + name: 'databaseName', + placeholder: '', + type: 'string' + }, + { + label: 'Collection Name', + name: 'collectionName', + placeholder: '', + type: 'string' + }, + { + label: 'Index Name', + name: 'indexName', + placeholder: '', + type: 'string' + }, + { + label: 'Content Field', + name: 'textKey', + description: 'Name of the field (column) that contains the actual content', + type: 'string', + default: 'text', + additionalParams: true, + optional: true + }, + { + label: 'Embedded Field', + name: 'embeddingKey', + description: 'Name of the field (column) that contains the Embedding', + type: 'string', + default: 'embedding', + additionalParams: true, + optional: true + }, + { + label: 'Top K', + name: 'topK', + description: 'Number of top results to fetch. Default to 4', + placeholder: '4', + type: 'number', + additionalParams: true, + optional: true + } + ] + this.outputs = [ + { + label: 'MongoDB Retriever', + name: 'retriever', + baseClasses: this.baseClasses + }, + { + label: 'MongoDB Vector Store', + name: 'vectorStore', + baseClasses: [this.type, ...getBaseClasses(MongoDBAtlasVectorSearch)] + } + ] + } + + //@ts-ignore + vectorStoreMethods = { + 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 + const indexName = nodeData.inputs?.indexName as string + let textKey = nodeData.inputs?.textKey as string + let embeddingKey = nodeData.inputs?.embeddingKey as string + const embeddings = nodeData.inputs?.embeddings as Embeddings + + let mongoDBConnectUrl = getCredentialParam('mongoDBConnectUrl', credentialData, nodeData) + + const docs = nodeData.inputs?.document as Document[] + + const flattenDocs = docs && docs.length ? flatten(docs) : [] + const finalDocs = [] + for (let i = 0; i < flattenDocs.length; i += 1) { + if (flattenDocs[i] && flattenDocs[i].pageContent) { + const document = new Document(flattenDocs[i]) + finalDocs.push(document) + } + } + + const mongoClient = new MongoClient(mongoDBConnectUrl) + const collection = mongoClient.db(databaseName).collection(collectionName) + + if (!textKey || textKey === '') textKey = 'text' + if (!embeddingKey || embeddingKey === '') embeddingKey = 'embedding' + + const mongoDBAtlasVectorSearch = new MongoDBAtlasVectorSearch(embeddings, { + collection, + indexName, + textKey, + embeddingKey + }) + + try { + await mongoDBAtlasVectorSearch.addDocuments(finalDocs) + } catch (e) { + throw new Error(e) + } + } + } + + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + const credentialData = await getCredentialData(nodeData.credential ?? '', options) + const databaseName = nodeData.inputs?.databaseName as string + const collectionName = nodeData.inputs?.collectionName as string + const indexName = nodeData.inputs?.indexName as string + let textKey = nodeData.inputs?.textKey as string + let embeddingKey = nodeData.inputs?.embeddingKey as string + const embeddings = nodeData.inputs?.embeddings as Embeddings + const topK = nodeData.inputs?.topK as string + const k = topK ? parseFloat(topK) : 4 + const output = nodeData.outputs?.output as string + + let mongoDBConnectUrl = getCredentialParam('mongoDBConnectUrl', credentialData, nodeData) + + const mongoClient = new MongoClient(mongoDBConnectUrl) + const collection = mongoClient.db(databaseName).collection(collectionName) + + if (!textKey || textKey === '') textKey = 'text' + if (!embeddingKey || embeddingKey === '') embeddingKey = 'embedding' + + const vectorStore = new MongoDBAtlasVectorSearch(embeddings, { + collection, + indexName, + textKey, + embeddingKey + }) + + if (output === 'retriever') { + return vectorStore.asRetriever(k) + } else if (output === 'vectorStore') { + ;(vectorStore as any).k = k + return vectorStore + } + return vectorStore + } +} + +module.exports = { nodeClass: MongoDBAtlas_VectorStores } diff --git a/packages/components/nodes/vectorstores/MongoDB/MongoDBSearchBase.ts b/packages/components/nodes/vectorstores/MongoDBAtlas/MongoDBSearchBase.ts similarity index 98% rename from packages/components/nodes/vectorstores/MongoDB/MongoDBSearchBase.ts rename to packages/components/nodes/vectorstores/MongoDBAtlas/MongoDBSearchBase.ts index e9ef8e9a..95930e4a 100644 --- a/packages/components/nodes/vectorstores/MongoDB/MongoDBSearchBase.ts +++ b/packages/components/nodes/vectorstores/MongoDBAtlas/MongoDBSearchBase.ts @@ -22,6 +22,7 @@ export abstract class MongoDBSearchBase { type: string icon: string category: string + badge: string baseClasses: string[] inputs: INodeParams[] credential: INodeParams @@ -33,6 +34,7 @@ export abstract class MongoDBSearchBase { this.icon = 'mongodb.png' this.category = 'Vector Stores' this.baseClasses = [this.type, 'VectorStoreRetriever', 'BaseRetriever'] + this.badge = 'DEPRECATING' this.credential = { label: 'Connect Credential', name: 'credential', diff --git a/packages/components/nodes/vectorstores/MongoDB/MongoDB_Existing.ts b/packages/components/nodes/vectorstores/MongoDBAtlas/MongoDB_Existing.ts similarity index 100% rename from packages/components/nodes/vectorstores/MongoDB/MongoDB_Existing.ts rename to packages/components/nodes/vectorstores/MongoDBAtlas/MongoDB_Existing.ts diff --git a/packages/components/nodes/vectorstores/MongoDB/MongoDB_Upsert.ts b/packages/components/nodes/vectorstores/MongoDBAtlas/MongoDB_Upsert.ts similarity index 97% rename from packages/components/nodes/vectorstores/MongoDB/MongoDB_Upsert.ts rename to packages/components/nodes/vectorstores/MongoDBAtlas/MongoDB_Upsert.ts index 7d22f035..d9287243 100644 --- a/packages/components/nodes/vectorstores/MongoDB/MongoDB_Upsert.ts +++ b/packages/components/nodes/vectorstores/MongoDBAtlas/MongoDB_Upsert.ts @@ -10,7 +10,7 @@ import { MongoDBSearchBase } from './MongoDBSearchBase' class MongoDBUpsert_VectorStores extends MongoDBSearchBase implements INode { constructor() { super() - this.label = 'MongoDB Upsert Document' + this.label = 'MongoDB Atlas Upsert Document' this.name = 'MongoDBUpsert' this.version = 1.0 this.description = 'Upsert documents to MongoDB Atlas' diff --git a/packages/components/nodes/vectorstores/MongoDB/mongodb.png b/packages/components/nodes/vectorstores/MongoDBAtlas/mongodb.png similarity index 100% rename from packages/components/nodes/vectorstores/MongoDB/mongodb.png rename to packages/components/nodes/vectorstores/MongoDBAtlas/mongodb.png From db2a1d3be8d4047d70ddc33083da4d5bb668bae5 Mon Sep 17 00:00:00 2001 From: Henry Date: Wed, 22 Nov 2023 23:07:25 +0000 Subject: [PATCH 50/64] expand accordian upon filter search --- packages/ui/src/views/canvas/AddNodes.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/views/canvas/AddNodes.js b/packages/ui/src/views/canvas/AddNodes.js index 44030d0e..0973cdda 100644 --- a/packages/ui/src/views/canvas/AddNodes.js +++ b/packages/ui/src/views/canvas/AddNodes.js @@ -58,7 +58,7 @@ const AddNodes = ({ nodesData, node }) => { const ps = useRef() // Temporary method to handle Deprecating Vector Store and New ones - const categorizeVectorStores = (nodes) => { + const categorizeVectorStores = (nodes, accordianCategories, isFilter) => { const obj = { ...nodes } const vsNodes = obj['Vector Stores'] ?? [] const deprecatingNodes = [] @@ -69,7 +69,9 @@ const AddNodes = ({ nodesData, node }) => { } delete obj['Vector Stores'] obj['Vector Stores;DEPRECATING'] = deprecatingNodes + accordianCategories['Vector Stores;DEPRECATING'] = isFilter ? true : false obj['Vector Stores;NEW'] = newNodes + accordianCategories['Vector Stores;NEW'] = isFilter ? true : false setNodes(obj) } @@ -112,7 +114,7 @@ const AddNodes = ({ nodesData, node }) => { return r }, Object.create(null)) setNodes(result) - categorizeVectorStores(result) + categorizeVectorStores(result, accordianCategories, isFilter) setCategoryExpanded(accordianCategories) } From 0d1cc487a7e1e764f6e15eecafdeb67391b9f813 Mon Sep 17 00:00:00 2001 From: Henry Date: Wed, 22 Nov 2023 23:55:00 +0000 Subject: [PATCH 51/64] slight ui update --- packages/ui/src/views/apikey/index.js | 92 +++++++++++++++------------ 1 file changed, 53 insertions(+), 39 deletions(-) diff --git a/packages/ui/src/views/apikey/index.js b/packages/ui/src/views/apikey/index.js index 73224cb2..68113af5 100644 --- a/packages/ui/src/views/apikey/index.js +++ b/packages/ui/src/views/apikey/index.js @@ -10,7 +10,6 @@ import { Stack, Table, TableBody, - TableCell, TableContainer, TableHead, TableRow, @@ -24,7 +23,8 @@ import { InputAdornment, ButtonGroup } from '@mui/material' -import { useTheme } from '@mui/material/styles' +import TableCell, { tableCellClasses } from '@mui/material/TableCell' +import { useTheme, styled } from '@mui/material/styles' // project imports import MainCard from 'ui-component/cards/MainCard' @@ -60,12 +60,24 @@ import * as PropTypes from 'prop-types' import moment from 'moment/moment' // ==============================|| APIKey ||============================== // +const StyledTableCell = styled(TableCell)(({ theme }) => ({ + [`&.${tableCellClasses.head}`]: { + backgroundColor: theme.palette.action.hover + } +})) + +const StyledTableRow = styled(TableRow)(() => ({ + // hide last border + '&:last-child td, &:last-child th': { + border: 0 + } +})) function APIKeyRow(props) { const [open, setOpen] = useState(false) return ( <> - + {props.apiKey.keyName} {props.showApiKeys.includes(props.apiKey.apiKey) @@ -100,7 +112,7 @@ function APIKeyRow(props) { {props.apiKey.chatFlows.length}{' '} {props.apiKey.chatFlows.length > 0 && ( - setOpen(!open)}> + setOpen(!open)}> {props.apiKey.chatFlows.length > 0 && open ? : } )} @@ -117,42 +129,44 @@ function APIKeyRow(props) { - - - - -
- - - Chatflow Name - Modified On - Category - - - - {props.apiKey.chatFlows.map((flow, index) => ( - - - {flow.flowName} - - {moment(flow.updatedDate).format('DD-MMM-YY')} - -   - {flow.category && - flow.category - .split(';') - .map((tag, index) => ( - - ))} - + {open && ( + + + + +
+ + + + Chatflow Name + + Modified On + Category - ))} - -
-
-
-
-
+ + + {props.apiKey.chatFlows.map((flow, index) => ( + + {flow.flowName} + {moment(flow.updatedDate).format('DD-MMM-YY')} + +   + {flow.category && + flow.category + .split(';') + .map((tag, index) => ( + + ))} + + + ))} + + + + + +
+ )} ) } From b3f44e04954fa205bdbcfbd55b99641e46d9a28e Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Thu, 23 Nov 2023 10:48:00 +0530 Subject: [PATCH 52/64] ResponsibleAI - Input Moderation - Renaming of files for better clarity and alignment to functionality --- .../components/nodes/chains/LLMChain/LLMChain.ts | 2 +- .../ResponsibleAI.ts => moderation/Moderation.ts} | 4 +--- .../OpenAIModeration}/OpenAIModeration.ts | 4 ++-- .../OpenAIModeration}/OpenAIModerationRunner.ts | 2 +- .../OpenAIModeration}/openai-moderation.png | Bin .../SimplePromptModeration.ts | 4 ++-- .../SimplePromptModerationRunner.ts | 2 +- .../SimplePromptModeration}/simple_moderation.png | Bin .../marketplaces/chatflows/Input Moderation.json | 12 ++++++------ 9 files changed, 14 insertions(+), 16 deletions(-) rename packages/components/nodes/{responsibleAI/ResponsibleAI.ts => moderation/Moderation.ts} (91%) rename packages/components/nodes/{responsibleAI/InputModeration => moderation/OpenAIModeration}/OpenAIModeration.ts (94%) rename packages/components/nodes/{responsibleAI/InputModeration => moderation/OpenAIModeration}/OpenAIModerationRunner.ts (96%) rename packages/components/nodes/{responsibleAI/InputModeration => moderation/OpenAIModeration}/openai-moderation.png (100%) rename packages/components/nodes/{responsibleAI/InputModeration => moderation/SimplePromptModeration}/SimplePromptModeration.ts (95%) rename packages/components/nodes/{responsibleAI/InputModeration => moderation/SimplePromptModeration}/SimplePromptModerationRunner.ts (94%) rename packages/components/nodes/{responsibleAI/InputModeration => moderation/SimplePromptModeration}/simple_moderation.png (100%) diff --git a/packages/components/nodes/chains/LLMChain/LLMChain.ts b/packages/components/nodes/chains/LLMChain/LLMChain.ts index 4d6eb551..ee532a27 100644 --- a/packages/components/nodes/chains/LLMChain/LLMChain.ts +++ b/packages/components/nodes/chains/LLMChain/LLMChain.ts @@ -7,7 +7,7 @@ import { BaseOutputParser } from 'langchain/schema/output_parser' import { formatResponse, injectOutputParser } from '../../outputparsers/OutputParserHelpers' import { BaseLLMOutputParser } from 'langchain/schema/output_parser' import { OutputFixingParser } from 'langchain/output_parsers' -import { checkInputs, Moderation, streamResponse } from '../../responsibleAI/ResponsibleAI' +import { checkInputs, Moderation, streamResponse } from '../../moderation/Moderation' class LLMChain_Chains implements INode { label: string diff --git a/packages/components/nodes/responsibleAI/ResponsibleAI.ts b/packages/components/nodes/moderation/Moderation.ts similarity index 91% rename from packages/components/nodes/responsibleAI/ResponsibleAI.ts rename to packages/components/nodes/moderation/Moderation.ts index 0c75501f..9c40f55a 100644 --- a/packages/components/nodes/responsibleAI/ResponsibleAI.ts +++ b/packages/components/nodes/moderation/Moderation.ts @@ -1,9 +1,7 @@ import { BaseLanguageModel } from 'langchain/base_language' import { Server } from 'socket.io' -export abstract class ResponsibleAI {} - -export abstract class Moderation extends ResponsibleAI { +export abstract class Moderation { abstract checkForViolations(llm: BaseLanguageModel, input: string): Promise } diff --git a/packages/components/nodes/responsibleAI/InputModeration/OpenAIModeration.ts b/packages/components/nodes/moderation/OpenAIModeration/OpenAIModeration.ts similarity index 94% rename from packages/components/nodes/responsibleAI/InputModeration/OpenAIModeration.ts rename to packages/components/nodes/moderation/OpenAIModeration/OpenAIModeration.ts index 2edf9ad0..5233f174 100644 --- a/packages/components/nodes/responsibleAI/InputModeration/OpenAIModeration.ts +++ b/packages/components/nodes/moderation/OpenAIModeration/OpenAIModeration.ts @@ -1,6 +1,6 @@ import { INode, INodeData, INodeParams } from '../../../src/Interface' import { getBaseClasses } from '../../../src' -import { Moderation } from '../ResponsibleAI' +import { Moderation } from '../Moderation' import { OpenAIModerationRunner } from './OpenAIModerationRunner' class OpenAIModeration implements INode { @@ -20,7 +20,7 @@ class OpenAIModeration implements INode { this.version = 1.0 this.type = 'Moderation' this.icon = 'openai-moderation.png' - this.category = 'Responsible AI' + this.category = 'Moderation' this.description = 'Check whether content complies with OpenAI usage policies.' this.baseClasses = [this.type, ...getBaseClasses(Moderation)] this.inputs = [ diff --git a/packages/components/nodes/responsibleAI/InputModeration/OpenAIModerationRunner.ts b/packages/components/nodes/moderation/OpenAIModeration/OpenAIModerationRunner.ts similarity index 96% rename from packages/components/nodes/responsibleAI/InputModeration/OpenAIModerationRunner.ts rename to packages/components/nodes/moderation/OpenAIModeration/OpenAIModerationRunner.ts index 293a75d0..c517f419 100644 --- a/packages/components/nodes/responsibleAI/InputModeration/OpenAIModerationRunner.ts +++ b/packages/components/nodes/moderation/OpenAIModeration/OpenAIModerationRunner.ts @@ -1,4 +1,4 @@ -import { Moderation } from '../ResponsibleAI' +import { Moderation } from '../Moderation' import { BaseLanguageModel } from 'langchain/base_language' import { OpenAIModerationChain } from 'langchain/chains' diff --git a/packages/components/nodes/responsibleAI/InputModeration/openai-moderation.png b/packages/components/nodes/moderation/OpenAIModeration/openai-moderation.png similarity index 100% rename from packages/components/nodes/responsibleAI/InputModeration/openai-moderation.png rename to packages/components/nodes/moderation/OpenAIModeration/openai-moderation.png diff --git a/packages/components/nodes/responsibleAI/InputModeration/SimplePromptModeration.ts b/packages/components/nodes/moderation/SimplePromptModeration/SimplePromptModeration.ts similarity index 95% rename from packages/components/nodes/responsibleAI/InputModeration/SimplePromptModeration.ts rename to packages/components/nodes/moderation/SimplePromptModeration/SimplePromptModeration.ts index 88bf2c42..bf5a32f6 100644 --- a/packages/components/nodes/responsibleAI/InputModeration/SimplePromptModeration.ts +++ b/packages/components/nodes/moderation/SimplePromptModeration/SimplePromptModeration.ts @@ -1,6 +1,6 @@ import { INode, INodeData, INodeParams } from '../../../src/Interface' import { getBaseClasses } from '../../../src' -import { Moderation } from '../ResponsibleAI' +import { Moderation } from '../Moderation' import { SimplePromptModerationRunner } from './SimplePromptModerationRunner' class SimplePromptModeration implements INode { @@ -20,7 +20,7 @@ class SimplePromptModeration implements INode { this.version = 1.0 this.type = 'Moderation' this.icon = 'simple_moderation.png' - this.category = 'Responsible AI' + this.category = 'Moderation' this.description = 'Check whether input consists of any text from Deny list, and prevent being sent to LLM' this.baseClasses = [this.type, ...getBaseClasses(Moderation)] this.inputs = [ diff --git a/packages/components/nodes/responsibleAI/InputModeration/SimplePromptModerationRunner.ts b/packages/components/nodes/moderation/SimplePromptModeration/SimplePromptModerationRunner.ts similarity index 94% rename from packages/components/nodes/responsibleAI/InputModeration/SimplePromptModerationRunner.ts rename to packages/components/nodes/moderation/SimplePromptModeration/SimplePromptModerationRunner.ts index fb71166c..7fc251ad 100644 --- a/packages/components/nodes/responsibleAI/InputModeration/SimplePromptModerationRunner.ts +++ b/packages/components/nodes/moderation/SimplePromptModeration/SimplePromptModerationRunner.ts @@ -1,4 +1,4 @@ -import { Moderation } from '../ResponsibleAI' +import { Moderation } from '../Moderation' import { BaseLanguageModel } from 'langchain/base_language' export class SimplePromptModerationRunner implements Moderation { diff --git a/packages/components/nodes/responsibleAI/InputModeration/simple_moderation.png b/packages/components/nodes/moderation/SimplePromptModeration/simple_moderation.png similarity index 100% rename from packages/components/nodes/responsibleAI/InputModeration/simple_moderation.png rename to packages/components/nodes/moderation/SimplePromptModeration/simple_moderation.png diff --git a/packages/server/marketplaces/chatflows/Input Moderation.json b/packages/server/marketplaces/chatflows/Input Moderation.json index 254df4ac..1f6cc624 100644 --- a/packages/server/marketplaces/chatflows/Input Moderation.json +++ b/packages/server/marketplaces/chatflows/Input Moderation.json @@ -17,8 +17,8 @@ "version": 1, "name": "inputModerationOpenAI", "type": "Moderation", - "baseClasses": ["Moderation", "ResponsibleAI"], - "category": "Responsible AI", + "baseClasses": ["Moderation"], + "category": "Moderation", "description": "Check whether content complies with OpenAI usage policies.", "inputParams": [ { @@ -37,10 +37,10 @@ }, "outputAnchors": [ { - "id": "inputModerationOpenAI_0-output-inputModerationOpenAI-Moderation|ResponsibleAI", + "id": "inputModerationOpenAI_0-output-inputModerationOpenAI-Moderation|Moderation", "name": "inputModerationOpenAI", "label": "Moderation", - "type": "Moderation | ResponsibleAI" + "type": "Moderation" } ], "outputs": {}, @@ -406,11 +406,11 @@ "edges": [ { "source": "inputModerationOpenAI_0", - "sourceHandle": "inputModerationOpenAI_0-output-inputModerationOpenAI-Moderation|ResponsibleAI", + "sourceHandle": "inputModerationOpenAI_0-output-inputModerationOpenAI-Moderation|Moderation", "target": "llmChain_0", "targetHandle": "llmChain_0-input-inputModeration-Moderation", "type": "buttonedge", - "id": "inputModerationOpenAI_0-inputModerationOpenAI_0-output-inputModerationOpenAI-Moderation|ResponsibleAI-llmChain_0-llmChain_0-input-inputModeration-Moderation", + "id": "inputModerationOpenAI_0-inputModerationOpenAI_0-output-inputModerationOpenAI-Moderation|Moderation-llmChain_0-llmChain_0-input-inputModeration-Moderation", "data": { "label": "" } From f953e9fd9c7b86175120940d95bf29e77d956033 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Thu, 23 Nov 2023 11:13:36 +0530 Subject: [PATCH 53/64] GitGuardian - Fix for MongoDB False Positive Alert --- packages/components/credentials/MongoDBUrlApi.credential.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/credentials/MongoDBUrlApi.credential.ts b/packages/components/credentials/MongoDBUrlApi.credential.ts index 2f2cba38..4412539c 100644 --- a/packages/components/credentials/MongoDBUrlApi.credential.ts +++ b/packages/components/credentials/MongoDBUrlApi.credential.ts @@ -16,7 +16,7 @@ class MongoDBUrlApi implements INodeCredential { label: 'ATLAS Connection URL', name: 'mongoDBConnectUrl', type: 'string', - placeholder: 'mongodb+srv://myDatabaseUser:D1fficultP%40ssw0rd@cluster0.example.mongodb.net/?retryWrites=true&w=majority' + placeholder: 'mongodb+srv://:@cluster0.example.mongodb.net/?retryWrites=true&w=majority' } ] } From e0a0c830e9371a3cdf6b5934f24411d7b877ca6a Mon Sep 17 00:00:00 2001 From: Henry Heng Date: Thu, 23 Nov 2023 11:29:02 +0000 Subject: [PATCH 54/64] update apache 2.0 licensing --- LICENSE.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/LICENSE.md b/LICENSE.md index 4a27187d..0f4afcd1 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,7 +1,23 @@ - Apache License + Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ +Flowise is governed by the Apache License 2.0, with additional terms and conditions outlined below: + +Flowise can be used for commercial purposes for "backend-as-a-service" for your applications or as a development platform for enterprises. However, under specific conditions, you must reach out to the project's administrators to secure a commercial license: + +a. Multi-tenant SaaS service: Unless you have explicit written authorization from Flowise, you may not utilize the Flowise source code to operate a multi-tenant SaaS service that closely resembles the Flowise cloud-based services. +b. Logo and copyright information: While using Flowise in commercial application, you are prohibited from removing or altering the LOGO or copyright information displayed in the Flowise console and UI. + +For inquiries regarding licensing matters, please contact hello@flowiseai.com via email. + +Contributors are required to consent to the following terms related to their contributed code: + +a. The project maintainers have the authority to modify the open-source agreement to be more stringent or lenient. +b. Contributed code can be used for commercial purposes, including Flowise's cloud-based services. + +All other rights and restrictions are in accordance with the Apache License 2.0. + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. From 262ef7fd0c266d3a734554ffdb3a06d3b8902b9c Mon Sep 17 00:00:00 2001 From: Marc Klingen Date: Thu, 23 Nov 2023 22:58:14 +0100 Subject: [PATCH 55/64] Link to Langfuse integration doc instead of get-started --- packages/components/credentials/LangfuseApi.credential.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/credentials/LangfuseApi.credential.ts b/packages/components/credentials/LangfuseApi.credential.ts index 452ca989..923af517 100644 --- a/packages/components/credentials/LangfuseApi.credential.ts +++ b/packages/components/credentials/LangfuseApi.credential.ts @@ -12,7 +12,7 @@ class LangfuseApi implements INodeCredential { this.name = 'langfuseApi' this.version = 1.0 this.description = - 'Refer to official guide on how to get API key on Langfuse' + 'Refer to integration guide on how to get API keys on Langfuse' this.inputs = [ { label: 'Secret Key', From 0d7b916b86ab8a5d43b6d78e9bb126cfed82fcbe Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 24 Nov 2023 11:55:56 +0000 Subject: [PATCH 56/64] update models for openai function --- .../ConversationalRetrievalAgent.ts | 6 +++--- .../OpenAIFunctionAgent/OpenAIFunctionAgent.ts | 10 ++++------ .../marketplaces/chatflows/API Agent OpenAI.json | 9 ++++----- .../chatflows/Conversational Retrieval Agent.json | 8 ++++---- .../server/marketplaces/chatflows/OpenAI Agent.json | 9 ++++----- packages/ui/src/views/canvas/AddNodes.js | 12 ++++++++---- 6 files changed, 27 insertions(+), 27 deletions(-) diff --git a/packages/components/nodes/agents/ConversationalRetrievalAgent/ConversationalRetrievalAgent.ts b/packages/components/nodes/agents/ConversationalRetrievalAgent/ConversationalRetrievalAgent.ts index 4a908d7f..7b71cb5f 100644 --- a/packages/components/nodes/agents/ConversationalRetrievalAgent/ConversationalRetrievalAgent.ts +++ b/packages/components/nodes/agents/ConversationalRetrievalAgent/ConversationalRetrievalAgent.ts @@ -21,7 +21,7 @@ class ConversationalRetrievalAgent_Agents implements INode { constructor() { this.label = 'Conversational Retrieval Agent' this.name = 'conversationalRetrievalAgent' - this.version = 1.0 + this.version = 2.0 this.type = 'AgentExecutor' this.category = 'Agents' this.icon = 'agent.svg' @@ -40,9 +40,9 @@ class ConversationalRetrievalAgent_Agents implements INode { type: 'BaseChatMemory' }, { - label: 'OpenAI Chat Model', + label: 'OpenAI/Azure Chat Model', name: 'model', - type: 'ChatOpenAI' + type: 'ChatOpenAI | AzureChatOpenAI' }, { label: 'System Message', diff --git a/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts b/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts index c920c399..ce6f576f 100644 --- a/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts +++ b/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts @@ -20,11 +20,11 @@ class OpenAIFunctionAgent_Agents implements INode { constructor() { this.label = 'OpenAI Function Agent' this.name = 'openAIFunctionAgent' - this.version = 1.0 + this.version = 2.0 this.type = 'AgentExecutor' this.category = 'Agents' this.icon = 'openai.png' - this.description = `An agent that uses OpenAI's Function Calling functionality to pick the tool and args to call` + this.description = `An agent that uses Function Calling to pick the tool and args to call` this.baseClasses = [this.type, ...getBaseClasses(AgentExecutor)] this.inputs = [ { @@ -39,11 +39,9 @@ class OpenAIFunctionAgent_Agents implements INode { type: 'BaseChatMemory' }, { - label: 'OpenAI Chat Model', + label: 'OpenAI/Azure Chat Model', name: 'model', - description: - 'Only works with gpt-3.5-turbo-0613 and gpt-4-0613. Refer docs for more info', - type: 'BaseChatModel' + type: 'ChatOpenAI | AzureChatOpenAI' }, { label: 'System Message', diff --git a/packages/server/marketplaces/chatflows/API Agent OpenAI.json b/packages/server/marketplaces/chatflows/API Agent OpenAI.json index 5498b4f3..4950a6a6 100644 --- a/packages/server/marketplaces/chatflows/API Agent OpenAI.json +++ b/packages/server/marketplaces/chatflows/API Agent OpenAI.json @@ -334,7 +334,7 @@ "id": "openAIFunctionAgent_0", "label": "OpenAI Function Agent", "name": "openAIFunctionAgent", - "version": 1, + "version": 2, "type": "AgentExecutor", "baseClasses": ["AgentExecutor", "BaseChain"], "category": "Agents", @@ -365,11 +365,10 @@ "id": "openAIFunctionAgent_0-input-memory-BaseChatMemory" }, { - "label": "OpenAI Chat Model", + "label": "OpenAI/Azure Chat Model", "name": "model", - "description": "Only works with gpt-3.5-turbo-0613 and gpt-4-0613. Refer docs for more info", - "type": "BaseChatModel", - "id": "openAIFunctionAgent_0-input-model-BaseChatModel" + "type": "ChatOpenAI | AzureChatOpenAI", + "id": "openAIFunctionAgent_0-input-model-ChatOpenAI | AzureChatOpenAI" } ], "inputs": { diff --git a/packages/server/marketplaces/chatflows/Conversational Retrieval Agent.json b/packages/server/marketplaces/chatflows/Conversational Retrieval Agent.json index aafc8e8e..800ae300 100644 --- a/packages/server/marketplaces/chatflows/Conversational Retrieval Agent.json +++ b/packages/server/marketplaces/chatflows/Conversational Retrieval Agent.json @@ -98,7 +98,7 @@ "data": { "id": "conversationalRetrievalAgent_0", "label": "Conversational Retrieval Agent", - "version": 1, + "version": 2, "name": "conversationalRetrievalAgent", "type": "AgentExecutor", "baseClasses": ["AgentExecutor", "BaseChain", "Runnable"], @@ -130,10 +130,10 @@ "id": "conversationalRetrievalAgent_0-input-memory-BaseChatMemory" }, { - "label": "OpenAI Chat Model", + "label": "OpenAI/Azure Chat Model", "name": "model", - "type": "ChatOpenAI", - "id": "conversationalRetrievalAgent_0-input-model-ChatOpenAI" + "type": "ChatOpenAI | AzureChatOpenAI", + "id": "conversationalRetrievalAgent_0-input-model-ChatOpenAI | AzureChatOpenAI" } ], "inputs": { diff --git a/packages/server/marketplaces/chatflows/OpenAI Agent.json b/packages/server/marketplaces/chatflows/OpenAI Agent.json index a4944af0..bc27a9fe 100644 --- a/packages/server/marketplaces/chatflows/OpenAI Agent.json +++ b/packages/server/marketplaces/chatflows/OpenAI Agent.json @@ -206,7 +206,7 @@ "id": "openAIFunctionAgent_0", "label": "OpenAI Function Agent", "name": "openAIFunctionAgent", - "version": 1, + "version": 2, "type": "AgentExecutor", "baseClasses": ["AgentExecutor", "BaseChain"], "category": "Agents", @@ -237,11 +237,10 @@ "id": "openAIFunctionAgent_0-input-memory-BaseChatMemory" }, { - "label": "OpenAI Chat Model", + "label": "OpenAI/Azure Chat Model", "name": "model", - "description": "Only works with gpt-3.5-turbo-0613 and gpt-4-0613. Refer docs for more info", - "type": "BaseChatModel", - "id": "openAIFunctionAgent_0-input-model-BaseChatModel" + "type": "ChatOpenAI | AzureChatOpenAI", + "id": "openAIFunctionAgent_0-input-model-ChatOpenAI | AzureChatOpenAI" } ], "inputs": { diff --git a/packages/ui/src/views/canvas/AddNodes.js b/packages/ui/src/views/canvas/AddNodes.js index 0973cdda..7bf3e7ff 100644 --- a/packages/ui/src/views/canvas/AddNodes.js +++ b/packages/ui/src/views/canvas/AddNodes.js @@ -68,10 +68,14 @@ const AddNodes = ({ nodesData, node }) => { else newNodes.push(vsNode) } delete obj['Vector Stores'] - obj['Vector Stores;DEPRECATING'] = deprecatingNodes - accordianCategories['Vector Stores;DEPRECATING'] = isFilter ? true : false - obj['Vector Stores;NEW'] = newNodes - accordianCategories['Vector Stores;NEW'] = isFilter ? true : false + if (deprecatingNodes.length) { + obj['Vector Stores;DEPRECATING'] = deprecatingNodes + accordianCategories['Vector Stores;DEPRECATING'] = isFilter ? true : false + } + if (newNodes.length) { + obj['Vector Stores;NEW'] = newNodes + accordianCategories['Vector Stores;NEW'] = isFilter ? true : false + } setNodes(obj) } From 12c688aae01c04ccf698f27ff3a30a869fcc32aa Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 24 Nov 2023 14:29:50 +0000 Subject: [PATCH 57/64] update langchain version --- .../nodes/chains/LLMChain/LLMChain.ts | 6 +++--- .../chatmodels/AWSBedrock/AWSChatBedrock.ts | 9 ++++++--- .../AWSBedrockEmbedding/AWSBedrockEmbedding.ts | 6 ++++-- .../nodes/llms/AWSBedrock/AWSBedrock.ts | 3 ++- .../components/nodes/moderation/Moderation.ts | 7 +++---- .../OpenAIModeration/OpenAIModeration.ts | 18 ++++++++++++++---- .../OpenAIModeration/OpenAIModerationRunner.ts | 13 ++++++++----- .../SimplePromptModerationRunner.ts | 3 +-- .../nodes/outputparsers/OutputParserHelpers.ts | 4 ++-- .../InMemory/InMemoryVectorStore.ts | 3 ++- packages/components/package.json | 2 +- packages/server/src/utils/index.ts | 2 +- 12 files changed, 47 insertions(+), 29 deletions(-) diff --git a/packages/components/nodes/chains/LLMChain/LLMChain.ts b/packages/components/nodes/chains/LLMChain/LLMChain.ts index ee532a27..fd398151 100644 --- a/packages/components/nodes/chains/LLMChain/LLMChain.ts +++ b/packages/components/nodes/chains/LLMChain/LLMChain.ts @@ -1,7 +1,7 @@ import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' import { getBaseClasses, handleEscapeCharacters } from '../../../src/utils' import { LLMChain } from 'langchain/chains' -import { BaseLanguageModel } from 'langchain/base_language' +import { BaseLanguageModel, BaseLanguageModelCallOptions } from 'langchain/base_language' import { ConsoleCallbackHandler, CustomChainHandler, additionalCallbacks } from '../../../src/handler' import { BaseOutputParser } from 'langchain/schema/output_parser' import { formatResponse, injectOutputParser } from '../../outputparsers/OutputParserHelpers' @@ -141,7 +141,7 @@ class LLMChain_Chains implements INode { const runPrediction = async ( inputVariables: string[], - chain: LLMChain, + chain: LLMChain>, input: string, promptValuesRaw: ICommonObject | undefined, options: ICommonObject, @@ -164,7 +164,7 @@ const runPrediction = async ( if (moderations && moderations.length > 0) { try { // Use the output of the moderation chain as input for the LLM chain - input = await checkInputs(moderations, chain.llm, input) + input = await checkInputs(moderations, input) } catch (e) { await new Promise((resolve) => setTimeout(resolve, 500)) streamResponse(isStreaming, e.message, socketIO, socketIOClientId) diff --git a/packages/components/nodes/chatmodels/AWSBedrock/AWSChatBedrock.ts b/packages/components/nodes/chatmodels/AWSBedrock/AWSChatBedrock.ts index ade46ab9..956fcdb3 100644 --- a/packages/components/nodes/chatmodels/AWSBedrock/AWSChatBedrock.ts +++ b/packages/components/nodes/chatmodels/AWSBedrock/AWSChatBedrock.ts @@ -27,7 +27,7 @@ class AWSChatBedrock_ChatModels implements INode { constructor() { this.label = 'AWS Bedrock' this.name = 'awsChatBedrock' - this.version = 2.0 + this.version = 3.0 this.type = 'AWSChatBedrock' this.icon = 'awsBedrock.png' this.category = 'Chat Models' @@ -97,7 +97,8 @@ class AWSChatBedrock_ChatModels implements INode { options: [ { label: 'anthropic.claude-instant-v1', name: 'anthropic.claude-instant-v1' }, { label: 'anthropic.claude-v1', name: 'anthropic.claude-v1' }, - { label: 'anthropic.claude-v2', name: 'anthropic.claude-v2' } + { label: 'anthropic.claude-v2', name: 'anthropic.claude-v2' }, + { label: 'meta.llama2-13b-chat-v1', name: 'meta.llama2-13b-chat-v1' } ], default: 'anthropic.claude-v2' }, @@ -128,12 +129,14 @@ class AWSChatBedrock_ChatModels implements INode { const iTemperature = nodeData.inputs?.temperature as string const iMax_tokens_to_sample = nodeData.inputs?.max_tokens_to_sample as string const cache = nodeData.inputs?.cache as BaseCache + const streaming = nodeData.inputs?.streaming as boolean const obj: BaseBedrockInput & BaseLLMParams = { region: iRegion, model: iModel, maxTokens: parseInt(iMax_tokens_to_sample, 10), - temperature: parseFloat(iTemperature) + temperature: parseFloat(iTemperature), + streaming: streaming ?? true } /** diff --git a/packages/components/nodes/embeddings/AWSBedrockEmbedding/AWSBedrockEmbedding.ts b/packages/components/nodes/embeddings/AWSBedrockEmbedding/AWSBedrockEmbedding.ts index ba2aa5e7..8249d512 100644 --- a/packages/components/nodes/embeddings/AWSBedrockEmbedding/AWSBedrockEmbedding.ts +++ b/packages/components/nodes/embeddings/AWSBedrockEmbedding/AWSBedrockEmbedding.ts @@ -18,7 +18,7 @@ class AWSBedrockEmbedding_Embeddings implements INode { constructor() { this.label = 'AWS Bedrock Embeddings' this.name = 'AWSBedrockEmbeddings' - this.version = 1.0 + this.version = 2.0 this.type = 'AWSBedrockEmbeddings' this.icon = 'awsBedrock.png' this.category = 'Embeddings' @@ -81,7 +81,9 @@ class AWSBedrockEmbedding_Embeddings implements INode { type: 'options', options: [ { label: 'amazon.titan-embed-text-v1', name: 'amazon.titan-embed-text-v1' }, - { label: 'amazon.titan-embed-g1-text-02', name: 'amazon.titan-embed-g1-text-02' } + { label: 'amazon.titan-embed-g1-text-02', name: 'amazon.titan-embed-g1-text-02' }, + { label: 'cohere.embed-english-v3', name: 'cohere.embed-english-v3' }, + { label: 'cohere.embed-multilingual-v3', name: 'cohere.embed-multilingual-v3' } ], default: 'amazon.titan-embed-text-v1' } diff --git a/packages/components/nodes/llms/AWSBedrock/AWSBedrock.ts b/packages/components/nodes/llms/AWSBedrock/AWSBedrock.ts index b67219f3..177a32ef 100644 --- a/packages/components/nodes/llms/AWSBedrock/AWSBedrock.ts +++ b/packages/components/nodes/llms/AWSBedrock/AWSBedrock.ts @@ -27,7 +27,7 @@ class AWSBedrock_LLMs implements INode { constructor() { this.label = 'AWS Bedrock' this.name = 'awsBedrock' - this.version = 1.2 + this.version = 2.0 this.type = 'AWSBedrock' this.icon = 'awsBedrock.png' this.category = 'LLMs' @@ -98,6 +98,7 @@ class AWSBedrock_LLMs implements INode { { label: 'amazon.titan-tg1-large', name: 'amazon.titan-tg1-large' }, { label: 'amazon.titan-e1t-medium', name: 'amazon.titan-e1t-medium' }, { label: 'cohere.command-text-v14', name: 'cohere.command-text-v14' }, + { label: 'cohere.command-light-text-v14', name: 'cohere.command-light-text-v14' }, { label: 'ai21.j2-grande-instruct', name: 'ai21.j2-grande-instruct' }, { label: 'ai21.j2-jumbo-instruct', name: 'ai21.j2-jumbo-instruct' }, { label: 'ai21.j2-mid', name: 'ai21.j2-mid' }, diff --git a/packages/components/nodes/moderation/Moderation.ts b/packages/components/nodes/moderation/Moderation.ts index 9c40f55a..9fd2bfde 100644 --- a/packages/components/nodes/moderation/Moderation.ts +++ b/packages/components/nodes/moderation/Moderation.ts @@ -1,13 +1,12 @@ -import { BaseLanguageModel } from 'langchain/base_language' import { Server } from 'socket.io' export abstract class Moderation { - abstract checkForViolations(llm: BaseLanguageModel, input: string): Promise + abstract checkForViolations(input: string): Promise } -export const checkInputs = async (inputModerations: Moderation[], llm: BaseLanguageModel, input: string): Promise => { +export const checkInputs = async (inputModerations: Moderation[], input: string): Promise => { for (const moderation of inputModerations) { - input = await moderation.checkForViolations(llm, input) + input = await moderation.checkForViolations(input) } return input } diff --git a/packages/components/nodes/moderation/OpenAIModeration/OpenAIModeration.ts b/packages/components/nodes/moderation/OpenAIModeration/OpenAIModeration.ts index 5233f174..51578630 100644 --- a/packages/components/nodes/moderation/OpenAIModeration/OpenAIModeration.ts +++ b/packages/components/nodes/moderation/OpenAIModeration/OpenAIModeration.ts @@ -1,5 +1,5 @@ -import { INode, INodeData, INodeParams } from '../../../src/Interface' -import { getBaseClasses } from '../../../src' +import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' +import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src' import { Moderation } from '../Moderation' import { OpenAIModerationRunner } from './OpenAIModerationRunner' @@ -12,6 +12,7 @@ class OpenAIModeration implements INode { icon: string category: string baseClasses: string[] + credential: INodeParams inputs: INodeParams[] constructor() { @@ -23,6 +24,12 @@ class OpenAIModeration implements INode { this.category = 'Moderation' this.description = 'Check whether content complies with OpenAI usage policies.' this.baseClasses = [this.type, ...getBaseClasses(Moderation)] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['openAIApi'] + } this.inputs = [ { label: 'Error Message', @@ -35,8 +42,11 @@ class OpenAIModeration implements INode { ] } - async init(nodeData: INodeData): Promise { - const runner = new OpenAIModerationRunner() + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + const credentialData = await getCredentialData(nodeData.credential ?? '', options) + const openAIApiKey = getCredentialParam('openAIApiKey', credentialData, nodeData) + + const runner = new OpenAIModerationRunner(openAIApiKey) const moderationErrorMessage = nodeData.inputs?.moderationErrorMessage as string if (moderationErrorMessage) runner.setErrorMessage(moderationErrorMessage) return runner diff --git a/packages/components/nodes/moderation/OpenAIModeration/OpenAIModerationRunner.ts b/packages/components/nodes/moderation/OpenAIModeration/OpenAIModerationRunner.ts index c517f419..3a3ec550 100644 --- a/packages/components/nodes/moderation/OpenAIModeration/OpenAIModerationRunner.ts +++ b/packages/components/nodes/moderation/OpenAIModeration/OpenAIModerationRunner.ts @@ -1,18 +1,21 @@ import { Moderation } from '../Moderation' -import { BaseLanguageModel } from 'langchain/base_language' import { OpenAIModerationChain } from 'langchain/chains' export class OpenAIModerationRunner implements Moderation { + private openAIApiKey = '' private moderationErrorMessage: string = "Text was found that violates OpenAI's content policy." - async checkForViolations(llm: BaseLanguageModel, input: string): Promise { - const openAIApiKey = (llm as any).openAIApiKey - if (!openAIApiKey) { + constructor(openAIApiKey: string) { + this.openAIApiKey = openAIApiKey + } + + async checkForViolations(input: string): Promise { + if (!this.openAIApiKey) { throw Error('OpenAI API key not found') } // Create a new instance of the OpenAIModerationChain const moderation = new OpenAIModerationChain({ - openAIApiKey: openAIApiKey, + openAIApiKey: this.openAIApiKey, throwError: false // If set to true, the call will throw an error when the moderation chain detects violating content. If set to false, violating content will return "Text was found that violates OpenAI's content policy.". }) // Send the user's input to the moderation chain and wait for the result diff --git a/packages/components/nodes/moderation/SimplePromptModeration/SimplePromptModerationRunner.ts b/packages/components/nodes/moderation/SimplePromptModeration/SimplePromptModerationRunner.ts index 7fc251ad..94967ba2 100644 --- a/packages/components/nodes/moderation/SimplePromptModeration/SimplePromptModerationRunner.ts +++ b/packages/components/nodes/moderation/SimplePromptModeration/SimplePromptModerationRunner.ts @@ -1,5 +1,4 @@ import { Moderation } from '../Moderation' -import { BaseLanguageModel } from 'langchain/base_language' export class SimplePromptModerationRunner implements Moderation { private readonly denyList: string = '' @@ -13,7 +12,7 @@ export class SimplePromptModerationRunner implements Moderation { this.moderationErrorMessage = moderationErrorMessage } - async checkForViolations(_: BaseLanguageModel, input: string): Promise { + async checkForViolations(input: string): Promise { this.denyList.split('\n').forEach((denyListItem) => { if (denyListItem && denyListItem !== '' && input.includes(denyListItem)) { throw Error(this.moderationErrorMessage) diff --git a/packages/components/nodes/outputparsers/OutputParserHelpers.ts b/packages/components/nodes/outputparsers/OutputParserHelpers.ts index a94edddd..8ea77e6b 100644 --- a/packages/components/nodes/outputparsers/OutputParserHelpers.ts +++ b/packages/components/nodes/outputparsers/OutputParserHelpers.ts @@ -1,6 +1,6 @@ import { BaseOutputParser } from 'langchain/schema/output_parser' import { LLMChain } from 'langchain/chains' -import { BaseLanguageModel } from 'langchain/base_language' +import { BaseLanguageModel, BaseLanguageModelCallOptions } from 'langchain/base_language' import { ICommonObject } from '../../src' import { ChatPromptTemplate, FewShotPromptTemplate, PromptTemplate, SystemMessagePromptTemplate } from 'langchain/prompts' @@ -15,7 +15,7 @@ export const formatResponse = (response: string | object): string | object => { export const injectOutputParser = ( outputParser: BaseOutputParser, - chain: LLMChain, + chain: LLMChain>, promptValues: ICommonObject | undefined = undefined ) => { if (outputParser && chain.prompt) { diff --git a/packages/components/nodes/vectorstores/InMemory/InMemoryVectorStore.ts b/packages/components/nodes/vectorstores/InMemory/InMemoryVectorStore.ts index 51394613..620c3af7 100644 --- a/packages/components/nodes/vectorstores/InMemory/InMemoryVectorStore.ts +++ b/packages/components/nodes/vectorstores/InMemory/InMemoryVectorStore.ts @@ -31,7 +31,8 @@ class InMemoryVectorStore_VectorStores implements INode { label: 'Document', name: 'document', type: 'Document', - list: true + list: true, + optional: true }, { label: 'Embeddings', diff --git a/packages/components/package.json b/packages/components/package.json index 1d4cea57..3f81ea67 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -49,7 +49,7 @@ "html-to-text": "^9.0.5", "husky": "^8.0.3", "ioredis": "^5.3.2", - "langchain": "^0.0.165", + "langchain": "^0.0.196", "langfuse-langchain": "^1.0.31", "langsmith": "^0.0.32", "linkifyjs": "^4.1.1", diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index 86d626c4..9dbb695e 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -844,7 +844,7 @@ export const findAvailableConfigs = (reactFlowNodes: IReactFlowNode[], component */ export const isFlowValidForStream = (reactFlowNodes: IReactFlowNode[], endingNodeData: INodeData) => { const streamAvailableLLMs = { - 'Chat Models': ['azureChatOpenAI', 'chatOpenAI', 'chatAnthropic', 'chatOllama'], + 'Chat Models': ['azureChatOpenAI', 'chatOpenAI', 'chatAnthropic', 'chatOllama', 'awsChatBedrock'], LLMs: ['azureOpenAI', 'openAI', 'ollama'] } From 19e9c67d12190e80869a6ca338fb2082430614fd Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 24 Nov 2023 16:29:25 +0000 Subject: [PATCH 58/64] update momento sdk --- packages/components/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/components/package.json b/packages/components/package.json index 3f81ea67..a965a0c7 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -22,7 +22,8 @@ "@dqbd/tiktoken": "^1.0.7", "@elastic/elasticsearch": "^8.9.0", "@getzep/zep-js": "^0.6.3", - "@gomomento/sdk": "^1.40.2", + "@gomomento/sdk": "^1.51.1", + "@gomomento/sdk-core": "^1.51.1", "@google-ai/generativelanguage": "^0.2.1", "@huggingface/inference": "^2.6.1", "@notionhq/client": "^2.2.8", From 9f2c01dc3759a9d2ab79ddd6312baf2ade09ae7d Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 24 Nov 2023 17:08:16 +0000 Subject: [PATCH 59/64] =?UTF-8?q?=F0=9F=A5=B3=20flowise@1.4.3=20release?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- packages/server/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 909eb345..649a9a47 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "flowise", - "version": "1.4.2", + "version": "1.4.3", "private": true, "homepage": "https://flowiseai.com", "workspaces": [ diff --git a/packages/server/package.json b/packages/server/package.json index 3503e982..6f4ccaf4 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "flowise", - "version": "1.4.2", + "version": "1.4.3", "description": "Flowiseai Server", "main": "dist/index", "types": "dist/index.d.ts", From aad91112042e77b20f45b95b688ef99c14701523 Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 24 Nov 2023 17:08:41 +0000 Subject: [PATCH 60/64] =?UTF-8?q?=F0=9F=A5=B3=20flowise-ui@1.4.1=20release?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/ui/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/package.json b/packages/ui/package.json index 6914c04a..76369ab2 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "flowise-ui", - "version": "1.4.0", + "version": "1.4.1", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://flowiseai.com", "author": { From 17198d8524659a6e21a92710af4af68802f93672 Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 24 Nov 2023 17:09:13 +0000 Subject: [PATCH 61/64] =?UTF-8?q?=F0=9F=A5=B3=20flowise-components@1.4.3?= =?UTF-8?q?=20release?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/components/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/package.json b/packages/components/package.json index a965a0c7..5566218c 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "flowise-components", - "version": "1.4.2", + "version": "1.4.3", "description": "Flowiseai Components", "main": "dist/src/index", "types": "dist/src/index.d.ts", From 3536c2afec7775189016534dfedc53371d2d5e76 Mon Sep 17 00:00:00 2001 From: Henry Date: Mon, 27 Nov 2023 15:34:28 +0000 Subject: [PATCH 62/64] fix memory nodes --- .../credentials/RedisCacheApi.credential.ts | 2 +- .../RedisCacheUrlApi.credential.ts | 4 +- .../ConversationalAgent.ts | 14 +- .../ConversationalRetrievalAgent.ts | 2 + .../OpenAIFunctionAgent.ts | 2 + .../ConversationChain/ConversationChain.ts | 4 +- .../nodes/memory/DynamoDb/DynamoDb.ts | 3 +- .../memory/MongoDBMemory/MongoDBMemory.ts | 3 +- .../RedisBackedChatMemory.ts | 2 +- .../UpstashRedisBackedChatMemory.ts | 1 + .../OpenAIModeration/OpenAIModeration.ts | 2 +- .../OpenAIModeration/openai-moderation.png | Bin 47939 -> 0 bytes .../moderation/OpenAIModeration/openai.png | Bin 0 -> 3991 bytes packages/components/package.json | 2 +- .../chatflows/API Agent OpenAI.json | 40 ++-- .../marketplaces/chatflows/API Agent.json | 52 ++--- .../chatflows/Conversational Agent.json | 36 ++-- .../Conversational Retrieval Agent.json | 4 +- .../chatflows/Multiple VectorDB.json | 198 +++++++++--------- .../marketplaces/chatflows/OpenAI Agent.json | 36 ++-- .../marketplaces/chatflows/WebBrowser.json | 188 ++++++++--------- 21 files changed, 301 insertions(+), 294 deletions(-) delete mode 100644 packages/components/nodes/moderation/OpenAIModeration/openai-moderation.png create mode 100644 packages/components/nodes/moderation/OpenAIModeration/openai.png diff --git a/packages/components/credentials/RedisCacheApi.credential.ts b/packages/components/credentials/RedisCacheApi.credential.ts index e09a94e7..4d1a2498 100644 --- a/packages/components/credentials/RedisCacheApi.credential.ts +++ b/packages/components/credentials/RedisCacheApi.credential.ts @@ -8,7 +8,7 @@ class RedisCacheApi implements INodeCredential { inputs: INodeParams[] constructor() { - this.label = 'Redis Cache API' + this.label = 'Redis API' this.name = 'redisCacheApi' this.version = 1.0 this.inputs = [ diff --git a/packages/components/credentials/RedisCacheUrlApi.credential.ts b/packages/components/credentials/RedisCacheUrlApi.credential.ts index fc2e2eb2..e016d78f 100644 --- a/packages/components/credentials/RedisCacheUrlApi.credential.ts +++ b/packages/components/credentials/RedisCacheUrlApi.credential.ts @@ -8,7 +8,7 @@ class RedisCacheUrlApi implements INodeCredential { inputs: INodeParams[] constructor() { - this.label = 'Redis Cache URL' + this.label = 'Redis URL' this.name = 'redisCacheUrlApi' this.version = 1.0 this.inputs = [ @@ -16,7 +16,7 @@ class RedisCacheUrlApi implements INodeCredential { label: 'Redis URL', name: 'redisUrl', type: 'string', - default: '127.0.0.1' + default: 'redis://localhost:6379' } ] } diff --git a/packages/components/nodes/agents/ConversationalAgent/ConversationalAgent.ts b/packages/components/nodes/agents/ConversationalAgent/ConversationalAgent.ts index 00f825d4..8a2329b5 100644 --- a/packages/components/nodes/agents/ConversationalAgent/ConversationalAgent.ts +++ b/packages/components/nodes/agents/ConversationalAgent/ConversationalAgent.ts @@ -3,7 +3,7 @@ import { initializeAgentExecutorWithOptions, AgentExecutor, InitializeAgentExecu import { Tool } from 'langchain/tools' import { BaseChatMemory } from 'langchain/memory' import { getBaseClasses, mapChatHistory } from '../../../src/utils' -import { BaseLanguageModel } from 'langchain/base_language' +import { BaseChatModel } from 'langchain/chat_models/base' import { flatten } from 'lodash' import { additionalCallbacks } from '../../../src/handler' @@ -29,7 +29,7 @@ class ConversationalAgent_Agents implements INode { constructor() { this.label = 'Conversational Agent' this.name = 'conversationalAgent' - this.version = 1.0 + this.version = 2.0 this.type = 'AgentExecutor' this.category = 'Agents' this.icon = 'agent.svg' @@ -45,7 +45,7 @@ class ConversationalAgent_Agents implements INode { { label: 'Language Model', name: 'model', - type: 'BaseLanguageModel' + type: 'BaseChatModel' }, { label: 'Memory', @@ -65,7 +65,7 @@ class ConversationalAgent_Agents implements INode { } async init(nodeData: INodeData): Promise { - const model = nodeData.inputs?.model as BaseLanguageModel + const model = nodeData.inputs?.model as BaseChatModel let tools = nodeData.inputs?.tools as Tool[] tools = flatten(tools) const memory = nodeData.inputs?.memory as BaseChatMemory @@ -92,8 +92,6 @@ class ConversationalAgent_Agents implements INode { const executor = nodeData.instance as AgentExecutor const memory = nodeData.inputs?.memory as BaseChatMemory - const callbacks = await additionalCallbacks(nodeData, options) - if (options && options.chatHistory) { const chatHistoryClassName = memory.chatHistory.constructor.name // Only replace when its In-Memory @@ -103,6 +101,10 @@ class ConversationalAgent_Agents implements INode { } } + ;(executor.memory as any).returnMessages = true // Return true for BaseChatModel + + const callbacks = await additionalCallbacks(nodeData, options) + const result = await executor.call({ input }, [...callbacks]) return result?.output } diff --git a/packages/components/nodes/agents/ConversationalRetrievalAgent/ConversationalRetrievalAgent.ts b/packages/components/nodes/agents/ConversationalRetrievalAgent/ConversationalRetrievalAgent.ts index 7b71cb5f..3c956833 100644 --- a/packages/components/nodes/agents/ConversationalRetrievalAgent/ConversationalRetrievalAgent.ts +++ b/packages/components/nodes/agents/ConversationalRetrievalAgent/ConversationalRetrievalAgent.ts @@ -82,6 +82,8 @@ class ConversationalRetrievalAgent_Agents implements INode { if (executor.memory) { ;(executor.memory as any).memoryKey = 'chat_history' ;(executor.memory as any).outputKey = 'output' + ;(executor.memory as any).returnMessages = true + const chatHistoryClassName = (executor.memory as any).chatHistory.constructor.name // Only replace when its In-Memory if (chatHistoryClassName && chatHistoryClassName === 'ChatMessageHistory') { diff --git a/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts b/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts index ce6f576f..0f5b9aec 100644 --- a/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts +++ b/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts @@ -87,6 +87,8 @@ class OpenAIFunctionAgent_Agents implements INode { } } + ;(executor.memory as any).returnMessages = true // Return true for BaseChatModel + const loggerHandler = new ConsoleCallbackHandler(options.logger) const callbacks = await additionalCallbacks(nodeData, options) diff --git a/packages/components/nodes/chains/ConversationChain/ConversationChain.ts b/packages/components/nodes/chains/ConversationChain/ConversationChain.ts index 92a0b5ea..7887ce97 100644 --- a/packages/components/nodes/chains/ConversationChain/ConversationChain.ts +++ b/packages/components/nodes/chains/ConversationChain/ConversationChain.ts @@ -106,16 +106,18 @@ class ConversationChain_Chains implements INode { async run(nodeData: INodeData, input: string, options: ICommonObject): Promise { const chain = nodeData.instance as ConversationChain const memory = nodeData.inputs?.memory as BufferMemory + memory.returnMessages = true // Return true for BaseChatModel if (options && options.chatHistory) { const chatHistoryClassName = memory.chatHistory.constructor.name // Only replace when its In-Memory if (chatHistoryClassName && chatHistoryClassName === 'ChatMessageHistory') { memory.chatHistory = mapChatHistory(options) - chain.memory = memory } } + chain.memory = memory + const loggerHandler = new ConsoleCallbackHandler(options.logger) const callbacks = await additionalCallbacks(nodeData, options) diff --git a/packages/components/nodes/memory/DynamoDb/DynamoDb.ts b/packages/components/nodes/memory/DynamoDb/DynamoDb.ts index 68b09b7b..ac4f7602 100644 --- a/packages/components/nodes/memory/DynamoDb/DynamoDb.ts +++ b/packages/components/nodes/memory/DynamoDb/DynamoDb.ts @@ -109,9 +109,8 @@ const initalizeDynamoDB = async (nodeData: INodeData, options: ICommonObject): P }) const memory = new BufferMemoryExtended({ - memoryKey, + memoryKey: memoryKey ?? 'chat_history', chatHistory: dynamoDb, - returnMessages: true, isSessionIdUsingChatMessageId }) return memory diff --git a/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts b/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts index 7de2ec34..6f800cdc 100644 --- a/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts +++ b/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts @@ -123,9 +123,8 @@ const initializeMongoDB = async (nodeData: INodeData, options: ICommonObject): P } return new BufferMemoryExtended({ - memoryKey, + memoryKey: memoryKey ?? 'chat_history', chatHistory: mongoDBChatMessageHistory, - returnMessages: true, isSessionIdUsingChatMessageId }) } diff --git a/packages/components/nodes/memory/RedisBackedChatMemory/RedisBackedChatMemory.ts b/packages/components/nodes/memory/RedisBackedChatMemory/RedisBackedChatMemory.ts index c65d729b..bdb62911 100644 --- a/packages/components/nodes/memory/RedisBackedChatMemory/RedisBackedChatMemory.ts +++ b/packages/components/nodes/memory/RedisBackedChatMemory/RedisBackedChatMemory.ts @@ -137,7 +137,7 @@ const initalizeRedis = async (nodeData: INodeData, options: ICommonObject): Prom } const memory = new BufferMemoryExtended({ - memoryKey, + memoryKey: memoryKey ?? 'chat_history', chatHistory: redisChatMessageHistory, isSessionIdUsingChatMessageId }) diff --git a/packages/components/nodes/memory/UpstashRedisBackedChatMemory/UpstashRedisBackedChatMemory.ts b/packages/components/nodes/memory/UpstashRedisBackedChatMemory/UpstashRedisBackedChatMemory.ts index 6b5fdf66..2b8b4650 100644 --- a/packages/components/nodes/memory/UpstashRedisBackedChatMemory/UpstashRedisBackedChatMemory.ts +++ b/packages/components/nodes/memory/UpstashRedisBackedChatMemory/UpstashRedisBackedChatMemory.ts @@ -95,6 +95,7 @@ const initalizeUpstashRedis = async (nodeData: INodeData, options: ICommonObject }) const memory = new BufferMemoryExtended({ + memoryKey: 'chat_history', chatHistory: redisChatMessageHistory, isSessionIdUsingChatMessageId }) diff --git a/packages/components/nodes/moderation/OpenAIModeration/OpenAIModeration.ts b/packages/components/nodes/moderation/OpenAIModeration/OpenAIModeration.ts index 51578630..85b27907 100644 --- a/packages/components/nodes/moderation/OpenAIModeration/OpenAIModeration.ts +++ b/packages/components/nodes/moderation/OpenAIModeration/OpenAIModeration.ts @@ -20,7 +20,7 @@ class OpenAIModeration implements INode { this.name = 'inputModerationOpenAI' this.version = 1.0 this.type = 'Moderation' - this.icon = 'openai-moderation.png' + this.icon = 'openai.png' this.category = 'Moderation' this.description = 'Check whether content complies with OpenAI usage policies.' this.baseClasses = [this.type, ...getBaseClasses(Moderation)] diff --git a/packages/components/nodes/moderation/OpenAIModeration/openai-moderation.png b/packages/components/nodes/moderation/OpenAIModeration/openai-moderation.png deleted file mode 100644 index e3b1b282a70fdcbf36da31b2856b7a0ade9c45c5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 47939 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

_wG68x><=35`nmw$-Nua@ojcWD{>y*)FJbQ! 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

I{36t7LlSB{^VRUNy;FO1H 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 P00000NkvXXu0mjf)=Eu{ diff --git a/packages/components/nodes/moderation/OpenAIModeration/openai.png b/packages/components/nodes/moderation/OpenAIModeration/openai.png new file mode 100644 index 0000000000000000000000000000000000000000..de08a05b28979826c4cc669c4899789763a938a1 GIT binary patch literal 3991 zcmV;I4`}d-P)gjhf~eq#s7M4i1UD8XqJW~vA_xftNZ1L8K*&N!(&?H%y1G)Gbam|=aK3jA=g{eX z@7=H7a^Jo8-Gcvf2q9|6MLi;kA{=m2P6cQ2)V1)TAs~(pT*(!*p&9W+1Lr8@H};dw zHug~X$0Z<~j`Sm$E?i7hfWKF8n(eG&Ik{BUEe-a=MOQM|iyKj+xXEW0-3hDfF58I& z#*>GLh)0tE?>F`_krs8`ZF?Zli!3TN1+P64R?{bBi?U;gWH|YTh4+>Hq!L-zB3MBb zV>qpA;HyoBGo%q+*J7AOBx5KtExwO}V$uTc8RtC&hEr%s{OVDVdLga_y~wvLzK??a z^sQ@gj3R+7Tg3NKu$q>k>9{@Whl|Gh`>Pxu-Wg^SlVy}NwlLW4Twghj6#mHhirCmm~&>D3b&!V;S94?d=RK$ zm*UB~=s*f7bfqyD1^9jm$Joe9zT=R}sBsiYlGd-CA*uv8` zKMGwKhufy*PekMhFLJ3|cWa(uw}INL_=N{(5K8gm_}Vt%i};Y<^0aKgz5Hn6RB@I? zbPgQ>S5aV#@Rh7(5HV7%5!}cUN=(wUoD6&QPY6>=!9usII*OSN;4%;Yvb;%^wNdhi0 zr39VgO}gQd>S)X({7RL@S+7<~8R;YeZP;h9LuD+dzpT*K<95F0oFmWPS2oesE^#A? zCxJw|a3xI*65v6kimg0EL#Z|wSMxTf9Ti?g#7&yINO})Ljp#&oy3m&9G$4{NGMHkB zJb^m0!|Z!DK@i3u7IE0@&m-x^1lDq*hLgi9zTOc~NG8|Iib*^p*`&j1 zVploKP_x`!!)y)&T%0EBCZL>e_$&3LI-|ISFDMO}@ZR#C8C!Ep(m9}7r9J{Y#1yf(U)Cd3yJ5ZTF_yw7nmxtCOht;i^v5v`AaL5#Oie195@ zL7;#|%wrb-N14WQ9^!AZwa^&i1CO7YA700^G_+BC^ETRIRx+FQQtXI;h{z7c@EM~? zHZh)}1Di+u324j&5^*AM##oJRe&RKjQw%@^y-8*nKT<^nS#0D^9yJ_OqN4`_PZ%(7 z>DvbLxCDR~b=T`}9)nI~P=LrGCeuOwv<(vto z1WXJ1tvx&=eGlMLUgU^I>-jvZI7)Y55(k5Rzm&U!i(j9`hQ!xPz#dHke&=<%$g{o) zkFi~sdCbj5Mi4LkE{q<$#~Iac@28X20=Q43K_>_(<#TS4BZBI4Cs~HfW2JmfisJVJ zSgo>;Es>AoD!AM53Ec<*0@DLL!A*>mpI{U{SU{n{K8T2%U^Z9CBd8hwBDGL=bYeygf8|aRgNq-z z@iu~PyunFR;){q>@;&#+9)Jk?vY2A&ZyqLT9j4=05h4Q0Si!9UqdXv*ek{|us|PB@ zd`w>=q}pN`!Vgp;lFQ{<6QH392bVqqauozr@r%MJuGW(W`Ne{h?bXV6Z z{&r{`XvjK;2-syh;ITdf_|}4}+}{(S3h(OZ=8Va1I)_r0Fqo& zy~6A2VG=$CA%_x22(SZ{tY$c)*kCd$xE@1UpcXaeBOsdslikxAoYuxb6bT4G5t$4k zoqUt^bZ0Ju11*U@0*>;tsfw#8cTjw|m{)mR+SLy-nSspXw268|+K|DZV2^7kXH9H_ z5!}VvAkmyT7B9mkkV7R|+#wsnjhMk|DoH`3##$LvhGxjWY(W~ijuEg!+STWCjh`88 zn_-1HVANRktSBO$8x6Q1TM$V;B|tIj`4$iD0_a{RSYT;+jb#{3fM~jsLOcg31j^V% z7H4UvW$E>U03;B{sz5F>fOc#)#HN3AZxqRVR?DoCO@amSm0@@uMBHpv7*WFL$s)rF zb1A9n&7~T)3l;G`H^}~_ct+F+eX);#Y5|mHuxjJE{>iXeJu)el_Y5yCB8Qo(u(-4( zU2#6JN0LVL7MI||o5g<~@ItEG>ph<@M8Z>H5;2sK0QBc#` z*M!GeOmm9_1ov0wO3!k#!JcLYbCiW~kD)o`U-FnJ2SE!YSiA?UMZkWkEu#eF(k@uD z0?7up#M+CDG7R1tT51rm&m;jw+~#c{u;L@Kiu+l}SyP=3<67o0U*YLJ{}AJ|6sv1~ z*^J^bw&KCek)}R(vWRJ1ZaLb-nM*GMiQdN(O!Y11Z3Z%#gC;xCl*jmlCoC?5PNA81 z8PwAK^GIh9nI>(90+%s`4;Uyb%;yiJs4?xsPZb*&#c;k+J3?q6g1)@P8wSndJ?MuE z5FESr744NhH~|-vhzls?Q-&~(Y|L3`8!&_qd0r9Z6b$W2XEC>!XvYY2MUADA!!ruC zu_G^W)YR7KyD##vZr4}_0?WBm#oRmzyVGg?7}~Bv~CU<`e#`@VgFXorz1$zH*YebeC)Mwbq?C{es?{ zCg5Ey9W}9r4t9*0ia47VJgG4_gG~mJb$<7|+cL3M`X#3cn5Z=YM)~>Wynfdl#jY;U znOFJE1O*|#*1Q4c9YH(zhwl4B(;OvWvDK(CB0O^{~(@5xY5g*h@jj>*H7jcq+Y%QmGGz)Z9q$hN_ zf;9@`d8F>t7%w?SfQRR_qsCV1t}a;UvWK0Fm92sTNxaN)o%MPN(7K&&hJc+~3m`P& zM?*s@aOmCZBf=`GoXjf-)XBYs3!7yk^;GpBY2>%at5u-8(9;q>MP)7PEdY~(e* z0I(?myTE>)W1+sQvtDFVV$qOkR{XuZ!vZb&D!V6f?^G5?ug(>yjyw|PvxWLQkFqzl%f#=ONpHAVts zCG(iIQtoWFNaZLow*`7JpLxvr%hcIh#i>4)=Ng|Qv#1oA` zIYpcxpKP{sKuUt;!&NNd66{UTmd{;mwXr^vh@t_FXi8HW6R&DN2xFp!kea|#?BAi# z0PI6cR@*lJJ&0tT7rq8V=xdWo?Lj1uUUe;waR{W^^eV2?48IUx#RXA}qu3G!9z=>5 za~_A`Yap6&7Dj>h>5sWE-$my`BqI$c?W!($47+fjz7GO@SZ!ictR#zG7v|irjTTIh z{Qi1h%9_Xc3vc5K1{d9!NuI9P^5&62SEtmTx*SsBbfiDYT%r16=2L9vYgUkp+o?{} z{hX@#YHpEo3i*wF>|h&volfvm_XK!x-oBju50C!=Nj{KH?md;N0000bbVXQnWMOn= zI%9HWVRU5xGB7eSEigGPF*Z~%IXW;nIx#mZFfckWFtzvO1ONa4C3HntbYx+4Wjbwd xWNBu305UK#GA%GUEipD!FgZFfI65&mD=;uRFfhcbT(|%L002ovPDHLkV1j%(J<0$8 literal 0 HcmV?d00001 diff --git a/packages/components/package.json b/packages/components/package.json index 5566218c..bb074392 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "flowise-components", - "version": "1.4.3", + "version": "1.4.4", "description": "Flowiseai Components", "main": "dist/src/index", "types": "dist/src/index.d.ts", diff --git a/packages/server/marketplaces/chatflows/API Agent OpenAI.json b/packages/server/marketplaces/chatflows/API Agent OpenAI.json index 4950a6a6..0e9aa091 100644 --- a/packages/server/marketplaces/chatflows/API Agent OpenAI.json +++ b/packages/server/marketplaces/chatflows/API Agent OpenAI.json @@ -3,7 +3,7 @@ "nodes": [ { "width": 300, - "height": 510, + "height": 491, "id": "openApiChain_1", "position": { "x": 1203.1825726424859, @@ -13,8 +13,8 @@ "data": { "id": "openApiChain_1", "label": "OpenAPI Chain", - "name": "openApiChain", "version": 1, + "name": "openApiChain", "type": "OpenAPIChain", "baseClasses": ["OpenAPIChain", "BaseChain"], "category": "Chains", @@ -78,7 +78,7 @@ }, { "width": 300, - "height": 523, + "height": 574, "id": "chatOpenAI_1", "position": { "x": 792.3201947594027, @@ -88,8 +88,8 @@ "data": { "id": "chatOpenAI_1", "label": "ChatOpenAI", - "name": "chatOpenAI", "version": 2, + "name": "chatOpenAI", "type": "ChatOpenAI", "baseClasses": ["ChatOpenAI", "BaseChatModel", "BaseLanguageModel"], "category": "Chat Models", @@ -259,8 +259,8 @@ "data": { "id": "chainTool_0", "label": "Chain Tool", - "name": "chainTool", "version": 1, + "name": "chainTool", "type": "ChainTool", "baseClasses": ["ChainTool", "DynamicTool", "Tool", "StructuredTool"], "category": "Tools", @@ -333,8 +333,8 @@ "data": { "id": "openAIFunctionAgent_0", "label": "OpenAI Function Agent", - "name": "openAIFunctionAgent", "version": 2, + "name": "openAIFunctionAgent", "type": "AgentExecutor", "baseClasses": ["AgentExecutor", "BaseChain"], "category": "Agents", @@ -397,7 +397,7 @@ }, { "width": 300, - "height": 523, + "height": 574, "id": "chatOpenAI_2", "position": { "x": 1645.450699499575, @@ -407,8 +407,8 @@ "data": { "id": "chatOpenAI_2", "label": "ChatOpenAI", - "name": "chatOpenAI", "version": 2, + "name": "chatOpenAI", "type": "ChatOpenAI", "baseClasses": ["ChatOpenAI", "BaseChatModel", "BaseLanguageModel"], "category": "Chat Models", @@ -578,8 +578,8 @@ "data": { "id": "bufferMemory_0", "label": "Buffer Memory", - "name": "bufferMemory", "version": 1, + "name": "bufferMemory", "type": "BufferMemory", "baseClasses": ["BufferMemory", "BaseChatMemory", "BaseMemory"], "category": "Memory", @@ -657,17 +657,6 @@ "label": "" } }, - { - "source": "chatOpenAI_2", - "sourceHandle": "chatOpenAI_2-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel", - "target": "openAIFunctionAgent_0", - "targetHandle": "openAIFunctionAgent_0-input-model-BaseChatModel", - "type": "buttonedge", - "id": "chatOpenAI_2-chatOpenAI_2-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel-openAIFunctionAgent_0-openAIFunctionAgent_0-input-model-BaseChatModel", - "data": { - "label": "" - } - }, { "source": "bufferMemory_0", "sourceHandle": "bufferMemory_0-output-bufferMemory-BufferMemory|BaseChatMemory|BaseMemory", @@ -678,6 +667,17 @@ "data": { "label": "" } + }, + { + "source": "chatOpenAI_2", + "sourceHandle": "chatOpenAI_2-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel", + "target": "openAIFunctionAgent_0", + "targetHandle": "openAIFunctionAgent_0-input-model-ChatOpenAI | AzureChatOpenAI", + "type": "buttonedge", + "id": "chatOpenAI_2-chatOpenAI_2-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel-openAIFunctionAgent_0-openAIFunctionAgent_0-input-model-ChatOpenAI | AzureChatOpenAI", + "data": { + "label": "" + } } ] } diff --git a/packages/server/marketplaces/chatflows/API Agent.json b/packages/server/marketplaces/chatflows/API Agent.json index d8fa22ad..93270848 100644 --- a/packages/server/marketplaces/chatflows/API Agent.json +++ b/packages/server/marketplaces/chatflows/API Agent.json @@ -13,8 +13,8 @@ "data": { "id": "getApiChain_0", "label": "GET API Chain", - "name": "getApiChain", "version": 1, + "name": "getApiChain", "type": "GETApiChain", "baseClasses": ["GETApiChain", "BaseChain", "BaseLangChain"], "category": "Chains", @@ -102,8 +102,8 @@ "data": { "id": "chainTool_0", "label": "Chain Tool", - "name": "chainTool", "version": 1, + "name": "chainTool", "type": "ChainTool", "baseClasses": ["ChainTool", "DynamicTool", "Tool", "StructuredTool", "BaseLangChain"], "category": "Tools", @@ -176,8 +176,8 @@ "data": { "id": "bufferMemory_0", "label": "Buffer Memory", - "name": "bufferMemory", "version": 1, + "name": "bufferMemory", "type": "BufferMemory", "baseClasses": ["BufferMemory", "BaseChatMemory", "BaseMemory"], "category": "Memory", @@ -233,8 +233,8 @@ "data": { "id": "chainTool_1", "label": "Chain Tool", - "name": "chainTool", "version": 1, + "name": "chainTool", "type": "ChainTool", "baseClasses": ["ChainTool", "DynamicTool", "Tool", "StructuredTool", "BaseLangChain"], "category": "Tools", @@ -307,8 +307,8 @@ "data": { "id": "postApiChain_0", "label": "POST API Chain", - "name": "postApiChain", "version": 1, + "name": "postApiChain", "type": "POSTApiChain", "baseClasses": ["POSTApiChain", "BaseChain", "BaseLangChain"], "category": "Chains", @@ -386,7 +386,7 @@ }, { "width": 300, - "height": 523, + "height": 574, "id": "chatOpenAI_2", "position": { "x": 572.8941615312035, @@ -396,8 +396,8 @@ "data": { "id": "chatOpenAI_2", "label": "ChatOpenAI", - "name": "chatOpenAI", "version": 2, + "name": "chatOpenAI", "type": "ChatOpenAI", "baseClasses": ["ChatOpenAI", "BaseChatModel", "BaseLanguageModel"], "category": "Chat Models", @@ -557,7 +557,7 @@ }, { "width": 300, - "height": 523, + "height": 574, "id": "chatOpenAI_1", "position": { "x": 828.7788305309582, @@ -567,8 +567,8 @@ "data": { "id": "chatOpenAI_1", "label": "ChatOpenAI", - "name": "chatOpenAI", "version": 2, + "name": "chatOpenAI", "type": "ChatOpenAI", "baseClasses": ["ChatOpenAI", "BaseChatModel", "BaseLanguageModel"], "category": "Chat Models", @@ -728,7 +728,7 @@ }, { "width": 300, - "height": 523, + "height": 574, "id": "chatOpenAI_3", "position": { "x": 1148.338912314111, @@ -738,8 +738,8 @@ "data": { "id": "chatOpenAI_3", "label": "ChatOpenAI", - "name": "chatOpenAI", "version": 2, + "name": "chatOpenAI", "type": "ChatOpenAI", "baseClasses": ["ChatOpenAI", "BaseChatModel", "BaseLanguageModel"], "category": "Chat Models", @@ -869,7 +869,7 @@ } ], "inputs": { - "modelName": "gpt-3.5-turbo", + "modelName": "gpt-3.5-turbo-16k", "temperature": 0.9, "maxTokens": "", "topP": "", @@ -902,17 +902,17 @@ "height": 383, "id": "conversationalAgent_0", "position": { - "x": 2114.071431691489, - "y": 941.7926368551367 + "x": 2090.570467632979, + "y": 969.5131357270544 }, "type": "customNode", "data": { "id": "conversationalAgent_0", "label": "Conversational Agent", + "version": 2, "name": "conversationalAgent", - "version": 1, "type": "AgentExecutor", - "baseClasses": ["AgentExecutor", "BaseChain"], + "baseClasses": ["AgentExecutor", "BaseChain", "Runnable"], "category": "Agents", "description": "Conversational agent for a chat model. It will utilize chat specific prompts", "inputParams": [ @@ -938,8 +938,8 @@ { "label": "Language Model", "name": "model", - "type": "BaseLanguageModel", - "id": "conversationalAgent_0-input-model-BaseLanguageModel" + "type": "BaseChatModel", + "id": "conversationalAgent_0-input-model-BaseChatModel" }, { "label": "Memory", @@ -956,21 +956,21 @@ }, "outputAnchors": [ { - "id": "conversationalAgent_0-output-conversationalAgent-AgentExecutor|BaseChain", + "id": "conversationalAgent_0-output-conversationalAgent-AgentExecutor|BaseChain|Runnable", "name": "conversationalAgent", "label": "AgentExecutor", - "type": "AgentExecutor | BaseChain" + "type": "AgentExecutor | BaseChain | Runnable" } ], "outputs": {}, "selected": false }, "selected": false, - "dragging": false, "positionAbsolute": { - "x": 2114.071431691489, - "y": 941.7926368551367 - } + "x": 2090.570467632979, + "y": 969.5131357270544 + }, + "dragging": false } ], "edges": [ @@ -1044,9 +1044,9 @@ "source": "chatOpenAI_3", "sourceHandle": "chatOpenAI_3-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel", "target": "conversationalAgent_0", - "targetHandle": "conversationalAgent_0-input-model-BaseLanguageModel", + "targetHandle": "conversationalAgent_0-input-model-BaseChatModel", "type": "buttonedge", - "id": "chatOpenAI_3-chatOpenAI_3-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel-conversationalAgent_0-conversationalAgent_0-input-model-BaseLanguageModel", + "id": "chatOpenAI_3-chatOpenAI_3-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel-conversationalAgent_0-conversationalAgent_0-input-model-BaseChatModel", "data": { "label": "" } diff --git a/packages/server/marketplaces/chatflows/Conversational Agent.json b/packages/server/marketplaces/chatflows/Conversational Agent.json index 2232ade0..8994594a 100644 --- a/packages/server/marketplaces/chatflows/Conversational Agent.json +++ b/packages/server/marketplaces/chatflows/Conversational Agent.json @@ -13,8 +13,8 @@ "data": { "id": "calculator_1", "label": "Calculator", - "name": "calculator", "version": 1, + "name": "calculator", "type": "Calculator", "baseClasses": ["Calculator", "Tool", "StructuredTool", "BaseLangChain"], "category": "Tools", @@ -52,8 +52,8 @@ "data": { "id": "bufferMemory_1", "label": "Buffer Memory", - "name": "bufferMemory", "version": 1, + "name": "bufferMemory", "type": "BufferMemory", "baseClasses": ["BufferMemory", "BaseChatMemory", "BaseMemory"], "category": "Memory", @@ -109,8 +109,8 @@ "data": { "id": "serpAPI_0", "label": "Serp API", - "name": "serpAPI", "version": 1, + "name": "serpAPI", "type": "SerpAPI", "baseClasses": ["SerpAPI", "Tool", "StructuredTool"], "category": "Tools", @@ -146,7 +146,7 @@ }, { "width": 300, - "height": 523, + "height": 574, "id": "chatOpenAI_0", "position": { "x": 97.01321406237057, @@ -156,8 +156,8 @@ "data": { "id": "chatOpenAI_0", "label": "ChatOpenAI", - "name": "chatOpenAI", "version": 2, + "name": "chatOpenAI", "type": "ChatOpenAI", "baseClasses": ["ChatOpenAI", "BaseChatModel", "BaseLanguageModel"], "category": "Chat Models", @@ -287,7 +287,7 @@ } ], "inputs": { - "modelName": "gpt-3.5-turbo", + "modelName": "gpt-3.5-turbo-16k", "temperature": 0.9, "maxTokens": "", "topP": "", @@ -320,17 +320,17 @@ "height": 383, "id": "conversationalAgent_0", "position": { - "x": 1164.4550359451973, - "y": 283.40041124403075 + "x": 1191.1524476753796, + "y": 324.2479396683294 }, "type": "customNode", "data": { "id": "conversationalAgent_0", "label": "Conversational Agent", + "version": 2, "name": "conversationalAgent", - "version": 1, "type": "AgentExecutor", - "baseClasses": ["AgentExecutor", "BaseChain"], + "baseClasses": ["AgentExecutor", "BaseChain", "Runnable"], "category": "Agents", "description": "Conversational agent for a chat model. It will utilize chat specific prompts", "inputParams": [ @@ -356,8 +356,8 @@ { "label": "Language Model", "name": "model", - "type": "BaseLanguageModel", - "id": "conversationalAgent_0-input-model-BaseLanguageModel" + "type": "BaseChatModel", + "id": "conversationalAgent_0-input-model-BaseChatModel" }, { "label": "Memory", @@ -374,10 +374,10 @@ }, "outputAnchors": [ { - "id": "conversationalAgent_0-output-conversationalAgent-AgentExecutor|BaseChain", + "id": "conversationalAgent_0-output-conversationalAgent-AgentExecutor|BaseChain|Runnable", "name": "conversationalAgent", "label": "AgentExecutor", - "type": "AgentExecutor | BaseChain" + "type": "AgentExecutor | BaseChain | Runnable" } ], "outputs": {}, @@ -385,8 +385,8 @@ }, "selected": false, "positionAbsolute": { - "x": 1164.4550359451973, - "y": 283.40041124403075 + "x": 1191.1524476753796, + "y": 324.2479396683294 }, "dragging": false } @@ -418,9 +418,9 @@ "source": "chatOpenAI_0", "sourceHandle": "chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel", "target": "conversationalAgent_0", - "targetHandle": "conversationalAgent_0-input-model-BaseLanguageModel", + "targetHandle": "conversationalAgent_0-input-model-BaseChatModel", "type": "buttonedge", - "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel-conversationalAgent_0-conversationalAgent_0-input-model-BaseLanguageModel", + "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel-conversationalAgent_0-conversationalAgent_0-input-model-BaseChatModel", "data": { "label": "" } diff --git a/packages/server/marketplaces/chatflows/Conversational Retrieval Agent.json b/packages/server/marketplaces/chatflows/Conversational Retrieval Agent.json index 800ae300..68618a63 100644 --- a/packages/server/marketplaces/chatflows/Conversational Retrieval Agent.json +++ b/packages/server/marketplaces/chatflows/Conversational Retrieval Agent.json @@ -642,9 +642,9 @@ "source": "chatOpenAI_0", "sourceHandle": "chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable", "target": "conversationalRetrievalAgent_0", - "targetHandle": "conversationalRetrievalAgent_0-input-model-ChatOpenAI", + "targetHandle": "conversationalRetrievalAgent_0-input-model-ChatOpenAI | AzureChatOpenAI", "type": "buttonedge", - "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable-conversationalRetrievalAgent_0-conversationalRetrievalAgent_0-input-model-ChatOpenAI", + "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable-conversationalRetrievalAgent_0-conversationalRetrievalAgent_0-input-model-ChatOpenAI | AzureChatOpenAI", "data": { "label": "" } diff --git a/packages/server/marketplaces/chatflows/Multiple VectorDB.json b/packages/server/marketplaces/chatflows/Multiple VectorDB.json index e7718616..789b0c08 100644 --- a/packages/server/marketplaces/chatflows/Multiple VectorDB.json +++ b/packages/server/marketplaces/chatflows/Multiple VectorDB.json @@ -1127,81 +1127,6 @@ }, "dragging": false }, - { - "width": 300, - "height": 383, - "id": "conversationalAgent_0", - "position": { - "x": 2506.011817109287, - "y": -241.58006840004734 - }, - "type": "customNode", - "data": { - "id": "conversationalAgent_0", - "label": "Conversational Agent", - "version": 1, - "name": "conversationalAgent", - "type": "AgentExecutor", - "baseClasses": ["AgentExecutor", "BaseChain", "Runnable"], - "category": "Agents", - "description": "Conversational agent for a chat model. It will utilize chat specific prompts", - "inputParams": [ - { - "label": "System Message", - "name": "systemMessage", - "type": "string", - "rows": 4, - "default": "Assistant is a large language model trained by OpenAI.\n\nAssistant is designed to be able to assist with a wide range of tasks, from answering simple questions to providing in-depth explanations and discussions on a wide range of topics. As a language model, Assistant is able to generate human-like text based on the input it receives, allowing it to engage in natural-sounding conversations and provide responses that are coherent and relevant to the topic at hand.\n\nAssistant is constantly learning and improving, and its capabilities are constantly evolving. It is able to process and understand large amounts of text, and can use this knowledge to provide accurate and informative responses to a wide range of questions. Additionally, Assistant is able to generate its own text based on the input it receives, allowing it to engage in discussions and provide explanations and descriptions on a wide range of topics.\n\nOverall, Assistant is a powerful system that can help with a wide range of tasks and provide valuable insights and information on a wide range of topics. Whether you need help with a specific question or just want to have a conversation about a particular topic, Assistant is here to assist.", - "optional": true, - "additionalParams": true, - "id": "conversationalAgent_0-input-systemMessage-string" - } - ], - "inputAnchors": [ - { - "label": "Allowed Tools", - "name": "tools", - "type": "Tool", - "list": true, - "id": "conversationalAgent_0-input-tools-Tool" - }, - { - "label": "Language Model", - "name": "model", - "type": "BaseLanguageModel", - "id": "conversationalAgent_0-input-model-BaseLanguageModel" - }, - { - "label": "Memory", - "name": "memory", - "type": "BaseChatMemory", - "id": "conversationalAgent_0-input-memory-BaseChatMemory" - } - ], - "inputs": { - "tools": ["{{chainTool_2.data.instance}}", "{{chainTool_3.data.instance}}"], - "model": "{{chatOpenAI_2.data.instance}}", - "memory": "{{bufferMemory_0.data.instance}}", - "systemMessage": "Assistant is a large language model trained by OpenAI.\n\nAssistant is designed to be able to assist with a wide range of tasks, from answering simple questions to providing in-depth explanations and discussions on a wide range of topics. As a language model, Assistant is able to generate human-like text based on the input it receives, allowing it to engage in natural-sounding conversations and provide responses that are coherent and relevant to the topic at hand.\n\nAssistant is constantly learning and improving, and its capabilities are constantly evolving. It is able to process and understand large amounts of text, and can use this knowledge to provide accurate and informative responses to a wide range of questions. Additionally, Assistant is able to generate its own text based on the input it receives, allowing it to engage in discussions and provide explanations and descriptions on a wide range of topics.\n\nOverall, Assistant is a powerful system that can help with a wide range of tasks and provide valuable insights and information on a wide range of topics. Whether you need help with a specific question or just want to have a conversation about a particular topic, Assistant is here to assist." - }, - "outputAnchors": [ - { - "id": "conversationalAgent_0-output-conversationalAgent-AgentExecutor|BaseChain|Runnable", - "name": "conversationalAgent", - "label": "AgentExecutor", - "type": "AgentExecutor | BaseChain | Runnable" - } - ], - "outputs": {}, - "selected": false - }, - "selected": false, - "positionAbsolute": { - "x": 2506.011817109287, - "y": -241.58006840004734 - }, - "dragging": false - }, { "width": 300, "height": 574, @@ -1602,6 +1527,81 @@ "y": 75.96855802341503 }, "dragging": false + }, + { + "width": 300, + "height": 383, + "id": "conversationalAgent_0", + "position": { + "x": 2432.125364763489, + "y": -105.27942167533908 + }, + "type": "customNode", + "data": { + "id": "conversationalAgent_0", + "label": "Conversational Agent", + "version": 2, + "name": "conversationalAgent", + "type": "AgentExecutor", + "baseClasses": ["AgentExecutor", "BaseChain", "Runnable"], + "category": "Agents", + "description": "Conversational agent for a chat model. It will utilize chat specific prompts", + "inputParams": [ + { + "label": "System Message", + "name": "systemMessage", + "type": "string", + "rows": 4, + "default": "Assistant is a large language model trained by OpenAI.\n\nAssistant is designed to be able to assist with a wide range of tasks, from answering simple questions to providing in-depth explanations and discussions on a wide range of topics. As a language model, Assistant is able to generate human-like text based on the input it receives, allowing it to engage in natural-sounding conversations and provide responses that are coherent and relevant to the topic at hand.\n\nAssistant is constantly learning and improving, and its capabilities are constantly evolving. It is able to process and understand large amounts of text, and can use this knowledge to provide accurate and informative responses to a wide range of questions. Additionally, Assistant is able to generate its own text based on the input it receives, allowing it to engage in discussions and provide explanations and descriptions on a wide range of topics.\n\nOverall, Assistant is a powerful system that can help with a wide range of tasks and provide valuable insights and information on a wide range of topics. Whether you need help with a specific question or just want to have a conversation about a particular topic, Assistant is here to assist.", + "optional": true, + "additionalParams": true, + "id": "conversationalAgent_0-input-systemMessage-string" + } + ], + "inputAnchors": [ + { + "label": "Allowed Tools", + "name": "tools", + "type": "Tool", + "list": true, + "id": "conversationalAgent_0-input-tools-Tool" + }, + { + "label": "Language Model", + "name": "model", + "type": "BaseChatModel", + "id": "conversationalAgent_0-input-model-BaseChatModel" + }, + { + "label": "Memory", + "name": "memory", + "type": "BaseChatMemory", + "id": "conversationalAgent_0-input-memory-BaseChatMemory" + } + ], + "inputs": { + "tools": ["{{chainTool_2.data.instance}}", "{{chainTool_3.data.instance}}"], + "model": "{{chatOpenAI_2.data.instance}}", + "memory": "{{bufferMemory_0.data.instance}}", + "systemMessage": "Assistant is a large language model trained by OpenAI.\n\nAssistant is designed to be able to assist with a wide range of tasks, from answering simple questions to providing in-depth explanations and discussions on a wide range of topics. As a language model, Assistant is able to generate human-like text based on the input it receives, allowing it to engage in natural-sounding conversations and provide responses that are coherent and relevant to the topic at hand.\n\nAssistant is constantly learning and improving, and its capabilities are constantly evolving. It is able to process and understand large amounts of text, and can use this knowledge to provide accurate and informative responses to a wide range of questions. Additionally, Assistant is able to generate its own text based on the input it receives, allowing it to engage in discussions and provide explanations and descriptions on a wide range of topics.\n\nOverall, Assistant is a powerful system that can help with a wide range of tasks and provide valuable insights and information on a wide range of topics. Whether you need help with a specific question or just want to have a conversation about a particular topic, Assistant is here to assist." + }, + "outputAnchors": [ + { + "id": "conversationalAgent_0-output-conversationalAgent-AgentExecutor|BaseChain|Runnable", + "name": "conversationalAgent", + "label": "AgentExecutor", + "type": "AgentExecutor | BaseChain | Runnable" + } + ], + "outputs": {}, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 2432.125364763489, + "y": -105.27942167533908 + }, + "dragging": false } ], "edges": [ @@ -1704,6 +1704,28 @@ "label": "" } }, + { + "source": "plainText_1", + "sourceHandle": "plainText_1-output-document-Document", + "target": "faiss_0", + "targetHandle": "faiss_0-input-document-Document", + "type": "buttonedge", + "id": "plainText_1-plainText_1-output-document-Document-faiss_0-faiss_0-input-document-Document", + "data": { + "label": "" + } + }, + { + "source": "recursiveCharacterTextSplitter_0", + "sourceHandle": "recursiveCharacterTextSplitter_0-output-recursiveCharacterTextSplitter-RecursiveCharacterTextSplitter|TextSplitter|BaseDocumentTransformer|Runnable", + "target": "plainText_1", + "targetHandle": "plainText_1-input-textSplitter-TextSplitter", + "type": "buttonedge", + "id": "recursiveCharacterTextSplitter_0-recursiveCharacterTextSplitter_0-output-recursiveCharacterTextSplitter-RecursiveCharacterTextSplitter|TextSplitter|BaseDocumentTransformer|Runnable-plainText_1-plainText_1-input-textSplitter-TextSplitter", + "data": { + "label": "" + } + }, { "source": "chainTool_2", "sourceHandle": "chainTool_2-output-chainTool-ChainTool|DynamicTool|Tool|StructuredTool|BaseLangChain", @@ -1730,9 +1752,9 @@ "source": "chatOpenAI_2", "sourceHandle": "chatOpenAI_2-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable", "target": "conversationalAgent_0", - "targetHandle": "conversationalAgent_0-input-model-BaseLanguageModel", + "targetHandle": "conversationalAgent_0-input-model-BaseChatModel", "type": "buttonedge", - "id": "chatOpenAI_2-chatOpenAI_2-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable-conversationalAgent_0-conversationalAgent_0-input-model-BaseLanguageModel", + "id": "chatOpenAI_2-chatOpenAI_2-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable-conversationalAgent_0-conversationalAgent_0-input-model-BaseChatModel", "data": { "label": "" } @@ -1747,28 +1769,6 @@ "data": { "label": "" } - }, - { - "source": "plainText_1", - "sourceHandle": "plainText_1-output-document-Document", - "target": "faiss_0", - "targetHandle": "faiss_0-input-document-Document", - "type": "buttonedge", - "id": "plainText_1-plainText_1-output-document-Document-faiss_0-faiss_0-input-document-Document", - "data": { - "label": "" - } - }, - { - "source": "recursiveCharacterTextSplitter_0", - "sourceHandle": "recursiveCharacterTextSplitter_0-output-recursiveCharacterTextSplitter-RecursiveCharacterTextSplitter|TextSplitter|BaseDocumentTransformer|Runnable", - "target": "plainText_1", - "targetHandle": "plainText_1-input-textSplitter-TextSplitter", - "type": "buttonedge", - "id": "recursiveCharacterTextSplitter_0-recursiveCharacterTextSplitter_0-output-recursiveCharacterTextSplitter-RecursiveCharacterTextSplitter|TextSplitter|BaseDocumentTransformer|Runnable-plainText_1-plainText_1-input-textSplitter-TextSplitter", - "data": { - "label": "" - } } ] } diff --git a/packages/server/marketplaces/chatflows/OpenAI Agent.json b/packages/server/marketplaces/chatflows/OpenAI Agent.json index bc27a9fe..d2c842c6 100644 --- a/packages/server/marketplaces/chatflows/OpenAI Agent.json +++ b/packages/server/marketplaces/chatflows/OpenAI Agent.json @@ -13,8 +13,8 @@ "data": { "id": "calculator_0", "label": "Calculator", - "name": "calculator", "version": 1, + "name": "calculator", "type": "Calculator", "baseClasses": ["Calculator", "Tool", "StructuredTool", "BaseLangChain", "Serializable"], "category": "Tools", @@ -52,8 +52,8 @@ "data": { "id": "bufferMemory_0", "label": "Buffer Memory", - "name": "bufferMemory", "version": 1, + "name": "bufferMemory", "type": "BufferMemory", "baseClasses": ["BufferMemory", "BaseChatMemory", "BaseMemory"], "category": "Memory", @@ -109,8 +109,8 @@ "data": { "id": "customTool_0", "label": "Custom Tool", - "name": "customTool", "version": 1, + "name": "customTool", "type": "CustomTool", "baseClasses": ["CustomTool", "Tool", "StructuredTool"], "category": "Tools", @@ -158,8 +158,8 @@ "data": { "id": "serper_0", "label": "Serper", - "name": "serper", "version": 1, + "name": "serper", "type": "Serper", "baseClasses": ["Serper", "Tool", "StructuredTool"], "category": "Tools", @@ -205,8 +205,8 @@ "data": { "id": "openAIFunctionAgent_0", "label": "OpenAI Function Agent", - "name": "openAIFunctionAgent", "version": 2, + "name": "openAIFunctionAgent", "type": "AgentExecutor", "baseClasses": ["AgentExecutor", "BaseChain"], "category": "Agents", @@ -269,7 +269,7 @@ }, { "width": 300, - "height": 523, + "height": 574, "id": "chatOpenAI_0", "position": { "x": 817.8210275868742, @@ -279,8 +279,8 @@ "data": { "id": "chatOpenAI_0", "label": "ChatOpenAI", - "name": "chatOpenAI", "version": 2, + "name": "chatOpenAI", "type": "ChatOpenAI", "baseClasses": ["ChatOpenAI", "BaseChatModel", "BaseLanguageModel"], "category": "Chat Models", @@ -473,17 +473,6 @@ "label": "" } }, - { - "source": "chatOpenAI_0", - "sourceHandle": "chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel", - "target": "openAIFunctionAgent_0", - "targetHandle": "openAIFunctionAgent_0-input-model-BaseChatModel", - "type": "buttonedge", - "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel-openAIFunctionAgent_0-openAIFunctionAgent_0-input-model-BaseChatModel", - "data": { - "label": "" - } - }, { "source": "bufferMemory_0", "sourceHandle": "bufferMemory_0-output-bufferMemory-BufferMemory|BaseChatMemory|BaseMemory", @@ -494,6 +483,17 @@ "data": { "label": "" } + }, + { + "source": "chatOpenAI_0", + "sourceHandle": "chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel", + "target": "openAIFunctionAgent_0", + "targetHandle": "openAIFunctionAgent_0-input-model-ChatOpenAI | AzureChatOpenAI", + "type": "buttonedge", + "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel-openAIFunctionAgent_0-openAIFunctionAgent_0-input-model-ChatOpenAI | AzureChatOpenAI", + "data": { + "label": "" + } } ] } diff --git a/packages/server/marketplaces/chatflows/WebBrowser.json b/packages/server/marketplaces/chatflows/WebBrowser.json index 2f6fb721..0547366a 100644 --- a/packages/server/marketplaces/chatflows/WebBrowser.json +++ b/packages/server/marketplaces/chatflows/WebBrowser.json @@ -13,8 +13,8 @@ "data": { "id": "bufferMemory_0", "label": "Buffer Memory", - "name": "bufferMemory", "version": 1, + "name": "bufferMemory", "type": "BufferMemory", "baseClasses": ["BufferMemory", "BaseChatMemory", "BaseMemory"], "category": "Memory", @@ -70,8 +70,8 @@ "data": { "id": "webBrowser_0", "label": "Web Browser", - "name": "webBrowser", "version": 1, + "name": "webBrowser", "type": "WebBrowser", "baseClasses": ["WebBrowser", "Tool", "StructuredTool", "BaseLangChain"], "category": "Tools", @@ -115,82 +115,7 @@ }, { "width": 300, - "height": 383, - "id": "conversationalAgent_0", - "position": { - "x": 1464.513303631911, - "y": 155.73036805253955 - }, - "type": "customNode", - "data": { - "id": "conversationalAgent_0", - "label": "Conversational Agent", - "name": "conversationalAgent", - "version": 1, - "type": "AgentExecutor", - "baseClasses": ["AgentExecutor", "BaseChain"], - "category": "Agents", - "description": "Conversational agent for a chat model. It will utilize chat specific prompts", - "inputParams": [ - { - "label": "System Message", - "name": "systemMessage", - "type": "string", - "rows": 4, - "default": "Assistant is a large language model trained by OpenAI.\n\nAssistant is designed to be able to assist with a wide range of tasks, from answering simple questions to providing in-depth explanations and discussions on a wide range of topics. As a language model, Assistant is able to generate human-like text based on the input it receives, allowing it to engage in natural-sounding conversations and provide responses that are coherent and relevant to the topic at hand.\n\nAssistant is constantly learning and improving, and its capabilities are constantly evolving. It is able to process and understand large amounts of text, and can use this knowledge to provide accurate and informative responses to a wide range of questions. Additionally, Assistant is able to generate its own text based on the input it receives, allowing it to engage in discussions and provide explanations and descriptions on a wide range of topics.\n\nOverall, Assistant is a powerful system that can help with a wide range of tasks and provide valuable insights and information on a wide range of topics. Whether you need help with a specific question or just want to have a conversation about a particular topic, Assistant is here to assist.", - "optional": true, - "additionalParams": true, - "id": "conversationalAgent_0-input-systemMessage-string" - } - ], - "inputAnchors": [ - { - "label": "Allowed Tools", - "name": "tools", - "type": "Tool", - "list": true, - "id": "conversationalAgent_0-input-tools-Tool" - }, - { - "label": "Language Model", - "name": "model", - "type": "BaseLanguageModel", - "id": "conversationalAgent_0-input-model-BaseLanguageModel" - }, - { - "label": "Memory", - "name": "memory", - "type": "BaseChatMemory", - "id": "conversationalAgent_0-input-memory-BaseChatMemory" - } - ], - "inputs": { - "tools": ["{{webBrowser_0.data.instance}}"], - "model": "{{chatOpenAI_1.data.instance}}", - "memory": "{{bufferMemory_0.data.instance}}", - "systemMessage": "Assistant is a large language model trained by OpenAI.\n\nAssistant is designed to be able to assist with a wide range of tasks, from answering simple questions to providing in-depth explanations and discussions on a wide range of topics. As a language model, Assistant is able to generate human-like text based on the input it receives, allowing it to engage in natural-sounding conversations and provide responses that are coherent and relevant to the topic at hand.\n\nAssistant is constantly learning and improving, and its capabilities are constantly evolving. It is able to process and understand large amounts of text, and can use this knowledge to provide accurate and informative responses to a wide range of questions. Additionally, Assistant is able to generate its own text based on the input it receives, allowing it to engage in discussions and provide explanations and descriptions on a wide range of topics.\n\nOverall, Assistant is a powerful system that can help with a wide range of tasks and provide valuable insights and information on a wide range of topics. Whether you need help with a specific question or just want to have a conversation about a particular topic, Assistant is here to assist." - }, - "outputAnchors": [ - { - "id": "conversationalAgent_0-output-conversationalAgent-AgentExecutor|BaseChain", - "name": "conversationalAgent", - "label": "AgentExecutor", - "type": "AgentExecutor | BaseChain" - } - ], - "outputs": {}, - "selected": false - }, - "selected": false, - "positionAbsolute": { - "x": 1464.513303631911, - "y": 155.73036805253955 - }, - "dragging": false - }, - { - "width": 300, - "height": 523, + "height": 574, "id": "chatOpenAI_0", "position": { "x": 734.7477982032904, @@ -200,8 +125,8 @@ "data": { "id": "chatOpenAI_0", "label": "ChatOpenAI", - "name": "chatOpenAI", "version": 2, + "name": "chatOpenAI", "type": "ChatOpenAI", "baseClasses": ["ChatOpenAI", "BaseChatModel", "BaseLanguageModel"], "category": "Chat Models", @@ -371,8 +296,8 @@ "data": { "id": "openAIEmbeddings_0", "label": "OpenAI Embeddings", - "name": "openAIEmbeddings", "version": 1, + "name": "openAIEmbeddings", "type": "OpenAIEmbeddings", "baseClasses": ["OpenAIEmbeddings", "Embeddings"], "category": "Embeddings", @@ -445,7 +370,7 @@ }, { "width": 300, - "height": 523, + "height": 574, "id": "chatOpenAI_1", "position": { "x": 68.312124033115, @@ -455,8 +380,8 @@ "data": { "id": "chatOpenAI_1", "label": "ChatOpenAI", - "name": "chatOpenAI", "version": 2, + "name": "chatOpenAI", "type": "ChatOpenAI", "baseClasses": ["ChatOpenAI", "BaseChatModel", "BaseLanguageModel"], "category": "Chat Models", @@ -586,7 +511,7 @@ } ], "inputs": { - "modelName": "gpt-3.5-turbo", + "modelName": "gpt-3.5-turbo-16k", "temperature": 0.9, "maxTokens": "", "topP": "", @@ -613,6 +538,81 @@ "y": -239.65476709991256 }, "dragging": false + }, + { + "width": 300, + "height": 383, + "id": "conversationalAgent_0", + "position": { + "x": 1518.944765840293, + "y": 212.2513364217197 + }, + "type": "customNode", + "data": { + "id": "conversationalAgent_0", + "label": "Conversational Agent", + "version": 2, + "name": "conversationalAgent", + "type": "AgentExecutor", + "baseClasses": ["AgentExecutor", "BaseChain", "Runnable"], + "category": "Agents", + "description": "Conversational agent for a chat model. It will utilize chat specific prompts", + "inputParams": [ + { + "label": "System Message", + "name": "systemMessage", + "type": "string", + "rows": 4, + "default": "Assistant is a large language model trained by OpenAI.\n\nAssistant is designed to be able to assist with a wide range of tasks, from answering simple questions to providing in-depth explanations and discussions on a wide range of topics. As a language model, Assistant is able to generate human-like text based on the input it receives, allowing it to engage in natural-sounding conversations and provide responses that are coherent and relevant to the topic at hand.\n\nAssistant is constantly learning and improving, and its capabilities are constantly evolving. It is able to process and understand large amounts of text, and can use this knowledge to provide accurate and informative responses to a wide range of questions. Additionally, Assistant is able to generate its own text based on the input it receives, allowing it to engage in discussions and provide explanations and descriptions on a wide range of topics.\n\nOverall, Assistant is a powerful system that can help with a wide range of tasks and provide valuable insights and information on a wide range of topics. Whether you need help with a specific question or just want to have a conversation about a particular topic, Assistant is here to assist.", + "optional": true, + "additionalParams": true, + "id": "conversationalAgent_0-input-systemMessage-string" + } + ], + "inputAnchors": [ + { + "label": "Allowed Tools", + "name": "tools", + "type": "Tool", + "list": true, + "id": "conversationalAgent_0-input-tools-Tool" + }, + { + "label": "Language Model", + "name": "model", + "type": "BaseChatModel", + "id": "conversationalAgent_0-input-model-BaseChatModel" + }, + { + "label": "Memory", + "name": "memory", + "type": "BaseChatMemory", + "id": "conversationalAgent_0-input-memory-BaseChatMemory" + } + ], + "inputs": { + "tools": ["{{webBrowser_0.data.instance}}"], + "model": "{{chatOpenAI_1.data.instance}}", + "memory": "{{bufferMemory_0.data.instance}}", + "systemMessage": "Assistant is a large language model trained by OpenAI.\n\nAssistant is designed to be able to assist with a wide range of tasks, from answering simple questions to providing in-depth explanations and discussions on a wide range of topics. As a language model, Assistant is able to generate human-like text based on the input it receives, allowing it to engage in natural-sounding conversations and provide responses that are coherent and relevant to the topic at hand.\n\nAssistant is constantly learning and improving, and its capabilities are constantly evolving. It is able to process and understand large amounts of text, and can use this knowledge to provide accurate and informative responses to a wide range of questions. Additionally, Assistant is able to generate its own text based on the input it receives, allowing it to engage in discussions and provide explanations and descriptions on a wide range of topics.\n\nOverall, Assistant is a powerful system that can help with a wide range of tasks and provide valuable insights and information on a wide range of topics. Whether you need help with a specific question or just want to have a conversation about a particular topic, Assistant is here to assist." + }, + "outputAnchors": [ + { + "id": "conversationalAgent_0-output-conversationalAgent-AgentExecutor|BaseChain|Runnable", + "name": "conversationalAgent", + "label": "AgentExecutor", + "type": "AgentExecutor | BaseChain | Runnable" + } + ], + "outputs": {}, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 1518.944765840293, + "y": 212.2513364217197 + }, + "dragging": false } ], "edges": [ @@ -638,17 +638,6 @@ "label": "" } }, - { - "source": "chatOpenAI_1", - "sourceHandle": "chatOpenAI_1-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel", - "target": "conversationalAgent_0", - "targetHandle": "conversationalAgent_0-input-model-BaseLanguageModel", - "type": "buttonedge", - "id": "chatOpenAI_1-chatOpenAI_1-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel-conversationalAgent_0-conversationalAgent_0-input-model-BaseLanguageModel", - "data": { - "label": "" - } - }, { "source": "webBrowser_0", "sourceHandle": "webBrowser_0-output-webBrowser-WebBrowser|Tool|StructuredTool|BaseLangChain", @@ -660,6 +649,17 @@ "label": "" } }, + { + "source": "chatOpenAI_1", + "sourceHandle": "chatOpenAI_1-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel", + "target": "conversationalAgent_0", + "targetHandle": "conversationalAgent_0-input-model-BaseChatModel", + "type": "buttonedge", + "id": "chatOpenAI_1-chatOpenAI_1-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel-conversationalAgent_0-conversationalAgent_0-input-model-BaseChatModel", + "data": { + "label": "" + } + }, { "source": "bufferMemory_0", "sourceHandle": "bufferMemory_0-output-bufferMemory-BufferMemory|BaseChatMemory|BaseMemory", From 887b003cdd9bfdd9a064bf65879ef4322b9d47db Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Tue, 28 Nov 2023 20:53:33 +0530 Subject: [PATCH 63/64] Bugfix: Simple Moderation, making the checks case-insensitive --- .../SimplePromptModeration/SimplePromptModerationRunner.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/nodes/moderation/SimplePromptModeration/SimplePromptModerationRunner.ts b/packages/components/nodes/moderation/SimplePromptModeration/SimplePromptModerationRunner.ts index 94967ba2..08f9ed1e 100644 --- a/packages/components/nodes/moderation/SimplePromptModeration/SimplePromptModerationRunner.ts +++ b/packages/components/nodes/moderation/SimplePromptModeration/SimplePromptModerationRunner.ts @@ -14,7 +14,7 @@ export class SimplePromptModerationRunner implements Moderation { async checkForViolations(input: string): Promise { this.denyList.split('\n').forEach((denyListItem) => { - if (denyListItem && denyListItem !== '' && input.includes(denyListItem)) { + if (denyListItem && denyListItem !== '' && input.toLowerCase().includes(denyListItem.toLowerCase())) { throw Error(this.moderationErrorMessage) } }) From 549a68d118aa0ba1fa79d2cc0ead227813731988 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Tue, 28 Nov 2023 20:58:39 +0530 Subject: [PATCH 64/64] Bugfix: UI Fix - left align long chat flow names for consistency. --- packages/ui/src/ui-component/table/FlowListTable.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/ui-component/table/FlowListTable.js b/packages/ui/src/ui-component/table/FlowListTable.js index e3baa2e2..358fc965 100644 --- a/packages/ui/src/ui-component/table/FlowListTable.js +++ b/packages/ui/src/ui-component/table/FlowListTable.js @@ -69,7 +69,9 @@ export const FlowListTable = ({ data, images, filterFunction, updateFlowsApi }) - +