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 osimport uuidfrom 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 dungeon_adventure_agent_connection import session_id_contextfrom fastapi import Requestfrom starlette.middleware.base import BaseHTTPMiddlewarefrom strands.session import FileSessionManager, S3SessionManager, SessionManager
from .agent import get_agent
logging.basicConfig(level=logging.INFO)
SESSION_ID_HEADER = "x-amzn-bedrock-agentcore-runtime-session-id"
@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-dev` (`LOCAL_DEV=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("LOCAL_DEV") == "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),)
class _SessionIdMiddleware(BaseHTTPMiddleware): """Bind the session ID for this request so downstream MCP / A2A clients forward it on outbound calls."""
async def dispatch(self, request: Request, call_next): session_id = request.headers.get(SESSION_ID_HEADER) or str(uuid.uuid4()) with session_id_context(session_id): return await call_next(request)
app = create_strands_app(agui_agent, path="/invocations")app.add_middleware(_SessionIdMiddleware)import loggingimport osimport uuidfrom 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 dungeon_adventure_agent_connection import session_id_contextfrom fastapi import Requestfrom starlette.middleware.base import BaseHTTPMiddlewarefrom strands.session import FileSessionManager, S3SessionManager, SessionManager
from .agent import get_agent
logging.basicConfig(level=logging.INFO)
SESSION_ID_HEADER = "x-amzn-bedrock-agentcore-runtime-session-id"
# 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-dev` (`LOCAL_DEV=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("LOCAL_DEV") == "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),)
class _SessionIdMiddleware(BaseHTTPMiddleware): """Bind the session ID for this request so downstream MCP / A2A clients forward it on outbound calls."""
async def dispatch(self, request: Request, call_next): session_id = request.headers.get(SESSION_ID_HEADER) or str(uuid.uuid4()) with session_id_context(session_id): return await call_next(request)
# Create FastAPI app with AG-UI endpoint and health checkapp = create_strands_app(agui_agent, path="/invocations")app.add_middleware(_SessionIdMiddleware)from contextlib import contextmanager
from dungeon_adventure_agent_connection import InventoryMcpServerClientStrands, log_model_errorsfrom strands import Agent
@contextmanagerdef get_agent(): inventory_mcp_server = InventoryMcpServerClientStrands.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.Item names in the inventory must be Title Case — match them exactly.Only use list-inventory-items if you are unsure of the exact item name before modifying it.IMPORTANT: Only call ONE tool per response turn. Never batch multiple tool calls in a single turn.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()], hooks=[log_model_errors], )from contextlib import contextmanager
from dungeon_adventure_agent_connection import InventoryMcpServerClientStrands, log_model_errorsfrom 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 = InventoryMcpServerClientStrands.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.Item names in the inventory must be Title Case — match them exactly.Only use list-inventory-items if you are unsure of the exact item name before modifying it.IMPORTANT: Only call ONE tool per response turn. Never batch multiple tool calls in a single turn.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()], hooks=[log_model_errors], )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-dev(LOCAL_DEV=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 lưu trữ các session vào đĩa và giao tiếp với local MCP server, mà không cần triển khai.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: Kiểm thử Agent của bạn ở cục bộ
Phần tiêu đề “Nhiệm vụ 2: Kiểm thử Agent của bạn ở cục bộ”Build code
Phần tiêu đề “Build code”Để build code:
pnpm buildyarn buildnpm run buildbun buildTrò chuyện với Agent của bạn
Phần tiêu đề “Trò chuyện với Agent của bạn”Target agent-chat được tạo ra sẽ mở một REPL tương tác với agent của bạn. Nó chạy độc lập và kết nối với agent đang chạy cục bộ của bạn, vì vậy trước tiên hãy khởi động local server của agent trong một terminal:
pnpm nx agent-dev storyyarn nx agent-dev storynpx nx agent-dev storybunx nx agent-dev storySau đó, trong terminal thứ hai, khởi động chat:
pnpm nx agent-chat storyyarn nx agent-chat storynpx nx agent-chat storybunx nx agent-chat storyTin 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ề.
Chúc mừng. Bạn đã xây dựng và kiểm thử Strands Agent đầu tiên của mình ở cục bộ! 🎉🎉🎉