mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
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 <vikhyath@simstudio.ai>
This commit is contained in:
@@ -213,24 +213,81 @@ function createUserFriendlyErrorMessage(
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves environment variables and tags in code
|
||||
* @param code - Code with variables
|
||||
* @param params - Parameters that may contain variable values
|
||||
* @param envVars - Environment variables from the workflow
|
||||
* @returns Resolved code
|
||||
* Resolves workflow variables with <variable.name> syntax
|
||||
*/
|
||||
function resolveWorkflowVariables(
|
||||
code: string,
|
||||
workflowVariables: Record<string, any>,
|
||||
contextVariables: Record<string, any>
|
||||
): string {
|
||||
let resolvedCode = code
|
||||
|
||||
function resolveCodeVariables(
|
||||
const variableMatches = resolvedCode.match(/<variable\.([^>]+)>/g) || []
|
||||
for (const match of variableMatches) {
|
||||
const variableName = match.slice('<variable.'.length, -1).trim()
|
||||
|
||||
// Find the variable by name (workflowVariables is indexed by ID, values are variable objects)
|
||||
const foundVariable = Object.entries(workflowVariables).find(
|
||||
([_, variable]) => (variable.name || '').replace(/\s+/g, '') === variableName
|
||||
)
|
||||
|
||||
if (foundVariable) {
|
||||
const variable = foundVariable[1]
|
||||
// Get the typed value - handle different variable types
|
||||
let variableValue = variable.value
|
||||
|
||||
if (variable.value !== undefined && variable.value !== null) {
|
||||
try {
|
||||
// Handle 'string' type the same as 'plain' for backward compatibility
|
||||
const type = variable.type === 'string' ? 'plain' : variable.type
|
||||
|
||||
// For plain text, use exactly what's entered without modifications
|
||||
if (type === 'plain' && typeof variableValue === 'string') {
|
||||
// Use as-is for plain text
|
||||
} else if (type === 'number') {
|
||||
variableValue = Number(variableValue)
|
||||
} else if (type === 'boolean') {
|
||||
variableValue = variableValue === 'true' || variableValue === true
|
||||
} else if (type === 'json') {
|
||||
try {
|
||||
variableValue =
|
||||
typeof variableValue === 'string' ? JSON.parse(variableValue) : variableValue
|
||||
} catch {
|
||||
// Keep original value if JSON parsing fails
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Fallback to original value on error
|
||||
variableValue = variable.value
|
||||
}
|
||||
}
|
||||
|
||||
// Create a safe variable reference
|
||||
const safeVarName = `__variable_${variableName.replace(/[^a-zA-Z0-9_]/g, '_')}`
|
||||
contextVariables[safeVarName] = variableValue
|
||||
|
||||
// Replace the variable reference with the safe variable name
|
||||
resolvedCode = resolvedCode.replace(new RegExp(escapeRegExp(match), 'g'), safeVarName)
|
||||
} else {
|
||||
// Variable not found - replace with empty string to avoid syntax errors
|
||||
resolvedCode = resolvedCode.replace(new RegExp(escapeRegExp(match), 'g'), '')
|
||||
}
|
||||
}
|
||||
|
||||
return resolvedCode
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves environment variables with {{var_name}} syntax
|
||||
*/
|
||||
function resolveEnvironmentVariables(
|
||||
code: string,
|
||||
params: Record<string, any>,
|
||||
envVars: Record<string, string> = {},
|
||||
blockData: Record<string, any> = {},
|
||||
blockNameMapping: Record<string, string> = {}
|
||||
): { resolvedCode: string; contextVariables: Record<string, any> } {
|
||||
envVars: Record<string, string>,
|
||||
contextVariables: Record<string, any>
|
||||
): string {
|
||||
let resolvedCode = code
|
||||
const contextVariables: Record<string, any> = {}
|
||||
|
||||
// Resolve environment variables with {{var_name}} syntax
|
||||
const envVarMatches = resolvedCode.match(/\{\{([^}]+)\}\}/g) || []
|
||||
for (const match of envVarMatches) {
|
||||
const varName = match.slice(2, -2).trim()
|
||||
@@ -245,7 +302,21 @@ function resolveCodeVariables(
|
||||
resolvedCode = resolvedCode.replace(new RegExp(escapeRegExp(match), 'g'), safeVarName)
|
||||
}
|
||||
|
||||
// Resolve tags with <tag_name> syntax (including nested paths like <block.response.data>)
|
||||
return resolvedCode
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves tags with <tag_name> syntax (including nested paths like <block.response.data>)
|
||||
*/
|
||||
function resolveTagVariables(
|
||||
code: string,
|
||||
params: Record<string, any>,
|
||||
blockData: Record<string, any>,
|
||||
blockNameMapping: Record<string, string>,
|
||||
contextVariables: Record<string, any>
|
||||
): string {
|
||||
let resolvedCode = code
|
||||
|
||||
const tagMatches = resolvedCode.match(/<([a-zA-Z_][a-zA-Z0-9_.]*[a-zA-Z0-9_])>/g) || []
|
||||
|
||||
for (const match of tagMatches) {
|
||||
@@ -300,6 +371,42 @@ function resolveCodeVariables(
|
||||
resolvedCode = resolvedCode.replace(new RegExp(escapeRegExp(match), 'g'), safeVarName)
|
||||
}
|
||||
|
||||
return resolvedCode
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves environment variables and tags in code
|
||||
* @param code - Code with variables
|
||||
* @param params - Parameters that may contain variable values
|
||||
* @param envVars - Environment variables from the workflow
|
||||
* @returns Resolved code
|
||||
*/
|
||||
function resolveCodeVariables(
|
||||
code: string,
|
||||
params: Record<string, any>,
|
||||
envVars: Record<string, string> = {},
|
||||
blockData: Record<string, any> = {},
|
||||
blockNameMapping: Record<string, string> = {},
|
||||
workflowVariables: Record<string, any> = {}
|
||||
): { resolvedCode: string; contextVariables: Record<string, any> } {
|
||||
let resolvedCode = code
|
||||
const contextVariables: Record<string, any> = {}
|
||||
|
||||
// Resolve workflow variables with <variable.name> syntax first
|
||||
resolvedCode = resolveWorkflowVariables(resolvedCode, workflowVariables, contextVariables)
|
||||
|
||||
// Resolve environment variables with {{var_name}} syntax
|
||||
resolvedCode = resolveEnvironmentVariables(resolvedCode, params, envVars, contextVariables)
|
||||
|
||||
// Resolve tags with <tag_name> syntax (including nested paths like <block.response.data>)
|
||||
resolvedCode = resolveTagVariables(
|
||||
resolvedCode,
|
||||
params,
|
||||
blockData,
|
||||
blockNameMapping,
|
||||
contextVariables
|
||||
)
|
||||
|
||||
return { resolvedCode, contextVariables }
|
||||
}
|
||||
|
||||
@@ -338,6 +445,7 @@ export async function POST(req: NextRequest) {
|
||||
envVars = {},
|
||||
blockData = {},
|
||||
blockNameMapping = {},
|
||||
workflowVariables = {},
|
||||
workflowId,
|
||||
isCustomTool = false,
|
||||
} = body
|
||||
@@ -360,7 +468,8 @@ export async function POST(req: NextRequest) {
|
||||
executionParams,
|
||||
envVars,
|
||||
blockData,
|
||||
blockNameMapping
|
||||
blockNameMapping,
|
||||
workflowVariables
|
||||
)
|
||||
resolvedCode = codeResolution.resolvedCode
|
||||
const contextVariables = codeResolution.contextVariables
|
||||
@@ -368,8 +477,8 @@ export async function POST(req: NextRequest) {
|
||||
const executionMethod = 'vm' // Default execution method
|
||||
|
||||
logger.info(`[${requestId}] Using VM for code execution`, {
|
||||
resolvedCode,
|
||||
hasEnvVars: Object.keys(envVars).length > 0,
|
||||
hasWorkflowVariables: Object.keys(workflowVariables).length > 0,
|
||||
})
|
||||
|
||||
// Create a secure context with console logging
|
||||
|
||||
@@ -39,6 +39,9 @@ export async function POST(request: NextRequest) {
|
||||
stream,
|
||||
messages,
|
||||
environmentVariables,
|
||||
workflowVariables,
|
||||
blockData,
|
||||
blockNameMapping,
|
||||
reasoningEffort,
|
||||
verbosity,
|
||||
} = body
|
||||
@@ -60,6 +63,7 @@ export async function POST(request: NextRequest) {
|
||||
messageCount: messages?.length || 0,
|
||||
hasEnvironmentVariables:
|
||||
!!environmentVariables && Object.keys(environmentVariables).length > 0,
|
||||
hasWorkflowVariables: !!workflowVariables && Object.keys(workflowVariables).length > 0,
|
||||
reasoningEffort,
|
||||
verbosity,
|
||||
})
|
||||
@@ -103,6 +107,9 @@ export async function POST(request: NextRequest) {
|
||||
stream,
|
||||
messages,
|
||||
environmentVariables,
|
||||
workflowVariables,
|
||||
blockData,
|
||||
blockNameMapping,
|
||||
reasoningEffort,
|
||||
verbosity,
|
||||
})
|
||||
|
||||
@@ -18,6 +18,7 @@ interface CodeEditorProps {
|
||||
highlightVariables?: boolean
|
||||
onKeyDown?: (e: React.KeyboardEvent) => void
|
||||
disabled?: boolean
|
||||
schemaParameters?: Array<{ name: string; type: string; description: string; required: boolean }>
|
||||
}
|
||||
|
||||
export function CodeEditor({
|
||||
@@ -30,6 +31,7 @@ export function CodeEditor({
|
||||
highlightVariables = true,
|
||||
onKeyDown,
|
||||
disabled = false,
|
||||
schemaParameters = [],
|
||||
}: CodeEditorProps) {
|
||||
const [code, setCode] = useState(value)
|
||||
const [visualLineHeights, setVisualLineHeights] = useState<number[]>([])
|
||||
@@ -120,25 +122,80 @@ export function CodeEditor({
|
||||
// First, get the default Prism highlighting
|
||||
let highlighted = highlight(code, languages[language], language)
|
||||
|
||||
// Then, highlight environment variables with {{var_name}} syntax in blue
|
||||
if (highlighted.includes('{{')) {
|
||||
highlighted = highlighted.replace(
|
||||
/\{\{([^}]+)\}\}/g,
|
||||
'<span class="text-blue-500">{{$1}}</span>'
|
||||
)
|
||||
// Collect all syntax highlights to apply in a single pass
|
||||
type SyntaxHighlight = {
|
||||
start: number
|
||||
end: number
|
||||
replacement: string
|
||||
}
|
||||
const highlights: SyntaxHighlight[] = []
|
||||
|
||||
// Also highlight tags with <tag_name> syntax in blue
|
||||
if (highlighted.includes('<') && !language.includes('html')) {
|
||||
highlighted = highlighted.replace(/<([^>\s/]+)>/g, (match, group) => {
|
||||
// Avoid replacing HTML tags in comments
|
||||
if (match.startsWith('<!--') || match.includes('</')) {
|
||||
return match
|
||||
}
|
||||
return `<span class="text-blue-500"><${group}></span>`
|
||||
// Find environment variables with {{var_name}} syntax
|
||||
let match
|
||||
const envVarRegex = /\{\{([^}]+)\}\}/g
|
||||
while ((match = envVarRegex.exec(highlighted)) !== null) {
|
||||
highlights.push({
|
||||
start: match.index,
|
||||
end: match.index + match[0].length,
|
||||
replacement: `<span class="text-blue-500">${match[0]}</span>`,
|
||||
})
|
||||
}
|
||||
|
||||
// Find tags with <tag_name> syntax (not in HTML context)
|
||||
if (!language.includes('html')) {
|
||||
const tagRegex = /<([^>\s/]+)>/g
|
||||
while ((match = tagRegex.exec(highlighted)) !== null) {
|
||||
// Skip HTML comments and closing tags
|
||||
if (!match[0].startsWith('<!--') && !match[0].includes('</')) {
|
||||
const escaped = `<${match[1]}>`
|
||||
highlights.push({
|
||||
start: match.index,
|
||||
end: match.index + match[0].length,
|
||||
replacement: `<span class="text-blue-500">${escaped}</span>`,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find schema parameters as whole words
|
||||
if (schemaParameters.length > 0) {
|
||||
schemaParameters.forEach((param) => {
|
||||
// Escape special regex characters in parameter name
|
||||
const escapedName = param.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
const paramRegex = new RegExp(`\\b(${escapedName})\\b`, 'g')
|
||||
while ((match = paramRegex.exec(highlighted)) !== null) {
|
||||
// Check if this position is already inside an HTML tag
|
||||
// by looking for unclosed < before this position
|
||||
let insideTag = false
|
||||
let pos = match.index - 1
|
||||
while (pos >= 0) {
|
||||
if (highlighted[pos] === '>') break
|
||||
if (highlighted[pos] === '<') {
|
||||
insideTag = true
|
||||
break
|
||||
}
|
||||
pos--
|
||||
}
|
||||
|
||||
if (!insideTag) {
|
||||
highlights.push({
|
||||
start: match.index,
|
||||
end: match.index + match[0].length,
|
||||
replacement: `<span class="text-green-600 font-medium">${match[0]}</span>`,
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Sort highlights by start position (reverse order to maintain positions)
|
||||
highlights.sort((a, b) => b.start - a.start)
|
||||
|
||||
// Apply all highlights
|
||||
highlights.forEach(({ start, end, replacement }) => {
|
||||
highlighted = highlighted.slice(0, start) + replacement + highlighted.slice(end)
|
||||
})
|
||||
|
||||
return highlighted
|
||||
}
|
||||
|
||||
@@ -204,12 +261,17 @@ export function CodeEditor({
|
||||
disabled={disabled}
|
||||
style={{
|
||||
fontFamily: 'inherit',
|
||||
minHeight: '46px',
|
||||
minHeight: minHeight,
|
||||
lineHeight: '21px',
|
||||
height: '100%',
|
||||
}}
|
||||
className={cn('focus:outline-none', isCollapsed && 'pointer-events-none select-none')}
|
||||
className={cn(
|
||||
'h-full focus:outline-none',
|
||||
isCollapsed && 'pointer-events-none select-none'
|
||||
)}
|
||||
textareaClassName={cn(
|
||||
'focus:outline-none focus:ring-0 bg-transparent',
|
||||
'!min-h-full !h-full resize-none !block',
|
||||
(isCollapsed || disabled) && 'pointer-events-none'
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
import { checkEnvVarTrigger, EnvVarDropdown } from '@/components/ui/env-var-dropdown'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { checkTagTrigger, TagDropdown } from '@/components/ui/tag-dropdown'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { WandPromptBar } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/wand-prompt-bar/wand-prompt-bar'
|
||||
@@ -36,6 +37,7 @@ interface CustomToolModalProps {
|
||||
onOpenChange: (open: boolean) => void
|
||||
onSave: (tool: CustomTool) => void
|
||||
onDelete?: (toolId: string) => void
|
||||
blockId: string
|
||||
initialValues?: {
|
||||
id?: string
|
||||
schema: any
|
||||
@@ -61,6 +63,7 @@ export function CustomToolModal({
|
||||
onOpenChange,
|
||||
onSave,
|
||||
onDelete,
|
||||
blockId,
|
||||
initialValues,
|
||||
}: CustomToolModalProps) {
|
||||
const [activeSection, setActiveSection] = useState<ToolSection>('schema')
|
||||
@@ -237,12 +240,16 @@ try {
|
||||
// Environment variables and tags dropdown state
|
||||
const [showEnvVars, setShowEnvVars] = useState(false)
|
||||
const [showTags, setShowTags] = useState(false)
|
||||
const [showSchemaParams, setShowSchemaParams] = useState(false)
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const [cursorPosition, setCursorPosition] = useState(0)
|
||||
const codeEditorRef = useRef<HTMLDivElement>(null)
|
||||
const schemaParamsDropdownRef = useRef<HTMLDivElement>(null)
|
||||
const [activeSourceBlockId, setActiveSourceBlockId] = useState<string | null>(null)
|
||||
// Add state for dropdown positioning
|
||||
const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0 })
|
||||
// Schema params keyboard navigation
|
||||
const [schemaParamSelectedIndex, setSchemaParamSelectedIndex] = useState(0)
|
||||
|
||||
const addTool = useCustomToolsStore((state) => state.addTool)
|
||||
const updateTool = useCustomToolsStore((state) => state.updateTool)
|
||||
@@ -270,6 +277,21 @@ try {
|
||||
}
|
||||
}, [open, initialValues])
|
||||
|
||||
// Close schema params dropdown on outside click
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
schemaParamsDropdownRef.current &&
|
||||
!schemaParamsDropdownRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setShowSchemaParams(false)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside)
|
||||
}, [])
|
||||
|
||||
const resetForm = () => {
|
||||
setJsonSchema('')
|
||||
setFunctionCode('')
|
||||
@@ -309,6 +331,15 @@ try {
|
||||
return false
|
||||
}
|
||||
|
||||
// Validate that parameters object exists with correct structure
|
||||
if (!parsed.function.parameters) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!parsed.function.parameters.type || parsed.function.parameters.properties === undefined) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (_error) {
|
||||
return false
|
||||
@@ -320,6 +351,25 @@ try {
|
||||
return true // Allow empty code
|
||||
}
|
||||
|
||||
// Extract parameters from JSON schema for autocomplete
|
||||
const schemaParameters = useMemo(() => {
|
||||
try {
|
||||
if (!jsonSchema) return []
|
||||
const parsed = JSON.parse(jsonSchema)
|
||||
const properties = parsed?.function?.parameters?.properties
|
||||
if (!properties) return []
|
||||
|
||||
return Object.keys(properties).map((key) => ({
|
||||
name: key,
|
||||
type: properties[key].type || 'any',
|
||||
description: properties[key].description || '',
|
||||
required: parsed?.function?.parameters?.required?.includes(key) || false,
|
||||
}))
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}, [jsonSchema])
|
||||
|
||||
// Memoize validation results to prevent unnecessary recalculations
|
||||
const isSchemaValid = useMemo(() => validateJsonSchema(jsonSchema), [jsonSchema])
|
||||
const isCodeValid = useMemo(() => validateFunctionCode(functionCode), [functionCode])
|
||||
@@ -350,6 +400,34 @@ try {
|
||||
return
|
||||
}
|
||||
|
||||
// Validate parameters structure - must be present
|
||||
if (!parsed.function.parameters) {
|
||||
setSchemaError('Missing function.parameters object')
|
||||
setActiveSection('schema')
|
||||
return
|
||||
}
|
||||
|
||||
if (!parsed.function.parameters.type) {
|
||||
setSchemaError('Missing parameters.type field')
|
||||
setActiveSection('schema')
|
||||
return
|
||||
}
|
||||
|
||||
if (parsed.function.parameters.properties === undefined) {
|
||||
setSchemaError('Missing parameters.properties field')
|
||||
setActiveSection('schema')
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
typeof parsed.function.parameters.properties !== 'object' ||
|
||||
parsed.function.parameters.properties === null
|
||||
) {
|
||||
setSchemaError('parameters.properties must be an object')
|
||||
setActiveSection('schema')
|
||||
return
|
||||
}
|
||||
|
||||
// Check for duplicate tool name
|
||||
const toolName = parsed.function.name
|
||||
const customToolsStore = useCustomToolsStore.getState()
|
||||
@@ -439,7 +517,52 @@ try {
|
||||
// Prevent updates during AI generation/streaming
|
||||
if (schemaGeneration.isLoading || schemaGeneration.isStreaming) return
|
||||
setJsonSchema(value)
|
||||
if (schemaError) {
|
||||
|
||||
// Real-time validation - show error immediately when schema is invalid
|
||||
if (value.trim()) {
|
||||
try {
|
||||
const parsed = JSON.parse(value)
|
||||
|
||||
if (!parsed.type || parsed.type !== 'function') {
|
||||
setSchemaError('Missing "type": "function"')
|
||||
return
|
||||
}
|
||||
|
||||
if (!parsed.function || !parsed.function.name) {
|
||||
setSchemaError('Missing function.name field')
|
||||
return
|
||||
}
|
||||
|
||||
if (!parsed.function.parameters) {
|
||||
setSchemaError('Missing function.parameters object')
|
||||
return
|
||||
}
|
||||
|
||||
if (!parsed.function.parameters.type) {
|
||||
setSchemaError('Missing parameters.type field')
|
||||
return
|
||||
}
|
||||
|
||||
if (parsed.function.parameters.properties === undefined) {
|
||||
setSchemaError('Missing parameters.properties field')
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
typeof parsed.function.parameters.properties !== 'object' ||
|
||||
parsed.function.parameters.properties === null
|
||||
) {
|
||||
setSchemaError('parameters.properties must be an object')
|
||||
return
|
||||
}
|
||||
|
||||
// Schema is valid, clear any existing error
|
||||
setSchemaError(null)
|
||||
} catch {
|
||||
setSchemaError('Invalid JSON format')
|
||||
}
|
||||
} else {
|
||||
// Clear error when schema is empty (will be caught during save)
|
||||
setSchemaError(null)
|
||||
}
|
||||
}
|
||||
@@ -499,9 +622,40 @@ try {
|
||||
if (!tagTrigger.show) {
|
||||
setActiveSourceBlockId(null)
|
||||
}
|
||||
|
||||
// Show/hide schema parameters dropdown based on typing context
|
||||
if (!codeGeneration.isStreaming && schemaParameters.length > 0) {
|
||||
const schemaParamTrigger = checkSchemaParamTrigger(value, pos, schemaParameters)
|
||||
if (schemaParamTrigger.show && !showSchemaParams) {
|
||||
setShowSchemaParams(true)
|
||||
setSchemaParamSelectedIndex(0)
|
||||
} else if (!schemaParamTrigger.show && showSchemaParams) {
|
||||
setShowSchemaParams(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Function to check if we should show schema parameters dropdown
|
||||
const checkSchemaParamTrigger = (text: string, cursorPos: number, parameters: any[]) => {
|
||||
if (parameters.length === 0) return { show: false, searchTerm: '' }
|
||||
|
||||
// Look for partial parameter names after common patterns like 'const ', '= ', etc.
|
||||
const beforeCursor = text.substring(0, cursorPos)
|
||||
const words = beforeCursor.split(/[\s=();,{}[\]]+/)
|
||||
const currentWord = words[words.length - 1] || ''
|
||||
|
||||
// Show dropdown if typing and current word could be a parameter
|
||||
if (currentWord.length > 0 && /^[a-zA-Z_][\w]*$/.test(currentWord)) {
|
||||
const matchingParams = parameters.filter((param) =>
|
||||
param.name.toLowerCase().startsWith(currentWord.toLowerCase())
|
||||
)
|
||||
return { show: matchingParams.length > 0, searchTerm: currentWord, matches: matchingParams }
|
||||
}
|
||||
|
||||
return { show: false, searchTerm: '' }
|
||||
}
|
||||
|
||||
// Handle environment variable selection
|
||||
const handleEnvVarSelect = (newValue: string) => {
|
||||
setFunctionCode(newValue)
|
||||
@@ -515,6 +669,32 @@ try {
|
||||
setActiveSourceBlockId(null)
|
||||
}
|
||||
|
||||
// Handle schema parameter selection
|
||||
const handleSchemaParamSelect = (paramName: string) => {
|
||||
const textarea = codeEditorRef.current?.querySelector('textarea')
|
||||
if (textarea) {
|
||||
const pos = textarea.selectionStart
|
||||
const beforeCursor = functionCode.substring(0, pos)
|
||||
const afterCursor = functionCode.substring(pos)
|
||||
|
||||
// Find the start of the current word
|
||||
const words = beforeCursor.split(/[\s=();,{}[\]]+/)
|
||||
const currentWord = words[words.length - 1] || ''
|
||||
const wordStart = beforeCursor.lastIndexOf(currentWord)
|
||||
|
||||
// Replace the current partial word with the selected parameter
|
||||
const newValue = beforeCursor.substring(0, wordStart) + paramName + afterCursor
|
||||
setFunctionCode(newValue)
|
||||
setShowSchemaParams(false)
|
||||
|
||||
// Set cursor position after the inserted parameter
|
||||
setTimeout(() => {
|
||||
textarea.focus()
|
||||
textarea.setSelectionRange(wordStart + paramName.length, wordStart + paramName.length)
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle key press events
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
// Allow AI prompt interaction (e.g., Escape to close prompt bar)
|
||||
@@ -535,10 +715,14 @@ try {
|
||||
e.stopPropagation()
|
||||
return
|
||||
}
|
||||
// Close dropdowns only if AI prompt isn't active
|
||||
if (!showEnvVars && !showTags) {
|
||||
// Close dropdowns first, only close modal if no dropdowns are open
|
||||
if (showEnvVars || showTags || showSchemaParams) {
|
||||
setShowEnvVars(false)
|
||||
setShowTags(false)
|
||||
setShowSchemaParams(false)
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -552,7 +736,37 @@ try {
|
||||
return
|
||||
}
|
||||
|
||||
// Let dropdowns handle their own keyboard events if visible
|
||||
// Handle schema parameters dropdown keyboard navigation
|
||||
if (showSchemaParams && schemaParameters.length > 0) {
|
||||
switch (e.key) {
|
||||
case 'ArrowDown':
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setSchemaParamSelectedIndex((prev) => Math.min(prev + 1, schemaParameters.length - 1))
|
||||
break
|
||||
case 'ArrowUp':
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setSchemaParamSelectedIndex((prev) => Math.max(prev - 1, 0))
|
||||
break
|
||||
case 'Enter':
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
if (schemaParamSelectedIndex >= 0 && schemaParamSelectedIndex < schemaParameters.length) {
|
||||
const selectedParam = schemaParameters[schemaParamSelectedIndex]
|
||||
handleSchemaParamSelect(selectedParam.name)
|
||||
}
|
||||
break
|
||||
case 'Escape':
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setShowSchemaParams(false)
|
||||
break
|
||||
}
|
||||
return // Don't handle other dropdown events when schema params is active
|
||||
}
|
||||
|
||||
// Let other dropdowns handle their own keyboard events if visible
|
||||
if (showEnvVars || showTags) {
|
||||
if (['ArrowDown', 'ArrowUp', 'Enter'].includes(e.key)) {
|
||||
e.preventDefault()
|
||||
@@ -619,6 +833,16 @@ try {
|
||||
<DialogContent
|
||||
className='flex h-[80vh] flex-col gap-0 p-0 sm:max-w-[700px]'
|
||||
hideCloseButton
|
||||
onKeyDown={(e) => {
|
||||
// Intercept Escape key when dropdowns are open
|
||||
if (e.key === 'Escape' && (showEnvVars || showTags || showSchemaParams)) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setShowEnvVars(false)
|
||||
setShowTags(false)
|
||||
setShowSchemaParams(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogHeader className='border-b px-6 py-4'>
|
||||
<div className='flex items-center justify-between'>
|
||||
@@ -729,7 +953,7 @@ try {
|
||||
</div>
|
||||
{schemaError &&
|
||||
!schemaGeneration.isStreaming && ( // Hide schema error while streaming
|
||||
<span className='ml-4 flex-shrink-0 text-red-600 text-sm'>{schemaError}</span>
|
||||
<div className='ml-4 break-words text-red-600 text-sm'>{schemaError}</div>
|
||||
)}
|
||||
</div>
|
||||
<CodeEditor
|
||||
@@ -799,9 +1023,25 @@ try {
|
||||
</div>
|
||||
{codeError &&
|
||||
!codeGeneration.isStreaming && ( // Hide code error while streaming
|
||||
<span className='ml-4 flex-shrink-0 text-red-600 text-sm'>{codeError}</span>
|
||||
<div className='ml-4 break-words text-red-600 text-sm'>{codeError}</div>
|
||||
)}
|
||||
</div>
|
||||
{schemaParameters.length > 0 && (
|
||||
<div className='mb-2 rounded-md bg-muted/50 p-2'>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
<span className='font-medium'>Available parameters:</span>{' '}
|
||||
{schemaParameters.map((param, index) => (
|
||||
<span key={param.name}>
|
||||
<code className='rounded bg-background px-1 py-0.5 text-foreground'>
|
||||
{param.name}
|
||||
</code>
|
||||
{index < schemaParameters.length - 1 && ', '}
|
||||
</span>
|
||||
))}
|
||||
{'. '}Start typing a parameter name for autocomplete.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div ref={codeEditorRef} className='relative'>
|
||||
<CodeEditor
|
||||
value={functionCode}
|
||||
@@ -819,6 +1059,7 @@ try {
|
||||
highlightVariables={true}
|
||||
disabled={codeGeneration.isLoading || codeGeneration.isStreaming} // Use disabled prop instead of readOnly
|
||||
onKeyDown={handleKeyDown} // Pass keydown handler
|
||||
schemaParameters={schemaParameters} // Pass schema parameters for highlighting
|
||||
/>
|
||||
|
||||
{/* Environment variables dropdown */}
|
||||
@@ -847,7 +1088,7 @@ try {
|
||||
<TagDropdown
|
||||
visible={showTags}
|
||||
onSelect={handleTagSelect}
|
||||
blockId=''
|
||||
blockId={blockId}
|
||||
activeSourceBlockId={activeSourceBlockId}
|
||||
inputValue={functionCode}
|
||||
cursorPosition={cursorPosition}
|
||||
@@ -863,6 +1104,49 @@ try {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Schema parameters dropdown */}
|
||||
{showSchemaParams && schemaParameters.length > 0 && (
|
||||
<div
|
||||
ref={schemaParamsDropdownRef}
|
||||
className='absolute z-[9999] mt-1 w-64 overflow-visible rounded-md border bg-popover shadow-md'
|
||||
style={{
|
||||
top: `${dropdownPosition.top}px`,
|
||||
left: `${dropdownPosition.left}px`,
|
||||
}}
|
||||
>
|
||||
<div className='py-1'>
|
||||
<div className='px-2 pt-2.5 pb-0.5 font-medium text-muted-foreground text-xs'>
|
||||
Available Parameters
|
||||
</div>
|
||||
<div>
|
||||
{schemaParameters.map((param, index) => (
|
||||
<button
|
||||
key={param.name}
|
||||
onClick={() => handleSchemaParamSelect(param.name)}
|
||||
onMouseEnter={() => setSchemaParamSelectedIndex(index)}
|
||||
className={cn(
|
||||
'flex w-full items-center gap-2 px-3 py-1.5 text-left text-sm',
|
||||
'hover:bg-accent hover:text-accent-foreground',
|
||||
'focus:bg-accent focus:text-accent-foreground focus:outline-none',
|
||||
index === schemaParamSelectedIndex &&
|
||||
'bg-accent text-accent-foreground'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className='flex h-5 w-5 items-center justify-center rounded'
|
||||
style={{ backgroundColor: '#2F8BFF' }}
|
||||
>
|
||||
<span className='h-3 w-3 font-bold text-white text-xs'>P</span>
|
||||
</div>
|
||||
<span className='flex-1 truncate'>{param.name}</span>
|
||||
<span className='text-muted-foreground text-xs'>{param.type}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className='h-6' />
|
||||
</div>
|
||||
@@ -899,11 +1183,27 @@ try {
|
||||
Cancel
|
||||
</Button>
|
||||
{activeSection === 'schema' ? (
|
||||
<Button onClick={() => setActiveSection('code')} disabled={!isSchemaValid}>
|
||||
<Button
|
||||
onClick={() => setActiveSection('code')}
|
||||
disabled={!isSchemaValid || !!schemaError}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
) : (
|
||||
<Button onClick={handleSave}>{isEditing ? 'Update Tool' : 'Save Tool'}</Button>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<Button onClick={handleSave} disabled={!isSchemaValid}>
|
||||
{isEditing ? 'Update Tool' : 'Save Tool'}
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
{!isSchemaValid && (
|
||||
<TooltipContent side='top'>
|
||||
<p>Invalid JSON schema</p>
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1320,7 +1320,7 @@ export function ToolInput({
|
||||
|
||||
// For custom tools, extract parameters from schema
|
||||
const customToolParams =
|
||||
isCustomTool && tool.schema
|
||||
isCustomTool && tool.schema && tool.schema.function?.parameters?.properties
|
||||
? Object.entries(tool.schema.function.parameters.properties || {}).map(
|
||||
([paramId, param]: [string, any]) => ({
|
||||
id: paramId,
|
||||
@@ -1824,6 +1824,7 @@ export function ToolInput({
|
||||
}}
|
||||
onSave={editingToolIndex !== null ? handleSaveCustomTool : handleAddCustomTool}
|
||||
onDelete={handleDeleteTool}
|
||||
blockId={blockId}
|
||||
initialValues={
|
||||
editingToolIndex !== null && selectedTools[editingToolIndex]?.type === 'custom-tool'
|
||||
? {
|
||||
|
||||
@@ -23,6 +23,33 @@ const DEFAULT_FUNCTION_TIMEOUT = 5000
|
||||
const REQUEST_TIMEOUT = 120000
|
||||
const CUSTOM_TOOL_PREFIX = 'custom_'
|
||||
|
||||
/**
|
||||
* Helper function to collect runtime block outputs and name mappings
|
||||
* for tag resolution in custom tools and prompts
|
||||
*/
|
||||
function collectBlockData(context: ExecutionContext): {
|
||||
blockData: Record<string, any>
|
||||
blockNameMapping: Record<string, string>
|
||||
} {
|
||||
const blockData: Record<string, any> = {}
|
||||
const blockNameMapping: Record<string, string> = {}
|
||||
|
||||
for (const [id, state] of context.blockStates.entries()) {
|
||||
if (state.output !== undefined) {
|
||||
blockData[id] = state.output
|
||||
const workflowBlock = context.workflow?.blocks?.find((b) => b.id === id)
|
||||
if (workflowBlock?.metadata?.name) {
|
||||
// Map both the display name and normalized form
|
||||
blockNameMapping[workflowBlock.metadata.name] = id
|
||||
const normalized = workflowBlock.metadata.name.replace(/\s+/g, '').toLowerCase()
|
||||
blockNameMapping[normalized] = id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { blockData, blockNameMapping }
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for Agent blocks that process LLM requests with optional tools.
|
||||
*/
|
||||
@@ -172,6 +199,9 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
// Merge user-provided parameters with LLM-generated parameters
|
||||
const mergedParams = mergeToolParameters(userProvidedParams, callParams)
|
||||
|
||||
// Collect block outputs for tag resolution
|
||||
const { blockData, blockNameMapping } = collectBlockData(context)
|
||||
|
||||
const result = await executeTool(
|
||||
'function_execute',
|
||||
{
|
||||
@@ -179,6 +209,9 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
...mergedParams,
|
||||
timeout: tool.timeout ?? DEFAULT_FUNCTION_TIMEOUT,
|
||||
envVars: context.environmentVariables || {},
|
||||
workflowVariables: context.workflowVariables || {},
|
||||
blockData,
|
||||
blockNameMapping,
|
||||
isCustomTool: true,
|
||||
_context: { workflowId: context.workflowId },
|
||||
},
|
||||
@@ -352,6 +385,9 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
|
||||
const validMessages = this.validateMessages(messages)
|
||||
|
||||
// Collect block outputs for runtime resolution
|
||||
const { blockData, blockNameMapping } = collectBlockData(context)
|
||||
|
||||
return {
|
||||
provider: providerId,
|
||||
model,
|
||||
@@ -368,6 +404,9 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
stream: streaming,
|
||||
messages,
|
||||
environmentVariables: context.environmentVariables || {},
|
||||
workflowVariables: context.workflowVariables || {},
|
||||
blockData,
|
||||
blockNameMapping,
|
||||
reasoningEffort: inputs.reasoningEffort,
|
||||
verbosity: inputs.verbosity,
|
||||
}
|
||||
@@ -457,6 +496,9 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
|
||||
const finalApiKey = this.getApiKey(providerId, model, providerRequest.apiKey)
|
||||
|
||||
// Collect block outputs for runtime resolution
|
||||
const { blockData, blockNameMapping } = collectBlockData(context)
|
||||
|
||||
const response = await executeProviderRequest(providerId, {
|
||||
model,
|
||||
systemPrompt: 'systemPrompt' in providerRequest ? providerRequest.systemPrompt : undefined,
|
||||
@@ -472,6 +514,9 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
stream: providerRequest.stream,
|
||||
messages: 'messages' in providerRequest ? providerRequest.messages : undefined,
|
||||
environmentVariables: context.environmentVariables || {},
|
||||
workflowVariables: context.workflowVariables || {},
|
||||
blockData,
|
||||
blockNameMapping,
|
||||
})
|
||||
|
||||
this.logExecutionSuccess(providerId, model, context, block, providerStartTime, response)
|
||||
|
||||
@@ -77,6 +77,7 @@ describe('FunctionBlockHandler', () => {
|
||||
code: inputs.code,
|
||||
timeout: inputs.timeout,
|
||||
envVars: {},
|
||||
workflowVariables: {},
|
||||
blockData: {},
|
||||
blockNameMapping: {},
|
||||
_context: { workflowId: mockContext.workflowId },
|
||||
@@ -108,6 +109,7 @@ describe('FunctionBlockHandler', () => {
|
||||
code: expectedCode,
|
||||
timeout: inputs.timeout,
|
||||
envVars: {},
|
||||
workflowVariables: {},
|
||||
blockData: {},
|
||||
blockNameMapping: {},
|
||||
_context: { workflowId: mockContext.workflowId },
|
||||
@@ -132,6 +134,7 @@ describe('FunctionBlockHandler', () => {
|
||||
code: inputs.code,
|
||||
timeout: 5000, // Default timeout
|
||||
envVars: {},
|
||||
workflowVariables: {},
|
||||
blockData: {},
|
||||
blockNameMapping: {},
|
||||
_context: { workflowId: mockContext.workflowId },
|
||||
|
||||
@@ -6,6 +6,33 @@ import { executeTool } from '@/tools'
|
||||
|
||||
const logger = createLogger('FunctionBlockHandler')
|
||||
|
||||
/**
|
||||
* Helper function to collect runtime block outputs and name mappings
|
||||
* for tag resolution in function execution
|
||||
*/
|
||||
function collectBlockData(context: ExecutionContext): {
|
||||
blockData: Record<string, any>
|
||||
blockNameMapping: Record<string, string>
|
||||
} {
|
||||
const blockData: Record<string, any> = {}
|
||||
const blockNameMapping: Record<string, string> = {}
|
||||
|
||||
for (const [id, state] of context.blockStates.entries()) {
|
||||
if (state.output !== undefined) {
|
||||
blockData[id] = state.output
|
||||
const workflowBlock = context.workflow?.blocks?.find((b) => b.id === id)
|
||||
if (workflowBlock?.metadata?.name) {
|
||||
// Map both the display name and normalized form
|
||||
blockNameMapping[workflowBlock.metadata.name] = id
|
||||
const normalized = workflowBlock.metadata.name.replace(/\s+/g, '').toLowerCase()
|
||||
blockNameMapping[normalized] = id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { blockData, blockNameMapping }
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for Function blocks that execute custom code.
|
||||
*/
|
||||
@@ -24,20 +51,7 @@ export class FunctionBlockHandler implements BlockHandler {
|
||||
: inputs.code
|
||||
|
||||
// Extract block data for variable resolution
|
||||
const blockData: Record<string, any> = {}
|
||||
const blockNameMapping: Record<string, string> = {}
|
||||
|
||||
for (const [blockId, blockState] of context.blockStates.entries()) {
|
||||
if (blockState.output) {
|
||||
blockData[blockId] = blockState.output
|
||||
|
||||
// Try to find the block name from the workflow
|
||||
const workflowBlock = context.workflow?.blocks?.find((b) => b.id === blockId)
|
||||
if (workflowBlock?.metadata?.name) {
|
||||
blockNameMapping[workflowBlock.metadata.name] = blockId
|
||||
}
|
||||
}
|
||||
}
|
||||
const { blockData, blockNameMapping } = collectBlockData(context)
|
||||
|
||||
// Directly use the function_execute tool which calls the API route
|
||||
const result = await executeTool(
|
||||
@@ -46,6 +60,7 @@ export class FunctionBlockHandler implements BlockHandler {
|
||||
code: codeContent,
|
||||
timeout: inputs.timeout || 5000,
|
||||
envVars: context.environmentVariables || {},
|
||||
workflowVariables: context.workflowVariables || {},
|
||||
blockData: blockData, // Pass block data for variable resolution
|
||||
blockNameMapping: blockNameMapping, // Pass block name to ID mapping
|
||||
_context: { workflowId: context.workflowId },
|
||||
|
||||
@@ -222,9 +222,10 @@ export class LoopBlockHandler implements BlockHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// If we have a resolver, use it to resolve any block references in the expression
|
||||
// If we have a resolver, use it to resolve any variable references first, then block references
|
||||
if (this.resolver) {
|
||||
const resolved = this.resolver.resolveBlockReferences(forEachItems, context, block)
|
||||
const resolvedVars = this.resolver.resolveVariableReferences(forEachItems, block)
|
||||
const resolved = this.resolver.resolveBlockReferences(resolvedVars, context, block)
|
||||
|
||||
// Try to parse the resolved value
|
||||
try {
|
||||
|
||||
@@ -413,9 +413,10 @@ export class ParallelBlockHandler implements BlockHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// If we have a resolver, use it to resolve any block references in the expression
|
||||
// If we have a resolver, use it to resolve any variable references first, then block references
|
||||
if (this.resolver) {
|
||||
const resolved = this.resolver.resolveBlockReferences(distribution, context, block)
|
||||
const resolvedVars = this.resolver.resolveVariableReferences(distribution, block)
|
||||
const resolved = this.resolver.resolveBlockReferences(resolvedVars, context, block)
|
||||
|
||||
// Try to parse the resolved value
|
||||
try {
|
||||
|
||||
@@ -119,23 +119,12 @@ export class Executor {
|
||||
if (options.contextExtensions) {
|
||||
this.contextExtensions = options.contextExtensions
|
||||
this.isChildExecution = options.contextExtensions.isChildExecution || false
|
||||
|
||||
if (this.contextExtensions.stream) {
|
||||
logger.info('Executor initialized with streaming enabled', {
|
||||
hasSelectedOutputIds: Array.isArray(this.contextExtensions.selectedOutputIds),
|
||||
selectedOutputCount: Array.isArray(this.contextExtensions.selectedOutputIds)
|
||||
? this.contextExtensions.selectedOutputIds.length
|
||||
: 0,
|
||||
selectedOutputIds: this.contextExtensions.selectedOutputIds || [],
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.actualWorkflow = workflowParam
|
||||
|
||||
if (workflowInput) {
|
||||
this.workflowInput = workflowInput
|
||||
logger.info('[Executor] Using workflow input:', JSON.stringify(this.workflowInput, null, 2))
|
||||
} else {
|
||||
this.workflowInput = {}
|
||||
}
|
||||
@@ -400,8 +389,7 @@ export class Executor {
|
||||
try {
|
||||
reader.releaseLock()
|
||||
} catch (releaseError: any) {
|
||||
// Reader might already be released
|
||||
logger.debug('Reader already released:', releaseError)
|
||||
// Reader might already be released - this is expected and safe to ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -641,15 +629,12 @@ export class Executor {
|
||||
* @throws Error if workflow validation fails
|
||||
*/
|
||||
private validateWorkflow(startBlockId?: string): void {
|
||||
let validationBlock: SerializedBlock | undefined
|
||||
|
||||
if (startBlockId) {
|
||||
// If starting from a specific block (webhook trigger or schedule trigger), validate that block exists
|
||||
const startBlock = this.actualWorkflow.blocks.find((block) => block.id === startBlockId)
|
||||
if (!startBlock || !startBlock.enabled) {
|
||||
throw new Error(`Start block ${startBlockId} not found or disabled`)
|
||||
}
|
||||
validationBlock = startBlock
|
||||
// Trigger blocks (webhook and schedule) can have incoming connections, so no need to check that
|
||||
} else {
|
||||
// Default validation for starter block
|
||||
@@ -659,7 +644,6 @@ export class Executor {
|
||||
if (!starterBlock || !starterBlock.enabled) {
|
||||
throw new Error('Workflow must have an enabled starter block')
|
||||
}
|
||||
validationBlock = starterBlock
|
||||
|
||||
const incomingToStarter = this.actualWorkflow.connections.filter(
|
||||
(conn) => conn.target === starterBlock.id
|
||||
@@ -741,6 +725,7 @@ export class Executor {
|
||||
duration: 0, // Initialize with zero, will be updated throughout execution
|
||||
},
|
||||
environmentVariables: this.environmentVariables,
|
||||
workflowVariables: this.workflowVariables,
|
||||
decisions: {
|
||||
router: new Map(),
|
||||
condition: new Map(),
|
||||
@@ -808,11 +793,6 @@ export class Executor {
|
||||
? this.workflowInput.input[field.name] // Try to get from input.field
|
||||
: this.workflowInput?.[field.name] // Fallback to direct field access
|
||||
|
||||
logger.info(
|
||||
`[Executor] Processing input field ${field.name} (${field.type}):`,
|
||||
inputValue !== undefined ? JSON.stringify(inputValue) : 'undefined'
|
||||
)
|
||||
|
||||
if (inputValue === undefined || inputValue === null) {
|
||||
if (Object.hasOwn(field, 'value')) {
|
||||
inputValue = (field as any).value
|
||||
@@ -873,8 +853,6 @@ export class Executor {
|
||||
blockOutput.files = this.workflowInput.files
|
||||
}
|
||||
|
||||
logger.info(`[Executor] Starting block output:`, JSON.stringify(blockOutput, null, 2))
|
||||
|
||||
context.blockStates.set(initBlock.id, {
|
||||
output: blockOutput,
|
||||
executed: true,
|
||||
@@ -967,11 +945,6 @@ export class Executor {
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(
|
||||
'[Executor] Fallback starting block output:',
|
||||
JSON.stringify(blockOutput, null, 2)
|
||||
)
|
||||
|
||||
context.blockStates.set(initBlock.id, {
|
||||
output: blockOutput,
|
||||
executed: true,
|
||||
@@ -1342,7 +1315,7 @@ export class Executor {
|
||||
const results: (NormalizedBlockOutput | StreamingExecution)[] = []
|
||||
const errors: Error[] = []
|
||||
|
||||
settledResults.forEach((result, index) => {
|
||||
settledResults.forEach((result) => {
|
||||
if (result.status === 'fulfilled') {
|
||||
results.push(result.value)
|
||||
} else {
|
||||
@@ -1443,7 +1416,6 @@ export class Executor {
|
||||
}
|
||||
|
||||
const addConsole = useConsoleStore.getState().addConsole
|
||||
const { setActiveBlocks } = useExecutionStore.getState()
|
||||
|
||||
try {
|
||||
if (block.enabled === false) {
|
||||
|
||||
@@ -107,6 +107,7 @@ export interface ExecutionContext {
|
||||
blockLogs: BlockLog[] // Chronological log of block executions
|
||||
metadata: ExecutionMetadata // Timing metadata for the execution
|
||||
environmentVariables: Record<string, string> // Environment variables available during execution
|
||||
workflowVariables?: Record<string, any> // Workflow variables available during execution
|
||||
|
||||
// Routing decisions for path determination
|
||||
decisions: {
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { StreamingExecution } from '@/executor/types'
|
||||
import { executeTool } from '@/tools'
|
||||
import { getProviderDefaultModel, getProviderModels } from '../models'
|
||||
import type { ProviderConfig, ProviderRequest, ProviderResponse, TimeSegment } from '../types'
|
||||
import { prepareToolsWithUsageControl, trackForcedToolUsage } from '../utils'
|
||||
import { prepareToolExecution, prepareToolsWithUsageControl, trackForcedToolUsage } from '../utils'
|
||||
|
||||
const logger = createLogger('AnthropicProvider')
|
||||
|
||||
@@ -456,28 +456,11 @@ ${fieldDescriptions}
|
||||
// Execute the tool
|
||||
const toolCallStartTime = Date.now()
|
||||
|
||||
// Only merge actual tool parameters for logging
|
||||
const toolParams = {
|
||||
...tool.params,
|
||||
...toolArgs,
|
||||
}
|
||||
|
||||
// Add system parameters for execution
|
||||
const executionParams = {
|
||||
...toolParams,
|
||||
...(request.workflowId
|
||||
? {
|
||||
_context: {
|
||||
workflowId: request.workflowId,
|
||||
...(request.chatId ? { chatId: request.chatId } : {}),
|
||||
...(request.userId ? { userId: request.userId } : {}),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(request.environmentVariables
|
||||
? { envVars: request.environmentVariables }
|
||||
: {}),
|
||||
}
|
||||
const { toolParams, executionParams } = prepareToolExecution(
|
||||
tool,
|
||||
toolArgs,
|
||||
request
|
||||
)
|
||||
|
||||
// Use general tool system for requests
|
||||
const result = await executeTool(toolName, executionParams, true)
|
||||
@@ -827,26 +810,7 @@ ${fieldDescriptions}
|
||||
// Execute the tool
|
||||
const toolCallStartTime = Date.now()
|
||||
|
||||
// Only merge actual tool parameters for logging
|
||||
const toolParams = {
|
||||
...tool.params,
|
||||
...toolArgs,
|
||||
}
|
||||
|
||||
// Add system parameters for execution
|
||||
const executionParams = {
|
||||
...toolParams,
|
||||
...(request.workflowId
|
||||
? {
|
||||
_context: {
|
||||
workflowId: request.workflowId,
|
||||
...(request.chatId ? { chatId: request.chatId } : {}),
|
||||
...(request.userId ? { userId: request.userId } : {}),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(request.environmentVariables ? { envVars: request.environmentVariables } : {}),
|
||||
}
|
||||
const { toolParams, executionParams } = prepareToolExecution(tool, toolArgs, request)
|
||||
|
||||
// Use general tool system for requests
|
||||
const result = await executeTool(toolName, executionParams, true)
|
||||
|
||||
@@ -9,7 +9,11 @@ import type {
|
||||
ProviderResponse,
|
||||
TimeSegment,
|
||||
} from '@/providers/types'
|
||||
import { prepareToolsWithUsageControl, trackForcedToolUsage } from '@/providers/utils'
|
||||
import {
|
||||
prepareToolExecution,
|
||||
prepareToolsWithUsageControl,
|
||||
trackForcedToolUsage,
|
||||
} from '@/providers/utils'
|
||||
import { executeTool } from '@/tools'
|
||||
|
||||
const logger = createLogger('AzureOpenAIProvider')
|
||||
@@ -383,25 +387,7 @@ export const azureOpenAIProvider: ProviderConfig = {
|
||||
// Execute the tool
|
||||
const toolCallStartTime = Date.now()
|
||||
|
||||
// Only merge actual tool parameters for logging
|
||||
const toolParams = {
|
||||
...tool.params,
|
||||
...toolArgs,
|
||||
}
|
||||
|
||||
// Add system parameters for execution
|
||||
const executionParams = {
|
||||
...toolParams,
|
||||
...(request.workflowId
|
||||
? {
|
||||
_context: {
|
||||
workflowId: request.workflowId,
|
||||
...(request.chatId ? { chatId: request.chatId } : {}),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(request.environmentVariables ? { envVars: request.environmentVariables } : {}),
|
||||
}
|
||||
const { toolParams, executionParams } = prepareToolExecution(tool, toolArgs, request)
|
||||
|
||||
const result = await executeTool(toolName, executionParams, true)
|
||||
const toolCallEndTime = Date.now()
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
ProviderResponse,
|
||||
TimeSegment,
|
||||
} from '@/providers/types'
|
||||
import { prepareToolExecution } from '@/providers/utils'
|
||||
import { executeTool } from '@/tools'
|
||||
|
||||
const logger = createLogger('CerebrasProvider')
|
||||
@@ -287,25 +288,7 @@ export const cerebrasProvider: ProviderConfig = {
|
||||
// Execute the tool
|
||||
const toolCallStartTime = Date.now()
|
||||
|
||||
// Only merge actual tool parameters for logging
|
||||
const toolParams = {
|
||||
...tool.params,
|
||||
...toolArgs,
|
||||
}
|
||||
|
||||
// Add system parameters for execution
|
||||
const executionParams = {
|
||||
...toolParams,
|
||||
...(request.workflowId
|
||||
? {
|
||||
_context: {
|
||||
workflowId: request.workflowId,
|
||||
...(request.chatId ? { chatId: request.chatId } : {}),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(request.environmentVariables ? { envVars: request.environmentVariables } : {}),
|
||||
}
|
||||
const { toolParams, executionParams } = prepareToolExecution(tool, toolArgs, request)
|
||||
|
||||
const result = await executeTool(toolName, executionParams, true)
|
||||
const toolCallEndTime = Date.now()
|
||||
|
||||
@@ -8,7 +8,11 @@ import type {
|
||||
ProviderResponse,
|
||||
TimeSegment,
|
||||
} from '@/providers/types'
|
||||
import { prepareToolsWithUsageControl, trackForcedToolUsage } from '@/providers/utils'
|
||||
import {
|
||||
prepareToolExecution,
|
||||
prepareToolsWithUsageControl,
|
||||
trackForcedToolUsage,
|
||||
} from '@/providers/utils'
|
||||
import { executeTool } from '@/tools'
|
||||
|
||||
const logger = createLogger('DeepseekProvider')
|
||||
@@ -289,25 +293,7 @@ export const deepseekProvider: ProviderConfig = {
|
||||
// Execute the tool
|
||||
const toolCallStartTime = Date.now()
|
||||
|
||||
// Only merge actual tool parameters for logging
|
||||
const toolParams = {
|
||||
...tool.params,
|
||||
...toolArgs,
|
||||
}
|
||||
|
||||
// Add system parameters for execution
|
||||
const executionParams = {
|
||||
...toolParams,
|
||||
...(request.workflowId
|
||||
? {
|
||||
_context: {
|
||||
workflowId: request.workflowId,
|
||||
...(request.chatId ? { chatId: request.chatId } : {}),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(request.environmentVariables ? { envVars: request.environmentVariables } : {}),
|
||||
}
|
||||
const { toolParams, executionParams } = prepareToolExecution(tool, toolArgs, request)
|
||||
|
||||
const result = await executeTool(toolName, executionParams, true)
|
||||
const toolCallEndTime = Date.now()
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
ProviderResponse,
|
||||
TimeSegment,
|
||||
} from '@/providers/types'
|
||||
import { prepareToolExecution } from '@/providers/utils'
|
||||
import { executeTool } from '@/tools'
|
||||
|
||||
const logger = createLogger('GroqProvider')
|
||||
@@ -258,25 +259,7 @@ export const groqProvider: ProviderConfig = {
|
||||
// Execute the tool
|
||||
const toolCallStartTime = Date.now()
|
||||
|
||||
// Only merge actual tool parameters for logging
|
||||
const toolParams = {
|
||||
...tool.params,
|
||||
...toolArgs,
|
||||
}
|
||||
|
||||
// Add system parameters for execution
|
||||
const executionParams = {
|
||||
...toolParams,
|
||||
...(request.workflowId
|
||||
? {
|
||||
_context: {
|
||||
workflowId: request.workflowId,
|
||||
...(request.chatId ? { chatId: request.chatId } : {}),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(request.environmentVariables ? { envVars: request.environmentVariables } : {}),
|
||||
}
|
||||
const { toolParams, executionParams } = prepareToolExecution(tool, toolArgs, request)
|
||||
|
||||
const result = await executeTool(toolName, executionParams, true)
|
||||
const toolCallEndTime = Date.now()
|
||||
|
||||
@@ -152,6 +152,9 @@ export interface ProviderRequest {
|
||||
stream?: boolean
|
||||
streamToolCalls?: boolean // Whether to stream tool call responses back to user (default: false)
|
||||
environmentVariables?: Record<string, string> // Environment variables for tool execution
|
||||
workflowVariables?: Record<string, any> // Workflow variables for <variable.name> resolution
|
||||
blockData?: Record<string, any> // Runtime block outputs for <block.field> resolution in custom tools
|
||||
blockNameMapping?: Record<string, string> // Mapping of block names to IDs for resolution
|
||||
isCopilotRequest?: boolean // Flag to indicate this request is from the copilot system
|
||||
// Azure OpenAI specific parameters
|
||||
azureEndpoint?: string
|
||||
|
||||
@@ -237,8 +237,6 @@ export function generateStructuredOutputInstructions(responseFormat: any): strin
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
logger.info(`Generated structured output instructions for ${responseFormat.fields.length} fields`)
|
||||
|
||||
return `
|
||||
Please provide your response in the following JSON format:
|
||||
{
|
||||
@@ -323,10 +321,6 @@ export function getCustomTools(): ProviderToolConfig[] {
|
||||
// Get custom tools from the store
|
||||
const customTools = useCustomToolsStore.getState().getAllTools()
|
||||
|
||||
if (customTools.length > 0) {
|
||||
logger.info(`Found ${customTools.length} custom tools`)
|
||||
}
|
||||
|
||||
// Transform each custom tool into a provider tool config
|
||||
return customTools.map(transformCustomTool)
|
||||
}
|
||||
@@ -912,7 +906,15 @@ export function supportsToolUsageControl(provider: string): boolean {
|
||||
export function prepareToolExecution(
|
||||
tool: { params?: Record<string, any> },
|
||||
llmArgs: Record<string, any>,
|
||||
request: { workflowId?: string; chatId?: string; environmentVariables?: Record<string, any> }
|
||||
request: {
|
||||
workflowId?: string
|
||||
chatId?: string
|
||||
userId?: string
|
||||
environmentVariables?: Record<string, any>
|
||||
workflowVariables?: Record<string, any>
|
||||
blockData?: Record<string, any>
|
||||
blockNameMapping?: Record<string, string>
|
||||
}
|
||||
): {
|
||||
toolParams: Record<string, any>
|
||||
executionParams: Record<string, any>
|
||||
@@ -931,10 +933,14 @@ export function prepareToolExecution(
|
||||
_context: {
|
||||
workflowId: request.workflowId,
|
||||
...(request.chatId ? { chatId: request.chatId } : {}),
|
||||
...(request.userId ? { userId: request.userId } : {}),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(request.environmentVariables ? { envVars: request.environmentVariables } : {}),
|
||||
...(request.workflowVariables ? { workflowVariables: request.workflowVariables } : {}),
|
||||
...(request.blockData ? { blockData: request.blockData } : {}),
|
||||
...(request.blockNameMapping ? { blockNameMapping: request.blockNameMapping } : {}),
|
||||
}
|
||||
|
||||
return { toolParams, executionParams }
|
||||
|
||||
@@ -8,7 +8,11 @@ import type {
|
||||
ProviderResponse,
|
||||
TimeSegment,
|
||||
} from '@/providers/types'
|
||||
import { prepareToolsWithUsageControl, trackForcedToolUsage } from '@/providers/utils'
|
||||
import {
|
||||
prepareToolExecution,
|
||||
prepareToolsWithUsageControl,
|
||||
trackForcedToolUsage,
|
||||
} from '@/providers/utils'
|
||||
import { executeTool } from '@/tools'
|
||||
|
||||
const logger = createLogger('XAIProvider')
|
||||
@@ -325,25 +329,7 @@ export const xAIProvider: ProviderConfig = {
|
||||
|
||||
const toolCallStartTime = Date.now()
|
||||
|
||||
// Only merge actual tool parameters for logging
|
||||
const toolParams = {
|
||||
...tool.params,
|
||||
...toolArgs,
|
||||
}
|
||||
|
||||
// Add system parameters for execution
|
||||
const executionParams = {
|
||||
...toolParams,
|
||||
...(request.workflowId
|
||||
? {
|
||||
_context: {
|
||||
workflowId: request.workflowId,
|
||||
...(request.chatId ? { chatId: request.chatId } : {}),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(request.environmentVariables ? { envVars: request.environmentVariables } : {}),
|
||||
}
|
||||
const { toolParams, executionParams } = prepareToolExecution(tool, toolArgs, request)
|
||||
|
||||
const result = await executeTool(toolName, executionParams, true)
|
||||
const toolCallEndTime = Date.now()
|
||||
|
||||
@@ -50,6 +50,7 @@ describe('Function Execute Tool', () => {
|
||||
expect(body).toEqual({
|
||||
code: 'return 42',
|
||||
envVars: {},
|
||||
workflowVariables: {},
|
||||
blockData: {},
|
||||
blockNameMapping: {},
|
||||
isCustomTool: false,
|
||||
@@ -75,6 +76,7 @@ describe('Function Execute Tool', () => {
|
||||
code: 'const x = 40;\nconst y = 2;\nreturn x + y;',
|
||||
timeout: 10000,
|
||||
envVars: {},
|
||||
workflowVariables: {},
|
||||
blockData: {},
|
||||
blockNameMapping: {},
|
||||
isCustomTool: false,
|
||||
@@ -91,6 +93,7 @@ describe('Function Execute Tool', () => {
|
||||
code: 'return 42',
|
||||
timeout: 10000,
|
||||
envVars: {},
|
||||
workflowVariables: {},
|
||||
blockData: {},
|
||||
blockNameMapping: {},
|
||||
isCustomTool: false,
|
||||
|
||||
@@ -45,6 +45,13 @@ export const functionExecuteTool: ToolConfig<CodeExecutionInput, CodeExecutionOu
|
||||
description: 'Mapping of block names to block IDs',
|
||||
default: {},
|
||||
},
|
||||
workflowVariables: {
|
||||
type: 'object',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Workflow variables for <variable.name> resolution',
|
||||
default: {},
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
@@ -62,6 +69,7 @@ export const functionExecuteTool: ToolConfig<CodeExecutionInput, CodeExecutionOu
|
||||
code: codeContent,
|
||||
timeout: params.timeout || DEFAULT_TIMEOUT,
|
||||
envVars: params.envVars || {},
|
||||
workflowVariables: params.workflowVariables || {},
|
||||
blockData: params.blockData || {},
|
||||
blockNameMapping: params.blockNameMapping || {},
|
||||
workflowId: params._context?.workflowId,
|
||||
|
||||
@@ -5,6 +5,7 @@ export interface CodeExecutionInput {
|
||||
timeout?: number
|
||||
memoryLimit?: number
|
||||
envVars?: Record<string, string>
|
||||
workflowVariables?: Record<string, any>
|
||||
blockData?: Record<string, any>
|
||||
blockNameMapping?: Record<string, string>
|
||||
_context?: {
|
||||
|
||||
@@ -40,16 +40,11 @@ async function processFileOutputs(
|
||||
return result
|
||||
}
|
||||
|
||||
logger.info(`File processing for tool ${tool.id}: checking outputs`, Object.keys(result.output))
|
||||
const processedOutput = await FileToolProcessor.processToolOutputs(
|
||||
result.output,
|
||||
tool,
|
||||
executionContext
|
||||
)
|
||||
logger.info(
|
||||
`File processing for tool ${tool.id}: processed outputs`,
|
||||
Object.keys(processedOutput)
|
||||
)
|
||||
|
||||
return {
|
||||
...result,
|
||||
@@ -536,7 +531,14 @@ function validateClientSideParams(
|
||||
}
|
||||
|
||||
// Internal parameters that should be excluded from validation
|
||||
const internalParamSet = new Set(['_context', 'workflowId', 'envVars'])
|
||||
const internalParamSet = new Set([
|
||||
'_context',
|
||||
'workflowId',
|
||||
'envVars',
|
||||
'workflowVariables',
|
||||
'blockData',
|
||||
'blockNameMapping',
|
||||
])
|
||||
|
||||
// Check required parameters
|
||||
if (schema.required) {
|
||||
|
||||
@@ -658,6 +658,9 @@ describe('createCustomToolRequestBody', () => {
|
||||
BASE_URL: 'https://example.com',
|
||||
},
|
||||
workflowId: undefined,
|
||||
workflowVariables: {},
|
||||
blockData: {},
|
||||
blockNameMapping: {},
|
||||
isCustomTool: true,
|
||||
})
|
||||
})
|
||||
@@ -682,6 +685,9 @@ describe('createCustomToolRequestBody', () => {
|
||||
schema: { type: 'object', properties: {} },
|
||||
envVars: {},
|
||||
workflowId: 'test-workflow-123',
|
||||
workflowVariables: {},
|
||||
blockData: {},
|
||||
blockNameMapping: {},
|
||||
isCustomTool: true,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -242,12 +242,22 @@ export function createCustomToolRequestBody(
|
||||
// 3. Empty object (fallback)
|
||||
const envVars = params.envVars || (isClient ? getClientEnvVars(getStore) : {})
|
||||
|
||||
// Get workflow variables from params (passed from execution context)
|
||||
const workflowVariables = params.workflowVariables || {}
|
||||
|
||||
// Get block data and mapping from params (passed from execution context)
|
||||
const blockData = params.blockData || {}
|
||||
const blockNameMapping = params.blockNameMapping || {}
|
||||
|
||||
// Include everything needed for execution
|
||||
return {
|
||||
code: customTool.code,
|
||||
params: params, // These will be available in the VM context
|
||||
schema: customTool.schema.function.parameters, // For validation
|
||||
envVars: envVars, // Environment variables
|
||||
workflowVariables: workflowVariables, // Workflow variables for <variable.name> resolution
|
||||
blockData: blockData, // Runtime block outputs for <block.field> resolution
|
||||
blockNameMapping: blockNameMapping, // Block name to ID mapping
|
||||
workflowId: workflowId, // Pass workflowId for server-side context
|
||||
isCustomTool: true, // Flag to indicate this is a custom tool execution
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user