Skip to content

Commit 9324ef3

Browse files
authored
feat(excel): expand workbook operations and align Graph contracts (#9203)
Expand Excel workbook operations, align Microsoft Graph inputs and outputs, preserve legacy workflows, and regenerate integration documentation.
1 parent da5d059 commit 9324ef3

40 files changed

Lines changed: 3631 additions & 676 deletions

‎apps/docs/content/docs/integrations/microsoft_excel.mdx‎

Lines changed: 485 additions & 22 deletions
Large diffs are not rendered by default.

‎apps/sim/blocks/blocks/microsoft_excel.ts‎

Lines changed: 1116 additions & 194 deletions
Large diffs are not rendered by default.

‎apps/sim/lib/oauth/oauth.ts‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -503,7 +503,15 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
503503
providerId: 'microsoft-excel',
504504
icon: MicrosoftExcelIcon,
505505
baseProviderIcon: MicrosoftIcon,
506-
scopes: ['openid', 'profile', 'email', 'Files.Read', 'Files.ReadWrite', 'offline_access'],
506+
scopes: [
507+
'openid',
508+
'profile',
509+
'email',
510+
'Files.Read',
511+
'Files.ReadWrite',
512+
'Sites.Read.All',
513+
'offline_access',
514+
],
507515
},
508516
'microsoft-intune': {
509517
name: 'Microsoft Intune',

‎apps/sim/lib/workflows/migrations/subblock-migrations.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,11 @@ function isFieldProjection(value: unknown): boolean {
112112
* secret onto a live subblock.
113113
*/
114114
export const SUBBLOCK_ID_MIGRATIONS: Record<string, readonly SubblockIdMigration[]> = {
115+
microsoft_excel: [{ from: 'valueInputOption', to: '_removed_valueInputOption' }],
116+
microsoft_excel_v2: [
117+
{ from: 'valueInputOption', to: '_removed_valueInputOption' },
118+
{ from: 'sortTableName', to: 'tableName', whenOperation: ['sort_range'] },
119+
],
115120
/** MCP normalization selects the active replacement before this rename pass. */
116121
mcp: [
117122
{ from: 'server', to: 'serverSelector' },

‎apps/sim/providers/utils.test.ts‎

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ vi.mock('@/lib/internal/workflows/read-tool-enrichment', () => ({
1212
}))
1313

1414
import { IntuneBlock } from '@/blocks/blocks/intune'
15+
import { MicrosoftExcelV2Block } from '@/blocks/blocks/microsoft_excel'
1516
import { RampBlock } from '@/blocks/blocks/ramp'
1617
import { RevenueCatBlock } from '@/blocks/blocks/revenuecat'
1718
import { VideoGeneratorV3Block } from '@/blocks/blocks/video_generator'
@@ -44,6 +45,9 @@ import {
4445
import { useProvidersStore } from '@/stores/providers/store'
4546
import { intuneListDetectedAppsTool } from '@/tools/intune/list-detected-apps'
4647
import { intuneListDevicesTool } from '@/tools/intune/list-devices'
48+
import { clearRangeTool } from '@/tools/microsoft_excel/clear_range'
49+
import { createTableTool } from '@/tools/microsoft_excel/create_table'
50+
import { sortRangeTool } from '@/tools/microsoft_excel/sort_range'
4751
import { rampListReimbursementsTool } from '@/tools/ramp/list-reimbursements'
4852
import { rampListTransactionsTool } from '@/tools/ramp/list-transactions'
4953
import { revenuecatGetCustomerTool } from '@/tools/revenuecat/get_customer'
@@ -178,6 +182,116 @@ describe('integration filters through agent execution', () => {
178182
})
179183
})
180184

185+
describe('Excel targets through agent execution', () => {
186+
it.each(['model', 'configured'] as const)(
187+
'clears the %s range after canonical argument mapping',
188+
async (source) => {
189+
const providerTool = await transformBlockTool(
190+
{
191+
type: 'microsoft_excel_v2',
192+
params: {
193+
manualSpreadsheetId: 'workbook1',
194+
manualSheetName: 'Sheet1',
195+
...(source === 'configured' ? { cellRange: 'B2:D5' } : {}),
196+
},
197+
},
198+
{
199+
selectedOperation: 'clear_range',
200+
canonicalModes: { '0:spreadsheetId': 'advanced', '0:sheetName': 'advanced' },
201+
toolIndex: 0,
202+
getAllBlocks: () => [MicrosoftExcelV2Block],
203+
getTool: () => clearRangeTool,
204+
}
205+
)
206+
if (!providerTool) throw new Error('Excel agent tool was not created')
207+
const { executionParams } = prepareToolExecution(
208+
providerTool,
209+
{ range: 'A1:C4' },
210+
{},
211+
undefined
212+
)
213+
const params = {
214+
...executionParams,
215+
accessToken: 'fixture-token',
216+
spreadsheetId: executionParams.spreadsheetId,
217+
range: executionParams.range,
218+
}
219+
const url =
220+
typeof clearRangeTool.request.url === 'function'
221+
? clearRangeTool.request.url(params)
222+
: clearRangeTool.request.url
223+
expect(decodeURIComponent(url)).toContain(
224+
`range(address='${source === 'configured' ? 'B2:D5' : 'A1:C4'}')/clear`
225+
)
226+
}
227+
)
228+
229+
it('sorts the model-supplied table instead of an unused range', async () => {
230+
const providerTool = await transformBlockTool(
231+
{
232+
type: 'microsoft_excel_v2',
233+
params: { manualSpreadsheetId: 'workbook1', manualSheetName: 'Sheet1', cellRange: 'A1:C4' },
234+
},
235+
{
236+
selectedOperation: 'sort_range',
237+
canonicalModes: { '0:spreadsheetId': 'advanced', '0:sheetName': 'advanced' },
238+
toolIndex: 0,
239+
getAllBlocks: () => [MicrosoftExcelV2Block],
240+
getTool: () => sortRangeTool,
241+
}
242+
)
243+
if (!providerTool) throw new Error('Excel agent tool was not created')
244+
const { executionParams } = prepareToolExecution(
245+
providerTool,
246+
{ tableName: 'Inventory', sortColumn: 0, matchCase: true },
247+
{},
248+
undefined
249+
)
250+
const params = {
251+
...executionParams,
252+
accessToken: 'fixture-token',
253+
spreadsheetId: executionParams.spreadsheetId,
254+
}
255+
const url =
256+
typeof sortRangeTool.request.url === 'function'
257+
? sortRangeTool.request.url(params)
258+
: sortRangeTool.request.url
259+
expect(decodeURIComponent(url)).toContain("tables('Inventory')/sort/apply")
260+
expect(sortRangeTool.request.body?.(params)).toMatchObject({ matchCase: true })
261+
})
262+
263+
it('creates a table without converting an explicit false header flag to true', async () => {
264+
const providerTool = await transformBlockTool(
265+
{
266+
type: 'microsoft_excel_v2',
267+
params: { manualSpreadsheetId: 'workbook1', manualSheetName: 'Sheet1', cellRange: 'A1:C4' },
268+
},
269+
{
270+
selectedOperation: 'create_table',
271+
canonicalModes: { '0:spreadsheetId': 'advanced', '0:sheetName': 'advanced' },
272+
toolIndex: 0,
273+
getAllBlocks: () => [MicrosoftExcelV2Block],
274+
getTool: () => createTableTool,
275+
}
276+
)
277+
if (!providerTool) throw new Error('Excel agent tool was not created')
278+
const { executionParams } = prepareToolExecution(
279+
providerTool,
280+
{ hasHeaders: false },
281+
{},
282+
undefined
283+
)
284+
expect(
285+
createTableTool.request.body?.({
286+
...executionParams,
287+
accessToken: 'fixture-token',
288+
spreadsheetId: executionParams.spreadsheetId,
289+
address: executionParams.address,
290+
})
291+
).toMatchObject({ hasHeaders: false })
292+
})
293+
})
294+
181295
afterAll(resetEnvFlagsMock)
182296

183297
describe('getApiKey', () => {

‎apps/sim/tools/generated/tool-ids.ts‎

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

‎apps/sim/tools/generated/tool-metadata.ts‎

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

‎apps/sim/tools/generated/tool-outputs.ts‎

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { ErrorExtractorId } from '@/tools/error-extractors'
2+
import {
3+
assertExcelSuccess,
4+
EXCEL_WORKBOOK_PARAMS,
5+
excelHeaders,
6+
excelWorkbookUrl,
7+
} from '@/tools/microsoft_excel/shared'
8+
import type { ExcelCalculateParams, ExcelCalculateResponse } from '@/tools/microsoft_excel/types'
9+
import type { ToolConfig } from '@/tools/types'
10+
export const calculateTool: ToolConfig<ExcelCalculateParams, ExcelCalculateResponse> = {
11+
id: 'microsoft_excel_calculate',
12+
name: 'Microsoft Excel Calculate',
13+
description: 'Calculate in a Microsoft Excel workbook.',
14+
version: '1.0.0',
15+
oauth: { required: true, provider: 'microsoft-excel' },
16+
errorExtractor: ErrorExtractorId.MICROSOFT_GRAPH_ERRORS,
17+
params: {
18+
...EXCEL_WORKBOOK_PARAMS,
19+
calculationType: {
20+
type: 'string',
21+
required: true,
22+
visibility: 'user-or-llm',
23+
description:
24+
'Recalculate recalculates dirty cells; Full recalculates all cells; FullRebuild rebuilds dependencies and recalculates all cells.',
25+
},
26+
},
27+
request: {
28+
url: (params) => `${excelWorkbookUrl(params)}/application/calculate`,
29+
method: 'POST',
30+
headers: excelHeaders,
31+
body: (params) => {
32+
if (!['Recalculate', 'Full', 'FullRebuild'].includes(params.calculationType))
33+
throw new Error('Invalid calculation type')
34+
return { calculationType: params.calculationType }
35+
},
36+
},
37+
transformResponse: async (response) => {
38+
await assertExcelSuccess(response)
39+
return { success: true, output: { calculated: true } }
40+
},
41+
outputs: {
42+
calculated: { type: 'boolean', description: 'Whether workbook calculation completed.' },
43+
},
44+
}

‎apps/sim/tools/microsoft_excel/clear_range.ts‎

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
import { ErrorExtractorId } from '@/tools/error-extractors'
2+
import {
3+
assertExcelSuccess,
4+
EXCEL_WORKBOOK_PARAMS,
5+
excelHeaders,
6+
} from '@/tools/microsoft_excel/shared'
27
import type {
38
MicrosoftExcelClearRangeParams,
49
MicrosoftExcelClearRangeResponse,
@@ -8,7 +13,7 @@ import {
813
getItemBasePath,
914
getSpreadsheetWebUrl,
1015
} from '@/tools/microsoft_excel/utils'
11-
import type { ToolConfig } from '@/tools/types'
16+
import type { ToolConfig, ToolResponseContext } from '@/tools/types'
1217

1318
/**
1419
* Clears the contents and/or formatting of a worksheet range.
@@ -30,6 +35,7 @@ export const clearRangeTool: ToolConfig<
3035
},
3136

3237
params: {
38+
sessionId: EXCEL_WORKBOOK_PARAMS.sessionId,
3339
accessToken: {
3440
type: 'string',
3541
required: true,
@@ -47,7 +53,7 @@ export const clearRangeTool: ToolConfig<
4753
required: false,
4854
visibility: 'user-or-llm',
4955
description:
50-
'The ID of the drive containing the spreadsheet. Required for SharePoint files. If omitted, uses personal OneDrive.',
56+
'The ID of the drive containing the spreadsheet. Required for SharePoint files. If omitted, uses the signed-in user’s default OneDrive. Excel workbook APIs require a business account.',
5157
},
5258
sheetName: {
5359
type: 'string',
@@ -80,29 +86,26 @@ export const clearRangeTool: ToolConfig<
8086
return `${buildWorksheetRangeUrl(basePath, params.range, params.sheetName)}/clear`
8187
},
8288
method: 'POST',
83-
headers: (params) => {
84-
if (!params.accessToken) {
85-
throw new Error('Access token is required')
86-
}
87-
return {
88-
Authorization: `Bearer ${params.accessToken}`,
89-
'Content-Type': 'application/json',
90-
}
91-
},
89+
headers: excelHeaders,
9290
body: (params) => ({
9391
applyTo: params.applyTo || 'All',
9492
}),
9593
},
9694

97-
transformResponse: async (_response: Response, params?: MicrosoftExcelClearRangeParams) => {
95+
transformResponse: async (
96+
response: Response,
97+
params?: MicrosoftExcelClearRangeParams,
98+
context?: ToolResponseContext
99+
) => {
100+
await assertExcelSuccess(response)
98101
const spreadsheetId = params?.spreadsheetId?.trim() || ''
99102
const driveId = params?.driveId
100103

101104
const accessToken = params?.accessToken
102105
if (!accessToken) {
103106
throw new Error('Access token is required')
104107
}
105-
const webUrl = await getSpreadsheetWebUrl(spreadsheetId, accessToken, driveId)
108+
const webUrl = await getSpreadsheetWebUrl(spreadsheetId, accessToken, driveId, context?.signal)
106109

107110
return {
108111
success: true,

0 commit comments

Comments
 (0)