Skip to content

Commit 5a790aa

Browse files
committed
feat: add AG-UI agent patterns and frontend support
- Add agui-langgraph-agent and agui-strands-agent patterns with AG-UI protocol - Add AG-UI streaming parser for frontend client - Add AGUI pattern comments and bump CDK dependencies in infra-cdk
1 parent 67c5b58 commit 5a790aa

20 files changed

Lines changed: 766 additions & 45 deletions

File tree

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim
5+
6+
WORKDIR /app
7+
8+
# Configure UV for container environment
9+
ENV UV_SYSTEM_PYTHON=1 \
10+
UV_COMPILE_BYTECODE=1 \
11+
DOCKER_CONTAINER=1 \
12+
OTEL_PYTHON_LOG_CORRELATION=true \
13+
PYTHONUNBUFFERED=1
14+
15+
# Copy pyproject.toml and shared packages for installation
16+
COPY pyproject.toml .
17+
COPY agentcore_gateway/ agentcore_gateway/
18+
COPY agentcore_tools/ agentcore_tools/
19+
20+
# Copy and install agent-specific requirements first
21+
COPY agent_patterns/agui-langgraph-agent/requirements.txt requirements.txt
22+
RUN uv pip install --no-cache -r requirements.txt && \
23+
uv pip install --no-cache aws-opentelemetry-distro==0.10.1
24+
25+
# Install FAST package with only core dependencies (no dev/agent optional deps)
26+
RUN uv pip install --no-cache -e . --no-deps && \
27+
uv pip install --no-cache requests>=2.31.0
28+
29+
# Create non-root user
30+
RUN useradd -m -u 1000 bedrock_agentcore
31+
USER bedrock_agentcore
32+
33+
EXPOSE 8080
34+
35+
# Copy agent code and tools
36+
COPY agent_patterns/agui-langgraph-agent/agent.py .
37+
COPY agent_patterns/agui-langgraph-agent/tools/ tools/
38+
COPY agent_patterns/utils/ utils/
39+
40+
# Healthcheck
41+
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
42+
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8080/ping', timeout=2)" || exit 1
43+
44+
# Start agent with OpenTelemetry instrumentation
45+
CMD ["opentelemetry-instrument", "python", "-m", "agent"]
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
"""AG-UI LangGraph agent with Gateway MCP tools, Memory, and Code Interpreter.
2+
3+
Uses copilotkit's LangGraphAGUIAgent to produce native AG-UI SSE events.
4+
AgentCore proxies these unchanged when deployed with --protocol AGUI.
5+
"""
6+
7+
import logging
8+
import os
9+
10+
from ag_ui.core import RunAgentInput, RunErrorEvent
11+
from bedrock_agentcore.runtime import BedrockAgentCoreApp, RequestContext
12+
from copilotkit import CopilotKitMiddleware, LangGraphAGUIAgent
13+
from langchain.agents import create_agent
14+
from langchain_aws import ChatBedrock
15+
from langgraph_checkpoint_aws import AgentCoreMemorySaver
16+
from tools.code_interpreter import LangGraphCodeInterpreterTools
17+
from tools.gateway import create_gateway_mcp_client
18+
from utils.auth import extract_user_id_from_context
19+
20+
logger = logging.getLogger(__name__)
21+
22+
app = BedrockAgentCoreApp()
23+
24+
SYSTEM_PROMPT = (
25+
"You are a helpful assistant with access to tools via the Gateway and Code Interpreter. "
26+
"When asked about your tools, list them and explain what they do."
27+
)
28+
29+
30+
def _build_model() -> ChatBedrock:
31+
return ChatBedrock(
32+
model_id="us.anthropic.claude-sonnet-4-5-20250929-v1:0",
33+
temperature=0.1,
34+
streaming=True,
35+
beta_use_converse_api=True,
36+
)
37+
38+
39+
def _create_checkpointer() -> AgentCoreMemorySaver:
40+
memory_id = os.environ.get("MEMORY_ID")
41+
if not memory_id:
42+
raise ValueError("MEMORY_ID environment variable is required")
43+
return AgentCoreMemorySaver(
44+
memory_id=memory_id,
45+
region_name=os.environ.get("AWS_DEFAULT_REGION", "us-east-1"),
46+
)
47+
48+
49+
async def create_langgraph_agent():
50+
"""Create a LangGraph agent with Gateway tools, Memory, and Code Interpreter."""
51+
mcp_client = await create_gateway_mcp_client()
52+
tools = await mcp_client.get_tools()
53+
54+
region = os.environ.get("AWS_DEFAULT_REGION", "us-east-1")
55+
code_tools = LangGraphCodeInterpreterTools(region)
56+
tools.append(code_tools.execute_python_securely)
57+
58+
return create_agent(
59+
model=_build_model(),
60+
tools=tools,
61+
checkpointer=_create_checkpointer(),
62+
middleware=[CopilotKitMiddleware()],
63+
system_prompt=SYSTEM_PROMPT,
64+
)
65+
66+
67+
class ActorAwareLangGraphAgent(LangGraphAGUIAgent):
68+
"""LangGraphAGUIAgent that creates the graph per-request with fresh tokens."""
69+
70+
async def run(self, input: RunAgentInput):
71+
self.graph = await create_langgraph_agent()
72+
async for event in super().run(input):
73+
yield event
74+
75+
76+
@app.entrypoint
77+
async def invocations(payload: dict, context: RequestContext):
78+
input_data = RunAgentInput.model_validate(payload)
79+
80+
user_id = extract_user_id_from_context(context)
81+
82+
agent = ActorAwareLangGraphAgent(
83+
name="agui_langgraph_agent",
84+
description="AG-UI LangGraph agent with Gateway MCP tools and Memory",
85+
graph=None,
86+
config={"configurable": {"actor_id": user_id}},
87+
)
88+
89+
try:
90+
async for event in agent.run(input_data):
91+
if event is not None:
92+
yield event.model_dump(mode="json", by_alias=True, exclude_none=True)
93+
except Exception as exc:
94+
logger.exception("Agent run failed")
95+
yield RunErrorEvent(
96+
message=str(exc) or type(exc).__name__,
97+
code=type(exc).__name__,
98+
).model_dump(mode="json", by_alias=True, exclude_none=True)
99+
100+
101+
if __name__ == "__main__":
102+
app.run()
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# LangGraph AG-UI agent dependencies
2+
copilotkit==0.1.83
3+
langchain==1.2.13
4+
langgraph==1.1.3
5+
langchain-aws==1.4.1
6+
langchain-mcp-adapters==0.2.2
7+
langgraph-checkpoint-aws==1.0.6
8+
ag-ui-protocol==0.1.14
9+
mcp==1.26.0
10+
bedrock-agentcore==1.4.7
11+
PyJWT[crypto]==2.12.1
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"""LangGraph-specific wrapper for Code Interpreter."""
2+
3+
from langchain_core.tools import tool
4+
5+
from agentcore_tools.code_interpreter.code_interpreter_tools import CodeInterpreterTools
6+
7+
8+
class LangGraphCodeInterpreterTools:
9+
"""LangGraph wrapper for Code Interpreter tools."""
10+
11+
def __init__(self, region: str):
12+
self.core_tools = CodeInterpreterTools(region)
13+
14+
def cleanup(self):
15+
"""Clean up code interpreter session."""
16+
self.core_tools.cleanup()
17+
18+
@property
19+
def execute_python_securely(self):
20+
"""Get the execute_python_securely tool function."""
21+
core = self.core_tools
22+
23+
@tool
24+
def execute_python_securely(code: str) -> str:
25+
"""Execute Python code in a secure AgentCore CodeInterpreter sandbox.
26+
27+
Args:
28+
code: Python code to execute
29+
30+
Returns:
31+
JSON string with execution result
32+
"""
33+
return core.execute_python_securely(code)
34+
35+
return execute_python_securely
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""AgentCore Gateway MCP client with OAuth2 authentication (async, for LangGraph)."""
2+
3+
import logging
4+
import os
5+
6+
from bedrock_agentcore.identity.auth import requires_access_token
7+
from langchain_mcp_adapters.client import MultiServerMCPClient
8+
from utils.ssm import get_ssm_parameter
9+
10+
logger = logging.getLogger(__name__)
11+
12+
13+
@requires_access_token(
14+
provider_name=os.environ["GATEWAY_CREDENTIAL_PROVIDER_NAME"],
15+
auth_flow="M2M",
16+
scopes=[],
17+
)
18+
async def _fetch_gateway_token(access_token: str) -> str:
19+
"""Fetch OAuth2 token for Gateway authentication.
20+
21+
The @requires_access_token decorator handles token retrieval and refresh.
22+
Async because it's awaited in create_gateway_mcp_client().
23+
"""
24+
return access_token
25+
26+
27+
async def create_gateway_mcp_client() -> MultiServerMCPClient:
28+
"""Create MCP client for AgentCore Gateway with OAuth2 authentication.
29+
30+
Fetches a fresh token per call (called per-request in agent entrypoint).
31+
"""
32+
stack_name = os.environ.get("STACK_NAME")
33+
if not stack_name:
34+
raise ValueError("STACK_NAME environment variable is required")
35+
if not stack_name.replace("-", "").replace("_", "").isalnum():
36+
raise ValueError("Invalid STACK_NAME format")
37+
38+
gateway_url = get_ssm_parameter(f"/{stack_name}/gateway_url")
39+
logger.info("[GATEWAY] URL: %s", gateway_url)
40+
41+
fresh_token = await _fetch_gateway_token()
42+
43+
return MultiServerMCPClient(
44+
{
45+
"gateway": {
46+
"transport": "streamable_http",
47+
"url": gateway_url,
48+
"headers": {"Authorization": f"Bearer {fresh_token}"},
49+
}
50+
}
51+
)
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim
5+
6+
WORKDIR /app
7+
8+
# Configure UV for container environment
9+
ENV UV_SYSTEM_PYTHON=1 \
10+
UV_COMPILE_BYTECODE=1 \
11+
DOCKER_CONTAINER=1 \
12+
OTEL_PYTHON_LOG_CORRELATION=true \
13+
PYTHONUNBUFFERED=1
14+
15+
# Copy pyproject.toml and shared packages for installation
16+
COPY pyproject.toml .
17+
COPY agentcore_gateway/ agentcore_gateway/
18+
COPY agentcore_tools/ agentcore_tools/
19+
20+
# Copy and install agent-specific requirements
21+
COPY agent_patterns/agui-strands-agent/requirements.txt requirements.txt
22+
RUN uv pip install --no-cache -r requirements.txt && \
23+
uv pip install --no-cache aws-opentelemetry-distro==0.10.1
24+
25+
# Install FAST package with only core dependencies (no dev/agent optional deps)
26+
RUN uv pip install --no-cache -e . --no-deps && \
27+
uv pip install --no-cache requests>=2.31.0
28+
29+
# Create non-root user
30+
RUN useradd -m -u 1000 bedrock_agentcore
31+
USER bedrock_agentcore
32+
33+
EXPOSE 8080
34+
35+
# Copy agent code and tools
36+
COPY agent_patterns/agui-strands-agent/agent.py .
37+
COPY agent_patterns/agui-strands-agent/tools/ tools/
38+
COPY agent_patterns/utils/ utils/
39+
40+
# Healthcheck
41+
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
42+
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8080/ping', timeout=2)" || exit 1
43+
44+
# Start agent with OpenTelemetry instrumentation
45+
CMD ["opentelemetry-instrument", "python", "-m", "agent"]
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
"""AG-UI Strands agent with Gateway MCP tools, Memory, and Code Interpreter.
2+
3+
Uses ag-ui-strands to produce native AG-UI SSE events.
4+
AgentCore proxies these unchanged when deployed with --protocol AGUI.
5+
"""
6+
7+
import logging
8+
import os
9+
10+
from ag_ui.core import RunAgentInput, RunErrorEvent
11+
from ag_ui_strands import StrandsAgent
12+
from bedrock_agentcore.memory.integrations.strands.config import AgentCoreMemoryConfig
13+
from bedrock_agentcore.memory.integrations.strands.session_manager import (
14+
AgentCoreMemorySessionManager,
15+
)
16+
from bedrock_agentcore.runtime import BedrockAgentCoreApp, RequestContext
17+
from strands import Agent
18+
from strands.models import BedrockModel
19+
from tools.code_interpreter import StrandsCodeInterpreterTools
20+
from tools.gateway import create_gateway_mcp_client
21+
from utils.auth import extract_user_id_from_context
22+
23+
logger = logging.getLogger(__name__)
24+
25+
app = BedrockAgentCoreApp()
26+
27+
SYSTEM_PROMPT = (
28+
"You are a helpful assistant with access to tools via the Gateway and Code Interpreter. "
29+
"When asked about your tools, list them and explain what they do."
30+
)
31+
32+
33+
def _create_session_manager(
34+
user_id: str, session_id: str
35+
) -> AgentCoreMemorySessionManager:
36+
memory_id = os.environ.get("MEMORY_ID")
37+
if not memory_id:
38+
raise ValueError("MEMORY_ID environment variable is required")
39+
config = AgentCoreMemoryConfig(
40+
memory_id=memory_id, session_id=session_id, actor_id=user_id
41+
)
42+
return AgentCoreMemorySessionManager(
43+
agentcore_memory_config=config,
44+
region_name=os.environ.get("AWS_DEFAULT_REGION", "us-east-1"),
45+
)
46+
47+
48+
def create_strands_agent(user_id: str, session_id: str) -> StrandsAgent:
49+
"""Create AG-UI Strands agent with Gateway tools, Memory, and Code Interpreter."""
50+
bedrock_model = BedrockModel(
51+
model_id="us.anthropic.claude-sonnet-4-5-20250929-v1:0", temperature=0.1
52+
)
53+
54+
session_manager = _create_session_manager(user_id, session_id=session_id)
55+
56+
gateway_client = create_gateway_mcp_client()
57+
tools = [gateway_client]
58+
59+
region = os.environ.get("AWS_DEFAULT_REGION", "us-east-1")
60+
code_tools = StrandsCodeInterpreterTools(region)
61+
tools.append(code_tools.execute_python_securely)
62+
63+
strands_agent = Agent(
64+
name="strands_agent",
65+
system_prompt=SYSTEM_PROMPT,
66+
tools=tools,
67+
model=bedrock_model,
68+
session_manager=session_manager,
69+
)
70+
71+
return StrandsAgent(
72+
agent=strands_agent,
73+
name="agui_strands_agent",
74+
description="AG-UI Strands agent with Gateway MCP tools and Code Interpreter",
75+
)
76+
77+
78+
@app.entrypoint
79+
async def invocations(payload: dict, context: RequestContext):
80+
input_data = RunAgentInput.model_validate(payload)
81+
82+
user_id = extract_user_id_from_context(context)
83+
agent = create_strands_agent(user_id, session_id=input_data.thread_id)
84+
85+
try:
86+
async for event in agent.run(input_data):
87+
if event is not None:
88+
yield event.model_dump(mode="json", by_alias=True, exclude_none=True)
89+
except Exception as exc:
90+
logger.exception("Agent run failed")
91+
yield RunErrorEvent(
92+
message=str(exc) or type(exc).__name__,
93+
code=type(exc).__name__,
94+
).model_dump(mode="json", by_alias=True, exclude_none=True)
95+
96+
97+
if __name__ == "__main__":
98+
app.run()

0 commit comments

Comments
 (0)