Juego de Mazmorra de IA Agéntica
Módulo 1: Configuración del monorepo
Sección titulada «Módulo 1: Configuración del monorepo»Comenzaremos creando un nuevo monorepo. Desde el directorio deseado, ejecuta el siguiente comando:
npx create-nx-workspace@21.6.4 dungeon-adventure --pm=pnpm --preset=@aws/nx-plugin --iacProvider=CDK --ci=skip --aiAgents
npx create-nx-workspace@21.6.4 dungeon-adventure --pm=yarn --preset=@aws/nx-plugin --iacProvider=CDK --ci=skip --aiAgents
npx create-nx-workspace@21.6.4 dungeon-adventure --pm=npm --preset=@aws/nx-plugin --iacProvider=CDK --ci=skip --aiAgents
npx create-nx-workspace@21.6.4 dungeon-adventure --pm=bun --preset=@aws/nx-plugin --iacProvider=CDK --ci=skip --aiAgents
Esto configurará un monorepo NX dentro del directorio dungeon-adventure
que podrás abrir en VSCode. Debería verse así:
Directory.nx/
- …
Directory.vscode/
- …
Directorynode_modules/
- …
Directorypackages/ aquí residirán tus subproyectos
- …
- .gitignore
- .npmrc
- .prettierignore
- .prettierrc
- nx.json configura el CLI de NX y los valores por defecto del monorepo
- package.json todas las dependencias de node se definen aquí
- pnpm-lock.yaml o bun.lock, yarn.lock, package-lock.json según el gestor de paquetes
- pnpm-workspace.yaml si usas pnpm
- README.md
- tsconfig.base.json todos los subproyectos basados en node extienden este
- tsconfig.json
- aws-nx-plugin.config.mts configuración para el Nx Plugin para AWS
Ahora estamos listos para comenzar a crear nuestros diferentes subproyectos usando el @aws/nx-plugin
.
API del Juego
Sección titulada «API del Juego»Primero creemos nuestra Game API. Para esto, crearemos una API tRPC llamada GameApi
siguiendo estos pasos:
- Instale el Nx Console VSCode Plugin si aún no lo ha hecho
- Abra la consola Nx en VSCode
- Haga clic en
Generate (UI)
en la sección "Common Nx Commands" - Busque
@aws/nx-plugin - ts#trpc-api
- Complete los parámetros requeridos
- name: GameApi
- Haga clic en
Generate
pnpm nx g @aws/nx-plugin:ts#trpc-api --name=GameApi --no-interactive
yarn nx g @aws/nx-plugin:ts#trpc-api --name=GameApi --no-interactive
npx nx g @aws/nx-plugin:ts#trpc-api --name=GameApi --no-interactive
bunx nx g @aws/nx-plugin:ts#trpc-api --name=GameApi --no-interactive
También puede realizar una ejecución en seco para ver qué archivos se cambiarían
pnpm nx g @aws/nx-plugin:ts#trpc-api --name=GameApi --no-interactive --dry-run
yarn nx g @aws/nx-plugin:ts#trpc-api --name=GameApi --no-interactive --dry-run
npx nx g @aws/nx-plugin:ts#trpc-api --name=GameApi --no-interactive --dry-run
bunx nx g @aws/nx-plugin:ts#trpc-api --name=GameApi --no-interactive --dry-run
Deberías ver que han aparecido algunos archivos nuevos en tu árbol de directorios.
Archivos actualizados por ts#trpc-api
A continuación se muestra una lista de todos los archivos generados por el generador ts#trpc-api
. Vamos a examinar algunos de los archivos clave resaltados en el árbol de archivos:
Directorypackages/
Directorycommon/
Directoryconstructs/
Directorysrc/
Directoryapp/ constructs CDK específicos de la aplicación
Directoryapis/
- game-api.ts construct CDK para crear tu API tRPC
- index.ts
- …
- index.ts
Directorycore/ constructs CDK genéricos
Directoryapi/
- rest-api.ts construct base CDK para API Gateway Rest API
- trpc-utils.ts utilidades para constructs CDK de API tRPC
- utils.ts utilidades para constructs de API
- index.ts
- runtime-config.ts
- index.ts
- project.json
- …
Directorytypes/ tipos compartidos
Directorysrc/
- index.ts
- runtime-config.ts definición de interfaz usada tanto por CDK como por el sitio web
- project.json
- …
Directorygame-api/ API tRPC
Directorysrc/
Directoryclient/ cliente vanilla típicamente usado para llamadas máquina a máquina en TS
- index.ts
- sigv4.ts
Directorymiddleware/ instrumentación con powertools
- error.ts
- index.ts
- logger.ts
- metrics.ts
- tracer.ts
Directoryschema/ definiciones de entradas y salidas para tu API
- echo.ts
Directoryprocedures/ implementaciones específicas de tus procedimientos/rutas de la API
- echo.ts
- index.ts
- init.ts configura el contexto y middleware
- local-server.ts usado al ejecutar el servidor tRPC localmente
- router.ts punto de entrada para tu lambda handler que define todos los procedimientos
- project.json
- …
- eslint.config.mjs
- vitest.workspace.ts
Echemos un vistazo a algunos de los archivos clave:
import { awsLambdaRequestHandler, CreateAWSLambdaContextOptions,} from '@trpc/server/adapters/aws-lambda';import { echo } from './procedures/echo.js';import { t } from './init.js';import { APIGatewayProxyEvent } from 'aws-lambda';
export const router = t.router;
export const appRouter = router({ echo,});
export const handler = awsLambdaRequestHandler({ router: appRouter, createContext: ( ctx: CreateAWSLambdaContextOptions<APIGatewayProxyEvent>, ) => ctx, responseMeta: () => ({ headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': '*', }, }),});
export type AppRouter = typeof appRouter;
El router define el punto de entrada para tu API tRPC y es donde declararás todos tus métodos de API. Como puedes ver arriba, tenemos un método llamado echo
con su implementación en el archivo ./procedures/echo.ts
.
import { publicProcedure } from '../init.js';import { EchoInputSchema, EchoOutputSchema,} from '../schema/echo.js';
export const echo = publicProcedure .input(EchoInputSchema) .output(EchoOutputSchema) .query((opts) => ({ result: opts.input.message }));
Este archivo es la implementación del método echo
y como puedes ver está fuertemente tipado declarando sus estructuras de datos de entrada y salida.
import { z } from 'zod';
export const EchoInputSchema = z.object({ message: z.string(),});
export type IEchoInput = z.TypeOf<typeof EchoInputSchema>;
export const EchoOutputSchema = z.object({ result: z.string(),});
export type IEchoOutput = z.TypeOf<typeof EchoOutputSchema>;
Todas las definiciones de esquema tRPC se definen usando Zod y se exportan como tipos TypeScript mediante la sintaxis z.TypeOf
.
import { Construct } from 'constructs';import * as url from 'url';import { Code, Runtime, Function, FunctionProps, Tracing,} from 'aws-cdk-lib/aws-lambda';import { AuthorizationType, Cors, LambdaIntegration,} from 'aws-cdk-lib/aws-apigateway';import { Duration, Stack } from 'aws-cdk-lib';import { PolicyDocument, PolicyStatement, Effect, AccountPrincipal, AnyPrincipal,} from 'aws-cdk-lib/aws-iam';import { IntegrationBuilder, RestApiIntegration,} from '../../core/api/utils.js';import { RestApi } from '../../core/api/rest-api.js';import { Procedures, routerToOperations } from '../../core/api/trpc-utils.js';import { AppRouter, appRouter } from ':dungeon-adventure/game-api';
// Tipo de unión de strings para todos los nombres de operaciones de la APItype Operations = Procedures<AppRouter>;
/** * Propiedades para crear un construct GameApi * * @template TIntegrations - Mapa de nombres de operación a sus integraciones */export interface GameApiProps< TIntegrations extends Record<Operations, RestApiIntegration>,> { /** * Mapa de nombres de operación a sus integraciones de API Gateway */ integrations: TIntegrations;}
/** * Un construct CDK que crea y configura una API REST de AWS API Gateway * específicamente para GameApi. * @template TIntegrations - Mapa de nombres de operación a sus integraciones */export class GameApi< TIntegrations extends Record<Operations, RestApiIntegration>,> extends RestApi<Operations, TIntegrations> { /** * Crea integraciones predeterminadas para todas las operaciones, que implementan cada operación como * su propia función lambda individual. * * @param scope - El alcance del construct CDK * @returns Un IntegrationBuilder con integraciones lambda predeterminadas */ public static defaultIntegrations = (scope: Construct) => { return IntegrationBuilder.rest({ operations: routerToOperations(appRouter), defaultIntegrationOptions: { runtime: Runtime.NODEJS_LATEST, handler: 'index.handler', code: Code.fromAsset( url.fileURLToPath( new URL( '../../../../../../dist/packages/game-api/bundle', import.meta.url, ), ), ), timeout: Duration.seconds(30), tracing: Tracing.ACTIVE, environment: { AWS_CONNECTION_REUSE_ENABLED: '1', }, } satisfies FunctionProps, buildDefaultIntegration: (op, props: FunctionProps) => { const handler = new Function(scope, `GameApi${op}Handler`, props); return { handler, integration: new LambdaIntegration(handler) }; }, }); };
constructor( scope: Construct, id: string, props: GameApiProps<TIntegrations>, ) { super(scope, id, { apiName: 'GameApi', defaultMethodOptions: { authorizationType: AuthorizationType.IAM, }, defaultCorsPreflightOptions: { allowOrigins: Cors.ALL_ORIGINS, allowMethods: Cors.ALL_METHODS, }, policy: new PolicyDocument({ statements: [ // Aquí permitimos que cualquier credencial AWS de la cuenta donde se despliega el proyecto pueda llamar a la API. // Se puede definir acceso fino máquina a máquina aquí usando principios más específicos (ej. roles o // usuarios) y recursos (ej. qué rutas de API pueden ser invocadas por qué principal) si es necesario. new PolicyStatement({ effect: Effect.ALLOW, principals: [new AccountPrincipal(Stack.of(scope).account)], actions: ['execute-api:Invoke'], resources: ['execute-api:/*'], }), // Abrimos OPTIONS para permitir que los navegadores hagan solicitudes preflight sin autenticar new PolicyStatement({ effect: Effect.ALLOW, principals: [new AnyPrincipal()], actions: ['execute-api:Invoke'], resources: ['execute-api:/*/OPTIONS/*'], }), ], }), operations: routerToOperations(appRouter), ...props, }); }}
Este es el construct CDK que define nuestro GameApi. Como puedes ver, proporciona un método defaultIntegrations
que automáticamente crea una función lambda para cada procedimiento en nuestra API tRPC, apuntando a la implementación de la API empaquetada. Esto significa que en el momento de cdk synth
, no ocurre empaquetado (a diferencia de usar NodeJsFunction) ya que ya lo hemos empaquetado como parte del target de build del proyecto backend.
Agente de Historia: Proyecto Python
Sección titulada «Agente de Historia: Proyecto Python»Ahora creemos nuestro Story Agent. Para esto, primero creemos un proyecto Python:
- Instale el Nx Console VSCode Plugin si aún no lo ha hecho
- Abra la consola Nx en VSCode
- Haga clic en
Generate (UI)
en la sección "Common Nx Commands" - Busque
@aws/nx-plugin - py#project
- Complete los parámetros requeridos
- name: story
- Haga clic en
Generate
pnpm nx g @aws/nx-plugin:py#project --name=story --no-interactive
yarn nx g @aws/nx-plugin:py#project --name=story --no-interactive
npx nx g @aws/nx-plugin:py#project --name=story --no-interactive
bunx nx g @aws/nx-plugin:py#project --name=story --no-interactive
También puede realizar una ejecución en seco para ver qué archivos se cambiarían
pnpm nx g @aws/nx-plugin:py#project --name=story --no-interactive --dry-run
yarn nx g @aws/nx-plugin:py#project --name=story --no-interactive --dry-run
npx nx g @aws/nx-plugin:py#project --name=story --no-interactive --dry-run
bunx nx g @aws/nx-plugin:py#project --name=story --no-interactive --dry-run
Deberías ver que han aparecido algunos archivos nuevos en tu árbol de directorios.
Archivos actualizados por py#project
A continuación se muestra una lista de archivos generados por el generador py#project
.
Directory.venv/ entorno virtual único para el monorepo
- …
Directorypackages/
Directorystory/
Directorydungeon_adventure_story/ módulo Python
- hello.py archivo Python de ejemplo (lo ignoraremos)
Directorytests/
- …
- .python-version
- pyproject.toml
- project.json
- .python-version versión de Python fijada para UV
- pyproject.toml
- uv.lock
Esto ha configurado un proyecto Python y un UV Workspace con entorno virtual compartido.
Agente de Historia: Agente Strands
Sección titulada «Agente de Historia: Agente Strands»Luego, agreguemos un agente Strands al proyecto con el generador py#strands-agent
:
- Instale el Nx Console VSCode Plugin si aún no lo ha hecho
- Abra la consola Nx en VSCode
- Haga clic en
Generate (UI)
en la sección "Common Nx Commands" - Busque
@aws/nx-plugin - py#strands-agent
- Complete los parámetros requeridos
- project: story
- Haga clic en
Generate
pnpm nx g @aws/nx-plugin:py#strands-agent --project=story --no-interactive
yarn nx g @aws/nx-plugin:py#strands-agent --project=story --no-interactive
npx nx g @aws/nx-plugin:py#strands-agent --project=story --no-interactive
bunx nx g @aws/nx-plugin:py#strands-agent --project=story --no-interactive
También puede realizar una ejecución en seco para ver qué archivos se cambiarían
pnpm nx g @aws/nx-plugin:py#strands-agent --project=story --no-interactive --dry-run
yarn nx g @aws/nx-plugin:py#strands-agent --project=story --no-interactive --dry-run
npx nx g @aws/nx-plugin:py#strands-agent --project=story --no-interactive --dry-run
bunx nx g @aws/nx-plugin:py#strands-agent --project=story --no-interactive --dry-run
Deberías ver que han aparecido algunos archivos nuevos en tu árbol de directorios.
Archivos actualizados por py#strands-agent
A continuación se muestra una lista de archivos generados por el generador py#strands-agent
.
Directorypackages/
Directorystory/
Directorydungeon_adventure_story/ módulo Python
Directoryagent/
- main.py punto de entrada para tu agente en Bedrock AgentCore Runtime
- agent.py define un agente y herramientas de ejemplo
- agentcore_mcp_client.py utilidad para crear clientes que interactúen con servidores MCP
- Dockerfile define la imagen Docker para despliegue en AgentCore Runtime
Directorycommon/constructs/
Directorysrc
Directorycore/agent-core/
- runtime.ts construct genérico para desplegar en AgentCore Runtime
Directoryapp/agents/story-agent/
- story-agent.ts construct para desplegar tu agente Story en AgentCore Runtime
Examinemos algunos de los archivos en detalle:
from contextlib import contextmanager
from strands import Agent, toolfrom strands_tools import current_time
# Define una herramienta personalizada@tooldef add(a: int, b: int) -> int: return a + b
@contextmanagerdef get_agent(session_id: str): yield Agent( system_prompt="""Eres un mago de la suma.Usa la herramienta 'add' para tareas de suma.Refiérete a las herramientas como tu 'grimorio'.""", tools=[add, current_time], )
Esto crea un agente Strands de ejemplo y define una herramienta de suma.
from bedrock_agentcore.runtime import BedrockAgentCoreApp
from .agent import get_agent
app = BedrockAgentCoreApp()
@app.entrypointasync def invoke(payload, context): """Manejador para invocación del agente""" prompt = payload.get( "prompt", "No se encontró prompt en la entrada, por favor guía al usuario " "a crear un payload JSON con la clave prompt" )
with get_agent(session_id=context.session_id) as agent: stream = agent.stream_async(prompt) async for event in stream: print(event) yield (event)
if __name__ == "__main__": app.run()
Este es el punto de entrada para el agente, configurado usando el Amazon Bedrock AgentCore SDK. Utiliza soporte de Strands para streaming y transmite eventos de vuelta al cliente a medida que ocurren.
import { Lazy, Names } from 'aws-cdk-lib';import { DockerImageAsset, Platform } from 'aws-cdk-lib/aws-ecr-assets';import { Construct } from 'constructs';import { execSync } from 'child_process';import * as path from 'path';import * as url from 'url';import { AgentCoreRuntime, AgentCoreRuntimeProps,} from '../../../core/agent-core/runtime.js';
export type StoryAgentProps = Omit< AgentCoreRuntimeProps, 'runtimeName' | 'serverProtocol' | 'containerUri'>;
export class StoryAgent extends Construct { public readonly dockerImage: DockerImageAsset; public readonly agentCoreRuntime: AgentCoreRuntime;
constructor(scope: Construct, id: string, props?: StoryAgentProps) { super(scope, id);
this.dockerImage = new DockerImageAsset(this, 'DockerImage', { platform: Platform.LINUX_ARM64, directory: path.dirname(url.fileURLToPath(new URL(import.meta.url))), extraHash: execSync( `docker inspect dungeon-adventure-story-agent:latest --format '{{.Id}}'`, { encoding: 'utf-8' }, ).trim(), });
this.agentCoreRuntime = new AgentCoreRuntime(this, 'StoryAgent', { runtimeName: Lazy.string({ produce: () => Names.uniqueResourceName(this.agentCoreRuntime, { maxLength: 40 }), }), serverProtocol: 'HTTP', containerUri: this.dockerImage.imageUri, ...props, }); }}
Esto configura un DockerImageAsset
de CDK que sube tu imagen Docker del agente a ECR, y la aloja usando AgentCore Runtime.
Puedes notar un Dockerfile
adicional - este simplemente referencia la imagen Docker del proyecto story
, permitiéndonos ubicar conjuntamente el Dockerfile y el código fuente del agente.
Inventario: Proyecto TypeScript
Sección titulada «Inventario: Proyecto TypeScript»Ahora creemos un servidor MCP que proveerá herramientas para que nuestro Story Agent gestione el inventario de un jugador.
Primero, creamos un proyecto TypeScript:
- Instale el Nx Console VSCode Plugin si aún no lo ha hecho
- Abra la consola Nx en VSCode
- Haga clic en
Generate (UI)
en la sección "Common Nx Commands" - Busque
@aws/nx-plugin - ts#project
- Complete los parámetros requeridos
- name: inventory
- Haga clic en
Generate
pnpm nx g @aws/nx-plugin:ts#project --name=inventory --no-interactive
yarn nx g @aws/nx-plugin:ts#project --name=inventory --no-interactive
npx nx g @aws/nx-plugin:ts#project --name=inventory --no-interactive
bunx nx g @aws/nx-plugin:ts#project --name=inventory --no-interactive
También puede realizar una ejecución en seco para ver qué archivos se cambiarían
pnpm nx g @aws/nx-plugin:ts#project --name=inventory --no-interactive --dry-run
yarn nx g @aws/nx-plugin:ts#project --name=inventory --no-interactive --dry-run
npx nx g @aws/nx-plugin:ts#project --name=inventory --no-interactive --dry-run
bunx nx g @aws/nx-plugin:ts#project --name=inventory --no-interactive --dry-run
Esto creará un proyecto TypeScript vacío.
Archivos actualizados por ts#project
A continuación se muestra una lista de archivos generados por el generador ts#project
.
Directorypackages/
Directoryinventory/
Directorysrc/
- index.ts punto de entrada con función de ejemplo
- project.json configuración del proyecto
- eslint.config.mjs configuración de lint
- vite.config.ts configuración de pruebas
- tsconfig.json configuración base de TypeScript para el proyecto
- tsconfig.lib.json configuración de TypeScript para compilación y empaquetado
- tsconfig.spec.json configuración de TypeScript para pruebas
- tsconfig.base.json actualizado para configurar un alias que otros proyectos usen para referenciar este
Inventario: Servidor MCP
Sección titulada «Inventario: Servidor MCP»Luego agregaremos un servidor MCP a nuestro proyecto TypeScript:
- Instale el Nx Console VSCode Plugin si aún no lo ha hecho
- Abra la consola Nx en VSCode
- Haga clic en
Generate (UI)
en la sección "Common Nx Commands" - Busque
@aws/nx-plugin - ts#mcp-server
- Complete los parámetros requeridos
- project: inventory
- Haga clic en
Generate
pnpm nx g @aws/nx-plugin:ts#mcp-server --project=inventory --no-interactive
yarn nx g @aws/nx-plugin:ts#mcp-server --project=inventory --no-interactive
npx nx g @aws/nx-plugin:ts#mcp-server --project=inventory --no-interactive
bunx nx g @aws/nx-plugin:ts#mcp-server --project=inventory --no-interactive
También puede realizar una ejecución en seco para ver qué archivos se cambiarían
pnpm nx g @aws/nx-plugin:ts#mcp-server --project=inventory --no-interactive --dry-run
yarn nx g @aws/nx-plugin:ts#mcp-server --project=inventory --no-interactive --dry-run
npx nx g @aws/nx-plugin:ts#mcp-server --project=inventory --no-interactive --dry-run
bunx nx g @aws/nx-plugin:ts#mcp-server --project=inventory --no-interactive --dry-run
Esto agregará
Archivos actualizados por ts#mcp-server
A continuación se muestra una lista de archivos generados por el generador ts#mcp-server
.
Directorypackages/
Directoryinventory/
Directorysrc/mcp-server/
- server.ts crea el servidor MCP
Directorytools/
- add.ts herramienta de ejemplo
Directoryresources/
- sample-guidance.ts recurso de ejemplo
- stdio.ts punto de entrada para MCP con transporte STDIO
- http.ts punto de entrada para MCP con transporte HTTP transmisible
- Dockerfile construye la imagen para AgentCore Runtime
- rolldown.config.ts configuración para empaquetar el servidor MCP para despliegue en AgentCore
Directorycommon/constructs/
Directorysrc
Directoryapp/mcp-servers/inventory-mcp-server/
- inventory-mcp-server.ts construct para desplegar tu servidor MCP de inventario en AgentCore Runtime
Interfaz del Juego: Sitio Web
Sección titulada «Interfaz del Juego: Sitio Web»Ahora creemos la UI que permitirá interactuar con el juego. Para esto, creemos un sitio web llamado GameUI
siguiendo estos pasos:
- Instale el Nx Console VSCode Plugin si aún no lo ha hecho
- Abra la consola Nx en VSCode
- Haga clic en
Generate (UI)
en la sección "Common Nx Commands" - Busque
@aws/nx-plugin - ts#react-website
- Complete los parámetros requeridos
- name: GameUI
- Haga clic en
Generate
pnpm nx g @aws/nx-plugin:ts#react-website --name=GameUI --no-interactive
yarn nx g @aws/nx-plugin:ts#react-website --name=GameUI --no-interactive
npx nx g @aws/nx-plugin:ts#react-website --name=GameUI --no-interactive
bunx nx g @aws/nx-plugin:ts#react-website --name=GameUI --no-interactive
También puede realizar una ejecución en seco para ver qué archivos se cambiarían
pnpm nx g @aws/nx-plugin:ts#react-website --name=GameUI --no-interactive --dry-run
yarn nx g @aws/nx-plugin:ts#react-website --name=GameUI --no-interactive --dry-run
npx nx g @aws/nx-plugin:ts#react-website --name=GameUI --no-interactive --dry-run
bunx nx g @aws/nx-plugin:ts#react-website --name=GameUI --no-interactive --dry-run
Deberías ver que han aparecido algunos archivos nuevos en tu árbol de directorios.
Archivos actualizados por ts#react-website
A continuación se muestra una lista de todos los archivos generados por el generador ts#react-website
. Examinaremos algunos de los archivos clave resaltados en el árbol de archivos:
Directorypackages/
Directorycommon/
Directoryconstructs/
Directorysrc/
Directoryapp/ constructs CDK específicos de la aplicación
Directorystatic-websites/
- game-ui.ts construct CDK para crear tu Game UI
Directorycore/
- static-website.ts construct genérico para sitio web estático
Directorygame-ui/
Directorypublic/
- …
Directorysrc/
Directorycomponents/
DirectoryAppLayout/
- index.ts diseño general de página: encabezado, pie, barra lateral, etc
- navitems.ts elementos de navegación de la barra lateral
Directoryhooks/
- useAppLayout.tsx permite configurar dinámicamente notificaciones, estilo de página, etc
Directoryroutes/ rutas basadas en @tanstack/react-router
- index.tsx página raíz ’/’ redirige a ‘/welcome’
- __root.tsx todas las páginas usan este componente como base
Directorywelcome/
- index.tsx
- config.ts
- main.tsx punto de entrada de React
- routeTree.gen.ts este archivo se actualiza automáticamente por @tanstack/react-router
- styles.css
- index.html
- project.json
- vite.config.ts
- …
import * as url from 'url';import { Construct } from 'constructs';import { StaticWebsite } from '../../core/index.js';
export class GameUI extends StaticWebsite { constructor(scope: Construct, id: string) { super(scope, id, { websiteName: 'GameUI', websiteFilePath: url.fileURLToPath( new URL( '../../../../../../dist/packages/game-ui/bundle', import.meta.url, ), ), }); }}
Este es el construct CDK que define nuestro GameUI. Como puedes ver, ya ha configurado la ruta al bundle generado para nuestra UI basada en Vite. Esto significa que en el momento de build
, el empaquetado ocurre dentro del target de build del proyecto game-ui y su salida se usa aquí.
import React from 'react';import { createRoot } from 'react-dom/client';import { I18nProvider } from '@cloudscape-design/components/i18n';import messages from '@cloudscape-design/components/i18n/messages/all.en';import { RouterProvider, createRouter } from '@tanstack/react-router';import { routeTree } from './routeTree.gen';
import '@cloudscape-design/global-styles/index.css';
const router = createRouter({ routeTree });
// Registra la instancia del router para seguridad de tiposdeclare module '@tanstack/react-router' { interface Register { router: typeof router; }}
const root = document.getElementById('root');root && createRoot(root).render( <React.StrictMode> <I18nProvider locale="en" messages={[messages]}> <RouterProvider router={router} /> </I18nProvider> </React.StrictMode>, );
Este es el punto de entrada donde se monta React. Como se muestra, inicialmente solo configura un @tanstack/react-router
en una configuración de enrutamiento basado en archivos
. Esto significa que, mientras tu servidor de desarrollo esté ejecutándose, puedes simplemente crear archivos dentro de la carpeta routes
y @tanstack/react-router
creará la configuración de archivos necesaria y actualizará el archivo routeTree.gen.ts
. Este archivo mantiene todas las rutas de manera type-safe, lo que significa que cuando usas <Link>
, la opción to
solo mostrará rutas válidas. Para más información, consulta la documentación de @tanstack/react-router
.
import { ContentLayout, Header, SpaceBetween, Container,} from '@cloudscape-design/components';import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/welcome/')({ component: RouteComponent,});
function RouteComponent() { return ( <ContentLayout header={<Header>Welcome</Header>}> <SpaceBetween size="l"> <Container>Welcome to your new Cloudscape website!</Container> </SpaceBetween> </ContentLayout> );}
Un componente que se renderizará al navegar a la ruta /welcome
. @tanstack/react-router
gestionará la Route
por ti cuando crees/muevas este archivo (siempre que el servidor de desarrollo esté ejecutándose). Esto se mostrará en una sección posterior de este tutorial.
Interfaz del Juego: Autenticación
Sección titulada «Interfaz del Juego: Autenticación»Ahora configuremos nuestra Game UI para requerir acceso autenticado mediante Amazon Cognito siguiendo estos pasos:
- Instale el Nx Console VSCode Plugin si aún no lo ha hecho
- Abra la consola Nx en VSCode
- Haga clic en
Generate (UI)
en la sección "Common Nx Commands" - Busque
@aws/nx-plugin - ts#react-website#auth
- Complete los parámetros requeridos
- cognitoDomain: game-ui
- project: @dungeon-adventure/game-ui
- allowSignup: true
- Haga clic en
Generate
pnpm nx g @aws/nx-plugin:ts#react-website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactive
yarn nx g @aws/nx-plugin:ts#react-website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactive
npx nx g @aws/nx-plugin:ts#react-website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactive
bunx nx g @aws/nx-plugin:ts#react-website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactive
También puede realizar una ejecución en seco para ver qué archivos se cambiarían
pnpm nx g @aws/nx-plugin:ts#react-website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactive --dry-run
yarn nx g @aws/nx-plugin:ts#react-website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactive --dry-run
npx nx g @aws/nx-plugin:ts#react-website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactive --dry-run
bunx nx g @aws/nx-plugin:ts#react-website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactive --dry-run
Deberías ver que han aparecido/cambiado algunos archivos nuevos en tu árbol de directorios.
Archivos actualizados por ts#react-website#auth
A continuación se muestra una lista de todos los archivos generados/actualizados por el generador ts#react-website#auth
. Examinaremos algunos de los archivos clave resaltados en el árbol de archivos:
Directorypackages/
Directorycommon/
Directoryconstructs/
Directorysrc/
Directorycore/
- user-identity.ts construct CDK para crear pools de usuarios/identidad
Directorytypes/
Directorysrc/
- runtime-config.ts actualizado para agregar cognitoProps
Directorygame-ui/
Directorysrc/
Directorycomponents/
DirectoryAppLayout/
- index.tsx agrega el usuario conectado/cierre de sesión al encabezado
DirectoryCognitoAuth/
- index.ts gestiona el inicio de sesión en Cognito
DirectoryRuntimeConfig/
- index.tsx obtiene el
runtime-config.json
y lo provee a los hijos vía contexto
- index.tsx obtiene el
Directoryhooks/
- useRuntimeConfig.tsx
- main.tsx Actualizado para agregar Cognito
import CognitoAuth from './components/CognitoAuth';import RuntimeConfigProvider from './components/RuntimeConfig';import React from 'react';import { createRoot } from 'react-dom/client';import { I18nProvider } from '@cloudscape-design/components/i18n';import messages from '@cloudscape-design/components/i18n/messages/all.en';import { RouterProvider, createRouter } from '@tanstack/react-router';import { routeTree } from './routeTree.gen';import '@cloudscape-design/global-styles/index.css';const router = createRouter({ routeTree });// Registra la instancia del router para seguridad de tiposdeclare module '@tanstack/react-router' { interface Register { router: typeof router; }}const root = document.getElementById('root');root && createRoot(root).render( <React.StrictMode> <I18nProvider locale="en" messages={[messages]}> <RuntimeConfigProvider> <CognitoAuth> <RouterProvider router={router} /> </CognitoAuth> </RuntimeConfigProvider> </I18nProvider> </React.StrictMode>, );
Los componentes RuntimeConfigProvider
y CognitoAuth
han sido agregados al archivo main.tsx
mediante una transformación AST. Esto permite que el componente CognitoAuth
se autentique con Amazon Cognito obteniendo el runtime-config.json
que contiene la configuración de conexión a Cognito necesaria para realizar las llamadas al backend al destino correcto.
Interfaz del Juego: Conectar a Game API
Sección titulada «Interfaz del Juego: Conectar a Game API»Ahora configuremos nuestra Game UI para conectarse a nuestra Game API creada previamente:
- Instale el Nx Console VSCode Plugin si aún no lo ha hecho
- Abra la consola Nx en VSCode
- Haga clic en
Generate (UI)
en la sección "Common Nx Commands" - Busque
@aws/nx-plugin - api-connection
- Complete los parámetros requeridos
- sourceProject: @dungeon-adventure/game-ui
- targetProject: @dungeon-adventure/game-api
- Haga clic en
Generate
pnpm nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactive
yarn nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactive
npx nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactive
bunx nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactive
También puede realizar una ejecución en seco para ver qué archivos se cambiarían
pnpm nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactive --dry-run
yarn nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactive --dry-run
npx nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactive --dry-run
bunx nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactive --dry-run
Deberías ver que han aparecido/cambiado algunos archivos nuevos en tu árbol de directorios.
Archivos actualizados por la conexión UI -> tRPC
A continuación se muestra una lista de todos los archivos generados/actualizados por el generador api-connection
. Examinaremos algunos de los archivos clave resaltados en el árbol de archivos:
Directorypackages/
Directorygame-ui/
Directorysrc/
Directorycomponents/
- GameApiClientProvider.tsx configura el cliente de GameAPI
Directoryhooks/
- useGameApi.tsx hooks para llamar a la GameApi
- main.tsx inyecta los proveedores del cliente trpc
- package.json
import { GameApiTRCPContext } from '../components/GameApiClientProvider';
export const useGameApi = GameApiTRCPContext.useTRPC;
Este hook usa la última integración de React Query de tRPC permitiendo a los usuarios interactuar con @tanstack/react-query
directamente sin capas adicionales de abstracción. Para ejemplos de cómo llamar a APIs tRPC, consulta la guía de uso del hook tRPC.
import GameApiClientProvider from './components/GameApiClientProvider';import QueryClientProvider from './components/QueryClientProvider';import CognitoAuth from './components/CognitoAuth';import RuntimeConfigProvider from './components/RuntimeConfig';import React from 'react';import { createRoot } from 'react-dom/client';import { I18nProvider } from '@cloudscape-design/components/i18n';import messages from '@cloudscape-design/components/i18n/messages/all.en';import { RouterProvider, createRouter } from '@tanstack/react-router';import { routeTree } from './routeTree.gen';import '@cloudscape-design/global-styles/index.css';const router = createRouter({ routeTree });// Registra la instancia del router para seguridad de tiposdeclare module '@tanstack/react-router' { interface Register { router: typeof router; }}const root = document.getElementById('root');root && createRoot(root).render( <React.StrictMode> <I18nProvider locale="en" messages={[messages]}> <RuntimeConfigProvider> <CognitoAuth> <QueryClientProvider> <GameApiClientProvider> <RouterProvider router={router} /> </GameApiClientProvider> </QueryClientProvider> </CognitoAuth> </RuntimeConfigProvider> </I18nProvider> </React.StrictMode>, );
El archivo main.tsx
ha sido actualizado mediante una transformación AST para inyectar los proveedores de tRPC.
Interfaz del Juego: Infraestructura
Sección titulada «Interfaz del Juego: Infraestructura»Ahora el último subproyecto que necesitamos crear es para la infraestructura CDK. Para crearlo, sigue estos pasos:
- Instale el Nx Console VSCode Plugin si aún no lo ha hecho
- Abra la consola Nx en VSCode
- Haga clic en
Generate (UI)
en la sección "Common Nx Commands" - Busque
@aws/nx-plugin - ts#infra
- Complete los parámetros requeridos
- name: infra
- Haga clic en
Generate
pnpm nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive
yarn nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive
npx nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive
bunx nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive
También puede realizar una ejecución en seco para ver qué archivos se cambiarían
pnpm nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive --dry-run
yarn nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive --dry-run
npx nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive --dry-run
bunx nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive --dry-run
Deberías ver que han aparecido/cambiado algunos archivos nuevos en tu árbol de directorios.
Archivos actualizados por ts#infra
A continuación se muestra una lista de todos los archivos generados/actualizados por el generador ts#infra
. Examinaremos algunos de los archivos clave resaltados en el árbol de archivos:
Directorypackages/
Directorycommon/
Directoryconstructs/
Directorysrc/
Directorycore/
- checkov.ts
- index.ts
Directoryinfra
Directorysrc/
Directorystages/
- application-stage.ts stacks CDK definidos aquí
Directorystacks/
- application-stack.ts recursos CDK definidos aquí
- index.ts
- main.ts punto de entrada que define todas las etapas
- cdk.json
- project.json
- …
- package.json
- tsconfig.json agregadas referencias
- tsconfig.base.json agregado alias
import { ApplicationStage } from './stacks/application-stage.js';import { App } from ':dungeon-adventure/common-constructs';
const app = new App();
// Usa esto para desplegar tu propio entorno sandbox (asume tus credenciales CLI)new ApplicationStage(app, 'dungeon-adventure-infra-sandbox', { env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION, },});
app.synth();
Este es el punto de entrada para tu aplicación CDK.
import * as cdk from 'aws-cdk-lib';import { Construct } from 'constructs';
export class ApplicationStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props);
// El código que define tu stack va aquí }}
Aquí es donde instanciaremos nuestros constructs CDK para construir nuestro juego de aventuras.
Actualizar nuestra infraestructura
Sección titulada «Actualizar nuestra infraestructura»Hagamos una actualización en nuestro packages/infra/src/stacks/application-stack.ts
para instanciar algunos de nuestros constructs ya generados:
import { GameApi, GameUI, InventoryMcpServer, RuntimeConfig, StoryAgent, UserIdentity,} from ':dungeon-adventure/common-constructs';import { Stack, StackProps, CfnOutput } from 'aws-cdk-lib';import { Construct } from 'constructs';
export class ApplicationStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props);
const userIdentity = new UserIdentity(this, 'UserIdentity');
const gameApi = new GameApi(this, 'GameApi', { integrations: GameApi.defaultIntegrations(this).build(), });
const { userPool, userPoolClient } = userIdentity;
const mcpServer = new InventoryMcpServer(this, 'InventoryMcpServer');
// Use Cognito for user authentication with the agent const storyAgent = new StoryAgent(this, 'StoryAgent', { authorizerConfiguration: { customJwtAuthorizer: { discoveryUrl: `https://cognito-idp.${Stack.of(userPool).region}.amazonaws.com/${userPool.userPoolId}/.well-known/openid-configuration`, allowedAudience: [userPoolClient.userPoolClientId], }, }, environment: { INVENTORY_MCP_ARN: mcpServer.agentCoreRuntime.arn, }, }); // Add the Story Agent ARN to runtime-config.json so it can be referenced by the website RuntimeConfig.ensure(this).config.agentArn = storyAgent.agentCoreRuntime.arn;
new CfnOutput(this, 'StoryAgentArn', { value: storyAgent.agentCoreRuntime.arn, }); new CfnOutput(this, 'InventoryMcpArn', { value: mcpServer.agentCoreRuntime.arn, });
// Grant the agent permissions to invoke our mcp server mcpServer.agentCoreRuntime.grantInvoke(storyAgent.agentCoreRuntime);
// Grant the authenticated role access to invoke the api gameApi.grantInvokeAccess(userIdentity.identityPool.authenticatedRole);
// Ensure this is instantiated last so our runtime-config.json can be automatically configured new GameUI(this, 'GameUI'); }}
import { Stack, StackProps } from 'aws-cdk-lib';import { GameApi, GameUI, InventoryMcpServer, RuntimeConfig, StoryAgent, UserIdentity,} from ':dungeon-adventure/common-constructs';import { Stack, StackProps, CfnOutput } from 'aws-cdk-lib';import { Construct } from 'constructs';
export class ApplicationStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props);
// The code that defines your stack goes here const userIdentity = new UserIdentity(this, 'UserIdentity');
const gameApi = new GameApi(this, 'GameApi', { integrations: GameApi.defaultIntegrations(this).build(), });
const { userPool, userPoolClient } = userIdentity;
const mcpServer = new InventoryMcpServer(this, 'InventoryMcpServer');
// Use Cognito for user authentication with the agent const storyAgent = new StoryAgent(this, 'StoryAgent', { authorizerConfiguration: { customJwtAuthorizer: { discoveryUrl: `https://cognito-idp.${Stack.of(userPool).region}.amazonaws.com/${userPool.userPoolId}/.well-known/openid-configuration`, allowedAudience: [userPoolClient.userPoolClientId], }, }, environment: { INVENTORY_MCP_ARN: mcpServer.agentCoreRuntime.arn, }, }); // Add the Story Agent ARN to runtime-config.json so it can be referenced by the website RuntimeConfig.ensure(this).config.agentArn = storyAgent.agentCoreRuntime.arn;
new CfnOutput(this, 'StoryAgentArn', { value: storyAgent.agentCoreRuntime.arn, }); new CfnOutput(this, 'InventoryMcpArn', { value: mcpServer.agentCoreRuntime.arn, });
// Grant the agent permissions to invoke our mcp server mcpServer.agentCoreRuntime.grantInvoke(storyAgent.agentCoreRuntime);
// Grant the authenticated role access to invoke the api gameApi.grantInvokeAccess(userIdentity.identityPool.authenticatedRole);
// Ensure this is instantiated last so our runtime-config.json can be automatically configured new GameUI(this, 'GameUI'); }}
Nota que aquí proveemos integraciones predeterminadas para nuestra Game API. Por defecto, cada operación en nuestra API se mapea a una función lambda individual para manejar esa operación.
Construyendo nuestro código
Sección titulada «Construyendo nuestro código»Comandos de Nx
Targets únicos vs múltiples
Sección titulada «Targets únicos vs múltiples»El comando run-many
ejecutará un target en múltiples subproyectos listados (--all
los seleccionará todos). Se asegurará de que las dependencias se ejecuten en el orden correcto.
También puedes disparar un build (o cualquier otra tarea) para un target de proyecto único ejecutando el target directamente en el proyecto. Por ejemplo, si queremos construir el proyecto @dungeon-adventure/infra
, puedes ejecutar:
pnpm nx run @dungeon-adventure/infra:build
yarn nx run @dungeon-adventure/infra:build
npx nx run @dungeon-adventure/infra:build
bunx nx run @dungeon-adventure/infra:build
También puedes omitir el scope y usar la sintaxis abreviada de Nx si prefieres:
pnpm nx build infra
yarn nx build infra
npx nx build infra
bunx nx build infra
Visualizando tus dependencias
Sección titulada «Visualizando tus dependencias»También puedes visualizar tus dependencias mediante:
pnpm nx graph
yarn nx graph
npx nx graph
bunx nx graph

Nx depende del caching para reutilizar artefactos de builds previos y acelerar el desarrollo. Se requiere cierta configuración para que esto funcione correctamente y puede haber casos donde quieras realizar un build sin usar la caché. Para eso, simplemente agrega el argumento --skip-nx-cache
a tu comando. Por ejemplo:
pnpm nx run @dungeon-adventure/infra:build --skip-nx-cache
yarn nx run @dungeon-adventure/infra:build --skip-nx-cache
npx nx run @dungeon-adventure/infra:build --skip-nx-cache
bunx nx run @dungeon-adventure/infra:build --skip-nx-cache
Si por alguna razón quisieras limpiar tu caché (almacenada en la carpeta .nx
), puedes ejecutar:
pnpm nx reset
yarn nx reset
npx nx reset
bunx nx reset
pnpm nx run-many --target build --all
yarn nx run-many --target build --all
npx nx run-many --target build --all
bunx nx run-many --target build --all
Deberías ver el siguiente mensaje:
NX The workspace is out of sync
[@nx/js:typescript-sync]: Some TypeScript configuration files are missing project references to the projects they depend on or contain outdated project references.
This will result in an error in CI.
? Would you like to sync the identified changes to get your workspace up to date? …Yes, sync the changes and run the tasksNo, run the tasks without syncing the changes
Este mensaje indica que NX ha detectado algunos archivos que pueden actualizarse automáticamente. En este caso, se refiere a los archivos tsconfig.json
que no tienen referencias de proyecto configuradas en proyectos dependientes. Selecciona la opción Yes, sync the changes and run the tasks para continuar. Deberías notar que todos los errores de importación en tu IDE se resuelven automáticamente, ya que el generador de sync agregará las referencias TypeScript faltantes.
Todos los artefactos construidos están ahora disponibles dentro de la carpeta dist/
ubicada en la raíz del monorepo. Esta es una práctica estándar cuando se usan proyectos generados por el @aws/nx-plugin
ya que no contamina tu árbol de archivos con archivos generados. En caso de que quieras limpiar tus archivos, simplemente puedes eliminar la carpeta dist/
sin preocuparte por archivos generados esparcidos por el árbol de directorios.
¡Felicidades! Has creado todos los subproyectos requeridos para comenzar a implementar el núcleo de nuestro juego Dungeon Adventure. 🎉🎉🎉