- add sandbox type for aws agentcore

- TODO: get images from agentcore
This commit is contained in:
Henry 2025-09-10 13:03:35 +01:00
parent 099cf481b4
commit 3e233c889c
7 changed files with 1407 additions and 79 deletions

View File

@ -162,4 +162,16 @@ JWT_REFRESH_TOKEN_EXPIRY_IN_MINUTES=43200
# REDIS_KEY= # REDIS_KEY=
# REDIS_CA= # REDIS_CA=
# REDIS_KEEP_ALIVE= # REDIS_KEEP_ALIVE=
# ENABLE_BULLMQ_DASHBOARD= # ENABLE_BULLMQ_DASHBOARD=
############################################################################################################
############################################## SECURITY ####################################################
############################################################################################################
# HTTP_DENY_LIST=
# SANDBOX_TYPE= #(aws | e2b)
# AWS_AGENTCORE_ACCESS_KEY_ID=
# AWS_AGENTCORE_SECRET_ACCESS_KEY=
# AWS_AGENTCORE_REGION=
# E2B_APIKEY=

View File

@ -0,0 +1,363 @@
import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
import { StructuredTool, ToolInputParsingException, ToolParams } from '@langchain/core/tools'
import { z } from 'zod'
import { CallbackManager, CallbackManagerForToolRun, Callbacks, parseCallbackConfigArg } from '@langchain/core/callbacks/manager'
import { RunnableConfig } from '@langchain/core/runnables'
import { ARTIFACTS_PREFIX } from '../../../src/agents'
import {
BedrockAgentCoreClient,
StopCodeInterpreterSessionCommand,
InvokeCodeInterpreterCommand,
InvokeCodeInterpreterCommandInput,
BedrockAgentCoreClientConfig
} from '@aws-sdk/client-bedrock-agentcore'
const DESC = `Evaluates python code in a sandbox environment. \
The environment is long running and exists across multiple executions. \
You must send the whole script every time and print your outputs. \
Script should be pure python code that can be evaluated. \
It should be in python format NOT markdown. \
The code should NOT be wrapped in backticks. \
All python packages including requests, matplotlib, scipy, numpy, pandas, \
etc are available. Create and display chart using "plt.show()".`
const NAME = 'code_interpreter'
class Code_InterpreterAWS_Tools implements INode {
label: string
name: string
version: number
description: string
type: string
icon: string
category: string
baseClasses: string[]
inputs: INodeParams[]
badge: string
credential: INodeParams
constructor() {
this.label = 'Code Interpreter by AWS AgentCore'
this.name = 'codeInterpreterAWS'
this.version = 1.0
this.type = 'CodeInterpreter'
this.icon = 'agentcore.png'
this.category = 'Tools'
this.description = 'Execute code in a sandbox environment by AWS AgentCore'
this.baseClasses = [this.type, 'Tool', ...getBaseClasses(AWSAgentCoreTool)]
this.credential = {
label: 'Connect Credential',
name: 'credential',
type: 'credential',
credentialNames: ['awsApi']
}
this.inputs = [
{
label: 'AWS Region',
name: 'region',
type: 'options',
options: [
{ label: 'US East (N. Virginia) - us-east-1', name: 'us-east-1' },
{ label: 'US East (Ohio) - us-east-2', name: 'us-east-2' },
{ label: 'US West (N. California) - us-west-1', name: 'us-west-1' },
{ label: 'US West (Oregon) - us-west-2', name: 'us-west-2' },
{ label: 'Africa (Cape Town) - af-south-1', name: 'af-south-1' },
{ label: 'Asia Pacific (Hong Kong) - ap-east-1', name: 'ap-east-1' },
{ label: 'Asia Pacific (Mumbai) - ap-south-1', name: 'ap-south-1' },
{ label: 'Asia Pacific (Osaka) - ap-northeast-3', name: 'ap-northeast-3' },
{ label: 'Asia Pacific (Seoul) - ap-northeast-2', name: 'ap-northeast-2' },
{ label: 'Asia Pacific (Singapore) - ap-southeast-1', name: 'ap-southeast-1' },
{ label: 'Asia Pacific (Sydney) - ap-southeast-2', name: 'ap-southeast-2' },
{ label: 'Asia Pacific (Tokyo) - ap-northeast-1', name: 'ap-northeast-1' },
{ label: 'Canada (Central) - ca-central-1', name: 'ca-central-1' },
{ label: 'Europe (Frankfurt) - eu-central-1', name: 'eu-central-1' },
{ label: 'Europe (Ireland) - eu-west-1', name: 'eu-west-1' },
{ label: 'Europe (London) - eu-west-2', name: 'eu-west-2' },
{ label: 'Europe (Milan) - eu-south-1', name: 'eu-south-1' },
{ label: 'Europe (Paris) - eu-west-3', name: 'eu-west-3' },
{ label: 'Europe (Stockholm) - eu-north-1', name: 'eu-north-1' },
{ label: 'Middle East (Bahrain) - me-south-1', name: 'me-south-1' },
{ label: 'South America (São Paulo) - sa-east-1', name: 'sa-east-1' }
],
default: 'us-east-1',
description: 'AWS Region for AgentCore'
},
{
label: 'Tool Name',
name: 'toolName',
type: 'string',
description: 'Specify the name of the tool',
default: 'code_interpreter'
},
{
label: 'Tool Description',
name: 'toolDesc',
type: 'string',
rows: 4,
description: 'Specify the description of the tool',
default: DESC
}
]
}
async init(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
const toolDesc = nodeData.inputs?.toolDesc as string
const toolName = nodeData.inputs?.toolName as string
const region = nodeData.inputs?.region as string
const credentialData = await getCredentialData(nodeData.credential ?? '', options)
const accessKeyId = getCredentialParam('awsKey', credentialData, nodeData)
const secretAccessKey = getCredentialParam('awsSecret', credentialData, nodeData)
const sessionToken = getCredentialParam('awsSession', credentialData, nodeData)
return await AWSAgentCoreTool.initialize({
description: toolDesc ?? DESC,
name: toolName ?? NAME,
accessKeyId: accessKeyId,
secretAccessKey: secretAccessKey,
sessionToken: sessionToken,
region: region,
schema: z.object({
input: z.string().describe('Python code to be executed in the sandbox environment')
}),
chatflowid: options.chatflowid,
orgId: options.orgId
})
}
}
type AWSAgentCoreToolParams = ToolParams
type AWSAgentCoreToolInput = {
name: string
description: string
accessKeyId: string
secretAccessKey: string
sessionToken?: string
region: string
schema: any
chatflowid: string
orgId: string
}
export class AWSAgentCoreTool extends StructuredTool {
static lc_name() {
return 'AWSAgentCoreTool'
}
name = NAME
description = DESC
client: BedrockAgentCoreClient
accessKeyId: string
secretAccessKey: string
sessionToken?: string
region: string
schema
chatflowid: string
orgId: string
flowObj: ICommonObject
constructor(options: AWSAgentCoreToolParams & AWSAgentCoreToolInput) {
super(options)
this.description = options.description
this.name = options.name
this.accessKeyId = options.accessKeyId
this.secretAccessKey = options.secretAccessKey
this.sessionToken = options.sessionToken
this.region = options.region
this.schema = options.schema
this.chatflowid = options.chatflowid
this.orgId = options.orgId
// Initialize AWS AgentCore client
const awsAgentcoreConfig: BedrockAgentCoreClientConfig = {
region: this.region
}
if (this.accessKeyId && this.secretAccessKey) {
awsAgentcoreConfig.credentials = {
accessKeyId: this.accessKeyId,
secretAccessKey: this.secretAccessKey,
...(this.sessionToken && { sessionToken: this.sessionToken })
}
}
this.client = new BedrockAgentCoreClient(awsAgentcoreConfig)
}
static async initialize(options: Partial<AWSAgentCoreToolParams> & AWSAgentCoreToolInput) {
return new this({
name: options.name,
description: options.description,
accessKeyId: options.accessKeyId,
secretAccessKey: options.secretAccessKey,
sessionToken: options.sessionToken,
region: options.region,
schema: options.schema,
chatflowid: options.chatflowid,
orgId: options.orgId
})
}
async call(
arg: z.infer<typeof this.schema>,
configArg?: RunnableConfig | Callbacks,
tags?: string[],
flowConfig?: { sessionId?: string; chatId?: string; input?: string; state?: ICommonObject }
): Promise<string> {
const config = parseCallbackConfigArg(configArg)
if (config.runName === undefined) {
config.runName = this.name
}
let parsed
try {
parsed = await this.schema.parseAsync(arg)
} catch (e) {
throw new ToolInputParsingException(`Received tool input did not match expected schema`, JSON.stringify(arg))
}
const callbackManager_ = await CallbackManager.configure(
config.callbacks,
this.callbacks,
config.tags || tags,
this.tags,
config.metadata,
this.metadata,
{ verbose: this.verbose }
)
const runManager = await callbackManager_?.handleToolStart(
this.toJSON(),
typeof parsed === 'string' ? parsed : JSON.stringify(parsed),
undefined,
undefined,
undefined,
undefined,
config.runName
)
let result
try {
result = await this._call(parsed, runManager, flowConfig)
} catch (e) {
await runManager?.handleToolError(e)
throw e
}
if (result && typeof result !== 'string') {
result = JSON.stringify(result)
}
await runManager?.handleToolEnd(result)
return result
}
// @ts-ignore
protected async _call(
arg: z.infer<typeof this.schema>,
_?: CallbackManagerForToolRun,
flowConfig?: { sessionId?: string; chatId?: string; input?: string }
): Promise<string> {
flowConfig = { ...this.flowObj, ...flowConfig }
try {
if ('input' in arg) {
const input: InvokeCodeInterpreterCommandInput = {
codeInterpreterIdentifier: 'aws.codeinterpreter.v1',
name: 'executeCode',
arguments: {
code: arg.input,
language: 'python'
}
}
const command = new InvokeCodeInterpreterCommand(input)
const execution = await this.client.send(command)
const sessionId = execution.sessionId
let output = ''
const artifacts: any[] = []
if (!execution.stream) {
if (sessionId) {
const stopSessionCommand = new StopCodeInterpreterSessionCommand({
codeInterpreterIdentifier: 'aws.codeinterpreter.v1',
sessionId
})
await this.client.send(stopSessionCommand)
}
return output
}
for await (const chunk of execution.stream) {
// Process each chunk from the stream
if (chunk.result) {
console.log('chunk.result =', chunk.result)
// Process content blocks
if (chunk.result.content) {
console.log('chunk.resultcontent =', chunk.result.content)
for (const contentBlock of chunk.result.content) {
if (contentBlock.type === 'text' && contentBlock.text) {
output += contentBlock.text
}
// TODO: Handle other content types that might contain images
}
}
// Process structured content (stdout/stderr)
if (!output && chunk.result.structuredContent) {
if (chunk.result.structuredContent.stdout) {
output += chunk.result.structuredContent.stdout
}
if (chunk.result.structuredContent.stderr) {
throw new Error(`Code execution error: ${chunk.result.structuredContent.stderr}`)
}
}
}
const err =
chunk.accessDeniedException ||
chunk.internalServerException ||
chunk.throttlingException ||
chunk.validationException ||
chunk.conflictException ||
chunk.resourceNotFoundException ||
chunk.serviceQuotaExceededException
if (err) {
if (sessionId) {
const stopSessionCommand = new StopCodeInterpreterSessionCommand({
codeInterpreterIdentifier: 'aws.codeinterpreter.v1',
sessionId
})
await this.client.send(stopSessionCommand)
}
throw new Error(`${err.name}: ${err.message}`)
}
}
// Clean up session
if (sessionId) {
const stopSessionCommand = new StopCodeInterpreterSessionCommand({
codeInterpreterIdentifier: 'aws.codeinterpreter.v1',
sessionId
})
await this.client.send(stopSessionCommand)
}
return artifacts.length > 0 ? output + ARTIFACTS_PREFIX + JSON.stringify(artifacts) : output
} else {
return 'No input provided'
}
} catch (e) {
return typeof e === 'string' ? e : JSON.stringify(e, null, 2)
}
}
setFlowObject(flowObj: ICommonObject) {
this.flowObj = flowObj
}
}
module.exports = { nodeClass: Code_InterpreterAWS_Tools }

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@ -23,6 +23,7 @@
"dependencies": { "dependencies": {
"@apidevtools/json-schema-ref-parser": "^11.7.0", "@apidevtools/json-schema-ref-parser": "^11.7.0",
"@arizeai/openinference-instrumentation-langchain": "^2.0.0", "@arizeai/openinference-instrumentation-langchain": "^2.0.0",
"@aws-sdk/client-bedrock-agentcore": "^3.883.0",
"@aws-sdk/client-bedrock-runtime": "3.422.0", "@aws-sdk/client-bedrock-runtime": "3.422.0",
"@aws-sdk/client-dynamodb": "^3.360.0", "@aws-sdk/client-dynamodb": "^3.360.0",
"@aws-sdk/client-kendra": "^3.750.0", "@aws-sdk/client-kendra": "^3.750.0",

View File

@ -18,6 +18,13 @@ import { TextSplitter } from 'langchain/text_splitter'
import { DocumentLoader } from 'langchain/document_loaders/base' import { DocumentLoader } from 'langchain/document_loaders/base'
import { NodeVM } from '@flowiseai/nodevm' import { NodeVM } from '@flowiseai/nodevm'
import { Sandbox } from '@e2b/code-interpreter' import { Sandbox } from '@e2b/code-interpreter'
import {
BedrockAgentCoreClient,
StopCodeInterpreterSessionCommand,
InvokeCodeInterpreterCommand,
InvokeCodeInterpreterCommandInput,
BedrockAgentCoreClientConfig
} from '@aws-sdk/client-bedrock-agentcore'
export const numberOrExpressionRegex = '^(\\d+\\.?\\d*|{{.*}})$' //return true if string consists only numbers OR expression {{}} export const numberOrExpressionRegex = '^(\\d+\\.?\\d*|{{.*}})$' //return true if string consists only numbers OR expression {{}}
export const notEmptyRegex = '(.|\\s)*\\S(.|\\s)*' //return true if string is not empty or blank export const notEmptyRegex = '(.|\\s)*\\S(.|\\s)*' //return true if string is not empty or blank
@ -1401,63 +1408,69 @@ export const executeJavaScriptCode = async (
} = {} } = {}
): Promise<any> => { ): Promise<any> => {
const { timeout = 300000, useSandbox = true, streamOutput, libraries = [], nodeVMOptions = {} } = options const { timeout = 300000, useSandbox = true, streamOutput, libraries = [], nodeVMOptions = {} } = options
const shouldUseSandbox = useSandbox && process.env.E2B_APIKEY
if (shouldUseSandbox) { const sandboxType = process.env.SANDBOX_TYPE
try {
const variableDeclarations = []
if (sandbox['$vars']) { if (useSandbox) {
variableDeclarations.push(`const $vars = ${JSON.stringify(sandbox['$vars'])};`) const variableDeclarations = []
if (sandbox['$vars']) {
variableDeclarations.push(`const $vars = ${JSON.stringify(sandbox['$vars'])};`)
}
if (sandbox['$flow']) {
variableDeclarations.push(`const $flow = ${JSON.stringify(sandbox['$flow'])};`)
}
// Add other sandbox variables
for (const [key, value] of Object.entries(sandbox)) {
if (
key !== '$vars' &&
key !== '$flow' &&
key !== 'util' &&
key !== 'Symbol' &&
key !== 'child_process' &&
key !== 'fs' &&
key !== 'process'
) {
variableDeclarations.push(`const ${key} = ${JSON.stringify(value)};`)
}
}
// Handle import statements properly - they must be at the top
const lines = code.split('\n')
const importLines = []
const otherLines = []
for (const line of lines) {
const trimmedLine = line.trim()
// Skip node-fetch imports since Node.js has built-in fetch
if (trimmedLine.includes('node-fetch') || trimmedLine.includes("'fetch'") || trimmedLine.includes('"fetch"')) {
continue // Skip this line entirely
} }
if (sandbox['$flow']) { // Check for existing ES6 imports and exports
variableDeclarations.push(`const $flow = ${JSON.stringify(sandbox['$flow'])};`) if (trimmedLine.startsWith('import ') || trimmedLine.startsWith('export ')) {
importLines.push(line)
} }
// Check for CommonJS require statements and convert them to ESM imports
// Add other sandbox variables else if (/^(const|let|var)\s+.*=\s*require\s*\(/.test(trimmedLine)) {
for (const [key, value] of Object.entries(sandbox)) { const convertedImport = convertRequireToImport(trimmedLine)
if ( if (convertedImport) {
key !== '$vars' && importLines.push(convertedImport)
key !== '$flow' &&
key !== 'util' &&
key !== 'Symbol' &&
key !== 'child_process' &&
key !== 'fs' &&
key !== 'process'
) {
variableDeclarations.push(`const ${key} = ${JSON.stringify(value)};`)
} }
} else {
otherLines.push(line)
} }
}
// Handle import statements properly - they must be at the top // Separate imports from the rest of the code for proper ES6 module structure
const lines = code.split('\n') const codeWithImports = [...importLines, `module.exports = async function() {`, ...variableDeclarations, ...otherLines, `}()`].join(
const importLines = [] '\n'
const otherLines = [] )
for (const line of lines) {
const trimmedLine = line.trim()
// Skip node-fetch imports since Node.js has built-in fetch
if (trimmedLine.includes('node-fetch') || trimmedLine.includes("'fetch'") || trimmedLine.includes('"fetch"')) {
continue // Skip this line entirely
}
// Check for existing ES6 imports and exports
if (trimmedLine.startsWith('import ') || trimmedLine.startsWith('export ')) {
importLines.push(line)
}
// Check for CommonJS require statements and convert them to ESM imports
else if (/^(const|let|var)\s+.*=\s*require\s*\(/.test(trimmedLine)) {
const convertedImport = convertRequireToImport(trimmedLine)
if (convertedImport) {
importLines.push(convertedImport)
}
} else {
otherLines.push(line)
}
}
if (sandboxType === 'e2b' && process.env.E2B_APIKEY) {
const sbx = await Sandbox.create({ apiKey: process.env.E2B_APIKEY, timeoutMs: timeout }) const sbx = await Sandbox.create({ apiKey: process.env.E2B_APIKEY, timeoutMs: timeout })
// Install libraries // Install libraries
@ -1465,41 +1478,162 @@ export const executeJavaScriptCode = async (
await sbx.commands.run(`npm install ${library}`) await sbx.commands.run(`npm install ${library}`)
} }
// Separate imports from the rest of the code for proper ES6 module structure try {
const codeWithImports = [ const execution = await sbx.runCode(codeWithImports, { language: 'js' })
let output = ''
if (execution.text) output = execution.text
if (!execution.text && execution.logs.stdout.length) output = execution.logs.stdout.join('\n')
if (execution.error) {
throw new Error(`${execution.error.name}: ${execution.error.value}`)
}
if (execution.logs.stderr.length) {
throw new Error(execution.logs.stderr.join('\n'))
}
// Stream output if streaming function provided
if (streamOutput && output) {
streamOutput(output)
}
// Clean up sandbox
sbx.kill()
return output
} catch (e) {
throw new Error(`Sandbox Execution Error: ${e}`)
}
} else if (sandboxType === 'aws') {
const accessKeyId = process.env.AWS_AGENTCORE_ACCESS_KEY_ID
const secretAccessKey = process.env.AWS_AGENTCORE_SECRET_ACCESS_KEY
const region = process.env.AWS_AGENTCORE_REGION
if (!region || region.trim() === '') {
throw new Error('aws agentcore region is missing')
}
if (!accessKeyId || accessKeyId.trim() === '' || !secretAccessKey || secretAccessKey.trim() === '') {
throw new Error('aws agentcore access key id or secret access key is missing')
}
const awsAgentcoreConfig: BedrockAgentCoreClientConfig = {
region: region
}
if (accessKeyId && accessKeyId.trim() !== '' && secretAccessKey && secretAccessKey.trim() !== '') {
awsAgentcoreConfig.credentials = {
accessKeyId: accessKeyId,
secretAccessKey: secretAccessKey
}
}
const client = new BedrockAgentCoreClient(awsAgentcoreConfig)
// For AWS AgentCore (Deno), wrap in async function and log the result
const awsCodeWithImports = [
...importLines, ...importLines,
`module.exports = async function() {`, `(async () => {`,
...variableDeclarations, ...variableDeclarations,
...otherLines, // Add console.log before return statements for AgentCore output
`}()` ...otherLines.map((line) => {
const trimmed = line.trim()
if (trimmed.startsWith('return ')) {
const returnValue = trimmed.substring(7) // Remove 'return '
const indent = line.match(/^(\s*)/)?.[1] || ''
return indent + `console.log(${returnValue})` + '\n' + line
}
return line
}),
`})()`
].join('\n') ].join('\n')
const execution = await sbx.runCode(codeWithImports, { language: 'js' }) const input: InvokeCodeInterpreterCommandInput = {
codeInterpreterIdentifier: 'aws.codeinterpreter.v1',
let output = '' name: 'executeCode',
arguments: {
if (execution.text) output = execution.text code: awsCodeWithImports,
if (!execution.text && execution.logs.stdout.length) output = execution.logs.stdout.join('\n') language: 'javascript',
clearContext: true
if (execution.error) { }
throw new Error(`${execution.error.name}: ${execution.error.value}`)
} }
if (execution.logs.stderr.length) { try {
throw new Error(execution.logs.stderr.join('\n')) const command = new InvokeCodeInterpreterCommand(input)
const execution = await client.send(command)
const sessionId = execution.sessionId
const stopSessionCommand = new StopCodeInterpreterSessionCommand({
codeInterpreterIdentifier: 'aws.codeinterpreter.v1',
sessionId
})
let output = ''
if (!execution.stream) {
if (sessionId) {
await client.send(stopSessionCommand)
}
client.destroy()
return output
}
for await (const chunk of execution.stream) {
// Process each chunk from the stream
if (chunk.result) {
// Process content blocks
if (chunk.result.content) {
for (const contentBlock of chunk.result.content) {
if (contentBlock.type === 'text' && contentBlock.text) {
output += contentBlock.text
}
}
}
// Process structured content (stdout/stderr)
if (!output && chunk.result.structuredContent) {
if (chunk.result.structuredContent.stdout) {
output += chunk.result.structuredContent.stdout
}
if (chunk.result.structuredContent.stderr) {
throw new Error(`Code execution error: ${chunk.result.structuredContent.stderr}`)
}
}
}
const err =
chunk.accessDeniedException ||
chunk.internalServerException ||
chunk.throttlingException ||
chunk.validationException ||
chunk.conflictException ||
chunk.resourceNotFoundException ||
chunk.serviceQuotaExceededException
if (err) {
if (sessionId) {
await client.send(stopSessionCommand)
}
client.destroy()
throw new Error(`${err.name}: ${err.message}`)
}
}
// Stream output if streaming function provided
if (streamOutput && output) {
streamOutput(output)
}
// Clean up sandbox
if (sessionId) {
await client.send(stopSessionCommand)
}
client.destroy()
return output
} catch (e) {
throw new Error(`Sandbox Execution Error: ${e}`)
} }
// Stream output if streaming function provided
if (streamOutput && output) {
streamOutput(output)
}
// Clean up sandbox
sbx.kill()
return output
} catch (e) {
throw new Error(`Sandbox Execution Error: ${e}`)
} }
} else { } else {
const builtinDeps = process.env.TOOL_FUNCTION_BUILTIN_DEP const builtinDeps = process.env.TOOL_FUNCTION_BUILTIN_DEP

View File

@ -170,7 +170,11 @@ JWT_REFRESH_TOKEN_EXPIRY_IN_MINUTES=43200
############################################################################################################ ############################################################################################################
# HTTP_DENY_LIST= # HTTP_DENY_LIST=
# SANDBOX_TYPE= #(aws | e2b)
# AWS_AGENTCORE_ACCESS_KEY_ID=
# AWS_AGENTCORE_SECRET_ACCESS_KEY=
# AWS_AGENTCORE_REGION=
# E2B_APIKEY=
############################################################################################################ ############################################################################################################
########################################### DOCUMENT LOADERS ############################################### ########################################### DOCUMENT LOADERS ###############################################

File diff suppressed because it is too large Load Diff