diff --git a/packages/components/credentials/GmailOAuth2.credential.ts b/packages/components/credentials/GmailOAuth2.credential.ts new file mode 100644 index 00000000..38d23a15 --- /dev/null +++ b/packages/components/credentials/GmailOAuth2.credential.ts @@ -0,0 +1,63 @@ +import { INodeParams, INodeCredential } from '../src/Interface' +const scopes = [ + 'https://www.googleapis.com/auth/gmail.readonly', + 'https://www.googleapis.com/auth/gmail.compose', + 'https://www.googleapis.com/auth/gmail.modify', + 'https://www.googleapis.com/auth/gmail.labels' +] + +class GmailOAuth2 implements INodeCredential { + label: string + name: string + version: number + inputs: INodeParams[] + description: string + + constructor() { + this.label = 'Gmail OAuth2' + this.name = 'gmailOAuth2' + this.version = 1.0 + this.description = + 'You can find the setup instructions here' + this.inputs = [ + { + label: 'Authorization URL', + name: 'authorizationUrl', + type: 'string', + default: 'https://accounts.google.com/o/oauth2/v2/auth' + }, + { + label: 'Access Token URL', + name: 'accessTokenUrl', + type: 'string', + default: 'https://oauth2.googleapis.com/token' + }, + { + label: 'Client ID', + name: 'clientId', + type: 'string' + }, + { + label: 'Client Secret', + name: 'clientSecret', + type: 'password' + }, + { + label: 'Additional Parameters', + name: 'additionalParameters', + type: 'string', + default: 'access_type=offline&prompt=consent', + hidden: true + }, + { + label: 'Scope', + name: 'scope', + type: 'string', + hidden: true, + default: scopes.join(' ') + } + ] + } +} + +module.exports = { credClass: GmailOAuth2 } diff --git a/packages/components/credentials/GoogleCalendarOAuth2.credential.ts b/packages/components/credentials/GoogleCalendarOAuth2.credential.ts new file mode 100644 index 00000000..5792067a --- /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 00000000..de027a8e --- /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 00000000..3e214792 --- /dev/null +++ b/packages/components/credentials/GoogleSheetsOAuth2.credential.ts @@ -0,0 +1,62 @@ +import { INodeParams, INodeCredential } from '../src/Interface' +const scopes = [ + 'https://www.googleapis.com/auth/drive.file', + 'https://www.googleapis.com/auth/spreadsheets', + 'https://www.googleapis.com/auth/drive.metadata' +] + +class GoogleSheetsOAuth2 implements INodeCredential { + label: string + name: string + version: number + inputs: INodeParams[] + description: string + + constructor() { + this.label = 'Google Sheets OAuth2' + this.name = 'googleSheetsOAuth2' + this.version = 1.0 + this.description = + 'You can find the setup instructions here' + this.inputs = [ + { + label: 'Authorization URL', + name: 'authorizationUrl', + type: 'string', + default: 'https://accounts.google.com/o/oauth2/v2/auth' + }, + { + label: 'Access Token URL', + name: 'accessTokenUrl', + type: 'string', + default: 'https://oauth2.googleapis.com/token' + }, + { + label: 'Client ID', + name: 'clientId', + type: 'string' + }, + { + label: 'Client Secret', + name: 'clientSecret', + type: 'password' + }, + { + label: 'Additional Parameters', + name: 'additionalParameters', + type: 'string', + default: 'access_type=offline&prompt=consent', + hidden: true + }, + { + label: 'Scope', + name: 'scope', + type: 'string', + hidden: true, + default: scopes.join(' ') + } + ] + } +} + +module.exports = { credClass: GoogleSheetsOAuth2 } diff --git a/packages/components/credentials/MicrosoftOutlookOAuth2.credential.ts b/packages/components/credentials/MicrosoftOutlookOAuth2.credential.ts new file mode 100644 index 00000000..0308969a --- /dev/null +++ b/packages/components/credentials/MicrosoftOutlookOAuth2.credential.ts @@ -0,0 +1,66 @@ +import { INodeParams, INodeCredential } from '../src/Interface' + +const scopes = [ + 'openid', + 'offline_access', + 'Contacts.Read', + 'Contacts.ReadWrite', + 'Calendars.Read', + 'Calendars.Read.Shared', + 'Calendars.ReadWrite', + 'Mail.Read', + 'Mail.ReadWrite', + 'Mail.ReadWrite.Shared', + 'Mail.Send', + 'Mail.Send.Shared', + 'MailboxSettings.Read' +] + +class MsoftOutlookOAuth2 implements INodeCredential { + label: string + name: string + version: number + description: string + inputs: INodeParams[] + + constructor() { + this.label = 'Microsoft Outlook OAuth2' + this.name = 'microsoftOutlookOAuth2' + this.version = 1.0 + this.description = + 'You can find the setup instructions here' + this.inputs = [ + { + label: 'Authorization URL', + name: 'authorizationUrl', + type: 'string', + default: 'https://login.microsoftonline.com//oauth2/v2.0/authorize' + }, + { + label: 'Access Token URL', + name: 'accessTokenUrl', + type: 'string', + default: 'https://login.microsoftonline.com//oauth2/v2.0/token' + }, + { + label: 'Client ID', + name: 'clientId', + type: 'string' + }, + { + label: 'Client Secret', + name: 'clientSecret', + type: 'password' + }, + { + label: 'Scope', + name: 'scope', + type: 'string', + hidden: true, + default: scopes.join(' ') + } + ] + } +} + +module.exports = { credClass: MsoftOutlookOAuth2 } diff --git a/packages/components/credentials/MicrosoftTeamsOAuth2.credential.ts b/packages/components/credentials/MicrosoftTeamsOAuth2.credential.ts new file mode 100644 index 00000000..ffda846a --- /dev/null +++ b/packages/components/credentials/MicrosoftTeamsOAuth2.credential.ts @@ -0,0 +1,87 @@ +import { INodeParams, INodeCredential } from '../src/Interface' + +// Comprehensive scopes for Microsoft Teams operations +const scopes = [ + // Basic authentication + 'openid', + 'offline_access', + + // User permissions + 'User.Read', + 'User.ReadWrite.All', + + // Teams and Groups + 'Group.ReadWrite.All', + 'Team.ReadBasic.All', + 'Team.Create', + 'TeamMember.ReadWrite.All', + + // Channels + 'Channel.ReadBasic.All', + 'Channel.Create', + 'Channel.Delete.All', + 'ChannelMember.ReadWrite.All', + + // Chat operations + 'Chat.ReadWrite', + 'Chat.Create', + 'ChatMember.ReadWrite', + + // Messages + 'ChatMessage.Send', + 'ChatMessage.Read', + 'ChannelMessage.Send', + 'ChannelMessage.Read.All', + + // Reactions and advanced features + 'TeamsActivity.Send' +] + +class MsoftTeamsOAuth2 implements INodeCredential { + label: string + name: string + version: number + inputs: INodeParams[] + description: string + + constructor() { + this.label = 'Microsoft Teams OAuth2' + this.name = 'microsoftTeamsOAuth2' + this.version = 1.0 + this.description = + 'You can find the setup instructions here' + this.inputs = [ + { + label: 'Authorization URL', + name: 'authorizationUrl', + type: 'string', + default: 'https://login.microsoftonline.com//oauth2/v2.0/authorize' + }, + { + label: 'Access Token URL', + name: 'accessTokenUrl', + type: 'string', + default: 'https://login.microsoftonline.com//oauth2/v2.0/token' + }, + { + label: 'Client ID', + name: 'clientId', + type: 'string' + }, + { + label: 'Client Secret', + name: 'clientSecret', + type: 'password' + }, + { + label: 'Scope', + name: 'scope', + type: 'string', + hidden: true, + default: scopes.join(' ') + } + ] + } +} + +module.exports = { credClass: MsoftTeamsOAuth2 } diff --git a/packages/components/nodes/agentflow/Agent/Agent.ts b/packages/components/nodes/agentflow/Agent/Agent.ts index 55f565d0..4c974cc3 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 d8365b11..db7d1126 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 { @@ -268,6 +268,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) } @@ -290,7 +301,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/documentloaders/File/File.ts b/packages/components/nodes/documentloaders/File/File.ts index 345a4ccc..d3049553 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 1a6afe05..d567f64a 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 00000000..441eb61a --- /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 00000000..03b2f212 --- /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 00000000..8c7a7a2a --- /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 00000000..43af0ccf --- /dev/null +++ b/packages/components/nodes/documentloaders/GoogleSheets/google-sheets.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/documentloaders/Jira/jira.svg b/packages/components/nodes/documentloaders/Jira/jira.svg index 807c5a31..4ace5cc8 100644 --- a/packages/components/nodes/documentloaders/Jira/jira.svg +++ b/packages/components/nodes/documentloaders/Jira/jira.svg @@ -1,2 +1 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/packages/components/nodes/documentloaders/MicrosoftExcel/ExcelLoader.ts b/packages/components/nodes/documentloaders/MicrosoftExcel/ExcelLoader.ts new file mode 100644 index 00000000..1e4889e1 --- /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 00000000..468d1673 --- /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 00000000..22d8f949 --- /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 00000000..bca5e9a5 --- /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 00000000..97f26682 --- /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 00000000..4d2f7b2a --- /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 00000000..7d74af25 --- /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 00000000..640e2c4f --- /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 00000000..dabac0ea --- /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 072822ae..4a55869e 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 51c19804..d77f37e0 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,6 +569,171 @@ 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 fileProcessingMethod = nodeData.inputs?.fileProcessingMethod as string + 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()) + } + + let credentials: S3ClientConfig['credentials'] | undefined + + if (nodeData.credential) { + const credentialData = await getCredentialData(nodeData.credential, options) + const accessKeyId = getCredentialParam('awsKey', credentialData, nodeData) + const secretAccessKey = getCredentialParam('awsSecret', credentialData, nodeData) + + if (accessKeyId && secretAccessKey) { + credentials = { + accessKeyId, + secretAccessKey + } + } + } + + const s3Config: S3ClientConfig = { + region, + 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 @@ -488,26 +756,6 @@ class S3_DocumentLoaders implements INode { const _omitMetadataKeys = nodeData.inputs?.omitMetadataKeys as string const output = nodeData.outputs?.output as string - let credentials: S3ClientConfig['credentials'] | undefined - - if (nodeData.credential) { - const credentialData = await getCredentialData(nodeData.credential, options) - const accessKeyId = getCredentialParam('awsKey', credentialData, nodeData) - const secretAccessKey = getCredentialParam('awsSecret', credentialData, nodeData) - - if (accessKeyId && secretAccessKey) { - credentials = { - accessKeyId, - secretAccessKey - } - } - } - - const s3Config: S3ClientConfig = { - region, - credentials - } - const loader = new S3Loader({ bucket: bucketName, key: keyName, @@ -586,5 +834,202 @@ class S3_DocumentLoaders implements INode { return loader.load() } + + private getMimeTypeFromExtension(fileName: string): string { + const extension = path.extname(fileName).toLowerCase() + const mimeTypeMap: { [key: string]: string } = { + '.pdf': 'application/pdf', + '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + '.doc': 'application/msword', + '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + '.xls': 'application/vnd.ms-excel', + '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + '.ppt': 'application/vnd.ms-powerpoint', + '.txt': 'text/plain', + '.csv': 'text/csv', + '.html': 'text/html', + '.htm': 'text/html', + '.json': 'application/json', + '.xml': 'application/xml', + '.md': 'text/markdown' + } + return mimeTypeMap[extension] || 'application/octet-stream' + } + + private async processFile(fileInfo: any, buffer: Buffer): Promise { + try { + // Handle different file types + if (this.isTextBasedFile(fileInfo.mimeType)) { + // Process text files directly from buffer + const content = buffer.toString('utf-8') + + // Create document with metadata + return [ + { + pageContent: content, + metadata: { + source: fileInfo.webViewLink, + fileId: fileInfo.key, + fileName: fileInfo.name, + mimeType: fileInfo.mimeType, + size: fileInfo.size, + lastModified: fileInfo.lastModified, + etag: fileInfo.etag, + bucketName: fileInfo.bucketName + } + } + ] + } else if (this.isSupportedBinaryFile(fileInfo.mimeType)) { + // Process binary files using loaders + return await this.processBinaryFile(fileInfo, buffer) + } else { + console.warn(`Unsupported file type ${fileInfo.mimeType} for file ${fileInfo.name}`) + return [] + } + } catch (error) { + console.warn(`Failed to process file ${fileInfo.name}: ${error.message}`) + return [] + } + } + + private isTextBasedFile(mimeType: string): boolean { + const textBasedMimeTypes = [ + 'text/plain', + 'text/html', + 'text/css', + 'text/javascript', + 'text/csv', + 'text/xml', + 'application/json', + 'application/xml', + 'text/markdown', + 'text/x-markdown' + ] + return textBasedMimeTypes.includes(mimeType) + } + + private isSupportedBinaryFile(mimeType: string): boolean { + const supportedBinaryTypes = [ + 'application/pdf', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/msword', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'application/vnd.ms-excel', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'application/vnd.ms-powerpoint' + ] + return supportedBinaryTypes.includes(mimeType) + } + + private async processBinaryFile(fileInfo: any, buffer: Buffer): Promise { + let tempFilePath: string | null = null + + try { + // Create temporary file + tempFilePath = await this.createTempFile(buffer, fileInfo.name, fileInfo.mimeType) + + let docs: IDocument[] = [] + const mimeType = fileInfo.mimeType.toLowerCase() + + switch (mimeType) { + case 'application/pdf': { + const pdfLoader = new PDFLoader(tempFilePath, { + // @ts-ignore + pdfjs: () => import('pdf-parse/lib/pdf.js/v1.10.100/build/pdf.js') + }) + docs = await pdfLoader.load() + break + } + case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': + case 'application/msword': { + const docxLoader = new DocxLoader(tempFilePath) + docs = await docxLoader.load() + break + } + case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': + case 'application/vnd.ms-excel': { + const excelLoader = new LoadOfSheet(tempFilePath) + docs = await excelLoader.load() + break + } + case 'application/vnd.openxmlformats-officedocument.presentationml.presentation': + case 'application/vnd.ms-powerpoint': { + const pptxLoader = new PowerpointLoader(tempFilePath) + docs = await pptxLoader.load() + break + } + case 'text/csv': { + const csvLoader = new CSVLoader(tempFilePath) + docs = await csvLoader.load() + break + } + default: + throw new Error(`Unsupported binary file type: ${mimeType}`) + } + + // Add S3 metadata to each document + if (docs.length > 0) { + const s3Metadata = { + source: fileInfo.webViewLink, + fileId: fileInfo.key, + fileName: fileInfo.name, + mimeType: fileInfo.mimeType, + size: fileInfo.size, + lastModified: fileInfo.lastModified, + etag: fileInfo.etag, + bucketName: fileInfo.bucketName, + totalPages: docs.length // Total number of pages/sheets in the file + } + + return docs.map((doc, index) => ({ + ...doc, + metadata: { + ...doc.metadata, // Keep original loader metadata (page numbers, etc.) + ...s3Metadata, // Add S3 metadata + pageIndex: index // Add page/sheet index + } + })) + } + + return [] + } catch (error) { + throw new Error(`Failed to process binary file: ${error.message}`) + } finally { + // Clean up temporary file + if (tempFilePath && fsDefault.existsSync(tempFilePath)) { + try { + fsDefault.unlinkSync(tempFilePath) + } catch (e) { + console.warn(`Failed to delete temporary file: ${tempFilePath}`) + } + } + } + } + + private async createTempFile(buffer: Buffer, fileName: string, mimeType: string): Promise { + // Get appropriate file extension + let extension = path.extname(fileName) + if (!extension) { + const extensionMap: { [key: string]: string } = { + 'application/pdf': '.pdf', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx', + 'application/msword': '.doc', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx', + 'application/vnd.ms-excel': '.xls', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation': '.pptx', + 'application/vnd.ms-powerpoint': '.ppt', + 'text/csv': '.csv' + } + extension = extensionMap[mimeType] || '.tmp' + } + + // Create temporary file + const tempDir = os.tmpdir() + const tempFileName = `s3_${Date.now()}_${Math.random().toString(36).substring(7)}${extension}` + const tempFilePath = path.join(tempDir, tempFileName) + + fsDefault.writeFileSync(tempFilePath, buffer) + return tempFilePath + } } module.exports = { nodeClass: S3_DocumentLoaders } diff --git a/packages/components/nodes/sequentialagents/Agent/Agent.ts b/packages/components/nodes/sequentialagents/Agent/Agent.ts index cb4e03e7..0ad1372c 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 cc6a260c..7ab010c1 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 00000000..4c305406 --- /dev/null +++ b/packages/components/nodes/tools/Gmail/Gmail.ts @@ -0,0 +1,698 @@ +import { convertMultiOptionsToStringArray, getCredentialData, getCredentialParam, refreshOAuth2Token } from '../../../src/utils' +import { createGmailTools } from './core' +import type { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' + +class Gmail_Tools implements INode { + label: string + name: string + version: number + type: string + icon: string + category: string + description: string + baseClasses: string[] + credential: INodeParams + inputs: INodeParams[] + + constructor() { + this.label = 'Gmail' + this.name = 'gmail' + this.version = 1.0 + this.type = 'Gmail' + this.icon = 'gmail.svg' + this.category = 'Tools' + this.description = 'Perform Gmail operations for drafts, messages, labels, and threads' + this.baseClasses = [this.type, 'Tool'] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['gmailOAuth2'] + } + this.inputs = [ + { + label: 'Type', + name: 'gmailType', + type: 'options', + options: [ + { + label: 'Drafts', + name: 'drafts' + }, + { + label: 'Messages', + name: 'messages' + }, + { + label: 'Labels', + name: 'labels' + }, + { + label: 'Threads', + name: 'threads' + } + ] + }, + // Draft Actions + { + label: 'Draft Actions', + name: 'draftActions', + type: 'multiOptions', + options: [ + { + label: 'List Drafts', + name: 'listDrafts' + }, + { + label: 'Create Draft', + name: 'createDraft' + }, + { + label: 'Get Draft', + name: 'getDraft' + }, + { + label: 'Update Draft', + name: 'updateDraft' + }, + { + label: 'Send Draft', + name: 'sendDraft' + }, + { + label: 'Delete Draft', + name: 'deleteDraft' + } + ], + show: { + gmailType: ['drafts'] + } + }, + // Message Actions + { + label: 'Message Actions', + name: 'messageActions', + type: 'multiOptions', + options: [ + { + label: 'List Messages', + name: 'listMessages' + }, + { + label: 'Get Message', + name: 'getMessage' + }, + { + label: 'Send Message', + name: 'sendMessage' + }, + { + label: 'Modify Message', + name: 'modifyMessage' + }, + { + label: 'Trash Message', + name: 'trashMessage' + }, + { + label: 'Untrash Message', + name: 'untrashMessage' + }, + { + label: 'Delete Message', + name: 'deleteMessage' + } + ], + show: { + gmailType: ['messages'] + } + }, + // Label Actions + { + label: 'Label Actions', + name: 'labelActions', + type: 'multiOptions', + options: [ + { + label: 'List Labels', + name: 'listLabels' + }, + { + label: 'Get Label', + name: 'getLabel' + }, + { + label: 'Create Label', + name: 'createLabel' + }, + { + label: 'Update Label', + name: 'updateLabel' + }, + { + label: 'Delete Label', + name: 'deleteLabel' + } + ], + show: { + gmailType: ['labels'] + } + }, + // Thread Actions + { + label: 'Thread Actions', + name: 'threadActions', + type: 'multiOptions', + options: [ + { + label: 'List Threads', + name: 'listThreads' + }, + { + label: 'Get Thread', + name: 'getThread' + }, + { + label: 'Modify Thread', + name: 'modifyThread' + }, + { + label: 'Trash Thread', + name: 'trashThread' + }, + { + label: 'Untrash Thread', + name: 'untrashThread' + }, + { + label: 'Delete Thread', + name: 'deleteThread' + } + ], + show: { + gmailType: ['threads'] + } + }, + // DRAFT PARAMETERS + // List Drafts Parameters + { + label: 'Max Results', + name: 'draftMaxResults', + type: 'number', + description: 'Maximum number of drafts to return', + default: 100, + show: { + draftActions: ['listDrafts'] + }, + additionalParams: true, + optional: true + }, + // Create Draft Parameters + { + label: 'To', + name: 'draftTo', + type: 'string', + description: 'Recipient email address(es), comma-separated', + placeholder: 'user1@example.com,user2@example.com', + show: { + draftActions: ['createDraft'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Subject', + name: 'draftSubject', + type: 'string', + description: 'Email subject', + placeholder: 'Email Subject', + show: { + draftActions: ['createDraft'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Body', + name: 'draftBody', + type: 'string', + description: 'Email body content', + placeholder: 'Email content', + rows: 4, + show: { + draftActions: ['createDraft'] + }, + additionalParams: true, + optional: true + }, + { + label: 'CC', + name: 'draftCc', + type: 'string', + description: 'CC email address(es), comma-separated', + placeholder: 'cc1@example.com,cc2@example.com', + show: { + draftActions: ['createDraft'] + }, + additionalParams: true, + optional: true + }, + { + label: 'BCC', + name: 'draftBcc', + type: 'string', + description: 'BCC email address(es), comma-separated', + placeholder: 'bcc1@example.com,bcc2@example.com', + show: { + draftActions: ['createDraft'] + }, + additionalParams: true, + optional: true + }, + // Draft ID for Get/Update/Send/Delete + { + label: 'Draft ID', + name: 'draftId', + type: 'string', + description: 'ID of the draft', + show: { + draftActions: ['getDraft', 'updateDraft', 'sendDraft', 'deleteDraft'] + }, + additionalParams: true, + optional: true + }, + // Update Draft Parameters + { + label: 'To (Update)', + name: 'draftUpdateTo', + type: 'string', + description: 'Recipient email address(es), comma-separated', + placeholder: 'user1@example.com,user2@example.com', + show: { + draftActions: ['updateDraft'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Subject (Update)', + name: 'draftUpdateSubject', + type: 'string', + description: 'Email subject', + placeholder: 'Email Subject', + show: { + draftActions: ['updateDraft'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Body (Update)', + name: 'draftUpdateBody', + type: 'string', + description: 'Email body content', + placeholder: 'Email content', + rows: 4, + show: { + draftActions: ['updateDraft'] + }, + additionalParams: true, + optional: true + }, + // MESSAGE PARAMETERS + // List Messages Parameters + { + label: 'Max Results', + name: 'messageMaxResults', + type: 'number', + description: 'Maximum number of messages to return', + default: 100, + show: { + messageActions: ['listMessages'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Query', + name: 'messageQuery', + type: 'string', + description: 'Query string for filtering results (Gmail search syntax)', + placeholder: 'is:unread from:example@gmail.com', + show: { + messageActions: ['listMessages'] + }, + additionalParams: true, + optional: true + }, + // Send Message Parameters + { + label: 'To', + name: 'messageTo', + type: 'string', + description: 'Recipient email address(es), comma-separated', + placeholder: 'user1@example.com,user2@example.com', + show: { + messageActions: ['sendMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Subject', + name: 'messageSubject', + type: 'string', + description: 'Email subject', + placeholder: 'Email Subject', + show: { + messageActions: ['sendMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Body', + name: 'messageBody', + type: 'string', + description: 'Email body content', + placeholder: 'Email content', + rows: 4, + show: { + messageActions: ['sendMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'CC', + name: 'messageCc', + type: 'string', + description: 'CC email address(es), comma-separated', + placeholder: 'cc1@example.com,cc2@example.com', + show: { + messageActions: ['sendMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'BCC', + name: 'messageBcc', + type: 'string', + description: 'BCC email address(es), comma-separated', + placeholder: 'bcc1@example.com,bcc2@example.com', + show: { + messageActions: ['sendMessage'] + }, + additionalParams: true, + optional: true + }, + // Message ID for Get/Modify/Trash/Untrash/Delete + { + label: 'Message ID', + name: 'messageId', + type: 'string', + description: 'ID of the message', + show: { + messageActions: ['getMessage', 'modifyMessage', 'trashMessage', 'untrashMessage', 'deleteMessage'] + }, + additionalParams: true, + optional: true + }, + // Message Label Modification + { + label: 'Add Label IDs', + name: 'messageAddLabelIds', + type: 'string', + description: 'Comma-separated label IDs to add', + placeholder: 'INBOX,STARRED', + show: { + messageActions: ['modifyMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Remove Label IDs', + name: 'messageRemoveLabelIds', + type: 'string', + description: 'Comma-separated label IDs to remove', + placeholder: 'UNREAD,SPAM', + show: { + messageActions: ['modifyMessage'] + }, + additionalParams: true, + optional: true + }, + // LABEL PARAMETERS + // Create Label Parameters + { + label: 'Label Name', + name: 'labelName', + type: 'string', + description: 'Name of the label', + placeholder: 'Important', + show: { + labelActions: ['createLabel', 'updateLabel'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Label Color', + name: 'labelColor', + type: 'string', + description: 'Color of the label (hex color code)', + placeholder: '#ff0000', + show: { + labelActions: ['createLabel', 'updateLabel'] + }, + additionalParams: true, + optional: true + }, + // Label ID for Get/Update/Delete + { + label: 'Label ID', + name: 'labelId', + type: 'string', + description: 'ID of the label', + show: { + labelActions: ['getLabel', 'updateLabel', 'deleteLabel'] + }, + additionalParams: true, + optional: true + }, + // THREAD PARAMETERS + // List Threads Parameters + { + label: 'Max Results', + name: 'threadMaxResults', + type: 'number', + description: 'Maximum number of threads to return', + default: 100, + show: { + threadActions: ['listThreads'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Query', + name: 'threadQuery', + type: 'string', + description: 'Query string for filtering results (Gmail search syntax)', + placeholder: 'is:unread from:example@gmail.com', + show: { + threadActions: ['listThreads'] + }, + additionalParams: true, + optional: true + }, + // Thread ID for Get/Modify/Trash/Untrash/Delete + { + label: 'Thread ID', + name: 'threadId', + type: 'string', + description: 'ID of the thread', + show: { + threadActions: ['getThread', 'modifyThread', 'trashThread', 'untrashThread', 'deleteThread'] + }, + additionalParams: true, + optional: true + }, + // Thread Label Modification + { + label: 'Add Label IDs', + name: 'threadAddLabelIds', + type: 'string', + description: 'Comma-separated label IDs to add', + placeholder: 'INBOX,STARRED', + show: { + threadActions: ['modifyThread'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Remove Label IDs', + name: 'threadRemoveLabelIds', + type: 'string', + description: 'Comma-separated label IDs to remove', + placeholder: 'UNREAD,SPAM', + show: { + threadActions: ['modifyThread'] + }, + additionalParams: true, + optional: true + } + ] + } + + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + let credentialData = await getCredentialData(nodeData.credential ?? '', options) + credentialData = await refreshOAuth2Token(nodeData.credential ?? '', credentialData, options) + const accessToken = getCredentialParam('access_token', credentialData, nodeData) + + if (!accessToken) { + throw new Error('No access token found in credential') + } + + // Get all actions based on type + const gmailType = nodeData.inputs?.gmailType as string + let actions: string[] = [] + + if (gmailType === 'drafts') { + actions = convertMultiOptionsToStringArray(nodeData.inputs?.draftActions) + } else if (gmailType === 'messages') { + actions = convertMultiOptionsToStringArray(nodeData.inputs?.messageActions) + } else if (gmailType === 'labels') { + actions = convertMultiOptionsToStringArray(nodeData.inputs?.labelActions) + } else if (gmailType === 'threads') { + actions = convertMultiOptionsToStringArray(nodeData.inputs?.threadActions) + } + + // 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 + }) + + 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 00000000..14d242c8 --- /dev/null +++ b/packages/components/nodes/tools/Gmail/core.ts @@ -0,0 +1,1199 @@ +import { z } from 'zod' +import fetch from 'node-fetch' +import { DynamicStructuredTool } from '../OpenAPIToolkit/core' +import { TOOL_ARGS_PREFIX } from '../../../src/agents' + +export const desc = `Use this when you want to access Gmail API for managing drafts, messages, labels, and threads` + +export interface Headers { + [key: string]: string +} + +export interface Body { + [key: string]: any +} + +export interface RequestParameters { + headers?: Headers + body?: Body + url?: string + description?: string + name?: string + actions?: string[] + accessToken?: string + defaultParams?: any +} + +// Define schemas for different Gmail operations +const ListSchema = z.object({ + maxResults: z.number().optional().default(100).describe('Maximum number of results to return'), + query: z.string().optional().describe('Query string for filtering results (Gmail search syntax)') +}) + +const CreateDraftSchema = z.object({ + to: z.string().describe('Recipient email address(es), comma-separated'), + subject: z.string().optional().describe('Email subject'), + body: z.string().optional().describe('Email body content'), + cc: z.string().optional().describe('CC email address(es), comma-separated'), + bcc: z.string().optional().describe('BCC email address(es), comma-separated') +}) + +const SendMessageSchema = z.object({ + to: z.string().describe('Recipient email address(es), comma-separated'), + subject: z.string().optional().describe('Email subject'), + body: z.string().optional().describe('Email body content'), + cc: z.string().optional().describe('CC email address(es), comma-separated'), + bcc: z.string().optional().describe('BCC email address(es), comma-separated') +}) + +const GetByIdSchema = z.object({ + id: z.string().describe('ID of the resource') +}) + +const ModifySchema = z.object({ + id: z.string().describe('ID of the resource'), + addLabelIds: z.array(z.string()).optional().describe('Label IDs to add'), + removeLabelIds: z.array(z.string()).optional().describe('Label IDs to remove') +}) + +const CreateLabelSchema = z.object({ + labelName: z.string().describe('Name of the label'), + labelColor: z.string().optional().describe('Color of the label (hex color code)') +}) + +class BaseGmailTool extends DynamicStructuredTool { + protected accessToken: string = '' + + constructor(args: any) { + super(args) + this.accessToken = args.accessToken ?? '' + } + + async makeGmailRequest(url: string, method: string = 'GET', body?: any, params?: any): Promise { + const headers = { + Authorization: `Bearer ${this.accessToken}`, + 'Content-Type': 'application/json', + ...this.headers + } + + const response = await fetch(url, { + method, + headers, + body: body ? JSON.stringify(body) : undefined + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`Gmail API Error ${response.status}: ${response.statusText} - ${errorText}`) + } + + const data = await response.text() + return data + TOOL_ARGS_PREFIX + JSON.stringify(params) + } + + createMimeMessage(to: string, subject?: string, body?: string, cc?: string, bcc?: string): string { + let message = '' + + message += `To: ${to}\r\n` + if (cc) message += `Cc: ${cc}\r\n` + if (bcc) message += `Bcc: ${bcc}\r\n` + if (subject) message += `Subject: ${subject}\r\n` + message += `MIME-Version: 1.0\r\n` + message += `Content-Type: text/html; charset=utf-8\r\n` + message += `Content-Transfer-Encoding: base64\r\n\r\n` + + if (body) { + message += Buffer.from(body, 'utf-8').toString('base64') + } + + return Buffer.from(message).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') + } +} + +// Draft Tools +class ListDraftsTool extends BaseGmailTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'list_drafts', + description: 'List drafts in Gmail mailbox', + schema: ListSchema, + baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/drafts', + method: 'GET', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const queryParams = new URLSearchParams() + + if (params.maxResults) queryParams.append('maxResults', params.maxResults.toString()) + if (params.query) queryParams.append('q', params.query) + + const url = `https://gmail.googleapis.com/gmail/v1/users/me/drafts?${queryParams.toString()}` + + try { + const response = await this.makeGmailRequest(url, 'GET', undefined, params) + return response + } catch (error) { + return `Error listing drafts: ${error}` + } + } +} + +class CreateDraftTool extends BaseGmailTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'create_draft', + description: 'Create a new draft in Gmail', + schema: CreateDraftSchema, + baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/drafts', + method: 'POST', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const raw = this.createMimeMessage(params.to, params.subject, params.body, params.cc, params.bcc) + const draftData = { + message: { + raw: raw + } + } + + const url = 'https://gmail.googleapis.com/gmail/v1/users/me/drafts' + const response = await this.makeGmailRequest(url, 'POST', draftData, params) + return response + } catch (error) { + return `Error creating draft: ${error}` + } + } +} + +class GetDraftTool extends BaseGmailTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'get_draft', + description: 'Get a specific draft from Gmail', + schema: GetByIdSchema, + baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/drafts', + method: 'GET', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const draftId = params.id || params.draftId + + if (!draftId) { + return 'Error: Draft ID is required' + } + + try { + const url = `https://gmail.googleapis.com/gmail/v1/users/me/drafts/${draftId}` + const response = await this.makeGmailRequest(url, 'GET', undefined, params) + return response + } catch (error) { + return `Error getting draft: ${error}` + } + } +} + +class UpdateDraftTool extends BaseGmailTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'update_draft', + description: 'Update a specific draft in Gmail', + schema: CreateDraftSchema, + baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/drafts', + method: 'PUT', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const draftId = params.id || params.draftId + + if (!draftId) { + return 'Error: Draft ID is required' + } + + try { + const raw = this.createMimeMessage(params.to, params.subject, params.body, params.cc, params.bcc) + const draftData = { + message: { + raw: raw + } + } + + const url = `https://gmail.googleapis.com/gmail/v1/users/me/drafts/${draftId}` + const response = await this.makeGmailRequest(url, 'PUT', draftData, params) + return response + } catch (error) { + return `Error updating draft: ${error}` + } + } +} + +class SendDraftTool extends BaseGmailTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'send_draft', + description: 'Send a specific draft from Gmail', + schema: GetByIdSchema, + baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/drafts/send', + method: 'POST', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const draftId = params.id || params.draftId + + if (!draftId) { + return 'Error: Draft ID is required' + } + + try { + const url = 'https://gmail.googleapis.com/gmail/v1/users/me/drafts/send' + const response = await this.makeGmailRequest(url, 'POST', { id: draftId }, params) + return response + } catch (error) { + return `Error sending draft: ${error}` + } + } +} + +class DeleteDraftTool extends BaseGmailTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'delete_draft', + description: 'Delete a specific draft from Gmail', + schema: GetByIdSchema, + baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/drafts', + method: 'DELETE', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const draftId = params.id || params.draftId + + if (!draftId) { + return 'Error: Draft ID is required' + } + + try { + const url = `https://gmail.googleapis.com/gmail/v1/users/me/drafts/${draftId}` + await this.makeGmailRequest(url, 'DELETE', undefined, params) + return `Draft ${draftId} deleted successfully` + } catch (error) { + return `Error deleting draft: ${error}` + } + } +} + +// Message Tools +class ListMessagesTool extends BaseGmailTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'list_messages', + description: 'List messages in Gmail mailbox', + schema: ListSchema, + baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/messages', + method: 'GET', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const queryParams = new URLSearchParams() + + if (params.maxResults) queryParams.append('maxResults', params.maxResults.toString()) + if (params.query) queryParams.append('q', params.query) + + const url = `https://gmail.googleapis.com/gmail/v1/users/me/messages?${queryParams.toString()}` + + try { + const response = await this.makeGmailRequest(url, 'GET', undefined, params) + return response + } catch (error) { + return `Error listing messages: ${error}` + } + } +} + +class GetMessageTool extends BaseGmailTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'get_message', + description: 'Get a specific message from Gmail', + schema: GetByIdSchema, + baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/messages', + method: 'GET', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const messageId = params.id || params.messageId + + if (!messageId) { + return 'Error: Message ID is required' + } + + try { + const url = `https://gmail.googleapis.com/gmail/v1/users/me/messages/${messageId}` + const response = await this.makeGmailRequest(url, 'GET', undefined, params) + return response + } catch (error) { + return `Error getting message: ${error}` + } + } +} + +class SendMessageTool extends BaseGmailTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'send_message', + description: 'Send a new message via Gmail', + schema: SendMessageSchema, + baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/messages/send', + method: 'POST', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const raw = this.createMimeMessage(params.to, params.subject, params.body, params.cc, params.bcc) + const messageData = { + raw: raw + } + + const url = 'https://gmail.googleapis.com/gmail/v1/users/me/messages/send' + const response = await this.makeGmailRequest(url, 'POST', messageData, params) + return response + } catch (error) { + return `Error sending message: ${error}` + } + } +} + +class ModifyMessageTool extends BaseGmailTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'modify_message', + description: 'Modify labels on a message in Gmail', + schema: ModifySchema, + baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/messages', + method: 'POST', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const messageId = params.id || params.messageId + + if (!messageId) { + return 'Error: Message ID is required' + } + + try { + const modifyData: any = {} + if (params.addLabelIds && params.addLabelIds.length > 0) { + modifyData.addLabelIds = params.addLabelIds + } + if (params.removeLabelIds && params.removeLabelIds.length > 0) { + modifyData.removeLabelIds = params.removeLabelIds + } + + const url = `https://gmail.googleapis.com/gmail/v1/users/me/messages/${messageId}/modify` + const response = await this.makeGmailRequest(url, 'POST', modifyData, params) + return response + } catch (error) { + return `Error modifying message: ${error}` + } + } +} + +class TrashMessageTool extends BaseGmailTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'trash_message', + description: 'Move a message to trash in Gmail', + schema: GetByIdSchema, + baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/messages', + method: 'POST', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const messageId = params.id || params.messageId + + if (!messageId) { + return 'Error: Message ID is required' + } + + try { + const url = `https://gmail.googleapis.com/gmail/v1/users/me/messages/${messageId}/trash` + const response = await this.makeGmailRequest(url, 'POST', undefined, params) + return response + } catch (error) { + return `Error moving message to trash: ${error}` + } + } +} + +class UntrashMessageTool extends BaseGmailTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'untrash_message', + description: 'Remove a message from trash in Gmail', + schema: GetByIdSchema, + baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/messages', + method: 'POST', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const messageId = params.id || params.messageId + + if (!messageId) { + return 'Error: Message ID is required' + } + + try { + const url = `https://gmail.googleapis.com/gmail/v1/users/me/messages/${messageId}/untrash` + const response = await this.makeGmailRequest(url, 'POST', undefined, params) + return response + } catch (error) { + return `Error removing message from trash: ${error}` + } + } +} + +class DeleteMessageTool extends BaseGmailTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'delete_message', + description: 'Permanently delete a message from Gmail', + schema: GetByIdSchema, + baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/messages', + method: 'DELETE', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const messageId = params.id || params.messageId + + if (!messageId) { + return 'Error: Message ID is required' + } + + try { + const url = `https://gmail.googleapis.com/gmail/v1/users/me/messages/${messageId}` + await this.makeGmailRequest(url, 'DELETE', undefined, params) + return `Message ${messageId} deleted successfully` + } catch (error) { + return `Error deleting message: ${error}` + } + } +} + +// Label Tools +class ListLabelsTool extends BaseGmailTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'list_labels', + description: 'List labels in Gmail mailbox', + schema: z.object({}), + baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/labels', + method: 'GET', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(): Promise { + try { + const url = 'https://gmail.googleapis.com/gmail/v1/users/me/labels' + const response = await this.makeGmailRequest(url, 'GET', undefined, {}) + return response + } catch (error) { + return `Error listing labels: ${error}` + } + } +} + +class GetLabelTool extends BaseGmailTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'get_label', + description: 'Get a specific label from Gmail', + schema: GetByIdSchema, + baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/labels', + method: 'GET', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const labelId = params.id || params.labelId + + if (!labelId) { + return 'Error: Label ID is required' + } + + try { + const url = `https://gmail.googleapis.com/gmail/v1/users/me/labels/${labelId}` + const response = await this.makeGmailRequest(url, 'GET', undefined, params) + return response + } catch (error) { + return `Error getting label: ${error}` + } + } +} + +class CreateLabelTool extends BaseGmailTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'create_label', + description: 'Create a new label in Gmail', + schema: CreateLabelSchema, + baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/labels', + method: 'POST', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + if (!params.labelName) { + return 'Error: Label name is required' + } + + try { + const labelData: any = { + name: params.labelName, + labelListVisibility: 'labelShow', + messageListVisibility: 'show' + } + + if (params.labelColor) { + labelData.color = { + backgroundColor: params.labelColor + } + } + + const url = 'https://gmail.googleapis.com/gmail/v1/users/me/labels' + const response = await this.makeGmailRequest(url, 'POST', labelData, params) + return response + } catch (error) { + return `Error creating label: ${error}` + } + } +} + +class UpdateLabelTool extends BaseGmailTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'update_label', + description: 'Update a label in Gmail', + schema: CreateLabelSchema, + baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/labels', + method: 'PUT', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const labelId = params.labelId + + if (!labelId) { + return 'Error: Label ID is required' + } + + try { + const labelData: any = {} + if (params.labelName) { + labelData.name = params.labelName + } + if (params.labelColor) { + labelData.color = { + backgroundColor: params.labelColor + } + } + + const url = `https://gmail.googleapis.com/gmail/v1/users/me/labels/${labelId}` + const response = await this.makeGmailRequest(url, 'PUT', labelData, params) + return response + } catch (error) { + return `Error updating label: ${error}` + } + } +} + +class DeleteLabelTool extends BaseGmailTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'delete_label', + description: 'Delete a label from Gmail', + schema: GetByIdSchema, + baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/labels', + method: 'DELETE', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const labelId = params.id || params.labelId + + if (!labelId) { + return 'Error: Label ID is required' + } + + try { + const url = `https://gmail.googleapis.com/gmail/v1/users/me/labels/${labelId}` + await this.makeGmailRequest(url, 'DELETE', undefined, params) + return `Label ${labelId} deleted successfully` + } catch (error) { + return `Error deleting label: ${error}` + } + } +} + +// Thread Tools +class ListThreadsTool extends BaseGmailTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'list_threads', + description: 'List threads in Gmail mailbox', + schema: ListSchema, + baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/threads', + method: 'GET', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const queryParams = new URLSearchParams() + + if (params.maxResults) queryParams.append('maxResults', params.maxResults.toString()) + if (params.query) queryParams.append('q', params.query) + + const url = `https://gmail.googleapis.com/gmail/v1/users/me/threads?${queryParams.toString()}` + + try { + const response = await this.makeGmailRequest(url, 'GET', undefined, params) + return response + } catch (error) { + return `Error listing threads: ${error}` + } + } +} + +class GetThreadTool extends BaseGmailTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'get_thread', + description: 'Get a specific thread from Gmail', + schema: GetByIdSchema, + baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/threads', + method: 'GET', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const threadId = params.id || params.threadId + + if (!threadId) { + return 'Error: Thread ID is required' + } + + try { + const url = `https://gmail.googleapis.com/gmail/v1/users/me/threads/${threadId}` + const response = await this.makeGmailRequest(url, 'GET', undefined, params) + return response + } catch (error) { + return `Error getting thread: ${error}` + } + } +} + +class ModifyThreadTool extends BaseGmailTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'modify_thread', + description: 'Modify labels on a thread in Gmail', + schema: ModifySchema, + baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/threads', + method: 'POST', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const threadId = params.id || params.threadId + + if (!threadId) { + return 'Error: Thread ID is required' + } + + try { + const modifyData: any = {} + if (params.addLabelIds && params.addLabelIds.length > 0) { + modifyData.addLabelIds = params.addLabelIds + } + if (params.removeLabelIds && params.removeLabelIds.length > 0) { + modifyData.removeLabelIds = params.removeLabelIds + } + + const url = `https://gmail.googleapis.com/gmail/v1/users/me/threads/${threadId}/modify` + const response = await this.makeGmailRequest(url, 'POST', modifyData, params) + return response + } catch (error) { + return `Error modifying thread: ${error}` + } + } +} + +class TrashThreadTool extends BaseGmailTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'trash_thread', + description: 'Move a thread to trash in Gmail', + schema: GetByIdSchema, + baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/threads', + method: 'POST', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const threadId = params.id || params.threadId + + if (!threadId) { + return 'Error: Thread ID is required' + } + + try { + const url = `https://gmail.googleapis.com/gmail/v1/users/me/threads/${threadId}/trash` + const response = await this.makeGmailRequest(url, 'POST', undefined, params) + return response + } catch (error) { + return `Error moving thread to trash: ${error}` + } + } +} + +class UntrashThreadTool extends BaseGmailTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'untrash_thread', + description: 'Remove a thread from trash in Gmail', + schema: GetByIdSchema, + baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/threads', + method: 'POST', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const threadId = params.id || params.threadId + + if (!threadId) { + return 'Error: Thread ID is required' + } + + try { + const url = `https://gmail.googleapis.com/gmail/v1/users/me/threads/${threadId}/untrash` + const response = await this.makeGmailRequest(url, 'POST', undefined, params) + return response + } catch (error) { + return `Error removing thread from trash: ${error}` + } + } +} + +class DeleteThreadTool extends BaseGmailTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'delete_thread', + description: 'Permanently delete a thread from Gmail', + schema: GetByIdSchema, + baseUrl: 'https://gmail.googleapis.com/gmail/v1/users/me/threads', + method: 'DELETE', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const threadId = params.id || params.threadId + + if (!threadId) { + return 'Error: Thread ID is required' + } + + try { + const url = `https://gmail.googleapis.com/gmail/v1/users/me/threads/${threadId}` + await this.makeGmailRequest(url, 'DELETE', undefined, params) + return `Thread ${threadId} deleted successfully` + } catch (error) { + return `Error deleting thread: ${error}` + } + } +} + +export const createGmailTools = (args?: RequestParameters): DynamicStructuredTool[] => { + const tools: DynamicStructuredTool[] = [] + const actions = args?.actions || [] + const accessToken = args?.accessToken || '' + const defaultParams = args?.defaultParams || {} + + // Draft tools + if (actions.includes('listDrafts')) { + tools.push( + new ListDraftsTool({ + accessToken, + defaultParams: defaultParams.listDrafts + }) + ) + } + + if (actions.includes('createDraft')) { + tools.push( + new CreateDraftTool({ + accessToken, + defaultParams: defaultParams.createDraft + }) + ) + } + + if (actions.includes('getDraft')) { + tools.push( + new GetDraftTool({ + accessToken, + defaultParams: defaultParams.getDraft + }) + ) + } + + if (actions.includes('updateDraft')) { + tools.push( + new UpdateDraftTool({ + accessToken, + defaultParams: defaultParams.updateDraft + }) + ) + } + + if (actions.includes('sendDraft')) { + tools.push( + new SendDraftTool({ + accessToken, + defaultParams: defaultParams.sendDraft + }) + ) + } + + if (actions.includes('deleteDraft')) { + tools.push( + new DeleteDraftTool({ + accessToken, + defaultParams: defaultParams.deleteDraft + }) + ) + } + + // Message tools + if (actions.includes('listMessages')) { + tools.push( + new ListMessagesTool({ + accessToken, + defaultParams: defaultParams.listMessages + }) + ) + } + + if (actions.includes('getMessage')) { + tools.push( + new GetMessageTool({ + accessToken, + defaultParams: defaultParams.getMessage + }) + ) + } + + if (actions.includes('sendMessage')) { + tools.push( + new SendMessageTool({ + accessToken, + defaultParams: defaultParams.sendMessage + }) + ) + } + + if (actions.includes('modifyMessage')) { + tools.push( + new ModifyMessageTool({ + accessToken, + defaultParams: defaultParams.modifyMessage + }) + ) + } + + if (actions.includes('trashMessage')) { + tools.push( + new TrashMessageTool({ + accessToken, + defaultParams: defaultParams.trashMessage + }) + ) + } + + if (actions.includes('untrashMessage')) { + tools.push( + new UntrashMessageTool({ + accessToken, + defaultParams: defaultParams.untrashMessage + }) + ) + } + + if (actions.includes('deleteMessage')) { + tools.push( + new DeleteMessageTool({ + accessToken, + defaultParams: defaultParams.deleteMessage + }) + ) + } + + // Label tools + if (actions.includes('listLabels')) { + tools.push( + new ListLabelsTool({ + accessToken, + defaultParams: defaultParams.listLabels + }) + ) + } + + if (actions.includes('getLabel')) { + tools.push( + new GetLabelTool({ + accessToken, + defaultParams: defaultParams.getLabel + }) + ) + } + + if (actions.includes('createLabel')) { + tools.push( + new CreateLabelTool({ + accessToken, + defaultParams: defaultParams.createLabel + }) + ) + } + + if (actions.includes('updateLabel')) { + tools.push( + new UpdateLabelTool({ + accessToken, + defaultParams: defaultParams.updateLabel + }) + ) + } + + if (actions.includes('deleteLabel')) { + tools.push( + new DeleteLabelTool({ + accessToken, + defaultParams: defaultParams.deleteLabel + }) + ) + } + + // Thread tools + if (actions.includes('listThreads')) { + tools.push( + new ListThreadsTool({ + accessToken, + defaultParams: defaultParams.listThreads + }) + ) + } + + if (actions.includes('getThread')) { + tools.push( + new GetThreadTool({ + accessToken, + defaultParams: defaultParams.getThread + }) + ) + } + + if (actions.includes('modifyThread')) { + tools.push( + new ModifyThreadTool({ + accessToken, + defaultParams: defaultParams.modifyThread + }) + ) + } + + if (actions.includes('trashThread')) { + tools.push( + new TrashThreadTool({ + accessToken, + defaultParams: defaultParams.trashThread + }) + ) + } + + if (actions.includes('untrashThread')) { + tools.push( + new UntrashThreadTool({ + accessToken, + defaultParams: defaultParams.untrashThread + }) + ) + } + + if (actions.includes('deleteThread')) { + tools.push( + new DeleteThreadTool({ + accessToken, + defaultParams: defaultParams.deleteThread + }) + ) + } + + return tools +} diff --git a/packages/components/nodes/tools/Gmail/gmail.svg b/packages/components/nodes/tools/Gmail/gmail.svg new file mode 100644 index 00000000..3dceea45 --- /dev/null +++ b/packages/components/nodes/tools/Gmail/gmail.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/tools/GoogleCalendar/GoogleCalendar.ts b/packages/components/nodes/tools/GoogleCalendar/GoogleCalendar.ts new file mode 100644 index 00000000..36ad7d81 --- /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 00000000..1c89c9c8 --- /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 00000000..c5ba2d56 --- /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 00000000..48939dd6 --- /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 00000000..62377f5d --- /dev/null +++ b/packages/components/nodes/tools/GoogleDrive/core.ts @@ -0,0 +1,982 @@ +import { z } from 'zod' +import fetch from 'node-fetch' +import { DynamicStructuredTool } from '../OpenAPIToolkit/core' +import { TOOL_ARGS_PREFIX } from '../../../src/agents' + +export const desc = `Use this when you want to access Google Drive API for managing files and folders` + +export interface Headers { + [key: string]: string +} + +export interface Body { + [key: string]: any +} + +export interface RequestParameters { + headers?: Headers + body?: Body + url?: string + description?: string + name?: string + actions?: string[] + accessToken?: string + defaultParams?: any +} + +// Define schemas for different Google Drive operations + +// File Schemas +const ListFilesSchema = z.object({ + pageSize: z.number().optional().default(10).describe('Maximum number of files to return (1-1000)'), + pageToken: z.string().optional().describe('Token for next page of results'), + orderBy: z.string().optional().describe('Sort order (name, folder, createdTime, modifiedTime, etc.)'), + query: z.string().optional().describe('Search query (e.g., "name contains \'hello\'")'), + spaces: z.string().optional().default('drive').describe('Spaces to search (drive, appDataFolder, photos)'), + fields: z.string().optional().describe('Fields to include in response'), + includeItemsFromAllDrives: z.boolean().optional().describe('Include items from all drives'), + supportsAllDrives: z.boolean().optional().describe('Whether the requesting application supports both My Drives and shared drives') +}) + +const GetFileSchema = z.object({ + fileId: z.string().describe('File ID'), + fields: z.string().optional().describe('Fields to include in response'), + supportsAllDrives: z.boolean().optional().describe('Whether the requesting application supports both My Drives and shared drives'), + acknowledgeAbuse: z + .boolean() + .optional() + .describe('Whether the user is acknowledging the risk of downloading known malware or other abusive files') +}) + +const CreateFileSchema = z.object({ + name: z.string().describe('File name'), + parents: z.string().optional().describe('Comma-separated list of parent folder IDs'), + mimeType: z.string().optional().describe('MIME type of the file'), + description: z.string().optional().describe('File description'), + content: z.string().optional().describe('File content (for text files)'), + supportsAllDrives: z.boolean().optional().describe('Whether the requesting application supports both My Drives and shared drives') +}) + +const UpdateFileSchema = z.object({ + fileId: z.string().describe('File ID to update'), + name: z.string().optional().describe('New file name'), + description: z.string().optional().describe('New file description'), + starred: z.boolean().optional().describe('Whether the file is starred'), + trashed: z.boolean().optional().describe('Whether the file is trashed'), + parents: z.string().optional().describe('Comma-separated list of new parent folder IDs'), + supportsAllDrives: z.boolean().optional().describe('Whether the requesting application supports both My Drives and shared drives') +}) + +const DeleteFileSchema = z.object({ + fileId: z.string().describe('File ID to delete'), + supportsAllDrives: z.boolean().optional().describe('Whether the requesting application supports both My Drives and shared drives') +}) + +const CopyFileSchema = z.object({ + fileId: z.string().describe('File ID to copy'), + name: z.string().describe('Name for the copied file'), + parents: z.string().optional().describe('Comma-separated list of parent folder IDs for the copy'), + supportsAllDrives: z.boolean().optional().describe('Whether the requesting application supports both My Drives and shared drives') +}) + +const DownloadFileSchema = z.object({ + fileId: z.string().describe('File ID to download'), + acknowledgeAbuse: z + .boolean() + .optional() + .describe('Whether the user is acknowledging the risk of downloading known malware or other abusive files'), + supportsAllDrives: z.boolean().optional().describe('Whether the requesting application supports both My Drives and shared drives') +}) + +const CreateFolderSchema = z.object({ + name: z.string().describe('Folder name'), + parents: z.string().optional().describe('Comma-separated list of parent folder IDs'), + description: z.string().optional().describe('Folder description'), + supportsAllDrives: z.boolean().optional().describe('Whether the requesting application supports both My Drives and shared drives') +}) + +const SearchFilesSchema = z.object({ + query: z.string().describe('Search query using Google Drive search syntax'), + pageSize: z.number().optional().default(10).describe('Maximum number of files to return'), + orderBy: z.string().optional().describe('Sort order'), + includeItemsFromAllDrives: z.boolean().optional().describe('Include items from all drives'), + supportsAllDrives: z.boolean().optional().describe('Whether the requesting application supports both My Drives and shared drives') +}) + +const ShareFileSchema = z.object({ + fileId: z.string().describe('File ID to share'), + role: z.enum(['reader', 'writer', 'commenter', 'owner']).describe('Permission role'), + type: z.enum(['user', 'group', 'domain', 'anyone']).describe('Permission type'), + emailAddress: z.string().optional().describe('Email address (required for user/group types)'), + domain: z.string().optional().describe('Domain name (required for domain type)'), + allowFileDiscovery: z.boolean().optional().describe('Whether the file can be discovered by search'), + sendNotificationEmail: z.boolean().optional().default(true).describe('Whether to send notification emails'), + emailMessage: z.string().optional().describe('Custom message to include in notification email'), + supportsAllDrives: z.boolean().optional().describe('Whether the requesting application supports both My Drives and shared drives') +}) + +class BaseGoogleDriveTool extends DynamicStructuredTool { + protected accessToken: string = '' + + constructor(args: any) { + super(args) + this.accessToken = args.accessToken ?? '' + } + + async makeGoogleDriveRequest({ + endpoint, + method = 'GET', + body, + params + }: { + endpoint: string + method?: string + body?: any + params?: any + }): Promise { + const baseUrl = 'https://www.googleapis.com/drive/v3' + const url = `${baseUrl}/${endpoint}` + + const headers: { [key: string]: string } = { + Authorization: `Bearer ${this.accessToken}`, + Accept: 'application/json', + ...this.headers + } + + if (method !== 'GET' && body) { + headers['Content-Type'] = 'application/json' + } + + const response = await fetch(url, { + method, + headers, + body: body ? (typeof body === 'string' ? body : JSON.stringify(body)) : undefined + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`Google Drive API Error ${response.status}: ${response.statusText} - ${errorText}`) + } + + const data = await response.text() + return data + TOOL_ARGS_PREFIX + JSON.stringify(params) + } +} + +// File Tools +class ListFilesTool extends BaseGoogleDriveTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'list_files', + description: 'List files and folders from Google Drive', + schema: ListFilesSchema, + baseUrl: '', + method: 'GET', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const queryParams = new URLSearchParams() + + if (params.pageSize) queryParams.append('pageSize', params.pageSize.toString()) + if (params.pageToken) queryParams.append('pageToken', params.pageToken) + if (params.orderBy) queryParams.append('orderBy', params.orderBy) + if (params.query) queryParams.append('q', params.query) + if (params.spaces) queryParams.append('spaces', params.spaces) + if (params.fields) queryParams.append('fields', params.fields) + if (params.includeItemsFromAllDrives) queryParams.append('includeItemsFromAllDrives', params.includeItemsFromAllDrives.toString()) + if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString()) + + const endpoint = `files?${queryParams.toString()}` + + try { + const response = await this.makeGoogleDriveRequest({ endpoint, params }) + return response + } catch (error) { + return `Error listing files: ${error}` + } + } +} + +class GetFileTool extends BaseGoogleDriveTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'get_file', + description: 'Get file metadata from Google Drive', + schema: GetFileSchema, + baseUrl: '', + method: 'GET', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const queryParams = new URLSearchParams() + + if (params.fields) queryParams.append('fields', params.fields) + if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString()) + if (params.acknowledgeAbuse) queryParams.append('acknowledgeAbuse', params.acknowledgeAbuse.toString()) + + const endpoint = `files/${encodeURIComponent(params.fileId)}?${queryParams.toString()}` + + try { + const response = await this.makeGoogleDriveRequest({ endpoint, params }) + return response + } catch (error) { + return `Error getting file: ${error}` + } + } +} + +class CreateFileTool extends BaseGoogleDriveTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'create_file', + description: 'Create a new file in Google Drive', + schema: CreateFileSchema, + baseUrl: '', + method: 'POST', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + // Validate required parameters + if (!params.name) { + throw new Error('File name is required') + } + + const queryParams = new URLSearchParams() + if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString()) + + // Prepare metadata + const fileMetadata: any = { + name: params.name + } + + if (params.parents) { + // Validate parent folder IDs format + const parentIds = params.parents + .split(',') + .map((p: string) => p.trim()) + .filter((p: string) => p.length > 0) + if (parentIds.length > 0) { + fileMetadata.parents = parentIds + } + } + if (params.mimeType) fileMetadata.mimeType = params.mimeType + if (params.description) fileMetadata.description = params.description + + // Determine upload type based on content and metadata + if (!params.content) { + // Metadata-only upload (no file content) - standard endpoint + const endpoint = `files?${queryParams.toString()}` + const response = await this.makeGoogleDriveRequest({ + endpoint, + method: 'POST', + body: fileMetadata, + params + }) + return response + } else { + // Validate content + if (typeof params.content !== 'string') { + throw new Error('File content must be a string') + } + + // Check if we have metadata beyond just the name + const hasAdditionalMetadata = params.parents || params.description || params.mimeType + + if (!hasAdditionalMetadata) { + // Simple upload (uploadType=media) - only file content, basic metadata + return await this.performSimpleUpload(params, queryParams) + } else { + // Multipart upload (uploadType=multipart) - file content + metadata + return await this.performMultipartUpload(params, fileMetadata, queryParams) + } + } + } catch (error) { + return `Error creating file: ${error}` + } + } + + private async performSimpleUpload(params: any, queryParams: URLSearchParams): Promise { + // Simple upload: POST https://www.googleapis.com/upload/drive/v3/files?uploadType=media + queryParams.append('uploadType', 'media') + const url = `https://www.googleapis.com/upload/drive/v3/files?${queryParams.toString()}` + + const headers: { [key: string]: string } = { + Authorization: `Bearer ${this.accessToken}`, + 'Content-Type': params.mimeType || 'application/octet-stream', + 'Content-Length': Buffer.byteLength(params.content, 'utf8').toString() + } + + const response = await fetch(url, { + method: 'POST', + headers, + body: params.content + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`Google Drive API Error ${response.status}: ${response.statusText} - ${errorText}`) + } + + const data = await response.text() + return data + TOOL_ARGS_PREFIX + JSON.stringify(params) + } + + private async performMultipartUpload(params: any, fileMetadata: any, queryParams: URLSearchParams): Promise { + // Multipart upload: POST https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart + queryParams.append('uploadType', 'multipart') + const url = `https://www.googleapis.com/upload/drive/v3/files?${queryParams.toString()}` + + // Create multipart/related body according to RFC 2387 + const boundary = '-------314159265358979323846' + + // Build multipart body - RFC 2387 format + let body = `--${boundary}\r\n` + + // Part 1: Metadata (application/json; charset=UTF-8) + body += 'Content-Type: application/json; charset=UTF-8\r\n\r\n' + body += JSON.stringify(fileMetadata) + '\r\n' + + // Part 2: Media content (any MIME type) + body += `--${boundary}\r\n` + body += `Content-Type: ${params.mimeType || 'application/octet-stream'}\r\n\r\n` + body += params.content + '\r\n' + + // Close boundary + body += `--${boundary}--` + + const headers: { [key: string]: string } = { + Authorization: `Bearer ${this.accessToken}`, + 'Content-Type': `multipart/related; boundary="${boundary}"`, + 'Content-Length': Buffer.byteLength(body, 'utf8').toString() + } + + try { + const response = await fetch(url, { + method: 'POST', + headers, + body: body + }) + + if (!response.ok) { + const errorText = await response.text() + console.error('Multipart upload failed:', { + url, + headers: { ...headers, Authorization: '[REDACTED]' }, + metadata: fileMetadata, + contentLength: params.content?.length || 0, + error: errorText + }) + throw new Error(`Google Drive API Error ${response.status}: ${response.statusText} - ${errorText}`) + } + + const data = await response.text() + return data + TOOL_ARGS_PREFIX + JSON.stringify(params) + } catch (error) { + throw new Error(`Multipart upload failed: ${error}`) + } + } +} + +class UpdateFileTool extends BaseGoogleDriveTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'update_file', + description: 'Update file metadata in Google Drive', + schema: UpdateFileSchema, + baseUrl: '', + method: 'PATCH', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const updateData: any = {} + + if (params.name) updateData.name = params.name + if (params.description) updateData.description = params.description + if (params.starred !== undefined) updateData.starred = params.starred + if (params.trashed !== undefined) updateData.trashed = params.trashed + + const queryParams = new URLSearchParams() + if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString()) + + const endpoint = `files/${encodeURIComponent(params.fileId)}?${queryParams.toString()}` + + const response = await this.makeGoogleDriveRequest({ + endpoint, + method: 'PATCH', + body: updateData, + params + }) + return response + } catch (error) { + return `Error updating file: ${error}` + } + } +} + +class DeleteFileTool extends BaseGoogleDriveTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'delete_file', + description: 'Delete a file from Google Drive', + schema: DeleteFileSchema, + baseUrl: '', + method: 'DELETE', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const queryParams = new URLSearchParams() + if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString()) + + const endpoint = `files/${encodeURIComponent(params.fileId)}?${queryParams.toString()}` + + await this.makeGoogleDriveRequest({ + endpoint, + method: 'DELETE', + params + }) + return `File deleted successfully` + } catch (error) { + return `Error deleting file: ${error}` + } + } +} + +class CopyFileTool extends BaseGoogleDriveTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'copy_file', + description: 'Copy a file in Google Drive', + schema: CopyFileSchema, + baseUrl: '', + method: 'POST', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const copyData: any = { + name: params.name + } + + if (params.parents) { + copyData.parents = params.parents.split(',').map((p: string) => p.trim()) + } + + const queryParams = new URLSearchParams() + if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString()) + + const endpoint = `files/${encodeURIComponent(params.fileId)}/copy?${queryParams.toString()}` + + const response = await this.makeGoogleDriveRequest({ + endpoint, + method: 'POST', + body: copyData, + params + }) + return response + } catch (error) { + return `Error copying file: ${error}` + } + } +} + +class DownloadFileTool extends BaseGoogleDriveTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'download_file', + description: 'Download a file from Google Drive', + schema: DownloadFileSchema, + baseUrl: '', + method: 'GET', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const queryParams = new URLSearchParams() + queryParams.append('alt', 'media') + if (params.acknowledgeAbuse) queryParams.append('acknowledgeAbuse', params.acknowledgeAbuse.toString()) + if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString()) + + const endpoint = `files/${encodeURIComponent(params.fileId)}?${queryParams.toString()}` + + const response = await this.makeGoogleDriveRequest({ endpoint, params }) + return response + } catch (error) { + return `Error downloading file: ${error}` + } + } +} + +class CreateFolderTool extends BaseGoogleDriveTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'create_folder', + description: 'Create a new folder in Google Drive', + schema: CreateFolderSchema, + baseUrl: '', + method: 'POST', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const folderData: any = { + name: params.name, + mimeType: 'application/vnd.google-apps.folder' + } + + if (params.parents) { + folderData.parents = params.parents.split(',').map((p: string) => p.trim()) + } + if (params.description) folderData.description = params.description + + const queryParams = new URLSearchParams() + if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString()) + + const endpoint = `files?${queryParams.toString()}` + + const response = await this.makeGoogleDriveRequest({ + endpoint, + method: 'POST', + body: folderData, + params + }) + return response + } catch (error) { + return `Error creating folder: ${error}` + } + } +} + +class SearchFilesTool extends BaseGoogleDriveTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'search_files', + description: 'Search files in Google Drive', + schema: SearchFilesSchema, + baseUrl: '', + method: 'GET', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const queryParams = new URLSearchParams() + queryParams.append('q', params.query) + if (params.pageSize) queryParams.append('pageSize', params.pageSize.toString()) + if (params.orderBy) queryParams.append('orderBy', params.orderBy) + if (params.includeItemsFromAllDrives) + queryParams.append('includeItemsFromAllDrives', params.includeItemsFromAllDrives.toString()) + if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString()) + + const endpoint = `files?${queryParams.toString()}` + + const response = await this.makeGoogleDriveRequest({ endpoint, params }) + return response + } catch (error) { + return `Error searching files: ${error}` + } + } +} + +class ShareFileTool extends BaseGoogleDriveTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'share_file', + description: 'Share a file in Google Drive', + schema: ShareFileSchema, + baseUrl: '', + method: 'POST', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const permissionData: any = { + role: params.role, + type: params.type + } + + if (params.emailAddress) permissionData.emailAddress = params.emailAddress + if (params.domain) permissionData.domain = params.domain + if (params.allowFileDiscovery !== undefined) permissionData.allowFileDiscovery = params.allowFileDiscovery + + const queryParams = new URLSearchParams() + if (params.sendNotificationEmail !== undefined) + queryParams.append('sendNotificationEmail', params.sendNotificationEmail.toString()) + if (params.emailMessage) queryParams.append('emailMessage', params.emailMessage) + if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString()) + + const endpoint = `files/${encodeURIComponent(params.fileId)}/permissions?${queryParams.toString()}` + + const response = await this.makeGoogleDriveRequest({ + endpoint, + method: 'POST', + body: permissionData, + params + }) + return response + } catch (error) { + return `Error sharing file: ${error}` + } + } +} + +class ListFolderContentsTool extends BaseGoogleDriveTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'list_folder_contents', + description: 'List contents of a specific folder in Google Drive', + schema: z.object({ + folderId: z.string().describe('Folder ID to list contents from'), + pageSize: z.number().optional().default(10).describe('Maximum number of files to return'), + orderBy: z.string().optional().describe('Sort order'), + includeItemsFromAllDrives: z.boolean().optional().describe('Include items from all drives'), + supportsAllDrives: z + .boolean() + .optional() + .describe('Whether the requesting application supports both My Drives and shared drives') + }), + baseUrl: '', + method: 'GET', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const queryParams = new URLSearchParams() + queryParams.append('q', `'${params.folderId}' in parents`) + if (params.pageSize) queryParams.append('pageSize', params.pageSize.toString()) + if (params.orderBy) queryParams.append('orderBy', params.orderBy) + if (params.includeItemsFromAllDrives) + queryParams.append('includeItemsFromAllDrives', params.includeItemsFromAllDrives.toString()) + if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString()) + + const endpoint = `files?${queryParams.toString()}` + + const response = await this.makeGoogleDriveRequest({ endpoint, params }) + return response + } catch (error) { + return `Error listing folder contents: ${error}` + } + } +} + +class DeleteFolderTool extends BaseGoogleDriveTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'delete_folder', + description: 'Delete a folder from Google Drive', + schema: z.object({ + folderId: z.string().describe('Folder ID to delete'), + supportsAllDrives: z + .boolean() + .optional() + .describe('Whether the requesting application supports both My Drives and shared drives') + }), + baseUrl: '', + method: 'DELETE', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const queryParams = new URLSearchParams() + if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString()) + + const endpoint = `files/${encodeURIComponent(params.folderId)}?${queryParams.toString()}` + + await this.makeGoogleDriveRequest({ + endpoint, + method: 'DELETE', + params + }) + return `Folder deleted successfully` + } catch (error) { + return `Error deleting folder: ${error}` + } + } +} + +class GetPermissionsTool extends BaseGoogleDriveTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'get_permissions', + description: 'Get permissions for a file in Google Drive', + schema: z.object({ + fileId: z.string().describe('File ID to get permissions for'), + supportsAllDrives: z + .boolean() + .optional() + .describe('Whether the requesting application supports both My Drives and shared drives') + }), + baseUrl: '', + method: 'GET', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const queryParams = new URLSearchParams() + if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString()) + + const endpoint = `files/${encodeURIComponent(params.fileId)}/permissions?${queryParams.toString()}` + + const response = await this.makeGoogleDriveRequest({ endpoint, params }) + return response + } catch (error) { + return `Error getting permissions: ${error}` + } + } +} + +class RemovePermissionTool extends BaseGoogleDriveTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'remove_permission', + description: 'Remove a permission from a file in Google Drive', + schema: z.object({ + fileId: z.string().describe('File ID to remove permission from'), + permissionId: z.string().describe('Permission ID to remove'), + supportsAllDrives: z + .boolean() + .optional() + .describe('Whether the requesting application supports both My Drives and shared drives') + }), + baseUrl: '', + method: 'DELETE', + headers: {} + } + super({ + ...toolInput, + accessToken: args.accessToken + }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const queryParams = new URLSearchParams() + if (params.supportsAllDrives) queryParams.append('supportsAllDrives', params.supportsAllDrives.toString()) + + const endpoint = `files/${encodeURIComponent(params.fileId)}/permissions/${encodeURIComponent( + params.permissionId + )}?${queryParams.toString()}` + + await this.makeGoogleDriveRequest({ + endpoint, + method: 'DELETE', + params + }) + return `Permission removed successfully` + } catch (error) { + return `Error removing permission: ${error}` + } + } +} + +export const createGoogleDriveTools = (args?: RequestParameters): DynamicStructuredTool[] => { + const tools: DynamicStructuredTool[] = [] + const actions = args?.actions || [] + const accessToken = args?.accessToken || '' + const defaultParams = args?.defaultParams || {} + + if (actions.includes('listFiles')) { + tools.push(new ListFilesTool({ accessToken, defaultParams })) + } + + if (actions.includes('getFile')) { + tools.push(new GetFileTool({ accessToken, defaultParams })) + } + + if (actions.includes('createFile')) { + tools.push(new CreateFileTool({ accessToken, defaultParams })) + } + + if (actions.includes('updateFile')) { + tools.push(new UpdateFileTool({ accessToken, defaultParams })) + } + + if (actions.includes('deleteFile')) { + tools.push(new DeleteFileTool({ accessToken, defaultParams })) + } + + if (actions.includes('copyFile')) { + tools.push(new CopyFileTool({ accessToken, defaultParams })) + } + + if (actions.includes('downloadFile')) { + tools.push(new DownloadFileTool({ accessToken, defaultParams })) + } + + if (actions.includes('createFolder')) { + tools.push(new CreateFolderTool({ accessToken, defaultParams })) + } + + if (actions.includes('listFolderContents')) { + tools.push(new ListFolderContentsTool({ accessToken, defaultParams })) + } + + if (actions.includes('deleteFolder')) { + tools.push(new DeleteFolderTool({ accessToken, defaultParams })) + } + + if (actions.includes('searchFiles')) { + tools.push(new SearchFilesTool({ accessToken, defaultParams })) + } + + if (actions.includes('shareFile')) { + tools.push(new ShareFileTool({ accessToken, defaultParams })) + } + + if (actions.includes('getPermissions')) { + tools.push(new GetPermissionsTool({ accessToken, defaultParams })) + } + + if (actions.includes('removePermission')) { + tools.push(new RemovePermissionTool({ accessToken, defaultParams })) + } + + return tools +} diff --git a/packages/components/nodes/tools/GoogleDrive/google-drive.svg b/packages/components/nodes/tools/GoogleDrive/google-drive.svg new file mode 100644 index 00000000..03b2f212 --- /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 00000000..785e1098 --- /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 00000000..8b635984 --- /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 00000000..43af0ccf --- /dev/null +++ b/packages/components/nodes/tools/GoogleSheets/google-sheets.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/tools/Jira/Jira.ts b/packages/components/nodes/tools/Jira/Jira.ts new file mode 100644 index 00000000..9087b5cf --- /dev/null +++ b/packages/components/nodes/tools/Jira/Jira.ts @@ -0,0 +1,504 @@ +import { convertMultiOptionsToStringArray, getCredentialData, getCredentialParam } from '../../../src/utils' +import { createJiraTools } from './core' +import type { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' + +class Jira_Tools implements INode { + label: string + name: string + version: number + type: string + icon: string + category: string + description: string + baseClasses: string[] + credential: INodeParams + inputs: INodeParams[] + + constructor() { + this.label = 'Jira' + this.name = 'jiraTool' + this.version = 1.0 + this.type = 'Jira' + this.icon = 'jira.svg' + this.category = 'Tools' + this.description = 'Perform Jira operations for issues, comments, and users' + this.baseClasses = [this.type, 'Tool'] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['jiraApi'] + } + this.inputs = [ + { + label: 'Host', + name: 'jiraHost', + type: 'string', + placeholder: 'https://example.atlassian.net' + }, + { + label: 'Type', + name: 'jiraType', + type: 'options', + options: [ + { + label: 'Issues', + name: 'issues' + }, + { + label: 'Issue Comments', + name: 'comments' + }, + { + label: 'Users', + name: 'users' + } + ] + }, + // Issue Actions + { + label: 'Issue Actions', + name: 'issueActions', + type: 'multiOptions', + options: [ + { + label: 'List Issues', + name: 'listIssues' + }, + { + label: 'Create Issue', + name: 'createIssue' + }, + { + label: 'Get Issue', + name: 'getIssue' + }, + { + label: 'Update Issue', + name: 'updateIssue' + }, + { + label: 'Delete Issue', + name: 'deleteIssue' + }, + { + label: 'Assign Issue', + name: 'assignIssue' + }, + { + label: 'Transition Issue', + name: 'transitionIssue' + } + ], + show: { + jiraType: ['issues'] + } + }, + // Comment Actions + { + label: 'Comment Actions', + name: 'commentActions', + type: 'multiOptions', + options: [ + { + label: 'List Comments', + name: 'listComments' + }, + { + label: 'Create Comment', + name: 'createComment' + }, + { + label: 'Get Comment', + name: 'getComment' + }, + { + label: 'Update Comment', + name: 'updateComment' + }, + { + label: 'Delete Comment', + name: 'deleteComment' + } + ], + show: { + jiraType: ['comments'] + } + }, + // User Actions + { + label: 'User Actions', + name: 'userActions', + type: 'multiOptions', + options: [ + { + label: 'Search Users', + name: 'searchUsers' + }, + { + label: 'Get User', + name: 'getUser' + }, + { + label: 'Create User', + name: 'createUser' + }, + { + label: 'Update User', + name: 'updateUser' + }, + { + label: 'Delete User', + name: 'deleteUser' + } + ], + show: { + jiraType: ['users'] + } + }, + // ISSUE PARAMETERS + { + label: 'Project Key', + name: 'projectKey', + type: 'string', + placeholder: 'PROJ', + description: 'Project key for the issue', + show: { + issueActions: ['listIssues', 'createIssue'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Issue Type', + name: 'issueType', + type: 'string', + placeholder: 'Bug, Task, Story', + description: 'Type of issue to create', + show: { + issueActions: ['createIssue'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Summary', + name: 'issueSummary', + type: 'string', + description: 'Issue summary/title', + show: { + issueActions: ['createIssue', 'updateIssue'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Description', + name: 'issueDescription', + type: 'string', + description: 'Issue description', + show: { + issueActions: ['createIssue', 'updateIssue'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Priority', + name: 'issuePriority', + type: 'string', + placeholder: 'Highest, High, Medium, Low, Lowest', + description: 'Issue priority', + show: { + issueActions: ['createIssue', 'updateIssue'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Issue Key', + name: 'issueKey', + type: 'string', + placeholder: 'PROJ-123', + description: 'Issue key (e.g., PROJ-123)', + show: { + issueActions: ['getIssue', 'updateIssue', 'deleteIssue', 'assignIssue', 'transitionIssue'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Assignee Account ID', + name: 'assigneeAccountId', + type: 'string', + description: 'Account ID of the user to assign', + show: { + issueActions: ['assignIssue', 'createIssue', 'updateIssue'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Transition ID', + name: 'transitionId', + type: 'string', + description: 'ID of the transition to execute', + show: { + issueActions: ['transitionIssue'] + }, + additionalParams: true, + optional: true + }, + { + label: 'JQL Query', + name: 'jqlQuery', + type: 'string', + placeholder: 'project = PROJ AND status = "To Do"', + description: 'JQL query for filtering issues', + show: { + issueActions: ['listIssues'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Max Results', + name: 'issueMaxResults', + type: 'number', + default: 50, + description: 'Maximum number of issues to return', + show: { + issueActions: ['listIssues'] + }, + additionalParams: true, + optional: true + }, + // COMMENT PARAMETERS + { + label: 'Issue Key (for Comments)', + name: 'commentIssueKey', + type: 'string', + placeholder: 'PROJ-123', + description: 'Issue key for comment operations', + show: { + commentActions: ['listComments', 'createComment'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Comment Text', + name: 'commentText', + type: 'string', + description: 'Comment content', + show: { + commentActions: ['createComment', 'updateComment'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Comment ID', + name: 'commentId', + type: 'string', + description: 'ID of the comment', + show: { + commentActions: ['getComment', 'updateComment', 'deleteComment'] + }, + additionalParams: true, + optional: true + }, + // USER PARAMETERS + { + label: 'Search Query', + name: 'userQuery', + type: 'string', + placeholder: 'john.doe', + description: 'Query string for user search', + show: { + userActions: ['searchUsers'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Account ID', + name: 'userAccountId', + type: 'string', + description: 'User account ID', + show: { + userActions: ['getUser', 'updateUser', 'deleteUser'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Email Address', + name: 'userEmail', + type: 'string', + placeholder: 'user@example.com', + description: 'User email address', + show: { + userActions: ['createUser', 'updateUser'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Display Name', + name: 'userDisplayName', + type: 'string', + description: 'User display name', + show: { + userActions: ['createUser', 'updateUser'] + }, + additionalParams: true, + optional: true + }, + { + label: 'User Max Results', + name: 'userMaxResults', + type: 'number', + default: 50, + description: 'Maximum number of users to return', + show: { + userActions: ['searchUsers'] + }, + additionalParams: true, + optional: true + } + ] + } + + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + let credentialData = await getCredentialData(nodeData.credential ?? '', options) + const username = getCredentialParam('username', credentialData, nodeData) + const accessToken = getCredentialParam('accessToken', credentialData, nodeData) + const jiraHost = nodeData.inputs?.jiraHost as string + + if (!username) { + throw new Error('No username found in credential') + } + + if (!accessToken) { + throw new Error('No access token found in credential') + } + + if (!jiraHost) { + throw new Error('No Jira host provided') + } + + // Get all actions based on type + const jiraType = nodeData.inputs?.jiraType as string + let actions: string[] = [] + + if (jiraType === 'issues') { + actions = convertMultiOptionsToStringArray(nodeData.inputs?.issueActions) + } else if (jiraType === 'comments') { + actions = convertMultiOptionsToStringArray(nodeData.inputs?.commentActions) + } else if (jiraType === 'users') { + actions = convertMultiOptionsToStringArray(nodeData.inputs?.userActions) + } + + // 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 00000000..09d73e0c --- /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 00000000..4ace5cc8 --- /dev/null +++ b/packages/components/nodes/tools/Jira/jira.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/tools/MicrosoftOutlook/MicrosoftOutlook.ts b/packages/components/nodes/tools/MicrosoftOutlook/MicrosoftOutlook.ts new file mode 100644 index 00000000..94c41356 --- /dev/null +++ b/packages/components/nodes/tools/MicrosoftOutlook/MicrosoftOutlook.ts @@ -0,0 +1,971 @@ +import { convertMultiOptionsToStringArray, getCredentialData, getCredentialParam, refreshOAuth2Token } from '../../../src/utils' +import { createOutlookTools } from './core' +import type { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' + +class MicrosoftOutlook_Tools implements INode { + label: string + name: string + version: number + type: string + icon: string + category: string + description: string + baseClasses: string[] + credential: INodeParams + inputs: INodeParams[] + + constructor() { + this.label = 'Microsoft Outlook' + this.name = 'microsoftOutlook' + this.version = 1.0 + this.type = 'MicrosoftOutlook' + this.icon = 'outlook.svg' + this.category = 'Tools' + this.description = 'Perform Microsoft Outlook operations for calendars, events, and messages' + this.baseClasses = [this.type, 'Tool'] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['microsoftOutlookOAuth2'] + } + this.inputs = [ + { + label: 'Type', + name: 'outlookType', + type: 'options', + options: [ + { + label: 'Calendar', + name: 'calendar' + }, + { + label: 'Message', + name: 'message' + } + ] + }, + // Calendar Actions + { + label: 'Calendar Actions', + name: 'calendarActions', + type: 'multiOptions', + options: [ + { + label: 'List Calendars', + name: 'listCalendars' + }, + { + label: 'Get Calendar', + name: 'getCalendar' + }, + { + label: 'Create Calendar', + name: 'createCalendar' + }, + { + label: 'Update Calendar', + name: 'updateCalendar' + }, + { + label: 'Delete Calendar', + name: 'deleteCalendar' + }, + { + label: 'List Events', + name: 'listEvents' + }, + { + label: 'Get Event', + name: 'getEvent' + }, + { + label: 'Create Event', + name: 'createEvent' + }, + { + label: 'Update Event', + name: 'updateEvent' + }, + { + label: 'Delete Event', + name: 'deleteEvent' + } + ], + show: { + outlookType: ['calendar'] + } + }, + // Message Actions + { + label: 'Message Actions', + name: 'messageActions', + type: 'multiOptions', + options: [ + { + label: 'List Messages', + name: 'listMessages' + }, + { + label: 'Get Message', + name: 'getMessage' + }, + { + label: 'Create Draft Message', + name: 'createDraftMessage' + }, + { + label: 'Send Message', + name: 'sendMessage' + }, + { + label: 'Update Message', + name: 'updateMessage' + }, + { + label: 'Delete Message', + name: 'deleteMessage' + }, + { + label: 'Copy Message', + name: 'copyMessage' + }, + { + label: 'Move Message', + name: 'moveMessage' + }, + { + label: 'Reply to Message', + name: 'replyMessage' + }, + { + label: 'Forward Message', + name: 'forwardMessage' + } + ], + show: { + outlookType: ['message'] + } + }, + // CALENDAR PARAMETERS + // List Calendars Parameters + { + label: 'Max Results [List Calendars]', + name: 'maxResultsListCalendars', + type: 'number', + description: 'Maximum number of calendars to return', + default: 50, + show: { + outlookType: ['calendar'], + calendarActions: ['listCalendars'] + }, + additionalParams: true, + optional: true + }, + // Get Calendar Parameters + { + label: 'Calendar ID [Get Calendar]', + name: 'calendarIdGetCalendar', + type: 'string', + description: 'ID of the calendar to retrieve', + show: { + outlookType: ['calendar'], + calendarActions: ['getCalendar'] + }, + additionalParams: true, + optional: true + }, + // Create Calendar Parameters + { + label: 'Calendar Name [Create Calendar]', + name: 'calendarNameCreateCalendar', + type: 'string', + description: 'Name of the calendar', + placeholder: 'My New Calendar', + show: { + outlookType: ['calendar'], + calendarActions: ['createCalendar'] + }, + additionalParams: true, + optional: true + }, + // Update Calendar Parameters + { + label: 'Calendar ID [Update Calendar]', + name: 'calendarIdUpdateCalendar', + type: 'string', + description: 'ID of the calendar to update', + show: { + outlookType: ['calendar'], + calendarActions: ['updateCalendar'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Calendar Name [Update Calendar]', + name: 'calendarNameUpdateCalendar', + type: 'string', + description: 'New name of the calendar', + show: { + outlookType: ['calendar'], + calendarActions: ['updateCalendar'] + }, + additionalParams: true, + optional: true + }, + // Delete Calendar Parameters + { + label: 'Calendar ID [Delete Calendar]', + name: 'calendarIdDeleteCalendar', + type: 'string', + description: 'ID of the calendar to delete', + show: { + outlookType: ['calendar'], + calendarActions: ['deleteCalendar'] + }, + additionalParams: true, + optional: true + }, + // List Events Parameters + { + label: 'Calendar ID [List Events]', + name: 'calendarIdListEvents', + type: 'string', + description: 'ID of the calendar (leave empty for primary calendar)', + show: { + outlookType: ['calendar'], + calendarActions: ['listEvents'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Max Results [List Events]', + name: 'maxResultsListEvents', + type: 'number', + description: 'Maximum number of events to return', + default: 50, + show: { + outlookType: ['calendar'], + calendarActions: ['listEvents'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Start Date Time [List Events]', + name: 'startDateTimeListEvents', + type: 'string', + description: 'Start date time filter in ISO format', + placeholder: '2024-01-01T00:00:00Z', + show: { + outlookType: ['calendar'], + calendarActions: ['listEvents'] + }, + additionalParams: true, + optional: true + }, + { + label: 'End Date Time [List Events]', + name: 'endDateTimeListEvents', + type: 'string', + description: 'End date time filter in ISO format', + placeholder: '2024-12-31T23:59:59Z', + show: { + outlookType: ['calendar'], + calendarActions: ['listEvents'] + }, + additionalParams: true, + optional: true + }, + // Get Event Parameters + { + label: 'Event ID [Get Event]', + name: 'eventIdGetEvent', + type: 'string', + description: 'ID of the event to retrieve', + show: { + outlookType: ['calendar'], + calendarActions: ['getEvent'] + }, + additionalParams: true, + optional: true + }, + // Create Event Parameters + { + label: 'Subject [Create Event]', + name: 'subjectCreateEvent', + type: 'string', + description: 'Subject/title of the event', + placeholder: 'Meeting Title', + show: { + outlookType: ['calendar'], + calendarActions: ['createEvent'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Body [Create Event]', + name: 'bodyCreateEvent', + type: 'string', + description: 'Body/description of the event', + placeholder: 'Meeting description', + rows: 3, + show: { + outlookType: ['calendar'], + calendarActions: ['createEvent'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Start Date Time [Create Event]', + name: 'startDateTimeCreateEvent', + type: 'string', + description: 'Start date and time in ISO format', + placeholder: '2024-01-15T10:00:00', + show: { + outlookType: ['calendar'], + calendarActions: ['createEvent'] + }, + additionalParams: true, + optional: true + }, + { + label: 'End Date Time [Create Event]', + name: 'endDateTimeCreateEvent', + type: 'string', + description: 'End date and time in ISO format', + placeholder: '2024-01-15T11:00:00', + show: { + outlookType: ['calendar'], + calendarActions: ['createEvent'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Time Zone [Create Event]', + name: 'timeZoneCreateEvent', + type: 'string', + description: 'Time zone for the event', + placeholder: 'UTC', + default: 'UTC', + show: { + outlookType: ['calendar'], + calendarActions: ['createEvent'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Location [Create Event]', + name: 'locationCreateEvent', + type: 'string', + description: 'Location of the event', + placeholder: 'Conference Room A', + show: { + outlookType: ['calendar'], + calendarActions: ['createEvent'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Attendees [Create Event]', + name: 'attendeesCreateEvent', + type: 'string', + description: 'Comma-separated list of attendee email addresses', + placeholder: 'user1@example.com,user2@example.com', + show: { + outlookType: ['calendar'], + calendarActions: ['createEvent'] + }, + additionalParams: true, + optional: true + }, + // Update Event Parameters + { + label: 'Event ID [Update Event]', + name: 'eventIdUpdateEvent', + type: 'string', + description: 'ID of the event to update', + show: { + outlookType: ['calendar'], + calendarActions: ['updateEvent'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Subject [Update Event]', + name: 'subjectUpdateEvent', + type: 'string', + description: 'New subject/title of the event', + show: { + outlookType: ['calendar'], + calendarActions: ['updateEvent'] + }, + additionalParams: true, + optional: true + }, + // Delete Event Parameters + { + label: 'Event ID [Delete Event]', + name: 'eventIdDeleteEvent', + type: 'string', + description: 'ID of the event to delete', + show: { + outlookType: ['calendar'], + calendarActions: ['deleteEvent'] + }, + additionalParams: true, + optional: true + }, + // MESSAGE PARAMETERS + // List Messages Parameters + { + label: 'Max Results [List Messages]', + name: 'maxResultsListMessages', + type: 'number', + description: 'Maximum number of messages to return', + default: 50, + show: { + outlookType: ['message'], + messageActions: ['listMessages'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Filter [List Messages]', + name: 'filterListMessages', + type: 'string', + description: 'Filter query (e.g., "isRead eq false")', + show: { + outlookType: ['message'], + messageActions: ['listMessages'] + }, + additionalParams: true, + optional: true + }, + // Get Message Parameters + { + label: 'Message ID [Get Message]', + name: 'messageIdGetMessage', + type: 'string', + description: 'ID of the message to retrieve', + show: { + outlookType: ['message'], + messageActions: ['getMessage'] + }, + additionalParams: true, + optional: true + }, + // Create Draft Message Parameters + { + label: 'To [Create Draft Message]', + name: 'toCreateDraftMessage', + type: 'string', + description: 'Recipient email address(es), comma-separated', + placeholder: 'user@example.com', + show: { + outlookType: ['message'], + messageActions: ['createDraftMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Subject [Create Draft Message]', + name: 'subjectCreateDraftMessage', + type: 'string', + description: 'Subject of the message', + placeholder: 'Email Subject', + show: { + outlookType: ['message'], + messageActions: ['createDraftMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Body [Create Draft Message]', + name: 'bodyCreateDraftMessage', + type: 'string', + description: 'Body content of the message', + placeholder: 'Email body content', + rows: 4, + show: { + outlookType: ['message'], + messageActions: ['createDraftMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'CC [Create Draft Message]', + name: 'ccCreateDraftMessage', + type: 'string', + description: 'CC email address(es), comma-separated', + placeholder: 'cc@example.com', + show: { + outlookType: ['message'], + messageActions: ['createDraftMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'BCC [Create Draft Message]', + name: 'bccCreateDraftMessage', + type: 'string', + description: 'BCC email address(es), comma-separated', + placeholder: 'bcc@example.com', + show: { + outlookType: ['message'], + messageActions: ['createDraftMessage'] + }, + additionalParams: true, + optional: true + }, + // Send Message Parameters + { + label: 'To [Send Message]', + name: 'toSendMessage', + type: 'string', + description: 'Recipient email address(es), comma-separated', + placeholder: 'user@example.com', + show: { + outlookType: ['message'], + messageActions: ['sendMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Subject [Send Message]', + name: 'subjectSendMessage', + type: 'string', + description: 'Subject of the message', + placeholder: 'Email Subject', + show: { + outlookType: ['message'], + messageActions: ['sendMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Body [Send Message]', + name: 'bodySendMessage', + type: 'string', + description: 'Body content of the message', + placeholder: 'Email body content', + rows: 4, + show: { + outlookType: ['message'], + messageActions: ['sendMessage'] + }, + additionalParams: true, + optional: true + }, + // Update Message Parameters + { + label: 'Message ID [Update Message]', + name: 'messageIdUpdateMessage', + type: 'string', + description: 'ID of the message to update', + show: { + outlookType: ['message'], + messageActions: ['updateMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Is Read [Update Message]', + name: 'isReadUpdateMessage', + type: 'boolean', + description: 'Mark message as read/unread', + show: { + outlookType: ['message'], + messageActions: ['updateMessage'] + }, + additionalParams: true, + optional: true + }, + // Delete Message Parameters + { + label: 'Message ID [Delete Message]', + name: 'messageIdDeleteMessage', + type: 'string', + description: 'ID of the message to delete', + show: { + outlookType: ['message'], + messageActions: ['deleteMessage'] + }, + additionalParams: true, + optional: true + }, + // Copy Message Parameters + { + label: 'Message ID [Copy Message]', + name: 'messageIdCopyMessage', + type: 'string', + description: 'ID of the message to copy', + show: { + outlookType: ['message'], + messageActions: ['copyMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Destination Folder ID [Copy Message]', + name: 'destinationFolderIdCopyMessage', + type: 'string', + description: 'ID of the destination folder', + show: { + outlookType: ['message'], + messageActions: ['copyMessage'] + }, + additionalParams: true, + optional: true + }, + // Move Message Parameters + { + label: 'Message ID [Move Message]', + name: 'messageIdMoveMessage', + type: 'string', + description: 'ID of the message to move', + show: { + outlookType: ['message'], + messageActions: ['moveMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Destination Folder ID [Move Message]', + name: 'destinationFolderIdMoveMessage', + type: 'string', + description: 'ID of the destination folder', + show: { + outlookType: ['message'], + messageActions: ['moveMessage'] + }, + additionalParams: true, + optional: true + }, + // Reply Message Parameters + { + label: 'Message ID [Reply Message]', + name: 'messageIdReplyMessage', + type: 'string', + description: 'ID of the message to reply to', + show: { + outlookType: ['message'], + messageActions: ['replyMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Reply Body [Reply Message]', + name: 'replyBodyReplyMessage', + type: 'string', + description: 'Reply message body', + rows: 4, + show: { + outlookType: ['message'], + messageActions: ['replyMessage'] + }, + additionalParams: true, + optional: true + }, + // Forward Message Parameters + { + label: 'Message ID [Forward Message]', + name: 'messageIdForwardMessage', + type: 'string', + description: 'ID of the message to forward', + show: { + outlookType: ['message'], + messageActions: ['forwardMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Forward To [Forward Message]', + name: 'forwardToForwardMessage', + type: 'string', + description: 'Email address(es) to forward to, comma-separated', + show: { + outlookType: ['message'], + messageActions: ['forwardMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Forward Comment [Forward Message]', + name: 'forwardCommentForwardMessage', + type: 'string', + description: 'Additional comment to include with forward', + rows: 2, + show: { + outlookType: ['message'], + messageActions: ['forwardMessage'] + }, + additionalParams: true, + optional: true + } + ] + } + + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + const outlookType = nodeData.inputs?.outlookType as string + const calendarActions = nodeData.inputs?.calendarActions as string + const messageActions = nodeData.inputs?.messageActions as string + + let credentialData = await getCredentialData(nodeData.credential ?? '', options) + credentialData = await refreshOAuth2Token(nodeData.credential ?? '', credentialData, options) + const accessToken = getCredentialParam('access_token', credentialData, nodeData) + + if (!accessToken) { + throw new Error('No access token found in credential') + } + + let actions: string[] = [] + if (outlookType === 'calendar') { + actions = convertMultiOptionsToStringArray(calendarActions) + } else if (outlookType === 'message') { + actions = convertMultiOptionsToStringArray(messageActions) + } + + // 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 00000000..ce6fe8ba --- /dev/null +++ b/packages/components/nodes/tools/MicrosoftOutlook/core.ts @@ -0,0 +1,1029 @@ +import { z } from 'zod' +import fetch from 'node-fetch' +import { DynamicStructuredTool } from '../OpenAPIToolkit/core' +import { TOOL_ARGS_PREFIX } from '../../../src/agents' + +export const desc = `Use this when you want to access Microsoft Outlook API for managing calendars, events, and messages` + +export interface Headers { + [key: string]: string +} + +export interface Body { + [key: string]: any +} + +export interface RequestParameters { + headers?: Headers + body?: Body + url?: string + description?: string + name?: string + actions?: string[] + accessToken?: string + defaultParams?: any +} + +// Define schemas for different Outlook operations + +// Calendar Schemas +const ListCalendarsSchema = z.object({ + maxResults: z.number().optional().default(50).describe('Maximum number of calendars to return') +}) + +const GetCalendarSchema = z.object({ + calendarId: z.string().describe('ID of the calendar to retrieve') +}) + +const CreateCalendarSchema = z.object({ + calendarName: z.string().describe('Name of the calendar') +}) + +const UpdateCalendarSchema = z.object({ + calendarId: z.string().describe('ID of the calendar to update'), + calendarName: z.string().describe('New name of the calendar') +}) + +const DeleteCalendarSchema = z.object({ + calendarId: z.string().describe('ID of the calendar to delete') +}) + +const ListEventsSchema = z.object({ + calendarId: z.string().optional().describe('ID of the calendar (empty for primary calendar)'), + maxResults: z.number().optional().default(50).describe('Maximum number of events to return'), + startDateTime: z.string().optional().describe('Start date time filter in ISO format'), + endDateTime: z.string().optional().describe('End date time filter in ISO format') +}) + +const GetEventSchema = z.object({ + eventId: z.string().describe('ID of the event to retrieve') +}) + +const CreateEventSchema = z.object({ + subject: z.string().describe('Subject/title of the event'), + body: z.string().optional().describe('Body/description of the event'), + startDateTime: z.string().describe('Start date and time in ISO format'), + endDateTime: z.string().describe('End date and time in ISO format'), + timeZone: z.string().optional().default('UTC').describe('Time zone for the event'), + location: z.string().optional().describe('Location of the event'), + attendees: z.string().optional().describe('Comma-separated list of attendee email addresses') +}) + +const UpdateEventSchema = z.object({ + eventId: z.string().describe('ID of the event to update'), + subject: z.string().optional().describe('New subject/title of the event') +}) + +const DeleteEventSchema = z.object({ + eventId: z.string().describe('ID of the event to delete') +}) + +// Message Schemas +const ListMessagesSchema = z.object({ + maxResults: z.number().optional().default(50).describe('Maximum number of messages to return'), + filter: z.string().optional().describe('Filter query (e.g., "isRead eq false")') +}) + +const GetMessageSchema = z.object({ + messageId: z.string().describe('ID of the message to retrieve') +}) + +const CreateDraftMessageSchema = z.object({ + to: z.string().describe('Recipient email address(es), comma-separated'), + subject: z.string().optional().describe('Subject of the message'), + body: z.string().optional().describe('Body content of the message'), + cc: z.string().optional().describe('CC email address(es), comma-separated'), + bcc: z.string().optional().describe('BCC email address(es), comma-separated') +}) + +const SendMessageSchema = z.object({ + to: z.string().describe('Recipient email address(es), comma-separated'), + subject: z.string().optional().describe('Subject of the message'), + body: z.string().optional().describe('Body content of the message') +}) + +const UpdateMessageSchema = z.object({ + messageId: z.string().describe('ID of the message to update'), + isRead: z.boolean().optional().describe('Mark message as read/unread') +}) + +const DeleteMessageSchema = z.object({ + messageId: z.string().describe('ID of the message to delete') +}) + +const CopyMessageSchema = z.object({ + messageId: z.string().describe('ID of the message to copy'), + destinationFolderId: z.string().describe('ID of the destination folder') +}) + +const MoveMessageSchema = z.object({ + messageId: z.string().describe('ID of the message to move'), + destinationFolderId: z.string().describe('ID of the destination folder') +}) + +const ReplyMessageSchema = z.object({ + messageId: z.string().describe('ID of the message to reply to'), + replyBody: z.string().describe('Reply message body') +}) + +const ForwardMessageSchema = z.object({ + messageId: z.string().describe('ID of the message to forward'), + forwardTo: z.string().describe('Email address(es) to forward to, comma-separated'), + forwardComment: z.string().optional().describe('Additional comment to include with forward') +}) + +class BaseOutlookTool extends DynamicStructuredTool { + protected accessToken: string = '' + + constructor(args: any) { + super(args) + this.accessToken = args.accessToken ?? '' + } + + async makeGraphRequest(url: string, method: string = 'GET', body?: any, params?: any): Promise { + const headers = { + Authorization: `Bearer ${this.accessToken}`, + 'Content-Type': 'application/json', + ...this.headers + } + + const response = await fetch(url, { + method, + headers, + body: body ? JSON.stringify(body) : undefined + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`Graph API Error ${response.status}: ${response.statusText} - ${errorText}`) + } + + const data = await response.text() + return data + TOOL_ARGS_PREFIX + JSON.stringify(params) + } + + parseEmailAddresses(emailString: string) { + return emailString.split(',').map((email) => ({ + emailAddress: { + address: email.trim(), + name: email.trim() + } + })) + } +} + +// Calendar Tools +class ListCalendarsTool extends BaseOutlookTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'list_calendars', + description: 'List calendars in Microsoft Outlook', + schema: ListCalendarsSchema, + baseUrl: 'https://graph.microsoft.com/v1.0/me/calendars', + method: 'GET', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const queryParams = new URLSearchParams() + + if (params.maxResults) queryParams.append('$top', params.maxResults.toString()) + + const url = `https://graph.microsoft.com/v1.0/me/calendars?${queryParams.toString()}` + + try { + const response = await this.makeGraphRequest(url, 'GET', undefined, params) + return response + } catch (error) { + return `Error listing calendars: ${error}` + } + } +} + +class GetCalendarTool extends BaseOutlookTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'get_calendar', + description: 'Get a specific calendar by ID from Microsoft Outlook', + schema: GetCalendarSchema, + baseUrl: 'https://graph.microsoft.com/v1.0/me/calendars', + method: 'GET', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const url = `https://graph.microsoft.com/v1.0/me/calendars/${params.calendarId}` + + try { + const response = await this.makeGraphRequest(url, 'GET', undefined, params) + return response + } catch (error) { + return `Error getting calendar: ${error}` + } + } +} + +class CreateCalendarTool extends BaseOutlookTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'create_calendar', + description: 'Create a new calendar in Microsoft Outlook', + schema: CreateCalendarSchema, + baseUrl: 'https://graph.microsoft.com/v1.0/me/calendars', + method: 'POST', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const calendarData = { + name: params.calendarName + } + + const url = 'https://graph.microsoft.com/v1.0/me/calendars' + const response = await this.makeGraphRequest(url, 'POST', calendarData, params) + return response + } catch (error) { + return `Error creating calendar: ${error}` + } + } +} + +class UpdateCalendarTool extends BaseOutlookTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'update_calendar', + description: 'Update a calendar in Microsoft Outlook', + schema: UpdateCalendarSchema, + baseUrl: 'https://graph.microsoft.com/v1.0/me/calendars', + method: 'PATCH', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const calendarData = { + name: params.calendarName + } + + const url = `https://graph.microsoft.com/v1.0/me/calendars/${params.calendarId}` + const response = await this.makeGraphRequest(url, 'PATCH', calendarData, params) + return response + } catch (error) { + return `Error updating calendar: ${error}` + } + } +} + +class DeleteCalendarTool extends BaseOutlookTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'delete_calendar', + description: 'Delete a calendar from Microsoft Outlook', + schema: DeleteCalendarSchema, + baseUrl: 'https://graph.microsoft.com/v1.0/me/calendars', + method: 'DELETE', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const url = `https://graph.microsoft.com/v1.0/me/calendars/${params.calendarId}` + + try { + await this.makeGraphRequest(url, 'DELETE', undefined, params) + return `Calendar ${params.calendarId} deleted successfully` + } catch (error) { + return `Error deleting calendar: ${error}` + } + } +} + +class ListEventsTool extends BaseOutlookTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'list_events', + description: 'List events from Microsoft Outlook calendar', + schema: ListEventsSchema, + baseUrl: 'https://graph.microsoft.com/v1.0/me/events', + method: 'GET', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const queryParams = new URLSearchParams() + + if (params.maxResults) queryParams.append('$top', params.maxResults.toString()) + if (params.startDateTime) queryParams.append('$filter', `start/dateTime ge '${params.startDateTime}'`) + if (params.endDateTime) { + const existingFilter = queryParams.get('$filter') + const endFilter = `end/dateTime le '${params.endDateTime}'` + if (existingFilter) { + queryParams.set('$filter', `${existingFilter} and ${endFilter}`) + } else { + queryParams.append('$filter', endFilter) + } + } + + const baseUrl = params.calendarId + ? `https://graph.microsoft.com/v1.0/me/calendars/${params.calendarId}/events` + : 'https://graph.microsoft.com/v1.0/me/events' + + const url = `${baseUrl}?${queryParams.toString()}` + + try { + const response = await this.makeGraphRequest(url, 'GET', undefined, params) + return response + } catch (error) { + return `Error listing events: ${error}` + } + } +} + +class GetEventTool extends BaseOutlookTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'get_event', + description: 'Get a specific event by ID from Microsoft Outlook', + schema: GetEventSchema, + baseUrl: 'https://graph.microsoft.com/v1.0/me/events', + method: 'GET', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const url = `https://graph.microsoft.com/v1.0/me/events/${params.eventId}` + + try { + const response = await this.makeGraphRequest(url, 'GET', undefined, params) + return response + } catch (error) { + return `Error getting event: ${error}` + } + } +} + +class CreateEventTool extends BaseOutlookTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'create_event', + description: 'Create a new event in Microsoft Outlook calendar', + schema: CreateEventSchema, + baseUrl: 'https://graph.microsoft.com/v1.0/me/events', + method: 'POST', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const eventData = { + subject: params.subject, + body: { + contentType: 'HTML', + content: params.body || '' + }, + start: { + dateTime: params.startDateTime, + timeZone: params.timeZone || 'UTC' + }, + end: { + dateTime: params.endDateTime, + timeZone: params.timeZone || 'UTC' + }, + location: params.location + ? { + displayName: params.location + } + : undefined, + attendees: params.attendees ? this.parseEmailAddresses(params.attendees) : [] + } + + const url = 'https://graph.microsoft.com/v1.0/me/events' + const response = await this.makeGraphRequest(url, 'POST', eventData, params) + return response + } catch (error) { + return `Error creating event: ${error}` + } + } +} + +class UpdateEventTool extends BaseOutlookTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'update_event', + description: 'Update an event in Microsoft Outlook calendar', + schema: UpdateEventSchema, + baseUrl: 'https://graph.microsoft.com/v1.0/me/events', + method: 'PATCH', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const eventData: any = {} + if (params.subject) eventData.subject = params.subject + + const url = `https://graph.microsoft.com/v1.0/me/events/${params.eventId}` + const response = await this.makeGraphRequest(url, 'PATCH', eventData, params) + return response + } catch (error) { + return `Error updating event: ${error}` + } + } +} + +class DeleteEventTool extends BaseOutlookTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'delete_event', + description: 'Delete an event from Microsoft Outlook calendar', + schema: DeleteEventSchema, + baseUrl: 'https://graph.microsoft.com/v1.0/me/events', + method: 'DELETE', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const url = `https://graph.microsoft.com/v1.0/me/events/${params.eventId}` + + try { + await this.makeGraphRequest(url, 'DELETE', undefined, params) + return `Event ${params.eventId} deleted successfully` + } catch (error) { + return `Error deleting event: ${error}` + } + } +} + +// Message Tools +class ListMessagesTool extends BaseOutlookTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'list_messages', + description: 'List messages from Microsoft Outlook mailbox', + schema: ListMessagesSchema, + baseUrl: 'https://graph.microsoft.com/v1.0/me/messages', + method: 'GET', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const queryParams = new URLSearchParams() + + if (params.maxResults) queryParams.append('$top', params.maxResults.toString()) + if (params.filter) queryParams.append('$filter', params.filter) + + const url = `https://graph.microsoft.com/v1.0/me/messages?${queryParams.toString()}` + + try { + const response = await this.makeGraphRequest(url, 'GET', undefined, params) + return response + } catch (error) { + return `Error listing messages: ${error}` + } + } +} + +class GetMessageTool extends BaseOutlookTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'get_message', + description: 'Get a specific message by ID from Microsoft Outlook', + schema: GetMessageSchema, + baseUrl: 'https://graph.microsoft.com/v1.0/me/messages', + method: 'GET', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const url = `https://graph.microsoft.com/v1.0/me/messages/${params.messageId}` + + try { + const response = await this.makeGraphRequest(url, 'GET', undefined, params) + return response + } catch (error) { + return `Error getting message: ${error}` + } + } +} + +class CreateDraftMessageTool extends BaseOutlookTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'create_draft_message', + description: 'Create a draft message in Microsoft Outlook', + schema: CreateDraftMessageSchema, + baseUrl: 'https://graph.microsoft.com/v1.0/me/messages', + method: 'POST', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const messageData = { + subject: params.subject || '', + body: { + contentType: 'HTML', + content: params.body || '' + }, + toRecipients: this.parseEmailAddresses(params.to), + ccRecipients: params.cc ? this.parseEmailAddresses(params.cc) : [], + bccRecipients: params.bcc ? this.parseEmailAddresses(params.bcc) : [] + } + + const url = 'https://graph.microsoft.com/v1.0/me/messages' + const response = await this.makeGraphRequest(url, 'POST', messageData, params) + return response + } catch (error) { + return `Error creating draft message: ${error}` + } + } +} + +class SendMessageTool extends BaseOutlookTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'send_message', + description: 'Send a message via Microsoft Outlook', + schema: SendMessageSchema, + baseUrl: 'https://graph.microsoft.com/v1.0/me/sendMail', + method: 'POST', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const messageData = { + message: { + subject: params.subject || '', + body: { + contentType: 'HTML', + content: params.body || '' + }, + toRecipients: this.parseEmailAddresses(params.to) + }, + saveToSentItems: true + } + + const url = 'https://graph.microsoft.com/v1.0/me/sendMail' + await this.makeGraphRequest(url, 'POST', messageData, params) + return 'Message sent successfully' + } catch (error) { + return `Error sending message: ${error}` + } + } +} + +class UpdateMessageTool extends BaseOutlookTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'update_message', + description: 'Update a message in Microsoft Outlook', + schema: UpdateMessageSchema, + baseUrl: 'https://graph.microsoft.com/v1.0/me/messages', + method: 'PATCH', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const messageData: any = {} + if (params.isRead !== undefined) messageData.isRead = params.isRead + + const url = `https://graph.microsoft.com/v1.0/me/messages/${params.messageId}` + const response = await this.makeGraphRequest(url, 'PATCH', messageData, params) + return response + } catch (error) { + return `Error updating message: ${error}` + } + } +} + +class DeleteMessageTool extends BaseOutlookTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'delete_message', + description: 'Delete a message from Microsoft Outlook', + schema: DeleteMessageSchema, + baseUrl: 'https://graph.microsoft.com/v1.0/me/messages', + method: 'DELETE', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const url = `https://graph.microsoft.com/v1.0/me/messages/${params.messageId}` + + try { + await this.makeGraphRequest(url, 'DELETE', undefined, params) + return `Message ${params.messageId} deleted successfully` + } catch (error) { + return `Error deleting message: ${error}` + } + } +} + +class CopyMessageTool extends BaseOutlookTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'copy_message', + description: 'Copy a message to another folder in Microsoft Outlook', + schema: CopyMessageSchema, + baseUrl: 'https://graph.microsoft.com/v1.0/me/messages', + method: 'POST', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const copyData = { + destinationId: params.destinationFolderId + } + + const url = `https://graph.microsoft.com/v1.0/me/messages/${params.messageId}/copy` + const response = await this.makeGraphRequest(url, 'POST', copyData, params) + return response + } catch (error) { + return `Error copying message: ${error}` + } + } +} + +class MoveMessageTool extends BaseOutlookTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'move_message', + description: 'Move a message to another folder in Microsoft Outlook', + schema: MoveMessageSchema, + baseUrl: 'https://graph.microsoft.com/v1.0/me/messages', + method: 'POST', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const moveData = { + destinationId: params.destinationFolderId + } + + const url = `https://graph.microsoft.com/v1.0/me/messages/${params.messageId}/move` + const response = await this.makeGraphRequest(url, 'POST', moveData, params) + return response + } catch (error) { + return `Error moving message: ${error}` + } + } +} + +class ReplyMessageTool extends BaseOutlookTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'reply_message', + description: 'Reply to a message in Microsoft Outlook', + schema: ReplyMessageSchema, + baseUrl: 'https://graph.microsoft.com/v1.0/me/messages', + method: 'POST', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const replyData = { + comment: params.replyBody + } + + const url = `https://graph.microsoft.com/v1.0/me/messages/${params.messageId}/reply` + await this.makeGraphRequest(url, 'POST', replyData, params) + return 'Reply sent successfully' + } catch (error) { + return `Error replying to message: ${error}` + } + } +} + +class ForwardMessageTool extends BaseOutlookTool { + defaultParams: any + + constructor(args: any) { + const toolInput = { + name: 'forward_message', + description: 'Forward a message in Microsoft Outlook', + schema: ForwardMessageSchema, + baseUrl: 'https://graph.microsoft.com/v1.0/me/messages', + method: 'POST', + headers: {} + } + super({ ...toolInput, accessToken: args.accessToken }) + this.defaultParams = args.defaultParams || {} + } + + async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + + try { + const forwardData = { + toRecipients: this.parseEmailAddresses(params.forwardTo), + comment: params.forwardComment || '' + } + + const url = `https://graph.microsoft.com/v1.0/me/messages/${params.messageId}/forward` + await this.makeGraphRequest(url, 'POST', forwardData, params) + return 'Message forwarded successfully' + } catch (error) { + return `Error forwarding message: ${error}` + } + } +} + +export const createOutlookTools = (args?: RequestParameters): DynamicStructuredTool[] => { + const tools: DynamicStructuredTool[] = [] + const actions = args?.actions || [] + const accessToken = args?.accessToken || '' + const defaultParams = args?.defaultParams || {} + + // Calendar tools + if (actions.includes('listCalendars')) { + const listTool = new ListCalendarsTool({ + accessToken, + defaultParams: defaultParams.listCalendars + }) + tools.push(listTool) + } + + if (actions.includes('getCalendar')) { + const getTool = new GetCalendarTool({ + accessToken, + defaultParams: defaultParams.getCalendar + }) + tools.push(getTool) + } + + if (actions.includes('createCalendar')) { + const createTool = new CreateCalendarTool({ + accessToken, + defaultParams: defaultParams.createCalendar + }) + tools.push(createTool) + } + + if (actions.includes('updateCalendar')) { + const updateTool = new UpdateCalendarTool({ + accessToken, + defaultParams: defaultParams.updateCalendar + }) + tools.push(updateTool) + } + + if (actions.includes('deleteCalendar')) { + const deleteTool = new DeleteCalendarTool({ + accessToken, + defaultParams: defaultParams.deleteCalendar + }) + tools.push(deleteTool) + } + + if (actions.includes('listEvents')) { + const listTool = new ListEventsTool({ + accessToken, + defaultParams: defaultParams.listEvents + }) + tools.push(listTool) + } + + if (actions.includes('getEvent')) { + const getTool = new GetEventTool({ + accessToken, + defaultParams: defaultParams.getEvent + }) + tools.push(getTool) + } + + if (actions.includes('createEvent')) { + const createTool = new CreateEventTool({ + accessToken, + defaultParams: defaultParams.createEvent + }) + tools.push(createTool) + } + + if (actions.includes('updateEvent')) { + const updateTool = new UpdateEventTool({ + accessToken, + defaultParams: defaultParams.updateEvent + }) + tools.push(updateTool) + } + + if (actions.includes('deleteEvent')) { + const deleteTool = new DeleteEventTool({ + accessToken, + defaultParams: defaultParams.deleteEvent + }) + tools.push(deleteTool) + } + + // Message tools + if (actions.includes('listMessages')) { + const listTool = new ListMessagesTool({ + accessToken, + defaultParams: defaultParams.listMessages + }) + tools.push(listTool) + } + + if (actions.includes('getMessage')) { + const getTool = new GetMessageTool({ + accessToken, + defaultParams: defaultParams.getMessage + }) + tools.push(getTool) + } + + if (actions.includes('createDraftMessage')) { + const createTool = new CreateDraftMessageTool({ + accessToken, + defaultParams: defaultParams.createDraftMessage + }) + tools.push(createTool) + } + + if (actions.includes('sendMessage')) { + const sendTool = new SendMessageTool({ + accessToken, + defaultParams: defaultParams.sendMessage + }) + tools.push(sendTool) + } + + if (actions.includes('updateMessage')) { + const updateTool = new UpdateMessageTool({ + accessToken, + defaultParams: defaultParams.updateMessage + }) + tools.push(updateTool) + } + + if (actions.includes('deleteMessage')) { + const deleteTool = new DeleteMessageTool({ + accessToken, + defaultParams: defaultParams.deleteMessage + }) + tools.push(deleteTool) + } + + if (actions.includes('copyMessage')) { + const copyTool = new CopyMessageTool({ + accessToken, + defaultParams: defaultParams.copyMessage + }) + tools.push(copyTool) + } + + if (actions.includes('moveMessage')) { + const moveTool = new MoveMessageTool({ + accessToken, + defaultParams: defaultParams.moveMessage + }) + tools.push(moveTool) + } + + if (actions.includes('replyMessage')) { + const replyTool = new ReplyMessageTool({ + accessToken, + defaultParams: defaultParams.replyMessage + }) + tools.push(replyTool) + } + + if (actions.includes('forwardMessage')) { + const forwardTool = new ForwardMessageTool({ + accessToken, + defaultParams: defaultParams.forwardMessage + }) + tools.push(forwardTool) + } + + return tools +} diff --git a/packages/components/nodes/tools/MicrosoftOutlook/outlook.svg b/packages/components/nodes/tools/MicrosoftOutlook/outlook.svg new file mode 100644 index 00000000..134a2ee9 --- /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 00000000..0303c324 --- /dev/null +++ b/packages/components/nodes/tools/MicrosoftTeams/MicrosoftTeams.ts @@ -0,0 +1,1226 @@ +import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' +import { convertMultiOptionsToStringArray, getCredentialData, getCredentialParam, refreshOAuth2Token } from '../../../src/utils' +import { createTeamsTools } from './core' + +class MicrosoftTeams_Tools implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + baseClasses: string[] + inputs: INodeParams[] + credential: INodeParams + + constructor() { + this.label = 'Microsoft Teams' + this.name = 'microsoftTeams' + this.version = 1.0 + this.type = 'MicrosoftTeams' + this.icon = 'teams.svg' + this.category = 'Tools' + this.description = 'Perform Microsoft Teams operations for channels, chats, and chat messages' + this.baseClasses = [this.type, 'Tool'] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['microsoftTeamsOAuth2'] + } + this.inputs = [ + { + label: 'Type', + name: 'teamsType', + type: 'options', + options: [ + { + label: 'Channel', + name: 'channel' + }, + { + label: 'Chat', + name: 'chat' + }, + { + label: 'Chat Message', + name: 'chatMessage' + } + ] + }, + // Channel Actions + { + label: 'Channel Actions', + name: 'channelActions', + type: 'multiOptions', + options: [ + { + label: 'List Channels', + name: 'listChannels' + }, + { + label: 'Get Channel', + name: 'getChannel' + }, + { + label: 'Create Channel', + name: 'createChannel' + }, + { + label: 'Update Channel', + name: 'updateChannel' + }, + { + label: 'Delete Channel', + name: 'deleteChannel' + }, + { + label: 'Archive Channel', + name: 'archiveChannel' + }, + { + label: 'Unarchive Channel', + name: 'unarchiveChannel' + }, + { + label: 'List Channel Members', + name: 'listChannelMembers' + }, + { + label: 'Add Channel Member', + name: 'addChannelMember' + }, + { + label: 'Remove Channel Member', + name: 'removeChannelMember' + } + ], + show: { + teamsType: ['channel'] + } + }, + // Chat Actions + { + label: 'Chat Actions', + name: 'chatActions', + type: 'multiOptions', + options: [ + { + label: 'List Chats', + name: 'listChats' + }, + { + label: 'Get Chat', + name: 'getChat' + }, + { + label: 'Create Chat', + name: 'createChat' + }, + { + label: 'Update Chat', + name: 'updateChat' + }, + { + label: 'Delete Chat', + name: 'deleteChat' + }, + { + label: 'List Chat Members', + name: 'listChatMembers' + }, + { + label: 'Add Chat Member', + name: 'addChatMember' + }, + { + label: 'Remove Chat Member', + name: 'removeChatMember' + }, + { + label: 'Pin Message', + name: 'pinMessage' + }, + { + label: 'Unpin Message', + name: 'unpinMessage' + } + ], + show: { + teamsType: ['chat'] + } + }, + // Chat Message Actions + { + label: 'Chat Message Actions', + name: 'chatMessageActions', + type: 'multiOptions', + options: [ + { + label: 'List Messages', + name: 'listMessages' + }, + { + label: 'Get Message', + name: 'getMessage' + }, + { + label: 'Send Message', + name: 'sendMessage' + }, + { + label: 'Update Message', + name: 'updateMessage' + }, + { + label: 'Delete Message', + name: 'deleteMessage' + }, + { + label: 'Reply to Message', + name: 'replyToMessage' + }, + { + label: 'Set Reaction', + name: 'setReaction' + }, + { + label: 'Unset Reaction', + name: 'unsetReaction' + }, + { + label: 'Get All Messages', + name: 'getAllMessages' + } + ], + show: { + teamsType: ['chatMessage'] + } + }, + + // CHANNEL PARAMETERS + // List Channels Parameters + { + label: 'Team ID [List Channels]', + name: 'teamIdListChannels', + type: 'string', + description: 'ID of the team to list channels from', + show: { + teamsType: ['channel'], + channelActions: ['listChannels'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Max Results [List Channels]', + name: 'maxResultsListChannels', + type: 'number', + description: 'Maximum number of channels to return', + default: 50, + show: { + teamsType: ['channel'], + channelActions: ['listChannels'] + }, + additionalParams: true, + optional: true + }, + + // Get Channel Parameters + { + label: 'Team ID [Get Channel]', + name: 'teamIdGetChannel', + type: 'string', + description: 'ID of the team that contains the channel', + show: { + teamsType: ['channel'], + channelActions: ['getChannel'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Channel ID [Get Channel]', + name: 'channelIdGetChannel', + type: 'string', + description: 'ID of the channel to retrieve', + show: { + teamsType: ['channel'], + channelActions: ['getChannel'] + }, + additionalParams: true, + optional: true + }, + + // Create Channel Parameters + { + label: 'Team ID [Create Channel]', + name: 'teamIdCreateChannel', + type: 'string', + description: 'ID of the team to create the channel in', + show: { + teamsType: ['channel'], + channelActions: ['createChannel'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Display Name [Create Channel]', + name: 'displayNameCreateChannel', + type: 'string', + description: 'Display name of the channel', + placeholder: 'My New Channel', + show: { + teamsType: ['channel'], + channelActions: ['createChannel'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Description [Create Channel]', + name: 'descriptionCreateChannel', + type: 'string', + description: 'Description of the channel', + placeholder: 'Channel description', + rows: 2, + show: { + teamsType: ['channel'], + channelActions: ['createChannel'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Membership Type [Create Channel]', + name: 'membershipTypeCreateChannel', + type: 'options', + options: [ + { label: 'Standard', name: 'standard' }, + { label: 'Private', name: 'private' }, + { label: 'Shared', name: 'shared' } + ], + default: 'standard', + description: 'Type of channel membership', + show: { + teamsType: ['channel'], + channelActions: ['createChannel'] + }, + additionalParams: true, + optional: true + }, + + // Update Channel Parameters + { + label: 'Team ID [Update Channel]', + name: 'teamIdUpdateChannel', + type: 'string', + description: 'ID of the team that contains the channel', + show: { + teamsType: ['channel'], + channelActions: ['updateChannel'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Channel ID [Update Channel]', + name: 'channelIdUpdateChannel', + type: 'string', + description: 'ID of the channel to update', + show: { + teamsType: ['channel'], + channelActions: ['updateChannel'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Display Name [Update Channel]', + name: 'displayNameUpdateChannel', + type: 'string', + description: 'New display name of the channel', + show: { + teamsType: ['channel'], + channelActions: ['updateChannel'] + }, + additionalParams: true, + optional: true + }, + + // Delete/Archive Channel Parameters + { + label: 'Team ID [Delete/Archive Channel]', + name: 'teamIdDeleteChannel', + type: 'string', + description: 'ID of the team that contains the channel', + show: { + teamsType: ['channel'], + channelActions: ['deleteChannel', 'archiveChannel', 'unarchiveChannel'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Channel ID [Delete/Archive Channel]', + name: 'channelIdDeleteChannel', + type: 'string', + description: 'ID of the channel to delete or archive', + show: { + teamsType: ['channel'], + channelActions: ['deleteChannel', 'archiveChannel', 'unarchiveChannel'] + }, + additionalParams: true, + optional: true + }, + + // Channel Members Parameters + { + label: 'Team ID [Channel Members]', + name: 'teamIdChannelMembers', + type: 'string', + description: 'ID of the team that contains the channel', + show: { + teamsType: ['channel'], + channelActions: ['listChannelMembers', 'addChannelMember', 'removeChannelMember'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Channel ID [Channel Members]', + name: 'channelIdChannelMembers', + type: 'string', + description: 'ID of the channel', + show: { + teamsType: ['channel'], + channelActions: ['listChannelMembers', 'addChannelMember', 'removeChannelMember'] + }, + additionalParams: true, + optional: true + }, + { + label: 'User ID [Add/Remove Channel Member]', + name: 'userIdChannelMember', + type: 'string', + description: 'ID of the user to add or remove', + show: { + teamsType: ['channel'], + channelActions: ['addChannelMember', 'removeChannelMember'] + }, + additionalParams: true, + optional: true + }, + + // CHAT PARAMETERS + // List Chats Parameters + { + label: 'Max Results [List Chats]', + name: 'maxResultsListChats', + type: 'number', + description: 'Maximum number of chats to return', + default: 50, + show: { + teamsType: ['chat'], + chatActions: ['listChats'] + }, + additionalParams: true, + optional: true + }, + + // Get Chat Parameters + { + label: 'Chat ID [Get Chat]', + name: 'chatIdGetChat', + type: 'string', + description: 'ID of the chat to retrieve', + show: { + teamsType: ['chat'], + chatActions: ['getChat'] + }, + additionalParams: true, + optional: true + }, + + // Create Chat Parameters + { + label: 'Chat Type [Create Chat]', + name: 'chatTypeCreateChat', + type: 'options', + options: [ + { label: 'One on One', name: 'oneOnOne' }, + { label: 'Group', name: 'group' } + ], + default: 'group', + description: 'Type of chat to create', + show: { + teamsType: ['chat'], + chatActions: ['createChat'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Topic [Create Chat]', + name: 'topicCreateChat', + type: 'string', + description: 'Topic/subject of the chat (for group chats)', + placeholder: 'Chat topic', + show: { + teamsType: ['chat'], + chatActions: ['createChat'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Members [Create Chat]', + name: 'membersCreateChat', + type: 'string', + description: 'Comma-separated list of user IDs to add to the chat', + placeholder: 'user1@example.com,user2@example.com', + show: { + teamsType: ['chat'], + chatActions: ['createChat'] + }, + additionalParams: true, + optional: true + }, + + // Update Chat Parameters + { + label: 'Chat ID [Update Chat]', + name: 'chatIdUpdateChat', + type: 'string', + description: 'ID of the chat to update', + show: { + teamsType: ['chat'], + chatActions: ['updateChat'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Topic [Update Chat]', + name: 'topicUpdateChat', + type: 'string', + description: 'New topic/subject of the chat', + show: { + teamsType: ['chat'], + chatActions: ['updateChat'] + }, + additionalParams: true, + optional: true + }, + + // Delete Chat Parameters + { + label: 'Chat ID [Delete Chat]', + name: 'chatIdDeleteChat', + type: 'string', + description: 'ID of the chat to delete', + show: { + teamsType: ['chat'], + chatActions: ['deleteChat'] + }, + additionalParams: true, + optional: true + }, + + // Chat Members Parameters + { + label: 'Chat ID [Chat Members]', + name: 'chatIdChatMembers', + type: 'string', + description: 'ID of the chat', + show: { + teamsType: ['chat'], + chatActions: ['listChatMembers', 'addChatMember', 'removeChatMember'] + }, + additionalParams: true, + optional: true + }, + { + label: 'User ID [Add/Remove Chat Member]', + name: 'userIdChatMember', + type: 'string', + description: 'ID of the user to add or remove', + show: { + teamsType: ['chat'], + chatActions: ['addChatMember', 'removeChatMember'] + }, + additionalParams: true, + optional: true + }, + + // Pin/Unpin Message Parameters + { + label: 'Chat ID [Pin/Unpin Message]', + name: 'chatIdPinMessage', + type: 'string', + description: 'ID of the chat', + show: { + teamsType: ['chat'], + chatActions: ['pinMessage', 'unpinMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Message ID [Pin/Unpin Message]', + name: 'messageIdPinMessage', + type: 'string', + description: 'ID of the message to pin or unpin', + show: { + teamsType: ['chat'], + chatActions: ['pinMessage', 'unpinMessage'] + }, + additionalParams: true, + optional: true + }, + + // CHAT MESSAGE PARAMETERS + // List Messages Parameters + { + label: 'Chat/Channel ID [List Messages]', + name: 'chatChannelIdListMessages', + type: 'string', + description: 'ID of the chat or channel to list messages from', + show: { + teamsType: ['chatMessage'], + chatMessageActions: ['listMessages'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Team ID [List Messages - Channel Only]', + name: 'teamIdListMessages', + type: 'string', + description: 'ID of the team (required for channel messages)', + show: { + teamsType: ['chatMessage'], + chatMessageActions: ['listMessages'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Max Results [List Messages]', + name: 'maxResultsListMessages', + type: 'number', + description: 'Maximum number of messages to return', + default: 50, + show: { + teamsType: ['chatMessage'], + chatMessageActions: ['listMessages'] + }, + additionalParams: true, + optional: true + }, + + // Get Message Parameters + { + label: 'Chat/Channel ID [Get Message]', + name: 'chatChannelIdGetMessage', + type: 'string', + description: 'ID of the chat or channel', + show: { + teamsType: ['chatMessage'], + chatMessageActions: ['getMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Team ID [Get Message - Channel Only]', + name: 'teamIdGetMessage', + type: 'string', + description: 'ID of the team (required for channel messages)', + show: { + teamsType: ['chatMessage'], + chatMessageActions: ['getMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Message ID [Get Message]', + name: 'messageIdGetMessage', + type: 'string', + description: 'ID of the message to retrieve', + show: { + teamsType: ['chatMessage'], + chatMessageActions: ['getMessage'] + }, + additionalParams: true, + optional: true + }, + + // Send Message Parameters + { + label: 'Chat/Channel ID [Send Message]', + name: 'chatChannelIdSendMessage', + type: 'string', + description: 'ID of the chat or channel to send message to', + show: { + teamsType: ['chatMessage'], + chatMessageActions: ['sendMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Team ID [Send Message - Channel Only]', + name: 'teamIdSendMessage', + type: 'string', + description: 'ID of the team (required for channel messages)', + show: { + teamsType: ['chatMessage'], + chatMessageActions: ['sendMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Message Body [Send Message]', + name: 'messageBodySendMessage', + type: 'string', + description: 'Content of the message', + placeholder: 'Hello, this is a message!', + rows: 4, + show: { + teamsType: ['chatMessage'], + chatMessageActions: ['sendMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Content Type [Send Message]', + name: 'contentTypeSendMessage', + type: 'options', + options: [ + { label: 'Text', name: 'text' }, + { label: 'HTML', name: 'html' } + ], + default: 'text', + description: 'Content type of the message', + show: { + teamsType: ['chatMessage'], + chatMessageActions: ['sendMessage'] + }, + additionalParams: true, + optional: true + }, + + // Update Message Parameters + { + label: 'Chat/Channel ID [Update Message]', + name: 'chatChannelIdUpdateMessage', + type: 'string', + description: 'ID of the chat or channel', + show: { + teamsType: ['chatMessage'], + chatMessageActions: ['updateMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Team ID [Update Message - Channel Only]', + name: 'teamIdUpdateMessage', + type: 'string', + description: 'ID of the team (required for channel messages)', + show: { + teamsType: ['chatMessage'], + chatMessageActions: ['updateMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Message ID [Update Message]', + name: 'messageIdUpdateMessage', + type: 'string', + description: 'ID of the message to update', + show: { + teamsType: ['chatMessage'], + chatMessageActions: ['updateMessage'] + }, + additionalParams: true, + optional: true + }, + + // Delete Message Parameters + { + label: 'Chat/Channel ID [Delete Message]', + name: 'chatChannelIdDeleteMessage', + type: 'string', + description: 'ID of the chat or channel', + show: { + teamsType: ['chatMessage'], + chatMessageActions: ['deleteMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Team ID [Delete Message - Channel Only]', + name: 'teamIdDeleteMessage', + type: 'string', + description: 'ID of the team (required for channel messages)', + show: { + teamsType: ['chatMessage'], + chatMessageActions: ['deleteMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Message ID [Delete Message]', + name: 'messageIdDeleteMessage', + type: 'string', + description: 'ID of the message to delete', + show: { + teamsType: ['chatMessage'], + chatMessageActions: ['deleteMessage'] + }, + additionalParams: true, + optional: true + }, + + // Reply to Message Parameters + { + label: 'Chat/Channel ID [Reply to Message]', + name: 'chatChannelIdReplyMessage', + type: 'string', + description: 'ID of the chat or channel', + show: { + teamsType: ['chatMessage'], + chatMessageActions: ['replyToMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Team ID [Reply to Message - Channel Only]', + name: 'teamIdReplyMessage', + type: 'string', + description: 'ID of the team (required for channel messages)', + show: { + teamsType: ['chatMessage'], + chatMessageActions: ['replyToMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Message ID [Reply to Message]', + name: 'messageIdReplyMessage', + type: 'string', + description: 'ID of the message to reply to', + show: { + teamsType: ['chatMessage'], + chatMessageActions: ['replyToMessage'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Reply Body [Reply to Message]', + name: 'replyBodyReplyMessage', + type: 'string', + description: 'Content of the reply', + placeholder: 'This is my reply', + rows: 3, + show: { + teamsType: ['chatMessage'], + chatMessageActions: ['replyToMessage'] + }, + additionalParams: true, + optional: true + }, + + // Set/Unset Reaction Parameters + { + label: 'Chat/Channel ID [Set/Unset Reaction]', + name: 'chatChannelIdReaction', + type: 'string', + description: 'ID of the chat or channel', + show: { + teamsType: ['chatMessage'], + chatMessageActions: ['setReaction', 'unsetReaction'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Team ID [Set/Unset Reaction - Channel Only]', + name: 'teamIdReaction', + type: 'string', + description: 'ID of the team (required for channel messages)', + show: { + teamsType: ['chatMessage'], + chatMessageActions: ['setReaction', 'unsetReaction'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Message ID [Set/Unset Reaction]', + name: 'messageIdReaction', + type: 'string', + description: 'ID of the message to react to', + show: { + teamsType: ['chatMessage'], + chatMessageActions: ['setReaction', 'unsetReaction'] + }, + additionalParams: true, + optional: true + }, + { + label: 'Reaction Type [Set Reaction]', + name: 'reactionTypeSetReaction', + type: 'options', + options: [ + { label: 'Like', name: 'like' }, + { label: 'Heart', name: 'heart' }, + { label: 'Laugh', name: 'laugh' }, + { label: 'Surprised', name: 'surprised' }, + { label: 'Sad', name: 'sad' }, + { label: 'Angry', name: 'angry' } + ], + default: 'like', + description: 'Type of reaction to set', + show: { + teamsType: ['chatMessage'], + chatMessageActions: ['setReaction'] + }, + additionalParams: true, + optional: true + } + ] + } + + async init(nodeData: INodeData, _: string, options: any): Promise { + const teamsType = nodeData.inputs?.teamsType as string + const channelActions = nodeData.inputs?.channelActions as string + const chatActions = nodeData.inputs?.chatActions as string + const chatMessageActions = nodeData.inputs?.chatMessageActions as string + + let actions: string[] = [] + if (teamsType === 'channel') { + actions = convertMultiOptionsToStringArray(channelActions) + } else if (teamsType === 'chat') { + actions = convertMultiOptionsToStringArray(chatActions) + } else if (teamsType === 'chatMessage') { + actions = convertMultiOptionsToStringArray(chatMessageActions) + } + + let credentialData = await getCredentialData(nodeData.credential ?? '', options) + credentialData = await refreshOAuth2Token(nodeData.credential ?? '', credentialData, options) + const accessToken = getCredentialParam('access_token', credentialData, nodeData) + + if (!accessToken) { + throw new Error('No access token found in credential') + } + + // 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 00000000..a0c08091 --- /dev/null +++ b/packages/components/nodes/tools/MicrosoftTeams/core.ts @@ -0,0 +1,1756 @@ +import { z } from 'zod' +import { CallbackManagerForToolRun } from '@langchain/core/callbacks/manager' +import { DynamicStructuredTool, DynamicStructuredToolInput } from '../OpenAPIToolkit/core' +import { TOOL_ARGS_PREFIX } from '../../../src/agents' + +interface TeamsToolOptions { + accessToken: string + actions: string[] + defaultParams: any + type: string +} + +const BASE_URL = 'https://graph.microsoft.com/v1.0' + +// Helper function to make Graph API requests +async function makeGraphRequest( + endpoint: string, + method: 'GET' | 'POST' | 'PATCH' | 'DELETE' = 'GET', + body?: any, + accessToken?: string +): Promise { + const headers: Record = { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json' + } + + const config: RequestInit = { + method, + headers + } + + if (body && (method === 'POST' || method === 'PATCH')) { + config.body = JSON.stringify(body) + } + + try { + const response = await fetch(`${BASE_URL}${endpoint}`, config) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`Microsoft Graph API error: ${response.status} ${response.statusText} - ${errorText}`) + } + + // Handle empty responses for DELETE operations + if (method === 'DELETE' || response.status === 204) { + return { success: true, message: 'Operation completed successfully' } + } + + return await response.json() + } catch (error) { + throw new Error(`Microsoft Graph request failed: ${error instanceof Error ? error.message : 'Unknown error'}`) + } +} + +// Base Teams Tool class +abstract class BaseTeamsTool extends DynamicStructuredTool { + accessToken = '' + protected defaultParams: any + + constructor(args: DynamicStructuredToolInput & { accessToken?: string; defaultParams?: any }) { + super(args) + this.accessToken = args.accessToken ?? '' + this.defaultParams = args.defaultParams || {} + } + + protected async makeTeamsRequest(endpoint: string, method: string = 'GET', body?: any) { + return await makeGraphRequest(endpoint, method as any, body, this.accessToken) + } + + protected formatResponse(data: any, params: any): string { + return JSON.stringify(data) + TOOL_ARGS_PREFIX + JSON.stringify(params) + } + + // Abstract method that must be implemented by subclasses + protected abstract _call(arg: any, runManager?: CallbackManagerForToolRun, parentConfig?: any): Promise +} + +// CHANNEL TOOLS + +class ListChannelsTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'list_channels', + description: 'List all channels in a team', + schema: z.object({ + teamId: z.string().describe('ID of the team to list channels from'), + maxResults: z.number().optional().default(50).describe('Maximum number of channels to return') + }), + baseUrl: BASE_URL, + method: 'GET', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { teamId, maxResults = 50 } = params + + if (!teamId) { + throw new Error('Team ID is required to list channels') + } + + try { + const endpoint = `/teams/${teamId}/channels` + const result = await this.makeTeamsRequest(endpoint) + + // Filter results to maxResults on client side since $top is not supported + const channels = result.value || [] + const limitedChannels = channels.slice(0, maxResults) + + const responseData = { + success: true, + channels: limitedChannels, + count: limitedChannels.length, + total: channels.length + } + + return this.formatResponse(responseData, params) + } catch (error) { + return `Error listing channels: ${error}` + } + } +} + +class GetChannelTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'get_channel', + description: 'Get details of a specific channel', + schema: z.object({ + teamId: z.string().describe('ID of the team that contains the channel'), + channelId: z.string().describe('ID of the channel to retrieve') + }), + baseUrl: BASE_URL, + method: 'GET', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { teamId, channelId } = params + + if (!teamId || !channelId) { + throw new Error('Both Team ID and Channel ID are required') + } + + try { + const endpoint = `/teams/${teamId}/channels/${channelId}` + const result = await this.makeTeamsRequest(endpoint) + + return this.formatResponse( + { + success: true, + channel: result + }, + params + ) + } catch (error) { + return this.formatResponse(`Error getting channel: ${error}`, params) + } + } +} + +class CreateChannelTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'create_channel', + description: 'Create a new channel in a team', + schema: z.object({ + teamId: z.string().describe('ID of the team to create the channel in'), + displayName: z.string().describe('Display name of the channel'), + description: z.string().optional().describe('Description of the channel'), + membershipType: z + .enum(['standard', 'private', 'shared']) + .optional() + .default('standard') + .describe('Type of channel membership') + }), + baseUrl: BASE_URL, + method: 'POST', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { teamId, displayName, description, membershipType = 'standard' } = params + + if (!teamId || !displayName) { + throw new Error('Team ID and Display Name are required to create a channel') + } + + try { + const body = { + displayName, + membershipType, + ...(description && { description }) + } + + const endpoint = `/teams/${teamId}/channels` + const result = await this.makeTeamsRequest(endpoint, 'POST', body) + + return this.formatResponse( + { + success: true, + channel: result, + message: `Channel "${displayName}" created successfully` + }, + params + ) + } catch (error) { + return this.formatResponse(`Error creating channel: ${error}`, params) + } + } +} + +class UpdateChannelTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'update_channel', + description: 'Update an existing channel', + schema: z.object({ + teamId: z.string().describe('ID of the team that contains the channel'), + channelId: z.string().describe('ID of the channel to update'), + displayName: z.string().optional().describe('New display name of the channel'), + description: z.string().optional().describe('New description of the channel') + }), + baseUrl: BASE_URL, + method: 'PATCH', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { teamId, channelId, displayName, description } = params + + if (!teamId || !channelId) { + throw new Error('Both Team ID and Channel ID are required') + } + + try { + const body: any = {} + if (displayName) body.displayName = displayName + if (description) body.description = description + + if (Object.keys(body).length === 0) { + throw new Error('At least one field to update must be provided') + } + + const endpoint = `/teams/${teamId}/channels/${channelId}` + await this.makeTeamsRequest(endpoint, 'PATCH', body) + + return this.formatResponse( + { + success: true, + message: 'Channel updated successfully' + }, + params + ) + } catch (error) { + return this.formatResponse(`Error updating channel: ${error}`, params) + } + } +} + +class DeleteChannelTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'delete_channel', + description: 'Delete a channel from a team', + schema: z.object({ + teamId: z.string().describe('ID of the team that contains the channel'), + channelId: z.string().describe('ID of the channel to delete') + }), + baseUrl: BASE_URL, + method: 'DELETE', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { teamId, channelId } = params + + if (!teamId || !channelId) { + throw new Error('Both Team ID and Channel ID are required') + } + + try { + const endpoint = `/teams/${teamId}/channels/${channelId}` + await this.makeTeamsRequest(endpoint, 'DELETE') + + return this.formatResponse( + { + success: true, + message: 'Channel deleted successfully' + }, + params + ) + } catch (error) { + return this.formatResponse(`Error deleting channel: ${error}`, params) + } + } +} + +class ArchiveChannelTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'archive_channel', + description: 'Archive a channel in a team', + schema: z.object({ + teamId: z.string().describe('ID of the team that contains the channel'), + channelId: z.string().describe('ID of the channel to archive') + }), + baseUrl: BASE_URL, + method: 'POST', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { teamId, channelId } = params + + if (!teamId || !channelId) { + throw new Error('Both Team ID and Channel ID are required') + } + + try { + const endpoint = `/teams/${teamId}/channels/${channelId}/archive` + await this.makeTeamsRequest(endpoint, 'POST', {}) + + return this.formatResponse( + { + success: true, + message: 'Channel archived successfully' + }, + params + ) + } catch (error) { + return this.formatResponse(`Error archiving channel: ${error}`, params) + } + } +} + +class UnarchiveChannelTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'unarchive_channel', + description: 'Unarchive a channel in a team', + schema: z.object({ + teamId: z.string().describe('ID of the team that contains the channel'), + channelId: z.string().describe('ID of the channel to unarchive') + }), + baseUrl: BASE_URL, + method: 'POST', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { teamId, channelId } = params + + if (!teamId || !channelId) { + throw new Error('Both Team ID and Channel ID are required') + } + + try { + const endpoint = `/teams/${teamId}/channels/${channelId}/unarchive` + await this.makeTeamsRequest(endpoint, 'POST', {}) + + return this.formatResponse( + { + success: true, + message: 'Channel unarchived successfully' + }, + params + ) + } catch (error) { + return this.formatResponse(`Error unarchiving channel: ${error}`, params) + } + } +} + +class ListChannelMembersTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'list_channel_members', + description: 'List members of a channel', + schema: z.object({ + teamId: z.string().describe('ID of the team that contains the channel'), + channelId: z.string().describe('ID of the channel') + }), + baseUrl: BASE_URL, + method: 'GET', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { teamId, channelId } = params + + if (!teamId || !channelId) { + throw new Error('Both Team ID and Channel ID are required') + } + + try { + const endpoint = `/teams/${teamId}/channels/${channelId}/members` + const result = await this.makeTeamsRequest(endpoint) + + return this.formatResponse( + { + success: true, + members: result.value || [], + count: result.value?.length || 0 + }, + params + ) + } catch (error) { + return this.formatResponse(`Error listing channel members: ${error}`, params) + } + } +} + +class AddChannelMemberTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'add_channel_member', + description: 'Add a member to a channel', + schema: z.object({ + teamId: z.string().describe('ID of the team that contains the channel'), + channelId: z.string().describe('ID of the channel'), + userId: z.string().describe('ID of the user to add') + }), + baseUrl: BASE_URL, + method: 'POST', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { teamId, channelId, userId } = params + + if (!teamId || !channelId || !userId) { + throw new Error('Team ID, Channel ID, and User ID are all required') + } + + try { + const body = { + '@odata.type': '#microsoft.graph.aadUserConversationMember', + 'user@odata.bind': `https://graph.microsoft.com/v1.0/users('${userId}')` + } + + const endpoint = `/teams/${teamId}/channels/${channelId}/members` + await this.makeTeamsRequest(endpoint, 'POST', body) + + return this.formatResponse( + { + success: true, + message: 'Member added to channel successfully' + }, + params + ) + } catch (error) { + return this.formatResponse(`Error adding channel member: ${error}`, params) + } + } +} + +class RemoveChannelMemberTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'remove_channel_member', + description: 'Remove a member from a channel', + schema: z.object({ + teamId: z.string().describe('ID of the team that contains the channel'), + channelId: z.string().describe('ID of the channel'), + userId: z.string().describe('ID of the user to remove') + }), + baseUrl: BASE_URL, + method: 'DELETE', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { teamId, channelId, userId } = params + + if (!teamId || !channelId || !userId) { + throw new Error('Team ID, Channel ID, and User ID are all required') + } + + try { + // First get the membership ID + const membersEndpoint = `/teams/${teamId}/channels/${channelId}/members` + const membersResult = await this.makeTeamsRequest(membersEndpoint) + + const member = membersResult.value?.find((m: any) => m.userId === userId) + if (!member) { + throw new Error('User is not a member of this channel') + } + + const endpoint = `/teams/${teamId}/channels/${channelId}/members/${member.id}` + await this.makeTeamsRequest(endpoint, 'DELETE') + + return this.formatResponse( + { + success: true, + message: 'Member removed from channel successfully' + }, + params + ) + } catch (error) { + return this.formatResponse(`Error removing channel member: ${error}`, params) + } + } +} + +// CHAT TOOLS + +class ListChatsTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'list_chats', + description: 'List all chats for the current user', + schema: z.object({ + maxResults: z.number().optional().default(50).describe('Maximum number of chats to return') + }), + baseUrl: BASE_URL, + method: 'GET', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { maxResults = 50 } = params + + try { + const endpoint = `/me/chats?$top=${maxResults}` + const result = await this.makeTeamsRequest(endpoint) + + return this.formatResponse( + { + success: true, + chats: result.value || [], + count: result.value?.length || 0 + }, + params + ) + } catch (error) { + return this.formatResponse(`Error listing chats: ${error}`, params) + } + } +} + +class GetChatTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'get_chat', + description: 'Get details of a specific chat', + schema: z.object({ + chatId: z.string().describe('ID of the chat to retrieve') + }), + baseUrl: BASE_URL, + method: 'GET', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { chatId } = params + + if (!chatId) { + throw new Error('Chat ID is required') + } + + try { + const endpoint = `/chats/${chatId}` + const result = await this.makeTeamsRequest(endpoint) + + return this.formatResponse( + { + success: true, + chat: result + }, + params + ) + } catch (error) { + return this.formatResponse(`Error getting chat: ${error}`, params) + } + } +} + +class CreateChatTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'create_chat', + description: 'Create a new chat', + schema: z.object({ + chatType: z.enum(['oneOnOne', 'group']).optional().default('group').describe('Type of chat to create'), + topic: z.string().optional().describe('Topic/subject of the chat (for group chats)'), + members: z.string().describe('Comma-separated list of user IDs to add to the chat') + }), + baseUrl: BASE_URL, + method: 'POST', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { chatType = 'group', topic, members } = params + + if (!members) { + throw new Error('Members list is required to create a chat') + } + + try { + const memberIds = members.split(',').map((id: string) => id.trim()) + const chatMembers = memberIds.map((userId: string) => ({ + '@odata.type': '#microsoft.graph.aadUserConversationMember', + 'user@odata.bind': `https://graph.microsoft.com/v1.0/users('${userId}')` + })) + + const body: any = { + chatType, + members: chatMembers + } + + if (topic && chatType === 'group') { + body.topic = topic + } + + const endpoint = '/chats' + const result = await this.makeTeamsRequest(endpoint, 'POST', body) + + return this.formatResponse( + { + success: true, + chat: result, + message: 'Chat created successfully' + }, + params + ) + } catch (error) { + return this.formatResponse(`Error creating chat: ${error}`, params) + } + } +} + +class UpdateChatTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'update_chat', + description: 'Update an existing chat', + schema: z.object({ + chatId: z.string().describe('ID of the chat to update'), + topic: z.string().describe('New topic/subject of the chat') + }), + baseUrl: BASE_URL, + method: 'PATCH', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { chatId, topic } = params + + if (!chatId) { + throw new Error('Chat ID is required') + } + + if (!topic) { + throw new Error('Topic is required to update a chat') + } + + try { + const body = { topic } + const endpoint = `/chats/${chatId}` + await this.makeTeamsRequest(endpoint, 'PATCH', body) + + return this.formatResponse( + { + success: true, + message: 'Chat updated successfully' + }, + params + ) + } catch (error) { + return this.formatResponse(`Error updating chat: ${error}`, params) + } + } +} + +class DeleteChatTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'delete_chat', + description: 'Delete a chat', + schema: z.object({ + chatId: z.string().describe('ID of the chat to delete') + }), + baseUrl: BASE_URL, + method: 'DELETE', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { chatId } = params + + if (!chatId) { + throw new Error('Chat ID is required') + } + + try { + const endpoint = `/chats/${chatId}` + await this.makeTeamsRequest(endpoint, 'DELETE') + + return this.formatResponse( + { + success: true, + message: 'Chat deleted successfully' + }, + params + ) + } catch (error) { + return this.formatResponse(`Error deleting chat: ${error}`, params) + } + } +} + +class ListChatMembersTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'list_chat_members', + description: 'List members of a chat', + schema: z.object({ + chatId: z.string().describe('ID of the chat') + }), + baseUrl: BASE_URL, + method: 'GET', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { chatId } = params + + if (!chatId) { + throw new Error('Chat ID is required') + } + + try { + const endpoint = `/chats/${chatId}/members` + const result = await this.makeTeamsRequest(endpoint) + + return this.formatResponse( + { + success: true, + members: result.value || [], + count: result.value?.length || 0 + }, + params + ) + } catch (error) { + return this.formatResponse(`Error listing chat members: ${error}`, params) + } + } +} + +class AddChatMemberTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'add_chat_member', + description: 'Add a member to a chat', + schema: z.object({ + chatId: z.string().describe('ID of the chat'), + userId: z.string().describe('ID of the user to add') + }), + baseUrl: BASE_URL, + method: 'POST', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { chatId, userId } = params + + if (!chatId || !userId) { + throw new Error('Both Chat ID and User ID are required') + } + + try { + const body = { + '@odata.type': '#microsoft.graph.aadUserConversationMember', + 'user@odata.bind': `https://graph.microsoft.com/v1.0/users('${userId}')` + } + + const endpoint = `/chats/${chatId}/members` + await this.makeTeamsRequest(endpoint, 'POST', body) + + return this.formatResponse( + { + success: true, + message: 'Member added to chat successfully' + }, + params + ) + } catch (error) { + return this.formatResponse(`Error adding chat member: ${error}`, params) + } + } +} + +class RemoveChatMemberTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'remove_chat_member', + description: 'Remove a member from a chat', + schema: z.object({ + chatId: z.string().describe('ID of the chat'), + userId: z.string().describe('ID of the user to remove') + }), + baseUrl: BASE_URL, + method: 'DELETE', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { chatId, userId } = params + + if (!chatId || !userId) { + throw new Error('Both Chat ID and User ID are required') + } + + try { + // First get the membership ID + const membersEndpoint = `/chats/${chatId}/members` + const membersResult = await this.makeTeamsRequest(membersEndpoint) + + const member = membersResult.value?.find((m: any) => m.userId === userId) + if (!member) { + throw new Error('User is not a member of this chat') + } + + const endpoint = `/chats/${chatId}/members/${member.id}` + await this.makeTeamsRequest(endpoint, 'DELETE') + + return this.formatResponse( + { + success: true, + message: 'Member removed from chat successfully' + }, + params + ) + } catch (error) { + return this.formatResponse(`Error removing chat member: ${error}`, params) + } + } +} + +class PinMessageTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'pin_message', + description: 'Pin a message in a chat', + schema: z.object({ + chatId: z.string().describe('ID of the chat'), + messageId: z.string().describe('ID of the message to pin') + }), + baseUrl: BASE_URL, + method: 'POST', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { chatId, messageId } = params + + if (!chatId || !messageId) { + throw new Error('Both Chat ID and Message ID are required') + } + + try { + const body = { + message: { + '@odata.bind': `https://graph.microsoft.com/v1.0/chats('${chatId}')/messages('${messageId}')` + } + } + + const endpoint = `/chats/${chatId}/pinnedMessages` + await this.makeTeamsRequest(endpoint, 'POST', body) + + return this.formatResponse( + { + success: true, + message: 'Message pinned successfully' + }, + params + ) + } catch (error) { + return this.formatResponse(`Error pinning message: ${error}`, params) + } + } +} + +class UnpinMessageTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'unpin_message', + description: 'Unpin a message from a chat', + schema: z.object({ + chatId: z.string().describe('ID of the chat'), + messageId: z.string().describe('ID of the message to unpin') + }), + baseUrl: BASE_URL, + method: 'DELETE', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { chatId, messageId } = params + + if (!chatId || !messageId) { + throw new Error('Both Chat ID and Message ID are required') + } + + try { + // First get the pinned messages to find the pinned message ID + const pinnedEndpoint = `/chats/${chatId}/pinnedMessages` + const pinnedResult = await this.makeTeamsRequest(pinnedEndpoint) + + const pinnedMessage = pinnedResult.value?.find((pm: any) => pm.message?.id === messageId) + if (!pinnedMessage) { + throw new Error('Message is not pinned in this chat') + } + + const endpoint = `/chats/${chatId}/pinnedMessages/${pinnedMessage.id}` + await this.makeTeamsRequest(endpoint, 'DELETE') + + return this.formatResponse( + { + success: true, + message: 'Message unpinned successfully' + }, + params + ) + } catch (error) { + return this.formatResponse(`Error unpinning message: ${error}`, params) + } + } +} + +// CHAT MESSAGE TOOLS + +class ListMessagesTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'list_messages', + description: 'List messages in a chat or channel', + schema: z.object({ + chatChannelId: z.string().describe('ID of the chat or channel to list messages from'), + teamId: z.string().optional().describe('ID of the team (required for channel messages)'), + maxResults: z.number().optional().default(50).describe('Maximum number of messages to return') + }), + baseUrl: BASE_URL, + method: 'GET', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { chatChannelId, teamId, maxResults = 50 } = params + + if (!chatChannelId) { + throw new Error('Chat or Channel ID is required') + } + + try { + let endpoint: string + if (teamId) { + // Channel messages + endpoint = `/teams/${teamId}/channels/${chatChannelId}/messages?$top=${maxResults}` + } else { + // Chat messages + endpoint = `/chats/${chatChannelId}/messages?$top=${maxResults}` + } + + const result = await this.makeTeamsRequest(endpoint) + + return this.formatResponse( + { + success: true, + messages: result.value || [], + count: result.value?.length || 0, + context: teamId ? 'channel' : 'chat' + }, + params + ) + } catch (error) { + return this.formatResponse(`Error listing messages: ${error}`, params) + } + } +} + +class GetMessageTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'get_message', + description: 'Get details of a specific message', + schema: z.object({ + chatChannelId: z.string().describe('ID of the chat or channel'), + teamId: z.string().optional().describe('ID of the team (required for channel messages)'), + messageId: z.string().describe('ID of the message to retrieve') + }), + baseUrl: BASE_URL, + method: 'GET', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { chatChannelId, teamId, messageId } = params + + if (!chatChannelId || !messageId) { + throw new Error('Chat/Channel ID and Message ID are required') + } + + try { + let endpoint: string + if (teamId) { + // Channel message + endpoint = `/teams/${teamId}/channels/${chatChannelId}/messages/${messageId}` + } else { + // Chat message + endpoint = `/chats/${chatChannelId}/messages/${messageId}` + } + + const result = await this.makeTeamsRequest(endpoint) + + return this.formatResponse( + { + success: true, + message: result, + context: teamId ? 'channel' : 'chat' + }, + params + ) + } catch (error) { + return this.formatResponse(`Error getting message: ${error}`, params) + } + } +} + +class SendMessageTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'send_message', + description: 'Send a message to a chat or channel', + schema: z.object({ + chatChannelId: z.string().describe('ID of the chat or channel to send message to'), + teamId: z.string().optional().describe('ID of the team (required for channel messages)'), + messageBody: z.string().describe('Content of the message'), + contentType: z.enum(['text', 'html']).optional().default('text').describe('Content type of the message') + }), + baseUrl: BASE_URL, + method: 'POST', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { chatChannelId, teamId, messageBody, contentType = 'text' } = params + + if (!chatChannelId || !messageBody) { + throw new Error('Chat/Channel ID and Message Body are required') + } + + try { + const body = { + body: { + contentType, + content: messageBody + } + } + + let endpoint: string + if (teamId) { + // Channel message + endpoint = `/teams/${teamId}/channels/${chatChannelId}/messages` + } else { + // Chat message + endpoint = `/chats/${chatChannelId}/messages` + } + + const result = await this.makeTeamsRequest(endpoint, 'POST', body) + + return this.formatResponse( + { + success: true, + message: result, + context: teamId ? 'channel' : 'chat', + messageText: 'Message sent successfully' + }, + params + ) + } catch (error) { + return this.formatResponse(`Error sending message: ${error}`, params) + } + } +} + +class UpdateMessageTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'update_message', + description: 'Update an existing message', + schema: z.object({ + chatChannelId: z.string().describe('ID of the chat or channel'), + teamId: z.string().optional().describe('ID of the team (required for channel messages)'), + messageId: z.string().describe('ID of the message to update') + }), + baseUrl: BASE_URL, + method: 'PATCH', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { chatChannelId, teamId, messageId } = params + + if (!chatChannelId || !messageId) { + throw new Error('Chat/Channel ID and Message ID are required') + } + + try { + // Note: Message update is primarily for policy violations in Teams + const body = { + policyViolation: null + } + + let endpoint: string + if (teamId) { + // Channel message + endpoint = `/teams/${teamId}/channels/${chatChannelId}/messages/${messageId}` + } else { + // Chat message + endpoint = `/chats/${chatChannelId}/messages/${messageId}` + } + + await this.makeTeamsRequest(endpoint, 'PATCH', body) + + return this.formatResponse( + { + success: true, + message: 'Message updated successfully', + context: teamId ? 'channel' : 'chat' + }, + params + ) + } catch (error) { + return this.formatResponse(`Error updating message: ${error}`, params) + } + } +} + +class DeleteMessageTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'delete_message', + description: 'Delete a message', + schema: z.object({ + chatChannelId: z.string().describe('ID of the chat or channel'), + teamId: z.string().optional().describe('ID of the team (required for channel messages)'), + messageId: z.string().describe('ID of the message to delete') + }), + baseUrl: BASE_URL, + method: 'DELETE', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { chatChannelId, teamId, messageId } = params + + if (!chatChannelId || !messageId) { + throw new Error('Chat/Channel ID and Message ID are required') + } + + try { + let endpoint: string + if (teamId) { + // Channel message - use soft delete + endpoint = `/teams/${teamId}/channels/${chatChannelId}/messages/${messageId}/softDelete` + } else { + // Chat message - use soft delete + endpoint = `/chats/${chatChannelId}/messages/${messageId}/softDelete` + } + + await this.makeTeamsRequest(endpoint, 'POST', {}) + + return this.formatResponse( + { + success: true, + message: 'Message deleted successfully', + context: teamId ? 'channel' : 'chat' + }, + params + ) + } catch (error) { + return this.formatResponse(`Error deleting message: ${error}`, params) + } + } +} + +class ReplyToMessageTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'reply_to_message', + description: 'Reply to a message in a chat or channel', + schema: z.object({ + chatChannelId: z.string().describe('ID of the chat or channel'), + teamId: z.string().optional().describe('ID of the team (required for channel messages)'), + messageId: z.string().describe('ID of the message to reply to'), + replyBody: z.string().describe('Content of the reply'), + contentType: z.enum(['text', 'html']).optional().default('text').describe('Content type of the reply') + }), + baseUrl: BASE_URL, + method: 'POST', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { chatChannelId, teamId, messageId, replyBody, contentType = 'text' } = params + + if (!chatChannelId || !messageId || !replyBody) { + throw new Error('Chat/Channel ID, Message ID, and Reply Body are required') + } + + try { + const body = { + body: { + contentType, + content: replyBody + } + } + + let endpoint: string + if (teamId) { + // Channel message reply + endpoint = `/teams/${teamId}/channels/${chatChannelId}/messages/${messageId}/replies` + } else { + // For chat messages, replies are just new messages + endpoint = `/chats/${chatChannelId}/messages` + } + + const result = await this.makeTeamsRequest(endpoint, 'POST', body) + + return this.formatResponse( + { + success: true, + reply: result, + message: 'Reply sent successfully', + context: teamId ? 'channel' : 'chat' + }, + params + ) + } catch (error) { + return this.formatResponse(`Error replying to message: ${error}`, params) + } + } +} + +class SetReactionTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'set_reaction', + description: 'Set a reaction to a message', + schema: z.object({ + chatChannelId: z.string().describe('ID of the chat or channel'), + teamId: z.string().optional().describe('ID of the team (required for channel messages)'), + messageId: z.string().describe('ID of the message to react to'), + reactionType: z + .enum(['like', 'heart', 'laugh', 'surprised', 'sad', 'angry']) + .optional() + .default('like') + .describe('Type of reaction to set') + }), + baseUrl: BASE_URL, + method: 'POST', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { chatChannelId, teamId, messageId, reactionType = 'like' } = params + + if (!chatChannelId || !messageId) { + throw new Error('Chat/Channel ID and Message ID are required') + } + + try { + let endpoint: string + if (teamId) { + // Channel message + endpoint = `/teams/${teamId}/channels/${chatChannelId}/messages/${messageId}/setReaction` + } else { + // Chat message + endpoint = `/chats/${chatChannelId}/messages/${messageId}/setReaction` + } + + const body = { + reactionType + } + + await this.makeTeamsRequest(endpoint, 'POST', body) + + return this.formatResponse( + { + success: true, + message: `Reaction "${reactionType}" set successfully`, + context: teamId ? 'channel' : 'chat' + }, + params + ) + } catch (error) { + return this.formatResponse(`Error setting reaction: ${error}`, params) + } + } +} + +class UnsetReactionTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'unset_reaction', + description: 'Remove a reaction from a message', + schema: z.object({ + chatChannelId: z.string().describe('ID of the chat or channel'), + teamId: z.string().optional().describe('ID of the team (required for channel messages)'), + messageId: z.string().describe('ID of the message to remove reaction from'), + reactionType: z + .enum(['like', 'heart', 'laugh', 'surprised', 'sad', 'angry']) + .optional() + .default('like') + .describe('Type of reaction to remove') + }), + baseUrl: BASE_URL, + method: 'POST', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { chatChannelId, teamId, messageId, reactionType = 'like' } = params + + if (!chatChannelId || !messageId) { + throw new Error('Chat/Channel ID and Message ID are required') + } + + try { + let endpoint: string + if (teamId) { + // Channel message + endpoint = `/teams/${teamId}/channels/${chatChannelId}/messages/${messageId}/unsetReaction` + } else { + // Chat message + endpoint = `/chats/${chatChannelId}/messages/${messageId}/unsetReaction` + } + + const body = { + reactionType + } + + await this.makeTeamsRequest(endpoint, 'POST', body) + + return this.formatResponse( + { + success: true, + message: `Reaction "${reactionType}" removed successfully`, + context: teamId ? 'channel' : 'chat' + }, + params + ) + } catch (error) { + return this.formatResponse(`Error unsetting reaction: ${error}`, params) + } + } +} + +class GetAllMessagesTool extends BaseTeamsTool { + constructor(args: { accessToken?: string; defaultParams?: any }) { + const toolInput: DynamicStructuredToolInput = { + name: 'get_all_messages', + description: 'Get messages across all chats and channels for the user', + schema: z.object({ + maxResults: z.number().optional().default(50).describe('Maximum number of messages to return') + }), + baseUrl: BASE_URL, + method: 'GET', + headers: {} + } + + super({ ...toolInput, accessToken: args.accessToken, defaultParams: args.defaultParams }) + } + + protected async _call(arg: any): Promise { + const params = { ...arg, ...this.defaultParams } + const { maxResults = 50 } = params + + try { + // Get messages from all chats + const chatEndpoint = `/me/chats/getAllMessages?$top=${maxResults}` + const chatResult = await this.makeTeamsRequest(chatEndpoint) + + return this.formatResponse( + { + success: true, + messages: chatResult.value || [], + count: chatResult.value?.length || 0, + source: 'all_chats_and_channels' + }, + params + ) + } catch (error) { + return this.formatResponse(`Error getting all messages: ${error}`, params) + } + } +} + +// Main function to create Teams tools +export function createTeamsTools(options: TeamsToolOptions): DynamicStructuredTool[] { + const tools: DynamicStructuredTool[] = [] + const actions = options.actions || [] + const accessToken = options.accessToken || '' + const defaultParams = options.defaultParams || {} + + // Channel tools + if (actions.includes('listChannels')) { + const listTool = new ListChannelsTool({ + accessToken, + defaultParams: defaultParams.listChannels + }) + tools.push(listTool) + } + + if (actions.includes('getChannel')) { + const getTool = new GetChannelTool({ + accessToken, + defaultParams: defaultParams.getChannel + }) + tools.push(getTool) + } + + if (actions.includes('createChannel')) { + const createTool = new CreateChannelTool({ + accessToken, + defaultParams: defaultParams.createChannel + }) + tools.push(createTool) + } + + if (actions.includes('updateChannel')) { + const updateTool = new UpdateChannelTool({ + accessToken, + defaultParams: defaultParams.updateChannel + }) + tools.push(updateTool) + } + + if (actions.includes('deleteChannel')) { + const deleteTool = new DeleteChannelTool({ + accessToken, + defaultParams: defaultParams.deleteChannel + }) + tools.push(deleteTool) + } + + if (actions.includes('archiveChannel')) { + const archiveTool = new ArchiveChannelTool({ + accessToken, + defaultParams: defaultParams.archiveChannel + }) + tools.push(archiveTool) + } + + if (actions.includes('unarchiveChannel')) { + const unarchiveTool = new UnarchiveChannelTool({ + accessToken, + defaultParams: defaultParams.unarchiveChannel + }) + tools.push(unarchiveTool) + } + + if (actions.includes('listChannelMembers')) { + const listMembersTool = new ListChannelMembersTool({ + accessToken, + defaultParams: defaultParams.listChannelMembers + }) + tools.push(listMembersTool) + } + + if (actions.includes('addChannelMember')) { + const addMemberTool = new AddChannelMemberTool({ + accessToken, + defaultParams: defaultParams.addChannelMember + }) + tools.push(addMemberTool) + } + + if (actions.includes('removeChannelMember')) { + const removeMemberTool = new RemoveChannelMemberTool({ + accessToken, + defaultParams: defaultParams.removeChannelMember + }) + tools.push(removeMemberTool) + } + + // Chat tools + if (actions.includes('listChats')) { + const listTool = new ListChatsTool({ + accessToken, + defaultParams: defaultParams.listChats + }) + tools.push(listTool) + } + + if (actions.includes('getChat')) { + const getTool = new GetChatTool({ + accessToken, + defaultParams: defaultParams.getChat + }) + tools.push(getTool) + } + + if (actions.includes('createChat')) { + const createTool = new CreateChatTool({ + accessToken, + defaultParams: defaultParams.createChat + }) + tools.push(createTool) + } + + if (actions.includes('updateChat')) { + const updateTool = new UpdateChatTool({ + accessToken, + defaultParams: defaultParams.updateChat + }) + tools.push(updateTool) + } + + if (actions.includes('deleteChat')) { + const deleteTool = new DeleteChatTool({ + accessToken, + defaultParams: defaultParams.deleteChat + }) + tools.push(deleteTool) + } + + if (actions.includes('listChatMembers')) { + const listMembersTool = new ListChatMembersTool({ + accessToken, + defaultParams: defaultParams.listChatMembers + }) + tools.push(listMembersTool) + } + + if (actions.includes('addChatMember')) { + const addMemberTool = new AddChatMemberTool({ + accessToken, + defaultParams: defaultParams.addChatMember + }) + tools.push(addMemberTool) + } + + if (actions.includes('removeChatMember')) { + const removeMemberTool = new RemoveChatMemberTool({ + accessToken, + defaultParams: defaultParams.removeChatMember + }) + tools.push(removeMemberTool) + } + + if (actions.includes('pinMessage')) { + const pinTool = new PinMessageTool({ + accessToken, + defaultParams: defaultParams.pinMessage + }) + tools.push(pinTool) + } + + if (actions.includes('unpinMessage')) { + const unpinTool = new UnpinMessageTool({ + accessToken, + defaultParams: defaultParams.unpinMessage + }) + tools.push(unpinTool) + } + + // Chat message tools + if (actions.includes('listMessages')) { + const listTool = new ListMessagesTool({ + accessToken, + defaultParams: defaultParams.listMessages + }) + tools.push(listTool) + } + + if (actions.includes('getMessage')) { + const getTool = new GetMessageTool({ + accessToken, + defaultParams: defaultParams.getMessage + }) + tools.push(getTool) + } + + if (actions.includes('sendMessage')) { + const sendTool = new SendMessageTool({ + accessToken, + defaultParams: defaultParams.sendMessage + }) + tools.push(sendTool) + } + + if (actions.includes('updateMessage')) { + const updateTool = new UpdateMessageTool({ + accessToken, + defaultParams: defaultParams.updateMessage + }) + tools.push(updateTool) + } + + if (actions.includes('deleteMessage')) { + const deleteTool = new DeleteMessageTool({ + accessToken, + defaultParams: defaultParams.deleteMessage + }) + tools.push(deleteTool) + } + + if (actions.includes('replyToMessage')) { + const replyTool = new ReplyToMessageTool({ + accessToken, + defaultParams: defaultParams.replyToMessage + }) + tools.push(replyTool) + } + + if (actions.includes('setReaction')) { + const reactionTool = new SetReactionTool({ + accessToken, + defaultParams: defaultParams.setReaction + }) + tools.push(reactionTool) + } + + if (actions.includes('unsetReaction')) { + const unsetReactionTool = new UnsetReactionTool({ + accessToken, + defaultParams: defaultParams.unsetReaction + }) + tools.push(unsetReactionTool) + } + + if (actions.includes('getAllMessages')) { + const getAllTool = new GetAllMessagesTool({ + accessToken, + defaultParams: defaultParams.getAllMessages + }) + tools.push(getAllTool) + } + + return tools +} diff --git a/packages/components/nodes/tools/MicrosoftTeams/teams.svg b/packages/components/nodes/tools/MicrosoftTeams/teams.svg new file mode 100644 index 00000000..f3a03a3b --- /dev/null +++ b/packages/components/nodes/tools/MicrosoftTeams/teams.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/package.json b/packages/components/package.json index ba6c5747..646b4e38 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/packages/components/src/agentflowv2Generator.ts b/packages/components/src/agentflowv2Generator.ts index a8033278..a9e4600d 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.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 + } 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 0bda4021..022a5be0 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 1e700839..c8e9fe00 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 63633e7b..4941a007 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 00000000..b5c5f571 --- /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 00000000..6b360974 --- /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 f16c96b1..f5126e97 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 3d3d6796..52cfa402 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 00000000..6546a504 --- /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 dae926a8..6bafd558 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 + // 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.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 + } 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 abd025a6..fecdb30a 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 && ( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7dc0a289..31390b5b 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 @@ -7580,6 +7586,10 @@ packages: resolution: { integrity: sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A== } engines: { node: '>=8.9' } + adler-32@1.3.1: + resolution: { integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A== } + engines: { node: '>=0.8' } + adm-zip@0.5.16: resolution: { integrity: sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ== } engines: { node: '>=12.0' } @@ -8462,6 +8472,10 @@ packages: ccount@2.0.1: resolution: { integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg== } + cfb@1.2.2: + resolution: { integrity: sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA== } + engines: { node: '>=0.8' } + chalk@1.1.3: resolution: { integrity: sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A== } engines: { node: '>=0.10.0' } @@ -8724,6 +8738,10 @@ packages: codemirror@6.0.1: resolution: { integrity: sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg== } + codepage@1.15.0: + resolution: { integrity: sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA== } + engines: { node: '>=0.8' } + codsen-utils@1.6.4: resolution: { integrity: sha512-PDyvQ5f2PValmqZZIJATimcokDt4JjIev8cKbZgEOoZm+U1IJDYuLeTcxZPQdep99R/X0RIlQ6ReQgPOVnPbNw== } engines: { node: '>=14.18.0' } @@ -8873,6 +8891,10 @@ packages: resolution: { integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== } engines: { '0': node >= 0.8 } + concat-stream@2.0.0: + resolution: { integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A== } + engines: { '0': node >= 6.0 } + concurrently@7.6.0: resolution: { integrity: sha512-BKtRgvcJGeZ4XttiDiNcFiRlxoAeZOseqUvyYRUp/Vtd+9p1ULmeoSqGsDA+2ivdeDFpqrJvGvmI+StKfKl5hw== } engines: { node: ^12.20.0 || ^14.13.0 || >=16.0.0 } @@ -9014,6 +9036,11 @@ packages: resolution: { integrity: sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA== } engines: { node: '>=10.0.0' } + crc-32@1.2.2: + resolution: { integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ== } + engines: { node: '>=0.8' } + hasBin: true + create-jest@29.7.0: resolution: { integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q== } engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } @@ -10653,6 +10680,10 @@ packages: resolution: { integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== } engines: { node: '>= 0.6' } + frac@1.1.2: + resolution: { integrity: sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA== } + engines: { node: '>=0.8' } + fraction.js@4.3.7: resolution: { integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew== } @@ -13819,6 +13850,10 @@ packages: engines: { node: '>=12.0.0' } hasBin: true + officeparser@5.1.1: + resolution: { integrity: sha512-trBCPmYQDFUCmch6YBxHhMFkDyhTl+vG8PDQHPOwRyeCDKnrrKpph2W7og7hg5T5RRF0yeyaOMasN7GZWbYuCA== } + hasBin: true + ollama@0.5.11: resolution: { integrity: sha512-lDAKcpmBU3VAOGF05NcQipHNKTdpKfAHpZ7bjCsElkUkmX7SNZImi6lwIxz/l1zQtLq0S3wuLneRuiXxX2KIew== } @@ -16357,6 +16392,10 @@ packages: resolution: { integrity: sha512-dANP1AyJTI503H0/kXwRza+7QxDB3BqeFvEKTF4MI9lQcBe8JbRUQTKVIGzGABJCwBovEYavZ2Qsdm/s8XKz8A== } hasBin: true + ssf@0.11.2: + resolution: { integrity: sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g== } + engines: { node: '>=0.8' } + ssh2@1.16.0: resolution: { integrity: sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg== } engines: { node: '>=10.16.0' } @@ -17995,10 +18034,18 @@ packages: resolution: { integrity: sha512-OwbxKaOlESDi01mC9rkM0dQqQt2I8DAUMRLZ/HpbwvDXm85IryEHgoogy5fziQy38PntgZsLlhAYHz//UPHZ5w== } engines: { node: '>= 12.0.0' } + wmf@1.0.2: + resolution: { integrity: sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw== } + engines: { node: '>=0.8' } + word-wrap@1.2.5: resolution: { integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== } engines: { node: '>=0.10.0' } + word@0.3.0: + resolution: { integrity: sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA== } + engines: { node: '>=0.8' } + wordwrap@1.0.0: resolution: { integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== } @@ -18186,6 +18233,11 @@ packages: resolution: { integrity: sha512-HY4G725+IDQr16N8XOjAms5qJGArdJaWIuC7Q7A8UXIwj2mifqnPXephazyL7sIkQPvmEoPX3E0v2yFv6hQUNg== } engines: { node: '>=4' } + xlsx@0.18.5: + resolution: { integrity: sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ== } + engines: { node: '>=0.8' } + hasBin: true + xml-name-validator@3.0.0: resolution: { integrity: sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== } @@ -18278,6 +18330,10 @@ packages: yauzl@2.10.0: resolution: { integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g== } + yauzl@3.2.0: + resolution: { integrity: sha512-Ow9nuGZE+qp1u4JIPvg+uCiUr7xGQWdff7JQSk5VGYTAZMDe2q8lxJ10ygv10qmSj031Ty/6FNJpLO4o1Sgc+w== } + engines: { node: '>=12' } + yeoman-environment@3.19.3: resolution: { integrity: sha512-/+ODrTUHtlDPRH9qIC0JREH8+7nsRcjDl3Bxn2Xo/rvAaVvixH5275jHwg0C85g4QsF4P6M2ojfScPPAl+pLAg== } engines: { node: '>=12.10.0' } @@ -23074,7 +23130,7 @@ snapshots: - encoding - openai - '@langchain/community@0.3.40(@aws-crypto/sha256-js@5.2.0)(@aws-sdk/client-bedrock-agent-runtime@3.755.0)(@aws-sdk/client-bedrock-runtime@3.422.0)(@aws-sdk/client-dynamodb@3.529.1)(@aws-sdk/client-kendra@3.750.0)(@aws-sdk/client-s3@3.529.1)(@aws-sdk/credential-provider-node@3.529.1)(@browserbasehq/sdk@2.0.0(encoding@0.1.13))(@browserbasehq/stagehand@1.9.0(@playwright/test@1.49.1)(bufferutil@4.0.8)(deepmerge@4.3.1)(dotenv@16.4.5)(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(utf-8-validate@6.0.4)(zod@3.22.4))(@datastax/astra-db-ts@1.5.0)(@elastic/elasticsearch@8.12.2)(@getzep/zep-cloud@1.0.7(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(langchain@0.3.6(@langchain/anthropic@0.3.14(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13))(@langchain/aws@0.1.4(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/cohere@0.0.7(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/google-genai@0.2.3(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(zod@3.22.4))(@langchain/google-vertexai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(zod@3.22.4))(@langchain/groq@0.1.2(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@langchain/mistralai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/ollama@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(axios@1.7.9)(cheerio@1.0.0-rc.12)(encoding@0.1.13)(handlebars@4.7.8)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(typeorm@0.3.20(ioredis@5.3.2)(mongodb@6.3.0(gcp-metadata@6.1.0(encoding@0.1.13))(socks@2.8.1))(mysql2@3.11.4)(pg@8.11.3)(redis@4.6.13)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.4.6)(@types/node@22.5.4)(typescript@5.5.2)))(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))))(@getzep/zep-js@0.9.0)(@gomomento/sdk-core@1.68.1)(@gomomento/sdk@1.68.1(encoding@0.1.13))(@google-ai/generativelanguage@2.6.0(encoding@0.1.13))(@google-cloud/storage@7.16.0(encoding@0.1.13))(@huggingface/inference@2.6.4)(@ibm-cloud/watsonx-ai@1.2.0)(@langchain/anthropic@0.3.14(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13))(@langchain/aws@0.1.4(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/cohere@0.0.7(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/google-genai@0.2.3(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(zod@3.22.4))(@langchain/google-vertexai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(zod@3.22.4))(@langchain/groq@0.1.2(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@langchain/mistralai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/ollama@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@mendable/firecrawl-js@1.25.1)(@notionhq/client@2.2.14(encoding@0.1.13))(@opensearch-project/opensearch@1.2.0)(@pinecone-database/pinecone@4.0.0)(@qdrant/js-client-rest@1.9.0(typescript@5.5.2))(@smithy/eventstream-codec@4.0.1)(@smithy/protocol-http@5.0.1)(@smithy/signature-v4@5.0.1)(@smithy/util-utf8@4.0.0)(@supabase/supabase-js@2.39.8(bufferutil@4.0.8)(utf-8-validate@6.0.4))(@upstash/redis@1.22.1(encoding@0.1.13))(@upstash/vector@1.1.5)(@zilliz/milvus2-sdk-node@2.3.5)(apify-client@2.9.3)(assemblyai@4.3.2(bufferutil@4.0.8)(utf-8-validate@6.0.4))(axios@1.7.9)(cheerio@1.0.0-rc.12)(chromadb@1.10.3(@google/generative-ai@0.24.0)(cohere-ai@7.10.0(encoding@0.1.13))(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(cohere-ai@7.10.0(encoding@0.1.13))(crypto-js@4.2.0)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(google-auth-library@9.6.3(encoding@0.1.13))(handlebars@4.7.8)(html-to-text@9.0.5)(ibm-cloud-sdk-core@5.1.0)(ignore@5.3.1)(ioredis@5.3.2)(jsdom@22.1.0(bufferutil@4.0.8)(canvas@2.11.2(encoding@0.1.13))(utf-8-validate@6.0.4))(jsonwebtoken@9.0.2)(lodash@4.17.21)(lunary@0.7.12(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(react@18.2.0))(mammoth@1.7.0)(mem0ai@2.1.16(@anthropic-ai/sdk@0.37.0(encoding@0.1.13))(@google/genai@0.7.0(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4))(@mistralai/mistralai@0.1.3(encoding@0.1.13))(@qdrant/js-client-rest@1.9.0(typescript@5.5.2))(@supabase/supabase-js@2.39.8(bufferutil@4.0.8)(utf-8-validate@6.0.4))(@types/jest@29.5.14)(@types/pg@8.11.2)(@types/sqlite3@3.1.11)(encoding@0.1.13)(groq-sdk@0.5.0(encoding@0.1.13))(neo4j-driver@5.27.0)(ollama@0.5.11)(pg@8.11.3)(redis@4.6.13)(sqlite3@5.1.7)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(mongodb@6.3.0(gcp-metadata@6.1.0(encoding@0.1.13))(socks@2.8.1))(mysql2@3.11.4)(neo4j-driver@5.27.0)(notion-to-md@3.1.1(encoding@0.1.13))(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(pdf-parse@1.1.1)(pg@8.11.3)(playwright@1.42.1)(portkey-ai@0.1.16)(puppeteer@20.9.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.5.2)(utf-8-validate@6.0.4))(pyodide@0.25.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(redis@4.6.13)(replicate@0.31.1)(srt-parser-2@1.2.3)(typeorm@0.3.20(ioredis@5.3.2)(mongodb@6.3.0(gcp-metadata@6.1.0(encoding@0.1.13))(socks@2.8.1))(mysql2@3.11.4)(pg@8.11.3)(redis@4.6.13)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.4.6)(@types/node@22.5.4)(typescript@5.5.2)))(weaviate-ts-client@1.6.0(encoding@0.1.13)(graphql@16.8.1))(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@langchain/community@0.3.40(@aws-crypto/sha256-js@5.2.0)(@aws-sdk/client-bedrock-agent-runtime@3.755.0)(@aws-sdk/client-bedrock-runtime@3.422.0)(@aws-sdk/client-dynamodb@3.529.1)(@aws-sdk/client-kendra@3.750.0)(@aws-sdk/client-s3@3.529.1)(@aws-sdk/credential-provider-node@3.529.1)(@browserbasehq/sdk@2.0.0(encoding@0.1.13))(@browserbasehq/stagehand@1.9.0(@playwright/test@1.49.1)(bufferutil@4.0.8)(deepmerge@4.3.1)(dotenv@16.4.5)(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(utf-8-validate@6.0.4)(zod@3.22.4))(@datastax/astra-db-ts@1.5.0)(@elastic/elasticsearch@8.12.2)(@getzep/zep-cloud@1.0.7(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(langchain@0.3.6(@langchain/anthropic@0.3.14(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13))(@langchain/aws@0.1.4(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/cohere@0.0.7(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/google-genai@0.2.3(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(zod@3.22.4))(@langchain/google-vertexai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(zod@3.22.4))(@langchain/groq@0.1.2(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@langchain/mistralai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/ollama@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(axios@1.7.9)(cheerio@1.0.0-rc.12)(encoding@0.1.13)(handlebars@4.7.8)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(typeorm@0.3.20(ioredis@5.3.2)(mongodb@6.3.0(gcp-metadata@6.1.0(encoding@0.1.13))(socks@2.8.1))(mysql2@3.11.4)(pg@8.11.3)(redis@4.6.13)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.4.6)(@types/node@22.5.4)(typescript@5.5.2)))(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))))(@getzep/zep-js@0.9.0)(@gomomento/sdk-core@1.68.1)(@gomomento/sdk@1.68.1(encoding@0.1.13))(@google-ai/generativelanguage@2.6.0(encoding@0.1.13))(@google-cloud/storage@7.16.0(encoding@0.1.13))(@huggingface/inference@2.6.4)(@ibm-cloud/watsonx-ai@1.2.0)(@langchain/anthropic@0.3.14(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13))(@langchain/aws@0.1.4(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/cohere@0.0.7(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/google-genai@0.2.3(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(zod@3.22.4))(@langchain/google-vertexai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(zod@3.22.4))(@langchain/groq@0.1.2(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@langchain/mistralai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/ollama@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@mendable/firecrawl-js@1.25.1)(@notionhq/client@2.2.14(encoding@0.1.13))(@opensearch-project/opensearch@1.2.0)(@pinecone-database/pinecone@4.0.0)(@qdrant/js-client-rest@1.9.0(typescript@5.5.2))(@smithy/eventstream-codec@4.0.1)(@smithy/protocol-http@5.0.1)(@smithy/signature-v4@5.0.1)(@smithy/util-utf8@4.0.0)(@supabase/supabase-js@2.39.8(bufferutil@4.0.8)(utf-8-validate@6.0.4))(@upstash/redis@1.22.1(encoding@0.1.13))(@upstash/vector@1.1.5)(@zilliz/milvus2-sdk-node@2.3.5)(apify-client@2.9.3)(assemblyai@4.3.2(bufferutil@4.0.8)(utf-8-validate@6.0.4))(axios@1.7.9)(cheerio@1.0.0-rc.12)(chromadb@1.10.3(@google/generative-ai@0.24.0)(cohere-ai@7.10.0(encoding@0.1.13))(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(cohere-ai@7.10.0(encoding@0.1.13))(crypto-js@4.2.0)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(google-auth-library@9.6.3(encoding@0.1.13))(handlebars@4.7.8)(html-to-text@9.0.5)(ibm-cloud-sdk-core@5.1.0)(ignore@5.3.1)(ioredis@5.3.2)(jsdom@22.1.0(bufferutil@4.0.8)(canvas@2.11.2(encoding@0.1.13))(utf-8-validate@6.0.4))(jsonwebtoken@9.0.2)(lodash@4.17.21)(lunary@0.7.12(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(react@18.2.0))(mammoth@1.7.0)(mem0ai@2.1.16(@anthropic-ai/sdk@0.37.0(encoding@0.1.13))(@google/genai@0.7.0(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4))(@mistralai/mistralai@0.1.3(encoding@0.1.13))(@qdrant/js-client-rest@1.9.0(typescript@5.5.2))(@supabase/supabase-js@2.39.8(bufferutil@4.0.8)(utf-8-validate@6.0.4))(@types/jest@29.5.14)(@types/pg@8.11.2)(@types/sqlite3@3.1.11)(encoding@0.1.13)(groq-sdk@0.5.0(encoding@0.1.13))(neo4j-driver@5.27.0)(ollama@0.5.11)(pg@8.11.3)(redis@4.6.13)(sqlite3@5.1.7)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(mongodb@6.3.0(gcp-metadata@6.1.0(encoding@0.1.13))(socks@2.8.1))(mysql2@3.11.4)(neo4j-driver@5.27.0)(notion-to-md@3.1.1(encoding@0.1.13))(officeparser@5.1.1)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(pdf-parse@1.1.1)(pg@8.11.3)(playwright@1.42.1)(portkey-ai@0.1.16)(puppeteer@20.9.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.5.2)(utf-8-validate@6.0.4))(pyodide@0.25.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(redis@4.6.13)(replicate@0.31.1)(srt-parser-2@1.2.3)(typeorm@0.3.20(ioredis@5.3.2)(mongodb@6.3.0(gcp-metadata@6.1.0(encoding@0.1.13))(socks@2.8.1))(mysql2@3.11.4)(pg@8.11.3)(redis@4.6.13)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.4.6)(@types/node@22.5.4)(typescript@5.5.2)))(weaviate-ts-client@1.6.0(encoding@0.1.13)(graphql@16.8.1))(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: '@browserbasehq/stagehand': 1.9.0(@playwright/test@1.49.1)(bufferutil@4.0.8)(deepmerge@4.3.1)(dotenv@16.4.5)(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(utf-8-validate@6.0.4)(zod@3.22.4) '@ibm-cloud/watsonx-ai': 1.2.0 @@ -23145,6 +23201,7 @@ snapshots: mysql2: 3.11.4 neo4j-driver: 5.27.0 notion-to-md: 3.1.1(encoding@0.1.13) + officeparser: 5.1.1 pdf-parse: 1.1.1 pg: 8.11.3 playwright: 1.42.1 @@ -23416,9 +23473,9 @@ snapshots: - encoding - supports-color - '@mem0/community@0.0.1(@anthropic-ai/sdk@0.37.0(encoding@0.1.13))(@aws-crypto/sha256-js@5.2.0)(@aws-sdk/client-bedrock-agent-runtime@3.755.0)(@aws-sdk/client-bedrock-runtime@3.422.0)(@aws-sdk/client-dynamodb@3.529.1)(@aws-sdk/client-kendra@3.750.0)(@aws-sdk/client-s3@3.529.1)(@aws-sdk/credential-provider-node@3.529.1)(@browserbasehq/sdk@2.0.0(encoding@0.1.13))(@browserbasehq/stagehand@1.9.0(@playwright/test@1.49.1)(bufferutil@4.0.8)(deepmerge@4.3.1)(dotenv@16.4.5)(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(utf-8-validate@6.0.4)(zod@3.22.4))(@datastax/astra-db-ts@1.5.0)(@elastic/elasticsearch@8.12.2)(@getzep/zep-cloud@1.0.7(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(langchain@0.3.6(@langchain/anthropic@0.3.14(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13))(@langchain/aws@0.1.4(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/cohere@0.0.7(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/google-genai@0.2.3(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(zod@3.22.4))(@langchain/google-vertexai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(zod@3.22.4))(@langchain/groq@0.1.2(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@langchain/mistralai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/ollama@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(axios@1.7.9)(cheerio@1.0.0-rc.12)(encoding@0.1.13)(handlebars@4.7.8)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(typeorm@0.3.20(ioredis@5.3.2)(mongodb@6.3.0(gcp-metadata@6.1.0(encoding@0.1.13))(socks@2.8.1))(mysql2@3.11.4)(pg@8.11.3)(redis@4.6.13)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.4.6)(@types/node@22.5.4)(typescript@5.5.2)))(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))))(@getzep/zep-js@0.9.0)(@gomomento/sdk-core@1.68.1)(@gomomento/sdk@1.68.1(encoding@0.1.13))(@google-ai/generativelanguage@2.6.0(encoding@0.1.13))(@google-cloud/storage@7.16.0(encoding@0.1.13))(@google/genai@0.7.0(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4))(@huggingface/inference@2.6.4)(@ibm-cloud/watsonx-ai@1.2.0)(@langchain/anthropic@0.3.14(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13))(@langchain/aws@0.1.4(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/cohere@0.0.7(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/google-genai@0.2.3(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(zod@3.22.4))(@langchain/google-vertexai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(zod@3.22.4))(@langchain/groq@0.1.2(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@langchain/mistralai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/ollama@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@mendable/firecrawl-js@1.25.1)(@mistralai/mistralai@0.1.3(encoding@0.1.13))(@notionhq/client@2.2.14(encoding@0.1.13))(@opensearch-project/opensearch@1.2.0)(@pinecone-database/pinecone@4.0.0)(@qdrant/js-client-rest@1.9.0(typescript@5.5.2))(@smithy/eventstream-codec@4.0.1)(@smithy/protocol-http@5.0.1)(@smithy/signature-v4@5.0.1)(@smithy/util-utf8@4.0.0)(@supabase/supabase-js@2.39.8(bufferutil@4.0.8)(utf-8-validate@6.0.4))(@types/jest@29.5.14)(@types/pg@8.11.2)(@types/sqlite3@3.1.11)(@upstash/redis@1.22.1(encoding@0.1.13))(@upstash/vector@1.1.5)(@zilliz/milvus2-sdk-node@2.3.5)(apify-client@2.9.3)(assemblyai@4.3.2(bufferutil@4.0.8)(utf-8-validate@6.0.4))(cheerio@1.0.0-rc.12)(chromadb@1.10.3(@google/generative-ai@0.24.0)(cohere-ai@7.10.0(encoding@0.1.13))(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(cohere-ai@7.10.0(encoding@0.1.13))(crypto-js@4.2.0)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(google-auth-library@9.6.3(encoding@0.1.13))(groq-sdk@0.5.0(encoding@0.1.13))(handlebars@4.7.8)(html-to-text@9.0.5)(ibm-cloud-sdk-core@5.1.0)(ignore@5.3.1)(ioredis@5.3.2)(jsdom@22.1.0(bufferutil@4.0.8)(canvas@2.11.2(encoding@0.1.13))(utf-8-validate@6.0.4))(jsonwebtoken@9.0.2)(lodash@4.17.21)(lunary@0.7.12(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(react@18.2.0))(mammoth@1.7.0)(mongodb@6.3.0(gcp-metadata@6.1.0(encoding@0.1.13))(socks@2.8.1))(mysql2@3.11.4)(neo4j-driver@5.27.0)(notion-to-md@3.1.1(encoding@0.1.13))(ollama@0.5.11)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(pdf-parse@1.1.1)(pg@8.11.3)(playwright@1.42.1)(portkey-ai@0.1.16)(puppeteer@20.9.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.5.2)(utf-8-validate@6.0.4))(pyodide@0.25.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(redis@4.6.13)(replicate@0.31.1)(sqlite3@5.1.7)(srt-parser-2@1.2.3)(typeorm@0.3.20(ioredis@5.3.2)(mongodb@6.3.0(gcp-metadata@6.1.0(encoding@0.1.13))(socks@2.8.1))(mysql2@3.11.4)(pg@8.11.3)(redis@4.6.13)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.4.6)(@types/node@22.5.4)(typescript@5.5.2)))(weaviate-ts-client@1.6.0(encoding@0.1.13)(graphql@16.8.1))(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@mem0/community@0.0.1(@anthropic-ai/sdk@0.37.0(encoding@0.1.13))(@aws-crypto/sha256-js@5.2.0)(@aws-sdk/client-bedrock-agent-runtime@3.755.0)(@aws-sdk/client-bedrock-runtime@3.422.0)(@aws-sdk/client-dynamodb@3.529.1)(@aws-sdk/client-kendra@3.750.0)(@aws-sdk/client-s3@3.529.1)(@aws-sdk/credential-provider-node@3.529.1)(@browserbasehq/sdk@2.0.0(encoding@0.1.13))(@browserbasehq/stagehand@1.9.0(@playwright/test@1.49.1)(bufferutil@4.0.8)(deepmerge@4.3.1)(dotenv@16.4.5)(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(utf-8-validate@6.0.4)(zod@3.22.4))(@datastax/astra-db-ts@1.5.0)(@elastic/elasticsearch@8.12.2)(@getzep/zep-cloud@1.0.7(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(langchain@0.3.6(@langchain/anthropic@0.3.14(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13))(@langchain/aws@0.1.4(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/cohere@0.0.7(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/google-genai@0.2.3(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(zod@3.22.4))(@langchain/google-vertexai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(zod@3.22.4))(@langchain/groq@0.1.2(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@langchain/mistralai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/ollama@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(axios@1.7.9)(cheerio@1.0.0-rc.12)(encoding@0.1.13)(handlebars@4.7.8)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(typeorm@0.3.20(ioredis@5.3.2)(mongodb@6.3.0(gcp-metadata@6.1.0(encoding@0.1.13))(socks@2.8.1))(mysql2@3.11.4)(pg@8.11.3)(redis@4.6.13)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.4.6)(@types/node@22.5.4)(typescript@5.5.2)))(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))))(@getzep/zep-js@0.9.0)(@gomomento/sdk-core@1.68.1)(@gomomento/sdk@1.68.1(encoding@0.1.13))(@google-ai/generativelanguage@2.6.0(encoding@0.1.13))(@google-cloud/storage@7.16.0(encoding@0.1.13))(@google/genai@0.7.0(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4))(@huggingface/inference@2.6.4)(@ibm-cloud/watsonx-ai@1.2.0)(@langchain/anthropic@0.3.14(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13))(@langchain/aws@0.1.4(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/cohere@0.0.7(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/google-genai@0.2.3(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(zod@3.22.4))(@langchain/google-vertexai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(zod@3.22.4))(@langchain/groq@0.1.2(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@langchain/mistralai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/ollama@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@mendable/firecrawl-js@1.25.1)(@mistralai/mistralai@0.1.3(encoding@0.1.13))(@notionhq/client@2.2.14(encoding@0.1.13))(@opensearch-project/opensearch@1.2.0)(@pinecone-database/pinecone@4.0.0)(@qdrant/js-client-rest@1.9.0(typescript@5.5.2))(@smithy/eventstream-codec@4.0.1)(@smithy/protocol-http@5.0.1)(@smithy/signature-v4@5.0.1)(@smithy/util-utf8@4.0.0)(@supabase/supabase-js@2.39.8(bufferutil@4.0.8)(utf-8-validate@6.0.4))(@types/jest@29.5.14)(@types/pg@8.11.2)(@types/sqlite3@3.1.11)(@upstash/redis@1.22.1(encoding@0.1.13))(@upstash/vector@1.1.5)(@zilliz/milvus2-sdk-node@2.3.5)(apify-client@2.9.3)(assemblyai@4.3.2(bufferutil@4.0.8)(utf-8-validate@6.0.4))(cheerio@1.0.0-rc.12)(chromadb@1.10.3(@google/generative-ai@0.24.0)(cohere-ai@7.10.0(encoding@0.1.13))(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(cohere-ai@7.10.0(encoding@0.1.13))(crypto-js@4.2.0)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(google-auth-library@9.6.3(encoding@0.1.13))(groq-sdk@0.5.0(encoding@0.1.13))(handlebars@4.7.8)(html-to-text@9.0.5)(ibm-cloud-sdk-core@5.1.0)(ignore@5.3.1)(ioredis@5.3.2)(jsdom@22.1.0(bufferutil@4.0.8)(canvas@2.11.2(encoding@0.1.13))(utf-8-validate@6.0.4))(jsonwebtoken@9.0.2)(lodash@4.17.21)(lunary@0.7.12(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(react@18.2.0))(mammoth@1.7.0)(mongodb@6.3.0(gcp-metadata@6.1.0(encoding@0.1.13))(socks@2.8.1))(mysql2@3.11.4)(neo4j-driver@5.27.0)(notion-to-md@3.1.1(encoding@0.1.13))(officeparser@5.1.1)(ollama@0.5.11)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(pdf-parse@1.1.1)(pg@8.11.3)(playwright@1.42.1)(portkey-ai@0.1.16)(puppeteer@20.9.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.5.2)(utf-8-validate@6.0.4))(pyodide@0.25.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(redis@4.6.13)(replicate@0.31.1)(sqlite3@5.1.7)(srt-parser-2@1.2.3)(typeorm@0.3.20(ioredis@5.3.2)(mongodb@6.3.0(gcp-metadata@6.1.0(encoding@0.1.13))(socks@2.8.1))(mysql2@3.11.4)(pg@8.11.3)(redis@4.6.13)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.4.6)(@types/node@22.5.4)(typescript@5.5.2)))(weaviate-ts-client@1.6.0(encoding@0.1.13)(graphql@16.8.1))(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: - '@langchain/community': 0.3.40(@aws-crypto/sha256-js@5.2.0)(@aws-sdk/client-bedrock-agent-runtime@3.755.0)(@aws-sdk/client-bedrock-runtime@3.422.0)(@aws-sdk/client-dynamodb@3.529.1)(@aws-sdk/client-kendra@3.750.0)(@aws-sdk/client-s3@3.529.1)(@aws-sdk/credential-provider-node@3.529.1)(@browserbasehq/sdk@2.0.0(encoding@0.1.13))(@browserbasehq/stagehand@1.9.0(@playwright/test@1.49.1)(bufferutil@4.0.8)(deepmerge@4.3.1)(dotenv@16.4.5)(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(utf-8-validate@6.0.4)(zod@3.22.4))(@datastax/astra-db-ts@1.5.0)(@elastic/elasticsearch@8.12.2)(@getzep/zep-cloud@1.0.7(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(langchain@0.3.6(@langchain/anthropic@0.3.14(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13))(@langchain/aws@0.1.4(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/cohere@0.0.7(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/google-genai@0.2.3(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(zod@3.22.4))(@langchain/google-vertexai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(zod@3.22.4))(@langchain/groq@0.1.2(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@langchain/mistralai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/ollama@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(axios@1.7.9)(cheerio@1.0.0-rc.12)(encoding@0.1.13)(handlebars@4.7.8)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(typeorm@0.3.20(ioredis@5.3.2)(mongodb@6.3.0(gcp-metadata@6.1.0(encoding@0.1.13))(socks@2.8.1))(mysql2@3.11.4)(pg@8.11.3)(redis@4.6.13)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.4.6)(@types/node@22.5.4)(typescript@5.5.2)))(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))))(@getzep/zep-js@0.9.0)(@gomomento/sdk-core@1.68.1)(@gomomento/sdk@1.68.1(encoding@0.1.13))(@google-ai/generativelanguage@2.6.0(encoding@0.1.13))(@google-cloud/storage@7.16.0(encoding@0.1.13))(@huggingface/inference@2.6.4)(@ibm-cloud/watsonx-ai@1.2.0)(@langchain/anthropic@0.3.14(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13))(@langchain/aws@0.1.4(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/cohere@0.0.7(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/google-genai@0.2.3(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(zod@3.22.4))(@langchain/google-vertexai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(zod@3.22.4))(@langchain/groq@0.1.2(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@langchain/mistralai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/ollama@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@mendable/firecrawl-js@1.25.1)(@notionhq/client@2.2.14(encoding@0.1.13))(@opensearch-project/opensearch@1.2.0)(@pinecone-database/pinecone@4.0.0)(@qdrant/js-client-rest@1.9.0(typescript@5.5.2))(@smithy/eventstream-codec@4.0.1)(@smithy/protocol-http@5.0.1)(@smithy/signature-v4@5.0.1)(@smithy/util-utf8@4.0.0)(@supabase/supabase-js@2.39.8(bufferutil@4.0.8)(utf-8-validate@6.0.4))(@upstash/redis@1.22.1(encoding@0.1.13))(@upstash/vector@1.1.5)(@zilliz/milvus2-sdk-node@2.3.5)(apify-client@2.9.3)(assemblyai@4.3.2(bufferutil@4.0.8)(utf-8-validate@6.0.4))(axios@1.7.9)(cheerio@1.0.0-rc.12)(chromadb@1.10.3(@google/generative-ai@0.24.0)(cohere-ai@7.10.0(encoding@0.1.13))(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(cohere-ai@7.10.0(encoding@0.1.13))(crypto-js@4.2.0)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(google-auth-library@9.6.3(encoding@0.1.13))(handlebars@4.7.8)(html-to-text@9.0.5)(ibm-cloud-sdk-core@5.1.0)(ignore@5.3.1)(ioredis@5.3.2)(jsdom@22.1.0(bufferutil@4.0.8)(canvas@2.11.2(encoding@0.1.13))(utf-8-validate@6.0.4))(jsonwebtoken@9.0.2)(lodash@4.17.21)(lunary@0.7.12(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(react@18.2.0))(mammoth@1.7.0)(mem0ai@2.1.16(@anthropic-ai/sdk@0.37.0(encoding@0.1.13))(@google/genai@0.7.0(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4))(@mistralai/mistralai@0.1.3(encoding@0.1.13))(@qdrant/js-client-rest@1.9.0(typescript@5.5.2))(@supabase/supabase-js@2.39.8(bufferutil@4.0.8)(utf-8-validate@6.0.4))(@types/jest@29.5.14)(@types/pg@8.11.2)(@types/sqlite3@3.1.11)(encoding@0.1.13)(groq-sdk@0.5.0(encoding@0.1.13))(neo4j-driver@5.27.0)(ollama@0.5.11)(pg@8.11.3)(redis@4.6.13)(sqlite3@5.1.7)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(mongodb@6.3.0(gcp-metadata@6.1.0(encoding@0.1.13))(socks@2.8.1))(mysql2@3.11.4)(neo4j-driver@5.27.0)(notion-to-md@3.1.1(encoding@0.1.13))(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(pdf-parse@1.1.1)(pg@8.11.3)(playwright@1.42.1)(portkey-ai@0.1.16)(puppeteer@20.9.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.5.2)(utf-8-validate@6.0.4))(pyodide@0.25.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(redis@4.6.13)(replicate@0.31.1)(srt-parser-2@1.2.3)(typeorm@0.3.20(ioredis@5.3.2)(mongodb@6.3.0(gcp-metadata@6.1.0(encoding@0.1.13))(socks@2.8.1))(mysql2@3.11.4)(pg@8.11.3)(redis@4.6.13)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.4.6)(@types/node@22.5.4)(typescript@5.5.2)))(weaviate-ts-client@1.6.0(encoding@0.1.13)(graphql@16.8.1))(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@langchain/community': 0.3.40(@aws-crypto/sha256-js@5.2.0)(@aws-sdk/client-bedrock-agent-runtime@3.755.0)(@aws-sdk/client-bedrock-runtime@3.422.0)(@aws-sdk/client-dynamodb@3.529.1)(@aws-sdk/client-kendra@3.750.0)(@aws-sdk/client-s3@3.529.1)(@aws-sdk/credential-provider-node@3.529.1)(@browserbasehq/sdk@2.0.0(encoding@0.1.13))(@browserbasehq/stagehand@1.9.0(@playwright/test@1.49.1)(bufferutil@4.0.8)(deepmerge@4.3.1)(dotenv@16.4.5)(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(utf-8-validate@6.0.4)(zod@3.22.4))(@datastax/astra-db-ts@1.5.0)(@elastic/elasticsearch@8.12.2)(@getzep/zep-cloud@1.0.7(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(langchain@0.3.6(@langchain/anthropic@0.3.14(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13))(@langchain/aws@0.1.4(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/cohere@0.0.7(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/google-genai@0.2.3(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(zod@3.22.4))(@langchain/google-vertexai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(zod@3.22.4))(@langchain/groq@0.1.2(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@langchain/mistralai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/ollama@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(axios@1.7.9)(cheerio@1.0.0-rc.12)(encoding@0.1.13)(handlebars@4.7.8)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(typeorm@0.3.20(ioredis@5.3.2)(mongodb@6.3.0(gcp-metadata@6.1.0(encoding@0.1.13))(socks@2.8.1))(mysql2@3.11.4)(pg@8.11.3)(redis@4.6.13)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.4.6)(@types/node@22.5.4)(typescript@5.5.2)))(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))))(@getzep/zep-js@0.9.0)(@gomomento/sdk-core@1.68.1)(@gomomento/sdk@1.68.1(encoding@0.1.13))(@google-ai/generativelanguage@2.6.0(encoding@0.1.13))(@google-cloud/storage@7.16.0(encoding@0.1.13))(@huggingface/inference@2.6.4)(@ibm-cloud/watsonx-ai@1.2.0)(@langchain/anthropic@0.3.14(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13))(@langchain/aws@0.1.4(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/cohere@0.0.7(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(@langchain/google-genai@0.2.3(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(zod@3.22.4))(@langchain/google-vertexai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(zod@3.22.4))(@langchain/groq@0.1.2(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@langchain/mistralai@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@langchain/ollama@0.2.0(@langchain/core@0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))))(@mendable/firecrawl-js@1.25.1)(@notionhq/client@2.2.14(encoding@0.1.13))(@opensearch-project/opensearch@1.2.0)(@pinecone-database/pinecone@4.0.0)(@qdrant/js-client-rest@1.9.0(typescript@5.5.2))(@smithy/eventstream-codec@4.0.1)(@smithy/protocol-http@5.0.1)(@smithy/signature-v4@5.0.1)(@smithy/util-utf8@4.0.0)(@supabase/supabase-js@2.39.8(bufferutil@4.0.8)(utf-8-validate@6.0.4))(@upstash/redis@1.22.1(encoding@0.1.13))(@upstash/vector@1.1.5)(@zilliz/milvus2-sdk-node@2.3.5)(apify-client@2.9.3)(assemblyai@4.3.2(bufferutil@4.0.8)(utf-8-validate@6.0.4))(axios@1.7.9)(cheerio@1.0.0-rc.12)(chromadb@1.10.3(@google/generative-ai@0.24.0)(cohere-ai@7.10.0(encoding@0.1.13))(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)))(cohere-ai@7.10.0(encoding@0.1.13))(crypto-js@4.2.0)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(google-auth-library@9.6.3(encoding@0.1.13))(handlebars@4.7.8)(html-to-text@9.0.5)(ibm-cloud-sdk-core@5.1.0)(ignore@5.3.1)(ioredis@5.3.2)(jsdom@22.1.0(bufferutil@4.0.8)(canvas@2.11.2(encoding@0.1.13))(utf-8-validate@6.0.4))(jsonwebtoken@9.0.2)(lodash@4.17.21)(lunary@0.7.12(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(react@18.2.0))(mammoth@1.7.0)(mem0ai@2.1.16(@anthropic-ai/sdk@0.37.0(encoding@0.1.13))(@google/genai@0.7.0(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4))(@mistralai/mistralai@0.1.3(encoding@0.1.13))(@qdrant/js-client-rest@1.9.0(typescript@5.5.2))(@supabase/supabase-js@2.39.8(bufferutil@4.0.8)(utf-8-validate@6.0.4))(@types/jest@29.5.14)(@types/pg@8.11.2)(@types/sqlite3@3.1.11)(encoding@0.1.13)(groq-sdk@0.5.0(encoding@0.1.13))(neo4j-driver@5.27.0)(ollama@0.5.11)(pg@8.11.3)(redis@4.6.13)(sqlite3@5.1.7)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(mongodb@6.3.0(gcp-metadata@6.1.0(encoding@0.1.13))(socks@2.8.1))(mysql2@3.11.4)(neo4j-driver@5.27.0)(notion-to-md@3.1.1(encoding@0.1.13))(officeparser@5.1.1)(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4))(pdf-parse@1.1.1)(pg@8.11.3)(playwright@1.42.1)(portkey-ai@0.1.16)(puppeteer@20.9.0(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.5.2)(utf-8-validate@6.0.4))(pyodide@0.25.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(redis@4.6.13)(replicate@0.31.1)(srt-parser-2@1.2.3)(typeorm@0.3.20(ioredis@5.3.2)(mongodb@6.3.0(gcp-metadata@6.1.0(encoding@0.1.13))(socks@2.8.1))(mysql2@3.11.4)(pg@8.11.3)(redis@4.6.13)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.4.6)(@types/node@22.5.4)(typescript@5.5.2)))(weaviate-ts-client@1.6.0(encoding@0.1.13)(graphql@16.8.1))(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)) '@langchain/core': 0.3.37(openai@4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4)) axios: 1.7.9(debug@4.3.4) mem0ai: 2.1.16(@anthropic-ai/sdk@0.37.0(encoding@0.1.13))(@google/genai@0.7.0(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4))(@mistralai/mistralai@0.1.3(encoding@0.1.13))(@qdrant/js-client-rest@1.9.0(typescript@5.5.2))(@supabase/supabase-js@2.39.8(bufferutil@4.0.8)(utf-8-validate@6.0.4))(@types/jest@29.5.14)(@types/pg@8.11.2)(@types/sqlite3@3.1.11)(encoding@0.1.13)(groq-sdk@0.5.0(encoding@0.1.13))(neo4j-driver@5.27.0)(ollama@0.5.11)(pg@8.11.3)(redis@4.6.13)(sqlite3@5.1.7)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)) @@ -27632,6 +27689,8 @@ snapshots: loader-utils: 2.0.4 regex-parser: 2.3.0 + adler-32@1.3.1: {} + adm-zip@0.5.16: {} agent-base@6.0.2: @@ -28796,6 +28855,11 @@ snapshots: ccount@2.0.1: {} + cfb@1.2.2: + dependencies: + adler-32: 1.3.1 + crc-32: 1.2.2 + chalk@1.1.3: dependencies: ansi-styles: 2.2.1 @@ -29090,6 +29154,8 @@ snapshots: transitivePeerDependencies: - '@lezer/common' + codepage@1.15.0: {} + codsen-utils@1.6.4: dependencies: rfdc: 1.3.1 @@ -29228,6 +29294,13 @@ snapshots: readable-stream: 2.3.8 typedarray: 0.0.6 + concat-stream@2.0.0: + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 3.6.2 + typedarray: 0.0.6 + concurrently@7.6.0: dependencies: chalk: 4.1.2 @@ -29378,6 +29451,8 @@ snapshots: nan: 2.22.2 optional: true + crc-32@1.2.2: {} + create-jest@29.7.0(@types/node@22.5.4)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.4.6)(@types/node@22.5.4)(typescript@5.5.2)): dependencies: '@jest/types': 29.6.3 @@ -31591,6 +31666,8 @@ snapshots: forwarded@0.2.0: {} + frac@1.1.2: {} + fraction.js@4.3.7: {} fragment-cache@0.2.1: @@ -35936,6 +36013,14 @@ snapshots: - supports-color - typescript + officeparser@5.1.1: + dependencies: + '@xmldom/xmldom': 0.8.10 + concat-stream: 2.0.0 + file-type: 16.5.4 + node-ensure: 0.0.0 + yauzl: 3.2.0 + ollama@0.5.11: dependencies: whatwg-fetch: 3.6.20 @@ -39032,6 +39117,10 @@ snapshots: srt-parser-2@1.2.3: {} + ssf@0.11.2: + dependencies: + frac: 1.1.2 + ssh2@1.16.0: dependencies: asn1: 0.2.6 @@ -40919,8 +41008,12 @@ snapshots: triple-beam: 1.4.1 winston-transport: 4.7.0 + wmf@1.0.2: {} + word-wrap@1.2.5: {} + word@0.3.0: {} + wordwrap@1.0.0: {} workbox-background-sync@6.6.0: @@ -41223,6 +41316,16 @@ snapshots: execa: 0.2.2 titleize: 1.0.1 + xlsx@0.18.5: + dependencies: + adler-32: 1.3.1 + cfb: 1.2.2 + codepage: 1.15.0 + crc-32: 1.2.2 + ssf: 0.11.2 + wmf: 1.0.2 + word: 0.3.0 + xml-name-validator@3.0.0: {} xml-name-validator@4.0.0: {} @@ -41320,6 +41423,11 @@ snapshots: buffer-crc32: 0.2.13 fd-slicer: 1.1.0 + yauzl@3.2.0: + dependencies: + buffer-crc32: 0.2.13 + pend: 1.2.0 + yeoman-environment@3.19.3: dependencies: '@npmcli/arborist': 4.3.1