import type { GoogleSheetsAppendResponse, GoogleSheetsToolParams, GoogleSheetsV2AppendResponse, GoogleSheetsV2ToolParams, } from '@/tools/google_sheets/types' import type { ToolConfig } from '@/tools/types' export const appendTool: ToolConfig = { id: 'google_sheets_append', name: 'Append to Google Sheets', description: 'Append data to the end of a Google Sheets spreadsheet', version: '1.0', oauth: { required: true, provider: 'google-sheets', }, params: { accessToken: { type: 'string', required: true, visibility: 'hidden', description: 'The access token for the Google Sheets API', }, spreadsheetId: { type: 'string', required: true, visibility: 'user-or-llm', description: 'The ID of the spreadsheet to append to', }, range: { type: 'string', required: false, visibility: 'user-or-llm', description: 'The A1 notation range to append after (e.g. "Sheet1", "Sheet1!A:D")', }, values: { type: 'array', required: true, visibility: 'user-or-llm', description: 'The data to append as a 2D array (e.g. [["Alice", 30], ["Bob", 25]]) or array of objects.', }, valueInputOption: { type: 'string', required: false, visibility: 'hidden', description: 'The format of the data to append', }, insertDataOption: { type: 'string', required: false, visibility: 'hidden', description: 'How to insert the data (OVERWRITE or INSERT_ROWS)', }, includeValuesInResponse: { type: 'boolean', required: false, visibility: 'hidden', description: 'Whether to include the appended values in the response', }, }, request: { url: (params) => { // If range is not provided, use a default range for the first sheet const range = params.range || 'Sheet1' const url = new URL( `https://sheets.googleapis.com/v4/spreadsheets/${params.spreadsheetId?.trim()}/values/${encodeURIComponent(range)}:append` ) // Default to USER_ENTERED if not specified const valueInputOption = params.valueInputOption || 'USER_ENTERED' url.searchParams.append('valueInputOption', valueInputOption) // Only send insertDataOption when the user provides it; the API defaults to OVERWRITE if (params.insertDataOption) { url.searchParams.append('insertDataOption', params.insertDataOption) } if (params.includeValuesInResponse) { url.searchParams.append('includeValuesInResponse', 'true') } return url.toString() }, method: 'POST', headers: (params) => ({ Authorization: `Bearer ${params.accessToken}`, 'Content-Type': 'application/json', }), body: (params) => { let processedValues: any = params.values || [] // Handle case where values might be a string (potentially JSON string) if (typeof processedValues === 'string') { try { // Try to parse it as JSON processedValues = JSON.parse(processedValues) } catch (_error) { // If the input contains literal newlines causing JSON parse to fail, // try a more robust approach try { // Replace literal newlines with escaped newlines for JSON parsing const sanitizedInput = (processedValues as string) .replace(/\n/g, '\\n') .replace(/\r/g, '\\r') .replace(/\t/g, '\\t') // Try to parse again with sanitized input processedValues = JSON.parse(sanitizedInput) } catch (_secondError) { // If all parsing attempts fail, wrap as a single cell value processedValues = [[processedValues]] } } } // New logic to handle array of objects if ( Array.isArray(processedValues) && processedValues.length > 0 && typeof processedValues[0] === 'object' && !Array.isArray(processedValues[0]) ) { // It's an array of objects // First, extract all unique keys from all objects to create headers const allKeys = new Set() processedValues.forEach((obj: any) => { if (obj && typeof obj === 'object') { Object.keys(obj).forEach((key) => allKeys.add(key)) } }) const headers = Array.from(allKeys) // Then create rows with object values in the order of headers const rows = processedValues.map((obj: any) => { if (!obj || typeof obj !== 'object') { // Handle non-object items by creating an array with empty values return Array(headers.length).fill('') } return headers.map((key) => { const value = obj[key] // Handle nested objects/arrays by converting to JSON string if (value !== null && typeof value === 'object') { return JSON.stringify(value) } return value === undefined ? '' : value }) }) // Add headers as the first row, then add data rows processedValues = [headers, ...rows] } // Continue with existing logic for other array types else if (!Array.isArray(processedValues)) { processedValues = [[String(processedValues)]] } else if (!processedValues.every((item: any) => Array.isArray(item))) { // If it's an array but not all elements are arrays, wrap each element processedValues = (processedValues as any[]).map((row: any) => Array.isArray(row) ? row : [String(row)] ) } const body: Record = { majorDimension: params.majorDimension || 'ROWS', values: processedValues, } // Only include range if it's provided if (params.range) { body.range = params.range } return body }, }, transformResponse: async (response: Response) => { const data = await response.json() // Extract spreadsheet ID from the URL (guard if url is missing) const urlParts = typeof response.url === 'string' ? response.url.split('/spreadsheets/') : [] const spreadsheetId = urlParts[1]?.split('/')[0] || '' // Create a simple metadata object with just the ID and URL const metadata = { spreadsheetId, properties: {}, spreadsheetUrl: `https://docs.google.com/spreadsheets/d/${spreadsheetId}`, } const result = { success: true, output: { tableRange: data.tableRange || '', updatedRange: data.updates?.updatedRange || '', updatedRows: data.updates?.updatedRows || 0, updatedColumns: data.updates?.updatedColumns || 0, updatedCells: data.updates?.updatedCells || 0, metadata: { spreadsheetId: metadata.spreadsheetId, spreadsheetUrl: metadata.spreadsheetUrl, }, }, } return result }, outputs: { tableRange: { type: 'string', description: 'Range of the table where data was appended' }, updatedRange: { type: 'string', description: 'Range of cells that were updated' }, updatedRows: { type: 'number', description: 'Number of rows updated' }, updatedColumns: { type: 'number', description: 'Number of columns updated' }, updatedCells: { type: 'number', description: 'Number of cells updated' }, metadata: { type: 'json', description: 'Spreadsheet metadata including ID and URL', properties: { spreadsheetId: { type: 'string', description: 'Google Sheets spreadsheet ID' }, spreadsheetUrl: { type: 'string', description: 'Spreadsheet URL' }, }, }, }, } export const appendV2Tool: ToolConfig = { id: 'google_sheets_append_v2', name: 'Append to Google Sheets V2', description: 'Append data to the end of a specific sheet in a Google Sheets spreadsheet', version: '2.0.0', oauth: { required: true, provider: 'google-sheets', }, params: { accessToken: { type: 'string', required: true, visibility: 'hidden', description: 'The access token for the Google Sheets API', }, spreadsheetId: { type: 'string', required: true, visibility: 'user-or-llm', description: 'Google Sheets spreadsheet ID', }, sheetName: { type: 'string', required: true, visibility: 'user-or-llm', description: 'The name of the sheet/tab to append to', }, values: { type: 'array', required: true, visibility: 'user-or-llm', description: 'The data to append as a 2D array (e.g. [["Alice", 30], ["Bob", 25]]) or array of objects.', }, valueInputOption: { type: 'string', required: false, visibility: 'hidden', description: 'The format of the data to append', }, insertDataOption: { type: 'string', required: false, visibility: 'hidden', description: 'How to insert the data (OVERWRITE or INSERT_ROWS)', }, includeValuesInResponse: { type: 'boolean', required: false, visibility: 'hidden', description: 'Whether to include the appended values in the response', }, }, request: { url: (params) => { const sheetName = params.sheetName?.trim() if (!sheetName) { throw new Error('Sheet name is required') } const url = new URL( `https://sheets.googleapis.com/v4/spreadsheets/${params.spreadsheetId?.trim()}/values/${encodeURIComponent(sheetName)}:append` ) const valueInputOption = params.valueInputOption || 'USER_ENTERED' url.searchParams.append('valueInputOption', valueInputOption) if (params.insertDataOption) { url.searchParams.append('insertDataOption', params.insertDataOption) } if (params.includeValuesInResponse) { url.searchParams.append('includeValuesInResponse', 'true') } return url.toString() }, method: 'POST', headers: (params) => ({ Authorization: `Bearer ${params.accessToken}`, 'Content-Type': 'application/json', }), body: (params) => { let processedValues: any = params.values || [] if (typeof processedValues === 'string') { try { processedValues = JSON.parse(processedValues) } catch (_error) { try { const sanitizedInput = (processedValues as string) .replace(/\n/g, '\\n') .replace(/\r/g, '\\r') .replace(/\t/g, '\\t') processedValues = JSON.parse(sanitizedInput) } catch (_secondError) { processedValues = [[processedValues]] } } } if ( Array.isArray(processedValues) && processedValues.length > 0 && typeof processedValues[0] === 'object' && !Array.isArray(processedValues[0]) ) { const allKeys = new Set() processedValues.forEach((obj: any) => { if (obj && typeof obj === 'object') { Object.keys(obj).forEach((key) => allKeys.add(key)) } }) const headers = Array.from(allKeys) const rows = processedValues.map((obj: any) => { if (!obj || typeof obj !== 'object') { return Array(headers.length).fill('') } return headers.map((key) => { const value = obj[key] if (value !== null && typeof value === 'object') { return JSON.stringify(value) } return value === undefined ? '' : value }) }) processedValues = [headers, ...rows] } else if (!Array.isArray(processedValues)) { processedValues = [[String(processedValues)]] } else if (!processedValues.every((item: any) => Array.isArray(item))) { processedValues = (processedValues as any[]).map((row: any) => Array.isArray(row) ? row : [String(row)] ) } const body: Record = { majorDimension: params.majorDimension || 'ROWS', values: processedValues, } return body }, }, transformResponse: async (response: Response) => { const data = await response.json() const urlParts = typeof response.url === 'string' ? response.url.split('/spreadsheets/') : [] const spreadsheetId = urlParts[1]?.split('/')[0] || '' const metadata = { spreadsheetId, spreadsheetUrl: `https://docs.google.com/spreadsheets/d/${spreadsheetId}`, } return { success: true, output: { tableRange: data.tableRange ?? '', updatedRange: data.updates?.updatedRange ?? '', updatedRows: data.updates?.updatedRows ?? 0, updatedColumns: data.updates?.updatedColumns ?? 0, updatedCells: data.updates?.updatedCells ?? 0, metadata: { spreadsheetId: metadata.spreadsheetId, spreadsheetUrl: metadata.spreadsheetUrl, }, }, } }, outputs: { tableRange: { type: 'string', description: 'Range of the table where data was appended' }, updatedRange: { type: 'string', description: 'Range of cells that were updated' }, updatedRows: { type: 'number', description: 'Number of rows updated' }, updatedColumns: { type: 'number', description: 'Number of columns updated' }, updatedCells: { type: 'number', description: 'Number of cells updated' }, metadata: { type: 'json', description: 'Spreadsheet metadata including ID and URL', properties: { spreadsheetId: { type: 'string', description: 'Google Sheets spreadsheet ID' }, spreadsheetUrl: { type: 'string', description: 'Spreadsheet URL' }, }, }, }, }