diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 741fdc91c56..767fbd83669 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -82,7 +82,7 @@ jobs: uses: docker/build-push-action@v5.3.0 with: context: . - file: Dockerfile + file: ${{ steps.defaults.outputs.registry == 'docker_hub' && './docker/Dockerfile' || 'Dockerfile' }} build-args: | NODE_VERSION=${{ steps.defaults.outputs.node_version }} platforms: linux/amd64,linux/arm64 diff --git a/docker/.env.example b/docker/.env.example index ecac73bf1c4..7e72923e926 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -89,12 +89,12 @@ BLOB_STORAGE_PATH=/root/.flowise/storage # ALLOW_UNAUTHORIZED_CERTS=false # SENDER_EMAIL=team@example.com -# JWT_AUTH_TOKEN_SECRET='AABBCCDDAABBCCDDAABBCCDDAABBCCDDAABBCCDD' -# JWT_REFRESH_TOKEN_SECRET='AABBCCDDAABBCCDDAABBCCDDAABBCCDDAABBCCDD' -# JWT_ISSUER='ISSUER' -# JWT_AUDIENCE='AUDIENCE' -# JWT_TOKEN_EXPIRY_IN_MINUTES=360 -# JWT_REFRESH_TOKEN_EXPIRY_IN_MINUTES=43200 +JWT_AUTH_TOKEN_SECRET='AABBCCDDAABBCCDDAABBCCDDAABBCCDDAABBCCDD' +JWT_REFRESH_TOKEN_SECRET='AABBCCDDAABBCCDDAABBCCDDAABBCCDDAABBCCDD' +JWT_ISSUER='ISSUER' +JWT_AUDIENCE='AUDIENCE' +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 diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index accc367b9b9..5cc563fc342 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -6,10 +6,8 @@ services: restart: always environment: - PORT=${PORT} - - CORS_ORIGINS=${CORS_ORIGINS} - - IFRAME_ORIGINS=${IFRAME_ORIGINS} - - FLOWISE_FILE_SIZE_LIMIT=${FLOWISE_FILE_SIZE_LIMIT} - - DEBUG=${DEBUG} + + # DATABASE - DATABASE_PATH=${DATABASE_PATH} - DATABASE_TYPE=${DATABASE_TYPE} - DATABASE_PORT=${DATABASE_PORT} @@ -19,32 +17,106 @@ services: - DATABASE_PASSWORD=${DATABASE_PASSWORD} - DATABASE_SSL=${DATABASE_SSL} - DATABASE_SSL_KEY_BASE64=${DATABASE_SSL_KEY_BASE64} + + # SECRET KEYS + - SECRETKEY_STORAGE_TYPE=${SECRETKEY_STORAGE_TYPE} - SECRETKEY_PATH=${SECRETKEY_PATH} - FLOWISE_SECRETKEY_OVERWRITE=${FLOWISE_SECRETKEY_OVERWRITE} - - SECRETKEY_STORAGE_TYPE=${SECRETKEY_STORAGE_TYPE} - SECRETKEY_AWS_ACCESS_KEY=${SECRETKEY_AWS_ACCESS_KEY} - SECRETKEY_AWS_SECRET_KEY=${SECRETKEY_AWS_SECRET_KEY} - SECRETKEY_AWS_REGION=${SECRETKEY_AWS_REGION} - SECRETKEY_AWS_NAME=${SECRETKEY_AWS_NAME} - - LOG_LEVEL=${LOG_LEVEL} + + # LOGGING + - DEBUG=${DEBUG} - LOG_PATH=${LOG_PATH} + - LOG_LEVEL=${LOG_LEVEL} + + # CUSTOM TOOL DEPENDENCIES + - TOOL_FUNCTION_BUILTIN_DEP=${TOOL_FUNCTION_BUILTIN_DEP} + - TOOL_FUNCTION_EXTERNAL_DEP=${TOOL_FUNCTION_EXTERNAL_DEP} + + # STORAGE + - STORAGE_TYPE=${STORAGE_TYPE} - BLOB_STORAGE_PATH=${BLOB_STORAGE_PATH} + - S3_STORAGE_BUCKET_NAME=${S3_STORAGE_BUCKET_NAME} + - S3_STORAGE_ACCESS_KEY_ID=${S3_STORAGE_ACCESS_KEY_ID} + - S3_STORAGE_SECRET_ACCESS_KEY=${S3_STORAGE_SECRET_ACCESS_KEY} + - S3_STORAGE_REGION=${S3_STORAGE_REGION} + - S3_ENDPOINT_URL=${S3_ENDPOINT_URL} + - S3_FORCE_PATH_STYLE=${S3_FORCE_PATH_STYLE} + - GOOGLE_CLOUD_STORAGE_CREDENTIAL=${GOOGLE_CLOUD_STORAGE_CREDENTIAL} + - GOOGLE_CLOUD_STORAGE_PROJ_ID=${GOOGLE_CLOUD_STORAGE_PROJ_ID} + - GOOGLE_CLOUD_STORAGE_BUCKET_NAME=${GOOGLE_CLOUD_STORAGE_BUCKET_NAME} + - GOOGLE_CLOUD_UNIFORM_BUCKET_ACCESS=${GOOGLE_CLOUD_UNIFORM_BUCKET_ACCESS} + + # SETTINGS + - NUMBER_OF_PROXIES=${NUMBER_OF_PROXIES} + - CORS_ORIGINS=${CORS_ORIGINS} + - IFRAME_ORIGINS=${IFRAME_ORIGINS} + - FLOWISE_FILE_SIZE_LIMIT=${FLOWISE_FILE_SIZE_LIMIT} + - SHOW_COMMUNITY_NODES=${SHOW_COMMUNITY_NODES} + - DISABLE_FLOWISE_TELEMETRY=${DISABLE_FLOWISE_TELEMETRY} + - DISABLED_NODES=${DISABLED_NODES} - MODEL_LIST_CONFIG_JSON=${MODEL_LIST_CONFIG_JSON} + + # AUTH PARAMETERS + - APP_URL=${APP_URL} + - JWT_AUTH_TOKEN_SECRET=${JWT_AUTH_TOKEN_SECRET} + - JWT_REFRESH_TOKEN_SECRET=${JWT_REFRESH_TOKEN_SECRET} + - JWT_ISSUER=${JWT_ISSUER} + - JWT_AUDIENCE=${JWT_AUDIENCE} + - JWT_TOKEN_EXPIRY_IN_MINUTES=${JWT_TOKEN_EXPIRY_IN_MINUTES} + - JWT_REFRESH_TOKEN_EXPIRY_IN_MINUTES=${JWT_REFRESH_TOKEN_EXPIRY_IN_MINUTES} + - EXPIRE_AUTH_TOKENS_ON_RESTART=${EXPIRE_AUTH_TOKENS_ON_RESTART} + - EXPRESS_SESSION_SECRET=${EXPRESS_SESSION_SECRET} + - 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} + + # EMAIL + - SMTP_HOST=${SMTP_HOST} + - SMTP_PORT=${SMTP_PORT} + - SMTP_USER=${SMTP_USER} + - SMTP_PASSWORD=${SMTP_PASSWORD} + - SMTP_SECURE=${SMTP_SECURE} + - ALLOW_UNAUTHORIZED_CERTS=${ALLOW_UNAUTHORIZED_CERTS} + - SENDER_EMAIL=${SENDER_EMAIL} + + # ENTERPRISE + - LICENSE_URL=${LICENSE_URL} + - FLOWISE_EE_LICENSE_KEY=${FLOWISE_EE_LICENSE_KEY} + - OFFLINE=${OFFLINE} + - INVITE_TOKEN_EXPIRY_IN_HOURS=${INVITE_TOKEN_EXPIRY_IN_HOURS} + - WORKSPACE_INVITE_TEMPLATE_PATH=${WORKSPACE_INVITE_TEMPLATE_PATH} + + # METRICS COLLECTION + - POSTHOG_PUBLIC_API_KEY=${POSTHOG_PUBLIC_API_KEY} + - ENABLE_METRICS=${ENABLE_METRICS} + - METRICS_PROVIDER=${METRICS_PROVIDER} + - METRICS_INCLUDE_NODE_METRICS=${METRICS_INCLUDE_NODE_METRICS} + - METRICS_SERVICE_NAME=${METRICS_SERVICE_NAME} + - METRICS_OPEN_TELEMETRY_METRIC_ENDPOINT=${METRICS_OPEN_TELEMETRY_METRIC_ENDPOINT} + - METRICS_OPEN_TELEMETRY_PROTOCOL=${METRICS_OPEN_TELEMETRY_PROTOCOL} + - METRICS_OPEN_TELEMETRY_DEBUG=${METRICS_OPEN_TELEMETRY_DEBUG} + + # PROXY - GLOBAL_AGENT_HTTP_PROXY=${GLOBAL_AGENT_HTTP_PROXY} - GLOBAL_AGENT_HTTPS_PROXY=${GLOBAL_AGENT_HTTPS_PROXY} - GLOBAL_AGENT_NO_PROXY=${GLOBAL_AGENT_NO_PROXY} - - DISABLED_NODES=${DISABLED_NODES} + + # QUEUE CONFIGURATION - MODE=${MODE} - - WORKER_CONCURRENCY=${WORKER_CONCURRENCY} - QUEUE_NAME=${QUEUE_NAME} - QUEUE_REDIS_EVENT_STREAM_MAX_LEN=${QUEUE_REDIS_EVENT_STREAM_MAX_LEN} + - WORKER_CONCURRENCY=${WORKER_CONCURRENCY} - REMOVE_ON_AGE=${REMOVE_ON_AGE} - REMOVE_ON_COUNT=${REMOVE_ON_COUNT} - REDIS_URL=${REDIS_URL} - REDIS_HOST=${REDIS_HOST} - REDIS_PORT=${REDIS_PORT} - - REDIS_PASSWORD=${REDIS_PASSWORD} - REDIS_USERNAME=${REDIS_USERNAME} + - REDIS_PASSWORD=${REDIS_PASSWORD} - REDIS_TLS=${REDIS_TLS} - REDIS_CERT=${REDIS_CERT} - REDIS_KEY=${REDIS_KEY} diff --git a/docker/worker/docker-compose.yml b/docker/worker/docker-compose.yml index 6140b84b5b1..a327d9ac40a 100644 --- a/docker/worker/docker-compose.yml +++ b/docker/worker/docker-compose.yml @@ -6,10 +6,8 @@ services: restart: always environment: - PORT=${PORT} - - CORS_ORIGINS=${CORS_ORIGINS} - - IFRAME_ORIGINS=${IFRAME_ORIGINS} - - FLOWISE_FILE_SIZE_LIMIT=${FLOWISE_FILE_SIZE_LIMIT} - - DEBUG=${DEBUG} + + # DATABASE - DATABASE_PATH=${DATABASE_PATH} - DATABASE_TYPE=${DATABASE_TYPE} - DATABASE_PORT=${DATABASE_PORT} @@ -19,27 +17,106 @@ services: - DATABASE_PASSWORD=${DATABASE_PASSWORD} - DATABASE_SSL=${DATABASE_SSL} - DATABASE_SSL_KEY_BASE64=${DATABASE_SSL_KEY_BASE64} + + # SECRET KEYS + - SECRETKEY_STORAGE_TYPE=${SECRETKEY_STORAGE_TYPE} - SECRETKEY_PATH=${SECRETKEY_PATH} - FLOWISE_SECRETKEY_OVERWRITE=${FLOWISE_SECRETKEY_OVERWRITE} - - LOG_LEVEL=${LOG_LEVEL} + - SECRETKEY_AWS_ACCESS_KEY=${SECRETKEY_AWS_ACCESS_KEY} + - SECRETKEY_AWS_SECRET_KEY=${SECRETKEY_AWS_SECRET_KEY} + - SECRETKEY_AWS_REGION=${SECRETKEY_AWS_REGION} + - SECRETKEY_AWS_NAME=${SECRETKEY_AWS_NAME} + + # LOGGING + - DEBUG=${DEBUG} - LOG_PATH=${LOG_PATH} + - LOG_LEVEL=${LOG_LEVEL} + + # CUSTOM TOOL DEPENDENCIES + - TOOL_FUNCTION_BUILTIN_DEP=${TOOL_FUNCTION_BUILTIN_DEP} + - TOOL_FUNCTION_EXTERNAL_DEP=${TOOL_FUNCTION_EXTERNAL_DEP} + + # STORAGE + - STORAGE_TYPE=${STORAGE_TYPE} - BLOB_STORAGE_PATH=${BLOB_STORAGE_PATH} + - S3_STORAGE_BUCKET_NAME=${S3_STORAGE_BUCKET_NAME} + - S3_STORAGE_ACCESS_KEY_ID=${S3_STORAGE_ACCESS_KEY_ID} + - S3_STORAGE_SECRET_ACCESS_KEY=${S3_STORAGE_SECRET_ACCESS_KEY} + - S3_STORAGE_REGION=${S3_STORAGE_REGION} + - S3_ENDPOINT_URL=${S3_ENDPOINT_URL} + - S3_FORCE_PATH_STYLE=${S3_FORCE_PATH_STYLE} + - GOOGLE_CLOUD_STORAGE_CREDENTIAL=${GOOGLE_CLOUD_STORAGE_CREDENTIAL} + - GOOGLE_CLOUD_STORAGE_PROJ_ID=${GOOGLE_CLOUD_STORAGE_PROJ_ID} + - GOOGLE_CLOUD_STORAGE_BUCKET_NAME=${GOOGLE_CLOUD_STORAGE_BUCKET_NAME} + - GOOGLE_CLOUD_UNIFORM_BUCKET_ACCESS=${GOOGLE_CLOUD_UNIFORM_BUCKET_ACCESS} + + # SETTINGS + - NUMBER_OF_PROXIES=${NUMBER_OF_PROXIES} + - CORS_ORIGINS=${CORS_ORIGINS} + - IFRAME_ORIGINS=${IFRAME_ORIGINS} + - FLOWISE_FILE_SIZE_LIMIT=${FLOWISE_FILE_SIZE_LIMIT} + - SHOW_COMMUNITY_NODES=${SHOW_COMMUNITY_NODES} + - DISABLE_FLOWISE_TELEMETRY=${DISABLE_FLOWISE_TELEMETRY} + - DISABLED_NODES=${DISABLED_NODES} - MODEL_LIST_CONFIG_JSON=${MODEL_LIST_CONFIG_JSON} + + # AUTH PARAMETERS + - APP_URL=${APP_URL} + - JWT_AUTH_TOKEN_SECRET=${JWT_AUTH_TOKEN_SECRET} + - JWT_REFRESH_TOKEN_SECRET=${JWT_REFRESH_TOKEN_SECRET} + - JWT_ISSUER=${JWT_ISSUER} + - JWT_AUDIENCE=${JWT_AUDIENCE} + - JWT_TOKEN_EXPIRY_IN_MINUTES=${JWT_TOKEN_EXPIRY_IN_MINUTES} + - JWT_REFRESH_TOKEN_EXPIRY_IN_MINUTES=${JWT_REFRESH_TOKEN_EXPIRY_IN_MINUTES} + - EXPIRE_AUTH_TOKENS_ON_RESTART=${EXPIRE_AUTH_TOKENS_ON_RESTART} + - EXPRESS_SESSION_SECRET=${EXPRESS_SESSION_SECRET} + - 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} + + # EMAIL + - SMTP_HOST=${SMTP_HOST} + - SMTP_PORT=${SMTP_PORT} + - SMTP_USER=${SMTP_USER} + - SMTP_PASSWORD=${SMTP_PASSWORD} + - SMTP_SECURE=${SMTP_SECURE} + - ALLOW_UNAUTHORIZED_CERTS=${ALLOW_UNAUTHORIZED_CERTS} + - SENDER_EMAIL=${SENDER_EMAIL} + + # ENTERPRISE + - LICENSE_URL=${LICENSE_URL} + - FLOWISE_EE_LICENSE_KEY=${FLOWISE_EE_LICENSE_KEY} + - OFFLINE=${OFFLINE} + - INVITE_TOKEN_EXPIRY_IN_HOURS=${INVITE_TOKEN_EXPIRY_IN_HOURS} + - WORKSPACE_INVITE_TEMPLATE_PATH=${WORKSPACE_INVITE_TEMPLATE_PATH} + + # METRICS COLLECTION + - POSTHOG_PUBLIC_API_KEY=${POSTHOG_PUBLIC_API_KEY} + - ENABLE_METRICS=${ENABLE_METRICS} + - METRICS_PROVIDER=${METRICS_PROVIDER} + - METRICS_INCLUDE_NODE_METRICS=${METRICS_INCLUDE_NODE_METRICS} + - METRICS_SERVICE_NAME=${METRICS_SERVICE_NAME} + - METRICS_OPEN_TELEMETRY_METRIC_ENDPOINT=${METRICS_OPEN_TELEMETRY_METRIC_ENDPOINT} + - METRICS_OPEN_TELEMETRY_PROTOCOL=${METRICS_OPEN_TELEMETRY_PROTOCOL} + - METRICS_OPEN_TELEMETRY_DEBUG=${METRICS_OPEN_TELEMETRY_DEBUG} + + # PROXY - GLOBAL_AGENT_HTTP_PROXY=${GLOBAL_AGENT_HTTP_PROXY} - GLOBAL_AGENT_HTTPS_PROXY=${GLOBAL_AGENT_HTTPS_PROXY} - GLOBAL_AGENT_NO_PROXY=${GLOBAL_AGENT_NO_PROXY} - - DISABLED_NODES=${DISABLED_NODES} + + # QUEUE CONFIGURATION - MODE=${MODE} - - WORKER_CONCURRENCY=${WORKER_CONCURRENCY} - QUEUE_NAME=${QUEUE_NAME} - QUEUE_REDIS_EVENT_STREAM_MAX_LEN=${QUEUE_REDIS_EVENT_STREAM_MAX_LEN} + - WORKER_CONCURRENCY=${WORKER_CONCURRENCY} - REMOVE_ON_AGE=${REMOVE_ON_AGE} - REMOVE_ON_COUNT=${REMOVE_ON_COUNT} - REDIS_URL=${REDIS_URL} - REDIS_HOST=${REDIS_HOST} - REDIS_PORT=${REDIS_PORT} - - REDIS_PASSWORD=${REDIS_PASSWORD} - REDIS_USERNAME=${REDIS_USERNAME} + - REDIS_PASSWORD=${REDIS_PASSWORD} - REDIS_TLS=${REDIS_TLS} - REDIS_CERT=${REDIS_CERT} - REDIS_KEY=${REDIS_KEY} diff --git a/package.json b/package.json index f0a83893714..3babec97cdf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "flowise", - "version": "3.0.1", + "version": "3.0.2", "private": true, "homepage": "https://flowiseai.com", "workspaces": [ @@ -20,6 +20,9 @@ "start-worker": "run-script-os", "start-worker:windows": "cd packages/server/bin && run worker", "start-worker:default": "cd packages/server/bin && ./run worker", + "user": "run-script-os", + "user:windows": "cd packages/server/bin && run user", + "user:default": "cd packages/server/bin && ./run user", "test": "turbo run test", "clean": "pnpm --filter \"./packages/**\" clean", "nuke": "pnpm --filter \"./packages/**\" nuke && rimraf node_modules .turbo", diff --git a/packages/components/credentials/GmailOAuth2.credential.ts b/packages/components/credentials/GmailOAuth2.credential.ts new file mode 100644 index 00000000000..38d23a15472 --- /dev/null +++ b/packages/components/credentials/GmailOAuth2.credential.ts @@ -0,0 +1,63 @@ +import { INodeParams, INodeCredential } from '../src/Interface' +const scopes = [ + 'https://www.googleapis.com/auth/gmail.readonly', + 'https://www.googleapis.com/auth/gmail.compose', + 'https://www.googleapis.com/auth/gmail.modify', + 'https://www.googleapis.com/auth/gmail.labels' +] + +class GmailOAuth2 implements INodeCredential { + label: string + name: string + version: number + inputs: INodeParams[] + description: string + + constructor() { + this.label = 'Gmail OAuth2' + this.name = 'gmailOAuth2' + this.version = 1.0 + this.description = + 'You can find the setup instructions here' + this.inputs = [ + { + label: 'Authorization URL', + name: 'authorizationUrl', + type: 'string', + default: 'https://accounts.google.com/o/oauth2/v2/auth' + }, + { + label: 'Access Token URL', + name: 'accessTokenUrl', + type: 'string', + default: 'https://oauth2.googleapis.com/token' + }, + { + label: 'Client ID', + name: 'clientId', + type: 'string' + }, + { + label: 'Client Secret', + name: 'clientSecret', + type: 'password' + }, + { + label: 'Additional Parameters', + name: 'additionalParameters', + type: 'string', + default: 'access_type=offline&prompt=consent', + hidden: true + }, + { + label: 'Scope', + name: 'scope', + type: 'string', + hidden: true, + default: scopes.join(' ') + } + ] + } +} + +module.exports = { credClass: GmailOAuth2 } diff --git a/packages/components/credentials/GoogleCalendarOAuth2.credential.ts b/packages/components/credentials/GoogleCalendarOAuth2.credential.ts new file mode 100644 index 00000000000..5792067a389 --- /dev/null +++ b/packages/components/credentials/GoogleCalendarOAuth2.credential.ts @@ -0,0 +1,58 @@ +import { INodeParams, INodeCredential } from '../src/Interface' +const scopes = ['https://www.googleapis.com/auth/calendar', 'https://www.googleapis.com/auth/calendar.events'] + +class GoogleCalendarOAuth2 implements INodeCredential { + label: string + name: string + version: number + inputs: INodeParams[] + description: string + + constructor() { + this.label = 'Google Calendar OAuth2' + this.name = 'googleCalendarOAuth2' + this.version = 1.0 + this.description = + 'You can find the setup instructions here' + this.inputs = [ + { + label: 'Authorization URL', + name: 'authorizationUrl', + type: 'string', + default: 'https://accounts.google.com/o/oauth2/v2/auth' + }, + { + label: 'Access Token URL', + name: 'accessTokenUrl', + type: 'string', + default: 'https://oauth2.googleapis.com/token' + }, + { + label: 'Client ID', + name: 'clientId', + type: 'string' + }, + { + label: 'Client Secret', + name: 'clientSecret', + type: 'password' + }, + { + label: 'Additional Parameters', + name: 'additionalParameters', + type: 'string', + default: 'access_type=offline&prompt=consent', + hidden: true + }, + { + label: 'Scope', + name: 'scope', + type: 'string', + hidden: true, + default: scopes.join(' ') + } + ] + } +} + +module.exports = { credClass: GoogleCalendarOAuth2 } diff --git a/packages/components/credentials/GoogleDocsOAuth2.credential.ts b/packages/components/credentials/GoogleDocsOAuth2.credential.ts new file mode 100644 index 00000000000..24cb5d6d57d --- /dev/null +++ b/packages/components/credentials/GoogleDocsOAuth2.credential.ts @@ -0,0 +1,62 @@ +import { INodeParams, INodeCredential } from '../src/Interface' +const scopes = [ + 'https://www.googleapis.com/auth/documents', + 'https://www.googleapis.com/auth/drive', + 'https://www.googleapis.com/auth/drive.file' +] + +class GoogleDocsOAuth2 implements INodeCredential { + label: string + name: string + version: number + inputs: INodeParams[] + description: string + + constructor() { + this.label = 'Google Docs OAuth2' + this.name = 'googleDocsOAuth2' + this.version = 1.0 + this.description = + 'You can find the setup instructions here' + this.inputs = [ + { + label: 'Authorization URL', + name: 'authorizationUrl', + type: 'string', + default: 'https://accounts.google.com/o/oauth2/v2/auth' + }, + { + label: 'Access Token URL', + name: 'accessTokenUrl', + type: 'string', + default: 'https://oauth2.googleapis.com/token' + }, + { + label: 'Client ID', + name: 'clientId', + type: 'string' + }, + { + label: 'Client Secret', + name: 'clientSecret', + type: 'password' + }, + { + label: 'Additional Parameters', + name: 'additionalParameters', + type: 'string', + default: 'access_type=offline&prompt=consent', + hidden: true + }, + { + label: 'Scope', + name: 'scope', + type: 'string', + hidden: true, + default: scopes.join(' ') + } + ] + } +} + +module.exports = { credClass: GoogleDocsOAuth2 } diff --git a/packages/components/credentials/GoogleDriveOAuth2.credential.ts b/packages/components/credentials/GoogleDriveOAuth2.credential.ts new file mode 100644 index 00000000000..de027a8e4f8 --- /dev/null +++ b/packages/components/credentials/GoogleDriveOAuth2.credential.ts @@ -0,0 +1,62 @@ +import { INodeParams, INodeCredential } from '../src/Interface' +const scopes = [ + 'https://www.googleapis.com/auth/drive', + 'https://www.googleapis.com/auth/drive.appdata', + 'https://www.googleapis.com/auth/drive.photos.readonly' +] + +class GoogleDriveOAuth2 implements INodeCredential { + label: string + name: string + version: number + inputs: INodeParams[] + description: string + + constructor() { + this.label = 'Google Drive OAuth2' + this.name = 'googleDriveOAuth2' + this.version = 1.0 + this.description = + 'You can find the setup instructions here' + this.inputs = [ + { + label: 'Authorization URL', + name: 'authorizationUrl', + type: 'string', + default: 'https://accounts.google.com/o/oauth2/v2/auth' + }, + { + label: 'Access Token URL', + name: 'accessTokenUrl', + type: 'string', + default: 'https://oauth2.googleapis.com/token' + }, + { + label: 'Client ID', + name: 'clientId', + type: 'string' + }, + { + label: 'Client Secret', + name: 'clientSecret', + type: 'password' + }, + { + label: 'Additional Parameters', + name: 'additionalParameters', + type: 'string', + default: 'access_type=offline&prompt=consent', + hidden: true + }, + { + label: 'Scope', + name: 'scope', + type: 'string', + hidden: true, + default: scopes.join(' ') + } + ] + } +} + +module.exports = { credClass: GoogleDriveOAuth2 } diff --git a/packages/components/credentials/GoogleSheetsOAuth2.credential.ts b/packages/components/credentials/GoogleSheetsOAuth2.credential.ts new file mode 100644 index 00000000000..3e21479224f --- /dev/null +++ b/packages/components/credentials/GoogleSheetsOAuth2.credential.ts @@ -0,0 +1,62 @@ +import { INodeParams, INodeCredential } from '../src/Interface' +const scopes = [ + 'https://www.googleapis.com/auth/drive.file', + 'https://www.googleapis.com/auth/spreadsheets', + 'https://www.googleapis.com/auth/drive.metadata' +] + +class GoogleSheetsOAuth2 implements INodeCredential { + label: string + name: string + version: number + inputs: INodeParams[] + description: string + + constructor() { + this.label = 'Google Sheets OAuth2' + this.name = 'googleSheetsOAuth2' + this.version = 1.0 + this.description = + 'You can find the setup instructions here' + this.inputs = [ + { + label: 'Authorization URL', + name: 'authorizationUrl', + type: 'string', + default: 'https://accounts.google.com/o/oauth2/v2/auth' + }, + { + label: 'Access Token URL', + name: 'accessTokenUrl', + type: 'string', + default: 'https://oauth2.googleapis.com/token' + }, + { + label: 'Client ID', + name: 'clientId', + type: 'string' + }, + { + label: 'Client Secret', + name: 'clientSecret', + type: 'password' + }, + { + label: 'Additional Parameters', + name: 'additionalParameters', + type: 'string', + default: 'access_type=offline&prompt=consent', + hidden: true + }, + { + label: 'Scope', + name: 'scope', + type: 'string', + hidden: true, + default: scopes.join(' ') + } + ] + } +} + +module.exports = { credClass: GoogleSheetsOAuth2 } diff --git a/packages/components/credentials/MicrosoftOutlookOAuth2.credential.ts b/packages/components/credentials/MicrosoftOutlookOAuth2.credential.ts new file mode 100644 index 00000000000..0308969a467 --- /dev/null +++ b/packages/components/credentials/MicrosoftOutlookOAuth2.credential.ts @@ -0,0 +1,66 @@ +import { INodeParams, INodeCredential } from '../src/Interface' + +const scopes = [ + 'openid', + 'offline_access', + 'Contacts.Read', + 'Contacts.ReadWrite', + 'Calendars.Read', + 'Calendars.Read.Shared', + 'Calendars.ReadWrite', + 'Mail.Read', + 'Mail.ReadWrite', + 'Mail.ReadWrite.Shared', + 'Mail.Send', + 'Mail.Send.Shared', + 'MailboxSettings.Read' +] + +class MsoftOutlookOAuth2 implements INodeCredential { + label: string + name: string + version: number + description: string + inputs: INodeParams[] + + constructor() { + this.label = 'Microsoft Outlook OAuth2' + this.name = 'microsoftOutlookOAuth2' + this.version = 1.0 + this.description = + 'You can find the setup instructions here' + this.inputs = [ + { + label: 'Authorization URL', + name: 'authorizationUrl', + type: 'string', + default: 'https://login.microsoftonline.com//oauth2/v2.0/authorize' + }, + { + label: 'Access Token URL', + name: 'accessTokenUrl', + type: 'string', + default: 'https://login.microsoftonline.com//oauth2/v2.0/token' + }, + { + label: 'Client ID', + name: 'clientId', + type: 'string' + }, + { + label: 'Client Secret', + name: 'clientSecret', + type: 'password' + }, + { + label: 'Scope', + name: 'scope', + type: 'string', + hidden: true, + default: scopes.join(' ') + } + ] + } +} + +module.exports = { credClass: MsoftOutlookOAuth2 } diff --git a/packages/components/credentials/MicrosoftTeamsOAuth2.credential.ts b/packages/components/credentials/MicrosoftTeamsOAuth2.credential.ts new file mode 100644 index 00000000000..ffda846ae0c --- /dev/null +++ b/packages/components/credentials/MicrosoftTeamsOAuth2.credential.ts @@ -0,0 +1,87 @@ +import { INodeParams, INodeCredential } from '../src/Interface' + +// Comprehensive scopes for Microsoft Teams operations +const scopes = [ + // Basic authentication + 'openid', + 'offline_access', + + // User permissions + 'User.Read', + 'User.ReadWrite.All', + + // Teams and Groups + 'Group.ReadWrite.All', + 'Team.ReadBasic.All', + 'Team.Create', + 'TeamMember.ReadWrite.All', + + // Channels + 'Channel.ReadBasic.All', + 'Channel.Create', + 'Channel.Delete.All', + 'ChannelMember.ReadWrite.All', + + // Chat operations + 'Chat.ReadWrite', + 'Chat.Create', + 'ChatMember.ReadWrite', + + // Messages + 'ChatMessage.Send', + 'ChatMessage.Read', + 'ChannelMessage.Send', + 'ChannelMessage.Read.All', + + // Reactions and advanced features + 'TeamsActivity.Send' +] + +class MsoftTeamsOAuth2 implements INodeCredential { + label: string + name: string + version: number + inputs: INodeParams[] + description: string + + constructor() { + this.label = 'Microsoft Teams OAuth2' + this.name = 'microsoftTeamsOAuth2' + this.version = 1.0 + this.description = + 'You can find the setup instructions here' + this.inputs = [ + { + label: 'Authorization URL', + name: 'authorizationUrl', + type: 'string', + default: 'https://login.microsoftonline.com//oauth2/v2.0/authorize' + }, + { + label: 'Access Token URL', + name: 'accessTokenUrl', + type: 'string', + default: 'https://login.microsoftonline.com//oauth2/v2.0/token' + }, + { + label: 'Client ID', + name: 'clientId', + type: 'string' + }, + { + label: 'Client Secret', + name: 'clientSecret', + type: 'password' + }, + { + label: 'Scope', + name: 'scope', + type: 'string', + hidden: true, + default: scopes.join(' ') + } + ] + } +} + +module.exports = { credClass: MsoftTeamsOAuth2 } diff --git a/packages/components/evaluation/EvaluationRunner.ts b/packages/components/evaluation/EvaluationRunner.ts index 73f22b70c37..acde7944604 100644 --- a/packages/components/evaluation/EvaluationRunner.ts +++ b/packages/components/evaluation/EvaluationRunner.ts @@ -6,6 +6,26 @@ import { getModelConfigByModelName, MODEL_TYPE } from '../src/modelLoader' export class EvaluationRunner { static metrics = new Map() + + static getCostMetrics = async (selectedProvider: string, selectedModel: string) => { + let modelConfig = await getModelConfigByModelName(MODEL_TYPE.CHAT, selectedProvider, selectedModel) + if (modelConfig) { + if (modelConfig['cost_values']) { + return modelConfig.cost_values + } + return { cost_values: modelConfig } + } else { + modelConfig = await getModelConfigByModelName(MODEL_TYPE.LLM, selectedProvider, selectedModel) + if (modelConfig) { + if (modelConfig['cost_values']) { + return modelConfig.cost_values + } + return { cost_values: modelConfig } + } + } + return undefined + } + static async getAndDeleteMetrics(id: string) { const val = EvaluationRunner.metrics.get(id) if (val) { @@ -34,11 +54,8 @@ export class EvaluationRunner { } } } - let modelConfig = await getModelConfigByModelName(MODEL_TYPE.CHAT, selectedProvider, selectedModel) - if (modelConfig) { - val.push(JSON.stringify({ cost_values: modelConfig })) - } else { - modelConfig = await getModelConfigByModelName(MODEL_TYPE.LLM, selectedProvider, selectedModel) + if (selectedProvider && selectedModel) { + const modelConfig = await EvaluationRunner.getCostMetrics(selectedProvider, selectedModel) if (modelConfig) { val.push(JSON.stringify({ cost_values: modelConfig })) } @@ -116,6 +133,40 @@ export class EvaluationRunner { } try { let response = await axios.post(`${this.baseURL}/api/v1/prediction/${chatflowId}`, postData, axiosConfig) + let agentFlowMetrics: any[] = [] + if (response?.data?.agentFlowExecutedData) { + for (let i = 0; i < response.data.agentFlowExecutedData.length; i++) { + const agentFlowExecutedData = response.data.agentFlowExecutedData[i] + const input_tokens = agentFlowExecutedData?.data?.output?.usageMetadata?.input_tokens || 0 + const output_tokens = agentFlowExecutedData?.data?.output?.usageMetadata?.output_tokens || 0 + const total_tokens = + agentFlowExecutedData?.data?.output?.usageMetadata?.total_tokens || input_tokens + output_tokens + const metrics: any = { + promptTokens: input_tokens, + completionTokens: output_tokens, + totalTokens: total_tokens, + provider: + agentFlowExecutedData.data?.input?.llmModelConfig?.llmModel || + agentFlowExecutedData.data?.input?.agentModelConfig?.agentModel, + model: + agentFlowExecutedData.data?.input?.llmModelConfig?.modelName || + agentFlowExecutedData.data?.input?.agentModelConfig?.modelName, + nodeLabel: agentFlowExecutedData?.nodeLabel, + nodeId: agentFlowExecutedData?.nodeId + } + if (metrics.provider && metrics.model) { + const modelConfig = await EvaluationRunner.getCostMetrics(metrics.provider, metrics.model) + if (modelConfig) { + metrics.cost_values = { + input_cost: (modelConfig.cost_values.input_cost || 0) * (input_tokens / 1000), + output_cost: (modelConfig.cost_values.output_cost || 0) * (output_tokens / 1000) + } + metrics.cost_values.total_cost = metrics.cost_values.input_cost + metrics.cost_values.output_cost + } + } + agentFlowMetrics.push(metrics) + } + } const endTime = performance.now() const timeTaken = (endTime - startTime).toFixed(2) if (response?.data?.metrics) { @@ -130,6 +181,9 @@ export class EvaluationRunner { } ] } + if (agentFlowMetrics.length > 0) { + runData.nested_metrics = agentFlowMetrics + } runData.status = 'complete' let resultText = '' if (response.data.text) resultText = response.data.text diff --git a/packages/components/nodes/agentflow/Agent/Agent.ts b/packages/components/nodes/agentflow/Agent/Agent.ts index 55f565d0e8e..fd227735b04 100644 --- a/packages/components/nodes/agentflow/Agent/Agent.ts +++ b/packages/components/nodes/agentflow/Agent/Agent.ts @@ -15,7 +15,7 @@ import { AnalyticHandler } from '../../../src/handler' import { DEFAULT_SUMMARIZER_TEMPLATE } from '../prompt' import { ILLMMessage } from '../Interface.Agentflow' import { Tool } from '@langchain/core/tools' -import { ARTIFACTS_PREFIX, SOURCE_DOCUMENTS_PREFIX } from '../../../src/agents' +import { ARTIFACTS_PREFIX, SOURCE_DOCUMENTS_PREFIX, TOOL_ARGS_PREFIX } from '../../../src/agents' import { flatten } from 'lodash' import zodToJsonSchema from 'zod-to-json-schema' import { getErrorMessage } from '../../../src/error' @@ -1401,10 +1401,19 @@ class Agent_Agentflow implements INode { return { response, usedTools, sourceDocuments, artifacts, totalTokens, isWaitingForHumanInput: true } } + let toolIds: ICommonObject | undefined + if (options.analyticHandlers) { + toolIds = await options.analyticHandlers.onToolStart(toolCall.name, toolCall.args, options.parentTraceIds) + } + try { //@ts-ignore let toolOutput = await selectedTool.call(toolCall.args, { signal: abortController?.signal }, undefined, flowConfig) + if (options.analyticHandlers && toolIds) { + await options.analyticHandlers.onToolEnd(toolIds, toolOutput) + } + // Extract source documents if present if (typeof toolOutput === 'string' && toolOutput.includes(SOURCE_DOCUMENTS_PREFIX)) { const [output, docs] = toolOutput.split(SOURCE_DOCUMENTS_PREFIX) @@ -1429,6 +1438,17 @@ class Agent_Agentflow implements INode { } } + let toolInput + if (typeof toolOutput === 'string' && toolOutput.includes(TOOL_ARGS_PREFIX)) { + const [output, args] = toolOutput.split(TOOL_ARGS_PREFIX) + toolOutput = output + try { + toolInput = JSON.parse(args) + } catch (e) { + console.error('Error parsing tool input from tool:', e) + } + } + // Add tool message to conversation messages.push({ role: 'tool', @@ -1444,10 +1464,14 @@ class Agent_Agentflow implements INode { // Track used tools usedTools.push({ tool: toolCall.name, - toolInput: toolCall.args, + toolInput: toolInput ?? toolCall.args, toolOutput }) } catch (e) { + if (options.analyticHandlers && toolIds) { + await options.analyticHandlers.onToolEnd(toolIds, e) + } + console.error('Error invoking tool:', e) usedTools.push({ tool: selectedTool.name, @@ -1639,10 +1663,19 @@ class Agent_Agentflow implements INode { toolsInstance = toolsInstance.filter((tool) => tool.name !== toolCall.name) } if (humanInput.type === 'proceed') { + let toolIds: ICommonObject | undefined + if (options.analyticHandlers) { + toolIds = await options.analyticHandlers.onToolStart(toolCall.name, toolCall.args, options.parentTraceIds) + } + try { //@ts-ignore let toolOutput = await selectedTool.call(toolCall.args, { signal: abortController?.signal }, undefined, flowConfig) + if (options.analyticHandlers && toolIds) { + await options.analyticHandlers.onToolEnd(toolIds, toolOutput) + } + // Extract source documents if present if (typeof toolOutput === 'string' && toolOutput.includes(SOURCE_DOCUMENTS_PREFIX)) { const [output, docs] = toolOutput.split(SOURCE_DOCUMENTS_PREFIX) @@ -1667,6 +1700,17 @@ class Agent_Agentflow implements INode { } } + let toolInput + if (typeof toolOutput === 'string' && toolOutput.includes(TOOL_ARGS_PREFIX)) { + const [output, args] = toolOutput.split(TOOL_ARGS_PREFIX) + toolOutput = output + try { + toolInput = JSON.parse(args) + } catch (e) { + console.error('Error parsing tool input from tool:', e) + } + } + // Add tool message to conversation messages.push({ role: 'tool', @@ -1682,10 +1726,14 @@ class Agent_Agentflow implements INode { // Track used tools usedTools.push({ tool: toolCall.name, - toolInput: toolCall.args, + toolInput: toolInput ?? toolCall.args, toolOutput }) } catch (e) { + if (options.analyticHandlers && toolIds) { + await options.analyticHandlers.onToolEnd(toolIds, e) + } + console.error('Error invoking tool:', e) usedTools.push({ tool: selectedTool.name, diff --git a/packages/components/nodes/agentflow/ConditionAgent/ConditionAgent.ts b/packages/components/nodes/agentflow/ConditionAgent/ConditionAgent.ts index 6ec809f9678..b23dd198ff1 100644 --- a/packages/components/nodes/agentflow/ConditionAgent/ConditionAgent.ts +++ b/packages/components/nodes/agentflow/ConditionAgent/ConditionAgent.ts @@ -27,7 +27,7 @@ class ConditionAgent_Agentflow implements INode { constructor() { this.label = 'Condition Agent' this.name = 'conditionAgentAgentflow' - this.version = 1.0 + this.version = 1.1 this.type = 'ConditionAgent' this.category = 'Agent Flows' this.description = `Utilize an agent to split flows based on dynamic conditions` @@ -80,6 +80,26 @@ class ConditionAgent_Agentflow implements INode { scenario: '' } ] + }, + { + label: 'Override System Prompt', + name: 'conditionAgentOverrideSystemPrompt', + type: 'boolean', + description: 'Override initial system prompt for Condition Agent', + optional: true + }, + { + label: 'Node System Prompt', + name: 'conditionAgentSystemPrompt', + type: 'string', + rows: 4, + optional: true, + acceptVariable: true, + default: CONDITION_AGENT_SYSTEM_PROMPT, + description: 'Expert use only. Modifying this can significantly alter agent behavior. Leave default if unsure', + show: { + conditionAgentOverrideSystemPrompt: true + } } /*{ label: 'Enable Memory', @@ -242,6 +262,12 @@ class ConditionAgent_Agentflow implements INode { const conditionAgentInput = nodeData.inputs?.conditionAgentInput as string let input = conditionAgentInput || question const conditionAgentInstructions = nodeData.inputs?.conditionAgentInstructions as string + const conditionAgentSystemPrompt = nodeData.inputs?.conditionAgentSystemPrompt as string + const conditionAgentOverrideSystemPrompt = nodeData.inputs?.conditionAgentOverrideSystemPrompt as boolean + let systemPrompt = CONDITION_AGENT_SYSTEM_PROMPT + if (conditionAgentSystemPrompt && conditionAgentOverrideSystemPrompt) { + systemPrompt = conditionAgentSystemPrompt + } // Extract memory and configuration options const enableMemory = nodeData.inputs?.conditionAgentEnableMemory as boolean @@ -277,31 +303,15 @@ class ConditionAgent_Agentflow implements INode { const messages: BaseMessageLike[] = [ { role: 'system', - content: CONDITION_AGENT_SYSTEM_PROMPT + content: systemPrompt }, { role: 'user', - content: `{"input": "Hello", "scenarios": ["user is asking about AI", "default"], "instruction": "Your task is to check and see if user is asking topic about AI"}` + content: `{"input": "Hello", "scenarios": ["user is asking about AI", "user is not asking about AI"], "instruction": "Your task is to check if the user is asking about AI."}` }, { role: 'assistant', - content: `\`\`\`json\n{"output": "default"}\n\`\`\`` - }, - { - role: 'user', - content: `{"input": "What is AIGC?", "scenarios": ["user is asking about AI", "default"], "instruction": "Your task is to check and see if user is asking topic about AI"}` - }, - { - role: 'assistant', - content: `\`\`\`json\n{"output": "user is asking about AI"}\n\`\`\`` - }, - { - role: 'user', - content: `{"input": "Can you explain deep learning?", "scenarios": ["user is interested in AI topics", "default"], "instruction": "Determine if the user is interested in learning about AI"}` - }, - { - role: 'assistant', - content: `\`\`\`json\n{"output": "user is interested in AI topics"}\n\`\`\`` + content: `\`\`\`json\n{"output": "user is not asking about AI"}\n\`\`\`` } ] // Use to store messages with image file references as we do not want to store the base64 data into database @@ -374,15 +384,19 @@ class ConditionAgent_Agentflow implements INode { ) } - let calledOutputName = 'default' + let calledOutputName: string try { const parsedResponse = this.parseJsonMarkdown(response.content as string) - if (!parsedResponse.output) { - throw new Error('Missing "output" key in response') + if (!parsedResponse.output || typeof parsedResponse.output !== 'string') { + throw new Error('LLM response is missing the "output" key or it is not a string.') } calledOutputName = parsedResponse.output } catch (error) { - console.warn(`Failed to parse LLM response: ${error}. Using default output.`) + throw new Error( + `Failed to parse a valid scenario from the LLM's response. Please check if the model is capable of following JSON output instructions. Raw LLM Response: "${ + response.content as string + }"` + ) } // Clean up empty inputs diff --git a/packages/components/nodes/agentflow/Tool/Tool.ts b/packages/components/nodes/agentflow/Tool/Tool.ts index c3945ff3e36..33947d8138b 100644 --- a/packages/components/nodes/agentflow/Tool/Tool.ts +++ b/packages/components/nodes/agentflow/Tool/Tool.ts @@ -1,7 +1,7 @@ import { ICommonObject, INode, INodeData, INodeOptionsValue, INodeParams, IServerSideEventStreamer } from '../../../src/Interface' import { updateFlowState } from '../utils' import { Tool } from '@langchain/core/tools' -import { ARTIFACTS_PREFIX } from '../../../src/agents' +import { ARTIFACTS_PREFIX, TOOL_ARGS_PREFIX } from '../../../src/agents' import zodToJsonSchema from 'zod-to-json-schema' interface IToolInputArgs { @@ -28,7 +28,7 @@ class Tool_Agentflow implements INode { constructor() { this.label = 'Tool' this.name = 'toolAgentflow' - this.version = 1.0 + this.version = 1.1 this.type = 'Tool' this.category = 'Agent Flows' this.description = 'Tools allow LLM to interact with external systems' @@ -37,7 +37,7 @@ class Tool_Agentflow implements INode { this.inputs = [ { label: 'Tool', - name: 'selectedTool', + name: 'toolAgentflowSelectedTool', type: 'asyncOptions', loadMethod: 'listTools', loadConfig: true @@ -64,7 +64,7 @@ class Tool_Agentflow implements INode { } ], show: { - selectedTool: '.+' + toolAgentflowSelectedTool: '.+' } }, { @@ -124,8 +124,11 @@ class Tool_Agentflow implements INode { }, async listToolInputArgs(nodeData: INodeData, options: ICommonObject): Promise { const currentNode = options.currentNode as ICommonObject - const selectedTool = currentNode?.inputs?.selectedTool as string - const selectedToolConfig = currentNode?.inputs?.selectedToolConfig as ICommonObject + const selectedTool = (currentNode?.inputs?.selectedTool as string) || (currentNode?.inputs?.toolAgentflowSelectedTool as string) + const selectedToolConfig = + (currentNode?.inputs?.selectedToolConfig as ICommonObject) || + (currentNode?.inputs?.toolAgentflowSelectedToolConfig as ICommonObject) || + {} const nodeInstanceFilePath = options.componentNodes[selectedTool].filePath as string @@ -183,8 +186,11 @@ class Tool_Agentflow implements INode { } async run(nodeData: INodeData, input: string, options: ICommonObject): Promise { - const selectedTool = nodeData.inputs?.selectedTool as string - const selectedToolConfig = nodeData.inputs?.selectedToolConfig as ICommonObject + const selectedTool = (nodeData.inputs?.selectedTool as string) || (nodeData.inputs?.toolAgentflowSelectedTool as string) + const selectedToolConfig = + (nodeData?.inputs?.selectedToolConfig as ICommonObject) || + (nodeData?.inputs?.toolAgentflowSelectedToolConfig as ICommonObject) || + {} const toolInputArgs = nodeData.inputs?.toolInputArgs as IToolInputArgs[] const _toolUpdateState = nodeData.inputs?.toolUpdateState @@ -220,6 +226,16 @@ class Tool_Agentflow implements INode { const toolInstance = (await newToolNodeInstance.init(newNodeData, '', options)) as Tool | Tool[] let toolCallArgs: Record = {} + + if (newToolNodeInstance.transformNodeInputsToToolArgs) { + const defaultParams = newToolNodeInstance.transformNodeInputsToToolArgs(newNodeData) + + toolCallArgs = { + ...defaultParams, + ...toolCallArgs + } + } + for (const item of toolInputArgs) { const variableName = item.inputArgName const variableValue = item.inputArgValue @@ -262,6 +278,17 @@ class Tool_Agentflow implements INode { } } + let toolInput + if (typeof toolOutput === 'string' && toolOutput.includes(TOOL_ARGS_PREFIX)) { + const [output, args] = toolOutput.split(TOOL_ARGS_PREFIX) + toolOutput = output + try { + toolInput = JSON.parse(args) + } catch (e) { + console.error('Error parsing tool input from tool:', e) + } + } + if (typeof toolOutput === 'object') { toolOutput = JSON.stringify(toolOutput, null, 2) } @@ -284,7 +311,7 @@ class Tool_Agentflow implements INode { id: nodeData.id, name: this.name, input: { - toolInputArgs: toolInputArgs, + toolInputArgs: toolInput ?? toolInputArgs, selectedTool: selectedTool }, output: { diff --git a/packages/components/nodes/agentflow/prompt.ts b/packages/components/nodes/agentflow/prompt.ts index a5d9cd89357..ee941ae22fd 100644 --- a/packages/components/nodes/agentflow/prompt.ts +++ b/packages/components/nodes/agentflow/prompt.ts @@ -39,37 +39,38 @@ export const DEFAULT_HUMAN_INPUT_DESCRIPTION_HTML = `

Summarize the conversati ` -export const CONDITION_AGENT_SYSTEM_PROMPT = `You are part of a multi-agent system designed to make agent coordination and execution easy. Your task is to analyze the given input and select one matching scenario from a provided set of scenarios. If none of the scenarios match the input, you should return "default." - -- **Input**: A string representing the user's query or message. -- **Scenarios**: A list of predefined scenarios that relate to the input. -- **Instruction**: Determine if the input fits any of the scenarios. - -## Steps - -1. **Read the input string** and the list of scenarios. -2. **Analyze the content of the input** to identify its main topic or intention. -3. **Compare the input with each scenario**: - - If a scenario matches the main topic of the input, select that scenario. - - If no scenarios match, prepare to output "\`\`\`json\n{"output": "default"}\`\`\`" -4. **Output the result**: If a match is found, return the corresponding scenario in JSON; otherwise, return "\`\`\`json\n{"output": "default"}\`\`\`" - -## Output Format - -Output should be a JSON object that either names the matching scenario or returns "\`\`\`json\n{"output": "default"}\`\`\`" if no scenarios match. No explanation is needed. - -## Examples - -1. **Input**: {"input": "Hello", "scenarios": ["user is asking about AI", "default"], "instruction": "Your task is to check and see if user is asking topic about AI"} - **Output**: "\`\`\`json\n{"output": "default"}\`\`\`" - -2. **Input**: {"input": "What is AIGC?", "scenarios": ["user is asking about AI", "default"], "instruction": "Your task is to check and see if user is asking topic about AI"} - **Output**: "\`\`\`json\n{"output": "user is asking about AI"}\`\`\`" - -3. **Input**: {"input": "Can you explain deep learning?", "scenarios": ["user is interested in AI topics", "default"], "instruction": "Determine if the user is interested in learning about AI"} - **Output**: "\`\`\`json\n{"output": "user is interested in AI topics"}\`\`\`" - -## Note -- Ensure that the input scenarios align well with potential user queries for accurate matching -- DO NOT include anything other than the JSON in your response. -` +export const CONDITION_AGENT_SYSTEM_PROMPT = `

You are part of a multi-agent system designed to make agent coordination and execution easy. Your task is to analyze the given input and select one matching scenario from a provided set of scenarios.

+
    +
  • Input: A string representing the user's query, message or data.
  • +
  • Scenarios: A list of predefined scenarios that relate to the input.
  • +
  • Instruction: Determine which of the provided scenarios is the best fit for the input.
  • +
+

Steps

+
    +
  1. Read the input string and the list of scenarios.
  2. +
  3. Analyze the content of the input to identify its main topic or intention.
  4. +
  5. Compare the input with each scenario: Evaluate how well the input's topic or intention aligns with each of the provided scenarios and select the one that is the best fit.
  6. +
  7. Output the result: Return the selected scenario in the specified JSON format.
  8. +
+

Output Format

+

Output should be a JSON object that names the selected scenario, like this: {"output": ""}. No explanation is needed.

+

Examples

+
    +
  1. +

    Input: {"input": "Hello", "scenarios": ["user is asking about AI", "user is not asking about AI"], "instruction": "Your task is to check if the user is asking about AI."}

    +

    Output: {"output": "user is not asking about AI"}

    +
  2. +
  3. +

    Input: {"input": "What is AIGC?", "scenarios": ["user is asking about AI", "user is asking about the weather"], "instruction": "Your task is to check and see if the user is asking a topic about AI."}

    +

    Output: {"output": "user is asking about AI"}

    +
  4. +
  5. +

    Input: {"input": "Can you explain deep learning?", "scenarios": ["user is interested in AI topics", "user wants to order food"], "instruction": "Determine if the user is interested in learning about AI."}

    +

    Output: {"output": "user is interested in AI topics"}

    +
  6. +
+

Note

+
    +
  • Ensure that the input scenarios align well with potential user queries for accurate matching.
  • +
  • DO NOT include anything other than the JSON in your response.
  • +
` diff --git a/packages/components/nodes/documentloaders/API/APILoader.ts b/packages/components/nodes/documentloaders/API/APILoader.ts index 02b77f789ee..5b88e30127f 100644 --- a/packages/components/nodes/documentloaders/API/APILoader.ts +++ b/packages/components/nodes/documentloaders/API/APILoader.ts @@ -1,8 +1,10 @@ -import axios, { AxiosRequestConfig } from 'axios' -import { omit } from 'lodash' import { Document } from '@langchain/core/documents' -import { TextSplitter } from 'langchain/text_splitter' +import axios, { AxiosRequestConfig } from 'axios' +import * as https from 'https' import { BaseDocumentLoader } from 'langchain/document_loaders/base' +import { TextSplitter } from 'langchain/text_splitter' +import { omit } from 'lodash' +import { getFileFromStorage } from '../../../src' import { ICommonObject, IDocument, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' import { handleEscapeCharacters } from '../../../src/utils' @@ -21,7 +23,7 @@ class API_DocumentLoaders implements INode { constructor() { this.label = 'API Loader' this.name = 'apiLoader' - this.version = 2.0 + this.version = 2.1 this.type = 'Document' this.icon = 'api.svg' this.category = 'Document Loaders' @@ -61,6 +63,15 @@ class API_DocumentLoaders implements INode { additionalParams: true, optional: true }, + { + label: 'SSL Certificate', + description: 'Please upload a SSL certificate file in either .pem or .crt', + name: 'caFile', + type: 'file', + fileType: '.pem, .crt', + additionalParams: true, + optional: true + }, { label: 'Body', name: 'body', @@ -105,8 +116,10 @@ class API_DocumentLoaders implements INode { } ] } - async init(nodeData: INodeData): Promise { + + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { const headers = nodeData.inputs?.headers as string + const caFileBase64 = nodeData.inputs?.caFile as string const url = nodeData.inputs?.url as string const body = nodeData.inputs?.body as string const method = nodeData.inputs?.method as string @@ -120,22 +133,37 @@ class API_DocumentLoaders implements INode { omitMetadataKeys = _omitMetadataKeys.split(',').map((key) => key.trim()) } - const options: ApiLoaderParams = { + const apiLoaderParam: ApiLoaderParams = { url, method } if (headers) { const parsedHeaders = typeof headers === 'object' ? headers : JSON.parse(headers) - options.headers = parsedHeaders + apiLoaderParam.headers = parsedHeaders + } + + if (caFileBase64.startsWith('FILE-STORAGE::')) { + let file = caFileBase64.replace('FILE-STORAGE::', '') + file = file.replace('[', '') + file = file.replace(']', '') + const orgId = options.orgId + const chatflowid = options.chatflowid + const fileData = await getFileFromStorage(file, orgId, chatflowid) + apiLoaderParam.ca = fileData.toString() + } else { + const splitDataURI = caFileBase64.split(',') + splitDataURI.pop() + const bf = Buffer.from(splitDataURI.pop() || '', 'base64') + apiLoaderParam.ca = bf.toString('utf-8') } if (body) { const parsedBody = typeof body === 'object' ? body : JSON.parse(body) - options.body = parsedBody + apiLoaderParam.body = parsedBody } - const loader = new ApiLoader(options) + const loader = new ApiLoader(apiLoaderParam) let docs: IDocument[] = [] @@ -195,6 +223,7 @@ interface ApiLoaderParams { method: string headers?: ICommonObject body?: ICommonObject + ca?: string } class ApiLoader extends BaseDocumentLoader { @@ -206,28 +235,36 @@ class ApiLoader extends BaseDocumentLoader { public readonly method: string - constructor({ url, headers, body, method }: ApiLoaderParams) { + public readonly ca?: string + + constructor({ url, headers, body, method, ca }: ApiLoaderParams) { super() this.url = url this.headers = headers this.body = body this.method = method + this.ca = ca } public async load(): Promise { if (this.method === 'POST') { - return this.executePostRequest(this.url, this.headers, this.body) + return this.executePostRequest(this.url, this.headers, this.body, this.ca) } else { - return this.executeGetRequest(this.url, this.headers) + return this.executeGetRequest(this.url, this.headers, this.ca) } } - protected async executeGetRequest(url: string, headers?: ICommonObject): Promise { + protected async executeGetRequest(url: string, headers?: ICommonObject, ca?: string): Promise { try { const config: AxiosRequestConfig = {} if (headers) { config.headers = headers } + if (ca) { + config.httpsAgent = new https.Agent({ + ca: ca + }) + } const response = await axios.get(url, config) const responseJsonString = JSON.stringify(response.data, null, 2) const doc = new Document({ @@ -242,12 +279,17 @@ class ApiLoader extends BaseDocumentLoader { } } - protected async executePostRequest(url: string, headers?: ICommonObject, body?: ICommonObject): Promise { + protected async executePostRequest(url: string, headers?: ICommonObject, body?: ICommonObject, ca?: string): Promise { try { const config: AxiosRequestConfig = {} if (headers) { config.headers = headers } + if (ca) { + config.httpsAgent = new https.Agent({ + ca: ca + }) + } const response = await axios.post(url, body ?? {}, config) const responseJsonString = JSON.stringify(response.data, null, 2) const doc = new Document({ diff --git a/packages/components/nodes/documentloaders/File/File.ts b/packages/components/nodes/documentloaders/File/File.ts index 345a4ccc571..d3049553f1b 100644 --- a/packages/components/nodes/documentloaders/File/File.ts +++ b/packages/components/nodes/documentloaders/File/File.ts @@ -7,6 +7,8 @@ import { CSVLoader } from '@langchain/community/document_loaders/fs/csv' import { PDFLoader } from '@langchain/community/document_loaders/fs/pdf' import { DocxLoader } from '@langchain/community/document_loaders/fs/docx' import { BaseDocumentLoader } from 'langchain/document_loaders/base' +import { LoadOfSheet } from '../MicrosoftExcel/ExcelLoader' +import { PowerpointLoader } from '../MicrosoftPowerpoint/PowerpointLoader' import { Document } from '@langchain/core/documents' import { getFileFromStorage } from '../../../src/storageUtils' import { handleEscapeCharacters, mapMimeTypeToExt } from '../../../src/utils' @@ -213,10 +215,14 @@ class File_DocumentLoaders implements INode { jsonl: (blob) => new JSONLinesLoader(blob, '/' + pointerName.trim()), txt: (blob) => new TextLoader(blob), csv: (blob) => new CSVLoader(blob), - xls: (blob) => new CSVLoader(blob), - xlsx: (blob) => new CSVLoader(blob), + xls: (blob) => new LoadOfSheet(blob), + xlsx: (blob) => new LoadOfSheet(blob), + xlsm: (blob) => new LoadOfSheet(blob), + xlsb: (blob) => new LoadOfSheet(blob), docx: (blob) => new DocxLoader(blob), doc: (blob) => new DocxLoader(blob), + ppt: (blob) => new PowerpointLoader(blob), + pptx: (blob) => new PowerpointLoader(blob), pdf: (blob) => pdfUsage === 'perFile' ? // @ts-ignore diff --git a/packages/components/nodes/documentloaders/FireCrawl/FireCrawl.ts b/packages/components/nodes/documentloaders/FireCrawl/FireCrawl.ts index 2c1778f422b..27bc3c5b669 100644 --- a/packages/components/nodes/documentloaders/FireCrawl/FireCrawl.ts +++ b/packages/components/nodes/documentloaders/FireCrawl/FireCrawl.ts @@ -67,6 +67,29 @@ interface ExtractResponse { data?: Record } +interface SearchResult { + url: string + title: string + description: string +} + +interface SearchResponse { + success: boolean + data?: SearchResult[] + warning?: string +} + +interface SearchRequest { + query: string + limit?: number + tbs?: string + lang?: string + country?: string + location?: string + timeout?: number + ignoreInvalidURLs?: boolean +} + interface Params { [key: string]: any extractorOptions?: { @@ -161,7 +184,11 @@ class FirecrawlApp { } try { - const response: AxiosResponse = await this.postRequest(this.apiUrl + '/v1/scrape', validParams, headers) + const parameters = { + ...validParams, + integration: 'flowise' + } + const response: AxiosResponse = await this.postRequest(this.apiUrl + '/v1/scrape', parameters, headers) if (response.status === 200) { const responseData = response.data if (responseData.success) { @@ -259,7 +286,11 @@ class FirecrawlApp { } try { - const response: AxiosResponse = await this.postRequest(this.apiUrl + '/v1/crawl', validParams, headers) + const parameters = { + ...validParams, + integration: 'flowise' + } + const response: AxiosResponse = await this.postRequest(this.apiUrl + '/v1/crawl', parameters, headers) if (response.status === 200) { const crawlResponse = response.data as CrawlResponse if (!crawlResponse.success) { @@ -367,7 +398,11 @@ class FirecrawlApp { } try { - const response: AxiosResponse = await this.postRequest(this.apiUrl + '/v1/extract', validParams, headers) + const parameters = { + ...validParams, + integration: 'flowise' + } + const response: AxiosResponse = await this.postRequest(this.apiUrl + '/v1/extract', parameters, headers) if (response.status === 200) { const extractResponse = response.data as ExtractResponse if (waitUntilDone) { @@ -384,18 +419,55 @@ class FirecrawlApp { return { success: false, id: '', url: '' } } + async search(request: SearchRequest): Promise { + const headers = this.prepareHeaders() + + // Create a clean payload with only valid parameters + const validParams: any = { + query: request.query + } + + // Add optional parameters if they exist and are not empty + const validSearchParams = ['limit', 'tbs', 'lang', 'country', 'location', 'timeout', 'ignoreInvalidURLs'] as const + + validSearchParams.forEach((param) => { + if (request[param] !== undefined && request[param] !== null) { + validParams[param] = request[param] + } + }) + + try { + const parameters = { + ...validParams, + integration: 'flowise' + } + const response: AxiosResponse = await this.postRequest(this.apiUrl + '/v1/search', parameters, headers) + if (response.status === 200) { + const searchResponse = response.data as SearchResponse + if (!searchResponse.success) { + throw new Error(`Search request failed: ${searchResponse.warning || 'Unknown error'}`) + } + return searchResponse + } else { + this.handleError(response, 'perform search') + } + } catch (error: any) { + throw new Error(error.message) + } + return { success: false } + } + private prepareHeaders(idempotencyKey?: string): AxiosRequestHeaders { return { 'Content-Type': 'application/json', Authorization: `Bearer ${this.apiKey}`, - 'X-Origin': 'flowise', - 'X-Origin-Type': 'integration', ...(idempotencyKey ? { 'x-idempotency-key': idempotencyKey } : {}) - } as AxiosRequestHeaders & { 'X-Origin': string; 'X-Origin-Type': string; 'x-idempotency-key'?: string } + } as AxiosRequestHeaders & { 'x-idempotency-key'?: string } } - private postRequest(url: string, data: Params, headers: AxiosRequestHeaders): Promise { - return axios.post(url, data, { headers }) + private async postRequest(url: string, data: Params, headers: AxiosRequestHeaders): Promise { + const result = await axios.post(url, data, { headers }) + return result } private getRequest(url: string, headers: AxiosRequestHeaders): Promise { @@ -468,29 +540,32 @@ class FirecrawlApp { // FireCrawl Loader interface FirecrawlLoaderParameters { - url: string + url?: string + query?: string apiKey?: string apiUrl?: string - mode?: 'crawl' | 'scrape' | 'extract' + mode?: 'crawl' | 'scrape' | 'extract' | 'search' params?: Record } export class FireCrawlLoader extends BaseDocumentLoader { private apiKey: string private apiUrl: string - private url: string - private mode: 'crawl' | 'scrape' | 'extract' + private url?: string + private query?: string + private mode: 'crawl' | 'scrape' | 'extract' | 'search' private params?: Record constructor(loaderParams: FirecrawlLoaderParameters) { super() - const { apiKey, apiUrl, url, mode = 'crawl', params } = loaderParams + const { apiKey, apiUrl, url, query, mode = 'crawl', params } = loaderParams if (!apiKey) { throw new Error('Firecrawl API key not set. You can set it as FIRECRAWL_API_KEY in your .env file, or pass it to Firecrawl.') } this.apiKey = apiKey this.url = url + this.query = query this.mode = mode this.params = params this.apiUrl = apiUrl || 'https://api.firecrawl.dev' @@ -500,13 +575,37 @@ export class FireCrawlLoader extends BaseDocumentLoader { const app = new FirecrawlApp({ apiKey: this.apiKey, apiUrl: this.apiUrl }) let firecrawlDocs: FirecrawlDocument[] - if (this.mode === 'scrape') { + if (this.mode === 'search') { + if (!this.query) { + throw new Error('Firecrawl: Query is required for search mode') + } + const response = await app.search({ query: this.query, ...this.params }) + if (!response.success) { + throw new Error(`Firecrawl: Failed to search. Warning: ${response.warning}`) + } + + // Convert search results to FirecrawlDocument format + firecrawlDocs = (response.data || []).map((result) => ({ + markdown: result.description, + metadata: { + title: result.title, + sourceURL: result.url, + description: result.description + } + })) + } else if (this.mode === 'scrape') { + if (!this.url) { + throw new Error('Firecrawl: URL is required for scrape mode') + } const response = await app.scrapeUrl(this.url, this.params) if (!response.success) { throw new Error(`Firecrawl: Failed to scrape URL. Error: ${response.error}`) } firecrawlDocs = [response.data as FirecrawlDocument] } else if (this.mode === 'crawl') { + if (!this.url) { + throw new Error('Firecrawl: URL is required for crawl mode') + } const response = await app.crawlUrl(this.url, this.params) if ('status' in response) { if (response.status === 'failed') { @@ -520,6 +619,9 @@ export class FireCrawlLoader extends BaseDocumentLoader { firecrawlDocs = [response.data as FirecrawlDocument] } } else if (this.mode === 'extract') { + if (!this.url) { + throw new Error('Firecrawl: URL is required for extract mode') + } this.params!.urls = [this.url] const response = await app.extract(this.params as any as ExtractRequest) if (!response.success) { @@ -557,7 +659,7 @@ export class FireCrawlLoader extends BaseDocumentLoader { } return [] } else { - throw new Error(`Unrecognized mode '${this.mode}'. Expected one of 'crawl', 'scrape', 'extract'.`) + throw new Error(`Unrecognized mode '${this.mode}'. Expected one of 'crawl', 'scrape', 'extract', 'search'.`) } // Convert Firecrawl documents to LangChain documents @@ -602,7 +704,7 @@ class FireCrawl_DocumentLoaders implements INode { this.name = 'fireCrawl' this.type = 'Document' this.icon = 'firecrawl.png' - this.version = 3.0 + this.version = 4.0 this.category = 'Document Loaders' this.description = 'Load data from URL using FireCrawl' this.baseClasses = [this.type] @@ -620,14 +722,7 @@ class FireCrawl_DocumentLoaders implements INode { optional: true }, { - label: 'URLs', - name: 'url', - type: 'string', - description: 'URL to be crawled/scraped/extracted', - placeholder: 'https://docs.flowiseai.com' - }, - { - label: 'Crawler type', + label: 'Type', type: 'options', name: 'crawlerType', options: [ @@ -645,89 +740,179 @@ class FireCrawl_DocumentLoaders implements INode { label: 'Extract', name: 'extract', description: 'Extract data from a URL' + }, + { + label: 'Search', + name: 'search', + description: 'Search the web using FireCrawl' } ], default: 'crawl' }, + { + label: 'URLs', + name: 'url', + type: 'string', + description: 'URL to be crawled/scraped/extracted', + placeholder: 'https://docs.flowiseai.com', + optional: true, + show: { + crawlerType: ['crawl', 'scrape', 'extract'] + } + }, { // includeTags - label: '[Scrape] Include Tags', + label: 'Include Tags', name: 'includeTags', type: 'string', description: 'Tags to include in the output. Use comma to separate multiple tags.', optional: true, - additionalParams: true + additionalParams: true, + show: { + crawlerType: ['scrape'] + } }, { // excludeTags - label: '[Scrape] Exclude Tags', + label: 'Exclude Tags', name: 'excludeTags', type: 'string', description: 'Tags to exclude from the output. Use comma to separate multiple tags.', optional: true, - additionalParams: true + additionalParams: true, + show: { + crawlerType: ['scrape'] + } }, { // onlyMainContent - label: '[Scrape] Only Main Content', + label: 'Only Main Content', name: 'onlyMainContent', type: 'boolean', description: 'Extract only the main content of the page', optional: true, - additionalParams: true + additionalParams: true, + show: { + crawlerType: ['scrape'] + } }, { // limit - label: '[Crawl] Limit', + label: 'Limit', name: 'limit', type: 'string', description: 'Maximum number of pages to crawl', optional: true, additionalParams: true, - default: '10000' + default: '10000', + show: { + crawlerType: ['crawl'] + } }, { - label: '[Crawl] Include Paths', + label: 'Include Paths', name: 'includePaths', type: 'string', description: 'URL pathname regex patterns that include matching URLs in the crawl. Only the paths that match the specified patterns will be included in the response.', placeholder: `blog/.*, news/.*`, optional: true, - additionalParams: true + additionalParams: true, + show: { + crawlerType: ['crawl'] + } }, { - label: '[Crawl] Exclude Paths', + label: 'Exclude Paths', name: 'excludePaths', type: 'string', description: 'URL pathname regex patterns that exclude matching URLs from the crawl.', placeholder: `blog/.*, news/.*`, optional: true, - additionalParams: true + additionalParams: true, + show: { + crawlerType: ['crawl'] + } }, { - label: '[Extract] Schema', + label: 'Schema', name: 'extractSchema', type: 'json', description: 'JSON schema for data extraction', optional: true, - additionalParams: true + additionalParams: true, + show: { + crawlerType: ['extract'] + } }, { - label: '[Extract] Prompt', + label: 'Prompt', name: 'extractPrompt', type: 'string', description: 'Prompt for data extraction', optional: true, - additionalParams: true + additionalParams: true, + show: { + crawlerType: ['extract'] + } }, { - label: '[Extract] Job ID', - name: 'extractJobId', + label: 'Query', + name: 'searchQuery', type: 'string', - description: 'ID of the extract job', + description: 'Search query to find relevant content', optional: true, - additionalParams: true + show: { + crawlerType: ['search'] + } + }, + { + label: 'Limit', + name: 'searchLimit', + type: 'string', + description: 'Maximum number of results to return', + optional: true, + additionalParams: true, + default: '5', + show: { + crawlerType: ['search'] + } + }, + { + label: 'Language', + name: 'searchLang', + type: 'string', + description: 'Language code for search results (e.g., en, es, fr)', + optional: true, + additionalParams: true, + default: 'en', + show: { + crawlerType: ['search'] + } + }, + { + label: 'Country', + name: 'searchCountry', + type: 'string', + description: 'Country code for search results (e.g., us, uk, ca)', + optional: true, + additionalParams: true, + default: 'us', + show: { + crawlerType: ['search'] + } + }, + { + label: 'Timeout', + name: 'searchTimeout', + type: 'number', + description: 'Timeout in milliseconds for search operation', + optional: true, + additionalParams: true, + default: 60000, + show: { + crawlerType: ['search'] + } } ] this.outputs = [ @@ -758,6 +943,11 @@ class FireCrawl_DocumentLoaders implements INode { const firecrawlApiUrl = getCredentialParam('firecrawlApiUrl', credentialData, nodeData, 'https://api.firecrawl.dev') const output = nodeData.outputs?.output as string + // Validate URL only for non-search methods + if (crawlerType !== 'search' && !url) { + throw new Error('Firecrawl: URL is required for ' + crawlerType + ' mode') + } + const includePaths = nodeData.inputs?.includePaths ? (nodeData.inputs.includePaths.split(',') as string[]) : undefined const excludePaths = nodeData.inputs?.excludePaths ? (nodeData.inputs.excludePaths.split(',') as string[]) : undefined @@ -767,9 +957,16 @@ class FireCrawl_DocumentLoaders implements INode { const extractSchema = nodeData.inputs?.extractSchema const extractPrompt = nodeData.inputs?.extractPrompt as string + const searchQuery = nodeData.inputs?.searchQuery as string + const searchLimit = nodeData.inputs?.searchLimit as string + const searchLang = nodeData.inputs?.searchLang as string + const searchCountry = nodeData.inputs?.searchCountry as string + const searchTimeout = nodeData.inputs?.searchTimeout as number + const input: FirecrawlLoaderParameters = { url, - mode: crawlerType as 'crawl' | 'scrape' | 'extract', + query: searchQuery, + mode: crawlerType as 'crawl' | 'scrape' | 'extract' | 'search', apiKey: firecrawlApiToken, apiUrl: firecrawlApiUrl, params: { @@ -785,6 +982,19 @@ class FireCrawl_DocumentLoaders implements INode { } } + // Add search-specific parameters only when in search mode + if (crawlerType === 'search') { + if (!searchQuery) { + throw new Error('Firecrawl: Search query is required for search mode') + } + input.params = { + limit: searchLimit ? parseInt(searchLimit, 10) : 5, + lang: searchLang, + country: searchCountry, + timeout: searchTimeout + } + } + if (onlyMainContent === true) { const scrapeOptions = input.params?.scrapeOptions as any input.params!.scrapeOptions = { diff --git a/packages/components/nodes/documentloaders/Folder/Folder.ts b/packages/components/nodes/documentloaders/Folder/Folder.ts index 1a6afe05718..d567f64aaf7 100644 --- a/packages/components/nodes/documentloaders/Folder/Folder.ts +++ b/packages/components/nodes/documentloaders/Folder/Folder.ts @@ -7,6 +7,8 @@ import { JSONLinesLoader, JSONLoader } from 'langchain/document_loaders/fs/json' import { CSVLoader } from '@langchain/community/document_loaders/fs/csv' import { PDFLoader } from '@langchain/community/document_loaders/fs/pdf' import { DocxLoader } from '@langchain/community/document_loaders/fs/docx' +import { LoadOfSheet } from '../MicrosoftExcel/ExcelLoader' +import { PowerpointLoader } from '../MicrosoftPowerpoint/PowerpointLoader' import { handleEscapeCharacters } from '../../../src/utils' class Folder_DocumentLoaders implements INode { @@ -135,10 +137,14 @@ class Folder_DocumentLoaders implements INode { '.jsonl': (blob) => new JSONLinesLoader(blob, '/' + pointerName.trim()), '.txt': (path) => new TextLoader(path), '.csv': (path) => new CSVLoader(path), - '.xls': (path) => new CSVLoader(path), - '.xlsx': (path) => new CSVLoader(path), + '.xls': (path) => new LoadOfSheet(path), + '.xlsx': (path) => new LoadOfSheet(path), + '.xlsm': (path) => new LoadOfSheet(path), + '.xlsb': (path) => new LoadOfSheet(path), '.doc': (path) => new DocxLoader(path), '.docx': (path) => new DocxLoader(path), + '.ppt': (path) => new PowerpointLoader(path), + '.pptx': (path) => new PowerpointLoader(path), '.pdf': (path) => pdfUsage === 'perFile' ? // @ts-ignore diff --git a/packages/components/nodes/documentloaders/GoogleDrive/GoogleDrive.ts b/packages/components/nodes/documentloaders/GoogleDrive/GoogleDrive.ts new file mode 100644 index 00000000000..441eb61a326 --- /dev/null +++ b/packages/components/nodes/documentloaders/GoogleDrive/GoogleDrive.ts @@ -0,0 +1,828 @@ +import { omit } from 'lodash' +import { ICommonObject, IDocument, INode, INodeData, INodeParams, INodeOptionsValue } from '../../../src/Interface' +import { TextSplitter } from 'langchain/text_splitter' +import { + convertMultiOptionsToStringArray, + getCredentialData, + getCredentialParam, + handleEscapeCharacters, + INodeOutputsValue, + refreshOAuth2Token +} from '../../../src' +import { PDFLoader } from '@langchain/community/document_loaders/fs/pdf' +import { DocxLoader } from '@langchain/community/document_loaders/fs/docx' +import { CSVLoader } from '@langchain/community/document_loaders/fs/csv' +import * as fs from 'fs' +import * as path from 'path' +import * as os from 'os' +import { LoadOfSheet } from '../MicrosoftExcel/ExcelLoader' +import { PowerpointLoader } from '../MicrosoftPowerpoint/PowerpointLoader' + +// Helper function to get human-readable MIME type labels +const getMimeTypeLabel = (mimeType: string): string | undefined => { + const mimeTypeLabels: { [key: string]: string } = { + 'application/vnd.google-apps.document': 'Google Doc', + 'application/vnd.google-apps.spreadsheet': 'Google Sheet', + 'application/vnd.google-apps.presentation': 'Google Slides', + 'application/pdf': 'PDF', + 'text/plain': 'Text File', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'Word Doc', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'PowerPoint', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'Excel File' + } + return mimeTypeLabels[mimeType] || undefined +} + +class GoogleDrive_DocumentLoaders implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + baseClasses: string[] + credential: INodeParams + inputs: INodeParams[] + outputs: INodeOutputsValue[] + + constructor() { + this.label = 'Google Drive' + this.name = 'googleDrive' + this.version = 1.0 + this.type = 'Document' + this.icon = 'google-drive.svg' + this.category = 'Document Loaders' + this.description = `Load documents from Google Drive files` + this.baseClasses = [this.type] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + description: 'Google Drive OAuth2 Credential', + credentialNames: ['googleDriveOAuth2'] + } + this.inputs = [ + { + label: 'Select Files', + name: 'selectedFiles', + type: 'asyncMultiOptions', + loadMethod: 'listFiles', + description: 'Select files from your Google Drive', + refresh: true + }, + { + label: 'Folder ID', + name: 'folderId', + type: 'string', + description: 'Google Drive folder ID to load all files from (alternative to selecting specific files)', + placeholder: '1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms', + optional: true + }, + { + label: 'File Types', + name: 'fileTypes', + type: 'multiOptions', + description: 'Types of files to load', + options: [ + { + label: 'Google Docs', + name: 'application/vnd.google-apps.document' + }, + { + label: 'Google Sheets', + name: 'application/vnd.google-apps.spreadsheet' + }, + { + label: 'Google Slides', + name: 'application/vnd.google-apps.presentation' + }, + { + label: 'PDF Files', + name: 'application/pdf' + }, + { + label: 'Text Files', + name: 'text/plain' + }, + { + label: 'Word Documents', + name: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' + }, + { + label: 'PowerPoint', + name: 'application/vnd.openxmlformats-officedocument.presentationml.presentation' + }, + { + label: 'Excel Files', + name: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + } + ], + default: [ + 'application/vnd.google-apps.document', + 'application/vnd.google-apps.spreadsheet', + 'application/vnd.google-apps.presentation', + 'text/plain', + 'application/pdf', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + ], + optional: true + }, + { + label: 'Include Subfolders', + name: 'includeSubfolders', + type: 'boolean', + description: 'Whether to include files from subfolders when loading from a folder', + default: false, + optional: true + }, + { + label: 'Include Shared Drives', + name: 'includeSharedDrives', + type: 'boolean', + description: 'Whether to include files from shared drives (Team Drives) that you have access to', + default: false, + optional: true + }, + { + label: 'Max Files', + name: 'maxFiles', + type: 'number', + description: 'Maximum number of files to load (default: 50)', + default: 50, + optional: true + }, + { + label: 'Text Splitter', + name: 'textSplitter', + type: 'TextSplitter', + optional: true + }, + { + label: 'Additional Metadata', + name: 'metadata', + type: 'json', + description: 'Additional metadata to be added to the extracted documents', + optional: true, + additionalParams: true + }, + { + label: 'Omit Metadata Keys', + name: 'omitMetadataKeys', + type: 'string', + rows: 4, + description: + 'Each document loader comes with a default set of metadata keys that are extracted from the document. You can use this field to omit some of the default metadata keys. The value should be a list of keys, seperated by comma. Use * to omit all metadata keys execept the ones you specify in the Additional Metadata field', + placeholder: 'key1, key2, key3.nestedKey1', + optional: true, + additionalParams: true + } + ] + this.outputs = [ + { + label: 'Document', + name: 'document', + description: 'Array of document objects containing metadata and pageContent', + baseClasses: [...this.baseClasses, 'json'] + }, + { + label: 'Text', + name: 'text', + description: 'Concatenated string from pageContent of documents', + baseClasses: ['string', 'json'] + } + ] + } + + //@ts-ignore + loadMethods = { + async listFiles(nodeData: INodeData, options: ICommonObject): Promise { + const returnData: INodeOptionsValue[] = [] + + try { + let credentialData = await getCredentialData(nodeData.credential ?? '', options) + credentialData = await refreshOAuth2Token(nodeData.credential ?? '', credentialData, options) + const accessToken = getCredentialParam('access_token', credentialData, nodeData) + + if (!accessToken) { + return returnData + } + + // Get file types from input to filter + const fileTypes = convertMultiOptionsToStringArray(nodeData.inputs?.fileTypes) + const includeSharedDrives = nodeData.inputs?.includeSharedDrives as boolean + const maxFiles = (nodeData.inputs?.maxFiles as number) || 100 + + let query = 'trashed = false' + + // Add file type filter if specified + if (fileTypes && fileTypes.length > 0) { + const mimeTypeQuery = fileTypes.map((type) => `mimeType='${type}'`).join(' or ') + query += ` and (${mimeTypeQuery})` + } + + const url = new URL('https://www.googleapis.com/drive/v3/files') + url.searchParams.append('q', query) + url.searchParams.append('pageSize', Math.min(maxFiles, 1000).toString()) + url.searchParams.append('fields', 'files(id, name, mimeType, size, createdTime, modifiedTime, webViewLink, driveId)') + url.searchParams.append('orderBy', 'modifiedTime desc') + + // Add shared drives support if requested + if (includeSharedDrives) { + url.searchParams.append('supportsAllDrives', 'true') + url.searchParams.append('includeItemsFromAllDrives', 'true') + } + + const response = await fetch(url.toString(), { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json' + } + }) + + if (!response.ok) { + console.error(`Failed to list files: ${response.statusText}`) + return returnData + } + + const data = await response.json() + + for (const file of data.files) { + const mimeTypeLabel = getMimeTypeLabel(file.mimeType) + if (!mimeTypeLabel) { + continue + } + + // Add drive context to description + const driveContext = file.driveId ? ' (Shared Drive)' : ' (My Drive)' + + const obj: INodeOptionsValue = { + name: file.id, + label: file.name, + description: `Type: ${mimeTypeLabel}${driveContext} | Modified: ${new Date(file.modifiedTime).toLocaleDateString()}` + } + returnData.push(obj) + } + } catch (error) { + console.error('Error listing Google Drive files:', error) + } + + return returnData + } + } + + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + const selectedFiles = nodeData.inputs?.selectedFiles as string + const folderId = nodeData.inputs?.folderId as string + const fileTypes = nodeData.inputs?.fileTypes as string[] + const includeSubfolders = nodeData.inputs?.includeSubfolders as boolean + const includeSharedDrives = nodeData.inputs?.includeSharedDrives as boolean + const maxFiles = (nodeData.inputs?.maxFiles as number) || 50 + const textSplitter = nodeData.inputs?.textSplitter as TextSplitter + const metadata = nodeData.inputs?.metadata + const _omitMetadataKeys = nodeData.inputs?.omitMetadataKeys as string + const output = nodeData.outputs?.output as string + + let omitMetadataKeys: string[] = [] + if (_omitMetadataKeys) { + omitMetadataKeys = _omitMetadataKeys.split(',').map((key) => key.trim()) + } + + if (!selectedFiles && !folderId) { + throw new Error('Either selected files or Folder ID is required') + } + + let credentialData = await getCredentialData(nodeData.credential ?? '', options) + credentialData = await refreshOAuth2Token(nodeData.credential ?? '', credentialData, options) + const accessToken = getCredentialParam('access_token', credentialData, nodeData) + + if (!accessToken) { + throw new Error('No access token found in credential') + } + + let docs: IDocument[] = [] + + try { + let filesToProcess: any[] = [] + + if (selectedFiles) { + // Load selected files (selectedFiles can be a single ID or comma-separated IDs) + let ids: string[] = [] + if (typeof selectedFiles === 'string' && selectedFiles.startsWith('[') && selectedFiles.endsWith(']')) { + ids = convertMultiOptionsToStringArray(selectedFiles) + } else if (typeof selectedFiles === 'string') { + ids = [selectedFiles] + } else if (Array.isArray(selectedFiles)) { + ids = selectedFiles + } + for (const id of ids) { + const fileInfo = await this.getFileInfo(id, accessToken, includeSharedDrives) + if (fileInfo && this.shouldProcessFile(fileInfo, fileTypes)) { + filesToProcess.push(fileInfo) + } + } + } else if (folderId) { + // Load files from folder + filesToProcess = await this.getFilesFromFolder( + folderId, + accessToken, + fileTypes, + includeSubfolders, + includeSharedDrives, + maxFiles + ) + } + + // Process each file + for (const fileInfo of filesToProcess) { + try { + const doc = await this.processFile(fileInfo, accessToken) + if (doc.length > 0) { + docs.push(...doc) + } + } catch (error) { + console.warn(`Failed to process file ${fileInfo.name}: ${error.message}`) + } + } + + // Apply text splitter if provided + if (textSplitter && docs.length > 0) { + docs = await textSplitter.splitDocuments(docs) + } + + // Apply metadata transformations + if (metadata) { + const parsedMetadata = typeof metadata === 'object' ? metadata : JSON.parse(metadata) + docs = docs.map((doc) => ({ + ...doc, + metadata: + _omitMetadataKeys === '*' + ? { + ...parsedMetadata + } + : omit( + { + ...doc.metadata, + ...parsedMetadata + }, + omitMetadataKeys + ) + })) + } else { + docs = docs.map((doc) => ({ + ...doc, + metadata: + _omitMetadataKeys === '*' + ? {} + : omit( + { + ...doc.metadata + }, + omitMetadataKeys + ) + })) + } + } catch (error) { + throw new Error(`Failed to load Google Drive documents: ${error.message}`) + } + + if (output === 'document') { + return docs + } else { + let finaltext = '' + for (const doc of docs) { + finaltext += `${doc.pageContent}\n` + } + return handleEscapeCharacters(finaltext, false) + } + } + + private async getFileInfo(fileId: string, accessToken: string, includeSharedDrives: boolean): Promise { + const url = new URL(`https://www.googleapis.com/drive/v3/files/${encodeURIComponent(fileId)}`) + url.searchParams.append('fields', 'id, name, mimeType, size, createdTime, modifiedTime, parents, webViewLink, driveId') + + // Add shared drives support if requested + if (includeSharedDrives) { + url.searchParams.append('supportsAllDrives', 'true') + } + + const response = await fetch(url.toString(), { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json' + } + }) + + if (!response.ok) { + throw new Error(`Failed to get file info: ${response.statusText}`) + } + + const fileInfo = await response.json() + + // Add drive context to description + const driveContext = fileInfo.driveId ? ' (Shared Drive)' : ' (My Drive)' + + return { + ...fileInfo, + driveContext + } + } + + private async getFilesFromFolder( + folderId: string, + accessToken: string, + fileTypes: string[] | undefined, + includeSubfolders: boolean, + includeSharedDrives: boolean, + maxFiles: number + ): Promise { + const files: any[] = [] + let nextPageToken: string | undefined + + do { + let query = `'${folderId}' in parents and trashed = false` + + // Add file type filter if specified + if (fileTypes && fileTypes.length > 0) { + const mimeTypeQuery = fileTypes.map((type) => `mimeType='${type}'`).join(' or ') + query += ` and (${mimeTypeQuery})` + } + + const url = new URL('https://www.googleapis.com/drive/v3/files') + url.searchParams.append('q', query) + url.searchParams.append('pageSize', Math.min(maxFiles - files.length, 1000).toString()) + url.searchParams.append( + 'fields', + 'nextPageToken, files(id, name, mimeType, size, createdTime, modifiedTime, parents, webViewLink, driveId)' + ) + + // Add shared drives support if requested + if (includeSharedDrives) { + url.searchParams.append('supportsAllDrives', 'true') + url.searchParams.append('includeItemsFromAllDrives', 'true') + } + + if (nextPageToken) { + url.searchParams.append('pageToken', nextPageToken) + } + + const response = await fetch(url.toString(), { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json' + } + }) + + if (!response.ok) { + throw new Error(`Failed to list files: ${response.statusText}`) + } + + const data = await response.json() + + // Add drive context to each file + const filesWithContext = data.files.map((file: any) => ({ + ...file, + driveContext: file.driveId ? ' (Shared Drive)' : ' (My Drive)' + })) + + files.push(...filesWithContext) + nextPageToken = data.nextPageToken + + // If includeSubfolders is true, also get files from subfolders + if (includeSubfolders) { + for (const file of data.files) { + if (file.mimeType === 'application/vnd.google-apps.folder') { + const subfolderFiles = await this.getFilesFromFolder( + file.id, + accessToken, + fileTypes, + includeSubfolders, + includeSharedDrives, + maxFiles - files.length + ) + files.push(...subfolderFiles) + } + } + } + } while (nextPageToken && files.length < maxFiles) + + return files.slice(0, maxFiles) + } + + private shouldProcessFile(fileInfo: any, fileTypes: string[] | undefined): boolean { + if (!fileTypes || fileTypes.length === 0) { + return true + } + return fileTypes.includes(fileInfo.mimeType) + } + + private async processFile(fileInfo: any, accessToken: string): Promise { + let content = '' + + try { + // Handle different file types + if (this.isTextBasedFile(fileInfo.mimeType)) { + // Download regular text files + content = await this.downloadFile(fileInfo.id, accessToken) + + // Create document with metadata + return [ + { + pageContent: content, + metadata: { + source: fileInfo.webViewLink || `https://drive.google.com/file/d/${fileInfo.id}/view`, + fileId: fileInfo.id, + fileName: fileInfo.name, + mimeType: fileInfo.mimeType, + size: fileInfo.size ? parseInt(fileInfo.size) : undefined, + createdTime: fileInfo.createdTime, + modifiedTime: fileInfo.modifiedTime, + parents: fileInfo.parents, + driveId: fileInfo.driveId, + driveContext: fileInfo.driveContext || (fileInfo.driveId ? ' (Shared Drive)' : ' (My Drive)') + } + } + ] + } else if (this.isSupportedBinaryFile(fileInfo.mimeType) || this.isGoogleWorkspaceFile(fileInfo.mimeType)) { + // Process binary files and Google Workspace files using loaders + return await this.processBinaryFile(fileInfo, accessToken) + } else { + console.warn(`Unsupported file type ${fileInfo.mimeType} for file ${fileInfo.name}`) + return [] + } + } catch (error) { + console.warn(`Failed to process file ${fileInfo.name}: ${error.message}`) + return [] + } + } + + private isSupportedBinaryFile(mimeType: string): boolean { + const supportedBinaryTypes = [ + 'application/pdf', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/msword', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'application/vnd.ms-excel', + 'text/csv' + ] + return supportedBinaryTypes.includes(mimeType) + } + + private async processBinaryFile(fileInfo: any, accessToken: string): Promise { + let tempFilePath: string | null = null + + try { + let buffer: Buffer + let processedMimeType: string + let processedFileName: string + + if (this.isGoogleWorkspaceFile(fileInfo.mimeType)) { + // Handle Google Workspace files by exporting to appropriate format + const exportResult = await this.exportGoogleWorkspaceFileAsBuffer(fileInfo.id, fileInfo.mimeType, accessToken) + buffer = exportResult.buffer + processedMimeType = exportResult.mimeType + processedFileName = exportResult.fileName + } else { + // Handle regular binary files + buffer = await this.downloadBinaryFile(fileInfo.id, accessToken) + processedMimeType = fileInfo.mimeType + processedFileName = fileInfo.name + } + + // Download file to temporary location + tempFilePath = await this.createTempFile(buffer, processedFileName, processedMimeType) + + let docs: IDocument[] = [] + const mimeType = processedMimeType.toLowerCase() + switch (mimeType) { + case 'application/pdf': { + const pdfLoader = new PDFLoader(tempFilePath, { + // @ts-ignore + pdfjs: () => import('pdf-parse/lib/pdf.js/v1.10.100/build/pdf.js') + }) + docs = await pdfLoader.load() + break + } + + case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': + case 'application/msword': { + const docxLoader = new DocxLoader(tempFilePath) + docs = await docxLoader.load() + break + } + + case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': + case 'application/vnd.ms-excel': { + const excelLoader = new LoadOfSheet(tempFilePath) + docs = await excelLoader.load() + break + } + case 'application/vnd.openxmlformats-officedocument.presentationml.presentation': + case 'application/vnd.ms-powerpoint': { + const pptxLoader = new PowerpointLoader(tempFilePath) + docs = await pptxLoader.load() + break + } + case 'text/csv': { + const csvLoader = new CSVLoader(tempFilePath) + docs = await csvLoader.load() + break + } + + default: + throw new Error(`Unsupported binary file type: ${mimeType}`) + } + + // Add Google Drive metadata to each document + if (docs.length > 0) { + const googleDriveMetadata = { + source: fileInfo.webViewLink || `https://drive.google.com/file/d/${fileInfo.id}/view`, + fileId: fileInfo.id, + fileName: fileInfo.name, + mimeType: fileInfo.mimeType, + size: fileInfo.size ? parseInt(fileInfo.size) : undefined, + createdTime: fileInfo.createdTime, + modifiedTime: fileInfo.modifiedTime, + parents: fileInfo.parents, + totalPages: docs.length // Total number of pages/sheets in the file + } + + return docs.map((doc, index) => ({ + ...doc, + metadata: { + ...doc.metadata, // Keep original loader metadata (page numbers, etc.) + ...googleDriveMetadata, // Add Google Drive metadata + pageIndex: index, // Add page/sheet index + driveId: fileInfo.driveId, + driveContext: fileInfo.driveContext || (fileInfo.driveId ? ' (Shared Drive)' : ' (My Drive)') + } + })) + } + + return [] + } catch (error) { + throw new Error(`Failed to process binary file: ${error.message}`) + } finally { + // Clean up temporary file + if (tempFilePath && fs.existsSync(tempFilePath)) { + try { + fs.unlinkSync(tempFilePath) + } catch (e) { + console.warn(`Failed to delete temporary file: ${tempFilePath}`) + } + } + } + } + + private async createTempFile(buffer: Buffer, fileName: string, mimeType: string): Promise { + // Get appropriate file extension + let extension = path.extname(fileName) + if (!extension) { + const extensionMap: { [key: string]: string } = { + 'application/pdf': '.pdf', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx', + 'application/msword': '.doc', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx', + 'application/vnd.ms-excel': '.xls', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation': '.pptx', + 'application/vnd.ms-powerpoint': '.ppt', + 'text/csv': '.csv' + } + extension = extensionMap[mimeType] || '.tmp' + } + + // Create temporary file + const tempDir = os.tmpdir() + const tempFileName = `gdrive_${Date.now()}_${Math.random().toString(36).substring(7)}${extension}` + const tempFilePath = path.join(tempDir, tempFileName) + + fs.writeFileSync(tempFilePath, buffer) + return tempFilePath + } + + private async downloadBinaryFile(fileId: string, accessToken: string): Promise { + const url = `https://www.googleapis.com/drive/v3/files/${encodeURIComponent(fileId)}?alt=media` + + const response = await fetch(url, { + headers: { + Authorization: `Bearer ${accessToken}` + } + }) + + if (!response.ok) { + throw new Error(`Failed to download file: ${response.statusText}`) + } + + const arrayBuffer = await response.arrayBuffer() + return Buffer.from(arrayBuffer) + } + + private async downloadFile(fileId: string, accessToken: string): Promise { + const url = `https://www.googleapis.com/drive/v3/files/${encodeURIComponent(fileId)}?alt=media` + + const response = await fetch(url, { + headers: { + Authorization: `Bearer ${accessToken}` + } + }) + + if (!response.ok) { + throw new Error(`Failed to download file: ${response.statusText}`) + } + + // Only call response.text() for text-based files + const contentType = response.headers.get('content-type') || '' + if (!contentType.startsWith('text/') && !contentType.includes('json') && !contentType.includes('xml')) { + throw new Error(`Cannot process binary file with content-type: ${contentType}`) + } + + return await response.text() + } + + private isGoogleWorkspaceFile(mimeType: string): boolean { + const googleWorkspaceMimeTypes = [ + 'application/vnd.google-apps.document', + 'application/vnd.google-apps.spreadsheet', + 'application/vnd.google-apps.presentation', + 'application/vnd.google-apps.drawing' + ] + return googleWorkspaceMimeTypes.includes(mimeType) + } + + private isTextBasedFile(mimeType: string): boolean { + const textBasedMimeTypes = [ + 'text/plain', + 'text/html', + 'text/css', + 'text/javascript', + 'text/csv', + 'text/xml', + 'application/json', + 'application/xml', + 'text/markdown', + 'text/x-markdown' + ] + return textBasedMimeTypes.includes(mimeType) + } + + private async exportGoogleWorkspaceFileAsBuffer( + fileId: string, + mimeType: string, + accessToken: string + ): Promise<{ buffer: Buffer; mimeType: string; fileName: string }> { + // Automatic mapping of Google Workspace MIME types to export formats + let exportMimeType: string + let fileExtension: string + + switch (mimeType) { + case 'application/vnd.google-apps.document': + exportMimeType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' + fileExtension = '.docx' + break + case 'application/vnd.google-apps.spreadsheet': + exportMimeType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + fileExtension = '.xlsx' + break + case 'application/vnd.google-apps.presentation': + exportMimeType = 'application/vnd.openxmlformats-officedocument.presentationml.presentation' + fileExtension = '.pptx' + break + case 'application/vnd.google-apps.drawing': + exportMimeType = 'application/pdf' + fileExtension = '.pdf' + break + default: + // Fallback to DOCX for any other Google Workspace file + exportMimeType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' + fileExtension = '.docx' + break + } + + const url = `https://www.googleapis.com/drive/v3/files/${encodeURIComponent(fileId)}/export?mimeType=${encodeURIComponent( + exportMimeType + )}` + + const response = await fetch(url, { + headers: { + Authorization: `Bearer ${accessToken}` + } + }) + + if (!response.ok) { + throw new Error(`Failed to export file: ${response.statusText}`) + } + + const arrayBuffer = await response.arrayBuffer() + const buffer = Buffer.from(arrayBuffer) + + return { + buffer, + mimeType: exportMimeType, + fileName: `exported_file${fileExtension}` + } + } +} + +module.exports = { nodeClass: GoogleDrive_DocumentLoaders } diff --git a/packages/components/nodes/documentloaders/GoogleDrive/google-drive.svg b/packages/components/nodes/documentloaders/GoogleDrive/google-drive.svg new file mode 100644 index 00000000000..03b2f21290a --- /dev/null +++ b/packages/components/nodes/documentloaders/GoogleDrive/google-drive.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/documentloaders/GoogleSheets/GoogleSheets.ts b/packages/components/nodes/documentloaders/GoogleSheets/GoogleSheets.ts new file mode 100644 index 00000000000..8c7a7a2a493 --- /dev/null +++ b/packages/components/nodes/documentloaders/GoogleSheets/GoogleSheets.ts @@ -0,0 +1,429 @@ +import { omit } from 'lodash' +import { ICommonObject, IDocument, INode, INodeData, INodeParams, INodeOptionsValue } from '../../../src/Interface' +import { TextSplitter } from 'langchain/text_splitter' +import { + convertMultiOptionsToStringArray, + getCredentialData, + getCredentialParam, + handleEscapeCharacters, + INodeOutputsValue, + refreshOAuth2Token +} from '../../../src' + +class GoogleSheets_DocumentLoaders implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + baseClasses: string[] + credential: INodeParams + inputs: INodeParams[] + outputs: INodeOutputsValue[] + + constructor() { + this.label = 'Google Sheets' + this.name = 'googleSheets' + this.version = 1.0 + this.type = 'Document' + this.icon = 'google-sheets.svg' + this.category = 'Document Loaders' + this.description = `Load data from Google Sheets as documents` + this.baseClasses = [this.type] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + description: 'Google Sheets OAuth2 Credential', + credentialNames: ['googleSheetsOAuth2'] + } + this.inputs = [ + { + label: 'Select Spreadsheet', + name: 'spreadsheetIds', + type: 'asyncMultiOptions', + loadMethod: 'listSpreadsheets', + description: 'Select spreadsheet from your Google Drive', + refresh: true + }, + { + label: 'Sheet Names', + name: 'sheetNames', + type: 'string', + description: 'Comma-separated list of sheet names to load. If empty, loads all sheets.', + placeholder: 'Sheet1, Sheet2', + optional: true + }, + { + label: 'Range', + name: 'range', + type: 'string', + description: 'Range to load (e.g., A1:E10). If empty, loads entire sheet.', + placeholder: 'A1:E10', + optional: true + }, + { + label: 'Include Headers', + name: 'includeHeaders', + type: 'boolean', + description: 'Whether to include the first row as headers', + default: true + }, + { + label: 'Value Render Option', + name: 'valueRenderOption', + type: 'options', + description: 'How values should be represented in the output', + options: [ + { + label: 'Formatted Value', + name: 'FORMATTED_VALUE' + }, + { + label: 'Unformatted Value', + name: 'UNFORMATTED_VALUE' + }, + { + label: 'Formula', + name: 'FORMULA' + } + ], + default: 'FORMATTED_VALUE', + optional: true + }, + { + label: 'Text Splitter', + name: 'textSplitter', + type: 'TextSplitter', + optional: true + }, + { + label: 'Additional Metadata', + name: 'metadata', + type: 'json', + description: 'Additional metadata to be added to the extracted documents', + optional: true, + additionalParams: true + }, + { + label: 'Omit Metadata Keys', + name: 'omitMetadataKeys', + type: 'string', + rows: 4, + description: + 'Each document loader comes with a default set of metadata keys that are extracted from the document. You can use this field to omit some of the default metadata keys. The value should be a list of keys, seperated by comma. Use * to omit all metadata keys execept the ones you specify in the Additional Metadata field', + placeholder: 'key1, key2, key3.nestedKey1', + optional: true, + additionalParams: true + } + ] + this.outputs = [ + { + label: 'Document', + name: 'document', + description: 'Array of document objects containing metadata and pageContent', + baseClasses: [...this.baseClasses, 'json'] + }, + { + label: 'Text', + name: 'text', + description: 'Concatenated string from pageContent of documents', + baseClasses: ['string', 'json'] + } + ] + } + + //@ts-ignore + loadMethods = { + async listSpreadsheets(nodeData: INodeData, options: ICommonObject): Promise { + const returnData: INodeOptionsValue[] = [] + + try { + let credentialData = await getCredentialData(nodeData.credential ?? '', options) + credentialData = await refreshOAuth2Token(nodeData.credential ?? '', credentialData, options) + const accessToken = getCredentialParam('access_token', credentialData, nodeData) + + if (!accessToken) { + return returnData + } + + // Query for Google Sheets files specifically + const query = "mimeType='application/vnd.google-apps.spreadsheet' and trashed = false" + + const url = new URL('https://www.googleapis.com/drive/v3/files') + url.searchParams.append('q', query) + url.searchParams.append('pageSize', '100') + url.searchParams.append('fields', 'files(id, name, modifiedTime, webViewLink)') + url.searchParams.append('orderBy', 'modifiedTime desc') + + const response = await fetch(url.toString(), { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json' + } + }) + + if (!response.ok) { + console.error(`Failed to list spreadsheets: ${response.statusText}`) + return returnData + } + + const data = await response.json() + + for (const file of data.files) { + const obj: INodeOptionsValue = { + name: file.id, + label: file.name, + description: `Modified: ${new Date(file.modifiedTime).toLocaleDateString()}` + } + returnData.push(obj) + } + } catch (error) { + console.error('Error listing Google Sheets:', error) + } + + return returnData + } + } + + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + const _spreadsheetIds = nodeData.inputs?.spreadsheetIds as string + const sheetNames = nodeData.inputs?.sheetNames as string + const range = nodeData.inputs?.range as string + const includeHeaders = nodeData.inputs?.includeHeaders as boolean + const valueRenderOption = (nodeData.inputs?.valueRenderOption as string) || 'FORMATTED_VALUE' + const textSplitter = nodeData.inputs?.textSplitter as TextSplitter + const metadata = nodeData.inputs?.metadata + const _omitMetadataKeys = nodeData.inputs?.omitMetadataKeys as string + const output = nodeData.outputs?.output as string + + let omitMetadataKeys: string[] = [] + if (_omitMetadataKeys) { + omitMetadataKeys = _omitMetadataKeys.split(',').map((key) => key.trim()) + } + + if (!_spreadsheetIds) { + throw new Error('At least one spreadsheet is required') + } + + let spreadsheetIds = convertMultiOptionsToStringArray(_spreadsheetIds) + + let credentialData = await getCredentialData(nodeData.credential ?? '', options) + credentialData = await refreshOAuth2Token(nodeData.credential ?? '', credentialData, options) + const accessToken = getCredentialParam('access_token', credentialData, nodeData) + + if (!accessToken) { + throw new Error('No access token found in credential') + } + + let docs: IDocument[] = [] + + try { + // Process each spreadsheet + for (const spreadsheetId of spreadsheetIds) { + try { + // Get spreadsheet metadata first + const spreadsheetMetadata = await this.getSpreadsheetMetadata(spreadsheetId, accessToken) + + // Determine which sheets to load + let sheetsToLoad: string[] = [] + if (sheetNames) { + sheetsToLoad = sheetNames.split(',').map((name) => name.trim()) + } else { + // Get all sheet names from metadata + sheetsToLoad = spreadsheetMetadata.sheets?.map((sheet: any) => sheet.properties.title) || [] + } + + // Load data from each sheet + for (const sheetName of sheetsToLoad) { + const sheetRange = range ? `${sheetName}!${range}` : sheetName + const sheetData = await this.getSheetData(spreadsheetId, sheetRange, valueRenderOption, accessToken) + + if (sheetData.values && sheetData.values.length > 0) { + const sheetDoc = this.convertSheetToDocument( + sheetData, + sheetName, + spreadsheetId, + spreadsheetMetadata, + includeHeaders + ) + docs.push(sheetDoc) + } + } + } catch (error) { + console.warn(`Failed to process spreadsheet ${spreadsheetId}: ${error.message}`) + // Continue processing other spreadsheets even if one fails + } + } + + // Apply text splitter if provided + if (textSplitter && docs.length > 0) { + docs = await textSplitter.splitDocuments(docs) + } + + // Apply metadata transformations + if (metadata) { + const parsedMetadata = typeof metadata === 'object' ? metadata : JSON.parse(metadata) + docs = docs.map((doc) => ({ + ...doc, + metadata: + _omitMetadataKeys === '*' + ? { + ...parsedMetadata + } + : omit( + { + ...doc.metadata, + ...parsedMetadata + }, + omitMetadataKeys + ) + })) + } else { + docs = docs.map((doc) => ({ + ...doc, + metadata: + _omitMetadataKeys === '*' + ? {} + : omit( + { + ...doc.metadata + }, + omitMetadataKeys + ) + })) + } + } catch (error) { + throw new Error(`Failed to load Google Sheets data: ${error.message}`) + } + + if (output === 'document') { + return docs + } else { + let finaltext = '' + for (const doc of docs) { + finaltext += `${doc.pageContent}\n` + } + return handleEscapeCharacters(finaltext, false) + } + } + + private async getSpreadsheetMetadata(spreadsheetId: string, accessToken: string): Promise { + const url = `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}` + + const response = await fetch(url, { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json' + } + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`Failed to get spreadsheet metadata: ${response.status} ${response.statusText} - ${errorText}`) + } + + return response.json() + } + + private async getSheetData(spreadsheetId: string, range: string, valueRenderOption: string, accessToken: string): Promise { + const url = `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}/values/${encodeURIComponent(range)}` + const params = new URLSearchParams({ + valueRenderOption, + dateTimeRenderOption: 'FORMATTED_STRING', + majorDimension: 'ROWS' + }) + + const response = await fetch(`${url}?${params}`, { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json' + } + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`Failed to get sheet data: ${response.status} ${response.statusText} - ${errorText}`) + } + + return response.json() + } + + private convertSheetToDocument( + sheetData: any, + sheetName: string, + spreadsheetId: string, + spreadsheetMetadata: any, + includeHeaders: boolean + ): IDocument { + const values = sheetData.values || [] + + if (values.length === 0) { + return { + pageContent: '', + metadata: { + source: `Google Sheets: ${spreadsheetMetadata.properties?.title || 'Unknown'} - ${sheetName}`, + spreadsheetId, + sheetName, + spreadsheetTitle: spreadsheetMetadata.properties?.title, + range: sheetData.range, + rowCount: 0, + columnCount: 0 + } + } + } + + let headers: string[] = [] + let dataRows: string[][] = [] + + if (includeHeaders && values.length > 0) { + headers = values[0] || [] + dataRows = values.slice(1) + } else { + // Generate default headers like A, B, C, etc. + const maxColumns = Math.max(...values.map((row: any[]) => row.length)) + headers = Array.from({ length: maxColumns }, (_, i) => String.fromCharCode(65 + i)) + dataRows = values + } + + // Convert to markdown table format + let content = '' + + if (headers.length > 0) { + // Create header row + content += '| ' + headers.join(' | ') + ' |\n' + // Create separator row + content += '| ' + headers.map(() => '---').join(' | ') + ' |\n' + + // Add data rows + for (const row of dataRows) { + const paddedRow = [...row] + // Pad row to match header length + while (paddedRow.length < headers.length) { + paddedRow.push('') + } + content += '| ' + paddedRow.join(' | ') + ' |\n' + } + } + + return { + pageContent: content, + metadata: { + source: `Google Sheets: ${spreadsheetMetadata.properties?.title || 'Unknown'} - ${sheetName}`, + spreadsheetId, + sheetName, + spreadsheetTitle: spreadsheetMetadata.properties?.title, + spreadsheetUrl: `https://docs.google.com/spreadsheets/d/${spreadsheetId}`, + range: sheetData.range, + rowCount: values.length, + columnCount: headers.length, + headers: includeHeaders ? headers : undefined, + totalDataRows: dataRows.length + } + } + } +} + +module.exports = { nodeClass: GoogleSheets_DocumentLoaders } diff --git a/packages/components/nodes/documentloaders/GoogleSheets/google-sheets.svg b/packages/components/nodes/documentloaders/GoogleSheets/google-sheets.svg new file mode 100644 index 00000000000..43af0ccf1fe --- /dev/null +++ b/packages/components/nodes/documentloaders/GoogleSheets/google-sheets.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/documentloaders/Jira/jira.svg b/packages/components/nodes/documentloaders/Jira/jira.svg index 807c5a31147..4ace5cc84a3 100644 --- a/packages/components/nodes/documentloaders/Jira/jira.svg +++ b/packages/components/nodes/documentloaders/Jira/jira.svg @@ -1,2 +1 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/packages/components/nodes/documentloaders/MicrosoftExcel/ExcelLoader.ts b/packages/components/nodes/documentloaders/MicrosoftExcel/ExcelLoader.ts new file mode 100644 index 00000000000..1e4889e1ce4 --- /dev/null +++ b/packages/components/nodes/documentloaders/MicrosoftExcel/ExcelLoader.ts @@ -0,0 +1,72 @@ +import { Document } from '@langchain/core/documents' +import { BufferLoader } from 'langchain/document_loaders/fs/buffer' +import { read, utils } from 'xlsx' + +/** + * Document loader that uses SheetJS to load documents. + * + * Each worksheet is parsed into an array of row objects using the SheetJS + * `sheet_to_json` method and projected to a `Document`. Metadata includes + * original sheet name, row data, and row index + */ +export class LoadOfSheet extends BufferLoader { + attributes: { name: string; description: string; type: string }[] = [] + + constructor(filePathOrBlob: string | Blob) { + super(filePathOrBlob) + this.attributes = [] + } + + /** + * Parse document + * + * NOTE: column labels in multiple sheets are not disambiguated! + * + * @param raw Raw data Buffer + * @param metadata Document metadata + * @returns Array of Documents + */ + async parse(raw: Buffer, metadata: Document['metadata']): Promise { + const result: Document[] = [] + + this.attributes = [ + { name: 'worksheet', description: 'Sheet or Worksheet Name', type: 'string' }, + { name: 'rowNum', description: 'Row index', type: 'number' } + ] + + const wb = read(raw, { type: 'buffer' }) + for (let name of wb.SheetNames) { + const fields: Record> = {} + const ws = wb.Sheets[name] + if (!ws) continue + + const aoo = utils.sheet_to_json(ws) as Record[] + aoo.forEach((row) => { + result.push({ + pageContent: + Object.entries(row) + .map((kv) => `- ${kv[0]}: ${kv[1]}`) + .join('\n') + '\n', + metadata: { + worksheet: name, + rowNum: row['__rowNum__'], + ...metadata, + ...row + } + }) + Object.entries(row).forEach(([k, v]) => { + if (v != null) (fields[k] || (fields[k] = {}))[v instanceof Date ? 'date' : typeof v] = true + }) + }) + Object.entries(fields).forEach(([k, v]) => + this.attributes.push({ + name: k, + description: k, + type: Object.keys(v).join(' or ') + }) + ) + } + + return result + } +} diff --git a/packages/components/nodes/documentloaders/MicrosoftExcel/MicrosoftExcel.ts b/packages/components/nodes/documentloaders/MicrosoftExcel/MicrosoftExcel.ts new file mode 100644 index 00000000000..468d16731b9 --- /dev/null +++ b/packages/components/nodes/documentloaders/MicrosoftExcel/MicrosoftExcel.ts @@ -0,0 +1,142 @@ +import { TextSplitter } from 'langchain/text_splitter' +import { LoadOfSheet } from './ExcelLoader' +import { getFileFromStorage, handleDocumentLoaderDocuments, handleDocumentLoaderMetadata, handleDocumentLoaderOutput } from '../../../src' +import { ICommonObject, IDocument, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' + +class MicrosoftExcel_DocumentLoaders implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + baseClasses: string[] + inputs: INodeParams[] + outputs: INodeOutputsValue[] + + constructor() { + this.label = 'Microsoft Excel' + this.name = 'microsoftExcel' + this.version = 1.0 + this.type = 'Document' + this.icon = 'excel.svg' + this.category = 'Document Loaders' + this.description = `Load data from Microsoft Excel files` + this.baseClasses = [this.type] + this.inputs = [ + { + label: 'Excel File', + name: 'excelFile', + type: 'file', + fileType: '.xlsx, .xls, .xlsm, .xlsb' + }, + { + label: 'Text Splitter', + name: 'textSplitter', + type: 'TextSplitter', + optional: true + }, + { + label: 'Additional Metadata', + name: 'metadata', + type: 'json', + description: 'Additional metadata to be added to the extracted documents', + optional: true, + additionalParams: true + }, + { + label: 'Omit Metadata Keys', + name: 'omitMetadataKeys', + type: 'string', + rows: 4, + description: + 'Each document loader comes with a default set of metadata keys that are extracted from the document. You can use this field to omit some of the default metadata keys. The value should be a list of keys, seperated by comma. Use * to omit all metadata keys execept the ones you specify in the Additional Metadata field', + placeholder: 'key1, key2, key3.nestedKey1', + optional: true, + additionalParams: true + } + ] + this.outputs = [ + { + label: 'Document', + name: 'document', + description: 'Array of document objects containing metadata and pageContent', + baseClasses: [...this.baseClasses, 'json'] + }, + { + label: 'Text', + name: 'text', + description: 'Concatenated string from pageContent of documents', + baseClasses: ['string', 'json'] + } + ] + } + + getFiles(nodeData: INodeData) { + const excelFileBase64 = nodeData.inputs?.excelFile as string + + let files: string[] = [] + let fromStorage: boolean = true + + if (excelFileBase64.startsWith('FILE-STORAGE::')) { + const fileName = excelFileBase64.replace('FILE-STORAGE::', '') + if (fileName.startsWith('[') && fileName.endsWith(']')) { + files = JSON.parse(fileName) + } else { + files = [fileName] + } + } else { + if (excelFileBase64.startsWith('[') && excelFileBase64.endsWith(']')) { + files = JSON.parse(excelFileBase64) + } else { + files = [excelFileBase64] + } + + fromStorage = false + } + + return { files, fromStorage } + } + + async getFileData(file: string, { orgId, chatflowid }: { orgId: string; chatflowid: string }, fromStorage?: boolean) { + if (fromStorage) { + return getFileFromStorage(file, orgId, chatflowid) + } else { + const splitDataURI = file.split(',') + splitDataURI.pop() + return Buffer.from(splitDataURI.pop() || '', 'base64') + } + } + + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + const textSplitter = nodeData.inputs?.textSplitter as TextSplitter + const metadata = nodeData.inputs?.metadata + const output = nodeData.outputs?.output as string + const _omitMetadataKeys = nodeData.inputs?.omitMetadataKeys as string + + let docs: IDocument[] = [] + + const orgId = options.orgId + const chatflowid = options.chatflowid + + const { files, fromStorage } = this.getFiles(nodeData) + + for (const file of files) { + if (!file) continue + + const fileData = await this.getFileData(file, { orgId, chatflowid }, fromStorage) + const blob = new Blob([fileData]) + const loader = new LoadOfSheet(blob) + + // use spread instead of push, because it raises RangeError: Maximum call stack size exceeded when too many docs + docs = [...docs, ...(await handleDocumentLoaderDocuments(loader, textSplitter))] + } + + docs = handleDocumentLoaderMetadata(docs, _omitMetadataKeys, metadata) + + return handleDocumentLoaderOutput(docs, output) + } +} + +module.exports = { nodeClass: MicrosoftExcel_DocumentLoaders } diff --git a/packages/components/nodes/documentloaders/MicrosoftExcel/excel.svg b/packages/components/nodes/documentloaders/MicrosoftExcel/excel.svg new file mode 100644 index 00000000000..22d8f9497d7 --- /dev/null +++ b/packages/components/nodes/documentloaders/MicrosoftExcel/excel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/documentloaders/MicrosoftPowerpoint/MicrosoftPowerpoint.ts b/packages/components/nodes/documentloaders/MicrosoftPowerpoint/MicrosoftPowerpoint.ts new file mode 100644 index 00000000000..bca5e9a5bc2 --- /dev/null +++ b/packages/components/nodes/documentloaders/MicrosoftPowerpoint/MicrosoftPowerpoint.ts @@ -0,0 +1,142 @@ +import { TextSplitter } from 'langchain/text_splitter' +import { PowerpointLoader } from './PowerpointLoader' +import { getFileFromStorage, handleDocumentLoaderDocuments, handleDocumentLoaderMetadata, handleDocumentLoaderOutput } from '../../../src' +import { ICommonObject, IDocument, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' + +class MicrosoftPowerpoint_DocumentLoaders implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + baseClasses: string[] + inputs: INodeParams[] + outputs: INodeOutputsValue[] + + constructor() { + this.label = 'Microsoft PowerPoint' + this.name = 'microsoftPowerpoint' + this.version = 1.0 + this.type = 'Document' + this.icon = 'powerpoint.svg' + this.category = 'Document Loaders' + this.description = `Load data from Microsoft PowerPoint files` + this.baseClasses = [this.type] + this.inputs = [ + { + label: 'PowerPoint File', + name: 'powerpointFile', + type: 'file', + fileType: '.pptx, .ppt' + }, + { + label: 'Text Splitter', + name: 'textSplitter', + type: 'TextSplitter', + optional: true + }, + { + label: 'Additional Metadata', + name: 'metadata', + type: 'json', + description: 'Additional metadata to be added to the extracted documents', + optional: true, + additionalParams: true + }, + { + label: 'Omit Metadata Keys', + name: 'omitMetadataKeys', + type: 'string', + rows: 4, + description: + 'Each document loader comes with a default set of metadata keys that are extracted from the document. You can use this field to omit some of the default metadata keys. The value should be a list of keys, seperated by comma. Use * to omit all metadata keys execept the ones you specify in the Additional Metadata field', + placeholder: 'key1, key2, key3.nestedKey1', + optional: true, + additionalParams: true + } + ] + this.outputs = [ + { + label: 'Document', + name: 'document', + description: 'Array of document objects containing metadata and pageContent', + baseClasses: [...this.baseClasses, 'json'] + }, + { + label: 'Text', + name: 'text', + description: 'Concatenated string from pageContent of documents', + baseClasses: ['string', 'json'] + } + ] + } + + getFiles(nodeData: INodeData) { + const powerpointFileBase64 = nodeData.inputs?.powerpointFile as string + + let files: string[] = [] + let fromStorage: boolean = true + + if (powerpointFileBase64.startsWith('FILE-STORAGE::')) { + const fileName = powerpointFileBase64.replace('FILE-STORAGE::', '') + if (fileName.startsWith('[') && fileName.endsWith(']')) { + files = JSON.parse(fileName) + } else { + files = [fileName] + } + } else { + if (powerpointFileBase64.startsWith('[') && powerpointFileBase64.endsWith(']')) { + files = JSON.parse(powerpointFileBase64) + } else { + files = [powerpointFileBase64] + } + + fromStorage = false + } + + return { files, fromStorage } + } + + async getFileData(file: string, { orgId, chatflowid }: { orgId: string; chatflowid: string }, fromStorage?: boolean) { + if (fromStorage) { + return getFileFromStorage(file, orgId, chatflowid) + } else { + const splitDataURI = file.split(',') + splitDataURI.pop() + return Buffer.from(splitDataURI.pop() || '', 'base64') + } + } + + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + const textSplitter = nodeData.inputs?.textSplitter as TextSplitter + const metadata = nodeData.inputs?.metadata + const output = nodeData.outputs?.output as string + const _omitMetadataKeys = nodeData.inputs?.omitMetadataKeys as string + + let docs: IDocument[] = [] + + const orgId = options.orgId + const chatflowid = options.chatflowid + + const { files, fromStorage } = this.getFiles(nodeData) + + for (const file of files) { + if (!file) continue + + const fileData = await this.getFileData(file, { orgId, chatflowid }, fromStorage) + const blob = new Blob([fileData]) + const loader = new PowerpointLoader(blob) + + // use spread instead of push, because it raises RangeError: Maximum call stack size exceeded when too many docs + docs = [...docs, ...(await handleDocumentLoaderDocuments(loader, textSplitter))] + } + + docs = handleDocumentLoaderMetadata(docs, _omitMetadataKeys, metadata) + + return handleDocumentLoaderOutput(docs, output) + } +} + +module.exports = { nodeClass: MicrosoftPowerpoint_DocumentLoaders } diff --git a/packages/components/nodes/documentloaders/MicrosoftPowerpoint/PowerpointLoader.ts b/packages/components/nodes/documentloaders/MicrosoftPowerpoint/PowerpointLoader.ts new file mode 100644 index 00000000000..97f266826f0 --- /dev/null +++ b/packages/components/nodes/documentloaders/MicrosoftPowerpoint/PowerpointLoader.ts @@ -0,0 +1,101 @@ +import { Document } from '@langchain/core/documents' +import { BufferLoader } from 'langchain/document_loaders/fs/buffer' +import { parseOfficeAsync } from 'officeparser' + +/** + * Document loader that uses officeparser to load PowerPoint documents. + * + * Each slide is parsed into a separate Document with metadata including + * slide number and extracted text content. + */ +export class PowerpointLoader extends BufferLoader { + attributes: { name: string; description: string; type: string }[] = [] + + constructor(filePathOrBlob: string | Blob) { + super(filePathOrBlob) + this.attributes = [] + } + + /** + * Parse PowerPoint document + * + * @param raw Raw data Buffer + * @param metadata Document metadata + * @returns Array of Documents + */ + async parse(raw: Buffer, metadata: Document['metadata']): Promise { + const result: Document[] = [] + + this.attributes = [ + { name: 'slideNumber', description: 'Slide number', type: 'number' }, + { name: 'documentType', description: 'Type of document', type: 'string' } + ] + + try { + // Use officeparser to extract text from PowerPoint + const data = await parseOfficeAsync(raw) + + if (typeof data === 'string' && data.trim()) { + // Split content by common slide separators or use the entire content as one document + const slides = this.splitIntoSlides(data) + + slides.forEach((slideContent, index) => { + if (slideContent.trim()) { + result.push({ + pageContent: slideContent.trim(), + metadata: { + slideNumber: index + 1, + documentType: 'powerpoint', + ...metadata + } + }) + } + }) + } + } catch (error) { + console.error('Error parsing PowerPoint file:', error) + throw new Error(`Failed to parse PowerPoint file: ${error instanceof Error ? error.message : 'Unknown error'}`) + } + + return result + } + + /** + * Split content into slides based on common patterns + * This is a heuristic approach since officeparser returns plain text + */ + private splitIntoSlides(content: string): string[] { + // Try to split by common slide patterns + const slidePatterns = [ + /\n\s*Slide\s+\d+/gi, + /\n\s*Page\s+\d+/gi, + /\n\s*\d+\s*\/\s*\d+/gi, + /\n\s*_{3,}/g, // Underscores as separators + /\n\s*-{3,}/g // Dashes as separators + ] + + let slides: string[] = [] + + // Try each pattern and use the one that creates the most reasonable splits + for (const pattern of slidePatterns) { + const potentialSlides = content.split(pattern) + if (potentialSlides.length > 1 && potentialSlides.length < 100) { + // Reasonable number of slides + slides = potentialSlides + break + } + } + + // If no good pattern found, split by double newlines as a fallback + if (slides.length === 0) { + slides = content.split(/\n\s*\n\s*\n/) + } + + // If still no good split, treat entire content as one slide + if (slides.length === 0 || slides.every((slide) => slide.trim().length < 10)) { + slides = [content] + } + + return slides.filter((slide) => slide.trim().length > 0) + } +} diff --git a/packages/components/nodes/documentloaders/MicrosoftPowerpoint/powerpoint.svg b/packages/components/nodes/documentloaders/MicrosoftPowerpoint/powerpoint.svg new file mode 100644 index 00000000000..4d2f7b2a1ca --- /dev/null +++ b/packages/components/nodes/documentloaders/MicrosoftPowerpoint/powerpoint.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/documentloaders/MicrosoftWord/MicrosoftWord.ts b/packages/components/nodes/documentloaders/MicrosoftWord/MicrosoftWord.ts new file mode 100644 index 00000000000..7d74af25999 --- /dev/null +++ b/packages/components/nodes/documentloaders/MicrosoftWord/MicrosoftWord.ts @@ -0,0 +1,142 @@ +import { TextSplitter } from 'langchain/text_splitter' +import { WordLoader } from './WordLoader' +import { getFileFromStorage, handleDocumentLoaderDocuments, handleDocumentLoaderMetadata, handleDocumentLoaderOutput } from '../../../src' +import { ICommonObject, IDocument, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' + +class MicrosoftWord_DocumentLoaders implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + baseClasses: string[] + inputs: INodeParams[] + outputs: INodeOutputsValue[] + + constructor() { + this.label = 'Microsoft Word' + this.name = 'microsoftWord' + this.version = 1.0 + this.type = 'Document' + this.icon = 'word.svg' + this.category = 'Document Loaders' + this.description = `Load data from Microsoft Word files` + this.baseClasses = [this.type] + this.inputs = [ + { + label: 'Word File', + name: 'docxFile', + type: 'file', + fileType: '.docx, .doc' + }, + { + label: 'Text Splitter', + name: 'textSplitter', + type: 'TextSplitter', + optional: true + }, + { + label: 'Additional Metadata', + name: 'metadata', + type: 'json', + description: 'Additional metadata to be added to the extracted documents', + optional: true, + additionalParams: true + }, + { + label: 'Omit Metadata Keys', + name: 'omitMetadataKeys', + type: 'string', + rows: 4, + description: + 'Each document loader comes with a default set of metadata keys that are extracted from the document. You can use this field to omit some of the default metadata keys. The value should be a list of keys, seperated by comma. Use * to omit all metadata keys execept the ones you specify in the Additional Metadata field', + placeholder: 'key1, key2, key3.nestedKey1', + optional: true, + additionalParams: true + } + ] + this.outputs = [ + { + label: 'Document', + name: 'document', + description: 'Array of document objects containing metadata and pageContent', + baseClasses: [...this.baseClasses, 'json'] + }, + { + label: 'Text', + name: 'text', + description: 'Concatenated string from pageContent of documents', + baseClasses: ['string', 'json'] + } + ] + } + + getFiles(nodeData: INodeData) { + const docxFileBase64 = nodeData.inputs?.docxFile as string + + let files: string[] = [] + let fromStorage: boolean = true + + if (docxFileBase64.startsWith('FILE-STORAGE::')) { + const fileName = docxFileBase64.replace('FILE-STORAGE::', '') + if (fileName.startsWith('[') && fileName.endsWith(']')) { + files = JSON.parse(fileName) + } else { + files = [fileName] + } + } else { + if (docxFileBase64.startsWith('[') && docxFileBase64.endsWith(']')) { + files = JSON.parse(docxFileBase64) + } else { + files = [docxFileBase64] + } + + fromStorage = false + } + + return { files, fromStorage } + } + + async getFileData(file: string, { orgId, chatflowid }: { orgId: string; chatflowid: string }, fromStorage?: boolean) { + if (fromStorage) { + return getFileFromStorage(file, orgId, chatflowid) + } else { + const splitDataURI = file.split(',') + splitDataURI.pop() + return Buffer.from(splitDataURI.pop() || '', 'base64') + } + } + + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + const textSplitter = nodeData.inputs?.textSplitter as TextSplitter + const metadata = nodeData.inputs?.metadata + const output = nodeData.outputs?.output as string + const _omitMetadataKeys = nodeData.inputs?.omitMetadataKeys as string + + let docs: IDocument[] = [] + + const orgId = options.orgId + const chatflowid = options.chatflowid + + const { files, fromStorage } = this.getFiles(nodeData) + + for (const file of files) { + if (!file) continue + + const fileData = await this.getFileData(file, { orgId, chatflowid }, fromStorage) + const blob = new Blob([fileData]) + const loader = new WordLoader(blob) + + // use spread instead of push, because it raises RangeError: Maximum call stack size exceeded when too many docs + docs = [...docs, ...(await handleDocumentLoaderDocuments(loader, textSplitter))] + } + + docs = handleDocumentLoaderMetadata(docs, _omitMetadataKeys, metadata) + + return handleDocumentLoaderOutput(docs, output) + } +} + +module.exports = { nodeClass: MicrosoftWord_DocumentLoaders } diff --git a/packages/components/nodes/documentloaders/MicrosoftWord/WordLoader.ts b/packages/components/nodes/documentloaders/MicrosoftWord/WordLoader.ts new file mode 100644 index 00000000000..640e2c4ff09 --- /dev/null +++ b/packages/components/nodes/documentloaders/MicrosoftWord/WordLoader.ts @@ -0,0 +1,108 @@ +import { Document } from '@langchain/core/documents' +import { BufferLoader } from 'langchain/document_loaders/fs/buffer' +import { parseOfficeAsync } from 'officeparser' + +/** + * Document loader that uses officeparser to load Word documents. + * + * The document is parsed into a single Document with metadata including + * document type and extracted text content. + */ +export class WordLoader extends BufferLoader { + attributes: { name: string; description: string; type: string }[] = [] + + constructor(filePathOrBlob: string | Blob) { + super(filePathOrBlob) + this.attributes = [] + } + + /** + * Parse Word document + * + * @param raw Raw data Buffer + * @param metadata Document metadata + * @returns Array of Documents + */ + async parse(raw: Buffer, metadata: Document['metadata']): Promise { + const result: Document[] = [] + + this.attributes = [ + { name: 'documentType', description: 'Type of document', type: 'string' }, + { name: 'pageCount', description: 'Number of pages/sections', type: 'number' } + ] + + try { + // Use officeparser to extract text from Word document + const data = await parseOfficeAsync(raw) + + if (typeof data === 'string' && data.trim()) { + // Split content by common page/section separators + const sections = this.splitIntoSections(data) + + sections.forEach((sectionContent, index) => { + if (sectionContent.trim()) { + result.push({ + pageContent: sectionContent.trim(), + metadata: { + documentType: 'word', + pageNumber: index + 1, + ...metadata + } + }) + } + }) + } + } catch (error) { + console.error('Error parsing Word file:', error) + throw new Error(`Failed to parse Word file: ${error instanceof Error ? error.message : 'Unknown error'}`) + } + + return result + } + + /** + * Split content into sections based on common patterns + * This is a heuristic approach since officeparser returns plain text + */ + private splitIntoSections(content: string): string[] { + // Try to split by common section patterns + const sectionPatterns = [ + /\n\s*Page\s+\d+/gi, + /\n\s*Section\s+\d+/gi, + /\n\s*Chapter\s+\d+/gi, + /\n\s*\d+\.\s+/gi, // Numbered sections like "1. ", "2. " + /\n\s*[A-Z][A-Z\s]{2,}\n/g, // ALL CAPS headings + /\n\s*_{5,}/g, // Long underscores as separators + /\n\s*-{5,}/g // Long dashes as separators + ] + + let sections: string[] = [] + + // Try each pattern and use the one that creates the most reasonable splits + for (const pattern of sectionPatterns) { + const potentialSections = content.split(pattern) + if (potentialSections.length > 1 && potentialSections.length < 50) { + // Reasonable number of sections + sections = potentialSections + break + } + } + + // If no good pattern found, split by multiple newlines as a fallback + if (sections.length === 0) { + sections = content.split(/\n\s*\n\s*\n\s*\n/) + } + + // If still no good split, split by double newlines + if (sections.length === 0 || sections.every((section) => section.trim().length < 20)) { + sections = content.split(/\n\s*\n\s*\n/) + } + + // If still no good split, treat entire content as one section + if (sections.length === 0 || sections.every((section) => section.trim().length < 10)) { + sections = [content] + } + + return sections.filter((section) => section.trim().length > 0) + } +} diff --git a/packages/components/nodes/documentloaders/MicrosoftWord/word.svg b/packages/components/nodes/documentloaders/MicrosoftWord/word.svg new file mode 100644 index 00000000000..dabac0ea598 --- /dev/null +++ b/packages/components/nodes/documentloaders/MicrosoftWord/word.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/documentloaders/S3Directory/S3Directory.ts b/packages/components/nodes/documentloaders/S3Directory/S3Directory.ts index 072822aef38..4a55869e0e5 100644 --- a/packages/components/nodes/documentloaders/S3Directory/S3Directory.ts +++ b/packages/components/nodes/documentloaders/S3Directory/S3Directory.ts @@ -19,9 +19,9 @@ import { PDFLoader } from '@langchain/community/document_loaders/fs/pdf' import { DocxLoader } from '@langchain/community/document_loaders/fs/docx' import { TextLoader } from 'langchain/document_loaders/fs/text' import { TextSplitter } from 'langchain/text_splitter' - import { CSVLoader } from '../Csv/CsvLoader' - +import { LoadOfSheet } from '../MicrosoftExcel/ExcelLoader' +import { PowerpointLoader } from '../MicrosoftPowerpoint/PowerpointLoader' class S3_DocumentLoaders implements INode { label: string name: string @@ -240,7 +240,13 @@ class S3_DocumentLoaders implements INode { '.json': (path) => new JSONLoader(path), '.txt': (path) => new TextLoader(path), '.csv': (path) => new CSVLoader(path), + '.xls': (path) => new LoadOfSheet(path), + '.xlsx': (path) => new LoadOfSheet(path), + '.xlsm': (path) => new LoadOfSheet(path), + '.xlsb': (path) => new LoadOfSheet(path), '.docx': (path) => new DocxLoader(path), + '.ppt': (path) => new PowerpointLoader(path), + '.pptx': (path) => new PowerpointLoader(path), '.pdf': (path) => new PDFLoader(path, { splitPages: pdfUsage !== 'perFile', diff --git a/packages/components/nodes/documentloaders/S3File/S3File.ts b/packages/components/nodes/documentloaders/S3File/S3File.ts index 51c1980459c..d77f37e090c 100644 --- a/packages/components/nodes/documentloaders/S3File/S3File.ts +++ b/packages/components/nodes/documentloaders/S3File/S3File.ts @@ -14,12 +14,21 @@ import { handleDocumentLoaderMetadata, handleDocumentLoaderOutput } from '../../../src/utils' -import { S3Client, GetObjectCommand, S3ClientConfig } from '@aws-sdk/client-s3' +import { S3Client, GetObjectCommand, HeadObjectCommand, S3ClientConfig } from '@aws-sdk/client-s3' import { getRegions, MODEL_TYPE } from '../../../src/modelLoader' import { Readable } from 'node:stream' import * as fsDefault from 'node:fs' import * as path from 'node:path' import * as os from 'node:os' +import { PDFLoader } from '@langchain/community/document_loaders/fs/pdf' +import { DocxLoader } from '@langchain/community/document_loaders/fs/docx' +import { CSVLoader } from '@langchain/community/document_loaders/fs/csv' +import { LoadOfSheet } from '../MicrosoftExcel/ExcelLoader' +import { PowerpointLoader } from '../MicrosoftPowerpoint/PowerpointLoader' +import { TextSplitter } from 'langchain/text_splitter' +import { IDocument } from '../../../src/Interface' +import { omit } from 'lodash' +import { handleEscapeCharacters } from '../../../src' class S3_DocumentLoaders implements INode { label: string @@ -37,7 +46,7 @@ class S3_DocumentLoaders implements INode { constructor() { this.label = 'S3' this.name = 'S3' - this.version = 4.0 + this.version = 5.0 this.type = 'Document' this.icon = 's3.svg' this.category = 'Document Loaders' @@ -70,6 +79,52 @@ class S3_DocumentLoaders implements INode { loadMethod: 'listRegions', default: 'us-east-1' }, + { + label: 'File Processing Method', + name: 'fileProcessingMethod', + type: 'options', + options: [ + { + label: 'Built In Loaders', + name: 'builtIn', + description: 'Use the built in loaders to process the file.' + }, + { + label: 'Unstructured', + name: 'unstructured', + description: 'Use the Unstructured API to process the file.' + } + ], + default: 'builtIn' + }, + { + label: 'Text Splitter', + name: 'textSplitter', + type: 'TextSplitter', + optional: true, + show: { + fileProcessingMethod: 'builtIn' + } + }, + { + label: 'Additional Metadata', + name: 'metadata', + type: 'json', + description: 'Additional metadata to be added to the extracted documents', + optional: true, + additionalParams: true + }, + { + label: 'Omit Metadata Keys', + name: 'omitMetadataKeys', + type: 'string', + rows: 4, + description: + 'Each document loader comes with a default set of metadata keys that are extracted from the document. You can use this field to omit some of the default metadata keys. The value should be a list of keys, seperated by comma. Use * to omit all metadata keys execept the ones you specify in the Additional Metadata field', + placeholder: 'key1, key2, key3.nestedKey1', + optional: true, + additionalParams: true + }, { label: 'Unstructured API URL', name: 'unstructuredAPIUrl', @@ -77,13 +132,21 @@ class S3_DocumentLoaders implements INode { 'Your Unstructured.io URL. Read more on how to get started', type: 'string', placeholder: process.env.UNSTRUCTURED_API_URL || 'http://localhost:8000/general/v0/general', - optional: !!process.env.UNSTRUCTURED_API_URL + optional: !!process.env.UNSTRUCTURED_API_URL, + additionalParams: true, + show: { + fileProcessingMethod: 'unstructured' + } }, { label: 'Unstructured API KEY', name: 'unstructuredAPIKey', type: 'password', - optional: true + optional: true, + additionalParams: true, + show: { + fileProcessingMethod: 'unstructured' + } }, { label: 'Strategy', @@ -110,7 +173,10 @@ class S3_DocumentLoaders implements INode { ], optional: true, additionalParams: true, - default: 'auto' + default: 'auto', + show: { + fileProcessingMethod: 'unstructured' + } }, { label: 'Encoding', @@ -119,7 +185,10 @@ class S3_DocumentLoaders implements INode { type: 'string', optional: true, additionalParams: true, - default: 'utf-8' + default: 'utf-8', + show: { + fileProcessingMethod: 'unstructured' + } }, { label: 'Skip Infer Table Types', @@ -214,7 +283,10 @@ class S3_DocumentLoaders implements INode { ], optional: true, additionalParams: true, - default: '["pdf", "jpg", "png"]' + default: '["pdf", "jpg", "png"]', + show: { + fileProcessingMethod: 'unstructured' + } }, { label: 'Hi-Res Model Name', @@ -247,7 +319,10 @@ class S3_DocumentLoaders implements INode { ], optional: true, additionalParams: true, - default: 'detectron2_onnx' + default: 'detectron2_onnx', + show: { + fileProcessingMethod: 'unstructured' + } }, { label: 'Chunking Strategy', @@ -267,7 +342,10 @@ class S3_DocumentLoaders implements INode { ], optional: true, additionalParams: true, - default: 'by_title' + default: 'by_title', + show: { + fileProcessingMethod: 'unstructured' + } }, { label: 'OCR Languages', @@ -337,7 +415,10 @@ class S3_DocumentLoaders implements INode { } ], optional: true, - additionalParams: true + additionalParams: true, + show: { + fileProcessingMethod: 'unstructured' + } }, { label: 'Source ID Key', @@ -348,7 +429,10 @@ class S3_DocumentLoaders implements INode { default: 'source', placeholder: 'source', optional: true, - additionalParams: true + additionalParams: true, + show: { + fileProcessingMethod: 'unstructured' + } }, { label: 'Coordinates', @@ -357,7 +441,10 @@ class S3_DocumentLoaders implements INode { description: 'If true, return coordinates for each element. Default: false.', optional: true, additionalParams: true, - default: false + default: false, + show: { + fileProcessingMethod: 'unstructured' + } }, { label: 'XML Keep Tags', @@ -366,7 +453,10 @@ class S3_DocumentLoaders implements INode { 'If True, will retain the XML tags in the output. Otherwise it will simply extract the text from within the tags. Only applies to partition_xml.', type: 'boolean', optional: true, - additionalParams: true + additionalParams: true, + show: { + fileProcessingMethod: 'unstructured' + } }, { label: 'Include Page Breaks', @@ -374,15 +464,10 @@ class S3_DocumentLoaders implements INode { description: 'When true, the output will include page break elements when the filetype supports it.', type: 'boolean', optional: true, - additionalParams: true - }, - { - label: 'XML Keep Tags', - name: 'xmlKeepTags', - description: 'Whether to keep XML tags in the output.', - type: 'boolean', - optional: true, - additionalParams: true + additionalParams: true, + show: { + fileProcessingMethod: 'unstructured' + } }, { label: 'Multi-Page Sections', @@ -390,7 +475,10 @@ class S3_DocumentLoaders implements INode { description: 'Whether to treat multi-page documents as separate sections.', type: 'boolean', optional: true, - additionalParams: true + additionalParams: true, + show: { + fileProcessingMethod: 'unstructured' + } }, { label: 'Combine Under N Chars', @@ -399,7 +487,10 @@ class S3_DocumentLoaders implements INode { "If chunking strategy is set, combine elements until a section reaches a length of n chars. Default: value of max_characters. Can't exceed value of max_characters.", type: 'number', optional: true, - additionalParams: true + additionalParams: true, + show: { + fileProcessingMethod: 'unstructured' + } }, { label: 'New After N Chars', @@ -408,7 +499,10 @@ class S3_DocumentLoaders implements INode { "If chunking strategy is set, cut off new sections after reaching a length of n chars (soft max). value of max_characters. Can't exceed value of max_characters.", type: 'number', optional: true, - additionalParams: true + additionalParams: true, + show: { + fileProcessingMethod: 'unstructured' + } }, { label: 'Max Characters', @@ -418,7 +512,10 @@ class S3_DocumentLoaders implements INode { type: 'number', optional: true, additionalParams: true, - default: '500' + default: '500', + show: { + fileProcessingMethod: 'unstructured' + } }, { label: 'Additional Metadata', @@ -426,7 +523,10 @@ class S3_DocumentLoaders implements INode { type: 'json', description: 'Additional metadata to be added to the extracted documents', optional: true, - additionalParams: true + additionalParams: true, + show: { + fileProcessingMethod: 'unstructured' + } }, { label: 'Omit Metadata Keys', @@ -437,7 +537,10 @@ class S3_DocumentLoaders implements INode { '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 + additionalParams: true, + show: { + fileProcessingMethod: 'unstructured' + } } ] this.outputs = [ @@ -466,28 +569,17 @@ class S3_DocumentLoaders implements INode { const bucketName = nodeData.inputs?.bucketName as string const keyName = nodeData.inputs?.keyName as string const region = nodeData.inputs?.region as string - const unstructuredAPIUrl = nodeData.inputs?.unstructuredAPIUrl as string - const unstructuredAPIKey = nodeData.inputs?.unstructuredAPIKey as string - const strategy = nodeData.inputs?.strategy as UnstructuredLoaderStrategy - const encoding = nodeData.inputs?.encoding as string - const coordinates = nodeData.inputs?.coordinates as boolean - const skipInferTableTypes = nodeData.inputs?.skipInferTableTypes - ? JSON.parse(nodeData.inputs?.skipInferTableTypes as string) - : ([] as SkipInferTableTypes[]) - const hiResModelName = nodeData.inputs?.hiResModelName as HiResModelName - const includePageBreaks = nodeData.inputs?.includePageBreaks as boolean - const chunkingStrategy = nodeData.inputs?.chunkingStrategy as 'None' | 'by_title' + const fileProcessingMethod = nodeData.inputs?.fileProcessingMethod as string + const textSplitter = nodeData.inputs?.textSplitter as TextSplitter const metadata = nodeData.inputs?.metadata - const sourceIdKey = (nodeData.inputs?.sourceIdKey as string) || 'source' - const ocrLanguages = nodeData.inputs?.ocrLanguages ? JSON.parse(nodeData.inputs?.ocrLanguages as string) : ([] as string[]) - const xmlKeepTags = nodeData.inputs?.xmlKeepTags as boolean - const multiPageSections = nodeData.inputs?.multiPageSections as boolean - const combineUnderNChars = nodeData.inputs?.combineUnderNChars as number - const newAfterNChars = nodeData.inputs?.newAfterNChars as number - const maxCharacters = nodeData.inputs?.maxCharacters as number const _omitMetadataKeys = nodeData.inputs?.omitMetadataKeys as string const output = nodeData.outputs?.output as string + let omitMetadataKeys: string[] = [] + if (_omitMetadataKeys) { + omitMetadataKeys = _omitMetadataKeys.split(',').map((key) => key.trim()) + } + let credentials: S3ClientConfig['credentials'] | undefined if (nodeData.credential) { @@ -508,6 +600,162 @@ class S3_DocumentLoaders implements INode { credentials } + if (fileProcessingMethod === 'builtIn') { + return await this.processWithBuiltInLoaders( + bucketName, + keyName, + s3Config, + textSplitter, + metadata, + omitMetadataKeys, + _omitMetadataKeys, + output + ) + } else { + return await this.processWithUnstructured(nodeData, options, bucketName, keyName, s3Config) + } + } + + private async processWithBuiltInLoaders( + bucketName: string, + keyName: string, + s3Config: S3ClientConfig, + textSplitter: TextSplitter, + metadata: any, + omitMetadataKeys: string[], + _omitMetadataKeys: string, + output: string + ): Promise { + let docs: IDocument[] = [] + + try { + const s3Client = new S3Client(s3Config) + + // Get file metadata to determine content type + const headCommand = new HeadObjectCommand({ + Bucket: bucketName, + Key: keyName + }) + + const headResponse = await s3Client.send(headCommand) + const contentType = headResponse.ContentType || this.getMimeTypeFromExtension(keyName) + + // Download the file + const getObjectCommand = new GetObjectCommand({ + Bucket: bucketName, + Key: keyName + }) + + const response = await s3Client.send(getObjectCommand) + + const objectData = await new Promise((resolve, reject) => { + const chunks: Buffer[] = [] + + if (response.Body instanceof Readable) { + response.Body.on('data', (chunk: Buffer) => chunks.push(chunk)) + response.Body.on('end', () => resolve(Buffer.concat(chunks))) + response.Body.on('error', reject) + } else { + reject(new Error('Response body is not a readable stream.')) + } + }) + + // Process the file based on content type + const fileInfo = { + id: keyName, + name: path.basename(keyName), + mimeType: contentType, + size: objectData.length, + webViewLink: `s3://${bucketName}/${keyName}`, + bucketName: bucketName, + key: keyName, + lastModified: headResponse.LastModified, + etag: headResponse.ETag + } + + docs = await this.processFile(fileInfo, objectData) + + // Apply text splitter if provided + if (textSplitter && docs.length > 0) { + docs = await textSplitter.splitDocuments(docs) + } + + // Apply metadata transformations + if (metadata) { + const parsedMetadata = typeof metadata === 'object' ? metadata : JSON.parse(metadata) + docs = docs.map((doc) => ({ + ...doc, + metadata: + _omitMetadataKeys === '*' + ? { + ...parsedMetadata + } + : omit( + { + ...doc.metadata, + ...parsedMetadata + }, + omitMetadataKeys + ) + })) + } else { + docs = docs.map((doc) => ({ + ...doc, + metadata: + _omitMetadataKeys === '*' + ? {} + : omit( + { + ...doc.metadata + }, + omitMetadataKeys + ) + })) + } + } catch (error) { + throw new Error(`Failed to load S3 document: ${error.message}`) + } + + if (output === 'document') { + return docs + } else { + let finaltext = '' + for (const doc of docs) { + finaltext += `${doc.pageContent}\n` + } + return handleEscapeCharacters(finaltext, false) + } + } + + private async processWithUnstructured( + nodeData: INodeData, + options: ICommonObject, + bucketName: string, + keyName: string, + s3Config: S3ClientConfig + ): Promise { + const unstructuredAPIUrl = nodeData.inputs?.unstructuredAPIUrl as string + const unstructuredAPIKey = nodeData.inputs?.unstructuredAPIKey as string + const strategy = nodeData.inputs?.strategy as UnstructuredLoaderStrategy + const encoding = nodeData.inputs?.encoding as string + const coordinates = nodeData.inputs?.coordinates as boolean + const skipInferTableTypes = nodeData.inputs?.skipInferTableTypes + ? JSON.parse(nodeData.inputs?.skipInferTableTypes as string) + : ([] as SkipInferTableTypes[]) + const hiResModelName = nodeData.inputs?.hiResModelName as HiResModelName + const includePageBreaks = nodeData.inputs?.includePageBreaks as boolean + const chunkingStrategy = nodeData.inputs?.chunkingStrategy as 'None' | 'by_title' + const metadata = nodeData.inputs?.metadata + const sourceIdKey = (nodeData.inputs?.sourceIdKey as string) || 'source' + const ocrLanguages = nodeData.inputs?.ocrLanguages ? JSON.parse(nodeData.inputs?.ocrLanguages as string) : ([] as string[]) + const xmlKeepTags = nodeData.inputs?.xmlKeepTags as boolean + const multiPageSections = nodeData.inputs?.multiPageSections as boolean + const combineUnderNChars = nodeData.inputs?.combineUnderNChars as number + const newAfterNChars = nodeData.inputs?.newAfterNChars as number + const maxCharacters = nodeData.inputs?.maxCharacters as number + const _omitMetadataKeys = nodeData.inputs?.omitMetadataKeys as string + const output = nodeData.outputs?.output as string + const loader = new S3Loader({ bucket: bucketName, key: keyName, @@ -586,5 +834,202 @@ class S3_DocumentLoaders implements INode { return loader.load() } + + private getMimeTypeFromExtension(fileName: string): string { + const extension = path.extname(fileName).toLowerCase() + const mimeTypeMap: { [key: string]: string } = { + '.pdf': 'application/pdf', + '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + '.doc': 'application/msword', + '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + '.xls': 'application/vnd.ms-excel', + '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + '.ppt': 'application/vnd.ms-powerpoint', + '.txt': 'text/plain', + '.csv': 'text/csv', + '.html': 'text/html', + '.htm': 'text/html', + '.json': 'application/json', + '.xml': 'application/xml', + '.md': 'text/markdown' + } + return mimeTypeMap[extension] || 'application/octet-stream' + } + + private async processFile(fileInfo: any, buffer: Buffer): Promise { + try { + // Handle different file types + if (this.isTextBasedFile(fileInfo.mimeType)) { + // Process text files directly from buffer + const content = buffer.toString('utf-8') + + // Create document with metadata + return [ + { + pageContent: content, + metadata: { + source: fileInfo.webViewLink, + fileId: fileInfo.key, + fileName: fileInfo.name, + mimeType: fileInfo.mimeType, + size: fileInfo.size, + lastModified: fileInfo.lastModified, + etag: fileInfo.etag, + bucketName: fileInfo.bucketName + } + } + ] + } else if (this.isSupportedBinaryFile(fileInfo.mimeType)) { + // Process binary files using loaders + return await this.processBinaryFile(fileInfo, buffer) + } else { + console.warn(`Unsupported file type ${fileInfo.mimeType} for file ${fileInfo.name}`) + return [] + } + } catch (error) { + console.warn(`Failed to process file ${fileInfo.name}: ${error.message}`) + return [] + } + } + + private isTextBasedFile(mimeType: string): boolean { + const textBasedMimeTypes = [ + 'text/plain', + 'text/html', + 'text/css', + 'text/javascript', + 'text/csv', + 'text/xml', + 'application/json', + 'application/xml', + 'text/markdown', + 'text/x-markdown' + ] + return textBasedMimeTypes.includes(mimeType) + } + + private isSupportedBinaryFile(mimeType: string): boolean { + const supportedBinaryTypes = [ + 'application/pdf', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/msword', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'application/vnd.ms-excel', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'application/vnd.ms-powerpoint' + ] + return supportedBinaryTypes.includes(mimeType) + } + + private async processBinaryFile(fileInfo: any, buffer: Buffer): Promise { + let tempFilePath: string | null = null + + try { + // Create temporary file + tempFilePath = await this.createTempFile(buffer, fileInfo.name, fileInfo.mimeType) + + let docs: IDocument[] = [] + const mimeType = fileInfo.mimeType.toLowerCase() + + switch (mimeType) { + case 'application/pdf': { + const pdfLoader = new PDFLoader(tempFilePath, { + // @ts-ignore + pdfjs: () => import('pdf-parse/lib/pdf.js/v1.10.100/build/pdf.js') + }) + docs = await pdfLoader.load() + break + } + case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': + case 'application/msword': { + const docxLoader = new DocxLoader(tempFilePath) + docs = await docxLoader.load() + break + } + case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': + case 'application/vnd.ms-excel': { + const excelLoader = new LoadOfSheet(tempFilePath) + docs = await excelLoader.load() + break + } + case 'application/vnd.openxmlformats-officedocument.presentationml.presentation': + case 'application/vnd.ms-powerpoint': { + const pptxLoader = new PowerpointLoader(tempFilePath) + docs = await pptxLoader.load() + break + } + case 'text/csv': { + const csvLoader = new CSVLoader(tempFilePath) + docs = await csvLoader.load() + break + } + default: + throw new Error(`Unsupported binary file type: ${mimeType}`) + } + + // Add S3 metadata to each document + if (docs.length > 0) { + const s3Metadata = { + source: fileInfo.webViewLink, + fileId: fileInfo.key, + fileName: fileInfo.name, + mimeType: fileInfo.mimeType, + size: fileInfo.size, + lastModified: fileInfo.lastModified, + etag: fileInfo.etag, + bucketName: fileInfo.bucketName, + totalPages: docs.length // Total number of pages/sheets in the file + } + + return docs.map((doc, index) => ({ + ...doc, + metadata: { + ...doc.metadata, // Keep original loader metadata (page numbers, etc.) + ...s3Metadata, // Add S3 metadata + pageIndex: index // Add page/sheet index + } + })) + } + + return [] + } catch (error) { + throw new Error(`Failed to process binary file: ${error.message}`) + } finally { + // Clean up temporary file + if (tempFilePath && fsDefault.existsSync(tempFilePath)) { + try { + fsDefault.unlinkSync(tempFilePath) + } catch (e) { + console.warn(`Failed to delete temporary file: ${tempFilePath}`) + } + } + } + } + + private async createTempFile(buffer: Buffer, fileName: string, mimeType: string): Promise { + // Get appropriate file extension + let extension = path.extname(fileName) + if (!extension) { + const extensionMap: { [key: string]: string } = { + 'application/pdf': '.pdf', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx', + 'application/msword': '.doc', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx', + 'application/vnd.ms-excel': '.xls', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation': '.pptx', + 'application/vnd.ms-powerpoint': '.ppt', + 'text/csv': '.csv' + } + extension = extensionMap[mimeType] || '.tmp' + } + + // Create temporary file + const tempDir = os.tmpdir() + const tempFileName = `s3_${Date.now()}_${Math.random().toString(36).substring(7)}${extension}` + const tempFilePath = path.join(tempDir, tempFileName) + + fsDefault.writeFileSync(tempFilePath, buffer) + return tempFilePath + } } module.exports = { nodeClass: S3_DocumentLoaders } diff --git a/packages/components/nodes/sequentialagents/Agent/Agent.ts b/packages/components/nodes/sequentialagents/Agent/Agent.ts index cb4e03e7e15..0ad1372c11d 100644 --- a/packages/components/nodes/sequentialagents/Agent/Agent.ts +++ b/packages/components/nodes/sequentialagents/Agent/Agent.ts @@ -22,7 +22,13 @@ import { IStateWithMessages, ConversationHistorySelection } from '../../../src/Interface' -import { ToolCallingAgentOutputParser, AgentExecutor, SOURCE_DOCUMENTS_PREFIX, ARTIFACTS_PREFIX } from '../../../src/agents' +import { + ToolCallingAgentOutputParser, + AgentExecutor, + SOURCE_DOCUMENTS_PREFIX, + ARTIFACTS_PREFIX, + TOOL_ARGS_PREFIX +} from '../../../src/agents' import { extractOutputFromArray, getInputVariables, @@ -1041,6 +1047,17 @@ class ToolNode extends RunnableCallable } } + let toolInput + if (typeof output === 'string' && output.includes(TOOL_ARGS_PREFIX)) { + const outputArray = output.split(TOOL_ARGS_PREFIX) + output = outputArray[0] + try { + toolInput = JSON.parse(outputArray[1]) + } catch (e) { + console.error('Error parsing tool input from tool') + } + } + return new ToolMessage({ name: tool.name, content: typeof output === 'string' ? output : JSON.stringify(output), @@ -1048,11 +1065,11 @@ class ToolNode extends RunnableCallable additional_kwargs: { sourceDocuments, artifacts, - args: call.args, + args: toolInput ?? call.args, usedTools: [ { tool: tool.name ?? '', - toolInput: call.args, + toolInput: toolInput ?? call.args, toolOutput: output } ] diff --git a/packages/components/nodes/sequentialagents/ToolNode/ToolNode.ts b/packages/components/nodes/sequentialagents/ToolNode/ToolNode.ts index cc6a260cf8b..7ab010c1a69 100644 --- a/packages/components/nodes/sequentialagents/ToolNode/ToolNode.ts +++ b/packages/components/nodes/sequentialagents/ToolNode/ToolNode.ts @@ -12,7 +12,7 @@ import { import { AIMessage, AIMessageChunk, BaseMessage, ToolMessage } from '@langchain/core/messages' import { StructuredTool } from '@langchain/core/tools' import { RunnableConfig } from '@langchain/core/runnables' -import { ARTIFACTS_PREFIX, SOURCE_DOCUMENTS_PREFIX } from '../../../src/agents' +import { ARTIFACTS_PREFIX, SOURCE_DOCUMENTS_PREFIX, TOOL_ARGS_PREFIX } from '../../../src/agents' import { Document } from '@langchain/core/documents' import { DataSource } from 'typeorm' import { MessagesState, RunnableCallable, customGet, getVM } from '../commonUtils' @@ -448,6 +448,17 @@ class ToolNode ext } } + let toolInput + if (typeof output === 'string' && output.includes(TOOL_ARGS_PREFIX)) { + const outputArray = output.split(TOOL_ARGS_PREFIX) + output = outputArray[0] + try { + toolInput = JSON.parse(outputArray[1]) + } catch (e) { + console.error('Error parsing tool input from tool') + } + } + return new ToolMessage({ name: tool.name, content: typeof output === 'string' ? output : JSON.stringify(output), @@ -455,11 +466,11 @@ class ToolNode ext additional_kwargs: { sourceDocuments, artifacts, - args: call.args, + args: toolInput ?? call.args, usedTools: [ { tool: tool.name ?? '', - toolInput: call.args, + toolInput: toolInput ?? call.args, toolOutput: output } ] diff --git a/packages/components/nodes/tools/Gmail/Gmail.ts b/packages/components/nodes/tools/Gmail/Gmail.ts new file mode 100644 index 00000000000..f164680aa0e --- /dev/null +++ b/packages/components/nodes/tools/Gmail/Gmail.ts @@ -0,0 +1,630 @@ +import { convertMultiOptionsToStringArray, getCredentialData, getCredentialParam, refreshOAuth2Token } from '../../../src/utils' +import { createGmailTools } from './core' +import type { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' + +class Gmail_Tools implements INode { + label: string + name: string + version: number + type: string + icon: string + category: string + description: string + baseClasses: string[] + credential: INodeParams + inputs: INodeParams[] + + constructor() { + this.label = 'Gmail' + this.name = 'gmail' + this.version = 1.0 + this.type = 'Gmail' + this.icon = 'gmail.svg' + this.category = 'Tools' + this.description = 'Perform Gmail operations for drafts, messages, labels, and threads' + this.baseClasses = [this.type, 'Tool'] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['gmailOAuth2'] + } + this.inputs = [ + { + label: 'Type', + name: 'gmailType', + type: 'options', + options: [ + { + label: 'Drafts', + name: 'drafts' + }, + { + label: 'Messages', + name: 'messages' + }, + { + label: 'Labels', + name: 'labels' + }, + { + label: 'Threads', + name: 'threads' + } + ] + }, + // Draft Actions + { + label: 'Draft Actions', + name: 'draftActions', + type: 'multiOptions', + options: [ + { + label: 'List Drafts', + name: 'listDrafts' + }, + { + label: 'Create Draft', + name: 'createDraft' + }, + { + label: 'Get Draft', + name: 'getDraft' + }, + { + label: 'Update Draft', + name: 'updateDraft' + }, + { + label: 'Send Draft', + name: 'sendDraft' + }, + { + label: 'Delete Draft', + name: 'deleteDraft' + } + ], + show: { + gmailType: ['drafts'] + } + }, + // Message Actions + { + label: 'Message Actions', + name: 'messageActions', + type: 'multiOptions', + options: [ + { + label: 'List Messages', + name: 'listMessages' + }, + { + label: 'Get Message', + name: 'getMessage' + }, + { + label: 'Send Message', + name: 'sendMessage' + }, + { + label: 'Modify Message', + name: 'modifyMessage' + }, + { + label: 'Trash Message', + name: 'trashMessage' + }, + { + label: 'Untrash Message', + name: 'untrashMessage' + }, + { + label: 'Delete Message', + name: 'deleteMessage' + } + ], + show: { + gmailType: ['messages'] + } + }, + // Label Actions + { + label: 'Label Actions', + name: 'labelActions', + type: 'multiOptions', + options: [ + { + label: 'List Labels', + name: 'listLabels' + }, + { + label: 'Get Label', + name: 'getLabel' + }, + { + label: 'Create Label', + name: 'createLabel' + }, + { + label: 'Update Label', + name: 'updateLabel' + }, + { + label: 'Delete Label', + name: 'deleteLabel' + } + ], + show: { + gmailType: ['labels'] + } + }, + // Thread Actions + { + label: 'Thread Actions', + name: 'threadActions', + type: 'multiOptions', + options: [ + { + label: 'List Threads', + name: 'listThreads' + }, + { + label: 'Get Thread', + name: 'getThread' + }, + { + label: 'Modify Thread', + name: 'modifyThread' + }, + { + label: 'Trash Thread', + name: 'trashThread' + }, + { + label: 'Untrash Thread', + name: 'untrashThread' + }, + { + label: 'Delete Thread', + name: 'deleteThread' + } + ], + show: { + gmailType: ['threads'] + } + }, + // DRAFT PARAMETERS + // List Drafts Parameters + { + label: 'Max Results', + name: 'draftMaxResults', + type: 'number', + description: 'Maximum number of drafts to return', + default: 100, + show: { + draftActions: ['listDrafts'] + }, + additionalParams: true, + optional: true + }, + // Create Draft Parameters + { + label: 'To', + name: 'draftTo', + type: 'string', + description: 'Recipient email address(es), comma-separated', + placeholder: 'user1@example.com,user2@example.com', + show: { + draftActions: ['createDraft'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Subject', + name: 'draftSubject', + type: 'string', + description: 'Email subject', + placeholder: 'Email Subject', + show: { + draftActions: ['createDraft'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Body', + name: 'draftBody', + type: 'string', + description: 'Email body content', + placeholder: 'Email content', + rows: 4, + show: { + draftActions: ['createDraft'] + }, + additionalParams: true, + optional: true + }, + { + label: 'CC', + name: 'draftCc', + type: 'string', + description: 'CC email address(es), comma-separated', + placeholder: 'cc1@example.com,cc2@example.com', + show: { + draftActions: ['createDraft'] + }, + additionalParams: true, + optional: true + }, + { + label: 'BCC', + name: 'draftBcc', + type: 'string', + description: 'BCC email address(es), comma-separated', + placeholder: 'bcc1@example.com,bcc2@example.com', + show: { + draftActions: ['createDraft'] + }, + additionalParams: true, + optional: true + }, + // Draft ID for Get/Update/Send/Delete + { + label: 'Draft ID', + name: 'draftId', + type: 'string', + description: 'ID of the draft', + show: { + draftActions: ['getDraft', 'updateDraft', 'sendDraft', 'deleteDraft'] + }, + additionalParams: true, + optional: true + }, + // Update Draft Parameters + { + label: 'To (Update)', + name: 'draftUpdateTo', + type: 'string', + description: 'Recipient email address(es), comma-separated', + placeholder: 'user1@example.com,user2@example.com', + show: { + draftActions: ['updateDraft'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Subject (Update)', + name: 'draftUpdateSubject', + type: 'string', + description: 'Email subject', + placeholder: 'Email Subject', + show: { + draftActions: ['updateDraft'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Body (Update)', + name: 'draftUpdateBody', + type: 'string', + description: 'Email body content', + placeholder: 'Email content', + rows: 4, + show: { + draftActions: ['updateDraft'] + }, + additionalParams: true, + optional: true + }, + // MESSAGE PARAMETERS + // List Messages Parameters + { + label: 'Max Results', + name: 'messageMaxResults', + type: 'number', + description: 'Maximum number of messages to return', + default: 100, + show: { + messageActions: ['listMessages'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Query', + name: 'messageQuery', + type: 'string', + description: 'Query string for filtering results (Gmail search syntax)', + placeholder: 'is:unread from:example@gmail.com', + show: { + messageActions: ['listMessages'] + }, + additionalParams: true, + optional: true + }, + // Send Message Parameters + { + label: 'To', + name: 'messageTo', + type: 'string', + description: 'Recipient email address(es), comma-separated', + placeholder: 'user1@example.com,user2@example.com', + show: { + messageActions: ['sendMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Subject', + name: 'messageSubject', + type: 'string', + description: 'Email subject', + placeholder: 'Email Subject', + show: { + messageActions: ['sendMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Body', + name: 'messageBody', + type: 'string', + description: 'Email body content', + placeholder: 'Email content', + rows: 4, + show: { + messageActions: ['sendMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'CC', + name: 'messageCc', + type: 'string', + description: 'CC email address(es), comma-separated', + placeholder: 'cc1@example.com,cc2@example.com', + show: { + messageActions: ['sendMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'BCC', + name: 'messageBcc', + type: 'string', + description: 'BCC email address(es), comma-separated', + placeholder: 'bcc1@example.com,bcc2@example.com', + show: { + messageActions: ['sendMessage'] + }, + additionalParams: true, + optional: true + }, + // Message ID for Get/Modify/Trash/Untrash/Delete + { + label: 'Message ID', + name: 'messageId', + type: 'string', + description: 'ID of the message', + show: { + messageActions: ['getMessage', 'modifyMessage', 'trashMessage', 'untrashMessage', 'deleteMessage'] + }, + additionalParams: true, + optional: true + }, + // Message Label Modification + { + label: 'Add Label IDs', + name: 'messageAddLabelIds', + type: 'string', + description: 'Comma-separated label IDs to add', + placeholder: 'INBOX,STARRED', + show: { + messageActions: ['modifyMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Remove Label IDs', + name: 'messageRemoveLabelIds', + type: 'string', + description: 'Comma-separated label IDs to remove', + placeholder: 'UNREAD,SPAM', + show: { + messageActions: ['modifyMessage'] + }, + additionalParams: true, + optional: true + }, + // LABEL PARAMETERS + // Create Label Parameters + { + label: 'Label Name', + name: 'labelName', + type: 'string', + description: 'Name of the label', + placeholder: 'Important', + show: { + labelActions: ['createLabel', 'updateLabel'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Label Color', + name: 'labelColor', + type: 'string', + description: 'Color of the label (hex color code)', + placeholder: '#ff0000', + show: { + labelActions: ['createLabel', 'updateLabel'] + }, + additionalParams: true, + optional: true + }, + // Label ID for Get/Update/Delete + { + label: 'Label ID', + name: 'labelId', + type: 'string', + description: 'ID of the label', + show: { + labelActions: ['getLabel', 'updateLabel', 'deleteLabel'] + }, + additionalParams: true, + optional: true + }, + // THREAD PARAMETERS + // List Threads Parameters + { + label: 'Max Results', + name: 'threadMaxResults', + type: 'number', + description: 'Maximum number of threads to return', + default: 100, + show: { + threadActions: ['listThreads'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Query', + name: 'threadQuery', + type: 'string', + description: 'Query string for filtering results (Gmail search syntax)', + placeholder: 'is:unread from:example@gmail.com', + show: { + threadActions: ['listThreads'] + }, + additionalParams: true, + optional: true + }, + // Thread ID for Get/Modify/Trash/Untrash/Delete + { + label: 'Thread ID', + name: 'threadId', + type: 'string', + description: 'ID of the thread', + show: { + threadActions: ['getThread', 'modifyThread', 'trashThread', 'untrashThread', 'deleteThread'] + }, + additionalParams: true, + optional: true + }, + // Thread Label Modification + { + label: 'Add Label IDs', + name: 'threadAddLabelIds', + type: 'string', + description: 'Comma-separated label IDs to add', + placeholder: 'INBOX,STARRED', + show: { + threadActions: ['modifyThread'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Remove Label IDs', + name: 'threadRemoveLabelIds', + type: 'string', + description: 'Comma-separated label IDs to remove', + placeholder: 'UNREAD,SPAM', + show: { + threadActions: ['modifyThread'] + }, + additionalParams: true, + optional: true + } + ] + } + + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + let credentialData = await getCredentialData(nodeData.credential ?? '', options) + credentialData = await refreshOAuth2Token(nodeData.credential ?? '', credentialData, options) + const accessToken = getCredentialParam('access_token', credentialData, nodeData) + + if (!accessToken) { + throw new Error('No access token found in credential') + } + + // Get all actions based on type + const gmailType = nodeData.inputs?.gmailType as string + let actions: string[] = [] + + if (gmailType === 'drafts') { + actions = convertMultiOptionsToStringArray(nodeData.inputs?.draftActions) + } else if (gmailType === 'messages') { + actions = convertMultiOptionsToStringArray(nodeData.inputs?.messageActions) + } else if (gmailType === 'labels') { + actions = convertMultiOptionsToStringArray(nodeData.inputs?.labelActions) + } else if (gmailType === 'threads') { + actions = convertMultiOptionsToStringArray(nodeData.inputs?.threadActions) + } + + const defaultParams = this.transformNodeInputsToToolArgs(nodeData) + + // Create and return tools based on selected actions + const tools = createGmailTools({ + actions, + accessToken, + defaultParams + }) + + return tools + } + + transformNodeInputsToToolArgs(nodeData: INodeData): Record { + // Collect default parameters from inputs + const defaultParams: Record = {} + + // Draft parameters + if (nodeData.inputs?.draftMaxResults) defaultParams.draftMaxResults = nodeData.inputs.draftMaxResults + if (nodeData.inputs?.draftTo) defaultParams.draftTo = nodeData.inputs.draftTo + if (nodeData.inputs?.draftSubject) defaultParams.draftSubject = nodeData.inputs.draftSubject + if (nodeData.inputs?.draftBody) defaultParams.draftBody = nodeData.inputs.draftBody + if (nodeData.inputs?.draftCc) defaultParams.draftCc = nodeData.inputs.draftCc + if (nodeData.inputs?.draftBcc) defaultParams.draftBcc = nodeData.inputs.draftBcc + if (nodeData.inputs?.draftId) defaultParams.draftId = nodeData.inputs.draftId + if (nodeData.inputs?.draftUpdateTo) defaultParams.draftUpdateTo = nodeData.inputs.draftUpdateTo + if (nodeData.inputs?.draftUpdateSubject) defaultParams.draftUpdateSubject = nodeData.inputs.draftUpdateSubject + if (nodeData.inputs?.draftUpdateBody) defaultParams.draftUpdateBody = nodeData.inputs.draftUpdateBody + + // Message parameters + if (nodeData.inputs?.messageMaxResults) defaultParams.messageMaxResults = nodeData.inputs.messageMaxResults + if (nodeData.inputs?.messageQuery) defaultParams.messageQuery = nodeData.inputs.messageQuery + if (nodeData.inputs?.messageTo) defaultParams.messageTo = nodeData.inputs.messageTo + if (nodeData.inputs?.messageSubject) defaultParams.messageSubject = nodeData.inputs.messageSubject + if (nodeData.inputs?.messageBody) defaultParams.messageBody = nodeData.inputs.messageBody + if (nodeData.inputs?.messageCc) defaultParams.messageCc = nodeData.inputs.messageCc + if (nodeData.inputs?.messageBcc) defaultParams.messageBcc = nodeData.inputs.messageBcc + if (nodeData.inputs?.messageId) defaultParams.messageId = nodeData.inputs.messageId + if (nodeData.inputs?.messageAddLabelIds) defaultParams.messageAddLabelIds = nodeData.inputs.messageAddLabelIds + if (nodeData.inputs?.messageRemoveLabelIds) defaultParams.messageRemoveLabelIds = nodeData.inputs.messageRemoveLabelIds + + // Label parameters + if (nodeData.inputs?.labelName) defaultParams.labelName = nodeData.inputs.labelName + if (nodeData.inputs?.labelColor) defaultParams.labelColor = nodeData.inputs.labelColor + if (nodeData.inputs?.labelId) defaultParams.labelId = nodeData.inputs.labelId + + // Thread parameters + if (nodeData.inputs?.threadMaxResults) defaultParams.threadMaxResults = nodeData.inputs.threadMaxResults + if (nodeData.inputs?.threadQuery) defaultParams.threadQuery = nodeData.inputs.threadQuery + if (nodeData.inputs?.threadId) defaultParams.threadId = nodeData.inputs.threadId + if (nodeData.inputs?.threadAddLabelIds) defaultParams.threadAddLabelIds = nodeData.inputs.threadAddLabelIds + if (nodeData.inputs?.threadRemoveLabelIds) defaultParams.threadRemoveLabelIds = nodeData.inputs.threadRemoveLabelIds + + return defaultParams + } +} + +module.exports = { nodeClass: Gmail_Tools } diff --git a/packages/components/nodes/tools/Gmail/core.ts b/packages/components/nodes/tools/Gmail/core.ts new file mode 100644 index 00000000000..14d242c84e1 --- /dev/null +++ b/packages/components/nodes/tools/Gmail/core.ts @@ -0,0 +1,1199 @@ +import { z } from 'zod' +import fetch from 'node-fetch' +import { DynamicStructuredTool } from '../OpenAPIToolkit/core' +import { TOOL_ARGS_PREFIX } from '../../../src/agents' + +export const desc = `Use this when you want to access Gmail API for managing drafts, messages, labels, and threads` + +export interface Headers { + [key: string]: string +} + +export interface Body { + [key: string]: any +} + +export interface RequestParameters { + headers?: Headers + body?: Body + url?: string + description?: string + name?: string + actions?: string[] + accessToken?: string + defaultParams?: any +} + +// Define schemas for different Gmail operations +const ListSchema = z.object({ + maxResults: z.number().optional().default(100).describe('Maximum number of results to return'), + query: z.string().optional().describe('Query string for filtering results (Gmail search syntax)') +}) + +const CreateDraftSchema = z.object({ + to: z.string().describe('Recipient email address(es), comma-separated'), + subject: z.string().optional().describe('Email subject'), + body: z.string().optional().describe('Email body content'), + cc: z.string().optional().describe('CC email address(es), comma-separated'), + bcc: z.string().optional().describe('BCC email address(es), comma-separated') +}) + +const SendMessageSchema = z.object({ + to: z.string().describe('Recipient email address(es), comma-separated'), + subject: z.string().optional().describe('Email subject'), + body: z.string().optional().describe('Email body content'), + cc: z.string().optional().describe('CC email address(es), comma-separated'), + bcc: z.string().optional().describe('BCC email address(es), comma-separated') +}) + +const GetByIdSchema = z.object({ + id: z.string().describe('ID of the resource') +}) + +const ModifySchema = z.object({ + id: z.string().describe('ID of the resource'), + addLabelIds: z.array(z.string()).optional().describe('Label IDs to add'), + removeLabelIds: z.array(z.string()).optional().describe('Label IDs to remove') +}) + +const CreateLabelSchema = z.object({ + labelName: z.string().describe('Name of the label'), + labelColor: z.string().optional().describe('Color of the label (hex color code)') +}) + +class BaseGmailTool extends DynamicStructuredTool { + protected accessToken: string = '' + + constructor(args: any) { + super(args) + this.accessToken = args.accessToken ?? '' + } + + async makeGmailRequest(url: string, method: string = 'GET', body?: any, params?: any): Promise { + const headers = { + Authorization: `Bearer ${this.accessToken}`, + 'Content-Type': 'application/json', + ...this.headers + } + + const response = await fetch(url, { + method, + headers, + body: body ? JSON.stringify(body) : undefined + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`Gmail API Error ${response.status}: ${response.statusText} - ${errorText}`) + } + + const data = await response.text() + return data + TOOL_ARGS_PREFIX + JSON.stringify(params) + } + + createMimeMessage(to: string, subject?: string, body?: string, cc?: string, bcc?: string): string { + let message = '' + + message += `To: ${to}\r\n` + if (cc) message += `Cc: ${cc}\r\n` + if (bcc) message += `Bcc: ${bcc}\r\n` + if (subject) message += `Subject: ${subject}\r\n` + message += `MIME-Version: 1.0\r\n` + message += `Content-Type: text/html; charset=utf-8\r\n` + message += `Content-Transfer-Encoding: base64\r\n\r\n` + + if (body) { + message += Buffer.from(body, 'utf-8').toString('base64') + } + + return Buffer.from(message).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') + } +} + +// Draft Tools +class ListDraftsTool extends BaseGmailTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'list_drafts', + description: 'List drafts in Gmail mailbox', + schema: ListSchema, + baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/drafts', + method: 'GET', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const queryParams = new URLSearchParams() + + if (params.maxResults) queryParams.append('maxResults', params.maxResults.toString()) + if (params.query) queryParams.append('q', params.query) + + const url = `https://gmail.googleapis.com/gmail/v1/users/me/drafts?${queryParams.toString()}` + + try { + const response = await this.makeGmailRequest(url, 'GET', undefined, params) + return response + } catch (error) { + return `Error listing drafts: ${error}` + } + } +} + +class CreateDraftTool extends BaseGmailTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'create_draft', + description: 'Create a new draft in Gmail', + schema: CreateDraftSchema, + baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/drafts', + method: 'POST', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const raw = this.createMimeMessage(params.to, params.subject, params.body, params.cc, params.bcc) + const draftData = { + message: { + raw: raw + } + } + + const url = 'https://gmail.googleapis.com/gmail/v1/users/me/drafts' + const response = await this.makeGmailRequest(url, 'POST', draftData, params) + return response + } catch (error) { + return `Error creating draft: ${error}` + } + } +} + +class GetDraftTool extends BaseGmailTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'get_draft', + description: 'Get a specific draft from Gmail', + schema: GetByIdSchema, + baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/drafts', + method: 'GET', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const draftId = params.id || params.draftId + + if (!draftId) { + return 'Error: Draft ID is required' + } + + try { + const url = `https://gmail.googleapis.com/gmail/v1/users/me/drafts/${draftId}` + const response = await this.makeGmailRequest(url, 'GET', undefined, params) + return response + } catch (error) { + return `Error getting draft: ${error}` + } + } +} + +class UpdateDraftTool extends BaseGmailTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'update_draft', + description: 'Update a specific draft in Gmail', + schema: CreateDraftSchema, + baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/drafts', + method: 'PUT', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const draftId = params.id || params.draftId + + if (!draftId) { + return 'Error: Draft ID is required' + } + + try { + const raw = this.createMimeMessage(params.to, params.subject, params.body, params.cc, params.bcc) + const draftData = { + message: { + raw: raw + } + } + + const url = `https://gmail.googleapis.com/gmail/v1/users/me/drafts/${draftId}` + const response = await this.makeGmailRequest(url, 'PUT', draftData, params) + return response + } catch (error) { + return `Error updating draft: ${error}` + } + } +} + +class SendDraftTool extends BaseGmailTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'send_draft', + description: 'Send a specific draft from Gmail', + schema: GetByIdSchema, + baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/drafts/send', + method: 'POST', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const draftId = params.id || params.draftId + + if (!draftId) { + return 'Error: Draft ID is required' + } + + try { + const url = 'https://gmail.googleapis.com/gmail/v1/users/me/drafts/send' + const response = await this.makeGmailRequest(url, 'POST', { id: draftId }, params) + return response + } catch (error) { + return `Error sending draft: ${error}` + } + } +} + +class DeleteDraftTool extends BaseGmailTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'delete_draft', + description: 'Delete a specific draft from Gmail', + schema: GetByIdSchema, + baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/drafts', + method: 'DELETE', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const draftId = params.id || params.draftId + + if (!draftId) { + return 'Error: Draft ID is required' + } + + try { + const url = `https://gmail.googleapis.com/gmail/v1/users/me/drafts/${draftId}` + await this.makeGmailRequest(url, 'DELETE', undefined, params) + return `Draft ${draftId} deleted successfully` + } catch (error) { + return `Error deleting draft: ${error}` + } + } +} + +// Message Tools +class ListMessagesTool extends BaseGmailTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'list_messages', + description: 'List messages in Gmail mailbox', + schema: ListSchema, + baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/messages', + method: 'GET', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const queryParams = new URLSearchParams() + + if (params.maxResults) queryParams.append('maxResults', params.maxResults.toString()) + if (params.query) queryParams.append('q', params.query) + + const url = `https://gmail.googleapis.com/gmail/v1/users/me/messages?${queryParams.toString()}` + + try { + const response = await this.makeGmailRequest(url, 'GET', undefined, params) + return response + } catch (error) { + return `Error listing messages: ${error}` + } + } +} + +class GetMessageTool extends BaseGmailTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'get_message', + description: 'Get a specific message from Gmail', + schema: GetByIdSchema, + baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/messages', + method: 'GET', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const messageId = params.id || params.messageId + + if (!messageId) { + return 'Error: Message ID is required' + } + + try { + const url = `https://gmail.googleapis.com/gmail/v1/users/me/messages/${messageId}` + const response = await this.makeGmailRequest(url, 'GET', undefined, params) + return response + } catch (error) { + return `Error getting message: ${error}` + } + } +} + +class SendMessageTool extends BaseGmailTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'send_message', + description: 'Send a new message via Gmail', + schema: SendMessageSchema, + baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/messages/send', + method: 'POST', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const raw = this.createMimeMessage(params.to, params.subject, params.body, params.cc, params.bcc) + const messageData = { + raw: raw + } + + const url = 'https://gmail.googleapis.com/gmail/v1/users/me/messages/send' + const response = await this.makeGmailRequest(url, 'POST', messageData, params) + return response + } catch (error) { + return `Error sending message: ${error}` + } + } +} + +class ModifyMessageTool extends BaseGmailTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'modify_message', + description: 'Modify labels on a message in Gmail', + schema: ModifySchema, + baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/messages', + method: 'POST', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const messageId = params.id || params.messageId + + if (!messageId) { + return 'Error: Message ID is required' + } + + try { + const modifyData: any = {} + if (params.addLabelIds && params.addLabelIds.length > 0) { + modifyData.addLabelIds = params.addLabelIds + } + if (params.removeLabelIds && params.removeLabelIds.length > 0) { + modifyData.removeLabelIds = params.removeLabelIds + } + + const url = `https://gmail.googleapis.com/gmail/v1/users/me/messages/${messageId}/modify` + const response = await this.makeGmailRequest(url, 'POST', modifyData, params) + return response + } catch (error) { + return `Error modifying message: ${error}` + } + } +} + +class TrashMessageTool extends BaseGmailTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'trash_message', + description: 'Move a message to trash in Gmail', + schema: GetByIdSchema, + baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/messages', + method: 'POST', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const messageId = params.id || params.messageId + + if (!messageId) { + return 'Error: Message ID is required' + } + + try { + const url = `https://gmail.googleapis.com/gmail/v1/users/me/messages/${messageId}/trash` + const response = await this.makeGmailRequest(url, 'POST', undefined, params) + return response + } catch (error) { + return `Error moving message to trash: ${error}` + } + } +} + +class UntrashMessageTool extends BaseGmailTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'untrash_message', + description: 'Remove a message from trash in Gmail', + schema: GetByIdSchema, + baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/messages', + method: 'POST', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const messageId = params.id || params.messageId + + if (!messageId) { + return 'Error: Message ID is required' + } + + try { + const url = `https://gmail.googleapis.com/gmail/v1/users/me/messages/${messageId}/untrash` + const response = await this.makeGmailRequest(url, 'POST', undefined, params) + return response + } catch (error) { + return `Error removing message from trash: ${error}` + } + } +} + +class DeleteMessageTool extends BaseGmailTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'delete_message', + description: 'Permanently delete a message from Gmail', + schema: GetByIdSchema, + baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/messages', + method: 'DELETE', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const messageId = params.id || params.messageId + + if (!messageId) { + return 'Error: Message ID is required' + } + + try { + const url = `https://gmail.googleapis.com/gmail/v1/users/me/messages/${messageId}` + await this.makeGmailRequest(url, 'DELETE', undefined, params) + return `Message ${messageId} deleted successfully` + } catch (error) { + return `Error deleting message: ${error}` + } + } +} + +// Label Tools +class ListLabelsTool extends BaseGmailTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'list_labels', + description: 'List labels in Gmail mailbox', + schema: z.object({}), + baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/labels', + method: 'GET', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(): Promise { + try { + const url = 'https://gmail.googleapis.com/gmail/v1/users/me/labels' + const response = await this.makeGmailRequest(url, 'GET', undefined, {}) + return response + } catch (error) { + return `Error listing labels: ${error}` + } + } +} + +class GetLabelTool extends BaseGmailTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'get_label', + description: 'Get a specific label from Gmail', + schema: GetByIdSchema, + baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/labels', + method: 'GET', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const labelId = params.id || params.labelId + + if (!labelId) { + return 'Error: Label ID is required' + } + + try { + const url = `https://gmail.googleapis.com/gmail/v1/users/me/labels/${labelId}` + const response = await this.makeGmailRequest(url, 'GET', undefined, params) + return response + } catch (error) { + return `Error getting label: ${error}` + } + } +} + +class CreateLabelTool extends BaseGmailTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'create_label', + description: 'Create a new label in Gmail', + schema: CreateLabelSchema, + baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/labels', + method: 'POST', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + if (!params.labelName) { + return 'Error: Label name is required' + } + + try { + const labelData: any = { + name: params.labelName, + labelListVisibility: 'labelShow', + messageListVisibility: 'show' + } + + if (params.labelColor) { + labelData.color = { + backgroundColor: params.labelColor + } + } + + const url = 'https://gmail.googleapis.com/gmail/v1/users/me/labels' + const response = await this.makeGmailRequest(url, 'POST', labelData, params) + return response + } catch (error) { + return `Error creating label: ${error}` + } + } +} + +class UpdateLabelTool extends BaseGmailTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'update_label', + description: 'Update a label in Gmail', + schema: CreateLabelSchema, + baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/labels', + method: 'PUT', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const labelId = params.labelId + + if (!labelId) { + return 'Error: Label ID is required' + } + + try { + const labelData: any = {} + if (params.labelName) { + labelData.name = params.labelName + } + if (params.labelColor) { + labelData.color = { + backgroundColor: params.labelColor + } + } + + const url = `https://gmail.googleapis.com/gmail/v1/users/me/labels/${labelId}` + const response = await this.makeGmailRequest(url, 'PUT', labelData, params) + return response + } catch (error) { + return `Error updating label: ${error}` + } + } +} + +class DeleteLabelTool extends BaseGmailTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'delete_label', + description: 'Delete a label from Gmail', + schema: GetByIdSchema, + baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/labels', + method: 'DELETE', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const labelId = params.id || params.labelId + + if (!labelId) { + return 'Error: Label ID is required' + } + + try { + const url = `https://gmail.googleapis.com/gmail/v1/users/me/labels/${labelId}` + await this.makeGmailRequest(url, 'DELETE', undefined, params) + return `Label ${labelId} deleted successfully` + } catch (error) { + return `Error deleting label: ${error}` + } + } +} + +// Thread Tools +class ListThreadsTool extends BaseGmailTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'list_threads', + description: 'List threads in Gmail mailbox', + schema: ListSchema, + baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/threads', + method: 'GET', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const queryParams = new URLSearchParams() + + if (params.maxResults) queryParams.append('maxResults', params.maxResults.toString()) + if (params.query) queryParams.append('q', params.query) + + const url = `https://gmail.googleapis.com/gmail/v1/users/me/threads?${queryParams.toString()}` + + try { + const response = await this.makeGmailRequest(url, 'GET', undefined, params) + return response + } catch (error) { + return `Error listing threads: ${error}` + } + } +} + +class GetThreadTool extends BaseGmailTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'get_thread', + description: 'Get a specific thread from Gmail', + schema: GetByIdSchema, + baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/threads', + method: 'GET', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const threadId = params.id || params.threadId + + if (!threadId) { + return 'Error: Thread ID is required' + } + + try { + const url = `https://gmail.googleapis.com/gmail/v1/users/me/threads/${threadId}` + const response = await this.makeGmailRequest(url, 'GET', undefined, params) + return response + } catch (error) { + return `Error getting thread: ${error}` + } + } +} + +class ModifyThreadTool extends BaseGmailTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'modify_thread', + description: 'Modify labels on a thread in Gmail', + schema: ModifySchema, + baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/threads', + method: 'POST', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const threadId = params.id || params.threadId + + if (!threadId) { + return 'Error: Thread ID is required' + } + + try { + const modifyData: any = {} + if (params.addLabelIds && params.addLabelIds.length > 0) { + modifyData.addLabelIds = params.addLabelIds + } + if (params.removeLabelIds && params.removeLabelIds.length > 0) { + modifyData.removeLabelIds = params.removeLabelIds + } + + const url = `https://gmail.googleapis.com/gmail/v1/users/me/threads/${threadId}/modify` + const response = await this.makeGmailRequest(url, 'POST', modifyData, params) + return response + } catch (error) { + return `Error modifying thread: ${error}` + } + } +} + +class TrashThreadTool extends BaseGmailTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'trash_thread', + description: 'Move a thread to trash in Gmail', + schema: GetByIdSchema, + baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/threads', + method: 'POST', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const threadId = params.id || params.threadId + + if (!threadId) { + return 'Error: Thread ID is required' + } + + try { + const url = `https://gmail.googleapis.com/gmail/v1/users/me/threads/${threadId}/trash` + const response = await this.makeGmailRequest(url, 'POST', undefined, params) + return response + } catch (error) { + return `Error moving thread to trash: ${error}` + } + } +} + +class UntrashThreadTool extends BaseGmailTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'untrash_thread', + description: 'Remove a thread from trash in Gmail', + schema: GetByIdSchema, + baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/threads', + method: 'POST', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const threadId = params.id || params.threadId + + if (!threadId) { + return 'Error: Thread ID is required' + } + + try { + const url = `https://gmail.googleapis.com/gmail/v1/users/me/threads/${threadId}/untrash` + const response = await this.makeGmailRequest(url, 'POST', undefined, params) + return response + } catch (error) { + return `Error removing thread from trash: ${error}` + } + } +} + +class DeleteThreadTool extends BaseGmailTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'delete_thread', + description: 'Permanently delete a thread from Gmail', + schema: GetByIdSchema, + baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/threads', + method: 'DELETE', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const threadId = params.id || params.threadId + + if (!threadId) { + return 'Error: Thread ID is required' + } + + try { + const url = `https://gmail.googleapis.com/gmail/v1/users/me/threads/${threadId}` + await this.makeGmailRequest(url, 'DELETE', undefined, params) + return `Thread ${threadId} deleted successfully` + } catch (error) { + return `Error deleting thread: ${error}` + } + } +} + +export const createGmailTools = (args?: RequestParameters): DynamicStructuredTool[] => { + const tools: DynamicStructuredTool[] = [] + const actions = args?.actions || [] + const accessToken = args?.accessToken || '' + const defaultParams = args?.defaultParams || {} + + // Draft tools + if (actions.includes('listDrafts')) { + tools.push( + new ListDraftsTool({ + accessToken, + defaultParams: defaultParams.listDrafts + }) + ) + } + + if (actions.includes('createDraft')) { + tools.push( + new CreateDraftTool({ + accessToken, + defaultParams: defaultParams.createDraft + }) + ) + } + + if (actions.includes('getDraft')) { + tools.push( + new GetDraftTool({ + accessToken, + defaultParams: defaultParams.getDraft + }) + ) + } + + if (actions.includes('updateDraft')) { + tools.push( + new UpdateDraftTool({ + accessToken, + defaultParams: defaultParams.updateDraft + }) + ) + } + + if (actions.includes('sendDraft')) { + tools.push( + new SendDraftTool({ + accessToken, + defaultParams: defaultParams.sendDraft + }) + ) + } + + if (actions.includes('deleteDraft')) { + tools.push( + new DeleteDraftTool({ + accessToken, + defaultParams: defaultParams.deleteDraft + }) + ) + } + + // Message tools + if (actions.includes('listMessages')) { + tools.push( + new ListMessagesTool({ + accessToken, + defaultParams: defaultParams.listMessages + }) + ) + } + + if (actions.includes('getMessage')) { + tools.push( + new GetMessageTool({ + accessToken, + defaultParams: defaultParams.getMessage + }) + ) + } + + if (actions.includes('sendMessage')) { + tools.push( + new SendMessageTool({ + accessToken, + defaultParams: defaultParams.sendMessage + }) + ) + } + + if (actions.includes('modifyMessage')) { + tools.push( + new ModifyMessageTool({ + accessToken, + defaultParams: defaultParams.modifyMessage + }) + ) + } + + if (actions.includes('trashMessage')) { + tools.push( + new TrashMessageTool({ + accessToken, + defaultParams: defaultParams.trashMessage + }) + ) + } + + if (actions.includes('untrashMessage')) { + tools.push( + new UntrashMessageTool({ + accessToken, + defaultParams: defaultParams.untrashMessage + }) + ) + } + + if (actions.includes('deleteMessage')) { + tools.push( + new DeleteMessageTool({ + accessToken, + defaultParams: defaultParams.deleteMessage + }) + ) + } + + // Label tools + if (actions.includes('listLabels')) { + tools.push( + new ListLabelsTool({ + accessToken, + defaultParams: defaultParams.listLabels + }) + ) + } + + if (actions.includes('getLabel')) { + tools.push( + new GetLabelTool({ + accessToken, + defaultParams: defaultParams.getLabel + }) + ) + } + + if (actions.includes('createLabel')) { + tools.push( + new CreateLabelTool({ + accessToken, + defaultParams: defaultParams.createLabel + }) + ) + } + + if (actions.includes('updateLabel')) { + tools.push( + new UpdateLabelTool({ + accessToken, + defaultParams: defaultParams.updateLabel + }) + ) + } + + if (actions.includes('deleteLabel')) { + tools.push( + new DeleteLabelTool({ + accessToken, + defaultParams: defaultParams.deleteLabel + }) + ) + } + + // Thread tools + if (actions.includes('listThreads')) { + tools.push( + new ListThreadsTool({ + accessToken, + defaultParams: defaultParams.listThreads + }) + ) + } + + if (actions.includes('getThread')) { + tools.push( + new GetThreadTool({ + accessToken, + defaultParams: defaultParams.getThread + }) + ) + } + + if (actions.includes('modifyThread')) { + tools.push( + new ModifyThreadTool({ + accessToken, + defaultParams: defaultParams.modifyThread + }) + ) + } + + if (actions.includes('trashThread')) { + tools.push( + new TrashThreadTool({ + accessToken, + defaultParams: defaultParams.trashThread + }) + ) + } + + if (actions.includes('untrashThread')) { + tools.push( + new UntrashThreadTool({ + accessToken, + defaultParams: defaultParams.untrashThread + }) + ) + } + + if (actions.includes('deleteThread')) { + tools.push( + new DeleteThreadTool({ + accessToken, + defaultParams: defaultParams.deleteThread + }) + ) + } + + return tools +} diff --git a/packages/components/nodes/tools/Gmail/gmail.svg b/packages/components/nodes/tools/Gmail/gmail.svg new file mode 100644 index 00000000000..3dceea456d6 --- /dev/null +++ b/packages/components/nodes/tools/Gmail/gmail.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/tools/GoogleCalendar/GoogleCalendar.ts b/packages/components/nodes/tools/GoogleCalendar/GoogleCalendar.ts new file mode 100644 index 00000000000..38a0a62910f --- /dev/null +++ b/packages/components/nodes/tools/GoogleCalendar/GoogleCalendar.ts @@ -0,0 +1,621 @@ +import { convertMultiOptionsToStringArray, getCredentialData, getCredentialParam, refreshOAuth2Token } from '../../../src/utils' +import { createGoogleCalendarTools } from './core' +import type { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' + +class GoogleCalendar_Tools implements INode { + label: string + name: string + version: number + type: string + icon: string + category: string + description: string + baseClasses: string[] + credential: INodeParams + inputs: INodeParams[] + + constructor() { + this.label = 'Google Calendar' + this.name = 'googleCalendarTool' + this.version = 1.0 + this.type = 'GoogleCalendar' + this.icon = 'google-calendar.svg' + this.category = 'Tools' + this.description = 'Perform Google Calendar operations such as managing events, calendars, and checking availability' + this.baseClasses = ['Tool'] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['googleCalendarOAuth2'] + } + this.inputs = [ + { + label: 'Type', + name: 'calendarType', + type: 'options', + description: 'Type of Google Calendar operation', + options: [ + { + label: 'Event', + name: 'event' + }, + { + label: 'Calendar', + name: 'calendar' + }, + { + label: 'Freebusy', + name: 'freebusy' + } + ] + }, + // Event Actions + { + label: 'Event Actions', + name: 'eventActions', + type: 'multiOptions', + description: 'Actions to perform', + options: [ + { + label: 'List Events', + name: 'listEvents' + }, + { + label: 'Create Event', + name: 'createEvent' + }, + { + label: 'Get Event', + name: 'getEvent' + }, + { + label: 'Update Event', + name: 'updateEvent' + }, + { + label: 'Delete Event', + name: 'deleteEvent' + }, + { + label: 'Quick Add Event', + name: 'quickAddEvent' + } + ], + show: { + calendarType: ['event'] + } + }, + // Calendar Actions + { + label: 'Calendar Actions', + name: 'calendarActions', + type: 'multiOptions', + description: 'Actions to perform', + options: [ + { + label: 'List Calendars', + name: 'listCalendars' + }, + { + label: 'Create Calendar', + name: 'createCalendar' + }, + { + label: 'Get Calendar', + name: 'getCalendar' + }, + { + label: 'Update Calendar', + name: 'updateCalendar' + }, + { + label: 'Delete Calendar', + name: 'deleteCalendar' + }, + { + label: 'Clear Calendar', + name: 'clearCalendar' + } + ], + show: { + calendarType: ['calendar'] + } + }, + // Freebusy Actions + { + label: 'Freebusy Actions', + name: 'freebusyActions', + type: 'multiOptions', + description: 'Actions to perform', + options: [ + { + label: 'Query Freebusy', + name: 'queryFreebusy' + } + ], + show: { + calendarType: ['freebusy'] + } + }, + // Event Parameters + { + label: 'Calendar ID', + name: 'calendarId', + type: 'string', + description: 'Calendar ID (use "primary" for primary calendar)', + default: 'primary', + show: { + calendarType: ['event'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Event ID', + name: 'eventId', + type: 'string', + description: 'Event ID for operations on specific events', + show: { + eventActions: ['getEvent', 'updateEvent', 'deleteEvent'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Summary', + name: 'summary', + type: 'string', + description: 'Event title/summary', + show: { + eventActions: ['createEvent', 'updateEvent'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Description', + name: 'description', + type: 'string', + description: 'Event description', + show: { + eventActions: ['createEvent', 'updateEvent'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Location', + name: 'location', + type: 'string', + description: 'Event location', + show: { + eventActions: ['createEvent', 'updateEvent'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Start Date Time', + name: 'startDateTime', + type: 'string', + description: 'Event start time (ISO 8601 format: 2023-12-25T10:00:00)', + show: { + eventActions: ['createEvent', 'updateEvent'] + }, + additionalParams: true, + optional: true + }, + { + label: 'End Date Time', + name: 'endDateTime', + type: 'string', + description: 'Event end time (ISO 8601 format: 2023-12-25T11:00:00)', + show: { + eventActions: ['createEvent', 'updateEvent'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Time Zone', + name: 'timeZone', + type: 'string', + description: 'Time zone (e.g., America/New_York)', + show: { + eventActions: ['createEvent', 'updateEvent'] + }, + additionalParams: true, + optional: true + }, + { + label: 'All Day Event', + name: 'allDay', + type: 'boolean', + description: 'Whether this is an all-day event', + show: { + eventActions: ['createEvent', 'updateEvent'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Start Date', + name: 'startDate', + type: 'string', + description: 'Start date for all-day events (YYYY-MM-DD format)', + show: { + eventActions: ['createEvent', 'updateEvent'] + }, + additionalParams: true, + optional: true + }, + { + label: 'End Date', + name: 'endDate', + type: 'string', + description: 'End date for all-day events (YYYY-MM-DD format)', + show: { + eventActions: ['createEvent', 'updateEvent'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Attendees', + name: 'attendees', + type: 'string', + description: 'Comma-separated list of attendee emails', + show: { + eventActions: ['createEvent', 'updateEvent'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Recurrence Rules', + name: 'recurrence', + type: 'string', + description: 'Recurrence rules (RRULE format)', + show: { + eventActions: ['createEvent', 'updateEvent'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Reminder Minutes', + name: 'reminderMinutes', + type: 'number', + description: 'Minutes before event to send reminder', + show: { + eventActions: ['createEvent', 'updateEvent'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Visibility', + name: 'visibility', + type: 'options', + description: 'Event visibility', + options: [ + { label: 'Default', name: 'default' }, + { label: 'Public', name: 'public' }, + { label: 'Private', name: 'private' }, + { label: 'Confidential', name: 'confidential' } + ], + show: { + eventActions: ['createEvent', 'updateEvent'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Quick Add Text', + name: 'quickAddText', + type: 'string', + description: 'Natural language text for quick event creation (e.g., "Lunch with John tomorrow at 12pm")', + show: { + eventActions: ['quickAddEvent'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Time Min', + name: 'timeMin', + type: 'string', + description: 'Lower bound for event search (ISO 8601 format)', + show: { + eventActions: ['listEvents'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Time Max', + name: 'timeMax', + type: 'string', + description: 'Upper bound for event search (ISO 8601 format)', + show: { + eventActions: ['listEvents'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Max Results', + name: 'maxResults', + type: 'number', + description: 'Maximum number of events to return', + default: 250, + show: { + eventActions: ['listEvents'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Single Events', + name: 'singleEvents', + type: 'boolean', + description: 'Whether to expand recurring events into instances', + default: true, + show: { + eventActions: ['listEvents'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Order By', + name: 'orderBy', + type: 'options', + description: 'Order of events returned', + options: [ + { label: 'Start Time', name: 'startTime' }, + { label: 'Updated', name: 'updated' } + ], + show: { + eventActions: ['listEvents'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Query', + name: 'query', + type: 'string', + description: 'Free text search terms', + show: { + eventActions: ['listEvents'] + }, + additionalParams: true, + optional: true + }, + // Calendar Parameters + { + label: 'Calendar ID', + name: 'calendarIdForCalendar', + type: 'string', + description: 'Calendar ID for operations on specific calendars', + show: { + calendarActions: ['getCalendar', 'updateCalendar', 'deleteCalendar', 'clearCalendar'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Calendar Summary', + name: 'calendarSummary', + type: 'string', + description: 'Calendar title/name', + show: { + calendarActions: ['createCalendar', 'updateCalendar'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Calendar Description', + name: 'calendarDescription', + type: 'string', + description: 'Calendar description', + show: { + calendarActions: ['createCalendar', 'updateCalendar'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Calendar Location', + name: 'calendarLocation', + type: 'string', + description: 'Calendar location', + show: { + calendarActions: ['createCalendar', 'updateCalendar'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Calendar Time Zone', + name: 'calendarTimeZone', + type: 'string', + description: 'Calendar time zone (e.g., America/New_York)', + show: { + calendarActions: ['createCalendar', 'updateCalendar'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Show Hidden', + name: 'showHidden', + type: 'boolean', + description: 'Whether to show hidden calendars', + show: { + calendarActions: ['listCalendars'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Min Access Role', + name: 'minAccessRole', + type: 'options', + description: 'Minimum access role for calendar list', + options: [ + { label: 'Free/Busy Reader', name: 'freeBusyReader' }, + { label: 'Reader', name: 'reader' }, + { label: 'Writer', name: 'writer' }, + { label: 'Owner', name: 'owner' } + ], + show: { + calendarActions: ['listCalendars'] + }, + additionalParams: true, + optional: true + }, + // Freebusy Parameters + { + label: 'Time Min', + name: 'freebusyTimeMin', + type: 'string', + description: 'Lower bound for freebusy query (ISO 8601 format)', + show: { + freebusyActions: ['queryFreebusy'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Time Max', + name: 'freebusyTimeMax', + type: 'string', + description: 'Upper bound for freebusy query (ISO 8601 format)', + show: { + freebusyActions: ['queryFreebusy'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Calendar IDs', + name: 'calendarIds', + type: 'string', + description: 'Comma-separated list of calendar IDs to check for free/busy info', + show: { + freebusyActions: ['queryFreebusy'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Group Expansion Max', + name: 'groupExpansionMax', + type: 'number', + description: 'Maximum number of calendars for which FreeBusy information is to be provided', + show: { + freebusyActions: ['queryFreebusy'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Calendar Expansion Max', + name: 'calendarExpansionMax', + type: 'number', + description: 'Maximum number of events that can be expanded for each calendar', + show: { + freebusyActions: ['queryFreebusy'] + }, + additionalParams: true, + optional: true + } + ] + } + + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + const calendarType = nodeData.inputs?.calendarType as string + + let credentialData = await getCredentialData(nodeData.credential ?? '', options) + credentialData = await refreshOAuth2Token(nodeData.credential ?? '', credentialData, options) + const accessToken = getCredentialParam('access_token', credentialData, nodeData) + + if (!accessToken) { + throw new Error('No access token found in credential') + } + + // Get all actions based on type + let actions: string[] = [] + + if (calendarType === 'event') { + actions = convertMultiOptionsToStringArray(nodeData.inputs?.eventActions) + } else if (calendarType === 'calendar') { + actions = convertMultiOptionsToStringArray(nodeData.inputs?.calendarActions) + } else if (calendarType === 'freebusy') { + actions = convertMultiOptionsToStringArray(nodeData.inputs?.freebusyActions) + } + + const defaultParams = this.transformNodeInputsToToolArgs(nodeData) + + const tools = createGoogleCalendarTools({ + accessToken, + actions, + defaultParams + }) + + return tools + } + + transformNodeInputsToToolArgs(nodeData: INodeData): Record { + // Collect default parameters from inputs + const defaultParams: Record = {} + + // Event parameters + if (nodeData.inputs?.calendarId) defaultParams.calendarId = nodeData.inputs.calendarId + if (nodeData.inputs?.eventId) defaultParams.eventId = nodeData.inputs.eventId + if (nodeData.inputs?.summary) defaultParams.summary = nodeData.inputs.summary + if (nodeData.inputs?.description) defaultParams.description = nodeData.inputs.description + if (nodeData.inputs?.location) defaultParams.location = nodeData.inputs.location + if (nodeData.inputs?.startDateTime) defaultParams.startDateTime = nodeData.inputs.startDateTime + if (nodeData.inputs?.endDateTime) defaultParams.endDateTime = nodeData.inputs.endDateTime + if (nodeData.inputs?.timeZone) defaultParams.timeZone = nodeData.inputs.timeZone + if (nodeData.inputs?.allDay !== undefined) defaultParams.allDay = nodeData.inputs.allDay + 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?.recurrence) defaultParams.recurrence = nodeData.inputs.recurrence + if (nodeData.inputs?.reminderMinutes) defaultParams.reminderMinutes = nodeData.inputs.reminderMinutes + if (nodeData.inputs?.visibility) defaultParams.visibility = nodeData.inputs.visibility + if (nodeData.inputs?.quickAddText) defaultParams.quickAddText = nodeData.inputs.quickAddText + if (nodeData.inputs?.timeMin) defaultParams.timeMin = nodeData.inputs.timeMin + if (nodeData.inputs?.timeMax) defaultParams.timeMax = nodeData.inputs.timeMax + if (nodeData.inputs?.maxResults) defaultParams.maxResults = nodeData.inputs.maxResults + if (nodeData.inputs?.singleEvents !== undefined) defaultParams.singleEvents = nodeData.inputs.singleEvents + if (nodeData.inputs?.orderBy) defaultParams.orderBy = nodeData.inputs.orderBy + if (nodeData.inputs?.query) defaultParams.query = nodeData.inputs.query + + // Calendar parameters + if (nodeData.inputs?.calendarIdForCalendar) defaultParams.calendarIdForCalendar = nodeData.inputs.calendarIdForCalendar + if (nodeData.inputs?.calendarSummary) defaultParams.calendarSummary = nodeData.inputs.calendarSummary + if (nodeData.inputs?.calendarDescription) defaultParams.calendarDescription = nodeData.inputs.calendarDescription + if (nodeData.inputs?.calendarLocation) defaultParams.calendarLocation = nodeData.inputs.calendarLocation + if (nodeData.inputs?.calendarTimeZone) defaultParams.calendarTimeZone = nodeData.inputs.calendarTimeZone + if (nodeData.inputs?.showHidden !== undefined) defaultParams.showHidden = nodeData.inputs.showHidden + if (nodeData.inputs?.minAccessRole) defaultParams.minAccessRole = nodeData.inputs.minAccessRole + + // Freebusy parameters + if (nodeData.inputs?.freebusyTimeMin) defaultParams.freebusyTimeMin = nodeData.inputs.freebusyTimeMin + if (nodeData.inputs?.freebusyTimeMax) defaultParams.freebusyTimeMax = nodeData.inputs.freebusyTimeMax + if (nodeData.inputs?.calendarIds) defaultParams.calendarIds = nodeData.inputs.calendarIds + if (nodeData.inputs?.groupExpansionMax) defaultParams.groupExpansionMax = nodeData.inputs.groupExpansionMax + if (nodeData.inputs?.calendarExpansionMax) defaultParams.calendarExpansionMax = nodeData.inputs.calendarExpansionMax + + return defaultParams + } +} + +module.exports = { nodeClass: GoogleCalendar_Tools } diff --git a/packages/components/nodes/tools/GoogleCalendar/core.ts b/packages/components/nodes/tools/GoogleCalendar/core.ts new file mode 100644 index 00000000000..1c89c9c8819 --- /dev/null +++ b/packages/components/nodes/tools/GoogleCalendar/core.ts @@ -0,0 +1,864 @@ +import { z } from 'zod' +import fetch from 'node-fetch' +import { DynamicStructuredTool } from '../OpenAPIToolkit/core' +import { TOOL_ARGS_PREFIX } from '../../../src/agents' + +export const desc = `Use this when you want to access Google Calendar API for managing events and calendars` + +export interface Headers { + [key: string]: string +} + +export interface Body { + [key: string]: any +} + +export interface RequestParameters { + headers?: Headers + body?: Body + url?: string + description?: string + name?: string + actions?: string[] + accessToken?: string + defaultParams?: any +} + +// Define schemas for different Google Calendar operations + +// Event Schemas +const ListEventsSchema = z.object({ + calendarId: z.string().default('primary').describe('Calendar ID (use "primary" for primary calendar)'), + timeMin: z.string().optional().describe('Lower bound for event search (RFC3339 timestamp)'), + timeMax: z.string().optional().describe('Upper bound for event search (RFC3339 timestamp)'), + maxResults: z.number().optional().default(250).describe('Maximum number of events to return'), + singleEvents: z.boolean().optional().default(true).describe('Whether to expand recurring events into instances'), + orderBy: z.enum(['startTime', 'updated']).optional().describe('Order of events returned'), + query: z.string().optional().describe('Free text search terms') +}) + +const CreateEventSchema = z.object({ + calendarId: z.string().default('primary').describe('Calendar ID where the event will be created'), + summary: z.string().describe('Event title/summary'), + description: z.string().optional().describe('Event description'), + location: z.string().optional().describe('Event location'), + startDateTime: z.string().optional().describe('Event start time (ISO 8601 format)'), + endDateTime: z.string().optional().describe('Event end time (ISO 8601 format)'), + startDate: z.string().optional().describe('Start date for all-day events (YYYY-MM-DD)'), + 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'), + 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') +}) + +const GetEventSchema = z.object({ + calendarId: z.string().default('primary').describe('Calendar ID'), + eventId: z.string().describe('Event ID') +}) + +const UpdateEventSchema = z.object({ + calendarId: z.string().default('primary').describe('Calendar ID'), + eventId: z.string().describe('Event ID'), + summary: z.string().optional().describe('Updated event title/summary'), + description: z.string().optional().describe('Updated event description'), + location: z.string().optional().describe('Updated event location'), + startDateTime: z.string().optional().describe('Updated event start time (ISO 8601 format)'), + endDateTime: z.string().optional().describe('Updated event end time (ISO 8601 format)'), + startDate: z.string().optional().describe('Updated start date for all-day events (YYYY-MM-DD)'), + 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'), + 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') +}) + +const DeleteEventSchema = z.object({ + calendarId: z.string().default('primary').describe('Calendar ID'), + eventId: z.string().describe('Event ID to delete') +}) + +const QuickAddEventSchema = z.object({ + calendarId: z.string().default('primary').describe('Calendar ID'), + quickAddText: z.string().describe('Natural language text for quick event creation') +}) + +// Calendar Schemas +const ListCalendarsSchema = z.object({ + showHidden: z.boolean().optional().describe('Whether to show hidden calendars'), + minAccessRole: z.enum(['freeBusyReader', 'reader', 'writer', 'owner']).optional().describe('Minimum access role') +}) + +const CreateCalendarSchema = z.object({ + summary: z.string().describe('Calendar title/name'), + description: z.string().optional().describe('Calendar description'), + location: z.string().optional().describe('Calendar location'), + timeZone: z.string().optional().describe('Calendar time zone (e.g., America/New_York)') +}) + +const GetCalendarSchema = z.object({ + calendarId: z.string().describe('Calendar ID') +}) + +const UpdateCalendarSchema = z.object({ + calendarId: z.string().describe('Calendar ID'), + summary: z.string().optional().describe('Updated calendar title/name'), + description: z.string().optional().describe('Updated calendar description'), + location: z.string().optional().describe('Updated calendar location'), + timeZone: z.string().optional().describe('Updated calendar time zone') +}) + +const DeleteCalendarSchema = z.object({ + calendarId: z.string().describe('Calendar ID to delete') +}) + +const ClearCalendarSchema = z.object({ + calendarId: z.string().describe('Calendar ID to clear (removes all events)') +}) + +// Freebusy Schemas +const QueryFreebusySchema = z.object({ + timeMin: z.string().describe('Lower bound for freebusy query (RFC3339 timestamp)'), + timeMax: z.string().describe('Upper bound for freebusy query (RFC3339 timestamp)'), + calendarIds: z.string().describe('Comma-separated list of calendar IDs to check for free/busy info'), + groupExpansionMax: z.number().optional().describe('Maximum number of calendars for which FreeBusy information is to be provided'), + calendarExpansionMax: z.number().optional().describe('Maximum number of events that can be expanded for each calendar') +}) + +class BaseGoogleCalendarTool extends DynamicStructuredTool { + protected accessToken: string = '' + + constructor(args: any) { + super(args) + this.accessToken = args.accessToken ?? '' + } + + async makeGoogleCalendarRequest({ + endpoint, + method = 'GET', + body, + params + }: { + endpoint: string + method?: string + body?: any + params?: any + }): Promise { + const url = `https://www.googleapis.com/calendar/v3/${endpoint}` + + const headers = { + Authorization: `Bearer ${this.accessToken}`, + 'Content-Type': 'application/json', + Accept: 'application/json', + ...this.headers + } + + const response = await fetch(url, { + method, + headers, + body: body ? JSON.stringify(body) : undefined + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`Google Calendar API Error ${response.status}: ${response.statusText} - ${errorText}`) + } + + const data = await response.text() + return data + TOOL_ARGS_PREFIX + JSON.stringify(params) + } +} + +// Event Tools +class ListEventsTool extends BaseGoogleCalendarTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'list_events', + description: 'List events from Google Calendar', + schema: ListEventsSchema, + baseUrl: '', + method: 'GET', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const queryParams = new URLSearchParams() + + if (params.timeMin) queryParams.append('timeMin', params.timeMin) + if (params.timeMax) queryParams.append('timeMax', params.timeMax) + if (params.maxResults) queryParams.append('maxResults', params.maxResults.toString()) + if (params.singleEvents !== undefined) queryParams.append('singleEvents', params.singleEvents.toString()) + if (params.orderBy) queryParams.append('orderBy', params.orderBy) + if (params.query) queryParams.append('q', params.query) + + const endpoint = `calendars/${encodeURIComponent(params.calendarId)}/events?${queryParams.toString()}` + + try { + const response = await this.makeGoogleCalendarRequest({ endpoint, params }) + return response + } catch (error) { + return `Error listing events: ${error}` + } + } +} + +class CreateEventTool extends BaseGoogleCalendarTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'create_event', + description: 'Create a new event in Google Calendar', + schema: CreateEventSchema, + baseUrl: '', + method: 'POST', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const eventData: any = { + summary: params.summary + } + + if (params.description) eventData.description = params.description + if (params.location) eventData.location = params.location + + // Handle date/time + if (params.startDate && params.endDate) { + // All-day event + eventData.start = { date: params.startDate } + eventData.end = { date: params.endDate } + } else if (params.startDateTime && params.endDateTime) { + // Timed event + eventData.start = { + dateTime: params.startDateTime, + timeZone: params.timeZone || 'UTC' + } + eventData.end = { + dateTime: params.endDateTime, + timeZone: params.timeZone || 'UTC' + } + } + + // Handle attendees + if (params.attendees) { + eventData.attendees = params.attendees.split(',').map((email: string) => ({ + email: email.trim() + })) + } + + // Handle recurrence + if (params.recurrence) { + eventData.recurrence = [params.recurrence] + } + + // Handle reminders + if (params.reminderMinutes !== undefined) { + eventData.reminders = { + useDefault: false, + overrides: [ + { + method: 'popup', + minutes: params.reminderMinutes + } + ] + } + } + + if (params.visibility) eventData.visibility = params.visibility + + const endpoint = `calendars/${encodeURIComponent(params.calendarId)}/events` + const response = await this.makeGoogleCalendarRequest({ endpoint, method: 'POST', body: eventData, params }) + return response + } catch (error) { + return `Error creating event: ${error}` + } + } +} + +class GetEventTool extends BaseGoogleCalendarTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'get_event', + description: 'Get a specific event from Google Calendar', + schema: GetEventSchema, + baseUrl: '', + method: 'GET', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const endpoint = `calendars/${encodeURIComponent(params.calendarId)}/events/${encodeURIComponent(params.eventId)}` + const response = await this.makeGoogleCalendarRequest({ endpoint, params }) + return response + } catch (error) { + return `Error getting event: ${error}` + } + } +} + +class UpdateEventTool extends BaseGoogleCalendarTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'update_event', + description: 'Update an existing event in Google Calendar', + schema: UpdateEventSchema, + baseUrl: '', + method: 'PUT', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const updateData: any = {} + + if (params.summary) updateData.summary = params.summary + if (params.description) updateData.description = params.description + if (params.location) updateData.location = params.location + + // Handle date/time updates + if (params.startDate && params.endDate) { + updateData.start = { date: params.startDate } + updateData.end = { date: params.endDate } + } else if (params.startDateTime && params.endDateTime) { + updateData.start = { + dateTime: params.startDateTime, + timeZone: params.timeZone || 'UTC' + } + updateData.end = { + dateTime: params.endDateTime, + timeZone: params.timeZone || 'UTC' + } + } + + if (params.attendees) { + updateData.attendees = params.attendees.split(',').map((email: string) => ({ + email: email.trim() + })) + } + + if (params.recurrence) { + updateData.recurrence = [params.recurrence] + } + + if (params.reminderMinutes !== undefined) { + updateData.reminders = { + useDefault: false, + overrides: [ + { + method: 'popup', + minutes: params.reminderMinutes + } + ] + } + } + + if (params.visibility) updateData.visibility = params.visibility + + 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) { + return `Error updating event: ${error}` + } + } +} + +class DeleteEventTool extends BaseGoogleCalendarTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'delete_event', + description: 'Delete an event from Google Calendar', + schema: DeleteEventSchema, + baseUrl: '', + method: 'DELETE', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const endpoint = `calendars/${encodeURIComponent(params.calendarId)}/events/${encodeURIComponent(params.eventId)}` + const response = await this.makeGoogleCalendarRequest({ endpoint, method: 'DELETE', params }) + return response || 'Event deleted successfully' + } catch (error) { + return `Error deleting event: ${error}` + } + } +} + +class QuickAddEventTool extends BaseGoogleCalendarTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'quick_add_event', + description: 'Quick add event to Google Calendar using natural language', + schema: QuickAddEventSchema, + baseUrl: '', + method: 'POST', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const queryParams = new URLSearchParams() + queryParams.append('text', params.quickAddText) + + const endpoint = `calendars/${encodeURIComponent(params.calendarId)}/events/quickAdd?${queryParams.toString()}` + const response = await this.makeGoogleCalendarRequest({ endpoint, method: 'POST', params }) + return response + } catch (error) { + return `Error quick adding event: ${error}` + } + } +} + +// Calendar Tools +class ListCalendarsTool extends BaseGoogleCalendarTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'list_calendars', + description: 'List calendars from Google Calendar', + schema: ListCalendarsSchema, + baseUrl: '', + method: 'GET', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const queryParams = new URLSearchParams() + + if (params.showHidden !== undefined) queryParams.append('showHidden', params.showHidden.toString()) + if (params.minAccessRole) queryParams.append('minAccessRole', params.minAccessRole) + + const endpoint = `users/me/calendarList?${queryParams.toString()}` + + try { + const response = await this.makeGoogleCalendarRequest({ endpoint, params }) + return response + } catch (error) { + return `Error listing calendars: ${error}` + } + } +} + +class CreateCalendarTool extends BaseGoogleCalendarTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'create_calendar', + description: 'Create a new calendar in Google Calendar', + schema: CreateCalendarSchema, + baseUrl: '', + method: 'POST', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const calendarData: any = { + summary: params.summary + } + + if (params.description) calendarData.description = params.description + if (params.location) calendarData.location = params.location + if (params.timeZone) calendarData.timeZone = params.timeZone + + const endpoint = 'calendars' + const response = await this.makeGoogleCalendarRequest({ endpoint, method: 'POST', body: calendarData, params }) + return response + } catch (error) { + return `Error creating calendar: ${error}` + } + } +} + +class GetCalendarTool extends BaseGoogleCalendarTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'get_calendar', + description: 'Get a specific calendar from Google Calendar', + schema: GetCalendarSchema, + baseUrl: '', + method: 'GET', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const endpoint = `calendars/${encodeURIComponent(params.calendarId)}` + const response = await this.makeGoogleCalendarRequest({ endpoint, params }) + return response + } catch (error) { + return `Error getting calendar: ${error}` + } + } +} + +class UpdateCalendarTool extends BaseGoogleCalendarTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'update_calendar', + description: 'Update an existing calendar in Google Calendar', + schema: UpdateCalendarSchema, + baseUrl: '', + method: 'PUT', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const updateData: any = {} + + if (params.summary) updateData.summary = params.summary + if (params.description) updateData.description = params.description + if (params.location) updateData.location = params.location + if (params.timeZone) updateData.timeZone = params.timeZone + + const endpoint = `calendars/${encodeURIComponent(params.calendarId)}` + const response = await this.makeGoogleCalendarRequest({ endpoint, method: 'PUT', body: updateData, params }) + return response + } catch (error) { + return `Error updating calendar: ${error}` + } + } +} + +class DeleteCalendarTool extends BaseGoogleCalendarTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'delete_calendar', + description: 'Delete a calendar from Google Calendar', + schema: DeleteCalendarSchema, + baseUrl: '', + method: 'DELETE', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const endpoint = `calendars/${encodeURIComponent(params.calendarId)}` + const response = await this.makeGoogleCalendarRequest({ endpoint, method: 'DELETE', params }) + return response || 'Calendar deleted successfully' + } catch (error) { + return `Error deleting calendar: ${error}` + } + } +} + +class ClearCalendarTool extends BaseGoogleCalendarTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'clear_calendar', + description: 'Clear all events from a Google Calendar', + schema: ClearCalendarSchema, + baseUrl: '', + method: 'POST', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const endpoint = `calendars/${encodeURIComponent(params.calendarId)}/clear` + const response = await this.makeGoogleCalendarRequest({ endpoint, method: 'POST', params }) + return response || 'Calendar cleared successfully' + } catch (error) { + return `Error clearing calendar: ${error}` + } + } +} + +// Freebusy Tools +class QueryFreebusyTool extends BaseGoogleCalendarTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'query_freebusy', + description: 'Query free/busy information for a set of calendars', + schema: QueryFreebusySchema, + baseUrl: '', + method: 'POST', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const freebusyData: any = { + timeMin: params.timeMin, + timeMax: params.timeMax, + items: params.calendarIds.split(',').map((id: string) => ({ + id: id.trim() + })) + } + + if (params.groupExpansionMax !== undefined) { + freebusyData.groupExpansionMax = params.groupExpansionMax + } + + if (params.calendarExpansionMax !== undefined) { + freebusyData.calendarExpansionMax = params.calendarExpansionMax + } + + const endpoint = 'freeBusy' + const response = await this.makeGoogleCalendarRequest({ endpoint, method: 'POST', body: freebusyData, params }) + return response + } catch (error) { + return `Error querying freebusy: ${error}` + } + } +} + +export const createGoogleCalendarTools = (args?: RequestParameters): DynamicStructuredTool[] => { + const tools: DynamicStructuredTool[] = [] + const actions = args?.actions || [] + const accessToken = args?.accessToken || '' + const defaultParams = args?.defaultParams || {} + + // Event tools + if (actions.includes('listEvents')) { + tools.push( + new ListEventsTool({ + accessToken, + defaultParams: defaultParams.listEvents + }) + ) + } + + if (actions.includes('createEvent')) { + tools.push( + new CreateEventTool({ + accessToken, + defaultParams: defaultParams.createEvent + }) + ) + } + + if (actions.includes('getEvent')) { + tools.push( + new GetEventTool({ + accessToken, + defaultParams: defaultParams.getEvent + }) + ) + } + + if (actions.includes('updateEvent')) { + tools.push( + new UpdateEventTool({ + accessToken, + defaultParams: defaultParams.updateEvent + }) + ) + } + + if (actions.includes('deleteEvent')) { + tools.push( + new DeleteEventTool({ + accessToken, + defaultParams: defaultParams.deleteEvent + }) + ) + } + + if (actions.includes('quickAddEvent')) { + tools.push( + new QuickAddEventTool({ + accessToken, + defaultParams: defaultParams.quickAddEvent + }) + ) + } + + // Calendar tools + if (actions.includes('listCalendars')) { + tools.push( + new ListCalendarsTool({ + accessToken, + defaultParams: defaultParams.listCalendars + }) + ) + } + + if (actions.includes('createCalendar')) { + tools.push( + new CreateCalendarTool({ + accessToken, + defaultParams: defaultParams.createCalendar + }) + ) + } + + if (actions.includes('getCalendar')) { + tools.push( + new GetCalendarTool({ + accessToken, + defaultParams: defaultParams.getCalendar + }) + ) + } + + if (actions.includes('updateCalendar')) { + tools.push( + new UpdateCalendarTool({ + accessToken, + defaultParams: defaultParams.updateCalendar + }) + ) + } + + if (actions.includes('deleteCalendar')) { + tools.push( + new DeleteCalendarTool({ + accessToken, + defaultParams: defaultParams.deleteCalendar + }) + ) + } + + if (actions.includes('clearCalendar')) { + tools.push( + new ClearCalendarTool({ + accessToken, + defaultParams: defaultParams.clearCalendar + }) + ) + } + + // Freebusy tools + if (actions.includes('queryFreebusy')) { + tools.push( + new QueryFreebusyTool({ + accessToken, + defaultParams: defaultParams.queryFreebusy + }) + ) + } + + return tools +} diff --git a/packages/components/nodes/tools/GoogleCalendar/google-calendar.svg b/packages/components/nodes/tools/GoogleCalendar/google-calendar.svg new file mode 100644 index 00000000000..c5ba2d56f30 --- /dev/null +++ b/packages/components/nodes/tools/GoogleCalendar/google-calendar.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/tools/GoogleDocs/GoogleDocs.ts b/packages/components/nodes/tools/GoogleDocs/GoogleDocs.ts new file mode 100644 index 00000000000..296a9dea81e --- /dev/null +++ b/packages/components/nodes/tools/GoogleDocs/GoogleDocs.ts @@ -0,0 +1,253 @@ +import { convertMultiOptionsToStringArray, getCredentialData, getCredentialParam, refreshOAuth2Token } from '../../../src/utils' +import { createGoogleDocsTools } from './core' +import type { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' + +class GoogleDocs_Tools implements INode { + label: string + name: string + version: number + type: string + icon: string + category: string + description: string + baseClasses: string[] + credential: INodeParams + inputs: INodeParams[] + + constructor() { + this.label = 'Google Docs' + this.name = 'googleDocsTool' + this.version = 1.0 + this.type = 'GoogleDocs' + this.icon = 'google-docs.svg' + this.category = 'Tools' + this.description = + 'Perform Google Docs operations such as creating, reading, updating, and deleting documents, as well as text manipulation' + this.baseClasses = ['Tool'] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['googleDocsOAuth2'] + } + this.inputs = [ + // Document Actions + { + label: 'Actions', + name: 'actions', + type: 'multiOptions', + description: 'Actions to perform', + options: [ + { + label: 'Create Document', + name: 'createDocument' + }, + { + label: 'Get Document', + name: 'getDocument' + }, + { + label: 'Update Document', + name: 'updateDocument' + }, + { + label: 'Insert Text', + name: 'insertText' + }, + { + label: 'Replace Text', + name: 'replaceText' + }, + { + label: 'Append Text', + name: 'appendText' + }, + { + label: 'Get Text Content', + name: 'getTextContent' + }, + { + label: 'Insert Image', + name: 'insertImage' + }, + { + label: 'Create Table', + name: 'createTable' + } + ] + }, + // Document Parameters + { + label: 'Document ID', + name: 'documentId', + type: 'string', + description: 'Document ID for operations on specific documents', + show: { + actions: [ + 'getDocument', + 'updateDocument', + 'insertText', + 'replaceText', + 'appendText', + 'getTextContent', + 'insertImage', + 'createTable' + ] + }, + additionalParams: true, + optional: true + }, + { + label: 'Title', + name: 'title', + type: 'string', + description: 'Document title', + show: { + actions: ['createDocument'] + }, + additionalParams: true, + optional: true + }, + // Text Parameters + { + label: 'Text', + name: 'text', + type: 'string', + description: 'Text content to insert or append', + show: { + actions: ['createDocument', 'updateDocument', 'insertText', 'appendText'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Index', + name: 'index', + type: 'number', + description: 'Index where to insert text or media (1-based, default: 1 for beginning)', + default: 1, + show: { + actions: ['createDocument', 'updateDocument', 'insertText', 'insertImage', 'createTable'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Replace Text', + name: 'replaceText', + type: 'string', + description: 'Text to replace', + show: { + actions: ['updateDocument', 'replaceText'] + }, + additionalParams: true, + optional: true + }, + { + label: 'New Text', + name: 'newText', + type: 'string', + description: 'New text to replace with', + show: { + actions: ['updateDocument', 'replaceText'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Match Case', + name: 'matchCase', + type: 'boolean', + description: 'Whether the search should be case-sensitive', + default: false, + show: { + actions: ['updateDocument', 'replaceText'] + }, + additionalParams: true, + optional: true + }, + + // Media Parameters + { + label: 'Image URL', + name: 'imageUrl', + type: 'string', + description: 'URL of the image to insert', + show: { + actions: ['createDocument', 'updateDocument', 'insertImage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Table Rows', + name: 'rows', + type: 'number', + description: 'Number of rows in the table', + show: { + actions: ['createDocument', 'updateDocument', 'createTable'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Table Columns', + name: 'columns', + type: 'number', + description: 'Number of columns in the table', + show: { + actions: ['createDocument', 'updateDocument', 'createTable'] + }, + additionalParams: true, + optional: true + } + ] + } + + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + let credentialData = await getCredentialData(nodeData.credential ?? '', options) + credentialData = await refreshOAuth2Token(nodeData.credential ?? '', credentialData, options) + const accessToken = getCredentialParam('access_token', credentialData, nodeData) + + if (!accessToken) { + throw new Error('No access token found in credential') + } + + // Get all actions + const actions = convertMultiOptionsToStringArray(nodeData.inputs?.actions) + + const defaultParams = this.transformNodeInputsToToolArgs(nodeData) + + const tools = createGoogleDocsTools({ + accessToken, + actions, + defaultParams + }) + + return tools + } + + transformNodeInputsToToolArgs(nodeData: INodeData): Record { + const nodeInputs: Record = {} + + // Document parameters + if (nodeData.inputs?.documentId) nodeInputs.documentId = nodeData.inputs.documentId + if (nodeData.inputs?.title) nodeInputs.title = nodeData.inputs.title + + // Text parameters + if (nodeData.inputs?.text) nodeInputs.text = nodeData.inputs.text + if (nodeData.inputs?.index) nodeInputs.index = nodeData.inputs.index + if (nodeData.inputs?.replaceText) nodeInputs.replaceText = nodeData.inputs.replaceText + if (nodeData.inputs?.newText) nodeInputs.newText = nodeData.inputs.newText + if (nodeData.inputs?.matchCase !== undefined) nodeInputs.matchCase = nodeData.inputs.matchCase + + // Media parameters + if (nodeData.inputs?.imageUrl) nodeInputs.imageUrl = nodeData.inputs.imageUrl + if (nodeData.inputs?.rows) nodeInputs.rows = nodeData.inputs.rows + if (nodeData.inputs?.columns) nodeInputs.columns = nodeData.inputs.columns + + return nodeInputs + } +} + +module.exports = { nodeClass: GoogleDocs_Tools } diff --git a/packages/components/nodes/tools/GoogleDocs/core.ts b/packages/components/nodes/tools/GoogleDocs/core.ts new file mode 100644 index 00000000000..2b43114f648 --- /dev/null +++ b/packages/components/nodes/tools/GoogleDocs/core.ts @@ -0,0 +1,729 @@ +import { z } from 'zod' +import fetch from 'node-fetch' +import { DynamicStructuredTool } from '../OpenAPIToolkit/core' +import { TOOL_ARGS_PREFIX } from '../../../src/agents' + +export const desc = `Use this when you want to access Google Docs API for managing documents` + +export interface Headers { + [key: string]: string +} + +export interface Body { + [key: string]: any +} + +export interface RequestParameters { + headers?: Headers + body?: Body + url?: string + description?: string + name?: string + actions?: string[] + accessToken?: string + defaultParams?: any +} + +// Define schemas for different Google Docs operations + +// Document Schemas +const CreateDocumentSchema = z.object({ + title: z.string().describe('Document title'), + text: z.string().optional().describe('Text content to insert after creating document'), + index: z.number().optional().default(1).describe('Index where to insert text or media (1-based, default: 1 for beginning)'), + imageUrl: z.string().optional().describe('URL of the image to insert after creating document'), + rows: z.number().optional().describe('Number of rows in the table to create'), + columns: z.number().optional().describe('Number of columns in the table to create') +}) + +const GetDocumentSchema = z.object({ + documentId: z.string().describe('Document ID to retrieve') +}) + +const UpdateDocumentSchema = z.object({ + documentId: z.string().describe('Document ID to update'), + text: z.string().optional().describe('Text content to insert'), + index: z.number().optional().default(1).describe('Index where to insert text or media (1-based, default: 1 for beginning)'), + replaceText: z.string().optional().describe('Text to replace'), + newText: z.string().optional().describe('New text to replace with'), + matchCase: z.boolean().optional().default(false).describe('Whether the search should be case-sensitive'), + imageUrl: z.string().optional().describe('URL of the image to insert'), + rows: z.number().optional().describe('Number of rows in the table to create'), + columns: z.number().optional().describe('Number of columns in the table to create') +}) + +const InsertTextSchema = z.object({ + documentId: z.string().describe('Document ID'), + text: z.string().describe('Text to insert'), + index: z.number().optional().default(1).describe('Index where to insert text (1-based, default: 1 for beginning)') +}) + +const ReplaceTextSchema = z.object({ + documentId: z.string().describe('Document ID'), + replaceText: z.string().describe('Text to replace'), + newText: z.string().describe('New text to replace with'), + matchCase: z.boolean().optional().default(false).describe('Whether the search should be case-sensitive') +}) + +const AppendTextSchema = z.object({ + documentId: z.string().describe('Document ID'), + text: z.string().describe('Text to append to the document') +}) + +const GetTextContentSchema = z.object({ + documentId: z.string().describe('Document ID to get text content from') +}) + +const InsertImageSchema = z.object({ + documentId: z.string().describe('Document ID'), + imageUrl: z.string().describe('URL of the image to insert'), + index: z.number().optional().default(1).describe('Index where to insert image (1-based)') +}) + +const CreateTableSchema = z.object({ + documentId: z.string().describe('Document ID'), + rows: z.number().describe('Number of rows in the table'), + columns: z.number().describe('Number of columns in the table'), + index: z.number().optional().default(1).describe('Index where to insert table (1-based)') +}) + +class BaseGoogleDocsTool extends DynamicStructuredTool { + protected accessToken: string = '' + + constructor(args: any) { + super(args) + this.accessToken = args.accessToken ?? '' + } + + async makeGoogleDocsRequest({ + endpoint, + method = 'GET', + body, + params + }: { + endpoint: string + method?: string + body?: any + params?: any + }): Promise { + const url = `https://docs.googleapis.com/v1/${endpoint}` + + const headers = { + Authorization: `Bearer ${this.accessToken}`, + 'Content-Type': 'application/json', + Accept: 'application/json', + ...this.headers + } + + const response = await fetch(url, { + method, + headers, + body: body ? JSON.stringify(body) : undefined + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`Google Docs API Error ${response.status}: ${response.statusText} - ${errorText}`) + } + + const data = await response.text() + return data + TOOL_ARGS_PREFIX + JSON.stringify(params) + } + + async makeDriveRequest({ + endpoint, + method = 'GET', + body, + params + }: { + endpoint: string + method?: string + body?: any + params?: any + }): Promise { + const url = `https://www.googleapis.com/drive/v3/${endpoint}` + + const headers = { + Authorization: `Bearer ${this.accessToken}`, + 'Content-Type': 'application/json', + Accept: 'application/json', + ...this.headers + } + + const response = await fetch(url, { + method, + headers, + body: body ? JSON.stringify(body) : undefined + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`Google Drive API Error ${response.status}: ${response.statusText} - ${errorText}`) + } + + const data = await response.text() + return data + TOOL_ARGS_PREFIX + JSON.stringify(params) + } +} + +// Document Tools +class CreateDocumentTool extends BaseGoogleDocsTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'create_document', + description: 'Create a new Google Docs document', + schema: CreateDocumentSchema, + baseUrl: '', + method: 'POST', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const documentData = { + title: params.title + } + + const endpoint = 'documents' + const createResponse = await this.makeGoogleDocsRequest({ + endpoint, + method: 'POST', + body: documentData, + params + }) + + // Get the document ID from the response + const documentResponse = JSON.parse(createResponse.split(TOOL_ARGS_PREFIX)[0]) + const documentId = documentResponse.documentId + + // Now add content if provided + const requests = [] + + if (params.text) { + requests.push({ + insertText: { + location: { + index: params.index || 1 + }, + text: params.text + } + }) + } + + if (params.imageUrl) { + requests.push({ + insertInlineImage: { + location: { + index: params.index || 1 + }, + uri: params.imageUrl + } + }) + } + + if (params.rows && params.columns) { + requests.push({ + insertTable: { + location: { + index: params.index || 1 + }, + rows: params.rows, + columns: params.columns + } + }) + } + + // If we have content to add, make a batch update + if (requests.length > 0) { + const updateEndpoint = `documents/${encodeURIComponent(documentId)}:batchUpdate` + await this.makeGoogleDocsRequest({ + endpoint: updateEndpoint, + method: 'POST', + body: { requests }, + params: {} + }) + } + + return createResponse + } catch (error) { + return `Error creating document: ${error}` + } + } +} + +class GetDocumentTool extends BaseGoogleDocsTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'get_document', + description: 'Get a Google Docs document by ID', + schema: GetDocumentSchema, + baseUrl: '', + method: 'GET', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const endpoint = `documents/${encodeURIComponent(params.documentId)}` + const response = await this.makeGoogleDocsRequest({ endpoint, params }) + return response + } catch (error) { + return `Error getting document: ${error}` + } + } +} + +class UpdateDocumentTool extends BaseGoogleDocsTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'update_document', + description: 'Update a Google Docs document with batch requests', + schema: UpdateDocumentSchema, + baseUrl: '', + method: 'POST', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const requests = [] + + // Insert text + if (params.text) { + requests.push({ + insertText: { + location: { + index: params.index || 1 + }, + text: params.text + } + }) + } + + // Replace text + if (params.replaceText && params.newText) { + requests.push({ + replaceAllText: { + containsText: { + text: params.replaceText, + matchCase: params.matchCase || false + }, + replaceText: params.newText + } + }) + } + + // Insert image + if (params.imageUrl) { + requests.push({ + insertInlineImage: { + location: { + index: params.index || 1 + }, + uri: params.imageUrl + } + }) + } + + // Create table + if (params.rows && params.columns) { + requests.push({ + insertTable: { + location: { + index: params.index || 1 + }, + rows: params.rows, + columns: params.columns + } + }) + } + + if (requests.length > 0) { + const endpoint = `documents/${encodeURIComponent(params.documentId)}:batchUpdate` + const response = await this.makeGoogleDocsRequest({ + endpoint, + method: 'POST', + body: { requests }, + params + }) + return response + } else { + return `No updates specified` + TOOL_ARGS_PREFIX + JSON.stringify(params) + } + } catch (error) { + return `Error updating document: ${error}` + } + } +} + +class InsertTextTool extends BaseGoogleDocsTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'insert_text', + description: 'Insert text into a Google Docs document', + schema: InsertTextSchema, + baseUrl: '', + method: 'POST', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const requests = [ + { + insertText: { + location: { + index: params.index + }, + text: params.text + } + } + ] + + const endpoint = `documents/${encodeURIComponent(params.documentId)}:batchUpdate` + const response = await this.makeGoogleDocsRequest({ + endpoint, + method: 'POST', + body: { requests }, + params + }) + return response + } catch (error) { + return `Error inserting text: ${error}` + } + } +} + +class ReplaceTextTool extends BaseGoogleDocsTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'replace_text', + description: 'Replace text in a Google Docs document', + schema: ReplaceTextSchema, + baseUrl: '', + method: 'POST', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const requests = [ + { + replaceAllText: { + containsText: { + text: params.replaceText, + matchCase: params.matchCase + }, + replaceText: params.newText + } + } + ] + + const endpoint = `documents/${encodeURIComponent(params.documentId)}:batchUpdate` + const response = await this.makeGoogleDocsRequest({ + endpoint, + method: 'POST', + body: { requests }, + params + }) + return response + } catch (error) { + return `Error replacing text: ${error}` + } + } +} + +class AppendTextTool extends BaseGoogleDocsTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'append_text', + description: 'Append text to the end of a Google Docs document', + schema: AppendTextSchema, + baseUrl: '', + method: 'POST', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + // First get the document to find the end index + const getEndpoint = `documents/${encodeURIComponent(params.documentId)}` + const docResponse = await this.makeGoogleDocsRequest({ endpoint: getEndpoint, params: {} }) + const docData = JSON.parse(docResponse.split(TOOL_ARGS_PREFIX)[0]) + + // Get the end index of the document body + const endIndex = docData.body.content[docData.body.content.length - 1].endIndex - 1 + + const requests = [ + { + insertText: { + location: { + index: endIndex + }, + text: params.text + } + } + ] + + const endpoint = `documents/${encodeURIComponent(params.documentId)}:batchUpdate` + const response = await this.makeGoogleDocsRequest({ + endpoint, + method: 'POST', + body: { requests }, + params + }) + return response + } catch (error) { + return `Error appending text: ${error}` + } + } +} + +class GetTextContentTool extends BaseGoogleDocsTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'get_text_content', + description: 'Get the text content from a Google Docs document', + schema: GetTextContentSchema, + baseUrl: '', + method: 'GET', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const endpoint = `documents/${encodeURIComponent(params.documentId)}` + const response = await this.makeGoogleDocsRequest({ endpoint, params }) + + // Extract and return just the text content + const docData = JSON.parse(response.split(TOOL_ARGS_PREFIX)[0]) + let textContent = '' + + const extractText = (element: any) => { + if (element.paragraph) { + element.paragraph.elements?.forEach((elem: any) => { + if (elem.textRun) { + textContent += elem.textRun.content + } + }) + } + } + + docData.body.content?.forEach(extractText) + + return JSON.stringify({ textContent }) + TOOL_ARGS_PREFIX + JSON.stringify(params) + } catch (error) { + return `Error getting text content: ${error}` + } + } +} + +class InsertImageTool extends BaseGoogleDocsTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'insert_image', + description: 'Insert an image into a Google Docs document', + schema: InsertImageSchema, + baseUrl: '', + method: 'POST', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const requests = [ + { + insertInlineImage: { + location: { + index: params.index + }, + uri: params.imageUrl + } + } + ] + + const endpoint = `documents/${encodeURIComponent(params.documentId)}:batchUpdate` + const response = await this.makeGoogleDocsRequest({ + endpoint, + method: 'POST', + body: { requests }, + params + }) + return response + } catch (error) { + return `Error inserting image: ${error}` + } + } +} + +class CreateTableTool extends BaseGoogleDocsTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'create_table', + description: 'Create a table in a Google Docs document', + schema: CreateTableSchema, + baseUrl: '', + method: 'POST', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const requests = [ + { + insertTable: { + location: { + index: params.index + }, + rows: params.rows, + columns: params.columns + } + } + ] + + const endpoint = `documents/${encodeURIComponent(params.documentId)}:batchUpdate` + const response = await this.makeGoogleDocsRequest({ + endpoint, + method: 'POST', + body: { requests }, + params + }) + return response + } catch (error) { + return `Error creating table: ${error}` + } + } +} + +export const createGoogleDocsTools = (args?: RequestParameters): DynamicStructuredTool[] => { + const actions = args?.actions || [] + const tools: DynamicStructuredTool[] = [] + + if (actions.includes('createDocument') || actions.length === 0) { + tools.push(new CreateDocumentTool(args)) + } + + if (actions.includes('getDocument') || actions.length === 0) { + tools.push(new GetDocumentTool(args)) + } + + if (actions.includes('updateDocument') || actions.length === 0) { + tools.push(new UpdateDocumentTool(args)) + } + + if (actions.includes('insertText') || actions.length === 0) { + tools.push(new InsertTextTool(args)) + } + + if (actions.includes('replaceText') || actions.length === 0) { + tools.push(new ReplaceTextTool(args)) + } + + if (actions.includes('appendText') || actions.length === 0) { + tools.push(new AppendTextTool(args)) + } + + if (actions.includes('getTextContent') || actions.length === 0) { + tools.push(new GetTextContentTool(args)) + } + + if (actions.includes('insertImage') || actions.length === 0) { + tools.push(new InsertImageTool(args)) + } + + if (actions.includes('createTable') || actions.length === 0) { + tools.push(new CreateTableTool(args)) + } + + return tools +} diff --git a/packages/components/nodes/tools/GoogleDocs/google-docs.svg b/packages/components/nodes/tools/GoogleDocs/google-docs.svg new file mode 100644 index 00000000000..7406241a194 --- /dev/null +++ b/packages/components/nodes/tools/GoogleDocs/google-docs.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/tools/GoogleDrive/GoogleDrive.ts b/packages/components/nodes/tools/GoogleDrive/GoogleDrive.ts new file mode 100644 index 00000000000..ec44367dfb1 --- /dev/null +++ b/packages/components/nodes/tools/GoogleDrive/GoogleDrive.ts @@ -0,0 +1,663 @@ +import { convertMultiOptionsToStringArray, getCredentialData, getCredentialParam, refreshOAuth2Token } from '../../../src/utils' +import { createGoogleDriveTools } from './core' +import type { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' + +class GoogleDrive_Tools implements INode { + label: string + name: string + version: number + type: string + icon: string + category: string + description: string + baseClasses: string[] + credential: INodeParams + inputs: INodeParams[] + + constructor() { + this.label = 'Google Drive' + this.name = 'googleDriveTool' + this.version = 1.0 + this.type = 'GoogleDrive' + this.icon = 'google-drive.svg' + this.category = 'Tools' + this.description = 'Perform Google Drive operations such as managing files, folders, sharing, and searching' + this.baseClasses = ['Tool'] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['googleDriveOAuth2'] + } + this.inputs = [ + { + label: 'Type', + name: 'driveType', + type: 'options', + description: 'Type of Google Drive operation', + options: [ + { + label: 'File', + name: 'file' + }, + { + label: 'Folder', + name: 'folder' + }, + { + label: 'Search', + name: 'search' + }, + { + label: 'Share', + name: 'share' + } + ] + }, + // File Actions + { + label: 'File Actions', + name: 'fileActions', + type: 'multiOptions', + description: 'Actions to perform on files', + options: [ + { + label: 'List Files', + name: 'listFiles' + }, + { + label: 'Get File', + name: 'getFile' + }, + { + label: 'Create File', + name: 'createFile' + }, + { + label: 'Update File', + name: 'updateFile' + }, + { + label: 'Delete File', + name: 'deleteFile' + }, + { + label: 'Copy File', + name: 'copyFile' + }, + { + label: 'Download File', + name: 'downloadFile' + } + ], + show: { + driveType: ['file'] + } + }, + // Folder Actions + { + label: 'Folder Actions', + name: 'folderActions', + type: 'multiOptions', + description: 'Actions to perform on folders', + options: [ + { + label: 'Create Folder', + name: 'createFolder' + }, + { + label: 'List Folder Contents', + name: 'listFolderContents' + }, + { + label: 'Delete Folder', + name: 'deleteFolder' + } + ], + show: { + driveType: ['folder'] + } + }, + // Search Actions + { + label: 'Search Actions', + name: 'searchActions', + type: 'multiOptions', + description: 'Search operations', + options: [ + { + label: 'Search Files', + name: 'searchFiles' + } + ], + show: { + driveType: ['search'] + } + }, + // Share Actions + { + label: 'Share Actions', + name: 'shareActions', + type: 'multiOptions', + description: 'Sharing operations', + options: [ + { + label: 'Share File', + name: 'shareFile' + }, + { + label: 'Get Permissions', + name: 'getPermissions' + }, + { + label: 'Remove Permission', + name: 'removePermission' + } + ], + show: { + driveType: ['share'] + } + }, + // File Parameters + { + label: 'File ID', + name: 'fileId', + type: 'string', + description: 'File ID for file operations', + show: { + fileActions: ['getFile', 'updateFile', 'deleteFile', 'copyFile', 'downloadFile'] + }, + additionalParams: true, + optional: true + }, + { + label: 'File ID', + name: 'fileId', + type: 'string', + description: 'File ID for sharing operations', + show: { + shareActions: ['shareFile', 'getPermissions', 'removePermission'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Folder ID', + name: 'folderId', + type: 'string', + description: 'Folder ID for folder operations', + show: { + folderActions: ['listFolderContents', 'deleteFolder'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Permission ID', + name: 'permissionId', + type: 'string', + description: 'Permission ID to remove', + show: { + shareActions: ['removePermission'] + }, + additionalParams: true, + optional: true + }, + { + label: 'File Name', + name: 'fileName', + type: 'string', + description: 'Name of the file', + show: { + fileActions: ['createFile', 'copyFile'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Folder Name', + name: 'fileName', + type: 'string', + description: 'Name of the folder', + show: { + folderActions: ['createFolder'] + }, + additionalParams: true, + optional: true + }, + { + label: 'File Content', + name: 'fileContent', + type: 'string', + description: 'Content of the file (for text files)', + show: { + fileActions: ['createFile'] + }, + additionalParams: true, + optional: true + }, + { + label: 'MIME Type', + name: 'mimeType', + type: 'string', + description: 'MIME type of the file (e.g., text/plain, application/pdf)', + show: { + fileActions: ['createFile'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Parent Folder ID', + name: 'parentFolderId', + type: 'string', + description: 'ID of the parent folder (comma-separated for multiple parents)', + show: { + fileActions: ['createFile', 'copyFile'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Parent Folder ID', + name: 'parentFolderId', + type: 'string', + description: 'ID of the parent folder for the new folder', + show: { + folderActions: ['createFolder'] + }, + additionalParams: true, + optional: true + }, + { + label: 'File Description', + name: 'description', + type: 'string', + description: 'File description', + show: { + fileActions: ['createFile', 'updateFile'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Folder Description', + name: 'description', + type: 'string', + description: 'Folder description', + show: { + folderActions: ['createFolder'] + }, + additionalParams: true, + optional: true + }, + // Search Parameters + { + label: 'Search Query', + name: 'searchQuery', + type: 'string', + description: 'Search query using Google Drive search syntax', + show: { + searchActions: ['searchFiles'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Max Results', + name: 'maxResults', + type: 'number', + description: 'Maximum number of results to return (1-1000)', + default: 10, + show: { + fileActions: ['listFiles'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Max Results', + name: 'maxResults', + type: 'number', + description: 'Maximum number of results to return (1-1000)', + default: 10, + show: { + searchActions: ['searchFiles'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Order By', + name: 'orderBy', + type: 'options', + description: 'Sort order for file results', + options: [ + { + label: 'Name', + name: 'name' + }, + { + label: 'Created Time', + name: 'createdTime' + }, + { + label: 'Modified Time', + name: 'modifiedTime' + }, + { + label: 'Size', + name: 'quotaBytesUsed' + }, + { + label: 'Folder', + name: 'folder' + } + ], + show: { + fileActions: ['listFiles'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Order By', + name: 'orderBy', + type: 'options', + description: 'Sort order for search results', + options: [ + { + label: 'Name', + name: 'name' + }, + { + label: 'Created Time', + name: 'createdTime' + }, + { + label: 'Modified Time', + name: 'modifiedTime' + }, + { + label: 'Size', + name: 'quotaBytesUsed' + }, + { + label: 'Folder', + name: 'folder' + } + ], + show: { + searchActions: ['searchFiles'] + }, + additionalParams: true, + optional: true + }, + // Share Parameters + { + label: 'Share Role', + name: 'shareRole', + type: 'options', + description: 'Permission role for sharing', + options: [ + { + label: 'Reader', + name: 'reader' + }, + { + label: 'Writer', + name: 'writer' + }, + { + label: 'Commenter', + name: 'commenter' + }, + { + label: 'Owner', + name: 'owner' + } + ], + show: { + shareActions: ['shareFile'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Share Type', + name: 'shareType', + type: 'options', + description: 'Type of permission', + options: [ + { + label: 'User', + name: 'user' + }, + { + label: 'Group', + name: 'group' + }, + { + label: 'Domain', + name: 'domain' + }, + { + label: 'Anyone', + name: 'anyone' + } + ], + show: { + shareActions: ['shareFile'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Email Address', + name: 'emailAddress', + type: 'string', + description: 'Email address for user/group sharing', + show: { + shareActions: ['shareFile'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Domain Name', + name: 'domainName', + type: 'string', + description: 'Domain name for domain sharing', + show: { + shareActions: ['shareFile'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Send Notification Email', + name: 'sendNotificationEmail', + type: 'boolean', + description: 'Whether to send notification emails when sharing', + default: true, + show: { + shareActions: ['shareFile'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Email Message', + name: 'emailMessage', + type: 'string', + description: 'Custom message to include in notification email', + show: { + shareActions: ['shareFile'] + }, + additionalParams: true, + optional: true + }, + // Advanced Parameters for File Actions + { + label: 'Include Items From All Drives', + name: 'includeItemsFromAllDrives', + type: 'boolean', + description: 'Include items from all drives (shared drives)', + show: { + fileActions: ['listFiles'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Include Items From All Drives', + name: 'includeItemsFromAllDrives', + type: 'boolean', + description: 'Include items from all drives (shared drives)', + show: { + searchActions: ['searchFiles'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Supports All Drives', + name: 'supportsAllDrives', + type: 'boolean', + description: 'Whether the application supports both My Drives and shared drives', + show: { + fileActions: ['listFiles', 'getFile', 'createFile', 'updateFile', 'deleteFile', 'copyFile', 'downloadFile'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Supports All Drives', + name: 'supportsAllDrives', + type: 'boolean', + description: 'Whether the application supports both My Drives and shared drives', + show: { + folderActions: ['createFolder', 'listFolderContents', 'deleteFolder'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Supports All Drives', + name: 'supportsAllDrives', + type: 'boolean', + description: 'Whether the application supports both My Drives and shared drives', + show: { + searchActions: ['searchFiles'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Supports All Drives', + name: 'supportsAllDrives', + type: 'boolean', + description: 'Whether the application supports both My Drives and shared drives', + show: { + shareActions: ['shareFile', 'getPermissions', 'removePermission'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Fields', + name: 'fields', + type: 'string', + description: 'Specific fields to include in response (e.g., "files(id,name,mimeType)")', + show: { + fileActions: ['listFiles', 'getFile'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Acknowledge Abuse', + name: 'acknowledgeAbuse', + type: 'boolean', + description: 'Acknowledge the risk of downloading known malware or abusive files', + show: { + fileActions: ['getFile', 'downloadFile'] + }, + additionalParams: true, + optional: true + } + ] + } + + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + let credentialData = await getCredentialData(nodeData.credential ?? '', options) + credentialData = await refreshOAuth2Token(nodeData.credential ?? '', credentialData, options) + const accessToken = getCredentialParam('access_token', credentialData, nodeData) + + if (!accessToken) { + throw new Error('No access token found in credential') + } + + const driveType = nodeData.inputs?.driveType as string + const fileActions = convertMultiOptionsToStringArray(nodeData.inputs?.fileActions) + const folderActions = convertMultiOptionsToStringArray(nodeData.inputs?.folderActions) + const searchActions = convertMultiOptionsToStringArray(nodeData.inputs?.searchActions) + const shareActions = convertMultiOptionsToStringArray(nodeData.inputs?.shareActions) + + // Combine all actions based on type + let actions: string[] = [] + if (driveType === 'file') { + actions = fileActions + } else if (driveType === 'folder') { + actions = folderActions + } else if (driveType === 'search') { + actions = searchActions + } else if (driveType === 'share') { + actions = shareActions + } + + const defaultParams = this.transformNodeInputsToToolArgs(nodeData) + + const tools = createGoogleDriveTools({ + accessToken, + actions, + defaultParams + }) + + return tools + } + + transformNodeInputsToToolArgs(nodeData: INodeData): Record { + // Collect default parameters from inputs + const defaultParams: Record = {} + + // Add parameters based on the inputs provided + if (nodeData.inputs?.fileId) defaultParams.fileId = nodeData.inputs.fileId + if (nodeData.inputs?.folderId) defaultParams.folderId = nodeData.inputs.folderId + if (nodeData.inputs?.permissionId) defaultParams.permissionId = nodeData.inputs.permissionId + if (nodeData.inputs?.fileName) defaultParams.name = nodeData.inputs.fileName + if (nodeData.inputs?.fileContent) defaultParams.content = nodeData.inputs.fileContent + if (nodeData.inputs?.mimeType) defaultParams.mimeType = nodeData.inputs.mimeType + if (nodeData.inputs?.parentFolderId) defaultParams.parents = nodeData.inputs.parentFolderId + if (nodeData.inputs?.description) defaultParams.description = nodeData.inputs.description + if (nodeData.inputs?.searchQuery) defaultParams.query = nodeData.inputs.searchQuery + if (nodeData.inputs?.maxResults) defaultParams.pageSize = nodeData.inputs.maxResults + if (nodeData.inputs?.orderBy) defaultParams.orderBy = nodeData.inputs.orderBy + if (nodeData.inputs?.shareRole) defaultParams.role = nodeData.inputs.shareRole + if (nodeData.inputs?.shareType) defaultParams.type = nodeData.inputs.shareType + if (nodeData.inputs?.emailAddress) defaultParams.emailAddress = nodeData.inputs.emailAddress + if (nodeData.inputs?.domainName) defaultParams.domain = nodeData.inputs.domainName + if (nodeData.inputs?.sendNotificationEmail !== undefined) + defaultParams.sendNotificationEmail = nodeData.inputs.sendNotificationEmail + if (nodeData.inputs?.emailMessage) defaultParams.emailMessage = nodeData.inputs.emailMessage + if (nodeData.inputs?.includeItemsFromAllDrives !== undefined) + defaultParams.includeItemsFromAllDrives = nodeData.inputs.includeItemsFromAllDrives + if (nodeData.inputs?.supportsAllDrives !== undefined) defaultParams.supportsAllDrives = nodeData.inputs.supportsAllDrives + if (nodeData.inputs?.fields) defaultParams.fields = nodeData.inputs.fields + if (nodeData.inputs?.acknowledgeAbuse !== undefined) defaultParams.acknowledgeAbuse = nodeData.inputs.acknowledgeAbuse + + return defaultParams + } +} + +module.exports = { nodeClass: GoogleDrive_Tools } diff --git a/packages/components/nodes/tools/GoogleDrive/core.ts b/packages/components/nodes/tools/GoogleDrive/core.ts new file mode 100644 index 00000000000..62377f5dc02 --- /dev/null +++ b/packages/components/nodes/tools/GoogleDrive/core.ts @@ -0,0 +1,982 @@ +import { z } from 'zod' +import fetch from 'node-fetch' +import { DynamicStructuredTool } from '../OpenAPIToolkit/core' +import { TOOL_ARGS_PREFIX } from '../../../src/agents' + +export const desc = `Use this when you want to access Google Drive API for managing files and folders` + +export interface Headers { + [key: string]: string +} + +export interface Body { + [key: string]: any +} + +export interface RequestParameters { + headers?: Headers + body?: Body + url?: string + description?: string + name?: string + actions?: string[] + accessToken?: string + defaultParams?: any +} + +// Define schemas for different Google Drive operations + +// File Schemas +const ListFilesSchema = z.object({ + pageSize: z.number().optional().default(10).describe('Maximum number of files to return (1-1000)'), + pageToken: z.string().optional().describe('Token for next page of results'), + orderBy: z.string().optional().describe('Sort order (name, folder, createdTime, modifiedTime, etc.)'), + query: z.string().optional().describe('Search query (e.g., "name contains \'hello\'")'), + spaces: z.string().optional().default('drive').describe('Spaces to search (drive, appDataFolder, photos)'), + fields: z.string().optional().describe('Fields to include in response'), + includeItemsFromAllDrives: z.boolean().optional().describe('Include items from all drives'), + supportsAllDrives: z.boolean().optional().describe('Whether the requesting application supports both My Drives and shared drives') +}) + +const GetFileSchema = z.object({ + fileId: z.string().describe('File ID'), + fields: z.string().optional().describe('Fields to include in response'), + supportsAllDrives: z.boolean().optional().describe('Whether the requesting application supports both My Drives and shared drives'), + acknowledgeAbuse: z + .boolean() + .optional() + .describe('Whether the user is acknowledging the risk of downloading known malware or other abusive files') +}) + +const CreateFileSchema = z.object({ + name: z.string().describe('File name'), + parents: z.string().optional().describe('Comma-separated list of parent folder IDs'), + mimeType: z.string().optional().describe('MIME type of the file'), + description: z.string().optional().describe('File description'), + content: z.string().optional().describe('File content (for text files)'), + supportsAllDrives: z.boolean().optional().describe('Whether the requesting application supports both My Drives and shared drives') +}) + +const UpdateFileSchema = z.object({ + fileId: z.string().describe('File ID to update'), + name: z.string().optional().describe('New file name'), + description: z.string().optional().describe('New file description'), + starred: z.boolean().optional().describe('Whether the file is starred'), + trashed: z.boolean().optional().describe('Whether the file is trashed'), + parents: z.string().optional().describe('Comma-separated list of new parent folder IDs'), + supportsAllDrives: z.boolean().optional().describe('Whether the requesting application supports both My Drives and shared drives') +}) + +const DeleteFileSchema = z.object({ + fileId: z.string().describe('File ID to delete'), + supportsAllDrives: z.boolean().optional().describe('Whether the requesting application supports both My Drives and shared drives') +}) + +const CopyFileSchema = z.object({ + fileId: z.string().describe('File ID to copy'), + name: z.string().describe('Name for the copied file'), + parents: z.string().optional().describe('Comma-separated list of parent folder IDs for the copy'), + supportsAllDrives: z.boolean().optional().describe('Whether the requesting application supports both My Drives and shared drives') +}) + +const DownloadFileSchema = z.object({ + fileId: z.string().describe('File ID to download'), + acknowledgeAbuse: z + .boolean() + .optional() + .describe('Whether the user is acknowledging the risk of downloading known malware or other abusive files'), + supportsAllDrives: z.boolean().optional().describe('Whether the requesting application supports both My Drives and shared drives') +}) + +const CreateFolderSchema = z.object({ + name: z.string().describe('Folder name'), + parents: z.string().optional().describe('Comma-separated list of parent folder IDs'), + description: z.string().optional().describe('Folder description'), + supportsAllDrives: z.boolean().optional().describe('Whether the requesting application supports both My Drives and shared drives') +}) + +const SearchFilesSchema = z.object({ + query: z.string().describe('Search query using Google Drive search syntax'), + pageSize: z.number().optional().default(10).describe('Maximum number of files to return'), + orderBy: z.string().optional().describe('Sort order'), + includeItemsFromAllDrives: z.boolean().optional().describe('Include items from all drives'), + supportsAllDrives: z.boolean().optional().describe('Whether the requesting application supports both My Drives and shared drives') +}) + +const ShareFileSchema = z.object({ + fileId: z.string().describe('File ID to share'), + role: z.enum(['reader', 'writer', 'commenter', 'owner']).describe('Permission role'), + type: z.enum(['user', 'group', 'domain', 'anyone']).describe('Permission type'), + emailAddress: z.string().optional().describe('Email address (required for user/group types)'), + domain: z.string().optional().describe('Domain name (required for domain type)'), + allowFileDiscovery: z.boolean().optional().describe('Whether the file can be discovered by search'), + sendNotificationEmail: z.boolean().optional().default(true).describe('Whether to send notification emails'), + emailMessage: z.string().optional().describe('Custom message to include in notification email'), + supportsAllDrives: z.boolean().optional().describe('Whether the requesting application supports both My Drives and shared drives') +}) + +class BaseGoogleDriveTool extends DynamicStructuredTool { + protected accessToken: string = '' + + constructor(args: any) { + super(args) + this.accessToken = args.accessToken ?? '' + } + + async makeGoogleDriveRequest({ + endpoint, + method = 'GET', + body, + params + }: { + endpoint: string + method?: string + body?: any + params?: any + }): Promise { + const baseUrl = 'https://www.googleapis.com/drive/v3' + const url = `${baseUrl}/${endpoint}` + + const headers: { [key: string]: string } = { + Authorization: `Bearer ${this.accessToken}`, + Accept: 'application/json', + ...this.headers + } + + if (method !== 'GET' && body) { + headers['Content-Type'] = 'application/json' + } + + const response = await fetch(url, { + method, + headers, + body: body ? (typeof body === 'string' ? body : JSON.stringify(body)) : undefined + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`Google Drive API Error ${response.status}: ${response.statusText} - ${errorText}`) + } + + const data = await response.text() + return data + TOOL_ARGS_PREFIX + JSON.stringify(params) + } +} + +// File Tools +class ListFilesTool extends BaseGoogleDriveTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'list_files', + description: 'List files and folders from Google Drive', + schema: ListFilesSchema, + baseUrl: '', + method: 'GET', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const queryParams = new URLSearchParams() + + if (params.pageSize) queryParams.append('pageSize', params.pageSize.toString()) + if (params.pageToken) queryParams.append('pageToken', params.pageToken) + if (params.orderBy) queryParams.append('orderBy', params.orderBy) + if (params.query) queryParams.append('q', params.query) + if (params.spaces) queryParams.append('spaces', params.spaces) + if (params.fields) queryParams.append('fields', params.fields) + if (params.includeItemsFromAllDrives) queryParams.append('includeItemsFromAllDrives', params.includeItemsFromAllDrives.toString()) + if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString()) + + const endpoint = `files?${queryParams.toString()}` + + try { + const response = await this.makeGoogleDriveRequest({ endpoint, params }) + return response + } catch (error) { + return `Error listing files: ${error}` + } + } +} + +class GetFileTool extends BaseGoogleDriveTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'get_file', + description: 'Get file metadata from Google Drive', + schema: GetFileSchema, + baseUrl: '', + method: 'GET', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const queryParams = new URLSearchParams() + + if (params.fields) queryParams.append('fields', params.fields) + if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString()) + if (params.acknowledgeAbuse) queryParams.append('acknowledgeAbuse', params.acknowledgeAbuse.toString()) + + const endpoint = `files/${encodeURIComponent(params.fileId)}?${queryParams.toString()}` + + try { + const response = await this.makeGoogleDriveRequest({ endpoint, params }) + return response + } catch (error) { + return `Error getting file: ${error}` + } + } +} + +class CreateFileTool extends BaseGoogleDriveTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'create_file', + description: 'Create a new file in Google Drive', + schema: CreateFileSchema, + baseUrl: '', + method: 'POST', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + // Validate required parameters + if (!params.name) { + throw new Error('File name is required') + } + + const queryParams = new URLSearchParams() + if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString()) + + // Prepare metadata + const fileMetadata: any = { + name: params.name + } + + if (params.parents) { + // Validate parent folder IDs format + const parentIds = params.parents + .split(',') + .map((p: string) => p.trim()) + .filter((p: string) => p.length > 0) + if (parentIds.length > 0) { + fileMetadata.parents = parentIds + } + } + if (params.mimeType) fileMetadata.mimeType = params.mimeType + if (params.description) fileMetadata.description = params.description + + // Determine upload type based on content and metadata + if (!params.content) { + // Metadata-only upload (no file content) - standard endpoint + const endpoint = `files?${queryParams.toString()}` + const response = await this.makeGoogleDriveRequest({ + endpoint, + method: 'POST', + body: fileMetadata, + params + }) + return response + } else { + // Validate content + if (typeof params.content !== 'string') { + throw new Error('File content must be a string') + } + + // Check if we have metadata beyond just the name + const hasAdditionalMetadata = params.parents || params.description || params.mimeType + + if (!hasAdditionalMetadata) { + // Simple upload (uploadType=media) - only file content, basic metadata + return await this.performSimpleUpload(params, queryParams) + } else { + // Multipart upload (uploadType=multipart) - file content + metadata + return await this.performMultipartUpload(params, fileMetadata, queryParams) + } + } + } catch (error) { + return `Error creating file: ${error}` + } + } + + private async performSimpleUpload(params: any, queryParams: URLSearchParams): Promise { + // Simple upload: POST https://www.googleapis.com/upload/drive/v3/files?uploadType=media + queryParams.append('uploadType', 'media') + const url = `https://www.googleapis.com/upload/drive/v3/files?${queryParams.toString()}` + + const headers: { [key: string]: string } = { + Authorization: `Bearer ${this.accessToken}`, + 'Content-Type': params.mimeType || 'application/octet-stream', + 'Content-Length': Buffer.byteLength(params.content, 'utf8').toString() + } + + const response = await fetch(url, { + method: 'POST', + headers, + body: params.content + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`Google Drive API Error ${response.status}: ${response.statusText} - ${errorText}`) + } + + const data = await response.text() + return data + TOOL_ARGS_PREFIX + JSON.stringify(params) + } + + private async performMultipartUpload(params: any, fileMetadata: any, queryParams: URLSearchParams): Promise { + // Multipart upload: POST https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart + queryParams.append('uploadType', 'multipart') + const url = `https://www.googleapis.com/upload/drive/v3/files?${queryParams.toString()}` + + // Create multipart/related body according to RFC 2387 + const boundary = '-------314159265358979323846' + + // Build multipart body - RFC 2387 format + let body = `--${boundary}\r\n` + + // Part 1: Metadata (application/json; charset=UTF-8) + body += 'Content-Type: application/json; charset=UTF-8\r\n\r\n' + body += JSON.stringify(fileMetadata) + '\r\n' + + // Part 2: Media content (any MIME type) + body += `--${boundary}\r\n` + body += `Content-Type: ${params.mimeType || 'application/octet-stream'}\r\n\r\n` + body += params.content + '\r\n' + + // Close boundary + body += `--${boundary}--` + + const headers: { [key: string]: string } = { + Authorization: `Bearer ${this.accessToken}`, + 'Content-Type': `multipart/related; boundary="${boundary}"`, + 'Content-Length': Buffer.byteLength(body, 'utf8').toString() + } + + try { + const response = await fetch(url, { + method: 'POST', + headers, + body: body + }) + + if (!response.ok) { + const errorText = await response.text() + console.error('Multipart upload failed:', { + url, + headers: { ...headers, Authorization: '[REDACTED]' }, + metadata: fileMetadata, + contentLength: params.content?.length || 0, + error: errorText + }) + throw new Error(`Google Drive API Error ${response.status}: ${response.statusText} - ${errorText}`) + } + + const data = await response.text() + return data + TOOL_ARGS_PREFIX + JSON.stringify(params) + } catch (error) { + throw new Error(`Multipart upload failed: ${error}`) + } + } +} + +class UpdateFileTool extends BaseGoogleDriveTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'update_file', + description: 'Update file metadata in Google Drive', + schema: UpdateFileSchema, + baseUrl: '', + method: 'PATCH', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const updateData: any = {} + + if (params.name) updateData.name = params.name + if (params.description) updateData.description = params.description + if (params.starred !== undefined) updateData.starred = params.starred + if (params.trashed !== undefined) updateData.trashed = params.trashed + + const queryParams = new URLSearchParams() + if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString()) + + const endpoint = `files/${encodeURIComponent(params.fileId)}?${queryParams.toString()}` + + const response = await this.makeGoogleDriveRequest({ + endpoint, + method: 'PATCH', + body: updateData, + params + }) + return response + } catch (error) { + return `Error updating file: ${error}` + } + } +} + +class DeleteFileTool extends BaseGoogleDriveTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'delete_file', + description: 'Delete a file from Google Drive', + schema: DeleteFileSchema, + baseUrl: '', + method: 'DELETE', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const queryParams = new URLSearchParams() + if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString()) + + const endpoint = `files/${encodeURIComponent(params.fileId)}?${queryParams.toString()}` + + await this.makeGoogleDriveRequest({ + endpoint, + method: 'DELETE', + params + }) + return `File deleted successfully` + } catch (error) { + return `Error deleting file: ${error}` + } + } +} + +class CopyFileTool extends BaseGoogleDriveTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'copy_file', + description: 'Copy a file in Google Drive', + schema: CopyFileSchema, + baseUrl: '', + method: 'POST', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const copyData: any = { + name: params.name + } + + if (params.parents) { + copyData.parents = params.parents.split(',').map((p: string) => p.trim()) + } + + const queryParams = new URLSearchParams() + if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString()) + + const endpoint = `files/${encodeURIComponent(params.fileId)}/copy?${queryParams.toString()}` + + const response = await this.makeGoogleDriveRequest({ + endpoint, + method: 'POST', + body: copyData, + params + }) + return response + } catch (error) { + return `Error copying file: ${error}` + } + } +} + +class DownloadFileTool extends BaseGoogleDriveTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'download_file', + description: 'Download a file from Google Drive', + schema: DownloadFileSchema, + baseUrl: '', + method: 'GET', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const queryParams = new URLSearchParams() + queryParams.append('alt', 'media') + if (params.acknowledgeAbuse) queryParams.append('acknowledgeAbuse', params.acknowledgeAbuse.toString()) + if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString()) + + const endpoint = `files/${encodeURIComponent(params.fileId)}?${queryParams.toString()}` + + const response = await this.makeGoogleDriveRequest({ endpoint, params }) + return response + } catch (error) { + return `Error downloading file: ${error}` + } + } +} + +class CreateFolderTool extends BaseGoogleDriveTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'create_folder', + description: 'Create a new folder in Google Drive', + schema: CreateFolderSchema, + baseUrl: '', + method: 'POST', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const folderData: any = { + name: params.name, + mimeType: 'application/vnd.google-apps.folder' + } + + if (params.parents) { + folderData.parents = params.parents.split(',').map((p: string) => p.trim()) + } + if (params.description) folderData.description = params.description + + const queryParams = new URLSearchParams() + if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString()) + + const endpoint = `files?${queryParams.toString()}` + + const response = await this.makeGoogleDriveRequest({ + endpoint, + method: 'POST', + body: folderData, + params + }) + return response + } catch (error) { + return `Error creating folder: ${error}` + } + } +} + +class SearchFilesTool extends BaseGoogleDriveTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'search_files', + description: 'Search files in Google Drive', + schema: SearchFilesSchema, + baseUrl: '', + method: 'GET', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const queryParams = new URLSearchParams() + queryParams.append('q', params.query) + if (params.pageSize) queryParams.append('pageSize', params.pageSize.toString()) + if (params.orderBy) queryParams.append('orderBy', params.orderBy) + if (params.includeItemsFromAllDrives) + queryParams.append('includeItemsFromAllDrives', params.includeItemsFromAllDrives.toString()) + if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString()) + + const endpoint = `files?${queryParams.toString()}` + + const response = await this.makeGoogleDriveRequest({ endpoint, params }) + return response + } catch (error) { + return `Error searching files: ${error}` + } + } +} + +class ShareFileTool extends BaseGoogleDriveTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'share_file', + description: 'Share a file in Google Drive', + schema: ShareFileSchema, + baseUrl: '', + method: 'POST', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const permissionData: any = { + role: params.role, + type: params.type + } + + if (params.emailAddress) permissionData.emailAddress = params.emailAddress + if (params.domain) permissionData.domain = params.domain + if (params.allowFileDiscovery !== undefined) permissionData.allowFileDiscovery = params.allowFileDiscovery + + const queryParams = new URLSearchParams() + if (params.sendNotificationEmail !== undefined) + queryParams.append('sendNotificationEmail', params.sendNotificationEmail.toString()) + if (params.emailMessage) queryParams.append('emailMessage', params.emailMessage) + if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString()) + + const endpoint = `files/${encodeURIComponent(params.fileId)}/permissions?${queryParams.toString()}` + + const response = await this.makeGoogleDriveRequest({ + endpoint, + method: 'POST', + body: permissionData, + params + }) + return response + } catch (error) { + return `Error sharing file: ${error}` + } + } +} + +class ListFolderContentsTool extends BaseGoogleDriveTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'list_folder_contents', + description: 'List contents of a specific folder in Google Drive', + schema: z.object({ + folderId: z.string().describe('Folder ID to list contents from'), + pageSize: z.number().optional().default(10).describe('Maximum number of files to return'), + orderBy: z.string().optional().describe('Sort order'), + includeItemsFromAllDrives: z.boolean().optional().describe('Include items from all drives'), + supportsAllDrives: z + .boolean() + .optional() + .describe('Whether the requesting application supports both My Drives and shared drives') + }), + baseUrl: '', + method: 'GET', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const queryParams = new URLSearchParams() + queryParams.append('q', `'${params.folderId}' in parents`) + if (params.pageSize) queryParams.append('pageSize', params.pageSize.toString()) + if (params.orderBy) queryParams.append('orderBy', params.orderBy) + if (params.includeItemsFromAllDrives) + queryParams.append('includeItemsFromAllDrives', params.includeItemsFromAllDrives.toString()) + if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString()) + + const endpoint = `files?${queryParams.toString()}` + + const response = await this.makeGoogleDriveRequest({ endpoint, params }) + return response + } catch (error) { + return `Error listing folder contents: ${error}` + } + } +} + +class DeleteFolderTool extends BaseGoogleDriveTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'delete_folder', + description: 'Delete a folder from Google Drive', + schema: z.object({ + folderId: z.string().describe('Folder ID to delete'), + supportsAllDrives: z + .boolean() + .optional() + .describe('Whether the requesting application supports both My Drives and shared drives') + }), + baseUrl: '', + method: 'DELETE', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const queryParams = new URLSearchParams() + if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString()) + + const endpoint = `files/${encodeURIComponent(params.folderId)}?${queryParams.toString()}` + + await this.makeGoogleDriveRequest({ + endpoint, + method: 'DELETE', + params + }) + return `Folder deleted successfully` + } catch (error) { + return `Error deleting folder: ${error}` + } + } +} + +class GetPermissionsTool extends BaseGoogleDriveTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'get_permissions', + description: 'Get permissions for a file in Google Drive', + schema: z.object({ + fileId: z.string().describe('File ID to get permissions for'), + supportsAllDrives: z + .boolean() + .optional() + .describe('Whether the requesting application supports both My Drives and shared drives') + }), + baseUrl: '', + method: 'GET', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const queryParams = new URLSearchParams() + if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString()) + + const endpoint = `files/${encodeURIComponent(params.fileId)}/permissions?${queryParams.toString()}` + + const response = await this.makeGoogleDriveRequest({ endpoint, params }) + return response + } catch (error) { + return `Error getting permissions: ${error}` + } + } +} + +class RemovePermissionTool extends BaseGoogleDriveTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'remove_permission', + description: 'Remove a permission from a file in Google Drive', + schema: z.object({ + fileId: z.string().describe('File ID to remove permission from'), + permissionId: z.string().describe('Permission ID to remove'), + supportsAllDrives: z + .boolean() + .optional() + .describe('Whether the requesting application supports both My Drives and shared drives') + }), + baseUrl: '', + method: 'DELETE', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const queryParams = new URLSearchParams() + if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString()) + + const endpoint = `files/${encodeURIComponent(params.fileId)}/permissions/${encodeURIComponent( + params.permissionId + )}?${queryParams.toString()}` + + await this.makeGoogleDriveRequest({ + endpoint, + method: 'DELETE', + params + }) + return `Permission removed successfully` + } catch (error) { + return `Error removing permission: ${error}` + } + } +} + +export const createGoogleDriveTools = (args?: RequestParameters): DynamicStructuredTool[] => { + const tools: DynamicStructuredTool[] = [] + const actions = args?.actions || [] + const accessToken = args?.accessToken || '' + const defaultParams = args?.defaultParams || {} + + if (actions.includes('listFiles')) { + tools.push(new ListFilesTool({ accessToken, defaultParams })) + } + + if (actions.includes('getFile')) { + tools.push(new GetFileTool({ accessToken, defaultParams })) + } + + if (actions.includes('createFile')) { + tools.push(new CreateFileTool({ accessToken, defaultParams })) + } + + if (actions.includes('updateFile')) { + tools.push(new UpdateFileTool({ accessToken, defaultParams })) + } + + if (actions.includes('deleteFile')) { + tools.push(new DeleteFileTool({ accessToken, defaultParams })) + } + + if (actions.includes('copyFile')) { + tools.push(new CopyFileTool({ accessToken, defaultParams })) + } + + if (actions.includes('downloadFile')) { + tools.push(new DownloadFileTool({ accessToken, defaultParams })) + } + + if (actions.includes('createFolder')) { + tools.push(new CreateFolderTool({ accessToken, defaultParams })) + } + + if (actions.includes('listFolderContents')) { + tools.push(new ListFolderContentsTool({ accessToken, defaultParams })) + } + + if (actions.includes('deleteFolder')) { + tools.push(new DeleteFolderTool({ accessToken, defaultParams })) + } + + if (actions.includes('searchFiles')) { + tools.push(new SearchFilesTool({ accessToken, defaultParams })) + } + + if (actions.includes('shareFile')) { + tools.push(new ShareFileTool({ accessToken, defaultParams })) + } + + if (actions.includes('getPermissions')) { + tools.push(new GetPermissionsTool({ accessToken, defaultParams })) + } + + if (actions.includes('removePermission')) { + tools.push(new RemovePermissionTool({ accessToken, defaultParams })) + } + + return tools +} diff --git a/packages/components/nodes/tools/GoogleDrive/google-drive.svg b/packages/components/nodes/tools/GoogleDrive/google-drive.svg new file mode 100644 index 00000000000..03b2f21290a --- /dev/null +++ b/packages/components/nodes/tools/GoogleDrive/google-drive.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/tools/GoogleSheets/GoogleSheets.ts b/packages/components/nodes/tools/GoogleSheets/GoogleSheets.ts new file mode 100644 index 00000000000..60f15903ac7 --- /dev/null +++ b/packages/components/nodes/tools/GoogleSheets/GoogleSheets.ts @@ -0,0 +1,368 @@ +import { convertMultiOptionsToStringArray, getCredentialData, getCredentialParam, refreshOAuth2Token } from '../../../src/utils' +import { createGoogleSheetsTools } from './core' +import type { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' + +class GoogleSheets_Tools implements INode { + label: string + name: string + version: number + type: string + icon: string + category: string + description: string + baseClasses: string[] + credential: INodeParams + inputs: INodeParams[] + + constructor() { + this.label = 'Google Sheets' + this.name = 'googleSheetsTool' + this.version = 1.0 + this.type = 'GoogleSheets' + this.icon = 'google-sheets.svg' + this.category = 'Tools' + this.description = 'Perform Google Sheets operations such as managing spreadsheets, reading and writing values' + this.baseClasses = ['Tool'] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['googleSheetsOAuth2'] + } + this.inputs = [ + { + label: 'Type', + name: 'sheetsType', + type: 'options', + description: 'Type of Google Sheets operation', + options: [ + { + label: 'Spreadsheet', + name: 'spreadsheet' + }, + { + label: 'Values', + name: 'values' + } + ] + }, + // Spreadsheet Actions + { + label: 'Spreadsheet Actions', + name: 'spreadsheetActions', + type: 'multiOptions', + description: 'Actions to perform on spreadsheets', + options: [ + { + label: 'Create Spreadsheet', + name: 'createSpreadsheet' + }, + { + label: 'Get Spreadsheet', + name: 'getSpreadsheet' + }, + { + label: 'Update Spreadsheet', + name: 'updateSpreadsheet' + } + ], + show: { + sheetsType: ['spreadsheet'] + } + }, + // Values Actions + { + label: 'Values Actions', + name: 'valuesActions', + type: 'multiOptions', + description: 'Actions to perform on sheet values', + options: [ + { + label: 'Get Values', + name: 'getValues' + }, + { + label: 'Update Values', + name: 'updateValues' + }, + { + label: 'Append Values', + name: 'appendValues' + }, + { + label: 'Clear Values', + name: 'clearValues' + }, + { + label: 'Batch Get Values', + name: 'batchGetValues' + }, + { + label: 'Batch Update Values', + name: 'batchUpdateValues' + }, + { + label: 'Batch Clear Values', + name: 'batchClearValues' + } + ], + show: { + sheetsType: ['values'] + } + }, + // Spreadsheet Parameters + { + label: 'Spreadsheet ID', + name: 'spreadsheetId', + type: 'string', + description: 'The ID of the spreadsheet', + show: { + sheetsType: ['spreadsheet', 'values'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Title', + name: 'title', + type: 'string', + description: 'The title of the spreadsheet', + show: { + spreadsheetActions: ['createSpreadsheet', 'updateSpreadsheet'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Sheet Count', + name: 'sheetCount', + type: 'number', + description: 'Number of sheets to create', + default: 1, + show: { + spreadsheetActions: ['createSpreadsheet'] + }, + additionalParams: true, + optional: true + }, + // Values Parameters + { + label: 'Range', + name: 'range', + type: 'string', + description: 'The range to read/write (e.g., A1:B2, Sheet1!A1:C10)', + show: { + valuesActions: ['getValues', 'updateValues', 'clearValues'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Ranges', + name: 'ranges', + type: 'string', + description: 'Comma-separated list of ranges for batch operations', + show: { + valuesActions: ['batchGetValues', 'batchClearValues'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Values', + name: 'values', + type: 'string', + description: 'JSON array of values to write (e.g., [["A1", "B1"], ["A2", "B2"]])', + show: { + valuesActions: ['updateValues', 'appendValues', 'batchUpdateValues'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Value Input Option', + name: 'valueInputOption', + type: 'options', + description: 'How input data should be interpreted', + options: [ + { + label: 'Raw', + name: 'RAW' + }, + { + label: 'User Entered', + name: 'USER_ENTERED' + } + ], + default: 'USER_ENTERED', + show: { + valuesActions: ['updateValues', 'appendValues', 'batchUpdateValues'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Value Render Option', + name: 'valueRenderOption', + type: 'options', + description: 'How values should be represented in the output', + options: [ + { + label: 'Formatted Value', + name: 'FORMATTED_VALUE' + }, + { + label: 'Unformatted Value', + name: 'UNFORMATTED_VALUE' + }, + { + label: 'Formula', + name: 'FORMULA' + } + ], + default: 'FORMATTED_VALUE', + show: { + valuesActions: ['getValues', 'batchGetValues'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Date Time Render Option', + name: 'dateTimeRenderOption', + type: 'options', + description: 'How dates, times, and durations should be represented', + options: [ + { + label: 'Serial Number', + name: 'SERIAL_NUMBER' + }, + { + label: 'Formatted String', + name: 'FORMATTED_STRING' + } + ], + default: 'FORMATTED_STRING', + show: { + valuesActions: ['getValues', 'batchGetValues'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Insert Data Option', + name: 'insertDataOption', + type: 'options', + description: 'How data should be inserted', + options: [ + { + label: 'Overwrite', + name: 'OVERWRITE' + }, + { + label: 'Insert Rows', + name: 'INSERT_ROWS' + } + ], + default: 'OVERWRITE', + show: { + valuesActions: ['appendValues'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Include Grid Data', + name: 'includeGridData', + type: 'boolean', + description: 'True if grid data should be returned', + default: false, + show: { + spreadsheetActions: ['getSpreadsheet'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Major Dimension', + name: 'majorDimension', + type: 'options', + description: 'The major dimension that results should use', + options: [ + { + label: 'Rows', + name: 'ROWS' + }, + { + label: 'Columns', + name: 'COLUMNS' + } + ], + default: 'ROWS', + show: { + valuesActions: ['getValues', 'updateValues', 'appendValues', 'batchGetValues', 'batchUpdateValues'] + }, + additionalParams: true, + optional: true + } + ] + } + + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + const sheetsType = nodeData.inputs?.sheetsType as string + + let credentialData = await getCredentialData(nodeData.credential ?? '', options) + credentialData = await refreshOAuth2Token(nodeData.credential ?? '', credentialData, options) + const accessToken = getCredentialParam('access_token', credentialData, nodeData) + + if (!accessToken) { + throw new Error('No access token found in credential') + } + + // Get all actions based on type + let actions: string[] = [] + + if (sheetsType === 'spreadsheet') { + actions = convertMultiOptionsToStringArray(nodeData.inputs?.spreadsheetActions) + } else if (sheetsType === 'values') { + actions = convertMultiOptionsToStringArray(nodeData.inputs?.valuesActions) + } + + const defaultParams = this.transformNodeInputsToToolArgs(nodeData) + + const tools = createGoogleSheetsTools({ + accessToken, + actions, + defaultParams + }) + + return tools + } + + transformNodeInputsToToolArgs(nodeData: INodeData): Record { + // Collect default parameters from inputs + const defaultParams: Record = {} + + // Common parameters + if (nodeData.inputs?.spreadsheetId) defaultParams.spreadsheetId = nodeData.inputs.spreadsheetId + + // Spreadsheet parameters + if (nodeData.inputs?.title) defaultParams.title = nodeData.inputs.title + if (nodeData.inputs?.sheetCount) defaultParams.sheetCount = nodeData.inputs.sheetCount + if (nodeData.inputs?.includeGridData !== undefined) defaultParams.includeGridData = nodeData.inputs.includeGridData + + // Values parameters + if (nodeData.inputs?.range) defaultParams.range = nodeData.inputs.range + if (nodeData.inputs?.ranges) defaultParams.ranges = nodeData.inputs.ranges + if (nodeData.inputs?.values) defaultParams.values = nodeData.inputs.values + if (nodeData.inputs?.valueInputOption) defaultParams.valueInputOption = nodeData.inputs.valueInputOption + if (nodeData.inputs?.valueRenderOption) defaultParams.valueRenderOption = nodeData.inputs.valueRenderOption + if (nodeData.inputs?.dateTimeRenderOption) defaultParams.dateTimeRenderOption = nodeData.inputs.dateTimeRenderOption + if (nodeData.inputs?.insertDataOption) defaultParams.insertDataOption = nodeData.inputs.insertDataOption + if (nodeData.inputs?.majorDimension) defaultParams.majorDimension = nodeData.inputs.majorDimension + + return defaultParams + } +} + +module.exports = { nodeClass: GoogleSheets_Tools } diff --git a/packages/components/nodes/tools/GoogleSheets/core.ts b/packages/components/nodes/tools/GoogleSheets/core.ts new file mode 100644 index 00000000000..8b6359844b7 --- /dev/null +++ b/packages/components/nodes/tools/GoogleSheets/core.ts @@ -0,0 +1,631 @@ +import { z } from 'zod' +import fetch from 'node-fetch' +import { DynamicStructuredTool } from '../OpenAPIToolkit/core' +import { TOOL_ARGS_PREFIX } from '../../../src/agents' + +export const desc = `Use this when you want to access Google Sheets API for managing spreadsheets and values` + +export interface Headers { + [key: string]: string +} + +export interface Body { + [key: string]: any +} + +export interface RequestParameters { + headers?: Headers + body?: Body + url?: string + description?: string + name?: string + actions?: string[] + accessToken?: string + defaultParams?: any +} + +// Define schemas for different Google Sheets operations + +// Spreadsheet Schemas +const CreateSpreadsheetSchema = z.object({ + title: z.string().describe('The title of the spreadsheet'), + sheetCount: z.number().optional().default(1).describe('Number of sheets to create'), + locale: z.string().optional().describe('The locale of the spreadsheet (e.g., en_US)'), + timeZone: z.string().optional().describe('The time zone of the spreadsheet (e.g., America/New_York)') +}) + +const GetSpreadsheetSchema = z.object({ + spreadsheetId: z.string().describe('The ID of the spreadsheet to retrieve'), + ranges: z.string().optional().describe('Comma-separated list of ranges to retrieve'), + includeGridData: z.boolean().optional().default(false).describe('True if grid data should be returned') +}) + +const UpdateSpreadsheetSchema = z.object({ + spreadsheetId: z.string().describe('The ID of the spreadsheet to update'), + title: z.string().optional().describe('New title for the spreadsheet'), + locale: z.string().optional().describe('New locale for the spreadsheet'), + timeZone: z.string().optional().describe('New time zone for the spreadsheet') +}) + +// Values Schemas +const GetValuesSchema = z.object({ + spreadsheetId: z.string().describe('The ID of the spreadsheet'), + range: z.string().describe('The A1 notation of the range to retrieve values from'), + valueRenderOption: z + .enum(['FORMATTED_VALUE', 'UNFORMATTED_VALUE', 'FORMULA']) + .optional() + .default('FORMATTED_VALUE') + .describe('How values should be represented'), + dateTimeRenderOption: z + .enum(['SERIAL_NUMBER', 'FORMATTED_STRING']) + .optional() + .default('FORMATTED_STRING') + .describe('How dates should be represented'), + majorDimension: z.enum(['ROWS', 'COLUMNS']).optional().default('ROWS').describe('The major dimension that results should use') +}) + +const UpdateValuesSchema = z.object({ + spreadsheetId: z.string().describe('The ID of the spreadsheet'), + range: z.string().describe('The A1 notation of the range to update'), + values: z.string().describe('JSON array of values to write (e.g., [["A1", "B1"], ["A2", "B2"]])'), + valueInputOption: z.enum(['RAW', 'USER_ENTERED']).optional().default('USER_ENTERED').describe('How input data should be interpreted'), + majorDimension: z.enum(['ROWS', 'COLUMNS']).optional().default('ROWS').describe('The major dimension of the values') +}) + +const AppendValuesSchema = z.object({ + spreadsheetId: z.string().describe('The ID of the spreadsheet'), + range: z.string().describe('The A1 notation of the range to append to'), + values: z.string().describe('JSON array of values to append'), + valueInputOption: z.enum(['RAW', 'USER_ENTERED']).optional().default('USER_ENTERED').describe('How input data should be interpreted'), + insertDataOption: z.enum(['OVERWRITE', 'INSERT_ROWS']).optional().default('OVERWRITE').describe('How data should be inserted'), + majorDimension: z.enum(['ROWS', 'COLUMNS']).optional().default('ROWS').describe('The major dimension of the values') +}) + +const ClearValuesSchema = z.object({ + spreadsheetId: z.string().describe('The ID of the spreadsheet'), + range: z.string().describe('The A1 notation of the range to clear') +}) + +const BatchGetValuesSchema = z.object({ + spreadsheetId: z.string().describe('The ID of the spreadsheet'), + ranges: z.string().describe('Comma-separated list of ranges to retrieve'), + valueRenderOption: z + .enum(['FORMATTED_VALUE', 'UNFORMATTED_VALUE', 'FORMULA']) + .optional() + .default('FORMATTED_VALUE') + .describe('How values should be represented'), + dateTimeRenderOption: z + .enum(['SERIAL_NUMBER', 'FORMATTED_STRING']) + .optional() + .default('FORMATTED_STRING') + .describe('How dates should be represented'), + majorDimension: z.enum(['ROWS', 'COLUMNS']).optional().default('ROWS').describe('The major dimension that results should use') +}) + +const BatchUpdateValuesSchema = z.object({ + spreadsheetId: z.string().describe('The ID of the spreadsheet'), + valueInputOption: z.enum(['RAW', 'USER_ENTERED']).optional().default('USER_ENTERED').describe('How input data should be interpreted'), + values: z + .string() + .describe('JSON array of value ranges to update (e.g., [{"range": "A1:B2", "values": [["A1", "B1"], ["A2", "B2"]]}])'), + includeValuesInResponse: z.boolean().optional().default(false).describe('Whether to return the updated values in the response') +}) + +const BatchClearValuesSchema = z.object({ + spreadsheetId: z.string().describe('The ID of the spreadsheet'), + ranges: z.string().describe('Comma-separated list of ranges to clear') +}) + +class BaseGoogleSheetsTool extends DynamicStructuredTool { + protected accessToken: string = '' + + constructor(args: any) { + super(args) + this.accessToken = args.accessToken ?? '' + } + + async makeGoogleSheetsRequest({ + endpoint, + method = 'GET', + body, + params + }: { + endpoint: string + method?: string + body?: any + params?: any + }): Promise { + const url = `https://sheets.googleapis.com/v4/${endpoint}` + + const headers = { + Authorization: `Bearer ${this.accessToken}`, + 'Content-Type': 'application/json', + Accept: 'application/json', + ...this.headers + } + + const response = await fetch(url, { + method, + headers, + body: body ? JSON.stringify(body) : undefined + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`Google Sheets API Error ${response.status}: ${response.statusText} - ${errorText}`) + } + + const data = await response.text() + return data + TOOL_ARGS_PREFIX + JSON.stringify(params) + } +} + +// Spreadsheet Tools +class CreateSpreadsheetTool extends BaseGoogleSheetsTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'create_spreadsheet', + description: 'Create a new Google Spreadsheet', + schema: CreateSpreadsheetSchema, + baseUrl: '', + method: 'POST', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + const body: any = { + properties: { + title: params.title + } + } + + if (params.locale) body.properties.locale = params.locale + if (params.timeZone) body.properties.timeZone = params.timeZone + + // Add sheets if specified + if (params.sheetCount && params.sheetCount > 1) { + body.sheets = [] + for (let i = 0; i < params.sheetCount; i++) { + body.sheets.push({ + properties: { + title: i === 0 ? 'Sheet1' : `Sheet${i + 1}` + } + }) + } + } + + return await this.makeGoogleSheetsRequest({ + endpoint: 'spreadsheets', + method: 'POST', + body, + params + }) + } +} + +class GetSpreadsheetTool extends BaseGoogleSheetsTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'get_spreadsheet', + description: 'Get a Google Spreadsheet by ID', + schema: GetSpreadsheetSchema, + baseUrl: '', + method: 'GET', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const queryParams = new URLSearchParams() + + if (params.ranges) { + params.ranges.split(',').forEach((range: string) => { + queryParams.append('ranges', range.trim()) + }) + } + if (params.includeGridData) queryParams.append('includeGridData', 'true') + + const queryString = queryParams.toString() + const endpoint = `spreadsheets/${params.spreadsheetId}${queryString ? `?${queryString}` : ''}` + + return await this.makeGoogleSheetsRequest({ + endpoint, + method: 'GET', + params + }) + } +} + +class UpdateSpreadsheetTool extends BaseGoogleSheetsTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'update_spreadsheet', + description: 'Update a Google Spreadsheet properties', + schema: UpdateSpreadsheetSchema, + baseUrl: '', + method: 'POST', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + const requests = [] + if (params.title || params.locale || params.timeZone) { + const updateProperties: any = {} + if (params.title) updateProperties.title = params.title + if (params.locale) updateProperties.locale = params.locale + if (params.timeZone) updateProperties.timeZone = params.timeZone + + requests.push({ + updateSpreadsheetProperties: { + properties: updateProperties, + fields: Object.keys(updateProperties).join(',') + } + }) + } + + const body = { requests } + + return await this.makeGoogleSheetsRequest({ + endpoint: `spreadsheets/${params.spreadsheetId}:batchUpdate`, + method: 'POST', + body, + params + }) + } +} + +// Values Tools +class GetValuesTool extends BaseGoogleSheetsTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'get_values', + description: 'Get values from a Google Spreadsheet range', + schema: GetValuesSchema, + baseUrl: '', + method: 'GET', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const queryParams = new URLSearchParams() + + if (params.valueRenderOption) queryParams.append('valueRenderOption', params.valueRenderOption) + if (params.dateTimeRenderOption) queryParams.append('dateTimeRenderOption', params.dateTimeRenderOption) + if (params.majorDimension) queryParams.append('majorDimension', params.majorDimension) + + const queryString = queryParams.toString() + const encodedRange = encodeURIComponent(params.range) + const endpoint = `spreadsheets/${params.spreadsheetId}/values/${encodedRange}${queryString ? `?${queryString}` : ''}` + + return await this.makeGoogleSheetsRequest({ + endpoint, + method: 'GET', + params + }) + } +} + +class UpdateValuesTool extends BaseGoogleSheetsTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'update_values', + description: 'Update values in a Google Spreadsheet range', + schema: UpdateValuesSchema, + baseUrl: '', + method: 'PUT', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + let values + try { + values = JSON.parse(params.values) + } catch (error) { + throw new Error('Values must be a valid JSON array') + } + + const body = { + values, + majorDimension: params.majorDimension || 'ROWS' + } + + const queryParams = new URLSearchParams() + queryParams.append('valueInputOption', params.valueInputOption || 'USER_ENTERED') + + const encodedRange = encodeURIComponent(params.range) + const endpoint = `spreadsheets/${params.spreadsheetId}/values/${encodedRange}?${queryParams.toString()}` + + return await this.makeGoogleSheetsRequest({ + endpoint, + method: 'PUT', + body, + params + }) + } +} + +class AppendValuesTool extends BaseGoogleSheetsTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'append_values', + description: 'Append values to a Google Spreadsheet range', + schema: AppendValuesSchema, + baseUrl: '', + method: 'POST', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + let values + try { + values = JSON.parse(params.values) + } catch (error) { + throw new Error('Values must be a valid JSON array') + } + + const body = { + values, + majorDimension: params.majorDimension || 'ROWS' + } + + const queryParams = new URLSearchParams() + queryParams.append('valueInputOption', params.valueInputOption || 'USER_ENTERED') + queryParams.append('insertDataOption', params.insertDataOption || 'OVERWRITE') + + const encodedRange = encodeURIComponent(params.range) + const endpoint = `spreadsheets/${params.spreadsheetId}/values/${encodedRange}:append?${queryParams.toString()}` + + return await this.makeGoogleSheetsRequest({ + endpoint, + method: 'POST', + body, + params + }) + } +} + +class ClearValuesTool extends BaseGoogleSheetsTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'clear_values', + description: 'Clear values from a Google Spreadsheet range', + schema: ClearValuesSchema, + baseUrl: '', + method: 'POST', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + const encodedRange = encodeURIComponent(params.range) + const endpoint = `spreadsheets/${params.spreadsheetId}/values/${encodedRange}:clear` + + return await this.makeGoogleSheetsRequest({ + endpoint, + method: 'POST', + body: {}, + params + }) + } +} + +class BatchGetValuesTool extends BaseGoogleSheetsTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'batch_get_values', + description: 'Get values from multiple Google Spreadsheet ranges', + schema: BatchGetValuesSchema, + baseUrl: '', + method: 'GET', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const queryParams = new URLSearchParams() + + // Add ranges + params.ranges.split(',').forEach((range: string) => { + queryParams.append('ranges', range.trim()) + }) + + if (params.valueRenderOption) queryParams.append('valueRenderOption', params.valueRenderOption) + if (params.dateTimeRenderOption) queryParams.append('dateTimeRenderOption', params.dateTimeRenderOption) + if (params.majorDimension) queryParams.append('majorDimension', params.majorDimension) + + const endpoint = `spreadsheets/${params.spreadsheetId}/values:batchGet?${queryParams.toString()}` + + return await this.makeGoogleSheetsRequest({ + endpoint, + method: 'GET', + params + }) + } +} + +class BatchUpdateValuesTool extends BaseGoogleSheetsTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'batch_update_values', + description: 'Update values in multiple Google Spreadsheet ranges', + schema: BatchUpdateValuesSchema, + baseUrl: '', + method: 'POST', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + let valueRanges + try { + valueRanges = JSON.parse(params.values) + } catch (error) { + throw new Error('Values must be a valid JSON array of value ranges') + } + + const body = { + valueInputOption: params.valueInputOption || 'USER_ENTERED', + data: valueRanges, + includeValuesInResponse: params.includeValuesInResponse || false + } + + const endpoint = `spreadsheets/${params.spreadsheetId}/values:batchUpdate` + + return await this.makeGoogleSheetsRequest({ + endpoint, + method: 'POST', + body, + params + }) + } +} + +class BatchClearValuesTool extends BaseGoogleSheetsTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'batch_clear_values', + description: 'Clear values from multiple Google Spreadsheet ranges', + schema: BatchClearValuesSchema, + baseUrl: '', + method: 'POST', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + const ranges = params.ranges.split(',').map((range: string) => range.trim()) + const body = { ranges } + + const endpoint = `spreadsheets/${params.spreadsheetId}/values:batchClear` + + return await this.makeGoogleSheetsRequest({ + endpoint, + method: 'POST', + body, + params + }) + } +} + +export const createGoogleSheetsTools = (args?: RequestParameters): DynamicStructuredTool[] => { + const { actions = [], accessToken, defaultParams } = args || {} + const tools: DynamicStructuredTool[] = [] + + // Define all available tools + const toolClasses = { + // Spreadsheet tools + createSpreadsheet: CreateSpreadsheetTool, + getSpreadsheet: GetSpreadsheetTool, + updateSpreadsheet: UpdateSpreadsheetTool, + // Values tools + getValues: GetValuesTool, + updateValues: UpdateValuesTool, + appendValues: AppendValuesTool, + clearValues: ClearValuesTool, + batchGetValues: BatchGetValuesTool, + batchUpdateValues: BatchUpdateValuesTool, + batchClearValues: BatchClearValuesTool + } + + // Create tools based on requested actions + actions.forEach((action) => { + const ToolClass = toolClasses[action as keyof typeof toolClasses] + if (ToolClass) { + tools.push(new ToolClass({ accessToken, defaultParams })) + } + }) + + return tools +} diff --git a/packages/components/nodes/tools/GoogleSheets/google-sheets.svg b/packages/components/nodes/tools/GoogleSheets/google-sheets.svg new file mode 100644 index 00000000000..43af0ccf1fe --- /dev/null +++ b/packages/components/nodes/tools/GoogleSheets/google-sheets.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/tools/Jira/Jira.ts b/packages/components/nodes/tools/Jira/Jira.ts new file mode 100644 index 00000000000..95c2b8c045b --- /dev/null +++ b/packages/components/nodes/tools/Jira/Jira.ts @@ -0,0 +1,449 @@ +import { convertMultiOptionsToStringArray, getCredentialData, getCredentialParam } from '../../../src/utils' +import { createJiraTools } from './core' +import type { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' + +class Jira_Tools implements INode { + label: string + name: string + version: number + type: string + icon: string + category: string + description: string + baseClasses: string[] + credential: INodeParams + inputs: INodeParams[] + + constructor() { + this.label = 'Jira' + this.name = 'jiraTool' + this.version = 1.0 + this.type = 'Jira' + this.icon = 'jira.svg' + this.category = 'Tools' + this.description = 'Perform Jira operations for issues, comments, and users' + this.baseClasses = [this.type, 'Tool'] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['jiraApi'] + } + this.inputs = [ + { + label: 'Host', + name: 'jiraHost', + type: 'string', + placeholder: 'https://example.atlassian.net' + }, + { + label: 'Type', + name: 'jiraType', + type: 'options', + options: [ + { + label: 'Issues', + name: 'issues' + }, + { + label: 'Issue Comments', + name: 'comments' + }, + { + label: 'Users', + name: 'users' + } + ] + }, + // Issue Actions + { + label: 'Issue Actions', + name: 'issueActions', + type: 'multiOptions', + options: [ + { + label: 'List Issues', + name: 'listIssues' + }, + { + label: 'Create Issue', + name: 'createIssue' + }, + { + label: 'Get Issue', + name: 'getIssue' + }, + { + label: 'Update Issue', + name: 'updateIssue' + }, + { + label: 'Delete Issue', + name: 'deleteIssue' + }, + { + label: 'Assign Issue', + name: 'assignIssue' + }, + { + label: 'Transition Issue', + name: 'transitionIssue' + } + ], + show: { + jiraType: ['issues'] + } + }, + // Comment Actions + { + label: 'Comment Actions', + name: 'commentActions', + type: 'multiOptions', + options: [ + { + label: 'List Comments', + name: 'listComments' + }, + { + label: 'Create Comment', + name: 'createComment' + }, + { + label: 'Get Comment', + name: 'getComment' + }, + { + label: 'Update Comment', + name: 'updateComment' + }, + { + label: 'Delete Comment', + name: 'deleteComment' + } + ], + show: { + jiraType: ['comments'] + } + }, + // User Actions + { + label: 'User Actions', + name: 'userActions', + type: 'multiOptions', + options: [ + { + label: 'Search Users', + name: 'searchUsers' + }, + { + label: 'Get User', + name: 'getUser' + }, + { + label: 'Create User', + name: 'createUser' + }, + { + label: 'Update User', + name: 'updateUser' + }, + { + label: 'Delete User', + name: 'deleteUser' + } + ], + show: { + jiraType: ['users'] + } + }, + // ISSUE PARAMETERS + { + label: 'Project Key', + name: 'projectKey', + type: 'string', + placeholder: 'PROJ', + description: 'Project key for the issue', + show: { + issueActions: ['listIssues', 'createIssue'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Issue Type', + name: 'issueType', + type: 'string', + placeholder: 'Bug, Task, Story', + description: 'Type of issue to create', + show: { + issueActions: ['createIssue'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Summary', + name: 'issueSummary', + type: 'string', + description: 'Issue summary/title', + show: { + issueActions: ['createIssue', 'updateIssue'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Description', + name: 'issueDescription', + type: 'string', + description: 'Issue description', + show: { + issueActions: ['createIssue', 'updateIssue'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Priority', + name: 'issuePriority', + type: 'string', + placeholder: 'Highest, High, Medium, Low, Lowest', + description: 'Issue priority', + show: { + issueActions: ['createIssue', 'updateIssue'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Issue Key', + name: 'issueKey', + type: 'string', + placeholder: 'PROJ-123', + description: 'Issue key (e.g., PROJ-123)', + show: { + issueActions: ['getIssue', 'updateIssue', 'deleteIssue', 'assignIssue', 'transitionIssue'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Assignee Account ID', + name: 'assigneeAccountId', + type: 'string', + description: 'Account ID of the user to assign', + show: { + issueActions: ['assignIssue', 'createIssue', 'updateIssue'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Transition ID', + name: 'transitionId', + type: 'string', + description: 'ID of the transition to execute', + show: { + issueActions: ['transitionIssue'] + }, + additionalParams: true, + optional: true + }, + { + label: 'JQL Query', + name: 'jqlQuery', + type: 'string', + placeholder: 'project = PROJ AND status = "To Do"', + description: 'JQL query for filtering issues', + show: { + issueActions: ['listIssues'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Max Results', + name: 'issueMaxResults', + type: 'number', + default: 50, + description: 'Maximum number of issues to return', + show: { + issueActions: ['listIssues'] + }, + additionalParams: true, + optional: true + }, + // COMMENT PARAMETERS + { + label: 'Issue Key (for Comments)', + name: 'commentIssueKey', + type: 'string', + placeholder: 'PROJ-123', + description: 'Issue key for comment operations', + show: { + commentActions: ['listComments', 'createComment'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Comment Text', + name: 'commentText', + type: 'string', + description: 'Comment content', + show: { + commentActions: ['createComment', 'updateComment'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Comment ID', + name: 'commentId', + type: 'string', + description: 'ID of the comment', + show: { + commentActions: ['getComment', 'updateComment', 'deleteComment'] + }, + additionalParams: true, + optional: true + }, + // USER PARAMETERS + { + label: 'Search Query', + name: 'userQuery', + type: 'string', + placeholder: 'john.doe', + description: 'Query string for user search', + show: { + userActions: ['searchUsers'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Account ID', + name: 'userAccountId', + type: 'string', + description: 'User account ID', + show: { + userActions: ['getUser', 'updateUser', 'deleteUser'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Email Address', + name: 'userEmail', + type: 'string', + placeholder: 'user@example.com', + description: 'User email address', + show: { + userActions: ['createUser', 'updateUser'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Display Name', + name: 'userDisplayName', + type: 'string', + description: 'User display name', + show: { + userActions: ['createUser', 'updateUser'] + }, + additionalParams: true, + optional: true + }, + { + label: 'User Max Results', + name: 'userMaxResults', + type: 'number', + default: 50, + description: 'Maximum number of users to return', + show: { + userActions: ['searchUsers'] + }, + additionalParams: true, + optional: true + } + ] + } + + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + let credentialData = await getCredentialData(nodeData.credential ?? '', options) + const username = getCredentialParam('username', credentialData, nodeData) + const accessToken = getCredentialParam('accessToken', credentialData, nodeData) + const jiraHost = nodeData.inputs?.jiraHost as string + + if (!username) { + throw new Error('No username found in credential') + } + + if (!accessToken) { + throw new Error('No access token found in credential') + } + + if (!jiraHost) { + throw new Error('No Jira host provided') + } + + // Get all actions based on type + const jiraType = nodeData.inputs?.jiraType as string + let actions: string[] = [] + + if (jiraType === 'issues') { + actions = convertMultiOptionsToStringArray(nodeData.inputs?.issueActions) + } else if (jiraType === 'comments') { + actions = convertMultiOptionsToStringArray(nodeData.inputs?.commentActions) + } else if (jiraType === 'users') { + actions = convertMultiOptionsToStringArray(nodeData.inputs?.userActions) + } + + const defaultParams = this.transformNodeInputsToToolArgs(nodeData) + + // Create and return tools based on selected actions + const tools = createJiraTools({ + actions, + username, + accessToken, + jiraHost, + defaultParams + }) + + return tools + } + + transformNodeInputsToToolArgs(nodeData: INodeData): Record { + // Collect default parameters from inputs + const defaultParams: Record = {} + + // Issue parameters + if (nodeData.inputs?.projectKey) defaultParams.projectKey = nodeData.inputs.projectKey + if (nodeData.inputs?.issueType) defaultParams.issueType = nodeData.inputs.issueType + if (nodeData.inputs?.issueSummary) defaultParams.issueSummary = nodeData.inputs.issueSummary + if (nodeData.inputs?.issueDescription) defaultParams.issueDescription = nodeData.inputs.issueDescription + if (nodeData.inputs?.issuePriority) defaultParams.issuePriority = nodeData.inputs.issuePriority + if (nodeData.inputs?.issueKey) defaultParams.issueKey = nodeData.inputs.issueKey + if (nodeData.inputs?.assigneeAccountId) defaultParams.assigneeAccountId = nodeData.inputs.assigneeAccountId + if (nodeData.inputs?.transitionId) defaultParams.transitionId = nodeData.inputs.transitionId + if (nodeData.inputs?.jqlQuery) defaultParams.jqlQuery = nodeData.inputs.jqlQuery + if (nodeData.inputs?.issueMaxResults) defaultParams.issueMaxResults = nodeData.inputs.issueMaxResults + + // Comment parameters + if (nodeData.inputs?.commentIssueKey) defaultParams.commentIssueKey = nodeData.inputs.commentIssueKey + if (nodeData.inputs?.commentText) defaultParams.commentText = nodeData.inputs.commentText + if (nodeData.inputs?.commentId) defaultParams.commentId = nodeData.inputs.commentId + + // User parameters + if (nodeData.inputs?.userQuery) defaultParams.userQuery = nodeData.inputs.userQuery + if (nodeData.inputs?.userAccountId) defaultParams.userAccountId = nodeData.inputs.userAccountId + if (nodeData.inputs?.userEmail) defaultParams.userEmail = nodeData.inputs.userEmail + if (nodeData.inputs?.userDisplayName) defaultParams.userDisplayName = nodeData.inputs.userDisplayName + if (nodeData.inputs?.userMaxResults) defaultParams.userMaxResults = nodeData.inputs.userMaxResults + + return defaultParams + } +} + +module.exports = { nodeClass: Jira_Tools } diff --git a/packages/components/nodes/tools/Jira/core.ts b/packages/components/nodes/tools/Jira/core.ts new file mode 100644 index 00000000000..09d73e0ca53 --- /dev/null +++ b/packages/components/nodes/tools/Jira/core.ts @@ -0,0 +1,1172 @@ +import { z } from 'zod' +import fetch from 'node-fetch' +import { DynamicStructuredTool } from '../OpenAPIToolkit/core' +import { TOOL_ARGS_PREFIX } from '../../../src/agents' + +export const desc = `Use this when you want to access Jira API for managing issues, comments, and users` + +export interface Headers { + [key: string]: string +} + +export interface Body { + [key: string]: any +} + +export interface RequestParameters { + headers?: Headers + body?: Body + url?: string + description?: string + maxOutputLength?: number + name?: string + actions?: string[] + username?: string + accessToken?: string + jiraHost?: string + defaultParams?: any +} + +// Define schemas for different Jira operations + +// Issue Schemas +const ListIssuesSchema = z.object({ + projectKey: z.string().optional().describe('Project key to filter issues'), + jql: z.string().optional().describe('JQL query for filtering issues'), + maxResults: z.number().optional().default(50).describe('Maximum number of results to return'), + startAt: z.number().optional().default(0).describe('Index of the first result to return') +}) + +const CreateIssueSchema = z.object({ + projectKey: z.string().describe('Project key where the issue will be created'), + issueType: z.string().describe('Type of issue (Bug, Task, Story, etc.)'), + summary: z.string().describe('Issue summary/title'), + description: z.string().optional().describe('Issue description'), + priority: z.string().optional().describe('Issue priority (Highest, High, Medium, Low, Lowest)'), + assigneeAccountId: z.string().optional().describe('Account ID of the assignee'), + labels: z.array(z.string()).optional().describe('Labels to add to the issue') +}) + +const GetIssueSchema = z.object({ + issueKey: z.string().describe('Issue key (e.g., PROJ-123)') +}) + +const UpdateIssueSchema = z.object({ + issueKey: z.string().describe('Issue key (e.g., PROJ-123)'), + summary: z.string().optional().describe('Updated issue summary/title'), + description: z.string().optional().describe('Updated issue description'), + priority: z.string().optional().describe('Updated issue priority'), + assigneeAccountId: z.string().optional().describe('Account ID of the new assignee') +}) + +const AssignIssueSchema = z.object({ + issueKey: z.string().describe('Issue key (e.g., PROJ-123)'), + assigneeAccountId: z.string().describe('Account ID of the user to assign') +}) + +const TransitionIssueSchema = z.object({ + issueKey: z.string().describe('Issue key (e.g., PROJ-123)'), + transitionId: z.string().describe('ID of the transition to execute') +}) + +// Comment Schemas +const ListCommentsSchema = z.object({ + issueKey: z.string().describe('Issue key to get comments for'), + maxResults: z.number().optional().default(50).describe('Maximum number of results to return'), + startAt: z.number().optional().default(0).describe('Index of the first result to return') +}) + +const CreateCommentSchema = z.object({ + issueKey: z.string().describe('Issue key to add comment to'), + text: z.string().describe('Comment text content'), + visibility: z + .object({ + type: z.string().optional(), + value: z.string().optional() + }) + .optional() + .describe('Comment visibility settings') +}) + +const GetCommentSchema = z.object({ + issueKey: z.string().describe('Issue key'), + commentId: z.string().describe('Comment ID') +}) + +const UpdateCommentSchema = z.object({ + issueKey: z.string().describe('Issue key'), + commentId: z.string().describe('Comment ID'), + text: z.string().describe('Updated comment text') +}) + +const DeleteCommentSchema = z.object({ + issueKey: z.string().describe('Issue key'), + commentId: z.string().describe('Comment ID to delete') +}) + +// User Schemas +const SearchUsersSchema = z.object({ + query: z.string().describe('Query string for user search'), + maxResults: z.number().optional().default(50).describe('Maximum number of results to return'), + startAt: z.number().optional().default(0).describe('Index of the first result to return') +}) + +const GetUserSchema = z.object({ + accountId: z.string().describe('Account ID of the user') +}) + +const CreateUserSchema = z.object({ + emailAddress: z.string().describe('Email address of the user'), + displayName: z.string().describe('Display name of the user'), + username: z.string().optional().describe('Username (deprecated in newer versions)') +}) + +const UpdateUserSchema = z.object({ + accountId: z.string().describe('Account ID of the user'), + emailAddress: z.string().optional().describe('Updated email address'), + displayName: z.string().optional().describe('Updated display name') +}) + +const DeleteUserSchema = z.object({ + accountId: z.string().describe('Account ID of the user to delete') +}) + +class BaseJiraTool extends DynamicStructuredTool { + protected username: string = '' + protected accessToken: string = '' + protected jiraHost: string = '' + + constructor(args: any) { + super(args) + this.username = args.username ?? '' + this.accessToken = args.accessToken ?? '' + this.jiraHost = args.jiraHost ?? '' + } + + async makeJiraRequest({ + endpoint, + method = 'GET', + body, + params + }: { + endpoint: string + method?: string + body?: any + params?: any + }): Promise { + const url = `${this.jiraHost}/rest/api/3/${endpoint}` + const auth = Buffer.from(`${this.username}:${this.accessToken}`).toString('base64') + + const headers = { + Authorization: `Basic ${auth}`, + 'Content-Type': 'application/json', + Accept: 'application/json', + ...this.headers + } + + const response = await fetch(url, { + method, + headers, + body: body ? JSON.stringify(body) : undefined + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`Jira API Error ${response.status}: ${response.statusText} - ${errorText}`) + } + + const data = await response.text() + return data + TOOL_ARGS_PREFIX + JSON.stringify(params) + } +} + +// Issue Tools +class ListIssuesTool extends BaseJiraTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'list_issues', + description: 'List issues from Jira using JQL query', + schema: ListIssuesSchema, + baseUrl: '', + method: 'GET', + headers: {} + } + super({ + ...toolInput, + username: args.username, + accessToken: args.accessToken, + jiraHost: args.jiraHost, + maxOutputLength: args.maxOutputLength + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const queryParams = new URLSearchParams() + + let jql = params.jql || '' + if (params.projectKey && !jql.includes('project')) { + jql = jql ? `project = ${params.projectKey} AND (${jql})` : `project = ${params.projectKey}` + } + + if (jql) queryParams.append('jql', jql) + if (params.maxResults) queryParams.append('maxResults', params.maxResults.toString()) + if (params.startAt) queryParams.append('startAt', params.startAt.toString()) + + const endpoint = `search?${queryParams.toString()}` + + try { + const response = await this.makeJiraRequest({ endpoint, params }) + return response + } catch (error) { + return `Error listing issues: ${error}` + } + } +} + +class CreateIssueTool extends BaseJiraTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'create_issue', + description: 'Create a new issue in Jira', + schema: CreateIssueSchema, + baseUrl: '', + method: 'POST', + headers: {} + } + super({ + ...toolInput, + username: args.username, + accessToken: args.accessToken, + jiraHost: args.jiraHost, + maxOutputLength: args.maxOutputLength + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const issueData: any = { + fields: { + project: { + key: params.projectKey + }, + issuetype: { + name: params.issueType + }, + summary: params.summary + } + } + + if (params.description) { + issueData.fields.description = { + type: 'doc', + version: 1, + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: params.description + } + ] + } + ] + } + } + + if (params.priority) { + issueData.fields.priority = { + name: params.priority + } + } + + if (params.assigneeAccountId) { + issueData.fields.assignee = { + accountId: params.assigneeAccountId + } + } + + if (params.labels) { + issueData.fields.labels = params.labels + } + + const response = await this.makeJiraRequest({ endpoint: 'issue', method: 'POST', body: issueData, params }) + return response + } catch (error) { + return `Error creating issue: ${error}` + } + } +} + +class GetIssueTool extends BaseJiraTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'get_issue', + description: 'Get a specific issue from Jira', + schema: GetIssueSchema, + baseUrl: '', + method: 'GET', + headers: {} + } + super({ + ...toolInput, + username: args.username, + accessToken: args.accessToken, + jiraHost: args.jiraHost, + maxOutputLength: args.maxOutputLength + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const endpoint = `issue/${params.issueKey}` + const response = await this.makeJiraRequest({ endpoint, params }) + return response + } catch (error) { + return `Error getting issue: ${error}` + } + } +} + +class UpdateIssueTool extends BaseJiraTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'update_issue', + description: 'Update an existing issue in Jira', + schema: UpdateIssueSchema, + baseUrl: '', + method: 'PUT', + headers: {} + } + super({ + ...toolInput, + username: args.username, + accessToken: args.accessToken, + jiraHost: args.jiraHost, + maxOutputLength: args.maxOutputLength + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const updateData: any = { + fields: {} + } + + if (params.summary) updateData.fields.summary = params.summary + if (params.description) { + updateData.fields.description = { + type: 'doc', + version: 1, + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: params.description + } + ] + } + ] + } + } + if (params.priority) { + updateData.fields.priority = { + name: params.priority + } + } + if (params.assigneeAccountId) { + updateData.fields.assignee = { + accountId: params.assigneeAccountId + } + } + + const endpoint = `issue/${params.issueKey}` + const response = await this.makeJiraRequest({ endpoint, method: 'PUT', body: updateData, params }) + return response || 'Issue updated successfully' + } catch (error) { + return `Error updating issue: ${error}` + } + } +} + +class DeleteIssueTool extends BaseJiraTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'delete_issue', + description: 'Delete an issue from Jira', + schema: GetIssueSchema, + baseUrl: '', + method: 'DELETE', + headers: {} + } + super({ + ...toolInput, + username: args.username, + accessToken: args.accessToken, + jiraHost: args.jiraHost, + maxOutputLength: args.maxOutputLength + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const endpoint = `issue/${params.issueKey}` + const response = await this.makeJiraRequest({ endpoint, method: 'DELETE', params }) + return response || 'Issue deleted successfully' + } catch (error) { + return `Error deleting issue: ${error}` + } + } +} + +class AssignIssueTool extends BaseJiraTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'assign_issue', + description: 'Assign an issue to a user in Jira', + schema: AssignIssueSchema, + baseUrl: '', + method: 'PUT', + headers: {} + } + super({ + ...toolInput, + username: args.username, + accessToken: args.accessToken, + jiraHost: args.jiraHost, + maxOutputLength: args.maxOutputLength + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const assignData = { + accountId: params.assigneeAccountId + } + + const endpoint = `issue/${params.issueKey}/assignee` + const response = await this.makeJiraRequest({ endpoint, method: 'PUT', body: assignData, params }) + return response || 'Issue assigned successfully' + } catch (error) { + return `Error assigning issue: ${error}` + } + } +} + +class TransitionIssueTool extends BaseJiraTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'transition_issue', + description: 'Transition an issue to a different status in Jira', + schema: TransitionIssueSchema, + baseUrl: '', + method: 'POST', + headers: {} + } + super({ + ...toolInput, + username: args.username, + accessToken: args.accessToken, + jiraHost: args.jiraHost, + maxOutputLength: args.maxOutputLength + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const transitionData = { + transition: { + id: params.transitionId + } + } + + const endpoint = `issue/${params.issueKey}/transitions` + const response = await this.makeJiraRequest({ endpoint, method: 'POST', body: transitionData, params }) + return response || 'Issue transitioned successfully' + } catch (error) { + return `Error transitioning issue: ${error}` + } + } +} + +// Comment Tools +class ListCommentsTool extends BaseJiraTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'list_comments', + description: 'List comments for a Jira issue', + schema: ListCommentsSchema, + baseUrl: '', + method: 'GET', + headers: {} + } + super({ + ...toolInput, + username: args.username, + accessToken: args.accessToken, + jiraHost: args.jiraHost, + maxOutputLength: args.maxOutputLength + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const queryParams = new URLSearchParams() + + if (params.maxResults) queryParams.append('maxResults', params.maxResults.toString()) + if (params.startAt) queryParams.append('startAt', params.startAt.toString()) + + const endpoint = `issue/${params.issueKey}/comment?${queryParams.toString()}` + + try { + const response = await this.makeJiraRequest({ endpoint, params }) + return response + } catch (error) { + return `Error listing comments: ${error}` + } + } +} + +class CreateCommentTool extends BaseJiraTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'create_comment', + description: 'Create a comment on a Jira issue', + schema: CreateCommentSchema, + baseUrl: '', + method: 'POST', + headers: {} + } + super({ + ...toolInput, + username: args.username, + accessToken: args.accessToken, + jiraHost: args.jiraHost, + maxOutputLength: args.maxOutputLength + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const commentData: any = { + body: { + type: 'doc', + version: 1, + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: params.text + } + ] + } + ] + } + } + + if (params.visibility) { + commentData.visibility = params.visibility + } + + const endpoint = `issue/${params.issueKey}/comment` + const response = await this.makeJiraRequest({ endpoint, method: 'POST', body: commentData, params }) + return response + } catch (error) { + return `Error creating comment: ${error}` + } + } +} + +class GetCommentTool extends BaseJiraTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'get_comment', + description: 'Get a specific comment from a Jira issue', + schema: GetCommentSchema, + baseUrl: '', + method: 'GET', + headers: {} + } + super({ + ...toolInput, + username: args.username, + accessToken: args.accessToken, + jiraHost: args.jiraHost, + maxOutputLength: args.maxOutputLength + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const endpoint = `issue/${params.issueKey}/comment/${params.commentId}` + const response = await this.makeJiraRequest({ endpoint, params }) + return response + } catch (error) { + return `Error getting comment: ${error}` + } + } +} + +class UpdateCommentTool extends BaseJiraTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'update_comment', + description: 'Update a comment on a Jira issue', + schema: UpdateCommentSchema, + baseUrl: '', + method: 'PUT', + headers: {} + } + super({ + ...toolInput, + username: args.username, + accessToken: args.accessToken, + jiraHost: args.jiraHost, + maxOutputLength: args.maxOutputLength + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const commentData = { + body: { + type: 'doc', + version: 1, + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: params.text + } + ] + } + ] + } + } + + const endpoint = `issue/${params.issueKey}/comment/${params.commentId}` + const response = await this.makeJiraRequest({ endpoint, method: 'PUT', body: commentData, params }) + return response || 'Comment updated successfully' + } catch (error) { + return `Error updating comment: ${error}` + } + } +} + +class DeleteCommentTool extends BaseJiraTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'delete_comment', + description: 'Delete a comment from a Jira issue', + schema: DeleteCommentSchema, + baseUrl: '', + method: 'DELETE', + headers: {} + } + super({ + ...toolInput, + username: args.username, + accessToken: args.accessToken, + jiraHost: args.jiraHost, + maxOutputLength: args.maxOutputLength + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const endpoint = `issue/${params.issueKey}/comment/${params.commentId}` + const response = await this.makeJiraRequest({ endpoint, method: 'DELETE', params }) + return response || 'Comment deleted successfully' + } catch (error) { + return `Error deleting comment: ${error}` + } + } +} + +// User Tools +class SearchUsersTool extends BaseJiraTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'search_users', + description: 'Search for users in Jira', + schema: SearchUsersSchema, + baseUrl: '', + method: 'GET', + headers: {} + } + super({ + ...toolInput, + username: args.username, + accessToken: args.accessToken, + jiraHost: args.jiraHost, + maxOutputLength: args.maxOutputLength + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const queryParams = new URLSearchParams() + + if (params.query) queryParams.append('query', params.query) + if (params.maxResults) queryParams.append('maxResults', params.maxResults.toString()) + if (params.startAt) queryParams.append('startAt', params.startAt.toString()) + + const endpoint = `user/search?${queryParams.toString()}` + + try { + const response = await this.makeJiraRequest({ endpoint, params }) + return response + } catch (error) { + return `Error searching users: ${error}` + } + } +} + +class GetUserTool extends BaseJiraTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'get_user', + description: 'Get a specific user from Jira', + schema: GetUserSchema, + baseUrl: '', + method: 'GET', + headers: {} + } + super({ + ...toolInput, + username: args.username, + accessToken: args.accessToken, + jiraHost: args.jiraHost, + maxOutputLength: args.maxOutputLength + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const queryParams = new URLSearchParams() + + queryParams.append('accountId', params.accountId) + + const endpoint = `user?${queryParams.toString()}` + + try { + const response = await this.makeJiraRequest({ endpoint, params }) + return response + } catch (error) { + return `Error getting user: ${error}` + } + } +} + +class CreateUserTool extends BaseJiraTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'create_user', + description: 'Create a new user in Jira', + schema: CreateUserSchema, + baseUrl: '', + method: 'POST', + headers: {} + } + super({ + ...toolInput, + username: args.username, + accessToken: args.accessToken, + jiraHost: args.jiraHost, + maxOutputLength: args.maxOutputLength + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const userData: any = { + emailAddress: params.emailAddress, + displayName: params.displayName + } + + if (params.username) { + userData.username = params.username + } + + const endpoint = 'user' + const response = await this.makeJiraRequest({ endpoint, method: 'POST', body: userData, params }) + return response + } catch (error) { + return `Error creating user: ${error}` + } + } +} + +class UpdateUserTool extends BaseJiraTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'update_user', + description: 'Update an existing user in Jira', + schema: UpdateUserSchema, + baseUrl: '', + method: 'PUT', + headers: {} + } + super({ + ...toolInput, + username: args.username, + accessToken: args.accessToken, + jiraHost: args.jiraHost, + maxOutputLength: args.maxOutputLength + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const userData: any = {} + + if (params.emailAddress) userData.emailAddress = params.emailAddress + if (params.displayName) userData.displayName = params.displayName + + const queryParams = new URLSearchParams() + queryParams.append('accountId', params.accountId) + + const endpoint = `user?${queryParams.toString()}` + const response = await this.makeJiraRequest({ endpoint, method: 'PUT', body: userData, params }) + return response || 'User updated successfully' + } catch (error) { + return `Error updating user: ${error}` + } + } +} + +class DeleteUserTool extends BaseJiraTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'delete_user', + description: 'Delete a user from Jira', + schema: DeleteUserSchema, + baseUrl: '', + method: 'DELETE', + headers: {} + } + super({ + ...toolInput, + username: args.username, + accessToken: args.accessToken, + jiraHost: args.jiraHost, + maxOutputLength: args.maxOutputLength + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const queryParams = new URLSearchParams() + queryParams.append('accountId', params.accountId) + + const endpoint = `user?${queryParams.toString()}` + const response = await this.makeJiraRequest({ endpoint, method: 'DELETE', params }) + return response || 'User deleted successfully' + } catch (error) { + return `Error deleting user: ${error}` + } + } +} + +export const createJiraTools = (args?: RequestParameters): DynamicStructuredTool[] => { + const tools: DynamicStructuredTool[] = [] + const actions = args?.actions || [] + const username = args?.username || '' + const accessToken = args?.accessToken || '' + const jiraHost = args?.jiraHost || '' + const maxOutputLength = args?.maxOutputLength || Infinity + const defaultParams = args?.defaultParams || {} + + // Issue tools + if (actions.includes('listIssues')) { + tools.push( + new ListIssuesTool({ + username, + accessToken, + jiraHost, + maxOutputLength, + defaultParams: defaultParams.listIssues + }) + ) + } + + if (actions.includes('createIssue')) { + tools.push( + new CreateIssueTool({ + username, + accessToken, + jiraHost, + maxOutputLength, + defaultParams: defaultParams.createIssue + }) + ) + } + + if (actions.includes('getIssue')) { + tools.push( + new GetIssueTool({ + username, + accessToken, + jiraHost, + maxOutputLength, + defaultParams: defaultParams.getIssue + }) + ) + } + + if (actions.includes('updateIssue')) { + tools.push( + new UpdateIssueTool({ + username, + accessToken, + jiraHost, + maxOutputLength, + defaultParams: defaultParams.updateIssue + }) + ) + } + + if (actions.includes('deleteIssue')) { + tools.push( + new DeleteIssueTool({ + username, + accessToken, + jiraHost, + maxOutputLength, + defaultParams: defaultParams.deleteIssue + }) + ) + } + + if (actions.includes('assignIssue')) { + tools.push( + new AssignIssueTool({ + username, + accessToken, + jiraHost, + maxOutputLength, + defaultParams: defaultParams.assignIssue + }) + ) + } + + if (actions.includes('transitionIssue')) { + tools.push( + new TransitionIssueTool({ + username, + accessToken, + jiraHost, + maxOutputLength, + defaultParams: defaultParams.transitionIssue + }) + ) + } + + // Comment tools + if (actions.includes('listComments')) { + tools.push( + new ListCommentsTool({ + username, + accessToken, + jiraHost, + maxOutputLength, + defaultParams: defaultParams.listComments + }) + ) + } + + if (actions.includes('createComment')) { + tools.push( + new CreateCommentTool({ + username, + accessToken, + jiraHost, + maxOutputLength, + defaultParams: defaultParams.createComment + }) + ) + } + + if (actions.includes('getComment')) { + tools.push( + new GetCommentTool({ + username, + accessToken, + jiraHost, + maxOutputLength, + defaultParams: defaultParams.getComment + }) + ) + } + + if (actions.includes('updateComment')) { + tools.push( + new UpdateCommentTool({ + username, + accessToken, + jiraHost, + maxOutputLength, + defaultParams: defaultParams.updateComment + }) + ) + } + + if (actions.includes('deleteComment')) { + tools.push( + new DeleteCommentTool({ + username, + accessToken, + jiraHost, + maxOutputLength, + defaultParams: defaultParams.deleteComment + }) + ) + } + + // User tools + if (actions.includes('searchUsers')) { + tools.push( + new SearchUsersTool({ + username, + accessToken, + jiraHost, + maxOutputLength, + defaultParams: defaultParams.searchUsers + }) + ) + } + + if (actions.includes('getUser')) { + tools.push( + new GetUserTool({ + username, + accessToken, + jiraHost, + maxOutputLength, + defaultParams: defaultParams.getUser + }) + ) + } + + if (actions.includes('createUser')) { + tools.push( + new CreateUserTool({ + username, + accessToken, + jiraHost, + maxOutputLength, + defaultParams: defaultParams.createUser + }) + ) + } + + if (actions.includes('updateUser')) { + tools.push( + new UpdateUserTool({ + username, + accessToken, + jiraHost, + maxOutputLength, + defaultParams: defaultParams.updateUser + }) + ) + } + + if (actions.includes('deleteUser')) { + tools.push( + new DeleteUserTool({ + username, + accessToken, + jiraHost, + maxOutputLength, + defaultParams: defaultParams.deleteUser + }) + ) + } + + return tools +} diff --git a/packages/components/nodes/tools/Jira/jira.svg b/packages/components/nodes/tools/Jira/jira.svg new file mode 100644 index 00000000000..4ace5cc84a3 --- /dev/null +++ b/packages/components/nodes/tools/Jira/jira.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/tools/MCP/CustomMCP/CustomMCP.ts b/packages/components/nodes/tools/MCP/CustomMCP/CustomMCP.ts index c782a357cf8..d81c5d778f9 100644 --- a/packages/components/nodes/tools/MCP/CustomMCP/CustomMCP.ts +++ b/packages/components/nodes/tools/MCP/CustomMCP/CustomMCP.ts @@ -27,6 +27,16 @@ For example, you have a variable called "var1": } } \`\`\` + +For example, when using SSE, you can use the variable "var1" in the headers: +\`\`\`json +{ + "url": "https://api.example.com/endpoint/sse", + "headers": { + "Authorization": "Bearer {{$vars.var1}}" + } +} +\`\`\` ` class Custom_MCP implements INode { diff --git a/packages/components/nodes/tools/MCP/core.ts b/packages/components/nodes/tools/MCP/core.ts index 7c894fcc479..9ac0ab1ae19 100644 --- a/packages/components/nodes/tools/MCP/core.ts +++ b/packages/components/nodes/tools/MCP/core.ts @@ -53,10 +53,29 @@ export class MCPToolkit extends BaseToolkit { const baseUrl = new URL(this.serverParams.url) try { - transport = new StreamableHTTPClientTransport(baseUrl) + if (this.serverParams.headers) { + transport = new StreamableHTTPClientTransport(baseUrl, { + requestInit: { + headers: this.serverParams.headers + } + }) + } else { + transport = new StreamableHTTPClientTransport(baseUrl) + } await client.connect(transport) } catch (error) { - transport = new SSEClientTransport(baseUrl) + if (this.serverParams.headers) { + transport = new SSEClientTransport(baseUrl, { + requestInit: { + headers: this.serverParams.headers + }, + eventSourceInit: { + fetch: (url, init) => fetch(url, { ...init, headers: this.serverParams.headers }) + } + }) + } else { + transport = new SSEClientTransport(baseUrl) + } await client.connect(transport) } } diff --git a/packages/components/nodes/tools/MicrosoftOutlook/MicrosoftOutlook.ts b/packages/components/nodes/tools/MicrosoftOutlook/MicrosoftOutlook.ts new file mode 100644 index 00000000000..a85af7221ea --- /dev/null +++ b/packages/components/nodes/tools/MicrosoftOutlook/MicrosoftOutlook.ts @@ -0,0 +1,822 @@ +import { convertMultiOptionsToStringArray, getCredentialData, getCredentialParam, refreshOAuth2Token } from '../../../src/utils' +import { createOutlookTools } from './core' +import type { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' + +class MicrosoftOutlook_Tools implements INode { + label: string + name: string + version: number + type: string + icon: string + category: string + description: string + baseClasses: string[] + credential: INodeParams + inputs: INodeParams[] + + constructor() { + this.label = 'Microsoft Outlook' + this.name = 'microsoftOutlook' + this.version = 1.0 + this.type = 'MicrosoftOutlook' + this.icon = 'outlook.svg' + this.category = 'Tools' + this.description = 'Perform Microsoft Outlook operations for calendars, events, and messages' + this.baseClasses = [this.type, 'Tool'] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['microsoftOutlookOAuth2'] + } + this.inputs = [ + { + label: 'Type', + name: 'outlookType', + type: 'options', + options: [ + { + label: 'Calendar', + name: 'calendar' + }, + { + label: 'Message', + name: 'message' + } + ] + }, + // Calendar Actions + { + label: 'Calendar Actions', + name: 'calendarActions', + type: 'multiOptions', + options: [ + { + label: 'List Calendars', + name: 'listCalendars' + }, + { + label: 'Get Calendar', + name: 'getCalendar' + }, + { + label: 'Create Calendar', + name: 'createCalendar' + }, + { + label: 'Update Calendar', + name: 'updateCalendar' + }, + { + label: 'Delete Calendar', + name: 'deleteCalendar' + }, + { + label: 'List Events', + name: 'listEvents' + }, + { + label: 'Get Event', + name: 'getEvent' + }, + { + label: 'Create Event', + name: 'createEvent' + }, + { + label: 'Update Event', + name: 'updateEvent' + }, + { + label: 'Delete Event', + name: 'deleteEvent' + } + ], + show: { + outlookType: ['calendar'] + } + }, + // Message Actions + { + label: 'Message Actions', + name: 'messageActions', + type: 'multiOptions', + options: [ + { + label: 'List Messages', + name: 'listMessages' + }, + { + label: 'Get Message', + name: 'getMessage' + }, + { + label: 'Create Draft Message', + name: 'createDraftMessage' + }, + { + label: 'Send Message', + name: 'sendMessage' + }, + { + label: 'Update Message', + name: 'updateMessage' + }, + { + label: 'Delete Message', + name: 'deleteMessage' + }, + { + label: 'Copy Message', + name: 'copyMessage' + }, + { + label: 'Move Message', + name: 'moveMessage' + }, + { + label: 'Reply to Message', + name: 'replyMessage' + }, + { + label: 'Forward Message', + name: 'forwardMessage' + } + ], + show: { + outlookType: ['message'] + } + }, + // CALENDAR PARAMETERS + // List Calendars Parameters + { + label: 'Max Results [List Calendars]', + name: 'maxResultsListCalendars', + type: 'number', + description: 'Maximum number of calendars to return', + default: 50, + show: { + outlookType: ['calendar'], + calendarActions: ['listCalendars'] + }, + additionalParams: true, + optional: true + }, + // Get Calendar Parameters + { + label: 'Calendar ID [Get Calendar]', + name: 'calendarIdGetCalendar', + type: 'string', + description: 'ID of the calendar to retrieve', + show: { + outlookType: ['calendar'], + calendarActions: ['getCalendar'] + }, + additionalParams: true, + optional: true + }, + // Create Calendar Parameters + { + label: 'Calendar Name [Create Calendar]', + name: 'calendarNameCreateCalendar', + type: 'string', + description: 'Name of the calendar', + placeholder: 'My New Calendar', + show: { + outlookType: ['calendar'], + calendarActions: ['createCalendar'] + }, + additionalParams: true, + optional: true + }, + // Update Calendar Parameters + { + label: 'Calendar ID [Update Calendar]', + name: 'calendarIdUpdateCalendar', + type: 'string', + description: 'ID of the calendar to update', + show: { + outlookType: ['calendar'], + calendarActions: ['updateCalendar'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Calendar Name [Update Calendar]', + name: 'calendarNameUpdateCalendar', + type: 'string', + description: 'New name of the calendar', + show: { + outlookType: ['calendar'], + calendarActions: ['updateCalendar'] + }, + additionalParams: true, + optional: true + }, + // Delete Calendar Parameters + { + label: 'Calendar ID [Delete Calendar]', + name: 'calendarIdDeleteCalendar', + type: 'string', + description: 'ID of the calendar to delete', + show: { + outlookType: ['calendar'], + calendarActions: ['deleteCalendar'] + }, + additionalParams: true, + optional: true + }, + // List Events Parameters + { + label: 'Calendar ID [List Events]', + name: 'calendarIdListEvents', + type: 'string', + description: 'ID of the calendar (leave empty for primary calendar)', + show: { + outlookType: ['calendar'], + calendarActions: ['listEvents'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Max Results [List Events]', + name: 'maxResultsListEvents', + type: 'number', + description: 'Maximum number of events to return', + default: 50, + show: { + outlookType: ['calendar'], + calendarActions: ['listEvents'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Start Date Time [List Events]', + name: 'startDateTimeListEvents', + type: 'string', + description: 'Start date time filter in ISO format', + placeholder: '2024-01-01T00:00:00Z', + show: { + outlookType: ['calendar'], + calendarActions: ['listEvents'] + }, + additionalParams: true, + optional: true + }, + { + label: 'End Date Time [List Events]', + name: 'endDateTimeListEvents', + type: 'string', + description: 'End date time filter in ISO format', + placeholder: '2024-12-31T23:59:59Z', + show: { + outlookType: ['calendar'], + calendarActions: ['listEvents'] + }, + additionalParams: true, + optional: true + }, + // Get Event Parameters + { + label: 'Event ID [Get Event]', + name: 'eventIdGetEvent', + type: 'string', + description: 'ID of the event to retrieve', + show: { + outlookType: ['calendar'], + calendarActions: ['getEvent'] + }, + additionalParams: true, + optional: true + }, + // Create Event Parameters + { + label: 'Subject [Create Event]', + name: 'subjectCreateEvent', + type: 'string', + description: 'Subject/title of the event', + placeholder: 'Meeting Title', + show: { + outlookType: ['calendar'], + calendarActions: ['createEvent'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Body [Create Event]', + name: 'bodyCreateEvent', + type: 'string', + description: 'Body/description of the event', + placeholder: 'Meeting description', + rows: 3, + show: { + outlookType: ['calendar'], + calendarActions: ['createEvent'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Start Date Time [Create Event]', + name: 'startDateTimeCreateEvent', + type: 'string', + description: 'Start date and time in ISO format', + placeholder: '2024-01-15T10:00:00', + show: { + outlookType: ['calendar'], + calendarActions: ['createEvent'] + }, + additionalParams: true, + optional: true + }, + { + label: 'End Date Time [Create Event]', + name: 'endDateTimeCreateEvent', + type: 'string', + description: 'End date and time in ISO format', + placeholder: '2024-01-15T11:00:00', + show: { + outlookType: ['calendar'], + calendarActions: ['createEvent'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Time Zone [Create Event]', + name: 'timeZoneCreateEvent', + type: 'string', + description: 'Time zone for the event', + placeholder: 'UTC', + default: 'UTC', + show: { + outlookType: ['calendar'], + calendarActions: ['createEvent'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Location [Create Event]', + name: 'locationCreateEvent', + type: 'string', + description: 'Location of the event', + placeholder: 'Conference Room A', + show: { + outlookType: ['calendar'], + calendarActions: ['createEvent'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Attendees [Create Event]', + name: 'attendeesCreateEvent', + type: 'string', + description: 'Comma-separated list of attendee email addresses', + placeholder: 'user1@example.com,user2@example.com', + show: { + outlookType: ['calendar'], + calendarActions: ['createEvent'] + }, + additionalParams: true, + optional: true + }, + // Update Event Parameters + { + label: 'Event ID [Update Event]', + name: 'eventIdUpdateEvent', + type: 'string', + description: 'ID of the event to update', + show: { + outlookType: ['calendar'], + calendarActions: ['updateEvent'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Subject [Update Event]', + name: 'subjectUpdateEvent', + type: 'string', + description: 'New subject/title of the event', + show: { + outlookType: ['calendar'], + calendarActions: ['updateEvent'] + }, + additionalParams: true, + optional: true + }, + // Delete Event Parameters + { + label: 'Event ID [Delete Event]', + name: 'eventIdDeleteEvent', + type: 'string', + description: 'ID of the event to delete', + show: { + outlookType: ['calendar'], + calendarActions: ['deleteEvent'] + }, + additionalParams: true, + optional: true + }, + // MESSAGE PARAMETERS + // List Messages Parameters + { + label: 'Max Results [List Messages]', + name: 'maxResultsListMessages', + type: 'number', + description: 'Maximum number of messages to return', + default: 50, + show: { + outlookType: ['message'], + messageActions: ['listMessages'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Filter [List Messages]', + name: 'filterListMessages', + type: 'string', + description: 'Filter query (e.g., "isRead eq false")', + show: { + outlookType: ['message'], + messageActions: ['listMessages'] + }, + additionalParams: true, + optional: true + }, + // Get Message Parameters + { + label: 'Message ID [Get Message]', + name: 'messageIdGetMessage', + type: 'string', + description: 'ID of the message to retrieve', + show: { + outlookType: ['message'], + messageActions: ['getMessage'] + }, + additionalParams: true, + optional: true + }, + // Create Draft Message Parameters + { + label: 'To [Create Draft Message]', + name: 'toCreateDraftMessage', + type: 'string', + description: 'Recipient email address(es), comma-separated', + placeholder: 'user@example.com', + show: { + outlookType: ['message'], + messageActions: ['createDraftMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Subject [Create Draft Message]', + name: 'subjectCreateDraftMessage', + type: 'string', + description: 'Subject of the message', + placeholder: 'Email Subject', + show: { + outlookType: ['message'], + messageActions: ['createDraftMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Body [Create Draft Message]', + name: 'bodyCreateDraftMessage', + type: 'string', + description: 'Body content of the message', + placeholder: 'Email body content', + rows: 4, + show: { + outlookType: ['message'], + messageActions: ['createDraftMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'CC [Create Draft Message]', + name: 'ccCreateDraftMessage', + type: 'string', + description: 'CC email address(es), comma-separated', + placeholder: 'cc@example.com', + show: { + outlookType: ['message'], + messageActions: ['createDraftMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'BCC [Create Draft Message]', + name: 'bccCreateDraftMessage', + type: 'string', + description: 'BCC email address(es), comma-separated', + placeholder: 'bcc@example.com', + show: { + outlookType: ['message'], + messageActions: ['createDraftMessage'] + }, + additionalParams: true, + optional: true + }, + // Send Message Parameters + { + label: 'To [Send Message]', + name: 'toSendMessage', + type: 'string', + description: 'Recipient email address(es), comma-separated', + placeholder: 'user@example.com', + show: { + outlookType: ['message'], + messageActions: ['sendMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Subject [Send Message]', + name: 'subjectSendMessage', + type: 'string', + description: 'Subject of the message', + placeholder: 'Email Subject', + show: { + outlookType: ['message'], + messageActions: ['sendMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Body [Send Message]', + name: 'bodySendMessage', + type: 'string', + description: 'Body content of the message', + placeholder: 'Email body content', + rows: 4, + show: { + outlookType: ['message'], + messageActions: ['sendMessage'] + }, + additionalParams: true, + optional: true + }, + // Update Message Parameters + { + label: 'Message ID [Update Message]', + name: 'messageIdUpdateMessage', + type: 'string', + description: 'ID of the message to update', + show: { + outlookType: ['message'], + messageActions: ['updateMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Is Read [Update Message]', + name: 'isReadUpdateMessage', + type: 'boolean', + description: 'Mark message as read/unread', + show: { + outlookType: ['message'], + messageActions: ['updateMessage'] + }, + additionalParams: true, + optional: true + }, + // Delete Message Parameters + { + label: 'Message ID [Delete Message]', + name: 'messageIdDeleteMessage', + type: 'string', + description: 'ID of the message to delete', + show: { + outlookType: ['message'], + messageActions: ['deleteMessage'] + }, + additionalParams: true, + optional: true + }, + // Copy Message Parameters + { + label: 'Message ID [Copy Message]', + name: 'messageIdCopyMessage', + type: 'string', + description: 'ID of the message to copy', + show: { + outlookType: ['message'], + messageActions: ['copyMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Destination Folder ID [Copy Message]', + name: 'destinationFolderIdCopyMessage', + type: 'string', + description: 'ID of the destination folder', + show: { + outlookType: ['message'], + messageActions: ['copyMessage'] + }, + additionalParams: true, + optional: true + }, + // Move Message Parameters + { + label: 'Message ID [Move Message]', + name: 'messageIdMoveMessage', + type: 'string', + description: 'ID of the message to move', + show: { + outlookType: ['message'], + messageActions: ['moveMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Destination Folder ID [Move Message]', + name: 'destinationFolderIdMoveMessage', + type: 'string', + description: 'ID of the destination folder', + show: { + outlookType: ['message'], + messageActions: ['moveMessage'] + }, + additionalParams: true, + optional: true + }, + // Reply Message Parameters + { + label: 'Message ID [Reply Message]', + name: 'messageIdReplyMessage', + type: 'string', + description: 'ID of the message to reply to', + show: { + outlookType: ['message'], + messageActions: ['replyMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Reply Body [Reply Message]', + name: 'replyBodyReplyMessage', + type: 'string', + description: 'Reply message body', + rows: 4, + show: { + outlookType: ['message'], + messageActions: ['replyMessage'] + }, + additionalParams: true, + optional: true + }, + // Forward Message Parameters + { + label: 'Message ID [Forward Message]', + name: 'messageIdForwardMessage', + type: 'string', + description: 'ID of the message to forward', + show: { + outlookType: ['message'], + messageActions: ['forwardMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Forward To [Forward Message]', + name: 'forwardToForwardMessage', + type: 'string', + description: 'Email address(es) to forward to, comma-separated', + show: { + outlookType: ['message'], + messageActions: ['forwardMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Forward Comment [Forward Message]', + name: 'forwardCommentForwardMessage', + type: 'string', + description: 'Additional comment to include with forward', + rows: 2, + show: { + outlookType: ['message'], + messageActions: ['forwardMessage'] + }, + additionalParams: true, + optional: true + } + ] + } + + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + const outlookType = nodeData.inputs?.outlookType as string + const calendarActions = nodeData.inputs?.calendarActions as string + const messageActions = nodeData.inputs?.messageActions as string + + let credentialData = await getCredentialData(nodeData.credential ?? '', options) + credentialData = await refreshOAuth2Token(nodeData.credential ?? '', credentialData, options) + const accessToken = getCredentialParam('access_token', credentialData, nodeData) + + if (!accessToken) { + throw new Error('No access token found in credential') + } + + let actions: string[] = [] + if (outlookType === 'calendar') { + actions = convertMultiOptionsToStringArray(calendarActions) + } else if (outlookType === 'message') { + actions = convertMultiOptionsToStringArray(messageActions) + } + + const defaultParams = this.transformNodeInputsToToolArgs(nodeData) + + const outlookTools = createOutlookTools({ + accessToken, + actions, + defaultParams + }) + + return outlookTools + } + + transformNodeInputsToToolArgs(nodeData: INodeData): Record { + // Collect default parameters from inputs + const defaultParams: Record = {} + + // Calendar parameters + if (nodeData.inputs?.maxResultsListCalendars) defaultParams.maxResultsListCalendars = nodeData.inputs.maxResultsListCalendars + if (nodeData.inputs?.calendarIdGetCalendar) defaultParams.calendarIdGetCalendar = nodeData.inputs.calendarIdGetCalendar + if (nodeData.inputs?.calendarNameCreateCalendar) + defaultParams.calendarNameCreateCalendar = nodeData.inputs.calendarNameCreateCalendar + if (nodeData.inputs?.calendarIdUpdateCalendar) defaultParams.calendarIdUpdateCalendar = nodeData.inputs.calendarIdUpdateCalendar + if (nodeData.inputs?.calendarNameUpdateCalendar) + defaultParams.calendarNameUpdateCalendar = nodeData.inputs.calendarNameUpdateCalendar + if (nodeData.inputs?.calendarIdDeleteCalendar) defaultParams.calendarIdDeleteCalendar = nodeData.inputs.calendarIdDeleteCalendar + if (nodeData.inputs?.calendarIdListEvents) defaultParams.calendarIdListEvents = nodeData.inputs.calendarIdListEvents + if (nodeData.inputs?.maxResultsListEvents) defaultParams.maxResultsListEvents = nodeData.inputs.maxResultsListEvents + if (nodeData.inputs?.startDateTimeListEvents) defaultParams.startDateTimeListEvents = nodeData.inputs.startDateTimeListEvents + if (nodeData.inputs?.endDateTimeListEvents) defaultParams.endDateTimeListEvents = nodeData.inputs.endDateTimeListEvents + if (nodeData.inputs?.eventIdGetEvent) defaultParams.eventIdGetEvent = nodeData.inputs.eventIdGetEvent + if (nodeData.inputs?.subjectCreateEvent) defaultParams.subjectCreateEvent = nodeData.inputs.subjectCreateEvent + if (nodeData.inputs?.bodyCreateEvent) defaultParams.bodyCreateEvent = nodeData.inputs.bodyCreateEvent + if (nodeData.inputs?.startDateTimeCreateEvent) defaultParams.startDateTimeCreateEvent = nodeData.inputs.startDateTimeCreateEvent + if (nodeData.inputs?.endDateTimeCreateEvent) defaultParams.endDateTimeCreateEvent = nodeData.inputs.endDateTimeCreateEvent + if (nodeData.inputs?.timeZoneCreateEvent) defaultParams.timeZoneCreateEvent = nodeData.inputs.timeZoneCreateEvent + if (nodeData.inputs?.locationCreateEvent) defaultParams.locationCreateEvent = nodeData.inputs.locationCreateEvent + if (nodeData.inputs?.attendeesCreateEvent) defaultParams.attendeesCreateEvent = nodeData.inputs.attendeesCreateEvent + if (nodeData.inputs?.eventIdUpdateEvent) defaultParams.eventIdUpdateEvent = nodeData.inputs.eventIdUpdateEvent + if (nodeData.inputs?.subjectUpdateEvent) defaultParams.subjectUpdateEvent = nodeData.inputs.subjectUpdateEvent + if (nodeData.inputs?.eventIdDeleteEvent) defaultParams.eventIdDeleteEvent = nodeData.inputs.eventIdDeleteEvent + + // Message parameters + if (nodeData.inputs?.maxResultsListMessages) defaultParams.maxResultsListMessages = nodeData.inputs.maxResultsListMessages + if (nodeData.inputs?.filterListMessages) defaultParams.filterListMessages = nodeData.inputs.filterListMessages + if (nodeData.inputs?.messageIdGetMessage) defaultParams.messageIdGetMessage = nodeData.inputs.messageIdGetMessage + if (nodeData.inputs?.toCreateDraftMessage) defaultParams.toCreateDraftMessage = nodeData.inputs.toCreateDraftMessage + if (nodeData.inputs?.subjectCreateDraftMessage) defaultParams.subjectCreateDraftMessage = nodeData.inputs.subjectCreateDraftMessage + if (nodeData.inputs?.bodyCreateDraftMessage) defaultParams.bodyCreateDraftMessage = nodeData.inputs.bodyCreateDraftMessage + if (nodeData.inputs?.ccCreateDraftMessage) defaultParams.ccCreateDraftMessage = nodeData.inputs.ccCreateDraftMessage + if (nodeData.inputs?.bccCreateDraftMessage) defaultParams.bccCreateDraftMessage = nodeData.inputs.bccCreateDraftMessage + if (nodeData.inputs?.toSendMessage) defaultParams.toSendMessage = nodeData.inputs.toSendMessage + if (nodeData.inputs?.subjectSendMessage) defaultParams.subjectSendMessage = nodeData.inputs.subjectSendMessage + if (nodeData.inputs?.bodySendMessage) defaultParams.bodySendMessage = nodeData.inputs.bodySendMessage + if (nodeData.inputs?.messageIdUpdateMessage) defaultParams.messageIdUpdateMessage = nodeData.inputs.messageIdUpdateMessage + if (nodeData.inputs?.isReadUpdateMessage !== undefined) defaultParams.isReadUpdateMessage = nodeData.inputs.isReadUpdateMessage + if (nodeData.inputs?.messageIdDeleteMessage) defaultParams.messageIdDeleteMessage = nodeData.inputs.messageIdDeleteMessage + if (nodeData.inputs?.messageIdCopyMessage) defaultParams.messageIdCopyMessage = nodeData.inputs.messageIdCopyMessage + if (nodeData.inputs?.destinationFolderIdCopyMessage) + defaultParams.destinationFolderIdCopyMessage = nodeData.inputs.destinationFolderIdCopyMessage + if (nodeData.inputs?.messageIdMoveMessage) defaultParams.messageIdMoveMessage = nodeData.inputs.messageIdMoveMessage + if (nodeData.inputs?.destinationFolderIdMoveMessage) + defaultParams.destinationFolderIdMoveMessage = nodeData.inputs.destinationFolderIdMoveMessage + if (nodeData.inputs?.messageIdReplyMessage) defaultParams.messageIdReplyMessage = nodeData.inputs.messageIdReplyMessage + if (nodeData.inputs?.replyBodyReplyMessage) defaultParams.replyBodyReplyMessage = nodeData.inputs.replyBodyReplyMessage + if (nodeData.inputs?.messageIdForwardMessage) defaultParams.messageIdForwardMessage = nodeData.inputs.messageIdForwardMessage + if (nodeData.inputs?.forwardToForwardMessage) defaultParams.forwardToForwardMessage = nodeData.inputs.forwardToForwardMessage + if (nodeData.inputs?.forwardCommentForwardMessage) + defaultParams.forwardCommentForwardMessage = nodeData.inputs.forwardCommentForwardMessage + + return defaultParams + } +} + +module.exports = { nodeClass: MicrosoftOutlook_Tools } diff --git a/packages/components/nodes/tools/MicrosoftOutlook/core.ts b/packages/components/nodes/tools/MicrosoftOutlook/core.ts new file mode 100644 index 00000000000..ce6fe8ba8fc --- /dev/null +++ b/packages/components/nodes/tools/MicrosoftOutlook/core.ts @@ -0,0 +1,1029 @@ +import { z } from 'zod' +import fetch from 'node-fetch' +import { DynamicStructuredTool } from '../OpenAPIToolkit/core' +import { TOOL_ARGS_PREFIX } from '../../../src/agents' + +export const desc = `Use this when you want to access Microsoft Outlook API for managing calendars, events, and messages` + +export interface Headers { + [key: string]: string +} + +export interface Body { + [key: string]: any +} + +export interface RequestParameters { + headers?: Headers + body?: Body + url?: string + description?: string + name?: string + actions?: string[] + accessToken?: string + defaultParams?: any +} + +// Define schemas for different Outlook operations + +// Calendar Schemas +const ListCalendarsSchema = z.object({ + maxResults: z.number().optional().default(50).describe('Maximum number of calendars to return') +}) + +const GetCalendarSchema = z.object({ + calendarId: z.string().describe('ID of the calendar to retrieve') +}) + +const CreateCalendarSchema = z.object({ + calendarName: z.string().describe('Name of the calendar') +}) + +const UpdateCalendarSchema = z.object({ + calendarId: z.string().describe('ID of the calendar to update'), + calendarName: z.string().describe('New name of the calendar') +}) + +const DeleteCalendarSchema = z.object({ + calendarId: z.string().describe('ID of the calendar to delete') +}) + +const ListEventsSchema = z.object({ + calendarId: z.string().optional().describe('ID of the calendar (empty for primary calendar)'), + maxResults: z.number().optional().default(50).describe('Maximum number of events to return'), + startDateTime: z.string().optional().describe('Start date time filter in ISO format'), + endDateTime: z.string().optional().describe('End date time filter in ISO format') +}) + +const GetEventSchema = z.object({ + eventId: z.string().describe('ID of the event to retrieve') +}) + +const CreateEventSchema = z.object({ + subject: z.string().describe('Subject/title of the event'), + body: z.string().optional().describe('Body/description of the event'), + startDateTime: z.string().describe('Start date and time in ISO format'), + endDateTime: z.string().describe('End date and time in ISO format'), + timeZone: z.string().optional().default('UTC').describe('Time zone for the event'), + location: z.string().optional().describe('Location of the event'), + attendees: z.string().optional().describe('Comma-separated list of attendee email addresses') +}) + +const UpdateEventSchema = z.object({ + eventId: z.string().describe('ID of the event to update'), + subject: z.string().optional().describe('New subject/title of the event') +}) + +const DeleteEventSchema = z.object({ + eventId: z.string().describe('ID of the event to delete') +}) + +// Message Schemas +const ListMessagesSchema = z.object({ + maxResults: z.number().optional().default(50).describe('Maximum number of messages to return'), + filter: z.string().optional().describe('Filter query (e.g., "isRead eq false")') +}) + +const GetMessageSchema = z.object({ + messageId: z.string().describe('ID of the message to retrieve') +}) + +const CreateDraftMessageSchema = z.object({ + to: z.string().describe('Recipient email address(es), comma-separated'), + subject: z.string().optional().describe('Subject of the message'), + body: z.string().optional().describe('Body content of the message'), + cc: z.string().optional().describe('CC email address(es), comma-separated'), + bcc: z.string().optional().describe('BCC email address(es), comma-separated') +}) + +const SendMessageSchema = z.object({ + to: z.string().describe('Recipient email address(es), comma-separated'), + subject: z.string().optional().describe('Subject of the message'), + body: z.string().optional().describe('Body content of the message') +}) + +const UpdateMessageSchema = z.object({ + messageId: z.string().describe('ID of the message to update'), + isRead: z.boolean().optional().describe('Mark message as read/unread') +}) + +const DeleteMessageSchema = z.object({ + messageId: z.string().describe('ID of the message to delete') +}) + +const CopyMessageSchema = z.object({ + messageId: z.string().describe('ID of the message to copy'), + destinationFolderId: z.string().describe('ID of the destination folder') +}) + +const MoveMessageSchema = z.object({ + messageId: z.string().describe('ID of the message to move'), + destinationFolderId: z.string().describe('ID of the destination folder') +}) + +const ReplyMessageSchema = z.object({ + messageId: z.string().describe('ID of the message to reply to'), + replyBody: z.string().describe('Reply message body') +}) + +const ForwardMessageSchema = z.object({ + messageId: z.string().describe('ID of the message to forward'), + forwardTo: z.string().describe('Email address(es) to forward to, comma-separated'), + forwardComment: z.string().optional().describe('Additional comment to include with forward') +}) + +class BaseOutlookTool extends DynamicStructuredTool { + protected accessToken: string = '' + + constructor(args: any) { + super(args) + this.accessToken = args.accessToken ?? '' + } + + async makeGraphRequest(url: string, method: string = 'GET', body?: any, params?: any): Promise { + const headers = { + Authorization: `Bearer ${this.accessToken}`, + 'Content-Type': 'application/json', + ...this.headers + } + + const response = await fetch(url, { + method, + headers, + body: body ? JSON.stringify(body) : undefined + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`Graph API Error ${response.status}: ${response.statusText} - ${errorText}`) + } + + const data = await response.text() + return data + TOOL_ARGS_PREFIX + JSON.stringify(params) + } + + parseEmailAddresses(emailString: string) { + return emailString.split(',').map((email) => ({ + emailAddress: { + address: email.trim(), + name: email.trim() + } + })) + } +} + +// Calendar Tools +class ListCalendarsTool extends BaseOutlookTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'list_calendars', + description: 'List calendars in Microsoft Outlook', + schema: ListCalendarsSchema, + baseUrl: 'https://graph.microsoft.com/v1.0/me/calendars', + method: 'GET', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const queryParams = new URLSearchParams() + + if (params.maxResults) queryParams.append('$top', params.maxResults.toString()) + + const url = `https://graph.microsoft.com/v1.0/me/calendars?${queryParams.toString()}` + + try { + const response = await this.makeGraphRequest(url, 'GET', undefined, params) + return response + } catch (error) { + return `Error listing calendars: ${error}` + } + } +} + +class GetCalendarTool extends BaseOutlookTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'get_calendar', + description: 'Get a specific calendar by ID from Microsoft Outlook', + schema: GetCalendarSchema, + baseUrl: 'https://graph.microsoft.com/v1.0/me/calendars', + method: 'GET', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const url = `https://graph.microsoft.com/v1.0/me/calendars/${params.calendarId}` + + try { + const response = await this.makeGraphRequest(url, 'GET', undefined, params) + return response + } catch (error) { + return `Error getting calendar: ${error}` + } + } +} + +class CreateCalendarTool extends BaseOutlookTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'create_calendar', + description: 'Create a new calendar in Microsoft Outlook', + schema: CreateCalendarSchema, + baseUrl: 'https://graph.microsoft.com/v1.0/me/calendars', + method: 'POST', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const calendarData = { + name: params.calendarName + } + + const url = 'https://graph.microsoft.com/v1.0/me/calendars' + const response = await this.makeGraphRequest(url, 'POST', calendarData, params) + return response + } catch (error) { + return `Error creating calendar: ${error}` + } + } +} + +class UpdateCalendarTool extends BaseOutlookTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'update_calendar', + description: 'Update a calendar in Microsoft Outlook', + schema: UpdateCalendarSchema, + baseUrl: 'https://graph.microsoft.com/v1.0/me/calendars', + method: 'PATCH', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const calendarData = { + name: params.calendarName + } + + const url = `https://graph.microsoft.com/v1.0/me/calendars/${params.calendarId}` + const response = await this.makeGraphRequest(url, 'PATCH', calendarData, params) + return response + } catch (error) { + return `Error updating calendar: ${error}` + } + } +} + +class DeleteCalendarTool extends BaseOutlookTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'delete_calendar', + description: 'Delete a calendar from Microsoft Outlook', + schema: DeleteCalendarSchema, + baseUrl: 'https://graph.microsoft.com/v1.0/me/calendars', + method: 'DELETE', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const url = `https://graph.microsoft.com/v1.0/me/calendars/${params.calendarId}` + + try { + await this.makeGraphRequest(url, 'DELETE', undefined, params) + return `Calendar ${params.calendarId} deleted successfully` + } catch (error) { + return `Error deleting calendar: ${error}` + } + } +} + +class ListEventsTool extends BaseOutlookTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'list_events', + description: 'List events from Microsoft Outlook calendar', + schema: ListEventsSchema, + baseUrl: 'https://graph.microsoft.com/v1.0/me/events', + method: 'GET', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const queryParams = new URLSearchParams() + + if (params.maxResults) queryParams.append('$top', params.maxResults.toString()) + if (params.startDateTime) queryParams.append('$filter', `start/dateTime ge '${params.startDateTime}'`) + if (params.endDateTime) { + const existingFilter = queryParams.get('$filter') + const endFilter = `end/dateTime le '${params.endDateTime}'` + if (existingFilter) { + queryParams.set('$filter', `${existingFilter} and ${endFilter}`) + } else { + queryParams.append('$filter', endFilter) + } + } + + const baseUrl = params.calendarId + ? `https://graph.microsoft.com/v1.0/me/calendars/${params.calendarId}/events` + : 'https://graph.microsoft.com/v1.0/me/events' + + const url = `${baseUrl}?${queryParams.toString()}` + + try { + const response = await this.makeGraphRequest(url, 'GET', undefined, params) + return response + } catch (error) { + return `Error listing events: ${error}` + } + } +} + +class GetEventTool extends BaseOutlookTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'get_event', + description: 'Get a specific event by ID from Microsoft Outlook', + schema: GetEventSchema, + baseUrl: 'https://graph.microsoft.com/v1.0/me/events', + method: 'GET', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const url = `https://graph.microsoft.com/v1.0/me/events/${params.eventId}` + + try { + const response = await this.makeGraphRequest(url, 'GET', undefined, params) + return response + } catch (error) { + return `Error getting event: ${error}` + } + } +} + +class CreateEventTool extends BaseOutlookTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'create_event', + description: 'Create a new event in Microsoft Outlook calendar', + schema: CreateEventSchema, + baseUrl: 'https://graph.microsoft.com/v1.0/me/events', + method: 'POST', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const eventData = { + subject: params.subject, + body: { + contentType: 'HTML', + content: params.body || '' + }, + start: { + dateTime: params.startDateTime, + timeZone: params.timeZone || 'UTC' + }, + end: { + dateTime: params.endDateTime, + timeZone: params.timeZone || 'UTC' + }, + location: params.location + ? { + displayName: params.location + } + : undefined, + attendees: params.attendees ? this.parseEmailAddresses(params.attendees) : [] + } + + const url = 'https://graph.microsoft.com/v1.0/me/events' + const response = await this.makeGraphRequest(url, 'POST', eventData, params) + return response + } catch (error) { + return `Error creating event: ${error}` + } + } +} + +class UpdateEventTool extends BaseOutlookTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'update_event', + description: 'Update an event in Microsoft Outlook calendar', + schema: UpdateEventSchema, + baseUrl: 'https://graph.microsoft.com/v1.0/me/events', + method: 'PATCH', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const eventData: any = {} + if (params.subject) eventData.subject = params.subject + + const url = `https://graph.microsoft.com/v1.0/me/events/${params.eventId}` + const response = await this.makeGraphRequest(url, 'PATCH', eventData, params) + return response + } catch (error) { + return `Error updating event: ${error}` + } + } +} + +class DeleteEventTool extends BaseOutlookTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'delete_event', + description: 'Delete an event from Microsoft Outlook calendar', + schema: DeleteEventSchema, + baseUrl: 'https://graph.microsoft.com/v1.0/me/events', + method: 'DELETE', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const url = `https://graph.microsoft.com/v1.0/me/events/${params.eventId}` + + try { + await this.makeGraphRequest(url, 'DELETE', undefined, params) + return `Event ${params.eventId} deleted successfully` + } catch (error) { + return `Error deleting event: ${error}` + } + } +} + +// Message Tools +class ListMessagesTool extends BaseOutlookTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'list_messages', + description: 'List messages from Microsoft Outlook mailbox', + schema: ListMessagesSchema, + baseUrl: 'https://graph.microsoft.com/v1.0/me/messages', + method: 'GET', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const queryParams = new URLSearchParams() + + if (params.maxResults) queryParams.append('$top', params.maxResults.toString()) + if (params.filter) queryParams.append('$filter', params.filter) + + const url = `https://graph.microsoft.com/v1.0/me/messages?${queryParams.toString()}` + + try { + const response = await this.makeGraphRequest(url, 'GET', undefined, params) + return response + } catch (error) { + return `Error listing messages: ${error}` + } + } +} + +class GetMessageTool extends BaseOutlookTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'get_message', + description: 'Get a specific message by ID from Microsoft Outlook', + schema: GetMessageSchema, + baseUrl: 'https://graph.microsoft.com/v1.0/me/messages', + method: 'GET', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const url = `https://graph.microsoft.com/v1.0/me/messages/${params.messageId}` + + try { + const response = await this.makeGraphRequest(url, 'GET', undefined, params) + return response + } catch (error) { + return `Error getting message: ${error}` + } + } +} + +class CreateDraftMessageTool extends BaseOutlookTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'create_draft_message', + description: 'Create a draft message in Microsoft Outlook', + schema: CreateDraftMessageSchema, + baseUrl: 'https://graph.microsoft.com/v1.0/me/messages', + method: 'POST', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const messageData = { + subject: params.subject || '', + body: { + contentType: 'HTML', + content: params.body || '' + }, + toRecipients: this.parseEmailAddresses(params.to), + ccRecipients: params.cc ? this.parseEmailAddresses(params.cc) : [], + bccRecipients: params.bcc ? this.parseEmailAddresses(params.bcc) : [] + } + + const url = 'https://graph.microsoft.com/v1.0/me/messages' + const response = await this.makeGraphRequest(url, 'POST', messageData, params) + return response + } catch (error) { + return `Error creating draft message: ${error}` + } + } +} + +class SendMessageTool extends BaseOutlookTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'send_message', + description: 'Send a message via Microsoft Outlook', + schema: SendMessageSchema, + baseUrl: 'https://graph.microsoft.com/v1.0/me/sendMail', + method: 'POST', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const messageData = { + message: { + subject: params.subject || '', + body: { + contentType: 'HTML', + content: params.body || '' + }, + toRecipients: this.parseEmailAddresses(params.to) + }, + saveToSentItems: true + } + + const url = 'https://graph.microsoft.com/v1.0/me/sendMail' + await this.makeGraphRequest(url, 'POST', messageData, params) + return 'Message sent successfully' + } catch (error) { + return `Error sending message: ${error}` + } + } +} + +class UpdateMessageTool extends BaseOutlookTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'update_message', + description: 'Update a message in Microsoft Outlook', + schema: UpdateMessageSchema, + baseUrl: 'https://graph.microsoft.com/v1.0/me/messages', + method: 'PATCH', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const messageData: any = {} + if (params.isRead !== undefined) messageData.isRead = params.isRead + + const url = `https://graph.microsoft.com/v1.0/me/messages/${params.messageId}` + const response = await this.makeGraphRequest(url, 'PATCH', messageData, params) + return response + } catch (error) { + return `Error updating message: ${error}` + } + } +} + +class DeleteMessageTool extends BaseOutlookTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'delete_message', + description: 'Delete a message from Microsoft Outlook', + schema: DeleteMessageSchema, + baseUrl: 'https://graph.microsoft.com/v1.0/me/messages', + method: 'DELETE', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const url = `https://graph.microsoft.com/v1.0/me/messages/${params.messageId}` + + try { + await this.makeGraphRequest(url, 'DELETE', undefined, params) + return `Message ${params.messageId} deleted successfully` + } catch (error) { + return `Error deleting message: ${error}` + } + } +} + +class CopyMessageTool extends BaseOutlookTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'copy_message', + description: 'Copy a message to another folder in Microsoft Outlook', + schema: CopyMessageSchema, + baseUrl: 'https://graph.microsoft.com/v1.0/me/messages', + method: 'POST', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const copyData = { + destinationId: params.destinationFolderId + } + + const url = `https://graph.microsoft.com/v1.0/me/messages/${params.messageId}/copy` + const response = await this.makeGraphRequest(url, 'POST', copyData, params) + return response + } catch (error) { + return `Error copying message: ${error}` + } + } +} + +class MoveMessageTool extends BaseOutlookTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'move_message', + description: 'Move a message to another folder in Microsoft Outlook', + schema: MoveMessageSchema, + baseUrl: 'https://graph.microsoft.com/v1.0/me/messages', + method: 'POST', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const moveData = { + destinationId: params.destinationFolderId + } + + const url = `https://graph.microsoft.com/v1.0/me/messages/${params.messageId}/move` + const response = await this.makeGraphRequest(url, 'POST', moveData, params) + return response + } catch (error) { + return `Error moving message: ${error}` + } + } +} + +class ReplyMessageTool extends BaseOutlookTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'reply_message', + description: 'Reply to a message in Microsoft Outlook', + schema: ReplyMessageSchema, + baseUrl: 'https://graph.microsoft.com/v1.0/me/messages', + method: 'POST', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const replyData = { + comment: params.replyBody + } + + const url = `https://graph.microsoft.com/v1.0/me/messages/${params.messageId}/reply` + await this.makeGraphRequest(url, 'POST', replyData, params) + return 'Reply sent successfully' + } catch (error) { + return `Error replying to message: ${error}` + } + } +} + +class ForwardMessageTool extends BaseOutlookTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'forward_message', + description: 'Forward a message in Microsoft Outlook', + schema: ForwardMessageSchema, + baseUrl: 'https://graph.microsoft.com/v1.0/me/messages', + method: 'POST', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const forwardData = { + toRecipients: this.parseEmailAddresses(params.forwardTo), + comment: params.forwardComment || '' + } + + const url = `https://graph.microsoft.com/v1.0/me/messages/${params.messageId}/forward` + await this.makeGraphRequest(url, 'POST', forwardData, params) + return 'Message forwarded successfully' + } catch (error) { + return `Error forwarding message: ${error}` + } + } +} + +export const createOutlookTools = (args?: RequestParameters): DynamicStructuredTool[] => { + const tools: DynamicStructuredTool[] = [] + const actions = args?.actions || [] + const accessToken = args?.accessToken || '' + const defaultParams = args?.defaultParams || {} + + // Calendar tools + if (actions.includes('listCalendars')) { + const listTool = new ListCalendarsTool({ + accessToken, + defaultParams: defaultParams.listCalendars + }) + tools.push(listTool) + } + + if (actions.includes('getCalendar')) { + const getTool = new GetCalendarTool({ + accessToken, + defaultParams: defaultParams.getCalendar + }) + tools.push(getTool) + } + + if (actions.includes('createCalendar')) { + const createTool = new CreateCalendarTool({ + accessToken, + defaultParams: defaultParams.createCalendar + }) + tools.push(createTool) + } + + if (actions.includes('updateCalendar')) { + const updateTool = new UpdateCalendarTool({ + accessToken, + defaultParams: defaultParams.updateCalendar + }) + tools.push(updateTool) + } + + if (actions.includes('deleteCalendar')) { + const deleteTool = new DeleteCalendarTool({ + accessToken, + defaultParams: defaultParams.deleteCalendar + }) + tools.push(deleteTool) + } + + if (actions.includes('listEvents')) { + const listTool = new ListEventsTool({ + accessToken, + defaultParams: defaultParams.listEvents + }) + tools.push(listTool) + } + + if (actions.includes('getEvent')) { + const getTool = new GetEventTool({ + accessToken, + defaultParams: defaultParams.getEvent + }) + tools.push(getTool) + } + + if (actions.includes('createEvent')) { + const createTool = new CreateEventTool({ + accessToken, + defaultParams: defaultParams.createEvent + }) + tools.push(createTool) + } + + if (actions.includes('updateEvent')) { + const updateTool = new UpdateEventTool({ + accessToken, + defaultParams: defaultParams.updateEvent + }) + tools.push(updateTool) + } + + if (actions.includes('deleteEvent')) { + const deleteTool = new DeleteEventTool({ + accessToken, + defaultParams: defaultParams.deleteEvent + }) + tools.push(deleteTool) + } + + // Message tools + if (actions.includes('listMessages')) { + const listTool = new ListMessagesTool({ + accessToken, + defaultParams: defaultParams.listMessages + }) + tools.push(listTool) + } + + if (actions.includes('getMessage')) { + const getTool = new GetMessageTool({ + accessToken, + defaultParams: defaultParams.getMessage + }) + tools.push(getTool) + } + + if (actions.includes('createDraftMessage')) { + const createTool = new CreateDraftMessageTool({ + accessToken, + defaultParams: defaultParams.createDraftMessage + }) + tools.push(createTool) + } + + if (actions.includes('sendMessage')) { + const sendTool = new SendMessageTool({ + accessToken, + defaultParams: defaultParams.sendMessage + }) + tools.push(sendTool) + } + + if (actions.includes('updateMessage')) { + const updateTool = new UpdateMessageTool({ + accessToken, + defaultParams: defaultParams.updateMessage + }) + tools.push(updateTool) + } + + if (actions.includes('deleteMessage')) { + const deleteTool = new DeleteMessageTool({ + accessToken, + defaultParams: defaultParams.deleteMessage + }) + tools.push(deleteTool) + } + + if (actions.includes('copyMessage')) { + const copyTool = new CopyMessageTool({ + accessToken, + defaultParams: defaultParams.copyMessage + }) + tools.push(copyTool) + } + + if (actions.includes('moveMessage')) { + const moveTool = new MoveMessageTool({ + accessToken, + defaultParams: defaultParams.moveMessage + }) + tools.push(moveTool) + } + + if (actions.includes('replyMessage')) { + const replyTool = new ReplyMessageTool({ + accessToken, + defaultParams: defaultParams.replyMessage + }) + tools.push(replyTool) + } + + if (actions.includes('forwardMessage')) { + const forwardTool = new ForwardMessageTool({ + accessToken, + defaultParams: defaultParams.forwardMessage + }) + tools.push(forwardTool) + } + + return tools +} diff --git a/packages/components/nodes/tools/MicrosoftOutlook/outlook.svg b/packages/components/nodes/tools/MicrosoftOutlook/outlook.svg new file mode 100644 index 00000000000..134a2ee9206 --- /dev/null +++ b/packages/components/nodes/tools/MicrosoftOutlook/outlook.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/tools/MicrosoftTeams/MicrosoftTeams.ts b/packages/components/nodes/tools/MicrosoftTeams/MicrosoftTeams.ts new file mode 100644 index 00000000000..d9391c50457 --- /dev/null +++ b/packages/components/nodes/tools/MicrosoftTeams/MicrosoftTeams.ts @@ -0,0 +1,1012 @@ +import { INode, INodeData, INodeParams } from '../../../src/Interface' +import { convertMultiOptionsToStringArray, getCredentialData, getCredentialParam, refreshOAuth2Token } from '../../../src/utils' +import { createTeamsTools } from './core' + +class MicrosoftTeams_Tools implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + baseClasses: string[] + inputs: INodeParams[] + credential: INodeParams + + constructor() { + this.label = 'Microsoft Teams' + this.name = 'microsoftTeams' + this.version = 1.0 + this.type = 'MicrosoftTeams' + this.icon = 'teams.svg' + this.category = 'Tools' + this.description = 'Perform Microsoft Teams operations for channels, chats, and chat messages' + this.baseClasses = [this.type, 'Tool'] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['microsoftTeamsOAuth2'] + } + this.inputs = [ + { + label: 'Type', + name: 'teamsType', + type: 'options', + options: [ + { + label: 'Channel', + name: 'channel' + }, + { + label: 'Chat', + name: 'chat' + }, + { + label: 'Chat Message', + name: 'chatMessage' + } + ] + }, + // Channel Actions + { + label: 'Channel Actions', + name: 'channelActions', + type: 'multiOptions', + options: [ + { + label: 'List Channels', + name: 'listChannels' + }, + { + label: 'Get Channel', + name: 'getChannel' + }, + { + label: 'Create Channel', + name: 'createChannel' + }, + { + label: 'Update Channel', + name: 'updateChannel' + }, + { + label: 'Delete Channel', + name: 'deleteChannel' + }, + { + label: 'Archive Channel', + name: 'archiveChannel' + }, + { + label: 'Unarchive Channel', + name: 'unarchiveChannel' + }, + { + label: 'List Channel Members', + name: 'listChannelMembers' + }, + { + label: 'Add Channel Member', + name: 'addChannelMember' + }, + { + label: 'Remove Channel Member', + name: 'removeChannelMember' + } + ], + show: { + teamsType: ['channel'] + } + }, + // Chat Actions + { + label: 'Chat Actions', + name: 'chatActions', + type: 'multiOptions', + options: [ + { + label: 'List Chats', + name: 'listChats' + }, + { + label: 'Get Chat', + name: 'getChat' + }, + { + label: 'Create Chat', + name: 'createChat' + }, + { + label: 'Update Chat', + name: 'updateChat' + }, + { + label: 'Delete Chat', + name: 'deleteChat' + }, + { + label: 'List Chat Members', + name: 'listChatMembers' + }, + { + label: 'Add Chat Member', + name: 'addChatMember' + }, + { + label: 'Remove Chat Member', + name: 'removeChatMember' + }, + { + label: 'Pin Message', + name: 'pinMessage' + }, + { + label: 'Unpin Message', + name: 'unpinMessage' + } + ], + show: { + teamsType: ['chat'] + } + }, + // Chat Message Actions + { + label: 'Chat Message Actions', + name: 'chatMessageActions', + type: 'multiOptions', + options: [ + { + label: 'List Messages', + name: 'listMessages' + }, + { + label: 'Get Message', + name: 'getMessage' + }, + { + label: 'Send Message', + name: 'sendMessage' + }, + { + label: 'Update Message', + name: 'updateMessage' + }, + { + label: 'Delete Message', + name: 'deleteMessage' + }, + { + label: 'Reply to Message', + name: 'replyToMessage' + }, + { + label: 'Set Reaction', + name: 'setReaction' + }, + { + label: 'Unset Reaction', + name: 'unsetReaction' + }, + { + label: 'Get All Messages', + name: 'getAllMessages' + } + ], + show: { + teamsType: ['chatMessage'] + } + }, + + // CHANNEL PARAMETERS + // List Channels Parameters + { + label: 'Team ID [List Channels]', + name: 'teamIdListChannels', + type: 'string', + description: 'ID of the team to list channels from', + show: { + teamsType: ['channel'], + channelActions: ['listChannels'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Max Results [List Channels]', + name: 'maxResultsListChannels', + type: 'number', + description: 'Maximum number of channels to return', + default: 50, + show: { + teamsType: ['channel'], + channelActions: ['listChannels'] + }, + additionalParams: true, + optional: true + }, + + // Get Channel Parameters + { + label: 'Team ID [Get Channel]', + name: 'teamIdGetChannel', + type: 'string', + description: 'ID of the team that contains the channel', + show: { + teamsType: ['channel'], + channelActions: ['getChannel'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Channel ID [Get Channel]', + name: 'channelIdGetChannel', + type: 'string', + description: 'ID of the channel to retrieve', + show: { + teamsType: ['channel'], + channelActions: ['getChannel'] + }, + additionalParams: true, + optional: true + }, + + // Create Channel Parameters + { + label: 'Team ID [Create Channel]', + name: 'teamIdCreateChannel', + type: 'string', + description: 'ID of the team to create the channel in', + show: { + teamsType: ['channel'], + channelActions: ['createChannel'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Display Name [Create Channel]', + name: 'displayNameCreateChannel', + type: 'string', + description: 'Display name of the channel', + placeholder: 'My New Channel', + show: { + teamsType: ['channel'], + channelActions: ['createChannel'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Description [Create Channel]', + name: 'descriptionCreateChannel', + type: 'string', + description: 'Description of the channel', + placeholder: 'Channel description', + rows: 2, + show: { + teamsType: ['channel'], + channelActions: ['createChannel'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Membership Type [Create Channel]', + name: 'membershipTypeCreateChannel', + type: 'options', + options: [ + { label: 'Standard', name: 'standard' }, + { label: 'Private', name: 'private' }, + { label: 'Shared', name: 'shared' } + ], + default: 'standard', + description: 'Type of channel membership', + show: { + teamsType: ['channel'], + channelActions: ['createChannel'] + }, + additionalParams: true, + optional: true + }, + + // Update Channel Parameters + { + label: 'Team ID [Update Channel]', + name: 'teamIdUpdateChannel', + type: 'string', + description: 'ID of the team that contains the channel', + show: { + teamsType: ['channel'], + channelActions: ['updateChannel'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Channel ID [Update Channel]', + name: 'channelIdUpdateChannel', + type: 'string', + description: 'ID of the channel to update', + show: { + teamsType: ['channel'], + channelActions: ['updateChannel'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Display Name [Update Channel]', + name: 'displayNameUpdateChannel', + type: 'string', + description: 'New display name of the channel', + show: { + teamsType: ['channel'], + channelActions: ['updateChannel'] + }, + additionalParams: true, + optional: true + }, + + // Delete/Archive Channel Parameters + { + label: 'Team ID [Delete/Archive Channel]', + name: 'teamIdDeleteChannel', + type: 'string', + description: 'ID of the team that contains the channel', + show: { + teamsType: ['channel'], + channelActions: ['deleteChannel', 'archiveChannel', 'unarchiveChannel'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Channel ID [Delete/Archive Channel]', + name: 'channelIdDeleteChannel', + type: 'string', + description: 'ID of the channel to delete or archive', + show: { + teamsType: ['channel'], + channelActions: ['deleteChannel', 'archiveChannel', 'unarchiveChannel'] + }, + additionalParams: true, + optional: true + }, + + // Channel Members Parameters + { + label: 'Team ID [Channel Members]', + name: 'teamIdChannelMembers', + type: 'string', + description: 'ID of the team that contains the channel', + show: { + teamsType: ['channel'], + channelActions: ['listChannelMembers', 'addChannelMember', 'removeChannelMember'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Channel ID [Channel Members]', + name: 'channelIdChannelMembers', + type: 'string', + description: 'ID of the channel', + show: { + teamsType: ['channel'], + channelActions: ['listChannelMembers', 'addChannelMember', 'removeChannelMember'] + }, + additionalParams: true, + optional: true + }, + { + label: 'User ID [Add/Remove Channel Member]', + name: 'userIdChannelMember', + type: 'string', + description: 'ID of the user to add or remove', + show: { + teamsType: ['channel'], + channelActions: ['addChannelMember', 'removeChannelMember'] + }, + additionalParams: true, + optional: true + }, + + // CHAT PARAMETERS + // List Chats Parameters + { + label: 'Max Results [List Chats]', + name: 'maxResultsListChats', + type: 'number', + description: 'Maximum number of chats to return', + default: 50, + show: { + teamsType: ['chat'], + chatActions: ['listChats'] + }, + additionalParams: true, + optional: true + }, + + // Get Chat Parameters + { + label: 'Chat ID [Get Chat]', + name: 'chatIdGetChat', + type: 'string', + description: 'ID of the chat to retrieve', + show: { + teamsType: ['chat'], + chatActions: ['getChat'] + }, + additionalParams: true, + optional: true + }, + + // Create Chat Parameters + { + label: 'Chat Type [Create Chat]', + name: 'chatTypeCreateChat', + type: 'options', + options: [ + { label: 'One on One', name: 'oneOnOne' }, + { label: 'Group', name: 'group' } + ], + default: 'group', + description: 'Type of chat to create', + show: { + teamsType: ['chat'], + chatActions: ['createChat'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Topic [Create Chat]', + name: 'topicCreateChat', + type: 'string', + description: 'Topic/subject of the chat (for group chats)', + placeholder: 'Chat topic', + show: { + teamsType: ['chat'], + chatActions: ['createChat'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Members [Create Chat]', + name: 'membersCreateChat', + type: 'string', + description: 'Comma-separated list of user IDs to add to the chat', + placeholder: 'user1@example.com,user2@example.com', + show: { + teamsType: ['chat'], + chatActions: ['createChat'] + }, + additionalParams: true, + optional: true + }, + + // Update Chat Parameters + { + label: 'Chat ID [Update Chat]', + name: 'chatIdUpdateChat', + type: 'string', + description: 'ID of the chat to update', + show: { + teamsType: ['chat'], + chatActions: ['updateChat'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Topic [Update Chat]', + name: 'topicUpdateChat', + type: 'string', + description: 'New topic/subject of the chat', + show: { + teamsType: ['chat'], + chatActions: ['updateChat'] + }, + additionalParams: true, + optional: true + }, + + // Delete Chat Parameters + { + label: 'Chat ID [Delete Chat]', + name: 'chatIdDeleteChat', + type: 'string', + description: 'ID of the chat to delete', + show: { + teamsType: ['chat'], + chatActions: ['deleteChat'] + }, + additionalParams: true, + optional: true + }, + + // Chat Members Parameters + { + label: 'Chat ID [Chat Members]', + name: 'chatIdChatMembers', + type: 'string', + description: 'ID of the chat', + show: { + teamsType: ['chat'], + chatActions: ['listChatMembers', 'addChatMember', 'removeChatMember'] + }, + additionalParams: true, + optional: true + }, + { + label: 'User ID [Add/Remove Chat Member]', + name: 'userIdChatMember', + type: 'string', + description: 'ID of the user to add or remove', + show: { + teamsType: ['chat'], + chatActions: ['addChatMember', 'removeChatMember'] + }, + additionalParams: true, + optional: true + }, + + // Pin/Unpin Message Parameters + { + label: 'Chat ID [Pin/Unpin Message]', + name: 'chatIdPinMessage', + type: 'string', + description: 'ID of the chat', + show: { + teamsType: ['chat'], + chatActions: ['pinMessage', 'unpinMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Message ID [Pin/Unpin Message]', + name: 'messageIdPinMessage', + type: 'string', + description: 'ID of the message to pin or unpin', + show: { + teamsType: ['chat'], + chatActions: ['pinMessage', 'unpinMessage'] + }, + additionalParams: true, + optional: true + }, + + // CHAT MESSAGE PARAMETERS + // List Messages Parameters + { + label: 'Chat/Channel ID [List Messages]', + name: 'chatChannelIdListMessages', + type: 'string', + description: 'ID of the chat or channel to list messages from', + show: { + teamsType: ['chatMessage'], + chatMessageActions: ['listMessages'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Team ID [List Messages - Channel Only]', + name: 'teamIdListMessages', + type: 'string', + description: 'ID of the team (required for channel messages)', + show: { + teamsType: ['chatMessage'], + chatMessageActions: ['listMessages'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Max Results [List Messages]', + name: 'maxResultsListMessages', + type: 'number', + description: 'Maximum number of messages to return', + default: 50, + show: { + teamsType: ['chatMessage'], + chatMessageActions: ['listMessages'] + }, + additionalParams: true, + optional: true + }, + + // Get Message Parameters + { + label: 'Chat/Channel ID [Get Message]', + name: 'chatChannelIdGetMessage', + type: 'string', + description: 'ID of the chat or channel', + show: { + teamsType: ['chatMessage'], + chatMessageActions: ['getMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Team ID [Get Message - Channel Only]', + name: 'teamIdGetMessage', + type: 'string', + description: 'ID of the team (required for channel messages)', + show: { + teamsType: ['chatMessage'], + chatMessageActions: ['getMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Message ID [Get Message]', + name: 'messageIdGetMessage', + type: 'string', + description: 'ID of the message to retrieve', + show: { + teamsType: ['chatMessage'], + chatMessageActions: ['getMessage'] + }, + additionalParams: true, + optional: true + }, + + // Send Message Parameters + { + label: 'Chat/Channel ID [Send Message]', + name: 'chatChannelIdSendMessage', + type: 'string', + description: 'ID of the chat or channel to send message to', + show: { + teamsType: ['chatMessage'], + chatMessageActions: ['sendMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Team ID [Send Message - Channel Only]', + name: 'teamIdSendMessage', + type: 'string', + description: 'ID of the team (required for channel messages)', + show: { + teamsType: ['chatMessage'], + chatMessageActions: ['sendMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Message Body [Send Message]', + name: 'messageBodySendMessage', + type: 'string', + description: 'Content of the message', + placeholder: 'Hello, this is a message!', + rows: 4, + show: { + teamsType: ['chatMessage'], + chatMessageActions: ['sendMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Content Type [Send Message]', + name: 'contentTypeSendMessage', + type: 'options', + options: [ + { label: 'Text', name: 'text' }, + { label: 'HTML', name: 'html' } + ], + default: 'text', + description: 'Content type of the message', + show: { + teamsType: ['chatMessage'], + chatMessageActions: ['sendMessage'] + }, + additionalParams: true, + optional: true + }, + + // Update Message Parameters + { + label: 'Chat/Channel ID [Update Message]', + name: 'chatChannelIdUpdateMessage', + type: 'string', + description: 'ID of the chat or channel', + show: { + teamsType: ['chatMessage'], + chatMessageActions: ['updateMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Team ID [Update Message - Channel Only]', + name: 'teamIdUpdateMessage', + type: 'string', + description: 'ID of the team (required for channel messages)', + show: { + teamsType: ['chatMessage'], + chatMessageActions: ['updateMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Message ID [Update Message]', + name: 'messageIdUpdateMessage', + type: 'string', + description: 'ID of the message to update', + show: { + teamsType: ['chatMessage'], + chatMessageActions: ['updateMessage'] + }, + additionalParams: true, + optional: true + }, + + // Delete Message Parameters + { + label: 'Chat/Channel ID [Delete Message]', + name: 'chatChannelIdDeleteMessage', + type: 'string', + description: 'ID of the chat or channel', + show: { + teamsType: ['chatMessage'], + chatMessageActions: ['deleteMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Team ID [Delete Message - Channel Only]', + name: 'teamIdDeleteMessage', + type: 'string', + description: 'ID of the team (required for channel messages)', + show: { + teamsType: ['chatMessage'], + chatMessageActions: ['deleteMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Message ID [Delete Message]', + name: 'messageIdDeleteMessage', + type: 'string', + description: 'ID of the message to delete', + show: { + teamsType: ['chatMessage'], + chatMessageActions: ['deleteMessage'] + }, + additionalParams: true, + optional: true + }, + + // Reply to Message Parameters + { + label: 'Chat/Channel ID [Reply to Message]', + name: 'chatChannelIdReplyMessage', + type: 'string', + description: 'ID of the chat or channel', + show: { + teamsType: ['chatMessage'], + chatMessageActions: ['replyToMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Team ID [Reply to Message - Channel Only]', + name: 'teamIdReplyMessage', + type: 'string', + description: 'ID of the team (required for channel messages)', + show: { + teamsType: ['chatMessage'], + chatMessageActions: ['replyToMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Message ID [Reply to Message]', + name: 'messageIdReplyMessage', + type: 'string', + description: 'ID of the message to reply to', + show: { + teamsType: ['chatMessage'], + chatMessageActions: ['replyToMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Reply Body [Reply to Message]', + name: 'replyBodyReplyMessage', + type: 'string', + description: 'Content of the reply', + placeholder: 'This is my reply', + rows: 3, + show: { + teamsType: ['chatMessage'], + chatMessageActions: ['replyToMessage'] + }, + additionalParams: true, + optional: true + }, + + // Set/Unset Reaction Parameters + { + label: 'Chat/Channel ID [Set/Unset Reaction]', + name: 'chatChannelIdReaction', + type: 'string', + description: 'ID of the chat or channel', + show: { + teamsType: ['chatMessage'], + chatMessageActions: ['setReaction', 'unsetReaction'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Team ID [Set/Unset Reaction - Channel Only]', + name: 'teamIdReaction', + type: 'string', + description: 'ID of the team (required for channel messages)', + show: { + teamsType: ['chatMessage'], + chatMessageActions: ['setReaction', 'unsetReaction'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Message ID [Set/Unset Reaction]', + name: 'messageIdReaction', + type: 'string', + description: 'ID of the message to react to', + show: { + teamsType: ['chatMessage'], + chatMessageActions: ['setReaction', 'unsetReaction'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Reaction Type [Set Reaction]', + name: 'reactionTypeSetReaction', + type: 'options', + options: [ + { label: 'Like', name: 'like' }, + { label: 'Heart', name: 'heart' }, + { label: 'Laugh', name: 'laugh' }, + { label: 'Surprised', name: 'surprised' }, + { label: 'Sad', name: 'sad' }, + { label: 'Angry', name: 'angry' } + ], + default: 'like', + description: 'Type of reaction to set', + show: { + teamsType: ['chatMessage'], + chatMessageActions: ['setReaction'] + }, + additionalParams: true, + optional: true + } + ] + } + + async init(nodeData: INodeData, _: string, options: any): Promise { + const teamsType = nodeData.inputs?.teamsType as string + const channelActions = nodeData.inputs?.channelActions as string + const chatActions = nodeData.inputs?.chatActions as string + const chatMessageActions = nodeData.inputs?.chatMessageActions as string + + let actions: string[] = [] + if (teamsType === 'channel') { + actions = convertMultiOptionsToStringArray(channelActions) + } else if (teamsType === 'chat') { + actions = convertMultiOptionsToStringArray(chatActions) + } else if (teamsType === 'chatMessage') { + actions = convertMultiOptionsToStringArray(chatMessageActions) + } + + let credentialData = await getCredentialData(nodeData.credential ?? '', options) + credentialData = await refreshOAuth2Token(nodeData.credential ?? '', credentialData, options) + const accessToken = getCredentialParam('access_token', credentialData, nodeData) + + if (!accessToken) { + throw new Error('No access token found in credential') + } + + const defaultParams = this.transformNodeInputsToToolArgs(nodeData) + + const teamsTools = createTeamsTools({ + accessToken, + actions, + defaultParams, + type: teamsType + }) + + return teamsTools + } + + transformNodeInputsToToolArgs(nodeData: INodeData): Record { + // Collect default parameters from inputs + const defaultParams: Record = {} + + // Channel parameters + if (nodeData.inputs?.teamIdListChannels) defaultParams.teamIdListChannels = nodeData.inputs.teamIdListChannels + if (nodeData.inputs?.maxResultsListChannels) defaultParams.maxResultsListChannels = nodeData.inputs.maxResultsListChannels + if (nodeData.inputs?.teamIdGetChannel) defaultParams.teamIdGetChannel = nodeData.inputs.teamIdGetChannel + if (nodeData.inputs?.channelIdGetChannel) defaultParams.channelIdGetChannel = nodeData.inputs.channelIdGetChannel + if (nodeData.inputs?.teamIdCreateChannel) defaultParams.teamIdCreateChannel = nodeData.inputs.teamIdCreateChannel + if (nodeData.inputs?.displayNameCreateChannel) defaultParams.displayNameCreateChannel = nodeData.inputs.displayNameCreateChannel + if (nodeData.inputs?.descriptionCreateChannel) defaultParams.descriptionCreateChannel = nodeData.inputs.descriptionCreateChannel + if (nodeData.inputs?.membershipTypeCreateChannel) + defaultParams.membershipTypeCreateChannel = nodeData.inputs.membershipTypeCreateChannel + if (nodeData.inputs?.teamIdUpdateChannel) defaultParams.teamIdUpdateChannel = nodeData.inputs.teamIdUpdateChannel + if (nodeData.inputs?.channelIdUpdateChannel) defaultParams.channelIdUpdateChannel = nodeData.inputs.channelIdUpdateChannel + if (nodeData.inputs?.displayNameUpdateChannel) defaultParams.displayNameUpdateChannel = nodeData.inputs.displayNameUpdateChannel + if (nodeData.inputs?.teamIdDeleteChannel) defaultParams.teamIdDeleteChannel = nodeData.inputs.teamIdDeleteChannel + if (nodeData.inputs?.channelIdDeleteChannel) defaultParams.channelIdDeleteChannel = nodeData.inputs.channelIdDeleteChannel + if (nodeData.inputs?.teamIdChannelMembers) defaultParams.teamIdChannelMembers = nodeData.inputs.teamIdChannelMembers + if (nodeData.inputs?.channelIdChannelMembers) defaultParams.channelIdChannelMembers = nodeData.inputs.channelIdChannelMembers + if (nodeData.inputs?.userIdChannelMember) defaultParams.userIdChannelMember = nodeData.inputs.userIdChannelMember + + // Chat parameters + if (nodeData.inputs?.maxResultsListChats) defaultParams.maxResultsListChats = nodeData.inputs.maxResultsListChats + if (nodeData.inputs?.chatIdGetChat) defaultParams.chatIdGetChat = nodeData.inputs.chatIdGetChat + if (nodeData.inputs?.chatTypeCreateChat) defaultParams.chatTypeCreateChat = nodeData.inputs.chatTypeCreateChat + if (nodeData.inputs?.topicCreateChat) defaultParams.topicCreateChat = nodeData.inputs.topicCreateChat + if (nodeData.inputs?.membersCreateChat) defaultParams.membersCreateChat = nodeData.inputs.membersCreateChat + if (nodeData.inputs?.chatIdUpdateChat) defaultParams.chatIdUpdateChat = nodeData.inputs.chatIdUpdateChat + if (nodeData.inputs?.topicUpdateChat) defaultParams.topicUpdateChat = nodeData.inputs.topicUpdateChat + if (nodeData.inputs?.chatIdDeleteChat) defaultParams.chatIdDeleteChat = nodeData.inputs.chatIdDeleteChat + if (nodeData.inputs?.chatIdChatMembers) defaultParams.chatIdChatMembers = nodeData.inputs.chatIdChatMembers + if (nodeData.inputs?.userIdChatMember) defaultParams.userIdChatMember = nodeData.inputs.userIdChatMember + if (nodeData.inputs?.chatIdPinMessage) defaultParams.chatIdPinMessage = nodeData.inputs.chatIdPinMessage + if (nodeData.inputs?.messageIdPinMessage) defaultParams.messageIdPinMessage = nodeData.inputs.messageIdPinMessage + + // Chat Message parameters + if (nodeData.inputs?.chatChannelIdListMessages) defaultParams.chatChannelIdListMessages = nodeData.inputs.chatChannelIdListMessages + if (nodeData.inputs?.teamIdListMessages) defaultParams.teamIdListMessages = nodeData.inputs.teamIdListMessages + if (nodeData.inputs?.maxResultsListMessages) defaultParams.maxResultsListMessages = nodeData.inputs.maxResultsListMessages + if (nodeData.inputs?.chatChannelIdGetMessage) defaultParams.chatChannelIdGetMessage = nodeData.inputs.chatChannelIdGetMessage + if (nodeData.inputs?.teamIdGetMessage) defaultParams.teamIdGetMessage = nodeData.inputs.teamIdGetMessage + if (nodeData.inputs?.messageIdGetMessage) defaultParams.messageIdGetMessage = nodeData.inputs.messageIdGetMessage + if (nodeData.inputs?.chatChannelIdSendMessage) defaultParams.chatChannelIdSendMessage = nodeData.inputs.chatChannelIdSendMessage + if (nodeData.inputs?.teamIdSendMessage) defaultParams.teamIdSendMessage = nodeData.inputs.teamIdSendMessage + if (nodeData.inputs?.messageBodySendMessage) defaultParams.messageBodySendMessage = nodeData.inputs.messageBodySendMessage + if (nodeData.inputs?.contentTypeSendMessage) defaultParams.contentTypeSendMessage = nodeData.inputs.contentTypeSendMessage + if (nodeData.inputs?.chatChannelIdUpdateMessage) + defaultParams.chatChannelIdUpdateMessage = nodeData.inputs.chatChannelIdUpdateMessage + if (nodeData.inputs?.teamIdUpdateMessage) defaultParams.teamIdUpdateMessage = nodeData.inputs.teamIdUpdateMessage + if (nodeData.inputs?.messageIdUpdateMessage) defaultParams.messageIdUpdateMessage = nodeData.inputs.messageIdUpdateMessage + if (nodeData.inputs?.chatChannelIdDeleteMessage) + defaultParams.chatChannelIdDeleteMessage = nodeData.inputs.chatChannelIdDeleteMessage + if (nodeData.inputs?.teamIdDeleteMessage) defaultParams.teamIdDeleteMessage = nodeData.inputs.teamIdDeleteMessage + if (nodeData.inputs?.messageIdDeleteMessage) defaultParams.messageIdDeleteMessage = nodeData.inputs.messageIdDeleteMessage + if (nodeData.inputs?.chatChannelIdReplyMessage) defaultParams.chatChannelIdReplyMessage = nodeData.inputs.chatChannelIdReplyMessage + if (nodeData.inputs?.teamIdReplyMessage) defaultParams.teamIdReplyMessage = nodeData.inputs.teamIdReplyMessage + if (nodeData.inputs?.messageIdReplyMessage) defaultParams.messageIdReplyMessage = nodeData.inputs.messageIdReplyMessage + if (nodeData.inputs?.replyBodyReplyMessage) defaultParams.replyBodyReplyMessage = nodeData.inputs.replyBodyReplyMessage + if (nodeData.inputs?.chatChannelIdReaction) defaultParams.chatChannelIdReaction = nodeData.inputs.chatChannelIdReaction + if (nodeData.inputs?.teamIdReaction) defaultParams.teamIdReaction = nodeData.inputs.teamIdReaction + if (nodeData.inputs?.messageIdReaction) defaultParams.messageIdReaction = nodeData.inputs.messageIdReaction + if (nodeData.inputs?.reactionTypeSetReaction) defaultParams.reactionTypeSetReaction = nodeData.inputs.reactionTypeSetReaction + + return defaultParams + } +} + +module.exports = { nodeClass: MicrosoftTeams_Tools } diff --git a/packages/components/nodes/tools/MicrosoftTeams/core.ts b/packages/components/nodes/tools/MicrosoftTeams/core.ts new file mode 100644 index 00000000000..a0c08091469 --- /dev/null +++ b/packages/components/nodes/tools/MicrosoftTeams/core.ts @@ -0,0 +1,1756 @@ +import { z } from 'zod' +import { CallbackManagerForToolRun } from '@langchain/core/callbacks/manager' +import { DynamicStructuredTool, DynamicStructuredToolInput } from '../OpenAPIToolkit/core' +import { TOOL_ARGS_PREFIX } from '../../../src/agents' + +interface TeamsToolOptions { + accessToken: string + actions: string[] + defaultParams: any + type: string +} + +const BASE_URL = 'https://graph.microsoft.com/v1.0' + +// Helper function to make Graph API requests +async function makeGraphRequest( + endpoint: string, + method: 'GET' | 'POST' | 'PATCH' | 'DELETE' = 'GET', + body?: any, + accessToken?: string +): Promise { + const headers: Record = { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json' + } + + const config: RequestInit = { + method, + headers + } + + if (body && (method === 'POST' || method === 'PATCH')) { + config.body = JSON.stringify(body) + } + + try { + const response = await fetch(`${BASE_URL}${endpoint}`, config) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`Microsoft Graph API error: ${response.status} ${response.statusText} - ${errorText}`) + } + + // Handle empty responses for DELETE operations + if (method === 'DELETE' || response.status === 204) { + return { success: true, message: 'Operation completed successfully' } + } + + return await response.json() + } catch (error) { + throw new Error(`Microsoft Graph request failed: ${error instanceof Error ? error.message : 'Unknown error'}`) + } +} + +// Base Teams Tool class +abstract class BaseTeamsTool extends DynamicStructuredTool { + accessToken = '' + protected defaultParams: any + + constructor(args: DynamicStructuredToolInput & { accessToken?: string; defaultParams?: any }) { + super(args) + this.accessToken = args.accessToken ?? '' + this.defaultParams = args.defaultParams || {} + } + + protected async makeTeamsRequest(endpoint: string, method: string = 'GET', body?: any) { + return await makeGraphRequest(endpoint, method as any, body, this.accessToken) + } + + protected formatResponse(data: any, params: any): string { + return JSON.stringify(data) + TOOL_ARGS_PREFIX + JSON.stringify(params) + } + + // Abstract method that must be implemented by subclasses + protected abstract _call(arg: any, runManager?: CallbackManagerForToolRun, parentConfig?: any): Promise +} + +// CHANNEL TOOLS + +class ListChannelsTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'list_channels', + description: 'List all channels in a team', + schema: z.object({ + teamId: z.string().describe('ID of the team to list channels from'), + maxResults: z.number().optional().default(50).describe('Maximum number of channels to return') + }), + baseUrl: BASE_URL, + method: 'GET', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { teamId, maxResults = 50 } = params + + if (!teamId) { + throw new Error('Team ID is required to list channels') + } + + try { + const endpoint = `/teams/${teamId}/channels` + const result = await this.makeTeamsRequest(endpoint) + + // Filter results to maxResults on client side since $top is not supported + const channels = result.value || [] + const limitedChannels = channels.slice(0, maxResults) + + const responseData = { + success: true, + channels: limitedChannels, + count: limitedChannels.length, + total: channels.length + } + + return this.formatResponse(responseData, params) + } catch (error) { + return `Error listing channels: ${error}` + } + } +} + +class GetChannelTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'get_channel', + description: 'Get details of a specific channel', + schema: z.object({ + teamId: z.string().describe('ID of the team that contains the channel'), + channelId: z.string().describe('ID of the channel to retrieve') + }), + baseUrl: BASE_URL, + method: 'GET', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { teamId, channelId } = params + + if (!teamId || !channelId) { + throw new Error('Both Team ID and Channel ID are required') + } + + try { + const endpoint = `/teams/${teamId}/channels/${channelId}` + const result = await this.makeTeamsRequest(endpoint) + + return this.formatResponse( + { + success: true, + channel: result + }, + params + ) + } catch (error) { + return this.formatResponse(`Error getting channel: ${error}`, params) + } + } +} + +class CreateChannelTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'create_channel', + description: 'Create a new channel in a team', + schema: z.object({ + teamId: z.string().describe('ID of the team to create the channel in'), + displayName: z.string().describe('Display name of the channel'), + description: z.string().optional().describe('Description of the channel'), + membershipType: z + .enum(['standard', 'private', 'shared']) + .optional() + .default('standard') + .describe('Type of channel membership') + }), + baseUrl: BASE_URL, + method: 'POST', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { teamId, displayName, description, membershipType = 'standard' } = params + + if (!teamId || !displayName) { + throw new Error('Team ID and Display Name are required to create a channel') + } + + try { + const body = { + displayName, + membershipType, + ...(description && { description }) + } + + const endpoint = `/teams/${teamId}/channels` + const result = await this.makeTeamsRequest(endpoint, 'POST', body) + + return this.formatResponse( + { + success: true, + channel: result, + message: `Channel "${displayName}" created successfully` + }, + params + ) + } catch (error) { + return this.formatResponse(`Error creating channel: ${error}`, params) + } + } +} + +class UpdateChannelTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'update_channel', + description: 'Update an existing channel', + schema: z.object({ + teamId: z.string().describe('ID of the team that contains the channel'), + channelId: z.string().describe('ID of the channel to update'), + displayName: z.string().optional().describe('New display name of the channel'), + description: z.string().optional().describe('New description of the channel') + }), + baseUrl: BASE_URL, + method: 'PATCH', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { teamId, channelId, displayName, description } = params + + if (!teamId || !channelId) { + throw new Error('Both Team ID and Channel ID are required') + } + + try { + const body: any = {} + if (displayName) body.displayName = displayName + if (description) body.description = description + + if (Object.keys(body).length === 0) { + throw new Error('At least one field to update must be provided') + } + + const endpoint = `/teams/${teamId}/channels/${channelId}` + await this.makeTeamsRequest(endpoint, 'PATCH', body) + + return this.formatResponse( + { + success: true, + message: 'Channel updated successfully' + }, + params + ) + } catch (error) { + return this.formatResponse(`Error updating channel: ${error}`, params) + } + } +} + +class DeleteChannelTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'delete_channel', + description: 'Delete a channel from a team', + schema: z.object({ + teamId: z.string().describe('ID of the team that contains the channel'), + channelId: z.string().describe('ID of the channel to delete') + }), + baseUrl: BASE_URL, + method: 'DELETE', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { teamId, channelId } = params + + if (!teamId || !channelId) { + throw new Error('Both Team ID and Channel ID are required') + } + + try { + const endpoint = `/teams/${teamId}/channels/${channelId}` + await this.makeTeamsRequest(endpoint, 'DELETE') + + return this.formatResponse( + { + success: true, + message: 'Channel deleted successfully' + }, + params + ) + } catch (error) { + return this.formatResponse(`Error deleting channel: ${error}`, params) + } + } +} + +class ArchiveChannelTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'archive_channel', + description: 'Archive a channel in a team', + schema: z.object({ + teamId: z.string().describe('ID of the team that contains the channel'), + channelId: z.string().describe('ID of the channel to archive') + }), + baseUrl: BASE_URL, + method: 'POST', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { teamId, channelId } = params + + if (!teamId || !channelId) { + throw new Error('Both Team ID and Channel ID are required') + } + + try { + const endpoint = `/teams/${teamId}/channels/${channelId}/archive` + await this.makeTeamsRequest(endpoint, 'POST', {}) + + return this.formatResponse( + { + success: true, + message: 'Channel archived successfully' + }, + params + ) + } catch (error) { + return this.formatResponse(`Error archiving channel: ${error}`, params) + } + } +} + +class UnarchiveChannelTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'unarchive_channel', + description: 'Unarchive a channel in a team', + schema: z.object({ + teamId: z.string().describe('ID of the team that contains the channel'), + channelId: z.string().describe('ID of the channel to unarchive') + }), + baseUrl: BASE_URL, + method: 'POST', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { teamId, channelId } = params + + if (!teamId || !channelId) { + throw new Error('Both Team ID and Channel ID are required') + } + + try { + const endpoint = `/teams/${teamId}/channels/${channelId}/unarchive` + await this.makeTeamsRequest(endpoint, 'POST', {}) + + return this.formatResponse( + { + success: true, + message: 'Channel unarchived successfully' + }, + params + ) + } catch (error) { + return this.formatResponse(`Error unarchiving channel: ${error}`, params) + } + } +} + +class ListChannelMembersTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'list_channel_members', + description: 'List members of a channel', + schema: z.object({ + teamId: z.string().describe('ID of the team that contains the channel'), + channelId: z.string().describe('ID of the channel') + }), + baseUrl: BASE_URL, + method: 'GET', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { teamId, channelId } = params + + if (!teamId || !channelId) { + throw new Error('Both Team ID and Channel ID are required') + } + + try { + const endpoint = `/teams/${teamId}/channels/${channelId}/members` + const result = await this.makeTeamsRequest(endpoint) + + return this.formatResponse( + { + success: true, + members: result.value || [], + count: result.value?.length || 0 + }, + params + ) + } catch (error) { + return this.formatResponse(`Error listing channel members: ${error}`, params) + } + } +} + +class AddChannelMemberTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'add_channel_member', + description: 'Add a member to a channel', + schema: z.object({ + teamId: z.string().describe('ID of the team that contains the channel'), + channelId: z.string().describe('ID of the channel'), + userId: z.string().describe('ID of the user to add') + }), + baseUrl: BASE_URL, + method: 'POST', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { teamId, channelId, userId } = params + + if (!teamId || !channelId || !userId) { + throw new Error('Team ID, Channel ID, and User ID are all required') + } + + try { + const body = { + '@odata.type': '#microsoft.graph.aadUserConversationMember', + 'user@odata.bind': `https://graph.microsoft.com/v1.0/users('${userId}')` + } + + const endpoint = `/teams/${teamId}/channels/${channelId}/members` + await this.makeTeamsRequest(endpoint, 'POST', body) + + return this.formatResponse( + { + success: true, + message: 'Member added to channel successfully' + }, + params + ) + } catch (error) { + return this.formatResponse(`Error adding channel member: ${error}`, params) + } + } +} + +class RemoveChannelMemberTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'remove_channel_member', + description: 'Remove a member from a channel', + schema: z.object({ + teamId: z.string().describe('ID of the team that contains the channel'), + channelId: z.string().describe('ID of the channel'), + userId: z.string().describe('ID of the user to remove') + }), + baseUrl: BASE_URL, + method: 'DELETE', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { teamId, channelId, userId } = params + + if (!teamId || !channelId || !userId) { + throw new Error('Team ID, Channel ID, and User ID are all required') + } + + try { + // First get the membership ID + const membersEndpoint = `/teams/${teamId}/channels/${channelId}/members` + const membersResult = await this.makeTeamsRequest(membersEndpoint) + + const member = membersResult.value?.find((m: any) => m.userId === userId) + if (!member) { + throw new Error('User is not a member of this channel') + } + + const endpoint = `/teams/${teamId}/channels/${channelId}/members/${member.id}` + await this.makeTeamsRequest(endpoint, 'DELETE') + + return this.formatResponse( + { + success: true, + message: 'Member removed from channel successfully' + }, + params + ) + } catch (error) { + return this.formatResponse(`Error removing channel member: ${error}`, params) + } + } +} + +// CHAT TOOLS + +class ListChatsTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'list_chats', + description: 'List all chats for the current user', + schema: z.object({ + maxResults: z.number().optional().default(50).describe('Maximum number of chats to return') + }), + baseUrl: BASE_URL, + method: 'GET', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { maxResults = 50 } = params + + try { + const endpoint = `/me/chats?$top=${maxResults}` + const result = await this.makeTeamsRequest(endpoint) + + return this.formatResponse( + { + success: true, + chats: result.value || [], + count: result.value?.length || 0 + }, + params + ) + } catch (error) { + return this.formatResponse(`Error listing chats: ${error}`, params) + } + } +} + +class GetChatTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'get_chat', + description: 'Get details of a specific chat', + schema: z.object({ + chatId: z.string().describe('ID of the chat to retrieve') + }), + baseUrl: BASE_URL, + method: 'GET', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { chatId } = params + + if (!chatId) { + throw new Error('Chat ID is required') + } + + try { + const endpoint = `/chats/${chatId}` + const result = await this.makeTeamsRequest(endpoint) + + return this.formatResponse( + { + success: true, + chat: result + }, + params + ) + } catch (error) { + return this.formatResponse(`Error getting chat: ${error}`, params) + } + } +} + +class CreateChatTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'create_chat', + description: 'Create a new chat', + schema: z.object({ + chatType: z.enum(['oneOnOne', 'group']).optional().default('group').describe('Type of chat to create'), + topic: z.string().optional().describe('Topic/subject of the chat (for group chats)'), + members: z.string().describe('Comma-separated list of user IDs to add to the chat') + }), + baseUrl: BASE_URL, + method: 'POST', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { chatType = 'group', topic, members } = params + + if (!members) { + throw new Error('Members list is required to create a chat') + } + + try { + const memberIds = members.split(',').map((id: string) => id.trim()) + const chatMembers = memberIds.map((userId: string) => ({ + '@odata.type': '#microsoft.graph.aadUserConversationMember', + 'user@odata.bind': `https://graph.microsoft.com/v1.0/users('${userId}')` + })) + + const body: any = { + chatType, + members: chatMembers + } + + if (topic && chatType === 'group') { + body.topic = topic + } + + const endpoint = '/chats' + const result = await this.makeTeamsRequest(endpoint, 'POST', body) + + return this.formatResponse( + { + success: true, + chat: result, + message: 'Chat created successfully' + }, + params + ) + } catch (error) { + return this.formatResponse(`Error creating chat: ${error}`, params) + } + } +} + +class UpdateChatTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'update_chat', + description: 'Update an existing chat', + schema: z.object({ + chatId: z.string().describe('ID of the chat to update'), + topic: z.string().describe('New topic/subject of the chat') + }), + baseUrl: BASE_URL, + method: 'PATCH', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { chatId, topic } = params + + if (!chatId) { + throw new Error('Chat ID is required') + } + + if (!topic) { + throw new Error('Topic is required to update a chat') + } + + try { + const body = { topic } + const endpoint = `/chats/${chatId}` + await this.makeTeamsRequest(endpoint, 'PATCH', body) + + return this.formatResponse( + { + success: true, + message: 'Chat updated successfully' + }, + params + ) + } catch (error) { + return this.formatResponse(`Error updating chat: ${error}`, params) + } + } +} + +class DeleteChatTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'delete_chat', + description: 'Delete a chat', + schema: z.object({ + chatId: z.string().describe('ID of the chat to delete') + }), + baseUrl: BASE_URL, + method: 'DELETE', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { chatId } = params + + if (!chatId) { + throw new Error('Chat ID is required') + } + + try { + const endpoint = `/chats/${chatId}` + await this.makeTeamsRequest(endpoint, 'DELETE') + + return this.formatResponse( + { + success: true, + message: 'Chat deleted successfully' + }, + params + ) + } catch (error) { + return this.formatResponse(`Error deleting chat: ${error}`, params) + } + } +} + +class ListChatMembersTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'list_chat_members', + description: 'List members of a chat', + schema: z.object({ + chatId: z.string().describe('ID of the chat') + }), + baseUrl: BASE_URL, + method: 'GET', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { chatId } = params + + if (!chatId) { + throw new Error('Chat ID is required') + } + + try { + const endpoint = `/chats/${chatId}/members` + const result = await this.makeTeamsRequest(endpoint) + + return this.formatResponse( + { + success: true, + members: result.value || [], + count: result.value?.length || 0 + }, + params + ) + } catch (error) { + return this.formatResponse(`Error listing chat members: ${error}`, params) + } + } +} + +class AddChatMemberTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'add_chat_member', + description: 'Add a member to a chat', + schema: z.object({ + chatId: z.string().describe('ID of the chat'), + userId: z.string().describe('ID of the user to add') + }), + baseUrl: BASE_URL, + method: 'POST', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { chatId, userId } = params + + if (!chatId || !userId) { + throw new Error('Both Chat ID and User ID are required') + } + + try { + const body = { + '@odata.type': '#microsoft.graph.aadUserConversationMember', + 'user@odata.bind': `https://graph.microsoft.com/v1.0/users('${userId}')` + } + + const endpoint = `/chats/${chatId}/members` + await this.makeTeamsRequest(endpoint, 'POST', body) + + return this.formatResponse( + { + success: true, + message: 'Member added to chat successfully' + }, + params + ) + } catch (error) { + return this.formatResponse(`Error adding chat member: ${error}`, params) + } + } +} + +class RemoveChatMemberTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'remove_chat_member', + description: 'Remove a member from a chat', + schema: z.object({ + chatId: z.string().describe('ID of the chat'), + userId: z.string().describe('ID of the user to remove') + }), + baseUrl: BASE_URL, + method: 'DELETE', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { chatId, userId } = params + + if (!chatId || !userId) { + throw new Error('Both Chat ID and User ID are required') + } + + try { + // First get the membership ID + const membersEndpoint = `/chats/${chatId}/members` + const membersResult = await this.makeTeamsRequest(membersEndpoint) + + const member = membersResult.value?.find((m: any) => m.userId === userId) + if (!member) { + throw new Error('User is not a member of this chat') + } + + const endpoint = `/chats/${chatId}/members/${member.id}` + await this.makeTeamsRequest(endpoint, 'DELETE') + + return this.formatResponse( + { + success: true, + message: 'Member removed from chat successfully' + }, + params + ) + } catch (error) { + return this.formatResponse(`Error removing chat member: ${error}`, params) + } + } +} + +class PinMessageTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'pin_message', + description: 'Pin a message in a chat', + schema: z.object({ + chatId: z.string().describe('ID of the chat'), + messageId: z.string().describe('ID of the message to pin') + }), + baseUrl: BASE_URL, + method: 'POST', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { chatId, messageId } = params + + if (!chatId || !messageId) { + throw new Error('Both Chat ID and Message ID are required') + } + + try { + const body = { + message: { + '@odata.bind': `https://graph.microsoft.com/v1.0/chats('${chatId}')/messages('${messageId}')` + } + } + + const endpoint = `/chats/${chatId}/pinnedMessages` + await this.makeTeamsRequest(endpoint, 'POST', body) + + return this.formatResponse( + { + success: true, + message: 'Message pinned successfully' + }, + params + ) + } catch (error) { + return this.formatResponse(`Error pinning message: ${error}`, params) + } + } +} + +class UnpinMessageTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'unpin_message', + description: 'Unpin a message from a chat', + schema: z.object({ + chatId: z.string().describe('ID of the chat'), + messageId: z.string().describe('ID of the message to unpin') + }), + baseUrl: BASE_URL, + method: 'DELETE', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { chatId, messageId } = params + + if (!chatId || !messageId) { + throw new Error('Both Chat ID and Message ID are required') + } + + try { + // First get the pinned messages to find the pinned message ID + const pinnedEndpoint = `/chats/${chatId}/pinnedMessages` + const pinnedResult = await this.makeTeamsRequest(pinnedEndpoint) + + const pinnedMessage = pinnedResult.value?.find((pm: any) => pm.message?.id === messageId) + if (!pinnedMessage) { + throw new Error('Message is not pinned in this chat') + } + + const endpoint = `/chats/${chatId}/pinnedMessages/${pinnedMessage.id}` + await this.makeTeamsRequest(endpoint, 'DELETE') + + return this.formatResponse( + { + success: true, + message: 'Message unpinned successfully' + }, + params + ) + } catch (error) { + return this.formatResponse(`Error unpinning message: ${error}`, params) + } + } +} + +// CHAT MESSAGE TOOLS + +class ListMessagesTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'list_messages', + description: 'List messages in a chat or channel', + schema: z.object({ + chatChannelId: z.string().describe('ID of the chat or channel to list messages from'), + teamId: z.string().optional().describe('ID of the team (required for channel messages)'), + maxResults: z.number().optional().default(50).describe('Maximum number of messages to return') + }), + baseUrl: BASE_URL, + method: 'GET', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { chatChannelId, teamId, maxResults = 50 } = params + + if (!chatChannelId) { + throw new Error('Chat or Channel ID is required') + } + + try { + let endpoint: string + if (teamId) { + // Channel messages + endpoint = `/teams/${teamId}/channels/${chatChannelId}/messages?$top=${maxResults}` + } else { + // Chat messages + endpoint = `/chats/${chatChannelId}/messages?$top=${maxResults}` + } + + const result = await this.makeTeamsRequest(endpoint) + + return this.formatResponse( + { + success: true, + messages: result.value || [], + count: result.value?.length || 0, + context: teamId ? 'channel' : 'chat' + }, + params + ) + } catch (error) { + return this.formatResponse(`Error listing messages: ${error}`, params) + } + } +} + +class GetMessageTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'get_message', + description: 'Get details of a specific message', + schema: z.object({ + chatChannelId: z.string().describe('ID of the chat or channel'), + teamId: z.string().optional().describe('ID of the team (required for channel messages)'), + messageId: z.string().describe('ID of the message to retrieve') + }), + baseUrl: BASE_URL, + method: 'GET', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { chatChannelId, teamId, messageId } = params + + if (!chatChannelId || !messageId) { + throw new Error('Chat/Channel ID and Message ID are required') + } + + try { + let endpoint: string + if (teamId) { + // Channel message + endpoint = `/teams/${teamId}/channels/${chatChannelId}/messages/${messageId}` + } else { + // Chat message + endpoint = `/chats/${chatChannelId}/messages/${messageId}` + } + + const result = await this.makeTeamsRequest(endpoint) + + return this.formatResponse( + { + success: true, + message: result, + context: teamId ? 'channel' : 'chat' + }, + params + ) + } catch (error) { + return this.formatResponse(`Error getting message: ${error}`, params) + } + } +} + +class SendMessageTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'send_message', + description: 'Send a message to a chat or channel', + schema: z.object({ + chatChannelId: z.string().describe('ID of the chat or channel to send message to'), + teamId: z.string().optional().describe('ID of the team (required for channel messages)'), + messageBody: z.string().describe('Content of the message'), + contentType: z.enum(['text', 'html']).optional().default('text').describe('Content type of the message') + }), + baseUrl: BASE_URL, + method: 'POST', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { chatChannelId, teamId, messageBody, contentType = 'text' } = params + + if (!chatChannelId || !messageBody) { + throw new Error('Chat/Channel ID and Message Body are required') + } + + try { + const body = { + body: { + contentType, + content: messageBody + } + } + + let endpoint: string + if (teamId) { + // Channel message + endpoint = `/teams/${teamId}/channels/${chatChannelId}/messages` + } else { + // Chat message + endpoint = `/chats/${chatChannelId}/messages` + } + + const result = await this.makeTeamsRequest(endpoint, 'POST', body) + + return this.formatResponse( + { + success: true, + message: result, + context: teamId ? 'channel' : 'chat', + messageText: 'Message sent successfully' + }, + params + ) + } catch (error) { + return this.formatResponse(`Error sending message: ${error}`, params) + } + } +} + +class UpdateMessageTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'update_message', + description: 'Update an existing message', + schema: z.object({ + chatChannelId: z.string().describe('ID of the chat or channel'), + teamId: z.string().optional().describe('ID of the team (required for channel messages)'), + messageId: z.string().describe('ID of the message to update') + }), + baseUrl: BASE_URL, + method: 'PATCH', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { chatChannelId, teamId, messageId } = params + + if (!chatChannelId || !messageId) { + throw new Error('Chat/Channel ID and Message ID are required') + } + + try { + // Note: Message update is primarily for policy violations in Teams + const body = { + policyViolation: null + } + + let endpoint: string + if (teamId) { + // Channel message + endpoint = `/teams/${teamId}/channels/${chatChannelId}/messages/${messageId}` + } else { + // Chat message + endpoint = `/chats/${chatChannelId}/messages/${messageId}` + } + + await this.makeTeamsRequest(endpoint, 'PATCH', body) + + return this.formatResponse( + { + success: true, + message: 'Message updated successfully', + context: teamId ? 'channel' : 'chat' + }, + params + ) + } catch (error) { + return this.formatResponse(`Error updating message: ${error}`, params) + } + } +} + +class DeleteMessageTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'delete_message', + description: 'Delete a message', + schema: z.object({ + chatChannelId: z.string().describe('ID of the chat or channel'), + teamId: z.string().optional().describe('ID of the team (required for channel messages)'), + messageId: z.string().describe('ID of the message to delete') + }), + baseUrl: BASE_URL, + method: 'DELETE', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { chatChannelId, teamId, messageId } = params + + if (!chatChannelId || !messageId) { + throw new Error('Chat/Channel ID and Message ID are required') + } + + try { + let endpoint: string + if (teamId) { + // Channel message - use soft delete + endpoint = `/teams/${teamId}/channels/${chatChannelId}/messages/${messageId}/softDelete` + } else { + // Chat message - use soft delete + endpoint = `/chats/${chatChannelId}/messages/${messageId}/softDelete` + } + + await this.makeTeamsRequest(endpoint, 'POST', {}) + + return this.formatResponse( + { + success: true, + message: 'Message deleted successfully', + context: teamId ? 'channel' : 'chat' + }, + params + ) + } catch (error) { + return this.formatResponse(`Error deleting message: ${error}`, params) + } + } +} + +class ReplyToMessageTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'reply_to_message', + description: 'Reply to a message in a chat or channel', + schema: z.object({ + chatChannelId: z.string().describe('ID of the chat or channel'), + teamId: z.string().optional().describe('ID of the team (required for channel messages)'), + messageId: z.string().describe('ID of the message to reply to'), + replyBody: z.string().describe('Content of the reply'), + contentType: z.enum(['text', 'html']).optional().default('text').describe('Content type of the reply') + }), + baseUrl: BASE_URL, + method: 'POST', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { chatChannelId, teamId, messageId, replyBody, contentType = 'text' } = params + + if (!chatChannelId || !messageId || !replyBody) { + throw new Error('Chat/Channel ID, Message ID, and Reply Body are required') + } + + try { + const body = { + body: { + contentType, + content: replyBody + } + } + + let endpoint: string + if (teamId) { + // Channel message reply + endpoint = `/teams/${teamId}/channels/${chatChannelId}/messages/${messageId}/replies` + } else { + // For chat messages, replies are just new messages + endpoint = `/chats/${chatChannelId}/messages` + } + + const result = await this.makeTeamsRequest(endpoint, 'POST', body) + + return this.formatResponse( + { + success: true, + reply: result, + message: 'Reply sent successfully', + context: teamId ? 'channel' : 'chat' + }, + params + ) + } catch (error) { + return this.formatResponse(`Error replying to message: ${error}`, params) + } + } +} + +class SetReactionTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'set_reaction', + description: 'Set a reaction to a message', + schema: z.object({ + chatChannelId: z.string().describe('ID of the chat or channel'), + teamId: z.string().optional().describe('ID of the team (required for channel messages)'), + messageId: z.string().describe('ID of the message to react to'), + reactionType: z + .enum(['like', 'heart', 'laugh', 'surprised', 'sad', 'angry']) + .optional() + .default('like') + .describe('Type of reaction to set') + }), + baseUrl: BASE_URL, + method: 'POST', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { chatChannelId, teamId, messageId, reactionType = 'like' } = params + + if (!chatChannelId || !messageId) { + throw new Error('Chat/Channel ID and Message ID are required') + } + + try { + let endpoint: string + if (teamId) { + // Channel message + endpoint = `/teams/${teamId}/channels/${chatChannelId}/messages/${messageId}/setReaction` + } else { + // Chat message + endpoint = `/chats/${chatChannelId}/messages/${messageId}/setReaction` + } + + const body = { + reactionType + } + + await this.makeTeamsRequest(endpoint, 'POST', body) + + return this.formatResponse( + { + success: true, + message: `Reaction "${reactionType}" set successfully`, + context: teamId ? 'channel' : 'chat' + }, + params + ) + } catch (error) { + return this.formatResponse(`Error setting reaction: ${error}`, params) + } + } +} + +class UnsetReactionTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'unset_reaction', + description: 'Remove a reaction from a message', + schema: z.object({ + chatChannelId: z.string().describe('ID of the chat or channel'), + teamId: z.string().optional().describe('ID of the team (required for channel messages)'), + messageId: z.string().describe('ID of the message to remove reaction from'), + reactionType: z + .enum(['like', 'heart', 'laugh', 'surprised', 'sad', 'angry']) + .optional() + .default('like') + .describe('Type of reaction to remove') + }), + baseUrl: BASE_URL, + method: 'POST', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { chatChannelId, teamId, messageId, reactionType = 'like' } = params + + if (!chatChannelId || !messageId) { + throw new Error('Chat/Channel ID and Message ID are required') + } + + try { + let endpoint: string + if (teamId) { + // Channel message + endpoint = `/teams/${teamId}/channels/${chatChannelId}/messages/${messageId}/unsetReaction` + } else { + // Chat message + endpoint = `/chats/${chatChannelId}/messages/${messageId}/unsetReaction` + } + + const body = { + reactionType + } + + await this.makeTeamsRequest(endpoint, 'POST', body) + + return this.formatResponse( + { + success: true, + message: `Reaction "${reactionType}" removed successfully`, + context: teamId ? 'channel' : 'chat' + }, + params + ) + } catch (error) { + return this.formatResponse(`Error unsetting reaction: ${error}`, params) + } + } +} + +class GetAllMessagesTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'get_all_messages', + description: 'Get messages across all chats and channels for the user', + schema: z.object({ + maxResults: z.number().optional().default(50).describe('Maximum number of messages to return') + }), + baseUrl: BASE_URL, + method: 'GET', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { maxResults = 50 } = params + + try { + // Get messages from all chats + const chatEndpoint = `/me/chats/getAllMessages?$top=${maxResults}` + const chatResult = await this.makeTeamsRequest(chatEndpoint) + + return this.formatResponse( + { + success: true, + messages: chatResult.value || [], + count: chatResult.value?.length || 0, + source: 'all_chats_and_channels' + }, + params + ) + } catch (error) { + return this.formatResponse(`Error getting all messages: ${error}`, params) + } + } +} + +// Main function to create Teams tools +export function createTeamsTools(options: TeamsToolOptions): DynamicStructuredTool[] { + const tools: DynamicStructuredTool[] = [] + const actions = options.actions || [] + const accessToken = options.accessToken || '' + const defaultParams = options.defaultParams || {} + + // Channel tools + if (actions.includes('listChannels')) { + const listTool = new ListChannelsTool({ + accessToken, + defaultParams: defaultParams.listChannels + }) + tools.push(listTool) + } + + if (actions.includes('getChannel')) { + const getTool = new GetChannelTool({ + accessToken, + defaultParams: defaultParams.getChannel + }) + tools.push(getTool) + } + + if (actions.includes('createChannel')) { + const createTool = new CreateChannelTool({ + accessToken, + defaultParams: defaultParams.createChannel + }) + tools.push(createTool) + } + + if (actions.includes('updateChannel')) { + const updateTool = new UpdateChannelTool({ + accessToken, + defaultParams: defaultParams.updateChannel + }) + tools.push(updateTool) + } + + if (actions.includes('deleteChannel')) { + const deleteTool = new DeleteChannelTool({ + accessToken, + defaultParams: defaultParams.deleteChannel + }) + tools.push(deleteTool) + } + + if (actions.includes('archiveChannel')) { + const archiveTool = new ArchiveChannelTool({ + accessToken, + defaultParams: defaultParams.archiveChannel + }) + tools.push(archiveTool) + } + + if (actions.includes('unarchiveChannel')) { + const unarchiveTool = new UnarchiveChannelTool({ + accessToken, + defaultParams: defaultParams.unarchiveChannel + }) + tools.push(unarchiveTool) + } + + if (actions.includes('listChannelMembers')) { + const listMembersTool = new ListChannelMembersTool({ + accessToken, + defaultParams: defaultParams.listChannelMembers + }) + tools.push(listMembersTool) + } + + if (actions.includes('addChannelMember')) { + const addMemberTool = new AddChannelMemberTool({ + accessToken, + defaultParams: defaultParams.addChannelMember + }) + tools.push(addMemberTool) + } + + if (actions.includes('removeChannelMember')) { + const removeMemberTool = new RemoveChannelMemberTool({ + accessToken, + defaultParams: defaultParams.removeChannelMember + }) + tools.push(removeMemberTool) + } + + // Chat tools + if (actions.includes('listChats')) { + const listTool = new ListChatsTool({ + accessToken, + defaultParams: defaultParams.listChats + }) + tools.push(listTool) + } + + if (actions.includes('getChat')) { + const getTool = new GetChatTool({ + accessToken, + defaultParams: defaultParams.getChat + }) + tools.push(getTool) + } + + if (actions.includes('createChat')) { + const createTool = new CreateChatTool({ + accessToken, + defaultParams: defaultParams.createChat + }) + tools.push(createTool) + } + + if (actions.includes('updateChat')) { + const updateTool = new UpdateChatTool({ + accessToken, + defaultParams: defaultParams.updateChat + }) + tools.push(updateTool) + } + + if (actions.includes('deleteChat')) { + const deleteTool = new DeleteChatTool({ + accessToken, + defaultParams: defaultParams.deleteChat + }) + tools.push(deleteTool) + } + + if (actions.includes('listChatMembers')) { + const listMembersTool = new ListChatMembersTool({ + accessToken, + defaultParams: defaultParams.listChatMembers + }) + tools.push(listMembersTool) + } + + if (actions.includes('addChatMember')) { + const addMemberTool = new AddChatMemberTool({ + accessToken, + defaultParams: defaultParams.addChatMember + }) + tools.push(addMemberTool) + } + + if (actions.includes('removeChatMember')) { + const removeMemberTool = new RemoveChatMemberTool({ + accessToken, + defaultParams: defaultParams.removeChatMember + }) + tools.push(removeMemberTool) + } + + if (actions.includes('pinMessage')) { + const pinTool = new PinMessageTool({ + accessToken, + defaultParams: defaultParams.pinMessage + }) + tools.push(pinTool) + } + + if (actions.includes('unpinMessage')) { + const unpinTool = new UnpinMessageTool({ + accessToken, + defaultParams: defaultParams.unpinMessage + }) + tools.push(unpinTool) + } + + // Chat message tools + if (actions.includes('listMessages')) { + const listTool = new ListMessagesTool({ + accessToken, + defaultParams: defaultParams.listMessages + }) + tools.push(listTool) + } + + if (actions.includes('getMessage')) { + const getTool = new GetMessageTool({ + accessToken, + defaultParams: defaultParams.getMessage + }) + tools.push(getTool) + } + + if (actions.includes('sendMessage')) { + const sendTool = new SendMessageTool({ + accessToken, + defaultParams: defaultParams.sendMessage + }) + tools.push(sendTool) + } + + if (actions.includes('updateMessage')) { + const updateTool = new UpdateMessageTool({ + accessToken, + defaultParams: defaultParams.updateMessage + }) + tools.push(updateTool) + } + + if (actions.includes('deleteMessage')) { + const deleteTool = new DeleteMessageTool({ + accessToken, + defaultParams: defaultParams.deleteMessage + }) + tools.push(deleteTool) + } + + if (actions.includes('replyToMessage')) { + const replyTool = new ReplyToMessageTool({ + accessToken, + defaultParams: defaultParams.replyToMessage + }) + tools.push(replyTool) + } + + if (actions.includes('setReaction')) { + const reactionTool = new SetReactionTool({ + accessToken, + defaultParams: defaultParams.setReaction + }) + tools.push(reactionTool) + } + + if (actions.includes('unsetReaction')) { + const unsetReactionTool = new UnsetReactionTool({ + accessToken, + defaultParams: defaultParams.unsetReaction + }) + tools.push(unsetReactionTool) + } + + if (actions.includes('getAllMessages')) { + const getAllTool = new GetAllMessagesTool({ + accessToken, + defaultParams: defaultParams.getAllMessages + }) + tools.push(getAllTool) + } + + return tools +} diff --git a/packages/components/nodes/tools/MicrosoftTeams/teams.svg b/packages/components/nodes/tools/MicrosoftTeams/teams.svg new file mode 100644 index 00000000000..f3a03a3bedf --- /dev/null +++ b/packages/components/nodes/tools/MicrosoftTeams/teams.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/vectorstores/Weaviate/Weaviate.ts b/packages/components/nodes/vectorstores/Weaviate/Weaviate.ts index ae2c0164f3b..5d83eaa9a6a 100644 --- a/packages/components/nodes/vectorstores/Weaviate/Weaviate.ts +++ b/packages/components/nodes/vectorstores/Weaviate/Weaviate.ts @@ -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 } 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' @@ -175,7 +175,11 @@ class Weaviate_VectorStores implements INode { const finalDocs = [] for (let i = 0; i < flattenDocs.length; i += 1) { if (flattenDocs[i] && flattenDocs[i].pageContent) { - finalDocs.push(new Document(flattenDocs[i])) + const doc = { ...flattenDocs[i] } + if (doc.metadata) { + doc.metadata = normalizeKeysRecursively(doc.metadata) + } + finalDocs.push(new Document(doc)) } } diff --git a/packages/components/package.json b/packages/components/package.json index ba6c5747b3e..4fe93d1b9ea 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "flowise-components", - "version": "3.0.1", + "version": "3.0.2", "description": "Flowiseai Components", "main": "dist/src/index", "types": "dist/src/index.d.ts", @@ -120,6 +120,7 @@ "node-html-markdown": "^1.3.0", "notion-to-md": "^3.1.1", "object-hash": "^3.0.0", + "officeparser": "5.1.1", "ollama": "^0.5.11", "openai": "^4.96.0", "papaparse": "^5.4.1", @@ -138,6 +139,7 @@ "weaviate-ts-client": "^1.1.0", "winston": "^3.9.0", "ws": "^8.18.0", + "xlsx": "0.18.5", "zod": "3.22.4", "zod-to-json-schema": "^3.21.4" }, diff --git a/packages/components/src/agentflowv2Generator.ts b/packages/components/src/agentflowv2Generator.ts index c5765f5211e..a9e4600d1e1 100644 --- a/packages/components/src/agentflowv2Generator.ts +++ b/packages/components/src/agentflowv2Generator.ts @@ -76,10 +76,10 @@ interface AgentToolConfig { interface NodeInputs { agentTools?: AgentToolConfig[] - selectedTool?: string + toolAgentflowSelectedTool?: string toolInputArgs?: Record[] - selectedToolConfig?: { - selectedTool: string + toolAgentflowSelectedToolConfig?: { + toolAgentflowSelectedTool: string } [key: string]: any } @@ -284,10 +284,10 @@ Now, select the ONLY tool that is needed to achieve the given task. You must onl if (Array.isArray(tools) && tools.length > 0) { selectedTools.push(...tools) - node.data.inputs.selectedTool = tools[0] + node.data.inputs.toolAgentflowSelectedTool = tools[0] node.data.inputs.toolInputArgs = [] - node.data.inputs.selectedToolConfig = { - selectedTool: tools[0] + node.data.inputs.toolAgentflowSelectedToolConfig = { + toolAgentflowSelectedTool: tools[0] } } } @@ -585,42 +585,87 @@ const _showHideOperation = (nodeData: Record, inputParam: Record groundValue.includes(val)) + if (displayType === 'show' && !hasIntersection) { + inputParam.display = false + } + if (displayType === 'hide' && hasIntersection) { + inputParam.display = false + } + } else if (typeof comparisonValue === 'string') { + // comparisonValue is string, groundValue is array - check if array contains the string + const matchFound = groundValue.some((val) => comparisonValue === val || new RegExp(comparisonValue).test(val)) + if (displayType === 'show' && !matchFound) { + inputParam.display = false + } + if (displayType === 'hide' && matchFound) { + inputParam.display = false + } + } else if (typeof comparisonValue === 'boolean' || typeof comparisonValue === 'number') { + // For boolean/number comparison with array, check if array contains the value + const matchFound = groundValue.includes(comparisonValue) + if (displayType === 'show' && !matchFound) { + inputParam.display = false + } + if (displayType === 'hide' && matchFound) { + inputParam.display = false + } + } else if (typeof comparisonValue === 'object') { + // For object comparison with array, use deep equality check + const matchFound = groundValue.some((val) => isEqual(comparisonValue, val)) + if (displayType === 'show' && !matchFound) { + inputParam.display = false + } + if (displayType === 'hide' && matchFound) { + inputParam.display = false + } } - if (displayType === 'hide' && comparisonValue === groundValue) { - inputParam.display = false + } else { + // Original logic for non-array groundValue + if (Array.isArray(comparisonValue)) { + if (displayType === 'show' && !comparisonValue.includes(groundValue)) { + inputParam.display = false + } + if (displayType === 'hide' && comparisonValue.includes(groundValue)) { + inputParam.display = false + } + } else if (typeof comparisonValue === 'string') { + if (displayType === 'show' && !(comparisonValue === groundValue || new RegExp(comparisonValue).test(groundValue))) { + inputParam.display = false + } + if (displayType === 'hide' && (comparisonValue === groundValue || new RegExp(comparisonValue).test(groundValue))) { + inputParam.display = false + } + } else if (typeof comparisonValue === 'boolean') { + if (displayType === 'show' && comparisonValue !== groundValue) { + inputParam.display = false + } + if (displayType === 'hide' && comparisonValue === groundValue) { + inputParam.display = false + } + } else if (typeof comparisonValue === 'object') { + if (displayType === 'show' && !isEqual(comparisonValue, groundValue)) { + inputParam.display = false + } + if (displayType === 'hide' && isEqual(comparisonValue, groundValue)) { + inputParam.display = false + } + } else if (typeof comparisonValue === 'number') { + if (displayType === 'show' && comparisonValue !== groundValue) { + inputParam.display = false + } + if (displayType === 'hide' && comparisonValue === groundValue) { + inputParam.display = false + } } } }) diff --git a/packages/components/src/agents.ts b/packages/components/src/agents.ts index 0bda4021ceb..022a5be09f6 100644 --- a/packages/components/src/agents.ts +++ b/packages/components/src/agents.ts @@ -28,6 +28,7 @@ import { getErrorMessage } from './error' export const SOURCE_DOCUMENTS_PREFIX = '\n\n----FLOWISE_SOURCE_DOCUMENTS----\n\n' export const ARTIFACTS_PREFIX = '\n\n----FLOWISE_ARTIFACTS----\n\n' +export const TOOL_ARGS_PREFIX = '\n\n----FLOWISE_TOOL_ARGS----\n\n' export type AgentFinish = { returnValues: Record @@ -444,9 +445,19 @@ export class AgentExecutor extends BaseChain { if (typeof toolOutput === 'string' && toolOutput.includes(ARTIFACTS_PREFIX)) { toolOutput = toolOutput.split(ARTIFACTS_PREFIX)[0] } + let toolInput + if (typeof toolOutput === 'string' && toolOutput.includes(TOOL_ARGS_PREFIX)) { + const splitArray = toolOutput.split(TOOL_ARGS_PREFIX) + toolOutput = splitArray[0] + try { + toolInput = JSON.parse(splitArray[1]) + } catch (e) { + console.error('Error parsing tool input from tool') + } + } usedTools.push({ tool: tool.name, - toolInput: action.toolInput as any, + toolInput: toolInput ?? (action.toolInput as any), toolOutput }) } else { @@ -502,6 +513,10 @@ export class AgentExecutor extends BaseChain { console.error('Error parsing source documents from tool') } } + if (typeof observation === 'string' && observation.includes(TOOL_ARGS_PREFIX)) { + const observationArray = observation.split(TOOL_ARGS_PREFIX) + observation = observationArray[0] + } return { action, observation: observation ?? '' } }) ) @@ -610,6 +625,10 @@ export class AgentExecutor extends BaseChain { const observationArray = observation.split(ARTIFACTS_PREFIX) observation = observationArray[0] } + if (typeof observation === 'string' && observation.includes(TOOL_ARGS_PREFIX)) { + const observationArray = observation.split(TOOL_ARGS_PREFIX) + observation = observationArray[0] + } } catch (e) { if (e instanceof ToolInputParsingException) { if (this.handleParsingErrors === true) { diff --git a/packages/components/src/utils.ts b/packages/components/src/utils.ts index 1e700839d5b..5a62f93c20a 100644 --- a/packages/components/src/utils.ts +++ b/packages/components/src/utils.ts @@ -1215,3 +1215,97 @@ export const handleDocumentLoaderDocuments = async (loader: DocumentLoader, text return docs } + +/** + * Normalize special characters in key to be used in vector store + * @param str - Key to normalize + * @returns Normalized key + */ +export const normalizeSpecialChars = (str: string) => { + return str.replace(/[^a-zA-Z0-9_]/g, '_') +} + +/** + * recursively normalize object keys + * @param data - Object to normalize + * @returns Normalized object + */ +export const normalizeKeysRecursively = (data: any): any => { + if (Array.isArray(data)) { + return data.map(normalizeKeysRecursively) + } + + if (data !== null && typeof data === 'object') { + return Object.entries(data).reduce((acc, [key, value]) => { + const newKey = normalizeSpecialChars(key) + acc[newKey] = normalizeKeysRecursively(value) + return acc + }, {} as Record) + } + return data +} + +/** + * Check if OAuth2 token is expired and refresh if needed + * @param {string} credentialId + * @param {ICommonObject} credentialData + * @param {ICommonObject} options + * @param {number} bufferTimeMs - Buffer time in milliseconds before expiry (default: 5 minutes) + * @returns {Promise} + */ +export const refreshOAuth2Token = async ( + credentialId: string, + credentialData: ICommonObject, + options: ICommonObject, + bufferTimeMs: number = 5 * 60 * 1000 +): Promise => { + // Check if token is expired and refresh if needed + if (credentialData.expires_at) { + const expiryTime = new Date(credentialData.expires_at) + const currentTime = new Date() + + if (currentTime.getTime() > expiryTime.getTime() - bufferTimeMs) { + if (!credentialData.refresh_token) { + throw new Error('Access token is expired and no refresh token is available. Please re-authorize the credential.') + } + + try { + // Import fetch dynamically to avoid issues + const fetch = (await import('node-fetch')).default + + // Call the refresh API endpoint + const refreshResponse = await fetch( + `${options.baseURL || 'http://localhost:3000'}/api/v1/oauth2-credential/refresh/${credentialId}`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + } + ) + + if (!refreshResponse.ok) { + const errorData = await refreshResponse.text() + throw new Error(`Failed to refresh token: ${refreshResponse.status} ${refreshResponse.statusText} - ${errorData}`) + } + + await refreshResponse.json() + + // Get the updated credential data + const updatedCredentialData = await getCredentialData(credentialId, options) + + return updatedCredentialData + } catch (error) { + console.error('Failed to refresh access token:', error) + throw new Error( + `Failed to refresh access token: ${ + error instanceof Error ? error.message : 'Unknown error' + }. Please re-authorize the credential.` + ) + } + } + } + + // Token is not expired, return original data + return credentialData +} diff --git a/packages/server/.env.example b/packages/server/.env.example index ea5cb01f90f..f38d6fa2889 100644 --- a/packages/server/.env.example +++ b/packages/server/.env.example @@ -89,12 +89,12 @@ PORT=3000 # ALLOW_UNAUTHORIZED_CERTS=false # SENDER_EMAIL=team@example.com -# JWT_AUTH_TOKEN_SECRET='AABBCCDDAABBCCDDAABBCCDDAABBCCDDAABBCCDD' -# JWT_REFRESH_TOKEN_SECRET='AABBCCDDAABBCCDDAABBCCDDAABBCCDDAABBCCDD' -# JWT_ISSUER='ISSUER' -# JWT_AUDIENCE='AUDIENCE' -# JWT_TOKEN_EXPIRY_IN_MINUTES=360 -# JWT_REFRESH_TOKEN_EXPIRY_IN_MINUTES=43200 +JWT_AUTH_TOKEN_SECRET='AABBCCDDAABBCCDDAABBCCDDAABBCCDDAABBCCDD' +JWT_REFRESH_TOKEN_SECRET='AABBCCDDAABBCCDDAABBCCDDAABBCCDDAABBCCDD' +JWT_ISSUER='ISSUER' +JWT_AUDIENCE='AUDIENCE' +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 diff --git a/packages/server/marketplaces/agentflowsv2/Slack Agent.json b/packages/server/marketplaces/agentflowsv2/Slack Agent.json index ec95fb4b9fc..a36876f8b31 100644 --- a/packages/server/marketplaces/agentflowsv2/Slack Agent.json +++ b/packages/server/marketplaces/agentflowsv2/Slack Agent.json @@ -526,7 +526,7 @@ "data": { "id": "toolAgentflow_0", "label": "Slack Reply", - "version": 1, + "version": 1.1, "name": "toolAgentflow", "type": "Tool", "color": "#d4a373", @@ -536,11 +536,11 @@ "inputParams": [ { "label": "Tool", - "name": "selectedTool", + "name": "toolAgentflowSelectedTool", "type": "asyncOptions", "loadMethod": "listTools", "loadConfig": true, - "id": "toolAgentflow_0-input-selectedTool-asyncOptions", + "id": "toolAgentflow_0-input-toolAgentflowSelectedTool-asyncOptions", "display": true }, { @@ -565,7 +565,7 @@ } ], "show": { - "selectedTool": ".+" + "toolAgentflowSelectedTool": ".+" }, "id": "toolAgentflow_0-input-toolInputArgs-array", "display": true @@ -599,7 +599,7 @@ ], "inputAnchors": [], "inputs": { - "selectedTool": "slackMCP", + "toolAgentflowSelectedTool": "slackMCP", "toolInputArgs": [ { "inputArgName": "channel_id", @@ -611,9 +611,9 @@ } ], "toolUpdateState": "", - "selectedToolConfig": { + "toolAgentflowSelectedToolConfig": { "mcpActions": "[\"slack_post_message\"]", - "selectedTool": "slackMCP" + "toolAgentflowSelectedTool": "slackMCP" } }, "outputAnchors": [ diff --git a/packages/server/package.json b/packages/server/package.json index 5ebe8318eca..69ed7749136 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "flowise", - "version": "3.0.1", + "version": "3.0.2", "description": "Flowiseai Server", "main": "dist/index", "types": "dist/index.d.ts", diff --git a/packages/server/src/commands/base.ts b/packages/server/src/commands/base.ts index 86222497bec..bdffb8f620e 100644 --- a/packages/server/src/commands/base.ts +++ b/packages/server/src/commands/base.ts @@ -1,6 +1,6 @@ import { Command, Flags } from '@oclif/core' -import path from 'path' import dotenv from 'dotenv' +import path from 'path' import logger from '../utils/logger' dotenv.config({ path: path.join(__dirname, '..', '..', '.env'), override: true }) @@ -120,7 +120,7 @@ export abstract class BaseCommand extends Command { logger.error('unhandledRejection: ', err) }) - const { flags } = await this.parse(BaseCommand) + const { flags } = await this.parse(this.constructor as any) if (flags.PORT) process.env.PORT = flags.PORT if (flags.CORS_ORIGINS) process.env.CORS_ORIGINS = flags.CORS_ORIGINS if (flags.IFRAME_ORIGINS) process.env.IFRAME_ORIGINS = flags.IFRAME_ORIGINS diff --git a/packages/server/src/commands/user.ts b/packages/server/src/commands/user.ts new file mode 100644 index 00000000000..1eecaaa2d33 --- /dev/null +++ b/packages/server/src/commands/user.ts @@ -0,0 +1,80 @@ +import { Args } from '@oclif/core' +import { QueryRunner } from 'typeorm' +import * as DataSource from '../DataSource' +import { User } from '../enterprise/database/entities/user.entity' +import { getHash } from '../enterprise/utils/encryption.util' +import { isInvalidPassword } from '../enterprise/utils/validation.util' +import logger from '../utils/logger' +import { BaseCommand } from './base' + +export default class user extends BaseCommand { + static args = { + email: Args.string({ + description: 'Email address to search for in the user database' + }), + password: Args.string({ + description: 'New password for that user' + }) + } + + async run(): Promise { + const { args } = await this.parse(user) + + let queryRunner: QueryRunner | undefined + try { + logger.info('Initializing DataSource') + const dataSource = await DataSource.getDataSource() + await dataSource.initialize() + + queryRunner = dataSource.createQueryRunner() + await queryRunner.connect() + + if (args.email && args.password) { + logger.info('Running resetPassword') + await this.resetPassword(queryRunner, args.email, args.password) + } else { + logger.info('Running listUserEmails') + await this.listUserEmails(queryRunner) + } + } catch (error) { + logger.error(error) + } finally { + if (queryRunner && !queryRunner.isReleased) await queryRunner.release() + await this.gracefullyExit() + } + } + + async listUserEmails(queryRunner: QueryRunner) { + logger.info('Listing all user emails') + const users = await queryRunner.manager.find(User, { + select: ['email'] + }) + + const emails = users.map((user) => user.email) + logger.info(`Email addresses: ${emails.join(', ')}`) + logger.info(`Email count: ${emails.length}`) + logger.info('To reset user password, run the following command: pnpm user --email "myEmail" --password "myPassword"') + } + + async resetPassword(queryRunner: QueryRunner, email: string, password: string) { + logger.info(`Finding user by email: ${email}`) + const user = await queryRunner.manager.findOne(User, { + where: { email } + }) + if (!user) throw new Error(`User not found with email: ${email}`) + + if (isInvalidPassword(password)) { + const errors = [] + if (!/(?=.*[a-z])/.test(password)) errors.push('at least one lowercase letter') + if (!/(?=.*[A-Z])/.test(password)) errors.push('at least one uppercase letter') + if (!/(?=.*\d)/.test(password)) errors.push('at least one number') + if (!/(?=.*[^a-zA-Z0-9])/.test(password)) errors.push('at least one special character') + if (password.length < 8) errors.push('minimum length of 8 characters') + throw new Error(`Invalid password: Must contain ${errors.join(', ')}`) + } + + user.credential = getHash(password) + await queryRunner.manager.save(user) + logger.info(`Password reset for user: ${email}`) + } +} diff --git a/packages/server/src/database/migrations/mariadb/1733752119696-AddSeqNoToDatasetRow.ts b/packages/server/src/database/migrations/mariadb/1733752119696-AddSeqNoToDatasetRow.ts index e6ec131f06d..3cb553220c8 100644 --- a/packages/server/src/database/migrations/mariadb/1733752119696-AddSeqNoToDatasetRow.ts +++ b/packages/server/src/database/migrations/mariadb/1733752119696-AddSeqNoToDatasetRow.ts @@ -7,6 +7,6 @@ export class AddSeqNoToDatasetRow1733752119696 implements MigrationInterface { } public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "dataset_row" DROP COLUMN "sequence_no";`) + await queryRunner.query(`ALTER TABLE \`dataset_row\` DROP COLUMN \`sequence_no\``) } } diff --git a/packages/server/src/database/migrations/mariadb/1744964560174-AddErrorToEvaluationRun.ts b/packages/server/src/database/migrations/mariadb/1744964560174-AddErrorToEvaluationRun.ts index 9a5d6488c56..10fc6f6ffa0 100644 --- a/packages/server/src/database/migrations/mariadb/1744964560174-AddErrorToEvaluationRun.ts +++ b/packages/server/src/database/migrations/mariadb/1744964560174-AddErrorToEvaluationRun.ts @@ -7,6 +7,6 @@ export class AddErrorToEvaluationRun1744964560174 implements MigrationInterface } public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "evaluation_run" DROP COLUMN "errors";`) + await queryRunner.query(`ALTER TABLE \`evaluation_run\` DROP COLUMN \`errors\`;`) } } diff --git a/packages/server/src/enterprise/Interface.Enterprise.ts b/packages/server/src/enterprise/Interface.Enterprise.ts index 0e0482d9365..d7ddfc393d6 100644 --- a/packages/server/src/enterprise/Interface.Enterprise.ts +++ b/packages/server/src/enterprise/Interface.Enterprise.ts @@ -104,8 +104,10 @@ export const OrgSetupSchema = z password: z .string() .min(8, 'Password must be at least 8 characters') + .regex(/[a-z]/, 'Password must contain at least one lowercase letter') .regex(/[A-Z]/, 'Password must contain at least one uppercase letter') - .regex(/[!@#$%^&*]/, 'Password must contain at least one special character'), + .regex(/\d/, 'Password must contain at least one digit') + .regex(/[^a-zA-Z0-9]/, 'Password must contain at least one special character'), confirmPassword: z.string().min(1, 'Confirm Password is required') }) .refine((data) => data.password === data.confirmPassword, { @@ -122,8 +124,10 @@ export const RegisterUserSchema = z password: z .string() .min(8, 'Password must be at least 8 characters') + .regex(/[a-z]/, 'Password must contain at least one lowercase letter') .regex(/[A-Z]/, 'Password must contain at least one uppercase letter') - .regex(/[!@#$%^&*]/, 'Password must contain at least one special character'), + .regex(/\d/, 'Password must contain at least one digit') + .regex(/[^a-zA-Z0-9]/, 'Password must contain at least one special character'), confirmPassword: z.string().min(1, 'Confirm Password is required'), token: z.string().min(1, 'Invite Code is required') }) diff --git a/packages/server/src/enterprise/database/migrations/mariadb/1730519457880-AddSSOColumns.ts b/packages/server/src/enterprise/database/migrations/mariadb/1730519457880-AddSSOColumns.ts index 2181b2060f2..5c00f4357d8 100644 --- a/packages/server/src/enterprise/database/migrations/mariadb/1730519457880-AddSSOColumns.ts +++ b/packages/server/src/enterprise/database/migrations/mariadb/1730519457880-AddSSOColumns.ts @@ -9,8 +9,8 @@ export class AddSSOColumns1730519457880 implements MigrationInterface { } public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "organization" DROP COLUMN "sso_config";`) - await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "user_type";`) - await queryRunner.query(`ALTER TABLE "login_activity" DROP COLUMN "login_mode";`) + await queryRunner.query(`ALTER TABLE \`organization\` DROP COLUMN \`sso_config\`;`) + await queryRunner.query(`ALTER TABLE \`user\` DROP COLUMN \`user_type\`;`) + await queryRunner.query(`ALTER TABLE \`login_activity\` DROP COLUMN \`login_mode\`;`) } } diff --git a/packages/server/src/enterprise/middleware/passport/index.ts b/packages/server/src/enterprise/middleware/passport/index.ts index cdbdeb2b954..b93d2413239 100644 --- a/packages/server/src/enterprise/middleware/passport/index.ts +++ b/packages/server/src/enterprise/middleware/passport/index.ts @@ -25,12 +25,12 @@ import { initializeDBClientAndStore, initializeRedisClientAndStore } from './Ses const localStrategy = require('passport-local').Strategy -const jwtAudience = process.env.JWT_AUDIENCE ?? 'AUDIENCE' -const jwtIssuer = process.env.JWT_ISSUER ?? 'ISSUER' +const jwtAudience = process.env.JWT_AUDIENCE || 'AUDIENCE' +const jwtIssuer = process.env.JWT_ISSUER || 'ISSUER' const expireAuthTokensOnRestart = process.env.EXPIRE_AUTH_TOKENS_ON_RESTART === 'true' -const jwtAuthTokenSecret = process.env.JWT_AUTH_TOKEN_SECRET ?? 'auth_token' -const jwtRefreshSecret = process.env.JWT_REFRESH_TOKEN_SECRET ?? process.env.JWT_AUTH_TOKEN_SECRET ?? 'refresh_token' +const jwtAuthTokenSecret = process.env.JWT_AUTH_TOKEN_SECRET || 'auth_token' +const jwtRefreshSecret = process.env.JWT_REFRESH_TOKEN_SECRET || process.env.JWT_AUTH_TOKEN_SECRET || 'refresh_token' const secureCookie = process.env.APP_URL?.startsWith('https') ? true : false const jwtOptions = { @@ -218,7 +218,7 @@ export const initializeJwtCookieMiddleware = async (app: express.Application, id if (!refreshToken) return res.sendStatus(401) jwt.verify(refreshToken, jwtRefreshSecret, async (err: any, payload: any) => { - if (err || !payload) return res.status(403).json({ message: ErrorMessage.REFRESH_TOKEN_EXPIRED }) + if (err || !payload) return res.status(401).json({ message: ErrorMessage.REFRESH_TOKEN_EXPIRED }) // @ts-ignore const loggedInUser = req.user as LoggedInUser let isSSO = false @@ -227,16 +227,16 @@ export const initializeJwtCookieMiddleware = async (app: express.Application, id try { newTokenResponse = await identityManager.getRefreshToken(loggedInUser.ssoProvider, loggedInUser.ssoRefreshToken) if (newTokenResponse.error) { - return res.status(403).json({ message: ErrorMessage.REFRESH_TOKEN_EXPIRED }) + return res.status(401).json({ message: ErrorMessage.REFRESH_TOKEN_EXPIRED }) } isSSO = true } catch (error) { - return res.status(403).json({ message: ErrorMessage.REFRESH_TOKEN_EXPIRED }) + return res.status(401).json({ message: ErrorMessage.REFRESH_TOKEN_EXPIRED }) } } const meta = decryptToken(payload.meta) if (!meta) { - return res.status(403).json({ message: ErrorMessage.REFRESH_TOKEN_EXPIRED }) + return res.status(401).json({ message: ErrorMessage.REFRESH_TOKEN_EXPIRED }) } if (isSSO) { loggedInUser.ssoToken = newTokenResponse.access_token diff --git a/packages/server/src/enterprise/services/account.service.ts b/packages/server/src/enterprise/services/account.service.ts index 268326542ef..5cc1107e699 100644 --- a/packages/server/src/enterprise/services/account.service.ts +++ b/packages/server/src/enterprise/services/account.service.ts @@ -104,6 +104,11 @@ export class AccountService { } } + private async ensureOneOrganizationOnly(queryRunner: QueryRunner) { + const organizations = await this.organizationservice.readOrganization(queryRunner) + if (organizations.length > 0) throw new InternalFlowiseError(StatusCodes.BAD_REQUEST, 'You can only have one organization') + } + private async createRegisterAccount(data: AccountDTO, queryRunner: QueryRunner) { data = this.initializeAccountDTO(data) @@ -111,6 +116,7 @@ export class AccountService { switch (platform) { case Platform.OPEN_SOURCE: + await this.ensureOneOrganizationOnly(queryRunner) data.organization.name = OrganizationName.DEFAULT_ORGANIZATION data.organizationUser.role = await this.roleService.readGeneralRoleByName(GeneralRole.OWNER, queryRunner) data.workspace.name = WorkspaceName.DEFAULT_WORKSPACE @@ -196,6 +202,7 @@ export class AccountService { data.workspace.name = WorkspaceName.DEFAULT_PERSONAL_WORKSPACE data.workspaceUser.role = await this.roleService.readGeneralRoleByName(GeneralRole.PERSONAL_WORKSPACE, queryRunner) } else { + await this.ensureOneOrganizationOnly(queryRunner) data.organizationUser.role = await this.roleService.readGeneralRoleByName(GeneralRole.OWNER, queryRunner) data.workspace.name = WorkspaceName.DEFAULT_WORKSPACE data.workspaceUser.role = data.organizationUser.role diff --git a/packages/server/src/enterprise/utils/validation.util.ts b/packages/server/src/enterprise/utils/validation.util.ts index c22abb1b51c..96015ee68d0 100644 --- a/packages/server/src/enterprise/utils/validation.util.ts +++ b/packages/server/src/enterprise/utils/validation.util.ts @@ -18,6 +18,6 @@ export function isInvalidDateTime(dateTime: unknown): boolean { } export function isInvalidPassword(password: unknown): boolean { - const regexPassword = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&-])[A-Za-z\d@$!%*?&-]{8,}$/ + const regexPassword = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^a-zA-Z0-9]).{8,}$/ return !password || typeof password !== 'string' || !regexPassword.test(password) } diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index faebc787812..9460bb26f1d 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -81,39 +81,49 @@ export class App { // Initialize database try { await this.AppDataSource.initialize() - logger.info('📦 [server]: Data Source is initializing...') + logger.info('📦 [server]: Data Source initialized successfully') // Run Migrations Scripts await this.AppDataSource.runMigrations({ transaction: 'each' }) + logger.info('🔄 [server]: Database migrations completed successfully') // Initialize Identity Manager this.identityManager = await IdentityManager.getInstance() + logger.info('🔐 [server]: Identity Manager initialized successfully') // Initialize nodes pool this.nodesPool = new NodesPool() await this.nodesPool.initialize() + logger.info('🔧 [server]: Nodes pool initialized successfully') // Initialize abort controllers pool this.abortControllerPool = new AbortControllerPool() + logger.info('⏹️ [server]: Abort controllers pool initialized successfully') // Initialize encryption key await getEncryptionKey() + logger.info('🔑 [server]: Encryption key initialized successfully') // Initialize Rate Limit this.rateLimiterManager = RateLimiterManager.getInstance() await this.rateLimiterManager.initializeRateLimiters(await getDataSource().getRepository(ChatFlow).find()) + logger.info('🚦 [server]: Rate limiters initialized successfully') // Initialize cache pool this.cachePool = new CachePool() + logger.info('💾 [server]: Cache pool initialized successfully') // Initialize usage cache manager this.usageCacheManager = await UsageCacheManager.getInstance() + logger.info('📊 [server]: Usage cache manager initialized successfully') // Initialize telemetry this.telemetry = new Telemetry() + logger.info('📈 [server]: Telemetry initialized successfully') // Initialize SSE Streamer this.sseStreamer = new SSEStreamer() + logger.info('🌊 [server]: SSE Streamer initialized successfully') // Init Queues if (process.env.MODE === MODE.QUEUE) { @@ -127,14 +137,16 @@ export class App { usageCacheManager: this.usageCacheManager }) logger.info('✅ [Queue]: All queues setup successfully') + this.redisSubscriber = new RedisEventSubscriber(this.sseStreamer) await this.redisSubscriber.connect() + logger.info('🔗 [server]: Redis event subscriber connected successfully') } // TODO: Remove this by end of 2025 await migrateApiKeysFromJsonToDb(this.AppDataSource, this.identityManager.getPlatformType()) - logger.info('📦 [server]: Data Source has been initialized!') + logger.info('🎉 [server]: All initialization steps completed successfully!') } catch (error) { logger.error('❌ [server]: Error during Data Source initialization:', error) } diff --git a/packages/server/src/routes/index.ts b/packages/server/src/routes/index.ts index 63633e7b6f9..4941a0076f9 100644 --- a/packages/server/src/routes/index.ts +++ b/packages/server/src/routes/index.ts @@ -31,6 +31,7 @@ import nodeCustomFunctionRouter from './node-custom-functions' import nodeIconRouter from './node-icons' import nodeLoadMethodRouter from './node-load-methods' import nodesRouter from './nodes' +import oauth2Router from './oauth2' import openaiAssistantsRouter from './openai-assistants' import openaiAssistantsFileRouter from './openai-assistants-files' import openaiAssistantsVectorStoreRouter from './openai-assistants-vector-store' @@ -100,6 +101,7 @@ router.use('/node-custom-function', nodeCustomFunctionRouter) router.use('/node-icon', nodeIconRouter) router.use('/node-load-method', nodeLoadMethodRouter) router.use('/nodes', nodesRouter) +router.use('/oauth2-credential', oauth2Router) router.use('/openai-assistants', openaiAssistantsRouter) router.use('/openai-assistants-file', openaiAssistantsFileRouter) router.use('/openai-assistants-vector-store', openaiAssistantsVectorStoreRouter) diff --git a/packages/server/src/routes/oauth2/index.ts b/packages/server/src/routes/oauth2/index.ts new file mode 100644 index 00000000000..b5c5f571b76 --- /dev/null +++ b/packages/server/src/routes/oauth2/index.ts @@ -0,0 +1,422 @@ +/** + * OAuth2 Authorization Code Flow Implementation + * + * This module implements a complete OAuth2 authorization code flow for Flowise credentials. + * It supports Microsoft Graph and other OAuth2 providers. + * + * CREDENTIAL DATA STRUCTURE: + * The credential's encryptedData should contain a JSON object with the following fields: + * + * Required fields: + * - client_id: OAuth2 application client ID + * - client_secret: OAuth2 application client secret + * + * Optional fields (provider-specific): + * - tenant_id: Microsoft Graph tenant ID (if using Microsoft Graph) + * - authorization_endpoint: Custom authorization URL (defaults to Microsoft Graph if tenant_id provided) + * - token_endpoint: Custom token URL (defaults to Microsoft Graph if tenant_id provided) + * - redirect_uri: Custom redirect URI (defaults to this callback endpoint) + * - scope: OAuth2 scopes to request (e.g., "user.read mail.read") + * - response_type: OAuth2 response type (defaults to "code") + * - response_mode: OAuth2 response mode (defaults to "query") + * + * ENDPOINTS: + * + * 1. POST /api/v1/oauth2/authorize/:credentialId + * - Generates authorization URL for initiating OAuth2 flow + * - Uses credential ID as state parameter for security + * - Returns authorization URL to redirect user to + * + * 2. GET /api/v1/oauth2/callback + * - Handles OAuth2 callback with authorization code + * - Exchanges code for access token + * - Updates credential with token data + * - Supports Microsoft Graph and custom OAuth2 providers + * + * 3. POST /api/v1/oauth2/refresh/:credentialId + * - Refreshes expired access tokens using refresh token + * - Updates credential with new token data + * + * USAGE FLOW: + * 1. Create a credential with OAuth2 configuration (client_id, client_secret, etc.) + * 2. Call POST /oauth2/authorize/:credentialId to get authorization URL + * 3. Redirect user to authorization URL + * 4. User authorizes and gets redirected to callback endpoint + * 5. Callback endpoint exchanges code for tokens and saves them + * 6. Use POST /oauth2/refresh/:credentialId when tokens expire + * + * TOKEN STORAGE: + * After successful authorization, the credential will contain additional fields: + * - access_token: OAuth2 access token + * - refresh_token: OAuth2 refresh token (if provided) + * - token_type: Token type (usually "Bearer") + * - expires_in: Token lifetime in seconds + * - expires_at: Token expiry timestamp (ISO string) + * - granted_scope: Actual scopes granted by provider + * - token_received_at: When token was received (ISO string) + */ + +import express from 'express' +import axios from 'axios' +import { Request, Response, NextFunction } from 'express' +import { getRunningExpressApp } from '../../utils/getRunningExpressApp' +import { Credential } from '../../database/entities/Credential' +import { decryptCredentialData, encryptCredentialData } from '../../utils' +import { InternalFlowiseError } from '../../errors/internalFlowiseError' +import { StatusCodes } from 'http-status-codes' +import { generateSuccessPage, generateErrorPage } from './templates' + +const router = express.Router() + +// Initiate OAuth2 authorization flow +router.post('/authorize/:credentialId', async (req: Request, res: Response, next: NextFunction) => { + try { + const { credentialId } = req.params + + const appServer = getRunningExpressApp() + const credentialRepository = appServer.AppDataSource.getRepository(Credential) + + // Find credential by ID + const credential = await credentialRepository.findOneBy({ + id: credentialId + }) + + if (!credential) { + return res.status(404).json({ + success: false, + message: 'Credential not found' + }) + } + + // Decrypt the credential data to get OAuth configuration + const decryptedData = await decryptCredentialData(credential.encryptedData) + + const { + clientId, + authorizationUrl, + redirect_uri, + scope, + response_type = 'code', + response_mode = 'query', + additionalParameters = '' + } = decryptedData + + if (!clientId) { + return res.status(400).json({ + success: false, + message: 'Missing clientId in credential data' + }) + } + + if (!authorizationUrl) { + return res.status(400).json({ + success: false, + message: 'No authorizationUrl specified in credential data' + }) + } + + const defaultRedirectUri = `${req.protocol}://${req.get('host')}/api/v1/oauth2-credential/callback` + const finalRedirectUri = redirect_uri || defaultRedirectUri + + const authParams = new URLSearchParams({ + client_id: clientId, + response_type, + response_mode, + state: credentialId, // Use credential ID as state parameter + redirect_uri: finalRedirectUri + }) + + if (scope) { + authParams.append('scope', scope) + } + + let fullAuthorizationUrl = `${authorizationUrl}?${authParams.toString()}` + + if (additionalParameters) { + fullAuthorizationUrl += `&${additionalParameters.toString()}` + } + + res.json({ + success: true, + message: 'Authorization URL generated successfully', + credentialId, + authorizationUrl: fullAuthorizationUrl, + redirectUri: finalRedirectUri + }) + } catch (error) { + next( + new InternalFlowiseError( + StatusCodes.INTERNAL_SERVER_ERROR, + `OAuth2 authorization error: ${error instanceof Error ? error.message : 'Unknown error'}` + ) + ) + } +}) + +// OAuth2 callback endpoint +router.get('/callback', async (req: Request, res: Response) => { + try { + const { code, state, error, error_description } = req.query + + if (error) { + const errorHtml = generateErrorPage( + error as string, + (error_description as string) || 'An error occurred', + error_description ? `Description: ${error_description}` : undefined + ) + + res.setHeader('Content-Type', 'text/html') + return res.status(400).send(errorHtml) + } + + if (!code || !state) { + const errorHtml = generateErrorPage('Missing required parameters', 'Missing code or state', 'Please try again later.') + + res.setHeader('Content-Type', 'text/html') + return res.status(400).send(errorHtml) + } + + const appServer = getRunningExpressApp() + const credentialRepository = appServer.AppDataSource.getRepository(Credential) + + // Find credential by state (assuming state contains the credential ID) + const credential = await credentialRepository.findOneBy({ + id: state as string + }) + + if (!credential) { + const errorHtml = generateErrorPage( + 'Credential not found', + `Credential not found for the provided state: ${state}`, + 'Please try the authorization process again.' + ) + + res.setHeader('Content-Type', 'text/html') + return res.status(404).send(errorHtml) + } + + const decryptedData = await decryptCredentialData(credential.encryptedData) + + const { clientId, clientSecret, accessTokenUrl, redirect_uri, scope } = decryptedData + + if (!clientId || !clientSecret) { + const errorHtml = generateErrorPage( + 'Missing OAuth configuration', + 'Missing clientId or clientSecret', + 'Please check your credential setup.' + ) + + res.setHeader('Content-Type', 'text/html') + return res.status(400).send(errorHtml) + } + + let tokenUrl = accessTokenUrl + if (!tokenUrl) { + const errorHtml = generateErrorPage( + 'Missing token endpoint URL', + 'No Access Token URL specified in credential data', + 'Please check your credential configuration.' + ) + + res.setHeader('Content-Type', 'text/html') + return res.status(400).send(errorHtml) + } + + const defaultRedirectUri = `${req.protocol}://${req.get('host')}/api/v1/oauth2-credential/callback` + const finalRedirectUri = redirect_uri || defaultRedirectUri + + const tokenRequestData: any = { + client_id: clientId, + client_secret: clientSecret, + code: code as string, + grant_type: 'authorization_code', + redirect_uri: finalRedirectUri + } + + if (scope) { + tokenRequestData.scope = scope + } + + const tokenResponse = await axios.post(tokenUrl, new URLSearchParams(tokenRequestData).toString(), { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Accept: 'application/json' + } + }) + + const tokenData = tokenResponse.data + + // Update the credential data with token information + const updatedCredentialData: any = { + ...decryptedData, + ...tokenData, + token_received_at: new Date().toISOString() + } + + // Add refresh token if provided + if (tokenData.refresh_token) { + updatedCredentialData.refresh_token = tokenData.refresh_token + } + + // Calculate token expiry time + if (tokenData.expires_in) { + const expiryTime = new Date(Date.now() + tokenData.expires_in * 1000) + updatedCredentialData.expires_at = expiryTime.toISOString() + } + + // Encrypt the updated credential data + const encryptedData = await encryptCredentialData(updatedCredentialData) + + // Update the credential in the database + await credentialRepository.update(credential.id, { + encryptedData, + updatedDate: new Date() + }) + + // Return HTML that closes the popup window on success + const successHtml = generateSuccessPage(credential.id) + + res.setHeader('Content-Type', 'text/html') + res.send(successHtml) + } catch (error) { + if (axios.isAxiosError(error)) { + const axiosError = error + const errorHtml = generateErrorPage( + axiosError.response?.data?.error || 'token_exchange_failed', + axiosError.response?.data?.error_description || 'Token exchange failed', + axiosError.response?.data?.error_description ? `Description: ${axiosError.response?.data?.error_description}` : undefined + ) + + res.setHeader('Content-Type', 'text/html') + return res.status(400).send(errorHtml) + } + + // Generic error HTML page + const errorHtml = generateErrorPage( + 'An unexpected error occurred', + 'Please try again later.', + error instanceof Error ? error.message : 'Unknown error' + ) + + res.setHeader('Content-Type', 'text/html') + res.status(500).send(errorHtml) + } +}) + +// Refresh OAuth2 access token +router.post('/refresh/:credentialId', async (req: Request, res: Response, next: NextFunction) => { + try { + const { credentialId } = req.params + + const appServer = getRunningExpressApp() + const credentialRepository = appServer.AppDataSource.getRepository(Credential) + + const credential = await credentialRepository.findOneBy({ + id: credentialId + }) + + if (!credential) { + return res.status(404).json({ + success: false, + message: 'Credential not found' + }) + } + + const decryptedData = await decryptCredentialData(credential.encryptedData) + + const { clientId, clientSecret, refresh_token, accessTokenUrl, scope } = decryptedData + + if (!clientId || !clientSecret || !refresh_token) { + return res.status(400).json({ + success: false, + message: 'Missing required OAuth configuration: clientId, clientSecret, or refresh_token' + }) + } + + let tokenUrl = accessTokenUrl + if (!tokenUrl) { + return res.status(400).json({ + success: false, + message: 'No Access Token URL specified in credential data' + }) + } + + const refreshRequestData: any = { + client_id: clientId, + client_secret: clientSecret, + grant_type: 'refresh_token', + refresh_token + } + + if (scope) { + refreshRequestData.scope = scope + } + + const tokenResponse = await axios.post(tokenUrl, new URLSearchParams(refreshRequestData).toString(), { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Accept: 'application/json' + } + }) + + // Extract token data from response + const tokenData = tokenResponse.data + + // Update the credential data with new token information + const updatedCredentialData: any = { + ...decryptedData, + ...tokenData, + token_received_at: new Date().toISOString() + } + + // Update refresh token if a new one was provided + if (tokenData.refresh_token) { + updatedCredentialData.refresh_token = tokenData.refresh_token + } + + // Calculate token expiry time + if (tokenData.expires_in) { + const expiryTime = new Date(Date.now() + tokenData.expires_in * 1000) + updatedCredentialData.expires_at = expiryTime.toISOString() + } + + // Encrypt the updated credential data + const encryptedData = await encryptCredentialData(updatedCredentialData) + + // Update the credential in the database + await credentialRepository.update(credential.id, { + encryptedData, + updatedDate: new Date() + }) + + // Return success response + res.json({ + success: true, + message: 'OAuth2 token refreshed successfully', + credentialId: credential.id, + tokenInfo: { + ...tokenData, + has_new_refresh_token: !!tokenData.refresh_token, + expires_at: updatedCredentialData.expires_at + } + }) + } catch (error) { + if (axios.isAxiosError(error)) { + const axiosError = error + return res.status(400).json({ + success: false, + message: `Token refresh failed: ${axiosError.response?.data?.error_description || axiosError.message}`, + details: axiosError.response?.data + }) + } + + next( + new InternalFlowiseError( + StatusCodes.INTERNAL_SERVER_ERROR, + `OAuth2 token refresh error: ${error instanceof Error ? error.message : 'Unknown error'}` + ) + ) + } +}) + +export default router diff --git a/packages/server/src/routes/oauth2/templates.ts b/packages/server/src/routes/oauth2/templates.ts new file mode 100644 index 00000000000..6b360974611 --- /dev/null +++ b/packages/server/src/routes/oauth2/templates.ts @@ -0,0 +1,128 @@ +/** + * HTML Templates for OAuth2 Callback Pages + * + * This module contains reusable HTML templates for OAuth2 authorization responses. + * The templates provide consistent styling and behavior for success and error pages. + */ + +export interface OAuth2PageOptions { + title: string + statusIcon: string + statusText: string + statusColor: string + message: string + details?: string + postMessageType: 'OAUTH2_SUCCESS' | 'OAUTH2_ERROR' + postMessageData: any + autoCloseDelay: number +} + +export const generateOAuth2ResponsePage = (options: OAuth2PageOptions): string => { + const { title, statusIcon, statusText, statusColor, message, details, postMessageType, postMessageData, autoCloseDelay } = options + + return ` + + + + ${title} + + + +
+
${statusIcon} ${statusText}
+
${message}
+ ${details ? `
${details}
` : ''} +
+ + + + ` +} + +export const generateSuccessPage = (credentialId: string): string => { + return generateOAuth2ResponsePage({ + title: 'OAuth2 Authorization Success', + statusIcon: '✓', + statusText: 'Authorization Successful', + statusColor: '#4caf50', + message: 'You can close this window now.', + postMessageType: 'OAUTH2_SUCCESS', + postMessageData: { + credentialId, + success: true, + message: 'OAuth2 authorization completed successfully' + }, + autoCloseDelay: 1000 + }) +} + +export const generateErrorPage = (error: string, message: string, details?: string): string => { + return generateOAuth2ResponsePage({ + title: 'OAuth2 Authorization Error', + statusIcon: '✗', + statusText: 'Authorization Failed', + statusColor: '#f44336', + message, + details, + postMessageType: 'OAUTH2_ERROR', + postMessageData: { + success: false, + message, + error + }, + autoCloseDelay: 3000 + }) +} diff --git a/packages/server/src/services/evaluations/CostCalculator.ts b/packages/server/src/services/evaluations/CostCalculator.ts index 8813ade958d..4013706ef4e 100644 --- a/packages/server/src/services/evaluations/CostCalculator.ts +++ b/packages/server/src/services/evaluations/CostCalculator.ts @@ -18,39 +18,29 @@ export const calculateCost = (metricsArray: ICommonObject[]) => { let completionTokensCost: string = '0' let totalTokensCost = '0' if (metric.cost_values) { - const costValues = metric.cost_values + let costValues: any = {} + if (metric.cost_values?.cost_values) { + costValues = metric.cost_values.cost_values + } else { + costValues = metric.cost_values + } + if (costValues.total_price > 0) { let cost = costValues.total_cost * (totalTokens / 1000) - if (cost < 0.01) { - totalTokensCost = '$ <0.01' - } else { - totalTokensCost = '$ ' + cost.toFixed(fractionDigits) - } + totalTokensCost = formatCost(cost) } else { let totalCost = 0 if (promptTokens) { const cost = costValues.input_cost * (promptTokens / 1000) totalCost += cost - if (cost < 0.01) { - promptTokensCost = '$ <0.01' - } else { - promptTokensCost = '$ ' + cost.toFixed(fractionDigits) - } + promptTokensCost = formatCost(cost) } if (completionTokens) { const cost = costValues.output_cost * (completionTokens / 1000) totalCost += cost - if (cost < 0.01) { - completionTokensCost = '$ <0.01' - } else { - completionTokensCost = '$ ' + cost.toFixed(fractionDigits) - } - } - if (totalCost < 0.01) { - totalTokensCost = '$ <0.01' - } else { - totalTokensCost = '$ ' + totalCost.toFixed(fractionDigits) + completionTokensCost = formatCost(cost) } + totalTokensCost = formatCost(totalCost) } } metric['totalCost'] = totalTokensCost @@ -58,3 +48,10 @@ export const calculateCost = (metricsArray: ICommonObject[]) => { metric['completionCost'] = completionTokensCost } } + +export const formatCost = (cost: number) => { + if (cost == 0) { + return '$ 0' + } + return cost < 0.01 ? '$ <0.01' : '$ ' + cost.toFixed(fractionDigits) +} diff --git a/packages/server/src/services/evaluations/index.ts b/packages/server/src/services/evaluations/index.ts index acca639cc05..2d44b3ce22b 100644 --- a/packages/server/src/services/evaluations/index.ts +++ b/packages/server/src/services/evaluations/index.ts @@ -15,10 +15,11 @@ import { getAppVersion } from '../../utils' import { In } from 'typeorm' import { getWorkspaceSearchOptions } from '../../enterprise/utils/ControllerServiceUtils' import { v4 as uuidv4 } from 'uuid' -import { calculateCost } from './CostCalculator' +import { calculateCost, formatCost } from './CostCalculator' import { runAdditionalEvaluators } from './EvaluatorRunner' import evaluatorsService from '../evaluator' import { LLMEvaluationRunner } from './LLMEvaluationRunner' +import { Assistant } from '../../database/entities/Assistant' const runAgain = async (id: string, baseURL: string, orgId: string) => { try { @@ -27,7 +28,7 @@ const runAgain = async (id: string, baseURL: string, orgId: string) => { id: id }) if (!evaluation) throw new Error(`Evaluation ${id} not found`) - const additionalConfig: any = JSON.parse(evaluation.additionalConfig) + const additionalConfig = evaluation.additionalConfig ? JSON.parse(evaluation.additionalConfig) : {} const data: ICommonObject = { chatflowId: evaluation.chatflowId, chatflowName: evaluation.chatflowName, @@ -35,7 +36,8 @@ const runAgain = async (id: string, baseURL: string, orgId: string) => { datasetId: evaluation.datasetId, evaluationType: evaluation.evaluationType, selectedSimpleEvaluators: JSON.stringify(additionalConfig.simpleEvaluators), - datasetAsOneConversation: additionalConfig.datasetAsOneConversation + datasetAsOneConversation: additionalConfig.datasetAsOneConversation, + chatflowType: JSON.stringify(additionalConfig.chatflowTypes ? additionalConfig.chatflowTypes : []) } data.name = evaluation.name data.workspaceId = evaluation.workspaceId @@ -69,7 +71,8 @@ const createEvaluation = async (body: ICommonObject, baseURL: string, orgId: str const row = appServer.AppDataSource.getRepository(Evaluation).create(newEval) row.average_metrics = JSON.stringify({}) - const additionalConfig: any = { + const additionalConfig: ICommonObject = { + chatflowTypes: body.chatflowType ? JSON.parse(body.chatflowType) : [], datasetAsOneConversation: body.datasetAsOneConversation, simpleEvaluators: body.selectedSimpleEvaluators.length > 0 ? JSON.parse(body.selectedSimpleEvaluators) : [] } @@ -152,7 +155,7 @@ const createEvaluation = async (body: ICommonObject, baseURL: string, orgId: str let evalMetrics = { passCount: 0, failCount: 0, errorCount: 0 } evalRunner .runEvaluations(data) - .then(async (result: any) => { + .then(async (result) => { let totalTime = 0 // let us assume that the eval is successful let allRowsSuccessful = true @@ -171,8 +174,48 @@ const createEvaluation = async (body: ICommonObject, baseURL: string, orgId: str totalTime += parseFloat(evaluationRow.latency) let metricsObjFromRun: ICommonObject = {} + let nested_metrics = evaluationRow.nested_metrics + + let promptTokens = 0, + completionTokens = 0, + totalTokens = 0 + let inputCost = 0, + outputCost = 0, + totalCost = 0 + if (nested_metrics && nested_metrics.length > 0) { + for (let i = 0; i < nested_metrics.length; i++) { + const nested_metric = nested_metrics[i] + if (nested_metric.model && nested_metric.promptTokens > 0) { + promptTokens += nested_metric.promptTokens + completionTokens += nested_metric.completionTokens + totalTokens += nested_metric.totalTokens + + inputCost += nested_metric.cost_values.input_cost + outputCost += nested_metric.cost_values.output_cost + totalCost += nested_metric.cost_values.total_cost + + nested_metric['totalCost'] = formatCost(nested_metric.cost_values.total_cost) + nested_metric['promptCost'] = formatCost(nested_metric.cost_values.input_cost) + nested_metric['completionCost'] = formatCost(nested_metric.cost_values.output_cost) + } + } + nested_metrics = nested_metrics.filter((metric: any) => { + return metric.model && metric.provider + }) + } const metrics = evaluationRow.metrics if (metrics) { + if (nested_metrics && nested_metrics.length > 0) { + metrics.push({ + promptTokens: promptTokens, + completionTokens: completionTokens, + totalTokens: totalTokens, + totalCost: formatCost(totalCost), + promptCost: formatCost(inputCost), + completionCost: formatCost(outputCost) + }) + metricsObjFromRun.nested_metrics = nested_metrics + } metrics.map((metric: any) => { if (metric) { const json = typeof metric === 'object' ? metric : JSON.parse(metric) @@ -211,7 +254,7 @@ const createEvaluation = async (body: ICommonObject, baseURL: string, orgId: str if (body.evaluationType === 'llm') { resultRow.llmConfig = additionalConfig.llmConfig resultRow.LLMEvaluators = body.selectedLLMEvaluators.length > 0 ? JSON.parse(body.selectedLLMEvaluators) : [] - const llmEvaluatorMap: any = [] + const llmEvaluatorMap: { evaluatorId: string; evaluator: any }[] = [] for (let i = 0; i < resultRow.LLMEvaluators.length; i++) { const evaluatorId = resultRow.LLMEvaluators[i] const evaluator = await evaluatorsService.getEvaluator(evaluatorId) @@ -243,23 +286,27 @@ const createEvaluation = async (body: ICommonObject, baseURL: string, orgId: str } appServer.AppDataSource.getRepository(Evaluation) .findOneBy({ id: newEvaluation.id }) - .then((evaluation: any) => { - evaluation.status = allRowsSuccessful ? EvaluationStatus.COMPLETED : EvaluationStatus.ERROR - evaluation.average_metrics = JSON.stringify({ - averageLatency: (totalTime / result.rows.length).toFixed(3), - totalRuns: result.rows.length, - ...evalMetrics, - passPcnt: passPercent.toFixed(2) - }) - appServer.AppDataSource.getRepository(Evaluation).save(evaluation) + .then((evaluation) => { + if (evaluation) { + evaluation.status = allRowsSuccessful ? EvaluationStatus.COMPLETED : EvaluationStatus.ERROR + evaluation.average_metrics = JSON.stringify({ + averageLatency: (totalTime / result.rows.length).toFixed(3), + totalRuns: result.rows.length, + ...evalMetrics, + passPcnt: passPercent.toFixed(2) + }) + appServer.AppDataSource.getRepository(Evaluation).save(evaluation) + } }) } catch (error) { //update the evaluation with status as error appServer.AppDataSource.getRepository(Evaluation) .findOneBy({ id: newEvaluation.id }) - .then((evaluation: any) => { - evaluation.status = EvaluationStatus.ERROR - appServer.AppDataSource.getRepository(Evaluation).save(evaluation) + .then((evaluation) => { + if (evaluation) { + evaluation.status = EvaluationStatus.ERROR + appServer.AppDataSource.getRepository(Evaluation).save(evaluation) + } }) } }) @@ -268,12 +315,14 @@ const createEvaluation = async (body: ICommonObject, baseURL: string, orgId: str console.error('Error running evaluations:', getErrorMessage(error)) appServer.AppDataSource.getRepository(Evaluation) .findOneBy({ id: newEvaluation.id }) - .then((evaluation: any) => { - evaluation.status = EvaluationStatus.ERROR - evaluation.average_metrics = JSON.stringify({ - error: getErrorMessage(error) - }) - appServer.AppDataSource.getRepository(Evaluation).save(evaluation) + .then((evaluation) => { + if (evaluation) { + evaluation.status = EvaluationStatus.ERROR + evaluation.average_metrics = JSON.stringify({ + error: getErrorMessage(error) + }) + appServer.AppDataSource.getRepository(Evaluation).save(evaluation) + } }) .catch((dbError) => { console.error('Error updating evaluation status:', getErrorMessage(dbError)) @@ -378,18 +427,31 @@ const isOutdated = async (id: string) => { returnObj.dataset = dataset } } else { - returnObj.errors.push(`Dataset ${evaluation.datasetName} not found`) + returnObj.errors.push({ + message: `Dataset ${evaluation.datasetName} not found`, + id: evaluation.datasetId + }) isOutdated = true } - const chatflows = JSON.parse(evaluation.chatflowId) - const chatflowNames = JSON.parse(evaluation.chatflowName) - - for (let i = 0; i < chatflows.length; i++) { + const chatflowIds = evaluation.chatflowId ? JSON.parse(evaluation.chatflowId) : [] + const chatflowNames = evaluation.chatflowName ? JSON.parse(evaluation.chatflowName) : [] + const chatflowTypes = evaluation.additionalConfig ? JSON.parse(evaluation.additionalConfig).chatflowTypes : [] + for (let i = 0; i < chatflowIds.length; i++) { + // check for backward compatibility, as previous versions did not the types in additionalConfig + if (chatflowTypes && chatflowTypes.length >= 0) { + if (chatflowTypes[i] === 'Custom Assistant') { + // if the chatflow type is custom assistant, then we should NOT check in the chatflows table + continue + } + } const chatflow = await appServer.AppDataSource.getRepository(ChatFlow).findOneBy({ - id: chatflows[i] + id: chatflowIds[i] }) if (!chatflow) { - returnObj.errors.push(`Chatflow ${chatflowNames[i]} not found`) + returnObj.errors.push({ + message: `Chatflow ${chatflowNames[i]} not found`, + id: chatflowIds[i] + }) isOutdated = true } else { const chatflowLastUpdated = chatflow.updatedDate.getTime() @@ -397,12 +459,42 @@ const isOutdated = async (id: string) => { isOutdated = true returnObj.chatflows.push({ chatflowName: chatflowNames[i], - chatflowId: chatflows[i], + chatflowId: chatflowIds[i], + chatflowType: chatflow.type === 'AGENTFLOW' ? 'Agentflow v2' : 'Chatflow', isOutdated: true }) } } } + if (chatflowTypes && chatflowTypes.length > 0) { + for (let i = 0; i < chatflowIds.length; i++) { + if (chatflowTypes[i] !== 'Custom Assistant') { + // if the chatflow type is NOT custom assistant, then bail out for this item + continue + } + const assistant = await appServer.AppDataSource.getRepository(Assistant).findOneBy({ + id: chatflowIds[i] + }) + if (!assistant) { + returnObj.errors.push({ + message: `Custom Assistant ${chatflowNames[i]} not found`, + id: chatflowIds[i] + }) + isOutdated = true + } else { + const chatflowLastUpdated = assistant.updatedDate.getTime() + if (chatflowLastUpdated > evaluationRunDate) { + isOutdated = true + returnObj.chatflows.push({ + chatflowName: chatflowNames[i], + chatflowId: chatflowIds[i], + chatflowType: 'Custom Assistant', + isOutdated: true + }) + } + } + } + } returnObj.isOutdated = isOutdated return returnObj } catch (error) { @@ -424,7 +516,7 @@ const getEvaluation = async (id: string) => { where: { evaluationId: id } }) const versions = (await getVersions(id)).versions - const versionNo = versions.findIndex((version: any) => version.id === id) + 1 + const versionNo = versions.findIndex((version) => version.id === id) + 1 return { ...evaluation, versionCount: versionCount, @@ -451,7 +543,7 @@ const getVersions = async (id: string) => { runDate: 'ASC' } }) - const returnResults: any[] = [] + const returnResults: { id: string; runDate: Date; version: number }[] = [] versions.map((version, index) => { returnResults.push({ id: version.id, diff --git a/packages/server/src/services/openai-realtime/index.ts b/packages/server/src/services/openai-realtime/index.ts index f16c96b1934..f5126e97720 100644 --- a/packages/server/src/services/openai-realtime/index.ts +++ b/packages/server/src/services/openai-realtime/index.ts @@ -23,6 +23,7 @@ import { Organization } from '../../enterprise/database/entities/organization.en const SOURCE_DOCUMENTS_PREFIX = '\n\n----FLOWISE_SOURCE_DOCUMENTS----\n\n' const ARTIFACTS_PREFIX = '\n\n----FLOWISE_ARTIFACTS----\n\n' +const TOOL_ARGS_PREFIX = '\n\n----FLOWISE_TOOL_ARGS----\n\n' const buildAndInitTool = async (chatflowid: string, _chatId?: string, _apiMessageId?: string) => { const appServer = getRunningExpressApp() @@ -211,6 +212,11 @@ const executeAgentTool = async ( } } + if (typeof toolOutput === 'string' && toolOutput.includes(TOOL_ARGS_PREFIX)) { + const _splitted = toolOutput.split(TOOL_ARGS_PREFIX) + toolOutput = _splitted[0] + } + return { output: toolOutput, sourceDocuments, diff --git a/packages/server/src/utils/buildAgentflow.ts b/packages/server/src/utils/buildAgentflow.ts index 4ce91285a57..40953bf3d92 100644 --- a/packages/server/src/utils/buildAgentflow.ts +++ b/packages/server/src/utils/buildAgentflow.ts @@ -1503,7 +1503,14 @@ export const executeAgentFlow = async ({ try { if (chatflow.analytic) { - analyticHandlers = AnalyticHandler.getInstance({ inputs: {} } as any, { + // Override config analytics + let analyticInputs: ICommonObject = {} + if (overrideConfig?.analytics && Object.keys(overrideConfig.analytics).length > 0) { + analyticInputs = { + ...overrideConfig.analytics + } + } + analyticHandlers = AnalyticHandler.getInstance({ inputs: { analytics: analyticInputs } } as any, { orgId, workspaceId, appDataSource, @@ -1798,7 +1805,7 @@ export const executeAgentFlow = async ({ role: 'userMessage', content: finalUserInput, chatflowid, - chatType: isInternal ? ChatType.INTERNAL : ChatType.EXTERNAL, + chatType: evaluationRunId ? ChatType.EVALUATION : isInternal ? ChatType.INTERNAL : ChatType.EXTERNAL, chatId, sessionId, createdDate: userMessageDateTime, @@ -1813,7 +1820,7 @@ export const executeAgentFlow = async ({ role: 'apiMessage', content: content, chatflowid, - chatType: isInternal ? ChatType.INTERNAL : ChatType.EXTERNAL, + chatType: evaluationRunId ? ChatType.EVALUATION : isInternal ? ChatType.INTERNAL : ChatType.EXTERNAL, chatId, sessionId, executionId: newExecution.id @@ -1849,7 +1856,7 @@ export const executeAgentFlow = async ({ version: await getAppVersion(), chatflowId: chatflowid, chatId, - type: isInternal ? ChatType.INTERNAL : ChatType.EXTERNAL, + type: evaluationRunId ? ChatType.EVALUATION : isInternal ? ChatType.INTERNAL : ChatType.EXTERNAL, flowGraph: getTelemetryFlowObj(nodes, edges) }, orgId diff --git a/packages/server/src/utils/buildChatflow.ts b/packages/server/src/utils/buildChatflow.ts index b138922ea76..df5b64e64e7 100644 --- a/packages/server/src/utils/buildChatflow.ts +++ b/packages/server/src/utils/buildChatflow.ts @@ -551,7 +551,7 @@ export const executeFlow = async ({ role: 'userMessage', content: incomingInput.question, chatflowid: agentflow.id, - chatType: isInternal ? ChatType.INTERNAL : ChatType.EXTERNAL, + chatType: isEvaluation ? ChatType.EVALUATION : isInternal ? ChatType.INTERNAL : ChatType.EXTERNAL, chatId, memoryType, sessionId, @@ -566,7 +566,7 @@ export const executeFlow = async ({ role: 'apiMessage', content: finalResult, chatflowid: agentflow.id, - chatType: isInternal ? ChatType.INTERNAL : ChatType.EXTERNAL, + chatType: isEvaluation ? ChatType.EVALUATION : isInternal ? ChatType.INTERNAL : ChatType.EXTERNAL, chatId, memoryType, sessionId @@ -598,7 +598,7 @@ export const executeFlow = async ({ version: await getAppVersion(), agentflowId: agentflow.id, chatId, - type: isInternal ? ChatType.INTERNAL : ChatType.EXTERNAL, + type: isEvaluation ? ChatType.EVALUATION : isInternal ? ChatType.INTERNAL : ChatType.EXTERNAL, flowGraph: getTelemetryFlowObj(nodes, edges) }, orgId @@ -807,7 +807,7 @@ export const executeFlow = async ({ version: await getAppVersion(), chatflowId: chatflowid, chatId, - type: isInternal ? ChatType.INTERNAL : ChatType.EXTERNAL, + type: isEvaluation ? ChatType.EVALUATION : isInternal ? ChatType.INTERNAL : ChatType.EXTERNAL, flowGraph: getTelemetryFlowObj(nodes, edges) }, orgId @@ -905,17 +905,17 @@ export const utilBuildChatflow = async (req: Request, isInternal: boolean = fals const isTool = req.get('flowise-tool') === 'true' const isEvaluation: boolean = req.headers['X-Flowise-Evaluation'] || req.body.evaluation let evaluationRunId = '' - if (isEvaluation) { - evaluationRunId = req.body.evaluationRunId - if (evaluationRunId) { - const newEval = { - evaluation: { - status: true, - evaluationRunId - } + evaluationRunId = req.body.evaluationRunId + if (isEvaluation && chatflow.type !== 'AGENTFLOW' && req.body.evaluationRunId) { + // this is needed for the collection of token metrics for non-agent flows, + // for agentflows the execution trace has the info needed + const newEval = { + evaluation: { + status: true, + evaluationRunId } - chatflow.analytic = JSON.stringify(newEval) } + chatflow.analytic = JSON.stringify(newEval) } try { diff --git a/packages/server/src/utils/constants.ts b/packages/server/src/utils/constants.ts index 3d3d6796697..52cfa4023f4 100644 --- a/packages/server/src/utils/constants.ts +++ b/packages/server/src/utils/constants.ts @@ -39,6 +39,8 @@ export const WHITELIST_URLS = [ '/api/v1/loginmethod', '/api/v1/pricing', '/api/v1/user/test', + '/api/v1/oauth2-credential/callback', + '/api/v1/oauth2-credential/refresh', AzureSSO.LOGIN_URI, AzureSSO.LOGOUT_URI, AzureSSO.CALLBACK_URI, diff --git a/packages/ui/package.json b/packages/ui/package.json index f3ed1bbdaeb..0719d7b5c64 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "flowise-ui", - "version": "3.0.1", + "version": "3.0.2", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://flowiseai.com", "author": { @@ -68,6 +68,7 @@ "rehype-raw": "^7.0.0", "remark-gfm": "^3.0.1", "remark-math": "^5.1.1", + "showdown": "^2.1.0", "tippy.js": "^6.3.7", "uuid": "^9.0.1", "yup": "^0.32.9" diff --git a/packages/ui/src/api/oauth2.js b/packages/ui/src/api/oauth2.js new file mode 100644 index 00000000000..6546a504bba --- /dev/null +++ b/packages/ui/src/api/oauth2.js @@ -0,0 +1,13 @@ +import client from './client' + +const authorize = (credentialId) => client.post(`/oauth2-credential/authorize/${credentialId}`) + +const refresh = (credentialId) => client.post(`/oauth2-credential/refresh/${credentialId}`) + +const getCallback = (queryParams) => client.get(`/oauth2-credential/callback?${queryParams}`) + +export default { + authorize, + refresh, + getCallback +} diff --git a/packages/ui/src/store/actions.js b/packages/ui/src/store/actions.js index 0c68f8f23ba..c6ace4a6422 100644 --- a/packages/ui/src/store/actions.js +++ b/packages/ui/src/store/actions.js @@ -32,6 +32,11 @@ export const enqueueSnackbar = (notification) => { type: ENQUEUE_SNACKBAR, notification: { ...notification, + options: { + ...notification.options, + persist: notification.options?.persist ?? false, // Default: auto-close enabled + autoHideDuration: notification.options?.autoHideDuration ?? 5000 // Default auto-close duration: 5 seconds + }, key: key || new Date().getTime() + Math.random() } } diff --git a/packages/ui/src/store/reducers/authSlice.js b/packages/ui/src/store/reducers/authSlice.js index 2136346899f..6397949cf56 100644 --- a/packages/ui/src/store/reducers/authSlice.js +++ b/packages/ui/src/store/reducers/authSlice.js @@ -11,7 +11,10 @@ const initialState = { localStorage.getItem('permissions') && localStorage.getItem('permissions') !== 'undefined' ? JSON.parse(localStorage.getItem('permissions')) : null, - features: localStorage.getItem('features') ? JSON.parse(localStorage.getItem('features')) : null + features: + localStorage.getItem('features') && localStorage.getItem('features') !== 'undefined' + ? JSON.parse(localStorage.getItem('features')) + : null } const authSlice = createSlice({ diff --git a/packages/ui/src/ui-component/dialog/AgentflowGeneratorDialog.jsx b/packages/ui/src/ui-component/dialog/AgentflowGeneratorDialog.jsx index 2819e2e6247..6495e6bf83c 100644 --- a/packages/ui/src/ui-component/dialog/AgentflowGeneratorDialog.jsx +++ b/packages/ui/src/ui-component/dialog/AgentflowGeneratorDialog.jsx @@ -15,7 +15,7 @@ import { Dropdown } from '@/ui-component/dropdown/Dropdown' import { useTheme } from '@mui/material/styles' import assistantsApi from '@/api/assistants' import { baseURL } from '@/store/constant' -import { initNode } from '@/utils/genericHelper' +import { initNode, showHideInputParams } from '@/utils/genericHelper' import DocStoreInputHandler from '@/views/docstore/DocStoreInputHandler' import useApi from '@/hooks/useApi' @@ -55,6 +55,15 @@ const AgentflowGeneratorDialog = ({ show, dialogProps, onCancel, onConfirm }) => const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)) const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) + const handleChatModelDataChange = ({ inputParam, newValue }) => { + setSelectedChatModel((prevData) => { + const updatedData = { ...prevData } + updatedData.inputs[inputParam.name] = newValue + updatedData.inputParams = showHideInputParams(updatedData) + return updatedData + }) + } + useEffect(() => { if (getChatModelsApi.data) { setChatModelsComponents(getChatModelsApi.data) @@ -122,7 +131,7 @@ const AgentflowGeneratorDialog = ({ show, dialogProps, onCancel, onConfirm }) => options: { key: new Date().getTime() + Math.random(), variant: 'error', - persist: true, + persist: false, action: (key) => ( + ) + } + }) + onConfirm(credentialId) + } else if (event.data.type === 'OAUTH2_ERROR') { + enqueueSnackbar({ + message: event.data.message || 'OAuth2 authorization failed', + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + persist: true, + action: (key) => ( + + ) + } + }) + } + + // Close the auth window if it's still open + if (authWindow && !authWindow.closed) { + authWindow.close() + } + } + } + + // Add message listener + window.addEventListener('message', handleMessage) + + // Fallback: Monitor the auth window and handle if it closes manually + const checkClosed = setInterval(() => { + if (authWindow.closed) { + clearInterval(checkClosed) + window.removeEventListener('message', handleMessage) + + // If no message was received, assume user closed window manually + // Don't show error in this case, just close dialog + onConfirm(credentialId) + } + }, 1000) + + // Cleanup after a reasonable timeout (5 minutes) + setTimeout(() => { + clearInterval(checkClosed) + window.removeEventListener('message', handleMessage) + if (authWindow && !authWindow.closed) { + authWindow.close() + } + }, 300000) // 5 minutes + } else { + throw new Error('Invalid response from authorization endpoint') + } + } catch (error) { + console.error('OAuth2 authorization error:', error) + if (setError) setError(error) + enqueueSnackbar({ + message: `OAuth2 authorization failed: ${error.response?.data?.message || error.message || 'Unknown error'}`, + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + persist: true, + action: (key) => ( + + ) + } + }) + } + } + const component = show ? ( )} + {!shared && componentCredential && componentCredential.name && componentCredential.name.includes('OAuth2') && ( + + + OAuth Redirect URL + + + + )} {!shared && componentCredential && componentCredential.inputs && - componentCredential.inputs.map((inputParam, index) => ( - - ))} + componentCredential.inputs + .filter((inputParam) => inputParam.hidden !== true) + .map((inputParam, index) => )} + + {!shared && componentCredential && componentCredential.name && componentCredential.name.includes('OAuth2') && ( + + + + )} {!shared && ( diff --git a/packages/ui/src/views/credentials/CredentialListDialog.jsx b/packages/ui/src/views/credentials/CredentialListDialog.jsx index d5d1058d2e7..edf5d6e23dd 100644 --- a/packages/ui/src/views/credentials/CredentialListDialog.jsx +++ b/packages/ui/src/views/credentials/CredentialListDialog.jsx @@ -140,7 +140,11 @@ const CredentialListDialog = ({ show, dialogProps, onCancel, onCredentialSelecte width: 50, height: 50, borderRadius: '50%', - backgroundColor: 'white' + backgroundColor: 'white', + flexShrink: 0, + display: 'flex', + alignItems: 'center', + justifyContent: 'center' }} > { +const DocStoreInputHandler = ({ inputParam, data, disabled = false, onNodeDataChange }) => { const customization = useSelector((state) => state.customization) + const flowContextValue = useContext(flowContext) + const nodeDataChangeHandler = onNodeDataChange || flowContextValue?.onNodeDataChange const [showExpandDialog, setShowExpandDialog] = useState(false) const [expandDialogProps, setExpandDialogProps] = useState({}) @@ -35,6 +38,14 @@ const DocStoreInputHandler = ({ inputParam, data, disabled = false }) => { const [manageScrapedLinksDialogProps, setManageScrapedLinksDialogProps] = useState({}) const [reloadTimestamp, setReloadTimestamp] = useState(Date.now().toString()) + const handleDataChange = ({ inputParam, newValue }) => { + data.inputs[inputParam.name] = newValue + const allowedShowHideInputTypes = ['boolean', 'asyncOptions', 'asyncMultiOptions', 'options', 'multiOptions'] + if (allowedShowHideInputTypes.includes(inputParam.type) && nodeDataChangeHandler) { + nodeDataChangeHandler({ nodeId: data.id, inputParam, newValue }) + } + } + const onExpandDialogClicked = (value, inputParam) => { const dialogProps = { value, @@ -149,7 +160,7 @@ const DocStoreInputHandler = ({ inputParam, data, disabled = false }) => { {inputParam.type === 'boolean' && ( (data.inputs[inputParam.name] = newValue)} + onChange={(newValue) => handleDataChange({ inputParam, newValue })} value={data.inputs[inputParam.name] ?? inputParam.default ?? false} /> )} @@ -203,7 +214,7 @@ const DocStoreInputHandler = ({ inputParam, data, disabled = false }) => { disabled={disabled} name={inputParam.name} options={inputParam.options} - onSelect={(newValue) => (data.inputs[inputParam.name] = newValue)} + onSelect={(newValue) => handleDataChange({ inputParam, newValue })} value={data.inputs[inputParam.name] ?? inputParam.default ?? 'choose an option'} /> )} @@ -213,7 +224,7 @@ const DocStoreInputHandler = ({ inputParam, data, disabled = false }) => { disabled={disabled} name={inputParam.name} options={inputParam.options} - onSelect={(newValue) => (data.inputs[inputParam.name] = newValue)} + onSelect={(newValue) => handleDataChange({ inputParam, newValue })} value={data.inputs[inputParam.name] ?? inputParam.default ?? 'choose an option'} /> )} @@ -230,7 +241,7 @@ const DocStoreInputHandler = ({ inputParam, data, disabled = false }) => { freeSolo={inputParam.freeSolo} multiple={inputParam.type === 'asyncMultiOptions'} value={data.inputs[inputParam.name] ?? inputParam.default ?? 'choose an option'} - onSelect={(newValue) => (data.inputs[inputParam.name] = newValue)} + onSelect={(newValue) => handleDataChange({ inputParam, newValue })} onCreateNew={() => addAsyncOption(inputParam.name)} /> @@ -296,7 +307,8 @@ const DocStoreInputHandler = ({ inputParam, data, disabled = false }) => { DocStoreInputHandler.propTypes = { inputParam: PropTypes.object, data: PropTypes.object, - disabled: PropTypes.bool + disabled: PropTypes.bool, + onNodeDataChange: PropTypes.func } export default DocStoreInputHandler diff --git a/packages/ui/src/views/docstore/DocumentLoaderListDialog.jsx b/packages/ui/src/views/docstore/DocumentLoaderListDialog.jsx index a8722ed358d..8a1caa38d48 100644 --- a/packages/ui/src/views/docstore/DocumentLoaderListDialog.jsx +++ b/packages/ui/src/views/docstore/DocumentLoaderListDialog.jsx @@ -153,7 +153,11 @@ const DocumentLoaderListDialog = ({ show, dialogProps, onCancel, onDocLoaderSele width: 50, height: 50, borderRadius: '50%', - backgroundColor: 'white' + backgroundColor: 'white', + flexShrink: 0, + display: 'flex', + alignItems: 'center', + justifyContent: 'center' }} > ({ @@ -98,6 +98,24 @@ const LoaderConfigPreviewChunks = () => { const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)) const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) + const handleDocumentLoaderDataChange = ({ inputParam, newValue }) => { + setSelectedDocumentLoader((prevData) => { + const updatedData = { ...prevData } + updatedData.inputs[inputParam.name] = newValue + updatedData.inputParams = showHideInputParams(updatedData) + return updatedData + }) + } + + const handleTextSplitterDataChange = ({ inputParam, newValue }) => { + setSelectedTextSplitter((prevData) => { + const updatedData = { ...prevData } + updatedData.inputs[inputParam.name] = newValue + updatedData.inputParams = showHideInputParams(updatedData) + return updatedData + }) + } + const onSplitterChange = (name) => { const textSplitter = (textSplitterNodes ?? []).find((splitter) => splitter.name === name) if (textSplitter) { @@ -452,13 +470,14 @@ const LoaderConfigPreviewChunks = () => { {selectedDocumentLoader && Object.keys(selectedDocumentLoader).length > 0 && - (selectedDocumentLoader.inputParams ?? []) - .filter((inputParam) => !inputParam.hidden) + showHideInputParams(selectedDocumentLoader) + .filter((inputParam) => !inputParam.hidden && inputParam.display !== false) .map((inputParam, index) => ( ))} {textSplitterNodes && textSplitterNodes.length > 0 && ( @@ -511,10 +530,15 @@ const LoaderConfigPreviewChunks = () => { )} {Object.keys(selectedTextSplitter).length > 0 && - (selectedTextSplitter.inputParams ?? []) - .filter((inputParam) => !inputParam.hidden) + showHideInputParams(selectedTextSplitter) + .filter((inputParam) => !inputParam.hidden && inputParam.display !== false) .map((inputParam, index) => ( - + ))} diff --git a/packages/ui/src/views/docstore/VectorStoreConfigure.jsx b/packages/ui/src/views/docstore/VectorStoreConfigure.jsx index e0cbcc4f75f..f05759a8eb5 100644 --- a/packages/ui/src/views/docstore/VectorStoreConfigure.jsx +++ b/packages/ui/src/views/docstore/VectorStoreConfigure.jsx @@ -40,7 +40,7 @@ import Storage from '@mui/icons-material/Storage' import DynamicFeed from '@mui/icons-material/Filter1' // utils -import { initNode } from '@/utils/genericHelper' +import { initNode, showHideInputParams } from '@/utils/genericHelper' import useNotifier from '@/utils/useNotifier' // const @@ -89,6 +89,33 @@ const VectorStoreConfigure = () => { const [showUpsertHistoryDetailsDialog, setShowUpsertHistoryDetailsDialog] = useState(false) const [upsertDetailsDialogProps, setUpsertDetailsDialogProps] = useState({}) + const handleEmbeddingsProviderDataChange = ({ inputParam, newValue }) => { + setSelectedEmbeddingsProvider((prevData) => { + const updatedData = { ...prevData } + updatedData.inputs[inputParam.name] = newValue + updatedData.inputParams = showHideInputParams(updatedData) + return updatedData + }) + } + + const handleVectorStoreProviderDataChange = ({ inputParam, newValue }) => { + setSelectedVectorStoreProvider((prevData) => { + const updatedData = { ...prevData } + updatedData.inputs[inputParam.name] = newValue + updatedData.inputParams = showHideInputParams(updatedData) + return updatedData + }) + } + + const handleRecordManagerProviderDataChange = ({ inputParam, newValue }) => { + setSelectedRecordManagerProvider((prevData) => { + const updatedData = { ...prevData } + updatedData.inputs[inputParam.name] = newValue + updatedData.inputParams = showHideInputParams(updatedData) + return updatedData + }) + } + const onEmbeddingsSelected = (component) => { const nodeData = cloneDeep(initNode(component, uuidv4())) if (!showEmbeddingsListDialog && documentStore.embeddingConfig) { @@ -599,14 +626,17 @@ const VectorStoreConfigure = () => { {selectedEmbeddingsProvider && Object.keys(selectedEmbeddingsProvider).length > 0 && - (selectedEmbeddingsProvider.inputParams ?? []) - .filter((inputParam) => !inputParam.hidden) + showHideInputParams(selectedEmbeddingsProvider) + .filter( + (inputParam) => !inputParam.hidden && inputParam.display !== false + ) .map((inputParam, index) => ( ))} @@ -714,14 +744,17 @@ const VectorStoreConfigure = () => { {selectedVectorStoreProvider && Object.keys(selectedVectorStoreProvider).length > 0 && - (selectedVectorStoreProvider.inputParams ?? []) - .filter((inputParam) => !inputParam.hidden) + showHideInputParams(selectedVectorStoreProvider) + .filter( + (inputParam) => !inputParam.hidden && inputParam.display !== false + ) .map((inputParam, index) => ( ))} @@ -837,17 +870,18 @@ const VectorStoreConfigure = () => { {selectedRecordManagerProvider && Object.keys(selectedRecordManagerProvider).length > 0 && - (selectedRecordManagerProvider.inputParams ?? []) - .filter((inputParam) => !inputParam.hidden) + showHideInputParams(selectedRecordManagerProvider) + .filter( + (inputParam) => !inputParam.hidden && inputParam.display !== false + ) .map((inputParam, index) => ( - <> - - + ))} diff --git a/packages/ui/src/views/docstore/VectorStoreQuery.jsx b/packages/ui/src/views/docstore/VectorStoreQuery.jsx index 6e0fd1b0ead..f0650ae41e7 100644 --- a/packages/ui/src/views/docstore/VectorStoreQuery.jsx +++ b/packages/ui/src/views/docstore/VectorStoreQuery.jsx @@ -31,7 +31,7 @@ import useApi from '@/hooks/useApi' import { useAuth } from '@/hooks/useAuth' import useNotifier from '@/utils/useNotifier' import { baseURL } from '@/store/constant' -import { initNode } from '@/utils/genericHelper' +import { initNode, showHideInputParams } from '@/utils/genericHelper' import { closeSnackbar as closeSnackbarAction, enqueueSnackbar as enqueueSnackbarAction } from '@/store/actions' const CardWrapper = styled(MainCard)(({ theme }) => ({ @@ -84,6 +84,15 @@ const VectorStoreQuery = () => { const getVectorStoreNodeDetailsApi = useApi(nodesApi.getSpecificNode) const [selectedVectorStoreProvider, setSelectedVectorStoreProvider] = useState({}) + const handleVectorStoreProviderDataChange = ({ inputParam, newValue }) => { + setSelectedVectorStoreProvider((prevData) => { + const updatedData = { ...prevData } + updatedData.inputs[inputParam.name] = newValue + updatedData.inputParams = showHideInputParams(updatedData) + return updatedData + }) + } + const chunkSelected = (chunkId, selectedChunkNumber) => { const selectedChunk = documentChunks.find((chunk) => chunk.id === chunkId) const dialogProps = { @@ -354,14 +363,15 @@ const VectorStoreQuery = () => { {selectedVectorStoreProvider && Object.keys(selectedVectorStoreProvider).length > 0 && - (selectedVectorStoreProvider.inputParams ?? []) - .filter((inputParam) => !inputParam.hidden) + showHideInputParams(selectedVectorStoreProvider) + .filter((inputParam) => !inputParam.hidden && inputParam.display !== false) .map((inputParam, index) => ( ))} diff --git a/packages/ui/src/views/evaluations/CreateEvaluationDialog.jsx b/packages/ui/src/views/evaluations/CreateEvaluationDialog.jsx index a18453989c5..1eb8df6549b 100644 --- a/packages/ui/src/views/evaluations/CreateEvaluationDialog.jsx +++ b/packages/ui/src/views/evaluations/CreateEvaluationDialog.jsx @@ -21,7 +21,8 @@ import { Switch, StepLabel, IconButton, - FormControlLabel + FormControlLabel, + Checkbox } from '@mui/material' import { useTheme } from '@mui/material/styles' @@ -42,6 +43,7 @@ import useApi from '@/hooks/useApi' import datasetsApi from '@/api/dataset' import evaluatorsApi from '@/api/evaluators' import nodesApi from '@/api/nodes' +import assistantsApi from '@/api/assistants' // utils import useNotifier from '@/utils/useNotifier' @@ -57,14 +59,18 @@ const CreateEvaluationDialog = ({ show, dialogProps, onCancel, onConfirm }) => { useNotifier() const getAllChatflowsApi = useApi(chatflowsApi.getAllChatflows) + const getAllAgentflowsApi = useApi(chatflowsApi.getAllAgentflows) + const getAllDatasetsApi = useApi(datasetsApi.getAllDatasets) const getAllEvaluatorsApi = useApi(evaluatorsApi.getAllEvaluators) const getNodesByCategoryApi = useApi(nodesApi.getNodesByCategory) const getModelsApi = useApi(nodesApi.executeNodeLoadMethod) + const getAssistantsApi = useApi(assistantsApi.getAllAssistants) const [chatflow, setChatflow] = useState([]) const [dataset, setDataset] = useState('') const [datasetAsOneConversation, setDatasetAsOneConversation] = useState(false) + const [flowTypes, setFlowTypes] = useState([]) const [flows, setFlows] = useState([]) const [datasets, setDatasets] = useState([]) @@ -163,6 +169,10 @@ const CreateEvaluationDialog = ({ show, dialogProps, onCancel, onConfirm }) => { for (let i = 0; i < selectedChatflows.length; i += 1) { selectedChatflowNames.push(flows.find((f) => f.name === selectedChatflows[i])?.label) } + const selectedChatflowTypes = [] + for (let i = 0; i < selectedChatflows.length; i += 1) { + selectedChatflowTypes.push(flows.find((f) => f.name === selectedChatflows[i])?.type) + } const chatflowName = JSON.stringify(selectedChatflowNames) const datasetName = datasets.find((f) => f.name === dataset)?.label const obj = { @@ -173,6 +183,7 @@ const CreateEvaluationDialog = ({ show, dialogProps, onCancel, onConfirm }) => { datasetName: datasetName, chatflowId: chatflow, chatflowName: chatflowName, + chatflowType: JSON.stringify(selectedChatflowTypes), selectedSimpleEvaluators: selectedSimpleEvaluators, selectedLLMEvaluators: selectedLLMEvaluators, model: selectedModel, @@ -216,6 +227,8 @@ const CreateEvaluationDialog = ({ show, dialogProps, onCancel, onConfirm }) => { getNodesByCategoryApi.request('Chat Models') if (flows.length === 0) { getAllChatflowsApi.request() + getAssistantsApi.request('CUSTOM') + getAllAgentflowsApi.request('AGENTFLOW') } if (datasets.length === 0) { getAllDatasetsApi.request() @@ -225,23 +238,18 @@ const CreateEvaluationDialog = ({ show, dialogProps, onCancel, onConfirm }) => { }, []) useEffect(() => { - if (getAllChatflowsApi.data) { + if (getAllAgentflowsApi.data && getAllChatflowsApi.data && getAssistantsApi.data) { try { - const chatflows = getAllChatflowsApi.data - let flowNames = [] - for (let i = 0; i < chatflows.length; i += 1) { - const flow = chatflows[i] - flowNames.push({ - label: flow.name, - name: flow.id - }) - } - setFlows(flowNames) + const agentFlows = populateFlowNames(getAllAgentflowsApi.data, 'Agentflow v2') + const chatFlows = populateFlowNames(getAllChatflowsApi.data, 'Chatflow') + const assistants = populateAssistants(getAssistantsApi.data) + setFlows([...agentFlows, ...chatFlows, ...assistants]) + setFlowTypes(['Agentflow v2', 'Chatflow', 'Custom Assistant']) } catch (e) { console.error(e) } } - }, [getAllChatflowsApi.data]) + }, [getAllAgentflowsApi.data, getAllChatflowsApi.data, getAssistantsApi.data]) useEffect(() => { if (getNodesByCategoryApi.data) { @@ -337,6 +345,44 @@ const CreateEvaluationDialog = ({ show, dialogProps, onCancel, onConfirm }) => { if (llm !== 'no_grading') getModelsApi.request(llm, { loadMethod: 'listModels' }) } + const onChangeFlowType = (flowType) => { + const selected = flowType.target.checked + const flowTypeValue = flowType.target.value + if (selected) { + setFlowTypes([...flowTypes, flowTypeValue]) + } else { + setFlowTypes(flowTypes.filter((f) => f !== flowTypeValue)) + } + } + + const populateFlowNames = (data, type) => { + let flowNames = [] + for (let i = 0; i < data.length; i += 1) { + const flow = data[i] + flowNames.push({ + label: flow.name, + name: flow.id, + type: type, + description: type + }) + } + return flowNames + } + + const populateAssistants = (assistants) => { + let assistantNames = [] + for (let i = 0; i < assistants.length; i += 1) { + const assistant = assistants[i] + assistantNames.push({ + label: JSON.parse(assistant.details).name || '', + name: assistant.id, + type: 'Custom Assistant', + description: 'Custom Assistant' + }) + } + return assistantNames + } + const component = show ? ( { Treat all dataset rows as one conversation ? } value={datasetAsOneConversation} onChange={() => setDatasetAsOneConversation(!datasetAsOneConversation)} /> - - Chatflow(s) to Evaluate * - +
+ + Select your flows to Evaluate +  * + + + {' '} + Chatflows + {' '} + Agentflows (v2) + {' '} + Custom Assistants + +
flowTypes.includes(f.type))} onSelect={(newValue) => setChatflow(newValue)} value={chatflow ?? chatflow ?? 'choose an option'} /> diff --git a/packages/ui/src/views/evaluations/EvalsResultDialog.jsx b/packages/ui/src/views/evaluations/EvalsResultDialog.jsx index 3fb013f0de4..a74c2b76eb4 100644 --- a/packages/ui/src/views/evaluations/EvalsResultDialog.jsx +++ b/packages/ui/src/views/evaluations/EvalsResultDialog.jsx @@ -2,7 +2,6 @@ import React from 'react' import { createPortal } from 'react-dom' import PropTypes from 'prop-types' import { useSelector } from 'react-redux' -import { useNavigate } from 'react-router-dom' // Material import { @@ -36,7 +35,6 @@ const EvalsResultDialog = ({ show, dialogProps, onCancel, openDetailsDrawer }) = const portalElement = document.getElementById('portal') const customization = useSelector((state) => state.customization) const theme = useTheme() - const navigate = useNavigate() const getColSpan = (evaluationsShown, llmEvaluations) => { let colSpan = 1 @@ -45,6 +43,23 @@ const EvalsResultDialog = ({ show, dialogProps, onCancel, openDetailsDrawer }) = return colSpan } + const getOpenLink = (index) => { + if (index === undefined) { + return '' + } + if (dialogProps.data?.additionalConfig?.chatflowTypes) { + switch (dialogProps.data.additionalConfig.chatflowTypes[index]) { + case 'Chatflow': + return '/canvas/' + dialogProps.data.evaluation.chatflowId[index] + case 'Custom Assistant': + return '/assistants/custom/' + dialogProps.data.evaluation.chatflowId[index] + case 'Agentflow v2': + return '/v2/agentcanvas/' + dialogProps.data.evaluation.chatflowId[index] + } + } + return '/canvas/' + dialogProps.data.evaluation.chatflowId[index] + } + const component = show ? ( @@ -65,7 +80,7 @@ const EvalsResultDialog = ({ show, dialogProps, onCancel, openDetailsDrawer }) = }} > - Chatflows Used: + Flows Used: {(dialogProps.data.evaluation.chatflowName || []).map((chatflowUsed, index) => ( navigate('/canvas/' + dialogProps.data.evaluation.chatflowId[index])} + onClick={() => window.open(getOpenLink(index), '_blank')} > ))} diff --git a/packages/ui/src/views/evaluations/EvaluationResult.jsx b/packages/ui/src/views/evaluations/EvaluationResult.jsx index e6d35079b4d..6fdde95b759 100644 --- a/packages/ui/src/views/evaluations/EvaluationResult.jsx +++ b/packages/ui/src/views/evaluations/EvaluationResult.jsx @@ -25,6 +25,7 @@ import { import { useTheme } from '@mui/material/styles' import moment from 'moment' import PaidIcon from '@mui/icons-material/Paid' +import { IconHierarchy, IconUsersGroup, IconRobot } from '@tabler/icons-react' import LLMIcon from '@mui/icons-material/ModelTraining' import AlarmIcon from '@mui/icons-material/AlarmOn' import TokensIcon from '@mui/icons-material/AutoAwesomeMotion' @@ -116,10 +117,13 @@ const EvalEvaluationRows = () => { const [expandTableProps, setExpandTableProps] = useState({}) const [isTableLoading, setTableLoading] = useState(false) + const [additionalConfig, setAdditionalConfig] = useState({}) + const openDetailsDrawer = (item) => { setSideDrawerDialogProps({ type: 'View', data: item, + additionalConfig: additionalConfig, evaluationType: evaluation.evaluationType, evaluationChatflows: evaluation.chatflowName }) @@ -169,7 +173,8 @@ const EvalEvaluationRows = () => { showCustomEvals, showTokenMetrics, showLatencyMetrics, - showCostMetrics + showCostMetrics, + additionalConfig } }) setShowExpandTableDialog(true) @@ -239,6 +244,9 @@ const EvalEvaluationRows = () => { const data = getEvaluation.data setSelectedEvaluationName(data.name) getIsOutdatedApi.request(data.id) + if (data.additionalConfig) { + setAdditionalConfig(JSON.parse(data.additionalConfig)) + } data.chatflowId = typeof data.chatflowId === 'object' ? data.chatflowId : JSON.parse(data.chatflowId) data.chatflowName = typeof data.chatflowName === 'object' ? data.chatflowName : JSON.parse(data.chatflowName) const rows = getEvaluation.data.rows @@ -314,6 +322,51 @@ const EvalEvaluationRows = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [getEvaluation.data]) + const getOpenLink = (index) => { + if (index === undefined) { + return undefined + } + const id = evaluation.chatflowId[index] + // this is to check if the evaluation is deleted! + if (outdated?.errors?.length > 0 && outdated.errors.find((e) => e.id === id)) { + return undefined + } + if (additionalConfig.chatflowTypes) { + switch (additionalConfig.chatflowTypes[index]) { + case 'Chatflow': + return '/canvas/' + evaluation.chatflowId[index] + case 'Custom Assistant': + return '/assistants/custom/' + evaluation.chatflowId[index] + case 'Agentflow v2': + return '/v2/agentcanvas/' + evaluation.chatflowId[index] + } + } + return '/canvas/' + evaluation.chatflowId[index] + } + + const openFlow = (index) => { + const url = getOpenLink(index) + if (url) { + window.open(getOpenLink(index), '_blank') + } + } + + const getFlowIcon = (index) => { + if (index === undefined) { + return + } + if (additionalConfig.chatflowTypes) { + switch (additionalConfig.chatflowTypes[index]) { + case 'Chatflow': + return + case 'Custom Assistant': + return + case 'Agentflow v2': + return + } + } + return + } return ( <> @@ -405,14 +458,14 @@ const EvalEvaluationRows = () => { }} variant='outlined' label={outdated.dataset.name} - onClick={() => navigate(`/dataset_rows/${outdated.dataset.id}`)} + onClick={() => window.open(`/dataset_rows/${outdated.dataset.id}`, '_blank')} > )} {outdated.chatflows && outdated?.errors?.length === 0 && outdated.chatflows.length > 0 && ( <>
- Chatflows: + Flows: {outdated.chatflows.map((chatflow, index) => ( { }} variant='outlined' label={chatflow.chatflowName} - onClick={() => navigate(`/canvas/${chatflow.chatflowId}`)} + onClick={() => + window.open( + chatflow.chatflowType === 'Chatflow' + ? '/canvas/' + chatflow.chatflowId + : chatflow.chatflowType === 'Custom Assistant' + ? '/assistants/custom/' + chatflow.chatflowId + : '/v2/agentcanvas/' + chatflow.chatflowId, + '_blank' + ) + } > ))} )} {outdated.errors.length > 0 && - outdated.errors.map((error, index) => {error})} + outdated.errors.map((error, index) => {error.message})} { {showCharts && ( {customEvalsDefined && ( - + { }} > - Chatflows Used: + Flows Used: {(evaluation.chatflowName || []).map((chatflowUsed, index) => ( { : '0 2px 14px 0 rgb(32 40 45 / 10%)' }} label={chatflowUsed} - onClick={() => navigate('/canvas/' + evaluation.chatflowId[index])} + onClick={() => openFlow(index)} > ))} diff --git a/packages/ui/src/views/evaluations/EvaluationResultSideDrawer.jsx b/packages/ui/src/views/evaluations/EvaluationResultSideDrawer.jsx index 9a3ada22be9..c415fb983a5 100644 --- a/packages/ui/src/views/evaluations/EvaluationResultSideDrawer.jsx +++ b/packages/ui/src/views/evaluations/EvaluationResultSideDrawer.jsx @@ -1,8 +1,25 @@ import PropTypes from 'prop-types' -import { CardContent, Card, Box, SwipeableDrawer, Stack, Button, Chip, Divider, Typography } from '@mui/material' +import { + CardContent, + Card, + Box, + SwipeableDrawer, + Stack, + Button, + Chip, + Divider, + Typography, + Table, + TableHead, + TableRow, + TableBody +} from '@mui/material' +import { IconHierarchy, IconUsersGroup, IconRobot } from '@tabler/icons-react' + import { useSelector } from 'react-redux' -import { IconSquareRoundedChevronsRight } from '@tabler/icons-react' import { evaluators as evaluatorsOptions, numericOperators } from '../evaluators/evaluatorConstant' +import TableCell from '@mui/material/TableCell' +import { Close } from '@mui/icons-material' const EvaluationResultSideDrawer = ({ show, dialogProps, onClickFunction }) => { const onOpen = () => {} @@ -19,12 +36,32 @@ const EvaluationResultSideDrawer = ({ show, dialogProps, onClickFunction }) => { return '' } + const getFlowIcon = (index) => { + if (index === undefined) { + return + } + if (dialogProps.additionalConfig.chatflowTypes) { + switch (dialogProps.additionalConfig.chatflowTypes[index]) { + case 'Chatflow': + return + case 'Custom Assistant': + return + case 'Agentflow v2': + return + } + } + return + } + return ( onClickFunction()} onOpen={onOpen}> - - +
+ + Evaluation Details + +
+ Evaluation Id @@ -61,13 +98,19 @@ const EvaluationResultSideDrawer = ({ show, dialogProps, onClickFunction }) => { {dialogProps.evaluationChatflows?.length > 0 && ( <> - - - Chatflow +
+ {getFlowIcon(index)} + + {dialogProps.evaluationChatflows[index]} - {dialogProps.evaluationChatflows[index]} - -
+
)} @@ -153,79 +196,222 @@ const EvaluationResultSideDrawer = ({ show, dialogProps, onClickFunction }) => {

- - - Tokens - - - - - - - - - + {dialogProps.data.metrics[index]?.nested_metrics ? ( + + + Tokens + + + + + + Node + + + Provider & Model + + + Input + + + Output + + + Total + + + + + {dialogProps.data.metrics[index]?.nested_metrics?.map((metric, index) => ( + + + {metric.nodeLabel} + + + {metric.provider} +
+ {metric.model} +
+ + {metric.promptTokens} + + + {metric.completionTokens} + + + {metric.totalTokens} + +
+ ))} + + + Total + + + {dialogProps.data.metrics[index].promptTokens} + + + {dialogProps.data.metrics[index].completionTokens} + + + {dialogProps.data.metrics[index].totalTokens} + + +
+
+
+ ) : ( + + + Tokens + + + + + + + + + + )}
- - - Cost - - - - - - - - - + {dialogProps.data.metrics[index]?.nested_metrics ? ( + + + Cost + + + + + + Node + + + Provider & Model + + + Input + + + Output + + + Total + + + + + {dialogProps.data.metrics[index]?.nested_metrics?.map((metric, index) => ( + + + {metric.nodeLabel} + + + {metric.provider}
+ {metric.model} +
+ + {metric.promptCost} + + + {metric.completionCost} + + + {metric.totalCost} + +
+ ))} + + + Total + + + {dialogProps.data.metrics[index].promptCost} + + + {dialogProps.data.metrics[index].completionCost} + + + {dialogProps.data.metrics[index].totalCost} + + +
+
+
+ ) : ( + + + Cost + + + + + + + + + + )}

diff --git a/packages/ui/src/views/evaluations/MetricsItemCard.jsx b/packages/ui/src/views/evaluations/MetricsItemCard.jsx index a8bdca820ef..380f19c72f9 100644 --- a/packages/ui/src/views/evaluations/MetricsItemCard.jsx +++ b/packages/ui/src/views/evaluations/MetricsItemCard.jsx @@ -11,7 +11,7 @@ import SkeletonChatflowCard from '@/ui-component/cards/Skeleton/ChatflowCard' const CardWrapper = styled(MainCard)(({ theme }) => ({ background: theme.palette.card.main, color: theme.darkTextPrimary, - overflow: 'auto', + overflow: 'hidden', position: 'relative', boxShadow: '0 2px 14px 0 rgb(32 40 45 / 8%)', cursor: 'pointer', diff --git a/packages/ui/src/views/evaluations/index.jsx b/packages/ui/src/views/evaluations/index.jsx index 2f28e1b1300..6f7c0931695 100644 --- a/packages/ui/src/views/evaluations/index.jsx +++ b/packages/ui/src/views/evaluations/index.jsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react' +import React, { useEffect, useState, useCallback } from 'react' import * as PropTypes from 'prop-types' import moment from 'moment/moment' import { useNavigate } from 'react-router-dom' @@ -20,7 +20,8 @@ import { TableBody, TableContainer, TableHead, - TableRow + TableRow, + ToggleButton } from '@mui/material' import { useTheme } from '@mui/material/styles' import { closeSnackbar as closeSnackbarAction, enqueueSnackbar as enqueueSnackbarAction } from '@/store/actions' @@ -35,7 +36,6 @@ import useNotifier from '@/utils/useNotifier' // project import MainCard from '@/ui-component/cards/MainCard' -import { StyledButton } from '@/ui-component/button/StyledButton' import { BackdropLoader } from '@/ui-component/loading/BackdropLoader' import ConfirmDialog from '@/ui-component/dialog/ConfirmDialog' import ErrorBoundary from '@/ErrorBoundary' @@ -53,7 +53,9 @@ import { IconTrash, IconX, IconChevronsUp, - IconChevronsDown + IconChevronsDown, + IconPlayerPlay, + IconPlayerPause } from '@tabler/icons-react' import empty_evalSVG from '@/assets/images/empty_evals.svg' @@ -79,6 +81,7 @@ const EvalsEvaluation = () => { const [loading, setLoading] = useState(false) const [isTableLoading, setTableLoading] = useState(false) const [selected, setSelected] = useState([]) + const [autoRefresh, setAutoRefresh] = useState(false) const onSelectAllClick = (event) => { if (event.target.checked) { @@ -240,14 +243,34 @@ const EvalsEvaluation = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [createNewEvaluation.error]) - const onRefresh = () => { + const onRefresh = useCallback(() => { getAllEvaluations.request() - } + }, [getAllEvaluations]) useEffect(() => { setTableLoading(getAllEvaluations.loading) }, [getAllEvaluations.loading]) + useEffect(() => { + let intervalId = null + + if (autoRefresh) { + intervalId = setInterval(() => { + onRefresh() + }, 5000) + } + + return () => { + if (intervalId) { + clearInterval(intervalId) + } + } + }, [autoRefresh, onRefresh]) + + const toggleAutoRefresh = () => { + setAutoRefresh(!autoRefresh) + } + return ( <> @@ -256,15 +279,52 @@ const EvalsEvaluation = () => { ) : ( - + {autoRefresh ? : } + + } + title='Refresh' > - Refresh - + +
{ Latest Version Average Metrics Last Evaluated - Chatflow(s) + Flow(s) Dataset @@ -438,7 +498,7 @@ function EvaluationRunRow(props) { } const goToDataset = (id) => { - navigate(`/dataset_rows/${id}`) + window.open(`/dataset_rows/${id}`, '_blank') } const onSelectAllChildClick = (event) => { @@ -513,10 +573,6 @@ function EvaluationRunRow(props) { } } - const goToCanvas = (id) => { - navigate(`/canvas/${id}`) - } - const getStatusColor = (status) => { switch (status) { case 'pending': @@ -619,16 +675,11 @@ function EvaluationRunRow(props) { {props.item?.usedFlows?.map((usedFlow, index) => ( goToCanvas(props.item.chatIds[index])} > ))} @@ -637,6 +688,7 @@ function EvaluationRunRow(props) { { if (isEnterpriseLicensed) { finalErrMessage = `Error in registering organization. Please contact your administrator. (${errMessage})` } else { - finalErrMessage = `Error in registering account.` + finalErrMessage = `Error in registering account: ${errMessage}` } setAuthError(finalErrMessage) setLoading(false) @@ -396,7 +396,7 @@ const OrganizationSetupPage = () => { Password must be at least 8 characters long and contain at least one lowercase letter, one uppercase - letter, one digit, and one special character (@$!%*?&-). + letter, one digit, and one special character.
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1ab10f735c5..31390b5b124 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -188,7 +188,7 @@ importers: version: 0.0.7(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)) '@langchain/community': specifier: ^0.3.29 - version: 0.3.40(@aws-crypto/sha256-js@5.2.0)(@aws-sdk/client-bedrock-agent-runtime@3.755.0)(@aws-sdk/client-bedrock-runtime@3.422.0)(@aws-sdk/client-dynamodb@3.529.1)(@aws-sdk/client-kendra@3.750.0)(@aws-sdk/client-s3@3.529.1)(@aws-sdk/credential-provider-node@3.529.1)(@browserbasehq/sdk@2.0.0(encoding@0.1.13))(@browserbasehq/stagehand@1.9.0(@playwright/test@1.49.1)(bufferutil@4.0.8)(deepmerge@4.3.1)(dotenv@16.4.5)(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(utf-8-validate@6.0.4)(zod@3.22.4))(@datastax/astra-db-ts@1.5.0)(@elastic/elasticsearch@8.12.2)(@getzep/zep-cloud@1.0.7(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(langchain@0.3.6(@langchain/anthropic@0.3.14(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13))(@langchain/aws@0.1.4(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/cohere@0.0.7(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/google-genai@0.2.3(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(zod@3.22.4))(@langchain/google-vertexai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(zod@3.22.4))(@langchain/groq@0.1.2(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@langchain/mistralai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/ollama@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(axios@1.7.9)(cheerio@1.0.0-rc.12)(encoding@0.1.13)(handlebars@4.7.8)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(typeorm@0.3.20(ioredis@5.3.2)(mongodb@6.3.0(gcp-metadata@6.1.0(encoding@0.1.13))(socks@2.8.1))(mysql2@3.11.4)(pg@8.11.3)(redis@4.6.13)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.4.6)(@types/node@22.5.4)(typescript@5.5.2)))(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))))(@getzep/zep-js@0.9.0)(@gomomento/sdk-core@1.68.1)(@gomomento/sdk@1.68.1(encoding@0.1.13))(@google-ai/generativelanguage@2.6.0(encoding@0.1.13))(@google-cloud/storage@7.16.0(encoding@0.1.13))(@huggingface/inference@2.6.4)(@ibm-cloud/watsonx-ai@1.2.0)(@langchain/anthropic@0.3.14(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13))(@langchain/aws@0.1.4(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/cohere@0.0.7(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/google-genai@0.2.3(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(zod@3.22.4))(@langchain/google-vertexai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(zod@3.22.4))(@langchain/groq@0.1.2(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@langchain/mistralai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/ollama@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@mendable/firecrawl-js@1.25.1)(@notionhq/client@2.2.14(encoding@0.1.13))(@opensearch-project/opensearch@1.2.0)(@pinecone-database/pinecone@4.0.0)(@qdrant/js-client-rest@1.9.0(typescript@5.5.2))(@smithy/eventstream-codec@4.0.1)(@smithy/protocol-http@5.0.1)(@smithy/signature-v4@5.0.1)(@smithy/util-utf8@4.0.0)(@supabase/supabase-js@2.39.8(bufferutil@4.0.8)(utf-8-validate@6.0.4))(@upstash/redis@1.22.1(encoding@0.1.13))(@upstash/vector@1.1.5)(@zilliz/milvus2-sdk-node@2.3.5)(apify-client@2.9.3)(assemblyai@4.3.2(bufferutil@4.0.8)(utf-8-validate@6.0.4))(axios@1.7.9)(cheerio@1.0.0-rc.12)(chromadb@1.10.3(@google/generative-ai@0.24.0)(cohere-ai@7.10.0(encoding@0.1.13))(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(cohere-ai@7.10.0(encoding@0.1.13))(crypto-js@4.2.0)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(google-auth-library@9.6.3(encoding@0.1.13))(handlebars@4.7.8)(html-to-text@9.0.5)(ibm-cloud-sdk-core@5.1.0)(ignore@5.3.1)(ioredis@5.3.2)(jsdom@22.1.0(bufferutil@4.0.8)(canvas@2.11.2(encoding@0.1.13))(utf-8-validate@6.0.4))(jsonwebtoken@9.0.2)(lodash@4.17.21)(lunary@0.7.12(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(react@18.2.0))(mammoth@1.7.0)(mem0ai@2.1.16(@anthropic-ai/sdk@0.37.0(encoding@0.1.13))(@google/genai@0.7.0(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4))(@mistralai/mistralai@0.1.3(encoding@0.1.13))(@qdrant/js-client-rest@1.9.0(typescript@5.5.2))(@supabase/supabase-js@2.39.8(bufferutil@4.0.8)(utf-8-validate@6.0.4))(@types/jest@29.5.14)(@types/pg@8.11.2)(@types/sqlite3@3.1.11)(encoding@0.1.13)(groq-sdk@0.5.0(encoding@0.1.13))(neo4j-driver@5.27.0)(ollama@0.5.11)(pg@8.11.3)(redis@4.6.13)(sqlite3@5.1.7)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(mongodb@6.3.0(gcp-metadata@6.1.0(encoding@0.1.13))(socks@2.8.1))(mysql2@3.11.4)(neo4j-driver@5.27.0)(notion-to-md@3.1.1(encoding@0.1.13))(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(pdf-parse@1.1.1)(pg@8.11.3)(playwright@1.42.1)(portkey-ai@0.1.16)(puppeteer@20.9.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.5.2)(utf-8-validate@6.0.4))(pyodide@0.25.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(redis@4.6.13)(replicate@0.31.1)(srt-parser-2@1.2.3)(typeorm@0.3.20(ioredis@5.3.2)(mongodb@6.3.0(gcp-metadata@6.1.0(encoding@0.1.13))(socks@2.8.1))(mysql2@3.11.4)(pg@8.11.3)(redis@4.6.13)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.4.6)(@types/node@22.5.4)(typescript@5.5.2)))(weaviate-ts-client@1.6.0(encoding@0.1.13)(graphql@16.8.1))(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + version: 0.3.40(@aws-crypto/sha256-js@5.2.0)(@aws-sdk/client-bedrock-agent-runtime@3.755.0)(@aws-sdk/client-bedrock-runtime@3.422.0)(@aws-sdk/client-dynamodb@3.529.1)(@aws-sdk/client-kendra@3.750.0)(@aws-sdk/client-s3@3.529.1)(@aws-sdk/credential-provider-node@3.529.1)(@browserbasehq/sdk@2.0.0(encoding@0.1.13))(@browserbasehq/stagehand@1.9.0(@playwright/test@1.49.1)(bufferutil@4.0.8)(deepmerge@4.3.1)(dotenv@16.4.5)(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(utf-8-validate@6.0.4)(zod@3.22.4))(@datastax/astra-db-ts@1.5.0)(@elastic/elasticsearch@8.12.2)(@getzep/zep-cloud@1.0.7(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(langchain@0.3.6(@langchain/anthropic@0.3.14(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13))(@langchain/aws@0.1.4(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/cohere@0.0.7(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/google-genai@0.2.3(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(zod@3.22.4))(@langchain/google-vertexai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(zod@3.22.4))(@langchain/groq@0.1.2(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@langchain/mistralai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/ollama@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(axios@1.7.9)(cheerio@1.0.0-rc.12)(encoding@0.1.13)(handlebars@4.7.8)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(typeorm@0.3.20(ioredis@5.3.2)(mongodb@6.3.0(gcp-metadata@6.1.0(encoding@0.1.13))(socks@2.8.1))(mysql2@3.11.4)(pg@8.11.3)(redis@4.6.13)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.4.6)(@types/node@22.5.4)(typescript@5.5.2)))(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))))(@getzep/zep-js@0.9.0)(@gomomento/sdk-core@1.68.1)(@gomomento/sdk@1.68.1(encoding@0.1.13))(@google-ai/generativelanguage@2.6.0(encoding@0.1.13))(@google-cloud/storage@7.16.0(encoding@0.1.13))(@huggingface/inference@2.6.4)(@ibm-cloud/watsonx-ai@1.2.0)(@langchain/anthropic@0.3.14(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13))(@langchain/aws@0.1.4(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/cohere@0.0.7(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/google-genai@0.2.3(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(zod@3.22.4))(@langchain/google-vertexai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(zod@3.22.4))(@langchain/groq@0.1.2(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@langchain/mistralai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/ollama@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@mendable/firecrawl-js@1.25.1)(@notionhq/client@2.2.14(encoding@0.1.13))(@opensearch-project/opensearch@1.2.0)(@pinecone-database/pinecone@4.0.0)(@qdrant/js-client-rest@1.9.0(typescript@5.5.2))(@smithy/eventstream-codec@4.0.1)(@smithy/protocol-http@5.0.1)(@smithy/signature-v4@5.0.1)(@smithy/util-utf8@4.0.0)(@supabase/supabase-js@2.39.8(bufferutil@4.0.8)(utf-8-validate@6.0.4))(@upstash/redis@1.22.1(encoding@0.1.13))(@upstash/vector@1.1.5)(@zilliz/milvus2-sdk-node@2.3.5)(apify-client@2.9.3)(assemblyai@4.3.2(bufferutil@4.0.8)(utf-8-validate@6.0.4))(axios@1.7.9)(cheerio@1.0.0-rc.12)(chromadb@1.10.3(@google/generative-ai@0.24.0)(cohere-ai@7.10.0(encoding@0.1.13))(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(cohere-ai@7.10.0(encoding@0.1.13))(crypto-js@4.2.0)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(google-auth-library@9.6.3(encoding@0.1.13))(handlebars@4.7.8)(html-to-text@9.0.5)(ibm-cloud-sdk-core@5.1.0)(ignore@5.3.1)(ioredis@5.3.2)(jsdom@22.1.0(bufferutil@4.0.8)(canvas@2.11.2(encoding@0.1.13))(utf-8-validate@6.0.4))(jsonwebtoken@9.0.2)(lodash@4.17.21)(lunary@0.7.12(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(react@18.2.0))(mammoth@1.7.0)(mem0ai@2.1.16(@anthropic-ai/sdk@0.37.0(encoding@0.1.13))(@google/genai@0.7.0(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4))(@mistralai/mistralai@0.1.3(encoding@0.1.13))(@qdrant/js-client-rest@1.9.0(typescript@5.5.2))(@supabase/supabase-js@2.39.8(bufferutil@4.0.8)(utf-8-validate@6.0.4))(@types/jest@29.5.14)(@types/pg@8.11.2)(@types/sqlite3@3.1.11)(encoding@0.1.13)(groq-sdk@0.5.0(encoding@0.1.13))(neo4j-driver@5.27.0)(ollama@0.5.11)(pg@8.11.3)(redis@4.6.13)(sqlite3@5.1.7)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(mongodb@6.3.0(gcp-metadata@6.1.0(encoding@0.1.13))(socks@2.8.1))(mysql2@3.11.4)(neo4j-driver@5.27.0)(notion-to-md@3.1.1(encoding@0.1.13))(officeparser@5.1.1)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(pdf-parse@1.1.1)(pg@8.11.3)(playwright@1.42.1)(portkey-ai@0.1.16)(puppeteer@20.9.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.5.2)(utf-8-validate@6.0.4))(pyodide@0.25.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(redis@4.6.13)(replicate@0.31.1)(srt-parser-2@1.2.3)(typeorm@0.3.20(ioredis@5.3.2)(mongodb@6.3.0(gcp-metadata@6.1.0(encoding@0.1.13))(socks@2.8.1))(mysql2@3.11.4)(pg@8.11.3)(redis@4.6.13)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.4.6)(@types/node@22.5.4)(typescript@5.5.2)))(weaviate-ts-client@1.6.0(encoding@0.1.13)(graphql@16.8.1))(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)) '@langchain/core': specifier: 0.3.37 version: 0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)) @@ -233,7 +233,7 @@ importers: version: 0.0.1(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)) '@mem0/community': specifier: ^0.0.1 - version: 0.0.1(@anthropic-ai/sdk@0.37.0(encoding@0.1.13))(@aws-crypto/sha256-js@5.2.0)(@aws-sdk/client-bedrock-agent-runtime@3.755.0)(@aws-sdk/client-bedrock-runtime@3.422.0)(@aws-sdk/client-dynamodb@3.529.1)(@aws-sdk/client-kendra@3.750.0)(@aws-sdk/client-s3@3.529.1)(@aws-sdk/credential-provider-node@3.529.1)(@browserbasehq/sdk@2.0.0(encoding@0.1.13))(@browserbasehq/stagehand@1.9.0(@playwright/test@1.49.1)(bufferutil@4.0.8)(deepmerge@4.3.1)(dotenv@16.4.5)(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(utf-8-validate@6.0.4)(zod@3.22.4))(@datastax/astra-db-ts@1.5.0)(@elastic/elasticsearch@8.12.2)(@getzep/zep-cloud@1.0.7(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(langchain@0.3.6(@langchain/anthropic@0.3.14(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13))(@langchain/aws@0.1.4(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/cohere@0.0.7(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/google-genai@0.2.3(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(zod@3.22.4))(@langchain/google-vertexai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(zod@3.22.4))(@langchain/groq@0.1.2(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@langchain/mistralai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/ollama@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(axios@1.7.9)(cheerio@1.0.0-rc.12)(encoding@0.1.13)(handlebars@4.7.8)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(typeorm@0.3.20(ioredis@5.3.2)(mongodb@6.3.0(gcp-metadata@6.1.0(encoding@0.1.13))(socks@2.8.1))(mysql2@3.11.4)(pg@8.11.3)(redis@4.6.13)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.4.6)(@types/node@22.5.4)(typescript@5.5.2)))(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))))(@getzep/zep-js@0.9.0)(@gomomento/sdk-core@1.68.1)(@gomomento/sdk@1.68.1(encoding@0.1.13))(@google-ai/generativelanguage@2.6.0(encoding@0.1.13))(@google-cloud/storage@7.16.0(encoding@0.1.13))(@google/genai@0.7.0(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4))(@huggingface/inference@2.6.4)(@ibm-cloud/watsonx-ai@1.2.0)(@langchain/anthropic@0.3.14(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13))(@langchain/aws@0.1.4(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/cohere@0.0.7(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/google-genai@0.2.3(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(zod@3.22.4))(@langchain/google-vertexai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(zod@3.22.4))(@langchain/groq@0.1.2(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@langchain/mistralai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/ollama@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@mendable/firecrawl-js@1.25.1)(@mistralai/mistralai@0.1.3(encoding@0.1.13))(@notionhq/client@2.2.14(encoding@0.1.13))(@opensearch-project/opensearch@1.2.0)(@pinecone-database/pinecone@4.0.0)(@qdrant/js-client-rest@1.9.0(typescript@5.5.2))(@smithy/eventstream-codec@4.0.1)(@smithy/protocol-http@5.0.1)(@smithy/signature-v4@5.0.1)(@smithy/util-utf8@4.0.0)(@supabase/supabase-js@2.39.8(bufferutil@4.0.8)(utf-8-validate@6.0.4))(@types/jest@29.5.14)(@types/pg@8.11.2)(@types/sqlite3@3.1.11)(@upstash/redis@1.22.1(encoding@0.1.13))(@upstash/vector@1.1.5)(@zilliz/milvus2-sdk-node@2.3.5)(apify-client@2.9.3)(assemblyai@4.3.2(bufferutil@4.0.8)(utf-8-validate@6.0.4))(cheerio@1.0.0-rc.12)(chromadb@1.10.3(@google/generative-ai@0.24.0)(cohere-ai@7.10.0(encoding@0.1.13))(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(cohere-ai@7.10.0(encoding@0.1.13))(crypto-js@4.2.0)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(google-auth-library@9.6.3(encoding@0.1.13))(groq-sdk@0.5.0(encoding@0.1.13))(handlebars@4.7.8)(html-to-text@9.0.5)(ibm-cloud-sdk-core@5.1.0)(ignore@5.3.1)(ioredis@5.3.2)(jsdom@22.1.0(bufferutil@4.0.8)(canvas@2.11.2(encoding@0.1.13))(utf-8-validate@6.0.4))(jsonwebtoken@9.0.2)(lodash@4.17.21)(lunary@0.7.12(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(react@18.2.0))(mammoth@1.7.0)(mongodb@6.3.0(gcp-metadata@6.1.0(encoding@0.1.13))(socks@2.8.1))(mysql2@3.11.4)(neo4j-driver@5.27.0)(notion-to-md@3.1.1(encoding@0.1.13))(ollama@0.5.11)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(pdf-parse@1.1.1)(pg@8.11.3)(playwright@1.42.1)(portkey-ai@0.1.16)(puppeteer@20.9.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.5.2)(utf-8-validate@6.0.4))(pyodide@0.25.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(redis@4.6.13)(replicate@0.31.1)(sqlite3@5.1.7)(srt-parser-2@1.2.3)(typeorm@0.3.20(ioredis@5.3.2)(mongodb@6.3.0(gcp-metadata@6.1.0(encoding@0.1.13))(socks@2.8.1))(mysql2@3.11.4)(pg@8.11.3)(redis@4.6.13)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.4.6)(@types/node@22.5.4)(typescript@5.5.2)))(weaviate-ts-client@1.6.0(encoding@0.1.13)(graphql@16.8.1))(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + version: 0.0.1(@anthropic-ai/sdk@0.37.0(encoding@0.1.13))(@aws-crypto/sha256-js@5.2.0)(@aws-sdk/client-bedrock-agent-runtime@3.755.0)(@aws-sdk/client-bedrock-runtime@3.422.0)(@aws-sdk/client-dynamodb@3.529.1)(@aws-sdk/client-kendra@3.750.0)(@aws-sdk/client-s3@3.529.1)(@aws-sdk/credential-provider-node@3.529.1)(@browserbasehq/sdk@2.0.0(encoding@0.1.13))(@browserbasehq/stagehand@1.9.0(@playwright/test@1.49.1)(bufferutil@4.0.8)(deepmerge@4.3.1)(dotenv@16.4.5)(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(utf-8-validate@6.0.4)(zod@3.22.4))(@datastax/astra-db-ts@1.5.0)(@elastic/elasticsearch@8.12.2)(@getzep/zep-cloud@1.0.7(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(langchain@0.3.6(@langchain/anthropic@0.3.14(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13))(@langchain/aws@0.1.4(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/cohere@0.0.7(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/google-genai@0.2.3(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(zod@3.22.4))(@langchain/google-vertexai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(zod@3.22.4))(@langchain/groq@0.1.2(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@langchain/mistralai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/ollama@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(axios@1.7.9)(cheerio@1.0.0-rc.12)(encoding@0.1.13)(handlebars@4.7.8)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(typeorm@0.3.20(ioredis@5.3.2)(mongodb@6.3.0(gcp-metadata@6.1.0(encoding@0.1.13))(socks@2.8.1))(mysql2@3.11.4)(pg@8.11.3)(redis@4.6.13)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.4.6)(@types/node@22.5.4)(typescript@5.5.2)))(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))))(@getzep/zep-js@0.9.0)(@gomomento/sdk-core@1.68.1)(@gomomento/sdk@1.68.1(encoding@0.1.13))(@google-ai/generativelanguage@2.6.0(encoding@0.1.13))(@google-cloud/storage@7.16.0(encoding@0.1.13))(@google/genai@0.7.0(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4))(@huggingface/inference@2.6.4)(@ibm-cloud/watsonx-ai@1.2.0)(@langchain/anthropic@0.3.14(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13))(@langchain/aws@0.1.4(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/cohere@0.0.7(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/google-genai@0.2.3(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(zod@3.22.4))(@langchain/google-vertexai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(zod@3.22.4))(@langchain/groq@0.1.2(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@langchain/mistralai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/ollama@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@mendable/firecrawl-js@1.25.1)(@mistralai/mistralai@0.1.3(encoding@0.1.13))(@notionhq/client@2.2.14(encoding@0.1.13))(@opensearch-project/opensearch@1.2.0)(@pinecone-database/pinecone@4.0.0)(@qdrant/js-client-rest@1.9.0(typescript@5.5.2))(@smithy/eventstream-codec@4.0.1)(@smithy/protocol-http@5.0.1)(@smithy/signature-v4@5.0.1)(@smithy/util-utf8@4.0.0)(@supabase/supabase-js@2.39.8(bufferutil@4.0.8)(utf-8-validate@6.0.4))(@types/jest@29.5.14)(@types/pg@8.11.2)(@types/sqlite3@3.1.11)(@upstash/redis@1.22.1(encoding@0.1.13))(@upstash/vector@1.1.5)(@zilliz/milvus2-sdk-node@2.3.5)(apify-client@2.9.3)(assemblyai@4.3.2(bufferutil@4.0.8)(utf-8-validate@6.0.4))(cheerio@1.0.0-rc.12)(chromadb@1.10.3(@google/generative-ai@0.24.0)(cohere-ai@7.10.0(encoding@0.1.13))(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(cohere-ai@7.10.0(encoding@0.1.13))(crypto-js@4.2.0)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(google-auth-library@9.6.3(encoding@0.1.13))(groq-sdk@0.5.0(encoding@0.1.13))(handlebars@4.7.8)(html-to-text@9.0.5)(ibm-cloud-sdk-core@5.1.0)(ignore@5.3.1)(ioredis@5.3.2)(jsdom@22.1.0(bufferutil@4.0.8)(canvas@2.11.2(encoding@0.1.13))(utf-8-validate@6.0.4))(jsonwebtoken@9.0.2)(lodash@4.17.21)(lunary@0.7.12(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(react@18.2.0))(mammoth@1.7.0)(mongodb@6.3.0(gcp-metadata@6.1.0(encoding@0.1.13))(socks@2.8.1))(mysql2@3.11.4)(neo4j-driver@5.27.0)(notion-to-md@3.1.1(encoding@0.1.13))(officeparser@5.1.1)(ollama@0.5.11)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(pdf-parse@1.1.1)(pg@8.11.3)(playwright@1.42.1)(portkey-ai@0.1.16)(puppeteer@20.9.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.5.2)(utf-8-validate@6.0.4))(pyodide@0.25.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(redis@4.6.13)(replicate@0.31.1)(sqlite3@5.1.7)(srt-parser-2@1.2.3)(typeorm@0.3.20(ioredis@5.3.2)(mongodb@6.3.0(gcp-metadata@6.1.0(encoding@0.1.13))(socks@2.8.1))(mysql2@3.11.4)(pg@8.11.3)(redis@4.6.13)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.4.6)(@types/node@22.5.4)(typescript@5.5.2)))(weaviate-ts-client@1.6.0(encoding@0.1.13)(graphql@16.8.1))(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)) '@mendable/firecrawl-js': specifier: ^1.18.2 version: 1.25.1 @@ -423,6 +423,9 @@ importers: object-hash: specifier: ^3.0.0 version: 3.0.0 + officeparser: + specifier: 5.1.1 + version: 5.1.1 ollama: specifier: ^0.5.11 version: 0.5.11 @@ -477,6 +480,9 @@ importers: ws: specifier: ^8.18.0 version: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) + xlsx: + specifier: 0.18.5 + version: 0.18.5 zod: specifier: 3.22.4 version: 3.22.4 @@ -726,7 +732,7 @@ importers: version: 6.9.15 openai: specifier: 4.96.0 - version: 4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4) + version: 4.96.0(encoding@0.1.13)(ws@8.18.2(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4) passport: specifier: ^0.7.0 version: 0.7.0 @@ -1067,6 +1073,9 @@ importers: remark-math: specifier: ^5.1.1 version: 5.1.1 + showdown: + specifier: ^2.1.0 + version: 2.1.0 tippy.js: specifier: ^6.3.7 version: 6.3.7 @@ -7577,6 +7586,10 @@ packages: resolution: { integrity: sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A== } engines: { node: '>=8.9' } + adler-32@1.3.1: + resolution: { integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A== } + engines: { node: '>=0.8' } + adm-zip@0.5.16: resolution: { integrity: sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ== } engines: { node: '>=12.0' } @@ -8459,6 +8472,10 @@ packages: ccount@2.0.1: resolution: { integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg== } + cfb@1.2.2: + resolution: { integrity: sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA== } + engines: { node: '>=0.8' } + chalk@1.1.3: resolution: { integrity: sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A== } engines: { node: '>=0.10.0' } @@ -8721,6 +8738,10 @@ packages: codemirror@6.0.1: resolution: { integrity: sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg== } + codepage@1.15.0: + resolution: { integrity: sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA== } + engines: { node: '>=0.8' } + codsen-utils@1.6.4: resolution: { integrity: sha512-PDyvQ5f2PValmqZZIJATimcokDt4JjIev8cKbZgEOoZm+U1IJDYuLeTcxZPQdep99R/X0RIlQ6ReQgPOVnPbNw== } engines: { node: '>=14.18.0' } @@ -8870,6 +8891,10 @@ packages: resolution: { integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== } engines: { '0': node >= 0.8 } + concat-stream@2.0.0: + resolution: { integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A== } + engines: { '0': node >= 6.0 } + concurrently@7.6.0: resolution: { integrity: sha512-BKtRgvcJGeZ4XttiDiNcFiRlxoAeZOseqUvyYRUp/Vtd+9p1ULmeoSqGsDA+2ivdeDFpqrJvGvmI+StKfKl5hw== } engines: { node: ^12.20.0 || ^14.13.0 || >=16.0.0 } @@ -9011,6 +9036,11 @@ packages: resolution: { integrity: sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA== } engines: { node: '>=10.0.0' } + crc-32@1.2.2: + resolution: { integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ== } + engines: { node: '>=0.8' } + hasBin: true + create-jest@29.7.0: resolution: { integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q== } engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } @@ -10650,6 +10680,10 @@ packages: resolution: { integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== } engines: { node: '>= 0.6' } + frac@1.1.2: + resolution: { integrity: sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA== } + engines: { node: '>=0.8' } + fraction.js@4.3.7: resolution: { integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew== } @@ -13816,6 +13850,10 @@ packages: engines: { node: '>=12.0.0' } hasBin: true + officeparser@5.1.1: + resolution: { integrity: sha512-trBCPmYQDFUCmch6YBxHhMFkDyhTl+vG8PDQHPOwRyeCDKnrrKpph2W7og7hg5T5RRF0yeyaOMasN7GZWbYuCA== } + hasBin: true + ollama@0.5.11: resolution: { integrity: sha512-lDAKcpmBU3VAOGF05NcQipHNKTdpKfAHpZ7bjCsElkUkmX7SNZImi6lwIxz/l1zQtLq0S3wuLneRuiXxX2KIew== } @@ -16119,6 +16157,10 @@ packages: shimmer@1.2.1: resolution: { integrity: sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw== } + showdown@2.1.0: + resolution: { integrity: sha512-/6NVYu4U819R2pUIk79n67SYgJHWCce0a5xTP979WbNp0FL9MN1I1QK662IDU1b6JzKTvmhgI7T7JYIxBi3kMQ== } + hasBin: true + shx@0.3.4: resolution: { integrity: sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g== } engines: { node: '>=6' } @@ -16350,6 +16392,10 @@ packages: resolution: { integrity: sha512-dANP1AyJTI503H0/kXwRza+7QxDB3BqeFvEKTF4MI9lQcBe8JbRUQTKVIGzGABJCwBovEYavZ2Qsdm/s8XKz8A== } hasBin: true + ssf@0.11.2: + resolution: { integrity: sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g== } + engines: { node: '>=0.8' } + ssh2@1.16.0: resolution: { integrity: sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg== } engines: { node: '>=10.16.0' } @@ -17988,10 +18034,18 @@ packages: resolution: { integrity: sha512-OwbxKaOlESDi01mC9rkM0dQqQt2I8DAUMRLZ/HpbwvDXm85IryEHgoogy5fziQy38PntgZsLlhAYHz//UPHZ5w== } engines: { node: '>= 12.0.0' } + wmf@1.0.2: + resolution: { integrity: sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw== } + engines: { node: '>=0.8' } + word-wrap@1.2.5: resolution: { integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== } engines: { node: '>=0.10.0' } + word@0.3.0: + resolution: { integrity: sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA== } + engines: { node: '>=0.8' } + wordwrap@1.0.0: resolution: { integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== } @@ -18179,6 +18233,11 @@ packages: resolution: { integrity: sha512-HY4G725+IDQr16N8XOjAms5qJGArdJaWIuC7Q7A8UXIwj2mifqnPXephazyL7sIkQPvmEoPX3E0v2yFv6hQUNg== } engines: { node: '>=4' } + xlsx@0.18.5: + resolution: { integrity: sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ== } + engines: { node: '>=0.8' } + hasBin: true + xml-name-validator@3.0.0: resolution: { integrity: sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== } @@ -18271,6 +18330,10 @@ packages: yauzl@2.10.0: resolution: { integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g== } + yauzl@3.2.0: + resolution: { integrity: sha512-Ow9nuGZE+qp1u4JIPvg+uCiUr7xGQWdff7JQSk5VGYTAZMDe2q8lxJ10ygv10qmSj031Ty/6FNJpLO4o1Sgc+w== } + engines: { node: '>=12' } + yeoman-environment@3.19.3: resolution: { integrity: sha512-/+ODrTUHtlDPRH9qIC0JREH8+7nsRcjDl3Bxn2Xo/rvAaVvixH5275jHwg0C85g4QsF4P6M2ojfScPPAl+pLAg== } engines: { node: '>=12.10.0' } @@ -21653,7 +21716,7 @@ snapshots: dotenv: 16.4.5 openai: 4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4) sharp: 0.33.5 - ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) + ws: 8.18.2(bufferutil@4.0.8)(utf-8-validate@6.0.4) zod: 3.22.4 zod-to-json-schema: 3.24.1(zod@3.22.4) transitivePeerDependencies: @@ -22402,7 +22465,7 @@ snapshots: '@google/genai@0.7.0(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4)': dependencies: google-auth-library: 9.15.1(encoding@0.1.13) - ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) + ws: 8.18.2(bufferutil@4.0.8)(utf-8-validate@6.0.4) transitivePeerDependencies: - bufferutil - encoding @@ -23067,7 +23130,7 @@ snapshots: - encoding - openai - '@langchain/community@0.3.40(@aws-crypto/sha256-js@5.2.0)(@aws-sdk/client-bedrock-agent-runtime@3.755.0)(@aws-sdk/client-bedrock-runtime@3.422.0)(@aws-sdk/client-dynamodb@3.529.1)(@aws-sdk/client-kendra@3.750.0)(@aws-sdk/client-s3@3.529.1)(@aws-sdk/credential-provider-node@3.529.1)(@browserbasehq/sdk@2.0.0(encoding@0.1.13))(@browserbasehq/stagehand@1.9.0(@playwright/test@1.49.1)(bufferutil@4.0.8)(deepmerge@4.3.1)(dotenv@16.4.5)(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(utf-8-validate@6.0.4)(zod@3.22.4))(@datastax/astra-db-ts@1.5.0)(@elastic/elasticsearch@8.12.2)(@getzep/zep-cloud@1.0.7(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(langchain@0.3.6(@langchain/anthropic@0.3.14(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13))(@langchain/aws@0.1.4(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/cohere@0.0.7(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/google-genai@0.2.3(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(zod@3.22.4))(@langchain/google-vertexai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(zod@3.22.4))(@langchain/groq@0.1.2(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@langchain/mistralai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/ollama@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(axios@1.7.9)(cheerio@1.0.0-rc.12)(encoding@0.1.13)(handlebars@4.7.8)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(typeorm@0.3.20(ioredis@5.3.2)(mongodb@6.3.0(gcp-metadata@6.1.0(encoding@0.1.13))(socks@2.8.1))(mysql2@3.11.4)(pg@8.11.3)(redis@4.6.13)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.4.6)(@types/node@22.5.4)(typescript@5.5.2)))(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))))(@getzep/zep-js@0.9.0)(@gomomento/sdk-core@1.68.1)(@gomomento/sdk@1.68.1(encoding@0.1.13))(@google-ai/generativelanguage@2.6.0(encoding@0.1.13))(@google-cloud/storage@7.16.0(encoding@0.1.13))(@huggingface/inference@2.6.4)(@ibm-cloud/watsonx-ai@1.2.0)(@langchain/anthropic@0.3.14(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13))(@langchain/aws@0.1.4(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/cohere@0.0.7(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/google-genai@0.2.3(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(zod@3.22.4))(@langchain/google-vertexai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(zod@3.22.4))(@langchain/groq@0.1.2(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@langchain/mistralai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/ollama@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@mendable/firecrawl-js@1.25.1)(@notionhq/client@2.2.14(encoding@0.1.13))(@opensearch-project/opensearch@1.2.0)(@pinecone-database/pinecone@4.0.0)(@qdrant/js-client-rest@1.9.0(typescript@5.5.2))(@smithy/eventstream-codec@4.0.1)(@smithy/protocol-http@5.0.1)(@smithy/signature-v4@5.0.1)(@smithy/util-utf8@4.0.0)(@supabase/supabase-js@2.39.8(bufferutil@4.0.8)(utf-8-validate@6.0.4))(@upstash/redis@1.22.1(encoding@0.1.13))(@upstash/vector@1.1.5)(@zilliz/milvus2-sdk-node@2.3.5)(apify-client@2.9.3)(assemblyai@4.3.2(bufferutil@4.0.8)(utf-8-validate@6.0.4))(axios@1.7.9)(cheerio@1.0.0-rc.12)(chromadb@1.10.3(@google/generative-ai@0.24.0)(cohere-ai@7.10.0(encoding@0.1.13))(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(cohere-ai@7.10.0(encoding@0.1.13))(crypto-js@4.2.0)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(google-auth-library@9.6.3(encoding@0.1.13))(handlebars@4.7.8)(html-to-text@9.0.5)(ibm-cloud-sdk-core@5.1.0)(ignore@5.3.1)(ioredis@5.3.2)(jsdom@22.1.0(bufferutil@4.0.8)(canvas@2.11.2(encoding@0.1.13))(utf-8-validate@6.0.4))(jsonwebtoken@9.0.2)(lodash@4.17.21)(lunary@0.7.12(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(react@18.2.0))(mammoth@1.7.0)(mem0ai@2.1.16(@anthropic-ai/sdk@0.37.0(encoding@0.1.13))(@google/genai@0.7.0(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4))(@mistralai/mistralai@0.1.3(encoding@0.1.13))(@qdrant/js-client-rest@1.9.0(typescript@5.5.2))(@supabase/supabase-js@2.39.8(bufferutil@4.0.8)(utf-8-validate@6.0.4))(@types/jest@29.5.14)(@types/pg@8.11.2)(@types/sqlite3@3.1.11)(encoding@0.1.13)(groq-sdk@0.5.0(encoding@0.1.13))(neo4j-driver@5.27.0)(ollama@0.5.11)(pg@8.11.3)(redis@4.6.13)(sqlite3@5.1.7)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(mongodb@6.3.0(gcp-metadata@6.1.0(encoding@0.1.13))(socks@2.8.1))(mysql2@3.11.4)(neo4j-driver@5.27.0)(notion-to-md@3.1.1(encoding@0.1.13))(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(pdf-parse@1.1.1)(pg@8.11.3)(playwright@1.42.1)(portkey-ai@0.1.16)(puppeteer@20.9.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.5.2)(utf-8-validate@6.0.4))(pyodide@0.25.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(redis@4.6.13)(replicate@0.31.1)(srt-parser-2@1.2.3)(typeorm@0.3.20(ioredis@5.3.2)(mongodb@6.3.0(gcp-metadata@6.1.0(encoding@0.1.13))(socks@2.8.1))(mysql2@3.11.4)(pg@8.11.3)(redis@4.6.13)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.4.6)(@types/node@22.5.4)(typescript@5.5.2)))(weaviate-ts-client@1.6.0(encoding@0.1.13)(graphql@16.8.1))(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@langchain/community@0.3.40(@aws-crypto/sha256-js@5.2.0)(@aws-sdk/client-bedrock-agent-runtime@3.755.0)(@aws-sdk/client-bedrock-runtime@3.422.0)(@aws-sdk/client-dynamodb@3.529.1)(@aws-sdk/client-kendra@3.750.0)(@aws-sdk/client-s3@3.529.1)(@aws-sdk/credential-provider-node@3.529.1)(@browserbasehq/sdk@2.0.0(encoding@0.1.13))(@browserbasehq/stagehand@1.9.0(@playwright/test@1.49.1)(bufferutil@4.0.8)(deepmerge@4.3.1)(dotenv@16.4.5)(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(utf-8-validate@6.0.4)(zod@3.22.4))(@datastax/astra-db-ts@1.5.0)(@elastic/elasticsearch@8.12.2)(@getzep/zep-cloud@1.0.7(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(langchain@0.3.6(@langchain/anthropic@0.3.14(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13))(@langchain/aws@0.1.4(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/cohere@0.0.7(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/google-genai@0.2.3(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(zod@3.22.4))(@langchain/google-vertexai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(zod@3.22.4))(@langchain/groq@0.1.2(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@langchain/mistralai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/ollama@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(axios@1.7.9)(cheerio@1.0.0-rc.12)(encoding@0.1.13)(handlebars@4.7.8)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(typeorm@0.3.20(ioredis@5.3.2)(mongodb@6.3.0(gcp-metadata@6.1.0(encoding@0.1.13))(socks@2.8.1))(mysql2@3.11.4)(pg@8.11.3)(redis@4.6.13)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.4.6)(@types/node@22.5.4)(typescript@5.5.2)))(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))))(@getzep/zep-js@0.9.0)(@gomomento/sdk-core@1.68.1)(@gomomento/sdk@1.68.1(encoding@0.1.13))(@google-ai/generativelanguage@2.6.0(encoding@0.1.13))(@google-cloud/storage@7.16.0(encoding@0.1.13))(@huggingface/inference@2.6.4)(@ibm-cloud/watsonx-ai@1.2.0)(@langchain/anthropic@0.3.14(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13))(@langchain/aws@0.1.4(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/cohere@0.0.7(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/google-genai@0.2.3(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(zod@3.22.4))(@langchain/google-vertexai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(zod@3.22.4))(@langchain/groq@0.1.2(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@langchain/mistralai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/ollama@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@mendable/firecrawl-js@1.25.1)(@notionhq/client@2.2.14(encoding@0.1.13))(@opensearch-project/opensearch@1.2.0)(@pinecone-database/pinecone@4.0.0)(@qdrant/js-client-rest@1.9.0(typescript@5.5.2))(@smithy/eventstream-codec@4.0.1)(@smithy/protocol-http@5.0.1)(@smithy/signature-v4@5.0.1)(@smithy/util-utf8@4.0.0)(@supabase/supabase-js@2.39.8(bufferutil@4.0.8)(utf-8-validate@6.0.4))(@upstash/redis@1.22.1(encoding@0.1.13))(@upstash/vector@1.1.5)(@zilliz/milvus2-sdk-node@2.3.5)(apify-client@2.9.3)(assemblyai@4.3.2(bufferutil@4.0.8)(utf-8-validate@6.0.4))(axios@1.7.9)(cheerio@1.0.0-rc.12)(chromadb@1.10.3(@google/generative-ai@0.24.0)(cohere-ai@7.10.0(encoding@0.1.13))(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(cohere-ai@7.10.0(encoding@0.1.13))(crypto-js@4.2.0)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(google-auth-library@9.6.3(encoding@0.1.13))(handlebars@4.7.8)(html-to-text@9.0.5)(ibm-cloud-sdk-core@5.1.0)(ignore@5.3.1)(ioredis@5.3.2)(jsdom@22.1.0(bufferutil@4.0.8)(canvas@2.11.2(encoding@0.1.13))(utf-8-validate@6.0.4))(jsonwebtoken@9.0.2)(lodash@4.17.21)(lunary@0.7.12(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(react@18.2.0))(mammoth@1.7.0)(mem0ai@2.1.16(@anthropic-ai/sdk@0.37.0(encoding@0.1.13))(@google/genai@0.7.0(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4))(@mistralai/mistralai@0.1.3(encoding@0.1.13))(@qdrant/js-client-rest@1.9.0(typescript@5.5.2))(@supabase/supabase-js@2.39.8(bufferutil@4.0.8)(utf-8-validate@6.0.4))(@types/jest@29.5.14)(@types/pg@8.11.2)(@types/sqlite3@3.1.11)(encoding@0.1.13)(groq-sdk@0.5.0(encoding@0.1.13))(neo4j-driver@5.27.0)(ollama@0.5.11)(pg@8.11.3)(redis@4.6.13)(sqlite3@5.1.7)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(mongodb@6.3.0(gcp-metadata@6.1.0(encoding@0.1.13))(socks@2.8.1))(mysql2@3.11.4)(neo4j-driver@5.27.0)(notion-to-md@3.1.1(encoding@0.1.13))(officeparser@5.1.1)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(pdf-parse@1.1.1)(pg@8.11.3)(playwright@1.42.1)(portkey-ai@0.1.16)(puppeteer@20.9.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.5.2)(utf-8-validate@6.0.4))(pyodide@0.25.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(redis@4.6.13)(replicate@0.31.1)(srt-parser-2@1.2.3)(typeorm@0.3.20(ioredis@5.3.2)(mongodb@6.3.0(gcp-metadata@6.1.0(encoding@0.1.13))(socks@2.8.1))(mysql2@3.11.4)(pg@8.11.3)(redis@4.6.13)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.4.6)(@types/node@22.5.4)(typescript@5.5.2)))(weaviate-ts-client@1.6.0(encoding@0.1.13)(graphql@16.8.1))(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: '@browserbasehq/stagehand': 1.9.0(@playwright/test@1.49.1)(bufferutil@4.0.8)(deepmerge@4.3.1)(dotenv@16.4.5)(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(utf-8-validate@6.0.4)(zod@3.22.4) '@ibm-cloud/watsonx-ai': 1.2.0 @@ -23138,6 +23201,7 @@ snapshots: mysql2: 3.11.4 neo4j-driver: 5.27.0 notion-to-md: 3.1.1(encoding@0.1.13) + officeparser: 5.1.1 pdf-parse: 1.1.1 pg: 8.11.3 playwright: 1.42.1 @@ -23409,9 +23473,9 @@ snapshots: - encoding - supports-color - '@mem0/community@0.0.1(@anthropic-ai/sdk@0.37.0(encoding@0.1.13))(@aws-crypto/sha256-js@5.2.0)(@aws-sdk/client-bedrock-agent-runtime@3.755.0)(@aws-sdk/client-bedrock-runtime@3.422.0)(@aws-sdk/client-dynamodb@3.529.1)(@aws-sdk/client-kendra@3.750.0)(@aws-sdk/client-s3@3.529.1)(@aws-sdk/credential-provider-node@3.529.1)(@browserbasehq/sdk@2.0.0(encoding@0.1.13))(@browserbasehq/stagehand@1.9.0(@playwright/test@1.49.1)(bufferutil@4.0.8)(deepmerge@4.3.1)(dotenv@16.4.5)(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(utf-8-validate@6.0.4)(zod@3.22.4))(@datastax/astra-db-ts@1.5.0)(@elastic/elasticsearch@8.12.2)(@getzep/zep-cloud@1.0.7(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(langchain@0.3.6(@langchain/anthropic@0.3.14(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13))(@langchain/aws@0.1.4(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/cohere@0.0.7(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/google-genai@0.2.3(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(zod@3.22.4))(@langchain/google-vertexai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(zod@3.22.4))(@langchain/groq@0.1.2(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@langchain/mistralai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/ollama@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(axios@1.7.9)(cheerio@1.0.0-rc.12)(encoding@0.1.13)(handlebars@4.7.8)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(typeorm@0.3.20(ioredis@5.3.2)(mongodb@6.3.0(gcp-metadata@6.1.0(encoding@0.1.13))(socks@2.8.1))(mysql2@3.11.4)(pg@8.11.3)(redis@4.6.13)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.4.6)(@types/node@22.5.4)(typescript@5.5.2)))(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))))(@getzep/zep-js@0.9.0)(@gomomento/sdk-core@1.68.1)(@gomomento/sdk@1.68.1(encoding@0.1.13))(@google-ai/generativelanguage@2.6.0(encoding@0.1.13))(@google-cloud/storage@7.16.0(encoding@0.1.13))(@google/genai@0.7.0(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4))(@huggingface/inference@2.6.4)(@ibm-cloud/watsonx-ai@1.2.0)(@langchain/anthropic@0.3.14(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13))(@langchain/aws@0.1.4(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/cohere@0.0.7(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/google-genai@0.2.3(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(zod@3.22.4))(@langchain/google-vertexai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(zod@3.22.4))(@langchain/groq@0.1.2(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@langchain/mistralai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/ollama@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@mendable/firecrawl-js@1.25.1)(@mistralai/mistralai@0.1.3(encoding@0.1.13))(@notionhq/client@2.2.14(encoding@0.1.13))(@opensearch-project/opensearch@1.2.0)(@pinecone-database/pinecone@4.0.0)(@qdrant/js-client-rest@1.9.0(typescript@5.5.2))(@smithy/eventstream-codec@4.0.1)(@smithy/protocol-http@5.0.1)(@smithy/signature-v4@5.0.1)(@smithy/util-utf8@4.0.0)(@supabase/supabase-js@2.39.8(bufferutil@4.0.8)(utf-8-validate@6.0.4))(@types/jest@29.5.14)(@types/pg@8.11.2)(@types/sqlite3@3.1.11)(@upstash/redis@1.22.1(encoding@0.1.13))(@upstash/vector@1.1.5)(@zilliz/milvus2-sdk-node@2.3.5)(apify-client@2.9.3)(assemblyai@4.3.2(bufferutil@4.0.8)(utf-8-validate@6.0.4))(cheerio@1.0.0-rc.12)(chromadb@1.10.3(@google/generative-ai@0.24.0)(cohere-ai@7.10.0(encoding@0.1.13))(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(cohere-ai@7.10.0(encoding@0.1.13))(crypto-js@4.2.0)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(google-auth-library@9.6.3(encoding@0.1.13))(groq-sdk@0.5.0(encoding@0.1.13))(handlebars@4.7.8)(html-to-text@9.0.5)(ibm-cloud-sdk-core@5.1.0)(ignore@5.3.1)(ioredis@5.3.2)(jsdom@22.1.0(bufferutil@4.0.8)(canvas@2.11.2(encoding@0.1.13))(utf-8-validate@6.0.4))(jsonwebtoken@9.0.2)(lodash@4.17.21)(lunary@0.7.12(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(react@18.2.0))(mammoth@1.7.0)(mongodb@6.3.0(gcp-metadata@6.1.0(encoding@0.1.13))(socks@2.8.1))(mysql2@3.11.4)(neo4j-driver@5.27.0)(notion-to-md@3.1.1(encoding@0.1.13))(ollama@0.5.11)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(pdf-parse@1.1.1)(pg@8.11.3)(playwright@1.42.1)(portkey-ai@0.1.16)(puppeteer@20.9.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.5.2)(utf-8-validate@6.0.4))(pyodide@0.25.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(redis@4.6.13)(replicate@0.31.1)(sqlite3@5.1.7)(srt-parser-2@1.2.3)(typeorm@0.3.20(ioredis@5.3.2)(mongodb@6.3.0(gcp-metadata@6.1.0(encoding@0.1.13))(socks@2.8.1))(mysql2@3.11.4)(pg@8.11.3)(redis@4.6.13)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.4.6)(@types/node@22.5.4)(typescript@5.5.2)))(weaviate-ts-client@1.6.0(encoding@0.1.13)(graphql@16.8.1))(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@mem0/community@0.0.1(@anthropic-ai/sdk@0.37.0(encoding@0.1.13))(@aws-crypto/sha256-js@5.2.0)(@aws-sdk/client-bedrock-agent-runtime@3.755.0)(@aws-sdk/client-bedrock-runtime@3.422.0)(@aws-sdk/client-dynamodb@3.529.1)(@aws-sdk/client-kendra@3.750.0)(@aws-sdk/client-s3@3.529.1)(@aws-sdk/credential-provider-node@3.529.1)(@browserbasehq/sdk@2.0.0(encoding@0.1.13))(@browserbasehq/stagehand@1.9.0(@playwright/test@1.49.1)(bufferutil@4.0.8)(deepmerge@4.3.1)(dotenv@16.4.5)(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(utf-8-validate@6.0.4)(zod@3.22.4))(@datastax/astra-db-ts@1.5.0)(@elastic/elasticsearch@8.12.2)(@getzep/zep-cloud@1.0.7(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(langchain@0.3.6(@langchain/anthropic@0.3.14(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13))(@langchain/aws@0.1.4(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/cohere@0.0.7(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/google-genai@0.2.3(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(zod@3.22.4))(@langchain/google-vertexai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(zod@3.22.4))(@langchain/groq@0.1.2(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@langchain/mistralai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/ollama@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(axios@1.7.9)(cheerio@1.0.0-rc.12)(encoding@0.1.13)(handlebars@4.7.8)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(typeorm@0.3.20(ioredis@5.3.2)(mongodb@6.3.0(gcp-metadata@6.1.0(encoding@0.1.13))(socks@2.8.1))(mysql2@3.11.4)(pg@8.11.3)(redis@4.6.13)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.4.6)(@types/node@22.5.4)(typescript@5.5.2)))(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))))(@getzep/zep-js@0.9.0)(@gomomento/sdk-core@1.68.1)(@gomomento/sdk@1.68.1(encoding@0.1.13))(@google-ai/generativelanguage@2.6.0(encoding@0.1.13))(@google-cloud/storage@7.16.0(encoding@0.1.13))(@google/genai@0.7.0(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4))(@huggingface/inference@2.6.4)(@ibm-cloud/watsonx-ai@1.2.0)(@langchain/anthropic@0.3.14(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13))(@langchain/aws@0.1.4(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/cohere@0.0.7(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/google-genai@0.2.3(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(zod@3.22.4))(@langchain/google-vertexai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(zod@3.22.4))(@langchain/groq@0.1.2(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@langchain/mistralai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/ollama@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@mendable/firecrawl-js@1.25.1)(@mistralai/mistralai@0.1.3(encoding@0.1.13))(@notionhq/client@2.2.14(encoding@0.1.13))(@opensearch-project/opensearch@1.2.0)(@pinecone-database/pinecone@4.0.0)(@qdrant/js-client-rest@1.9.0(typescript@5.5.2))(@smithy/eventstream-codec@4.0.1)(@smithy/protocol-http@5.0.1)(@smithy/signature-v4@5.0.1)(@smithy/util-utf8@4.0.0)(@supabase/supabase-js@2.39.8(bufferutil@4.0.8)(utf-8-validate@6.0.4))(@types/jest@29.5.14)(@types/pg@8.11.2)(@types/sqlite3@3.1.11)(@upstash/redis@1.22.1(encoding@0.1.13))(@upstash/vector@1.1.5)(@zilliz/milvus2-sdk-node@2.3.5)(apify-client@2.9.3)(assemblyai@4.3.2(bufferutil@4.0.8)(utf-8-validate@6.0.4))(cheerio@1.0.0-rc.12)(chromadb@1.10.3(@google/generative-ai@0.24.0)(cohere-ai@7.10.0(encoding@0.1.13))(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(cohere-ai@7.10.0(encoding@0.1.13))(crypto-js@4.2.0)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(google-auth-library@9.6.3(encoding@0.1.13))(groq-sdk@0.5.0(encoding@0.1.13))(handlebars@4.7.8)(html-to-text@9.0.5)(ibm-cloud-sdk-core@5.1.0)(ignore@5.3.1)(ioredis@5.3.2)(jsdom@22.1.0(bufferutil@4.0.8)(canvas@2.11.2(encoding@0.1.13))(utf-8-validate@6.0.4))(jsonwebtoken@9.0.2)(lodash@4.17.21)(lunary@0.7.12(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(react@18.2.0))(mammoth@1.7.0)(mongodb@6.3.0(gcp-metadata@6.1.0(encoding@0.1.13))(socks@2.8.1))(mysql2@3.11.4)(neo4j-driver@5.27.0)(notion-to-md@3.1.1(encoding@0.1.13))(officeparser@5.1.1)(ollama@0.5.11)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(pdf-parse@1.1.1)(pg@8.11.3)(playwright@1.42.1)(portkey-ai@0.1.16)(puppeteer@20.9.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.5.2)(utf-8-validate@6.0.4))(pyodide@0.25.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(redis@4.6.13)(replicate@0.31.1)(sqlite3@5.1.7)(srt-parser-2@1.2.3)(typeorm@0.3.20(ioredis@5.3.2)(mongodb@6.3.0(gcp-metadata@6.1.0(encoding@0.1.13))(socks@2.8.1))(mysql2@3.11.4)(pg@8.11.3)(redis@4.6.13)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.4.6)(@types/node@22.5.4)(typescript@5.5.2)))(weaviate-ts-client@1.6.0(encoding@0.1.13)(graphql@16.8.1))(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: - '@langchain/community': 0.3.40(@aws-crypto/sha256-js@5.2.0)(@aws-sdk/client-bedrock-agent-runtime@3.755.0)(@aws-sdk/client-bedrock-runtime@3.422.0)(@aws-sdk/client-dynamodb@3.529.1)(@aws-sdk/client-kendra@3.750.0)(@aws-sdk/client-s3@3.529.1)(@aws-sdk/credential-provider-node@3.529.1)(@browserbasehq/sdk@2.0.0(encoding@0.1.13))(@browserbasehq/stagehand@1.9.0(@playwright/test@1.49.1)(bufferutil@4.0.8)(deepmerge@4.3.1)(dotenv@16.4.5)(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(utf-8-validate@6.0.4)(zod@3.22.4))(@datastax/astra-db-ts@1.5.0)(@elastic/elasticsearch@8.12.2)(@getzep/zep-cloud@1.0.7(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(langchain@0.3.6(@langchain/anthropic@0.3.14(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13))(@langchain/aws@0.1.4(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/cohere@0.0.7(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/google-genai@0.2.3(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(zod@3.22.4))(@langchain/google-vertexai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(zod@3.22.4))(@langchain/groq@0.1.2(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@langchain/mistralai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/ollama@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(axios@1.7.9)(cheerio@1.0.0-rc.12)(encoding@0.1.13)(handlebars@4.7.8)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(typeorm@0.3.20(ioredis@5.3.2)(mongodb@6.3.0(gcp-metadata@6.1.0(encoding@0.1.13))(socks@2.8.1))(mysql2@3.11.4)(pg@8.11.3)(redis@4.6.13)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.4.6)(@types/node@22.5.4)(typescript@5.5.2)))(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))))(@getzep/zep-js@0.9.0)(@gomomento/sdk-core@1.68.1)(@gomomento/sdk@1.68.1(encoding@0.1.13))(@google-ai/generativelanguage@2.6.0(encoding@0.1.13))(@google-cloud/storage@7.16.0(encoding@0.1.13))(@huggingface/inference@2.6.4)(@ibm-cloud/watsonx-ai@1.2.0)(@langchain/anthropic@0.3.14(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13))(@langchain/aws@0.1.4(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/cohere@0.0.7(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/google-genai@0.2.3(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(zod@3.22.4))(@langchain/google-vertexai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(zod@3.22.4))(@langchain/groq@0.1.2(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@langchain/mistralai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/ollama@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@mendable/firecrawl-js@1.25.1)(@notionhq/client@2.2.14(encoding@0.1.13))(@opensearch-project/opensearch@1.2.0)(@pinecone-database/pinecone@4.0.0)(@qdrant/js-client-rest@1.9.0(typescript@5.5.2))(@smithy/eventstream-codec@4.0.1)(@smithy/protocol-http@5.0.1)(@smithy/signature-v4@5.0.1)(@smithy/util-utf8@4.0.0)(@supabase/supabase-js@2.39.8(bufferutil@4.0.8)(utf-8-validate@6.0.4))(@upstash/redis@1.22.1(encoding@0.1.13))(@upstash/vector@1.1.5)(@zilliz/milvus2-sdk-node@2.3.5)(apify-client@2.9.3)(assemblyai@4.3.2(bufferutil@4.0.8)(utf-8-validate@6.0.4))(axios@1.7.9)(cheerio@1.0.0-rc.12)(chromadb@1.10.3(@google/generative-ai@0.24.0)(cohere-ai@7.10.0(encoding@0.1.13))(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(cohere-ai@7.10.0(encoding@0.1.13))(crypto-js@4.2.0)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(google-auth-library@9.6.3(encoding@0.1.13))(handlebars@4.7.8)(html-to-text@9.0.5)(ibm-cloud-sdk-core@5.1.0)(ignore@5.3.1)(ioredis@5.3.2)(jsdom@22.1.0(bufferutil@4.0.8)(canvas@2.11.2(encoding@0.1.13))(utf-8-validate@6.0.4))(jsonwebtoken@9.0.2)(lodash@4.17.21)(lunary@0.7.12(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(react@18.2.0))(mammoth@1.7.0)(mem0ai@2.1.16(@anthropic-ai/sdk@0.37.0(encoding@0.1.13))(@google/genai@0.7.0(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4))(@mistralai/mistralai@0.1.3(encoding@0.1.13))(@qdrant/js-client-rest@1.9.0(typescript@5.5.2))(@supabase/supabase-js@2.39.8(bufferutil@4.0.8)(utf-8-validate@6.0.4))(@types/jest@29.5.14)(@types/pg@8.11.2)(@types/sqlite3@3.1.11)(encoding@0.1.13)(groq-sdk@0.5.0(encoding@0.1.13))(neo4j-driver@5.27.0)(ollama@0.5.11)(pg@8.11.3)(redis@4.6.13)(sqlite3@5.1.7)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(mongodb@6.3.0(gcp-metadata@6.1.0(encoding@0.1.13))(socks@2.8.1))(mysql2@3.11.4)(neo4j-driver@5.27.0)(notion-to-md@3.1.1(encoding@0.1.13))(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(pdf-parse@1.1.1)(pg@8.11.3)(playwright@1.42.1)(portkey-ai@0.1.16)(puppeteer@20.9.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.5.2)(utf-8-validate@6.0.4))(pyodide@0.25.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(redis@4.6.13)(replicate@0.31.1)(srt-parser-2@1.2.3)(typeorm@0.3.20(ioredis@5.3.2)(mongodb@6.3.0(gcp-metadata@6.1.0(encoding@0.1.13))(socks@2.8.1))(mysql2@3.11.4)(pg@8.11.3)(redis@4.6.13)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.4.6)(@types/node@22.5.4)(typescript@5.5.2)))(weaviate-ts-client@1.6.0(encoding@0.1.13)(graphql@16.8.1))(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@langchain/community': 0.3.40(@aws-crypto/sha256-js@5.2.0)(@aws-sdk/client-bedrock-agent-runtime@3.755.0)(@aws-sdk/client-bedrock-runtime@3.422.0)(@aws-sdk/client-dynamodb@3.529.1)(@aws-sdk/client-kendra@3.750.0)(@aws-sdk/client-s3@3.529.1)(@aws-sdk/credential-provider-node@3.529.1)(@browserbasehq/sdk@2.0.0(encoding@0.1.13))(@browserbasehq/stagehand@1.9.0(@playwright/test@1.49.1)(bufferutil@4.0.8)(deepmerge@4.3.1)(dotenv@16.4.5)(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(utf-8-validate@6.0.4)(zod@3.22.4))(@datastax/astra-db-ts@1.5.0)(@elastic/elasticsearch@8.12.2)(@getzep/zep-cloud@1.0.7(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(langchain@0.3.6(@langchain/anthropic@0.3.14(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13))(@langchain/aws@0.1.4(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/cohere@0.0.7(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/google-genai@0.2.3(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(zod@3.22.4))(@langchain/google-vertexai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(zod@3.22.4))(@langchain/groq@0.1.2(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@langchain/mistralai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/ollama@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(axios@1.7.9)(cheerio@1.0.0-rc.12)(encoding@0.1.13)(handlebars@4.7.8)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(typeorm@0.3.20(ioredis@5.3.2)(mongodb@6.3.0(gcp-metadata@6.1.0(encoding@0.1.13))(socks@2.8.1))(mysql2@3.11.4)(pg@8.11.3)(redis@4.6.13)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.4.6)(@types/node@22.5.4)(typescript@5.5.2)))(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))))(@getzep/zep-js@0.9.0)(@gomomento/sdk-core@1.68.1)(@gomomento/sdk@1.68.1(encoding@0.1.13))(@google-ai/generativelanguage@2.6.0(encoding@0.1.13))(@google-cloud/storage@7.16.0(encoding@0.1.13))(@huggingface/inference@2.6.4)(@ibm-cloud/watsonx-ai@1.2.0)(@langchain/anthropic@0.3.14(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13))(@langchain/aws@0.1.4(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/cohere@0.0.7(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/google-genai@0.2.3(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(zod@3.22.4))(@langchain/google-vertexai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(zod@3.22.4))(@langchain/groq@0.1.2(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@langchain/mistralai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/ollama@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@mendable/firecrawl-js@1.25.1)(@notionhq/client@2.2.14(encoding@0.1.13))(@opensearch-project/opensearch@1.2.0)(@pinecone-database/pinecone@4.0.0)(@qdrant/js-client-rest@1.9.0(typescript@5.5.2))(@smithy/eventstream-codec@4.0.1)(@smithy/protocol-http@5.0.1)(@smithy/signature-v4@5.0.1)(@smithy/util-utf8@4.0.0)(@supabase/supabase-js@2.39.8(bufferutil@4.0.8)(utf-8-validate@6.0.4))(@upstash/redis@1.22.1(encoding@0.1.13))(@upstash/vector@1.1.5)(@zilliz/milvus2-sdk-node@2.3.5)(apify-client@2.9.3)(assemblyai@4.3.2(bufferutil@4.0.8)(utf-8-validate@6.0.4))(axios@1.7.9)(cheerio@1.0.0-rc.12)(chromadb@1.10.3(@google/generative-ai@0.24.0)(cohere-ai@7.10.0(encoding@0.1.13))(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(cohere-ai@7.10.0(encoding@0.1.13))(crypto-js@4.2.0)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(google-auth-library@9.6.3(encoding@0.1.13))(handlebars@4.7.8)(html-to-text@9.0.5)(ibm-cloud-sdk-core@5.1.0)(ignore@5.3.1)(ioredis@5.3.2)(jsdom@22.1.0(bufferutil@4.0.8)(canvas@2.11.2(encoding@0.1.13))(utf-8-validate@6.0.4))(jsonwebtoken@9.0.2)(lodash@4.17.21)(lunary@0.7.12(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(react@18.2.0))(mammoth@1.7.0)(mem0ai@2.1.16(@anthropic-ai/sdk@0.37.0(encoding@0.1.13))(@google/genai@0.7.0(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4))(@mistralai/mistralai@0.1.3(encoding@0.1.13))(@qdrant/js-client-rest@1.9.0(typescript@5.5.2))(@supabase/supabase-js@2.39.8(bufferutil@4.0.8)(utf-8-validate@6.0.4))(@types/jest@29.5.14)(@types/pg@8.11.2)(@types/sqlite3@3.1.11)(encoding@0.1.13)(groq-sdk@0.5.0(encoding@0.1.13))(neo4j-driver@5.27.0)(ollama@0.5.11)(pg@8.11.3)(redis@4.6.13)(sqlite3@5.1.7)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(mongodb@6.3.0(gcp-metadata@6.1.0(encoding@0.1.13))(socks@2.8.1))(mysql2@3.11.4)(neo4j-driver@5.27.0)(notion-to-md@3.1.1(encoding@0.1.13))(officeparser@5.1.1)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(pdf-parse@1.1.1)(pg@8.11.3)(playwright@1.42.1)(portkey-ai@0.1.16)(puppeteer@20.9.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.5.2)(utf-8-validate@6.0.4))(pyodide@0.25.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(redis@4.6.13)(replicate@0.31.1)(srt-parser-2@1.2.3)(typeorm@0.3.20(ioredis@5.3.2)(mongodb@6.3.0(gcp-metadata@6.1.0(encoding@0.1.13))(socks@2.8.1))(mysql2@3.11.4)(pg@8.11.3)(redis@4.6.13)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.4.6)(@types/node@22.5.4)(typescript@5.5.2)))(weaviate-ts-client@1.6.0(encoding@0.1.13)(graphql@16.8.1))(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)) '@langchain/core': 0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)) axios: 1.7.9(debug@4.3.4) mem0ai: 2.1.16(@anthropic-ai/sdk@0.37.0(encoding@0.1.13))(@google/genai@0.7.0(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4))(@mistralai/mistralai@0.1.3(encoding@0.1.13))(@qdrant/js-client-rest@1.9.0(typescript@5.5.2))(@supabase/supabase-js@2.39.8(bufferutil@4.0.8)(utf-8-validate@6.0.4))(@types/jest@29.5.14)(@types/pg@8.11.2)(@types/sqlite3@3.1.11)(encoding@0.1.13)(groq-sdk@0.5.0(encoding@0.1.13))(neo4j-driver@5.27.0)(ollama@0.5.11)(pg@8.11.3)(redis@4.6.13)(sqlite3@5.1.7)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)) @@ -27625,6 +27689,8 @@ snapshots: loader-utils: 2.0.4 regex-parser: 2.3.0 + adler-32@1.3.1: {} + adm-zip@0.5.16: {} agent-base@6.0.2: @@ -28789,6 +28855,11 @@ snapshots: ccount@2.0.1: {} + cfb@1.2.2: + dependencies: + adler-32: 1.3.1 + crc-32: 1.2.2 + chalk@1.1.3: dependencies: ansi-styles: 2.2.1 @@ -29083,6 +29154,8 @@ snapshots: transitivePeerDependencies: - '@lezer/common' + codepage@1.15.0: {} + codsen-utils@1.6.4: dependencies: rfdc: 1.3.1 @@ -29221,6 +29294,13 @@ snapshots: readable-stream: 2.3.8 typedarray: 0.0.6 + concat-stream@2.0.0: + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 3.6.2 + typedarray: 0.0.6 + concurrently@7.6.0: dependencies: chalk: 4.1.2 @@ -29371,6 +29451,8 @@ snapshots: nan: 2.22.2 optional: true + crc-32@1.2.2: {} + create-jest@29.7.0(@types/node@22.5.4)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.4.6)(@types/node@22.5.4)(typescript@5.5.2)): dependencies: '@jest/types': 29.6.3 @@ -31584,6 +31666,8 @@ snapshots: forwarded@0.2.0: {} + frac@1.1.2: {} + fraction.js@4.3.7: {} fragment-cache@0.2.1: @@ -35929,6 +36013,14 @@ snapshots: - supports-color - typescript + officeparser@5.1.1: + dependencies: + '@xmldom/xmldom': 0.8.10 + concat-stream: 2.0.0 + file-type: 16.5.4 + node-ensure: 0.0.0 + yauzl: 3.2.0 + ollama@0.5.11: dependencies: whatwg-fetch: 3.6.20 @@ -36019,6 +36111,21 @@ snapshots: transitivePeerDependencies: - encoding + openai@4.96.0(encoding@0.1.13)(ws@8.18.2(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4): + dependencies: + '@types/node': 18.19.23 + '@types/node-fetch': 2.6.12 + abort-controller: 3.0.0 + agentkeepalive: 4.5.0 + form-data-encoder: 1.7.2 + formdata-node: 4.4.1 + node-fetch: 2.7.0(encoding@0.1.13) + optionalDependencies: + ws: 8.18.2(bufferutil@4.0.8)(utf-8-validate@6.0.4) + zod: 3.22.4 + transitivePeerDependencies: + - encoding + openapi-types@12.1.3: {} openapi-typescript-fetch@1.1.3: {} @@ -38734,6 +38841,10 @@ snapshots: shimmer@1.2.1: {} + showdown@2.1.0: + dependencies: + commander: 9.5.0 + shx@0.3.4: dependencies: minimist: 1.2.8 @@ -39006,6 +39117,10 @@ snapshots: srt-parser-2@1.2.3: {} + ssf@0.11.2: + dependencies: + frac: 1.1.2 + ssh2@1.16.0: dependencies: asn1: 0.2.6 @@ -40893,8 +41008,12 @@ snapshots: triple-beam: 1.4.1 winston-transport: 4.7.0 + wmf@1.0.2: {} + word-wrap@1.2.5: {} + word@0.3.0: {} + wordwrap@1.0.0: {} workbox-background-sync@6.6.0: @@ -41197,6 +41316,16 @@ snapshots: execa: 0.2.2 titleize: 1.0.1 + xlsx@0.18.5: + dependencies: + adler-32: 1.3.1 + cfb: 1.2.2 + codepage: 1.15.0 + crc-32: 1.2.2 + ssf: 0.11.2 + wmf: 1.0.2 + word: 0.3.0 + xml-name-validator@3.0.0: {} xml-name-validator@4.0.0: {} @@ -41294,6 +41423,11 @@ snapshots: buffer-crc32: 0.2.13 fd-slicer: 1.1.0 + yauzl@3.2.0: + dependencies: + buffer-crc32: 0.2.13 + pend: 1.2.0 + yeoman-environment@3.19.3: dependencies: '@npmcli/arborist': 4.3.1