feat(tools): added google docs tools/block

This commit is contained in:
Waleed Latif
2025-03-10 04:35:21 -07:00
parent d2621844bf
commit 85175aef2b
9 changed files with 642 additions and 4 deletions
+14 -4
View File
@@ -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 })
+192
View File
@@ -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<GoogleDocsResponse> = {
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',
},
},
},
}
+3
View File
@@ -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<string, BlockConfig> = {
function: FunctionBlock,
github: GitHubBlock,
gmail: GmailBlock,
google_docs: GoogleDocsBlock,
google_drive: GoogleDriveBlock,
google_sheets: GoogleSheetsBlock,
// guesty: GuestyBlock,
+156
View File
@@ -0,0 +1,156 @@
import { ToolConfig } from '../types'
import { GoogleDocsCreateResponse, GoogleDocsToolParams } from './types'
export const createTool: ToolConfig<GoogleDocsToolParams, GoogleDocsCreateResponse> = {
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'
},
}
+7
View File
@@ -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
+118
View File
@@ -0,0 +1,118 @@
import { ToolConfig } from '../types'
import { GoogleDocsReadResponse, GoogleDocsToolParams } from './types'
export const readTool: ToolConfig<GoogleDocsToolParams, GoogleDocsReadResponse> = {
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
}
+38
View File
@@ -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
}
+109
View File
@@ -0,0 +1,109 @@
import { ToolConfig } from '../types'
import { GoogleDocsToolParams, GoogleDocsWriteResponse } from './types'
export const writeTool: ToolConfig<GoogleDocsToolParams, GoogleDocsWriteResponse> = {
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'
},
}
+5
View File
@@ -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<string, ToolConfig> = {
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()