Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/cdk/consts.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as lambda from 'aws-cdk-lib/aws-lambda';

export const LAMBDA_RUNTIME_NODEJS = lambda.Runtime.NODEJS_22_X;
export const LAMBDA_RUNTIME_PYTHON = lambda.Runtime.PYTHON_3_12;

export const TAG_KEY = 'GenU';
2 changes: 2 additions & 0 deletions packages/cdk/lambda-python/generic-agent-core-runtime/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ async def invocations(request: Request):
model_info = request_data.get("model", {})
user_id = request_data.get("user_id")
mcp_servers = request_data.get("mcp_servers")
sub_agents = request_data.get("sub_agents", [])
agent_session_id = request_data.get("session_id")
agent_id = request_data.get("agent_id")
code_execution_enabled = request_data.get("code_execution_enabled", False)
Expand All @@ -87,6 +88,7 @@ async def generate():
model_info=model_info,
user_id=user_id,
mcp_servers=mcp_servers,
sub_agents=sub_agents,
session_id=agent_session_id or session_id,
agent_id=agent_id,
code_execution_enabled=code_execution_enabled,
Expand Down
115 changes: 114 additions & 1 deletion packages/cdk/lambda-python/generic-agent-core-runtime/src/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@

import json
import logging
import uuid
from collections.abc import AsyncGenerator
from typing import Any

import boto3
from strands import Agent as StrandsAgent
from strands.models import BedrockModel
from strands.tools import tool
from strands_tools.browser import AgentCoreBrowser

from .config import extract_model_info, get_max_iterations, get_system_prompt, supports_prompt_cache, supports_tools_cache
from .tools import ToolManager
Expand All @@ -20,6 +23,93 @@
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

# Initialize Bedrock AgentCore client (will be set when needed)
agent_core_client = None

# Initialize the Browser tool
browser_tool = AgentCoreBrowser(region="us-east-1")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The region should be configurable

Suggestion: Use environment variable consistently

Suggested change
browser_tool = AgentCoreBrowser(region="us-east-1")
browser_tool = AgentCoreBrowser(region=os.environ.get("AWS_REGION", "us-east-1"))



class SubAgent:
"""Class to represent a sub-agent with its description and ARN"""

def __init__(self, name: str, description: str, arn: str):
self.name = name
self.description = description
self.arn = arn

def create_tool_function(self):
"""Create a tool function for this specific sub-agent"""

# Capture self in closure
sub_agent_instance = self

def agent_tool(task: str, session_id: str | None = None) -> str:
"""Dynamic docstring for sub-agent tool.

Args:
task: The task or question to delegate to the sub-agent
session_id: Optional session ID for maintaining conversation context

Returns:
The response from the sub-agent
"""
return sub_agent_instance._invoke_agent(task, session_id)

# Set function metadata
agent_tool.__name__ = f"call_{self.name.lower().replace(' ', '_')}_agent"
agent_tool.__doc__ = f"{self.description}\nUse this when you need: {self.description.lower()}"

return tool(agent_tool)

def _invoke_agent(self, task: str, session_id: str | None = None) -> str:
"""Internal method to invoke this specific agent"""
global agent_core_client

# Initialize client if not already done
if agent_core_client is None:
agent_core_client = boto3.client("bedrock-agentcore", region_name="us-east-1") # fixed to us-east-1 for now

sid = session_id or str(uuid.uuid4())

try:
# Format payload according to Bedrock AgentCore API requirements
formatted_payload = {"prompt": task}

# Serialize payload to JSON bytes as required by AWS API
payload_bytes = json.dumps(formatted_payload).encode("utf-8")

logger.info(f"Invoking sub-agent {self.name} with ARN: {self.arn}")

response = agent_core_client.invoke_agent_runtime(agentRuntimeArn=self.arn, runtimeSessionId=sid, payload=payload_bytes)

# Process and return the response
if "text/event-stream" in response.get("contentType", ""):
# Handle streaming response
content = []
for line in response["response"].iter_lines(chunk_size=10):
if line:
line = line.decode("utf-8")
if line.startswith("data: "):
line = line[6:]
content.append(line)
return "\n".join(content)

elif response.get("contentType") == "application/json":
# Handle standard JSON response
content = []
for chunk in response.get("response", []):
content.append(chunk.decode("utf-8"))
return "".join(content)

else:
# Handle other response types
return str(response)

except Exception as e:
logger.error(f"Error calling {self.name} agent: {str(e)}")
return f"Error: Failed to call {self.name} agent - {str(e)}"


class IterationLimitExceededError(Exception):
"""Exception raised when iteration limit is exceeded"""
Expand Down Expand Up @@ -55,6 +145,7 @@ async def process_request_streaming(
model_info: ModelInfo,
user_id: str | None = None,
mcp_servers: list[str] | None = None,
sub_agents: list[dict[str, str]] | None = None,
session_id: str | None = None,
agent_id: str | None = None,
code_execution_enabled: bool | None = False,
Expand All @@ -73,7 +164,29 @@ async def process_request_streaming(

# Get tools (MCP handling is done in ToolManager)
tools = self.tool_manager.get_tools_with_options(code_execution_enabled=code_execution_enabled, mcp_servers=mcp_servers)
logger.info(f"Loaded {len(tools)} tools (code execution: {code_execution_enabled})")
logger.info(f"Loaded {len(tools)} base tools (code execution: {code_execution_enabled})")

# Log sub-agents if provided and add them as tools
if sub_agents:
logger.info(f"Sub-agents configured: {len(sub_agents)}")

# Create SubAgent instances from the provided configuration
SUB_AGENTS = []
for sub_agent_config in sub_agents:
sub_agent = SubAgent(name=sub_agent_config.get("name", "Unknown"), description=sub_agent_config.get("description", "No description"), arn=sub_agent_config.get("arn", ""))
SUB_AGENTS.append(sub_agent)
logger.debug(f" - {sub_agent.name}: {sub_agent.arn}")

# Create tool functions for each sub-agent and add to tools list
for sub_agent in SUB_AGENTS:
try:
sub_agent_tool = sub_agent.create_tool_function()
tools.append(sub_agent_tool)
logger.info(f"Added sub-agent tool: {sub_agent.name}")
except Exception as e:
logger.error(f"Failed to create tool for sub-agent {sub_agent.name}: {e}")

logger.info(f"Total tools available: {len(tools)}")

# Log agent info
if agent_id:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
"""Lambda function to list AgentCore Runtime agents."""
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the contribution!
We prefer TypeScript in this project. (Strands and fargate s3 server is exception)

We are migrating most of typescript lambda behind ALB to lambda monolith in another PR. Can you implement it in TypeScript so it would be easier to merge conflict?


import json
import logging
import os
from typing import Any, Dict

import boto3
from botocore.exceptions import ClientError

# Configure logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)

# Get region from environment
REGION = os.environ.get("REGION", "us-east-1")


def handler(event: Dict[str, Any], context: Any) -> Dict[str, Any]:
"""
List all available AgentCore Runtime agents.

Args:
event: API Gateway event
context: Lambda context

Returns:
API Gateway response with list of runtimes
"""
logger.info(f"Event: {json.dumps(event)}")

try:
# Initialize Bedrock Agent Runtime client
client = boto3.client("bedrock-agentcore-control", region_name=REGION)

# List all agent runtimes
response = client.list_agent_runtimes()

# Format the response
runtimes = []
for runtime in response.get("agentRuntimes", []):
runtime_name = runtime.get("agentRuntimeName", "Unnamed Runtime")
if runtime_name not in ("GenUGenericRuntime", "GenUAgentBuilderRuntime"):
runtimes.append({
"name": runtime_name,
"description": runtime.get("description", "No description available"),
"arn": runtime.get("agentRuntimeArn", ""),
"status": runtime.get("status", "UNKNOWN"),
"createdAt": runtime.get("createdAt").isoformat() if runtime.get("createdAt") else None,
"updatedAt": runtime.get("updatedAt").isoformat() if runtime.get("updatedAt") else None,
})

logger.info(f"Found {len(runtimes)} AgentCore runtimes")

return {
"statusCode": 200,
"headers": {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
"body": json.dumps({
"runtimes": runtimes,
"count": len(runtimes),
}),
}

except ClientError as e:
error_code = e.response.get("Error", {}).get("Code", "Unknown")
error_message = e.response.get("Error", {}).get("Message", str(e))

logger.error(f"AWS ClientError ({error_code}): {error_message}")

return {
"statusCode": 500,
"headers": {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
"body": json.dumps({
"error": "Failed to list AgentCore runtimes",
"message": error_message,
"code": error_code,
"runtimes": [],
"count": 0,
}),
}

except Exception as e:
logger.error(f"Unexpected error: {str(e)}", exc_info=True)

return {
"statusCode": 500,
"headers": {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
"body": json.dumps({
"error": "Failed to list AgentCore runtimes",
"message": str(e),
"runtimes": [],
"count": 0,
}),
}
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,7 @@ export const createAgent = async (
systemPrompt: content.systemPrompt,
modelId: content.modelId,
mcpServers: content.mcpServers,
subAgents: content.subAgents || [],
codeExecutionEnabled: content.codeExecutionEnabled ?? false,
tags: content.tags || [],
isPublic: content.isPublic ?? false,
Expand Down Expand Up @@ -391,14 +392,15 @@ export const updateAgent = async (
TableName: TABLE_NAME,
Key: { id: agent.id, dataType: agent.dataType },
UpdateExpression:
'set #name = :name, description = :description, systemPrompt = :systemPrompt, modelId = :modelId, mcpServers = :mcpServers, codeExecutionEnabled = :codeExecutionEnabled, tags = :tags, isPublic = :isPublic, updatedAt = :updatedAt, createdByEmail = :createdByEmail',
'set #name = :name, description = :description, systemPrompt = :systemPrompt, modelId = :modelId, mcpServers = :mcpServers, subAgents = :subAgents, codeExecutionEnabled = :codeExecutionEnabled, tags = :tags, isPublic = :isPublic, updatedAt = :updatedAt, createdByEmail = :createdByEmail',
ExpressionAttributeNames: { '#name': 'name' },
ExpressionAttributeValues: {
':name': content.name,
':description': content.description || '',
':systemPrompt': content.systemPrompt,
':modelId': content.modelId,
':mcpServers': content.mcpServers,
':subAgents': content.subAgents || [],
':codeExecutionEnabled': content.codeExecutionEnabled ?? false,
':tags': content.tags || [],
':isPublic': isNowPublic,
Expand All @@ -413,6 +415,7 @@ export const updateAgent = async (
...agent,
...content,
description: content.description || '',
subAgents: content.subAgents || [],
codeExecutionEnabled: content.codeExecutionEnabled ?? false,
tags: content.tags || [],
isPublic: isNowPublic,
Expand Down Expand Up @@ -635,6 +638,7 @@ export const listFavoriteAgents = async (
systemPrompt: '',
modelId: '',
mcpServers: [],
subAgents: [],
codeExecutionEnabled: false,
tags: [],
isPublic: false,
Expand Down
10 changes: 10 additions & 0 deletions packages/cdk/lambda/agentBuilder/services/agent-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ function convertToAgentConfiguration(
description: agent.description,
systemPrompt: agent.systemPrompt,
mcpServers: agent.mcpServers || [],
subAgents: agent.subAgents || [],
modelId: agent.modelId,
codeExecutionEnabled: agent.codeExecutionEnabled || false,
isPublic: agent.isPublic || false,
Expand All @@ -52,6 +53,9 @@ export async function createAgent(
// MCP server names (no sanitization needed for string array)
const mcpServerNames = request.mcpServers || [];

// Sub-agents (no sanitization needed, validated by schema)
const subAgents = request.subAgents || [];

// Get user email from Cognito
const userEmail = await getUserEmail(userId);

Expand All @@ -60,6 +64,7 @@ export async function createAgent(
description: (request.description || '').trim(),
systemPrompt: request.systemPrompt.trim(),
mcpServers: mcpServerNames,
subAgents: subAgents,
modelId: request.modelId,
codeExecutionEnabled: request.codeExecutionEnabled ?? false,
isPublic: request.isPublic ?? false,
Expand Down Expand Up @@ -108,6 +113,9 @@ export async function updateAgent(
// MCP server names
const mcpServerNames = request.mcpServers || existingAgent.mcpServers;

// Sub-agents
const subAgents = request.subAgents ?? existingAgent.subAgents ?? [];

// Get user email from Cognito if not provided in request
const userEmail = request.createdByEmail || (await getUserEmail(userId));

Expand All @@ -117,6 +125,7 @@ export async function updateAgent(
description: request.description?.trim() || existingAgent.description,
systemPrompt: request.systemPrompt?.trim() || existingAgent.systemPrompt,
mcpServers: mcpServerNames,
subAgents: subAgents,
modelId: request.modelId || existingAgent.modelId,
codeExecutionEnabled:
request.codeExecutionEnabled ??
Expand Down Expand Up @@ -298,6 +307,7 @@ export async function cloneAgent(
systemPrompt: sourceAgent.systemPrompt,
modelId: sourceAgent.modelId,
mcpServers: sourceAgent.mcpServers || [],
subAgents: sourceAgent.subAgents || [],
codeExecutionEnabled: sourceAgent.codeExecutionEnabled || false,
tags: sourceAgent.tags || [],
isPublic: false, // Cloned agents are private by default
Expand Down
Loading