Skip to content

Commit f4ba31e

Browse files
committed
fix(tables): add ncontains to the v2 operator lists and guard the enrichment wiring
Review findings. The operator list the v2 surfaces advertise omitted `ncontains`, which `FILTER_OPS` declares and `sql.ts` compiles to a negated ILIKE. An agent reading the tool schema therefore never knew the operator existed. The list is spelled out verbatim in several places and had already drifted from its source of truth on arrival; deriving it from `FILTER_OPS` is the real fix and is left as follow-up. The `toolEnrichment` block this branch adds had no test, which is the same gap that let v2 ship without enrichment in the first place — a refactor could silently drop it again, and a typo in the tool-id literal degrades to the generic description with no failure. Pin both halves: the tool declares the wiring, and the enricher answers in predicate grammar with `filter` left optional. Also cover the two `v2PredicateExample` arms an all-numeric or all-text table takes, and assert the empty-order case the previous test only executed without checking.
1 parent a690907 commit f4ba31e

4 files changed

Lines changed: 80 additions & 3 deletions

File tree

apps/sim/lib/table/llm/enrichment.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,30 @@ describe('enrichTableToolDescription for table_query_rows_v2', () => {
4646
expect(enriched).toContain('Example order: [{"field":"wins","direction":"desc"}]')
4747
})
4848

49+
/**
50+
* A metrics table is all-numeric and a lookup table is all-text; both are
51+
* common, and each picks a different arm of the example builder.
52+
*/
53+
it('builds a numeric example when the table has no string column', () => {
54+
const numeric = enrichTableToolDescription(
55+
'Query rows.',
56+
{ name: 'Scores', columns: [{ name: 'wins', type: 'number' }] },
57+
'table_query_rows_v2'
58+
)
59+
expect(numeric).toContain('{"field":"wins","op":"gte","value":10}')
60+
expect(numeric).not.toContain('AND group')
61+
})
62+
63+
it('builds a string example when the table has no numeric column', () => {
64+
const textual = enrichTableToolDescription(
65+
'Query rows.',
66+
{ name: 'Statuses', columns: [{ name: 'status', type: 'string' }] },
67+
'table_query_rows_v2'
68+
)
69+
expect(textual).toContain('{"field":"status","op":"eq","value":"active"}')
70+
expect(textual).not.toContain('"op":"gte"')
71+
})
72+
4973
it('omits the example rather than naming a placeholder column', () => {
5074
const bare = enrichTableToolDescription(
5175
'Query rows.',
@@ -54,6 +78,7 @@ describe('enrichTableToolDescription for table_query_rows_v2', () => {
5478
)
5579
expect(bare).toContain('payload (json)')
5680
expect(bare).not.toContain('Example filter')
81+
expect(bare).not.toContain('Example order')
5782
})
5883
})
5984

apps/sim/lib/table/llm/enrichment.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ INSTRUCTIONS:
9999
1. Build the filter yourself from the user's question - do NOT ask for confirmation
100100
2. A single condition is a plain object: {"field":"<column>","op":"<operator>","value":<value>}
101101
3. For multiple conditions wrap them in {"all":[...]} for AND or {"any":[...]} for OR; groups nest
102-
4. Operators: eq, ne, gt, gte, lt, lte, in, nin, like, ilike, nlike, nilike, contains, startsWith, endsWith, isNull, isNotNull, isEmpty, isNotEmpty
102+
4. Operators: eq, ne, gt, gte, lt, lte, in, nin, like, ilike, nlike, nilike, contains, ncontains, startsWith, endsWith, isNull, isNotNull, isEmpty, isNotEmpty
103103
5. like/ilike use * as the wildcard, e.g. {"field":"name","op":"ilike","value":"*jo*"}
104104
6. There are no array columns - for substring matching use ilike with *x*
105105
7. For ranking queries (highest, lowest, Nth, top N) set order and a small limit, e.g. limit 1 for the highest, 2 for the second highest
@@ -215,7 +215,7 @@ export function enrichTableToolParameters(
215215
if (enrichedProperties.filter) {
216216
enrichedProperties.filter = {
217217
...enrichedProperties.filter,
218-
description: `Predicate built from the user's question using columns: ${columnNames}. One condition is {"field":"<column>","op":"<operator>","value":<value>}; combine with {"all":[...]} for AND or {"any":[...]} for OR. Operators: eq, ne, gt, gte, lt, lte, in, nin, like, ilike, nlike, nilike, contains, startsWith, endsWith, isNull, isNotNull, isEmpty, isNotEmpty. Omit only to match every row.`,
218+
description: `Predicate built from the user's question using columns: ${columnNames}. One condition is {"field":"<column>","op":"<operator>","value":<value>}; combine with {"all":[...]} for AND or {"any":[...]} for OR. Operators: eq, ne, gt, gte, lt, lte, in, nin, like, ilike, nlike, nilike, contains, ncontains, startsWith, endsWith, isNull, isNotNull, isEmpty, isNotEmpty. Omit only to match every row.`,
219219
}
220220
}
221221

apps/sim/tools/schema-enrichers.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ vi.mock('@/lib/internal/table/read-schema', () => ({
1717
}))
1818

1919
import { enrichKBTagsSchema, enrichTableToolSchema } from '@/tools/schema-enrichers'
20+
import { tableQueryRowsV2Tool } from '@/tools/table/query_rows_v2'
2021

2122
const ORIGINAL_SCHEMA = {
2223
type: 'object' as const,
@@ -27,6 +28,18 @@ const ORIGINAL_SCHEMA = {
2728
required: [],
2829
}
2930

31+
const V2_SCHEMA = {
32+
type: 'object' as const,
33+
properties: {
34+
filter: { type: 'object' },
35+
order: { type: 'array' },
36+
columns: { type: 'array' },
37+
limit: { type: 'number' },
38+
cursor: { type: 'string' },
39+
},
40+
required: [],
41+
}
42+
3043
const EXECUTOR_ORIGIN = {
3144
subjectUserId: 'user-1',
3245
workflowId: 'workflow-1',
@@ -95,6 +108,45 @@ describe('enrichTableToolSchema', () => {
95108
enrichTableToolSchema('table-1', 'table_query_rows', ORIGINAL_SCHEMA, 'Query rows', {})
96109
).rejects.toThrow('Workflow ID is required to enrich table tool schema for table-1')
97110
})
111+
112+
/**
113+
* The v2 query tool shipped with no enrichment at all, so an agent using it
114+
* never saw the table's columns. These pin both halves of the fix: the tool
115+
* declares the wiring, and the enricher answers it in v2's grammar rather
116+
* than v1's.
117+
*/
118+
it('wires the v2 query tool to the enricher under its own tool id', () => {
119+
expect(tableQueryRowsV2Tool.toolEnrichment?.dependsOn).toBe('tableId')
120+
expect(tableQueryRowsV2Tool.toolEnrichment?.enrichTool).toBeTypeOf('function')
121+
})
122+
123+
it('enriches the v2 query tool with predicate grammar, not the v1 filter grammar', async () => {
124+
const result = await enrichTableToolSchema(
125+
'table-1',
126+
'table_query_rows_v2',
127+
V2_SCHEMA,
128+
'Query rows',
129+
{
130+
workspaceId: 'workspace-1',
131+
userId: 'user-1',
132+
workflowId: 'workflow-1',
133+
executionId: 'execution-1',
134+
executorDelegationOrigin: EXECUTOR_ORIGIN,
135+
}
136+
)
137+
138+
expect(result.description).toContain('Table "Customers" columns:')
139+
expect(result.description).toContain('"op":"gte"')
140+
expect(result.description).not.toContain('$eq')
141+
expect(result.parameters.properties.filter).toMatchObject({
142+
description: expect.stringContaining('email, score'),
143+
})
144+
expect(result.parameters.properties.cursor).toMatchObject({
145+
description: expect.stringContaining('nextCursor'),
146+
})
147+
// v2 returns every row when the filter is omitted, so it must stay optional.
148+
expect(result.parameters.required).not.toContain('filter')
149+
})
98150
})
99151

100152
describe('enrichKBTagsSchema', () => {

apps/sim/tools/table/query_rows_v2.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export const tableQueryRowsV2Tool: InternalToolConfig<TableRowQueryV2Params, Tab
1616
'Query rows with a typed predicate filter and cursor pagination. ' +
1717
'A single filter can be a plain condition: `{"field":"wins","op":"gte","value":10}`. ' +
1818
'Use `all` (AND) or `any` (OR) groups for multiple or nested conditions. Operators: eq, ne, gt, gte, lt, lte, in, nin, like, ilike, ' +
19-
'nlike, nilike, contains, startsWith, endsWith, isNull, isNotNull, isEmpty, isNotEmpty. ' +
19+
'nlike, nilike, contains, ncontains, startsWith, endsWith, isNull, isNotNull, isEmpty, isNotEmpty. ' +
2020
'Order is a sort spec, e.g. `[{"field":"wins","direction":"desc"}]`. Omit limit to return the entire result — ' +
2121
'the query fails if it exceeds the 5MB budget (narrow with a filter or set a limit). With a limit, ' +
2222
'a page can end early at the byte budget: a non-null nextCursor means more rows exist — pass it back ' +

0 commit comments

Comments
 (0)