Compare commits

..

3 Commits

Author SHA1 Message Date
Henry d42e5f0701 Enhance file handling tools with security features
- Introduced new input parameters: workspacePath, enforceWorkspaceBoundaries, maxFileSize, and allowedExtensions for better control over file operations.
- Added validation for file paths and sizes to prevent unsafe operations.
- Implemented workspace boundary checks to restrict file access based on user-defined settings.
2025-10-01 09:54:23 +01:00
Henry 3525da7a6e Merge branch 'main' into chore/Tools-Warning 2025-09-30 23:57:05 +01:00
Henry 5731858db1 add tools warning 2025-09-23 15:51:46 +01:00
273 changed files with 3270 additions and 8567 deletions

View File

@ -1,72 +0,0 @@
name: Docker Image CI - Docker Hub
on:
workflow_dispatch:
inputs:
node_version:
description: 'Node.js version to build this image with.'
type: choice
required: true
default: '20'
options:
- '20'
tag_version:
description: 'Tag version of the image to be pushed.'
type: string
required: true
default: 'latest'
jobs:
docker:
runs-on: ubuntu-latest
steps:
- name: Set default values
id: defaults
run: |
echo "node_version=${{ github.event.inputs.node_version || '20' }}" >> $GITHUB_OUTPUT
echo "tag_version=${{ github.event.inputs.tag_version || 'latest' }}" >> $GITHUB_OUTPUT
- name: Checkout
uses: actions/checkout@v4.1.1
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.0.0
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# -------------------------
# Build and push main image
# -------------------------
- name: Build and push main image
uses: docker/build-push-action@v5.3.0
with:
context: .
file: ./docker/Dockerfile
build-args: |
NODE_VERSION=${{ steps.defaults.outputs.node_version }}
platforms: linux/amd64,linux/arm64
push: true
tags: |
flowiseai/flowise:${{ steps.defaults.outputs.tag_version }}
# -------------------------
# Build and push worker image
# -------------------------
- name: Build and push worker image
uses: docker/build-push-action@v5.3.0
with:
context: .
file: docker/worker/Dockerfile
build-args: |
NODE_VERSION=${{ steps.defaults.outputs.node_version }}
platforms: linux/amd64,linux/arm64
push: true
tags: |
flowiseai/flowise-worker:${{ steps.defaults.outputs.tag_version }}

View File

@ -1,73 +0,0 @@
name: Docker Image CI - AWS ECR
on:
workflow_dispatch:
inputs:
environment:
description: 'Environment to push the image to.'
required: true
default: 'dev'
type: choice
options:
- dev
- prod
node_version:
description: 'Node.js version to build this image with.'
type: choice
required: true
default: '20'
options:
- '20'
tag_version:
description: 'Tag version of the image to be pushed.'
type: string
required: true
default: 'latest'
jobs:
docker:
runs-on: ubuntu-latest
environment: ${{ github.event.inputs.environment }}
steps:
- name: Set default values
id: defaults
run: |
echo "node_version=${{ github.event.inputs.node_version || '20' }}" >> $GITHUB_OUTPUT
echo "tag_version=${{ github.event.inputs.tag_version || 'latest' }}" >> $GITHUB_OUTPUT
- name: Checkout
uses: actions/checkout@v4.1.1
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.0.0
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v3
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ secrets.AWS_REGION }}
- name: Login to Amazon ECR
uses: aws-actions/amazon-ecr-login@v1
# -------------------------
# Build and push main image
# -------------------------
- name: Build and push main image
uses: docker/build-push-action@v5.3.0
with:
context: .
file: Dockerfile
build-args: |
NODE_VERSION=${{ steps.defaults.outputs.node_version }}
platforms: linux/amd64,linux/arm64
push: true
tags: |
${{ format('{0}.dkr.ecr.{1}.amazonaws.com/flowise:{2}',
secrets.AWS_ACCOUNT_ID,
secrets.AWS_REGION,
steps.defaults.outputs.tag_version) }}

114
.github/workflows/docker-image.yml vendored Normal file
View File

@ -0,0 +1,114 @@
name: Docker Image CI
on:
workflow_dispatch:
inputs:
registry:
description: 'Container Registry to push the image to.'
type: choice
required: true
default: 'aws_ecr'
options:
- 'docker_hub'
- 'aws_ecr'
environment:
description: 'Environment to push the image to.'
required: true
default: 'dev'
type: choice
options:
- dev
- prod
image_type:
description: 'Type of image to build and push.'
type: choice
required: true
default: 'main'
options:
- 'main'
- 'worker'
node_version:
description: 'Node.js version to build this image with.'
type: choice
required: true
default: '20'
options:
- '20'
tag_version:
description: 'Tag version of the image to be pushed.'
type: string
required: true
default: 'latest'
jobs:
docker:
runs-on: ubuntu-latest
environment: ${{ github.event.inputs.environment }}
steps:
- name: Set default values
id: defaults
run: |
echo "registry=${{ github.event.inputs.registry || 'aws_ecr' }}" >> $GITHUB_OUTPUT
echo "image_type=${{ github.event.inputs.image_type || 'main' }}" >> $GITHUB_OUTPUT
echo "node_version=${{ github.event.inputs.node_version || '20' }}" >> $GITHUB_OUTPUT
echo "tag_version=${{ github.event.inputs.tag_version || 'latest' }}" >> $GITHUB_OUTPUT
- name: Checkout
uses: actions/checkout@v4.1.1
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.0.0
# ------------------------
# Login Steps (conditional)
# ------------------------
- name: Login to Docker Hub
if: steps.defaults.outputs.registry == 'docker_hub'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Configure AWS Credentials
if: steps.defaults.outputs.registry == 'aws_ecr'
uses: aws-actions/configure-aws-credentials@v3
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ secrets.AWS_REGION }}
- name: Login to Amazon ECR
if: steps.defaults.outputs.registry == 'aws_ecr'
uses: aws-actions/amazon-ecr-login@v1
# -------------------------
# Build and push (conditional tags)
# -------------------------
- name: Build and push
uses: docker/build-push-action@v5.3.0
with:
context: .
file: |
${{
steps.defaults.outputs.image_type == 'worker' && 'docker/worker/Dockerfile' ||
(steps.defaults.outputs.registry == 'docker_hub' && './docker/Dockerfile' || 'Dockerfile')
}}
build-args: |
NODE_VERSION=${{ steps.defaults.outputs.node_version }}
platforms: linux/amd64,linux/arm64
push: true
tags: |
${{
steps.defaults.outputs.registry == 'docker_hub' &&
format('flowiseai/flowise{0}:{1}',
steps.defaults.outputs.image_type == 'worker' && '-worker' || '',
steps.defaults.outputs.tag_version) ||
format('{0}.dkr.ecr.{1}.amazonaws.com/flowise{2}:{3}',
secrets.AWS_ACCOUNT_ID,
secrets.AWS_REGION,
steps.defaults.outputs.image_type == 'worker' && '-worker' || '',
steps.defaults.outputs.tag_version)
}}

View File

@ -114,7 +114,7 @@ Flowise has 3 different modules in a single mono repository.
to make sure everything works fine in production.
11. Commit code and submit Pull Request from forked branch pointing to [Flowise main](https://github.com/FlowiseAI/Flowise/tree/main).
11. Commit code and submit Pull Request from forked branch pointing to [Flowise master](https://github.com/FlowiseAI/Flowise/tree/master).
## 🌱 Env Variables

View File

@ -5,41 +5,34 @@
# docker run -d -p 3000:3000 flowise
FROM node:20-alpine
RUN apk add --update libc6-compat python3 make g++
# needed for pdfjs-dist
RUN apk add --no-cache build-base cairo-dev pango-dev
# Install system dependencies and build tools
RUN apk update && \
apk add --no-cache \
libc6-compat \
python3 \
make \
g++ \
build-base \
cairo-dev \
pango-dev \
chromium \
curl && \
npm install -g pnpm
# Install Chromium
RUN apk add --no-cache chromium
# Install curl for container-level health checks
# Fixes: https://github.com/FlowiseAI/Flowise/issues/4126
RUN apk add --no-cache curl
#install PNPM globaly
RUN npm install -g pnpm
ENV PUPPETEER_SKIP_DOWNLOAD=true
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser
ENV NODE_OPTIONS=--max-old-space-size=8192
WORKDIR /usr/src/flowise
WORKDIR /usr/src
# Copy app source
COPY . .
# Install dependencies and build
RUN pnpm install && \
pnpm build
RUN pnpm install
# Give the node user ownership of the application files
RUN chown -R node:node .
# Switch to non-root user (node user already exists in node:20-alpine)
USER node
RUN pnpm build
EXPOSE 3000
CMD [ "pnpm", "start" ]
CMD [ "pnpm", "start" ]

View File

@ -190,10 +190,6 @@ Deploy Flowise self-hosted in your existing infrastructure, we support various [
[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/pn4G8S?referralCode=WVNPD9)
- [Northflank](https://northflank.com/stacks/deploy-flowiseai)
[![Deploy to Northflank](https://assets.northflank.com/deploy_to_northflank_smm_36700fb050.svg)](https://northflank.com/stacks/deploy-flowiseai)
- [Render](https://docs.flowiseai.com/configuration/deployment/render)
[![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://docs.flowiseai.com/configuration/deployment/render)

View File

@ -1,38 +1,40 @@
### Responsible Disclosure Policy
### Responsible Disclosure Policy
At Flowise, we prioritize security and continuously work to safeguard our systems. However, vulnerabilities can still exist. If you identify a security issue, please report it to us so we can address it promptly. Your cooperation helps us better protect our platform and users.
At Flowise, we prioritize security and continuously work to safeguard our systems. However, vulnerabilities can still exist. If you identify a security issue, please report it to us so we can address it promptly. Your cooperation helps us better protect our platform and users.
### Out of scope vulnerabilities
### Vulnerabilities
- Clickjacking on pages without sensitive actions
- CSRF on unauthenticated/logout/login pages
- Attacks requiring MITM (Man-in-the-Middle) or physical device access
- Social engineering attacks
- Activities that cause service disruption (DoS)
- Content spoofing and text injection without a valid attack vector
- Email spoofing
- Absence of DNSSEC, CAA, CSP headers
- Missing Secure or HTTP-only flag on non-sensitive cookies
- Deadlinks
- User enumeration
The following types of issues are some of the most common vulnerabilities:
### Reporting Guidelines
- Clickjacking on pages without sensitive actions
- CSRF on unauthenticated/logout/login pages
- Attacks requiring MITM (Man-in-the-Middle) or physical device access
- Social engineering attacks
- Activities that cause service disruption (DoS)
- Content spoofing and text injection without a valid attack vector
- Email spoofing
- Absence of DNSSEC, CAA, CSP headers
- Missing Secure or HTTP-only flag on non-sensitive cookies
- Deadlinks
- User enumeration
- Submit your findings to https://github.com/FlowiseAI/Flowise/security
- Provide clear details to help us reproduce and fix the issue quickly.
### Reporting Guidelines
### Disclosure Guidelines
- Submit your findings to https://github.com/FlowiseAI/Flowise/security
- Provide clear details to help us reproduce and fix the issue quickly.
- Do not publicly disclose vulnerabilities until we have assessed, resolved, and notified affected users.
- If you plan to present your research (e.g., at a conference or in a blog), share a draft with us at least **30 days in advance** for review.
- Avoid including:
- Data from any Flowise customer projects
- Flowise user/customer information
- Details about Flowise employees, contractors, or partners
### Disclosure Guidelines
### Response to Reports
- Do not publicly disclose vulnerabilities until we have assessed, resolved, and notified affected users.
- If you plan to present your research (e.g., at a conference or in a blog), share a draft with us at least **30 days in advance** for review.
- Avoid including:
- Data from any Flowise customer projects
- Flowise user/customer information
- Details about Flowise employees, contractors, or partners
- We will acknowledge your report within **5 business days** and provide an estimated resolution timeline.
- Your report will be kept **confidential**, and your details will not be shared without your consent.
### Response to Reports
We appreciate your efforts in helping us maintain a secure platform and look forward to working together to resolve any issues responsibly.
- We will acknowledge your report within **5 business days** and provide an estimated resolution timeline.
- Your report will be kept **confidential**, and your details will not be shared without your consent.
We appreciate your efforts in helping us maintain a secure platform and look forward to working together to resolve any issues responsibly.

View File

@ -38,8 +38,6 @@ SECRETKEY_PATH=/root/.flowise
# DEBUG=true
LOG_PATH=/root/.flowise/logs
# LOG_LEVEL=info #(error | warn | info | verbose | debug)
# LOG_SANITIZE_BODY_FIELDS=password,pwd,pass,secret,token,apikey,api_key,accesstoken,access_token,refreshtoken,refresh_token,clientsecret,client_secret,privatekey,private_key,secretkey,secret_key,auth,authorization,credential,credentials
# LOG_SANITIZE_HEADER_FIELDS=authorization,x-api-key,x-auth-token,cookie
# TOOL_FUNCTION_BUILTIN_DEP=crypto,fs
# TOOL_FUNCTION_EXTERNAL_DEP=moment,lodash
# ALLOW_BUILTIN_DEP=false
@ -101,7 +99,6 @@ JWT_TOKEN_EXPIRY_IN_MINUTES=360
JWT_REFRESH_TOKEN_EXPIRY_IN_MINUTES=43200
# EXPIRE_AUTH_TOKENS_ON_RESTART=true # (if you need to expire all tokens on app restart)
# EXPRESS_SESSION_SECRET=flowise
# SECURE_COOKIES=
# INVITE_TOKEN_EXPIRY_IN_HOURS=24
# PASSWORD_RESET_TOKEN_EXPIRY_IN_MINS=15

View File

@ -46,8 +46,6 @@ services:
- DEBUG=${DEBUG}
- LOG_PATH=${LOG_PATH}
- LOG_LEVEL=${LOG_LEVEL}
- LOG_SANITIZE_BODY_FIELDS=${LOG_SANITIZE_BODY_FIELDS}
- LOG_SANITIZE_HEADER_FIELDS=${LOG_SANITIZE_HEADER_FIELDS}
# CUSTOM TOOL/FUNCTION DEPENDENCIES
- TOOL_FUNCTION_BUILTIN_DEP=${TOOL_FUNCTION_BUILTIN_DEP}
@ -91,7 +89,6 @@ services:
- PASSWORD_RESET_TOKEN_EXPIRY_IN_MINS=${PASSWORD_RESET_TOKEN_EXPIRY_IN_MINS}
- PASSWORD_SALT_HASH_ROUNDS=${PASSWORD_SALT_HASH_ROUNDS}
- TOKEN_HASH_SECRET=${TOKEN_HASH_SECRET}
- SECURE_COOKIES=${SECURE_COOKIES}
# EMAIL
- SMTP_HOST=${SMTP_HOST}
@ -192,8 +189,6 @@ services:
- DEBUG=${DEBUG}
- LOG_PATH=${LOG_PATH}
- LOG_LEVEL=${LOG_LEVEL}
- LOG_SANITIZE_BODY_FIELDS=${LOG_SANITIZE_BODY_FIELDS}
- LOG_SANITIZE_HEADER_FIELDS=${LOG_SANITIZE_HEADER_FIELDS}
# CUSTOM TOOL/FUNCTION DEPENDENCIES
- TOOL_FUNCTION_BUILTIN_DEP=${TOOL_FUNCTION_BUILTIN_DEP}
@ -237,7 +232,6 @@ services:
- PASSWORD_RESET_TOKEN_EXPIRY_IN_MINS=${PASSWORD_RESET_TOKEN_EXPIRY_IN_MINS}
- PASSWORD_SALT_HASH_ROUNDS=${PASSWORD_SALT_HASH_ROUNDS}
- TOKEN_HASH_SECRET=${TOKEN_HASH_SECRET}
- SECURE_COOKIES=${SECURE_COOKIES}
# EMAIL
- SMTP_HOST=${SMTP_HOST}

View File

@ -31,8 +31,6 @@ services:
- DEBUG=${DEBUG}
- LOG_PATH=${LOG_PATH}
- LOG_LEVEL=${LOG_LEVEL}
- LOG_SANITIZE_BODY_FIELDS=${LOG_SANITIZE_BODY_FIELDS}
- LOG_SANITIZE_HEADER_FIELDS=${LOG_SANITIZE_HEADER_FIELDS}
# CUSTOM TOOL/FUNCTION DEPENDENCIES
- TOOL_FUNCTION_BUILTIN_DEP=${TOOL_FUNCTION_BUILTIN_DEP}
@ -76,7 +74,6 @@ services:
- PASSWORD_RESET_TOKEN_EXPIRY_IN_MINS=${PASSWORD_RESET_TOKEN_EXPIRY_IN_MINS}
- PASSWORD_SALT_HASH_ROUNDS=${PASSWORD_SALT_HASH_ROUNDS}
- TOKEN_HASH_SECRET=${TOKEN_HASH_SECRET}
- SECURE_COOKIES=${SECURE_COOKIES}
# EMAIL
- SMTP_HOST=${SMTP_HOST}

View File

@ -38,8 +38,6 @@ SECRETKEY_PATH=/root/.flowise
# DEBUG=true
LOG_PATH=/root/.flowise/logs
# LOG_LEVEL=info #(error | warn | info | verbose | debug)
# LOG_SANITIZE_BODY_FIELDS=password,pwd,pass,secret,token,apikey,api_key,accesstoken,access_token,refreshtoken,refresh_token,clientsecret,client_secret,privatekey,private_key,secretkey,secret_key,auth,authorization,credential,credentials
# LOG_SANITIZE_HEADER_FIELDS=authorization,x-api-key,x-auth-token,cookie
# TOOL_FUNCTION_BUILTIN_DEP=crypto,fs
# TOOL_FUNCTION_EXTERNAL_DEP=moment,lodash
# ALLOW_BUILTIN_DEP=false
@ -101,7 +99,6 @@ JWT_TOKEN_EXPIRY_IN_MINUTES=360
JWT_REFRESH_TOKEN_EXPIRY_IN_MINUTES=43200
# EXPIRE_AUTH_TOKENS_ON_RESTART=true # (if you need to expire all tokens on app restart)
# EXPRESS_SESSION_SECRET=flowise
# SECURE_COOKIES=
# INVITE_TOKEN_EXPIRY_IN_HOURS=24
# PASSWORD_RESET_TOKEN_EXPIRY_IN_MINS=15

View File

@ -7,7 +7,7 @@ RUN apk add --no-cache build-base cairo-dev pango-dev
# Install Chromium and curl for container-level health checks
RUN apk add --no-cache chromium curl
#install PNPM globally
#install PNPM globaly
RUN npm install -g pnpm
ENV PUPPETEER_SKIP_DOWNLOAD=true

View File

@ -31,8 +31,6 @@ services:
- DEBUG=${DEBUG}
- LOG_PATH=${LOG_PATH}
- LOG_LEVEL=${LOG_LEVEL}
- LOG_SANITIZE_BODY_FIELDS=${LOG_SANITIZE_BODY_FIELDS}
- LOG_SANITIZE_HEADER_FIELDS=${LOG_SANITIZE_HEADER_FIELDS}
# CUSTOM TOOL/FUNCTION DEPENDENCIES
- TOOL_FUNCTION_BUILTIN_DEP=${TOOL_FUNCTION_BUILTIN_DEP}
@ -76,7 +74,6 @@ services:
- PASSWORD_RESET_TOKEN_EXPIRY_IN_MINS=${PASSWORD_RESET_TOKEN_EXPIRY_IN_MINS}
- PASSWORD_SALT_HASH_ROUNDS=${PASSWORD_SALT_HASH_ROUNDS}
- TOKEN_HASH_SECRET=${TOKEN_HASH_SECRET}
- SECURE_COOKIES=${SECURE_COOKIES}
# EMAIL
- SMTP_HOST=${SMTP_HOST}

View File

@ -112,7 +112,7 @@ Flowise 在一个单一的单体存储库中有 3 个不同的模块。
pnpm start
```
11. 提交代码并从指向 [Flowise 主分支](https://github.com/FlowiseAI/Flowise/tree/main) 的分叉分支上提交 Pull Request。
11. 提交代码并从指向 [Flowise 主分支](https://github.com/FlowiseAI/Flowise/tree/master) 的分叉分支上提交 Pull Request。
## 🌱 环境变量

View File

@ -1,7 +1,6 @@
version: "2"
services:
otel-collector:
read_only: true
image: otel/opentelemetry-collector-contrib
command: ["--config=/etc/otelcol-contrib/config.yaml", "--feature-gates=-exporter.datadogexporter.DisableAPMStats", "${OTELCOL_ARGS}"]
volumes:

View File

@ -1,6 +1,6 @@
{
"name": "flowise",
"version": "3.0.11",
"version": "3.0.7",
"private": true,
"homepage": "https://flowiseai.com",
"workspaces": [
@ -51,7 +51,7 @@
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-unused-imports": "^2.0.0",
"husky": "^8.0.1",
"kill-port": "2.0.1",
"kill-port": "^2.0.1",
"lint-staged": "^13.0.3",
"prettier": "^2.7.1",
"pretty-quick": "^3.1.3",

View File

@ -1,26 +0,0 @@
import { INodeParams, INodeCredential } from '../src/Interface'
class ElevenLabsApi implements INodeCredential {
label: string
name: string
version: number
description: string
inputs: INodeParams[]
constructor() {
this.label = 'Eleven Labs API'
this.name = 'elevenLabsApi'
this.version = 1.0
this.description =
'Sign up for a Eleven Labs account and <a target="_blank" href="https://elevenlabs.io/app/settings/api-keys">create an API Key</a>.'
this.inputs = [
{
label: 'Eleven Labs API Key',
name: 'elevenLabsApiKey',
type: 'password'
}
]
}
}
module.exports = { credClass: ElevenLabsApi }

View File

@ -1,47 +0,0 @@
import { INodeParams, INodeCredential } from '../src/Interface'
class TeradataVectorStoreApiCredentials implements INodeCredential {
label: string
name: string
version: number
inputs: INodeParams[]
constructor() {
this.label = 'Teradata Vector Store API Credentials'
this.name = 'teradataVectorStoreApiCredentials'
this.version = 1.0
this.inputs = [
{
label: 'Teradata Host IP',
name: 'tdHostIp',
type: 'string'
},
{
label: 'Username',
name: 'tdUsername',
type: 'string'
},
{
label: 'Password',
name: 'tdPassword',
type: 'password'
},
{
label: 'Vector_Store_Base_URL',
name: 'baseURL',
description: 'Teradata Vector Store Base URL',
placeholder: `Base_URL`,
type: 'string'
},
{
label: 'JWT Token',
name: 'jwtToken',
type: 'password',
description: 'Bearer token for JWT authentication',
optional: true
}
]
}
}
module.exports = { credClass: TeradataVectorStoreApiCredentials }

View File

@ -3,13 +3,6 @@
{
"name": "awsChatBedrock",
"models": [
{
"label": "anthropic.claude-opus-4-5-20251101-v1:0",
"name": "anthropic.claude-opus-4-5-20251101-v1:0",
"description": "Claude 4.5 Opus",
"input_cost": 0.000005,
"output_cost": 0.000025
},
{
"label": "anthropic.claude-sonnet-4-5-20250929-v1:0",
"name": "anthropic.claude-sonnet-4-5-20250929-v1:0",
@ -17,13 +10,6 @@
"input_cost": 0.000003,
"output_cost": 0.000015
},
{
"label": "anthropic.claude-haiku-4-5-20251001-v1:0",
"name": "anthropic.claude-haiku-4-5-20251001-v1:0",
"description": "Claude 4.5 Haiku",
"input_cost": 0.000001,
"output_cost": 0.000005
},
{
"label": "openai.gpt-oss-20b-1:0",
"name": "openai.gpt-oss-20b-1:0",
@ -322,12 +308,6 @@
{
"name": "azureChatOpenAI",
"models": [
{
"label": "gpt-5.1",
"name": "gpt-5.1",
"input_cost": 0.00000125,
"output_cost": 0.00001
},
{
"label": "gpt-5",
"name": "gpt-5",
@ -512,13 +492,6 @@
{
"name": "chatAnthropic",
"models": [
{
"label": "claude-opus-4-5",
"name": "claude-opus-4-5",
"description": "Claude 4.5 Opus",
"input_cost": 0.000005,
"output_cost": 0.000025
},
{
"label": "claude-sonnet-4-5",
"name": "claude-sonnet-4-5",
@ -526,13 +499,6 @@
"input_cost": 0.000003,
"output_cost": 0.000015
},
{
"label": "claude-haiku-4-5",
"name": "claude-haiku-4-5",
"description": "Claude 4.5 Haiku",
"input_cost": 0.000001,
"output_cost": 0.000005
},
{
"label": "claude-sonnet-4-0",
"name": "claude-sonnet-4-0",
@ -641,18 +607,6 @@
{
"name": "chatGoogleGenerativeAI",
"models": [
{
"label": "gemini-3-pro-preview",
"name": "gemini-3-pro-preview",
"input_cost": 0.00002,
"output_cost": 0.00012
},
{
"label": "gemini-3-pro-image-preview",
"name": "gemini-3-pro-image-preview",
"input_cost": 0.00002,
"output_cost": 0.00012
},
{
"label": "gemini-2.5-pro",
"name": "gemini-2.5-pro",
@ -665,12 +619,6 @@
"input_cost": 1.25e-6,
"output_cost": 0.00001
},
{
"label": "gemini-2.5-flash-image",
"name": "gemini-2.5-flash-image",
"input_cost": 1.25e-6,
"output_cost": 0.00001
},
{
"label": "gemini-2.5-flash-lite",
"name": "gemini-2.5-flash-lite",
@ -723,12 +671,6 @@
{
"name": "chatGoogleVertexAI",
"models": [
{
"label": "gemini-3-pro-preview",
"name": "gemini-3-pro-preview",
"input_cost": 0.00002,
"output_cost": 0.00012
},
{
"label": "gemini-2.5-pro",
"name": "gemini-2.5-pro",
@ -795,13 +737,6 @@
"input_cost": 1.25e-7,
"output_cost": 3.75e-7
},
{
"label": "claude-opus-4-5@20251101",
"name": "claude-opus-4-5@20251101",
"description": "Claude 4.5 Opus",
"input_cost": 0.000005,
"output_cost": 0.000025
},
{
"label": "claude-sonnet-4-5@20250929",
"name": "claude-sonnet-4-5@20250929",
@ -809,13 +744,6 @@
"input_cost": 0.000003,
"output_cost": 0.000015
},
{
"label": "claude-haiku-4-5@20251001",
"name": "claude-haiku-4-5@20251001",
"description": "Claude 4.5 Haiku",
"input_cost": 0.000001,
"output_cost": 0.000005
},
{
"label": "claude-opus-4-1@20250805",
"name": "claude-opus-4-1@20250805",
@ -1047,12 +975,6 @@
{
"name": "chatOpenAI",
"models": [
{
"label": "gpt-5.1",
"name": "gpt-5.1",
"input_cost": 0.00000125,
"output_cost": 0.00001
},
{
"label": "gpt-5",
"name": "gpt-5",

View File

@ -22,16 +22,15 @@ import zodToJsonSchema from 'zod-to-json-schema'
import { getErrorMessage } from '../../../src/error'
import { DataSource } from 'typeorm'
import {
addImageArtifactsToMessages,
extractArtifactsFromResponse,
getPastChatHistoryImageMessages,
getUniqueImageMessages,
processMessagesWithImages,
replaceBase64ImagesWithFileReferences,
replaceInlineDataWithFileReferences,
updateFlowState
} from '../utils'
import { convertMultiOptionsToStringArray, processTemplateVariables, configureStructuredOutput } from '../../../src/utils'
import { convertMultiOptionsToStringArray, getCredentialData, getCredentialParam, processTemplateVariables } from '../../../src/utils'
import { addSingleFileToStorage } from '../../../src/storageUtils'
import fetch from 'node-fetch'
interface ITool {
agentSelectedTool: string
@ -82,7 +81,7 @@ class Agent_Agentflow implements INode {
constructor() {
this.label = 'Agent'
this.name = 'agentAgentflow'
this.version = 3.2
this.version = 2.2
this.type = 'Agent'
this.category = 'Agent Flows'
this.description = 'Dynamically choose and utilize tools during runtime, enabling multi-step reasoning'
@ -177,11 +176,6 @@ class Agent_Agentflow implements INode {
label: 'Google Search',
name: 'googleSearch',
description: 'Search real-time web content'
},
{
label: 'Code Execution',
name: 'codeExecution',
description: 'Write and run Python code in a sandboxed environment'
}
],
show: {
@ -400,108 +394,6 @@ class Agent_Agentflow implements INode {
],
default: 'userMessage'
},
{
label: 'JSON Structured Output',
name: 'agentStructuredOutput',
description: 'Instruct the Agent to give output in a JSON structured schema',
type: 'array',
optional: true,
acceptVariable: true,
array: [
{
label: 'Key',
name: 'key',
type: 'string'
},
{
label: 'Type',
name: 'type',
type: 'options',
options: [
{
label: 'String',
name: 'string'
},
{
label: 'String Array',
name: 'stringArray'
},
{
label: 'Number',
name: 'number'
},
{
label: 'Boolean',
name: 'boolean'
},
{
label: 'Enum',
name: 'enum'
},
{
label: 'JSON Array',
name: 'jsonArray'
}
]
},
{
label: 'Enum Values',
name: 'enumValues',
type: 'string',
placeholder: 'value1, value2, value3',
description: 'Enum values. Separated by comma',
optional: true,
show: {
'agentStructuredOutput[$index].type': 'enum'
}
},
{
label: 'JSON Schema',
name: 'jsonSchema',
type: 'code',
placeholder: `{
"answer": {
"type": "string",
"description": "Value of the answer"
},
"reason": {
"type": "string",
"description": "Reason for the answer"
},
"optional": {
"type": "boolean"
},
"count": {
"type": "number"
},
"children": {
"type": "array",
"items": {
"type": "object",
"properties": {
"value": {
"type": "string",
"description": "Value of the children's answer"
}
}
}
}
}`,
description: 'JSON schema for the structured output',
optional: true,
hideCodeExecute: true,
show: {
'agentStructuredOutput[$index].type': 'jsonArray'
}
},
{
label: 'Description',
name: 'description',
type: 'string',
placeholder: 'Description of the key'
}
]
},
{
label: 'Update Flow State',
name: 'agentUpdateState',
@ -514,7 +406,8 @@ class Agent_Agentflow implements INode {
label: 'Key',
name: 'key',
type: 'asyncOptions',
loadMethod: 'listRuntimeStateKeys'
loadMethod: 'listRuntimeStateKeys',
freeSolo: true
},
{
label: 'Value',
@ -877,7 +770,6 @@ class Agent_Agentflow implements INode {
const memoryType = nodeData.inputs?.agentMemoryType as string
const userMessage = nodeData.inputs?.agentUserMessage as string
const _agentUpdateState = nodeData.inputs?.agentUpdateState
const _agentStructuredOutput = nodeData.inputs?.agentStructuredOutput
const agentMessages = (nodeData.inputs?.agentMessages as unknown as ILLMMessage[]) ?? []
// Extract runtime state and history
@ -903,8 +795,6 @@ class Agent_Agentflow implements INode {
const llmWithoutToolsBind = (await newLLMNodeInstance.init(newNodeData, '', options)) as BaseChatModel
let llmNodeInstance = llmWithoutToolsBind
const isStructuredOutput = _agentStructuredOutput && Array.isArray(_agentStructuredOutput) && _agentStructuredOutput.length > 0
const agentToolsBuiltInOpenAI = convertMultiOptionsToStringArray(nodeData.inputs?.agentToolsBuiltInOpenAI)
if (agentToolsBuiltInOpenAI && agentToolsBuiltInOpenAI.length > 0) {
for (const tool of agentToolsBuiltInOpenAI) {
@ -1063,7 +953,7 @@ class Agent_Agentflow implements INode {
// Initialize response and determine if streaming is possible
let response: AIMessageChunk = new AIMessageChunk('')
const isLastNode = options.isLastNode as boolean
const isStreamable = isLastNode && options.sseStreamer !== undefined && modelConfig?.streaming !== false && !isStructuredOutput
const isStreamable = isLastNode && options.sseStreamer !== undefined && modelConfig?.streaming !== false
// Start analytics
if (analyticHandlers && options.parentTraceIds) {
@ -1071,6 +961,12 @@ class Agent_Agentflow implements INode {
llmIds = await analyticHandlers.onLLMStart(llmLabel, messages, options.parentTraceIds)
}
// Track execution time
const startTime = Date.now()
// Get initial response from LLM
const sseStreamer: IServerSideEventStreamer | undefined = options.sseStreamer
// Handle tool calls with support for recursion
let usedTools: IUsedTool[] = []
let sourceDocuments: Array<any> = []
@ -1083,24 +979,12 @@ class Agent_Agentflow implements INode {
const messagesBeforeToolCalls = [...messages]
let _toolCallMessages: BaseMessageLike[] = []
/**
* Add image artifacts from previous assistant responses as user messages
* Images are converted from FILE-STORAGE::<image_path> to base 64 image_url format
*/
await addImageArtifactsToMessages(messages, options)
// Check if this is hummanInput for tool calls
const _humanInput = nodeData.inputs?.humanInput
const humanInput: IHumanInput = typeof _humanInput === 'string' ? JSON.parse(_humanInput) : _humanInput
const humanInputAction = options.humanInputAction
const iterationContext = options.iterationContext
// Track execution time
const startTime = Date.now()
// Get initial response from LLM
const sseStreamer: IServerSideEventStreamer | undefined = options.sseStreamer
if (humanInput) {
if (humanInput.type !== 'proceed' && humanInput.type !== 'reject') {
throw new Error(`Invalid human input type. Expected 'proceed' or 'reject', but got '${humanInput.type}'`)
@ -1118,8 +1002,7 @@ class Agent_Agentflow implements INode {
llmWithoutToolsBind,
isStreamable,
isLastNode,
iterationContext,
isStructuredOutput
iterationContext
})
response = result.response
@ -1148,14 +1031,7 @@ class Agent_Agentflow implements INode {
}
} else {
if (isStreamable) {
response = await this.handleStreamingResponse(
sseStreamer,
llmNodeInstance,
messages,
chatId,
abortController,
isStructuredOutput
)
response = await this.handleStreamingResponse(sseStreamer, llmNodeInstance, messages, chatId, abortController)
} else {
response = await llmNodeInstance.invoke(messages, { signal: abortController?.signal })
}
@ -1177,8 +1053,7 @@ class Agent_Agentflow implements INode {
llmNodeInstance,
isStreamable,
isLastNode,
iterationContext,
isStructuredOutput
iterationContext
})
response = result.response
@ -1205,20 +1080,11 @@ class Agent_Agentflow implements INode {
sseStreamer.streamArtifactsEvent(chatId, flatten(artifacts))
}
}
} else if (!humanInput && !isStreamable && isLastNode && sseStreamer && !isStructuredOutput) {
} else if (!humanInput && !isStreamable && isLastNode && sseStreamer) {
// Stream whole response back to UI if not streaming and no tool calls
// Skip this if structured output is enabled - it will be streamed after conversion
let finalResponse = ''
if (response.content && Array.isArray(response.content)) {
finalResponse = response.content
.map((item: any) => {
if ((item.text && !item.type) || (item.type === 'text' && item.text)) {
return item.text
}
return ''
})
.filter((text: string) => text)
.join('\n')
finalResponse = response.content.map((item: any) => item.text).join('\n')
} else if (response.content && typeof response.content === 'string') {
finalResponse = response.content
} else {
@ -1247,53 +1113,9 @@ class Agent_Agentflow implements INode {
// Prepare final response and output object
let finalResponse = ''
if (response.content && Array.isArray(response.content)) {
// Process items and concatenate consecutive text items
const processedParts: string[] = []
let currentTextBuffer = ''
for (const item of response.content) {
const itemAny = item as any
const isTextItem = (itemAny.text && !itemAny.type) || (itemAny.type === 'text' && itemAny.text)
if (isTextItem) {
// Accumulate consecutive text items
currentTextBuffer += itemAny.text
} else {
// Flush accumulated text before processing other types
if (currentTextBuffer) {
processedParts.push(currentTextBuffer)
currentTextBuffer = ''
}
// Process non-text items
if (itemAny.type === 'executableCode' && itemAny.executableCode) {
// Format executable code as a code block
const language = itemAny.executableCode.language?.toLowerCase() || 'python'
processedParts.push(`\n\`\`\`${language}\n${itemAny.executableCode.code}\n\`\`\`\n`)
} else if (itemAny.type === 'codeExecutionResult' && itemAny.codeExecutionResult) {
// Format code execution result
const outcome = itemAny.codeExecutionResult.outcome || 'OUTCOME_OK'
const output = itemAny.codeExecutionResult.output || ''
if (outcome === 'OUTCOME_OK' && output) {
processedParts.push(`**Code Output:**\n\`\`\`\n${output}\n\`\`\`\n`)
} else if (outcome !== 'OUTCOME_OK') {
processedParts.push(`**Code Execution Error:**\n\`\`\`\n${output}\n\`\`\`\n`)
}
}
}
}
// Flush any remaining text
if (currentTextBuffer) {
processedParts.push(currentTextBuffer)
}
finalResponse = processedParts.filter((text) => text).join('\n')
finalResponse = response.content.map((item: any) => item.text).join('\n')
} else if (response.content && typeof response.content === 'string') {
finalResponse = response.content
} else if (response.content === '') {
// Empty response content, this could happen when there is only image data
finalResponse = ''
} else {
finalResponse = JSON.stringify(response, null, 2)
}
@ -1309,13 +1131,10 @@ class Agent_Agentflow implements INode {
}
}
// Extract artifacts from annotations in response metadata and replace inline data
// Extract artifacts from annotations in response metadata
if (response.response_metadata) {
const {
artifacts: extractedArtifacts,
fileAnnotations: extractedFileAnnotations,
savedInlineImages
} = await extractArtifactsFromResponse(response.response_metadata, newNodeData, options)
const { artifacts: extractedArtifacts, fileAnnotations: extractedFileAnnotations } =
await this.extractArtifactsFromResponse(response.response_metadata, newNodeData, options)
if (extractedArtifacts.length > 0) {
artifacts = [...artifacts, ...extractedArtifacts]
@ -1333,11 +1152,6 @@ class Agent_Agentflow implements INode {
sseStreamer.streamFileAnnotationsEvent(chatId, fileAnnotations)
}
}
// Replace inlineData base64 with file references in the response
if (savedInlineImages && savedInlineImages.length > 0) {
replaceInlineDataWithFileReferences(response, savedInlineImages)
}
}
// Replace sandbox links with proper download URLs. Example: [Download the script](sandbox:/mnt/data/dummy_bar_graph.py)
@ -1345,23 +1159,6 @@ class Agent_Agentflow implements INode {
finalResponse = await this.processSandboxLinks(finalResponse, options.baseURL, options.chatflowid, chatId)
}
// If is structured output, then invoke LLM again with structured output at the very end after all tool calls
if (isStructuredOutput) {
llmNodeInstance = configureStructuredOutput(llmNodeInstance, _agentStructuredOutput)
const prompt = 'Convert the following response to the structured output format: ' + finalResponse
response = await llmNodeInstance.invoke(prompt, { signal: abortController?.signal })
if (typeof response === 'object') {
finalResponse = '```json\n' + JSON.stringify(response, null, 2) + '\n```'
} else {
finalResponse = response
}
if (isLastNode && sseStreamer) {
sseStreamer.streamTokenEvent(chatId, finalResponse)
}
}
const output = this.prepareOutputObject(
response,
availableTools,
@ -1374,8 +1171,7 @@ class Agent_Agentflow implements INode {
artifacts,
additionalTokens,
isWaitingForHumanInput,
fileAnnotations,
isStructuredOutput
fileAnnotations
)
// End analytics tracking
@ -1396,15 +1192,9 @@ class Agent_Agentflow implements INode {
// Process template variables in state
newState = processTemplateVariables(newState, finalResponse)
/**
* Remove the temporarily added image artifact messages before storing
* This is to avoid storing the actual base64 data into database
*/
const messagesToStore = messages.filter((msg: any) => !msg._isTemporaryImageMessage)
// Replace the actual messages array with one that includes the file references for images instead of base64 data
const messagesWithFileReferences = replaceBase64ImagesWithFileReferences(
messagesToStore,
messages,
runtimeImageMessagesWithFileRef,
pastImageMessagesWithFileRef
)
@ -1543,12 +1333,7 @@ class Agent_Agentflow implements INode {
// Handle Gemini googleSearch tool
if (groundingMetadata && groundingMetadata.webSearchQueries && Array.isArray(groundingMetadata.webSearchQueries)) {
// Check for duplicates
const isDuplicate = builtInUsedTools.find(
(tool) =>
tool.tool === 'googleSearch' &&
JSON.stringify((tool.toolInput as any)?.queries) === JSON.stringify(groundingMetadata.webSearchQueries)
)
if (!isDuplicate) {
if (!builtInUsedTools.find((tool) => tool.tool === 'googleSearch')) {
builtInUsedTools.push({
tool: 'googleSearch',
toolInput: {
@ -1562,12 +1347,7 @@ class Agent_Agentflow implements INode {
// Handle Gemini urlContext tool
if (urlContextMetadata && urlContextMetadata.urlMetadata && Array.isArray(urlContextMetadata.urlMetadata)) {
// Check for duplicates
const isDuplicate = builtInUsedTools.find(
(tool) =>
tool.tool === 'urlContext' &&
JSON.stringify((tool.toolInput as any)?.urlMetadata) === JSON.stringify(urlContextMetadata.urlMetadata)
)
if (!isDuplicate) {
if (!builtInUsedTools.find((tool) => tool.tool === 'urlContext')) {
builtInUsedTools.push({
tool: 'urlContext',
toolInput: {
@ -1578,55 +1358,47 @@ class Agent_Agentflow implements INode {
}
}
// Handle Gemini codeExecution tool
if (response.content && Array.isArray(response.content)) {
for (let i = 0; i < response.content.length; i++) {
const item = response.content[i]
if (item.type === 'executableCode' && item.executableCode) {
const language = item.executableCode.language || 'PYTHON'
const code = item.executableCode.code || ''
let toolOutput = ''
// Check for duplicates
const isDuplicate = builtInUsedTools.find(
(tool) =>
tool.tool === 'codeExecution' &&
(tool.toolInput as any)?.language === language &&
(tool.toolInput as any)?.code === code
)
if (isDuplicate) {
continue
}
// Check the next item for the output
const nextItem = i + 1 < response.content.length ? response.content[i + 1] : null
if (nextItem) {
if (nextItem.type === 'codeExecutionResult' && nextItem.codeExecutionResult) {
const outcome = nextItem.codeExecutionResult.outcome
const output = nextItem.codeExecutionResult.output || ''
toolOutput = outcome === 'OUTCOME_OK' ? output : `Error: ${output}`
} else if (nextItem.type === 'inlineData') {
toolOutput = 'Generated image data'
}
}
builtInUsedTools.push({
tool: 'codeExecution',
toolInput: {
language,
code
},
toolOutput
})
}
}
}
return builtInUsedTools
}
/**
* Saves base64 image data to storage and returns file information
*/
private async saveBase64Image(
outputItem: any,
options: ICommonObject
): Promise<{ filePath: string; fileName: string; totalSize: number } | null> {
try {
if (!outputItem.result) {
return null
}
// Extract base64 data and create buffer
const base64Data = outputItem.result
const imageBuffer = Buffer.from(base64Data, 'base64')
// Determine file extension and MIME type
const outputFormat = outputItem.output_format || 'png'
const fileName = `generated_image_${outputItem.id || Date.now()}.${outputFormat}`
const mimeType = outputFormat === 'png' ? 'image/png' : 'image/jpeg'
// Save the image using the existing storage utility
const { path, totalSize } = await addSingleFileToStorage(
mimeType,
imageBuffer,
fileName,
options.orgId,
options.chatflowid,
options.chatId
)
return { filePath: path, fileName, totalSize }
} catch (error) {
console.error('Error saving base64 image:', error)
return null
}
}
/**
* Handles memory management based on the specified memory type
*/
@ -1789,62 +1561,32 @@ class Agent_Agentflow implements INode {
llmNodeInstance: BaseChatModel,
messages: BaseMessageLike[],
chatId: string,
abortController: AbortController,
isStructuredOutput: boolean = false
abortController: AbortController
): Promise<AIMessageChunk> {
let response = new AIMessageChunk('')
try {
for await (const chunk of await llmNodeInstance.stream(messages, { signal: abortController?.signal })) {
if (sseStreamer && !isStructuredOutput) {
if (sseStreamer) {
let content = ''
if (typeof chunk === 'string') {
content = chunk
} else if (Array.isArray(chunk.content) && chunk.content.length > 0) {
content = chunk.content
.map((item: any) => {
if ((item.text && !item.type) || (item.type === 'text' && item.text)) {
return item.text
} else if (item.type === 'executableCode' && item.executableCode) {
const language = item.executableCode.language?.toLowerCase() || 'python'
return `\n\`\`\`${language}\n${item.executableCode.code}\n\`\`\`\n`
} else if (item.type === 'codeExecutionResult' && item.codeExecutionResult) {
const outcome = item.codeExecutionResult.outcome || 'OUTCOME_OK'
const output = item.codeExecutionResult.output || ''
if (outcome === 'OUTCOME_OK' && output) {
return `**Code Output:**\n\`\`\`\n${output}\n\`\`\`\n`
} else if (outcome !== 'OUTCOME_OK') {
return `**Code Execution Error:**\n\`\`\`\n${output}\n\`\`\`\n`
}
}
return ''
})
.filter((text: string) => text)
.join('')
} else if (chunk.content) {
if (Array.isArray(chunk.content) && chunk.content.length > 0) {
const contents = chunk.content as MessageContentText[]
content = contents.map((item) => item.text).join('')
} else {
content = chunk.content.toString()
}
sseStreamer.streamTokenEvent(chatId, content)
}
const messageChunk = typeof chunk === 'string' ? new AIMessageChunk(chunk) : chunk
response = response.concat(messageChunk)
response = response.concat(chunk)
}
} catch (error) {
console.error('Error during streaming:', error)
throw error
}
// Only convert to string if all content items are text (no inlineData or other special types)
if (Array.isArray(response.content) && response.content.length > 0) {
const hasNonTextContent = response.content.some(
(item: any) => item.type === 'inlineData' || item.type === 'executableCode' || item.type === 'codeExecutionResult'
)
if (!hasNonTextContent) {
const responseContents = response.content as MessageContentText[]
response.content = responseContents.map((item) => item.text).join('')
}
const responseContents = response.content as MessageContentText[]
response.content = responseContents.map((item) => item.text).join('')
}
return response
}
@ -1864,8 +1606,7 @@ class Agent_Agentflow implements INode {
artifacts: any[],
additionalTokens: number = 0,
isWaitingForHumanInput: boolean = false,
fileAnnotations: any[] = [],
isStructuredOutput: boolean = false
fileAnnotations: any[] = []
): any {
const output: any = {
content: finalResponse,
@ -1900,15 +1641,6 @@ class Agent_Agentflow implements INode {
output.responseMetadata = response.response_metadata
}
if (isStructuredOutput && typeof response === 'object') {
const structuredOutput = response as Record<string, any>
for (const key in structuredOutput) {
if (structuredOutput[key] !== undefined && structuredOutput[key] !== null) {
output[key] = structuredOutput[key]
}
}
}
// Add used tools, source documents and artifacts to output
if (usedTools && usedTools.length > 0) {
output.usedTools = flatten(usedTools)
@ -1944,12 +1676,7 @@ class Agent_Agentflow implements INode {
const sseStreamer: IServerSideEventStreamer = options.sseStreamer as IServerSideEventStreamer
if (response.tool_calls) {
const formattedToolCalls = response.tool_calls.map((toolCall: any) => ({
tool: toolCall.name || 'tool',
toolInput: toolCall.args,
toolOutput: ''
}))
sseStreamer.streamCalledToolsEvent(chatId, flatten(formattedToolCalls))
sseStreamer.streamCalledToolsEvent(chatId, response.tool_calls)
}
if (response.usage_metadata) {
@ -1974,8 +1701,7 @@ class Agent_Agentflow implements INode {
llmNodeInstance,
isStreamable,
isLastNode,
iterationContext,
isStructuredOutput = false
iterationContext
}: {
response: AIMessageChunk
messages: BaseMessageLike[]
@ -1989,7 +1715,6 @@ class Agent_Agentflow implements INode {
isStreamable: boolean
isLastNode: boolean
iterationContext: ICommonObject
isStructuredOutput?: boolean
}): Promise<{
response: AIMessageChunk
usedTools: IUsedTool[]
@ -2011,12 +1736,7 @@ class Agent_Agentflow implements INode {
// Stream tool calls if available
if (sseStreamer) {
const formattedToolCalls = response.tool_calls.map((toolCall: any) => ({
tool: toolCall.name || 'tool',
toolInput: toolCall.args,
toolOutput: ''
}))
sseStreamer.streamCalledToolsEvent(chatId, flatten(formattedToolCalls))
sseStreamer.streamCalledToolsEvent(chatId, JSON.stringify(response.tool_calls))
}
// Remove tool calls with no id
@ -2069,9 +1789,7 @@ class Agent_Agentflow implements INode {
const toolCallDetails = '```json\n' + JSON.stringify(toolCall, null, 2) + '\n```'
const responseContent = response.content + `\nAttempting to use tool:\n${toolCallDetails}`
response.content = responseContent
if (!isStructuredOutput) {
sseStreamer?.streamTokenEvent(chatId, responseContent)
}
sseStreamer?.streamTokenEvent(chatId, responseContent)
return { response, usedTools, sourceDocuments, artifacts, totalTokens, isWaitingForHumanInput: true }
}
@ -2177,7 +1895,7 @@ class Agent_Agentflow implements INode {
const lastToolOutput = usedTools[0]?.toolOutput || ''
const lastToolOutputString = typeof lastToolOutput === 'string' ? lastToolOutput : JSON.stringify(lastToolOutput, null, 2)
if (sseStreamer && !isStructuredOutput) {
if (sseStreamer) {
sseStreamer.streamTokenEvent(chatId, lastToolOutputString)
}
@ -2206,19 +1924,12 @@ class Agent_Agentflow implements INode {
let newResponse: AIMessageChunk
if (isStreamable) {
newResponse = await this.handleStreamingResponse(
sseStreamer,
llmNodeInstance,
messages,
chatId,
abortController,
isStructuredOutput
)
newResponse = await this.handleStreamingResponse(sseStreamer, llmNodeInstance, messages, chatId, abortController)
} else {
newResponse = await llmNodeInstance.invoke(messages, { signal: abortController?.signal })
// Stream non-streaming response if this is the last node
if (isLastNode && sseStreamer && !isStructuredOutput) {
if (isLastNode && sseStreamer) {
let responseContent = JSON.stringify(newResponse, null, 2)
if (typeof newResponse.content === 'string') {
responseContent = newResponse.content
@ -2253,8 +1964,7 @@ class Agent_Agentflow implements INode {
llmNodeInstance,
isStreamable,
isLastNode,
iterationContext,
isStructuredOutput
iterationContext
})
// Merge results from recursive tool calls
@ -2285,8 +1995,7 @@ class Agent_Agentflow implements INode {
llmWithoutToolsBind,
isStreamable,
isLastNode,
iterationContext,
isStructuredOutput = false
iterationContext
}: {
humanInput: IHumanInput
humanInputAction: Record<string, any> | undefined
@ -2301,7 +2010,6 @@ class Agent_Agentflow implements INode {
isStreamable: boolean
isLastNode: boolean
iterationContext: ICommonObject
isStructuredOutput?: boolean
}): Promise<{
response: AIMessageChunk
usedTools: IUsedTool[]
@ -2337,12 +2045,7 @@ class Agent_Agentflow implements INode {
// Stream tool calls if available
if (sseStreamer) {
const formattedToolCalls = response.tool_calls.map((toolCall: any) => ({
tool: toolCall.name || 'tool',
toolInput: toolCall.args,
toolOutput: ''
}))
sseStreamer.streamCalledToolsEvent(chatId, flatten(formattedToolCalls))
sseStreamer.streamCalledToolsEvent(chatId, JSON.stringify(response.tool_calls))
}
// Remove tool calls with no id
@ -2504,7 +2207,7 @@ class Agent_Agentflow implements INode {
const lastToolOutput = usedTools[0]?.toolOutput || ''
const lastToolOutputString = typeof lastToolOutput === 'string' ? lastToolOutput : JSON.stringify(lastToolOutput, null, 2)
if (sseStreamer && !isStructuredOutput) {
if (sseStreamer) {
sseStreamer.streamTokenEvent(chatId, lastToolOutputString)
}
@ -2535,19 +2238,12 @@ class Agent_Agentflow implements INode {
}
if (isStreamable) {
newResponse = await this.handleStreamingResponse(
sseStreamer,
llmNodeInstance,
messages,
chatId,
abortController,
isStructuredOutput
)
newResponse = await this.handleStreamingResponse(sseStreamer, llmNodeInstance, messages, chatId, abortController)
} else {
newResponse = await llmNodeInstance.invoke(messages, { signal: abortController?.signal })
// Stream non-streaming response if this is the last node
if (isLastNode && sseStreamer && !isStructuredOutput) {
if (isLastNode && sseStreamer) {
let responseContent = JSON.stringify(newResponse, null, 2)
if (typeof newResponse.content === 'string') {
responseContent = newResponse.content
@ -2582,8 +2278,7 @@ class Agent_Agentflow implements INode {
llmNodeInstance,
isStreamable,
isLastNode,
iterationContext,
isStructuredOutput
iterationContext
})
// Merge results from recursive tool calls
@ -2598,6 +2293,190 @@ class Agent_Agentflow implements INode {
return { response: newResponse, usedTools, sourceDocuments, artifacts, totalTokens, isWaitingForHumanInput }
}
/**
* Extracts artifacts from response metadata (both annotations and built-in tools)
*/
private async extractArtifactsFromResponse(
responseMetadata: any,
modelNodeData: INodeData,
options: ICommonObject
): Promise<{ artifacts: any[]; fileAnnotations: any[] }> {
const artifacts: any[] = []
const fileAnnotations: any[] = []
if (!responseMetadata?.output || !Array.isArray(responseMetadata.output)) {
return { artifacts, fileAnnotations }
}
for (const outputItem of responseMetadata.output) {
// Handle container file citations from annotations
if (outputItem.type === 'message' && outputItem.content && Array.isArray(outputItem.content)) {
for (const contentItem of outputItem.content) {
if (contentItem.annotations && Array.isArray(contentItem.annotations)) {
for (const annotation of contentItem.annotations) {
if (annotation.type === 'container_file_citation' && annotation.file_id && annotation.filename) {
try {
// Download and store the file content
const downloadResult = await this.downloadContainerFile(
annotation.container_id,
annotation.file_id,
annotation.filename,
modelNodeData,
options
)
if (downloadResult) {
const fileType = this.getArtifactTypeFromFilename(annotation.filename)
if (fileType === 'png' || fileType === 'jpeg' || fileType === 'jpg') {
const artifact = {
type: fileType,
data: downloadResult.filePath
}
artifacts.push(artifact)
} else {
fileAnnotations.push({
filePath: downloadResult.filePath,
fileName: annotation.filename
})
}
}
} catch (error) {
console.error('Error processing annotation:', error)
}
}
}
}
}
}
// Handle built-in tool artifacts (like image generation)
if (outputItem.type === 'image_generation_call' && outputItem.result) {
try {
const savedImageResult = await this.saveBase64Image(outputItem, options)
if (savedImageResult) {
// Replace the base64 result with the file path in the response metadata
outputItem.result = savedImageResult.filePath
// Create artifact in the same format as other image artifacts
const fileType = this.getArtifactTypeFromFilename(savedImageResult.fileName)
artifacts.push({
type: fileType,
data: savedImageResult.filePath
})
}
} catch (error) {
console.error('Error processing image generation artifact:', error)
}
}
}
return { artifacts, fileAnnotations }
}
/**
* Downloads file content from container file citation
*/
private async downloadContainerFile(
containerId: string,
fileId: string,
filename: string,
modelNodeData: INodeData,
options: ICommonObject
): Promise<{ filePath: string; totalSize: number } | null> {
try {
const credentialData = await getCredentialData(modelNodeData.credential ?? '', options)
const openAIApiKey = getCredentialParam('openAIApiKey', credentialData, modelNodeData)
if (!openAIApiKey) {
console.warn('No OpenAI API key available for downloading container file')
return null
}
// Download the file using OpenAI Container API
const response = await fetch(`https://api.openai.com/v1/containers/${containerId}/files/${fileId}/content`, {
method: 'GET',
headers: {
Accept: '*/*',
Authorization: `Bearer ${openAIApiKey}`
}
})
if (!response.ok) {
console.warn(
`Failed to download container file ${fileId} from container ${containerId}: ${response.status} ${response.statusText}`
)
return null
}
// Extract the binary data from the Response object
const data = await response.arrayBuffer()
const dataBuffer = Buffer.from(data)
const mimeType = this.getMimeTypeFromFilename(filename)
// Store the file using the same storage utility as OpenAIAssistant
const { path, totalSize } = await addSingleFileToStorage(
mimeType,
dataBuffer,
filename,
options.orgId,
options.chatflowid,
options.chatId
)
return { filePath: path, totalSize }
} catch (error) {
console.error('Error downloading container file:', error)
return null
}
}
/**
* Gets MIME type from filename extension
*/
private getMimeTypeFromFilename(filename: string): string {
const extension = filename.toLowerCase().split('.').pop()
const mimeTypes: { [key: string]: string } = {
png: 'image/png',
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
gif: 'image/gif',
pdf: 'application/pdf',
txt: 'text/plain',
csv: 'text/csv',
json: 'application/json',
html: 'text/html',
xml: 'application/xml'
}
return mimeTypes[extension || ''] || 'application/octet-stream'
}
/**
* Gets artifact type from filename extension for UI rendering
*/
private getArtifactTypeFromFilename(filename: string): string {
const extension = filename.toLowerCase().split('.').pop()
const artifactTypes: { [key: string]: string } = {
png: 'png',
jpg: 'jpeg',
jpeg: 'jpeg',
html: 'html',
htm: 'html',
md: 'markdown',
markdown: 'markdown',
json: 'json',
js: 'javascript',
javascript: 'javascript',
tex: 'latex',
latex: 'latex',
txt: 'text',
csv: 'text',
pdf: 'text'
}
return artifactTypes[extension || ''] || 'text'
}
/**
* Processes sandbox links in the response text and converts them to file annotations
*/

View File

@ -317,7 +317,7 @@ class Condition_Agentflow implements INode {
}
}
// If no condition is fulfilled, add isFulfilled to the ELSE condition
// If no condition is fullfilled, add isFulfilled to the ELSE condition
const dummyElseConditionData = {
type: 'string',
value1: '',

View File

@ -60,7 +60,7 @@ class CustomFunction_Agentflow implements INode {
constructor() {
this.label = 'Custom Function'
this.name = 'customFunctionAgentflow'
this.version = 1.1
this.version = 1.0
this.type = 'CustomFunction'
this.category = 'Agent Flows'
this.description = 'Execute custom function'
@ -107,7 +107,8 @@ class CustomFunction_Agentflow implements INode {
label: 'Key',
name: 'key',
type: 'asyncOptions',
loadMethod: 'listRuntimeStateKeys'
loadMethod: 'listRuntimeStateKeys',
freeSolo: true
},
{
label: 'Value',
@ -133,7 +134,7 @@ class CustomFunction_Agentflow implements INode {
async run(nodeData: INodeData, input: string, options: ICommonObject): Promise<any> {
const javascriptFunction = nodeData.inputs?.customFunctionJavascriptFunction as string
const functionInputVariables = (nodeData.inputs?.customFunctionInputVariables as ICustomFunctionInputVariables[]) ?? []
const functionInputVariables = nodeData.inputs?.customFunctionInputVariables as ICustomFunctionInputVariables[]
const _customFunctionUpdateState = nodeData.inputs?.customFunctionUpdateState
const state = options.agentflowRuntime?.state as ICommonObject
@ -146,17 +147,11 @@ class CustomFunction_Agentflow implements INode {
const variables = await getVars(appDataSource, databaseEntities, nodeData, options)
const flow = {
input,
state,
chatflowId: options.chatflowid,
sessionId: options.sessionId,
chatId: options.chatId,
rawOutput: options.postProcessing?.rawOutput || '',
chatHistory: options.postProcessing?.chatHistory || [],
sourceDocuments: options.postProcessing?.sourceDocuments,
usedTools: options.postProcessing?.usedTools,
artifacts: options.postProcessing?.artifacts,
fileAnnotations: options.postProcessing?.fileAnnotations
input,
state
}
// Create additional sandbox variables for custom function inputs

View File

@ -30,7 +30,7 @@ class ExecuteFlow_Agentflow implements INode {
constructor() {
this.label = 'Execute Flow'
this.name = 'executeFlowAgentflow'
this.version = 1.2
this.version = 1.1
this.type = 'ExecuteFlow'
this.category = 'Agent Flows'
this.description = 'Execute another flow'
@ -102,7 +102,8 @@ class ExecuteFlow_Agentflow implements INode {
label: 'Key',
name: 'key',
type: 'asyncOptions',
loadMethod: 'listRuntimeStateKeys'
loadMethod: 'listRuntimeStateKeys',
freeSolo: true
},
{
label: 'Value',

View File

@ -67,8 +67,7 @@ class HTTP_Agentflow implements INode {
{
label: 'URL',
name: 'url',
type: 'string',
acceptVariable: true
type: 'string'
},
{
label: 'Headers',

View File

@ -241,11 +241,8 @@ class HumanInput_Agentflow implements INode {
if (isStreamable) {
const sseStreamer: IServerSideEventStreamer = options.sseStreamer as IServerSideEventStreamer
for await (const chunk of await llmNodeInstance.stream(messages)) {
const content = typeof chunk === 'string' ? chunk : chunk.content.toString()
sseStreamer.streamTokenEvent(chatId, content)
const messageChunk = typeof chunk === 'string' ? new AIMessageChunk(chunk) : chunk
response = response.concat(messageChunk)
sseStreamer.streamTokenEvent(chatId, chunk.content.toString())
response = response.concat(chunk)
}
humanInputDescription = response.content as string
} else {

View File

@ -2,20 +2,17 @@ import { BaseChatModel } from '@langchain/core/language_models/chat_models'
import { ICommonObject, IMessage, INode, INodeData, INodeOptionsValue, INodeParams, IServerSideEventStreamer } from '../../../src/Interface'
import { AIMessageChunk, BaseMessageLike, MessageContentText } from '@langchain/core/messages'
import { DEFAULT_SUMMARIZER_TEMPLATE } from '../prompt'
import { z } from 'zod'
import { AnalyticHandler } from '../../../src/handler'
import { ILLMMessage } from '../Interface.Agentflow'
import { ILLMMessage, IStructuredOutput } from '../Interface.Agentflow'
import {
addImageArtifactsToMessages,
extractArtifactsFromResponse,
getPastChatHistoryImageMessages,
getUniqueImageMessages,
processMessagesWithImages,
replaceBase64ImagesWithFileReferences,
replaceInlineDataWithFileReferences,
updateFlowState
} from '../utils'
import { processTemplateVariables, configureStructuredOutput } from '../../../src/utils'
import { flatten } from 'lodash'
import { processTemplateVariables } from '../../../src/utils'
class LLM_Agentflow implements INode {
label: string
@ -34,7 +31,7 @@ class LLM_Agentflow implements INode {
constructor() {
this.label = 'LLM'
this.name = 'llmAgentflow'
this.version = 1.1
this.version = 1.0
this.type = 'LLM'
this.category = 'Agent Flows'
this.description = 'Large language models to analyze user-provided inputs and generate responses'
@ -290,7 +287,8 @@ class LLM_Agentflow implements INode {
label: 'Key',
name: 'key',
type: 'asyncOptions',
loadMethod: 'listRuntimeStateKeys'
loadMethod: 'listRuntimeStateKeys',
freeSolo: true
},
{
label: 'Value',
@ -450,16 +448,10 @@ class LLM_Agentflow implements INode {
}
delete nodeData.inputs?.llmMessages
/**
* Add image artifacts from previous assistant responses as user messages
* Images are converted from FILE-STORAGE::<image_path> to base 64 image_url format
*/
await addImageArtifactsToMessages(messages, options)
// Configure structured output if specified
const isStructuredOutput = _llmStructuredOutput && Array.isArray(_llmStructuredOutput) && _llmStructuredOutput.length > 0
if (isStructuredOutput) {
llmNodeInstance = configureStructuredOutput(llmNodeInstance, _llmStructuredOutput)
llmNodeInstance = this.configureStructuredOutput(llmNodeInstance, _llmStructuredOutput)
}
// Initialize response and determine if streaming is possible
@ -475,11 +467,9 @@ class LLM_Agentflow implements INode {
// Track execution time
const startTime = Date.now()
const sseStreamer: IServerSideEventStreamer | undefined = options.sseStreamer
/*
* Invoke LLM
*/
if (isStreamable) {
response = await this.handleStreamingResponse(sseStreamer, llmNodeInstance, messages, chatId, abortController)
} else {
@ -504,40 +494,6 @@ class LLM_Agentflow implements INode {
const endTime = Date.now()
const timeDelta = endTime - startTime
// Extract artifacts and file annotations from response metadata
let artifacts: any[] = []
let fileAnnotations: any[] = []
if (response.response_metadata) {
const {
artifacts: extractedArtifacts,
fileAnnotations: extractedFileAnnotations,
savedInlineImages
} = await extractArtifactsFromResponse(response.response_metadata, newNodeData, options)
if (extractedArtifacts.length > 0) {
artifacts = extractedArtifacts
// Stream artifacts if this is the last node
if (isLastNode && sseStreamer) {
sseStreamer.streamArtifactsEvent(chatId, artifacts)
}
}
if (extractedFileAnnotations.length > 0) {
fileAnnotations = extractedFileAnnotations
// Stream file annotations if this is the last node
if (isLastNode && sseStreamer) {
sseStreamer.streamFileAnnotationsEvent(chatId, fileAnnotations)
}
}
// Replace inlineData base64 with file references in the response
if (savedInlineImages && savedInlineImages.length > 0) {
replaceInlineDataWithFileReferences(response, savedInlineImages)
}
}
// Update flow state if needed
let newState = { ...state }
if (_llmUpdateState && Array.isArray(_llmUpdateState) && _llmUpdateState.length > 0) {
@ -557,22 +513,10 @@ class LLM_Agentflow implements INode {
finalResponse = response.content.map((item: any) => item.text).join('\n')
} else if (response.content && typeof response.content === 'string') {
finalResponse = response.content
} else if (response.content === '') {
// Empty response content, this could happen when there is only image data
finalResponse = ''
} else {
finalResponse = JSON.stringify(response, null, 2)
}
const output = this.prepareOutputObject(
response,
finalResponse,
startTime,
endTime,
timeDelta,
isStructuredOutput,
artifacts,
fileAnnotations
)
const output = this.prepareOutputObject(response, finalResponse, startTime, endTime, timeDelta, isStructuredOutput)
// End analytics tracking
if (analyticHandlers && llmIds) {
@ -584,23 +528,12 @@ class LLM_Agentflow implements INode {
this.sendStreamingEvents(options, chatId, response)
}
// Stream file annotations if any were extracted
if (fileAnnotations.length > 0 && isLastNode && sseStreamer) {
sseStreamer.streamFileAnnotationsEvent(chatId, fileAnnotations)
}
// Process template variables in state
newState = processTemplateVariables(newState, finalResponse)
/**
* Remove the temporarily added image artifact messages before storing
* This is to avoid storing the actual base64 data into database
*/
const messagesToStore = messages.filter((msg: any) => !msg._isTemporaryImageMessage)
// Replace the actual messages array with one that includes the file references for images instead of base64 data
const messagesWithFileReferences = replaceBase64ImagesWithFileReferences(
messagesToStore,
messages,
runtimeImageMessagesWithFileRef,
pastImageMessagesWithFileRef
)
@ -651,13 +584,7 @@ class LLM_Agentflow implements INode {
{
role: returnRole,
content: finalResponse,
name: nodeData?.label ? nodeData?.label.toLowerCase().replace(/\s/g, '_').trim() : nodeData?.id,
...(((artifacts && artifacts.length > 0) || (fileAnnotations && fileAnnotations.length > 0)) && {
additional_kwargs: {
...(artifacts && artifacts.length > 0 && { artifacts }),
...(fileAnnotations && fileAnnotations.length > 0 && { fileAnnotations })
}
})
name: nodeData?.label ? nodeData?.label.toLowerCase().replace(/\s/g, '_').trim() : nodeData?.id
}
]
}
@ -827,6 +754,59 @@ class LLM_Agentflow implements INode {
}
}
/**
* Configures structured output for the LLM
*/
private configureStructuredOutput(llmNodeInstance: BaseChatModel, llmStructuredOutput: IStructuredOutput[]): BaseChatModel {
try {
const zodObj: ICommonObject = {}
for (const sch of llmStructuredOutput) {
if (sch.type === 'string') {
zodObj[sch.key] = z.string().describe(sch.description || '')
} else if (sch.type === 'stringArray') {
zodObj[sch.key] = z.array(z.string()).describe(sch.description || '')
} else if (sch.type === 'number') {
zodObj[sch.key] = z.number().describe(sch.description || '')
} else if (sch.type === 'boolean') {
zodObj[sch.key] = z.boolean().describe(sch.description || '')
} else if (sch.type === 'enum') {
const enumValues = sch.enumValues?.split(',').map((item: string) => item.trim()) || []
zodObj[sch.key] = z
.enum(enumValues.length ? (enumValues as [string, ...string[]]) : ['default'])
.describe(sch.description || '')
} else if (sch.type === 'jsonArray') {
const jsonSchema = sch.jsonSchema
if (jsonSchema) {
try {
// Parse the JSON schema
const schemaObj = JSON.parse(jsonSchema)
// Create a Zod schema from the JSON schema
const itemSchema = this.createZodSchemaFromJSON(schemaObj)
// Create an array schema of the item schema
zodObj[sch.key] = z.array(itemSchema).describe(sch.description || '')
} catch (err) {
console.error(`Error parsing JSON schema for ${sch.key}:`, err)
// Fallback to generic array of records
zodObj[sch.key] = z.array(z.record(z.any())).describe(sch.description || '')
}
} else {
// If no schema provided, use generic array of records
zodObj[sch.key] = z.array(z.record(z.any())).describe(sch.description || '')
}
}
}
const structuredOutput = z.object(zodObj)
// @ts-ignore
return llmNodeInstance.withStructuredOutput(structuredOutput)
} catch (exception) {
console.error(exception)
return llmNodeInstance
}
}
/**
* Handles streaming response from the LLM
*/
@ -843,20 +823,16 @@ class LLM_Agentflow implements INode {
for await (const chunk of await llmNodeInstance.stream(messages, { signal: abortController?.signal })) {
if (sseStreamer) {
let content = ''
if (typeof chunk === 'string') {
content = chunk
} else if (Array.isArray(chunk.content) && chunk.content.length > 0) {
if (Array.isArray(chunk.content) && chunk.content.length > 0) {
const contents = chunk.content as MessageContentText[]
content = contents.map((item) => item.text).join('')
} else if (chunk.content) {
} else {
content = chunk.content.toString()
}
sseStreamer.streamTokenEvent(chatId, content)
}
const messageChunk = typeof chunk === 'string' ? new AIMessageChunk(chunk) : chunk
response = response.concat(messageChunk)
response = response.concat(chunk)
}
} catch (error) {
console.error('Error during streaming:', error)
@ -878,9 +854,7 @@ class LLM_Agentflow implements INode {
startTime: number,
endTime: number,
timeDelta: number,
isStructuredOutput: boolean,
artifacts: any[] = [],
fileAnnotations: any[] = []
isStructuredOutput: boolean
): any {
const output: any = {
content: finalResponse,
@ -899,10 +873,6 @@ class LLM_Agentflow implements INode {
output.usageMetadata = response.usage_metadata
}
if (response.response_metadata) {
output.responseMetadata = response.response_metadata
}
if (isStructuredOutput && typeof response === 'object') {
const structuredOutput = response as Record<string, any>
for (const key in structuredOutput) {
@ -912,14 +882,6 @@ class LLM_Agentflow implements INode {
}
}
if (artifacts && artifacts.length > 0) {
output.artifacts = flatten(artifacts)
}
if (fileAnnotations && fileAnnotations.length > 0) {
output.fileAnnotations = fileAnnotations
}
return output
}
@ -930,12 +892,7 @@ class LLM_Agentflow implements INode {
const sseStreamer: IServerSideEventStreamer = options.sseStreamer as IServerSideEventStreamer
if (response.tool_calls) {
const formattedToolCalls = response.tool_calls.map((toolCall: any) => ({
tool: toolCall.name || 'tool',
toolInput: toolCall.args,
toolOutput: ''
}))
sseStreamer.streamCalledToolsEvent(chatId, flatten(formattedToolCalls))
sseStreamer.streamCalledToolsEvent(chatId, response.tool_calls)
}
if (response.usage_metadata) {
@ -944,6 +901,107 @@ class LLM_Agentflow implements INode {
sseStreamer.streamEndEvent(chatId)
}
/**
* Creates a Zod schema from a JSON schema object
* @param jsonSchema The JSON schema object
* @returns A Zod schema
*/
private createZodSchemaFromJSON(jsonSchema: any): z.ZodTypeAny {
// If the schema is an object with properties, create an object schema
if (typeof jsonSchema === 'object' && jsonSchema !== null) {
const schemaObj: Record<string, z.ZodTypeAny> = {}
// Process each property in the schema
for (const [key, value] of Object.entries(jsonSchema)) {
if (value === null) {
// Handle null values
schemaObj[key] = z.null()
} else if (typeof value === 'object' && !Array.isArray(value)) {
// Check if the property has a type definition
if ('type' in value) {
const type = value.type as string
const description = ('description' in value ? (value.description as string) : '') || ''
// Create the appropriate Zod type based on the type property
if (type === 'string') {
schemaObj[key] = z.string().describe(description)
} else if (type === 'number') {
schemaObj[key] = z.number().describe(description)
} else if (type === 'boolean') {
schemaObj[key] = z.boolean().describe(description)
} else if (type === 'array') {
// If it's an array type, check if items is defined
if ('items' in value && value.items) {
const itemSchema = this.createZodSchemaFromJSON(value.items)
schemaObj[key] = z.array(itemSchema).describe(description)
} else {
// Default to array of any if items not specified
schemaObj[key] = z.array(z.any()).describe(description)
}
} else if (type === 'object') {
// If it's an object type, check if properties is defined
if ('properties' in value && value.properties) {
const nestedSchema = this.createZodSchemaFromJSON(value.properties)
schemaObj[key] = nestedSchema.describe(description)
} else {
// Default to record of any if properties not specified
schemaObj[key] = z.record(z.any()).describe(description)
}
} else {
// Default to any for unknown types
schemaObj[key] = z.any().describe(description)
}
// Check if the property is optional
if ('optional' in value && value.optional === true) {
schemaObj[key] = schemaObj[key].optional()
}
} else if (Array.isArray(value)) {
// Array values without a type property
if (value.length > 0) {
// If the array has items, recursively create a schema for the first item
const itemSchema = this.createZodSchemaFromJSON(value[0])
schemaObj[key] = z.array(itemSchema)
} else {
// Empty array, allow any array
schemaObj[key] = z.array(z.any())
}
} else {
// It's a nested object without a type property, recursively create schema
schemaObj[key] = this.createZodSchemaFromJSON(value)
}
} else if (Array.isArray(value)) {
// Array values
if (value.length > 0) {
// If the array has items, recursively create a schema for the first item
const itemSchema = this.createZodSchemaFromJSON(value[0])
schemaObj[key] = z.array(itemSchema)
} else {
// Empty array, allow any array
schemaObj[key] = z.array(z.any())
}
} else {
// For primitive values (which shouldn't be in the schema directly)
// Use the corresponding Zod type
if (typeof value === 'string') {
schemaObj[key] = z.string()
} else if (typeof value === 'number') {
schemaObj[key] = z.number()
} else if (typeof value === 'boolean') {
schemaObj[key] = z.boolean()
} else {
schemaObj[key] = z.any()
}
}
}
return z.object(schemaObj)
}
// Fallback to any for unknown types
return z.any()
}
}
module.exports = { nodeClass: LLM_Agentflow }

View File

@ -20,7 +20,7 @@ class Loop_Agentflow implements INode {
constructor() {
this.label = 'Loop'
this.name = 'loopAgentflow'
this.version = 1.2
this.version = 1.1
this.type = 'Loop'
this.category = 'Agent Flows'
this.description = 'Loop back to a previous node'
@ -64,7 +64,8 @@ class Loop_Agentflow implements INode {
label: 'Key',
name: 'key',
type: 'asyncOptions',
loadMethod: 'listRuntimeStateKeys'
loadMethod: 'listRuntimeStateKeys',
freeSolo: true
},
{
label: 'Value',

View File

@ -36,7 +36,7 @@ class Retriever_Agentflow implements INode {
constructor() {
this.label = 'Retriever'
this.name = 'retrieverAgentflow'
this.version = 1.1
this.version = 1.0
this.type = 'Retriever'
this.category = 'Agent Flows'
this.description = 'Retrieve information from vector database'
@ -87,7 +87,8 @@ class Retriever_Agentflow implements INode {
label: 'Key',
name: 'key',
type: 'asyncOptions',
loadMethod: 'listRuntimeStateKeys'
loadMethod: 'listRuntimeStateKeys',
freeSolo: true
},
{
label: 'Value',

View File

@ -29,7 +29,7 @@ class Tool_Agentflow implements INode {
constructor() {
this.label = 'Tool'
this.name = 'toolAgentflow'
this.version = 1.2
this.version = 1.1
this.type = 'Tool'
this.category = 'Agent Flows'
this.description = 'Tools allow LLM to interact with external systems'
@ -80,7 +80,8 @@ class Tool_Agentflow implements INode {
label: 'Key',
name: 'key',
type: 'asyncOptions',
loadMethod: 'listRuntimeStateKeys'
loadMethod: 'listRuntimeStateKeys',
freeSolo: true
},
{
label: 'Value',

View File

@ -1,11 +1,10 @@
import { BaseMessage, MessageContentImageUrl, AIMessageChunk } from '@langchain/core/messages'
import { BaseMessage, MessageContentImageUrl } from '@langchain/core/messages'
import { getImageUploads } from '../../src/multiModalUtils'
import { addSingleFileToStorage, getFileFromStorage } from '../../src/storageUtils'
import { ICommonObject, IFileUpload, INodeData } from '../../src/Interface'
import { getFileFromStorage } from '../../src/storageUtils'
import { ICommonObject, IFileUpload } from '../../src/Interface'
import { BaseMessageLike } from '@langchain/core/messages'
import { IFlowState } from './Interface.Agentflow'
import { getCredentialData, getCredentialParam, handleEscapeCharacters, mapMimeTypeToInputField } from '../../src/utils'
import fetch from 'node-fetch'
import { handleEscapeCharacters, mapMimeTypeToInputField } from '../../src/utils'
export const addImagesToMessages = async (
options: ICommonObject,
@ -19,8 +18,7 @@ export const addImagesToMessages = async (
for (const upload of imageUploads) {
let bf = upload.data
if (upload.type == 'stored-file') {
const fileName = upload.name.replace(/^FILE-STORAGE::/, '')
const contents = await getFileFromStorage(fileName, options.orgId, options.chatflowid, options.chatId)
const contents = await getFileFromStorage(upload.name, options.orgId, options.chatflowid, options.chatId)
// as the image is stored in the server, read the file and convert it to base64
bf = 'data:' + upload.mime + ';base64,' + contents.toString('base64')
@ -91,9 +89,8 @@ export const processMessagesWithImages = async (
if (item.type === 'stored-file' && item.name && item.mime.startsWith('image/')) {
hasImageReferences = true
try {
const fileName = item.name.replace(/^FILE-STORAGE::/, '')
// Get file contents from storage
const contents = await getFileFromStorage(fileName, options.orgId, options.chatflowid, options.chatId)
const contents = await getFileFromStorage(item.name, options.orgId, options.chatflowid, options.chatId)
// Create base64 data URL
const base64Data = 'data:' + item.mime + ';base64,' + contents.toString('base64')
@ -325,8 +322,7 @@ export const getPastChatHistoryImageMessages = async (
const imageContents: MessageContentImageUrl[] = []
for (const upload of uploads) {
if (upload.type === 'stored-file' && upload.mime.startsWith('image/')) {
const fileName = upload.name.replace(/^FILE-STORAGE::/, '')
const fileData = await getFileFromStorage(fileName, options.orgId, options.chatflowid, options.chatId)
const fileData = await getFileFromStorage(upload.name, options.orgId, options.chatflowid, options.chatId)
// as the image is stored in the server, read the file and convert it to base64
const bf = 'data:' + upload.mime + ';base64,' + fileData.toString('base64')
@ -460,437 +456,6 @@ export const getPastChatHistoryImageMessages = async (
}
}
/**
* Gets MIME type from filename extension
*/
export const getMimeTypeFromFilename = (filename: string): string => {
const extension = filename.toLowerCase().split('.').pop()
const mimeTypes: { [key: string]: string } = {
png: 'image/png',
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
gif: 'image/gif',
pdf: 'application/pdf',
txt: 'text/plain',
csv: 'text/csv',
json: 'application/json',
html: 'text/html',
xml: 'application/xml'
}
return mimeTypes[extension || ''] || 'application/octet-stream'
}
/**
* Gets artifact type from filename extension for UI rendering
*/
export const getArtifactTypeFromFilename = (filename: string): string => {
const extension = filename.toLowerCase().split('.').pop()
const artifactTypes: { [key: string]: string } = {
png: 'png',
jpg: 'jpeg',
jpeg: 'jpeg',
html: 'html',
htm: 'html',
md: 'markdown',
markdown: 'markdown',
json: 'json',
js: 'javascript',
javascript: 'javascript',
tex: 'latex',
latex: 'latex',
txt: 'text',
csv: 'text',
pdf: 'text'
}
return artifactTypes[extension || ''] || 'text'
}
/**
* Saves base64 image data to storage and returns file information
*/
export const saveBase64Image = async (
outputItem: any,
options: ICommonObject
): Promise<{ filePath: string; fileName: string; totalSize: number } | null> => {
try {
if (!outputItem.result) {
return null
}
// Extract base64 data and create buffer
const base64Data = outputItem.result
const imageBuffer = Buffer.from(base64Data, 'base64')
// Determine file extension and MIME type
const outputFormat = outputItem.output_format || 'png'
const fileName = `generated_image_${outputItem.id || Date.now()}.${outputFormat}`
const mimeType = outputFormat === 'png' ? 'image/png' : 'image/jpeg'
// Save the image using the existing storage utility
const { path, totalSize } = await addSingleFileToStorage(
mimeType,
imageBuffer,
fileName,
options.orgId,
options.chatflowid,
options.chatId
)
return { filePath: path, fileName, totalSize }
} catch (error) {
console.error('Error saving base64 image:', error)
return null
}
}
/**
* Saves Gemini inline image data to storage and returns file information
*/
export const saveGeminiInlineImage = async (
inlineItem: any,
options: ICommonObject
): Promise<{ filePath: string; fileName: string; totalSize: number } | null> => {
try {
if (!inlineItem.data || !inlineItem.mimeType) {
return null
}
// Extract base64 data and create buffer
const base64Data = inlineItem.data
const imageBuffer = Buffer.from(base64Data, 'base64')
// Determine file extension from MIME type
const mimeType = inlineItem.mimeType
let extension = 'png'
if (mimeType.includes('jpeg') || mimeType.includes('jpg')) {
extension = 'jpg'
} else if (mimeType.includes('png')) {
extension = 'png'
} else if (mimeType.includes('gif')) {
extension = 'gif'
} else if (mimeType.includes('webp')) {
extension = 'webp'
}
const fileName = `gemini_generated_image_${Date.now()}.${extension}`
// Save the image using the existing storage utility
const { path, totalSize } = await addSingleFileToStorage(
mimeType,
imageBuffer,
fileName,
options.orgId,
options.chatflowid,
options.chatId
)
return { filePath: path, fileName, totalSize }
} catch (error) {
console.error('Error saving Gemini inline image:', error)
return null
}
}
/**
* Downloads file content from container file citation
*/
export const downloadContainerFile = async (
containerId: string,
fileId: string,
filename: string,
modelNodeData: INodeData,
options: ICommonObject
): Promise<{ filePath: string; totalSize: number } | null> => {
try {
const credentialData = await getCredentialData(modelNodeData.credential ?? '', options)
const openAIApiKey = getCredentialParam('openAIApiKey', credentialData, modelNodeData)
if (!openAIApiKey) {
console.warn('No OpenAI API key available for downloading container file')
return null
}
// Download the file using OpenAI Container API
const response = await fetch(`https://api.openai.com/v1/containers/${containerId}/files/${fileId}/content`, {
method: 'GET',
headers: {
Accept: '*/*',
Authorization: `Bearer ${openAIApiKey}`
}
})
if (!response.ok) {
console.warn(
`Failed to download container file ${fileId} from container ${containerId}: ${response.status} ${response.statusText}`
)
return null
}
// Extract the binary data from the Response object
const data = await response.arrayBuffer()
const dataBuffer = Buffer.from(data)
const mimeType = getMimeTypeFromFilename(filename)
// Store the file using the same storage utility as OpenAIAssistant
const { path, totalSize } = await addSingleFileToStorage(
mimeType,
dataBuffer,
filename,
options.orgId,
options.chatflowid,
options.chatId
)
return { filePath: path, totalSize }
} catch (error) {
console.error('Error downloading container file:', error)
return null
}
}
/**
* Replace inlineData base64 with file references in the response content
*/
export const replaceInlineDataWithFileReferences = (
response: AIMessageChunk,
savedInlineImages: Array<{ filePath: string; fileName: string; mimeType: string }>
): void => {
// Check if content is an array
if (!Array.isArray(response.content)) {
return
}
// Replace base64 data with file references in response content
let savedImageIndex = 0
for (let i = 0; i < response.content.length; i++) {
const contentItem = response.content[i]
if (
typeof contentItem === 'object' &&
contentItem.type === 'inlineData' &&
contentItem.inlineData &&
savedImageIndex < savedInlineImages.length
) {
const savedImage = savedInlineImages[savedImageIndex]
// Replace with file reference
response.content[i] = {
type: 'stored-file',
name: savedImage.fileName,
mime: savedImage.mimeType,
path: savedImage.filePath
}
savedImageIndex++
}
}
// Clear the inlineData from response_metadata to avoid duplication
if (response.response_metadata?.inlineData) {
delete response.response_metadata.inlineData
}
}
/**
* Extracts artifacts from response metadata (both annotations and built-in tools)
*/
export const extractArtifactsFromResponse = async (
responseMetadata: any,
modelNodeData: INodeData,
options: ICommonObject
): Promise<{
artifacts: any[]
fileAnnotations: any[]
savedInlineImages?: Array<{ filePath: string; fileName: string; mimeType: string }>
}> => {
const artifacts: any[] = []
const fileAnnotations: any[] = []
const savedInlineImages: Array<{ filePath: string; fileName: string; mimeType: string }> = []
// Handle Gemini inline data (image generation)
if (responseMetadata?.inlineData && Array.isArray(responseMetadata.inlineData)) {
for (const inlineItem of responseMetadata.inlineData) {
if (inlineItem.type === 'gemini_inline_data' && inlineItem.data && inlineItem.mimeType) {
try {
const savedImageResult = await saveGeminiInlineImage(inlineItem, options)
if (savedImageResult) {
// Create artifact in the same format as other image artifacts
const fileType = getArtifactTypeFromFilename(savedImageResult.fileName)
artifacts.push({
type: fileType,
data: savedImageResult.filePath
})
// Track saved image for replacing base64 data in content
savedInlineImages.push({
filePath: savedImageResult.filePath,
fileName: savedImageResult.fileName,
mimeType: inlineItem.mimeType
})
}
} catch (error) {
console.error('Error processing Gemini inline image artifact:', error)
}
}
}
}
if (!responseMetadata?.output || !Array.isArray(responseMetadata.output)) {
return { artifacts, fileAnnotations, savedInlineImages: savedInlineImages.length > 0 ? savedInlineImages : undefined }
}
for (const outputItem of responseMetadata.output) {
// Handle container file citations from annotations
if (outputItem.type === 'message' && outputItem.content && Array.isArray(outputItem.content)) {
for (const contentItem of outputItem.content) {
if (contentItem.annotations && Array.isArray(contentItem.annotations)) {
for (const annotation of contentItem.annotations) {
if (annotation.type === 'container_file_citation' && annotation.file_id && annotation.filename) {
try {
// Download and store the file content
const downloadResult = await downloadContainerFile(
annotation.container_id,
annotation.file_id,
annotation.filename,
modelNodeData,
options
)
if (downloadResult) {
const fileType = getArtifactTypeFromFilename(annotation.filename)
if (fileType === 'png' || fileType === 'jpeg' || fileType === 'jpg') {
const artifact = {
type: fileType,
data: downloadResult.filePath
}
artifacts.push(artifact)
} else {
fileAnnotations.push({
filePath: downloadResult.filePath,
fileName: annotation.filename
})
}
}
} catch (error) {
console.error('Error processing annotation:', error)
}
}
}
}
}
}
// Handle built-in tool artifacts (like image generation)
if (outputItem.type === 'image_generation_call' && outputItem.result) {
try {
const savedImageResult = await saveBase64Image(outputItem, options)
if (savedImageResult) {
// Replace the base64 result with the file path in the response metadata
outputItem.result = savedImageResult.filePath
// Create artifact in the same format as other image artifacts
const fileType = getArtifactTypeFromFilename(savedImageResult.fileName)
artifacts.push({
type: fileType,
data: savedImageResult.filePath
})
}
} catch (error) {
console.error('Error processing image generation artifact:', error)
}
}
}
return { artifacts, fileAnnotations, savedInlineImages: savedInlineImages.length > 0 ? savedInlineImages : undefined }
}
/**
* Add image artifacts from previous assistant messages as user messages
* This allows the LLM to see and reference the generated images in the conversation
* Messages are marked with a special flag for later removal
*/
export const addImageArtifactsToMessages = async (messages: BaseMessageLike[], options: ICommonObject): Promise<void> => {
const imageExtensions = ['png', 'jpg', 'jpeg', 'gif', 'webp']
const messagesToInsert: Array<{ index: number; message: any }> = []
// Iterate through messages to find assistant messages with image artifacts
for (let i = 0; i < messages.length; i++) {
const message = messages[i] as any
// Check if this is an assistant message with artifacts
if (
(message.role === 'assistant' || message.role === 'ai') &&
message.additional_kwargs?.artifacts &&
Array.isArray(message.additional_kwargs.artifacts)
) {
const artifacts = message.additional_kwargs.artifacts
const imageArtifacts: Array<{ type: string; name: string; mime: string }> = []
// Extract image artifacts
for (const artifact of artifacts) {
if (artifact.type && artifact.data) {
// Check if this is an image artifact by file type
if (imageExtensions.includes(artifact.type.toLowerCase())) {
// Extract filename from the file path
const fileName = artifact.data.split('/').pop() || artifact.data
const mimeType = `image/${artifact.type.toLowerCase()}`
imageArtifacts.push({
type: 'stored-file',
name: fileName,
mime: mimeType
})
}
}
}
// If we found image artifacts, prepare to insert a user message after this assistant message
if (imageArtifacts.length > 0) {
// Check if the next message already contains these image artifacts to avoid duplicates
const nextMessage = messages[i + 1] as any
const shouldInsert =
!nextMessage ||
nextMessage.role !== 'user' ||
!Array.isArray(nextMessage.content) ||
!nextMessage.content.some(
(item: any) =>
(item.type === 'stored-file' || item.type === 'image_url') &&
imageArtifacts.some((artifact) => {
// Compare with and without FILE-STORAGE:: prefix
const artifactName = artifact.name.replace('FILE-STORAGE::', '')
const itemName = item.name?.replace('FILE-STORAGE::', '') || ''
return artifactName === itemName
})
)
if (shouldInsert) {
messagesToInsert.push({
index: i + 1,
message: {
role: 'user',
content: imageArtifacts,
_isTemporaryImageMessage: true // Mark for later removal
}
})
}
}
}
}
// Insert messages in reverse order to maintain correct indices
for (let i = messagesToInsert.length - 1; i >= 0; i--) {
const { index, message } = messagesToInsert[i]
messages.splice(index, 0, message)
}
// Convert stored-file references to base64 image_url format
if (messagesToInsert.length > 0) {
const { updatedMessages } = await processMessagesWithImages(messages, options)
// Replace the messages array content with the updated messages
messages.length = 0
messages.push(...updatedMessages)
}
}
/**
* Updates the flow state with new values
*/

View File

@ -183,7 +183,7 @@ json.dumps(my_dict)`
// TODO: get print console output
finalResult = await pyodide.runPythonAsync(code)
} catch (error) {
throw new Error(`Sorry, I'm unable to find answer for question: "${input}" using following code: "${pythonCode}"`)
throw new Error(`Sorry, I'm unable to find answer for question: "${input}" using follwoing code: "${pythonCode}"`)
}
}

View File

@ -5,7 +5,7 @@ import { RunnableSequence } from '@langchain/core/runnables'
import { BaseChatModel } from '@langchain/core/language_models/chat_models'
import { ChatPromptTemplate, MessagesPlaceholder, HumanMessagePromptTemplate, PromptTemplate } from '@langchain/core/prompts'
import { formatToOpenAIToolMessages } from 'langchain/agents/format_scratchpad/openai_tools'
import { getBaseClasses, transformBracesWithColon, convertChatHistoryToText, convertBaseMessagetoIMessage } from '../../../src/utils'
import { getBaseClasses, transformBracesWithColon } from '../../../src/utils'
import { type ToolsAgentStep } from 'langchain/agents/openai/output_parser'
import {
FlowiseMemory,
@ -23,10 +23,8 @@ import { Moderation, checkInputs, streamResponse } from '../../moderation/Modera
import { formatResponse } from '../../outputparsers/OutputParserHelpers'
import type { Document } from '@langchain/core/documents'
import { BaseRetriever } from '@langchain/core/retrievers'
import { RESPONSE_TEMPLATE, REPHRASE_TEMPLATE } from '../../chains/ConversationalRetrievalQAChain/prompts'
import { RESPONSE_TEMPLATE } from '../../chains/ConversationalRetrievalQAChain/prompts'
import { addImagesToMessages, llmSupportsVision } from '../../../src/multiModalUtils'
import { StringOutputParser } from '@langchain/core/output_parsers'
import { Tool } from '@langchain/core/tools'
class ConversationalRetrievalToolAgent_Agents implements INode {
label: string
@ -44,7 +42,7 @@ class ConversationalRetrievalToolAgent_Agents implements INode {
constructor(fields?: { sessionId?: string }) {
this.label = 'Conversational Retrieval Tool Agent'
this.name = 'conversationalRetrievalToolAgent'
this.author = 'niztal(falkor) and nikitas-novatix'
this.author = 'niztal(falkor)'
this.version = 1.0
this.type = 'AgentExecutor'
this.category = 'Agents'
@ -81,26 +79,6 @@ class ConversationalRetrievalToolAgent_Agents implements INode {
optional: true,
default: RESPONSE_TEMPLATE
},
{
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: 'Rephrase Model',
name: 'rephraseModel',
type: 'BaseChatModel',
description:
'Optional: Use a different (faster/cheaper) model for rephrasing. If not specified, uses the main Tool Calling Chat Model.',
optional: true,
additionalParams: true
},
{
label: 'Input Moderation',
description: 'Detect text that could generate harmful output and prevent it from being sent to the language model',
@ -125,9 +103,8 @@ class ConversationalRetrievalToolAgent_Agents implements INode {
this.sessionId = fields?.sessionId
}
// The agent will be prepared in run() with the correct user message - it needs the actual runtime input for rephrasing
async init(_nodeData: INodeData, _input: string, _options: ICommonObject): Promise<any> {
return null
async init(nodeData: INodeData, input: string, options: ICommonObject): Promise<any> {
return prepareAgent(nodeData, options, { sessionId: this.sessionId, chatId: options.chatId, input })
}
async run(nodeData: INodeData, input: string, options: ICommonObject): Promise<string | ICommonObject> {
@ -171,23 +148,6 @@ class ConversationalRetrievalToolAgent_Agents implements INode {
sseStreamer.streamUsedToolsEvent(chatId, res.usedTools)
usedTools = res.usedTools
}
// If the tool is set to returnDirect, stream the output to the client
if (res.usedTools && res.usedTools.length) {
let inputTools = nodeData.inputs?.tools
inputTools = flatten(inputTools)
for (const tool of res.usedTools) {
const inputTool = inputTools.find((inputTool: Tool) => inputTool.name === tool.tool)
if (inputTool && (inputTool as any).returnDirect && shouldStreamResponse) {
sseStreamer.streamTokenEvent(chatId, tool.toolOutput)
// Prevent CustomChainHandler from streaming the same output again
if (res.output === tool.toolOutput) {
res.output = ''
}
}
}
}
// The CustomChainHandler will send the stream end event
} else {
res = await executor.invoke({ input }, { callbacks: [loggerHandler, ...callbacks] })
if (res.sourceDocuments) {
@ -250,11 +210,9 @@ const prepareAgent = async (
flowObj: { sessionId?: string; chatId?: string; input?: string }
) => {
const model = nodeData.inputs?.model as BaseChatModel
const rephraseModel = (nodeData.inputs?.rephraseModel as BaseChatModel) || model // Use main model if not specified
const maxIterations = nodeData.inputs?.maxIterations as string
const memory = nodeData.inputs?.memory as FlowiseMemory
let systemMessage = nodeData.inputs?.systemMessage as string
let rephrasePrompt = nodeData.inputs?.rephrasePrompt as string
let tools = nodeData.inputs?.tools
tools = flatten(tools)
const memoryKey = memory.memoryKey ? memory.memoryKey : 'chat_history'
@ -262,9 +220,6 @@ const prepareAgent = async (
const vectorStoreRetriever = nodeData.inputs?.vectorStoreRetriever as BaseRetriever
systemMessage = transformBracesWithColon(systemMessage)
if (rephrasePrompt) {
rephrasePrompt = transformBracesWithColon(rephrasePrompt)
}
const prompt = ChatPromptTemplate.fromMessages([
['system', systemMessage ? systemMessage : `You are a helpful AI assistant.`],
@ -308,37 +263,6 @@ const prepareAgent = async (
const modelWithTools = model.bindTools(tools)
// Function to get standalone question (either rephrased or original)
const getStandaloneQuestion = async (input: string): Promise<string> => {
// If no rephrase prompt, return the original input
if (!rephrasePrompt) {
return input
}
// Get chat history (use empty string if none)
const messages = (await memory.getChatMessages(flowObj?.sessionId, true)) as BaseMessage[]
const iMessages = convertBaseMessagetoIMessage(messages)
const chatHistoryString = convertChatHistoryToText(iMessages)
// Always rephrase to normalize/expand user queries for better retrieval
try {
const CONDENSE_QUESTION_PROMPT = PromptTemplate.fromTemplate(rephrasePrompt)
const condenseQuestionChain = RunnableSequence.from([CONDENSE_QUESTION_PROMPT, rephraseModel, new StringOutputParser()])
const res = await condenseQuestionChain.invoke({
question: input,
chat_history: chatHistoryString
})
return res
} catch (error) {
console.error('Error rephrasing question:', error)
// On error, fall back to original input
return input
}
}
// Get standalone question before creating runnable
const standaloneQuestion = await getStandaloneQuestion(flowObj?.input || '')
const runnableAgent = RunnableSequence.from([
{
[inputKey]: (i: { input: string; steps: ToolsAgentStep[] }) => i.input,
@ -348,9 +272,7 @@ const prepareAgent = async (
return messages ?? []
},
context: async (i: { input: string; chatHistory?: string }) => {
// Use the standalone question (rephrased or original) for retrieval
const retrievalQuery = standaloneQuestion || i.input
const relevantDocs = await vectorStoreRetriever.invoke(retrievalQuery)
const relevantDocs = await vectorStoreRetriever.invoke(i.input)
const formattedDocs = formatDocs(relevantDocs)
return formattedDocs
}
@ -373,6 +295,4 @@ const prepareAgent = async (
return executor
}
module.exports = {
nodeClass: ConversationalRetrievalToolAgent_Agents
}
module.exports = { nodeClass: ConversationalRetrievalToolAgent_Agents }

View File

@ -578,7 +578,7 @@ class OpenAIAssistant_Agents implements INode {
toolOutput
})
} catch (e) {
await analyticHandlers.onToolError(toolIds, e)
await analyticHandlers.onToolEnd(toolIds, e)
console.error('Error executing tool', e)
throw new Error(
`Error executing tool. Tool: ${tool.name}. Thread ID: ${threadId}. Run ID: ${runThreadId}`
@ -703,7 +703,7 @@ class OpenAIAssistant_Agents implements INode {
toolOutput
})
} catch (e) {
await analyticHandlers.onToolError(toolIds, e)
await analyticHandlers.onToolEnd(toolIds, e)
console.error('Error executing tool', e)
clearInterval(timeout)
reject(
@ -1096,7 +1096,7 @@ async function handleToolSubmission(params: ToolSubmissionParams): Promise<ToolS
toolOutput
})
} catch (e) {
await analyticHandlers.onToolError(toolIds, e)
await analyticHandlers.onToolEnd(toolIds, e)
console.error('Error executing tool', e)
throw new Error(`Error executing tool. Tool: ${tool.name}. Thread ID: ${threadId}. Run ID: ${runThreadId}`)
}

View File

@ -91,7 +91,7 @@ class ChatAnthropic_ChatModels implements INode {
label: 'Extended Thinking',
name: 'extendedThinking',
type: 'boolean',
description: 'Enable extended thinking for reasoning model such as Claude Sonnet 3.7 and Claude 4',
description: 'Enable extended thinking for reasoning model such as Claude Sonnet 3.7',
optional: true,
additionalParams: true
},

View File

@ -174,18 +174,6 @@ class GoogleGenerativeAI_ChatModels implements INode {
optional: true,
additionalParams: true
},
{
label: 'Thinking Budget',
name: 'thinkingBudget',
type: 'number',
description: 'Guides the number of thinking tokens. -1 for dynamic, 0 to disable, or positive integer (Gemini 2.5 models).',
step: 1,
optional: true,
additionalParams: true,
show: {
modelName: ['gemini-2.5-pro', 'gemini-2.5-flash', 'gemini-2.5-flash-lite']
}
},
{
label: 'Base URL',
name: 'baseUrl',
@ -228,7 +216,6 @@ class GoogleGenerativeAI_ChatModels implements INode {
const cache = nodeData.inputs?.cache as BaseCache
const streaming = nodeData.inputs?.streaming as boolean
const baseUrl = nodeData.inputs?.baseUrl as string | undefined
const thinkingBudget = nodeData.inputs?.thinkingBudget as string
const allowImageUploads = nodeData.inputs?.allowImageUploads as boolean
@ -248,7 +235,6 @@ class GoogleGenerativeAI_ChatModels implements INode {
if (cache) obj.cache = cache
if (temperature) obj.temperature = parseFloat(temperature)
if (baseUrl) obj.baseUrl = baseUrl
if (thinkingBudget) obj.thinkingBudget = parseInt(thinkingBudget, 10)
let safetySettings: SafetySetting[] = []
if (_safetySettings) {

View File

@ -174,9 +174,6 @@ export interface GoogleGenerativeAIChatInput extends BaseChatModelParams, Pick<G
* - Gemini 1.0 Pro version gemini-1.0-pro-002
*/
convertSystemMessageToHumanContent?: boolean | undefined
/** Thinking budget for Gemini 2.5 thinking models. Supports -1 (dynamic), 0 (off), or positive integers. */
thinkingBudget?: number
}
/**
@ -602,17 +599,10 @@ export class LangchainChatGoogleGenerativeAI
convertSystemMessageToHumanContent: boolean | undefined
thinkingBudget?: number
private client: GenerativeModel
get _isMultimodalModel() {
return (
this.model.includes('vision') ||
this.model.startsWith('gemini-1.5') ||
this.model.startsWith('gemini-2') ||
this.model.startsWith('gemini-3')
)
return this.model.includes('vision') || this.model.startsWith('gemini-1.5') || this.model.startsWith('gemini-2')
}
constructor(fields: GoogleGenerativeAIChatInput) {
@ -667,7 +657,6 @@ export class LangchainChatGoogleGenerativeAI
this.streaming = fields.streaming ?? this.streaming
this.json = fields.json
this.thinkingBudget = fields.thinkingBudget
this.client = new GenerativeAI(this.apiKey).getGenerativeModel(
{
@ -687,22 +676,12 @@ export class LangchainChatGoogleGenerativeAI
baseUrl: fields.baseUrl
}
)
if (this.thinkingBudget !== undefined) {
;(this.client.generationConfig as any).thinkingConfig = {
...(this.thinkingBudget !== undefined ? { thinkingBudget: this.thinkingBudget } : {})
}
}
this.streamUsage = fields.streamUsage ?? this.streamUsage
}
useCachedContent(cachedContent: CachedContent, modelParams?: ModelParams, requestOptions?: RequestOptions): void {
if (!this.apiKey) return
this.client = new GenerativeAI(this.apiKey).getGenerativeModelFromCachedContent(cachedContent, modelParams, requestOptions)
if (this.thinkingBudget !== undefined) {
;(this.client.generationConfig as any).thinkingConfig = {
...(this.thinkingBudget !== undefined ? { thinkingBudget: this.thinkingBudget } : {})
}
}
}
get useSystemInstruction(): boolean {

View File

@ -48,8 +48,6 @@ export function getMessageAuthor(message: BaseMessage) {
}
/**
* !!! IMPORTANT: Must return 'user' as default instead of throwing error
* https://github.com/FlowiseAI/Flowise/issues/4743
* Maps a message type to a Google Generative AI chat author.
* @param message The message to map.
* @param model The model to use for mapping.
@ -452,7 +450,6 @@ export function mapGenerateContentResultToChatResult(
const [candidate] = response.candidates
const { content: candidateContent, ...generationInfo } = candidate
let content: MessageContent | undefined
const inlineDataItems: any[] = []
if (Array.isArray(candidateContent?.parts) && candidateContent.parts.length === 1 && candidateContent.parts[0].text) {
content = candidateContent.parts[0].text
@ -473,18 +470,6 @@ export function mapGenerateContentResultToChatResult(
type: 'codeExecutionResult',
codeExecutionResult: p.codeExecutionResult
}
} else if ('inlineData' in p && p.inlineData) {
// Extract inline image data for processing by Agent
inlineDataItems.push({
type: 'gemini_inline_data',
mimeType: p.inlineData.mimeType,
data: p.inlineData.data
})
// Return the inline data as part of the content structure
return {
type: 'inlineData',
inlineData: p.inlineData
}
}
return p
})
@ -501,12 +486,6 @@ export function mapGenerateContentResultToChatResult(
text = block?.text ?? text
}
// Build response_metadata with inline data if present
const response_metadata: any = {}
if (inlineDataItems.length > 0) {
response_metadata.inlineData = inlineDataItems
}
const generation: ChatGeneration = {
text,
message: new AIMessage({
@ -521,8 +500,7 @@ export function mapGenerateContentResultToChatResult(
additional_kwargs: {
...generationInfo
},
usage_metadata: extra?.usageMetadata,
response_metadata: Object.keys(response_metadata).length > 0 ? response_metadata : undefined
usage_metadata: extra?.usageMetadata
}),
generationInfo
}
@ -553,8 +531,6 @@ export function convertResponseContentToChatGenerationChunk(
const [candidate] = response.candidates
const { content: candidateContent, ...generationInfo } = candidate
let content: MessageContent | undefined
const inlineDataItems: any[] = []
// Checks if some parts do not have text. If false, it means that the content is a string.
if (Array.isArray(candidateContent?.parts) && candidateContent.parts.every((p) => 'text' in p)) {
content = candidateContent.parts.map((p) => p.text).join('')
@ -575,18 +551,6 @@ export function convertResponseContentToChatGenerationChunk(
type: 'codeExecutionResult',
codeExecutionResult: p.codeExecutionResult
}
} else if ('inlineData' in p && p.inlineData) {
// Extract inline image data for processing by Agent
inlineDataItems.push({
type: 'gemini_inline_data',
mimeType: p.inlineData.mimeType,
data: p.inlineData.data
})
// Return the inline data as part of the content structure
return {
type: 'inlineData',
inlineData: p.inlineData
}
}
return p
})
@ -616,12 +580,6 @@ export function convertResponseContentToChatGenerationChunk(
)
}
// Build response_metadata with inline data if present
const response_metadata: any = {}
if (inlineDataItems.length > 0) {
response_metadata.inlineData = inlineDataItems
}
return new ChatGenerationChunk({
text,
message: new AIMessageChunk({
@ -631,8 +589,7 @@ export function convertResponseContentToChatGenerationChunk(
// Each chunk can have unique "generationInfo", and merging strategy is unclear,
// so leave blank for now.
additional_kwargs: {},
usage_metadata: extra.usageMetadata,
response_metadata: Object.keys(response_metadata).length > 0 ? response_metadata : undefined
usage_metadata: extra.usageMetadata
}),
generationInfo
})

View File

@ -41,17 +41,15 @@ class ChatHuggingFace_ChatModels implements INode {
label: 'Model',
name: 'model',
type: 'string',
description:
'Model name (e.g., deepseek-ai/DeepSeek-V3.2-Exp:novita). If model includes provider (:) or using router endpoint, leave Endpoint blank.',
placeholder: 'deepseek-ai/DeepSeek-V3.2-Exp:novita'
description: 'If using own inference endpoint, leave this blank',
placeholder: 'gpt2'
},
{
label: 'Endpoint',
name: 'endpoint',
type: 'string',
placeholder: 'https://xyz.eu-west-1.aws.endpoints.huggingface.cloud/gpt2',
description:
'Custom inference endpoint (optional). Not needed for models with providers (:) or router endpoints. Leave blank to use Inference Providers.',
description: 'Using your own inference endpoint',
optional: true
},
{
@ -105,7 +103,7 @@ class ChatHuggingFace_ChatModels implements INode {
type: 'string',
rows: 4,
placeholder: 'AI assistant:',
description: 'Sets the stop sequences to use. Use comma to separate different sequences.',
description: 'Sets the stop sequences to use. Use comma to seperate different sequences.',
optional: true,
additionalParams: true
}
@ -126,15 +124,6 @@ class ChatHuggingFace_ChatModels implements INode {
const credentialData = await getCredentialData(nodeData.credential ?? '', options)
const huggingFaceApiKey = getCredentialParam('huggingFaceApiKey', credentialData, nodeData)
if (!huggingFaceApiKey) {
console.error('[ChatHuggingFace] API key validation failed: No API key found')
throw new Error('HuggingFace API key is required. Please configure it in the credential settings.')
}
if (!huggingFaceApiKey.startsWith('hf_')) {
console.warn('[ChatHuggingFace] API key format warning: Key does not start with "hf_"')
}
const obj: Partial<HFInput> = {
model,
apiKey: huggingFaceApiKey

View File

@ -56,9 +56,9 @@ export class HuggingFaceInference extends LLM implements HFInput {
this.apiKey = fields?.apiKey ?? getEnvironmentVariable('HUGGINGFACEHUB_API_KEY')
this.endpointUrl = fields?.endpointUrl
this.includeCredentials = fields?.includeCredentials
if (!this.apiKey || this.apiKey.trim() === '') {
if (!this.apiKey) {
throw new Error(
'Please set an API key for HuggingFace Hub. Either configure it in the credential settings in the UI, or set the environment variable HUGGINGFACEHUB_API_KEY.'
'Please set an API key for HuggingFace Hub in the environment variable HUGGINGFACEHUB_API_KEY or in the apiKey field of the HuggingFaceInference constructor.'
)
}
}
@ -68,21 +68,19 @@ export class HuggingFaceInference extends LLM implements HFInput {
}
invocationParams(options?: this['ParsedCallOptions']) {
// Return parameters compatible with chatCompletion API (OpenAI-compatible format)
const params: any = {
temperature: this.temperature,
max_tokens: this.maxTokens,
stop: options?.stop ?? this.stopSequences,
top_p: this.topP
return {
model: this.model,
parameters: {
// make it behave similar to openai, returning only the generated text
return_full_text: false,
temperature: this.temperature,
max_new_tokens: this.maxTokens,
stop: options?.stop ?? this.stopSequences,
top_p: this.topP,
top_k: this.topK,
repetition_penalty: this.frequencyPenalty
}
}
// Include optional parameters if they are defined
if (this.topK !== undefined) {
params.top_k = this.topK
}
if (this.frequencyPenalty !== undefined) {
params.frequency_penalty = this.frequencyPenalty
}
return params
}
async *_streamResponseChunks(
@ -90,109 +88,51 @@ export class HuggingFaceInference extends LLM implements HFInput {
options: this['ParsedCallOptions'],
runManager?: CallbackManagerForLLMRun
): AsyncGenerator<GenerationChunk> {
try {
const client = await this._prepareHFInference()
const stream = await this.caller.call(async () =>
client.chatCompletionStream({
model: this.model,
messages: [{ role: 'user', content: prompt }],
...this.invocationParams(options)
const hfi = await this._prepareHFInference()
const stream = await this.caller.call(async () =>
hfi.textGenerationStream({
...this.invocationParams(options),
inputs: prompt
})
)
for await (const chunk of stream) {
const token = chunk.token.text
yield new GenerationChunk({ text: token, generationInfo: chunk })
await runManager?.handleLLMNewToken(token ?? '')
// stream is done
if (chunk.generated_text)
yield new GenerationChunk({
text: '',
generationInfo: { finished: true }
})
)
for await (const chunk of stream) {
const token = chunk.choices[0]?.delta?.content || ''
if (token) {
yield new GenerationChunk({ text: token, generationInfo: chunk })
await runManager?.handleLLMNewToken(token)
}
// stream is done when finish_reason is set
if (chunk.choices[0]?.finish_reason) {
yield new GenerationChunk({
text: '',
generationInfo: { finished: true }
})
break
}
}
} catch (error: any) {
console.error('[ChatHuggingFace] Error in _streamResponseChunks:', error)
// Provide more helpful error messages
if (error?.message?.includes('endpointUrl') || error?.message?.includes('third-party provider')) {
throw new Error(
`Cannot use custom endpoint with model "${this.model}" that includes a provider. Please leave the Endpoint field blank in the UI. Original error: ${error.message}`
)
}
throw error
}
}
/** @ignore */
async _call(prompt: string, options: this['ParsedCallOptions']): Promise<string> {
try {
const client = await this._prepareHFInference()
// Use chatCompletion for chat models (v4 supports conversational models via Inference Providers)
const args = {
model: this.model,
messages: [{ role: 'user', content: prompt }],
...this.invocationParams(options)
}
const res = await this.caller.callWithOptions({ signal: options.signal }, client.chatCompletion.bind(client), args)
const content = res.choices[0]?.message?.content || ''
if (!content) {
console.error('[ChatHuggingFace] No content in response:', JSON.stringify(res))
throw new Error(`No content received from HuggingFace API. Response: ${JSON.stringify(res)}`)
}
return content
} catch (error: any) {
console.error('[ChatHuggingFace] Error in _call:', error.message)
// Provide more helpful error messages
if (error?.message?.includes('endpointUrl') || error?.message?.includes('third-party provider')) {
throw new Error(
`Cannot use custom endpoint with model "${this.model}" that includes a provider. Please leave the Endpoint field blank in the UI. Original error: ${error.message}`
)
}
if (error?.message?.includes('Invalid username or password') || error?.message?.includes('authentication')) {
throw new Error(
`HuggingFace API authentication failed. Please verify your API key is correct and starts with "hf_". Original error: ${error.message}`
)
}
throw error
}
const hfi = await this._prepareHFInference()
const args = { ...this.invocationParams(options), inputs: prompt }
const res = await this.caller.callWithOptions({ signal: options.signal }, hfi.textGeneration.bind(hfi), args)
return res.generated_text
}
/** @ignore */
private async _prepareHFInference() {
if (!this.apiKey || this.apiKey.trim() === '') {
console.error('[ChatHuggingFace] API key validation failed: Empty or undefined')
throw new Error('HuggingFace API key is required. Please configure it in the credential settings.')
}
const { InferenceClient } = await HuggingFaceInference.imports()
// Use InferenceClient for chat models (works better with Inference Providers)
const client = new InferenceClient(this.apiKey)
// Don't override endpoint if model uses a provider (contains ':') or if endpoint is router-based
// When using Inference Providers, endpoint should be left blank - InferenceClient handles routing automatically
if (
this.endpointUrl &&
!this.model.includes(':') &&
!this.endpointUrl.includes('/v1/chat/completions') &&
!this.endpointUrl.includes('router.huggingface.co')
) {
return client.endpoint(this.endpointUrl)
}
// Return client without endpoint override - InferenceClient will use Inference Providers automatically
return client
const { HfInference } = await HuggingFaceInference.imports()
const hfi = new HfInference(this.apiKey, {
includeCredentials: this.includeCredentials
})
return this.endpointUrl ? hfi.endpoint(this.endpointUrl) : hfi
}
/** @ignore */
static async imports(): Promise<{
InferenceClient: typeof import('@huggingface/inference').InferenceClient
HfInference: typeof import('@huggingface/inference').HfInference
}> {
try {
const { InferenceClient } = await import('@huggingface/inference')
return { InferenceClient }
const { HfInference } = await import('@huggingface/inference')
return { HfInference }
} catch (e) {
throw new Error('Please install huggingface as a dependency with, e.g. `pnpm install @huggingface/inference`')
}

View File

@ -1,8 +1,7 @@
import { ChatOpenAI as LangchainChatOpenAI, ChatOpenAIFields } from '@langchain/openai'
import { ChatOpenAI, ChatOpenAIFields } from '@langchain/openai'
import { BaseCache } from '@langchain/core/caches'
import { ICommonObject, IMultiModalOption, INode, INodeData, INodeParams } from '../../../src/Interface'
import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
import { ChatOpenRouter } from './FlowiseChatOpenRouter'
class ChatOpenRouter_ChatModels implements INode {
label: string
@ -24,7 +23,7 @@ class ChatOpenRouter_ChatModels implements INode {
this.icon = 'openRouter.svg'
this.category = 'Chat Models'
this.description = 'Wrapper around Open Router Inference API'
this.baseClasses = [this.type, ...getBaseClasses(LangchainChatOpenAI)]
this.baseClasses = [this.type, ...getBaseClasses(ChatOpenAI)]
this.credential = {
label: 'Connect Credential',
name: 'credential',
@ -115,40 +114,6 @@ class ChatOpenRouter_ChatModels implements INode {
type: 'json',
optional: true,
additionalParams: true
},
{
label: 'Allow Image Uploads',
name: 'allowImageUploads',
type: 'boolean',
description:
'Allow image input. Refer to the <a href="https://docs.flowiseai.com/using-flowise/uploads#image" target="_blank">docs</a> for more details.',
default: false,
optional: true
},
{
label: 'Image Resolution',
description: 'This parameter controls the resolution in which the model views the image.',
name: 'imageResolution',
type: 'options',
options: [
{
label: 'Low',
name: 'low'
},
{
label: 'High',
name: 'high'
},
{
label: 'Auto',
name: 'auto'
}
],
default: 'low',
optional: false,
show: {
allowImageUploads: true
}
}
]
}
@ -165,8 +130,6 @@ class ChatOpenRouter_ChatModels implements INode {
const basePath = (nodeData.inputs?.basepath as string) || 'https://openrouter.ai/api/v1'
const baseOptions = nodeData.inputs?.baseOptions
const cache = nodeData.inputs?.cache as BaseCache
const allowImageUploads = nodeData.inputs?.allowImageUploads as boolean
const imageResolution = nodeData.inputs?.imageResolution as string
const credentialData = await getCredentialData(nodeData.credential ?? '', options)
const openRouterApiKey = getCredentialParam('openRouterApiKey', credentialData, nodeData)
@ -192,7 +155,7 @@ class ChatOpenRouter_ChatModels implements INode {
try {
parsedBaseOptions = typeof baseOptions === 'object' ? baseOptions : JSON.parse(baseOptions)
} catch (exception) {
throw new Error("Invalid JSON in the ChatOpenRouter's BaseOptions: " + exception)
throw new Error("Invalid JSON in the ChatCerebras's BaseOptions: " + exception)
}
}
@ -203,15 +166,7 @@ class ChatOpenRouter_ChatModels implements INode {
}
}
const multiModalOption: IMultiModalOption = {
image: {
allowImageUploads: allowImageUploads ?? false,
imageResolution
}
}
const model = new ChatOpenRouter(nodeData.id, obj)
model.setMultiModalOption(multiModalOption)
const model = new ChatOpenAI(obj)
return model
}
}

View File

@ -1,29 +0,0 @@
import { ChatOpenAI as LangchainChatOpenAI, ChatOpenAIFields } from '@langchain/openai'
import { IMultiModalOption, IVisionChatModal } from '../../../src'
export class ChatOpenRouter extends LangchainChatOpenAI implements IVisionChatModal {
configuredModel: string
configuredMaxToken?: number
multiModalOption: IMultiModalOption
id: string
constructor(id: string, fields?: ChatOpenAIFields) {
super(fields)
this.id = id
this.configuredModel = fields?.modelName ?? ''
this.configuredMaxToken = fields?.maxTokens
}
revertToOriginalModel(): void {
this.model = this.configuredModel
this.maxTokens = this.configuredMaxToken
}
setMultiModalOption(multiModalOption: IMultiModalOption): void {
this.multiModalOption = multiModalOption
}
setVisionModel(): void {
// pass - OpenRouter models don't need model switching
}
}

View File

@ -95,7 +95,7 @@ class API_DocumentLoaders implements INode {
type: 'string',
rows: 4,
description:
'Each document loader comes with a default set of metadata keys that are extracted from the document. You can use this field to omit some of the default metadata keys. The value should be a list of keys, separated by comma. Use * to omit all metadata keys except the ones you specify in the Additional Metadata field',
'Each document loader comes with a default set of metadata keys that are extracted from the document. You can use this field to omit some of the default metadata keys. The value should be a list of keys, seperated by comma. Use * to omit all metadata keys execept the ones you specify in the Additional Metadata field',
placeholder: 'key1, key2, key3.nestedKey1',
optional: true,
additionalParams: true

View File

@ -2,7 +2,7 @@ import { TextLoader } from 'langchain/document_loaders/fs/text'
import Papa from 'papaparse'
type CSVLoaderOptions = {
// Return specific column from key (string) or index (integer)
// Return specifific column from key (string) or index (integer)
column?: string | number
// Force separator (default: auto detect)
separator?: string

View File

@ -47,7 +47,7 @@ class Json_DocumentLoaders implements INode {
constructor() {
this.label = 'Json File'
this.name = 'jsonFile'
this.version = 3.1
this.version = 3.0
this.type = 'Document'
this.icon = 'json.svg'
this.category = 'Document Loaders'
@ -66,14 +66,6 @@ class Json_DocumentLoaders implements INode {
type: 'TextSplitter',
optional: true
},
{
label: 'Separate by JSON Object (JSON Array)',
name: 'separateByObject',
type: 'boolean',
description: 'If enabled and the file is a JSON Array, each JSON object will be extracted as a chunk',
optional: true,
additionalParams: true
},
{
label: 'Pointers Extraction (separated by commas)',
name: 'pointersName',
@ -81,10 +73,7 @@ class Json_DocumentLoaders implements INode {
description:
'Ex: { "key": "value" }, Pointer Extraction = "key", "value" will be extracted as pageContent of the chunk. Use comma to separate multiple pointers',
placeholder: 'key1, key2',
optional: true,
hide: {
separateByObject: true
}
optional: true
},
{
label: 'Additional Metadata',
@ -133,7 +122,6 @@ class Json_DocumentLoaders implements INode {
const pointersName = nodeData.inputs?.pointersName as string
const metadata = nodeData.inputs?.metadata
const _omitMetadataKeys = nodeData.inputs?.omitMetadataKeys as string
const separateByObject = nodeData.inputs?.separateByObject as boolean
const output = nodeData.outputs?.output as string
let omitMetadataKeys: string[] = []
@ -165,7 +153,7 @@ class Json_DocumentLoaders implements INode {
if (!file) continue
const fileData = await getFileFromStorage(file, orgId, chatflowid)
const blob = new Blob([fileData])
const loader = new JSONLoader(blob, pointers.length != 0 ? pointers : undefined, metadata, separateByObject)
const loader = new JSONLoader(blob, pointers.length != 0 ? pointers : undefined, metadata)
if (textSplitter) {
let splittedDocs = await loader.load()
@ -188,7 +176,7 @@ class Json_DocumentLoaders implements INode {
splitDataURI.pop()
const bf = Buffer.from(splitDataURI.pop() || '', 'base64')
const blob = new Blob([bf])
const loader = new JSONLoader(blob, pointers.length != 0 ? pointers : undefined, metadata, separateByObject)
const loader = new JSONLoader(blob, pointers.length != 0 ? pointers : undefined, metadata)
if (textSplitter) {
let splittedDocs = await loader.load()
@ -318,20 +306,13 @@ class TextLoader extends BaseDocumentLoader {
class JSONLoader extends TextLoader {
public pointers: string[]
private metadataMapping: Record<string, string>
private separateByObject: boolean
constructor(
filePathOrBlob: string | Blob,
pointers: string | string[] = [],
metadataMapping: Record<string, string> = {},
separateByObject: boolean = false
) {
constructor(filePathOrBlob: string | Blob, pointers: string | string[] = [], metadataMapping: Record<string, string> = {}) {
super(filePathOrBlob)
this.pointers = Array.isArray(pointers) ? pointers : [pointers]
if (metadataMapping) {
this.metadataMapping = typeof metadataMapping === 'object' ? metadataMapping : JSON.parse(metadataMapping)
}
this.separateByObject = separateByObject
}
protected async parse(raw: string): Promise<Document[]> {
@ -342,24 +323,14 @@ class JSONLoader extends TextLoader {
const jsonArray = Array.isArray(json) ? json : [json]
for (const item of jsonArray) {
if (this.separateByObject) {
if (typeof item === 'object' && item !== null && !Array.isArray(item)) {
const metadata = this.extractMetadata(item)
const pageContent = this.formatObjectAsKeyValue(item)
documents.push({
pageContent,
metadata
})
}
} else {
const content = this.extractContent(item)
const metadata = this.extractMetadata(item)
for (const pageContent of content) {
documents.push({
pageContent,
metadata
})
}
const content = this.extractContent(item)
const metadata = this.extractMetadata(item)
for (const pageContent of content) {
documents.push({
pageContent,
metadata
})
}
}
@ -399,30 +370,6 @@ class JSONLoader extends TextLoader {
return metadata
}
/**
* Formats a JSON object as readable key-value pairs
*/
private formatObjectAsKeyValue(obj: any, prefix: string = ''): string {
const lines: string[] = []
for (const [key, value] of Object.entries(obj)) {
const fullKey = prefix ? `${prefix}.${key}` : key
if (value === null || value === undefined) {
lines.push(`${fullKey}: ${value}`)
} else if (Array.isArray(value)) {
lines.push(`${fullKey}: ${JSON.stringify(value)}`)
} else if (typeof value === 'object') {
// Recursively format nested objects
lines.push(this.formatObjectAsKeyValue(value, fullKey))
} else {
lines.push(`${fullKey}: ${value}`)
}
}
return lines.join('\n')
}
/**
* If JSON pointers are specified, return all strings below any of them
* and exclude all other nodes expect if they match a JSON pointer.

View File

@ -100,7 +100,7 @@ export class OxylabsLoader extends BaseDocumentLoader {
const params = {
source: this.params.source,
geo_location: this.params.geo_location,
render: this.params.render ? 'html' : null,
render: this.params.render,
parse: this.params.parse,
user_agent_type: this.params.user_agent_type,
markdown: !this.params.parse,
@ -110,14 +110,11 @@ export class OxylabsLoader extends BaseDocumentLoader {
const response = await this.sendAPIRequest<OxylabsResponse>(params)
const docs: OxylabsDocument[] = response.data.results.map((result, index) => {
const content = typeof result.content === 'string' ? result.content : JSON.stringify(result.content)
return {
id: `${response.data.job.id.toString()}-${index}`,
pageContent: content,
metadata: {}
}
})
const docs: OxylabsDocument[] = response.data.results.map((result, index) => ({
id: `${response.data.job.id.toString()}-${index}`,
pageContent: result.content,
metadata: {}
}))
return docs
}

View File

@ -190,14 +190,11 @@ class Playwright_DocumentLoaders implements INode {
async function playwrightLoader(url: string): Promise<Document[] | undefined> {
try {
let docs = []
const executablePath = process.env.PLAYWRIGHT_EXECUTABLE_PATH
const config: PlaywrightWebBaseLoaderOptions = {
launchOptions: {
args: ['--no-sandbox'],
headless: true,
executablePath: executablePath
executablePath: process.env.PLAYWRIGHT_EXECUTABLE_FILE_PATH
}
}
if (waitUntilGoToOption) {

View File

@ -181,14 +181,11 @@ class Puppeteer_DocumentLoaders implements INode {
async function puppeteerLoader(url: string): Promise<Document[] | undefined> {
try {
let docs: Document[] = []
const executablePath = process.env.PUPPETEER_EXECUTABLE_PATH
const config: PuppeteerWebBaseLoaderOptions = {
launchOptions: {
args: ['--no-sandbox'],
headless: 'new',
executablePath: executablePath
executablePath: process.env.PUPPETEER_EXECUTABLE_FILE_PATH
}
}
if (waitUntilGoToOption) {

View File

@ -27,6 +27,8 @@ type Element = {
}
export class UnstructuredLoader extends BaseDocumentLoader {
public filePath: string
private apiUrl = process.env.UNSTRUCTURED_API_URL || 'https://api.unstructuredapp.io/general/v0/general'
private apiKey: string | undefined = process.env.UNSTRUCTURED_API_KEY
@ -136,7 +138,7 @@ export class UnstructuredLoader extends BaseDocumentLoader {
})
if (!response.ok) {
throw new Error(`Failed to partition file with error ${response.status} and message ${await response.text()}`)
throw new Error(`Failed to partition file ${this.filePath} with error ${response.status} and message ${await response.text()}`)
}
const elements = await response.json()

View File

@ -4,11 +4,15 @@ import {
UnstructuredLoaderOptions,
UnstructuredLoaderStrategy,
SkipInferTableTypes,
HiResModelName
HiResModelName,
UnstructuredLoader as LCUnstructuredLoader
} from '@langchain/community/document_loaders/fs/unstructured'
import { getCredentialData, getCredentialParam, handleEscapeCharacters } from '../../../src/utils'
import { getFileFromStorage, INodeOutputsValue } from '../../../src'
import { UnstructuredLoader } from './Unstructured'
import { isPathTraversal } from '../../../src/validator'
import sanitize from 'sanitize-filename'
import path from 'path'
class UnstructuredFile_DocumentLoaders implements INode {
label: string
@ -40,6 +44,17 @@ class UnstructuredFile_DocumentLoaders implements INode {
optional: true
}
this.inputs = [
/** Deprecated
{
label: 'File Path',
name: 'filePath',
type: 'string',
placeholder: '',
optional: true,
warning:
'Use the File Upload instead of File path. If file is uploaded, this path is ignored. Path will be deprecated in future releases.'
},
*/
{
label: 'Files Upload',
name: 'fileObject',
@ -440,6 +455,7 @@ class UnstructuredFile_DocumentLoaders implements INode {
}
async init(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
const filePath = nodeData.inputs?.filePath as string
const unstructuredAPIUrl = nodeData.inputs?.unstructuredAPIUrl as string
const strategy = nodeData.inputs?.strategy as UnstructuredLoaderStrategy
const encoding = nodeData.inputs?.encoding as string
@ -544,8 +560,37 @@ class UnstructuredFile_DocumentLoaders implements INode {
docs.push(...loaderDocs)
}
}
} else if (filePath) {
if (!filePath || typeof filePath !== 'string') {
throw new Error('Invalid file path format')
}
if (isPathTraversal(filePath)) {
throw new Error('Invalid path characters detected in filePath - path traversal not allowed')
}
const parsedPath = path.parse(filePath)
const sanitizedFilename = sanitize(parsedPath.base)
if (!sanitizedFilename || sanitizedFilename.trim() === '') {
throw new Error('Invalid filename after sanitization')
}
const sanitizedFilePath = path.join(parsedPath.dir, sanitizedFilename)
if (!path.isAbsolute(sanitizedFilePath)) {
throw new Error('File path must be absolute')
}
if (sanitizedFilePath.includes('..')) {
throw new Error('Invalid file path - directory traversal not allowed')
}
const loader = new LCUnstructuredLoader(sanitizedFilePath, obj)
const loaderDocs = await loader.load()
docs.push(...loaderDocs)
} else {
throw new Error('File upload is required')
throw new Error('File path or File upload is required')
}
if (metadata) {

View File

@ -1,6 +1,3 @@
/*
* Uncomment this if you want to use the UnstructuredFolder to load a folder from the file system
import { omit } from 'lodash'
import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface'
import {
@ -519,4 +516,3 @@ class UnstructuredFolder_DocumentLoaders implements INode {
}
module.exports = { nodeClass: UnstructuredFolder_DocumentLoaders }
*/

View File

@ -96,7 +96,7 @@ class AWSBedrockEmbedding_Embeddings implements INode {
{
label: 'Max AWS API retries',
name: 'maxRetries',
description: 'This will limit the number of AWS API for Titan model embeddings call retries. Used to avoid throttling.',
description: 'This will limit the nubmer of AWS API for Titan model embeddings call retries. Used to avoid throttling.',
type: 'number',
optional: true,
default: 5,

View File

@ -23,22 +23,24 @@ export class HuggingFaceInferenceEmbeddings extends Embeddings implements Huggin
this.model = fields?.model ?? 'sentence-transformers/distilbert-base-nli-mean-tokens'
this.apiKey = fields?.apiKey ?? getEnvironmentVariable('HUGGINGFACEHUB_API_KEY')
this.endpoint = fields?.endpoint ?? ''
const hf = new HfInference(this.apiKey)
// v4 uses Inference Providers by default; only override if custom endpoint provided
this.client = this.endpoint ? hf.endpoint(this.endpoint) : hf
this.client = new HfInference(this.apiKey)
if (this.endpoint) this.client.endpoint(this.endpoint)
}
async _embed(texts: string[]): Promise<number[][]> {
// replace newlines, which can negatively affect performance.
const clean = texts.map((text) => text.replace(/\n/g, ' '))
const hf = new HfInference(this.apiKey)
const obj: any = {
inputs: clean
}
if (!this.endpoint) {
if (this.endpoint) {
hf.endpoint(this.endpoint)
} else {
obj.model = this.model
}
const res = await this.caller.callWithOptions({}, this.client.featureExtraction.bind(this.client), obj)
const res = await this.caller.callWithOptions({}, hf.featureExtraction.bind(hf), obj)
return res as number[][]
}

View File

@ -39,7 +39,7 @@ class SubQuestionQueryEngine_LlamaIndex implements INode {
this.icon = 'subQueryEngine.svg'
this.category = 'Engine'
this.description =
'Breaks complex query into sub questions for each relevant data source, then gather all the intermediate responses and synthesizes a final response'
'Breaks complex query into sub questions for each relevant data source, then gather all the intermediate reponses and synthesizes a final response'
this.baseClasses = [this.type, 'BaseQueryEngine']
this.tags = ['LlamaIndex']
this.inputs = [

View File

@ -78,8 +78,6 @@ export class HuggingFaceInference extends LLM implements HFInput {
async _call(prompt: string, options: this['ParsedCallOptions']): Promise<string> {
const { HfInference } = await HuggingFaceInference.imports()
const hf = new HfInference(this.apiKey)
// v4 uses Inference Providers by default; only override if custom endpoint provided
const hfClient = this.endpoint ? hf.endpoint(this.endpoint) : hf
const obj: any = {
parameters: {
// make it behave similar to openai, returning only the generated text
@ -92,10 +90,12 @@ export class HuggingFaceInference extends LLM implements HFInput {
},
inputs: prompt
}
if (!this.endpoint) {
if (this.endpoint) {
hf.endpoint(this.endpoint)
} else {
obj.model = this.model
}
const res = await this.caller.callWithOptions({ signal: options.signal }, hfClient.textGeneration.bind(hfClient), obj)
const res = await this.caller.callWithOptions({ signal: options.signal }, hf.textGeneration.bind(hf), obj)
return res.generated_text
}

View File

@ -21,7 +21,6 @@ import { ChatOpenAI } from '../../chatmodels/ChatOpenAI/FlowiseChatOpenAI'
import { ChatAnthropic } from '../../chatmodels/ChatAnthropic/FlowiseChatAnthropic'
import { addImagesToMessages, llmSupportsVision } from '../../../src/multiModalUtils'
import { ChatGoogleGenerativeAI } from '../../chatmodels/ChatGoogleGenerativeAI/FlowiseChatGoogleGenerativeAI'
import { AzureChatOpenAI } from '../../chatmodels/AzureChatOpenAI/FlowiseAzureChatOpenAI'
const sysPrompt = `You are a supervisor tasked with managing a conversation between the following workers: {team_members}.
Given the following user request, respond with the worker to act next.
@ -243,7 +242,7 @@ class Supervisor_MultiAgents implements INode {
}
}
})
} else if (llm instanceof ChatOpenAI || llm instanceof AzureChatOpenAI) {
} else if (llm instanceof ChatOpenAI) {
let prompt = ChatPromptTemplate.fromMessages([
['system', systemPrompt],
new MessagesPlaceholder('messages'),

View File

@ -11,7 +11,7 @@ return [
tool_calls: [
{
id: "12345",
name: "calculator",
name: "calulator",
args: {
number1: 333382,
number2: 1932,

View File

@ -62,6 +62,7 @@ class MySQLRecordManager_RecordManager implements INode {
label: 'Namespace',
name: 'namespace',
type: 'string',
description: 'If not specified, chatflowid will be used',
additionalParams: true,
optional: true
},
@ -204,8 +205,8 @@ class MySQLRecordManager implements RecordManagerInterface {
}
async createSchema(): Promise<void> {
const dataSource = await this.getDataSource()
try {
const dataSource = await this.getDataSource()
const queryRunner = dataSource.createQueryRunner()
const tableName = this.sanitizeTableName(this.tableName)
@ -218,16 +219,7 @@ class MySQLRecordManager implements RecordManagerInterface {
unique key \`unique_key_namespace\` (\`key\`,
\`namespace\`));`)
// Add doc_id column if it doesn't exist (migration for existing tables)
const checkColumn = await queryRunner.manager.query(
`SELECT COUNT(1) ColumnExists FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_schema=DATABASE() AND table_name='${tableName}' AND column_name='doc_id';`
)
if (checkColumn[0].ColumnExists === 0) {
await queryRunner.manager.query(`ALTER TABLE \`${tableName}\` ADD COLUMN \`doc_id\` longtext;`)
}
const columns = [`updated_at`, `key`, `namespace`, `group_id`, `doc_id`]
const columns = [`updated_at`, `key`, `namespace`, `group_id`]
for (const column of columns) {
// MySQL does not support 'IF NOT EXISTS' function for Index
const Check = await queryRunner.manager.query(
@ -249,8 +241,6 @@ class MySQLRecordManager implements RecordManagerInterface {
return
}
throw e
} finally {
await dataSource.destroy()
}
}
@ -269,7 +259,7 @@ class MySQLRecordManager implements RecordManagerInterface {
}
}
async update(keys: Array<{ uid: string; docId: string }> | string[], updateOptions?: UpdateOptions): Promise<void> {
async update(keys: string[], updateOptions?: UpdateOptions): Promise<void> {
if (keys.length === 0) {
return
}
@ -285,23 +275,23 @@ class MySQLRecordManager implements RecordManagerInterface {
throw new Error(`Time sync issue with database ${updatedAt} < ${timeAtLeast}`)
}
// Handle both new format (objects with uid and docId) and old format (strings)
const isNewFormat = keys.length > 0 && typeof keys[0] === 'object' && 'uid' in keys[0]
const keyStrings = isNewFormat ? (keys as Array<{ uid: string; docId: string }>).map((k) => k.uid) : (keys as string[])
const docIds = isNewFormat ? (keys as Array<{ uid: string; docId: string }>).map((k) => k.docId) : keys.map(() => null)
const groupIds = _groupIds ?? keys.map(() => null)
const groupIds = _groupIds ?? keyStrings.map(() => null)
if (groupIds.length !== keyStrings.length) {
throw new Error(`Number of keys (${keyStrings.length}) does not match number of group_ids (${groupIds.length})`)
if (groupIds.length !== keys.length) {
throw new Error(`Number of keys (${keys.length}) does not match number of group_ids (${groupIds.length})`)
}
const recordsToUpsert = keyStrings.map((key, i) => [key, this.namespace, updatedAt, groupIds[i] ?? null, docIds[i] ?? null])
const recordsToUpsert = keys.map((key, i) => [
key,
this.namespace,
updatedAt,
groupIds[i] ?? null // Ensure groupIds[i] is null if undefined
])
const query = `
INSERT INTO \`${tableName}\` (\`key\`, \`namespace\`, \`updated_at\`, \`group_id\`, \`doc_id\`)
VALUES (?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE \`updated_at\` = VALUES(\`updated_at\`), \`doc_id\` = VALUES(\`doc_id\`)`
INSERT INTO \`${tableName}\` (\`key\`, \`namespace\`, \`updated_at\`, \`group_id\`)
VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE \`updated_at\` = VALUES(\`updated_at\`)`
// To handle multiple files upsert
try {
@ -357,13 +347,13 @@ class MySQLRecordManager implements RecordManagerInterface {
}
}
async listKeys(options?: ListKeyOptions & { docId?: string }): Promise<string[]> {
async listKeys(options?: ListKeyOptions): Promise<string[]> {
const dataSource = await this.getDataSource()
const queryRunner = dataSource.createQueryRunner()
const tableName = this.sanitizeTableName(this.tableName)
try {
const { before, after, limit, groupIds, docId } = options ?? {}
const { before, after, limit, groupIds } = options ?? {}
let query = `SELECT \`key\` FROM \`${tableName}\` WHERE \`namespace\` = ?`
const values: (string | number | string[])[] = [this.namespace]
@ -390,11 +380,6 @@ class MySQLRecordManager implements RecordManagerInterface {
values.push(...groupIds.filter((gid): gid is string => gid !== null))
}
if (docId) {
query += ` AND \`doc_id\` = ?`
values.push(docId)
}
query += ';'
// Directly using try/catch with async/await for cleaner flow

View File

@ -78,6 +78,7 @@ class PostgresRecordManager_RecordManager implements INode {
label: 'Namespace',
name: 'namespace',
type: 'string',
description: 'If not specified, chatflowid will be used',
additionalParams: true,
optional: true
},
@ -221,8 +222,8 @@ class PostgresRecordManager implements RecordManagerInterface {
}
async createSchema(): Promise<void> {
const dataSource = await this.getDataSource()
try {
const dataSource = await this.getDataSource()
const queryRunner = dataSource.createQueryRunner()
const tableName = this.sanitizeTableName(this.tableName)
@ -240,19 +241,6 @@ class PostgresRecordManager implements RecordManagerInterface {
CREATE INDEX IF NOT EXISTS namespace_index ON "${tableName}" (namespace);
CREATE INDEX IF NOT EXISTS group_id_index ON "${tableName}" (group_id);`)
// Add doc_id column if it doesn't exist (migration for existing tables)
await queryRunner.manager.query(`
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = '${tableName}' AND column_name = 'doc_id'
) THEN
ALTER TABLE "${tableName}" ADD COLUMN doc_id TEXT;
CREATE INDEX IF NOT EXISTS doc_id_index ON "${tableName}" (doc_id);
END IF;
END $$;`)
await queryRunner.release()
} catch (e: any) {
// This error indicates that the table already exists
@ -263,8 +251,6 @@ class PostgresRecordManager implements RecordManagerInterface {
return
}
throw e
} finally {
await dataSource.destroy()
}
}
@ -298,7 +284,7 @@ class PostgresRecordManager implements RecordManagerInterface {
return `(${placeholders.join(', ')})`
}
async update(keys: Array<{ uid: string; docId: string }> | string[], updateOptions?: UpdateOptions): Promise<void> {
async update(keys: string[], updateOptions?: UpdateOptions): Promise<void> {
if (keys.length === 0) {
return
}
@ -314,22 +300,17 @@ class PostgresRecordManager implements RecordManagerInterface {
throw new Error(`Time sync issue with database ${updatedAt} < ${timeAtLeast}`)
}
// Handle both new format (objects with uid and docId) and old format (strings)
const isNewFormat = keys.length > 0 && typeof keys[0] === 'object' && 'uid' in keys[0]
const keyStrings = isNewFormat ? (keys as Array<{ uid: string; docId: string }>).map((k) => k.uid) : (keys as string[])
const docIds = isNewFormat ? (keys as Array<{ uid: string; docId: string }>).map((k) => k.docId) : keys.map(() => null)
const groupIds = _groupIds ?? keys.map(() => null)
const groupIds = _groupIds ?? keyStrings.map(() => null)
if (groupIds.length !== keyStrings.length) {
throw new Error(`Number of keys (${keyStrings.length}) does not match number of group_ids ${groupIds.length})`)
if (groupIds.length !== keys.length) {
throw new Error(`Number of keys (${keys.length}) does not match number of group_ids ${groupIds.length})`)
}
const recordsToUpsert = keyStrings.map((key, i) => [key, this.namespace, updatedAt, groupIds[i], docIds[i]])
const recordsToUpsert = keys.map((key, i) => [key, this.namespace, updatedAt, groupIds[i]])
const valuesPlaceholders = recordsToUpsert.map((_, j) => this.generatePlaceholderForRowAt(j, recordsToUpsert[0].length)).join(', ')
const query = `INSERT INTO "${tableName}" (key, namespace, updated_at, group_id, doc_id) VALUES ${valuesPlaceholders} ON CONFLICT (key, namespace) DO UPDATE SET updated_at = EXCLUDED.updated_at, doc_id = EXCLUDED.doc_id;`
const query = `INSERT INTO "${tableName}" (key, namespace, updated_at, group_id) VALUES ${valuesPlaceholders} ON CONFLICT (key, namespace) DO UPDATE SET updated_at = EXCLUDED.updated_at;`
try {
await queryRunner.manager.query(query, recordsToUpsert.flat())
await queryRunner.release()
@ -368,8 +349,8 @@ class PostgresRecordManager implements RecordManagerInterface {
}
}
async listKeys(options?: ListKeyOptions & { docId?: string }): Promise<string[]> {
const { before, after, limit, groupIds, docId } = options ?? {}
async listKeys(options?: ListKeyOptions): Promise<string[]> {
const { before, after, limit, groupIds } = options ?? {}
const tableName = this.sanitizeTableName(this.tableName)
let query = `SELECT key FROM "${tableName}" WHERE namespace = $1`
@ -400,12 +381,6 @@ class PostgresRecordManager implements RecordManagerInterface {
index += 1
}
if (docId) {
values.push(docId)
query += ` AND doc_id = $${index}`
index += 1
}
query += ';'
const dataSource = await this.getDataSource()

View File

@ -51,6 +51,7 @@ class SQLiteRecordManager_RecordManager implements INode {
label: 'Namespace',
name: 'namespace',
type: 'string',
description: 'If not specified, chatflowid will be used',
additionalParams: true,
optional: true
},
@ -178,8 +179,8 @@ class SQLiteRecordManager implements RecordManagerInterface {
}
async createSchema(): Promise<void> {
const dataSource = await this.getDataSource()
try {
const dataSource = await this.getDataSource()
const queryRunner = dataSource.createQueryRunner()
const tableName = this.sanitizeTableName(this.tableName)
@ -197,15 +198,6 @@ CREATE INDEX IF NOT EXISTS key_index ON "${tableName}" (key);
CREATE INDEX IF NOT EXISTS namespace_index ON "${tableName}" (namespace);
CREATE INDEX IF NOT EXISTS group_id_index ON "${tableName}" (group_id);`)
// Add doc_id column if it doesn't exist (migration for existing tables)
const checkColumn = await queryRunner.manager.query(
`SELECT COUNT(*) as count FROM pragma_table_info('${tableName}') WHERE name='doc_id';`
)
if (checkColumn[0].count === 0) {
await queryRunner.manager.query(`ALTER TABLE "${tableName}" ADD COLUMN doc_id TEXT;`)
await queryRunner.manager.query(`CREATE INDEX IF NOT EXISTS doc_id_index ON "${tableName}" (doc_id);`)
}
await queryRunner.release()
} catch (e: any) {
// This error indicates that the table already exists
@ -216,8 +208,6 @@ CREATE INDEX IF NOT EXISTS group_id_index ON "${tableName}" (group_id);`)
return
}
throw e
} finally {
await dataSource.destroy()
}
}
@ -236,7 +226,7 @@ CREATE INDEX IF NOT EXISTS group_id_index ON "${tableName}" (group_id);`)
}
}
async update(keys: Array<{ uid: string; docId: string }> | string[], updateOptions?: UpdateOptions): Promise<void> {
async update(keys: string[], updateOptions?: UpdateOptions): Promise<void> {
if (keys.length === 0) {
return
}
@ -251,23 +241,23 @@ CREATE INDEX IF NOT EXISTS group_id_index ON "${tableName}" (group_id);`)
throw new Error(`Time sync issue with database ${updatedAt} < ${timeAtLeast}`)
}
// Handle both new format (objects with uid and docId) and old format (strings)
const isNewFormat = keys.length > 0 && typeof keys[0] === 'object' && 'uid' in keys[0]
const keyStrings = isNewFormat ? (keys as Array<{ uid: string; docId: string }>).map((k) => k.uid) : (keys as string[])
const docIds = isNewFormat ? (keys as Array<{ uid: string; docId: string }>).map((k) => k.docId) : keys.map(() => null)
const groupIds = _groupIds ?? keys.map(() => null)
const groupIds = _groupIds ?? keyStrings.map(() => null)
if (groupIds.length !== keyStrings.length) {
throw new Error(`Number of keys (${keyStrings.length}) does not match number of group_ids (${groupIds.length})`)
if (groupIds.length !== keys.length) {
throw new Error(`Number of keys (${keys.length}) does not match number of group_ids (${groupIds.length})`)
}
const recordsToUpsert = keyStrings.map((key, i) => [key, this.namespace, updatedAt, groupIds[i] ?? null, docIds[i] ?? null])
const recordsToUpsert = keys.map((key, i) => [
key,
this.namespace,
updatedAt,
groupIds[i] ?? null // Ensure groupIds[i] is null if undefined
])
const query = `
INSERT INTO "${tableName}" (key, namespace, updated_at, group_id, doc_id)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT (key, namespace) DO UPDATE SET updated_at = excluded.updated_at, doc_id = excluded.doc_id`
INSERT INTO "${tableName}" (key, namespace, updated_at, group_id)
VALUES (?, ?, ?, ?)
ON CONFLICT (key, namespace) DO UPDATE SET updated_at = excluded.updated_at`
try {
// To handle multiple files upsert
@ -322,8 +312,8 @@ CREATE INDEX IF NOT EXISTS group_id_index ON "${tableName}" (group_id);`)
}
}
async listKeys(options?: ListKeyOptions & { docId?: string }): Promise<string[]> {
const { before, after, limit, groupIds, docId } = options ?? {}
async listKeys(options?: ListKeyOptions): Promise<string[]> {
const { before, after, limit, groupIds } = options ?? {}
const tableName = this.sanitizeTableName(this.tableName)
let query = `SELECT key FROM "${tableName}" WHERE namespace = ?`
@ -352,11 +342,6 @@ CREATE INDEX IF NOT EXISTS group_id_index ON "${tableName}" (group_id);`)
values.push(...groupIds.filter((gid): gid is string => gid !== null))
}
if (docId) {
query += ` AND doc_id = ?`
values.push(docId)
}
query += ';'
const dataSource = await this.getDataSource()

View File

@ -238,7 +238,7 @@ export function filterConversationHistory(
export const restructureMessages = (llm: BaseChatModel, state: ISeqAgentsState) => {
const messages: BaseMessage[] = []
for (const message of state.messages as unknown as BaseMessage[]) {
// Sometimes Anthropic can return a message with content types of array, ignore that EXCEPT when tool calls are present
// Sometimes Anthropic can return a message with content types of array, ignore that EXECEPT when tool calls are present
if ((message as any).tool_calls?.length && message.content !== '') {
message.content = JSON.stringify(message.content)
}

View File

@ -4,13 +4,7 @@ import { RunnableConfig } from '@langchain/core/runnables'
import { CallbackManagerForToolRun, Callbacks, CallbackManager, parseCallbackConfigArg } from '@langchain/core/callbacks/manager'
import { StructuredTool } from '@langchain/core/tools'
import { ICommonObject, IDatabaseEntity, INode, INodeData, INodeOptionsValue, INodeParams } from '../../../src/Interface'
import {
getCredentialData,
getCredentialParam,
executeJavaScriptCode,
createCodeExecutionSandbox,
parseWithTypeConversion
} from '../../../src/utils'
import { getCredentialData, getCredentialParam, executeJavaScriptCode, createCodeExecutionSandbox } from '../../../src/utils'
import { isValidUUID, isValidURL } from '../../../src/validator'
import { v4 as uuidv4 } from 'uuid'
@ -279,7 +273,7 @@ class AgentflowTool extends StructuredTool {
}
let parsed
try {
parsed = await parseWithTypeConversion(this.schema, arg)
parsed = await this.schema.parseAsync(arg)
} catch (e) {
throw new Error(`Received tool input did not match expected schema: ${JSON.stringify(arg)}`)
}

View File

@ -4,13 +4,7 @@ import { RunnableConfig } from '@langchain/core/runnables'
import { CallbackManagerForToolRun, Callbacks, CallbackManager, parseCallbackConfigArg } from '@langchain/core/callbacks/manager'
import { StructuredTool } from '@langchain/core/tools'
import { ICommonObject, IDatabaseEntity, INode, INodeData, INodeOptionsValue, INodeParams } from '../../../src/Interface'
import {
getCredentialData,
getCredentialParam,
executeJavaScriptCode,
createCodeExecutionSandbox,
parseWithTypeConversion
} from '../../../src/utils'
import { getCredentialData, getCredentialParam, executeJavaScriptCode, createCodeExecutionSandbox } from '../../../src/utils'
import { isValidUUID, isValidURL } from '../../../src/validator'
import { v4 as uuidv4 } from 'uuid'
@ -287,7 +281,7 @@ class ChatflowTool extends StructuredTool {
}
let parsed
try {
parsed = await parseWithTypeConversion(this.schema, arg)
parsed = await this.schema.parseAsync(arg)
} catch (e) {
throw new Error(`Received tool input did not match expected schema: ${JSON.stringify(arg)}`)
}

View File

@ -1,5 +1,5 @@
import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
import { getBaseClasses, getCredentialData, getCredentialParam, parseWithTypeConversion } from '../../../src/utils'
import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
import { StructuredTool, ToolInputParsingException, ToolParams } from '@langchain/core/tools'
import { Sandbox } from '@e2b/code-interpreter'
import { z } from 'zod'
@ -159,7 +159,7 @@ export class E2BTool extends StructuredTool {
}
let parsed
try {
parsed = await parseWithTypeConversion(this.schema, arg)
parsed = await this.schema.parseAsync(arg)
} catch (e) {
throw new ToolInputParsingException(`Received tool input did not match expected schema`, JSON.stringify(arg))
}

View File

@ -2,7 +2,7 @@ import { z } from 'zod'
import { RunnableConfig } from '@langchain/core/runnables'
import { StructuredTool, ToolParams } from '@langchain/core/tools'
import { CallbackManagerForToolRun, Callbacks, CallbackManager, parseCallbackConfigArg } from '@langchain/core/callbacks/manager'
import { executeJavaScriptCode, createCodeExecutionSandbox, parseWithTypeConversion } from '../../../src/utils'
import { executeJavaScriptCode, createCodeExecutionSandbox } from '../../../src/utils'
import { ICommonObject } from '../../../src/Interface'
class ToolInputParsingException extends Error {
@ -68,7 +68,7 @@ export class DynamicStructuredTool<
}
let parsed
try {
parsed = await parseWithTypeConversion(this.schema, arg)
parsed = await this.schema.parseAsync(arg)
} catch (e) {
throw new ToolInputParsingException(`Received tool input did not match expected schema`, JSON.stringify(arg))
}

View File

@ -272,22 +272,6 @@ class GoogleCalendar_Tools implements INode {
additionalParams: true,
optional: true
},
{
label: 'Send Updates to',
name: 'sendUpdates',
type: 'options',
description: 'Send Updates to attendees',
options: [
{ label: 'All', name: 'all' },
{ label: 'External Only', name: 'externalOnly' },
{ label: 'None', name: 'none' }
],
show: {
eventActions: ['createEvent', 'updateEvent']
},
additionalParams: true,
optional: true
},
{
label: 'Recurrence Rules',
name: 'recurrence',
@ -576,6 +560,7 @@ class GoogleCalendar_Tools implements INode {
}
const defaultParams = this.transformNodeInputsToToolArgs(nodeData)
const tools = createGoogleCalendarTools({
accessToken,
actions,
@ -602,7 +587,6 @@ class GoogleCalendar_Tools implements INode {
if (nodeData.inputs?.startDate) defaultParams.startDate = nodeData.inputs.startDate
if (nodeData.inputs?.endDate) defaultParams.endDate = nodeData.inputs.endDate
if (nodeData.inputs?.attendees) defaultParams.attendees = nodeData.inputs.attendees
if (nodeData.inputs?.sendUpdates) defaultParams.sendUpdates = nodeData.inputs.sendUpdates
if (nodeData.inputs?.recurrence) defaultParams.recurrence = nodeData.inputs.recurrence
if (nodeData.inputs?.reminderMinutes) defaultParams.reminderMinutes = nodeData.inputs.reminderMinutes
if (nodeData.inputs?.visibility) defaultParams.visibility = nodeData.inputs.visibility

View File

@ -48,7 +48,6 @@ const CreateEventSchema = z.object({
endDate: z.string().optional().describe('End date for all-day events (YYYY-MM-DD)'),
timeZone: z.string().optional().describe('Time zone (e.g., America/New_York)'),
attendees: z.string().optional().describe('Comma-separated list of attendee emails'),
sendUpdates: z.enum(['all', 'externalOnly', 'none']).optional().default('all').describe('Whether to send notifications to attendees'),
recurrence: z.string().optional().describe('Recurrence rules (RRULE format)'),
reminderMinutes: z.number().optional().describe('Minutes before event to send reminder'),
visibility: z.enum(['default', 'public', 'private', 'confidential']).optional().describe('Event visibility')
@ -71,7 +70,6 @@ const UpdateEventSchema = z.object({
endDate: z.string().optional().describe('Updated end date for all-day events (YYYY-MM-DD)'),
timeZone: z.string().optional().describe('Updated time zone'),
attendees: z.string().optional().describe('Updated comma-separated list of attendee emails'),
sendUpdates: z.enum(['all', 'externalOnly', 'none']).optional().default('all').describe('Whether to send notifications to attendees'),
recurrence: z.string().optional().describe('Updated recurrence rules'),
reminderMinutes: z.number().optional().describe('Updated reminder minutes'),
visibility: z.enum(['default', 'public', 'private', 'confidential']).optional().describe('Updated event visibility')
@ -288,11 +286,8 @@ class CreateEventTool extends BaseGoogleCalendarTool {
}
if (params.visibility) eventData.visibility = params.visibility
const queryParams = new URLSearchParams()
if (params.sendUpdates) queryParams.append('sendUpdates', params.sendUpdates)
const endpoint = `calendars/${encodeURIComponent(params.calendarId)}/events?${queryParams.toString()}`
const endpoint = `calendars/${encodeURIComponent(params.calendarId)}/events`
const response = await this.makeGoogleCalendarRequest({ endpoint, method: 'POST', body: eventData, params })
return response
} catch (error) {
@ -400,12 +395,8 @@ class UpdateEventTool extends BaseGoogleCalendarTool {
}
if (params.visibility) updateData.visibility = params.visibility
const queryParams = new URLSearchParams()
if (params.sendUpdates) queryParams.append('sendUpdates', params.sendUpdates)
const endpoint = `calendars/${encodeURIComponent(params.calendarId)}/events/${encodeURIComponent(
params.eventId
)}?${queryParams.toString()}`
const endpoint = `calendars/${encodeURIComponent(params.calendarId)}/events/${encodeURIComponent(params.eventId)}`
const response = await this.makeGoogleCalendarRequest({ endpoint, method: 'PUT', body: updateData, params })
return response
} catch (error) {

View File

@ -136,17 +136,17 @@ class Custom_MCP implements INode {
}
let sandbox: ICommonObject = {}
const workspaceId = options?.searchOptions?.workspaceId?._value || options?.workspaceId
if (mcpServerConfig.includes('$vars')) {
const appDataSource = options.appDataSource as DataSource
const databaseEntities = options.databaseEntities as IDatabaseEntity
// If options.workspaceId is not set, create a new options object with the workspaceId for getVars.
const optionsWithWorkspaceId = options.workspaceId ? options : { ...options, workspaceId }
const variables = await getVars(appDataSource, databaseEntities, nodeData, optionsWithWorkspaceId)
const variables = await getVars(appDataSource, databaseEntities, nodeData, options)
sandbox['$vars'] = prepareSandboxVars(variables)
}
const workspaceId = options?.searchOptions?.workspaceId?._value || options?.workspaceId
let canonicalConfig
try {
canonicalConfig = JSON.parse(mcpServerConfig)

View File

@ -114,7 +114,7 @@ export class MCPToolkit extends BaseToolkit {
const res = await Promise.allSettled(toolsPromises)
const errors = res.filter((r) => r.status === 'rejected')
if (errors.length !== 0) {
console.error('MCP Tools failed to be resolved', errors)
console.error('MCP Tools falied to be resolved', errors)
}
const successes = res.filter((r) => r.status === 'fulfilled').map((r) => r.value)
return successes

View File

@ -5,7 +5,6 @@ import $RefParser from '@apidevtools/json-schema-ref-parser'
import { z, ZodSchema, ZodTypeAny } from 'zod'
import { defaultCode, DynamicStructuredTool, howToUseCode } from './core'
import { DataSource } from 'typeorm'
import fetch from 'node-fetch'
class OpenAPIToolkit_Tools implements INode {
label: string
@ -22,64 +21,17 @@ class OpenAPIToolkit_Tools implements INode {
constructor() {
this.label = 'OpenAPI Toolkit'
this.name = 'openAPIToolkit'
this.version = 2.1
this.version = 2.0
this.type = 'OpenAPIToolkit'
this.icon = 'openapi.svg'
this.category = 'Tools'
this.description = 'Load OpenAPI specification, and converts each API endpoint to a tool'
this.inputs = [
{
label: 'Input Type',
name: 'inputType',
type: 'options',
options: [
{
label: 'Upload File',
name: 'file'
},
{
label: 'Provide Link',
name: 'link'
}
],
default: 'file',
description: 'Choose how to provide the OpenAPI specification'
},
{
label: 'OpenAPI File',
name: 'openApiFile',
label: 'YAML File',
name: 'yamlFile',
type: 'file',
fileType: '.yaml,.json',
description: 'Upload your OpenAPI specification file (YAML or JSON)',
show: {
inputType: 'file'
}
},
{
label: 'OpenAPI Link',
name: 'openApiLink',
type: 'string',
placeholder: 'https://api.example.com/openapi.yaml or https://api.example.com/openapi.json',
description: 'Provide a link to your OpenAPI specification (YAML or JSON)',
show: {
inputType: 'link'
}
},
{
label: 'Server',
name: 'selectedServer',
type: 'asyncOptions',
loadMethod: 'listServers',
description: 'Select which server to use for API calls',
refresh: true
},
{
label: 'Available Endpoints',
name: 'selectedEndpoints',
type: 'asyncMultiOptions',
loadMethod: 'listEndpoints',
description: 'Select which endpoints to expose as tools',
refresh: true
fileType: '.yaml'
},
{
label: 'Return Direct',
@ -94,7 +46,8 @@ class OpenAPIToolkit_Tools implements INode {
type: 'json',
description: 'Request headers to be sent with the API request. For example, {"Authorization": "Bearer token"}',
additionalParams: true,
optional: true
optional: true,
acceptVariable: true
},
{
label: 'Remove null parameters',
@ -123,237 +76,49 @@ class OpenAPIToolkit_Tools implements INode {
async init(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
const toolReturnDirect = nodeData.inputs?.returnDirect as boolean
const inputType = nodeData.inputs?.inputType as string
const openApiFile = nodeData.inputs?.openApiFile as string
const openApiLink = nodeData.inputs?.openApiLink as string
const selectedServer = nodeData.inputs?.selectedServer as string
const yamlFileBase64 = nodeData.inputs?.yamlFile as string
const customCode = nodeData.inputs?.customCode as string
const _headers = nodeData.inputs?.headers as string
const removeNulls = nodeData.inputs?.removeNulls as boolean
const headers = typeof _headers === 'object' ? _headers : _headers ? JSON.parse(_headers) : {}
const specData = await this.loadOpenApiSpec(
{
inputType,
openApiFile,
openApiLink
},
options
)
if (!specData) throw new Error('Failed to load OpenAPI spec')
let data
if (yamlFileBase64.startsWith('FILE-STORAGE::')) {
const file = yamlFileBase64.replace('FILE-STORAGE::', '')
const orgId = options.orgId
const chatflowid = options.chatflowid
const fileData = await getFileFromStorage(file, orgId, chatflowid)
const utf8String = fileData.toString('utf-8')
const _data: any = await $RefParser.dereference(specData)
// Use selected server or fallback to first server
let baseUrl: string
if (selectedServer && selectedServer !== 'error') {
baseUrl = selectedServer
data = load(utf8String)
} else {
baseUrl = _data.servers?.[0]?.url
const splitDataURI = yamlFileBase64.split(',')
splitDataURI.pop()
const bf = Buffer.from(splitDataURI.pop() || '', 'base64')
const utf8String = bf.toString('utf-8')
data = load(utf8String)
}
if (!data) {
throw new Error('Failed to load OpenAPI spec')
}
if (!baseUrl) throw new Error('OpenAPI spec does not contain a server URL')
const _data: any = await $RefParser.dereference(data)
const baseUrl = _data.servers[0]?.url
if (!baseUrl) {
throw new Error('OpenAPI spec does not contain a server URL')
}
const appDataSource = options.appDataSource as DataSource
const databaseEntities = options.databaseEntities as IDatabaseEntity
const variables = await getVars(appDataSource, databaseEntities, nodeData, options)
const flow = { chatflowId: options.chatflowid }
let tools = getTools(_data.paths, baseUrl, headers, variables, flow, toolReturnDirect, customCode, removeNulls)
// Filter by selected endpoints if provided
const _selected = nodeData.inputs?.selectedEndpoints
let selected: string[] = []
if (_selected) {
try {
selected = typeof _selected === 'string' ? JSON.parse(_selected) : _selected
} catch (e) {
selected = []
}
}
if (selected.length) {
tools = tools.filter((t: any) => selected.includes(t.name))
}
const tools = getTools(_data.paths, baseUrl, headers, variables, flow, toolReturnDirect, customCode, removeNulls)
return tools
}
//@ts-ignore
loadMethods = {
listServers: async (nodeData: INodeData, options: ICommonObject) => {
try {
const inputType = nodeData.inputs?.inputType as string
const openApiFile = nodeData.inputs?.openApiFile as string
const openApiLink = nodeData.inputs?.openApiLink as string
const specData: any = await this.loadOpenApiSpec(
{
inputType,
openApiFile,
openApiLink
},
options
)
if (!specData) return []
const _data: any = await $RefParser.dereference(specData)
const items: { label: string; name: string; description?: string }[] = []
const servers = _data.servers || []
if (servers.length === 0) {
return [
{
label: 'No Servers Found',
name: 'error',
description: 'No servers defined in the OpenAPI specification'
}
]
}
for (let i = 0; i < servers.length; i++) {
const server = servers[i]
const serverUrl = server.url || `Server ${i + 1}`
const serverDesc = server.description || serverUrl
items.push({
label: serverUrl,
name: serverUrl,
description: serverDesc
})
}
return items
} catch (e) {
return [
{
label: 'No Servers Found',
name: 'error',
description: 'No available servers, check the link/file and refresh'
}
]
}
},
listEndpoints: async (nodeData: INodeData, options: ICommonObject) => {
try {
const inputType = nodeData.inputs?.inputType as string
const openApiFile = nodeData.inputs?.openApiFile as string
const openApiLink = nodeData.inputs?.openApiLink as string
const specData: any = await this.loadOpenApiSpec(
{
inputType,
openApiFile,
openApiLink
},
options
)
if (!specData) return []
const _data: any = await $RefParser.dereference(specData)
const items: { label: string; name: string; description?: string }[] = []
const paths = _data.paths || {}
for (const path in paths) {
const methods = paths[path]
for (const method in methods) {
if (['get', 'post', 'put', 'delete', 'patch'].includes(method)) {
const spec = methods[method]
const opId = spec.operationId || `${method.toUpperCase()} ${path}`
const desc = spec.description || spec.summary || opId
items.push({ label: opId, name: opId, description: desc })
}
}
}
items.sort((a, b) => a.label.localeCompare(b.label))
return items
} catch (e) {
return [
{
label: 'No Endpoints Found',
name: 'error',
description: 'No available endpoints, check the link/file and refresh'
}
]
}
}
}
private async loadOpenApiSpec(
args: {
inputType?: string
openApiFile?: string
openApiLink?: string
},
options: ICommonObject
): Promise<any | null> {
const { inputType = 'file', openApiFile = '', openApiLink = '' } = args
try {
if (inputType === 'link' && openApiLink) {
const res = await fetch(openApiLink)
const text = await res.text()
// Auto-detect format from URL extension or content
const isJsonUrl = openApiLink.toLowerCase().includes('.json')
const isYamlUrl = openApiLink.toLowerCase().includes('.yaml') || openApiLink.toLowerCase().includes('.yml')
if (isJsonUrl) {
return JSON.parse(text)
} else if (isYamlUrl) {
return load(text)
} else {
// Auto-detect format from content
try {
return JSON.parse(text)
} catch (_) {
return load(text)
}
}
}
if (inputType === 'file' && openApiFile) {
let utf8String: string
let fileName = ''
if (openApiFile.startsWith('FILE-STORAGE::')) {
const file = openApiFile.replace('FILE-STORAGE::', '')
fileName = file
const orgId = options.orgId
const chatflowid = options.chatflowid
const fileData = await getFileFromStorage(file, orgId, chatflowid)
utf8String = fileData.toString('utf-8')
} else {
// Extract filename from data URI if possible
const splitDataURI = openApiFile.split(',')
const mimeType = splitDataURI[0] || ''
if (mimeType.includes('filename=')) {
const filenameMatch = mimeType.match(/filename=([^;]+)/)
if (filenameMatch) {
fileName = filenameMatch[1]
}
}
splitDataURI.pop()
const bf = Buffer.from(splitDataURI.pop() || '', 'base64')
utf8String = bf.toString('utf-8')
}
// Auto-detect format from file extension or content
const isJsonFile = fileName.toLowerCase().endsWith('.json')
const isYamlFile = fileName.toLowerCase().endsWith('.yaml') || fileName.toLowerCase().endsWith('.yml')
if (isJsonFile) {
return JSON.parse(utf8String)
} else if (isYamlFile) {
return load(utf8String)
} else {
// Auto-detect format from content
try {
return JSON.parse(utf8String)
} catch (_) {
return load(utf8String)
}
}
}
} catch (e) {
console.error('Error loading OpenAPI spec:', e)
return null
}
return null
}
}
const jsonSchemaToZodSchema = (schema: any, requiredList: string[], keyName: string): ZodSchema<any> => {

View File

@ -3,7 +3,7 @@ import { RequestInit } from 'node-fetch'
import { RunnableConfig } from '@langchain/core/runnables'
import { StructuredTool, ToolParams } from '@langchain/core/tools'
import { CallbackManagerForToolRun, Callbacks, CallbackManager, parseCallbackConfigArg } from '@langchain/core/callbacks/manager'
import { executeJavaScriptCode, createCodeExecutionSandbox, parseWithTypeConversion } from '../../../src/utils'
import { executeJavaScriptCode, createCodeExecutionSandbox } from '../../../src/utils'
import { ICommonObject } from '../../../src/Interface'
const removeNulls = (obj: Record<string, any>) => {
@ -174,7 +174,7 @@ export class DynamicStructuredTool<
}
let parsed
try {
parsed = await parseWithTypeConversion(this.schema, arg)
parsed = await this.schema.parseAsync(arg)
} catch (e) {
throw new ToolInputParsingException(`Received tool input did not match expected schema ${e}`, JSON.stringify(arg))
}

View File

@ -0,0 +1,147 @@
import { z } from 'zod'
import path from 'path'
import { StructuredTool, ToolParams } from '@langchain/core/tools'
import { Serializable } from '@langchain/core/load/serializable'
import { INode, INodeData, INodeParams } from '../../../src/Interface'
import { getBaseClasses, getUserHome } from '../../../src/utils'
import { SecureFileStore, FileSecurityConfig } from '../../../src/SecureFileStore'
abstract class BaseFileStore extends Serializable {
abstract readFile(path: string): Promise<string>
abstract writeFile(path: string, contents: string): Promise<void>
}
class ReadFile_Tools implements INode {
label: string
name: string
version: number
description: string
type: string
icon: string
category: string
baseClasses: string[]
inputs: INodeParams[]
warning: string
constructor() {
this.label = 'Read File'
this.name = 'readFile'
this.version = 2.0
this.type = 'ReadFile'
this.icon = 'readfile.svg'
this.category = 'Tools'
this.warning = 'This tool can be used to read files from the disk. It is recommended to use this tool with caution.'
this.description = 'Read file from disk'
this.baseClasses = [this.type, 'Tool', ...getBaseClasses(ReadFileTool)]
this.inputs = [
{
label: 'Workspace Path',
name: 'workspacePath',
placeholder: `C:\\Users\\User\\MyProject`,
type: 'string',
description: 'Base workspace directory for file operations. All file paths will be relative to this directory.',
optional: true
},
{
label: 'Enforce Workspace Boundaries',
name: 'enforceWorkspaceBoundaries',
type: 'boolean',
description: 'When enabled, restricts file access to the workspace directory for security. Recommended: true',
default: true,
optional: true
},
{
label: 'Max File Size (MB)',
name: 'maxFileSize',
type: 'number',
description: 'Maximum file size in megabytes that can be read',
default: 10,
optional: true
},
{
label: 'Allowed Extensions',
name: 'allowedExtensions',
type: 'string',
description: 'Comma-separated list of allowed file extensions (e.g., .txt,.json,.md). Leave empty to allow all.',
placeholder: '.txt,.json,.md,.py,.js',
optional: true
}
]
}
async init(nodeData: INodeData): Promise<any> {
const workspacePath = nodeData.inputs?.workspacePath as string
const enforceWorkspaceBoundaries = nodeData.inputs?.enforceWorkspaceBoundaries !== false // Default to true
const maxFileSize = nodeData.inputs?.maxFileSize as number
const allowedExtensions = nodeData.inputs?.allowedExtensions as string
// Parse allowed extensions
const allowedExtensionsList = allowedExtensions ? allowedExtensions.split(',').map((ext) => ext.trim().toLowerCase()) : []
let store: BaseFileStore
if (workspacePath) {
// Create secure file store with workspace boundaries
const config: FileSecurityConfig = {
workspacePath,
enforceWorkspaceBoundaries,
maxFileSize: maxFileSize ? maxFileSize * 1024 * 1024 : undefined, // Convert MB to bytes
allowedExtensions: allowedExtensionsList.length > 0 ? allowedExtensionsList : undefined
}
store = new SecureFileStore(config)
} else {
// Fallback to current working directory with security warnings
if (enforceWorkspaceBoundaries) {
const fallbackWorkspacePath = path.join(getUserHome(), '.flowise')
console.warn(`[ReadFile] No workspace path specified, using ${fallbackWorkspacePath} with security restrictions`)
store = new SecureFileStore({
workspacePath: fallbackWorkspacePath,
enforceWorkspaceBoundaries: true,
maxFileSize: maxFileSize ? maxFileSize * 1024 * 1024 : undefined,
allowedExtensions: allowedExtensionsList.length > 0 ? allowedExtensionsList : undefined
})
} else {
console.warn('[ReadFile] SECURITY WARNING: Workspace boundaries disabled - unrestricted file access enabled')
store = SecureFileStore.createUnsecure()
}
}
return new ReadFileTool({ store })
}
}
interface ReadFileParams extends ToolParams {
store: BaseFileStore
}
/**
* Class for reading files from the disk. Extends the StructuredTool
* class.
*/
export class ReadFileTool extends StructuredTool {
static lc_name() {
return 'ReadFileTool'
}
schema = z.object({
file_path: z.string().describe('name of file')
}) as any
name = 'read_file'
description = 'Read file from disk'
store: BaseFileStore
constructor({ store }: ReadFileParams) {
super(...arguments)
this.store = store
}
async _call({ file_path }: z.infer<typeof this.schema>) {
return await this.store.readFile(file_path)
}
}
module.exports = { nodeClass: ReadFile_Tools }

View File

@ -0,0 +1,4 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 5H9C7.89543 5 7 5.89543 7 7V25C7 26.1046 7.89543 27 9 27H12M18 5L25 12M18 5V12H25M25 12V25C25 26.1046 24.1046 27 23 27H20" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M16 17V29M16 17L13 20.1361M16 17L19 20.1361" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 455 B

View File

@ -3,7 +3,7 @@ import { CallbackManager, CallbackManagerForToolRun, Callbacks, parseCallbackCon
import { BaseDynamicToolInput, DynamicTool, StructuredTool, ToolInputParsingException } from '@langchain/core/tools'
import { BaseRetriever } from '@langchain/core/retrievers'
import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
import { getBaseClasses, resolveFlowObjValue, parseWithTypeConversion } from '../../../src/utils'
import { getBaseClasses, resolveFlowObjValue } from '../../../src/utils'
import { SOURCE_DOCUMENTS_PREFIX } from '../../../src/agents'
import { RunnableConfig } from '@langchain/core/runnables'
import { VectorStoreRetriever } from '@langchain/core/vectorstores'
@ -58,7 +58,7 @@ class DynamicStructuredTool<T extends z.ZodObject<any, any, any, any> = z.ZodObj
}
let parsed
try {
parsed = await parseWithTypeConversion(this.schema, arg)
parsed = await this.schema.parseAsync(arg)
} catch (e) {
throw new ToolInputParsingException(`Received tool input did not match expected schema`, JSON.stringify(arg))
}

View File

@ -0,0 +1,149 @@
import { z } from 'zod'
import path from 'path'
import { StructuredTool, ToolParams } from '@langchain/core/tools'
import { Serializable } from '@langchain/core/load/serializable'
import { INode, INodeData, INodeParams } from '../../../src/Interface'
import { getBaseClasses, getUserHome } from '../../../src/utils'
import { SecureFileStore, FileSecurityConfig } from '../../../src/SecureFileStore'
abstract class BaseFileStore extends Serializable {
abstract readFile(path: string): Promise<string>
abstract writeFile(path: string, contents: string): Promise<void>
}
class WriteFile_Tools implements INode {
label: string
name: string
version: number
description: string
type: string
icon: string
category: string
baseClasses: string[]
inputs: INodeParams[]
warning: string
constructor() {
this.label = 'Write File'
this.name = 'writeFile'
this.version = 2.0
this.type = 'WriteFile'
this.icon = 'writefile.svg'
this.category = 'Tools'
this.warning = 'This tool can be used to write files to the disk. It is recommended to use this tool with caution.'
this.description = 'Write file to disk'
this.baseClasses = [this.type, 'Tool', ...getBaseClasses(WriteFileTool)]
this.inputs = [
{
label: 'Workspace Path',
name: 'workspacePath',
placeholder: `C:\\Users\\User\\MyProject`,
type: 'string',
description: 'Base workspace directory for file operations. All file paths will be relative to this directory.',
optional: true
},
{
label: 'Enforce Workspace Boundaries',
name: 'enforceWorkspaceBoundaries',
type: 'boolean',
description: 'When enabled, restricts file access to the workspace directory for security. Recommended: true',
default: true,
optional: true
},
{
label: 'Max File Size (MB)',
name: 'maxFileSize',
type: 'number',
description: 'Maximum file size in megabytes that can be written',
default: 10,
optional: true
},
{
label: 'Allowed Extensions',
name: 'allowedExtensions',
type: 'string',
description: 'Comma-separated list of allowed file extensions (e.g., .txt,.json,.md). Leave empty to allow all.',
placeholder: '.txt,.json,.md,.py,.js',
optional: true
}
]
}
async init(nodeData: INodeData): Promise<any> {
const workspacePath = nodeData.inputs?.workspacePath as string
const enforceWorkspaceBoundaries = nodeData.inputs?.enforceWorkspaceBoundaries !== false // Default to true
const maxFileSize = nodeData.inputs?.maxFileSize as number
const allowedExtensions = nodeData.inputs?.allowedExtensions as string
// Parse allowed extensions
const allowedExtensionsList = allowedExtensions ? allowedExtensions.split(',').map((ext) => ext.trim().toLowerCase()) : []
let store: BaseFileStore
if (workspacePath) {
// Create secure file store with workspace boundaries
const config: FileSecurityConfig = {
workspacePath,
enforceWorkspaceBoundaries,
maxFileSize: maxFileSize ? maxFileSize * 1024 * 1024 : undefined, // Convert MB to bytes
allowedExtensions: allowedExtensionsList.length > 0 ? allowedExtensionsList : undefined
}
store = new SecureFileStore(config)
} else {
// Fallback to current working directory with security warnings
if (enforceWorkspaceBoundaries) {
const fallbackWorkspacePath = path.join(getUserHome(), '.flowise')
console.warn(`[WriteFile] No workspace path specified, using ${fallbackWorkspacePath} with security restrictions`)
store = new SecureFileStore({
workspacePath: fallbackWorkspacePath,
enforceWorkspaceBoundaries: true,
maxFileSize: maxFileSize ? maxFileSize * 1024 * 1024 : undefined,
allowedExtensions: allowedExtensionsList.length > 0 ? allowedExtensionsList : undefined
})
} else {
console.warn('[WriteFile] SECURITY WARNING: Workspace boundaries disabled - unrestricted file access enabled')
store = SecureFileStore.createUnsecure()
}
}
return new WriteFileTool({ store })
}
}
interface WriteFileParams extends ToolParams {
store: BaseFileStore
}
/**
* Class for writing data to files on the disk. Extends the StructuredTool
* class.
*/
export class WriteFileTool extends StructuredTool {
static lc_name() {
return 'WriteFileTool'
}
schema = z.object({
file_path: z.string().describe('name of file'),
text: z.string().describe('text to write to file')
}) as any
name = 'write_file'
description = 'Write file to disk'
store: BaseFileStore
constructor({ store, ...rest }: WriteFileParams) {
super(rest)
this.store = store
}
async _call({ file_path, text }: z.infer<typeof this.schema>) {
await this.store.writeFile(file_path, text)
return `File written to ${file_path} successfully.`
}
}
module.exports = { nodeClass: WriteFile_Tools }

View File

@ -0,0 +1,4 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M25 18V25C25 26.1046 24.1046 27 23 27H9C7.89543 27 7 26.1046 7 25V7C7 5.89543 7.89543 5 9 5H18L19 6" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 19.3284V22H14.6716C15.202 22 15.7107 21.7893 16.0858 21.4142L24.5858 12.9142C25.3668 12.1332 25.3668 10.8668 24.5858 10.0858L23.9142 9.41421C23.1332 8.63316 21.8668 8.63317 21.0858 9.41421L12.5858 17.9142C12.2107 18.2893 12 18.798 12 19.3284Z" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 632 B

View File

@ -84,16 +84,11 @@ class CustomFunction_Utilities implements INode {
const variables = await getVars(appDataSource, databaseEntities, nodeData, options)
const flow = {
input,
chatflowId: options.chatflowid,
sessionId: options.sessionId,
chatId: options.chatId,
rawOutput: options.postProcessing?.rawOutput || '',
chatHistory: options.postProcessing?.chatHistory || [],
sourceDocuments: options.postProcessing?.sourceDocuments,
usedTools: options.postProcessing?.usedTools,
artifacts: options.postProcessing?.artifacts,
fileAnnotations: options.postProcessing?.fileAnnotations
rawOutput: options.rawOutput || '',
input
}
let inputVars: ICommonObject = {}

View File

@ -3,7 +3,7 @@ import { Chroma } from '@langchain/community/vectorstores/chroma'
import { Embeddings } from '@langchain/core/embeddings'
import { Document } from '@langchain/core/documents'
import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams, IndexingResult } from '../../../src/Interface'
import { getBaseClasses, getCredentialData, getCredentialParam, parseJsonBody } from '../../../src/utils'
import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
import { ChromaExtended } from './core'
import { index } from '../../../src/indexing'
@ -186,11 +186,7 @@ class Chroma_VectorStores implements INode {
const vectorStoreName = collectionName
await recordManager.createSchema()
;(recordManager as any).namespace = (recordManager as any).namespace + '_' + vectorStoreName
const filterKeys: ICommonObject = {}
if (options.docId) {
filterKeys.docId = options.docId
}
const keys: string[] = await recordManager.listKeys(filterKeys)
const keys: string[] = await recordManager.listKeys({})
const chromaStore = new ChromaExtended(embeddings, obj)
@ -233,7 +229,7 @@ class Chroma_VectorStores implements INode {
if (chromaTenant) obj.chromaTenant = chromaTenant
if (chromaDatabase) obj.chromaDatabase = chromaDatabase
if (chromaMetadataFilter) {
const metadatafilter = typeof chromaMetadataFilter === 'object' ? chromaMetadataFilter : parseJsonBody(chromaMetadataFilter)
const metadatafilter = typeof chromaMetadataFilter === 'object' ? chromaMetadataFilter : JSON.parse(chromaMetadataFilter)
obj.filter = metadatafilter
}

View File

@ -4,7 +4,7 @@ import { Document } from '@langchain/core/documents'
import { CouchbaseVectorStore, CouchbaseVectorStoreArgs } from '@langchain/community/vectorstores/couchbase'
import { Cluster } from 'couchbase'
import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams, IndexingResult } from '../../../src/Interface'
import { getBaseClasses, getCredentialData, getCredentialParam, parseJsonBody } from '../../../src/utils'
import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
import { resolveVectorStoreOrRetriever } from '../VectorStoreUtils'
class Couchbase_VectorStores implements INode {
@ -215,8 +215,7 @@ class Couchbase_VectorStores implements INode {
if (!embeddingKey || embeddingKey === '') couchbaseConfig.embeddingKey = 'embedding'
if (couchbaseMetadataFilter) {
metadatafilter =
typeof couchbaseMetadataFilter === 'object' ? couchbaseMetadataFilter : parseJsonBody(couchbaseMetadataFilter)
metadatafilter = typeof couchbaseMetadataFilter === 'object' ? couchbaseMetadataFilter : JSON.parse(couchbaseMetadataFilter)
}
const vectorStore = await CouchbaseVectorStore.initialize(embeddings, couchbaseConfig)

View File

@ -198,11 +198,7 @@ class Elasticsearch_VectorStores implements INode {
const vectorStoreName = indexName
await recordManager.createSchema()
;(recordManager as any).namespace = (recordManager as any).namespace + '_' + vectorStoreName
const filterKeys: ICommonObject = {}
if (options.docId) {
filterKeys.docId = options.docId
}
const keys: string[] = await recordManager.listKeys(filterKeys)
const keys: string[] = await recordManager.listKeys({})
await vectorStore.delete({ ids: keys })
await recordManager.deleteKeys(keys)

View File

@ -3,7 +3,7 @@ import { AmazonKendraRetriever } from '@langchain/aws'
import { KendraClient, BatchPutDocumentCommand, BatchDeleteDocumentCommand } from '@aws-sdk/client-kendra'
import { Document } from '@langchain/core/documents'
import { ICommonObject, INode, INodeData, INodeOptionsValue, INodeOutputsValue, INodeParams, IndexingResult } from '../../../src/Interface'
import { FLOWISE_CHATID, getCredentialData, getCredentialParam, parseJsonBody } from '../../../src/utils'
import { FLOWISE_CHATID, getCredentialData, getCredentialParam } from '../../../src/utils'
import { howToUseFileUpload } from '../VectorStoreUtils'
import { MODEL_TYPE, getRegions } from '../../../src/modelLoader'
@ -248,7 +248,7 @@ class Kendra_VectorStores implements INode {
let filter = undefined
if (attributeFilter) {
filter = typeof attributeFilter === 'object' ? attributeFilter : parseJsonBody(attributeFilter)
filter = typeof attributeFilter === 'object' ? attributeFilter : JSON.parse(attributeFilter)
}
// Add chat-specific filtering if file upload is enabled

View File

@ -79,7 +79,7 @@ class MeilisearchRetriever_node implements INode {
label: 'Semantic Ratio',
name: 'semanticRatio',
type: 'number',
description: 'percentage of semantic reasoning in meilisearch hybrid search, default is 0.75',
description: 'percentage of sematic reasoning in meilisearch hybrid search, default is 0.75',
additionalParams: true,
optional: true
},
@ -162,7 +162,7 @@ class MeilisearchRetriever_node implements INode {
}
} catch (error) {
console.error(error)
console.warn('Error occurred when deleting your index, if it did not exist, we will create one for you... ')
console.warn('Error occured when deleting your index, if it did not exist, we will create one for you... ')
}
}

View File

@ -2,7 +2,7 @@ import { flatten } from 'lodash'
import { Embeddings } from '@langchain/core/embeddings'
import { Document } from '@langchain/core/documents'
import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams, IndexingResult } from '../../../src/Interface'
import { getBaseClasses, getCredentialData, getCredentialParam, parseJsonBody } from '../../../src/utils'
import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
import { addMMRInputParams, resolveVectorStoreOrRetriever } from '../VectorStoreUtils'
import { MongoDBAtlasVectorSearch } from './core'
@ -187,7 +187,7 @@ class MongoDBAtlas_VectorStores implements INode {
})
if (mongoMetadataFilter) {
const metadataFilter = typeof mongoMetadataFilter === 'object' ? mongoMetadataFilter : parseJsonBody(mongoMetadataFilter)
const metadataFilter = typeof mongoMetadataFilter === 'object' ? mongoMetadataFilter : JSON.parse(mongoMetadataFilter)
for (const key in metadataFilter) {
mongoDbFilter.preFilter = {

View File

@ -5,7 +5,7 @@ import { Embeddings } from '@langchain/core/embeddings'
import { Document } from '@langchain/core/documents'
import { VectorStore } from '@langchain/core/vectorstores'
import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams, IndexingResult } from '../../../src/Interface'
import { FLOWISE_CHATID, getBaseClasses, getCredentialData, getCredentialParam, parseJsonBody } from '../../../src/utils'
import { FLOWISE_CHATID, getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
import { addMMRInputParams, howToUseFileUpload, resolveVectorStoreOrRetriever } from '../VectorStoreUtils'
import { index } from '../../../src/indexing'
@ -212,11 +212,7 @@ class Pinecone_VectorStores implements INode {
const vectorStoreName = pineconeNamespace
await recordManager.createSchema()
;(recordManager as any).namespace = (recordManager as any).namespace + '_' + vectorStoreName
const filterKeys: ICommonObject = {}
if (options.docId) {
filterKeys.docId = options.docId
}
const keys: string[] = await recordManager.listKeys(filterKeys)
const keys: string[] = await recordManager.listKeys({})
await pineconeStore.delete({ ids: keys })
await recordManager.deleteKeys(keys)
@ -252,8 +248,7 @@ class Pinecone_VectorStores implements INode {
if (pineconeNamespace) obj.namespace = pineconeNamespace
if (pineconeMetadataFilter) {
const metadatafilter =
typeof pineconeMetadataFilter === 'object' ? pineconeMetadataFilter : parseJsonBody(pineconeMetadataFilter)
const metadatafilter = typeof pineconeMetadataFilter === 'object' ? pineconeMetadataFilter : JSON.parse(pineconeMetadataFilter)
obj.filter = metadatafilter
}
if (isFileUploadEnabled && options.chatId) {

View File

@ -16,7 +16,7 @@ import { FetchResponse, Index, Pinecone, ScoredPineconeRecord } from '@pinecone-
import { flatten } from 'lodash'
import { Document as LCDocument } from 'langchain/document'
import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams, IndexingResult } from '../../../src/Interface'
import { flattenObject, getCredentialData, getCredentialParam, parseJsonBody } from '../../../src/utils'
import { flattenObject, getCredentialData, getCredentialParam } from '../../../src/utils'
class PineconeLlamaIndex_VectorStores implements INode {
label: string
@ -176,7 +176,7 @@ class PineconeLlamaIndex_VectorStores implements INode {
let metadatafilter = {}
if (pineconeMetadataFilter) {
metadatafilter = typeof pineconeMetadataFilter === 'object' ? pineconeMetadataFilter : parseJsonBody(pineconeMetadataFilter)
metadatafilter = typeof pineconeMetadataFilter === 'object' ? pineconeMetadataFilter : JSON.parse(pineconeMetadataFilter)
obj.queryFilter = metadatafilter
}

View File

@ -1,7 +1,7 @@
import { flatten } from 'lodash'
import { Document } from '@langchain/core/documents'
import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams, IndexingResult } from '../../../src/Interface'
import { FLOWISE_CHATID, getBaseClasses, parseJsonBody } from '../../../src/utils'
import { FLOWISE_CHATID, getBaseClasses } from '../../../src/utils'
import { index } from '../../../src/indexing'
import { howToUseFileUpload } from '../VectorStoreUtils'
import { VectorStore } from '@langchain/core/vectorstores'
@ -49,7 +49,7 @@ class Postgres_VectorStores implements INode {
constructor() {
this.label = 'Postgres'
this.name = 'postgres'
this.version = 7.1
this.version = 7.0
this.type = 'Postgres'
this.icon = 'postgres.svg'
this.category = 'Vector Stores'
@ -173,15 +173,6 @@ class Postgres_VectorStores implements INode {
additionalParams: true,
optional: true
},
{
label: 'Upsert Batch Size',
name: 'batchSize',
type: 'number',
step: 1,
description: 'Upsert in batches of size N',
additionalParams: true,
optional: true
},
{
label: 'Additional Configuration',
name: 'additionalConfig',
@ -241,7 +232,6 @@ class Postgres_VectorStores implements INode {
const docs = nodeData.inputs?.document as Document[]
const recordManager = nodeData.inputs?.recordManager
const isFileUploadEnabled = nodeData.inputs?.fileUpload as boolean
const _batchSize = nodeData.inputs?.batchSize
const vectorStoreDriver: VectorStoreDriver = Postgres_VectorStores.getDriverFromConfig(nodeData, options)
const flattenDocs = docs && docs.length ? flatten(docs) : []
@ -275,15 +265,7 @@ class Postgres_VectorStores implements INode {
return res
} else {
if (_batchSize) {
const batchSize = parseInt(_batchSize, 10)
for (let i = 0; i < finalDocs.length; i += batchSize) {
const batch = finalDocs.slice(i, i + batchSize)
await vectorStoreDriver.fromDocuments(batch)
}
} else {
await vectorStoreDriver.fromDocuments(finalDocs)
}
await vectorStoreDriver.fromDocuments(finalDocs)
return { numAdded: finalDocs.length, addedDocs: finalDocs }
}
@ -303,11 +285,7 @@ class Postgres_VectorStores implements INode {
const vectorStoreName = tableName
await recordManager.createSchema()
;(recordManager as any).namespace = (recordManager as any).namespace + '_' + vectorStoreName
const filterKeys: ICommonObject = {}
if (options.docId) {
filterKeys.docId = options.docId
}
const keys: string[] = await recordManager.listKeys(filterKeys)
const keys: string[] = await recordManager.listKeys({})
await vectorStore.delete({ ids: keys })
await recordManager.deleteKeys(keys)
@ -330,7 +308,7 @@ class Postgres_VectorStores implements INode {
let pgMetadataFilter: any
if (_pgMetadataFilter) {
pgMetadataFilter = typeof _pgMetadataFilter === 'object' ? _pgMetadataFilter : parseJsonBody(_pgMetadataFilter)
pgMetadataFilter = typeof _pgMetadataFilter === 'object' ? _pgMetadataFilter : JSON.parse(_pgMetadataFilter)
}
if (isFileUploadEnabled && options.chatId) {
pgMetadataFilter = {

View File

@ -5,11 +5,6 @@ import { TypeORMVectorStore, TypeORMVectorStoreArgs, TypeORMVectorStoreDocument
import { VectorStore } from '@langchain/core/vectorstores'
import { Document } from '@langchain/core/documents'
import { Pool } from 'pg'
import { v4 as uuid } from 'uuid'
type TypeORMAddDocumentOptions = {
ids?: string[]
}
export class TypeORMDriver extends VectorStoreDriver {
protected _postgresConnectionOptions: DataSourceOptions
@ -100,45 +95,15 @@ export class TypeORMDriver extends VectorStoreDriver {
try {
instance.appDataSource.getRepository(instance.documentEntity).delete(ids)
} catch (e) {
console.error('Failed to delete', e)
console.error('Failed to delete')
}
}
}
instance.addVectors = async (
vectors: number[][],
documents: Document[],
documentOptions?: TypeORMAddDocumentOptions
): Promise<void> => {
const rows = vectors.map((embedding, idx) => {
const embeddingString = `[${embedding.join(',')}]`
const documentRow = {
id: documentOptions?.ids?.length ? documentOptions.ids[idx] : uuid(),
pageContent: documents[idx].pageContent,
embedding: embeddingString,
metadata: documents[idx].metadata
}
return documentRow
})
const baseAddVectorsFn = instance.addVectors.bind(instance)
const documentRepository = instance.appDataSource.getRepository(instance.documentEntity)
const _batchSize = this.nodeData.inputs?.batchSize
const chunkSize = _batchSize ? parseInt(_batchSize, 10) : 500
for (let i = 0; i < rows.length; i += chunkSize) {
const chunk = rows.slice(i, i + chunkSize)
try {
await documentRepository.save(chunk)
} catch (e) {
console.error(e)
throw new Error(`Error inserting: ${chunk[0].pageContent}`)
}
}
}
instance.addDocuments = async (documents: Document[], options?: { ids?: string[] }): Promise<void> => {
const texts = documents.map(({ pageContent }) => pageContent)
return (instance.addVectors as any)(await this.getEmbeddings().embedDocuments(texts), documents, options)
instance.addVectors = async (vectors, documents) => {
return baseAddVectorsFn(vectors, this.sanitizeDocuments(documents))
}
return instance

View File

@ -6,7 +6,7 @@ import { Document } from '@langchain/core/documents'
import { QdrantVectorStore, QdrantLibArgs } from '@langchain/qdrant'
import { Embeddings } from '@langchain/core/embeddings'
import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams, IndexingResult } from '../../../src/Interface'
import { FLOWISE_CHATID, getBaseClasses, getCredentialData, getCredentialParam, parseJsonBody } from '../../../src/utils'
import { FLOWISE_CHATID, getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
import { index } from '../../../src/indexing'
import { howToUseFileUpload } from '../VectorStoreUtils'
@ -77,8 +77,7 @@ class Qdrant_VectorStores implements INode {
{
label: 'Qdrant Collection Name',
name: 'qdrantCollection',
type: 'string',
acceptVariable: true
type: 'string'
},
{
label: 'File Upload',
@ -385,11 +384,7 @@ class Qdrant_VectorStores implements INode {
const vectorStoreName = collectionName
await recordManager.createSchema()
;(recordManager as any).namespace = (recordManager as any).namespace + '_' + vectorStoreName
const filterKeys: ICommonObject = {}
if (options.docId) {
filterKeys.docId = options.docId
}
const keys: string[] = await recordManager.listKeys(filterKeys)
const keys: string[] = await recordManager.listKeys({})
await vectorStore.delete({ ids: keys })
await recordManager.deleteKeys(keys)
@ -444,7 +439,7 @@ class Qdrant_VectorStores implements INode {
qdrantCollectionConfiguration =
typeof qdrantCollectionConfiguration === 'object'
? qdrantCollectionConfiguration
: parseJsonBody(qdrantCollectionConfiguration)
: JSON.parse(qdrantCollectionConfiguration)
dbConfig.collectionConfig = {
...qdrantCollectionConfiguration,
vectors: {
@ -456,7 +451,7 @@ class Qdrant_VectorStores implements INode {
}
if (queryFilter) {
retrieverConfig.filter = typeof queryFilter === 'object' ? queryFilter : parseJsonBody(queryFilter)
retrieverConfig.filter = typeof queryFilter === 'object' ? queryFilter : JSON.parse(queryFilter)
}
if (isFileUploadEnabled && options.chatId) {
retrieverConfig.filter = retrieverConfig.filter || {}

View File

@ -5,7 +5,7 @@ import { Document } from '@langchain/core/documents'
import { Embeddings } from '@langchain/core/embeddings'
import { SupabaseVectorStore, SupabaseLibArgs } from '@langchain/community/vectorstores/supabase'
import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams, IndexingResult } from '../../../src/Interface'
import { getBaseClasses, getCredentialData, getCredentialParam, parseJsonBody } from '../../../src/utils'
import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
import { addMMRInputParams, resolveVectorStoreOrRetriever } from '../VectorStoreUtils'
import { index } from '../../../src/indexing'
import { FilterParser } from './filterParser'
@ -197,11 +197,7 @@ class Supabase_VectorStores implements INode {
const vectorStoreName = tableName + '_' + queryName
await recordManager.createSchema()
;(recordManager as any).namespace = (recordManager as any).namespace + '_' + vectorStoreName
const filterKeys: ICommonObject = {}
if (options.docId) {
filterKeys.docId = options.docId
}
const keys: string[] = await recordManager.listKeys(filterKeys)
const keys: string[] = await recordManager.listKeys({})
await supabaseStore.delete({ ids: keys })
await recordManager.deleteKeys(keys)
@ -234,8 +230,7 @@ class Supabase_VectorStores implements INode {
}
if (supabaseMetadataFilter) {
const metadatafilter =
typeof supabaseMetadataFilter === 'object' ? supabaseMetadataFilter : parseJsonBody(supabaseMetadataFilter)
const metadatafilter = typeof supabaseMetadataFilter === 'object' ? supabaseMetadataFilter : JSON.parse(supabaseMetadataFilter)
obj.filter = metadatafilter
}

View File

@ -187,11 +187,7 @@ class Upstash_VectorStores implements INode {
const vectorStoreName = UPSTASH_VECTOR_REST_URL
await recordManager.createSchema()
;(recordManager as any).namespace = (recordManager as any).namespace + '_' + vectorStoreName
const filterKeys: ICommonObject = {}
if (options.docId) {
filterKeys.docId = options.docId
}
const keys: string[] = await recordManager.listKeys(filterKeys)
const keys: string[] = await recordManager.listKeys({})
await upstashStore.delete({ ids: keys })
await recordManager.deleteKeys(keys)

View File

@ -4,7 +4,7 @@ import { WeaviateLibArgs, WeaviateStore } from '@langchain/weaviate'
import { Document } from '@langchain/core/documents'
import { Embeddings } from '@langchain/core/embeddings'
import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams, IndexingResult } from '../../../src/Interface'
import { getBaseClasses, getCredentialData, getCredentialParam, normalizeKeysRecursively, parseJsonBody } from '../../../src/utils'
import { getBaseClasses, getCredentialData, getCredentialParam, normalizeKeysRecursively } from '../../../src/utils'
import { addMMRInputParams, resolveVectorStoreOrRetriever } from '../VectorStoreUtils'
import { index } from '../../../src/indexing'
import { VectorStore } from '@langchain/core/vectorstores'
@ -252,11 +252,7 @@ class Weaviate_VectorStores implements INode {
const vectorStoreName = weaviateTextKey ? weaviateIndex + '_' + weaviateTextKey : weaviateIndex
await recordManager.createSchema()
;(recordManager as any).namespace = (recordManager as any).namespace + '_' + vectorStoreName
const filterKeys: ICommonObject = {}
if (options.docId) {
filterKeys.docId = options.docId
}
const keys: string[] = await recordManager.listKeys(filterKeys)
const keys: string[] = await recordManager.listKeys({})
await weaviateStore.delete({ ids: keys })
await recordManager.deleteKeys(keys)
@ -298,7 +294,7 @@ class Weaviate_VectorStores implements INode {
if (weaviateTextKey) obj.textKey = weaviateTextKey
if (weaviateMetadataKeys) obj.metadataKeys = JSON.parse(weaviateMetadataKeys.replace(/\s/g, ''))
if (weaviateFilter) {
weaviateFilter = typeof weaviateFilter === 'object' ? weaviateFilter : parseJsonBody(weaviateFilter)
weaviateFilter = typeof weaviateFilter === 'object' ? weaviateFilter : JSON.parse(weaviateFilter)
}
const vectorStore = (await WeaviateStore.fromExistingIndex(embeddings, obj)) as unknown as VectorStore

View File

@ -4,7 +4,7 @@ import { ZepVectorStore, IZepConfig } from '@langchain/community/vectorstores/ze
import { Embeddings } from '@langchain/core/embeddings'
import { Document } from '@langchain/core/documents'
import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams, IndexingResult } from '../../../src/Interface'
import { getBaseClasses, getCredentialData, getCredentialParam, parseJsonBody } from '../../../src/utils'
import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
import { addMMRInputParams, resolveVectorStoreOrRetriever } from '../VectorStoreUtils'
class Zep_VectorStores implements INode {
@ -159,7 +159,7 @@ class Zep_VectorStores implements INode {
}
if (apiKey) zepConfig.apiKey = apiKey
if (zepMetadataFilter) {
const metadatafilter = typeof zepMetadataFilter === 'object' ? zepMetadataFilter : parseJsonBody(zepMetadataFilter)
const metadatafilter = typeof zepMetadataFilter === 'object' ? zepMetadataFilter : JSON.parse(zepMetadataFilter)
zepConfig.filter = metadatafilter
}

View File

@ -3,7 +3,7 @@ import { ZepClient } from '@getzep/zep-cloud'
import { IZepConfig, ZepVectorStore } from '@getzep/zep-cloud/langchain'
import { Document } from 'langchain/document'
import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams, IndexingResult } from '../../../src/Interface'
import { getBaseClasses, getCredentialData, getCredentialParam, parseJsonBody } from '../../../src/utils'
import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
import { addMMRInputParams, resolveVectorStoreOrRetriever } from '../VectorStoreUtils'
import { FakeEmbeddings } from 'langchain/embeddings/fake'
import { Embeddings } from '@langchain/core/embeddings'
@ -129,7 +129,7 @@ class Zep_CloudVectorStores implements INode {
collectionName: zepCollection
}
if (zepMetadataFilter) {
zepConfig.filter = typeof zepMetadataFilter === 'object' ? zepMetadataFilter : parseJsonBody(zepMetadataFilter)
zepConfig.filter = typeof zepMetadataFilter === 'object' ? zepMetadataFilter : JSON.parse(zepMetadataFilter)
}
zepConfig.client = new ZepClient({
apiKey: apiKey

View File

@ -1,6 +1,6 @@
{
"name": "flowise-components",
"version": "3.0.11",
"version": "3.0.7",
"description": "Flowiseai Components",
"main": "dist/src/index",
"types": "dist/src/index.d.ts",
@ -33,7 +33,6 @@
"@dqbd/tiktoken": "^1.0.21",
"@e2b/code-interpreter": "^1.5.1",
"@elastic/elasticsearch": "^8.9.0",
"@elevenlabs/elevenlabs-js": "^2.8.0",
"@flowiseai/nodevm": "^3.9.25",
"@getzep/zep-cloud": "~1.0.7",
"@getzep/zep-js": "^0.9.0",
@ -42,9 +41,8 @@
"@google-ai/generativelanguage": "^2.5.0",
"@google-cloud/storage": "^7.15.2",
"@google/generative-ai": "^0.24.0",
"@grpc/grpc-js": "^1.10.10",
"@huggingface/inference": "^4.13.2",
"@langchain/anthropic": "0.3.33",
"@huggingface/inference": "^2.6.1",
"@langchain/anthropic": "0.3.29",
"@langchain/aws": "^0.1.11",
"@langchain/baidu-qianfan": "^0.1.0",
"@langchain/cohere": "^0.0.7",
@ -74,20 +72,6 @@
"@modelcontextprotocol/server-slack": "^2025.1.17",
"@notionhq/client": "^2.2.8",
"@opensearch-project/opensearch": "^1.2.0",
"@opentelemetry/api": "1.9.0",
"@opentelemetry/auto-instrumentations-node": "^0.52.0",
"@opentelemetry/core": "1.27.0",
"@opentelemetry/exporter-metrics-otlp-grpc": "0.54.0",
"@opentelemetry/exporter-metrics-otlp-http": "0.54.0",
"@opentelemetry/exporter-metrics-otlp-proto": "0.54.0",
"@opentelemetry/exporter-trace-otlp-grpc": "0.54.0",
"@opentelemetry/exporter-trace-otlp-http": "0.54.0",
"@opentelemetry/exporter-trace-otlp-proto": "0.54.0",
"@opentelemetry/resources": "1.27.0",
"@opentelemetry/sdk-metrics": "1.27.0",
"@opentelemetry/sdk-node": "^0.54.0",
"@opentelemetry/sdk-trace-base": "1.27.0",
"@opentelemetry/semantic-conventions": "1.27.0",
"@pinecone-database/pinecone": "4.0.0",
"@qdrant/js-client-rest": "^1.9.0",
"@stripe/agent-toolkit": "^0.1.20",

View File

@ -442,9 +442,6 @@ export interface IServerSideEventStreamer {
streamAbortEvent(chatId: string): void
streamEndEvent(chatId: string): void
streamUsageMetadataEvent(chatId: string, data: any): void
streamTTSStartEvent(chatId: string, chatMessageId: string, format: string): void
streamTTSDataEvent(chatId: string, chatMessageId: string, audioChunk: string): void
streamTTSEndEvent(chatId: string, chatMessageId: string): void
}
export enum FollowUpPromptProvider {

View File

@ -0,0 +1,167 @@
import { Serializable } from '@langchain/core/load/serializable'
import { NodeFileStore } from 'langchain/stores/file/node'
import { isUnsafeFilePath, isWithinWorkspace } from './validator'
import * as path from 'path'
import * as fs from 'fs'
/**
* Security configuration for file operations
*/
export interface FileSecurityConfig {
/** Base workspace path - all file operations are restricted to this directory */
workspacePath: string
/** Whether to enforce workspace boundaries (default: true) */
enforceWorkspaceBoundaries?: boolean
/** Maximum file size in bytes (default: 10MB) */
maxFileSize?: number
/** Allowed file extensions (if empty, all extensions allowed) */
allowedExtensions?: string[]
/** Blocked file extensions */
blockedExtensions?: string[]
}
/**
* Secure file store that enforces workspace boundaries and validates file operations
*/
export class SecureFileStore extends Serializable {
lc_namespace = ['flowise', 'components', 'stores', 'file']
private config: Required<FileSecurityConfig>
private nodeFileStore: NodeFileStore
constructor(config: FileSecurityConfig) {
super()
// Set default configuration
this.config = {
workspacePath: config.workspacePath,
enforceWorkspaceBoundaries: config.enforceWorkspaceBoundaries ?? true,
maxFileSize: config.maxFileSize ?? 10 * 1024 * 1024, // 10MB default
allowedExtensions: config.allowedExtensions ?? [],
blockedExtensions: config.blockedExtensions ?? [
'.exe',
'.bat',
'.cmd',
'.sh',
'.ps1',
'.vbs',
'.scr',
'.com',
'.pif',
'.dll',
'.sys',
'.msi',
'.jar'
]
}
// Validate workspace path
if (!this.config.workspacePath || !path.isAbsolute(this.config.workspacePath)) {
throw new Error('Workspace path must be an absolute path')
}
// Ensure workspace directory exists
if (!fs.existsSync(this.config.workspacePath)) {
throw new Error(`Workspace directory does not exist: ${this.config.workspacePath}`)
}
// Initialize the underlying NodeFileStore with workspace path
this.nodeFileStore = new NodeFileStore(this.config.workspacePath)
}
/**
* Validates a file path against security policies
*/
private validateFilePath(filePath: string): void {
// Check for unsafe path patterns
if (isUnsafeFilePath(filePath)) {
throw new Error(`Unsafe file path detected: ${filePath}`)
}
// Enforce workspace boundaries if enabled
if (this.config.enforceWorkspaceBoundaries) {
if (!isWithinWorkspace(filePath, this.config.workspacePath)) {
throw new Error(`File path outside workspace boundaries: ${filePath}`)
}
}
// Check file extension
const ext = path.extname(filePath).toLowerCase()
// Check blocked extensions
if (this.config.blockedExtensions.includes(ext)) {
throw new Error(`File extension not allowed: ${ext}`)
}
// Check allowed extensions (if specified)
if (this.config.allowedExtensions.length > 0 && !this.config.allowedExtensions.includes(ext)) {
throw new Error(`File extension not in allowed list: ${ext}`)
}
}
/**
* Validates file size
*/
private validateFileSize(content: string): void {
const sizeInBytes = Buffer.byteLength(content, 'utf8')
if (sizeInBytes > this.config.maxFileSize) {
throw new Error(`File size exceeds maximum allowed size: ${sizeInBytes} > ${this.config.maxFileSize}`)
}
}
/**
* Reads a file with security validation
*/
async readFile(filePath: string): Promise<string> {
this.validateFilePath(filePath)
try {
return await this.nodeFileStore.readFile(filePath)
} catch (error) {
// Provide generic error message to avoid information leakage
throw new Error(`Failed to read file: ${path.basename(filePath)}`)
}
}
/**
* Writes a file with security validation
*/
async writeFile(filePath: string, contents: string): Promise<void> {
this.validateFilePath(filePath)
this.validateFileSize(contents)
try {
// Ensure the directory exists
const dir = path.dirname(path.resolve(this.config.workspacePath, filePath))
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true })
}
await this.nodeFileStore.writeFile(filePath, contents)
} catch (error) {
// Provide generic error message to avoid information leakage
throw new Error(`Failed to write file: ${path.basename(filePath)}`)
}
}
/**
* Gets the workspace configuration
*/
getConfig(): Readonly<Required<FileSecurityConfig>> {
return { ...this.config }
}
/**
* Creates a secure file store with workspace enforcement disabled (for backward compatibility)
* WARNING: This should only be used when absolutely necessary and with proper user consent
*/
static createUnsecure(basePath?: string): SecureFileStore {
const workspacePath = basePath || process.cwd()
return new SecureFileStore({
workspacePath,
enforceWorkspaceBoundaries: false,
maxFileSize: 50 * 1024 * 1024, // 50MB for unsecure mode
blockedExtensions: [] // No extension restrictions in unsecure mode
})
}
}

View File

@ -1021,7 +1021,7 @@ export class JsonOutputToolsParser extends BaseLLMOutputParser<ParsedToolCall[]>
const parsedToolCalls = []
if (!toolCalls) {
// @ts-expect-error name and arguments are defined by Object.defineProperty
// @ts-expect-error name and arguemnts are defined by Object.defineProperty
const parsedToolCall: ParsedToolCall = {
type: 'undefined',
args: {}
@ -1047,7 +1047,7 @@ export class JsonOutputToolsParser extends BaseLLMOutputParser<ParsedToolCall[]>
const clonedToolCalls = JSON.parse(JSON.stringify(toolCalls))
for (const toolCall of clonedToolCalls) {
if (toolCall.function !== undefined) {
// @ts-expect-error name and arguments are defined by Object.defineProperty
// @ts-expect-error name and arguemnts are defined by Object.defineProperty
const parsedToolCall: ParsedToolCall = {
type: toolCall.function.name,
args: JSON.parse(toolCall.function.arguments)

View File

@ -1774,7 +1774,7 @@ export class AnalyticHandler {
}
if (Object.prototype.hasOwnProperty.call(this.handlers, 'lunary')) {
const toolEventId: string = this.handlers['lunary'].toolEvent[returnIds['lunary'].toolEvent]
const toolEventId: string = this.handlers['lunary'].llmEvent[returnIds['lunary'].toolEvent]
const monitor = this.handlers['lunary'].client
if (monitor && toolEventId) {

View File

@ -7,7 +7,6 @@ dotenv.config({ path: envPath, override: true })
export * from './Interface'
export * from './utils'
export * from './speechToText'
export * from './textToSpeech'
export * from './storageUtils'
export * from './handler'
export * from '../evaluation/EvaluationRunner'

View File

@ -8,10 +8,6 @@ import { IndexingResult } from './Interface'
type Metadata = Record<string, unknown>
export interface ExtendedRecordManagerInterface extends RecordManagerInterface {
update(keys: Array<{ uid: string; docId: string }> | string[], updateOptions?: Record<string, any>): Promise<void>
}
type StringOrDocFunc = string | ((doc: DocumentInterface) => string)
export interface HashedDocumentInterface extends DocumentInterface {
@ -211,7 +207,7 @@ export const _isBaseDocumentLoader = (arg: any): arg is BaseDocumentLoader => {
interface IndexArgs {
docsSource: BaseDocumentLoader | DocumentInterface[]
recordManager: ExtendedRecordManagerInterface
recordManager: RecordManagerInterface
vectorStore: VectorStore
options?: IndexOptions
}
@ -279,7 +275,7 @@ export async function index(args: IndexArgs): Promise<IndexingResult> {
const uids: string[] = []
const docsToIndex: DocumentInterface[] = []
const docsToUpdate: Array<{ uid: string; docId: string }> = []
const docsToUpdate: string[] = []
const seenDocs = new Set<string>()
hashedDocs.forEach((hashedDoc, i) => {
const docExists = batchExists[i]
@ -287,7 +283,7 @@ export async function index(args: IndexArgs): Promise<IndexingResult> {
if (forceUpdate) {
seenDocs.add(hashedDoc.uid)
} else {
docsToUpdate.push({ uid: hashedDoc.uid, docId: hashedDoc.metadata.docId as string })
docsToUpdate.push(hashedDoc.uid)
return
}
}
@ -312,7 +308,7 @@ export async function index(args: IndexArgs): Promise<IndexingResult> {
}
await recordManager.update(
hashedDocs.map((doc) => ({ uid: doc.uid, docId: doc.metadata.docId as string })),
hashedDocs.map((doc) => doc.uid),
{ timeAtLeast: indexStartDt, groupIds: sourceIds }
)

View File

@ -1,240 +0,0 @@
import { ICommonObject } from './Interface'
import { getCredentialData } from './utils'
import OpenAI from 'openai'
import { ElevenLabsClient } from '@elevenlabs/elevenlabs-js'
import { Readable } from 'node:stream'
import type { ReadableStream } from 'node:stream/web'
const TextToSpeechType = {
OPENAI_TTS: 'openai',
ELEVEN_LABS_TTS: 'elevenlabs'
}
export const convertTextToSpeechStream = async (
text: string,
textToSpeechConfig: ICommonObject,
options: ICommonObject,
abortController: AbortController,
onStart: (format: string) => void,
onChunk: (chunk: Buffer) => void,
onEnd: () => void
): Promise<void> => {
return new Promise<void>((resolve, reject) => {
let streamDestroyed = false
// Handle abort signal early
if (abortController.signal.aborted) {
reject(new Error('TTS generation aborted'))
return
}
const processStream = async () => {
try {
if (textToSpeechConfig) {
const credentialId = textToSpeechConfig.credentialId as string
const credentialData = await getCredentialData(credentialId ?? '', options)
switch (textToSpeechConfig.name) {
case TextToSpeechType.OPENAI_TTS: {
onStart('mp3')
const openai = new OpenAI({
apiKey: credentialData.openAIApiKey
})
const response = await openai.audio.speech.create(
{
model: 'gpt-4o-mini-tts',
voice: (textToSpeechConfig.voice || 'alloy') as
| 'alloy'
| 'ash'
| 'ballad'
| 'coral'
| 'echo'
| 'fable'
| 'nova'
| 'onyx'
| 'sage'
| 'shimmer',
input: text,
response_format: 'mp3'
},
{
signal: abortController.signal
}
)
const stream = response.body as unknown as Readable
if (!stream) {
throw new Error('Failed to get response stream')
}
await processStreamWithRateLimit(stream, onChunk, onEnd, resolve, reject, 640, 20, abortController, () => {
streamDestroyed = true
})
break
}
case TextToSpeechType.ELEVEN_LABS_TTS: {
onStart('mp3')
const client = new ElevenLabsClient({
apiKey: credentialData.elevenLabsApiKey
})
const response = await client.textToSpeech.stream(
textToSpeechConfig.voice || '21m00Tcm4TlvDq8ikWAM',
{
text: text,
modelId: 'eleven_multilingual_v2'
},
{ abortSignal: abortController.signal }
)
const stream = Readable.fromWeb(response as unknown as ReadableStream)
if (!stream) {
throw new Error('Failed to get response stream')
}
await processStreamWithRateLimit(stream, onChunk, onEnd, resolve, reject, 640, 40, abortController, () => {
streamDestroyed = true
})
break
}
}
} else {
reject(new Error('Text to speech is not selected. Please configure TTS in the chatflow.'))
}
} catch (error) {
reject(error)
}
}
// Handle abort signal
abortController.signal.addEventListener('abort', () => {
if (!streamDestroyed) {
reject(new Error('TTS generation aborted'))
}
})
processStream()
})
}
const processStreamWithRateLimit = async (
stream: Readable,
onChunk: (chunk: Buffer) => void,
onEnd: () => void,
resolve: () => void,
reject: (error: any) => void,
targetChunkSize: number = 640,
rateLimitMs: number = 20,
abortController: AbortController,
onStreamDestroy?: () => void
) => {
const TARGET_CHUNK_SIZE = targetChunkSize
const RATE_LIMIT_MS = rateLimitMs
let buffer: Buffer = Buffer.alloc(0)
let isEnded = false
const processChunks = async () => {
while (!isEnded || buffer.length > 0) {
// Check if aborted
if (abortController.signal.aborted) {
if (!stream.destroyed) {
stream.destroy()
}
onStreamDestroy?.()
reject(new Error('TTS generation aborted'))
return
}
if (buffer.length >= TARGET_CHUNK_SIZE) {
const chunk = buffer.subarray(0, TARGET_CHUNK_SIZE)
buffer = buffer.subarray(TARGET_CHUNK_SIZE)
onChunk(chunk)
await sleep(RATE_LIMIT_MS)
} else if (isEnded && buffer.length > 0) {
onChunk(buffer)
buffer = Buffer.alloc(0)
} else if (!isEnded) {
await sleep(RATE_LIMIT_MS)
} else {
break
}
}
onEnd()
resolve()
}
stream.on('data', (chunk) => {
if (!abortController.signal.aborted) {
buffer = Buffer.concat([buffer, Buffer.from(chunk)])
}
})
stream.on('end', () => {
isEnded = true
})
stream.on('error', (error) => {
reject(error)
})
// Handle abort signal
abortController.signal.addEventListener('abort', () => {
if (!stream.destroyed) {
stream.destroy()
}
onStreamDestroy?.()
reject(new Error('TTS generation aborted'))
})
processChunks().catch(reject)
}
const sleep = (ms: number): Promise<void> => {
return new Promise((resolve) => setTimeout(resolve, ms))
}
export const getVoices = async (provider: string, credentialId: string, options: ICommonObject) => {
const credentialData = await getCredentialData(credentialId ?? '', options)
switch (provider) {
case TextToSpeechType.OPENAI_TTS:
return [
{ id: 'alloy', name: 'Alloy' },
{ id: 'ash', name: 'Ash' },
{ id: 'ballad', name: 'Ballad' },
{ id: 'coral', name: 'Coral' },
{ id: 'echo', name: 'Echo' },
{ id: 'fable', name: 'Fable' },
{ id: 'nova', name: 'Nova' },
{ id: 'onyx', name: 'Onyx' },
{ id: 'sage', name: 'Sage' },
{ id: 'shimmer', name: 'Shimmer' }
]
case TextToSpeechType.ELEVEN_LABS_TTS: {
const client = new ElevenLabsClient({
apiKey: credentialData.elevenLabsApiKey
})
const voices = await client.voices.search({
pageSize: 100,
voiceType: 'default',
category: 'premade'
})
return voices.voices.map((voice) => ({
id: voice.voiceId,
name: voice.name,
category: voice.category
}))
}
default:
throw new Error(`Unsupported TTS provider: ${provider}`)
}
}

Some files were not shown because too many files have changed in this diff Show More