From 31ccaa52277b7af13b4cc4b6de510c985c711e23 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 20 May 2025 23:13:03 -0700 Subject: [PATCH] feat(google-drive): added additional tools to interact with google drive (#387) * added additional google drive tools * added folder id and doc id fields for google docs and google drive, added additional google drive tools * added google drive upload to list of google drive tools * consolidated consts * resolved PR comments --- apps/docs/content/docs/tools/google_drive.mdx | 35 +-- apps/docs/content/docs/tools/thinking.mdx | 31 ++- apps/sim/app/api/files/utils.ts | 4 + apps/sim/blocks/blocks/google_docs.ts | 47 ++-- apps/sim/blocks/blocks/google_drive.ts | 206 +++++++++++++----- apps/sim/blocks/types.ts | 2 +- apps/sim/tools/google_docs/create.ts | 67 +++--- apps/sim/tools/google_docs/read.ts | 2 +- apps/sim/tools/google_docs/types.ts | 2 + apps/sim/tools/google_docs/write.ts | 2 +- apps/sim/tools/google_drive/create_folder.ts | 79 +++++++ apps/sim/tools/google_drive/download.ts | 69 ------ apps/sim/tools/google_drive/export.ts | 124 ----------- apps/sim/tools/google_drive/get_content.ts | 170 +++++++++++++++ apps/sim/tools/google_drive/index.ts | 6 +- apps/sim/tools/google_drive/list.ts | 5 +- apps/sim/tools/google_drive/types.ts | 4 +- apps/sim/tools/google_drive/upload.ts | 166 +++++++++++--- apps/sim/tools/google_drive/utils.ts | 23 ++ apps/sim/tools/registry.ts | 12 +- 20 files changed, 674 insertions(+), 382 deletions(-) create mode 100644 apps/sim/tools/google_drive/create_folder.ts delete mode 100644 apps/sim/tools/google_drive/download.ts delete mode 100644 apps/sim/tools/google_drive/export.ts create mode 100644 apps/sim/tools/google_drive/get_content.ts create mode 100644 apps/sim/tools/google_drive/utils.ts diff --git a/apps/docs/content/docs/tools/google_drive.mdx b/apps/docs/content/docs/tools/google_drive.mdx index cafb05500..7350a9d30 100644 --- a/apps/docs/content/docs/tools/google_drive.mdx +++ b/apps/docs/content/docs/tools/google_drive.mdx @@ -1,6 +1,6 @@ --- title: Google Drive -description: Upload, download, and list files +description: Create and list files --- import { BlockInfoCard } from '@/components/ui/block-info-card' @@ -72,7 +72,7 @@ In Sim Studio, the Google Drive integration enables your agents to interact dire ## Usage Instructions -Integrate Google Drive functionality to manage files and folders. Upload new files, download existing ones, and list contents of folders using OAuth authentication. Supports file operations with custom MIME types and folder organization. +Integrate Google Drive functionality to manage files and folders. Upload new files, get content from existing files, create new folders, and list contents of folders using OAuth authentication. Supports file operations with custom MIME types and folder organization. ## Tools @@ -104,22 +104,23 @@ Upload a file to Google Drive | `modifiedTime` | string | | `parents` | string | -### `google_drive_download` +### `google_drive_create_folder` -Download a file from Google Drive +Create a new folder in Google Drive #### Input -| Parameter | Type | Required | Description | -| ------------- | ------ | -------- | ----------------------------------------- | -| `accessToken` | string | Yes | The access token for the Google Drive API | -| `fileId` | string | Yes | The ID of the file to download | +| Parameter | Type | Required | Description | +| ------------- | ------ | -------- | ------------------------------------------------------- | +| `accessToken` | string | Yes | The access token for the Google Drive API | +| `fileName` | string | Yes | Name of the folder to create | +| `folderId` | string | No | ID of the parent folder \(leave empty for root folder\) | #### Output | Parameter | Type | | ---------------- | ------ | -| `metadata` | string | +| `file` | string | | `name` | string | | `mimeType` | string | | `webViewLink` | string | @@ -159,15 +160,19 @@ List files and folders in Google Drive ## Block Configuration -No configuration parameters required. +### Input + +| Parameter | Type | Required | Description | +| ----------- | ------ | -------- | ----------- | +| `operation` | string | Yes | Operation | ### Outputs -| Output | Type | Description | -| ------------ | ------ | ------------------------ | -| `response` | object | Output from response | -| ↳ `content` | string | content of the response | -| ↳ `metadata` | json | metadata of the response | +| Output | Type | Description | +| ---------- | ------ | --------------------- | +| `response` | object | Output from response | +| ↳ `file` | json | file of the response | +| ↳ `files` | json | files of the response | ## Notes diff --git a/apps/docs/content/docs/tools/thinking.mdx b/apps/docs/content/docs/tools/thinking.mdx index 72933433c..1f2d1b959 100644 --- a/apps/docs/content/docs/tools/thinking.mdx +++ b/apps/docs/content/docs/tools/thinking.mdx @@ -9,16 +9,27 @@ import { BlockInfoCard } from '@/components/ui/block-info-card' type="thinking" color="#181C1E" icon={true} - iconSvg={` - Brain - - - - - - - - + iconSvg={` + + + + + + + + + `} /> diff --git a/apps/sim/app/api/files/utils.ts b/apps/sim/app/api/files/utils.ts index 3960f2db3..81826a654 100644 --- a/apps/sim/app/api/files/utils.ts +++ b/apps/sim/app/api/files/utils.ts @@ -55,11 +55,13 @@ export const contentTypeMap: Record = { ts: 'application/typescript', // Document formats pdf: 'application/pdf', + googleDoc: 'application/vnd.google-apps.document', doc: 'application/msword', docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // Spreadsheet formats xls: 'application/vnd.ms-excel', xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + googleSheet: 'application/vnd.google-apps.spreadsheet', // Presentation formats ppt: 'application/vnd.ms-powerpoint', pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation', @@ -71,6 +73,8 @@ export const contentTypeMap: Record = { svg: 'image/svg+xml', // Archive formats zip: 'application/zip', + // Folder format + googleFolder: 'application/vnd.google-apps.folder', } /** diff --git a/apps/sim/blocks/blocks/google_docs.ts b/apps/sim/blocks/blocks/google_docs.ts index afd531c4b..ea8c0e90d 100644 --- a/apps/sim/blocks/blocks/google_docs.ts +++ b/apps/sim/blocks/blocks/google_docs.ts @@ -77,12 +77,8 @@ export const GoogleDocsBlock: BlockConfig = { title: 'Or Enter Document ID Manually', type: 'short-input', layout: 'full', - placeholder: 'ID of the document (from URL)', - condition: { - field: 'operation', - value: 'read', - and: { field: 'documentId', value: '' }, - }, + placeholder: 'ID of the document', + condition: { field: 'operation', value: 'read' }, }, // Manual Document ID for write operation { @@ -90,12 +86,8 @@ export const GoogleDocsBlock: BlockConfig = { title: 'Or Enter Document ID Manually', type: 'short-input', layout: 'full', - placeholder: 'ID of the document (from URL)', - condition: { - field: 'operation', - value: 'write', - and: { field: 'documentId', value: '' }, - }, + placeholder: 'ID of the document', + condition: { field: 'operation', value: 'write' }, }, // Create-specific Fields { @@ -106,9 +98,23 @@ export const GoogleDocsBlock: BlockConfig = { placeholder: 'Enter title for the new document', condition: { field: 'operation', value: 'create' }, }, + // Folder Selector for create operation + { + id: 'folderSelector', + title: 'Select Parent Folder', + type: 'file-selector', + layout: 'full', + provider: 'google-drive', + serviceId: 'google-drive', + requiredScopes: [], + mimeType: 'application/vnd.google-apps.folder', + placeholder: 'Select a parent folder', + condition: { field: 'operation', value: 'create' }, + }, + // Manual Folder ID for create operation { id: 'folderId', - title: 'Parent Folder ID (Optional)', + title: 'Or Enter Parent Folder ID Manually', type: 'short-input', layout: 'full', placeholder: 'ID of the parent folder (leave empty for root folder)', @@ -149,22 +155,16 @@ export const GoogleDocsBlock: BlockConfig = { } }, params: (params) => { - const { credential, documentId, manualDocumentId, ...rest } = params + const { credential, documentId, manualDocumentId, folderSelector, folderId, ...rest } = + params - // Use the selected document ID or the manually entered one - // If documentId is provided, it's from the file selector and contains the file ID - // If not, fall back to manually entered ID const effectiveDocumentId = (documentId || manualDocumentId || '').trim() - - if (params.operation !== 'create' && !effectiveDocumentId) { - throw new Error( - 'Document ID is required. Please select a document or enter an ID manually.' - ) - } + const effectiveFolderId = (folderSelector || folderId || '').trim() return { ...rest, documentId: effectiveDocumentId, + folderId: effectiveFolderId, credential, } }, @@ -176,6 +176,7 @@ export const GoogleDocsBlock: BlockConfig = { documentId: { type: 'string', required: false }, manualDocumentId: { type: 'string', required: false }, title: { type: 'string', required: false }, + folderSelector: { type: 'string', required: false }, folderId: { type: 'string', required: false }, content: { type: 'string', required: false }, }, diff --git a/apps/sim/blocks/blocks/google_drive.ts b/apps/sim/blocks/blocks/google_drive.ts index 98214889a..e1ed438bf 100644 --- a/apps/sim/blocks/blocks/google_drive.ts +++ b/apps/sim/blocks/blocks/google_drive.ts @@ -1,6 +1,6 @@ import { GoogleDriveIcon } from '@/components/icons' import { - GoogleDriveDownloadResponse, + GoogleDriveGetContentResponse, GoogleDriveListResponse, GoogleDriveUploadResponse, } from '@/tools/google_drive/types' @@ -8,32 +8,33 @@ import { BlockConfig } from '../types' type GoogleDriveResponse = | GoogleDriveUploadResponse - | GoogleDriveDownloadResponse + | GoogleDriveGetContentResponse | GoogleDriveListResponse export const GoogleDriveBlock: BlockConfig = { type: 'google_drive', name: 'Google Drive', - description: 'Upload, download, and list files', + description: 'Create, upload, and list files', longDescription: - 'Integrate Google Drive functionality to manage files and folders. Upload new files, download existing ones, and list contents of folders using OAuth authentication. Supports file operations with custom MIME types and folder organization.', + 'Integrate Google Drive functionality to manage files and folders. Upload new files, get content from existing files, create new folders, and list contents of folders using OAuth authentication. Supports file operations with custom MIME types and folder organization.', docsLink: 'https://docs.simstudio.ai/tools/google_drive', category: 'tools', bgColor: '#E0E0E0', icon: GoogleDriveIcon, subBlocks: [ // Operation selector - // { - // id: 'operation', - // title: 'Operation', - // type: 'dropdown', - // layout: 'full', - // options: [ - // // { label: 'Upload File', id: 'upload' }, - // // { label: 'Download File', id: 'download' }, - // { label: 'List Files', id: 'list' }, - // ], - // }, + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'Create Folder', id: 'create_folder' }, + { label: 'Upload File', id: 'upload' }, + // { label: 'Get File Content', id: 'get_content' }, + { label: 'List Files', id: 'list' }, + ], + }, // Google Drive Credentials { id: 'credential', @@ -51,7 +52,7 @@ export const GoogleDriveBlock: BlockConfig = { title: 'File Name', type: 'short-input', layout: 'full', - placeholder: 'Name for the uploaded file (e.g., document.txt)', + placeholder: 'Name of the file', condition: { field: 'operation', value: 'upload' }, }, { @@ -65,41 +66,126 @@ export const GoogleDriveBlock: BlockConfig = { { id: 'mimeType', title: 'MIME Type', - type: 'short-input', + type: 'dropdown', layout: 'full', - placeholder: - 'File MIME type (default: text/plain, e.g., text/plain, application/json, text/csv)', + options: [ + { label: 'Google Doc', id: 'application/vnd.google-apps.document' }, + { label: 'Google Sheet', id: 'application/vnd.google-apps.spreadsheet' }, + { label: 'Google Slides', id: 'application/vnd.google-apps.presentation' }, + { label: 'PDF (application/pdf)', id: 'application/pdf' }, + ], + placeholder: 'Select a file type', + condition: { field: 'operation', value: 'upload' }, + }, + { + id: 'folderSelector', + title: 'Select Parent Folder', + type: 'file-selector', + layout: 'full', + provider: 'google-drive', + serviceId: 'google-drive', + requiredScopes: ['https://www.googleapis.com/auth/drive.file'], + mimeType: 'application/vnd.google-apps.folder', + placeholder: 'Select a parent folder', condition: { field: 'operation', value: 'upload' }, }, { id: 'folderId', - title: 'Parent Folder ID', + title: 'Or Enter Parent Folder ID Manually', type: 'short-input', layout: 'full', placeholder: 'ID of the parent folder (leave empty for root folder)', - condition: { field: 'operation', value: 'upload' }, + condition: { + field: 'operation', + value: 'upload', + }, }, - // Download Fields + // Get Content Fields + // { + // id: 'fileId', + // title: 'Select File', + // type: 'file-selector', + // layout: 'full', + // provider: 'google-drive', + // serviceId: 'google-drive', + // requiredScopes: [], + // placeholder: 'Select a file', + // condition: { field: 'operation', value: 'get_content' }, + // }, + // // Manual File ID input (shown only when no file is selected) + // { + // id: 'fileId', + // title: 'Or Enter File ID Manually', + // type: 'short-input', + // layout: 'full', + // placeholder: 'ID of the file to get content from', + // condition: { + // field: 'operation', + // value: 'get_content', + // and: { + // field: 'fileId', + // value: '', + // }, + // }, + // }, + // Export format for Google Workspace files + // { + // id: 'mimeType', + // title: 'Export Format', + // type: 'dropdown', + // layout: 'full', + // options: [ + // { label: 'Plain Text', id: 'text/plain' }, + // { label: 'HTML', id: 'text/html' }, + // ], + // placeholder: 'Optional: Choose export format for Google Workspace files', + // condition: { field: 'operation', value: 'get_content' }, + // }, + // Create Folder Fields { - id: 'fileId', - title: 'File ID', + id: 'fileName', + title: 'Folder Name', type: 'short-input', layout: 'full', - placeholder: 'ID of the file to download (find in file URL or by listing files)', - condition: { field: 'operation', value: 'download' }, + placeholder: 'Name for the new folder', + condition: { field: 'operation', value: 'create_folder' }, + }, + { + id: 'folderSelector', + title: 'Select Parent Folder', + type: 'file-selector', + layout: 'full', + provider: 'google-drive', + serviceId: 'google-drive', + requiredScopes: ['https://www.googleapis.com/auth/drive.file'], + mimeType: 'application/vnd.google-apps.folder', + placeholder: 'Select a parent folder', + condition: { field: 'operation', value: 'create_folder' }, + }, + // Manual Folder ID input (shown only when no folder is selected) + { + id: 'folderId', + title: 'Or Enter Parent Folder ID Manually', + type: 'short-input', + layout: 'full', + placeholder: 'ID of the parent folder (leave empty for root folder)', + condition: { + field: 'operation', + value: 'create_folder', + }, }, // List Fields - Folder Selector { - id: 'folderId', + id: 'folderSelector', title: 'Select Folder', type: 'file-selector', layout: 'full', provider: 'google-drive', serviceId: 'google-drive', - requiredScopes: [], + requiredScopes: ['https://www.googleapis.com/auth/drive.file'], mimeType: 'application/vnd.google-apps.folder', - placeholder: 'Select a folder', - // condition: { field: 'operation', value: 'list' }, + placeholder: 'Select a folder to list files from', + condition: { field: 'operation', value: 'list' }, }, // Manual Folder ID input (shown only when no folder is selected) { @@ -109,12 +195,8 @@ export const GoogleDriveBlock: BlockConfig = { layout: 'full', placeholder: 'ID of the folder to list (leave empty for root folder)', condition: { - // field: 'operation', - // value: 'list', - // and: { - field: 'folderId', - value: '', - // }, + field: 'operation', + value: 'list', }, }, { @@ -123,7 +205,7 @@ export const GoogleDriveBlock: BlockConfig = { type: 'short-input', layout: 'full', placeholder: 'Search for specific files (e.g., name contains "report")', - // condition: { field: 'operation', value: 'list' }, + condition: { field: 'operation', value: 'list' }, }, { id: 'pageSize', @@ -131,58 +213,62 @@ export const GoogleDriveBlock: BlockConfig = { type: 'short-input', layout: 'full', placeholder: 'Number of results (default: 100, max: 1000)', - // condition: { field: 'operation', value: 'list' }, + condition: { field: 'operation', value: 'list' }, }, ], tools: { - access: ['google_drive_upload', 'google_drive_download', 'google_drive_list'], + access: ['google_drive_upload', 'google_drive_create_folder', 'google_drive_list'], config: { tool: (params) => { - // Since we only have 'list' now, we can simplify this - return 'google_drive_list' - - // switch (params.operation) { - // case 'upload': - // return 'google_drive_upload' - // case 'download': - // return 'google_drive_download' - // case 'list': - // return 'google_drive_list' - // default: - // throw new Error(`Invalid Google Drive operation: ${params.operation}`) - // } + switch (params.operation) { + case 'upload': + return 'google_drive_upload' + // case 'get_content': + // return 'google_drive_get_content' + case 'create_folder': + return 'google_drive_create_folder' + case 'list': + return 'google_drive_list' + default: + throw new Error(`Invalid Google Drive operation: ${params.operation}`) + } }, params: (params) => { - const { credential, folderId, ...rest } = params + const { credential, folderId, folderSelector, mimeType, ...rest } = params + + // Use folderSelector if provided, otherwise use folderId + const effectiveFolderId = folderSelector || folderId || '' return { accessToken: credential, - folderId: folderId?.trim() || '', + folderId: effectiveFolderId.trim(), pageSize: rest.pageSize ? parseInt(rest.pageSize as string, 10) : undefined, + mimeType: mimeType, ...rest, } }, }, }, inputs: { - // operation: { type: 'string', required: true }, + operation: { type: 'string', required: true }, credential: { type: 'string', required: true }, - // Upload operation inputs + // Upload and Create Folder operation inputs fileName: { type: 'string', required: false }, content: { type: 'string', required: false }, mimeType: { type: 'string', required: false }, - // Download operation inputs - fileId: { type: 'string', required: false }, + // Get Content operation inputs + // fileId: { type: 'string', required: false }, // List operation inputs folderId: { type: 'string', required: false }, + folderSelector: { type: 'string', required: false }, query: { type: 'string', required: false }, pageSize: { type: 'number', required: false }, }, outputs: { response: { type: { - content: 'string', - metadata: 'json', + file: 'json', + files: 'json', }, }, }, diff --git a/apps/sim/blocks/types.ts b/apps/sim/blocks/types.ts index 6f98e998c..f953d191a 100644 --- a/apps/sim/blocks/types.ts +++ b/apps/sim/blocks/types.ts @@ -107,7 +107,7 @@ export interface SubBlockConfig { not?: boolean and?: { field: string - value: string | number | boolean | Array + value: string | number | boolean | Array | undefined not?: boolean } } diff --git a/apps/sim/tools/google_docs/create.ts b/apps/sim/tools/google_docs/create.ts index 0bd498b11..791b31f46 100644 --- a/apps/sim/tools/google_docs/create.ts +++ b/apps/sim/tools/google_docs/create.ts @@ -34,7 +34,7 @@ export const createTool: ToolConfig { - return 'https://docs.googleapis.com/v1/documents' + return 'https://www.googleapis.com/drive/v3/files' }, method: 'POST', headers: (params) => { @@ -49,51 +49,52 @@ export const createTool: ToolConfig { - // Validate title if (!params.title) { throw new Error('Title is required') } - // Create a new document with the specified title - const requestBody = { - title: params.title, + const requestBody: any = { + name: params.title, + mimeType: 'application/vnd.google-apps.document', + } + + // Add parent folder if specified + if (params.folderId || params.folderSelector) { + requestBody.parents = [params.folderId || params.folderSelector] } return requestBody }, }, postProcess: async (result, params, executeTool) => { - // Only add content if it was provided and not already added during creation - // The Google Docs API doesn't directly support content in the create request, - // so we need to add it separately via the write tool - if (result.success && params.content) { - const documentId = result.output.metadata.documentId + if (!result.success) { + return result + } - if (documentId) { - try { - const writeParams = { - accessToken: params.accessToken, - documentId: documentId, - content: params.content, - } + const documentId = result.output.metadata.documentId - // Use the write tool to add content - const writeResult = await executeTool('google_docs_write', writeParams) - - if (!writeResult.success) { - logger.warn( - 'Failed to add content to document, but document was created:', - writeResult.error - ) - } - } catch (error) { - logger.warn('Error adding content to document:', { error }) - // Don't fail the overall operation if adding content fails + if (params.content && documentId) { + try { + const writeParams = { + accessToken: params.accessToken, + documentId: documentId, + content: params.content, } + + const writeResult = await executeTool('google_docs_write', writeParams) + + if (!writeResult.success) { + logger.warn( + 'Failed to add content to document, but document was created:', + writeResult.error + ) + } + } catch (error) { + logger.warn('Error adding content to document:', { error }) + // Don't fail the overall operation if adding content fails } } - // Return the original result regardless of post-processing outcome return result }, transformResponse: async (response: Response) => { @@ -113,13 +114,11 @@ export const createTool: ToolConfig request: { url: (params) => { // Ensure documentId is valid - const documentId = params.documentId?.trim() + const documentId = params.documentId?.trim() || params.manualDocumentId?.trim() if (!documentId) { throw new Error('Document ID is required') } diff --git a/apps/sim/tools/google_docs/types.ts b/apps/sim/tools/google_docs/types.ts index 4dded1a52..ea9112dc3 100644 --- a/apps/sim/tools/google_docs/types.ts +++ b/apps/sim/tools/google_docs/types.ts @@ -32,7 +32,9 @@ export interface GoogleDocsCreateResponse extends ToolResponse { export interface GoogleDocsToolParams { accessToken: string documentId?: string + manualDocumentId?: string title?: string content?: string folderId?: string + folderSelector?: string } diff --git a/apps/sim/tools/google_docs/write.ts b/apps/sim/tools/google_docs/write.ts index a40808884..52767f6a4 100644 --- a/apps/sim/tools/google_docs/write.ts +++ b/apps/sim/tools/google_docs/write.ts @@ -31,7 +31,7 @@ export const writeTool: ToolConfig { // Ensure documentId is valid - const documentId = params.documentId?.trim() + const documentId = params.documentId?.trim() || params.manualDocumentId?.trim() if (!documentId) { throw new Error('Document ID is required') } diff --git a/apps/sim/tools/google_drive/create_folder.ts b/apps/sim/tools/google_drive/create_folder.ts new file mode 100644 index 000000000..72bb4f82c --- /dev/null +++ b/apps/sim/tools/google_drive/create_folder.ts @@ -0,0 +1,79 @@ +import { ToolConfig } from '../types' +import { GoogleDriveToolParams, GoogleDriveUploadResponse } from './types' + +export const createFolderTool: ToolConfig = { + id: 'google_drive_create_folder', + name: 'Create Folder in Google Drive', + description: 'Create a new folder in Google Drive', + version: '1.0', + oauth: { + required: true, + provider: 'google-drive', + additionalScopes: ['https://www.googleapis.com/auth/drive.file'], + }, + params: { + accessToken: { + type: 'string', + required: true, + description: 'The access token for the Google Drive API', + }, + fileName: { + type: 'string', + required: true, + description: 'Name of the folder to create', + }, + folderId: { + type: 'string', + required: false, + description: 'ID of the parent folder (leave empty for root folder)', + }, + }, + request: { + url: 'https://www.googleapis.com/drive/v3/files', + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params) => { + const metadata = { + name: params.fileName, + mimeType: 'application/vnd.google-apps.folder', + ...(params.folderId ? { parents: [params.folderId] } : {}), + } + + if (params.folderSelector) { + metadata.parents = [params.folderSelector] + } + + return metadata + }, + }, + transformResponse: async (response: Response) => { + if (!response.ok) { + const data = await response.json().catch(() => ({})) + throw new Error(data.error?.message || 'Failed to create folder in Google Drive') + } + const data = await response.json() + + return { + success: true, + output: { + file: { + id: data.id, + name: data.name, + mimeType: data.mimeType, + webViewLink: data.webViewLink, + webContentLink: data.webContentLink, + size: data.size, + createdTime: data.createdTime, + modifiedTime: data.modifiedTime, + parents: data.parents, + }, + }, + } + }, + transformError: (error) => { + return error.message || 'An error occurred while creating folder in Google Drive' + }, +} diff --git a/apps/sim/tools/google_drive/download.ts b/apps/sim/tools/google_drive/download.ts deleted file mode 100644 index 4516c8d08..000000000 --- a/apps/sim/tools/google_drive/download.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { ToolConfig } from '../types' -import { GoogleDriveDownloadResponse, GoogleDriveToolParams } from './types' - -export const downloadTool: ToolConfig = { - id: 'google_drive_download', - name: 'Download from Google Drive', - description: 'Download a file from Google Drive', - version: '1.0', - oauth: { - required: true, - provider: 'google-drive', - additionalScopes: ['https://www.googleapis.com/auth/drive.file'], - }, - params: { - accessToken: { - type: 'string', - required: true, - description: 'The access token for the Google Drive API', - }, - fileId: { type: 'string', required: true, description: 'The ID of the file to download' }, - }, - request: { - url: (params) => `https://www.googleapis.com/drive/v3/files/${params.fileId}?alt=media`, - method: 'GET', - headers: (params) => ({ - Authorization: `Bearer ${params.accessToken}`, - }), - }, - transformResponse: async (response: Response) => { - if (!response.ok) { - const error = await response.json() - throw new Error(error.error?.message || 'Failed to download file from Google Drive') - } - - // Get file metadata - const metadataResponse = await fetch( - `https://www.googleapis.com/drive/v3/files/${response.url.split('files/')[1].split('?')[0]}`, - { - headers: { - Authorization: response.headers.get('Authorization') || '', - }, - } - ) - - const metadata = await metadataResponse.json() - const content = await response.text() - - return { - success: true, - output: { - content, - metadata: { - id: metadata.id, - name: metadata.name, - mimeType: metadata.mimeType, - webViewLink: metadata.webViewLink, - webContentLink: metadata.webContentLink, - size: metadata.size, - createdTime: metadata.createdTime, - modifiedTime: metadata.modifiedTime, - parents: metadata.parents, - }, - }, - } - }, - transformError: (error) => { - return error.message || 'An error occurred while downloading from Google Drive' - }, -} diff --git a/apps/sim/tools/google_drive/export.ts b/apps/sim/tools/google_drive/export.ts deleted file mode 100644 index 89e4a496f..000000000 --- a/apps/sim/tools/google_drive/export.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { createLogger } from '@/lib/logs/console-logger' -import { ToolConfig } from '../types' -import { GoogleDriveDownloadResponse } from './types' -import { GoogleDriveToolParams } from './types' - -const logger = createLogger('GoogleDriveExportTool') - -export const exportTool: ToolConfig< - GoogleDriveToolParams & { mimeType?: string }, - GoogleDriveDownloadResponse -> = { - id: 'google_drive_export', - name: 'Export from Google Drive', - description: 'Export a Google Workspace file (Docs, Sheets, Slides) from Google Drive', - version: '1.0', - oauth: { - required: true, - provider: 'google-drive', - additionalScopes: ['https://www.googleapis.com/auth/drive.file'], - }, - params: { - accessToken: { - type: 'string', - required: true, - description: 'The access token for the Google Drive API', - }, - fileId: { type: 'string', required: true, description: 'The ID of the file to export' }, - mimeType: { - type: 'string', - required: false, - description: 'The MIME type to export the file as', - }, - }, - request: { - url: (params) => { - const exportMimeType = params.mimeType || 'application/pdf' - return `https://www.googleapis.com/drive/v3/files/${params.fileId}/export?mimeType=${encodeURIComponent(exportMimeType)}` - }, - method: 'GET', - headers: (params) => ({ - Authorization: `Bearer ${params.accessToken}`, - }), - }, - transformResponse: async (response: Response) => { - if (!response.ok) { - const error = await response.json() - logger.error('Google Drive export error:', { - status: response.status, - statusText: response.statusText, - error, - fileId: response.url.split('files/')[1]?.split('?')[0], - }) - throw new Error( - `Failed to export file from Google Drive: ${response.status} ${response.statusText} - ${error.error?.message || 'Unknown error'}` - ) - } - - // Get file metadata - const fileId = response.url.split('files/')[1]?.split('?')[0] - const metadataResponse = await fetch(`https://www.googleapis.com/drive/v3/files/${fileId}`, { - headers: { - Authorization: response.headers.get('Authorization') || '', - }, - }) - - if (!metadataResponse.ok) { - const metadataError = await metadataResponse.json() - logger.error('Google Drive metadata error:', { - status: metadataResponse.status, - statusText: metadataResponse.statusText, - error: metadataError, - }) - throw new Error( - `Failed to get file metadata: ${metadataResponse.status} ${metadataResponse.statusText} - ${metadataError.error?.message || 'Unknown error'}` - ) - } - - const metadata = await metadataResponse.json() - let content - try { - content = await response.text() - } catch (error: any) { - logger.error('Error reading response content:', { - message: error.message, - stack: error.stack, - error: JSON.stringify(error), - }) - throw new Error(`Failed to read file content: ${error?.message || 'Unknown error'}`) - } - - return { - success: true, - output: { - content, - metadata: { - id: metadata.id, - name: metadata.name, - mimeType: metadata.mimeType, - webViewLink: metadata.webViewLink, - webContentLink: metadata.webContentLink, - size: metadata.size, - createdTime: metadata.createdTime, - modifiedTime: metadata.modifiedTime, - parents: metadata.parents, - }, - }, - } - }, - transformError: (error: any) => { - logger.error('Export tool error:', { - message: error.message, - stack: error.stack, - error: JSON.stringify(error, null, 2), - }) - if (typeof error === 'string') { - return error - } - return ( - error.message || - JSON.stringify(error, null, 2) || - 'An error occurred while exporting from Google Drive' - ) - }, -} diff --git a/apps/sim/tools/google_drive/get_content.ts b/apps/sim/tools/google_drive/get_content.ts new file mode 100644 index 000000000..25cead169 --- /dev/null +++ b/apps/sim/tools/google_drive/get_content.ts @@ -0,0 +1,170 @@ +import { createLogger } from '@/lib/logs/console-logger' +import { ToolConfig } from '../types' +import { GoogleDriveGetContentResponse, GoogleDriveToolParams } from './types' +import { DEFAULT_EXPORT_FORMATS, GOOGLE_WORKSPACE_MIME_TYPES } from './utils' + +const logger = createLogger('GoogleDriveGetContentTool') + +export const getContentTool: ToolConfig = { + id: 'google_drive_get_content', + name: 'Get Content from Google Drive', + description: + 'Get content from a file in Google Drive (exports Google Workspace files automatically)', + version: '1.0', + oauth: { + required: true, + provider: 'google-drive', + additionalScopes: ['https://www.googleapis.com/auth/drive.file'], + }, + params: { + accessToken: { + type: 'string', + required: true, + description: 'The access token for the Google Drive API', + }, + fileId: { + type: 'string', + required: true, + description: 'The ID of the file to get content from', + }, + mimeType: { + type: 'string', + required: false, + description: 'The MIME type to export Google Workspace files to (optional)', + }, + }, + request: { + url: (params) => + `https://www.googleapis.com/drive/v3/files/${params.fileId}?fields=id,name,mimeType`, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + }), + }, + transformResponse: async (response: Response, params?: GoogleDriveToolParams) => { + try { + if (!response.ok) { + const errorDetails = await response.json().catch(() => ({})) + logger.error('Failed to get file metadata', { + status: response.status, + statusText: response.statusText, + error: errorDetails, + }) + throw new Error(errorDetails.error?.message || 'Failed to get file metadata') + } + + const metadata = await response.json() + const fileId = metadata.id + const mimeType = metadata.mimeType + const authHeader = `Bearer ${params?.accessToken || ''}` + + let content: string + + if (GOOGLE_WORKSPACE_MIME_TYPES.includes(mimeType)) { + const exportFormat = params?.mimeType || DEFAULT_EXPORT_FORMATS[mimeType] || 'text/plain' + logger.info('Exporting Google Workspace file', { + fileId, + mimeType, + exportFormat, + }) + + const exportResponse = await fetch( + `https://www.googleapis.com/drive/v3/files/${fileId}/export?mimeType=${encodeURIComponent(exportFormat)}`, + { + headers: { + Authorization: authHeader, + }, + } + ) + + if (!exportResponse.ok) { + const exportError = await exportResponse.json().catch(() => ({})) + logger.error('Failed to export file', { + status: exportResponse.status, + statusText: exportResponse.statusText, + error: exportError, + }) + throw new Error(exportError.error?.message || 'Failed to export Google Workspace file') + } + + content = await exportResponse.text() + } else { + logger.info('Downloading regular file', { + fileId, + mimeType, + }) + + const downloadResponse = await fetch( + `https://www.googleapis.com/drive/v3/files/${fileId}?alt=media`, + { + headers: { + Authorization: authHeader, + }, + } + ) + + if (!downloadResponse.ok) { + const downloadError = await downloadResponse.json().catch(() => ({})) + logger.error('Failed to download file', { + status: downloadResponse.status, + statusText: downloadResponse.statusText, + error: downloadError, + }) + throw new Error(downloadError.error?.message || 'Failed to download file') + } + + content = await downloadResponse.text() + } + + const metadataResponse = await fetch( + `https://www.googleapis.com/drive/v3/files/${fileId}?fields=id,name,mimeType,webViewLink,webContentLink,size,createdTime,modifiedTime,parents`, + { + headers: { + Authorization: authHeader, + }, + } + ) + + if (!metadataResponse.ok) { + logger.warn('Failed to get full metadata, using partial metadata', { + status: metadataResponse.status, + statusText: metadataResponse.statusText, + }) + } else { + const fullMetadata = await metadataResponse.json() + Object.assign(metadata, fullMetadata) + } + + return { + success: true, + output: { + content, + metadata: { + id: metadata.id, + name: metadata.name, + mimeType: metadata.mimeType, + webViewLink: metadata.webViewLink, + webContentLink: metadata.webContentLink, + size: metadata.size, + createdTime: metadata.createdTime, + modifiedTime: metadata.modifiedTime, + parents: metadata.parents, + }, + }, + } + } catch (error: any) { + logger.error('Error in transform response', { + error: error.message, + stack: error.stack, + }) + throw error + } + }, + transformError: (error) => { + logger.error('Download error', { + message: error.message, + stack: error.stack, + }) + return error.message || 'An error occurred while getting content from Google Drive' + }, +} diff --git a/apps/sim/tools/google_drive/index.ts b/apps/sim/tools/google_drive/index.ts index ba7b0e856..7680fa6b6 100644 --- a/apps/sim/tools/google_drive/index.ts +++ b/apps/sim/tools/google_drive/index.ts @@ -1,7 +1,9 @@ -import { downloadTool } from './download' +import { createFolderTool } from './create_folder' +import { getContentTool } from './get_content' import { listTool } from './list' import { uploadTool } from './upload' -export const driveDownloadTool = downloadTool +export const driveCreateFolderTool = createFolderTool +export const driveGetContentTool = getContentTool export const driveListTool = listTool export const driveUploadTool = uploadTool diff --git a/apps/sim/tools/google_drive/list.ts b/apps/sim/tools/google_drive/list.ts index 2dc6619fd..e7f114919 100644 --- a/apps/sim/tools/google_drive/list.ts +++ b/apps/sim/tools/google_drive/list.ts @@ -40,8 +40,9 @@ export const listTool: ToolConfig = { id: 'google_drive_upload', @@ -31,59 +35,149 @@ export const uploadTool: ToolConfig ({ Authorization: `Bearer ${params.accessToken}`, - 'Content-Type': 'multipart/related; boundary=boundary', + 'Content-Type': 'application/json', }), body: (params) => { const metadata = { - name: params.fileName, - ...(params.folderId ? { parents: [params.folderId] } : {}), + name: params.fileName, // Important: Always include the filename in metadata + mimeType: params.mimeType || 'text/plain', + ...(params.folderId && params.folderId.trim() !== '' ? { parents: [params.folderId] } : {}), } - const mimeType = params.mimeType || 'text/plain' + if (params.folderSelector) { + metadata.parents = [params.folderSelector] + } - const body = `--boundary -Content-Type: application/json; charset=UTF-8 - -${JSON.stringify(metadata)} - ---boundary -Content-Type: ${mimeType} - -${params.content} ---boundary--` - - return { body } + return metadata }, }, - transformResponse: async (response: Response) => { - const data = await response.json() + transformResponse: async (response: Response, params?: GoogleDriveToolParams) => { + try { + const data = await response.json() - if (!response.ok) { - throw new Error(data.error?.message || 'Failed to upload file to Google Drive') - } + if (!response.ok) { + logger.error('Failed to create file in Google Drive', { + status: response.status, + statusText: response.statusText, + data, + }) + throw new Error(data.error?.message || 'Failed to create file in Google Drive') + } - return { - success: true, - output: { - file: { - id: data.id, - name: data.name, - mimeType: data.mimeType, - webViewLink: data.webViewLink, - webContentLink: data.webContentLink, - size: data.size, - createdTime: data.createdTime, - modifiedTime: data.modifiedTime, - parents: data.parents, + // Now upload content to the created file + const fileId = data.id + const requestedMimeType = params?.mimeType || 'text/plain' + const authHeader = + response.headers.get('Authorization') || `Bearer ${params?.accessToken || ''}` + + // For Google Workspace formats, use the appropriate source MIME type for content upload + const uploadMimeType = GOOGLE_WORKSPACE_MIME_TYPES.includes(requestedMimeType) + ? SOURCE_MIME_TYPES[requestedMimeType] || 'text/plain' + : requestedMimeType + + logger.info('Uploading content to file', { + fileId, + fileName: params?.fileName, + requestedMimeType, + uploadMimeType, + }) + + const uploadResponse = await fetch( + `https://www.googleapis.com/upload/drive/v3/files/${fileId}?uploadType=media`, + { + method: 'PATCH', + headers: { + Authorization: authHeader, + 'Content-Type': uploadMimeType, + }, + body: params?.content || '', + } + ) + + if (!uploadResponse.ok) { + const uploadError = await uploadResponse.json() + logger.error('Failed to upload content to file', { + status: uploadResponse.status, + statusText: uploadResponse.statusText, + error: uploadError, + }) + throw new Error(uploadError.error?.message || 'Failed to upload content to file') + } + + // For Google Workspace documents, update the name again to ensure it sticks after conversion + if (GOOGLE_WORKSPACE_MIME_TYPES.includes(requestedMimeType)) { + logger.info('Updating file name to ensure it persists after conversion', { + fileId, + fileName: params?.fileName, + }) + + const updateNameResponse = await fetch( + `https://www.googleapis.com/drive/v3/files/${fileId}`, + { + method: 'PATCH', + headers: { + Authorization: authHeader, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + name: params?.fileName, + }), + } + ) + + if (!updateNameResponse.ok) { + logger.warn('Failed to update filename after conversion, but content was uploaded', { + status: updateNameResponse.status, + statusText: updateNameResponse.statusText, + }) + } + } + + // Get the final file data + const finalFileResponse = await fetch( + `https://www.googleapis.com/drive/v3/files/${fileId}?fields=id,name,mimeType,webViewLink,webContentLink,size,createdTime,modifiedTime,parents`, + { + headers: { + Authorization: authHeader, + }, + } + ) + + const finalFile = await finalFileResponse.json() + + return { + success: true, + output: { + file: { + id: finalFile.id, + name: finalFile.name, + mimeType: finalFile.mimeType, + webViewLink: finalFile.webViewLink, + webContentLink: finalFile.webContentLink, + size: finalFile.size, + createdTime: finalFile.createdTime, + modifiedTime: finalFile.modifiedTime, + parents: finalFile.parents, + }, }, - }, + } + } catch (error: any) { + logger.error('Error in upload transformation', { + error: error.message, + stack: error.stack, + }) + throw error } }, transformError: (error) => { + logger.error('Upload error', { + error: error.message, + stack: error.stack, + }) return error.message || 'An error occurred while uploading to Google Drive' }, } diff --git a/apps/sim/tools/google_drive/utils.ts b/apps/sim/tools/google_drive/utils.ts new file mode 100644 index 000000000..0b2ea9e40 --- /dev/null +++ b/apps/sim/tools/google_drive/utils.ts @@ -0,0 +1,23 @@ +export const GOOGLE_WORKSPACE_MIME_TYPES = [ + 'application/vnd.google-apps.document', // Google Docs + 'application/vnd.google-apps.spreadsheet', // Google Sheets + 'application/vnd.google-apps.presentation', // Google Slides + 'application/vnd.google-apps.drawing', // Google Drawings + 'application/vnd.google-apps.form', // Google Forms + 'application/vnd.google-apps.script', // Google Apps Scripts +] + +export const DEFAULT_EXPORT_FORMATS: Record = { + 'application/vnd.google-apps.document': 'text/plain', + 'application/vnd.google-apps.spreadsheet': 'text/csv', + 'application/vnd.google-apps.presentation': 'text/plain', + 'application/vnd.google-apps.drawing': 'image/png', + 'application/vnd.google-apps.form': 'application/pdf', + 'application/vnd.google-apps.script': 'application/json', +} + +export const SOURCE_MIME_TYPES: Record = { + 'application/vnd.google-apps.document': 'text/plain', + 'application/vnd.google-apps.spreadsheet': 'text/csv', + 'application/vnd.google-apps.presentation': 'application/vnd.ms-powerpoint', +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 5ae72556c..ffd42865e 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -28,7 +28,12 @@ import { import { gmailReadTool, gmailSearchTool, gmailSendTool } from './gmail' import { searchTool as googleSearchTool } from './google' import { docsCreateTool, docsReadTool, docsWriteTool } from './google_docs' -import { driveDownloadTool, driveListTool, driveUploadTool } from './google_drive' +import { + driveCreateFolderTool, + driveGetContentTool, + driveListTool, + driveUploadTool, +} from './google_drive' import { sheetsAppendTool, sheetsReadTool, @@ -42,7 +47,7 @@ import { readUrlTool } from './jina' import { jiraBulkRetrieveTool, jiraRetrieveTool, jiraUpdateTool, jiraWriteTool } from './jira' import { linkupSearchTool } from './linkup' import { mem0AddMemoriesTool, mem0GetMemoriesTool, mem0SearchMemoriesTool } from './mem0' -import { memoryAddTool, memoryGetTool, memoryGetAllTool, memoryDeleteTool } from './memory' +import { memoryAddTool, memoryDeleteTool, memoryGetAllTool, memoryGetTool } from './memory' import { mistralParserTool } from './mistral' import { notionReadTool, notionWriteTool } from './notion' import { dalleTool, embeddingsTool as openAIEmbeddings } from './openai' @@ -127,9 +132,10 @@ export const tools: Record = { reddit_hot_posts: redditHotPostsTool, reddit_get_posts: redditGetPostsTool, reddit_get_comments: redditGetCommentsTool, - google_drive_download: driveDownloadTool, + google_drive_get_content: driveGetContentTool, google_drive_list: driveListTool, google_drive_upload: driveUploadTool, + google_drive_create_folder: driveCreateFolderTool, google_docs_read: docsReadTool, google_docs_write: docsWriteTool, google_docs_create: docsCreateTool,