mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-09 15:07:55 -05:00
feat(e2b-execution): add remote code execution to support Python + Imports (#1226)
* feat(e2b-execution): add remote code execution via e2b * ux improvements * fix streaming * progress * fix tooltip text * make supported languages an enum * fix error handling * fix tests
This commit is contained in:
committed by
GitHub
parent
9de0d91f9a
commit
1a5d5ddffa
@@ -32,6 +32,14 @@ describe('Function Execute API Route', () => {
|
||||
createLogger: vi.fn().mockReturnValue(mockLogger),
|
||||
}))
|
||||
|
||||
vi.doMock('@/lib/execution/e2b', () => ({
|
||||
executeInE2B: vi.fn().mockResolvedValue({
|
||||
result: 'e2b success',
|
||||
stdout: 'e2b output',
|
||||
sandboxId: 'test-sandbox-id',
|
||||
}),
|
||||
}))
|
||||
|
||||
mockRunInContext.mockResolvedValue('vm success')
|
||||
mockCreateContext.mockReturnValue({})
|
||||
})
|
||||
@@ -45,6 +53,7 @@ describe('Function Execute API Route', () => {
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'return "Hello World"',
|
||||
timeout: 5000,
|
||||
useLocalVM: true,
|
||||
})
|
||||
|
||||
const { POST } = await import('@/app/api/function/execute/route')
|
||||
@@ -74,6 +83,7 @@ describe('Function Execute API Route', () => {
|
||||
it('should use default timeout when not provided', async () => {
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'return "test"',
|
||||
useLocalVM: true,
|
||||
})
|
||||
|
||||
const { POST } = await import('@/app/api/function/execute/route')
|
||||
@@ -93,6 +103,7 @@ describe('Function Execute API Route', () => {
|
||||
it('should resolve environment variables with {{var_name}} syntax', async () => {
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'return {{API_KEY}}',
|
||||
useLocalVM: true,
|
||||
envVars: {
|
||||
API_KEY: 'secret-key-123',
|
||||
},
|
||||
@@ -108,6 +119,7 @@ describe('Function Execute API Route', () => {
|
||||
it('should resolve tag variables with <tag_name> syntax', async () => {
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'return <email>',
|
||||
useLocalVM: true,
|
||||
params: {
|
||||
email: { id: '123', subject: 'Test Email' },
|
||||
},
|
||||
@@ -123,6 +135,7 @@ describe('Function Execute API Route', () => {
|
||||
it('should NOT treat email addresses as template variables', async () => {
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'return "Email sent to user"',
|
||||
useLocalVM: true,
|
||||
params: {
|
||||
email: {
|
||||
from: 'Waleed Latif <waleed@sim.ai>',
|
||||
@@ -141,6 +154,7 @@ describe('Function Execute API Route', () => {
|
||||
it('should only match valid variable names in angle brackets', async () => {
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'return <validVar> + "<invalid@email.com>" + <another_valid>',
|
||||
useLocalVM: true,
|
||||
params: {
|
||||
validVar: 'hello',
|
||||
another_valid: 'world',
|
||||
@@ -178,6 +192,7 @@ describe('Function Execute API Route', () => {
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'return <email>',
|
||||
useLocalVM: true,
|
||||
params: gmailData,
|
||||
})
|
||||
|
||||
@@ -200,6 +215,7 @@ describe('Function Execute API Route', () => {
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'return <email>',
|
||||
useLocalVM: true,
|
||||
params: complexEmailData,
|
||||
})
|
||||
|
||||
@@ -214,6 +230,7 @@ describe('Function Execute API Route', () => {
|
||||
it('should handle custom tool execution with direct parameter access', async () => {
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'return location + " weather is sunny"',
|
||||
useLocalVM: true,
|
||||
params: {
|
||||
location: 'San Francisco',
|
||||
},
|
||||
@@ -245,6 +262,7 @@ describe('Function Execute API Route', () => {
|
||||
it('should handle timeout parameter', async () => {
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'return "test"',
|
||||
useLocalVM: true,
|
||||
timeout: 10000,
|
||||
})
|
||||
|
||||
@@ -262,6 +280,7 @@ describe('Function Execute API Route', () => {
|
||||
it('should handle empty parameters object', async () => {
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'return "no params"',
|
||||
useLocalVM: true,
|
||||
params: {},
|
||||
})
|
||||
|
||||
@@ -295,6 +314,7 @@ SyntaxError: Invalid or unexpected token
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'const obj = {\n name: "test",\n description: "This has a missing closing quote\n};\nreturn obj;',
|
||||
useLocalVM: true,
|
||||
timeout: 5000,
|
||||
})
|
||||
|
||||
@@ -338,6 +358,7 @@ SyntaxError: Invalid or unexpected token
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'const obj = null;\nreturn obj.someMethod();',
|
||||
useLocalVM: true,
|
||||
timeout: 5000,
|
||||
})
|
||||
|
||||
@@ -379,6 +400,7 @@ SyntaxError: Invalid or unexpected token
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'const x = 42;\nreturn undefinedVariable + x;',
|
||||
useLocalVM: true,
|
||||
timeout: 5000,
|
||||
})
|
||||
|
||||
@@ -409,6 +431,7 @@ SyntaxError: Invalid or unexpected token
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'return "test";',
|
||||
useLocalVM: true,
|
||||
timeout: 5000,
|
||||
})
|
||||
|
||||
@@ -445,6 +468,7 @@ SyntaxError: Invalid or unexpected token
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'const a = 1;\nconst b = 2;\nconst c = 3;\nconst d = 4;\nreturn a + b + c + d;',
|
||||
useLocalVM: true,
|
||||
timeout: 5000,
|
||||
})
|
||||
|
||||
@@ -476,6 +500,7 @@ SyntaxError: Invalid or unexpected token
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'const obj = {\n name: "test"\n// Missing closing brace',
|
||||
useLocalVM: true,
|
||||
timeout: 5000,
|
||||
})
|
||||
|
||||
@@ -496,6 +521,7 @@ SyntaxError: Invalid or unexpected token
|
||||
// This tests the escapeRegExp function indirectly
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'return {{special.chars+*?}}',
|
||||
useLocalVM: true,
|
||||
envVars: {
|
||||
'special.chars+*?': 'escaped-value',
|
||||
},
|
||||
@@ -512,6 +538,7 @@ SyntaxError: Invalid or unexpected token
|
||||
// Test with complex but not circular data first
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'return <complexData>',
|
||||
useLocalVM: true,
|
||||
params: {
|
||||
complexData: {
|
||||
special: 'chars"with\'quotes',
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { createContext, Script } from 'vm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { executeInE2B } from '@/lib/execution/e2b'
|
||||
import { CodeLanguage, DEFAULT_CODE_LANGUAGE, isValidCodeLanguage } from '@/lib/execution/languages'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
@@ -8,6 +10,10 @@ export const maxDuration = 60
|
||||
|
||||
const logger = createLogger('FunctionExecuteAPI')
|
||||
|
||||
// Constants for E2B code wrapping line counts
|
||||
const E2B_JS_WRAPPER_LINES = 3 // Lines before user code: ';(async () => {', ' try {', ' const __sim_result = await (async () => {'
|
||||
const E2B_PYTHON_WRAPPER_LINES = 1 // Lines before user code: 'def __sim_main__():'
|
||||
|
||||
/**
|
||||
* Enhanced error information interface
|
||||
*/
|
||||
@@ -124,6 +130,103 @@ function extractEnhancedError(
|
||||
return enhanced
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and format E2B error message
|
||||
* Removes E2B-specific line references and adds correct user line numbers
|
||||
*/
|
||||
function formatE2BError(
|
||||
errorMessage: string,
|
||||
errorOutput: string,
|
||||
language: CodeLanguage,
|
||||
userCode: string,
|
||||
prologueLineCount: number
|
||||
): { formattedError: string; cleanedOutput: string } {
|
||||
// Calculate line offset based on language and prologue
|
||||
const wrapperLines =
|
||||
language === CodeLanguage.Python ? E2B_PYTHON_WRAPPER_LINES : E2B_JS_WRAPPER_LINES
|
||||
const totalOffset = prologueLineCount + wrapperLines
|
||||
|
||||
let userLine: number | undefined
|
||||
let cleanErrorType = ''
|
||||
let cleanErrorMsg = ''
|
||||
|
||||
if (language === CodeLanguage.Python) {
|
||||
// Python error format: "Cell In[X], line Y" followed by error details
|
||||
// Extract line number from the Cell reference
|
||||
const cellMatch = errorOutput.match(/Cell In\[\d+\], line (\d+)/)
|
||||
if (cellMatch) {
|
||||
const originalLine = Number.parseInt(cellMatch[1], 10)
|
||||
userLine = originalLine - totalOffset
|
||||
}
|
||||
|
||||
// Extract clean error message from the error string
|
||||
// Remove file references like "(detected at line X) (file.py, line Y)"
|
||||
cleanErrorMsg = errorMessage
|
||||
.replace(/\s*\(detected at line \d+\)/g, '')
|
||||
.replace(/\s*\([^)]+\.py, line \d+\)/g, '')
|
||||
.trim()
|
||||
} else if (language === CodeLanguage.JavaScript) {
|
||||
// JavaScript error format from E2B: "SyntaxError: /path/file.ts: Message. (line:col)\n\n 9 | ..."
|
||||
// First, extract the error type and message from the first line
|
||||
const firstLineEnd = errorMessage.indexOf('\n')
|
||||
const firstLine = firstLineEnd > 0 ? errorMessage.substring(0, firstLineEnd) : errorMessage
|
||||
|
||||
// Parse: "SyntaxError: /home/user/index.ts: Missing semicolon. (11:9)"
|
||||
const jsErrorMatch = firstLine.match(/^(\w+Error):\s*[^:]+:\s*([^(]+)\.\s*\((\d+):(\d+)\)/)
|
||||
if (jsErrorMatch) {
|
||||
cleanErrorType = jsErrorMatch[1]
|
||||
cleanErrorMsg = jsErrorMatch[2].trim()
|
||||
const originalLine = Number.parseInt(jsErrorMatch[3], 10)
|
||||
userLine = originalLine - totalOffset
|
||||
} else {
|
||||
// Fallback: look for line number in the arrow pointer line (> 11 |)
|
||||
const arrowMatch = errorMessage.match(/^>\s*(\d+)\s*\|/m)
|
||||
if (arrowMatch) {
|
||||
const originalLine = Number.parseInt(arrowMatch[1], 10)
|
||||
userLine = originalLine - totalOffset
|
||||
}
|
||||
// Try to extract error type and message
|
||||
const errorMatch = firstLine.match(/^(\w+Error):\s*(.+)/)
|
||||
if (errorMatch) {
|
||||
cleanErrorType = errorMatch[1]
|
||||
cleanErrorMsg = errorMatch[2]
|
||||
.replace(/^[^:]+:\s*/, '') // Remove file path
|
||||
.replace(/\s*\(\d+:\d+\)\s*$/, '') // Remove line:col at end
|
||||
.trim()
|
||||
} else {
|
||||
cleanErrorMsg = firstLine
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build the final clean error message
|
||||
const finalErrorMsg =
|
||||
cleanErrorType && cleanErrorMsg
|
||||
? `${cleanErrorType}: ${cleanErrorMsg}`
|
||||
: cleanErrorMsg || errorMessage
|
||||
|
||||
// Format with line number if available
|
||||
let formattedError = finalErrorMsg
|
||||
if (userLine && userLine > 0) {
|
||||
const codeLines = userCode.split('\n')
|
||||
// Clamp userLine to the actual user code range
|
||||
const actualUserLine = Math.min(userLine, codeLines.length)
|
||||
if (actualUserLine > 0 && actualUserLine <= codeLines.length) {
|
||||
const lineContent = codeLines[actualUserLine - 1]?.trim()
|
||||
if (lineContent) {
|
||||
formattedError = `Line ${actualUserLine}: \`${lineContent}\` - ${finalErrorMsg}`
|
||||
} else {
|
||||
formattedError = `Line ${actualUserLine} - ${finalErrorMsg}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For stdout, just return the clean error message without the full traceback
|
||||
const cleanedOutput = finalErrorMsg
|
||||
|
||||
return { formattedError, cleanedOutput }
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a detailed error message for users
|
||||
*/
|
||||
@@ -442,6 +545,8 @@ export async function POST(req: NextRequest) {
|
||||
code,
|
||||
params = {},
|
||||
timeout = 5000,
|
||||
language = DEFAULT_CODE_LANGUAGE,
|
||||
useLocalVM = false,
|
||||
envVars = {},
|
||||
blockData = {},
|
||||
blockNameMapping = {},
|
||||
@@ -474,19 +579,163 @@ export async function POST(req: NextRequest) {
|
||||
resolvedCode = codeResolution.resolvedCode
|
||||
const contextVariables = codeResolution.contextVariables
|
||||
|
||||
const executionMethod = 'vm' // Default execution method
|
||||
const e2bEnabled = process.env.E2B_ENABLED === 'true'
|
||||
const lang = isValidCodeLanguage(language) ? language : DEFAULT_CODE_LANGUAGE
|
||||
const useE2B =
|
||||
e2bEnabled &&
|
||||
!useLocalVM &&
|
||||
(lang === CodeLanguage.JavaScript || lang === CodeLanguage.Python)
|
||||
|
||||
logger.info(`[${requestId}] Using VM for code execution`, {
|
||||
hasEnvVars: Object.keys(envVars).length > 0,
|
||||
hasWorkflowVariables: Object.keys(workflowVariables).length > 0,
|
||||
})
|
||||
if (useE2B) {
|
||||
logger.info(`[${requestId}] E2B status`, {
|
||||
enabled: e2bEnabled,
|
||||
hasApiKey: Boolean(process.env.E2B_API_KEY),
|
||||
language: lang,
|
||||
})
|
||||
let prologue = ''
|
||||
const epilogue = ''
|
||||
|
||||
// Create a secure context with console logging
|
||||
if (lang === CodeLanguage.JavaScript) {
|
||||
// Track prologue lines for error adjustment
|
||||
let prologueLineCount = 0
|
||||
prologue += `const params = JSON.parse(${JSON.stringify(JSON.stringify(executionParams))});\n`
|
||||
prologueLineCount++
|
||||
prologue += `const environmentVariables = JSON.parse(${JSON.stringify(JSON.stringify(envVars))});\n`
|
||||
prologueLineCount++
|
||||
for (const [k, v] of Object.entries(contextVariables)) {
|
||||
prologue += `const ${k} = JSON.parse(${JSON.stringify(JSON.stringify(v))});\n`
|
||||
prologueLineCount++
|
||||
}
|
||||
const wrapped = [
|
||||
';(async () => {',
|
||||
' try {',
|
||||
' const __sim_result = await (async () => {',
|
||||
` ${resolvedCode.split('\n').join('\n ')}`,
|
||||
' })();',
|
||||
" console.log('__SIM_RESULT__=' + JSON.stringify(__sim_result));",
|
||||
' } catch (error) {',
|
||||
' console.log(String((error && (error.stack || error.message)) || error));',
|
||||
' throw error;',
|
||||
' }',
|
||||
'})();',
|
||||
].join('\n')
|
||||
const codeForE2B = prologue + wrapped + epilogue
|
||||
|
||||
const execStart = Date.now()
|
||||
const {
|
||||
result: e2bResult,
|
||||
stdout: e2bStdout,
|
||||
sandboxId,
|
||||
error: e2bError,
|
||||
} = await executeInE2B({
|
||||
code: codeForE2B,
|
||||
language: CodeLanguage.JavaScript,
|
||||
timeoutMs: timeout,
|
||||
})
|
||||
const executionTime = Date.now() - execStart
|
||||
stdout += e2bStdout
|
||||
|
||||
logger.info(`[${requestId}] E2B JS sandbox`, {
|
||||
sandboxId,
|
||||
stdoutPreview: e2bStdout?.slice(0, 200),
|
||||
error: e2bError,
|
||||
})
|
||||
|
||||
// If there was an execution error, format it properly
|
||||
if (e2bError) {
|
||||
const { formattedError, cleanedOutput } = formatE2BError(
|
||||
e2bError,
|
||||
e2bStdout,
|
||||
lang,
|
||||
resolvedCode,
|
||||
prologueLineCount
|
||||
)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: formattedError,
|
||||
output: { result: null, stdout: cleanedOutput, executionTime },
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: { result: e2bResult ?? null, stdout, executionTime },
|
||||
})
|
||||
}
|
||||
// Track prologue lines for error adjustment
|
||||
let prologueLineCount = 0
|
||||
prologue += 'import json\n'
|
||||
prologueLineCount++
|
||||
prologue += `params = json.loads(${JSON.stringify(JSON.stringify(executionParams))})\n`
|
||||
prologueLineCount++
|
||||
prologue += `environmentVariables = json.loads(${JSON.stringify(JSON.stringify(envVars))})\n`
|
||||
prologueLineCount++
|
||||
for (const [k, v] of Object.entries(contextVariables)) {
|
||||
prologue += `${k} = json.loads(${JSON.stringify(JSON.stringify(v))})\n`
|
||||
prologueLineCount++
|
||||
}
|
||||
const wrapped = [
|
||||
'def __sim_main__():',
|
||||
...resolvedCode.split('\n').map((l) => ` ${l}`),
|
||||
'__sim_result__ = __sim_main__()',
|
||||
"print('__SIM_RESULT__=' + json.dumps(__sim_result__))",
|
||||
].join('\n')
|
||||
const codeForE2B = prologue + wrapped + epilogue
|
||||
|
||||
const execStart = Date.now()
|
||||
const {
|
||||
result: e2bResult,
|
||||
stdout: e2bStdout,
|
||||
sandboxId,
|
||||
error: e2bError,
|
||||
} = await executeInE2B({
|
||||
code: codeForE2B,
|
||||
language: CodeLanguage.Python,
|
||||
timeoutMs: timeout,
|
||||
})
|
||||
const executionTime = Date.now() - execStart
|
||||
stdout += e2bStdout
|
||||
|
||||
logger.info(`[${requestId}] E2B Py sandbox`, {
|
||||
sandboxId,
|
||||
stdoutPreview: e2bStdout?.slice(0, 200),
|
||||
error: e2bError,
|
||||
})
|
||||
|
||||
// If there was an execution error, format it properly
|
||||
if (e2bError) {
|
||||
const { formattedError, cleanedOutput } = formatE2BError(
|
||||
e2bError,
|
||||
e2bStdout,
|
||||
lang,
|
||||
resolvedCode,
|
||||
prologueLineCount
|
||||
)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: formattedError,
|
||||
output: { result: null, stdout: cleanedOutput, executionTime },
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: { result: e2bResult ?? null, stdout, executionTime },
|
||||
})
|
||||
}
|
||||
|
||||
const executionMethod = 'vm'
|
||||
const context = createContext({
|
||||
params: executionParams,
|
||||
environmentVariables: envVars,
|
||||
...contextVariables, // Add resolved variables directly to context
|
||||
fetch: globalThis.fetch || require('node-fetch').default,
|
||||
...contextVariables,
|
||||
fetch: (globalThis as any).fetch || require('node-fetch').default,
|
||||
console: {
|
||||
log: (...args: any[]) => {
|
||||
const logMessage = `${args
|
||||
@@ -504,23 +753,17 @@ export async function POST(req: NextRequest) {
|
||||
},
|
||||
})
|
||||
|
||||
// Calculate line offset for user code to provide accurate error reporting
|
||||
const wrapperLines = ['(async () => {', ' try {']
|
||||
|
||||
// Add custom tool parameter declarations if needed
|
||||
if (isCustomTool) {
|
||||
wrapperLines.push(' // For custom tools, make parameters directly accessible')
|
||||
Object.keys(executionParams).forEach((key) => {
|
||||
wrapperLines.push(` const ${key} = params.${key};`)
|
||||
})
|
||||
}
|
||||
|
||||
userCodeStartLine = wrapperLines.length + 1 // +1 because user code starts on next line
|
||||
|
||||
// Build the complete script with proper formatting for line numbers
|
||||
userCodeStartLine = wrapperLines.length + 1
|
||||
const fullScript = [
|
||||
...wrapperLines,
|
||||
` ${resolvedCode.split('\n').join('\n ')}`, // Indent user code
|
||||
` ${resolvedCode.split('\n').join('\n ')}`,
|
||||
' } catch (error) {',
|
||||
' console.error(error);',
|
||||
' throw error;',
|
||||
@@ -529,33 +772,26 @@ export async function POST(req: NextRequest) {
|
||||
].join('\n')
|
||||
|
||||
const script = new Script(fullScript, {
|
||||
filename: 'user-function.js', // This filename will appear in stack traces
|
||||
lineOffset: 0, // Start line numbering from 0
|
||||
columnOffset: 0, // Start column numbering from 0
|
||||
filename: 'user-function.js',
|
||||
lineOffset: 0,
|
||||
columnOffset: 0,
|
||||
})
|
||||
|
||||
const result = await script.runInContext(context, {
|
||||
timeout,
|
||||
displayErrors: true,
|
||||
breakOnSigint: true, // Allow breaking on SIGINT for better debugging
|
||||
breakOnSigint: true,
|
||||
})
|
||||
// }
|
||||
|
||||
const executionTime = Date.now() - startTime
|
||||
logger.info(`[${requestId}] Function executed successfully using ${executionMethod}`, {
|
||||
executionTime,
|
||||
})
|
||||
|
||||
const response = {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
result,
|
||||
stdout,
|
||||
executionTime,
|
||||
},
|
||||
}
|
||||
|
||||
return NextResponse.json(response)
|
||||
output: { result, stdout, executionTime },
|
||||
})
|
||||
} catch (error: any) {
|
||||
const executionTime = Date.now() - startTime
|
||||
logger.error(`[${requestId}] Function execution failed`, {
|
||||
|
||||
@@ -5,10 +5,12 @@ import { useParams } from 'next/navigation'
|
||||
import { highlight, languages } from 'prismjs'
|
||||
import 'prismjs/components/prism-javascript'
|
||||
import 'prismjs/themes/prism.css'
|
||||
import 'prismjs/components/prism-python'
|
||||
import Editor from 'react-simple-code-editor'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { checkEnvVarTrigger, EnvVarDropdown } from '@/components/ui/env-var-dropdown'
|
||||
import { checkTagTrigger, TagDropdown } from '@/components/ui/tag-dropdown'
|
||||
import { CodeLanguage } from '@/lib/execution/languages'
|
||||
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'
|
||||
@@ -26,7 +28,7 @@ interface CodeProps {
|
||||
subBlockId: string
|
||||
isConnecting: boolean
|
||||
placeholder?: string
|
||||
language?: 'javascript' | 'json'
|
||||
language?: 'javascript' | 'json' | 'python'
|
||||
generationType?: GenerationType
|
||||
value?: string
|
||||
isPreview?: boolean
|
||||
@@ -125,35 +127,73 @@ export function Code({
|
||||
if (onValidationChange && subBlockId === 'responseFormat') {
|
||||
const timeoutId = setTimeout(() => {
|
||||
onValidationChange(isValidJson)
|
||||
}, 150) // Match debounce time from setStoreValue
|
||||
}, 150)
|
||||
return () => clearTimeout(timeoutId)
|
||||
}
|
||||
}, [isValidJson, onValidationChange, subBlockId])
|
||||
|
||||
const editorRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Function to toggle collapsed state
|
||||
const toggleCollapsed = () => {
|
||||
setCollapsedValue(blockId, collapsedStateKey, !isCollapsed)
|
||||
}
|
||||
|
||||
// Create refs to hold the handlers
|
||||
const handleStreamStartRef = useRef<() => void>(() => {})
|
||||
const handleGeneratedContentRef = useRef<(generatedCode: string) => void>(() => {})
|
||||
const handleStreamChunkRef = useRef<(chunk: string) => void>(() => {})
|
||||
|
||||
// AI Code Generation Hook - use new wand system
|
||||
const wandHook = wandConfig?.enabled
|
||||
const [languageValue] = useSubBlockValue<string>(blockId, 'language')
|
||||
const [remoteExecution] = useSubBlockValue<boolean>(blockId, 'remoteExecution')
|
||||
|
||||
const effectiveLanguage = (languageValue as 'javascript' | 'python' | 'json') || language
|
||||
|
||||
const dynamicPlaceholder = useMemo(() => {
|
||||
if (remoteExecution && languageValue === CodeLanguage.Python) {
|
||||
return 'Write Python...'
|
||||
}
|
||||
return placeholder
|
||||
}, [remoteExecution, languageValue, placeholder])
|
||||
|
||||
const dynamicWandConfig = useMemo(() => {
|
||||
if (remoteExecution && languageValue === CodeLanguage.Python) {
|
||||
return {
|
||||
...wandConfig,
|
||||
prompt: `You are an expert Python programmer.
|
||||
Generate ONLY the raw body of a Python function based on the user's request.
|
||||
The code should be executable within a Python function body context.
|
||||
- 'params' (object): Contains input parameters derived from the JSON schema. Access these directly using the parameter name wrapped in angle brackets, e.g., '<paramName>'. Do NOT use 'params.paramName'.
|
||||
- 'environmentVariables' (object): Contains environment variables. Reference these using the double curly brace syntax: '{{ENV_VAR_NAME}}'. Do NOT use os.environ or env.
|
||||
|
||||
Current code context: {context}
|
||||
|
||||
IMPORTANT FORMATTING RULES:
|
||||
1. Reference Environment Variables: Use the exact syntax {{VARIABLE_NAME}}. Do NOT wrap it in quotes.
|
||||
2. Reference Input Parameters/Workflow Variables: Use the exact syntax <variable_name>. Do NOT wrap it in quotes.
|
||||
3. Function Body ONLY: Do NOT include the function signature (e.g., 'def my_func(...)') or surrounding braces. Return the final value with 'return'.
|
||||
4. Imports: You may add imports as needed (standard library or pip-installed packages) without comments.
|
||||
5. No Markdown: Do NOT include backticks, code fences, or any markdown.
|
||||
6. Clarity: Write clean, readable Python code.`,
|
||||
placeholder: 'Describe the Python function you want to create...',
|
||||
}
|
||||
}
|
||||
return wandConfig
|
||||
}, [wandConfig, remoteExecution, languageValue])
|
||||
|
||||
const wandHook = dynamicWandConfig?.enabled
|
||||
? useWand({
|
||||
wandConfig,
|
||||
wandConfig: dynamicWandConfig,
|
||||
currentValue: code,
|
||||
onStreamStart: () => handleStreamStartRef.current?.(),
|
||||
onStreamChunk: (chunk: string) => handleStreamChunkRef.current?.(chunk),
|
||||
onGeneratedContent: (content: string) => handleGeneratedContentRef.current?.(content),
|
||||
onStreamChunk: (chunk: string) => {
|
||||
setCode((prev) => prev + chunk)
|
||||
handleStreamChunkRef.current?.(chunk)
|
||||
},
|
||||
onGeneratedContent: (content: string) => {
|
||||
handleGeneratedContentRef.current?.(content)
|
||||
},
|
||||
})
|
||||
: null
|
||||
|
||||
// Extract values from wand hook
|
||||
const isAiLoading = wandHook?.isLoading || false
|
||||
const isAiStreaming = wandHook?.isStreaming || false
|
||||
const generateCodeStream = wandHook?.generateStream || (() => {})
|
||||
@@ -164,7 +204,6 @@ export function Code({
|
||||
const updatePromptValue = wandHook?.updatePromptValue || (() => {})
|
||||
const cancelGeneration = wandHook?.cancelGeneration || (() => {})
|
||||
|
||||
// State management - useSubBlockValue with explicit streaming control
|
||||
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId, false, {
|
||||
isStreaming: isAiStreaming,
|
||||
onStreamingEnd: () => {
|
||||
@@ -174,43 +213,28 @@ export function Code({
|
||||
|
||||
const emitTagSelection = useTagSelection(blockId, subBlockId)
|
||||
|
||||
// Use preview value when in preview mode, otherwise use store value or prop value
|
||||
const value = isPreview ? previewValue : propValue !== undefined ? propValue : storeValue
|
||||
|
||||
// Define the handlers in useEffect to avoid setState during render
|
||||
useEffect(() => {
|
||||
handleStreamStartRef.current = () => {
|
||||
setCode('')
|
||||
// Streaming state is now controlled by isAiStreaming
|
||||
}
|
||||
|
||||
handleGeneratedContentRef.current = (generatedCode: string) => {
|
||||
setCode(generatedCode)
|
||||
if (!isPreview && !disabled) {
|
||||
setStoreValue(generatedCode)
|
||||
// Final value will be persisted when isAiStreaming becomes false
|
||||
}
|
||||
}
|
||||
|
||||
handleStreamChunkRef.current = (chunk: string) => {
|
||||
setCode((currentCode) => {
|
||||
const newCode = currentCode + chunk
|
||||
if (!isPreview && !disabled) {
|
||||
// Update the value - it won't be persisted until streaming ends
|
||||
setStoreValue(newCode)
|
||||
}
|
||||
return newCode
|
||||
})
|
||||
}
|
||||
}, [isPreview, disabled, setStoreValue])
|
||||
|
||||
// Effects
|
||||
useEffect(() => {
|
||||
if (isAiStreaming) return
|
||||
const valueString = value?.toString() ?? ''
|
||||
if (valueString !== code) {
|
||||
setCode(valueString)
|
||||
}
|
||||
}, [value])
|
||||
}, [value, code, isAiStreaming])
|
||||
|
||||
useEffect(() => {
|
||||
if (!editorRef.current) return
|
||||
@@ -279,7 +303,6 @@ export function Code({
|
||||
}
|
||||
}, [code])
|
||||
|
||||
// Handlers
|
||||
const handleDrop = (e: React.DragEvent) => {
|
||||
if (isPreview) return
|
||||
e.preventDefault()
|
||||
@@ -338,12 +361,11 @@ export function Code({
|
||||
}, 0)
|
||||
}
|
||||
|
||||
// Render helpers
|
||||
const renderLineNumbers = () => {
|
||||
const renderLineNumbers = (): ReactElement[] => {
|
||||
const numbers: ReactElement[] = []
|
||||
let lineNumber = 1
|
||||
|
||||
visualLineHeights.forEach((height, index) => {
|
||||
visualLineHeights.forEach((height) => {
|
||||
numbers.push(
|
||||
<div key={`${lineNumber}-0`} className={cn('text-muted-foreground text-xs leading-[21px]')}>
|
||||
{lineNumber}
|
||||
@@ -364,7 +386,7 @@ export function Code({
|
||||
|
||||
if (numbers.length === 0) {
|
||||
numbers.push(
|
||||
<div key='1-0' className={cn('text-muted-foreground text-xs leading-[21px]')}>
|
||||
<div key={'1-0'} className={cn('text-muted-foreground text-xs leading-[21px]')}>
|
||||
1
|
||||
</div>
|
||||
)
|
||||
@@ -383,7 +405,7 @@ export function Code({
|
||||
onSubmit={(prompt: string) => generateCodeStream({ prompt })}
|
||||
onCancel={isAiStreaming ? cancelGeneration : hidePromptInline}
|
||||
onChange={updatePromptValue}
|
||||
placeholder={wandConfig?.placeholder || aiPromptPlaceholder}
|
||||
placeholder={dynamicWandConfig?.placeholder || aiPromptPlaceholder}
|
||||
/>
|
||||
|
||||
<div
|
||||
@@ -440,7 +462,7 @@ export function Code({
|
||||
>
|
||||
{code.length === 0 && !isCollapsed && (
|
||||
<div className='pointer-events-none absolute top-[12px] left-[42px] select-none text-muted-foreground/50'>
|
||||
{placeholder}
|
||||
{dynamicPlaceholder}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -478,7 +500,11 @@ export function Code({
|
||||
}
|
||||
}}
|
||||
highlight={(codeToHighlight) =>
|
||||
highlight(codeToHighlight, languages[language], language)
|
||||
highlight(
|
||||
codeToHighlight,
|
||||
languages[effectiveLanguage === 'python' ? 'python' : 'javascript'],
|
||||
effectiveLanguage === 'python' ? 'python' : 'javascript'
|
||||
)
|
||||
}
|
||||
padding={12}
|
||||
style={{
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Info } from 'lucide-react'
|
||||
import { Label, Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui'
|
||||
import { Switch as UISwitch } from '@/components/ui/switch'
|
||||
import { env } from '@/lib/env'
|
||||
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value'
|
||||
|
||||
interface E2BSwitchProps {
|
||||
blockId: string
|
||||
subBlockId: string
|
||||
title: string
|
||||
value?: boolean
|
||||
isPreview?: boolean
|
||||
previewValue?: boolean | null
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export function E2BSwitch({
|
||||
blockId,
|
||||
subBlockId,
|
||||
title,
|
||||
value: propValue,
|
||||
isPreview = false,
|
||||
previewValue,
|
||||
disabled = false,
|
||||
}: E2BSwitchProps) {
|
||||
const e2bEnabled = env.NEXT_PUBLIC_E2B_ENABLED === 'true'
|
||||
if (!e2bEnabled) return null
|
||||
|
||||
const [storeValue, setStoreValue] = useSubBlockValue<boolean>(blockId, subBlockId)
|
||||
const value = isPreview ? previewValue : propValue !== undefined ? propValue : storeValue
|
||||
|
||||
const handleChange = (checked: boolean) => {
|
||||
if (!isPreview && !disabled) setStoreValue(checked)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex items-center gap-2'>
|
||||
<UISwitch
|
||||
id={`${blockId}-${subBlockId}`}
|
||||
checked={Boolean(value)}
|
||||
onCheckedChange={handleChange}
|
||||
disabled={isPreview || disabled}
|
||||
/>
|
||||
<Label
|
||||
htmlFor={`${blockId}-${subBlockId}`}
|
||||
className='cursor-pointer font-normal text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
|
||||
>
|
||||
{title}
|
||||
</Label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className='h-4 w-4 cursor-pointer text-muted-foreground' />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side='top' className='max-w-[320px] select-text whitespace-pre-wrap'>
|
||||
Python/Javascript code run in a sandbox environment. Can have slower execution times.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components'
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
import { DocumentTagEntry } from './components/document-tag-entry/document-tag-entry'
|
||||
import { E2BSwitch } from './components/e2b-switch'
|
||||
import { KnowledgeTagFilters } from './components/knowledge-tag-filters/knowledge-tag-filters'
|
||||
|
||||
interface SubBlockProps {
|
||||
@@ -203,6 +204,18 @@ export function SubBlock({
|
||||
/>
|
||||
)
|
||||
case 'switch':
|
||||
if (config.id === 'remoteExecution') {
|
||||
return (
|
||||
<E2BSwitch
|
||||
blockId={blockId}
|
||||
subBlockId={config.id}
|
||||
title={config.title ?? ''}
|
||||
isPreview={isPreview}
|
||||
previewValue={previewValue}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Switch
|
||||
blockId={blockId}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { env } from '@/lib/env'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { parseCronToHumanReadable } from '@/lib/schedules/utils'
|
||||
import { cn, validateName } from '@/lib/utils'
|
||||
@@ -442,11 +443,17 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
|
||||
const isTriggerMode = useWorkflowStore.getState().blocks[blockId]?.triggerMode ?? false
|
||||
const effectiveAdvanced = currentWorkflow.isDiffMode ? displayAdvancedMode : isAdvancedMode
|
||||
const effectiveTrigger = currentWorkflow.isDiffMode ? displayTriggerMode : isTriggerMode
|
||||
const e2bClientEnabled = env.NEXT_PUBLIC_E2B_ENABLED === 'true'
|
||||
|
||||
// Filter visible blocks and those that meet their conditions
|
||||
const visibleSubBlocks = subBlocks.filter((block) => {
|
||||
if (block.hidden) return false
|
||||
|
||||
// Filter out E2B-related blocks if E2B is not enabled on the client
|
||||
if (!e2bClientEnabled && (block.id === 'remoteExecution' || block.id === 'language')) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Special handling for trigger mode
|
||||
if (block.type === ('trigger-config' as SubBlockType)) {
|
||||
// Show trigger-config blocks when in trigger mode OR for pure trigger blocks
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { CodeIcon } from '@/components/icons'
|
||||
import { CodeLanguage, getLanguageDisplayName } from '@/lib/execution/languages'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import type { CodeExecutionOutput } from '@/tools/function/types'
|
||||
|
||||
@@ -7,12 +8,34 @@ export const FunctionBlock: BlockConfig<CodeExecutionOutput> = {
|
||||
name: 'Function',
|
||||
description: 'Run custom logic',
|
||||
longDescription:
|
||||
'Execute custom JavaScript or TypeScript code within your workflow to transform data or implement complex logic. Create reusable functions to process inputs and generate outputs for other blocks.',
|
||||
'Execute custom JavaScript or Python code within your workflow. Use E2B for remote execution with imports or enable Fast Mode (bolt) to run JavaScript locally for lowest latency.',
|
||||
docsLink: 'https://docs.sim.ai/blocks/function',
|
||||
category: 'blocks',
|
||||
bgColor: '#FF402F',
|
||||
icon: CodeIcon,
|
||||
subBlocks: [
|
||||
{
|
||||
id: 'remoteExecution',
|
||||
type: 'switch',
|
||||
layout: 'full',
|
||||
title: 'Remote Code Execution',
|
||||
description: 'Python/Javascript code run in a sandbox environment. Slower execution times.',
|
||||
},
|
||||
{
|
||||
id: 'language',
|
||||
type: 'dropdown',
|
||||
layout: 'full',
|
||||
options: [
|
||||
{ label: getLanguageDisplayName(CodeLanguage.JavaScript), id: CodeLanguage.JavaScript },
|
||||
{ label: getLanguageDisplayName(CodeLanguage.Python), id: CodeLanguage.Python },
|
||||
],
|
||||
placeholder: 'Select language',
|
||||
value: () => CodeLanguage.JavaScript,
|
||||
condition: {
|
||||
field: 'remoteExecution',
|
||||
value: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'code',
|
||||
type: 'code',
|
||||
@@ -76,7 +99,9 @@ try {
|
||||
access: ['function_execute'],
|
||||
},
|
||||
inputs: {
|
||||
code: { type: 'string', description: 'JavaScript/TypeScript code to execute' },
|
||||
code: { type: 'string', description: 'JavaScript or Python code to execute' },
|
||||
remoteExecution: { type: 'boolean', description: 'Use E2B remote execution' },
|
||||
language: { type: 'string', description: 'Language (javascript or python)' },
|
||||
timeout: { type: 'number', description: 'Execution timeout' },
|
||||
},
|
||||
outputs: {
|
||||
|
||||
@@ -75,6 +75,8 @@ describe('FunctionBlockHandler', () => {
|
||||
}
|
||||
const expectedToolParams = {
|
||||
code: inputs.code,
|
||||
language: 'javascript',
|
||||
useLocalVM: true,
|
||||
timeout: inputs.timeout,
|
||||
envVars: {},
|
||||
workflowVariables: {},
|
||||
@@ -107,6 +109,8 @@ describe('FunctionBlockHandler', () => {
|
||||
const expectedCode = 'const x = 5;\nreturn x * 2;'
|
||||
const expectedToolParams = {
|
||||
code: expectedCode,
|
||||
language: 'javascript',
|
||||
useLocalVM: true,
|
||||
timeout: inputs.timeout,
|
||||
envVars: {},
|
||||
workflowVariables: {},
|
||||
@@ -132,6 +136,8 @@ describe('FunctionBlockHandler', () => {
|
||||
const inputs = { code: 'return true;' }
|
||||
const expectedToolParams = {
|
||||
code: inputs.code,
|
||||
language: 'javascript',
|
||||
useLocalVM: true,
|
||||
timeout: 5000, // Default timeout
|
||||
envVars: {},
|
||||
workflowVariables: {},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { DEFAULT_CODE_LANGUAGE } from '@/lib/execution/languages'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { BlockType } from '@/executor/consts'
|
||||
import type { BlockHandler, ExecutionContext } from '@/executor/types'
|
||||
@@ -58,6 +59,8 @@ export class FunctionBlockHandler implements BlockHandler {
|
||||
'function_execute',
|
||||
{
|
||||
code: codeContent,
|
||||
language: inputs.language || DEFAULT_CODE_LANGUAGE,
|
||||
useLocalVM: !inputs.remoteExecution,
|
||||
timeout: inputs.timeout || 5000,
|
||||
envVars: context.environmentVariables || {},
|
||||
workflowVariables: context.workflowVariables || {},
|
||||
|
||||
@@ -189,6 +189,10 @@ export const env = createEnv({
|
||||
SLACK_CLIENT_SECRET: z.string().optional(), // Slack OAuth client secret
|
||||
REDDIT_CLIENT_ID: z.string().optional(), // Reddit OAuth client ID
|
||||
REDDIT_CLIENT_SECRET: z.string().optional(), // Reddit OAuth client secret
|
||||
|
||||
// E2B Remote Code Execution
|
||||
E2B_ENABLED: z.string().optional(), // Enable E2B remote code execution
|
||||
E2B_API_KEY: z.string().optional(), // E2B API key for sandbox creation
|
||||
},
|
||||
|
||||
client: {
|
||||
@@ -220,6 +224,8 @@ export const env = createEnv({
|
||||
NEXT_PUBLIC_BRAND_FAVICON_URL: z.string().url().optional(), // Custom favicon URL
|
||||
NEXT_PUBLIC_CUSTOM_CSS_URL: z.string().url().optional(), // Custom CSS stylesheet URL
|
||||
NEXT_PUBLIC_SUPPORT_EMAIL: z.string().email().optional(), // Custom support email
|
||||
|
||||
NEXT_PUBLIC_E2B_ENABLED: z.string().optional(), // Enable E2B remote code execution (client-side)
|
||||
NEXT_PUBLIC_DOCUMENTATION_URL: z.string().url().optional(), // Custom documentation URL
|
||||
NEXT_PUBLIC_TERMS_URL: z.string().url().optional(), // Custom terms of service URL
|
||||
NEXT_PUBLIC_PRIVACY_URL: z.string().url().optional(), // Custom privacy policy URL
|
||||
@@ -268,6 +274,7 @@ export const env = createEnv({
|
||||
NEXT_PUBLIC_BRAND_ACCENT_HOVER_COLOR: process.env.NEXT_PUBLIC_BRAND_ACCENT_HOVER_COLOR,
|
||||
NEXT_PUBLIC_BRAND_BACKGROUND_COLOR: process.env.NEXT_PUBLIC_BRAND_BACKGROUND_COLOR,
|
||||
NEXT_PUBLIC_TRIGGER_DEV_ENABLED: process.env.NEXT_PUBLIC_TRIGGER_DEV_ENABLED,
|
||||
NEXT_PUBLIC_E2B_ENABLED: process.env.NEXT_PUBLIC_E2B_ENABLED,
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
NEXT_TELEMETRY_DISABLED: process.env.NEXT_TELEMETRY_DISABLED,
|
||||
},
|
||||
|
||||
98
apps/sim/lib/execution/e2b.ts
Normal file
98
apps/sim/lib/execution/e2b.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { Sandbox } from '@e2b/code-interpreter'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { CodeLanguage } from './languages'
|
||||
|
||||
export interface E2BExecutionRequest {
|
||||
code: string
|
||||
language: CodeLanguage
|
||||
timeoutMs: number
|
||||
}
|
||||
|
||||
export interface E2BExecutionResult {
|
||||
result: unknown
|
||||
stdout: string
|
||||
sandboxId?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
const logger = createLogger('E2BExecution')
|
||||
|
||||
export async function executeInE2B(req: E2BExecutionRequest): Promise<E2BExecutionResult> {
|
||||
const { code, language, timeoutMs } = req
|
||||
|
||||
logger.info(`Executing code in E2B`, {
|
||||
code,
|
||||
language,
|
||||
timeoutMs,
|
||||
})
|
||||
|
||||
const apiKey = process.env.E2B_API_KEY
|
||||
if (!apiKey) {
|
||||
throw new Error('E2B_API_KEY is required when E2B is enabled')
|
||||
}
|
||||
|
||||
const sandbox = await Sandbox.create({ apiKey })
|
||||
const sandboxId = sandbox.sandboxId
|
||||
|
||||
const stdoutChunks = []
|
||||
|
||||
try {
|
||||
const execution = await sandbox.runCode(code, {
|
||||
language: language === CodeLanguage.Python ? 'python' : 'javascript',
|
||||
timeoutMs,
|
||||
})
|
||||
|
||||
// Check for execution errors
|
||||
if (execution.error) {
|
||||
const errorMessage = `${execution.error.name}: ${execution.error.value}`
|
||||
logger.error(`E2B execution error`, {
|
||||
sandboxId,
|
||||
error: execution.error,
|
||||
errorMessage,
|
||||
})
|
||||
|
||||
// Include error traceback in stdout if available
|
||||
const errorOutput = execution.error.traceback || errorMessage
|
||||
return {
|
||||
result: null,
|
||||
stdout: errorOutput,
|
||||
error: errorMessage,
|
||||
sandboxId,
|
||||
}
|
||||
}
|
||||
|
||||
// Get output from execution
|
||||
if (execution.text) {
|
||||
stdoutChunks.push(execution.text)
|
||||
}
|
||||
if (execution.logs?.stdout) {
|
||||
stdoutChunks.push(...execution.logs.stdout)
|
||||
}
|
||||
if (execution.logs?.stderr) {
|
||||
stdoutChunks.push(...execution.logs.stderr)
|
||||
}
|
||||
|
||||
const stdout = stdoutChunks.join('\n')
|
||||
|
||||
let result: unknown = null
|
||||
const prefix = '__SIM_RESULT__='
|
||||
const lines = stdout.split('\n')
|
||||
const marker = lines.find((l) => l.startsWith(prefix))
|
||||
let cleanedStdout = stdout
|
||||
if (marker) {
|
||||
const jsonPart = marker.slice(prefix.length)
|
||||
try {
|
||||
result = JSON.parse(jsonPart)
|
||||
} catch {
|
||||
result = jsonPart
|
||||
}
|
||||
cleanedStdout = lines.filter((l) => !l.startsWith(prefix)).join('\n')
|
||||
}
|
||||
|
||||
return { result, stdout: cleanedStdout, sandboxId }
|
||||
} finally {
|
||||
try {
|
||||
await sandbox.kill()
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
33
apps/sim/lib/execution/languages.ts
Normal file
33
apps/sim/lib/execution/languages.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Supported code execution languages
|
||||
*/
|
||||
export enum CodeLanguage {
|
||||
JavaScript = 'javascript',
|
||||
Python = 'python',
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if a string is a valid CodeLanguage
|
||||
*/
|
||||
export function isValidCodeLanguage(value: string): value is CodeLanguage {
|
||||
return Object.values(CodeLanguage).includes(value as CodeLanguage)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get language display name
|
||||
*/
|
||||
export function getLanguageDisplayName(language: CodeLanguage): string {
|
||||
switch (language) {
|
||||
case CodeLanguage.JavaScript:
|
||||
return 'JavaScript'
|
||||
case CodeLanguage.Python:
|
||||
return 'Python'
|
||||
default:
|
||||
return language
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default language for code execution
|
||||
*/
|
||||
export const DEFAULT_CODE_LANGUAGE = CodeLanguage.JavaScript
|
||||
@@ -26,6 +26,7 @@
|
||||
"test:billing:suite": "bun run scripts/test-billing-suite.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@e2b/code-interpreter": "^2.0.0",
|
||||
"@anthropic-ai/sdk": "^0.39.0",
|
||||
"@aws-sdk/client-s3": "^3.779.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.779.0",
|
||||
|
||||
@@ -54,6 +54,8 @@ describe('Function Execute Tool', () => {
|
||||
blockData: {},
|
||||
blockNameMapping: {},
|
||||
isCustomTool: false,
|
||||
language: 'javascript',
|
||||
useLocalVM: false,
|
||||
timeout: 5000,
|
||||
workflowId: undefined,
|
||||
})
|
||||
@@ -80,6 +82,8 @@ describe('Function Execute Tool', () => {
|
||||
blockData: {},
|
||||
blockNameMapping: {},
|
||||
isCustomTool: false,
|
||||
language: 'javascript',
|
||||
useLocalVM: false,
|
||||
workflowId: undefined,
|
||||
})
|
||||
})
|
||||
@@ -97,6 +101,8 @@ describe('Function Execute Tool', () => {
|
||||
blockData: {},
|
||||
blockNameMapping: {},
|
||||
isCustomTool: false,
|
||||
language: 'javascript',
|
||||
useLocalVM: false,
|
||||
workflowId: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { DEFAULT_CODE_LANGUAGE } from '@/lib/execution/languages'
|
||||
import type { CodeExecutionInput, CodeExecutionOutput } from '@/tools/function/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
@@ -17,6 +18,21 @@ export const functionExecuteTool: ToolConfig<CodeExecutionInput, CodeExecutionOu
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The code to execute',
|
||||
},
|
||||
language: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Language to execute (javascript or python)',
|
||||
default: DEFAULT_CODE_LANGUAGE,
|
||||
},
|
||||
useLocalVM: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'If true, execute JavaScript in local VM for faster execution. If false, use remote E2B execution.',
|
||||
default: false,
|
||||
},
|
||||
timeout: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
@@ -67,6 +83,8 @@ export const functionExecuteTool: ToolConfig<CodeExecutionInput, CodeExecutionOu
|
||||
|
||||
return {
|
||||
code: codeContent,
|
||||
language: params.language || DEFAULT_CODE_LANGUAGE,
|
||||
useLocalVM: params.useLocalVM || false,
|
||||
timeout: params.timeout || DEFAULT_TIMEOUT,
|
||||
envVars: params.envVars || {},
|
||||
workflowVariables: params.workflowVariables || {},
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import type { CodeLanguage } from '@/lib/execution/languages'
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
|
||||
export interface CodeExecutionInput {
|
||||
code: Array<{ content: string; id: string }> | string
|
||||
language?: CodeLanguage
|
||||
useLocalVM?: boolean
|
||||
timeout?: number
|
||||
memoryLimit?: number
|
||||
envVars?: Record<string, string>
|
||||
workflowVariables?: Record<string, any>
|
||||
blockData?: Record<string, any>
|
||||
workflowVariables?: Record<string, unknown>
|
||||
blockData?: Record<string, unknown>
|
||||
blockNameMapping?: Record<string, string>
|
||||
_context?: {
|
||||
workflowId?: string
|
||||
|
||||
19
bun.lock
19
bun.lock
@@ -64,6 +64,7 @@
|
||||
"@cerebras/cerebras_cloud_sdk": "^1.23.0",
|
||||
"@chatscope/chat-ui-kit-react": "2.1.1",
|
||||
"@chatscope/chat-ui-kit-styles": "1.4.0",
|
||||
"@e2b/code-interpreter": "^2.0.0",
|
||||
"@hookform/resolvers": "^4.1.3",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/exporter-collector": "^0.25.0",
|
||||
@@ -463,6 +464,8 @@
|
||||
|
||||
"@browserbasehq/stagehand": ["@browserbasehq/stagehand@2.4.4", "", { "dependencies": { "@anthropic-ai/sdk": "0.39.0", "@browserbasehq/sdk": "^2.4.0", "@google/genai": "^0.8.0", "ai": "^4.3.9", "devtools-protocol": "^0.0.1464554", "fetch-cookie": "^3.1.0", "openai": "^4.87.1", "pino": "^9.6.0", "pino-pretty": "^13.0.0", "playwright": "^1.52.0", "ws": "^8.18.0", "zod-to-json-schema": "^3.23.5" }, "optionalDependencies": { "@ai-sdk/anthropic": "^1.2.6", "@ai-sdk/azure": "^1.3.19", "@ai-sdk/cerebras": "^0.2.6", "@ai-sdk/deepseek": "^0.2.13", "@ai-sdk/google": "^1.2.6", "@ai-sdk/groq": "^1.2.4", "@ai-sdk/mistral": "^1.2.7", "@ai-sdk/openai": "^1.0.14", "@ai-sdk/perplexity": "^1.1.7", "@ai-sdk/togetherai": "^0.2.6", "@ai-sdk/xai": "^1.2.15", "ollama-ai-provider": "^1.2.0" }, "peerDependencies": { "deepmerge": "^4.3.1", "dotenv": "^16.4.5", "zod": ">=3.25.0 <4.1.0" } }, "sha512-PSctDxqMrLLCwv+fg3piYfzUvhe9vWZjSkBWiDVfTL2/611+PrTMGPjnikz1rhyDrTo7IU9KGui/ZMntZHEnzQ=="],
|
||||
|
||||
"@bufbuild/protobuf": ["@bufbuild/protobuf@2.7.0", "", {}, "sha512-qn6tAIZEw5i/wiESBF4nQxZkl86aY4KoO0IkUa2Lh+rya64oTOdJQFlZuMwI1Qz9VBJQrQC4QlSA2DNek5gCOA=="],
|
||||
|
||||
"@bugsnag/cuid": ["@bugsnag/cuid@3.2.1", "", {}, "sha512-zpvN8xQ5rdRWakMd/BcVkdn2F8HKlDSbM3l7duueK590WmI1T0ObTLc1V/1e55r14WNjPd5AJTYX4yPEAFVi+Q=="],
|
||||
|
||||
"@cerebras/cerebras_cloud_sdk": ["@cerebras/cerebras_cloud_sdk@1.46.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" } }, "sha512-5IFlD9jvZr5fRsHd4pL4sFkzDPSvHxzM0dmk3mJG2/biRBl+3EV6CdXCabf7J8kr8hyQyK67N0rQLnYy6NSh9w=="],
|
||||
@@ -471,6 +474,10 @@
|
||||
|
||||
"@chatscope/chat-ui-kit-styles": ["@chatscope/chat-ui-kit-styles@1.4.0", "", {}, "sha512-016mBJD3DESw7Nh+lkKcPd22xG92ghA0VpIXIbjQtmXhC7Ve6wRazTy8z1Ahut+Tbv179+JxrftuMngsj/yV8Q=="],
|
||||
|
||||
"@connectrpc/connect": ["@connectrpc/connect@2.0.0-rc.3", "", { "peerDependencies": { "@bufbuild/protobuf": "^2.2.0" } }, "sha512-ARBt64yEyKbanyRETTjcjJuHr2YXorzQo0etyS5+P6oSeW8xEuzajA9g+zDnMcj1hlX2dQE93foIWQGfpru7gQ=="],
|
||||
|
||||
"@connectrpc/connect-web": ["@connectrpc/connect-web@2.0.0-rc.3", "", { "peerDependencies": { "@bufbuild/protobuf": "^2.2.0", "@connectrpc/connect": "2.0.0-rc.3" } }, "sha512-w88P8Lsn5CCsA7MFRl2e6oLY4J/5toiNtJns/YJrlyQaWOy3RO8pDgkz+iIkG98RPMhj2thuBvsd3Cn4DKKCkw=="],
|
||||
|
||||
"@csstools/color-helpers": ["@csstools/color-helpers@5.1.0", "", {}, "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA=="],
|
||||
|
||||
"@csstools/css-calc": ["@csstools/css-calc@2.1.4", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ=="],
|
||||
@@ -485,6 +492,8 @@
|
||||
|
||||
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="],
|
||||
|
||||
"@e2b/code-interpreter": ["@e2b/code-interpreter@2.0.0", "", { "dependencies": { "e2b": "^2.0.1" } }, "sha512-rCIW4dV544sUx2YQB/hhwDrK6sQzIb5lr5h/1CIVoOIRgU9q3NUxsFj+2OsgWd4rMG8l6b/oA7FVZJwP4sGX/A=="],
|
||||
|
||||
"@electric-sql/client": ["@electric-sql/client@1.0.0-beta.1", "", { "optionalDependencies": { "@rollup/rollup-darwin-arm64": "^4.18.1" } }, "sha512-Ei9jN3pDoGzc+a/bGqnB5ajb52IvSv7/n2btuyzUlcOHIR2kM9fqtYTJXPwZYKLkGZlHWlpHgWyRtrinkP2nHg=="],
|
||||
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.5.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ=="],
|
||||
@@ -1741,6 +1750,8 @@
|
||||
|
||||
"commondir": ["commondir@1.0.1", "", {}, "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="],
|
||||
|
||||
"compare-versions": ["compare-versions@6.1.1", "", {}, "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg=="],
|
||||
|
||||
"compute-scroll-into-view": ["compute-scroll-into-view@3.1.1", "", {}, "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw=="],
|
||||
|
||||
"concat-stream": ["concat-stream@2.0.0", "", { "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.0.2", "typedarray": "^0.0.6" } }, "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A=="],
|
||||
@@ -1905,6 +1916,8 @@
|
||||
|
||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||
|
||||
"e2b": ["e2b@2.0.1", "", { "dependencies": { "@bufbuild/protobuf": "^2.6.2", "@connectrpc/connect": "2.0.0-rc.3", "@connectrpc/connect-web": "2.0.0-rc.3", "compare-versions": "^6.1.0", "openapi-fetch": "^0.9.7", "platform": "^1.3.6" } }, "sha512-wJgZTV1QFeh5WKQ23n6hWmMODPmyKiiWaQy+uxvV/5M9NH/zMY/ONzhh7j7ONYgeH8jdRZxOS2e+G345EodGeA=="],
|
||||
|
||||
"eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
|
||||
|
||||
"ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
|
||||
@@ -2601,6 +2614,10 @@
|
||||
|
||||
"openai": ["openai@4.104.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" }, "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA=="],
|
||||
|
||||
"openapi-fetch": ["openapi-fetch@0.9.8", "", { "dependencies": { "openapi-typescript-helpers": "^0.0.8" } }, "sha512-zM6elH0EZStD/gSiNlcPrzXcVQ/pZo3BDvC6CDwRDUt1dDzxlshpmQnpD6cZaJ39THaSmwVCxxRrPKNM1hHrDg=="],
|
||||
|
||||
"openapi-typescript-helpers": ["openapi-typescript-helpers@0.0.8", "", {}, "sha512-1eNjQtbfNi5Z/kFhagDIaIRj6qqDzhjNJKz8cmMW0CVdGwT6e1GLbAfgI0d28VTJa1A8jz82jm/4dG8qNoNS8g=="],
|
||||
|
||||
"opentracing": ["opentracing@0.14.7", "", {}, "sha512-vz9iS7MJ5+Bp1URw8Khvdyw1H/hGvzHWlKQ7eRrQojSCDL1/SrWfrY9QebLw97n2deyRtzHRC3MkQfVNUCo91Q=="],
|
||||
|
||||
"option": ["option@0.2.4", "", {}, "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A=="],
|
||||
@@ -2681,6 +2698,8 @@
|
||||
|
||||
"pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
|
||||
|
||||
"platform": ["platform@1.3.6", "", {}, "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg=="],
|
||||
|
||||
"playwright": ["playwright@1.55.0", "", { "dependencies": { "playwright-core": "1.55.0" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-sdCWStblvV1YU909Xqx0DhOjPZE4/5lJsIS84IfN9dAZfcl/CIZ5O8l3o0j7hPMjDvqoTF8ZUcc+i/GL5erstA=="],
|
||||
|
||||
"playwright-core": ["playwright-core@1.55.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-GvZs4vU3U5ro2nZpeiwyb0zuFaqb9sUiAJuyrWpcGouD8y9/HLgGbNRjIph7zU9D3hnPaisMl9zG9CgFi/biIg=="],
|
||||
|
||||
Reference in New Issue
Block a user