Implementar y configurar el agente Story
Tarea 1: Implementar el Agente de Historia
Sección titulada «Tarea 1: Implementar el Agente de Historia»El Agente de Historia es un agente de Strands que, dado un Game y una lista de Actions como contexto, avanzará una historia. Configuraremos el agente para interactuar con nuestro Inventory MCP Server y gestionar los objetos disponibles de un jugador.
Implementación del agente
Sección titulada «Implementación del agente»Para implementar nuestro agente, actualiza los siguientes archivos en packages/story/dungeon_adventure_story/agent:
import uuid
import uvicornfrom bedrock_agentcore.runtime.models import PingStatusfrom fastapi.responses import PlainTextResponse, StreamingResponsefrom pydantic import BaseModel
from .agent import get_agentfrom .init import app
class Action(BaseModel): role: str content: str
class InvokeInput(BaseModel): playerName: str genre: str actions: list[Action]
async def handle_invoke(input: InvokeInput): """Streaming handler for agent invocation""" messages = [{"role": "user", "content": [{"text": "Continue or create a new story..."}]}] for action in input.actions: messages.append({"role": action.role, "content": [{"text": action.content}]})
with get_agent(input.playerName, input.genre, session_id=str(uuid.uuid4())) as agent: stream = agent.stream_async(messages) async for event in stream: print(event) content = event.get("event", {}).get("contentBlockDelta", {}).get("delta", {}).get("text") if content is not None: yield content elif event.get("event", {}).get("messageStop") is not None: yield "\n"
@app.post("/invocations", openapi_extra={"x-streaming": True}, response_class=PlainTextResponse)async def invoke(input: InvokeInput) -> str: """Entry point for agent invocation""" return StreamingResponse(handle_invoke(input), media_type="text/event-stream")
@app.get("/ping")def ping() -> str: # TODO: if running an async task, return PingStatus.HEALTHY_BUSY return PingStatus.HEALTHY
if __name__ == "__main__": uvicorn.run("dungeon_adventure_story.agent.main:app", port=8080)import uuid
import uvicornfrom bedrock_agentcore.runtime.models import PingStatusfrom fastapi.responses import PlainTextResponse, StreamingResponsefrom pydantic import BaseModel
from .agent import get_agentfrom .init import app
class Action(BaseModel): role: str content: str
class InvokeInput(BaseModel): prompt: str session_id: str playerName: str genre: str actions: list[Action]
async def handle_invoke(input: InvokeInput): """Streaming handler for agent invocation""" with get_agent(session_id=input.session_id) as agent: stream = agent.stream_async(input.prompt) messages = [{"role": "user", "content": [{"text": "Continue or create a new story..."}]}] for action in input.actions: messages.append({"role": action.role, "content": [{"text": action.content}]})
with get_agent(input.playerName, input.genre, session_id=str(uuid.uuid4())) as agent: stream = agent.stream_async(messages) async for event in stream: print(event) content = event.get("event", {}).get("contentBlockDelta", {}).get("delta", {}).get("text") if content is not None: yield content elif event.get("event", {}).get("messageStop") is not None: yield "\n"
@app.post("/invocations", openapi_extra={"x-streaming": True}, response_class=PlainTextResponse)async def invoke(input: InvokeInput) -> str: """Entry point for agent invocation""" return StreamingResponse(handle_invoke(input), media_type="text/event-stream")
@app.get("/ping")def ping() -> str: # TODO: if running an async task, return PingStatus.HEALTHY_BUSY return PingStatus.HEALTHY
if __name__ == "__main__": uvicorn.run("dungeon_adventure_story.agent.main:app", port=8080)import osfrom contextlib import contextmanager
import boto3from strands import Agent
from .agentcore_mcp_client import AgentCoreMCPClient
# Obtain the region and credentialsregion = os.environ["AWS_REGION"]boto_session = boto3.Session(region_name=region)credentials = boto_session.get_credentials()
@contextmanagerdef get_agent(player_name: str, genre: str, session_id: str): mcp_client = AgentCoreMCPClient.with_iam_auth( agent_runtime_arn=os.environ["INVENTORY_MCP_ARN"], credentials=credentials, region=region, session_id=session_id, ) with mcp_client: yield Agent( system_prompt=f"""You are running a text adventure game in the genre <genre>{genre}</genre> for player <player>{player_name}</player>.Construct a scenario and give the player decisions to make.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.When starting a game, populate the inventory with a few initial items. Items should be a key part of the narrative.Keep responses under 100 words.""", tools=[*mcp_client.list_tools_sync()], )import osfrom contextlib import contextmanager
from strands import Agent, toolfrom strands_tools import current_timeimport boto3from strands import Agent
from .agentcore_mcp_client import AgentCoreMCPClient
# Define a custom tool@tooldef add(a: int, b: int) -> int: return a + b# Obtain the region and credentialsregion = os.environ["AWS_REGION"]boto_session = boto3.Session(region_name=region)credentials = boto_session.get_credentials()
@contextmanagerdef get_agent(session_id: str): yield Agent( system_prompt="""You are an addition wizard.Use the 'add' tool for addition tasks.Refer to tools as your 'spellbook'.""", tools=[add, current_time],def get_agent(player_name: str, genre: str, session_id: str): mcp_client = AgentCoreMCPClient.with_iam_auth( agent_runtime_arn=os.environ["INVENTORY_MCP_ARN"], credentials=credentials, region=region, session_id=session_id, ) with mcp_client: yield Agent( system_prompt=f"""You are running a text adventure game in the genre <genre>{genre}</genre> for player <player>{player_name}</player>.Construct a scenario and give the player decisions to make.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.When starting a game, populate the inventory with a few initial items. Items should be a key part of the narrative.Keep responses under 100 words.""", tools=[*mcp_client.list_tools_sync()], )Esto configurará lo siguiente:
- Extracción del jugador, género y acciones del payload del agente,
- Construcción de un cliente que el Agente puede usar para invocar nuestro servidor MCP con Autenticación SigV4, y
- Construcción del agente con un prompt del sistema y las herramientas del servidor MCP.
Tarea 2: Despliegue y pruebas
Sección titulada «Tarea 2: Despliegue y pruebas»Compilar el código
Sección titulada «Compilar el código»Para compilar el código:
pnpm nx run-many --target build --allyarn nx run-many --target build --allnpx nx run-many --target build --allbunx nx run-many --target build --allDesplegar tu aplicación
Sección titulada «Desplegar tu aplicación»Para desplegar tu aplicación, ejecuta el siguiente comando:
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/*Este despliegue tomará aproximadamente 2 minutos en completarse.
Una vez que el despliegue se complete, verás salidas similares a las siguientes (algunos valores han sido omitidos):
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.StoryAgentArn = arn:aws:bedrock-agentcore:region:xxxxxxx:runtime/dungeonadventurecationStoryAgentXXXX-YYYYdungeon-adventure-infra-sandbox-Application.UserIdentityUserIdentityIdentityPoolIdXXX = region:xxxdungeon-adventure-infra-sandbox-Application.UserIdentityUserIdentityUserPoolIdXXX = region_xxxProbar tu API
Sección titulada «Probar tu API»Puedes probar tu API de dos formas:
- Iniciando una instancia local del servidor del Agente e invocándola con
curl, o - Llamando a la API desplegada usando curl con un token JWT.
Inicia tu servidor local del Agente ejecutando el siguiente comando:
INVENTORY_MCP_ARN=arn:aws:bedrock-agentcore:region:xxxxxxx:runtime/dungeonadventureventoryMcpServerXXXX-YYYY AWS_REGION=<region> pnpm nx run dungeon_adventure.story:agent-serveINVENTORY_MCP_ARN=arn:aws:bedrock-agentcore:region:xxxxxxx:runtime/dungeonadventureventoryMcpServerXXXX-YYYY AWS_REGION=<region> yarn nx run dungeon_adventure.story:agent-serveINVENTORY_MCP_ARN=arn:aws:bedrock-agentcore:region:xxxxxxx:runtime/dungeonadventureventoryMcpServerXXXX-YYYY AWS_REGION=<region> npx nx run dungeon_adventure.story:agent-serveINVENTORY_MCP_ARN=arn:aws:bedrock-agentcore:region:xxxxxxx:runtime/dungeonadventureventoryMcpServerXXXX-YYYY AWS_REGION=<region> bunx nx run dungeon_adventure.story:agent-serveUna vez que el servidor del Agente esté en ejecución (no verás ninguna salida), invócalo ejecutando el siguiente comando:
curl -N -X POST http://127.0.0.1:8081/invocations \ -d '{"genre":"superhero", "actions":[], "playerName":"UnnamedHero"}' \ -H "Content-Type: application/json"Para probar el agente desplegado, necesitarás autenticarte con Cognito y obtener un token JWT. Primero, configura tus variables de entorno:
# Establece tu User Pool ID y Client ID de Cognito desde los outputs de CDKexport POOL_ID="<UserPoolId from CDK outputs>"export CLIENT_ID="<UserPoolClientId from CDK outputs>"export REGION="<your-region>"Crea un usuario de prueba y obtén un token de autenticación:
# Create Useraws cognito-idp admin-create-user \ --user-pool-id $POOL_ID \ --username "testuser" \ --temporary-password "TempPass123!" \ --region $REGION \ --message-action SUPPRESS > /dev/null
# Set Permanent Password (replace with something more secure!)aws cognito-idp admin-set-user-password \ --user-pool-id $POOL_ID \ --username "testuser" \ --password "PermanentPass123!" \ --region $REGION \ --permanent > /dev/null
# Authenticate User and capture ID Tokenexport BEARER_TOKEN=$(aws cognito-idp initiate-auth \ --client-id "$CLIENT_ID" \ --auth-flow USER_PASSWORD_AUTH \ --auth-parameters USERNAME='testuser',PASSWORD='PermanentPass123!' \ --region $REGION | jq -r '.AuthenticationResult.IdToken')Invoca el agente desplegado usando la URL de Bedrock AgentCore Runtime:
# Set the Story Agent ARN from CDK outputsexport AGENT_ARN="<StoryAgentArn from CDK outputs>"
# URL-encode the ARNexport ENCODED_ARN=$(echo $AGENT_ARN | sed 's/:/%3A/g' | sed 's/\//%2F/g')
# Construct the invocation URLexport MCP_URL="https://bedrock-agentcore.$REGION.amazonaws.com/runtimes/$ENCODED_ARN/invocations?qualifier=DEFAULT"
# Invoke the agentcurl -N -X POST "$MCP_URL" \ -H "authorization: Bearer $BEARER_TOKEN" \ -H "Content-Type: application/json" \ -H "X-Amzn-Bedrock-AgentCore-Runtime-Session-Id: abcdefghijklmnopqrstuvwxyz-123456789" \ -d '{"genre":"superhero", "actions":[], "playerName":"UnnamedHero"}'Si el comando se ejecuta correctamente, verás eventos transmitidos similares a:
data: {"init_event_loop": true}
data: {"start": true}
data: {"start_event_loop": true}
data: {"event": {"messageStart": {"role": "assistant"}}}
data: {"event": {"contentBlockDelta": {"delta": {"text": "Welcome"}, "contentBlockIndex": 0}}}
...¡Felicidades! Has construido y desplegado tu primer Agente de Strands en Bedrock AgentCore Runtime. 🎉🎉🎉