diff --git a/README.md b/README.md index ce4e815f778..e70adea0027 100644 --- a/README.md +++ b/README.md @@ -41,9 +41,10 @@ Download and Install [NodeJS](https://nodejs.org/en/download) >= 18.15.0 ## 🐳 Docker ### Docker Compose + 1. Clone the Flowise project 2. Go to `docker` folder at the root of the project -3. Copy `.env.example` file, paste it into the same location, and rename to `.env` +3. Copy `.env.example` file, paste it into the same location, and rename to `.env` file 4. `docker compose up -d` 5. Open [http://localhost:3000](http://localhost:3000) 6. You can bring the containers down by `docker compose stop` diff --git a/docker/.env.example b/docker/.env.example index ee1896c613f..2edb488d7f1 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -51,4 +51,10 @@ BLOB_STORAGE_PATH=/root/.flowise/storage # S3_FORCE_PATH_STYLE=false # APIKEY_STORAGE_TYPE=json (json | db) -# SHOW_COMMUNITY_NODES=true \ No newline at end of file +# SHOW_COMMUNITY_NODES=true + +# Uncomment the following lines to enable global agent proxy +# see https://www.npmjs.com/package/global-agent for more details +# GLOBAL_AGENT_HTTP_PROXY=CorporateHttpProxyUrl +# GLOBAL_AGENT_HTTPS_PROXY=CorporateHttpsProxyUrl +# GLOBAL_AGENT_NO_PROXY=ExceptionHostsToBypassProxyIfNeeded \ No newline at end of file diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 55f890e69ba..38d4dc3c60f 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -21,6 +21,7 @@ services: - DATABASE_PASSWORD=${DATABASE_PASSWORD} - DATABASE_SSL=${DATABASE_SSL} - DATABASE_SSL_KEY_BASE64=${DATABASE_SSL_KEY_BASE64} + - APIKEY_STORAGE_TYPE=${APIKEY_STORAGE_TYPE} - APIKEY_PATH=${APIKEY_PATH} - SECRETKEY_PATH=${SECRETKEY_PATH} - FLOWISE_SECRETKEY_OVERWRITE=${FLOWISE_SECRETKEY_OVERWRITE} @@ -29,6 +30,9 @@ services: - BLOB_STORAGE_PATH=${BLOB_STORAGE_PATH} - DISABLE_FLOWISE_TELEMETRY=${DISABLE_FLOWISE_TELEMETRY} - MODEL_LIST_CONFIG_JSON=${MODEL_LIST_CONFIG_JSON} + - GLOBAL_AGENT_HTTP_PROXY=${GLOBAL_AGENT_HTTP_PROXY} + - GLOBAL_AGENT_HTTPS_PROXY=${GLOBAL_AGENT_HTTPS_PROXY} + - GLOBAL_AGENT_NO_PROXY=${GLOBAL_AGENT_NO_PROXY} ports: - '${PORT}:${PORT}' volumes: diff --git a/i18n/CONTRIBUTING-ZH.md b/i18n/CONTRIBUTING-ZH.md index 50b84865988..c84f0f3613d 100644 --- a/i18n/CONTRIBUTING-ZH.md +++ b/i18n/CONTRIBUTING-ZH.md @@ -150,7 +150,7 @@ Flowise 支持不同的环境变量来配置您的实例。您可以在 `package | S3_STORAGE_SECRET_ACCESS_KEY | AWS 密钥 (Secret Key) | 字符串 | | | S3_STORAGE_REGION | S3 存储地区 | 字符串 | | | S3_ENDPOINT_URL | S3 端点 URL | 字符串 | | -| S3_FORCE_PATH_STYLE | 将其设置为 true 以强制请求使用路径样式寻址 | Boolean | false | +| S3_FORCE_PATH_STYLE | 将其设置为 true 以强制请求使用路径样式寻址 | Boolean | false | | SHOW_COMMUNITY_NODES | 显示由社区创建的节点 | 布尔值 | | 您也可以在使用 `npx` 时指定环境变量。例如: diff --git a/package.json b/package.json index 65442216edc..44a07088df8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "flowise", - "version": "2.1.2", + "version": "2.1.3", "private": true, "homepage": "https://flowiseai.com", "workspaces": [ diff --git a/packages/api-documentation/src/yml/swagger.yml b/packages/api-documentation/src/yml/swagger.yml index 66531111d5e..da52b2f05c1 100644 --- a/packages/api-documentation/src/yml/swagger.yml +++ b/packages/api-documentation/src/yml/swagger.yml @@ -1,5 +1,6 @@ tags: - name: assistants + - name: attachments - name: chatmessage - name: chatflows - name: document-store @@ -270,6 +271,61 @@ paths: '500': description: Internal error + /attachments/{chatflowId}/{chatId}: + post: + tags: + - attachments + security: + - bearerAuth: [] + operationId: createAttachment + summary: Create attachments array + description: Return contents of the files in plain string format + parameters: + - in: path + name: chatflowId + required: true + schema: + type: string + description: Chatflow ID + - in: path + name: chatId + required: true + schema: + type: string + description: Chat ID + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + files: + type: array + items: + type: string + format: binary + description: Files to be uploaded + required: + - files + required: true + responses: + '200': + description: Attachments created successfully + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/CreateAttachmentResponse' + '400': + description: Invalid input provided + '404': + description: Chatflow or ChatId not found + '422': + description: Validation error + '500': + description: Internal server error + /chatflows: post: tags: @@ -1825,7 +1881,8 @@ components: properties: type: type: string - description: The type of file upload (e.g., 'file', 'audio', 'url') + enum: [audio, url, file, file:rag, file:full] + description: The type of file upload example: file name: type: string @@ -2193,6 +2250,22 @@ components: format: date-time description: Date and time when the feedback was created + CreateAttachmentResponse: + type: object + properties: + name: + type: string + description: Name of the file + mimeType: + type: string + description: Mime type of the file + size: + type: string + description: Size of the file + content: + type: string + description: Content of the file in string format + securitySchemes: bearerAuth: type: http diff --git a/packages/components/credentials/CerebrasApi.credential.ts b/packages/components/credentials/CerebrasApi.credential.ts new file mode 100644 index 00000000000..5a701536f0f --- /dev/null +++ b/packages/components/credentials/CerebrasApi.credential.ts @@ -0,0 +1,25 @@ +import { INodeParams, INodeCredential } from '../src/Interface' + +class CerebrasAPIAuth implements INodeCredential { + label: string + name: string + version: number + description: string + inputs: INodeParams[] + + constructor() { + this.label = 'Cerebras API Key' + this.name = 'cerebrasAIApi' + this.version = 1.0 + this.inputs = [ + { + label: 'Cerebras API Key', + name: 'cerebrasApiKey', + type: 'password', + description: 'API Key (cloud.cerebras.ai)' + } + ] + } +} + +module.exports = { credClass: CerebrasAPIAuth } diff --git a/packages/components/credentials/CouchbaseApi.credential.ts b/packages/components/credentials/CouchbaseApi.credential.ts index 5772014afa7..8d3c68898b8 100644 --- a/packages/components/credentials/CouchbaseApi.credential.ts +++ b/packages/components/credentials/CouchbaseApi.credential.ts @@ -1,7 +1,3 @@ -/* -* Temporary disabled due to the incompatibility with the docker node-alpine: -* https://github.com/FlowiseAI/Flowise/pull/2303 - import { INodeParams, INodeCredential } from '../src/Interface' class CouchbaseApi implements INodeCredential { @@ -36,4 +32,3 @@ class CouchbaseApi implements INodeCredential { } module.exports = { credClass: CouchbaseApi } -*/ diff --git a/packages/components/models.json b/packages/components/models.json index e6ac76618c9..2435354c087 100644 --- a/packages/components/models.json +++ b/packages/components/models.json @@ -4,24 +4,29 @@ "name": "awsChatBedrock", "models": [ { - "label": "anthropic.claude-3-haiku", - "name": "anthropic.claude-3-haiku-20240307-v1:0", - "description": "Image to text, conversation, chat optimized" + "label": "anthropic.claude-3.5-sonnet-20241022-v2:0", + "name": "anthropic.claude-3-5-sonnet-20241022-v2:0", + "description": "(20241022-v2:0) specific version of Claude Sonnet 3.5 - most intelligent model" }, { - "label": "anthropic.claude-3.5-sonnet", + "label": "anthropic.claude-3.5-sonnet-20240620-v1:0", "name": "anthropic.claude-3-5-sonnet-20240620-v1:0", - "description": "3.5 version of Claude Sonnet model" + "description": "(20240620-v1:0) specific version of Claude Sonnet 3.5 - most intelligent model" + }, + { + "label": "anthropic.claude-3-opus", + "name": "anthropic.claude-3-opus-20240229-v1:0", + "description": "Powerful model for highly complex tasks, reasoning and analysis" }, { "label": "anthropic.claude-3-sonnet", "name": "anthropic.claude-3-sonnet-20240229-v1:0", - "description": "Image to text and code, multilingual conversation, complex reasoning and analysis" + "description": "Balance of intelligence and speed" }, { - "label": "anthropic.claude-3-opus", - "name": "anthropic.claude-3-opus-20240229-v1:0", - "description": "Image to text and code, multilingual conversation, complex reasoning and analysis" + "label": "anthropic.claude-3-haiku", + "name": "anthropic.claude-3-haiku-20240307-v1:0", + "description": "Fastest and most compact model for near-instant responsiveness" }, { "label": "anthropic.claude-instant-v1", @@ -295,25 +300,35 @@ "name": "chatAnthropic", "models": [ { - "label": "claude-3-haiku", - "name": "claude-3-haiku-20240307", - "description": "Fastest and most compact model, designed for near-instant responsiveness" + "label": "claude-3.5-sonnet-latest", + "name": "claude-3-5-sonnet-latest", + "description": "Most recent snapshot version of Claude Sonnet 3.5 model - most intelligent model" }, { - "label": "claude-3-opus", - "name": "claude-3-opus-20240229", - "description": "Most powerful model for highly complex tasks" + "label": "claude-3.5-sonnet-20241022", + "name": "claude-3-5-sonnet-20241022", + "description": "(20241022) specific version of Claude Sonnet 3.5 - most intelligent model" }, { - "label": "claude-3.5-sonnet", + "label": "claude-3.5-sonnet-20240620", "name": "claude-3-5-sonnet-20240620", - "description": "3.5 version of Claude Sonnet model" + "description": "(20240620) specific version of Claude Sonnet 3.5 - most intelligent model" + }, + { + "label": "claude-3-opus", + "name": "claude-3-opus-20240229", + "description": "Powerful model for highly complex tasks, reasoning and analysis" }, { "label": "claude-3-sonnet", "name": "claude-3-sonnet-20240229", "description": "Ideal balance of intelligence and speed for enterprise workloads" }, + { + "label": "claude-3-haiku", + "name": "claude-3-haiku-20240307", + "description": "Fastest and most compact model, designed for near-instant responsiveness" + }, { "label": "claude-2.0 (legacy)", "name": "claude-2.0", @@ -439,6 +454,26 @@ "label": "gemini-1.0-pro-vision", "name": "gemini-1.0-pro-vision" }, + { + "label": "claude-3-5-sonnet-v2@20241022", + "name": "claude-3-5-sonnet-v2@20241022", + "description": "(20241022-v2:0) specific version of Claude Sonnet 3.5 - most intelligent model" + }, + { + "label": "claude-3-opus@20240229", + "name": "claude-3-opus@20240229", + "description": "Powerful model for highly complex tasks, reasoning and analysis" + }, + { + "label": "claude-3-sonnet@20240229", + "name": "claude-3-sonnet@20240229", + "description": "Balance of intelligence and speed" + }, + { + "label": "claude-3-haiku@20240307", + "name": "claude-3-haiku@20240307", + "description": "Fastest and most compact model for near-instant responsiveness" + }, { "label": "chat-bison", "name": "chat-bison" @@ -1153,6 +1188,16 @@ { "name": "voyageAIEmbeddings", "models": [ + { + "label": "voyage-3", + "name": "voyage-3", + "description": "High-performance embedding model with excellent retrieval quality, 32K token context, and 1024 dimension size." + }, + { + "label": "voyage-3-lite", + "name": "voyage-3-lite", + "description": "Lightweight embedding model optimized for low latency and cost, 32K token context, and 512 dimension size." + }, { "label": "voyage-2", "name": "voyage-2", diff --git a/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts b/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts index db834d4c271..f7c496c350d 100644 --- a/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts +++ b/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts @@ -267,28 +267,54 @@ class OpenAIAssistant_Agents implements INode { // List all runs, in case existing thread is still running if (!isNewThread) { const promise = (threadId: string) => { - return new Promise((resolve) => { + return new Promise((resolve, reject) => { + const maxWaitTime = 30000 // Maximum wait time of 30 seconds + const startTime = Date.now() + let delay = 500 // Initial delay between retries + const maxRetries = 10 + let retries = 0 + const timeout = setInterval(async () => { - const allRuns = await openai.beta.threads.runs.list(threadId) - if (allRuns.data && allRuns.data.length) { - const firstRunId = allRuns.data[0].id - const runStatus = allRuns.data.find((run) => run.id === firstRunId)?.status - if ( - runStatus && - (runStatus === 'cancelled' || - runStatus === 'completed' || - runStatus === 'expired' || - runStatus === 'failed' || - runStatus === 'requires_action') - ) { + try { + const allRuns = await openai.beta.threads.runs.list(threadId) + if (allRuns.data && allRuns.data.length) { + const firstRunId = allRuns.data[0].id + const runStatus = allRuns.data.find((run) => run.id === firstRunId)?.status + if ( + runStatus && + (runStatus === 'cancelled' || + runStatus === 'completed' || + runStatus === 'expired' || + runStatus === 'failed' || + runStatus === 'requires_action') + ) { + clearInterval(timeout) + resolve() + } + } else { clearInterval(timeout) - resolve() + reject(new Error(`Empty Thread: ${threadId}`)) } - } else { + } catch (error: any) { + if (error.response?.status === 404) { + clearInterval(timeout) + reject(new Error(`Thread not found: ${threadId}`)) + } else if (error.response?.status === 429 && retries < maxRetries) { + retries++ + delay *= 2 + console.warn(`Rate limit exceeded, retrying in ${delay}ms...`) + } else { + clearInterval(timeout) + reject(new Error(`Unexpected error: ${error.message}`)) + } + } + + // Timeout condition to stop the loop if maxWaitTime is exceeded + if (Date.now() - startTime > maxWaitTime) { clearInterval(timeout) - resolve() + reject(new Error('Timeout waiting for thread to finish.')) } - }, 500) + }, delay) }) } await promise(threadId) @@ -576,96 +602,127 @@ class OpenAIAssistant_Agents implements INode { const promise = (threadId: string, runId: string) => { return new Promise((resolve, reject) => { + const maxWaitTime = 30000 // Maximum wait time of 30 seconds + const startTime = Date.now() + let delay = 500 // Initial delay between retries + const maxRetries = 10 + let retries = 0 + const timeout = setInterval(async () => { - const run = await openai.beta.threads.runs.retrieve(threadId, runId) - const state = run.status - if (state === 'completed') { - clearInterval(timeout) - resolve(state) - } else if (state === 'requires_action') { - if (run.required_action?.submit_tool_outputs.tool_calls) { + try { + const run = await openai.beta.threads.runs.retrieve(threadId, runId) + const state = run.status + + if (state === 'completed') { clearInterval(timeout) - const actions: ICommonObject[] = [] - run.required_action.submit_tool_outputs.tool_calls.forEach((item) => { - const functionCall = item.function - let args = {} - try { - args = JSON.parse(functionCall.arguments) - } catch (e) { - console.error('Error parsing arguments, default to empty object') - } - actions.push({ - tool: functionCall.name, - toolInput: args, - toolCallId: item.id + resolve(state) + } else if (state === 'requires_action') { + if (run.required_action?.submit_tool_outputs.tool_calls) { + clearInterval(timeout) + const actions: ICommonObject[] = [] + run.required_action.submit_tool_outputs.tool_calls.forEach((item) => { + const functionCall = item.function + let args = {} + try { + args = JSON.parse(functionCall.arguments) + } catch (e) { + console.error('Error parsing arguments, default to empty object') + } + actions.push({ + tool: functionCall.name, + toolInput: args, + toolCallId: item.id + }) }) - }) - const submitToolOutputs = [] - for (let i = 0; i < actions.length; i += 1) { - const tool = tools.find((tool: any) => tool.name === actions[i].tool) - if (!tool) continue + const submitToolOutputs = [] + for (let i = 0; i < actions.length; i += 1) { + const tool = tools.find((tool: any) => tool.name === actions[i].tool) + if (!tool) continue - // Start tool analytics - const toolIds = await analyticHandlers.onToolStart(tool.name, actions[i].toolInput, parentIds) - if (shouldStreamResponse && sseStreamer) { - sseStreamer.streamToolEvent(chatId, tool.name) + // Start tool analytics + const toolIds = await analyticHandlers.onToolStart(tool.name, actions[i].toolInput, parentIds) + if (shouldStreamResponse && sseStreamer) { + sseStreamer.streamToolEvent(chatId, tool.name) + } + + try { + const toolOutput = await tool.call(actions[i].toolInput, undefined, undefined, { + sessionId: threadId, + chatId: options.chatId, + input + }) + await analyticHandlers.onToolEnd(toolIds, toolOutput) + submitToolOutputs.push({ + tool_call_id: actions[i].toolCallId, + output: toolOutput + }) + usedTools.push({ + tool: tool.name, + toolInput: actions[i].toolInput, + toolOutput + }) + } catch (e) { + await analyticHandlers.onToolEnd(toolIds, e) + console.error('Error executing tool', e) + clearInterval(timeout) + reject( + new Error( + `Error processing thread: ${state}, Thread ID: ${threadId}, Run ID: ${runId}, Tool: ${tool.name}` + ) + ) + return + } } + const newRun = await openai.beta.threads.runs.retrieve(threadId, runId) + const newStatus = newRun?.status + try { - const toolOutput = await tool.call(actions[i].toolInput, undefined, undefined, { - sessionId: threadId, - chatId: options.chatId, - input - }) - await analyticHandlers.onToolEnd(toolIds, toolOutput) - submitToolOutputs.push({ - tool_call_id: actions[i].toolCallId, - output: toolOutput - }) - usedTools.push({ - tool: tool.name, - toolInput: actions[i].toolInput, - toolOutput - }) + if (submitToolOutputs.length && newStatus === 'requires_action') { + await openai.beta.threads.runs.submitToolOutputs(threadId, runId, { + tool_outputs: submitToolOutputs + }) + resolve(state) + } else { + await openai.beta.threads.runs.cancel(threadId, runId) + resolve('requires_action_retry') + } } catch (e) { - await analyticHandlers.onToolEnd(toolIds, e) - console.error('Error executing tool', e) clearInterval(timeout) reject( - new Error( - `Error processing thread: ${state}, Thread ID: ${threadId}, Run ID: ${runId}, Tool: ${tool.name}` - ) + new Error(`Error submitting tool outputs: ${state}, Thread ID: ${threadId}, Run ID: ${runId}`) ) - break - } - } - - const newRun = await openai.beta.threads.runs.retrieve(threadId, runId) - const newStatus = newRun?.status - - try { - if (submitToolOutputs.length && newStatus === 'requires_action') { - await openai.beta.threads.runs.submitToolOutputs(threadId, runId, { - tool_outputs: submitToolOutputs - }) - resolve(state) - } else { - await openai.beta.threads.runs.cancel(threadId, runId) - resolve('requires_action_retry') } - } catch (e) { - clearInterval(timeout) - reject(new Error(`Error submitting tool outputs: ${state}, Thread ID: ${threadId}, Run ID: ${runId}`)) } + } else if (state === 'cancelled' || state === 'expired' || state === 'failed') { + clearInterval(timeout) + reject( + new Error( + `Error processing thread: ${state}, Thread ID: ${threadId}, Run ID: ${runId}, Status: ${state}` + ) + ) } - } else if (state === 'cancelled' || state === 'expired' || state === 'failed') { + } catch (error: any) { + if (error.response?.status === 404 || error.response?.status === 429) { + clearInterval(timeout) + reject(new Error(`API error: ${error.response?.status} for Thread ID: ${threadId}, Run ID: ${runId}`)) + } else if (retries < maxRetries) { + retries++ + delay *= 2 // Exponential backoff + console.warn(`Transient error, retrying in ${delay}ms...`) + } else { + clearInterval(timeout) + reject(new Error(`Max retries reached. Error: ${error.message}`)) + } + } + + // Stop the loop if maximum wait time is exceeded + if (Date.now() - startTime > maxWaitTime) { clearInterval(timeout) - reject( - new Error(`Error processing thread: ${state}, Thread ID: ${threadId}, Run ID: ${runId}, Status: ${state}`) - ) + reject(new Error('Timeout waiting for thread to finish.')) } - }, 500) + }, delay) }) } diff --git a/packages/components/nodes/chatmodels/AzureChatOpenAI/AzureChatOpenAI.ts b/packages/components/nodes/chatmodels/AzureChatOpenAI/AzureChatOpenAI.ts index b2445cc559b..d974a8b03a5 100644 --- a/packages/components/nodes/chatmodels/AzureChatOpenAI/AzureChatOpenAI.ts +++ b/packages/components/nodes/chatmodels/AzureChatOpenAI/AzureChatOpenAI.ts @@ -6,6 +6,12 @@ import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../ import { ChatOpenAI } from '../ChatOpenAI/FlowiseChatOpenAI' import { getModels, MODEL_TYPE } from '../../../src/modelLoader' +const serverCredentialsExists = + !!process.env.AZURE_OPENAI_API_KEY && + !!process.env.AZURE_OPENAI_API_INSTANCE_NAME && + !!process.env.AZURE_OPENAI_API_DEPLOYMENT_NAME && + !!process.env.AZURE_OPENAI_API_VERSION + class AzureChatOpenAI_ChatModels implements INode { label: string name: string @@ -31,7 +37,8 @@ class AzureChatOpenAI_ChatModels implements INode { label: 'Connect Credential', name: 'credential', type: 'credential', - credentialNames: ['azureOpenAIApi'] + credentialNames: ['azureOpenAIApi'], + optional: serverCredentialsExists } this.inputs = [ { diff --git a/packages/components/nodes/chatmodels/AzureChatOpenAI/README.md b/packages/components/nodes/chatmodels/AzureChatOpenAI/README.md new file mode 100644 index 00000000000..f12f42dc198 --- /dev/null +++ b/packages/components/nodes/chatmodels/AzureChatOpenAI/README.md @@ -0,0 +1,16 @@ +# Azure OpenAI Chat Model + +Azure OpenAI Chat Model integration for Flowise + +## 🌱 Env Variables + +| Variable | Description | Type | Default | +| ---------------------------- | ----------------------------------------------------------------------------------------------- | ------------------------------------------------ | ----------------------------------- | +| AZURE_OPENAI_API_KEY | Default `credential.azureOpenAIApiKey` for Azure OpenAI Model | String | | +| AZURE_OPENAI_API_INSTANCE_NAME | Default `credential.azureOpenAIApiInstanceName` for Azure OpenAI Model | String | | +| AZURE_OPENAI_API_DEPLOYMENT_NAME | Default `credential.azureOpenAIApiDeploymentName` for Azure OpenAI Model | String | | +| AZURE_OPENAI_API_VERSION | Default `credential.azureOpenAIApiVersion` for Azure OpenAI Model | String | | + +## License + +Source code in this repository is made available under the [Apache License Version 2.0](https://github.com/FlowiseAI/Flowise/blob/master/LICENSE.md). \ No newline at end of file diff --git a/packages/components/nodes/chatmodels/ChatCerebras/ChatCerebras.ts b/packages/components/nodes/chatmodels/ChatCerebras/ChatCerebras.ts new file mode 100644 index 00000000000..40951b3a8b4 --- /dev/null +++ b/packages/components/nodes/chatmodels/ChatCerebras/ChatCerebras.ts @@ -0,0 +1,161 @@ +import { ChatOpenAI, OpenAIChatInput } from '@langchain/openai' +import { BaseCache } from '@langchain/core/caches' +import { BaseLLMParams } from '@langchain/core/language_models/llms' +import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' +import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' + +class ChatCerebras_ChatModels implements INode { + label: string + name: string + version: number + type: string + icon: string + category: string + description: string + baseClasses: string[] + credential: INodeParams + inputs: INodeParams[] + + constructor() { + this.label = 'ChatCerebras' + this.name = 'chatCerebras' + this.version = 1.0 + this.type = 'ChatCerebras' + this.icon = 'cerebras.png' + this.category = 'Chat Models' + this.description = 'Models available via Cerebras' + this.baseClasses = [this.type, ...getBaseClasses(ChatOpenAI)] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['cerebrasAIApi'], + optional: true + } + this.inputs = [ + { + label: 'Cache', + name: 'cache', + type: 'BaseCache', + optional: true + }, + { + label: 'Model Name', + name: 'modelName', + type: 'string', + placeholder: 'llama3.1-8b' + }, + { + label: 'Temperature', + name: 'temperature', + type: 'number', + step: 0.1, + default: 0.9, + optional: true + }, + { + label: 'Max Tokens', + name: 'maxTokens', + type: 'number', + step: 1, + optional: true, + additionalParams: true + }, + { + label: 'Top Probability', + name: 'topP', + type: 'number', + step: 0.1, + optional: true, + additionalParams: true + }, + { + label: 'Frequency Penalty', + name: 'frequencyPenalty', + type: 'number', + step: 0.1, + optional: true, + additionalParams: true + }, + { + label: 'Presence Penalty', + name: 'presencePenalty', + type: 'number', + step: 0.1, + optional: true, + additionalParams: true + }, + { + label: 'Timeout', + name: 'timeout', + type: 'number', + step: 1, + optional: true, + additionalParams: true + }, + { + label: 'BasePath', + name: 'basepath', + type: 'string', + optional: true, + default: 'https://api.cerebras.ai/v1', + additionalParams: true + }, + { + label: 'BaseOptions', + name: 'baseOptions', + type: 'json', + optional: true, + additionalParams: true + } + ] + } + + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + const temperature = nodeData.inputs?.temperature as string + const modelName = nodeData.inputs?.modelName as string + const maxTokens = nodeData.inputs?.maxTokens as string + const topP = nodeData.inputs?.topP as string + const frequencyPenalty = nodeData.inputs?.frequencyPenalty as string + const presencePenalty = nodeData.inputs?.presencePenalty as string + const timeout = nodeData.inputs?.timeout as string + const streaming = nodeData.inputs?.streaming as boolean + const basePath = nodeData.inputs?.basepath as string + const baseOptions = nodeData.inputs?.baseOptions + const cache = nodeData.inputs?.cache as BaseCache + + const credentialData = await getCredentialData(nodeData.credential ?? '', options) + const cerebrasAIApiKey = getCredentialParam('cerebrasApiKey', credentialData, nodeData) + + const obj: Partial & BaseLLMParams & { cerebrasAIApiKey?: string } = { + temperature: parseFloat(temperature), + modelName, + openAIApiKey: cerebrasAIApiKey, + streaming: streaming ?? true + } + + if (maxTokens) obj.maxTokens = parseInt(maxTokens, 10) + if (topP) obj.topP = parseFloat(topP) + if (frequencyPenalty) obj.frequencyPenalty = parseFloat(frequencyPenalty) + if (presencePenalty) obj.presencePenalty = parseFloat(presencePenalty) + if (timeout) obj.timeout = parseInt(timeout, 10) + if (cache) obj.cache = cache + + let parsedBaseOptions: any | undefined = undefined + + if (baseOptions) { + try { + parsedBaseOptions = typeof baseOptions === 'object' ? baseOptions : JSON.parse(baseOptions) + } catch (exception) { + throw new Error("Invalid JSON in the ChatCerebras's BaseOptions: " + exception) + } + } + const model = new ChatOpenAI(obj, { + basePath, + baseOptions: parsedBaseOptions + }) + return model + } +} + +module.exports = { nodeClass: ChatCerebras_ChatModels } diff --git a/packages/components/nodes/chatmodels/ChatCerebras/cerebras.png b/packages/components/nodes/chatmodels/ChatCerebras/cerebras.png new file mode 100644 index 00000000000..0b12258f1cc Binary files /dev/null and b/packages/components/nodes/chatmodels/ChatCerebras/cerebras.png differ diff --git a/packages/components/nodes/chatmodels/ChatNemoGuardrails/readme.md b/packages/components/nodes/chatmodels/ChatNemoGuardrails/readme.md index 44d1e5d7f51..88dfdfeb60e 100644 --- a/packages/components/nodes/chatmodels/ChatNemoGuardrails/readme.md +++ b/packages/components/nodes/chatmodels/ChatNemoGuardrails/readme.md @@ -10,9 +10,11 @@ baseUrl ```json { "config_id": "bedrock", - "messages": [{ - "role":"user", - "content":"Hello! What can you do for me?" - }] + "messages": [ + { + "role": "user", + "content": "Hello! What can you do for me?" + } + ] } -``` \ No newline at end of file +``` diff --git a/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI.ts b/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI.ts index 5f3e1542904..e12524ad022 100644 --- a/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI.ts +++ b/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI.ts @@ -111,6 +111,15 @@ class ChatOpenAI_ChatModels implements INode { optional: true, additionalParams: true }, + { + label: 'Stop Sequence', + name: 'stopSequence', + type: 'string', + rows: 4, + optional: true, + description: 'List of stop words to use when generating. Use comma to separate multiple stop words.', + additionalParams: true + }, { label: 'BaseOptions', name: 'baseOptions', @@ -168,6 +177,7 @@ class ChatOpenAI_ChatModels implements INode { const frequencyPenalty = nodeData.inputs?.frequencyPenalty as string const presencePenalty = nodeData.inputs?.presencePenalty as string const timeout = nodeData.inputs?.timeout as string + const stopSequence = nodeData.inputs?.stopSequence as string const streaming = nodeData.inputs?.streaming as boolean const basePath = nodeData.inputs?.basepath as string const proxyUrl = nodeData.inputs?.proxyUrl as string @@ -199,6 +209,10 @@ class ChatOpenAI_ChatModels implements INode { if (presencePenalty) obj.presencePenalty = parseFloat(presencePenalty) if (timeout) obj.timeout = parseInt(timeout, 10) if (cache) obj.cache = cache + if (stopSequence) { + const stopSequenceArray = stopSequence.split(',').map((item) => item.trim()) + obj.stop = stopSequenceArray + } let parsedBaseOptions: any | undefined = undefined diff --git a/packages/components/nodes/documentloaders/File/File.ts b/packages/components/nodes/documentloaders/File/File.ts index 0369787155f..da7a9a4106c 100644 --- a/packages/components/nodes/documentloaders/File/File.ts +++ b/packages/components/nodes/documentloaders/File/File.ts @@ -121,11 +121,22 @@ class File_DocumentLoaders implements INode { } const chatflowid = options.chatflowid - for (const file of files) { - if (!file) continue - const fileData = await getFileFromStorage(file, chatflowid) - const blob = new Blob([fileData]) - fileBlobs.push({ blob, ext: file.split('.').pop() || '' }) + // specific to createAttachment to get files from chatId + const retrieveAttachmentChatId = options.retrieveAttachmentChatId + if (retrieveAttachmentChatId) { + for (const file of files) { + if (!file) continue + const fileData = await getFileFromStorage(file, chatflowid, options.chatId) + const blob = new Blob([fileData]) + fileBlobs.push({ blob, ext: file.split('.').pop() || '' }) + } + } else { + for (const file of files) { + if (!file) continue + const fileData = await getFileFromStorage(file, chatflowid) + const blob = new Blob([fileData]) + fileBlobs.push({ blob, ext: file.split('.').pop() || '' }) + } } } else { if (totalFiles.startsWith('[') && totalFiles.endsWith(']')) { @@ -288,7 +299,12 @@ class MultiFileLoader extends BaseDocumentLoader { const loader = loaderFactory(fileBlob.blob) documents.push(...(await loader.load())) } else { - throw new Error(`Error loading file`) + const loader = new TextLoader(fileBlob.blob) + try { + documents.push(...(await loader.load())) + } catch (error) { + throw new Error(`Error loading file`) + } } } diff --git a/packages/components/nodes/documentloaders/S3File/README.md b/packages/components/nodes/documentloaders/S3File/README.md new file mode 100644 index 00000000000..142ed86e1b9 --- /dev/null +++ b/packages/components/nodes/documentloaders/S3File/README.md @@ -0,0 +1,13 @@ +# S3 File Loader + +DS File Loarder integration for Flowise + +## 🌱 Env Variables + +| Variable | Description | Type | Default | +| ---------------------------- | ----------------------------------------------------------------------------------------------- | ------------------------------------------------ | ----------------------------------- | +| UNSTRUCTURED_API_URL | Default `unstructuredApiUrl` for S3 File Loader | String | http://localhost:8000/general/v0/general | + +## License + +Source code in this repository is made available under the [Apache License Version 2.0](https://github.com/FlowiseAI/Flowise/blob/master/LICENSE.md). \ No newline at end of file diff --git a/packages/components/nodes/documentloaders/S3File/S3File.ts b/packages/components/nodes/documentloaders/S3File/S3File.ts index 2e8fcaf1852..fc83a448ed6 100644 --- a/packages/components/nodes/documentloaders/S3File/S3File.ts +++ b/packages/components/nodes/documentloaders/S3File/S3File.ts @@ -70,7 +70,8 @@ class S3_DocumentLoaders implements INode { description: 'Your Unstructured.io URL. Read more on how to get started', type: 'string', - default: 'http://localhost:8000/general/v0/general' + placeholder: process.env.UNSTRUCTURED_API_URL || 'http://localhost:8000/general/v0/general', + optional: !!process.env.UNSTRUCTURED_API_URL }, { label: 'Unstructured API KEY', diff --git a/packages/components/nodes/documentloaders/Unstructured/README.md b/packages/components/nodes/documentloaders/Unstructured/README.md new file mode 100644 index 00000000000..0854cc67d7b --- /dev/null +++ b/packages/components/nodes/documentloaders/Unstructured/README.md @@ -0,0 +1,13 @@ +# Unstructured File/Floder Loader + +Unstructured File Loader integration for Flowise + +## 🌱 Env Variables + +| Variable | Description | Type | Default | +| ---------------------------- | ----------------------------------------------------------------------------------------------- | ------------------------------------------------ | ----------------------------------- | +| UNSTRUCTURED_API_URL | Default `apiUrl` for Unstructured File/Floder Loader | String | http://localhost:8000/general/v0/general | + +## License + +Source code in this repository is made available under the [Apache License Version 2.0](https://github.com/FlowiseAI/Flowise/blob/master/LICENSE.md). \ No newline at end of file diff --git a/packages/components/nodes/documentloaders/Unstructured/Unstructured.ts b/packages/components/nodes/documentloaders/Unstructured/Unstructured.ts index 18ca94f2898..8f3e49f6258 100644 --- a/packages/components/nodes/documentloaders/Unstructured/Unstructured.ts +++ b/packages/components/nodes/documentloaders/Unstructured/Unstructured.ts @@ -29,9 +29,9 @@ type Element = { export class UnstructuredLoader extends BaseDocumentLoader { public filePath: string - private apiUrl = 'https://api.unstructuredapp.io/general/v0/general' + private apiUrl = process.env.UNSTRUCTURED_API_URL || 'https://api.unstructuredapp.io/general/v0/general' - private apiKey?: string + private apiKey: string | undefined = process.env.UNSTRUCTURED_API_KEY private strategy: StringWithAutocomplete = 'hi_res' @@ -66,10 +66,10 @@ export class UnstructuredLoader extends BaseDocumentLoader { const options = optionsOrLegacyFilePath this.apiKey = options.apiKey - this.apiUrl = options.apiUrl ?? this.apiUrl - this.strategy = options.strategy ?? this.strategy + this.apiUrl = options.apiUrl || this.apiUrl + this.strategy = options.strategy || this.strategy this.encoding = options.encoding - this.ocrLanguages = options.ocrLanguages ?? this.ocrLanguages + this.ocrLanguages = options.ocrLanguages || this.ocrLanguages this.coordinates = options.coordinates this.pdfInferTableStructure = options.pdfInferTableStructure this.xmlKeepTags = options.xmlKeepTags @@ -128,7 +128,7 @@ export class UnstructuredLoader extends BaseDocumentLoader { } const headers = { - 'UNSTRUCTURED-API-KEY': this.apiKey ?? '' + 'UNSTRUCTURED-API-KEY': this.apiKey || '' } const response = await fetch(this.apiUrl, { diff --git a/packages/components/nodes/documentloaders/Unstructured/UnstructuredFile.ts b/packages/components/nodes/documentloaders/Unstructured/UnstructuredFile.ts index df680e63781..28dc793ec45 100644 --- a/packages/components/nodes/documentloaders/Unstructured/UnstructuredFile.ts +++ b/packages/components/nodes/documentloaders/Unstructured/UnstructuredFile.ts @@ -63,7 +63,8 @@ class UnstructuredFile_DocumentLoaders implements INode { description: 'Unstructured API URL. Read more on how to get started', type: 'string', - default: 'http://localhost:8000/general/v0/general' + placeholder: process.env.UNSTRUCTURED_API_URL || 'http://localhost:8000/general/v0/general', + optional: !!process.env.UNSTRUCTURED_API_URL }, { label: 'Strategy', diff --git a/packages/components/nodes/documentloaders/Unstructured/UnstructuredFolder.ts b/packages/components/nodes/documentloaders/Unstructured/UnstructuredFolder.ts index 996af5ac1ed..df396590d1f 100644 --- a/packages/components/nodes/documentloaders/Unstructured/UnstructuredFolder.ts +++ b/packages/components/nodes/documentloaders/Unstructured/UnstructuredFolder.ts @@ -51,7 +51,8 @@ class UnstructuredFolder_DocumentLoaders implements INode { description: 'Unstructured API URL. Read more on how to get started', type: 'string', - default: 'http://localhost:8000/general/v0/general' + placeholder: process.env.UNSTRUCTURED_API_URL || 'http://localhost:8000/general/v0/general', + optional: !!process.env.UNSTRUCTURED_API_URL }, { label: 'Strategy', diff --git a/packages/components/nodes/embeddings/AzureOpenAIEmbedding/AzureOpenAIEmbedding.ts b/packages/components/nodes/embeddings/AzureOpenAIEmbedding/AzureOpenAIEmbedding.ts index e39e843dcc1..b6d6c7429b5 100644 --- a/packages/components/nodes/embeddings/AzureOpenAIEmbedding/AzureOpenAIEmbedding.ts +++ b/packages/components/nodes/embeddings/AzureOpenAIEmbedding/AzureOpenAIEmbedding.ts @@ -2,6 +2,12 @@ import { AzureOpenAIInput, OpenAIEmbeddings, OpenAIEmbeddingsParams } from '@lan import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' +const serverCredentialsExists = + !!process.env.AZURE_OPENAI_API_KEY && + !!process.env.AZURE_OPENAI_API_INSTANCE_NAME && + (!!process.env.AZURE_OPENAI_API_EMBEDDINGS_DEPLOYMENT_NAME || !!process.env.AZURE_OPENAI_API_DEPLOYMENT_NAME) && + !!process.env.AZURE_OPENAI_API_VERSION + class AzureOpenAIEmbedding_Embeddings implements INode { label: string name: string @@ -27,7 +33,8 @@ class AzureOpenAIEmbedding_Embeddings implements INode { label: 'Connect Credential', name: 'credential', type: 'credential', - credentialNames: ['azureOpenAIApi'] + credentialNames: ['azureOpenAIApi'], + optional: serverCredentialsExists } this.inputs = [ { diff --git a/packages/components/nodes/embeddings/AzureOpenAIEmbedding/README.md b/packages/components/nodes/embeddings/AzureOpenAIEmbedding/README.md new file mode 100644 index 00000000000..c3bd59e54e8 --- /dev/null +++ b/packages/components/nodes/embeddings/AzureOpenAIEmbedding/README.md @@ -0,0 +1,16 @@ +# Azure OpenAI Embedding Model + +Azure OpenAI Embedding Model integration for Flowise + +## 🌱 Env Variables + +| Variable | Description | Type | Default | +| ---------------------------- | ----------------------------------------------------------------------------------------------- | ------------------------------------------------ | ----------------------------------- | +| AZURE_OPENAI_API_KEY | Default `credential.azureOpenAIApiKey` for Azure OpenAI Model | String | | +| AZURE_OPENAI_API_INSTANCE_NAME | Default `credential.azureOpenAIApiInstanceName` for Azure OpenAI Model | String | | +| AZURE_OPENAI_API_EMBEDDINGS_DEPLOYMENT_NAME | Default `credential.azureOpenAIApiDeploymentName` for Azure OpenAI Model | String | | +| AZURE_OPENAI_API_VERSION | Default `credential.azureOpenAIApiVersion` for Azure OpenAI Model | String | | + +## License + +Source code in this repository is made available under the [Apache License Version 2.0](https://github.com/FlowiseAI/Flowise/blob/master/LICENSE.md). \ No newline at end of file diff --git a/packages/components/nodes/llms/Azure OpenAI/AzureOpenAI.ts b/packages/components/nodes/llms/Azure OpenAI/AzureOpenAI.ts index 0996699374e..8dfd74b703e 100644 --- a/packages/components/nodes/llms/Azure OpenAI/AzureOpenAI.ts +++ b/packages/components/nodes/llms/Azure OpenAI/AzureOpenAI.ts @@ -5,6 +5,12 @@ import { ICommonObject, INode, INodeData, INodeOptionsValue, INodeParams } from import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' import { getModels, MODEL_TYPE } from '../../../src/modelLoader' +const serverCredentialsExists = + !!process.env.AZURE_OPENAI_API_KEY && + !!process.env.AZURE_OPENAI_API_INSTANCE_NAME && + !!process.env.AZURE_OPENAI_API_DEPLOYMENT_NAME && + !!process.env.AZURE_OPENAI_API_VERSION + class AzureOpenAI_LLMs implements INode { label: string name: string @@ -30,7 +36,8 @@ class AzureOpenAI_LLMs implements INode { label: 'Connect Credential', name: 'credential', type: 'credential', - credentialNames: ['azureOpenAIApi'] + credentialNames: ['azureOpenAIApi'], + optional: serverCredentialsExists } this.inputs = [ { diff --git a/packages/components/nodes/llms/Azure OpenAI/README.md b/packages/components/nodes/llms/Azure OpenAI/README.md new file mode 100644 index 00000000000..de47c4dd813 --- /dev/null +++ b/packages/components/nodes/llms/Azure OpenAI/README.md @@ -0,0 +1,16 @@ +# Azure OpenAI LLM + +Azure OpenAI LLM integration for Flowise + +## 🌱 Env Variables + +| Variable | Description | Type | Default | +| ---------------------------- | ----------------------------------------------------------------------------------------------- | ------------------------------------------------ | ----------------------------------- | +| AZURE_OPENAI_API_KEY | Default `credential.azureOpenAIApiKey` for Azure OpenAI LLM | String | | +| AZURE_OPENAI_API_INSTANCE_NAME | Default `credential.azureOpenAIApiInstanceName` for Azure OpenAI LLM | String | | +| AZURE_OPENAI_API_DEPLOYMENT_NAME | Default `credential.azureOpenAIApiDeploymentName` for Azure OpenAI LLM | String | | +| AZURE_OPENAI_API_VERSION | Default `credential.azureOpenAIApiVersion` for Azure OpenAI LLM | String | | + +## License + +Source code in this repository is made available under the [Apache License Version 2.0](https://github.com/FlowiseAI/Flowise/blob/master/LICENSE.md). \ No newline at end of file diff --git a/packages/components/nodes/recordmanager/PostgresRecordManager/PostgresRecordManager.ts b/packages/components/nodes/recordmanager/PostgresRecordManager/PostgresRecordManager.ts index e01d85b5b0a..59f640c6a30 100644 --- a/packages/components/nodes/recordmanager/PostgresRecordManager/PostgresRecordManager.ts +++ b/packages/components/nodes/recordmanager/PostgresRecordManager/PostgresRecordManager.ts @@ -2,6 +2,10 @@ import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Inter import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' import { ListKeyOptions, RecordManagerInterface, UpdateOptions } from '@langchain/community/indexes/base' import { DataSource, QueryRunner } from 'typeorm' +import { getHost } from '../../vectorstores/Postgres/utils' +import { getDatabase, getPort, getTableName } from './utils' + +const serverCredentialsExists = !!process.env.POSTGRES_RECORDMANAGER_USER && !!process.env.POSTGRES_RECORDMANAGER_PASSWORD class PostgresRecordManager_RecordManager implements INode { label: string @@ -29,18 +33,22 @@ class PostgresRecordManager_RecordManager implements INode { { label: 'Host', name: 'host', - type: 'string' + type: 'string', + placeholder: getHost(), + optional: !!getHost() }, { label: 'Database', name: 'database', - type: 'string' + type: 'string', + placeholder: getDatabase(), + optional: !!getDatabase() }, { label: 'Port', name: 'port', type: 'number', - placeholder: '5432', + placeholder: getPort(), optional: true }, { @@ -54,7 +62,7 @@ class PostgresRecordManager_RecordManager implements INode { label: 'Table Name', name: 'tableName', type: 'string', - placeholder: 'upsertion_records', + placeholder: getTableName(), additionalParams: true, optional: true }, @@ -110,16 +118,16 @@ class PostgresRecordManager_RecordManager implements INode { label: 'Connect Credential', name: 'credential', type: 'credential', - credentialNames: ['PostgresApi'] + credentialNames: ['PostgresApi'], + optional: serverCredentialsExists } } async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { const credentialData = await getCredentialData(nodeData.credential ?? '', options) - const user = getCredentialParam('user', credentialData, nodeData) - const password = getCredentialParam('password', credentialData, nodeData) - const _tableName = nodeData.inputs?.tableName as string - const tableName = _tableName ? _tableName : 'upsertion_records' + const user = getCredentialParam('user', credentialData, nodeData, process.env.POSTGRES_RECORDMANAGER_USER) + const password = getCredentialParam('password', credentialData, nodeData, process.env.POSTGRES_RECORDMANAGER_PASSWORD) + const tableName = getTableName(nodeData) const additionalConfig = nodeData.inputs?.additionalConfig as string const _namespace = nodeData.inputs?.namespace as string const namespace = _namespace ? _namespace : options.chatflowid @@ -139,11 +147,11 @@ class PostgresRecordManager_RecordManager implements INode { const postgresConnectionOptions = { ...additionalConfiguration, type: 'postgres', - host: nodeData.inputs?.host as string, - port: nodeData.inputs?.port as number, + host: getHost(nodeData), + port: getPort(nodeData), username: user, password: password, - database: nodeData.inputs?.database as string + database: getDatabase(nodeData) } const args = { @@ -162,7 +170,7 @@ class PostgresRecordManager_RecordManager implements INode { type PostgresRecordManagerOptions = { postgresConnectionOptions: any - tableName?: string + tableName: string } class PostgresRecordManager implements RecordManagerInterface { @@ -180,7 +188,7 @@ class PostgresRecordManager implements RecordManagerInterface { const { postgresConnectionOptions, tableName } = config this.namespace = namespace this.datasource = new DataSource(postgresConnectionOptions) - this.tableName = tableName || 'upsertion_records' + this.tableName = tableName } async createSchema(): Promise { diff --git a/packages/components/nodes/recordmanager/PostgresRecordManager/README.md b/packages/components/nodes/recordmanager/PostgresRecordManager/README.md new file mode 100644 index 00000000000..b2a73ded6bf --- /dev/null +++ b/packages/components/nodes/recordmanager/PostgresRecordManager/README.md @@ -0,0 +1,18 @@ +# Postgres Record Manager + +Postgres Record Manager integration for Flowise + +## 🌱 Env Variables + +| Variable | Description | Type | Default | +| ---------------------------- | ----------------------------------------------------------------------------------------------- | ------------------------------------------------ | ----------------------------------- | +| POSTGRES_RECORDMANAGER_HOST | Default `host` for Postgres Record Manager | String | | +| POSTGRES_RECORDMANAGER_PORT | Default `port` for Postgres Record Manager | Number | 5432 | +| POSTGRES_RECORDMANAGER_USER | Default `user` for Postgres Record Manager | String | | +| POSTGRES_RECORDMANAGER_PASSWORD | Default `password` for Postgres Record Manager | String | | +| POSTGRES_RECORDMANAGER_DATABASE | Default `database` for Postgres Record Manager | String | | +| POSTGRES_RECORDMANAGER_TABLE_NAME | Default `tableName` for Postgres Record Manager | String | upsertion_records | + +## License + +Source code in this repository is made available under the [Apache License Version 2.0](https://github.com/FlowiseAI/Flowise/blob/master/LICENSE.md). \ No newline at end of file diff --git a/packages/components/nodes/recordmanager/PostgresRecordManager/utils.ts b/packages/components/nodes/recordmanager/PostgresRecordManager/utils.ts new file mode 100644 index 00000000000..f9a8d9ae06e --- /dev/null +++ b/packages/components/nodes/recordmanager/PostgresRecordManager/utils.ts @@ -0,0 +1,17 @@ +import { defaultChain, INodeData } from '../../../src' + +export function getHost(nodeData?: INodeData) { + return defaultChain(nodeData?.inputs?.host, process.env.POSTGRES_RECORDMANAGER_HOST) +} + +export function getDatabase(nodeData?: INodeData) { + return defaultChain(nodeData?.inputs?.database, process.env.POSTGRES_RECORDMANAGER_DATABASE) +} + +export function getPort(nodeData?: INodeData) { + return defaultChain(nodeData?.inputs?.port, process.env.POSTGRES_RECORDMANAGER_PORT, '5432') +} + +export function getTableName(nodeData?: INodeData) { + return defaultChain(nodeData?.inputs?.tableName, process.env.POSTGRES_RECORDMANAGER_TABLE_NAME, 'upsertion_records') +} diff --git a/packages/components/nodes/sequentialagents/Agent/Agent.ts b/packages/components/nodes/sequentialagents/Agent/Agent.ts index 8153595cdbd..a5517095543 100644 --- a/packages/components/nodes/sequentialagents/Agent/Agent.ts +++ b/packages/components/nodes/sequentialagents/Agent/Agent.ts @@ -68,9 +68,9 @@ const howToUseCode = ` "sourceDocuments": [ { "pageContent": "This is the page content", - "metadata": "{foo: var}", + "metadata": "{foo: var}" } - ], + ] } \`\`\` @@ -102,10 +102,10 @@ const howToUse = ` |-----------|-----------| | user | john doe | -2. If you want to use the agent's output as the value to update state, it is available as available as \`$flow.output\` with the following structure: +2. If you want to use the Agent's output as the value to update state, it is available as available as \`$flow.output\` with the following structure: \`\`\`json { - "output": "Hello! How can I assist you today?", + "content": "Hello! How can I assist you today?", "usedTools": [ { "tool": "tool-name", @@ -116,9 +116,9 @@ const howToUse = ` "sourceDocuments": [ { "pageContent": "This is the page content", - "metadata": "{foo: var}", + "metadata": "{foo: var}" } - ], + ] } \`\`\` @@ -195,7 +195,7 @@ class Agent_SeqAgents implements INode { constructor() { this.label = 'Agent' this.name = 'seqAgent' - this.version = 3.0 + this.version = 3.1 this.type = 'Agent' this.icon = 'seqAgent.png' this.category = 'Sequential Agents' diff --git a/packages/components/nodes/sequentialagents/LLMNode/LLMNode.ts b/packages/components/nodes/sequentialagents/LLMNode/LLMNode.ts index a5a570064a1..0fcb7eb20e0 100644 --- a/packages/components/nodes/sequentialagents/LLMNode/LLMNode.ts +++ b/packages/components/nodes/sequentialagents/LLMNode/LLMNode.ts @@ -88,7 +88,7 @@ const howToUse = ` |-----------|-----------| | user | john doe | -2. If you want to use the agent's output as the value to update state, it is available as available as \`$flow.output\` with the following structure: +2. If you want to use the LLM Node's output as the value to update state, it is available as available as \`$flow.output\` with the following structure: \`\`\`json { "content": 'Hello! How can I assist you today?', diff --git a/packages/components/nodes/sequentialagents/ToolNode/ToolNode.ts b/packages/components/nodes/sequentialagents/ToolNode/ToolNode.ts index 2c4daee48e0..fda82aec95b 100644 --- a/packages/components/nodes/sequentialagents/ToolNode/ToolNode.ts +++ b/packages/components/nodes/sequentialagents/ToolNode/ToolNode.ts @@ -48,9 +48,9 @@ const howToUseCode = ` "sourceDocuments": [ { "pageContent": "This is the page content", - "metadata": "{foo: var}", + "metadata": "{foo: var}" } - ], + ] } ] \`\`\` @@ -64,7 +64,7 @@ const howToUseCode = ` */ return { - "sources": $flow.output[0].sourceDocuments + "sources": $flow.output[0].toolOutput } \`\`\` @@ -89,17 +89,19 @@ const howToUse = ` |-----------|-----------| | user | john doe | -2. If you want to use the agent's output as the value to update state, it is available as available as \`$flow.output\` with the following structure (array): +2. If you want to use the Tool Node's output as the value to update state, it is available as available as \`$flow.output\` with the following structure (array): \`\`\`json [ { - "content": "Hello! How can I assist you today?", + "tool": "tool's name", + "toolInput": {}, + "toolOutput": "tool's output content", "sourceDocuments": [ { "pageContent": "This is the page content", - "metadata": "{foo: var}", + "metadata": "{foo: var}" } - ], + ] } ] \`\`\` @@ -107,7 +109,7 @@ const howToUse = ` For example: | Key | Value | |--------------|-------------------------------------------| - | sources | \`$flow.output[0].sourceDocuments\` | + | sources | \`$flow.output[0].toolOutput\` | 3. You can get default flow config, including the current "state": - \`$flow.sessionId\` @@ -152,7 +154,7 @@ class ToolNode_SeqAgents implements INode { constructor() { this.label = 'Tool Node' this.name = 'seqToolNode' - this.version = 2.0 + this.version = 2.1 this.type = 'ToolNode' this.icon = 'toolNode.svg' this.category = 'Sequential Agents' diff --git a/packages/components/nodes/vectorstores/Couchbase/Couchbase.ts b/packages/components/nodes/vectorstores/Couchbase/Couchbase.ts index 3dd79d751cd..83c45271e6e 100644 --- a/packages/components/nodes/vectorstores/Couchbase/Couchbase.ts +++ b/packages/components/nodes/vectorstores/Couchbase/Couchbase.ts @@ -1,7 +1,3 @@ -/* -* Temporary disabled due to the incompatibility with the docker node-alpine: -* https://github.com/FlowiseAI/Flowise/pull/2303 - import { flatten } from 'lodash' import { Embeddings } from '@langchain/core/embeddings' import { Document } from '@langchain/core/documents' @@ -231,4 +227,3 @@ class Couchbase_VectorStores implements INode { } module.exports = { nodeClass: Couchbase_VectorStores } -*/ diff --git a/packages/components/nodes/vectorstores/Postgres/Postgres.ts b/packages/components/nodes/vectorstores/Postgres/Postgres.ts index d5cdf069fda..1cdacb85fc7 100644 --- a/packages/components/nodes/vectorstores/Postgres/Postgres.ts +++ b/packages/components/nodes/vectorstores/Postgres/Postgres.ts @@ -1,13 +1,16 @@ -import { Pool } from 'pg' import { flatten } from 'lodash' -import { DataSourceOptions } from 'typeorm' -import { Embeddings } from '@langchain/core/embeddings' import { Document } from '@langchain/core/documents' -import { TypeORMVectorStore, TypeORMVectorStoreDocument } from '@langchain/community/vectorstores/typeorm' import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams, IndexingResult } from '../../../src/Interface' -import { FLOWISE_CHATID, getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' +import { FLOWISE_CHATID, getBaseClasses } from '../../../src/utils' import { index } from '../../../src/indexing' import { howToUseFileUpload } from '../VectorStoreUtils' +import { VectorStore } from '@langchain/core/vectorstores' +import { VectorStoreDriver } from './driver/Base' +import { TypeORMDriver } from './driver/TypeORM' +import { PGVectorDriver } from './driver/PGVector' +import { getContentColumnName, getDatabase, getHost, getPort, getTableName } from './utils' + +const serverCredentialsExists = !!process.env.POSTGRES_VECTORSTORE_USER && !!process.env.POSTGRES_VECTORSTORE_PASSWORD class Postgres_VectorStores implements INode { label: string @@ -26,7 +29,7 @@ class Postgres_VectorStores implements INode { constructor() { this.label = 'Postgres' this.name = 'postgres' - this.version = 6.0 + this.version = 7.0 this.type = 'Postgres' this.icon = 'postgres.svg' this.category = 'Vector Stores' @@ -36,7 +39,8 @@ class Postgres_VectorStores implements INode { label: 'Connect Credential', name: 'credential', type: 'credential', - credentialNames: ['PostgresApi'] + credentialNames: ['PostgresApi'], + optional: serverCredentialsExists } this.inputs = [ { @@ -61,28 +65,74 @@ class Postgres_VectorStores implements INode { { label: 'Host', name: 'host', - type: 'string' + type: 'string', + placeholder: getHost(), + optional: !!getHost() }, { label: 'Database', name: 'database', - type: 'string' + type: 'string', + placeholder: getDatabase(), + optional: !!getDatabase() }, { label: 'Port', name: 'port', type: 'number', - placeholder: '5432', + placeholder: getPort(), optional: true }, { label: 'Table Name', name: 'tableName', type: 'string', - placeholder: 'documents', + placeholder: getTableName(), additionalParams: true, optional: true }, + { + label: 'Driver', + name: 'driver', + type: 'options', + default: 'typeorm', + description: 'Different option to connect to Postgres', + options: [ + { + label: 'TypeORM', + name: 'typeorm' + }, + { + label: 'PGVector', + name: 'pgvector' + } + ], + optional: true, + additionalParams: true + }, + { + label: 'Distance Strategy', + name: 'distanceStrategy', + description: 'Strategy for calculating distances between vectors', + type: 'options', + options: [ + { + label: 'Cosine', + name: 'cosine' + }, + { + label: 'Euclidean', + name: 'euclidean' + }, + { + label: 'Inner Product', + name: 'innerProduct' + } + ], + additionalParams: true, + default: 'cosine', + optional: true + }, { label: 'File Upload', name: 'fileUpload', @@ -117,6 +167,15 @@ class Postgres_VectorStores implements INode { type: 'json', additionalParams: true, optional: true + }, + { + label: 'Content Column Name', + name: 'contentColumnName', + description: 'Column name to store the text content (PGVector Driver only, others use pageContent)', + type: 'string', + placeholder: getContentColumnName(), + additionalParams: true, + optional: true } ] this.outputs = [ @@ -128,7 +187,7 @@ class Postgres_VectorStores implements INode { { label: 'Postgres Vector Store', name: 'vectorStore', - baseClasses: [this.type, ...getBaseClasses(TypeORMVectorStore)] + baseClasses: [this.type, ...getBaseClasses(VectorStore)] } ] } @@ -136,43 +195,15 @@ class Postgres_VectorStores implements INode { //@ts-ignore vectorStoreMethods = { async upsert(nodeData: INodeData, options: ICommonObject): Promise> { - const credentialData = await getCredentialData(nodeData.credential ?? '', options) - const user = getCredentialParam('user', credentialData, nodeData) - const password = getCredentialParam('password', credentialData, nodeData) - const _tableName = nodeData.inputs?.tableName as string - const tableName = _tableName ? _tableName : 'documents' + const tableName = getTableName(nodeData) const docs = nodeData.inputs?.document as Document[] - const embeddings = nodeData.inputs?.embeddings as Embeddings - const additionalConfig = nodeData.inputs?.additionalConfig as string const recordManager = nodeData.inputs?.recordManager const isFileUploadEnabled = nodeData.inputs?.fileUpload as boolean - - let additionalConfiguration = {} - if (additionalConfig) { - try { - additionalConfiguration = typeof additionalConfig === 'object' ? additionalConfig : JSON.parse(additionalConfig) - } catch (exception) { - throw new Error('Invalid JSON in the Additional Configuration: ' + exception) - } - } - - const postgresConnectionOptions = { - ...additionalConfiguration, - type: 'postgres', - host: nodeData.inputs?.host as string, - port: nodeData.inputs?.port as number, - username: user, - password: password, - database: nodeData.inputs?.database as string - } - - const args = { - postgresConnectionOptions: postgresConnectionOptions as DataSourceOptions, - tableName: tableName - } + const vectorStoreDriver: VectorStoreDriver = Postgres_VectorStores.getDriverFromConfig(nodeData, options) const flattenDocs = docs && docs.length ? flatten(docs) : [] const finalDocs = [] + for (let i = 0; i < flattenDocs.length; i += 1) { if (flattenDocs[i] && flattenDocs[i].pageContent) { if (isFileUploadEnabled && options.chatId) { @@ -184,24 +215,7 @@ class Postgres_VectorStores implements INode { try { if (recordManager) { - const vectorStore = await TypeORMVectorStore.fromDataSource(embeddings, args) - - // Avoid Illegal invocation error - vectorStore.similaritySearchVectorWithScore = async (query: number[], k: number, filter?: any) => { - return await similaritySearchVectorWithScore(query, k, tableName, postgresConnectionOptions, filter) - } - - vectorStore.delete = async (params: { ids: string[] }): Promise => { - const { ids } = params - - if (ids?.length) { - try { - vectorStore.appDataSource.getRepository(vectorStore.documentEntity).delete(ids) - } catch (e) { - console.error('Failed to delete') - } - } - } + const vectorStore = await vectorStoreDriver.instanciate() await recordManager.createSchema() @@ -218,12 +232,7 @@ class Postgres_VectorStores implements INode { return res } else { - const vectorStore = await TypeORMVectorStore.fromDocuments(finalDocs, embeddings, args) - - // Avoid Illegal invocation error - vectorStore.similaritySearchVectorWithScore = async (query: number[], k: number, filter?: any) => { - return await similaritySearchVectorWithScore(query, k, tableName, postgresConnectionOptions, filter) - } + await vectorStoreDriver.fromDocuments(finalDocs) return { numAdded: finalDocs.length, addedDocs: finalDocs } } @@ -232,40 +241,11 @@ class Postgres_VectorStores implements INode { } }, async delete(nodeData: INodeData, ids: string[], options: ICommonObject): Promise { - const credentialData = await getCredentialData(nodeData.credential ?? '', options) - const user = getCredentialParam('user', credentialData, nodeData) - const password = getCredentialParam('password', credentialData, nodeData) - const _tableName = nodeData.inputs?.tableName as string - const tableName = _tableName ? _tableName : 'documents' - const embeddings = nodeData.inputs?.embeddings as Embeddings - const additionalConfig = nodeData.inputs?.additionalConfig as string + const vectorStoreDriver: VectorStoreDriver = Postgres_VectorStores.getDriverFromConfig(nodeData, options) + const tableName = getTableName(nodeData) const recordManager = nodeData.inputs?.recordManager - let additionalConfiguration = {} - if (additionalConfig) { - try { - additionalConfiguration = typeof additionalConfig === 'object' ? additionalConfig : JSON.parse(additionalConfig) - } catch (exception) { - throw new Error('Invalid JSON in the Additional Configuration: ' + exception) - } - } - - const postgresConnectionOptions = { - ...additionalConfiguration, - type: 'postgres', - host: nodeData.inputs?.host as string, - port: nodeData.inputs?.port as number, - username: user, - password: password, - database: nodeData.inputs?.database as string - } - - const args = { - postgresConnectionOptions: postgresConnectionOptions as DataSourceOptions, - tableName: tableName - } - - const vectorStore = await TypeORMVectorStore.fromDataSource(embeddings, args) + const vectorStore = await vectorStoreDriver.instanciate() try { if (recordManager) { @@ -286,13 +266,7 @@ class Postgres_VectorStores implements INode { } async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { - const credentialData = await getCredentialData(nodeData.credential ?? '', options) - const user = getCredentialParam('user', credentialData, nodeData) - const password = getCredentialParam('password', credentialData, nodeData) - const _tableName = nodeData.inputs?.tableName as string - const tableName = _tableName ? _tableName : 'documents' - const embeddings = nodeData.inputs?.embeddings as Embeddings - const additionalConfig = nodeData.inputs?.additionalConfig as string + const vectorStoreDriver: VectorStoreDriver = Postgres_VectorStores.getDriverFromConfig(nodeData, options) const output = nodeData.outputs?.output as string const topK = nodeData.inputs?.topK as string const k = topK ? parseFloat(topK) : 4 @@ -304,50 +278,13 @@ class Postgres_VectorStores implements INode { pgMetadataFilter = typeof _pgMetadataFilter === 'object' ? _pgMetadataFilter : JSON.parse(_pgMetadataFilter) } if (isFileUploadEnabled && options.chatId) { - pgMetadataFilter = pgMetadataFilter || {} pgMetadataFilter = { - ...pgMetadataFilter, - [FLOWISE_CHATID]: options.chatId, - $notexists: FLOWISE_CHATID // special filter to check if the field does not exist + ...(pgMetadataFilter || {}), + [FLOWISE_CHATID]: options.chatId } } - let additionalConfiguration = {} - if (additionalConfig) { - try { - additionalConfiguration = typeof additionalConfig === 'object' ? additionalConfig : JSON.parse(additionalConfig) - } catch (exception) { - throw new Error('Invalid JSON in the Additional Configuration: ' + exception) - } - } - - const postgresConnectionOptions = { - ...additionalConfiguration, - type: 'postgres', - host: nodeData.inputs?.host as string, - port: nodeData.inputs?.port as number, - username: user, // Required by TypeORMVectorStore - user: user, // Required by Pool in similaritySearchVectorWithScore - password: password, - database: nodeData.inputs?.database as string - } - - const args = { - postgresConnectionOptions: postgresConnectionOptions as DataSourceOptions, - tableName: tableName - } - - const vectorStore = await TypeORMVectorStore.fromDataSource(embeddings, args) - - // Rewrite the method to use pg pool connection instead of the default connection - /* Otherwise a connection error is displayed when the chain tries to execute the function - [chain/start] [1:chain:ConversationalRetrievalQAChain] Entering Chain run with input: { "question": "what the document is about", "chat_history": [] } - [retriever/start] [1:chain:ConversationalRetrievalQAChain > 2:retriever:VectorStoreRetriever] Entering Retriever run with input: { "query": "what the document is about" } - [ERROR]: uncaughtException: Illegal invocation TypeError: Illegal invocation at Socket.ref (node:net:1524:18) at Connection.ref (.../node_modules/pg/lib/connection.js:183:17) at Client.ref (.../node_modules/pg/lib/client.js:591:21) at BoundPool._pulseQueue (/node_modules/pg-pool/index.js:148:28) at .../node_modules/pg-pool/index.js:184:37 at process.processTicksAndRejections (node:internal/process/task_queues:77:11) - */ - vectorStore.similaritySearchVectorWithScore = async (query: number[], k: number, filter?: any) => { - return await similaritySearchVectorWithScore(query, k, tableName, postgresConnectionOptions, filter ?? pgMetadataFilter) - } + const vectorStore = await vectorStoreDriver.instanciate(pgMetadataFilter) if (output === 'retriever') { const retriever = vectorStore.asRetriever(k) @@ -361,51 +298,17 @@ class Postgres_VectorStores implements INode { } return vectorStore } -} - -const similaritySearchVectorWithScore = async ( - query: number[], - k: number, - tableName: string, - postgresConnectionOptions: ICommonObject, - filter?: any -) => { - const embeddingString = `[${query.join(',')}]` - let _filter = '{}' - let notExists = '' - if (filter && typeof filter === 'object') { - if (filter.$notexists) { - notExists = `OR NOT (metadata ? '${filter.$notexists}')` - delete filter.$notexists - } - _filter = JSON.stringify(filter) - } - - const queryString = ` - SELECT *, embedding <=> $1 as "_distance" - FROM ${tableName} - WHERE metadata @> $2 - ${notExists} - ORDER BY "_distance" ASC - LIMIT $3;` - - const pool = new Pool(postgresConnectionOptions) - const conn = await pool.connect() - const documents = await conn.query(queryString, [embeddingString, _filter, k]) - - conn.release() - - const results = [] as [TypeORMVectorStoreDocument, number][] - for (const doc of documents.rows) { - if (doc._distance != null && doc.pageContent != null) { - const document = new Document(doc) as TypeORMVectorStoreDocument - document.id = doc.id - results.push([document, doc._distance]) + static getDriverFromConfig(nodeData: INodeData, options: ICommonObject): VectorStoreDriver { + switch (nodeData.inputs?.driver) { + case 'typeorm': + return new TypeORMDriver(nodeData, options) + case 'pgvector': + return new PGVectorDriver(nodeData, options) + default: + return new TypeORMDriver(nodeData, options) } } - - return results } module.exports = { nodeClass: Postgres_VectorStores } diff --git a/packages/components/nodes/vectorstores/Postgres/README.md b/packages/components/nodes/vectorstores/Postgres/README.md new file mode 100644 index 00000000000..84e31b0a7c0 --- /dev/null +++ b/packages/components/nodes/vectorstores/Postgres/README.md @@ -0,0 +1,19 @@ +# Postgres Vector Store + +Postgres Vector Store integration for Flowise + +## 🌱 Env Variables + +| Variable | Description | Type | Default | +| ---------------------------- | ----------------------------------------------------------------------------------------------- | ------------------------------------------------ | ----------------------------------- | +| POSTGRES_VECTORSTORE_HOST | Default `host` for Postgres Vector Store | String | | +| POSTGRES_VECTORSTORE_PORT | Default `port` for Postgres Vector Store | Number | 5432 | +| POSTGRES_VECTORSTORE_USER | Default `user` for Postgres Vector Store | String | | +| POSTGRES_VECTORSTORE_PASSWORD | Default `password` for Postgres Vector Store | String | | +| POSTGRES_VECTORSTORE_DATABASE | Default `database` for Postgres Vector Store | String | | +| POSTGRES_VECTORSTORE_TABLE_NAME | Default `tableName` for Postgres Vector Store | String | documents | +| POSTGRES_VECTORSTORE_CONTENT_COLUMN_NAME | Default `contentColumnName` for Postgres Vector Store | String | pageContent | + +## License + +Source code in this repository is made available under the [Apache License Version 2.0](https://github.com/FlowiseAI/Flowise/blob/master/LICENSE.md). \ No newline at end of file diff --git a/packages/components/nodes/vectorstores/Postgres/driver/Base.ts b/packages/components/nodes/vectorstores/Postgres/driver/Base.ts new file mode 100644 index 00000000000..1616c71f7d4 --- /dev/null +++ b/packages/components/nodes/vectorstores/Postgres/driver/Base.ts @@ -0,0 +1,48 @@ +import { VectorStore } from '@langchain/core/vectorstores' +import { getCredentialData, getCredentialParam, ICommonObject, INodeData } from '../../../../src' +import { Document } from '@langchain/core/documents' +import { Embeddings } from '@langchain/core/embeddings' +import { getDatabase, getHost, getPort, getTableName } from '../utils' + +export abstract class VectorStoreDriver { + constructor(protected nodeData: INodeData, protected options: ICommonObject) {} + + abstract instanciate(metaDataFilters?: any): Promise + + abstract fromDocuments(documents: Document[]): Promise + + protected async adaptInstance(instance: VectorStore, _metaDataFilters?: any): Promise { + return instance + } + + getHost() { + return getHost(this.nodeData) as string + } + + getPort() { + return getPort(this.nodeData) as number + } + + getDatabase() { + return getDatabase(this.nodeData) as string + } + + getTableName() { + return getTableName(this.nodeData) + } + + getEmbeddings() { + return this.nodeData.inputs?.embeddings as Embeddings + } + + async getCredentials() { + const credentialData = await getCredentialData(this.nodeData.credential ?? '', this.options) + const user = getCredentialParam('user', credentialData, this.nodeData, process.env.POSTGRES_VECTORSTORE_USER) + const password = getCredentialParam('password', credentialData, this.nodeData, process.env.POSTGRES_VECTORSTORE_PASSWORD) + + return { + user, + password + } + } +} diff --git a/packages/components/nodes/vectorstores/Postgres/driver/PGVector.ts b/packages/components/nodes/vectorstores/Postgres/driver/PGVector.ts new file mode 100644 index 00000000000..39ec62ad9e7 --- /dev/null +++ b/packages/components/nodes/vectorstores/Postgres/driver/PGVector.ts @@ -0,0 +1,117 @@ +import { VectorStoreDriver } from './Base' +import { FLOWISE_CHATID } from '../../../../src' +import { DistanceStrategy, PGVectorStore, PGVectorStoreArgs } from '@langchain/community/vectorstores/pgvector' +import { Document } from '@langchain/core/documents' +import { PoolConfig } from 'pg' +import { getContentColumnName } from '../utils' + +export class PGVectorDriver extends VectorStoreDriver { + static CONTENT_COLUMN_NAME_DEFAULT: string = 'pageContent' + + protected _postgresConnectionOptions: PoolConfig + + protected async getPostgresConnectionOptions() { + if (!this._postgresConnectionOptions) { + const { user, password } = await this.getCredentials() + const additionalConfig = this.nodeData.inputs?.additionalConfig as string + + let additionalConfiguration = {} + + if (additionalConfig) { + try { + additionalConfiguration = typeof additionalConfig === 'object' ? additionalConfig : JSON.parse(additionalConfig) + } catch (exception) { + throw new Error('Invalid JSON in the Additional Configuration: ' + exception) + } + } + + this._postgresConnectionOptions = { + ...additionalConfiguration, + host: this.getHost(), + port: this.getPort(), + user: user, + password: password, + database: this.getDatabase() + } + } + + return this._postgresConnectionOptions + } + + async getArgs(): Promise { + return { + postgresConnectionOptions: await this.getPostgresConnectionOptions(), + tableName: this.getTableName(), + columns: { + contentColumnName: getContentColumnName(this.nodeData) + }, + distanceStrategy: (this.nodeData.inputs?.distanceStrategy || 'cosine') as DistanceStrategy + } + } + + async instanciate(metadataFilters?: any) { + return this.adaptInstance(await PGVectorStore.initialize(this.getEmbeddings(), await this.getArgs()), metadataFilters) + } + + async fromDocuments(documents: Document[]) { + const instance = await this.instanciate() + + await instance.addDocuments(documents) + + return this.adaptInstance(instance) + } + + protected async adaptInstance(instance: PGVectorStore, metadataFilters?: any): Promise { + const { [FLOWISE_CHATID]: chatId, ...pgMetadataFilter } = metadataFilters || {} + + const baseSimilaritySearchVectorWithScoreFn = instance.similaritySearchVectorWithScore.bind(instance) + + instance.similaritySearchVectorWithScore = async (query, k, filter) => { + return await baseSimilaritySearchVectorWithScoreFn(query, k, filter ?? pgMetadataFilter) + } + + const basePoolQueryFn = instance.pool.query.bind(instance.pool) + + // @ts-ignore + instance.pool.query = async (queryString: string, parameters: any[]) => { + if (!instance.client) { + instance.client = await instance.pool.connect() + } + + const whereClauseRegex = /WHERE ([^\n]+)/ + let chatflowOr = '' + + // Match chatflow uploaded file and keep filtering on other files: + // https://github.com/FlowiseAI/Flowise/pull/3367#discussion_r1804229295 + if (chatId) { + parameters.push({ [FLOWISE_CHATID]: chatId }) + + chatflowOr = `OR metadata @> $${parameters.length}` + } + + if (queryString.match(whereClauseRegex)) { + queryString = queryString.replace(whereClauseRegex, `WHERE (($1) AND NOT (metadata ? '${FLOWISE_CHATID}')) ${chatflowOr}`) + } else { + const orderByClauseRegex = /ORDER BY (.*)/ + // Insert WHERE clause before ORDER BY + queryString = queryString.replace( + orderByClauseRegex, + `WHERE (metadata @> '{}' AND NOT (metadata ? '${FLOWISE_CHATID}')) ${chatflowOr} + ORDER BY $1 + ` + ) + } + + // Run base function + const queryResult = await basePoolQueryFn(queryString, parameters) + + // ensure connection is released + instance.client.release() + instance.client = undefined + + return queryResult + } + + return instance + } +} diff --git a/packages/components/nodes/vectorstores/Postgres/driver/TypeORM.ts b/packages/components/nodes/vectorstores/Postgres/driver/TypeORM.ts new file mode 100644 index 00000000000..0217713b13f --- /dev/null +++ b/packages/components/nodes/vectorstores/Postgres/driver/TypeORM.ts @@ -0,0 +1,169 @@ +import { DataSourceOptions } from 'typeorm' +import { VectorStoreDriver } from './Base' +import { FLOWISE_CHATID, ICommonObject } from '../../../../src' +import { TypeORMVectorStore, TypeORMVectorStoreArgs, TypeORMVectorStoreDocument } from '@langchain/community/vectorstores/typeorm' +import { VectorStore } from '@langchain/core/vectorstores' +import { Document } from '@langchain/core/documents' +import { Pool } from 'pg' + +export class TypeORMDriver extends VectorStoreDriver { + protected _postgresConnectionOptions: DataSourceOptions + + protected async getPostgresConnectionOptions() { + if (!this._postgresConnectionOptions) { + const { user, password } = await this.getCredentials() + const additionalConfig = this.nodeData.inputs?.additionalConfig as string + + let additionalConfiguration = {} + + if (additionalConfig) { + try { + additionalConfiguration = typeof additionalConfig === 'object' ? additionalConfig : JSON.parse(additionalConfig) + } catch (exception) { + throw new Error('Invalid JSON in the Additional Configuration: ' + exception) + } + } + + this._postgresConnectionOptions = { + ...additionalConfiguration, + type: 'postgres', + host: this.getHost(), + port: this.getPort(), + username: user, // Required by TypeORMVectorStore + user: user, // Required by Pool in similaritySearchVectorWithScore + password: password, + database: this.getDatabase() + } as DataSourceOptions + } + return this._postgresConnectionOptions + } + + async getArgs(): Promise { + return { + postgresConnectionOptions: await this.getPostgresConnectionOptions(), + tableName: this.getTableName() + } + } + + async instanciate(metadataFilters?: any) { + return this.adaptInstance(await TypeORMVectorStore.fromDataSource(this.getEmbeddings(), await this.getArgs()), metadataFilters) + } + + async fromDocuments(documents: Document[]) { + return this.adaptInstance(await TypeORMVectorStore.fromDocuments(documents, this.getEmbeddings(), await this.getArgs())) + } + + sanitizeDocuments(documents: Document[]) { + // Remove NULL characters which triggers error on PG + for (var i in documents) { + documents[i].pageContent = documents[i].pageContent.replace(/\0/g, '') + } + + return documents + } + + protected async adaptInstance(instance: TypeORMVectorStore, metadataFilters?: any): Promise { + const tableName = this.getTableName() + + // Rewrite the method to use pg pool connection instead of the default connection + /* Otherwise a connection error is displayed when the chain tries to execute the function + [chain/start] [1:chain:ConversationalRetrievalQAChain] Entering Chain run with input: { "question": "what the document is about", "chat_history": [] } + [retriever/start] [1:chain:ConversationalRetrievalQAChain > 2:retriever:VectorStoreRetriever] Entering Retriever run with input: { "query": "what the document is about" } + [ERROR]: uncaughtException: Illegal invocation TypeError: Illegal invocation at Socket.ref (node:net:1524:18) at Connection.ref (.../node_modules/pg/lib/connection.js:183:17) at Client.ref (.../node_modules/pg/lib/client.js:591:21) at BoundPool._pulseQueue (/node_modules/pg-pool/index.js:148:28) at .../node_modules/pg-pool/index.js:184:37 at process.processTicksAndRejections (node:internal/process/task_queues:77:11) + */ + instance.similaritySearchVectorWithScore = async (query: number[], k: number, filter?: any) => { + return await TypeORMDriver.similaritySearchVectorWithScore( + query, + k, + tableName, + await this.getPostgresConnectionOptions(), + filter ?? metadataFilters, + this.computedOperatorString + ) + } + + instance.delete = async (params: { ids: string[] }): Promise => { + const { ids } = params + + if (ids?.length) { + try { + instance.appDataSource.getRepository(instance.documentEntity).delete(ids) + } catch (e) { + console.error('Failed to delete') + } + } + } + + const baseAddVectorsFn = instance.addVectors.bind(instance) + + instance.addVectors = async (vectors, documents) => { + return baseAddVectorsFn(vectors, this.sanitizeDocuments(documents)) + } + + return instance + } + + get computedOperatorString() { + const { distanceStrategy = 'cosine' } = this.nodeData.inputs || {} + + switch (distanceStrategy) { + case 'cosine': + return '<=>' + case 'innerProduct': + return '<#>' + case 'euclidean': + return '<->' + default: + throw new Error(`Unknown distance strategy: ${distanceStrategy}`) + } + } + + static similaritySearchVectorWithScore = async ( + query: number[], + k: number, + tableName: string, + postgresConnectionOptions: ICommonObject, + filter?: any, + distanceOperator: string = '<=>' + ) => { + const embeddingString = `[${query.join(',')}]` + let chatflowOr = '' + const { [FLOWISE_CHATID]: chatId, ...restFilters } = filter || {} + + const _filter = JSON.stringify(restFilters || {}) + const parameters: any[] = [embeddingString, _filter, k] + + // Match chatflow uploaded file and keep filtering on other files: + // https://github.com/FlowiseAI/Flowise/pull/3367#discussion_r1804229295 + if (chatId) { + parameters.push({ [FLOWISE_CHATID]: chatId }) + chatflowOr = `OR metadata @> $${parameters.length}` + } + + const queryString = ` + SELECT *, embedding ${distanceOperator} $1 as "_distance" + FROM ${tableName} + WHERE ((metadata @> $2) AND NOT (metadata ? '${FLOWISE_CHATID}')) ${chatflowOr} + ORDER BY "_distance" ASC + LIMIT $3;` + + const pool = new Pool(postgresConnectionOptions) + + const conn = await pool.connect() + + const documents = await conn.query(queryString, parameters) + + conn.release() + + const results = [] as [TypeORMVectorStoreDocument, number][] + for (const doc of documents.rows) { + if (doc._distance != null && doc.pageContent != null) { + const document = new Document(doc) as TypeORMVectorStoreDocument + document.id = doc.id + results.push([document, doc._distance]) + } + } + + return results + } +} diff --git a/packages/components/nodes/vectorstores/Postgres/utils.ts b/packages/components/nodes/vectorstores/Postgres/utils.ts new file mode 100644 index 00000000000..e2b18b57075 --- /dev/null +++ b/packages/components/nodes/vectorstores/Postgres/utils.ts @@ -0,0 +1,21 @@ +import { defaultChain, INodeData } from '../../../src' + +export function getHost(nodeData?: INodeData) { + return defaultChain(nodeData?.inputs?.host, process.env.POSTGRES_VECTORSTORE_HOST) +} + +export function getDatabase(nodeData?: INodeData) { + return defaultChain(nodeData?.inputs?.database, process.env.POSTGRES_VECTORSTORE_DATABASE) +} + +export function getPort(nodeData?: INodeData) { + return defaultChain(nodeData?.inputs?.port, process.env.POSTGRES_VECTORSTORE_PORT, '5432') +} + +export function getTableName(nodeData?: INodeData) { + return defaultChain(nodeData?.inputs?.tableName, process.env.POSTGRES_VECTORSTORE_TABLE_NAME, 'documents') +} + +export function getContentColumnName(nodeData?: INodeData) { + return defaultChain(nodeData?.inputs?.contentColumnName, process.env.POSTGRES_VECTORSTORE_CONTENT_COLUMN_NAME, 'pageContent') +} diff --git a/packages/components/package.json b/packages/components/package.json index 288cc300c1e..e00e464af9d 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "flowise-components", - "version": "2.1.2", + "version": "2.1.3", "description": "Flowiseai Components", "main": "dist/src/index", "types": "dist/src/index.d.ts", @@ -69,6 +69,7 @@ "assemblyai": "^4.2.2", "axios": "1.6.2", "cheerio": "^1.0.0-rc.12", + "couchbase": "4.4.1", "chromadb": "^1.5.11", "cohere-ai": "^7.7.5", "crypto-js": "^4.1.1", diff --git a/packages/components/src/handler.ts b/packages/components/src/handler.ts index e6959d5511f..b97648dac59 100644 --- a/packages/components/src/handler.ts +++ b/packages/components/src/handler.ts @@ -243,12 +243,14 @@ class ExtendedLunaryHandler extends LunaryHandler { databaseEntities: IDatabaseEntity currentRunId: string | null thread: any + apiMessageId: string constructor({ flowiseOptions, ...options }: any) { super(options) this.appDataSource = flowiseOptions.appDataSource this.databaseEntities = flowiseOptions.databaseEntities this.chatId = flowiseOptions.chatId + this.apiMessageId = flowiseOptions.apiMessageId } async initThread() { @@ -258,14 +260,18 @@ class ExtendedLunaryHandler extends LunaryHandler { } }) + const userId = entity?.email ?? entity?.id + this.thread = lunary.openThread({ id: this.chatId, - userId: entity?.email ?? entity?.id, - userProps: { - name: entity?.name ?? undefined, - email: entity?.email ?? undefined, - phone: entity?.phone ?? undefined - } + userId, + userProps: userId + ? { + name: entity?.name ?? undefined, + email: entity?.email ?? undefined, + phone: entity?.phone ?? undefined + } + : undefined }) } @@ -298,6 +304,7 @@ class ExtendedLunaryHandler extends LunaryHandler { const answer = outputs.output this.thread.trackMessage({ + id: this.apiMessageId, content: answer, role: 'assistant' }) diff --git a/packages/components/src/utils.ts b/packages/components/src/utils.ts index 387cca64f1c..64b20a47f33 100644 --- a/packages/components/src/utils.ts +++ b/packages/components/src/utils.ts @@ -5,7 +5,7 @@ import * as path from 'path' import { JSDOM } from 'jsdom' import { z } from 'zod' import { DataSource } from 'typeorm' -import { ICommonObject, IDatabaseEntity, IMessage, INodeData, IVariable, MessageContentImageUrl } from './Interface' +import { ICommonObject, IDatabaseEntity, IDocument, IMessage, INodeData, IVariable, MessageContentImageUrl } from './Interface' import { AES, enc } from 'crypto-js' import { AIMessage, HumanMessage, BaseMessage } from '@langchain/core/messages' import { getFileFromStorage } from './storageUtils' @@ -542,8 +542,19 @@ export const getCredentialData = async (selectedCredentialId: string, options: I } } -export const getCredentialParam = (paramName: string, credentialData: ICommonObject, nodeData: INodeData): any => { - return (nodeData.inputs as ICommonObject)[paramName] ?? credentialData[paramName] ?? undefined +/** + * Get first non falsy value + * + * @param {...any} values + * + * @returns {any|undefined} + */ +export const defaultChain = (...values: any[]): any | undefined => { + return values.filter(Boolean)[0] +} + +export const getCredentialParam = (paramName: string, credentialData: ICommonObject, nodeData: INodeData, defaultValue?: any): any => { + return (nodeData.inputs as ICommonObject)[paramName] ?? credentialData[paramName] ?? defaultValue ?? undefined } // reference https://www.freeformatter.com/json-escape.html @@ -609,10 +620,11 @@ export const mapChatMessageToBaseMessage = async (chatmessages: any[] = []): Pro if (message.role === 'apiMessage' || message.type === 'apiMessage') { chatHistory.push(new AIMessage(message.content || '')) } else if (message.role === 'userMessage' || message.role === 'userMessage') { - // check for image uploads + // check for image/files uploads if (message.fileUploads) { // example: [{"type":"stored-file","name":"0_DiXc4ZklSTo3M8J4.jpg","mime":"image/jpeg"}] try { + let messageWithFileUploads = '' const uploads = JSON.parse(message.fileUploads) const imageContents: MessageContentImageUrl[] = [] for (const upload of uploads) { @@ -634,14 +646,32 @@ export const mapChatMessageToBaseMessage = async (chatmessages: any[] = []): Pro url: upload.data } }) + } else if (upload.type === 'stored-file:full') { + const fileLoaderNodeModule = await import('../nodes/documentloaders/File/File') + // @ts-ignore + const fileLoaderNodeInstance = new fileLoaderNodeModule.nodeClass() + const options = { + retrieveAttachmentChatId: true, + chatflowid: message.chatflowid, + chatId: message.chatId + } + const nodeData = { + inputs: { + txtFile: `FILE-STORAGE::${JSON.stringify([upload.name])}` + } + } + const documents: IDocument[] = await fileLoaderNodeInstance.init(nodeData, '', options) + const pageContents = documents.map((doc) => doc.pageContent).join('\n') + messageWithFileUploads += `${pageContents}\n\n` } } + const messageContent = messageWithFileUploads ? `${messageWithFileUploads}\n\n${message.content}` : message.content chatHistory.push( new HumanMessage({ content: [ { type: 'text', - text: message.content + text: messageContent }, ...imageContents ] diff --git a/packages/server/.env.example b/packages/server/.env.example index 556d5fb5485..47148a4235b 100644 --- a/packages/server/.env.example +++ b/packages/server/.env.example @@ -52,3 +52,9 @@ PORT=3000 # APIKEY_STORAGE_TYPE=json (json | db) # SHOW_COMMUNITY_NODES=true + +# Uncomment the following lines to enable global agent proxy +# see https://www.npmjs.com/package/global-agent for more details +# GLOBAL_AGENT_HTTP_PROXY=CorporateHttpProxyUrl +# GLOBAL_AGENT_HTTPS_PROXY=CorporateHttpsProxyUrl +# GLOBAL_AGENT_NO_PROXY=ExceptionHostsToBypassProxyIfNeeded \ No newline at end of file diff --git a/packages/server/package.json b/packages/server/package.json index 9379bea452e..a77cd561237 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "flowise", - "version": "2.1.2", + "version": "2.1.3", "description": "Flowiseai Server", "main": "dist/index", "types": "dist/index.d.ts", @@ -85,7 +85,8 @@ "sqlite3": "^5.1.6", "typeorm": "^0.3.6", "uuid": "^9.0.1", - "winston": "^3.9.0" + "winston": "^3.9.0", + "global-agent": "^3.0.0" }, "devDependencies": { "@types/content-disposition": "0.5.8", diff --git a/packages/server/src/Interface.ts b/packages/server/src/Interface.ts index 04ac2519d05..c8b981d5015 100644 --- a/packages/server/src/Interface.ts +++ b/packages/server/src/Interface.ts @@ -4,7 +4,7 @@ export type MessageType = 'apiMessage' | 'userMessage' export type ChatflowType = 'CHATFLOW' | 'MULTIAGENT' -export enum chatType { +export enum ChatType { INTERNAL = 'INTERNAL', EXTERNAL = 'EXTERNAL' } diff --git a/packages/server/src/controllers/attachments/index.ts b/packages/server/src/controllers/attachments/index.ts new file mode 100644 index 00000000000..9f87f5eaf48 --- /dev/null +++ b/packages/server/src/controllers/attachments/index.ts @@ -0,0 +1,15 @@ +import { Request, Response, NextFunction } from 'express' +import attachmentsService from '../../services/attachments' + +const createAttachment = async (req: Request, res: Response, next: NextFunction) => { + try { + const apiResponse = await attachmentsService.createAttachment(req) + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +export default { + createAttachment +} diff --git a/packages/server/src/controllers/chat-messages/index.ts b/packages/server/src/controllers/chat-messages/index.ts index 903a313bc7b..0e914a474c7 100644 --- a/packages/server/src/controllers/chat-messages/index.ts +++ b/packages/server/src/controllers/chat-messages/index.ts @@ -1,13 +1,36 @@ import { Request, Response, NextFunction } from 'express' -import { ChatMessageRatingType, 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' +import { aMonthAgo, clearSessionMemory, setDateToStartOrEndOfDay } from '../../utils' import { getRunningExpressApp } from '../../utils/getRunningExpressApp' -import { FindOptionsWhere } from 'typeorm' +import { Between, FindOptionsWhere } from 'typeorm' import { ChatMessage } from '../../database/entities/ChatMessage' import { InternalFlowiseError } from '../../errors/internalFlowiseError' import { StatusCodes } from 'http-status-codes' +import { utilGetChatMessage } from '../../utils/getChatMessage' + +const getFeedbackTypeFilters = (_feedbackTypeFilters: ChatMessageRatingType[]): ChatMessageRatingType[] | undefined => { + try { + let feedbackTypeFilters + 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 + } + return feedbackTypeFilters + } catch (e) { + return _feedbackTypeFilters + } +} const createChatMessage = async (req: Request, res: Response, next: NextFunction) => { try { @@ -26,16 +49,16 @@ const createChatMessage = async (req: Request, res: Response, next: NextFunction const getAllChatMessages = async (req: Request, res: Response, next: NextFunction) => { try { - let chatTypeFilter = req.query?.chatType as chatType | undefined + let chatTypeFilter = req.query?.chatType as ChatType | undefined if (chatTypeFilter) { try { const chatTypeFilterArray = JSON.parse(chatTypeFilter) - if (chatTypeFilterArray.includes(chatType.EXTERNAL) && chatTypeFilterArray.includes(chatType.INTERNAL)) { + if (chatTypeFilterArray.includes(ChatType.EXTERNAL) && chatTypeFilterArray.includes(ChatType.INTERNAL)) { chatTypeFilter = undefined - } else if (chatTypeFilterArray.includes(chatType.EXTERNAL)) { - chatTypeFilter = chatType.EXTERNAL - } else if (chatTypeFilterArray.includes(chatType.INTERNAL)) { - chatTypeFilter = chatType.INTERNAL + } else if (chatTypeFilterArray.includes(ChatType.EXTERNAL)) { + chatTypeFilter = ChatType.EXTERNAL + } else if (chatTypeFilterArray.includes(ChatType.INTERNAL)) { + chatTypeFilter = ChatType.INTERNAL } } catch (e) { return res.status(500).send(e) @@ -51,23 +74,7 @@ const getAllChatMessages = async (req: Request, res: Response, next: NextFunctio 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) - } + feedbackTypeFilters = getFeedbackTypeFilters(feedbackTypeFilters) } if (typeof req.params === 'undefined' || !req.params.id) { throw new InternalFlowiseError( @@ -105,9 +112,13 @@ const getAllInternalChatMessages = async (req: Request, res: Response, next: Nex 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) { + feedbackTypeFilters = getFeedbackTypeFilters(feedbackTypeFilters) + } const apiResponse = await chatMessagesService.getAllInternalChatMessages( req.params.id, - chatType.INTERNAL, + ChatType.INTERNAL, sortOrder, chatId, memoryType, @@ -115,7 +126,8 @@ const getAllInternalChatMessages = async (req: Request, res: Response, next: Nex startDate, endDate, messageId, - feedback + feedback, + feedbackTypeFilters ) return res.json(parseAPIResponse(apiResponse)) } catch (error) { @@ -123,7 +135,6 @@ const getAllInternalChatMessages = async (req: Request, res: Response, next: Nex } } -//Delete all chatmessages from chatId const removeAllChatMessages = async (req: Request, res: Response, next: NextFunction) => { try { const appServer = getRunningExpressApp() @@ -138,35 +149,102 @@ const removeAllChatMessages = async (req: Request, res: Response, next: NextFunc if (!chatflow) { return res.status(404).send(`Chatflow ${req.params.id} not found`) } + const flowData = chatflow.flowData + const parsedFlowData: IReactFlowObject = JSON.parse(flowData) + const nodes = parsedFlowData.nodes const chatId = req.query?.chatId as string const memoryType = req.query?.memoryType as string | undefined const sessionId = req.query?.sessionId as string | undefined - const chatType = req.query?.chatType as string | undefined + const _chatType = req.query?.chatType as string | undefined + const startDate = req.query?.startDate as string | undefined + const endDate = req.query?.endDate as string | undefined const isClearFromViewMessageDialog = req.query?.isClearFromViewMessageDialog as string | undefined - const flowData = chatflow.flowData - const parsedFlowData: IReactFlowObject = JSON.parse(flowData) - const nodes = parsedFlowData.nodes - try { - await clearSessionMemory( - nodes, - appServer.nodesPool.componentNodes, - chatId, - appServer.AppDataSource, - sessionId, - memoryType, - isClearFromViewMessageDialog - ) - } catch (e) { - return res.status(500).send('Error clearing chat messages') + let feedbackTypeFilters = req.query?.feedbackType as ChatMessageRatingType[] | undefined + if (feedbackTypeFilters) { + feedbackTypeFilters = getFeedbackTypeFilters(feedbackTypeFilters) } - const deleteOptions: FindOptionsWhere = { chatflowid } - if (chatId) deleteOptions.chatId = chatId - if (memoryType) deleteOptions.memoryType = memoryType - if (sessionId) deleteOptions.sessionId = sessionId - if (chatType) deleteOptions.chatType = chatType - const apiResponse = await chatMessagesService.removeAllChatMessages(chatId, chatflowid, deleteOptions) - return res.json(apiResponse) + if (!chatId) { + const isFeedback = feedbackTypeFilters?.length ? true : false + const hardDelete = req.query?.hardDelete as boolean | undefined + const messages = await utilGetChatMessage( + chatflowid, + _chatType as ChatType | undefined, + undefined, + undefined, + undefined, + undefined, + startDate, + endDate, + undefined, + isFeedback, + feedbackTypeFilters + ) + const messageIds = messages.map((message) => message.id) + + // Categorize by chatId_memoryType_sessionId + const chatIdMap = new Map() + messages.forEach((message) => { + const chatId = message.chatId + const memoryType = message.memoryType + const sessionId = message.sessionId + const composite_key = `${chatId}_${memoryType}_${sessionId}` + if (!chatIdMap.has(composite_key)) { + chatIdMap.set(composite_key, []) + } + chatIdMap.get(composite_key)?.push(message) + }) + + // If hardDelete is ON, we clearSessionMemory from third party integrations + if (hardDelete) { + for (const [composite_key] of chatIdMap) { + const [chatId, memoryType, sessionId] = composite_key.split('_') + try { + await clearSessionMemory( + nodes, + appServer.nodesPool.componentNodes, + chatId, + appServer.AppDataSource, + sessionId, + memoryType, + isClearFromViewMessageDialog + ) + } catch (e) { + console.error('Error clearing chat messages') + } + } + } + + const apiResponse = await chatMessagesService.removeChatMessagesByMessageIds(chatflowid, chatIdMap, messageIds) + return res.json(apiResponse) + } else { + try { + await clearSessionMemory( + nodes, + appServer.nodesPool.componentNodes, + chatId, + appServer.AppDataSource, + sessionId, + memoryType, + isClearFromViewMessageDialog + ) + } catch (e) { + return res.status(500).send('Error clearing chat messages') + } + + const deleteOptions: FindOptionsWhere = { chatflowid } + if (chatId) deleteOptions.chatId = chatId + if (memoryType) deleteOptions.memoryType = memoryType + if (sessionId) deleteOptions.sessionId = sessionId + if (_chatType) deleteOptions.chatType = _chatType + if (startDate && endDate) { + const fromDate = setDateToStartOrEndOfDay(startDate, 'start') + const toDate = setDateToStartOrEndOfDay(endDate, 'end') + deleteOptions.createdDate = Between(fromDate ?? aMonthAgo(), toDate ?? new Date()) + } + const apiResponse = await chatMessagesService.removeAllChatMessages(chatId, chatflowid, deleteOptions) + return res.json(apiResponse) + } } catch (error) { next(error) } diff --git a/packages/server/src/controllers/openai-realtime/index.ts b/packages/server/src/controllers/openai-realtime/index.ts index b5a504ed2c4..a16ea3db50c 100644 --- a/packages/server/src/controllers/openai-realtime/index.ts +++ b/packages/server/src/controllers/openai-realtime/index.ts @@ -54,7 +54,8 @@ const executeAgentTool = async (req: Request, res: Response, next: NextFunction) req.params.id, req.body.chatId, req.body.toolName, - req.body.inputArgs + req.body.inputArgs, + req.body.apiMessageId ) return res.json(apiResponse) } catch (error) { diff --git a/packages/server/src/controllers/stats/index.ts b/packages/server/src/controllers/stats/index.ts index a96464514f5..20a0bd7fa82 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 { ChatMessageRatingType, chatType } from '../../Interface' +import { ChatMessageRatingType, ChatType } from '../../Interface' import { InternalFlowiseError } from '../../errors/internalFlowiseError' import { getErrorMessage } from '../../errors/utils' @@ -11,19 +11,19 @@ const getChatflowStats = async (req: Request, res: Response, next: NextFunction) throw new InternalFlowiseError(StatusCodes.PRECONDITION_FAILED, `Error: statsController.getChatflowStats - id not provided!`) } const chatflowid = req.params.id - let chatTypeFilter = req.query?.chatType as chatType | undefined + 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) - if (chatTypeFilterArray.includes(chatType.EXTERNAL) && chatTypeFilterArray.includes(chatType.INTERNAL)) { + if (chatTypeFilterArray.includes(ChatType.EXTERNAL) && chatTypeFilterArray.includes(ChatType.INTERNAL)) { chatTypeFilter = undefined - } else if (chatTypeFilterArray.includes(chatType.EXTERNAL)) { - chatTypeFilter = chatType.EXTERNAL - } else if (chatTypeFilterArray.includes(chatType.INTERNAL)) { - chatTypeFilter = chatType.INTERNAL + } else if (chatTypeFilterArray.includes(ChatType.EXTERNAL)) { + chatTypeFilter = ChatType.EXTERNAL + } else if (chatTypeFilterArray.includes(ChatType.INTERNAL)) { + chatTypeFilter = ChatType.INTERNAL } } catch (e) { throw new InternalFlowiseError( diff --git a/packages/server/src/database/migrations/mariadb/1715861032479-AddVectorStoreConfigToDocStore.ts b/packages/server/src/database/migrations/mariadb/1715861032479-AddVectorStoreConfigToDocStore.ts new file mode 100644 index 00000000000..c5d5422751d --- /dev/null +++ b/packages/server/src/database/migrations/mariadb/1715861032479-AddVectorStoreConfigToDocStore.ts @@ -0,0 +1,18 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddVectorStoreConfigToDocStore1715861032479 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + const columnExists = await queryRunner.hasColumn('document_store', 'vectorStoreConfig') + if (!columnExists) { + await queryRunner.query(`ALTER TABLE \`document_store\` ADD COLUMN \`vectorStoreConfig\` TEXT;`) + await queryRunner.query(`ALTER TABLE \`document_store\` ADD COLUMN \`embeddingConfig\` TEXT;`) + await queryRunner.query(`ALTER TABLE \`document_store\` ADD COLUMN \`recordManagerConfig\` TEXT;`) + } + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`document_store\` DROP COLUMN \`vectorStoreConfig\`;`) + await queryRunner.query(`ALTER TABLE \`document_store\` DROP COLUMN \`embeddingConfig\`;`) + await queryRunner.query(`ALTER TABLE \`document_store\` DROP COLUMN \`recordManagerConfig\`;`) + } +} diff --git a/packages/server/src/database/migrations/mariadb/index.ts b/packages/server/src/database/migrations/mariadb/index.ts index 6c0f6274d17..cf246a644a4 100644 --- a/packages/server/src/database/migrations/mariadb/index.ts +++ b/packages/server/src/database/migrations/mariadb/index.ts @@ -17,6 +17,7 @@ import { AddFeedback1707213626553 } from './1707213626553-AddFeedback' import { AddUpsertHistoryEntity1709814301358 } from './1709814301358-AddUpsertHistoryEntity' import { AddLead1710832127079 } from './1710832127079-AddLead' import { AddLeadToChatMessage1711538023578 } from './1711538023578-AddLeadToChatMessage' +import { AddVectorStoreConfigToDocStore1715861032479 } from './1715861032479-AddVectorStoreConfigToDocStore' import { AddDocumentStore1711637331047 } from './1711637331047-AddDocumentStore' import { AddAgentReasoningToChatMessage1714679514451 } from './1714679514451-AddAgentReasoningToChatMessage' import { AddTypeToChatFlow1716300000000 } from './1716300000000-AddTypeToChatFlow' @@ -50,6 +51,7 @@ export const mariadbMigrations = [ AddLeadToChatMessage1711538023578, AddAgentReasoningToChatMessage1714679514451, AddTypeToChatFlow1716300000000, + AddVectorStoreConfigToDocStore1715861032479, AddApiKey1720230151480, AddActionToChatMessage1721078251523, LongTextColumn1722301395521, diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 5285e5d28f2..a5fb40b9fca 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -22,6 +22,7 @@ import flowiseApiV1Router from './routes' import errorHandlerMiddleware from './middlewares/errors' import { SSEStreamer } from './utils/SSEStreamer' import { validateAPIKey } from './utils/validateKey' +import 'global-agent/bootstrap' declare global { namespace Express { @@ -136,7 +137,8 @@ export class App { '/api/v1/get-upload-file', '/api/v1/ip', '/api/v1/ping', - '/api/v1/version' + '/api/v1/version', + '/api/v1/attachments' ] const URL_CASE_INSENSITIVE_REGEX: RegExp = /\/api\/v1\//i const URL_CASE_SENSITIVE_REGEX: RegExp = /\/api\/v1\// diff --git a/packages/server/src/routes/attachments/index.ts b/packages/server/src/routes/attachments/index.ts new file mode 100644 index 00000000000..abe09a6c89c --- /dev/null +++ b/packages/server/src/routes/attachments/index.ts @@ -0,0 +1,13 @@ +import express from 'express' +import multer from 'multer' +import path from 'path' +import attachmentsController from '../../controllers/attachments' + +const router = express.Router() + +const upload = multer({ dest: `${path.join(__dirname, '..', '..', '..', 'uploads')}/` }) + +// CREATE +router.post('/:chatflowId/:chatId', upload.array('files'), attachmentsController.createAttachment) + +export default router diff --git a/packages/server/src/routes/index.ts b/packages/server/src/routes/index.ts index 3df0dd30438..89fb7350a87 100644 --- a/packages/server/src/routes/index.ts +++ b/packages/server/src/routes/index.ts @@ -1,6 +1,7 @@ import express from 'express' import apikeyRouter from './apikey' import assistantsRouter from './assistants' +import attachmentsRouter from './attachments' import chatMessageRouter from './chat-messages' import chatflowsRouter from './chatflows' import chatflowsStreamingRouter from './chatflows-streaming' @@ -47,6 +48,7 @@ const router = express.Router() router.use('/ping', pingRouter) router.use('/apikey', apikeyRouter) router.use('/assistants', assistantsRouter) +router.use('/attachments', attachmentsRouter) router.use('/chatflows', chatflowsRouter) router.use('/chatflows-streaming', chatflowsStreamingRouter) router.use('/chatmessage', chatMessageRouter) diff --git a/packages/server/src/services/attachments/index.ts b/packages/server/src/services/attachments/index.ts new file mode 100644 index 00000000000..ea9814da534 --- /dev/null +++ b/packages/server/src/services/attachments/index.ts @@ -0,0 +1,20 @@ +import { Request } from 'express' +import { StatusCodes } from 'http-status-codes' +import { createFileAttachment } from '../../utils/createAttachment' +import { InternalFlowiseError } from '../../errors/internalFlowiseError' +import { getErrorMessage } from '../../errors/utils' + +const createAttachment = async (req: Request) => { + try { + return await createFileAttachment(req) + } catch (error) { + throw new InternalFlowiseError( + StatusCodes.INTERNAL_SERVER_ERROR, + `Error: attachmentService.createAttachment - ${getErrorMessage(error)}` + ) + } +} + +export default { + createAttachment +} diff --git a/packages/server/src/services/chat-messages/index.ts b/packages/server/src/services/chat-messages/index.ts index 9f2d53f7e03..621b37d1a72 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 { ChatMessageRatingType, 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' @@ -27,7 +27,7 @@ const createChatMessage = async (chatMessage: Partial) => { // Get all chatmessages from chatflowid const getAllChatMessages = async ( chatflowId: string, - chatTypeFilter: chatType | undefined, + chatTypeFilter: ChatType | undefined, sortOrder: string = 'ASC', chatId?: string, memoryType?: string, @@ -64,7 +64,7 @@ const getAllChatMessages = async ( // Get internal chatmessages from chatflowid const getAllInternalChatMessages = async ( chatflowId: string, - chatTypeFilter: chatType | undefined, + chatTypeFilter: ChatType | undefined, sortOrder: string = 'ASC', chatId?: string, memoryType?: string, @@ -128,6 +128,35 @@ const removeAllChatMessages = async ( } } +const removeChatMessagesByMessageIds = async ( + chatflowid: string, + chatIdMap: Map, + messageIds: string[] +): Promise => { + try { + const appServer = getRunningExpressApp() + + for (const [composite_key] of chatIdMap) { + const [chatId] = composite_key.split('_') + + // Remove all related feedback records + const feedbackDeleteOptions: FindOptionsWhere = { chatId } + await appServer.AppDataSource.getRepository(ChatMessageFeedback).delete(feedbackDeleteOptions) + + // Delete all uploads corresponding to this chatflow/chatId + await removeFilesFromStorage(chatflowid, chatId) + } + + const dbResponse = await appServer.AppDataSource.getRepository(ChatMessage).delete(messageIds) + return dbResponse + } catch (error) { + throw new InternalFlowiseError( + StatusCodes.INTERNAL_SERVER_ERROR, + `Error: chatMessagesService.removeAllChatMessages - ${getErrorMessage(error)}` + ) + } +} + const abortChatMessage = async (chatId: string, chatflowid: string) => { try { const appServer = getRunningExpressApp() @@ -155,5 +184,6 @@ export default { getAllChatMessages, getAllInternalChatMessages, removeAllChatMessages, + removeChatMessagesByMessageIds, abortChatMessage } diff --git a/packages/server/src/services/documentstore/index.ts b/packages/server/src/services/documentstore/index.ts index 53b3cefb263..d5ef3dd9ea1 100644 --- a/packages/server/src/services/documentstore/index.ts +++ b/packages/server/src/services/documentstore/index.ts @@ -9,7 +9,7 @@ import { removeSpecificFileFromStorage } from 'flowise-components' import { - chatType, + ChatType, DocumentStoreStatus, IDocumentStoreFileChunkPagedResponse, IDocumentStoreLoader, @@ -995,7 +995,7 @@ const _insertIntoVectorStoreWorkerThread = async (data: ICommonObject) => { data: { version: await getAppVersion(), chatlowId: chatflowid, - type: chatType.INTERNAL, + type: ChatType.INTERNAL, flowGraph: omit(indexResult['result'], ['totalKeys', 'addedDocs']) } }) @@ -1029,7 +1029,9 @@ const getEmbeddingProviders = async () => { const getVectorStoreProviders = async () => { try { const dbResponse = await nodesService.getAllNodesForCategory('Vector Stores') - return dbResponse.filter((node) => !node.tags?.includes('LlamaIndex') && node.name !== 'documentStoreVS') + return dbResponse.filter( + (node) => !node.tags?.includes('LlamaIndex') && node.name !== 'documentStoreVS' && node.name !== 'memoryVectorStore' + ) } catch (error) { throw new InternalFlowiseError( StatusCodes.INTERNAL_SERVER_ERROR, diff --git a/packages/server/src/services/openai-realtime/index.ts b/packages/server/src/services/openai-realtime/index.ts index 3a9249be329..cdc02a11e05 100644 --- a/packages/server/src/services/openai-realtime/index.ts +++ b/packages/server/src/services/openai-realtime/index.ts @@ -12,7 +12,7 @@ import { v4 as uuidv4 } from 'uuid' const SOURCE_DOCUMENTS_PREFIX = '\n\n----FLOWISE_SOURCE_DOCUMENTS----\n\n' const ARTIFACTS_PREFIX = '\n\n----FLOWISE_ARTIFACTS----\n\n' -const buildAndInitTool = async (chatflowid: string, _chatId?: string) => { +const buildAndInitTool = async (chatflowid: string, _chatId?: string, _apiMessageId?: string) => { const appServer = getRunningExpressApp() const chatflow = await appServer.AppDataSource.getRepository(ChatFlow).findOneBy({ id: chatflowid @@ -22,6 +22,7 @@ const buildAndInitTool = async (chatflowid: string, _chatId?: string) => { } const chatId = _chatId || uuidv4() + const apiMessageId = _apiMessageId || uuidv4() const flowData = JSON.parse(chatflow.flowData) const nodes = flowData.nodes const edges = flowData.edges @@ -62,6 +63,7 @@ const buildAndInitTool = async (chatflowid: string, _chatId?: string) => { chatId: chatId, sessionId: chatId, chatflowid, + apiMessageId, appDataSource: appServer.AppDataSource }) @@ -113,9 +115,15 @@ const getAgentTools = async (chatflowid: string): Promise => { } } -const executeAgentTool = async (chatflowid: string, chatId: string, toolName: string, inputArgs: string): Promise => { +const executeAgentTool = async ( + chatflowid: string, + chatId: string, + toolName: string, + inputArgs: string, + apiMessageId?: string +): Promise => { try { - const agent = await buildAndInitTool(chatflowid, chatId) + const agent = await buildAndInitTool(chatflowid, chatId, apiMessageId) const tools = agent.tools const tool = tools.find((tool: any) => tool.name === toolName) diff --git a/packages/server/src/services/stats/index.ts b/packages/server/src/services/stats/index.ts index 8d9b99da524..c4c0e52887c 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 { ChatMessageRatingType, 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' @@ -9,7 +9,7 @@ import { getErrorMessage } from '../../errors/utils' // get stats for showing in chatflow const getChatflowStats = async ( chatflowid: string, - chatTypeFilter: chatType | undefined, + chatTypeFilter: ChatType | undefined, startDate?: string, endDate?: string, messageId?: string, diff --git a/packages/server/src/utils/buildAgentGraph.ts b/packages/server/src/utils/buildAgentGraph.ts index ea44382bb30..6acf641bd7f 100644 --- a/packages/server/src/utils/buildAgentGraph.ts +++ b/packages/server/src/utils/buildAgentGraph.ts @@ -57,12 +57,14 @@ import logger from './logger' export const buildAgentGraph = async ( chatflow: IChatFlow, chatId: string, + apiMessageId: string, sessionId: string, incomingInput: IncomingInput, isInternal: boolean, baseURL?: string, sseStreamer?: IServerSideEventStreamer, - shouldStreamResponse?: boolean + shouldStreamResponse?: boolean, + uploadedFilesContent?: string ): Promise => { try { const appServer = getRunningExpressApp() @@ -114,6 +116,7 @@ export const buildAgentGraph = async ( startingNodeIds, reactFlowNodes: nodes, reactFlowEdges: edges, + apiMessageId, graph, depthQueue, componentNodes: appServer.nodesPool.componentNodes, @@ -127,7 +130,8 @@ export const buildAgentGraph = async ( cachePool: appServer.cachePool, isUpsert: false, uploads: incomingInput.uploads, - baseURL + baseURL, + uploadedFilesContent }) const options = { @@ -186,7 +190,8 @@ export const buildAgentGraph = async ( chatHistory, incomingInput?.overrideConfig, sessionId || chatId, - seqAgentNodes.some((node) => node.data.inputs?.summarization) + seqAgentNodes.some((node) => node.data.inputs?.summarization), + uploadedFilesContent ) } else { isSequential = true @@ -202,7 +207,8 @@ export const buildAgentGraph = async ( chatHistory, incomingInput?.overrideConfig, sessionId || chatId, - incomingInput.action + incomingInput.action, + uploadedFilesContent ) } @@ -346,7 +352,6 @@ export const buildAgentGraph = async ( if (isSequential && !finalResult && agentReasoning.length) { const lastMessages = agentReasoning[agentReasoning.length - 1].messages const lastAgentReasoningMessage = lastMessages[lastMessages.length - 1] - // If last message is an AI Message with tool calls, that means the last node was interrupted if (lastMessageRaw.tool_calls && lastMessageRaw.tool_calls.length > 0) { // The last node that got interrupted @@ -454,6 +459,7 @@ export const buildAgentGraph = async ( * @param {ICommonObject} overrideConfig * @param {string} threadId * @param {boolean} summarization + * @param {string} uploadedFilesContent, */ const compileMultiAgentsGraph = async ( chatflow: IChatFlow, @@ -468,7 +474,8 @@ const compileMultiAgentsGraph = async ( chatHistory: IMessage[] = [], overrideConfig?: ICommonObject, threadId?: string, - summarization?: boolean + summarization?: boolean, + uploadedFilesContent?: string ) => { const appServer = getRunningExpressApp() const channels: ITeamState = { @@ -500,7 +507,15 @@ const compileMultiAgentsGraph = async ( let flowNodeData = cloneDeep(workerNode.data) if (overrideConfig) flowNodeData = replaceInputsWithConfig(flowNodeData, overrideConfig) - flowNodeData = await resolveVariables(appServer.AppDataSource, flowNodeData, reactflowNodes, question, chatHistory, overrideConfig) + flowNodeData = await resolveVariables( + appServer.AppDataSource, + flowNodeData, + reactflowNodes, + question, + chatHistory, + overrideConfig, + uploadedFilesContent + ) try { const workerResult: IMultiAgentNode = await newNodeInstance.init(flowNodeData, question, options) @@ -531,7 +546,15 @@ const compileMultiAgentsGraph = async ( let flowNodeData = cloneDeep(supervisorNode.data) if (overrideConfig) flowNodeData = replaceInputsWithConfig(flowNodeData, overrideConfig) - flowNodeData = await resolveVariables(appServer.AppDataSource, flowNodeData, reactflowNodes, question, chatHistory, overrideConfig) + flowNodeData = await resolveVariables( + appServer.AppDataSource, + flowNodeData, + reactflowNodes, + question, + chatHistory, + overrideConfig, + uploadedFilesContent + ) if (flowNodeData.inputs) flowNodeData.inputs.workerNodes = supervisorWorkers[supervisor] @@ -601,9 +624,10 @@ const compileMultiAgentsGraph = async ( } // Return stream result as we should only have 1 supervisor + const finalQuestion = uploadedFilesContent ? `${uploadedFilesContent}\n\n${question}` : question return await graph.stream( { - messages: [...prependMessages, new HumanMessage({ content: question })] + messages: [...prependMessages, new HumanMessage({ content: finalQuestion })] }, { recursionLimit: supervisorResult?.recursionLimit ?? 100, callbacks: [loggerHandler, ...callbacks], configurable: config } ) @@ -639,7 +663,8 @@ const compileSeqAgentsGraph = async ( chatHistory: IMessage[] = [], overrideConfig?: ICommonObject, threadId?: string, - action?: IAction + action?: IAction, + uploadedFilesContent?: string ) => { const appServer = getRunningExpressApp() @@ -691,7 +716,15 @@ const compileSeqAgentsGraph = async ( flowNodeData = cloneDeep(node.data) if (overrideConfig) flowNodeData = replaceInputsWithConfig(flowNodeData, overrideConfig) - flowNodeData = await resolveVariables(appServer.AppDataSource, flowNodeData, reactflowNodes, question, chatHistory, overrideConfig) + flowNodeData = await resolveVariables( + appServer.AppDataSource, + flowNodeData, + reactflowNodes, + question, + chatHistory, + overrideConfig, + uploadedFilesContent + ) const seqAgentNode: ISeqAgentNode = await newNodeInstance.init(flowNodeData, question, options) return seqAgentNode @@ -995,8 +1028,9 @@ const compileSeqAgentsGraph = async ( } } + const finalQuestion = uploadedFilesContent ? `${uploadedFilesContent}\n\n${question}` : question let humanMsg: { messages: HumanMessage[] | ToolMessage[] } | null = { - messages: [...prependMessages, new HumanMessage({ content: question })] + messages: [...prependMessages, new HumanMessage({ content: finalQuestion })] } if (action && action.mapping && question === action.mapping.approve) { diff --git a/packages/server/src/utils/buildChatflow.ts b/packages/server/src/utils/buildChatflow.ts index 3257f0a8a9e..e2d2db12b7e 100644 --- a/packages/server/src/utils/buildChatflow.ts +++ b/packages/server/src/utils/buildChatflow.ts @@ -19,7 +19,7 @@ import { IReactFlowObject, IReactFlowNode, IDepthQueue, - chatType, + ChatType, IChatMessage, IChatFlow, IReactFlowEdge @@ -88,12 +88,14 @@ export const utilBuildChatflow = async (req: Request, isInternal: boolean = fals } let fileUploads: IFileUpload[] = [] + let uploadedFilesContent = '' if (incomingInput.uploads) { fileUploads = incomingInput.uploads for (let i = 0; i < fileUploads.length; i += 1) { const upload = fileUploads[i] - if ((upload.type === 'file' || upload.type === 'audio') && upload.data) { + // if upload in an image, a rag file, or audio + if ((upload.type === 'file' || upload.type === 'file:rag' || upload.type === 'audio') && upload.data) { const filename = upload.name const splitDataURI = upload.data.split(',') const bf = Buffer.from(splitDataURI.pop() || '', 'base64') @@ -139,6 +141,13 @@ export const utilBuildChatflow = async (req: Request, isInternal: boolean = fals } } } + + if (upload.type === 'file:full' && upload.data) { + upload.type = 'stored-file:full' + // Omit upload.data since we don't store the content in database + uploadedFilesContent += `${upload.data}\n\n` + fileUploads[i] = omit(upload, ['data']) + } } } @@ -202,6 +211,8 @@ export const utilBuildChatflow = async (req: Request, isInternal: boolean = fals const nodes = parsedFlowData.nodes const edges = parsedFlowData.edges + const apiMessageId = uuidv4() + /*** Get session ID ***/ const memoryNode = findMemoryNode(nodes, edges) const memoryType = memoryNode?.data.label @@ -217,6 +228,7 @@ export const utilBuildChatflow = async (req: Request, isInternal: boolean = fals chatflow, isInternal, chatId, + apiMessageId, memoryType ?? '', sessionId, userMessageDateTime, @@ -226,7 +238,8 @@ export const utilBuildChatflow = async (req: Request, isInternal: boolean = fals edges, baseURL, appServer.sseStreamer, - true + true, + uploadedFilesContent ) } @@ -339,8 +352,10 @@ export const utilBuildChatflow = async (req: Request, isInternal: boolean = fals reactFlowEdges: edges, graph, depthQueue, + apiMessageId, componentNodes: appServer.nodesPool.componentNodes, question: incomingInput.question, + uploadedFilesContent, chatHistory, chatId, sessionId: sessionId ?? '', @@ -369,6 +384,7 @@ export const utilBuildChatflow = async (req: Request, isInternal: boolean = fals chatflowid, chatId, sessionId, + apiMessageId, chatHistory, ...incomingInput.overrideConfig } @@ -379,7 +395,8 @@ export const utilBuildChatflow = async (req: Request, isInternal: boolean = fals reactFlowNodes, incomingInput.question, chatHistory, - flowData + flowData, + uploadedFilesContent ) nodeToExecuteData = reactFlowNodeData @@ -393,30 +410,25 @@ export const utilBuildChatflow = async (req: Request, isInternal: boolean = fals const nodeInstance = new nodeModule.nodeClass({ sessionId }) isStreamValid = (req.body.streaming === 'true' || req.body.streaming === true) && isStreamValid + const finalQuestion = uploadedFilesContent ? `${uploadedFilesContent}\n\n${incomingInput.question}` : incomingInput.question + + const runParams = { + chatId, + chatflowid, + apiMessageId, + logger, + appDataSource: appServer.AppDataSource, + databaseEntities, + analytic: chatflow.analytic, + uploads: incomingInput.uploads, + prependMessages + } + + let result = await nodeInstance.run(nodeToExecuteData, finalQuestion, { + ...runParams, + ...(isStreamValid && { sseStreamer: appServer.sseStreamer, shouldStreamResponse: true }) + }) - let result = isStreamValid - ? await nodeInstance.run(nodeToExecuteData, incomingInput.question, { - chatId, - chatflowid, - logger, - appDataSource: appServer.AppDataSource, - databaseEntities, - analytic: chatflow.analytic, - uploads: incomingInput.uploads, - prependMessages, - sseStreamer: appServer.sseStreamer, - shouldStreamResponse: isStreamValid - }) - : await nodeInstance.run(nodeToExecuteData, incomingInput.question, { - chatId, - chatflowid, - logger, - appDataSource: appServer.AppDataSource, - databaseEntities, - analytic: chatflow.analytic, - uploads: incomingInput.uploads, - prependMessages - }) result = typeof result === 'string' ? { text: result } : result // Retrieve threadId from assistant if exists @@ -428,7 +440,7 @@ export const utilBuildChatflow = async (req: Request, isInternal: boolean = fals role: 'userMessage', content: incomingInput.question, chatflowid, - chatType: isInternal ? chatType.INTERNAL : chatType.EXTERNAL, + chatType: isInternal ? ChatType.INTERNAL : ChatType.EXTERNAL, chatId, memoryType, sessionId, @@ -443,11 +455,12 @@ export const utilBuildChatflow = async (req: Request, isInternal: boolean = fals else if (result.json) resultText = '```json\n' + JSON.stringify(result.json, null, 2) else resultText = JSON.stringify(result, null, 2) - const apiMessage: Omit = { + const apiMessage: Omit = { + id: apiMessageId, role: 'apiMessage', content: resultText, chatflowid, - chatType: isInternal ? chatType.INTERNAL : chatType.EXTERNAL, + chatType: isInternal ? ChatType.INTERNAL : ChatType.EXTERNAL, chatId, memoryType, sessionId @@ -476,7 +489,7 @@ export const utilBuildChatflow = async (req: Request, isInternal: boolean = fals version: await getAppVersion(), chatflowId: chatflowid, chatId, - type: isInternal ? chatType.INTERNAL : chatType.EXTERNAL, + type: isInternal ? ChatType.INTERNAL : ChatType.EXTERNAL, flowGraph: getTelemetryFlowObj(nodes, edges) }) @@ -507,6 +520,7 @@ const utilBuildAgentResponse = async ( agentflow: IChatFlow, isInternal: boolean, chatId: string, + apiMessageId: string, memoryType: string, sessionId: string, userMessageDateTime: Date, @@ -516,19 +530,22 @@ const utilBuildAgentResponse = async ( edges: IReactFlowEdge[], baseURL?: string, sseStreamer?: IServerSideEventStreamer, - shouldStreamResponse?: boolean + shouldStreamResponse?: boolean, + uploadedFilesContent?: string ) => { try { const appServer = getRunningExpressApp() const streamResults = await buildAgentGraph( agentflow, chatId, + apiMessageId, sessionId, incomingInput, isInternal, baseURL, sseStreamer, - shouldStreamResponse + shouldStreamResponse, + uploadedFilesContent ) if (streamResults) { const { finalResult, finalAction, sourceDocuments, artifacts, usedTools, agentReasoning } = streamResults @@ -536,7 +553,7 @@ const utilBuildAgentResponse = async ( role: 'userMessage', content: incomingInput.question, chatflowid: agentflow.id, - chatType: isInternal ? chatType.INTERNAL : chatType.EXTERNAL, + chatType: isInternal ? ChatType.INTERNAL : ChatType.EXTERNAL, chatId, memoryType, sessionId, @@ -546,11 +563,12 @@ const utilBuildAgentResponse = async ( } await utilAddChatMessage(userMessage) - const apiMessage: Omit = { + const apiMessage: Omit = { + id: apiMessageId, role: 'apiMessage', content: finalResult, chatflowid: agentflow.id, - chatType: isInternal ? chatType.INTERNAL : chatType.EXTERNAL, + chatType: isInternal ? ChatType.INTERNAL : ChatType.EXTERNAL, chatId, memoryType, sessionId @@ -578,7 +596,7 @@ const utilBuildAgentResponse = async ( version: await getAppVersion(), agentflowId: agentflow.id, chatId, - type: isInternal ? chatType.INTERNAL : chatType.EXTERNAL, + type: isInternal ? ChatType.INTERNAL : ChatType.EXTERNAL, flowGraph: getTelemetryFlowObj(nodes, edges) }) diff --git a/packages/server/src/utils/createAttachment.ts b/packages/server/src/utils/createAttachment.ts new file mode 100644 index 00000000000..544a08aa70d --- /dev/null +++ b/packages/server/src/utils/createAttachment.ts @@ -0,0 +1,84 @@ +import { Request } from 'express' +import * as path from 'path' +import * as fs from 'fs' +import { addArrayFilesToStorage, IDocument, mapExtToInputField, mapMimeTypeToInputField } from 'flowise-components' +import { getRunningExpressApp } from './getRunningExpressApp' +import { getErrorMessage } from '../errors/utils' + +/** + * Create attachment + * @param {Request} req + */ +export const createFileAttachment = async (req: Request) => { + const appServer = getRunningExpressApp() + + const chatflowid = req.params.chatflowId + if (!chatflowid) { + throw new Error( + 'Params chatflowId is required! Please provide chatflowId and chatId in the URL: /api/v1/attachments/:chatflowId/:chatId' + ) + } + + const chatId = req.params.chatId + if (!chatId) { + throw new Error( + 'Params chatId is required! Please provide chatflowId and chatId in the URL: /api/v1/attachments/:chatflowId/:chatId' + ) + } + + // Find FileLoader node + const fileLoaderComponent = appServer.nodesPool.componentNodes['fileLoader'] + const fileLoaderNodeInstanceFilePath = fileLoaderComponent.filePath as string + const fileLoaderNodeModule = await import(fileLoaderNodeInstanceFilePath) + const fileLoaderNodeInstance = new fileLoaderNodeModule.nodeClass() + const options = { + retrieveAttachmentChatId: true, + chatflowid, + chatId + } + const files = (req.files as Express.Multer.File[]) || [] + const fileAttachments = [] + if (files.length) { + for (const file of files) { + const fileBuffer = fs.readFileSync(file.path) + const fileNames: string[] = [] + const storagePath = await addArrayFilesToStorage(file.mimetype, fileBuffer, file.originalname, fileNames, chatflowid, chatId) + + const fileInputFieldFromMimeType = mapMimeTypeToInputField(file.mimetype) + + const fileExtension = path.extname(file.originalname) + + const fileInputFieldFromExt = mapExtToInputField(fileExtension) + + let fileInputField = 'txtFile' + + if (fileInputFieldFromExt !== 'txtFile') { + fileInputField = fileInputFieldFromExt + } else if (fileInputFieldFromMimeType !== 'txtFile') { + fileInputField = fileInputFieldFromExt + } + + fs.unlinkSync(file.path) + + try { + const nodeData = { + inputs: { + [fileInputField]: storagePath + } + } + const documents: IDocument[] = await fileLoaderNodeInstance.init(nodeData, '', options) + const pageContents = documents.map((doc) => doc.pageContent).join('\n') + fileAttachments.push({ + name: file.originalname, + mimeType: file.mimetype, + size: file.size, + content: pageContents + }) + } catch (error) { + throw new Error(`Failed operation: createFileAttachment - ${getErrorMessage(error)}`) + } + } + } + + return fileAttachments +} diff --git a/packages/server/src/utils/getChatMessage.ts b/packages/server/src/utils/getChatMessage.ts index 459d62f73a7..7ffde0eb8cf 100644 --- a/packages/server/src/utils/getChatMessage.ts +++ b/packages/server/src/utils/getChatMessage.ts @@ -1,13 +1,14 @@ import { MoreThanOrEqual, LessThanOrEqual } from 'typeorm' -import { ChatMessageRatingType, 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' +import { aMonthAgo, setDateToStartOrEndOfDay } from '.' /** * Method that get chat messages. * @param {string} chatflowid - * @param {chatType} chatType + * @param {ChatType} chatType * @param {string} sortOrder * @param {string} chatId * @param {string} memoryType @@ -19,7 +20,7 @@ import { getRunningExpressApp } from '../utils/getRunningExpressApp' */ export const utilGetChatMessage = async ( chatflowid: string, - chatType: chatType | undefined, + chatType: ChatType | undefined, sortOrder: string = 'ASC', chatId?: string, memoryType?: string, @@ -31,20 +32,6 @@ export const utilGetChatMessage = async ( feedbackTypes?: ChatMessageRatingType[] ): Promise => { const appServer = getRunningExpressApp() - const setDateToStartOrEndOfDay = (dateTimeStr: string, setHours: 'start' | 'end') => { - const date = new Date(dateTimeStr) - if (isNaN(date.getTime())) { - return undefined - } - setHours === 'start' ? date.setHours(0, 0, 0, 0) : date.setHours(23, 59, 59, 999) - return date - } - - const aMonthAgo = () => { - const date = new Date() - date.setMonth(new Date().getMonth() - 1) - return date - } let fromDate if (startDate) fromDate = setDateToStartOrEndOfDay(startDate, 'start') diff --git a/packages/server/src/utils/getUploadsConfig.ts b/packages/server/src/utils/getUploadsConfig.ts index 4688d680ed5..1bd65bb81ba 100644 --- a/packages/server/src/utils/getUploadsConfig.ts +++ b/packages/server/src/utils/getUploadsConfig.ts @@ -8,7 +8,7 @@ import { InternalFlowiseError } from '../errors/internalFlowiseError' type IUploadConfig = { isSpeechToTextEnabled: boolean isImageUploadAllowed: boolean - isFileUploadAllowed: boolean + isRAGFileUploadAllowed: boolean imgUploadSizeAndTypes: IUploadFileSizeAndTypes[] fileUploadSizeAndTypes: IUploadFileSizeAndTypes[] } @@ -32,7 +32,7 @@ export const utilGetUploadsConfig = async (chatflowid: string): Promise { const isObject = typeof paramValue === 'object' const initialValue = (isObject ? JSON.stringify(paramValue) : paramValue) ?? '' @@ -800,6 +809,10 @@ export const getVariableValue = async ( variableDict[`{{${variableFullPath}}}`] = handleEscapeCharacters(question, false) } + if (isAcceptVariable && variableFullPath === FILE_ATTACHMENT_PREFIX) { + variableDict[`{{${variableFullPath}}}`] = handleEscapeCharacters(uploadedFilesContent, false) + } + if (isAcceptVariable && variableFullPath === CHAT_HISTORY_VAR_PREFIX) { variableDict[`{{${variableFullPath}}}`] = handleEscapeCharacters(convertChatHistoryToText(chatHistory), false) } @@ -913,7 +926,8 @@ export const resolveVariables = async ( reactFlowNodes: IReactFlowNode[], question: string, chatHistory: IMessage[], - flowData?: ICommonObject + flowData?: ICommonObject, + uploadedFilesContent?: string ): Promise => { let flowNodeData = cloneDeep(reactFlowNodeData) const types = 'inputs' @@ -931,7 +945,8 @@ export const resolveVariables = async ( question, chatHistory, undefined, - flowData + flowData, + uploadedFilesContent ) resolvedInstances.push(resolvedInstance) } @@ -945,7 +960,8 @@ export const resolveVariables = async ( question, chatHistory, isAcceptVariable, - flowData + flowData, + uploadedFilesContent ) paramsObj[key] = resolvedInstance } @@ -1569,3 +1585,18 @@ export const convertToValidFilename = (word: string) => { .replace(' ', '') .toLowerCase() } + +export const setDateToStartOrEndOfDay = (dateTimeStr: string, setHours: 'start' | 'end') => { + const date = new Date(dateTimeStr) + if (isNaN(date.getTime())) { + return undefined + } + setHours === 'start' ? date.setHours(0, 0, 0, 0) : date.setHours(23, 59, 59, 999) + return date +} + +export const aMonthAgo = () => { + const date = new Date() + date.setMonth(new Date().getMonth() - 1) + return date +} diff --git a/packages/server/src/utils/updateChatMessageFeedback.ts b/packages/server/src/utils/updateChatMessageFeedback.ts index ef327fa78f3..69cdffd8b4a 100644 --- a/packages/server/src/utils/updateChatMessageFeedback.ts +++ b/packages/server/src/utils/updateChatMessageFeedback.ts @@ -1,6 +1,8 @@ import { IChatMessageFeedback } from '../Interface' import { getRunningExpressApp } from '../utils/getRunningExpressApp' import { ChatMessageFeedback } from '../database/entities/ChatMessageFeedback' +import { ChatFlow } from '../database/entities/ChatFlow' +import lunary from 'lunary' /** * Method that updates chat message feedback. @@ -11,6 +13,21 @@ export const utilUpdateChatMessageFeedback = async (id: string, chatMessageFeedb const appServer = getRunningExpressApp() const newChatMessageFeedback = new ChatMessageFeedback() Object.assign(newChatMessageFeedback, chatMessageFeedback) + await appServer.AppDataSource.getRepository(ChatMessageFeedback).update({ id }, chatMessageFeedback) + + // Fetch the updated entity + const updatedFeedback = await appServer.AppDataSource.getRepository(ChatMessageFeedback).findOne({ where: { id } }) + + const chatflow = await appServer.AppDataSource.getRepository(ChatFlow).findOne({ where: { id: updatedFeedback?.chatflowid } }) + const analytic = JSON.parse(chatflow?.analytic ?? '{}') + + if (analytic?.lunary?.status === true && updatedFeedback?.rating) { + lunary.trackFeedback(updatedFeedback.messageId, { + comment: updatedFeedback?.content, + thumb: updatedFeedback?.rating === 'THUMBS_UP' ? 'up' : 'down' + }) + } + return { status: 'OK' } } diff --git a/packages/server/src/utils/upsertVector.ts b/packages/server/src/utils/upsertVector.ts index c0e5e4d98f3..7d7fd70302a 100644 --- a/packages/server/src/utils/upsertVector.ts +++ b/packages/server/src/utils/upsertVector.ts @@ -16,14 +16,14 @@ import { getStartingNodes } from '../utils' import { validateChatflowAPIKey } from './validateKey' -import { IncomingInput, INodeDirectedGraph, IReactFlowObject, chatType } from '../Interface' +import { IncomingInput, INodeDirectedGraph, IReactFlowObject, ChatType } from '../Interface' import { ChatFlow } from '../database/entities/ChatFlow' import { getRunningExpressApp } from '../utils/getRunningExpressApp' import { UpsertHistory } from '../database/entities/UpsertHistory' import { InternalFlowiseError } from '../errors/internalFlowiseError' import { StatusCodes } from 'http-status-codes' import { getErrorMessage } from '../errors/utils' - +import { v4 as uuidv4 } from 'uuid' /** * Upsert documents * @param {Request} req @@ -108,6 +108,8 @@ export const upsertVector = async (req: Request, isInternal: boolean = false) => const nodes = parsedFlowData.nodes const edges = parsedFlowData.edges + const apiMessageId = req.body.apiMessageId ?? uuidv4() + let stopNodeId = incomingInput?.stopNodeId ?? '' let chatHistory: IMessage[] = [] let chatId = incomingInput.chatId ?? '' @@ -162,6 +164,7 @@ export const upsertVector = async (req: Request, isInternal: boolean = false) => question: incomingInput.question, chatHistory, chatId, + apiMessageId, sessionId: sessionId ?? '', chatflowid, appDataSource: appServer.AppDataSource, @@ -192,7 +195,7 @@ export const upsertVector = async (req: Request, isInternal: boolean = false) => data: { version: await getAppVersion(), chatlowId: chatflowid, - type: isInternal ? chatType.INTERNAL : chatType.EXTERNAL, + type: isInternal ? ChatType.INTERNAL : ChatType.EXTERNAL, flowGraph: getTelemetryFlowObj(nodes, edges), stopNodeId } diff --git a/packages/ui/package.json b/packages/ui/package.json index d50c4afc2a1..d55d634934d 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "flowise-ui", - "version": "2.1.2", + "version": "2.1.3", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://flowiseai.com", "author": { diff --git a/packages/ui/src/api/attachments.js b/packages/ui/src/api/attachments.js new file mode 100644 index 00000000000..7e9ef372f9d --- /dev/null +++ b/packages/ui/src/api/attachments.js @@ -0,0 +1,10 @@ +import client from './client' + +const createAttachment = (chatflowid, chatid, formData) => + client.post(`/attachments/${chatflowid}/${chatid}`, formData, { + headers: { 'Content-Type': 'multipart/form-data' } + }) + +export default { + createAttachment +} diff --git a/packages/ui/src/assets/images/fileAttachment.png b/packages/ui/src/assets/images/fileAttachment.png new file mode 100644 index 00000000000..8fb385c8c1b Binary files /dev/null and b/packages/ui/src/assets/images/fileAttachment.png differ diff --git a/packages/ui/src/ui-component/dialog/ChatflowConfigurationDialog.jsx b/packages/ui/src/ui-component/dialog/ChatflowConfigurationDialog.jsx index 1155eec56ef..48d6f3700a7 100644 --- a/packages/ui/src/ui-component/dialog/ChatflowConfigurationDialog.jsx +++ b/packages/ui/src/ui-component/dialog/ChatflowConfigurationDialog.jsx @@ -11,6 +11,7 @@ import AnalyseFlow from '@/ui-component/extended/AnalyseFlow' import StarterPrompts from '@/ui-component/extended/StarterPrompts' import Leads from '@/ui-component/extended/Leads' import FollowUpPrompts from '@/ui-component/extended/FollowUpPrompts' +import FileUpload from '@/ui-component/extended/FileUpload' const CHATFLOW_CONFIGURATION_TABS = [ { @@ -44,6 +45,10 @@ const CHATFLOW_CONFIGURATION_TABS = [ { label: 'Leads', id: 'leads' + }, + { + label: 'File Upload', + id: 'fileUpload' } ] @@ -85,7 +90,7 @@ const ChatflowConfigurationDialog = ({ show, dialogProps, onCancel }) => { onClose={onCancel} open={show} fullWidth - maxWidth={'md'} + maxWidth={'lg'} aria-labelledby='alert-dialog-title' aria-describedby='alert-dialog-description' > @@ -127,6 +132,7 @@ const ChatflowConfigurationDialog = ({ show, dialogProps, onCancel }) => { {item.id === 'allowedDomains' ? : null} {item.id === 'analyseChatflow' ? : null} {item.id === 'leads' ? : null} + {item.id === 'fileUpload' ? : null} ))} diff --git a/packages/ui/src/ui-component/dialog/ViewMessagesDialog.jsx b/packages/ui/src/ui-component/dialog/ViewMessagesDialog.jsx index 5b40d1e7c21..1b803826b46 100644 --- a/packages/ui/src/ui-component/dialog/ViewMessagesDialog.jsx +++ b/packages/ui/src/ui-component/dialog/ViewMessagesDialog.jsx @@ -25,7 +25,10 @@ import { Chip, Card, CardMedia, - CardContent + CardContent, + FormControlLabel, + Checkbox, + DialogActions } from '@mui/material' import { useTheme } from '@mui/material/styles' import DatePicker from 'react-datepicker' @@ -84,6 +87,52 @@ const messageImageStyle = { objectFit: 'cover' } +const ConfirmDeleteMessageDialog = ({ show, dialogProps, onCancel, onConfirm }) => { + const portalElement = document.getElementById('portal') + const [hardDelete, setHardDelete] = useState(false) + + const onSubmit = () => { + onConfirm(hardDelete) + } + + const component = show ? ( + + + {dialogProps.title} + + + {dialogProps.description} + setHardDelete(event.target.checked)} />} + label='Remove messages from 3rd party Memory Node' + /> + + + + + {dialogProps.confirmButtonName} + + + + ) : null + + return createPortal(component, portalElement) +} + +ConfirmDeleteMessageDialog.propTypes = { + show: PropTypes.bool, + dialogProps: PropTypes.object, + onCancel: PropTypes.func, + onConfirm: PropTypes.func +} + const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { const portalElement = document.getElementById('portal') const dispatch = useDispatch() @@ -103,6 +152,8 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { const [selectedChatId, setSelectedChatId] = useState('') const [sourceDialogOpen, setSourceDialogOpen] = useState(false) const [sourceDialogProps, setSourceDialogProps] = useState({}) + const [hardDeleteDialogOpen, setHardDeleteDialogOpen] = useState(false) + const [hardDeleteDialogProps, setHardDeleteDialogProps] = useState({}) const [chatTypeFilter, setChatTypeFilter] = useState([]) const [feedbackTypeFilter, setFeedbackTypeFilter] = useState([]) const [startDate, setStartDate] = useState(new Date().setMonth(new Date().getMonth() - 1)) @@ -175,6 +226,83 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { }) } + const onDeleteMessages = () => { + setHardDeleteDialogProps({ + title: 'Delete Messages', + description: 'Are you sure you want to delete messages? This action cannot be undone.', + confirmButtonName: 'Delete', + cancelButtonName: 'Cancel' + }) + setHardDeleteDialogOpen(true) + } + + const deleteMessages = async (hardDelete) => { + setHardDeleteDialogOpen(false) + const chatflowid = dialogProps.chatflow.id + try { + const obj = { chatflowid, isClearFromViewMessageDialog: true } + + let _chatTypeFilter = chatTypeFilter + if (typeof chatTypeFilter === 'string') { + _chatTypeFilter = JSON.parse(chatTypeFilter) + } + if (_chatTypeFilter.length === 1) { + obj.chatType = _chatTypeFilter[0] + } + + let _feedbackTypeFilter = feedbackTypeFilter + if (typeof feedbackTypeFilter === 'string') { + _feedbackTypeFilter = JSON.parse(feedbackTypeFilter) + } + if (_feedbackTypeFilter.length === 1) { + obj.feedbackType = _feedbackTypeFilter[0] + } + + if (startDate) obj.startDate = startDate + if (endDate) obj.endDate = endDate + if (hardDelete) obj.hardDelete = true + + await chatmessageApi.deleteChatmessage(chatflowid, obj) + enqueueSnackbar({ + message: 'Succesfully deleted messages', + options: { + key: new Date().getTime() + Math.random(), + variant: 'success', + action: (key) => ( + + ) + } + }) + getChatmessageApi.request(chatflowid, { + chatType: chatTypeFilter.length ? chatTypeFilter : undefined, + startDate: startDate, + endDate: endDate + }) + getStatsApi.request(chatflowid, { + chatType: chatTypeFilter.length ? chatTypeFilter : undefined, + startDate: startDate, + endDate: endDate + }) + } catch (error) { + console.error(error) + enqueueSnackbar({ + message: 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) => ( + + ) + } + }) + } + } + const exportMessages = async () => { if (!storagePath && getStoragePathFromServer.data) { storagePath = getStoragePathFromServer.data.storagePath @@ -675,7 +803,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { onClose={onCancel} open={show} fullWidth - maxWidth={chatlogs && chatlogs.length == 0 ? 'md' : 'lg'} + maxWidth={'lg'} aria-labelledby='alert-dialog-title' aria-describedby='alert-dialog-description' > @@ -781,6 +909,11 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { />
+ {stats.totalMessages > 0 && ( + + )}
{ )}
setSourceDialogOpen(false)} /> + setHardDeleteDialogOpen(false)} + onConfirm={(hardDelete) => deleteMessages(hardDelete)} + /> diff --git a/packages/ui/src/ui-component/extended/FileUpload.jsx b/packages/ui/src/ui-component/extended/FileUpload.jsx new file mode 100644 index 00000000000..b1ccb08ca4b --- /dev/null +++ b/packages/ui/src/ui-component/extended/FileUpload.jsx @@ -0,0 +1,122 @@ +import { useDispatch } from 'react-redux' +import { useState, useEffect } from 'react' +import PropTypes from 'prop-types' +import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction, SET_CHATFLOW } from '@/store/actions' + +// material-ui +import { Button, Box, Typography } from '@mui/material' +import { IconX } from '@tabler/icons-react' + +// Project import +import { StyledButton } from '@/ui-component/button/StyledButton' +import { TooltipWithParser } from '@/ui-component/tooltip/TooltipWithParser' +import { SwitchInput } from '@/ui-component/switch/Switch' + +// store +import useNotifier from '@/utils/useNotifier' + +// API +import chatflowsApi from '@/api/chatflows' + +const message = `Allow files to be uploaded from the chat. Uploaded files will be parsed as string and sent to LLM. If File Upload is enabled on Vector Store as well, this will override and takes precedence.` + +const FileUpload = ({ dialogProps }) => { + const dispatch = useDispatch() + + useNotifier() + + const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)) + const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) + + const [fullFileUpload, setFullFileUpload] = useState(false) + const [chatbotConfig, setChatbotConfig] = useState({}) + + const handleChange = (value) => { + setFullFileUpload(value) + } + + const onSave = async () => { + try { + const value = { + status: fullFileUpload + } + chatbotConfig.fullFileUpload = value + + const saveResp = await chatflowsApi.updateChatflow(dialogProps.chatflow.id, { + chatbotConfig: JSON.stringify(chatbotConfig) + }) + if (saveResp.data) { + enqueueSnackbar({ + message: 'File Upload Configuration Saved', + options: { + key: new Date().getTime() + Math.random(), + variant: 'success', + action: (key) => ( + + ) + } + }) + dispatch({ type: SET_CHATFLOW, chatflow: saveResp.data }) + } + } catch (error) { + enqueueSnackbar({ + message: `Failed to save File Upload Configuration: ${ + 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) => ( + + ) + } + }) + } + } + + useEffect(() => { + if (dialogProps.chatflow) { + if (dialogProps.chatflow.chatbotConfig) { + try { + let chatbotConfig = JSON.parse(dialogProps.chatflow.chatbotConfig) + setChatbotConfig(chatbotConfig || {}) + if (chatbotConfig.fullFileUpload) { + setFullFileUpload(chatbotConfig.fullFileUpload.status) + } + } catch (e) { + setChatbotConfig({}) + } + } + } + + return () => {} + }, [dialogProps]) + + return ( + <> + +
+ + Enable Full File Upload + + +
+ +
+ + Save + + + ) +} + +FileUpload.propTypes = { + dialogProps: PropTypes.object +} + +export default FileUpload diff --git a/packages/ui/src/ui-component/json/SelectVariable.jsx b/packages/ui/src/ui-component/json/SelectVariable.jsx index 9f4e2f737ad..55fdce061d6 100644 --- a/packages/ui/src/ui-component/json/SelectVariable.jsx +++ b/packages/ui/src/ui-component/json/SelectVariable.jsx @@ -5,6 +5,7 @@ import PerfectScrollbar from 'react-perfect-scrollbar' import robotPNG from '@/assets/images/robot.png' import chatPNG from '@/assets/images/chathistory.png' import diskPNG from '@/assets/images/floppy-disc.png' +import fileAttachmentPNG from '@/assets/images/fileAttachment.png' import { baseURL } from '@/store/constant' const sequentialStateMessagesSelection = [ @@ -119,6 +120,45 @@ const SelectVariable = ({ availableNodesForVariable, disabled = false, onSelectA /> + onSelectOutputResponseClick(null, 'file_attachment')} + > + + +
+ fileAttachment +
+
+ +
+
{availableNodesForVariable && availableNodesForVariable.length > 0 && availableNodesForVariable.map((node, index) => { diff --git a/packages/ui/src/views/canvas/index.css b/packages/ui/src/views/canvas/index.css index b7bb04d8f18..e13d875e62f 100644 --- a/packages/ui/src/views/canvas/index.css +++ b/packages/ui/src/views/canvas/index.css @@ -37,11 +37,11 @@ } .chatflow-canvas .react-flow__handle-connecting { - cursor: not-allowed; - background: #db4e4e !important; + cursor: not-allowed; + background: #db4e4e !important; } .chatflow-canvas .react-flow__handle-valid { - cursor: crosshair; - background: #5dba62 !important; -} \ No newline at end of file + cursor: crosshair; + background: #5dba62 !important; +} diff --git a/packages/ui/src/views/chatflows/ShareChatbot.jsx b/packages/ui/src/views/chatflows/ShareChatbot.jsx index 9c5de4c36ae..7b09b0ff396 100644 --- a/packages/ui/src/views/chatflows/ShareChatbot.jsx +++ b/packages/ui/src/views/chatflows/ShareChatbot.jsx @@ -56,6 +56,7 @@ const ShareChatbot = ({ isSessionMemory, isAgentCanvas }) => { const [isPublicChatflow, setChatflowIsPublic] = useState(chatflow.isPublic ?? false) const [generateNewSession, setGenerateNewSession] = useState(chatbotConfig?.generateNewSession ?? false) + const [renderHTML, setRenderHTML] = useState(chatbotConfig?.renderHTML ?? false) const [title, setTitle] = useState(chatbotConfig?.title ?? '') const [titleAvatarSrc, setTitleAvatarSrc] = useState(chatbotConfig?.titleAvatarSrc ?? '') @@ -138,6 +139,12 @@ const ShareChatbot = ({ isSessionMemory, isAgentCanvas }) => { if (isSessionMemory) obj.overrideConfig.generateNewSession = generateNewSession + if (renderHTML) { + obj.overrideConfig.renderHTML = true + } else { + obj.overrideConfig.renderHTML = false + } + if (chatbotConfig?.starterPrompts) obj.starterPrompts = chatbotConfig.starterPrompts if (isAgentCanvas) { @@ -312,6 +319,9 @@ const ShareChatbot = ({ isSessionMemory, isAgentCanvas }) => { case 'showAgentMessages': setShowAgentMessages(value) break + case 'renderHTML': + setRenderHTML(value) + break } } @@ -480,6 +490,13 @@ const ShareChatbot = ({ isSessionMemory, isAgentCanvas }) => { {textField(textInputPlaceholder, 'textInputPlaceholder', 'TextInput Placeholder', 'string', `Type question..`)} {colorField(textInputSendButtonColor, 'textInputSendButtonColor', 'TextIntput Send Button Color')} + <> + + Render HTML + + {booleanField(renderHTML, 'renderHTML', 'Render HTML on the chat')} + + {/*Session Memory Input*/} {isSessionMemory && ( <> diff --git a/packages/ui/src/views/chatmessage/ChatMessage.jsx b/packages/ui/src/views/chatmessage/ChatMessage.jsx index c2faa51a2f9..96d722b3b1f 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.jsx +++ b/packages/ui/src/views/chatmessage/ChatMessage.jsx @@ -67,6 +67,7 @@ import chatmessageApi from '@/api/chatmessage' import chatflowsApi from '@/api/chatflows' import predictionApi from '@/api/prediction' import vectorstoreApi from '@/api/vectorstore' +import attachmentsApi from '@/api/attachments' import chatmessagefeedbackApi from '@/api/chatmessagefeedback' import leadsApi from '@/api/lead' @@ -88,7 +89,7 @@ const messageImageStyle = { objectFit: 'cover' } -const CardWithDeleteOverlay = ({ item, customization, onDelete }) => { +const CardWithDeleteOverlay = ({ item, disabled, customization, onDelete }) => { const [isHovered, setIsHovered] = useState(false) const defaultBackgroundColor = customization.isDarkMode ? 'rgba(0, 0, 0, 0.3)' : 'transparent' @@ -125,8 +126,9 @@ const CardWithDeleteOverlay = ({ item, customization, onDelete }) => { {item.name} - {isHovered && ( + {isHovered && !disabled && (