Jogo de Dungeons de IA Agêntica
Módulo 1: Configuração do monorepo
Seção intitulada “Módulo 1: Configuração do monorepo”Vamos começar criando um novo monorepo. Dentro do diretório desejado, execute o seguinte 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
Isso configurará um monorepo NX dentro do diretório dungeon-adventure
que você pode abrir no vscode. Deve parecer com o seguinte:
Directory.nx/
- …
Directory.vscode/
- …
Directorynode_modules/
- …
Directorypackages/ aqui residirão seus subprojetos
- …
- .gitignore
- .npmrc
- .prettierignore
- .prettierrc
- nx.json configura o CLI do NX e padrões do monorepo
- package.json todas as dependências Node são definidas aqui
- pnpm-lock.yaml ou bun.lock, yarn.lock, package-lock.json dependendo do gerenciador de pacotes
- pnpm-workspace.yaml se usar pnpm
- README.md
- tsconfig.base.json todos os subprojetos baseados em Node estendem este
- tsconfig.json
- aws-nx-plugin.config.mts configuração do Nx Plugin para AWS
Agora estamos prontos para começar a criar nossos diferentes subprojetos usando o @aws/nx-plugin
.
API do Jogo
Seção intitulada “API do Jogo”Primeiro vamos criar nossa Game API. Para isso, vamos criar uma API tRPC chamada GameApi
seguindo os passos abaixo:
- Instale o Nx Console VSCode Plugin se ainda não o fez
- Abra o console Nx no VSCode
- Clique em
Generate (UI)
na seção "Common Nx Commands" - Procure por
@aws/nx-plugin - ts#trpc-api
- Preencha os parâmetros obrigatórios
- name: GameApi
- Clique em
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
Você também pode realizar uma execução simulada para ver quais arquivos seriam alterados
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
Você deve ver que alguns novos arquivos apareceram na sua árvore de arquivos.
Arquivos atualizados do ts#trpc-api
Abaixo está uma lista de todos os arquivos gerados pelo gerador ts#trpc-api
. Vamos examinar alguns dos arquivos-chave destacados na árvore de arquivos:
Directorypackages/
Directorycommon/
Directoryconstructs/
Directorysrc/
Directoryapp/ constructs CDK específicos do aplicativo
Directoryapis/
- game-api.ts construct CDK para criar sua API tRPC
- index.ts
- …
- index.ts
Directorycore/ constructs CDK genéricos
Directoryapi/
- rest-api.ts construct CDK base para uma API Gateway Rest API
- trpc-utils.ts utilitários para constructs CDK de API tRPC
- utils.ts utilitários para constructs de API
- index.ts
- runtime-config.ts
- index.ts
- project.json
- …
Directorytypes/ tipos compartilhados
Directorysrc/
- index.ts
- runtime-config.ts definição de interface usada por CDK e website
- project.json
- …
Directorygame-api/ API tRPC
Directorysrc/
Directoryclient/ cliente vanilla tipicamente usado para chamadas máquina a máquina em TS
- index.ts
- sigv4.ts
Directorymiddleware/ instrumentação com powertools
- error.ts
- index.ts
- logger.ts
- metrics.ts
- tracer.ts
Directoryschema/ definições de inputs e outputs da API
- echo.ts
Directoryprocedures/ implementações específicas dos procedimentos/rotas da API
- echo.ts
- index.ts
- init.ts configura contexto e middleware
- local-server.ts usado ao executar o servidor tRPC localmente
- router.ts ponto de entrada para o lambda handler que define todos os procedimentos
- project.json
- …
- eslint.config.mjs
- vitest.workspace.ts
Analisando alguns dos arquivos-chave:
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;
O router define o ponto de entrada para sua API tRPC e é onde você declarará todos os métodos da API. Como visto acima, temos um método chamado echo
com sua implementação no arquivo ./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 arquivo é a implementação do método echo
e como visto é fortemente tipado declarando suas estruturas de dados de entrada e saída.
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 as definições de schema tRPC são definidas usando Zod e exportadas como tipos TypeScript via sintaxe 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 union de strings para todos os nomes de operações da APItype Operations = Procedures<AppRouter>;
/** * Propriedades para criar um construct GameApi * * @template TIntegrations - Mapa de nomes de operação para suas integrações */export interface GameApiProps< TIntegrations extends Record<Operations, RestApiIntegration>,> { /** * Mapa de nomes de operação para suas integrações no API Gateway */ integrations: TIntegrations;}
/** * Um construct CDK que cria e configura uma AWS API Gateway REST API * especificamente para GameApi. * @template TIntegrations - Mapa de nomes de operação para suas integrações */export class GameApi< TIntegrations extends Record<Operations, RestApiIntegration>,> extends RestApi<Operations, TIntegrations> { /** * Cria integrações padrão para todas as operações, implementando cada operação como * sua própria função lambda individual. * * @param scope - O escopo do construct CDK * @returns Um IntegrationBuilder com integrações lambda padrão */ 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: [ // Aqui concedemos a qualquer credencial AWS da conta em que o projeto é implantado para chamar a API. // Acesso refinado máquina a máquina pode ser definido aqui usando principals mais específicos (ex: roles ou // usuários) e recursos (ex: quais caminhos da API podem ser invocados por qual principal) se necessário. new PolicyStatement({ effect: Effect.ALLOW, principals: [new AccountPrincipal(Stack.of(scope).account)], actions: ['execute-api:Invoke'], resources: ['execute-api:/*'], }), // Abre OPTIONS para permitir que navegadores façam requisições preflight não autenticadas new PolicyStatement({ effect: Effect.ALLOW, principals: [new AnyPrincipal()], actions: ['execute-api:Invoke'], resources: ['execute-api:/*/OPTIONS/*'], }), ], }), operations: routerToOperations(appRouter), ...props, }); }}
Este é o construct CDK que define nossa GameApi. Como visto, ele fornece um método defaultIntegrations
que cria automaticamente uma função lambda para cada procedimento em nossa API tRPC, apontando para a implementação da API empacotada. Isso significa que no tempo de cdk synth
, o empacotamento não ocorre (diferente de usar NodeJsFunction) pois já o empacotamos como parte do target de build do projeto backend.
Agente de História: Projeto Python
Seção intitulada “Agente de História: Projeto Python”Agora vamos criar nosso Story Agent. Para isso, primeiro vamos criar um Projeto Python:
- Instale o Nx Console VSCode Plugin se ainda não o fez
- Abra o console Nx no VSCode
- Clique em
Generate (UI)
na seção "Common Nx Commands" - Procure por
@aws/nx-plugin - py#project
- Preencha os parâmetros obrigatórios
- name: story
- Clique em
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
Você também pode realizar uma execução simulada para ver quais arquivos seriam alterados
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
Você deve ver que alguns novos arquivos apareceram na sua árvore de arquivos.
Arquivos atualizados do py#project
Abaixo está uma lista de arquivos gerados pelo gerador py#project
.
Directory.venv/ único ambiente virtual para o monorepo
- …
Directorypackages/
Directorystory/
Directorydungeon_adventure_story/ módulo Python
- hello.py exemplo de arquivo Python (vamos ignorar este)
Directorytests/
- …
- .python-version
- pyproject.toml
- project.json
- .python-version versão Python fixada
- pyproject.toml
- uv.lock
Isso configurou um projeto Python e UV Workspace com ambiente virtual compartilhado.
Agente de História: Agente Strands
Seção intitulada “Agente de História: Agente Strands”Em seguida, vamos adicionar um agente Strands ao projeto com o gerador py#strands-agent
:
- Instale o Nx Console VSCode Plugin se ainda não o fez
- Abra o console Nx no VSCode
- Clique em
Generate (UI)
na seção "Common Nx Commands" - Procure por
@aws/nx-plugin - py#strands-agent
- Preencha os parâmetros obrigatórios
- project: story
- Clique em
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
Você também pode realizar uma execução simulada para ver quais arquivos seriam alterados
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
Você deve ver que alguns novos arquivos apareceram na sua árvore de arquivos.
Arquivos atualizados do py#strands-agent
Abaixo está uma lista de arquivos gerados pelo gerador py#strands-agent
.
Directorypackages/
Directorystory/
Directorydungeon_adventure_story/ módulo Python
Directoryagent/
- main.py ponto de entrada do agente no Bedrock AgentCore Runtime
- agent.py define um agente e ferramentas de exemplo
- agentcore_mcp_client.py utilitário para criar clientes para interagir com servidores MCP
- Dockerfile define a imagem Docker para implantação no AgentCore Runtime
Directorycommon/constructs/
Directorysrc
Directorycore/agent-core/
- runtime.ts construct genérico para implantação no AgentCore Runtime
Directoryapp/agents/story-agent/
- story-agent.ts construct para implantar seu agente Story no AgentCore Runtime
Vamos analisar alguns dos arquivos em detalhes:
from contextlib import contextmanager
from strands import Agent, toolfrom strands_tools import current_time
# Define uma ferramenta customizada@tooldef add(a: int, b: int) -> int: return a + b
@contextmanagerdef get_agent(session_id: str): yield Agent( system_prompt="""Você é um mago da adição.Use a ferramenta 'add' para tarefas de adição.Refira-se às ferramentas como seu 'grimório'.""", tools=[add, current_time], )
Isso cria um agente Strands de exemplo e define uma ferramenta de adição.
from bedrock_agentcore.runtime import BedrockAgentCoreApp
from .agent import get_agent
app = BedrockAgentCoreApp()
@app.entrypointasync def invoke(payload, context): """Handler para invocação do agente""" prompt = payload.get( "prompt", "Nenhum prompt encontrado no input, por favor oriente o usuário " "a criar um payload json com a chave 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 é o ponto de entrada do agente, configurado usando o Amazon Bedrock AgentCore SDK. Ele usa o suporte do Strands para streaming e transmite eventos de volta ao cliente conforme ocorrem.
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, }); }}
Isso configura um DockerImageAsset
do CDK que envia sua imagem Docker do agente para o ECR e a hospeda usando o AgentCore Runtime.
Você pode notar um Dockerfile
extra - isso simplesmente referencia a imagem Docker do projeto story
, permitindo que co-localizemos o Dockerfile e o código-fonte do agente.
Inventário: Projeto TypeScript
Seção intitulada “Inventário: Projeto TypeScript”Agora vamos criar um servidor MCP que fornecerá ferramentas para nosso Story Agent gerenciar o inventário de um jogador.
Primeiro, criamos um projeto TypeScript:
- Instale o Nx Console VSCode Plugin se ainda não o fez
- Abra o console Nx no VSCode
- Clique em
Generate (UI)
na seção "Common Nx Commands" - Procure por
@aws/nx-plugin - ts#project
- Preencha os parâmetros obrigatórios
- name: inventory
- Clique em
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
Você também pode realizar uma execução simulada para ver quais arquivos seriam alterados
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
Isso criará um projeto TypeScript vazio.
Arquivos atualizados do ts#project
Abaixo está uma lista de arquivos gerados pelo gerador ts#project
.
Directorypackages/
Directoryinventory/
Directorysrc/
- index.ts ponto de entrada com função de exemplo
- project.json configuração do projeto
- eslint.config.mjs configuração de lint
- vite.config.ts configuração de testes
- tsconfig.json configuração base TypeScript para o projeto
- tsconfig.lib.json configuração TypeScript para compilação e empacotamento
- tsconfig.spec.json configuração TypeScript para testes
- tsconfig.base.json atualizado para configurar um alias para outros projetos referenciarem este
Inventário: Servidor MCP
Seção intitulada “Inventário: Servidor MCP”Em seguida, adicionaremos um servidor MCP ao nosso projeto TypeScript:
- Instale o Nx Console VSCode Plugin se ainda não o fez
- Abra o console Nx no VSCode
- Clique em
Generate (UI)
na seção "Common Nx Commands" - Procure por
@aws/nx-plugin - ts#mcp-server
- Preencha os parâmetros obrigatórios
- project: inventory
- Clique em
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
Você também pode realizar uma execução simulada para ver quais arquivos seriam alterados
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
Isso adicionará
Arquivos atualizados do ts#mcp-server
Abaixo está uma lista de arquivos gerados pelo gerador ts#mcp-server
.
Directorypackages/
Directoryinventory/
Directorysrc/mcp-server/
- server.ts cria o servidor MCP
Directorytools/
- add.ts ferramenta de exemplo
Directoryresources/
- sample-guidance.ts recurso de exemplo
- stdio.ts ponto de entrada para MCP com transporte STDIO
- http.ts ponto de entrada para MCP com transporte HTTP streamable
- Dockerfile constrói a imagem para AgentCore Runtime
- rolldown.config.ts configuração para empacotar o servidor MCP para implantação no AgentCore
Directorycommon/constructs/
Directorysrc
Directoryapp/mcp-servers/inventory-mcp-server/
- inventory-mcp-server.ts construct para implantar seu servidor MCP de inventário no AgentCore Runtime
UI do Jogo: Website
Seção intitulada “UI do Jogo: Website”Agora vamos criar a UI que permitirá interagir com o jogo. Para isso, vamos criar um website chamado GameUI
seguindo os passos abaixo:
- Instale o Nx Console VSCode Plugin se ainda não o fez
- Abra o console Nx no VSCode
- Clique em
Generate (UI)
na seção "Common Nx Commands" - Procure por
@aws/nx-plugin - ts#react-website
- Preencha os parâmetros obrigatórios
- name: GameUI
- Clique em
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
Você também pode realizar uma execução simulada para ver quais arquivos seriam alterados
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
Você deve ver que alguns novos arquivos apareceram na sua árvore de arquivos.
Arquivos atualizados do ts#react-website
Abaixo está uma lista de todos os arquivos gerados pelo gerador ts#react-website
. Vamos examinar alguns dos arquivos-chave destacados na árvore de arquivos:
Directorypackages/
Directorycommon/
Directoryconstructs/
Directorysrc/
Directoryapp/ constructs CDK específicos do aplicativo
Directorystatic-websites/
- game-ui.ts construct CDK para criar sua Game UI
Directorycore/
- static-website.ts construct genérico para website estático
Directorygame-ui/
Directorypublic/
- …
Directorysrc/
Directorycomponents/
DirectoryAppLayout/
- index.ts layout geral da página: cabeçalho, rodapé, sidebar, etc
- navitems.ts itens de navegação da sidebar
Directoryhooks/
- useAppLayout.tsx permite definir dinamicamente notificações, estilo da página, etc
Directoryroutes/ rotas baseadas em arquivo do @tanstack/react-router
- index.tsx página raiz ’/’ redireciona para ‘/welcome’
- __root.tsx todas as páginas usam este componente como base
Directorywelcome/
- index.tsx
- config.ts
- main.tsx ponto de entrada do React
- routeTree.gen.ts atualizado automaticamente pelo @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 é o construct CDK que define nossa GameUI. Como visto, ele já configurou o caminho para o bundle gerado para nossa UI baseada em Vite. Isso significa que no tempo de build
, o empacotamento ocorre dentro do target de build do projeto game-ui e sua saída é usada aqui.
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 a instância do router para type safetydeclare 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 é o ponto de entrada onde o React é montado. Como mostrado, inicialmente apenas configura um @tanstack/react-router
em uma configuração de roteamento baseado em arquivo
. Isso significa que, enquanto o servidor de desenvolvimento estiver em execução, você pode simplesmente criar arquivos na pasta routes
e o @tanstack/react-router
criará a configuração de arquivo boilerplate para você, atualizando o arquivo routeTree.gen.ts
. Este arquivo mantém todas as rotas de forma type-safe, o que significa que ao usar <Link>
, a opção to
só mostrará rotas válidas. Para mais informações, consulte a documentação do @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> );}
Um componente que será renderizado ao navegar para a rota /welcome
. O @tanstack/react-router
gerenciará a Route
para você sempre que criar/mover este arquivo (desde que o servidor de desenvolvimento esteja em execução). Isso será mostrado em uma seção posterior deste tutorial.
UI do Jogo: Autenticação
Seção intitulada “UI do Jogo: Autenticação”Agora vamos configurar nossa Game UI para exigir acesso autenticado via Amazon Cognito seguindo os passos abaixo:
- Instale o Nx Console VSCode Plugin se ainda não o fez
- Abra o console Nx no VSCode
- Clique em
Generate (UI)
na seção "Common Nx Commands" - Procure por
@aws/nx-plugin - ts#react-website#auth
- Preencha os parâmetros obrigatórios
- cognitoDomain: game-ui
- project: @dungeon-adventure/game-ui
- allowSignup: true
- Clique em
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
Você também pode realizar uma execução simulada para ver quais arquivos seriam alterados
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
Você deve ver que alguns novos arquivos apareceram/mudaram na sua árvore de arquivos.
Arquivos atualizados do ts#react-website#auth
Abaixo está uma lista de todos os arquivos gerados/atualizados pelo gerador ts#react-website#auth
. Vamos examinar alguns dos arquivos-chave destacados na árvore de arquivos:
Directorypackages/
Directorycommon/
Directoryconstructs/
Directorysrc/
Directorycore/
- user-identity.ts construct CDK para criar pools de usuário/identidade
Directorytypes/
Directorysrc/
- runtime-config.ts atualizado para adicionar cognitoProps
Directorygame-ui/
Directorysrc/
Directorycomponents/
DirectoryAppLayout/
- index.tsx adiciona o usuário logado/logout ao cabeçalho
DirectoryCognitoAuth/
- index.ts gerencia login no Cognito
DirectoryRuntimeConfig/
- index.tsx busca o
runtime-config.json
e o fornece aos filhos via contexto
- index.tsx busca o
Directoryhooks/
- useRuntimeConfig.tsx
- main.tsx Atualizado para adicionar 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 a instância do router para type safetydeclare 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>, );
Os componentes RuntimeConfigProvider
e CognitoAuth
foram adicionados ao arquivo main.tsx
via transformação AST. Isso permite que o componente CognitoAuth
autentique com o Amazon Cognito buscando o runtime-config.json
que contém a configuração de conexão do Cognito necessária para fazer chamadas backend para o destino correto.
UI do Jogo: Conectar à Game API
Seção intitulada “UI do Jogo: Conectar à Game API”Agora vamos configurar nossa Game UI para se conectar à nossa Game API criada anteriormente:
- Instale o Nx Console VSCode Plugin se ainda não o fez
- Abra o console Nx no VSCode
- Clique em
Generate (UI)
na seção "Common Nx Commands" - Procure por
@aws/nx-plugin - api-connection
- Preencha os parâmetros obrigatórios
- sourceProject: @dungeon-adventure/game-ui
- targetProject: @dungeon-adventure/game-api
- Clique em
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
Você também pode realizar uma execução simulada para ver quais arquivos seriam alterados
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
Você deve ver que alguns novos arquivos apareceram/mudaram na sua árvore de arquivos.
Arquivos atualizados da conexão UI -> tRPC
Abaixo está uma lista de todos os arquivos gerados/atualizados pelo gerador api-connection
. Vamos examinar alguns dos arquivos-chave destacados na árvore de arquivos:
Directorypackages/
Directorygame-ui/
Directorysrc/
Directorycomponents/
- GameApiClientProvider.tsx configura o cliente GameAPI
Directoryhooks/
- useGameApi.tsx hooks para chamar a GameApi
- main.tsx injeta os providers do cliente trpc
- package.json
import { GameApiTRCPContext } from '../components/GameApiClientProvider';
export const useGameApi = GameApiTRCPContext.useTRPC;
Este hook usa a integração mais recente do React Query do tRPC, permitindo que usuários interajam com @tanstack/react-query
diretamente sem camadas adicionais de abstração. Para exemplos de como chamar APIs tRPC, consulte o guia de uso do 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 a instância do router para type safetydeclare 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>, );
O arquivo main.tsx
foi atualizado via transformação AST para injetar os providers do tRPC.
UI do Jogo: Infraestrutura
Seção intitulada “UI do Jogo: Infraestrutura”Agora o subprojeto final que precisamos criar é para a infraestrutura CDK. Para criar isso, siga os passos abaixo:
- Instale o Nx Console VSCode Plugin se ainda não o fez
- Abra o console Nx no VSCode
- Clique em
Generate (UI)
na seção "Common Nx Commands" - Procure por
@aws/nx-plugin - ts#infra
- Preencha os parâmetros obrigatórios
- name: infra
- Clique em
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
Você também pode realizar uma execução simulada para ver quais arquivos seriam alterados
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
Você deve ver que alguns novos arquivos apareceram/mudaram na sua árvore de arquivos.
Arquivos atualizados do ts#infra
Abaixo está uma lista de todos os arquivos gerados/atualizados pelo gerador ts#infra
. Vamos examinar alguns dos arquivos-chave destacados na árvore de arquivos:
Directorypackages/
Directorycommon/
Directoryconstructs/
Directorysrc/
Directorycore/
- checkov.ts
- index.ts
Directoryinfra
Directorysrc/
Directorystages/
- application-stage.ts stacks CDK definidas aqui
Directorystacks/
- application-stack.ts recursos CDK definidos aqui
- index.ts
- main.ts ponto de entrada que define todos os stages
- cdk.json
- project.json
- …
- package.json
- tsconfig.json adiciona referências
- tsconfig.base.json adiciona alias
import { ApplicationStage } from './stacks/application-stage.js';import { App } from ':dungeon-adventure/common-constructs';
const app = new App();
// Use isto para implantar seu próprio ambiente sandbox (assume suas credenciais CLI)new ApplicationStage(app, 'dungeon-adventure-infra-sandbox', { env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION, },});
app.synth();
Este é o ponto de entrada para sua aplicação 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);
// O código que define sua stack vai aqui }}
Este é onde instanciaremos nossos constructs CDK para construir nosso jogo dungeon adventure.
Atualizar nossa infraestrutura
Seção intitulada “Atualizar nossa infraestrutura”Vamos atualizar nosso packages/infra/src/stacks/application-stack.ts
para instanciar alguns de nossos constructs já gerados:
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'); }}
Note que fornecemos integrações padrão para nossa Game API. Por padrão, cada operação em nossa API é mapeada para uma função lambda individual para lidar com essa operação.
Construindo nosso código
Seção intitulada “Construindo nosso código”Comandos Nx
Targets únicos vs múltiplos
Seção intitulada “Targets únicos vs múltiplos”O comando run-many
executará um target em múltiplos subprojetos listados (--all
os seleciona todos). Ele garantirá que dependências sejam executadas na ordem correta.
Você também pode disparar um build (ou qualquer outra tarefa) para um target de projeto único executando o target diretamente no projeto. Por exemplo, se quisermos buildar o projeto @dungeon-adventure/infra
, você pode executar:
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
Você também pode omitir o escopo e usar a sintaxe abreviada do Nx se preferir:
pnpm nx build infra
yarn nx build infra
npx nx build infra
bunx nx build infra
Visualizando suas dependências
Seção intitulada “Visualizando suas dependências”Você também pode visualizar suas dependências via:
pnpm nx graph
yarn nx graph
npx nx graph
bunx nx graph

O Nx depende de cache para reutilizar artefatos de builds anteriores e acelerar o desenvolvimento. É necessária alguma configuração para que isso funcione corretamente e pode haver casos onde você queira executar um build sem usar o cache. Para isso, simplesmente adicione o argumento --skip-nx-cache
ao seu comando. Por exemplo:
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
Se por qualquer motivo você quiser limpar seu cache (armazenado na pasta .nx
), você pode executar:
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
Você deve ser questionado com o seguinte:
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
Esta mensagem indica que o NX detectou alguns arquivos que podem ser atualizados automaticamente. Neste caso, está se referindo aos arquivos tsconfig.json
que não têm referências de projeto TypeScript configuradas em projetos dependentes. Selecione a opção Yes, sync the changes and run the tasks para prosseguir. Você deve notar que todos os erros de importação relacionados à IDE são resolvidos automaticamente, pois o gerador de sincronização adicionará as referências TypeScript faltantes automaticamente!
Todos os artefatos construídos estão agora disponíveis dentro da pasta dist/
na raiz do monorepo. Esta é uma prática padrão ao usar projetos gerados pelo @aws/nx-plugin
, pois não polui sua árvore de arquivos com arquivos gerados. Caso queira limpar seus arquivos, você pode simplesmente deletar a pasta dist/
sem se preocupar com arquivos gerados espalhados pela árvore de arquivos.
Parabéns! Você criou todos os subprojetos necessários para começar a implementar o núcleo do nosso jogo Dunegeon Adventure. 🎉🎉🎉