Skip to content

Commit 94c6795

Browse files
committed
updates
1 parent 86c5e1b commit 94c6795

File tree

5 files changed

+246
-29
lines changed

5 files changed

+246
-29
lines changed

apps/sim/app/api/table/[tableId]/rows/[rowId]/route.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ export async function GET(request: NextRequest, { params }: RowRouteParams) {
105105
}
106106
}
107107

108-
/** PATCH /api/table/[tableId]/rows/[rowId] - Updates a single row. */
108+
/** PATCH /api/table/[tableId]/rows/[rowId] - Updates a single row (supports partial updates). */
109109
export async function PATCH(request: NextRequest, { params }: RowRouteParams) {
110110
const requestId = generateRequestId()
111111
const { tableId, rowId } = await params
@@ -132,10 +132,31 @@ export async function PATCH(request: NextRequest, { params }: RowRouteParams) {
132132
return NextResponse.json({ error: 'Invalid workspace ID' }, { status: 400 })
133133
}
134134

135-
const rowData = validated.data as RowData
135+
// Fetch existing row to support partial updates
136+
const [existingRow] = await db
137+
.select({ data: userTableRows.data })
138+
.from(userTableRows)
139+
.where(
140+
and(
141+
eq(userTableRows.id, rowId),
142+
eq(userTableRows.tableId, tableId),
143+
eq(userTableRows.workspaceId, validated.workspaceId)
144+
)
145+
)
146+
.limit(1)
147+
148+
if (!existingRow) {
149+
return NextResponse.json({ error: 'Row not found' }, { status: 404 })
150+
}
151+
152+
// Merge existing data with incoming partial data (incoming takes precedence)
153+
const mergedData = {
154+
...(existingRow.data as RowData),
155+
...(validated.data as RowData),
156+
}
136157

137158
const validation = await validateRowData({
138-
rowData,
159+
rowData: mergedData,
139160
schema: table.schema as TableSchema,
140161
tableId,
141162
excludeRowId: rowId,
@@ -147,7 +168,7 @@ export async function PATCH(request: NextRequest, { params }: RowRouteParams) {
147168
const [updatedRow] = await db
148169
.update(userTableRows)
149170
.set({
150-
data: validated.data,
171+
data: mergedData,
151172
updatedAt: now,
152173
})
153174
.where(

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

Lines changed: 105 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,85 @@ import type { TableSummary } from '../types'
1010

1111
const logger = createLogger('TableLLMEnrichment')
1212

13+
/**
14+
* Cache for in-flight and recently fetched table schemas.
15+
* Key: tableId, Value: { promise, timestamp }
16+
* This deduplicates concurrent requests for the same table schema.
17+
*/
18+
const schemaCache = new Map<
19+
string,
20+
{
21+
promise: Promise<TableSummary | null>
22+
timestamp: number
23+
}
24+
>()
25+
26+
/** Schema cache TTL in milliseconds (5 seconds) */
27+
const SCHEMA_CACHE_TTL_MS = 5000
28+
29+
/**
30+
* Clears expired entries from the schema cache.
31+
*/
32+
function cleanupSchemaCache(): void {
33+
const now = Date.now()
34+
for (const [key, entry] of schemaCache.entries()) {
35+
if (now - entry.timestamp > SCHEMA_CACHE_TTL_MS) {
36+
schemaCache.delete(key)
37+
}
38+
}
39+
}
40+
41+
/**
42+
* Fetches table schema with caching and request deduplication.
43+
* If a request for the same table is already in flight, returns the same promise.
44+
*/
45+
async function fetchTableSchemaWithCache(
46+
tableId: string,
47+
context: TableEnrichmentContext
48+
): Promise<TableSummary | null> {
49+
// Clean up old entries periodically
50+
if (schemaCache.size > 50) {
51+
cleanupSchemaCache()
52+
}
53+
54+
const cacheKey = `${context.workspaceId}:${tableId}`
55+
const cached = schemaCache.get(cacheKey)
56+
57+
// If we have a cached entry that's still valid, return it
58+
if (cached && Date.now() - cached.timestamp < SCHEMA_CACHE_TTL_MS) {
59+
return cached.promise
60+
}
61+
62+
// Create a new fetch promise
63+
const fetchPromise = (async (): Promise<TableSummary | null> => {
64+
const schemaResult = await context.executeTool('table_get_schema', {
65+
tableId,
66+
_context: {
67+
workspaceId: context.workspaceId,
68+
workflowId: context.workflowId,
69+
},
70+
})
71+
72+
if (!schemaResult.success || !schemaResult.output) {
73+
logger.warn(`Failed to fetch table schema: ${schemaResult.error}`)
74+
return null
75+
}
76+
77+
return {
78+
name: schemaResult.output.name,
79+
columns: schemaResult.output.columns || [],
80+
}
81+
})()
82+
83+
// Cache the promise immediately to deduplicate concurrent requests
84+
schemaCache.set(cacheKey, {
85+
promise: fetchPromise,
86+
timestamp: Date.now(),
87+
})
88+
89+
return fetchPromise
90+
}
91+
1392
export interface TableEnrichmentContext {
1493
workspaceId: string
1594
workflowId: string
@@ -50,33 +129,17 @@ export async function enrichTableToolForLLM(
50129
}
51130

52131
try {
53-
logger.info(`Fetching schema for table ${tableId}`)
54-
55-
const schemaResult = await context.executeTool('table_get_schema', {
56-
tableId,
57-
_context: {
58-
workspaceId: context.workspaceId,
59-
workflowId: context.workflowId,
60-
},
61-
})
132+
// Use cached schema fetch to deduplicate concurrent requests for the same table
133+
const tableSchema = await fetchTableSchemaWithCache(tableId, context)
62134

63-
if (!schemaResult.success || !schemaResult.output) {
64-
logger.warn(`Failed to fetch table schema: ${schemaResult.error}`)
135+
if (!tableSchema) {
65136
return null
66137
}
67138

68-
const tableSchema: TableSummary = {
69-
name: schemaResult.output.name,
70-
columns: schemaResult.output.columns || [],
71-
}
72-
73139
// Apply enrichment using the existing utility functions
74140
const enrichedDescription = enrichTableToolDescription(originalDescription, tableSchema, toolId)
75-
76141
const enrichedParams = enrichTableToolParameters(llmSchema, tableSchema, toolId)
77142

78-
logger.info(`Enriched ${toolId} with ${tableSchema.columns.length} columns`)
79-
80143
return {
81144
description: enrichedDescription,
82145
parameters: {
@@ -86,7 +149,7 @@ export async function enrichTableToolForLLM(
86149
},
87150
}
88151
} catch (error) {
89-
logger.warn(`Error fetching table schema:`, error)
152+
logger.warn('Error fetching table schema:', error)
90153
return null
91154
}
92155
}
@@ -190,6 +253,16 @@ ${filterExample}${sortExample}`
190253
{} as Record<string, unknown>
191254
)
192255

256+
// Update operations support partial updates
257+
if (toolId === 'table_update_row') {
258+
return `${originalDescription}
259+
260+
Table "${table.name}" available columns:
261+
${columnList}
262+
263+
For updates, only include the fields you want to change. Example: {"${exampleCols[0]?.name || 'field'}": "new_value"}`
264+
}
265+
193266
return `${originalDescription}
194267
195268
Table "${table.name}" available columns:
@@ -268,9 +341,18 @@ export function enrichTableToolParameters(
268341
},
269342
{} as Record<string, unknown>
270343
)
271-
enrichedProperties.data = {
272-
...enrichedProperties.data,
273-
description: `REQUIRED object containing row values. Use columns: ${columnNames}. Example value: ${JSON.stringify(exampleData)}`,
344+
345+
// Update operations support partial updates - only include fields to change
346+
if (toolId === 'table_update_row') {
347+
enrichedProperties.data = {
348+
...enrichedProperties.data,
349+
description: `Object containing fields to update. Only include fields you want to change. Available columns: ${columnNames}`,
350+
}
351+
} else {
352+
enrichedProperties.data = {
353+
...enrichedProperties.data,
354+
description: `REQUIRED object containing row values. Use columns: ${columnNames}. Example value: ${JSON.stringify(exampleData)}`,
355+
}
274356
}
275357
}
276358

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

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/**
2+
* Wand enricher for table schema context.
3+
*/
4+
5+
import { db } from '@sim/db'
6+
import { userTableDefinitions } from '@sim/db/schema'
7+
import { createLogger } from '@sim/logger'
8+
import { and, eq } from 'drizzle-orm'
9+
import type { TableSchema } from '../types'
10+
11+
const logger = createLogger('TableWandEnricher')
12+
13+
/**
14+
* Wand enricher that provides table schema context.
15+
* Used by the wand API to inject table column information into the system prompt.
16+
*/
17+
export async function enrichTableSchema(
18+
workspaceId: string | null,
19+
context: Record<string, unknown>
20+
): Promise<string | null> {
21+
const tableId = context.tableId as string | undefined
22+
if (!tableId || !workspaceId) {
23+
return null
24+
}
25+
26+
try {
27+
const [table] = await db
28+
.select({
29+
name: userTableDefinitions.name,
30+
schema: userTableDefinitions.schema,
31+
})
32+
.from(userTableDefinitions)
33+
.where(
34+
and(eq(userTableDefinitions.id, tableId), eq(userTableDefinitions.workspaceId, workspaceId))
35+
)
36+
.limit(1)
37+
38+
if (!table) {
39+
return null
40+
}
41+
42+
const schema = table.schema as TableSchema | null
43+
if (!schema?.columns?.length) {
44+
return null
45+
}
46+
47+
const columnLines = schema.columns
48+
.map((col) => {
49+
const flags = [col.type, col.required && 'required', col.unique && 'unique'].filter(Boolean)
50+
return `- ${col.name} (${flags.join(', ')})`
51+
})
52+
.join('\n')
53+
54+
const label = table.name ? `${table.name} (${tableId})` : tableId
55+
return `Table schema for ${label}:\n${columnLines}\nBuilt-in columns: createdAt, updatedAt`
56+
} catch (error) {
57+
logger.debug('Failed to fetch table schema', { tableId, error })
58+
return null
59+
}
60+
}

apps/sim/tools/index.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,58 @@ function normalizeToolId(toolId: string): string {
4343
*/
4444
const MAX_REQUEST_BODY_SIZE_BYTES = 10 * 1024 * 1024 // 10MB
4545

46+
/**
47+
* Parameter aliases that LLMs commonly use as synonyms.
48+
* Maps alternative parameter names to their canonical names.
49+
* Key: toolId, Value: map of alias -> canonical parameter name
50+
*/
51+
const PARAMETER_ALIASES: Record<string, Record<string, string>> = {
52+
table_update_row: {
53+
values: 'data',
54+
row: 'data',
55+
fields: 'data',
56+
update: 'data',
57+
updates: 'data',
58+
changes: 'data',
59+
newData: 'data',
60+
rowData: 'data',
61+
},
62+
table_insert_row: {
63+
values: 'data',
64+
row: 'data',
65+
fields: 'data',
66+
rowData: 'data',
67+
},
68+
table_upsert_row: {
69+
values: 'data',
70+
row: 'data',
71+
fields: 'data',
72+
rowData: 'data',
73+
},
74+
}
75+
76+
/**
77+
* Applies parameter aliases to normalize LLM-provided parameters.
78+
* If the LLM uses an alias (e.g., "values" instead of "data"),
79+
* this function maps it to the canonical parameter name.
80+
*/
81+
function applyParameterAliases(toolId: string, params: Record<string, any>): Record<string, any> {
82+
const aliases = PARAMETER_ALIASES[toolId]
83+
if (!aliases) return params
84+
85+
const normalizedParams = { ...params }
86+
87+
for (const [alias, canonical] of Object.entries(aliases)) {
88+
// If the alias is present and the canonical name is not, copy the value
89+
if (alias in normalizedParams && !(canonical in normalizedParams)) {
90+
normalizedParams[canonical] = normalizedParams[alias]
91+
delete normalizedParams[alias]
92+
}
93+
}
94+
95+
return normalizedParams
96+
}
97+
4698
/**
4799
* User-friendly error message for body size limit exceeded
48100
*/
@@ -235,7 +287,8 @@ export async function executeTool(
235287
}
236288

237289
// Ensure context is preserved if it exists
238-
const contextParams = { ...params }
290+
// Apply parameter aliases to handle common LLM synonym usage (e.g., "values" -> "data")
291+
const contextParams = applyParameterAliases(normalizedToolId, { ...params })
239292

240293
// Validate the tool and its parameters
241294
validateRequiredParametersAfterMerge(toolId, tool, contextParams)

apps/sim/tools/table/update-row.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import type { TableRowResponse, TableRowUpdateParams } from './types'
44
export const tableUpdateRowTool: ToolConfig<TableRowUpdateParams, TableRowResponse> = {
55
id: 'table_update_row',
66
name: 'Update Row',
7-
description: 'Update an existing row in a table',
7+
description:
8+
'Update an existing row in a table. Supports partial updates - only include the fields you want to change.',
89
version: '1.0.0',
910

1011
params: {

0 commit comments

Comments
 (0)