From 80e405a897fe089883f39adfcd1fff88f2236327 Mon Sep 17 00:00:00 2001 From: Henry Date: Wed, 4 Jun 2025 13:02:22 +0100 Subject: [PATCH 1/7] add teams, gmail, outlook tools --- .../credentials/GoogleOAuth2.credential.ts | 60 + .../MsoftOutlookOAuth2.credential.ts | 63 + .../MsoftTeamsOAuth2.credential.ts | 84 + .../components/nodes/agentflow/Agent/Agent.ts | 28 +- .../components/nodes/agentflow/Tool/Tool.ts | 15 +- .../nodes/sequentialagents/Agent/Agent.ts | 23 +- .../sequentialagents/ToolNode/ToolNode.ts | 17 +- .../components/nodes/tools/Gmail/Gmail.ts | 699 +++++++ packages/components/nodes/tools/Gmail/core.ts | 1226 ++++++++++++ .../components/nodes/tools/Gmail/gmail.svg | 1 + .../MicrosoftOutlook/MicrosoftOutlook.ts | 971 +++++++++ .../nodes/tools/MicrosoftOutlook/core.ts | 1052 ++++++++++ .../nodes/tools/MicrosoftOutlook/outlook.svg | 1 + .../tools/MicrosoftTeams/MicrosoftTeams.ts | 1226 ++++++++++++ .../nodes/tools/MicrosoftTeams/core.ts | 1782 +++++++++++++++++ .../nodes/tools/MicrosoftTeams/teams.svg | 1 + .../components/src/agentflowv2Generator.ts | 113 +- packages/components/src/agents.ts | 21 +- packages/components/src/utils.ts | 65 + packages/server/src/routes/index.ts | 2 + packages/server/src/routes/oauth2/index.ts | 422 ++++ .../server/src/routes/oauth2/templates.ts | 128 ++ .../src/services/openai-realtime/index.ts | 6 + packages/server/src/utils/constants.ts | 2 + packages/ui/src/api/oauth2.js | 13 + packages/ui/src/utils/genericHelper.js | 113 +- .../credentials/AddEditCredentialDialog.jsx | 172 +- 27 files changed, 8223 insertions(+), 83 deletions(-) create mode 100644 packages/components/credentials/GoogleOAuth2.credential.ts create mode 100644 packages/components/credentials/MsoftOutlookOAuth2.credential.ts create mode 100644 packages/components/credentials/MsoftTeamsOAuth2.credential.ts create mode 100644 packages/components/nodes/tools/Gmail/Gmail.ts create mode 100644 packages/components/nodes/tools/Gmail/core.ts create mode 100644 packages/components/nodes/tools/Gmail/gmail.svg create mode 100644 packages/components/nodes/tools/MicrosoftOutlook/MicrosoftOutlook.ts create mode 100644 packages/components/nodes/tools/MicrosoftOutlook/core.ts create mode 100644 packages/components/nodes/tools/MicrosoftOutlook/outlook.svg create mode 100644 packages/components/nodes/tools/MicrosoftTeams/MicrosoftTeams.ts create mode 100644 packages/components/nodes/tools/MicrosoftTeams/core.ts create mode 100644 packages/components/nodes/tools/MicrosoftTeams/teams.svg create mode 100644 packages/server/src/routes/oauth2/index.ts create mode 100644 packages/server/src/routes/oauth2/templates.ts create mode 100644 packages/ui/src/api/oauth2.js diff --git a/packages/components/credentials/GoogleOAuth2.credential.ts b/packages/components/credentials/GoogleOAuth2.credential.ts new file mode 100644 index 00000000000..2bbf4fe9179 --- /dev/null +++ b/packages/components/credentials/GoogleOAuth2.credential.ts @@ -0,0 +1,60 @@ +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[] + + constructor() { + this.label = 'Gmail OAuth2' + this.name = 'gmailOAuth2' + this.version = 1.0 + 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: 'string' + }, + { + 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/MsoftOutlookOAuth2.credential.ts b/packages/components/credentials/MsoftOutlookOAuth2.credential.ts new file mode 100644 index 00000000000..15db1de203d --- /dev/null +++ b/packages/components/credentials/MsoftOutlookOAuth2.credential.ts @@ -0,0 +1,63 @@ +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 + inputs: INodeParams[] + + constructor() { + this.label = 'Microsoft Outlook OAuth2' + this.name = 'microsoftOutlookOAuth2' + this.version = 1.0 + 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: 'string' + }, + { + label: 'Scope', + name: 'scope', + type: 'string', + hidden: true, + default: scopes.join(' ') + } + ] + } +} + +module.exports = { credClass: MsoftOutlookOAuth2 } diff --git a/packages/components/credentials/MsoftTeamsOAuth2.credential.ts b/packages/components/credentials/MsoftTeamsOAuth2.credential.ts new file mode 100644 index 00000000000..1845e21414b --- /dev/null +++ b/packages/components/credentials/MsoftTeamsOAuth2.credential.ts @@ -0,0 +1,84 @@ +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[] + + constructor() { + this.label = 'Microsoft Teams OAuth2' + this.name = 'microsoftTeamsOAuth2' + this.version = 1.0 + 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: 'string' + }, + { + label: 'Scope', + name: 'scope', + type: 'string', + hidden: true, + default: scopes.join(' ') + } + ] + } +} + +module.exports = { credClass: MsoftTeamsOAuth2 } diff --git a/packages/components/nodes/agentflow/Agent/Agent.ts b/packages/components/nodes/agentflow/Agent/Agent.ts index 55f565d0e8e..4c974cc3054 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' @@ -1429,6 +1429,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,7 +1455,7 @@ class Agent_Agentflow implements INode { // Track used tools usedTools.push({ tool: toolCall.name, - toolInput: toolCall.args, + toolInput: toolInput ?? toolCall.args, toolOutput }) } catch (e) { @@ -1667,6 +1678,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,7 +1704,7 @@ class Agent_Agentflow implements INode { // Track used tools usedTools.push({ tool: toolCall.name, - toolInput: toolCall.args, + toolInput: toolInput ?? toolCall.args, toolOutput }) } catch (e) { diff --git a/packages/components/nodes/agentflow/Tool/Tool.ts b/packages/components/nodes/agentflow/Tool/Tool.ts index f2ab96e7548..8150cde5c5c 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 { @@ -262,6 +262,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 +295,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/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..37c8d2274bb --- /dev/null +++ b/packages/components/nodes/tools/Gmail/Gmail.ts @@ -0,0 +1,699 @@ +import { 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 = nodeData.inputs?.draftActions ? JSON.parse(nodeData.inputs?.draftActions) : [] + } else if (gmailType === 'messages') { + actions = nodeData.inputs?.messageActions ? JSON.parse(nodeData.inputs?.messageActions) : [] + } else if (gmailType === 'labels') { + actions = nodeData.inputs?.labelActions ? JSON.parse(nodeData.inputs?.labelActions) : [] + } else if (gmailType === 'threads') { + actions = nodeData.inputs?.threadActions ? JSON.parse(nodeData.inputs?.threadActions) : [] + } + + // Prepare default parameters for each action + const defaultParams: ICommonObject = {} + + // Draft parameters + const draftMaxResults = nodeData.inputs?.draftMaxResults + const draftTo = nodeData.inputs?.draftTo + const draftSubject = nodeData.inputs?.draftSubject + const draftBody = nodeData.inputs?.draftBody + const draftCc = nodeData.inputs?.draftCc + const draftBcc = nodeData.inputs?.draftBcc + const draftId = nodeData.inputs?.draftId + const draftUpdateTo = nodeData.inputs?.draftUpdateTo + const draftUpdateSubject = nodeData.inputs?.draftUpdateSubject + const draftUpdateBody = nodeData.inputs?.draftUpdateBody + + // Message parameters + const messageMaxResults = nodeData.inputs?.messageMaxResults + const messageQuery = nodeData.inputs?.messageQuery + const messageTo = nodeData.inputs?.messageTo + const messageSubject = nodeData.inputs?.messageSubject + const messageBody = nodeData.inputs?.messageBody + const messageCc = nodeData.inputs?.messageCc + const messageBcc = nodeData.inputs?.messageBcc + const messageId = nodeData.inputs?.messageId + const messageAddLabelIds = nodeData.inputs?.messageAddLabelIds + const messageRemoveLabelIds = nodeData.inputs?.messageRemoveLabelIds + + // Label parameters + const labelName = nodeData.inputs?.labelName + const labelColor = nodeData.inputs?.labelColor + const labelId = nodeData.inputs?.labelId + + // Thread parameters + const threadMaxResults = nodeData.inputs?.threadMaxResults + const threadQuery = nodeData.inputs?.threadQuery + const threadId = nodeData.inputs?.threadId + const threadAddLabelIds = nodeData.inputs?.threadAddLabelIds + const threadRemoveLabelIds = nodeData.inputs?.threadRemoveLabelIds + + // Set default parameters based on actions + actions.forEach((action) => { + const params: ICommonObject = {} + + // Draft action parameters + if (action.startsWith('list') && draftMaxResults) params.maxResults = draftMaxResults + if (action === 'createDraft') { + if (draftTo) params.to = draftTo + if (draftSubject) params.subject = draftSubject + if (draftBody) params.body = draftBody + if (draftCc) params.cc = draftCc + if (draftBcc) params.bcc = draftBcc + } + if (action === 'updateDraft') { + if (draftId) params.draftId = draftId + if (draftUpdateTo) params.to = draftUpdateTo + if (draftUpdateSubject) params.subject = draftUpdateSubject + if (draftUpdateBody) params.body = draftUpdateBody + } + if (['getDraft', 'sendDraft', 'deleteDraft'].includes(action) && draftId) { + params.draftId = draftId + } + + // Message action parameters + if (action === 'listMessages') { + if (messageMaxResults) params.maxResults = messageMaxResults + if (messageQuery) params.query = messageQuery + } + if (action === 'sendMessage') { + if (messageTo) params.to = messageTo + if (messageSubject) params.subject = messageSubject + if (messageBody) params.body = messageBody + if (messageCc) params.cc = messageCc + if (messageBcc) params.bcc = messageBcc + } + if (['getMessage', 'trashMessage', 'untrashMessage', 'deleteMessage'].includes(action) && messageId) { + params.messageId = messageId + } + if (action === 'modifyMessage') { + if (messageId) params.messageId = messageId + if (messageAddLabelIds) params.addLabelIds = messageAddLabelIds.split(',').map((id: string) => id.trim()) + if (messageRemoveLabelIds) params.removeLabelIds = messageRemoveLabelIds.split(',').map((id: string) => id.trim()) + } + + // Label action parameters + if (action === 'createLabel') { + if (labelName) params.labelName = labelName + if (labelColor) params.labelColor = labelColor + } + if (['getLabel', 'updateLabel', 'deleteLabel'].includes(action) && labelId) { + params.labelId = labelId + } + if (action === 'updateLabel') { + if (labelName) params.labelName = labelName + if (labelColor) params.labelColor = labelColor + } + + // Thread action parameters + if (action === 'listThreads') { + if (threadMaxResults) params.maxResults = threadMaxResults + if (threadQuery) params.query = threadQuery + } + if (['getThread', 'trashThread', 'untrashThread', 'deleteThread'].includes(action) && threadId) { + params.threadId = threadId + } + if (action === 'modifyThread') { + if (threadId) params.threadId = threadId + if (threadAddLabelIds) params.addLabelIds = threadAddLabelIds.split(',').map((id: string) => id.trim()) + if (threadRemoveLabelIds) params.removeLabelIds = threadRemoveLabelIds.split(',').map((id: string) => id.trim()) + } + + defaultParams[action] = params + }) + + // Create and return tools based on selected actions + const tools = createGmailTools({ + actions, + accessToken, + defaultParams, + maxOutputLength: Infinity + }) + + return tools + } +} + +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..e1883e56bfa --- /dev/null +++ b/packages/components/nodes/tools/Gmail/core.ts @@ -0,0 +1,1226 @@ +import { z } from 'zod' +import fetch from 'node-fetch' +import { DynamicStructuredTool } from '../OpenAPIToolkit/core' + +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 + maxOutputLength?: number + 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 = '' + protected maxOutputLength: number = Infinity + + constructor(args: any) { + super(args) + this.accessToken = args.accessToken ?? '' + this.maxOutputLength = args.maxOutputLength ?? Infinity + } + + async makeGmailRequest(url: string, method: string = 'GET', body?: 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.slice(0, this.maxOutputLength) + } + + 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, 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.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) + 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, maxOutputLength: args.maxOutputLength }) + 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) + 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, maxOutputLength: args.maxOutputLength }) + 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) + 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, maxOutputLength: args.maxOutputLength }) + 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) + 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, maxOutputLength: args.maxOutputLength }) + 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 }) + 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, maxOutputLength: args.maxOutputLength }) + 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') + 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, 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.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) + 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, maxOutputLength: args.maxOutputLength }) + 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) + 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, maxOutputLength: args.maxOutputLength }) + 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) + 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, maxOutputLength: args.maxOutputLength }) + 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) + 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, maxOutputLength: args.maxOutputLength }) + 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') + 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, maxOutputLength: args.maxOutputLength }) + 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') + 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, maxOutputLength: args.maxOutputLength }) + 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') + 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, maxOutputLength: args.maxOutputLength }) + 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) + 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, maxOutputLength: args.maxOutputLength }) + 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) + 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, maxOutputLength: args.maxOutputLength }) + 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) + 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, maxOutputLength: args.maxOutputLength }) + 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) + 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, maxOutputLength: args.maxOutputLength }) + 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') + 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, 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.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) + 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, maxOutputLength: args.maxOutputLength }) + 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) + 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, maxOutputLength: args.maxOutputLength }) + 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) + 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, maxOutputLength: args.maxOutputLength }) + 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') + 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, maxOutputLength: args.maxOutputLength }) + 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') + 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, maxOutputLength: args.maxOutputLength }) + 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') + 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 maxOutputLength = args?.maxOutputLength || Infinity + const defaultParams = args?.defaultParams || {} + + // Draft tools + if (actions.includes('listDrafts')) { + tools.push( + new ListDraftsTool({ + accessToken, + maxOutputLength, + defaultParams: defaultParams.listDrafts + }) + ) + } + + if (actions.includes('createDraft')) { + tools.push( + new CreateDraftTool({ + accessToken, + maxOutputLength, + defaultParams: defaultParams.createDraft + }) + ) + } + + if (actions.includes('getDraft')) { + tools.push( + new GetDraftTool({ + accessToken, + maxOutputLength, + defaultParams: defaultParams.getDraft + }) + ) + } + + if (actions.includes('updateDraft')) { + tools.push( + new UpdateDraftTool({ + accessToken, + maxOutputLength, + defaultParams: defaultParams.updateDraft + }) + ) + } + + if (actions.includes('sendDraft')) { + tools.push( + new SendDraftTool({ + accessToken, + maxOutputLength, + defaultParams: defaultParams.sendDraft + }) + ) + } + + if (actions.includes('deleteDraft')) { + tools.push( + new DeleteDraftTool({ + accessToken, + maxOutputLength, + defaultParams: defaultParams.deleteDraft + }) + ) + } + + // Message tools + if (actions.includes('listMessages')) { + tools.push( + new ListMessagesTool({ + accessToken, + maxOutputLength, + defaultParams: defaultParams.listMessages + }) + ) + } + + if (actions.includes('getMessage')) { + tools.push( + new GetMessageTool({ + accessToken, + maxOutputLength, + defaultParams: defaultParams.getMessage + }) + ) + } + + if (actions.includes('sendMessage')) { + tools.push( + new SendMessageTool({ + accessToken, + maxOutputLength, + defaultParams: defaultParams.sendMessage + }) + ) + } + + if (actions.includes('modifyMessage')) { + tools.push( + new ModifyMessageTool({ + accessToken, + maxOutputLength, + defaultParams: defaultParams.modifyMessage + }) + ) + } + + if (actions.includes('trashMessage')) { + tools.push( + new TrashMessageTool({ + accessToken, + maxOutputLength, + defaultParams: defaultParams.trashMessage + }) + ) + } + + if (actions.includes('untrashMessage')) { + tools.push( + new UntrashMessageTool({ + accessToken, + maxOutputLength, + defaultParams: defaultParams.untrashMessage + }) + ) + } + + if (actions.includes('deleteMessage')) { + tools.push( + new DeleteMessageTool({ + accessToken, + maxOutputLength, + defaultParams: defaultParams.deleteMessage + }) + ) + } + + // Label tools + if (actions.includes('listLabels')) { + tools.push( + new ListLabelsTool({ + accessToken, + maxOutputLength, + defaultParams: defaultParams.listLabels + }) + ) + } + + if (actions.includes('getLabel')) { + tools.push( + new GetLabelTool({ + accessToken, + maxOutputLength, + defaultParams: defaultParams.getLabel + }) + ) + } + + if (actions.includes('createLabel')) { + tools.push( + new CreateLabelTool({ + accessToken, + maxOutputLength, + defaultParams: defaultParams.createLabel + }) + ) + } + + if (actions.includes('updateLabel')) { + tools.push( + new UpdateLabelTool({ + accessToken, + maxOutputLength, + defaultParams: defaultParams.updateLabel + }) + ) + } + + if (actions.includes('deleteLabel')) { + tools.push( + new DeleteLabelTool({ + accessToken, + maxOutputLength, + defaultParams: defaultParams.deleteLabel + }) + ) + } + + // Thread tools + if (actions.includes('listThreads')) { + tools.push( + new ListThreadsTool({ + accessToken, + maxOutputLength, + defaultParams: defaultParams.listThreads + }) + ) + } + + if (actions.includes('getThread')) { + tools.push( + new GetThreadTool({ + accessToken, + maxOutputLength, + defaultParams: defaultParams.getThread + }) + ) + } + + if (actions.includes('modifyThread')) { + tools.push( + new ModifyThreadTool({ + accessToken, + maxOutputLength, + defaultParams: defaultParams.modifyThread + }) + ) + } + + if (actions.includes('trashThread')) { + tools.push( + new TrashThreadTool({ + accessToken, + maxOutputLength, + defaultParams: defaultParams.trashThread + }) + ) + } + + if (actions.includes('untrashThread')) { + tools.push( + new UntrashThreadTool({ + accessToken, + maxOutputLength, + defaultParams: defaultParams.untrashThread + }) + ) + } + + if (actions.includes('deleteThread')) { + tools.push( + new DeleteThreadTool({ + accessToken, + maxOutputLength, + 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/MicrosoftOutlook/MicrosoftOutlook.ts b/packages/components/nodes/tools/MicrosoftOutlook/MicrosoftOutlook.ts new file mode 100644 index 00000000000..1782d55725f --- /dev/null +++ b/packages/components/nodes/tools/MicrosoftOutlook/MicrosoftOutlook.ts @@ -0,0 +1,971 @@ +import { 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 = typeof calendarActions === 'string' ? JSON.parse(calendarActions) : calendarActions + } else if (outlookType === 'message') { + actions = typeof messageActions === 'string' ? JSON.parse(messageActions) : messageActions + } + + // Prepare default parameters for each action based on type + const defaultParams: ICommonObject = {} + + if (outlookType === 'calendar') { + // Map calendar actions to their parameters + actions.forEach((action) => { + defaultParams[action] = {} + + switch (action) { + case 'listCalendars': + if (nodeData.inputs?.maxResultsListCalendars) { + defaultParams[action].maxResults = nodeData.inputs.maxResultsListCalendars + } + break + + case 'getCalendar': + if (nodeData.inputs?.calendarIdGetCalendar) { + defaultParams[action].calendarId = nodeData.inputs.calendarIdGetCalendar + } + break + + case 'createCalendar': + if (nodeData.inputs?.calendarNameCreateCalendar) { + defaultParams[action].calendarName = nodeData.inputs.calendarNameCreateCalendar + } + break + + case 'updateCalendar': + if (nodeData.inputs?.calendarIdUpdateCalendar) { + defaultParams[action].calendarId = nodeData.inputs.calendarIdUpdateCalendar + } + if (nodeData.inputs?.calendarNameUpdateCalendar) { + defaultParams[action].calendarName = nodeData.inputs.calendarNameUpdateCalendar + } + break + + case 'deleteCalendar': + if (nodeData.inputs?.calendarIdDeleteCalendar) { + defaultParams[action].calendarId = nodeData.inputs.calendarIdDeleteCalendar + } + break + + case 'listEvents': + if (nodeData.inputs?.calendarIdListEvents) { + defaultParams[action].calendarId = nodeData.inputs.calendarIdListEvents + } + if (nodeData.inputs?.maxResultsListEvents) { + defaultParams[action].maxResults = nodeData.inputs.maxResultsListEvents + } + if (nodeData.inputs?.startDateTimeListEvents) { + defaultParams[action].startDateTime = nodeData.inputs.startDateTimeListEvents + } + if (nodeData.inputs?.endDateTimeListEvents) { + defaultParams[action].endDateTime = nodeData.inputs.endDateTimeListEvents + } + break + + case 'getEvent': + if (nodeData.inputs?.eventIdGetEvent) { + defaultParams[action].eventId = nodeData.inputs.eventIdGetEvent + } + break + + case 'createEvent': + if (nodeData.inputs?.subjectCreateEvent) { + defaultParams[action].subject = nodeData.inputs.subjectCreateEvent + } + if (nodeData.inputs?.bodyCreateEvent) { + defaultParams[action].body = nodeData.inputs.bodyCreateEvent + } + if (nodeData.inputs?.startDateTimeCreateEvent) { + defaultParams[action].startDateTime = nodeData.inputs.startDateTimeCreateEvent + } + if (nodeData.inputs?.endDateTimeCreateEvent) { + defaultParams[action].endDateTime = nodeData.inputs.endDateTimeCreateEvent + } + if (nodeData.inputs?.timeZoneCreateEvent) { + defaultParams[action].timeZone = nodeData.inputs.timeZoneCreateEvent + } + if (nodeData.inputs?.locationCreateEvent) { + defaultParams[action].location = nodeData.inputs.locationCreateEvent + } + if (nodeData.inputs?.attendeesCreateEvent) { + defaultParams[action].attendees = nodeData.inputs.attendeesCreateEvent + } + break + + case 'updateEvent': + if (nodeData.inputs?.eventIdUpdateEvent) { + defaultParams[action].eventId = nodeData.inputs.eventIdUpdateEvent + } + if (nodeData.inputs?.subjectUpdateEvent) { + defaultParams[action].subject = nodeData.inputs.subjectUpdateEvent + } + break + + case 'deleteEvent': + if (nodeData.inputs?.eventIdDeleteEvent) { + defaultParams[action].eventId = nodeData.inputs.eventIdDeleteEvent + } + break + } + }) + } else if (outlookType === 'message') { + // Map message actions to their parameters + actions.forEach((action) => { + defaultParams[action] = {} + + switch (action) { + case 'listMessages': + if (nodeData.inputs?.maxResultsListMessages) { + defaultParams[action].maxResults = nodeData.inputs.maxResultsListMessages + } + if (nodeData.inputs?.filterListMessages) { + defaultParams[action].filter = nodeData.inputs.filterListMessages + } + break + + case 'getMessage': + if (nodeData.inputs?.messageIdGetMessage) { + defaultParams[action].messageId = nodeData.inputs.messageIdGetMessage + } + break + + case 'createDraftMessage': + if (nodeData.inputs?.toCreateDraftMessage) { + defaultParams[action].to = nodeData.inputs.toCreateDraftMessage + } + if (nodeData.inputs?.subjectCreateDraftMessage) { + defaultParams[action].subject = nodeData.inputs.subjectCreateDraftMessage + } + if (nodeData.inputs?.bodyCreateDraftMessage) { + defaultParams[action].body = nodeData.inputs.bodyCreateDraftMessage + } + if (nodeData.inputs?.ccCreateDraftMessage) { + defaultParams[action].cc = nodeData.inputs.ccCreateDraftMessage + } + if (nodeData.inputs?.bccCreateDraftMessage) { + defaultParams[action].bcc = nodeData.inputs.bccCreateDraftMessage + } + break + + case 'sendMessage': + if (nodeData.inputs?.toSendMessage) { + defaultParams[action].to = nodeData.inputs.toSendMessage + } + if (nodeData.inputs?.subjectSendMessage) { + defaultParams[action].subject = nodeData.inputs.subjectSendMessage + } + if (nodeData.inputs?.bodySendMessage) { + defaultParams[action].body = nodeData.inputs.bodySendMessage + } + break + + case 'updateMessage': + if (nodeData.inputs?.messageIdUpdateMessage) { + defaultParams[action].messageId = nodeData.inputs.messageIdUpdateMessage + } + if (nodeData.inputs?.isReadUpdateMessage !== undefined) { + defaultParams[action].isRead = nodeData.inputs.isReadUpdateMessage + } + break + + case 'deleteMessage': + if (nodeData.inputs?.messageIdDeleteMessage) { + defaultParams[action].messageId = nodeData.inputs.messageIdDeleteMessage + } + break + + case 'copyMessage': + if (nodeData.inputs?.messageIdCopyMessage) { + defaultParams[action].messageId = nodeData.inputs.messageIdCopyMessage + } + if (nodeData.inputs?.destinationFolderIdCopyMessage) { + defaultParams[action].destinationFolderId = nodeData.inputs.destinationFolderIdCopyMessage + } + break + + case 'moveMessage': + if (nodeData.inputs?.messageIdMoveMessage) { + defaultParams[action].messageId = nodeData.inputs.messageIdMoveMessage + } + if (nodeData.inputs?.destinationFolderIdMoveMessage) { + defaultParams[action].destinationFolderId = nodeData.inputs.destinationFolderIdMoveMessage + } + break + + case 'replyMessage': + if (nodeData.inputs?.messageIdReplyMessage) { + defaultParams[action].messageId = nodeData.inputs.messageIdReplyMessage + } + if (nodeData.inputs?.replyBodyReplyMessage) { + defaultParams[action].replyBody = nodeData.inputs.replyBodyReplyMessage + } + break + + case 'forwardMessage': + if (nodeData.inputs?.messageIdForwardMessage) { + defaultParams[action].messageId = nodeData.inputs.messageIdForwardMessage + } + if (nodeData.inputs?.forwardToForwardMessage) { + defaultParams[action].forwardTo = nodeData.inputs.forwardToForwardMessage + } + if (nodeData.inputs?.forwardCommentForwardMessage) { + defaultParams[action].forwardComment = nodeData.inputs.forwardCommentForwardMessage + } + break + } + }) + } + + const outlookTools = createOutlookTools({ + accessToken, + actions, + defaultParams + }) + + return outlookTools + } +} + +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..6289d889931 --- /dev/null +++ b/packages/components/nodes/tools/MicrosoftOutlook/core.ts @@ -0,0 +1,1052 @@ +import { z } from 'zod' +import fetch from 'node-fetch' +import { DynamicStructuredTool } from '../OpenAPIToolkit/core' + +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 + maxOutputLength?: number + 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 = '' + protected maxOutputLength: number = Infinity + + constructor(args: any) { + super(args) + this.accessToken = args.accessToken ?? '' + this.maxOutputLength = args.maxOutputLength ?? Infinity + } + + async makeGraphRequest(url: string, method: string = 'GET', body?: 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.slice(0, this.maxOutputLength) + } + + 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, 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('$top', params.maxResults.toString()) + + const url = `https://graph.microsoft.com/v1.0/me/calendars?${queryParams.toString()}` + + try { + const response = await this.makeGraphRequest(url) + 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, maxOutputLength: args.maxOutputLength }) + 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) + 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, maxOutputLength: args.maxOutputLength }) + 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) + 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, maxOutputLength: args.maxOutputLength }) + 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) + 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, maxOutputLength: args.maxOutputLength }) + 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') + 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, 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('$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) + 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, maxOutputLength: args.maxOutputLength }) + 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) + 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, maxOutputLength: args.maxOutputLength }) + 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) + 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, maxOutputLength: args.maxOutputLength }) + 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) + 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, maxOutputLength: args.maxOutputLength }) + 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') + 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, 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('$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) + 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, maxOutputLength: args.maxOutputLength }) + 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) + 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, maxOutputLength: args.maxOutputLength }) + 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) + 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, maxOutputLength: args.maxOutputLength }) + 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) + 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, maxOutputLength: args.maxOutputLength }) + 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) + 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, maxOutputLength: args.maxOutputLength }) + 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') + 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, maxOutputLength: args.maxOutputLength }) + 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) + 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, maxOutputLength: args.maxOutputLength }) + 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) + 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, maxOutputLength: args.maxOutputLength }) + 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) + 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, maxOutputLength: args.maxOutputLength }) + 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) + 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 maxOutputLength = args?.maxOutputLength || Infinity + const defaultParams = args?.defaultParams || {} + + // Calendar tools + if (actions.includes('listCalendars')) { + const listTool = new ListCalendarsTool({ + accessToken, + maxOutputLength, + defaultParams: defaultParams.listCalendars + }) + tools.push(listTool) + } + + if (actions.includes('getCalendar')) { + const getTool = new GetCalendarTool({ + accessToken, + maxOutputLength, + defaultParams: defaultParams.getCalendar + }) + tools.push(getTool) + } + + if (actions.includes('createCalendar')) { + const createTool = new CreateCalendarTool({ + accessToken, + maxOutputLength, + defaultParams: defaultParams.createCalendar + }) + tools.push(createTool) + } + + if (actions.includes('updateCalendar')) { + const updateTool = new UpdateCalendarTool({ + accessToken, + maxOutputLength, + defaultParams: defaultParams.updateCalendar + }) + tools.push(updateTool) + } + + if (actions.includes('deleteCalendar')) { + const deleteTool = new DeleteCalendarTool({ + accessToken, + maxOutputLength, + defaultParams: defaultParams.deleteCalendar + }) + tools.push(deleteTool) + } + + if (actions.includes('listEvents')) { + const listTool = new ListEventsTool({ + accessToken, + maxOutputLength, + defaultParams: defaultParams.listEvents + }) + tools.push(listTool) + } + + if (actions.includes('getEvent')) { + const getTool = new GetEventTool({ + accessToken, + maxOutputLength, + defaultParams: defaultParams.getEvent + }) + tools.push(getTool) + } + + if (actions.includes('createEvent')) { + const createTool = new CreateEventTool({ + accessToken, + maxOutputLength, + defaultParams: defaultParams.createEvent + }) + tools.push(createTool) + } + + if (actions.includes('updateEvent')) { + const updateTool = new UpdateEventTool({ + accessToken, + maxOutputLength, + defaultParams: defaultParams.updateEvent + }) + tools.push(updateTool) + } + + if (actions.includes('deleteEvent')) { + const deleteTool = new DeleteEventTool({ + accessToken, + maxOutputLength, + defaultParams: defaultParams.deleteEvent + }) + tools.push(deleteTool) + } + + // Message tools + if (actions.includes('listMessages')) { + const listTool = new ListMessagesTool({ + accessToken, + maxOutputLength, + defaultParams: defaultParams.listMessages + }) + tools.push(listTool) + } + + if (actions.includes('getMessage')) { + const getTool = new GetMessageTool({ + accessToken, + maxOutputLength, + defaultParams: defaultParams.getMessage + }) + tools.push(getTool) + } + + if (actions.includes('createDraftMessage')) { + const createTool = new CreateDraftMessageTool({ + accessToken, + maxOutputLength, + defaultParams: defaultParams.createDraftMessage + }) + tools.push(createTool) + } + + if (actions.includes('sendMessage')) { + const sendTool = new SendMessageTool({ + accessToken, + maxOutputLength, + defaultParams: defaultParams.sendMessage + }) + tools.push(sendTool) + } + + if (actions.includes('updateMessage')) { + const updateTool = new UpdateMessageTool({ + accessToken, + maxOutputLength, + defaultParams: defaultParams.updateMessage + }) + tools.push(updateTool) + } + + if (actions.includes('deleteMessage')) { + const deleteTool = new DeleteMessageTool({ + accessToken, + maxOutputLength, + defaultParams: defaultParams.deleteMessage + }) + tools.push(deleteTool) + } + + if (actions.includes('copyMessage')) { + const copyTool = new CopyMessageTool({ + accessToken, + maxOutputLength, + defaultParams: defaultParams.copyMessage + }) + tools.push(copyTool) + } + + if (actions.includes('moveMessage')) { + const moveTool = new MoveMessageTool({ + accessToken, + maxOutputLength, + defaultParams: defaultParams.moveMessage + }) + tools.push(moveTool) + } + + if (actions.includes('replyMessage')) { + const replyTool = new ReplyMessageTool({ + accessToken, + maxOutputLength, + defaultParams: defaultParams.replyMessage + }) + tools.push(replyTool) + } + + if (actions.includes('forwardMessage')) { + const forwardTool = new ForwardMessageTool({ + accessToken, + maxOutputLength, + 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..2dd43d72c60 --- /dev/null +++ b/packages/components/nodes/tools/MicrosoftTeams/MicrosoftTeams.ts @@ -0,0 +1,1226 @@ +import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' +import { 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 = typeof channelActions === 'string' ? JSON.parse(channelActions) : channelActions + } else if (teamsType === 'chat') { + actions = typeof chatActions === 'string' ? JSON.parse(chatActions) : chatActions + } else if (teamsType === 'chatMessage') { + actions = typeof chatMessageActions === 'string' ? JSON.parse(chatMessageActions) : 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') + } + + // Prepare default parameters for each action based on type + const defaultParams: ICommonObject = {} + + if (teamsType === 'channel') { + // Map channel actions to their parameters + actions.forEach((action) => { + defaultParams[action] = {} + + switch (action) { + case 'listChannels': + if (nodeData.inputs?.teamIdListChannels) { + defaultParams[action].teamId = nodeData.inputs.teamIdListChannels + } + if (nodeData.inputs?.maxResultsListChannels) { + defaultParams[action].maxResults = nodeData.inputs.maxResultsListChannels + } + break + + case 'getChannel': + if (nodeData.inputs?.teamIdGetChannel) { + defaultParams[action].teamId = nodeData.inputs.teamIdGetChannel + } + if (nodeData.inputs?.channelIdGetChannel) { + defaultParams[action].channelId = nodeData.inputs.channelIdGetChannel + } + break + + case 'createChannel': + if (nodeData.inputs?.teamIdCreateChannel) { + defaultParams[action].teamId = nodeData.inputs.teamIdCreateChannel + } + if (nodeData.inputs?.displayNameCreateChannel) { + defaultParams[action].displayName = nodeData.inputs.displayNameCreateChannel + } + if (nodeData.inputs?.descriptionCreateChannel) { + defaultParams[action].description = nodeData.inputs.descriptionCreateChannel + } + if (nodeData.inputs?.membershipTypeCreateChannel) { + defaultParams[action].membershipType = nodeData.inputs.membershipTypeCreateChannel + } + break + + case 'updateChannel': + if (nodeData.inputs?.teamIdUpdateChannel) { + defaultParams[action].teamId = nodeData.inputs.teamIdUpdateChannel + } + if (nodeData.inputs?.channelIdUpdateChannel) { + defaultParams[action].channelId = nodeData.inputs.channelIdUpdateChannel + } + if (nodeData.inputs?.displayNameUpdateChannel) { + defaultParams[action].displayName = nodeData.inputs.displayNameUpdateChannel + } + break + + case 'deleteChannel': + case 'archiveChannel': + case 'unarchiveChannel': + if (nodeData.inputs?.teamIdDeleteChannel) { + defaultParams[action].teamId = nodeData.inputs.teamIdDeleteChannel + } + if (nodeData.inputs?.channelIdDeleteChannel) { + defaultParams[action].channelId = nodeData.inputs.channelIdDeleteChannel + } + break + + case 'listChannelMembers': + if (nodeData.inputs?.teamIdChannelMembers) { + defaultParams[action].teamId = nodeData.inputs.teamIdChannelMembers + } + if (nodeData.inputs?.channelIdChannelMembers) { + defaultParams[action].channelId = nodeData.inputs.channelIdChannelMembers + } + break + + case 'addChannelMember': + case 'removeChannelMember': + if (nodeData.inputs?.teamIdChannelMembers) { + defaultParams[action].teamId = nodeData.inputs.teamIdChannelMembers + } + if (nodeData.inputs?.channelIdChannelMembers) { + defaultParams[action].channelId = nodeData.inputs.channelIdChannelMembers + } + if (nodeData.inputs?.userIdChannelMember) { + defaultParams[action].userId = nodeData.inputs.userIdChannelMember + } + break + } + }) + } else if (teamsType === 'chat') { + // Map chat actions to their parameters + actions.forEach((action) => { + defaultParams[action] = {} + + switch (action) { + case 'listChats': + if (nodeData.inputs?.maxResultsListChats) { + defaultParams[action].maxResults = nodeData.inputs.maxResultsListChats + } + break + + case 'getChat': + if (nodeData.inputs?.chatIdGetChat) { + defaultParams[action].chatId = nodeData.inputs.chatIdGetChat + } + break + + case 'createChat': + if (nodeData.inputs?.chatTypeCreateChat) { + defaultParams[action].chatType = nodeData.inputs.chatTypeCreateChat + } + if (nodeData.inputs?.topicCreateChat) { + defaultParams[action].topic = nodeData.inputs.topicCreateChat + } + if (nodeData.inputs?.membersCreateChat) { + defaultParams[action].members = nodeData.inputs.membersCreateChat + } + break + + case 'updateChat': + if (nodeData.inputs?.chatIdUpdateChat) { + defaultParams[action].chatId = nodeData.inputs.chatIdUpdateChat + } + if (nodeData.inputs?.topicUpdateChat) { + defaultParams[action].topic = nodeData.inputs.topicUpdateChat + } + break + + case 'deleteChat': + if (nodeData.inputs?.chatIdDeleteChat) { + defaultParams[action].chatId = nodeData.inputs.chatIdDeleteChat + } + break + + case 'listChatMembers': + if (nodeData.inputs?.chatIdChatMembers) { + defaultParams[action].chatId = nodeData.inputs.chatIdChatMembers + } + break + + case 'addChatMember': + case 'removeChatMember': + if (nodeData.inputs?.chatIdChatMembers) { + defaultParams[action].chatId = nodeData.inputs.chatIdChatMembers + } + if (nodeData.inputs?.userIdChatMember) { + defaultParams[action].userId = nodeData.inputs.userIdChatMember + } + break + + case 'pinMessage': + case 'unpinMessage': + if (nodeData.inputs?.chatIdPinMessage) { + defaultParams[action].chatId = nodeData.inputs.chatIdPinMessage + } + if (nodeData.inputs?.messageIdPinMessage) { + defaultParams[action].messageId = nodeData.inputs.messageIdPinMessage + } + break + } + }) + } else if (teamsType === 'chatMessage') { + // Map chat message actions to their parameters + actions.forEach((action) => { + defaultParams[action] = {} + + switch (action) { + case 'listMessages': + if (nodeData.inputs?.chatChannelIdListMessages) { + defaultParams[action].chatChannelId = nodeData.inputs.chatChannelIdListMessages + } + if (nodeData.inputs?.teamIdListMessages) { + defaultParams[action].teamId = nodeData.inputs.teamIdListMessages + } + if (nodeData.inputs?.maxResultsListMessages) { + defaultParams[action].maxResults = nodeData.inputs.maxResultsListMessages + } + break + + case 'getMessage': + if (nodeData.inputs?.chatChannelIdGetMessage) { + defaultParams[action].chatChannelId = nodeData.inputs.chatChannelIdGetMessage + } + if (nodeData.inputs?.teamIdGetMessage) { + defaultParams[action].teamId = nodeData.inputs.teamIdGetMessage + } + if (nodeData.inputs?.messageIdGetMessage) { + defaultParams[action].messageId = nodeData.inputs.messageIdGetMessage + } + break + + case 'sendMessage': + if (nodeData.inputs?.chatChannelIdSendMessage) { + defaultParams[action].chatChannelId = nodeData.inputs.chatChannelIdSendMessage + } + if (nodeData.inputs?.teamIdSendMessage) { + defaultParams[action].teamId = nodeData.inputs.teamIdSendMessage + } + if (nodeData.inputs?.messageBodySendMessage) { + defaultParams[action].messageBody = nodeData.inputs.messageBodySendMessage + } + if (nodeData.inputs?.contentTypeSendMessage) { + defaultParams[action].contentType = nodeData.inputs.contentTypeSendMessage + } + break + + case 'updateMessage': + if (nodeData.inputs?.chatChannelIdUpdateMessage) { + defaultParams[action].chatChannelId = nodeData.inputs.chatChannelIdUpdateMessage + } + if (nodeData.inputs?.teamIdUpdateMessage) { + defaultParams[action].teamId = nodeData.inputs.teamIdUpdateMessage + } + if (nodeData.inputs?.messageIdUpdateMessage) { + defaultParams[action].messageId = nodeData.inputs.messageIdUpdateMessage + } + break + + case 'deleteMessage': + if (nodeData.inputs?.chatChannelIdDeleteMessage) { + defaultParams[action].chatChannelId = nodeData.inputs.chatChannelIdDeleteMessage + } + if (nodeData.inputs?.teamIdDeleteMessage) { + defaultParams[action].teamId = nodeData.inputs.teamIdDeleteMessage + } + if (nodeData.inputs?.messageIdDeleteMessage) { + defaultParams[action].messageId = nodeData.inputs.messageIdDeleteMessage + } + break + + case 'replyToMessage': + if (nodeData.inputs?.chatChannelIdReplyMessage) { + defaultParams[action].chatChannelId = nodeData.inputs.chatChannelIdReplyMessage + } + if (nodeData.inputs?.teamIdReplyMessage) { + defaultParams[action].teamId = nodeData.inputs.teamIdReplyMessage + } + if (nodeData.inputs?.messageIdReplyMessage) { + defaultParams[action].messageId = nodeData.inputs.messageIdReplyMessage + } + if (nodeData.inputs?.replyBodyReplyMessage) { + defaultParams[action].replyBody = nodeData.inputs.replyBodyReplyMessage + } + break + + case 'setReaction': + if (nodeData.inputs?.chatChannelIdReaction) { + defaultParams[action].chatChannelId = nodeData.inputs.chatChannelIdReaction + } + if (nodeData.inputs?.teamIdReaction) { + defaultParams[action].teamId = nodeData.inputs.teamIdReaction + } + if (nodeData.inputs?.messageIdReaction) { + defaultParams[action].messageId = nodeData.inputs.messageIdReaction + } + if (nodeData.inputs?.reactionTypeSetReaction) { + defaultParams[action].reactionType = nodeData.inputs.reactionTypeSetReaction + } + break + + case 'unsetReaction': + if (nodeData.inputs?.chatChannelIdReaction) { + defaultParams[action].chatChannelId = nodeData.inputs.chatChannelIdReaction + } + if (nodeData.inputs?.teamIdReaction) { + defaultParams[action].teamId = nodeData.inputs.teamIdReaction + } + if (nodeData.inputs?.messageIdReaction) { + defaultParams[action].messageId = nodeData.inputs.messageIdReaction + } + break + + case 'getAllMessages': + // getAllMessages might use similar params to listMessages + if (nodeData.inputs?.chatChannelIdListMessages) { + defaultParams[action].chatChannelId = nodeData.inputs.chatChannelIdListMessages + } + if (nodeData.inputs?.teamIdListMessages) { + defaultParams[action].teamId = nodeData.inputs.teamIdListMessages + } + break + } + }) + } + + const teamsTools = createTeamsTools({ + accessToken, + actions, + defaultParams, + type: teamsType + }) + + return teamsTools + } +} + +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..eaf455ced9e --- /dev/null +++ b/packages/components/nodes/tools/MicrosoftTeams/core.ts @@ -0,0 +1,1782 @@ +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) + } + + // 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) + + return ( + JSON.stringify({ + success: true, + channels: limitedChannels, + count: limitedChannels.length, + total: channels.length + }) + + TOOL_ARGS_PREFIX + + JSON.stringify(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 ( + JSON.stringify({ + success: true, + channel: result + }) + + TOOL_ARGS_PREFIX + + JSON.stringify(params) + ) + } catch (error) { + return `Error getting channel: ${error}` + } + } +} + +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 ( + JSON.stringify({ + success: true, + channel: result, + message: `Channel "${displayName}" created successfully` + }) + + TOOL_ARGS_PREFIX + + JSON.stringify(params) + ) + } catch (error) { + return `Error creating channel: ${error}` + } + } +} + +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 ( + JSON.stringify({ + success: true, + message: 'Channel updated successfully' + }) + + TOOL_ARGS_PREFIX + + JSON.stringify(params) + ) + } catch (error) { + return `Error updating channel: ${error}` + } + } +} + +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 ( + JSON.stringify({ + success: true, + message: 'Channel deleted successfully' + }) + + TOOL_ARGS_PREFIX + + JSON.stringify(params) + ) + } catch (error) { + return `Error deleting channel: ${error}` + } + } +} + +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 ( + JSON.stringify({ + success: true, + message: 'Channel archived successfully' + }) + + TOOL_ARGS_PREFIX + + JSON.stringify(params) + ) + } catch (error) { + return `Error archiving channel: ${error}` + } + } +} + +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 ( + JSON.stringify({ + success: true, + message: 'Channel unarchived successfully' + }) + + TOOL_ARGS_PREFIX + + JSON.stringify(params) + ) + } catch (error) { + return `Error unarchiving channel: ${error}` + } + } +} + +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 ( + JSON.stringify({ + success: true, + members: result.value || [], + count: result.value?.length || 0 + }) + + TOOL_ARGS_PREFIX + + JSON.stringify(params) + ) + } catch (error) { + return `Error listing channel members: ${error}` + } + } +} + +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 ( + JSON.stringify({ + success: true, + message: 'Member added to channel successfully' + }) + + TOOL_ARGS_PREFIX + + JSON.stringify(params) + ) + } catch (error) { + return `Error adding channel member: ${error}` + } + } +} + +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 ( + JSON.stringify({ + success: true, + message: 'Member removed from channel successfully' + }) + + TOOL_ARGS_PREFIX + + JSON.stringify(params) + ) + } catch (error) { + return `Error removing channel member: ${error}` + } + } +} + +// 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 ( + JSON.stringify({ + success: true, + chats: result.value || [], + count: result.value?.length || 0 + }) + + TOOL_ARGS_PREFIX + + JSON.stringify(params) + ) + } catch (error) { + return `Error listing chats: ${error}` + } + } +} + +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 ( + JSON.stringify({ + success: true, + chat: result + }) + + TOOL_ARGS_PREFIX + + JSON.stringify(params) + ) + } catch (error) { + return `Error getting chat: ${error}` + } + } +} + +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 ( + JSON.stringify({ + success: true, + chat: result, + message: 'Chat created successfully' + }) + + TOOL_ARGS_PREFIX + + JSON.stringify(params) + ) + } catch (error) { + return `Error creating chat: ${error}` + } + } +} + +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 ( + JSON.stringify({ + success: true, + message: 'Chat updated successfully' + }) + + TOOL_ARGS_PREFIX + + JSON.stringify(params) + ) + } catch (error) { + return `Error updating chat: ${error}` + } + } +} + +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 ( + JSON.stringify({ + success: true, + message: 'Chat deleted successfully' + }) + + TOOL_ARGS_PREFIX + + JSON.stringify(params) + ) + } catch (error) { + return `Error deleting chat: ${error}` + } + } +} + +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 ( + JSON.stringify({ + success: true, + members: result.value || [], + count: result.value?.length || 0 + }) + + TOOL_ARGS_PREFIX + + JSON.stringify(params) + ) + } catch (error) { + return `Error listing chat members: ${error}` + } + } +} + +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 ( + JSON.stringify({ + success: true, + message: 'Member added to chat successfully' + }) + + TOOL_ARGS_PREFIX + + JSON.stringify(params) + ) + } catch (error) { + return `Error adding chat member: ${error}` + } + } +} + +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 ( + JSON.stringify({ + success: true, + message: 'Member removed from chat successfully' + }) + + TOOL_ARGS_PREFIX + + JSON.stringify(params) + ) + } catch (error) { + return `Error removing chat member: ${error}` + } + } +} + +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 ( + JSON.stringify({ + success: true, + message: 'Message pinned successfully' + }) + + TOOL_ARGS_PREFIX + + JSON.stringify(params) + ) + } catch (error) { + return `Error pinning message: ${error}` + } + } +} + +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 ( + JSON.stringify({ + success: true, + message: 'Message unpinned successfully' + }) + + TOOL_ARGS_PREFIX + + JSON.stringify(params) + ) + } catch (error) { + return `Error unpinning message: ${error}` + } + } +} + +// 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 ( + JSON.stringify({ + success: true, + messages: result.value || [], + count: result.value?.length || 0, + context: teamId ? 'channel' : 'chat' + }) + + TOOL_ARGS_PREFIX + + JSON.stringify(params) + ) + } catch (error) { + return `Error listing messages: ${error}` + } + } +} + +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 ( + JSON.stringify({ + success: true, + message: result, + context: teamId ? 'channel' : 'chat' + }) + + TOOL_ARGS_PREFIX + + JSON.stringify(params) + ) + } catch (error) { + return `Error getting message: ${error}` + } + } +} + +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 ( + JSON.stringify({ + success: true, + message: result, + context: teamId ? 'channel' : 'chat', + messageText: 'Message sent successfully' + }) + + TOOL_ARGS_PREFIX + + JSON.stringify(params) + ) + } catch (error) { + return `Error sending message: ${error}` + } + } +} + +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 ( + JSON.stringify({ + success: true, + message: 'Message updated successfully', + context: teamId ? 'channel' : 'chat' + }) + + TOOL_ARGS_PREFIX + + JSON.stringify(params) + ) + } catch (error) { + return `Error updating message: ${error}` + } + } +} + +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 ( + JSON.stringify({ + success: true, + message: 'Message deleted successfully', + context: teamId ? 'channel' : 'chat' + }) + + TOOL_ARGS_PREFIX + + JSON.stringify(params) + ) + } catch (error) { + return `Error deleting message: ${error}` + } + } +} + +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 ( + JSON.stringify({ + success: true, + reply: result, + message: 'Reply sent successfully', + context: teamId ? 'channel' : 'chat' + }) + + TOOL_ARGS_PREFIX + + JSON.stringify(params) + ) + } catch (error) { + return `Error replying to message: ${error}` + } + } +} + +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 ( + JSON.stringify({ + success: true, + message: `Reaction "${reactionType}" set successfully`, + context: teamId ? 'channel' : 'chat' + }) + + TOOL_ARGS_PREFIX + + JSON.stringify(params) + ) + } catch (error) { + return `Error setting reaction: ${error}` + } + } +} + +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 ( + JSON.stringify({ + success: true, + message: `Reaction "${reactionType}" removed successfully`, + context: teamId ? 'channel' : 'chat' + }) + + TOOL_ARGS_PREFIX + + JSON.stringify(params) + ) + } catch (error) { + return `Error unsetting reaction: ${error}` + } + } +} + +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 ( + JSON.stringify({ + success: true, + messages: chatResult.value || [], + count: chatResult.value?.length || 0, + source: 'all_chats_and_channels' + }) + + TOOL_ARGS_PREFIX + + JSON.stringify(params) + ) + } catch (error) { + return `Error getting all messages: ${error}` + } + } +} + +// 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/src/agentflowv2Generator.ts b/packages/components/src/agentflowv2Generator.ts index c5765f5211e..9bafee93440 100644 --- a/packages/components/src/agentflowv2Generator.ts +++ b/packages/components/src/agentflowv2Generator.ts @@ -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..c8e9fe00ad1 100644 --- a/packages/components/src/utils.ts +++ b/packages/components/src/utils.ts @@ -1215,3 +1215,68 @@ export const handleDocumentLoaderDocuments = async (loader: DocumentLoader, text return docs } + +/** + * 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/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/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/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/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/utils/genericHelper.js b/packages/ui/src/utils/genericHelper.js index dae926a8162..6bafd558657 100644 --- a/packages/ui/src/utils/genericHelper.js +++ b/packages/ui/src/utils/genericHelper.js @@ -1118,42 +1118,87 @@ const _showHideOperation = (nodeData, inputParam, displayType, index) => { if (path.includes('$index')) { path = path.replace('$index', index) } - const groundValue = get(nodeData.inputs, path, '') + let groundValue = get(nodeData.inputs, path, '') + if (groundValue && typeof groundValue === 'string' && groundValue.startsWith('[') && groundValue.endsWith(']')) { + groundValue = JSON.parse(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 + // Handle case where groundValue is an array + if (Array.isArray(groundValue)) { + if (Array.isArray(comparisonValue)) { + // Both are arrays - check if there's any intersection + const hasIntersection = comparisonValue.some((val) => 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/ui/src/views/credentials/AddEditCredentialDialog.jsx b/packages/ui/src/views/credentials/AddEditCredentialDialog.jsx index abd025a6930..fecdb30a74f 100644 --- a/packages/ui/src/views/credentials/AddEditCredentialDialog.jsx +++ b/packages/ui/src/views/credentials/AddEditCredentialDialog.jsx @@ -18,6 +18,7 @@ import { IconHandStop, IconX } from '@tabler/icons-react' // API import credentialsApi from '@/api/credentials' +import oauth2Api from '@/api/oauth2' // Hooks import useApi from '@/hooks/useApi' @@ -212,6 +213,149 @@ const AddEditCredentialDialog = ({ show, dialogProps, onCancel, onConfirm, setEr } } + const setOAuth2 = async () => { + try { + let credentialId = null + + // First save or add the credential + if (dialogProps.type === 'ADD') { + // Add new credential first + const obj = { + name, + credentialName: componentCredential.name, + plainDataObj: credentialData + } + const createResp = await credentialsApi.createCredential(obj) + if (createResp.data) { + credentialId = createResp.data.id + } + } else { + // Save existing credential first + const saveObj = { + name, + credentialName: componentCredential.name + } + + let plainDataObj = {} + for (const key in credentialData) { + if (credentialData[key] !== REDACTED_CREDENTIAL_VALUE) { + plainDataObj[key] = credentialData[key] + } + } + if (Object.keys(plainDataObj).length) saveObj.plainDataObj = plainDataObj + + const saveResp = await credentialsApi.updateCredential(credential.id, saveObj) + if (saveResp.data) { + credentialId = credential.id + } + } + + if (!credentialId) { + throw new Error('Failed to save credential') + } + + const authResponse = await oauth2Api.authorize(credentialId) + + if (authResponse.data && authResponse.data.success && authResponse.data.authorizationUrl) { + // Open the authorization URL in a new window/tab + const authWindow = window.open( + authResponse.data.authorizationUrl, + '_blank', + 'width=600,height=700,scrollbars=yes,resizable=yes' + ) + + if (!authWindow) { + throw new Error('Failed to open authorization window. Please check if popups are blocked.') + } + + // Listen for messages from the popup window + const handleMessage = (event) => { + // Verify origin if needed (you may want to add origin checking) + if (event.data && (event.data.type === 'OAUTH2_SUCCESS' || event.data.type === 'OAUTH2_ERROR')) { + window.removeEventListener('message', handleMessage) + + if (event.data.type === 'OAUTH2_SUCCESS') { + enqueueSnackbar({ + message: 'OAuth2 authorization completed successfully', + options: { + key: new Date().getTime() + Math.random(), + variant: 'success', + 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 && ( From 3a39ff54e1e21e29dd933344df4fdef8cd478a9a Mon Sep 17 00:00:00 2001 From: Henry Date: Wed, 4 Jun 2025 13:55:37 +0100 Subject: [PATCH 2/7] update docs link --- .../{GoogleOAuth2.credential.ts => GmailOAuth2.credential.ts} | 3 +++ .../components/credentials/MsoftOutlookOAuth2.credential.ts | 3 +++ packages/components/credentials/MsoftTeamsOAuth2.credential.ts | 3 +++ 3 files changed, 9 insertions(+) rename packages/components/credentials/{GoogleOAuth2.credential.ts => GmailOAuth2.credential.ts} (89%) diff --git a/packages/components/credentials/GoogleOAuth2.credential.ts b/packages/components/credentials/GmailOAuth2.credential.ts similarity index 89% rename from packages/components/credentials/GoogleOAuth2.credential.ts rename to packages/components/credentials/GmailOAuth2.credential.ts index 2bbf4fe9179..e986e40cb65 100644 --- a/packages/components/credentials/GoogleOAuth2.credential.ts +++ b/packages/components/credentials/GmailOAuth2.credential.ts @@ -11,11 +11,14 @@ class GmailOAuth2 implements INodeCredential { 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', diff --git a/packages/components/credentials/MsoftOutlookOAuth2.credential.ts b/packages/components/credentials/MsoftOutlookOAuth2.credential.ts index 15db1de203d..28340393038 100644 --- a/packages/components/credentials/MsoftOutlookOAuth2.credential.ts +++ b/packages/components/credentials/MsoftOutlookOAuth2.credential.ts @@ -20,12 +20,15 @@ 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', diff --git a/packages/components/credentials/MsoftTeamsOAuth2.credential.ts b/packages/components/credentials/MsoftTeamsOAuth2.credential.ts index 1845e21414b..afe473ef8ee 100644 --- a/packages/components/credentials/MsoftTeamsOAuth2.credential.ts +++ b/packages/components/credentials/MsoftTeamsOAuth2.credential.ts @@ -42,11 +42,14 @@ class MsoftTeamsOAuth2 implements INodeCredential { 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', From 6a4277017cd5d2b45e1b34999418416f852e27a3 Mon Sep 17 00:00:00 2001 From: Henry Date: Wed, 4 Jun 2025 14:00:53 +0100 Subject: [PATCH 3/7] update credentials for oauth2 --- packages/components/credentials/GmailOAuth2.credential.ts | 2 +- ...Auth2.credential.ts => MicrosoftOutlookOAuth2.credential.ts} | 2 +- ...sOAuth2.credential.ts => MicrosoftTeamsOAuth2.credential.ts} | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename packages/components/credentials/{MsoftOutlookOAuth2.credential.ts => MicrosoftOutlookOAuth2.credential.ts} (98%) rename packages/components/credentials/{MsoftTeamsOAuth2.credential.ts => MicrosoftTeamsOAuth2.credential.ts} (98%) diff --git a/packages/components/credentials/GmailOAuth2.credential.ts b/packages/components/credentials/GmailOAuth2.credential.ts index e986e40cb65..38d23a15472 100644 --- a/packages/components/credentials/GmailOAuth2.credential.ts +++ b/packages/components/credentials/GmailOAuth2.credential.ts @@ -40,7 +40,7 @@ class GmailOAuth2 implements INodeCredential { { label: 'Client Secret', name: 'clientSecret', - type: 'string' + type: 'password' }, { label: 'Additional Parameters', diff --git a/packages/components/credentials/MsoftOutlookOAuth2.credential.ts b/packages/components/credentials/MicrosoftOutlookOAuth2.credential.ts similarity index 98% rename from packages/components/credentials/MsoftOutlookOAuth2.credential.ts rename to packages/components/credentials/MicrosoftOutlookOAuth2.credential.ts index 28340393038..0308969a467 100644 --- a/packages/components/credentials/MsoftOutlookOAuth2.credential.ts +++ b/packages/components/credentials/MicrosoftOutlookOAuth2.credential.ts @@ -50,7 +50,7 @@ class MsoftOutlookOAuth2 implements INodeCredential { { label: 'Client Secret', name: 'clientSecret', - type: 'string' + type: 'password' }, { label: 'Scope', diff --git a/packages/components/credentials/MsoftTeamsOAuth2.credential.ts b/packages/components/credentials/MicrosoftTeamsOAuth2.credential.ts similarity index 98% rename from packages/components/credentials/MsoftTeamsOAuth2.credential.ts rename to packages/components/credentials/MicrosoftTeamsOAuth2.credential.ts index afe473ef8ee..ffda846ae0c 100644 --- a/packages/components/credentials/MsoftTeamsOAuth2.credential.ts +++ b/packages/components/credentials/MicrosoftTeamsOAuth2.credential.ts @@ -71,7 +71,7 @@ class MsoftTeamsOAuth2 implements INodeCredential { { label: 'Client Secret', name: 'clientSecret', - type: 'string' + type: 'password' }, { label: 'Scope', From bece1dc62bf3e450633153c5d94a96948e09f6a0 Mon Sep 17 00:00:00 2001 From: Henry Date: Wed, 4 Jun 2025 19:33:08 +0100 Subject: [PATCH 4/7] add jira tool --- packages/components/nodes/tools/Jira/Jira.ts | 504 +++++++ packages/components/nodes/tools/Jira/core.ts | 1172 +++++++++++++++++ packages/components/nodes/tools/Jira/jira.svg | 2 + 3 files changed, 1678 insertions(+) create mode 100644 packages/components/nodes/tools/Jira/Jira.ts create mode 100644 packages/components/nodes/tools/Jira/core.ts create mode 100644 packages/components/nodes/tools/Jira/jira.svg diff --git a/packages/components/nodes/tools/Jira/Jira.ts b/packages/components/nodes/tools/Jira/Jira.ts new file mode 100644 index 00000000000..deedca3f8ee --- /dev/null +++ b/packages/components/nodes/tools/Jira/Jira.ts @@ -0,0 +1,504 @@ +import { 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 = nodeData.inputs?.issueActions ? JSON.parse(nodeData.inputs?.issueActions) : [] + } else if (jiraType === 'comments') { + actions = nodeData.inputs?.commentActions ? JSON.parse(nodeData.inputs?.commentActions) : [] + } else if (jiraType === 'users') { + actions = nodeData.inputs?.userActions ? JSON.parse(nodeData.inputs?.userActions) : [] + } + + // Prepare default parameters for each action + const defaultParams: ICommonObject = {} + + // Issue parameters + const projectKey = nodeData.inputs?.projectKey + const issueType = nodeData.inputs?.issueType + const issueSummary = nodeData.inputs?.issueSummary + const issueDescription = nodeData.inputs?.issueDescription + const issuePriority = nodeData.inputs?.issuePriority + const issueKey = nodeData.inputs?.issueKey + const assigneeAccountId = nodeData.inputs?.assigneeAccountId + const transitionId = nodeData.inputs?.transitionId + const jqlQuery = nodeData.inputs?.jqlQuery + const issueMaxResults = nodeData.inputs?.issueMaxResults + + // Comment parameters + const commentIssueKey = nodeData.inputs?.commentIssueKey + const commentText = nodeData.inputs?.commentText + const commentId = nodeData.inputs?.commentId + + // User parameters + const userQuery = nodeData.inputs?.userQuery + const userAccountId = nodeData.inputs?.userAccountId + const userEmail = nodeData.inputs?.userEmail + const userDisplayName = nodeData.inputs?.userDisplayName + const userMaxResults = nodeData.inputs?.userMaxResults + + // Set default parameters based on actions + actions.forEach((action) => { + const params: ICommonObject = {} + + // Issue action parameters + if (action === 'listIssues') { + if (projectKey) params.projectKey = projectKey + if (jqlQuery) params.jql = jqlQuery + if (issueMaxResults) params.maxResults = issueMaxResults + } + if (action === 'createIssue') { + if (projectKey) params.projectKey = projectKey + if (issueType) params.issueType = issueType + if (issueSummary) params.summary = issueSummary + if (issueDescription) params.description = issueDescription + if (issuePriority) params.priority = issuePriority + if (assigneeAccountId) params.assigneeAccountId = assigneeAccountId + } + if (['getIssue', 'updateIssue', 'deleteIssue', 'assignIssue', 'transitionIssue'].includes(action)) { + if (issueKey) params.issueKey = issueKey + } + if (action === 'updateIssue') { + if (issueSummary) params.summary = issueSummary + if (issueDescription) params.description = issueDescription + if (issuePriority) params.priority = issuePriority + if (assigneeAccountId) params.assigneeAccountId = assigneeAccountId + } + if (action === 'assignIssue') { + if (assigneeAccountId) params.assigneeAccountId = assigneeAccountId + } + if (action === 'transitionIssue') { + if (transitionId) params.transitionId = transitionId + } + + // Comment action parameters + if (['listComments', 'createComment'].includes(action) && commentIssueKey) { + params.issueKey = commentIssueKey + } + if (['createComment', 'updateComment'].includes(action) && commentText) { + params.text = commentText + } + if (['getComment', 'updateComment', 'deleteComment'].includes(action) && commentId) { + params.commentId = commentId + } + + // User action parameters + if (action === 'searchUsers') { + if (userQuery) params.query = userQuery + if (userMaxResults) params.maxResults = userMaxResults + } + if (['getUser', 'updateUser', 'deleteUser'].includes(action) && userAccountId) { + params.accountId = userAccountId + } + if (['createUser', 'updateUser'].includes(action)) { + if (userEmail) params.emailAddress = userEmail + if (userDisplayName) params.displayName = userDisplayName + } + + defaultParams[action] = params + }) + + // Create and return tools based on selected actions + const tools = createJiraTools({ + actions, + username, + accessToken, + jiraHost, + defaultParams + }) + + return tools + } +} + +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..807c5a31147 --- /dev/null +++ b/packages/components/nodes/tools/Jira/jira.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file From 848074589ead25ca3186493eb16f82aa22f62062 Mon Sep 17 00:00:00 2001 From: Henry Date: Thu, 5 Jun 2025 18:43:27 +0100 Subject: [PATCH 5/7] add google drive, google calendar, google sheets tools, powerpoint, excel, word doc loader --- .../GoogleCalendarOAuth2.credential.ts | 58 ++ .../GoogleDriveOAuth2.credential.ts | 62 ++ .../GoogleSheetsOAuth2.credential.ts | 62 ++ .../nodes/documentloaders/File/File.ts | 10 +- .../nodes/documentloaders/Folder/Folder.ts | 10 +- .../GoogleDrive/GoogleDrive.ts | 828 +++++++++++++++ .../GoogleDrive/google-drive.svg | 1 + .../GoogleSheets/GoogleSheets.ts | 429 ++++++++ .../GoogleSheets/google-sheets.svg | 1 + .../MicrosoftExcel/ExcelLoader.ts | 72 ++ .../MicrosoftExcel/MicrosoftExcel.ts | 142 +++ .../documentloaders/MicrosoftExcel/excel.svg | 1 + .../MicrosoftPowerpoint.ts | 142 +++ .../MicrosoftPowerpoint/PowerpointLoader.ts | 101 ++ .../MicrosoftPowerpoint/powerpoint.svg | 1 + .../MicrosoftWord/MicrosoftWord.ts | 142 +++ .../MicrosoftWord/WordLoader.ts | 108 ++ .../documentloaders/MicrosoftWord/word.svg | 1 + .../S3Directory/S3Directory.ts | 10 +- .../nodes/documentloaders/S3File/S3File.ts | 537 +++++++++- .../components/nodes/tools/Gmail/Gmail.ts | 10 +- .../tools/GoogleCalendar/GoogleCalendar.ts | 655 ++++++++++++ .../nodes/tools/GoogleCalendar/core.ts | 864 +++++++++++++++ .../tools/GoogleCalendar/google-calendar.svg | 1 + .../nodes/tools/GoogleDrive/GoogleDrive.ts | 657 ++++++++++++ .../nodes/tools/GoogleDrive/core.ts | 982 ++++++++++++++++++ .../nodes/tools/GoogleDrive/google-drive.svg | 1 + .../nodes/tools/GoogleSheets/GoogleSheets.ts | 424 ++++++++ .../nodes/tools/GoogleSheets/core.ts | 631 +++++++++++ .../tools/GoogleSheets/google-sheets.svg | 1 + packages/components/nodes/tools/Jira/Jira.ts | 8 +- .../MicrosoftOutlook/MicrosoftOutlook.ts | 10 +- .../tools/MicrosoftTeams/MicrosoftTeams.ts | 14 +- packages/components/package.json | 2 + pnpm-lock.yaml | 139 ++- 35 files changed, 7036 insertions(+), 81 deletions(-) create mode 100644 packages/components/credentials/GoogleCalendarOAuth2.credential.ts create mode 100644 packages/components/credentials/GoogleDriveOAuth2.credential.ts create mode 100644 packages/components/credentials/GoogleSheetsOAuth2.credential.ts create mode 100644 packages/components/nodes/documentloaders/GoogleDrive/GoogleDrive.ts create mode 100644 packages/components/nodes/documentloaders/GoogleDrive/google-drive.svg create mode 100644 packages/components/nodes/documentloaders/GoogleSheets/GoogleSheets.ts create mode 100644 packages/components/nodes/documentloaders/GoogleSheets/google-sheets.svg create mode 100644 packages/components/nodes/documentloaders/MicrosoftExcel/ExcelLoader.ts create mode 100644 packages/components/nodes/documentloaders/MicrosoftExcel/MicrosoftExcel.ts create mode 100644 packages/components/nodes/documentloaders/MicrosoftExcel/excel.svg create mode 100644 packages/components/nodes/documentloaders/MicrosoftPowerpoint/MicrosoftPowerpoint.ts create mode 100644 packages/components/nodes/documentloaders/MicrosoftPowerpoint/PowerpointLoader.ts create mode 100644 packages/components/nodes/documentloaders/MicrosoftPowerpoint/powerpoint.svg create mode 100644 packages/components/nodes/documentloaders/MicrosoftWord/MicrosoftWord.ts create mode 100644 packages/components/nodes/documentloaders/MicrosoftWord/WordLoader.ts create mode 100644 packages/components/nodes/documentloaders/MicrosoftWord/word.svg create mode 100644 packages/components/nodes/tools/GoogleCalendar/GoogleCalendar.ts create mode 100644 packages/components/nodes/tools/GoogleCalendar/core.ts create mode 100644 packages/components/nodes/tools/GoogleCalendar/google-calendar.svg create mode 100644 packages/components/nodes/tools/GoogleDrive/GoogleDrive.ts create mode 100644 packages/components/nodes/tools/GoogleDrive/core.ts create mode 100644 packages/components/nodes/tools/GoogleDrive/google-drive.svg create mode 100644 packages/components/nodes/tools/GoogleSheets/GoogleSheets.ts create mode 100644 packages/components/nodes/tools/GoogleSheets/core.ts create mode 100644 packages/components/nodes/tools/GoogleSheets/google-sheets.svg 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/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/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/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/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/tools/Gmail/Gmail.ts b/packages/components/nodes/tools/Gmail/Gmail.ts index 37c8d2274bb..6bdc05312e0 100644 --- a/packages/components/nodes/tools/Gmail/Gmail.ts +++ b/packages/components/nodes/tools/Gmail/Gmail.ts @@ -1,4 +1,4 @@ -import { getCredentialData, getCredentialParam, refreshOAuth2Token } from '../../../src/utils' +import { convertMultiOptionsToStringArray, getCredentialData, getCredentialParam, refreshOAuth2Token } from '../../../src/utils' import { createGmailTools } from './core' import type { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' @@ -562,13 +562,13 @@ class Gmail_Tools implements INode { let actions: string[] = [] if (gmailType === 'drafts') { - actions = nodeData.inputs?.draftActions ? JSON.parse(nodeData.inputs?.draftActions) : [] + actions = convertMultiOptionsToStringArray(nodeData.inputs?.draftActions) } else if (gmailType === 'messages') { - actions = nodeData.inputs?.messageActions ? JSON.parse(nodeData.inputs?.messageActions) : [] + actions = convertMultiOptionsToStringArray(nodeData.inputs?.messageActions) } else if (gmailType === 'labels') { - actions = nodeData.inputs?.labelActions ? JSON.parse(nodeData.inputs?.labelActions) : [] + actions = convertMultiOptionsToStringArray(nodeData.inputs?.labelActions) } else if (gmailType === 'threads') { - actions = nodeData.inputs?.threadActions ? JSON.parse(nodeData.inputs?.threadActions) : [] + actions = convertMultiOptionsToStringArray(nodeData.inputs?.threadActions) } // Prepare default parameters for each action diff --git a/packages/components/nodes/tools/GoogleCalendar/GoogleCalendar.ts b/packages/components/nodes/tools/GoogleCalendar/GoogleCalendar.ts new file mode 100644 index 00000000000..36ad7d81033 --- /dev/null +++ b/packages/components/nodes/tools/GoogleCalendar/GoogleCalendar.ts @@ -0,0 +1,655 @@ +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) + } + + // Create default params object based on inputs + const defaultParams: any = {} + + // Event-specific default params + if (calendarType === 'event') { + actions.forEach((action) => { + const params: any = {} + + if (nodeData.inputs?.calendarId) params.calendarId = nodeData.inputs.calendarId + if (action === 'getEvent' || action === 'updateEvent' || action === 'deleteEvent') { + if (nodeData.inputs?.eventId) params.eventId = nodeData.inputs.eventId + } + if (action === 'createEvent' || action === 'updateEvent') { + if (nodeData.inputs?.summary) params.summary = nodeData.inputs.summary + if (nodeData.inputs?.description) params.description = nodeData.inputs.description + if (nodeData.inputs?.location) params.location = nodeData.inputs.location + if (nodeData.inputs?.startDateTime) params.startDateTime = nodeData.inputs.startDateTime + if (nodeData.inputs?.endDateTime) params.endDateTime = nodeData.inputs.endDateTime + if (nodeData.inputs?.timeZone) params.timeZone = nodeData.inputs.timeZone + if (nodeData.inputs?.allDay !== undefined) params.allDay = nodeData.inputs.allDay + if (nodeData.inputs?.startDate) params.startDate = nodeData.inputs.startDate + if (nodeData.inputs?.endDate) params.endDate = nodeData.inputs.endDate + if (nodeData.inputs?.attendees) params.attendees = nodeData.inputs.attendees + if (nodeData.inputs?.recurrence) params.recurrence = nodeData.inputs.recurrence + if (nodeData.inputs?.reminderMinutes) params.reminderMinutes = nodeData.inputs.reminderMinutes + if (nodeData.inputs?.visibility) params.visibility = nodeData.inputs.visibility + } + if (action === 'quickAddEvent') { + if (nodeData.inputs?.quickAddText) params.quickAddText = nodeData.inputs.quickAddText + } + if (action === 'listEvents') { + if (nodeData.inputs?.timeMin) params.timeMin = nodeData.inputs.timeMin + if (nodeData.inputs?.timeMax) params.timeMax = nodeData.inputs.timeMax + if (nodeData.inputs?.maxResults) params.maxResults = nodeData.inputs.maxResults + if (nodeData.inputs?.singleEvents !== undefined) params.singleEvents = nodeData.inputs.singleEvents + if (nodeData.inputs?.orderBy) params.orderBy = nodeData.inputs.orderBy + if (nodeData.inputs?.query) params.query = nodeData.inputs.query + } + + defaultParams[action] = params + }) + } + + // Calendar-specific default params + if (calendarType === 'calendar') { + actions.forEach((action) => { + const params: any = {} + + if (['getCalendar', 'updateCalendar', 'deleteCalendar', 'clearCalendar'].includes(action)) { + if (nodeData.inputs?.calendarIdForCalendar) params.calendarId = nodeData.inputs.calendarIdForCalendar + } + if (action === 'createCalendar' || action === 'updateCalendar') { + if (nodeData.inputs?.calendarSummary) params.summary = nodeData.inputs.calendarSummary + if (nodeData.inputs?.calendarDescription) params.description = nodeData.inputs.calendarDescription + if (nodeData.inputs?.calendarLocation) params.location = nodeData.inputs.calendarLocation + if (nodeData.inputs?.calendarTimeZone) params.timeZone = nodeData.inputs.calendarTimeZone + } + if (action === 'listCalendars') { + if (nodeData.inputs?.showHidden !== undefined) params.showHidden = nodeData.inputs.showHidden + if (nodeData.inputs?.minAccessRole) params.minAccessRole = nodeData.inputs.minAccessRole + } + + defaultParams[action] = params + }) + } + + // Freebusy-specific default params + if (calendarType === 'freebusy') { + actions.forEach((action) => { + const params: any = {} + + if (action === 'queryFreebusy') { + if (nodeData.inputs?.freebusyTimeMin) params.timeMin = nodeData.inputs.freebusyTimeMin + if (nodeData.inputs?.freebusyTimeMax) params.timeMax = nodeData.inputs.freebusyTimeMax + if (nodeData.inputs?.calendarIds) params.calendarIds = nodeData.inputs.calendarIds + if (nodeData.inputs?.groupExpansionMax) params.groupExpansionMax = nodeData.inputs.groupExpansionMax + if (nodeData.inputs?.calendarExpansionMax) params.calendarExpansionMax = nodeData.inputs.calendarExpansionMax + } + + defaultParams[action] = params + }) + } + + const tools = createGoogleCalendarTools({ + accessToken, + actions, + defaultParams + }) + + return tools + } +} + +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/GoogleDrive/GoogleDrive.ts b/packages/components/nodes/tools/GoogleDrive/GoogleDrive.ts new file mode 100644 index 00000000000..48939dd63e5 --- /dev/null +++ b/packages/components/nodes/tools/GoogleDrive/GoogleDrive.ts @@ -0,0 +1,657 @@ +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 + } + + // Collect default parameters from inputs + const defaultParams: any = {} + + // 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 + + const tools = createGoogleDriveTools({ + accessToken, + actions, + defaultParams + }) + + return tools + } +} + +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..f14ba3c95a6 --- /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${TOOL_ARGS_PREFIX}${JSON.stringify(params)}` + } 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${TOOL_ARGS_PREFIX}${JSON.stringify(params)}` + } 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${TOOL_ARGS_PREFIX}${JSON.stringify(params)}` + } 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..785e1098a3e --- /dev/null +++ b/packages/components/nodes/tools/GoogleSheets/GoogleSheets.ts @@ -0,0 +1,424 @@ +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) + } + + // Create default params object based on inputs + const defaultParams: any = {} + + // Spreadsheet-specific default params + if (sheetsType === 'spreadsheet') { + actions.forEach((action) => { + const params: any = {} + + // Common spreadsheet parameters + if (nodeData.inputs?.spreadsheetId) params.spreadsheetId = nodeData.inputs.spreadsheetId + + if (action === 'createSpreadsheet') { + if (nodeData.inputs?.title) params.title = nodeData.inputs.title + if (nodeData.inputs?.sheetCount) params.sheetCount = nodeData.inputs.sheetCount + } + + if (action === 'getSpreadsheet') { + if (nodeData.inputs?.ranges) params.ranges = nodeData.inputs.ranges + if (nodeData.inputs?.includeGridData !== undefined) params.includeGridData = nodeData.inputs.includeGridData + } + + if (action === 'updateSpreadsheet') { + if (nodeData.inputs?.title) params.title = nodeData.inputs.title + } + + defaultParams[action] = params + }) + } + + // Values-specific default params + if (sheetsType === 'values') { + actions.forEach((action) => { + const params: any = {} + + // Common values parameters + if (nodeData.inputs?.spreadsheetId) params.spreadsheetId = nodeData.inputs.spreadsheetId + + if (action === 'getValues') { + if (nodeData.inputs?.range) params.range = nodeData.inputs.range + if (nodeData.inputs?.valueRenderOption) params.valueRenderOption = nodeData.inputs.valueRenderOption + if (nodeData.inputs?.dateTimeRenderOption) params.dateTimeRenderOption = nodeData.inputs.dateTimeRenderOption + if (nodeData.inputs?.majorDimension) params.majorDimension = nodeData.inputs.majorDimension + } + + if (action === 'updateValues') { + if (nodeData.inputs?.range) params.range = nodeData.inputs.range + if (nodeData.inputs?.values) params.values = nodeData.inputs.values + if (nodeData.inputs?.valueInputOption) params.valueInputOption = nodeData.inputs.valueInputOption + if (nodeData.inputs?.majorDimension) params.majorDimension = nodeData.inputs.majorDimension + } + + if (action === 'appendValues') { + if (nodeData.inputs?.range) params.range = nodeData.inputs.range + if (nodeData.inputs?.values) params.values = nodeData.inputs.values + if (nodeData.inputs?.valueInputOption) params.valueInputOption = nodeData.inputs.valueInputOption + if (nodeData.inputs?.insertDataOption) params.insertDataOption = nodeData.inputs.insertDataOption + if (nodeData.inputs?.majorDimension) params.majorDimension = nodeData.inputs.majorDimension + } + + if (action === 'clearValues') { + if (nodeData.inputs?.range) params.range = nodeData.inputs.range + } + + if (action === 'batchGetValues') { + if (nodeData.inputs?.ranges) params.ranges = nodeData.inputs.ranges + if (nodeData.inputs?.valueRenderOption) params.valueRenderOption = nodeData.inputs.valueRenderOption + if (nodeData.inputs?.dateTimeRenderOption) params.dateTimeRenderOption = nodeData.inputs.dateTimeRenderOption + if (nodeData.inputs?.majorDimension) params.majorDimension = nodeData.inputs.majorDimension + } + + if (action === 'batchUpdateValues') { + if (nodeData.inputs?.values) params.values = nodeData.inputs.values + if (nodeData.inputs?.valueInputOption) params.valueInputOption = nodeData.inputs.valueInputOption + } + + if (action === 'batchClearValues') { + if (nodeData.inputs?.ranges) params.ranges = nodeData.inputs.ranges + } + + defaultParams[action] = params + }) + } + + const tools = createGoogleSheetsTools({ + accessToken, + actions, + defaultParams + }) + + return tools + } +} + +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 index deedca3f8ee..9087b5cfff1 100644 --- a/packages/components/nodes/tools/Jira/Jira.ts +++ b/packages/components/nodes/tools/Jira/Jira.ts @@ -1,4 +1,4 @@ -import { getCredentialData, getCredentialParam } from '../../../src/utils' +import { convertMultiOptionsToStringArray, getCredentialData, getCredentialParam } from '../../../src/utils' import { createJiraTools } from './core' import type { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' @@ -393,11 +393,11 @@ class Jira_Tools implements INode { let actions: string[] = [] if (jiraType === 'issues') { - actions = nodeData.inputs?.issueActions ? JSON.parse(nodeData.inputs?.issueActions) : [] + actions = convertMultiOptionsToStringArray(nodeData.inputs?.issueActions) } else if (jiraType === 'comments') { - actions = nodeData.inputs?.commentActions ? JSON.parse(nodeData.inputs?.commentActions) : [] + actions = convertMultiOptionsToStringArray(nodeData.inputs?.commentActions) } else if (jiraType === 'users') { - actions = nodeData.inputs?.userActions ? JSON.parse(nodeData.inputs?.userActions) : [] + actions = convertMultiOptionsToStringArray(nodeData.inputs?.userActions) } // Prepare default parameters for each action diff --git a/packages/components/nodes/tools/MicrosoftOutlook/MicrosoftOutlook.ts b/packages/components/nodes/tools/MicrosoftOutlook/MicrosoftOutlook.ts index 1782d55725f..94c41356409 100644 --- a/packages/components/nodes/tools/MicrosoftOutlook/MicrosoftOutlook.ts +++ b/packages/components/nodes/tools/MicrosoftOutlook/MicrosoftOutlook.ts @@ -1,4 +1,4 @@ -import { getCredentialData, getCredentialParam, refreshOAuth2Token } from '../../../src/utils' +import { convertMultiOptionsToStringArray, getCredentialData, getCredentialParam, refreshOAuth2Token } from '../../../src/utils' import { createOutlookTools } from './core' import type { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' @@ -729,8 +729,8 @@ class MicrosoftOutlook_Tools implements INode { 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[] + 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) @@ -742,9 +742,9 @@ class MicrosoftOutlook_Tools implements INode { let actions: string[] = [] if (outlookType === 'calendar') { - actions = typeof calendarActions === 'string' ? JSON.parse(calendarActions) : calendarActions + actions = convertMultiOptionsToStringArray(calendarActions) } else if (outlookType === 'message') { - actions = typeof messageActions === 'string' ? JSON.parse(messageActions) : messageActions + actions = convertMultiOptionsToStringArray(messageActions) } // Prepare default parameters for each action based on type diff --git a/packages/components/nodes/tools/MicrosoftTeams/MicrosoftTeams.ts b/packages/components/nodes/tools/MicrosoftTeams/MicrosoftTeams.ts index 2dd43d72c60..0303c324f38 100644 --- a/packages/components/nodes/tools/MicrosoftTeams/MicrosoftTeams.ts +++ b/packages/components/nodes/tools/MicrosoftTeams/MicrosoftTeams.ts @@ -1,5 +1,5 @@ import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' -import { getCredentialData, getCredentialParam, refreshOAuth2Token } from '../../../src/utils' +import { convertMultiOptionsToStringArray, getCredentialData, getCredentialParam, refreshOAuth2Token } from '../../../src/utils' import { createTeamsTools } from './core' class MicrosoftTeams_Tools implements INode { @@ -907,17 +907,17 @@ class MicrosoftTeams_Tools implements INode { 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[] + 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 = typeof channelActions === 'string' ? JSON.parse(channelActions) : channelActions + actions = convertMultiOptionsToStringArray(channelActions) } else if (teamsType === 'chat') { - actions = typeof chatActions === 'string' ? JSON.parse(chatActions) : chatActions + actions = convertMultiOptionsToStringArray(chatActions) } else if (teamsType === 'chatMessage') { - actions = typeof chatMessageActions === 'string' ? JSON.parse(chatMessageActions) : chatMessageActions + actions = convertMultiOptionsToStringArray(chatMessageActions) } let credentialData = await getCredentialData(nodeData.credential ?? '', options) diff --git a/packages/components/package.json b/packages/components/package.json index ba6c5747b3e..646b4e387aa 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -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/pnpm-lock.yaml b/pnpm-lock.yaml index 1ab10f735c5..93916d16808 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 @@ -7577,6 +7583,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 +8469,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 +8735,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 +8888,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 +9033,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 +10677,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 +13847,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== } @@ -16350,6 +16385,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 +18027,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 +18226,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 +18323,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 +21709,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 +22458,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 +23123,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 +23194,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 +23466,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 +27682,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 +28848,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 +29147,8 @@ snapshots: transitivePeerDependencies: - '@lezer/common' + codepage@1.15.0: {} + codsen-utils@1.6.4: dependencies: rfdc: 1.3.1 @@ -29221,6 +29287,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 +29444,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 +31659,8 @@ snapshots: forwarded@0.2.0: {} + frac@1.1.2: {} + fraction.js@4.3.7: {} fragment-cache@0.2.1: @@ -35929,6 +36006,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 +36104,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: {} @@ -39006,6 +39106,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 +40997,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 +41305,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 +41412,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 From 0d0b294e5f4db6c624c27fae38bc6b2ef2ad3da2 Mon Sep 17 00:00:00 2001 From: Henry Date: Thu, 5 Jun 2025 20:10:36 +0100 Subject: [PATCH 6/7] update jira logo --- packages/components/nodes/documentloaders/Jira/jira.svg | 3 +-- packages/components/nodes/tools/Jira/jira.svg | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) 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/tools/Jira/jira.svg b/packages/components/nodes/tools/Jira/jira.svg index 807c5a31147..4ace5cc84a3 100644 --- a/packages/components/nodes/tools/Jira/jira.svg +++ b/packages/components/nodes/tools/Jira/jira.svg @@ -1,2 +1 @@ - - \ No newline at end of file + \ No newline at end of file From c1ea6462c8385a48d9ed0089eecb168d9ad262b9 Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 6 Jun 2025 18:31:56 +0100 Subject: [PATCH 7/7] Refactor Gmail and Outlook tools to remove maxOutputLength parameter and enhance request handling. Update response formatting to include parameters in the output. Adjust Google Drive tools to simplify success messages by removing unnecessary parameter details. --- .../components/nodes/tools/Gmail/Gmail.ts | 3 +- packages/components/nodes/tools/Gmail/core.ts | 129 +++---- .../nodes/tools/GoogleDrive/core.ts | 6 +- .../nodes/tools/MicrosoftOutlook/core.ts | 109 +++--- .../nodes/tools/MicrosoftTeams/core.ts | 330 ++++++++---------- 5 files changed, 250 insertions(+), 327 deletions(-) diff --git a/packages/components/nodes/tools/Gmail/Gmail.ts b/packages/components/nodes/tools/Gmail/Gmail.ts index 6bdc05312e0..4c3054064b6 100644 --- a/packages/components/nodes/tools/Gmail/Gmail.ts +++ b/packages/components/nodes/tools/Gmail/Gmail.ts @@ -688,8 +688,7 @@ class Gmail_Tools implements INode { const tools = createGmailTools({ actions, accessToken, - defaultParams, - maxOutputLength: Infinity + defaultParams }) return tools diff --git a/packages/components/nodes/tools/Gmail/core.ts b/packages/components/nodes/tools/Gmail/core.ts index e1883e56bfa..14d242c84e1 100644 --- a/packages/components/nodes/tools/Gmail/core.ts +++ b/packages/components/nodes/tools/Gmail/core.ts @@ -1,6 +1,7 @@ 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` @@ -17,7 +18,6 @@ export interface RequestParameters { body?: Body url?: string description?: string - maxOutputLength?: number name?: string actions?: string[] accessToken?: string @@ -63,15 +63,13 @@ const CreateLabelSchema = z.object({ class BaseGmailTool extends DynamicStructuredTool { protected accessToken: string = '' - protected maxOutputLength: number = Infinity constructor(args: any) { super(args) this.accessToken = args.accessToken ?? '' - this.maxOutputLength = args.maxOutputLength ?? Infinity } - async makeGmailRequest(url: string, method: string = 'GET', body?: any): Promise { + async makeGmailRequest(url: string, method: string = 'GET', body?: any, params?: any): Promise { const headers = { Authorization: `Bearer ${this.accessToken}`, 'Content-Type': 'application/json', @@ -90,7 +88,7 @@ class BaseGmailTool extends DynamicStructuredTool { } const data = await response.text() - return data.slice(0, this.maxOutputLength) + return data + TOOL_ARGS_PREFIX + JSON.stringify(params) } createMimeMessage(to: string, subject?: string, body?: string, cc?: string, bcc?: string): string { @@ -125,7 +123,7 @@ class ListDraftsTool extends BaseGmailTool { method: 'GET', headers: {} } - super({ ...toolInput, accessToken: args.accessToken, maxOutputLength: args.maxOutputLength }) + super({ ...toolInput, accessToken: args.accessToken }) this.defaultParams = args.defaultParams || {} } @@ -139,7 +137,7 @@ class ListDraftsTool extends BaseGmailTool { const url = `https://gmail.googleapis.com/gmail/v1/users/me/drafts?${queryParams.toString()}` try { - const response = await this.makeGmailRequest(url) + const response = await this.makeGmailRequest(url, 'GET', undefined, params) return response } catch (error) { return `Error listing drafts: ${error}` @@ -159,7 +157,7 @@ class CreateDraftTool extends BaseGmailTool { method: 'POST', headers: {} } - super({ ...toolInput, accessToken: args.accessToken, maxOutputLength: args.maxOutputLength }) + super({ ...toolInput, accessToken: args.accessToken }) this.defaultParams = args.defaultParams || {} } @@ -175,7 +173,7 @@ class CreateDraftTool extends BaseGmailTool { } const url = 'https://gmail.googleapis.com/gmail/v1/users/me/drafts' - const response = await this.makeGmailRequest(url, 'POST', draftData) + const response = await this.makeGmailRequest(url, 'POST', draftData, params) return response } catch (error) { return `Error creating draft: ${error}` @@ -195,7 +193,7 @@ class GetDraftTool extends BaseGmailTool { method: 'GET', headers: {} } - super({ ...toolInput, accessToken: args.accessToken, maxOutputLength: args.maxOutputLength }) + super({ ...toolInput, accessToken: args.accessToken }) this.defaultParams = args.defaultParams || {} } @@ -209,7 +207,7 @@ class GetDraftTool extends BaseGmailTool { try { const url = `https://gmail.googleapis.com/gmail/v1/users/me/drafts/${draftId}` - const response = await this.makeGmailRequest(url) + const response = await this.makeGmailRequest(url, 'GET', undefined, params) return response } catch (error) { return `Error getting draft: ${error}` @@ -229,7 +227,7 @@ class UpdateDraftTool extends BaseGmailTool { method: 'PUT', headers: {} } - super({ ...toolInput, accessToken: args.accessToken, maxOutputLength: args.maxOutputLength }) + super({ ...toolInput, accessToken: args.accessToken }) this.defaultParams = args.defaultParams || {} } @@ -250,7 +248,7 @@ class UpdateDraftTool extends BaseGmailTool { } const url = `https://gmail.googleapis.com/gmail/v1/users/me/drafts/${draftId}` - const response = await this.makeGmailRequest(url, 'PUT', draftData) + const response = await this.makeGmailRequest(url, 'PUT', draftData, params) return response } catch (error) { return `Error updating draft: ${error}` @@ -270,7 +268,7 @@ class SendDraftTool extends BaseGmailTool { method: 'POST', headers: {} } - super({ ...toolInput, accessToken: args.accessToken, maxOutputLength: args.maxOutputLength }) + super({ ...toolInput, accessToken: args.accessToken }) this.defaultParams = args.defaultParams || {} } @@ -284,7 +282,7 @@ class SendDraftTool extends BaseGmailTool { try { const url = 'https://gmail.googleapis.com/gmail/v1/users/me/drafts/send' - const response = await this.makeGmailRequest(url, 'POST', { id: draftId }) + const response = await this.makeGmailRequest(url, 'POST', { id: draftId }, params) return response } catch (error) { return `Error sending draft: ${error}` @@ -304,7 +302,7 @@ class DeleteDraftTool extends BaseGmailTool { method: 'DELETE', headers: {} } - super({ ...toolInput, accessToken: args.accessToken, maxOutputLength: args.maxOutputLength }) + super({ ...toolInput, accessToken: args.accessToken }) this.defaultParams = args.defaultParams || {} } @@ -318,7 +316,7 @@ class DeleteDraftTool extends BaseGmailTool { try { const url = `https://gmail.googleapis.com/gmail/v1/users/me/drafts/${draftId}` - await this.makeGmailRequest(url, 'DELETE') + await this.makeGmailRequest(url, 'DELETE', undefined, params) return `Draft ${draftId} deleted successfully` } catch (error) { return `Error deleting draft: ${error}` @@ -339,7 +337,7 @@ class ListMessagesTool extends BaseGmailTool { method: 'GET', headers: {} } - super({ ...toolInput, accessToken: args.accessToken, maxOutputLength: args.maxOutputLength }) + super({ ...toolInput, accessToken: args.accessToken }) this.defaultParams = args.defaultParams || {} } @@ -353,7 +351,7 @@ class ListMessagesTool extends BaseGmailTool { const url = `https://gmail.googleapis.com/gmail/v1/users/me/messages?${queryParams.toString()}` try { - const response = await this.makeGmailRequest(url) + const response = await this.makeGmailRequest(url, 'GET', undefined, params) return response } catch (error) { return `Error listing messages: ${error}` @@ -373,7 +371,7 @@ class GetMessageTool extends BaseGmailTool { method: 'GET', headers: {} } - super({ ...toolInput, accessToken: args.accessToken, maxOutputLength: args.maxOutputLength }) + super({ ...toolInput, accessToken: args.accessToken }) this.defaultParams = args.defaultParams || {} } @@ -387,7 +385,7 @@ class GetMessageTool extends BaseGmailTool { try { const url = `https://gmail.googleapis.com/gmail/v1/users/me/messages/${messageId}` - const response = await this.makeGmailRequest(url) + const response = await this.makeGmailRequest(url, 'GET', undefined, params) return response } catch (error) { return `Error getting message: ${error}` @@ -407,7 +405,7 @@ class SendMessageTool extends BaseGmailTool { method: 'POST', headers: {} } - super({ ...toolInput, accessToken: args.accessToken, maxOutputLength: args.maxOutputLength }) + super({ ...toolInput, accessToken: args.accessToken }) this.defaultParams = args.defaultParams || {} } @@ -421,7 +419,7 @@ class SendMessageTool extends BaseGmailTool { } const url = 'https://gmail.googleapis.com/gmail/v1/users/me/messages/send' - const response = await this.makeGmailRequest(url, 'POST', messageData) + const response = await this.makeGmailRequest(url, 'POST', messageData, params) return response } catch (error) { return `Error sending message: ${error}` @@ -441,7 +439,7 @@ class ModifyMessageTool extends BaseGmailTool { method: 'POST', headers: {} } - super({ ...toolInput, accessToken: args.accessToken, maxOutputLength: args.maxOutputLength }) + super({ ...toolInput, accessToken: args.accessToken }) this.defaultParams = args.defaultParams || {} } @@ -463,7 +461,7 @@ class ModifyMessageTool extends BaseGmailTool { } const url = `https://gmail.googleapis.com/gmail/v1/users/me/messages/${messageId}/modify` - const response = await this.makeGmailRequest(url, 'POST', modifyData) + const response = await this.makeGmailRequest(url, 'POST', modifyData, params) return response } catch (error) { return `Error modifying message: ${error}` @@ -483,7 +481,7 @@ class TrashMessageTool extends BaseGmailTool { method: 'POST', headers: {} } - super({ ...toolInput, accessToken: args.accessToken, maxOutputLength: args.maxOutputLength }) + super({ ...toolInput, accessToken: args.accessToken }) this.defaultParams = args.defaultParams || {} } @@ -497,7 +495,7 @@ class TrashMessageTool extends BaseGmailTool { try { const url = `https://gmail.googleapis.com/gmail/v1/users/me/messages/${messageId}/trash` - const response = await this.makeGmailRequest(url, 'POST') + const response = await this.makeGmailRequest(url, 'POST', undefined, params) return response } catch (error) { return `Error moving message to trash: ${error}` @@ -517,7 +515,7 @@ class UntrashMessageTool extends BaseGmailTool { method: 'POST', headers: {} } - super({ ...toolInput, accessToken: args.accessToken, maxOutputLength: args.maxOutputLength }) + super({ ...toolInput, accessToken: args.accessToken }) this.defaultParams = args.defaultParams || {} } @@ -531,7 +529,7 @@ class UntrashMessageTool extends BaseGmailTool { try { const url = `https://gmail.googleapis.com/gmail/v1/users/me/messages/${messageId}/untrash` - const response = await this.makeGmailRequest(url, 'POST') + const response = await this.makeGmailRequest(url, 'POST', undefined, params) return response } catch (error) { return `Error removing message from trash: ${error}` @@ -551,7 +549,7 @@ class DeleteMessageTool extends BaseGmailTool { method: 'DELETE', headers: {} } - super({ ...toolInput, accessToken: args.accessToken, maxOutputLength: args.maxOutputLength }) + super({ ...toolInput, accessToken: args.accessToken }) this.defaultParams = args.defaultParams || {} } @@ -565,7 +563,7 @@ class DeleteMessageTool extends BaseGmailTool { try { const url = `https://gmail.googleapis.com/gmail/v1/users/me/messages/${messageId}` - await this.makeGmailRequest(url, 'DELETE') + await this.makeGmailRequest(url, 'DELETE', undefined, params) return `Message ${messageId} deleted successfully` } catch (error) { return `Error deleting message: ${error}` @@ -586,14 +584,14 @@ class ListLabelsTool extends BaseGmailTool { method: 'GET', headers: {} } - super({ ...toolInput, accessToken: args.accessToken, maxOutputLength: args.maxOutputLength }) + 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) + const response = await this.makeGmailRequest(url, 'GET', undefined, {}) return response } catch (error) { return `Error listing labels: ${error}` @@ -613,7 +611,7 @@ class GetLabelTool extends BaseGmailTool { method: 'GET', headers: {} } - super({ ...toolInput, accessToken: args.accessToken, maxOutputLength: args.maxOutputLength }) + super({ ...toolInput, accessToken: args.accessToken }) this.defaultParams = args.defaultParams || {} } @@ -627,7 +625,7 @@ class GetLabelTool extends BaseGmailTool { try { const url = `https://gmail.googleapis.com/gmail/v1/users/me/labels/${labelId}` - const response = await this.makeGmailRequest(url) + const response = await this.makeGmailRequest(url, 'GET', undefined, params) return response } catch (error) { return `Error getting label: ${error}` @@ -647,7 +645,7 @@ class CreateLabelTool extends BaseGmailTool { method: 'POST', headers: {} } - super({ ...toolInput, accessToken: args.accessToken, maxOutputLength: args.maxOutputLength }) + super({ ...toolInput, accessToken: args.accessToken }) this.defaultParams = args.defaultParams || {} } @@ -672,7 +670,7 @@ class CreateLabelTool extends BaseGmailTool { } const url = 'https://gmail.googleapis.com/gmail/v1/users/me/labels' - const response = await this.makeGmailRequest(url, 'POST', labelData) + const response = await this.makeGmailRequest(url, 'POST', labelData, params) return response } catch (error) { return `Error creating label: ${error}` @@ -692,7 +690,7 @@ class UpdateLabelTool extends BaseGmailTool { method: 'PUT', headers: {} } - super({ ...toolInput, accessToken: args.accessToken, maxOutputLength: args.maxOutputLength }) + super({ ...toolInput, accessToken: args.accessToken }) this.defaultParams = args.defaultParams || {} } @@ -716,7 +714,7 @@ class UpdateLabelTool extends BaseGmailTool { } const url = `https://gmail.googleapis.com/gmail/v1/users/me/labels/${labelId}` - const response = await this.makeGmailRequest(url, 'PUT', labelData) + const response = await this.makeGmailRequest(url, 'PUT', labelData, params) return response } catch (error) { return `Error updating label: ${error}` @@ -736,7 +734,7 @@ class DeleteLabelTool extends BaseGmailTool { method: 'DELETE', headers: {} } - super({ ...toolInput, accessToken: args.accessToken, maxOutputLength: args.maxOutputLength }) + super({ ...toolInput, accessToken: args.accessToken }) this.defaultParams = args.defaultParams || {} } @@ -750,7 +748,7 @@ class DeleteLabelTool extends BaseGmailTool { try { const url = `https://gmail.googleapis.com/gmail/v1/users/me/labels/${labelId}` - await this.makeGmailRequest(url, 'DELETE') + await this.makeGmailRequest(url, 'DELETE', undefined, params) return `Label ${labelId} deleted successfully` } catch (error) { return `Error deleting label: ${error}` @@ -771,7 +769,7 @@ class ListThreadsTool extends BaseGmailTool { method: 'GET', headers: {} } - super({ ...toolInput, accessToken: args.accessToken, maxOutputLength: args.maxOutputLength }) + super({ ...toolInput, accessToken: args.accessToken }) this.defaultParams = args.defaultParams || {} } @@ -785,7 +783,7 @@ class ListThreadsTool extends BaseGmailTool { const url = `https://gmail.googleapis.com/gmail/v1/users/me/threads?${queryParams.toString()}` try { - const response = await this.makeGmailRequest(url) + const response = await this.makeGmailRequest(url, 'GET', undefined, params) return response } catch (error) { return `Error listing threads: ${error}` @@ -805,7 +803,7 @@ class GetThreadTool extends BaseGmailTool { method: 'GET', headers: {} } - super({ ...toolInput, accessToken: args.accessToken, maxOutputLength: args.maxOutputLength }) + super({ ...toolInput, accessToken: args.accessToken }) this.defaultParams = args.defaultParams || {} } @@ -819,7 +817,7 @@ class GetThreadTool extends BaseGmailTool { try { const url = `https://gmail.googleapis.com/gmail/v1/users/me/threads/${threadId}` - const response = await this.makeGmailRequest(url) + const response = await this.makeGmailRequest(url, 'GET', undefined, params) return response } catch (error) { return `Error getting thread: ${error}` @@ -839,7 +837,7 @@ class ModifyThreadTool extends BaseGmailTool { method: 'POST', headers: {} } - super({ ...toolInput, accessToken: args.accessToken, maxOutputLength: args.maxOutputLength }) + super({ ...toolInput, accessToken: args.accessToken }) this.defaultParams = args.defaultParams || {} } @@ -861,7 +859,7 @@ class ModifyThreadTool extends BaseGmailTool { } const url = `https://gmail.googleapis.com/gmail/v1/users/me/threads/${threadId}/modify` - const response = await this.makeGmailRequest(url, 'POST', modifyData) + const response = await this.makeGmailRequest(url, 'POST', modifyData, params) return response } catch (error) { return `Error modifying thread: ${error}` @@ -881,7 +879,7 @@ class TrashThreadTool extends BaseGmailTool { method: 'POST', headers: {} } - super({ ...toolInput, accessToken: args.accessToken, maxOutputLength: args.maxOutputLength }) + super({ ...toolInput, accessToken: args.accessToken }) this.defaultParams = args.defaultParams || {} } @@ -895,7 +893,7 @@ class TrashThreadTool extends BaseGmailTool { try { const url = `https://gmail.googleapis.com/gmail/v1/users/me/threads/${threadId}/trash` - const response = await this.makeGmailRequest(url, 'POST') + const response = await this.makeGmailRequest(url, 'POST', undefined, params) return response } catch (error) { return `Error moving thread to trash: ${error}` @@ -915,7 +913,7 @@ class UntrashThreadTool extends BaseGmailTool { method: 'POST', headers: {} } - super({ ...toolInput, accessToken: args.accessToken, maxOutputLength: args.maxOutputLength }) + super({ ...toolInput, accessToken: args.accessToken }) this.defaultParams = args.defaultParams || {} } @@ -929,7 +927,7 @@ class UntrashThreadTool extends BaseGmailTool { try { const url = `https://gmail.googleapis.com/gmail/v1/users/me/threads/${threadId}/untrash` - const response = await this.makeGmailRequest(url, 'POST') + const response = await this.makeGmailRequest(url, 'POST', undefined, params) return response } catch (error) { return `Error removing thread from trash: ${error}` @@ -949,7 +947,7 @@ class DeleteThreadTool extends BaseGmailTool { method: 'DELETE', headers: {} } - super({ ...toolInput, accessToken: args.accessToken, maxOutputLength: args.maxOutputLength }) + super({ ...toolInput, accessToken: args.accessToken }) this.defaultParams = args.defaultParams || {} } @@ -963,7 +961,7 @@ class DeleteThreadTool extends BaseGmailTool { try { const url = `https://gmail.googleapis.com/gmail/v1/users/me/threads/${threadId}` - await this.makeGmailRequest(url, 'DELETE') + await this.makeGmailRequest(url, 'DELETE', undefined, params) return `Thread ${threadId} deleted successfully` } catch (error) { return `Error deleting thread: ${error}` @@ -975,7 +973,6 @@ export const createGmailTools = (args?: RequestParameters): DynamicStructuredToo const tools: DynamicStructuredTool[] = [] const actions = args?.actions || [] const accessToken = args?.accessToken || '' - const maxOutputLength = args?.maxOutputLength || Infinity const defaultParams = args?.defaultParams || {} // Draft tools @@ -983,7 +980,6 @@ export const createGmailTools = (args?: RequestParameters): DynamicStructuredToo tools.push( new ListDraftsTool({ accessToken, - maxOutputLength, defaultParams: defaultParams.listDrafts }) ) @@ -993,7 +989,6 @@ export const createGmailTools = (args?: RequestParameters): DynamicStructuredToo tools.push( new CreateDraftTool({ accessToken, - maxOutputLength, defaultParams: defaultParams.createDraft }) ) @@ -1003,7 +998,6 @@ export const createGmailTools = (args?: RequestParameters): DynamicStructuredToo tools.push( new GetDraftTool({ accessToken, - maxOutputLength, defaultParams: defaultParams.getDraft }) ) @@ -1013,7 +1007,6 @@ export const createGmailTools = (args?: RequestParameters): DynamicStructuredToo tools.push( new UpdateDraftTool({ accessToken, - maxOutputLength, defaultParams: defaultParams.updateDraft }) ) @@ -1023,7 +1016,6 @@ export const createGmailTools = (args?: RequestParameters): DynamicStructuredToo tools.push( new SendDraftTool({ accessToken, - maxOutputLength, defaultParams: defaultParams.sendDraft }) ) @@ -1033,7 +1025,6 @@ export const createGmailTools = (args?: RequestParameters): DynamicStructuredToo tools.push( new DeleteDraftTool({ accessToken, - maxOutputLength, defaultParams: defaultParams.deleteDraft }) ) @@ -1044,7 +1035,6 @@ export const createGmailTools = (args?: RequestParameters): DynamicStructuredToo tools.push( new ListMessagesTool({ accessToken, - maxOutputLength, defaultParams: defaultParams.listMessages }) ) @@ -1054,7 +1044,6 @@ export const createGmailTools = (args?: RequestParameters): DynamicStructuredToo tools.push( new GetMessageTool({ accessToken, - maxOutputLength, defaultParams: defaultParams.getMessage }) ) @@ -1064,7 +1053,6 @@ export const createGmailTools = (args?: RequestParameters): DynamicStructuredToo tools.push( new SendMessageTool({ accessToken, - maxOutputLength, defaultParams: defaultParams.sendMessage }) ) @@ -1074,7 +1062,6 @@ export const createGmailTools = (args?: RequestParameters): DynamicStructuredToo tools.push( new ModifyMessageTool({ accessToken, - maxOutputLength, defaultParams: defaultParams.modifyMessage }) ) @@ -1084,7 +1071,6 @@ export const createGmailTools = (args?: RequestParameters): DynamicStructuredToo tools.push( new TrashMessageTool({ accessToken, - maxOutputLength, defaultParams: defaultParams.trashMessage }) ) @@ -1094,7 +1080,6 @@ export const createGmailTools = (args?: RequestParameters): DynamicStructuredToo tools.push( new UntrashMessageTool({ accessToken, - maxOutputLength, defaultParams: defaultParams.untrashMessage }) ) @@ -1104,7 +1089,6 @@ export const createGmailTools = (args?: RequestParameters): DynamicStructuredToo tools.push( new DeleteMessageTool({ accessToken, - maxOutputLength, defaultParams: defaultParams.deleteMessage }) ) @@ -1115,7 +1099,6 @@ export const createGmailTools = (args?: RequestParameters): DynamicStructuredToo tools.push( new ListLabelsTool({ accessToken, - maxOutputLength, defaultParams: defaultParams.listLabels }) ) @@ -1125,7 +1108,6 @@ export const createGmailTools = (args?: RequestParameters): DynamicStructuredToo tools.push( new GetLabelTool({ accessToken, - maxOutputLength, defaultParams: defaultParams.getLabel }) ) @@ -1135,7 +1117,6 @@ export const createGmailTools = (args?: RequestParameters): DynamicStructuredToo tools.push( new CreateLabelTool({ accessToken, - maxOutputLength, defaultParams: defaultParams.createLabel }) ) @@ -1145,7 +1126,6 @@ export const createGmailTools = (args?: RequestParameters): DynamicStructuredToo tools.push( new UpdateLabelTool({ accessToken, - maxOutputLength, defaultParams: defaultParams.updateLabel }) ) @@ -1155,7 +1135,6 @@ export const createGmailTools = (args?: RequestParameters): DynamicStructuredToo tools.push( new DeleteLabelTool({ accessToken, - maxOutputLength, defaultParams: defaultParams.deleteLabel }) ) @@ -1166,7 +1145,6 @@ export const createGmailTools = (args?: RequestParameters): DynamicStructuredToo tools.push( new ListThreadsTool({ accessToken, - maxOutputLength, defaultParams: defaultParams.listThreads }) ) @@ -1176,7 +1154,6 @@ export const createGmailTools = (args?: RequestParameters): DynamicStructuredToo tools.push( new GetThreadTool({ accessToken, - maxOutputLength, defaultParams: defaultParams.getThread }) ) @@ -1186,7 +1163,6 @@ export const createGmailTools = (args?: RequestParameters): DynamicStructuredToo tools.push( new ModifyThreadTool({ accessToken, - maxOutputLength, defaultParams: defaultParams.modifyThread }) ) @@ -1196,7 +1172,6 @@ export const createGmailTools = (args?: RequestParameters): DynamicStructuredToo tools.push( new TrashThreadTool({ accessToken, - maxOutputLength, defaultParams: defaultParams.trashThread }) ) @@ -1206,7 +1181,6 @@ export const createGmailTools = (args?: RequestParameters): DynamicStructuredToo tools.push( new UntrashThreadTool({ accessToken, - maxOutputLength, defaultParams: defaultParams.untrashThread }) ) @@ -1216,7 +1190,6 @@ export const createGmailTools = (args?: RequestParameters): DynamicStructuredToo tools.push( new DeleteThreadTool({ accessToken, - maxOutputLength, defaultParams: defaultParams.deleteThread }) ) diff --git a/packages/components/nodes/tools/GoogleDrive/core.ts b/packages/components/nodes/tools/GoogleDrive/core.ts index f14ba3c95a6..62377f5dc02 100644 --- a/packages/components/nodes/tools/GoogleDrive/core.ts +++ b/packages/components/nodes/tools/GoogleDrive/core.ts @@ -490,7 +490,7 @@ class DeleteFileTool extends BaseGoogleDriveTool { method: 'DELETE', params }) - return `File deleted successfully${TOOL_ARGS_PREFIX}${JSON.stringify(params)}` + return `File deleted successfully` } catch (error) { return `Error deleting file: ${error}` } @@ -818,7 +818,7 @@ class DeleteFolderTool extends BaseGoogleDriveTool { method: 'DELETE', params }) - return `Folder deleted successfully${TOOL_ARGS_PREFIX}${JSON.stringify(params)}` + return `Folder deleted successfully` } catch (error) { return `Error deleting folder: ${error}` } @@ -909,7 +909,7 @@ class RemovePermissionTool extends BaseGoogleDriveTool { method: 'DELETE', params }) - return `Permission removed successfully${TOOL_ARGS_PREFIX}${JSON.stringify(params)}` + return `Permission removed successfully` } catch (error) { return `Error removing permission: ${error}` } diff --git a/packages/components/nodes/tools/MicrosoftOutlook/core.ts b/packages/components/nodes/tools/MicrosoftOutlook/core.ts index 6289d889931..ce6fe8ba8fc 100644 --- a/packages/components/nodes/tools/MicrosoftOutlook/core.ts +++ b/packages/components/nodes/tools/MicrosoftOutlook/core.ts @@ -1,6 +1,7 @@ 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` @@ -17,7 +18,6 @@ export interface RequestParameters { body?: Body url?: string description?: string - maxOutputLength?: number name?: string actions?: string[] accessToken?: string @@ -134,15 +134,13 @@ const ForwardMessageSchema = z.object({ class BaseOutlookTool extends DynamicStructuredTool { protected accessToken: string = '' - protected maxOutputLength: number = Infinity constructor(args: any) { super(args) this.accessToken = args.accessToken ?? '' - this.maxOutputLength = args.maxOutputLength ?? Infinity } - async makeGraphRequest(url: string, method: string = 'GET', body?: any): Promise { + async makeGraphRequest(url: string, method: string = 'GET', body?: any, params?: any): Promise { const headers = { Authorization: `Bearer ${this.accessToken}`, 'Content-Type': 'application/json', @@ -161,7 +159,7 @@ class BaseOutlookTool extends DynamicStructuredTool { } const data = await response.text() - return data.slice(0, this.maxOutputLength) + return data + TOOL_ARGS_PREFIX + JSON.stringify(params) } parseEmailAddresses(emailString: string) { @@ -187,7 +185,7 @@ class ListCalendarsTool extends BaseOutlookTool { method: 'GET', headers: {} } - super({ ...toolInput, accessToken: args.accessToken, maxOutputLength: args.maxOutputLength }) + super({ ...toolInput, accessToken: args.accessToken }) this.defaultParams = args.defaultParams || {} } @@ -200,7 +198,7 @@ class ListCalendarsTool extends BaseOutlookTool { const url = `https://graph.microsoft.com/v1.0/me/calendars?${queryParams.toString()}` try { - const response = await this.makeGraphRequest(url) + const response = await this.makeGraphRequest(url, 'GET', undefined, params) return response } catch (error) { return `Error listing calendars: ${error}` @@ -220,7 +218,7 @@ class GetCalendarTool extends BaseOutlookTool { method: 'GET', headers: {} } - super({ ...toolInput, accessToken: args.accessToken, maxOutputLength: args.maxOutputLength }) + super({ ...toolInput, accessToken: args.accessToken }) this.defaultParams = args.defaultParams || {} } @@ -229,7 +227,7 @@ class GetCalendarTool extends BaseOutlookTool { const url = `https://graph.microsoft.com/v1.0/me/calendars/${params.calendarId}` try { - const response = await this.makeGraphRequest(url) + const response = await this.makeGraphRequest(url, 'GET', undefined, params) return response } catch (error) { return `Error getting calendar: ${error}` @@ -249,7 +247,7 @@ class CreateCalendarTool extends BaseOutlookTool { method: 'POST', headers: {} } - super({ ...toolInput, accessToken: args.accessToken, maxOutputLength: args.maxOutputLength }) + super({ ...toolInput, accessToken: args.accessToken }) this.defaultParams = args.defaultParams || {} } @@ -262,7 +260,7 @@ class CreateCalendarTool extends BaseOutlookTool { } const url = 'https://graph.microsoft.com/v1.0/me/calendars' - const response = await this.makeGraphRequest(url, 'POST', calendarData) + const response = await this.makeGraphRequest(url, 'POST', calendarData, params) return response } catch (error) { return `Error creating calendar: ${error}` @@ -282,7 +280,7 @@ class UpdateCalendarTool extends BaseOutlookTool { method: 'PATCH', headers: {} } - super({ ...toolInput, accessToken: args.accessToken, maxOutputLength: args.maxOutputLength }) + super({ ...toolInput, accessToken: args.accessToken }) this.defaultParams = args.defaultParams || {} } @@ -295,7 +293,7 @@ class UpdateCalendarTool extends BaseOutlookTool { } const url = `https://graph.microsoft.com/v1.0/me/calendars/${params.calendarId}` - const response = await this.makeGraphRequest(url, 'PATCH', calendarData) + const response = await this.makeGraphRequest(url, 'PATCH', calendarData, params) return response } catch (error) { return `Error updating calendar: ${error}` @@ -315,7 +313,7 @@ class DeleteCalendarTool extends BaseOutlookTool { method: 'DELETE', headers: {} } - super({ ...toolInput, accessToken: args.accessToken, maxOutputLength: args.maxOutputLength }) + super({ ...toolInput, accessToken: args.accessToken }) this.defaultParams = args.defaultParams || {} } @@ -324,7 +322,7 @@ class DeleteCalendarTool extends BaseOutlookTool { const url = `https://graph.microsoft.com/v1.0/me/calendars/${params.calendarId}` try { - await this.makeGraphRequest(url, 'DELETE') + await this.makeGraphRequest(url, 'DELETE', undefined, params) return `Calendar ${params.calendarId} deleted successfully` } catch (error) { return `Error deleting calendar: ${error}` @@ -344,7 +342,7 @@ class ListEventsTool extends BaseOutlookTool { method: 'GET', headers: {} } - super({ ...toolInput, accessToken: args.accessToken, maxOutputLength: args.maxOutputLength }) + super({ ...toolInput, accessToken: args.accessToken }) this.defaultParams = args.defaultParams || {} } @@ -371,7 +369,7 @@ class ListEventsTool extends BaseOutlookTool { const url = `${baseUrl}?${queryParams.toString()}` try { - const response = await this.makeGraphRequest(url) + const response = await this.makeGraphRequest(url, 'GET', undefined, params) return response } catch (error) { return `Error listing events: ${error}` @@ -391,7 +389,7 @@ class GetEventTool extends BaseOutlookTool { method: 'GET', headers: {} } - super({ ...toolInput, accessToken: args.accessToken, maxOutputLength: args.maxOutputLength }) + super({ ...toolInput, accessToken: args.accessToken }) this.defaultParams = args.defaultParams || {} } @@ -400,7 +398,7 @@ class GetEventTool extends BaseOutlookTool { const url = `https://graph.microsoft.com/v1.0/me/events/${params.eventId}` try { - const response = await this.makeGraphRequest(url) + const response = await this.makeGraphRequest(url, 'GET', undefined, params) return response } catch (error) { return `Error getting event: ${error}` @@ -420,7 +418,7 @@ class CreateEventTool extends BaseOutlookTool { method: 'POST', headers: {} } - super({ ...toolInput, accessToken: args.accessToken, maxOutputLength: args.maxOutputLength }) + super({ ...toolInput, accessToken: args.accessToken }) this.defaultParams = args.defaultParams || {} } @@ -451,7 +449,7 @@ class CreateEventTool extends BaseOutlookTool { } const url = 'https://graph.microsoft.com/v1.0/me/events' - const response = await this.makeGraphRequest(url, 'POST', eventData) + const response = await this.makeGraphRequest(url, 'POST', eventData, params) return response } catch (error) { return `Error creating event: ${error}` @@ -471,7 +469,7 @@ class UpdateEventTool extends BaseOutlookTool { method: 'PATCH', headers: {} } - super({ ...toolInput, accessToken: args.accessToken, maxOutputLength: args.maxOutputLength }) + super({ ...toolInput, accessToken: args.accessToken }) this.defaultParams = args.defaultParams || {} } @@ -483,7 +481,7 @@ class UpdateEventTool extends BaseOutlookTool { 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) + const response = await this.makeGraphRequest(url, 'PATCH', eventData, params) return response } catch (error) { return `Error updating event: ${error}` @@ -503,7 +501,7 @@ class DeleteEventTool extends BaseOutlookTool { method: 'DELETE', headers: {} } - super({ ...toolInput, accessToken: args.accessToken, maxOutputLength: args.maxOutputLength }) + super({ ...toolInput, accessToken: args.accessToken }) this.defaultParams = args.defaultParams || {} } @@ -512,7 +510,7 @@ class DeleteEventTool extends BaseOutlookTool { const url = `https://graph.microsoft.com/v1.0/me/events/${params.eventId}` try { - await this.makeGraphRequest(url, 'DELETE') + await this.makeGraphRequest(url, 'DELETE', undefined, params) return `Event ${params.eventId} deleted successfully` } catch (error) { return `Error deleting event: ${error}` @@ -533,7 +531,7 @@ class ListMessagesTool extends BaseOutlookTool { method: 'GET', headers: {} } - super({ ...toolInput, accessToken: args.accessToken, maxOutputLength: args.maxOutputLength }) + super({ ...toolInput, accessToken: args.accessToken }) this.defaultParams = args.defaultParams || {} } @@ -547,7 +545,7 @@ class ListMessagesTool extends BaseOutlookTool { const url = `https://graph.microsoft.com/v1.0/me/messages?${queryParams.toString()}` try { - const response = await this.makeGraphRequest(url) + const response = await this.makeGraphRequest(url, 'GET', undefined, params) return response } catch (error) { return `Error listing messages: ${error}` @@ -567,7 +565,7 @@ class GetMessageTool extends BaseOutlookTool { method: 'GET', headers: {} } - super({ ...toolInput, accessToken: args.accessToken, maxOutputLength: args.maxOutputLength }) + super({ ...toolInput, accessToken: args.accessToken }) this.defaultParams = args.defaultParams || {} } @@ -576,7 +574,7 @@ class GetMessageTool extends BaseOutlookTool { const url = `https://graph.microsoft.com/v1.0/me/messages/${params.messageId}` try { - const response = await this.makeGraphRequest(url) + const response = await this.makeGraphRequest(url, 'GET', undefined, params) return response } catch (error) { return `Error getting message: ${error}` @@ -596,7 +594,7 @@ class CreateDraftMessageTool extends BaseOutlookTool { method: 'POST', headers: {} } - super({ ...toolInput, accessToken: args.accessToken, maxOutputLength: args.maxOutputLength }) + super({ ...toolInput, accessToken: args.accessToken }) this.defaultParams = args.defaultParams || {} } @@ -616,7 +614,7 @@ class CreateDraftMessageTool extends BaseOutlookTool { } const url = 'https://graph.microsoft.com/v1.0/me/messages' - const response = await this.makeGraphRequest(url, 'POST', messageData) + const response = await this.makeGraphRequest(url, 'POST', messageData, params) return response } catch (error) { return `Error creating draft message: ${error}` @@ -636,7 +634,7 @@ class SendMessageTool extends BaseOutlookTool { method: 'POST', headers: {} } - super({ ...toolInput, accessToken: args.accessToken, maxOutputLength: args.maxOutputLength }) + super({ ...toolInput, accessToken: args.accessToken }) this.defaultParams = args.defaultParams || {} } @@ -657,7 +655,7 @@ class SendMessageTool extends BaseOutlookTool { } const url = 'https://graph.microsoft.com/v1.0/me/sendMail' - await this.makeGraphRequest(url, 'POST', messageData) + await this.makeGraphRequest(url, 'POST', messageData, params) return 'Message sent successfully' } catch (error) { return `Error sending message: ${error}` @@ -677,7 +675,7 @@ class UpdateMessageTool extends BaseOutlookTool { method: 'PATCH', headers: {} } - super({ ...toolInput, accessToken: args.accessToken, maxOutputLength: args.maxOutputLength }) + super({ ...toolInput, accessToken: args.accessToken }) this.defaultParams = args.defaultParams || {} } @@ -689,7 +687,7 @@ class UpdateMessageTool extends BaseOutlookTool { 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) + const response = await this.makeGraphRequest(url, 'PATCH', messageData, params) return response } catch (error) { return `Error updating message: ${error}` @@ -709,7 +707,7 @@ class DeleteMessageTool extends BaseOutlookTool { method: 'DELETE', headers: {} } - super({ ...toolInput, accessToken: args.accessToken, maxOutputLength: args.maxOutputLength }) + super({ ...toolInput, accessToken: args.accessToken }) this.defaultParams = args.defaultParams || {} } @@ -718,7 +716,7 @@ class DeleteMessageTool extends BaseOutlookTool { const url = `https://graph.microsoft.com/v1.0/me/messages/${params.messageId}` try { - await this.makeGraphRequest(url, 'DELETE') + await this.makeGraphRequest(url, 'DELETE', undefined, params) return `Message ${params.messageId} deleted successfully` } catch (error) { return `Error deleting message: ${error}` @@ -738,7 +736,7 @@ class CopyMessageTool extends BaseOutlookTool { method: 'POST', headers: {} } - super({ ...toolInput, accessToken: args.accessToken, maxOutputLength: args.maxOutputLength }) + super({ ...toolInput, accessToken: args.accessToken }) this.defaultParams = args.defaultParams || {} } @@ -751,7 +749,7 @@ class CopyMessageTool extends BaseOutlookTool { } const url = `https://graph.microsoft.com/v1.0/me/messages/${params.messageId}/copy` - const response = await this.makeGraphRequest(url, 'POST', copyData) + const response = await this.makeGraphRequest(url, 'POST', copyData, params) return response } catch (error) { return `Error copying message: ${error}` @@ -771,7 +769,7 @@ class MoveMessageTool extends BaseOutlookTool { method: 'POST', headers: {} } - super({ ...toolInput, accessToken: args.accessToken, maxOutputLength: args.maxOutputLength }) + super({ ...toolInput, accessToken: args.accessToken }) this.defaultParams = args.defaultParams || {} } @@ -784,7 +782,7 @@ class MoveMessageTool extends BaseOutlookTool { } const url = `https://graph.microsoft.com/v1.0/me/messages/${params.messageId}/move` - const response = await this.makeGraphRequest(url, 'POST', moveData) + const response = await this.makeGraphRequest(url, 'POST', moveData, params) return response } catch (error) { return `Error moving message: ${error}` @@ -804,7 +802,7 @@ class ReplyMessageTool extends BaseOutlookTool { method: 'POST', headers: {} } - super({ ...toolInput, accessToken: args.accessToken, maxOutputLength: args.maxOutputLength }) + super({ ...toolInput, accessToken: args.accessToken }) this.defaultParams = args.defaultParams || {} } @@ -817,7 +815,7 @@ class ReplyMessageTool extends BaseOutlookTool { } const url = `https://graph.microsoft.com/v1.0/me/messages/${params.messageId}/reply` - await this.makeGraphRequest(url, 'POST', replyData) + await this.makeGraphRequest(url, 'POST', replyData, params) return 'Reply sent successfully' } catch (error) { return `Error replying to message: ${error}` @@ -837,7 +835,7 @@ class ForwardMessageTool extends BaseOutlookTool { method: 'POST', headers: {} } - super({ ...toolInput, accessToken: args.accessToken, maxOutputLength: args.maxOutputLength }) + super({ ...toolInput, accessToken: args.accessToken }) this.defaultParams = args.defaultParams || {} } @@ -851,7 +849,7 @@ class ForwardMessageTool extends BaseOutlookTool { } const url = `https://graph.microsoft.com/v1.0/me/messages/${params.messageId}/forward` - await this.makeGraphRequest(url, 'POST', forwardData) + await this.makeGraphRequest(url, 'POST', forwardData, params) return 'Message forwarded successfully' } catch (error) { return `Error forwarding message: ${error}` @@ -863,14 +861,12 @@ export const createOutlookTools = (args?: RequestParameters): DynamicStructuredT const tools: DynamicStructuredTool[] = [] const actions = args?.actions || [] const accessToken = args?.accessToken || '' - const maxOutputLength = args?.maxOutputLength || Infinity const defaultParams = args?.defaultParams || {} // Calendar tools if (actions.includes('listCalendars')) { const listTool = new ListCalendarsTool({ accessToken, - maxOutputLength, defaultParams: defaultParams.listCalendars }) tools.push(listTool) @@ -879,7 +875,6 @@ export const createOutlookTools = (args?: RequestParameters): DynamicStructuredT if (actions.includes('getCalendar')) { const getTool = new GetCalendarTool({ accessToken, - maxOutputLength, defaultParams: defaultParams.getCalendar }) tools.push(getTool) @@ -888,7 +883,6 @@ export const createOutlookTools = (args?: RequestParameters): DynamicStructuredT if (actions.includes('createCalendar')) { const createTool = new CreateCalendarTool({ accessToken, - maxOutputLength, defaultParams: defaultParams.createCalendar }) tools.push(createTool) @@ -897,7 +891,6 @@ export const createOutlookTools = (args?: RequestParameters): DynamicStructuredT if (actions.includes('updateCalendar')) { const updateTool = new UpdateCalendarTool({ accessToken, - maxOutputLength, defaultParams: defaultParams.updateCalendar }) tools.push(updateTool) @@ -906,7 +899,6 @@ export const createOutlookTools = (args?: RequestParameters): DynamicStructuredT if (actions.includes('deleteCalendar')) { const deleteTool = new DeleteCalendarTool({ accessToken, - maxOutputLength, defaultParams: defaultParams.deleteCalendar }) tools.push(deleteTool) @@ -915,7 +907,6 @@ export const createOutlookTools = (args?: RequestParameters): DynamicStructuredT if (actions.includes('listEvents')) { const listTool = new ListEventsTool({ accessToken, - maxOutputLength, defaultParams: defaultParams.listEvents }) tools.push(listTool) @@ -924,7 +915,6 @@ export const createOutlookTools = (args?: RequestParameters): DynamicStructuredT if (actions.includes('getEvent')) { const getTool = new GetEventTool({ accessToken, - maxOutputLength, defaultParams: defaultParams.getEvent }) tools.push(getTool) @@ -933,7 +923,6 @@ export const createOutlookTools = (args?: RequestParameters): DynamicStructuredT if (actions.includes('createEvent')) { const createTool = new CreateEventTool({ accessToken, - maxOutputLength, defaultParams: defaultParams.createEvent }) tools.push(createTool) @@ -942,7 +931,6 @@ export const createOutlookTools = (args?: RequestParameters): DynamicStructuredT if (actions.includes('updateEvent')) { const updateTool = new UpdateEventTool({ accessToken, - maxOutputLength, defaultParams: defaultParams.updateEvent }) tools.push(updateTool) @@ -951,7 +939,6 @@ export const createOutlookTools = (args?: RequestParameters): DynamicStructuredT if (actions.includes('deleteEvent')) { const deleteTool = new DeleteEventTool({ accessToken, - maxOutputLength, defaultParams: defaultParams.deleteEvent }) tools.push(deleteTool) @@ -961,7 +948,6 @@ export const createOutlookTools = (args?: RequestParameters): DynamicStructuredT if (actions.includes('listMessages')) { const listTool = new ListMessagesTool({ accessToken, - maxOutputLength, defaultParams: defaultParams.listMessages }) tools.push(listTool) @@ -970,7 +956,6 @@ export const createOutlookTools = (args?: RequestParameters): DynamicStructuredT if (actions.includes('getMessage')) { const getTool = new GetMessageTool({ accessToken, - maxOutputLength, defaultParams: defaultParams.getMessage }) tools.push(getTool) @@ -979,7 +964,6 @@ export const createOutlookTools = (args?: RequestParameters): DynamicStructuredT if (actions.includes('createDraftMessage')) { const createTool = new CreateDraftMessageTool({ accessToken, - maxOutputLength, defaultParams: defaultParams.createDraftMessage }) tools.push(createTool) @@ -988,7 +972,6 @@ export const createOutlookTools = (args?: RequestParameters): DynamicStructuredT if (actions.includes('sendMessage')) { const sendTool = new SendMessageTool({ accessToken, - maxOutputLength, defaultParams: defaultParams.sendMessage }) tools.push(sendTool) @@ -997,7 +980,6 @@ export const createOutlookTools = (args?: RequestParameters): DynamicStructuredT if (actions.includes('updateMessage')) { const updateTool = new UpdateMessageTool({ accessToken, - maxOutputLength, defaultParams: defaultParams.updateMessage }) tools.push(updateTool) @@ -1006,7 +988,6 @@ export const createOutlookTools = (args?: RequestParameters): DynamicStructuredT if (actions.includes('deleteMessage')) { const deleteTool = new DeleteMessageTool({ accessToken, - maxOutputLength, defaultParams: defaultParams.deleteMessage }) tools.push(deleteTool) @@ -1015,7 +996,6 @@ export const createOutlookTools = (args?: RequestParameters): DynamicStructuredT if (actions.includes('copyMessage')) { const copyTool = new CopyMessageTool({ accessToken, - maxOutputLength, defaultParams: defaultParams.copyMessage }) tools.push(copyTool) @@ -1024,7 +1004,6 @@ export const createOutlookTools = (args?: RequestParameters): DynamicStructuredT if (actions.includes('moveMessage')) { const moveTool = new MoveMessageTool({ accessToken, - maxOutputLength, defaultParams: defaultParams.moveMessage }) tools.push(moveTool) @@ -1033,7 +1012,6 @@ export const createOutlookTools = (args?: RequestParameters): DynamicStructuredT if (actions.includes('replyMessage')) { const replyTool = new ReplyMessageTool({ accessToken, - maxOutputLength, defaultParams: defaultParams.replyMessage }) tools.push(replyTool) @@ -1042,7 +1020,6 @@ export const createOutlookTools = (args?: RequestParameters): DynamicStructuredT if (actions.includes('forwardMessage')) { const forwardTool = new ForwardMessageTool({ accessToken, - maxOutputLength, defaultParams: defaultParams.forwardMessage }) tools.push(forwardTool) diff --git a/packages/components/nodes/tools/MicrosoftTeams/core.ts b/packages/components/nodes/tools/MicrosoftTeams/core.ts index eaf455ced9e..a0c08091469 100644 --- a/packages/components/nodes/tools/MicrosoftTeams/core.ts +++ b/packages/components/nodes/tools/MicrosoftTeams/core.ts @@ -67,6 +67,10 @@ abstract class BaseTeamsTool extends DynamicStructuredTool { 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 } @@ -106,16 +110,14 @@ class ListChannelsTool extends BaseTeamsTool { const channels = result.value || [] const limitedChannels = channels.slice(0, maxResults) - return ( - JSON.stringify({ - success: true, - channels: limitedChannels, - count: limitedChannels.length, - total: channels.length - }) + - TOOL_ARGS_PREFIX + - JSON.stringify(params) - ) + const responseData = { + success: true, + channels: limitedChannels, + count: limitedChannels.length, + total: channels.length + } + + return this.formatResponse(responseData, params) } catch (error) { return `Error listing channels: ${error}` } @@ -151,16 +153,15 @@ class GetChannelTool extends BaseTeamsTool { const endpoint = `/teams/${teamId}/channels/${channelId}` const result = await this.makeTeamsRequest(endpoint) - return ( - JSON.stringify({ + return this.formatResponse( + { success: true, channel: result - }) + - TOOL_ARGS_PREFIX + - JSON.stringify(params) + }, + params ) } catch (error) { - return `Error getting channel: ${error}` + return this.formatResponse(`Error getting channel: ${error}`, params) } } } @@ -206,17 +207,16 @@ class CreateChannelTool extends BaseTeamsTool { const endpoint = `/teams/${teamId}/channels` const result = await this.makeTeamsRequest(endpoint, 'POST', body) - return ( - JSON.stringify({ + return this.formatResponse( + { success: true, channel: result, message: `Channel "${displayName}" created successfully` - }) + - TOOL_ARGS_PREFIX + - JSON.stringify(params) + }, + params ) } catch (error) { - return `Error creating channel: ${error}` + return this.formatResponse(`Error creating channel: ${error}`, params) } } } @@ -260,16 +260,15 @@ class UpdateChannelTool extends BaseTeamsTool { const endpoint = `/teams/${teamId}/channels/${channelId}` await this.makeTeamsRequest(endpoint, 'PATCH', body) - return ( - JSON.stringify({ + return this.formatResponse( + { success: true, message: 'Channel updated successfully' - }) + - TOOL_ARGS_PREFIX + - JSON.stringify(params) + }, + params ) } catch (error) { - return `Error updating channel: ${error}` + return this.formatResponse(`Error updating channel: ${error}`, params) } } } @@ -303,16 +302,15 @@ class DeleteChannelTool extends BaseTeamsTool { const endpoint = `/teams/${teamId}/channels/${channelId}` await this.makeTeamsRequest(endpoint, 'DELETE') - return ( - JSON.stringify({ + return this.formatResponse( + { success: true, message: 'Channel deleted successfully' - }) + - TOOL_ARGS_PREFIX + - JSON.stringify(params) + }, + params ) } catch (error) { - return `Error deleting channel: ${error}` + return this.formatResponse(`Error deleting channel: ${error}`, params) } } } @@ -346,16 +344,15 @@ class ArchiveChannelTool extends BaseTeamsTool { const endpoint = `/teams/${teamId}/channels/${channelId}/archive` await this.makeTeamsRequest(endpoint, 'POST', {}) - return ( - JSON.stringify({ + return this.formatResponse( + { success: true, message: 'Channel archived successfully' - }) + - TOOL_ARGS_PREFIX + - JSON.stringify(params) + }, + params ) } catch (error) { - return `Error archiving channel: ${error}` + return this.formatResponse(`Error archiving channel: ${error}`, params) } } } @@ -389,16 +386,15 @@ class UnarchiveChannelTool extends BaseTeamsTool { const endpoint = `/teams/${teamId}/channels/${channelId}/unarchive` await this.makeTeamsRequest(endpoint, 'POST', {}) - return ( - JSON.stringify({ + return this.formatResponse( + { success: true, message: 'Channel unarchived successfully' - }) + - TOOL_ARGS_PREFIX + - JSON.stringify(params) + }, + params ) } catch (error) { - return `Error unarchiving channel: ${error}` + return this.formatResponse(`Error unarchiving channel: ${error}`, params) } } } @@ -432,17 +428,16 @@ class ListChannelMembersTool extends BaseTeamsTool { const endpoint = `/teams/${teamId}/channels/${channelId}/members` const result = await this.makeTeamsRequest(endpoint) - return ( - JSON.stringify({ + return this.formatResponse( + { success: true, members: result.value || [], count: result.value?.length || 0 - }) + - TOOL_ARGS_PREFIX + - JSON.stringify(params) + }, + params ) } catch (error) { - return `Error listing channel members: ${error}` + return this.formatResponse(`Error listing channel members: ${error}`, params) } } } @@ -482,16 +477,15 @@ class AddChannelMemberTool extends BaseTeamsTool { const endpoint = `/teams/${teamId}/channels/${channelId}/members` await this.makeTeamsRequest(endpoint, 'POST', body) - return ( - JSON.stringify({ + return this.formatResponse( + { success: true, message: 'Member added to channel successfully' - }) + - TOOL_ARGS_PREFIX + - JSON.stringify(params) + }, + params ) } catch (error) { - return `Error adding channel member: ${error}` + return this.formatResponse(`Error adding channel member: ${error}`, params) } } } @@ -535,16 +529,15 @@ class RemoveChannelMemberTool extends BaseTeamsTool { const endpoint = `/teams/${teamId}/channels/${channelId}/members/${member.id}` await this.makeTeamsRequest(endpoint, 'DELETE') - return ( - JSON.stringify({ + return this.formatResponse( + { success: true, message: 'Member removed from channel successfully' - }) + - TOOL_ARGS_PREFIX + - JSON.stringify(params) + }, + params ) } catch (error) { - return `Error removing channel member: ${error}` + return this.formatResponse(`Error removing channel member: ${error}`, params) } } } @@ -575,17 +568,16 @@ class ListChatsTool extends BaseTeamsTool { const endpoint = `/me/chats?$top=${maxResults}` const result = await this.makeTeamsRequest(endpoint) - return ( - JSON.stringify({ + return this.formatResponse( + { success: true, chats: result.value || [], count: result.value?.length || 0 - }) + - TOOL_ARGS_PREFIX + - JSON.stringify(params) + }, + params ) } catch (error) { - return `Error listing chats: ${error}` + return this.formatResponse(`Error listing chats: ${error}`, params) } } } @@ -618,16 +610,15 @@ class GetChatTool extends BaseTeamsTool { const endpoint = `/chats/${chatId}` const result = await this.makeTeamsRequest(endpoint) - return ( - JSON.stringify({ + return this.formatResponse( + { success: true, chat: result - }) + - TOOL_ARGS_PREFIX + - JSON.stringify(params) + }, + params ) } catch (error) { - return `Error getting chat: ${error}` + return this.formatResponse(`Error getting chat: ${error}`, params) } } } @@ -677,17 +668,16 @@ class CreateChatTool extends BaseTeamsTool { const endpoint = '/chats' const result = await this.makeTeamsRequest(endpoint, 'POST', body) - return ( - JSON.stringify({ + return this.formatResponse( + { success: true, chat: result, message: 'Chat created successfully' - }) + - TOOL_ARGS_PREFIX + - JSON.stringify(params) + }, + params ) } catch (error) { - return `Error creating chat: ${error}` + return this.formatResponse(`Error creating chat: ${error}`, params) } } } @@ -726,16 +716,15 @@ class UpdateChatTool extends BaseTeamsTool { const endpoint = `/chats/${chatId}` await this.makeTeamsRequest(endpoint, 'PATCH', body) - return ( - JSON.stringify({ + return this.formatResponse( + { success: true, message: 'Chat updated successfully' - }) + - TOOL_ARGS_PREFIX + - JSON.stringify(params) + }, + params ) } catch (error) { - return `Error updating chat: ${error}` + return this.formatResponse(`Error updating chat: ${error}`, params) } } } @@ -768,16 +757,15 @@ class DeleteChatTool extends BaseTeamsTool { const endpoint = `/chats/${chatId}` await this.makeTeamsRequest(endpoint, 'DELETE') - return ( - JSON.stringify({ + return this.formatResponse( + { success: true, message: 'Chat deleted successfully' - }) + - TOOL_ARGS_PREFIX + - JSON.stringify(params) + }, + params ) } catch (error) { - return `Error deleting chat: ${error}` + return this.formatResponse(`Error deleting chat: ${error}`, params) } } } @@ -810,17 +798,16 @@ class ListChatMembersTool extends BaseTeamsTool { const endpoint = `/chats/${chatId}/members` const result = await this.makeTeamsRequest(endpoint) - return ( - JSON.stringify({ + return this.formatResponse( + { success: true, members: result.value || [], count: result.value?.length || 0 - }) + - TOOL_ARGS_PREFIX + - JSON.stringify(params) + }, + params ) } catch (error) { - return `Error listing chat members: ${error}` + return this.formatResponse(`Error listing chat members: ${error}`, params) } } } @@ -859,16 +846,15 @@ class AddChatMemberTool extends BaseTeamsTool { const endpoint = `/chats/${chatId}/members` await this.makeTeamsRequest(endpoint, 'POST', body) - return ( - JSON.stringify({ + return this.formatResponse( + { success: true, message: 'Member added to chat successfully' - }) + - TOOL_ARGS_PREFIX + - JSON.stringify(params) + }, + params ) } catch (error) { - return `Error adding chat member: ${error}` + return this.formatResponse(`Error adding chat member: ${error}`, params) } } } @@ -911,16 +897,15 @@ class RemoveChatMemberTool extends BaseTeamsTool { const endpoint = `/chats/${chatId}/members/${member.id}` await this.makeTeamsRequest(endpoint, 'DELETE') - return ( - JSON.stringify({ + return this.formatResponse( + { success: true, message: 'Member removed from chat successfully' - }) + - TOOL_ARGS_PREFIX + - JSON.stringify(params) + }, + params ) } catch (error) { - return `Error removing chat member: ${error}` + return this.formatResponse(`Error removing chat member: ${error}`, params) } } } @@ -960,16 +945,15 @@ class PinMessageTool extends BaseTeamsTool { const endpoint = `/chats/${chatId}/pinnedMessages` await this.makeTeamsRequest(endpoint, 'POST', body) - return ( - JSON.stringify({ + return this.formatResponse( + { success: true, message: 'Message pinned successfully' - }) + - TOOL_ARGS_PREFIX + - JSON.stringify(params) + }, + params ) } catch (error) { - return `Error pinning message: ${error}` + return this.formatResponse(`Error pinning message: ${error}`, params) } } } @@ -1012,16 +996,15 @@ class UnpinMessageTool extends BaseTeamsTool { const endpoint = `/chats/${chatId}/pinnedMessages/${pinnedMessage.id}` await this.makeTeamsRequest(endpoint, 'DELETE') - return ( - JSON.stringify({ + return this.formatResponse( + { success: true, message: 'Message unpinned successfully' - }) + - TOOL_ARGS_PREFIX + - JSON.stringify(params) + }, + params ) } catch (error) { - return `Error unpinning message: ${error}` + return this.formatResponse(`Error unpinning message: ${error}`, params) } } } @@ -1066,18 +1049,17 @@ class ListMessagesTool extends BaseTeamsTool { const result = await this.makeTeamsRequest(endpoint) - return ( - JSON.stringify({ + return this.formatResponse( + { success: true, messages: result.value || [], count: result.value?.length || 0, context: teamId ? 'channel' : 'chat' - }) + - TOOL_ARGS_PREFIX + - JSON.stringify(params) + }, + params ) } catch (error) { - return `Error listing messages: ${error}` + return this.formatResponse(`Error listing messages: ${error}`, params) } } } @@ -1120,17 +1102,16 @@ class GetMessageTool extends BaseTeamsTool { const result = await this.makeTeamsRequest(endpoint) - return ( - JSON.stringify({ + return this.formatResponse( + { success: true, message: result, context: teamId ? 'channel' : 'chat' - }) + - TOOL_ARGS_PREFIX + - JSON.stringify(params) + }, + params ) } catch (error) { - return `Error getting message: ${error}` + return this.formatResponse(`Error getting message: ${error}`, params) } } } @@ -1181,18 +1162,17 @@ class SendMessageTool extends BaseTeamsTool { const result = await this.makeTeamsRequest(endpoint, 'POST', body) - return ( - JSON.stringify({ + return this.formatResponse( + { success: true, message: result, context: teamId ? 'channel' : 'chat', messageText: 'Message sent successfully' - }) + - TOOL_ARGS_PREFIX + - JSON.stringify(params) + }, + params ) } catch (error) { - return `Error sending message: ${error}` + return this.formatResponse(`Error sending message: ${error}`, params) } } } @@ -1240,17 +1220,16 @@ class UpdateMessageTool extends BaseTeamsTool { await this.makeTeamsRequest(endpoint, 'PATCH', body) - return ( - JSON.stringify({ + return this.formatResponse( + { success: true, message: 'Message updated successfully', context: teamId ? 'channel' : 'chat' - }) + - TOOL_ARGS_PREFIX + - JSON.stringify(params) + }, + params ) } catch (error) { - return `Error updating message: ${error}` + return this.formatResponse(`Error updating message: ${error}`, params) } } } @@ -1293,17 +1272,16 @@ class DeleteMessageTool extends BaseTeamsTool { await this.makeTeamsRequest(endpoint, 'POST', {}) - return ( - JSON.stringify({ + return this.formatResponse( + { success: true, message: 'Message deleted successfully', context: teamId ? 'channel' : 'chat' - }) + - TOOL_ARGS_PREFIX + - JSON.stringify(params) + }, + params ) } catch (error) { - return `Error deleting message: ${error}` + return this.formatResponse(`Error deleting message: ${error}`, params) } } } @@ -1355,18 +1333,17 @@ class ReplyToMessageTool extends BaseTeamsTool { const result = await this.makeTeamsRequest(endpoint, 'POST', body) - return ( - JSON.stringify({ + return this.formatResponse( + { success: true, reply: result, message: 'Reply sent successfully', context: teamId ? 'channel' : 'chat' - }) + - TOOL_ARGS_PREFIX + - JSON.stringify(params) + }, + params ) } catch (error) { - return `Error replying to message: ${error}` + return this.formatResponse(`Error replying to message: ${error}`, params) } } } @@ -1418,17 +1395,16 @@ class SetReactionTool extends BaseTeamsTool { await this.makeTeamsRequest(endpoint, 'POST', body) - return ( - JSON.stringify({ + return this.formatResponse( + { success: true, message: `Reaction "${reactionType}" set successfully`, context: teamId ? 'channel' : 'chat' - }) + - TOOL_ARGS_PREFIX + - JSON.stringify(params) + }, + params ) } catch (error) { - return `Error setting reaction: ${error}` + return this.formatResponse(`Error setting reaction: ${error}`, params) } } } @@ -1480,17 +1456,16 @@ class UnsetReactionTool extends BaseTeamsTool { await this.makeTeamsRequest(endpoint, 'POST', body) - return ( - JSON.stringify({ + return this.formatResponse( + { success: true, message: `Reaction "${reactionType}" removed successfully`, context: teamId ? 'channel' : 'chat' - }) + - TOOL_ARGS_PREFIX + - JSON.stringify(params) + }, + params ) } catch (error) { - return `Error unsetting reaction: ${error}` + return this.formatResponse(`Error unsetting reaction: ${error}`, params) } } } @@ -1520,18 +1495,17 @@ class GetAllMessagesTool extends BaseTeamsTool { const chatEndpoint = `/me/chats/getAllMessages?$top=${maxResults}` const chatResult = await this.makeTeamsRequest(chatEndpoint) - return ( - JSON.stringify({ + return this.formatResponse( + { success: true, messages: chatResult.value || [], count: chatResult.value?.length || 0, source: 'all_chats_and_channels' - }) + - TOOL_ARGS_PREFIX + - JSON.stringify(params) + }, + params ) } catch (error) { - return `Error getting all messages: ${error}` + return this.formatResponse(`Error getting all messages: ${error}`, params) } } }