chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:58:18 +08:00
commit 6d5d58c1a9
18293 changed files with 3502153 additions and 0 deletions
@@ -0,0 +1,36 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim
WORKDIR /app
# Configure UV for container environment
ENV UV_SYSTEM_PYTHON=1 \
UV_COMPILE_BYTECODE=1 \
DOCKER_CONTAINER=1 \
OTEL_PYTHON_LOG_CORRELATION=true \
PYTHONUNBUFFERED=1
# Copy and install agent-specific requirements first
COPY agents/strands-single-agent/requirements.txt requirements.txt
RUN uv pip install --no-cache -r requirements.txt && \
uv pip install --no-cache aws-opentelemetry-distro==0.16.0
# Create non-root user
RUN useradd -m -u 1000 bedrock_agentcore
USER bedrock_agentcore
EXPOSE 8080
# Copy agent code and shared utilities
COPY agents/strands-single-agent/strands_agent.py .
COPY agents/strands-single-agent/tools/ tools/
COPY agents/utils/ utils/
# Healthcheck using Python (no extra dependencies needed)
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8080/ping', timeout=2)" || exit 1
# Start agent with OpenTelemetry instrumentation
CMD ["opentelemetry-instrument", "python", "-m", "strands_agent"]
@@ -0,0 +1,8 @@
# Strands agent dependencies with pinned versions
strands-agents==1.24.0
mcp==1.26.0
bedrock-agentcore[strands-agents]==1.2.0
PyJWT[crypto]>=2.10.1
ag-ui-protocol>=0.1.10
ag-ui-strands==0.1.2
fastapi>=0.115.12
@@ -0,0 +1,229 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
import json
import logging
import os
import traceback
from ag_ui.core import RunAgentInput, RunErrorEvent
from ag_ui_strands import (
StrandsAgent,
StrandsAgentConfig,
ToolBehavior,
PredictStateMapping,
)
from ag_ui_strands.config import ToolCallContext
from bedrock_agentcore.memory.integrations.strands.config import AgentCoreMemoryConfig
from bedrock_agentcore.memory.integrations.strands.session_manager import (
AgentCoreMemorySessionManager,
)
from bedrock_agentcore.runtime import BedrockAgentCoreApp, RequestContext
from mcp.client.streamable_http import streamablehttp_client
from strands import Agent
from strands.models import BedrockModel
from strands.tools.mcp import MCPClient
from tools.query_data import query_data
from tools.todos import manage_todos
from utils.auth import extract_user_id_from_context, get_gateway_access_token
from utils.ssm import get_ssm_parameter
app = BedrockAgentCoreApp()
logger = logging.getLogger(__name__)
ACTOR_ID_KEYS = ("actor_id", "actorId", "user_id", "userId", "sub")
SYSTEM_PROMPT = """You are a helpful assistant with access to tools via the Gateway and built-in data tools.
When demonstrating charts, always call the query_data tool first to fetch data from the database before calling any chart tool.
When managing todos, use manage_todos to update the list.
When asked about your tools, list them and explain what they do."""
BEDROCK_MODEL = BedrockModel(
model_id="us.anthropic.claude-sonnet-4-5-20250929-v1:0",
temperature=0.1,
)
def create_gateway_mcp_client() -> MCPClient:
"""
Create MCP client for AgentCore Gateway with OAuth2 authentication.
Calls get_gateway_access_token() inside the lambda factory to ensure a fresh
token is fetched on every MCP reconnection (avoids the closure trap).
"""
stack_name = os.environ.get("STACK_NAME")
if not stack_name:
raise ValueError("STACK_NAME environment variable is required")
if not stack_name.replace("-", "").replace("_", "").isalnum():
raise ValueError("Invalid STACK_NAME format")
gateway_url = get_ssm_parameter(f"/{stack_name}/gateway_url")
return MCPClient(
lambda: streamablehttp_client(
url=gateway_url,
headers={"Authorization": f"Bearer {get_gateway_access_token()}"},
),
prefix="gateway",
)
def create_strands_agent(actor_id: str, session_id: str) -> StrandsAgent:
"""
Create a StrandsAgent wrapping a Strands SDK agent with AgentCore memory,
Gateway MCP tools, and CopilotKit-compatible AG-UI configuration.
Memory: AgentCoreMemorySessionManager provides cloud-persistent conversation
history keyed by actor_id, matching the AgentCoreMemorySaver approach used
in the LangGraph pattern.
"""
memory_id = os.environ.get("MEMORY_ID")
if not memory_id:
raise ValueError("MEMORY_ID environment variable is required")
agentcore_memory_config = AgentCoreMemoryConfig(
memory_id=memory_id, session_id=session_id, actor_id=actor_id
)
session_manager = AgentCoreMemorySessionManager(
agentcore_memory_config=agentcore_memory_config,
region_name=os.environ.get("AWS_DEFAULT_REGION", "us-east-1"),
)
gateway_client = create_gateway_mcp_client()
# Inject current todos into the system prompt so the agent always knows
# the latest todo state without needing a separate get_todos tool.
def state_context_builder(state: dict) -> str:
todos = state.get("todos", [])
if todos:
return f"\nCurrent todos:\n{json.dumps(todos, indent=2)}"
return ""
# When manage_todos is called, emit a StateSnapshotEvent with the new todos
# so the frontend updates immediately (before the tool result arrives).
async def todos_state_from_args(ctx: ToolCallContext) -> dict:
todos = (ctx.tool_input or {}).get("todos", [])
return {"todos": todos}
# Frontend tools (generative UI / canvas controls): let the agent continue after
# calling them so it generates a proper conclusion text. The run then finishes
# naturally and ag_ui_strands sends a MessagesSnapshotEvent that preserves the
# chat history. Without continue_after_frontend_call the stream halts and
# CopilotKit v2 clears the UI because no snapshot was sent.
frontend_tool_behavior = ToolBehavior(
continue_after_frontend_call=False,
skip_messages_snapshot=False,
)
config = StrandsAgentConfig(
tool_behaviors={
"manage_todos": ToolBehavior(
state_from_args=todos_state_from_args,
predict_state=[
PredictStateMapping(
state_key="todos",
tool="manage_todos",
tool_argument="todos",
)
],
),
"pieChart": frontend_tool_behavior,
"barChart": frontend_tool_behavior,
"toggleTheme": frontend_tool_behavior,
"scheduleTime": frontend_tool_behavior,
"enableAppMode": frontend_tool_behavior,
"enableChatMode": frontend_tool_behavior,
},
state_context_builder=state_context_builder,
)
# Build the underlying Strands agent with persistent memory and tools.
core_agent = Agent(
name="FASTAgent",
system_prompt=SYSTEM_PROMPT,
tools=[gateway_client, query_data, manage_todos],
model=BEDROCK_MODEL,
session_manager=session_manager,
record_direct_tool_call=True,
trace_attributes={
"user.id": actor_id,
"session.id": session_id,
},
)
strands_agent = StrandsAgent(
agent=core_agent,
name="FASTAgent",
description="FAST Strands agent with CopilotKit generative UI support",
config=config,
)
# Pre-seed the per-thread agent cache so StrandsAgent.run() uses our
# core_agent (which has AgentCoreMemorySessionManager) rather than creating
# a new instance without it.
strands_agent._agents_by_thread[session_id] = core_agent
return strands_agent
@app.entrypoint
async def invocations(payload: dict, context: RequestContext):
"""
Main entrypoint for the Strands agent using AG-UI protocol.
Accepts RunAgentInput payloads from the CopilotKit Lambda Runtime,
streams AG-UI events back, and supports generative UI, shared state
(todos), and human-in-the-loop interactions via CopilotKit.
"""
input_data = RunAgentInput.model_validate(payload)
# Extract actor identity securely from the validated JWT token.
try:
actor_id = extract_user_id_from_context(context)
except ValueError:
# Fall back to forwarded props if JWT extraction fails (e.g. local dev).
forwarded = (
input_data.forwarded_props
if isinstance(input_data.forwarded_props, dict)
else {}
)
actor_id = next(
(forwarded[k] for k in ACTOR_ID_KEYS if k in forwarded and forwarded[k]),
None,
)
if not actor_id:
raise ValueError(
"Missing actor identity. Provide forwardedProps.actor_id/user_id "
"or include sub claim in the bearer token."
)
# Use thread_id from the request (set by CopilotKit runtime) or fall back
# to actor_id so each user gets their own persistent conversation thread.
session_id = input_data.thread_id or actor_id
# Ensure thread_id in the payload matches so StrandsAgent uses our pre-seeded agent.
input_data = input_data.model_copy(update={"thread_id": session_id})
try:
strands_agent = create_strands_agent(actor_id, session_id)
async for event in strands_agent.run(input_data):
if event is not None:
yield event.model_dump(mode="json", by_alias=True, exclude_none=True)
except Exception as exc:
logger.exception("Agent run failed")
yield RunErrorEvent(
message=str(exc) or type(exc).__name__,
code=type(exc).__name__,
).model_dump(mode="json", by_alias=True, exclude_none=True)
if __name__ == "__main__":
app.run()
@@ -0,0 +1,4 @@
from .query_data import query_data
from .todos import manage_todos, Todo
__all__ = ["query_data", "manage_todos", "Todo"]
@@ -0,0 +1,16 @@
date,category,amount,type
2026-01-05,Food,42.50,expense
2026-01-10,Transport,15.00,expense
2026-01-15,Salary,3500.00,income
2026-01-20,Entertainment,80.00,expense
2026-01-25,Utilities,120.00,expense
2026-02-03,Food,55.20,expense
2026-02-08,Freelance,800.00,income
2026-02-14,Dining,65.00,expense
2026-02-20,Transport,22.50,expense
2026-02-28,Salary,3500.00,income
2026-03-05,Groceries,95.40,expense
2026-03-10,Gym,40.00,expense
2026-03-15,Salary,3500.00,income
2026-03-18,Coffee,18.75,expense
2026-03-22,Books,35.00,expense
1 date category amount type
2 2026-01-05 Food 42.50 expense
3 2026-01-10 Transport 15.00 expense
4 2026-01-15 Salary 3500.00 income
5 2026-01-20 Entertainment 80.00 expense
6 2026-01-25 Utilities 120.00 expense
7 2026-02-03 Food 55.20 expense
8 2026-02-08 Freelance 800.00 income
9 2026-02-14 Dining 65.00 expense
10 2026-02-20 Transport 22.50 expense
11 2026-02-28 Salary 3500.00 income
12 2026-03-05 Groceries 95.40 expense
13 2026-03-10 Gym 40.00 expense
14 2026-03-15 Salary 3500.00 income
15 2026-03-18 Coffee 18.75 expense
16 2026-03-22 Books 35.00 expense
@@ -0,0 +1,21 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
import csv
import os
from strands import tool
@tool
def query_data(query: str) -> str:
"""
Query financial data from the database. Use this tool to fetch data before
rendering any charts. Returns CSV-formatted data relevant to the query.
"""
db_path = os.path.join(os.path.dirname(__file__), "db.csv")
try:
with open(db_path, "r") as f:
content = f.read()
return content
except FileNotFoundError:
return "No data available."
@@ -0,0 +1,23 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
from typing import Literal, TypedDict
from strands import tool
class Todo(TypedDict):
id: str
title: str
description: str
emoji: str
status: Literal["pending", "completed"]
@tool
def manage_todos(todos: list) -> str:
"""
Manage the current todos. Replaces the entire todo list.
Each todo should have: id (str), title (str), description (str), emoji (str), status ('pending' or 'completed').
"""
return "Todos updated successfully"