Skip to content

Commit 1f3f074

Browse files
fix(tables): pass enriched query schema to agents
1 parent 5dbe95e commit 1f3f074

15 files changed

Lines changed: 311 additions & 52 deletions

apps/sim/executor/handlers/agent/agent-handler.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { toError } from '@sim/utils/errors'
55
import { sleep } from '@sim/utils/helpers'
66
import { truncate } from '@sim/utils/string'
77
import { and, eq, inArray, isNull } from 'drizzle-orm'
8+
import { isDev } from '@/lib/core/config/env-flags'
89
import { normalizeStringRecord, normalizeWorkflowVariables } from '@/lib/core/utils/records'
910
import { createMcpToolId } from '@/lib/mcp/utils'
1011
import {
@@ -182,6 +183,32 @@ export class AgentBlockHandler implements BlockHandler {
182183
streaming: streamingConfig.shouldUseStreaming ?? false,
183184
})
184185

186+
if (isDev) {
187+
const tableQueryRowsV1Tools = formattedTools.filter((tool) => {
188+
const toolId = tool?.id
189+
if (typeof toolId !== 'string') return false
190+
191+
const isTableQueryRows =
192+
toolId === 'table_query_rows' || toolId.startsWith('table_query_rows_')
193+
const isV2 = toolId === 'table_query_rows_v2' || toolId.startsWith('table_query_rows_v2_')
194+
return isTableQueryRows && !isV2
195+
})
196+
197+
if (tableQueryRowsV1Tools.length > 0) {
198+
logger.info('Passing table_query_rows v1 tool schema to agent provider', {
199+
blockId: block.id,
200+
executionId: ctx.executionId,
201+
providerId,
202+
model,
203+
tools: tableQueryRowsV1Tools.map((tool) => ({
204+
id: tool.id,
205+
description: tool.description,
206+
parameters: tool.parameters,
207+
})),
208+
})
209+
}
210+
}
211+
185212
const result = await this.executeProviderRequest(ctx, providerRequest, block, responseFormat)
186213

187214
if (autoRouting && autoRouting.billableRoutingCost > 0) {
@@ -783,6 +810,12 @@ export class AgentBlockHandler implements BlockHandler {
783810
}),
784811
getTool,
785812
canonicalModes,
813+
enrichmentContext: {
814+
workflowId: ctx.workflowId,
815+
workspaceId: ctx.workspaceId,
816+
executionId: ctx.executionId,
817+
userId: ctx.userId,
818+
},
786819
toolIndex,
787820
resolveCustomBlockBinding: (blockType: string) =>
788821
resolveCustomBlockToolBinding(blockType, ctx.workspaceId),

apps/sim/executor/handlers/pi/sim-tools.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ export async function buildSimToolSpecs(
5454
getAllBlocks,
5555
getTool,
5656
getToolAsync,
57+
enrichmentContext: {
58+
workflowId: ctx.workflowId,
59+
workspaceId: ctx.workspaceId,
60+
executionId: ctx.executionId,
61+
userId: ctx.userId,
62+
},
5763
resolveCustomBlockBinding: (blockType: string) =>
5864
resolveCustomBlockToolBinding(blockType, ctx.workspaceId),
5965
})

apps/sim/providers/utils.test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1622,6 +1622,78 @@ describe('transformBlockTool multi-instance unique IDs', () => {
16221622
expect(result?.id).toBe('table_query_rows_tbl_abc')
16231623
})
16241624

1625+
it('resolves the canonical table id before enriching the LLM tool schema', async () => {
1626+
const enrichTool = vi.fn(
1627+
async (
1628+
tableId: string,
1629+
schema: {
1630+
type: 'object'
1631+
properties: Record<string, unknown>
1632+
required: string[]
1633+
}
1634+
) => ({
1635+
description: `Query rows from ${tableId}`,
1636+
parameters: {
1637+
...schema,
1638+
properties: {
1639+
...schema.properties,
1640+
customer_name: { type: 'string' },
1641+
},
1642+
},
1643+
})
1644+
)
1645+
const result = await transformBlockTool(
1646+
{
1647+
type: 'table',
1648+
operation: 'query_rows',
1649+
params: { tableSelector: 'tbl_abc' },
1650+
},
1651+
{
1652+
selectedOperation: 'query_rows',
1653+
getAllBlocks,
1654+
enrichmentContext: {
1655+
workspaceId: 'workspace-1',
1656+
userId: 'user-1',
1657+
},
1658+
getTool: (id: string) => ({
1659+
id,
1660+
name: 'Query Rows',
1661+
description: 'Query table rows',
1662+
params: {
1663+
tableId: { type: 'string', required: true, visibility: 'user-only' },
1664+
filter: { type: 'object', visibility: 'user-or-llm' },
1665+
},
1666+
toolEnrichment: {
1667+
dependsOn: 'tableId',
1668+
enrichTool,
1669+
},
1670+
}),
1671+
}
1672+
)
1673+
1674+
expect(enrichTool).toHaveBeenCalledWith(
1675+
'tbl_abc',
1676+
expect.objectContaining({
1677+
properties: expect.objectContaining({ filter: expect.any(Object) }),
1678+
}),
1679+
'Query table rows',
1680+
{
1681+
workspaceId: 'workspace-1',
1682+
userId: 'user-1',
1683+
}
1684+
)
1685+
expect(result).toMatchObject({
1686+
id: 'table_query_rows_tbl_abc',
1687+
description: 'Query rows from tbl_abc',
1688+
params: { tableSelector: 'tbl_abc' },
1689+
parameters: {
1690+
properties: {
1691+
customer_name: { type: 'string' },
1692+
},
1693+
},
1694+
})
1695+
})
1696+
16251697
it('appends the table id resolved from the advanced manual input', async () => {
16261698
const result = await transformTable(
16271699
{ manualTableId: 'tbl_xyz' },

apps/sim/providers/utils.ts

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type OpenAI from 'openai'
55
import type { BillingAttributionSnapshot } from '@/lib/billing/core/billing-attribution'
66
import { formatCreditCost } from '@/lib/billing/credits/conversion'
77
import { env } from '@/lib/core/config/env'
8-
import { getBlacklistedProvidersFromEnv, isHosted } from '@/lib/core/config/env-flags'
8+
import { getBlacklistedProvidersFromEnv, isDev, isHosted } from '@/lib/core/config/env-flags'
99
import {
1010
normalizeRecord,
1111
normalizeStringRecord,
@@ -53,6 +53,7 @@ import {
5353
import type { ProviderId, ProviderToolConfig } from '@/providers/types'
5454
import { useProvidersStore } from '@/stores/providers/store'
5555
import { mergeToolParameters } from '@/tools/merge-params'
56+
import type { WorkflowToolExecutionContext } from '@/tools/types'
5657

5758
const logger = createLogger('ProviderUtils')
5859

@@ -629,6 +630,7 @@ export async function transformBlockTool(
629630
getTool: (toolId: string) => any
630631
getToolAsync?: (toolId: string) => Promise<any>
631632
canonicalModes?: Record<string, 'basic' | 'advanced'>
633+
enrichmentContext?: WorkflowToolExecutionContext
632634
/**
633635
* Server-only resolver for a custom (deploy-as-block) tool's binding (bound
634636
* workflow + input schema), org-scoped to the consumer. Injected as a dependency
@@ -646,8 +648,15 @@ export async function transformBlockTool(
646648
toolIndex?: number
647649
}
648650
): Promise<ProviderToolConfig | null> {
649-
const { selectedOperation, getAllBlocks, getTool, getToolAsync, canonicalModes, toolIndex } =
650-
options
651+
const {
652+
selectedOperation,
653+
getAllBlocks,
654+
getTool,
655+
getToolAsync,
656+
canonicalModes,
657+
enrichmentContext,
658+
toolIndex,
659+
} = options
651660
const scopedCanonicalModes = scopeCanonicalModesForTool(canonicalModes, toolIndex, block.type)
652661

653662
const blockDef = getAllBlocks().find((b: any) => b.type === block.type)
@@ -755,12 +764,6 @@ export async function transformBlockTool(
755764

756765
const userProvidedParams = block.params || {}
757766

758-
const {
759-
schema: llmSchema,
760-
enrichedDescription,
761-
modelBlockedParams,
762-
} = await createLLMToolSchema(toolConfig, userProvidedParams)
763-
764767
const canonicalGroups: CanonicalGroup[] = blockDef?.subBlocks
765768
? Object.values(buildCanonicalIndex(blockDef.subBlocks).groupsById).filter(isCanonicalPair)
766769
: []
@@ -771,6 +774,12 @@ export async function transformBlockTool(
771774
scopedCanonicalModes
772775
)
773776

777+
const {
778+
schema: llmSchema,
779+
enrichedDescription,
780+
modelBlockedParams,
781+
} = await createLLMToolSchema(toolConfig, resolvedResourceParams, enrichmentContext)
782+
774783
let uniqueToolId = toolConfig.id
775784
let toolName = toolConfig.name
776785
let toolDescription = enrichedDescription || toolConfig.description
@@ -857,6 +866,16 @@ export async function transformBlockTool(
857866
}
858867
: undefined
859868

869+
if (isDev && toolConfig.id === 'table_query_rows') {
870+
logger.info('Prepared table_query_rows v1 tool schema', {
871+
toolId: uniqueToolId,
872+
tableId: resolvedResourceParams.tableId,
873+
enrichmentApplied: Boolean(enrichedDescription),
874+
description: toolDescription,
875+
parameters: llmSchema,
876+
})
877+
}
878+
860879
return {
861880
id: uniqueToolId,
862881
name: toolName,

apps/sim/tools/params.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import type {
2525
ParameterVisibility,
2626
ToolConfig,
2727
ToolParameterItemSchema,
28+
WorkflowToolExecutionContext,
2829
} from '@/tools/types'
2930

3031
const logger = createLogger('ToolsParams')
@@ -630,7 +631,8 @@ export function createUserToolSchema(
630631

631632
export async function createLLMToolSchema(
632633
toolConfig: ToolConfig,
633-
userProvidedParams: Record<string, unknown>
634+
userProvidedParams: Record<string, unknown>,
635+
enrichmentContext: WorkflowToolExecutionContext = {}
634636
): Promise<LLMToolSchemaResult> {
635637
const schema: ToolSchema = {
636638
type: 'object',
@@ -707,7 +709,8 @@ export async function createLLMToolSchema(
707709
const enriched = await toolConfig.toolEnrichment.enrichTool(
708710
dependencyValue,
709711
schema,
710-
toolConfig.description
712+
toolConfig.description,
713+
enrichmentContext
711714
)
712715
if (enriched) {
713716
return {
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
5+
6+
const { mockBuildAPIUrl, mockBuildAuthHeaders, mockExtractAPIErrorMessage } = vi.hoisted(() => ({
7+
mockBuildAPIUrl: vi.fn((path: string, params?: Record<string, string>) => {
8+
const url = new URL(path, 'http://localhost:3000')
9+
for (const [key, value] of Object.entries(params ?? {})) {
10+
url.searchParams.set(key, value)
11+
}
12+
return url
13+
}),
14+
mockBuildAuthHeaders: vi.fn(),
15+
mockExtractAPIErrorMessage: vi.fn(),
16+
}))
17+
18+
vi.mock('@/executor/utils/http', () => ({
19+
buildAPIUrl: mockBuildAPIUrl,
20+
buildAuthHeaders: mockBuildAuthHeaders,
21+
extractAPIErrorMessage: mockExtractAPIErrorMessage,
22+
}))
23+
24+
import { enrichTableToolSchema } from '@/tools/schema-enrichers'
25+
26+
const ORIGINAL_SCHEMA = {
27+
type: 'object' as const,
28+
properties: {
29+
filter: { type: 'object' },
30+
sort: { type: 'object' },
31+
},
32+
required: [],
33+
}
34+
35+
describe('enrichTableToolSchema', () => {
36+
beforeEach(() => {
37+
vi.clearAllMocks()
38+
mockBuildAuthHeaders.mockResolvedValue({ Authorization: 'Bearer internal-token' })
39+
})
40+
41+
afterEach(() => {
42+
vi.unstubAllGlobals()
43+
})
44+
45+
it('fetches the table through the authenticated detail route and enriches the schema', async () => {
46+
const mockFetch = vi.fn().mockResolvedValue(
47+
new Response(
48+
JSON.stringify({
49+
success: true,
50+
data: {
51+
table: {
52+
name: 'Customers',
53+
schema: {
54+
columns: [
55+
{ name: 'email', type: 'string' },
56+
{ name: 'score', type: 'number' },
57+
],
58+
},
59+
},
60+
},
61+
}),
62+
{ status: 200, headers: { 'Content-Type': 'application/json' } }
63+
)
64+
)
65+
vi.stubGlobal('fetch', mockFetch)
66+
67+
const result = await enrichTableToolSchema(
68+
'table-1',
69+
'table_query_rows',
70+
ORIGINAL_SCHEMA,
71+
'Query rows',
72+
{ workspaceId: 'workspace-1', userId: 'user-1' }
73+
)
74+
75+
expect(mockBuildAuthHeaders).toHaveBeenCalledWith('user-1')
76+
expect(mockFetch).toHaveBeenCalledWith(
77+
'http://localhost:3000/api/table/table-1?workspaceId=workspace-1',
78+
{ headers: { Authorization: 'Bearer internal-token' } }
79+
)
80+
expect(result.description).toContain('Table "Customers" columns:')
81+
expect(result.parameters.required).toContain('filter')
82+
expect(result.parameters.properties.filter).toMatchObject({
83+
description: expect.stringContaining('email, score'),
84+
})
85+
})
86+
87+
it('fails when the table detail request fails', async () => {
88+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(null, { status: 404 })))
89+
mockExtractAPIErrorMessage.mockResolvedValue('Table not found')
90+
91+
await expect(
92+
enrichTableToolSchema('missing-table', 'table_query_rows', ORIGINAL_SCHEMA, 'Query rows', {
93+
workspaceId: 'workspace-1',
94+
userId: 'user-1',
95+
})
96+
).rejects.toThrow('Failed to fetch table schema for missing-table: Table not found')
97+
})
98+
99+
it('fails when trusted execution identity is missing', async () => {
100+
await expect(
101+
enrichTableToolSchema('table-1', 'table_query_rows', ORIGINAL_SCHEMA, 'Query rows', {})
102+
).rejects.toThrow('Workspace ID is required to enrich table tool schema for table-1')
103+
})
104+
})

0 commit comments

Comments
 (0)