diff --git a/.github/workflows/autoSyncMergedPullRequest.yml b/.github/workflows/autoSyncMergedPullRequest.yml new file mode 100644 index 00000000..8fee6650 --- /dev/null +++ b/.github/workflows/autoSyncMergedPullRequest.yml @@ -0,0 +1,33 @@ +name: autoSyncMergedPullRequest +on: + pull_request: + types: + - closed + branches: ['main'] +jobs: + autoSyncMergedPullRequest: + if: github.event.pull_request.merged == true + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v3 + - name: Show PR info + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + echo The PR #${{ github.event.pull_request.number }} was merged on main branch! + - name: Repository Dispatch + uses: peter-evans/repository-dispatch@v2 + with: + token: ${{ secrets.AUTOSYNC_TOKEN }} + repository: ${{ secrets.AUTOSYNC_CH_URL }} + event-type: ${{ secrets.AUTOSYNC_PR_EVENT_TYPE }} + client-payload: >- + { + "ref": "${{ github.ref }}", + "prNumber": "${{ github.event.pull_request.number }}", + "prTitle": "${{ github.event.pull_request.title }}", + "prDescription": "${{ github.event.pull_request.description }}", + "sha": "${{ github.sha }}" + } diff --git a/.github/workflows/autoSyncSingleCommit.yml b/.github/workflows/autoSyncSingleCommit.yml new file mode 100644 index 00000000..0a8c7814 --- /dev/null +++ b/.github/workflows/autoSyncSingleCommit.yml @@ -0,0 +1,36 @@ +name: autoSyncSingleCommit +on: + push: + branches: + - main +jobs: + doNotAutoSyncSingleCommit: + if: github.event.commits[1] != null + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: IGNORE autoSyncSingleCommit + run: | + echo This single commit has came from a merged commit. We will ignore it. This case is handled in autoSyncMergedPullRequest workflow for merge commits comming from merged pull requests only! Beware, the regular merge commits are not handled by any workflow for the moment. + autoSyncSingleCommit: + if: github.event.commits[1] == null + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: autoSyncSingleCommit + env: + GITHUB_CONTEXT: ${{ toJSON(github) }} + run: | + echo Autosync a single commit with id: ${{ github.sha }} from openSource main branch towards cloud hosted version. + - name: Repository Dispatch + uses: peter-evans/repository-dispatch@v2 + with: + token: ${{ secrets.AUTOSYNC_TOKEN }} + repository: ${{ secrets.AUTOSYNC_CH_URL }} + event-type: ${{ secrets.AUTOSYNC_SC_EVENT_TYPE }} + client-payload: >- + { + "ref": "${{ github.ref }}", + "sha": "${{ github.sha }}", + "commitMessage": "${{ github.event.commits[0].message }}" + } diff --git a/CONTRIBUTING-ZH.md b/CONTRIBUTING-ZH.md index 5a752280..7e35d194 100644 --- a/CONTRIBUTING-ZH.md +++ b/CONTRIBUTING-ZH.md @@ -138,6 +138,7 @@ Flowise 支持不同的环境变量来配置您的实例。您可以在 `package | DATABASE_NAME | 数据库名称(当 DATABASE_TYPE 不是 sqlite 时) | 字符串 | | | SECRETKEY_PATH | 保存加密密钥(用于加密/解密凭据)的位置 | 字符串 | `your-path/Flowise/packages/server` | | FLOWISE_SECRETKEY_OVERWRITE | 加密密钥用于替代存储在 SECRETKEY_PATH 中的密钥 | 字符串 | +| DISABLE_FLOWISE_TELEMETRY | 关闭遥测 | 字符串 | 您也可以在使用 `npx` 时指定环境变量。例如: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cfb7d3a9..88d1aaac 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -138,8 +138,10 @@ Flowise support different environment variables to configure your instance. You | DATABASE_USER | Database username (When DATABASE_TYPE is not sqlite) | String | | | DATABASE_PASSWORD | Database password (When DATABASE_TYPE is not sqlite) | String | | | DATABASE_NAME | Database name (When DATABASE_TYPE is not sqlite) | String | | +| DATABASE_SSL | Database connection overssl (When DATABASE_TYPE is postgre) | Boolean | false | | SECRETKEY_PATH | Location where encryption key (used to encrypt/decrypt credentials) is saved | String | `your-path/Flowise/packages/server` | | FLOWISE_SECRETKEY_OVERWRITE | Encryption key to be used instead of the key stored in SECRETKEY_PATH | String | +| DISABLE_FLOWISE_TELEMETRY | Turn off telemetry | Boolean | You can also specify the env variables when using `npx`. For example: diff --git a/LICENSE.md b/LICENSE.md index 0f4afcd1..80800001 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -2,22 +2,6 @@ Version 2.0, January 2004 http://www.apache.org/licenses/ -Flowise is governed by the Apache License 2.0, with additional terms and conditions outlined below: - -Flowise can be used for commercial purposes for "backend-as-a-service" for your applications or as a development platform for enterprises. However, under specific conditions, you must reach out to the project's administrators to secure a commercial license: - -a. Multi-tenant SaaS service: Unless you have explicit written authorization from Flowise, you may not utilize the Flowise source code to operate a multi-tenant SaaS service that closely resembles the Flowise cloud-based services. -b. Logo and copyright information: While using Flowise in commercial application, you are prohibited from removing or altering the LOGO or copyright information displayed in the Flowise console and UI. - -For inquiries regarding licensing matters, please contact hello@flowiseai.com via email. - -Contributors are required to consent to the following terms related to their contributed code: - -a. The project maintainers have the authority to modify the open-source agreement to be more stringent or lenient. -b. Contributed code can be used for commercial purposes, including Flowise's cloud-based services. - -All other rights and restrictions are in accordance with the Apache License 2.0. - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. diff --git a/README-ZH.md b/README-ZH.md index 2805ef9b..8750ebc7 100644 --- a/README-ZH.md +++ b/README-ZH.md @@ -145,25 +145,40 @@ Flowise 支持不同的环境变量来配置您的实例。您可以在 `package ## 🌐 自托管 -### [Railway](https://docs.flowiseai.com/deployment/railway) +在您现有的基础设施中部署自托管的 Flowise,我们支持各种[部署](https://docs.flowiseai.com/configuration/deployment) -[![在 Railway 上部署](https://railway.app/button.svg)](https://railway.app/template/pn4G8S?referralCode=WVNPD9) +- [AWS](https://docs.flowiseai.com/deployment/aws) +- [Azure](https://docs.flowiseai.com/deployment/azure) +- [Digital Ocean](https://docs.flowiseai.com/deployment/digital-ocean) +- [GCP](https://docs.flowiseai.com/deployment/gcp) +-
+ 其他 -### [Render](https://docs.flowiseai.com/deployment/render) + - [Railway](https://docs.flowiseai.com/deployment/railway) -[![部署到 Render](https://render.com/images/deploy-to-render-button.svg)](https://docs.flowiseai.com/deployment/render) + [![在 Railway 上部署](https://railway.app/button.svg)](https://railway.app/template/pn4G8S?referralCode=WVNPD9) -### [HuggingFace Spaces](https://docs.flowiseai.com/deployment/hugging-face) + - [Render](https://docs.flowiseai.com/deployment/render) -HuggingFace Spaces + [![部署到 Render](https://render.com/images/deploy-to-render-button.svg)](https://docs.flowiseai.com/deployment/render) -### [AWS](https://docs.flowiseai.com/deployment/aws) + - [HuggingFace Spaces](https://docs.flowiseai.com/deployment/hugging-face) -### [Azure](https://docs.flowiseai.com/deployment/azure) + HuggingFace Spaces -### [DigitalOcean](https://docs.flowiseai.com/deployment/digital-ocean) + - [Elestio](https://elest.io/open-source/flowiseai) -### [GCP](https://docs.flowiseai.com/deployment/gcp) + [![Deploy](https://pub-da36157c854648669813f3f76c526c2b.r2.dev/deploy-on-elestio-black.png)](https://elest.io/open-source/flowiseai) + + - [Sealos](https://cloud.sealos.io/?openapp=system-template%3FtemplateName%3Dflowise) + + [![部署到 Sealos](https://raw.githubusercontent.com/labring-actions/templates/main/Deploy-on-Sealos.svg)](https://cloud.sealos.io/?openapp=system-template%3FtemplateName%3Dflowise) + + - [RepoCloud](https://repocloud.io/details/?app_id=29) + + [![部署到 RepoCloud](https://d16t0pc4846x52.cloudfront.net/deploy.png)](https://repocloud.io/details/?app_id=29) + +
## 💻 云托管 diff --git a/README.md b/README.md index 25026237..3e6b7e56 100644 --- a/README.md +++ b/README.md @@ -145,29 +145,40 @@ Flowise support different environment variables to configure your instance. You ## 🌐 Self Host -### [Railway](https://docs.flowiseai.com/deployment/railway) +Deploy Flowise self-hosted in your existing infrastructure, we support various [deployments](https://docs.flowiseai.com/configuration/deployment) -[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/pn4G8S?referralCode=WVNPD9) +- [AWS](https://docs.flowiseai.com/deployment/aws) +- [Azure](https://docs.flowiseai.com/deployment/azure) +- [Digital Ocean](https://docs.flowiseai.com/deployment/digital-ocean) +- [GCP](https://docs.flowiseai.com/deployment/gcp) +-
+ Others -### [Render](https://docs.flowiseai.com/deployment/render) + - [Railway](https://docs.flowiseai.com/deployment/railway) -[![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://docs.flowiseai.com/deployment/render) + [![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/pn4G8S?referralCode=WVNPD9) -### [Elestio](https://elest.io/open-source/flowiseai) + - [Render](https://docs.flowiseai.com/deployment/render) -[![Deploy](https://pub-da36157c854648669813f3f76c526c2b.r2.dev/deploy-on-elestio-black.png)](https://elest.io/open-source/flowiseai) + [![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://docs.flowiseai.com/deployment/render) -### [HuggingFace Spaces](https://docs.flowiseai.com/deployment/hugging-face) + - [HuggingFace Spaces](https://docs.flowiseai.com/deployment/hugging-face) -HuggingFace Spaces + HuggingFace Spaces -### [AWS](https://docs.flowiseai.com/deployment/aws) + - [Elestio](https://elest.io/open-source/flowiseai) -### [Azure](https://docs.flowiseai.com/deployment/azure) + [![Deploy](https://pub-da36157c854648669813f3f76c526c2b.r2.dev/deploy-on-elestio-black.png)](https://elest.io/open-source/flowiseai) -### [DigitalOcean](https://docs.flowiseai.com/deployment/digital-ocean) + - [Sealos](https://cloud.sealos.io/?openapp=system-template%3FtemplateName%3Dflowise) -### [GCP](https://docs.flowiseai.com/deployment/gcp) + [![](https://raw.githubusercontent.com/labring-actions/templates/main/Deploy-on-Sealos.svg)](https://cloud.sealos.io/?openapp=system-template%3FtemplateName%3Dflowise) + + - [RepoCloud](https://repocloud.io/details/?app_id=29) + + [![Deploy on RepoCloud](https://d16t0pc4846x52.cloudfront.net/deploy.png)](https://repocloud.io/details/?app_id=29) + +
## 💻 Cloud Hosted diff --git a/artillery-load-test.yml b/artillery-load-test.yml index 6b1c8140..809a2a8e 100644 --- a/artillery-load-test.yml +++ b/artillery-load-test.yml @@ -33,4 +33,4 @@ scenarios: # Seconds # Total Users = 2 + 3 + 3 = 8 # Each making 1 HTTP call -# Over a duration of 3 seconds +# Over a durations of 3 seconds diff --git a/docker/.env.example b/docker/.env.example index 967a1ab6..0fe69dd1 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -12,6 +12,7 @@ LOG_PATH=/root/.flowise/logs # DATABASE_NAME="flowise" # DATABASE_USER="" # DATABASE_PASSWORD="" +# DATABASE_SSL=true # FLOWISE_USERNAME=user # FLOWISE_PASSWORD=1234 @@ -24,4 +25,6 @@ LOG_PATH=/root/.flowise/logs # LANGCHAIN_TRACING_V2=true # LANGCHAIN_ENDPOINT=https://api.smith.langchain.com # LANGCHAIN_API_KEY=your_api_key -# LANGCHAIN_PROJECT=your_project \ No newline at end of file +# LANGCHAIN_PROJECT=your_project + +# DISABLE_FLOWISE_TELEMETRY=true \ No newline at end of file diff --git a/docker/README.md b/docker/README.md index d3ad1c19..11b29cf3 100644 --- a/docker/README.md +++ b/docker/README.md @@ -1,6 +1,6 @@ # Flowise Docker Hub Image -Starts Flowise from [DockerHub Image](https://hub.docker.com/repository/docker/flowiseai/flowise/general) +Starts Flowise from [DockerHub Image](https://hub.docker.com/r/flowiseai/flowise) ## Usage diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 8e0e1af5..c8c88bf3 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -16,11 +16,13 @@ services: - DATABASE_NAME=${DATABASE_NAME} - DATABASE_USER=${DATABASE_USER} - DATABASE_PASSWORD=${DATABASE_PASSWORD} + - DATABASE_SSL=${DATABASE_SSL} - APIKEY_PATH=${APIKEY_PATH} - SECRETKEY_PATH=${SECRETKEY_PATH} - FLOWISE_SECRETKEY_OVERWRITE=${FLOWISE_SECRETKEY_OVERWRITE} - LOG_LEVEL=${LOG_LEVEL} - LOG_PATH=${LOG_PATH} + - DISABLE_FLOWISE_TELEMETRY=${DISABLE_FLOWISE_TELEMETRY} ports: - '${PORT}:${PORT}' volumes: diff --git a/package.json b/package.json index 649a9a47..561694d0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "flowise", - "version": "1.4.3", + "version": "1.4.10", "private": true, "homepage": "https://flowiseai.com", "workspaces": [ @@ -48,7 +48,7 @@ "pretty-quick": "^3.1.3", "rimraf": "^3.0.2", "run-script-os": "^1.1.6", - "turbo": "1.7.4", + "turbo": "^1.7.4", "typescript": "^4.8.4" }, "engines": { diff --git a/packages/components/credentials/AstraApi.credential.ts b/packages/components/credentials/AstraApi.credential.ts new file mode 100644 index 00000000..a89a259f --- /dev/null +++ b/packages/components/credentials/AstraApi.credential.ts @@ -0,0 +1,34 @@ +import { INodeParams, INodeCredential } from '../src/Interface' + +class AstraDBApi implements INodeCredential { + label: string + name: string + version: number + description: string + inputs: INodeParams[] + + constructor() { + this.label = 'Astra DB API' + this.name = 'AstraDBApi' + this.version = 1.0 + this.inputs = [ + { + label: 'Astra DB Collection Name', + name: 'collectionName', + type: 'string' + }, + { + label: 'Astra DB Application Token', + name: 'applicationToken', + type: 'password' + }, + { + label: 'Astra DB Api Endpoint', + name: 'dbEndPoint', + type: 'string' + } + ] + } +} + +module.exports = { credClass: AstraDBApi } diff --git a/packages/components/credentials/GoogleGenerativeAI.credential.ts b/packages/components/credentials/GoogleGenerativeAI.credential.ts new file mode 100644 index 00000000..e5ad45bf --- /dev/null +++ b/packages/components/credentials/GoogleGenerativeAI.credential.ts @@ -0,0 +1,26 @@ +import { INodeParams, INodeCredential } from '../src/Interface' + +class GoogleGenerativeAICredential implements INodeCredential { + label: string + name: string + version: number + description: string + inputs: INodeParams[] + + constructor() { + this.label = 'Google Generative AI' + this.name = 'googleGenerativeAI' + this.version = 1.0 + this.description = + 'You can get your API key from official page here.' + this.inputs = [ + { + label: 'Google AI API Key', + name: 'googleGenerativeAPIKey', + type: 'password' + } + ] + } +} + +module.exports = { credClass: GoogleGenerativeAICredential } diff --git a/packages/components/credentials/ZapierNLAApi.credential.ts b/packages/components/credentials/LocalAIApi.credential.ts similarity index 51% rename from packages/components/credentials/ZapierNLAApi.credential.ts rename to packages/components/credentials/LocalAIApi.credential.ts index 72035660..4aafe040 100644 --- a/packages/components/credentials/ZapierNLAApi.credential.ts +++ b/packages/components/credentials/LocalAIApi.credential.ts @@ -1,24 +1,23 @@ import { INodeParams, INodeCredential } from '../src/Interface' -class ZapierNLAApi implements INodeCredential { +class LocalAIApi implements INodeCredential { label: string name: string version: number - description: string inputs: INodeParams[] constructor() { - this.label = 'Zapier NLA API' - this.name = 'zapierNLAApi' + this.label = 'LocalAI API' + this.name = 'localAIApi' this.version = 1.0 this.inputs = [ { - label: 'Zapier NLA Api Key', - name: 'zapierNLAApiKey', + label: 'LocalAI Api Key', + name: 'localAIApiKey', type: 'password' } ] } } -module.exports = { credClass: ZapierNLAApi } +module.exports = { credClass: LocalAIApi } diff --git a/packages/components/credentials/MistralApi.credential.ts b/packages/components/credentials/MistralApi.credential.ts new file mode 100644 index 00000000..a254f665 --- /dev/null +++ b/packages/components/credentials/MistralApi.credential.ts @@ -0,0 +1,25 @@ +import { INodeParams, INodeCredential } from '../src/Interface' + +class MistralAICredential implements INodeCredential { + label: string + name: string + version: number + description: string + inputs: INodeParams[] + + constructor() { + this.label = 'MistralAI API' + this.name = 'mistralAIApi' + this.version = 1.0 + this.description = 'You can get your API key from official console here.' + this.inputs = [ + { + label: 'MistralAI API Key', + name: 'mistralAIAPIKey', + type: 'password' + } + ] + } +} + +module.exports = { credClass: MistralAICredential } diff --git a/packages/components/credentials/PineconeApi.credential.ts b/packages/components/credentials/PineconeApi.credential.ts index 4c5f62fe..486c9834 100644 --- a/packages/components/credentials/PineconeApi.credential.ts +++ b/packages/components/credentials/PineconeApi.credential.ts @@ -16,11 +16,6 @@ class PineconeApi implements INodeCredential { label: 'Pinecone Api Key', name: 'pineconeApiKey', type: 'password' - }, - { - label: 'Pinecone Environment', - name: 'pineconeEnv', - type: 'string' } ] } diff --git a/packages/components/credentials/RedisCacheApi.credential.ts b/packages/components/credentials/RedisCacheApi.credential.ts index 4d1a2498..2b4ad618 100644 --- a/packages/components/credentials/RedisCacheApi.credential.ts +++ b/packages/components/credentials/RedisCacheApi.credential.ts @@ -35,6 +35,11 @@ class RedisCacheApi implements INodeCredential { name: 'redisCachePwd', type: 'password', placeholder: '' + }, + { + label: 'Use SSL', + name: 'redisCacheSslEnabled', + type: 'boolean' } ] } diff --git a/packages/components/nodes/agents/AirtableAgent/airtable.svg b/packages/components/nodes/agents/AirtableAgent/airtable.svg index 867c3b5a..5c2a9950 100644 --- a/packages/components/nodes/agents/AirtableAgent/airtable.svg +++ b/packages/components/nodes/agents/AirtableAgent/airtable.svg @@ -1,9 +1,5 @@ - - - - - - - - + + + + diff --git a/packages/components/nodes/agents/AutoGPT/AutoGPT.ts b/packages/components/nodes/agents/AutoGPT/AutoGPT.ts index 6c26b944..43b490f4 100644 --- a/packages/components/nodes/agents/AutoGPT/AutoGPT.ts +++ b/packages/components/nodes/agents/AutoGPT/AutoGPT.ts @@ -29,7 +29,7 @@ class AutoGPT_Agents implements INode { this.version = 1.0 this.type = 'AutoGPT' this.category = 'Agents' - this.icon = 'autogpt.png' + this.icon = 'autogpt.svg' this.description = 'Autonomous agent with chain of thoughts for self-guided task completion' this.baseClasses = ['AutoGPT'] this.inputs = [ diff --git a/packages/components/nodes/agents/AutoGPT/autogpt.png b/packages/components/nodes/agents/AutoGPT/autogpt.png deleted file mode 100644 index bdeff726..00000000 Binary files a/packages/components/nodes/agents/AutoGPT/autogpt.png and /dev/null differ diff --git a/packages/components/nodes/agents/AutoGPT/autogpt.svg b/packages/components/nodes/agents/AutoGPT/autogpt.svg new file mode 100644 index 00000000..99da2d0f --- /dev/null +++ b/packages/components/nodes/agents/AutoGPT/autogpt.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/packages/components/nodes/agents/BabyAGI/BabyAGI.ts b/packages/components/nodes/agents/BabyAGI/BabyAGI.ts index e31f31c6..f82f134b 100644 --- a/packages/components/nodes/agents/BabyAGI/BabyAGI.ts +++ b/packages/components/nodes/agents/BabyAGI/BabyAGI.ts @@ -20,7 +20,7 @@ class BabyAGI_Agents implements INode { this.version = 1.0 this.type = 'BabyAGI' this.category = 'Agents' - this.icon = 'babyagi.jpg' + this.icon = 'babyagi.svg' this.description = 'Task Driven Autonomous Agent which creates new task and reprioritizes task list based on objective' this.baseClasses = ['BabyAGI'] this.inputs = [ diff --git a/packages/components/nodes/agents/BabyAGI/babyagi.jpg b/packages/components/nodes/agents/BabyAGI/babyagi.jpg deleted file mode 100644 index cd585139..00000000 Binary files a/packages/components/nodes/agents/BabyAGI/babyagi.jpg and /dev/null differ diff --git a/packages/components/nodes/agents/BabyAGI/babyagi.svg b/packages/components/nodes/agents/BabyAGI/babyagi.svg new file mode 100644 index 00000000..2fadd1b9 --- /dev/null +++ b/packages/components/nodes/agents/BabyAGI/babyagi.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/components/nodes/agents/CSVAgent/CSVAgent.ts b/packages/components/nodes/agents/CSVAgent/CSVAgent.ts index 5baad2ec..70996c4d 100644 --- a/packages/components/nodes/agents/CSVAgent/CSVAgent.ts +++ b/packages/components/nodes/agents/CSVAgent/CSVAgent.ts @@ -23,7 +23,7 @@ class CSV_Agents implements INode { this.version = 1.0 this.type = 'AgentExecutor' this.category = 'Agents' - this.icon = 'csvagent.png' + this.icon = 'CSVagent.svg' this.description = 'Agent used to to answer queries on CSV data' this.baseClasses = [this.type, ...getBaseClasses(AgentExecutor)] this.inputs = [ diff --git a/packages/components/nodes/agents/CSVAgent/CSVagent.svg b/packages/components/nodes/agents/CSVAgent/CSVagent.svg new file mode 100644 index 00000000..f57bd886 --- /dev/null +++ b/packages/components/nodes/agents/CSVAgent/CSVagent.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/packages/components/nodes/agents/CSVAgent/csvagent.png b/packages/components/nodes/agents/CSVAgent/csvagent.png deleted file mode 100644 index 3ed16bb2..00000000 Binary files a/packages/components/nodes/agents/CSVAgent/csvagent.png and /dev/null differ diff --git a/packages/components/nodes/agents/ConversationalAgent/ConversationalAgent.ts b/packages/components/nodes/agents/ConversationalAgent/ConversationalAgent.ts index cb5d3189..7f857b1c 100644 --- a/packages/components/nodes/agents/ConversationalAgent/ConversationalAgent.ts +++ b/packages/components/nodes/agents/ConversationalAgent/ConversationalAgent.ts @@ -1,11 +1,14 @@ -import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' -import { initializeAgentExecutorWithOptions, AgentExecutor, InitializeAgentExecutorOptions } from 'langchain/agents' import { Tool } from 'langchain/tools' -import { BaseChatMemory } from 'langchain/memory' -import { getBaseClasses } from '../../../src/utils' import { BaseChatModel } from 'langchain/chat_models/base' import { flatten } from 'lodash' -import { additionalCallbacks } from '../../../src/handler' +import { AgentStep, BaseMessage, ChainValues, AIMessage, HumanMessage } from 'langchain/schema' +import { RunnableSequence } from 'langchain/schema/runnable' +import { getBaseClasses } from '../../../src/utils' +import { ConsoleCallbackHandler, CustomChainHandler, additionalCallbacks } from '../../../src/handler' +import { FlowiseMemory, ICommonObject, IMessage, INode, INodeData, INodeParams } from '../../../src/Interface' +import { AgentExecutor } from '../../../src/agents' +import { ChatConversationalAgent } from 'langchain/agents' +import { renderTemplate } from '@langchain/core/prompts' const DEFAULT_PREFIX = `Assistant is a large language model trained by OpenAI. @@ -15,6 +18,15 @@ Assistant is constantly learning and improving, and its capabilities are constan Overall, Assistant is a powerful system that can help with a wide range of tasks and provide valuable insights and information on a wide range of topics. Whether you need help with a specific question or just want to have a conversation about a particular topic, Assistant is here to assist.` +const TEMPLATE_TOOL_RESPONSE = `TOOL RESPONSE: +--------------------- +{observation} + +USER'S INPUT +-------------------- + +Okay, so what is the response to my last comment? If using information obtained from the tools you must mention it explicitly without mentioning the tool names - I have forgotten all TOOL RESPONSES! Remember to respond with a markdown code snippet of a json blob with a single action, and NOTHING else.` + class ConversationalAgent_Agents implements INode { label: string name: string @@ -25,8 +37,9 @@ class ConversationalAgent_Agents implements INode { category: string baseClasses: string[] inputs: INodeParams[] + sessionId?: string - constructor() { + constructor(fields?: { sessionId?: string }) { this.label = 'Conversational Agent' this.name = 'conversationalAgent' this.version = 2.0 @@ -43,7 +56,7 @@ class ConversationalAgent_Agents implements INode { list: true }, { - label: 'Language Model', + label: 'Chat Model', name: 'model', type: 'BaseChatModel' }, @@ -62,51 +75,114 @@ class ConversationalAgent_Agents implements INode { additionalParams: true } ] + this.sessionId = fields?.sessionId } - async init(nodeData: INodeData): Promise { - const model = nodeData.inputs?.model as BaseChatModel - let tools = nodeData.inputs?.tools as Tool[] - tools = flatten(tools) - const memory = nodeData.inputs?.memory as BaseChatMemory - const systemMessage = nodeData.inputs?.systemMessage as string - - const obj: InitializeAgentExecutorOptions = { - agentType: 'chat-conversational-react-description', - verbose: process.env.DEBUG === 'true' ? true : false - } - - const agentArgs: any = {} - if (systemMessage) { - agentArgs.systemMessage = systemMessage - } - - if (Object.keys(agentArgs).length) obj.agentArgs = agentArgs - - const executor = await initializeAgentExecutorWithOptions(tools, model, obj) - executor.memory = memory - return executor + async init(nodeData: INodeData, input: string, options: ICommonObject): Promise { + return prepareAgent(nodeData, { sessionId: this.sessionId, chatId: options.chatId, input }, options.chatHistory) } async run(nodeData: INodeData, input: string, options: ICommonObject): Promise { - const executor = nodeData.instance as AgentExecutor - const memory = nodeData.inputs?.memory - memory.returnMessages = true // Return true for BaseChatModel - - /* When incomingInput.history is provided, only force replace chatHistory if its ShortTermMemory - * LongTermMemory will automatically retrieved chatHistory from sessionId - */ - if (options && options.chatHistory && memory.isShortTermMemory) { - await memory.resumeMessages(options.chatHistory) - } - - executor.memory = memory + const memory = nodeData.inputs?.memory as FlowiseMemory + const executor = await prepareAgent(nodeData, { sessionId: this.sessionId, chatId: options.chatId, input }, options.chatHistory) + const loggerHandler = new ConsoleCallbackHandler(options.logger) const callbacks = await additionalCallbacks(nodeData, options) - const result = await executor.call({ input }, [...callbacks]) - return result?.output + let res: ChainValues = {} + + if (options.socketIO && options.socketIOClientId) { + const handler = new CustomChainHandler(options.socketIO, options.socketIOClientId) + res = await executor.invoke({ input }, { callbacks: [loggerHandler, handler, ...callbacks] }) + } else { + res = await executor.invoke({ input }, { callbacks: [loggerHandler, ...callbacks] }) + } + + await memory.addChatMessages( + [ + { + text: input, + type: 'userMessage' + }, + { + text: res?.output, + type: 'apiMessage' + } + ], + this.sessionId + ) + + return res?.output } } +const prepareAgent = async ( + nodeData: INodeData, + flowObj: { sessionId?: string; chatId?: string; input?: string }, + chatHistory: IMessage[] = [] +) => { + const model = nodeData.inputs?.model as BaseChatModel + let tools = nodeData.inputs?.tools as Tool[] + tools = flatten(tools) + const memory = nodeData.inputs?.memory as FlowiseMemory + const systemMessage = nodeData.inputs?.systemMessage as string + const memoryKey = memory.memoryKey ? memory.memoryKey : 'chat_history' + const inputKey = memory.inputKey ? memory.inputKey : 'input' + + /** Bind a stop token to the model */ + const modelWithStop = model.bind({ + stop: ['\nObservation'] + }) + + const outputParser = ChatConversationalAgent.getDefaultOutputParser({ + llm: model, + toolNames: tools.map((tool) => tool.name) + }) + + const prompt = ChatConversationalAgent.createPrompt(tools, { + systemMessage: systemMessage ? systemMessage : DEFAULT_PREFIX, + outputParser + }) + + const runnableAgent = RunnableSequence.from([ + { + [inputKey]: (i: { input: string; steps: AgentStep[] }) => i.input, + agent_scratchpad: async (i: { input: string; steps: AgentStep[] }) => await constructScratchPad(i.steps), + [memoryKey]: async (_: { input: string; steps: AgentStep[] }) => { + const messages = (await memory.getChatMessages(flowObj?.sessionId, true, chatHistory)) as BaseMessage[] + return messages ?? [] + } + }, + prompt, + modelWithStop, + outputParser + ]) + + const executor = AgentExecutor.fromAgentAndTools({ + agent: runnableAgent, + tools, + sessionId: flowObj?.sessionId, + chatId: flowObj?.chatId, + input: flowObj?.input, + verbose: process.env.DEBUG === 'true' ? true : false + }) + + return executor +} + +const constructScratchPad = async (steps: AgentStep[]): Promise => { + const thoughts: BaseMessage[] = [] + for (const step of steps) { + thoughts.push(new AIMessage(step.action.log)) + thoughts.push( + new HumanMessage( + renderTemplate(TEMPLATE_TOOL_RESPONSE, 'f-string', { + observation: step.observation + }) + ) + ) + } + return thoughts +} + module.exports = { nodeClass: ConversationalAgent_Agents } diff --git a/packages/components/nodes/agents/ConversationalAgent/agent.svg b/packages/components/nodes/agents/ConversationalAgent/agent.svg index c87861e5..62fd4a65 100644 --- a/packages/components/nodes/agents/ConversationalAgent/agent.svg +++ b/packages/components/nodes/agents/ConversationalAgent/agent.svg @@ -1,9 +1,7 @@ - - - - - - - - - \ No newline at end of file + + + + + + + diff --git a/packages/components/nodes/agents/ConversationalRetrievalAgent/ConversationalRetrievalAgent.ts b/packages/components/nodes/agents/ConversationalRetrievalAgent/ConversationalRetrievalAgent.ts index ce5b5e18..b238456a 100644 --- a/packages/components/nodes/agents/ConversationalRetrievalAgent/ConversationalRetrievalAgent.ts +++ b/packages/components/nodes/agents/ConversationalRetrievalAgent/ConversationalRetrievalAgent.ts @@ -1,9 +1,14 @@ -import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' -import { initializeAgentExecutorWithOptions, AgentExecutor } from 'langchain/agents' -import { getBaseClasses } from '../../../src/utils' +import { ChainValues, AgentStep, BaseMessage } from 'langchain/schema' import { flatten } from 'lodash' -import { BaseChatMemory } from 'langchain/memory' +import { ChatOpenAI } from 'langchain/chat_models/openai' +import { ChatPromptTemplate, MessagesPlaceholder } from 'langchain/prompts' +import { formatToOpenAIFunction } from 'langchain/tools' +import { RunnableSequence } from 'langchain/schema/runnable' +import { FlowiseMemory, ICommonObject, IMessage, INode, INodeData, INodeParams } from '../../../src/Interface' +import { getBaseClasses } from '../../../src/utils' import { ConsoleCallbackHandler, CustomChainHandler, additionalCallbacks } from '../../../src/handler' +import { OpenAIFunctionsAgentOutputParser } from 'langchain/agents/openai/output_parser' +import { AgentExecutor, formatAgentSteps } from '../../../src/agents' const defaultMessage = `Do your best to answer the questions. Feel free to use any tools available to look up relevant information, only if necessary.` @@ -17,8 +22,9 @@ class ConversationalRetrievalAgent_Agents implements INode { category: string baseClasses: string[] inputs: INodeParams[] + sessionId?: string - constructor() { + constructor(fields?: { sessionId?: string }) { this.label = 'Conversational Retrieval Agent' this.name = 'conversationalRetrievalAgent' this.version = 3.0 @@ -54,57 +60,98 @@ class ConversationalRetrievalAgent_Agents implements INode { additionalParams: true } ] + this.sessionId = fields?.sessionId } - async init(nodeData: INodeData): Promise { - const model = nodeData.inputs?.model - const systemMessage = nodeData.inputs?.systemMessage as string - const memory = nodeData.inputs?.memory as BaseChatMemory - - let tools = nodeData.inputs?.tools - tools = flatten(tools) - - const executor = await initializeAgentExecutorWithOptions(tools, model, { - agentType: 'openai-functions', - verbose: process.env.DEBUG === 'true' ? true : false, - agentArgs: { - prefix: systemMessage ?? defaultMessage - }, - returnIntermediateSteps: true - }) - executor.memory = memory - return executor + async init(nodeData: INodeData, input: string, options: ICommonObject): Promise { + return prepareAgent(nodeData, { sessionId: this.sessionId, chatId: options.chatId, input }, options.chatHistory) } async run(nodeData: INodeData, input: string, options: ICommonObject): Promise { - const executor = nodeData.instance as AgentExecutor - const memory = nodeData.inputs?.memory - - memory.memoryKey = 'chat_history' - memory.outputKey = 'output' - memory.returnMessages = true - - /* When incomingInput.history is provided, only force replace chatHistory if its ShortTermMemory - * LongTermMemory will automatically retrieved chatHistory from sessionId - */ - if (options && options.chatHistory && memory.isShortTermMemory) { - await memory.resumeMessages(options.chatHistory) - } + const memory = nodeData.inputs?.memory as FlowiseMemory + const executor = prepareAgent(nodeData, { sessionId: this.sessionId, chatId: options.chatId, input }, options.chatHistory) executor.memory = memory const loggerHandler = new ConsoleCallbackHandler(options.logger) const callbacks = await additionalCallbacks(nodeData, options) + let res: ChainValues = {} + if (options.socketIO && options.socketIOClientId) { const handler = new CustomChainHandler(options.socketIO, options.socketIOClientId) - const result = await executor.call({ input }, [loggerHandler, handler, ...callbacks]) - return result?.output + res = await executor.invoke({ input }, { callbacks: [loggerHandler, handler, ...callbacks] }) } else { - const result = await executor.call({ input }, [loggerHandler, ...callbacks]) - return result?.output + res = await executor.invoke({ input }, { callbacks: [loggerHandler, ...callbacks] }) } + + await memory.addChatMessages( + [ + { + text: input, + type: 'userMessage' + }, + { + text: res?.output, + type: 'apiMessage' + } + ], + this.sessionId + ) + + return res?.output } } +const prepareAgent = ( + nodeData: INodeData, + flowObj: { sessionId?: string; chatId?: string; input?: string }, + chatHistory: IMessage[] = [] +) => { + const model = nodeData.inputs?.model as ChatOpenAI + const memory = nodeData.inputs?.memory as FlowiseMemory + const systemMessage = nodeData.inputs?.systemMessage as string + let tools = nodeData.inputs?.tools + tools = flatten(tools) + const memoryKey = memory.memoryKey ? memory.memoryKey : 'chat_history' + const inputKey = memory.inputKey ? memory.inputKey : 'input' + + const prompt = ChatPromptTemplate.fromMessages([ + ['ai', systemMessage ? systemMessage : defaultMessage], + new MessagesPlaceholder(memoryKey), + ['human', `{${inputKey}}`], + new MessagesPlaceholder('agent_scratchpad') + ]) + + const modelWithFunctions = model.bind({ + functions: [...tools.map((tool: any) => formatToOpenAIFunction(tool))] + }) + + const runnableAgent = RunnableSequence.from([ + { + [inputKey]: (i: { input: string; steps: AgentStep[] }) => i.input, + agent_scratchpad: (i: { input: string; steps: AgentStep[] }) => formatAgentSteps(i.steps), + [memoryKey]: async (_: { input: string; steps: AgentStep[] }) => { + const messages = (await memory.getChatMessages(flowObj?.sessionId, true, chatHistory)) as BaseMessage[] + return messages ?? [] + } + }, + prompt, + modelWithFunctions, + new OpenAIFunctionsAgentOutputParser() + ]) + + const executor = AgentExecutor.fromAgentAndTools({ + agent: runnableAgent, + tools, + sessionId: flowObj?.sessionId, + chatId: flowObj?.chatId, + input: flowObj?.input, + returnIntermediateSteps: true, + verbose: process.env.DEBUG === 'true' ? true : false + }) + + return executor +} + module.exports = { nodeClass: ConversationalRetrievalAgent_Agents } diff --git a/packages/components/nodes/agents/ConversationalRetrievalAgent/agent.svg b/packages/components/nodes/agents/ConversationalRetrievalAgent/agent.svg index c87861e5..62fd4a65 100644 --- a/packages/components/nodes/agents/ConversationalRetrievalAgent/agent.svg +++ b/packages/components/nodes/agents/ConversationalRetrievalAgent/agent.svg @@ -1,9 +1,7 @@ - - - - - - - - - \ No newline at end of file + + + + + + + diff --git a/packages/components/nodes/agents/MRKLAgentChat/agent.svg b/packages/components/nodes/agents/MRKLAgentChat/agent.svg index c87861e5..62fd4a65 100644 --- a/packages/components/nodes/agents/MRKLAgentChat/agent.svg +++ b/packages/components/nodes/agents/MRKLAgentChat/agent.svg @@ -1,9 +1,7 @@ - - - - - - - - - \ No newline at end of file + + + + + + + diff --git a/packages/components/nodes/agents/MRKLAgentLLM/agent.svg b/packages/components/nodes/agents/MRKLAgentLLM/agent.svg index c87861e5..62fd4a65 100644 --- a/packages/components/nodes/agents/MRKLAgentLLM/agent.svg +++ b/packages/components/nodes/agents/MRKLAgentLLM/agent.svg @@ -1,9 +1,7 @@ - - - - - - - - - \ No newline at end of file + + + + + + + diff --git a/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts b/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts index 1a48be50..62ecec5b 100644 --- a/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts +++ b/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts @@ -8,6 +8,9 @@ import * as path from 'node:path' import fetch from 'node-fetch' import { flatten, uniqWith, isEqual } from 'lodash' import { zodToJsonSchema } from 'zod-to-json-schema' +import { AnalyticHandler } from '../../../src/handler' +import { Moderation, checkInputs, streamResponse } from '../../moderation/Moderation' +import { formatResponse } from '../../outputparsers/OutputParserHelpers' class OpenAIAssistant_Agents implements INode { label: string @@ -23,10 +26,10 @@ class OpenAIAssistant_Agents implements INode { constructor() { this.label = 'OpenAI Assistant' this.name = 'openAIAssistant' - this.version = 2.0 + this.version = 3.0 this.type = 'OpenAIAssistant' this.category = 'Agents' - this.icon = 'openai.png' + this.icon = 'assistant.svg' this.description = `An agent that uses OpenAI Assistant API to pick the tool and args to call` this.baseClasses = [this.type] this.inputs = [ @@ -42,6 +45,14 @@ class OpenAIAssistant_Agents implements INode { type: 'Tool', list: true }, + { + label: 'Input Moderation', + description: 'Detect text that could generate harmful output and prevent it from being sent to the language model', + name: 'inputModeration', + type: 'Moderation', + optional: true, + list: true + }, { label: 'Disable File Download', name: 'disableFileDownload', @@ -81,8 +92,56 @@ class OpenAIAssistant_Agents implements INode { } } - async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { - return new OpenAIAssistant({ nodeData, options }) + async init(): Promise { + return null + } + + async clearChatMessages(nodeData: INodeData, options: ICommonObject, sessionIdObj: { type: string; id: string }): Promise { + const selectedAssistantId = nodeData.inputs?.selectedAssistant as string + const appDataSource = options.appDataSource as DataSource + const databaseEntities = options.databaseEntities as IDatabaseEntity + + const assistant = await appDataSource.getRepository(databaseEntities['Assistant']).findOneBy({ + id: selectedAssistantId + }) + + if (!assistant) { + options.logger.error(`Assistant ${selectedAssistantId} not found`) + return + } + + if (!sessionIdObj) return + + let sessionId = '' + if (sessionIdObj.type === 'chatId') { + const chatId = sessionIdObj.id + const chatmsg = await appDataSource.getRepository(databaseEntities['ChatMessage']).findOneBy({ + chatId + }) + if (!chatmsg) { + options.logger.error(`Chat Message with Chat Id: ${chatId} not found`) + return + } + sessionId = chatmsg.sessionId + } else if (sessionIdObj.type === 'threadId') { + sessionId = sessionIdObj.id + } + + const credentialData = await getCredentialData(assistant.credential ?? '', options) + const openAIApiKey = getCredentialParam('openAIApiKey', credentialData, nodeData) + if (!openAIApiKey) { + options.logger.error(`OpenAI ApiKey not found`) + return + } + + const openai = new OpenAI({ apiKey: openAIApiKey }) + options.logger.info(`Clearing OpenAI Thread ${sessionId}`) + try { + if (sessionId) await openai.beta.threads.del(sessionId) + options.logger.info(`Successfully cleared OpenAI Thread ${sessionId}`) + } catch (e) { + throw new Error(e) + } } async run(nodeData: INodeData, input: string, options: ICommonObject): Promise { @@ -90,6 +149,20 @@ class OpenAIAssistant_Agents implements INode { const appDataSource = options.appDataSource as DataSource const databaseEntities = options.databaseEntities as IDatabaseEntity const disableFileDownload = nodeData.inputs?.disableFileDownload as boolean + const moderations = nodeData.inputs?.inputModeration as Moderation[] + const isStreaming = options.socketIO && options.socketIOClientId + const socketIO = isStreaming ? options.socketIO : undefined + const socketIOClientId = isStreaming ? options.socketIOClientId : '' + + if (moderations && moderations.length > 0) { + try { + input = await checkInputs(moderations, input) + } catch (e) { + await new Promise((resolve) => setTimeout(resolve, 500)) + streamResponse(isStreaming, e.message, socketIO, socketIOClientId) + return formatResponse(e.message) + } + } let tools = nodeData.inputs?.tools tools = flatten(tools) @@ -107,6 +180,11 @@ class OpenAIAssistant_Agents implements INode { const openai = new OpenAI({ apiKey: openAIApiKey }) + // Start analytics + const analyticHandlers = new AnalyticHandler(nodeData, options) + await analyticHandlers.init() + const parentIds = await analyticHandlers.onChainStart('OpenAIAssistant', input) + try { const assistantDetails = JSON.parse(assistant.details) const openAIAssistantId = assistantDetails.id @@ -129,7 +207,8 @@ class OpenAIAssistant_Agents implements INode { } const chatmessage = await appDataSource.getRepository(databaseEntities['ChatMessage']).findOneBy({ - chatId: options.chatId + chatId: options.chatId, + chatflowid: options.chatflowid }) let threadId = '' @@ -143,7 +222,7 @@ class OpenAIAssistant_Agents implements INode { threadId = thread.id } - // List all runs + // List all runs, in case existing thread is still running if (!isNewThread) { const promise = (threadId: string) => { return new Promise((resolve) => { @@ -179,6 +258,7 @@ class OpenAIAssistant_Agents implements INode { }) // Run assistant thread + const llmIds = await analyticHandlers.onLLMStart('ChatOpenAI', input, parentIds) const runThread = await openai.beta.threads.runs.create(threadId, { assistant_id: retrievedAssistant.id }) @@ -199,7 +279,12 @@ class OpenAIAssistant_Agents implements INode { const actions: ICommonObject[] = [] run.required_action.submit_tool_outputs.tool_calls.forEach((item) => { const functionCall = item.function - const args = JSON.parse(functionCall.arguments) + 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, @@ -211,26 +296,57 @@ class OpenAIAssistant_Agents implements INode { for (let i = 0; i < actions.length; i += 1) { const tool = tools.find((tool: any) => tool.name === actions[i].tool) if (!tool) continue - const toolOutput = await tool.call(actions[i].toolInput) - submitToolOutputs.push({ - tool_call_id: actions[i].toolCallId, - output: toolOutput - }) - usedTools.push({ - tool: tool.name, - toolInput: actions[i].toolInput, - toolOutput - }) + + // Start tool analytics + const toolIds = await analyticHandlers.onToolStart(tool.name, actions[i].toolInput, parentIds) + if (options.socketIO && options.socketIOClientId) + options.socketIO.to(options.socketIOClientId).emit('tool', 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}` + ) + ) + break + } } - if (submitToolOutputs.length) { - 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') + 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') { @@ -260,7 +376,9 @@ class OpenAIAssistant_Agents implements INode { runThreadId = newRunThread.id state = await promise(threadId, newRunThread.id) } else { - throw new Error(`Error processing thread: ${state}, Thread ID: ${threadId}`) + const errMsg = `Error processing thread: ${state}, Thread ID: ${threadId}` + await analyticHandlers.onChainError(parentIds, errMsg) + throw new Error(errMsg) } } @@ -345,11 +463,19 @@ class OpenAIAssistant_Agents implements INode { const bitmap = fsDefault.readFileSync(filePath) const base64String = Buffer.from(bitmap).toString('base64') + // TODO: Use a file path and retrieve image on the fly. Storing as base64 to localStorage and database will easily hit limits const imgHTML = `${fileObj.filename}
` returnVal += imgHTML } } + const imageRegex = /]*\/>/g + let llmOutput = returnVal.replace(imageRegex, '') + llmOutput = llmOutput.replace('
', '') + + await analyticHandlers.onLLMEnd(llmIds, llmOutput) + await analyticHandlers.onChainEnd(parentIds, messageData, true) + return { text: returnVal, usedTools, @@ -357,6 +483,7 @@ class OpenAIAssistant_Agents implements INode { assistant: { assistantId: openAIAssistantId, threadId, runId: runThreadId, messages: messageData } } } catch (error) { + await analyticHandlers.onChainError(parentIds, error, true) throw new Error(error) } } @@ -417,58 +544,4 @@ const formatToOpenAIAssistantTool = (tool: any): OpenAI.Beta.AssistantCreatePara } } -interface OpenAIAssistantInput { - nodeData: INodeData - options: ICommonObject -} - -class OpenAIAssistant { - nodeData: INodeData - options: ICommonObject = {} - - constructor(fields: OpenAIAssistantInput) { - this.nodeData = fields.nodeData - this.options = fields.options - } - - async clearChatMessages(): Promise { - const selectedAssistantId = this.nodeData.inputs?.selectedAssistant as string - const appDataSource = this.options.appDataSource as DataSource - const databaseEntities = this.options.databaseEntities as IDatabaseEntity - let sessionId = this.nodeData.inputs?.sessionId as string - - const assistant = await appDataSource.getRepository(databaseEntities['Assistant']).findOneBy({ - id: selectedAssistantId - }) - - if (!assistant) { - this.options.logger.error(`Assistant ${selectedAssistantId} not found`) - return - } - - if (!sessionId && this.options.chatId) { - const chatmsg = await appDataSource.getRepository(databaseEntities['ChatMessage']).findOneBy({ - chatId: this.options.chatId - }) - if (!chatmsg) { - this.options.logger.error(`Chat Message with Chat Id: ${this.options.chatId} not found`) - return - } - sessionId = chatmsg.sessionId - } - - const credentialData = await getCredentialData(assistant.credential ?? '', this.options) - const openAIApiKey = getCredentialParam('openAIApiKey', credentialData, this.nodeData) - if (!openAIApiKey) { - this.options.logger.error(`OpenAI ApiKey not found`) - return - } - - const openai = new OpenAI({ apiKey: openAIApiKey }) - this.options.logger.info(`Clearing OpenAI Thread ${sessionId}`) - if (sessionId) await openai.beta.threads.del(sessionId) - this.options.logger.info(`Successfully cleared OpenAI Thread ${sessionId}`) - } -} - module.exports = { nodeClass: OpenAIAssistant_Agents } diff --git a/packages/components/nodes/agents/OpenAIAssistant/assistant.svg b/packages/components/nodes/agents/OpenAIAssistant/assistant.svg new file mode 100644 index 00000000..ddef9617 --- /dev/null +++ b/packages/components/nodes/agents/OpenAIAssistant/assistant.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/packages/components/nodes/agents/OpenAIAssistant/openai.png b/packages/components/nodes/agents/OpenAIAssistant/openai.png deleted file mode 100644 index de08a05b..00000000 Binary files a/packages/components/nodes/agents/OpenAIAssistant/openai.png and /dev/null differ diff --git a/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts b/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts index 781292d7..c21c887a 100644 --- a/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts +++ b/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts @@ -1,10 +1,14 @@ -import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' -import { initializeAgentExecutorWithOptions, AgentExecutor } from 'langchain/agents' +import { ChainValues, AgentStep, BaseMessage } from 'langchain/schema' import { getBaseClasses } from '../../../src/utils' -import { BaseLanguageModel } from 'langchain/base_language' import { flatten } from 'lodash' -import { BaseChatMemory } from 'langchain/memory' +import { RunnableSequence } from 'langchain/schema/runnable' +import { formatToOpenAIFunction } from 'langchain/tools' +import { ChatOpenAI } from 'langchain/chat_models/openai' +import { FlowiseMemory, ICommonObject, IMessage, INode, INodeData, INodeParams } from '../../../src/Interface' import { ConsoleCallbackHandler, CustomChainHandler, additionalCallbacks } from '../../../src/handler' +import { ChatPromptTemplate, MessagesPlaceholder } from 'langchain/prompts' +import { OpenAIFunctionsAgentOutputParser } from 'langchain/agents/openai/output_parser' +import { AgentExecutor, formatAgentSteps } from '../../../src/agents' class OpenAIFunctionAgent_Agents implements INode { label: string @@ -16,14 +20,15 @@ class OpenAIFunctionAgent_Agents implements INode { category: string baseClasses: string[] inputs: INodeParams[] + sessionId?: string - constructor() { + constructor(fields?: { sessionId?: string }) { this.label = 'OpenAI Function Agent' this.name = 'openAIFunctionAgent' this.version = 3.0 this.type = 'AgentExecutor' this.category = 'Agents' - this.icon = 'openai.png' + this.icon = 'function.svg' this.description = `An agent that uses Function Calling to pick the tool and args to call` this.baseClasses = [this.type, ...getBaseClasses(AgentExecutor)] this.inputs = [ @@ -52,53 +57,95 @@ class OpenAIFunctionAgent_Agents implements INode { additionalParams: true } ] + this.sessionId = fields?.sessionId } - async init(nodeData: INodeData): Promise { - const model = nodeData.inputs?.model as BaseLanguageModel - const systemMessage = nodeData.inputs?.systemMessage as string - const memory = nodeData.inputs?.memory as BaseChatMemory - - let tools = nodeData.inputs?.tools - tools = flatten(tools) - - const executor = await initializeAgentExecutorWithOptions(tools, model, { - agentType: 'openai-functions', - verbose: process.env.DEBUG === 'true' ? true : false, - agentArgs: { - prefix: systemMessage ?? `You are a helpful AI assistant.` - } - }) - executor.memory = memory - return executor + async init(nodeData: INodeData, input: string, options: ICommonObject): Promise { + return prepareAgent(nodeData, { sessionId: this.sessionId, chatId: options.chatId, input }, options.chatHistory) } async run(nodeData: INodeData, input: string, options: ICommonObject): Promise { - const executor = nodeData.instance as AgentExecutor - const memory = nodeData.inputs?.memory - memory.returnMessages = true // Return true for BaseChatModel - - /* When incomingInput.history is provided, only force replace chatHistory if its ShortTermMemory - * LongTermMemory will automatically retrieved chatHistory from sessionId - */ - if (options && options.chatHistory && memory.isShortTermMemory) { - await memory.resumeMessages(options.chatHistory) - } - - executor.memory = memory + const memory = nodeData.inputs?.memory as FlowiseMemory + const executor = prepareAgent(nodeData, { sessionId: this.sessionId, chatId: options.chatId, input }, options.chatHistory) const loggerHandler = new ConsoleCallbackHandler(options.logger) const callbacks = await additionalCallbacks(nodeData, options) + let res: ChainValues = {} + if (options.socketIO && options.socketIOClientId) { const handler = new CustomChainHandler(options.socketIO, options.socketIOClientId) - const result = await executor.run(input, [loggerHandler, handler, ...callbacks]) - return result + res = await executor.invoke({ input }, { callbacks: [loggerHandler, handler, ...callbacks] }) } else { - const result = await executor.run(input, [loggerHandler, ...callbacks]) - return result + res = await executor.invoke({ input }, { callbacks: [loggerHandler, ...callbacks] }) } + + await memory.addChatMessages( + [ + { + text: input, + type: 'userMessage' + }, + { + text: res?.output, + type: 'apiMessage' + } + ], + this.sessionId + ) + + return res?.output } } +const prepareAgent = ( + nodeData: INodeData, + flowObj: { sessionId?: string; chatId?: string; input?: string }, + chatHistory: IMessage[] = [] +) => { + const model = nodeData.inputs?.model as ChatOpenAI + const memory = nodeData.inputs?.memory as FlowiseMemory + const systemMessage = nodeData.inputs?.systemMessage as string + let tools = nodeData.inputs?.tools + tools = flatten(tools) + const memoryKey = memory.memoryKey ? memory.memoryKey : 'chat_history' + const inputKey = memory.inputKey ? memory.inputKey : 'input' + + const prompt = ChatPromptTemplate.fromMessages([ + ['system', systemMessage ? systemMessage : `You are a helpful AI assistant.`], + new MessagesPlaceholder(memoryKey), + ['human', `{${inputKey}}`], + new MessagesPlaceholder('agent_scratchpad') + ]) + + const modelWithFunctions = model.bind({ + functions: [...tools.map((tool: any) => formatToOpenAIFunction(tool))] + }) + + const runnableAgent = RunnableSequence.from([ + { + [inputKey]: (i: { input: string; steps: AgentStep[] }) => i.input, + agent_scratchpad: (i: { input: string; steps: AgentStep[] }) => formatAgentSteps(i.steps), + [memoryKey]: async (_: { input: string; steps: AgentStep[] }) => { + const messages = (await memory.getChatMessages(flowObj?.sessionId, true, chatHistory)) as BaseMessage[] + return messages ?? [] + } + }, + prompt, + modelWithFunctions, + new OpenAIFunctionsAgentOutputParser() + ]) + + const executor = AgentExecutor.fromAgentAndTools({ + agent: runnableAgent, + tools, + sessionId: flowObj?.sessionId, + chatId: flowObj?.chatId, + input: flowObj?.input, + verbose: process.env.DEBUG === 'true' ? true : false + }) + + return executor +} + module.exports = { nodeClass: OpenAIFunctionAgent_Agents } diff --git a/packages/components/nodes/agents/OpenAIFunctionAgent/function.svg b/packages/components/nodes/agents/OpenAIFunctionAgent/function.svg new file mode 100644 index 00000000..9e283b91 --- /dev/null +++ b/packages/components/nodes/agents/OpenAIFunctionAgent/function.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/packages/components/nodes/agents/OpenAIFunctionAgent/openai.png b/packages/components/nodes/agents/OpenAIFunctionAgent/openai.png deleted file mode 100644 index de08a05b..00000000 Binary files a/packages/components/nodes/agents/OpenAIFunctionAgent/openai.png and /dev/null differ diff --git a/packages/components/nodes/analytic/LLMonitor/LLMonitor.ts b/packages/components/nodes/analytic/LLMonitor/LLMonitor.ts index a1ae317c..93bfbbf3 100644 --- a/packages/components/nodes/analytic/LLMonitor/LLMonitor.ts +++ b/packages/components/nodes/analytic/LLMonitor/LLMonitor.ts @@ -17,7 +17,7 @@ class LLMonitor_Analytic implements INode { this.name = 'llmonitor' this.version = 1.0 this.type = 'LLMonitor' - this.icon = 'llmonitor.png' + this.icon = 'Lunary.svg' this.category = 'Analytic' this.baseClasses = [this.type] this.inputs = [] diff --git a/packages/components/nodes/analytic/LLMonitor/Lunary.svg b/packages/components/nodes/analytic/LLMonitor/Lunary.svg new file mode 100644 index 00000000..1528de5a --- /dev/null +++ b/packages/components/nodes/analytic/LLMonitor/Lunary.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/components/nodes/analytic/LLMonitor/llmonitor.png b/packages/components/nodes/analytic/LLMonitor/llmonitor.png deleted file mode 100644 index d50a7044..00000000 Binary files a/packages/components/nodes/analytic/LLMonitor/llmonitor.png and /dev/null differ diff --git a/packages/components/nodes/analytic/LangFuse/LangFuse.ts b/packages/components/nodes/analytic/LangFuse/LangFuse.ts index dcfc3d2a..52e1ea52 100644 --- a/packages/components/nodes/analytic/LangFuse/LangFuse.ts +++ b/packages/components/nodes/analytic/LangFuse/LangFuse.ts @@ -17,7 +17,7 @@ class LangFuse_Analytic implements INode { this.name = 'langFuse' this.version = 1.0 this.type = 'LangFuse' - this.icon = 'langfuse.png' + this.icon = 'Langfuse.svg' this.category = 'Analytic' this.baseClasses = [this.type] this.inputs = [] diff --git a/packages/components/nodes/analytic/LangFuse/Langfuse.svg b/packages/components/nodes/analytic/LangFuse/Langfuse.svg new file mode 100644 index 00000000..7ebb4402 --- /dev/null +++ b/packages/components/nodes/analytic/LangFuse/Langfuse.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/components/nodes/analytic/LangFuse/langfuse.png b/packages/components/nodes/analytic/LangFuse/langfuse.png deleted file mode 100644 index df9181b8..00000000 Binary files a/packages/components/nodes/analytic/LangFuse/langfuse.png and /dev/null differ diff --git a/packages/components/nodes/cache/InMemoryCache/InMemoryCache.ts b/packages/components/nodes/cache/InMemoryCache/InMemoryCache.ts index 1ea03566..a8aa9fd4 100644 --- a/packages/components/nodes/cache/InMemoryCache/InMemoryCache.ts +++ b/packages/components/nodes/cache/InMemoryCache/InMemoryCache.ts @@ -20,7 +20,7 @@ class InMemoryCache implements INode { this.version = 1.0 this.type = 'InMemoryCache' this.description = 'Cache LLM response in memory, will be cleared once app restarted' - this.icon = 'inmemorycache.png' + this.icon = 'Memory.svg' this.category = 'Cache' this.baseClasses = [this.type, ...getBaseClasses(InMemoryCacheExtended)] this.inputs = [] diff --git a/packages/components/nodes/cache/InMemoryCache/InMemoryEmbeddingCache.ts b/packages/components/nodes/cache/InMemoryCache/InMemoryEmbeddingCache.ts index fad72482..f83fca3d 100644 --- a/packages/components/nodes/cache/InMemoryCache/InMemoryEmbeddingCache.ts +++ b/packages/components/nodes/cache/InMemoryCache/InMemoryEmbeddingCache.ts @@ -21,7 +21,7 @@ class InMemoryEmbeddingCache implements INode { this.version = 1.0 this.type = 'InMemoryEmbeddingCache' this.description = 'Cache generated Embeddings in memory to avoid needing to recompute them.' - this.icon = 'inmemorycache.png' + this.icon = 'Memory.svg' this.category = 'Cache' this.baseClasses = [this.type, ...getBaseClasses(CacheBackedEmbeddings)] this.inputs = [ diff --git a/packages/components/nodes/cache/InMemoryCache/Memory.svg b/packages/components/nodes/cache/InMemoryCache/Memory.svg new file mode 100644 index 00000000..cc97b9e6 --- /dev/null +++ b/packages/components/nodes/cache/InMemoryCache/Memory.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/packages/components/nodes/cache/InMemoryCache/inmemorycache.png b/packages/components/nodes/cache/InMemoryCache/inmemorycache.png deleted file mode 100644 index 1e5fe6d1..00000000 Binary files a/packages/components/nodes/cache/InMemoryCache/inmemorycache.png and /dev/null differ diff --git a/packages/components/nodes/cache/MomentoCache/Momento.svg b/packages/components/nodes/cache/MomentoCache/Momento.svg new file mode 100644 index 00000000..22e9474d --- /dev/null +++ b/packages/components/nodes/cache/MomentoCache/Momento.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/components/nodes/cache/MomentoCache/MomentoCache.ts b/packages/components/nodes/cache/MomentoCache/MomentoCache.ts index 2bd2625b..8b4ce416 100644 --- a/packages/components/nodes/cache/MomentoCache/MomentoCache.ts +++ b/packages/components/nodes/cache/MomentoCache/MomentoCache.ts @@ -20,7 +20,7 @@ class MomentoCache implements INode { this.version = 1.0 this.type = 'MomentoCache' this.description = 'Cache LLM response using Momento, a distributed, serverless cache' - this.icon = 'momento.png' + this.icon = 'Momento.svg' this.category = 'Cache' this.baseClasses = [this.type, ...getBaseClasses(LangchainMomentoCache)] this.credential = { diff --git a/packages/components/nodes/cache/MomentoCache/momento.png b/packages/components/nodes/cache/MomentoCache/momento.png deleted file mode 100644 index 0f2b54b6..00000000 Binary files a/packages/components/nodes/cache/MomentoCache/momento.png and /dev/null differ diff --git a/packages/components/nodes/cache/RedisCache/RedisCache.ts b/packages/components/nodes/cache/RedisCache/RedisCache.ts index 3b68cf12..4e61c239 100644 --- a/packages/components/nodes/cache/RedisCache/RedisCache.ts +++ b/packages/components/nodes/cache/RedisCache/RedisCache.ts @@ -56,12 +56,16 @@ class RedisCache implements INode { const password = getCredentialParam('redisCachePwd', credentialData, nodeData) const portStr = getCredentialParam('redisCachePort', credentialData, nodeData) const host = getCredentialParam('redisCacheHost', credentialData, nodeData) + const sslEnabled = getCredentialParam('redisCacheSslEnabled', credentialData, nodeData) + + const tlsOptions = sslEnabled === true ? { tls: { rejectUnauthorized: false } } : {} client = new Redis({ port: portStr ? parseInt(portStr) : 6379, host, username, - password + password, + ...tlsOptions }) } else { client = new Redis(redisUrl) @@ -89,7 +93,7 @@ class RedisCache implements INode { redisClient.update = async (prompt: string, llmKey: string, value: Generation[]) => { for (let i = 0; i < value.length; i += 1) { const key = getCacheKey(prompt, llmKey, String(i)) - if (ttl !== undefined) { + if (ttl) { await client.set(key, JSON.stringify(serializeGeneration(value[i])), 'EX', parseInt(ttl, 10)) } else { await client.set(key, JSON.stringify(serializeGeneration(value[i]))) diff --git a/packages/components/nodes/cache/RedisCache/RedisEmbeddingsCache.ts b/packages/components/nodes/cache/RedisCache/RedisEmbeddingsCache.ts index f15869d7..fe1b4df8 100644 --- a/packages/components/nodes/cache/RedisCache/RedisEmbeddingsCache.ts +++ b/packages/components/nodes/cache/RedisCache/RedisEmbeddingsCache.ts @@ -71,12 +71,16 @@ class RedisEmbeddingsCache implements INode { const password = getCredentialParam('redisCachePwd', credentialData, nodeData) const portStr = getCredentialParam('redisCachePort', credentialData, nodeData) const host = getCredentialParam('redisCacheHost', credentialData, nodeData) + const sslEnabled = getCredentialParam('redisCacheSslEnabled', credentialData, nodeData) + + const tlsOptions = sslEnabled === true ? { tls: { rejectUnauthorized: false } } : {} client = new Redis({ port: portStr ? parseInt(portStr) : 6379, host, username, - password + password, + ...tlsOptions }) } else { client = new Redis(redisUrl) diff --git a/packages/components/nodes/cache/RedisCache/redis.svg b/packages/components/nodes/cache/RedisCache/redis.svg index 90359069..b94c650f 100644 --- a/packages/components/nodes/cache/RedisCache/redis.svg +++ b/packages/components/nodes/cache/RedisCache/redis.svg @@ -1 +1,12 @@ - \ No newline at end of file + + + + + + + + + + + + diff --git a/packages/components/nodes/cache/UpstashRedisCache/Upstash.svg b/packages/components/nodes/cache/UpstashRedisCache/Upstash.svg new file mode 100644 index 00000000..582d151a --- /dev/null +++ b/packages/components/nodes/cache/UpstashRedisCache/Upstash.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/components/nodes/cache/UpstashRedisCache/UpstashRedisCache.ts b/packages/components/nodes/cache/UpstashRedisCache/UpstashRedisCache.ts index f4ed947b..311979c2 100644 --- a/packages/components/nodes/cache/UpstashRedisCache/UpstashRedisCache.ts +++ b/packages/components/nodes/cache/UpstashRedisCache/UpstashRedisCache.ts @@ -19,7 +19,7 @@ class UpstashRedisCache implements INode { this.version = 1.0 this.type = 'UpstashRedisCache' this.description = 'Cache LLM response in Upstash Redis, serverless data for Redis and Kafka' - this.icon = 'upstash.png' + this.icon = 'Upstash.svg' this.category = 'Cache' this.baseClasses = [this.type, ...getBaseClasses(LangchainUpstashRedisCache)] this.credential = { diff --git a/packages/components/nodes/cache/UpstashRedisCache/upstash.png b/packages/components/nodes/cache/UpstashRedisCache/upstash.png deleted file mode 100644 index e27e02f4..00000000 Binary files a/packages/components/nodes/cache/UpstashRedisCache/upstash.png and /dev/null differ diff --git a/packages/components/nodes/chains/ApiChain/GETApiChain.ts b/packages/components/nodes/chains/ApiChain/GETApiChain.ts index d88ab6f9..3494fa1c 100644 --- a/packages/components/nodes/chains/ApiChain/GETApiChain.ts +++ b/packages/components/nodes/chains/ApiChain/GETApiChain.ts @@ -32,7 +32,7 @@ class GETApiChain_Chains implements INode { this.name = 'getApiChain' this.version = 1.0 this.type = 'GETApiChain' - this.icon = 'apichain.svg' + this.icon = 'get.svg' this.category = 'Chains' this.description = 'Chain to run queries against GET API' this.baseClasses = [this.type, ...getBaseClasses(APIChain)] diff --git a/packages/components/nodes/chains/ApiChain/OpenAPIChain.ts b/packages/components/nodes/chains/ApiChain/OpenAPIChain.ts index 08dc3cc8..51459daa 100644 --- a/packages/components/nodes/chains/ApiChain/OpenAPIChain.ts +++ b/packages/components/nodes/chains/ApiChain/OpenAPIChain.ts @@ -20,7 +20,7 @@ class OpenApiChain_Chains implements INode { this.name = 'openApiChain' this.version = 1.0 this.type = 'OpenAPIChain' - this.icon = 'openapi.png' + this.icon = 'openapi.svg' this.category = 'Chains' this.description = 'Chain that automatically select and call APIs based only on an OpenAPI spec' this.baseClasses = [this.type, ...getBaseClasses(APIChain)] diff --git a/packages/components/nodes/chains/ApiChain/POSTApiChain.ts b/packages/components/nodes/chains/ApiChain/POSTApiChain.ts index f005f47a..309b3310 100644 --- a/packages/components/nodes/chains/ApiChain/POSTApiChain.ts +++ b/packages/components/nodes/chains/ApiChain/POSTApiChain.ts @@ -21,7 +21,7 @@ class POSTApiChain_Chains implements INode { this.name = 'postApiChain' this.version = 1.0 this.type = 'POSTApiChain' - this.icon = 'apichain.svg' + this.icon = 'post.svg' this.category = 'Chains' this.description = 'Chain to run queries against POST API' this.baseClasses = [this.type, ...getBaseClasses(APIChain)] diff --git a/packages/components/nodes/chains/ApiChain/apichain.svg b/packages/components/nodes/chains/ApiChain/apichain.svg deleted file mode 100644 index 3b86b905..00000000 --- a/packages/components/nodes/chains/ApiChain/apichain.svg +++ /dev/null @@ -1,3 +0,0 @@ - \ No newline at end of file diff --git a/packages/components/nodes/chains/ApiChain/get.svg b/packages/components/nodes/chains/ApiChain/get.svg new file mode 100644 index 00000000..0adfdaa8 --- /dev/null +++ b/packages/components/nodes/chains/ApiChain/get.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/components/nodes/chains/ApiChain/openapi.png b/packages/components/nodes/chains/ApiChain/openapi.png deleted file mode 100644 index 457c2e40..00000000 Binary files a/packages/components/nodes/chains/ApiChain/openapi.png and /dev/null differ diff --git a/packages/components/nodes/chains/ApiChain/openapi.svg b/packages/components/nodes/chains/ApiChain/openapi.svg new file mode 100644 index 00000000..0f623b94 --- /dev/null +++ b/packages/components/nodes/chains/ApiChain/openapi.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/components/nodes/chains/ApiChain/post.svg b/packages/components/nodes/chains/ApiChain/post.svg new file mode 100644 index 00000000..250ce69a --- /dev/null +++ b/packages/components/nodes/chains/ApiChain/post.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/packages/components/nodes/chains/ConversationChain/ConversationChain.ts b/packages/components/nodes/chains/ConversationChain/ConversationChain.ts index aa9b1a8a..764f4f0e 100644 --- a/packages/components/nodes/chains/ConversationChain/ConversationChain.ts +++ b/packages/components/nodes/chains/ConversationChain/ConversationChain.ts @@ -1,14 +1,15 @@ -import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' +import { FlowiseMemory, ICommonObject, IMessage, INode, INodeData, INodeParams } from '../../../src/Interface' import { ConversationChain } from 'langchain/chains' -import { getBaseClasses } from '../../../src/utils' +import { getBaseClasses, handleEscapeCharacters } from '../../../src/utils' import { ChatPromptTemplate, HumanMessagePromptTemplate, MessagesPlaceholder, SystemMessagePromptTemplate } from 'langchain/prompts' -import { BufferMemory } from 'langchain/memory' import { BaseChatModel } from 'langchain/chat_models/base' import { ConsoleCallbackHandler, CustomChainHandler, additionalCallbacks } from '../../../src/handler' -import { flatten } from 'lodash' -import { Document } from 'langchain/document' +import { RunnableSequence } from 'langchain/schema/runnable' +import { StringOutputParser } from 'langchain/schema/output_parser' +import { ConsoleCallbackHandler as LCConsoleCallbackHandler } from '@langchain/core/tracers/console' let systemMessage = `The following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.` +const inputKey = 'input' class ConversationChain_Chains implements INode { label: string @@ -20,19 +21,20 @@ class ConversationChain_Chains implements INode { baseClasses: string[] description: string inputs: INodeParams[] + sessionId?: string - constructor() { + constructor(fields?: { sessionId?: string }) { this.label = 'Conversation Chain' this.name = 'conversationChain' - this.version = 1.0 + this.version = 2.0 this.type = 'ConversationChain' - this.icon = 'chain.svg' + this.icon = 'conv.svg' this.category = 'Chains' this.description = 'Chat models specific conversational chain with memory' this.baseClasses = [this.type, ...getBaseClasses(ConversationChain)] this.inputs = [ { - label: 'Language Model', + label: 'Chat Model', name: 'model', type: 'BaseChatModel' }, @@ -41,6 +43,14 @@ class ConversationChain_Chains implements INode { name: 'memory', type: 'BaseMemory' }, + { + label: 'Chat Prompt Template', + name: 'chatPromptTemplate', + type: 'ChatPromptTemplate', + description: 'Override existing prompt with Chat Prompt Template. Human Message must includes {input} variable', + optional: true + }, + /* Deprecated { label: 'Document', name: 'document', @@ -49,86 +59,133 @@ class ConversationChain_Chains implements INode { 'Include whole document into the context window, if you get maximum context length error, please use model with higher context window like Claude 100k, or gpt4 32k', optional: true, list: true - }, + },*/ { label: 'System Message', name: 'systemMessagePrompt', type: 'string', rows: 4, + description: 'If Chat Prompt Template is provided, this will be ignored', additionalParams: true, optional: true, - placeholder: 'You are a helpful assistant that write codes' + default: systemMessage, + placeholder: systemMessage } ] + this.sessionId = fields?.sessionId } - async init(nodeData: INodeData): Promise { - const model = nodeData.inputs?.model as BaseChatModel - const memory = nodeData.inputs?.memory as BufferMemory - const prompt = nodeData.inputs?.systemMessagePrompt as string - const docs = nodeData.inputs?.document as Document[] - - const flattenDocs = docs && docs.length ? flatten(docs) : [] - const finalDocs = [] - for (let i = 0; i < flattenDocs.length; i += 1) { - if (flattenDocs[i] && flattenDocs[i].pageContent) { - finalDocs.push(new Document(flattenDocs[i])) - } - } - - let finalText = '' - for (let i = 0; i < finalDocs.length; i += 1) { - finalText += finalDocs[i].pageContent - } - - const replaceChar: string[] = ['{', '}'] - for (const char of replaceChar) finalText = finalText.replaceAll(char, '') - - if (finalText) systemMessage = `${systemMessage}\nThe AI has the following context:\n${finalText}` - - const obj: any = { - llm: model, - memory, - verbose: process.env.DEBUG === 'true' ? true : false - } - - const chatPrompt = ChatPromptTemplate.fromMessages([ - SystemMessagePromptTemplate.fromTemplate(prompt ? `${prompt}\n${systemMessage}` : systemMessage), - new MessagesPlaceholder(memory.memoryKey ?? 'chat_history'), - HumanMessagePromptTemplate.fromTemplate('{input}') - ]) - obj.prompt = chatPrompt - - const chain = new ConversationChain(obj) + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + const chain = prepareChain(nodeData, this.sessionId, options.chatHistory) return chain } async run(nodeData: INodeData, input: string, options: ICommonObject): Promise { - const chain = nodeData.instance as ConversationChain const memory = nodeData.inputs?.memory - memory.returnMessages = true // Return true for BaseChatModel - - /* When incomingInput.history is provided, only force replace chatHistory if its ShortTermMemory - * LongTermMemory will automatically retrieved chatHistory from sessionId - */ - if (options && options.chatHistory && memory.isShortTermMemory) { - await memory.resumeMessages(options.chatHistory) - } - - chain.memory = memory + const chain = prepareChain(nodeData, this.sessionId, options.chatHistory) const loggerHandler = new ConsoleCallbackHandler(options.logger) - const callbacks = await additionalCallbacks(nodeData, options) + const additionalCallback = await additionalCallbacks(nodeData, options) + + let res = '' + let callbacks = [loggerHandler, ...additionalCallback] + + if (process.env.DEBUG === 'true') { + callbacks.push(new LCConsoleCallbackHandler()) + } if (options.socketIO && options.socketIOClientId) { const handler = new CustomChainHandler(options.socketIO, options.socketIOClientId) - const res = await chain.call({ input }, [loggerHandler, handler, ...callbacks]) - return res?.response + callbacks.push(handler) + res = await chain.invoke({ input }, { callbacks }) } else { - const res = await chain.call({ input }, [loggerHandler, ...callbacks]) - return res?.response + res = await chain.invoke({ input }, { callbacks }) } + + await memory.addChatMessages( + [ + { + text: input, + type: 'userMessage' + }, + { + text: res, + type: 'apiMessage' + } + ], + this.sessionId + ) + + return res } } +const prepareChatPrompt = (nodeData: INodeData) => { + const memory = nodeData.inputs?.memory as FlowiseMemory + const prompt = nodeData.inputs?.systemMessagePrompt as string + const chatPromptTemplate = nodeData.inputs?.chatPromptTemplate as ChatPromptTemplate + + if (chatPromptTemplate && chatPromptTemplate.promptMessages.length) { + const sysPrompt = chatPromptTemplate.promptMessages[0] + const humanPrompt = chatPromptTemplate.promptMessages[chatPromptTemplate.promptMessages.length - 1] + const chatPrompt = ChatPromptTemplate.fromMessages([ + sysPrompt, + new MessagesPlaceholder(memory.memoryKey ?? 'chat_history'), + humanPrompt + ]) + + if ((chatPromptTemplate as any).promptValues) { + // @ts-ignore + chatPrompt.promptValues = (chatPromptTemplate as any).promptValues + } + + return chatPrompt + } + + const chatPrompt = ChatPromptTemplate.fromMessages([ + SystemMessagePromptTemplate.fromTemplate(prompt ? prompt : systemMessage), + new MessagesPlaceholder(memory.memoryKey ?? 'chat_history'), + HumanMessagePromptTemplate.fromTemplate(`{${inputKey}}`) + ]) + + return chatPrompt +} + +const prepareChain = (nodeData: INodeData, sessionId?: string, chatHistory: IMessage[] = []) => { + const model = nodeData.inputs?.model as BaseChatModel + const memory = nodeData.inputs?.memory as FlowiseMemory + const memoryKey = memory.memoryKey ?? 'chat_history' + + const chatPrompt = prepareChatPrompt(nodeData) + let promptVariables = {} + const promptValuesRaw = (chatPrompt as any).promptValues + if (promptValuesRaw) { + const promptValues = handleEscapeCharacters(promptValuesRaw, true) + for (const val in promptValues) { + promptVariables = { + ...promptVariables, + [val]: () => { + return promptValues[val] + } + } + } + } + + const conversationChain = RunnableSequence.from([ + { + [inputKey]: (input: { input: string }) => input.input, + [memoryKey]: async () => { + const history = await memory.getChatMessages(sessionId, true, chatHistory) + return history + }, + ...promptVariables + }, + chatPrompt, + model, + new StringOutputParser() + ]) + + return conversationChain +} + module.exports = { nodeClass: ConversationChain_Chains } diff --git a/packages/components/nodes/chains/ConversationChain/chain.svg b/packages/components/nodes/chains/ConversationChain/chain.svg deleted file mode 100644 index a5b32f90..00000000 --- a/packages/components/nodes/chains/ConversationChain/chain.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/packages/components/nodes/chains/ConversationChain/conv.svg b/packages/components/nodes/chains/ConversationChain/conv.svg new file mode 100644 index 00000000..5f6c5a47 --- /dev/null +++ b/packages/components/nodes/chains/ConversationChain/conv.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/packages/components/nodes/chains/ConversationalRetrievalQAChain/ConversationalRetrievalQAChain.ts b/packages/components/nodes/chains/ConversationalRetrievalQAChain/ConversationalRetrievalQAChain.ts index d8fb4225..964543de 100644 --- a/packages/components/nodes/chains/ConversationalRetrievalQAChain/ConversationalRetrievalQAChain.ts +++ b/packages/components/nodes/chains/ConversationalRetrievalQAChain/ConversationalRetrievalQAChain.ts @@ -1,20 +1,26 @@ import { BaseLanguageModel } from 'langchain/base_language' -import { ICommonObject, IMessage, INode, INodeData, INodeParams } from '../../../src/Interface' -import { getBaseClasses } from '../../../src/utils' -import { ConversationalRetrievalQAChain, QAChainParams } from 'langchain/chains' +import { ConversationalRetrievalQAChain } from 'langchain/chains' import { BaseRetriever } from 'langchain/schema/retriever' -import { BufferMemoryInput, BufferMemory } from 'langchain/memory' +import { BufferMemoryInput } from 'langchain/memory' import { PromptTemplate } from 'langchain/prompts' -import { ConsoleCallbackHandler, CustomChainHandler, additionalCallbacks } from '../../../src/handler' -import { - default_map_reduce_template, - default_qa_template, - qa_template, - map_reduce_template, - CUSTOM_QUESTION_GENERATOR_CHAIN_PROMPT, - refine_question_template, - refine_template -} from './prompts' +import { QA_TEMPLATE, REPHRASE_TEMPLATE, RESPONSE_TEMPLATE } from './prompts' +import { Runnable, RunnableSequence, RunnableMap, RunnableBranch, RunnableLambda } from 'langchain/schema/runnable' +import { BaseMessage, HumanMessage, AIMessage } from 'langchain/schema' +import { StringOutputParser } from 'langchain/schema/output_parser' +import type { Document } from 'langchain/document' +import { ChatPromptTemplate, MessagesPlaceholder } from 'langchain/prompts' +import { applyPatch } from 'fast-json-patch' +import { convertBaseMessagetoIMessage, getBaseClasses } from '../../../src/utils' +import { ConsoleCallbackHandler, additionalCallbacks } from '../../../src/handler' +import { FlowiseMemory, ICommonObject, IMessage, INode, INodeData, INodeParams, MemoryMethods } from '../../../src/Interface' +import { ConsoleCallbackHandler as LCConsoleCallbackHandler } from '@langchain/core/tracers/console' + +type RetrievalChainInput = { + chat_history: string + question: string +} + +const sourceRunnableName = 'FindDocs' class ConversationalRetrievalQAChain_Chains implements INode { label: string @@ -26,21 +32,22 @@ class ConversationalRetrievalQAChain_Chains implements INode { baseClasses: string[] description: string inputs: INodeParams[] + sessionId?: string - constructor() { + constructor(fields?: { sessionId?: string }) { this.label = 'Conversational Retrieval QA Chain' this.name = 'conversationalRetrievalQAChain' - this.version = 1.0 + this.version = 2.0 this.type = 'ConversationalRetrievalQAChain' - this.icon = 'chain.svg' + this.icon = 'qa.svg' this.category = 'Chains' this.description = 'Document QA - built on RetrievalQAChain to provide a chat history component' this.baseClasses = [this.type, ...getBaseClasses(ConversationalRetrievalQAChain)] this.inputs = [ { - label: 'Language Model', + label: 'Chat Model', name: 'model', - type: 'BaseLanguageModel' + type: 'BaseChatModel' }, { label: 'Vector Store Retriever', @@ -60,6 +67,29 @@ class ConversationalRetrievalQAChain_Chains implements INode { type: 'boolean', optional: true }, + { + label: 'Rephrase Prompt', + name: 'rephrasePrompt', + type: 'string', + description: 'Using previous chat history, rephrase question into a standalone question', + warning: 'Prompt must include input variables: {chat_history} and {question}', + rows: 4, + additionalParams: true, + optional: true, + default: REPHRASE_TEMPLATE + }, + { + label: 'Response Prompt', + name: 'responsePrompt', + type: 'string', + description: 'Taking the rephrased question, search for answer from the provided context', + warning: 'Prompt must include input variable: {context}', + rows: 4, + additionalParams: true, + optional: true, + default: RESPONSE_TEMPLATE + } + /** Deprecated { label: 'System Message', name: 'systemMessagePrompt', @@ -70,6 +100,7 @@ class ConversationalRetrievalQAChain_Chains implements INode { placeholder: 'I want you to act as a document that I am having a conversation with. Your name is "AI Assistant". You will provide me with answers from the given info. If the answer is not included, say exactly "Hmm, I am not sure." and stop after that. Refuse to answer any question not about the info. Never break character.' }, + // TODO: create standalone chains for these 3 modes as they are not compatible with memory { label: 'Chain Option', name: 'chainOption', @@ -95,147 +126,253 @@ class ConversationalRetrievalQAChain_Chains implements INode { additionalParams: true, optional: true } + */ ] + this.sessionId = fields?.sessionId } async init(nodeData: INodeData): Promise { const model = nodeData.inputs?.model as BaseLanguageModel const vectorStoreRetriever = nodeData.inputs?.vectorStoreRetriever as BaseRetriever const systemMessagePrompt = nodeData.inputs?.systemMessagePrompt as string - const returnSourceDocuments = nodeData.inputs?.returnSourceDocuments as boolean - const chainOption = nodeData.inputs?.chainOption as string - const externalMemory = nodeData.inputs?.memory + const rephrasePrompt = nodeData.inputs?.rephrasePrompt as string + const responsePrompt = nodeData.inputs?.responsePrompt as string - const obj: any = { - verbose: process.env.DEBUG === 'true' ? true : false, - questionGeneratorChainOptions: { - template: CUSTOM_QUESTION_GENERATOR_CHAIN_PROMPT - } + let customResponsePrompt = responsePrompt + // If the deprecated systemMessagePrompt is still exists + if (systemMessagePrompt) { + customResponsePrompt = `${systemMessagePrompt}\n${QA_TEMPLATE}` } - if (returnSourceDocuments) obj.returnSourceDocuments = returnSourceDocuments - - if (chainOption === 'map_reduce') { - obj.qaChainOptions = { - type: 'map_reduce', - combinePrompt: PromptTemplate.fromTemplate( - systemMessagePrompt ? `${systemMessagePrompt}\n${map_reduce_template}` : default_map_reduce_template - ) - } as QAChainParams - } else if (chainOption === 'refine') { - const qprompt = new PromptTemplate({ - inputVariables: ['context', 'question'], - template: refine_question_template(systemMessagePrompt) - }) - const rprompt = new PromptTemplate({ - inputVariables: ['context', 'question', 'existing_answer'], - template: refine_template - }) - obj.qaChainOptions = { - type: 'refine', - questionPrompt: qprompt, - refinePrompt: rprompt - } as QAChainParams - } else { - obj.qaChainOptions = { - type: 'stuff', - prompt: PromptTemplate.fromTemplate(systemMessagePrompt ? `${systemMessagePrompt}\n${qa_template}` : default_qa_template) - } as QAChainParams - } - - if (externalMemory) { - externalMemory.memoryKey = 'chat_history' - externalMemory.inputKey = 'question' - externalMemory.outputKey = 'text' - externalMemory.returnMessages = true - if (chainOption === 'refine') externalMemory.outputKey = 'output_text' - obj.memory = externalMemory - } else { - const fields: BufferMemoryInput = { - memoryKey: 'chat_history', - inputKey: 'question', - outputKey: 'text', - returnMessages: true - } - if (chainOption === 'refine') fields.outputKey = 'output_text' - obj.memory = new BufferMemoryExtended(fields) - } - - const chain = ConversationalRetrievalQAChain.fromLLM(model, vectorStoreRetriever, obj) - return chain + const answerChain = createChain(model, vectorStoreRetriever, rephrasePrompt, customResponsePrompt) + return answerChain } async run(nodeData: INodeData, input: string, options: ICommonObject): Promise { - const chain = nodeData.instance as ConversationalRetrievalQAChain + const model = nodeData.inputs?.model as BaseLanguageModel + const externalMemory = nodeData.inputs?.memory + const vectorStoreRetriever = nodeData.inputs?.vectorStoreRetriever as BaseRetriever + const systemMessagePrompt = nodeData.inputs?.systemMessagePrompt as string + const rephrasePrompt = nodeData.inputs?.rephrasePrompt as string + const responsePrompt = nodeData.inputs?.responsePrompt as string const returnSourceDocuments = nodeData.inputs?.returnSourceDocuments as boolean - const chainOption = nodeData.inputs?.chainOption as string - let model = nodeData.inputs?.model - - // Temporary fix: https://github.com/hwchase17/langchainjs/issues/754 - model.streaming = false - chain.questionGeneratorChain.llm = model - - const obj = { question: input } - - /* When incomingInput.history is provided, only force replace chatHistory if its ShortTermMemory - * LongTermMemory will automatically retrieved chatHistory from sessionId - */ - if (options && options.chatHistory && chain.memory && (chain.memory as any).isShortTermMemory) { - await (chain.memory as any).resumeMessages(options.chatHistory) + let customResponsePrompt = responsePrompt + // If the deprecated systemMessagePrompt is still exists + if (systemMessagePrompt) { + customResponsePrompt = `${systemMessagePrompt}\n${QA_TEMPLATE}` } + let memory: FlowiseMemory | undefined = externalMemory + if (!memory) { + memory = new BufferMemory({ + returnMessages: true, + memoryKey: 'chat_history', + inputKey: 'input' + }) + } + + const answerChain = createChain(model, vectorStoreRetriever, rephrasePrompt, customResponsePrompt) + + const history = ((await memory.getChatMessages(this.sessionId, false, options.chatHistory)) as IMessage[]) ?? [] + const loggerHandler = new ConsoleCallbackHandler(options.logger) - const callbacks = await additionalCallbacks(nodeData, options) + const additionalCallback = await additionalCallbacks(nodeData, options) - if (options.socketIO && options.socketIOClientId) { - const handler = new CustomChainHandler( - options.socketIO, - options.socketIOClientId, - chainOption === 'refine' ? 4 : undefined, - returnSourceDocuments - ) - const res = await chain.call(obj, [loggerHandler, handler, ...callbacks]) - if (chainOption === 'refine') { - if (res.output_text && res.sourceDocuments) { - return { - text: res.output_text, - sourceDocuments: res.sourceDocuments - } - } - return res?.output_text - } - if (res.text && res.sourceDocuments) return res - return res?.text - } else { - const res = await chain.call(obj, [loggerHandler, ...callbacks]) - if (res.text && res.sourceDocuments) return res - return res?.text + let callbacks = [loggerHandler, ...additionalCallback] + + if (process.env.DEBUG === 'true') { + callbacks.push(new LCConsoleCallbackHandler()) } + + const stream = answerChain.streamLog( + { question: input, chat_history: history }, + { callbacks }, + { + includeNames: [sourceRunnableName] + } + ) + + let streamedResponse: Record = {} + let sourceDocuments: ICommonObject[] = [] + let text = '' + let isStreamingStarted = false + const isStreamingEnabled = options.socketIO && options.socketIOClientId + + for await (const chunk of stream) { + streamedResponse = applyPatch(streamedResponse, chunk.ops).newDocument + + if (streamedResponse.final_output) { + text = streamedResponse.final_output?.output + if (isStreamingEnabled) options.socketIO.to(options.socketIOClientId).emit('end') + if (Array.isArray(streamedResponse?.logs?.[sourceRunnableName]?.final_output?.output)) { + sourceDocuments = streamedResponse?.logs?.[sourceRunnableName]?.final_output?.output + if (isStreamingEnabled && returnSourceDocuments) + options.socketIO.to(options.socketIOClientId).emit('sourceDocuments', sourceDocuments) + } + } + + if ( + Array.isArray(streamedResponse?.streamed_output) && + streamedResponse?.streamed_output.length && + !streamedResponse.final_output + ) { + const token = streamedResponse.streamed_output[streamedResponse.streamed_output.length - 1] + + if (!isStreamingStarted) { + isStreamingStarted = true + if (isStreamingEnabled) options.socketIO.to(options.socketIOClientId).emit('start', token) + } + if (isStreamingEnabled) options.socketIO.to(options.socketIOClientId).emit('token', token) + } + } + + await memory.addChatMessages( + [ + { + text: input, + type: 'userMessage' + }, + { + text: text, + type: 'apiMessage' + } + ], + this.sessionId + ) + + if (returnSourceDocuments) return { text, sourceDocuments } + else return { text } } } -class BufferMemoryExtended extends BufferMemory { - isShortTermMemory = true +const createRetrieverChain = (llm: BaseLanguageModel, retriever: Runnable, rephrasePrompt: string) => { + // Small speed/accuracy optimization: no need to rephrase the first question + // since there shouldn't be any meta-references to prior chat history + const CONDENSE_QUESTION_PROMPT = PromptTemplate.fromTemplate(rephrasePrompt) + const condenseQuestionChain = RunnableSequence.from([CONDENSE_QUESTION_PROMPT, llm, new StringOutputParser()]).withConfig({ + runName: 'CondenseQuestion' + }) + const hasHistoryCheckFn = RunnableLambda.from((input: RetrievalChainInput) => input.chat_history.length > 0).withConfig({ + runName: 'HasChatHistoryCheck' + }) + + const conversationChain = condenseQuestionChain.pipe(retriever).withConfig({ + runName: 'RetrievalChainWithHistory' + }) + + const basicRetrievalChain = RunnableLambda.from((input: RetrievalChainInput) => input.question) + .withConfig({ + runName: 'Itemgetter:question' + }) + .pipe(retriever) + .withConfig({ runName: 'RetrievalChainWithNoHistory' }) + + return RunnableBranch.from([[hasHistoryCheckFn, conversationChain], basicRetrievalChain]).withConfig({ runName: sourceRunnableName }) +} + +const formatDocs = (docs: Document[]) => { + return docs.map((doc, i) => `${doc.pageContent}`).join('\n') +} + +const formatChatHistoryAsString = (history: BaseMessage[]) => { + return history.map((message) => `${message._getType()}: ${message.content}`).join('\n') +} + +const serializeHistory = (input: any) => { + const chatHistory: IMessage[] = input.chat_history || [] + const convertedChatHistory = [] + for (const message of chatHistory) { + if (message.type === 'userMessage') { + convertedChatHistory.push(new HumanMessage({ content: message.message })) + } + if (message.type === 'apiMessage') { + convertedChatHistory.push(new AIMessage({ content: message.message })) + } + } + return convertedChatHistory +} + +const createChain = ( + llm: BaseLanguageModel, + retriever: Runnable, + rephrasePrompt = REPHRASE_TEMPLATE, + responsePrompt = RESPONSE_TEMPLATE +) => { + const retrieverChain = createRetrieverChain(llm, retriever, rephrasePrompt) + + const context = RunnableMap.from({ + context: RunnableSequence.from([ + ({ question, chat_history }) => ({ + question, + chat_history: formatChatHistoryAsString(chat_history) + }), + retrieverChain, + RunnableLambda.from(formatDocs).withConfig({ + runName: 'FormatDocumentChunks' + }) + ]), + question: RunnableLambda.from((input: RetrievalChainInput) => input.question).withConfig({ + runName: 'Itemgetter:question' + }), + chat_history: RunnableLambda.from((input: RetrievalChainInput) => input.chat_history).withConfig({ + runName: 'Itemgetter:chat_history' + }) + }).withConfig({ tags: ['RetrieveDocs'] }) + + const prompt = ChatPromptTemplate.fromMessages([ + ['system', responsePrompt], + new MessagesPlaceholder('chat_history'), + ['human', `{question}`] + ]) + + const responseSynthesizerChain = RunnableSequence.from([prompt, llm, new StringOutputParser()]).withConfig({ + tags: ['GenerateResponse'] + }) + + const conversationalQAChain = RunnableSequence.from([ + { + question: RunnableLambda.from((input: RetrievalChainInput) => input.question).withConfig({ + runName: 'Itemgetter:question' + }), + chat_history: RunnableLambda.from(serializeHistory).withConfig({ + runName: 'SerializeHistory' + }) + }, + context, + responseSynthesizerChain + ]) + + return conversationalQAChain +} + +class BufferMemory extends FlowiseMemory implements MemoryMethods { constructor(fields: BufferMemoryInput) { super(fields) } + async getChatMessages(_?: string, returnBaseMessages = false, prevHistory: IMessage[] = []): Promise { + await this.chatHistory.clear() + + for (const msg of prevHistory) { + if (msg.type === 'userMessage') await this.chatHistory.addUserMessage(msg.message) + else if (msg.type === 'apiMessage') await this.chatHistory.addAIChatMessage(msg.message) + } + + const memoryResult = await this.loadMemoryVariables({}) + const baseMessages = memoryResult[this.memoryKey ?? 'chat_history'] + return returnBaseMessages ? baseMessages : convertBaseMessagetoIMessage(baseMessages) + } + + async addChatMessages(): Promise { + // adding chat messages will be done on the fly in getChatMessages() + return + } + async clearChatMessages(): Promise { await this.clear() } - - async resumeMessages(messages: IMessage[]): Promise { - // Clear existing chatHistory to avoid duplication - if (messages.length) await this.clear() - - // Insert into chatHistory - for (const msg of messages) { - if (msg.type === 'userMessage') await this.chatHistory.addUserMessage(msg.message) - else if (msg.type === 'apiMessage') await this.chatHistory.addAIChatMessage(msg.message) - } - } } module.exports = { nodeClass: ConversationalRetrievalQAChain_Chains } diff --git a/packages/components/nodes/chains/ConversationalRetrievalQAChain/chain.svg b/packages/components/nodes/chains/ConversationalRetrievalQAChain/chain.svg deleted file mode 100644 index a5b32f90..00000000 --- a/packages/components/nodes/chains/ConversationalRetrievalQAChain/chain.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/packages/components/nodes/chains/ConversationalRetrievalQAChain/prompts.ts b/packages/components/nodes/chains/ConversationalRetrievalQAChain/prompts.ts index 132e3a97..dccc7358 100644 --- a/packages/components/nodes/chains/ConversationalRetrievalQAChain/prompts.ts +++ b/packages/components/nodes/chains/ConversationalRetrievalQAChain/prompts.ts @@ -1,64 +1,27 @@ -export const default_qa_template = `Use the following pieces of context to answer the question at the end. If you don't know the answer, just say that you don't know, don't try to make up an answer. - -{context} - -Question: {question} -Helpful Answer:` - -export const qa_template = `Use the following pieces of context to answer the question at the end. - -{context} - -Question: {question} -Helpful Answer:` - -export const default_map_reduce_template = `Given the following extracted parts of a long document and a question, create a final answer. -If you don't know the answer, just say that you don't know. Don't try to make up an answer. - -{summaries} - -Question: {question} -Helpful Answer:` - -export const map_reduce_template = `Given the following extracted parts of a long document and a question, create a final answer. - -{summaries} - -Question: {question} -Helpful Answer:` - -export const refine_question_template = (sysPrompt?: string) => { - let returnPrompt = '' - if (sysPrompt) - returnPrompt = `Context information is below. ---------------------- -{context} ---------------------- -Given the context information and not prior knowledge, ${sysPrompt} -Answer the question: {question}. -Answer:` - if (!sysPrompt) - returnPrompt = `Context information is below. ---------------------- -{context} ---------------------- -Given the context information and not prior knowledge, answer the question: {question}. -Answer:` - return returnPrompt -} - -export const refine_template = `The original question is as follows: {question} -We have provided an existing answer: {existing_answer} -We have the opportunity to refine the existing answer (only if needed) with some more context below. ------------- -{context} ------------- -Given the new context, refine the original answer to better answer the question. -If you can't find answer from the context, return the original answer.` - export const CUSTOM_QUESTION_GENERATOR_CHAIN_PROMPT = `Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question, answer in the same language as the follow up question. include it in the standalone question. Chat History: {chat_history} Follow Up Input: {question} Standalone question:` + +export const RESPONSE_TEMPLATE = `I want you to act as a document that I am having a conversation with. Your name is "AI Assistant". Using the provided context, answer the user's question to the best of your ability using the resources provided. +If there is nothing in the context relevant to the question at hand, just say "Hmm, I'm not sure" and stop after that. Refuse to answer any question not about the info. Never break character. +------------ +{context} +------------ +REMEMBER: If there is no relevant information within the context, just say "Hmm, I'm not sure". Don't try to make up an answer. Never break character.` + +export const QA_TEMPLATE = `Use the following pieces of context to answer the question at the end. + +{context} + +Question: {question} +Helpful Answer:` + +export const REPHRASE_TEMPLATE = `Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question. + +Chat History: +{chat_history} +Follow Up Input: {question} +Standalone Question:` diff --git a/packages/components/nodes/chains/ConversationalRetrievalQAChain/qa.svg b/packages/components/nodes/chains/ConversationalRetrievalQAChain/qa.svg new file mode 100644 index 00000000..b367f7bc --- /dev/null +++ b/packages/components/nodes/chains/ConversationalRetrievalQAChain/qa.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/components/nodes/chains/LLMChain/LLMChain.ts b/packages/components/nodes/chains/LLMChain/LLMChain.ts index fd398151..f83fc36a 100644 --- a/packages/components/nodes/chains/LLMChain/LLMChain.ts +++ b/packages/components/nodes/chains/LLMChain/LLMChain.ts @@ -27,7 +27,7 @@ class LLMChain_Chains implements INode { this.name = 'llmChain' this.version = 3.0 this.type = 'LLMChain' - this.icon = 'chain.svg' + this.icon = 'LLM_Chain.svg' this.category = 'Chains' this.description = 'Chain to run queries against LLMs' this.baseClasses = [this.type, ...getBaseClasses(LLMChain)] @@ -82,7 +82,7 @@ class LLMChain_Chains implements INode { const model = nodeData.inputs?.model as BaseLanguageModel const prompt = nodeData.inputs?.prompt const output = nodeData.outputs?.output as string - const promptValues = prompt.promptValues as ICommonObject + let promptValues: ICommonObject | undefined = nodeData.inputs?.prompt.promptValues as ICommonObject const llmOutputParser = nodeData.inputs?.outputParser as BaseOutputParser this.outputParser = llmOutputParser if (llmOutputParser) { @@ -107,17 +107,24 @@ class LLMChain_Chains implements INode { verbose: process.env.DEBUG === 'true' }) const inputVariables = chain.prompt.inputVariables as string[] // ["product"] + promptValues = injectOutputParser(this.outputParser, chain, promptValues) const res = await runPrediction(inputVariables, chain, input, promptValues, options, nodeData) // eslint-disable-next-line no-console console.log('\x1b[92m\x1b[1m\n*****OUTPUT PREDICTION*****\n\x1b[0m\x1b[0m') // eslint-disable-next-line no-console console.log(res) + + let finalRes = res + if (this.outputParser && typeof res === 'object' && Object.prototype.hasOwnProperty.call(res, 'json')) { + finalRes = (res as ICommonObject).json + } + /** * Apply string transformation to convert special chars: * FROM: hello i am ben\n\n\thow are you? * TO: hello i am benFLOWISE_NEWLINEFLOWISE_NEWLINEFLOWISE_TABhow are you? */ - return handleEscapeCharacters(res, false) + return handleEscapeCharacters(finalRes, false) } } diff --git a/packages/components/nodes/chains/LLMChain/LLM_Chain.svg b/packages/components/nodes/chains/LLMChain/LLM_Chain.svg new file mode 100644 index 00000000..5d4b56f5 --- /dev/null +++ b/packages/components/nodes/chains/LLMChain/LLM_Chain.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/components/nodes/chains/MultiPromptChain/MultiPromptChain.ts b/packages/components/nodes/chains/MultiPromptChain/MultiPromptChain.ts index d171e514..72c25566 100644 --- a/packages/components/nodes/chains/MultiPromptChain/MultiPromptChain.ts +++ b/packages/components/nodes/chains/MultiPromptChain/MultiPromptChain.ts @@ -20,7 +20,7 @@ class MultiPromptChain_Chains implements INode { this.name = 'multiPromptChain' this.version = 1.0 this.type = 'MultiPromptChain' - this.icon = 'chain.svg' + this.icon = 'prompt.svg' this.category = 'Chains' this.description = 'Chain automatically picks an appropriate prompt from multiple prompt templates' this.baseClasses = [this.type, ...getBaseClasses(MultiPromptChain)] diff --git a/packages/components/nodes/chains/MultiPromptChain/chain.svg b/packages/components/nodes/chains/MultiPromptChain/chain.svg deleted file mode 100644 index a5b32f90..00000000 --- a/packages/components/nodes/chains/MultiPromptChain/chain.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/packages/components/nodes/chains/MultiPromptChain/prompt.svg b/packages/components/nodes/chains/MultiPromptChain/prompt.svg new file mode 100644 index 00000000..cf51913f --- /dev/null +++ b/packages/components/nodes/chains/MultiPromptChain/prompt.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/packages/components/nodes/chains/MultiRetrievalQAChain/MultiRetrievalQAChain.ts b/packages/components/nodes/chains/MultiRetrievalQAChain/MultiRetrievalQAChain.ts index 93bf2255..a2cfc309 100644 --- a/packages/components/nodes/chains/MultiRetrievalQAChain/MultiRetrievalQAChain.ts +++ b/packages/components/nodes/chains/MultiRetrievalQAChain/MultiRetrievalQAChain.ts @@ -20,7 +20,7 @@ class MultiRetrievalQAChain_Chains implements INode { this.name = 'multiRetrievalQAChain' this.version = 1.0 this.type = 'MultiRetrievalQAChain' - this.icon = 'chain.svg' + this.icon = 'qa.svg' this.category = 'Chains' this.description = 'QA Chain that automatically picks an appropriate vector store from multiple retrievers' this.baseClasses = [this.type, ...getBaseClasses(MultiRetrievalQAChain)] diff --git a/packages/components/nodes/chains/MultiRetrievalQAChain/chain.svg b/packages/components/nodes/chains/MultiRetrievalQAChain/chain.svg deleted file mode 100644 index a5b32f90..00000000 --- a/packages/components/nodes/chains/MultiRetrievalQAChain/chain.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/packages/components/nodes/chains/MultiRetrievalQAChain/qa.svg b/packages/components/nodes/chains/MultiRetrievalQAChain/qa.svg new file mode 100644 index 00000000..b367f7bc --- /dev/null +++ b/packages/components/nodes/chains/MultiRetrievalQAChain/qa.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/components/nodes/chains/RetrievalQAChain/RetrievalQAChain.ts b/packages/components/nodes/chains/RetrievalQAChain/RetrievalQAChain.ts index bff2a0a7..7f13fcfd 100644 --- a/packages/components/nodes/chains/RetrievalQAChain/RetrievalQAChain.ts +++ b/packages/components/nodes/chains/RetrievalQAChain/RetrievalQAChain.ts @@ -21,7 +21,7 @@ class RetrievalQAChain_Chains implements INode { this.name = 'retrievalQAChain' this.version = 1.0 this.type = 'RetrievalQAChain' - this.icon = 'chain.svg' + this.icon = 'qa.svg' this.category = 'Chains' this.description = 'QA chain to answer a question based on the retrieved documents' this.baseClasses = [this.type, ...getBaseClasses(RetrievalQAChain)] diff --git a/packages/components/nodes/chains/RetrievalQAChain/chain.svg b/packages/components/nodes/chains/RetrievalQAChain/chain.svg deleted file mode 100644 index a5b32f90..00000000 --- a/packages/components/nodes/chains/RetrievalQAChain/chain.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/packages/components/nodes/chains/RetrievalQAChain/qa.svg b/packages/components/nodes/chains/RetrievalQAChain/qa.svg new file mode 100644 index 00000000..b367f7bc --- /dev/null +++ b/packages/components/nodes/chains/RetrievalQAChain/qa.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/components/nodes/chains/SqlDatabaseChain/sqlchain.svg b/packages/components/nodes/chains/SqlDatabaseChain/sqlchain.svg index dcf937b3..68758320 100644 --- a/packages/components/nodes/chains/SqlDatabaseChain/sqlchain.svg +++ b/packages/components/nodes/chains/SqlDatabaseChain/sqlchain.svg @@ -1,7 +1,7 @@ - - - - - - - \ No newline at end of file + + + + + + + diff --git a/packages/components/nodes/chains/VectaraChain/VectaraChain.ts b/packages/components/nodes/chains/VectaraChain/VectaraChain.ts index 3799d062..7d65c9cd 100644 --- a/packages/components/nodes/chains/VectaraChain/VectaraChain.ts +++ b/packages/components/nodes/chains/VectaraChain/VectaraChain.ts @@ -69,22 +69,23 @@ class VectaraChain_Chains implements INode { options: [ { label: 'vectara-summary-ext-v1.2.0 (gpt-3.5-turbo)', - name: 'vectara-summary-ext-v1.2.0' + name: 'vectara-summary-ext-v1.2.0', + description: 'base summarizer, available to all Vectara users' }, { label: 'vectara-experimental-summary-ext-2023-10-23-small (gpt-3.5-turbo)', name: 'vectara-experimental-summary-ext-2023-10-23-small', - description: 'In beta, available to both Growth and Scale Vectara users' + description: `In beta, available to both Growth and Scale Vectara users` }, { label: 'vectara-summary-ext-v1.3.0 (gpt-4.0)', name: 'vectara-summary-ext-v1.3.0', - description: 'Only available to paying Scale Vectara users' + description: 'Only available to Scale Vectara users' }, { label: 'vectara-experimental-summary-ext-2023-10-23-med (gpt-4.0)', name: 'vectara-experimental-summary-ext-2023-10-23-med', - description: 'In beta, only available to paying Scale Vectara users' + description: `In beta, only available to Scale Vectara users` } ], default: 'vectara-summary-ext-v1.2.0' @@ -228,7 +229,7 @@ class VectaraChain_Chains implements INode { async run(nodeData: INodeData, input: string): Promise { const vectorStore = nodeData.inputs?.vectaraStore as VectaraStore - const responseLang = (nodeData.inputs?.responseLang as string) ?? 'auto' + const responseLang = (nodeData.inputs?.responseLang as string) ?? 'eng' const summarizerPromptName = nodeData.inputs?.summarizerPromptName as string const maxSummarizedResultsStr = nodeData.inputs?.maxSummarizedResults as string const maxSummarizedResults = maxSummarizedResultsStr ? parseInt(maxSummarizedResultsStr, 10) : 7 @@ -247,17 +248,31 @@ class VectaraChain_Chains implements INode { lexicalInterpolationConfig: { lambda: vectaraFilter?.lambda ?? 0.025 } })) + // Vectara reranker ID for MMR (https://docs.vectara.com/docs/api-reference/search-apis/reranking#maximal-marginal-relevance-mmr-reranker) + const mmrRerankerId = 272725718 + const mmrEnabled = vectaraFilter?.mmrConfig?.enabled + const data = { query: [ { query: input, start: 0, - numResults: topK, + numResults: mmrEnabled ? vectaraFilter?.mmrTopK : topK, + corpusKey: corpusKeys, contextConfig: { sentencesAfter: vectaraFilter?.contextConfig?.sentencesAfter ?? 2, sentencesBefore: vectaraFilter?.contextConfig?.sentencesBefore ?? 2 }, - corpusKey: corpusKeys, + ...(mmrEnabled + ? { + rerankingConfig: { + rerankerId: mmrRerankerId, + mmrConfig: { + diversityBias: vectaraFilter?.mmrConfig.diversityBias + } + } + } + : {}), summary: [ { summarizerPromptName, @@ -285,6 +300,14 @@ class VectaraChain_Chains implements INode { const documents = result.responseSet[0].document let rawSummarizedText = '' + // remove responses that are not in the topK (in case of MMR) + // Note that this does not really matter functionally due to the reorder citations, but it is more efficient + const maxResponses = mmrEnabled ? Math.min(responses.length, topK) : responses.length + if (responses.length > maxResponses) { + responses.splice(0, maxResponses) + } + + // Add metadata to each text response given its corresponding document metadata for (let i = 0; i < responses.length; i += 1) { const responseMetadata = responses[i].metadata const documentMetadata = documents[responses[i].documentIndex].metadata @@ -301,13 +324,13 @@ class VectaraChain_Chains implements INode { responses[i].metadata = combinedMetadata } + // Create the summarization response const summaryStatus = result.responseSet[0].summary[0].status if (summaryStatus.length > 0 && summaryStatus[0].code === 'BAD_REQUEST') { throw new Error( `BAD REQUEST: Too much text for the summarizer to summarize. Please try reducing the number of search results to summarize, or the context of each result by adjusting the 'summary_num_sentences', and 'summary_num_results' parameters respectively.` ) } - if ( summaryStatus.length > 0 && summaryStatus[0].code === 'NOT_FOUND' && @@ -316,8 +339,8 @@ class VectaraChain_Chains implements INode { throw new Error(`BAD REQUEST: summarizer ${summarizerPromptName} is invalid for this account.`) } + // Reorder citations in summary and create the list of returned source documents rawSummarizedText = result.responseSet[0].summary[0]?.text - let summarizedText = reorderCitations(rawSummarizedText) let summaryResponses = applyCitationOrder(responses, rawSummarizedText) diff --git a/packages/components/nodes/chains/VectorDBQAChain/VectorDBQAChain.ts b/packages/components/nodes/chains/VectorDBQAChain/VectorDBQAChain.ts index 13de17dc..594ed921 100644 --- a/packages/components/nodes/chains/VectorDBQAChain/VectorDBQAChain.ts +++ b/packages/components/nodes/chains/VectorDBQAChain/VectorDBQAChain.ts @@ -21,7 +21,7 @@ class VectorDBQAChain_Chains implements INode { this.name = 'vectorDBQAChain' this.version = 1.0 this.type = 'VectorDBQAChain' - this.icon = 'chain.svg' + this.icon = 'vectordb.svg' this.category = 'Chains' this.description = 'QA chain for vector databases' this.baseClasses = [this.type, ...getBaseClasses(VectorDBQAChain)] diff --git a/packages/components/nodes/chains/VectorDBQAChain/chain.svg b/packages/components/nodes/chains/VectorDBQAChain/chain.svg deleted file mode 100644 index a5b32f90..00000000 --- a/packages/components/nodes/chains/VectorDBQAChain/chain.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/packages/components/nodes/chains/VectorDBQAChain/vectordb.svg b/packages/components/nodes/chains/VectorDBQAChain/vectordb.svg new file mode 100644 index 00000000..7add9f8d --- /dev/null +++ b/packages/components/nodes/chains/VectorDBQAChain/vectordb.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/components/nodes/chatmodels/AWSBedrock/AWSChatBedrock.ts b/packages/components/nodes/chatmodels/AWSBedrock/AWSChatBedrock.ts index 956fcdb3..fedd731d 100644 --- a/packages/components/nodes/chatmodels/AWSBedrock/AWSChatBedrock.ts +++ b/packages/components/nodes/chatmodels/AWSBedrock/AWSChatBedrock.ts @@ -1,9 +1,9 @@ import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' -import { ChatBedrock } from 'langchain/chat_models/bedrock' +import { BedrockChat } from 'langchain/chat_models/bedrock' import { BaseBedrockInput } from 'langchain/dist/util/bedrock' import { BaseCache } from 'langchain/schema' -import { BaseLLMParams } from 'langchain/llms/base' +import { BaseChatModelParams } from 'langchain/chat_models/base' /** * I had to run the following to build the component @@ -25,14 +25,14 @@ class AWSChatBedrock_ChatModels implements INode { inputs: INodeParams[] constructor() { - this.label = 'AWS Bedrock' + this.label = 'AWS ChatBedrock' this.name = 'awsChatBedrock' this.version = 3.0 this.type = 'AWSChatBedrock' - this.icon = 'awsBedrock.png' + this.icon = 'aws.svg' this.category = 'Chat Models' this.description = 'Wrapper around AWS Bedrock large language models that use the Chat endpoint' - this.baseClasses = [this.type, ...getBaseClasses(ChatBedrock)] + this.baseClasses = [this.type, ...getBaseClasses(BedrockChat)] this.credential = { label: 'AWS Credential', name: 'credential', @@ -102,6 +102,13 @@ class AWSChatBedrock_ChatModels implements INode { ], default: 'anthropic.claude-v2' }, + { + label: 'Custom Model Name', + name: 'customModel', + description: 'If provided, will override model selected from Model Name option', + type: 'string', + optional: true + }, { label: 'Temperature', name: 'temperature', @@ -109,6 +116,7 @@ class AWSChatBedrock_ChatModels implements INode { step: 0.1, description: 'Temperature parameter may not apply to certain model. Please check available model parameters', optional: true, + additionalParams: true, default: 0.7 }, { @@ -118,6 +126,7 @@ class AWSChatBedrock_ChatModels implements INode { step: 10, description: 'Max Tokens parameter may not apply to certain model. Please check available model parameters', optional: true, + additionalParams: true, default: 200 } ] @@ -126,14 +135,15 @@ class AWSChatBedrock_ChatModels implements INode { async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { const iRegion = nodeData.inputs?.region as string const iModel = nodeData.inputs?.model as string + const customModel = nodeData.inputs?.customModel as string const iTemperature = nodeData.inputs?.temperature as string const iMax_tokens_to_sample = nodeData.inputs?.max_tokens_to_sample as string const cache = nodeData.inputs?.cache as BaseCache const streaming = nodeData.inputs?.streaming as boolean - const obj: BaseBedrockInput & BaseLLMParams = { + const obj: BaseBedrockInput & BaseChatModelParams = { region: iRegion, - model: iModel, + model: customModel ? customModel : iModel, maxTokens: parseInt(iMax_tokens_to_sample, 10), temperature: parseFloat(iTemperature), streaming: streaming ?? true @@ -160,7 +170,7 @@ class AWSChatBedrock_ChatModels implements INode { } if (cache) obj.cache = cache - const amazonBedrock = new ChatBedrock(obj) + const amazonBedrock = new BedrockChat(obj) return amazonBedrock } } diff --git a/packages/components/nodes/chatmodels/AWSBedrock/aws.svg b/packages/components/nodes/chatmodels/AWSBedrock/aws.svg new file mode 100644 index 00000000..d783497e --- /dev/null +++ b/packages/components/nodes/chatmodels/AWSBedrock/aws.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/chatmodels/AWSBedrock/awsBedrock.png b/packages/components/nodes/chatmodels/AWSBedrock/awsBedrock.png deleted file mode 100644 index 483bc69a..00000000 Binary files a/packages/components/nodes/chatmodels/AWSBedrock/awsBedrock.png and /dev/null differ diff --git a/packages/components/nodes/chatmodels/AzureChatOpenAI/Azure.svg b/packages/components/nodes/chatmodels/AzureChatOpenAI/Azure.svg index 47ad8c44..7b150811 100644 --- a/packages/components/nodes/chatmodels/AzureChatOpenAI/Azure.svg +++ b/packages/components/nodes/chatmodels/AzureChatOpenAI/Azure.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/packages/components/nodes/chatmodels/AzureChatOpenAI/AzureChatOpenAI.ts b/packages/components/nodes/chatmodels/AzureChatOpenAI/AzureChatOpenAI.ts index 99e151e6..9b7b724a 100644 --- a/packages/components/nodes/chatmodels/AzureChatOpenAI/AzureChatOpenAI.ts +++ b/packages/components/nodes/chatmodels/AzureChatOpenAI/AzureChatOpenAI.ts @@ -1,7 +1,6 @@ -import { OpenAIBaseInput } from 'langchain/dist/types/openai-types' import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' -import { AzureOpenAIInput, ChatOpenAI } from 'langchain/chat_models/openai' +import { AzureOpenAIInput, ChatOpenAI, OpenAIChatInput } from 'langchain/chat_models/openai' import { BaseCache } from 'langchain/schema' import { BaseLLMParams } from 'langchain/llms/base' @@ -123,7 +122,7 @@ class AzureChatOpenAI_ChatModels implements INode { const azureOpenAIApiDeploymentName = getCredentialParam('azureOpenAIApiDeploymentName', credentialData, nodeData) const azureOpenAIApiVersion = getCredentialParam('azureOpenAIApiVersion', credentialData, nodeData) - const obj: Partial & BaseLLMParams & Partial = { + const obj: Partial & BaseLLMParams & Partial = { temperature: parseFloat(temperature), modelName, azureOpenAIApiKey, diff --git a/packages/components/nodes/chatmodels/AzureChatOpenAI/AzureChatOpenAI_LlamaIndex.ts b/packages/components/nodes/chatmodels/AzureChatOpenAI/AzureChatOpenAI_LlamaIndex.ts index 850c2bc5..e570b263 100644 --- a/packages/components/nodes/chatmodels/AzureChatOpenAI/AzureChatOpenAI_LlamaIndex.ts +++ b/packages/components/nodes/chatmodels/AzureChatOpenAI/AzureChatOpenAI_LlamaIndex.ts @@ -29,7 +29,7 @@ class AzureChatOpenAI_LlamaIndex_ChatModels implements INode { this.type = 'AzureChatOpenAI' this.icon = 'Azure.svg' this.category = 'Chat Models' - this.description = 'Wrapper around Azure OpenAI Chat LLM with LlamaIndex implementation' + this.description = 'Wrapper around Azure OpenAI Chat LLM specific for LlamaIndex' this.baseClasses = [this.type, 'BaseChatModel_LlamaIndex', ...getBaseClasses(OpenAI)] this.tags = ['LlamaIndex'] this.credential = { diff --git a/packages/components/nodes/chatmodels/Bittensor/Bittensor.ts b/packages/components/nodes/chatmodels/Bittensor/Bittensor.ts index 36b084e6..9563ea43 100644 --- a/packages/components/nodes/chatmodels/Bittensor/Bittensor.ts +++ b/packages/components/nodes/chatmodels/Bittensor/Bittensor.ts @@ -19,7 +19,7 @@ class Bittensor_ChatModels implements INode { this.name = 'NIBittensorChatModel' this.version = 2.0 this.type = 'BittensorChat' - this.icon = 'logo.png' + this.icon = 'NIBittensor.svg' this.category = 'Chat Models' this.description = 'Wrapper around Bittensor subnet 1 large language models' this.baseClasses = [this.type, ...getBaseClasses(NIBittensorChatModel)] diff --git a/packages/components/nodes/chatmodels/Bittensor/NIBittensor.svg b/packages/components/nodes/chatmodels/Bittensor/NIBittensor.svg new file mode 100644 index 00000000..062cd66b --- /dev/null +++ b/packages/components/nodes/chatmodels/Bittensor/NIBittensor.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/chatmodels/Bittensor/logo.png b/packages/components/nodes/chatmodels/Bittensor/logo.png deleted file mode 100644 index ad51774d..00000000 Binary files a/packages/components/nodes/chatmodels/Bittensor/logo.png and /dev/null differ diff --git a/packages/components/nodes/chatmodels/ChatAnthropic/Anthropic.svg b/packages/components/nodes/chatmodels/ChatAnthropic/Anthropic.svg new file mode 100644 index 00000000..84bc18ca --- /dev/null +++ b/packages/components/nodes/chatmodels/ChatAnthropic/Anthropic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/chatmodels/ChatAnthropic/ChatAnthropic.ts b/packages/components/nodes/chatmodels/ChatAnthropic/ChatAnthropic.ts index 8209c04a..794058bd 100644 --- a/packages/components/nodes/chatmodels/ChatAnthropic/ChatAnthropic.ts +++ b/packages/components/nodes/chatmodels/ChatAnthropic/ChatAnthropic.ts @@ -22,7 +22,7 @@ class ChatAnthropic_ChatModels implements INode { this.name = 'chatAnthropic' this.version = 3.0 this.type = 'ChatAnthropic' - this.icon = 'chatAnthropic.png' + this.icon = 'Anthropic.svg' this.category = 'Chat Models' this.description = 'Wrapper around ChatAnthropic large language models that use the Chat endpoint' this.baseClasses = [this.type, ...getBaseClasses(ChatAnthropic)] diff --git a/packages/components/nodes/chatmodels/ChatAnthropic/ChatAnthropic_LlamaIndex.ts b/packages/components/nodes/chatmodels/ChatAnthropic/ChatAnthropic_LlamaIndex.ts index b989ef76..69a15114 100644 --- a/packages/components/nodes/chatmodels/ChatAnthropic/ChatAnthropic_LlamaIndex.ts +++ b/packages/components/nodes/chatmodels/ChatAnthropic/ChatAnthropic_LlamaIndex.ts @@ -1,7 +1,6 @@ import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' import { Anthropic } from 'llamaindex' -import { availableModels } from './utils' class ChatAnthropic_LlamaIndex_ChatModels implements INode { label: string @@ -21,9 +20,9 @@ class ChatAnthropic_LlamaIndex_ChatModels implements INode { this.name = 'chatAnthropic_LlamaIndex' this.version = 1.0 this.type = 'ChatAnthropic' - this.icon = 'chatAnthropic.png' + this.icon = 'Anthropic.svg' this.category = 'Chat Models' - this.description = 'Wrapper around ChatAnthropic LLM with LlamaIndex implementation' + this.description = 'Wrapper around ChatAnthropic LLM specific for LlamaIndex' this.baseClasses = [this.type, 'BaseChatModel_LlamaIndex', ...getBaseClasses(Anthropic)] this.tags = ['LlamaIndex'] this.credential = { @@ -37,7 +36,18 @@ class ChatAnthropic_LlamaIndex_ChatModels implements INode { label: 'Model Name', name: 'modelName', type: 'options', - options: [...availableModels], + options: [ + { + label: 'claude-2', + name: 'claude-2', + description: 'Claude 2 latest major version, automatically get updates to the model as they are released' + }, + { + label: 'claude-instant-1', + name: 'claude-instant-1', + description: 'Claude Instant latest major version, automatically get updates to the model as they are released' + } + ], default: 'claude-2', optional: true }, @@ -70,7 +80,7 @@ class ChatAnthropic_LlamaIndex_ChatModels implements INode { async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { const temperature = nodeData.inputs?.temperature as string - const modelName = nodeData.inputs?.modelName as string + const modelName = nodeData.inputs?.modelName as 'claude-2' | 'claude-instant-1' | undefined const maxTokensToSample = nodeData.inputs?.maxTokensToSample as string const topP = nodeData.inputs?.topP as string diff --git a/packages/components/nodes/chatmodels/ChatAnthropic/chatAnthropic.png b/packages/components/nodes/chatmodels/ChatAnthropic/chatAnthropic.png deleted file mode 100644 index 42324cb7..00000000 Binary files a/packages/components/nodes/chatmodels/ChatAnthropic/chatAnthropic.png and /dev/null differ diff --git a/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/ChatGoogleGenerativeAI.ts b/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/ChatGoogleGenerativeAI.ts new file mode 100644 index 00000000..9a4b8891 --- /dev/null +++ b/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/ChatGoogleGenerativeAI.ts @@ -0,0 +1,194 @@ +import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' +import { convertMultiOptionsToStringArray, getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' +import { BaseCache } from 'langchain/schema' +import { ChatGoogleGenerativeAI, GoogleGenerativeAIChatInput } from '@langchain/google-genai' +import { HarmBlockThreshold, HarmCategory } from '@google/generative-ai' +import type { SafetySetting } from '@google/generative-ai' + +class GoogleGenerativeAI_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 = 'ChatGoogleGenerativeAI' + this.name = 'chatGoogleGenerativeAI' + this.version = 1.0 + this.type = 'ChatGoogleGenerativeAI' + this.icon = 'GoogleGemini.svg' + this.category = 'Chat Models' + this.description = 'Wrapper around Google Gemini large language models that use the Chat endpoint' + this.baseClasses = [this.type, ...getBaseClasses(ChatGoogleGenerativeAI)] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['googleGenerativeAI'], + optional: false, + description: 'Google Generative AI credential.' + } + this.inputs = [ + { + label: 'Cache', + name: 'cache', + type: 'BaseCache', + optional: true + }, + { + label: 'Model Name', + name: 'modelName', + type: 'options', + options: [ + { + label: 'gemini-pro', + name: 'gemini-pro' + } + ], + default: 'gemini-pro' + }, + { + label: 'Temperature', + name: 'temperature', + type: 'number', + step: 0.1, + default: 0.9, + optional: true + }, + { + label: 'Max Output Tokens', + name: 'maxOutputTokens', + type: 'number', + step: 1, + optional: true, + additionalParams: true + }, + { + label: 'Top Probability', + name: 'topP', + type: 'number', + step: 0.1, + optional: true, + additionalParams: true + }, + { + label: 'Top Next Highest Probability Tokens', + name: 'topK', + type: 'number', + description: `Decode using top-k sampling: consider the set of top_k most probable tokens. Must be positive`, + step: 1, + optional: true, + additionalParams: true + }, + { + label: 'Harm Category', + name: 'harmCategory', + type: 'multiOptions', + description: + 'Refer to official guide on how to use Harm Category', + options: [ + { + label: 'Dangerous', + name: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT + }, + { + label: 'Harassment', + name: HarmCategory.HARM_CATEGORY_HARASSMENT + }, + { + label: 'Hate Speech', + name: HarmCategory.HARM_CATEGORY_HATE_SPEECH + }, + { + label: 'Sexually Explicit', + name: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT + } + ], + optional: true, + additionalParams: true + }, + { + label: 'Harm Block Threshold', + name: 'harmBlockThreshold', + type: 'multiOptions', + description: + 'Refer to official guide on how to use Harm Block Threshold', + options: [ + { + label: 'Low and Above', + name: HarmBlockThreshold.BLOCK_LOW_AND_ABOVE + }, + { + label: 'Medium and Above', + name: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE + }, + { + label: 'None', + name: HarmBlockThreshold.BLOCK_NONE + }, + { + label: 'Only High', + name: HarmBlockThreshold.BLOCK_ONLY_HIGH + }, + { + label: 'Threshold Unspecified', + name: HarmBlockThreshold.HARM_BLOCK_THRESHOLD_UNSPECIFIED + } + ], + optional: true, + additionalParams: true + } + ] + } + + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + const credentialData = await getCredentialData(nodeData.credential ?? '', options) + const apiKey = getCredentialParam('googleGenerativeAPIKey', credentialData, nodeData) + + const temperature = nodeData.inputs?.temperature as string + const modelName = nodeData.inputs?.modelName as string + const maxOutputTokens = nodeData.inputs?.maxOutputTokens as string + const topP = nodeData.inputs?.topP as string + const topK = nodeData.inputs?.topK as string + const harmCategory = nodeData.inputs?.harmCategory as string + const harmBlockThreshold = nodeData.inputs?.harmBlockThreshold as string + const cache = nodeData.inputs?.cache as BaseCache + + const obj: Partial = { + apiKey: apiKey, + modelName: modelName, + maxOutputTokens: 2048 + } + + if (maxOutputTokens) obj.maxOutputTokens = parseInt(maxOutputTokens, 10) + + const model = new ChatGoogleGenerativeAI(obj) + if (topP) model.topP = parseFloat(topP) + if (topK) model.topK = parseFloat(topK) + if (cache) model.cache = cache + if (temperature) model.temperature = parseFloat(temperature) + + // Safety Settings + let harmCategories: string[] = convertMultiOptionsToStringArray(harmCategory) + let harmBlockThresholds: string[] = convertMultiOptionsToStringArray(harmBlockThreshold) + if (harmCategories.length != harmBlockThresholds.length) + throw new Error(`Harm Category & Harm Block Threshold are not the same length`) + const safetySettings: SafetySetting[] = harmCategories.map((harmCategory, index) => { + return { + category: harmCategory as HarmCategory, + threshold: harmBlockThresholds[index] as HarmBlockThreshold + } + }) + if (safetySettings.length > 0) model.safetySettings = safetySettings + + return model + } +} + +module.exports = { nodeClass: GoogleGenerativeAI_ChatModels } diff --git a/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/GoogleGemini.svg b/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/GoogleGemini.svg new file mode 100644 index 00000000..53b497fa --- /dev/null +++ b/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/GoogleGemini.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/components/nodes/chatmodels/ChatGooglePaLM/ChatGooglePaLM.ts b/packages/components/nodes/chatmodels/ChatGooglePaLM/ChatGooglePaLM.ts index 121406c7..ab7a6169 100644 --- a/packages/components/nodes/chatmodels/ChatGooglePaLM/ChatGooglePaLM.ts +++ b/packages/components/nodes/chatmodels/ChatGooglePaLM/ChatGooglePaLM.ts @@ -20,7 +20,7 @@ class ChatGooglePaLM_ChatModels implements INode { this.name = 'chatGooglePaLM' this.version = 2.0 this.type = 'ChatGooglePaLM' - this.icon = 'Google_PaLM_Logo.svg' + this.icon = 'GooglePaLM.svg' this.category = 'Chat Models' this.description = 'Wrapper around Google MakerSuite PaLM large language models using the Chat endpoint' this.baseClasses = [this.type, ...getBaseClasses(ChatGooglePaLM)] diff --git a/packages/components/nodes/chatmodels/ChatGooglePaLM/GooglePaLM.svg b/packages/components/nodes/chatmodels/ChatGooglePaLM/GooglePaLM.svg new file mode 100644 index 00000000..ed47326a --- /dev/null +++ b/packages/components/nodes/chatmodels/ChatGooglePaLM/GooglePaLM.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/chatmodels/ChatGooglePaLM/Google_PaLM_Logo.svg b/packages/components/nodes/chatmodels/ChatGooglePaLM/Google_PaLM_Logo.svg deleted file mode 100644 index 5c345fe1..00000000 --- a/packages/components/nodes/chatmodels/ChatGooglePaLM/Google_PaLM_Logo.svg +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/components/nodes/chatmodels/ChatGoogleVertexAI/ChatGoogleVertexAI.ts b/packages/components/nodes/chatmodels/ChatGoogleVertexAI/ChatGoogleVertexAI.ts index 6b070bd9..4c961853 100644 --- a/packages/components/nodes/chatmodels/ChatGoogleVertexAI/ChatGoogleVertexAI.ts +++ b/packages/components/nodes/chatmodels/ChatGoogleVertexAI/ChatGoogleVertexAI.ts @@ -21,7 +21,7 @@ class GoogleVertexAI_ChatModels implements INode { this.name = 'chatGoogleVertexAI' this.version = 2.0 this.type = 'ChatGoogleVertexAI' - this.icon = 'vertexai.svg' + this.icon = 'GoogleVertex.svg' this.category = 'Chat Models' this.description = 'Wrapper around VertexAI large language models that use the Chat endpoint' this.baseClasses = [this.type, ...getBaseClasses(ChatGoogleVertexAI)] diff --git a/packages/components/nodes/chatmodels/ChatGoogleVertexAI/GoogleVertex.svg b/packages/components/nodes/chatmodels/ChatGoogleVertexAI/GoogleVertex.svg new file mode 100644 index 00000000..a517740f --- /dev/null +++ b/packages/components/nodes/chatmodels/ChatGoogleVertexAI/GoogleVertex.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/chatmodels/ChatGoogleVertexAI/vertexai.svg b/packages/components/nodes/chatmodels/ChatGoogleVertexAI/vertexai.svg deleted file mode 100644 index 31244412..00000000 --- a/packages/components/nodes/chatmodels/ChatGoogleVertexAI/vertexai.svg +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/packages/components/nodes/chatmodels/ChatHuggingFace/ChatHuggingFace.ts b/packages/components/nodes/chatmodels/ChatHuggingFace/ChatHuggingFace.ts index 153c5d10..dff78193 100644 --- a/packages/components/nodes/chatmodels/ChatHuggingFace/ChatHuggingFace.ts +++ b/packages/components/nodes/chatmodels/ChatHuggingFace/ChatHuggingFace.ts @@ -20,7 +20,7 @@ class ChatHuggingFace_ChatModels implements INode { this.name = 'chatHuggingFace' this.version = 2.0 this.type = 'ChatHuggingFace' - this.icon = 'huggingface.png' + this.icon = 'HuggingFace.svg' this.category = 'Chat Models' this.description = 'Wrapper around HuggingFace large language models' this.baseClasses = [this.type, 'BaseChatModel', ...getBaseClasses(HuggingFaceInference)] diff --git a/packages/components/nodes/chatmodels/ChatHuggingFace/HuggingFace.svg b/packages/components/nodes/chatmodels/ChatHuggingFace/HuggingFace.svg new file mode 100644 index 00000000..58c85d57 --- /dev/null +++ b/packages/components/nodes/chatmodels/ChatHuggingFace/HuggingFace.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/chatmodels/ChatHuggingFace/huggingface.png b/packages/components/nodes/chatmodels/ChatHuggingFace/huggingface.png deleted file mode 100644 index f8f202a4..00000000 Binary files a/packages/components/nodes/chatmodels/ChatHuggingFace/huggingface.png and /dev/null differ diff --git a/packages/components/nodes/chatmodels/ChatLocalAI/ChatLocalAI.ts b/packages/components/nodes/chatmodels/ChatLocalAI/ChatLocalAI.ts index 18ed409b..f2825d0d 100644 --- a/packages/components/nodes/chatmodels/ChatLocalAI/ChatLocalAI.ts +++ b/packages/components/nodes/chatmodels/ChatLocalAI/ChatLocalAI.ts @@ -1,5 +1,5 @@ -import { INode, INodeData, INodeParams } from '../../../src/Interface' -import { getBaseClasses } from '../../../src/utils' +import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' +import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' import { OpenAIChat } from 'langchain/llms/openai' import { OpenAIChatInput } from 'langchain/chat_models/openai' import { BaseCache } from 'langchain/schema' @@ -14,6 +14,7 @@ class ChatLocalAI_ChatModels implements INode { category: string description: string baseClasses: string[] + credential: INodeParams inputs: INodeParams[] constructor() { @@ -25,6 +26,13 @@ class ChatLocalAI_ChatModels implements INode { this.category = 'Chat Models' this.description = 'Use local LLMs like llama.cpp, gpt4all using LocalAI' this.baseClasses = [this.type, 'BaseChatModel', ...getBaseClasses(OpenAIChat)] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['localAIApi'], + optional: true + } this.inputs = [ { label: 'Cache', @@ -79,13 +87,16 @@ class ChatLocalAI_ChatModels implements INode { ] } - async init(nodeData: INodeData): Promise { + 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 timeout = nodeData.inputs?.timeout as string const basePath = nodeData.inputs?.basePath as string + const credentialData = await getCredentialData(nodeData.credential ?? '', options) + const localAIApiKey = getCredentialParam('localAIApiKey', credentialData, nodeData) + const cache = nodeData.inputs?.cache as BaseCache const obj: Partial & BaseLLMParams & { openAIApiKey?: string } = { @@ -98,6 +109,7 @@ class ChatLocalAI_ChatModels implements INode { if (topP) obj.topP = parseFloat(topP) if (timeout) obj.timeout = parseInt(timeout, 10) if (cache) obj.cache = cache + if (localAIApiKey) obj.openAIApiKey = localAIApiKey const model = new OpenAIChat(obj, { basePath }) diff --git a/packages/components/nodes/chatmodels/ChatMistral/ChatMistral.ts b/packages/components/nodes/chatmodels/ChatMistral/ChatMistral.ts new file mode 100644 index 00000000..4524db46 --- /dev/null +++ b/packages/components/nodes/chatmodels/ChatMistral/ChatMistral.ts @@ -0,0 +1,150 @@ +import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' +import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' +import { BaseCache } from 'langchain/schema' +import { ChatMistralAI, ChatMistralAIInput } from '@langchain/mistralai' + +class ChatMistral_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 = 'ChatMistralAI' + this.name = 'chatMistralAI' + this.version = 1.0 + this.type = 'ChatMistralAI' + this.icon = 'MistralAI.svg' + this.category = 'Chat Models' + this.description = 'Wrapper around Mistral large language models that use the Chat endpoint' + this.baseClasses = [this.type, ...getBaseClasses(ChatMistralAI)] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['mistralAIApi'] + } + this.inputs = [ + { + label: 'Cache', + name: 'cache', + type: 'BaseCache', + optional: true + }, + { + label: 'Model Name', + name: 'modelName', + type: 'options', + options: [ + { + label: 'mistral-tiny', + name: 'mistral-tiny' + }, + { + label: 'mistral-small', + name: 'mistral-small' + }, + { + label: 'mistral-medium', + name: 'mistral-medium' + } + ], + default: 'mistral-tiny' + }, + { + label: 'Temperature', + name: 'temperature', + type: 'number', + description: + 'What sampling temperature to use, between 0.0 and 1.0. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.', + step: 0.1, + default: 0.9, + optional: true + }, + { + label: 'Max Output Tokens', + name: 'maxOutputTokens', + type: 'number', + description: 'The maximum number of tokens to generate in the completion.', + step: 1, + optional: true, + additionalParams: true + }, + { + label: 'Top Probability', + name: 'topP', + type: 'number', + description: + 'Nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered.', + step: 0.1, + optional: true, + additionalParams: true + }, + { + label: 'Random Seed', + name: 'randomSeed', + type: 'number', + description: 'The seed to use for random sampling. If set, different calls will generate deterministic results.', + step: 1, + optional: true, + additionalParams: true + }, + { + label: 'Safe Mode', + name: 'safeMode', + type: 'boolean', + description: 'Whether to inject a safety prompt before all conversations.', + optional: true, + additionalParams: true + }, + { + label: 'Override Endpoint', + name: 'overrideEndpoint', + type: 'string', + optional: true, + additionalParams: true + } + ] + } + + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + const credentialData = await getCredentialData(nodeData.credential ?? '', options) + const apiKey = getCredentialParam('mistralAIAPIKey', credentialData, nodeData) + + const temperature = nodeData.inputs?.temperature as string + const modelName = nodeData.inputs?.modelName as string + const maxOutputTokens = nodeData.inputs?.maxOutputTokens as string + const topP = nodeData.inputs?.topP as string + const safeMode = nodeData.inputs?.safeMode as boolean + const randomSeed = nodeData.inputs?.safeMode as string + const overrideEndpoint = nodeData.inputs?.overrideEndpoint as string + const streaming = nodeData.inputs?.streaming as boolean + const cache = nodeData.inputs?.cache as BaseCache + + const obj: ChatMistralAIInput = { + apiKey: apiKey, + modelName: modelName, + streaming: streaming ?? true + } + + if (maxOutputTokens) obj.maxTokens = parseInt(maxOutputTokens, 10) + if (topP) obj.topP = parseFloat(topP) + if (cache) obj.cache = cache + if (temperature) obj.temperature = parseFloat(temperature) + if (randomSeed) obj.randomSeed = parseFloat(randomSeed) + if (safeMode) obj.safeMode = safeMode + if (overrideEndpoint) obj.endpoint = overrideEndpoint + + const model = new ChatMistralAI(obj) + + return model + } +} + +module.exports = { nodeClass: ChatMistral_ChatModels } diff --git a/packages/components/nodes/chatmodels/ChatMistral/MistralAI.svg b/packages/components/nodes/chatmodels/ChatMistral/MistralAI.svg new file mode 100644 index 00000000..aa84b39c --- /dev/null +++ b/packages/components/nodes/chatmodels/ChatMistral/MistralAI.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/chatmodels/ChatOllama/ChatOllama.ts b/packages/components/nodes/chatmodels/ChatOllama/ChatOllama.ts index 31267743..d445c7e1 100644 --- a/packages/components/nodes/chatmodels/ChatOllama/ChatOllama.ts +++ b/packages/components/nodes/chatmodels/ChatOllama/ChatOllama.ts @@ -1,8 +1,7 @@ import { INode, INodeData, INodeParams } from '../../../src/Interface' import { getBaseClasses } from '../../../src/utils' -import { ChatOllama } from 'langchain/chat_models/ollama' +import { ChatOllama, ChatOllamaInput } from 'langchain/chat_models/ollama' import { BaseCache } from 'langchain/schema' -import { OllamaInput } from 'langchain/dist/util/ollama' import { BaseLLMParams } from 'langchain/llms/base' class ChatOllama_ChatModels implements INode { @@ -22,7 +21,7 @@ class ChatOllama_ChatModels implements INode { this.name = 'chatOllama' this.version = 2.0 this.type = 'ChatOllama' - this.icon = 'ollama.png' + this.icon = 'Ollama.svg' this.category = 'Chat Models' this.description = 'Chat completion using open-source LLM on Ollama' this.baseClasses = [this.type, ...getBaseClasses(ChatOllama)] @@ -209,7 +208,7 @@ class ChatOllama_ChatModels implements INode { const cache = nodeData.inputs?.cache as BaseCache - const obj: OllamaInput & BaseLLMParams = { + const obj: ChatOllamaInput & BaseLLMParams = { baseUrl, temperature: parseFloat(temperature), model: modelName diff --git a/packages/components/nodes/chatmodels/ChatOllama/Ollama.svg b/packages/components/nodes/chatmodels/ChatOllama/Ollama.svg new file mode 100644 index 00000000..2dc8df53 --- /dev/null +++ b/packages/components/nodes/chatmodels/ChatOllama/Ollama.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/chatmodels/ChatOllama/ollama.png b/packages/components/nodes/chatmodels/ChatOllama/ollama.png deleted file mode 100644 index 8cd2cf1e..00000000 Binary files a/packages/components/nodes/chatmodels/ChatOllama/ollama.png and /dev/null differ diff --git a/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI.ts b/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI.ts index 32ff5d64..49326163 100644 --- a/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI.ts +++ b/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI.ts @@ -21,7 +21,7 @@ class ChatOpenAI_ChatModels implements INode { this.name = 'chatOpenAI' this.version = 2.0 this.type = 'ChatOpenAI' - this.icon = 'openai.png' + this.icon = 'openai.svg' this.category = 'Chat Models' this.description = 'Wrapper around OpenAI large language models that use the Chat endpoint' this.baseClasses = [this.type, ...getBaseClasses(ChatOpenAI)] diff --git a/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI_LlamaIndex.ts b/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI_LlamaIndex.ts index 147bfe3f..58b40823 100644 --- a/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI_LlamaIndex.ts +++ b/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI_LlamaIndex.ts @@ -20,9 +20,9 @@ class ChatOpenAI_LlamaIndex_LLMs implements INode { this.name = 'chatOpenAI_LlamaIndex' this.version = 1.0 this.type = 'ChatOpenAI' - this.icon = 'openai.png' + this.icon = 'openai.svg' this.category = 'Chat Models' - this.description = 'Wrapper around OpenAI Chat LLM with LlamaIndex implementation' + this.description = 'Wrapper around OpenAI Chat LLM specific for LlamaIndex' this.baseClasses = [this.type, 'BaseChatModel_LlamaIndex', ...getBaseClasses(OpenAI)] this.tags = ['LlamaIndex'] this.credential = { diff --git a/packages/components/nodes/chatmodels/ChatOpenAI/openai.png b/packages/components/nodes/chatmodels/ChatOpenAI/openai.png deleted file mode 100644 index de08a05b..00000000 Binary files a/packages/components/nodes/chatmodels/ChatOpenAI/openai.png and /dev/null differ diff --git a/packages/components/nodes/chatmodels/ChatOpenAI/openai.svg b/packages/components/nodes/chatmodels/ChatOpenAI/openai.svg new file mode 100644 index 00000000..5c20398a --- /dev/null +++ b/packages/components/nodes/chatmodels/ChatOpenAI/openai.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/packages/components/nodes/chatmodels/ChatOpenAICustom/ChatOpenAICustom.ts b/packages/components/nodes/chatmodels/ChatOpenAICustom/ChatOpenAICustom.ts index 2a01a2e5..e80f17b7 100644 --- a/packages/components/nodes/chatmodels/ChatOpenAICustom/ChatOpenAICustom.ts +++ b/packages/components/nodes/chatmodels/ChatOpenAICustom/ChatOpenAICustom.ts @@ -21,7 +21,7 @@ class ChatOpenAICustom_ChatModels implements INode { this.name = 'chatOpenAICustom' this.version = 2.0 this.type = 'ChatOpenAI-Custom' - this.icon = 'openai.png' + this.icon = 'openai.svg' this.category = 'Chat Models' this.description = 'Custom/FineTuned model using OpenAI Chat compatible API' this.baseClasses = [this.type, ...getBaseClasses(ChatOpenAI)] diff --git a/packages/components/nodes/chatmodels/ChatOpenAICustom/openai.png b/packages/components/nodes/chatmodels/ChatOpenAICustom/openai.png deleted file mode 100644 index de08a05b..00000000 Binary files a/packages/components/nodes/chatmodels/ChatOpenAICustom/openai.png and /dev/null differ diff --git a/packages/components/nodes/chatmodels/ChatOpenAICustom/openai.svg b/packages/components/nodes/chatmodels/ChatOpenAICustom/openai.svg new file mode 100644 index 00000000..5c20398a --- /dev/null +++ b/packages/components/nodes/chatmodels/ChatOpenAICustom/openai.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/packages/components/nodes/documentloaders/API/APILoader.ts b/packages/components/nodes/documentloaders/API/APILoader.ts index 3de6d636..e2212366 100644 --- a/packages/components/nodes/documentloaders/API/APILoader.ts +++ b/packages/components/nodes/documentloaders/API/APILoader.ts @@ -20,7 +20,7 @@ class API_DocumentLoaders implements INode { this.name = 'apiLoader' this.version = 1.0 this.type = 'Document' - this.icon = 'api-loader.png' + this.icon = 'api.svg' this.category = 'Document Loaders' this.description = `Load data from an API` this.baseClasses = [this.type] diff --git a/packages/components/nodes/documentloaders/API/api-loader.png b/packages/components/nodes/documentloaders/API/api-loader.png deleted file mode 100644 index 93668c4c..00000000 Binary files a/packages/components/nodes/documentloaders/API/api-loader.png and /dev/null differ diff --git a/packages/components/nodes/documentloaders/API/api.svg b/packages/components/nodes/documentloaders/API/api.svg new file mode 100644 index 00000000..c353c052 --- /dev/null +++ b/packages/components/nodes/documentloaders/API/api.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/components/nodes/documentloaders/Airtable/Airtable.ts b/packages/components/nodes/documentloaders/Airtable/Airtable.ts index 70d0c674..9a824ac9 100644 --- a/packages/components/nodes/documentloaders/Airtable/Airtable.ts +++ b/packages/components/nodes/documentloaders/Airtable/Airtable.ts @@ -20,7 +20,7 @@ class Airtable_DocumentLoaders implements INode { constructor() { this.label = 'Airtable' this.name = 'airtable' - this.version = 1.0 + this.version = 2.0 this.type = 'Document' this.icon = 'airtable.svg' this.category = 'Document Loaders' @@ -55,6 +55,15 @@ class Airtable_DocumentLoaders implements INode { description: 'If your table URL looks like: https://airtable.com/app11RobdGoX0YNsC/tblJdmvbrgizbYICO/viw9UrP77Id0CE4ee, tblJdmvbrgizbYICO is the table id' }, + { + label: 'View Id', + name: 'viewId', + type: 'string', + placeholder: 'viw9UrP77Id0CE4ee', + description: + 'If your view URL looks like: https://airtable.com/app11RobdGoX0YNsC/tblJdmvbrgizbYICO/viw9UrP77Id0CE4ee, viw9UrP77Id0CE4ee is the view id', + optional: true + }, { label: 'Return All', name: 'returnAll', @@ -83,6 +92,7 @@ class Airtable_DocumentLoaders implements INode { async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { const baseId = nodeData.inputs?.baseId as string const tableId = nodeData.inputs?.tableId as string + const viewId = nodeData.inputs?.viewId as string const returnAll = nodeData.inputs?.returnAll as boolean const limit = nodeData.inputs?.limit as string const textSplitter = nodeData.inputs?.textSplitter as TextSplitter @@ -94,6 +104,7 @@ class Airtable_DocumentLoaders implements INode { const airtableOptions: AirtableLoaderParams = { baseId, tableId, + viewId, returnAll, accessToken, limit: limit ? parseInt(limit, 10) : 100 @@ -133,6 +144,7 @@ interface AirtableLoaderParams { baseId: string tableId: string accessToken: string + viewId?: string limit?: number returnAll?: boolean } @@ -153,16 +165,19 @@ class AirtableLoader extends BaseDocumentLoader { public readonly tableId: string + public readonly viewId?: string + public readonly accessToken: string public readonly limit: number public readonly returnAll: boolean - constructor({ baseId, tableId, accessToken, limit = 100, returnAll = false }: AirtableLoaderParams) { + constructor({ baseId, tableId, viewId, accessToken, limit = 100, returnAll = false }: AirtableLoaderParams) { super() this.baseId = baseId this.tableId = tableId + this.viewId = viewId this.accessToken = accessToken this.limit = limit this.returnAll = returnAll @@ -203,7 +218,7 @@ class AirtableLoader extends BaseDocumentLoader { } private async loadLimit(): Promise { - const params = { maxRecords: this.limit } + const params = { maxRecords: this.limit, view: this.viewId } const data = await this.fetchAirtableData(`https://api.airtable.com/v0/${this.baseId}/${this.tableId}`, params) if (data.records.length === 0) { return [] @@ -212,7 +227,7 @@ class AirtableLoader extends BaseDocumentLoader { } private async loadAll(): Promise { - const params: ICommonObject = { pageSize: 100 } + const params: ICommonObject = { pageSize: 100, view: this.viewId } let data: AirtableLoaderResponse let returnPages: AirtableLoaderPage[] = [] diff --git a/packages/components/nodes/documentloaders/Airtable/airtable.svg b/packages/components/nodes/documentloaders/Airtable/airtable.svg index 867c3b5a..5c2a9950 100644 --- a/packages/components/nodes/documentloaders/Airtable/airtable.svg +++ b/packages/components/nodes/documentloaders/Airtable/airtable.svg @@ -1,9 +1,5 @@ - - - - - - - - + + + + diff --git a/packages/components/nodes/documentloaders/ApifyWebsiteContentCrawler/apify-symbol-transparent.svg b/packages/components/nodes/documentloaders/ApifyWebsiteContentCrawler/apify-symbol-transparent.svg index 423a3328..457caaaa 100644 --- a/packages/components/nodes/documentloaders/ApifyWebsiteContentCrawler/apify-symbol-transparent.svg +++ b/packages/components/nodes/documentloaders/ApifyWebsiteContentCrawler/apify-symbol-transparent.svg @@ -1 +1,5 @@ - \ No newline at end of file + + + + + diff --git a/packages/components/nodes/documentloaders/Cheerio/cheerio.svg b/packages/components/nodes/documentloaders/Cheerio/cheerio.svg index 8e3334b9..575888ef 100644 --- a/packages/components/nodes/documentloaders/Cheerio/cheerio.svg +++ b/packages/components/nodes/documentloaders/Cheerio/cheerio.svg @@ -1,5 +1,4 @@ - - - - - \ No newline at end of file + + + + diff --git a/packages/components/nodes/documentloaders/Confluence/Confluence.ts b/packages/components/nodes/documentloaders/Confluence/Confluence.ts index a17c41b9..bccda24c 100644 --- a/packages/components/nodes/documentloaders/Confluence/Confluence.ts +++ b/packages/components/nodes/documentloaders/Confluence/Confluence.ts @@ -20,7 +20,7 @@ class Confluence_DocumentLoaders implements INode { this.name = 'confluence' this.version = 1.0 this.type = 'Document' - this.icon = 'confluence.png' + this.icon = 'confluence.svg' this.category = 'Document Loaders' this.description = `Load data from a Confluence Document` this.baseClasses = [this.type] diff --git a/packages/components/nodes/documentloaders/Confluence/confluence.png b/packages/components/nodes/documentloaders/Confluence/confluence.png deleted file mode 100644 index 3cbb7b3d..00000000 Binary files a/packages/components/nodes/documentloaders/Confluence/confluence.png and /dev/null differ diff --git a/packages/components/nodes/documentloaders/Confluence/confluence.svg b/packages/components/nodes/documentloaders/Confluence/confluence.svg new file mode 100644 index 00000000..f78072aa --- /dev/null +++ b/packages/components/nodes/documentloaders/Confluence/confluence.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/components/nodes/documentloaders/Csv/Csv.png b/packages/components/nodes/documentloaders/Csv/Csv.png deleted file mode 100644 index 41b84e16..00000000 Binary files a/packages/components/nodes/documentloaders/Csv/Csv.png and /dev/null differ diff --git a/packages/components/nodes/documentloaders/Csv/Csv.ts b/packages/components/nodes/documentloaders/Csv/Csv.ts index 750490b7..a6170b2d 100644 --- a/packages/components/nodes/documentloaders/Csv/Csv.ts +++ b/packages/components/nodes/documentloaders/Csv/Csv.ts @@ -18,7 +18,7 @@ class Csv_DocumentLoaders implements INode { this.name = 'csvFile' this.version = 1.0 this.type = 'Document' - this.icon = 'Csv.png' + this.icon = 'csv.svg' this.category = 'Document Loaders' this.description = `Load data from CSV files` this.baseClasses = [this.type] diff --git a/packages/components/nodes/documentloaders/Csv/csv.svg b/packages/components/nodes/documentloaders/Csv/csv.svg new file mode 100644 index 00000000..df85bfc9 --- /dev/null +++ b/packages/components/nodes/documentloaders/Csv/csv.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/components/nodes/documentloaders/Docx/Docx.png b/packages/components/nodes/documentloaders/Docx/Docx.png deleted file mode 100644 index 6d527bd2..00000000 Binary files a/packages/components/nodes/documentloaders/Docx/Docx.png and /dev/null differ diff --git a/packages/components/nodes/documentloaders/Docx/Docx.ts b/packages/components/nodes/documentloaders/Docx/Docx.ts index 41922775..26883ada 100644 --- a/packages/components/nodes/documentloaders/Docx/Docx.ts +++ b/packages/components/nodes/documentloaders/Docx/Docx.ts @@ -18,7 +18,7 @@ class Docx_DocumentLoaders implements INode { this.name = 'docxFile' this.version = 1.0 this.type = 'Document' - this.icon = 'Docx.png' + this.icon = 'docx.svg' this.category = 'Document Loaders' this.description = `Load data from DOCX files` this.baseClasses = [this.type] diff --git a/packages/components/nodes/documentloaders/Docx/docx.svg b/packages/components/nodes/documentloaders/Docx/docx.svg new file mode 100644 index 00000000..86a46116 --- /dev/null +++ b/packages/components/nodes/documentloaders/Docx/docx.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/components/nodes/documentloaders/Figma/Figma.ts b/packages/components/nodes/documentloaders/Figma/Figma.ts index 3d313044..6d7aa530 100644 --- a/packages/components/nodes/documentloaders/Figma/Figma.ts +++ b/packages/components/nodes/documentloaders/Figma/Figma.ts @@ -1,6 +1,7 @@ import { getCredentialData, getCredentialParam } from '../../../src' import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' import { FigmaFileLoader, FigmaLoaderParams } from 'langchain/document_loaders/web/figma' +import { TextSplitter } from 'langchain/text_splitter' class Figma_DocumentLoaders implements INode { label: string @@ -71,6 +72,8 @@ class Figma_DocumentLoaders implements INode { async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { const nodeIds = (nodeData.inputs?.nodeIds as string)?.trim().split(',') || [] const fileKey = nodeData.inputs?.fileKey as string + const textSplitter = nodeData.inputs?.textSplitter as TextSplitter + const metadata = nodeData.inputs?.metadata const credentialData = await getCredentialData(nodeData.credential ?? '', options) const accessToken = getCredentialParam('accessToken', credentialData, nodeData) @@ -82,7 +85,21 @@ class Figma_DocumentLoaders implements INode { } const loader = new FigmaFileLoader(figmaOptions) - const docs = await loader.load() + + const docs = textSplitter ? await loader.loadAndSplit() : await loader.load() + + if (metadata) { + const parsedMetadata = typeof metadata === 'object' ? metadata : JSON.parse(metadata) + return docs.map((doc) => { + return { + ...doc, + metadata: { + ...doc.metadata, + ...parsedMetadata + } + } + }) + } return docs } diff --git a/packages/components/nodes/documentloaders/Figma/figma.svg b/packages/components/nodes/documentloaders/Figma/figma.svg index c4f85674..355d4ba9 100644 --- a/packages/components/nodes/documentloaders/Figma/figma.svg +++ b/packages/components/nodes/documentloaders/Figma/figma.svg @@ -1 +1,7 @@ - \ No newline at end of file + + + + + + + diff --git a/packages/components/nodes/documentloaders/Folder/folder.svg b/packages/components/nodes/documentloaders/Folder/folder.svg index eb2b9de9..d26c6b69 100644 --- a/packages/components/nodes/documentloaders/Folder/folder.svg +++ b/packages/components/nodes/documentloaders/Folder/folder.svg @@ -1,4 +1,3 @@ - - - - \ No newline at end of file + + + diff --git a/packages/components/nodes/documentloaders/Gitbook/gitbook.svg b/packages/components/nodes/documentloaders/Gitbook/gitbook.svg index df16237a..a970c8b1 100644 --- a/packages/components/nodes/documentloaders/Gitbook/gitbook.svg +++ b/packages/components/nodes/documentloaders/Gitbook/gitbook.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + diff --git a/packages/components/nodes/documentloaders/Github/Github.ts b/packages/components/nodes/documentloaders/Github/Github.ts index 6495e8e9..935b9762 100644 --- a/packages/components/nodes/documentloaders/Github/Github.ts +++ b/packages/components/nodes/documentloaders/Github/Github.ts @@ -20,7 +20,7 @@ class Github_DocumentLoaders implements INode { this.name = 'github' this.version = 2.0 this.type = 'Document' - this.icon = 'github.png' + this.icon = 'github.svg' this.category = 'Document Loaders' this.description = `Load data from a GitHub repository` this.baseClasses = [this.type] diff --git a/packages/components/nodes/documentloaders/Github/github.png b/packages/components/nodes/documentloaders/Github/github.png deleted file mode 100644 index e4400818..00000000 Binary files a/packages/components/nodes/documentloaders/Github/github.png and /dev/null differ diff --git a/packages/components/nodes/documentloaders/Github/github.svg b/packages/components/nodes/documentloaders/Github/github.svg new file mode 100644 index 00000000..01f228d1 --- /dev/null +++ b/packages/components/nodes/documentloaders/Github/github.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/packages/components/nodes/documentloaders/Json/json.svg b/packages/components/nodes/documentloaders/Json/json.svg index c27646e2..2581cd3b 100644 --- a/packages/components/nodes/documentloaders/Json/json.svg +++ b/packages/components/nodes/documentloaders/Json/json.svg @@ -1,7 +1,7 @@ - - - - - - - \ No newline at end of file + + + + + + + diff --git a/packages/components/nodes/documentloaders/Jsonlines/jsonlines.svg b/packages/components/nodes/documentloaders/Jsonlines/jsonlines.svg index f3686f0c..7b5882f6 100644 --- a/packages/components/nodes/documentloaders/Jsonlines/jsonlines.svg +++ b/packages/components/nodes/documentloaders/Jsonlines/jsonlines.svg @@ -1,16 +1,9 @@ - - - - - background - - - - - - - Layer 1 - JSON - Lines - - \ No newline at end of file + + + + + + + + + diff --git a/packages/components/nodes/documentloaders/Notion/NotionDB.ts b/packages/components/nodes/documentloaders/Notion/NotionDB.ts index 74879dd2..4e37ad22 100644 --- a/packages/components/nodes/documentloaders/Notion/NotionDB.ts +++ b/packages/components/nodes/documentloaders/Notion/NotionDB.ts @@ -20,7 +20,7 @@ class NotionDB_DocumentLoaders implements INode { this.name = 'notionDB' this.version = 1.0 this.type = 'Document' - this.icon = 'notion.png' + this.icon = 'notion-db.svg' this.category = 'Document Loaders' this.description = 'Load data from Notion Database (each row is a separate document with all properties as metadata)' this.baseClasses = [this.type] @@ -66,6 +66,10 @@ class NotionDB_DocumentLoaders implements INode { auth: notionIntegrationToken }, id: databaseId, + callerOptions: { + maxConcurrency: 64 // Default value + }, + propertiesAsHeader: true, // Prepends a front matter header of the page properties to the page contents type: 'database' } const loader = new NotionAPILoader(obj) diff --git a/packages/components/nodes/documentloaders/Notion/NotionFolder.ts b/packages/components/nodes/documentloaders/Notion/NotionFolder.ts index 8b8254a4..7da8d4a0 100644 --- a/packages/components/nodes/documentloaders/Notion/NotionFolder.ts +++ b/packages/components/nodes/documentloaders/Notion/NotionFolder.ts @@ -18,7 +18,7 @@ class NotionFolder_DocumentLoaders implements INode { this.name = 'notionFolder' this.version = 1.0 this.type = 'Document' - this.icon = 'notion.png' + this.icon = 'notion-folder.svg' this.category = 'Document Loaders' this.description = 'Load data from the exported and unzipped Notion folder' this.baseClasses = [this.type] diff --git a/packages/components/nodes/documentloaders/Notion/NotionPage.ts b/packages/components/nodes/documentloaders/Notion/NotionPage.ts index b45067ab..1ea3e483 100644 --- a/packages/components/nodes/documentloaders/Notion/NotionPage.ts +++ b/packages/components/nodes/documentloaders/Notion/NotionPage.ts @@ -20,7 +20,7 @@ class NotionPage_DocumentLoaders implements INode { this.name = 'notionPage' this.version = 1.0 this.type = 'Document' - this.icon = 'notion.png' + this.icon = 'notion-page.svg' this.category = 'Document Loaders' this.description = 'Load data from Notion Page (including child pages all as separate documents)' this.baseClasses = [this.type] diff --git a/packages/components/nodes/documentloaders/Notion/notion-db.svg b/packages/components/nodes/documentloaders/Notion/notion-db.svg new file mode 100644 index 00000000..9a04380f --- /dev/null +++ b/packages/components/nodes/documentloaders/Notion/notion-db.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/components/nodes/documentloaders/Notion/notion-folder.svg b/packages/components/nodes/documentloaders/Notion/notion-folder.svg new file mode 100644 index 00000000..8fafe569 --- /dev/null +++ b/packages/components/nodes/documentloaders/Notion/notion-folder.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/components/nodes/documentloaders/Notion/notion-page.svg b/packages/components/nodes/documentloaders/Notion/notion-page.svg new file mode 100644 index 00000000..201dcd3b --- /dev/null +++ b/packages/components/nodes/documentloaders/Notion/notion-page.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/packages/components/nodes/documentloaders/Notion/notion.png b/packages/components/nodes/documentloaders/Notion/notion.png deleted file mode 100644 index 39105167..00000000 Binary files a/packages/components/nodes/documentloaders/Notion/notion.png and /dev/null differ diff --git a/packages/components/nodes/documentloaders/Pdf/pdf.svg b/packages/components/nodes/documentloaders/Pdf/pdf.svg index 20af94f8..5042b227 100644 --- a/packages/components/nodes/documentloaders/Pdf/pdf.svg +++ b/packages/components/nodes/documentloaders/Pdf/pdf.svg @@ -1,7 +1,6 @@ - - - - - - - \ No newline at end of file + + + + + + diff --git a/packages/components/nodes/documentloaders/PlainText/plaintext.svg b/packages/components/nodes/documentloaders/PlainText/plaintext.svg index b9fec035..8f38ca6b 100644 --- a/packages/components/nodes/documentloaders/PlainText/plaintext.svg +++ b/packages/components/nodes/documentloaders/PlainText/plaintext.svg @@ -1,7 +1,4 @@ - - - - - - - \ No newline at end of file + + + + diff --git a/packages/components/nodes/documentloaders/Playwright/playwright.svg b/packages/components/nodes/documentloaders/Playwright/playwright.svg index 0992832d..bbfd8120 100644 --- a/packages/components/nodes/documentloaders/Playwright/playwright.svg +++ b/packages/components/nodes/documentloaders/Playwright/playwright.svg @@ -1,9 +1,9 @@ - - - - - - - - - \ No newline at end of file + + + + + + + + + diff --git a/packages/components/nodes/documentloaders/Puppeteer/puppeteer.svg b/packages/components/nodes/documentloaders/Puppeteer/puppeteer.svg index 8477fc52..0e394fb7 100644 --- a/packages/components/nodes/documentloaders/Puppeteer/puppeteer.svg +++ b/packages/components/nodes/documentloaders/Puppeteer/puppeteer.svg @@ -1,14 +1,7 @@ - - - - - - - - \ 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 07295aba..eadb4d99 100644 --- a/packages/components/nodes/documentloaders/S3File/S3File.ts +++ b/packages/components/nodes/documentloaders/S3File/S3File.ts @@ -30,7 +30,7 @@ class S3_DocumentLoaders implements INode { constructor() { this.label = 'S3' this.name = 'S3' - this.version = 1.0 + this.version = 2.0 this.type = 'Document' this.icon = 's3.svg' this.category = 'Document Loaders' @@ -113,12 +113,62 @@ class S3_DocumentLoaders implements INode { optional: true }, { - label: 'NarrativeText Only', - name: 'narrativeTextOnly', + label: 'Element Type', + name: 'elementType', description: - 'Only load documents with NarrativeText metadata from Unstructured. See how Unstructured partition data here', - default: true, - type: 'boolean', + 'Unstructured partition document into different types, select the types to return. If not selected, all types will be returned', + type: 'multiOptions', + options: [ + { + label: 'FigureCaption', + name: 'FigureCaption' + }, + { + label: 'NarrativeText', + name: 'NarrativeText' + }, + { + label: 'ListItem', + name: 'ListItem' + }, + { + label: 'Title', + name: 'Title' + }, + { + label: 'Address', + name: 'Address' + }, + { + label: 'Table', + name: 'Table' + }, + { + label: 'PageBreak', + name: 'PageBreak' + }, + { + label: 'Header', + name: 'Header' + }, + { + label: 'Footer', + name: 'Footer' + }, + { + label: 'UncategorizedText', + name: 'UncategorizedText' + }, + { + label: 'Image', + name: 'Image' + }, + { + label: 'Formula', + name: 'Formula' + } + ], + default: [], optional: true, additionalParams: true }, @@ -138,7 +188,7 @@ class S3_DocumentLoaders implements INode { const unstructuredAPIUrl = nodeData.inputs?.unstructuredAPIUrl as string const unstructuredAPIKey = nodeData.inputs?.unstructuredAPIKey as string const metadata = nodeData.inputs?.metadata - const narrativeTextOnly = nodeData.inputs?.narrativeTextOnly as boolean + const elementType = nodeData.inputs?.elementType as string const credentialData = await getCredentialData(nodeData.credential ?? '', options) const accessKeyId = getCredentialParam('awsKey', credentialData, nodeData) @@ -162,8 +212,20 @@ class S3_DocumentLoaders implements INode { accessKeyId?: string secretAccessKey?: string } = { - accessKeyId, - secretAccessKey + region, + credentials: { + accessKeyId, + secretAccessKey + } + } + + let elementTypes: string[] = [] + if (elementType) { + try { + elementTypes = JSON.parse(elementType) + } catch (e) { + elementTypes = [] + } } loader.load = async () => { @@ -232,10 +294,10 @@ class S3_DocumentLoaders implements INode { } } }) - return narrativeTextOnly ? finaldocs.filter((doc) => doc.metadata.category === 'NarrativeText') : finaldocs + return elementTypes.length ? finaldocs.filter((doc) => elementTypes.includes(doc.metadata.category)) : finaldocs } - return narrativeTextOnly ? docs.filter((doc) => doc.metadata.category === 'NarrativeText') : docs + return elementTypes.length ? docs.filter((doc) => elementTypes.includes(doc.metadata.category)) : docs } } module.exports = { nodeClass: S3_DocumentLoaders } diff --git a/packages/components/nodes/documentloaders/S3File/s3.svg b/packages/components/nodes/documentloaders/S3File/s3.svg index cd203eaa..33948d2f 100644 --- a/packages/components/nodes/documentloaders/S3File/s3.svg +++ b/packages/components/nodes/documentloaders/S3File/s3.svg @@ -1 +1,5 @@ - \ No newline at end of file + + + + + diff --git a/packages/components/nodes/documentloaders/SearchApi/searchapi.svg b/packages/components/nodes/documentloaders/SearchApi/searchapi.svg index c44c29c4..0c566712 100644 --- a/packages/components/nodes/documentloaders/SearchApi/searchapi.svg +++ b/packages/components/nodes/documentloaders/SearchApi/searchapi.svg @@ -1 +1,8 @@ -0479_octopus_verti + + + + + + + + diff --git a/packages/components/nodes/documentloaders/SerpApi/SerpAPI.ts b/packages/components/nodes/documentloaders/SerpApi/SerpAPI.ts index fd482710..c46db6b0 100644 --- a/packages/components/nodes/documentloaders/SerpApi/SerpAPI.ts +++ b/packages/components/nodes/documentloaders/SerpApi/SerpAPI.ts @@ -20,7 +20,7 @@ class SerpAPI_DocumentLoaders implements INode { this.name = 'serpApi' this.version = 1.0 this.type = 'Document' - this.icon = 'serp.png' + this.icon = 'serp.svg' this.category = 'Document Loaders' this.description = 'Load and process data from web search results' this.baseClasses = [this.type] diff --git a/packages/components/nodes/documentloaders/SerpApi/serp.png b/packages/components/nodes/documentloaders/SerpApi/serp.png deleted file mode 100644 index 338aeaea..00000000 Binary files a/packages/components/nodes/documentloaders/SerpApi/serp.png and /dev/null differ diff --git a/packages/components/nodes/documentloaders/SerpApi/serp.svg b/packages/components/nodes/documentloaders/SerpApi/serp.svg new file mode 100644 index 00000000..04999b54 --- /dev/null +++ b/packages/components/nodes/documentloaders/SerpApi/serp.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/packages/components/nodes/documentloaders/Subtitles/subtitlesFile.svg b/packages/components/nodes/documentloaders/Subtitles/subtitlesFile.svg index a6ee925b..251f51e4 100644 --- a/packages/components/nodes/documentloaders/Subtitles/subtitlesFile.svg +++ b/packages/components/nodes/documentloaders/Subtitles/subtitlesFile.svg @@ -1 +1,6 @@ - \ No newline at end of file + + + + + + diff --git a/packages/components/nodes/documentloaders/Text/Text.ts b/packages/components/nodes/documentloaders/Text/Text.ts index 3f12e490..e41c5a9f 100644 --- a/packages/components/nodes/documentloaders/Text/Text.ts +++ b/packages/components/nodes/documentloaders/Text/Text.ts @@ -21,7 +21,7 @@ class Text_DocumentLoaders implements INode { this.name = 'textFile' this.version = 3.0 this.type = 'Document' - this.icon = 'textFile.svg' + this.icon = 'Txt.svg' this.category = 'Document Loaders' this.description = `Load data from text files` this.baseClasses = [this.type] diff --git a/packages/components/nodes/documentloaders/Text/Txt.svg b/packages/components/nodes/documentloaders/Text/Txt.svg new file mode 100644 index 00000000..e361c0c5 --- /dev/null +++ b/packages/components/nodes/documentloaders/Text/Txt.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/components/nodes/documentloaders/Text/textFile.svg b/packages/components/nodes/documentloaders/Text/textFile.svg deleted file mode 100644 index 200be563..00000000 --- a/packages/components/nodes/documentloaders/Text/textFile.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/packages/components/nodes/documentloaders/Unstructured/UnstructuredFile.ts b/packages/components/nodes/documentloaders/Unstructured/UnstructuredFile.ts index 3ee7ff73..d4de1ece 100644 --- a/packages/components/nodes/documentloaders/Unstructured/UnstructuredFile.ts +++ b/packages/components/nodes/documentloaders/Unstructured/UnstructuredFile.ts @@ -19,7 +19,7 @@ class UnstructuredFile_DocumentLoaders implements INode { this.name = 'unstructuredFileLoader' this.version = 1.0 this.type = 'Document' - this.icon = 'unstructured.png' + this.icon = 'unstructured-file.svg' this.category = 'Document Loaders' this.description = 'Use Unstructured.io to load data from a file path' this.baseClasses = [this.type] diff --git a/packages/components/nodes/documentloaders/Unstructured/UnstructuredFolder.ts b/packages/components/nodes/documentloaders/Unstructured/UnstructuredFolder.ts index c56ff023..a0e7ee6c 100644 --- a/packages/components/nodes/documentloaders/Unstructured/UnstructuredFolder.ts +++ b/packages/components/nodes/documentloaders/Unstructured/UnstructuredFolder.ts @@ -19,7 +19,7 @@ class UnstructuredFolder_DocumentLoaders implements INode { this.name = 'unstructuredFolderLoader' this.version = 1.0 this.type = 'Document' - this.icon = 'unstructured.png' + this.icon = 'unstructured-folder.svg' this.category = 'Document Loaders' this.description = 'Use Unstructured.io to load data from a folder' this.baseClasses = [this.type] diff --git a/packages/components/nodes/documentloaders/Unstructured/unstructured-file.svg b/packages/components/nodes/documentloaders/Unstructured/unstructured-file.svg new file mode 100644 index 00000000..88ec620a --- /dev/null +++ b/packages/components/nodes/documentloaders/Unstructured/unstructured-file.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/packages/components/nodes/documentloaders/Unstructured/unstructured-folder.svg b/packages/components/nodes/documentloaders/Unstructured/unstructured-folder.svg new file mode 100644 index 00000000..8ffd51ae --- /dev/null +++ b/packages/components/nodes/documentloaders/Unstructured/unstructured-folder.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/components/nodes/documentloaders/Unstructured/unstructured.png b/packages/components/nodes/documentloaders/Unstructured/unstructured.png deleted file mode 100644 index 435219bf..00000000 Binary files a/packages/components/nodes/documentloaders/Unstructured/unstructured.png and /dev/null differ diff --git a/packages/components/nodes/documentloaders/VectorStoreToDocument/vectorretriever.svg b/packages/components/nodes/documentloaders/VectorStoreToDocument/vectorretriever.svg index 208a59f1..0c8cd296 100644 --- a/packages/components/nodes/documentloaders/VectorStoreToDocument/vectorretriever.svg +++ b/packages/components/nodes/documentloaders/VectorStoreToDocument/vectorretriever.svg @@ -1,7 +1,4 @@ - - - - - - - \ No newline at end of file + + + + diff --git a/packages/components/nodes/embeddings/AWSBedrockEmbedding/AWSBedrockEmbedding.ts b/packages/components/nodes/embeddings/AWSBedrockEmbedding/AWSBedrockEmbedding.ts index 8249d512..d7445c30 100644 --- a/packages/components/nodes/embeddings/AWSBedrockEmbedding/AWSBedrockEmbedding.ts +++ b/packages/components/nodes/embeddings/AWSBedrockEmbedding/AWSBedrockEmbedding.ts @@ -18,9 +18,9 @@ class AWSBedrockEmbedding_Embeddings implements INode { constructor() { this.label = 'AWS Bedrock Embeddings' this.name = 'AWSBedrockEmbeddings' - this.version = 2.0 + this.version = 3.0 this.type = 'AWSBedrockEmbeddings' - this.icon = 'awsBedrock.png' + this.icon = 'aws.svg' this.category = 'Embeddings' this.description = 'AWSBedrock embedding models to generate embeddings for a given text' this.baseClasses = [this.type, ...getBaseClasses(BedrockEmbeddings)] @@ -86,6 +86,13 @@ class AWSBedrockEmbedding_Embeddings implements INode { { label: 'cohere.embed-multilingual-v3', name: 'cohere.embed-multilingual-v3' } ], default: 'amazon.titan-embed-text-v1' + }, + { + label: 'Custom Model Name', + name: 'customModel', + description: 'If provided, will override model selected from Model Name option', + type: 'string', + optional: true } ] } @@ -93,9 +100,10 @@ class AWSBedrockEmbedding_Embeddings implements INode { async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { const iRegion = nodeData.inputs?.region as string const iModel = nodeData.inputs?.model as string + const customModel = nodeData.inputs?.customModel as string const obj: BedrockEmbeddingsParams = { - model: iModel, + model: customModel ? customModel : iModel, region: iRegion } diff --git a/packages/components/nodes/embeddings/AWSBedrockEmbedding/aws.svg b/packages/components/nodes/embeddings/AWSBedrockEmbedding/aws.svg new file mode 100644 index 00000000..d783497e --- /dev/null +++ b/packages/components/nodes/embeddings/AWSBedrockEmbedding/aws.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/embeddings/AWSBedrockEmbedding/awsBedrock.png b/packages/components/nodes/embeddings/AWSBedrockEmbedding/awsBedrock.png deleted file mode 100644 index 483bc69a..00000000 Binary files a/packages/components/nodes/embeddings/AWSBedrockEmbedding/awsBedrock.png and /dev/null differ diff --git a/packages/components/nodes/embeddings/AzureOpenAIEmbedding/Azure.svg b/packages/components/nodes/embeddings/AzureOpenAIEmbedding/Azure.svg index 47ad8c44..7b150811 100644 --- a/packages/components/nodes/embeddings/AzureOpenAIEmbedding/Azure.svg +++ b/packages/components/nodes/embeddings/AzureOpenAIEmbedding/Azure.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/packages/components/nodes/embeddings/AzureOpenAIEmbedding/AzureOpenAIEmbedding_LlamaIndex.ts b/packages/components/nodes/embeddings/AzureOpenAIEmbedding/AzureOpenAIEmbedding_LlamaIndex.ts index 38e45402..92f320be 100644 --- a/packages/components/nodes/embeddings/AzureOpenAIEmbedding/AzureOpenAIEmbedding_LlamaIndex.ts +++ b/packages/components/nodes/embeddings/AzureOpenAIEmbedding/AzureOpenAIEmbedding_LlamaIndex.ts @@ -29,7 +29,7 @@ class AzureOpenAIEmbedding_LlamaIndex_Embeddings implements INode { this.type = 'AzureOpenAIEmbeddings' this.icon = 'Azure.svg' this.category = 'Embeddings' - this.description = 'Azure OpenAI API embeddings with LlamaIndex implementation' + this.description = 'Azure OpenAI API embeddings specific for LlamaIndex' this.baseClasses = [this.type, 'BaseEmbedding_LlamaIndex', ...getBaseClasses(OpenAIEmbedding)] this.tags = ['LlamaIndex'] this.credential = { diff --git a/packages/components/nodes/embeddings/CohereEmbedding/Cohere.svg b/packages/components/nodes/embeddings/CohereEmbedding/Cohere.svg new file mode 100644 index 00000000..88bcabe3 --- /dev/null +++ b/packages/components/nodes/embeddings/CohereEmbedding/Cohere.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/embeddings/CohereEmbedding/CohereEmbedding.ts b/packages/components/nodes/embeddings/CohereEmbedding/CohereEmbedding.ts index b42a0357..92d0fe7d 100644 --- a/packages/components/nodes/embeddings/CohereEmbedding/CohereEmbedding.ts +++ b/packages/components/nodes/embeddings/CohereEmbedding/CohereEmbedding.ts @@ -19,7 +19,7 @@ class CohereEmbedding_Embeddings implements INode { this.name = 'cohereEmbeddings' this.version = 1.0 this.type = 'CohereEmbeddings' - this.icon = 'cohere.png' + this.icon = 'Cohere.svg' this.category = 'Embeddings' this.description = 'Cohere API to generate embeddings for a given text' this.baseClasses = [this.type, ...getBaseClasses(CohereEmbeddings)] diff --git a/packages/components/nodes/embeddings/CohereEmbedding/cohere.png b/packages/components/nodes/embeddings/CohereEmbedding/cohere.png deleted file mode 100644 index 266adeac..00000000 Binary files a/packages/components/nodes/embeddings/CohereEmbedding/cohere.png and /dev/null differ diff --git a/packages/components/nodes/embeddings/GoogleGenerativeAIEmbedding/GoogleGemini.svg b/packages/components/nodes/embeddings/GoogleGenerativeAIEmbedding/GoogleGemini.svg new file mode 100644 index 00000000..53b497fa --- /dev/null +++ b/packages/components/nodes/embeddings/GoogleGenerativeAIEmbedding/GoogleGemini.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/components/nodes/embeddings/GoogleGenerativeAIEmbedding/GoogleGenerativeAIEmbedding.ts b/packages/components/nodes/embeddings/GoogleGenerativeAIEmbedding/GoogleGenerativeAIEmbedding.ts new file mode 100644 index 00000000..ac84fd27 --- /dev/null +++ b/packages/components/nodes/embeddings/GoogleGenerativeAIEmbedding/GoogleGenerativeAIEmbedding.ts @@ -0,0 +1,104 @@ +import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' +import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' +import { GoogleGenerativeAIEmbeddings, GoogleGenerativeAIEmbeddingsParams } from '@langchain/google-genai' +import { TaskType } from '@google/generative-ai' + +class GoogleGenerativeAIEmbedding_Embeddings implements INode { + label: string + name: string + version: number + type: string + icon: string + category: string + description: string + baseClasses: string[] + inputs: INodeParams[] + credential: INodeParams + + constructor() { + this.label = 'GoogleGenerativeAI Embeddings' + this.name = 'googleGenerativeAiEmbeddings' + this.version = 1.0 + this.type = 'GoogleGenerativeAiEmbeddings' + this.icon = 'GoogleGemini.svg' + this.category = 'Embeddings' + this.description = 'Google Generative API to generate embeddings for a given text' + this.baseClasses = [this.type, ...getBaseClasses(GoogleGenerativeAIEmbeddings)] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['googleGenerativeAI'], + optional: false, + description: 'Google Generative AI credential.' + } + this.inputs = [ + { + label: 'Model Name', + name: 'modelName', + type: 'options', + options: [ + { + label: 'embedding-001', + name: 'embedding-001' + } + ], + default: 'embedding-001' + }, + { + label: 'Task Type', + name: 'tasktype', + type: 'options', + description: 'Type of task for which the embedding will be used', + options: [ + { label: 'TASK_TYPE_UNSPECIFIED', name: 'TASK_TYPE_UNSPECIFIED' }, + { label: 'RETRIEVAL_QUERY', name: 'RETRIEVAL_QUERY' }, + { label: 'RETRIEVAL_DOCUMENT', name: 'RETRIEVAL_DOCUMENT' }, + { label: 'SEMANTIC_SIMILARITY', name: 'SEMANTIC_SIMILARITY' }, + { label: 'CLASSIFICATION', name: 'CLASSIFICATION' }, + { label: 'CLUSTERING', name: 'CLUSTERING' } + ], + default: 'TASK_TYPE_UNSPECIFIED' + } + ] + } + + // eslint-disable-next-line unused-imports/no-unused-vars + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + const modelName = nodeData.inputs?.modelName as string + const credentialData = await getCredentialData(nodeData.credential ?? '', options) + const apiKey = getCredentialParam('googleGenerativeAPIKey', credentialData, nodeData) + + let taskType: TaskType + switch (nodeData.inputs?.tasktype as string) { + case 'RETRIEVAL_QUERY': + taskType = TaskType.RETRIEVAL_QUERY + break + case 'RETRIEVAL_DOCUMENT': + taskType = TaskType.RETRIEVAL_DOCUMENT + break + case 'SEMANTIC_SIMILARITY': + taskType = TaskType.SEMANTIC_SIMILARITY + break + case 'CLASSIFICATION': + taskType = TaskType.CLASSIFICATION + break + case 'CLUSTERING': + taskType = TaskType.CLUSTERING + break + default: + taskType = TaskType.TASK_TYPE_UNSPECIFIED + break + } + const obj: GoogleGenerativeAIEmbeddingsParams = { + apiKey: apiKey, + modelName: modelName, + taskType: taskType + } + + const model = new GoogleGenerativeAIEmbeddings(obj) + return model + } +} + +module.exports = { nodeClass: GoogleGenerativeAIEmbedding_Embeddings } diff --git a/packages/components/nodes/embeddings/GooglePaLMEmbedding/GooglePaLM.svg b/packages/components/nodes/embeddings/GooglePaLMEmbedding/GooglePaLM.svg new file mode 100644 index 00000000..ed47326a --- /dev/null +++ b/packages/components/nodes/embeddings/GooglePaLMEmbedding/GooglePaLM.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/embeddings/GooglePaLMEmbedding/GooglePaLMEmbedding.ts b/packages/components/nodes/embeddings/GooglePaLMEmbedding/GooglePaLMEmbedding.ts index 81507d00..d003a928 100644 --- a/packages/components/nodes/embeddings/GooglePaLMEmbedding/GooglePaLMEmbedding.ts +++ b/packages/components/nodes/embeddings/GooglePaLMEmbedding/GooglePaLMEmbedding.ts @@ -19,7 +19,7 @@ class GooglePaLMEmbedding_Embeddings implements INode { this.name = 'googlePaLMEmbeddings' this.version = 1.0 this.type = 'GooglePaLMEmbeddings' - this.icon = 'Google_PaLM_Logo.svg' + this.icon = 'GooglePaLM.svg' this.category = 'Embeddings' this.description = 'Google MakerSuite PaLM API to generate embeddings for a given text' this.baseClasses = [this.type, ...getBaseClasses(GooglePaLMEmbeddings)] diff --git a/packages/components/nodes/embeddings/GooglePaLMEmbedding/Google_PaLM_Logo.svg b/packages/components/nodes/embeddings/GooglePaLMEmbedding/Google_PaLM_Logo.svg deleted file mode 100644 index 5c345fe1..00000000 --- a/packages/components/nodes/embeddings/GooglePaLMEmbedding/Google_PaLM_Logo.svg +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/components/nodes/embeddings/GoogleVertexAIEmbedding/GoogleVertex.svg b/packages/components/nodes/embeddings/GoogleVertexAIEmbedding/GoogleVertex.svg new file mode 100644 index 00000000..a517740f --- /dev/null +++ b/packages/components/nodes/embeddings/GoogleVertexAIEmbedding/GoogleVertex.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/embeddings/GoogleVertexAIEmbedding/GoogleVertexAIEmbedding.ts b/packages/components/nodes/embeddings/GoogleVertexAIEmbedding/GoogleVertexAIEmbedding.ts index 7d086e0c..e60f688d 100644 --- a/packages/components/nodes/embeddings/GoogleVertexAIEmbedding/GoogleVertexAIEmbedding.ts +++ b/packages/components/nodes/embeddings/GoogleVertexAIEmbedding/GoogleVertexAIEmbedding.ts @@ -20,7 +20,7 @@ class GoogleVertexAIEmbedding_Embeddings implements INode { this.name = 'googlevertexaiEmbeddings' this.version = 1.0 this.type = 'GoogleVertexAIEmbeddings' - this.icon = 'vertexai.svg' + this.icon = 'GoogleVertex.svg' this.category = 'Embeddings' this.description = 'Google vertexAI API to generate embeddings for a given text' this.baseClasses = [this.type, ...getBaseClasses(GoogleVertexAIEmbeddings)] diff --git a/packages/components/nodes/embeddings/GoogleVertexAIEmbedding/vertexai.svg b/packages/components/nodes/embeddings/GoogleVertexAIEmbedding/vertexai.svg deleted file mode 100644 index 31244412..00000000 --- a/packages/components/nodes/embeddings/GoogleVertexAIEmbedding/vertexai.svg +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/packages/components/nodes/embeddings/HuggingFaceInferenceEmbedding/HuggingFace.svg b/packages/components/nodes/embeddings/HuggingFaceInferenceEmbedding/HuggingFace.svg new file mode 100644 index 00000000..58c85d57 --- /dev/null +++ b/packages/components/nodes/embeddings/HuggingFaceInferenceEmbedding/HuggingFace.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/embeddings/HuggingFaceInferenceEmbedding/HuggingFaceInferenceEmbedding.ts b/packages/components/nodes/embeddings/HuggingFaceInferenceEmbedding/HuggingFaceInferenceEmbedding.ts index 6d75b955..5768f1d9 100644 --- a/packages/components/nodes/embeddings/HuggingFaceInferenceEmbedding/HuggingFaceInferenceEmbedding.ts +++ b/packages/components/nodes/embeddings/HuggingFaceInferenceEmbedding/HuggingFaceInferenceEmbedding.ts @@ -19,7 +19,7 @@ class HuggingFaceInferenceEmbedding_Embeddings implements INode { this.name = 'huggingFaceInferenceEmbeddings' this.version = 1.0 this.type = 'HuggingFaceInferenceEmbeddings' - this.icon = 'huggingface.png' + this.icon = 'HuggingFace.svg' this.category = 'Embeddings' this.description = 'HuggingFace Inference API to generate embeddings for a given text' this.baseClasses = [this.type, ...getBaseClasses(HuggingFaceInferenceEmbeddings)] diff --git a/packages/components/nodes/embeddings/HuggingFaceInferenceEmbedding/huggingface.png b/packages/components/nodes/embeddings/HuggingFaceInferenceEmbedding/huggingface.png deleted file mode 100644 index f8f202a4..00000000 Binary files a/packages/components/nodes/embeddings/HuggingFaceInferenceEmbedding/huggingface.png and /dev/null differ diff --git a/packages/components/nodes/embeddings/LocalAIEmbedding/LocalAIEmbedding.ts b/packages/components/nodes/embeddings/LocalAIEmbedding/LocalAIEmbedding.ts index 557e35d6..24efaf8c 100644 --- a/packages/components/nodes/embeddings/LocalAIEmbedding/LocalAIEmbedding.ts +++ b/packages/components/nodes/embeddings/LocalAIEmbedding/LocalAIEmbedding.ts @@ -1,4 +1,5 @@ -import { INode, INodeData, INodeParams } from '../../../src/Interface' +import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' +import { getCredentialData, getCredentialParam } from '../../../src/utils' import { OpenAIEmbeddings, OpenAIEmbeddingsParams } from 'langchain/embeddings/openai' class LocalAIEmbedding_Embeddings implements INode { @@ -10,6 +11,7 @@ class LocalAIEmbedding_Embeddings implements INode { category: string description: string baseClasses: string[] + credential: INodeParams inputs: INodeParams[] constructor() { @@ -21,6 +23,13 @@ class LocalAIEmbedding_Embeddings implements INode { this.category = 'Embeddings' this.description = 'Use local embeddings models like llama.cpp' this.baseClasses = [this.type, 'Embeddings'] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['localAIApi'], + optional: true + } this.inputs = [ { label: 'Base Path', @@ -37,15 +46,20 @@ class LocalAIEmbedding_Embeddings implements INode { ] } - async init(nodeData: INodeData): Promise { + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { const modelName = nodeData.inputs?.modelName as string const basePath = nodeData.inputs?.basePath as string + const credentialData = await getCredentialData(nodeData.credential ?? '', options) + const localAIApiKey = getCredentialParam('localAIApiKey', credentialData, nodeData) + const obj: Partial & { openAIApiKey?: string } = { modelName, openAIApiKey: 'sk-' } + if (localAIApiKey) obj.openAIApiKey = localAIApiKey + const model = new OpenAIEmbeddings(obj, { basePath }) return model diff --git a/packages/components/nodes/embeddings/MistralEmbedding/MistralAI.svg b/packages/components/nodes/embeddings/MistralEmbedding/MistralAI.svg new file mode 100644 index 00000000..aa84b39c --- /dev/null +++ b/packages/components/nodes/embeddings/MistralEmbedding/MistralAI.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/embeddings/MistralEmbedding/MistralEmbedding.ts b/packages/components/nodes/embeddings/MistralEmbedding/MistralEmbedding.ts new file mode 100644 index 00000000..9ad63533 --- /dev/null +++ b/packages/components/nodes/embeddings/MistralEmbedding/MistralEmbedding.ts @@ -0,0 +1,95 @@ +import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' +import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' +import { MistralAIEmbeddings, MistralAIEmbeddingsParams } from '@langchain/mistralai' + +class MistralEmbedding_Embeddings implements INode { + label: string + name: string + version: number + type: string + icon: string + category: string + description: string + baseClasses: string[] + inputs: INodeParams[] + credential: INodeParams + + constructor() { + this.label = 'MistralAI Embeddings' + this.name = 'mistralAI Embeddings' + this.version = 1.0 + this.type = 'MistralAIEmbeddings' + this.icon = 'MistralAI.svg' + this.category = 'Embeddings' + this.description = 'MistralAI API to generate embeddings for a given text' + this.baseClasses = [this.type, ...getBaseClasses(MistralAIEmbeddings)] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['mistralAIApi'] + } + this.inputs = [ + { + label: 'Model Name', + name: 'modelName', + type: 'options', + options: [ + { + label: 'mistral-embed', + name: 'mistral-embed' + } + ], + default: 'mistral-embed' + }, + { + label: 'Batch Size', + name: 'batchSize', + type: 'number', + step: 1, + default: 512, + optional: true, + additionalParams: true + }, + { + label: 'Strip New Lines', + name: 'stripNewLines', + type: 'boolean', + default: true, + optional: true, + additionalParams: true + }, + { + label: 'Override Endpoint', + name: 'overrideEndpoint', + type: 'string', + optional: true, + additionalParams: true + } + ] + } + + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + const modelName = nodeData.inputs?.modelName as string + const batchSize = nodeData.inputs?.batchSize as string + const stripNewLines = nodeData.inputs?.stripNewLines as boolean + const overrideEndpoint = nodeData.inputs?.overrideEndpoint as string + + const credentialData = await getCredentialData(nodeData.credential ?? '', options) + const apiKey = getCredentialParam('mistralAIAPIKey', credentialData, nodeData) + + const obj: MistralAIEmbeddingsParams = { + apiKey: apiKey, + modelName: modelName + } + + if (batchSize) obj.batchSize = parseInt(batchSize, 10) + if (stripNewLines) obj.stripNewLines = stripNewLines + if (overrideEndpoint) obj.endpoint = overrideEndpoint + + const model = new MistralAIEmbeddings(obj) + return model + } +} + +module.exports = { nodeClass: MistralEmbedding_Embeddings } diff --git a/packages/components/nodes/embeddings/OllamaEmbedding/Ollama.svg b/packages/components/nodes/embeddings/OllamaEmbedding/Ollama.svg new file mode 100644 index 00000000..2dc8df53 --- /dev/null +++ b/packages/components/nodes/embeddings/OllamaEmbedding/Ollama.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/embeddings/OllamaEmbedding/OllamaEmbedding.ts b/packages/components/nodes/embeddings/OllamaEmbedding/OllamaEmbedding.ts index eb528aff..8892b03f 100644 --- a/packages/components/nodes/embeddings/OllamaEmbedding/OllamaEmbedding.ts +++ b/packages/components/nodes/embeddings/OllamaEmbedding/OllamaEmbedding.ts @@ -1,7 +1,7 @@ import { INode, INodeData, INodeParams } from '../../../src/Interface' import { getBaseClasses } from '../../../src/utils' +import { OllamaInput } from 'langchain/llms/ollama' import { OllamaEmbeddings } from 'langchain/embeddings/ollama' -import { OllamaInput } from 'langchain/dist/util/ollama' class OllamaEmbedding_Embeddings implements INode { label: string @@ -20,7 +20,7 @@ class OllamaEmbedding_Embeddings implements INode { this.name = 'ollamaEmbedding' this.version = 1.0 this.type = 'OllamaEmbeddings' - this.icon = 'ollama.png' + this.icon = 'Ollama.svg' this.category = 'Embeddings' this.description = 'Generate embeddings for a given text using open source model on Ollama' this.baseClasses = [this.type, ...getBaseClasses(OllamaEmbeddings)] diff --git a/packages/components/nodes/embeddings/OllamaEmbedding/ollama.png b/packages/components/nodes/embeddings/OllamaEmbedding/ollama.png deleted file mode 100644 index 8cd2cf1e..00000000 Binary files a/packages/components/nodes/embeddings/OllamaEmbedding/ollama.png and /dev/null differ diff --git a/packages/components/nodes/embeddings/OpenAIEmbedding/OpenAIEmbedding.ts b/packages/components/nodes/embeddings/OpenAIEmbedding/OpenAIEmbedding.ts index d21b6dca..b3d0045b 100644 --- a/packages/components/nodes/embeddings/OpenAIEmbedding/OpenAIEmbedding.ts +++ b/packages/components/nodes/embeddings/OpenAIEmbedding/OpenAIEmbedding.ts @@ -19,7 +19,7 @@ class OpenAIEmbedding_Embeddings implements INode { this.name = 'openAIEmbeddings' this.version = 1.0 this.type = 'OpenAIEmbeddings' - this.icon = 'openai.png' + this.icon = 'openai.svg' this.category = 'Embeddings' this.description = 'OpenAI API to generate embeddings for a given text' this.baseClasses = [this.type, ...getBaseClasses(OpenAIEmbeddings)] diff --git a/packages/components/nodes/embeddings/OpenAIEmbedding/OpenAIEmbedding_LlamaIndex.ts b/packages/components/nodes/embeddings/OpenAIEmbedding/OpenAIEmbedding_LlamaIndex.ts index 4ff780e3..dfd6bbf5 100644 --- a/packages/components/nodes/embeddings/OpenAIEmbedding/OpenAIEmbedding_LlamaIndex.ts +++ b/packages/components/nodes/embeddings/OpenAIEmbedding/OpenAIEmbedding_LlamaIndex.ts @@ -20,9 +20,9 @@ class OpenAIEmbedding_LlamaIndex_Embeddings implements INode { this.name = 'openAIEmbedding_LlamaIndex' this.version = 1.0 this.type = 'OpenAIEmbedding' - this.icon = 'openai.png' + this.icon = 'openai.svg' this.category = 'Embeddings' - this.description = 'OpenAI Embedding with LlamaIndex implementation' + this.description = 'OpenAI Embedding specific for LlamaIndex' this.baseClasses = [this.type, 'BaseEmbedding_LlamaIndex', ...getBaseClasses(OpenAIEmbedding)] this.tags = ['LlamaIndex'] this.credential = { diff --git a/packages/components/nodes/embeddings/OpenAIEmbedding/openai.png b/packages/components/nodes/embeddings/OpenAIEmbedding/openai.png deleted file mode 100644 index de08a05b..00000000 Binary files a/packages/components/nodes/embeddings/OpenAIEmbedding/openai.png and /dev/null differ diff --git a/packages/components/nodes/embeddings/OpenAIEmbedding/openai.svg b/packages/components/nodes/embeddings/OpenAIEmbedding/openai.svg new file mode 100644 index 00000000..5c20398a --- /dev/null +++ b/packages/components/nodes/embeddings/OpenAIEmbedding/openai.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/packages/components/nodes/embeddings/OpenAIEmbeddingCustom/OpenAIEmbeddingCustom.ts b/packages/components/nodes/embeddings/OpenAIEmbeddingCustom/OpenAIEmbeddingCustom.ts index 185236b1..9bf87f6b 100644 --- a/packages/components/nodes/embeddings/OpenAIEmbeddingCustom/OpenAIEmbeddingCustom.ts +++ b/packages/components/nodes/embeddings/OpenAIEmbeddingCustom/OpenAIEmbeddingCustom.ts @@ -19,7 +19,7 @@ class OpenAIEmbeddingCustom_Embeddings implements INode { this.name = 'openAIEmbeddingsCustom' this.version = 1.0 this.type = 'OpenAIEmbeddingsCustom' - this.icon = 'openai.png' + this.icon = 'openai.svg' this.category = 'Embeddings' this.description = 'OpenAI API to generate embeddings for a given text' this.baseClasses = [this.type, ...getBaseClasses(OpenAIEmbeddings)] diff --git a/packages/components/nodes/embeddings/OpenAIEmbeddingCustom/openai.png b/packages/components/nodes/embeddings/OpenAIEmbeddingCustom/openai.png deleted file mode 100644 index de08a05b..00000000 Binary files a/packages/components/nodes/embeddings/OpenAIEmbeddingCustom/openai.png and /dev/null differ diff --git a/packages/components/nodes/embeddings/OpenAIEmbeddingCustom/openai.svg b/packages/components/nodes/embeddings/OpenAIEmbeddingCustom/openai.svg new file mode 100644 index 00000000..5c20398a --- /dev/null +++ b/packages/components/nodes/embeddings/OpenAIEmbeddingCustom/openai.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/packages/components/nodes/engine/ChatEngine/ContextChatEngine.ts b/packages/components/nodes/engine/ChatEngine/ContextChatEngine.ts index dd77601a..262ceb7c 100644 --- a/packages/components/nodes/engine/ChatEngine/ContextChatEngine.ts +++ b/packages/components/nodes/engine/ChatEngine/ContextChatEngine.ts @@ -1,5 +1,6 @@ -import { ICommonObject, IMessage, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' -import { ContextChatEngine, ChatMessage } from 'llamaindex' +import { FlowiseMemory, ICommonObject, IMessage, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' +import { BaseNode, Metadata, BaseRetriever, LLM, ContextChatEngine, ChatMessage } from 'llamaindex' +import { reformatSourceDocuments } from '../EngineUtils' class ContextChatEngine_LlamaIndex implements INode { label: string @@ -13,8 +14,9 @@ class ContextChatEngine_LlamaIndex implements INode { tags: string[] inputs: INodeParams[] outputs: INodeOutputsValue[] + sessionId?: string - constructor() { + constructor(fields?: { sessionId?: string }) { this.label = 'Context Chat Engine' this.name = 'contextChatEngine' this.version = 1.0 @@ -40,6 +42,12 @@ class ContextChatEngine_LlamaIndex implements INode { name: 'memory', type: 'BaseChatMemory' }, + { + label: 'Return Source Documents', + name: 'returnSourceDocuments', + type: 'boolean', + optional: true + }, { label: 'System Message', name: 'systemMessagePrompt', @@ -50,31 +58,22 @@ class ContextChatEngine_LlamaIndex implements INode { 'I want you to act as a document that I am having a conversation with. Your name is "AI Assistant". You will provide me with answers from the given info. If the answer is not included, say exactly "Hmm, I am not sure." and stop after that. Refuse to answer any question not about the info. Never break character.' } ] + this.sessionId = fields?.sessionId } - async init(nodeData: INodeData): Promise { - const model = nodeData.inputs?.model - const vectorStoreRetriever = nodeData.inputs?.vectorStoreRetriever - const memory = nodeData.inputs?.memory - - const chatEngine = new ContextChatEngine({ chatModel: model, retriever: vectorStoreRetriever }) - ;(chatEngine as any).memory = memory - return chatEngine + async init(): Promise { + return null } - async run(nodeData: INodeData, input: string, options: ICommonObject): Promise { - const chatEngine = nodeData.instance as ContextChatEngine + async run(nodeData: INodeData, input: string, options: ICommonObject): Promise { + const model = nodeData.inputs?.model as LLM + const vectorStoreRetriever = nodeData.inputs?.vectorStoreRetriever as BaseRetriever const systemMessagePrompt = nodeData.inputs?.systemMessagePrompt as string - const memory = nodeData.inputs?.memory + const memory = nodeData.inputs?.memory as FlowiseMemory + const returnSourceDocuments = nodeData.inputs?.returnSourceDocuments as boolean const chatHistory = [] as ChatMessage[] - let sessionId = '' - if (memory) { - if (memory.isSessionIdUsingChatMessageId) sessionId = options.chatId - else sessionId = nodeData.inputs?.sessionId - } - if (systemMessagePrompt) { chatHistory.push({ content: systemMessagePrompt, @@ -82,14 +81,9 @@ class ContextChatEngine_LlamaIndex implements INode { }) } - /* When incomingInput.history is provided, only force replace chatHistory if its ShortTermMemory - * LongTermMemory will automatically retrieved chatHistory from sessionId - */ - if (options && options.chatHistory && memory.isShortTermMemory) { - await memory.resumeMessages(options.chatHistory) - } + const chatEngine = new ContextChatEngine({ chatModel: model, retriever: vectorStoreRetriever }) - const msgs: IMessage[] = await memory.getChatMessages(sessionId) + const msgs = (await memory.getChatMessages(this.sessionId, false, options.chatHistory)) as IMessage[] for (const message of msgs) { if (message.type === 'apiMessage') { chatHistory.push({ @@ -104,74 +98,51 @@ class ContextChatEngine_LlamaIndex implements INode { } } - if (options.socketIO && options.socketIOClientId) { - let response = '' - const stream = await chatEngine.chat(input, chatHistory, true) - let isStart = true - const onNextPromise = () => { - return new Promise((resolve, reject) => { - const onNext = async () => { - try { - const { value, done } = await stream.next() - if (!done) { - if (isStart) { - options.socketIO.to(options.socketIOClientId).emit('start') - isStart = false - } - options.socketIO.to(options.socketIOClientId).emit('token', value) - response += value - onNext() - } else { - resolve(response) - } - } catch (error) { - reject(error) - } - } - onNext() - }) + let text = '' + let isStreamingStarted = false + let sourceDocuments: ICommonObject[] = [] + let sourceNodes: BaseNode[] = [] + const isStreamingEnabled = options.socketIO && options.socketIOClientId + + if (isStreamingEnabled) { + const stream = await chatEngine.chat({ message: input, chatHistory, stream: true }) + for await (const chunk of stream) { + text += chunk.response + if (chunk.sourceNodes) sourceNodes = chunk.sourceNodes + if (!isStreamingStarted) { + isStreamingStarted = true + options.socketIO.to(options.socketIOClientId).emit('start', chunk.response) + } + + options.socketIO.to(options.socketIOClientId).emit('token', chunk.response) } - try { - const result = await onNextPromise() - if (memory) { - await memory.addChatMessages( - [ - { - text: input, - type: 'userMessage' - }, - { - text: result, - type: 'apiMessage' - } - ], - sessionId - ) - } - return result as string - } catch (error) { - throw new Error(error) + if (returnSourceDocuments) { + sourceDocuments = reformatSourceDocuments(sourceNodes) + options.socketIO.to(options.socketIOClientId).emit('sourceDocuments', sourceDocuments) } } else { - const response = await chatEngine.chat(input, chatHistory) - if (memory) { - await memory.addChatMessages( - [ - { - text: input, - type: 'userMessage' - }, - { - text: response?.response, - type: 'apiMessage' - } - ], - sessionId - ) - } - return response?.response + const response = await chatEngine.chat({ message: input, chatHistory }) + text = response?.response + sourceDocuments = reformatSourceDocuments(response?.sourceNodes ?? []) } + + await memory.addChatMessages( + [ + { + text: input, + type: 'userMessage' + }, + { + text: text, + type: 'apiMessage' + } + ], + this.sessionId + ) + + if (returnSourceDocuments) return { text, sourceDocuments } + else return { text } } } diff --git a/packages/components/nodes/engine/ChatEngine/SimpleChatEngine.ts b/packages/components/nodes/engine/ChatEngine/SimpleChatEngine.ts index 9ae9c2f1..9bc9f3c0 100644 --- a/packages/components/nodes/engine/ChatEngine/SimpleChatEngine.ts +++ b/packages/components/nodes/engine/ChatEngine/SimpleChatEngine.ts @@ -1,5 +1,5 @@ -import { ICommonObject, IMessage, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' -import { ChatMessage, SimpleChatEngine } from 'llamaindex' +import { FlowiseMemory, ICommonObject, IMessage, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' +import { LLM, ChatMessage, SimpleChatEngine } from 'llamaindex' class SimpleChatEngine_LlamaIndex implements INode { label: string @@ -13,8 +13,9 @@ class SimpleChatEngine_LlamaIndex implements INode { tags: string[] inputs: INodeParams[] outputs: INodeOutputsValue[] + sessionId?: string - constructor() { + constructor(fields?: { sessionId?: string }) { this.label = 'Simple Chat Engine' this.name = 'simpleChatEngine' this.version = 1.0 @@ -44,30 +45,20 @@ class SimpleChatEngine_LlamaIndex implements INode { placeholder: 'You are a helpful assistant' } ] + this.sessionId = fields?.sessionId } - async init(nodeData: INodeData): Promise { - const model = nodeData.inputs?.model - const memory = nodeData.inputs?.memory - - const chatEngine = new SimpleChatEngine({ llm: model }) - ;(chatEngine as any).memory = memory - return chatEngine + async init(): Promise { + return null } async run(nodeData: INodeData, input: string, options: ICommonObject): Promise { - const chatEngine = nodeData.instance as SimpleChatEngine + const model = nodeData.inputs?.model as LLM const systemMessagePrompt = nodeData.inputs?.systemMessagePrompt as string - const memory = nodeData.inputs?.memory + const memory = nodeData.inputs?.memory as FlowiseMemory const chatHistory = [] as ChatMessage[] - let sessionId = '' - if (memory) { - if (memory.isSessionIdUsingChatMessageId) sessionId = options.chatId - else sessionId = nodeData.inputs?.sessionId - } - if (systemMessagePrompt) { chatHistory.push({ content: systemMessagePrompt, @@ -75,14 +66,9 @@ class SimpleChatEngine_LlamaIndex implements INode { }) } - /* When incomingInput.history is provided, only force replace chatHistory if its ShortTermMemory - * LongTermMemory will automatically retrieved chatHistory from sessionId - */ - if (options && options.chatHistory && memory.isShortTermMemory) { - await memory.resumeMessages(options.chatHistory) - } + const chatEngine = new SimpleChatEngine({ llm: model }) - const msgs: IMessage[] = await memory.getChatMessages(sessionId) + const msgs = (await memory.getChatMessages(this.sessionId, false, options.chatHistory)) as IMessage[] for (const message of msgs) { if (message.type === 'apiMessage') { chatHistory.push({ @@ -97,74 +83,41 @@ class SimpleChatEngine_LlamaIndex implements INode { } } - if (options.socketIO && options.socketIOClientId) { - let response = '' - const stream = await chatEngine.chat(input, chatHistory, true) - let isStart = true - const onNextPromise = () => { - return new Promise((resolve, reject) => { - const onNext = async () => { - try { - const { value, done } = await stream.next() - if (!done) { - if (isStart) { - options.socketIO.to(options.socketIOClientId).emit('start') - isStart = false - } - options.socketIO.to(options.socketIOClientId).emit('token', value) - response += value - onNext() - } else { - resolve(response) - } - } catch (error) { - reject(error) - } - } - onNext() - }) - } + let text = '' + let isStreamingStarted = false + const isStreamingEnabled = options.socketIO && options.socketIOClientId - try { - const result = await onNextPromise() - if (memory) { - await memory.addChatMessages( - [ - { - text: input, - type: 'userMessage' - }, - { - text: result, - type: 'apiMessage' - } - ], - sessionId - ) + if (isStreamingEnabled) { + const stream = await chatEngine.chat({ message: input, chatHistory, stream: true }) + for await (const chunk of stream) { + text += chunk.response + if (!isStreamingStarted) { + isStreamingStarted = true + options.socketIO.to(options.socketIOClientId).emit('start', chunk.response) } - return result as string - } catch (error) { - throw new Error(error) + + options.socketIO.to(options.socketIOClientId).emit('token', chunk.response) } } else { - const response = await chatEngine.chat(input, chatHistory) - if (memory) { - await memory.addChatMessages( - [ - { - text: input, - type: 'userMessage' - }, - { - text: response?.response, - type: 'apiMessage' - } - ], - sessionId - ) - } - return response?.response + const response = await chatEngine.chat({ message: input, chatHistory }) + text = response?.response } + + await memory.addChatMessages( + [ + { + text: input, + type: 'userMessage' + }, + { + text: text, + type: 'apiMessage' + } + ], + this.sessionId + ) + + return text } } diff --git a/packages/components/nodes/engine/EngineUtils.ts b/packages/components/nodes/engine/EngineUtils.ts new file mode 100644 index 00000000..9424e789 --- /dev/null +++ b/packages/components/nodes/engine/EngineUtils.ts @@ -0,0 +1,12 @@ +import { BaseNode, Metadata } from 'llamaindex' + +export const reformatSourceDocuments = (sourceNodes: BaseNode[]) => { + const sourceDocuments = [] + for (const node of sourceNodes) { + sourceDocuments.push({ + pageContent: (node as any).text, + metadata: node.metadata + }) + } + return sourceDocuments +} diff --git a/packages/components/nodes/engine/QueryEngine/QueryEngine.ts b/packages/components/nodes/engine/QueryEngine/QueryEngine.ts index 7059c260..14774703 100644 --- a/packages/components/nodes/engine/QueryEngine/QueryEngine.ts +++ b/packages/components/nodes/engine/QueryEngine/QueryEngine.ts @@ -1,14 +1,15 @@ -import { INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' +import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' import { RetrieverQueryEngine, - BaseNode, - Metadata, ResponseSynthesizer, CompactAndRefine, TreeSummarize, Refine, - SimpleResponseBuilder + SimpleResponseBuilder, + BaseNode, + Metadata } from 'llamaindex' +import { reformatSourceDocuments } from '../EngineUtils' class QueryEngine_LlamaIndex implements INode { label: string @@ -22,8 +23,9 @@ class QueryEngine_LlamaIndex implements INode { tags: string[] inputs: INodeParams[] outputs: INodeOutputsValue[] + sessionId?: string - constructor() { + constructor(fields?: { sessionId?: string }) { this.label = 'Query Engine' this.name = 'queryEngine' this.version = 1.0 @@ -54,9 +56,15 @@ class QueryEngine_LlamaIndex implements INode { optional: true } ] + this.sessionId = fields?.sessionId } - async init(nodeData: INodeData): Promise { + async init(): Promise { + return null + } + + async run(nodeData: INodeData, input: string, options: ICommonObject): Promise { + const returnSourceDocuments = nodeData.inputs?.returnSourceDocuments as boolean const vectorStoreRetriever = nodeData.inputs?.vectorStoreRetriever const responseSynthesizerObj = nodeData.inputs?.responseSynthesizer @@ -97,30 +105,39 @@ class QueryEngine_LlamaIndex implements INode { } const queryEngine = new RetrieverQueryEngine(vectorStoreRetriever) - return queryEngine + + let text = '' + let sourceDocuments: ICommonObject[] = [] + let sourceNodes: BaseNode[] = [] + let isStreamingStarted = false + const isStreamingEnabled = options.socketIO && options.socketIOClientId + + if (isStreamingEnabled) { + const stream = await queryEngine.query({ query: input, stream: true }) + for await (const chunk of stream) { + text += chunk.response + if (chunk.sourceNodes) sourceNodes = chunk.sourceNodes + if (!isStreamingStarted) { + isStreamingStarted = true + options.socketIO.to(options.socketIOClientId).emit('start', chunk.response) + } + + options.socketIO.to(options.socketIOClientId).emit('token', chunk.response) + } + + if (returnSourceDocuments) { + sourceDocuments = reformatSourceDocuments(sourceNodes) + options.socketIO.to(options.socketIOClientId).emit('sourceDocuments', sourceDocuments) + } + } else { + const response = await queryEngine.query({ query: input }) + text = response?.response + sourceDocuments = reformatSourceDocuments(response?.sourceNodes ?? []) + } + + if (returnSourceDocuments) return { text, sourceDocuments } + else return { text } } - - async run(nodeData: INodeData, input: string): Promise { - const queryEngine = nodeData.instance as RetrieverQueryEngine - const returnSourceDocuments = nodeData.inputs?.returnSourceDocuments as boolean - - const response = await queryEngine.query(input) - if (returnSourceDocuments && response.sourceNodes?.length) - return { text: response?.response, sourceDocuments: reformatSourceDocuments(response.sourceNodes) } - - return response?.response - } -} - -const reformatSourceDocuments = (sourceNodes: BaseNode[]) => { - const sourceDocuments = [] - for (const node of sourceNodes) { - sourceDocuments.push({ - pageContent: (node as any).text, - metadata: node.metadata - }) - } - return sourceDocuments } module.exports = { nodeClass: QueryEngine_LlamaIndex } diff --git a/packages/components/nodes/llms/AWSBedrock/AWSBedrock.ts b/packages/components/nodes/llms/AWSBedrock/AWSBedrock.ts index 177a32ef..0fa417b4 100644 --- a/packages/components/nodes/llms/AWSBedrock/AWSBedrock.ts +++ b/packages/components/nodes/llms/AWSBedrock/AWSBedrock.ts @@ -27,9 +27,9 @@ class AWSBedrock_LLMs implements INode { constructor() { this.label = 'AWS Bedrock' this.name = 'awsBedrock' - this.version = 2.0 + this.version = 3.0 this.type = 'AWSBedrock' - this.icon = 'awsBedrock.png' + this.icon = 'aws.svg' this.category = 'LLMs' this.description = 'Wrapper around AWS Bedrock large language models' this.baseClasses = [this.type, ...getBaseClasses(Bedrock)] @@ -105,6 +105,13 @@ class AWSBedrock_LLMs implements INode { { label: 'ai21.j2-ultra', name: 'ai21.j2-ultra' } ] }, + { + label: 'Custom Model Name', + name: 'customModel', + description: 'If provided, will override model selected from Model Name option', + type: 'string', + optional: true + }, { label: 'Temperature', name: 'temperature', @@ -112,6 +119,7 @@ class AWSBedrock_LLMs implements INode { step: 0.1, description: 'Temperature parameter may not apply to certain model. Please check available model parameters', optional: true, + additionalParams: true, default: 0.7 }, { @@ -121,6 +129,7 @@ class AWSBedrock_LLMs implements INode { step: 10, description: 'Max Tokens parameter may not apply to certain model. Please check available model parameters', optional: true, + additionalParams: true, default: 200 } ] @@ -129,11 +138,12 @@ class AWSBedrock_LLMs implements INode { async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { const iRegion = nodeData.inputs?.region as string const iModel = nodeData.inputs?.model as string + const customModel = nodeData.inputs?.customModel as string const iTemperature = nodeData.inputs?.temperature as string const iMax_tokens_to_sample = nodeData.inputs?.max_tokens_to_sample as string const cache = nodeData.inputs?.cache as BaseCache const obj: Partial & BaseLLMParams = { - model: iModel, + model: customModel ? customModel : iModel, region: iRegion, temperature: parseFloat(iTemperature), maxTokens: parseInt(iMax_tokens_to_sample, 10) diff --git a/packages/components/nodes/llms/AWSBedrock/aws.svg b/packages/components/nodes/llms/AWSBedrock/aws.svg new file mode 100644 index 00000000..d783497e --- /dev/null +++ b/packages/components/nodes/llms/AWSBedrock/aws.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/llms/AWSBedrock/awsBedrock.png b/packages/components/nodes/llms/AWSBedrock/awsBedrock.png deleted file mode 100644 index 483bc69a..00000000 Binary files a/packages/components/nodes/llms/AWSBedrock/awsBedrock.png and /dev/null differ diff --git a/packages/components/nodes/llms/Azure OpenAI/Azure.svg b/packages/components/nodes/llms/Azure OpenAI/Azure.svg index 47ad8c44..7b150811 100644 --- a/packages/components/nodes/llms/Azure OpenAI/Azure.svg +++ b/packages/components/nodes/llms/Azure OpenAI/Azure.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/packages/components/nodes/llms/Bittensor/Bittensor.ts b/packages/components/nodes/llms/Bittensor/Bittensor.ts index e6cc2bb6..68652828 100644 --- a/packages/components/nodes/llms/Bittensor/Bittensor.ts +++ b/packages/components/nodes/llms/Bittensor/Bittensor.ts @@ -20,7 +20,7 @@ class Bittensor_LLMs implements INode { this.name = 'NIBittensorLLM' this.version = 2.0 this.type = 'Bittensor' - this.icon = 'logo.png' + this.icon = 'NIBittensor.svg' this.category = 'LLMs' this.description = 'Wrapper around Bittensor subnet 1 large language models' this.baseClasses = [this.type, ...getBaseClasses(NIBittensorLLM)] diff --git a/packages/components/nodes/llms/Bittensor/NIBittensor.svg b/packages/components/nodes/llms/Bittensor/NIBittensor.svg new file mode 100644 index 00000000..062cd66b --- /dev/null +++ b/packages/components/nodes/llms/Bittensor/NIBittensor.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/llms/Bittensor/logo.png b/packages/components/nodes/llms/Bittensor/logo.png deleted file mode 100644 index ad51774d..00000000 Binary files a/packages/components/nodes/llms/Bittensor/logo.png and /dev/null differ diff --git a/packages/components/nodes/llms/Cohere/Cohere.svg b/packages/components/nodes/llms/Cohere/Cohere.svg new file mode 100644 index 00000000..88bcabe3 --- /dev/null +++ b/packages/components/nodes/llms/Cohere/Cohere.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/llms/Cohere/Cohere.ts b/packages/components/nodes/llms/Cohere/Cohere.ts index 3fde0af0..1760b10d 100644 --- a/packages/components/nodes/llms/Cohere/Cohere.ts +++ b/packages/components/nodes/llms/Cohere/Cohere.ts @@ -20,7 +20,7 @@ class Cohere_LLMs implements INode { this.name = 'cohere' this.version = 2.0 this.type = 'Cohere' - this.icon = 'cohere.png' + this.icon = 'Cohere.svg' this.category = 'LLMs' this.description = 'Wrapper around Cohere large language models' this.baseClasses = [this.type, ...getBaseClasses(Cohere)] diff --git a/packages/components/nodes/llms/Cohere/cohere.png b/packages/components/nodes/llms/Cohere/cohere.png deleted file mode 100644 index 266adeac..00000000 Binary files a/packages/components/nodes/llms/Cohere/cohere.png and /dev/null differ diff --git a/packages/components/nodes/llms/GooglePaLM/GooglePaLM.svg b/packages/components/nodes/llms/GooglePaLM/GooglePaLM.svg new file mode 100644 index 00000000..ed47326a --- /dev/null +++ b/packages/components/nodes/llms/GooglePaLM/GooglePaLM.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/llms/GooglePaLM/GooglePaLM.ts b/packages/components/nodes/llms/GooglePaLM/GooglePaLM.ts index d3212a1c..d22b70f7 100644 --- a/packages/components/nodes/llms/GooglePaLM/GooglePaLM.ts +++ b/packages/components/nodes/llms/GooglePaLM/GooglePaLM.ts @@ -19,7 +19,7 @@ class GooglePaLM_LLMs implements INode { this.name = 'GooglePaLM' this.version = 2.0 this.type = 'GooglePaLM' - this.icon = 'Google_PaLM_Logo.svg' + this.icon = 'GooglePaLM.svg' this.category = 'LLMs' this.description = 'Wrapper around Google MakerSuite PaLM large language models' this.baseClasses = [this.type, ...getBaseClasses(GooglePaLM)] diff --git a/packages/components/nodes/llms/GooglePaLM/Google_PaLM_Logo.svg b/packages/components/nodes/llms/GooglePaLM/Google_PaLM_Logo.svg deleted file mode 100644 index 5c345fe1..00000000 --- a/packages/components/nodes/llms/GooglePaLM/Google_PaLM_Logo.svg +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/components/nodes/llms/GoogleVertexAI/GoogleVertex.svg b/packages/components/nodes/llms/GoogleVertexAI/GoogleVertex.svg new file mode 100644 index 00000000..a517740f --- /dev/null +++ b/packages/components/nodes/llms/GoogleVertexAI/GoogleVertex.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/llms/GoogleVertexAI/GoogleVertexAI.ts b/packages/components/nodes/llms/GoogleVertexAI/GoogleVertexAI.ts index 6b6d534b..f3f807f8 100644 --- a/packages/components/nodes/llms/GoogleVertexAI/GoogleVertexAI.ts +++ b/packages/components/nodes/llms/GoogleVertexAI/GoogleVertexAI.ts @@ -21,7 +21,7 @@ class GoogleVertexAI_LLMs implements INode { this.name = 'googlevertexai' this.version = 2.0 this.type = 'GoogleVertexAI' - this.icon = 'vertexai.svg' + this.icon = 'GoogleVertex.svg' this.category = 'LLMs' this.description = 'Wrapper around GoogleVertexAI large language models' this.baseClasses = [this.type, ...getBaseClasses(GoogleVertexAI)] diff --git a/packages/components/nodes/llms/GoogleVertexAI/vertexai.svg b/packages/components/nodes/llms/GoogleVertexAI/vertexai.svg deleted file mode 100644 index 31244412..00000000 --- a/packages/components/nodes/llms/GoogleVertexAI/vertexai.svg +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/packages/components/nodes/llms/HuggingFaceInference/HuggingFace.svg b/packages/components/nodes/llms/HuggingFaceInference/HuggingFace.svg new file mode 100644 index 00000000..58c85d57 --- /dev/null +++ b/packages/components/nodes/llms/HuggingFaceInference/HuggingFace.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/llms/HuggingFaceInference/HuggingFaceInference.ts b/packages/components/nodes/llms/HuggingFaceInference/HuggingFaceInference.ts index 8dcf021b..17260e9b 100644 --- a/packages/components/nodes/llms/HuggingFaceInference/HuggingFaceInference.ts +++ b/packages/components/nodes/llms/HuggingFaceInference/HuggingFaceInference.ts @@ -20,7 +20,7 @@ class HuggingFaceInference_LLMs implements INode { this.name = 'huggingFaceInference_LLMs' this.version = 2.0 this.type = 'HuggingFaceInference' - this.icon = 'huggingface.png' + this.icon = 'HuggingFace.svg' this.category = 'LLMs' this.description = 'Wrapper around HuggingFace large language models' this.baseClasses = [this.type, ...getBaseClasses(HuggingFaceInference)] diff --git a/packages/components/nodes/llms/HuggingFaceInference/huggingface.png b/packages/components/nodes/llms/HuggingFaceInference/huggingface.png deleted file mode 100644 index f8f202a4..00000000 Binary files a/packages/components/nodes/llms/HuggingFaceInference/huggingface.png and /dev/null differ diff --git a/packages/components/nodes/llms/Ollama/Ollama.svg b/packages/components/nodes/llms/Ollama/Ollama.svg new file mode 100644 index 00000000..2dc8df53 --- /dev/null +++ b/packages/components/nodes/llms/Ollama/Ollama.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/llms/Ollama/Ollama.ts b/packages/components/nodes/llms/Ollama/Ollama.ts index 348b1883..385890c9 100644 --- a/packages/components/nodes/llms/Ollama/Ollama.ts +++ b/packages/components/nodes/llms/Ollama/Ollama.ts @@ -1,8 +1,7 @@ import { INode, INodeData, INodeParams } from '../../../src/Interface' import { getBaseClasses } from '../../../src/utils' -import { Ollama } from 'langchain/llms/ollama' +import { Ollama, OllamaInput } from 'langchain/llms/ollama' import { BaseCache } from 'langchain/schema' -import { OllamaInput } from 'langchain/dist/util/ollama' import { BaseLLMParams } from 'langchain/llms/base' class Ollama_LLMs implements INode { @@ -22,7 +21,7 @@ class Ollama_LLMs implements INode { this.name = 'ollama' this.version = 2.0 this.type = 'Ollama' - this.icon = 'ollama.png' + this.icon = 'Ollama.svg' this.category = 'LLMs' this.description = 'Wrapper around open source large language models on Ollama' this.baseClasses = [this.type, ...getBaseClasses(Ollama)] diff --git a/packages/components/nodes/llms/Ollama/ollama.png b/packages/components/nodes/llms/Ollama/ollama.png deleted file mode 100644 index 8cd2cf1e..00000000 Binary files a/packages/components/nodes/llms/Ollama/ollama.png and /dev/null differ diff --git a/packages/components/nodes/llms/OpenAI/OpenAI.ts b/packages/components/nodes/llms/OpenAI/OpenAI.ts index 9109dd40..f2a2a85f 100644 --- a/packages/components/nodes/llms/OpenAI/OpenAI.ts +++ b/packages/components/nodes/llms/OpenAI/OpenAI.ts @@ -21,7 +21,7 @@ class OpenAI_LLMs implements INode { this.name = 'openAI' this.version = 3.0 this.type = 'OpenAI' - this.icon = 'openai.png' + this.icon = 'openai.svg' this.category = 'LLMs' this.description = 'Wrapper around OpenAI large language models' this.baseClasses = [this.type, ...getBaseClasses(OpenAI)] diff --git a/packages/components/nodes/llms/OpenAI/openai.png b/packages/components/nodes/llms/OpenAI/openai.png deleted file mode 100644 index de08a05b..00000000 Binary files a/packages/components/nodes/llms/OpenAI/openai.png and /dev/null differ diff --git a/packages/components/nodes/llms/OpenAI/openai.svg b/packages/components/nodes/llms/OpenAI/openai.svg new file mode 100644 index 00000000..5c20398a --- /dev/null +++ b/packages/components/nodes/llms/OpenAI/openai.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/packages/components/nodes/llms/Replicate/replicate.svg b/packages/components/nodes/llms/Replicate/replicate.svg index 2e46453f..a341b880 100644 --- a/packages/components/nodes/llms/Replicate/replicate.svg +++ b/packages/components/nodes/llms/Replicate/replicate.svg @@ -1,7 +1,6 @@ - \ No newline at end of file + + + + + + diff --git a/packages/components/nodes/memory/BufferMemory/BufferMemory.ts b/packages/components/nodes/memory/BufferMemory/BufferMemory.ts index 5310f88d..4a6252b5 100644 --- a/packages/components/nodes/memory/BufferMemory/BufferMemory.ts +++ b/packages/components/nodes/memory/BufferMemory/BufferMemory.ts @@ -1,6 +1,7 @@ -import { IMessage, INode, INodeData, INodeParams, MessageType } from '../../../src/Interface' +import { FlowiseMemory, IMessage, INode, INodeData, INodeParams, MemoryMethods } from '../../../src/Interface' import { convertBaseMessagetoIMessage, getBaseClasses } from '../../../src/utils' import { BufferMemory, BufferMemoryInput } from 'langchain/memory' +import { BaseMessage } from 'langchain/schema' class BufferMemory_Memory implements INode { label: string @@ -49,43 +50,32 @@ class BufferMemory_Memory implements INode { } } -class BufferMemoryExtended extends BufferMemory { - isShortTermMemory = true - +class BufferMemoryExtended extends FlowiseMemory implements MemoryMethods { constructor(fields: BufferMemoryInput) { super(fields) } - async getChatMessages(): Promise { + async getChatMessages(_?: string, returnBaseMessages = false, prevHistory: IMessage[] = []): Promise { + await this.chatHistory.clear() + + for (const msg of prevHistory) { + if (msg.type === 'userMessage') await this.chatHistory.addUserMessage(msg.message) + else if (msg.type === 'apiMessage') await this.chatHistory.addAIChatMessage(msg.message) + } + const memoryResult = await this.loadMemoryVariables({}) const baseMessages = memoryResult[this.memoryKey ?? 'chat_history'] - return convertBaseMessagetoIMessage(baseMessages) + return returnBaseMessages ? baseMessages : convertBaseMessagetoIMessage(baseMessages) } - async addChatMessages(msgArray: { text: string; type: MessageType }[]): Promise { - const input = msgArray.find((msg) => msg.type === 'userMessage') - const output = msgArray.find((msg) => msg.type === 'apiMessage') - - const inputValues = { [this.inputKey ?? 'input']: input?.text } - const outputValues = { output: output?.text } - - await this.saveContext(inputValues, outputValues) + async addChatMessages(): Promise { + // adding chat messages will be done on the fly in getChatMessages() + return } async clearChatMessages(): Promise { await this.clear() } - - async resumeMessages(messages: IMessage[]): Promise { - // Clear existing chatHistory to avoid duplication - if (messages.length) await this.clear() - - // Insert into chatHistory - for (const msg of messages) { - if (msg.type === 'userMessage') await this.chatHistory.addUserMessage(msg.message) - else if (msg.type === 'apiMessage') await this.chatHistory.addAIChatMessage(msg.message) - } - } } module.exports = { nodeClass: BufferMemory_Memory } diff --git a/packages/components/nodes/memory/BufferMemory/memory.svg b/packages/components/nodes/memory/BufferMemory/memory.svg index ca8e17da..d60f5df4 100644 --- a/packages/components/nodes/memory/BufferMemory/memory.svg +++ b/packages/components/nodes/memory/BufferMemory/memory.svg @@ -1,8 +1,16 @@ - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + + diff --git a/packages/components/nodes/memory/BufferWindowMemory/BufferWindowMemory.ts b/packages/components/nodes/memory/BufferWindowMemory/BufferWindowMemory.ts index 9915d48d..c21405a4 100644 --- a/packages/components/nodes/memory/BufferWindowMemory/BufferWindowMemory.ts +++ b/packages/components/nodes/memory/BufferWindowMemory/BufferWindowMemory.ts @@ -1,6 +1,7 @@ -import { IMessage, INode, INodeData, INodeParams, MessageType } from '../../../src/Interface' +import { FlowiseWindowMemory, IMessage, INode, INodeData, INodeParams, MemoryMethods } from '../../../src/Interface' import { convertBaseMessagetoIMessage, getBaseClasses } from '../../../src/utils' import { BufferWindowMemory, BufferWindowMemoryInput } from 'langchain/memory' +import { BaseMessage } from 'langchain/schema' class BufferWindowMemory_Memory implements INode { label: string @@ -61,43 +62,33 @@ class BufferWindowMemory_Memory implements INode { } } -class BufferWindowMemoryExtended extends BufferWindowMemory { - isShortTermMemory = true - +class BufferWindowMemoryExtended extends FlowiseWindowMemory implements MemoryMethods { constructor(fields: BufferWindowMemoryInput) { super(fields) } - async getChatMessages(): Promise { + async getChatMessages(_?: string, returnBaseMessages = false, prevHistory: IMessage[] = []): Promise { + await this.chatHistory.clear() + + // Insert into chatHistory + for (const msg of prevHistory) { + if (msg.type === 'userMessage') await this.chatHistory.addUserMessage(msg.message) + else if (msg.type === 'apiMessage') await this.chatHistory.addAIChatMessage(msg.message) + } + const memoryResult = await this.loadMemoryVariables({}) const baseMessages = memoryResult[this.memoryKey ?? 'chat_history'] - return convertBaseMessagetoIMessage(baseMessages) + return returnBaseMessages ? baseMessages : convertBaseMessagetoIMessage(baseMessages) } - async addChatMessages(msgArray: { text: string; type: MessageType }[]): Promise { - const input = msgArray.find((msg) => msg.type === 'userMessage') - const output = msgArray.find((msg) => msg.type === 'apiMessage') - - const inputValues = { [this.inputKey ?? 'input']: input?.text } - const outputValues = { output: output?.text } - - await this.saveContext(inputValues, outputValues) + async addChatMessages(): Promise { + // adding chat messages will be done on the fly in getChatMessages() + return } async clearChatMessages(): Promise { await this.clear() } - - async resumeMessages(messages: IMessage[]): Promise { - // Clear existing chatHistory to avoid duplication - if (messages.length) await this.clear() - - // Insert into chatHistory - for (const msg of messages) { - if (msg.type === 'userMessage') await this.chatHistory.addUserMessage(msg.message) - else if (msg.type === 'apiMessage') await this.chatHistory.addAIChatMessage(msg.message) - } - } } module.exports = { nodeClass: BufferWindowMemory_Memory } diff --git a/packages/components/nodes/memory/BufferWindowMemory/memory.svg b/packages/components/nodes/memory/BufferWindowMemory/memory.svg index ca8e17da..224f9158 100644 --- a/packages/components/nodes/memory/BufferWindowMemory/memory.svg +++ b/packages/components/nodes/memory/BufferWindowMemory/memory.svg @@ -1,8 +1,19 @@ - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + diff --git a/packages/components/nodes/memory/ConversationSummaryMemory/ConversationSummaryMemory.ts b/packages/components/nodes/memory/ConversationSummaryMemory/ConversationSummaryMemory.ts index e88beb13..45d39326 100644 --- a/packages/components/nodes/memory/ConversationSummaryMemory/ConversationSummaryMemory.ts +++ b/packages/components/nodes/memory/ConversationSummaryMemory/ConversationSummaryMemory.ts @@ -1,7 +1,8 @@ -import { IMessage, INode, INodeData, INodeParams, MessageType } from '../../../src/Interface' +import { FlowiseSummaryMemory, IMessage, INode, INodeData, INodeParams, MemoryMethods } from '../../../src/Interface' import { convertBaseMessagetoIMessage, getBaseClasses } from '../../../src/utils' import { ConversationSummaryMemory, ConversationSummaryMemoryInput } from 'langchain/memory' import { BaseLanguageModel } from 'langchain/base_language' +import { BaseMessage } from 'langchain/schema' class ConversationSummaryMemory_Memory implements INode { label: string @@ -60,47 +61,37 @@ class ConversationSummaryMemory_Memory implements INode { } } -class ConversationSummaryMemoryExtended extends ConversationSummaryMemory { - isShortTermMemory = true - +class ConversationSummaryMemoryExtended extends FlowiseSummaryMemory implements MemoryMethods { constructor(fields: ConversationSummaryMemoryInput) { super(fields) } - async getChatMessages(): Promise { + async getChatMessages(_?: string, returnBaseMessages = false, prevHistory: IMessage[] = []): Promise { + await this.chatHistory.clear() + this.buffer = '' + + for (const msg of prevHistory) { + if (msg.type === 'userMessage') await this.chatHistory.addUserMessage(msg.message) + else if (msg.type === 'apiMessage') await this.chatHistory.addAIChatMessage(msg.message) + } + + // Get summary + const chatMessages = await this.chatHistory.getMessages() + this.buffer = chatMessages.length ? await this.predictNewSummary(chatMessages.slice(-2), this.buffer) : '' + const memoryResult = await this.loadMemoryVariables({}) const baseMessages = memoryResult[this.memoryKey ?? 'chat_history'] - return convertBaseMessagetoIMessage(baseMessages) + return returnBaseMessages ? baseMessages : convertBaseMessagetoIMessage(baseMessages) } - async addChatMessages(msgArray: { text: string; type: MessageType }[]): Promise { - const input = msgArray.find((msg) => msg.type === 'userMessage') - const output = msgArray.find((msg) => msg.type === 'apiMessage') - - const inputValues = { [this.inputKey ?? 'input']: input?.text } - const outputValues = { output: output?.text } - - await this.saveContext(inputValues, outputValues) + async addChatMessages(): Promise { + // adding chat messages will be done on the fly in getChatMessages() + return } async clearChatMessages(): Promise { await this.clear() } - - async resumeMessages(messages: IMessage[]): Promise { - // Clear existing chatHistory to avoid duplication - if (messages.length) await this.clear() - - // Insert into chatHistory - for (const msg of messages) { - if (msg.type === 'userMessage') await this.chatHistory.addUserMessage(msg.message) - else if (msg.type === 'apiMessage') await this.chatHistory.addAIChatMessage(msg.message) - } - - // Replace buffer - const chatMessages = await this.chatHistory.getMessages() - this.buffer = await this.predictNewSummary(chatMessages.slice(-2), this.buffer) - } } module.exports = { nodeClass: ConversationSummaryMemory_Memory } diff --git a/packages/components/nodes/memory/ConversationSummaryMemory/memory.svg b/packages/components/nodes/memory/ConversationSummaryMemory/memory.svg index ca8e17da..c7aadc96 100644 --- a/packages/components/nodes/memory/ConversationSummaryMemory/memory.svg +++ b/packages/components/nodes/memory/ConversationSummaryMemory/memory.svg @@ -1,8 +1,19 @@ - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + diff --git a/packages/components/nodes/memory/DynamoDb/DynamoDb.ts b/packages/components/nodes/memory/DynamoDb/DynamoDb.ts index a1c44554..91c1d369 100644 --- a/packages/components/nodes/memory/DynamoDb/DynamoDb.ts +++ b/packages/components/nodes/memory/DynamoDb/DynamoDb.ts @@ -11,9 +11,9 @@ import { } from '@aws-sdk/client-dynamodb' import { DynamoDBChatMessageHistory } from 'langchain/stores/message/dynamodb' import { BufferMemory, BufferMemoryInput } from 'langchain/memory' -import { mapStoredMessageToChatMessage, AIMessage, HumanMessage, StoredMessage } from 'langchain/schema' +import { mapStoredMessageToChatMessage, AIMessage, HumanMessage, StoredMessage, BaseMessage } from 'langchain/schema' import { convertBaseMessagetoIMessage, getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' -import { ICommonObject, IMessage, INode, INodeData, INodeParams, MessageType } from '../../../src/Interface' +import { FlowiseMemory, ICommonObject, IMessage, INode, INodeData, INodeParams, MemoryMethods, MessageType } from '../../../src/Interface' class DynamoDb_Memory implements INode { label: string @@ -90,17 +90,7 @@ const initalizeDynamoDB = async (nodeData: INodeData, options: ICommonObject): P const partitionKey = nodeData.inputs?.partitionKey as string const region = nodeData.inputs?.region as string const memoryKey = nodeData.inputs?.memoryKey as string - const chatId = options.chatId - - let isSessionIdUsingChatMessageId = false - let sessionId = '' - - if (!nodeData.inputs?.sessionId && chatId) { - isSessionIdUsingChatMessageId = true - sessionId = chatId - } else { - sessionId = nodeData.inputs?.sessionId - } + const sessionId = nodeData.inputs?.sessionId as string const credentialData = await getCredentialData(nodeData.credential ?? '', options) const accessKeyId = getCredentialParam('accessKey', credentialData, nodeData) @@ -119,14 +109,13 @@ const initalizeDynamoDB = async (nodeData: INodeData, options: ICommonObject): P const dynamoDb = new DynamoDBChatMessageHistory({ tableName, partitionKey, - sessionId: sessionId ? sessionId : chatId, + sessionId, config }) const memory = new BufferMemoryExtended({ memoryKey: memoryKey ?? 'chat_history', chatHistory: dynamoDb, - isSessionIdUsingChatMessageId, sessionId, dynamodbClient: client }) @@ -134,7 +123,6 @@ const initalizeDynamoDB = async (nodeData: INodeData, options: ICommonObject): P } interface BufferMemoryExtendedInput { - isSessionIdUsingChatMessageId: boolean dynamodbClient: DynamoDBClient sessionId: string } @@ -153,14 +141,12 @@ interface DynamoDBSerializedChatMessage { } } -class BufferMemoryExtended extends BufferMemory { - isSessionIdUsingChatMessageId = false +class BufferMemoryExtended extends FlowiseMemory implements MemoryMethods { sessionId = '' dynamodbClient: DynamoDBClient constructor(fields: BufferMemoryInput & BufferMemoryExtendedInput) { super(fields) - this.isSessionIdUsingChatMessageId = fields.isSessionIdUsingChatMessageId this.sessionId = fields.sessionId this.dynamodbClient = fields.dynamodbClient } @@ -220,7 +206,7 @@ class BufferMemoryExtended extends BufferMemory { await client.send(new UpdateItemCommand(params)) } - async getChatMessages(overrideSessionId = ''): Promise { + async getChatMessages(overrideSessionId = '', returnBaseMessages = false): Promise { if (!this.dynamodbClient) return [] const dynamoKey = overrideSessionId ? this.overrideDynamoKey(overrideSessionId) : (this as any).dynamoKey @@ -244,7 +230,7 @@ class BufferMemoryExtended extends BufferMemory { })) .filter((x): x is StoredMessage => x.type !== undefined && x.data.content !== undefined) const baseMessages = messages.map(mapStoredMessageToChatMessage) - return convertBaseMessagetoIMessage(baseMessages) + return returnBaseMessages ? baseMessages : convertBaseMessagetoIMessage(baseMessages) } async addChatMessages(msgArray: { text: string; type: MessageType }[], overrideSessionId = ''): Promise { diff --git a/packages/components/nodes/memory/DynamoDb/dynamodb.svg b/packages/components/nodes/memory/DynamoDb/dynamodb.svg index f2798350..66f070b1 100644 --- a/packages/components/nodes/memory/DynamoDb/dynamodb.svg +++ b/packages/components/nodes/memory/DynamoDb/dynamodb.svg @@ -1,18 +1,15 @@ - - - - Icon-Architecture/16/Arch_Amazon-DynamoDB_16 - Created with Sketch. - - - - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + diff --git a/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts b/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts index 8bebcfad..c593c20d 100644 --- a/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts +++ b/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts @@ -1,9 +1,9 @@ import { MongoClient, Collection, Document } from 'mongodb' import { MongoDBChatMessageHistory } from 'langchain/stores/message/mongodb' import { BufferMemory, BufferMemoryInput } from 'langchain/memory' -import { BaseMessage, mapStoredMessageToChatMessage, AIMessage, HumanMessage } from 'langchain/schema' +import { mapStoredMessageToChatMessage, AIMessage, HumanMessage, BaseMessage } from 'langchain/schema' import { convertBaseMessagetoIMessage, getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' -import { ICommonObject, IMessage, INode, INodeData, INodeParams, MessageType } from '../../../src/Interface' +import { FlowiseMemory, ICommonObject, IMessage, INode, INodeData, INodeParams, MemoryMethods, MessageType } from '../../../src/Interface' class MongoDB_Memory implements INode { label: string @@ -22,7 +22,7 @@ class MongoDB_Memory implements INode { this.name = 'MongoDBAtlasChatMemory' this.version = 1.0 this.type = 'MongoDBAtlasChatMemory' - this.icon = 'mongodb.png' + this.icon = 'mongodb.svg' this.category = 'Memory' this.description = 'Stores the conversation in MongoDB Atlas' this.baseClasses = [this.type, ...getBaseClasses(BufferMemory)] @@ -74,30 +74,19 @@ const initializeMongoDB = async (nodeData: INodeData, options: ICommonObject): P const databaseName = nodeData.inputs?.databaseName as string const collectionName = nodeData.inputs?.collectionName as string const memoryKey = nodeData.inputs?.memoryKey as string - const chatId = options?.chatId as string - - let isSessionIdUsingChatMessageId = false - let sessionId = '' - - if (!nodeData.inputs?.sessionId && chatId) { - isSessionIdUsingChatMessageId = true - sessionId = chatId - } else { - sessionId = nodeData.inputs?.sessionId - } + const sessionId = nodeData.inputs?.sessionId as string const credentialData = await getCredentialData(nodeData.credential ?? '', options) - let mongoDBConnectUrl = getCredentialParam('mongoDBConnectUrl', credentialData, nodeData) + const mongoDBConnectUrl = getCredentialParam('mongoDBConnectUrl', credentialData, nodeData) const client = new MongoClient(mongoDBConnectUrl) await client.connect() const collection = client.db(databaseName).collection(collectionName) - /**** Methods below are needed to override the original implementations ****/ const mongoDBChatMessageHistory = new MongoDBChatMessageHistory({ collection, - sessionId: sessionId ? sessionId : chatId + sessionId }) mongoDBChatMessageHistory.getMessages = async (): Promise => { @@ -122,43 +111,38 @@ const initializeMongoDB = async (nodeData: INodeData, options: ICommonObject): P mongoDBChatMessageHistory.clear = async (): Promise => { await collection.deleteOne({ sessionId: (mongoDBChatMessageHistory as any).sessionId }) } - /**** End of override functions ****/ return new BufferMemoryExtended({ memoryKey: memoryKey ?? 'chat_history', chatHistory: mongoDBChatMessageHistory, - isSessionIdUsingChatMessageId, sessionId, collection }) } interface BufferMemoryExtendedInput { - isSessionIdUsingChatMessageId: boolean collection: Collection sessionId: string } -class BufferMemoryExtended extends BufferMemory { - isSessionIdUsingChatMessageId = false +class BufferMemoryExtended extends FlowiseMemory implements MemoryMethods { sessionId = '' collection: Collection constructor(fields: BufferMemoryInput & BufferMemoryExtendedInput) { super(fields) - this.isSessionIdUsingChatMessageId = fields.isSessionIdUsingChatMessageId this.sessionId = fields.sessionId this.collection = fields.collection } - async getChatMessages(overrideSessionId = ''): Promise { + async getChatMessages(overrideSessionId = '', returnBaseMessages = false): Promise { if (!this.collection) return [] const id = overrideSessionId ?? this.sessionId const document = await this.collection.findOne({ sessionId: id }) const messages = document?.messages || [] const baseMessages = messages.map(mapStoredMessageToChatMessage) - return convertBaseMessagetoIMessage(baseMessages) + return returnBaseMessages ? baseMessages : convertBaseMessagetoIMessage(baseMessages) } async addChatMessages(msgArray: { text: string; type: MessageType }[], overrideSessionId = ''): Promise { diff --git a/packages/components/nodes/memory/MongoDBMemory/mongodb.png b/packages/components/nodes/memory/MongoDBMemory/mongodb.png deleted file mode 100644 index 5586fe0a..00000000 Binary files a/packages/components/nodes/memory/MongoDBMemory/mongodb.png and /dev/null differ diff --git a/packages/components/nodes/memory/MongoDBMemory/mongodb.svg b/packages/components/nodes/memory/MongoDBMemory/mongodb.svg new file mode 100644 index 00000000..49c5f05a --- /dev/null +++ b/packages/components/nodes/memory/MongoDBMemory/mongodb.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/components/nodes/memory/MotorheadMemory/MotorheadMemory.ts b/packages/components/nodes/memory/MotorheadMemory/MotorheadMemory.ts index 6dd94944..19506fc1 100644 --- a/packages/components/nodes/memory/MotorheadMemory/MotorheadMemory.ts +++ b/packages/components/nodes/memory/MotorheadMemory/MotorheadMemory.ts @@ -1,9 +1,14 @@ -import { IMessage, INode, INodeData, INodeParams, MessageType } from '../../../src/Interface' +import { IMessage, INode, INodeData, INodeParams, MemoryMethods, MessageType } from '../../../src/Interface' import { convertBaseMessagetoIMessage, getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' import { ICommonObject } from '../../../src' -import { MotorheadMemory, MotorheadMemoryInput } from 'langchain/memory' +import { MotorheadMemory, MotorheadMemoryInput, InputValues, OutputValues } from 'langchain/memory' import fetch from 'node-fetch' -import { InputValues, MemoryVariables, OutputValues } from 'langchain/memory' +import { AIMessage, BaseMessage, ChatMessage, HumanMessage } from 'langchain/schema' + +type MotorheadMessage = { + content: string + role: 'Human' | 'AI' +} class MotorMemory_Memory implements INode { label: string @@ -22,7 +27,7 @@ class MotorMemory_Memory implements INode { this.name = 'motorheadMemory' this.version = 1.0 this.type = 'MotorheadMemory' - this.icon = 'motorhead.png' + this.icon = 'motorhead.svg' this.category = 'Memory' this.description = 'Use Motorhead Memory to store chat conversations' this.baseClasses = [this.type, ...getBaseClasses(MotorheadMemory)] @@ -70,27 +75,16 @@ class MotorMemory_Memory implements INode { const initalizeMotorhead = async (nodeData: INodeData, options: ICommonObject): Promise => { const memoryKey = nodeData.inputs?.memoryKey as string const baseURL = nodeData.inputs?.baseURL as string - const chatId = options?.chatId as string - - let isSessionIdUsingChatMessageId = false - let sessionId = '' - - if (!nodeData.inputs?.sessionId && chatId) { - isSessionIdUsingChatMessageId = true - sessionId = chatId - } else { - sessionId = nodeData.inputs?.sessionId - } + const sessionId = nodeData.inputs?.sessionId as string const credentialData = await getCredentialData(nodeData.credential ?? '', options) const apiKey = getCredentialParam('apiKey', credentialData, nodeData) const clientId = getCredentialParam('clientId', credentialData, nodeData) - let obj: MotorheadMemoryInput & Partial = { + let obj: MotorheadMemoryInput = { returnMessages: true, sessionId, - memoryKey, - isSessionIdUsingChatMessageId + memoryKey } if (baseURL) { @@ -114,35 +108,21 @@ const initalizeMotorhead = async (nodeData: INodeData, options: ICommonObject): return motorheadMemory } -interface MotorheadMemoryExtendedInput { - isSessionIdUsingChatMessageId: boolean -} - -class MotorheadMemoryExtended extends MotorheadMemory { - isSessionIdUsingChatMessageId? = false - - constructor(fields: MotorheadMemoryInput & Partial) { +class MotorheadMemoryExtended extends MotorheadMemory implements MemoryMethods { + constructor(fields: MotorheadMemoryInput) { super(fields) - this.isSessionIdUsingChatMessageId = fields.isSessionIdUsingChatMessageId - } - - async loadMemoryVariables(values: InputValues, overrideSessionId = ''): Promise { - if (overrideSessionId) { - super.sessionId = overrideSessionId - } - return super.loadMemoryVariables({ values }) } async saveContext(inputValues: InputValues, outputValues: OutputValues, overrideSessionId = ''): Promise { if (overrideSessionId) { - super.sessionId = overrideSessionId + this.sessionId = overrideSessionId } return super.saveContext(inputValues, outputValues) } async clear(overrideSessionId = ''): Promise { if (overrideSessionId) { - super.sessionId = overrideSessionId + this.sessionId = overrideSessionId } try { await this.caller.call(fetch, `${this.url}/sessions/${this.sessionId}/memory`, { @@ -160,11 +140,35 @@ class MotorheadMemoryExtended extends MotorheadMemory { await super.clear() } - async getChatMessages(overrideSessionId = ''): Promise { + async getChatMessages(overrideSessionId = '', returnBaseMessages = false): Promise { const id = overrideSessionId ?? this.sessionId - const memoryVariables = await this.loadMemoryVariables({}, id) - const baseMessages = memoryVariables[this.memoryKey] - return convertBaseMessagetoIMessage(baseMessages) + try { + const resp = await this.caller.call(fetch, `${this.url}/sessions/${id}/memory`, { + //@ts-ignore + signal: this.timeout ? AbortSignal.timeout(this.timeout) : undefined, + headers: this._getHeaders() as ICommonObject, + method: 'GET' + }) + const data = await resp.json() + const rawStoredMessages: MotorheadMessage[] = data?.data?.messages ?? [] + + const baseMessages = rawStoredMessages.reverse().map((message) => { + const { content, role } = message + if (role === 'Human') { + return new HumanMessage(content) + } else if (role === 'AI') { + return new AIMessage(content) + } else { + // default to generic ChatMessage + return new ChatMessage(content, role) + } + }) + + return returnBaseMessages ? baseMessages : convertBaseMessagetoIMessage(baseMessages) + } catch (error) { + console.error('Error getting session: ', error) + return [] + } } async addChatMessages(msgArray: { text: string; type: MessageType }[], overrideSessionId = ''): Promise { diff --git a/packages/components/nodes/memory/MotorheadMemory/motorhead.png b/packages/components/nodes/memory/MotorheadMemory/motorhead.png deleted file mode 100644 index e1dfbde0..00000000 Binary files a/packages/components/nodes/memory/MotorheadMemory/motorhead.png and /dev/null differ diff --git a/packages/components/nodes/memory/MotorheadMemory/motorhead.svg b/packages/components/nodes/memory/MotorheadMemory/motorhead.svg new file mode 100644 index 00000000..55ca8c7d --- /dev/null +++ b/packages/components/nodes/memory/MotorheadMemory/motorhead.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/packages/components/nodes/memory/RedisBackedChatMemory/RedisBackedChatMemory.ts b/packages/components/nodes/memory/RedisBackedChatMemory/RedisBackedChatMemory.ts index 4692088b..baf4ea6b 100644 --- a/packages/components/nodes/memory/RedisBackedChatMemory/RedisBackedChatMemory.ts +++ b/packages/components/nodes/memory/RedisBackedChatMemory/RedisBackedChatMemory.ts @@ -2,7 +2,7 @@ import { Redis } from 'ioredis' import { BufferMemory, BufferMemoryInput } from 'langchain/memory' import { RedisChatMessageHistory, RedisChatMessageHistoryInput } from 'langchain/stores/message/ioredis' import { mapStoredMessageToChatMessage, BaseMessage, AIMessage, HumanMessage } from 'langchain/schema' -import { INode, INodeData, INodeParams, ICommonObject, MessageType, IMessage } from '../../../src/Interface' +import { INode, INodeData, INodeParams, ICommonObject, MessageType, IMessage, MemoryMethods, FlowiseMemory } from '../../../src/Interface' import { convertBaseMessagetoIMessage, getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' class RedisBackedChatMemory_Memory implements INode { @@ -58,6 +58,14 @@ class RedisBackedChatMemory_Memory implements INode { type: 'string', default: 'chat_history', additionalParams: true + }, + { + label: 'Window Size', + name: 'windowSize', + type: 'number', + description: 'Window of size k to surface the last k back-and-forth to use as memory.', + additionalParams: true, + optional: true } ] } @@ -70,17 +78,8 @@ class RedisBackedChatMemory_Memory implements INode { const initalizeRedis = async (nodeData: INodeData, options: ICommonObject): Promise => { const sessionTTL = nodeData.inputs?.sessionTTL as number const memoryKey = nodeData.inputs?.memoryKey as string - const chatId = options?.chatId as string - - let isSessionIdUsingChatMessageId = false - let sessionId = '' - - if (!nodeData.inputs?.sessionId && chatId) { - isSessionIdUsingChatMessageId = true - sessionId = chatId - } else { - sessionId = nodeData.inputs?.sessionId - } + const sessionId = nodeData.inputs?.sessionId as string + const windowSize = nodeData.inputs?.windowSize as number const credentialData = await getCredentialData(nodeData.credential ?? '', options) const redisUrl = getCredentialParam('redisUrl', credentialData, nodeData) @@ -92,12 +91,16 @@ const initalizeRedis = async (nodeData: INodeData, options: ICommonObject): Prom const password = getCredentialParam('redisCachePwd', credentialData, nodeData) const portStr = getCredentialParam('redisCachePort', credentialData, nodeData) const host = getCredentialParam('redisCacheHost', credentialData, nodeData) + const sslEnabled = getCredentialParam('redisCacheSslEnabled', credentialData, nodeData) + + const tlsOptions = sslEnabled === true ? { tls: { rejectUnauthorized: false } } : {} client = new Redis({ port: portStr ? parseInt(portStr) : 6379, host, username, - password + password, + ...tlsOptions }) } else { client = new Redis(redisUrl) @@ -117,9 +120,8 @@ const initalizeRedis = async (nodeData: INodeData, options: ICommonObject): Prom const redisChatMessageHistory = new RedisChatMessageHistory(obj) - /**** Methods below are needed to override the original implementations ****/ - redisChatMessageHistory.getMessages = async (): Promise => { - const rawStoredMessages = await client.lrange((redisChatMessageHistory as any).sessionId, 0, -1) + /*redisChatMessageHistory.getMessages = async (): Promise => { + const rawStoredMessages = await client.lrange((redisChatMessageHistory as any).sessionId, windowSize ? -windowSize : 0, -1) const orderedMessages = rawStoredMessages.reverse().map((message) => JSON.parse(message)) return orderedMessages.map(mapStoredMessageToChatMessage) } @@ -134,14 +136,13 @@ const initalizeRedis = async (nodeData: INodeData, options: ICommonObject): Prom redisChatMessageHistory.clear = async (): Promise => { await client.del((redisChatMessageHistory as any).sessionId) - } - /**** End of override functions ****/ + }*/ const memory = new BufferMemoryExtended({ memoryKey: memoryKey ?? 'chat_history', chatHistory: redisChatMessageHistory, - isSessionIdUsingChatMessageId, sessionId, + windowSize, redisClient: client }) @@ -149,31 +150,31 @@ const initalizeRedis = async (nodeData: INodeData, options: ICommonObject): Prom } interface BufferMemoryExtendedInput { - isSessionIdUsingChatMessageId: boolean redisClient: Redis sessionId: string + windowSize?: number } -class BufferMemoryExtended extends BufferMemory { - isSessionIdUsingChatMessageId = false +class BufferMemoryExtended extends FlowiseMemory implements MemoryMethods { sessionId = '' redisClient: Redis + windowSize?: number constructor(fields: BufferMemoryInput & BufferMemoryExtendedInput) { super(fields) - this.isSessionIdUsingChatMessageId = fields.isSessionIdUsingChatMessageId this.sessionId = fields.sessionId this.redisClient = fields.redisClient + this.windowSize = fields.windowSize } - async getChatMessages(overrideSessionId = ''): Promise { + async getChatMessages(overrideSessionId = '', returnBaseMessages = false): Promise { if (!this.redisClient) return [] const id = overrideSessionId ?? this.sessionId - const rawStoredMessages = await this.redisClient.lrange(id, 0, -1) + const rawStoredMessages = await this.redisClient.lrange(id, this.windowSize ? this.windowSize * -1 : 0, -1) const orderedMessages = rawStoredMessages.reverse().map((message) => JSON.parse(message)) const baseMessages = orderedMessages.map(mapStoredMessageToChatMessage) - return convertBaseMessagetoIMessage(baseMessages) + return returnBaseMessages ? baseMessages : convertBaseMessagetoIMessage(baseMessages) } async addChatMessages(msgArray: { text: string; type: MessageType }[], overrideSessionId = ''): Promise { diff --git a/packages/components/nodes/memory/UpstashRedisBackedChatMemory/UpstashRedisBackedChatMemory.ts b/packages/components/nodes/memory/UpstashRedisBackedChatMemory/UpstashRedisBackedChatMemory.ts index 327f0f32..3d7f6dbf 100644 --- a/packages/components/nodes/memory/UpstashRedisBackedChatMemory/UpstashRedisBackedChatMemory.ts +++ b/packages/components/nodes/memory/UpstashRedisBackedChatMemory/UpstashRedisBackedChatMemory.ts @@ -1,8 +1,8 @@ import { Redis } from '@upstash/redis' import { BufferMemory, BufferMemoryInput } from 'langchain/memory' import { UpstashRedisChatMessageHistory } from 'langchain/stores/message/upstash_redis' -import { mapStoredMessageToChatMessage, AIMessage, HumanMessage, StoredMessage } from 'langchain/schema' -import { IMessage, INode, INodeData, INodeParams, MessageType } from '../../../src/Interface' +import { mapStoredMessageToChatMessage, AIMessage, HumanMessage, StoredMessage, BaseMessage } from 'langchain/schema' +import { FlowiseMemory, IMessage, INode, INodeData, INodeParams, MemoryMethods, MessageType } from '../../../src/Interface' import { convertBaseMessagetoIMessage, getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' import { ICommonObject } from '../../../src/Interface' @@ -70,17 +70,7 @@ class UpstashRedisBackedChatMemory_Memory implements INode { const initalizeUpstashRedis = async (nodeData: INodeData, options: ICommonObject): Promise => { const baseURL = nodeData.inputs?.baseURL as string const sessionTTL = nodeData.inputs?.sessionTTL as string - const chatId = options?.chatId as string - - let isSessionIdUsingChatMessageId = false - let sessionId = '' - - if (!nodeData.inputs?.sessionId && chatId) { - isSessionIdUsingChatMessageId = true - sessionId = chatId - } else { - sessionId = nodeData.inputs?.sessionId - } + const sessionId = nodeData.inputs?.sessionId as string const credentialData = await getCredentialData(nodeData.credential ?? '', options) const upstashRestToken = getCredentialParam('upstashRestToken', credentialData, nodeData) @@ -91,7 +81,7 @@ const initalizeUpstashRedis = async (nodeData: INodeData, options: ICommonObject }) const redisChatMessageHistory = new UpstashRedisChatMessageHistory({ - sessionId: sessionId ? sessionId : chatId, + sessionId, sessionTTL: sessionTTL ? parseInt(sessionTTL, 10) : undefined, client }) @@ -99,7 +89,6 @@ const initalizeUpstashRedis = async (nodeData: INodeData, options: ICommonObject const memory = new BufferMemoryExtended({ memoryKey: 'chat_history', chatHistory: redisChatMessageHistory, - isSessionIdUsingChatMessageId, sessionId, redisClient: client }) @@ -108,24 +97,21 @@ const initalizeUpstashRedis = async (nodeData: INodeData, options: ICommonObject } interface BufferMemoryExtendedInput { - isSessionIdUsingChatMessageId: boolean redisClient: Redis sessionId: string } -class BufferMemoryExtended extends BufferMemory { - isSessionIdUsingChatMessageId = false +class BufferMemoryExtended extends FlowiseMemory implements MemoryMethods { sessionId = '' redisClient: Redis constructor(fields: BufferMemoryInput & BufferMemoryExtendedInput) { super(fields) - this.isSessionIdUsingChatMessageId = fields.isSessionIdUsingChatMessageId this.sessionId = fields.sessionId this.redisClient = fields.redisClient } - async getChatMessages(overrideSessionId = ''): Promise { + async getChatMessages(overrideSessionId = '', returnBaseMessages = false): Promise { if (!this.redisClient) return [] const id = overrideSessionId ?? this.sessionId @@ -133,7 +119,7 @@ class BufferMemoryExtended extends BufferMemory { const orderedMessages = rawStoredMessages.reverse() const previousMessages = orderedMessages.filter((x): x is StoredMessage => x.type !== undefined && x.data.content !== undefined) const baseMessages = previousMessages.map(mapStoredMessageToChatMessage) - return convertBaseMessagetoIMessage(baseMessages) + return returnBaseMessages ? baseMessages : convertBaseMessagetoIMessage(baseMessages) } async addChatMessages(msgArray: { text: string; type: MessageType }[], overrideSessionId = ''): Promise { diff --git a/packages/components/nodes/memory/UpstashRedisBackedChatMemory/upstash.svg b/packages/components/nodes/memory/UpstashRedisBackedChatMemory/upstash.svg index a0fb96a7..582d151a 100644 --- a/packages/components/nodes/memory/UpstashRedisBackedChatMemory/upstash.svg +++ b/packages/components/nodes/memory/UpstashRedisBackedChatMemory/upstash.svg @@ -1,12 +1,6 @@ - - - upstash - - - - - - - - + + + + + diff --git a/packages/components/nodes/memory/ZepMemory/ZepMemory.ts b/packages/components/nodes/memory/ZepMemory/ZepMemory.ts index ac0ac896..597eee8a 100644 --- a/packages/components/nodes/memory/ZepMemory/ZepMemory.ts +++ b/packages/components/nodes/memory/ZepMemory/ZepMemory.ts @@ -1,8 +1,9 @@ -import { IMessage, INode, INodeData, INodeParams, MessageType } from '../../../src/Interface' +import { IMessage, INode, INodeData, INodeParams, MemoryMethods, MessageType } from '../../../src/Interface' import { convertBaseMessagetoIMessage, getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' import { ZepMemory, ZepMemoryInput } from 'langchain/memory/zep' import { ICommonObject } from '../../../src' import { InputValues, MemoryVariables, OutputValues } from 'langchain/memory' +import { BaseMessage } from 'langchain/schema' class ZepMemory_Memory implements INode { label: string @@ -21,7 +22,7 @@ class ZepMemory_Memory implements INode { this.name = 'ZepMemory' this.version = 2.0 this.type = 'ZepMemory' - this.icon = 'zep.png' + this.icon = 'zep.svg' this.category = 'Memory' this.description = 'Summarizes the conversation and stores the memory in zep server' this.baseClasses = [this.type, ...getBaseClasses(ZepMemory)] @@ -108,17 +109,7 @@ const initalizeZep = async (nodeData: INodeData, options: ICommonObject): Promis const memoryKey = nodeData.inputs?.memoryKey as string const inputKey = nodeData.inputs?.inputKey as string const k = nodeData.inputs?.k as string - const chatId = options?.chatId as string - - let isSessionIdUsingChatMessageId = false - let sessionId = '' - - if (!nodeData.inputs?.sessionId && chatId) { - isSessionIdUsingChatMessageId = true - sessionId = chatId - } else { - sessionId = nodeData.inputs?.sessionId - } + const sessionId = nodeData.inputs?.sessionId as string const credentialData = await getCredentialData(nodeData.credential ?? '', options) const apiKey = getCredentialParam('apiKey', credentialData, nodeData) @@ -131,7 +122,6 @@ const initalizeZep = async (nodeData: INodeData, options: ICommonObject): Promis memoryKey, inputKey, sessionId, - isSessionIdUsingChatMessageId, k: k ? parseInt(k, 10) : undefined } if (apiKey) obj.apiKey = apiKey @@ -140,46 +130,43 @@ const initalizeZep = async (nodeData: INodeData, options: ICommonObject): Promis } interface ZepMemoryExtendedInput { - isSessionIdUsingChatMessageId: boolean k?: number } -class ZepMemoryExtended extends ZepMemory { - isSessionIdUsingChatMessageId = false +class ZepMemoryExtended extends ZepMemory implements MemoryMethods { lastN?: number constructor(fields: ZepMemoryInput & ZepMemoryExtendedInput) { super(fields) - this.isSessionIdUsingChatMessageId = fields.isSessionIdUsingChatMessageId this.lastN = fields.k } async loadMemoryVariables(values: InputValues, overrideSessionId = ''): Promise { if (overrideSessionId) { - super.sessionId = overrideSessionId + this.sessionId = overrideSessionId } return super.loadMemoryVariables({ ...values, lastN: this.lastN }) } async saveContext(inputValues: InputValues, outputValues: OutputValues, overrideSessionId = ''): Promise { if (overrideSessionId) { - super.sessionId = overrideSessionId + this.sessionId = overrideSessionId } return super.saveContext(inputValues, outputValues) } async clear(overrideSessionId = ''): Promise { if (overrideSessionId) { - super.sessionId = overrideSessionId + this.sessionId = overrideSessionId } return super.clear() } - async getChatMessages(overrideSessionId = ''): Promise { + async getChatMessages(overrideSessionId = '', returnBaseMessages = false): Promise { const id = overrideSessionId ?? this.sessionId const memoryVariables = await this.loadMemoryVariables({}, id) const baseMessages = memoryVariables[this.memoryKey] - return convertBaseMessagetoIMessage(baseMessages) + return returnBaseMessages ? baseMessages : convertBaseMessagetoIMessage(baseMessages) } async addChatMessages(msgArray: { text: string; type: MessageType }[], overrideSessionId = ''): Promise { diff --git a/packages/components/nodes/memory/ZepMemory/zep.png b/packages/components/nodes/memory/ZepMemory/zep.png deleted file mode 100644 index 2fdb2382..00000000 Binary files a/packages/components/nodes/memory/ZepMemory/zep.png and /dev/null differ diff --git a/packages/components/nodes/memory/ZepMemory/zep.svg b/packages/components/nodes/memory/ZepMemory/zep.svg new file mode 100644 index 00000000..6cbbaad2 --- /dev/null +++ b/packages/components/nodes/memory/ZepMemory/zep.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/packages/components/nodes/moderation/OpenAIModeration/OpenAIModeration.ts b/packages/components/nodes/moderation/OpenAIModeration/OpenAIModeration.ts index 85b27907..8f780605 100644 --- a/packages/components/nodes/moderation/OpenAIModeration/OpenAIModeration.ts +++ b/packages/components/nodes/moderation/OpenAIModeration/OpenAIModeration.ts @@ -20,7 +20,7 @@ class OpenAIModeration implements INode { this.name = 'inputModerationOpenAI' this.version = 1.0 this.type = 'Moderation' - this.icon = 'openai.png' + this.icon = 'openai.svg' this.category = 'Moderation' this.description = 'Check whether content complies with OpenAI usage policies.' this.baseClasses = [this.type, ...getBaseClasses(Moderation)] diff --git a/packages/components/nodes/moderation/OpenAIModeration/openai.png b/packages/components/nodes/moderation/OpenAIModeration/openai.png deleted file mode 100644 index de08a05b..00000000 Binary files a/packages/components/nodes/moderation/OpenAIModeration/openai.png and /dev/null differ diff --git a/packages/components/nodes/moderation/OpenAIModeration/openai.svg b/packages/components/nodes/moderation/OpenAIModeration/openai.svg new file mode 100644 index 00000000..e288e6bd --- /dev/null +++ b/packages/components/nodes/moderation/OpenAIModeration/openai.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/packages/components/nodes/moderation/SimplePromptModeration/SimplePromptModeration.ts b/packages/components/nodes/moderation/SimplePromptModeration/SimplePromptModeration.ts index bf5a32f6..ad3cfadd 100644 --- a/packages/components/nodes/moderation/SimplePromptModeration/SimplePromptModeration.ts +++ b/packages/components/nodes/moderation/SimplePromptModeration/SimplePromptModeration.ts @@ -2,6 +2,7 @@ import { INode, INodeData, INodeParams } from '../../../src/Interface' import { getBaseClasses } from '../../../src' import { Moderation } from '../Moderation' import { SimplePromptModerationRunner } from './SimplePromptModerationRunner' +import { BaseChatModel } from 'langchain/chat_models/base' class SimplePromptModeration implements INode { label: string @@ -17,9 +18,9 @@ class SimplePromptModeration implements INode { constructor() { this.label = 'Simple Prompt Moderation' this.name = 'inputModerationSimple' - this.version = 1.0 + this.version = 2.0 this.type = 'Moderation' - this.icon = 'simple_moderation.png' + this.icon = 'moderation.svg' this.category = 'Moderation' this.description = 'Check whether input consists of any text from Deny list, and prevent being sent to LLM' this.baseClasses = [this.type, ...getBaseClasses(Moderation)] @@ -30,8 +31,14 @@ class SimplePromptModeration implements INode { type: 'string', rows: 4, placeholder: `ignore previous instructions\ndo not follow the directions\nyou must ignore all previous instructions`, - description: 'An array of string literals (enter one per line) that should not appear in the prompt text.', - optional: false + description: 'An array of string literals (enter one per line) that should not appear in the prompt text.' + }, + { + label: 'Chat Model', + name: 'model', + type: 'BaseChatModel', + description: 'Use LLM to detect if the input is similar to those specified in Deny List', + optional: true }, { label: 'Error Message', @@ -46,9 +53,10 @@ class SimplePromptModeration implements INode { async init(nodeData: INodeData): Promise { const denyList = nodeData.inputs?.denyList as string + const model = nodeData.inputs?.model as BaseChatModel const moderationErrorMessage = nodeData.inputs?.moderationErrorMessage as string - return new SimplePromptModerationRunner(denyList, moderationErrorMessage) + return new SimplePromptModerationRunner(denyList, moderationErrorMessage, model) } } diff --git a/packages/components/nodes/moderation/SimplePromptModeration/SimplePromptModerationRunner.ts b/packages/components/nodes/moderation/SimplePromptModeration/SimplePromptModerationRunner.ts index 08f9ed1e..c9a11643 100644 --- a/packages/components/nodes/moderation/SimplePromptModeration/SimplePromptModerationRunner.ts +++ b/packages/components/nodes/moderation/SimplePromptModeration/SimplePromptModerationRunner.ts @@ -1,23 +1,39 @@ import { Moderation } from '../Moderation' +import { BaseChatModel } from 'langchain/chat_models/base' export class SimplePromptModerationRunner implements Moderation { private readonly denyList: string = '' private readonly moderationErrorMessage: string = '' + private readonly model: BaseChatModel - constructor(denyList: string, moderationErrorMessage: string) { + constructor(denyList: string, moderationErrorMessage: string, model?: BaseChatModel) { this.denyList = denyList if (denyList.indexOf('\n') === -1) { this.denyList += '\n' } this.moderationErrorMessage = moderationErrorMessage + if (model) this.model = model } async checkForViolations(input: string): Promise { - this.denyList.split('\n').forEach((denyListItem) => { - if (denyListItem && denyListItem !== '' && input.toLowerCase().includes(denyListItem.toLowerCase())) { - throw Error(this.moderationErrorMessage) + if (this.model) { + const denyArray = this.denyList.split('\n') + for (const denyStr of denyArray) { + if (!denyStr || denyStr === '') continue + const res = await this.model.invoke( + `Are these two sentences similar to each other? Only return Yes or No.\nFirst sentence: ${input}\nSecond sentence: ${denyStr}` + ) + if (res.content.toString().toLowerCase().includes('yes')) { + throw Error(this.moderationErrorMessage) + } } - }) + } else { + this.denyList.split('\n').forEach((denyListItem) => { + if (denyListItem && denyListItem !== '' && input.toLowerCase().includes(denyListItem.toLowerCase())) { + throw Error(this.moderationErrorMessage) + } + }) + } return Promise.resolve(input) } } diff --git a/packages/components/nodes/moderation/SimplePromptModeration/moderation.svg b/packages/components/nodes/moderation/SimplePromptModeration/moderation.svg new file mode 100644 index 00000000..77bc3a1c --- /dev/null +++ b/packages/components/nodes/moderation/SimplePromptModeration/moderation.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/components/nodes/moderation/SimplePromptModeration/simple_moderation.png b/packages/components/nodes/moderation/SimplePromptModeration/simple_moderation.png deleted file mode 100644 index 47d78cb6..00000000 Binary files a/packages/components/nodes/moderation/SimplePromptModeration/simple_moderation.png and /dev/null differ diff --git a/packages/components/nodes/outputparsers/CSVListOutputParser/CSVListOutputParser.ts b/packages/components/nodes/outputparsers/CSVListOutputParser/CSVListOutputParser.ts index 8758d4f7..7b3e7e67 100644 --- a/packages/components/nodes/outputparsers/CSVListOutputParser/CSVListOutputParser.ts +++ b/packages/components/nodes/outputparsers/CSVListOutputParser/CSVListOutputParser.ts @@ -21,7 +21,7 @@ class CSVListOutputParser implements INode { this.version = 1.0 this.type = 'CSVListOutputParser' this.description = 'Parse the output of an LLM call as a comma-separated list of values' - this.icon = 'csv.png' + this.icon = 'csv.svg' this.category = CATEGORY this.baseClasses = [this.type, ...getBaseClasses(BaseOutputParser)] this.inputs = [ diff --git a/packages/components/nodes/outputparsers/CSVListOutputParser/csv.png b/packages/components/nodes/outputparsers/CSVListOutputParser/csv.png deleted file mode 100644 index 41b84e16..00000000 Binary files a/packages/components/nodes/outputparsers/CSVListOutputParser/csv.png and /dev/null differ diff --git a/packages/components/nodes/outputparsers/CSVListOutputParser/csv.svg b/packages/components/nodes/outputparsers/CSVListOutputParser/csv.svg new file mode 100644 index 00000000..e23791b6 --- /dev/null +++ b/packages/components/nodes/outputparsers/CSVListOutputParser/csv.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/components/nodes/outputparsers/CustomListOutputParser/CustomListOutputParser.ts b/packages/components/nodes/outputparsers/CustomListOutputParser/CustomListOutputParser.ts index 06523afb..d420a88d 100644 --- a/packages/components/nodes/outputparsers/CustomListOutputParser/CustomListOutputParser.ts +++ b/packages/components/nodes/outputparsers/CustomListOutputParser/CustomListOutputParser.ts @@ -21,7 +21,7 @@ class CustomListOutputParser implements INode { this.version = 1.0 this.type = 'CustomListOutputParser' this.description = 'Parse the output of an LLM call as a list of values.' - this.icon = 'list.png' + this.icon = 'list.svg' this.category = CATEGORY this.baseClasses = [this.type, ...getBaseClasses(BaseOutputParser)] this.inputs = [ diff --git a/packages/components/nodes/outputparsers/CustomListOutputParser/list.png b/packages/components/nodes/outputparsers/CustomListOutputParser/list.png deleted file mode 100644 index acb4e5d6..00000000 Binary files a/packages/components/nodes/outputparsers/CustomListOutputParser/list.png and /dev/null differ diff --git a/packages/components/nodes/outputparsers/CustomListOutputParser/list.svg b/packages/components/nodes/outputparsers/CustomListOutputParser/list.svg new file mode 100644 index 00000000..c7dad9a9 --- /dev/null +++ b/packages/components/nodes/outputparsers/CustomListOutputParser/list.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/packages/components/nodes/outputparsers/StructuredOutputParser/StructuredOutputParser.ts b/packages/components/nodes/outputparsers/StructuredOutputParser/StructuredOutputParser.ts index 10a5f0bb..fc28fd1c 100644 --- a/packages/components/nodes/outputparsers/StructuredOutputParser/StructuredOutputParser.ts +++ b/packages/components/nodes/outputparsers/StructuredOutputParser/StructuredOutputParser.ts @@ -22,7 +22,7 @@ class StructuredOutputParser implements INode { this.version = 1.0 this.type = 'StructuredOutputParser' this.description = 'Parse the output of an LLM call into a given (JSON) structure.' - this.icon = 'structure.png' + this.icon = 'structure.svg' this.category = CATEGORY this.baseClasses = [this.type, ...getBaseClasses(BaseOutputParser)] this.inputs = [ diff --git a/packages/components/nodes/outputparsers/StructuredOutputParser/structure.png b/packages/components/nodes/outputparsers/StructuredOutputParser/structure.png deleted file mode 100644 index c56b2dd7..00000000 Binary files a/packages/components/nodes/outputparsers/StructuredOutputParser/structure.png and /dev/null differ diff --git a/packages/components/nodes/outputparsers/StructuredOutputParser/structure.svg b/packages/components/nodes/outputparsers/StructuredOutputParser/structure.svg new file mode 100644 index 00000000..3875982d --- /dev/null +++ b/packages/components/nodes/outputparsers/StructuredOutputParser/structure.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/packages/components/nodes/prompts/ChatPromptTemplate/prompt.svg b/packages/components/nodes/prompts/ChatPromptTemplate/prompt.svg index 7e486118..3f666d94 100644 --- a/packages/components/nodes/prompts/ChatPromptTemplate/prompt.svg +++ b/packages/components/nodes/prompts/ChatPromptTemplate/prompt.svg @@ -1,6 +1,5 @@ - - - - - - \ No newline at end of file + + + + + diff --git a/packages/components/nodes/prompts/FewShotPromptTemplate/prompt.svg b/packages/components/nodes/prompts/FewShotPromptTemplate/prompt.svg index 7e486118..e3a0c868 100644 --- a/packages/components/nodes/prompts/FewShotPromptTemplate/prompt.svg +++ b/packages/components/nodes/prompts/FewShotPromptTemplate/prompt.svg @@ -1,6 +1,5 @@ - - - - - - \ No newline at end of file + + + + + diff --git a/packages/components/nodes/prompts/PromptTemplate/prompt.svg b/packages/components/nodes/prompts/PromptTemplate/prompt.svg index 7e486118..e3a0c868 100644 --- a/packages/components/nodes/prompts/PromptTemplate/prompt.svg +++ b/packages/components/nodes/prompts/PromptTemplate/prompt.svg @@ -1,6 +1,5 @@ - - - - - - \ No newline at end of file + + + + + diff --git a/packages/components/nodes/retrievers/CohereRerankRetriever/Cohere.svg b/packages/components/nodes/retrievers/CohereRerankRetriever/Cohere.svg new file mode 100644 index 00000000..88bcabe3 --- /dev/null +++ b/packages/components/nodes/retrievers/CohereRerankRetriever/Cohere.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/retrievers/CohereRerankRetriever/CohereRerank.ts b/packages/components/nodes/retrievers/CohereRerankRetriever/CohereRerank.ts new file mode 100644 index 00000000..f74b8365 --- /dev/null +++ b/packages/components/nodes/retrievers/CohereRerankRetriever/CohereRerank.ts @@ -0,0 +1,55 @@ +import { Callbacks } from 'langchain/callbacks' +import { Document } from 'langchain/document' +import { BaseDocumentCompressor } from 'langchain/retrievers/document_compressors' +import axios from 'axios' +export class CohereRerank extends BaseDocumentCompressor { + private cohereAPIKey: any + private COHERE_API_URL = 'https://api.cohere.ai/v1/rerank' + private readonly model: string + private readonly k: number + private readonly maxChunksPerDoc: number + constructor(cohereAPIKey: string, model: string, k: number, maxChunksPerDoc: number) { + super() + this.cohereAPIKey = cohereAPIKey + this.model = model + this.k = k + this.maxChunksPerDoc = maxChunksPerDoc + } + async compressDocuments( + documents: Document>[], + query: string, + _?: Callbacks | undefined + ): Promise>[]> { + // avoid empty api call + if (documents.length === 0) { + return [] + } + const config = { + headers: { + Authorization: `Bearer ${this.cohereAPIKey}`, + 'Content-Type': 'application/json', + Accept: 'application/json' + } + } + const data = { + model: this.model, + topN: this.k, + max_chunks_per_doc: this.maxChunksPerDoc, + query: query, + return_documents: false, + documents: documents.map((doc) => doc.pageContent) + } + try { + let returnedDocs = await axios.post(this.COHERE_API_URL, data, config) + const finalResults: Document>[] = [] + returnedDocs.data.results.forEach((result: any) => { + const doc = documents[result.index] + doc.metadata.relevance_score = result.relevance_score + finalResults.push(doc) + }) + return finalResults + } catch (error) { + return documents + } + } +} diff --git a/packages/components/nodes/retrievers/CohereRerankRetriever/CohereRerankRetriever.ts b/packages/components/nodes/retrievers/CohereRerankRetriever/CohereRerankRetriever.ts new file mode 100644 index 00000000..442fdc7a --- /dev/null +++ b/packages/components/nodes/retrievers/CohereRerankRetriever/CohereRerankRetriever.ts @@ -0,0 +1,142 @@ +import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' +import { BaseRetriever } from 'langchain/schema/retriever' +import { ContextualCompressionRetriever } from 'langchain/retrievers/contextual_compression' +import { getCredentialData, getCredentialParam, handleEscapeCharacters } from '../../../src' +import { CohereRerank } from './CohereRerank' +import { VectorStoreRetriever } from 'langchain/vectorstores/base' + +class CohereRerankRetriever_Retrievers implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + baseClasses: string[] + inputs: INodeParams[] + credential: INodeParams + badge: string + outputs: INodeOutputsValue[] + + constructor() { + this.label = 'Cohere Rerank Retriever' + this.name = 'cohereRerankRetriever' + this.version = 1.0 + this.type = 'Cohere Rerank Retriever' + this.icon = 'Cohere.svg' + this.category = 'Retrievers' + this.badge = 'NEW' + this.description = 'Cohere Rerank indexes the documents from most to least semantically relevant to the query.' + this.baseClasses = [this.type, 'BaseRetriever'] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['cohereApi'] + } + this.inputs = [ + { + label: 'Vector Store Retriever', + name: 'baseRetriever', + type: 'VectorStoreRetriever' + }, + { + label: 'Model Name', + name: 'model', + type: 'options', + options: [ + { + label: 'rerank-english-v2.0', + name: 'rerank-english-v2.0' + }, + { + label: 'rerank-multilingual-v2.0', + name: 'rerank-multilingual-v2.0' + } + ], + default: 'rerank-english-v2.0', + optional: true + }, + { + label: 'Query', + name: 'query', + type: 'string', + description: 'Query to retrieve documents from retriever. If not specified, user question will be used', + optional: true, + acceptVariable: true + }, + { + label: 'Top K', + name: 'topK', + description: 'Number of top results to fetch. Default to the TopK of the Base Retriever', + placeholder: '4', + type: 'number', + additionalParams: true, + optional: true + }, + { + label: 'Max Chunks Per Doc', + name: 'maxChunksPerDoc', + description: 'The maximum number of chunks to produce internally from a document. Default to 10', + placeholder: '10', + type: 'number', + additionalParams: true, + optional: true + } + ] + this.outputs = [ + { + label: 'Cohere Rerank Retriever', + name: 'retriever', + baseClasses: this.baseClasses + }, + { + label: 'Document', + name: 'document', + baseClasses: ['Document'] + }, + { + label: 'Text', + name: 'text', + baseClasses: ['string', 'json'] + } + ] + } + + async init(nodeData: INodeData, input: string, options: ICommonObject): Promise { + const baseRetriever = nodeData.inputs?.baseRetriever as BaseRetriever + const model = nodeData.inputs?.model as string + const query = nodeData.inputs?.query as string + const credentialData = await getCredentialData(nodeData.credential ?? '', options) + const cohereApiKey = getCredentialParam('cohereApiKey', credentialData, nodeData) + const topK = nodeData.inputs?.topK as string + const k = topK ? parseFloat(topK) : (baseRetriever as VectorStoreRetriever).k ?? 4 + const maxChunksPerDoc = nodeData.inputs?.maxChunksPerDoc as string + const max_chunks_per_doc = maxChunksPerDoc ? parseFloat(maxChunksPerDoc) : 10 + const output = nodeData.outputs?.output as string + + const cohereCompressor = new CohereRerank(cohereApiKey, model, k, max_chunks_per_doc) + + const retriever = new ContextualCompressionRetriever({ + baseCompressor: cohereCompressor, + baseRetriever: baseRetriever + }) + + if (output === 'retriever') return retriever + else if (output === 'document') return await retriever.getRelevantDocuments(query ? query : input) + else if (output === 'text') { + let finaltext = '' + + const docs = await retriever.getRelevantDocuments(query ? query : input) + + for (const doc of docs) finaltext += `${doc.pageContent}\n` + + return handleEscapeCharacters(finaltext, false) + } + + return retriever + } +} + +module.exports = { nodeClass: CohereRerankRetriever_Retrievers } diff --git a/packages/components/nodes/retrievers/EmbeddingsFilterRetriever/EmbeddingsFilterRetriever.ts b/packages/components/nodes/retrievers/EmbeddingsFilterRetriever/EmbeddingsFilterRetriever.ts new file mode 100644 index 00000000..d1049fa4 --- /dev/null +++ b/packages/components/nodes/retrievers/EmbeddingsFilterRetriever/EmbeddingsFilterRetriever.ts @@ -0,0 +1,133 @@ +import { INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' +import { BaseRetriever } from 'langchain/schema/retriever' +import { Embeddings } from 'langchain/embeddings/base' +import { ContextualCompressionRetriever } from 'langchain/retrievers/contextual_compression' +import { EmbeddingsFilter } from 'langchain/retrievers/document_compressors/embeddings_filter' +import { handleEscapeCharacters } from '../../../src/utils' + +class EmbeddingsFilterRetriever_Retrievers implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + baseClasses: string[] + inputs: INodeParams[] + outputs: INodeOutputsValue[] + badge: string + + constructor() { + this.label = 'Embeddings Filter Retriever' + this.name = 'embeddingsFilterRetriever' + this.version = 1.0 + this.type = 'EmbeddingsFilterRetriever' + this.icon = 'compressionRetriever.svg' + this.category = 'Retrievers' + this.badge = 'NEW' + this.description = 'A document compressor that uses embeddings to drop documents unrelated to the query' + this.baseClasses = [this.type, 'BaseRetriever'] + this.inputs = [ + { + label: 'Vector Store Retriever', + name: 'baseRetriever', + type: 'VectorStoreRetriever' + }, + { + label: 'Embeddings', + name: 'embeddings', + type: 'Embeddings' + }, + { + label: 'Query', + name: 'query', + type: 'string', + description: 'Query to retrieve documents from retriever. If not specified, user question will be used', + optional: true, + acceptVariable: true + }, + { + label: 'Similarity Threshold', + name: 'similarityThreshold', + description: + 'Threshold for determining when two documents are similar enough to be considered redundant. Must be specified if `k` is not set', + type: 'number', + default: 0.8, + step: 0.1, + optional: true + }, + { + label: 'K', + name: 'k', + description: + 'The number of relevant documents to return. Can be explicitly set to undefined, in which case similarity_threshold must be specified. Defaults to 20', + type: 'number', + default: 20, + step: 1, + optional: true, + additionalParams: true + } + ] + this.outputs = [ + { + label: 'Embeddings Filter Retriever', + name: 'retriever', + baseClasses: this.baseClasses + }, + { + label: 'Document', + name: 'document', + baseClasses: ['Document'] + }, + { + label: 'Text', + name: 'text', + baseClasses: ['string', 'json'] + } + ] + } + + async init(nodeData: INodeData, input: string): Promise { + const baseRetriever = nodeData.inputs?.baseRetriever as BaseRetriever + const embeddings = nodeData.inputs?.embeddings as Embeddings + const query = nodeData.inputs?.query as string + const similarityThreshold = nodeData.inputs?.similarityThreshold as string + const k = nodeData.inputs?.k as string + const output = nodeData.outputs?.output as string + + if (k === undefined && similarityThreshold === undefined) { + throw new Error(`Must specify one of "k" or "similarity_threshold".`) + } + + const similarityThresholdNumber = similarityThreshold ? parseFloat(similarityThreshold) : 0.8 + const kNumber = k ? parseFloat(k) : undefined + + const baseCompressor = new EmbeddingsFilter({ + embeddings: embeddings, + similarityThreshold: similarityThresholdNumber, + k: kNumber + }) + + const retriever = new ContextualCompressionRetriever({ + baseCompressor, + baseRetriever: baseRetriever + }) + + if (output === 'retriever') return retriever + else if (output === 'document') return await retriever.getRelevantDocuments(query ? query : input) + else if (output === 'text') { + let finaltext = '' + + const docs = await retriever.getRelevantDocuments(query ? query : input) + + for (const doc of docs) finaltext += `${doc.pageContent}\n` + + return handleEscapeCharacters(finaltext, false) + } + + return retriever + } +} + +module.exports = { nodeClass: EmbeddingsFilterRetriever_Retrievers } diff --git a/packages/components/nodes/retrievers/EmbeddingsFilterRetriever/compressionRetriever.svg b/packages/components/nodes/retrievers/EmbeddingsFilterRetriever/compressionRetriever.svg new file mode 100644 index 00000000..23c52d25 --- /dev/null +++ b/packages/components/nodes/retrievers/EmbeddingsFilterRetriever/compressionRetriever.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/packages/components/nodes/retrievers/HydeRetriever/HydeRetriever.ts b/packages/components/nodes/retrievers/HydeRetriever/HydeRetriever.ts index 9ec7ada0..10fff764 100644 --- a/packages/components/nodes/retrievers/HydeRetriever/HydeRetriever.ts +++ b/packages/components/nodes/retrievers/HydeRetriever/HydeRetriever.ts @@ -1,8 +1,9 @@ import { VectorStore } from 'langchain/vectorstores/base' -import { INode, INodeData, INodeParams } from '../../../src/Interface' +import { INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' import { HydeRetriever, HydeRetrieverOptions, PromptKey } from 'langchain/retrievers/hyde' import { BaseLanguageModel } from 'langchain/base_language' import { PromptTemplate } from 'langchain/prompts' +import { handleEscapeCharacters } from '../../../src/utils' class HydeRetriever_Retrievers implements INode { label: string @@ -14,11 +15,12 @@ class HydeRetriever_Retrievers implements INode { category: string baseClasses: string[] inputs: INodeParams[] + outputs: INodeOutputsValue[] constructor() { - this.label = 'Hyde Retriever' + this.label = 'HyDE Retriever' this.name = 'HydeRetriever' - this.version = 1.0 + this.version = 3.0 this.type = 'HydeRetriever' this.icon = 'hyderetriever.svg' this.category = 'Retrievers' @@ -36,41 +38,74 @@ class HydeRetriever_Retrievers implements INode { type: 'VectorStore' }, { - label: 'Prompt Key', + label: 'Query', + name: 'query', + type: 'string', + description: 'Query to retrieve documents from retriever. If not specified, user question will be used', + optional: true, + acceptVariable: true + }, + { + label: 'Select Defined Prompt', name: 'promptKey', + description: 'Select a pre-defined prompt', type: 'options', options: [ { label: 'websearch', - name: 'websearch' + name: 'websearch', + description: `Please write a passage to answer the question +Question: {question} +Passage:` }, { label: 'scifact', - name: 'scifact' + name: 'scifact', + description: `Please write a scientific paper passage to support/refute the claim +Claim: {question} +Passage:` }, { label: 'arguana', - name: 'arguana' + name: 'arguana', + description: `Please write a counter argument for the passage +Passage: {question} +Counter Argument:` }, { label: 'trec-covid', - name: 'trec-covid' + name: 'trec-covid', + description: `Please write a scientific paper passage to answer the question +Question: {question} +Passage:` }, { label: 'fiqa', - name: 'fiqa' + name: 'fiqa', + description: `Please write a financial article passage to answer the question +Question: {question} +Passage:` }, { label: 'dbpedia-entity', - name: 'dbpedia-entity' + name: 'dbpedia-entity', + description: `Please write a passage to answer the question. +Question: {question} +Passage:` }, { label: 'trec-news', - name: 'trec-news' + name: 'trec-news', + description: `Please write a news passage about the topic. +Topic: {question} +Passage:` }, { label: 'mr-tydi', - name: 'mr-tydi' + name: 'mr-tydi', + description: `Please write a passage in Swahili/Korean/Japanese/Bengali to answer the question in detail. +Question: {question} +Passage:` } ], default: 'websearch' @@ -78,7 +113,7 @@ class HydeRetriever_Retrievers implements INode { { label: 'Custom Prompt', name: 'customPrompt', - description: 'If custom prompt is used, this will override Prompt Key', + description: 'If custom prompt is used, this will override Defined Prompt', placeholder: 'Please write a passage to answer the question\nQuestion: {question}\nPassage:', type: 'string', rows: 4, @@ -96,15 +131,34 @@ class HydeRetriever_Retrievers implements INode { optional: true } ] + this.outputs = [ + { + label: 'HyDE Retriever', + name: 'retriever', + baseClasses: this.baseClasses + }, + { + label: 'Document', + name: 'document', + baseClasses: ['Document'] + }, + { + label: 'Text', + name: 'text', + baseClasses: ['string', 'json'] + } + ] } - async init(nodeData: INodeData): Promise { + async init(nodeData: INodeData, input: string): Promise { const llm = nodeData.inputs?.model as BaseLanguageModel const vectorStore = nodeData.inputs?.vectorStore as VectorStore const promptKey = nodeData.inputs?.promptKey as PromptKey const customPrompt = nodeData.inputs?.customPrompt as string + const query = nodeData.inputs?.query as string const topK = nodeData.inputs?.topK as string const k = topK ? parseFloat(topK) : 4 + const output = nodeData.outputs?.output as string const obj: HydeRetrieverOptions = { llm, @@ -116,6 +170,19 @@ class HydeRetriever_Retrievers implements INode { else if (promptKey) obj.promptTemplate = promptKey const retriever = new HydeRetriever(obj) + + if (output === 'retriever') return retriever + else if (output === 'document') return await retriever.getRelevantDocuments(query ? query : input) + else if (output === 'text') { + let finaltext = '' + + const docs = await retriever.getRelevantDocuments(query ? query : input) + + for (const doc of docs) finaltext += `${doc.pageContent}\n` + + return handleEscapeCharacters(finaltext, false) + } + return retriever } } diff --git a/packages/components/nodes/retrievers/HydeRetriever/hyderetriever.svg b/packages/components/nodes/retrievers/HydeRetriever/hyderetriever.svg index da3a9f20..322939e4 100644 --- a/packages/components/nodes/retrievers/HydeRetriever/hyderetriever.svg +++ b/packages/components/nodes/retrievers/HydeRetriever/hyderetriever.svg @@ -1,9 +1,4 @@ - - - - - - - - - \ No newline at end of file + + + + diff --git a/packages/components/nodes/retrievers/LLMFilterRetriever/LLMFilterCompressionRetriever.ts b/packages/components/nodes/retrievers/LLMFilterRetriever/LLMFilterCompressionRetriever.ts new file mode 100644 index 00000000..6b710cf3 --- /dev/null +++ b/packages/components/nodes/retrievers/LLMFilterRetriever/LLMFilterCompressionRetriever.ts @@ -0,0 +1,100 @@ +import { INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' +import { BaseRetriever } from 'langchain/schema/retriever' +import { ContextualCompressionRetriever } from 'langchain/retrievers/contextual_compression' +import { BaseLanguageModel } from 'langchain/base_language' +import { LLMChainExtractor } from 'langchain/retrievers/document_compressors/chain_extract' +import { handleEscapeCharacters } from '../../../src/utils' + +class LLMFilterCompressionRetriever_Retrievers implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + baseClasses: string[] + inputs: INodeParams[] + outputs: INodeOutputsValue[] + badge: string + + constructor() { + this.label = 'LLM Filter Retriever' + this.name = 'llmFilterRetriever' + this.version = 1.0 + this.type = 'LLMFilterRetriever' + this.icon = 'llmFilterRetriever.svg' + this.category = 'Retrievers' + this.badge = 'NEW' + this.description = + 'Iterate over the initially returned documents and extract, from each, only the content that is relevant to the query' + this.baseClasses = [this.type, 'BaseRetriever'] + this.inputs = [ + { + label: 'Vector Store Retriever', + name: 'baseRetriever', + type: 'VectorStoreRetriever' + }, + { + label: 'Language Model', + name: 'model', + type: 'BaseLanguageModel' + }, + { + label: 'Query', + name: 'query', + type: 'string', + description: 'Query to retrieve documents from retriever. If not specified, user question will be used', + optional: true, + acceptVariable: true + } + ] + this.outputs = [ + { + label: 'LLM Filter Retriever', + name: 'retriever', + baseClasses: this.baseClasses + }, + { + label: 'Document', + name: 'document', + baseClasses: ['Document'] + }, + { + label: 'Text', + name: 'text', + baseClasses: ['string', 'json'] + } + ] + } + + async init(nodeData: INodeData, input: string): Promise { + const baseRetriever = nodeData.inputs?.baseRetriever as BaseRetriever + const model = nodeData.inputs?.model as BaseLanguageModel + const query = nodeData.inputs?.query as string + const output = nodeData.outputs?.output as string + + if (!model) throw new Error('There must be a LLM model connected to LLM Filter Retriever') + + const retriever = new ContextualCompressionRetriever({ + baseCompressor: LLMChainExtractor.fromLLM(model), + baseRetriever: baseRetriever + }) + + if (output === 'retriever') return retriever + else if (output === 'document') return await retriever.getRelevantDocuments(query ? query : input) + else if (output === 'text') { + let finaltext = '' + + const docs = await retriever.getRelevantDocuments(query ? query : input) + + for (const doc of docs) finaltext += `${doc.pageContent}\n` + + return handleEscapeCharacters(finaltext, false) + } + + return retriever + } +} + +module.exports = { nodeClass: LLMFilterCompressionRetriever_Retrievers } diff --git a/packages/components/nodes/retrievers/LLMFilterRetriever/llmFilterRetriever.svg b/packages/components/nodes/retrievers/LLMFilterRetriever/llmFilterRetriever.svg new file mode 100644 index 00000000..d3f4d15f --- /dev/null +++ b/packages/components/nodes/retrievers/LLMFilterRetriever/llmFilterRetriever.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/retrievers/PromptRetriever/promptretriever.svg b/packages/components/nodes/retrievers/PromptRetriever/promptretriever.svg index db48e8a5..f32ffa7b 100644 --- a/packages/components/nodes/retrievers/PromptRetriever/promptretriever.svg +++ b/packages/components/nodes/retrievers/PromptRetriever/promptretriever.svg @@ -1,8 +1,5 @@ - - - - - - - - \ No newline at end of file + + + + + diff --git a/packages/components/nodes/retrievers/RRFRetriever/RRFRetriever.ts b/packages/components/nodes/retrievers/RRFRetriever/RRFRetriever.ts new file mode 100644 index 00000000..ed15ed24 --- /dev/null +++ b/packages/components/nodes/retrievers/RRFRetriever/RRFRetriever.ts @@ -0,0 +1,136 @@ +import { INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' +import { BaseLanguageModel } from 'langchain/base_language' +import { ContextualCompressionRetriever } from 'langchain/retrievers/contextual_compression' +import { BaseRetriever } from 'langchain/schema/retriever' +import { ReciprocalRankFusion } from './ReciprocalRankFusion' +import { VectorStoreRetriever } from 'langchain/vectorstores/base' +import { handleEscapeCharacters } from '../../../src/utils' + +class RRFRetriever_Retrievers implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + baseClasses: string[] + inputs: INodeParams[] + badge: string + outputs: INodeOutputsValue[] + + constructor() { + this.label = 'Reciprocal Rank Fusion Retriever' + this.name = 'RRFRetriever' + this.version = 1.0 + this.type = 'RRFRetriever' + this.badge = 'NEW' + this.icon = 'rrfRetriever.svg' + this.category = 'Retrievers' + this.description = 'Reciprocal Rank Fusion to re-rank search results by multiple query generation.' + this.baseClasses = [this.type, 'BaseRetriever'] + this.inputs = [ + { + label: 'Vector Store Retriever', + name: 'baseRetriever', + type: 'VectorStoreRetriever' + }, + { + label: 'Language Model', + name: 'model', + type: 'BaseLanguageModel' + }, + { + label: 'Query', + name: 'query', + type: 'string', + description: 'Query to retrieve documents from retriever. If not specified, user question will be used', + optional: true, + acceptVariable: true + }, + { + label: 'Query Count', + name: 'queryCount', + description: 'Number of synthetic queries to generate. Default to 4', + placeholder: '4', + type: 'number', + default: 4, + additionalParams: true, + optional: true + }, + { + label: 'Top K', + name: 'topK', + description: 'Number of top results to fetch. Default to the TopK of the Base Retriever', + placeholder: '0', + type: 'number', + additionalParams: true, + optional: true + }, + { + label: 'Constant', + name: 'c', + description: + 'A constant added to the rank, controlling the balance between the importance of high-ranked items and the consideration given to lower-ranked items.\n' + + 'Default is 60', + placeholder: '60', + type: 'number', + default: 60, + additionalParams: true, + optional: true + } + ] + this.outputs = [ + { + label: 'Reciprocal Rank Fusion Retriever', + name: 'retriever', + baseClasses: this.baseClasses + }, + { + label: 'Document', + name: 'document', + baseClasses: ['Document'] + }, + { + label: 'Text', + name: 'text', + baseClasses: ['string', 'json'] + } + ] + } + + async init(nodeData: INodeData, input: string): Promise { + const llm = nodeData.inputs?.model as BaseLanguageModel + const baseRetriever = nodeData.inputs?.baseRetriever as BaseRetriever + const query = nodeData.inputs?.query as string + const queryCount = nodeData.inputs?.queryCount as string + const q = queryCount ? parseFloat(queryCount) : 4 + const topK = nodeData.inputs?.topK as string + const k = topK ? parseFloat(topK) : (baseRetriever as VectorStoreRetriever).k ?? 4 + const constantC = nodeData.inputs?.c as string + const c = topK ? parseFloat(constantC) : 60 + const output = nodeData.outputs?.output as string + + const ragFusion = new ReciprocalRankFusion(llm, baseRetriever as VectorStoreRetriever, q, k, c) + const retriever = new ContextualCompressionRetriever({ + baseCompressor: ragFusion, + baseRetriever: baseRetriever + }) + + if (output === 'retriever') return retriever + else if (output === 'document') return await retriever.getRelevantDocuments(query ? query : input) + else if (output === 'text') { + let finaltext = '' + + const docs = await retriever.getRelevantDocuments(query ? query : input) + + for (const doc of docs) finaltext += `${doc.pageContent}\n` + + return handleEscapeCharacters(finaltext, false) + } + + return retriever + } +} + +module.exports = { nodeClass: RRFRetriever_Retrievers } diff --git a/packages/components/nodes/retrievers/RRFRetriever/ReciprocalRankFusion.ts b/packages/components/nodes/retrievers/RRFRetriever/ReciprocalRankFusion.ts new file mode 100644 index 00000000..0789ca17 --- /dev/null +++ b/packages/components/nodes/retrievers/RRFRetriever/ReciprocalRankFusion.ts @@ -0,0 +1,96 @@ +import { BaseDocumentCompressor } from 'langchain/retrievers/document_compressors' +import { Document } from 'langchain/document' +import { Callbacks } from 'langchain/callbacks' +import { BaseLanguageModel } from 'langchain/base_language' +import { ChatPromptTemplate, HumanMessagePromptTemplate, SystemMessagePromptTemplate } from 'langchain/prompts' +import { LLMChain } from 'langchain/chains' +import { VectorStoreRetriever } from 'langchain/vectorstores/base' + +export class ReciprocalRankFusion extends BaseDocumentCompressor { + private readonly llm: BaseLanguageModel + private readonly queryCount: number + private readonly topK: number + private readonly c: number + private baseRetriever: VectorStoreRetriever + constructor(llm: BaseLanguageModel, baseRetriever: VectorStoreRetriever, queryCount: number, topK: number, c: number) { + super() + this.queryCount = queryCount + this.llm = llm + this.baseRetriever = baseRetriever + this.topK = topK + this.c = c + } + async compressDocuments( + documents: Document>[], + query: string, + _?: Callbacks | undefined + ): Promise>[]> { + // avoid empty api call + if (documents.length === 0) { + return [] + } + const chatPrompt = ChatPromptTemplate.fromMessages([ + SystemMessagePromptTemplate.fromTemplate( + 'You are a helpful assistant that generates multiple search queries based on a single input query.' + ), + HumanMessagePromptTemplate.fromTemplate( + 'Generate multiple search queries related to: {input}. Provide these alternative questions separated by newlines, do not add any numbers.' + ), + HumanMessagePromptTemplate.fromTemplate('OUTPUT (' + this.queryCount + ' queries):') + ]) + const llmChain = new LLMChain({ + llm: this.llm, + prompt: chatPrompt + }) + const multipleQueries = await llmChain.call({ input: query }) + const queries = [] + queries.push(query) + multipleQueries.text.split('\n').map((q: string) => { + queries.push(q) + }) + const docList: Document>[][] = [] + for (let i = 0; i < queries.length; i++) { + const resultOne = await this.baseRetriever.vectorStore.similaritySearch(queries[i], 5) + const docs: any[] = [] + resultOne.forEach((doc) => { + docs.push(doc) + }) + docList.push(docs) + } + + return this.reciprocalRankFunction(docList, this.c) + } + + reciprocalRankFunction(docList: Document>[][], k: number): Document>[] { + docList.forEach((docs: Document>[]) => { + docs.forEach((doc: any, index: number) => { + let rank = index + 1 + if (doc.metadata.relevancy_score) { + doc.metadata.relevancy_score += 1 / (rank + k) + } else { + doc.metadata.relevancy_score = 1 / (rank + k) + } + }) + }) + const scoreArray: any[] = [] + docList.forEach((docs: Document>[]) => { + docs.forEach((doc: any) => { + scoreArray.push(doc.metadata.relevancy_score) + }) + }) + scoreArray.sort((a, b) => b - a) + const rerankedDocuments: Document>[] = [] + const seenScores: any[] = [] + scoreArray.forEach((score) => { + docList.forEach((docs) => { + docs.forEach((doc: any) => { + if (doc.metadata.relevancy_score === score && seenScores.indexOf(score) === -1) { + rerankedDocuments.push(doc) + seenScores.push(doc.metadata.relevancy_score) + } + }) + }) + }) + return rerankedDocuments.splice(0, this.topK) + } +} diff --git a/packages/components/nodes/retrievers/RRFRetriever/rrfRetriever.svg b/packages/components/nodes/retrievers/RRFRetriever/rrfRetriever.svg new file mode 100644 index 00000000..56fbcc5a --- /dev/null +++ b/packages/components/nodes/retrievers/RRFRetriever/rrfRetriever.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/retrievers/SimilarityThresholdRetriever/SimilarityThresholdRetriever.ts b/packages/components/nodes/retrievers/SimilarityThresholdRetriever/SimilarityThresholdRetriever.ts index a9f4b3d8..5f5a9ed0 100644 --- a/packages/components/nodes/retrievers/SimilarityThresholdRetriever/SimilarityThresholdRetriever.ts +++ b/packages/components/nodes/retrievers/SimilarityThresholdRetriever/SimilarityThresholdRetriever.ts @@ -18,7 +18,7 @@ class SimilarityThresholdRetriever_Retrievers implements INode { constructor() { this.label = 'Similarity Score Threshold Retriever' this.name = 'similarityThresholdRetriever' - this.version = 1.0 + this.version = 2.0 this.type = 'SimilarityThresholdRetriever' this.icon = 'similaritythreshold.svg' this.category = 'Retrievers' @@ -30,6 +30,14 @@ class SimilarityThresholdRetriever_Retrievers implements INode { name: 'vectorStore', type: 'VectorStore' }, + { + label: 'Query', + name: 'query', + type: 'string', + description: 'Query to retrieve documents from retriever. If not specified, user question will be used', + optional: true, + acceptVariable: true + }, { label: 'Minimum Similarity Score (%)', name: 'minSimilarityScore', @@ -44,7 +52,8 @@ class SimilarityThresholdRetriever_Retrievers implements INode { description: `The maximum number of results to fetch`, type: 'number', default: 20, - step: 1 + step: 1, + additionalParams: true }, { label: 'K Increment', @@ -52,7 +61,8 @@ class SimilarityThresholdRetriever_Retrievers implements INode { description: `How much to increase K by each time. It'll fetch N results, then N + kIncrement, then N + kIncrement * 2, etc.`, type: 'number', default: 2, - step: 1 + step: 1, + additionalParams: true } ] this.outputs = [ @@ -77,6 +87,7 @@ class SimilarityThresholdRetriever_Retrievers implements INode { async init(nodeData: INodeData, input: string): Promise { const vectorStore = nodeData.inputs?.vectorStore as VectorStore const minSimilarityScore = nodeData.inputs?.minSimilarityScore as number + const query = nodeData.inputs?.query as string const maxK = nodeData.inputs?.maxK as string const kIncrement = nodeData.inputs?.kIncrement as string @@ -89,11 +100,11 @@ class SimilarityThresholdRetriever_Retrievers implements INode { }) if (output === 'retriever') return retriever - else if (output === 'document') return await retriever.getRelevantDocuments(input) + else if (output === 'document') return await retriever.getRelevantDocuments(query ? query : input) else if (output === 'text') { let finaltext = '' - const docs = await retriever.getRelevantDocuments(input) + const docs = await retriever.getRelevantDocuments(query ? query : input) for (const doc of docs) finaltext += `${doc.pageContent}\n` diff --git a/packages/components/nodes/retrievers/SimilarityThresholdRetriever/similaritythreshold.svg b/packages/components/nodes/retrievers/SimilarityThresholdRetriever/similaritythreshold.svg index 6b918fd8..e82ea3f9 100644 --- a/packages/components/nodes/retrievers/SimilarityThresholdRetriever/similaritythreshold.svg +++ b/packages/components/nodes/retrievers/SimilarityThresholdRetriever/similaritythreshold.svg @@ -1,5 +1,4 @@ - - - - - \ No newline at end of file + + + + diff --git a/packages/components/nodes/retrievers/VectorStoreRetriever/vectorretriever.svg b/packages/components/nodes/retrievers/VectorStoreRetriever/vectorretriever.svg index da3a9f20..9d453432 100644 --- a/packages/components/nodes/retrievers/VectorStoreRetriever/vectorretriever.svg +++ b/packages/components/nodes/retrievers/VectorStoreRetriever/vectorretriever.svg @@ -1,9 +1,4 @@ - - - - - - - - - \ No newline at end of file + + + + diff --git a/packages/components/nodes/textsplitters/CharacterTextSplitter/textsplitter.svg b/packages/components/nodes/textsplitters/CharacterTextSplitter/textsplitter.svg index 73145e2d..5fb03c92 100644 --- a/packages/components/nodes/textsplitters/CharacterTextSplitter/textsplitter.svg +++ b/packages/components/nodes/textsplitters/CharacterTextSplitter/textsplitter.svg @@ -1,7 +1,5 @@ - - - - - - - \ No newline at end of file + + + + + diff --git a/packages/components/nodes/textsplitters/CodeTextSplitter/codeTextSplitter.svg b/packages/components/nodes/textsplitters/CodeTextSplitter/codeTextSplitter.svg index d3b3d188..9d74c891 100644 --- a/packages/components/nodes/textsplitters/CodeTextSplitter/codeTextSplitter.svg +++ b/packages/components/nodes/textsplitters/CodeTextSplitter/codeTextSplitter.svg @@ -1,8 +1,5 @@ - - - - - - - - \ No newline at end of file + + + + + diff --git a/packages/components/nodes/textsplitters/HtmlToMarkdownTextSplitter/htmlToMarkdownTextSplitter.svg b/packages/components/nodes/textsplitters/HtmlToMarkdownTextSplitter/htmlToMarkdownTextSplitter.svg index f7d45d60..b62d20dd 100644 --- a/packages/components/nodes/textsplitters/HtmlToMarkdownTextSplitter/htmlToMarkdownTextSplitter.svg +++ b/packages/components/nodes/textsplitters/HtmlToMarkdownTextSplitter/htmlToMarkdownTextSplitter.svg @@ -1,6 +1,5 @@ - - - - - - \ No newline at end of file + + + + + diff --git a/packages/components/nodes/textsplitters/MarkdownTextSplitter/markdownTextSplitter.svg b/packages/components/nodes/textsplitters/MarkdownTextSplitter/markdownTextSplitter.svg index f7d45d60..b62d20dd 100644 --- a/packages/components/nodes/textsplitters/MarkdownTextSplitter/markdownTextSplitter.svg +++ b/packages/components/nodes/textsplitters/MarkdownTextSplitter/markdownTextSplitter.svg @@ -1,6 +1,5 @@ - - - - - - \ No newline at end of file + + + + + diff --git a/packages/components/nodes/textsplitters/RecursiveCharacterTextSplitter/textsplitter.svg b/packages/components/nodes/textsplitters/RecursiveCharacterTextSplitter/textsplitter.svg index 73145e2d..5fb03c92 100644 --- a/packages/components/nodes/textsplitters/RecursiveCharacterTextSplitter/textsplitter.svg +++ b/packages/components/nodes/textsplitters/RecursiveCharacterTextSplitter/textsplitter.svg @@ -1,7 +1,5 @@ - - - - - - - \ No newline at end of file + + + + + diff --git a/packages/components/nodes/textsplitters/TokenTextSplitter/tiktoken.svg b/packages/components/nodes/textsplitters/TokenTextSplitter/tiktoken.svg index 833cfae1..c7dbe973 100644 --- a/packages/components/nodes/textsplitters/TokenTextSplitter/tiktoken.svg +++ b/packages/components/nodes/textsplitters/TokenTextSplitter/tiktoken.svg @@ -1,7 +1,5 @@ - - - - - - - \ No newline at end of file + + + + + diff --git a/packages/components/nodes/tools/AIPlugin/aiplugin.svg b/packages/components/nodes/tools/AIPlugin/aiplugin.svg index e617e45c..a48cb717 100644 --- a/packages/components/nodes/tools/AIPlugin/aiplugin.svg +++ b/packages/components/nodes/tools/AIPlugin/aiplugin.svg @@ -1,7 +1,6 @@ - - - - - - - \ No newline at end of file + + + + + + diff --git a/packages/components/nodes/tools/BraveSearchAPI/brave.svg b/packages/components/nodes/tools/BraveSearchAPI/brave.svg index 0c0c0e86..b1e23357 100644 --- a/packages/components/nodes/tools/BraveSearchAPI/brave.svg +++ b/packages/components/nodes/tools/BraveSearchAPI/brave.svg @@ -1 +1,8 @@ - \ No newline at end of file + + + + + + + + diff --git a/packages/components/nodes/tools/Calculator/calculator.svg b/packages/components/nodes/tools/Calculator/calculator.svg index 6fa49e15..a0614326 100644 --- a/packages/components/nodes/tools/Calculator/calculator.svg +++ b/packages/components/nodes/tools/Calculator/calculator.svg @@ -1,11 +1,10 @@ - - - - - - - - - - - \ No newline at end of file + + + + + + + + + + diff --git a/packages/components/nodes/tools/ChainTool/chaintool.svg b/packages/components/nodes/tools/ChainTool/chaintool.svg index ab76749b..58b1cb13 100644 --- a/packages/components/nodes/tools/ChainTool/chaintool.svg +++ b/packages/components/nodes/tools/ChainTool/chaintool.svg @@ -1,8 +1,4 @@ - - - - - - - - \ No newline at end of file + + + + diff --git a/packages/components/nodes/tools/CustomTool/CustomTool.ts b/packages/components/nodes/tools/CustomTool/CustomTool.ts index 541edcf0..6ba5bc26 100644 --- a/packages/components/nodes/tools/CustomTool/CustomTool.ts +++ b/packages/components/nodes/tools/CustomTool/CustomTool.ts @@ -1,5 +1,5 @@ import { ICommonObject, IDatabaseEntity, INode, INodeData, INodeOptionsValue, INodeParams } from '../../../src/Interface' -import { convertSchemaToZod, getBaseClasses } from '../../../src/utils' +import { convertSchemaToZod, getBaseClasses, getVars } from '../../../src/utils' import { DynamicStructuredTool } from './core' import { z } from 'zod' import { DataSource } from 'typeorm' @@ -80,7 +80,16 @@ class CustomTool_Tools implements INode { code: tool.func } if (customToolFunc) obj.code = customToolFunc - return new DynamicStructuredTool(obj) + + const variables = await getVars(appDataSource, databaseEntities, nodeData) + + const flow = { chatflowId: options.chatflowid } + + let dynamicStructuredTool = new DynamicStructuredTool(obj) + dynamicStructuredTool.setVariables(variables) + dynamicStructuredTool.setFlowObject(flow) + + return dynamicStructuredTool } catch (e) { throw new Error(e) } diff --git a/packages/components/nodes/tools/CustomTool/core.ts b/packages/components/nodes/tools/CustomTool/core.ts index 12dd72f1..19be88f1 100644 --- a/packages/components/nodes/tools/CustomTool/core.ts +++ b/packages/components/nodes/tools/CustomTool/core.ts @@ -1,38 +1,18 @@ import { z } from 'zod' -import { CallbackManagerForToolRun } from 'langchain/callbacks' -import { StructuredTool, ToolParams } from 'langchain/tools' import { NodeVM } from 'vm2' +import { availableDependencies, defaultAllowBuiltInDep, prepareSandboxVars } from '../../../src/utils' +import { RunnableConfig } from '@langchain/core/runnables' +import { StructuredTool, ToolParams } from '@langchain/core/tools' +import { CallbackManagerForToolRun, Callbacks, CallbackManager, parseCallbackConfigArg } from '@langchain/core/callbacks/manager' -/* - * List of dependencies allowed to be import in vm2 - */ -const availableDependencies = [ - '@dqbd/tiktoken', - '@getzep/zep-js', - '@huggingface/inference', - '@pinecone-database/pinecone', - '@supabase/supabase-js', - 'axios', - 'cheerio', - 'chromadb', - 'cohere-ai', - 'd3-dsv', - 'form-data', - 'graphql', - 'html-to-text', - 'langchain', - 'linkifyjs', - 'mammoth', - 'moment', - 'node-fetch', - 'pdf-parse', - 'pdfjs-dist', - 'playwright', - 'puppeteer', - 'srt-parser-2', - 'typeorm', - 'weaviate-ts-client' -] +class ToolInputParsingException extends Error { + output?: string + + constructor(message: string, output?: string) { + super(message) + this.output = output + } +} export interface BaseDynamicToolInput extends ToolParams { name: string @@ -62,6 +42,8 @@ export class DynamicStructuredTool< func: DynamicStructuredToolInput['func'] schema: T + private variables: any[] + private flowObj: any constructor(fields: DynamicStructuredToolInput) { super(fields) @@ -73,7 +55,56 @@ export class DynamicStructuredTool< this.schema = fields.schema } - protected async _call(arg: z.output): Promise { + async call( + arg: z.output, + configArg?: RunnableConfig | Callbacks, + tags?: string[], + flowConfig?: { sessionId?: string; chatId?: string; input?: string } + ): Promise { + const config = parseCallbackConfigArg(configArg) + if (config.runName === undefined) { + config.runName = this.name + } + let parsed + try { + parsed = await this.schema.parseAsync(arg) + } catch (e) { + throw new ToolInputParsingException(`Received tool input did not match expected schema`, JSON.stringify(arg)) + } + const callbackManager_ = await CallbackManager.configure( + config.callbacks, + this.callbacks, + config.tags || tags, + this.tags, + config.metadata, + this.metadata, + { verbose: this.verbose } + ) + const runManager = await callbackManager_?.handleToolStart( + this.toJSON(), + typeof parsed === 'string' ? parsed : JSON.stringify(parsed), + undefined, + undefined, + undefined, + undefined, + config.runName + ) + let result + try { + result = await this._call(parsed, runManager, flowConfig) + } catch (e) { + await runManager?.handleToolError(e) + throw e + } + await runManager?.handleToolEnd(result) + return result + } + + protected async _call( + arg: z.output, + _?: CallbackManagerForToolRun, + flowConfig?: { sessionId?: string; chatId?: string; input?: string } + ): Promise { let sandbox: any = {} if (typeof arg === 'object' && Object.keys(arg).length) { for (const item in arg) { @@ -81,21 +112,12 @@ export class DynamicStructuredTool< } } - const defaultAllowBuiltInDep = [ - 'assert', - 'buffer', - 'crypto', - 'events', - 'http', - 'https', - 'net', - 'path', - 'querystring', - 'timers', - 'tls', - 'url', - 'zlib' - ] + sandbox['$vars'] = prepareSandboxVars(this.variables) + + // inject flow properties + if (this.flowObj) { + sandbox['$flow'] = { ...this.flowObj, ...flowConfig } + } const builtinDeps = process.env.TOOL_FUNCTION_BUILTIN_DEP ? defaultAllowBuiltInDep.concat(process.env.TOOL_FUNCTION_BUILTIN_DEP.split(',')) @@ -117,4 +139,12 @@ export class DynamicStructuredTool< return response } + + setVariables(variables: any[]) { + this.variables = variables + } + + setFlowObject(flow: any) { + this.flowObj = flow + } } diff --git a/packages/components/nodes/tools/CustomTool/customtool.svg b/packages/components/nodes/tools/CustomTool/customtool.svg index c5bd0fbc..6c1977d2 100644 --- a/packages/components/nodes/tools/CustomTool/customtool.svg +++ b/packages/components/nodes/tools/CustomTool/customtool.svg @@ -1,4 +1,3 @@ - - - - \ No newline at end of file + + + diff --git a/packages/components/nodes/tools/GoogleSearchAPI/GoogleSearchAPI.ts b/packages/components/nodes/tools/GoogleSearchAPI/GoogleSearchAPI.ts index 29ebae8b..78fe8e73 100644 --- a/packages/components/nodes/tools/GoogleSearchAPI/GoogleSearchAPI.ts +++ b/packages/components/nodes/tools/GoogleSearchAPI/GoogleSearchAPI.ts @@ -19,7 +19,7 @@ class GoogleCustomSearchAPI_Tools implements INode { this.name = 'googleCustomSearch' this.version = 1.0 this.type = 'GoogleCustomSearchAPI' - this.icon = 'google.png' + this.icon = 'google.svg' this.category = 'Tools' this.description = 'Wrapper around Google Custom Search API - a real-time API to access Google search results' this.inputs = [] diff --git a/packages/components/nodes/tools/GoogleSearchAPI/google.png b/packages/components/nodes/tools/GoogleSearchAPI/google.png deleted file mode 100644 index c7cd4ca1..00000000 Binary files a/packages/components/nodes/tools/GoogleSearchAPI/google.png and /dev/null differ diff --git a/packages/components/nodes/tools/GoogleSearchAPI/google.svg b/packages/components/nodes/tools/GoogleSearchAPI/google.svg new file mode 100644 index 00000000..b4f76f2c --- /dev/null +++ b/packages/components/nodes/tools/GoogleSearchAPI/google.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/components/nodes/tools/OpenAPIToolkit/OpenAPIToolkit.ts b/packages/components/nodes/tools/OpenAPIToolkit/OpenAPIToolkit.ts index d1bf3891..1dc0705b 100644 --- a/packages/components/nodes/tools/OpenAPIToolkit/OpenAPIToolkit.ts +++ b/packages/components/nodes/tools/OpenAPIToolkit/OpenAPIToolkit.ts @@ -22,7 +22,7 @@ class OpenAPIToolkit_Tools implements INode { this.name = 'openAPIToolkit' this.version = 1.0 this.type = 'OpenAPIToolkit' - this.icon = 'openapi.png' + this.icon = 'openapi.svg' this.category = 'Tools' this.description = 'Load OpenAPI specification' this.credential = { diff --git a/packages/components/nodes/tools/OpenAPIToolkit/openapi.png b/packages/components/nodes/tools/OpenAPIToolkit/openapi.png deleted file mode 100644 index 457c2e40..00000000 Binary files a/packages/components/nodes/tools/OpenAPIToolkit/openapi.png and /dev/null differ diff --git a/packages/components/nodes/tools/OpenAPIToolkit/openapi.svg b/packages/components/nodes/tools/OpenAPIToolkit/openapi.svg new file mode 100644 index 00000000..0f623b94 --- /dev/null +++ b/packages/components/nodes/tools/OpenAPIToolkit/openapi.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/components/nodes/tools/ReadFile/readfile.svg b/packages/components/nodes/tools/ReadFile/readfile.svg index 3a57a762..c7cba0ef 100644 --- a/packages/components/nodes/tools/ReadFile/readfile.svg +++ b/packages/components/nodes/tools/ReadFile/readfile.svg @@ -1,6 +1,4 @@ - - - - - - \ No newline at end of file + + + + diff --git a/packages/components/nodes/tools/RequestsGet/requestsget.svg b/packages/components/nodes/tools/RequestsGet/requestsget.svg index 03777e7c..d92c5b51 100644 --- a/packages/components/nodes/tools/RequestsGet/requestsget.svg +++ b/packages/components/nodes/tools/RequestsGet/requestsget.svg @@ -1,8 +1,6 @@ - - - - - - - - \ No newline at end of file + + + + + + diff --git a/packages/components/nodes/tools/RequestsPost/requestspost.svg b/packages/components/nodes/tools/RequestsPost/requestspost.svg index 2bea6e96..477b1baf 100644 --- a/packages/components/nodes/tools/RequestsPost/requestspost.svg +++ b/packages/components/nodes/tools/RequestsPost/requestspost.svg @@ -1,6 +1,7 @@ - - - - - - \ No newline at end of file + + + + + + + diff --git a/packages/components/nodes/tools/RetrieverTool/RetrieverTool.ts b/packages/components/nodes/tools/RetrieverTool/RetrieverTool.ts index 6217ca6e..cc74a015 100644 --- a/packages/components/nodes/tools/RetrieverTool/RetrieverTool.ts +++ b/packages/components/nodes/tools/RetrieverTool/RetrieverTool.ts @@ -21,7 +21,7 @@ class Retriever_Tools implements INode { this.name = 'retrieverTool' this.version = 1.0 this.type = 'RetrieverTool' - this.icon = 'retriever-tool.png' + this.icon = 'retrievertool.svg' this.category = 'Tools' this.description = 'Use a retriever as allowed tool for agent' this.baseClasses = [this.type, 'DynamicTool', ...getBaseClasses(DynamicTool)] diff --git a/packages/components/nodes/tools/RetrieverTool/retriever-tool.png b/packages/components/nodes/tools/RetrieverTool/retriever-tool.png deleted file mode 100644 index 4814d007..00000000 Binary files a/packages/components/nodes/tools/RetrieverTool/retriever-tool.png and /dev/null differ diff --git a/packages/components/nodes/tools/RetrieverTool/retrievertool.svg b/packages/components/nodes/tools/RetrieverTool/retrievertool.svg new file mode 100644 index 00000000..c25a32c8 --- /dev/null +++ b/packages/components/nodes/tools/RetrieverTool/retrievertool.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/components/nodes/tools/SearchApi/searchapi.svg b/packages/components/nodes/tools/SearchApi/searchapi.svg index c44c29c4..0c566712 100644 --- a/packages/components/nodes/tools/SearchApi/searchapi.svg +++ b/packages/components/nodes/tools/SearchApi/searchapi.svg @@ -1 +1,8 @@ -0479_octopus_verti + + + + + + + + diff --git a/packages/components/nodes/tools/SerpAPI/SerpAPI.ts b/packages/components/nodes/tools/SerpAPI/SerpAPI.ts index b7230c85..184dc1c4 100644 --- a/packages/components/nodes/tools/SerpAPI/SerpAPI.ts +++ b/packages/components/nodes/tools/SerpAPI/SerpAPI.ts @@ -19,7 +19,7 @@ class SerpAPI_Tools implements INode { this.name = 'serpAPI' this.version = 1.0 this.type = 'SerpAPI' - this.icon = 'serp.png' + this.icon = 'serp.svg' this.category = 'Tools' this.description = 'Wrapper around SerpAPI - a real-time API to access Google search results' this.inputs = [] diff --git a/packages/components/nodes/tools/SerpAPI/serp.png b/packages/components/nodes/tools/SerpAPI/serp.png deleted file mode 100644 index 338aeaea..00000000 Binary files a/packages/components/nodes/tools/SerpAPI/serp.png and /dev/null differ diff --git a/packages/components/nodes/tools/SerpAPI/serp.svg b/packages/components/nodes/tools/SerpAPI/serp.svg new file mode 100644 index 00000000..04999b54 --- /dev/null +++ b/packages/components/nodes/tools/SerpAPI/serp.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/packages/components/nodes/tools/Serper/Serper.ts b/packages/components/nodes/tools/Serper/Serper.ts index 1facdb3d..2e67a61d 100644 --- a/packages/components/nodes/tools/Serper/Serper.ts +++ b/packages/components/nodes/tools/Serper/Serper.ts @@ -19,7 +19,7 @@ class Serper_Tools implements INode { this.name = 'serper' this.version = 1.0 this.type = 'Serper' - this.icon = 'serper.png' + this.icon = 'serper.svg' this.category = 'Tools' this.description = 'Wrapper around Serper.dev - Google Search API' this.inputs = [] diff --git a/packages/components/nodes/tools/Serper/serper.png b/packages/components/nodes/tools/Serper/serper.png deleted file mode 100644 index 0b094037..00000000 Binary files a/packages/components/nodes/tools/Serper/serper.png and /dev/null differ diff --git a/packages/components/nodes/tools/Serper/serper.svg b/packages/components/nodes/tools/Serper/serper.svg new file mode 100644 index 00000000..819f1888 --- /dev/null +++ b/packages/components/nodes/tools/Serper/serper.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/components/nodes/tools/WebBrowser/webBrowser.svg b/packages/components/nodes/tools/WebBrowser/webBrowser.svg index 01eea4f2..d9b0d629 100644 --- a/packages/components/nodes/tools/WebBrowser/webBrowser.svg +++ b/packages/components/nodes/tools/WebBrowser/webBrowser.svg @@ -1,12 +1,9 @@ - - - - - - - - - - - - \ No newline at end of file + + + + + + + + + diff --git a/packages/components/nodes/tools/WriteFile/writefile.svg b/packages/components/nodes/tools/WriteFile/writefile.svg index 72500bf6..0df04ea4 100644 --- a/packages/components/nodes/tools/WriteFile/writefile.svg +++ b/packages/components/nodes/tools/WriteFile/writefile.svg @@ -1,6 +1,4 @@ - - - - - - \ No newline at end of file + + + + diff --git a/packages/components/nodes/utilities/CustomFunction/CustomFunction.ts b/packages/components/nodes/utilities/CustomFunction/CustomFunction.ts new file mode 100644 index 00000000..3dbb7c7d --- /dev/null +++ b/packages/components/nodes/utilities/CustomFunction/CustomFunction.ts @@ -0,0 +1,136 @@ +import { ICommonObject, IDatabaseEntity, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' +import { NodeVM } from 'vm2' +import { DataSource } from 'typeorm' +import { availableDependencies, defaultAllowBuiltInDep, getVars, handleEscapeCharacters, prepareSandboxVars } from '../../../src/utils' + +class CustomFunction_Utilities implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + baseClasses: string[] + inputs: INodeParams[] + outputs: INodeOutputsValue[] + + constructor() { + this.label = 'Custom JS Function' + this.name = 'customFunction' + this.version = 1.0 + this.type = 'CustomFunction' + this.icon = 'customfunction.svg' + this.category = 'Utilities' + this.description = `Execute custom javascript function` + this.baseClasses = [this.type, 'Utilities'] + this.inputs = [ + { + label: 'Input Variables', + name: 'functionInputVariables', + description: 'Input variables can be used in the function with prefix $. For example: $var', + type: 'json', + optional: true, + acceptVariable: true, + list: true + }, + { + label: 'Function Name', + name: 'functionName', + type: 'string', + optional: true, + placeholder: 'My Function' + }, + { + label: 'Javascript Function', + name: 'javascriptFunction', + type: 'code' + } + ] + this.outputs = [ + { + label: 'Output', + name: 'output', + baseClasses: ['string', 'number', 'boolean', 'json', 'array'] + } + ] + } + + async init(nodeData: INodeData, input: string, options: ICommonObject): Promise { + const javascriptFunction = nodeData.inputs?.javascriptFunction as string + const functionInputVariablesRaw = nodeData.inputs?.functionInputVariables + const appDataSource = options.appDataSource as DataSource + const databaseEntities = options.databaseEntities as IDatabaseEntity + + const variables = await getVars(appDataSource, databaseEntities, nodeData) + const flow = { + chatflowId: options.chatflowid, + sessionId: options.sessionId, + chatId: options.chatId, + input + } + + let inputVars: ICommonObject = {} + if (functionInputVariablesRaw) { + try { + inputVars = + typeof functionInputVariablesRaw === 'object' ? functionInputVariablesRaw : JSON.parse(functionInputVariablesRaw) + } catch (exception) { + throw new Error('Invalid JSON in the Custom Function Input Variables: ' + exception) + } + } + + // Some values might be a stringified JSON, parse it + for (const key in inputVars) { + if (typeof inputVars[key] === 'string' && inputVars[key].startsWith('{') && inputVars[key].endsWith('}')) { + try { + inputVars[key] = JSON.parse(inputVars[key]) + } catch (e) { + continue + } + } + } + + let sandbox: any = { $input: input } + sandbox['$vars'] = prepareSandboxVars(variables) + sandbox['$flow'] = flow + + if (Object.keys(inputVars).length) { + for (const item in inputVars) { + let value = inputVars[item] + if (typeof value === 'string') { + value = handleEscapeCharacters(value, true) + } + sandbox[`$${item}`] = value + } + } + + const builtinDeps = process.env.TOOL_FUNCTION_BUILTIN_DEP + ? defaultAllowBuiltInDep.concat(process.env.TOOL_FUNCTION_BUILTIN_DEP.split(',')) + : defaultAllowBuiltInDep + const externalDeps = process.env.TOOL_FUNCTION_EXTERNAL_DEP ? process.env.TOOL_FUNCTION_EXTERNAL_DEP.split(',') : [] + const deps = availableDependencies.concat(externalDeps) + + const nodeVMOptions = { + console: 'inherit', + sandbox, + require: { + external: { modules: deps }, + builtin: builtinDeps + } + } as any + + const vm = new NodeVM(nodeVMOptions) + try { + const response = await vm.run(`module.exports = async function() {${javascriptFunction}}()`, __dirname) + if (typeof response === 'string') { + return handleEscapeCharacters(response, false) + } + return response + } catch (e) { + throw new Error(e) + } + } +} + +module.exports = { nodeClass: CustomFunction_Utilities } diff --git a/packages/components/nodes/utilities/CustomFunction/customfunction.svg b/packages/components/nodes/utilities/CustomFunction/customfunction.svg new file mode 100644 index 00000000..506f3248 --- /dev/null +++ b/packages/components/nodes/utilities/CustomFunction/customfunction.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/components/nodes/utilities/GetVariable/GetVariable.ts b/packages/components/nodes/utilities/GetVariable/GetVariable.ts new file mode 100644 index 00000000..dde5a2d9 --- /dev/null +++ b/packages/components/nodes/utilities/GetVariable/GetVariable.ts @@ -0,0 +1,52 @@ +import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' + +class GetVariable_Utilities implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + baseClasses: string[] + inputs: INodeParams[] + outputs: INodeOutputsValue[] + + constructor() { + this.label = 'Get Variable' + this.name = 'getVariable' + this.version = 1.0 + this.type = 'GetVariable' + this.icon = 'getvar.svg' + this.category = 'Utilities' + this.description = `Get variable that was saved using Set Variable node` + this.baseClasses = [this.type, 'Utilities'] + this.inputs = [ + { + label: 'Variable Name', + name: 'variableName', + type: 'string', + placeholder: 'var1' + } + ] + this.outputs = [ + { + label: 'Output', + name: 'output', + baseClasses: ['string', 'number', 'boolean', 'json', 'array'] + } + ] + } + + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + const variableName = nodeData.inputs?.variableName as string + const dynamicVars = options.dynamicVariables as Record + + if (Object.prototype.hasOwnProperty.call(dynamicVars, variableName)) { + return dynamicVars[variableName] + } + return undefined + } +} + +module.exports = { nodeClass: GetVariable_Utilities } diff --git a/packages/components/nodes/utilities/GetVariable/getvar.svg b/packages/components/nodes/utilities/GetVariable/getvar.svg new file mode 100644 index 00000000..0528c977 --- /dev/null +++ b/packages/components/nodes/utilities/GetVariable/getvar.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/components/nodes/utilities/IfElseFunction/IfElseFunction.ts b/packages/components/nodes/utilities/IfElseFunction/IfElseFunction.ts new file mode 100644 index 00000000..1c5c0b7d --- /dev/null +++ b/packages/components/nodes/utilities/IfElseFunction/IfElseFunction.ts @@ -0,0 +1,155 @@ +import { ICommonObject, IDatabaseEntity, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' +import { NodeVM } from 'vm2' +import { DataSource } from 'typeorm' +import { availableDependencies, defaultAllowBuiltInDep, getVars, handleEscapeCharacters, prepareSandboxVars } from '../../../src/utils' + +class IfElseFunction_Utilities implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + baseClasses: string[] + inputs: INodeParams[] + outputs: INodeOutputsValue[] + + constructor() { + this.label = 'IfElse Function' + this.name = 'ifElseFunction' + this.version = 1.0 + this.type = 'IfElseFunction' + this.icon = 'ifelsefunction.svg' + this.category = 'Utilities' + this.description = `Split flows based on If Else javascript functions` + this.baseClasses = [this.type, 'Utilities'] + this.inputs = [ + { + label: 'Input Variables', + name: 'functionInputVariables', + description: 'Input variables can be used in the function with prefix $. For example: $var', + type: 'json', + optional: true, + acceptVariable: true, + list: true + }, + { + label: 'IfElse Name', + name: 'functionName', + type: 'string', + optional: true, + placeholder: 'If Condition Match' + }, + { + label: 'If Function', + name: 'ifFunction', + description: 'Function must return a value', + type: 'code', + rows: 2, + default: `if ("hello" == "hello") { + return true; +}` + }, + { + label: 'Else Function', + name: 'elseFunction', + description: 'Function must return a value', + type: 'code', + rows: 2, + default: `return false;` + } + ] + this.outputs = [ + { + label: 'True', + name: 'returnTrue', + baseClasses: ['string', 'number', 'boolean', 'json', 'array'] + }, + { + label: 'False', + name: 'returnFalse', + baseClasses: ['string', 'number', 'boolean', 'json', 'array'] + } + ] + } + + async init(nodeData: INodeData, input: string, options: ICommonObject): Promise { + const ifFunction = nodeData.inputs?.ifFunction as string + const elseFunction = nodeData.inputs?.elseFunction as string + const functionInputVariablesRaw = nodeData.inputs?.functionInputVariables + const appDataSource = options.appDataSource as DataSource + const databaseEntities = options.databaseEntities as IDatabaseEntity + + const variables = await getVars(appDataSource, databaseEntities, nodeData) + const flow = { + chatflowId: options.chatflowid, + sessionId: options.sessionId, + chatId: options.chatId, + input + } + + let inputVars: ICommonObject = {} + if (functionInputVariablesRaw) { + try { + inputVars = + typeof functionInputVariablesRaw === 'object' ? functionInputVariablesRaw : JSON.parse(functionInputVariablesRaw) + } catch (exception) { + throw new Error("Invalid JSON in the IfElse's Input Variables: " + exception) + } + } + + // Some values might be a stringified JSON, parse it + for (const key in inputVars) { + if (typeof inputVars[key] === 'string' && inputVars[key].startsWith('{') && inputVars[key].endsWith('}')) { + try { + inputVars[key] = JSON.parse(inputVars[key]) + } catch (e) { + continue + } + } + } + + let sandbox: any = { $input: input } + sandbox['$vars'] = prepareSandboxVars(variables) + sandbox['$flow'] = flow + + if (Object.keys(inputVars).length) { + for (const item in inputVars) { + let value = inputVars[item] + if (typeof value === 'string') { + value = handleEscapeCharacters(value, true) + } + sandbox[`$${item}`] = value + } + } + + const builtinDeps = process.env.TOOL_FUNCTION_BUILTIN_DEP + ? defaultAllowBuiltInDep.concat(process.env.TOOL_FUNCTION_BUILTIN_DEP.split(',')) + : defaultAllowBuiltInDep + const externalDeps = process.env.TOOL_FUNCTION_EXTERNAL_DEP ? process.env.TOOL_FUNCTION_EXTERNAL_DEP.split(',') : [] + const deps = availableDependencies.concat(externalDeps) + + const nodeVMOptions = { + console: 'inherit', + sandbox, + require: { + external: { modules: deps }, + builtin: builtinDeps + } + } as any + + const vm = new NodeVM(nodeVMOptions) + try { + const responseTrue = await vm.run(`module.exports = async function() {${ifFunction}}()`, __dirname) + if (responseTrue) return { output: responseTrue, type: true } + + const responseFalse = await vm.run(`module.exports = async function() {${elseFunction}}()`, __dirname) + return { output: responseFalse, type: false } + } catch (e) { + throw new Error(e) + } + } +} + +module.exports = { nodeClass: IfElseFunction_Utilities } diff --git a/packages/components/nodes/utilities/IfElseFunction/ifelsefunction.svg b/packages/components/nodes/utilities/IfElseFunction/ifelsefunction.svg new file mode 100644 index 00000000..f4ccc78b --- /dev/null +++ b/packages/components/nodes/utilities/IfElseFunction/ifelsefunction.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/utilities/SetVariable/SetVariable.ts b/packages/components/nodes/utilities/SetVariable/SetVariable.ts new file mode 100644 index 00000000..8542668c --- /dev/null +++ b/packages/components/nodes/utilities/SetVariable/SetVariable.ts @@ -0,0 +1,56 @@ +import { INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' + +class SetVariable_Utilities implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + baseClasses: string[] + inputs: INodeParams[] + outputs: INodeOutputsValue[] + + constructor() { + this.label = 'Set Variable' + this.name = 'setVariable' + this.version = 1.0 + this.type = 'SetVariable' + this.icon = 'setvar.svg' + this.category = 'Utilities' + this.description = `Set variable which can be retrieved at a later stage. Variable is only available during runtime.` + this.baseClasses = [this.type, 'Utilities'] + this.inputs = [ + { + label: 'Input', + name: 'input', + type: 'string | number | boolean | json | array', + optional: true, + list: true + }, + { + label: 'Variable Name', + name: 'variableName', + type: 'string', + placeholder: 'var1' + } + ] + this.outputs = [ + { + label: 'Output', + name: 'output', + baseClasses: ['string', 'number', 'boolean', 'json', 'array'] + } + ] + } + + async init(nodeData: INodeData): Promise { + const inputRaw = nodeData.inputs?.input + const variableName = nodeData.inputs?.variableName as string + + return { output: inputRaw, dynamicVariables: { [variableName]: inputRaw } } + } +} + +module.exports = { nodeClass: SetVariable_Utilities } diff --git a/packages/components/nodes/utilities/SetVariable/setvar.svg b/packages/components/nodes/utilities/SetVariable/setvar.svg new file mode 100644 index 00000000..a763c4b3 --- /dev/null +++ b/packages/components/nodes/utilities/SetVariable/setvar.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/components/nodes/utilities/StickyNote/StickyNote.ts b/packages/components/nodes/utilities/StickyNote/StickyNote.ts new file mode 100644 index 00000000..8b0ec208 --- /dev/null +++ b/packages/components/nodes/utilities/StickyNote/StickyNote.ts @@ -0,0 +1,40 @@ +import { INode, INodeParams } from '../../../src/Interface' + +class StickyNote implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + baseClasses: string[] + inputs: INodeParams[] + + constructor() { + this.label = 'Sticky Note' + this.name = 'stickyNote' + this.version = 1.0 + this.type = 'StickyNote' + this.icon = 'stickyNote.svg' + this.category = 'Utilities' + this.description = 'Add a sticky note' + this.inputs = [ + { + label: '', + name: 'note', + type: 'string', + rows: 1, + placeholder: 'Type something here', + optional: true + } + ] + this.baseClasses = [this.type] + } + + async init(): Promise { + return new StickyNote() + } +} + +module.exports = { nodeClass: StickyNote } diff --git a/packages/components/nodes/utilities/StickyNote/stickyNote.svg b/packages/components/nodes/utilities/StickyNote/stickyNote.svg new file mode 100644 index 00000000..81c45058 --- /dev/null +++ b/packages/components/nodes/utilities/StickyNote/stickyNote.svg @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/packages/components/nodes/vectorstores/Astra/Astra.ts b/packages/components/nodes/vectorstores/Astra/Astra.ts new file mode 100644 index 00000000..edaadc9c --- /dev/null +++ b/packages/components/nodes/vectorstores/Astra/Astra.ts @@ -0,0 +1,182 @@ +import { flatten } from 'lodash' +import { Embeddings } from 'langchain/embeddings/base' +import { Document } from 'langchain/document' +import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' +import { getBaseClasses, getCredentialData } from '../../../src/utils' +import { AstraDBVectorStore, AstraLibArgs } from '@langchain/community/vectorstores/astradb' +import { addMMRInputParams, resolveVectorStoreOrRetriever } from '../VectorStoreUtils' + +class Astra_VectorStores implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + badge: string + baseClasses: string[] + inputs: INodeParams[] + credential: INodeParams + outputs: INodeOutputsValue[] + + constructor() { + this.label = 'Astra' + this.name = 'Astra' + this.version = 1.0 + this.type = 'Astra' + this.icon = 'astra.svg' + this.category = 'Vector Stores' + this.description = `Upsert embedded data and perform similarity or mmr search upon query using DataStax Astra DB, a serverless vector database that’s perfect for managing mission-critical AI workloads` + this.baseClasses = [this.type, 'VectorStoreRetriever', 'BaseRetriever'] + this.badge = 'NEW' + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['AstraDBApi'] + } + this.inputs = [ + { + label: 'Document', + name: 'document', + type: 'Document', + list: true, + optional: true + }, + { + label: 'Embeddings', + name: 'embeddings', + type: 'Embeddings' + }, + { + label: 'Vector Dimension', + name: 'vectorDimension', + type: 'number', + placeholder: '1536', + optional: true, + description: 'Dimension used for storing vector embedding' + }, + { + label: 'Similarity Metric', + name: 'similarityMetric', + type: 'string', + placeholder: 'cosine', + optional: true, + description: 'cosine | euclidean | dot_product' + }, + { + label: 'Top K', + name: 'topK', + description: 'Number of top results to fetch. Default to 4', + placeholder: '4', + type: 'number', + additionalParams: true, + optional: true + } + ] + addMMRInputParams(this.inputs) + this.outputs = [ + { + label: 'Astra Retriever', + name: 'retriever', + baseClasses: this.baseClasses + }, + { + label: 'Astra Vector Store', + name: 'vectorStore', + baseClasses: [this.type, ...getBaseClasses(AstraDBVectorStore)] + } + ] + } + + //@ts-ignore + vectorStoreMethods = { + async upsert(nodeData: INodeData, options: ICommonObject): Promise { + const docs = nodeData.inputs?.document as Document[] + const embeddings = nodeData.inputs?.embeddings as Embeddings + const vectorDimension = nodeData.inputs?.vectorDimension as number + const similarityMetric = nodeData.inputs?.similarityMetric as 'cosine' | 'euclidean' | 'dot_product' | undefined + const credentialData = await getCredentialData(nodeData.credential ?? '', options) + + const expectedSimilarityMetric = ['cosine', 'euclidean', 'dot_product'] + if (similarityMetric && !expectedSimilarityMetric.includes(similarityMetric)) { + throw new Error(`Invalid Similarity Metric should be one of 'cosine' | 'euclidean' | 'dot_product'`) + } + + const clientConfig = { + token: credentialData?.applicationToken, + endpoint: credentialData?.dbEndPoint + } + + const astraConfig: AstraLibArgs = { + ...clientConfig, + collection: credentialData.collectionName ?? 'flowise_test', + collectionOptions: { + vector: { + dimension: vectorDimension ?? 1536, + metric: similarityMetric ?? 'cosine' + } + } + } + + const flattenDocs = docs && docs.length ? flatten(docs) : [] + const finalDocs = [] + for (let i = 0; i < flattenDocs.length; i += 1) { + if (flattenDocs[i] && flattenDocs[i].pageContent) { + finalDocs.push(new Document(flattenDocs[i])) + } + } + + try { + await AstraDBVectorStore.fromDocuments(finalDocs, embeddings, astraConfig) + } catch (e) { + throw new Error(e) + } + } + } + + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + const docs = nodeData.inputs?.document as Document[] + const embeddings = nodeData.inputs?.embeddings as Embeddings + const vectorDimension = nodeData.inputs?.vectorDimension as number + const similarityMetric = nodeData.inputs?.similarityMetric as 'cosine' | 'euclidean' | 'dot_product' | undefined + + const credentialData = await getCredentialData(nodeData.credential ?? '', options) + + const expectedSimilarityMetric = ['cosine', 'euclidean', 'dot_product'] + if (similarityMetric && !expectedSimilarityMetric.includes(similarityMetric)) { + throw new Error(`Invalid Similarity Metric should be one of 'cosine' | 'euclidean' | 'dot_product'`) + } + + const clientConfig = { + token: credentialData?.applicationToken, + endpoint: credentialData?.dbEndPoint + } + + const astraConfig: AstraLibArgs = { + ...clientConfig, + collection: credentialData.collectionName ?? 'flowise_test', + collectionOptions: { + vector: { + dimension: vectorDimension ?? 1536, + metric: similarityMetric ?? 'cosine' + } + } + } + + const flattenDocs = docs && docs.length ? flatten(docs) : [] + const finalDocs = [] + for (let i = 0; i < flattenDocs.length; i += 1) { + if (flattenDocs[i] && flattenDocs[i].pageContent) { + finalDocs.push(new Document(flattenDocs[i])) + } + } + + const vectorStore = await AstraDBVectorStore.fromExistingIndex(embeddings, astraConfig) + + return resolveVectorStoreOrRetriever(nodeData, vectorStore) + } +} + +module.exports = { nodeClass: Astra_VectorStores } diff --git a/packages/components/nodes/vectorstores/Astra/astra.svg b/packages/components/nodes/vectorstores/Astra/astra.svg new file mode 100644 index 00000000..de58397d --- /dev/null +++ b/packages/components/nodes/vectorstores/Astra/astra.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/packages/components/nodes/vectorstores/Elasticsearch/Elasticsearch.ts b/packages/components/nodes/vectorstores/Elasticsearch/Elasticsearch.ts index 5f3cf206..04c90c6b 100644 --- a/packages/components/nodes/vectorstores/Elasticsearch/Elasticsearch.ts +++ b/packages/components/nodes/vectorstores/Elasticsearch/Elasticsearch.ts @@ -183,13 +183,26 @@ const prepareConnectionOptions = ( } else if (cloudId) { let username = getCredentialParam('username', credentialData, nodeData) let password = getCredentialParam('password', credentialData, nodeData) - elasticSearchClientOptions = { - cloud: { - id: cloudId - }, - auth: { - username: username, - password: password + if (cloudId.startsWith('http')) { + elasticSearchClientOptions = { + node: cloudId, + auth: { + username: username, + password: password + }, + tls: { + rejectUnauthorized: false + } + } + } else { + elasticSearchClientOptions = { + cloud: { + id: cloudId + }, + auth: { + username: username, + password: password + } } } } diff --git a/packages/components/nodes/vectorstores/Faiss/faiss.svg b/packages/components/nodes/vectorstores/Faiss/faiss.svg index 5fbe9832..d56ad86b 100644 --- a/packages/components/nodes/vectorstores/Faiss/faiss.svg +++ b/packages/components/nodes/vectorstores/Faiss/faiss.svg @@ -1,10 +1,9 @@ - - - - - - - - - - \ No newline at end of file + + + + + + + + + diff --git a/packages/components/nodes/vectorstores/InMemory/memory.svg b/packages/components/nodes/vectorstores/InMemory/memory.svg index e7f97c87..4e0d9bcb 100644 --- a/packages/components/nodes/vectorstores/InMemory/memory.svg +++ b/packages/components/nodes/vectorstores/InMemory/memory.svg @@ -1,7 +1,19 @@ - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + diff --git a/packages/components/nodes/vectorstores/Milvus/Milvus.ts b/packages/components/nodes/vectorstores/Milvus/Milvus.ts index 090f35f7..7566f8a8 100644 --- a/packages/components/nodes/vectorstores/Milvus/Milvus.ts +++ b/packages/components/nodes/vectorstores/Milvus/Milvus.ts @@ -65,6 +65,14 @@ class Milvus_VectorStores implements INode { name: 'milvusCollection', type: 'string' }, + { + label: 'Milvus Text Field', + name: 'milvusTextField', + type: 'string', + placeholder: 'langchain_text', + optional: true, + additionalParams: true + }, { label: 'Milvus Filter', name: 'milvusFilter', @@ -150,6 +158,7 @@ class Milvus_VectorStores implements INode { const address = nodeData.inputs?.milvusServerUrl as string const collectionName = nodeData.inputs?.milvusCollection as string const milvusFilter = nodeData.inputs?.milvusFilter as string + const textField = nodeData.inputs?.milvusTextField as string // embeddings const embeddings = nodeData.inputs?.embeddings as Embeddings @@ -169,7 +178,8 @@ class Milvus_VectorStores implements INode { // init MilvusLibArgs const milVusArgs: MilvusLibArgs = { url: address, - collectionName: collectionName + collectionName: collectionName, + textField: textField } if (milvusUser) milVusArgs.username = milvusUser diff --git a/packages/components/nodes/vectorstores/MongoDBAtlas/MongoDBAtlas.ts b/packages/components/nodes/vectorstores/MongoDBAtlas/MongoDBAtlas.ts index a0699f6b..6ba7199f 100644 --- a/packages/components/nodes/vectorstores/MongoDBAtlas/MongoDBAtlas.ts +++ b/packages/components/nodes/vectorstores/MongoDBAtlas/MongoDBAtlas.ts @@ -5,6 +5,7 @@ import { Embeddings } from 'langchain/embeddings/base' import { Document } from 'langchain/document' import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' +import { addMMRInputParams, resolveVectorStoreOrRetriever } from '../VectorStoreUtils' class MongoDBAtlas_VectorStores implements INode { label: string @@ -24,9 +25,9 @@ class MongoDBAtlas_VectorStores implements INode { this.label = 'MongoDB Atlas' this.name = 'mongoDBAtlas' this.version = 1.0 - this.description = `Upsert embedded data and perform similarity search upon query using MongoDB Atlas, a managed cloud mongodb database` + this.description = `Upsert embedded data and perform similarity or mmr search upon query using MongoDB Atlas, a managed cloud mongodb database` this.type = 'MongoDB Atlas' - this.icon = 'mongodb.png' + this.icon = 'mongodb.svg' this.category = 'Vector Stores' this.baseClasses = [this.type, 'VectorStoreRetriever', 'BaseRetriever'] this.badge = 'NEW' @@ -95,6 +96,7 @@ class MongoDBAtlas_VectorStores implements INode { optional: true } ] + addMMRInputParams(this.inputs) this.outputs = [ { label: 'MongoDB Retriever', @@ -162,9 +164,6 @@ class MongoDBAtlas_VectorStores implements INode { let textKey = nodeData.inputs?.textKey as string let embeddingKey = nodeData.inputs?.embeddingKey as string const embeddings = nodeData.inputs?.embeddings as Embeddings - const topK = nodeData.inputs?.topK as string - const k = topK ? parseFloat(topK) : 4 - const output = nodeData.outputs?.output as string let mongoDBConnectUrl = getCredentialParam('mongoDBConnectUrl', credentialData, nodeData) @@ -181,13 +180,7 @@ class MongoDBAtlas_VectorStores implements INode { embeddingKey }) - if (output === 'retriever') { - return vectorStore.asRetriever(k) - } else if (output === 'vectorStore') { - ;(vectorStore as any).k = k - return vectorStore - } - return vectorStore + return resolveVectorStoreOrRetriever(nodeData, vectorStore) } } diff --git a/packages/components/nodes/vectorstores/MongoDBAtlas/MongoDBSearchBase.ts b/packages/components/nodes/vectorstores/MongoDBAtlas/MongoDBSearchBase.ts index 95930e4a..9866f6a9 100644 --- a/packages/components/nodes/vectorstores/MongoDBAtlas/MongoDBSearchBase.ts +++ b/packages/components/nodes/vectorstores/MongoDBAtlas/MongoDBSearchBase.ts @@ -31,7 +31,7 @@ export abstract class MongoDBSearchBase { protected constructor() { this.type = 'MongoDB Atlas' - this.icon = 'mongodb.png' + this.icon = 'mongodb.svg' this.category = 'Vector Stores' this.baseClasses = [this.type, 'VectorStoreRetriever', 'BaseRetriever'] this.badge = 'DEPRECATING' diff --git a/packages/components/nodes/vectorstores/MongoDBAtlas/mongodb.png b/packages/components/nodes/vectorstores/MongoDBAtlas/mongodb.png deleted file mode 100644 index 5586fe0a..00000000 Binary files a/packages/components/nodes/vectorstores/MongoDBAtlas/mongodb.png and /dev/null differ diff --git a/packages/components/nodes/vectorstores/MongoDBAtlas/mongodb.svg b/packages/components/nodes/vectorstores/MongoDBAtlas/mongodb.svg new file mode 100644 index 00000000..49c5f05a --- /dev/null +++ b/packages/components/nodes/vectorstores/MongoDBAtlas/mongodb.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/components/nodes/vectorstores/OpenSearch/OpenSearch.ts b/packages/components/nodes/vectorstores/OpenSearch/OpenSearch.ts index e3e18ce1..6be8977d 100644 --- a/packages/components/nodes/vectorstores/OpenSearch/OpenSearch.ts +++ b/packages/components/nodes/vectorstores/OpenSearch/OpenSearch.ts @@ -24,7 +24,7 @@ class OpenSearch_VectorStores implements INode { this.name = 'openSearch' this.version = 1.0 this.type = 'OpenSearch' - this.icon = 'opensearch.png' + this.icon = 'opensearch.svg' this.category = 'Vector Stores' this.description = `Upsert embedded data and perform similarity search upon query using OpenSearch, an open-source, all-in-one vector database` this.baseClasses = [this.type, 'VectorStoreRetriever', 'BaseRetriever'] diff --git a/packages/components/nodes/vectorstores/OpenSearch/OpenSearch_Upsert.ts b/packages/components/nodes/vectorstores/OpenSearch/OpenSearch_Upsert.ts index 2eb47316..e5512df8 100644 --- a/packages/components/nodes/vectorstores/OpenSearch/OpenSearch_Upsert.ts +++ b/packages/components/nodes/vectorstores/OpenSearch/OpenSearch_Upsert.ts @@ -24,7 +24,7 @@ class OpenSearchUpsert_VectorStores implements INode { this.name = 'openSearchUpsertDocument' this.version = 1.0 this.type = 'OpenSearch' - this.icon = 'opensearch.png' + this.icon = 'opensearch.svg' this.category = 'Vector Stores' this.description = 'Upsert documents to OpenSearch' this.baseClasses = [this.type, 'VectorStoreRetriever', 'BaseRetriever'] diff --git a/packages/components/nodes/vectorstores/OpenSearch/OpenSearch_existing.ts b/packages/components/nodes/vectorstores/OpenSearch/OpenSearch_existing.ts index a012a2e5..b4765a9a 100644 --- a/packages/components/nodes/vectorstores/OpenSearch/OpenSearch_existing.ts +++ b/packages/components/nodes/vectorstores/OpenSearch/OpenSearch_existing.ts @@ -22,7 +22,7 @@ class OpenSearch_Existing_VectorStores implements INode { this.name = 'openSearchExistingIndex' this.version = 1.0 this.type = 'OpenSearch' - this.icon = 'opensearch.png' + this.icon = 'opensearch.svg' this.category = 'Vector Stores' this.description = 'Load existing index from OpenSearch (i.e: Document has been upserted)' this.baseClasses = [this.type, 'VectorStoreRetriever', 'BaseRetriever'] diff --git a/packages/components/nodes/vectorstores/OpenSearch/opensearch.png b/packages/components/nodes/vectorstores/OpenSearch/opensearch.png deleted file mode 100644 index 3fdcfd3f..00000000 Binary files a/packages/components/nodes/vectorstores/OpenSearch/opensearch.png and /dev/null differ diff --git a/packages/components/nodes/vectorstores/OpenSearch/opensearch.svg b/packages/components/nodes/vectorstores/OpenSearch/opensearch.svg new file mode 100644 index 00000000..6363d52d --- /dev/null +++ b/packages/components/nodes/vectorstores/OpenSearch/opensearch.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/components/nodes/vectorstores/Pinecone/Pinecone.ts b/packages/components/nodes/vectorstores/Pinecone/Pinecone.ts index 4ece4720..a1ec0e57 100644 --- a/packages/components/nodes/vectorstores/Pinecone/Pinecone.ts +++ b/packages/components/nodes/vectorstores/Pinecone/Pinecone.ts @@ -5,6 +5,7 @@ import { Embeddings } from 'langchain/embeddings/base' import { Document } from 'langchain/document' import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' +import { addMMRInputParams, resolveVectorStoreOrRetriever } from '../VectorStoreUtils' class Pinecone_VectorStores implements INode { label: string @@ -23,11 +24,11 @@ class Pinecone_VectorStores implements INode { constructor() { this.label = 'Pinecone' this.name = 'pinecone' - this.version = 1.0 + this.version = 2.0 this.type = 'Pinecone' - this.icon = 'pinecone.png' + this.icon = 'pinecone.svg' this.category = 'Vector Stores' - this.description = `Upsert embedded data and perform similarity search upon query using Pinecone, a leading fully managed hosted vector database` + this.description = `Upsert embedded data and perform similarity or mmr search using Pinecone, a leading fully managed hosted vector database` this.baseClasses = [this.type, 'VectorStoreRetriever', 'BaseRetriever'] this.badge = 'NEW' this.credential = { @@ -79,6 +80,7 @@ class Pinecone_VectorStores implements INode { optional: true } ] + addMMRInputParams(this.inputs) this.outputs = [ { label: 'Pinecone Retriever', @@ -103,11 +105,9 @@ class Pinecone_VectorStores implements INode { const credentialData = await getCredentialData(nodeData.credential ?? '', options) const pineconeApiKey = getCredentialParam('pineconeApiKey', credentialData, nodeData) - const pineconeEnv = getCredentialParam('pineconeEnv', credentialData, nodeData) const client = new Pinecone({ - apiKey: pineconeApiKey, - environment: pineconeEnv + apiKey: pineconeApiKey }) const pineconeIndex = client.Index(index) @@ -140,17 +140,12 @@ class Pinecone_VectorStores implements INode { const pineconeMetadataFilter = nodeData.inputs?.pineconeMetadataFilter const docs = nodeData.inputs?.document as Document[] const embeddings = nodeData.inputs?.embeddings as Embeddings - const output = nodeData.outputs?.output as string - const topK = nodeData.inputs?.topK as string - const k = topK ? parseFloat(topK) : 4 const credentialData = await getCredentialData(nodeData.credential ?? '', options) const pineconeApiKey = getCredentialParam('pineconeApiKey', credentialData, nodeData) - const pineconeEnv = getCredentialParam('pineconeEnv', credentialData, nodeData) const client = new Pinecone({ - apiKey: pineconeApiKey, - environment: pineconeEnv + apiKey: pineconeApiKey }) const pineconeIndex = client.Index(index) @@ -175,14 +170,7 @@ class Pinecone_VectorStores implements INode { const vectorStore = await PineconeStore.fromExistingIndex(embeddings, obj) - if (output === 'retriever') { - const retriever = vectorStore.asRetriever(k) - return retriever - } else if (output === 'vectorStore') { - ;(vectorStore as any).k = k - return vectorStore - } - return vectorStore + return resolveVectorStoreOrRetriever(nodeData, vectorStore) } } diff --git a/packages/components/nodes/vectorstores/Pinecone/Pinecone_Existing.ts b/packages/components/nodes/vectorstores/Pinecone/Pinecone_Existing.ts index ee2db071..f805fc33 100644 --- a/packages/components/nodes/vectorstores/Pinecone/Pinecone_Existing.ts +++ b/packages/components/nodes/vectorstores/Pinecone/Pinecone_Existing.ts @@ -23,7 +23,7 @@ class Pinecone_Existing_VectorStores implements INode { this.name = 'pineconeExistingIndex' this.version = 1.0 this.type = 'Pinecone' - this.icon = 'pinecone.png' + this.icon = 'pinecone.svg' this.category = 'Vector Stores' this.description = 'Load existing index from Pinecone (i.e: Document has been upserted)' this.baseClasses = [this.type, 'VectorStoreRetriever', 'BaseRetriever'] @@ -95,11 +95,9 @@ class Pinecone_Existing_VectorStores implements INode { const credentialData = await getCredentialData(nodeData.credential ?? '', options) const pineconeApiKey = getCredentialParam('pineconeApiKey', credentialData, nodeData) - const pineconeEnv = getCredentialParam('pineconeEnv', credentialData, nodeData) const client = new Pinecone({ - apiKey: pineconeApiKey, - environment: pineconeEnv + apiKey: pineconeApiKey }) const pineconeIndex = client.Index(index) diff --git a/packages/components/nodes/vectorstores/Pinecone/Pinecone_LlamaIndex.ts b/packages/components/nodes/vectorstores/Pinecone/Pinecone_LlamaIndex.ts index 683e6f25..a584fede 100644 --- a/packages/components/nodes/vectorstores/Pinecone/Pinecone_LlamaIndex.ts +++ b/packages/components/nodes/vectorstores/Pinecone/Pinecone_LlamaIndex.ts @@ -34,7 +34,7 @@ class PineconeLlamaIndex_VectorStores implements INode { this.name = 'pineconeLlamaIndex' this.version = 1.0 this.type = 'Pinecone' - this.icon = 'pinecone.png' + this.icon = 'pinecone.svg' this.category = 'Vector Stores' this.description = `Upsert embedded data and perform similarity search upon query using Pinecone, a leading fully managed hosted vector database` this.baseClasses = [this.type, 'VectorIndexRetriever'] @@ -106,12 +106,10 @@ class PineconeLlamaIndex_VectorStores implements INode { const credentialData = await getCredentialData(nodeData.credential ?? '', options) const pineconeApiKey = getCredentialParam('pineconeApiKey', credentialData, nodeData) - const pineconeEnv = getCredentialParam('pineconeEnv', credentialData, nodeData) const pcvs = new PineconeVectorStore({ indexName, apiKey: pineconeApiKey, - environment: pineconeEnv, namespace: pineconeNamespace }) @@ -150,12 +148,10 @@ class PineconeLlamaIndex_VectorStores implements INode { const credentialData = await getCredentialData(nodeData.credential ?? '', options) const pineconeApiKey = getCredentialParam('pineconeApiKey', credentialData, nodeData) - const pineconeEnv = getCredentialParam('pineconeEnv', credentialData, nodeData) const obj: PineconeParams = { indexName, - apiKey: pineconeApiKey, - environment: pineconeEnv + apiKey: pineconeApiKey } if (pineconeNamespace) obj.namespace = pineconeNamespace @@ -186,7 +182,6 @@ class PineconeLlamaIndex_VectorStores implements INode { type PineconeParams = { indexName: string apiKey: string - environment: string namespace?: string chunkSize?: number queryFilter?: object @@ -197,7 +192,6 @@ class PineconeVectorStore implements VectorStore { db?: Pinecone indexName: string apiKey: string - environment: string chunkSize: number namespace?: string queryFilter?: object @@ -205,7 +199,6 @@ class PineconeVectorStore implements VectorStore { constructor(params: PineconeParams) { this.indexName = params?.indexName this.apiKey = params?.apiKey - this.environment = params?.environment this.namespace = params?.namespace ?? '' this.chunkSize = params?.chunkSize ?? Number.parseInt(process.env.PINECONE_CHUNK_SIZE ?? '100') this.queryFilter = params?.queryFilter ?? {} @@ -214,8 +207,7 @@ class PineconeVectorStore implements VectorStore { private async getDb(): Promise { if (!this.db) { this.db = new Pinecone({ - apiKey: this.apiKey, - environment: this.environment + apiKey: this.apiKey }) } return Promise.resolve(this.db) diff --git a/packages/components/nodes/vectorstores/Pinecone/Pinecone_Upsert.ts b/packages/components/nodes/vectorstores/Pinecone/Pinecone_Upsert.ts index 0c63ce7b..14dbf663 100644 --- a/packages/components/nodes/vectorstores/Pinecone/Pinecone_Upsert.ts +++ b/packages/components/nodes/vectorstores/Pinecone/Pinecone_Upsert.ts @@ -25,7 +25,7 @@ class PineconeUpsert_VectorStores implements INode { this.name = 'pineconeUpsert' this.version = 1.0 this.type = 'Pinecone' - this.icon = 'pinecone.png' + this.icon = 'pinecone.svg' this.category = 'Vector Stores' this.description = 'Upsert documents to Pinecone' this.baseClasses = [this.type, 'VectorStoreRetriever', 'BaseRetriever'] @@ -96,11 +96,9 @@ class PineconeUpsert_VectorStores implements INode { const credentialData = await getCredentialData(nodeData.credential ?? '', options) const pineconeApiKey = getCredentialParam('pineconeApiKey', credentialData, nodeData) - const pineconeEnv = getCredentialParam('pineconeEnv', credentialData, nodeData) const client = new Pinecone({ - apiKey: pineconeApiKey, - environment: pineconeEnv + apiKey: pineconeApiKey }) const pineconeIndex = client.Index(index) diff --git a/packages/components/nodes/vectorstores/Pinecone/pinecone.png b/packages/components/nodes/vectorstores/Pinecone/pinecone.png deleted file mode 100644 index 1ae189fd..00000000 Binary files a/packages/components/nodes/vectorstores/Pinecone/pinecone.png and /dev/null differ diff --git a/packages/components/nodes/vectorstores/Pinecone/pinecone.svg b/packages/components/nodes/vectorstores/Pinecone/pinecone.svg new file mode 100644 index 00000000..ca047054 --- /dev/null +++ b/packages/components/nodes/vectorstores/Pinecone/pinecone.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/packages/components/nodes/vectorstores/Postgres/Postgres.ts b/packages/components/nodes/vectorstores/Postgres/Postgres.ts index ac4b80c3..4e8bae32 100644 --- a/packages/components/nodes/vectorstores/Postgres/Postgres.ts +++ b/packages/components/nodes/vectorstores/Postgres/Postgres.ts @@ -24,7 +24,7 @@ class Postgres_VectorStores implements INode { constructor() { this.label = 'Postgres' this.name = 'postgres' - this.version = 1.0 + this.version = 2.0 this.type = 'Postgres' this.icon = 'postgres.svg' this.category = 'Vector Stores' @@ -60,6 +60,13 @@ class Postgres_VectorStores implements INode { name: 'database', type: 'string' }, + { + label: 'SSL Connection', + name: 'sslConnection', + type: 'boolean', + default: false, + optional: false + }, { label: 'Port', name: 'port', @@ -117,6 +124,7 @@ class Postgres_VectorStores implements INode { const docs = nodeData.inputs?.document as Document[] const embeddings = nodeData.inputs?.embeddings as Embeddings const additionalConfig = nodeData.inputs?.additionalConfig as string + const sslConnection = nodeData.inputs?.sslConnection as boolean let additionalConfiguration = {} if (additionalConfig) { @@ -134,7 +142,8 @@ class Postgres_VectorStores implements INode { port: nodeData.inputs?.port as number, username: user, password: password, - database: nodeData.inputs?.database as string + database: nodeData.inputs?.database as string, + ssl: sslConnection } const args = { diff --git a/packages/components/nodes/vectorstores/Postgres/Postgres_Exisiting.ts b/packages/components/nodes/vectorstores/Postgres/Postgres_Exisiting.ts index 99794a0d..3fa8a107 100644 --- a/packages/components/nodes/vectorstores/Postgres/Postgres_Exisiting.ts +++ b/packages/components/nodes/vectorstores/Postgres/Postgres_Exisiting.ts @@ -23,7 +23,7 @@ class Postgres_Existing_VectorStores implements INode { constructor() { this.label = 'Postgres Load Existing Index' this.name = 'postgresExistingIndex' - this.version = 1.0 + this.version = 2.0 this.type = 'Postgres' this.icon = 'postgres.svg' this.category = 'Vector Stores' @@ -52,6 +52,13 @@ class Postgres_Existing_VectorStores implements INode { name: 'database', type: 'string' }, + { + label: 'SSL Connection', + name: 'sslConnection', + type: 'boolean', + default: false, + optional: false + }, { label: 'Port', name: 'port', @@ -109,6 +116,7 @@ class Postgres_Existing_VectorStores implements INode { const output = nodeData.outputs?.output as string const topK = nodeData.inputs?.topK as string const k = topK ? parseFloat(topK) : 4 + const sslConnection = nodeData.inputs?.sslConnection as boolean let additionalConfiguration = {} if (additionalConfig) { @@ -126,7 +134,8 @@ class Postgres_Existing_VectorStores implements INode { port: nodeData.inputs?.port as number, username: user, password: password, - database: nodeData.inputs?.database as string + database: nodeData.inputs?.database as string, + ssl: sslConnection } const args = { diff --git a/packages/components/nodes/vectorstores/Postgres/Postgres_Upsert.ts b/packages/components/nodes/vectorstores/Postgres/Postgres_Upsert.ts index f706cbe8..d26a642d 100644 --- a/packages/components/nodes/vectorstores/Postgres/Postgres_Upsert.ts +++ b/packages/components/nodes/vectorstores/Postgres/Postgres_Upsert.ts @@ -24,7 +24,7 @@ class PostgresUpsert_VectorStores implements INode { constructor() { this.label = 'Postgres Upsert Document' this.name = 'postgresUpsert' - this.version = 1.0 + this.version = 2.0 this.type = 'Postgres' this.icon = 'postgres.svg' this.category = 'Vector Stores' @@ -59,6 +59,13 @@ class PostgresUpsert_VectorStores implements INode { name: 'database', type: 'string' }, + { + label: 'SSL Connection', + name: 'sslConnection', + type: 'boolean', + default: false, + optional: false + }, { label: 'Port', name: 'port', @@ -117,6 +124,7 @@ class PostgresUpsert_VectorStores implements INode { const output = nodeData.outputs?.output as string const topK = nodeData.inputs?.topK as string const k = topK ? parseFloat(topK) : 4 + const sslConnection = nodeData.inputs?.sslConnection as boolean let additionalConfiguration = {} if (additionalConfig) { @@ -134,7 +142,8 @@ class PostgresUpsert_VectorStores implements INode { port: nodeData.inputs?.port as number, username: user, password: password, - database: nodeData.inputs?.database as string + database: nodeData.inputs?.database as string, + ssl: sslConnection } const args = { diff --git a/packages/components/nodes/vectorstores/Qdrant/Qdrant.ts b/packages/components/nodes/vectorstores/Qdrant/Qdrant.ts index 6413f8bf..e07b728a 100644 --- a/packages/components/nodes/vectorstores/Qdrant/Qdrant.ts +++ b/packages/components/nodes/vectorstores/Qdrant/Qdrant.ts @@ -149,9 +149,12 @@ class Qdrant_VectorStores implements INode { const credentialData = await getCredentialData(nodeData.credential ?? '', options) const qdrantApiKey = getCredentialParam('qdrantApiKey', credentialData, nodeData) + const port = Qdrant_VectorStores.determinePortByUrl(qdrantServerUrl) + const client = new QdrantClient({ url: qdrantServerUrl, - apiKey: qdrantApiKey + apiKey: qdrantApiKey, + port: port }) const flattenDocs = docs && docs.length ? flatten(docs) : [] @@ -198,9 +201,12 @@ class Qdrant_VectorStores implements INode { const credentialData = await getCredentialData(nodeData.credential ?? '', options) const qdrantApiKey = getCredentialParam('qdrantApiKey', credentialData, nodeData) + const port = Qdrant_VectorStores.determinePortByUrl(qdrantServerUrl) + const client = new QdrantClient({ url: qdrantServerUrl, - apiKey: qdrantApiKey + apiKey: qdrantApiKey, + port: port }) const dbConfig: QdrantLibArgs = { @@ -242,6 +248,28 @@ class Qdrant_VectorStores implements INode { } return vectorStore } + + /** + * Determine the port number from the given URL. + * + * The problem is when not doing this the qdrant-client.js will fall back on 6663 when you enter a port 443 and 80. + * See: https://stackoverflow.com/questions/59104197/nodejs-new-url-urlhttps-myurl-com80-lists-the-port-as-empty + * @param qdrantServerUrl the url to get the port from + */ + static determinePortByUrl(qdrantServerUrl: string): number { + const parsedUrl = new URL(qdrantServerUrl) + + let port = parsedUrl.port ? parseInt(parsedUrl.port) : 6663 + + if (parsedUrl.protocol === 'https:' && parsedUrl.port === '') { + port = 443 + } + if (parsedUrl.protocol === 'http:' && parsedUrl.port === '') { + port = 80 + } + + return port + } } module.exports = { nodeClass: Qdrant_VectorStores } diff --git a/packages/components/nodes/vectorstores/Redis/Redis.ts b/packages/components/nodes/vectorstores/Redis/Redis.ts index dc993b86..49f9e8ff 100644 --- a/packages/components/nodes/vectorstores/Redis/Redis.ts +++ b/packages/components/nodes/vectorstores/Redis/Redis.ts @@ -148,10 +148,10 @@ class Redis_VectorStores implements INode { } } - const redisClient = createClient({ url: redisUrl }) - await redisClient.connect() - try { + const redisClient = createClient({ url: redisUrl }) + await redisClient.connect() + const storeConfig: RedisVectorStoreConfig = { redisClient: redisClient, indexName: indexName diff --git a/packages/components/nodes/vectorstores/Supabase/Supabase.ts b/packages/components/nodes/vectorstores/Supabase/Supabase.ts index 13840ab7..a5477914 100644 --- a/packages/components/nodes/vectorstores/Supabase/Supabase.ts +++ b/packages/components/nodes/vectorstores/Supabase/Supabase.ts @@ -5,6 +5,7 @@ import { Embeddings } from 'langchain/embeddings/base' import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' import { SupabaseLibArgs, SupabaseVectorStore } from 'langchain/vectorstores/supabase' +import { addMMRInputParams, resolveVectorStoreOrRetriever } from '../VectorStoreUtils' class Supabase_VectorStores implements INode { label: string @@ -23,11 +24,11 @@ class Supabase_VectorStores implements INode { constructor() { this.label = 'Supabase' this.name = 'supabase' - this.version = 1.0 + this.version = 2.0 this.type = 'Supabase' this.icon = 'supabase.svg' this.category = 'Vector Stores' - this.description = 'Upsert embedded data and perform similarity search upon query using Supabase via pgvector extension' + this.description = 'Upsert embedded data and perform similarity or mmr search upon query using Supabase via pgvector extension' this.baseClasses = [this.type, 'VectorStoreRetriever', 'BaseRetriever'] this.badge = 'NEW' this.credential = { @@ -81,6 +82,7 @@ class Supabase_VectorStores implements INode { optional: true } ] + addMMRInputParams(this.inputs) this.outputs = [ { label: 'Supabase Retriever', @@ -135,9 +137,6 @@ class Supabase_VectorStores implements INode { const queryName = nodeData.inputs?.queryName as string const embeddings = nodeData.inputs?.embeddings as Embeddings const supabaseMetadataFilter = nodeData.inputs?.supabaseMetadataFilter - const output = nodeData.outputs?.output as string - const topK = nodeData.inputs?.topK as string - const k = topK ? parseFloat(topK) : 4 const credentialData = await getCredentialData(nodeData.credential ?? '', options) const supabaseApiKey = getCredentialParam('supabaseApiKey', credentialData, nodeData) @@ -157,14 +156,7 @@ class Supabase_VectorStores implements INode { const vectorStore = await SupabaseVectorStore.fromExistingIndex(embeddings, obj) - if (output === 'retriever') { - const retriever = vectorStore.asRetriever(k) - return retriever - } else if (output === 'vectorStore') { - ;(vectorStore as any).k = k - return vectorStore - } - return vectorStore + return resolveVectorStoreOrRetriever(nodeData, vectorStore) } } diff --git a/packages/components/nodes/vectorstores/Vectara/Vectara.ts b/packages/components/nodes/vectorstores/Vectara/Vectara.ts index 7460c586..939a4ac3 100644 --- a/packages/components/nodes/vectorstores/Vectara/Vectara.ts +++ b/packages/components/nodes/vectorstores/Vectara/Vectara.ts @@ -1,5 +1,5 @@ import { flatten } from 'lodash' -import { VectaraStore, VectaraLibArgs, VectaraFilter, VectaraContextConfig, VectaraFile } from 'langchain/vectorstores/vectara' +import { VectaraStore, VectaraLibArgs, VectaraFilter, VectaraContextConfig, VectaraFile, MMRConfig } from 'langchain/vectorstores/vectara' import { Document } from 'langchain/document' import { Embeddings } from 'langchain/embeddings/base' import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' @@ -22,7 +22,7 @@ class Vectara_VectorStores implements INode { constructor() { this.label = 'Vectara' this.name = 'vectara' - this.version = 1.0 + this.version = 2.0 this.type = 'Vectara' this.icon = 'vectara.png' this.category = 'Vector Stores' @@ -82,7 +82,9 @@ class Vectara_VectorStores implements INode { label: 'Lambda', name: 'lambda', description: - 'Improves retrieval accuracy by adjusting the balance (from 0 to 1) between neural search and keyword-based search factors.', + 'Enable hybrid search to improve retrieval accuracy by adjusting the balance (from 0 to 1) between neural search and keyword-based search factors.' + + 'A value of 0.0 means that only neural search is used, while a value of 1.0 means that only keyword-based search is used. Defaults to 0.0 (neural only).', + default: 0.0, type: 'number', additionalParams: true, optional: true @@ -90,8 +92,30 @@ class Vectara_VectorStores implements INode { { label: 'Top K', name: 'topK', - description: 'Number of top results to fetch. Defaults to 4', - placeholder: '4', + description: 'Number of top results to fetch. Defaults to 5', + placeholder: '5', + type: 'number', + additionalParams: true, + optional: true + }, + { + label: 'MMR K', + name: 'mmrK', + description: 'Number of top results to fetch for MMR. Defaults to 50', + placeholder: '50', + type: 'number', + additionalParams: true, + optional: true + }, + { + label: 'MMR diversity bias', + name: 'mmrDiversityBias', + step: 0.1, + description: + 'The diversity bias to use for MMR. This is a value between 0.0 and 1.0' + + 'Values closer to 1.0 optimize for the most diverse results.' + + 'Defaults to 0 (MMR disabled)', + placeholder: '0.0', type: 'number', additionalParams: true, optional: true @@ -191,7 +215,9 @@ class Vectara_VectorStores implements INode { const lambda = nodeData.inputs?.lambda as number const output = nodeData.outputs?.output as string const topK = nodeData.inputs?.topK as string - const k = topK ? parseFloat(topK) : 4 + const k = topK ? parseFloat(topK) : 5 + const mmrK = nodeData.inputs?.mmrK as number + const mmrDiversityBias = nodeData.inputs?.mmrDiversityBias as number const vectaraArgs: VectaraLibArgs = { apiKey: apiKey, @@ -208,6 +234,11 @@ class Vectara_VectorStores implements INode { if (sentencesBefore) vectaraContextConfig.sentencesBefore = sentencesBefore if (sentencesAfter) vectaraContextConfig.sentencesAfter = sentencesAfter vectaraFilter.contextConfig = vectaraContextConfig + const mmrConfig: MMRConfig = {} + mmrConfig.enabled = mmrDiversityBias > 0 + mmrConfig.mmrTopK = mmrK + mmrConfig.diversityBias = mmrDiversityBias + vectaraFilter.mmrConfig = mmrConfig const vectorStore = new VectaraStore(vectaraArgs) diff --git a/packages/components/nodes/vectorstores/VectorStoreUtils.ts b/packages/components/nodes/vectorstores/VectorStoreUtils.ts new file mode 100644 index 00000000..0d92587f --- /dev/null +++ b/packages/components/nodes/vectorstores/VectorStoreUtils.ts @@ -0,0 +1,75 @@ +import { INodeData } from '../../src' + +export const resolveVectorStoreOrRetriever = (nodeData: INodeData, vectorStore: any) => { + const output = nodeData.outputs?.output as string + const searchType = nodeData.outputs?.searchType as string + const topK = nodeData.inputs?.topK as string + const k = topK ? parseFloat(topK) : 4 + + if (output === 'retriever') { + if ('mmr' === searchType) { + const fetchK = nodeData.inputs?.fetchK as string + const lambda = nodeData.inputs?.lambda as string + const f = fetchK ? parseInt(fetchK) : 20 + const l = lambda ? parseFloat(lambda) : 0.5 + return vectorStore.asRetriever({ + searchType: 'mmr', + k: k, + searchKwargs: { + fetchK: f, + lambda: l + } + }) + } else { + // "searchType" is "similarity" + return vectorStore.asRetriever(k) + } + } else if (output === 'vectorStore') { + ;(vectorStore as any).k = k + return vectorStore + } +} + +export const addMMRInputParams = (inputs: any[]) => { + const mmrInputParams = [ + { + label: 'Search Type', + name: 'searchType', + type: 'options', + default: 'similarity', + options: [ + { + label: 'Similarity', + name: 'similarity' + }, + { + label: 'Max Marginal Relevance', + name: 'mmr' + } + ], + additionalParams: true, + optional: true + }, + { + label: 'Fetch K (for MMR Search)', + name: 'fetchK', + description: 'Number of initial documents to fetch for MMR reranking. Default to 20. Used only when the search type is MMR', + placeholder: '20', + type: 'number', + additionalParams: true, + optional: true + }, + { + label: 'Lambda (for MMR Search)', + name: 'lambda', + description: + 'Number between 0 and 1 that determines the degree of diversity among the results, where 0 corresponds to maximum diversity and 1 to minimum diversity. Used only when the search type is MMR', + placeholder: '0.5', + type: 'number', + additionalParams: true, + optional: true + } + ] + + inputs.push(...mmrInputParams) +} diff --git a/packages/components/nodes/vectorstores/Weaviate/Weaviate.ts b/packages/components/nodes/vectorstores/Weaviate/Weaviate.ts index 5c31c737..0eface58 100644 --- a/packages/components/nodes/vectorstores/Weaviate/Weaviate.ts +++ b/packages/components/nodes/vectorstores/Weaviate/Weaviate.ts @@ -5,6 +5,7 @@ import { Document } from 'langchain/document' import { Embeddings } from 'langchain/embeddings/base' import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' +import { addMMRInputParams, resolveVectorStoreOrRetriever } from '../VectorStoreUtils' class Weaviate_VectorStores implements INode { label: string @@ -23,12 +24,12 @@ class Weaviate_VectorStores implements INode { constructor() { this.label = 'Weaviate' this.name = 'weaviate' - this.version = 1.0 + this.version = 2.0 this.type = 'Weaviate' this.icon = 'weaviate.png' this.category = 'Vector Stores' this.description = - 'Upsert embedded data and perform similarity search upon query using Weaviate, a scalable open-source vector database' + 'Upsert embedded data and perform similarity or mmr search using Weaviate, a scalable open-source vector database' this.baseClasses = [this.type, 'VectorStoreRetriever', 'BaseRetriever'] this.badge = 'NEW' this.credential = { @@ -107,6 +108,7 @@ class Weaviate_VectorStores implements INode { optional: true } ] + addMMRInputParams(this.inputs) this.outputs = [ { label: 'Weaviate Retriever', @@ -174,9 +176,6 @@ class Weaviate_VectorStores implements INode { const weaviateTextKey = nodeData.inputs?.weaviateTextKey as string const weaviateMetadataKeys = nodeData.inputs?.weaviateMetadataKeys as string const embeddings = nodeData.inputs?.embeddings as Embeddings - const output = nodeData.outputs?.output as string - const topK = nodeData.inputs?.topK as string - const k = topK ? parseFloat(topK) : 4 const credentialData = await getCredentialData(nodeData.credential ?? '', options) const weaviateApiKey = getCredentialParam('weaviateApiKey', credentialData, nodeData) @@ -199,14 +198,7 @@ class Weaviate_VectorStores implements INode { const vectorStore = await WeaviateStore.fromExistingIndex(embeddings, obj) - if (output === 'retriever') { - const retriever = vectorStore.asRetriever(k) - return retriever - } else if (output === 'vectorStore') { - ;(vectorStore as any).k = k - return vectorStore - } - return vectorStore + return resolveVectorStoreOrRetriever(nodeData, vectorStore) } } diff --git a/packages/components/nodes/vectorstores/Zep/Zep.ts b/packages/components/nodes/vectorstores/Zep/Zep.ts index 21c885b4..3d9f1978 100644 --- a/packages/components/nodes/vectorstores/Zep/Zep.ts +++ b/packages/components/nodes/vectorstores/Zep/Zep.ts @@ -5,6 +5,7 @@ import { Embeddings } from 'langchain/embeddings/base' import { Document } from 'langchain/document' import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' +import { addMMRInputParams, resolveVectorStoreOrRetriever } from '../VectorStoreUtils' class Zep_VectorStores implements INode { label: string @@ -23,12 +24,12 @@ class Zep_VectorStores implements INode { constructor() { this.label = 'Zep' this.name = 'zep' - this.version = 1.0 + this.version = 2.0 this.type = 'Zep' - this.icon = 'zep.png' + this.icon = 'zep.svg' this.category = 'Vector Stores' this.description = - 'Upsert embedded data and perform similarity search upon query using Zep, a fast and scalable building block for LLM apps' + 'Upsert embedded data and perform similarity or mmr search upon query using Zep, a fast and scalable building block for LLM apps' this.baseClasses = [this.type, 'VectorStoreRetriever', 'BaseRetriever'] this.badge = 'NEW' this.credential = { @@ -88,6 +89,7 @@ class Zep_VectorStores implements INode { optional: true } ] + addMMRInputParams(this.inputs) this.outputs = [ { label: 'Zep Retriever', @@ -144,9 +146,6 @@ class Zep_VectorStores implements INode { const zepMetadataFilter = nodeData.inputs?.zepMetadataFilter const dimension = nodeData.inputs?.dimension as number const embeddings = nodeData.inputs?.embeddings as Embeddings - const output = nodeData.outputs?.output as string - const topK = nodeData.inputs?.topK as string - const k = topK ? parseFloat(topK) : 4 const credentialData = await getCredentialData(nodeData.credential ?? '', options) const apiKey = getCredentialParam('apiKey', credentialData, nodeData) @@ -165,14 +164,7 @@ class Zep_VectorStores implements INode { const vectorStore = await ZepExistingVS.fromExistingIndex(embeddings, zepConfig) - if (output === 'retriever') { - const retriever = vectorStore.asRetriever(k) - return retriever - } else if (output === 'vectorStore') { - ;(vectorStore as any).k = k - return vectorStore - } - return vectorStore + return resolveVectorStoreOrRetriever(nodeData, vectorStore) } } @@ -210,7 +202,7 @@ class ZepExistingVS extends ZepVectorStore { this.args = args } - async initalizeCollection(args: IZepConfig & Partial) { + async initializeCollection(args: IZepConfig & Partial) { this.client = await ZepClient.init(args.apiUrl, args.apiKey) try { this.collection = await this.client.document.getCollection(args.collectionName) @@ -259,7 +251,7 @@ class ZepExistingVS extends ZepVectorStore { const newfilter = { where: { and: ANDFilters } } - await this.initalizeCollection(this.args!).catch((err) => { + await this.initializeCollection(this.args!).catch((err) => { console.error('Error initializing collection:', err) throw err }) diff --git a/packages/components/nodes/vectorstores/Zep/Zep_Existing.ts b/packages/components/nodes/vectorstores/Zep/Zep_Existing.ts index 5e9d7e1f..e6dbd252 100644 --- a/packages/components/nodes/vectorstores/Zep/Zep_Existing.ts +++ b/packages/components/nodes/vectorstores/Zep/Zep_Existing.ts @@ -24,7 +24,7 @@ class Zep_Existing_VectorStores implements INode { this.name = 'zepExistingIndex' this.version = 1.0 this.type = 'Zep' - this.icon = 'zep.png' + this.icon = 'zep.svg' this.category = 'Vector Stores' this.description = 'Load existing index from Zep (i.e: Document has been upserted)' this.baseClasses = [this.type, 'VectorStoreRetriever', 'BaseRetriever'] diff --git a/packages/components/nodes/vectorstores/Zep/Zep_Upsert.ts b/packages/components/nodes/vectorstores/Zep/Zep_Upsert.ts index 40b08cbd..0b2e003a 100644 --- a/packages/components/nodes/vectorstores/Zep/Zep_Upsert.ts +++ b/packages/components/nodes/vectorstores/Zep/Zep_Upsert.ts @@ -24,7 +24,7 @@ class Zep_Upsert_VectorStores implements INode { this.name = 'zepUpsert' this.version = 1.0 this.type = 'Zep' - this.icon = 'zep.png' + this.icon = 'zep.svg' this.category = 'Vector Stores' this.description = 'Upsert documents to Zep' this.baseClasses = [this.type, 'VectorStoreRetriever', 'BaseRetriever'] diff --git a/packages/components/nodes/vectorstores/Zep/zep.png b/packages/components/nodes/vectorstores/Zep/zep.png deleted file mode 100644 index 2fdb2382..00000000 Binary files a/packages/components/nodes/vectorstores/Zep/zep.png and /dev/null differ diff --git a/packages/components/nodes/vectorstores/Zep/zep.svg b/packages/components/nodes/vectorstores/Zep/zep.svg new file mode 100644 index 00000000..6cbbaad2 --- /dev/null +++ b/packages/components/nodes/vectorstores/Zep/zep.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/packages/components/package.json b/packages/components/package.json index d1485b37..80ad2975 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "flowise-components", - "version": "1.4.5", + "version": "1.5.1", "description": "Flowiseai Components", "main": "dist/src/index", "types": "dist/src/index.d.ts", @@ -19,24 +19,29 @@ "@aws-sdk/client-bedrock-runtime": "3.422.0", "@aws-sdk/client-dynamodb": "^3.360.0", "@aws-sdk/client-s3": "^3.427.0", + "@datastax/astra-db-ts": "^0.1.2", "@dqbd/tiktoken": "^1.0.7", "@elastic/elasticsearch": "^8.9.0", "@getzep/zep-js": "^0.9.0", "@gomomento/sdk": "^1.51.1", "@gomomento/sdk-core": "^1.51.1", "@google-ai/generativelanguage": "^0.2.1", + "@google/generative-ai": "^0.1.3", "@huggingface/inference": "^2.6.1", + "@langchain/community": "^0.0.16", + "@langchain/google-genai": "^0.0.6", + "@langchain/mistralai": "^0.0.6", "@notionhq/client": "^2.2.8", "@opensearch-project/opensearch": "^1.2.0", - "@pinecone-database/pinecone": "^1.1.1", + "@pinecone-database/pinecone": "^2.0.1", "@qdrant/js-client-rest": "^1.2.2", "@supabase/supabase-js": "^2.29.0", "@types/js-yaml": "^4.0.5", "@types/jsdom": "^21.1.1", - "@upstash/redis": "^1.22.1", + "@upstash/redis": "1.22.1", "@zilliz/milvus2-sdk-node": "^2.2.24", "apify-client": "^2.7.1", - "axios": "^0.27.2", + "axios": "1.6.2", "cheerio": "^1.0.0-rc.12", "chromadb": "^1.5.11", "cohere-ai": "^6.2.0", @@ -44,17 +49,19 @@ "dotenv": "^16.0.0", "express": "^4.17.3", "faiss-node": "^0.2.2", + "fast-json-patch": "^3.1.1", "form-data": "^4.0.0", - "google-auth-library": "^9.0.0", + "google-auth-library": "^9.4.0", "graphql": "^16.6.0", "html-to-text": "^9.0.5", "husky": "^8.0.3", "ioredis": "^5.3.2", - "langchain": "^0.0.196", - "langfuse-langchain": "^1.0.31", - "langsmith": "^0.0.32", + "langchain": "^0.0.214", + "langfuse": "2.0.2", + "langfuse-langchain": "2.3.3", + "langsmith": "0.0.53", "linkifyjs": "^4.1.1", - "llamaindex": "^0.0.30", + "llamaindex": "^0.0.48", "llmonitor": "^0.5.5", "mammoth": "^1.5.1", "moment": "^2.29.3", diff --git a/packages/components/src/Interface.ts b/packages/components/src/Interface.ts index 53d72cb4..40d84d06 100644 --- a/packages/components/src/Interface.ts +++ b/packages/components/src/Interface.ts @@ -29,6 +29,12 @@ export interface ICommonObject { [key: string]: any | CommonType | ICommonObject | CommonType[] | ICommonObject[] } +export interface IVariable { + name: string + value: string + type: string +} + export type IDatabaseEntity = { [key: string]: any } @@ -73,6 +79,7 @@ export interface INodeParams { additionalParams?: boolean loadMethod?: string hidden?: boolean + variables?: ICommonObject[] } export interface INodeExecutionData { @@ -89,7 +96,7 @@ export interface INodeProperties { type: string icon: string version: number - category: string + category: string // TODO: use enum instead of string baseClasses: string[] tags?: string[] description?: string @@ -192,3 +199,45 @@ export class VectorStoreRetriever { this.vectorStore = fields.vectorStore } } + +/** + * Implement abstract classes and interface for memory + */ +import { BaseMessage } from 'langchain/schema' +import { BufferMemory, BufferWindowMemory, ConversationSummaryMemory } from 'langchain/memory' + +export interface MemoryMethods { + getChatMessages(overrideSessionId?: string, returnBaseMessages?: boolean, prevHistory?: IMessage[]): Promise + addChatMessages(msgArray: { text: string; type: MessageType }[], overrideSessionId?: string): Promise + clearChatMessages(overrideSessionId?: string): Promise +} + +export abstract class FlowiseMemory extends BufferMemory implements MemoryMethods { + abstract getChatMessages( + overrideSessionId?: string, + returnBaseMessages?: boolean, + prevHistory?: IMessage[] + ): Promise + abstract addChatMessages(msgArray: { text: string; type: MessageType }[], overrideSessionId?: string): Promise + abstract clearChatMessages(overrideSessionId?: string): Promise +} + +export abstract class FlowiseWindowMemory extends BufferWindowMemory implements MemoryMethods { + abstract getChatMessages( + overrideSessionId?: string, + returnBaseMessages?: boolean, + prevHistory?: IMessage[] + ): Promise + abstract addChatMessages(msgArray: { text: string; type: MessageType }[], overrideSessionId?: string): Promise + abstract clearChatMessages(overrideSessionId?: string): Promise +} + +export abstract class FlowiseSummaryMemory extends ConversationSummaryMemory implements MemoryMethods { + abstract getChatMessages( + overrideSessionId?: string, + returnBaseMessages?: boolean, + prevHistory?: IMessage[] + ): Promise + abstract addChatMessages(msgArray: { text: string; type: MessageType }[], overrideSessionId?: string): Promise + abstract clearChatMessages(overrideSessionId?: string): Promise +} diff --git a/packages/components/src/agents.ts b/packages/components/src/agents.ts new file mode 100644 index 00000000..5e241d50 --- /dev/null +++ b/packages/components/src/agents.ts @@ -0,0 +1,624 @@ +import { AgentExecutorInput, BaseSingleActionAgent, BaseMultiActionAgent, RunnableAgent, StoppingMethod } from 'langchain/agents' +import { ChainValues, AgentStep, AgentFinish, AgentAction, BaseMessage, FunctionMessage, AIMessage } from 'langchain/schema' +import { OutputParserException } from 'langchain/schema/output_parser' +import { CallbackManager, CallbackManagerForChainRun, Callbacks } from 'langchain/callbacks' +import { ToolInputParsingException, Tool } from '@langchain/core/tools' +import { Runnable } from 'langchain/schema/runnable' +import { BaseChain, SerializedLLMChain } from 'langchain/chains' +import { Serializable } from '@langchain/core/load/serializable' + +type AgentExecutorOutput = ChainValues + +interface AgentExecutorIteratorInput { + agentExecutor: AgentExecutor + inputs: Record + callbacks?: Callbacks + tags?: string[] + metadata?: Record + runName?: string + runManager?: CallbackManagerForChainRun +} + +//TODO: stream tools back +export class AgentExecutorIterator extends Serializable implements AgentExecutorIteratorInput { + lc_namespace = ['langchain', 'agents', 'executor_iterator'] + + agentExecutor: AgentExecutor + + inputs: Record + + callbacks: Callbacks + + tags: string[] | undefined + + metadata: Record | undefined + + runName: string | undefined + + private _finalOutputs: Record | undefined + + get finalOutputs(): Record | undefined { + return this._finalOutputs + } + + /** Intended to be used as a setter method, needs to be async. */ + async setFinalOutputs(value: Record | undefined) { + this._finalOutputs = undefined + if (value) { + const preparedOutputs: Record = await this.agentExecutor.prepOutputs(this.inputs, value, true) + this._finalOutputs = preparedOutputs + } + } + + runManager: CallbackManagerForChainRun | undefined + + intermediateSteps: AgentStep[] = [] + + iterations = 0 + + get nameToToolMap(): Record { + const toolMap = this.agentExecutor.tools.map((tool) => ({ + [tool.name]: tool + })) + return Object.assign({}, ...toolMap) + } + + constructor(fields: AgentExecutorIteratorInput) { + super(fields) + this.agentExecutor = fields.agentExecutor + this.inputs = fields.inputs + this.tags = fields.tags + this.metadata = fields.metadata + this.runName = fields.runName + this.runManager = fields.runManager + } + + /** + * Reset the iterator to its initial state, clearing intermediate steps, + * iterations, and the final output. + */ + reset(): void { + this.intermediateSteps = [] + this.iterations = 0 + this._finalOutputs = undefined + } + + updateIterations(): void { + this.iterations += 1 + } + + async *streamIterator() { + this.reset() + + // Loop to handle iteration + while (true) { + try { + if (this.iterations === 0) { + await this.onFirstStep() + } + + const result = await this._callNext() + yield result + } catch (e: any) { + if ('message' in e && e.message.startsWith('Final outputs already reached: ')) { + if (!this.finalOutputs) { + throw e + } + return this.finalOutputs + } + if (this.runManager) { + await this.runManager.handleChainError(e) + } + throw e + } + } + } + + /** + * Perform any necessary setup for the first step + * of the asynchronous iterator. + */ + async onFirstStep(): Promise { + if (this.iterations === 0) { + const callbackManager = await CallbackManager.configure( + this.callbacks, + this.agentExecutor.callbacks, + this.tags, + this.agentExecutor.tags, + this.metadata, + this.agentExecutor.metadata, + { + verbose: this.agentExecutor.verbose + } + ) + this.runManager = await callbackManager?.handleChainStart( + this.agentExecutor.toJSON(), + this.inputs, + undefined, + undefined, + this.tags, + this.metadata, + this.runName + ) + } + } + + /** + * Execute the next step in the chain using the + * AgentExecutor's _takeNextStep method. + */ + async _executeNextStep(runManager?: CallbackManagerForChainRun): Promise { + return this.agentExecutor._takeNextStep(this.nameToToolMap, this.inputs, this.intermediateSteps, runManager) + } + + /** + * Process the output of the next step, + * handling AgentFinish and tool return cases. + */ + async _processNextStepOutput( + nextStepOutput: AgentFinish | AgentStep[], + runManager?: CallbackManagerForChainRun + ): Promise> { + if ('returnValues' in nextStepOutput) { + const output = await this.agentExecutor._return(nextStepOutput as AgentFinish, this.intermediateSteps, runManager) + if (this.runManager) { + await this.runManager.handleChainEnd(output) + } + await this.setFinalOutputs(output) + return output + } + + this.intermediateSteps = this.intermediateSteps.concat(nextStepOutput as AgentStep[]) + + let output: Record = {} + if (Array.isArray(nextStepOutput) && nextStepOutput.length === 1) { + const nextStep = nextStepOutput[0] + const toolReturn = await this.agentExecutor._getToolReturn(nextStep) + if (toolReturn) { + output = await this.agentExecutor._return(toolReturn, this.intermediateSteps, runManager) + if (this.runManager) { + await this.runManager.handleChainEnd(output) + } + await this.setFinalOutputs(output) + } + } + output = { intermediateSteps: nextStepOutput as AgentStep[] } + return output + } + + async _stop(): Promise> { + const output = await this.agentExecutor.agent.returnStoppedResponse( + this.agentExecutor.earlyStoppingMethod, + this.intermediateSteps, + this.inputs + ) + const returnedOutput = await this.agentExecutor._return(output, this.intermediateSteps, this.runManager) + await this.setFinalOutputs(returnedOutput) + return returnedOutput + } + + async _callNext(): Promise> { + // final output already reached: stopiteration (final output) + if (this.finalOutputs) { + throw new Error(`Final outputs already reached: ${JSON.stringify(this.finalOutputs, null, 2)}`) + } + // timeout/max iterations: stopiteration (stopped response) + if (!this.agentExecutor.shouldContinueGetter(this.iterations)) { + return this._stop() + } + const nextStepOutput = await this._executeNextStep(this.runManager) + const output = await this._processNextStepOutput(nextStepOutput, this.runManager) + this.updateIterations() + return output + } +} + +export class AgentExecutor extends BaseChain { + static lc_name() { + return 'AgentExecutor' + } + + get lc_namespace() { + return ['langchain', 'agents', 'executor'] + } + + agent: BaseSingleActionAgent | BaseMultiActionAgent + + tools: this['agent']['ToolType'][] + + returnIntermediateSteps = false + + maxIterations?: number = 15 + + earlyStoppingMethod: StoppingMethod = 'force' + + sessionId?: string + + chatId?: string + + input?: string + + /** + * How to handle errors raised by the agent's output parser. + Defaults to `False`, which raises the error. + + If `true`, the error will be sent back to the LLM as an observation. + If a string, the string itself will be sent to the LLM as an observation. + If a callable function, the function will be called with the exception + as an argument, and the result of that function will be passed to the agent + as an observation. + */ + handleParsingErrors: boolean | string | ((e: OutputParserException | ToolInputParsingException) => string) = false + + get inputKeys() { + return this.agent.inputKeys + } + + get outputKeys() { + return this.agent.returnValues + } + + constructor(input: AgentExecutorInput & { sessionId?: string; chatId?: string; input?: string }) { + let agent: BaseSingleActionAgent | BaseMultiActionAgent + if (Runnable.isRunnable(input.agent)) { + agent = new RunnableAgent({ runnable: input.agent }) + } else { + agent = input.agent + } + + super(input) + this.agent = agent + this.tools = input.tools + this.handleParsingErrors = input.handleParsingErrors ?? this.handleParsingErrors + /* Getting rid of this because RunnableAgent doesnt allow return direct + if (this.agent._agentActionType() === "multi") { + for (const tool of this.tools) { + if (tool.returnDirect) { + throw new Error( + `Tool with return direct ${tool.name} not supported for multi-action agent.` + ); + } + } + }*/ + this.returnIntermediateSteps = input.returnIntermediateSteps ?? this.returnIntermediateSteps + this.maxIterations = input.maxIterations ?? this.maxIterations + this.earlyStoppingMethod = input.earlyStoppingMethod ?? this.earlyStoppingMethod + this.sessionId = input.sessionId + this.chatId = input.chatId + this.input = input.input + } + + static fromAgentAndTools(fields: AgentExecutorInput & { sessionId?: string; chatId?: string; input?: string }): AgentExecutor { + const newInstance = new AgentExecutor(fields) + if (fields.sessionId) newInstance.sessionId = fields.sessionId + if (fields.chatId) newInstance.chatId = fields.chatId + if (fields.input) newInstance.input = fields.input + return newInstance + } + + get shouldContinueGetter() { + return this.shouldContinue.bind(this) + } + + /** + * Method that checks if the agent execution should continue based on the + * number of iterations. + * @param iterations The current number of iterations. + * @returns A boolean indicating whether the agent execution should continue. + */ + private shouldContinue(iterations: number): boolean { + return this.maxIterations === undefined || iterations < this.maxIterations + } + + async _call(inputs: ChainValues, runManager?: CallbackManagerForChainRun): Promise { + const toolsByName = Object.fromEntries(this.tools.map((t) => [t.name.toLowerCase(), t])) + + const steps: AgentStep[] = [] + let iterations = 0 + + const getOutput = async (finishStep: AgentFinish): Promise => { + const { returnValues } = finishStep + const additional = await this.agent.prepareForOutput(returnValues, steps) + + if (this.returnIntermediateSteps) { + return { ...returnValues, intermediateSteps: steps, ...additional } + } + await runManager?.handleAgentEnd(finishStep) + return { ...returnValues, ...additional } + } + + while (this.shouldContinue(iterations)) { + let output + try { + output = await this.agent.plan(steps, inputs, runManager?.getChild()) + } catch (e) { + if (e instanceof OutputParserException) { + let observation + let text = e.message + if (this.handleParsingErrors === true) { + if (e.sendToLLM) { + observation = e.observation + text = e.llmOutput ?? '' + } else { + observation = 'Invalid or incomplete response' + } + } else if (typeof this.handleParsingErrors === 'string') { + observation = this.handleParsingErrors + } else if (typeof this.handleParsingErrors === 'function') { + observation = this.handleParsingErrors(e) + } else { + throw e + } + output = { + tool: '_Exception', + toolInput: observation, + log: text + } as AgentAction + } else { + throw e + } + } + // Check if the agent has finished + if ('returnValues' in output) { + return getOutput(output) + } + + let actions: AgentAction[] + if (Array.isArray(output)) { + actions = output as AgentAction[] + } else { + actions = [output as AgentAction] + } + + const newSteps = await Promise.all( + actions.map(async (action) => { + await runManager?.handleAgentAction(action) + const tool = action.tool === '_Exception' ? new ExceptionTool() : toolsByName[action.tool?.toLowerCase()] + let observation + try { + /* Here we need to override Tool call method to include sessionId, chatId, input as parameter + * Tool Call Parameters: + * - arg: z.output + * - configArg?: RunnableConfig | Callbacks + * - tags?: string[] + * - flowConfig?: { sessionId?: string, chatId?: string, input?: string } + */ + observation = tool + ? // @ts-ignore + await tool.call(action.toolInput, runManager?.getChild(), undefined, { + sessionId: this.sessionId, + chatId: this.chatId, + input: this.input + }) + : `${action.tool} is not a valid tool, try another one.` + } catch (e) { + if (e instanceof ToolInputParsingException) { + if (this.handleParsingErrors === true) { + observation = 'Invalid or incomplete tool input. Please try again.' + } else if (typeof this.handleParsingErrors === 'string') { + observation = this.handleParsingErrors + } else if (typeof this.handleParsingErrors === 'function') { + observation = this.handleParsingErrors(e) + } else { + throw e + } + observation = await new ExceptionTool().call(observation, runManager?.getChild()) + return { action, observation: observation ?? '' } + } + } + return { action, observation: observation ?? '' } + }) + ) + + steps.push(...newSteps) + + const lastStep = steps[steps.length - 1] + const lastTool = toolsByName[lastStep.action.tool?.toLowerCase()] + + if (lastTool?.returnDirect) { + return getOutput({ + returnValues: { [this.agent.returnValues[0]]: lastStep.observation }, + log: '' + }) + } + + iterations += 1 + } + + const finish = await this.agent.returnStoppedResponse(this.earlyStoppingMethod, steps, inputs) + + return getOutput(finish) + } + + async _takeNextStep( + nameToolMap: Record, + inputs: ChainValues, + intermediateSteps: AgentStep[], + runManager?: CallbackManagerForChainRun + ): Promise { + let output + try { + output = await this.agent.plan(intermediateSteps, inputs, runManager?.getChild()) + } catch (e) { + if (e instanceof OutputParserException) { + let observation + let text = e.message + if (this.handleParsingErrors === true) { + if (e.sendToLLM) { + observation = e.observation + text = e.llmOutput ?? '' + } else { + observation = 'Invalid or incomplete response' + } + } else if (typeof this.handleParsingErrors === 'string') { + observation = this.handleParsingErrors + } else if (typeof this.handleParsingErrors === 'function') { + observation = this.handleParsingErrors(e) + } else { + throw e + } + output = { + tool: '_Exception', + toolInput: observation, + log: text + } as AgentAction + } else { + throw e + } + } + + if ('returnValues' in output) { + return output + } + + let actions: AgentAction[] + if (Array.isArray(output)) { + actions = output as AgentAction[] + } else { + actions = [output as AgentAction] + } + + const result: AgentStep[] = [] + for (const agentAction of actions) { + let observation = '' + if (runManager) { + await runManager?.handleAgentAction(agentAction) + } + if (agentAction.tool in nameToolMap) { + const tool = nameToolMap[agentAction.tool] + try { + /* Here we need to override Tool call method to include sessionId, chatId, input as parameter + * Tool Call Parameters: + * - arg: z.output + * - configArg?: RunnableConfig | Callbacks + * - tags?: string[] + * - flowConfig?: { sessionId?: string, chatId?: string, input?: string } + */ + // @ts-ignore + observation = await tool.call(agentAction.toolInput, runManager?.getChild(), undefined, { + sessionId: this.sessionId, + chatId: this.chatId, + input: this.input + }) + } catch (e) { + if (e instanceof ToolInputParsingException) { + if (this.handleParsingErrors === true) { + observation = 'Invalid or incomplete tool input. Please try again.' + } else if (typeof this.handleParsingErrors === 'string') { + observation = this.handleParsingErrors + } else if (typeof this.handleParsingErrors === 'function') { + observation = this.handleParsingErrors(e) + } else { + throw e + } + observation = await new ExceptionTool().call(observation, runManager?.getChild()) + } + } + } else { + observation = `${agentAction.tool} is not a valid tool, try another available tool: ${Object.keys(nameToolMap).join(', ')}` + } + result.push({ + action: agentAction, + observation + }) + } + return result + } + + async _return( + output: AgentFinish, + intermediateSteps: AgentStep[], + runManager?: CallbackManagerForChainRun + ): Promise { + if (runManager) { + await runManager.handleAgentEnd(output) + } + const finalOutput: Record = output.returnValues + if (this.returnIntermediateSteps) { + finalOutput.intermediateSteps = intermediateSteps + } + return finalOutput + } + + async _getToolReturn(nextStepOutput: AgentStep): Promise { + const { action, observation } = nextStepOutput + const nameToolMap = Object.fromEntries(this.tools.map((t) => [t.name.toLowerCase(), t])) + const [returnValueKey = 'output'] = this.agent.returnValues + // Invalid tools won't be in the map, so we return False. + if (action.tool in nameToolMap) { + if (nameToolMap[action.tool].returnDirect) { + return { + returnValues: { [returnValueKey]: observation }, + log: '' + } + } + } + return null + } + + _returnStoppedResponse(earlyStoppingMethod: StoppingMethod) { + if (earlyStoppingMethod === 'force') { + return { + returnValues: { + output: 'Agent stopped due to iteration limit or time limit.' + }, + log: '' + } as AgentFinish + } + throw new Error(`Got unsupported early_stopping_method: ${earlyStoppingMethod}`) + } + + async *_streamIterator(inputs: Record): AsyncGenerator { + const agentExecutorIterator = new AgentExecutorIterator({ + inputs, + agentExecutor: this, + metadata: this.metadata, + tags: this.tags, + callbacks: this.callbacks + }) + const iterator = agentExecutorIterator.streamIterator() + for await (const step of iterator) { + if (!step) { + continue + } + yield step + } + } + + _chainType() { + return 'agent_executor' as const + } + + serialize(): SerializedLLMChain { + throw new Error('Cannot serialize an AgentExecutor') + } +} + +class ExceptionTool extends Tool { + name = '_Exception' + + description = 'Exception tool' + + async _call(query: string) { + return query + } +} + +export const formatAgentSteps = (steps: AgentStep[]): BaseMessage[] => + steps.flatMap(({ action, observation }) => { + const create_function_message = (observation: string, action: AgentAction) => { + let content: string + if (typeof observation !== 'string') { + content = JSON.stringify(observation) + } else { + content = observation + } + return new FunctionMessage(content, action.tool) + } + if ('messageLog' in action && action.messageLog !== undefined) { + const log = action.messageLog as BaseMessage[] + return log.concat(create_function_message(observation, action)) + } else { + return [new AIMessage(action.log)] + } + }) diff --git a/packages/components/src/handler.ts b/packages/components/src/handler.ts index 456cf39c..5d2b53f6 100644 --- a/packages/components/src/handler.ts +++ b/packages/components/src/handler.ts @@ -1,13 +1,17 @@ -import { BaseTracer, Run, BaseCallbackHandler } from 'langchain/callbacks' +import { BaseTracer, Run, BaseCallbackHandler, LangChainTracer } from 'langchain/callbacks' import { AgentAction, ChainValues } from 'langchain/schema' import { Logger } from 'winston' import { Server } from 'socket.io' import { Client } from 'langsmith' -import { LangChainTracer } from 'langchain/callbacks' -import { LLMonitorHandler } from 'langchain/callbacks/handlers/llmonitor' +import { LLMonitorHandler, LLMonitorHandlerFields } from 'langchain/callbacks/handlers/llmonitor' import { getCredentialData, getCredentialParam } from './utils' import { ICommonObject, INodeData } from './Interface' import CallbackHandler from 'langfuse-langchain' +import { LangChainTracerFields } from '@langchain/core/tracers/tracer_langchain' +import { RunTree, RunTreeConfig, Client as LangsmithClient } from 'langsmith' +import { Langfuse, LangfuseTraceClient, LangfuseSpanClient, LangfuseGenerationClient } from 'langfuse' +import monitor from 'llmonitor' +import { v4 as uuidv4 } from 'uuid' interface AgentRun extends Run { actions: AgentAction[] @@ -231,11 +235,17 @@ export const additionalCallbacks = async (nodeData: INodeData, options: ICommonO apiKey: langSmithApiKey }) - const tracer = new LangChainTracer({ + let langSmithField: LangChainTracerFields = { projectName: langSmithProject ?? 'default', //@ts-ignore client - }) + } + + if (nodeData?.inputs?.analytics?.langSmith) { + langSmithField = { ...langSmithField, ...nodeData?.inputs?.analytics?.langSmith } + } + + const tracer = new LangChainTracer(langSmithField) callbacks.push(tracer) } else if (provider === 'langFuse') { const release = analytic[provider].release as string @@ -244,13 +254,17 @@ export const additionalCallbacks = async (nodeData: INodeData, options: ICommonO const langFusePublicKey = getCredentialParam('langFusePublicKey', credentialData, nodeData) const langFuseEndpoint = getCredentialParam('langFuseEndpoint', credentialData, nodeData) - const langFuseOptions: any = { + let langFuseOptions: any = { secretKey: langFuseSecretKey, publicKey: langFusePublicKey, baseUrl: langFuseEndpoint ?? 'https://cloud.langfuse.com' } if (release) langFuseOptions.release = release - if (options.chatId) langFuseOptions.userId = options.chatId + if (options.chatId) langFuseOptions.sessionId = options.chatId + + if (nodeData?.inputs?.analytics?.langFuse) { + langFuseOptions = { ...langFuseOptions, ...nodeData?.inputs?.analytics?.langFuse } + } const handler = new CallbackHandler(langFuseOptions) callbacks.push(handler) @@ -258,11 +272,15 @@ export const additionalCallbacks = async (nodeData: INodeData, options: ICommonO const llmonitorAppId = getCredentialParam('llmonitorAppId', credentialData, nodeData) const llmonitorEndpoint = getCredentialParam('llmonitorEndpoint', credentialData, nodeData) - const llmonitorFields: ICommonObject = { + let llmonitorFields: LLMonitorHandlerFields = { appId: llmonitorAppId, apiUrl: llmonitorEndpoint ?? 'https://app.llmonitor.com' } + if (nodeData?.inputs?.analytics?.llmonitor) { + llmonitorFields = { ...llmonitorFields, ...nodeData?.inputs?.analytics?.llmonitor } + } + const handler = new LLMonitorHandler(llmonitorFields) callbacks.push(handler) } @@ -273,3 +291,491 @@ export const additionalCallbacks = async (nodeData: INodeData, options: ICommonO throw new Error(e) } } + +export class AnalyticHandler { + nodeData: INodeData + options: ICommonObject = {} + handlers: ICommonObject = {} + + constructor(nodeData: INodeData, options: ICommonObject) { + this.options = options + this.nodeData = nodeData + this.init() + } + + async init() { + try { + if (!this.options.analytic) return + + const analytic = JSON.parse(this.options.analytic) + + for (const provider in analytic) { + const providerStatus = analytic[provider].status as boolean + + if (providerStatus) { + const credentialId = analytic[provider].credentialId as string + const credentialData = await getCredentialData(credentialId ?? '', this.options) + if (provider === 'langSmith') { + const langSmithProject = analytic[provider].projectName as string + const langSmithApiKey = getCredentialParam('langSmithApiKey', credentialData, this.nodeData) + const langSmithEndpoint = getCredentialParam('langSmithEndpoint', credentialData, this.nodeData) + + const client = new LangsmithClient({ + apiUrl: langSmithEndpoint ?? 'https://api.smith.langchain.com', + apiKey: langSmithApiKey + }) + + this.handlers['langSmith'] = { client, langSmithProject } + } else if (provider === 'langFuse') { + const release = analytic[provider].release as string + const langFuseSecretKey = getCredentialParam('langFuseSecretKey', credentialData, this.nodeData) + const langFusePublicKey = getCredentialParam('langFusePublicKey', credentialData, this.nodeData) + const langFuseEndpoint = getCredentialParam('langFuseEndpoint', credentialData, this.nodeData) + + const langfuse = new Langfuse({ + secretKey: langFuseSecretKey, + publicKey: langFusePublicKey, + baseUrl: langFuseEndpoint ?? 'https://cloud.langfuse.com', + release + }) + this.handlers['langFuse'] = { client: langfuse } + } else if (provider === 'llmonitor') { + const llmonitorAppId = getCredentialParam('llmonitorAppId', credentialData, this.nodeData) + const llmonitorEndpoint = getCredentialParam('llmonitorEndpoint', credentialData, this.nodeData) + + monitor.init({ + appId: llmonitorAppId, + apiUrl: llmonitorEndpoint + }) + + this.handlers['llmonitor'] = { client: monitor } + } + } + } + } catch (e) { + throw new Error(e) + } + } + + async onChainStart(name: string, input: string, parentIds?: ICommonObject) { + const returnIds: ICommonObject = { + langSmith: {}, + langFuse: {}, + llmonitor: {} + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'langSmith')) { + if (!parentIds || !Object.keys(parentIds).length) { + const parentRunConfig: RunTreeConfig = { + name, + run_type: 'chain', + inputs: { + text: input + }, + serialized: {}, + project_name: this.handlers['langSmith'].langSmithProject, + client: this.handlers['langSmith'].client, + ...this.nodeData?.inputs?.analytics?.langSmith + } + const parentRun = new RunTree(parentRunConfig) + await parentRun.postRun() + this.handlers['langSmith'].chainRun = { [parentRun.id]: parentRun } + returnIds['langSmith'].chainRun = parentRun.id + } else { + const parentRun: RunTree | undefined = this.handlers['langSmith'].chainRun[parentIds['langSmith'].chainRun] + if (parentRun) { + const childChainRun = await parentRun.createChild({ + name, + run_type: 'chain', + inputs: { + text: input + } + }) + await childChainRun.postRun() + this.handlers['langSmith'].chainRun = { [childChainRun.id]: childChainRun } + returnIds['langSmith'].chainRun = childChainRun.id + } + } + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'langFuse')) { + let langfuseTraceClient: LangfuseTraceClient + + if (!parentIds || !Object.keys(parentIds).length) { + const langfuse: Langfuse = this.handlers['langFuse'].client + langfuseTraceClient = langfuse.trace({ + name, + sessionId: this.options.chatId, + metadata: { tags: ['openai-assistant'] }, + ...this.nodeData?.inputs?.analytics?.langFuse + }) + } else { + langfuseTraceClient = this.handlers['langFuse'].trace[parentIds['langFuse']] + } + + if (langfuseTraceClient) { + const span = langfuseTraceClient.span({ + name, + input: { + text: input + } + }) + this.handlers['langFuse'].trace = { [langfuseTraceClient.id]: langfuseTraceClient } + this.handlers['langFuse'].span = { [span.id]: span } + returnIds['langFuse'].trace = langfuseTraceClient.id + returnIds['langFuse'].span = span.id + } + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'llmonitor')) { + const monitor = this.handlers['llmonitor'].client + + if (monitor) { + const runId = uuidv4() + await monitor.trackEvent('chain', 'start', { + runId, + name, + userId: this.options.chatId, + input, + ...this.nodeData?.inputs?.analytics?.llmonitor + }) + this.handlers['llmonitor'].chainEvent = { [runId]: runId } + returnIds['llmonitor'].chainEvent = runId + } + } + + return returnIds + } + + async onChainEnd(returnIds: ICommonObject, output: string | object, shutdown = false) { + if (Object.prototype.hasOwnProperty.call(this.handlers, 'langSmith')) { + const chainRun: RunTree | undefined = this.handlers['langSmith'].chainRun[returnIds['langSmith'].chainRun] + if (chainRun) { + await chainRun.end({ + outputs: { + output + } + }) + await chainRun.patchRun() + } + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'langFuse')) { + const span: LangfuseSpanClient | undefined = this.handlers['langFuse'].span[returnIds['langFuse'].span] + if (span) { + span.end({ + output + }) + if (shutdown) { + const langfuse: Langfuse = this.handlers['langFuse'].client + await langfuse.shutdownAsync() + } + } + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'llmonitor')) { + const chainEventId = returnIds['llmonitor'].chainEvent + const monitor = this.handlers['llmonitor'].client + + if (monitor && chainEventId) { + await monitor.trackEvent('chain', 'end', { + runId: chainEventId, + output + }) + } + } + } + + async onChainError(returnIds: ICommonObject, error: string | object, shutdown = false) { + if (Object.prototype.hasOwnProperty.call(this.handlers, 'langSmith')) { + const chainRun: RunTree | undefined = this.handlers['langSmith'].chainRun[returnIds['langSmith'].chainRun] + if (chainRun) { + await chainRun.end({ + error: { + error + } + }) + await chainRun.patchRun() + } + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'langFuse')) { + const span: LangfuseSpanClient | undefined = this.handlers['langFuse'].span[returnIds['langFuse'].span] + if (span) { + span.end({ + output: { + error + } + }) + if (shutdown) { + const langfuse: Langfuse = this.handlers['langFuse'].client + await langfuse.shutdownAsync() + } + } + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'llmonitor')) { + const chainEventId = returnIds['llmonitor'].chainEvent + const monitor = this.handlers['llmonitor'].client + + if (monitor && chainEventId) { + await monitor.trackEvent('chain', 'end', { + runId: chainEventId, + output: error + }) + } + } + } + + async onLLMStart(name: string, input: string, parentIds: ICommonObject) { + const returnIds: ICommonObject = { + langSmith: {}, + langFuse: {}, + llmonitor: {} + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'langSmith')) { + const parentRun: RunTree | undefined = this.handlers['langSmith'].chainRun[parentIds['langSmith'].chainRun] + if (parentRun) { + const childLLMRun = await parentRun.createChild({ + name, + run_type: 'llm', + inputs: { + prompts: [input] + } + }) + await childLLMRun.postRun() + this.handlers['langSmith'].llmRun = { [childLLMRun.id]: childLLMRun } + returnIds['langSmith'].llmRun = childLLMRun.id + } + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'langFuse')) { + const trace: LangfuseTraceClient | undefined = this.handlers['langFuse'].trace[parentIds['langFuse'].trace] + if (trace) { + const generation = trace.generation({ + name, + input: input + }) + this.handlers['langFuse'].generation = { [generation.id]: generation } + returnIds['langFuse'].generation = generation.id + } + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'llmonitor')) { + const monitor = this.handlers['llmonitor'].client + const chainEventId: string = this.handlers['llmonitor'].chainEvent[parentIds['llmonitor'].chainEvent] + + if (monitor && chainEventId) { + const runId = uuidv4() + await monitor.trackEvent('llm', 'start', { + runId, + parentRunId: chainEventId, + name, + userId: this.options.chatId, + input + }) + this.handlers['llmonitor'].llmEvent = { [runId]: runId } + returnIds['llmonitor'].llmEvent = runId + } + } + + return returnIds + } + + async onLLMEnd(returnIds: ICommonObject, output: string) { + if (Object.prototype.hasOwnProperty.call(this.handlers, 'langSmith')) { + const llmRun: RunTree | undefined = this.handlers['langSmith'].llmRun[returnIds['langSmith'].llmRun] + if (llmRun) { + await llmRun.end({ + outputs: { + generations: [output] + } + }) + await llmRun.patchRun() + } + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'langFuse')) { + const generation: LangfuseGenerationClient | undefined = this.handlers['langFuse'].generation[returnIds['langFuse'].generation] + if (generation) { + generation.end({ + output: output + }) + } + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'llmonitor')) { + const llmEventId: string = this.handlers['llmonitor'].llmEvent[returnIds['llmonitor'].llmEvent] + const monitor = this.handlers['llmonitor'].client + + if (monitor && llmEventId) { + await monitor.trackEvent('llm', 'end', { + runId: llmEventId, + output + }) + } + } + } + + async onLLMError(returnIds: ICommonObject, error: string | object) { + if (Object.prototype.hasOwnProperty.call(this.handlers, 'langSmith')) { + const llmRun: RunTree | undefined = this.handlers['langSmith'].llmRun[returnIds['langSmith'].llmRun] + if (llmRun) { + await llmRun.end({ + error: { + error + } + }) + await llmRun.patchRun() + } + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'langFuse')) { + const generation: LangfuseGenerationClient | undefined = this.handlers['langFuse'].generation[returnIds['langFuse'].generation] + if (generation) { + generation.end({ + output: error + }) + } + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'llmonitor')) { + const llmEventId: string = this.handlers['llmonitor'].llmEvent[returnIds['llmonitor'].llmEvent] + const monitor = this.handlers['llmonitor'].client + + if (monitor && llmEventId) { + await monitor.trackEvent('llm', 'end', { + runId: llmEventId, + output: error + }) + } + } + } + + async onToolStart(name: string, input: string | object, parentIds: ICommonObject) { + const returnIds: ICommonObject = { + langSmith: {}, + langFuse: {}, + llmonitor: {} + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'langSmith')) { + const parentRun: RunTree | undefined = this.handlers['langSmith'].chainRun[parentIds['langSmith'].chainRun] + if (parentRun) { + const childToolRun = await parentRun.createChild({ + name, + run_type: 'tool', + inputs: { + input + } + }) + await childToolRun.postRun() + this.handlers['langSmith'].toolRun = { [childToolRun.id]: childToolRun } + returnIds['langSmith'].toolRun = childToolRun.id + } + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'langFuse')) { + const trace: LangfuseTraceClient | undefined = this.handlers['langFuse'].trace[parentIds['langFuse'].trace] + if (trace) { + const toolSpan = trace.span({ + name, + input + }) + this.handlers['langFuse'].toolSpan = { [toolSpan.id]: toolSpan } + returnIds['langFuse'].toolSpan = toolSpan.id + } + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'llmonitor')) { + const monitor = this.handlers['llmonitor'].client + const chainEventId: string = this.handlers['llmonitor'].chainEvent[parentIds['llmonitor'].chainEvent] + + if (monitor && chainEventId) { + const runId = uuidv4() + await monitor.trackEvent('tool', 'start', { + runId, + parentRunId: chainEventId, + name, + userId: this.options.chatId, + input + }) + this.handlers['llmonitor'].toolEvent = { [runId]: runId } + returnIds['llmonitor'].toolEvent = runId + } + } + + return returnIds + } + + async onToolEnd(returnIds: ICommonObject, output: string | object) { + if (Object.prototype.hasOwnProperty.call(this.handlers, 'langSmith')) { + const toolRun: RunTree | undefined = this.handlers['langSmith'].toolRun[returnIds['langSmith'].toolRun] + if (toolRun) { + await toolRun.end({ + outputs: { + output + } + }) + await toolRun.patchRun() + } + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'langFuse')) { + const toolSpan: LangfuseSpanClient | undefined = this.handlers['langFuse'].toolSpan[returnIds['langFuse'].toolSpan] + if (toolSpan) { + toolSpan.end({ + output + }) + } + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'llmonitor')) { + const toolEventId: string = this.handlers['llmonitor'].toolEvent[returnIds['llmonitor'].toolEvent] + const monitor = this.handlers['llmonitor'].client + + if (monitor && toolEventId) { + await monitor.trackEvent('tool', 'end', { + runId: toolEventId, + output + }) + } + } + } + + async onToolError(returnIds: ICommonObject, error: string | object) { + if (Object.prototype.hasOwnProperty.call(this.handlers, 'langSmith')) { + const toolRun: RunTree | undefined = this.handlers['langSmith'].toolRun[returnIds['langSmith'].toolRun] + if (toolRun) { + await toolRun.end({ + error: { + error + } + }) + await toolRun.patchRun() + } + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'langFuse')) { + const toolSpan: LangfuseSpanClient | undefined = this.handlers['langFuse'].toolSpan[returnIds['langFuse'].toolSpan] + if (toolSpan) { + toolSpan.end({ + output: error + }) + } + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'llmonitor')) { + const toolEventId: string = this.handlers['llmonitor'].llmEvent[returnIds['llmonitor'].toolEvent] + const monitor = this.handlers['llmonitor'].client + + if (monitor && toolEventId) { + await monitor.trackEvent('tool', 'end', { + runId: toolEventId, + output: error + }) + } + } + } +} diff --git a/packages/components/src/utils.ts b/packages/components/src/utils.ts index ceeb402a..548632ea 100644 --- a/packages/components/src/utils.ts +++ b/packages/components/src/utils.ts @@ -5,13 +5,86 @@ import * as path from 'path' import { JSDOM } from 'jsdom' import { z } from 'zod' import { DataSource } from 'typeorm' -import { ICommonObject, IDatabaseEntity, IMessage, INodeData } from './Interface' +import { ICommonObject, IDatabaseEntity, IMessage, INodeData, IVariable } from './Interface' import { AES, enc } from 'crypto-js' import { ChatMessageHistory } from 'langchain/memory' import { AIMessage, HumanMessage, BaseMessage } from 'langchain/schema' export const numberOrExpressionRegex = '^(\\d+\\.?\\d*|{{.*}})$' //return true if string consists only numbers OR expression {{}} export const notEmptyRegex = '(.|\\s)*\\S(.|\\s)*' //return true if string is not empty or blank +/* + * List of dependencies allowed to be import in vm2 + */ +export const availableDependencies = [ + '@aws-sdk/client-bedrock-runtime', + '@aws-sdk/client-dynamodb', + '@aws-sdk/client-s3', + '@elastic/elasticsearch', + '@dqbd/tiktoken', + '@getzep/zep-js', + '@gomomento/sdk', + '@gomomento/sdk-core', + '@google-ai/generativelanguage', + '@huggingface/inference', + '@notionhq/client', + '@opensearch-project/opensearch', + '@pinecone-database/pinecone', + '@qdrant/js-client-rest', + '@supabase/supabase-js', + '@upstash/redis', + '@zilliz/milvus2-sdk-node', + 'apify-client', + 'axios', + 'cheerio', + 'chromadb', + 'cohere-ai', + 'd3-dsv', + 'faiss-node', + 'form-data', + 'google-auth-library', + 'graphql', + 'html-to-text', + 'ioredis', + 'langchain', + 'langfuse', + 'langsmith', + 'linkifyjs', + 'llmonitor', + 'mammoth', + 'moment', + 'mongodb', + 'mysql2', + 'node-fetch', + 'node-html-markdown', + 'notion-to-md', + 'openai', + 'pdf-parse', + 'pdfjs-dist', + 'pg', + 'playwright', + 'puppeteer', + 'redis', + 'replicate', + 'srt-parser-2', + 'typeorm', + 'weaviate-ts-client' +] + +export const defaultAllowBuiltInDep = [ + 'assert', + 'buffer', + 'crypto', + 'events', + 'http', + 'https', + 'net', + 'path', + 'querystring', + 'timers', + 'tls', + 'url', + 'zlib' +] /** * Get base classes of components @@ -379,7 +452,8 @@ const getEncryptionKeyFilePath = (): string => { path.join(__dirname, '..', '..', '..', '..', 'encryption.key'), path.join(__dirname, '..', '..', '..', '..', 'server', 'encryption.key'), path.join(__dirname, '..', '..', '..', '..', '..', 'encryption.key'), - path.join(__dirname, '..', '..', '..', '..', '..', 'server', 'encryption.key') + path.join(__dirname, '..', '..', '..', '..', '..', 'server', 'encryption.key'), + path.join(getUserHome(), '.flowise', 'encryption.key') ] for (const checkPath of checkPaths) { if (fs.existsSync(checkPath)) { @@ -389,7 +463,7 @@ const getEncryptionKeyFilePath = (): string => { return '' } -const getEncryptionKeyPath = (): string => { +export const getEncryptionKeyPath = (): string => { return process.env.SECRETKEY_PATH ? path.join(process.env.SECRETKEY_PATH, 'encryption.key') : getEncryptionKeyFilePath() } @@ -612,9 +686,8 @@ export const flattenObject = (obj: ICommonObject, parentKey?: string) => { /** * Convert BaseMessage to IMessage - * @param {ICommonObject} obj - * @param {string} parentKey - * @returns {ICommonObject} + * @param {BaseMessage[]} messages + * @returns {IMessage[]} */ export const convertBaseMessagetoIMessage = (messages: BaseMessage[]): IMessage[] => { const formatmessages: IMessage[] = [] @@ -638,3 +711,72 @@ export const convertBaseMessagetoIMessage = (messages: BaseMessage[]): IMessage[ } return formatmessages } + +/** + * Convert MultiOptions String to String Array + * @param {string} inputString + * @returns {string[]} + */ +export const convertMultiOptionsToStringArray = (inputString: string): string[] => { + let ArrayString: string[] = [] + try { + ArrayString = JSON.parse(inputString) + } catch (e) { + ArrayString = [] + } + return ArrayString +} + +/** + * Get variables + * @param {DataSource} appDataSource + * @param {IDatabaseEntity} databaseEntities + * @param {INodeData} nodeData + */ +export const getVars = async (appDataSource: DataSource, databaseEntities: IDatabaseEntity, nodeData: INodeData) => { + const variables = ((await appDataSource.getRepository(databaseEntities['Variable']).find()) as IVariable[]) ?? [] + + // override variables defined in overrideConfig + // nodeData.inputs.variables is an Object, check each property and override the variable + if (nodeData?.inputs?.vars) { + for (const propertyName of Object.getOwnPropertyNames(nodeData.inputs.vars)) { + const foundVar = variables.find((v) => v.name === propertyName) + if (foundVar) { + // even if the variable was defined as runtime, we override it with static value + foundVar.type = 'static' + foundVar.value = nodeData.inputs.vars[propertyName] + } else { + // add it the variables, if not found locally in the db + variables.push({ name: propertyName, type: 'static', value: nodeData.inputs.vars[propertyName] }) + } + } + } + + return variables +} + +/** + * Prepare sandbox variables + * @param {IVariable[]} variables + */ +export const prepareSandboxVars = (variables: IVariable[]) => { + let vars = {} + if (variables) { + for (const item of variables) { + let value = item.value + + // read from .env file + if (item.type === 'runtime') { + value = process.env[item.name] ?? '' + } + + Object.defineProperty(vars, item.name, { + enumerable: true, + configurable: true, + writable: true, + value: value + }) + } + } + return vars +} diff --git a/packages/server/.env.example b/packages/server/.env.example index 0ad11f3f..ed54ac66 100644 --- a/packages/server/.env.example +++ b/packages/server/.env.example @@ -12,6 +12,7 @@ PORT=3000 # DATABASE_NAME="flowise" # DATABASE_USER="" # DATABASE_PASSWORD="" +# DATABASE_SSL=true # FLOWISE_USERNAME=user # FLOWISE_PASSWORD=1234 @@ -25,3 +26,5 @@ PORT=3000 # LANGCHAIN_ENDPOINT=https://api.smith.langchain.com # LANGCHAIN_API_KEY=your_api_key # LANGCHAIN_PROJECT=your_project + +# DISABLE_FLOWISE_TELEMETRY=true \ No newline at end of file diff --git a/packages/server/marketplaces/chatflows/API Agent.json b/packages/server/marketplaces/chatflows/API Agent.json index 93270848..eabc8f2e 100644 --- a/packages/server/marketplaces/chatflows/API Agent.json +++ b/packages/server/marketplaces/chatflows/API Agent.json @@ -936,7 +936,7 @@ "id": "conversationalAgent_0-input-tools-Tool" }, { - "label": "Language Model", + "label": "Chat Model", "name": "model", "type": "BaseChatModel", "id": "conversationalAgent_0-input-model-BaseChatModel" diff --git a/packages/server/marketplaces/chatflows/AutoGPT.json b/packages/server/marketplaces/chatflows/AutoGPT.json index 150fe17e..0062cd43 100644 --- a/packages/server/marketplaces/chatflows/AutoGPT.json +++ b/packages/server/marketplaces/chatflows/AutoGPT.json @@ -511,7 +511,7 @@ "type": "Pinecone", "baseClasses": ["Pinecone", "VectorStoreRetriever", "BaseRetriever"], "category": "Vector Stores", - "description": "Upsert embedded data and perform similarity search upon query using Pinecone, a leading fully managed hosted vector database", + "description": "Upsert embedded data and perform similarity or mmr search using Pinecone, a leading fully managed hosted vector database", "inputParams": [ { "label": "Connect Credential", @@ -552,6 +552,45 @@ "additionalParams": true, "optional": true, "id": "pinecone_0-input-topK-number" + }, + { + "label": "Search Type", + "name": "searchType", + "type": "options", + "default": "similarity", + "options": [ + { + "label": "Similarity", + "name": "similarity" + }, + { + "label": "Max Marginal Relevance", + "name": "mmr" + } + ], + "additionalParams": true, + "optional": true, + "id": "pinecone_0-input-searchType-options" + }, + { + "label": "Fetch K (for MMR Search)", + "name": "fetchK", + "description": "Number of initial documents to fetch for MMR reranking. Default to 20. Used only when the search type is MMR", + "placeholder": "20", + "type": "number", + "additionalParams": true, + "optional": true, + "id": "pinecone_0-input-fetchK-number" + }, + { + "label": "Lambda (for MMR Search)", + "name": "lambda", + "description": "Number between 0 and 1 that determines the degree of diversity among the results, where 0 corresponds to maximum diversity and 1 to minimum diversity. Used only when the search type is MMR", + "placeholder": "0.5", + "type": "number", + "additionalParams": true, + "optional": true, + "id": "pinecone_0-input-lambda-number" } ], "inputAnchors": [ @@ -576,7 +615,10 @@ "pineconeIndex": "", "pineconeNamespace": "", "pineconeMetadataFilter": "", - "topK": "" + "topK": "", + "searchType": "similarity", + "fetchK": "", + "lambda": "" }, "outputAnchors": [ { diff --git a/packages/server/marketplaces/chatflows/BabyAGI.json b/packages/server/marketplaces/chatflows/BabyAGI.json index ab387205..81e3f230 100644 --- a/packages/server/marketplaces/chatflows/BabyAGI.json +++ b/packages/server/marketplaces/chatflows/BabyAGI.json @@ -166,7 +166,7 @@ "type": "Pinecone", "baseClasses": ["Pinecone", "VectorStoreRetriever", "BaseRetriever"], "category": "Vector Stores", - "description": "Upsert embedded data and perform similarity search upon query using Pinecone, a leading fully managed hosted vector database", + "description": "Upsert embedded data and perform similarity or mmr search using Pinecone, a leading fully managed hosted vector database", "inputParams": [ { "label": "Connect Credential", @@ -207,6 +207,45 @@ "additionalParams": true, "optional": true, "id": "pinecone_0-input-topK-number" + }, + { + "label": "Search Type", + "name": "searchType", + "type": "options", + "default": "similarity", + "options": [ + { + "label": "Similarity", + "name": "similarity" + }, + { + "label": "Max Marginal Relevance", + "name": "mmr" + } + ], + "additionalParams": true, + "optional": true, + "id": "pinecone_0-input-searchType-options" + }, + { + "label": "Fetch K (for MMR Search)", + "name": "fetchK", + "description": "Number of initial documents to fetch for MMR reranking. Default to 20. Used only when the search type is MMR", + "placeholder": "20", + "type": "number", + "additionalParams": true, + "optional": true, + "id": "pinecone_0-input-fetchK-number" + }, + { + "label": "Lambda (for MMR Search)", + "name": "lambda", + "description": "Number between 0 and 1 that determines the degree of diversity among the results, where 0 corresponds to maximum diversity and 1 to minimum diversity. Used only when the search type is MMR", + "placeholder": "0.5", + "type": "number", + "additionalParams": true, + "optional": true, + "id": "pinecone_0-input-lambda-number" } ], "inputAnchors": [ @@ -231,7 +270,10 @@ "pineconeIndex": "", "pineconeNamespace": "", "pineconeMetadataFilter": "", - "topK": "" + "topK": "", + "searchType": "similarity", + "fetchK": "", + "lambda": "" }, "outputAnchors": [ { diff --git a/packages/server/marketplaces/chatflows/Chat with a Podcast.json b/packages/server/marketplaces/chatflows/Chat with a Podcast.json index 2a9e05b5..0a5d4ac6 100644 --- a/packages/server/marketplaces/chatflows/Chat with a Podcast.json +++ b/packages/server/marketplaces/chatflows/Chat with a Podcast.json @@ -13,7 +13,7 @@ "data": { "id": "conversationalRetrievalQAChain_0", "label": "Conversational Retrieval QA Chain", - "version": 1, + "version": 2, "name": "conversationalRetrievalQAChain", "type": "ConversationalRetrievalQAChain", "baseClasses": ["ConversationalRetrievalQAChain", "BaseChain", "Runnable"], @@ -28,47 +28,36 @@ "id": "conversationalRetrievalQAChain_0-input-returnSourceDocuments-boolean" }, { - "label": "System Message", - "name": "systemMessagePrompt", + "label": "Rephrase Prompt", + "name": "rephrasePrompt", "type": "string", + "description": "Using previous chat history, rephrase question into a standalone question", + "warning": "Prompt must include input variables: {chat_history} and {question}", "rows": 4, "additionalParams": true, "optional": true, - "placeholder": "I want you to act as a document that I am having a conversation with. Your name is \"AI Assistant\". You will provide me with answers from the given info. If the answer is not included, say exactly \"Hmm, I am not sure.\" and stop after that. Refuse to answer any question not about the info. Never break character.", - "id": "conversationalRetrievalQAChain_0-input-systemMessagePrompt-string" + "default": "Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.\n\nChat History:\n{chat_history}\nFollow Up Input: {question}\nStandalone Question:", + "id": "conversationalRetrievalQAChain_0-input-rephrasePrompt-string" }, { - "label": "Chain Option", - "name": "chainOption", - "type": "options", - "options": [ - { - "label": "MapReduceDocumentsChain", - "name": "map_reduce", - "description": "Suitable for QA tasks over larger documents and can run the preprocessing step in parallel, reducing the running time" - }, - { - "label": "RefineDocumentsChain", - "name": "refine", - "description": "Suitable for QA tasks over a large number of documents." - }, - { - "label": "StuffDocumentsChain", - "name": "stuff", - "description": "Suitable for QA tasks over a small number of documents." - } - ], + "label": "Response Prompt", + "name": "responsePrompt", + "type": "string", + "description": "Taking the rephrased question, search for answer from the provided context", + "warning": "Prompt must include input variable: {context}", + "rows": 4, "additionalParams": true, "optional": true, - "id": "conversationalRetrievalQAChain_0-input-chainOption-options" + "default": "You are a helpful assistant. Using the provided context, answer the user's question to the best of your ability using the resources provided.\nIf there is nothing in the context relevant to the question at hand, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.\n------------\n{context}\n------------\nREMEMBER: If there is no relevant information within the context, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.", + "id": "conversationalRetrievalQAChain_0-input-responsePrompt-string" } ], "inputAnchors": [ { - "label": "Language Model", + "label": "Chat Model", "name": "model", - "type": "BaseLanguageModel", - "id": "conversationalRetrievalQAChain_0-input-model-BaseLanguageModel" + "type": "BaseChatModel", + "id": "conversationalRetrievalQAChain_0-input-model-BaseChatModel" }, { "label": "Vector Store Retriever", @@ -89,9 +78,8 @@ "model": "{{chatOpenAI_0.data.instance}}", "vectorStoreRetriever": "{{memoryVectorStore_0.data.instance}}", "memory": "", - "returnSourceDocuments": "", - "systemMessagePrompt": "", - "chainOption": "" + "rephrasePrompt": "Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.\n\nChat History:\n{chat_history}\nFollow Up Input: {question}\nStandalone Question:", + "responsePrompt": "You are a helpful assistant. Using the provided context, answer the user's question to the best of your ability using the resources provided.\nIf there is nothing in the context relevant to the question at hand, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.\n------------\n{context}\n------------\nREMEMBER: If there is no relevant information within the context, just say \"Hmm, I'm not sure.\" Don't try to make up an answer." }, "outputAnchors": [ { @@ -625,9 +613,9 @@ "source": "chatOpenAI_0", "sourceHandle": "chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable", "target": "conversationalRetrievalQAChain_0", - "targetHandle": "conversationalRetrievalQAChain_0-input-model-BaseLanguageModel", + "targetHandle": "conversationalRetrievalQAChain_0-input-model-BaseChatModel", "type": "buttonedge", - "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable-conversationalRetrievalQAChain_0-conversationalRetrievalQAChain_0-input-model-BaseLanguageModel", + "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable-conversationalRetrievalQAChain_0-conversationalRetrievalQAChain_0-input-model-BaseChatModel", "data": { "label": "" } diff --git a/packages/server/marketplaces/chatflows/Claude LLM.json b/packages/server/marketplaces/chatflows/Claude LLM.json index 0ead3dd8..5d632ff1 100644 --- a/packages/server/marketplaces/chatflows/Claude LLM.json +++ b/packages/server/marketplaces/chatflows/Claude LLM.json @@ -6,15 +6,15 @@ "height": 376, "id": "bufferMemory_0", "position": { - "x": 451.4449437285705, - "y": 118.30026803362762 + "x": 240.5161028076149, + "y": 165.35849026339048 }, "type": "customNode", "data": { "id": "bufferMemory_0", "label": "Buffer Memory", - "name": "bufferMemory", "version": 1, + "name": "bufferMemory", "type": "BufferMemory", "baseClasses": ["BufferMemory", "BaseChatMemory", "BaseMemory"], "category": "Memory", @@ -53,8 +53,8 @@ }, "selected": false, "positionAbsolute": { - "x": 451.4449437285705, - "y": 118.30026803362762 + "x": 240.5161028076149, + "y": 165.35849026339048 }, "dragging": false }, @@ -63,17 +63,17 @@ "height": 383, "id": "conversationChain_0", "position": { - "x": 1176.1569322079652, - "y": 303.56879146735974 + "x": 958.9887390513221, + "y": 318.8734467468765 }, "type": "customNode", "data": { "id": "conversationChain_0", "label": "Conversation Chain", + "version": 2, "name": "conversationChain", - "version": 1, "type": "ConversationChain", - "baseClasses": ["ConversationChain", "LLMChain", "BaseChain"], + "baseClasses": ["ConversationChain", "LLMChain", "BaseChain", "Runnable"], "category": "Chains", "description": "Chat models specific conversational chain with memory", "inputParams": [ @@ -82,15 +82,17 @@ "name": "systemMessagePrompt", "type": "string", "rows": 4, + "description": "If Chat Prompt Template is provided, this will be ignored", "additionalParams": true, "optional": true, - "placeholder": "You are a helpful assistant that write codes", + "default": "The following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.", + "placeholder": "The following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.", "id": "conversationChain_0-input-systemMessagePrompt-string" } ], "inputAnchors": [ { - "label": "Language Model", + "label": "Chat Model", "name": "model", "type": "BaseChatModel", "id": "conversationChain_0-input-model-BaseChatModel" @@ -102,27 +104,26 @@ "id": "conversationChain_0-input-memory-BaseMemory" }, { - "label": "Document", - "name": "document", - "type": "Document", - "description": "Include whole document into the context window", + "label": "Chat Prompt Template", + "name": "chatPromptTemplate", + "type": "ChatPromptTemplate", + "description": "Override existing prompt with Chat Prompt Template. Human Message must includes {input} variable", "optional": true, - "list": true, - "id": "conversationChain_0-input-document-Document" + "id": "conversationChain_0-input-chatPromptTemplate-ChatPromptTemplate" } ], "inputs": { "model": "{{chatAnthropic_0.data.instance}}", "memory": "{{bufferMemory_0.data.instance}}", - "document": ["{{pdfFile_0.data.instance}}"], - "systemMessagePrompt": "" + "chatPromptTemplate": "{{chatPromptTemplate_0.data.instance}}", + "systemMessagePrompt": "The following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know." }, "outputAnchors": [ { - "id": "conversationChain_0-output-conversationChain-ConversationChain|LLMChain|BaseChain", + "id": "conversationChain_0-output-conversationChain-ConversationChain|LLMChain|BaseChain|Runnable", "name": "conversationChain", "label": "ConversationChain", - "type": "ConversationChain | LLMChain | BaseChain" + "type": "ConversationChain | LLMChain | BaseChain | Runnable" } ], "outputs": {}, @@ -130,27 +131,27 @@ }, "selected": false, "positionAbsolute": { - "x": 1176.1569322079652, - "y": 303.56879146735974 + "x": 958.9887390513221, + "y": 318.8734467468765 }, "dragging": false }, { "width": 300, - "height": 523, + "height": 574, "id": "chatAnthropic_0", "position": { - "x": 800.5525382783799, - "y": -130.7988221837009 + "x": 585.3308245972187, + "y": -116.32789506560908 }, "type": "customNode", "data": { "id": "chatAnthropic_0", "label": "ChatAnthropic", - "name": "chatAnthropic", "version": 3, + "name": "chatAnthropic", "type": "ChatAnthropic", - "baseClasses": ["ChatAnthropic", "BaseChatModel", "BaseLanguageModel"], + "baseClasses": ["ChatAnthropic", "BaseChatModel", "BaseLanguageModel", "Runnable"], "category": "Chat Models", "description": "Wrapper around ChatAnthropic large language models that use the Chat endpoint", "inputParams": [ @@ -226,7 +227,7 @@ "name": "claude-instant-v1.1-100k" } ], - "default": "claude-v1", + "default": "claude-2", "optional": true, "id": "chatAnthropic_0-input-modelName-options" }, @@ -234,6 +235,7 @@ "label": "Temperature", "name": "temperature", "type": "number", + "step": 0.1, "default": 0.9, "optional": true, "id": "chatAnthropic_0-input-temperature-number" @@ -242,6 +244,7 @@ "label": "Max Tokens", "name": "maxTokensToSample", "type": "number", + "step": 1, "optional": true, "additionalParams": true, "id": "chatAnthropic_0-input-maxTokensToSample-number" @@ -250,6 +253,7 @@ "label": "Top P", "name": "topP", "type": "number", + "step": 0.1, "optional": true, "additionalParams": true, "id": "chatAnthropic_0-input-topP-number" @@ -258,6 +262,7 @@ "label": "Top K", "name": "topK", "type": "number", + "step": 0.1, "optional": true, "additionalParams": true, "id": "chatAnthropic_0-input-topK-number" @@ -273,6 +278,7 @@ } ], "inputs": { + "cache": "", "modelName": "claude-2.1", "temperature": 0.9, "maxTokensToSample": "", @@ -281,10 +287,10 @@ }, "outputAnchors": [ { - "id": "chatAnthropic_0-output-chatAnthropic-ChatAnthropic|BaseChatModel|BaseLanguageModel", + "id": "chatAnthropic_0-output-chatAnthropic-ChatAnthropic|BaseChatModel|BaseLanguageModel|Runnable", "name": "chatAnthropic", "label": "ChatAnthropic", - "type": "ChatAnthropic | BaseChatModel | BaseLanguageModel" + "type": "ChatAnthropic | BaseChatModel | BaseLanguageModel | Runnable" } ], "outputs": {}, @@ -292,61 +298,106 @@ }, "selected": false, "positionAbsolute": { - "x": 800.5525382783799, - "y": -130.7988221837009 + "x": 585.3308245972187, + "y": -116.32789506560908 }, "dragging": false }, { "width": 300, - "height": 507, - "id": "pdfFile_0", + "height": 688, + "id": "chatPromptTemplate_0", "position": { - "x": 94.16886576108482, - "y": 37.12056504707391 + "x": -106.44189698270114, + "y": 20.133956087516538 }, "type": "customNode", "data": { - "id": "pdfFile_0", - "label": "Pdf File", - "name": "pdfFile", + "id": "chatPromptTemplate_0", + "label": "Chat Prompt Template", "version": 1, + "name": "chatPromptTemplate", + "type": "ChatPromptTemplate", + "baseClasses": ["ChatPromptTemplate", "BaseChatPromptTemplate", "BasePromptTemplate", "Runnable"], + "category": "Prompts", + "description": "Schema to represent a chat prompt", + "inputParams": [ + { + "label": "System Message", + "name": "systemMessagePrompt", + "type": "string", + "rows": 4, + "placeholder": "You are a helpful assistant that translates {input_language} to {output_language}.", + "id": "chatPromptTemplate_0-input-systemMessagePrompt-string" + }, + { + "label": "Human Message", + "name": "humanMessagePrompt", + "type": "string", + "rows": 4, + "placeholder": "{text}", + "id": "chatPromptTemplate_0-input-humanMessagePrompt-string" + }, + { + "label": "Format Prompt Values", + "name": "promptValues", + "type": "json", + "optional": true, + "acceptVariable": true, + "list": true, + "id": "chatPromptTemplate_0-input-promptValues-json" + } + ], + "inputAnchors": [], + "inputs": { + "systemMessagePrompt": "The following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.\nThe AI has the following context:\n{context}", + "humanMessagePrompt": "{input}", + "promptValues": "{\"context\":\"{{plainText_0.data.instance}}\",\"input\":\"{{question}}\"}" + }, + "outputAnchors": [ + { + "id": "chatPromptTemplate_0-output-chatPromptTemplate-ChatPromptTemplate|BaseChatPromptTemplate|BasePromptTemplate|Runnable", + "name": "chatPromptTemplate", + "label": "ChatPromptTemplate", + "type": "ChatPromptTemplate | BaseChatPromptTemplate | BasePromptTemplate | Runnable" + } + ], + "outputs": {}, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": -106.44189698270114, + "y": 20.133956087516538 + }, + "dragging": false + }, + { + "width": 300, + "height": 485, + "id": "plainText_0", + "position": { + "x": -487.7511991135089, + "y": 77.83838996645807 + }, + "type": "customNode", + "data": { + "id": "plainText_0", + "label": "Plain Text", + "version": 2, + "name": "plainText", "type": "Document", "baseClasses": ["Document"], "category": "Document Loaders", - "description": "Load data from PDF files", + "description": "Load data from plain text", "inputParams": [ { - "label": "Pdf File", - "name": "pdfFile", - "type": "file", - "fileType": ".pdf", - "id": "pdfFile_0-input-pdfFile-file" - }, - { - "label": "Usage", - "name": "usage", - "type": "options", - "options": [ - { - "label": "One document per page", - "name": "perPage" - }, - { - "label": "One document per file", - "name": "perFile" - } - ], - "default": "perPage", - "id": "pdfFile_0-input-usage-options" - }, - { - "label": "Use Legacy Build", - "name": "legacyBuild", - "type": "boolean", - "optional": true, - "additionalParams": true, - "id": "pdfFile_0-input-legacyBuild-boolean" + "label": "Text", + "name": "text", + "type": "string", + "rows": 4, + "placeholder": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua...", + "id": "plainText_0-input-text-string" }, { "label": "Metadata", @@ -354,7 +405,7 @@ "type": "json", "optional": true, "additionalParams": true, - "id": "pdfFile_0-input-metadata-json" + "id": "plainText_0-input-metadata-json" } ], "inputAnchors": [ @@ -363,30 +414,45 @@ "name": "textSplitter", "type": "TextSplitter", "optional": true, - "id": "pdfFile_0-input-textSplitter-TextSplitter" + "id": "plainText_0-input-textSplitter-TextSplitter" } ], "inputs": { + "text": "Welcome to Skyworld Hotel, where your dreams take flight and your stay soars to new heights. Nestled amidst breathtaking cityscape views, our upscale establishment offers an unparalleled blend of luxury and comfort. Our rooms are elegantly appointed, featuring modern amenities and plush furnishings to ensure your relaxation.\n\nIndulge in culinary delights at our rooftop restaurant, offering a gastronomic journey with panoramic vistas. Skyworld Hotel boasts state-of-the-art conference facilities, perfect for business travelers, and an inviting spa for relaxation seekers. Our attentive staff is dedicated to ensuring your every need is met, making your stay memorable.\n\nCentrally located, we offer easy access to local attractions, making us an ideal choice for both leisure and business travelers. Experience the world of hospitality like never before at Skyworld Hotel.", "textSplitter": "", - "usage": "perPage", - "legacyBuild": "", "metadata": "" }, "outputAnchors": [ { - "id": "pdfFile_0-output-pdfFile-Document", - "name": "pdfFile", - "label": "Document", - "type": "Document" + "name": "output", + "label": "Output", + "type": "options", + "options": [ + { + "id": "plainText_0-output-document-Document", + "name": "document", + "label": "Document", + "type": "Document" + }, + { + "id": "plainText_0-output-text-string|json", + "name": "text", + "label": "Text", + "type": "string | json" + } + ], + "default": "document" } ], - "outputs": {}, + "outputs": { + "output": "text" + }, "selected": false }, "selected": false, "positionAbsolute": { - "x": 94.16886576108482, - "y": 37.12056504707391 + "x": -487.7511991135089, + "y": 77.83838996645807 }, "dragging": false } @@ -398,32 +464,31 @@ "target": "conversationChain_0", "targetHandle": "conversationChain_0-input-memory-BaseMemory", "type": "buttonedge", - "id": "bufferMemory_0-bufferMemory_0-output-bufferMemory-BufferMemory|BaseChatMemory|BaseMemory-conversationChain_0-conversationChain_0-input-memory-BaseMemory", - "data": { - "label": "" - } + "id": "bufferMemory_0-bufferMemory_0-output-bufferMemory-BufferMemory|BaseChatMemory|BaseMemory-conversationChain_0-conversationChain_0-input-memory-BaseMemory" }, { "source": "chatAnthropic_0", - "sourceHandle": "chatAnthropic_0-output-chatAnthropic-ChatAnthropic|BaseChatModel|BaseLanguageModel", + "sourceHandle": "chatAnthropic_0-output-chatAnthropic-ChatAnthropic|BaseChatModel|BaseLanguageModel|Runnable", "target": "conversationChain_0", "targetHandle": "conversationChain_0-input-model-BaseChatModel", "type": "buttonedge", - "id": "chatAnthropic_0-chatAnthropic_0-output-chatAnthropic-ChatAnthropic|BaseChatModel|BaseLanguageModel-conversationChain_0-conversationChain_0-input-model-BaseChatModel", - "data": { - "label": "" - } + "id": "chatAnthropic_0-chatAnthropic_0-output-chatAnthropic-ChatAnthropic|BaseChatModel|BaseLanguageModel|Runnable-conversationChain_0-conversationChain_0-input-model-BaseChatModel" }, { - "source": "pdfFile_0", - "sourceHandle": "pdfFile_0-output-pdfFile-Document", - "target": "conversationChain_0", - "targetHandle": "conversationChain_0-input-document-Document", + "source": "plainText_0", + "sourceHandle": "plainText_0-output-text-string|json", + "target": "chatPromptTemplate_0", + "targetHandle": "chatPromptTemplate_0-input-promptValues-json", "type": "buttonedge", - "id": "pdfFile_0-pdfFile_0-output-pdfFile-Document-conversationChain_0-conversationChain_0-input-document-Document", - "data": { - "label": "" - } + "id": "plainText_0-plainText_0-output-text-string|json-chatPromptTemplate_0-chatPromptTemplate_0-input-promptValues-json" + }, + { + "source": "chatPromptTemplate_0", + "sourceHandle": "chatPromptTemplate_0-output-chatPromptTemplate-ChatPromptTemplate|BaseChatPromptTemplate|BasePromptTemplate|Runnable", + "target": "conversationChain_0", + "targetHandle": "conversationChain_0-input-chatPromptTemplate-ChatPromptTemplate", + "type": "buttonedge", + "id": "chatPromptTemplate_0-chatPromptTemplate_0-output-chatPromptTemplate-ChatPromptTemplate|BaseChatPromptTemplate|BasePromptTemplate|Runnable-conversationChain_0-conversationChain_0-input-chatPromptTemplate-ChatPromptTemplate" } ] } diff --git a/packages/server/marketplaces/chatflows/Context Chat Engine.json b/packages/server/marketplaces/chatflows/Context Chat Engine.json index 971aeea5..e4fdd4f5 100644 --- a/packages/server/marketplaces/chatflows/Context Chat Engine.json +++ b/packages/server/marketplaces/chatflows/Context Chat Engine.json @@ -172,7 +172,7 @@ "baseClasses": ["OpenAIEmbedding", "BaseEmbedding_LlamaIndex", "BaseEmbedding"], "tags": ["LlamaIndex"], "category": "Embeddings", - "description": "OpenAI Embedding with LlamaIndex implementation", + "description": "OpenAI Embedding specific for LlamaIndex", "inputParams": [ { "label": "Connect Credential", @@ -349,7 +349,7 @@ "baseClasses": ["ChatOpenAI", "BaseChatModel_LlamaIndex"], "tags": ["LlamaIndex"], "category": "Chat Models", - "description": "Wrapper around OpenAI Chat LLM with LlamaIndex implementation", + "description": "Wrapper around OpenAI Chat LLM specific for LlamaIndex", "inputParams": [ { "label": "Connect Credential", @@ -646,7 +646,7 @@ "baseClasses": ["ChatOpenAI", "BaseChatModel_LlamaIndex"], "tags": ["LlamaIndex"], "category": "Chat Models", - "description": "Wrapper around OpenAI Chat LLM with LlamaIndex implementation", + "description": "Wrapper around OpenAI Chat LLM specific for LlamaIndex", "inputParams": [ { "label": "Connect Credential", diff --git a/packages/server/marketplaces/chatflows/Conversational Agent.json b/packages/server/marketplaces/chatflows/Conversational Agent.json index 8994594a..b27d3886 100644 --- a/packages/server/marketplaces/chatflows/Conversational Agent.json +++ b/packages/server/marketplaces/chatflows/Conversational Agent.json @@ -354,7 +354,7 @@ "id": "conversationalAgent_0-input-tools-Tool" }, { - "label": "Language Model", + "label": "Chat Model", "name": "model", "type": "BaseChatModel", "id": "conversationalAgent_0-input-model-BaseChatModel" diff --git a/packages/server/marketplaces/chatflows/Conversational Retrieval Agent.json b/packages/server/marketplaces/chatflows/Conversational Retrieval Agent.json index 0e9e41bd..4378a47d 100644 --- a/packages/server/marketplaces/chatflows/Conversational Retrieval Agent.json +++ b/packages/server/marketplaces/chatflows/Conversational Retrieval Agent.json @@ -301,7 +301,7 @@ "type": "Pinecone", "baseClasses": ["Pinecone", "VectorStoreRetriever", "BaseRetriever"], "category": "Vector Stores", - "description": "Upsert embedded data and perform similarity search upon query using Pinecone, a leading fully managed hosted vector database", + "description": "Upsert embedded data and perform similarity or mmr search using Pinecone, a leading fully managed hosted vector database", "inputParams": [ { "label": "Connect Credential", @@ -342,6 +342,45 @@ "additionalParams": true, "optional": true, "id": "pinecone_0-input-topK-number" + }, + { + "label": "Search Type", + "name": "searchType", + "type": "options", + "default": "similarity", + "options": [ + { + "label": "Similarity", + "name": "similarity" + }, + { + "label": "Max Marginal Relevance", + "name": "mmr" + } + ], + "additionalParams": true, + "optional": true, + "id": "pinecone_0-input-searchType-options" + }, + { + "label": "Fetch K (for MMR Search)", + "name": "fetchK", + "description": "Number of initial documents to fetch for MMR reranking. Default to 20. Used only when the search type is MMR", + "placeholder": "20", + "type": "number", + "additionalParams": true, + "optional": true, + "id": "pinecone_0-input-fetchK-number" + }, + { + "label": "Lambda (for MMR Search)", + "name": "lambda", + "description": "Number between 0 and 1 that determines the degree of diversity among the results, where 0 corresponds to maximum diversity and 1 to minimum diversity. Used only when the search type is MMR", + "placeholder": "0.5", + "type": "number", + "additionalParams": true, + "optional": true, + "id": "pinecone_0-input-lambda-number" } ], "inputAnchors": [ @@ -366,7 +405,10 @@ "pineconeIndex": "", "pineconeNamespace": "", "pineconeMetadataFilter": "", - "topK": "" + "topK": "", + "searchType": "similarity", + "fetchK": "", + "lambda": "" }, "outputAnchors": [ { diff --git a/packages/server/marketplaces/chatflows/Conversational Retrieval QA Chain.json b/packages/server/marketplaces/chatflows/Conversational Retrieval QA Chain.json index 5c55d833..253a1dfc 100644 --- a/packages/server/marketplaces/chatflows/Conversational Retrieval QA Chain.json +++ b/packages/server/marketplaces/chatflows/Conversational Retrieval QA Chain.json @@ -249,10 +249,10 @@ "data": { "id": "conversationalRetrievalQAChain_0", "label": "Conversational Retrieval QA Chain", - "version": 1, + "version": 2, "name": "conversationalRetrievalQAChain", "type": "ConversationalRetrievalQAChain", - "baseClasses": ["ConversationalRetrievalQAChain", "BaseChain"], + "baseClasses": ["ConversationalRetrievalQAChain", "BaseChain", "Runnable"], "category": "Chains", "description": "Document QA - built on RetrievalQAChain to provide a chat history component", "inputParams": [ @@ -264,47 +264,36 @@ "id": "conversationalRetrievalQAChain_0-input-returnSourceDocuments-boolean" }, { - "label": "System Message", - "name": "systemMessagePrompt", + "label": "Rephrase Prompt", + "name": "rephrasePrompt", "type": "string", + "description": "Using previous chat history, rephrase question into a standalone question", + "warning": "Prompt must include input variables: {chat_history} and {question}", "rows": 4, "additionalParams": true, "optional": true, - "placeholder": "I want you to act as a document that I am having a conversation with. Your name is \"AI Assistant\". You will provide me with answers from the given info. If the answer is not included, say exactly \"Hmm, I am not sure.\" and stop after that. Refuse to answer any question not about the info. Never break character.", - "id": "conversationalRetrievalQAChain_0-input-systemMessagePrompt-string" + "default": "Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.\n\nChat History:\n{chat_history}\nFollow Up Input: {question}\nStandalone Question:", + "id": "conversationalRetrievalQAChain_0-input-rephrasePrompt-string" }, { - "label": "Chain Option", - "name": "chainOption", - "type": "options", - "options": [ - { - "label": "MapReduceDocumentsChain", - "name": "map_reduce", - "description": "Suitable for QA tasks over larger documents and can run the preprocessing step in parallel, reducing the running time" - }, - { - "label": "RefineDocumentsChain", - "name": "refine", - "description": "Suitable for QA tasks over a large number of documents." - }, - { - "label": "StuffDocumentsChain", - "name": "stuff", - "description": "Suitable for QA tasks over a small number of documents." - } - ], + "label": "Response Prompt", + "name": "responsePrompt", + "type": "string", + "description": "Taking the rephrased question, search for answer from the provided context", + "warning": "Prompt must include input variable: {context}", + "rows": 4, "additionalParams": true, "optional": true, - "id": "conversationalRetrievalQAChain_0-input-chainOption-options" + "default": "You are a helpful assistant. Using the provided context, answer the user's question to the best of your ability using the resources provided.\nIf there is nothing in the context relevant to the question at hand, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.\n------------\n{context}\n------------\nREMEMBER: If there is no relevant information within the context, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.", + "id": "conversationalRetrievalQAChain_0-input-responsePrompt-string" } ], "inputAnchors": [ { - "label": "Language Model", + "label": "Chat Model", "name": "model", - "type": "BaseLanguageModel", - "id": "conversationalRetrievalQAChain_0-input-model-BaseLanguageModel" + "type": "BaseChatModel", + "id": "conversationalRetrievalQAChain_0-input-model-BaseChatModel" }, { "label": "Vector Store Retriever", @@ -325,16 +314,15 @@ "model": "{{chatOpenAI_0.data.instance}}", "vectorStoreRetriever": "{{pinecone_0.data.instance}}", "memory": "", - "returnSourceDocuments": "", - "systemMessagePrompt": "", - "chainOption": "" + "rephrasePrompt": "Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.\n\nChat History:\n{chat_history}\nFollow Up Input: {question}\nStandalone Question:", + "responsePrompt": "You are a helpful assistant. Using the provided context, answer the user's question to the best of your ability using the resources provided.\nIf there is nothing in the context relevant to the question at hand, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.\n------------\n{context}\n------------\nREMEMBER: If there is no relevant information within the context, just say \"Hmm, I'm not sure.\" Don't try to make up an answer." }, "outputAnchors": [ { - "id": "conversationalRetrievalQAChain_0-output-conversationalRetrievalQAChain-ConversationalRetrievalQAChain|BaseChain", + "id": "conversationalRetrievalQAChain_0-output-conversationalRetrievalQAChain-ConversationalRetrievalQAChain|BaseChain|Runnable", "name": "conversationalRetrievalQAChain", "label": "ConversationalRetrievalQAChain", - "type": "ConversationalRetrievalQAChain | BaseChain" + "type": "ConversationalRetrievalQAChain | BaseChain | Runnable" } ], "outputs": {}, @@ -553,7 +541,7 @@ "type": "Pinecone", "baseClasses": ["Pinecone", "VectorStoreRetriever", "BaseRetriever"], "category": "Vector Stores", - "description": "Upsert embedded data and perform similarity search upon query using Pinecone, a leading fully managed hosted vector database", + "description": "Upsert embedded data and perform similarity or mmr search using Pinecone, a leading fully managed hosted vector database", "inputParams": [ { "label": "Connect Credential", @@ -594,6 +582,45 @@ "additionalParams": true, "optional": true, "id": "pinecone_0-input-topK-number" + }, + { + "label": "Search Type", + "name": "searchType", + "type": "options", + "default": "similarity", + "options": [ + { + "label": "Similarity", + "name": "similarity" + }, + { + "label": "Max Marginal Relevance", + "name": "mmr" + } + ], + "additionalParams": true, + "optional": true, + "id": "pinecone_0-input-searchType-options" + }, + { + "label": "Fetch K (for MMR Search)", + "name": "fetchK", + "description": "Number of initial documents to fetch for MMR reranking. Default to 20. Used only when the search type is MMR", + "placeholder": "20", + "type": "number", + "additionalParams": true, + "optional": true, + "id": "pinecone_0-input-fetchK-number" + }, + { + "label": "Lambda (for MMR Search)", + "name": "lambda", + "description": "Number between 0 and 1 that determines the degree of diversity among the results, where 0 corresponds to maximum diversity and 1 to minimum diversity. Used only when the search type is MMR", + "placeholder": "0.5", + "type": "number", + "additionalParams": true, + "optional": true, + "id": "pinecone_0-input-lambda-number" } ], "inputAnchors": [ @@ -618,7 +645,10 @@ "pineconeIndex": "", "pineconeNamespace": "", "pineconeMetadataFilter": "", - "topK": "" + "topK": "", + "searchType": "similarity", + "fetchK": "", + "lambda": "" }, "outputAnchors": [ { @@ -704,9 +734,9 @@ "source": "chatOpenAI_0", "sourceHandle": "chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable", "target": "conversationalRetrievalQAChain_0", - "targetHandle": "conversationalRetrievalQAChain_0-input-model-BaseLanguageModel", + "targetHandle": "conversationalRetrievalQAChain_0-input-model-BaseChatModel", "type": "buttonedge", - "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable-conversationalRetrievalQAChain_0-conversationalRetrievalQAChain_0-input-model-BaseLanguageModel", + "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable-conversationalRetrievalQAChain_0-conversationalRetrievalQAChain_0-input-model-BaseChatModel", "data": { "label": "" } diff --git a/packages/server/marketplaces/chatflows/Flowise Docs QnA.json b/packages/server/marketplaces/chatflows/Flowise Docs QnA.json index ac84cf56..16f70801 100644 --- a/packages/server/marketplaces/chatflows/Flowise Docs QnA.json +++ b/packages/server/marketplaces/chatflows/Flowise Docs QnA.json @@ -156,9 +156,9 @@ "id": "conversationalRetrievalQAChain_0", "label": "Conversational Retrieval QA Chain", "name": "conversationalRetrievalQAChain", - "version": 1, + "version": 2, "type": "ConversationalRetrievalQAChain", - "baseClasses": ["ConversationalRetrievalQAChain", "BaseChain"], + "baseClasses": ["ConversationalRetrievalQAChain", "BaseChain", "Runnable"], "category": "Chains", "description": "Document QA - built on RetrievalQAChain to provide a chat history component", "inputParams": [ @@ -170,47 +170,36 @@ "id": "conversationalRetrievalQAChain_0-input-returnSourceDocuments-boolean" }, { - "label": "System Message", - "name": "systemMessagePrompt", + "label": "Rephrase Prompt", + "name": "rephrasePrompt", "type": "string", + "description": "Using previous chat history, rephrase question into a standalone question", + "warning": "Prompt must include input variables: {chat_history} and {question}", "rows": 4, "additionalParams": true, "optional": true, - "placeholder": "I want you to act as a document that I am having a conversation with. Your name is \"AI Assistant\". You will provide me with answers from the given info. If the answer is not included, say exactly \"Hmm, I am not sure.\" and stop after that. Refuse to answer any question not about the info. Never break character.", - "id": "conversationalRetrievalQAChain_0-input-systemMessagePrompt-string" + "default": "Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.\n\nChat History:\n{chat_history}\nFollow Up Input: {question}\nStandalone Question:", + "id": "conversationalRetrievalQAChain_0-input-rephrasePrompt-string" }, { - "label": "Chain Option", - "name": "chainOption", - "type": "options", - "options": [ - { - "label": "MapReduceDocumentsChain", - "name": "map_reduce", - "description": "Suitable for QA tasks over larger documents and can run the preprocessing step in parallel, reducing the running time" - }, - { - "label": "RefineDocumentsChain", - "name": "refine", - "description": "Suitable for QA tasks over a large number of documents." - }, - { - "label": "StuffDocumentsChain", - "name": "stuff", - "description": "Suitable for QA tasks over a small number of documents." - } - ], + "label": "Response Prompt", + "name": "responsePrompt", + "type": "string", + "description": "Taking the rephrased question, search for answer from the provided context", + "warning": "Prompt must include input variable: {context}", + "rows": 4, "additionalParams": true, "optional": true, - "id": "conversationalRetrievalQAChain_0-input-chainOption-options" + "default": "You are a helpful assistant. Using the provided context, answer the user's question to the best of your ability using the resources provided.\nIf there is nothing in the context relevant to the question at hand, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.\n------------\n{context}\n------------\nREMEMBER: If there is no relevant information within the context, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.", + "id": "conversationalRetrievalQAChain_0-input-responsePrompt-string" } ], "inputAnchors": [ { - "label": "Language Model", + "label": "Chat Model", "name": "model", - "type": "BaseLanguageModel", - "id": "conversationalRetrievalQAChain_0-input-model-BaseLanguageModel" + "type": "BaseChatModel", + "id": "conversationalRetrievalQAChain_0-input-model-BaseChatModel" }, { "label": "Vector Store Retriever", @@ -232,15 +221,15 @@ "vectorStoreRetriever": "{{memoryVectorStore_0.data.instance}}", "memory": "", "returnSourceDocuments": true, - "systemMessagePrompt": "", - "chainOption": "" + "rephrasePrompt": "Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.\n\nChat History:\n{chat_history}\nFollow Up Input: {question}\nStandalone Question:", + "responsePrompt": "You are a helpful assistant. Using the provided context, answer the user's question to the best of your ability using the resources provided.\nIf there is nothing in the context relevant to the question at hand, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.\n------------\n{context}\n------------\nREMEMBER: If there is no relevant information within the context, just say \"Hmm, I'm not sure.\" Don't try to make up an answer." }, "outputAnchors": [ { - "id": "conversationalRetrievalQAChain_0-output-conversationalRetrievalQAChain-ConversationalRetrievalQAChain|BaseChain", + "id": "conversationalRetrievalQAChain_0-output-conversationalRetrievalQAChain-ConversationalRetrievalQAChain|BaseChain|Runnable", "name": "conversationalRetrievalQAChain", "label": "ConversationalRetrievalQAChain", - "type": "ConversationalRetrievalQAChain | BaseChain" + "type": "ConversationalRetrievalQAChain | BaseChain | Runnable" } ], "outputs": {}, @@ -668,9 +657,9 @@ "source": "chatOpenAI_0", "sourceHandle": "chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel", "target": "conversationalRetrievalQAChain_0", - "targetHandle": "conversationalRetrievalQAChain_0-input-model-BaseLanguageModel", + "targetHandle": "conversationalRetrievalQAChain_0-input-model-BaseChatModel", "type": "buttonedge", - "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel-conversationalRetrievalQAChain_0-conversationalRetrievalQAChain_0-input-model-BaseLanguageModel", + "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel-conversationalRetrievalQAChain_0-conversationalRetrievalQAChain_0-input-model-BaseChatModel", "data": { "label": "" } diff --git a/packages/server/marketplaces/chatflows/IfElse.json b/packages/server/marketplaces/chatflows/IfElse.json new file mode 100644 index 00000000..690d3ce5 --- /dev/null +++ b/packages/server/marketplaces/chatflows/IfElse.json @@ -0,0 +1,1156 @@ +{ + "description": "Split flows based on if else condition", + "badge": "new", + "nodes": [ + { + "width": 300, + "height": 511, + "id": "promptTemplate_0", + "position": { + "x": 792.9464838535649, + "y": 527.1718536712464 + }, + "type": "customNode", + "data": { + "id": "promptTemplate_0", + "label": "Prompt Template", + "version": 1, + "name": "promptTemplate", + "type": "PromptTemplate", + "baseClasses": ["PromptTemplate", "BaseStringPromptTemplate", "BasePromptTemplate"], + "category": "Prompts", + "description": "Schema to represent a basic prompt for an LLM", + "inputParams": [ + { + "label": "Template", + "name": "template", + "type": "string", + "rows": 4, + "placeholder": "What is a good name for a company that makes {product}?", + "id": "promptTemplate_0-input-template-string" + }, + { + "label": "Format Prompt Values", + "name": "promptValues", + "type": "json", + "optional": true, + "acceptVariable": true, + "list": true, + "id": "promptTemplate_0-input-promptValues-json" + } + ], + "inputAnchors": [], + "inputs": { + "template": "You are an AI who performs one task based on the following objective: {objective}.\nRespond with how you would complete this task:", + "promptValues": "{\"objective\":\"{{question}}\"}" + }, + "outputAnchors": [ + { + "id": "promptTemplate_0-output-promptTemplate-PromptTemplate|BaseStringPromptTemplate|BasePromptTemplate", + "name": "promptTemplate", + "label": "PromptTemplate", + "type": "PromptTemplate | BaseStringPromptTemplate | BasePromptTemplate" + } + ], + "outputs": {}, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 792.9464838535649, + "y": 527.1718536712464 + }, + "dragging": false + }, + { + "width": 300, + "height": 511, + "id": "promptTemplate_1", + "position": { + "x": 1995.1328578238122, + "y": -14.648035759690174 + }, + "type": "customNode", + "data": { + "id": "promptTemplate_1", + "label": "Prompt Template", + "version": 1, + "name": "promptTemplate", + "type": "PromptTemplate", + "baseClasses": ["PromptTemplate", "BaseStringPromptTemplate", "BasePromptTemplate"], + "category": "Prompts", + "description": "Schema to represent a basic prompt for an LLM", + "inputParams": [ + { + "label": "Template", + "name": "template", + "type": "string", + "rows": 4, + "placeholder": "What is a good name for a company that makes {product}?", + "id": "promptTemplate_1-input-template-string" + }, + { + "label": "Format Prompt Values", + "name": "promptValues", + "type": "json", + "optional": true, + "acceptVariable": true, + "list": true, + "id": "promptTemplate_1-input-promptValues-json" + } + ], + "inputAnchors": [], + "inputs": { + "template": "You are a task creation AI that uses the result of an execution agent to create new tasks with the following objective: {objective}.\nThe last completed task has the result: {result}.\nBased on the result, create new tasks to be completed by the AI system that do not overlap with result.\nReturn the tasks as an array.", + "promptValues": "{\"objective\":\"{{question}}\",\"result\":\"{{ifElseFunction_0.data.instance}}\"}" + }, + "outputAnchors": [ + { + "id": "promptTemplate_1-output-promptTemplate-PromptTemplate|BaseStringPromptTemplate|BasePromptTemplate", + "name": "promptTemplate", + "label": "PromptTemplate", + "type": "PromptTemplate | BaseStringPromptTemplate | BasePromptTemplate" + } + ], + "outputs": {}, + "selected": false + }, + "positionAbsolute": { + "x": 1995.1328578238122, + "y": -14.648035759690174 + }, + "selected": false, + "dragging": false + }, + { + "width": 300, + "height": 574, + "id": "openAI_1", + "position": { + "x": 791.6102007244282, + "y": -83.71386876566092 + }, + "type": "customNode", + "data": { + "id": "openAI_1", + "label": "OpenAI", + "version": 3, + "name": "openAI", + "type": "OpenAI", + "baseClasses": ["OpenAI", "BaseLLM", "BaseLanguageModel"], + "category": "LLMs", + "description": "Wrapper around OpenAI large language models", + "inputParams": [ + { + "label": "Connect Credential", + "name": "credential", + "type": "credential", + "credentialNames": ["openAIApi"], + "id": "openAI_1-input-credential-credential" + }, + { + "label": "Model Name", + "name": "modelName", + "type": "options", + "options": [ + { + "label": "gpt-3.5-turbo-instruct", + "name": "gpt-3.5-turbo-instruct" + }, + { + "label": "babbage-002", + "name": "babbage-002" + }, + { + "label": "davinci-002", + "name": "davinci-002" + } + ], + "default": "gpt-3.5-turbo-instruct", + "optional": true, + "id": "openAI_1-input-modelName-options" + }, + { + "label": "Temperature", + "name": "temperature", + "type": "number", + "default": 0.7, + "optional": true, + "id": "openAI_1-input-temperature-number" + }, + { + "label": "Max Tokens", + "name": "maxTokens", + "type": "number", + "optional": true, + "additionalParams": true, + "id": "openAI_1-input-maxTokens-number" + }, + { + "label": "Top Probability", + "name": "topP", + "type": "number", + "optional": true, + "additionalParams": true, + "id": "openAI_1-input-topP-number" + }, + { + "label": "Best Of", + "name": "bestOf", + "type": "number", + "optional": true, + "additionalParams": true, + "id": "openAI_1-input-bestOf-number" + }, + { + "label": "Frequency Penalty", + "name": "frequencyPenalty", + "type": "number", + "optional": true, + "additionalParams": true, + "id": "openAI_1-input-frequencyPenalty-number" + }, + { + "label": "Presence Penalty", + "name": "presencePenalty", + "type": "number", + "optional": true, + "additionalParams": true, + "id": "openAI_1-input-presencePenalty-number" + }, + { + "label": "Batch Size", + "name": "batchSize", + "type": "number", + "optional": true, + "additionalParams": true, + "id": "openAI_1-input-batchSize-number" + }, + { + "label": "Timeout", + "name": "timeout", + "type": "number", + "optional": true, + "additionalParams": true, + "id": "openAI_1-input-timeout-number" + }, + { + "label": "BasePath", + "name": "basepath", + "type": "string", + "optional": true, + "additionalParams": true, + "id": "openAI_1-input-basepath-string" + } + ], + "inputAnchors": [ + { + "label": "Cache", + "name": "cache", + "type": "BaseCache", + "optional": true, + "id": "openAI_1-input-cache-BaseCache" + } + ], + "inputs": { + "modelName": "gpt-3.5-turbo-instruct", + "temperature": 0.7, + "maxTokens": "", + "topP": "", + "bestOf": "", + "frequencyPenalty": "", + "presencePenalty": "", + "batchSize": "", + "timeout": "", + "basepath": "" + }, + "outputAnchors": [ + { + "id": "openAI_1-output-openAI-OpenAI|BaseLLM|BaseLanguageModel", + "name": "openAI", + "label": "OpenAI", + "type": "OpenAI | BaseLLM | BaseLanguageModel" + } + ], + "outputs": {}, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 791.6102007244282, + "y": -83.71386876566092 + }, + "dragging": false + }, + { + "width": 300, + "height": 574, + "id": "openAI_2", + "position": { + "x": 2340.5995455075863, + "y": -310.7609446553905 + }, + "type": "customNode", + "data": { + "id": "openAI_2", + "label": "OpenAI", + "version": 3, + "name": "openAI", + "type": "OpenAI", + "baseClasses": ["OpenAI", "BaseLLM", "BaseLanguageModel"], + "category": "LLMs", + "description": "Wrapper around OpenAI large language models", + "inputParams": [ + { + "label": "Connect Credential", + "name": "credential", + "type": "credential", + "credentialNames": ["openAIApi"], + "id": "openAI_2-input-credential-credential" + }, + { + "label": "Model Name", + "name": "modelName", + "type": "options", + "options": [ + { + "label": "gpt-3.5-turbo-instruct", + "name": "gpt-3.5-turbo-instruct" + }, + { + "label": "babbage-002", + "name": "babbage-002" + }, + { + "label": "davinci-002", + "name": "davinci-002" + } + ], + "default": "gpt-3.5-turbo-instruct", + "optional": true, + "id": "openAI_2-input-modelName-options" + }, + { + "label": "Temperature", + "name": "temperature", + "type": "number", + "default": 0.7, + "optional": true, + "id": "openAI_2-input-temperature-number" + }, + { + "label": "Max Tokens", + "name": "maxTokens", + "type": "number", + "optional": true, + "additionalParams": true, + "id": "openAI_2-input-maxTokens-number" + }, + { + "label": "Top Probability", + "name": "topP", + "type": "number", + "optional": true, + "additionalParams": true, + "id": "openAI_2-input-topP-number" + }, + { + "label": "Best Of", + "name": "bestOf", + "type": "number", + "optional": true, + "additionalParams": true, + "id": "openAI_2-input-bestOf-number" + }, + { + "label": "Frequency Penalty", + "name": "frequencyPenalty", + "type": "number", + "optional": true, + "additionalParams": true, + "id": "openAI_2-input-frequencyPenalty-number" + }, + { + "label": "Presence Penalty", + "name": "presencePenalty", + "type": "number", + "optional": true, + "additionalParams": true, + "id": "openAI_2-input-presencePenalty-number" + }, + { + "label": "Batch Size", + "name": "batchSize", + "type": "number", + "optional": true, + "additionalParams": true, + "id": "openAI_2-input-batchSize-number" + }, + { + "label": "Timeout", + "name": "timeout", + "type": "number", + "optional": true, + "additionalParams": true, + "id": "openAI_2-input-timeout-number" + }, + { + "label": "BasePath", + "name": "basepath", + "type": "string", + "optional": true, + "additionalParams": true, + "id": "openAI_2-input-basepath-string" + } + ], + "inputAnchors": [ + { + "label": "Cache", + "name": "cache", + "type": "BaseCache", + "optional": true, + "id": "openAI_2-input-cache-BaseCache" + } + ], + "inputs": { + "modelName": "gpt-3.5-turbo-instruct", + "temperature": 0.7, + "maxTokens": "", + "topP": "", + "bestOf": "", + "frequencyPenalty": "", + "presencePenalty": "", + "batchSize": "", + "timeout": "", + "basepath": "" + }, + "outputAnchors": [ + { + "id": "openAI_2-output-openAI-OpenAI|BaseLLM|BaseLanguageModel", + "name": "openAI", + "label": "OpenAI", + "type": "OpenAI | BaseLLM | BaseLanguageModel" + } + ], + "outputs": {}, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 2340.5995455075863, + "y": -310.7609446553905 + }, + "dragging": false + }, + { + "width": 300, + "height": 456, + "id": "llmChain_0", + "position": { + "x": 1183.0899727188096, + "y": 385.0159960992951 + }, + "type": "customNode", + "data": { + "id": "llmChain_0", + "label": "LLM Chain", + "version": 3, + "name": "llmChain", + "type": "LLMChain", + "baseClasses": ["LLMChain", "BaseChain", "Runnable"], + "category": "Chains", + "description": "Chain to run queries against LLMs", + "inputParams": [ + { + "label": "Chain Name", + "name": "chainName", + "type": "string", + "placeholder": "Name Your Chain", + "optional": true, + "id": "llmChain_0-input-chainName-string" + } + ], + "inputAnchors": [ + { + "label": "Language Model", + "name": "model", + "type": "BaseLanguageModel", + "id": "llmChain_0-input-model-BaseLanguageModel" + }, + { + "label": "Prompt", + "name": "prompt", + "type": "BasePromptTemplate", + "id": "llmChain_0-input-prompt-BasePromptTemplate" + }, + { + "label": "Output Parser", + "name": "outputParser", + "type": "BaseLLMOutputParser", + "optional": true, + "id": "llmChain_0-input-outputParser-BaseLLMOutputParser" + } + ], + "inputs": { + "model": "{{openAI_1.data.instance}}", + "prompt": "{{promptTemplate_0.data.instance}}", + "outputParser": "", + "chainName": "FirstChain" + }, + "outputAnchors": [ + { + "name": "output", + "label": "Output", + "type": "options", + "options": [ + { + "id": "llmChain_0-output-llmChain-LLMChain|BaseChain|Runnable", + "name": "llmChain", + "label": "LLM Chain", + "type": "LLMChain | BaseChain | Runnable" + }, + { + "id": "llmChain_0-output-outputPrediction-string|json", + "name": "outputPrediction", + "label": "Output Prediction", + "type": "string | json" + } + ], + "default": "llmChain" + } + ], + "outputs": { + "output": "outputPrediction" + }, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 1183.0899727188096, + "y": 385.0159960992951 + }, + "dragging": false + }, + { + "width": 300, + "height": 456, + "id": "llmChain_1", + "position": { + "x": 2773.675809586143, + "y": 114.39482869328754 + }, + "type": "customNode", + "data": { + "id": "llmChain_1", + "label": "LLM Chain", + "version": 3, + "name": "llmChain", + "type": "LLMChain", + "baseClasses": ["LLMChain", "BaseChain", "Runnable"], + "category": "Chains", + "description": "Chain to run queries against LLMs", + "inputParams": [ + { + "label": "Chain Name", + "name": "chainName", + "type": "string", + "placeholder": "Name Your Chain", + "optional": true, + "id": "llmChain_1-input-chainName-string" + } + ], + "inputAnchors": [ + { + "label": "Language Model", + "name": "model", + "type": "BaseLanguageModel", + "id": "llmChain_1-input-model-BaseLanguageModel" + }, + { + "label": "Prompt", + "name": "prompt", + "type": "BasePromptTemplate", + "id": "llmChain_1-input-prompt-BasePromptTemplate" + }, + { + "label": "Output Parser", + "name": "outputParser", + "type": "BaseLLMOutputParser", + "optional": true, + "id": "llmChain_1-input-outputParser-BaseLLMOutputParser" + } + ], + "inputs": { + "model": "{{openAI_2.data.instance}}", + "prompt": "{{promptTemplate_1.data.instance}}", + "outputParser": "", + "chainName": "LastChain" + }, + "outputAnchors": [ + { + "name": "output", + "label": "Output", + "type": "options", + "options": [ + { + "id": "llmChain_1-output-llmChain-LLMChain|BaseChain|Runnable", + "name": "llmChain", + "label": "LLM Chain", + "type": "LLMChain | BaseChain | Runnable" + }, + { + "id": "llmChain_1-output-outputPrediction-string|json", + "name": "outputPrediction", + "label": "Output Prediction", + "type": "string | json" + } + ], + "default": "llmChain" + } + ], + "outputs": { + "output": "llmChain" + }, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 2773.675809586143, + "y": 114.39482869328754 + }, + "dragging": false + }, + { + "width": 300, + "height": 511, + "id": "promptTemplate_2", + "position": { + "x": 1992.5456174373144, + "y": 675.5277193898106 + }, + "type": "customNode", + "data": { + "id": "promptTemplate_2", + "label": "Prompt Template", + "version": 1, + "name": "promptTemplate", + "type": "PromptTemplate", + "baseClasses": ["PromptTemplate", "BaseStringPromptTemplate", "BasePromptTemplate"], + "category": "Prompts", + "description": "Schema to represent a basic prompt for an LLM", + "inputParams": [ + { + "label": "Template", + "name": "template", + "type": "string", + "rows": 4, + "placeholder": "What is a good name for a company that makes {product}?", + "id": "promptTemplate_2-input-template-string" + }, + { + "label": "Format Prompt Values", + "name": "promptValues", + "type": "json", + "optional": true, + "acceptVariable": true, + "list": true, + "id": "promptTemplate_2-input-promptValues-json" + } + ], + "inputAnchors": [], + "inputs": { + "template": "Politely say \"I'm not able to answer the query\"", + "promptValues": "{\"objective\":\"{{question}}\",\"result\":\"\"}" + }, + "outputAnchors": [ + { + "id": "promptTemplate_2-output-promptTemplate-PromptTemplate|BaseStringPromptTemplate|BasePromptTemplate", + "name": "promptTemplate", + "label": "PromptTemplate", + "type": "PromptTemplate | BaseStringPromptTemplate | BasePromptTemplate" + } + ], + "outputs": {}, + "selected": false + }, + "positionAbsolute": { + "x": 1992.5456174373144, + "y": 675.5277193898106 + }, + "selected": false, + "dragging": false + }, + { + "width": 300, + "height": 507, + "id": "llmChain_2", + "position": { + "x": 2830.477603228176, + "y": 907.9116984679802 + }, + "type": "customNode", + "data": { + "id": "llmChain_2", + "label": "LLM Chain", + "version": 3, + "name": "llmChain", + "type": "LLMChain", + "baseClasses": ["LLMChain", "BaseChain", "Runnable"], + "category": "Chains", + "description": "Chain to run queries against LLMs", + "inputParams": [ + { + "label": "Chain Name", + "name": "chainName", + "type": "string", + "placeholder": "Name Your Chain", + "optional": true, + "id": "llmChain_2-input-chainName-string" + } + ], + "inputAnchors": [ + { + "label": "Language Model", + "name": "model", + "type": "BaseLanguageModel", + "id": "llmChain_2-input-model-BaseLanguageModel" + }, + { + "label": "Prompt", + "name": "prompt", + "type": "BasePromptTemplate", + "id": "llmChain_2-input-prompt-BasePromptTemplate" + }, + { + "label": "Output Parser", + "name": "outputParser", + "type": "BaseLLMOutputParser", + "optional": true, + "id": "llmChain_2-input-outputParser-BaseLLMOutputParser" + }, + { + "label": "Input Moderation", + "description": "Detect text that could generate harmful output and prevent it from being sent to the language model", + "name": "inputModeration", + "type": "Moderation", + "optional": true, + "list": true, + "id": "llmChain_2-input-inputModeration-Moderation" + } + ], + "inputs": { + "model": "{{chatOpenAI_0.data.instance}}", + "prompt": "{{promptTemplate_2.data.instance}}", + "outputParser": "", + "inputModeration": "", + "chainName": "FallbackChain" + }, + "outputAnchors": [ + { + "name": "output", + "label": "Output", + "type": "options", + "options": [ + { + "id": "llmChain_2-output-llmChain-LLMChain|BaseChain|Runnable", + "name": "llmChain", + "label": "LLM Chain", + "type": "LLMChain | BaseChain | Runnable" + }, + { + "id": "llmChain_2-output-outputPrediction-string|json", + "name": "outputPrediction", + "label": "Output Prediction", + "type": "string | json" + } + ], + "default": "llmChain" + } + ], + "outputs": { + "output": "llmChain" + }, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 2830.477603228176, + "y": 907.9116984679802 + }, + "dragging": false + }, + { + "width": 300, + "height": 755, + "id": "ifElseFunction_0", + "position": { + "x": 1590.6560099561739, + "y": 265.36655719326177 + }, + "type": "customNode", + "data": { + "id": "ifElseFunction_0", + "label": "IfElse Function", + "version": 1, + "name": "ifElseFunction", + "type": "IfElseFunction", + "baseClasses": ["IfElseFunction", "Utilities"], + "category": "Utilities", + "description": "Split flows based on If Else javascript functions", + "inputParams": [ + { + "label": "Input Variables", + "name": "functionInputVariables", + "description": "Input variables can be used in the function with prefix $. For example: $var", + "type": "json", + "optional": true, + "acceptVariable": true, + "list": true, + "id": "ifElseFunction_0-input-functionInputVariables-json" + }, + { + "label": "IfElse Name", + "name": "functionName", + "type": "string", + "optional": true, + "placeholder": "If Condition Match", + "id": "ifElseFunction_0-input-functionName-string" + }, + { + "label": "If Function", + "name": "ifFunction", + "description": "Function must return a value", + "type": "code", + "rows": 2, + "default": "if (\"hello\" == \"hello\") {\n return true;\n}", + "id": "ifElseFunction_0-input-ifFunction-code" + }, + { + "label": "Else Function", + "name": "elseFunction", + "description": "Function must return a value", + "type": "code", + "rows": 2, + "default": "return false;", + "id": "ifElseFunction_0-input-elseFunction-code" + } + ], + "inputAnchors": [], + "inputs": { + "functionInputVariables": "{\"task\":\"{{llmChain_0.data.instance}}\"}", + "functionName": "If Condition Match", + "ifFunction": "if (\"hello\" == \"21\") {\n return $task;\n}", + "elseFunction": "return false;" + }, + "outputAnchors": [ + { + "name": "output", + "label": "Output", + "type": "options", + "options": [ + { + "id": "ifElseFunction_0-output-returnTrue-string|number|boolean|json|array", + "name": "returnTrue", + "label": "True", + "type": "string | number | boolean | json | array" + }, + { + "id": "ifElseFunction_0-output-returnFalse-string|number|boolean|json|array", + "name": "returnFalse", + "label": "False", + "type": "string | number | boolean | json | array" + } + ], + "default": "returnTrue" + } + ], + "outputs": { + "output": "returnTrue" + }, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 1590.6560099561739, + "y": 265.36655719326177 + }, + "dragging": false + }, + { + "width": 300, + "height": 574, + "id": "chatOpenAI_0", + "position": { + "x": 2373.5711587130127, + "y": 487.8533802540226 + }, + "type": "customNode", + "data": { + "id": "chatOpenAI_0", + "label": "ChatOpenAI", + "version": 2, + "name": "chatOpenAI", + "type": "ChatOpenAI", + "baseClasses": ["ChatOpenAI", "BaseChatModel", "BaseLanguageModel", "Runnable"], + "category": "Chat Models", + "description": "Wrapper around OpenAI large language models that use the Chat endpoint", + "inputParams": [ + { + "label": "Connect Credential", + "name": "credential", + "type": "credential", + "credentialNames": ["openAIApi"], + "id": "chatOpenAI_0-input-credential-credential" + }, + { + "label": "Model Name", + "name": "modelName", + "type": "options", + "options": [ + { + "label": "gpt-4", + "name": "gpt-4" + }, + { + "label": "gpt-4-1106-preview", + "name": "gpt-4-1106-preview" + }, + { + "label": "gpt-4-vision-preview", + "name": "gpt-4-vision-preview" + }, + { + "label": "gpt-4-0613", + "name": "gpt-4-0613" + }, + { + "label": "gpt-4-32k", + "name": "gpt-4-32k" + }, + { + "label": "gpt-4-32k-0613", + "name": "gpt-4-32k-0613" + }, + { + "label": "gpt-3.5-turbo", + "name": "gpt-3.5-turbo" + }, + { + "label": "gpt-3.5-turbo-1106", + "name": "gpt-3.5-turbo-1106" + }, + { + "label": "gpt-3.5-turbo-0613", + "name": "gpt-3.5-turbo-0613" + }, + { + "label": "gpt-3.5-turbo-16k", + "name": "gpt-3.5-turbo-16k" + }, + { + "label": "gpt-3.5-turbo-16k-0613", + "name": "gpt-3.5-turbo-16k-0613" + } + ], + "default": "gpt-3.5-turbo", + "optional": true, + "id": "chatOpenAI_0-input-modelName-options" + }, + { + "label": "Temperature", + "name": "temperature", + "type": "number", + "step": 0.1, + "default": 0.9, + "optional": true, + "id": "chatOpenAI_0-input-temperature-number" + }, + { + "label": "Max Tokens", + "name": "maxTokens", + "type": "number", + "step": 1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-maxTokens-number" + }, + { + "label": "Top Probability", + "name": "topP", + "type": "number", + "step": 0.1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-topP-number" + }, + { + "label": "Frequency Penalty", + "name": "frequencyPenalty", + "type": "number", + "step": 0.1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-frequencyPenalty-number" + }, + { + "label": "Presence Penalty", + "name": "presencePenalty", + "type": "number", + "step": 0.1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-presencePenalty-number" + }, + { + "label": "Timeout", + "name": "timeout", + "type": "number", + "step": 1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-timeout-number" + }, + { + "label": "BasePath", + "name": "basepath", + "type": "string", + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-basepath-string" + }, + { + "label": "BaseOptions", + "name": "baseOptions", + "type": "json", + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-baseOptions-json" + } + ], + "inputAnchors": [ + { + "label": "Cache", + "name": "cache", + "type": "BaseCache", + "optional": true, + "id": "chatOpenAI_0-input-cache-BaseCache" + } + ], + "inputs": { + "cache": "", + "modelName": "gpt-3.5-turbo", + "temperature": 0.9, + "maxTokens": "", + "topP": "", + "frequencyPenalty": "", + "presencePenalty": "", + "timeout": "", + "basepath": "", + "baseOptions": "" + }, + "outputAnchors": [ + { + "id": "chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable", + "name": "chatOpenAI", + "label": "ChatOpenAI", + "type": "ChatOpenAI | BaseChatModel | BaseLanguageModel | Runnable" + } + ], + "outputs": {}, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 2373.5711587130127, + "y": 487.8533802540226 + }, + "dragging": false + } + ], + "edges": [ + { + "source": "openAI_1", + "sourceHandle": "openAI_1-output-openAI-OpenAI|BaseLLM|BaseLanguageModel", + "target": "llmChain_0", + "targetHandle": "llmChain_0-input-model-BaseLanguageModel", + "type": "buttonedge", + "id": "openAI_1-openAI_1-output-openAI-OpenAI|BaseLLM|BaseLanguageModel-llmChain_0-llmChain_0-input-model-BaseLanguageModel", + "data": { + "label": "" + } + }, + { + "source": "promptTemplate_0", + "sourceHandle": "promptTemplate_0-output-promptTemplate-PromptTemplate|BaseStringPromptTemplate|BasePromptTemplate", + "target": "llmChain_0", + "targetHandle": "llmChain_0-input-prompt-BasePromptTemplate", + "type": "buttonedge", + "id": "promptTemplate_0-promptTemplate_0-output-promptTemplate-PromptTemplate|BaseStringPromptTemplate|BasePromptTemplate-llmChain_0-llmChain_0-input-prompt-BasePromptTemplate", + "data": { + "label": "" + } + }, + { + "source": "promptTemplate_1", + "sourceHandle": "promptTemplate_1-output-promptTemplate-PromptTemplate|BaseStringPromptTemplate|BasePromptTemplate", + "target": "llmChain_1", + "targetHandle": "llmChain_1-input-prompt-BasePromptTemplate", + "type": "buttonedge", + "id": "promptTemplate_1-promptTemplate_1-output-promptTemplate-PromptTemplate|BaseStringPromptTemplate|BasePromptTemplate-llmChain_1-llmChain_1-input-prompt-BasePromptTemplate", + "data": { + "label": "" + } + }, + { + "source": "openAI_2", + "sourceHandle": "openAI_2-output-openAI-OpenAI|BaseLLM|BaseLanguageModel", + "target": "llmChain_1", + "targetHandle": "llmChain_1-input-model-BaseLanguageModel", + "type": "buttonedge", + "id": "openAI_2-openAI_2-output-openAI-OpenAI|BaseLLM|BaseLanguageModel-llmChain_1-llmChain_1-input-model-BaseLanguageModel", + "data": { + "label": "" + } + }, + { + "source": "promptTemplate_2", + "sourceHandle": "promptTemplate_2-output-promptTemplate-PromptTemplate|BaseStringPromptTemplate|BasePromptTemplate", + "target": "llmChain_2", + "targetHandle": "llmChain_2-input-prompt-BasePromptTemplate", + "type": "buttonedge", + "id": "promptTemplate_2-promptTemplate_2-output-promptTemplate-PromptTemplate|BaseStringPromptTemplate|BasePromptTemplate-llmChain_2-llmChain_2-input-prompt-BasePromptTemplate" + }, + { + "source": "llmChain_0", + "sourceHandle": "llmChain_0-output-outputPrediction-string|json", + "target": "ifElseFunction_0", + "targetHandle": "ifElseFunction_0-input-functionInputVariables-json", + "type": "buttonedge", + "id": "llmChain_0-llmChain_0-output-outputPrediction-string|json-ifElseFunction_0-ifElseFunction_0-input-functionInputVariables-json" + }, + { + "source": "ifElseFunction_0", + "sourceHandle": "ifElseFunction_0-output-returnFalse-string|number|boolean|json|array", + "target": "promptTemplate_2", + "targetHandle": "promptTemplate_2-input-promptValues-json", + "type": "buttonedge", + "id": "ifElseFunction_0-ifElseFunction_0-output-returnFalse-string|number|boolean|json|array-promptTemplate_2-promptTemplate_2-input-promptValues-json" + }, + { + "source": "ifElseFunction_0", + "sourceHandle": "ifElseFunction_0-output-returnTrue-string|number|boolean|json|array", + "target": "promptTemplate_1", + "targetHandle": "promptTemplate_1-input-promptValues-json", + "type": "buttonedge", + "id": "ifElseFunction_0-ifElseFunction_0-output-returnTrue-string|number|boolean|json|array-promptTemplate_1-promptTemplate_1-input-promptValues-json" + }, + { + "source": "chatOpenAI_0", + "sourceHandle": "chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable", + "target": "llmChain_2", + "targetHandle": "llmChain_2-input-model-BaseLanguageModel", + "type": "buttonedge", + "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable-llmChain_2-llmChain_2-input-model-BaseLanguageModel" + } + ] +} diff --git a/packages/server/marketplaces/chatflows/Local QnA.json b/packages/server/marketplaces/chatflows/Local QnA.json index e24ad7ca..6f78cb05 100644 --- a/packages/server/marketplaces/chatflows/Local QnA.json +++ b/packages/server/marketplaces/chatflows/Local QnA.json @@ -83,10 +83,10 @@ "data": { "id": "conversationalRetrievalQAChain_0", "label": "Conversational Retrieval QA Chain", - "version": 1, + "version": 2, "name": "conversationalRetrievalQAChain", "type": "ConversationalRetrievalQAChain", - "baseClasses": ["ConversationalRetrievalQAChain", "BaseChain", "BaseLangChain"], + "baseClasses": ["ConversationalRetrievalQAChain", "BaseChain", "Runnable"], "category": "Chains", "description": "Document QA - built on RetrievalQAChain to provide a chat history component", "inputParams": [ @@ -98,47 +98,36 @@ "id": "conversationalRetrievalQAChain_0-input-returnSourceDocuments-boolean" }, { - "label": "System Message", - "name": "systemMessagePrompt", + "label": "Rephrase Prompt", + "name": "rephrasePrompt", "type": "string", + "description": "Using previous chat history, rephrase question into a standalone question", + "warning": "Prompt must include input variables: {chat_history} and {question}", "rows": 4, "additionalParams": true, "optional": true, - "placeholder": "I want you to act as a document that I am having a conversation with. Your name is \"AI Assistant\". You will provide me with answers from the given info. If the answer is not included, say exactly \"Hmm, I am not sure.\" and stop after that. Refuse to answer any question not about the info. Never break character.", - "id": "conversationalRetrievalQAChain_0-input-systemMessagePrompt-string" + "default": "Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.\n\nChat History:\n{chat_history}\nFollow Up Input: {question}\nStandalone Question:", + "id": "conversationalRetrievalQAChain_0-input-rephrasePrompt-string" }, { - "label": "Chain Option", - "name": "chainOption", - "type": "options", - "options": [ - { - "label": "MapReduceDocumentsChain", - "name": "map_reduce", - "description": "Suitable for QA tasks over larger documents and can run the preprocessing step in parallel, reducing the running time" - }, - { - "label": "RefineDocumentsChain", - "name": "refine", - "description": "Suitable for QA tasks over a large number of documents." - }, - { - "label": "StuffDocumentsChain", - "name": "stuff", - "description": "Suitable for QA tasks over a small number of documents." - } - ], + "label": "Response Prompt", + "name": "responsePrompt", + "type": "string", + "description": "Taking the rephrased question, search for answer from the provided context", + "warning": "Prompt must include input variable: {context}", + "rows": 4, "additionalParams": true, "optional": true, - "id": "conversationalRetrievalQAChain_0-input-chainOption-options" + "default": "You are a helpful assistant. Using the provided context, answer the user's question to the best of your ability using the resources provided.\nIf there is nothing in the context relevant to the question at hand, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.\n------------\n{context}\n------------\nREMEMBER: If there is no relevant information within the context, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.", + "id": "conversationalRetrievalQAChain_0-input-responsePrompt-string" } ], "inputAnchors": [ { - "label": "Language Model", + "label": "Chat Model", "name": "model", - "type": "BaseLanguageModel", - "id": "conversationalRetrievalQAChain_0-input-model-BaseLanguageModel" + "type": "BaseChatModel", + "id": "conversationalRetrievalQAChain_0-input-model-BaseChatModel" }, { "label": "Vector Store Retriever", @@ -158,14 +147,16 @@ "inputs": { "model": "{{chatOllama_0.data.instance}}", "vectorStoreRetriever": "{{faiss_0.data.instance}}", - "memory": "" + "memory": "", + "rephrasePrompt": "Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.\n\nChat History:\n{chat_history}\nFollow Up Input: {question}\nStandalone Question:", + "responsePrompt": "You are a helpful assistant. Using the provided context, answer the user's question to the best of your ability using the resources provided.\nIf there is nothing in the context relevant to the question at hand, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.\n------------\n{context}\n------------\nREMEMBER: If there is no relevant information within the context, just say \"Hmm, I'm not sure.\" Don't try to make up an answer." }, "outputAnchors": [ { - "id": "conversationalRetrievalQAChain_0-output-conversationalRetrievalQAChain-ConversationalRetrievalQAChain|BaseChain|BaseLangChain", + "id": "conversationalRetrievalQAChain_0-output-conversationalRetrievalQAChain-ConversationalRetrievalQAChain|BaseChain|Runnable", "name": "conversationalRetrievalQAChain", "label": "ConversationalRetrievalQAChain", - "type": "ConversationalRetrievalQAChain | BaseChain | BaseLangChain" + "type": "ConversationalRetrievalQAChain | BaseChain | Runnable" } ], "outputs": {}, @@ -649,9 +640,9 @@ "source": "chatOllama_0", "sourceHandle": "chatOllama_0-output-chatOllama-ChatOllama|SimpleChatModel|BaseChatModel|BaseLanguageModel|Runnable", "target": "conversationalRetrievalQAChain_0", - "targetHandle": "conversationalRetrievalQAChain_0-input-model-BaseLanguageModel", + "targetHandle": "conversationalRetrievalQAChain_0-input-model-BaseChatModel", "type": "buttonedge", - "id": "chatOllama_0-chatOllama_0-output-chatOllama-ChatOllama|SimpleChatModel|BaseChatModel|BaseLanguageModel|Runnable-conversationalRetrievalQAChain_0-conversationalRetrievalQAChain_0-input-model-BaseLanguageModel", + "id": "chatOllama_0-chatOllama_0-output-chatOllama-ChatOllama|SimpleChatModel|BaseChatModel|BaseLanguageModel|Runnable-conversationalRetrievalQAChain_0-conversationalRetrievalQAChain_0-input-model-BaseChatModel", "data": { "label": "" } diff --git a/packages/server/marketplaces/chatflows/Long Term Memory.json b/packages/server/marketplaces/chatflows/Long Term Memory.json index 2973594f..cf0fa4d4 100644 --- a/packages/server/marketplaces/chatflows/Long Term Memory.json +++ b/packages/server/marketplaces/chatflows/Long Term Memory.json @@ -13,10 +13,10 @@ "data": { "id": "conversationalRetrievalQAChain_0", "label": "Conversational Retrieval QA Chain", - "version": 1, + "version": 2, "name": "conversationalRetrievalQAChain", "type": "ConversationalRetrievalQAChain", - "baseClasses": ["ConversationalRetrievalQAChain", "BaseChain", "BaseLangChain"], + "baseClasses": ["ConversationalRetrievalQAChain", "BaseChain", "Runnable"], "category": "Chains", "description": "Document QA - built on RetrievalQAChain to provide a chat history component", "inputParams": [ @@ -28,47 +28,36 @@ "id": "conversationalRetrievalQAChain_0-input-returnSourceDocuments-boolean" }, { - "label": "System Message", - "name": "systemMessagePrompt", + "label": "Rephrase Prompt", + "name": "rephrasePrompt", "type": "string", + "description": "Using previous chat history, rephrase question into a standalone question", + "warning": "Prompt must include input variables: {chat_history} and {question}", "rows": 4, "additionalParams": true, "optional": true, - "placeholder": "I want you to act as a document that I am having a conversation with. Your name is \"AI Assistant\". You will provide me with answers from the given info. If the answer is not included, say exactly \"Hmm, I am not sure.\" and stop after that. Refuse to answer any question not about the info. Never break character.", - "id": "conversationalRetrievalQAChain_0-input-systemMessagePrompt-string" + "default": "Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.\n\nChat History:\n{chat_history}\nFollow Up Input: {question}\nStandalone Question:", + "id": "conversationalRetrievalQAChain_0-input-rephrasePrompt-string" }, { - "label": "Chain Option", - "name": "chainOption", - "type": "options", - "options": [ - { - "label": "MapReduceDocumentsChain", - "name": "map_reduce", - "description": "Suitable for QA tasks over larger documents and can run the preprocessing step in parallel, reducing the running time" - }, - { - "label": "RefineDocumentsChain", - "name": "refine", - "description": "Suitable for QA tasks over a large number of documents." - }, - { - "label": "StuffDocumentsChain", - "name": "stuff", - "description": "Suitable for QA tasks over a small number of documents." - } - ], + "label": "Response Prompt", + "name": "responsePrompt", + "type": "string", + "description": "Taking the rephrased question, search for answer from the provided context", + "warning": "Prompt must include input variable: {context}", + "rows": 4, "additionalParams": true, "optional": true, - "id": "conversationalRetrievalQAChain_0-input-chainOption-options" + "default": "You are a helpful assistant. Using the provided context, answer the user's question to the best of your ability using the resources provided.\nIf there is nothing in the context relevant to the question at hand, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.\n------------\n{context}\n------------\nREMEMBER: If there is no relevant information within the context, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.", + "id": "conversationalRetrievalQAChain_0-input-responsePrompt-string" } ], "inputAnchors": [ { - "label": "Language Model", + "label": "Chat Model", "name": "model", - "type": "BaseLanguageModel", - "id": "conversationalRetrievalQAChain_0-input-model-BaseLanguageModel" + "type": "BaseChatModel", + "id": "conversationalRetrievalQAChain_0-input-model-BaseChatModel" }, { "label": "Vector Store Retriever", @@ -89,14 +78,16 @@ "model": "{{chatOpenAI_0.data.instance}}", "vectorStoreRetriever": "{{qdrant_0.data.instance}}", "memory": "{{ZepMemory_0.data.instance}}", - "returnSourceDocuments": true + "returnSourceDocuments": true, + "rephrasePrompt": "Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.\n\nChat History:\n{chat_history}\nFollow Up Input: {question}\nStandalone Question:", + "responsePrompt": "You are a helpful assistant. Using the provided context, answer the user's question to the best of your ability using the resources provided.\nIf there is nothing in the context relevant to the question at hand, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.\n------------\n{context}\n------------\nREMEMBER: If there is no relevant information within the context, just say \"Hmm, I'm not sure.\" Don't try to make up an answer." }, "outputAnchors": [ { - "id": "conversationalRetrievalQAChain_0-output-conversationalRetrievalQAChain-ConversationalRetrievalQAChain|BaseChain|BaseLangChain", + "id": "conversationalRetrievalQAChain_0-output-conversationalRetrievalQAChain-ConversationalRetrievalQAChain|BaseChain|Runnable", "name": "conversationalRetrievalQAChain", "label": "ConversationalRetrievalQAChain", - "type": "ConversationalRetrievalQAChain | BaseChain | BaseLangChain" + "type": "ConversationalRetrievalQAChain | BaseChain | Runnable" } ], "outputs": {}, @@ -232,7 +223,7 @@ "label": "Session Id", "name": "sessionId", "type": "string", - "description": "if empty, chatId will be used automatically", + "description": "If not specified, a random id will be used. Learn more", "default": "", "additionalParams": true, "optional": true, @@ -244,8 +235,8 @@ "type": "number", "default": "10", "step": 1, - "description": "Window of size k to surface the last k back-and-forths to use as memory.", "additionalParams": true, + "description": "Window of size k to surface the last k back-and-forths to use as memory.", "id": "ZepMemory_0-input-k-number" }, { @@ -709,9 +700,9 @@ "source": "chatOpenAI_0", "sourceHandle": "chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable", "target": "conversationalRetrievalQAChain_0", - "targetHandle": "conversationalRetrievalQAChain_0-input-model-BaseLanguageModel", + "targetHandle": "conversationalRetrievalQAChain_0-input-model-BaseChatModel", "type": "buttonedge", - "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable-conversationalRetrievalQAChain_0-conversationalRetrievalQAChain_0-input-model-BaseLanguageModel", + "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable-conversationalRetrievalQAChain_0-conversationalRetrievalQAChain_0-input-model-BaseChatModel", "data": { "label": "" } diff --git a/packages/server/marketplaces/chatflows/Metadata Filter.json b/packages/server/marketplaces/chatflows/Metadata Filter.json index 9865ae70..f7b2fbfb 100644 --- a/packages/server/marketplaces/chatflows/Metadata Filter.json +++ b/packages/server/marketplaces/chatflows/Metadata Filter.json @@ -249,10 +249,10 @@ "data": { "id": "conversationalRetrievalQAChain_0", "label": "Conversational Retrieval QA Chain", - "version": 1, + "version": 2, "name": "conversationalRetrievalQAChain", "type": "ConversationalRetrievalQAChain", - "baseClasses": ["ConversationalRetrievalQAChain", "BaseChain", "BaseLangChain"], + "baseClasses": ["ConversationalRetrievalQAChain", "BaseChain", "Runnable"], "category": "Chains", "description": "Document QA - built on RetrievalQAChain to provide a chat history component", "inputParams": [ @@ -264,47 +264,36 @@ "id": "conversationalRetrievalQAChain_0-input-returnSourceDocuments-boolean" }, { - "label": "System Message", - "name": "systemMessagePrompt", + "label": "Rephrase Prompt", + "name": "rephrasePrompt", "type": "string", + "description": "Using previous chat history, rephrase question into a standalone question", + "warning": "Prompt must include input variables: {chat_history} and {question}", "rows": 4, "additionalParams": true, "optional": true, - "placeholder": "I want you to act as a document that I am having a conversation with. Your name is \"AI Assistant\". You will provide me with answers from the given info. If the answer is not included, say exactly \"Hmm, I am not sure.\" and stop after that. Refuse to answer any question not about the info. Never break character.", - "id": "conversationalRetrievalQAChain_0-input-systemMessagePrompt-string" + "default": "Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.\n\nChat History:\n{chat_history}\nFollow Up Input: {question}\nStandalone Question:", + "id": "conversationalRetrievalQAChain_0-input-rephrasePrompt-string" }, { - "label": "Chain Option", - "name": "chainOption", - "type": "options", - "options": [ - { - "label": "MapReduceDocumentsChain", - "name": "map_reduce", - "description": "Suitable for QA tasks over larger documents and can run the preprocessing step in parallel, reducing the running time" - }, - { - "label": "RefineDocumentsChain", - "name": "refine", - "description": "Suitable for QA tasks over a large number of documents." - }, - { - "label": "StuffDocumentsChain", - "name": "stuff", - "description": "Suitable for QA tasks over a small number of documents." - } - ], + "label": "Response Prompt", + "name": "responsePrompt", + "type": "string", + "description": "Taking the rephrased question, search for answer from the provided context", + "warning": "Prompt must include input variable: {context}", + "rows": 4, "additionalParams": true, "optional": true, - "id": "conversationalRetrievalQAChain_0-input-chainOption-options" + "default": "You are a helpful assistant. Using the provided context, answer the user's question to the best of your ability using the resources provided.\nIf there is nothing in the context relevant to the question at hand, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.\n------------\n{context}\n------------\nREMEMBER: If there is no relevant information within the context, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.", + "id": "conversationalRetrievalQAChain_0-input-responsePrompt-string" } ], "inputAnchors": [ { - "label": "Language Model", + "label": "Chat Model", "name": "model", - "type": "BaseLanguageModel", - "id": "conversationalRetrievalQAChain_0-input-model-BaseLanguageModel" + "type": "BaseChatModel", + "id": "conversationalRetrievalQAChain_0-input-model-BaseChatModel" }, { "label": "Vector Store Retriever", @@ -323,14 +312,16 @@ ], "inputs": { "model": "{{chatOpenAI_0.data.instance}}", - "vectorStoreRetriever": "{{pinecone_0.data.instance}}" + "vectorStoreRetriever": "{{pinecone_0.data.instance}}", + "rephrasePrompt": "Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.\n\nChat History:\n{chat_history}\nFollow Up Input: {question}\nStandalone Question:", + "responsePrompt": "You are a helpful assistant. Using the provided context, answer the user's question to the best of your ability using the resources provided.\nIf there is nothing in the context relevant to the question at hand, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.\n------------\n{context}\n------------\nREMEMBER: If there is no relevant information within the context, just say \"Hmm, I'm not sure.\" Don't try to make up an answer." }, "outputAnchors": [ { - "id": "conversationalRetrievalQAChain_0-output-conversationalRetrievalQAChain-ConversationalRetrievalQAChain|BaseChain|BaseLangChain", + "id": "conversationalRetrievalQAChain_0-output-conversationalRetrievalQAChain-ConversationalRetrievalQAChain|BaseChain|Runnable", "name": "conversationalRetrievalQAChain", "label": "ConversationalRetrievalQAChain", - "type": "ConversationalRetrievalQAChain | BaseChain | BaseLangChain" + "type": "ConversationalRetrievalQAChain | BaseChain | Runnable" } ], "outputs": {}, @@ -634,7 +625,7 @@ "type": "Pinecone", "baseClasses": ["Pinecone", "VectorStoreRetriever", "BaseRetriever"], "category": "Vector Stores", - "description": "Upsert embedded data and perform similarity search upon query using Pinecone, a leading fully managed hosted vector database", + "description": "Upsert embedded data and perform similarity or mmr search using Pinecone, a leading fully managed hosted vector database", "inputParams": [ { "label": "Connect Credential", @@ -675,6 +666,45 @@ "additionalParams": true, "optional": true, "id": "pinecone_0-input-topK-number" + }, + { + "label": "Search Type", + "name": "searchType", + "type": "options", + "default": "similarity", + "options": [ + { + "label": "Similarity", + "name": "similarity" + }, + { + "label": "Max Marginal Relevance", + "name": "mmr" + } + ], + "additionalParams": true, + "optional": true, + "id": "pinecone_0-input-searchType-options" + }, + { + "label": "Fetch K (for MMR Search)", + "name": "fetchK", + "description": "Number of initial documents to fetch for MMR reranking. Default to 20. Used only when the search type is MMR", + "placeholder": "20", + "type": "number", + "additionalParams": true, + "optional": true, + "id": "pinecone_0-input-fetchK-number" + }, + { + "label": "Lambda (for MMR Search)", + "name": "lambda", + "description": "Number between 0 and 1 that determines the degree of diversity among the results, where 0 corresponds to maximum diversity and 1 to minimum diversity. Used only when the search type is MMR", + "placeholder": "0.5", + "type": "number", + "additionalParams": true, + "optional": true, + "id": "pinecone_0-input-lambda-number" } ], "inputAnchors": [ @@ -699,7 +729,10 @@ "pineconeIndex": "", "pineconeNamespace": "", "pineconeMetadataFilter": "{\"id\":{\"$in\":[\"doc1\",\"doc2\"]}}", - "topK": "" + "topK": "", + "searchType": "similarity", + "fetchK": "", + "lambda": "" }, "outputAnchors": [ { @@ -763,9 +796,9 @@ "source": "chatOpenAI_0", "sourceHandle": "chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable", "target": "conversationalRetrievalQAChain_0", - "targetHandle": "conversationalRetrievalQAChain_0-input-model-BaseLanguageModel", + "targetHandle": "conversationalRetrievalQAChain_0-input-model-BaseChatModel", "type": "buttonedge", - "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable-conversationalRetrievalQAChain_0-conversationalRetrievalQAChain_0-input-model-BaseLanguageModel", + "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable-conversationalRetrievalQAChain_0-conversationalRetrievalQAChain_0-input-model-BaseChatModel", "data": { "label": "" } diff --git a/packages/server/marketplaces/chatflows/Multi Retrieval QA Chain.json b/packages/server/marketplaces/chatflows/Multi Retrieval QA Chain.json index 5388d965..e86b28c9 100644 --- a/packages/server/marketplaces/chatflows/Multi Retrieval QA Chain.json +++ b/packages/server/marketplaces/chatflows/Multi Retrieval QA Chain.json @@ -560,7 +560,7 @@ "type": "Pinecone", "baseClasses": ["Pinecone", "VectorStoreRetriever", "BaseRetriever"], "category": "Vector Stores", - "description": "Upsert embedded data and perform similarity search upon query using Pinecone, a leading fully managed hosted vector database", + "description": "Upsert embedded data and perform similarity or mmr search using Pinecone, a leading fully managed hosted vector database", "inputParams": [ { "label": "Connect Credential", @@ -601,6 +601,45 @@ "additionalParams": true, "optional": true, "id": "pinecone_0-input-topK-number" + }, + { + "label": "Search Type", + "name": "searchType", + "type": "options", + "default": "similarity", + "options": [ + { + "label": "Similarity", + "name": "similarity" + }, + { + "label": "Max Marginal Relevance", + "name": "mmr" + } + ], + "additionalParams": true, + "optional": true, + "id": "pinecone_0-input-searchType-options" + }, + { + "label": "Fetch K (for MMR Search)", + "name": "fetchK", + "description": "Number of initial documents to fetch for MMR reranking. Default to 20. Used only when the search type is MMR", + "placeholder": "20", + "type": "number", + "additionalParams": true, + "optional": true, + "id": "pinecone_0-input-fetchK-number" + }, + { + "label": "Lambda (for MMR Search)", + "name": "lambda", + "description": "Number between 0 and 1 that determines the degree of diversity among the results, where 0 corresponds to maximum diversity and 1 to minimum diversity. Used only when the search type is MMR", + "placeholder": "0.5", + "type": "number", + "additionalParams": true, + "optional": true, + "id": "pinecone_0-input-lambda-number" } ], "inputAnchors": [ @@ -625,7 +664,10 @@ "pineconeIndex": "", "pineconeNamespace": "", "pineconeMetadataFilter": "", - "topK": "" + "topK": "", + "searchType": "similarity", + "fetchK": "", + "lambda": "" }, "outputAnchors": [ { @@ -840,6 +882,45 @@ "additionalParams": true, "optional": true, "id": "supabase_0-input-topK-number" + }, + { + "label": "Search Type", + "name": "searchType", + "type": "options", + "default": "similarity", + "options": [ + { + "label": "Similarity", + "name": "similarity" + }, + { + "label": "Max Marginal Relevance", + "name": "mmr" + } + ], + "additionalParams": true, + "optional": true, + "id": "pinecone_0-input-searchType-options" + }, + { + "label": "Fetch K (for MMR Search)", + "name": "fetchK", + "description": "Number of initial documents to fetch for MMR reranking. Default to 20. Used only when the search type is MMR", + "placeholder": "20", + "type": "number", + "additionalParams": true, + "optional": true, + "id": "pinecone_0-input-fetchK-number" + }, + { + "label": "Lambda (for MMR Search)", + "name": "lambda", + "description": "Number between 0 and 1 that determines the degree of diversity among the results, where 0 corresponds to maximum diversity and 1 to minimum diversity. Used only when the search type is MMR", + "placeholder": "0.5", + "type": "number", + "additionalParams": true, + "optional": true, + "id": "pinecone_0-input-lambda-number" } ], "inputAnchors": [ @@ -865,7 +946,10 @@ "tableName": "", "queryName": "", "supabaseMetadataFilter": "", - "topK": "" + "topK": "", + "searchType": "similarity", + "fetchK": "", + "lambda": "" }, "outputAnchors": [ { diff --git a/packages/server/marketplaces/chatflows/Multiple VectorDB.json b/packages/server/marketplaces/chatflows/Multiple VectorDB.json index 789b0c08..d53cb55e 100644 --- a/packages/server/marketplaces/chatflows/Multiple VectorDB.json +++ b/packages/server/marketplaces/chatflows/Multiple VectorDB.json @@ -54,7 +54,7 @@ "inputs": { "name": "ai-paper-qa", "description": "AI Paper QA - useful for when you need to ask questions about the AI-Generated Content paper.", - "returnDirect": "", + "returnDirect": true, "baseChain": "{{retrievalQAChain_0.data.instance}}" }, "outputAnchors": [ @@ -128,7 +128,7 @@ "inputs": { "name": "state-of-union-qa", "description": "State of the Union QA - useful for when you need to ask questions about the president speech and most recent state of the union address.", - "returnDirect": "", + "returnDirect": true, "baseChain": "{{retrievalQAChain_1.data.instance}}" }, "outputAnchors": [ @@ -1567,7 +1567,7 @@ "id": "conversationalAgent_0-input-tools-Tool" }, { - "label": "Language Model", + "label": "Chat Model", "name": "model", "type": "BaseChatModel", "id": "conversationalAgent_0-input-model-BaseChatModel" diff --git a/packages/server/marketplaces/chatflows/OpenAI Assistant.json b/packages/server/marketplaces/chatflows/OpenAI Assistant.json index ba4c6134..e9311c97 100644 --- a/packages/server/marketplaces/chatflows/OpenAI Assistant.json +++ b/packages/server/marketplaces/chatflows/OpenAI Assistant.json @@ -14,7 +14,7 @@ "data": { "id": "openAIAssistant_0", "label": "OpenAI Assistant", - "version": 2, + "version": 3, "name": "openAIAssistant", "type": "OpenAIAssistant", "baseClasses": ["OpenAIAssistant"], @@ -45,6 +45,15 @@ "type": "Tool", "list": true, "id": "openAIAssistant_0-input-tools-Tool" + }, + { + "label": "Input Moderation", + "description": "Detect text that could generate harmful output and prevent it from being sent to the language model", + "name": "inputModeration", + "type": "Moderation", + "optional": true, + "list": true, + "id": "openAIAssistant_0-input-inputModeration-Moderation" } ], "inputs": { diff --git a/packages/server/marketplaces/chatflows/Query Engine.json b/packages/server/marketplaces/chatflows/Query Engine.json index 921ff1d6..625097cc 100644 --- a/packages/server/marketplaces/chatflows/Query Engine.json +++ b/packages/server/marketplaces/chatflows/Query Engine.json @@ -197,7 +197,7 @@ "baseClasses": ["OpenAIEmbedding", "BaseEmbedding_LlamaIndex", "BaseEmbedding"], "tags": ["LlamaIndex"], "category": "Embeddings", - "description": "OpenAI Embedding with LlamaIndex implementation", + "description": "OpenAI Embedding specific for LlamaIndex", "inputParams": [ { "label": "Connect Credential", @@ -328,7 +328,7 @@ "baseClasses": ["ChatAnthropic", "BaseChatModel_LlamaIndex"], "tags": ["LlamaIndex"], "category": "Chat Models", - "description": "Wrapper around ChatAnthropic LLM with LlamaIndex implementation", + "description": "Wrapper around ChatAnthropic LLM specific for LlamaIndex", "inputParams": [ { "label": "Connect Credential", diff --git a/packages/server/marketplaces/chatflows/SQL Prompt.json b/packages/server/marketplaces/chatflows/SQL Prompt.json new file mode 100644 index 00000000..ad08fed8 --- /dev/null +++ b/packages/server/marketplaces/chatflows/SQL Prompt.json @@ -0,0 +1,1712 @@ +{ + "description": "Manually construct prompts to query a SQL database", + "badge": "new", + "nodes": [ + { + "width": 300, + "height": 511, + "id": "promptTemplate_0", + "position": { + "x": 384.84394025989127, + "y": 61.21205260943492 + }, + "type": "customNode", + "data": { + "id": "promptTemplate_0", + "label": "Prompt Template", + "version": 1, + "name": "promptTemplate", + "type": "PromptTemplate", + "baseClasses": ["PromptTemplate", "BaseStringPromptTemplate", "BasePromptTemplate", "Runnable"], + "category": "Prompts", + "description": "Schema to represent a basic prompt for an LLM", + "inputParams": [ + { + "label": "Template", + "name": "template", + "type": "string", + "rows": 4, + "placeholder": "What is a good name for a company that makes {product}?", + "id": "promptTemplate_0-input-template-string" + }, + { + "label": "Format Prompt Values", + "name": "promptValues", + "type": "json", + "optional": true, + "acceptVariable": true, + "list": true, + "id": "promptTemplate_0-input-promptValues-json" + } + ], + "inputAnchors": [], + "inputs": { + "template": "Based on the provided SQL table schema and question below, return a SQL SELECT ALL query that would answer the user's question. For example: SELECT * FROM table WHERE id = '1'.\n------------\nSCHEMA: {schema}\n------------\nQUESTION: {question}\n------------\nSQL QUERY:", + "promptValues": "{\"schema\":\"{{setVariable_0.data.instance}}\",\"question\":\"{{question}}\"}" + }, + "outputAnchors": [ + { + "id": "promptTemplate_0-output-promptTemplate-PromptTemplate|BaseStringPromptTemplate|BasePromptTemplate|Runnable", + "name": "promptTemplate", + "label": "PromptTemplate", + "type": "PromptTemplate | BaseStringPromptTemplate | BasePromptTemplate | Runnable" + } + ], + "outputs": {}, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 384.84394025989127, + "y": 61.21205260943492 + }, + "dragging": false + }, + { + "width": 300, + "height": 507, + "id": "llmChain_0", + "position": { + "x": 770.4559230968546, + "y": -127.11351409346554 + }, + "type": "customNode", + "data": { + "id": "llmChain_0", + "label": "LLM Chain", + "version": 3, + "name": "llmChain", + "type": "LLMChain", + "baseClasses": ["LLMChain", "BaseChain", "Runnable"], + "category": "Chains", + "description": "Chain to run queries against LLMs", + "inputParams": [ + { + "label": "Chain Name", + "name": "chainName", + "type": "string", + "placeholder": "Name Your Chain", + "optional": true, + "id": "llmChain_0-input-chainName-string" + } + ], + "inputAnchors": [ + { + "label": "Language Model", + "name": "model", + "type": "BaseLanguageModel", + "id": "llmChain_0-input-model-BaseLanguageModel" + }, + { + "label": "Prompt", + "name": "prompt", + "type": "BasePromptTemplate", + "id": "llmChain_0-input-prompt-BasePromptTemplate" + }, + { + "label": "Output Parser", + "name": "outputParser", + "type": "BaseLLMOutputParser", + "optional": true, + "id": "llmChain_0-input-outputParser-BaseLLMOutputParser" + }, + { + "label": "Input Moderation", + "description": "Detect text that could generate harmful output and prevent it from being sent to the language model", + "name": "inputModeration", + "type": "Moderation", + "optional": true, + "list": true, + "id": "llmChain_0-input-inputModeration-Moderation" + } + ], + "inputs": { + "model": "{{chatOpenAI_0.data.instance}}", + "prompt": "{{promptTemplate_0.data.instance}}", + "outputParser": "", + "inputModeration": "", + "chainName": "SQL Query Chain" + }, + "outputAnchors": [ + { + "name": "output", + "label": "Output", + "type": "options", + "options": [ + { + "id": "llmChain_0-output-llmChain-LLMChain|BaseChain|Runnable", + "name": "llmChain", + "label": "LLM Chain", + "type": "LLMChain | BaseChain | Runnable" + }, + { + "id": "llmChain_0-output-outputPrediction-string|json", + "name": "outputPrediction", + "label": "Output Prediction", + "type": "string | json" + } + ], + "default": "llmChain" + } + ], + "outputs": { + "output": "outputPrediction" + }, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 770.4559230968546, + "y": -127.11351409346554 + }, + "dragging": false + }, + { + "width": 300, + "height": 574, + "id": "chatOpenAI_0", + "position": { + "x": 372.72389181000057, + "y": -561.0744498265477 + }, + "type": "customNode", + "data": { + "id": "chatOpenAI_0", + "label": "ChatOpenAI", + "version": 2, + "name": "chatOpenAI", + "type": "ChatOpenAI", + "baseClasses": ["ChatOpenAI", "BaseChatModel", "BaseLanguageModel", "Runnable"], + "category": "Chat Models", + "description": "Wrapper around OpenAI large language models that use the Chat endpoint", + "inputParams": [ + { + "label": "Connect Credential", + "name": "credential", + "type": "credential", + "credentialNames": ["openAIApi"], + "id": "chatOpenAI_0-input-credential-credential" + }, + { + "label": "Model Name", + "name": "modelName", + "type": "options", + "options": [ + { + "label": "gpt-4", + "name": "gpt-4" + }, + { + "label": "gpt-4-1106-preview", + "name": "gpt-4-1106-preview" + }, + { + "label": "gpt-4-vision-preview", + "name": "gpt-4-vision-preview" + }, + { + "label": "gpt-4-0613", + "name": "gpt-4-0613" + }, + { + "label": "gpt-4-32k", + "name": "gpt-4-32k" + }, + { + "label": "gpt-4-32k-0613", + "name": "gpt-4-32k-0613" + }, + { + "label": "gpt-3.5-turbo", + "name": "gpt-3.5-turbo" + }, + { + "label": "gpt-3.5-turbo-1106", + "name": "gpt-3.5-turbo-1106" + }, + { + "label": "gpt-3.5-turbo-0613", + "name": "gpt-3.5-turbo-0613" + }, + { + "label": "gpt-3.5-turbo-16k", + "name": "gpt-3.5-turbo-16k" + }, + { + "label": "gpt-3.5-turbo-16k-0613", + "name": "gpt-3.5-turbo-16k-0613" + } + ], + "default": "gpt-3.5-turbo", + "optional": true, + "id": "chatOpenAI_0-input-modelName-options" + }, + { + "label": "Temperature", + "name": "temperature", + "type": "number", + "step": 0.1, + "default": 0.9, + "optional": true, + "id": "chatOpenAI_0-input-temperature-number" + }, + { + "label": "Max Tokens", + "name": "maxTokens", + "type": "number", + "step": 1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-maxTokens-number" + }, + { + "label": "Top Probability", + "name": "topP", + "type": "number", + "step": 0.1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-topP-number" + }, + { + "label": "Frequency Penalty", + "name": "frequencyPenalty", + "type": "number", + "step": 0.1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-frequencyPenalty-number" + }, + { + "label": "Presence Penalty", + "name": "presencePenalty", + "type": "number", + "step": 0.1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-presencePenalty-number" + }, + { + "label": "Timeout", + "name": "timeout", + "type": "number", + "step": 1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-timeout-number" + }, + { + "label": "BasePath", + "name": "basepath", + "type": "string", + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-basepath-string" + }, + { + "label": "BaseOptions", + "name": "baseOptions", + "type": "json", + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-baseOptions-json" + } + ], + "inputAnchors": [ + { + "label": "Cache", + "name": "cache", + "type": "BaseCache", + "optional": true, + "id": "chatOpenAI_0-input-cache-BaseCache" + } + ], + "inputs": { + "cache": "", + "modelName": "gpt-3.5-turbo-16k", + "temperature": "0", + "maxTokens": "", + "topP": "", + "frequencyPenalty": "", + "presencePenalty": "", + "timeout": "", + "basepath": "", + "baseOptions": "" + }, + "outputAnchors": [ + { + "id": "chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable", + "name": "chatOpenAI", + "label": "ChatOpenAI", + "type": "ChatOpenAI | BaseChatModel | BaseLanguageModel | Runnable" + } + ], + "outputs": {}, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 372.72389181000057, + "y": -561.0744498265477 + }, + "dragging": false + }, + { + "width": 300, + "height": 574, + "id": "chatOpenAI_1", + "position": { + "x": 2636.1598769864936, + "y": -653.0025971757484 + }, + "type": "customNode", + "data": { + "id": "chatOpenAI_1", + "label": "ChatOpenAI", + "version": 2, + "name": "chatOpenAI", + "type": "ChatOpenAI", + "baseClasses": ["ChatOpenAI", "BaseChatModel", "BaseLanguageModel", "Runnable"], + "category": "Chat Models", + "description": "Wrapper around OpenAI large language models that use the Chat endpoint", + "inputParams": [ + { + "label": "Connect Credential", + "name": "credential", + "type": "credential", + "credentialNames": ["openAIApi"], + "id": "chatOpenAI_1-input-credential-credential" + }, + { + "label": "Model Name", + "name": "modelName", + "type": "options", + "options": [ + { + "label": "gpt-4", + "name": "gpt-4" + }, + { + "label": "gpt-4-1106-preview", + "name": "gpt-4-1106-preview" + }, + { + "label": "gpt-4-vision-preview", + "name": "gpt-4-vision-preview" + }, + { + "label": "gpt-4-0613", + "name": "gpt-4-0613" + }, + { + "label": "gpt-4-32k", + "name": "gpt-4-32k" + }, + { + "label": "gpt-4-32k-0613", + "name": "gpt-4-32k-0613" + }, + { + "label": "gpt-3.5-turbo", + "name": "gpt-3.5-turbo" + }, + { + "label": "gpt-3.5-turbo-1106", + "name": "gpt-3.5-turbo-1106" + }, + { + "label": "gpt-3.5-turbo-0613", + "name": "gpt-3.5-turbo-0613" + }, + { + "label": "gpt-3.5-turbo-16k", + "name": "gpt-3.5-turbo-16k" + }, + { + "label": "gpt-3.5-turbo-16k-0613", + "name": "gpt-3.5-turbo-16k-0613" + } + ], + "default": "gpt-3.5-turbo", + "optional": true, + "id": "chatOpenAI_1-input-modelName-options" + }, + { + "label": "Temperature", + "name": "temperature", + "type": "number", + "step": 0.1, + "default": 0.9, + "optional": true, + "id": "chatOpenAI_1-input-temperature-number" + }, + { + "label": "Max Tokens", + "name": "maxTokens", + "type": "number", + "step": 1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_1-input-maxTokens-number" + }, + { + "label": "Top Probability", + "name": "topP", + "type": "number", + "step": 0.1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_1-input-topP-number" + }, + { + "label": "Frequency Penalty", + "name": "frequencyPenalty", + "type": "number", + "step": 0.1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_1-input-frequencyPenalty-number" + }, + { + "label": "Presence Penalty", + "name": "presencePenalty", + "type": "number", + "step": 0.1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_1-input-presencePenalty-number" + }, + { + "label": "Timeout", + "name": "timeout", + "type": "number", + "step": 1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_1-input-timeout-number" + }, + { + "label": "BasePath", + "name": "basepath", + "type": "string", + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_1-input-basepath-string" + }, + { + "label": "BaseOptions", + "name": "baseOptions", + "type": "json", + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_1-input-baseOptions-json" + } + ], + "inputAnchors": [ + { + "label": "Cache", + "name": "cache", + "type": "BaseCache", + "optional": true, + "id": "chatOpenAI_1-input-cache-BaseCache" + } + ], + "inputs": { + "cache": "", + "modelName": "gpt-3.5-turbo-16k", + "temperature": "0", + "maxTokens": "", + "topP": "", + "frequencyPenalty": "", + "presencePenalty": "", + "timeout": "", + "basepath": "", + "baseOptions": "" + }, + "outputAnchors": [ + { + "id": "chatOpenAI_1-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable", + "name": "chatOpenAI", + "label": "ChatOpenAI", + "type": "ChatOpenAI | BaseChatModel | BaseLanguageModel | Runnable" + } + ], + "outputs": {}, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 2636.1598769864936, + "y": -653.0025971757484 + }, + "dragging": false + }, + { + "width": 300, + "height": 507, + "id": "llmChain_1", + "position": { + "x": 3089.9937691022837, + "y": -109.24001734925716 + }, + "type": "customNode", + "data": { + "id": "llmChain_1", + "label": "LLM Chain", + "version": 3, + "name": "llmChain", + "type": "LLMChain", + "baseClasses": ["LLMChain", "BaseChain", "Runnable"], + "category": "Chains", + "description": "Chain to run queries against LLMs", + "inputParams": [ + { + "label": "Chain Name", + "name": "chainName", + "type": "string", + "placeholder": "Name Your Chain", + "optional": true, + "id": "llmChain_1-input-chainName-string" + } + ], + "inputAnchors": [ + { + "label": "Language Model", + "name": "model", + "type": "BaseLanguageModel", + "id": "llmChain_1-input-model-BaseLanguageModel" + }, + { + "label": "Prompt", + "name": "prompt", + "type": "BasePromptTemplate", + "id": "llmChain_1-input-prompt-BasePromptTemplate" + }, + { + "label": "Output Parser", + "name": "outputParser", + "type": "BaseLLMOutputParser", + "optional": true, + "id": "llmChain_1-input-outputParser-BaseLLMOutputParser" + }, + { + "label": "Input Moderation", + "description": "Detect text that could generate harmful output and prevent it from being sent to the language model", + "name": "inputModeration", + "type": "Moderation", + "optional": true, + "list": true, + "id": "llmChain_1-input-inputModeration-Moderation" + } + ], + "inputs": { + "model": "{{chatOpenAI_1.data.instance}}", + "prompt": "{{promptTemplate_1.data.instance}}", + "outputParser": "", + "inputModeration": "", + "chainName": "Final Chain" + }, + "outputAnchors": [ + { + "name": "output", + "label": "Output", + "type": "options", + "options": [ + { + "id": "llmChain_1-output-llmChain-LLMChain|BaseChain|Runnable", + "name": "llmChain", + "label": "LLM Chain", + "type": "LLMChain | BaseChain | Runnable" + }, + { + "id": "llmChain_1-output-outputPrediction-string|json", + "name": "outputPrediction", + "label": "Output Prediction", + "type": "string | json" + } + ], + "default": "llmChain" + } + ], + "outputs": { + "output": "llmChain" + }, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 3089.9937691022837, + "y": -109.24001734925716 + }, + "dragging": false + }, + { + "width": 300, + "height": 669, + "id": "customFunction_2", + "position": { + "x": -395.18079694059173, + "y": -222.8935573325382 + }, + "type": "customNode", + "data": { + "id": "customFunction_2", + "label": "Custom JS Function", + "version": 1, + "name": "customFunction", + "type": "CustomFunction", + "baseClasses": ["CustomFunction", "Utilities"], + "category": "Utilities", + "description": "Execute custom javascript function", + "inputParams": [ + { + "label": "Input Variables", + "name": "functionInputVariables", + "description": "Input variables can be used in the function with prefix $. For example: $var", + "type": "json", + "optional": true, + "acceptVariable": true, + "list": true, + "id": "customFunction_2-input-functionInputVariables-json" + }, + { + "label": "Function Name", + "name": "functionName", + "type": "string", + "placeholder": "My Function", + "id": "customFunction_2-input-functionName-string" + }, + { + "label": "Javascript Function", + "name": "javascriptFunction", + "type": "code", + "id": "customFunction_2-input-javascriptFunction-code" + } + ], + "inputAnchors": [], + "inputs": { + "functionInputVariables": "", + "functionName": "Get SQL Schema Prompt", + "javascriptFunction": "const HOST = 'singlestore-host.com';\nconst USER = 'admin';\nconst PASSWORD = 'mypassword';\nconst DATABASE = 'mydb';\nconst TABLE = 'samples';\nconst mysql = require('mysql2/promise');\n\nlet sqlSchemaPrompt;\n\n/**\n * Ideal prompt contains schema info and examples\n * Follows best practices as specified form https://arxiv.org/abs/2204.00498\n * =========================================\n * CREATE TABLE samples (firstName varchar NOT NULL, lastName varchar)\n * SELECT * FROM samples LIMIT 3\n * firstName lastName\n * Stephen Tyler\n * Jack McGinnis\n * Steven Repici\n * =========================================\n*/\nfunction getSQLPrompt() {\n return new Promise(async (resolve, reject) => {\n try {\n const singleStoreConnection = mysql.createPool({\n host: HOST,\n user: USER,\n password: PASSWORD,\n database: DATABASE,\n });\n \n // Get schema info\n const [schemaInfo] = await singleStoreConnection.execute(\n `SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = \"${TABLE}\"`\n );\n \n const createColumns = [];\n const columnNames = [];\n \n for (const schemaData of schemaInfo) {\n columnNames.push(`${schemaData['COLUMN_NAME']}`);\n createColumns.push(`${schemaData['COLUMN_NAME']} ${schemaData['COLUMN_TYPE']} ${schemaData['IS_NULLABLE'] === 'NO' ? 'NOT NULL' : ''}`);\n }\n \n const sqlCreateTableQuery = `CREATE TABLE samples (${createColumns.join(', ')})`;\n const sqlSelectTableQuery = `SELECT * FROM samples LIMIT 3`;\n \n // Get first 3 rows\n const [rows] = await singleStoreConnection.execute(\n sqlSelectTableQuery,\n );\n \n const allValues = [];\n for (const row of rows) {\n const rowValues = [];\n for (const colName in row) {\n rowValues.push(row[colName]);\n }\n allValues.push(rowValues.join(' '));\n }\n \n sqlSchemaPrompt = sqlCreateTableQuery + '\\n' + sqlSelectTableQuery + '\\n' + columnNames.join(' ') + '\\n' + allValues.join('\\n');\n \n resolve();\n } catch (e) {\n console.error(e);\n return reject(e);\n }\n });\n}\n\nasync function main() {\n await getSQLPrompt();\n}\n\nawait main();\n\nreturn sqlSchemaPrompt;" + }, + "outputAnchors": [ + { + "name": "output", + "label": "Output", + "type": "options", + "options": [ + { + "id": "customFunction_2-output-output-string|number|boolean|json|array", + "name": "output", + "label": "Output", + "type": "string | number | boolean | json | array" + } + ], + "default": "output" + } + ], + "outputs": { + "output": "output" + }, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": -395.18079694059173, + "y": -222.8935573325382 + }, + "dragging": false + }, + { + "width": 300, + "height": 669, + "id": "customFunction_1", + "position": { + "x": 1887.4670208331604, + "y": -275.95340782935716 + }, + "type": "customNode", + "data": { + "id": "customFunction_1", + "label": "Custom JS Function", + "version": 1, + "name": "customFunction", + "type": "CustomFunction", + "baseClasses": ["CustomFunction", "Utilities"], + "category": "Utilities", + "description": "Execute custom javascript function", + "inputParams": [ + { + "label": "Input Variables", + "name": "functionInputVariables", + "description": "Input variables can be used in the function with prefix $. For example: $var", + "type": "json", + "optional": true, + "acceptVariable": true, + "list": true, + "id": "customFunction_1-input-functionInputVariables-json" + }, + { + "label": "Function Name", + "name": "functionName", + "type": "string", + "placeholder": "My Function", + "id": "customFunction_1-input-functionName-string" + }, + { + "label": "Javascript Function", + "name": "javascriptFunction", + "type": "code", + "id": "customFunction_1-input-javascriptFunction-code" + } + ], + "inputAnchors": [], + "inputs": { + "functionInputVariables": "{\"sqlQuery\":\"{{setVariable_1.data.instance}}\"}", + "functionName": "Run SQL Query", + "javascriptFunction": "const HOST = 'singlestore-host.com';\nconst USER = 'admin';\nconst PASSWORD = 'mypassword';\nconst DATABASE = 'mydb';\nconst TABLE = 'samples';\nconst mysql = require('mysql2/promise');\n\nlet result;\n\nfunction getSQLResult() {\n return new Promise(async (resolve, reject) => {\n try {\n const singleStoreConnection = mysql.createPool({\n host: HOST,\n user: USER,\n password: PASSWORD,\n database: DATABASE,\n });\n \n const [rows] = await singleStoreConnection.execute(\n $sqlQuery\n );\n \n result = JSON.stringify(rows)\n \n resolve();\n } catch (e) {\n console.error(e);\n return reject(e);\n }\n });\n}\n\nasync function main() {\n await getSQLResult();\n}\n\nawait main();\n\nreturn result;" + }, + "outputAnchors": [ + { + "name": "output", + "label": "Output", + "type": "options", + "options": [ + { + "id": "customFunction_1-output-output-string|number|boolean|json|array", + "name": "output", + "label": "Output", + "type": "string | number | boolean | json | array" + } + ], + "default": "output" + } + ], + "outputs": { + "output": "output" + }, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 1887.4670208331604, + "y": -275.95340782935716 + }, + "dragging": false + }, + { + "width": 300, + "height": 511, + "id": "promptTemplate_1", + "position": { + "x": 2638.3935631956588, + "y": -18.55855423639423 + }, + "type": "customNode", + "data": { + "id": "promptTemplate_1", + "label": "Prompt Template", + "version": 1, + "name": "promptTemplate", + "type": "PromptTemplate", + "baseClasses": ["PromptTemplate", "BaseStringPromptTemplate", "BasePromptTemplate", "Runnable"], + "category": "Prompts", + "description": "Schema to represent a basic prompt for an LLM", + "inputParams": [ + { + "label": "Template", + "name": "template", + "type": "string", + "rows": 4, + "placeholder": "What is a good name for a company that makes {product}?", + "id": "promptTemplate_1-input-template-string" + }, + { + "label": "Format Prompt Values", + "name": "promptValues", + "type": "json", + "optional": true, + "acceptVariable": true, + "list": true, + "id": "promptTemplate_1-input-promptValues-json" + } + ], + "inputAnchors": [], + "inputs": { + "template": "Based on the table schema below, question, SQL query, and SQL response, write a natural language response, be details as possible:\n------------\nSCHEMA: {schema}\n------------\nQUESTION: {question}\n------------\nSQL QUERY: {sqlQuery}\n------------\nSQL RESPONSE: {sqlResponse}\n------------\nNATURAL LANGUAGE RESPONSE:", + "promptValues": "{\"schema\":\"{{getVariable_0.data.instance}}\",\"question\":\"{{question}}\",\"sqlResponse\":\"{{customFunction_1.data.instance}}\",\"sqlQuery\":\"{{getVariable_1.data.instance}}\"}" + }, + "outputAnchors": [ + { + "id": "promptTemplate_1-output-promptTemplate-PromptTemplate|BaseStringPromptTemplate|BasePromptTemplate|Runnable", + "name": "promptTemplate", + "label": "PromptTemplate", + "type": "PromptTemplate | BaseStringPromptTemplate | BasePromptTemplate | Runnable" + } + ], + "outputs": {}, + "selected": false + }, + "selected": false, + "dragging": false, + "positionAbsolute": { + "x": 2638.3935631956588, + "y": -18.55855423639423 + } + }, + { + "width": 300, + "height": 355, + "id": "setVariable_0", + "position": { + "x": 18.689175061831122, + "y": -62.81166351070223 + }, + "type": "customNode", + "data": { + "id": "setVariable_0", + "label": "Set Variable", + "version": 1, + "name": "setVariable", + "type": "SetVariable", + "baseClasses": ["SetVariable", "Utilities"], + "category": "Utilities", + "description": "Set variable which can be retrieved at a later stage. Variable is only available during runtime.", + "inputParams": [ + { + "label": "Variable Name", + "name": "variableName", + "type": "string", + "placeholder": "var1", + "id": "setVariable_0-input-variableName-string" + } + ], + "inputAnchors": [ + { + "label": "Input", + "name": "input", + "type": "string | number | boolean | json | array", + "optional": true, + "list": true, + "id": "setVariable_0-input-input-string | number | boolean | json | array" + } + ], + "inputs": { + "input": ["{{customFunction_2.data.instance}}"], + "variableName": "schemaPrompt" + }, + "outputAnchors": [ + { + "name": "output", + "label": "Output", + "type": "options", + "options": [ + { + "id": "setVariable_0-output-output-string|number|boolean|json|array", + "name": "output", + "label": "Output", + "type": "string | number | boolean | json | array" + } + ], + "default": "output" + } + ], + "outputs": { + "output": "output" + }, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 18.689175061831122, + "y": -62.81166351070223 + }, + "dragging": false + }, + { + "width": 300, + "height": 304, + "id": "getVariable_0", + "position": { + "x": 2248.4540716891547, + "y": -47.21232652005119 + }, + "type": "customNode", + "data": { + "id": "getVariable_0", + "label": "Get Variable", + "version": 1, + "name": "getVariable", + "type": "GetVariable", + "baseClasses": ["GetVariable", "Utilities"], + "category": "Utilities", + "description": "Get variable that was saved using Set Variable node", + "inputParams": [ + { + "label": "Variable Name", + "name": "variableName", + "type": "string", + "placeholder": "var1", + "id": "getVariable_0-input-variableName-string" + } + ], + "inputAnchors": [], + "inputs": { + "variableName": "schemaPrompt" + }, + "outputAnchors": [ + { + "name": "output", + "label": "Output", + "type": "options", + "options": [ + { + "id": "getVariable_0-output-output-string|number|boolean|json|array", + "name": "output", + "label": "Output", + "type": "string | number | boolean | json | array" + } + ], + "default": "output" + } + ], + "outputs": { + "output": "output" + }, + "selected": false + }, + "positionAbsolute": { + "x": 2248.4540716891547, + "y": -47.21232652005119 + }, + "selected": false, + "dragging": false + }, + { + "width": 300, + "height": 304, + "id": "getVariable_1", + "position": { + "x": 2256.0258940322105, + "y": 437.4363694364632 + }, + "type": "customNode", + "data": { + "id": "getVariable_1", + "label": "Get Variable", + "version": 1, + "name": "getVariable", + "type": "GetVariable", + "baseClasses": ["GetVariable", "Utilities"], + "category": "Utilities", + "description": "Get variable that was saved using Set Variable node", + "inputParams": [ + { + "label": "Variable Name", + "name": "variableName", + "type": "string", + "placeholder": "var1", + "id": "getVariable_1-input-variableName-string" + } + ], + "inputAnchors": [], + "inputs": { + "variableName": "sqlQuery" + }, + "outputAnchors": [ + { + "name": "output", + "label": "Output", + "type": "options", + "options": [ + { + "id": "getVariable_1-output-output-string|number|boolean|json|array", + "name": "output", + "label": "Output", + "type": "string | number | boolean | json | array" + } + ], + "default": "output" + } + ], + "outputs": { + "output": "output" + }, + "selected": false + }, + "positionAbsolute": { + "x": 2256.0258940322105, + "y": 437.4363694364632 + }, + "selected": false, + "dragging": false + }, + { + "width": 300, + "height": 355, + "id": "setVariable_1", + "position": { + "x": 1516.338224315744, + "y": -133.6986023683283 + }, + "type": "customNode", + "data": { + "id": "setVariable_1", + "label": "Set Variable", + "version": 1, + "name": "setVariable", + "type": "SetVariable", + "baseClasses": ["SetVariable", "Utilities"], + "category": "Utilities", + "description": "Set variable which can be retrieved at a later stage. Variable is only available during runtime.", + "inputParams": [ + { + "label": "Variable Name", + "name": "variableName", + "type": "string", + "placeholder": "var1", + "id": "setVariable_1-input-variableName-string" + } + ], + "inputAnchors": [ + { + "label": "Input", + "name": "input", + "type": "string | number | boolean | json | array", + "optional": true, + "list": true, + "id": "setVariable_1-input-input-string | number | boolean | json | array" + } + ], + "inputs": { + "input": ["{{ifElseFunction_0.data.instance}}"], + "variableName": "sqlQuery" + }, + "outputAnchors": [ + { + "name": "output", + "label": "Output", + "type": "options", + "options": [ + { + "id": "setVariable_1-output-output-string|number|boolean|json|array", + "name": "output", + "label": "Output", + "type": "string | number | boolean | json | array" + } + ], + "default": "output" + } + ], + "outputs": { + "output": "output" + }, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 1516.338224315744, + "y": -133.6986023683283 + }, + "dragging": false + }, + { + "width": 300, + "height": 755, + "id": "ifElseFunction_0", + "position": { + "x": 1147.8020838770517, + "y": -237.39478763322148 + }, + "type": "customNode", + "data": { + "id": "ifElseFunction_0", + "label": "IfElse Function", + "version": 1, + "name": "ifElseFunction", + "type": "IfElseFunction", + "baseClasses": ["IfElseFunction", "Utilities"], + "category": "Utilities", + "description": "Split flows based on If Else javascript functions", + "inputParams": [ + { + "label": "Input Variables", + "name": "functionInputVariables", + "description": "Input variables can be used in the function with prefix $. For example: $var", + "type": "json", + "optional": true, + "acceptVariable": true, + "list": true, + "id": "ifElseFunction_0-input-functionInputVariables-json" + }, + { + "label": "IfElse Name", + "name": "functionName", + "type": "string", + "optional": true, + "placeholder": "If Condition Match", + "id": "ifElseFunction_0-input-functionName-string" + }, + { + "label": "If Function", + "name": "ifFunction", + "description": "Function must return a value", + "type": "code", + "rows": 2, + "default": "if (\"hello\" == \"hello\") {\n return true;\n}", + "id": "ifElseFunction_0-input-ifFunction-code" + }, + { + "label": "Else Function", + "name": "elseFunction", + "description": "Function must return a value", + "type": "code", + "rows": 2, + "default": "return false;", + "id": "ifElseFunction_0-input-elseFunction-code" + } + ], + "inputAnchors": [], + "inputs": { + "functionInputVariables": "{\"sqlQuery\":\"{{llmChain_0.data.instance}}\"}", + "functionName": "IF SQL Query contains SELECT and WHERE", + "ifFunction": "const sqlQuery = $sqlQuery.trim();\n\nif (sqlQuery.includes(\"SELECT\") && sqlQuery.includes(\"WHERE\")) {\n return sqlQuery;\n}", + "elseFunction": "return $sqlQuery;" + }, + "outputAnchors": [ + { + "name": "output", + "label": "Output", + "type": "options", + "options": [ + { + "id": "ifElseFunction_0-output-returnTrue-string|number|boolean|json|array", + "name": "returnTrue", + "label": "True", + "type": "string | number | boolean | json | array" + }, + { + "id": "ifElseFunction_0-output-returnFalse-string|number|boolean|json|array", + "name": "returnFalse", + "label": "False", + "type": "string | number | boolean | json | array" + } + ], + "default": "returnTrue" + } + ], + "outputs": { + "output": "returnTrue" + }, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 1147.8020838770517, + "y": -237.39478763322148 + }, + "dragging": false + }, + { + "width": 300, + "height": 511, + "id": "promptTemplate_2", + "position": { + "x": 1530.0647779039386, + "y": 944.9904482583751 + }, + "type": "customNode", + "data": { + "id": "promptTemplate_2", + "label": "Prompt Template", + "version": 1, + "name": "promptTemplate", + "type": "PromptTemplate", + "baseClasses": ["PromptTemplate", "BaseStringPromptTemplate", "BasePromptTemplate", "Runnable"], + "category": "Prompts", + "description": "Schema to represent a basic prompt for an LLM", + "inputParams": [ + { + "label": "Template", + "name": "template", + "type": "string", + "rows": 4, + "placeholder": "What is a good name for a company that makes {product}?", + "id": "promptTemplate_2-input-template-string" + }, + { + "label": "Format Prompt Values", + "name": "promptValues", + "type": "json", + "optional": true, + "acceptVariable": true, + "list": true, + "id": "promptTemplate_2-input-promptValues-json" + } + ], + "inputAnchors": [], + "inputs": { + "template": "Politely say \"I'm not able to answer query\"", + "promptValues": "{\"schema\":\"{{setVariable_0.data.instance}}\",\"question\":\"{{question}}\"}" + }, + "outputAnchors": [ + { + "id": "promptTemplate_2-output-promptTemplate-PromptTemplate|BaseStringPromptTemplate|BasePromptTemplate|Runnable", + "name": "promptTemplate", + "label": "PromptTemplate", + "type": "PromptTemplate | BaseStringPromptTemplate | BasePromptTemplate | Runnable" + } + ], + "outputs": {}, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 1530.0647779039386, + "y": 944.9904482583751 + }, + "dragging": false + }, + { + "width": 300, + "height": 574, + "id": "chatOpenAI_2", + "position": { + "x": 1537.0307928738125, + "y": 330.7727229610632 + }, + "type": "customNode", + "data": { + "id": "chatOpenAI_2", + "label": "ChatOpenAI", + "version": 2, + "name": "chatOpenAI", + "type": "ChatOpenAI", + "baseClasses": ["ChatOpenAI", "BaseChatModel", "BaseLanguageModel", "Runnable"], + "category": "Chat Models", + "description": "Wrapper around OpenAI large language models that use the Chat endpoint", + "inputParams": [ + { + "label": "Connect Credential", + "name": "credential", + "type": "credential", + "credentialNames": ["openAIApi"], + "id": "chatOpenAI_2-input-credential-credential" + }, + { + "label": "Model Name", + "name": "modelName", + "type": "options", + "options": [ + { + "label": "gpt-4", + "name": "gpt-4" + }, + { + "label": "gpt-4-1106-preview", + "name": "gpt-4-1106-preview" + }, + { + "label": "gpt-4-vision-preview", + "name": "gpt-4-vision-preview" + }, + { + "label": "gpt-4-0613", + "name": "gpt-4-0613" + }, + { + "label": "gpt-4-32k", + "name": "gpt-4-32k" + }, + { + "label": "gpt-4-32k-0613", + "name": "gpt-4-32k-0613" + }, + { + "label": "gpt-3.5-turbo", + "name": "gpt-3.5-turbo" + }, + { + "label": "gpt-3.5-turbo-1106", + "name": "gpt-3.5-turbo-1106" + }, + { + "label": "gpt-3.5-turbo-0613", + "name": "gpt-3.5-turbo-0613" + }, + { + "label": "gpt-3.5-turbo-16k", + "name": "gpt-3.5-turbo-16k" + }, + { + "label": "gpt-3.5-turbo-16k-0613", + "name": "gpt-3.5-turbo-16k-0613" + } + ], + "default": "gpt-3.5-turbo", + "optional": true, + "id": "chatOpenAI_2-input-modelName-options" + }, + { + "label": "Temperature", + "name": "temperature", + "type": "number", + "step": 0.1, + "default": 0.9, + "optional": true, + "id": "chatOpenAI_2-input-temperature-number" + }, + { + "label": "Max Tokens", + "name": "maxTokens", + "type": "number", + "step": 1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_2-input-maxTokens-number" + }, + { + "label": "Top Probability", + "name": "topP", + "type": "number", + "step": 0.1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_2-input-topP-number" + }, + { + "label": "Frequency Penalty", + "name": "frequencyPenalty", + "type": "number", + "step": 0.1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_2-input-frequencyPenalty-number" + }, + { + "label": "Presence Penalty", + "name": "presencePenalty", + "type": "number", + "step": 0.1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_2-input-presencePenalty-number" + }, + { + "label": "Timeout", + "name": "timeout", + "type": "number", + "step": 1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_2-input-timeout-number" + }, + { + "label": "BasePath", + "name": "basepath", + "type": "string", + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_2-input-basepath-string" + }, + { + "label": "BaseOptions", + "name": "baseOptions", + "type": "json", + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_2-input-baseOptions-json" + } + ], + "inputAnchors": [ + { + "label": "Cache", + "name": "cache", + "type": "BaseCache", + "optional": true, + "id": "chatOpenAI_2-input-cache-BaseCache" + } + ], + "inputs": { + "cache": "", + "modelName": "gpt-3.5-turbo-16k", + "temperature": "0.7", + "maxTokens": "", + "topP": "", + "frequencyPenalty": "", + "presencePenalty": "", + "timeout": "", + "basepath": "", + "baseOptions": "" + }, + "outputAnchors": [ + { + "id": "chatOpenAI_2-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable", + "name": "chatOpenAI", + "label": "ChatOpenAI", + "type": "ChatOpenAI | BaseChatModel | BaseLanguageModel | Runnable" + } + ], + "outputs": {}, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 1537.0307928738125, + "y": 330.7727229610632 + }, + "dragging": false + }, + { + "width": 300, + "height": 507, + "id": "llmChain_2", + "position": { + "x": 2077.2866807477812, + "y": 958.6594167386253 + }, + "type": "customNode", + "data": { + "id": "llmChain_2", + "label": "LLM Chain", + "version": 3, + "name": "llmChain", + "type": "LLMChain", + "baseClasses": ["LLMChain", "BaseChain", "Runnable"], + "category": "Chains", + "description": "Chain to run queries against LLMs", + "inputParams": [ + { + "label": "Chain Name", + "name": "chainName", + "type": "string", + "placeholder": "Name Your Chain", + "optional": true, + "id": "llmChain_2-input-chainName-string" + } + ], + "inputAnchors": [ + { + "label": "Language Model", + "name": "model", + "type": "BaseLanguageModel", + "id": "llmChain_2-input-model-BaseLanguageModel" + }, + { + "label": "Prompt", + "name": "prompt", + "type": "BasePromptTemplate", + "id": "llmChain_2-input-prompt-BasePromptTemplate" + }, + { + "label": "Output Parser", + "name": "outputParser", + "type": "BaseLLMOutputParser", + "optional": true, + "id": "llmChain_2-input-outputParser-BaseLLMOutputParser" + }, + { + "label": "Input Moderation", + "description": "Detect text that could generate harmful output and prevent it from being sent to the language model", + "name": "inputModeration", + "type": "Moderation", + "optional": true, + "list": true, + "id": "llmChain_2-input-inputModeration-Moderation" + } + ], + "inputs": { + "model": "{{chatOpenAI_2.data.instance}}", + "prompt": "{{promptTemplate_2.data.instance}}", + "outputParser": "", + "inputModeration": "", + "chainName": "Fallback Chain" + }, + "outputAnchors": [ + { + "name": "output", + "label": "Output", + "type": "options", + "options": [ + { + "id": "llmChain_2-output-llmChain-LLMChain|BaseChain|Runnable", + "name": "llmChain", + "label": "LLM Chain", + "type": "LLMChain | BaseChain | Runnable" + }, + { + "id": "llmChain_2-output-outputPrediction-string|json", + "name": "outputPrediction", + "label": "Output Prediction", + "type": "string | json" + } + ], + "default": "llmChain" + } + ], + "outputs": { + "output": "llmChain" + }, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 2077.2866807477812, + "y": 958.6594167386253 + }, + "dragging": false + } + ], + "edges": [ + { + "source": "promptTemplate_0", + "sourceHandle": "promptTemplate_0-output-promptTemplate-PromptTemplate|BaseStringPromptTemplate|BasePromptTemplate|Runnable", + "target": "llmChain_0", + "targetHandle": "llmChain_0-input-prompt-BasePromptTemplate", + "type": "buttonedge", + "id": "promptTemplate_0-promptTemplate_0-output-promptTemplate-PromptTemplate|BaseStringPromptTemplate|BasePromptTemplate|Runnable-llmChain_0-llmChain_0-input-prompt-BasePromptTemplate", + "data": { + "label": "" + } + }, + { + "source": "chatOpenAI_0", + "sourceHandle": "chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable", + "target": "llmChain_0", + "targetHandle": "llmChain_0-input-model-BaseLanguageModel", + "type": "buttonedge", + "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable-llmChain_0-llmChain_0-input-model-BaseLanguageModel", + "data": { + "label": "" + } + }, + { + "source": "chatOpenAI_1", + "sourceHandle": "chatOpenAI_1-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable", + "target": "llmChain_1", + "targetHandle": "llmChain_1-input-model-BaseLanguageModel", + "type": "buttonedge", + "id": "chatOpenAI_1-chatOpenAI_1-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable-llmChain_1-llmChain_1-input-model-BaseLanguageModel", + "data": { + "label": "" + } + }, + { + "source": "customFunction_1", + "sourceHandle": "customFunction_1-output-output-string|number|boolean|json|array", + "target": "promptTemplate_1", + "targetHandle": "promptTemplate_1-input-promptValues-json", + "type": "buttonedge", + "id": "customFunction_1-customFunction_1-output-output-string|number|boolean|json|array-promptTemplate_1-promptTemplate_1-input-promptValues-json", + "data": { + "label": "" + } + }, + { + "source": "promptTemplate_1", + "sourceHandle": "promptTemplate_1-output-promptTemplate-PromptTemplate|BaseStringPromptTemplate|BasePromptTemplate|Runnable", + "target": "llmChain_1", + "targetHandle": "llmChain_1-input-prompt-BasePromptTemplate", + "type": "buttonedge", + "id": "promptTemplate_1-promptTemplate_1-output-promptTemplate-PromptTemplate|BaseStringPromptTemplate|BasePromptTemplate|Runnable-llmChain_1-llmChain_1-input-prompt-BasePromptTemplate", + "data": { + "label": "" + } + }, + { + "source": "customFunction_2", + "sourceHandle": "customFunction_2-output-output-string|number|boolean|json|array", + "target": "setVariable_0", + "targetHandle": "setVariable_0-input-input-string | number | boolean | json | array", + "type": "buttonedge", + "id": "customFunction_2-customFunction_2-output-output-string|number|boolean|json|array-setVariable_0-setVariable_0-input-input-string | number | boolean | json | array", + "data": { + "label": "" + } + }, + { + "source": "setVariable_0", + "sourceHandle": "setVariable_0-output-output-string|number|boolean|json|array", + "target": "promptTemplate_0", + "targetHandle": "promptTemplate_0-input-promptValues-json", + "type": "buttonedge", + "id": "setVariable_0-setVariable_0-output-output-string|number|boolean|json|array-promptTemplate_0-promptTemplate_0-input-promptValues-json", + "data": { + "label": "" + } + }, + { + "source": "getVariable_0", + "sourceHandle": "getVariable_0-output-output-string|number|boolean|json|array", + "target": "promptTemplate_1", + "targetHandle": "promptTemplate_1-input-promptValues-json", + "type": "buttonedge", + "id": "getVariable_0-getVariable_0-output-output-string|number|boolean|json|array-promptTemplate_1-promptTemplate_1-input-promptValues-json", + "data": { + "label": "" + } + }, + { + "source": "getVariable_1", + "sourceHandle": "getVariable_1-output-output-string|number|boolean|json|array", + "target": "promptTemplate_1", + "targetHandle": "promptTemplate_1-input-promptValues-json", + "type": "buttonedge", + "id": "getVariable_1-getVariable_1-output-output-string|number|boolean|json|array-promptTemplate_1-promptTemplate_1-input-promptValues-json", + "data": { + "label": "" + } + }, + { + "source": "setVariable_1", + "sourceHandle": "setVariable_1-output-output-string|number|boolean|json|array", + "target": "customFunction_1", + "targetHandle": "customFunction_1-input-functionInputVariables-json", + "type": "buttonedge", + "id": "setVariable_1-setVariable_1-output-output-string|number|boolean|json|array-customFunction_1-customFunction_1-input-functionInputVariables-json", + "data": { + "label": "" + } + }, + { + "source": "llmChain_0", + "sourceHandle": "llmChain_0-output-outputPrediction-string|json", + "target": "ifElseFunction_0", + "targetHandle": "ifElseFunction_0-input-functionInputVariables-json", + "type": "buttonedge", + "id": "llmChain_0-llmChain_0-output-outputPrediction-string|json-ifElseFunction_0-ifElseFunction_0-input-functionInputVariables-json" + }, + { + "source": "ifElseFunction_0", + "sourceHandle": "ifElseFunction_0-output-returnTrue-string|number|boolean|json|array", + "target": "setVariable_1", + "targetHandle": "setVariable_1-input-input-string | number | boolean | json | array", + "type": "buttonedge", + "id": "ifElseFunction_0-ifElseFunction_0-output-returnTrue-string|number|boolean|json|array-setVariable_1-setVariable_1-input-input-string | number | boolean | json | array" + }, + { + "source": "ifElseFunction_0", + "sourceHandle": "ifElseFunction_0-output-returnFalse-string|number|boolean|json|array", + "target": "promptTemplate_2", + "targetHandle": "promptTemplate_2-input-promptValues-json", + "type": "buttonedge", + "id": "ifElseFunction_0-ifElseFunction_0-output-returnFalse-string|number|boolean|json|array-promptTemplate_2-promptTemplate_2-input-promptValues-json" + }, + { + "source": "chatOpenAI_2", + "sourceHandle": "chatOpenAI_2-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable", + "target": "llmChain_2", + "targetHandle": "llmChain_2-input-model-BaseLanguageModel", + "type": "buttonedge", + "id": "chatOpenAI_2-chatOpenAI_2-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable-llmChain_2-llmChain_2-input-model-BaseLanguageModel" + }, + { + "source": "promptTemplate_2", + "sourceHandle": "promptTemplate_2-output-promptTemplate-PromptTemplate|BaseStringPromptTemplate|BasePromptTemplate|Runnable", + "target": "llmChain_2", + "targetHandle": "llmChain_2-input-prompt-BasePromptTemplate", + "type": "buttonedge", + "id": "promptTemplate_2-promptTemplate_2-output-promptTemplate-PromptTemplate|BaseStringPromptTemplate|BasePromptTemplate|Runnable-llmChain_2-llmChain_2-input-prompt-BasePromptTemplate" + } + ] +} diff --git a/packages/server/marketplaces/chatflows/Simple Chat Engine.json b/packages/server/marketplaces/chatflows/Simple Chat Engine.json index b3854a51..630b6833 100644 --- a/packages/server/marketplaces/chatflows/Simple Chat Engine.json +++ b/packages/server/marketplaces/chatflows/Simple Chat Engine.json @@ -144,7 +144,7 @@ "baseClasses": ["AzureChatOpenAI", "BaseChatModel_LlamaIndex"], "tags": ["LlamaIndex"], "category": "Chat Models", - "description": "Wrapper around Azure OpenAI Chat LLM with LlamaIndex implementation", + "description": "Wrapper around Azure OpenAI Chat LLM specific for LlamaIndex", "inputParams": [ { "label": "Connect Credential", diff --git a/packages/server/marketplaces/chatflows/Simple Conversation Chain.json b/packages/server/marketplaces/chatflows/Simple Conversation Chain.json index 2dac3823..9689bf8c 100644 --- a/packages/server/marketplaces/chatflows/Simple Conversation Chain.json +++ b/packages/server/marketplaces/chatflows/Simple Conversation Chain.json @@ -2,20 +2,210 @@ "description": "Basic example of Conversation Chain with built-in memory - works exactly like ChatGPT", "badge": "POPULAR", "nodes": [ + { + "width": 300, + "height": 574, + "id": "chatOpenAI_0", + "position": { + "x": 579.0877964395976, + "y": -138.68792413227874 + }, + "type": "customNode", + "data": { + "id": "chatOpenAI_0", + "label": "ChatOpenAI", + "version": 2, + "name": "chatOpenAI", + "type": "ChatOpenAI", + "baseClasses": ["ChatOpenAI", "BaseChatModel", "BaseLanguageModel", "Runnable"], + "category": "Chat Models", + "description": "Wrapper around OpenAI large language models that use the Chat endpoint", + "inputParams": [ + { + "label": "Connect Credential", + "name": "credential", + "type": "credential", + "credentialNames": ["openAIApi"], + "id": "chatOpenAI_0-input-credential-credential" + }, + { + "label": "Model Name", + "name": "modelName", + "type": "options", + "options": [ + { + "label": "gpt-4", + "name": "gpt-4" + }, + { + "label": "gpt-4-1106-preview", + "name": "gpt-4-1106-preview" + }, + { + "label": "gpt-4-vision-preview", + "name": "gpt-4-vision-preview" + }, + { + "label": "gpt-4-0613", + "name": "gpt-4-0613" + }, + { + "label": "gpt-4-32k", + "name": "gpt-4-32k" + }, + { + "label": "gpt-4-32k-0613", + "name": "gpt-4-32k-0613" + }, + { + "label": "gpt-3.5-turbo", + "name": "gpt-3.5-turbo" + }, + { + "label": "gpt-3.5-turbo-1106", + "name": "gpt-3.5-turbo-1106" + }, + { + "label": "gpt-3.5-turbo-0613", + "name": "gpt-3.5-turbo-0613" + }, + { + "label": "gpt-3.5-turbo-16k", + "name": "gpt-3.5-turbo-16k" + }, + { + "label": "gpt-3.5-turbo-16k-0613", + "name": "gpt-3.5-turbo-16k-0613" + } + ], + "default": "gpt-3.5-turbo", + "optional": true, + "id": "chatOpenAI_0-input-modelName-options" + }, + { + "label": "Temperature", + "name": "temperature", + "type": "number", + "step": 0.1, + "default": 0.9, + "optional": true, + "id": "chatOpenAI_0-input-temperature-number" + }, + { + "label": "Max Tokens", + "name": "maxTokens", + "type": "number", + "step": 1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-maxTokens-number" + }, + { + "label": "Top Probability", + "name": "topP", + "type": "number", + "step": 0.1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-topP-number" + }, + { + "label": "Frequency Penalty", + "name": "frequencyPenalty", + "type": "number", + "step": 0.1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-frequencyPenalty-number" + }, + { + "label": "Presence Penalty", + "name": "presencePenalty", + "type": "number", + "step": 0.1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-presencePenalty-number" + }, + { + "label": "Timeout", + "name": "timeout", + "type": "number", + "step": 1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-timeout-number" + }, + { + "label": "BasePath", + "name": "basepath", + "type": "string", + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-basepath-string" + }, + { + "label": "BaseOptions", + "name": "baseOptions", + "type": "json", + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-baseOptions-json" + } + ], + "inputAnchors": [ + { + "label": "Cache", + "name": "cache", + "type": "BaseCache", + "optional": true, + "id": "chatOpenAI_0-input-cache-BaseCache" + } + ], + "inputs": { + "cache": "", + "modelName": "gpt-3.5-turbo-16k", + "temperature": 0.9, + "maxTokens": "", + "topP": "", + "frequencyPenalty": "", + "presencePenalty": "", + "timeout": "", + "basepath": "", + "baseOptions": "" + }, + "outputAnchors": [ + { + "id": "chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable", + "name": "chatOpenAI", + "label": "ChatOpenAI", + "type": "ChatOpenAI | BaseChatModel | BaseLanguageModel | Runnable" + } + ], + "outputs": {}, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 579.0877964395976, + "y": -138.68792413227874 + }, + "dragging": false + }, { "width": 300, "height": 376, "id": "bufferMemory_0", "position": { - "x": 753.4300788823234, - "y": 479.5336426526603 + "x": 220.30240896145915, + "y": 351.61324070296877 }, "type": "customNode", "data": { "id": "bufferMemory_0", "label": "Buffer Memory", - "name": "bufferMemory", "version": 1, + "name": "bufferMemory", "type": "BufferMemory", "baseClasses": ["BufferMemory", "BaseChatMemory", "BaseMemory"], "category": "Memory", @@ -54,179 +244,8 @@ }, "selected": false, "positionAbsolute": { - "x": 753.4300788823234, - "y": 479.5336426526603 - }, - "dragging": false - }, - { - "width": 300, - "height": 523, - "id": "chatOpenAI_0", - "position": { - "x": 754.8942497823595, - "y": -140 - }, - "type": "customNode", - "data": { - "id": "chatOpenAI_0", - "label": "ChatOpenAI", - "name": "chatOpenAI", - "version": 2, - "type": "ChatOpenAI", - "baseClasses": ["ChatOpenAI", "BaseChatModel", "BaseLanguageModel"], - "category": "Chat Models", - "description": "Wrapper around OpenAI large language models that use the Chat endpoint", - "inputParams": [ - { - "label": "Connect Credential", - "name": "credential", - "type": "credential", - "credentialNames": ["openAIApi"], - "id": "chatOpenAI_0-input-credential-credential" - }, - { - "label": "Model Name", - "name": "modelName", - "type": "options", - "options": [ - { - "label": "gpt-4", - "name": "gpt-4" - }, - { - "label": "gpt-4-0613", - "name": "gpt-4-0613" - }, - { - "label": "gpt-4-32k", - "name": "gpt-4-32k" - }, - { - "label": "gpt-4-32k-0613", - "name": "gpt-4-32k-0613" - }, - { - "label": "gpt-3.5-turbo", - "name": "gpt-3.5-turbo" - }, - { - "label": "gpt-3.5-turbo-0613", - "name": "gpt-3.5-turbo-0613" - }, - { - "label": "gpt-3.5-turbo-16k", - "name": "gpt-3.5-turbo-16k" - }, - { - "label": "gpt-3.5-turbo-16k-0613", - "name": "gpt-3.5-turbo-16k-0613" - } - ], - "default": "gpt-3.5-turbo", - "optional": true, - "id": "chatOpenAI_0-input-modelName-options" - }, - { - "label": "Temperature", - "name": "temperature", - "type": "number", - "default": 0.9, - "optional": true, - "id": "chatOpenAI_0-input-temperature-number" - }, - { - "label": "Max Tokens", - "name": "maxTokens", - "type": "number", - "optional": true, - "additionalParams": true, - "id": "chatOpenAI_0-input-maxTokens-number" - }, - { - "label": "Top Probability", - "name": "topP", - "type": "number", - "optional": true, - "additionalParams": true, - "id": "chatOpenAI_0-input-topP-number" - }, - { - "label": "Frequency Penalty", - "name": "frequencyPenalty", - "type": "number", - "optional": true, - "additionalParams": true, - "id": "chatOpenAI_0-input-frequencyPenalty-number" - }, - { - "label": "Presence Penalty", - "name": "presencePenalty", - "type": "number", - "optional": true, - "additionalParams": true, - "id": "chatOpenAI_0-input-presencePenalty-number" - }, - { - "label": "Timeout", - "name": "timeout", - "type": "number", - "optional": true, - "additionalParams": true, - "id": "chatOpenAI_0-input-timeout-number" - }, - { - "label": "BasePath", - "name": "basepath", - "type": "string", - "optional": true, - "additionalParams": true, - "id": "chatOpenAI_0-input-basepath-string" - }, - { - "label": "BaseOptions", - "name": "baseOptions", - "type": "json", - "optional": true, - "additionalParams": true, - "id": "chatOpenAI_0-input-baseOptions-json" - } - ], - "inputAnchors": [ - { - "label": "Cache", - "name": "cache", - "type": "BaseCache", - "optional": true, - "id": "chatOpenAI_0-input-cache-BaseCache" - } - ], - "inputs": { - "modelName": "gpt-3.5-turbo", - "temperature": 0.9, - "maxTokens": "", - "topP": "", - "frequencyPenalty": "", - "presencePenalty": "", - "timeout": "", - "basepath": "", - "baseOptions": "" - }, - "outputAnchors": [ - { - "id": "chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel", - "name": "chatOpenAI", - "label": "ChatOpenAI", - "type": "ChatOpenAI | BaseChatModel | BaseLanguageModel" - } - ], - "outputs": {}, - "selected": false - }, - "selected": false, - "positionAbsolute": { - "x": 754.8942497823595, - "y": -140 + "x": 220.30240896145915, + "y": 351.61324070296877 }, "dragging": false }, @@ -235,17 +254,17 @@ "height": 383, "id": "conversationChain_0", "position": { - "x": 1174.6496397666272, - "y": 311.1052536740497 + "x": 958.9887390513221, + "y": 318.8734467468765 }, "type": "customNode", "data": { "id": "conversationChain_0", "label": "Conversation Chain", + "version": 2, "name": "conversationChain", - "version": 1, "type": "ConversationChain", - "baseClasses": ["ConversationChain", "LLMChain", "BaseChain"], + "baseClasses": ["ConversationChain", "LLMChain", "BaseChain", "Runnable"], "category": "Chains", "description": "Chat models specific conversational chain with memory", "inputParams": [ @@ -254,15 +273,17 @@ "name": "systemMessagePrompt", "type": "string", "rows": 4, + "description": "If Chat Prompt Template is provided, this will be ignored", "additionalParams": true, "optional": true, - "placeholder": "You are a helpful assistant that write codes", + "default": "The following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.", + "placeholder": "The following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.", "id": "conversationChain_0-input-systemMessagePrompt-string" } ], "inputAnchors": [ { - "label": "Language Model", + "label": "Chat Model", "name": "model", "type": "BaseChatModel", "id": "conversationChain_0-input-model-BaseChatModel" @@ -274,27 +295,26 @@ "id": "conversationChain_0-input-memory-BaseMemory" }, { - "label": "Document", - "name": "document", - "type": "Document", - "description": "Include whole document into the context window", + "label": "Chat Prompt Template", + "name": "chatPromptTemplate", + "type": "ChatPromptTemplate", + "description": "Override existing prompt with Chat Prompt Template. Human Message must includes {input} variable", "optional": true, - "list": true, - "id": "conversationChain_0-input-document-Document" + "id": "conversationChain_0-input-chatPromptTemplate-ChatPromptTemplate" } ], "inputs": { "model": "{{chatOpenAI_0.data.instance}}", "memory": "{{bufferMemory_0.data.instance}}", - "document": "", - "systemMessagePrompt": "" + "chatPromptTemplate": "", + "systemMessagePrompt": "The following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know." }, "outputAnchors": [ { - "id": "conversationChain_0-output-conversationChain-ConversationChain|LLMChain|BaseChain", + "id": "conversationChain_0-output-conversationChain-ConversationChain|LLMChain|BaseChain|Runnable", "name": "conversationChain", "label": "ConversationChain", - "type": "ConversationChain | LLMChain | BaseChain" + "type": "ConversationChain | LLMChain | BaseChain | Runnable" } ], "outputs": {}, @@ -302,8 +322,8 @@ }, "selected": false, "positionAbsolute": { - "x": 1174.6496397666272, - "y": 311.1052536740497 + "x": 958.9887390513221, + "y": 318.8734467468765 }, "dragging": false } @@ -311,14 +331,11 @@ "edges": [ { "source": "chatOpenAI_0", - "sourceHandle": "chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel", + "sourceHandle": "chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable", "target": "conversationChain_0", "targetHandle": "conversationChain_0-input-model-BaseChatModel", "type": "buttonedge", - "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel-conversationChain_0-conversationChain_0-input-model-BaseChatModel", - "data": { - "label": "" - } + "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable-conversationChain_0-conversationChain_0-input-model-BaseChatModel" }, { "source": "bufferMemory_0", @@ -326,10 +343,7 @@ "target": "conversationChain_0", "targetHandle": "conversationChain_0-input-memory-BaseMemory", "type": "buttonedge", - "id": "bufferMemory_0-bufferMemory_0-output-bufferMemory-BufferMemory|BaseChatMemory|BaseMemory-conversationChain_0-conversationChain_0-input-memory-BaseMemory", - "data": { - "label": "" - } + "id": "bufferMemory_0-bufferMemory_0-output-bufferMemory-BufferMemory|BaseChatMemory|BaseMemory-conversationChain_0-conversationChain_0-input-memory-BaseMemory" } ] } diff --git a/packages/server/marketplaces/chatflows/Vectara LLM Chain Upload.json b/packages/server/marketplaces/chatflows/Vectara LLM Chain Upload.json index d9f9fb49..1a440be7 100644 --- a/packages/server/marketplaces/chatflows/Vectara LLM Chain Upload.json +++ b/packages/server/marketplaces/chatflows/Vectara LLM Chain Upload.json @@ -190,7 +190,7 @@ "data": { "id": "conversationalRetrievalQAChain_0", "label": "Conversational Retrieval QA Chain", - "version": 1, + "version": 2, "name": "conversationalRetrievalQAChain", "type": "ConversationalRetrievalQAChain", "baseClasses": ["ConversationalRetrievalQAChain", "BaseChain", "Runnable"], @@ -205,47 +205,36 @@ "id": "conversationalRetrievalQAChain_0-input-returnSourceDocuments-boolean" }, { - "label": "System Message", - "name": "systemMessagePrompt", + "label": "Rephrase Prompt", + "name": "rephrasePrompt", "type": "string", + "description": "Using previous chat history, rephrase question into a standalone question", + "warning": "Prompt must include input variables: {chat_history} and {question}", "rows": 4, "additionalParams": true, "optional": true, - "placeholder": "I want you to act as a document that I am having a conversation with. Your name is \"AI Assistant\". You will provide me with answers from the given info. If the answer is not included, say exactly \"Hmm, I am not sure.\" and stop after that. Refuse to answer any question not about the info. Never break character.", - "id": "conversationalRetrievalQAChain_0-input-systemMessagePrompt-string" + "default": "Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.\n\nChat History:\n{chat_history}\nFollow Up Input: {question}\nStandalone Question:", + "id": "conversationalRetrievalQAChain_0-input-rephrasePrompt-string" }, { - "label": "Chain Option", - "name": "chainOption", - "type": "options", - "options": [ - { - "label": "MapReduceDocumentsChain", - "name": "map_reduce", - "description": "Suitable for QA tasks over larger documents and can run the preprocessing step in parallel, reducing the running time" - }, - { - "label": "RefineDocumentsChain", - "name": "refine", - "description": "Suitable for QA tasks over a large number of documents." - }, - { - "label": "StuffDocumentsChain", - "name": "stuff", - "description": "Suitable for QA tasks over a small number of documents." - } - ], + "label": "Response Prompt", + "name": "responsePrompt", + "type": "string", + "description": "Taking the rephrased question, search for answer from the provided context", + "warning": "Prompt must include input variable: {context}", + "rows": 4, "additionalParams": true, "optional": true, - "id": "conversationalRetrievalQAChain_0-input-chainOption-options" + "default": "You are a helpful assistant. Using the provided context, answer the user's question to the best of your ability using the resources provided.\nIf there is nothing in the context relevant to the question at hand, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.\n------------\n{context}\n------------\nREMEMBER: If there is no relevant information within the context, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.", + "id": "conversationalRetrievalQAChain_0-input-responsePrompt-string" } ], "inputAnchors": [ { - "label": "Language Model", + "label": "Chat Model", "name": "model", - "type": "BaseLanguageModel", - "id": "conversationalRetrievalQAChain_0-input-model-BaseLanguageModel" + "type": "BaseChatModel", + "id": "conversationalRetrievalQAChain_0-input-model-BaseChatModel" }, { "label": "Vector Store Retriever", @@ -267,8 +256,8 @@ "vectorStoreRetriever": "{{vectara_0.data.instance}}", "memory": "", "returnSourceDocuments": true, - "systemMessagePrompt": "", - "chainOption": "" + "rephrasePrompt": "Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.\n\nChat History:\n{chat_history}\nFollow Up Input: {question}\nStandalone Question:", + "responsePrompt": "You are a helpful assistant. Using the provided context, answer the user's question to the best of your ability using the resources provided.\nIf there is nothing in the context relevant to the question at hand, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.\n------------\n{context}\n------------\nREMEMBER: If there is no relevant information within the context, just say \"Hmm, I'm not sure.\" Don't try to make up an answer." }, "outputAnchors": [ { @@ -361,12 +350,33 @@ { "label": "Top K", "name": "topK", - "description": "Number of top results to fetch. Defaults to 4", - "placeholder": "4", + "description": "Number of top results to fetch. Defaults to 5", + "placeholder": "5", "type": "number", "additionalParams": true, "optional": true, "id": "vectara_0-input-topK-number" + }, + { + "label": "MMR K", + "name": "mmrK", + "description": "The number of results to rerank if MMR is enabled.", + "placeholder": "50", + "type": "number", + "additionalParams": true, + "optional": true, + "id": "vectara_0-input-mmrK-number" + }, + { + "label": "MMR Diversity Bias", + "name": "mmrDiversityBias", + "step": 0.1, + "description": "Diversity Bias parameter for MMR, if enabled. 0.0 means no diversiry bias, 1.0 means maximum diversity bias. Defaults to 0.0 (MMR disabled).", + "placeholder": "0.0", + "type": "number", + "additionalParams": true, + "optional": true, + "id": "vectara_0-input-mmrDiversityBias-number" } ], "inputAnchors": [ @@ -385,7 +395,9 @@ "sentencesBefore": "", "sentencesAfter": "", "lambda": "", - "topK": "" + "topK": "", + "mmrK": "", + "mmrDiversityBias": "" }, "outputAnchors": [ { @@ -427,9 +439,9 @@ "source": "chatOpenAI_0", "sourceHandle": "chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable", "target": "conversationalRetrievalQAChain_0", - "targetHandle": "conversationalRetrievalQAChain_0-input-model-BaseLanguageModel", + "targetHandle": "conversationalRetrievalQAChain_0-input-model-BaseChatModel", "type": "buttonedge", - "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable-conversationalRetrievalQAChain_0-conversationalRetrievalQAChain_0-input-model-BaseLanguageModel", + "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable-conversationalRetrievalQAChain_0-conversationalRetrievalQAChain_0-input-model-BaseChatModel", "data": { "label": "" } diff --git a/packages/server/marketplaces/chatflows/WebBrowser.json b/packages/server/marketplaces/chatflows/WebBrowser.json index 0547366a..d905b54b 100644 --- a/packages/server/marketplaces/chatflows/WebBrowser.json +++ b/packages/server/marketplaces/chatflows/WebBrowser.json @@ -578,7 +578,7 @@ "id": "conversationalAgent_0-input-tools-Tool" }, { - "label": "Language Model", + "label": "Chat Model", "name": "model", "type": "BaseChatModel", "id": "conversationalAgent_0-input-model-BaseChatModel" diff --git a/packages/server/marketplaces/chatflows/WebPage QnA.json b/packages/server/marketplaces/chatflows/WebPage QnA.json index be077f97..df05feef 100644 --- a/packages/server/marketplaces/chatflows/WebPage QnA.json +++ b/packages/server/marketplaces/chatflows/WebPage QnA.json @@ -162,10 +162,10 @@ "data": { "id": "conversationalRetrievalQAChain_0", "label": "Conversational Retrieval QA Chain", - "version": 1, + "version": 2, "name": "conversationalRetrievalQAChain", "type": "ConversationalRetrievalQAChain", - "baseClasses": ["ConversationalRetrievalQAChain", "BaseChain"], + "baseClasses": ["ConversationalRetrievalQAChain", "BaseChain", "Runnable"], "category": "Chains", "description": "Document QA - built on RetrievalQAChain to provide a chat history component", "inputParams": [ @@ -177,47 +177,36 @@ "id": "conversationalRetrievalQAChain_0-input-returnSourceDocuments-boolean" }, { - "label": "System Message", - "name": "systemMessagePrompt", + "label": "Rephrase Prompt", + "name": "rephrasePrompt", "type": "string", + "description": "Using previous chat history, rephrase question into a standalone question", + "warning": "Prompt must include input variables: {chat_history} and {question}", "rows": 4, "additionalParams": true, "optional": true, - "placeholder": "I want you to act as a document that I am having a conversation with. Your name is \"AI Assistant\". You will provide me with answers from the given info. If the answer is not included, say exactly \"Hmm, I am not sure.\" and stop after that. Refuse to answer any question not about the info. Never break character.", - "id": "conversationalRetrievalQAChain_0-input-systemMessagePrompt-string" + "default": "Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.\n\nChat History:\n{chat_history}\nFollow Up Input: {question}\nStandalone Question:", + "id": "conversationalRetrievalQAChain_0-input-rephrasePrompt-string" }, { - "label": "Chain Option", - "name": "chainOption", - "type": "options", - "options": [ - { - "label": "MapReduceDocumentsChain", - "name": "map_reduce", - "description": "Suitable for QA tasks over larger documents and can run the preprocessing step in parallel, reducing the running time" - }, - { - "label": "RefineDocumentsChain", - "name": "refine", - "description": "Suitable for QA tasks over a large number of documents." - }, - { - "label": "StuffDocumentsChain", - "name": "stuff", - "description": "Suitable for QA tasks over a small number of documents." - } - ], + "label": "Response Prompt", + "name": "responsePrompt", + "type": "string", + "description": "Taking the rephrased question, search for answer from the provided context", + "warning": "Prompt must include input variable: {context}", + "rows": 4, "additionalParams": true, "optional": true, - "id": "conversationalRetrievalQAChain_0-input-chainOption-options" + "default": "You are a helpful assistant. Using the provided context, answer the user's question to the best of your ability using the resources provided.\nIf there is nothing in the context relevant to the question at hand, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.\n------------\n{context}\n------------\nREMEMBER: If there is no relevant information within the context, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.", + "id": "conversationalRetrievalQAChain_0-input-responsePrompt-string" } ], "inputAnchors": [ { - "label": "Language Model", + "label": "Chat Model", "name": "model", - "type": "BaseLanguageModel", - "id": "conversationalRetrievalQAChain_0-input-model-BaseLanguageModel" + "type": "BaseChatModel", + "id": "conversationalRetrievalQAChain_0-input-model-BaseChatModel" }, { "label": "Vector Store Retriever", @@ -239,15 +228,15 @@ "vectorStoreRetriever": "{{pinecone_0.data.instance}}", "memory": "{{RedisBackedChatMemory_0.data.instance}}", "returnSourceDocuments": true, - "systemMessagePrompt": "I want you to act as a document that I am having a conversation with. Your name is \"AI Assistant\". You will provide me with answers from the given context. If the answer is not included, say exactly \"Hmm, I am not sure.\" and stop after that. Do not make up any information that is not in the context. Refuse to answer any question not about the info. Never break character.", - "chainOption": "" + "rephrasePrompt": "Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.\n\nChat History:\n{chat_history}\nFollow Up Input: {question}\nStandalone Question:", + "responsePrompt": "You are a helpful assistant. Using the provided context, answer the user's question to the best of your ability using the resources provided.\nIf there is nothing in the context relevant to the question at hand, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.\n------------\n{context}\n------------\nREMEMBER: If there is no relevant information within the context, just say \"Hmm, I'm not sure.\" Don't try to make up an answer." }, "outputAnchors": [ { - "id": "conversationalRetrievalQAChain_0-output-conversationalRetrievalQAChain-ConversationalRetrievalQAChain|BaseChain", + "id": "conversationalRetrievalQAChain_0-output-conversationalRetrievalQAChain-ConversationalRetrievalQAChain|BaseChain|Runnable", "name": "conversationalRetrievalQAChain", "label": "ConversationalRetrievalQAChain", - "type": "ConversationalRetrievalQAChain | BaseChain" + "type": "ConversationalRetrievalQAChain | BaseChain | Runnable" } ], "outputs": {}, @@ -654,7 +643,7 @@ "type": "Pinecone", "baseClasses": ["Pinecone", "VectorStoreRetriever", "BaseRetriever"], "category": "Vector Stores", - "description": "Upsert embedded data and perform similarity search upon query using Pinecone, a leading fully managed hosted vector database", + "description": "Upsert embedded data and perform similarity or mmr search using Pinecone, a leading fully managed hosted vector database", "inputParams": [ { "label": "Connect Credential", @@ -695,6 +684,45 @@ "additionalParams": true, "optional": true, "id": "pinecone_0-input-topK-number" + }, + { + "label": "Search Type", + "name": "searchType", + "type": "options", + "default": "similarity", + "options": [ + { + "label": "Similarity", + "name": "similarity" + }, + { + "label": "Max Marginal Relevance", + "name": "mmr" + } + ], + "additionalParams": true, + "optional": true, + "id": "pinecone_0-input-searchType-options" + }, + { + "label": "Fetch K (for MMR Search)", + "name": "fetchK", + "description": "Number of initial documents to fetch for MMR reranking. Default to 20. Used only when the search type is MMR", + "placeholder": "20", + "type": "number", + "additionalParams": true, + "optional": true, + "id": "pinecone_0-input-fetchK-number" + }, + { + "label": "Lambda (for MMR Search)", + "name": "lambda", + "description": "Number between 0 and 1 that determines the degree of diversity among the results, where 0 corresponds to maximum diversity and 1 to minimum diversity. Used only when the search type is MMR", + "placeholder": "0.5", + "type": "number", + "additionalParams": true, + "optional": true, + "id": "pinecone_0-input-lambda-number" } ], "inputAnchors": [ @@ -719,7 +747,10 @@ "pineconeIndex": "", "pineconeNamespace": "", "pineconeMetadataFilter": "", - "topK": "" + "topK": "", + "searchType": "similarity", + "fetchK": "", + "lambda": "" }, "outputAnchors": [ { @@ -772,9 +803,9 @@ "source": "chatOpenAI_0", "sourceHandle": "chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable", "target": "conversationalRetrievalQAChain_0", - "targetHandle": "conversationalRetrievalQAChain_0-input-model-BaseLanguageModel", + "targetHandle": "conversationalRetrievalQAChain_0-input-model-BaseChatModel", "type": "buttonedge", - "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable-conversationalRetrievalQAChain_0-conversationalRetrievalQAChain_0-input-model-BaseLanguageModel", + "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable-conversationalRetrievalQAChain_0-conversationalRetrievalQAChain_0-input-model-BaseChatModel", "data": { "label": "" } diff --git a/packages/server/package.json b/packages/server/package.json index 6f4ccaf4..fe27f8d6 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "flowise", - "version": "1.4.3", + "version": "1.4.10", "description": "Flowiseai Server", "main": "dist/index", "types": "dist/index.d.ts", @@ -47,7 +47,7 @@ "dependencies": { "@oclif/core": "^1.13.10", "async-mutex": "^0.4.0", - "axios": "^0.27.2", + "axios": "1.6.2", "cors": "^2.8.5", "crypto-js": "^4.1.1", "dotenv": "^16.0.0", @@ -60,7 +60,9 @@ "multer": "^1.4.5-lts.1", "mysql": "^2.18.1", "pg": "^8.11.1", + "posthog-node": "^3.5.0", "reflect-metadata": "^0.1.13", + "sanitize-html": "^2.11.0", "socket.io": "^4.6.1", "sqlite3": "^5.1.6", "typeorm": "^0.3.6", @@ -71,6 +73,7 @@ "@types/cors": "^2.8.12", "@types/crypto-js": "^4.1.1", "@types/multer": "^1.4.7", + "@types/sanitize-html": "^2.9.5", "concurrently": "^7.1.0", "nodemon": "^2.0.15", "oclif": "^3", diff --git a/packages/server/src/ChatflowPool.ts b/packages/server/src/ChatflowPool.ts index d296dcfe..325fac56 100644 --- a/packages/server/src/ChatflowPool.ts +++ b/packages/server/src/ChatflowPool.ts @@ -16,7 +16,7 @@ export class ChatflowPool { * @param {IReactFlowNode[]} startingNodes * @param {ICommonObject} overrideConfig */ - add(chatflowid: string, endingNodeData: INodeData, startingNodes: IReactFlowNode[], overrideConfig?: ICommonObject) { + add(chatflowid: string, endingNodeData: INodeData | undefined, startingNodes: IReactFlowNode[], overrideConfig?: ICommonObject) { this.activeChatflows[chatflowid] = { startingNodes, endingNodeData, diff --git a/packages/server/src/DataSource.ts b/packages/server/src/DataSource.ts index 9265e55f..762315ac 100644 --- a/packages/server/src/DataSource.ts +++ b/packages/server/src/DataSource.ts @@ -46,6 +46,7 @@ export const init = async (): Promise => { username: process.env.DATABASE_USER, password: process.env.DATABASE_PASSWORD, database: process.env.DATABASE_NAME, + ssl: process.env.DATABASE_SSL === 'true', synchronize: false, migrationsRun: false, entities: Object.values(entities), diff --git a/packages/server/src/Interface.ts b/packages/server/src/Interface.ts index d5890ab6..126aac38 100644 --- a/packages/server/src/Interface.ts +++ b/packages/server/src/Interface.ts @@ -68,6 +68,15 @@ export interface ICredential { createdDate: Date } +export interface IVariable { + id: string + name: string + value: string + type: string + updatedDate: Date + createdDate: Date +} + export interface IComponentNodes { [key: string]: INode } @@ -172,7 +181,7 @@ export interface IncomingInput { export interface IActiveChatflows { [key: string]: { startingNodes: IReactFlowNode[] - endingNodeData: INodeData + endingNodeData?: INodeData inSync: boolean overrideConfig?: ICommonObject } diff --git a/packages/server/src/commands/start.ts b/packages/server/src/commands/start.ts index cd874264..08cd8298 100644 --- a/packages/server/src/commands/start.ts +++ b/packages/server/src/commands/start.ts @@ -35,10 +35,12 @@ export default class Start extends Command { DATABASE_NAME: Flags.string(), DATABASE_USER: Flags.string(), DATABASE_PASSWORD: Flags.string(), + DATABASE_SSL: Flags.string(), LANGCHAIN_TRACING_V2: Flags.string(), LANGCHAIN_ENDPOINT: Flags.string(), LANGCHAIN_API_KEY: Flags.string(), - LANGCHAIN_PROJECT: Flags.string() + LANGCHAIN_PROJECT: Flags.string(), + DISABLE_FLOWISE_TELEMETRY: Flags.string() } async stopProcess() { @@ -104,6 +106,7 @@ export default class Start extends Command { if (flags.DATABASE_NAME) process.env.DATABASE_NAME = flags.DATABASE_NAME if (flags.DATABASE_USER) process.env.DATABASE_USER = flags.DATABASE_USER if (flags.DATABASE_PASSWORD) process.env.DATABASE_PASSWORD = flags.DATABASE_PASSWORD + if (flags.DATABASE_SSL) process.env.DATABASE_SSL = flags.DATABASE_SSL // Langsmith tracing if (flags.LANGCHAIN_TRACING_V2) process.env.LANGCHAIN_TRACING_V2 = flags.LANGCHAIN_TRACING_V2 @@ -111,6 +114,9 @@ export default class Start extends Command { if (flags.LANGCHAIN_API_KEY) process.env.LANGCHAIN_API_KEY = flags.LANGCHAIN_API_KEY if (flags.LANGCHAIN_PROJECT) process.env.LANGCHAIN_PROJECT = flags.LANGCHAIN_PROJECT + // Telemetry + if (flags.DISABLE_FLOWISE_TELEMETRY) process.env.DISABLE_FLOWISE_TELEMETRY = flags.DISABLE_FLOWISE_TELEMETRY + await (async () => { try { logger.info('Starting Flowise...') diff --git a/packages/server/src/database/entities/Variable.ts b/packages/server/src/database/entities/Variable.ts new file mode 100644 index 00000000..6af7a237 --- /dev/null +++ b/packages/server/src/database/entities/Variable.ts @@ -0,0 +1,24 @@ +/* eslint-disable */ +import { Entity, Column, CreateDateColumn, UpdateDateColumn, PrimaryGeneratedColumn } from 'typeorm' +import { IVariable } from '../../Interface' + +@Entity() +export class Variable implements IVariable { + @PrimaryGeneratedColumn('uuid') + id: string + + @Column() + name: string + + @Column({ nullable: true, type: 'text' }) + value: string + + @Column({ default: 'string', type: 'text' }) + type: string + + @CreateDateColumn() + createdDate: Date + + @UpdateDateColumn() + updatedDate: Date +} diff --git a/packages/server/src/database/entities/index.ts b/packages/server/src/database/entities/index.ts index 58447a1f..af5c559f 100644 --- a/packages/server/src/database/entities/index.ts +++ b/packages/server/src/database/entities/index.ts @@ -3,11 +3,13 @@ import { ChatMessage } from './ChatMessage' import { Credential } from './Credential' import { Tool } from './Tool' import { Assistant } from './Assistant' +import { Variable } from './Variable' export const entities = { ChatFlow, ChatMessage, Credential, Tool, - Assistant + Assistant, + Variable } diff --git a/packages/server/src/database/migrations/mysql/1702200925471-AddVariableEntity.ts b/packages/server/src/database/migrations/mysql/1702200925471-AddVariableEntity.ts new file mode 100644 index 00000000..a6c81887 --- /dev/null +++ b/packages/server/src/database/migrations/mysql/1702200925471-AddVariableEntity.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddVariableEntity1699325775451 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE IF NOT EXISTS \`variable\` ( + \`id\` varchar(36) NOT NULL, + \`name\` varchar(255) NOT NULL, + \`value\` text NOT NULL, + \`type\` varchar(255) DEFAULT NULL, + \`createdDate\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + \`updatedDate\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + PRIMARY KEY (\`id\`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE variable`) + } +} diff --git a/packages/server/src/database/migrations/mysql/index.ts b/packages/server/src/database/migrations/mysql/index.ts index 8f9824a8..a5220ad8 100644 --- a/packages/server/src/database/migrations/mysql/index.ts +++ b/packages/server/src/database/migrations/mysql/index.ts @@ -10,6 +10,7 @@ import { AddAssistantEntity1699325775451 } from './1699325775451-AddAssistantEnt import { AddUsedToolsToChatMessage1699481607341 } from './1699481607341-AddUsedToolsToChatMessage' import { AddCategoryToChatFlow1699900910291 } from './1699900910291-AddCategoryToChatFlow' import { AddFileAnnotationsToChatMessage1700271021237 } from './1700271021237-AddFileAnnotationsToChatMessage' +import { AddVariableEntity1699325775451 } from './1702200925471-AddVariableEntity' export const mysqlMigrations = [ Init1693840429259, @@ -23,5 +24,6 @@ export const mysqlMigrations = [ AddAssistantEntity1699325775451, AddUsedToolsToChatMessage1699481607341, AddCategoryToChatFlow1699900910291, - AddFileAnnotationsToChatMessage1700271021237 + AddFileAnnotationsToChatMessage1700271021237, + AddVariableEntity1699325775451 ] diff --git a/packages/server/src/database/migrations/postgres/1702200925471-AddVariableEntity.ts b/packages/server/src/database/migrations/postgres/1702200925471-AddVariableEntity.ts new file mode 100644 index 00000000..c6d3902f --- /dev/null +++ b/packages/server/src/database/migrations/postgres/1702200925471-AddVariableEntity.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddVariableEntity1699325775451 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE IF NOT EXISTS variable ( + id uuid NOT NULL DEFAULT uuid_generate_v4(), + "name" varchar NOT NULL, + "value" text NOT NULL, + "type" text NULL, + "createdDate" timestamp NOT NULL DEFAULT now(), + "updatedDate" timestamp NOT NULL DEFAULT now(), + CONSTRAINT "PK_98419043dd704f54-9830ab78f8" PRIMARY KEY (id) + );` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE variable`) + } +} diff --git a/packages/server/src/database/migrations/postgres/index.ts b/packages/server/src/database/migrations/postgres/index.ts index d196fbc1..3c3fa396 100644 --- a/packages/server/src/database/migrations/postgres/index.ts +++ b/packages/server/src/database/migrations/postgres/index.ts @@ -10,6 +10,7 @@ import { AddAssistantEntity1699325775451 } from './1699325775451-AddAssistantEnt import { AddUsedToolsToChatMessage1699481607341 } from './1699481607341-AddUsedToolsToChatMessage' import { AddCategoryToChatFlow1699900910291 } from './1699900910291-AddCategoryToChatFlow' import { AddFileAnnotationsToChatMessage1700271021237 } from './1700271021237-AddFileAnnotationsToChatMessage' +import { AddVariableEntity1699325775451 } from './1702200925471-AddVariableEntity' export const postgresMigrations = [ Init1693891895163, @@ -23,5 +24,6 @@ export const postgresMigrations = [ AddAssistantEntity1699325775451, AddUsedToolsToChatMessage1699481607341, AddCategoryToChatFlow1699900910291, - AddFileAnnotationsToChatMessage1700271021237 + AddFileAnnotationsToChatMessage1700271021237, + AddVariableEntity1699325775451 ] diff --git a/packages/server/src/database/migrations/sqlite/1702200925471-AddVariableEntity.ts b/packages/server/src/database/migrations/sqlite/1702200925471-AddVariableEntity.ts new file mode 100644 index 00000000..63ec709f --- /dev/null +++ b/packages/server/src/database/migrations/sqlite/1702200925471-AddVariableEntity.ts @@ -0,0 +1,13 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddVariableEntity1699325775451 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE IF NOT EXISTS "variable" ("id" varchar PRIMARY KEY NOT NULL, "name" text NOT NULL, "value" text NOT NULL, "type" varchar, "createdDate" datetime NOT NULL DEFAULT (datetime('now')), "updatedDate" datetime NOT NULL DEFAULT (datetime('now')));` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE variable`) + } +} diff --git a/packages/server/src/database/migrations/sqlite/index.ts b/packages/server/src/database/migrations/sqlite/index.ts index fdd83064..c0ade080 100644 --- a/packages/server/src/database/migrations/sqlite/index.ts +++ b/packages/server/src/database/migrations/sqlite/index.ts @@ -10,6 +10,7 @@ import { AddAssistantEntity1699325775451 } from './1699325775451-AddAssistantEnt import { AddUsedToolsToChatMessage1699481607341 } from './1699481607341-AddUsedToolsToChatMessage' import { AddCategoryToChatFlow1699900910291 } from './1699900910291-AddCategoryToChatFlow' import { AddFileAnnotationsToChatMessage1700271021237 } from './1700271021237-AddFileAnnotationsToChatMessage' +import { AddVariableEntity1699325775451 } from './1702200925471-AddVariableEntity' export const sqliteMigrations = [ Init1693835579790, @@ -23,5 +24,6 @@ export const sqliteMigrations = [ AddAssistantEntity1699325775451, AddUsedToolsToChatMessage1699481607341, AddCategoryToChatFlow1699900910291, - AddFileAnnotationsToChatMessage1700271021237 + AddFileAnnotationsToChatMessage1700271021237, + AddVariableEntity1699325775451 ] diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 78cc4260..673c5792 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -20,13 +20,14 @@ import { ICredentialReturnResponse, chatType, IChatMessage, - IReactFlowEdge + IDepthQueue, + INodeDirectedGraph } from './Interface' import { getNodeModulesPackagePath, getStartingNodes, buildLangchain, - getEndingNode, + getEndingNodes, constructGraphs, resolveVariables, isStartNodeDependOnInput, @@ -39,10 +40,14 @@ import { decryptCredentialData, replaceInputsWithConfig, getEncryptionKey, - replaceMemorySessionId, + getMemorySessionId, getUserHome, - replaceChatHistory, - clearSessionMemory + getSessionChatHistory, + getAllConnectedNodes, + clearSessionMemory, + findMemoryNode, + getTelemetryFlowObj, + getAppVersion } from './utils' import { cloneDeep, omit, uniqWith, isEqual } from 'lodash' import { getDataSource } from './DataSource' @@ -54,15 +59,22 @@ import { Tool } from './database/entities/Tool' import { Assistant } from './database/entities/Assistant' import { ChatflowPool } from './ChatflowPool' import { CachePool } from './CachePool' -import { ICommonObject, IMessage, INodeOptionsValue } from 'flowise-components' +import { ICommonObject, IMessage, INodeOptionsValue, handleEscapeCharacters } from 'flowise-components' import { createRateLimiter, getRateLimiter, initializeRateLimiter } from './utils/rateLimit' import { addAPIKey, compareKeys, deleteAPIKey, getApiKey, getAPIKeys, updateAPIKey } from './utils/apiKey' +import { sanitizeMiddleware } from './utils/XSS' +import axios from 'axios' +import { Client } from 'langchainhub' +import { parsePrompt } from './utils/hub' +import { Telemetry } from './utils/telemetry' +import { Variable } from './database/entities/Variable' export class App { app: express.Application nodesPool: NodesPool chatflowPool: ChatflowPool cachePool: CachePool + telemetry: Telemetry AppDataSource = getDataSource() constructor() { @@ -97,6 +109,9 @@ export class App { // Initialize cache pool this.cachePool = new CachePool() + + // Initialize telemetry + this.telemetry = new Telemetry() }) .catch((err) => { logger.error('❌ [server]: Error during Data Source initialization:', err) @@ -114,9 +129,15 @@ export class App { // Allow access from * this.app.use(cors()) + // Switch off the default 'X-Powered-By: Express' header + this.app.disable('x-powered-by') + // Add the expressRequestLogger middleware to log all requests this.app.use(expressRequestLogger) + // Add the sanitizeMiddleware to guard against XSS + this.app.use(sanitizeMiddleware) + if (process.env.FLOWISE_USERNAME && process.env.FLOWISE_PASSWORD) { const username = process.env.FLOWISE_USERNAME const password = process.env.FLOWISE_PASSWORD @@ -127,6 +148,7 @@ export class App { '/api/v1/verify/apikey/', '/api/v1/chatflows/apikey/', '/api/v1/public-chatflows', + '/api/v1/public-chatbotConfig', '/api/v1/prediction/', '/api/v1/vector/upsert/', '/api/v1/node-icon/', @@ -189,7 +211,7 @@ export class App { // Get component credential via name this.app.get('/api/v1/components-credentials/:name', (req: Request, res: Response) => { - if (!req.params.name.includes('&')) { + if (!req.params.name.includes('&')) { if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentCredentials, req.params.name)) { return res.json(this.nodesPool.componentCredentials[req.params.name]) } else { @@ -197,7 +219,7 @@ export class App { } } else { const returnResponse = [] - for (const name of req.params.name.split('&')) { + for (const name of req.params.name.split('&')) { if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentCredentials, name)) { returnResponse.push(this.nodesPool.componentCredentials[name]) } else { @@ -269,6 +291,35 @@ export class App { } }) + // execute custom function node + this.app.post('/api/v1/node-custom-function', async (req: Request, res: Response) => { + const body = req.body + const nodeData = { inputs: body } + if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentNodes, 'customFunction')) { + try { + const nodeInstanceFilePath = this.nodesPool.componentNodes['customFunction'].filePath as string + const nodeModule = await import(nodeInstanceFilePath) + const newNodeInstance = new nodeModule.nodeClass() + + const options: ICommonObject = { + appDataSource: this.AppDataSource, + databaseEntities, + logger + } + + const returnData = await newNodeInstance.init(nodeData, '', options) + const result = typeof returnData === 'string' ? handleEscapeCharacters(returnData, true) : returnData + + return res.json(result) + } catch (error) { + return res.status(500).send(`Error running custom function: ${error}`) + } + } else { + res.status(404).send(`Node customFunction not found`) + return + } + }) + // ---------------------------------------- // Chatflows // ---------------------------------------- @@ -317,6 +368,24 @@ export class App { return res.status(404).send(`Chatflow ${req.params.id} not found`) }) + // Get specific chatflow chatbotConfig via id (PUBLIC endpoint, used to retrieve config for embedded chat) + // Safe as public endpoint as chatbotConfig doesn't contain sensitive credential + this.app.get('/api/v1/public-chatbotConfig/:id', async (req: Request, res: Response) => { + const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ + id: req.params.id + }) + if (!chatflow) return res.status(404).send(`Chatflow ${req.params.id} not found`) + if (chatflow.chatbotConfig) { + try { + const parsedConfig = JSON.parse(chatflow.chatbotConfig) + return res.json(parsedConfig) + } catch (e) { + return res.status(500).send(`Error parsing Chatbot Config for Chatflow ${req.params.id}`) + } + } + return res.status(200).send('OK') + }) + // Save chatflow this.app.post('/api/v1/chatflows', async (req: Request, res: Response) => { const body = req.body @@ -326,6 +395,12 @@ export class App { const chatflow = this.AppDataSource.getRepository(ChatFlow).create(newChatFlow) const results = await this.AppDataSource.getRepository(ChatFlow).save(chatflow) + await this.telemetry.sendTelemetry('chatflow_created', { + version: await getAppVersion(), + chatlowId: results.id, + flowGraph: getTelemetryFlowObj(JSON.parse(results.flowData)?.nodes, JSON.parse(results.flowData)?.edges) + }) + return res.json(results) }) @@ -380,24 +455,29 @@ export class App { const edges = parsedFlowData.edges const { graph, nodeDependencies } = constructGraphs(nodes, edges) - const endingNodeId = getEndingNode(nodeDependencies, graph) - if (!endingNodeId) return res.status(500).send(`Ending node ${endingNodeId} not found`) + const endingNodeIds = getEndingNodes(nodeDependencies, graph) + if (!endingNodeIds.length) return res.status(500).send(`Ending nodes not found`) - const endingNodeData = nodes.find((nd) => nd.id === endingNodeId)?.data - if (!endingNodeData) return res.status(500).send(`Ending node ${endingNodeId} data not found`) + const endingNodes = nodes.filter((nd) => endingNodeIds.includes(nd.id)) - if ( - endingNodeData && - endingNodeData.category !== 'Chains' && - endingNodeData.category !== 'Agents' && - endingNodeData.category !== 'Engine' - ) { - return res.status(500).send(`Ending node must be either a Chain or Agent`) + let isStreaming = false + for (const endingNode of endingNodes) { + const endingNodeData = endingNode.data + if (!endingNodeData) return res.status(500).send(`Ending node ${endingNode.id} data not found`) + + if ( + endingNodeData && + endingNodeData.category !== 'Chains' && + endingNodeData.category !== 'Agents' && + endingNodeData.category !== 'Engine' + ) { + return res.status(500).send(`Ending node must be either a Chain or Agent`) + } + + isStreaming = isFlowValidForStream(nodes, endingNodeData) } - const obj = { - isStreaming: isFlowValidForStream(nodes, endingNodeData) - } + const obj = { isStreaming } return res.json(obj) }) @@ -466,7 +546,7 @@ export class App { res.status(404).send(`Chatflow ${chatflowid} not found`) return } - const chatId = (req.query?.chatId as string) ?? (await getChatId(chatflowid)) + 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 @@ -476,17 +556,22 @@ export class App { const parsedFlowData: IReactFlowObject = JSON.parse(flowData) const nodes = parsedFlowData.nodes - await clearSessionMemory( - nodes, - this.nodesPool.componentNodes, - chatId, - this.AppDataSource, - sessionId, - memoryType, - isClearFromViewMessageDialog - ) + try { + await clearSessionMemory( + nodes, + this.nodesPool.componentNodes, + chatId, + this.AppDataSource, + sessionId, + memoryType, + isClearFromViewMessageDialog + ) + } catch (e) { + return res.status(500).send('Error clearing chat messages') + } - const deleteOptions: FindOptionsWhere = { chatflowid, chatId } + const deleteOptions: FindOptionsWhere = { chatflowid } + if (chatId) deleteOptions.chatId = chatId if (memoryType) deleteOptions.memoryType = memoryType if (sessionId) deleteOptions.sessionId = sessionId if (chatType) deleteOptions.chatType = chatType @@ -574,7 +659,7 @@ export class App { return res.json(result) }) - // Delete all chatmessages from chatflowid + // Delete all credentials from chatflowid this.app.delete('/api/v1/credentials/:id', async (req: Request, res: Response) => { const results = await this.AppDataSource.getRepository(Credential).delete({ id: req.params.id }) return res.json(results) @@ -607,6 +692,12 @@ export class App { const tool = this.AppDataSource.getRepository(Tool).create(newTool) const results = await this.AppDataSource.getRepository(Tool).save(tool) + await this.telemetry.sendTelemetry('tool_created', { + version: await getAppVersion(), + toolId: results.id, + toolName: results.name + }) + return res.json(results) }) @@ -813,6 +904,11 @@ export class App { const assistant = this.AppDataSource.getRepository(Assistant).create(newAssistant) const results = await this.AppDataSource.getRepository(Assistant).save(assistant) + await this.telemetry.sendTelemetry('assistant_created', { + version: await getAppVersion(), + assistantId: results.id + }) + return res.json(results) }) @@ -965,6 +1061,12 @@ export class App { // Download file from assistant this.app.post('/api/v1/openai-assistants-file', async (req: Request, res: Response) => { const filePath = path.join(getUserHome(), '.flowise', 'openai-assistant', req.body.fileName) + //raise error if file path is not absolute + if (!path.isAbsolute(filePath)) return res.status(500).send(`Invalid file path`) + //raise error if file path contains '..' + if (filePath.includes('..')) return res.status(500).send(`Invalid file path`) + //only return from the .flowise openai-assistant folder + if (!(filePath.includes('.flowise') && filePath.includes('openai-assistant'))) return res.status(500).send(`Invalid file path`) res.setHeader('Content-Disposition', 'attachment; filename=' + path.basename(filePath)) const fileStream = fs.createReadStream(filePath) fileStream.pipe(res) @@ -1029,12 +1131,41 @@ export class App { upload.array('files'), (req: Request, res: Response, next: NextFunction) => getRateLimiter(req, res, next), async (req: Request, res: Response) => { - await this.buildChatflow(req, res, undefined, false, true) + await this.upsertVector(req, res) } ) this.app.post('/api/v1/vector/internal-upsert/:id', async (req: Request, res: Response) => { - await this.buildChatflow(req, res, undefined, true, true) + await this.upsertVector(req, res, true) + }) + + // ---------------------------------------- + // Prompt from Hub + // ---------------------------------------- + this.app.post('/api/v1/load-prompt', async (req: Request, res: Response) => { + try { + let hub = new Client() + const prompt = await hub.pull(req.body.promptName) + const templates = parsePrompt(prompt) + return res.json({ status: 'OK', prompt: req.body.promptName, templates: templates }) + } catch (e: any) { + return res.json({ status: 'ERROR', prompt: req.body.promptName, error: e?.message }) + } + }) + + this.app.post('/api/v1/prompts-list', async (req: Request, res: Response) => { + try { + const tags = req.body.tags ? `tags=${req.body.tags}` : '' + // Default to 100, TODO: add pagination and use offset & limit + const url = `https://api.hub.langchain.com/repos/?limit=100&${tags}has_commits=true&sort_field=num_likes&sort_direction=desc&is_archived=false` + axios.get(url).then((response) => { + if (response.data.repos) { + return res.json({ status: 'OK', repos: response.data.repos }) + } + }) + } catch (e: any) { + return res.json({ status: 'ERROR', repos: [] }) + } }) // ---------------------------------------- @@ -1106,6 +1237,47 @@ export class App { return res.json(templates) }) + // ---------------------------------------- + // Variables + // ---------------------------------------- + this.app.get('/api/v1/variables', async (req: Request, res: Response) => { + const variables = await getDataSource().getRepository(Variable).find() + return res.json(variables) + }) + + // Create new variable + this.app.post('/api/v1/variables', async (req: Request, res: Response) => { + const body = req.body + const newVariable = new Variable() + Object.assign(newVariable, body) + const variable = this.AppDataSource.getRepository(Variable).create(newVariable) + const results = await this.AppDataSource.getRepository(Variable).save(variable) + return res.json(results) + }) + + // Update variable + this.app.put('/api/v1/variables/:id', async (req: Request, res: Response) => { + const variable = await this.AppDataSource.getRepository(Variable).findOneBy({ + id: req.params.id + }) + + if (!variable) return res.status(404).send(`Variable ${req.params.id} not found`) + + const body = req.body + const updateVariable = new Variable() + Object.assign(updateVariable, body) + this.AppDataSource.getRepository(Variable).merge(variable, updateVariable) + const result = await this.AppDataSource.getRepository(Variable).save(variable) + + return res.json(result) + }) + + // Delete variable via id + this.app.delete('/api/v1/variables/:id', async (req: Request, res: Response) => { + const results = await this.AppDataSource.getRepository(Variable).delete({ id: req.params.id }) + return res.json(results) + }) + // ---------------------------------------- // API Keys // ---------------------------------------- @@ -1262,24 +1434,123 @@ export class App { return await this.AppDataSource.getRepository(ChatMessage).save(chatmessage) } - /** - * Method that find memory label that is connected within chatflow - * In a chatflow, there should only be 1 memory node - * @param {IReactFlowNode[]} nodes - * @param {IReactFlowEdge[]} edges - * @returns {string | undefined} - */ - findMemoryLabel(nodes: IReactFlowNode[], edges: IReactFlowEdge[]): IReactFlowNode | undefined { - const memoryNodes = nodes.filter((node) => node.data.category === 'Memory') - const memoryNodeIds = memoryNodes.map((mem) => mem.data.id) + async upsertVector(req: Request, res: Response, isInternal: boolean = false) { + try { + const chatflowid = req.params.id + let incomingInput: IncomingInput = req.body - for (const edge of edges) { - if (memoryNodeIds.includes(edge.source)) { - const memoryNode = nodes.find((node) => node.data.id === edge.source) - return memoryNode + const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ + id: chatflowid + }) + if (!chatflow) return res.status(404).send(`Chatflow ${chatflowid} not found`) + + if (!isInternal) { + const isKeyValidated = await this.validateKey(req, chatflow) + if (!isKeyValidated) return res.status(401).send('Unauthorized') } + + const files = (req.files as any[]) || [] + + if (files.length) { + const overrideConfig: ICommonObject = { ...req.body } + for (const file of files) { + const fileData = fs.readFileSync(file.path, { encoding: 'base64' }) + const dataBase64String = `data:${file.mimetype};base64,${fileData},filename:${file.filename}` + + const fileInputField = mapMimeTypeToInputField(file.mimetype) + if (overrideConfig[fileInputField]) { + overrideConfig[fileInputField] = JSON.stringify([...JSON.parse(overrideConfig[fileInputField]), dataBase64String]) + } else { + overrideConfig[fileInputField] = JSON.stringify([dataBase64String]) + } + } + incomingInput = { + question: req.body.question ?? 'hello', + overrideConfig, + history: [], + stopNodeId: req.body.stopNodeId + } + } + + /*** Get chatflows and prepare data ***/ + const flowData = chatflow.flowData + const parsedFlowData: IReactFlowObject = JSON.parse(flowData) + const nodes = parsedFlowData.nodes + const edges = parsedFlowData.edges + + let stopNodeId = incomingInput?.stopNodeId ?? '' + let chatHistory = incomingInput?.history + let chatId = incomingInput.chatId ?? '' + let isUpsert = true + + // Get session ID + const memoryNode = findMemoryNode(nodes, edges) + let sessionId = undefined + if (memoryNode) sessionId = getMemorySessionId(memoryNode, incomingInput, chatId, isInternal) + + const vsNodes = nodes.filter( + (node) => + node.data.category === 'Vector Stores' && + !node.data.label.includes('Upsert') && + !node.data.label.includes('Load Existing') + ) + if (vsNodes.length > 1 && !stopNodeId) { + return res.status(500).send('There are multiple vector nodes, please provide stopNodeId in body request') + } else if (vsNodes.length === 1 && !stopNodeId) { + stopNodeId = vsNodes[0].data.id + } else if (!vsNodes.length && !stopNodeId) { + return res.status(500).send('No vector node found') + } + + const { graph } = constructGraphs(nodes, edges, { isReversed: true }) + + const nodeIds = getAllConnectedNodes(graph, stopNodeId) + + const filteredGraph: INodeDirectedGraph = {} + for (const key of nodeIds) { + if (Object.prototype.hasOwnProperty.call(graph, key)) { + filteredGraph[key] = graph[key] + } + } + + const { startingNodeIds, depthQueue } = getStartingNodes(filteredGraph, stopNodeId) + + await buildLangchain( + startingNodeIds, + nodes, + edges, + filteredGraph, + depthQueue, + this.nodesPool.componentNodes, + incomingInput.question, + chatHistory, + chatId, + sessionId ?? '', + chatflowid, + this.AppDataSource, + incomingInput?.overrideConfig, + this.cachePool, + isUpsert, + stopNodeId + ) + + const startingNodes = nodes.filter((nd) => startingNodeIds.includes(nd.data.id)) + + this.chatflowPool.add(chatflowid, undefined, startingNodes, incomingInput?.overrideConfig) + + await this.telemetry.sendTelemetry('vector_upserted', { + version: await getAppVersion(), + chatlowId: chatflowid, + type: isInternal ? chatType.INTERNAL : chatType.EXTERNAL, + flowGraph: getTelemetryFlowObj(nodes, edges), + stopNodeId + }) + + return res.status(201).send('Successfully Upserted') + } catch (e: any) { + logger.error('[server]: Error:', e) + return res.status(500).send(e.message) } - return undefined } /** @@ -1290,7 +1561,7 @@ export class App { * @param {boolean} isInternal * @param {boolean} isUpsert */ - async buildChatflow(req: Request, res: Response, socketIO?: Server, isInternal: boolean = false, isUpsert: boolean = false) { + async buildChatflow(req: Request, res: Response, socketIO?: Server, isInternal: boolean = false) { try { const chatflowid = req.params.id let incomingInput: IncomingInput = req.body @@ -1331,8 +1602,7 @@ export class App { question: req.body.question ?? 'hello', overrideConfig, history: [], - socketIOClientId: req.body.socketIOClientId, - stopNodeId: req.body.stopNodeId + socketIOClientId: req.body.socketIOClientId } } @@ -1342,28 +1612,35 @@ export class App { const nodes = parsedFlowData.nodes const edges = parsedFlowData.edges - /* Reuse the flow without having to rebuild (to avoid duplicated upsert, recomputation) when all these conditions met: + // Get session ID + const memoryNode = findMemoryNode(nodes, edges) + const memoryType = memoryNode?.data.label + let sessionId = undefined + if (memoryNode) sessionId = getMemorySessionId(memoryNode, incomingInput, chatId, isInternal) + + /* Reuse the flow without having to rebuild (to avoid duplicated upsert, recomputation, reinitialization of memory) when all these conditions met: * - Node Data already exists in pool * - Still in sync (i.e the flow has not been modified since) * - Existing overrideConfig and new overrideConfig are the same * - Flow doesn't start with/contain nodes that depend on incomingInput.question + * TODO: convert overrideConfig to hash when we no longer store base64 string but filepath ***/ const isFlowReusable = () => { return ( Object.prototype.hasOwnProperty.call(this.chatflowPool.activeChatflows, chatflowid) && this.chatflowPool.activeChatflows[chatflowid].inSync && + this.chatflowPool.activeChatflows[chatflowid].endingNodeData && isSameOverrideConfig( isInternal, this.chatflowPool.activeChatflows[chatflowid].overrideConfig, incomingInput.overrideConfig ) && - !isStartNodeDependOnInput(this.chatflowPool.activeChatflows[chatflowid].startingNodes, nodes) && - !isUpsert + !isStartNodeDependOnInput(this.chatflowPool.activeChatflows[chatflowid].startingNodes, nodes) ) } if (isFlowReusable()) { - nodeToExecuteData = this.chatflowPool.activeChatflows[chatflowid].endingNodeData + nodeToExecuteData = this.chatflowPool.activeChatflows[chatflowid].endingNodeData as INodeData isStreamValid = isFlowValidForStream(nodes, nodeToExecuteData) logger.debug( `[server]: Reuse existing chatflow ${chatflowid} with ending node ${nodeToExecuteData.label} (${nodeToExecuteData.id})` @@ -1372,79 +1649,101 @@ export class App { /*** Get Ending Node with Directed Graph ***/ const { graph, nodeDependencies } = constructGraphs(nodes, edges) const directedGraph = graph - const endingNodeId = getEndingNode(nodeDependencies, directedGraph) - if (!endingNodeId) return res.status(500).send(`Ending node ${endingNodeId} not found`) + const endingNodeIds = getEndingNodes(nodeDependencies, directedGraph) + if (!endingNodeIds.length) return res.status(500).send(`Ending nodes not found`) - const endingNodeData = nodes.find((nd) => nd.id === endingNodeId)?.data - if (!endingNodeData) return res.status(500).send(`Ending node ${endingNodeId} data not found`) + const endingNodes = nodes.filter((nd) => endingNodeIds.includes(nd.id)) + for (const endingNode of endingNodes) { + const endingNodeData = endingNode.data + if (!endingNodeData) return res.status(500).send(`Ending node ${endingNode.id} data not found`) - if ( - endingNodeData && - endingNodeData.category !== 'Chains' && - endingNodeData.category !== 'Agents' && - endingNodeData.category !== 'Engine' && - !isUpsert - ) { - return res.status(500).send(`Ending node must be either a Chain or Agent`) + if ( + endingNodeData && + endingNodeData.category !== 'Chains' && + endingNodeData.category !== 'Agents' && + endingNodeData.category !== 'Engine' + ) { + return res.status(500).send(`Ending node must be either a Chain or Agent`) + } + + if ( + endingNodeData.outputs && + Object.keys(endingNodeData.outputs).length && + !Object.values(endingNodeData.outputs).includes(endingNodeData.name) + ) { + return res + .status(500) + .send( + `Output of ${endingNodeData.label} (${endingNodeData.id}) must be ${endingNodeData.label}, can't be an Output Prediction` + ) + } + + isStreamValid = isFlowValidForStream(nodes, endingNodeData) } - if ( - endingNodeData.outputs && - Object.keys(endingNodeData.outputs).length && - !Object.values(endingNodeData.outputs).includes(endingNodeData.name) && - !isUpsert - ) { - return res - .status(500) - .send( - `Output of ${endingNodeData.label} (${endingNodeData.id}) must be ${endingNodeData.label}, can't be an Output Prediction` - ) - } + let chatHistory: IMessage[] = incomingInput.history ?? [] - isStreamValid = isFlowValidForStream(nodes, endingNodeData) + // When {{chat_history}} is used in Prompt Template, fetch the chat conversations from memory node + for (const endingNode of endingNodes) { + const endingNodeData = endingNode.data - let chatHistory: IMessage[] = incomingInput.history + if (!endingNodeData.inputs?.memory) continue - // If chatHistory is empty, and sessionId/chatId is present, replace it - if ( - endingNodeData.inputs?.memory && - !incomingInput.history && - (incomingInput.chatId || incomingInput.overrideConfig?.sessionId) - ) { const memoryNodeId = endingNodeData.inputs?.memory.split('.')[0].replace('{{', '') const memoryNode = nodes.find((node) => node.data.id === memoryNodeId) - if (memoryNode) { - chatHistory = await replaceChatHistory(memoryNode, incomingInput, this.AppDataSource, databaseEntities, logger) + + if (!memoryNode) continue + + if (!chatHistory.length && (incomingInput.chatId || incomingInput.overrideConfig?.sessionId)) { + chatHistory = await getSessionChatHistory( + memoryNode, + this.nodesPool.componentNodes, + incomingInput, + this.AppDataSource, + databaseEntities, + logger + ) } } - /*** Get Starting Nodes with Non-Directed Graph ***/ - const constructedObj = constructGraphs(nodes, edges, true) + /*** Get Starting Nodes with Reversed Graph ***/ + const constructedObj = constructGraphs(nodes, edges, { isReversed: true }) const nonDirectedGraph = constructedObj.graph - const { startingNodeIds, depthQueue } = getStartingNodes(nonDirectedGraph, endingNodeId) + let startingNodeIds: string[] = [] + let depthQueue: IDepthQueue = {} + for (const endingNodeId of endingNodeIds) { + const res = getStartingNodes(nonDirectedGraph, endingNodeId) + startingNodeIds.push(...res.startingNodeIds) + depthQueue = Object.assign(depthQueue, res.depthQueue) + } + startingNodeIds = [...new Set(startingNodeIds)] + + const startingNodes = nodes.filter((nd) => startingNodeIds.includes(nd.id)) logger.debug(`[server]: Start building chatflow ${chatflowid}`) /*** BFS to traverse from Starting Nodes to Ending Node ***/ const reactFlowNodes = await buildLangchain( startingNodeIds, nodes, + edges, graph, depthQueue, this.nodesPool.componentNodes, incomingInput.question, chatHistory, chatId, + sessionId ?? '', chatflowid, this.AppDataSource, incomingInput?.overrideConfig, - this.cachePool, - isUpsert, - incomingInput.stopNodeId + this.cachePool ) - if (isUpsert) return res.status(201).send('Successfully Upserted') - const nodeToExecute = reactFlowNodes.find((node: IReactFlowNode) => node.id === endingNodeId) - if (!nodeToExecute) return res.status(404).send(`Node ${endingNodeId} not found`) + const nodeToExecute = + endingNodeIds.length === 1 + ? reactFlowNodes.find((node: IReactFlowNode) => endingNodeIds[0] === node.id) + : reactFlowNodes[reactFlowNodes.length - 1] + if (!nodeToExecute) return res.status(404).send(`Node not found`) if (incomingInput.overrideConfig) { nodeToExecute.data = replaceInputsWithConfig(nodeToExecute.data, incomingInput.overrideConfig) @@ -1458,37 +1757,35 @@ export class App { ) nodeToExecuteData = reactFlowNodeData - const startingNodes = nodes.filter((nd) => startingNodeIds.includes(nd.id)) this.chatflowPool.add(chatflowid, nodeToExecuteData, startingNodes, incomingInput?.overrideConfig) } - const nodeInstanceFilePath = this.nodesPool.componentNodes[nodeToExecuteData.name].filePath as string - const nodeModule = await import(nodeInstanceFilePath) - const nodeInstance = new nodeModule.nodeClass() - logger.debug(`[server]: Running ${nodeToExecuteData.label} (${nodeToExecuteData.id})`) - let sessionId = undefined - if (nodeToExecuteData.instance) sessionId = replaceMemorySessionId(nodeToExecuteData.instance, chatId) + const nodeInstanceFilePath = this.nodesPool.componentNodes[nodeToExecuteData.name].filePath as string + const nodeModule = await import(nodeInstanceFilePath) + const nodeInstance = new nodeModule.nodeClass({ sessionId }) let result = isStreamValid ? await nodeInstance.run(nodeToExecuteData, incomingInput.question, { + chatId, + chatflowid, chatHistory: incomingInput.history, - socketIO, - socketIOClientId: incomingInput.socketIOClientId, logger, appDataSource: this.AppDataSource, databaseEntities, analytic: chatflow.analytic, - chatId + socketIO, + socketIOClientId: incomingInput.socketIOClientId }) : await nodeInstance.run(nodeToExecuteData, incomingInput.question, { + chatId, + chatflowid, chatHistory: incomingInput.history, logger, appDataSource: this.AppDataSource, databaseEntities, - analytic: chatflow.analytic, - chatId + analytic: chatflow.analytic }) result = typeof result === 'string' ? { text: result } : result @@ -1498,9 +1795,6 @@ export class App { sessionId = result.assistant.threadId } - const memoryNode = this.findMemoryLabel(nodes, edges) - const memoryType = memoryNode?.data.label - const userMessage: Omit = { role: 'userMessage', content: incomingInput.question, @@ -1533,9 +1827,18 @@ export class App { await this.addChatMessage(apiMessage) logger.debug(`[server]: Finished running ${nodeToExecuteData.label} (${nodeToExecuteData.id})`) + await this.telemetry.sendTelemetry('prediction_sent', { + version: await getAppVersion(), + chatlowId: chatflowid, + chatId, + type: isInternal ? chatType.INTERNAL : chatType.EXTERNAL, + flowGraph: getTelemetryFlowObj(nodes, edges) + }) - // Only return ChatId when its Internal OR incoming input has ChatId, to avoid confusion when calling API - if (incomingInput.chatId || isInternal) result.chatId = chatId + // Prepare response + result.chatId = chatId + if (sessionId) result.sessionId = sessionId + if (memoryType) result.memoryType = memoryType return res.json(result) } catch (e: any) { @@ -1547,6 +1850,7 @@ export class App { async stopApp() { try { const removePromises: any[] = [] + removePromises.push(this.telemetry.flush()) await Promise.all(removePromises) } catch (e) { logger.error(`❌[server]: Flowise Server shut down error: ${e}`) @@ -1554,23 +1858,6 @@ export class App { } } -/** - * Get first chat message id - * @param {string} chatflowid - * @returns {string} - */ -export async function getChatId(chatflowid: string): Promise { - // first chatmessage id as the unique chat id - const firstChatMessage = await getDataSource() - .getRepository(ChatMessage) - .createQueryBuilder('cm') - .select('cm.id') - .where('chatflowid = :chatflowid', { chatflowid }) - .orderBy('cm.createdDate', 'ASC') - .getOne() - return firstChatMessage ? firstChatMessage.id : '' -} - let serverApp: App | undefined export async function getAllChatFlow(): Promise { diff --git a/packages/server/src/utils/XSS.ts b/packages/server/src/utils/XSS.ts new file mode 100644 index 00000000..5d8b81e9 --- /dev/null +++ b/packages/server/src/utils/XSS.ts @@ -0,0 +1,20 @@ +import { Request, Response, NextFunction } from 'express' +import sanitizeHtml from 'sanitize-html' + +export function sanitizeMiddleware(req: Request, res: Response, next: NextFunction): void { + // decoding is necessary as the url is encoded by the browser + const decodedURI = decodeURI(req.url) + req.url = sanitizeHtml(decodedURI) + for (let p in req.query) { + if (Array.isArray(req.query[p])) { + const sanitizedQ = [] + for (const q of req.query[p] as string[]) { + sanitizedQ.push(sanitizeHtml(q)) + } + req.query[p] = sanitizedQ + } else { + req.query[p] = sanitizeHtml(req.query[p] as string) + } + } + next() +} diff --git a/packages/server/src/utils/hub.ts b/packages/server/src/utils/hub.ts new file mode 100644 index 00000000..9a324228 --- /dev/null +++ b/packages/server/src/utils/hub.ts @@ -0,0 +1,36 @@ +export function parsePrompt(prompt: string): any[] { + const promptObj = JSON.parse(prompt) + let response = [] + if (promptObj.kwargs.messages) { + promptObj.kwargs.messages.forEach((message: any) => { + let messageType = message.id.includes('SystemMessagePromptTemplate') + ? 'systemMessagePrompt' + : message.id.includes('HumanMessagePromptTemplate') + ? 'humanMessagePrompt' + : message.id.includes('AIMessagePromptTemplate') + ? 'aiMessagePrompt' + : 'template' + let messageTypeDisplay = message.id.includes('SystemMessagePromptTemplate') + ? 'System Message' + : message.id.includes('HumanMessagePromptTemplate') + ? 'Human Message' + : message.id.includes('AIMessagePromptTemplate') + ? 'AI Message' + : 'Message' + let template = message.kwargs.prompt.kwargs.template + response.push({ + type: messageType, + typeDisplay: messageTypeDisplay, + template: template + }) + }) + } else if (promptObj.kwargs.template) { + let template = promptObj.kwargs.template + response.push({ + type: 'template', + typeDisplay: 'Prompt', + template: template + }) + } + return response +} diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index 0b37b608..d0063343 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -23,9 +23,11 @@ import { convertChatHistoryToText, getInputVariables, handleEscapeCharacters, + getEncryptionKeyPath, ICommonObject, IDatabaseEntity, - IMessage + IMessage, + FlowiseMemory } from 'flowise-components' import { randomBytes } from 'crypto' import { AES, enc } from 'crypto-js' @@ -37,6 +39,7 @@ import { Tool } from '../database/entities/Tool' import { Assistant } from '../database/entities/Assistant' import { DataSource } from 'typeorm' import { CachePool } from '../CachePool' +import { Variable } from '../database/entities/Variable' const QUESTION_VAR_PREFIX = 'question' const CHAT_HISTORY_VAR_PREFIX = 'chat_history' @@ -47,7 +50,8 @@ export const databaseEntities: IDatabaseEntity = { ChatMessage: ChatMessage, Tool: Tool, Credential: Credential, - Assistant: Assistant + Assistant: Assistant, + Variable: Variable } /** @@ -95,9 +99,13 @@ export const getNodeModulesPackagePath = (packageName: string): string => { * Construct graph and node dependencies score * @param {IReactFlowNode[]} reactFlowNodes * @param {IReactFlowEdge[]} reactFlowEdges - * @param {boolean} isNondirected + * @param {{ isNonDirected?: boolean, isReversed?: boolean }} options */ -export const constructGraphs = (reactFlowNodes: IReactFlowNode[], reactFlowEdges: IReactFlowEdge[], isNondirected = false) => { +export const constructGraphs = ( + reactFlowNodes: IReactFlowNode[], + reactFlowEdges: IReactFlowEdge[], + options?: { isNonDirected?: boolean; isReversed?: boolean } +) => { const nodeDependencies = {} as INodeDependencies const graph = {} as INodeDirectedGraph @@ -107,6 +115,23 @@ export const constructGraphs = (reactFlowNodes: IReactFlowNode[], reactFlowEdges graph[nodeId] = [] } + if (options && options.isReversed) { + for (let i = 0; i < reactFlowEdges.length; i += 1) { + const source = reactFlowEdges[i].source + const target = reactFlowEdges[i].target + + if (Object.prototype.hasOwnProperty.call(graph, target)) { + graph[target].push(source) + } else { + graph[target] = [source] + } + + nodeDependencies[target] += 1 + } + + return { graph, nodeDependencies } + } + for (let i = 0; i < reactFlowEdges.length; i += 1) { const source = reactFlowEdges[i].source const target = reactFlowEdges[i].target @@ -117,7 +142,7 @@ export const constructGraphs = (reactFlowNodes: IReactFlowNode[], reactFlowEdges graph[source] = [target] } - if (isNondirected) { + if (options && options.isNonDirected) { if (Object.prototype.hasOwnProperty.call(graph, target)) { graph[target].push(source) } else { @@ -179,21 +204,49 @@ export const getStartingNodes = (graph: INodeDirectedGraph, endNodeId: string) = return { startingNodeIds, depthQueue: depthQueueReversed } } +/** + * Get all connected nodes from startnode + * @param {INodeDependencies} graph + * @param {string} startNodeId + */ +export const getAllConnectedNodes = (graph: INodeDirectedGraph, startNodeId: string) => { + const visited = new Set() + const queue: Array<[string]> = [[startNodeId]] + + while (queue.length > 0) { + const [currentNode] = queue.shift()! + + if (visited.has(currentNode)) { + continue + } + + visited.add(currentNode) + + for (const neighbor of graph[currentNode]) { + if (!visited.has(neighbor)) { + queue.push([neighbor]) + } + } + } + + return [...visited] +} + /** * Get ending node and check if flow is valid * @param {INodeDependencies} nodeDependencies * @param {INodeDirectedGraph} graph */ -export const getEndingNode = (nodeDependencies: INodeDependencies, graph: INodeDirectedGraph) => { - let endingNodeId = '' +export const getEndingNodes = (nodeDependencies: INodeDependencies, graph: INodeDirectedGraph) => { + const endingNodeIds: string[] = [] Object.keys(graph).forEach((nodeId) => { if (Object.keys(nodeDependencies).length === 1) { - endingNodeId = nodeId + endingNodeIds.push(nodeId) } else if (!graph[nodeId].length && nodeDependencies[nodeId] > 0) { - endingNodeId = nodeId + endingNodeIds.push(nodeId) } }) - return endingNodeId + return endingNodeIds } /** @@ -213,12 +266,14 @@ export const getEndingNode = (nodeDependencies: INodeDependencies, graph: INodeD export const buildLangchain = async ( startingNodeIds: string[], reactFlowNodes: IReactFlowNode[], + reactFlowEdges: IReactFlowEdge[], graph: INodeDirectedGraph, depthQueue: IDepthQueue, componentNodes: IComponentNodes, question: string, chatHistory: IMessage[], chatId: string, + sessionId: string, chatflowid: string, appDataSource: DataSource, overrideConfig?: ICommonObject, @@ -231,6 +286,8 @@ export const buildLangchain = async ( // Create a Queue and add our initial node in it const nodeQueue = [] as INodeQueue[] const exploredNode = {} as IExploredNode + const dynamicVariables = {} as Record + let ignoreNodeIds: string[] = [] // In the case of infinite loop, only max 3 loops will be executed const maxLoop = 3 @@ -256,31 +313,72 @@ export const buildLangchain = async ( if (overrideConfig) flowNodeData = replaceInputsWithConfig(flowNodeData, overrideConfig) const reactFlowNodeData: INodeData = resolveVariables(flowNodeData, flowNodes, question, chatHistory) - if ( - isUpsert && - ((stopNodeId && reactFlowNodeData.id === stopNodeId) || (!stopNodeId && reactFlowNodeData.category === 'Vector Stores')) - ) { + // TODO: Avoid processing Text Splitter + Doc Loader once Upsert & Load Existing Vector Nodes are deprecated + if (isUpsert && stopNodeId && nodeId === stopNodeId) { logger.debug(`[server]: Upserting ${reactFlowNode.data.label} (${reactFlowNode.data.id})`) await newNodeInstance.vectorStoreMethods!['upsert']!.call(newNodeInstance, reactFlowNodeData, { chatId, + sessionId, chatflowid, + chatHistory, + logger, appDataSource, databaseEntities, - logger, - cachePool + cachePool, + dynamicVariables }) logger.debug(`[server]: Finished upserting ${reactFlowNode.data.label} (${reactFlowNode.data.id})`) break } else { logger.debug(`[server]: Initializing ${reactFlowNode.data.label} (${reactFlowNode.data.id})`) - flowNodes[nodeIndex].data.instance = await newNodeInstance.init(reactFlowNodeData, question, { + let outputResult = await newNodeInstance.init(reactFlowNodeData, question, { chatId, + sessionId, chatflowid, + chatHistory, + logger, appDataSource, databaseEntities, - logger, - cachePool + cachePool, + dynamicVariables }) + + // Save dynamic variables + if (reactFlowNode.data.name === 'setVariable') { + const dynamicVars = outputResult?.dynamicVariables ?? {} + + for (const variableKey in dynamicVars) { + dynamicVariables[variableKey] = dynamicVars[variableKey] + } + + outputResult = outputResult?.output + } + + // Determine which nodes to route next when it comes to ifElse + if (reactFlowNode.data.name === 'ifElseFunction' && typeof outputResult === 'object') { + let sourceHandle = '' + if (outputResult.type === true) { + sourceHandle = `${nodeId}-output-returnFalse-string|number|boolean|json|array` + } else if (outputResult.type === false) { + sourceHandle = `${nodeId}-output-returnTrue-string|number|boolean|json|array` + } + + const ifElseEdge = reactFlowEdges.find((edg) => edg.source === nodeId && edg.sourceHandle === sourceHandle) + if (ifElseEdge) { + const { graph } = constructGraphs( + reactFlowNodes, + reactFlowEdges.filter((edg) => !(edg.source === nodeId && edg.sourceHandle === sourceHandle)), + { isNonDirected: true } + ) + ignoreNodeIds.push(ifElseEdge.target, ...getAllConnectedNodes(graph, ifElseEdge.target)) + ignoreNodeIds = [...new Set(ignoreNodeIds)] + } + + outputResult = outputResult?.output + } + + flowNodes[nodeIndex].data.instance = outputResult + logger.debug(`[server]: Finished initializing ${reactFlowNode.data.label} (${reactFlowNode.data.id})`) } } catch (e: any) { @@ -288,7 +386,7 @@ export const buildLangchain = async ( throw new Error(e) } - const neighbourNodeIds = graph[nodeId] + let neighbourNodeIds = graph[nodeId] const nextDepth = depth + 1 // Find other nodes that are on the same depth level @@ -299,9 +397,11 @@ export const buildLangchain = async ( neighbourNodeIds.push(id) } + neighbourNodeIds = neighbourNodeIds.filter((neigh) => !ignoreNodeIds.includes(neigh)) + for (let i = 0; i < neighbourNodeIds.length; i += 1) { const neighNodeId = neighbourNodeIds[i] - + if (ignoreNodeIds.includes(neighNodeId)) continue // If nodeId has been seen, cycle detected if (Object.prototype.hasOwnProperty.call(exploredNode, neighNodeId)) { const { remainingLoop, lastSeenDepth } = exploredNode[neighNodeId] @@ -319,6 +419,12 @@ export const buildLangchain = async ( nodeQueue.push({ nodeId: neighNodeId, depth: nextDepth }) } } + + // Move end node to last + if (!neighbourNodeIds.length) { + const index = flowNodes.findIndex((nd) => nd.data.id === nodeId) + flowNodes.push(flowNodes.splice(index, 1)[0]) + } } return flowNodes } @@ -351,15 +457,25 @@ export const clearSessionMemory = async ( const nodeInstanceFilePath = componentNodes[node.data.name].filePath as string const nodeModule = await import(nodeInstanceFilePath) const newNodeInstance = new nodeModule.nodeClass() + const options: ICommonObject = { chatId, appDataSource, databaseEntities, logger } + // SessionId always take priority first because it is the sessionId used for 3rd party memory node if (sessionId && node.data.inputs) { - node.data.inputs.sessionId = sessionId - } - - const initializedInstance = await newNodeInstance.init(node.data, '', { chatId, appDataSource, databaseEntities, logger }) - - if (initializedInstance.clearChatMessages) { - await initializedInstance.clearChatMessages() + if (node.data.type === 'OpenAIAssistant') { + await newNodeInstance.clearChatMessages(node.data, options, { type: 'threadId', id: sessionId }) + } else { + node.data.inputs.sessionId = sessionId + const initializedInstance: FlowiseMemory = await newNodeInstance.init(node.data, '', options) + await initializedInstance.clearChatMessages(sessionId) + } + } else if (chatId && node.data.inputs) { + if (node.data.type === 'OpenAIAssistant') { + await newNodeInstance.clearChatMessages(node.data, options, { type: 'chatId', id: chatId }) + } else { + node.data.inputs.sessionId = chatId + const initializedInstance: FlowiseMemory = await newNodeInstance.init(node.data, '', options) + await initializedInstance.clearChatMessages(chatId) + } } } } @@ -434,7 +550,11 @@ export const getVariableValue = ( variablePaths.forEach((path) => { const variableValue = variableDict[path] // Replace all occurrence - returnVal = returnVal.split(path).join(variableValue) + if (typeof variableValue === 'object') { + returnVal = returnVal.split(path).join(JSON.stringify(variableValue).replace(/"/g, '\\"')) + } else { + returnVal = returnVal.split(path).join(variableValue) + } }) return returnVal } @@ -533,7 +653,18 @@ export const isStartNodeDependOnInput = (startingNodes: IReactFlowNode[], nodes: } const whitelistNodeNames = ['vectorStoreToDocument', 'autoGPT', 'chatPromptTemplate', 'promptTemplate'] //If these nodes are found, chatflow cannot be reused for (const node of nodes) { - if (whitelistNodeNames.includes(node.data.name)) return true + if (node.data.name === 'chatPromptTemplate' || node.data.name === 'promptTemplate') { + let promptValues: ICommonObject = {} + const promptValuesRaw = node.data.inputs?.promptValues + if (promptValuesRaw) { + try { + promptValues = typeof promptValuesRaw === 'object' ? promptValuesRaw : JSON.parse(promptValuesRaw) + } catch (exception) { + console.error(exception) + } + } + if (getAllValuesFromJson(promptValues).includes(`{{${QUESTION_VAR_PREFIX}}}`)) return true + } else if (whitelistNodeNames.includes(node.data.name)) return true } return false } @@ -673,6 +804,7 @@ export const findAvailableConfigs = (reactFlowNodes: IReactFlowNode[], component /** * Check to see if flow valid for stream + * TODO: perform check from component level. i.e: set streaming on component, and check here * @param {IReactFlowNode[]} reactFlowNodes * @param {INodeData} endingNodeData * @returns {boolean} @@ -686,7 +818,8 @@ export const isFlowValidForStream = (reactFlowNodes: IReactFlowNode[], endingNod 'chatAnthropic', 'chatAnthropic_LlamaIndex', 'chatOllama', - 'awsChatBedrock' + 'awsChatBedrock', + 'chatMistralAI' ], LLMs: ['azureOpenAI', 'openAI', 'ollama'] } @@ -711,7 +844,7 @@ export const isFlowValidForStream = (reactFlowNodes: IReactFlowNode[], endingNod const whitelistAgents = ['openAIFunctionAgent', 'csvAgent', 'airtableAgent', 'conversationalRetrievalAgent'] isValidChainOrAgent = whitelistAgents.includes(endingNodeData.name) } else if (endingNodeData.category === 'Engine') { - const whitelistEngine = ['contextChatEngine', 'simpleChatEngine'] + const whitelistEngine = ['contextChatEngine', 'simpleChatEngine', 'queryEngine'] isValidChainOrAgent = whitelistEngine.includes(endingNodeData.name) } @@ -727,16 +860,6 @@ export const isFlowValidForStream = (reactFlowNodes: IReactFlowNode[], endingNod return isChatOrLLMsExist && isValidChainOrAgent && !isOutputParserExist } -/** - * Returns the path of encryption key - * @returns {string} - */ -export const getEncryptionKeyPath = (): string => { - return process.env.SECRETKEY_PATH - ? path.join(process.env.SECRETKEY_PATH, 'encryption.key') - : path.join(__dirname, '..', '..', 'encryption.key') -} - /** * Generate an encryption key * @returns {string} @@ -757,7 +880,10 @@ export const getEncryptionKey = async (): Promise => { return await fs.promises.readFile(getEncryptionKeyPath(), 'utf8') } catch (error) { const encryptKey = generateEncryptKey() - await fs.promises.writeFile(getEncryptionKeyPath(), encryptKey) + const defaultLocation = process.env.SECRETKEY_PATH + ? path.join(process.env.SECRETKEY_PATH, 'encryption.key') + : path.join(getUserHome(), '.flowise', 'encryption.key') + await fs.promises.writeFile(defaultLocation, encryptKey) return encryptKey } } @@ -845,21 +971,43 @@ export const redactCredentialWithPasswordType = ( } /** - * Replace sessionId with new chatId - * Ex: after clear chat history, use the new chatId as sessionId + * Get sessionId + * Hierarchy of sessionId (top down) + * API/Embed: + * (1) Provided in API body - incomingInput.overrideConfig: { sessionId: 'abc' } + * (2) Provided in API body - incomingInput.chatId + * + * API/Embed + UI: + * (3) Hard-coded sessionId in UI + * (4) Not specified on UI nor API, default to chatId * @param {any} instance + * @param {IncomingInput} incomingInput * @param {string} chatId */ -export const replaceMemorySessionId = (instance: any, chatId: string): string | undefined => { - if (instance.memory && instance.memory.isSessionIdUsingChatMessageId && chatId) { - instance.memory.sessionId = chatId - instance.memory.chatHistory.sessionId = chatId +export const getMemorySessionId = ( + memoryNode: IReactFlowNode, + incomingInput: IncomingInput, + chatId: string, + isInternal: boolean +): string | undefined => { + if (!isInternal) { + // Provided in API body - incomingInput.overrideConfig: { sessionId: 'abc' } + if (incomingInput.overrideConfig?.sessionId) { + return incomingInput.overrideConfig?.sessionId + } + // Provided in API body - incomingInput.chatId + if (incomingInput.chatId) { + return incomingInput.chatId + } } - if (instance.memory && instance.memory.sessionId) return instance.memory.sessionId - else if (instance.memory && instance.memory.chatHistory && instance.memory.chatHistory.sessionId) - return instance.memory.chatHistory.sessionId - return undefined + // Hard-coded sessionId in UI + if (memoryNode.data.inputs?.sessionId) { + return memoryNode.data.inputs.sessionId + } + + // Default chatId + return chatId } /** @@ -871,27 +1019,135 @@ export const replaceMemorySessionId = (instance: any, chatId: string): string | * @param {any} logger * @returns {string} */ -export const replaceChatHistory = async ( +export const getSessionChatHistory = async ( memoryNode: IReactFlowNode, + componentNodes: IComponentNodes, incomingInput: IncomingInput, appDataSource: DataSource, databaseEntities: IDatabaseEntity, logger: any ): Promise => { - const nodeInstanceFilePath = memoryNode.data.filePath as string + const nodeInstanceFilePath = componentNodes[memoryNode.data.name].filePath as string const nodeModule = await import(nodeInstanceFilePath) const newNodeInstance = new nodeModule.nodeClass() + // Replace memory's sessionId/chatId if (incomingInput.overrideConfig?.sessionId && memoryNode.data.inputs) { memoryNode.data.inputs.sessionId = incomingInput.overrideConfig.sessionId + } else if (incomingInput.chatId && memoryNode.data.inputs) { + memoryNode.data.inputs.sessionId = incomingInput.chatId } - const initializedInstance = await newNodeInstance.init(memoryNode.data, '', { - chatId: incomingInput.chatId, + const initializedInstance: FlowiseMemory = await newNodeInstance.init(memoryNode.data, '', { appDataSource, databaseEntities, logger }) - return await initializedInstance.getChatMessages() + return (await initializedInstance.getChatMessages()) as IMessage[] +} + +/** + * Method that find memory that is connected within chatflow + * In a chatflow, there should only be 1 memory node + * @param {IReactFlowNode[]} nodes + * @param {IReactFlowEdge[]} edges + * @returns {string | undefined} + */ +export const findMemoryNode = (nodes: IReactFlowNode[], edges: IReactFlowEdge[]): IReactFlowNode | undefined => { + const memoryNodes = nodes.filter((node) => node.data.category === 'Memory') + const memoryNodeIds = memoryNodes.map((mem) => mem.data.id) + + for (const edge of edges) { + if (memoryNodeIds.includes(edge.source)) { + const memoryNode = nodes.find((node) => node.data.id === edge.source) + return memoryNode + } + } + return undefined +} + +/** + * Get all values from a JSON object + * @param {any} obj + * @returns {any[]} + */ +export const getAllValuesFromJson = (obj: any): any[] => { + const values: any[] = [] + + function extractValues(data: any) { + if (typeof data === 'object' && data !== null) { + if (Array.isArray(data)) { + for (const item of data) { + extractValues(item) + } + } else { + for (const key in data) { + extractValues(data[key]) + } + } + } else { + values.push(data) + } + } + + extractValues(obj) + return values +} + +/** + * Get only essential flow data items for telemetry + * @param {IReactFlowNode[]} nodes + * @param {IReactFlowEdge[]} edges + */ +export const getTelemetryFlowObj = (nodes: IReactFlowNode[], edges: IReactFlowEdge[]) => { + const nodeData = nodes.map((node) => node.id) + const edgeData = edges.map((edge) => ({ source: edge.source, target: edge.target })) + return { nodes: nodeData, edges: edgeData } +} + +/** + * Get user settings file + * TODO: move env variables to settings json file, easier configuration + */ +export const getUserSettingsFilePath = () => { + if (process.env.SECRETKEY_PATH) return path.join(process.env.SECRETKEY_PATH, 'settings.json') + const checkPaths = [path.join(getUserHome(), '.flowise', 'settings.json')] + for (const checkPath of checkPaths) { + if (fs.existsSync(checkPath)) { + return checkPath + } + } + return '' +} + +/** + * Get app current version + */ +export const getAppVersion = async () => { + const getPackageJsonPath = (): string => { + const checkPaths = [ + path.join(__dirname, '..', 'package.json'), + path.join(__dirname, '..', '..', 'package.json'), + path.join(__dirname, '..', '..', '..', 'package.json'), + path.join(__dirname, '..', '..', '..', '..', 'package.json'), + path.join(__dirname, '..', '..', '..', '..', '..', 'package.json') + ] + for (const checkPath of checkPaths) { + if (fs.existsSync(checkPath)) { + return checkPath + } + } + return '' + } + + const packagejsonPath = getPackageJsonPath() + if (!packagejsonPath) return '' + try { + const content = await fs.promises.readFile(packagejsonPath, 'utf8') + const parsedContent = JSON.parse(content) + return parsedContent.version + } catch (error) { + return '' + } } diff --git a/packages/server/src/utils/telemetry.ts b/packages/server/src/utils/telemetry.ts new file mode 100644 index 00000000..4b033f20 --- /dev/null +++ b/packages/server/src/utils/telemetry.ts @@ -0,0 +1,52 @@ +import { v4 as uuidv4 } from 'uuid' +import { PostHog } from 'posthog-node' +import path from 'path' +import fs from 'fs' +import { getUserHome, getUserSettingsFilePath } from '.' + +export class Telemetry { + postHog?: PostHog + + constructor() { + if (process.env.DISABLE_FLOWISE_TELEMETRY !== 'true') { + this.postHog = new PostHog('phc_jEDuFYnOnuXsws986TLWzuisbRjwFqTl9JL8tDMgqme') + } else { + this.postHog = undefined + } + } + + async id(): Promise { + try { + const settingsContent = await fs.promises.readFile(getUserSettingsFilePath(), 'utf8') + const settings = JSON.parse(settingsContent) + return settings.instanceId + } catch (error) { + const instanceId = uuidv4() + const settings = { + instanceId + } + const defaultLocation = process.env.SECRETKEY_PATH + ? path.join(process.env.SECRETKEY_PATH, 'settings.json') + : path.join(getUserHome(), '.flowise', 'settings.json') + await fs.promises.writeFile(defaultLocation, JSON.stringify(settings, null, 2)) + return instanceId + } + } + + async sendTelemetry(event: string, properties = {}): Promise { + if (this.postHog) { + const distinctId = await this.id() + this.postHog.capture({ + event, + distinctId, + properties + }) + } + } + + async flush(): Promise { + if (this.postHog) { + await this.postHog.shutdownAsync() + } + } +} diff --git a/packages/ui/package.json b/packages/ui/package.json index 76369ab2..fef08851 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "flowise-ui", - "version": "1.4.1", + "version": "1.4.7", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://flowiseai.com", "author": { @@ -8,13 +8,20 @@ "email": "henryheng@flowiseai.com" }, "dependencies": { + "@codemirror/lang-javascript": "^6.2.1", + "@codemirror/lang-json": "^6.0.1", + "@codemirror/view": "^6.22.3", "@emotion/cache": "^11.4.0", "@emotion/react": "^11.10.6", "@emotion/styled": "^11.10.6", "@mui/icons-material": "^5.0.3", - "@mui/material": "^5.11.12", + "@mui/lab": "^5.0.0-alpha.156", + "@mui/material": "^5.15.0", "@mui/x-data-grid": "^6.8.0", "@tabler/icons": "^1.39.1", + "@uiw/codemirror-theme-sublime": "^4.21.21", + "@uiw/codemirror-theme-vscode": "^4.21.21", + "@uiw/react-codemirror": "^4.21.21", "clsx": "^1.1.1", "flowise-embed": "*", "flowise-embed-react": "*", @@ -26,7 +33,6 @@ "lodash": "^4.17.21", "moment": "^2.29.3", "notistack": "^2.0.4", - "prismjs": "^1.28.0", "prop-types": "^15.7.2", "react": "^18.2.0", "react-code-blocks": "^0.0.9-0", @@ -39,7 +45,6 @@ "react-redux": "^8.0.5", "react-router": "~6.3.0", "react-router-dom": "~6.3.0", - "react-simple-code-editor": "^0.11.2", "react-syntax-highlighter": "^15.5.0", "reactflow": "^11.5.6", "redux": "^4.0.5", diff --git a/packages/ui/src/api/nodes.js b/packages/ui/src/api/nodes.js index 7eb4c351..3b7eacc5 100644 --- a/packages/ui/src/api/nodes.js +++ b/packages/ui/src/api/nodes.js @@ -4,7 +4,10 @@ const getAllNodes = () => client.get('/nodes') const getSpecificNode = (name) => client.get(`/nodes/${name}`) +const executeCustomFunctionNode = (body) => client.post(`/node-custom-function`, body) + export default { getAllNodes, - getSpecificNode + getSpecificNode, + executeCustomFunctionNode } diff --git a/packages/ui/src/api/prompt.js b/packages/ui/src/api/prompt.js new file mode 100644 index 00000000..42b1bdbc --- /dev/null +++ b/packages/ui/src/api/prompt.js @@ -0,0 +1,9 @@ +import client from './client' + +const getAvailablePrompts = (body) => client.post(`/prompts-list`, body) +const getPrompt = (body) => client.post(`/load-prompt`, body) + +export default { + getAvailablePrompts, + getPrompt +} diff --git a/packages/ui/src/api/variables.js b/packages/ui/src/api/variables.js new file mode 100644 index 00000000..944b8319 --- /dev/null +++ b/packages/ui/src/api/variables.js @@ -0,0 +1,16 @@ +import client from './client' + +const getAllVariables = () => client.get('/variables') + +const createVariable = (body) => client.post(`/variables`, body) + +const updateVariable = (id, body) => client.put(`/variables/${id}`, body) + +const deleteVariable = (id) => client.delete(`/variables/${id}`) + +export default { + getAllVariables, + createVariable, + updateVariable, + deleteVariable +} diff --git a/packages/ui/src/assets/images/langfuse.svg b/packages/ui/src/assets/images/langfuse.svg new file mode 100644 index 00000000..50b6bd74 --- /dev/null +++ b/packages/ui/src/assets/images/langfuse.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/ui/src/assets/images/lunary.svg b/packages/ui/src/assets/images/lunary.svg new file mode 100644 index 00000000..1528de5a --- /dev/null +++ b/packages/ui/src/assets/images/lunary.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/ui/src/assets/images/prompt_empty.svg b/packages/ui/src/assets/images/prompt_empty.svg new file mode 100644 index 00000000..61df7e32 --- /dev/null +++ b/packages/ui/src/assets/images/prompt_empty.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/ui/src/assets/images/variables_empty.svg b/packages/ui/src/assets/images/variables_empty.svg new file mode 100644 index 00000000..eb461e39 --- /dev/null +++ b/packages/ui/src/assets/images/variables_empty.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/ui/src/menu-items/dashboard.js b/packages/ui/src/menu-items/dashboard.js index 8bf5b392..793bc290 100644 --- a/packages/ui/src/menu-items/dashboard.js +++ b/packages/ui/src/menu-items/dashboard.js @@ -1,8 +1,8 @@ // assets -import { IconHierarchy, IconBuildingStore, IconKey, IconTool, IconLock, IconRobot } from '@tabler/icons' +import { IconHierarchy, IconBuildingStore, IconKey, IconTool, IconLock, IconRobot, IconVariable } from '@tabler/icons' // constant -const icons = { IconHierarchy, IconBuildingStore, IconKey, IconTool, IconLock, IconRobot } +const icons = { IconHierarchy, IconBuildingStore, IconKey, IconTool, IconLock, IconRobot, IconVariable } // ==============================|| DASHBOARD MENU ITEMS ||============================== // @@ -51,6 +51,14 @@ const dashboard = { icon: icons.IconLock, breadcrumbs: true }, + { + id: 'variables', + title: 'Variables', + type: 'item', + url: '/variables', + icon: icons.IconVariable, + breadcrumbs: true + }, { id: 'apikey', title: 'API Keys', diff --git a/packages/ui/src/menu-items/settings.js b/packages/ui/src/menu-items/settings.js index 307bd0bd..1e0f58dd 100644 --- a/packages/ui/src/menu-items/settings.js +++ b/packages/ui/src/menu-items/settings.js @@ -1,8 +1,8 @@ // assets -import { IconTrash, IconFileUpload, IconFileExport, IconCopy, IconSearch, IconMessage } from '@tabler/icons' +import { IconTrash, IconFileUpload, IconFileExport, IconCopy, IconSearch, IconMessage, IconPictureInPictureOff } from '@tabler/icons' // constant -const icons = { IconTrash, IconFileUpload, IconFileExport, IconCopy, IconSearch, IconMessage } +const icons = { IconTrash, IconFileUpload, IconFileExport, IconCopy, IconSearch, IconMessage, IconPictureInPictureOff } // ==============================|| SETTINGS MENU ITEMS ||============================== // @@ -11,6 +11,13 @@ const settings = { title: '', type: 'group', children: [ + { + id: 'conversationStarters', + title: 'Starter Prompts', + type: 'item', + url: '', + icon: icons.IconPictureInPictureOff + }, { id: 'viewMessages', title: 'View Messages', diff --git a/packages/ui/src/routes/MainRoutes.js b/packages/ui/src/routes/MainRoutes.js index bce0de13..08dd721d 100644 --- a/packages/ui/src/routes/MainRoutes.js +++ b/packages/ui/src/routes/MainRoutes.js @@ -22,6 +22,9 @@ const Assistants = Loadable(lazy(() => import('views/assistants'))) // credentials routing const Credentials = Loadable(lazy(() => import('views/credentials'))) +// variables routing +const Variables = Loadable(lazy(() => import('views/variables'))) + // ==============================|| MAIN ROUTING ||============================== // const MainRoutes = { @@ -55,6 +58,10 @@ const MainRoutes = { { path: '/credentials', element: + }, + { + path: '/variables', + element: } ] } diff --git a/packages/ui/src/ui-component/button/FlowListMenu.js b/packages/ui/src/ui-component/button/FlowListMenu.js index b242d2cb..16bc86f2 100644 --- a/packages/ui/src/ui-component/button/FlowListMenu.js +++ b/packages/ui/src/ui-component/button/FlowListMenu.js @@ -11,6 +11,7 @@ import FileCopyIcon from '@mui/icons-material/FileCopy' import FileDownloadIcon from '@mui/icons-material/Downloading' import FileDeleteIcon from '@mui/icons-material/Delete' import FileCategoryIcon from '@mui/icons-material/Category' +import PictureInPictureAltIcon from '@mui/icons-material/PictureInPictureAlt' import Button from '@mui/material/Button' import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown' import { IconX } from '@tabler/icons' @@ -22,12 +23,12 @@ import useConfirm from 'hooks/useConfirm' import { uiBaseURL } from '../../store/constant' import { closeSnackbar as closeSnackbarAction, enqueueSnackbar as enqueueSnackbarAction } from '../../store/actions' -import ConfirmDialog from '../dialog/ConfirmDialog' import SaveChatflowDialog from '../dialog/SaveChatflowDialog' import TagDialog from '../dialog/TagDialog' import { generateExportFlowData } from '../../utils/genericHelper' import useNotifier from '../../utils/useNotifier' +import StarterPromptsDialog from '../dialog/StarterPromptsDialog' const StyledMenu = styled((props) => ( { setAnchorEl(event.currentTarget) @@ -93,6 +96,20 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) { setFlowDialogOpen(true) } + const handleFlowStarterPrompts = () => { + setAnchorEl(null) + setConversationStartersDialogProps({ + title: 'Starter Prompts - ' + chatflow.name, + chatflow: chatflow + }) + setConversationStartersDialogOpen(true) + } + + const saveFlowStarterPrompts = async () => { + setConversationStartersDialogOpen(false) + await updateFlowsApi.request() + } + const saveFlowRename = async (chatflowName) => { const updateBody = { name: chatflowName, @@ -254,6 +271,10 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) { Export + + + Starter Prompts + Update Category @@ -264,7 +285,6 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) { Delete - setCategoryDialogOpen(false)} onSubmit={saveFlowCategory} /> + setConversationStartersDialogOpen(false)} + /> ) } diff --git a/packages/ui/src/ui-component/cards/NodeCardWrapper.js b/packages/ui/src/ui-component/cards/NodeCardWrapper.js new file mode 100644 index 00000000..7d7cafe5 --- /dev/null +++ b/packages/ui/src/ui-component/cards/NodeCardWrapper.js @@ -0,0 +1,21 @@ +// material-ui +import { styled } from '@mui/material/styles' + +// project imports +import MainCard from './MainCard' + +const NodeCardWrapper = styled(MainCard)(({ theme }) => ({ + background: theme.palette.card.main, + color: theme.darkTextPrimary, + border: 'solid 1px', + borderColor: theme.palette.primary[200] + 75, + width: '300px', + height: 'auto', + padding: '10px', + boxShadow: '0 2px 14px 0 rgb(32 40 45 / 8%)', + '&:hover': { + borderColor: theme.palette.primary.main + } +})) + +export default NodeCardWrapper diff --git a/packages/ui/src/ui-component/cards/StarterPromptsCard.css b/packages/ui/src/ui-component/cards/StarterPromptsCard.css new file mode 100644 index 00000000..85c2d415 --- /dev/null +++ b/packages/ui/src/ui-component/cards/StarterPromptsCard.css @@ -0,0 +1,14 @@ +.button-container { + position: absolute; + bottom: 0; + z-index: 1000; + display: flex; + overflow-x: auto; + -webkit-overflow-scrolling: touch; /* For momentum scroll on mobile devices */ + scrollbar-width: none; /* For Firefox */ +} + +.button { + flex: 0 0 auto; /* Don't grow, don't shrink, base width on content */ + margin: 5px; /* Adjust as needed for spacing between buttons */ +} diff --git a/packages/ui/src/ui-component/cards/StarterPromptsCard.js b/packages/ui/src/ui-component/cards/StarterPromptsCard.js new file mode 100644 index 00000000..3abd8378 --- /dev/null +++ b/packages/ui/src/ui-component/cards/StarterPromptsCard.js @@ -0,0 +1,22 @@ +import Box from '@mui/material/Box' +import PropTypes from 'prop-types' +import { Chip } from '@mui/material' +import './StarterPromptsCard.css' + +const StarterPromptsCard = ({ isGrid, starterPrompts, onPromptClick }) => { + return ( + + {starterPrompts.map((sp, index) => ( + onPromptClick(sp.prompt, e)} /> + ))} + + ) +} + +StarterPromptsCard.propTypes = { + isGrid: PropTypes.bool, + starterPrompts: PropTypes.arrayOf(PropTypes.string), + onPromptClick: PropTypes.func +} + +export default StarterPromptsCard diff --git a/packages/ui/src/ui-component/dialog/AnalyseFlowDialog.js b/packages/ui/src/ui-component/dialog/AnalyseFlowDialog.js index dd6bb8ab..fe753957 100644 --- a/packages/ui/src/ui-component/dialog/AnalyseFlowDialog.js +++ b/packages/ui/src/ui-component/dialog/AnalyseFlowDialog.js @@ -30,8 +30,8 @@ import { SwitchInput } from 'ui-component/switch/Switch' import { Input } from 'ui-component/input/Input' import { StyledButton } from 'ui-component/button/StyledButton' import langsmithPNG from 'assets/images/langchain.png' -import langfusePNG from 'assets/images/langfuse.png' -import llmonitorPNG from 'assets/images/llmonitor.png' +import langfuseSVG from 'assets/images/langfuse.svg' +import llmonitorSVG from 'assets/images/lunary.svg' // store import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions' @@ -72,7 +72,7 @@ const analyticProviders = [ { label: 'LangFuse', name: 'langFuse', - icon: langfusePNG, + icon: langfuseSVG, url: 'https://langfuse.com', inputs: [ { @@ -99,7 +99,7 @@ const analyticProviders = [ { label: 'LLMonitor', name: 'llmonitor', - icon: llmonitorPNG, + icon: llmonitorSVG, url: 'https://llmonitor.com', inputs: [ { diff --git a/packages/ui/src/ui-component/dialog/ExpandTextDialog.js b/packages/ui/src/ui-component/dialog/ExpandTextDialog.js index 2a4ec4f5..f4fdb9f9 100644 --- a/packages/ui/src/ui-component/dialog/ExpandTextDialog.js +++ b/packages/ui/src/ui-component/dialog/ExpandTextDialog.js @@ -2,14 +2,24 @@ import { createPortal } from 'react-dom' import { useState, useEffect } from 'react' import { useSelector, useDispatch } from 'react-redux' import PropTypes from 'prop-types' +import PerfectScrollbar from 'react-perfect-scrollbar' + +// MUI import { Button, Dialog, DialogActions, DialogContent, Typography } from '@mui/material' import { useTheme } from '@mui/material/styles' -import PerfectScrollbar from 'react-perfect-scrollbar' +import { LoadingButton } from '@mui/lab' + +// Project Import import { StyledButton } from 'ui-component/button/StyledButton' -import { DarkCodeEditor } from 'ui-component/editor/DarkCodeEditor' -import { LightCodeEditor } from 'ui-component/editor/LightCodeEditor' +import { CodeEditor } from 'ui-component/editor/CodeEditor' + +// Store import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions' +// API +import nodesApi from 'api/nodes' +import useApi from 'hooks/useApi' + import './ExpandTextDialog.css' const ExpandTextDialog = ({ show, dialogProps, onCancel, onConfirm }) => { @@ -18,18 +28,30 @@ const ExpandTextDialog = ({ show, dialogProps, onCancel, onConfirm }) => { const theme = useTheme() const dispatch = useDispatch() const customization = useSelector((state) => state.customization) - const languageType = 'json' const [inputValue, setInputValue] = useState('') const [inputParam, setInputParam] = useState(null) + const [languageType, setLanguageType] = useState('json') + const [loading, setLoading] = useState(false) + const [codeExecutedResult, setCodeExecutedResult] = useState('') + + const executeCustomFunctionNodeApi = useApi(nodesApi.executeCustomFunctionNode) useEffect(() => { if (dialogProps.value) setInputValue(dialogProps.value) - if (dialogProps.inputParam) setInputParam(dialogProps.inputParam) + if (dialogProps.inputParam) { + setInputParam(dialogProps.inputParam) + if (dialogProps.inputParam.type === 'code') { + setLanguageType('js') + } + } return () => { setInputValue('') + setLoading(false) setInputParam(null) + setLanguageType('json') + setCodeExecutedResult('') } }, [dialogProps]) @@ -39,11 +61,35 @@ const ExpandTextDialog = ({ show, dialogProps, onCancel, onConfirm }) => { return () => dispatch({ type: HIDE_CANVAS_DIALOG }) }, [show, dispatch]) + useEffect(() => { + setLoading(executeCustomFunctionNodeApi.loading) + }, [executeCustomFunctionNodeApi.loading]) + + useEffect(() => { + if (executeCustomFunctionNodeApi.data) { + if (typeof executeCustomFunctionNodeApi.data === 'object') { + setCodeExecutedResult(JSON.stringify(executeCustomFunctionNodeApi.data, null, 2)) + } else { + setCodeExecutedResult(executeCustomFunctionNodeApi.data) + } + } + }, [executeCustomFunctionNodeApi.data]) + + useEffect(() => { + if (executeCustomFunctionNodeApi.error) { + if (typeof executeCustomFunctionNodeApi.error === 'object' && executeCustomFunctionNodeApi.error?.response?.data) { + setCodeExecutedResult(executeCustomFunctionNodeApi.error?.response?.data) + } else if (typeof executeCustomFunctionNodeApi.error === 'string') { + setCodeExecutedResult(executeCustomFunctionNodeApi.error) + } + } + }, [executeCustomFunctionNodeApi.error]) + const component = show ? (
- {inputParam && inputParam.type === 'string' && ( + {inputParam && (inputParam.type === 'string' || inputParam.type === 'code') && (
{inputParam.label} @@ -54,42 +100,66 @@ const ExpandTextDialog = ({ show, dialogProps, onCancel, onConfirm }) => { borderColor: theme.palette.grey['500'], borderRadius: '12px', height: '100%', - maxHeight: 'calc(100vh - 220px)', + maxHeight: languageType === 'js' ? 'calc(100vh - 250px)' : 'calc(100vh - 220px)', overflowX: 'hidden', backgroundColor: 'white' }} > - {customization.isDarkMode ? ( - setInputValue(code)} - placeholder={inputParam.placeholder} - type={languageType} - style={{ - fontSize: '0.875rem', - minHeight: 'calc(100vh - 220px)', - width: '100%' - }} - /> - ) : ( - setInputValue(code)} - placeholder={inputParam.placeholder} - type={languageType} - style={{ - fontSize: '0.875rem', - minHeight: 'calc(100vh - 220px)', - width: '100%' - }} - /> - )} + setInputValue(code)} + />
)}
+ {languageType === 'js' && ( + { + setLoading(true) + executeCustomFunctionNodeApi.request({ javascriptFunction: inputValue }) + }} + > + Execute + + )} + {codeExecutedResult && ( +
+ +
+ )}
diff --git a/packages/ui/src/ui-component/dialog/PromptLangsmithHubDialog.js b/packages/ui/src/ui-component/dialog/PromptLangsmithHubDialog.js new file mode 100644 index 00000000..8d89efc9 --- /dev/null +++ b/packages/ui/src/ui-component/dialog/PromptLangsmithHubDialog.js @@ -0,0 +1,600 @@ +import { createPortal } from 'react-dom' +import { useState, useEffect } from 'react' +import { useDispatch, useSelector } from 'react-redux' +import PropTypes from 'prop-types' + +import rehypeMathjax from 'rehype-mathjax' +import rehypeRaw from 'rehype-raw' +import remarkGfm from 'remark-gfm' +import remarkMath from 'remark-math' + +// MUI +import { + Box, + Button, + Card, + CardContent, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Chip, + Grid, + InputLabel, + List, + ListItemButton, + ListItemText, + OutlinedInput, + Select, + Typography, + Stack, + IconButton, + FormControl, + Checkbox, + MenuItem +} from '@mui/material' +import MuiAccordion from '@mui/material/Accordion' +import MuiAccordionSummary from '@mui/material/AccordionSummary' +import MuiAccordionDetails from '@mui/material/AccordionDetails' +import ExpandMoreIcon from '@mui/icons-material/ExpandMore' +import ArrowForwardIosSharpIcon from '@mui/icons-material/ArrowForwardIosSharp' +import ClearIcon from '@mui/icons-material/Clear' +import { styled } from '@mui/material/styles' + +//Project Import +import { StyledButton } from 'ui-component/button/StyledButton' +import { MemoizedReactMarkdown } from 'ui-component/markdown/MemoizedReactMarkdown' +import { CodeBlock } from 'ui-component/markdown/CodeBlock' +import promptEmptySVG from 'assets/images/prompt_empty.svg' + +import useApi from 'hooks/useApi' +import promptApi from 'api/prompt' +import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions' + +const NewLineToBr = ({ children = '' }) => { + return children.split('\n').reduce(function (arr, line) { + return arr.concat(line,
) + }, []) +} + +const Accordion = styled((props) => )(({ theme }) => ({ + border: `1px solid ${theme.palette.divider}`, + '&:not(:last-child)': { + borderBottom: 0 + }, + '&:before': { + display: 'none' + } +})) + +const AccordionSummary = styled((props) => ( + } {...props} /> +))(({ theme }) => ({ + backgroundColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, .05)' : 'rgba(0, 0, 0, .03)', + flexDirection: 'row-reverse', + '& .MuiAccordionSummary-expandIconWrapper.Mui-expanded': { + transform: 'rotate(180deg)' + }, + '& .MuiAccordionSummary-content': { + marginLeft: theme.spacing(1) + } +})) + +const AccordionDetails = styled(MuiAccordionDetails)(({ theme }) => ({ + padding: theme.spacing(2), + borderTop: '1px solid rgba(0, 0, 0, .125)' +})) + +const PromptLangsmithHubDialog = ({ promptType, show, onCancel, onSubmit }) => { + const portalElement = document.getElementById('portal') + const dispatch = useDispatch() + const customization = useSelector((state) => state.customization) + const getAvailablePromptsApi = useApi(promptApi.getAvailablePrompts) + + useEffect(() => { + if (show) { + dispatch({ type: SHOW_CANVAS_DIALOG }) + } else dispatch({ type: HIDE_CANVAS_DIALOG }) + return () => dispatch({ type: HIDE_CANVAS_DIALOG }) + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [show, dispatch]) + + useEffect(() => { + if (promptType && show) { + setLoading(true) + getAvailablePromptsApi.request({ tags: promptType === 'template' ? 'StringPromptTemplate&' : 'ChatPromptTemplate&' }) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [promptType, show]) + + useEffect(() => { + if (getAvailablePromptsApi.data && getAvailablePromptsApi.data.repos) { + setAvailablePrompNameList(getAvailablePromptsApi.data.repos) + if (getAvailablePromptsApi.data.repos?.length) handleListItemClick(0, getAvailablePromptsApi.data.repos) + setLoading(false) + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [getAvailablePromptsApi.data]) + + const ITEM_HEIGHT = 48 + const ITEM_PADDING_TOP = 8 + const MenuProps = { + PaperProps: { + style: { + maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP, + width: 250 + } + } + } + + const models = [ + { id: 101, name: 'anthropic:claude-instant-1' }, + { id: 102, name: 'anthropic:claude-instant-1.2' }, + { id: 103, name: 'anthropic:claude-2' }, + { id: 104, name: 'google:palm-2-chat-bison' }, + { id: 105, name: 'google:palm-2-codechat-bison' }, + { id: 106, name: 'google:palm-2-text-bison' }, + { id: 107, name: 'meta:llama-2-13b-chat' }, + { id: 108, name: 'meta:llama-2-70b-chat' }, + { id: 109, name: 'openai:gpt-3.5-turbo' }, + { id: 110, name: 'openai:gpt-4' }, + { id: 111, name: 'openai:text-davinci-003' } + ] + const [modelName, setModelName] = useState([]) + + const usecases = [ + { id: 201, name: 'Agents' }, + { id: 202, name: 'Agent Stimulation' }, + { id: 203, name: 'Autonomous agents' }, + { id: 204, name: 'Classification' }, + { id: 205, name: 'Chatbots' }, + { id: 206, name: 'Code understanding' }, + { id: 207, name: 'Code writing' }, + { id: 208, name: 'Evaluation' }, + { id: 209, name: 'Extraction' }, + { id: 210, name: 'Interacting with APIs' }, + { id: 211, name: 'Multi-modal' }, + { id: 212, name: 'QA over documents' }, + { id: 213, name: 'Self-checking' }, + { id: 214, name: 'SQL' }, + { id: 215, name: 'Summarization' }, + { id: 216, name: 'Tagging' } + ] + const [usecase, setUsecase] = useState([]) + + const languages = [ + { id: 301, name: 'Chinese' }, + { id: 302, name: 'English' }, + { id: 303, name: 'French' }, + { id: 304, name: 'German' }, + { id: 305, name: 'Russian' }, + { id: 306, name: 'Spanish' } + ] + const [language, setLanguage] = useState([]) + const [availablePrompNameList, setAvailablePrompNameList] = useState([]) + const [selectedPrompt, setSelectedPrompt] = useState({}) + + const [accordionExpanded, setAccordionExpanded] = useState(['prompt']) + const [loading, setLoading] = useState(false) + + const handleAccordionChange = (accordionName) => (event, isExpanded) => { + const accordians = [...accordionExpanded] + if (!isExpanded) setAccordionExpanded(accordians.filter((accr) => accr !== accordionName)) + else { + accordians.push(accordionName) + setAccordionExpanded(accordians) + } + } + + const handleListItemClick = async (index, overridePromptNameList = []) => { + const prompt = overridePromptNameList.length ? overridePromptNameList[index] : availablePrompNameList[index] + + if (!prompt.detailed) { + const createResp = await promptApi.getPrompt({ + promptName: prompt.full_name + }) + if (createResp.data) { + prompt.detailed = createResp.data.templates + } + } + setSelectedPrompt(prompt) + } + + const fetchPrompts = async () => { + let tags = promptType === 'template' ? 'StringPromptTemplate&' : 'ChatPromptTemplate&' + modelName.forEach((item) => { + tags += `tags=${item.name}&` + }) + usecase.forEach((item) => { + tags += `tags=${item.name}&` + }) + language.forEach((item) => { + tags += `tags=${item.name}&` + }) + setLoading(true) + getAvailablePromptsApi.request({ tags: tags }) + } + + const removeDuplicates = (value) => { + let duplicateRemoved = [] + + value.forEach((item) => { + if (value.filter((o) => o.id === item.id).length === 1) { + duplicateRemoved.push(item) + } + }) + return duplicateRemoved + } + + const handleModelChange = (event) => { + const { + target: { value } + } = event + + setModelName(removeDuplicates(value)) + } + + const handleUsecaseChange = (event) => { + const { + target: { value } + } = event + + setUsecase(removeDuplicates(value)) + } + const handleLanguageChange = (event) => { + const { + target: { value } + } = event + + setLanguage(removeDuplicates(value)) + } + + const component = show ? ( + + + Langchain Hub ({promptType === 'template' ? 'PromptTemplate' : 'ChatPromptTemplate'}) + + + + + + Model + + + + + + Usecase + + + + + + Language + + + + + + + + + {loading && ( + + + promptEmptySVG + +
Please wait....loading Prompts
+
+ )} + {!loading && availablePrompNameList && availablePrompNameList.length === 0 && ( + + + promptEmptySVG + +
No Available Prompts
+
+ )} + {!loading && availablePrompNameList && availablePrompNameList.length > 0 && ( + + + + + + + + + Available Prompts + + + {availablePrompNameList.map((item, index) => ( + handleListItemClick(index)} + > +
+ + {item.full_name} + +
+ {item.tags.map((tag, index) => ( + + ))} +
+
+
+ ))} +
+
+
+
+
+ + + + + + } + id='panel2d-header' + > + Prompt + + + + {selectedPrompt?.detailed?.map((item) => ( + <> + + {item.typeDisplay.toUpperCase()} + + +

+ {item.template} +

+
+ + ))} +
+
+
+ + } + id='panel1d-header' + > + Description + + + + {selectedPrompt?.description} + + + + + } + aria-controls='panel3d-content' + id='panel3d-header' + > + Readme + + +
+ + ) : ( + + {children} + + ) + } + }} + > + {selectedPrompt?.readme} + +
+
+
+
+
+
+
+
+
+
+ )} +
+ {availablePrompNameList && availablePrompNameList.length > 0 && ( + + + onSubmit(selectedPrompt.detailed)} + variant='contained' + > + Load + + + )} +
+ ) : null + + return createPortal(component, portalElement) +} + +PromptLangsmithHubDialog.propTypes = { + promptType: PropTypes.string, + show: PropTypes.bool, + onCancel: PropTypes.func, + onSubmit: PropTypes.func +} + +export default PromptLangsmithHubDialog diff --git a/packages/ui/src/ui-component/dialog/StarterPromptsDialog.js b/packages/ui/src/ui-component/dialog/StarterPromptsDialog.js new file mode 100644 index 00000000..78866b4c --- /dev/null +++ b/packages/ui/src/ui-component/dialog/StarterPromptsDialog.js @@ -0,0 +1,252 @@ +import { createPortal } from 'react-dom' +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, + IconButton, + Dialog, + DialogContent, + OutlinedInput, + DialogTitle, + DialogActions, + Box, + List, + InputAdornment +} from '@mui/material' +import { IconX, IconTrash, IconPlus, IconBulb } from '@tabler/icons' + +// Project import +import { StyledButton } from 'ui-component/button/StyledButton' + +// store +import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions' +import useNotifier from 'utils/useNotifier' + +// API +import chatflowsApi from 'api/chatflows' + +const StarterPromptsDialog = ({ show, dialogProps, onCancel, onConfirm }) => { + const portalElement = document.getElementById('portal') + const dispatch = useDispatch() + + useNotifier() + + const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)) + const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) + + const [inputFields, setInputFields] = useState([ + { + prompt: '' + } + ]) + + const [chatbotConfig, setChatbotConfig] = useState({}) + + const addInputField = () => { + setInputFields([ + ...inputFields, + { + prompt: '' + } + ]) + } + const removeInputFields = (index) => { + const rows = [...inputFields] + rows.splice(index, 1) + setInputFields(rows) + } + + const handleChange = (index, evnt) => { + const { name, value } = evnt.target + const list = [...inputFields] + list[index][name] = value + setInputFields(list) + } + + const onSave = async () => { + try { + let value = { + starterPrompts: { + ...inputFields + } + } + chatbotConfig.starterPrompts = value.starterPrompts + const saveResp = await chatflowsApi.updateChatflow(dialogProps.chatflow.id, { + chatbotConfig: JSON.stringify(chatbotConfig) + }) + if (saveResp.data) { + enqueueSnackbar({ + message: 'Conversation Starter Prompts Saved', + options: { + key: new Date().getTime() + Math.random(), + variant: 'success', + action: (key) => ( + + ) + } + }) + dispatch({ type: SET_CHATFLOW, chatflow: saveResp.data }) + } + onConfirm() + } catch (error) { + const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` + enqueueSnackbar({ + message: `Failed to save Conversation Starter Prompts: ${errorData}`, + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + persist: true, + action: (key) => ( + + ) + } + }) + } + } + + useEffect(() => { + if (dialogProps.chatflow && dialogProps.chatflow.chatbotConfig) { + try { + let chatbotConfig = JSON.parse(dialogProps.chatflow.chatbotConfig) + setChatbotConfig(chatbotConfig || {}) + if (chatbotConfig.starterPrompts) { + let inputFields = [] + Object.getOwnPropertyNames(chatbotConfig.starterPrompts).forEach((key) => { + if (chatbotConfig.starterPrompts[key]) { + inputFields.push(chatbotConfig.starterPrompts[key]) + } + }) + setInputFields(inputFields) + } else { + setInputFields([ + { + prompt: '' + } + ]) + } + } catch (e) { + setInputFields([ + { + prompt: '' + } + ]) + } + } + + return () => {} + }, [dialogProps]) + + useEffect(() => { + if (show) dispatch({ type: SHOW_CANVAS_DIALOG }) + else dispatch({ type: HIDE_CANVAS_DIALOG }) + return () => dispatch({ type: HIDE_CANVAS_DIALOG }) + }, [show, dispatch]) + + const component = show ? ( + + + {dialogProps.title || 'Conversation Starter Prompts'} + + +
+
+ + + Starter prompts will only be shown when there is no messages on the chat + +
+
+ :not(style)': { m: 1 }, pt: 2 }}> + + {inputFields.map((data, index) => { + return ( +
+ + handleChange(index, e)} + size='small' + value={data.prompt} + name='prompt' + endAdornment={ + + {inputFields.length > 1 && ( + removeInputFields(index)} + edge='end' + > + + + )} + + } + /> + + + {index === inputFields.length - 1 && ( + + + + )} + +
+ ) + })} +
+
+
+ + + + Save + + +
+ ) : null + + return createPortal(component, portalElement) +} + +StarterPromptsDialog.propTypes = { + show: PropTypes.bool, + dialogProps: PropTypes.object, + onCancel: PropTypes.func, + onConfirm: PropTypes.func +} + +export default StarterPromptsDialog diff --git a/packages/ui/src/ui-component/editor/CodeEditor.js b/packages/ui/src/ui-component/editor/CodeEditor.js new file mode 100644 index 00000000..120e19a0 --- /dev/null +++ b/packages/ui/src/ui-component/editor/CodeEditor.js @@ -0,0 +1,48 @@ +import PropTypes from 'prop-types' +import CodeMirror from '@uiw/react-codemirror' +import { javascript } from '@codemirror/lang-javascript' +import { json } from '@codemirror/lang-json' +import { vscodeDark } from '@uiw/codemirror-theme-vscode' +import { sublime } from '@uiw/codemirror-theme-sublime' +import { EditorView } from '@codemirror/view' + +export const CodeEditor = ({ value, height, theme, lang, placeholder, disabled = false, basicSetup = {}, onValueChange }) => { + const customStyle = EditorView.baseTheme({ + '&': { + color: '#191b1f', + padding: '10px' + }, + '.cm-placeholder': { + color: 'rgba(120, 120, 120, 0.5)' + } + }) + + return ( + + ) +} + +CodeEditor.propTypes = { + value: PropTypes.string, + height: PropTypes.string, + theme: PropTypes.string, + lang: PropTypes.string, + placeholder: PropTypes.string, + disabled: PropTypes.bool, + basicSetup: PropTypes.object, + onValueChange: PropTypes.func +} diff --git a/packages/ui/src/ui-component/editor/DarkCodeEditor.js b/packages/ui/src/ui-component/editor/DarkCodeEditor.js deleted file mode 100644 index bf0719dd..00000000 --- a/packages/ui/src/ui-component/editor/DarkCodeEditor.js +++ /dev/null @@ -1,43 +0,0 @@ -import Editor from 'react-simple-code-editor' -import { highlight, languages } from 'prismjs/components/prism-core' -import 'prismjs/components/prism-clike' -import 'prismjs/components/prism-javascript' -import 'prismjs/components/prism-json' -import 'prismjs/components/prism-markup' -import './prism-dark.css' -import PropTypes from 'prop-types' -import { useTheme } from '@mui/material/styles' - -export const DarkCodeEditor = ({ value, placeholder, disabled = false, type, style, onValueChange, onMouseUp, onBlur }) => { - const theme = useTheme() - - return ( - highlight(code, type === 'json' ? languages.json : languages.js)} - padding={10} - onValueChange={onValueChange} - onMouseUp={onMouseUp} - onBlur={onBlur} - tabSize={4} - style={{ - ...style, - background: theme.palette.codeEditor.main - }} - textareaClassName='editor__textarea' - /> - ) -} - -DarkCodeEditor.propTypes = { - value: PropTypes.string, - placeholder: PropTypes.string, - disabled: PropTypes.bool, - type: PropTypes.string, - style: PropTypes.object, - onValueChange: PropTypes.func, - onMouseUp: PropTypes.func, - onBlur: PropTypes.func -} diff --git a/packages/ui/src/ui-component/editor/LightCodeEditor.js b/packages/ui/src/ui-component/editor/LightCodeEditor.js deleted file mode 100644 index 14dcbf29..00000000 --- a/packages/ui/src/ui-component/editor/LightCodeEditor.js +++ /dev/null @@ -1,43 +0,0 @@ -import Editor from 'react-simple-code-editor' -import { highlight, languages } from 'prismjs/components/prism-core' -import 'prismjs/components/prism-clike' -import 'prismjs/components/prism-javascript' -import 'prismjs/components/prism-json' -import 'prismjs/components/prism-markup' -import './prism-light.css' -import PropTypes from 'prop-types' -import { useTheme } from '@mui/material/styles' - -export const LightCodeEditor = ({ value, placeholder, disabled = false, type, style, onValueChange, onMouseUp, onBlur }) => { - const theme = useTheme() - - return ( - highlight(code, type === 'json' ? languages.json : languages.js)} - padding={10} - onValueChange={onValueChange} - onMouseUp={onMouseUp} - onBlur={onBlur} - tabSize={4} - style={{ - ...style, - background: theme.palette.card.main - }} - textareaClassName='editor__textarea' - /> - ) -} - -LightCodeEditor.propTypes = { - value: PropTypes.string, - placeholder: PropTypes.string, - disabled: PropTypes.bool, - type: PropTypes.string, - style: PropTypes.object, - onValueChange: PropTypes.func, - onMouseUp: PropTypes.func, - onBlur: PropTypes.func -} diff --git a/packages/ui/src/ui-component/editor/prism-dark.css b/packages/ui/src/ui-component/editor/prism-dark.css deleted file mode 100644 index c4bfb413..00000000 --- a/packages/ui/src/ui-component/editor/prism-dark.css +++ /dev/null @@ -1,275 +0,0 @@ -pre[class*='language-'], -code[class*='language-'] { - color: #d4d4d4; - font-size: 13px; - text-shadow: none; - font-family: Menlo, Monaco, Consolas, 'Andale Mono', 'Ubuntu Mono', 'Courier New', monospace; - direction: ltr; - text-align: left; - white-space: pre; - word-spacing: normal; - word-break: normal; - line-height: 1.5; - -moz-tab-size: 4; - -o-tab-size: 4; - tab-size: 4; - -webkit-hyphens: none; - -moz-hyphens: none; - -ms-hyphens: none; - hyphens: none; -} - -pre[class*='language-']::selection, -code[class*='language-']::selection, -pre[class*='language-'] *::selection, -code[class*='language-'] *::selection { - text-shadow: none; - background: #264f78; -} - -@media print { - pre[class*='language-'], - code[class*='language-'] { - text-shadow: none; - } -} - -pre[class*='language-'] { - padding: 1em; - margin: 0.5em 0; - overflow: auto; - background: #1e1e1e; -} - -:not(pre) > code[class*='language-'] { - padding: 0.1em 0.3em; - border-radius: 0.3em; - color: #db4c69; - background: #1e1e1e; -} -/********************************************************* -* Tokens -*/ -.namespace { - opacity: 0.7; -} - -.token.doctype .token.doctype-tag { - color: #569cd6; -} - -.token.doctype .token.name { - color: #9cdcfe; -} - -.token.comment, -.token.prolog { - color: #6a9955; -} - -.token.punctuation, -.language-html .language-css .token.punctuation, -.language-html .language-javascript .token.punctuation { - color: #d4d4d4; -} - -.token.property, -.token.tag, -.token.boolean, -.token.number, -.token.constant, -.token.symbol, -.token.inserted, -.token.unit { - color: #b5cea8; -} - -.token.selector, -.token.attr-name, -.token.string, -.token.char, -.token.builtin, -.token.deleted { - color: #ce9178; -} - -.language-css .token.string.url { - text-decoration: underline; -} - -.token.operator, -.token.entity { - color: #d4d4d4; -} - -.token.operator.arrow { - color: #569cd6; -} - -.token.atrule { - color: #ce9178; -} - -.token.atrule .token.rule { - color: #c586c0; -} - -.token.atrule .token.url { - color: #9cdcfe; -} - -.token.atrule .token.url .token.function { - color: #dcdcaa; -} - -.token.atrule .token.url .token.punctuation { - color: #d4d4d4; -} - -.token.keyword { - color: #569cd6; -} - -.token.keyword.module, -.token.keyword.control-flow { - color: #c586c0; -} - -.token.function, -.token.function .token.maybe-class-name { - color: #dcdcaa; -} - -.token.regex { - color: #d16969; -} - -.token.important { - color: #569cd6; -} - -.token.italic { - font-style: italic; -} - -.token.constant { - color: #9cdcfe; -} - -.token.class-name, -.token.maybe-class-name { - color: #4ec9b0; -} - -.token.console { - color: #9cdcfe; -} - -.token.parameter { - color: #9cdcfe; -} - -.token.interpolation { - color: #9cdcfe; -} - -.token.punctuation.interpolation-punctuation { - color: #569cd6; -} - -.token.boolean { - color: #569cd6; -} - -.token.property, -.token.variable, -.token.imports .token.maybe-class-name, -.token.exports .token.maybe-class-name { - color: #9cdcfe; -} - -.token.selector { - color: #d7ba7d; -} - -.token.escape { - color: #d7ba7d; -} - -.token.tag { - color: #569cd6; -} - -.token.tag .token.punctuation { - color: #808080; -} - -.token.cdata { - color: #808080; -} - -.token.attr-name { - color: #9cdcfe; -} - -.token.attr-value, -.token.attr-value .token.punctuation { - color: #ce9178; -} - -.token.attr-value .token.punctuation.attr-equals { - color: #d4d4d4; -} - -.token.entity { - color: #569cd6; -} - -.token.namespace { - color: #4ec9b0; -} -/********************************************************* -* Language Specific -*/ - -pre[class*='language-javascript'], -code[class*='language-javascript'], -pre[class*='language-jsx'], -code[class*='language-jsx'], -pre[class*='language-typescript'], -code[class*='language-typescript'], -pre[class*='language-tsx'], -code[class*='language-tsx'] { - color: #9cdcfe; -} - -pre[class*='language-css'], -code[class*='language-css'] { - color: #ce9178; -} - -pre[class*='language-html'], -code[class*='language-html'] { - color: #d4d4d4; -} - -.language-regex .token.anchor { - color: #dcdcaa; -} - -.language-html .token.punctuation { - color: #808080; -} -/********************************************************* -* Line highlighting -*/ -pre[class*='language-'] > code[class*='language-'] { - position: relative; - z-index: 1; -} - -.line-highlight.line-highlight { - background: #f7ebc6; - box-shadow: inset 5px 0 0 #f7d87c; - z-index: 0; -} diff --git a/packages/ui/src/ui-component/editor/prism-light.css b/packages/ui/src/ui-component/editor/prism-light.css deleted file mode 100644 index 95d6d6eb..00000000 --- a/packages/ui/src/ui-component/editor/prism-light.css +++ /dev/null @@ -1,207 +0,0 @@ -code[class*='language-'], -pre[class*='language-'] { - text-align: left; - white-space: pre; - word-spacing: normal; - word-break: normal; - word-wrap: normal; - color: #90a4ae; - background: #fafafa; - font-family: Roboto Mono, monospace; - font-size: 1em; - line-height: 1.5em; - - -moz-tab-size: 4; - -o-tab-size: 4; - tab-size: 4; - - -webkit-hyphens: none; - -moz-hyphens: none; - -ms-hyphens: none; - hyphens: none; -} - -code[class*='language-']::-moz-selection, -pre[class*='language-']::-moz-selection, -code[class*='language-'] ::-moz-selection, -pre[class*='language-'] ::-moz-selection { - background: #cceae7; - color: #263238; -} - -code[class*='language-']::selection, -pre[class*='language-']::selection, -code[class*='language-'] ::selection, -pre[class*='language-'] ::selection { - background: #cceae7; - color: #263238; -} - -:not(pre) > code[class*='language-'] { - white-space: normal; - border-radius: 0.2em; - padding: 0.1em; -} - -pre[class*='language-'] { - overflow: auto; - position: relative; - margin: 0.5em 0; - padding: 1.25em 1em; -} - -.language-css > code, -.language-sass > code, -.language-scss > code { - color: #f76d47; -} - -[class*='language-'] .namespace { - opacity: 0.7; -} - -.token.atrule { - color: #7c4dff; -} - -.token.attr-name { - color: #39adb5; -} - -.token.attr-value { - color: #f6a434; -} - -.token.attribute { - color: #f6a434; -} - -.token.boolean { - color: #7c4dff; -} - -.token.builtin { - color: #39adb5; -} - -.token.cdata { - color: #39adb5; -} - -.token.char { - color: #39adb5; -} - -.token.class { - color: #39adb5; -} - -.token.class-name { - color: #6182b8; -} - -.token.comment { - color: #aabfc9; -} - -.token.constant { - color: #7c4dff; -} - -.token.deleted { - color: #e53935; -} - -.token.doctype { - color: #aabfc9; -} - -.token.entity { - color: #e53935; -} - -.token.function { - color: #7c4dff; -} - -.token.hexcode { - color: #f76d47; -} - -.token.id { - color: #7c4dff; - font-weight: bold; -} - -.token.important { - color: #7c4dff; - font-weight: bold; -} - -.token.inserted { - color: #39adb5; -} - -.token.keyword { - color: #7c4dff; -} - -.token.number { - color: #f76d47; -} - -.token.operator { - color: #39adb5; -} - -.token.prolog { - color: #aabfc9; -} - -.token.property { - color: #39adb5; -} - -.token.pseudo-class { - color: #f6a434; -} - -.token.pseudo-element { - color: #f6a434; -} - -.token.punctuation { - color: #39adb5; -} - -.token.regex { - color: #6182b8; -} - -.token.selector { - color: #e53935; -} - -.token.string { - color: #f6a434; -} - -.token.symbol { - color: #7c4dff; -} - -.token.tag { - color: #e53935; -} - -.token.unit { - color: #f76d47; -} - -.token.url { - color: #e53935; -} - -.token.variable { - color: #e53935; -} diff --git a/packages/ui/src/ui-component/input/Input.js b/packages/ui/src/ui-component/input/Input.js index 6993847b..e59f012c 100644 --- a/packages/ui/src/ui-component/input/Input.js +++ b/packages/ui/src/ui-component/input/Input.js @@ -1,23 +1,10 @@ import { useState, useEffect, useRef } from 'react' import PropTypes from 'prop-types' -import { FormControl, OutlinedInput, Popover } from '@mui/material' -import ExpandTextDialog from 'ui-component/dialog/ExpandTextDialog' +import { FormControl, OutlinedInput, InputBase, Popover } from '@mui/material' import SelectVariable from 'ui-component/json/SelectVariable' import { getAvailableNodesForVariable } from 'utils/genericHelper' -export const Input = ({ - inputParam, - value, - nodes, - edges, - nodeId, - onChange, - disabled = false, - showDialog, - dialogProps, - onDialogCancel, - onDialogConfirm -}) => { +export const Input = ({ inputParam, value, nodes, edges, nodeId, onChange, disabled = false }) => { const [myValue, setMyValue] = useState(value ?? '') const [anchorEl, setAnchorEl] = useState(null) const [availableNodesForVariable, setAvailableNodesForVariable] = useState([]) @@ -63,39 +50,66 @@ export const Input = ({ return ( <> - - { - setMyValue(e.target.value) - onChange(e.target.value) - }} - inputProps={{ - step: inputParam.step ?? 1, - style: { - height: inputParam.rows ? '90px' : 'inherit' - } - }} - /> - - {showDialog && ( - { - setMyValue(newValue) - onDialogConfirm(newValue, inputParamName) - }} - > + {inputParam.name === 'note' ? ( + + { + setMyValue(e.target.value) + onChange(e.target.value) + }} + inputProps={{ + step: inputParam.step ?? 1, + style: { + border: 'none', + background: 'none', + color: '#212121' + } + }} + sx={{ + border: 'none', + background: 'none', + padding: '10px 14px', + textarea: { + '&::placeholder': { + color: '#616161' + } + } + }} + /> + + ) : ( + + { + setMyValue(e.target.value) + onChange(e.target.value) + }} + inputProps={{ + step: inputParam.step ?? 1, + style: { + height: inputParam.rows ? '90px' : 'inherit' + } + }} + /> + )}
{inputParam?.acceptVariable && ( @@ -131,11 +145,7 @@ Input.propTypes = { value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), onChange: PropTypes.func, disabled: PropTypes.bool, - showDialog: PropTypes.bool, - dialogProps: PropTypes.object, nodes: PropTypes.array, edges: PropTypes.array, - nodeId: PropTypes.string, - onDialogCancel: PropTypes.func, - onDialogConfirm: PropTypes.func + nodeId: PropTypes.string } diff --git a/packages/ui/src/ui-component/json/SelectVariable.js b/packages/ui/src/ui-component/json/SelectVariable.js index 7a482bae..50c9f839 100644 --- a/packages/ui/src/ui-component/json/SelectVariable.js +++ b/packages/ui/src/ui-component/json/SelectVariable.js @@ -141,8 +141,17 @@ const SelectVariable = ({ availableNodesForVariable, disabled = false, onSelectA diff --git a/packages/ui/src/ui-component/table/FlowListTable.js b/packages/ui/src/ui-component/table/FlowListTable.js index 358fc965..e1ac3b6c 100644 --- a/packages/ui/src/ui-component/table/FlowListTable.js +++ b/packages/ui/src/ui-component/table/FlowListTable.js @@ -147,8 +147,8 @@ export const FlowListTable = ({ data, images, filterFunction, updateFlowsApi }) } FlowListTable.propTypes = { - data: PropTypes.object, - images: PropTypes.array, + data: PropTypes.array, + images: PropTypes.object, filterFunction: PropTypes.func, updateFlowsApi: PropTypes.object } diff --git a/packages/ui/src/ui-component/tooltip/NodeTooltip.js b/packages/ui/src/ui-component/tooltip/NodeTooltip.js new file mode 100644 index 00000000..30bd7cf8 --- /dev/null +++ b/packages/ui/src/ui-component/tooltip/NodeTooltip.js @@ -0,0 +1,12 @@ +import { styled } from '@mui/material/styles' +import Tooltip, { tooltipClasses } from '@mui/material/Tooltip' + +const NodeTooltip = styled(({ className, ...props }) => )(({ theme }) => ({ + [`& .${tooltipClasses.tooltip}`]: { + backgroundColor: theme.palette.nodeToolTip.background, + color: theme.palette.nodeToolTip.color, + boxShadow: theme.shadows[1] + } +})) + +export default NodeTooltip diff --git a/packages/ui/src/utils/genericHelper.js b/packages/ui/src/utils/genericHelper.js index 69491fbc..eadbdb88 100644 --- a/packages/ui/src/utils/genericHelper.js +++ b/packages/ui/src/utils/genericHelper.js @@ -182,15 +182,6 @@ export const initNode = (nodeData, newNodeId) => { return nodeData } -export const getEdgeLabelName = (source) => { - const sourceSplit = source.split('-') - if (sourceSplit.length && sourceSplit[0].includes('ifElse')) { - const outputAnchorsIndex = sourceSplit[sourceSplit.length - 1] - return outputAnchorsIndex === '0' ? 'true' : 'false' - } - return '' -} - export const isValidConnection = (connection, reactFlowInstance) => { const sourceHandle = connection.sourceHandle const targetHandle = connection.targetHandle diff --git a/packages/ui/src/views/canvas/AddNodes.js b/packages/ui/src/views/canvas/AddNodes.js index ea813df5..61db1716 100644 --- a/packages/ui/src/views/canvas/AddNodes.js +++ b/packages/ui/src/views/canvas/AddNodes.js @@ -271,7 +271,13 @@ const AddNodes = ({ nodesData, node }) => { 'aria-label': 'weight' }} /> - + {['LangChain', 'LlamaIndex'].map((item, index) => ( { } iconPosition='start' + sx={{ minHeight: '50px', height: '50px' }} key={index} - label={ - item === 'LlamaIndex' ? ( - <> -

{item}

-   - - - ) : ( -

{item}

- ) - } + label={item} {...a11yProps(index)} >
))} +
+ BETA +
+ { }} style={{ height: '100%', maxHeight: 'calc(100vh - 380px)', overflowX: 'hidden' }} > - + setAPIDialogOpen(false)} /> setAnalyseDialogOpen(false)} /> + setConversationStartersDialogOpen(false)} + onCancel={() => setConversationStartersDialogOpen(false)} + /> ({ - background: theme.palette.card.main, - color: theme.darkTextPrimary, - border: 'solid 1px', - borderColor: theme.palette.primary[200] + 75, - width: '300px', - height: 'auto', - padding: '10px', - boxShadow: '0 2px 14px 0 rgb(32 40 45 / 8%)', - '&:hover': { - borderColor: theme.palette.primary.main - } -})) - -const LightTooltip = styled(({ className, ...props }) => )(({ theme }) => ({ - [`& .${tooltipClasses.tooltip}`]: { - backgroundColor: theme.palette.nodeToolTip.background, - color: theme.palette.nodeToolTip.color, - boxShadow: theme.shadows[1] - } -})) - // ===========================|| CANVAS NODE ||=========================== // const CanvasNode = ({ data }) => { @@ -94,7 +73,7 @@ const CanvasNode = ({ data }) => { return ( <> - { }} border={false} > - { - {data.outputAnchors.map((outputAnchor, index) => ( ))} - - + + { const dialogProp = { @@ -69,7 +75,18 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA setShowExpandDialog(true) } - const onFormatPromptValuesClicked = (value, inputParam) => { + const onShowPromptHubButtonClicked = () => { + setShowPromptHubDialog(true) + } + const onShowPromptHubButtonSubmit = (templates) => { + setShowPromptHubDialog(false) + for (const t of templates) { + if (Object.prototype.hasOwnProperty.call(data.inputs, t.type)) { + data.inputs[t.type] = t.template + } + } + } + const onEditJSONClicked = (value, inputParam) => { // Preset values if the field is format prompt values let inputValue = value if (inputParam.name === 'promptValues' && !value) { @@ -209,6 +226,31 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA )} + {(data.name === 'promptTemplate' || data.name === 'chatPromptTemplate') && + (inputParam.name === 'template' || inputParam.name === 'systemMessagePrompt') && ( + <> + + setShowPromptHubDialog(false)} + onSubmit={onShowPromptHubButtonSubmit} + > + + )}
{inputParam.label} @@ -216,7 +258,7 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA {inputParam.description && }
- {inputParam.type === 'string' && inputParam.rows && ( + {((inputParam.type === 'string' && inputParam.rows) || inputParam.type === 'code') && ( - + {inputParam.warning}
)} @@ -260,6 +303,7 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA }} /> )} + {inputParam.type === 'file' && ( (data.inputs[inputParam.name] = newValue)} /> )} + {inputParam.type === 'code' && ( + <> +
+
+ (data.inputs[inputParam.name] = code)} + basicSetup={{ highlightActiveLine: false, highlightActiveLineGutter: false }} + /> +
+ + )} {(inputParam.type === 'string' || inputParam.type === 'password' || inputParam.type === 'number') && ( setShowExpandDialog(false)} - onDialogConfirm={(newValue, inputParamName) => onExpandDialogSave(newValue, inputParamName)} /> )} {inputParam.type === 'json' && ( @@ -313,11 +370,17 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA {inputParam?.acceptVariable && ( <> setAsyncOptionEditDialog('')} onConfirm={onConfirmAsyncOption} > + setShowExpandDialog(false)} + onConfirm={(newValue, inputParamName) => onExpandDialogSave(newValue, inputParamName)} + > ) } diff --git a/packages/ui/src/views/canvas/NodeOutputHandler.js b/packages/ui/src/views/canvas/NodeOutputHandler.js index c5fc1345..3970ef9d 100644 --- a/packages/ui/src/views/canvas/NodeOutputHandler.js +++ b/packages/ui/src/views/canvas/NodeOutputHandler.js @@ -73,42 +73,104 @@ const NodeOutputHandler = ({ outputAnchor, data, disabled = false }) => {
)} - {outputAnchor.type === 'options' && outputAnchor.options && outputAnchor.options.length > 0 && ( - <> - opt.name === data.outputs?.[outputAnchor.name])?.type ?? outputAnchor.type - } - > - opt.name === data.outputs?.[outputAnchor.name])?.id ?? ''} - isValidConnection={(connection) => isValidConnection(connection, reactFlowInstance)} - style={{ - height: 10, - width: 10, - backgroundColor: data.selected ? theme.palette.primary.main : theme.palette.text.secondary, - top: position - }} - /> - - - { - setDropdownValue(newValue) - data.outputs[outputAnchor.name] = newValue - }} - value={data.outputs[outputAnchor.name] ?? outputAnchor.default ?? 'choose an option'} - /> - - + {data.name === 'ifElseFunction' && outputAnchor.type === 'options' && outputAnchor.options && ( +
+
+ opt.name === data.outputs?.[outputAnchor.name])?.type ?? + outputAnchor.type + } + > + opt.name === 'returnTrue')?.id ?? ''} + id={outputAnchor.options.find((opt) => opt.name === 'returnTrue')?.id ?? ''} + isValidConnection={(connection) => isValidConnection(connection, reactFlowInstance)} + style={{ + height: 10, + width: 10, + backgroundColor: data.selected ? theme.palette.primary.main : theme.palette.text.secondary, + top: position - 25 + }} + /> + +
+ + True + +
+
+ opt.name === data.outputs?.[outputAnchor.name])?.type ?? + outputAnchor.type + } + > + opt.name === 'returnFalse')?.id ?? ''} + id={outputAnchor.options.find((opt) => opt.name === 'returnFalse')?.id ?? ''} + isValidConnection={(connection) => isValidConnection(connection, reactFlowInstance)} + style={{ + height: 10, + width: 10, + backgroundColor: data.selected ? theme.palette.primary.main : theme.palette.text.secondary, + top: position + 25 + }} + /> + +
+ + False + +
+
)} + {data.name !== 'ifElseFunction' && + outputAnchor.type === 'options' && + outputAnchor.options && + outputAnchor.options.length > 0 && ( + <> + opt.name === data.outputs?.[outputAnchor.name])?.type ?? + outputAnchor.type + } + > + opt.name === data.outputs?.[outputAnchor.name])?.id ?? ''} + isValidConnection={(connection) => isValidConnection(connection, reactFlowInstance)} + style={{ + height: 10, + width: 10, + backgroundColor: data.selected ? theme.palette.primary.main : theme.palette.text.secondary, + top: position + }} + /> + + + { + setDropdownValue(newValue) + data.outputs[outputAnchor.name] = newValue + }} + value={data.outputs[outputAnchor.name] ?? outputAnchor.default ?? 'choose an option'} + /> + + + )} ) } diff --git a/packages/ui/src/views/canvas/StickyNote.js b/packages/ui/src/views/canvas/StickyNote.js new file mode 100644 index 00000000..7276bbbc --- /dev/null +++ b/packages/ui/src/views/canvas/StickyNote.js @@ -0,0 +1,103 @@ +import PropTypes from 'prop-types' +import { useContext, useState } from 'react' +import { useSelector } from 'react-redux' + +// material-ui +import { useTheme } from '@mui/material/styles' + +// project imports +import NodeCardWrapper from '../../ui-component/cards/NodeCardWrapper' +import NodeTooltip from '../../ui-component/tooltip/NodeTooltip' +import { IconButton, Box } from '@mui/material' +import { IconCopy, IconTrash } from '@tabler/icons' +import { Input } from 'ui-component/input/Input' + +// const +import { flowContext } from '../../store/context/ReactFlowContext' + +const StickyNote = ({ data }) => { + const theme = useTheme() + const canvas = useSelector((state) => state.canvas) + const { deleteNode, duplicateNode } = useContext(flowContext) + const [inputParam] = data.inputParams + + const [open, setOpen] = useState(false) + + const handleClose = () => { + setOpen(false) + } + + const handleOpen = () => { + setOpen(true) + } + + return ( + <> + + + { + duplicateNode(data.id) + }} + sx={{ height: '35px', width: '35px', '&:hover': { color: theme?.palette.primary.main } }} + color={theme?.customization?.isDarkMode ? theme.colors?.paper : 'inherit'} + > + + + { + deleteNode(data.id) + }} + sx={{ height: '35px', width: '35px', '&:hover': { color: 'red' } }} + color={theme?.customization?.isDarkMode ? theme.colors?.paper : 'inherit'} + > + + + + } + placement='right-start' + > + + (data.inputs[inputParam.name] = newValue)} + value={data.inputs[inputParam.name] ?? inputParam.default ?? ''} + nodes={inputParam?.acceptVariable && reactFlowInstance ? reactFlowInstance.getNodes() : []} + edges={inputParam?.acceptVariable && reactFlowInstance ? reactFlowInstance.getEdges() : []} + nodeId={data.id} + /> + + + + + ) +} + +StickyNote.propTypes = { + data: PropTypes.object +} + +export default StickyNote diff --git a/packages/ui/src/views/canvas/index.js b/packages/ui/src/views/canvas/index.js index 29602a4f..29f83ea4 100644 --- a/packages/ui/src/views/canvas/index.js +++ b/packages/ui/src/views/canvas/index.js @@ -21,6 +21,7 @@ import { useTheme } from '@mui/material/styles' // project imports import CanvasNode from './CanvasNode' import ButtonEdge from './ButtonEdge' +import StickyNote from './StickyNote' import CanvasHeader from './CanvasHeader' import AddNodes from './AddNodes' import ConfirmDialog from 'ui-component/dialog/ConfirmDialog' @@ -40,13 +41,13 @@ import useConfirm from 'hooks/useConfirm' import { IconX } from '@tabler/icons' // utils -import { getUniqueNodeId, initNode, getEdgeLabelName, rearrangeToolsOrdering, getUpsertDetails } from 'utils/genericHelper' +import { getUniqueNodeId, initNode, rearrangeToolsOrdering, getUpsertDetails } from 'utils/genericHelper' import useNotifier from 'utils/useNotifier' // const import { FLOWISE_CREDENTIAL_ID } from 'store/constant' -const nodeTypes = { customNode: CanvasNode } +const nodeTypes = { customNode: CanvasNode, stickyNote: StickyNote } const edgeTypes = { buttonedge: ButtonEdge } // ==============================|| CANVAS ||============================== // @@ -100,8 +101,7 @@ const Canvas = () => { const newEdge = { ...params, type: 'buttonedge', - id: `${params.source}-${params.sourceHandle}-${params.target}-${params.targetHandle}`, - data: { label: getEdgeLabelName(params.sourceHandle) } + id: `${params.source}-${params.sourceHandle}-${params.target}-${params.targetHandle}` } const targetNodeId = params.targetHandle.split('-')[0] @@ -170,7 +170,7 @@ const Canvas = () => { try { await chatflowsApi.deleteChatflow(chatflow.id) localStorage.removeItem(`${chatflow.id}_INTERNAL`) - navigate(-1) + navigate('/') } catch (error) { const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` enqueueSnackbar({ @@ -277,7 +277,7 @@ const Canvas = () => { const newNode = { id: newNodeId, position, - type: 'customNode', + type: nodeData.type !== 'StickyNote' ? 'customNode' : 'stickyNote', data: initNode(nodeData, newNodeId) } diff --git a/packages/ui/src/views/chatflows/ShareChatbot.js b/packages/ui/src/views/chatflows/ShareChatbot.js index dc6c0621..0bf5fc39 100644 --- a/packages/ui/src/views/chatflows/ShareChatbot.js +++ b/packages/ui/src/views/chatflows/ShareChatbot.js @@ -135,6 +135,8 @@ const ShareChatbot = ({ isSessionMemory }) => { if (isSessionMemory) obj.overrideConfig.generateNewSession = generateNewSession + if (chatbotConfig?.starterPrompts) obj.starterPrompts = chatbotConfig.starterPrompts + return obj } diff --git a/packages/ui/src/views/chatflows/index.js b/packages/ui/src/views/chatflows/index.js index 3c4b8972..c87ad306 100644 --- a/packages/ui/src/views/chatflows/index.js +++ b/packages/ui/src/views/chatflows/index.js @@ -12,6 +12,7 @@ import ItemCard from 'ui-component/cards/ItemCard' import { gridSpacing } from 'store/constant' import WorkflowEmptySVG from 'assets/images/workflow_empty.svg' import LoginDialog from 'ui-component/dialog/LoginDialog' +import ConfirmDialog from 'ui-component/dialog/ConfirmDialog' // API import chatflowsApi from 'api/chatflows' @@ -160,7 +161,6 @@ const Chatflows = () => { variant='contained' value='card' title='Card View' - selectedColor='#00abc0' > @@ -212,6 +212,7 @@ const Chatflows = () => { )} + ) } diff --git a/packages/ui/src/views/chatmessage/ChatMessage.js b/packages/ui/src/views/chatmessage/ChatMessage.js index c610f944..b52ff2da 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.js +++ b/packages/ui/src/views/chatmessage/ChatMessage.js @@ -32,6 +32,7 @@ import { baseURL, maxScroll } from 'store/constant' import robotPNG from 'assets/images/robot.png' import userPNG from 'assets/images/account.png' +import StarterPromptsCard from '../../ui-component/cards/StarterPromptsCard' import { isValidURL, removeDuplicateURL, setLocalStorageChatflow } from 'utils/genericHelper' export const ChatMessage = ({ open, chatflowid, isDialog }) => { @@ -57,6 +58,9 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { const inputRef = useRef(null) const getChatmessageApi = useApi(chatmessageApi.getInternalChatmessageFromChatflow) const getIsChatflowStreamingApi = useApi(chatflowsApi.getIsChatflowStreaming) + const getChatflowConfig = useApi(chatflowsApi.getSpecificChatflow) + + const [starterPrompts, setStarterPrompts] = useState([]) const onSourceDialogClick = (data, title) => { setSourceDialogProps({ data, title }) @@ -104,21 +108,30 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { }, 100) } - // Handle form submission - const handleSubmit = async (e) => { - e.preventDefault() + const handlePromptClick = async (promptStarterInput) => { + setUserInput(promptStarterInput) + handleSubmit(undefined, promptStarterInput) + } - if (userInput.trim() === '') { + // Handle form submission + const handleSubmit = async (e, promptStarterInput) => { + if (e) e.preventDefault() + + if (!promptStarterInput && userInput.trim() === '') { return } + let input = userInput + + if (promptStarterInput !== undefined && promptStarterInput.trim() !== '') input = promptStarterInput + setLoading(true) - setMessages((prevMessages) => [...prevMessages, { message: userInput, type: 'userMessage' }]) + setMessages((prevMessages) => [...prevMessages, { message: input, type: 'userMessage' }]) // Send user question and history to API try { const params = { - question: userInput, + question: input, history: messages.filter((msg) => msg.message !== 'Hi there! How can I help?'), chatId } @@ -223,10 +236,27 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { if (getIsChatflowStreamingApi.data) { setIsChatFlowAvailableToStream(getIsChatflowStreamingApi.data?.isStreaming ?? false) } - // eslint-disable-next-line react-hooks/exhaustive-deps }, [getIsChatflowStreamingApi.data]) + useEffect(() => { + if (getChatflowConfig.data) { + if (getChatflowConfig.data?.chatbotConfig && JSON.parse(getChatflowConfig.data?.chatbotConfig)) { + let config = JSON.parse(getChatflowConfig.data?.chatbotConfig) + if (config.starterPrompts) { + let inputFields = [] + Object.getOwnPropertyNames(config.starterPrompts).forEach((key) => { + if (config.starterPrompts[key]) { + inputFields.push(config.starterPrompts[key]) + } + }) + setStarterPrompts(inputFields) + } + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [getChatflowConfig.data]) + // Auto scroll chat to bottom useEffect(() => { scrollToBottom() @@ -245,6 +275,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { if (open && chatflowid) { getChatmessageApi.request(chatflowid) getIsChatflowStreamingApi.request(chatflowid) + getChatflowConfig.request(chatflowid) scrollToBottom() socket = socketIOClient(baseURL) @@ -412,7 +443,13 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { })} - + +
+ {messages && messages.length === 1 && ( + + )} + +
diff --git a/packages/ui/src/views/marketplaces/MarketplaceCanvas.js b/packages/ui/src/views/marketplaces/MarketplaceCanvas.js index 7ce29451..613f3cdb 100644 --- a/packages/ui/src/views/marketplaces/MarketplaceCanvas.js +++ b/packages/ui/src/views/marketplaces/MarketplaceCanvas.js @@ -11,10 +11,10 @@ import { useTheme } from '@mui/material/styles' // project imports import MarketplaceCanvasNode from './MarketplaceCanvasNode' - import MarketplaceCanvasHeader from './MarketplaceCanvasHeader' +import StickyNote from '../canvas/StickyNote' -const nodeTypes = { customNode: MarketplaceCanvasNode } +const nodeTypes = { customNode: MarketplaceCanvasNode, stickyNote: StickyNote } const edgeTypes = { buttonedge: '' } // ==============================|| CANVAS ||============================== // diff --git a/packages/ui/src/views/tools/HowToUseFunctionDialog.js b/packages/ui/src/views/tools/HowToUseFunctionDialog.js new file mode 100644 index 00000000..47ecee89 --- /dev/null +++ b/packages/ui/src/views/tools/HowToUseFunctionDialog.js @@ -0,0 +1,68 @@ +import { createPortal } from 'react-dom' +import PropTypes from 'prop-types' +import { Dialog, DialogContent, DialogTitle } from '@mui/material' + +const HowToUseFunctionDialog = ({ show, onCancel }) => { + const portalElement = document.getElementById('portal') + + const component = show ? ( + + + How To Use Function + + +
    +
  • You can use any libraries imported in Flowise
  • +
  • + You can use properties specified in Output Schema as variables with prefix $: +
      +
    • + Property = userid +
    • +
    • + Variable = $userid +
    • +
    +
  • +
  • + You can get default flow config: +
      +
    • + $flow.sessionId +
    • +
    • + $flow.chatId +
    • +
    • + $flow.chatflowId +
    • +
    • + $flow.input +
    • +
    +
  • +
  • + You can get custom variables: {`$vars.`} +
  • +
  • Must return a string value at the end of function
  • +
+
+
+ ) : null + + return createPortal(component, portalElement) +} + +HowToUseFunctionDialog.propTypes = { + show: PropTypes.bool, + onCancel: PropTypes.func +} + +export default HowToUseFunctionDialog diff --git a/packages/ui/src/views/tools/ToolDialog.js b/packages/ui/src/views/tools/ToolDialog.js index 398e9eb8..ab6d6aa0 100644 --- a/packages/ui/src/views/tools/ToolDialog.js +++ b/packages/ui/src/views/tools/ToolDialog.js @@ -12,9 +12,8 @@ import { TooltipWithParser } from 'ui-component/tooltip/TooltipWithParser' import { GridActionsCellItem } from '@mui/x-data-grid' import DeleteIcon from '@mui/icons-material/Delete' import ConfirmDialog from 'ui-component/dialog/ConfirmDialog' -import { DarkCodeEditor } from 'ui-component/editor/DarkCodeEditor' -import { LightCodeEditor } from 'ui-component/editor/LightCodeEditor' -import { useTheme } from '@mui/material/styles' +import { CodeEditor } from 'ui-component/editor/CodeEditor' +import HowToUseFunctionDialog from './HowToUseFunctionDialog' // Icons import { IconX, IconFileExport } from '@tabler/icons' @@ -34,6 +33,8 @@ import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions' const exampleAPIFunc = `/* * You can use any libraries imported in Flowise * You can use properties specified in Output Schema as variables. Ex: Property = userid, Variable = $userid +* You can get default flow config: $flow.sessionId, $flow.chatId, $flow.chatflowId, $flow.input +* You can get custom variables: $vars. * Must return a string value at the end of function */ @@ -56,7 +57,6 @@ try { const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm }) => { const portalElement = document.getElementById('portal') - const theme = useTheme() const customization = useSelector((state) => state.customization) const dispatch = useDispatch() @@ -77,6 +77,7 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm }) = const [toolIcon, setToolIcon] = useState('') const [toolSchema, setToolSchema] = useState([]) const [toolFunc, setToolFunc] = useState('') + const [showHowToDialog, setShowHowToDialog] = useState(false) const deleteItem = useCallback( (id) => () => { @@ -485,37 +486,27 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm }) = /> + {dialogProps.type !== 'TEMPLATE' && ( )} - {customization.isDarkMode ? ( - setToolFunc(code)} - style={{ - fontSize: '0.875rem', - minHeight: 'calc(100vh - 220px)', - width: '100%', - borderRadius: 5 - }} - /> - ) : ( - setToolFunc(code)} - style={{ - fontSize: '0.875rem', - minHeight: 'calc(100vh - 220px)', - width: '100%', - border: `1px solid ${theme.palette.grey[300]}`, - borderRadius: 5 - }} - /> - )} + setToolFunc(code)} + /> @@ -540,6 +531,7 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm }) = )} + setShowHowToDialog(false)} />
) : null diff --git a/packages/ui/src/views/variables/AddEditVariableDialog.js b/packages/ui/src/views/variables/AddEditVariableDialog.js new file mode 100644 index 00000000..933039e7 --- /dev/null +++ b/packages/ui/src/views/variables/AddEditVariableDialog.js @@ -0,0 +1,285 @@ +import { createPortal } from 'react-dom' +import PropTypes from 'prop-types' +import { useState, useEffect } from 'react' +import { useDispatch } from 'react-redux' +import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from 'store/actions' + +// Material +import { Button, Dialog, DialogActions, DialogContent, DialogTitle, Box, Typography, OutlinedInput } from '@mui/material' + +// Project imports +import { StyledButton } from 'ui-component/button/StyledButton' +import ConfirmDialog from 'ui-component/dialog/ConfirmDialog' + +// Icons +import { IconX, IconVariable } from '@tabler/icons' + +// API +import variablesApi from 'api/variables' + +// Hooks + +// utils +import useNotifier from 'utils/useNotifier' + +// const +import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions' +import { Dropdown } from '../../ui-component/dropdown/Dropdown' + +const variableTypes = [ + { + label: 'Static', + name: 'static', + description: 'Variable value will be read from the value entered below' + }, + { + label: 'Runtime', + name: 'runtime', + description: 'Variable value will be read from .env file' + } +] + +const AddEditVariableDialog = ({ show, dialogProps, onCancel, onConfirm }) => { + const portalElement = document.getElementById('portal') + + const dispatch = useDispatch() + + // ==============================|| Snackbar ||============================== // + + useNotifier() + + const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)) + const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) + + const [variableName, setVariableName] = useState('') + const [variableValue, setVariableValue] = useState('') + const [variableType, setVariableType] = useState('static') + const [dialogType, setDialogType] = useState('ADD') + const [variable, setVariable] = useState({}) + + useEffect(() => { + if (dialogProps.type === 'EDIT' && dialogProps.data) { + setVariableName(dialogProps.data.name) + setVariableValue(dialogProps.data.value) + setVariableType(dialogProps.data.type) + setDialogType('EDIT') + setVariable(dialogProps.data) + } else if (dialogProps.type === 'ADD') { + setVariableName('') + setVariableValue('') + setVariableType('static') + setDialogType('ADD') + setVariable({}) + } + + return () => { + setVariableName('') + setVariableValue('') + setVariableType('static') + setDialogType('ADD') + setVariable({}) + } + }, [dialogProps]) + + useEffect(() => { + if (show) dispatch({ type: SHOW_CANVAS_DIALOG }) + else dispatch({ type: HIDE_CANVAS_DIALOG }) + return () => dispatch({ type: HIDE_CANVAS_DIALOG }) + }, [show, dispatch]) + + const addNewVariable = async () => { + try { + const obj = { + name: variableName, + value: variableValue, + type: variableType + } + const createResp = await variablesApi.createVariable(obj) + if (createResp.data) { + enqueueSnackbar({ + message: 'New Variable added', + options: { + key: new Date().getTime() + Math.random(), + variant: 'success', + action: (key) => ( + + ) + } + }) + onConfirm(createResp.data.id) + } + } catch (err) { + const errorData = typeof err === 'string' ? err : err.response?.data || `${err.response?.status}: ${err.response?.statusText}` + enqueueSnackbar({ + message: `Failed to add new Variable: ${errorData}`, + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + persist: true, + action: (key) => ( + + ) + } + }) + onCancel() + } + } + + const saveVariable = async () => { + try { + const saveObj = { + name: variableName, + value: variableValue, + type: variableType + } + + const saveResp = await variablesApi.updateVariable(variable.id, saveObj) + if (saveResp.data) { + enqueueSnackbar({ + message: 'Variable saved', + options: { + key: new Date().getTime() + Math.random(), + variant: 'success', + action: (key) => ( + + ) + } + }) + onConfirm(saveResp.data.id) + } + } catch (error) { + const errorData = error.response?.data || `${error.response?.status}: ${error.response?.statusText}` + enqueueSnackbar({ + message: `Failed to save Variable: ${errorData}`, + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + persist: true, + action: (key) => ( + + ) + } + }) + onCancel() + } + } + + const component = show ? ( + + +
+
+ +
+ {dialogProps.type === 'ADD' ? 'Add Variable' : 'Edit Variable'} +
+
+ + +
+ + Variable Name * + + +
+
+ setVariableName(e.target.value)} + value={variableName ?? ''} + /> +
+ +
+ + Type * + +
+
+ setVariableType(newValue)} + value={variableType ?? 'choose an option'} + /> +
+ {variableType === 'static' && ( + +
+ + Value * + +
+
+ setVariableValue(e.target.value)} + value={variableValue ?? ''} + /> +
+ )} +
+ + (dialogType === 'ADD' ? addNewVariable() : saveVariable())} + > + {dialogProps.confirmButtonName} + + + +
+ ) : null + + return createPortal(component, portalElement) +} + +AddEditVariableDialog.propTypes = { + show: PropTypes.bool, + dialogProps: PropTypes.object, + onCancel: PropTypes.func, + onConfirm: PropTypes.func +} + +export default AddEditVariableDialog diff --git a/packages/ui/src/views/variables/HowToUseVariablesDialog.js b/packages/ui/src/views/variables/HowToUseVariablesDialog.js new file mode 100644 index 00000000..f328f226 --- /dev/null +++ b/packages/ui/src/views/variables/HowToUseVariablesDialog.js @@ -0,0 +1,72 @@ +import { createPortal } from 'react-dom' +import PropTypes from 'prop-types' +import { Dialog, DialogContent, DialogTitle } from '@mui/material' +import { CodeEditor } from 'ui-component/editor/CodeEditor' + +const overrideConfig = `{ + overrideConfig: { + vars: { + var1: 'abc' + } + } +}` + +const HowToUseVariablesDialog = ({ show, onCancel }) => { + const portalElement = document.getElementById('portal') + + const component = show ? ( + + + How To Use Variables + + +

Variables can be used in Custom Tool Function with the $ prefix.

+ `} + height={'50px'} + theme={'dark'} + lang={'js'} + basicSetup={{ highlightActiveLine: false, highlightActiveLineGutter: false }} + /> +

+ If variable type is Static, the value will be retrieved as it is. If variable type is Runtime, the value will be + retrieved from .env file. +

+

+ You can also override variable values in API overrideConfig using vars: +

+ +

+ Read more from{' '} + + docs + +

+
+
+ ) : null + + return createPortal(component, portalElement) +} + +HowToUseVariablesDialog.propTypes = { + show: PropTypes.bool, + onCancel: PropTypes.func +} + +export default HowToUseVariablesDialog diff --git a/packages/ui/src/views/variables/index.js b/packages/ui/src/views/variables/index.js new file mode 100644 index 00000000..9d0b2e3f --- /dev/null +++ b/packages/ui/src/views/variables/index.js @@ -0,0 +1,314 @@ +import { useEffect, useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from 'store/actions' +import moment from 'moment' + +// material-ui +import { + Button, + Box, + Stack, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + IconButton, + Toolbar, + TextField, + InputAdornment, + ButtonGroup, + Chip +} from '@mui/material' +import { useTheme } from '@mui/material/styles' + +// project imports +import MainCard from 'ui-component/cards/MainCard' +import { StyledButton } from 'ui-component/button/StyledButton' +import ConfirmDialog from 'ui-component/dialog/ConfirmDialog' + +// API +import variablesApi from 'api/variables' + +// Hooks +import useApi from 'hooks/useApi' +import useConfirm from 'hooks/useConfirm' + +// utils +import useNotifier from 'utils/useNotifier' + +// Icons +import { IconTrash, IconEdit, IconX, IconPlus, IconSearch, IconVariable } from '@tabler/icons' +import VariablesEmptySVG from 'assets/images/variables_empty.svg' + +// const +import AddEditVariableDialog from './AddEditVariableDialog' +import HowToUseVariablesDialog from './HowToUseVariablesDialog' + +// ==============================|| Credentials ||============================== // + +const Variables = () => { + const theme = useTheme() + const customization = useSelector((state) => state.customization) + + const dispatch = useDispatch() + useNotifier() + + const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)) + const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) + + const [showVariableDialog, setShowVariableDialog] = useState(false) + const [variableDialogProps, setVariableDialogProps] = useState({}) + const [variables, setVariables] = useState([]) + const [showHowToDialog, setShowHowToDialog] = useState(false) + + const { confirm } = useConfirm() + + const getAllVariables = useApi(variablesApi.getAllVariables) + + const [search, setSearch] = useState('') + const onSearchChange = (event) => { + setSearch(event.target.value) + } + function filterVariables(data) { + return data.name.toLowerCase().indexOf(search.toLowerCase()) > -1 + } + + const addNew = () => { + const dialogProp = { + type: 'ADD', + cancelButtonName: 'Cancel', + confirmButtonName: 'Add', + data: {} + } + setVariableDialogProps(dialogProp) + setShowVariableDialog(true) + } + + const edit = (variable) => { + const dialogProp = { + type: 'EDIT', + cancelButtonName: 'Cancel', + confirmButtonName: 'Save', + data: variable + } + setVariableDialogProps(dialogProp) + setShowVariableDialog(true) + } + + const deleteVariable = async (variable) => { + const confirmPayload = { + title: `Delete`, + description: `Delete variable ${variable.name}?`, + confirmButtonName: 'Delete', + cancelButtonName: 'Cancel' + } + const isConfirmed = await confirm(confirmPayload) + + if (isConfirmed) { + try { + const deleteResp = await variablesApi.deleteVariable(variable.id) + if (deleteResp.data) { + enqueueSnackbar({ + message: 'Variable deleted', + options: { + key: new Date().getTime() + Math.random(), + variant: 'success', + action: (key) => ( + + ) + } + }) + onConfirm() + } + } catch (error) { + const errorData = error.response?.data || `${error.response?.status}: ${error.response?.statusText}` + enqueueSnackbar({ + message: `Failed to delete Variable: ${errorData}`, + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + persist: true, + action: (key) => ( + + ) + } + }) + } + } + } + + const onConfirm = () => { + setShowVariableDialog(false) + getAllVariables.request() + } + + useEffect(() => { + getAllVariables.request() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + useEffect(() => { + if (getAllVariables.data) { + setVariables(getAllVariables.data) + } + }, [getAllVariables.data]) + + return ( + <> + + + + +

Variables 

+ + + + ) + }} + /> + + + + + } + > + Add Variable + + + +
+
+
+ {variables.length === 0 && ( + + + VariablesEmptySVG + +
No Variables Yet
+
+ )} + {variables.length > 0 && ( + + + + + Name + Value + Type + Last Updated + Created + + + + + + {variables.filter(filterVariables).map((variable, index) => ( + + +
+
+ +
+ {variable.name} +
+
+ {variable.value} + + + + {moment(variable.updatedDate).format('DD-MMM-YY')} + {moment(variable.createdDate).format('DD-MMM-YY')} + + edit(variable)}> + + + + + deleteVariable(variable)}> + + + +
+ ))} +
+
+
+ )} +
+ setShowVariableDialog(false)} + onConfirm={onConfirm} + > + setShowHowToDialog(false)}> + + + ) +} + +export default Variables