Implémenter et configurer l'agent Story
Tâche 1 : Implémenter le Story Agent
Section intitulée « Tâche 1 : Implémenter le Story Agent »Le Story Agent est un agent Strands généré avec --protocol=AG-UI dans le Module 1, afin que l’interface utilisateur puisse diffuser depuis celui-ci via le protocole Agent-User Interaction via CopilotKit. Il utilise l’Inventory MCP Server pour gérer les objets du joueur, et le S3SessionManager intégré de Strands pour persister l’historique des conversations dans le bucket de sessions que nous avons provisionné dans le Module 2.
Implémentation de l’agent
Section intitulée « Implémentation de l’agent »Mettez à jour les fichiers suivants dans 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-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),)
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-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),)
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 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()], )Les modifications sont :
main.pyajoute unsession_manager_providerqui crée unS3SessionManagerparthread_idlors du déploiement, et se replie sur unFileSessionManagersur disque sous/tmp/strands-sessionslors de l’exécution sousagent-serve-local(SERVE_LOCAL=true). En déploiement, le bucket S3 est le même que celui depuis lequel lequeryActionsde l’API Game lit, de sorte que le navigateur peut reconstruire les transcriptions lors d’une nouvelle visite ; localement, l’agent s’exécute entièrement hors ligne contre le serveur MCP local sans aucun appel AWS.agent.pysupprime l’outil d’exemplesubtractet remplace le prompt système par un prompt de maître de donjon qui invite le premier message utilisateur à indiquer le nom du joueur et le genre, et utilise les outils de l’Inventory MCP Server.
Tâche 2 : Déploiement et tests
Section intitulée « Tâche 2 : Déploiement et tests »Compiler le code
Section intitulée « Compiler le code »Pour compiler le code :
pnpm buildyarn buildnpm run buildbun buildDéployer votre application
Section intitulée « Déployer votre application »Pour déployer votre application, exécutez la commande suivante :
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/*"Ce déploiement prendra environ 2 minutes.
Une fois le déploiement terminé, vous verrez des sorties similaires à ceci (certaines valeurs ont été masquées) :
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_xxxTester votre Agent
Section intitulée « Tester votre Agent »Vous pouvez tester votre Agent via :
- Le démarrage d’une instance locale du serveur Agent et le dialogue avec celui-ci via la cible générée
agent-chat, ou - L’appel de l’API déployée en utilisant curl avec un token JWT.
Ouvrez un REPL interactif contre une copie servie localement de l’agent AG-UI en utilisant la cible générée agent-chat :
pnpm nx agent-chat storyyarn nx agent-chat storynpx nx agent-chat storybunx nx agent-chat storyagent-chat a dependsOn: ['agent-serve-local'], il n’est donc pas nécessaire de démarrer le serveur agent séparément — Nx lancera agent-serve-local (qui à son tour démarre le serveur MCP Inventory localement), puis lancera le REPL AG-UI agent-chat-cli contre http://localhost:8081/invocations. Votre premier message devrait indiquer à l’agent le nom de votre héros et le genre (par exemple : My name is Alice. Start my zombie adventure.) et l’histoire sera diffusée en retour.
Pour tester l’agent déployé, vous devrez vous authentifier avec Cognito et obtenir un token JWT. Tout d’abord, configurez vos variables d’environnement :
# Définissez votre User Pool ID et Client ID Cognito depuis les sorties CDKexport POOL_ID="<UserPoolId depuis les sorties CDK>"export CLIENT_ID="<UserPoolClientId depuis les sorties CDK>"export REGION="<votre-région>"Créez un utilisateur test et obtenez un token d’authentification :
# Désactiver MFA pour simplifier la création d'utilisateuraws cognito-idp set-user-pool-mfa-config \ --mfa-configuration OFF \ --user-pool-id $POOL_ID
# Créer l'utilisateuraws cognito-idp admin-create-user \ --user-pool-id $POOL_ID \ --username "test" \ --temporary-password "TempPass123-" \ --region $REGION \ --message-action SUPPRESS > /dev/null
# Définir un mot de passe permanent (remplacez par quelque chose de plus sécurisé !)aws cognito-idp admin-set-user-password \ --user-pool-id $POOL_ID \ --username "test" \ --password "PermanentPass123-" \ --region $REGION \ --permanent > /dev/null
# Authentifier l'utilisateur et capturer le token d'accèsexport 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)Invoquez l’agent déployé en utilisant l’URL du runtime Bedrock AgentCore :
# Définir l'ARN du Story Agent depuis les sorties CDKexport AGENT_ARN="<StoryAgentArn depuis les sorties CDK>"
# Encoder l'ARN en URLexport ENCODED_ARN=$(echo $AGENT_ARN | sed 's/:/%3A/g' | sed 's/\//%2F/g')
# Construire l'URL d'invocationexport AGENT_URL="https://bedrock-agentcore.$REGION.amazonaws.com/runtimes/$ENCODED_ARN/invocations?qualifier=DEFAULT"
# Invoquer l'agent avec une charge utile 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": {} }'Si la commande s’exécute avec succès, vous devriez commencer à voir l’histoire diffusée en retour sous forme de flux d’événements AG-UI (Server-Sent Events) — les blocs RUN_STARTED / TEXT_MESSAGE_START / TEXT_MESSAGE_CONTENT encapsulent les tokens réels de l’histoire :
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..."}Félicitations. Vous avez construit et déployé votre premier agent Strands sur Bedrock AgentCore Runtime ! 🎉🎉🎉