From 292b90ccb10b298b3519f3631e48d0d989e66806 Mon Sep 17 00:00:00 2001 From: Avin Date: Thu, 23 Nov 2023 11:37:49 +0530 Subject: [PATCH 01/54] refactor: move api key utilities to seperate file --- packages/server/src/index.ts | 8 +- packages/server/src/utils/apiKey.ts | 147 +++++++++++++++++++++++++ packages/server/src/utils/index.ts | 164 ++-------------------------- 3 files changed, 159 insertions(+), 160 deletions(-) create mode 100644 packages/server/src/utils/apiKey.ts diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 2f7d31e25..7e10c817a 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -31,18 +31,11 @@ import { constructGraphs, resolveVariables, isStartNodeDependOnInput, - getAPIKeys, - addAPIKey, - updateAPIKey, - deleteAPIKey, - compareKeys, mapMimeTypeToInputField, findAvailableConfigs, isSameOverrideConfig, - replaceAllAPIKeys, isFlowValidForStream, databaseEntities, - getApiKey, transformToCredentialEntity, decryptCredentialData, clearAllSessionMemory, @@ -64,6 +57,7 @@ import { ChatflowPool } from './ChatflowPool' import { CachePool } from './CachePool' import { ICommonObject, INodeOptionsValue } from 'flowise-components' import { createRateLimiter, getRateLimiter, initializeRateLimiter } from './utils/rateLimit' +import { addAPIKey, compareKeys, deleteAPIKey, getApiKey, getAPIKeys, replaceAllAPIKeys, updateAPIKey } from './utils/apiKey' export class App { app: express.Application diff --git a/packages/server/src/utils/apiKey.ts b/packages/server/src/utils/apiKey.ts new file mode 100644 index 000000000..08a9ecd37 --- /dev/null +++ b/packages/server/src/utils/apiKey.ts @@ -0,0 +1,147 @@ +import { randomBytes, scryptSync, timingSafeEqual } from 'crypto' +import { ICommonObject } from 'flowise-components' +import moment from 'moment' +import fs from 'fs' +import path from 'path' +import logger from './logger' + +/** + * Returns the api key path + * @returns {string} + */ +export const getAPIKeyPath = (): string => { + return process.env.APIKEY_PATH ? path.join(process.env.APIKEY_PATH, 'api.json') : path.join(__dirname, '..', '..', 'api.json') +} + +/** + * Generate the api key + * @returns {string} + */ +export const generateAPIKey = (): string => { + const buffer = randomBytes(32) + return buffer.toString('base64') +} + +/** + * Generate the secret key + * @param {string} apiKey + * @returns {string} + */ +export const generateSecretHash = (apiKey: string): string => { + const salt = randomBytes(8).toString('hex') + const buffer = scryptSync(apiKey, salt, 64) as Buffer + return `${buffer.toString('hex')}.${salt}` +} + +/** + * Verify valid keys + * @param {string} storedKey + * @param {string} suppliedKey + * @returns {boolean} + */ +export const compareKeys = (storedKey: string, suppliedKey: string): boolean => { + const [hashedPassword, salt] = storedKey.split('.') + const buffer = scryptSync(suppliedKey, salt, 64) as Buffer + return timingSafeEqual(Buffer.from(hashedPassword, 'hex'), buffer) +} + +/** + * Get API keys + * @returns {Promise} + */ +export const getAPIKeys = async (): Promise => { + try { + const content = await fs.promises.readFile(getAPIKeyPath(), 'utf8') + return JSON.parse(content) + } catch (error) { + const keyName = 'DefaultKey' + const apiKey = generateAPIKey() + const apiSecret = generateSecretHash(apiKey) + const content = [ + { + keyName, + apiKey, + apiSecret, + createdAt: moment().format('DD-MMM-YY'), + id: randomBytes(16).toString('hex') + } + ] + await fs.promises.writeFile(getAPIKeyPath(), JSON.stringify(content), 'utf8') + return content + } +} + +/** + * Add new API key + * @param {string} keyName + * @returns {Promise} + */ +export const addAPIKey = async (keyName: string): Promise => { + const existingAPIKeys = await getAPIKeys() + const apiKey = generateAPIKey() + const apiSecret = generateSecretHash(apiKey) + const content = [ + ...existingAPIKeys, + { + keyName, + apiKey, + apiSecret, + createdAt: moment().format('DD-MMM-YY'), + id: randomBytes(16).toString('hex') + } + ] + await fs.promises.writeFile(getAPIKeyPath(), JSON.stringify(content), 'utf8') + return content +} + +/** + * Get API Key details + * @param {string} apiKey + * @returns {Promise} + */ +export const getApiKey = async (apiKey: string) => { + const existingAPIKeys = await getAPIKeys() + const keyIndex = existingAPIKeys.findIndex((key) => key.apiKey === apiKey) + if (keyIndex < 0) return undefined + return existingAPIKeys[keyIndex] +} + +/** + * Update existing API key + * @param {string} keyIdToUpdate + * @param {string} newKeyName + * @returns {Promise} + */ +export const updateAPIKey = async (keyIdToUpdate: string, newKeyName: string): Promise => { + const existingAPIKeys = await getAPIKeys() + const keyIndex = existingAPIKeys.findIndex((key) => key.id === keyIdToUpdate) + if (keyIndex < 0) return [] + existingAPIKeys[keyIndex].keyName = newKeyName + await fs.promises.writeFile(getAPIKeyPath(), JSON.stringify(existingAPIKeys), 'utf8') + return existingAPIKeys +} + +/** + * Delete API key + * @param {string} keyIdToDelete + * @returns {Promise} + */ +export const deleteAPIKey = async (keyIdToDelete: string): Promise => { + const existingAPIKeys = await getAPIKeys() + const result = existingAPIKeys.filter((key) => key.id !== keyIdToDelete) + await fs.promises.writeFile(getAPIKeyPath(), JSON.stringify(result), 'utf8') + return result +} + +/** + * Replace all api keys + * @param {ICommonObject[]} content + * @returns {Promise} + */ +export const replaceAllAPIKeys = async (content: ICommonObject[]): Promise => { + try { + await fs.promises.writeFile(getAPIKeyPath(), JSON.stringify(content), 'utf8') + } catch (error) { + logger.error(error) + } +} diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index 0c6f23624..bc36c78fa 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -1,33 +1,32 @@ import path from 'path' import fs from 'fs' -import moment from 'moment' import logger from './logger' import { + IComponentCredentials, IComponentNodes, + ICredentialDataDecrypted, + ICredentialReqBody, IDepthQueue, IExploredNode, + INodeData, INodeDependencies, INodeDirectedGraph, INodeQueue, + IOverrideConfig, IReactFlowEdge, IReactFlowNode, - IVariableDict, - INodeData, - IOverrideConfig, - ICredentialDataDecrypted, - IComponentCredentials, - ICredentialReqBody + IVariableDict } from '../Interface' import { cloneDeep, get, isEqual } from 'lodash' import { - ICommonObject, + convertChatHistoryToText, getInputVariables, - IDatabaseEntity, handleEscapeCharacters, - IMessage, - convertChatHistoryToText + ICommonObject, + IDatabaseEntity, + IMessage } from 'flowise-components' -import { scryptSync, randomBytes, timingSafeEqual } from 'crypto' +import { randomBytes } from 'crypto' import { AES, enc } from 'crypto-js' import { ChatFlow } from '../database/entities/ChatFlow' @@ -574,147 +573,6 @@ export const isSameOverrideConfig = ( return false } -/** - * Returns the api key path - * @returns {string} - */ -export const getAPIKeyPath = (): string => { - return process.env.APIKEY_PATH ? path.join(process.env.APIKEY_PATH, 'api.json') : path.join(__dirname, '..', '..', 'api.json') -} - -/** - * Generate the api key - * @returns {string} - */ -export const generateAPIKey = (): string => { - const buffer = randomBytes(32) - return buffer.toString('base64') -} - -/** - * Generate the secret key - * @param {string} apiKey - * @returns {string} - */ -export const generateSecretHash = (apiKey: string): string => { - const salt = randomBytes(8).toString('hex') - const buffer = scryptSync(apiKey, salt, 64) as Buffer - return `${buffer.toString('hex')}.${salt}` -} - -/** - * Verify valid keys - * @param {string} storedKey - * @param {string} suppliedKey - * @returns {boolean} - */ -export const compareKeys = (storedKey: string, suppliedKey: string): boolean => { - const [hashedPassword, salt] = storedKey.split('.') - const buffer = scryptSync(suppliedKey, salt, 64) as Buffer - return timingSafeEqual(Buffer.from(hashedPassword, 'hex'), buffer) -} - -/** - * Get API keys - * @returns {Promise} - */ -export const getAPIKeys = async (): Promise => { - try { - const content = await fs.promises.readFile(getAPIKeyPath(), 'utf8') - return JSON.parse(content) - } catch (error) { - const keyName = 'DefaultKey' - const apiKey = generateAPIKey() - const apiSecret = generateSecretHash(apiKey) - const content = [ - { - keyName, - apiKey, - apiSecret, - createdAt: moment().format('DD-MMM-YY'), - id: randomBytes(16).toString('hex') - } - ] - await fs.promises.writeFile(getAPIKeyPath(), JSON.stringify(content), 'utf8') - return content - } -} - -/** - * Add new API key - * @param {string} keyName - * @returns {Promise} - */ -export const addAPIKey = async (keyName: string): Promise => { - const existingAPIKeys = await getAPIKeys() - const apiKey = generateAPIKey() - const apiSecret = generateSecretHash(apiKey) - const content = [ - ...existingAPIKeys, - { - keyName, - apiKey, - apiSecret, - createdAt: moment().format('DD-MMM-YY'), - id: randomBytes(16).toString('hex') - } - ] - await fs.promises.writeFile(getAPIKeyPath(), JSON.stringify(content), 'utf8') - return content -} - -/** - * Get API Key details - * @param {string} apiKey - * @returns {Promise} - */ -export const getApiKey = async (apiKey: string) => { - const existingAPIKeys = await getAPIKeys() - const keyIndex = existingAPIKeys.findIndex((key) => key.apiKey === apiKey) - if (keyIndex < 0) return undefined - return existingAPIKeys[keyIndex] -} - -/** - * Update existing API key - * @param {string} keyIdToUpdate - * @param {string} newKeyName - * @returns {Promise} - */ -export const updateAPIKey = async (keyIdToUpdate: string, newKeyName: string): Promise => { - const existingAPIKeys = await getAPIKeys() - const keyIndex = existingAPIKeys.findIndex((key) => key.id === keyIdToUpdate) - if (keyIndex < 0) return [] - existingAPIKeys[keyIndex].keyName = newKeyName - await fs.promises.writeFile(getAPIKeyPath(), JSON.stringify(existingAPIKeys), 'utf8') - return existingAPIKeys -} - -/** - * Delete API key - * @param {string} keyIdToDelete - * @returns {Promise} - */ -export const deleteAPIKey = async (keyIdToDelete: string): Promise => { - const existingAPIKeys = await getAPIKeys() - const result = existingAPIKeys.filter((key) => key.id !== keyIdToDelete) - await fs.promises.writeFile(getAPIKeyPath(), JSON.stringify(result), 'utf8') - return result -} - -/** - * Replace all api keys - * @param {ICommonObject[]} content - * @returns {Promise} - */ -export const replaceAllAPIKeys = async (content: ICommonObject[]): Promise => { - try { - await fs.promises.writeFile(getAPIKeyPath(), JSON.stringify(content), 'utf8') - } catch (error) { - logger.error(error) - } -} - /** * Map MimeType to InputField * @param {string} mimeType From 262ef7fd0c266d3a734554ffdb3a06d3b8902b9c Mon Sep 17 00:00:00 2001 From: Marc Klingen Date: Thu, 23 Nov 2023 22:58:14 +0100 Subject: [PATCH 02/54] 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 452ca9897..923af5177 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 03/54] 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 4a908d7fe..7b71cb5f8 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 c920c399e..ce6f576fc 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 5498b4f36..4950a6a66 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 aafc8e8e2..800ae3000 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 a4944af09..bc27a9fe8 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 0973cdda4..7bf3e7ff0 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 04/54] 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 ee532a279..fd398151a 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 ade46ab94..956fcdb33 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 ba2aa5e7d..8249d5121 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 b67219f37..177a32ef9 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 9c40f55ab..9fd2bfde3 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 5233f174f..51578630c 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 c517f419a..3a3ec5502 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 7fc251ad4..94967ba2d 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 a94edddd3..8ea77e6bf 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 51394613e..620c3af7f 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 1d4cea573..3f81ea673 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 86d626c44..9dbb695ee 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 05/54] 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 3f81ea673..a965a0c7d 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 06/54] =?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 909eb3453..649a9a471 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 3503e9821..6f4ccaf41 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 07/54] =?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 6914c04a5..76369ab2e 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 08/54] =?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 a965a0c7d..5566218c2 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 4a3e1784d8f8b4532587bfb5ddce61eb29c35977 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Mon, 27 Nov 2023 06:06:18 +0530 Subject: [PATCH 09/54] LS Prompt Hub: Initial Commit --- .../dialog/PromptLangsmithHubDialog.js | 368 ++++++++++++++++++ .../ui/src/views/canvas/NodeInputHandler.js | 29 ++ 2 files changed, 397 insertions(+) create mode 100644 packages/ui/src/ui-component/dialog/PromptLangsmithHubDialog.js diff --git a/packages/ui/src/ui-component/dialog/PromptLangsmithHubDialog.js b/packages/ui/src/ui-component/dialog/PromptLangsmithHubDialog.js new file mode 100644 index 000000000..16d5c30f1 --- /dev/null +++ b/packages/ui/src/ui-component/dialog/PromptLangsmithHubDialog.js @@ -0,0 +1,368 @@ +import { createPortal } from 'react-dom' +import { useState, useEffect } from 'react' +import PropTypes from 'prop-types' +import { + Box, + Button, + Card, + CardContent, + Chip, + Dialog, + DialogContent, + DialogTitle, + Divider, + Grid, + InputLabel, + List, + ListItemButton, + ListItemText, + OutlinedInput, + Select, + Typography +} from '@mui/material' +import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from '../../store/actions' +import { useDispatch } from 'react-redux' +import FormControl from '@mui/material/FormControl' +import Checkbox from '@mui/material/Checkbox' +import MenuItem from '@mui/material/MenuItem' +import axios from 'axios' +import ReactMarkdown from 'react-markdown' +import CredentialInputHandler from '../../views/canvas/CredentialInputHandler' + +const PromptLangsmithHubDialog = ({ promptType, show, onCancel }) => { + const portalElement = document.getElementById('portal') + const dispatch = useDispatch() + + useEffect(() => { + if (show) dispatch({ type: SHOW_CANVAS_DIALOG }) + else dispatch({ type: HIDE_CANVAS_DIALOG }) + return () => dispatch({ type: HIDE_CANVAS_DIALOG }) + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [show, dispatch]) + + const ITEM_HEIGHT = 48 + const ITEM_PADDING_TOP = 8 + const MenuProps = { + PaperProps: { + style: { + maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP, + width: 250 + } + } + } + + const models = [ + { id: 101, name: 'anthropic:claude-instant-1' }, + { id: 102, name: 'anthropic:claude-instant-1.2' }, + { id: 103, name: 'anthropic:claude-2' }, + { id: 104, name: 'google:palm-2-chat-bison' }, + { id: 105, name: 'google:palm-2-codechat-bison' }, + { id: 106, name: 'google:palm-2-text-bison' }, + { id: 107, name: 'meta:llama-2-13b-chat' }, + { id: 108, name: 'meta:llama-2-70b-chat' }, + { id: 109, name: 'openai:gpt-3.5-turbo' }, + { id: 110, name: 'openai:gpt-4' }, + { id: 111, name: 'openai:text-davinci-003' } + ] + const [modelName, setModelName] = useState([]) + + const usecases = [ + { id: 201, name: 'Agents' }, + { id: 202, name: 'Agent Stimulation' }, + { id: 203, name: 'Autonomous agents' }, + { id: 204, name: 'Classification' }, + { id: 205, name: 'Chatbots' }, + { id: 206, name: 'Code understanding' }, + { id: 207, name: 'Code writing' }, + { id: 208, name: 'Evaluation' }, + { id: 209, name: 'Extraction' }, + { id: 210, name: 'Interacting with APIs' }, + { id: 211, name: 'Multi-modal' }, + { id: 212, name: 'QA over documents' }, + { id: 213, name: 'Self-checking' }, + { id: 214, name: 'SQL' }, + { id: 215, name: 'Summarization' }, + { id: 216, name: 'Tagging' } + ] + const [usecase, setUsecase] = useState([]) + + const languages = [ + { id: 301, name: 'Chinese' }, + { id: 302, name: 'English' }, + { id: 303, name: 'French' }, + { id: 304, name: 'German' }, + { id: 305, name: 'Russian' }, + { id: 306, name: 'Spanish' } + ] + const [language, setLanguage] = useState([]) + const [prompts, setPrompts] = useState([]) + const [selectedPrompt, setSelectedPrompt] = useState({}) + + const [credentialId, setCredentialId] = useState('') + + const handleListItemClick = (event, index) => { + setSelectedPrompt(prompts[index]) + } + + const fetchPrompts = async () => { + let tags = promptType === 'template' ? 'StringPromptTemplate&' : 'ChatPromptTemplate&' + modelName.forEach((item) => { + tags += `tags=${item.name}&` + }) + usecase.forEach((item) => { + tags += `tags=${item.name}&` + }) + language.forEach((item) => { + tags += `tags=${item.name}&` + }) + const url = `https://web.hub.langchain.com/repos/?${tags}offset=0&limit=20&has_commits=true&sort_field=num_likes&sort_direction=desc&is_archived=false` + axios.get(url).then((response) => { + if (response.data.repos) { + setPrompts(response.data.repos) + // response.data.repos.forEach((item) => { + // console.log(item) + // }) + } + }) + // latestReleaseReq.then() + } + const removeDuplicates = (value) => { + let duplicateRemoved = [] + + value.forEach((item) => { + if (value.filter((o) => o.id === item.id).length === 1) { + duplicateRemoved.push(item) + } + }) + return duplicateRemoved + } + + const handleModelChange = (event) => { + const { + target: { value } + } = event + + setModelName(removeDuplicates(value)) + } + + const handleUsecaseChange = (event) => { + const { + target: { value } + } = event + + setUsecase(removeDuplicates(value)) + } + const handleLanguageChange = (event) => { + const { + target: { value } + } = event + + setLanguage(removeDuplicates(value)) + } + + const component = show ? ( + + + Load Prompts from Langsmith Hub ({promptType === 'template' ? 'PromptTemplate' : 'ChatPromptTemplate'}) + + + + + Langsmith Credential + * + + { + setCredentialId(newValue) + }} + /> + + + + + Model + + + + + + Usecase + + + + + + Language + + + + + + + + + + + + + + + Available Prompts + + + {prompts.map((item, index) => ( + handleListItemClick(event, index)} + > + {item.full_name} + + ))} + + + + + + + + + + + Description + + {selectedPrompt?.description} + + + + + + Tags + + {selectedPrompt?.tags?.map((item) => ( + + ))} + + + + + + Readme + + + {selectedPrompt?.readme} + + + + + + + + + ) : null + + return createPortal(component, portalElement) +} + +PromptLangsmithHubDialog.propTypes = { + promptType: PropTypes.string, + show: PropTypes.bool, + onCancel: PropTypes.func +} + +export default PromptLangsmithHubDialog diff --git a/packages/ui/src/views/canvas/NodeInputHandler.js b/packages/ui/src/views/canvas/NodeInputHandler.js index 7eb31bdb1..162455dfd 100644 --- a/packages/ui/src/views/canvas/NodeInputHandler.js +++ b/packages/ui/src/views/canvas/NodeInputHandler.js @@ -6,6 +6,7 @@ import { useSelector } from 'react-redux' // material-ui import { useTheme, styled } from '@mui/material/styles' import { Box, Typography, Tooltip, IconButton, Button } from '@mui/material' +import IconAutoFixHigh from '@mui/icons-material/AutoFixHigh' import { tooltipClasses } from '@mui/material/Tooltip' import { IconArrowsMaximize, IconEdit, IconAlertTriangle } from '@tabler/icons' @@ -31,6 +32,7 @@ import { getInputVariables } from 'utils/genericHelper' // const import { FLOWISE_CREDENTIAL_ID } from 'store/constant' +import PromptLangsmithHubDialog from '../../ui-component/dialog/PromptLangsmithHubDialog' const EDITABLE_OPTIONS = ['selectedTool', 'selectedAssistant'] @@ -56,6 +58,7 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA const [reloadTimestamp, setReloadTimestamp] = useState(Date.now().toString()) const [showFormatPromptValuesDialog, setShowFormatPromptValuesDialog] = useState(false) const [formatPromptValuesDialogProps, setFormatPromptValuesDialogProps] = useState({}) + const [showPromptHubDialog, setShowPromptHubDialog] = useState(false) const onExpandDialogClicked = (value, inputParam) => { const dialogProp = { @@ -69,6 +72,9 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA setShowExpandDialog(true) } + const onShowPromptHubButtonClicked = () => { + setShowPromptHubDialog(true) + } const onFormatPromptValuesClicked = (value, inputParam) => { // Preset values if the field is format prompt values let inputValue = value @@ -209,6 +215,28 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA )} + {(inputParam.name === 'template' || inputParam.name === 'systemMessagePrompt') && ( + <> + + setShowPromptHubDialog(false)} + > + + )}
{inputParam.label} @@ -260,6 +288,7 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA }} /> )} + {inputParam.type === 'file' && ( Date: Mon, 27 Nov 2023 15:34:28 +0000 Subject: [PATCH 10/54] 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 e09a94e7a..4d1a2498f 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 fc2e2eb26..e016d78f7 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 00f825d44..8a2329b58 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 7b71cb5f8..3c9568333 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 ce6f576fc..0f5b9aec3 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 92a0b5eab..7887ce97b 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 68b09b7b2..ac4f76020 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 7de2ec347..6f800cdc4 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 c65d729b2..bdb62911c 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 6b5fdf660..2b8b46503 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 51578630c..85b279070 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 z?E@s_bD5moM9Pm#(($G0E5bIxzLvH zNo%F&g7AsMz7rl)!S%RKK{AunPBFt4HR?i?gp7(L9R1msK@!64WDgmVkBCJ1&`mQeIg+r;?5*& z3`^JqB#cVq4BYLdQCtg&Noyy429A^+JNKECBhC7(*d|m#@B)kA+p_@97e!cQ=5~N6 zxWGga+{af&*z4TK@S1Ur$@%Pg7Um`14*OEwU|d#dEvTqA)O#^>fzPHPqOkiy;|N?2 zxCvtbmnwkDgsdrez59K--;))6w@#vMh(GqF=sup$p9<#s1hSwYkS%Gs*P?QRcnKF) zRf~v>0IuVvZQCVdz+g#k$x6IriB4)Ek?}1h9=em-@>z*X=_F0#a4e>UG>%D<)}5b$ z?tLLY9Q#RYPFWelXQTB~0#ey;BN4&BMpuIA*PQ@16gX;YjO}$i=Z<3I=Y5@w#=_Pz zYwa@cTzd(S@Vt`n3+eFm^B`e-fP^Gqqa?VaDeWXWy`vJ5B;>JP1BZu@5U&K={IGn9 zBw=1bK3*(7ob$36QdxM3c^Bq^?Qb_Kz1#qJu6;@7y71>!hCSa6RX`lP6RH&F7z2C% zaOp6qb;D#H7ED8JKr%yBgYn@n!8=zEA!)9Hgq6YGOlF&vDr_pyT1}*dWjD494jw*I z(p$BYM$yTzhAq|VMJKk^g=S-uJ4#$4GW59C5*42coqJ9``|@i!cJho`LN;?yn9-Pz z;G{K#i24q&Wf73T4qlIIlt4BPd#ox6#_wdOl|!b%9bX;DZ}^P#!!KO9t|aXAEG?l! z!e;HIVPY$3z_!7Z_R=J^gG8oxkSC+lB?blT%weOnVU{n8FT@mrE2OBXK%olad>r#( zO~NZHN!4=4cjc1mo_D{*-v8%HM_t{Q-_Cv)R(uo`<|x6~6^ylwBqWD@vH2WlC^-Df zW&z$ZpJu%K4*dX2I0Pi@AWh?&OH5Kbv&u6<5|4;aY$pjU=0wHIQ%^rDpO5`gPMdT zw+%kjBl)?+DtJ)Q`Ay7s<2U3?<|Lr81}GCbMC6DUUpa~zC>wGJSg`yo+F0MMcYkf7 zpc_g8CZs(DOW0l_V_+3yTY-daB!z9xjiaSgr>EqLFUHBaa~E)55)UbGRKvR7Rg=)9 zw`x565+LEltJmbyFTSco!X_!Lq;Ya9wTAWa(n!Q6sqLhGd^3q_*-5(f8zh^z@4%y$ zp)7!05U`7{a9c;6!wT{@p%#&6BedJMzqBAMS?>gs2S38Sg5SZt&39_wzpa!o9**u%Pq_kB6(k&$kX&qyc%e~sAr)=1~Maf3eu`i$Y=fkHV!AMB{CIPu^_uG$t zClUre`dwh-dW3rizeC`=IE1s`z}xo=}k##*;$(-8o~u_9M@Lj;gTjdXYFVQiA{tx zgqPKzVN-eTxff){j9GH!3MaEPV#|fLeXwc?C0!^Ueo=Rn*6_y7ELpf{iM-jRyEKl9 zlgNZLiA(_zLAEFmQOO4)M!-5Y1_@~;lUsGva^&G3k0{ajQEbl!en?hVlc4N^WIry? z3AgtXVa+t7cz-j8J@Qg+g6IB_IgGHbEauA!p3uS91=$01@ibiJ$lQm zdGqDUjhkA_YKrec7llJk*!II?EVSFs0YO-02oKv1_6YY6wsQjCwdOn zGOew)6Ox1xa3LC@OxP$cUFt<9NUQdpWbiPKrr0Bwuiub@VsD_qmBcgUgfb1arwQ-p zLXe2zLqlA1?W3^nLlV%Ekf1KGoz4=Fp@Qwy0OyPKgK?!ks5m|#_$VIizj6ba`1S)b z{QXa*O@|ldclBenS5;z*XC*oXWy8ewfYSqug91RpCebOZde*7o#c)Wr8 zl7uAW9|j5WwASUgnRQDhPMRW}pM6216H=r}8~}?)my{MzD`^r;Lwyv;_+-;qiHu8< zPB?!I+|o_kcgdyewV@lN=g``OpgZr=&oFY-}%|3U}$ee;`e zLOc#h@^X@9oJi2>`F#~q>?zeytssb4SZoNmY1=`0|D$p8{L5V=E(zXJwCNujliEp| zCPIJUf;Nb21NtNeccttB^va)h?C=`iDB9eq8qYpRvgCL;=A9(J( z1@g+PuSsfiSiI;&olhnbgyW3ctfXOgHM*$m4;Yu;N+RMCLBvE@#LhBs=zFqg`D!_S z`kdqxa@K0&su23o9oP?E72M}O!h?bRMzm}sA!q3)U>w!r84_Y27|Y+>&dKq~{)4Av z?!quu`bpSxUs5GsZiA?QidR&2ow2RR=bnN2ne6O$feI4#@2{0Q>t7m%)QSkfB3AlK0;GScVN7 zEdvIOklwuq%WJRoln$LEfG9e{f5o*+M8Xaaa4kS9KO87mYw8@ris!31!|TFBjVGPTm;V{*+?>u zAtN{;RMYr${m=xrIa8b6-y|+gw+U`r*nOe*ey5Yr<(Uii>>#D@Y>pH}|V<{@O{X-DULoVD`&FLVpPg$pv!rR<4Yn zI89o#?X2SqlUsF`R-IqcsVz7{nlF-_Fo)zm zPLH7>$_BLo<9LMHL*FG_MuHQ3C8U5bDAy&YcF@k&1iPA9(DG@o7Z5N$wWAJI(rXb9 z8|tG$I_gAY=mEW_Ce#zwDIoK14>xq|*Wc;zIWLGt@`aL+R*<0l`bo%VBN35cPF3Ld z`XnbOM|^kzyZ0ZGF`ti<7VV!_%SLjML?jaln9{nF=63vuBpr|O<&$6}AW2DBl7@qh zHLC@}#rX&|1j=#IAQV9|S;PP&S{%qlQqtWd`MAw6Cccd{j&3fI@vWt4oQZ?&e~qG3 zrG8Yh>He1ra<_U6*;I_4dEEiT?ITK2{ zP(1vCGGXP!vP{SslS#r`S$VQ;*IpU^!D#7(m!~cy{CU1iemuTp7QmyDI1!Wqf7|(K z2<~%FAp8iw9B>4Lg&CBKB8OuwIX2LOWkR~8w4khviHHL+p&!hQnOQfCK@lc0UfMtV zg1rC9=dx}0KDnNqr%fU37(k0h!jX6sOG49LSfNFvs@n>!{#J#DT}1wWoE=(iK?g(Y 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 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 literal 0 HcmV?d00001 diff --git a/packages/components/package.json b/packages/components/package.json index 5566218c2..bb0743926 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 4950a6a66..0e9aa0917 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 d8fa22ad1..932708483 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 2232ade06..8994594a1 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 800ae3000..68618a638 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 e77186160..789b0c08e 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 bc27a9fe8..d2c842c6b 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 2f6fb7218..0547366a6 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 8897dd9c5f50fd50e1f1cb623105c8066219b816 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Mon, 27 Nov 2023 22:42:04 +0530 Subject: [PATCH 11/54] LS Prompt Hub: Moving calls to server side and adding functionality to show the detailed prompt --- packages/server/src/index.ts | 40 +++++++ packages/server/src/utils/hub.ts | 27 +++++ packages/ui/src/api/prompt.js | 9 ++ .../dialog/PromptLangsmithHubDialog.js | 111 +++++++++++------- 4 files changed, 144 insertions(+), 43 deletions(-) create mode 100644 packages/server/src/utils/hub.ts create mode 100644 packages/ui/src/api/prompt.js diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 91de4f4cd..92b32b592 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -64,6 +64,9 @@ import { ChatflowPool } from './ChatflowPool' import { CachePool } from './CachePool' import { ICommonObject, INodeOptionsValue } from 'flowise-components' import { createRateLimiter, getRateLimiter, initializeRateLimiter } from './utils/rateLimit' +import axios from 'axios' +import { Client } from 'langchainhub' +import { parsePrompt } from './utils/hub' export class App { app: express.Application @@ -1093,6 +1096,43 @@ export class App { await this.buildChatflow(req, res, undefined, true, true) }) + // ---------------------------------------- + // Prompt from Hub + // ---------------------------------------- + this.app.post('/api/v1/load-prompt', async (req: Request, res: Response) => { + try { + const credential = await this.AppDataSource.getRepository(Credential).findOneBy({ + id: req.body.credential + }) + + if (!credential) return res.status(404).json({ error: `Credential ${req.body.credential} not found` }) + + // Decrypt credentialData + const decryptedCredentialData = await decryptCredentialData(credential.encryptedData, credential.credentialName, undefined) + let hub = new Client({ apiKey: decryptedCredentialData.langsmithApiKey, apiUrl: decryptedCredentialData.langsmithEndpoint }) + const prompt = await hub.pull(req.body.promptName) + const templates = parsePrompt(prompt) + + return res.json({ status: 'OK', prompt: req.body.promptName, templates: templates }) + } catch (e: any) { + return res.json({ status: 'ERROR', prompt: req.body.promptName, error: e?.message }) + } + }) + + this.app.post('/api/v1/prompts-list', async (req: Request, res: Response) => { + try { + const tags = req.body.tags ? `tags=${req.body.tags}` : '' + const url = `https://web.hub.langchain.com/repos/?${tags}offset=0&limit=20&has_commits=true&sort_field=num_likes&sort_direction=desc&is_archived=false` + axios.get(url).then((response) => { + if (response.data.repos) { + return res.json({ status: 'OK', repos: response.data.repos }) + } + }) + } catch (e: any) { + return res.json({ status: 'ERROR', repos: [] }) + } + }) + // ---------------------------------------- // Prediction // ---------------------------------------- diff --git a/packages/server/src/utils/hub.ts b/packages/server/src/utils/hub.ts new file mode 100644 index 000000000..79e7136f0 --- /dev/null +++ b/packages/server/src/utils/hub.ts @@ -0,0 +1,27 @@ +export function parsePrompt(prompt: string): any[] { + const promptObj = JSON.parse(prompt) + let response = [] + if (promptObj.kwargs.messages) { + promptObj.kwargs.messages.forEach((message: any) => { + let messageType = message.id.includes('SystemMessagePromptTemplate') + ? 'systemMessagePrompt' + : message.id.includes('HumanMessagePromptTemplate') + ? 'humanMessagePrompt' + : message.id.includes('AIMessagePromptTemplate') + ? 'aiMessagePrompt' + : 'template' + let template = message.kwargs.prompt.kwargs.template + response.push({ + type: messageType, + template: template + }) + }) + } else if (promptObj.kwargs.template) { + let template = promptObj.kwargs.template + response.push({ + type: 'template', + template: template + }) + } + return response +} diff --git a/packages/ui/src/api/prompt.js b/packages/ui/src/api/prompt.js new file mode 100644 index 000000000..42b1bdbc4 --- /dev/null +++ b/packages/ui/src/api/prompt.js @@ -0,0 +1,9 @@ +import client from './client' + +const getAvailablePrompts = (body) => client.post(`/prompts-list`, body) +const getPrompt = (body) => client.post(`/load-prompt`, body) + +export default { + getAvailablePrompts, + getPrompt +} diff --git a/packages/ui/src/ui-component/dialog/PromptLangsmithHubDialog.js b/packages/ui/src/ui-component/dialog/PromptLangsmithHubDialog.js index 16d5c30f1..4db61633e 100644 --- a/packages/ui/src/ui-component/dialog/PromptLangsmithHubDialog.js +++ b/packages/ui/src/ui-component/dialog/PromptLangsmithHubDialog.js @@ -25,9 +25,9 @@ import { useDispatch } from 'react-redux' import FormControl from '@mui/material/FormControl' import Checkbox from '@mui/material/Checkbox' import MenuItem from '@mui/material/MenuItem' -import axios from 'axios' import ReactMarkdown from 'react-markdown' import CredentialInputHandler from '../../views/canvas/CredentialInputHandler' +import promptApi from '../../api/prompt' const PromptLangsmithHubDialog = ({ promptType, show, onCancel }) => { const portalElement = document.getElementById('portal') @@ -96,13 +96,24 @@ const PromptLangsmithHubDialog = ({ promptType, show, onCancel }) => { { id: 306, name: 'Spanish' } ] const [language, setLanguage] = useState([]) - const [prompts, setPrompts] = useState([]) + const [availablePrompNameList, setAvailablePrompNameList] = useState([]) const [selectedPrompt, setSelectedPrompt] = useState({}) const [credentialId, setCredentialId] = useState('') - const handleListItemClick = (event, index) => { - setSelectedPrompt(prompts[index]) + const handleListItemClick = async (event, index) => { + const prompt = availablePrompNameList[index] + + if (!prompt.detailed) { + const createResp = await promptApi.getPrompt({ + credential: credentialId, + promptName: selectedPrompt.full_name + }) + if (createResp.data) { + prompt.detailed = createResp.data.templates + } + } + setSelectedPrompt(prompt) } const fetchPrompts = async () => { @@ -116,17 +127,15 @@ const PromptLangsmithHubDialog = ({ promptType, show, onCancel }) => { language.forEach((item) => { tags += `tags=${item.name}&` }) - const url = `https://web.hub.langchain.com/repos/?${tags}offset=0&limit=20&has_commits=true&sort_field=num_likes&sort_direction=desc&is_archived=false` - axios.get(url).then((response) => { - if (response.data.repos) { - setPrompts(response.data.repos) - // response.data.repos.forEach((item) => { - // console.log(item) - // }) - } + const createResp = await promptApi.getAvailablePrompts({ + credential: credentialId, + tags: tags }) - // latestReleaseReq.then() + if (createResp.data) { + setAvailablePrompNameList(createResp.data.repos) + } } + const removeDuplicates = (value) => { let duplicateRemoved = [] @@ -173,26 +182,29 @@ const PromptLangsmithHubDialog = ({ promptType, show, onCancel }) => { Load Prompts from Langsmith Hub ({promptType === 'template' ? 'PromptTemplate' : 'ChatPromptTemplate'}) - - + + Langsmith Credential * - { - setCredentialId(newValue) - }} - /> + + { + setCredentialId(newValue) + }} + /> + { Model { handleFileUpload(e)} /> - setAboutDialogOpen(false)} /> ) From 147526ff72802a00132d41eedcc2ef9ee0d1c7c7 Mon Sep 17 00:00:00 2001 From: Henry Date: Tue, 28 Nov 2023 17:00:10 +0000 Subject: [PATCH 18/54] remove zapier NLA as its not supported anymore --- .../nodes/tools/ZapierNLA/ZapierNLA.ts | 52 ------------------- .../nodes/tools/ZapierNLA/zapier.svg | 8 --- 2 files changed, 60 deletions(-) delete mode 100644 packages/components/nodes/tools/ZapierNLA/ZapierNLA.ts delete mode 100644 packages/components/nodes/tools/ZapierNLA/zapier.svg diff --git a/packages/components/nodes/tools/ZapierNLA/ZapierNLA.ts b/packages/components/nodes/tools/ZapierNLA/ZapierNLA.ts deleted file mode 100644 index 31ac989b5..000000000 --- a/packages/components/nodes/tools/ZapierNLA/ZapierNLA.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { ZapierNLAWrapper, ZapierNLAWrapperParams } from 'langchain/tools' -import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' -import { ZapierToolKit } from 'langchain/agents' -import { getCredentialData, getCredentialParam } from '../../../src' - -class ZapierNLA_Tools implements INode { - label: string - name: string - version: number - description: string - type: string - icon: string - category: string - badge: string - baseClasses: string[] - inputs: INodeParams[] - credential: INodeParams - - constructor() { - this.label = 'Zapier NLA' - this.name = 'zapierNLA' - this.version = 1.0 - this.type = 'ZapierNLA' - this.icon = 'zapier.svg' - this.category = 'Tools' - this.description = "Access to apps and actions on Zapier's platform through a natural language API interface" - this.badge = 'DEPRECATING' - this.inputs = [] - this.credential = { - label: 'Connect Credential', - name: 'credential', - type: 'credential', - credentialNames: ['zapierNLAApi'] - } - this.baseClasses = [this.type, 'Tool'] - } - - async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { - const credentialData = await getCredentialData(nodeData.credential ?? '', options) - const zapierNLAApiKey = getCredentialParam('zapierNLAApiKey', credentialData, nodeData) - - const obj: Partial = { - apiKey: zapierNLAApiKey - } - const zapier = new ZapierNLAWrapper(obj) - const toolkit = await ZapierToolKit.fromZapierNLAWrapper(zapier) - - return toolkit.tools - } -} - -module.exports = { nodeClass: ZapierNLA_Tools } diff --git a/packages/components/nodes/tools/ZapierNLA/zapier.svg b/packages/components/nodes/tools/ZapierNLA/zapier.svg deleted file mode 100644 index 6ed35f295..000000000 --- a/packages/components/nodes/tools/ZapierNLA/zapier.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file From 464a23e0f49b63553faca6222e42c733b78fbc2a Mon Sep 17 00:00:00 2001 From: Henry Date: Wed, 29 Nov 2023 00:25:08 +0000 Subject: [PATCH 19/54] update agent model --- .../ConversationalRetrievalAgent.ts | 4 ++-- .../agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts | 4 ++-- .../marketplaces/chatflows/API Agent OpenAI.json | 6 +++--- .../chatflows/Conversational Retrieval Agent.json | 10 +++++----- .../server/marketplaces/chatflows/OpenAI Agent.json | 6 +++--- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/components/nodes/agents/ConversationalRetrievalAgent/ConversationalRetrievalAgent.ts b/packages/components/nodes/agents/ConversationalRetrievalAgent/ConversationalRetrievalAgent.ts index 7b71cb5f8..c6354e18d 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 = 2.0 + this.version = 3.0 this.type = 'AgentExecutor' this.category = 'Agents' this.icon = 'agent.svg' @@ -42,7 +42,7 @@ class ConversationalRetrievalAgent_Agents implements INode { { label: 'OpenAI/Azure Chat Model', name: 'model', - type: 'ChatOpenAI | AzureChatOpenAI' + type: 'BaseChatModel' }, { label: 'System Message', diff --git a/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts b/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts index ce6f576fc..25a540e87 100644 --- a/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts +++ b/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts @@ -20,7 +20,7 @@ class OpenAIFunctionAgent_Agents implements INode { constructor() { this.label = 'OpenAI Function Agent' this.name = 'openAIFunctionAgent' - this.version = 2.0 + this.version = 3.0 this.type = 'AgentExecutor' this.category = 'Agents' this.icon = 'openai.png' @@ -41,7 +41,7 @@ class OpenAIFunctionAgent_Agents implements INode { { label: 'OpenAI/Azure Chat Model', name: 'model', - type: 'ChatOpenAI | AzureChatOpenAI' + type: 'BaseChatModel' }, { label: 'System Message', diff --git a/packages/server/marketplaces/chatflows/API Agent OpenAI.json b/packages/server/marketplaces/chatflows/API Agent OpenAI.json index 4950a6a66..1a07e60db 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": 2, + "version": 3, "type": "AgentExecutor", "baseClasses": ["AgentExecutor", "BaseChain"], "category": "Agents", @@ -367,8 +367,8 @@ { "label": "OpenAI/Azure Chat Model", "name": "model", - "type": "ChatOpenAI | AzureChatOpenAI", - "id": "openAIFunctionAgent_0-input-model-ChatOpenAI | AzureChatOpenAI" + "type": "BaseChatModel", + "id": "openAIFunctionAgent_0-input-model-BaseChatModel" } ], "inputs": { diff --git a/packages/server/marketplaces/chatflows/Conversational Retrieval Agent.json b/packages/server/marketplaces/chatflows/Conversational Retrieval Agent.json index 800ae3000..0e9e41bdd 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": 2, + "version": 3, "name": "conversationalRetrievalAgent", "type": "AgentExecutor", "baseClasses": ["AgentExecutor", "BaseChain", "Runnable"], @@ -132,8 +132,8 @@ { "label": "OpenAI/Azure Chat Model", "name": "model", - "type": "ChatOpenAI | AzureChatOpenAI", - "id": "conversationalRetrievalAgent_0-input-model-ChatOpenAI | AzureChatOpenAI" + "type": "BaseChatModel", + "id": "conversationalRetrievalAgent_0-input-model-BaseChatModel" } ], "inputs": { @@ -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-BaseChatModel", "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-BaseChatModel", "data": { "label": "" } diff --git a/packages/server/marketplaces/chatflows/OpenAI Agent.json b/packages/server/marketplaces/chatflows/OpenAI Agent.json index bc27a9fe8..931bae0c5 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": 2, + "version": 3, "type": "AgentExecutor", "baseClasses": ["AgentExecutor", "BaseChain"], "category": "Agents", @@ -239,8 +239,8 @@ { "label": "OpenAI/Azure Chat Model", "name": "model", - "type": "ChatOpenAI | AzureChatOpenAI", - "id": "openAIFunctionAgent_0-input-model-ChatOpenAI | AzureChatOpenAI" + "type": "BaseChatModel", + "id": "openAIFunctionAgent_0-input-model-BaseChatModel" } ], "inputs": { From 6a7726956c76c27da01413520c534339243ad229 Mon Sep 17 00:00:00 2001 From: Henry Date: Wed, 29 Nov 2023 00:52:48 +0000 Subject: [PATCH 20/54] update marketplace templates --- packages/server/marketplaces/chatflows/API Agent OpenAI.json | 4 ++-- packages/server/marketplaces/chatflows/OpenAI Agent.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/server/marketplaces/chatflows/API Agent OpenAI.json b/packages/server/marketplaces/chatflows/API Agent OpenAI.json index 26c1961ee..002c08c18 100644 --- a/packages/server/marketplaces/chatflows/API Agent OpenAI.json +++ b/packages/server/marketplaces/chatflows/API Agent OpenAI.json @@ -672,9 +672,9 @@ "source": "chatOpenAI_2", "sourceHandle": "chatOpenAI_2-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel", "target": "openAIFunctionAgent_0", - "targetHandle": "openAIFunctionAgent_0-input-model-ChatOpenAI | AzureChatOpenAI", + "targetHandle": "openAIFunctionAgent_0-input-model-BaseChatModel", "type": "buttonedge", - "id": "chatOpenAI_2-chatOpenAI_2-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel-openAIFunctionAgent_0-openAIFunctionAgent_0-input-model-ChatOpenAI | AzureChatOpenAI", + "id": "chatOpenAI_2-chatOpenAI_2-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel-openAIFunctionAgent_0-openAIFunctionAgent_0-input-model-BaseChatModel", "data": { "label": "" } diff --git a/packages/server/marketplaces/chatflows/OpenAI Agent.json b/packages/server/marketplaces/chatflows/OpenAI Agent.json index 0cb3c3b3e..17e59236d 100644 --- a/packages/server/marketplaces/chatflows/OpenAI Agent.json +++ b/packages/server/marketplaces/chatflows/OpenAI Agent.json @@ -488,9 +488,9 @@ "source": "chatOpenAI_0", "sourceHandle": "chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel", "target": "openAIFunctionAgent_0", - "targetHandle": "openAIFunctionAgent_0-input-model-ChatOpenAI | AzureChatOpenAI", + "targetHandle": "openAIFunctionAgent_0-input-model-BaseChatModel", "type": "buttonedge", - "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel-openAIFunctionAgent_0-openAIFunctionAgent_0-input-model-ChatOpenAI | AzureChatOpenAI", + "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel-openAIFunctionAgent_0-openAIFunctionAgent_0-input-model-BaseChatModel", "data": { "label": "" } From 5a5097e9970be804642c714d99c99ababc1b82d9 Mon Sep 17 00:00:00 2001 From: Henry Date: Wed, 29 Nov 2023 13:42:17 +0000 Subject: [PATCH 21/54] update flowise-components to 1.4.5 --- 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 bb0743926..bea9a7a06 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "flowise-components", - "version": "1.4.4", + "version": "1.4.5", "description": "Flowiseai Components", "main": "dist/src/index", "types": "dist/src/index.d.ts", From e7583c30363c0de5684060f2d388ba1b27345e07 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Wed, 29 Nov 2023 20:16:32 +0530 Subject: [PATCH 22/54] LS Prompt hub: right side details display changes --- packages/server/src/utils/hub.ts | 1 + .../dialog/PromptLangsmithHubDialog.js | 151 +++++++++++++----- 2 files changed, 111 insertions(+), 41 deletions(-) diff --git a/packages/server/src/utils/hub.ts b/packages/server/src/utils/hub.ts index 38e8a6c09..9a3242283 100644 --- a/packages/server/src/utils/hub.ts +++ b/packages/server/src/utils/hub.ts @@ -28,6 +28,7 @@ export function parsePrompt(prompt: string): any[] { let template = promptObj.kwargs.template response.push({ type: 'template', + typeDisplay: 'Prompt', template: template }) } diff --git a/packages/ui/src/ui-component/dialog/PromptLangsmithHubDialog.js b/packages/ui/src/ui-component/dialog/PromptLangsmithHubDialog.js index e425dada9..35f1f7543 100644 --- a/packages/ui/src/ui-component/dialog/PromptLangsmithHubDialog.js +++ b/packages/ui/src/ui-component/dialog/PromptLangsmithHubDialog.js @@ -6,7 +6,6 @@ import { Button, Card, CardContent, - Chip, Dialog, DialogActions, DialogContent, @@ -30,6 +29,46 @@ import ReactMarkdown from 'react-markdown' import CredentialInputHandler from '../../views/canvas/CredentialInputHandler' import promptApi from '../../api/prompt' import { StyledButton } from '../button/StyledButton' +import ExpandMoreIcon from '@mui/icons-material/ExpandMore' +import { styled } from '@mui/material/styles' +import ArrowForwardIosSharpIcon from '@mui/icons-material/ArrowForwardIosSharp' +import MuiAccordion from '@mui/material/Accordion' +import MuiAccordionSummary from '@mui/material/AccordionSummary' +import MuiAccordionDetails from '@mui/material/AccordionDetails' + +const NewLineToBr = ({ children = '' }) => { + return children.split('\n').reduce(function (arr, line) { + return arr.concat(line,
) + }, []) +} + +const Accordion = styled((props) => )(({ theme }) => ({ + border: `1px solid ${theme.palette.divider}`, + '&:not(:last-child)': { + borderBottom: 0 + }, + '&:before': { + display: 'none' + } +})) + +const AccordionSummary = styled((props) => ( + } {...props} /> +))(({ theme }) => ({ + backgroundColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, .05)' : 'rgba(0, 0, 0, .03)', + flexDirection: 'row-reverse', + '& .MuiAccordionSummary-expandIconWrapper.Mui-expanded': { + transform: 'rotate(180deg)' + }, + '& .MuiAccordionSummary-content': { + marginLeft: theme.spacing(1) + } +})) + +const AccordionDetails = styled(MuiAccordionDetails)(({ theme }) => ({ + padding: theme.spacing(2), + borderTop: '1px solid rgba(0, 0, 0, .125)' +})) const PromptLangsmithHubDialog = ({ promptType, show, onCancel, onSubmit }) => { const portalElement = document.getElementById('portal') @@ -102,6 +141,11 @@ const PromptLangsmithHubDialog = ({ promptType, show, onCancel, onSubmit }) => { const [selectedPrompt, setSelectedPrompt] = useState({}) const [credentialId, setCredentialId] = useState('') + const [accordionExpanded, setAccordionExpanded] = useState('panel2') + + const handleAccordionChange = (panel) => { + setAccordionExpanded(panel) + } const handleListItemClick = async (event, index) => { const prompt = availablePrompNameList[index] @@ -178,7 +222,7 @@ const PromptLangsmithHubDialog = ({ promptType, show, onCancel, onSubmit }) => { onClose={onCancel} open={show} fullWidth - maxWidth='md' + maxWidth='lg' aria-labelledby='prompt-dialog-title' aria-describedby='prompt-dialog-description' > @@ -314,10 +358,10 @@ const PromptLangsmithHubDialog = ({ promptType, show, onCancel, onSubmit }) => {
- + - + Available Prompts @@ -339,45 +383,70 @@ const PromptLangsmithHubDialog = ({ promptType, show, onCancel, onSubmit }) => { - - - - Description - - - {selectedPrompt?.description} -
- {selectedPrompt?.tags?.map((item) => ( - - ))} -
-
-
- - - - Readme - - - {selectedPrompt?.readme} - - - - - - {selectedPrompt?.detailed?.map((item) => ( - <> - - {item.typeDisplay.toUpperCase()} + + + handleAccordionChange('panel1')}> + } + id='panel1d-header' + > + Description + + + + {selectedPrompt?.description} - -

{item.template}
+ + + handleAccordionChange('panel2')}> + } + id='panel2d-header' + > + Prompt + + + + {selectedPrompt?.detailed?.map((item) => ( + <> + + {item.typeDisplay.toUpperCase()} + + +

+ {item.template} +

+
+ + ))}
- - ))} +
+
+ handleAccordionChange('panel3')}> + } + aria-controls='panel3d-content' + id='panel3d-header' + > + Readme + + + + {selectedPrompt?.readme} + + + From 4b5f7028e3320d72d92d60deb55ebee44b56deb8 Mon Sep 17 00:00:00 2001 From: Henry Date: Thu, 30 Nov 2023 16:01:16 +0000 Subject: [PATCH 23/54] fix chat history --- .../agents/OpenAIAssistant/OpenAIAssistant.ts | 70 ++++++++++--------- .../nodes/memory/DynamoDb/DynamoDb.ts | 35 +++++++--- .../memory/MongoDBMemory/MongoDBMemory.ts | 35 +++++++--- .../memory/MotorheadMemory/MotorheadMemory.ts | 25 +++++-- .../RedisBackedChatMemory.ts | 26 ++++--- .../UpstashRedisBackedChatMemory.ts | 25 ++++--- .../nodes/memory/ZepMemory/ZepMemory.ts | 27 +++++-- .../components/nodes/tools/ChainTool/core.ts | 4 +- packages/components/src/Interface.ts | 5 +- packages/components/src/utils.ts | 12 ++++ packages/server/src/index.ts | 38 +++++++--- packages/server/src/utils/index.ts | 58 ++++++++++++--- 12 files changed, 262 insertions(+), 98 deletions(-) diff --git a/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts b/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts index 35299057c..7f2377bde 100644 --- a/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts +++ b/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts @@ -85,43 +85,46 @@ class OpenAIAssistant_Agents implements INode { return null } - async clearSessionMemory(nodeData: INodeData, options: ICommonObject): Promise { - const selectedAssistantId = nodeData.inputs?.selectedAssistant as string - const appDataSource = options.appDataSource as DataSource - const databaseEntities = options.databaseEntities as IDatabaseEntity - let sessionId = nodeData.inputs?.sessionId as string + //@ts-ignore + memoryMethods = { + async clearSessionMemory(nodeData: INodeData, options: ICommonObject): Promise { + const selectedAssistantId = nodeData.inputs?.selectedAssistant as string + const appDataSource = options.appDataSource as DataSource + const databaseEntities = options.databaseEntities as IDatabaseEntity + let sessionId = nodeData.inputs?.sessionId as string - const assistant = await appDataSource.getRepository(databaseEntities['Assistant']).findOneBy({ - id: selectedAssistantId - }) - - if (!assistant) { - options.logger.error(`Assistant ${selectedAssistantId} not found`) - return - } - - if (!sessionId && options.chatId) { - const chatmsg = await appDataSource.getRepository(databaseEntities['ChatMessage']).findOneBy({ - chatId: options.chatId + const assistant = await appDataSource.getRepository(databaseEntities['Assistant']).findOneBy({ + id: selectedAssistantId }) - if (!chatmsg) { - options.logger.error(`Chat Message with Chat Id: ${options.chatId} not found`) + + if (!assistant) { + options.logger.error(`Assistant ${selectedAssistantId} not found`) return } - sessionId = chatmsg.sessionId - } - const credentialData = await getCredentialData(assistant.credential ?? '', options) - const openAIApiKey = getCredentialParam('openAIApiKey', credentialData, nodeData) - if (!openAIApiKey) { - options.logger.error(`OpenAI ApiKey not found`) - return - } + if (!sessionId && options.chatId) { + const chatmsg = await appDataSource.getRepository(databaseEntities['ChatMessage']).findOneBy({ + chatId: options.chatId + }) + if (!chatmsg) { + options.logger.error(`Chat Message with Chat Id: ${options.chatId} not found`) + return + } + sessionId = chatmsg.sessionId + } - const openai = new OpenAI({ apiKey: openAIApiKey }) - options.logger.info(`Clearing OpenAI Thread ${sessionId}`) - if (sessionId) await openai.beta.threads.del(sessionId) - options.logger.info(`Successfully cleared OpenAI Thread ${sessionId}`) + const credentialData = await getCredentialData(assistant.credential ?? '', options) + const openAIApiKey = getCredentialParam('openAIApiKey', credentialData, nodeData) + if (!openAIApiKey) { + options.logger.error(`OpenAI ApiKey not found`) + return + } + + const openai = new OpenAI({ apiKey: openAIApiKey }) + options.logger.info(`Clearing OpenAI Thread ${sessionId}`) + if (sessionId) await openai.beta.threads.del(sessionId) + options.logger.info(`Successfully cleared OpenAI Thread ${sessionId}`) + } } async run(nodeData: INodeData, input: string, options: ICommonObject): Promise { @@ -359,7 +362,10 @@ class OpenAIAssistant_Agents implements INode { } // Replace the text with a footnote - message_content.value = message_content.value.replace(`${annotation.text}`, `${filePath}`) + message_content.value = message_content.value.replace( + `${annotation.text}`, + `${disableFileDownload ? '' : filePath}` + ) } returnVal += message_content.value diff --git a/packages/components/nodes/memory/DynamoDb/DynamoDb.ts b/packages/components/nodes/memory/DynamoDb/DynamoDb.ts index ac4f76020..8ca6cf9e5 100644 --- a/packages/components/nodes/memory/DynamoDb/DynamoDb.ts +++ b/packages/components/nodes/memory/DynamoDb/DynamoDb.ts @@ -1,4 +1,13 @@ -import { ICommonObject, INode, INodeData, INodeParams, getBaseClasses, getCredentialData, getCredentialParam } from '../../../src' +import { + ICommonObject, + INode, + INodeData, + INodeParams, + getBaseClasses, + getCredentialData, + getCredentialParam, + serializeChatHistory +} from '../../../src' import { DynamoDBChatMessageHistory } from 'langchain/stores/message/dynamodb' import { BufferMemory, BufferMemoryInput } from 'langchain/memory' @@ -70,13 +79,23 @@ class DynamoDb_Memory implements INode { return initalizeDynamoDB(nodeData, options) } - async clearSessionMemory(nodeData: INodeData, options: ICommonObject): Promise { - const dynamodbMemory = await initalizeDynamoDB(nodeData, options) - const sessionId = nodeData.inputs?.sessionId as string - const chatId = options?.chatId as string - options.logger.info(`Clearing DynamoDb memory session ${sessionId ? sessionId : chatId}`) - await dynamodbMemory.clear() - options.logger.info(`Successfully cleared DynamoDb memory session ${sessionId ? sessionId : chatId}`) + //@ts-ignore + memoryMethods = { + async clearSessionMemory(nodeData: INodeData, options: ICommonObject): Promise { + const dynamodbMemory = await initalizeDynamoDB(nodeData, options) + const sessionId = nodeData.inputs?.sessionId as string + const chatId = options?.chatId as string + options.logger.info(`Clearing DynamoDb memory session ${sessionId ? sessionId : chatId}`) + await dynamodbMemory.clear() + options.logger.info(`Successfully cleared DynamoDb memory session ${sessionId ? sessionId : chatId}`) + }, + async getChatMessages(nodeData: INodeData, options: ICommonObject): Promise { + const memoryKey = nodeData.inputs?.memoryKey as string + const dynamodbMemory = await initalizeDynamoDB(nodeData, options) + const key = memoryKey ?? 'chat_history' + const memoryResult = await dynamodbMemory.loadMemoryVariables({}) + return serializeChatHistory(memoryResult[key]) + } } } diff --git a/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts b/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts index 6f800cdc4..76cb7e313 100644 --- a/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts +++ b/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts @@ -1,4 +1,13 @@ -import { getBaseClasses, getCredentialData, getCredentialParam, ICommonObject, INode, INodeData, INodeParams } from '../../../src' +import { + getBaseClasses, + getCredentialData, + getCredentialParam, + ICommonObject, + INode, + INodeData, + INodeParams, + serializeChatHistory +} from '../../../src' import { MongoDBChatMessageHistory } from 'langchain/stores/message/mongodb' import { BufferMemory, BufferMemoryInput } from 'langchain/memory' import { BaseMessage, mapStoredMessageToChatMessage } from 'langchain/schema' @@ -67,13 +76,23 @@ class MongoDB_Memory implements INode { return initializeMongoDB(nodeData, options) } - async clearSessionMemory(nodeData: INodeData, options: ICommonObject): Promise { - const mongodbMemory = await initializeMongoDB(nodeData, options) - const sessionId = nodeData.inputs?.sessionId as string - 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 ? sessionId : chatId}`) + //@ts-ignore + memoryMethods = { + async clearSessionMemory(nodeData: INodeData, options: ICommonObject): Promise { + const mongodbMemory = await initializeMongoDB(nodeData, options) + const sessionId = nodeData.inputs?.sessionId as string + 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 ? sessionId : chatId}`) + }, + async getChatMessages(nodeData: INodeData, options: ICommonObject): Promise { + const memoryKey = nodeData.inputs?.memoryKey as string + const mongodbMemory = await initializeMongoDB(nodeData, options) + const key = memoryKey ?? 'chat_history' + const memoryResult = await mongodbMemory.loadMemoryVariables({}) + return serializeChatHistory(memoryResult[key]) + } } } diff --git a/packages/components/nodes/memory/MotorheadMemory/MotorheadMemory.ts b/packages/components/nodes/memory/MotorheadMemory/MotorheadMemory.ts index 0ec2f42ad..9cdbcd5cc 100644 --- a/packages/components/nodes/memory/MotorheadMemory/MotorheadMemory.ts +++ b/packages/components/nodes/memory/MotorheadMemory/MotorheadMemory.ts @@ -3,6 +3,7 @@ import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../ import { ICommonObject } from '../../../src' import { MotorheadMemory, MotorheadMemoryInput } from 'langchain/memory' import fetch from 'node-fetch' +import { getBufferString } from 'langchain/memory' class MotorMemory_Memory implements INode { label: string @@ -64,13 +65,23 @@ class MotorMemory_Memory implements INode { return initalizeMotorhead(nodeData, options) } - async clearSessionMemory(nodeData: INodeData, options: ICommonObject): Promise { - const motorhead = await initalizeMotorhead(nodeData, options) - const sessionId = nodeData.inputs?.sessionId as string - const chatId = options?.chatId as string - options.logger.info(`Clearing Motorhead memory session ${sessionId ? sessionId : chatId}`) - await motorhead.clear() - options.logger.info(`Successfully cleared Motorhead memory session ${sessionId ? sessionId : chatId}`) + //@ts-ignore + memoryMethods = { + async clearSessionMemory(nodeData: INodeData, options: ICommonObject): Promise { + const motorhead = await initalizeMotorhead(nodeData, options) + const sessionId = nodeData.inputs?.sessionId as string + const chatId = options?.chatId as string + options.logger.info(`Clearing Motorhead memory session ${sessionId ? sessionId : chatId}`) + await motorhead.clear() + options.logger.info(`Successfully cleared Motorhead memory session ${sessionId ? sessionId : chatId}`) + }, + async getChatMessages(nodeData: INodeData, options: ICommonObject): Promise { + const memoryKey = nodeData.inputs?.memoryKey as string + const motorhead = await initalizeMotorhead(nodeData, options) + const key = memoryKey ?? 'chat_history' + const memoryResult = await motorhead.loadMemoryVariables({}) + return getBufferString(memoryResult[key]) + } } } diff --git a/packages/components/nodes/memory/RedisBackedChatMemory/RedisBackedChatMemory.ts b/packages/components/nodes/memory/RedisBackedChatMemory/RedisBackedChatMemory.ts index bdb62911c..7fe447ad5 100644 --- a/packages/components/nodes/memory/RedisBackedChatMemory/RedisBackedChatMemory.ts +++ b/packages/components/nodes/memory/RedisBackedChatMemory/RedisBackedChatMemory.ts @@ -1,5 +1,5 @@ import { INode, INodeData, INodeParams, ICommonObject } from '../../../src/Interface' -import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' +import { getBaseClasses, getCredentialData, getCredentialParam, serializeChatHistory } from '../../../src/utils' import { BufferMemory, BufferMemoryInput } from 'langchain/memory' import { RedisChatMessageHistory, RedisChatMessageHistoryInput } from 'langchain/stores/message/ioredis' import { mapStoredMessageToChatMessage, BaseMessage } from 'langchain/schema' @@ -65,13 +65,23 @@ class RedisBackedChatMemory_Memory implements INode { return await initalizeRedis(nodeData, options) } - async clearSessionMemory(nodeData: INodeData, options: ICommonObject): Promise { - const redis = await initalizeRedis(nodeData, options) - const sessionId = nodeData.inputs?.sessionId as string - const chatId = options?.chatId as string - options.logger.info(`Clearing Redis memory session ${sessionId ? sessionId : chatId}`) - await redis.clear() - options.logger.info(`Successfully cleared Redis memory session ${sessionId ? sessionId : chatId}`) + //@ts-ignore + memoryMethods = { + async clearSessionMemory(nodeData: INodeData, options: ICommonObject): Promise { + const redis = await initalizeRedis(nodeData, options) + const sessionId = nodeData.inputs?.sessionId as string + const chatId = options?.chatId as string + options.logger.info(`Clearing Redis memory session ${sessionId ? sessionId : chatId}`) + await redis.clear() + options.logger.info(`Successfully cleared Redis memory session ${sessionId ? sessionId : chatId}`) + }, + async getChatMessages(nodeData: INodeData, options: ICommonObject): Promise { + const memoryKey = nodeData.inputs?.memoryKey as string + const redis = await initalizeRedis(nodeData, options) + const key = memoryKey ?? 'chat_history' + const memoryResult = await redis.loadMemoryVariables({}) + return serializeChatHistory(memoryResult[key]) + } } } diff --git a/packages/components/nodes/memory/UpstashRedisBackedChatMemory/UpstashRedisBackedChatMemory.ts b/packages/components/nodes/memory/UpstashRedisBackedChatMemory/UpstashRedisBackedChatMemory.ts index 2b8b46503..8bca04404 100644 --- a/packages/components/nodes/memory/UpstashRedisBackedChatMemory/UpstashRedisBackedChatMemory.ts +++ b/packages/components/nodes/memory/UpstashRedisBackedChatMemory/UpstashRedisBackedChatMemory.ts @@ -1,5 +1,5 @@ import { INode, INodeData, INodeParams } from '../../../src/Interface' -import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' +import { getBaseClasses, getCredentialData, getCredentialParam, serializeChatHistory } from '../../../src/utils' import { ICommonObject } from '../../../src' import { BufferMemory, BufferMemoryInput } from 'langchain/memory' import { UpstashRedisChatMessageHistory } from 'langchain/stores/message/upstash_redis' @@ -63,13 +63,22 @@ class UpstashRedisBackedChatMemory_Memory implements INode { return initalizeUpstashRedis(nodeData, options) } - async clearSessionMemory(nodeData: INodeData, options: ICommonObject): Promise { - const redis = await initalizeUpstashRedis(nodeData, options) - const sessionId = nodeData.inputs?.sessionId as string - const chatId = options?.chatId as string - options.logger.info(`Clearing Upstash Redis memory session ${sessionId ? sessionId : chatId}`) - await redis.clear() - options.logger.info(`Successfully cleared Upstash Redis memory session ${sessionId ? sessionId : chatId}`) + //@ts-ignore + memoryMethods = { + async clearSessionMemory(nodeData: INodeData, options: ICommonObject): Promise { + const redis = await initalizeUpstashRedis(nodeData, options) + const sessionId = nodeData.inputs?.sessionId as string + const chatId = options?.chatId as string + options.logger.info(`Clearing Upstash Redis memory session ${sessionId ? sessionId : chatId}`) + await redis.clear() + options.logger.info(`Successfully cleared Upstash Redis memory session ${sessionId ? sessionId : chatId}`) + }, + async getChatMessages(nodeData: INodeData, options: ICommonObject): Promise { + const redis = await initalizeUpstashRedis(nodeData, options) + const key = 'chat_history' + const memoryResult = await redis.loadMemoryVariables({}) + return serializeChatHistory(memoryResult[key]) + } } } diff --git a/packages/components/nodes/memory/ZepMemory/ZepMemory.ts b/packages/components/nodes/memory/ZepMemory/ZepMemory.ts index c44986444..ced871a1e 100644 --- a/packages/components/nodes/memory/ZepMemory/ZepMemory.ts +++ b/packages/components/nodes/memory/ZepMemory/ZepMemory.ts @@ -3,6 +3,7 @@ import { INode, INodeData, INodeParams } from '../../../src/Interface' import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' import { ZepMemory, ZepMemoryInput } from 'langchain/memory/zep' import { ICommonObject } from '../../../src' +import { getBufferString } from 'langchain/memory' class ZepMemory_Memory implements INode { label: string @@ -140,13 +141,25 @@ class ZepMemory_Memory implements INode { return zep } - async clearSessionMemory(nodeData: INodeData, options: ICommonObject): Promise { - const zep = await initalizeZep(nodeData, options) - const sessionId = nodeData.inputs?.sessionId as string - const chatId = options?.chatId as string - options.logger.info(`Clearing Zep memory session ${sessionId ? sessionId : chatId}`) - await zep.clear() - options.logger.info(`Successfully cleared Zep memory session ${sessionId ? sessionId : chatId}`) + //@ts-ignore + memoryMethods = { + async clearSessionMemory(nodeData: INodeData, options: ICommonObject): Promise { + const zep = await initalizeZep(nodeData, options) + const sessionId = nodeData.inputs?.sessionId as string + const chatId = options?.chatId as string + options.logger.info(`Clearing Zep memory session ${sessionId ? sessionId : chatId}`) + await zep.clear() + options.logger.info(`Successfully cleared Zep memory session ${sessionId ? sessionId : chatId}`) + }, + async getChatMessages(nodeData: INodeData, options: ICommonObject): Promise { + const memoryKey = nodeData.inputs?.memoryKey as string + const aiPrefix = nodeData.inputs?.aiPrefix as string + const humanPrefix = nodeData.inputs?.humanPrefix as string + const zep = await initalizeZep(nodeData, options) + const key = memoryKey ?? 'chat_history' + const memoryResult = await zep.loadMemoryVariables({}) + return getBufferString(memoryResult[key], humanPrefix, aiPrefix) + } } } diff --git a/packages/components/nodes/tools/ChainTool/core.ts b/packages/components/nodes/tools/ChainTool/core.ts index 6c3dba554..5520d6df0 100644 --- a/packages/components/nodes/tools/ChainTool/core.ts +++ b/packages/components/nodes/tools/ChainTool/core.ts @@ -1,5 +1,6 @@ import { DynamicTool, DynamicToolInput } from 'langchain/tools' import { BaseChain } from 'langchain/chains' +import { handleEscapeCharacters } from '../../../src/utils' export interface ChainToolInput extends Omit { chain: BaseChain @@ -14,7 +15,8 @@ export class ChainTool extends DynamicTool { func: async (input, runManager) => { // To enable LLM Chain which has promptValues if ((chain as any).prompt && (chain as any).prompt.promptValues) { - const values = await chain.call((chain as any).prompt.promptValues, runManager?.getChild()) + const promptValues = handleEscapeCharacters((chain as any).prompt.promptValues, true) + const values = await chain.call(promptValues, runManager?.getChild()) return values?.text } return chain.run(input, runManager?.getChild()) diff --git a/packages/components/src/Interface.ts b/packages/components/src/Interface.ts index af3042720..6752f9440 100644 --- a/packages/components/src/Interface.ts +++ b/packages/components/src/Interface.ts @@ -107,9 +107,12 @@ export interface INode extends INodeProperties { search: (nodeData: INodeData, options?: ICommonObject) => Promise delete: (nodeData: INodeData, options?: ICommonObject) => Promise } + memoryMethods?: { + clearSessionMemory: (nodeData: INodeData, options?: ICommonObject) => Promise + getChatMessages: (nodeData: INodeData, options?: ICommonObject) => Promise + } init?(nodeData: INodeData, input: string, options?: ICommonObject): Promise run?(nodeData: INodeData, input: string, options?: ICommonObject): Promise - clearSessionMemory?(nodeData: INodeData, options?: ICommonObject): Promise } export interface INodeData extends INodeProperties { diff --git a/packages/components/src/utils.ts b/packages/components/src/utils.ts index 69f8b268e..404f7c75d 100644 --- a/packages/components/src/utils.ts +++ b/packages/components/src/utils.ts @@ -549,6 +549,18 @@ export const convertChatHistoryToText = (chatHistory: IMessage[] = []): string = .join('\n') } +/** + * Serialize array chat history to string + * @param {IMessage[]} chatHistory + * @returns {string} + */ +export const serializeChatHistory = (chatHistory: string | Array) => { + if (Array.isArray(chatHistory)) { + return chatHistory.join('\n') + } + return chatHistory +} + /** * Convert schema to zod schema * @param {string | object} schema diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 1ebd9312d..d87d2c0ac 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -42,7 +42,8 @@ import { getEncryptionKey, checkMemorySessionId, clearSessionMemoryFromViewMessageDialog, - getUserHome + getUserHome, + replaceChatHistory } from './utils' import { cloneDeep, omit, uniqWith, isEqual } from 'lodash' import { getDataSource } from './DataSource' @@ -54,7 +55,7 @@ import { Tool } from './database/entities/Tool' import { Assistant } from './database/entities/Assistant' import { ChatflowPool } from './ChatflowPool' import { CachePool } from './CachePool' -import { ICommonObject, INodeOptionsValue } from 'flowise-components' +import { ICommonObject, IMessage, INodeOptionsValue } from 'flowise-components' import { createRateLimiter, getRateLimiter, initializeRateLimiter } from './utils/rateLimit' import { addAPIKey, compareKeys, deleteAPIKey, getApiKey, getAPIKeys, updateAPIKey } from './utils/apiKey' @@ -1267,14 +1268,14 @@ export class App { * @param {IReactFlowEdge[]} edges * @returns {string | undefined} */ - findMemoryLabel(nodes: IReactFlowNode[], edges: IReactFlowEdge[]): string | undefined { + findMemoryLabel(nodes: IReactFlowNode[], edges: IReactFlowEdge[]): IReactFlowNode | undefined { const memoryNodes = nodes.filter((node) => node.data.category === 'Memory') const memoryNodeIds = memoryNodes.map((mem) => mem.data.id) for (const edge of edges) { if (memoryNodeIds.includes(edge.source)) { const memoryNode = nodes.find((node) => node.data.id === edge.source) - return memoryNode ? memoryNode.data.label : undefined + return memoryNode } } return undefined @@ -1395,6 +1396,19 @@ export class App { isStreamValid = isFlowValidForStream(nodes, endingNodeData) + let chatHistory: IMessage[] | string = incomingInput.history + if ( + endingNodeData.inputs?.memory && + !incomingInput.history && + (incomingInput.chatId || incomingInput.overrideConfig?.sessionId) + ) { + const memoryNodeId = endingNodeData.inputs?.memory.split('.')[0].replace('{{', '') + const memoryNode = nodes.find((node) => node.data.id === memoryNodeId) + if (memoryNode) { + chatHistory = await replaceChatHistory(memoryNode, incomingInput, this.AppDataSource, databaseEntities, logger) + } + } + /*** Get Starting Nodes with Non-Directed Graph ***/ const constructedObj = constructGraphs(nodes, edges, true) const nonDirectedGraph = constructedObj.graph @@ -1409,7 +1423,7 @@ export class App { depthQueue, this.nodesPool.componentNodes, incomingInput.question, - incomingInput.history, + chatHistory, chatId, chatflowid, this.AppDataSource, @@ -1429,7 +1443,7 @@ export class App { nodeToExecute.data, reactFlowNodes, incomingInput.question, - incomingInput.history + chatHistory ) nodeToExecuteData = reactFlowNodeData @@ -1446,11 +1460,17 @@ export class App { let sessionId = undefined if (nodeToExecuteData.instance) sessionId = checkMemorySessionId(nodeToExecuteData.instance, chatId) - const memoryType = this.findMemoryLabel(nodes, edges) + const memoryNode = this.findMemoryLabel(nodes, edges) + const memoryType = memoryNode?.data.label + + let chatHistory: IMessage[] | string = incomingInput.history + if (memoryNode && !incomingInput.history && (incomingInput.chatId || incomingInput.overrideConfig?.sessionId)) { + chatHistory = await replaceChatHistory(memoryNode, incomingInput, this.AppDataSource, databaseEntities, logger) + } let result = isStreamValid ? await nodeInstance.run(nodeToExecuteData, incomingInput.question, { - chatHistory: incomingInput.history, + chatHistory, socketIO, socketIOClientId: incomingInput.socketIOClientId, logger, @@ -1460,7 +1480,7 @@ export class App { chatId }) : await nodeInstance.run(nodeToExecuteData, incomingInput.question, { - chatHistory: incomingInput.history, + chatHistory, logger, appDataSource: this.AppDataSource, databaseEntities, diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index 423d32afe..0b1e62d25 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -15,7 +15,8 @@ import { IOverrideConfig, IReactFlowEdge, IReactFlowNode, - IVariableDict + IVariableDict, + IncomingInput } from '../Interface' import { cloneDeep, get, isEqual } from 'lodash' import { @@ -216,7 +217,7 @@ export const buildLangchain = async ( depthQueue: IDepthQueue, componentNodes: IComponentNodes, question: string, - chatHistory: IMessage[], + chatHistory: IMessage[] | string, chatId: string, chatflowid: string, appDataSource: DataSource, @@ -347,8 +348,8 @@ export const clearAllSessionMemory = async ( node.data.inputs.sessionId = sessionId } - if (newNodeInstance.clearSessionMemory) { - await newNodeInstance?.clearSessionMemory(node.data, { chatId, appDataSource, databaseEntities, logger }) + if (newNodeInstance.memoryMethods && newNodeInstance.memoryMethods.clearSessionMemory) { + await newNodeInstance.memoryMethods.clearSessionMemory(node.data, { chatId, appDataSource, databaseEntities, logger }) } } } @@ -380,8 +381,8 @@ export const clearSessionMemoryFromViewMessageDialog = async ( if (sessionId && node.data.inputs) node.data.inputs.sessionId = sessionId - if (newNodeInstance.clearSessionMemory) { - await newNodeInstance?.clearSessionMemory(node.data, { chatId, appDataSource, databaseEntities, logger }) + if (newNodeInstance.memoryMethods && newNodeInstance.memoryMethods.clearSessionMemory) { + await newNodeInstance.memoryMethods.clearSessionMemory(node.data, { chatId, appDataSource, databaseEntities, logger }) return } } @@ -399,7 +400,7 @@ export const getVariableValue = ( paramValue: string, reactFlowNodes: IReactFlowNode[], question: string, - chatHistory: IMessage[], + chatHistory: IMessage[] | string, isAcceptVariable = false ) => { let returnVal = paramValue @@ -432,7 +433,10 @@ export const getVariableValue = ( } if (isAcceptVariable && variableFullPath === CHAT_HISTORY_VAR_PREFIX) { - variableDict[`{{${variableFullPath}}}`] = handleEscapeCharacters(convertChatHistoryToText(chatHistory), false) + variableDict[`{{${variableFullPath}}}`] = handleEscapeCharacters( + typeof chatHistory === 'string' ? chatHistory : convertChatHistoryToText(chatHistory), + false + ) } // Split by first occurrence of '.' to get just nodeId @@ -475,7 +479,7 @@ export const resolveVariables = ( reactFlowNodeData: INodeData, reactFlowNodes: IReactFlowNode[], question: string, - chatHistory: IMessage[] + chatHistory: IMessage[] | string ): INodeData => { let flowNodeData = cloneDeep(reactFlowNodeData) const types = 'inputs' @@ -873,3 +877,39 @@ export const checkMemorySessionId = (instance: any, chatId: string): string | un return instance.memory.chatHistory.sessionId return undefined } + +/** + * Replace chatHistory if incomingInput.history is empty and sessionId/chatId is provided + * @param {IReactFlowNode} memoryNode + * @param {IncomingInput} incomingInput + * @param {DataSource} appDataSource + * @param {IDatabaseEntity} databaseEntities + * @param {any} logger + * @returns {string} + */ +export const replaceChatHistory = async ( + memoryNode: IReactFlowNode, + incomingInput: IncomingInput, + appDataSource: DataSource, + databaseEntities: IDatabaseEntity, + logger: any +): Promise => { + const nodeInstanceFilePath = memoryNode.data.filePath as string + const nodeModule = await import(nodeInstanceFilePath) + const newNodeInstance = new nodeModule.nodeClass() + + if (incomingInput.overrideConfig?.sessionId && memoryNode.data.inputs) { + memoryNode.data.inputs.sessionId = incomingInput.overrideConfig.sessionId + } + + if (newNodeInstance.memoryMethods && newNodeInstance.memoryMethods.getChatMessages) { + return await newNodeInstance.memoryMethods.getChatMessages(memoryNode.data, { + chatId: incomingInput.chatId, + appDataSource, + databaseEntities, + logger + }) + } + + return '' +} From 674b8996f013ec8d9ea3c3db9f13ddf43dac0b38 Mon Sep 17 00:00:00 2001 From: Henry Date: Thu, 30 Nov 2023 16:57:34 +0000 Subject: [PATCH 24/54] add back accidentally removed about dialog --- .../MainLayout/Header/ProfileSection/index.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/layout/MainLayout/Header/ProfileSection/index.js b/packages/ui/src/layout/MainLayout/Header/ProfileSection/index.js index c2de41b2d..ac114c6cc 100644 --- a/packages/ui/src/layout/MainLayout/Header/ProfileSection/index.js +++ b/packages/ui/src/layout/MainLayout/Header/ProfileSection/index.js @@ -28,7 +28,7 @@ import Transitions from 'ui-component/extended/Transitions' import AboutDialog from 'ui-component/dialog/AboutDialog' // assets -import { IconLogout, IconSettings } from '@tabler/icons' +import { IconLogout, IconSettings, IconInfoCircle } from '@tabler/icons' import './index.css' @@ -135,6 +135,18 @@ const ProfileSection = ({ username, handleLogout }) => { } }} > + { + setOpen(false) + setAboutDialogOpen(true) + }} + > + + + + About Flowise
} /> + {localStorage.getItem('username') && localStorage.getItem('password') && ( Date: Fri, 1 Dec 2023 10:30:56 +0530 Subject: [PATCH 25/54] LS Prompt hub: adding credentials to the filtering logic --- packages/server/src/index.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 92b32b592..35139bf0d 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1121,9 +1121,21 @@ export class App { this.app.post('/api/v1/prompts-list', async (req: Request, res: Response) => { try { + const credential = await this.AppDataSource.getRepository(Credential).findOneBy({ + id: req.body.credential + }) + + if (!credential) return res.status(404).json({ error: `Credential ${req.body.credential} not found` }) + // Decrypt credentialData + const decryptedCredentialData = await decryptCredentialData(credential.encryptedData, credential.credentialName, undefined) + + const headers = {} + // @ts-ignore + headers['x-api-key'] = decryptedCredentialData.langsmithApiKey + const tags = req.body.tags ? `tags=${req.body.tags}` : '' const url = `https://web.hub.langchain.com/repos/?${tags}offset=0&limit=20&has_commits=true&sort_field=num_likes&sort_direction=desc&is_archived=false` - axios.get(url).then((response) => { + axios.get(url, headers).then((response) => { if (response.data.repos) { return res.json({ status: 'OK', repos: response.data.repos }) } From 86b236182b920c5332af6713173641eabd315a51 Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 1 Dec 2023 15:35:00 +0000 Subject: [PATCH 26/54] added fix to restart flow when prompt is question prefix --- packages/server/src/utils/index.ts | 43 ++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index 0b1e62d25..2bf1c04a4 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -558,9 +558,20 @@ export const isStartNodeDependOnInput = (startingNodes: IReactFlowNode[], nodes: if (inputVariables.length > 0) return true } } - const whitelistNodeNames = ['vectorStoreToDocument', 'autoGPT'] + const whitelistNodeNames = ['vectorStoreToDocument', 'autoGPT', 'chatPromptTemplate', 'promptTemplate'] //If these nodes are found, chatflow cannot be reused for (const node of nodes) { - if (whitelistNodeNames.includes(node.data.name)) return true + if (node.data.name === 'chatPromptTemplate' || node.data.name === 'promptTemplate') { + let promptValues: ICommonObject = {} + const promptValuesRaw = node.data.inputs?.promptValues + if (promptValuesRaw) { + try { + promptValues = typeof promptValuesRaw === 'object' ? promptValuesRaw : JSON.parse(promptValuesRaw) + } catch (exception) { + console.error(exception) + } + } + if (getAllValuesFromJson(promptValues).includes(`{{${QUESTION_VAR_PREFIX}}}`)) return true + } else if (whitelistNodeNames.includes(node.data.name)) return true } return false } @@ -913,3 +924,31 @@ export const replaceChatHistory = async ( return '' } + +/** + * Get all values from a JSON object + * @param {any} obj + * @returns {any[]} + */ +export const getAllValuesFromJson = (obj: any): any[] => { + const values: any[] = [] + + function extractValues(data: any) { + if (typeof data === 'object' && data !== null) { + if (Array.isArray(data)) { + for (const item of data) { + extractValues(item) + } + } else { + for (const key in data) { + extractValues(data[key]) + } + } + } else { + values.push(data) + } + } + + extractValues(obj) + return values +} From 7df2e3fe6d7058649ac2c33d9dc5bf548043fe23 Mon Sep 17 00:00:00 2001 From: automaton82 Date: Fri, 1 Dec 2023 20:23:29 -0500 Subject: [PATCH 27/54] Fix for issue#1323 - delete duplicate chatflow --- packages/ui/src/views/canvas/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/views/canvas/index.js b/packages/ui/src/views/canvas/index.js index 29602a4f2..086983987 100644 --- a/packages/ui/src/views/canvas/index.js +++ b/packages/ui/src/views/canvas/index.js @@ -170,7 +170,7 @@ const Canvas = () => { try { await chatflowsApi.deleteChatflow(chatflow.id) localStorage.removeItem(`${chatflow.id}_INTERNAL`) - navigate(-1) + navigate('/') } catch (error) { const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` enqueueSnackbar({ From f60360f3b94ba8e2ec080f44d683902eebb5e16e Mon Sep 17 00:00:00 2001 From: automaton82 Date: Fri, 1 Dec 2023 21:07:43 -0500 Subject: [PATCH 28/54] Fixing #1324 by lifting ConfirmDialog up. --- packages/ui/src/ui-component/button/FlowListMenu.js | 2 -- packages/ui/src/views/chatflows/index.js | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/ui-component/button/FlowListMenu.js b/packages/ui/src/ui-component/button/FlowListMenu.js index b242d2cb2..94ecdd01c 100644 --- a/packages/ui/src/ui-component/button/FlowListMenu.js +++ b/packages/ui/src/ui-component/button/FlowListMenu.js @@ -22,7 +22,6 @@ 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 TagDialog from '../dialog/TagDialog' @@ -264,7 +263,6 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) { Delete - { )} + ) } From 14c506adb34552717fc476c6e6d22de9e8a3b6f4 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Sat, 2 Dec 2023 11:10:37 +0530 Subject: [PATCH 29/54] XSS Fixes --- packages/server/package.json | 1 + packages/server/src/index.ts | 430 ++++++++++++++++++++++++----------- 2 files changed, 303 insertions(+), 128 deletions(-) diff --git a/packages/server/package.json b/packages/server/package.json index 6f4ccaf41..f285e6639 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -54,6 +54,7 @@ "express": "^4.17.3", "express-basic-auth": "^1.2.1", "express-rate-limit": "^6.9.0", + "express-validator": "^7.0.1", "flowise-components": "*", "flowise-ui": "*", "moment-timezone": "^0.5.34", diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index d87d2c0ac..61f34e923 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -58,6 +58,7 @@ import { CachePool } from './CachePool' import { ICommonObject, IMessage, INodeOptionsValue } from 'flowise-components' import { createRateLimiter, getRateLimiter, initializeRateLimiter } from './utils/rateLimit' import { addAPIKey, compareKeys, deleteAPIKey, getApiKey, getAPIKeys, updateAPIKey } from './utils/apiKey' +import { body, param, query, validationResult } from 'express-validator' export class App { app: express.Application @@ -115,6 +116,9 @@ export class App { // Allow access from * this.app.use(cors()) + // Switch off the default 'X-Powered-By: Express' header + this.app.disable('x-powered-by') + // Add the expressRequestLogger middleware to log all requests this.app.use(expressRequestLogger) @@ -180,17 +184,27 @@ export class App { }) // Get specific component node via name - this.app.get('/api/v1/nodes/:name', (req: Request, res: Response) => { - if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentNodes, req.params.name)) { + this.app.get('/api/v1/nodes/:name', param('name').notEmpty().escape(), (req: Request, res: Response) => { + const name = req.params.name + const result = validationResult(req) + if (!result.isEmpty()) { + throw new Error(`Node ${name} not found`) + } + if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentNodes, name)) { return res.json(this.nodesPool.componentNodes[req.params.name]) } else { - throw new Error(`Node ${req.params.name} not found`) + throw new Error(`Node ${name} not found`) } }) // Get component credential via name - this.app.get('/api/v1/components-credentials/:name', (req: Request, res: Response) => { - if (!req.params.name.includes('&')) { + this.app.get('/api/v1/components-credentials/:name', param('name').notEmpty().escape(), (req: Request, res: Response) => { + const name = req.params.name + const result = validationResult(req) + if (!result.isEmpty()) { + throw new Error(`Credential ${name} not found`) + } + if (!req.params.name.includes('&')) { if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentCredentials, req.params.name)) { return res.json(this.nodesPool.componentCredentials[req.params.name]) } else { @@ -198,7 +212,7 @@ export class App { } } else { const returnResponse = [] - for (const name of req.params.name.split('&')) { + for (const name of req.params.name.split('&')) { if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentCredentials, name)) { returnResponse.push(this.nodesPool.componentCredentials[name]) } else { @@ -210,9 +224,14 @@ export class App { }) // Returns specific component node icon via name - this.app.get('/api/v1/node-icon/:name', (req: Request, res: Response) => { - if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentNodes, req.params.name)) { - const nodeInstance = this.nodesPool.componentNodes[req.params.name] + this.app.get('/api/v1/node-icon/:name', param('name').notEmpty().escape(), (req: Request, res: Response) => { + const name = req.params.name + const result = validationResult(req) + if (!result.isEmpty()) { + throw new Error(`Node ${name} not found`) + } + if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentNodes, name)) { + const nodeInstance = this.nodesPool.componentNodes[name] if (nodeInstance.icon === undefined) { throw new Error(`Node ${req.params.name} icon not found`) } @@ -221,38 +240,48 @@ export class App { const filepath = nodeInstance.icon res.sendFile(filepath) } else { - throw new Error(`Node ${req.params.name} icon is missing icon`) + throw new Error(`Node ${name} icon is missing icon`) } } else { - throw new Error(`Node ${req.params.name} not found`) + throw new Error(`Node ${name} not found`) } }) // Returns specific component credential icon via name - this.app.get('/api/v1/components-credentials-icon/:name', (req: Request, res: Response) => { - if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentCredentials, req.params.name)) { - const credInstance = this.nodesPool.componentCredentials[req.params.name] + this.app.get('/api/v1/components-credentials-icon/:name', param('name').notEmpty().escape(), (req: Request, res: Response) => { + const name = req.params.name + const result = validationResult(req) + if (!result.isEmpty()) { + throw new Error(`Credential ${name} not found`) + } + if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentCredentials, name)) { + const credInstance = this.nodesPool.componentCredentials[name] if (credInstance.icon === undefined) { - throw new Error(`Credential ${req.params.name} icon not found`) + throw new Error(`Credential ${name} icon not found`) } if (credInstance.icon.endsWith('.svg') || credInstance.icon.endsWith('.png') || credInstance.icon.endsWith('.jpg')) { const filepath = credInstance.icon res.sendFile(filepath) } else { - throw new Error(`Credential ${req.params.name} icon is missing icon`) + throw new Error(`Credential ${name} is missing icon`) } } else { - throw new Error(`Credential ${req.params.name} not found`) + throw new Error(`Credential ${name} not found`) } }) // load async options - this.app.post('/api/v1/node-load-method/:name', async (req: Request, res: Response) => { + this.app.post('/api/v1/node-load-method/:name', param('name').notEmpty().escape(), async (req: Request, res: Response) => { + const name = req.params.name + const result = validationResult(req) + if (!result.isEmpty()) { + throw new Error(`Node ${name} not found`) + } const nodeData: INodeData = req.body - if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentNodes, req.params.name)) { + if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentNodes, name)) { try { - const nodeInstance = this.nodesPool.componentNodes[req.params.name] + const nodeInstance = this.nodesPool.componentNodes[name] const methodName = nodeData.loadMethod || '' const returnOptions: INodeOptionsValue[] = await nodeInstance.loadMethods![methodName]!.call(nodeInstance, nodeData, { @@ -265,7 +294,7 @@ export class App { return res.json([]) } } else { - res.status(404).send(`Node ${req.params.name} not found`) + res.status(404).send(`Node ${name} not found`) return } }) @@ -281,7 +310,11 @@ export class App { }) // Get specific chatflow via api key - this.app.get('/api/v1/chatflows/apikey/:apiKey', async (req: Request, res: Response) => { + this.app.get('/api/v1/chatflows/apikey/:apiKey', param('apiKey').notEmpty().escape(), async (req: Request, res: Response) => { + const result = validationResult(req) + if (!result.isEmpty()) { + return res.status(401).send('Unauthorized') + } try { const apiKey = await getApiKey(req.params.apiKey) if (!apiKey) return res.status(401).send('Unauthorized') @@ -293,14 +326,19 @@ export class App { .orderBy('cf.name', 'ASC') .getMany() if (chatflows.length >= 1) return res.status(200).send(chatflows) - return res.status(404).send('Chatflow not found') + return res.status(404).send('APIKey not found') } catch (err: any) { return res.status(500).send(err?.message) } }) // Get specific chatflow via id - this.app.get('/api/v1/chatflows/:id', async (req: Request, res: Response) => { + this.app.get('/api/v1/chatflows/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { + const chatflowId = req.params.id + const result = validationResult(req) + if (!result.isEmpty()) { + return res.status(404).send(`Chatflow ${chatflowId} not found`) + } const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ id: req.params.id }) @@ -309,7 +347,12 @@ export class App { }) // Get specific chatflow via id (PUBLIC endpoint, used when sharing chatbot link) - this.app.get('/api/v1/public-chatflows/:id', async (req: Request, res: Response) => { + this.app.get('/api/v1/public-chatflows/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { + const chatflowId = req.params.id + const result = validationResult(req) + if (!result.isEmpty()) { + return res.status(404).send(`Chatflow ${chatflowId} not found`) + } const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ id: req.params.id }) @@ -331,48 +374,69 @@ export class App { }) // Update chatflow - this.app.put('/api/v1/chatflows/:id', async (req: Request, res: Response) => { - const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ - id: req.params.id - }) + this.app.put( + '/api/v1/chatflows/:id', + body('chatflow.id').notEmpty(), + param('id').notEmpty().escape(), + async (req: Request, res: Response) => { + const chatflowId = req.params.id + const valResult = validationResult(req) + if (!valResult.isEmpty()) { + return res.status(404).send(`Chatflow ${chatflowId} not found`) + } + const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ + id: chatflowId + }) - if (!chatflow) { - res.status(404).send(`Chatflow ${req.params.id} not found`) - return + if (!chatflow) { + res.status(404).send(`Chatflow ${chatflowId} not found`) + return + } + + const body = req.body + const updateChatFlow = new ChatFlow() + Object.assign(updateChatFlow, body) + + updateChatFlow.id = chatflow.id + createRateLimiter(updateChatFlow) + + this.AppDataSource.getRepository(ChatFlow).merge(chatflow, updateChatFlow) + const result = await this.AppDataSource.getRepository(ChatFlow).save(chatflow) + + // 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) } - - const body = req.body - const updateChatFlow = new ChatFlow() - Object.assign(updateChatFlow, body) - - updateChatFlow.id = chatflow.id - createRateLimiter(updateChatFlow) - - this.AppDataSource.getRepository(ChatFlow).merge(chatflow, updateChatFlow) - const result = await this.AppDataSource.getRepository(ChatFlow).save(chatflow) - - // 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) - }) + ) // Delete chatflow via id - this.app.delete('/api/v1/chatflows/:id', async (req: Request, res: Response) => { + this.app.delete('/api/v1/chatflows/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { + const chatflowId = req.params.id + const result = validationResult(req) + if (!result.isEmpty()) { + return res.status(404).send(`Chatflow ${chatflowId} not found`) + } const results = await this.AppDataSource.getRepository(ChatFlow).delete({ id: req.params.id }) return res.json(results) }) // Check if chatflow valid for streaming - this.app.get('/api/v1/chatflows-streaming/:id', async (req: Request, res: Response) => { + this.app.get('/api/v1/chatflows-streaming/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { + const chatflowId = req.params.id + const result = validationResult(req) + if (!result.isEmpty()) { + return res.status(404).send(`Chatflow ${chatflowId} not found`) + } + const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ - id: req.params.id + id: chatflowId }) - if (!chatflow) return res.status(404).send(`Chatflow ${req.params.id} not found`) + if (!chatflow) return res.status(404).send(`Chatflow ${chatflowId} not found`) /*** Get Ending Node with Directed Graph ***/ const flowData = chatflow.flowData @@ -402,58 +466,84 @@ export class App { // ---------------------------------------- // Get all chatmessages from chatflowid - this.app.get('/api/v1/chatmessage/:id', async (req: Request, res: Response) => { - const sortOrder = req.query?.order as string | undefined - const chatId = req.query?.chatId as string | undefined - const memoryType = req.query?.memoryType as string | undefined - const sessionId = req.query?.sessionId as string | undefined - const startDate = req.query?.startDate as string | undefined - const endDate = req.query?.endDate as string | undefined - let chatTypeFilter = req.query?.chatType as chatType | undefined - - if (chatTypeFilter) { - try { - const chatTypeFilterArray = JSON.parse(chatTypeFilter) - if (chatTypeFilterArray.includes(chatType.EXTERNAL) && chatTypeFilterArray.includes(chatType.INTERNAL)) { - chatTypeFilter = undefined - } else if (chatTypeFilterArray.includes(chatType.EXTERNAL)) { - chatTypeFilter = chatType.EXTERNAL - } else if (chatTypeFilterArray.includes(chatType.INTERNAL)) { - chatTypeFilter = chatType.INTERNAL - } - } catch (e) { - return res.status(500).send(e) + this.app.get( + '/api/v1/chatmessage/:id', + query('chatId').notEmpty().escape(), + query('sortOrder').notEmpty().escape(), + query('memoryType').notEmpty().escape(), + query('sessionId').notEmpty().escape(), + query('startDate').notEmpty().escape(), + query('endDate').notEmpty().escape(), + query('chatTypeFilter').notEmpty().escape(), + async (req: Request, res: Response) => { + const result = validationResult(req) + if (!result.isEmpty()) { + return res.status(404).send(`Chatmessage not found`) } - } + const sortOrder = req.query?.order as string | undefined + const chatId = req.query?.chatId as string | undefined + const memoryType = req.query?.memoryType as string | undefined + const sessionId = req.query?.sessionId as string | undefined + const startDate = req.query?.startDate as string | undefined + const endDate = req.query?.endDate as string | undefined + let chatTypeFilter = req.query?.chatType as chatType | undefined - const chatmessages = await this.getChatMessage( - req.params.id, - chatTypeFilter, - sortOrder, - chatId, - memoryType, - sessionId, - startDate, - endDate - ) - return res.json(chatmessages) - }) + if (chatTypeFilter) { + try { + const chatTypeFilterArray = JSON.parse(chatTypeFilter) + if (chatTypeFilterArray.includes(chatType.EXTERNAL) && chatTypeFilterArray.includes(chatType.INTERNAL)) { + chatTypeFilter = undefined + } else if (chatTypeFilterArray.includes(chatType.EXTERNAL)) { + chatTypeFilter = chatType.EXTERNAL + } else if (chatTypeFilterArray.includes(chatType.INTERNAL)) { + chatTypeFilter = chatType.INTERNAL + } + } catch (e) { + return res.status(500).send(e) + } + } + + const chatmessages = await this.getChatMessage( + req.params.id, + chatTypeFilter, + sortOrder, + chatId, + memoryType, + sessionId, + startDate, + endDate + ) + return res.json(chatmessages) + } + ) // Get internal chatmessages from chatflowid - this.app.get('/api/v1/internal-chatmessage/:id', async (req: Request, res: Response) => { + this.app.get('/api/v1/internal-chatmessage/:id', param('chatId').notEmpty().escape(), async (req: Request, res: Response) => { + const result = validationResult(req) + if (!result.isEmpty()) { + return res.status(404).send(`Chatmessage not found`) + } const chatmessages = await this.getChatMessage(req.params.id, chatType.INTERNAL) return res.json(chatmessages) }) // Add chatmessages for chatflowid - this.app.post('/api/v1/chatmessage/:id', async (req: Request, res: Response) => { + this.app.post('/api/v1/chatmessage/:id', param('chatId').notEmpty().escape(), async (req: Request, res: Response) => { + const result = validationResult(req) + if (!result.isEmpty()) { + return res.status(404).send(`Chatmessage not found`) + } const body = req.body const results = await this.addChatMessage(body) return res.json(results) }) // Delete all chatmessages from chatId - this.app.delete('/api/v1/chatmessage/:id', async (req: Request, res: Response) => { + this.app.delete('/api/v1/chatmessage/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { + const result = validationResult(req) + if (!result.isEmpty()) { + return res.status(404).send(`Chatmessage not found`) + } const chatflowid = req.params.id const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ id: chatflowid @@ -537,7 +627,11 @@ export class App { }) // Get specific credential - this.app.get('/api/v1/credentials/:id', async (req: Request, res: Response) => { + this.app.get('/api/v1/credentials/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { + const result = validationResult(req) + if (!result.isEmpty()) { + return res.status(404).send(`Credential ${req.params.id} not found`) + } const credential = await this.AppDataSource.getRepository(Credential).findOneBy({ id: req.params.id }) @@ -558,7 +652,11 @@ export class App { }) // Update credential - this.app.put('/api/v1/credentials/:id', async (req: Request, res: Response) => { + this.app.put('/api/v1/credentials/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { + const valResult = validationResult(req) + if (!valResult.isEmpty()) { + return res.status(404).send(`Credential ${req.params.id} not found`) + } const credential = await this.AppDataSource.getRepository(Credential).findOneBy({ id: req.params.id }) @@ -574,7 +672,11 @@ export class App { }) // Delete all chatmessages from chatflowid - this.app.delete('/api/v1/credentials/:id', async (req: Request, res: Response) => { + this.app.delete('/api/v1/credentials/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { + const valResult = validationResult(req) + if (!valResult.isEmpty()) { + return res.status(404).send(`Credential ${req.params.id} not found`) + } const results = await this.AppDataSource.getRepository(Credential).delete({ id: req.params.id }) return res.json(results) }) @@ -590,7 +692,11 @@ export class App { }) // Get specific tool - this.app.get('/api/v1/tools/:id', async (req: Request, res: Response) => { + this.app.get('/api/v1/tools/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { + const valResult = validationResult(req) + if (!valResult.isEmpty()) { + return res.status(404).send(`Tool ${req.params.id} not found`) + } const tool = await this.AppDataSource.getRepository(Tool).findOneBy({ id: req.params.id }) @@ -610,7 +716,11 @@ export class App { }) // Update tool - this.app.put('/api/v1/tools/:id', async (req: Request, res: Response) => { + this.app.put('/api/v1/tools/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { + const valResult = validationResult(req) + if (!valResult.isEmpty()) { + return res.status(404).send(`Tool ${req.params.id} not found`) + } const tool = await this.AppDataSource.getRepository(Tool).findOneBy({ id: req.params.id }) @@ -631,7 +741,11 @@ export class App { }) // Delete tool - this.app.delete('/api/v1/tools/:id', async (req: Request, res: Response) => { + this.app.delete('/api/v1/tools/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { + const valResult = validationResult(req) + if (!valResult.isEmpty()) { + return res.status(404).send(`Tool ${req.params.id} not found`) + } const results = await this.AppDataSource.getRepository(Tool).delete({ id: req.params.id }) return res.json(results) }) @@ -647,7 +761,11 @@ export class App { }) // Get specific assistant - this.app.get('/api/v1/assistants/:id', async (req: Request, res: Response) => { + this.app.get('/api/v1/assistants/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { + const valResult = validationResult(req) + if (!valResult.isEmpty()) { + return res.status(404).send(`Assistant ${req.params.id} not found`) + } const assistant = await this.AppDataSource.getRepository(Assistant).findOneBy({ id: req.params.id }) @@ -655,33 +773,46 @@ export class App { }) // Get assistant object - this.app.get('/api/v1/openai-assistants/:id', async (req: Request, res: Response) => { - const credentialId = req.query.credential as string - const credential = await this.AppDataSource.getRepository(Credential).findOneBy({ - id: credentialId - }) + this.app.get( + '/api/v1/openai-assistants/:id', + param('id').notEmpty().escape(), + query('credential').notEmpty().escape(), + async (req: Request, res: Response) => { + const valResult = validationResult(req) + if (!valResult.isEmpty()) { + return res.status(404).send(`Assistant or Credential not found`) + } + const credentialId = req.query.credential as string + const credential = await this.AppDataSource.getRepository(Credential).findOneBy({ + id: credentialId + }) - if (!credential) return res.status(404).send(`Credential ${credentialId} not found`) + if (!credential) return res.status(404).send(`Credential ${credentialId} not found`) - // Decrpyt credentialData - const decryptedCredentialData = await decryptCredentialData(credential.encryptedData) - const openAIApiKey = decryptedCredentialData['openAIApiKey'] - if (!openAIApiKey) return res.status(404).send(`OpenAI ApiKey not found`) + // Decrpyt credentialData + const decryptedCredentialData = await decryptCredentialData(credential.encryptedData) + const openAIApiKey = decryptedCredentialData['openAIApiKey'] + if (!openAIApiKey) return res.status(404).send(`OpenAI ApiKey not found`) - const openai = new OpenAI({ apiKey: openAIApiKey }) - const retrievedAssistant = await openai.beta.assistants.retrieve(req.params.id) - const resp = await openai.files.list() - const existingFiles = resp.data ?? [] + const openai = new OpenAI({ apiKey: openAIApiKey }) + const retrievedAssistant = await openai.beta.assistants.retrieve(req.params.id) + const resp = await openai.files.list() + const existingFiles = resp.data ?? [] - if (retrievedAssistant.file_ids && retrievedAssistant.file_ids.length) { - ;(retrievedAssistant as any).files = existingFiles.filter((file) => retrievedAssistant.file_ids.includes(file.id)) + if (retrievedAssistant.file_ids && retrievedAssistant.file_ids.length) { + ;(retrievedAssistant as any).files = existingFiles.filter((file) => retrievedAssistant.file_ids.includes(file.id)) + } + + return res.json(retrievedAssistant) } - - return res.json(retrievedAssistant) - }) + ) // List available assistants - this.app.get('/api/v1/openai-assistants', async (req: Request, res: Response) => { + this.app.get('/api/v1/openai-assistants', query('credential').notEmpty().escape(), async (req: Request, res: Response) => { + const valResult = validationResult(req) + if (!valResult.isEmpty()) { + return res.status(404).send(`Assistant or Credential not found`) + } const credentialId = req.query.credential as string const credential = await this.AppDataSource.getRepository(Credential).findOneBy({ id: credentialId @@ -816,7 +947,11 @@ export class App { }) // Update assistant - this.app.put('/api/v1/assistants/:id', async (req: Request, res: Response) => { + this.app.put('/api/v1/assistants/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { + const valResult = validationResult(req) + if (!valResult.isEmpty()) { + return res.status(404).send(`Assistant ${req.params.id} not found`) + } const assistant = await this.AppDataSource.getRepository(Assistant).findOneBy({ id: req.params.id }) @@ -924,7 +1059,11 @@ export class App { }) // Delete assistant - this.app.delete('/api/v1/assistants/:id', async (req: Request, res: Response) => { + this.app.delete('/api/v1/assistants/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { + const valResult = validationResult(req) + if (!valResult.isEmpty()) { + return res.status(404).send(`Assistant ${req.params.id} not found`) + } const assistant = await this.AppDataSource.getRepository(Assistant).findOneBy({ id: req.params.id }) @@ -964,6 +1103,12 @@ export class App { // Download file from assistant this.app.post('/api/v1/openai-assistants-file', async (req: Request, res: Response) => { const filePath = path.join(getUserHome(), '.flowise', 'openai-assistant', req.body.fileName) + //raise error if file path is not absolute + if (!path.isAbsolute(filePath)) return res.status(500).send(`Invalid file path`) + //raise error if file path contains '..' + if (filePath.includes('..')) return res.status(500).send(`Invalid file path`) + //only return from the .flowise openai-assistant folder + if (!(filePath.includes('.flowise') && filePath.includes('openai-assistant'))) return res.status(500).send(`Invalid file path`) res.setHeader('Content-Disposition', 'attachment; filename=' + path.basename(filePath)) const fileStream = fs.createReadStream(filePath) fileStream.pipe(res) @@ -973,7 +1118,11 @@ export class App { // Configuration // ---------------------------------------- - this.app.get('/api/v1/flow-config/:id', async (req: Request, res: Response) => { + this.app.get('/api/v1/flow-config/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { + const valResult = validationResult(req) + if (!valResult.isEmpty()) { + return res.status(404).send(`Chatflow ${req.params.id} not found`) + } const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ id: req.params.id }) @@ -1032,7 +1181,11 @@ export class App { } ) - this.app.post('/api/v1/vector/internal-upsert/:id', async (req: Request, res: Response) => { + this.app.post('/api/v1/vector/internal-upsert/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { + const valResult = validationResult(req) + if (!valResult.isEmpty()) { + return res.status(404).send(`Upsert ${req.params.id} not found`) + } await this.buildChatflow(req, res, undefined, true, true) }) @@ -1043,15 +1196,24 @@ export class App { // Send input message and get prediction result (External) this.app.post( '/api/v1/prediction/:id', + param('id').notEmpty().escape(), upload.array('files'), (req: Request, res: Response, next: NextFunction) => getRateLimiter(req, res, next), async (req: Request, res: Response) => { + const valResult = validationResult(req) + if (!valResult.isEmpty()) { + return res.status(404).send(`Error Processing Prediction`) + } await this.buildChatflow(req, res, socketIO) } ) // Send input message and get prediction result (Internal) - this.app.post('/api/v1/internal-prediction/:id', async (req: Request, res: Response) => { + this.app.post('/api/v1/internal-prediction/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { + const valResult = validationResult(req) + if (!valResult.isEmpty()) { + return res.status(404).send(`Error Processing Prediction`) + } await this.buildChatflow(req, res, socketIO, true) }) @@ -1146,19 +1308,31 @@ export class App { }) // Update api key - this.app.put('/api/v1/apikey/:id', async (req: Request, res: Response) => { + this.app.put('/api/v1/apikey/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { + const valResult = validationResult(req) + if (!valResult.isEmpty()) { + return res.status(404).send(`Error Processing Update API Key`) + } const keys = await updateAPIKey(req.params.id, req.body.keyName) return addChatflowsCount(keys, res) }) // Delete new api key - this.app.delete('/api/v1/apikey/:id', async (req: Request, res: Response) => { + this.app.delete('/api/v1/apikey/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { + const valResult = validationResult(req) + if (!valResult.isEmpty()) { + return res.status(404).send(`Error Processing Update API Key`) + } const keys = await deleteAPIKey(req.params.id) return addChatflowsCount(keys, res) }) // Verify api key - this.app.get('/api/v1/verify/apikey/:apiKey', async (req: Request, res: Response) => { + this.app.get('/api/v1/verify/apikey/:apiKey', param('apikey').notEmpty().escape(), async (req: Request, res: Response) => { + const valResult = validationResult(req) + if (!valResult.isEmpty()) { + return res.status(404).send(`Error Processing API Key`) + } try { const apiKey = await getApiKey(req.params.apiKey) if (!apiKey) return res.status(401).send('Unauthorized') From c856d94f7c46aea8428ce649c80feefc7442525d Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Sat, 2 Dec 2023 11:21:06 +0530 Subject: [PATCH 30/54] Dependabot Alerts --- packages/components/package.json | 2 +- packages/server/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/components/package.json b/packages/components/package.json index bea9a7a06..a75b9eb53 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -36,7 +36,7 @@ "@upstash/redis": "^1.22.1", "@zilliz/milvus2-sdk-node": "^2.2.24", "apify-client": "^2.7.1", - "axios": "^0.27.2", + "axios": "1.6.2", "cheerio": "^1.0.0-rc.12", "chromadb": "^1.5.11", "cohere-ai": "^6.2.0", diff --git a/packages/server/package.json b/packages/server/package.json index f285e6639..38c203896 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -47,7 +47,7 @@ "dependencies": { "@oclif/core": "^1.13.10", "async-mutex": "^0.4.0", - "axios": "^0.27.2", + "axios": "1.6.2", "cors": "^2.8.5", "crypto-js": "^4.1.1", "dotenv": "^16.0.0", From d3877e76d4ec41c11637ff2ca84531e02ca4121b Mon Sep 17 00:00:00 2001 From: takuabonn Date: Tue, 5 Dec 2023 09:16:40 +0900 Subject: [PATCH 31/54] bugfix_console_warning --- packages/ui/src/ui-component/table/FlowListTable.js | 4 ++-- packages/ui/src/views/chatflows/index.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/ui-component/table/FlowListTable.js b/packages/ui/src/ui-component/table/FlowListTable.js index 358fc965a..e1ac3b6c8 100644 --- a/packages/ui/src/ui-component/table/FlowListTable.js +++ b/packages/ui/src/ui-component/table/FlowListTable.js @@ -147,8 +147,8 @@ export const FlowListTable = ({ data, images, filterFunction, updateFlowsApi }) } FlowListTable.propTypes = { - data: PropTypes.object, - images: PropTypes.array, + data: PropTypes.array, + images: PropTypes.object, 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 d91a9cfa7..0ace40336 100644 --- a/packages/ui/src/views/chatflows/index.js +++ b/packages/ui/src/views/chatflows/index.js @@ -161,7 +161,7 @@ const Chatflows = () => { variant='contained' value='card' title='Card View' - selectedColor='#00abc0' + selectedcolor='#00abc0' > From 264a571abf5eff51702079ff43b5936c8a96e609 Mon Sep 17 00:00:00 2001 From: Henry Date: Tue, 5 Dec 2023 03:01:13 +0000 Subject: [PATCH 32/54] update UI --- .../ui/src/assets/images/prompt_empty.svg | 1 + .../dialog/PromptLangsmithHubDialog.js | 590 +++++++++++------- .../ui/src/views/canvas/NodeInputHandler.js | 2 +- 3 files changed, 368 insertions(+), 225 deletions(-) create mode 100644 packages/ui/src/assets/images/prompt_empty.svg diff --git a/packages/ui/src/assets/images/prompt_empty.svg b/packages/ui/src/assets/images/prompt_empty.svg new file mode 100644 index 000000000..61df7e32e --- /dev/null +++ b/packages/ui/src/assets/images/prompt_empty.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/ui/src/ui-component/dialog/PromptLangsmithHubDialog.js b/packages/ui/src/ui-component/dialog/PromptLangsmithHubDialog.js index 35f1f7543..e1cfaaa98 100644 --- a/packages/ui/src/ui-component/dialog/PromptLangsmithHubDialog.js +++ b/packages/ui/src/ui-component/dialog/PromptLangsmithHubDialog.js @@ -1,6 +1,14 @@ import { createPortal } from 'react-dom' import { useState, useEffect } from 'react' +import { useDispatch, useSelector } from 'react-redux' import PropTypes from 'prop-types' + +import rehypeMathjax from 'rehype-mathjax' +import rehypeRaw from 'rehype-raw' +import remarkGfm from 'remark-gfm' +import remarkMath from 'remark-math' + +// MUI import { Box, Button, @@ -10,7 +18,7 @@ import { DialogActions, DialogContent, DialogTitle, - Divider, + Chip, Grid, InputLabel, List, @@ -18,23 +26,30 @@ import { ListItemText, OutlinedInput, Select, - Typography + Typography, + Stack, + IconButton, + FormControl, + Checkbox, + MenuItem } from '@mui/material' -import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from '../../store/actions' -import { useDispatch } from 'react-redux' -import FormControl from '@mui/material/FormControl' -import Checkbox from '@mui/material/Checkbox' -import MenuItem from '@mui/material/MenuItem' -import ReactMarkdown from 'react-markdown' -import CredentialInputHandler from '../../views/canvas/CredentialInputHandler' -import promptApi from '../../api/prompt' -import { StyledButton } from '../button/StyledButton' -import ExpandMoreIcon from '@mui/icons-material/ExpandMore' -import { styled } from '@mui/material/styles' -import ArrowForwardIosSharpIcon from '@mui/icons-material/ArrowForwardIosSharp' import MuiAccordion from '@mui/material/Accordion' import MuiAccordionSummary from '@mui/material/AccordionSummary' import MuiAccordionDetails from '@mui/material/AccordionDetails' +import ExpandMoreIcon from '@mui/icons-material/ExpandMore' +import ArrowForwardIosSharpIcon from '@mui/icons-material/ArrowForwardIosSharp' +import ClearIcon from '@mui/icons-material/Clear' +import { styled } from '@mui/material/styles' + +//Project Import +import CredentialInputHandler from 'views/canvas/CredentialInputHandler' +import { StyledButton } from 'ui-component/button/StyledButton' +import { MemoizedReactMarkdown } from 'ui-component/markdown/MemoizedReactMarkdown' +import { CodeBlock } from 'ui-component/markdown/CodeBlock' +import promptEmptySVG from 'assets/images/prompt_empty.svg' + +import promptApi from 'api/prompt' +import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions' const NewLineToBr = ({ children = '' }) => { return children.split('\n').reduce(function (arr, line) { @@ -73,6 +88,7 @@ const AccordionDetails = styled(MuiAccordionDetails)(({ theme }) => ({ const PromptLangsmithHubDialog = ({ promptType, show, onCancel, onSubmit }) => { const portalElement = document.getElementById('portal') const dispatch = useDispatch() + const customization = useSelector((state) => state.customization) useEffect(() => { if (show) dispatch({ type: SHOW_CANVAS_DIALOG }) @@ -141,19 +157,24 @@ const PromptLangsmithHubDialog = ({ promptType, show, onCancel, onSubmit }) => { const [selectedPrompt, setSelectedPrompt] = useState({}) const [credentialId, setCredentialId] = useState('') - const [accordionExpanded, setAccordionExpanded] = useState('panel2') + const [accordionExpanded, setAccordionExpanded] = useState(['prompt']) - const handleAccordionChange = (panel) => { - setAccordionExpanded(panel) + const handleAccordionChange = (accordionName) => (event, isExpanded) => { + const accordians = [...accordionExpanded] + if (!isExpanded) setAccordionExpanded(accordians.filter((accr) => accr !== accordionName)) + else { + accordians.push(accordionName) + setAccordionExpanded(accordians) + } } - const handleListItemClick = async (event, index) => { - const prompt = availablePrompNameList[index] + const handleListItemClick = async (index, overridePromptNameList = []) => { + const prompt = overridePromptNameList.length ? overridePromptNameList[index] : availablePrompNameList[index] if (!prompt.detailed) { const createResp = await promptApi.getPrompt({ credential: credentialId, - promptName: selectedPrompt.full_name + promptName: prompt.full_name }) if (createResp.data) { prompt.detailed = createResp.data.templates @@ -180,6 +201,7 @@ const PromptLangsmithHubDialog = ({ promptType, show, onCancel, onSubmit }) => { }) if (createResp.data) { setAvailablePrompNameList(createResp.data.repos) + if (createResp.data.repos?.length) handleListItemClick(0, createResp.data.repos) } } @@ -217,12 +239,21 @@ const PromptLangsmithHubDialog = ({ promptType, show, onCancel, onSubmit }) => { setLanguage(removeDuplicates(value)) } + const clear = () => { + setModelName([]) + setUsecase([]) + setLanguage([]) + setSelectedPrompt({}) + setAvailablePrompNameList([]) + setAccordionExpanded(['prompt']) + } + const component = show ? ( @@ -230,12 +261,12 @@ const PromptLangsmithHubDialog = ({ promptType, show, onCancel, onSubmit }) => { Load Prompts from Langsmith Hub ({promptType === 'template' ? 'PromptTemplate' : 'ChatPromptTemplate'}) - - - Langsmith Credential + + + Langsmith Credential   * - + { }} onSelect={(newValue) => { setCredentialId(newValue) + if (!newValue) clear() }} /> - - - - Model - - - - - - Usecase - - - - - - Language - - - - - - - - - - - - - - - Available Prompts - - - {availablePrompNameList.map((item, index) => ( - handleListItemClick(event, index)} - > - {item.full_name} - - ))} - - - + {credentialId && ( + + + + Model + + + + + + Usecase + + + + + + Language + + + + + + + + )} + {availablePrompNameList && availablePrompNameList.length == 0 && ( + + + promptEmptySVG - - - - - - handleAccordionChange('panel1')}> - } - id='panel1d-header' - > - Description - - - - {selectedPrompt?.description} - - - - handleAccordionChange('panel2')}> - } - id='panel2d-header' - > - Prompt - - - - {selectedPrompt?.detailed?.map((item) => ( - <> - - {item.typeDisplay.toUpperCase()} +
No Available Prompts
+ + )} + {availablePrompNameList && availablePrompNameList.length > 0 && ( + + + + + + + + + Available Prompts + + + {availablePrompNameList.map((item, index) => ( + handleListItemClick(index)} + > +
+ + {item.full_name} + +
+ {item.tags.map((tag, index) => ( + + ))} +
+
+
+ ))} +
+
+
+
+
+ + + + + + } + id='panel2d-header' + > + Prompt + + + + {selectedPrompt?.detailed?.map((item) => ( + <> + + {item.typeDisplay.toUpperCase()} + + +

+ {item.template} +

+
+ + ))}
- -

+ + + } + id='panel1d-header' + > + Description + + + + {selectedPrompt?.description} + + + + + } + aria-controls='panel3d-content' + id='panel3d-header' + > + Readme + + +

+ + ) : ( + + {children} + + ) + } }} > - {item.template} -

- - - ))} - - - - handleAccordionChange('panel3')}> - } - aria-controls='panel3d-content' - id='panel3d-header' - > - Readme - - - - {selectedPrompt?.readme} - - - - - + {selectedPrompt?.readme} +
+
+
+
+
+
+
+
+
-
-
+ + )}
- - - onSubmit(selectedPrompt.detailed)} variant='contained'> - Submit - - + {availablePrompNameList && availablePrompNameList.length > 0 && ( + + + onSubmit(selectedPrompt.detailed)} + variant='contained' + > + Load + + + )}
) : null diff --git a/packages/ui/src/views/canvas/NodeInputHandler.js b/packages/ui/src/views/canvas/NodeInputHandler.js index 24ba3c921..7920af6ab 100644 --- a/packages/ui/src/views/canvas/NodeInputHandler.js +++ b/packages/ui/src/views/canvas/NodeInputHandler.js @@ -231,7 +231,7 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA flexDirection: 'row', width: '100%' }} - sx={{ borderRadius: 25, width: '100%', mb: 2, mt: 2 }} + sx={{ borderRadius: 25, width: '100%', mb: 2, mt: 0 }} variant='outlined' onClick={() => onShowPromptHubButtonClicked()} endIcon={} From bfeebcd229477b95bf67b404a234e1d9416704fa Mon Sep 17 00:00:00 2001 From: Henry Date: Tue, 5 Dec 2023 19:02:33 +0000 Subject: [PATCH 33/54] update Zep version --- .../nodes/memory/ZepMemory/ZepMemory.ts | 104 ++++++++---------- packages/components/package.json | 2 +- .../chatflows/Long Term Memory.json | 20 +--- 3 files changed, 51 insertions(+), 75 deletions(-) diff --git a/packages/components/nodes/memory/ZepMemory/ZepMemory.ts b/packages/components/nodes/memory/ZepMemory/ZepMemory.ts index ced871a1e..e72a6704f 100644 --- a/packages/components/nodes/memory/ZepMemory/ZepMemory.ts +++ b/packages/components/nodes/memory/ZepMemory/ZepMemory.ts @@ -1,9 +1,8 @@ -import { SystemMessage } from 'langchain/schema' +import { ZepMemory, ZepMemoryInput } from 'langchain/memory/zep' +import { getBufferString, InputValues, MemoryVariables, OutputValues } from 'langchain/memory' import { INode, INodeData, INodeParams } from '../../../src/Interface' import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' -import { ZepMemory, ZepMemoryInput } from 'langchain/memory/zep' import { ICommonObject } from '../../../src' -import { getBufferString } from 'langchain/memory' class ZepMemory_Memory implements INode { label: string @@ -20,7 +19,7 @@ class ZepMemory_Memory implements INode { constructor() { this.label = 'Zep Memory' this.name = 'ZepMemory' - this.version = 1.0 + this.version = 2.0 this.type = 'ZepMemory' this.icon = 'zep.png' this.category = 'Memory' @@ -41,17 +40,12 @@ class ZepMemory_Memory implements INode { type: 'string', default: 'http://127.0.0.1:8000' }, - { - label: 'Auto Summary', - name: 'autoSummary', - type: 'boolean', - default: true - }, { label: 'Session Id', name: 'sessionId', type: 'string', - description: 'If not specified, the first CHAT_MESSAGE_ID will be used as sessionId', + description: + 'If not specified, a random id will be used. Learn more', default: '', additionalParams: true, optional: true @@ -60,15 +54,10 @@ class ZepMemory_Memory implements INode { label: 'Size', name: 'k', type: 'number', - default: '10', - description: 'Window of size k to surface the last k back-and-forth to use as memory.' - }, - { - label: 'Auto Summary Template', - name: 'autoSummaryTemplate', - type: 'string', - default: 'This is the summary of the following conversation:\n{summary}', - additionalParams: true + placeholder: '10', + description: 'Window of size k to surface the last k back-and-forth to use as memory.', + additionalParams: true, + optional: true }, { label: 'AI Prefix', @@ -109,36 +98,7 @@ class ZepMemory_Memory implements INode { } async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { - const autoSummaryTemplate = nodeData.inputs?.autoSummaryTemplate as string - const autoSummary = nodeData.inputs?.autoSummary as boolean - - const k = nodeData.inputs?.k as string - - let zep = await initalizeZep(nodeData, options) - - // hack to support summary - let tmpFunc = zep.loadMemoryVariables - zep.loadMemoryVariables = async (values) => { - let data = await tmpFunc.bind(zep, values)() - if (autoSummary && zep.returnMessages && data[zep.memoryKey] && data[zep.memoryKey].length) { - const zepClient = await zep.zepClientPromise - const memory = await zepClient.memory.getMemory(zep.sessionId, parseInt(k, 10) ?? 10) - if (memory?.summary) { - let summary = autoSummaryTemplate.replace(/{summary}/g, memory.summary.content) - // eslint-disable-next-line no-console - console.log('[ZepMemory] auto summary:', summary) - data[zep.memoryKey].unshift(new SystemMessage(summary)) - } - } - // for langchain zep memory compatibility, or we will get "Missing value for input variable chat_history" - if (data instanceof Array) { - data = { - [zep.memoryKey]: data - } - } - return data - } - return zep + return await initalizeZep(nodeData, options) } //@ts-ignore @@ -169,40 +129,72 @@ const initalizeZep = async (nodeData: INodeData, options: ICommonObject): Promis const humanPrefix = nodeData.inputs?.humanPrefix as string const memoryKey = nodeData.inputs?.memoryKey as string const inputKey = nodeData.inputs?.inputKey as string - const sessionId = nodeData.inputs?.sessionId as string + const k = nodeData.inputs?.k as string const chatId = options?.chatId as string let isSessionIdUsingChatMessageId = false - if (!sessionId && chatId) isSessionIdUsingChatMessageId = true + let sessionId = '' + + if (!nodeData.inputs?.sessionId && chatId) { + isSessionIdUsingChatMessageId = true + sessionId = chatId + } else { + sessionId = nodeData.inputs?.sessionId + } const credentialData = await getCredentialData(nodeData.credential ?? '', options) const apiKey = getCredentialParam('apiKey', credentialData, nodeData) - const obj: ZepMemoryInput & Partial = { + const obj: ZepMemoryInput & ZepMemoryExtendedInput = { baseURL, sessionId: sessionId ? sessionId : chatId, aiPrefix, humanPrefix, returnMessages: true, memoryKey, - inputKey + inputKey, + isSessionIdUsingChatMessageId, + k: k ? parseInt(k, 10) : undefined } if (apiKey) obj.apiKey = apiKey - if (isSessionIdUsingChatMessageId) obj.isSessionIdUsingChatMessageId = true return new ZepMemoryExtended(obj) } interface ZepMemoryExtendedInput { isSessionIdUsingChatMessageId: boolean + k?: number } class ZepMemoryExtended extends ZepMemory { isSessionIdUsingChatMessageId? = false + lastN?: number - constructor(fields: ZepMemoryInput & Partial) { + constructor(fields: ZepMemoryInput & ZepMemoryExtendedInput) { super(fields) this.isSessionIdUsingChatMessageId = fields.isSessionIdUsingChatMessageId + this.lastN = fields.k + } + + async loadMemoryVariables(values: InputValues, overrideSessionId = ''): Promise { + if (overrideSessionId) { + super.sessionId = overrideSessionId + } + return super.loadMemoryVariables({ ...values, lastN: this.lastN }) + } + + async saveContext(inputValues: InputValues, outputValues: OutputValues, overrideSessionId = ''): Promise { + if (overrideSessionId) { + super.sessionId = overrideSessionId + } + return super.saveContext(inputValues, outputValues) + } + + async clear(overrideSessionId = ''): Promise { + if (overrideSessionId) { + super.sessionId = overrideSessionId + } + return super.clear() } } diff --git a/packages/components/package.json b/packages/components/package.json index bea9a7a06..dd87754d5 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -21,7 +21,7 @@ "@aws-sdk/client-s3": "^3.427.0", "@dqbd/tiktoken": "^1.0.7", "@elastic/elasticsearch": "^8.9.0", - "@getzep/zep-js": "^0.6.3", + "@getzep/zep-js": "^0.9.0", "@gomomento/sdk": "^1.51.1", "@gomomento/sdk-core": "^1.51.1", "@google-ai/generativelanguage": "^0.2.1", diff --git a/packages/server/marketplaces/chatflows/Long Term Memory.json b/packages/server/marketplaces/chatflows/Long Term Memory.json index c508b4807..c39f746a2 100644 --- a/packages/server/marketplaces/chatflows/Long Term Memory.json +++ b/packages/server/marketplaces/chatflows/Long Term Memory.json @@ -205,7 +205,7 @@ "data": { "id": "ZepMemory_0", "label": "Zep Memory", - "version": 1, + "version": 2, "name": "ZepMemory", "type": "ZepMemory", "baseClasses": ["ZepMemory", "BaseChatMemory", "BaseMemory"], @@ -228,13 +228,6 @@ "default": "http://127.0.0.1:8000", "id": "ZepMemory_0-input-baseURL-string" }, - { - "label": "Auto Summary", - "name": "autoSummary", - "type": "boolean", - "default": true, - "id": "ZepMemory_0-input-autoSummary-boolean" - }, { "label": "Session Id", "name": "sessionId", @@ -251,17 +244,10 @@ "type": "number", "default": "10", "step": 1, + "additionalParams": true, "description": "Window of size k to surface the last k back-and-forths to use as memory.", "id": "ZepMemory_0-input-k-number" }, - { - "label": "Auto Summary Template", - "name": "autoSummaryTemplate", - "type": "string", - "default": "This is the summary of the following conversation:\n{summary}", - "additionalParams": true, - "id": "ZepMemory_0-input-autoSummaryTemplate-string" - }, { "label": "AI Prefix", "name": "aiPrefix", @@ -306,10 +292,8 @@ "inputAnchors": [], "inputs": { "baseURL": "http://127.0.0.1:8000", - "autoSummary": true, "sessionId": "", "k": "10", - "autoSummaryTemplate": "This is the summary of the following conversation:\n{summary}", "aiPrefix": "ai", "humanPrefix": "human", "memoryKey": "chat_history", From 59308665c2b8c83b2b77ecd2a0753a86847ea037 Mon Sep 17 00:00:00 2001 From: takuabonn Date: Wed, 6 Dec 2023 07:32:45 +0900 Subject: [PATCH 34/54] remove_props --- packages/ui/src/views/chatflows/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/ui/src/views/chatflows/index.js b/packages/ui/src/views/chatflows/index.js index 0ace40336..c87ad306c 100644 --- a/packages/ui/src/views/chatflows/index.js +++ b/packages/ui/src/views/chatflows/index.js @@ -161,7 +161,6 @@ const Chatflows = () => { variant='contained' value='card' title='Card View' - selectedcolor='#00abc0' > From d397adb47a1c363e44b1ddcd3b2965ad8466cf09 Mon Sep 17 00:00:00 2001 From: Henry Date: Wed, 6 Dec 2023 00:30:51 +0000 Subject: [PATCH 35/54] avoid button showing up for other systemprompt like conversation chain --- .../ui/src/views/canvas/NodeInputHandler.js | 47 ++++++++++--------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/packages/ui/src/views/canvas/NodeInputHandler.js b/packages/ui/src/views/canvas/NodeInputHandler.js index 7920af6ab..fc2e7ac83 100644 --- a/packages/ui/src/views/canvas/NodeInputHandler.js +++ b/packages/ui/src/views/canvas/NodeInputHandler.js @@ -223,29 +223,30 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA )} - {(inputParam.name === 'template' || inputParam.name === 'systemMessagePrompt') && ( - <> - - setShowPromptHubDialog(false)} - onSubmit={onShowPromptHubButtonSubmit} - > - - )} + {(data.name === 'promptTemplate' || data.name === 'chatPromptTemplate') && + (inputParam.name === 'template' || inputParam.name === 'systemMessagePrompt') && ( + <> + + setShowPromptHubDialog(false)} + onSubmit={onShowPromptHubButtonSubmit} + > + + )}
{inputParam.label} From 2da6f598340593556bc40860babbf474a52fae62 Mon Sep 17 00:00:00 2001 From: Henry Date: Wed, 6 Dec 2023 01:51:14 +0000 Subject: [PATCH 36/54] fix ttl parseInt error --- packages/components/nodes/cache/RedisCache/RedisCache.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/nodes/cache/RedisCache/RedisCache.ts b/packages/components/nodes/cache/RedisCache/RedisCache.ts index 3b68cf127..8128b6e32 100644 --- a/packages/components/nodes/cache/RedisCache/RedisCache.ts +++ b/packages/components/nodes/cache/RedisCache/RedisCache.ts @@ -89,7 +89,7 @@ class RedisCache implements INode { redisClient.update = async (prompt: string, llmKey: string, value: Generation[]) => { for (let i = 0; i < value.length; i += 1) { const key = getCacheKey(prompt, llmKey, String(i)) - if (ttl !== undefined) { + if (ttl) { await client.set(key, JSON.stringify(serializeGeneration(value[i])), 'EX', parseInt(ttl, 10)) } else { await client.set(key, JSON.stringify(serializeGeneration(value[i]))) From 8122377bbb794564d89fa79096fcff6a209a2f9e Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Wed, 6 Dec 2023 12:53:43 +0530 Subject: [PATCH 37/54] LS Prompt Hub: Minor fixes --- packages/server/src/index.ts | 2 +- .../ui/src/ui-component/dialog/PromptLangsmithHubDialog.js | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 42fb326d8..9df016d09 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1077,7 +1077,7 @@ export class App { headers['x-api-key'] = decryptedCredentialData.langsmithApiKey const tags = req.body.tags ? `tags=${req.body.tags}` : '' - const url = `https://web.hub.langchain.com/repos/?${tags}offset=0&limit=20&has_commits=true&sort_field=num_likes&sort_direction=desc&is_archived=false` + const url = `https://web.hub.langchain.com/repos/?${tags}has_commits=true&sort_field=num_likes&sort_direction=desc&is_archived=false` axios.get(url, headers).then((response) => { if (response.data.repos) { return res.json({ status: 'OK', repos: response.data.repos }) diff --git a/packages/ui/src/ui-component/dialog/PromptLangsmithHubDialog.js b/packages/ui/src/ui-component/dialog/PromptLangsmithHubDialog.js index e1cfaaa98..e6f06e209 100644 --- a/packages/ui/src/ui-component/dialog/PromptLangsmithHubDialog.js +++ b/packages/ui/src/ui-component/dialog/PromptLangsmithHubDialog.js @@ -181,7 +181,6 @@ const PromptLangsmithHubDialog = ({ promptType, show, onCancel, onSubmit }) => { } } setSelectedPrompt(prompt) - await new Promise((resolve) => setTimeout(resolve, 500)) } const fetchPrompts = async () => { @@ -201,7 +200,7 @@ const PromptLangsmithHubDialog = ({ promptType, show, onCancel, onSubmit }) => { }) if (createResp.data) { setAvailablePrompNameList(createResp.data.repos) - if (createResp.data.repos?.length) handleListItemClick(0, createResp.data.repos) + if (createResp.data.repos?.length) await handleListItemClick(0, createResp.data.repos) } } From cc1a3101e26d22642ac4d74d63528f474f0bd43f Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Wed, 6 Dec 2023 15:01:30 +0530 Subject: [PATCH 38/54] S3 File Loader: Region missing fix --- packages/components/nodes/documentloaders/S3File/S3File.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/components/nodes/documentloaders/S3File/S3File.ts b/packages/components/nodes/documentloaders/S3File/S3File.ts index 07295abaa..58ffd8af7 100644 --- a/packages/components/nodes/documentloaders/S3File/S3File.ts +++ b/packages/components/nodes/documentloaders/S3File/S3File.ts @@ -162,8 +162,11 @@ class S3_DocumentLoaders implements INode { accessKeyId?: string secretAccessKey?: string } = { - accessKeyId, - secretAccessKey + region, + credentials: { + accessKeyId, + secretAccessKey + } } loader.load = async () => { From 275540d1830575c56679666e759ba35f74699f91 Mon Sep 17 00:00:00 2001 From: Henry Date: Wed, 6 Dec 2023 17:39:18 +0000 Subject: [PATCH 39/54] add default limit to 100 --- packages/server/src/index.ts | 3 ++- packages/ui/src/views/canvas/NodeInputHandler.js | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 9df016d09..3d8208f91 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1077,7 +1077,8 @@ export class App { headers['x-api-key'] = decryptedCredentialData.langsmithApiKey const tags = req.body.tags ? `tags=${req.body.tags}` : '' - const url = `https://web.hub.langchain.com/repos/?${tags}has_commits=true&sort_field=num_likes&sort_direction=desc&is_archived=false` + // Default to 100, TODO: add pagination and use offset & limit + const url = `https://web.hub.langchain.com/repos/?limit=100&${tags}has_commits=true&sort_field=num_likes&sort_direction=desc&is_archived=false` axios.get(url, headers).then((response) => { if (response.data.repos) { return res.json({ status: 'OK', repos: response.data.repos }) diff --git a/packages/ui/src/views/canvas/NodeInputHandler.js b/packages/ui/src/views/canvas/NodeInputHandler.js index fc2e7ac83..103af6b43 100644 --- a/packages/ui/src/views/canvas/NodeInputHandler.js +++ b/packages/ui/src/views/canvas/NodeInputHandler.js @@ -232,6 +232,7 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA flexDirection: 'row', width: '100%' }} + disabled={disabled} sx={{ borderRadius: 25, width: '100%', mb: 2, mt: 0 }} variant='outlined' onClick={() => onShowPromptHubButtonClicked()} From e67c43157a53cc208776431c1fad829f5170d9fd Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Thu, 7 Dec 2023 16:06:32 +0530 Subject: [PATCH 40/54] XSS: Simplified by adding XSS middleware --- packages/server/package.json | 2 +- packages/server/src/index.ts | 421 ++++++++++--------------------- packages/server/src/utils/XSS.ts | 11 + 3 files changed, 142 insertions(+), 292 deletions(-) create mode 100644 packages/server/src/utils/XSS.ts diff --git a/packages/server/package.json b/packages/server/package.json index 38c203896..97a95d43c 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -54,7 +54,6 @@ "express": "^4.17.3", "express-basic-auth": "^1.2.1", "express-rate-limit": "^6.9.0", - "express-validator": "^7.0.1", "flowise-components": "*", "flowise-ui": "*", "moment-timezone": "^0.5.34", @@ -64,6 +63,7 @@ "reflect-metadata": "^0.1.13", "socket.io": "^4.6.1", "sqlite3": "^5.1.6", + "strip-js": "^1.2.0", "typeorm": "^0.3.6", "uuid": "^9.0.1", "winston": "^3.9.0" diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 61f34e923..d40b42bfa 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -58,7 +58,7 @@ import { CachePool } from './CachePool' import { ICommonObject, IMessage, INodeOptionsValue } from 'flowise-components' import { createRateLimiter, getRateLimiter, initializeRateLimiter } from './utils/rateLimit' import { addAPIKey, compareKeys, deleteAPIKey, getApiKey, getAPIKeys, updateAPIKey } from './utils/apiKey' -import { body, param, query, validationResult } from 'express-validator' +import { sanitizeMiddleware } from './utils/XSS' export class App { app: express.Application @@ -122,6 +122,9 @@ export class App { // Add the expressRequestLogger middleware to log all requests this.app.use(expressRequestLogger) + // Add the sanitizeMiddleware to guard against XSS + this.app.use(sanitizeMiddleware) + if (process.env.FLOWISE_USERNAME && process.env.FLOWISE_PASSWORD) { const username = process.env.FLOWISE_USERNAME const password = process.env.FLOWISE_PASSWORD @@ -184,27 +187,17 @@ export class App { }) // Get specific component node via name - this.app.get('/api/v1/nodes/:name', param('name').notEmpty().escape(), (req: Request, res: Response) => { - const name = req.params.name - const result = validationResult(req) - if (!result.isEmpty()) { - throw new Error(`Node ${name} not found`) - } - if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentNodes, name)) { + this.app.get('/api/v1/nodes/:name', (req: Request, res: Response) => { + if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentNodes, req.params.name)) { return res.json(this.nodesPool.componentNodes[req.params.name]) } else { - throw new Error(`Node ${name} not found`) + throw new Error(`Node ${req.params.name} not found`) } }) // Get component credential via name - this.app.get('/api/v1/components-credentials/:name', param('name').notEmpty().escape(), (req: Request, res: Response) => { - const name = req.params.name - const result = validationResult(req) - if (!result.isEmpty()) { - throw new Error(`Credential ${name} not found`) - } - if (!req.params.name.includes('&')) { + this.app.get('/api/v1/components-credentials/:name', (req: Request, res: Response) => { + if (!req.params.name.includes('&')) { if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentCredentials, req.params.name)) { return res.json(this.nodesPool.componentCredentials[req.params.name]) } else { @@ -212,7 +205,7 @@ export class App { } } else { const returnResponse = [] - for (const name of req.params.name.split('&')) { + for (const name of req.params.name.split('&')) { if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentCredentials, name)) { returnResponse.push(this.nodesPool.componentCredentials[name]) } else { @@ -224,14 +217,9 @@ export class App { }) // Returns specific component node icon via name - this.app.get('/api/v1/node-icon/:name', param('name').notEmpty().escape(), (req: Request, res: Response) => { - const name = req.params.name - const result = validationResult(req) - if (!result.isEmpty()) { - throw new Error(`Node ${name} not found`) - } - if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentNodes, name)) { - const nodeInstance = this.nodesPool.componentNodes[name] + this.app.get('/api/v1/node-icon/:name', (req: Request, res: Response) => { + if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentNodes, req.params.name)) { + const nodeInstance = this.nodesPool.componentNodes[req.params.name] if (nodeInstance.icon === undefined) { throw new Error(`Node ${req.params.name} icon not found`) } @@ -240,48 +228,38 @@ export class App { const filepath = nodeInstance.icon res.sendFile(filepath) } else { - throw new Error(`Node ${name} icon is missing icon`) + throw new Error(`Node ${req.params.name} icon is missing icon`) } } else { - throw new Error(`Node ${name} not found`) + throw new Error(`Node ${req.params.name} not found`) } }) // Returns specific component credential icon via name - this.app.get('/api/v1/components-credentials-icon/:name', param('name').notEmpty().escape(), (req: Request, res: Response) => { - const name = req.params.name - const result = validationResult(req) - if (!result.isEmpty()) { - throw new Error(`Credential ${name} not found`) - } - if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentCredentials, name)) { - const credInstance = this.nodesPool.componentCredentials[name] + this.app.get('/api/v1/components-credentials-icon/:name', (req: Request, res: Response) => { + if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentCredentials, req.params.name)) { + const credInstance = this.nodesPool.componentCredentials[req.params.name] if (credInstance.icon === undefined) { - throw new Error(`Credential ${name} icon not found`) + throw new Error(`Credential ${req.params.name} icon not found`) } if (credInstance.icon.endsWith('.svg') || credInstance.icon.endsWith('.png') || credInstance.icon.endsWith('.jpg')) { const filepath = credInstance.icon res.sendFile(filepath) } else { - throw new Error(`Credential ${name} is missing icon`) + throw new Error(`Credential ${req.params.name} icon is missing icon`) } } else { - throw new Error(`Credential ${name} not found`) + throw new Error(`Credential ${req.params.name} not found`) } }) // load async options - this.app.post('/api/v1/node-load-method/:name', param('name').notEmpty().escape(), async (req: Request, res: Response) => { - const name = req.params.name - const result = validationResult(req) - if (!result.isEmpty()) { - throw new Error(`Node ${name} not found`) - } + this.app.post('/api/v1/node-load-method/:name', async (req: Request, res: Response) => { const nodeData: INodeData = req.body - if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentNodes, name)) { + if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentNodes, req.params.name)) { try { - const nodeInstance = this.nodesPool.componentNodes[name] + const nodeInstance = this.nodesPool.componentNodes[req.params.name] const methodName = nodeData.loadMethod || '' const returnOptions: INodeOptionsValue[] = await nodeInstance.loadMethods![methodName]!.call(nodeInstance, nodeData, { @@ -294,7 +272,7 @@ export class App { return res.json([]) } } else { - res.status(404).send(`Node ${name} not found`) + res.status(404).send(`Node ${req.params.name} not found`) return } }) @@ -310,11 +288,7 @@ export class App { }) // Get specific chatflow via api key - this.app.get('/api/v1/chatflows/apikey/:apiKey', param('apiKey').notEmpty().escape(), async (req: Request, res: Response) => { - const result = validationResult(req) - if (!result.isEmpty()) { - return res.status(401).send('Unauthorized') - } + this.app.get('/api/v1/chatflows/apikey/:apiKey', async (req: Request, res: Response) => { try { const apiKey = await getApiKey(req.params.apiKey) if (!apiKey) return res.status(401).send('Unauthorized') @@ -326,19 +300,14 @@ export class App { .orderBy('cf.name', 'ASC') .getMany() if (chatflows.length >= 1) return res.status(200).send(chatflows) - return res.status(404).send('APIKey not found') + return res.status(404).send('Chatflow not found') } catch (err: any) { return res.status(500).send(err?.message) } }) // Get specific chatflow via id - this.app.get('/api/v1/chatflows/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const chatflowId = req.params.id - const result = validationResult(req) - if (!result.isEmpty()) { - return res.status(404).send(`Chatflow ${chatflowId} not found`) - } + this.app.get('/api/v1/chatflows/:id', async (req: Request, res: Response) => { const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ id: req.params.id }) @@ -347,12 +316,7 @@ export class App { }) // Get specific chatflow via id (PUBLIC endpoint, used when sharing chatbot link) - this.app.get('/api/v1/public-chatflows/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const chatflowId = req.params.id - const result = validationResult(req) - if (!result.isEmpty()) { - return res.status(404).send(`Chatflow ${chatflowId} not found`) - } + this.app.get('/api/v1/public-chatflows/:id', async (req: Request, res: Response) => { const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ id: req.params.id }) @@ -374,69 +338,48 @@ export class App { }) // Update chatflow - this.app.put( - '/api/v1/chatflows/:id', - body('chatflow.id').notEmpty(), - param('id').notEmpty().escape(), - async (req: Request, res: Response) => { - const chatflowId = req.params.id - const valResult = validationResult(req) - if (!valResult.isEmpty()) { - return res.status(404).send(`Chatflow ${chatflowId} not found`) - } - const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ - id: chatflowId - }) + this.app.put('/api/v1/chatflows/:id', async (req: Request, res: Response) => { + const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ + id: req.params.id + }) - if (!chatflow) { - res.status(404).send(`Chatflow ${chatflowId} not found`) - return - } - - const body = req.body - const updateChatFlow = new ChatFlow() - Object.assign(updateChatFlow, body) - - updateChatFlow.id = chatflow.id - createRateLimiter(updateChatFlow) - - this.AppDataSource.getRepository(ChatFlow).merge(chatflow, updateChatFlow) - const result = await this.AppDataSource.getRepository(ChatFlow).save(chatflow) - - // 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) + if (!chatflow) { + res.status(404).send(`Chatflow ${req.params.id} not found`) + return } - ) + + const body = req.body + const updateChatFlow = new ChatFlow() + Object.assign(updateChatFlow, body) + + updateChatFlow.id = chatflow.id + createRateLimiter(updateChatFlow) + + this.AppDataSource.getRepository(ChatFlow).merge(chatflow, updateChatFlow) + const result = await this.AppDataSource.getRepository(ChatFlow).save(chatflow) + + // 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) + }) // Delete chatflow via id - this.app.delete('/api/v1/chatflows/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const chatflowId = req.params.id - const result = validationResult(req) - if (!result.isEmpty()) { - return res.status(404).send(`Chatflow ${chatflowId} not found`) - } + this.app.delete('/api/v1/chatflows/:id', async (req: Request, res: Response) => { const results = await this.AppDataSource.getRepository(ChatFlow).delete({ id: req.params.id }) return res.json(results) }) // Check if chatflow valid for streaming - this.app.get('/api/v1/chatflows-streaming/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const chatflowId = req.params.id - const result = validationResult(req) - if (!result.isEmpty()) { - return res.status(404).send(`Chatflow ${chatflowId} not found`) - } - + this.app.get('/api/v1/chatflows-streaming/:id', async (req: Request, res: Response) => { const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ - id: chatflowId + id: req.params.id }) - if (!chatflow) return res.status(404).send(`Chatflow ${chatflowId} not found`) + if (!chatflow) return res.status(404).send(`Chatflow ${req.params.id} not found`) /*** Get Ending Node with Directed Graph ***/ const flowData = chatflow.flowData @@ -466,84 +409,58 @@ export class App { // ---------------------------------------- // Get all chatmessages from chatflowid - this.app.get( - '/api/v1/chatmessage/:id', - query('chatId').notEmpty().escape(), - query('sortOrder').notEmpty().escape(), - query('memoryType').notEmpty().escape(), - query('sessionId').notEmpty().escape(), - query('startDate').notEmpty().escape(), - query('endDate').notEmpty().escape(), - query('chatTypeFilter').notEmpty().escape(), - async (req: Request, res: Response) => { - const result = validationResult(req) - if (!result.isEmpty()) { - return res.status(404).send(`Chatmessage not found`) - } - const sortOrder = req.query?.order as string | undefined - const chatId = req.query?.chatId as string | undefined - const memoryType = req.query?.memoryType as string | undefined - const sessionId = req.query?.sessionId as string | undefined - const startDate = req.query?.startDate as string | undefined - const endDate = req.query?.endDate as string | undefined - let chatTypeFilter = req.query?.chatType as chatType | undefined + this.app.get('/api/v1/chatmessage/:id', async (req: Request, res: Response) => { + const sortOrder = req.query?.order as string | undefined + const chatId = req.query?.chatId as string | undefined + const memoryType = req.query?.memoryType as string | undefined + const sessionId = req.query?.sessionId as string | undefined + const startDate = req.query?.startDate as string | undefined + const endDate = req.query?.endDate as string | undefined + let chatTypeFilter = req.query?.chatType as chatType | undefined - if (chatTypeFilter) { - try { - const chatTypeFilterArray = JSON.parse(chatTypeFilter) - if (chatTypeFilterArray.includes(chatType.EXTERNAL) && chatTypeFilterArray.includes(chatType.INTERNAL)) { - chatTypeFilter = undefined - } else if (chatTypeFilterArray.includes(chatType.EXTERNAL)) { - chatTypeFilter = chatType.EXTERNAL - } else if (chatTypeFilterArray.includes(chatType.INTERNAL)) { - chatTypeFilter = chatType.INTERNAL - } - } catch (e) { - return res.status(500).send(e) + if (chatTypeFilter) { + try { + const chatTypeFilterArray = JSON.parse(chatTypeFilter) + if (chatTypeFilterArray.includes(chatType.EXTERNAL) && chatTypeFilterArray.includes(chatType.INTERNAL)) { + chatTypeFilter = undefined + } else if (chatTypeFilterArray.includes(chatType.EXTERNAL)) { + chatTypeFilter = chatType.EXTERNAL + } else if (chatTypeFilterArray.includes(chatType.INTERNAL)) { + chatTypeFilter = chatType.INTERNAL } + } catch (e) { + return res.status(500).send(e) } - - const chatmessages = await this.getChatMessage( - req.params.id, - chatTypeFilter, - sortOrder, - chatId, - memoryType, - sessionId, - startDate, - endDate - ) - return res.json(chatmessages) } - ) + + const chatmessages = await this.getChatMessage( + req.params.id, + chatTypeFilter, + sortOrder, + chatId, + memoryType, + sessionId, + startDate, + endDate + ) + return res.json(chatmessages) + }) // Get internal chatmessages from chatflowid - this.app.get('/api/v1/internal-chatmessage/:id', param('chatId').notEmpty().escape(), async (req: Request, res: Response) => { - const result = validationResult(req) - if (!result.isEmpty()) { - return res.status(404).send(`Chatmessage not found`) - } + this.app.get('/api/v1/internal-chatmessage/:id', async (req: Request, res: Response) => { const chatmessages = await this.getChatMessage(req.params.id, chatType.INTERNAL) return res.json(chatmessages) }) // Add chatmessages for chatflowid - this.app.post('/api/v1/chatmessage/:id', param('chatId').notEmpty().escape(), async (req: Request, res: Response) => { - const result = validationResult(req) - if (!result.isEmpty()) { - return res.status(404).send(`Chatmessage not found`) - } + this.app.post('/api/v1/chatmessage/:id', async (req: Request, res: Response) => { const body = req.body const results = await this.addChatMessage(body) return res.json(results) }) // Delete all chatmessages from chatId - this.app.delete('/api/v1/chatmessage/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const result = validationResult(req) - if (!result.isEmpty()) { - return res.status(404).send(`Chatmessage not found`) - } + this.app.delete('/api/v1/chatmessage/:id', async (req: Request, res: Response) => { const chatflowid = req.params.id const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ id: chatflowid @@ -627,11 +544,7 @@ export class App { }) // Get specific credential - this.app.get('/api/v1/credentials/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const result = validationResult(req) - if (!result.isEmpty()) { - return res.status(404).send(`Credential ${req.params.id} not found`) - } + this.app.get('/api/v1/credentials/:id', async (req: Request, res: Response) => { const credential = await this.AppDataSource.getRepository(Credential).findOneBy({ id: req.params.id }) @@ -652,11 +565,7 @@ export class App { }) // Update credential - this.app.put('/api/v1/credentials/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const valResult = validationResult(req) - if (!valResult.isEmpty()) { - return res.status(404).send(`Credential ${req.params.id} not found`) - } + this.app.put('/api/v1/credentials/:id', async (req: Request, res: Response) => { const credential = await this.AppDataSource.getRepository(Credential).findOneBy({ id: req.params.id }) @@ -672,11 +581,7 @@ export class App { }) // Delete all chatmessages from chatflowid - this.app.delete('/api/v1/credentials/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const valResult = validationResult(req) - if (!valResult.isEmpty()) { - return res.status(404).send(`Credential ${req.params.id} not found`) - } + this.app.delete('/api/v1/credentials/:id', async (req: Request, res: Response) => { const results = await this.AppDataSource.getRepository(Credential).delete({ id: req.params.id }) return res.json(results) }) @@ -692,11 +597,7 @@ export class App { }) // Get specific tool - this.app.get('/api/v1/tools/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const valResult = validationResult(req) - if (!valResult.isEmpty()) { - return res.status(404).send(`Tool ${req.params.id} not found`) - } + this.app.get('/api/v1/tools/:id', async (req: Request, res: Response) => { const tool = await this.AppDataSource.getRepository(Tool).findOneBy({ id: req.params.id }) @@ -716,11 +617,7 @@ export class App { }) // Update tool - this.app.put('/api/v1/tools/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const valResult = validationResult(req) - if (!valResult.isEmpty()) { - return res.status(404).send(`Tool ${req.params.id} not found`) - } + this.app.put('/api/v1/tools/:id', async (req: Request, res: Response) => { const tool = await this.AppDataSource.getRepository(Tool).findOneBy({ id: req.params.id }) @@ -741,11 +638,7 @@ export class App { }) // Delete tool - this.app.delete('/api/v1/tools/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const valResult = validationResult(req) - if (!valResult.isEmpty()) { - return res.status(404).send(`Tool ${req.params.id} not found`) - } + this.app.delete('/api/v1/tools/:id', async (req: Request, res: Response) => { const results = await this.AppDataSource.getRepository(Tool).delete({ id: req.params.id }) return res.json(results) }) @@ -761,11 +654,7 @@ export class App { }) // Get specific assistant - this.app.get('/api/v1/assistants/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const valResult = validationResult(req) - if (!valResult.isEmpty()) { - return res.status(404).send(`Assistant ${req.params.id} not found`) - } + this.app.get('/api/v1/assistants/:id', async (req: Request, res: Response) => { const assistant = await this.AppDataSource.getRepository(Assistant).findOneBy({ id: req.params.id }) @@ -773,46 +662,33 @@ export class App { }) // Get assistant object - this.app.get( - '/api/v1/openai-assistants/:id', - param('id').notEmpty().escape(), - query('credential').notEmpty().escape(), - async (req: Request, res: Response) => { - const valResult = validationResult(req) - if (!valResult.isEmpty()) { - return res.status(404).send(`Assistant or Credential not found`) - } - const credentialId = req.query.credential as string - const credential = await this.AppDataSource.getRepository(Credential).findOneBy({ - id: credentialId - }) + this.app.get('/api/v1/openai-assistants/:id', async (req: Request, res: Response) => { + const credentialId = req.query.credential as string + const credential = await this.AppDataSource.getRepository(Credential).findOneBy({ + id: credentialId + }) - if (!credential) return res.status(404).send(`Credential ${credentialId} not found`) + if (!credential) return res.status(404).send(`Credential ${credentialId} not found`) - // Decrpyt credentialData - const decryptedCredentialData = await decryptCredentialData(credential.encryptedData) - const openAIApiKey = decryptedCredentialData['openAIApiKey'] - if (!openAIApiKey) return res.status(404).send(`OpenAI ApiKey not found`) + // Decrpyt credentialData + const decryptedCredentialData = await decryptCredentialData(credential.encryptedData) + const openAIApiKey = decryptedCredentialData['openAIApiKey'] + if (!openAIApiKey) return res.status(404).send(`OpenAI ApiKey not found`) - const openai = new OpenAI({ apiKey: openAIApiKey }) - const retrievedAssistant = await openai.beta.assistants.retrieve(req.params.id) - const resp = await openai.files.list() - const existingFiles = resp.data ?? [] + const openai = new OpenAI({ apiKey: openAIApiKey }) + const retrievedAssistant = await openai.beta.assistants.retrieve(req.params.id) + const resp = await openai.files.list() + const existingFiles = resp.data ?? [] - if (retrievedAssistant.file_ids && retrievedAssistant.file_ids.length) { - ;(retrievedAssistant as any).files = existingFiles.filter((file) => retrievedAssistant.file_ids.includes(file.id)) - } - - return res.json(retrievedAssistant) + if (retrievedAssistant.file_ids && retrievedAssistant.file_ids.length) { + ;(retrievedAssistant as any).files = existingFiles.filter((file) => retrievedAssistant.file_ids.includes(file.id)) } - ) + + return res.json(retrievedAssistant) + }) // List available assistants - this.app.get('/api/v1/openai-assistants', query('credential').notEmpty().escape(), async (req: Request, res: Response) => { - const valResult = validationResult(req) - if (!valResult.isEmpty()) { - return res.status(404).send(`Assistant or Credential not found`) - } + this.app.get('/api/v1/openai-assistants', async (req: Request, res: Response) => { const credentialId = req.query.credential as string const credential = await this.AppDataSource.getRepository(Credential).findOneBy({ id: credentialId @@ -947,11 +823,7 @@ export class App { }) // Update assistant - this.app.put('/api/v1/assistants/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const valResult = validationResult(req) - if (!valResult.isEmpty()) { - return res.status(404).send(`Assistant ${req.params.id} not found`) - } + this.app.put('/api/v1/assistants/:id', async (req: Request, res: Response) => { const assistant = await this.AppDataSource.getRepository(Assistant).findOneBy({ id: req.params.id }) @@ -1059,11 +931,7 @@ export class App { }) // Delete assistant - this.app.delete('/api/v1/assistants/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const valResult = validationResult(req) - if (!valResult.isEmpty()) { - return res.status(404).send(`Assistant ${req.params.id} not found`) - } + this.app.delete('/api/v1/assistants/:id', async (req: Request, res: Response) => { const assistant = await this.AppDataSource.getRepository(Assistant).findOneBy({ id: req.params.id }) @@ -1118,11 +986,7 @@ export class App { // Configuration // ---------------------------------------- - this.app.get('/api/v1/flow-config/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const valResult = validationResult(req) - if (!valResult.isEmpty()) { - return res.status(404).send(`Chatflow ${req.params.id} not found`) - } + this.app.get('/api/v1/flow-config/:id', async (req: Request, res: Response) => { const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ id: req.params.id }) @@ -1181,11 +1045,7 @@ export class App { } ) - this.app.post('/api/v1/vector/internal-upsert/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const valResult = validationResult(req) - if (!valResult.isEmpty()) { - return res.status(404).send(`Upsert ${req.params.id} not found`) - } + this.app.post('/api/v1/vector/internal-upsert/:id', async (req: Request, res: Response) => { await this.buildChatflow(req, res, undefined, true, true) }) @@ -1196,24 +1056,15 @@ export class App { // Send input message and get prediction result (External) this.app.post( '/api/v1/prediction/:id', - param('id').notEmpty().escape(), upload.array('files'), (req: Request, res: Response, next: NextFunction) => getRateLimiter(req, res, next), async (req: Request, res: Response) => { - const valResult = validationResult(req) - if (!valResult.isEmpty()) { - return res.status(404).send(`Error Processing Prediction`) - } await this.buildChatflow(req, res, socketIO) } ) // Send input message and get prediction result (Internal) - this.app.post('/api/v1/internal-prediction/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const valResult = validationResult(req) - if (!valResult.isEmpty()) { - return res.status(404).send(`Error Processing Prediction`) - } + this.app.post('/api/v1/internal-prediction/:id', async (req: Request, res: Response) => { await this.buildChatflow(req, res, socketIO, true) }) @@ -1308,31 +1159,19 @@ export class App { }) // Update api key - this.app.put('/api/v1/apikey/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const valResult = validationResult(req) - if (!valResult.isEmpty()) { - return res.status(404).send(`Error Processing 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 addChatflowsCount(keys, res) }) // Delete new api key - this.app.delete('/api/v1/apikey/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const valResult = validationResult(req) - if (!valResult.isEmpty()) { - return res.status(404).send(`Error Processing Update API Key`) - } + this.app.delete('/api/v1/apikey/:id', async (req: Request, res: Response) => { const keys = await deleteAPIKey(req.params.id) return addChatflowsCount(keys, res) }) // Verify api key - this.app.get('/api/v1/verify/apikey/:apiKey', param('apikey').notEmpty().escape(), async (req: Request, res: Response) => { - const valResult = validationResult(req) - if (!valResult.isEmpty()) { - return res.status(404).send(`Error Processing API Key`) - } + this.app.get('/api/v1/verify/apikey/:apiKey', async (req: Request, res: Response) => { try { const apiKey = await getApiKey(req.params.apiKey) if (!apiKey) return res.status(401).send('Unauthorized') diff --git a/packages/server/src/utils/XSS.ts b/packages/server/src/utils/XSS.ts new file mode 100644 index 000000000..a69cde218 --- /dev/null +++ b/packages/server/src/utils/XSS.ts @@ -0,0 +1,11 @@ +import { Request, Response, NextFunction } from 'express' +let stripJs = require('strip-js') + +export function sanitizeMiddleware(req: Request, res: Response, next: NextFunction): void { + req.url = stripJs(req.url) + for (let p in req.query) { + req.query[p] = stripJs(req.query[p]) + } + + next() +} From 7578183ac25e74518e07f06a61b3eec8609ac2ed Mon Sep 17 00:00:00 2001 From: Henry Date: Thu, 7 Dec 2023 18:46:03 +0000 Subject: [PATCH 41/54] add custom analytics --- .../agents/OpenAIAssistant/OpenAIAssistant.ts | 32 +- packages/components/package.json | 3 +- packages/components/src/handler.ts | 489 ++++++++++++++++++ packages/server/src/index.ts | 2 + 4 files changed, 522 insertions(+), 4 deletions(-) diff --git a/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts b/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts index 7f2377bde..d44263948 100644 --- a/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts +++ b/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts @@ -8,6 +8,7 @@ import * as path from 'node:path' import fetch from 'node-fetch' import { flatten, uniqWith, isEqual } from 'lodash' import { zodToJsonSchema } from 'zod-to-json-schema' +import { AnalyticHandler } from '../../../src/handler' class OpenAIAssistant_Agents implements INode { label: string @@ -149,6 +150,11 @@ class OpenAIAssistant_Agents implements INode { const openai = new OpenAI({ apiKey: openAIApiKey }) + // Start analytics + const analyticHandlers = new AnalyticHandler(nodeData, options) + await analyticHandlers.init() + const parentIds = await analyticHandlers.onChainStart('OpenAIAssistant', input) + try { const assistantDetails = JSON.parse(assistant.details) const openAIAssistantId = assistantDetails.id @@ -171,7 +177,8 @@ class OpenAIAssistant_Agents implements INode { } const chatmessage = await appDataSource.getRepository(databaseEntities['ChatMessage']).findOneBy({ - chatId: options.chatId + chatId: options.chatId, + chatflowid: options.chatflowid }) let threadId = '' @@ -185,7 +192,7 @@ class OpenAIAssistant_Agents implements INode { threadId = thread.id } - // List all runs + // List all runs, in case existing thread is still running if (!isNewThread) { const promise = (threadId: string) => { return new Promise((resolve) => { @@ -221,6 +228,7 @@ class OpenAIAssistant_Agents implements INode { }) // Run assistant thread + const llmIds = await analyticHandlers.onLLMStart('ChatOpenAI', input, parentIds) const runThread = await openai.beta.threads.runs.create(threadId, { assistant_id: retrievedAssistant.id }) @@ -253,7 +261,15 @@ class OpenAIAssistant_Agents implements INode { for (let i = 0; i < actions.length; i += 1) { const tool = tools.find((tool: any) => tool.name === actions[i].tool) if (!tool) continue + + // Start tool analytics + const toolIds = await analyticHandlers.onToolStart(tool.name, actions[i].toolInput, parentIds) + const toolOutput = await tool.call(actions[i].toolInput) + + // End tool analytics + await analyticHandlers.onToolEnd(toolIds, toolOutput) + submitToolOutputs.push({ tool_call_id: actions[i].toolCallId, output: toolOutput @@ -302,7 +318,9 @@ class OpenAIAssistant_Agents implements INode { runThreadId = newRunThread.id state = await promise(threadId, newRunThread.id) } else { - throw new Error(`Error processing thread: ${state}, Thread ID: ${threadId}`) + const errMsg = `Error processing thread: ${state}, Thread ID: ${threadId}` + await analyticHandlers.onChainError(parentIds, errMsg) + throw new Error(errMsg) } } @@ -387,11 +405,18 @@ class OpenAIAssistant_Agents implements INode { const bitmap = fsDefault.readFileSync(filePath) const base64String = Buffer.from(bitmap).toString('base64') + // TODO: Use a file path and retrieve image on the fly. Storing as base64 to localStorage and database will easily hit limits const imgHTML = `${fileObj.filename}
` returnVal += imgHTML } } + const imageRegex = /]*\/>/g + let llmOutput = returnVal.replace(imageRegex, '') + llmOutput = llmOutput.replace('
', '') + await analyticHandlers.onLLMEnd(llmIds, llmOutput) + await analyticHandlers.onChainEnd(parentIds, messageData, true) + return { text: returnVal, usedTools, @@ -399,6 +424,7 @@ class OpenAIAssistant_Agents implements INode { assistant: { assistantId: openAIAssistantId, threadId, runId: runThreadId, messages: messageData } } } catch (error) { + await analyticHandlers.onChainError(parentIds, error, true) throw new Error(error) } } diff --git a/packages/components/package.json b/packages/components/package.json index dd87754d5..a775e630d 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -51,8 +51,9 @@ "husky": "^8.0.3", "ioredis": "^5.3.2", "langchain": "^0.0.196", + "langfuse": "^1.2.0", "langfuse-langchain": "^1.0.31", - "langsmith": "^0.0.32", + "langsmith": "^0.0.49", "linkifyjs": "^4.1.1", "llmonitor": "^0.5.5", "mammoth": "^1.5.1", diff --git a/packages/components/src/handler.ts b/packages/components/src/handler.ts index 456cf39c3..ae5a9de00 100644 --- a/packages/components/src/handler.ts +++ b/packages/components/src/handler.ts @@ -8,6 +8,10 @@ import { LLMonitorHandler } from 'langchain/callbacks/handlers/llmonitor' import { getCredentialData, getCredentialParam } from './utils' import { ICommonObject, INodeData } from './Interface' import CallbackHandler from 'langfuse-langchain' +import { RunTree, RunTreeConfig, Client as LangsmithClient } from 'langsmith' +import { Langfuse, LangfuseTraceClient, LangfuseSpanClient, LangfuseGenerationClient } from 'langfuse' // or "langfuse-node" +import monitor from 'llmonitor' +import { v4 as uuidv4 } from 'uuid' interface AgentRun extends Run { actions: AgentAction[] @@ -273,3 +277,488 @@ export const additionalCallbacks = async (nodeData: INodeData, options: ICommonO throw new Error(e) } } + +export class AnalyticHandler { + nodeData: INodeData + options: ICommonObject = {} + handlers: ICommonObject = {} + + constructor(nodeData: INodeData, options: ICommonObject) { + this.options = options + this.nodeData = nodeData + this.init() + } + + async init() { + try { + if (!this.options.analytic) return + + const analytic = JSON.parse(this.options.analytic) + + for (const provider in analytic) { + const providerStatus = analytic[provider].status as boolean + + if (providerStatus) { + const credentialId = analytic[provider].credentialId as string + const credentialData = await getCredentialData(credentialId ?? '', this.options) + if (provider === 'langSmith') { + const langSmithProject = analytic[provider].projectName as string + const langSmithApiKey = getCredentialParam('langSmithApiKey', credentialData, this.nodeData) + const langSmithEndpoint = getCredentialParam('langSmithEndpoint', credentialData, this.nodeData) + + const client = new LangsmithClient({ + apiUrl: langSmithEndpoint ?? 'https://api.smith.langchain.com', + apiKey: langSmithApiKey + }) + + this.handlers['langSmith'] = { client, langSmithProject } + } else if (provider === 'langFuse') { + const release = analytic[provider].release as string + const langFuseSecretKey = getCredentialParam('langFuseSecretKey', credentialData, this.nodeData) + const langFusePublicKey = getCredentialParam('langFusePublicKey', credentialData, this.nodeData) + const langFuseEndpoint = getCredentialParam('langFuseEndpoint', credentialData, this.nodeData) + + const langfuse = new Langfuse({ + secretKey: langFuseSecretKey, + publicKey: langFusePublicKey, + baseUrl: langFuseEndpoint ?? 'https://cloud.langfuse.com', + release + }) + this.handlers['langFuse'] = { client: langfuse } + } else if (provider === 'llmonitor') { + const llmonitorAppId = getCredentialParam('llmonitorAppId', credentialData, this.nodeData) + const llmonitorEndpoint = getCredentialParam('llmonitorEndpoint', credentialData, this.nodeData) + + monitor.init({ + appId: llmonitorAppId, + apiUrl: llmonitorEndpoint + }) + + this.handlers['llmonitor'] = { client: monitor } + } + } + } + } catch (e) { + throw new Error(e) + } + } + + async onChainStart(name: string, input: string, parentIds?: ICommonObject) { + const returnIds: ICommonObject = { + langSmith: {}, + langFuse: {}, + llmonitor: {} + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'langSmith')) { + if (!parentIds || !Object.keys(parentIds).length) { + const parentRunConfig: RunTreeConfig = { + name, + run_type: 'chain', + inputs: { + text: input + }, + serialized: {}, + project_name: this.handlers['langSmith'].langSmithProject, + client: this.handlers['langSmith'].client + } + const parentRun = new RunTree(parentRunConfig) + await parentRun.postRun() + this.handlers['langSmith'].chainRun = { [parentRun.id]: parentRun } + returnIds['langSmith'].chainRun = parentRun.id + } else { + const parentRun: RunTree | undefined = this.handlers['langSmith'].chainRun[parentIds['langSmith'].chainRun] + if (parentRun) { + const childChainRun = await parentRun.createChild({ + name, + run_type: 'chain', + inputs: { + text: input + } + }) + await childChainRun.postRun() + this.handlers['langSmith'].chainRun = { [childChainRun.id]: childChainRun } + returnIds['langSmith'].chainRun = childChainRun.id + } + } + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'langFuse')) { + let langfuseTraceClient: LangfuseTraceClient + + if (!parentIds || !Object.keys(parentIds).length) { + const langfuse: Langfuse = this.handlers['langFuse'].client + langfuseTraceClient = langfuse.trace({ + name, + userId: this.options.chatId, + metadata: { tags: ['openai-assistant'] } + }) + } else { + langfuseTraceClient = this.handlers['langFuse'].trace[parentIds['langFuse']] + } + + if (langfuseTraceClient) { + const span = langfuseTraceClient.span({ + name, + input: { + text: input + } + }) + this.handlers['langFuse'].trace = { [langfuseTraceClient.id]: langfuseTraceClient } + this.handlers['langFuse'].span = { [span.id]: span } + returnIds['langFuse'].trace = langfuseTraceClient.id + returnIds['langFuse'].span = span.id + } + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'llmonitor')) { + const monitor = this.handlers['llmonitor'].client + + if (monitor) { + const runId = uuidv4() + await monitor.trackEvent('chain', 'start', { + runId, + name, + userId: this.options.chatId, + input + }) + this.handlers['llmonitor'].chainEvent = { [runId]: runId } + returnIds['llmonitor'].chainEvent = runId + } + } + + return returnIds + } + + async onChainEnd(returnIds: ICommonObject, output: string | object, shutdown = false) { + if (Object.prototype.hasOwnProperty.call(this.handlers, 'langSmith')) { + const chainRun: RunTree | undefined = this.handlers['langSmith'].chainRun[returnIds['langSmith'].chainRun] + if (chainRun) { + await chainRun.end({ + outputs: { + output + } + }) + await chainRun.patchRun() + } + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'langFuse')) { + const span: LangfuseSpanClient | undefined = this.handlers['langFuse'].span[returnIds['langFuse'].span] + if (span) { + span.end({ + output + }) + if (shutdown) { + const langfuse: Langfuse = this.handlers['langFuse'].client + await langfuse.shutdownAsync() + } + } + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'llmonitor')) { + const chainEventId = returnIds['llmonitor'].chainEvent + const monitor = this.handlers['llmonitor'].client + + if (monitor && chainEventId) { + await monitor.trackEvent('chain', 'end', { + runId: chainEventId, + output + }) + } + } + } + + async onChainError(returnIds: ICommonObject, error: string | object, shutdown = false) { + if (Object.prototype.hasOwnProperty.call(this.handlers, 'langSmith')) { + const chainRun: RunTree | undefined = this.handlers['langSmith'].chainRun[returnIds['langSmith'].chainRun] + if (chainRun) { + await chainRun.end({ + error: { + error + } + }) + await chainRun.patchRun() + } + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'langFuse')) { + const span: LangfuseSpanClient | undefined = this.handlers['langFuse'].span[returnIds['langFuse'].span] + if (span) { + span.end({ + output: { + error + } + }) + if (shutdown) { + const langfuse: Langfuse = this.handlers['langFuse'].client + await langfuse.shutdownAsync() + } + } + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'llmonitor')) { + const chainEventId = returnIds['llmonitor'].chainEvent + const monitor = this.handlers['llmonitor'].client + + if (monitor && chainEventId) { + await monitor.trackEvent('chain', 'end', { + runId: chainEventId, + output: error + }) + } + } + } + + async onLLMStart(name: string, input: string, parentIds: ICommonObject) { + const returnIds: ICommonObject = { + langSmith: {}, + langFuse: {}, + llmonitor: {} + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'langSmith')) { + const parentRun: RunTree | undefined = this.handlers['langSmith'].chainRun[parentIds['langSmith'].chainRun] + if (parentRun) { + const childLLMRun = await parentRun.createChild({ + name, + run_type: 'llm', + inputs: { + prompts: [input] + } + }) + await childLLMRun.postRun() + this.handlers['langSmith'].llmRun = { [childLLMRun.id]: childLLMRun } + returnIds['langSmith'].llmRun = childLLMRun.id + } + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'langFuse')) { + const trace: LangfuseTraceClient | undefined = this.handlers['langFuse'].trace[parentIds['langFuse'].trace] + if (trace) { + const generation = trace.generation({ + name, + prompt: input + }) + this.handlers['langFuse'].generation = { [generation.id]: generation } + returnIds['langFuse'].generation = generation.id + } + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'llmonitor')) { + const monitor = this.handlers['llmonitor'].client + const chainEventId: string = this.handlers['llmonitor'].chainEvent[parentIds['llmonitor'].chainEvent] + + if (monitor && chainEventId) { + const runId = uuidv4() + await monitor.trackEvent('llm', 'start', { + runId, + parentRunId: chainEventId, + name, + userId: this.options.chatId, + input + }) + this.handlers['llmonitor'].llmEvent = { [runId]: runId } + returnIds['llmonitor'].llmEvent = runId + } + } + + return returnIds + } + + async onLLMEnd(returnIds: ICommonObject, output: string) { + if (Object.prototype.hasOwnProperty.call(this.handlers, 'langSmith')) { + const llmRun: RunTree | undefined = this.handlers['langSmith'].llmRun[returnIds['langSmith'].llmRun] + if (llmRun) { + await llmRun.end({ + outputs: { + generations: [output] + } + }) + await llmRun.patchRun() + } + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'langFuse')) { + const generation: LangfuseGenerationClient | undefined = this.handlers['langFuse'].generation[returnIds['langFuse'].generation] + if (generation) { + generation.end({ + completion: output + }) + } + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'llmonitor')) { + const llmEventId: string = this.handlers['llmonitor'].llmEvent[returnIds['llmonitor'].llmEvent] + const monitor = this.handlers['llmonitor'].client + + if (monitor && llmEventId) { + await monitor.trackEvent('llm', 'end', { + runId: llmEventId, + output + }) + } + } + } + + async onLLMError(returnIds: ICommonObject, error: string | object) { + if (Object.prototype.hasOwnProperty.call(this.handlers, 'langSmith')) { + const llmRun: RunTree | undefined = this.handlers['langSmith'].llmRun[returnIds['langSmith'].llmRun] + if (llmRun) { + await llmRun.end({ + error: { + error + } + }) + await llmRun.patchRun() + } + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'langFuse')) { + const generation: LangfuseGenerationClient | undefined = this.handlers['langFuse'].generation[returnIds['langFuse'].generation] + if (generation) { + generation.end({ + completion: error + }) + } + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'llmonitor')) { + const llmEventId: string = this.handlers['llmonitor'].llmEvent[returnIds['llmonitor'].llmEvent] + const monitor = this.handlers['llmonitor'].client + + if (monitor && llmEventId) { + await monitor.trackEvent('llm', 'end', { + runId: llmEventId, + output: error + }) + } + } + } + + async onToolStart(name: string, input: string | object, parentIds: ICommonObject) { + const returnIds: ICommonObject = { + langSmith: {}, + langFuse: {}, + llmonitor: {} + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'langSmith')) { + const parentRun: RunTree | undefined = this.handlers['langSmith'].chainRun[parentIds['langSmith'].chainRun] + if (parentRun) { + const childToolRun = await parentRun.createChild({ + name, + run_type: 'tool', + inputs: { + input + } + }) + await childToolRun.postRun() + this.handlers['langSmith'].toolRun = { [childToolRun.id]: childToolRun } + returnIds['langSmith'].toolRun = childToolRun.id + } + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'langFuse')) { + const trace: LangfuseTraceClient | undefined = this.handlers['langFuse'].trace[parentIds['langFuse'].trace] + if (trace) { + const toolSpan = trace.span({ + name, + input + }) + this.handlers['langFuse'].toolSpan = { [toolSpan.id]: toolSpan } + returnIds['langFuse'].toolSpan = toolSpan.id + } + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'llmonitor')) { + const monitor = this.handlers['llmonitor'].client + const chainEventId: string = this.handlers['llmonitor'].chainEvent[parentIds['llmonitor'].chainEvent] + + if (monitor && chainEventId) { + const runId = uuidv4() + await monitor.trackEvent('tool', 'start', { + runId, + parentRunId: chainEventId, + name, + userId: this.options.chatId, + input + }) + this.handlers['llmonitor'].toolEvent = { [runId]: runId } + returnIds['llmonitor'].toolEvent = runId + } + } + + return returnIds + } + + async onToolEnd(returnIds: ICommonObject, output: string | object) { + if (Object.prototype.hasOwnProperty.call(this.handlers, 'langSmith')) { + const toolRun: RunTree | undefined = this.handlers['langSmith'].toolRun[returnIds['langSmith'].toolRun] + if (toolRun) { + await toolRun.end({ + outputs: { + output + } + }) + await toolRun.patchRun() + } + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'langFuse')) { + const toolSpan: LangfuseSpanClient | undefined = this.handlers['langFuse'].toolSpan[returnIds['langFuse'].toolSpan] + if (toolSpan) { + toolSpan.end({ + output + }) + } + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'llmonitor')) { + const toolEventId: string = this.handlers['llmonitor'].toolEvent[returnIds['llmonitor'].toolEvent] + const monitor = this.handlers['llmonitor'].client + + if (monitor && toolEventId) { + await monitor.trackEvent('tool', 'end', { + runId: toolEventId, + output + }) + } + } + } + + async onToolError(returnIds: ICommonObject, error: string | object) { + if (Object.prototype.hasOwnProperty.call(this.handlers, 'langSmith')) { + const toolRun: RunTree | undefined = this.handlers['langSmith'].toolRun[returnIds['langSmith'].toolRun] + if (toolRun) { + await toolRun.end({ + error: { + error + } + }) + await toolRun.patchRun() + } + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'langFuse')) { + const toolSpan: LangfuseSpanClient | undefined = this.handlers['langFuse'].toolSpan[returnIds['langFuse'].toolSpan] + if (toolSpan) { + toolSpan.end({ + output: error + }) + } + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'llmonitor')) { + const toolEventId: string = this.handlers['llmonitor'].llmEvent[returnIds['llmonitor'].toolEvent] + const monitor = this.handlers['llmonitor'].client + + if (monitor && toolEventId) { + await monitor.trackEvent('tool', 'end', { + runId: toolEventId, + output: error + }) + } + } + } +} diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index d87d2c0ac..61e55159b 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1470,6 +1470,7 @@ export class App { let result = isStreamValid ? await nodeInstance.run(nodeToExecuteData, incomingInput.question, { + chatflowid, chatHistory, socketIO, socketIOClientId: incomingInput.socketIOClientId, @@ -1480,6 +1481,7 @@ export class App { chatId }) : await nodeInstance.run(nodeToExecuteData, incomingInput.question, { + chatflowid, chatHistory, logger, appDataSource: this.AppDataSource, From c9a7ee2ad4c1d23471b926babf848e18c22d7f1b Mon Sep 17 00:00:00 2001 From: Henry Date: Thu, 7 Dec 2023 23:17:27 +0000 Subject: [PATCH 42/54] update hyde retriever --- .../retrievers/HydeRetriever/HydeRetriever.ts | 47 ++++++++++++++----- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/packages/components/nodes/retrievers/HydeRetriever/HydeRetriever.ts b/packages/components/nodes/retrievers/HydeRetriever/HydeRetriever.ts index 9ec7ada0c..10d9a6e7a 100644 --- a/packages/components/nodes/retrievers/HydeRetriever/HydeRetriever.ts +++ b/packages/components/nodes/retrievers/HydeRetriever/HydeRetriever.ts @@ -18,7 +18,7 @@ class HydeRetriever_Retrievers implements INode { constructor() { this.label = 'Hyde Retriever' this.name = 'HydeRetriever' - this.version = 1.0 + this.version = 2.0 this.type = 'HydeRetriever' this.icon = 'hyderetriever.svg' this.category = 'Retrievers' @@ -36,41 +36,66 @@ class HydeRetriever_Retrievers implements INode { type: 'VectorStore' }, { - label: 'Prompt Key', + label: 'Select Defined Prompt', name: 'promptKey', + description: 'Select a pre-defined prompt', type: 'options', options: [ { label: 'websearch', - name: 'websearch' + name: 'websearch', + description: `Please write a passage to answer the question +Question: {question} +Passage:` }, { label: 'scifact', - name: 'scifact' + name: 'scifact', + description: `Please write a scientific paper passage to support/refute the claim +Claim: {question} +Passage:` }, { label: 'arguana', - name: 'arguana' + name: 'arguana', + description: `Please write a counter argument for the passage +Passage: {question} +Counter Argument:` }, { label: 'trec-covid', - name: 'trec-covid' + name: 'trec-covid', + description: `Please write a scientific paper passage to answer the question +Question: {question} +Passage:` }, { label: 'fiqa', - name: 'fiqa' + name: 'fiqa', + description: `Please write a financial article passage to answer the question +Question: {question} +Passage:` }, { label: 'dbpedia-entity', - name: 'dbpedia-entity' + name: 'dbpedia-entity', + description: `Please write a passage to answer the question. +Question: {question} +Passage:` }, { label: 'trec-news', - name: 'trec-news' + name: 'trec-news', + description: `Please write a news passage about the topic. +Topic: {question} +Passage:` }, { label: 'mr-tydi', - name: 'mr-tydi' + name: 'mr-tydi', + description: `Please write a passage in Swahili/Korean/Japanese/Bengali to answer the question in detail. +Question: {question} +Passage:` } ], default: 'websearch' @@ -78,7 +103,7 @@ class HydeRetriever_Retrievers implements INode { { label: 'Custom Prompt', name: 'customPrompt', - description: 'If custom prompt is used, this will override Prompt Key', + description: 'If custom prompt is used, this will override Defined Prompt', placeholder: 'Please write a passage to answer the question\nQuestion: {question}\nPassage:', type: 'string', rows: 4, From da2fe78e4491a3d611266325ba9b2b2e06c1b732 Mon Sep 17 00:00:00 2001 From: Henry Heng Date: Fri, 8 Dec 2023 12:09:29 +0000 Subject: [PATCH 43/54] Update handler.ts --- packages/components/src/handler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/src/handler.ts b/packages/components/src/handler.ts index ae5a9de00..29aff3e2f 100644 --- a/packages/components/src/handler.ts +++ b/packages/components/src/handler.ts @@ -9,7 +9,7 @@ import { getCredentialData, getCredentialParam } from './utils' import { ICommonObject, INodeData } from './Interface' import CallbackHandler from 'langfuse-langchain' import { RunTree, RunTreeConfig, Client as LangsmithClient } from 'langsmith' -import { Langfuse, LangfuseTraceClient, LangfuseSpanClient, LangfuseGenerationClient } from 'langfuse' // or "langfuse-node" +import { Langfuse, LangfuseTraceClient, LangfuseSpanClient, LangfuseGenerationClient } from 'langfuse' import monitor from 'llmonitor' import { v4 as uuidv4 } from 'uuid' From 99bc9d64fbd79aacfd2d488ace0044dcb61fb391 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Fri, 8 Dec 2023 18:50:58 +0530 Subject: [PATCH 44/54] XSS: replacing deprecated sanitize-js with sanitize-html --- packages/server/package.json | 2 +- packages/server/src/utils/XSS.ts | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/server/package.json b/packages/server/package.json index 97a95d43c..013e60072 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -61,9 +61,9 @@ "mysql": "^2.18.1", "pg": "^8.11.1", "reflect-metadata": "^0.1.13", + "sanitize-html": "^2.11.0", "socket.io": "^4.6.1", "sqlite3": "^5.1.6", - "strip-js": "^1.2.0", "typeorm": "^0.3.6", "uuid": "^9.0.1", "winston": "^3.9.0" diff --git a/packages/server/src/utils/XSS.ts b/packages/server/src/utils/XSS.ts index a69cde218..329c2ed2f 100644 --- a/packages/server/src/utils/XSS.ts +++ b/packages/server/src/utils/XSS.ts @@ -1,10 +1,12 @@ import { Request, Response, NextFunction } from 'express' -let stripJs = require('strip-js') +const sanitizeHtml = require('sanitize-html') export function sanitizeMiddleware(req: Request, res: Response, next: NextFunction): void { - req.url = stripJs(req.url) + // decoding is necessary as the url is encoded by the browser + const decodedURI = decodeURI(req.url) + req.url = sanitizeHtml(decodedURI) for (let p in req.query) { - req.query[p] = stripJs(req.query[p]) + req.query[p] = sanitizeHtml(req.query[p]) } next() From b91cf551288016855407c5f22ac9cc1027e0f855 Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 8 Dec 2023 14:14:22 +0000 Subject: [PATCH 45/54] =?UTF-8?q?=F0=9F=A5=B3=20flowise-components@1.4.6?= =?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 a775e630d..66e6d6d93 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "flowise-components", - "version": "1.4.5", + "version": "1.4.6", "description": "Flowiseai Components", "main": "dist/src/index", "types": "dist/src/index.d.ts", From 1e5a37ad0f673d1ee8e2ed2d13b3b5711b86578f Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 8 Dec 2023 14:16:04 +0000 Subject: [PATCH 46/54] =?UTF-8?q?=F0=9F=A5=B3=20flowise-ui@1.4.2=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 76369ab2e..2b1f49e90 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "flowise-ui", - "version": "1.4.1", + "version": "1.4.2", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://flowiseai.com", "author": { From a0c2b8b26a3721fff503aa17a210cb298ffee1fb Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 8 Dec 2023 14:19:57 +0000 Subject: [PATCH 47/54] =?UTF-8?q?=F0=9F=A5=B3=20flowise@1.4.4=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 649a9a471..1993c7e5b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "flowise", - "version": "1.4.3", + "version": "1.4.4", "private": true, "homepage": "https://flowiseai.com", "workspaces": [ diff --git a/packages/server/package.json b/packages/server/package.json index 6f4ccaf41..f2ec5c098 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "flowise", - "version": "1.4.3", + "version": "1.4.4", "description": "Flowiseai Server", "main": "dist/index", "types": "dist/index.d.ts", From d2d21c45fe36128a0d299f2eb349c93520902c45 Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 8 Dec 2023 18:51:40 +0000 Subject: [PATCH 48/54] fix upser vector API --- packages/server/src/ChatflowPool.ts | 2 +- packages/server/src/Interface.ts | 2 +- packages/server/src/index.ts | 18 +++++++++++++----- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/server/src/ChatflowPool.ts b/packages/server/src/ChatflowPool.ts index d296dcfed..325fac560 100644 --- a/packages/server/src/ChatflowPool.ts +++ b/packages/server/src/ChatflowPool.ts @@ -16,7 +16,7 @@ export class ChatflowPool { * @param {IReactFlowNode[]} startingNodes * @param {ICommonObject} overrideConfig */ - add(chatflowid: string, endingNodeData: INodeData, startingNodes: IReactFlowNode[], overrideConfig?: ICommonObject) { + add(chatflowid: string, endingNodeData: INodeData | undefined, startingNodes: IReactFlowNode[], overrideConfig?: ICommonObject) { this.activeChatflows[chatflowid] = { startingNodes, endingNodeData, diff --git a/packages/server/src/Interface.ts b/packages/server/src/Interface.ts index d5890ab6c..f82c66902 100644 --- a/packages/server/src/Interface.ts +++ b/packages/server/src/Interface.ts @@ -172,7 +172,7 @@ export interface IncomingInput { export interface IActiveChatflows { [key: string]: { startingNodes: IReactFlowNode[] - endingNodeData: INodeData + endingNodeData?: INodeData inSync: boolean overrideConfig?: ICommonObject } diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 0262bff4c..95b8aa013 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1394,16 +1394,19 @@ export class App { const nodes = parsedFlowData.nodes const edges = parsedFlowData.edges - /* Reuse the flow without having to rebuild (to avoid duplicated upsert, recomputation) when all these conditions met: + /* Reuse the flow without having to rebuild (to avoid duplicated upsert, recomputation, reinitialization of memory) when all these conditions met: * - Node Data already exists in pool * - Still in sync (i.e the flow has not been modified since) * - Existing overrideConfig and new overrideConfig are the same * - Flow doesn't start with/contain nodes that depend on incomingInput.question + * - Its not an Upsert request + * TODO: convert overrideConfig to hash when we no longer store base64 string but filepath ***/ const isFlowReusable = () => { return ( Object.prototype.hasOwnProperty.call(this.chatflowPool.activeChatflows, chatflowid) && this.chatflowPool.activeChatflows[chatflowid].inSync && + this.chatflowPool.activeChatflows[chatflowid].endingNodeData && isSameOverrideConfig( isInternal, this.chatflowPool.activeChatflows[chatflowid].overrideConfig, @@ -1415,7 +1418,7 @@ export class App { } if (isFlowReusable()) { - nodeToExecuteData = this.chatflowPool.activeChatflows[chatflowid].endingNodeData + nodeToExecuteData = this.chatflowPool.activeChatflows[chatflowid].endingNodeData as INodeData isStreamValid = isFlowValidForStream(nodes, nodeToExecuteData) logger.debug( `[server]: Reuse existing chatflow ${chatflowid} with ending node ${nodeToExecuteData.label} (${nodeToExecuteData.id})` @@ -1466,6 +1469,7 @@ export class App { const constructedObj = constructGraphs(nodes, edges, true) const nonDirectedGraph = constructedObj.graph const { startingNodeIds, depthQueue } = getStartingNodes(nonDirectedGraph, endingNodeId) + const startingNodes = nodes.filter((nd) => startingNodeIds.includes(nd.id)) logger.debug(`[server]: Start building chatflow ${chatflowid}`) /*** BFS to traverse from Starting Nodes to Ending Node ***/ @@ -1485,13 +1489,18 @@ export class App { isUpsert, incomingInput.stopNodeId ) - if (isUpsert) return res.status(201).send('Successfully Upserted') + if (isUpsert) { + this.chatflowPool.add(chatflowid, undefined, startingNodes, incomingInput?.overrideConfig) + return res.status(201).send('Successfully Upserted') + } const nodeToExecute = reactFlowNodes.find((node: IReactFlowNode) => node.id === endingNodeId) if (!nodeToExecute) return res.status(404).send(`Node ${endingNodeId} not found`) - if (incomingInput.overrideConfig) + if (incomingInput.overrideConfig) { nodeToExecute.data = replaceInputsWithConfig(nodeToExecute.data, incomingInput.overrideConfig) + } + const reactFlowNodeData: INodeData = resolveVariables( nodeToExecute.data, reactFlowNodes, @@ -1500,7 +1509,6 @@ export class App { ) nodeToExecuteData = reactFlowNodeData - const startingNodes = nodes.filter((nd) => startingNodeIds.includes(nd.id)) this.chatflowPool.add(chatflowid, nodeToExecuteData, startingNodes, incomingInput?.overrideConfig) } From 9a5d5720f9fa67f6f90d0cb5b482967a61d1d16f Mon Sep 17 00:00:00 2001 From: Henry Date: Sat, 9 Dec 2023 13:49:53 +0000 Subject: [PATCH 49/54] get rid of credential for langchain hub --- packages/server/src/index.ts | 27 +- .../dialog/PromptLangsmithHubDialog.js | 301 ++++++++---------- .../ui/src/views/canvas/NodeInputHandler.js | 2 +- 3 files changed, 140 insertions(+), 190 deletions(-) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 95b8aa013..805a0fec6 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1044,18 +1044,9 @@ export class App { // ---------------------------------------- this.app.post('/api/v1/load-prompt', async (req: Request, res: Response) => { try { - const credential = await this.AppDataSource.getRepository(Credential).findOneBy({ - id: req.body.credential - }) - - if (!credential) return res.status(404).json({ error: `Credential ${req.body.credential} not found` }) - - // Decrypt credentialData - const decryptedCredentialData = await decryptCredentialData(credential.encryptedData, credential.credentialName, undefined) - let hub = new Client({ apiKey: decryptedCredentialData.langsmithApiKey, apiUrl: decryptedCredentialData.langsmithEndpoint }) + let hub = new Client() const prompt = await hub.pull(req.body.promptName) const templates = parsePrompt(prompt) - return res.json({ status: 'OK', prompt: req.body.promptName, templates: templates }) } catch (e: any) { return res.json({ status: 'ERROR', prompt: req.body.promptName, error: e?.message }) @@ -1064,22 +1055,10 @@ export class App { this.app.post('/api/v1/prompts-list', async (req: Request, res: Response) => { try { - const credential = await this.AppDataSource.getRepository(Credential).findOneBy({ - id: req.body.credential - }) - - if (!credential) return res.status(404).json({ error: `Credential ${req.body.credential} not found` }) - // Decrypt credentialData - const decryptedCredentialData = await decryptCredentialData(credential.encryptedData, credential.credentialName, undefined) - - const headers = {} - // @ts-ignore - headers['x-api-key'] = decryptedCredentialData.langsmithApiKey - const tags = req.body.tags ? `tags=${req.body.tags}` : '' // Default to 100, TODO: add pagination and use offset & limit - const url = `https://web.hub.langchain.com/repos/?limit=100&${tags}has_commits=true&sort_field=num_likes&sort_direction=desc&is_archived=false` - axios.get(url, headers).then((response) => { + const url = `https://api.hub.langchain.com/repos/?limit=100&${tags}has_commits=true&sort_field=num_likes&sort_direction=desc&is_archived=false` + axios.get(url).then((response) => { if (response.data.repos) { return res.json({ status: 'OK', repos: response.data.repos }) } diff --git a/packages/ui/src/ui-component/dialog/PromptLangsmithHubDialog.js b/packages/ui/src/ui-component/dialog/PromptLangsmithHubDialog.js index e6f06e209..35b4ead78 100644 --- a/packages/ui/src/ui-component/dialog/PromptLangsmithHubDialog.js +++ b/packages/ui/src/ui-component/dialog/PromptLangsmithHubDialog.js @@ -42,12 +42,12 @@ import ClearIcon from '@mui/icons-material/Clear' import { styled } from '@mui/material/styles' //Project Import -import CredentialInputHandler from 'views/canvas/CredentialInputHandler' import { StyledButton } from 'ui-component/button/StyledButton' import { MemoizedReactMarkdown } from 'ui-component/markdown/MemoizedReactMarkdown' import { CodeBlock } from 'ui-component/markdown/CodeBlock' import promptEmptySVG from 'assets/images/prompt_empty.svg' +import useApi from 'hooks/useApi' import promptApi from 'api/prompt' import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions' @@ -89,6 +89,7 @@ const PromptLangsmithHubDialog = ({ promptType, show, onCancel, onSubmit }) => { const portalElement = document.getElementById('portal') const dispatch = useDispatch() const customization = useSelector((state) => state.customization) + const getAvailablePromptsApi = useApi(promptApi.getAvailablePrompts) useEffect(() => { if (show) dispatch({ type: SHOW_CANVAS_DIALOG }) @@ -98,6 +99,22 @@ const PromptLangsmithHubDialog = ({ promptType, show, onCancel, onSubmit }) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [show, dispatch]) + useEffect(() => { + if (promptType) { + getAvailablePromptsApi.request({ tags: promptType === 'template' ? 'StringPromptTemplate&' : 'ChatPromptTemplate&' }) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [promptType]) + + useEffect(() => { + if (getAvailablePromptsApi.data && getAvailablePromptsApi.data.repos) { + setAvailablePrompNameList(getAvailablePromptsApi.data.repos) + if (getAvailablePromptsApi.data.repos?.length) handleListItemClick(0, getAvailablePromptsApi.data.repos) + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [getAvailablePromptsApi.data]) + const ITEM_HEIGHT = 48 const ITEM_PADDING_TOP = 8 const MenuProps = { @@ -156,7 +173,6 @@ const PromptLangsmithHubDialog = ({ promptType, show, onCancel, onSubmit }) => { const [availablePrompNameList, setAvailablePrompNameList] = useState([]) const [selectedPrompt, setSelectedPrompt] = useState({}) - const [credentialId, setCredentialId] = useState('') const [accordionExpanded, setAccordionExpanded] = useState(['prompt']) const handleAccordionChange = (accordionName) => (event, isExpanded) => { @@ -173,7 +189,6 @@ const PromptLangsmithHubDialog = ({ promptType, show, onCancel, onSubmit }) => { if (!prompt.detailed) { const createResp = await promptApi.getPrompt({ - credential: credentialId, promptName: prompt.full_name }) if (createResp.data) { @@ -194,14 +209,7 @@ const PromptLangsmithHubDialog = ({ promptType, show, onCancel, onSubmit }) => { language.forEach((item) => { tags += `tags=${item.name}&` }) - const createResp = await promptApi.getAvailablePrompts({ - credential: credentialId, - tags: tags - }) - if (createResp.data) { - setAvailablePrompNameList(createResp.data.repos) - if (createResp.data.repos?.length) await handleListItemClick(0, createResp.data.repos) - } + getAvailablePromptsApi.request({ tags: tags }) } const removeDuplicates = (value) => { @@ -238,176 +246,139 @@ const PromptLangsmithHubDialog = ({ promptType, show, onCancel, onSubmit }) => { setLanguage(removeDuplicates(value)) } - const clear = () => { - setModelName([]) - setUsecase([]) - setLanguage([]) - setSelectedPrompt({}) - setAvailablePrompNameList([]) - setAccordionExpanded(['prompt']) - } - const component = show ? ( - Load Prompts from Langsmith Hub ({promptType === 'template' ? 'PromptTemplate' : 'ChatPromptTemplate'}) + Langchain Hub ({promptType === 'template' ? 'PromptTemplate' : 'ChatPromptTemplate'}) - - - Langsmith Credential   - * - - - + + + Model + + + + + + Usecase + + + + + + Language + + + + + - {credentialId && ( - - - - Model - - - - - - Usecase - - - - - - Language - - - - - - - - )} + {availablePrompNameList && availablePrompNameList.length == 0 && ( diff --git a/packages/ui/src/views/canvas/NodeInputHandler.js b/packages/ui/src/views/canvas/NodeInputHandler.js index 103af6b43..892a6273d 100644 --- a/packages/ui/src/views/canvas/NodeInputHandler.js +++ b/packages/ui/src/views/canvas/NodeInputHandler.js @@ -238,7 +238,7 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA onClick={() => onShowPromptHubButtonClicked()} endIcon={} > - Langsmith Prompt Hub + Langchain Hub Date: Sat, 9 Dec 2023 14:12:30 +0000 Subject: [PATCH 50/54] add sanitize html types --- packages/server/package.json | 1 + packages/server/src/utils/XSS.ts | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/server/package.json b/packages/server/package.json index 013e60072..1eeb43f1e 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -72,6 +72,7 @@ "@types/cors": "^2.8.12", "@types/crypto-js": "^4.1.1", "@types/multer": "^1.4.7", + "@types/sanitize-html": "^2.9.5", "concurrently": "^7.1.0", "nodemon": "^2.0.15", "oclif": "^3", diff --git a/packages/server/src/utils/XSS.ts b/packages/server/src/utils/XSS.ts index 329c2ed2f..3e96e6c8f 100644 --- a/packages/server/src/utils/XSS.ts +++ b/packages/server/src/utils/XSS.ts @@ -1,12 +1,12 @@ import { Request, Response, NextFunction } from 'express' -const sanitizeHtml = require('sanitize-html') +import sanitizeHtml from 'sanitize-html' export function sanitizeMiddleware(req: Request, res: Response, next: NextFunction): void { // decoding is necessary as the url is encoded by the browser const decodedURI = decodeURI(req.url) req.url = sanitizeHtml(decodedURI) for (let p in req.query) { - req.query[p] = sanitizeHtml(req.query[p]) + req.query[p] = sanitizeHtml(req.query[p] as string) } next() From c26e37a923b39a07ac81b2da23409b8efbc8c85c Mon Sep 17 00:00:00 2001 From: Henry Date: Sat, 9 Dec 2023 14:44:01 +0000 Subject: [PATCH 51/54] =?UTF-8?q?=F0=9F=A5=B3=20flowise-ui@1.4.3=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 2b1f49e90..7a739978e 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "flowise-ui", - "version": "1.4.2", + "version": "1.4.3", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://flowiseai.com", "author": { From bac91eed00bd6a97567c245798818f93b681a152 Mon Sep 17 00:00:00 2001 From: Henry Date: Sat, 9 Dec 2023 14:44:53 +0000 Subject: [PATCH 52/54] =?UTF-8?q?=F0=9F=A5=B3=20flowise@1.4.5=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 1993c7e5b..804c3c968 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "flowise", - "version": "1.4.4", + "version": "1.4.5", "private": true, "homepage": "https://flowiseai.com", "workspaces": [ diff --git a/packages/server/package.json b/packages/server/package.json index a73159995..ab1f61492 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "flowise", - "version": "1.4.4", + "version": "1.4.5", "description": "Flowiseai Server", "main": "dist/index", "types": "dist/index.d.ts", From f51c1d5b7a53cd877b000786d221298ed1766187 Mon Sep 17 00:00:00 2001 From: Henry Date: Mon, 11 Dec 2023 20:35:30 +0000 Subject: [PATCH 53/54] check for array query parameter --- packages/server/src/utils/XSS.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/server/src/utils/XSS.ts b/packages/server/src/utils/XSS.ts index 3e96e6c8f..5d8b81e91 100644 --- a/packages/server/src/utils/XSS.ts +++ b/packages/server/src/utils/XSS.ts @@ -6,8 +6,15 @@ export function sanitizeMiddleware(req: Request, res: Response, next: NextFuncti const decodedURI = decodeURI(req.url) req.url = sanitizeHtml(decodedURI) for (let p in req.query) { - req.query[p] = sanitizeHtml(req.query[p] as string) + if (Array.isArray(req.query[p])) { + const sanitizedQ = [] + for (const q of req.query[p] as string[]) { + sanitizedQ.push(sanitizeHtml(q)) + } + req.query[p] = sanitizedQ + } else { + req.query[p] = sanitizeHtml(req.query[p] as string) + } } - next() } From 8e1ef2d5337f8140e8789830b7c5844ee854cc24 Mon Sep 17 00:00:00 2001 From: Henry Date: Tue, 12 Dec 2023 12:44:42 +0000 Subject: [PATCH 54/54] fix sanitized & --- packages/server/src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 2ab454ad7..2d40f32ed 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -200,7 +200,7 @@ export class App { // Get component credential via name this.app.get('/api/v1/components-credentials/:name', (req: Request, res: Response) => { - if (!req.params.name.includes('&')) { + if (!req.params.name.includes('&')) { if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentCredentials, req.params.name)) { return res.json(this.nodesPool.componentCredentials[req.params.name]) } else { @@ -208,7 +208,7 @@ export class App { } } else { const returnResponse = [] - for (const name of req.params.name.split('&')) { + for (const name of req.params.name.split('&')) { if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentCredentials, name)) { returnResponse.push(this.nodesPool.componentCredentials[name]) } else {