import { ErrorExtractorId } from '@/tools/error-extractors' import type { MicrosoftExcelToolParams, MicrosoftExcelV2ToolParams, MicrosoftExcelV2WriteResponse, MicrosoftExcelWriteResponse, } from '@/tools/microsoft_excel/types' import { escapeODataString, getItemBasePath, getSpreadsheetWebUrl, } from '@/tools/microsoft_excel/utils' import type { ToolConfig } from '@/tools/types' /** * Range writes (PATCH /workbook/.../range) are semantically idempotent — * the same payload produces the same result — so we permit retries on * transient 429/5xx even though PATCH is not in the default idempotent set. */ const EXCEL_RETRY_CONFIG = { enabled: true, maxRetries: 3, initialDelayMs: 500, maxDelayMs: 30000, retryIdempotentOnly: false, } as const export const writeTool: ToolConfig = { id: 'microsoft_excel_write', name: 'Write to Microsoft Excel', description: 'Write data to a Microsoft Excel spreadsheet', version: '1.0', errorExtractor: ErrorExtractorId.MICROSOFT_GRAPH_ERRORS, oauth: { required: true, provider: 'microsoft-excel', }, params: { accessToken: { type: 'string', required: true, visibility: 'hidden', description: 'The access token for the Microsoft Excel API', }, spreadsheetId: { type: 'string', required: true, visibility: 'user-or-llm', description: 'The ID of the spreadsheet/workbook to write to (e.g., "01ABC123DEF456")', }, driveId: { type: 'string', required: false, visibility: 'user-or-llm', description: 'The ID of the drive containing the spreadsheet. Required for SharePoint files. If omitted, uses personal OneDrive.', }, range: { type: 'string', required: false, visibility: 'user-or-llm', description: 'The range of cells to write to (e.g., "Sheet1!A1:B2")', }, values: { type: 'array', required: true, visibility: 'user-or-llm', description: 'The data to write as a 2D array (e.g., [["Name", "Age"], ["Alice", 30]]) or array of objects', }, valueInputOption: { type: 'string', required: false, visibility: 'user-only', description: 'The format of the data to write', }, includeValuesInResponse: { type: 'boolean', required: false, visibility: 'hidden', description: 'Whether to include the written values in the response', }, }, request: { url: (params) => { const spreadsheetId = params.spreadsheetId?.trim() if (!spreadsheetId) { throw new Error('Spreadsheet ID is required') } const rangeInput = params.range?.trim() const match = rangeInput?.match(/^([^!]+)!(.+)$/) if (!match) { throw new Error(`Invalid range format: "${params.range}". Use the format "Sheet1!A1:B2"`) } const sheetName = encodeURIComponent(escapeODataString(match[1])) const address = encodeURIComponent(match[2]) const basePath = getItemBasePath(spreadsheetId, params.driveId) const url = new URL( `${basePath}/workbook/worksheets('${sheetName}')/range(address='${address}')` ) const valueInputOption = params.valueInputOption || 'USER_ENTERED' url.searchParams.append('valueInputOption', valueInputOption) if (params.includeValuesInResponse) { url.searchParams.append('includeValuesInResponse', 'true') } return url.toString() }, method: 'PATCH', headers: (params) => ({ Authorization: `Bearer ${params.accessToken}`, 'Content-Type': 'application/json', }), body: (params) => { let processedValues: any = params.values || [] 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] } const body: Record = { majorDimension: 'ROWS', values: processedValues, } if (params.range) { body.range = params.range } return body }, retry: EXCEL_RETRY_CONFIG, }, transformResponse: async (response: Response, params?: MicrosoftExcelToolParams) => { const data = await response.json() const spreadsheetId = params?.spreadsheetId?.trim() || '' const driveId = params?.driveId const accessToken = params?.accessToken if (!accessToken) { throw new Error('Access token is required') } const webUrl = await getSpreadsheetWebUrl(spreadsheetId, accessToken, driveId) return { success: true, output: { updatedRange: data.address ?? '', updatedRows: data.rowCount ?? 0, updatedColumns: data.columnCount ?? 0, updatedCells: (data.rowCount ?? 0) * (data.columnCount ?? 0), metadata: { spreadsheetId, spreadsheetUrl: webUrl, }, }, } }, outputs: { updatedRange: { type: 'string', description: 'The range that was updated' }, updatedRows: { type: 'number', description: 'Number of rows that were updated' }, updatedColumns: { type: 'number', description: 'Number of columns that were updated' }, updatedCells: { type: 'number', description: 'Number of cells that were updated' }, metadata: { type: 'object', description: 'Spreadsheet metadata', properties: { spreadsheetId: { type: 'string', description: 'The ID of the spreadsheet' }, spreadsheetUrl: { type: 'string', description: 'URL to access the spreadsheet' }, }, }, }, } export const writeV2Tool: ToolConfig = { id: 'microsoft_excel_write_v2', name: 'Write to Microsoft Excel V2', description: 'Write data to a specific sheet in a Microsoft Excel spreadsheet', version: '2.0.0', errorExtractor: ErrorExtractorId.MICROSOFT_GRAPH_ERRORS, oauth: { required: true, provider: 'microsoft-excel', }, params: { accessToken: { type: 'string', required: true, visibility: 'hidden', description: 'The access token for the Microsoft Excel API', }, spreadsheetId: { type: 'string', required: true, visibility: 'user-or-llm', description: 'The ID of the spreadsheet/workbook to write to (e.g., "01ABC123DEF456")', }, driveId: { type: 'string', required: false, visibility: 'user-or-llm', description: 'The ID of the drive containing the spreadsheet. Required for SharePoint files. If omitted, uses personal OneDrive.', }, sheetName: { type: 'string', required: true, visibility: 'user-or-llm', description: 'The name of the sheet/tab to write to (e.g., "Sheet1", "Sales Data")', }, cellRange: { type: 'string', required: false, visibility: 'user-or-llm', description: 'The cell range to write to (e.g., "A1:D10", "A1"). Defaults to "A1" if not specified.', }, values: { type: 'array', required: true, visibility: 'user-or-llm', description: 'The data to write as a 2D array (e.g. [["Name", "Age"], ["Alice", 30], ["Bob", 25]]) or array of objects.', }, valueInputOption: { type: 'string', required: false, visibility: 'hidden', description: 'The format of the data to write', }, includeValuesInResponse: { type: 'boolean', required: false, visibility: 'hidden', description: 'Whether to include the written values in the response', }, }, request: { url: (params) => { const spreadsheetId = params.spreadsheetId?.trim() if (!spreadsheetId) { throw new Error('Spreadsheet ID is required') } const sheetName = params.sheetName?.trim() if (!sheetName) { throw new Error('Sheet name is required') } const cellRange = params.cellRange?.trim() || 'A1' const encodedSheetName = encodeURIComponent(escapeODataString(sheetName)) const encodedAddress = encodeURIComponent(cellRange) const basePath = getItemBasePath(spreadsheetId, params.driveId) const url = new URL( `${basePath}/workbook/worksheets('${encodedSheetName}')/range(address='${encodedAddress}')` ) const valueInputOption = params.valueInputOption || 'USER_ENTERED' url.searchParams.append('valueInputOption', valueInputOption) if (params.includeValuesInResponse) { url.searchParams.append('includeValuesInResponse', 'true') } return url.toString() }, method: 'PATCH', headers: (params) => ({ Authorization: `Bearer ${params.accessToken}`, 'Content-Type': 'application/json', }), body: (params) => { let processedValues: any = params.values || [] // Handle array of objects 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] } const body: Record = { majorDimension: 'ROWS', values: processedValues, } return body }, retry: EXCEL_RETRY_CONFIG, }, transformResponse: async (response: Response, params?: MicrosoftExcelV2ToolParams) => { const data = await response.json() const spreadsheetId = params?.spreadsheetId?.trim() || '' const driveId = params?.driveId const accessToken = params?.accessToken if (!accessToken) { throw new Error('Access token is required') } const webUrl = await getSpreadsheetWebUrl(spreadsheetId, accessToken, driveId) return { success: true, output: { updatedRange: data.address ?? null, updatedRows: data.rowCount ?? 0, updatedColumns: data.columnCount ?? 0, updatedCells: (data.rowCount ?? 0) * (data.columnCount ?? 0), metadata: { spreadsheetId, spreadsheetUrl: webUrl, }, }, } }, outputs: { 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: 'Microsoft Excel spreadsheet ID' }, spreadsheetUrl: { type: 'string', description: 'Spreadsheet URL' }, }, }, }, }