Triển khai và cấu hình Story agent
Nhiệm vụ 1: Triển khai Story Agent
Phần tiêu đề “Nhiệm vụ 1: Triển khai Story Agent”Story Agent là một agent Strands được tạo với --protocol=AG-UI trong Module 1, để UI có thể stream từ nó qua Agent-User Interaction protocol thông qua CopilotKit. Nó sử dụng Inventory MCP Server để quản lý các vật phẩm của người chơi, và S3SessionManager tích hợp sẵn của Strands để lưu trữ lịch sử hội thoại vào sessions bucket mà chúng ta đã cung cấp trong Module 2.
Triển khai Agent
Phần tiêu đề “Triển khai Agent”Cập nhật các file sau trong packages/story/dungeon_adventure_story/agent:
import loggingimport osfrom functools import cachefrom typing import Any, cast
from ag_ui.core import RunAgentInputfrom ag_ui_strands import StrandsAgent, StrandsAgentConfig, create_strands_appfrom aws_lambda_powertools.utilities import parametersfrom strands.session import FileSessionManager, S3SessionManager, SessionManager
from .agent import get_agent
logging.basicConfig(level=logging.INFO)
@cachedef _resolve_sessions_bucket() -> str: """Read the conversation-history bucket name from runtime config.
Resolved lazily (and memoised) so `fastapi dev` can import this module before ``RUNTIME_CONFIG_APP_ID`` is in the environment. """ application = os.environ.get("RUNTIME_CONFIG_APP_ID") if not application: raise RuntimeError("RUNTIME_CONFIG_APP_ID is not set — cannot resolve the StorySessions bucket.") provider = parameters.AppConfigProvider(environment="default", application=application) buckets = cast(dict[str, Any], provider.get("buckets", transform="json")) return buckets["StorySessions"]["bucketName"]
def _session_manager_provider(input_data: RunAgentInput) -> SessionManager: """Create a session manager keyed by the AG-UI thread_id.
- In AgentCore (and `agent-serve`), persist to the shared ``StorySessions`` S3 bucket — the same bucket the Game API reads from to rebuild conversation history on revisit. - In `agent-serve-local` (`SERVE_LOCAL=true`), persist to a temp directory so the agent can run fully offline against the local MCP server without any AWS calls. """ session_id = input_data.thread_id or "default" if os.environ.get("SERVE_LOCAL") == "true": return FileSessionManager(session_id=session_id, storage_dir="/tmp/strands-sessions") return S3SessionManager(session_id=session_id, bucket=_resolve_sessions_bucket())
# The template Agent is cloned per thread_id by ``StrandsAgent`` — we plug in# a ``session_manager_provider`` so each thread gets its own session manager# and conversation history is replayed on subsequent turns and survives agent# restarts._agent_ctx = get_agent()_agent = _agent_ctx.__enter__()
agui_agent = StrandsAgent( agent=_agent, name="StoryAgent", description="A Strands Agent exposed via the AG-UI protocol.", config=StrandsAgentConfig(session_manager_provider=_session_manager_provider),)
app = create_strands_app(agui_agent, path="/invocations")import loggingimport osfrom functools import cachefrom typing import Any, cast
from ag_ui_strands import StrandsAgent, create_strands_appfrom ag_ui.core import RunAgentInputfrom ag_ui_strands import StrandsAgent, StrandsAgentConfig, create_strands_appfrom aws_lambda_powertools.utilities import parametersfrom strands.session import FileSessionManager, S3SessionManager, SessionManager
from .agent import get_agent
logging.basicConfig(level=logging.INFO)
# Create AG-UI agent wrapper
@cachedef _resolve_sessions_bucket() -> str: """Read the conversation-history bucket name from runtime config.
Resolved lazily (and memoised) so `fastapi dev` can import this module before ``RUNTIME_CONFIG_APP_ID`` is in the environment. """ application = os.environ.get("RUNTIME_CONFIG_APP_ID") if not application: raise RuntimeError("RUNTIME_CONFIG_APP_ID is not set — cannot resolve the StorySessions bucket.") provider = parameters.AppConfigProvider(environment="default", application=application) buckets = cast(dict[str, Any], provider.get("buckets", transform="json")) return buckets["StorySessions"]["bucketName"]
def _session_manager_provider(input_data: RunAgentInput) -> SessionManager: """Create a session manager keyed by the AG-UI thread_id.
- In AgentCore (and `agent-serve`), persist to the shared ``StorySessions`` S3 bucket — the same bucket the Game API reads from to rebuild conversation history on revisit. - In `agent-serve-local` (`SERVE_LOCAL=true`), persist to a temp directory so the agent can run fully offline against the local MCP server without any AWS calls. """ session_id = input_data.thread_id or "default" if os.environ.get("SERVE_LOCAL") == "true": return FileSessionManager(session_id=session_id, storage_dir="/tmp/strands-sessions") return S3SessionManager(session_id=session_id, bucket=_resolve_sessions_bucket())
# The template Agent is cloned per thread_id by ``StrandsAgent`` — we plug in# a ``session_manager_provider`` so each thread gets its own session manager# and conversation history is replayed on subsequent turns and survives agent# restarts._agent_ctx = get_agent()_agent = _agent_ctx.__enter__()
agui_agent = StrandsAgent( agent=_agent, name="StoryAgent", description="A Strands Agent exposed via the AG-UI protocol.", config=StrandsAgentConfig(session_manager_provider=_session_manager_provider),)
# Create FastAPI app with AG-UI endpoint and health checkapp = create_strands_app(agui_agent, path="/invocations")from contextlib import contextmanager
from dungeon_adventure_agent_connection import InventoryMcpServerClientfrom strands import Agent
@contextmanagerdef get_agent(): inventory_mcp_server = InventoryMcpServerClient.create() with ( inventory_mcp_server, ): yield Agent( system_prompt="""You are running a text adventure game for a lone adventurer. When a new storybegins, the first user message will tell you the player's name and the genre(one of 'medieval', 'zombie', 'superhero'). Greet the player by name, set thescene in the chosen genre, and populate their inventory with a few startingitems. On subsequent turns, advance the story in response to the player'sactions and keep item state in sync with the narrative.Use the tools to manage the player's inventory as items are obtained or lost.When adding, removing or updating items in the inventory, always list items to check the current state,and be careful to match item names exactly. Item names in the inventory must be Title Case.Ensure you specify a suitable emoji when adding items if available.Items should be a key part of the narrative.Keep responses under 100 words.""", tools=[*inventory_mcp_server.list_tools_sync()], )from contextlib import contextmanager
from dungeon_adventure_agent_connection import InventoryMcpServerClientfrom strands import Agent, toolfrom strands_tools import current_timefrom strands import Agent
@tooldef subtract(a: int, b: int) -> int: return a - b
@contextmanagerdef get_agent(): inventory_mcp_server = InventoryMcpServerClient.create() with ( inventory_mcp_server, ): yield Agent( name="StoryAgent", description="StoryAgent Strands Agent", system_prompt="""You are a mathematical wizard.Use your tools for mathematical tasks.Refer to tools as your 'spellbook'.You are running a text adventure game for a lone adventurer. When a new storybegins, the first user message will tell you the player's name and the genre(one of 'medieval', 'zombie', 'superhero'). Greet the player by name, set thescene in the chosen genre, and populate their inventory with a few startingitems. On subsequent turns, advance the story in response to the player'sactions and keep item state in sync with the narrative.Use the tools to manage the player's inventory as items are obtained or lost.When adding, removing or updating items in the inventory, always list items to check the current state,and be careful to match item names exactly. Item names in the inventory must be Title Case.Ensure you specify a suitable emoji when adding items if available.Items should be a key part of the narrative.Keep responses under 100 words.""", tools=[subtract, current_time, *inventory_mcp_server.list_tools_sync()], tools=[*inventory_mcp_server.list_tools_sync()], )Các thay đổi là:
main.pythêm mộtsession_manager_providertạo ra mộtS3SessionManagercho mỗithread_idkhi được triển khai, và quay về sử dụngFileSessionManagertrên đĩa tại/tmp/strands-sessionskhi chạy dướiagent-serve-local(SERVE_LOCAL=true). Khi triển khai, S3 bucket là cùng một bucket màqueryActionscủa Game API đọc từ đó, do đó trình duyệt có thể xây dựng lại bản ghi khi truy cập lại; ở cục bộ, agent chạy hoàn toàn offline với local MCP server mà không có bất kỳ lời gọi AWS nào.agent.pyloại bỏ sample toolsubtractvà thay thế system prompt bằng một prompt dungeon-master mời tin nhắn đầu tiên của người dùng nêu tên người chơi và thể loại, đồng thời sử dụng các tool của Inventory MCP Server.
Nhiệm vụ 2: Triển khai và kiểm thử
Phần tiêu đề “Nhiệm vụ 2: Triển khai và kiểm thử”Build code
Phần tiêu đề “Build code”Để build code:
pnpm buildyarn buildnpm run buildbun buildTriển khai ứng dụng của bạn
Phần tiêu đề “Triển khai ứng dụng của bạn”Để triển khai ứng dụng của bạn, chạy lệnh sau:
pnpm nx deploy infra "dungeon-adventure-infra-sandbox/*"yarn nx deploy infra "dungeon-adventure-infra-sandbox/*"npx nx deploy infra "dungeon-adventure-infra-sandbox/*"bunx nx deploy infra "dungeon-adventure-infra-sandbox/*"Quá trình triển khai này sẽ mất khoảng 2 phút để hoàn thành.
Khi quá trình triển khai hoàn tất, bạn sẽ thấy các output tương tự như sau (một số giá trị đã được ẩn):
dungeon-adventure-infra-sandbox-Applicationdungeon-adventure-infra-sandbox-Application: deploying... [2/2]
✅ dungeon-adventure-infra-sandbox-Application
✨ Deployment time: 354s
Outputs:dungeon-adventure-infra-sandbox-Application.ElectroDbTableTableNameXXX = dungeon-adventure-infra-sandbox-Application-ElectroDbTableXXX-YYYdungeon-adventure-infra-sandbox-Application.GameApiEndpointXXX = https://xxx.execute-api.region.amazonaws.com/prod/dungeon-adventure-infra-sandbox-Application.GameUIDistributionDomainNameXXX = xxx.cloudfront.netdungeon-adventure-infra-sandbox-Application.InventoryMcpArn = arn:aws:bedrock-agentcore:region:xxxxxxx:runtime/dungeonadventureventoryMcpServerXXXX-YYYYdungeon-adventure-infra-sandbox-Application.RuntimeConfigApplicationId = xxxxdungeon-adventure-infra-sandbox-Application.StoryAgentArn = arn:aws:bedrock-agentcore:region:xxxxxxx:runtime/dungeonadventurecationStoryAgentXXXX-YYYYdungeon-adventure-infra-sandbox-Application.UserIdentityUserIdentityIdentityPoolIdXXX = region:xxxdungeon-adventure-infra-sandbox-Application.UserIdentityUserIdentityUserPoolIdXXX = region_xxxKiểm thử Agent của bạn
Phần tiêu đề “Kiểm thử Agent của bạn”Bạn có thể kiểm thử Agent của mình bằng một trong hai cách:
- Khởi động một instance cục bộ của Agent server và trò chuyện với nó thông qua target
agent-chatđược tạo ra, hoặc - Gọi API đã triển khai bằng curl với JWT token.
Mở một REPL tương tác với bản sao được phục vụ cục bộ của AG-UI agent bằng cách sử dụng target agent-chat được tạo ra:
pnpm nx agent-chat storyyarn nx agent-chat storynpx nx agent-chat storybunx nx agent-chat storyagent-chat có dependsOn: ['agent-serve-local'], do đó không cần khởi động agent server riêng biệt — Nx sẽ spawn agent-serve-local (lần lượt khởi động Inventory MCP server cục bộ), sau đó khởi chạy agent-chat-cli AG-UI REPL với http://localhost:8081/invocations. Tin nhắn đầu tiên của bạn nên cho agent biết tên anh hùng của bạn và thể loại (ví dụ: My name is Alice. Start my zombie adventure.) và câu chuyện sẽ được stream trả về.
Để kiểm thử agent đã triển khai, bạn cần xác thực với Cognito và lấy JWT token. Đầu tiên, thiết lập các biến môi trường của bạn:
# Đặt Cognito User Pool ID và Client ID từ các output của CDKexport POOL_ID="<UserPoolId từ CDK outputs>"export CLIENT_ID="<UserPoolClientId từ CDK outputs>"export REGION="<your-region>"Tạo một test user và lấy authentication token:
# Vô hiệu hóa MFA để đơn giản hóa việc tạo useraws cognito-idp set-user-pool-mfa-config \ --mfa-configuration OFF \ --user-pool-id $POOL_ID
# Tạo Useraws cognito-idp admin-create-user \ --user-pool-id $POOL_ID \ --username "test" \ --temporary-password "TempPass123-" \ --region $REGION \ --message-action SUPPRESS > /dev/null
# Đặt Permanent Password (thay thế bằng một mật khẩu an toàn hơn!)aws cognito-idp admin-set-user-password \ --user-pool-id $POOL_ID \ --username "test" \ --password "PermanentPass123-" \ --region $REGION \ --permanent > /dev/null
# Xác thực User và lấy Access Tokenexport BEARER_TOKEN=$(aws cognito-idp initiate-auth \ --client-id "$CLIENT_ID" \ --auth-flow USER_PASSWORD_AUTH \ --auth-parameters USERNAME='test',PASSWORD='PermanentPass123-' \ --region $REGION \ --query "AuthenticationResult.AccessToken" \ --output text)Gọi agent đã triển khai bằng URL runtime của Bedrock AgentCore:
# Đặt Story Agent ARN từ CDK outputsexport AGENT_ARN="<StoryAgentArn từ CDK outputs>"
# URL-encode ARNexport ENCODED_ARN=$(echo $AGENT_ARN | sed 's/:/%3A/g' | sed 's/\//%2F/g')
# Xây dựng invocation URLexport AGENT_URL="https://bedrock-agentcore.$REGION.amazonaws.com/runtimes/$ENCODED_ARN/invocations?qualifier=DEFAULT"
# Gọi agent với payload AG-UI RunAgentInputcurl -N -X POST "$AGENT_URL" \ -H "authorization: Bearer $BEARER_TOKEN" \ -H "Content-Type: application/json" \ -H "X-Amzn-Bedrock-AgentCore-Runtime-Session-Id: Alice-zombie00000000000000000000000" \ -d '{ "threadId": "Alice-zombie00000000000000000000000", "runId": "run-1", "messages": [{ "id": "m1", "role": "user", "content": "My name is Alice. Start my zombie adventure." }], "tools": [], "context": [], "state": {}, "forwardedProps": {} }'Nếu lệnh chạy thành công, bạn sẽ bắt đầu thấy câu chuyện được stream trả về dưới dạng một AG-UI event stream (Server-Sent Events) — các chunk RUN_STARTED / TEXT_MESSAGE_START / TEXT_MESSAGE_CONTENT bao bọc các token câu chuyện thực tế:
data: {"type":"RUN_STARTED","threadId":"Alice-zombie00000000000000000000000",...}data: {"type":"TEXT_MESSAGE_START","messageId":"...","role":"assistant"}data: {"type":"TEXT_MESSAGE_CONTENT","messageId":"...","delta":"Greetings, Alice. "}data: {"type":"TEXT_MESSAGE_CONTENT","messageId":"...","delta":"The moans grow louder beyond the barricaded door..."}Chúc mừng. Bạn đã xây dựng và triển khai Strands Agent đầu tiên của mình trên Bedrock AgentCore Runtime! 🎉🎉🎉