From 85175aef2b1b4a9f6d336f30f144993a299dac09 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Mon, 10 Mar 2025 02:49:38 -0700 Subject: [PATCH] feat(tools): added google docs tools/block --- app/api/auth/oauth/drive/files/route.ts | 18 ++- blocks/blocks/docs.ts | 192 ++++++++++++++++++++++++ blocks/index.ts | 3 + tools/docs/create.ts | 156 +++++++++++++++++++ tools/docs/index.ts | 7 + tools/docs/read.ts | 118 +++++++++++++++ tools/docs/types.ts | 38 +++++ tools/docs/write.ts | 109 ++++++++++++++ tools/index.ts | 5 + 9 files changed, 642 insertions(+), 4 deletions(-) create mode 100644 blocks/blocks/docs.ts create mode 100644 tools/docs/create.ts create mode 100644 tools/docs/index.ts create mode 100644 tools/docs/read.ts create mode 100644 tools/docs/types.ts create mode 100644 tools/docs/write.ts diff --git a/app/api/auth/oauth/drive/files/route.ts b/app/api/auth/oauth/drive/files/route.ts index 131824d085..a45331f0ce 100644 --- a/app/api/auth/oauth/drive/files/route.ts +++ b/app/api/auth/oauth/drive/files/route.ts @@ -51,12 +51,22 @@ export async function GET(request: NextRequest) { // Add mimeType filter if provided if (mimeType) { - queryParams += `&mimeType='${mimeType}'` + // For Google Drive API, we need to use 'q' parameter for mimeType filtering + // Instead of using the mimeType parameter directly, we'll add it to the query + if (queryParams.includes('q=')) { + queryParams += ` and mimeType='${mimeType}'` + } else { + queryParams += `&q=mimeType='${mimeType}'` + } } // Add search query if provided if (query) { - queryParams += `&q=name contains '${query}'` + if (queryParams.includes('q=')) { + queryParams += ` and name contains '${query}'` + } else { + queryParams += `&q=name contains '${query}'` + } } // Fetch files from Google Drive @@ -78,14 +88,14 @@ export async function GET(request: NextRequest) { } const data = await response.json() - - // Filter for Google Sheets files if mimeType is for spreadsheets let files = data.files || [] if (mimeType === 'application/vnd.google-apps.spreadsheet') { files = files.filter( (file: any) => file.mimeType === 'application/vnd.google-apps.spreadsheet' ) + } else if (mimeType === 'application/vnd.google-apps.document') { + files = files.filter((file: any) => file.mimeType === 'application/vnd.google-apps.document') } return NextResponse.json({ files }, { status: 200 }) diff --git a/blocks/blocks/docs.ts b/blocks/blocks/docs.ts new file mode 100644 index 0000000000..9d862ab33e --- /dev/null +++ b/blocks/blocks/docs.ts @@ -0,0 +1,192 @@ +import { GoogleDocsIcon } from '@/components/icons' +import { + GoogleDocsCreateResponse, + GoogleDocsReadResponse, + GoogleDocsWriteResponse, +} from '@/tools/docs/types' +import { BlockConfig } from '../types' + +type GoogleDocsResponse = + | GoogleDocsReadResponse + | GoogleDocsWriteResponse + | GoogleDocsCreateResponse + +export const GoogleDocsBlock: BlockConfig = { + type: 'google_docs', + name: 'Google Docs', + description: 'Read, write, and create documents', + longDescription: + 'Integrate Google Docs functionality to manage documents. Read content from existing documents, write to documents, and create new documents using OAuth authentication. Supports text content manipulation for document creation and editing.', + category: 'tools', + bgColor: '#E0E0E0', + icon: GoogleDocsIcon, + subBlocks: [ + // Operation selector + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'Read Document', id: 'read' }, + { label: 'Write to Document', id: 'write' }, + { label: 'Create Document', id: 'create' }, + ], + }, + // Google Docs Credentials + { + id: 'credential', + title: 'Google Account', + type: 'oauth-input', + layout: 'full', + provider: 'google-docs', + serviceId: 'google-docs', + requiredScopes: ['https://www.googleapis.com/auth/documents'], + placeholder: 'Select Google account', + }, + // Document Selector for read operation + { + id: 'documentId', + title: 'Select Document', + type: 'file-selector', + layout: 'full', + provider: 'google-drive', + serviceId: 'google-drive', + requiredScopes: ['https://www.googleapis.com/auth/drive.readonly'], + mimeType: 'application/vnd.google-apps.document', + placeholder: 'Select a document', + condition: { field: 'operation', value: 'read' }, + }, + // Document Selector for write operation + { + id: 'documentId', + title: 'Select Document', + type: 'file-selector', + layout: 'full', + provider: 'google-drive', + serviceId: 'google-drive', + requiredScopes: ['https://www.googleapis.com/auth/drive.readonly'], + mimeType: 'application/vnd.google-apps.document', + placeholder: 'Select a document', + condition: { field: 'operation', value: 'write' }, + }, + // Manual Document ID for read operation + { + id: 'manualDocumentId', + 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: '' }, + }, + }, + // Manual Document ID for write operation + { + id: 'manualDocumentId', + 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: '' }, + }, + }, + // Create-specific Fields + { + id: 'title', + title: 'Document Title', + type: 'short-input', + layout: 'full', + placeholder: 'Enter title for the new document', + condition: { field: 'operation', value: 'create' }, + }, + { + id: 'folderId', + title: 'Parent Folder ID (Optional)', + type: 'short-input', + layout: 'full', + placeholder: 'ID of the parent folder (leave empty for root folder)', + condition: { field: 'operation', value: 'create' }, + }, + // Content Field for write operation + { + id: 'content', + title: 'Content', + type: 'long-input', + layout: 'full', + placeholder: 'Enter document content', + condition: { field: 'operation', value: 'write' }, + }, + // Content Field for create operation + { + id: 'content', + title: 'Content', + type: 'long-input', + layout: 'full', + placeholder: 'Enter document content', + condition: { field: 'operation', value: 'create' }, + }, + ], + tools: { + access: ['google_docs_read', 'google_docs_write', 'google_docs_create'], + config: { + tool: (params) => { + switch (params.operation) { + case 'read': + return 'google_docs_read' + case 'write': + return 'google_docs_write' + case 'create': + return 'google_docs_create' + default: + throw new Error(`Invalid Google Docs operation: ${params.operation}`) + } + }, + params: (params) => { + const { credential, documentId, manualDocumentId, ...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.' + ) + } + + return { + ...rest, + documentId: effectiveDocumentId, + credential, + } + }, + }, + }, + inputs: { + operation: { type: 'string', required: true }, + credential: { type: 'string', required: true }, + documentId: { type: 'string', required: false }, + manualDocumentId: { type: 'string', required: false }, + // Create operation inputs + title: { type: 'string', required: false }, + folderId: { type: 'string', required: false }, + // Write/Create operation inputs + content: { type: 'string', required: false }, + }, + outputs: { + response: { + type: { + content: 'string', + metadata: 'json', + updatedContent: 'boolean', + }, + }, + }, +} diff --git a/blocks/index.ts b/blocks/index.ts index 7aa14e6b71..75b8dac6e7 100644 --- a/blocks/index.ts +++ b/blocks/index.ts @@ -2,6 +2,7 @@ import { AgentBlock } from './blocks/agent' import { ApiBlock } from './blocks/api' import { ConditionBlock } from './blocks/condition' +import { GoogleDocsBlock } from './blocks/docs' import { GoogleDriveBlock } from './blocks/drive' import { EvaluatorBlock } from './blocks/evaluator' import { ExaBlock } from './blocks/exa' @@ -57,6 +58,7 @@ export { ExaBlock, RedditBlock, GoogleDriveBlock, + GoogleDocsBlock, WhatsAppBlock, GoogleSheetsBlock, } @@ -72,6 +74,7 @@ const blocks: Record = { function: FunctionBlock, github: GitHubBlock, gmail: GmailBlock, + google_docs: GoogleDocsBlock, google_drive: GoogleDriveBlock, google_sheets: GoogleSheetsBlock, // guesty: GuestyBlock, diff --git a/tools/docs/create.ts b/tools/docs/create.ts new file mode 100644 index 0000000000..b447610d68 --- /dev/null +++ b/tools/docs/create.ts @@ -0,0 +1,156 @@ +import { ToolConfig } from '../types' +import { GoogleDocsCreateResponse, GoogleDocsToolParams } from './types' + +export const createTool: ToolConfig = { + id: 'google_docs_create', + name: 'Create Google Docs Document', + description: 'Create a new Google Docs document', + version: '1.0', + oauth: { + required: true, + provider: 'google-docs', + additionalScopes: [ + 'https://www.googleapis.com/auth/documents', + 'https://www.googleapis.com/auth/drive', + ], + }, + params: { + accessToken: { type: 'string', required: true }, + title: { type: 'string', required: true }, + content: { type: 'string', required: false }, + folderId: { type: 'string', required: false }, + }, + request: { + url: (params) => { + // Build URL with query parameters to pass content and folderId + const url = new URL('https://docs.googleapis.com/v1/documents') + if (params.content) { + url.searchParams.append('content', params.content) + } + if (params.folderId) { + url.searchParams.append('folderId', params.folderId) + } + return url.toString() + }, + method: 'POST', + headers: (params) => { + // Validate access token + if (!params.accessToken) { + throw new Error('Access token is required') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + 'X-Access-Token': params.accessToken, // Store access token in a custom header for later use + } + }, + body: (params) => { + // Validate title + if (!params.title) { + throw new Error('Title is required') + } + + // Create a new document with the specified title + return { + title: params.title, + } + }, + }, + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorText = await response.text() + throw new Error(`Failed to create Google Docs document: ${errorText}`) + } + + const data = await response.json() + const documentId = data.documentId + + // Get access token from the custom header we set in the request + const accessToken = response.headers.get('X-Access-Token') || '' + + // Extract content and folderId from the request URL query parameters + const requestUrl = new URL(response.url) + const content = requestUrl.searchParams.get('content') || '' + const folderId = requestUrl.searchParams.get('folderId') || '' + + // If content was provided, we need to update the document with content + if (documentId && content) { + try { + // Make a second request to update the document with content + const updateResponse = await fetch( + `https://docs.googleapis.com/v1/documents/${documentId}:batchUpdate`, + { + method: 'POST', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + requests: [ + { + insertText: { + location: { + index: 1, + }, + text: content, + }, + }, + ], + }), + } + ) + + if (!updateResponse.ok) { + console.warn('Failed to update document with content, but document was created') + } + } catch (error) { + console.warn('Error updating document with content:', error) + } + } + + // If folderId was provided, we need to move the document to that folder + if (documentId && folderId) { + try { + // Make a request to the Drive API to move the file + const moveResponse = await fetch( + `https://www.googleapis.com/drive/v3/files/${documentId}?addParents=${folderId}`, + { + method: 'PATCH', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + } + ) + + if (!moveResponse.ok) { + console.warn('Failed to move document to specified folder, but document was created') + } + } catch (error) { + console.warn('Error moving document to folder:', error) + } + } + + // Create document metadata + const metadata = { + documentId: data.documentId, + title: data.title || 'Untitled Document', + mimeType: 'application/vnd.google-apps.document', + url: `https://docs.google.com/document/d/${data.documentId}/edit`, + } + + return { + success: true, + output: { + metadata, + }, + } + }, + transformError: (error) => { + if (typeof error === 'object' && error !== null) { + return JSON.stringify(error) || 'An error occurred while creating Google Docs document' + } + return error.message || 'An error occurred while creating Google Docs document' + }, +} diff --git a/tools/docs/index.ts b/tools/docs/index.ts new file mode 100644 index 0000000000..bf5dc35932 --- /dev/null +++ b/tools/docs/index.ts @@ -0,0 +1,7 @@ +import { createTool } from './create' +import { readTool } from './read' +import { writeTool } from './write' + +export const docsReadTool = readTool +export const docsWriteTool = writeTool +export const docsCreateTool = createTool diff --git a/tools/docs/read.ts b/tools/docs/read.ts new file mode 100644 index 0000000000..521eafca53 --- /dev/null +++ b/tools/docs/read.ts @@ -0,0 +1,118 @@ +import { ToolConfig } from '../types' +import { GoogleDocsReadResponse, GoogleDocsToolParams } from './types' + +export const readTool: ToolConfig = { + id: 'google_docs_read', + name: 'Read Google Docs Document', + description: 'Read content from a Google Docs document', + version: '1.0', + oauth: { + required: true, + provider: 'google-docs', + additionalScopes: [ + 'https://www.googleapis.com/auth/documents', + 'https://www.googleapis.com/auth/drive.readonly', + ], + }, + params: { + accessToken: { type: 'string', required: true }, + documentId: { type: 'string', required: true }, + }, + request: { + url: (params) => { + // Ensure documentId is valid + const documentId = params.documentId?.trim() + if (!documentId) { + throw new Error('Document ID is required') + } + + return `https://docs.googleapis.com/v1/documents/${documentId}` + }, + method: 'GET', + headers: (params) => { + // Validate access token + if (!params.accessToken) { + throw new Error('Access token is required') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + } + }, + }, + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorText = await response.text() + throw new Error(`Failed to read Google Docs document: ${errorText}`) + } + + const data = await response.json() + + // Extract document content from the response + let content = '' + if (data.body && data.body.content) { + content = extractTextFromDocument(data) + } + + // Create document metadata + const metadata = { + documentId: data.documentId, + title: data.title || 'Untitled Document', + mimeType: 'application/vnd.google-apps.document', + url: `https://docs.google.com/document/d/${data.documentId}/edit`, + } + + return { + success: true, + output: { + content, + metadata, + }, + } + }, + transformError: (error) => { + if (typeof error === 'object' && error !== null) { + return JSON.stringify(error) || 'An error occurred while reading from Google Docs' + } + return error.message || 'An error occurred while reading from Google Docs' + }, +} + +// Helper function to extract text content from Google Docs document structure +function extractTextFromDocument(document: any): string { + let text = '' + + if (!document.body || !document.body.content) { + return text + } + + // Process each structural element in the document + for (const element of document.body.content) { + if (element.paragraph) { + for (const paragraphElement of element.paragraph.elements) { + if (paragraphElement.textRun && paragraphElement.textRun.content) { + text += paragraphElement.textRun.content + } + } + } else if (element.table) { + // Process tables if needed + for (const tableRow of element.table.tableRows) { + for (const tableCell of tableRow.tableCells) { + if (tableCell.content) { + for (const cellContent of tableCell.content) { + if (cellContent.paragraph) { + for (const paragraphElement of cellContent.paragraph.elements) { + if (paragraphElement.textRun && paragraphElement.textRun.content) { + text += paragraphElement.textRun.content + } + } + } + } + } + } + } + } + } + + return text +} diff --git a/tools/docs/types.ts b/tools/docs/types.ts new file mode 100644 index 0000000000..4dded1a525 --- /dev/null +++ b/tools/docs/types.ts @@ -0,0 +1,38 @@ +import { ToolResponse } from '../types' + +export interface GoogleDocsMetadata { + documentId: string + title: string + mimeType?: string + createdTime?: string + modifiedTime?: string + url?: string +} + +export interface GoogleDocsReadResponse extends ToolResponse { + output: { + content: string + metadata: GoogleDocsMetadata + } +} + +export interface GoogleDocsWriteResponse extends ToolResponse { + output: { + updatedContent: boolean + metadata: GoogleDocsMetadata + } +} + +export interface GoogleDocsCreateResponse extends ToolResponse { + output: { + metadata: GoogleDocsMetadata + } +} + +export interface GoogleDocsToolParams { + accessToken: string + documentId?: string + title?: string + content?: string + folderId?: string +} diff --git a/tools/docs/write.ts b/tools/docs/write.ts new file mode 100644 index 0000000000..52639d59eb --- /dev/null +++ b/tools/docs/write.ts @@ -0,0 +1,109 @@ +import { ToolConfig } from '../types' +import { GoogleDocsToolParams, GoogleDocsWriteResponse } from './types' + +export const writeTool: ToolConfig = { + id: 'google_docs_write', + name: 'Write to Google Docs Document', + description: 'Write or update content in a Google Docs document', + version: '1.0', + oauth: { + required: true, + provider: 'google-docs', + additionalScopes: [ + 'https://www.googleapis.com/auth/documents', + 'https://www.googleapis.com/auth/drive', + ], + }, + params: { + accessToken: { type: 'string', required: true }, + documentId: { type: 'string', required: true }, + content: { type: 'string', required: true }, + }, + request: { + url: (params) => { + // Ensure documentId is valid + const documentId = params.documentId?.trim() + if (!documentId) { + throw new Error('Document ID is required') + } + + return `https://docs.googleapis.com/v1/documents/${documentId}:batchUpdate` + }, + method: 'POST', + headers: (params) => { + // Validate access token + if (!params.accessToken) { + throw new Error('Access token is required') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } + }, + body: (params) => { + // Validate content + if (!params.content) { + throw new Error('Content is required') + } + + // Create a batch update request to replace all content + return { + requests: [ + { + // First, delete all existing content + deleteContentRange: { + range: { + startIndex: 1, // Start after the initial paragraph marker + endIndex: 999999, // A large number to ensure all content is deleted + }, + }, + }, + { + // Then insert the new content + insertText: { + location: { + index: 1, // Insert after the initial paragraph marker + }, + text: params.content, + }, + }, + ], + } + }, + }, + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorText = await response.text() + throw new Error(`Failed to write to Google Docs document: ${errorText}`) + } + + const data = await response.json() + + // Get the document ID from the URL + const urlParts = response.url.split('/') + const documentId = urlParts[urlParts.length - 2].split(':')[0] + + // Create document metadata + const metadata = { + documentId, + title: 'Updated Document', // We don't get the title back from the batchUpdate endpoint + mimeType: 'application/vnd.google-apps.document', + url: `https://docs.google.com/document/d/${documentId}/edit`, + } + + return { + success: true, + output: { + updatedContent: true, + metadata, + }, + } + }, + transformError: (error) => { + if (typeof error === 'object' && error !== null) { + return JSON.stringify(error) || 'An error occurred while writing to Google Docs' + } + return error.message || 'An error occurred while writing to Google Docs' + }, +} diff --git a/tools/index.ts b/tools/index.ts index 672f4f9de3..8d375bf40f 100644 --- a/tools/index.ts +++ b/tools/index.ts @@ -1,5 +1,6 @@ import { useCustomToolsStore } from '@/stores/custom-tools/store' import { useEnvironmentStore } from '@/stores/settings/environment/store' +import { docsCreateTool, docsReadTool, docsWriteTool } from './docs' import { driveDownloadTool, driveListTool, driveUploadTool } from './drive' import { exaAnswerTool, exaFindSimilarLinksTool, exaGetContentsTool, exaSearchTool } from './exa' import { scrapeTool } from './firecrawl/scrape' @@ -78,6 +79,9 @@ export const tools: Record = { google_drive_download: driveDownloadTool, google_drive_list: driveListTool, google_drive_upload: driveUploadTool, + google_docs_read: docsReadTool, + google_docs_write: docsWriteTool, + google_docs_create: docsCreateTool, google_sheets_read: sheetsReadTool, google_sheets_write: sheetsWriteTool, google_sheets_update: sheetsUpdateTool, @@ -513,6 +517,7 @@ async function handleProxyRequest( headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ toolId, params }), }) + console.log('Proxy response:', response) const result = await response.json()