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
This commit is contained in:
Waleed Latif
2025-05-20 23:13:03 -07:00
committed by GitHub
parent d2cf31b42c
commit 31ccaa5227
20 changed files with 674 additions and 382 deletions

View File

@@ -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

View File

@@ -9,16 +9,27 @@ import { BlockInfoCard } from '@/components/ui/block-info-card'
type="thinking"
color="#181C1E"
icon={true}
iconSvg={`<svg className="block-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="currentColor">
<title>Brain</title>
<g id="Brain">
<path d="M11,5.34c-.12,1,.26.66-1,.66A7,7,0,0,0,4.85,17.75,7,7,0,0,0,2.62,28.48,12.14,12.14,0,0,0,.06,37.22C1,46.59,12.33,51.56,19.31,45A11.66,11.66,0,0,0,23,36.51V6.22C23-1.57,11.86-2.14,11,5.34Zm-.55,40.55a8.89,8.89,0,0,1-4.78-2.18l.42-.42a2.1,2.1,0,0,1,2.42-.4,1,1,0,0,0,.9-1.78c-2-1-1.24.83-2-3.11-.61-3,1.55-4.14,0-4.89-1.79-.9-2.38,3.32-2,5.28.79,4,.85,1.7-1.19,3.94a10,10,0,0,1-.13-12.5c1.51,1,4.86,2,4.86.17a1,1,0,0,0-1-1c-5.79,0-6.94-8.33-1.27-9.82C8,19.87,11,20.73,11,19a1,1,0,0,0-1-1,5,5,0,1,1,1.44-9.77C13,12,18,13,18,11a1,1,0,0,0-1-1,4,4,0,1,1,4-3.78v9.37l-1.16,1.15a3.42,3.42,0,0,1-4.29.43,1,1,0,0,0-1.38.28c-1.12,1.68,3.7,3.68,6.83.92V36.51A9.3,9.3,0,0,1,10.49,45.89Z" />
<path d="M16.21,23.79a3.14,3.14,0,0,0-4.42,0c-1,1-2-.42-3.08-1.5a1,1,0,0,0-1.42,1.42l.86.85-1.47.49A1,1,0,0,0,7.32,27L10.18,26c2.71.74,3.26-2.15,4.61-.79l2.5,2.5a1,1,0,0,0,1.42-1.42Z" />
<path d="M17,33H16a3,3,0,0,0-3,3,1,1,0,0,1-1,1H11a1,1,0,0,0,0,2h1a3,3,0,0,0,3-3,1,1,0,0,1,1-1h1A1,1,0,0,0,17,33Z" />
<path d="M45.36,28.49a7,7,0,0,0-2.21-10.74A7,7,0,0,0,38,6c-1.28,0-.93.35-1-.63A6,6,0,0,0,31,0a6.13,6.13,0,0,0-6,6.22V36.51C25,42.89,30.26,48.4,36.82,48A12,12,0,0,0,45.36,28.49Zm-1.65,13.8A4.92,4.92,0,0,0,42,41c.55-2.79,1.21-4.79-.13-7.47a1,1,0,0,0-1.78.9c1,2.06.45,3.65-.07,6.25-3.33.28-1.92,2.85-.59,2.19s2.33.34,2.88.84A9.28,9.28,0,0,1,27,36.51V18.37c3.12,2.75,8,.76,6.83-.92a1,1,0,0,0-1.38-.28,3.42,3.42,0,0,1-4.29-.43L27,15.59V6.22A4,4,0,1,1,31,10a1,1,0,0,0-1,1c0,2.05,5.07,1,6.57-2.78A5,5,0,1,1,38,18a1,1,0,0,0,0,2,7,7,0,0,0,3.27-.82C47,20.68,45.73,29,40,29a1,1,0,0,0,0,2,6.89,6.89,0,0,0,3.86-1.17C48.62,35.85,44,42.55,43.71,42.29Z" />
<path d="M41,27a1,1,0,0,0,.32-1.95l-1.47-.49.86-.85a1,1,0,0,0-1.42-1.42c-1.09,1.09-2.07,2.51-3.08,1.5a3.14,3.14,0,0,0-4.42,0l-2.5,2.5A1,1,0,0,0,30,28c.56,0,.54-.13,3.21-2.79,1.38-1.38,1.86,1.54,4.61.79C40.92,27,40.78,27,41,27Z" />
<path d="M37,37H36a1,1,0,0,1-1-1,3,3,0,0,0-3-3H31a1,1,0,0,0,0,2h1a1,1,0,0,1,1,1,3,3,0,0,0,3,3h1A1,1,0,0,0,37,37Z" />
</g>
iconSvg={`<svg className="block-icon"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z" />
<path d="M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z" />
<path d="M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4" />
<path d="M17.599 6.5a3 3 0 0 0 .399-1.375" />
<path d="M6.003 5.125A3 3 0 0 0 6.401 6.5" />
<path d="M3.477 10.896a4 4 0 0 1 .585-.396" />
<path d="M19.938 10.5a4 4 0 0 1 .585.396" />
<path d="M6 18a4 4 0 0 1-1.967-.516" />
<path d="M19.967 17.484A4 4 0 0 1 18 18" />
</svg>`}
/>

View File

@@ -55,11 +55,13 @@ export const contentTypeMap: Record<string, string> = {
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<string, string> = {
svg: 'image/svg+xml',
// Archive formats
zip: 'application/zip',
// Folder format
googleFolder: 'application/vnd.google-apps.folder',
}
/**

View File

@@ -77,12 +77,8 @@ export const GoogleDocsBlock: BlockConfig<GoogleDocsResponse> = {
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<GoogleDocsResponse> = {
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<GoogleDocsResponse> = {
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<GoogleDocsResponse> = {
}
},
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<GoogleDocsResponse> = {
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 },
},

View File

@@ -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<GoogleDriveResponse> = {
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<GoogleDriveResponse> = {
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<GoogleDriveResponse> = {
{
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<GoogleDriveResponse> = {
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<GoogleDriveResponse> = {
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<GoogleDriveResponse> = {
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',
},
},
},

View File

@@ -107,7 +107,7 @@ export interface SubBlockConfig {
not?: boolean
and?: {
field: string
value: string | number | boolean | Array<string | number | boolean>
value: string | number | boolean | Array<string | number | boolean> | undefined
not?: boolean
}
}

View File

@@ -34,7 +34,7 @@ export const createTool: ToolConfig<GoogleDocsToolParams, GoogleDocsCreateRespon
},
request: {
url: () => {
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<GoogleDocsToolParams, GoogleDocsCreateRespon
}
},
body: (params) => {
// 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<GoogleDocsToolParams, GoogleDocsCreateRespon
try {
// Get the response data
const responseText = await response.text()
const data = JSON.parse(responseText)
const documentId = data.documentId
const title = data.title
const documentId = data.id
const title = data.name
// Create document metadata
const metadata = {
documentId,
title: title || 'Untitled Document',

View File

@@ -22,7 +22,7 @@ export const readTool: ToolConfig<GoogleDocsToolParams, GoogleDocsReadResponse>
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')
}

View File

@@ -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
}

View File

@@ -31,7 +31,7 @@ export const writeTool: ToolConfig<GoogleDocsToolParams, GoogleDocsWriteResponse
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')
}

View File

@@ -0,0 +1,79 @@
import { ToolConfig } from '../types'
import { GoogleDriveToolParams, GoogleDriveUploadResponse } from './types'
export const createFolderTool: ToolConfig<GoogleDriveToolParams, GoogleDriveUploadResponse> = {
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'
},
}

View File

@@ -1,69 +0,0 @@
import { ToolConfig } from '../types'
import { GoogleDriveDownloadResponse, GoogleDriveToolParams } from './types'
export const downloadTool: ToolConfig<GoogleDriveToolParams, GoogleDriveDownloadResponse> = {
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'
},
}

View File

@@ -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'
)
},
}

View File

@@ -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<GoogleDriveToolParams, GoogleDriveGetContentResponse> = {
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'
},
}

View File

@@ -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

View File

@@ -40,8 +40,9 @@ export const listTool: ToolConfig<GoogleDriveToolParams, GoogleDriveListResponse
// Build the query conditions
const conditions = ['trashed = false'] // Always exclude trashed files
if (params.folderId) {
conditions.push(`'${params.folderId}' in parents`)
const folderId = params.folderId || params.folderSelector
if (folderId) {
conditions.push(`'${folderId}' in parents`)
}
// Combine all conditions with AND

View File

@@ -25,7 +25,7 @@ export interface GoogleDriveUploadResponse extends ToolResponse {
}
}
export interface GoogleDriveDownloadResponse extends ToolResponse {
export interface GoogleDriveGetContentResponse extends ToolResponse {
output: {
content: string
metadata: GoogleDriveFile
@@ -35,6 +35,7 @@ export interface GoogleDriveDownloadResponse extends ToolResponse {
export interface GoogleDriveToolParams {
accessToken: string
folderId?: string
folderSelector?: string
fileId?: string
fileName?: string
content?: string
@@ -42,4 +43,5 @@ export interface GoogleDriveToolParams {
query?: string
pageSize?: number
pageToken?: string
exportMimeType?: string
}

View File

@@ -1,5 +1,9 @@
import { createLogger } from '@/lib/logs/console-logger'
import { ToolConfig } from '../types'
import { GoogleDriveToolParams, GoogleDriveUploadResponse } from './types'
import { GOOGLE_WORKSPACE_MIME_TYPES, SOURCE_MIME_TYPES } from './utils'
const logger = createLogger('GoogleDriveUploadTool')
export const uploadTool: ToolConfig<GoogleDriveToolParams, GoogleDriveUploadResponse> = {
id: 'google_drive_upload',
@@ -31,59 +35,149 @@ export const uploadTool: ToolConfig<GoogleDriveToolParams, GoogleDriveUploadResp
},
},
request: {
url: 'https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart',
url: 'https://www.googleapis.com/drive/v3/files',
method: 'POST',
headers: (params) => ({
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'
},
}

View File

@@ -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<string, string> = {
'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<string, string> = {
'application/vnd.google-apps.document': 'text/plain',
'application/vnd.google-apps.spreadsheet': 'text/csv',
'application/vnd.google-apps.presentation': 'application/vnd.ms-powerpoint',
}

View File

@@ -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<string, ToolConfig> = {
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,