diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4d1795ad151..35365219c54 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -133,7 +133,8 @@ Flowise support different environment variables to configure your instance. You | LOG_PATH | Location where log files are stored | String | `your-path/Flowise/logs` | | LOG_LEVEL | Different levels of logs | Enum String: `error`, `info`, `verbose`, `debug` | `info` | | LOG_JSON_SPACES | Spaces to beautify JSON logs | | 2 | -| APIKEY_PATH | Location where api keys are saved | String | `your-path/Flowise/packages/server` | +| APIKEY_STORAGE_TYPE | To store api keys on a JSON file or database. Default is `json` | Enum String: `json`, `db` | `json` | +| APIKEY_PATH | Location where api keys are saved when `APIKEY_STORAGE_TYPE` is `json` | String | `your-path/Flowise/packages/server` | | TOOL_FUNCTION_BUILTIN_DEP | NodeJS built-in modules to be used for Tool Function | String | | | TOOL_FUNCTION_EXTERNAL_DEP | External modules to be used for Tool Function | String | | | DATABASE_TYPE | Type of database to store the flowise data | Enum String: `sqlite`, `mysql`, `postgres` | `sqlite` | @@ -146,8 +147,8 @@ Flowise support different environment variables to configure your instance. You | DATABASE_SSL_KEY_BASE64 | Database SSL client cert in base64 (takes priority over DATABASE_SSL) | Boolean | false | | DATABASE_SSL | Database connection overssl (When DATABASE_TYPE is postgre) | Boolean | false | | SECRETKEY_PATH | Location where encryption key (used to encrypt/decrypt credentials) is saved | String | `your-path/Flowise/packages/server` | -| FLOWISE_SECRETKEY_OVERWRITE | Encryption key to be used instead of the key stored in SECRETKEY_PATH | String | -| DISABLE_FLOWISE_TELEMETRY | Turn off telemetry | Boolean | +| FLOWISE_SECRETKEY_OVERWRITE | Encryption key to be used instead of the key stored in SECRETKEY_PATH | String | | +| DISABLE_FLOWISE_TELEMETRY | Turn off telemetry | Boolean | | | MODEL_LIST_CONFIG_JSON | File path to load list of models from your local config file | String | `/your_model_list_config_file_path` | | STORAGE_TYPE | Type of storage for uploaded files. default is `local` | Enum String: `s3`, `local` | `local` | | BLOB_STORAGE_PATH | Local folder path where uploaded files are stored when `STORAGE_TYPE` is `local` | String | `your-home-dir/.flowise/storage` | @@ -155,6 +156,8 @@ Flowise support different environment variables to configure your instance. You | S3_STORAGE_ACCESS_KEY_ID | AWS Access Key | String | | | S3_STORAGE_SECRET_ACCESS_KEY | AWS Secret Key | String | | | S3_STORAGE_REGION | Region for S3 bucket | String | | +| S3_ENDPOINT_URL | Custom Endpoint for S3 | String | | +| SHOW_COMMUNITY_NODES | Show nodes created by community | Boolean | | You can also specify the env variables when using `npx`. For example: diff --git a/Dockerfile b/Dockerfile index 314178d6d37..dfbf58d1bd0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,6 +18,8 @@ RUN npm install -g pnpm ENV PUPPETEER_SKIP_DOWNLOAD=true ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser +ENV NODE_OPTIONS=--max-old-space-size=8192 + WORKDIR /usr/src # Copy app source diff --git a/README.md b/README.md index c5a6c0cb1fe..420435714e9 100644 --- a/README.md +++ b/README.md @@ -82,31 +82,40 @@ Flowise has 3 different modules in a single mono repository. ### Setup -1. Clone the repository +1. Clone the repository ```bash git clone https://github.com/FlowiseAI/Flowise.git ``` -2. Go into repository folder +2. Go into repository folder ```bash cd Flowise ``` -3. Install all dependencies of all modules: +3. Install all dependencies of all modules: ```bash pnpm install ``` -4. Build all the code: +4. Build all the code: ```bash pnpm build ``` -5. Start the app: +
+ Exit code 134 (JavaScript heap out of memory) + If you get this error when running the above `build` script, try increasing the Node.js heap size and run the script again: + + export NODE_OPTIONS="--max-old-space-size=4096" + pnpm build + +
+ +5. Start the app: ```bash pnpm start @@ -114,7 +123,7 @@ Flowise has 3 different modules in a single mono repository. You can now access the app on [http://localhost:3000](http://localhost:3000) -6. For development build: +6. For development build: - Create `.env` file and specify the `VITE_PORT` (refer to `.env.example`) in `packages/ui` - Create `.env` file and specify the `PORT` (refer to `.env.example`) in `packages/server` diff --git a/docker/.env.example b/docker/.env.example index 5e368f96858..e429df91ab7 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -46,4 +46,8 @@ BLOB_STORAGE_PATH=/root/.flowise/storage # S3_STORAGE_BUCKET_NAME=flowise # S3_STORAGE_ACCESS_KEY_ID= # S3_STORAGE_SECRET_ACCESS_KEY= -# S3_STORAGE_REGION=us-west-2 \ No newline at end of file +# S3_STORAGE_REGION=us-west-2 +# S3_ENDPOINT_URL= + +# APIKEY_STORAGE_TYPE=json (json | db) +# SHOW_COMMUNITY_NODES=true \ No newline at end of file diff --git a/i18n/CONTRIBUTING-ZH.md b/i18n/CONTRIBUTING-ZH.md index bbf5db828ec..00bc490296b 100644 --- a/i18n/CONTRIBUTING-ZH.md +++ b/i18n/CONTRIBUTING-ZH.md @@ -128,7 +128,8 @@ Flowise 支持不同的环境变量来配置您的实例。您可以在 `package | DEBUG | 打印组件的日志 | 布尔值 | | | LOG_PATH | 存储日志文件的位置 | 字符串 | `your-path/Flowise/logs` | | LOG_LEVEL | 日志的不同级别 | 枚举字符串: `error`, `info`, `verbose`, `debug` | `info` | -| APIKEY_PATH | 存储 API 密钥的位置 | 字符串 | `your-path/Flowise/packages/server` | +| APIKEY_STORAGE_TYPE | 存储 API 密钥的存储类型 | 枚举字符串: `json`, `db` | `json` | +| APIKEY_PATH | 存储 API 密钥的位置, 当`APIKEY_STORAGE_TYPE`是`json` | 字符串 | `your-path/Flowise/packages/server` | | TOOL_FUNCTION_BUILTIN_DEP | 用于工具函数的 NodeJS 内置模块 | 字符串 | | | TOOL_FUNCTION_EXTERNAL_DEP | 用于工具函数的外部模块 | 字符串 | | | DATABASE_TYPE | 存储 flowise 数据的数据库类型 | 枚举字符串: `sqlite`, `mysql`, `postgres` | `sqlite` | @@ -148,6 +149,8 @@ Flowise 支持不同的环境变量来配置您的实例。您可以在 `package | S3_STORAGE_ACCESS_KEY_ID | AWS 访问密钥 (Access Key) | 字符串 | | | S3_STORAGE_SECRET_ACCESS_KEY | AWS 密钥 (Secret Key) | 字符串 | | | S3_STORAGE_REGION | S3 存储地区 | 字符串 | | +| S3_ENDPOINT_URL | S3 端点 URL | 字符串 | | +| SHOW_COMMUNITY_NODES | 显示由社区创建的节点 | 布尔值 | | 您也可以在使用 `npx` 时指定环境变量。例如: diff --git a/package.json b/package.json index 22384a4e67c..018b7815768 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "flowise", - "version": "2.0.0", + "version": "2.0.1", "private": true, "homepage": "https://flowiseai.com", "workspaces": [ @@ -56,7 +56,11 @@ "onlyBuiltDependencies": [ "faiss-node", "sqlite3" - ] + ], + "overrides": { + "@langchain/core": "0.2.18", + "@langchain/aws": "^0.0.6" + } }, "engines": { "node": ">=18.15.0 <19.0.0 || ^20", diff --git a/packages/components/models.json b/packages/components/models.json index 7b5a479e0fd..0863ccdd0a6 100644 --- a/packages/components/models.json +++ b/packages/components/models.json @@ -423,6 +423,26 @@ { "name": "groqChat", "models": [ + { + "label": "llama-3.1-405b-reasoning", + "name": "llama-3.1-405b-reasoning" + }, + { + "label": "llama-3.1-70b-versatile", + "name": "llama-3.1-70b-versatile" + }, + { + "label": "llama-3.1-8b-instant", + "name": "llama-3.1-8b-instant" + }, + { + "label": "llama3-groq-70b-8192-tool-use-preview", + "name": "llama3-groq-70b-8192-tool-use-preview" + }, + { + "label": "llama3-groq-8b-8192-tool-use-preview", + "name": "llama3-groq-8b-8192-tool-use-preview" + }, { "label": "gemma-7b-it", "name": "gemma-7b-it" diff --git a/packages/components/nodes/chatmodels/ChatFireworks/ChatFireworks.ts b/packages/components/nodes/chatmodels/ChatFireworks/ChatFireworks.ts index 91b19b229ad..471c0872923 100644 --- a/packages/components/nodes/chatmodels/ChatFireworks/ChatFireworks.ts +++ b/packages/components/nodes/chatmodels/ChatFireworks/ChatFireworks.ts @@ -59,6 +59,7 @@ class ChatFireworks_ChatModels implements INode { const cache = nodeData.inputs?.cache as BaseCache const temperature = nodeData.inputs?.temperature as string const modelName = nodeData.inputs?.modelName as string + const streaming = nodeData.inputs?.streaming as boolean const credentialData = await getCredentialData(nodeData.credential ?? '', options) const fireworksApiKey = getCredentialParam('fireworksApiKey', credentialData, nodeData) @@ -67,7 +68,8 @@ class ChatFireworks_ChatModels implements INode { fireworksApiKey, model: modelName, modelName, - temperature: temperature ? parseFloat(temperature) : undefined + temperature: temperature ? parseFloat(temperature) : undefined, + streaming: streaming ?? true } if (cache) obj.cache = cache diff --git a/packages/components/nodes/chatmodels/ChatOllama/ChatOllama.ts b/packages/components/nodes/chatmodels/ChatOllama/ChatOllama.ts index 68b74e41e71..0d72df79e30 100644 --- a/packages/components/nodes/chatmodels/ChatOllama/ChatOllama.ts +++ b/packages/components/nodes/chatmodels/ChatOllama/ChatOllama.ts @@ -1,9 +1,8 @@ -import { ChatOllama } from '@langchain/community/chat_models/ollama' +import { ChatOllama, ChatOllamaInput } from '@langchain/ollama' +import { BaseChatModelParams } from '@langchain/core/language_models/chat_models' import { BaseCache } from '@langchain/core/caches' import { INode, INodeData, INodeParams } from '../../../src/Interface' import { getBaseClasses } from '../../../src/utils' -import { OllamaInput } from '@langchain/community/llms/ollama' -import { BaseChatModelParams } from '@langchain/core/language_models/chat_models' class ChatOllama_ChatModels implements INode { label: string @@ -20,7 +19,7 @@ class ChatOllama_ChatModels implements INode { constructor() { this.label = 'ChatOllama' this.name = 'chatOllama' - this.version = 2.0 + this.version = 3.0 this.type = 'ChatOllama' this.icon = 'Ollama.svg' this.category = 'Chat Models' @@ -55,6 +54,15 @@ class ChatOllama_ChatModels implements INode { default: 0.9, optional: true }, + { + label: 'Keep Alive', + name: 'keepAlive', + type: 'string', + description: 'How long to keep connection alive. A duration string (such as "10m" or "24h")', + default: '5m', + optional: true, + additionalParams: true + }, { label: 'Top P', name: 'topP', @@ -115,16 +123,6 @@ class ChatOllama_ChatModels implements INode { optional: true, additionalParams: true }, - { - label: 'Number of GQA groups', - name: 'numGqa', - type: 'number', - description: - 'The number of GQA groups in the transformer layer. Required for some models, for example it is 8 for llama2:70b. Refer to docs for more details', - step: 1, - optional: true, - additionalParams: true - }, { label: 'Number of GPU', name: 'numGpu', @@ -199,17 +197,16 @@ class ChatOllama_ChatModels implements INode { const mirostatEta = nodeData.inputs?.mirostatEta as string const mirostatTau = nodeData.inputs?.mirostatTau as string const numCtx = nodeData.inputs?.numCtx as string - const numGqa = nodeData.inputs?.numGqa as string + const keepAlive = nodeData.inputs?.keepAlive as string const numGpu = nodeData.inputs?.numGpu as string const numThread = nodeData.inputs?.numThread as string const repeatLastN = nodeData.inputs?.repeatLastN as string const repeatPenalty = nodeData.inputs?.repeatPenalty as string - const stop = nodeData.inputs?.stop as string const tfsZ = nodeData.inputs?.tfsZ as string const cache = nodeData.inputs?.cache as BaseCache - const obj: OllamaInput & BaseChatModelParams = { + const obj: ChatOllamaInput & BaseChatModelParams = { baseUrl, temperature: parseFloat(temperature), model: modelName @@ -221,16 +218,12 @@ class ChatOllama_ChatModels implements INode { if (mirostatEta) obj.mirostatEta = parseFloat(mirostatEta) if (mirostatTau) obj.mirostatTau = parseFloat(mirostatTau) if (numCtx) obj.numCtx = parseFloat(numCtx) - if (numGqa) obj.numGqa = parseFloat(numGqa) if (numGpu) obj.numGpu = parseFloat(numGpu) if (numThread) obj.numThread = parseFloat(numThread) if (repeatLastN) obj.repeatLastN = parseFloat(repeatLastN) if (repeatPenalty) obj.repeatPenalty = parseFloat(repeatPenalty) if (tfsZ) obj.tfsZ = parseFloat(tfsZ) - if (stop) { - const stopSequences = stop.split(',') - obj.stop = stopSequences - } + if (keepAlive) obj.keepAlive = keepAlive if (cache) obj.cache = cache const model = new ChatOllama(obj) diff --git a/packages/components/nodes/chatmodels/ChatOllamaFunction/ChatOllamaFunction.ts b/packages/components/nodes/chatmodels/ChatOllamaFunction/ChatOllamaFunction.ts index 97425deec78..1268c624e5e 100644 --- a/packages/components/nodes/chatmodels/ChatOllamaFunction/ChatOllamaFunction.ts +++ b/packages/components/nodes/chatmodels/ChatOllamaFunction/ChatOllamaFunction.ts @@ -43,6 +43,7 @@ class ChatOllamaFunction_ChatModels implements INode { this.type = 'ChatOllamaFunction' this.icon = 'Ollama.svg' this.category = 'Chat Models' + this.badge = 'DEPRECATING' this.description = 'Run open-source function-calling compatible LLM on Ollama' this.baseClasses = [this.type, ...getBaseClasses(OllamaFunctions)] this.inputs = [ diff --git a/packages/components/nodes/documentloaders/Spider/Spider.ts b/packages/components/nodes/documentloaders/Spider/Spider.ts index e4817ac974e..3dbb4baf5ba 100644 --- a/packages/components/nodes/documentloaders/Spider/Spider.ts +++ b/packages/components/nodes/documentloaders/Spider/Spider.ts @@ -1,3 +1,4 @@ +import { omit } from 'lodash' import { TextSplitter } from 'langchain/text_splitter' import { Document, DocumentInterface } from '@langchain/core/documents' import { BaseDocumentLoader } from 'langchain/document_loaders/base' @@ -10,6 +11,7 @@ interface SpiderLoaderParameters { apiKey?: string mode?: 'crawl' | 'scrape' limit?: number + additionalMetadata?: Record params?: Record } @@ -18,11 +20,12 @@ class SpiderLoader extends BaseDocumentLoader { private url: string private mode: 'crawl' | 'scrape' private limit?: number + private additionalMetadata?: Record private params?: Record constructor(loaderParams: SpiderLoaderParameters) { super() - const { apiKey, url, mode = 'crawl', limit, params } = loaderParams + const { apiKey, url, mode = 'crawl', limit, additionalMetadata, params } = loaderParams if (!apiKey) { throw new Error('Spider API key not set. You can set it as SPIDER_API_KEY in your .env file, or pass it to Spider.') } @@ -31,6 +34,7 @@ class SpiderLoader extends BaseDocumentLoader { this.url = url this.mode = mode this.limit = Number(limit) + this.additionalMetadata = additionalMetadata this.params = params } @@ -61,7 +65,10 @@ class SpiderLoader extends BaseDocumentLoader { (doc) => new Document({ pageContent: doc.content || '', - metadata: { source: doc.url } + metadata: { + ...(this.additionalMetadata || {}), + source: doc.url + } }) ) } @@ -125,6 +132,14 @@ class Spider_DocumentLoaders implements INode { type: 'number', default: 25 }, + { + label: 'Additional Metadata', + name: 'additional_metadata', + type: 'json', + description: 'Additional metadata to be added to the extracted documents', + optional: true, + additionalParams: true + }, { label: 'Additional Parameters', name: 'params', @@ -134,6 +149,17 @@ class Spider_DocumentLoaders implements INode { placeholder: '{ "anti_bot": true }', type: 'json', optional: true + }, + { + label: 'Omit Metadata Keys', + name: 'omitMetadataKeys', + type: 'string', + rows: 4, + description: + 'Each document loader comes with a default set of metadata keys that are extracted from the document. You can use this field to omit some of the default metadata keys. The value should be a list of keys, seperated by comma. Use * to omit all metadata keys execept the ones you specify in the Additional Metadata field', + placeholder: 'key1, key2, key3.nestedKey1', + optional: true, + additionalParams: true } ] this.credential = { @@ -149,18 +175,39 @@ class Spider_DocumentLoaders implements INode { const url = nodeData.inputs?.url as string const mode = nodeData.inputs?.mode as 'crawl' | 'scrape' const limit = nodeData.inputs?.limit as number + let additionalMetadata = nodeData.inputs?.additional_metadata let params = nodeData.inputs?.params || {} const credentialData = await getCredentialData(nodeData.credential ?? '', options) const spiderApiKey = getCredentialParam('spiderApiKey', credentialData, nodeData) + const _omitMetadataKeys = nodeData.inputs?.omitMetadataKeys as string + + let omitMetadataKeys: string[] = [] + if (_omitMetadataKeys) { + omitMetadataKeys = _omitMetadataKeys.split(',').map((key) => key.trim()) + } if (typeof params === 'string') { try { params = JSON.parse(params) } catch (e) { - throw new Error('Invalid JSON string provided for params') + console.error('Invalid JSON string provided for params') } } + if (additionalMetadata) { + if (typeof additionalMetadata === 'string') { + try { + additionalMetadata = JSON.parse(additionalMetadata) + } catch (e) { + console.error('Invalid JSON string provided for additional metadata') + } + } else if (typeof additionalMetadata !== 'object') { + console.error('Additional metadata must be a valid JSON object') + } + } else { + additionalMetadata = {} + } + // Ensure return_format is set to markdown params.return_format = 'markdown' @@ -169,6 +216,7 @@ class Spider_DocumentLoaders implements INode { mode: mode as 'crawl' | 'scrape', apiKey: spiderApiKey, limit: limit as number, + additionalMetadata: additionalMetadata as Record, params: params as Record } @@ -182,6 +230,20 @@ class Spider_DocumentLoaders implements INode { docs = await loader.load() } + docs = docs.map((doc: DocumentInterface) => ({ + ...doc, + metadata: + _omitMetadataKeys === '*' + ? additionalMetadata + : omit( + { + ...doc.metadata, + ...additionalMetadata + }, + omitMetadataKeys + ) + })) + return docs } } diff --git a/packages/components/nodes/embeddings/AWSBedrockEmbedding/AWSBedrockEmbedding.ts b/packages/components/nodes/embeddings/AWSBedrockEmbedding/AWSBedrockEmbedding.ts index 5c5f6352b93..4946fa8bbc2 100644 --- a/packages/components/nodes/embeddings/AWSBedrockEmbedding/AWSBedrockEmbedding.ts +++ b/packages/components/nodes/embeddings/AWSBedrockEmbedding/AWSBedrockEmbedding.ts @@ -83,6 +83,24 @@ class AWSBedrockEmbedding_Embeddings implements INode { } ], optional: true + }, + { + label: 'Batch Size', + name: 'batchSize', + description: 'Documents batch size to send to AWS API for Titan model embeddings. Used to avoid throttling.', + type: 'number', + optional: true, + default: 50, + additionalParams: true + }, + { + label: 'Max AWS API retries', + name: 'maxRetries', + description: 'This will limit the nubmer of AWS API for Titan model embeddings call retries. Used to avoid throttling.', + type: 'number', + optional: true, + default: 5, + additionalParams: true } ] } @@ -144,7 +162,9 @@ class AWSBedrockEmbedding_Embeddings implements INode { if (iModel.startsWith('cohere')) { return await embedTextCohere(documents, client, iModel, inputType) } else { - return Promise.all(documents.map((document) => embedTextTitan(document, client, iModel))) + const batchSize = nodeData.inputs?.batchSize as number + const maxRetries = nodeData.inputs?.maxRetries as number + return processInBatches(documents, batchSize, maxRetries, (document) => embedTextTitan(document, client, iModel)) } } return model @@ -195,4 +215,38 @@ const embedTextCohere = async (texts: string[], client: BedrockRuntimeClient, mo } } +const processInBatches = async ( + documents: string[], + batchSize: number, + maxRetries: number, + processFunc: (document: string) => Promise +): Promise => { + let sleepTime = 0 + let retryCounter = 0 + let result: number[][] = [] + for (let i = 0; i < documents.length; i += batchSize) { + let chunk = documents.slice(i, i + batchSize) + try { + let chunkResult = await Promise.all(chunk.map(processFunc)) + result.push(...chunkResult) + retryCounter = 0 + } catch (e) { + if (retryCounter < maxRetries && e.name.includes('ThrottlingException')) { + retryCounter = retryCounter + 1 + i = i - batchSize + sleepTime = sleepTime + 100 + } else { + // Split to distinguish between throttling retry error and other errors in trance + if (e.name.includes('ThrottlingException')) { + throw new Error('AWS Bedrock retry limit reached: ' + e) + } else { + throw new Error(e) + } + } + } + await new Promise((resolve) => setTimeout(resolve, sleepTime)) + } + return result +} + module.exports = { nodeClass: AWSBedrockEmbedding_Embeddings } diff --git a/packages/components/nodes/embeddings/OllamaEmbedding/OllamaEmbedding.ts b/packages/components/nodes/embeddings/OllamaEmbedding/OllamaEmbedding.ts index adb13020488..5b20fe57e5f 100644 --- a/packages/components/nodes/embeddings/OllamaEmbedding/OllamaEmbedding.ts +++ b/packages/components/nodes/embeddings/OllamaEmbedding/OllamaEmbedding.ts @@ -61,6 +61,7 @@ class OllamaEmbedding_Embeddings implements INode { label: 'Use MMap', name: 'useMMap', type: 'boolean', + default: true, optional: true, additionalParams: true } @@ -83,7 +84,9 @@ class OllamaEmbedding_Embeddings implements INode { const requestOptions: OllamaInput = {} if (numThread) requestOptions.numThread = parseFloat(numThread) if (numGpu) requestOptions.numGpu = parseFloat(numGpu) - if (useMMap !== undefined) requestOptions.useMMap = useMMap + + // default useMMap to true + requestOptions.useMMap = useMMap === undefined ? true : useMMap if (Object.keys(requestOptions).length) obj.requestOptions = requestOptions diff --git a/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts b/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts index f4351aaf8aa..98979d9fab4 100644 --- a/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts +++ b/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts @@ -111,6 +111,7 @@ const initializeMongoDB = async (nodeData: INodeData, options: ICommonObject): P sessionId }) + // @ts-ignore mongoDBChatMessageHistory.getMessages = async (): Promise => { const document = await collection.findOne({ sessionId: (mongoDBChatMessageHistory as any).sessionId @@ -119,6 +120,7 @@ const initializeMongoDB = async (nodeData: INodeData, options: ICommonObject): P return messages.map(mapStoredMessageToChatMessage) } + // @ts-ignore mongoDBChatMessageHistory.addMessage = async (message: BaseMessage): Promise => { const messages = [message].map((msg) => msg.toDict()) await collection.updateOne( @@ -136,6 +138,7 @@ const initializeMongoDB = async (nodeData: INodeData, options: ICommonObject): P return new BufferMemoryExtended({ memoryKey: memoryKey ?? 'chat_history', + // @ts-ignore chatHistory: mongoDBChatMessageHistory, sessionId, collection diff --git a/packages/components/nodes/outputparsers/StructuredOutputParser/StructuredOutputParser.ts b/packages/components/nodes/outputparsers/StructuredOutputParser/StructuredOutputParser.ts index 2d12a4f59e6..5162730d84f 100644 --- a/packages/components/nodes/outputparsers/StructuredOutputParser/StructuredOutputParser.ts +++ b/packages/components/nodes/outputparsers/StructuredOutputParser/StructuredOutputParser.ts @@ -71,7 +71,8 @@ class StructuredOutputParser implements INode { const autoFix = nodeData.inputs?.autofixParser as boolean try { - const structuredOutputParser = LangchainStructuredOutputParser.fromZodSchema(z.object(convertSchemaToZod(jsonStructure))) + const zodSchema = z.object(convertSchemaToZod(jsonStructure)) as any + const structuredOutputParser = LangchainStructuredOutputParser.fromZodSchema(zodSchema) // NOTE: When we change Flowise to return a json response, the following has to be changed to: JsonStructuredOutputParser Object.defineProperty(structuredOutputParser, 'autoFix', { diff --git a/packages/components/nodes/recordmanager/MySQLRecordManager/MySQLrecordManager.ts b/packages/components/nodes/recordmanager/MySQLRecordManager/MySQLrecordManager.ts index 2d71727d87f..9e7055208eb 100644 --- a/packages/components/nodes/recordmanager/MySQLRecordManager/MySQLrecordManager.ts +++ b/packages/components/nodes/recordmanager/MySQLRecordManager/MySQLrecordManager.ts @@ -194,7 +194,7 @@ class MySQLRecordManager implements RecordManagerInterface { \`key\` varchar(255) not null, \`namespace\` varchar(255) not null, \`updated_at\` DOUBLE precision not null, - \`group_id\` varchar(36), + \`group_id\` longtext, unique key \`unique_key_namespace\` (\`key\`, \`namespace\`));`) const columns = [`updated_at`, `key`, `namespace`, `group_id`] diff --git a/packages/components/nodes/retrievers/AWSBedrockKBRetriever/AWSBedrockKBRetriever.svg b/packages/components/nodes/retrievers/AWSBedrockKBRetriever/AWSBedrockKBRetriever.svg new file mode 100644 index 00000000000..d783497e8e6 --- /dev/null +++ b/packages/components/nodes/retrievers/AWSBedrockKBRetriever/AWSBedrockKBRetriever.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/retrievers/AWSBedrockKBRetriever/AWSBedrockKBRetriever.ts b/packages/components/nodes/retrievers/AWSBedrockKBRetriever/AWSBedrockKBRetriever.ts new file mode 100644 index 00000000000..70dc664aa7f --- /dev/null +++ b/packages/components/nodes/retrievers/AWSBedrockKBRetriever/AWSBedrockKBRetriever.ts @@ -0,0 +1,143 @@ +import { AmazonKnowledgeBaseRetriever } from '@langchain/aws' +import { ICommonObject, INode, INodeData, INodeParams, INodeOptionsValue } from '../../../src/Interface' +import { getCredentialData, getCredentialParam } from '../../../src/utils' +import { RetrievalFilter } from '@aws-sdk/client-bedrock-agent-runtime' +import { MODEL_TYPE, getRegions } from '../../../src/modelLoader' + +class AWSBedrockKBRetriever_Retrievers implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + baseClasses: string[] + credential: INodeParams + inputs: INodeParams[] + badge: string + + constructor() { + this.label = 'AWS Bedrock Knowledge Base Retriever' + this.name = 'awsBedrockKBRetriever' + this.version = 1.0 + this.type = 'AWSBedrockKBRetriever' + this.icon = 'AWSBedrockKBRetriever.svg' + this.category = 'Retrievers' + this.badge = 'NEW' + this.description = 'Connect to AWS Bedrock Knowledge Base API and retrieve relevant chunks' + this.baseClasses = [this.type, 'BaseRetriever'] + this.credential = { + label: 'AWS Credential', + name: 'credential', + type: 'credential', + credentialNames: ['awsApi'], + optional: true + } + this.inputs = [ + { + label: 'Region', + name: 'region', + type: 'asyncOptions', + loadMethod: 'listRegions', + default: 'us-east-1' + }, + { + label: 'Knowledge Base ID', + name: 'knoledgeBaseID', + type: 'string' + }, + { + label: 'Query', + name: 'query', + type: 'string', + description: 'Query to retrieve documents from retriever. If not specified, user question will be used', + optional: true, + acceptVariable: true + }, + { + label: 'TopK', + name: 'topK', + type: 'number', + description: 'Number of chunks to retrieve', + optional: true, + additionalParams: true, + default: 5 + }, + { + label: 'SearchType', + name: 'searchType', + type: 'options', + description: + 'Knowledge Base search type. Possible values are HYBRID and SEMANTIC. If not specified, default will be used. Consult AWS documentation for more', + options: [ + { + label: 'HYBRID', + name: 'HYBRID', + description: 'Hybrid seach type' + }, + { + label: 'SEMANTIC', + name: 'SEMANTIC', + description: 'Semantic seach type' + } + ], + optional: true, + additionalParams: true, + default: undefined + }, + { + label: 'Filter', + name: 'filter', + type: 'string', + description: 'Knowledge Base retrieval filter. Read documentation for filter syntax', + optional: true, + additionalParams: true + } + ] + } + + loadMethods = { + // Reuse the AWS Bedrock Embeddings region list as it should be same for all Bedrock functions + async listRegions(): Promise { + return await getRegions(MODEL_TYPE.EMBEDDING, 'AWSBedrockEmbeddings') + } + } + + async init(nodeData: INodeData, input: string, options: ICommonObject): Promise { + const knoledgeBaseID = nodeData.inputs?.knoledgeBaseID as string + const region = nodeData.inputs?.region as string + const topK = nodeData.inputs?.topK as number + const overrideSearchType = (nodeData.inputs?.searchType != '' ? nodeData.inputs?.searchType : undefined) as 'HYBRID' | 'SEMANTIC' + const filter = (nodeData.inputs?.filter != '' ? JSON.parse(nodeData.inputs?.filter) : undefined) as RetrievalFilter + let credentialApiKey = '' + let credentialApiSecret = '' + let credentialApiSession = '' + + const credentialData = await getCredentialData(nodeData.credential ?? '', options) + if (credentialData && Object.keys(credentialData).length !== 0) { + credentialApiKey = getCredentialParam('awsKey', credentialData, nodeData) + credentialApiSecret = getCredentialParam('awsSecret', credentialData, nodeData) + credentialApiSession = getCredentialParam('awsSession', credentialData, nodeData) + } + + const retriever = new AmazonKnowledgeBaseRetriever({ + topK: topK, + knowledgeBaseId: knoledgeBaseID, + region: region, + filter, + overrideSearchType, + clientOptions: { + credentials: { + accessKeyId: credentialApiKey, + secretAccessKey: credentialApiSecret, + sessionToken: credentialApiSession + } + } + }) + + return retriever + } +} + +module.exports = { nodeClass: AWSBedrockKBRetriever_Retrievers } diff --git a/packages/components/nodes/sequentialagents/Agent/Agent.ts b/packages/components/nodes/sequentialagents/Agent/Agent.ts index 79ce8706574..eea8b140138 100644 --- a/packages/components/nodes/sequentialagents/Agent/Agent.ts +++ b/packages/components/nodes/sequentialagents/Agent/Agent.ts @@ -754,6 +754,7 @@ const getReturnOutput = async (nodeData: INodeData, input: string, options: ICom const tabIdentifier = nodeData.inputs?.[`${TAB_IDENTIFIER}_${nodeData.id}`] as string const updateStateMemoryUI = nodeData.inputs?.updateStateMemoryUI as string const updateStateMemoryCode = nodeData.inputs?.updateStateMemoryCode as string + const updateStateMemory = nodeData.inputs?.updateStateMemory as string const selectedTab = tabIdentifier ? tabIdentifier.split(`_${nodeData.id}`)[0] : 'updateStateMemoryUI' const variables = await getVars(appDataSource, databaseEntities, nodeData) @@ -768,6 +769,27 @@ const getReturnOutput = async (nodeData: INodeData, input: string, options: ICom vars: prepareSandboxVars(variables) } + if (updateStateMemory && updateStateMemory !== 'updateStateMemoryUI' && updateStateMemory !== 'updateStateMemoryCode') { + try { + const parsedSchema = typeof updateStateMemory === 'string' ? JSON.parse(updateStateMemory) : updateStateMemory + const obj: ICommonObject = {} + for (const sch of parsedSchema) { + const key = sch.Key + if (!key) throw new Error(`Key is required`) + let value = sch.Value as string + if (value.startsWith('$flow')) { + value = customGet(flow, sch.Value.replace('$flow.', '')) + } else if (value.startsWith('$vars')) { + value = customGet(flow, sch.Value.replace('$', '')) + } + obj[key] = value + } + return obj + } catch (e) { + throw new Error(e) + } + } + if (selectedTab === 'updateStateMemoryUI' && updateStateMemoryUI) { try { const parsedSchema = typeof updateStateMemoryUI === 'string' ? JSON.parse(updateStateMemoryUI) : updateStateMemoryUI diff --git a/packages/components/nodes/sequentialagents/LLMNode/LLMNode.ts b/packages/components/nodes/sequentialagents/LLMNode/LLMNode.ts index a09bf45cc53..143e4cd4299 100644 --- a/packages/components/nodes/sequentialagents/LLMNode/LLMNode.ts +++ b/packages/components/nodes/sequentialagents/LLMNode/LLMNode.ts @@ -557,6 +557,7 @@ const getReturnOutput = async (nodeData: INodeData, input: string, options: ICom const tabIdentifier = nodeData.inputs?.[`${TAB_IDENTIFIER}_${nodeData.id}`] as string const updateStateMemoryUI = nodeData.inputs?.updateStateMemoryUI as string const updateStateMemoryCode = nodeData.inputs?.updateStateMemoryCode as string + const updateStateMemory = nodeData.inputs?.updateStateMemory as string const selectedTab = tabIdentifier ? tabIdentifier.split(`_${nodeData.id}`)[0] : 'updateStateMemoryUI' const variables = await getVars(appDataSource, databaseEntities, nodeData) @@ -571,6 +572,27 @@ const getReturnOutput = async (nodeData: INodeData, input: string, options: ICom vars: prepareSandboxVars(variables) } + if (updateStateMemory && updateStateMemory !== 'updateStateMemoryUI' && updateStateMemory !== 'updateStateMemoryCode') { + try { + const parsedSchema = typeof updateStateMemory === 'string' ? JSON.parse(updateStateMemory) : updateStateMemory + const obj: ICommonObject = {} + for (const sch of parsedSchema) { + const key = sch.Key + if (!key) throw new Error(`Key is required`) + let value = sch.Value as string + if (value.startsWith('$flow')) { + value = customGet(flow, sch.Value.replace('$flow.', '')) + } else if (value.startsWith('$vars')) { + value = customGet(flow, sch.Value.replace('$', '')) + } + obj[key] = value + } + return obj + } catch (e) { + throw new Error(e) + } + } + if (selectedTab === 'updateStateMemoryUI' && updateStateMemoryUI) { try { const parsedSchema = typeof updateStateMemoryUI === 'string' ? JSON.parse(updateStateMemoryUI) : updateStateMemoryUI diff --git a/packages/components/nodes/sequentialagents/State/State.ts b/packages/components/nodes/sequentialagents/State/State.ts index 71d9ea3d4ff..06331186eb8 100644 --- a/packages/components/nodes/sequentialagents/State/State.ts +++ b/packages/components/nodes/sequentialagents/State/State.ts @@ -101,6 +101,43 @@ class State_SeqAgents implements INode { const appDataSource = options.appDataSource as DataSource const databaseEntities = options.databaseEntities as IDatabaseEntity const selectedTab = tabIdentifier ? tabIdentifier.split(`_${nodeData.id}`)[0] : 'stateMemoryUI' + const stateMemory = nodeData.inputs?.stateMemory as string + + if (stateMemory && stateMemory !== 'stateMemoryUI' && stateMemory !== 'stateMemoryCode') { + try { + const parsedSchema = typeof stateMemory === 'string' ? JSON.parse(stateMemory) : stateMemory + const obj: ICommonObject = {} + for (const sch of parsedSchema) { + const key = sch.Key + if (!key) throw new Error(`Key is required`) + const type = sch.Operation + const defaultValue = sch['Default Value'] + + if (type === 'Append') { + obj[key] = { + value: (x: any, y: any) => (Array.isArray(y) ? x.concat(y) : x.concat([y])), + default: () => (defaultValue ? JSON.parse(defaultValue) : []) + } + } else { + obj[key] = { + value: (x: any, y: any) => y ?? x, + default: () => defaultValue + } + } + } + const returnOutput: ISeqAgentNode = { + id: nodeData.id, + node: obj, + name: 'state', + label: 'state', + type: 'state', + output: START + } + return returnOutput + } catch (e) { + throw new Error(e) + } + } if (!stateMemoryUI && !stateMemoryCode) { const returnOutput: ISeqAgentNode = { diff --git a/packages/components/nodes/sequentialagents/ToolNode/ToolNode.ts b/packages/components/nodes/sequentialagents/ToolNode/ToolNode.ts index 607b152b472..c498d2fb6da 100644 --- a/packages/components/nodes/sequentialagents/ToolNode/ToolNode.ts +++ b/packages/components/nodes/sequentialagents/ToolNode/ToolNode.ts @@ -441,6 +441,7 @@ const getReturnOutput = async ( const tabIdentifier = nodeData.inputs?.[`${TAB_IDENTIFIER}_${nodeData.id}`] as string const updateStateMemoryUI = nodeData.inputs?.updateStateMemoryUI as string const updateStateMemoryCode = nodeData.inputs?.updateStateMemoryCode as string + const updateStateMemory = nodeData.inputs?.updateStateMemory as string const selectedTab = tabIdentifier ? tabIdentifier.split(`_${nodeData.id}`)[0] : 'updateStateMemoryUI' const variables = await getVars(appDataSource, databaseEntities, nodeData) @@ -464,6 +465,27 @@ const getReturnOutput = async ( vars: prepareSandboxVars(variables) } + if (updateStateMemory && updateStateMemory !== 'updateStateMemoryUI' && updateStateMemory !== 'updateStateMemoryCode') { + try { + const parsedSchema = typeof updateStateMemory === 'string' ? JSON.parse(updateStateMemory) : updateStateMemory + const obj: ICommonObject = {} + for (const sch of parsedSchema) { + const key = sch.Key + if (!key) throw new Error(`Key is required`) + let value = sch.Value as string + if (value.startsWith('$flow')) { + value = customGet(flow, sch.Value.replace('$flow.', '')) + } else if (value.startsWith('$vars')) { + value = customGet(flow, sch.Value.replace('$', '')) + } + obj[key] = value + } + return obj + } catch (e) { + throw new Error(e) + } + } + if (selectedTab === 'updateStateMemoryUI' && updateStateMemoryUI) { try { const parsedSchema = typeof updateStateMemoryUI === 'string' ? JSON.parse(updateStateMemoryUI) : updateStateMemoryUI diff --git a/packages/components/nodes/sequentialagents/commonUtils.ts b/packages/components/nodes/sequentialagents/commonUtils.ts index 431b34d0260..b68de0bc429 100644 --- a/packages/components/nodes/sequentialagents/commonUtils.ts +++ b/packages/components/nodes/sequentialagents/commonUtils.ts @@ -13,7 +13,8 @@ import { ICommonObject, IDatabaseEntity, INodeData, ISeqAgentsState, IVisionChat import { availableDependencies, defaultAllowBuiltInDep, getVars, prepareSandboxVars } from '../../src/utils' export const checkCondition = (input: string | number | undefined, condition: string, value: string | number = ''): boolean => { - if (!input) return false + if (!input && condition === 'Is Empty') return true + else if (!input) return false // Function to check if a string is a valid number const isNumericString = (str: string): boolean => /^-?\d*\.?\d+$/.test(str) diff --git a/packages/components/nodes/tools/ChatflowTool/ChatflowTool.ts b/packages/components/nodes/tools/ChatflowTool/ChatflowTool.ts index 850674bc1c6..0a8d5516804 100644 --- a/packages/components/nodes/tools/ChatflowTool/ChatflowTool.ts +++ b/packages/components/nodes/tools/ChatflowTool/ChatflowTool.ts @@ -174,7 +174,7 @@ class ChatflowTool extends StructuredTool { schema = z.object({ input: z.string().describe('input question') - }) + }) as any constructor({ name, diff --git a/packages/components/nodes/tools/CustomTool/core.ts b/packages/components/nodes/tools/CustomTool/core.ts index d7953883858..3e032579c96 100644 --- a/packages/components/nodes/tools/CustomTool/core.ts +++ b/packages/components/nodes/tools/CustomTool/core.ts @@ -42,6 +42,7 @@ export class DynamicStructuredTool< func: DynamicStructuredToolInput['func'] + // @ts-ignore schema: T private variables: any[] private flowObj: any diff --git a/packages/components/nodes/tools/ReadFile/ReadFile.ts b/packages/components/nodes/tools/ReadFile/ReadFile.ts index 68c0d38a25e..6fa4f72ac79 100644 --- a/packages/components/nodes/tools/ReadFile/ReadFile.ts +++ b/packages/components/nodes/tools/ReadFile/ReadFile.ts @@ -63,7 +63,7 @@ export class ReadFileTool extends StructuredTool { schema = z.object({ file_path: z.string().describe('name of file') - }) + }) as any name = 'read_file' diff --git a/packages/components/nodes/tools/RetrieverTool/RetrieverTool.ts b/packages/components/nodes/tools/RetrieverTool/RetrieverTool.ts index 3654228772a..1e5f76d12ce 100644 --- a/packages/components/nodes/tools/RetrieverTool/RetrieverTool.ts +++ b/packages/components/nodes/tools/RetrieverTool/RetrieverTool.ts @@ -77,7 +77,7 @@ class Retriever_Tools implements INode { const schema = z.object({ input: z.string().describe('input to look up in retriever') - }) + }) as any const tool = new DynamicStructuredTool({ ...input, func, schema }) return tool diff --git a/packages/components/nodes/tools/Searxng/SearXNG.svg b/packages/components/nodes/tools/Searxng/SearXNG.svg index 915d2783b4c..417e7edff58 100644 --- a/packages/components/nodes/tools/Searxng/SearXNG.svg +++ b/packages/components/nodes/tools/Searxng/SearXNG.svg @@ -1,19 +1,19 @@ - - - - - - - image/svg+xml - - - - - - - - - - - + + + + + + + image/svg+xml + + + + + + + + + + + \ No newline at end of file diff --git a/packages/components/nodes/tools/WriteFile/WriteFile.ts b/packages/components/nodes/tools/WriteFile/WriteFile.ts index 22d58e42f47..bcb372f86e8 100644 --- a/packages/components/nodes/tools/WriteFile/WriteFile.ts +++ b/packages/components/nodes/tools/WriteFile/WriteFile.ts @@ -64,7 +64,7 @@ export class WriteFileTool extends StructuredTool { schema = z.object({ file_path: z.string().describe('name of file'), text: z.string().describe('text to write to file') - }) + }) as any name = 'write_file' diff --git a/packages/components/package.json b/packages/components/package.json index 11ab150072b..2814e3362d2 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "flowise-components", - "version": "2.0.0", + "version": "2.0.1", "description": "Flowiseai Components", "main": "dist/src/index", "types": "dist/src/index.d.ts", @@ -37,7 +37,6 @@ "@langchain/anthropic": "^0.2.1", "@langchain/cohere": "^0.0.7", "@langchain/community": "^0.2.17", - "@langchain/core": "^0.2.14", "@langchain/exa": "^0.0.5", "@langchain/google-genai": "^0.0.22", "@langchain/google-vertexai": "^0.0.19", @@ -45,6 +44,7 @@ "@langchain/langgraph": "^0.0.22", "@langchain/mistralai": "^0.0.26", "@langchain/mongodb": "^0.0.1", + "@langchain/ollama": "^0.0.2", "@langchain/openai": "^0.0.30", "@langchain/pinecone": "^0.0.3", "@langchain/qdrant": "^0.0.5", @@ -59,7 +59,7 @@ "@types/js-yaml": "^4.0.5", "@types/jsdom": "^21.1.1", "@upstash/redis": "1.22.1", - "@upstash/vector": "1.0.7", + "@upstash/vector": "1.1.5", "@zilliz/milvus2-sdk-node": "^2.2.24", "apify-client": "^2.7.1", "assemblyai": "^4.2.2", @@ -82,7 +82,7 @@ "ioredis": "^5.3.2", "jsdom": "^22.1.0", "jsonpointer": "^5.0.1", - "langchain": "^0.2.8", + "langchain": "^0.2.11", "langfuse": "3.3.4", "langfuse-langchain": "^3.3.4", "langsmith": "0.1.6", diff --git a/packages/components/src/Interface.ts b/packages/components/src/Interface.ts index 5b4c4d23d9a..baa3d3b0978 100644 --- a/packages/components/src/Interface.ts +++ b/packages/components/src/Interface.ts @@ -117,6 +117,7 @@ export interface INodeProperties { badge?: string deprecateMessage?: string hideOutput?: boolean + author?: string } export interface INode extends INodeProperties { @@ -153,6 +154,8 @@ export interface INodeCredential { export interface IMessage { message: string type: MessageType + role?: MessageType + content?: string } export interface IUsedTool { diff --git a/packages/components/src/agents.ts b/packages/components/src/agents.ts index c09e03c66f4..f464a6742eb 100644 --- a/packages/components/src/agents.ts +++ b/packages/components/src/agents.ts @@ -339,7 +339,7 @@ export class AgentExecutor extends BaseChain { } async _call(inputs: ChainValues, runManager?: CallbackManagerForChainRun): Promise { - const toolsByName = Object.fromEntries(this.tools.map((t) => [t.name.toLowerCase(), t])) + const toolsByName = Object.fromEntries(this.tools.map((t) => [t.name?.toLowerCase(), t])) const steps: AgentStep[] = [] let iterations = 0 @@ -608,7 +608,7 @@ export class AgentExecutor extends BaseChain { async _getToolReturn(nextStepOutput: AgentStep): Promise { const { action, observation } = nextStepOutput - const nameToolMap = Object.fromEntries(this.tools.map((t) => [t.name.toLowerCase(), t])) + const nameToolMap = Object.fromEntries(this.tools.map((t) => [t.name?.toLowerCase(), t])) const [returnValueKey = 'output'] = this.agent.returnValues // Invalid tools won't be in the map, so we return False. if (action.tool in nameToolMap) { diff --git a/packages/components/src/storageUtils.ts b/packages/components/src/storageUtils.ts index 483eb9ae9b0..8363ebf10f1 100644 --- a/packages/components/src/storageUtils.ts +++ b/packages/components/src/storageUtils.ts @@ -318,6 +318,8 @@ export const getS3Config = () => { const secretAccessKey = process.env.S3_STORAGE_SECRET_ACCESS_KEY const region = process.env.S3_STORAGE_REGION const Bucket = process.env.S3_STORAGE_BUCKET_NAME + const customURL = process.env.S3_ENDPOINT_URL + if (!region || !Bucket) { throw new Error('S3 storage configuration is missing') } @@ -332,7 +334,8 @@ export const getS3Config = () => { const s3Client = new S3Client({ credentials, - region + region, + endpoint: customURL }) return { s3Client, Bucket } } diff --git a/packages/components/src/utils.ts b/packages/components/src/utils.ts index 6f5b7c5c744..57ac493c814 100644 --- a/packages/components/src/utils.ts +++ b/packages/components/src/utils.ts @@ -587,10 +587,10 @@ export const mapChatMessageToBaseMessage = (chatmessages: any[] = []): BaseMessa const chatHistory = [] for (const message of chatmessages) { - if (message.role === 'apiMessage') { - chatHistory.push(new AIMessage(message.content)) - } else if (message.role === 'userMessage') { - chatHistory.push(new HumanMessage(message.content)) + if (message.role === 'apiMessage' || message.type === 'apiMessage') { + chatHistory.push(new AIMessage(message.content || '')) + } else if (message.role === 'userMessage' || message.role === 'userMessage') { + chatHistory.push(new HumanMessage(message.content || '')) } } return chatHistory diff --git a/packages/server/.env.example b/packages/server/.env.example index a8550532154..c6e034c96ed 100644 --- a/packages/server/.env.example +++ b/packages/server/.env.example @@ -46,4 +46,8 @@ PORT=3000 # S3_STORAGE_BUCKET_NAME=flowise # S3_STORAGE_ACCESS_KEY_ID= # S3_STORAGE_SECRET_ACCESS_KEY= -# S3_STORAGE_REGION=us-west-2 \ No newline at end of file +# S3_STORAGE_REGION=us-west-2 +# S3_ENDPOINT_URL= + +# APIKEY_STORAGE_TYPE=json (json | db) +# SHOW_COMMUNITY_NODES=true diff --git a/packages/server/marketplaces/tools/Perplexity AI Search.json b/packages/server/marketplaces/tools/Perplexity AI Search.json index 5984f5fb505..db10f19a9c5 100644 --- a/packages/server/marketplaces/tools/Perplexity AI Search.json +++ b/packages/server/marketplaces/tools/Perplexity AI Search.json @@ -4,5 +4,5 @@ "color": "linear-gradient(rgb(155,190,84), rgb(176,69,245))", "iconSrc": "https://raw.githubusercontent.com/AsharibAli/project-images/main/perplexity-ai-icon.svg", "schema": "[{\"id\":1,\"property\":\"query\",\"description\":\"Query for research\",\"type\":\"string\",\"required\":true}]", - "func": "const fetch = require('node-fetch');\nconst apiKey = 'YOUR_PERPLEXITY_API_KEY'; // Put Your Perplexity AI API key here\n\nconst query = $query;\n\nconst options = {\n\tmethod: 'POST',\n\theaders: {\n\t\t'Content-Type': 'application/json',\n\t\t'Authorization': 'Bearer '\n\t},\n\tbody: JSON.stringify({\n\t\tmodel: 'llama-3-sonar-small-32k-online', // Model\n\t\tmessages: [\n\t\t\t{\n\t\t\t\trole: 'system',\n\t\t\t\tcontent: 'You are a research assistant.'\n\t\t\t},\n\t\t\t{\n\t\t\t\trole: 'user',\n\t\t\t\tcontent: query\n\t\t\t}\n\t\t]\n\t})\n};\n\ntry {\n\tconst response = await fetch('https://api.perplexity.ai/chat/completions', options);\n\tconst data = await response.json();\n\treturn JSON.stringify(data);\n} catch (error) {\n\tconsole.error(error);\n\treturn 'Error occurred while fetching data from Perplexity AI';\n}\n\n// For more details: https://docs.perplexity.ai/docs/getting-started" + "func": "const fetch = require('node-fetch');\nconst apiKey = ''; // Put your Perplexity API key here.\n\nconst query = $query;\n\nconst options = {\n\tmethod: 'POST',\n\theaders: {\n\t\t'Content-Type': 'application/json',\n\t\t'Authorization': `Bearer ${apiKey}`\n\t},\n\tbody: JSON.stringify({\n\t\tmodel: 'llama-3.1-sonar-small-128k-online', // Model\n\t\tmessages: [\n\t\t\t{\n\t\t\t\trole: 'system',\n\t\t\t\tcontent: 'You are a market research assistant.'\n\t\t\t},\n\t\t\t{\n\t\t\t\trole: 'user',\n\t\t\t\tcontent: query\n\t\t\t}\n\t\t]\n\t})\n};\n\ntry {\n\tconsole.log(`Sending request with query: ${query}`);\n\tconst response = await fetch('https://api.perplexity.ai/chat/completions', options);\n\tconst data = await response.json();\n\n\tif (!response.ok) {\n\t\tthrow new Error(`API error: ${response.status} ${response.statusText} - ${JSON.stringify(data)}`);\n\t}\n\n\tconsole.log('API response:', data);\n\treturn JSON.stringify(data);\n} catch (error) {\n\tconsole.error('Error occurred while fetching data from Perplexity AI:', error);\n\treturn `Error occurred while fetching data from Perplexity AI: ${error.message}`;\n}" } diff --git a/packages/server/package.json b/packages/server/package.json index f0cb2c2ed63..01341096ecd 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "flowise", - "version": "2.0.0", + "version": "2.0.1", "description": "Flowiseai Server", "main": "dist/index", "types": "dist/index.d.ts", diff --git a/packages/server/src/AppConfig.ts b/packages/server/src/AppConfig.ts new file mode 100644 index 00000000000..fa3919aaec0 --- /dev/null +++ b/packages/server/src/AppConfig.ts @@ -0,0 +1,7 @@ +export const appConfig = { + apiKeys: { + storageType: process.env.APIKEY_STORAGE_TYPE ? process.env.APIKEY_STORAGE_TYPE.toLowerCase() : 'json' + }, + showCommunityNodes: process.env.SHOW_COMMUNITY_NODES ? process.env.SHOW_COMMUNITY_NODES.toLowerCase() === 'true' : false + // todo: add more config options here like database, log, storage, credential and allow modification from UI +} diff --git a/packages/server/src/Interface.ts b/packages/server/src/Interface.ts index e8fb84a8256..803d044df2f 100644 --- a/packages/server/src/Interface.ts +++ b/packages/server/src/Interface.ts @@ -209,6 +209,8 @@ export interface IDepthQueue { export interface IMessage { message: string type: MessageType + role?: MessageType + content?: string } export interface IncomingInput { @@ -263,5 +265,13 @@ export interface IUploadFileSizeAndTypes { maxUploadSize: number } +export interface IApiKey { + id: string + keyName: string + apiKey: string + apiSecret: string + updatedDate: Date +} + // DocumentStore related export * from './Interface.DocumentStore' diff --git a/packages/server/src/NodesPool.ts b/packages/server/src/NodesPool.ts index 82c97f2a5ba..16604c34598 100644 --- a/packages/server/src/NodesPool.ts +++ b/packages/server/src/NodesPool.ts @@ -5,6 +5,7 @@ import { getNodeModulesPackagePath } from './utils' import { promises } from 'fs' import { ICommonObject } from 'flowise-components' import logger from './utils/logger' +import { appConfig } from './AppConfig' export class NodesPool { componentNodes: IComponentNodes = {} @@ -57,7 +58,14 @@ export class NodesPool { } const skipCategories = ['Analytic', 'SpeechToText'] - if (!skipCategories.includes(newNodeInstance.category)) { + const conditionOne = !skipCategories.includes(newNodeInstance.category) + + const isCommunityNodesAllowed = appConfig.showCommunityNodes + const isAuthorPresent = newNodeInstance.author + let conditionTwo = true + if (!isCommunityNodesAllowed && isAuthorPresent) conditionTwo = false + + if (conditionOne && conditionTwo) { this.componentNodes[newNodeInstance.name] = newNodeInstance } } diff --git a/packages/server/src/commands/start.ts b/packages/server/src/commands/start.ts index d50133e432b..a31c37c8ef2 100644 --- a/packages/server/src/commands/start.ts +++ b/packages/server/src/commands/start.ts @@ -24,6 +24,7 @@ export default class Start extends Command { IFRAME_ORIGINS: Flags.string(), DEBUG: Flags.string(), BLOB_STORAGE_PATH: Flags.string(), + APIKEY_STORAGE_TYPE: Flags.string(), APIKEY_PATH: Flags.string(), SECRETKEY_PATH: Flags.string(), FLOWISE_SECRETKEY_OVERWRITE: Flags.string(), @@ -52,7 +53,9 @@ export default class Start extends Command { S3_STORAGE_BUCKET_NAME: Flags.string(), S3_STORAGE_ACCESS_KEY_ID: Flags.string(), S3_STORAGE_SECRET_ACCESS_KEY: Flags.string(), - S3_STORAGE_REGION: Flags.string() + S3_STORAGE_REGION: Flags.string(), + S3_ENDPOINT_URL: Flags.string(), + SHOW_COMMUNITY_NODES: Flags.string() } async stopProcess() { @@ -95,10 +98,12 @@ export default class Start extends Command { if (flags.DEBUG) process.env.DEBUG = flags.DEBUG if (flags.NUMBER_OF_PROXIES) process.env.NUMBER_OF_PROXIES = flags.NUMBER_OF_PROXIES if (flags.DISABLE_CHATFLOW_REUSE) process.env.DISABLE_CHATFLOW_REUSE = flags.DISABLE_CHATFLOW_REUSE + if (flags.SHOW_COMMUNITY_NODES) process.env.SHOW_COMMUNITY_NODES = flags.SHOW_COMMUNITY_NODES // Authorization if (flags.FLOWISE_USERNAME) process.env.FLOWISE_USERNAME = flags.FLOWISE_USERNAME if (flags.FLOWISE_PASSWORD) process.env.FLOWISE_PASSWORD = flags.FLOWISE_PASSWORD + if (flags.APIKEY_STORAGE_TYPE) process.env.APIKEY_STORAGE_TYPE = flags.APIKEY_STORAGE_TYPE if (flags.APIKEY_PATH) process.env.APIKEY_PATH = flags.APIKEY_PATH // API Configuration @@ -146,6 +151,7 @@ export default class Start extends Command { if (flags.S3_STORAGE_ACCESS_KEY_ID) process.env.S3_STORAGE_ACCESS_KEY_ID = flags.S3_STORAGE_ACCESS_KEY_ID if (flags.S3_STORAGE_SECRET_ACCESS_KEY) process.env.S3_STORAGE_SECRET_ACCESS_KEY = flags.S3_STORAGE_SECRET_ACCESS_KEY if (flags.S3_STORAGE_REGION) process.env.S3_STORAGE_REGION = flags.S3_STORAGE_REGION + if (flags.S3_ENDPOINT_URL) process.env.S3_ENDPOINT_URL = flags.S3_ENDPOINT_URL await (async () => { try { diff --git a/packages/server/src/controllers/apikey/index.ts b/packages/server/src/controllers/apikey/index.ts index a2448e14537..40452b71999 100644 --- a/packages/server/src/controllers/apikey/index.ts +++ b/packages/server/src/controllers/apikey/index.ts @@ -41,6 +41,19 @@ const updateApiKey = async (req: Request, res: Response, next: NextFunction) => } } +// Import Keys from JSON file +const importKeys = async (req: Request, res: Response, next: NextFunction) => { + try { + if (typeof req.body === 'undefined' || !req.body.jsonFile) { + throw new InternalFlowiseError(StatusCodes.PRECONDITION_FAILED, `Error: apikeyController.importKeys - body not provided!`) + } + const apiResponse = await apikeyService.importKeys(req.body) + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + // Delete api key const deleteApiKey = async (req: Request, res: Response, next: NextFunction) => { try { @@ -72,5 +85,6 @@ export default { deleteApiKey, getAllApiKeys, updateApiKey, - verifyApiKey + verifyApiKey, + importKeys } diff --git a/packages/server/src/controllers/chat-messages/index.ts b/packages/server/src/controllers/chat-messages/index.ts index 8ed67399210..6e9bebd8691 100644 --- a/packages/server/src/controllers/chat-messages/index.ts +++ b/packages/server/src/controllers/chat-messages/index.ts @@ -1,5 +1,5 @@ import { Request, Response, NextFunction } from 'express' -import { chatType, IReactFlowObject } from '../../Interface' +import { ChatMessageRatingType, chatType, IReactFlowObject } from '../../Interface' import chatflowsService from '../../services/chatflows' import chatMessagesService from '../../services/chat-messages' import { clearSessionMemory } from '../../utils' @@ -49,6 +49,26 @@ const getAllChatMessages = async (req: Request, res: Response, next: NextFunctio const startDate = req.query?.startDate as string | undefined const endDate = req.query?.endDate as string | undefined const feedback = req.query?.feedback as boolean | undefined + let feedbackTypeFilters = req.query?.feedbackType as ChatMessageRatingType[] | undefined + if (feedbackTypeFilters) { + try { + const feedbackTypeFilterArray = JSON.parse(JSON.stringify(feedbackTypeFilters)) + if ( + feedbackTypeFilterArray.includes(ChatMessageRatingType.THUMBS_UP) && + feedbackTypeFilterArray.includes(ChatMessageRatingType.THUMBS_DOWN) + ) { + feedbackTypeFilters = [ChatMessageRatingType.THUMBS_UP, ChatMessageRatingType.THUMBS_DOWN] + } else if (feedbackTypeFilterArray.includes(ChatMessageRatingType.THUMBS_UP)) { + feedbackTypeFilters = [ChatMessageRatingType.THUMBS_UP] + } else if (feedbackTypeFilterArray.includes(ChatMessageRatingType.THUMBS_DOWN)) { + feedbackTypeFilters = [ChatMessageRatingType.THUMBS_DOWN] + } else { + feedbackTypeFilters = undefined + } + } catch (e) { + return res.status(500).send(e) + } + } if (typeof req.params === 'undefined' || !req.params.id) { throw new InternalFlowiseError( StatusCodes.PRECONDITION_FAILED, @@ -65,7 +85,8 @@ const getAllChatMessages = async (req: Request, res: Response, next: NextFunctio startDate, endDate, messageId, - feedback + feedback, + feedbackTypeFilters ) return res.json(apiResponse) } catch (error) { diff --git a/packages/server/src/controllers/chatflows/index.ts b/packages/server/src/controllers/chatflows/index.ts index 61ba0691d70..523a6572739 100644 --- a/packages/server/src/controllers/chatflows/index.ts +++ b/packages/server/src/controllers/chatflows/index.ts @@ -1,11 +1,11 @@ import { NextFunction, Request, Response } from 'express' import { StatusCodes } from 'http-status-codes' +import apiKeyService from '../../services/apikey' import { ChatFlow } from '../../database/entities/ChatFlow' +import { createRateLimiter } from '../../utils/rateLimit' import { InternalFlowiseError } from '../../errors/internalFlowiseError' import { ChatflowType } from '../../Interface' import chatflowsService from '../../services/chatflows' -import { getApiKey } from '../../utils/apiKey' -import { createRateLimiter } from '../../utils/rateLimit' const checkIfChatflowIsValidForStreaming = async (req: Request, res: Response, next: NextFunction) => { try { @@ -67,7 +67,7 @@ const getChatflowByApiKey = async (req: Request, res: Response, next: NextFuncti `Error: chatflowsRouter.getChatflowByApiKey - apikey not provided!` ) } - const apikey = await getApiKey(req.params.apikey) + const apikey = await apiKeyService.getApiKey(req.params.apikey) if (!apikey) { return res.status(401).send('Unauthorized') } diff --git a/packages/server/src/controllers/stats/index.ts b/packages/server/src/controllers/stats/index.ts index e4dd4dad270..a96464514f5 100644 --- a/packages/server/src/controllers/stats/index.ts +++ b/packages/server/src/controllers/stats/index.ts @@ -1,7 +1,7 @@ import { StatusCodes } from 'http-status-codes' import { Request, Response, NextFunction } from 'express' import statsService from '../../services/stats' -import { chatType } from '../../Interface' +import { ChatMessageRatingType, chatType } from '../../Interface' import { InternalFlowiseError } from '../../errors/internalFlowiseError' import { getErrorMessage } from '../../errors/utils' @@ -14,6 +14,7 @@ const getChatflowStats = async (req: Request, res: Response, next: NextFunction) let chatTypeFilter = req.query?.chatType as chatType | undefined const startDate = req.query?.startDate as string | undefined const endDate = req.query?.endDate as string | undefined + let feedbackTypeFilters = req.query?.feedbackType as ChatMessageRatingType[] | undefined if (chatTypeFilter) { try { const chatTypeFilterArray = JSON.parse(chatTypeFilter) @@ -31,7 +32,34 @@ const getChatflowStats = async (req: Request, res: Response, next: NextFunction) ) } } - const apiResponse = await statsService.getChatflowStats(chatflowid, chatTypeFilter, startDate, endDate, '', true) + if (feedbackTypeFilters) { + try { + const feedbackTypeFilterArray = JSON.parse(JSON.stringify(feedbackTypeFilters)) + if ( + feedbackTypeFilterArray.includes(ChatMessageRatingType.THUMBS_UP) && + feedbackTypeFilterArray.includes(ChatMessageRatingType.THUMBS_DOWN) + ) { + feedbackTypeFilters = [ChatMessageRatingType.THUMBS_UP, ChatMessageRatingType.THUMBS_DOWN] + } else if (feedbackTypeFilterArray.includes(ChatMessageRatingType.THUMBS_UP)) { + feedbackTypeFilters = [ChatMessageRatingType.THUMBS_UP] + } else if (feedbackTypeFilterArray.includes(ChatMessageRatingType.THUMBS_DOWN)) { + feedbackTypeFilters = [ChatMessageRatingType.THUMBS_DOWN] + } else { + feedbackTypeFilters = undefined + } + } catch (e) { + return res.status(500).send(e) + } + } + const apiResponse = await statsService.getChatflowStats( + chatflowid, + chatTypeFilter, + startDate, + endDate, + '', + true, + feedbackTypeFilters + ) return res.json(apiResponse) } catch (error) { next(error) diff --git a/packages/server/src/database/entities/ApiKey.ts b/packages/server/src/database/entities/ApiKey.ts new file mode 100644 index 00000000000..d96610df20a --- /dev/null +++ b/packages/server/src/database/entities/ApiKey.ts @@ -0,0 +1,21 @@ +import { Column, Entity, PrimaryColumn, UpdateDateColumn } from 'typeorm' +import { IApiKey } from '../../Interface' + +@Entity('apikey') +export class ApiKey implements IApiKey { + @PrimaryColumn({ type: 'varchar', length: 20 }) + id: string + + @Column({ type: 'text' }) + apiKey: string + + @Column({ type: 'text' }) + apiSecret: string + + @Column({ type: 'text' }) + keyName: string + + @Column({ type: 'timestamp' }) + @UpdateDateColumn() + updatedDate: Date +} diff --git a/packages/server/src/database/entities/index.ts b/packages/server/src/database/entities/index.ts index 738c25d782f..0e715e68b5b 100644 --- a/packages/server/src/database/entities/index.ts +++ b/packages/server/src/database/entities/index.ts @@ -9,6 +9,7 @@ import { DocumentStore } from './DocumentStore' import { DocumentStoreFileChunk } from './DocumentStoreFileChunk' import { Lead } from './Lead' import { UpsertHistory } from './UpsertHistory' +import { ApiKey } from './ApiKey' export const entities = { ChatFlow, @@ -21,5 +22,6 @@ export const entities = { DocumentStore, DocumentStoreFileChunk, Lead, - UpsertHistory + UpsertHistory, + ApiKey } diff --git a/packages/server/src/database/migrations/mariadb/1720230151480-AddApiKey.ts b/packages/server/src/database/migrations/mariadb/1720230151480-AddApiKey.ts new file mode 100644 index 00000000000..5f2a923090b --- /dev/null +++ b/packages/server/src/database/migrations/mariadb/1720230151480-AddApiKey.ts @@ -0,0 +1,20 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddApiKey1720230151480 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE IF NOT EXISTS \`apikey\` ( + \`id\` varchar(36) NOT NULL, + \`apiKey\` varchar(255) NOT NULL, + \`apiSecret\` varchar(255) NOT NULL, + \`keyName\` varchar(255) NOT NULL, + \`updatedDate\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + PRIMARY KEY (\`id\`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE apikey`) + } +} diff --git a/packages/server/src/database/migrations/mariadb/1722301395521-LongTextColumn.ts b/packages/server/src/database/migrations/mariadb/1722301395521-LongTextColumn.ts new file mode 100644 index 00000000000..43b3ff88472 --- /dev/null +++ b/packages/server/src/database/migrations/mariadb/1722301395521-LongTextColumn.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class LongTextColumn1722301395521 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`chat_flow\` MODIFY \`flowData\` LONGTEXT;`) + await queryRunner.query(`ALTER TABLE \`chat_message\` MODIFY \`content\` LONGTEXT;`) + await queryRunner.query(`ALTER TABLE \`chat_message\` MODIFY \`usedTools\` LONGTEXT;`) + await queryRunner.query(`ALTER TABLE \`document_store\` MODIFY \`loaders\` LONGTEXT;`) + await queryRunner.query(`ALTER TABLE \`upsert_history\` MODIFY \`flowData\` LONGTEXT;`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`chat_flow\` MODIFY \`flowData\` TEXT;`) + await queryRunner.query(`ALTER TABLE \`chat_message\` MODIFY \`content\` TEXT;`) + await queryRunner.query(`ALTER TABLE \`chat_message\` MODIFY \`usedTools\` TEXT;`) + await queryRunner.query(`ALTER TABLE \`document_store\` MODIFY \`loaders\` TEXT;`) + await queryRunner.query(`ALTER TABLE \`upsert_history\` MODIFY \`flowData\` TEXT;`) + } +} diff --git a/packages/server/src/database/migrations/mariadb/index.ts b/packages/server/src/database/migrations/mariadb/index.ts index c231a94dcfd..e2dbd8c226f 100644 --- a/packages/server/src/database/migrations/mariadb/index.ts +++ b/packages/server/src/database/migrations/mariadb/index.ts @@ -20,7 +20,9 @@ import { AddLead1710832127079 } from './1710832127079-AddLead' import { AddLeadToChatMessage1711538023578 } from './1711538023578-AddLeadToChatMessage' import { AddAgentReasoningToChatMessage1714679514451 } from './1714679514451-AddAgentReasoningToChatMessage' import { AddTypeToChatFlow1766759476232 } from './1766759476232-AddTypeToChatFlow' +import { AddApiKey1720230151480 } from './1720230151480-AddApiKey' import { AddActionToChatMessage1721078251523 } from './1721078251523-AddActionToChatMessage' +import { LongTextColumn1722301395521 } from './1722301395521-LongTextColumn' export const mariadbMigrations = [ Init1693840429259, @@ -45,5 +47,7 @@ export const mariadbMigrations = [ AddLeadToChatMessage1711538023578, AddAgentReasoningToChatMessage1714679514451, AddTypeToChatFlow1766759476232, - AddActionToChatMessage1721078251523 + AddApiKey1720230151480, + AddActionToChatMessage1721078251523, + LongTextColumn1722301395521 ] diff --git a/packages/server/src/database/migrations/mysql/1720230151480-AddApiKey.ts b/packages/server/src/database/migrations/mysql/1720230151480-AddApiKey.ts new file mode 100644 index 00000000000..a264607c55f --- /dev/null +++ b/packages/server/src/database/migrations/mysql/1720230151480-AddApiKey.ts @@ -0,0 +1,20 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddApiKey1720230151480 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE IF NOT EXISTS \`apikey\` ( + \`id\` varchar(36) NOT NULL, + \`apiKey\` varchar(255) NOT NULL, + \`apiSecret\` varchar(255) NOT NULL, + \`keyName\` varchar(255) NOT NULL, + \`updatedDate\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + PRIMARY KEY (\`id\`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE apikey`) + } +} diff --git a/packages/server/src/database/migrations/mysql/1722301395521-LongTextColumn.ts b/packages/server/src/database/migrations/mysql/1722301395521-LongTextColumn.ts new file mode 100644 index 00000000000..43b3ff88472 --- /dev/null +++ b/packages/server/src/database/migrations/mysql/1722301395521-LongTextColumn.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class LongTextColumn1722301395521 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`chat_flow\` MODIFY \`flowData\` LONGTEXT;`) + await queryRunner.query(`ALTER TABLE \`chat_message\` MODIFY \`content\` LONGTEXT;`) + await queryRunner.query(`ALTER TABLE \`chat_message\` MODIFY \`usedTools\` LONGTEXT;`) + await queryRunner.query(`ALTER TABLE \`document_store\` MODIFY \`loaders\` LONGTEXT;`) + await queryRunner.query(`ALTER TABLE \`upsert_history\` MODIFY \`flowData\` LONGTEXT;`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`chat_flow\` MODIFY \`flowData\` TEXT;`) + await queryRunner.query(`ALTER TABLE \`chat_message\` MODIFY \`content\` TEXT;`) + await queryRunner.query(`ALTER TABLE \`chat_message\` MODIFY \`usedTools\` TEXT;`) + await queryRunner.query(`ALTER TABLE \`document_store\` MODIFY \`loaders\` TEXT;`) + await queryRunner.query(`ALTER TABLE \`upsert_history\` MODIFY \`flowData\` TEXT;`) + } +} diff --git a/packages/server/src/database/migrations/mysql/index.ts b/packages/server/src/database/migrations/mysql/index.ts index ebd68336606..8578d4b61bd 100644 --- a/packages/server/src/database/migrations/mysql/index.ts +++ b/packages/server/src/database/migrations/mysql/index.ts @@ -20,7 +20,9 @@ import { AddLead1710832127079 } from './1710832127079-AddLead' import { AddLeadToChatMessage1711538023578 } from './1711538023578-AddLeadToChatMessage' import { AddAgentReasoningToChatMessage1714679514451 } from './1714679514451-AddAgentReasoningToChatMessage' import { AddTypeToChatFlow1766759476232 } from './1766759476232-AddTypeToChatFlow' +import { AddApiKey1720230151480 } from './1720230151480-AddApiKey' import { AddActionToChatMessage1721078251523 } from './1721078251523-AddActionToChatMessage' +import { LongTextColumn1722301395521 } from './1722301395521-LongTextColumn' export const mysqlMigrations = [ Init1693840429259, @@ -45,5 +47,7 @@ export const mysqlMigrations = [ AddLeadToChatMessage1711538023578, AddAgentReasoningToChatMessage1714679514451, AddTypeToChatFlow1766759476232, - AddActionToChatMessage1721078251523 + AddApiKey1720230151480, + AddActionToChatMessage1721078251523, + LongTextColumn1722301395521 ] diff --git a/packages/server/src/database/migrations/postgres/1720230151480-AddApiKey.ts b/packages/server/src/database/migrations/postgres/1720230151480-AddApiKey.ts new file mode 100644 index 00000000000..a32f37564d3 --- /dev/null +++ b/packages/server/src/database/migrations/postgres/1720230151480-AddApiKey.ts @@ -0,0 +1,20 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddApiKey1720230151480 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE IF NOT EXISTS apikey ( + id uuid NOT NULL DEFAULT uuid_generate_v4(), + "apiKey" varchar NOT NULL, + "apiSecret" varchar NOT NULL, + "keyName" varchar NOT NULL, + "updatedDate" timestamp NOT NULL DEFAULT now(), + CONSTRAINT "PK_96109043dd704f53-9830ab78f0" PRIMARY KEY (id) + );` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE apikey`) + } +} diff --git a/packages/server/src/database/migrations/postgres/index.ts b/packages/server/src/database/migrations/postgres/index.ts index 10836deca10..5334b4fac42 100644 --- a/packages/server/src/database/migrations/postgres/index.ts +++ b/packages/server/src/database/migrations/postgres/index.ts @@ -21,6 +21,7 @@ import { AddLead1710832137905 } from './1710832137905-AddLead' import { AddLeadToChatMessage1711538016098 } from './1711538016098-AddLeadToChatMessage' import { AddAgentReasoningToChatMessage1714679514451 } from './1714679514451-AddAgentReasoningToChatMessage' import { AddTypeToChatFlow1766759476232 } from './1766759476232-AddTypeToChatFlow' +import { AddApiKey1720230151480 } from './1720230151480-AddApiKey' import { AddActionToChatMessage1721078251523 } from './1721078251523-AddActionToChatMessage' export const postgresMigrations = [ @@ -47,5 +48,6 @@ export const postgresMigrations = [ AddLeadToChatMessage1711538016098, AddAgentReasoningToChatMessage1714679514451, AddTypeToChatFlow1766759476232, + AddApiKey1720230151480, AddActionToChatMessage1721078251523 ] diff --git a/packages/server/src/database/migrations/sqlite/1720230151480-AddApiKey.ts b/packages/server/src/database/migrations/sqlite/1720230151480-AddApiKey.ts new file mode 100644 index 00000000000..8d836a9ab08 --- /dev/null +++ b/packages/server/src/database/migrations/sqlite/1720230151480-AddApiKey.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddApiKey1720230151480 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE IF NOT EXISTS "apikey" ("id" varchar PRIMARY KEY NOT NULL, + "apiKey" varchar NOT NULL, + "apiSecret" varchar NOT NULL, + "keyName" varchar NOT NULL, + "updatedDate" datetime NOT NULL DEFAULT (datetime('now')));` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE IF EXISTS "apikey";`) + } +} diff --git a/packages/server/src/database/migrations/sqlite/index.ts b/packages/server/src/database/migrations/sqlite/index.ts index cdcd02d8151..44a602c4266 100644 --- a/packages/server/src/database/migrations/sqlite/index.ts +++ b/packages/server/src/database/migrations/sqlite/index.ts @@ -21,6 +21,7 @@ import { AddLeadToChatMessage1711537986113 } from './1711537986113-AddLeadToChat import { AddAgentReasoningToChatMessage1714679514451 } from './1714679514451-AddAgentReasoningToChatMessage' import { AddTypeToChatFlow1766759476232 } from './1766759476232-AddTypeToChatFlow' import { AddActionToChatMessage1721078251523 } from './1721078251523-AddActionToChatMessage' +import { AddApiKey1720230151480 } from './1720230151480-AddApiKey' export const sqliteMigrations = [ Init1693835579790, @@ -45,5 +46,6 @@ export const sqliteMigrations = [ AddLeadToChatMessage1711537986113, AddAgentReasoningToChatMessage1714679514451, AddTypeToChatFlow1766759476232, + AddApiKey1720230151480, AddActionToChatMessage1721078251523 ] diff --git a/packages/server/src/routes/apikey/index.ts b/packages/server/src/routes/apikey/index.ts index 566d12e8bf1..dbc043dd59e 100644 --- a/packages/server/src/routes/apikey/index.ts +++ b/packages/server/src/routes/apikey/index.ts @@ -4,6 +4,7 @@ const router = express.Router() // CREATE router.post('/', apikeyController.createApiKey) +router.post('/import', apikeyController.importKeys) // READ router.get('/', apikeyController.getAllApiKeys) diff --git a/packages/server/src/services/apikey/index.ts b/packages/server/src/services/apikey/index.ts index dd519612ee7..84ebac0a7dc 100644 --- a/packages/server/src/services/apikey/index.ts +++ b/packages/server/src/services/apikey/index.ts @@ -1,25 +1,94 @@ import { StatusCodes } from 'http-status-codes' -import { addAPIKey, deleteAPIKey, getAPIKeys, updateAPIKey } from '../../utils/apiKey' +import { + addAPIKey as addAPIKey_json, + deleteAPIKey as deleteAPIKey_json, + generateAPIKey, + generateSecretHash, + getApiKey as getApiKey_json, + getAPIKeys as getAPIKeys_json, + updateAPIKey as updateAPIKey_json, + replaceAllAPIKeys as replaceAllAPIKeys_json, + importKeys as importKeys_json +} from '../../utils/apiKey' import { addChatflowsCount } from '../../utils/addChatflowsCount' -import { getApiKey } from '../../utils/apiKey' import { InternalFlowiseError } from '../../errors/internalFlowiseError' import { getErrorMessage } from '../../errors/utils' +import { getRunningExpressApp } from '../../utils/getRunningExpressApp' +import { ApiKey } from '../../database/entities/ApiKey' +import { appConfig } from '../../AppConfig' +import { randomBytes } from 'crypto' +import { Not, IsNull } from 'typeorm' + +const _apikeysStoredInJson = (): boolean => { + return appConfig.apiKeys.storageType === 'json' +} + +const _apikeysStoredInDb = (): boolean => { + return appConfig.apiKeys.storageType === 'db' +} const getAllApiKeys = async () => { try { - const keys = await getAPIKeys() - const dbResponse = await addChatflowsCount(keys) - return dbResponse + if (_apikeysStoredInJson()) { + const keys = await getAPIKeys_json() + return await addChatflowsCount(keys) + } else if (_apikeysStoredInDb()) { + const appServer = getRunningExpressApp() + let keys = await appServer.AppDataSource.getRepository(ApiKey).find() + if (keys.length === 0) { + await createApiKey('DefaultKey') + keys = await appServer.AppDataSource.getRepository(ApiKey).find() + } + return await addChatflowsCount(keys) + } else { + throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `UNKNOWN APIKEY_STORAGE_TYPE`) + } } catch (error) { throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `Error: apikeyService.getAllApiKeys - ${getErrorMessage(error)}`) } } +const getApiKey = async (keyName: string) => { + try { + if (_apikeysStoredInJson()) { + return getApiKey_json(keyName) + } else if (_apikeysStoredInDb()) { + const appServer = getRunningExpressApp() + const currentKey = await appServer.AppDataSource.getRepository(ApiKey).findOneBy({ + keyName: keyName + }) + if (!currentKey) { + return undefined + } + return currentKey + } else { + throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `UNKNOWN APIKEY_STORAGE_TYPE`) + } + } catch (error) { + throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `Error: apikeyService.createApiKey - ${getErrorMessage(error)}`) + } +} + const createApiKey = async (keyName: string) => { try { - const keys = await addAPIKey(keyName) - const dbResponse = await addChatflowsCount(keys) - return dbResponse + if (_apikeysStoredInJson()) { + const keys = await addAPIKey_json(keyName) + return await addChatflowsCount(keys) + } else if (_apikeysStoredInDb()) { + const apiKey = generateAPIKey() + const apiSecret = generateSecretHash(apiKey) + const appServer = getRunningExpressApp() + const newKey = new ApiKey() + newKey.id = randomBytes(16).toString('hex') + newKey.apiKey = apiKey + newKey.apiSecret = apiSecret + newKey.keyName = keyName + const key = appServer.AppDataSource.getRepository(ApiKey).create(newKey) + await appServer.AppDataSource.getRepository(ApiKey).save(key) + return getAllApiKeys() + } else { + throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `UNKNOWN APIKEY_STORAGE_TYPE`) + } } catch (error) { throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `Error: apikeyService.createApiKey - ${getErrorMessage(error)}`) } @@ -28,9 +97,23 @@ const createApiKey = async (keyName: string) => { // Update api key const updateApiKey = async (id: string, keyName: string) => { try { - const keys = await updateAPIKey(id, keyName) - const dbResponse = await addChatflowsCount(keys) - return dbResponse + if (_apikeysStoredInJson()) { + const keys = await updateAPIKey_json(id, keyName) + return await addChatflowsCount(keys) + } else if (_apikeysStoredInDb()) { + const appServer = getRunningExpressApp() + const currentKey = await appServer.AppDataSource.getRepository(ApiKey).findOneBy({ + id: id + }) + if (!currentKey) { + throw new InternalFlowiseError(StatusCodes.NOT_FOUND, `ApiKey ${currentKey} not found`) + } + currentKey.keyName = keyName + await appServer.AppDataSource.getRepository(ApiKey).save(currentKey) + return getAllApiKeys() + } else { + throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `UNKNOWN APIKEY_STORAGE_TYPE`) + } } catch (error) { throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `Error: apikeyService.updateApiKey - ${getErrorMessage(error)}`) } @@ -38,22 +121,123 @@ const updateApiKey = async (id: string, keyName: string) => { const deleteApiKey = async (id: string) => { try { - const keys = await deleteAPIKey(id) - const dbResponse = await addChatflowsCount(keys) - return dbResponse + if (_apikeysStoredInJson()) { + const keys = await deleteAPIKey_json(id) + return await addChatflowsCount(keys) + } else if (_apikeysStoredInDb()) { + const appServer = getRunningExpressApp() + const dbResponse = await appServer.AppDataSource.getRepository(ApiKey).delete({ id: id }) + if (!dbResponse) { + throw new InternalFlowiseError(StatusCodes.NOT_FOUND, `ApiKey ${id} not found`) + } + return getAllApiKeys() + } else { + throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `UNKNOWN APIKEY_STORAGE_TYPE`) + } } catch (error) { throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `Error: apikeyService.deleteApiKey - ${getErrorMessage(error)}`) } } +const importKeys = async (body: any) => { + try { + const jsonFile = body.jsonFile + const splitDataURI = jsonFile.split(',') + if (splitDataURI[0] !== 'data:application/json;base64') { + throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `Invalid dataURI`) + } + const bf = Buffer.from(splitDataURI[1] || '', 'base64') + const plain = bf.toString('utf8') + const keys = JSON.parse(plain) + if (_apikeysStoredInJson()) { + if (body.importMode === 'replaceAll') { + await replaceAllAPIKeys_json(keys) + } else { + await importKeys_json(keys, body.importMode) + } + return await addChatflowsCount(keys) + } else if (_apikeysStoredInDb()) { + const appServer = getRunningExpressApp() + const allApiKeys = await appServer.AppDataSource.getRepository(ApiKey).find() + if (body.importMode === 'replaceAll') { + await appServer.AppDataSource.getRepository(ApiKey).delete({ + id: Not(IsNull()) + }) + } + if (body.importMode === 'errorIfExist') { + // if importMode is errorIfExist, check for existing keys and raise error before any modification to the DB + for (const key of keys) { + const keyNameExists = allApiKeys.find((k) => k.keyName === key.keyName) + if (keyNameExists) { + throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `Key with name ${key.keyName} already exists`) + } + } + } + // iterate through the keys and add them to the database + for (const key of keys) { + const keyNameExists = allApiKeys.find((k) => k.keyName === key.keyName) + if (keyNameExists) { + const keyIndex = allApiKeys.findIndex((k) => k.keyName === key.keyName) + switch (body.importMode) { + case 'overwriteIfExist': { + const currentKey = allApiKeys[keyIndex] + currentKey.id = key.id + currentKey.apiKey = key.apiKey + currentKey.apiSecret = key.apiSecret + await appServer.AppDataSource.getRepository(ApiKey).save(currentKey) + break + } + case 'ignoreIfExist': { + // ignore this key and continue + continue + } + case 'errorIfExist': { + // should not reach here as we have already checked for existing keys + throw new Error(`Key with name ${key.keyName} already exists`) + } + default: { + throw new Error(`Unknown overwrite option ${body.importMode}`) + } + } + } else { + const newKey = new ApiKey() + newKey.id = key.id + newKey.apiKey = key.apiKey + newKey.apiSecret = key.apiSecret + newKey.keyName = key.keyName + const newKeyEntity = appServer.AppDataSource.getRepository(ApiKey).create(newKey) + await appServer.AppDataSource.getRepository(ApiKey).save(newKeyEntity) + } + } + return getAllApiKeys() + } else { + throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `UNKNOWN APIKEY_STORAGE_TYPE`) + } + } catch (error) { + throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `Error: apikeyService.importKeys - ${getErrorMessage(error)}`) + } +} + const verifyApiKey = async (paramApiKey: string): Promise => { try { - const apiKey = await getApiKey(paramApiKey) - if (!apiKey) { - throw new InternalFlowiseError(StatusCodes.UNAUTHORIZED, `Unauthorized`) + if (_apikeysStoredInJson()) { + const apiKey = await getApiKey_json(paramApiKey) + if (!apiKey) { + throw new InternalFlowiseError(StatusCodes.UNAUTHORIZED, `Unauthorized`) + } + return 'OK' + } else if (_apikeysStoredInDb()) { + const appServer = getRunningExpressApp() + const apiKey = await appServer.AppDataSource.getRepository(ApiKey).findOneBy({ + apiKey: paramApiKey + }) + if (!apiKey) { + throw new InternalFlowiseError(StatusCodes.UNAUTHORIZED, `Unauthorized`) + } + return 'OK' + } else { + throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `UNKNOWN APIKEY_STORAGE_TYPE`) } - const dbResponse = 'OK' - return dbResponse } catch (error) { if (error instanceof InternalFlowiseError && error.statusCode === StatusCodes.UNAUTHORIZED) { throw error @@ -71,5 +255,7 @@ export default { deleteApiKey, getAllApiKeys, updateApiKey, - verifyApiKey + verifyApiKey, + getApiKey, + importKeys } diff --git a/packages/server/src/services/chat-messages/index.ts b/packages/server/src/services/chat-messages/index.ts index 39d0606c7c8..9f2d53f7e03 100644 --- a/packages/server/src/services/chat-messages/index.ts +++ b/packages/server/src/services/chat-messages/index.ts @@ -1,6 +1,6 @@ import { DeleteResult, FindOptionsWhere } from 'typeorm' import { StatusCodes } from 'http-status-codes' -import { chatType, IChatMessage } from '../../Interface' +import { ChatMessageRatingType, chatType, IChatMessage } from '../../Interface' import { utilGetChatMessage } from '../../utils/getChatMessage' import { utilAddChatMessage } from '../../utils/addChatMesage' import { getRunningExpressApp } from '../../utils/getRunningExpressApp' @@ -35,7 +35,8 @@ const getAllChatMessages = async ( startDate?: string, endDate?: string, messageId?: string, - feedback?: boolean + feedback?: boolean, + feedbackTypes?: ChatMessageRatingType[] ): Promise => { try { const dbResponse = await utilGetChatMessage( @@ -48,7 +49,8 @@ const getAllChatMessages = async ( startDate, endDate, messageId, - feedback + feedback, + feedbackTypes ) return dbResponse } catch (error) { @@ -70,7 +72,8 @@ const getAllInternalChatMessages = async ( startDate?: string, endDate?: string, messageId?: string, - feedback?: boolean + feedback?: boolean, + feedbackTypes?: ChatMessageRatingType[] ): Promise => { try { const dbResponse = await utilGetChatMessage( @@ -83,7 +86,8 @@ const getAllInternalChatMessages = async ( startDate, endDate, messageId, - feedback + feedback, + feedbackTypes ) return dbResponse } catch (error) { diff --git a/packages/server/src/services/stats/index.ts b/packages/server/src/services/stats/index.ts index 2f6faff23fc..8d9b99da524 100644 --- a/packages/server/src/services/stats/index.ts +++ b/packages/server/src/services/stats/index.ts @@ -1,5 +1,5 @@ import { StatusCodes } from 'http-status-codes' -import { chatType } from '../../Interface' +import { ChatMessageRatingType, chatType } from '../../Interface' import { ChatMessage } from '../../database/entities/ChatMessage' import { utilGetChatMessage } from '../../utils/getChatMessage' import { ChatMessageFeedback } from '../../database/entities/ChatMessageFeedback' @@ -13,7 +13,8 @@ const getChatflowStats = async ( startDate?: string, endDate?: string, messageId?: string, - feedback?: boolean + feedback?: boolean, + feedbackTypes?: ChatMessageRatingType[] ): Promise => { try { const chatmessages = (await utilGetChatMessage( @@ -26,7 +27,8 @@ const getChatflowStats = async ( startDate, endDate, messageId, - feedback + feedback, + feedbackTypes )) as Array const totalMessages = chatmessages.length const totalFeedback = chatmessages.filter((message) => message?.feedback).length diff --git a/packages/server/src/utils/apiKey.ts b/packages/server/src/utils/apiKey.ts index 57239825bdb..a50b2b54a85 100644 --- a/packages/server/src/utils/apiKey.ts +++ b/packages/server/src/utils/apiKey.ts @@ -4,6 +4,7 @@ import moment from 'moment' import fs from 'fs' import path from 'path' import logger from './logger' +import { appConfig } from '../AppConfig' /** * Returns the api key path @@ -50,6 +51,9 @@ export const compareKeys = (storedKey: string, suppliedKey: string): boolean => * @returns {Promise} */ export const getAPIKeys = async (): Promise => { + if (appConfig.apiKeys.storageType !== 'json') { + return [] + } try { const content = await fs.promises.readFile(getAPIKeyPath(), 'utf8') return JSON.parse(content) @@ -94,6 +98,47 @@ export const addAPIKey = async (keyName: string): Promise => { return content } +/** + * import API keys + * @param {[]} keys + * @returns {Promise} + */ +export const importKeys = async (keys: any[], importMode: string): Promise => { + const allApiKeys = await getAPIKeys() + // if importMode is errorIfExist, check for existing keys and raise error before any modification to the file + if (importMode === 'errorIfExist') { + for (const key of keys) { + const keyNameExists = allApiKeys.find((k) => k.keyName === key.keyName) + if (keyNameExists) { + throw new Error(`Key with name ${key.keyName} already exists`) + } + } + } + for (const key of keys) { + // Check if keyName already exists, if overwrite is false, raise an error else overwrite the key + const keyNameExists = allApiKeys.find((k) => k.keyName === key.keyName) + if (keyNameExists) { + const keyIndex = allApiKeys.findIndex((k) => k.keyName === key.keyName) + switch (importMode) { + case 'overwriteIfExist': + allApiKeys[keyIndex] = key + continue + case 'ignoreIfExist': + // ignore this key and continue + continue + case 'errorIfExist': + // should not reach here as we have already checked for existing keys + throw new Error(`Key with name ${key.keyName} already exists`) + default: + throw new Error(`Unknown overwrite option ${importMode}`) + } + } + allApiKeys.push(key) + } + await fs.promises.writeFile(getAPIKeyPath(), JSON.stringify(allApiKeys), 'utf8') + return allApiKeys +} + /** * Get API Key details * @param {string} apiKey diff --git a/packages/server/src/utils/buildAgentGraph.ts b/packages/server/src/utils/buildAgentGraph.ts index e6a61a4cbf6..c4ef963ca67 100644 --- a/packages/server/src/utils/buildAgentGraph.ts +++ b/packages/server/src/utils/buildAgentGraph.ts @@ -181,6 +181,7 @@ export const buildAgentGraph = async ( options, startingNodeIds, incomingInput.question, + incomingInput.history, chatHistory, incomingInput?.overrideConfig, sessionId || chatId, @@ -196,6 +197,7 @@ export const buildAgentGraph = async ( appServer.nodesPool.componentNodes, options, incomingInput.question, + incomingInput.history, chatHistory, incomingInput?.overrideConfig, sessionId || chatId, @@ -447,6 +449,7 @@ const compileMultiAgentsGraph = async ( options: ICommonObject, startingNodeIds: string[], question: string, + prependHistoryMessages: IMessage[] = [], chatHistory: IMessage[] = [], overrideConfig?: ICommonObject, threadId?: string, @@ -570,10 +573,22 @@ const compileMultiAgentsGraph = async ( const callbacks = await additionalCallbacks(flowNodeData, options) const config = { configurable: { thread_id: threadId } } + let prependMessages = [] + // Only append in the first message + if (prependHistoryMessages.length === chatHistory.length) { + for (const message of prependHistoryMessages) { + if (message.role === 'apiMessage' || message.type === 'apiMessage') { + prependMessages.push(new AIMessage({ content: message.message || message.content || '' })) + } else if (message.role === 'userMessage' || message.type === 'userMessage') { + prependMessages.push(new HumanMessage({ content: message.message || message.content || '' })) + } + } + } + // Return stream result as we should only have 1 supervisor return await graph.stream( { - messages: [new HumanMessage({ content: question })] + messages: [...prependMessages, new HumanMessage({ content: question })] }, { recursionLimit: supervisorResult?.recursionLimit ?? 100, callbacks: [loggerHandler, ...callbacks], configurable: config } ) @@ -605,6 +620,7 @@ const compileSeqAgentsGraph = async ( componentNodes: IComponentNodes, options: ICommonObject, question: string, + prependHistoryMessages: IMessage[] = [], chatHistory: IMessage[] = [], overrideConfig?: ICommonObject, threadId?: string, @@ -952,8 +968,20 @@ const compileSeqAgentsGraph = async ( const callbacks = await additionalCallbacks(flowNodeData as any, options) const config = { configurable: { thread_id: threadId }, bindModel } + let prependMessages = [] + // Only append in the first message + if (prependHistoryMessages.length === chatHistory.length) { + for (const message of prependHistoryMessages) { + if (message.role === 'apiMessage' || message.type === 'apiMessage') { + prependMessages.push(new AIMessage({ content: message.message || message.content || '' })) + } else if (message.role === 'userMessage' || message.type === 'userMessage') { + prependMessages.push(new HumanMessage({ content: message.message || message.content || '' })) + } + } + } + let humanMsg: { messages: HumanMessage[] | ToolMessage[] } | null = { - messages: [new HumanMessage({ content: question })] + messages: [...prependMessages, new HumanMessage({ content: question })] } if (action && action.mapping && question === action.mapping.approve) { diff --git a/packages/server/src/utils/getChatMessage.ts b/packages/server/src/utils/getChatMessage.ts index 29335cbf416..459d62f73a7 100644 --- a/packages/server/src/utils/getChatMessage.ts +++ b/packages/server/src/utils/getChatMessage.ts @@ -1,8 +1,9 @@ import { MoreThanOrEqual, LessThanOrEqual } from 'typeorm' -import { chatType } from '../Interface' +import { ChatMessageRatingType, chatType } from '../Interface' import { ChatMessage } from '../database/entities/ChatMessage' import { ChatMessageFeedback } from '../database/entities/ChatMessageFeedback' import { getRunningExpressApp } from '../utils/getRunningExpressApp' + /** * Method that get chat messages. * @param {string} chatflowid @@ -14,6 +15,7 @@ import { getRunningExpressApp } from '../utils/getRunningExpressApp' * @param {string} startDate * @param {string} endDate * @param {boolean} feedback + * @param {ChatMessageRatingType[]} feedbackTypes */ export const utilGetChatMessage = async ( chatflowid: string, @@ -25,7 +27,8 @@ export const utilGetChatMessage = async ( startDate?: string, endDate?: string, messageId?: string, - feedback?: boolean + feedback?: boolean, + feedbackTypes?: ChatMessageRatingType[] ): Promise => { const appServer = getRunningExpressApp() const setDateToStartOrEndOfDay = (dateTimeStr: string, setHours: 'start' | 'end') => { @@ -79,7 +82,23 @@ export const utilGetChatMessage = async ( // sort query.orderBy('chat_message.createdDate', sortOrder === 'DESC' ? 'DESC' : 'ASC') - const messages = await query.getMany() + const messages = (await query.getMany()) as Array + + if (feedbackTypes && feedbackTypes.length > 0) { + // just applying a filter to the messages array will only return the messages that have feedback, + // but we also want the message before the feedback message which is the user message. + const indicesToKeep = new Set() + + messages.forEach((message, index) => { + if (message.role === 'apiMessage' && message.feedback && feedbackTypes.includes(message.feedback.rating)) { + if (index > 0) indicesToKeep.add(index - 1) + indicesToKeep.add(index) + } + }) + + return messages.filter((_, index) => indicesToKeep.has(index)) + } + return messages } diff --git a/packages/server/src/utils/validateKey.ts b/packages/server/src/utils/validateKey.ts index 02d36cf28da..3c4f272ca83 100644 --- a/packages/server/src/utils/validateKey.ts +++ b/packages/server/src/utils/validateKey.ts @@ -1,7 +1,7 @@ import { Request } from 'express' import { ChatFlow } from '../database/entities/ChatFlow' -import { getAPIKeys, compareKeys } from './apiKey' - +import { compareKeys } from './apiKey' +import apikeyService from '../services/apikey' /** * Validate API Key * @param {Request} req @@ -17,8 +17,8 @@ export const utilValidateKey = async (req: Request, chatflow: ChatFlow) => { const suppliedKey = authorizationHeader.split(`Bearer `).pop() if (suppliedKey) { - const keys = await getAPIKeys() - const apiSecret = keys.find((key) => key.id === chatFlowApiKeyId)?.apiSecret + const keys = await apikeyService.getAllApiKeys() + const apiSecret = keys.find((key: any) => key.id === chatFlowApiKeyId)?.apiSecret if (!compareKeys(apiSecret, suppliedKey)) return false return true } diff --git a/packages/ui/package.json b/packages/ui/package.json index 96e9aa43a88..b7a461d0c81 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "flowise-ui", - "version": "2.0.0", + "version": "2.0.1", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://flowiseai.com", "author": { diff --git a/packages/ui/src/api/apikey.js b/packages/ui/src/api/apikey.js index aed0a2d5f23..ca554d574cb 100644 --- a/packages/ui/src/api/apikey.js +++ b/packages/ui/src/api/apikey.js @@ -8,9 +8,12 @@ const updateAPI = (id, body) => client.put(`/apikey/${id}`, body) const deleteAPI = (id) => client.delete(`/apikey/${id}`) +const importAPI = (body) => client.post(`/apikey/import`, body) + export default { getAllAPIKeys, createNewAPI, updateAPI, - deleteAPI + deleteAPI, + importAPI } diff --git a/packages/ui/src/ui-component/dialog/ViewMessagesDialog.jsx b/packages/ui/src/ui-component/dialog/ViewMessagesDialog.jsx index e12d79e5c57..25246ddd01b 100644 --- a/packages/ui/src/ui-component/dialog/ViewMessagesDialog.jsx +++ b/packages/ui/src/ui-component/dialog/ViewMessagesDialog.jsx @@ -103,6 +103,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { const [sourceDialogOpen, setSourceDialogOpen] = useState(false) const [sourceDialogProps, setSourceDialogProps] = useState({}) const [chatTypeFilter, setChatTypeFilter] = useState([]) + const [feedbackTypeFilter, setFeedbackTypeFilter] = useState([]) const [startDate, setStartDate] = useState(new Date().setMonth(new Date().getMonth() - 1)) const [endDate, setEndDate] = useState(new Date()) const [leadEmail, setLeadEmail] = useState('') @@ -155,6 +156,24 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { }) } + const onFeedbackTypeSelected = (feedbackTypes) => { + setFeedbackTypeFilter(feedbackTypes) + + getChatmessageApi.request(dialogProps.chatflow.id, { + chatType: chatTypeFilter.length ? chatTypeFilter : undefined, + feedbackType: feedbackTypes.length ? feedbackTypes : undefined, + startDate: startDate, + endDate: endDate, + order: 'ASC' + }) + getStatsApi.request(dialogProps.chatflow.id, { + chatType: chatTypeFilter.length ? chatTypeFilter : undefined, + feedbackType: feedbackTypes.length ? feedbackTypes : undefined, + startDate: startDate, + endDate: endDate + }) + } + const exportMessages = async () => { if (!storagePath && getStoragePathFromServer.data) { storagePath = getStoragePathFromServer.data.storagePath @@ -382,7 +401,14 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { const handleItemClick = (idx, chatmsg) => { setSelectedMessageIndex(idx) - getChatmessageFromPKApi.request(dialogProps.chatflow.id, transformChatPKToParams(getChatPK(chatmsg))) + if (feedbackTypeFilter.length > 0) { + getChatmessageFromPKApi.request(dialogProps.chatflow.id, { + ...transformChatPKToParams(getChatPK(chatmsg)), + feedbackType: feedbackTypeFilter + }) + } else { + getChatmessageFromPKApi.request(dialogProps.chatflow.id, transformChatPKToParams(getChatPK(chatmsg))) + } } const onURLClick = (data) => { @@ -436,7 +462,16 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { setAllChatLogs(getChatmessageApi.data) const chatPK = processChatLogs(getChatmessageApi.data) setSelectedMessageIndex(0) - if (chatPK) getChatmessageFromPKApi.request(dialogProps.chatflow.id, transformChatPKToParams(chatPK)) + if (chatPK) { + if (feedbackTypeFilter.length > 0) { + getChatmessageFromPKApi.request(dialogProps.chatflow.id, { + ...transformChatPKToParams(chatPK), + feedbackType: feedbackTypeFilter + }) + } else { + getChatmessageFromPKApi.request(dialogProps.chatflow.id, transformChatPKToParams(chatPK)) + } + } } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -459,6 +494,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { setAllChatLogs([]) setChatMessages([]) setChatTypeFilter([]) + setFeedbackTypeFilter([]) setSelectedMessageIndex(0) setSelectedChatId('') setStartDate(new Date().setMonth(new Date().getMonth() - 1)) @@ -476,6 +512,17 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { return () => dispatch({ type: HIDE_CANVAS_DIALOG }) }, [show, dispatch]) + useEffect(() => { + if (dialogProps.chatflow) { + // when the filter is cleared fetch all messages + if (feedbackTypeFilter.length === 0) { + getChatmessageApi.request(dialogProps.chatflow.id) + getStatsApi.request(dialogProps.chatflow.id) + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [feedbackTypeFilter]) + const component = show ? ( { customInput={} /> -
+
Source { formControlSx={{ mt: 0 }} />
+
+ Feedback + onFeedbackTypeSelected(newValue)} + value={feedbackTypeFilter} + formControlSx={{ mt: 0 }} + /> +
({ + borderColor: theme.palette.grey[900] + 25, + + [`&.${tableCellClasses.head}`]: { + color: theme.palette.grey[900] + }, + [`&.${tableCellClasses.body}`]: { + fontSize: 14, + height: 64 + } +})) + +const StyledTableRow = styled(TableRow)(() => ({ + // hide last border + '&:last-child td, &:last-child th': { + border: 0 + } +})) + +export const ToolsTable = ({ data, isLoading, onSelect }) => { + const theme = useTheme() + const customization = useSelector((state) => state.customization) + + return ( + <> + + + + + + Name + + Description + +   + + + + + {isLoading ? ( + <> + + + + + + + + + + + + + + + + + + + + + + + + ) : ( + <> + {data?.map((row, index) => ( + + +
+ + + +
+ + + {row.description || ''} + + + +
+ ))} + + )} +
+
+
+ + ) +} + +ToolsTable.propTypes = { + data: PropTypes.array, + isLoading: PropTypes.bool, + onSelect: PropTypes.func +} diff --git a/packages/ui/src/views/apikey/UploadJSONFileDialog.jsx b/packages/ui/src/views/apikey/UploadJSONFileDialog.jsx new file mode 100644 index 00000000000..4d39d895a43 --- /dev/null +++ b/packages/ui/src/views/apikey/UploadJSONFileDialog.jsx @@ -0,0 +1,186 @@ +import { createPortal } from 'react-dom' +import PropTypes from 'prop-types' +import { useState, useEffect } from 'react' +import { useDispatch } from 'react-redux' +import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from '@/store/actions' + +// Material +import { Button, Dialog, DialogActions, DialogContent, DialogTitle, Box, Typography, Stack } from '@mui/material' + +// Project imports +import { StyledButton } from '@/ui-component/button/StyledButton' +import ConfirmDialog from '@/ui-component/dialog/ConfirmDialog' +import { File } from '@/ui-component/file/File' + +// Icons +import { IconFileUpload, IconX } from '@tabler/icons-react' + +// API +import apikeyAPI from '@/api/apikey' + +// utils +import useNotifier from '@/utils/useNotifier' + +// const +import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from '@/store/actions' +import { Dropdown } from '@/ui-component/dropdown/Dropdown' + +const importModes = [ + { + label: 'Add & Overwrite', + name: 'overwriteIfExist', + description: 'Add keys and overwrite existing keys with the same name' + }, + { + label: 'Add & Ignore', + name: 'ignoreIfExist', + description: 'Add keys and ignore existing keys with the same name' + }, + { + label: 'Add & Verify', + name: 'errorIfExist', + description: 'Add Keys and throw error if key with same name exists' + }, + { + label: 'Replace All', + name: 'replaceAll', + description: 'Replace all keys with the imported keys' + } +] + +const UploadJSONFileDialog = ({ show, dialogProps, onCancel, onConfirm }) => { + const portalElement = document.getElementById('portal') + + const dispatch = useDispatch() + + // ==============================|| Snackbar ||============================== // + + useNotifier() + + const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)) + const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) + + const [selectedFile, setSelectedFile] = useState() + const [importMode, setImportMode] = useState('overwrite') + + useEffect(() => { + return () => { + setSelectedFile() + } + }, [dialogProps]) + + useEffect(() => { + if (show) dispatch({ type: SHOW_CANVAS_DIALOG }) + else dispatch({ type: HIDE_CANVAS_DIALOG }) + return () => dispatch({ type: HIDE_CANVAS_DIALOG }) + }, [show, dispatch]) + + const importKeys = async () => { + try { + const obj = { + importMode: importMode, + jsonFile: selectedFile + } + const createResp = await apikeyAPI.importAPI(obj) + if (createResp.data) { + enqueueSnackbar({ + message: 'Imported keys successfully!', + options: { + key: new Date().getTime() + Math.random(), + variant: 'success', + action: (key) => ( + + ) + } + }) + onConfirm(createResp.data.id) + } + } catch (error) { + enqueueSnackbar({ + message: `Failed to import keys: ${ + typeof error.response.data === 'object' ? error.response.data.message : error.response.data + }`, + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + persist: true, + action: (key) => ( + + ) + } + }) + onCancel() + } + } + + const component = show ? ( + + +
+ + Import API Keys +
+
+ + + + + Import api.json file +  * + + + setSelectedFile(newValue)} + value={selectedFile ?? 'Choose a file to upload'} + /> + + + + + Import Mode +  * + + + setImportMode(newValue)} + value={importMode ?? 'choose an option'} + /> + + + + + + {dialogProps.confirmButtonName} + + + +
+ ) : null + + return createPortal(component, portalElement) +} + +UploadJSONFileDialog.propTypes = { + show: PropTypes.bool, + dialogProps: PropTypes.object, + onCancel: PropTypes.func, + onConfirm: PropTypes.func +} + +export default UploadJSONFileDialog diff --git a/packages/ui/src/views/apikey/index.jsx b/packages/ui/src/views/apikey/index.jsx index d0c3bd5665e..56bc3ecfedd 100644 --- a/packages/ui/src/views/apikey/index.jsx +++ b/packages/ui/src/views/apikey/index.jsx @@ -44,8 +44,20 @@ import useConfirm from '@/hooks/useConfirm' import useNotifier from '@/utils/useNotifier' // Icons -import { IconTrash, IconEdit, IconCopy, IconChevronsUp, IconChevronsDown, IconX, IconPlus, IconEye, IconEyeOff } from '@tabler/icons-react' +import { + IconTrash, + IconEdit, + IconCopy, + IconChevronsUp, + IconChevronsDown, + IconX, + IconPlus, + IconEye, + IconEyeOff, + IconFileUpload +} from '@tabler/icons-react' import APIEmptySVG from '@/assets/images/api_empty.svg' +import UploadJSONFileDialog from '@/views/apikey/UploadJSONFileDialog' // ==============================|| APIKey ||============================== // @@ -200,6 +212,9 @@ const APIKey = () => { const [showApiKeys, setShowApiKeys] = useState([]) const openPopOver = Boolean(anchorEl) + const [showUploadDialog, setShowUploadDialog] = useState(false) + const [uploadDialogProps, setUploadDialogProps] = useState({}) + const [search, setSearch] = useState('') const onSearchChange = (event) => { setSearch(event.target.value) @@ -254,6 +269,17 @@ const APIKey = () => { setShowDialog(true) } + const uploadDialog = () => { + const dialogProp = { + type: 'ADD', + cancelButtonName: 'Cancel', + confirmButtonName: 'Upload', + data: {} + } + setUploadDialogProps(dialogProp) + setShowUploadDialog(true) + } + const deleteKey = async (key) => { const confirmPayload = { title: `Delete`, @@ -308,6 +334,7 @@ const APIKey = () => { const onConfirm = () => { setShowDialog(false) + setShowUploadDialog(false) getAllAPIKeysApi.request() } @@ -341,6 +368,15 @@ const APIKey = () => { ) : ( + { onConfirm={onConfirm} setError={setError} > + {showUploadDialog && ( + setShowUploadDialog(false)} + onConfirm={onConfirm} + > + )} ) diff --git a/packages/ui/src/views/canvas/AddNodes.jsx b/packages/ui/src/views/canvas/AddNodes.jsx index 5d15d98de20..08ba0cdd59d 100644 --- a/packages/ui/src/views/canvas/AddNodes.jsx +++ b/packages/ui/src/views/canvas/AddNodes.jsx @@ -296,6 +296,8 @@ const AddNodes = ({ nodesData, node, isAgentCanvas }) => { Add Nodes { - {node.label} -   - {node.badge && ( - +
+ {node.label} +   + {node.badge && ( + + )} +
+ {node.author && ( + + > + By {node.author} + )} -
+ } secondary={node.description} /> diff --git a/packages/ui/src/views/tools/index.jsx b/packages/ui/src/views/tools/index.jsx index 358f1e487f1..5840935a55c 100644 --- a/packages/ui/src/views/tools/index.jsx +++ b/packages/ui/src/views/tools/index.jsx @@ -1,7 +1,7 @@ import { useEffect, useState, useRef } from 'react' // material-ui -import { Box, Stack, Button, ButtonGroup, Skeleton } from '@mui/material' +import { Box, Stack, Button, ButtonGroup, Skeleton, ToggleButtonGroup, ToggleButton } from '@mui/material' // project imports import MainCard from '@/ui-component/cards/MainCard' @@ -10,6 +10,7 @@ import { gridSpacing } from '@/store/constant' import ToolEmptySVG from '@/assets/images/tools_empty.svg' import { StyledButton } from '@/ui-component/button/StyledButton' import ToolDialog from './ToolDialog' +import { ToolsTable } from '@/ui-component/table/ToolsListTable' // API import toolsApi from '@/api/tools' @@ -18,22 +19,31 @@ import toolsApi from '@/api/tools' import useApi from '@/hooks/useApi' // icons -import { IconPlus, IconFileUpload } from '@tabler/icons-react' +import { IconPlus, IconFileUpload, IconLayoutGrid, IconList } from '@tabler/icons-react' import ViewHeader from '@/layout/MainLayout/ViewHeader' import ErrorBoundary from '@/ErrorBoundary' +import { useTheme } from '@mui/material/styles' // ==============================|| CHATFLOWS ||============================== // const Tools = () => { + const theme = useTheme() const getAllToolsApi = useApi(toolsApi.getAllTools) const [isLoading, setLoading] = useState(true) const [error, setError] = useState(null) const [showDialog, setShowDialog] = useState(false) const [dialogProps, setDialogProps] = useState({}) + const [view, setView] = useState(localStorage.getItem('toolsDisplayStyle') || 'card') const inputRef = useRef(null) + const handleChange = (event, nextView) => { + if (nextView === null) return + localStorage.setItem('toolsDisplayStyle', nextView) + setView(nextView) + } + const onUploadFile = (file) => { try { const dialogProp = { @@ -118,6 +128,38 @@ const Tools = () => { ) : ( + + + + + + + +