Skip to content

Commit 730164a

Browse files
fix(custom-tool): fix textarea, param dropdown for available params, validation for invalid schemas, variable resolution in custom tools and subflow tags (#1117)
* fix(custom-tools): fix text area for custom tools * added param dropdown in agent custom tool * add syntax highlighting for params, fix dropdown styling * ux * add tooltip to prevent indicate invalid json schema on schema and code tabs * feat(custom-tool): added stricter JSON schema validation and error when saving json schema for custom tools * fix(custom-tool): allow variable resolution in custom tools * fix variable resolution in subflow tags * refactored function execution to use helpers * cleanup * fix block variable resolution to inject at runtime * fix highlighting code --------- Co-authored-by: Vikhyath Mondreti <[email protected]>
1 parent 25b2c45 commit 730164a

File tree

26 files changed

+687
-243
lines changed

26 files changed

+687
-243
lines changed

apps/sim/app/api/function/execute/route.ts

Lines changed: 124 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -213,24 +213,81 @@ function createUserFriendlyErrorMessage(
213213
}
214214

215215
/**
216-
* Resolves environment variables and tags in code
217-
* @param code - Code with variables
218-
* @param params - Parameters that may contain variable values
219-
* @param envVars - Environment variables from the workflow
220-
* @returns Resolved code
216+
* Resolves workflow variables with <variable.name> syntax
221217
*/
218+
function resolveWorkflowVariables(
219+
code: string,
220+
workflowVariables: Record<string, any>,
221+
contextVariables: Record<string, any>
222+
): string {
223+
let resolvedCode = code
222224

223-
function resolveCodeVariables(
225+
const variableMatches = resolvedCode.match(/<variable\.([^>]+)>/g) || []
226+
for (const match of variableMatches) {
227+
const variableName = match.slice('<variable.'.length, -1).trim()
228+
229+
// Find the variable by name (workflowVariables is indexed by ID, values are variable objects)
230+
const foundVariable = Object.entries(workflowVariables).find(
231+
([_, variable]) => (variable.name || '').replace(/\s+/g, '') === variableName
232+
)
233+
234+
if (foundVariable) {
235+
const variable = foundVariable[1]
236+
// Get the typed value - handle different variable types
237+
let variableValue = variable.value
238+
239+
if (variable.value !== undefined && variable.value !== null) {
240+
try {
241+
// Handle 'string' type the same as 'plain' for backward compatibility
242+
const type = variable.type === 'string' ? 'plain' : variable.type
243+
244+
// For plain text, use exactly what's entered without modifications
245+
if (type === 'plain' && typeof variableValue === 'string') {
246+
// Use as-is for plain text
247+
} else if (type === 'number') {
248+
variableValue = Number(variableValue)
249+
} else if (type === 'boolean') {
250+
variableValue = variableValue === 'true' || variableValue === true
251+
} else if (type === 'json') {
252+
try {
253+
variableValue =
254+
typeof variableValue === 'string' ? JSON.parse(variableValue) : variableValue
255+
} catch {
256+
// Keep original value if JSON parsing fails
257+
}
258+
}
259+
} catch (error) {
260+
// Fallback to original value on error
261+
variableValue = variable.value
262+
}
263+
}
264+
265+
// Create a safe variable reference
266+
const safeVarName = `__variable_${variableName.replace(/[^a-zA-Z0-9_]/g, '_')}`
267+
contextVariables[safeVarName] = variableValue
268+
269+
// Replace the variable reference with the safe variable name
270+
resolvedCode = resolvedCode.replace(new RegExp(escapeRegExp(match), 'g'), safeVarName)
271+
} else {
272+
// Variable not found - replace with empty string to avoid syntax errors
273+
resolvedCode = resolvedCode.replace(new RegExp(escapeRegExp(match), 'g'), '')
274+
}
275+
}
276+
277+
return resolvedCode
278+
}
279+
280+
/**
281+
* Resolves environment variables with {{var_name}} syntax
282+
*/
283+
function resolveEnvironmentVariables(
224284
code: string,
225285
params: Record<string, any>,
226-
envVars: Record<string, string> = {},
227-
blockData: Record<string, any> = {},
228-
blockNameMapping: Record<string, string> = {}
229-
): { resolvedCode: string; contextVariables: Record<string, any> } {
286+
envVars: Record<string, string>,
287+
contextVariables: Record<string, any>
288+
): string {
230289
let resolvedCode = code
231-
const contextVariables: Record<string, any> = {}
232290

233-
// Resolve environment variables with {{var_name}} syntax
234291
const envVarMatches = resolvedCode.match(/\{\{([^}]+)\}\}/g) || []
235292
for (const match of envVarMatches) {
236293
const varName = match.slice(2, -2).trim()
@@ -245,7 +302,21 @@ function resolveCodeVariables(
245302
resolvedCode = resolvedCode.replace(new RegExp(escapeRegExp(match), 'g'), safeVarName)
246303
}
247304

248-
// Resolve tags with <tag_name> syntax (including nested paths like <block.response.data>)
305+
return resolvedCode
306+
}
307+
308+
/**
309+
* Resolves tags with <tag_name> syntax (including nested paths like <block.response.data>)
310+
*/
311+
function resolveTagVariables(
312+
code: string,
313+
params: Record<string, any>,
314+
blockData: Record<string, any>,
315+
blockNameMapping: Record<string, string>,
316+
contextVariables: Record<string, any>
317+
): string {
318+
let resolvedCode = code
319+
249320
const tagMatches = resolvedCode.match(/<([a-zA-Z_][a-zA-Z0-9_.]*[a-zA-Z0-9_])>/g) || []
250321

251322
for (const match of tagMatches) {
@@ -300,6 +371,42 @@ function resolveCodeVariables(
300371
resolvedCode = resolvedCode.replace(new RegExp(escapeRegExp(match), 'g'), safeVarName)
301372
}
302373

374+
return resolvedCode
375+
}
376+
377+
/**
378+
* Resolves environment variables and tags in code
379+
* @param code - Code with variables
380+
* @param params - Parameters that may contain variable values
381+
* @param envVars - Environment variables from the workflow
382+
* @returns Resolved code
383+
*/
384+
function resolveCodeVariables(
385+
code: string,
386+
params: Record<string, any>,
387+
envVars: Record<string, string> = {},
388+
blockData: Record<string, any> = {},
389+
blockNameMapping: Record<string, string> = {},
390+
workflowVariables: Record<string, any> = {}
391+
): { resolvedCode: string; contextVariables: Record<string, any> } {
392+
let resolvedCode = code
393+
const contextVariables: Record<string, any> = {}
394+
395+
// Resolve workflow variables with <variable.name> syntax first
396+
resolvedCode = resolveWorkflowVariables(resolvedCode, workflowVariables, contextVariables)
397+
398+
// Resolve environment variables with {{var_name}} syntax
399+
resolvedCode = resolveEnvironmentVariables(resolvedCode, params, envVars, contextVariables)
400+
401+
// Resolve tags with <tag_name> syntax (including nested paths like <block.response.data>)
402+
resolvedCode = resolveTagVariables(
403+
resolvedCode,
404+
params,
405+
blockData,
406+
blockNameMapping,
407+
contextVariables
408+
)
409+
303410
return { resolvedCode, contextVariables }
304411
}
305412

@@ -338,6 +445,7 @@ export async function POST(req: NextRequest) {
338445
envVars = {},
339446
blockData = {},
340447
blockNameMapping = {},
448+
workflowVariables = {},
341449
workflowId,
342450
isCustomTool = false,
343451
} = body
@@ -360,16 +468,17 @@ export async function POST(req: NextRequest) {
360468
executionParams,
361469
envVars,
362470
blockData,
363-
blockNameMapping
471+
blockNameMapping,
472+
workflowVariables
364473
)
365474
resolvedCode = codeResolution.resolvedCode
366475
const contextVariables = codeResolution.contextVariables
367476

368477
const executionMethod = 'vm' // Default execution method
369478

370479
logger.info(`[${requestId}] Using VM for code execution`, {
371-
resolvedCode,
372480
hasEnvVars: Object.keys(envVars).length > 0,
481+
hasWorkflowVariables: Object.keys(workflowVariables).length > 0,
373482
})
374483

375484
// Create a secure context with console logging

apps/sim/app/api/providers/route.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ export async function POST(request: NextRequest) {
3939
stream,
4040
messages,
4141
environmentVariables,
42+
workflowVariables,
43+
blockData,
44+
blockNameMapping,
4245
reasoningEffort,
4346
verbosity,
4447
} = body
@@ -60,6 +63,7 @@ export async function POST(request: NextRequest) {
6063
messageCount: messages?.length || 0,
6164
hasEnvironmentVariables:
6265
!!environmentVariables && Object.keys(environmentVariables).length > 0,
66+
hasWorkflowVariables: !!workflowVariables && Object.keys(workflowVariables).length > 0,
6367
reasoningEffort,
6468
verbosity,
6569
})
@@ -103,6 +107,9 @@ export async function POST(request: NextRequest) {
103107
stream,
104108
messages,
105109
environmentVariables,
110+
workflowVariables,
111+
blockData,
112+
blockNameMapping,
106113
reasoningEffort,
107114
verbosity,
108115
})

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/tool-input/components/code-editor/code-editor.tsx

Lines changed: 77 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ interface CodeEditorProps {
1818
highlightVariables?: boolean
1919
onKeyDown?: (e: React.KeyboardEvent) => void
2020
disabled?: boolean
21+
schemaParameters?: Array<{ name: string; type: string; description: string; required: boolean }>
2122
}
2223

2324
export function CodeEditor({
@@ -30,6 +31,7 @@ export function CodeEditor({
3031
highlightVariables = true,
3132
onKeyDown,
3233
disabled = false,
34+
schemaParameters = [],
3335
}: CodeEditorProps) {
3436
const [code, setCode] = useState(value)
3537
const [visualLineHeights, setVisualLineHeights] = useState<number[]>([])
@@ -120,25 +122,80 @@ export function CodeEditor({
120122
// First, get the default Prism highlighting
121123
let highlighted = highlight(code, languages[language], language)
122124

123-
// Then, highlight environment variables with {{var_name}} syntax in blue
124-
if (highlighted.includes('{{')) {
125-
highlighted = highlighted.replace(
126-
/\{\{([^}]+)\}\}/g,
127-
'<span class="text-blue-500">{{$1}}</span>'
128-
)
125+
// Collect all syntax highlights to apply in a single pass
126+
type SyntaxHighlight = {
127+
start: number
128+
end: number
129+
replacement: string
129130
}
131+
const highlights: SyntaxHighlight[] = []
130132

131-
// Also highlight tags with <tag_name> syntax in blue
132-
if (highlighted.includes('<') && !language.includes('html')) {
133-
highlighted = highlighted.replace(/<([^>\s/]+)>/g, (match, group) => {
134-
// Avoid replacing HTML tags in comments
135-
if (match.startsWith('<!--') || match.includes('</')) {
136-
return match
133+
// Find environment variables with {{var_name}} syntax
134+
let match
135+
const envVarRegex = /\{\{([^}]+)\}\}/g
136+
while ((match = envVarRegex.exec(highlighted)) !== null) {
137+
highlights.push({
138+
start: match.index,
139+
end: match.index + match[0].length,
140+
replacement: `<span class="text-blue-500">${match[0]}</span>`,
141+
})
142+
}
143+
144+
// Find tags with <tag_name> syntax (not in HTML context)
145+
if (!language.includes('html')) {
146+
const tagRegex = /<([^>\s/]+)>/g
147+
while ((match = tagRegex.exec(highlighted)) !== null) {
148+
// Skip HTML comments and closing tags
149+
if (!match[0].startsWith('<!--') && !match[0].includes('</')) {
150+
const escaped = `&lt;${match[1]}&gt;`
151+
highlights.push({
152+
start: match.index,
153+
end: match.index + match[0].length,
154+
replacement: `<span class="text-blue-500">${escaped}</span>`,
155+
})
156+
}
157+
}
158+
}
159+
160+
// Find schema parameters as whole words
161+
if (schemaParameters.length > 0) {
162+
schemaParameters.forEach((param) => {
163+
// Escape special regex characters in parameter name
164+
const escapedName = param.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
165+
const paramRegex = new RegExp(`\\b(${escapedName})\\b`, 'g')
166+
while ((match = paramRegex.exec(highlighted)) !== null) {
167+
// Check if this position is already inside an HTML tag
168+
// by looking for unclosed < before this position
169+
let insideTag = false
170+
let pos = match.index - 1
171+
while (pos >= 0) {
172+
if (highlighted[pos] === '>') break
173+
if (highlighted[pos] === '<') {
174+
insideTag = true
175+
break
176+
}
177+
pos--
178+
}
179+
180+
if (!insideTag) {
181+
highlights.push({
182+
start: match.index,
183+
end: match.index + match[0].length,
184+
replacement: `<span class="text-green-600 font-medium">${match[0]}</span>`,
185+
})
186+
}
137187
}
138-
return `<span class="text-blue-500">&lt;${group}&gt;</span>`
139188
})
140189
}
141190

191+
// Sort highlights by start position (reverse order to maintain positions)
192+
highlights.sort((a, b) => b.start - a.start)
193+
194+
// Apply all highlights
195+
highlights.forEach(({ start, end, replacement }) => {
196+
highlighted = highlighted.slice(0, start) + replacement + highlighted.slice(end)
197+
})
198+
142199
return highlighted
143200
}
144201

@@ -204,12 +261,17 @@ export function CodeEditor({
204261
disabled={disabled}
205262
style={{
206263
fontFamily: 'inherit',
207-
minHeight: '46px',
264+
minHeight: minHeight,
208265
lineHeight: '21px',
266+
height: '100%',
209267
}}
210-
className={cn('focus:outline-none', isCollapsed && 'pointer-events-none select-none')}
268+
className={cn(
269+
'h-full focus:outline-none',
270+
isCollapsed && 'pointer-events-none select-none'
271+
)}
211272
textareaClassName={cn(
212273
'focus:outline-none focus:ring-0 bg-transparent',
274+
'!min-h-full !h-full resize-none !block',
213275
(isCollapsed || disabled) && 'pointer-events-none'
214276
)}
215277
/>

0 commit comments

Comments
 (0)