diff --git a/02-use-cases/video-games-sales-assistant/README.md b/02-use-cases/video-games-sales-assistant/README.md index 1d808be19..829724e25 100644 --- a/02-use-cases/video-games-sales-assistant/README.md +++ b/02-use-cases/video-games-sales-assistant/README.md @@ -1,7 +1,7 @@ # Deploying a Conversational Data Analyst Assistant Solution with Amazon Bedrock AgentCore > [!IMPORTANT] -> **🚀 Ready-to-Deploy Agent Web Application**: Use this reference solution to build other agent-powered web applications across different industries. Extend the agent capabilities by adding custom tools for specific industry workflows and adapt it to various business domains. +> **🚀 Ready-to-Deploy Agent Web Application**: Use this reference solution to build agent-powered web applications across different industries. Adapt it to your business domain by adding custom agent tools for specific workflows. To accelerate development, use **[Kiro](https://kiro.dev/)** with its **[Powers](https://kiro.dev/powers/)** for Strands Agents SDK, Amazon Bedrock AgentCore, and AWS Amplify, along with the **[AWS CDK MCP Server](https://awslabs.github.io/mcp/servers/cdk-mcp-server)** for infrastructure guidance — so you can extend this solution **without starting from scratch**. This solution provides a Generative AI application reference that allows users to interact with data through a natural language interface. The solution leverages **[Amazon Bedrock AgentCore](https://aws.amazon.com/bedrock/agentcore/)**, a managed service that enables you to deploy, run, and scale custom agent applications, along with the **[Strands Agents SDK](https://strandsagents.com/)** to build an agent that connects to a PostgreSQL database, providing data analysis capabilities through a Next.js web application built with **[AWS Amplify Gen 2](https://docs.amplify.aws/)**. @@ -41,7 +41,10 @@ The AWS CDK stack deploys and configures the following managed services: **Amazon Bedrock AgentCore Resources:** - **[AgentCore Runtime](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/agents-tools-runtime.html)**: Provides the managed execution environment with invocation endpoints (`/invocations`) and health monitoring (`/ping`) for your agent instances -- **[AgentCore Memory](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/memory.html)**: A fully managed service that gives AI agents the ability to remember, learn, and evolve through interactions by capturing events, transforming them into memories, and retrieving relevant context when needed +- **[AgentCore Memory](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/memory.html)**: A fully managed service that gives AI agents the ability to remember, learn, and evolve through interactions. Configured with: + - **Short-term memory (STM)**: Event-based conversation history scoped per user and session, providing immediate conversational context within a session + - **Long-term memory (LTM)**: A semantic "Facts" strategy that asynchronously extracts knowledge from conversations and stores it in per-user namespaces (`/facts/{actorId}`). Facts persist across sessions and are retrieved via vector similarity search, enabling the agent to recall insights from previous conversations +- **Observability**: Runtime application logs and memory extraction logs delivered to CloudWatch Logs, plus runtime traces to AWS X-Ray — all with 14-day retention The AgentCore infrastructure handles all storage complexity and provides efficient retrieval without requiring developers to manage underlying infrastructure, ensuring continuity and traceability across agent interactions. @@ -57,10 +60,11 @@ All configuration values (database ARNs, secret ARNs, model ID, etc.) are passed ### Amplify Deployment for the Front-End Application - **Next.js Web Application (Amplify Gen 2)**: Delivers the user interface for the assistant - - Uses Amazon Cognito (via Amplify Gen 2) for user authentication and IAM permissions — no manual IAM configuration needed + - Uses Amazon Cognito (via Amplify Gen 2) for user authentication and IAM permissions — no manual IAM configuration needed. Each authenticated user's Cognito `sub` is used as the `actorId` for memory, ensuring isolated per-user memory namespaces - The application invokes Amazon Bedrock AgentCore for interacting with the assistant (client-side streaming) - For chart generation, the application directly invokes the Claude Haiku 4.5 model (client-side) - DynamoDB query results are fetched through a Next.js API route (server-side) + - A Memory Facts panel lets users view the long-term knowledge extracted from their conversations by AgentCore Memory ### Strands Agent Features @@ -84,7 +88,7 @@ The **user interaction workflow** operates as follows: - The web application sends user business questions to the AgentCore Invoke (via client-side streaming) - The Strands Agent (powered by Claude Haiku 4.5) processes natural language and determines when to execute database queries - The agent's built-in tools execute SQL queries against the Aurora PostgreSQL database and formulate an answer to the question -- AgentCore Memory captures session interactions and retrieves previous conversations for context +- AgentCore Memory manages conversation context through the `AgentCoreMemorySessionManager` integration. STM provides conversation continuity within a session (scoped by `sessionId`), while LTM retrieves relevant facts from the `/facts/{actorId}` namespace across all past sessions for that user. LTM extraction is asynchronous (20-40 seconds after events are saved) - After the agent's streaming response completes, the raw data query results are fetched from DynamoDB through a Next.js API route to display both the answer and the corresponding records - For chart generation, the application invokes a model (powered by Claude Haiku 4.5) to analyze the agent's answer and raw data query results to generate the necessary data to render an appropriate chart visualization @@ -105,23 +109,29 @@ The deployment consists of two main steps: The following images showcase a conversational experience analysis that includes: natural language answers, the reasoning process used by the LLM to generate SQL queries, the database records retrieved from those queries, and the resulting chart visualizations. -![Video Games Sales Assistant](./images/preview.png) +- **AgentCore data analyst assistant welcome with Memory Facts access** -- **Conversational interface with an agent responding to user questions** +![Welcome screen with AgentCore branding](./images/preview.png) -![Video Games Sales Assistant](./images/preview1.png) +- **Long-term Memory Facts from AgentCore Memory** -- **Raw query results displayed in tabular format** +![Memory Facts panel with extracted knowledge](./images/preview1.png) -![Video Games Sales Assistant](./images/preview2.png) +- **Conversational agent with tool use and reasoning** -- **Chart visualization generated from the agent's answer and the data query results (created using [Apexcharts](https://apexcharts.com/))**. +![Agent conversation with SQL query execution](./images/preview2.png) -![Video Games Sales Assistant](./images/preview3.png) +- **Raw query results in tabular format** -- **Summary and conclusion derived from the data analysis conversation** +![Query results displayed as data table](./images/preview3.png) -![Video Games Sales Assistant](./images/preview4.png) +- **Auto-generated chart from answer and data** + +![Chart visualization from query results](./images/preview4.png) + +- **Conversation summary and data analysis conclusion** + +![Summary and conclusion of analysis conversation](./images/preview5.png) ## Thank You diff --git a/02-use-cases/video-games-sales-assistant/amplify-video-games-sales-assistant-agentcore-strands/.env.local.example b/02-use-cases/video-games-sales-assistant/amplify-video-games-sales-assistant-agentcore-strands/.env.local.example index 7909aa61d..024a464b4 100644 --- a/02-use-cases/video-games-sales-assistant/amplify-video-games-sales-assistant-agentcore-strands/.env.local.example +++ b/02-use-cases/video-games-sales-assistant/amplify-video-games-sales-assistant-agentcore-strands/.env.local.example @@ -23,8 +23,8 @@ AGENT_RUNTIME_ARN="arn:aws:bedrock-agentcore:us-east-1:000000000000:runtime/Your # The endpoint name for the agent (usually "DEFAULT") AGENT_ENDPOINT_NAME="DEFAULT" -# Number of conversation turns to keep in memory for context -LAST_K_TURNS="10" +# The AgentCore Memory ID for long-term memory +MEMORY_ID="" # ────────────────────────────────────────────────────────────── # Assistant UI Configuration diff --git a/02-use-cases/video-games-sales-assistant/amplify-video-games-sales-assistant-agentcore-strands/README.md b/02-use-cases/video-games-sales-assistant/amplify-video-games-sales-assistant-agentcore-strands/README.md index 9bdc67c1f..829724e25 100644 --- a/02-use-cases/video-games-sales-assistant/amplify-video-games-sales-assistant-agentcore-strands/README.md +++ b/02-use-cases/video-games-sales-assistant/amplify-video-games-sales-assistant-agentcore-strands/README.md @@ -1,277 +1,140 @@ -# Front-End Implementation - Integrating AgentCore with a Ready-to-Use Data Analyst Assistant Application (Next.js) - -This tutorial guides you through setting up a Next.js web application that integrates with your **[Amazon Bedrock AgentCore](https://aws.amazon.com/bedrock/agentcore/)** deployment, creating a Data Analyst Assistant for Video Game Sales. - -This is the Next.js + Amplify Gen 2 version of the original React application. It uses the same AgentCore backend but replaces the React + Amplify Gen 1 frontend with a modern Next.js App Router architecture, Tailwind CSS, and Amplify Gen 2 for authentication and IAM. - -> [!NOTE] -> **Working Directory**: Make sure you are in the `amplify-video-games-sales-assistant-agentcore-strands/` folder before starting this tutorial. All commands in this guide should be executed from this directory. - -## Overview - -By the end of this tutorial, you'll have a fully functional Generative AI web application that allows users to interact with a Data Analyst Assistant interface powered by Amazon Bedrock AgentCore. - -The application consists of two main components: - -- **Next.js Web Application**: Provides the user interface with server components, protected routes, and streaming chat -- **Amazon Bedrock AgentCore Integration:** - - Uses your AgentCore deployment for data analysis and natural language processing - - The application invokes Amazon Bedrock AgentCore for interacting with the assistant - - Directly invokes Claude Haiku 4.5 model for chart generation and visualization +# Deploying a Conversational Data Analyst Assistant Solution with Amazon Bedrock AgentCore > [!IMPORTANT] -> This sample application is for demonstration purposes only and is not production-ready. Please validate the code against your organization's security best practices. - -## Prerequisites - -Before you begin, ensure you have: - -- [Node.js version 18+](https://nodejs.org/en/download/package-manager) -- [pnpm](https://pnpm.io/installation) -- [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) configured with credentials -- A deployed Amazon Bedrock AgentCore runtime (from the CDK stack in this repository) - -Verify credentials: - -```bash -aws sts get-caller-identity -``` +> **🚀 Ready-to-Deploy Agent Web Application**: Use this reference solution to build agent-powered web applications across different industries. Adapt it to your business domain by adding custom agent tools for specific workflows. To accelerate development, use **[Kiro](https://kiro.dev/)** with its **[Powers](https://kiro.dev/powers/)** for Strands Agents SDK, Amazon Bedrock AgentCore, and AWS Amplify, along with the **[AWS CDK MCP Server](https://awslabs.github.io/mcp/servers/cdk-mcp-server)** for infrastructure guidance — so you can extend this solution **without starting from scratch**. -## Set Up the Front-End Application +This solution provides a Generative AI application reference that allows users to interact with data through a natural language interface. The solution leverages **[Amazon Bedrock AgentCore](https://aws.amazon.com/bedrock/agentcore/)**, a managed service that enables you to deploy, run, and scale custom agent applications, along with the **[Strands Agents SDK](https://strandsagents.com/)** to build an agent that connects to a PostgreSQL database, providing data analysis capabilities through a Next.js web application built with **[AWS Amplify Gen 2](https://docs.amplify.aws/)**. -### Install Dependencies +
+Conversational Data Analyst Assistant Solution with Amazon Bedrock AgentCore +
-Navigate to the application folder and install the dependencies: +🤖 A Data Analyst Assistant offers an approach to data analysis that enables enterprises to interact with their structured data through natural language conversations rather than complex SQL queries. This kind of assistant provides an intuitive question-answering for data analysis conversations and can be improved by offering data visualizations to enhance the user experience. -```bash -pnpm install -``` +✨ This solution enables users to: -## Configure Environment Variables +- Ask questions about video game sales data in natural language +- Receive AI-generated responses based on SQL queries to a PostgreSQL database +- View query results in tabular format +- Explore data through automatically generated visualizations +- Get insights and analysis from the AI assistant -Run the following script to automatically copy the example file, fetch the CDK output values, and update your `.env.local`: +🚀 This reference solution can help you explore use cases like: -```bash -cp .env.local.example .env.local +- Empower analysts with real-time business intelligence +- Provide quick answers to C-level executives for common business questions +- Unlock new revenue streams through data monetization (consumer behavior, audience segmentation) +- Optimize infrastructure through performance insights -export STACK_NAME=CdkDataAnalystAssistantAgentcoreStrandsStack +## Solution Overview -export QUESTION_ANSWERS_TABLE_NAME=$(aws cloudformation describe-stacks --stack-name "$STACK_NAME" --query "Stacks[0].Outputs[?OutputKey=='QuestionAnswersTableName'].OutputValue" --output text) -export AGENT_RUNTIME_ARN=$(aws cloudformation describe-stacks --stack-name "$STACK_NAME" --query "Stacks[0].Outputs[?OutputKey=='AgentRuntimeArn'].OutputValue" --output text) +The following architecture diagram illustrates a reference solution for a generative AI data analyst assistant that is built using Strands Agents SDK and powered by Amazon Bedrock. This assistant enables users to access structured data that is stored in a PostgreSQL database through a question-answering interface. -sed -i.bak \ - -e "s|AGENT_RUNTIME_ARN=.*|AGENT_RUNTIME_ARN=\"${AGENT_RUNTIME_ARN}\"|" \ - -e "s|QUESTION_ANSWERS_TABLE_NAME=.*|QUESTION_ANSWERS_TABLE_NAME=\"${QUESTION_ANSWERS_TABLE_NAME}\"|" \ - .env.local && rm -f .env.local.bak - -echo "✅ .env.local configured successfully" -cat .env.local -``` - -> [!NOTE] -> All variables are server-side only (no `NEXT_PUBLIC_` prefix). Client components receive values as props from server component wrappers — they are never exposed to the browser. You can also manually edit `.env.local` using `.env.local.example` as a reference. - -## Deploy Authentication and IAM Permissions - -This project uses **Amplify Gen 2** to deploy authentication (Cognito User Pool + Identity Pool) and IAM policies. Unlike the original React app where you manually configure IAM roles, Amplify Gen 2 handles everything through code in `amplify/backend.ts`. - -### Start the Amplify Sandbox - -The sandbox deploys a personal cloud environment to your AWS account. Run in a separate terminal: - -```bash -QUESTION_ANSWERS_TABLE_NAME="$QUESTION_ANSWERS_TABLE_NAME" \ -AGENT_RUNTIME_ARN="$AGENT_RUNTIME_ARN" \ -pnpm ampx sandbox -``` - -These environment variables are read at CDK synth time by `amplify/backend.ts` to scope IAM policies. Wait until you see: - -``` -✔ Deployment completed -File written: amplify_outputs.json -Watching for file changes... -``` - -Once `amplify_outputs.json` is generated, the sandbox has finished its work. You can safely press `Ctrl+C` to stop the watcher — the cloud resources (Cognito, IAM policies) stay deployed. Only keep it running if you plan to make changes to files in `amplify/` and want them hot-deployed. - -> [!NOTE] -> The sandbox automatically creates a Cognito User Pool, Identity Pool, and attaches three inline IAM policies to the authenticated role: -> - **DynamoDBReadPolicy** — Read access to the query results table -> - **BedrockAgentCorePolicy** — Permission to invoke the AgentCore runtime -> - **BedrockInvokeModelPolicy** — Permission to invoke Bedrock models for chart generation -> -> No manual IAM configuration is needed. +![Video Games Sales Assistant](./images/gen-ai-assistant-diagram.png) -## Test Your Data Analyst Assistant - -Start the application locally: - -```bash -pnpm dev -``` - -The application will open in your browser at [http://localhost:3000](http://localhost:3000). - -First-time access: -1. **Create Account**: Click "Create Account" and use your email address -2. **Verify Email**: Check your email for a verification code -3. **Sign In**: Use your email and password to sign in - -Try these sample questions to test the assistant: - -``` -Hello! -``` - -``` -How can you help me? -``` - -``` -What is the structure of the data? -``` - -``` -Which developers tend to get the best reviews? -``` - -``` -What were the total sales for each region between 2000 and 2010? Give me the data in percentages. -``` - -``` -What were the best-selling games in the last 10 years? -``` - -``` -What are the best-selling video game genres? -``` - -``` -Give me the top 3 game publishers. -``` - -``` -Give me the top 3 video games with the best reviews and the best sales. -``` - -``` -Which is the year with the highest number of games released? -``` +> [!IMPORTANT] +> This sample application is meant for demo purposes and is not production ready. Please make sure to validate the code with your organizations security best practices. -``` -Which are the most popular consoles and why? -``` +### CDK Infrastructure Deployment -``` -Give me a short summary and conclusion of our conversation. -``` +The AWS CDK stack deploys and configures the following managed services: -## Deploy Your Application with Amplify Hosting +**Amazon Bedrock AgentCore Resources:** +- **[AgentCore Runtime](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/agents-tools-runtime.html)**: Provides the managed execution environment with invocation endpoints (`/invocations`) and health monitoring (`/ping`) for your agent instances +- **[AgentCore Memory](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/memory.html)**: A fully managed service that gives AI agents the ability to remember, learn, and evolve through interactions. Configured with: + - **Short-term memory (STM)**: Event-based conversation history scoped per user and session, providing immediate conversational context within a session + - **Long-term memory (LTM)**: A semantic "Facts" strategy that asynchronously extracts knowledge from conversations and stores it in per-user namespaces (`/facts/{actorId}`). Facts persist across sessions and are retrieved via vector similarity search, enabling the agent to recall insights from previous conversations +- **Observability**: Runtime application logs and memory extraction logs delivered to CloudWatch Logs, plus runtime traces to AWS X-Ray — all with 14-day retention -To deploy your application you can use AWS Amplify Hosting. +The AgentCore infrastructure handles all storage complexity and provides efficient retrieval without requiring developers to manage underlying infrastructure, ensuring continuity and traceability across agent interactions. -> [!IMPORTANT] -> Amplify Hosting requires a Git-based repository. This project must be pushed to its own repository on one of the supported providers: **GitHub**, **Bitbucket**, **GitLab**, or **AWS CodeCommit**. If this project lives inside a monorepo, push only this folder as a standalone repo before connecting it to Amplify. See [Getting started with Amplify Hosting](https://docs.aws.amazon.com/amplify/latest/userguide/getting-started.html) for details. +**Data Source and VPC Infrastructure:** +- **VPC with Public and Private Subnets**: Network isolation and security for database resources +- **Amazon Aurora Serverless v2 PostgreSQL**: Stores the video game sales data with RDS Data API integration +- **Amazon DynamoDB**: Stores raw query results for data analysis audit trails +- **AWS Secrets Manager**: Secure storage for database credentials (admin and read-only) +- **Amazon S3**: Import bucket for loading data into Aurora PostgreSQL -### 1. Connect Repository +All configuration values (database ARNs, secret ARNs, model ID, etc.) are passed directly as environment variables to the AgentCore Runtime — no SSM Parameter Store required. -Open the [Amplify Console](https://console.aws.amazon.com/amplify/), click **Create new app**, and select your Git provider and branch. +### Amplify Deployment for the Front-End Application -### 2. Configure Environment Variables +- **Next.js Web Application (Amplify Gen 2)**: Delivers the user interface for the assistant + - Uses Amazon Cognito (via Amplify Gen 2) for user authentication and IAM permissions — no manual IAM configuration needed. Each authenticated user's Cognito `sub` is used as the `actorId` for memory, ensuring isolated per-user memory namespaces + - The application invokes Amazon Bedrock AgentCore for interacting with the assistant (client-side streaming) + - For chart generation, the application directly invokes the Claude Haiku 4.5 model (client-side) + - DynamoDB query results are fetched through a Next.js API route (server-side) + - A Memory Facts panel lets users view the long-term knowledge extracted from their conversations by AgentCore Memory -Under **App settings → Advanced settings → Environment variables**, add: +### Strands Agent Features -| Variable | Required | Purpose | Default | -|---|---|---|---| -| `APP_NAME` | Yes | Display name in the UI header | `Data Analyst Assistant` | -| `APP_DESCRIPTION` | Yes | Subtitle on the sign-in page | `Video Games Sales Data Analyst powered by Amazon Bedrock AgentCore` | -| `AGENT_RUNTIME_ARN` | Yes | Bedrock AgentCore runtime ARN | — | -| `AGENT_ENDPOINT_NAME` | No | Agent endpoint | `DEFAULT` | -| `LAST_K_TURNS` | No | Conversation memory depth | `10` | -| `WELCOME_MESSAGE` | No | Chat welcome message | `I'm your AI Data Analyst, crunching data for insights.` | -| `MAX_LENGTH_INPUT_SEARCH` | No | Max characters for assistant input | `500` | -| `MODEL_ID_FOR_CHART` | No | Bedrock model for chart generation | `us.anthropic.claude-haiku-4-5-20251001-v1:0` | -| `QUESTION_ANSWERS_TABLE_NAME` | Yes | DynamoDB table for agent query results | — | +| Feature | Description | +|----------|----------| +| Native Tools | current_time - A built-in Strands tool that provides the current date and time information based on user's timezone. | +| Custom Tools | get_tables_information - A custom tool that retrieves metadata about the database tables, including their structure, columns, and relationships, to help the agent understand the database schema.
execute_sql_query - A custom tool that allows the agent to run SQL queries against the PostgreSQL database based on the user's natural language questions, retrieving the requested data for analysis. | +| Model Provider | Amazon Bedrock | > [!NOTE] -> These environment variables are passed to the Next.js runtime via `next.config.mjs`. If you add new server-side env vars, make sure to also register them in that file for Amplify Hosting SSR to pick them up. - -### 3. Deploy +> The Next.js Web Application uses Amazon Cognito (deployed by Amplify Gen 2) for user authentication and permissions management, providing secure access to Amazon Bedrock AgentCore and Amazon DynamoDB services through authenticated user roles. -Review and click **Save and deploy**. Amplify runs the build pipeline defined in `amplify.yml`: +> [!TIP] +> You can also change the data source to connect to your preferred database engine by adapting the Agent's instructions and tool implementations. -- **Backend phase** — Deploys the CDK stack (Cognito + IAM policies) -- **Frontend phase** — Builds the Next.js app with environment variables baked into the server-side bundle +> [!IMPORTANT] +> Enhance AI safety and compliance by implementing **[Amazon Bedrock Guardrails](https://aws.amazon.com/bedrock/guardrails/)** for your AI applications with the seamless integration offered by **[Strands Agents SDK](https://strandsagents.com/latest/user-guide/safety-security/guardrails/)**. -## Clean Up Resources +The **user interaction workflow** operates as follows: -To avoid incurring ongoing costs, remove the resources created by this tutorial: +- The web application sends user business questions to the AgentCore Invoke (via client-side streaming) +- The Strands Agent (powered by Claude Haiku 4.5) processes natural language and determines when to execute database queries +- The agent's built-in tools execute SQL queries against the Aurora PostgreSQL database and formulate an answer to the question +- AgentCore Memory manages conversation context through the `AgentCoreMemorySessionManager` integration. STM provides conversation continuity within a session (scoped by `sessionId`), while LTM retrieves relevant facts from the `/facts/{actorId}` namespace across all past sessions for that user. LTM extraction is asynchronous (20-40 seconds after events are saved) +- After the agent's streaming response completes, the raw data query results are fetched from DynamoDB through a Next.js API route to display both the answer and the corresponding records +- For chart generation, the application invokes a model (powered by Claude Haiku 4.5) to analyze the agent's answer and raw data query results to generate the necessary data to render an appropriate chart visualization -1. **Delete the Amplify Hosting app**: Amplify Console → App settings → General settings → **Delete app**. This removes hosting and the backend stack (Cognito, IAM roles). +## Deployment Instructions -2. **Delete the sandbox** (if still running): +The deployment consists of two main steps: -```bash -pnpm ampx sandbox delete -``` +1. **Back-End Deployment - [Amazon Bedrock AgentCore and Data Source Deployment with CDK](./cdk-data-analyst-assistant-agentcore-strands/)** +2. **Front-End Implementation - [Integrating AgentCore with a Ready-to-Use Data Analyst Assistant Application (Next.js)](./amplify-video-games-sales-assistant-agentcore-strands/)** > [!NOTE] -> These steps remove only the front-end resources (Cognito, IAM roles, hosting). External resources like DynamoDB tables and Bedrock AgentCore runtimes are managed by the CDK stack and must be deleted separately. - -## AWS Service Calls and Routes +> *It is recommended to use the Oregon (us-west-2) or N. Virginia (us-east-1) regions to deploy the application.* -### Routes +> [!IMPORTANT] +> Remember to clean up resources after testing to avoid unnecessary costs by following the clean-up steps provided. -| Path | Method | Access | Description | -|---|---|---|---| -| `/` | GET | Public | Redirects to `/app` | -| `/app` | GET | Public | Sign in / sign up (Amplify Authenticator) | -| `/app/assistant` | GET | Protected | AI Assistant chat interface | -| `/api/agent/query-results` | POST | Authenticated | Fetch query results from DynamoDB | - -### Client-Side AWS Calls - -These run directly in the browser using Cognito Identity Pool credentials obtained via `src/lib/aws-client.ts`. - -| Service | SDK Client | File | Purpose | -|---|---|---|---| -| Bedrock AgentCore | `BedrockAgentCoreClient` | `agent-core-call.ts` | Streaming agent invocation — sends user questions and processes real-time response chunks | -| Bedrock Runtime | `BedrockRuntimeClient` | `aws-calls.ts` | Chart generation — sends agent answers to Claude Haiku to produce ApexCharts configurations | - -### Server-Side AWS Calls +## Application Features -These run on the Next.js server through API routes. +The following images showcase a conversational experience analysis that includes: natural language answers, the reasoning process used by the LLM to generate SQL queries, the database records retrieved from those queries, and the resulting chart visualizations. -| Service | SDK Client | File | Purpose | -|---|---|---|---| -| DynamoDB | `DynamoDBClient` | `api/agent/query-results/route.ts` | Queries the results table by `queryUuid` to fetch SQL results stored by the agent | +- **AgentCore data analyst assistant welcome with Memory Facts access** -## Application Features +![Welcome screen with AgentCore branding](./images/preview.png) -Congratulations! Your Data Analyst Assistant can provide you with the following conversational experience: +- **Long-term Memory Facts from AgentCore Memory** -![Video Games Sales Assistant](../images/preview.png) +![Memory Facts panel with extracted knowledge](./images/preview1.png) -- **Conversational interface with an agent responding to user questions** +- **Conversational agent with tool use and reasoning** -![Video Games Sales Assistant](../images/preview1.png) +![Agent conversation with SQL query execution](./images/preview2.png) -- **Raw query results displayed in tabular format** +- **Raw query results in tabular format** -![Video Games Sales Assistant](../images/preview2.png) +![Query results displayed as data table](./images/preview3.png) -- **Chart visualization generated from the agent's answer and the data query results (created using [Apexcharts](https://apexcharts.com/))** +- **Auto-generated chart from answer and data** -![Video Games Sales Assistant](../images/preview3.png) +![Chart visualization from query results](./images/preview4.png) -- **Summary and conclusion derived from the data analysis conversation** +- **Conversation summary and data analysis conclusion** -![Video Games Sales Assistant](../images/preview4.png) +![Summary and conclusion of analysis conversation](./images/preview5.png) ## Thank You ## License -This project is licensed under the Apache-2.0 License. +This project is licensed under the Apache-2.0 License. \ No newline at end of file diff --git a/02-use-cases/video-games-sales-assistant/amplify-video-games-sales-assistant-agentcore-strands/amplify/backend.ts b/02-use-cases/video-games-sales-assistant/amplify-video-games-sales-assistant-agentcore-strands/amplify/backend.ts index 6fdac771a..0b03b3690 100644 --- a/02-use-cases/video-games-sales-assistant/amplify-video-games-sales-assistant-agentcore-strands/amplify/backend.ts +++ b/02-use-cases/video-games-sales-assistant/amplify-video-games-sales-assistant-agentcore-strands/amplify/backend.ts @@ -83,6 +83,22 @@ if (AGENT_RUNTIME_ARN) { ); } +// ─── Bedrock AgentCore Memory: read long-term memory facts ────────────────── +authenticatedRole.attachInlinePolicy( + new Policy(stack, 'BedrockAgentCoreMemoryPolicy', { + statements: [ + new PolicyStatement({ + effect: Effect.ALLOW, + actions: [ + 'bedrock-agentcore:ListMemoryRecords', + 'bedrock-agentcore:RetrieveMemoryRecords', + ], + resources: ['*'], + }), + ], + }) +); + // ─── Bedrock: invoke model for chart generation ───────────────────────────── // Cross-region inference profiles (us.anthropic.*) route requests to multiple // regions. The policy must cover all regions the profile may use. diff --git a/02-use-cases/video-games-sales-assistant/amplify-video-games-sales-assistant-agentcore-strands/next.config.mjs b/02-use-cases/video-games-sales-assistant/amplify-video-games-sales-assistant-agentcore-strands/next.config.mjs index 2e09aa094..e268b37fb 100644 --- a/02-use-cases/video-games-sales-assistant/amplify-video-games-sales-assistant-agentcore-strands/next.config.mjs +++ b/02-use-cases/video-games-sales-assistant/amplify-video-games-sales-assistant-agentcore-strands/next.config.mjs @@ -7,11 +7,11 @@ const nextConfig = { APP_DESCRIPTION: process.env.APP_DESCRIPTION, AGENT_RUNTIME_ARN: process.env.AGENT_RUNTIME_ARN, AGENT_ENDPOINT_NAME: process.env.AGENT_ENDPOINT_NAME, - LAST_K_TURNS: process.env.LAST_K_TURNS, WELCOME_MESSAGE: process.env.WELCOME_MESSAGE, MAX_LENGTH_INPUT_SEARCH: process.env.MAX_LENGTH_INPUT_SEARCH, MODEL_ID_FOR_CHART: process.env.MODEL_ID_FOR_CHART, QUESTION_ANSWERS_TABLE_NAME: process.env.QUESTION_ANSWERS_TABLE_NAME, + MEMORY_ID: process.env.MEMORY_ID, }, // Required for AWS Amplify UI React components diff --git a/02-use-cases/video-games-sales-assistant/amplify-video-games-sales-assistant-agentcore-strands/public/images/agentcore-memory.png b/02-use-cases/video-games-sales-assistant/amplify-video-games-sales-assistant-agentcore-strands/public/images/agentcore-memory.png new file mode 100644 index 000000000..ca08cbcf9 Binary files /dev/null and b/02-use-cases/video-games-sales-assistant/amplify-video-games-sales-assistant-agentcore-strands/public/images/agentcore-memory.png differ diff --git a/02-use-cases/video-games-sales-assistant/amplify-video-games-sales-assistant-agentcore-strands/src/app/api/agent/query-results/route.ts b/02-use-cases/video-games-sales-assistant/amplify-video-games-sales-assistant-agentcore-strands/src/app/api/agent/query-results/route.ts index 91534a6df..905ab8937 100644 --- a/02-use-cases/video-games-sales-assistant/amplify-video-games-sales-assistant-agentcore-strands/src/app/api/agent/query-results/route.ts +++ b/02-use-cases/video-games-sales-assistant/amplify-video-games-sales-assistant-agentcore-strands/src/app/api/agent/query-results/route.ts @@ -5,7 +5,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { DynamoDBClient, QueryCommand } from '@aws-sdk/client-dynamodb'; -import { getAwsClient } from '@/lib/aws-client'; +import { getAwsClient } from '@/utils/aws-client'; export async function POST(req: NextRequest) { try { diff --git a/02-use-cases/video-games-sales-assistant/amplify-video-games-sales-assistant-agentcore-strands/src/app/app/assistant/AssistantClient.tsx b/02-use-cases/video-games-sales-assistant/amplify-video-games-sales-assistant-agentcore-strands/src/app/app/assistant/AssistantClient.tsx index 89185ad84..ffd903f5c 100644 --- a/02-use-cases/video-games-sales-assistant/amplify-video-games-sales-assistant-agentcore-strands/src/app/app/assistant/AssistantClient.tsx +++ b/02-use-cases/video-games-sales-assistant/amplify-video-games-sales-assistant-agentcore-strands/src/app/app/assistant/AssistantClient.tsx @@ -56,22 +56,66 @@ function AssistantContent({ assistantConfig }: { assistantConfig: AssistantConfi }} >

{assistantConfig.appName}

-
- {userName} +
+ {/* Memory button */} + {assistantConfig.memoryId && ( + + )} + + {/* Separator */} +
+ + {/* User name */} + {userName} + + {/* Sign out button */} diff --git a/02-use-cases/video-games-sales-assistant/amplify-video-games-sales-assistant-agentcore-strands/src/app/app/assistant/page.tsx b/02-use-cases/video-games-sales-assistant/amplify-video-games-sales-assistant-agentcore-strands/src/app/app/assistant/page.tsx index 07cc6f0c7..dc81b62d1 100644 --- a/02-use-cases/video-games-sales-assistant/amplify-video-games-sales-assistant-agentcore-strands/src/app/app/assistant/page.tsx +++ b/02-use-cases/video-games-sales-assistant/amplify-video-games-sales-assistant-agentcore-strands/src/app/app/assistant/page.tsx @@ -6,11 +6,11 @@ export default function AssistantPage() { const assistantConfig: AssistantConfig = { agentRuntimeArn: process.env.AGENT_RUNTIME_ARN || '', agentEndpointName: process.env.AGENT_ENDPOINT_NAME || 'DEFAULT', - lastKTurns: parseInt(process.env.LAST_K_TURNS || '10', 10), welcomeMessage: process.env.WELCOME_MESSAGE || "I'm your AI Data Analyst, crunching data for insights.", appName: process.env.APP_NAME || 'Data Analyst Assistant', modelIdForChart: process.env.MODEL_ID_FOR_CHART || 'us.anthropic.claude-haiku-4-5-20251001-v1:0', questionAnswersTableName: process.env.QUESTION_ANSWERS_TABLE_NAME || '', + memoryId: process.env.MEMORY_ID || '', maxLengthInputSearch: parseInt(process.env.MAX_LENGTH_INPUT_SEARCH || '500', 10), }; diff --git a/02-use-cases/video-games-sales-assistant/amplify-video-games-sales-assistant-agentcore-strands/src/app/components/assistant/services/agent-core-call.ts b/02-use-cases/video-games-sales-assistant/amplify-video-games-sales-assistant-agentcore-strands/src/app/components/assistant/services/agent-core-call.ts index d4ff70162..81c7b7613 100644 --- a/02-use-cases/video-games-sales-assistant/amplify-video-games-sales-assistant-agentcore-strands/src/app/components/assistant/services/agent-core-call.ts +++ b/02-use-cases/video-games-sales-assistant/amplify-video-games-sales-assistant-agentcore-strands/src/app/components/assistant/services/agent-core-call.ts @@ -8,16 +8,17 @@ import { BedrockAgentCoreClient, InvokeAgentRuntimeCommand, } from '@aws-sdk/client-bedrock-agentcore'; -import { getAwsClient, type CognitoAuthParams } from '@/lib/aws-client'; +import { getAwsClient, type CognitoAuthParams } from '@/utils/aws-client'; import { getQueryResults } from './aws-calls'; import type { Answer, ControlAnswer, MessageItem } from '../types'; interface GetAnswerParams { query: string; sessionId: string; + userId: string; + userName: string; agentRuntimeArn: string; agentEndpointName: string; - lastKTurns: number; questionAnswersTableName: string; auth: CognitoAuthParams; setControlAnswers: React.Dispatch>; @@ -32,9 +33,10 @@ interface GetAnswerParams { export const getAnswer = async ({ query: myQuery, sessionId, + userId, + userName, agentRuntimeArn, agentEndpointName, - lastKTurns, questionAnswersTableName, auth, setControlAnswers, @@ -70,9 +72,10 @@ export const getAnswer = async ({ const payload = JSON.stringify({ prompt: myQuery, session_id: sessionId, + user_id: userId, + user_name: userName, prompt_uuid: queryUuid, user_timezone: timezone, - last_k_turns: lastKTurns, }); const input = { diff --git a/02-use-cases/video-games-sales-assistant/amplify-video-games-sales-assistant-agentcore-strands/src/app/components/assistant/services/aws-calls.ts b/02-use-cases/video-games-sales-assistant/amplify-video-games-sales-assistant-agentcore-strands/src/app/components/assistant/services/aws-calls.ts index e8e2cf88d..608bd55ea 100644 --- a/02-use-cases/video-games-sales-assistant/amplify-video-games-sales-assistant-agentcore-strands/src/app/components/assistant/services/aws-calls.ts +++ b/02-use-cases/video-games-sales-assistant/amplify-video-games-sales-assistant-agentcore-strands/src/app/components/assistant/services/aws-calls.ts @@ -3,7 +3,11 @@ // (same as original React app) since it needs Cognito credentials. import { BedrockRuntimeClient, InvokeModelCommand } from '@aws-sdk/client-bedrock-runtime'; -import { getAwsClient, type CognitoAuthParams } from '@/lib/aws-client'; +import { + BedrockAgentCoreClient, + ListMemoryRecordsCommand, +} from '@aws-sdk/client-bedrock-agentcore'; +import { getAwsClient, type CognitoAuthParams } from '@/utils/aws-client'; import type { QueryResult, ChartData, Answer } from '../types'; import { extractBetweenTags, @@ -132,3 +136,70 @@ export const generateChart = async ( }; } }; + + +export interface MemoryFact { + memoryRecordId: string; + content: string; + createdAt: string; + namespace: string; +} + +/** + * Fetch long-term memory facts for the current user from AgentCore Memory. + */ +export const getMemoryFacts = async ( + memoryId: string, + userId: string, + auth: CognitoAuthParams +): Promise => { + const client = getAwsClient(BedrockAgentCoreClient, auth); + const namespace = `facts/${userId}/`; + + const namespacesToTry = [ + `facts/${userId}/`, + `/facts/${userId}/`, + `facts/${userId}`, + `/facts/${userId}`, + ]; + + console.log('🧠 Fetching memory facts:', { memoryId, userId, namespacesToTry }); + + try { + for (const ns of namespacesToTry) { + console.log(`🧠 Trying namespace: "${ns}"`); + const command = new ListMemoryRecordsCommand({ + memoryId, + namespace: ns, + }); + + const response = await client.send(command); + const records = response.memoryRecordSummaries || []; + console.log(`🧠 Namespace "${ns}" returned ${records.length} records`); + + if (records.length > 0) { + console.log('🧠 Full response:', JSON.stringify(response, null, 2)); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const facts = records.map((record: any) => ({ + memoryRecordId: record.memoryRecordId || record.id || '', + content: record.content?.text || JSON.stringify(record.content) || '', + createdAt: record.createdAt || record.createdTimestamp || '', + namespace: record.namespace || ns, + })); + + facts.forEach((fact: MemoryFact, i: number) => { + console.log(`🧠 Fact ${i + 1}:`, fact.content.slice(0, 100)); + }); + + return facts; + } + } + + console.log('🧠 No facts found in any namespace pattern'); + return []; + } catch (error) { + console.error('❌ Failed to fetch memory facts:', { memoryId, error }); + return []; + } +}; diff --git a/02-use-cases/video-games-sales-assistant/amplify-video-games-sales-assistant-agentcore-strands/src/app/components/assistant/types.ts b/02-use-cases/video-games-sales-assistant/amplify-video-games-sales-assistant-agentcore-strands/src/app/components/assistant/types.ts index 79f87f277..268cf3604 100644 --- a/02-use-cases/video-games-sales-assistant/amplify-video-games-sales-assistant-agentcore-strands/src/app/components/assistant/types.ts +++ b/02-use-cases/video-games-sales-assistant/amplify-video-games-sales-assistant-agentcore-strands/src/app/components/assistant/types.ts @@ -52,10 +52,10 @@ export interface ControlAnswer { export interface AssistantConfig { agentRuntimeArn: string; agentEndpointName: string; - lastKTurns: number; welcomeMessage: string; appName: string; modelIdForChart: string; questionAnswersTableName: string; + memoryId: string; maxLengthInputSearch?: number; } diff --git a/02-use-cases/video-games-sales-assistant/amplify-video-games-sales-assistant-agentcore-strands/src/app/components/assistant/ui/Chat.tsx b/02-use-cases/video-games-sales-assistant/amplify-video-games-sales-assistant-agentcore-strands/src/app/components/assistant/ui/Chat.tsx index 992bb10e4..a95c4cd02 100644 --- a/02-use-cases/video-games-sales-assistant/amplify-video-games-sales-assistant-agentcore-strands/src/app/components/assistant/ui/Chat.tsx +++ b/02-use-cases/video-games-sales-assistant/amplify-video-games-sales-assistant-agentcore-strands/src/app/components/assistant/ui/Chat.tsx @@ -2,11 +2,11 @@ import { useState, useEffect, useRef, useCallback } from 'react'; import { v4 as uuidv4 } from 'uuid'; -import { fetchAuthSession } from 'aws-amplify/auth'; -import type { CognitoAuthParams } from '@/lib/aws-client'; +import { fetchAuthSession, getCurrentUser, fetchUserAttributes } from 'aws-amplify/auth'; +import type { CognitoAuthParams } from '@/utils/aws-client'; import type { Answer, ControlAnswer, ChartConfig, AssistantConfig } from '../types'; import { getAnswer } from '../services/agent-core-call'; -import { generateChart } from '../services/aws-calls'; +import { generateChart, getMemoryFacts, type MemoryFact } from '../services/aws-calls'; import MarkdownRenderer from './MarkdownRenderer'; import ToolBox from './ToolBox'; import LoadingIndicator from './LoadingIndicator'; @@ -22,6 +22,8 @@ export default function Chat({ config, identityPoolId, userPoolId }: ChatProps) const [answers, setAnswers] = useState([]); const [query, setQuery] = useState(''); const [sessionId] = useState(uuidv4()); + const [userId, setUserId] = useState('guest'); + const [userName, setUserName] = useState('Guest'); const [errorMessage, setErrorMessage] = useState(''); const [currentWorkingToolId, setCurrentWorkingToolId] = useState(null); const [inputHovered, setInputHovered] = useState(false); @@ -29,8 +31,26 @@ export default function Chat({ config, identityPoolId, userPoolId }: ChatProps) const chatContainerRef = useRef(null); const textareaRef = useRef(null); const isSubmittingRef = useRef(false); + const [memoryPanelOpen, setMemoryPanelOpen] = useState(false); + const [memoryFacts, setMemoryFacts] = useState([]); + const [memoryLoading, setMemoryLoading] = useState(false); const maxLength = config.maxLengthInputSearch ?? 500; + // Resolve Cognito user ID and name on mount + useEffect(() => { + (async () => { + try { + const user = await getCurrentUser(); + setUserId(user.userId); + const loginId = user.signInDetails?.loginId || ''; + const fallback = loginId.split('@')[0]; + setUserName(fallback.charAt(0).toUpperCase() + fallback.slice(1).toLowerCase()); + const attrs = await fetchUserAttributes(); + if (attrs.name) setUserName(attrs.name); + } catch { /* keep guest fallback */ } + })(); + }, []); + // Auto-scroll only while the agent is actively answering useEffect(() => { if (!loading) return; @@ -49,6 +69,27 @@ export default function Chat({ config, identityPoolId, userPoolId }: ChatProps) return { idToken, identityPoolId, userPoolId }; }, [identityPoolId, userPoolId]); + const loadMemoryFacts = useCallback(async () => { + if (!config.memoryId || userId === 'guest') return; + setMemoryLoading(true); + try { + const auth = await getAuth(); + const facts = await getMemoryFacts(config.memoryId, userId, auth); + setMemoryFacts(facts); + } catch (err) { + console.error('Failed to load memory facts:', err); + } finally { + setMemoryLoading(false); + } + }, [config.memoryId, userId, getAuth]); + + // Listen for header memory button click + useEffect(() => { + const handler = () => { setMemoryPanelOpen(true); loadMemoryFacts(); }; + window.addEventListener('open-memory-panel', handler); + return () => window.removeEventListener('open-memory-panel', handler); + }, [loadMemoryFacts]); + // Auto-generate charts when queryResults arrive useEffect(() => { const gen = async (i: number, a: Answer) => { @@ -94,7 +135,7 @@ export default function Chat({ config, identityPoolId, userPoolId }: ChatProps) isSubmittingRef.current = true; try { const auth = await getAuth(); - await getAnswer({ query: myQuery, sessionId, agentRuntimeArn: config.agentRuntimeArn, agentEndpointName: config.agentEndpointName, lastKTurns: config.lastKTurns, questionAnswersTableName: config.questionAnswersTableName, auth, setControlAnswers, setAnswers, setEnabled, setLoading, setErrorMessage, setQuery, setCurrentWorkingToolId }); + await getAnswer({ query: myQuery, sessionId, userId, userName, agentRuntimeArn: config.agentRuntimeArn, agentEndpointName: config.agentEndpointName, questionAnswersTableName: config.questionAnswersTableName, auth, setControlAnswers, setAnswers, setEnabled, setLoading, setErrorMessage, setQuery, setCurrentWorkingToolId }); } catch (err) { setErrorMessage(String(err)); setLoading(false); } finally { isSubmittingRef.current = false; } }; const handleShowTab = (index: number, type: 'answer' | 'records' | 'chart') => () => { @@ -104,7 +145,7 @@ export default function Chat({ config, identityPoolId, userPoolId }: ChatProps) typeof chart === 'object' && chart !== null && 'chart_type' in chart; return ( -
+
{errorMessage && (
{errorMessage} @@ -268,6 +309,68 @@ export default function Chat({ config, identityPoolId, userPoolId }: ChatProps)
+ + {/* Memory Facts Panel — full width on tablet, constrained on desktop */} + {memoryPanelOpen && ( +
+ {/* Panel header */} +
+
+ + + + +
+ Long-term Memory + ({memoryFacts.length}) +
+
+
+ + +
+
+ + {/* Description */} +
+

+ Facts extracted by Amazon Bedrock AgentCore Memory from your conversations. These persist across sessions and help the assistant provide personalized, context-aware responses. +

+
+ + {/* Facts list */} +
+ {memoryLoading && memoryFacts.length === 0 ? ( +
+ +

Loading memory facts...

+
+ ) : memoryFacts.length === 0 ? ( +
+

No facts extracted yet

+

Facts are extracted asynchronously after conversations. They may take 20-40 seconds to appear.

+
+ ) : ( +
+ {memoryFacts.map((fact) => ( +
+

{fact.content}

+ {fact.createdAt && ( +

+ {new Date(fact.createdAt).toLocaleString()} +

+ )} +
+ ))} +
+ )} +
+
+ )}
); } diff --git a/02-use-cases/video-games-sales-assistant/amplify-video-games-sales-assistant-agentcore-strands/src/app/globals.css b/02-use-cases/video-games-sales-assistant/amplify-video-games-sales-assistant-agentcore-strands/src/app/globals.css index c17e99ea9..7f77c4c3f 100644 --- a/02-use-cases/video-games-sales-assistant/amplify-video-games-sales-assistant-agentcore-strands/src/app/globals.css +++ b/02-use-cases/video-games-sales-assistant/amplify-video-games-sales-assistant-agentcore-strands/src/app/globals.css @@ -98,3 +98,13 @@ a { overflow-y: auto !important; -webkit-overflow-scrolling: touch; } + +/* Memory panel — full width on tablet and below, constrained on desktop */ +.memory-panel { + max-width: 100%; +} +@media (min-width: 1024px) { + .memory-panel { + max-width: 480px; + } +} diff --git a/02-use-cases/video-games-sales-assistant/amplify-video-games-sales-assistant-agentcore-strands/src/utils/aws-client.ts b/02-use-cases/video-games-sales-assistant/amplify-video-games-sales-assistant-agentcore-strands/src/utils/aws-client.ts new file mode 100644 index 000000000..1cee251ae --- /dev/null +++ b/02-use-cases/video-games-sales-assistant/amplify-video-games-sales-assistant-agentcore-strands/src/utils/aws-client.ts @@ -0,0 +1,57 @@ +/** + * AWS Client Factory + * + * Creates AWS SDK v3 clients authenticated via Cognito Identity Pool. + * The Cognito JWT (ID token) is exchanged for temporary AWS credentials, + * which are then used to instantiate any AWS service client. + * + * Usage: + * const dynamo = await getAwsClient(DynamoDBClient, { idToken, identityPoolId, userPoolId, region }); + * const s3 = await getAwsClient(S3Client, { idToken, identityPoolId, userPoolId, region }); + */ + +import { fromCognitoIdentityPool } from '@aws-sdk/credential-providers'; + +export interface CognitoAuthParams { + /** Cognito ID token (JWT) from fetchAuthSession() */ + idToken: string; + /** Cognito Identity Pool ID from amplify_outputs.json */ + identityPoolId: string; + /** Cognito User Pool ID from amplify_outputs.json */ + userPoolId: string; + /** AWS region — auto-detected from identityPoolId if not provided */ + region?: string; +} + +/** + * Generic AWS client factory. + * Pass any AWS SDK v3 client constructor and Cognito auth params. + * + * @example + * import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; + * const client = getAwsClient(DynamoDBClient, authParams); + * + * @example + * import { S3Client } from '@aws-sdk/client-s3'; + * const client = getAwsClient(S3Client, authParams); + * + * @example + * import { BedrockAgentRuntimeClient } from '@aws-sdk/client-bedrock-agent-runtime'; + * const client = getAwsClient(BedrockAgentRuntimeClient, authParams); + */ +export function getAwsClient( + ClientClass: new (config: { region: string; credentials: ReturnType }) => T, + { idToken, identityPoolId, userPoolId, region }: CognitoAuthParams +): T { + const resolvedRegion = region || identityPoolId.split(':')[0] || 'us-east-1'; + + const credentials = fromCognitoIdentityPool({ + clientConfig: { region: resolvedRegion }, + identityPoolId, + logins: { + [`cognito-idp.${resolvedRegion}.amazonaws.com/${userPoolId}`]: idToken, + }, + }); + + return new ClientClass({ region: resolvedRegion, credentials }); +} diff --git a/02-use-cases/video-games-sales-assistant/cdk-data-analyst-assistant-agentcore-strands/.gitignore b/02-use-cases/video-games-sales-assistant/cdk-data-analyst-assistant-agentcore-strands/.gitignore index f60797b6a..1914ecdfb 100644 --- a/02-use-cases/video-games-sales-assistant/cdk-data-analyst-assistant-agentcore-strands/.gitignore +++ b/02-use-cases/video-games-sales-assistant/cdk-data-analyst-assistant-agentcore-strands/.gitignore @@ -6,3 +6,7 @@ node_modules # CDK asset staging directory .cdk.staging cdk.out + +# Python +__pycache__/ +*.pyc diff --git a/02-use-cases/video-games-sales-assistant/cdk-data-analyst-assistant-agentcore-strands/README.md b/02-use-cases/video-games-sales-assistant/cdk-data-analyst-assistant-agentcore-strands/README.md index 51a1dc35a..fc3589deb 100644 --- a/02-use-cases/video-games-sales-assistant/cdk-data-analyst-assistant-agentcore-strands/README.md +++ b/02-use-cases/video-games-sales-assistant/cdk-data-analyst-assistant-agentcore-strands/README.md @@ -11,9 +11,9 @@ This CDK stack deploys a complete data analyst assistant powered by Amazon Bedro ### Amazon Bedrock AgentCore Resources -- **AgentCore Memory**: Short-term memory for maintaining conversation context with 7-day event expiration -- **AgentCore Runtime**: Container-based runtime hosting the Strands Agent with ARM64 architecture -- **AgentCore Runtime Endpoint**: HTTP endpoint for invoking the data analyst assistant +- **AgentCore Memory**: Long-term semantic memory with a "Facts" strategy (`/facts/{actorId}` namespace) for extracting and persisting knowledge across sessions per user, plus short-term event-based conversation history with 90-day retention +- **AgentCore Runtime**: Container-based runtime hosting the Strands Agent with ARM64 architecture and DEFAULT endpoint +- **Observability**: CloudWatch Logs delivery for runtime application logs and memory extraction, plus X-Ray traces for runtime invocations ### Data Source and VPC Infrastructure @@ -81,11 +81,16 @@ Default Parameters: ### Deployed Resources **AgentCore Resources:** -- AgentCore Memory with 7-day event expiration -- AgentCore Runtime (container-based, ARM64) -- AgentCore Runtime Endpoint +- AgentCore Memory with semantic "Facts" strategy, `/facts/{actorId}` namespace, and 90-day event retention +- AgentCore Runtime (container-based, ARM64) with DEFAULT endpoint - ECR repository with agent container image +**Observability:** +- Runtime application logs → CloudWatch Logs (`/aws/vendedlogs/bedrock-agentcore/`) +- Runtime traces → AWS X-Ray +- Memory extraction logs → CloudWatch Logs (`/aws/vendedlogs/bedrock-agentcore/memory/`) +- All log groups configured with 14-day retention + **Data Infrastructure:** - VPC with public/private subnets, NAT Gateway, security groups, VPC endpoints - Aurora PostgreSQL Serverless v2 (v17.4) with RDS Data API enabled @@ -114,11 +119,19 @@ After deployment, the stack exports: - `QuestionAnswersTableName`: DynamoDB table name - `QuestionAnswersTableArn`: DynamoDB table ARN - `AgentRuntimeArn`: AgentCore runtime ARN -- `AgentEndpointName`: AgentCore runtime endpoint name > [!IMPORTANT] > Enhance AI safety and compliance by implementing **[Amazon Bedrock Guardrails](https://aws.amazon.com/bedrock/guardrails/)** for your AI applications with the seamless integration offered by **[Strands Agents SDK](https://strandsagents.com/latest/user-guide/safety-security/guardrails/)**. +### How Memory Works + +The agent uses the [AgentCoreMemorySessionManager](https://strandsagents.com/docs/community/session-managers/agentcore-memory/) (Strands integration) to manage both short-term and long-term memory automatically: + +- **Short-term memory (STM)**: Scoped by `actorId` + `sessionId`. Stores raw conversation events within a session. Each page load generates a new `sessionId`, so STM only contains the current conversation. +- **Long-term memory (LTM)**: Scoped by `/facts/{actorId}` namespace. After events are saved, AgentCore asynchronously extracts facts using the semantic strategy and stores them per user. When a new session starts, the agent searches this namespace using the user's query via vector similarity, retrieving relevant knowledge from all past sessions. +- **Per-user isolation**: The `actorId` is the Cognito user `sub`, so each user's facts are completely isolated from other users. +- **Async extraction**: LTM extraction takes 20-40 seconds after events are saved. Within the same session, STM handles continuity. LTM provides cross-session knowledge. + ## Set Up the PostgreSQL Database 1. Install required Python dependencies: @@ -139,7 +152,6 @@ export STACK_NAME=CdkDataAnalystAssistantAgentcoreStrandsStack export BEDROCK_MODEL_ID=$(aws cloudformation describe-stacks --stack-name "$STACK_NAME" --query "Stacks[0].Parameters[?ParameterKey=='BedrockModelId'].ParameterValue" --output text) export MEMORY_ID=$(aws cloudformation describe-stacks --stack-name "$STACK_NAME" --query "Stacks[0].Outputs[?OutputKey=='MemoryId'].OutputValue" --output text) export AGENT_RUNTIME_ARN=$(aws cloudformation describe-stacks --stack-name "$STACK_NAME" --query "Stacks[0].Outputs[?OutputKey=='AgentRuntimeArn'].OutputValue" --output text) -export AGENT_ENDPOINT_NAME=$(aws cloudformation describe-stacks --stack-name "$STACK_NAME" --query "Stacks[0].Outputs[?OutputKey=='AgentEndpointName'].OutputValue" --output text) # Database resources export SECRET_ARN=$(aws cloudformation describe-stacks --stack-name "$STACK_NAME" --query "Stacks[0].Outputs[?OutputKey=='SecretARN'].OutputValue" --output text) @@ -165,7 +177,6 @@ BEDROCK_MODEL_ID: ${BEDROCK_MODEL_ID} # AgentCore Resources MEMORY_ID: ${MEMORY_ID} AGENT_RUNTIME_ARN: ${AGENT_RUNTIME_ARN} -AGENT_ENDPOINT_NAME: ${AGENT_ENDPOINT_NAME} # Database Resources SECRET_ARN: ${SECRET_ARN} @@ -236,25 +247,25 @@ export SESSION_ID=$(uuidgen) ```bash curl -X POST http://localhost:8080/invocations \ -H "Content-Type: application/json" \ --d '{"prompt": "Hello world!", "session_id": "'$SESSION_ID'", "last_k_turns": 20}' +-d '{"prompt": "Hello world!", "session_id": "'$SESSION_ID'", "user_id": "local-test-user"}' ``` ```bash curl -X POST http://localhost:8080/invocations \ -H "Content-Type: application/json" \ --d '{"prompt": "what is the structure of your data available?!", "session_id": "'$SESSION_ID'", "last_k_turns": 20}' +-d '{"prompt": "what is the structure of your data available?!", "session_id": "'$SESSION_ID'", "user_id": "local-test-user"}' ``` ```bash curl -X POST http://localhost:8080/invocations \ -H "Content-Type: application/json" \ --d '{"prompt": "Which developers tend to get the best reviews?", "session_id": "'$SESSION_ID'", "last_k_turns": 20}' +-d '{"prompt": "Which developers tend to get the best reviews?", "session_id": "'$SESSION_ID'", "user_id": "local-test-user"}' ``` ```bash curl -X POST http://localhost:8080/invocations \ -H "Content-Type: application/json" \ --d '{"prompt": "Give me a summary of our conversation", "session_id": "'$SESSION_ID'", "last_k_turns": 20}' +-d '{"prompt": "Give me a summary of our conversation", "session_id": "'$SESSION_ID'", "user_id": "local-test-user"}' ``` ## Invoking the Agent @@ -276,3 +287,4 @@ cdk destroy ## License This project is licensed under the Apache-2.0 License. + diff --git a/02-use-cases/video-games-sales-assistant/cdk-data-analyst-assistant-agentcore-strands/cdklib/cdk-data-analyst-assistant-agentcore-strands-stack.ts b/02-use-cases/video-games-sales-assistant/cdk-data-analyst-assistant-agentcore-strands/cdklib/cdk-data-analyst-assistant-agentcore-strands-stack.ts index ee915bbef..c7322e78d 100644 --- a/02-use-cases/video-games-sales-assistant/cdk-data-analyst-assistant-agentcore-strands/cdklib/cdk-data-analyst-assistant-agentcore-strands-stack.ts +++ b/02-use-cases/video-games-sales-assistant/cdk-data-analyst-assistant-agentcore-strands/cdklib/cdk-data-analyst-assistant-agentcore-strands-stack.ts @@ -18,6 +18,7 @@ import * as s3 from "aws-cdk-lib/aws-s3"; import * as iam from "aws-cdk-lib/aws-iam"; import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; import * as ecr_assets from 'aws-cdk-lib/aws-ecr-assets'; +import * as logs from 'aws-cdk-lib/aws-logs'; import * as path from 'path'; import { aws_bedrockagentcore as bedrockagentcore } from 'aws-cdk-lib'; @@ -414,16 +415,25 @@ export class CdkDataAnalystAssistantAgentcoreStrandsStack extends cdk.Stack { }); // ================================ - // BEDROCK AGENTCORE MEMORY + // BEDROCK AGENTCORE MEMORY (LTM) // ================================ - // Short-term memory for AgentCore to maintain conversation context + // Long-term memory with semantic strategy for AgentCore const uniqueSuffix = cdk.Names.uniqueId(this).slice(-8).toLowerCase().replace(/[^a-z0-9]/g, ''); const agentMemory = new bedrockagentcore.CfnMemory(this, 'AgentMemory', { name: `DataAnalystAssistantMemory_${uniqueSuffix}`, - eventExpiryDuration: 7, // Events expire after 7 days + eventExpiryDuration: 90, // Events expire after 90 days memoryExecutionRoleArn: agentCoreRole.roleArn, - description: 'Short-term memory for data analyst assistant conversations', + description: 'Long-term semantic memory for data analyst assistant conversations', + memoryStrategies: [ + { + semanticMemoryStrategy: { + name: 'Facts', + description: 'Extracts and stores facts about video game sales data analysis conversations', + namespaces: ['/facts/{actorId}'], + }, + }, + ], }); // ================================ @@ -470,6 +480,96 @@ export class CdkDataAnalystAssistantAgentcoreStrandsStack extends cdk.Stack { // Endpoint depends on runtime being created first runtimeEndpoint.addDependency(agentRuntime); + // ================================ + // OBSERVABILITY - LOG DELIVERY + // ================================ + + // --- AgentCore Runtime Logs --- + + // CloudWatch Log Group for AgentCore Runtime application logs + const runtimeLogGroup = new logs.LogGroup(this, 'RuntimeLogGroup', { + logGroupName: `/aws/vendedlogs/bedrock-agentcore/${agentRuntime.attrAgentRuntimeId}`, + retention: logs.RetentionDays.TWO_WEEKS, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + + // Delivery Source — Runtime application logs + const runtimeLogSource = new logs.CfnDeliverySource(this, 'RuntimeLogSource', { + name: `rt-${uniqueSuffix}-log-src`, + logType: 'APPLICATION_LOGS', + resourceArn: agentRuntime.attrAgentRuntimeArn, + }); + runtimeLogSource.addDependency(agentRuntime); + + // Delivery Destination — CloudWatch Logs for runtime + const runtimeLogDestination = new logs.CfnDeliveryDestination(this, 'RuntimeLogDestination', { + name: `rt-${uniqueSuffix}-log-dst`, + destinationResourceArn: runtimeLogGroup.logGroupArn, + }); + + // Delivery — connects runtime source to destination + const runtimeLogDelivery = new logs.CfnDelivery(this, 'RuntimeLogDelivery', { + deliverySourceName: runtimeLogSource.ref, + deliveryDestinationArn: runtimeLogDestination.attrArn, + }); + runtimeLogDelivery.addDependency(runtimeLogSource); + runtimeLogDelivery.addDependency(runtimeLogDestination); + + // --- AgentCore Runtime Traces (X-Ray) --- + + // Delivery Source — Runtime traces + const runtimeTracesSource = new logs.CfnDeliverySource(this, 'RuntimeTracesSource', { + name: `rt-${uniqueSuffix}-trc-src`, + logType: 'TRACES', + resourceArn: agentRuntime.attrAgentRuntimeArn, + }); + runtimeTracesSource.addDependency(agentRuntime); + + // Delivery Destination — X-Ray for traces + const runtimeTracesDestination = new logs.CfnDeliveryDestination(this, 'RuntimeTracesDestination', { + name: `rt-${uniqueSuffix}-trc-dst`, + deliveryDestinationType: 'XRAY', + }); + + // Delivery — connects traces source to X-Ray destination + const runtimeTracesDelivery = new logs.CfnDelivery(this, 'RuntimeTracesDelivery', { + deliverySourceName: runtimeTracesSource.ref, + deliveryDestinationArn: runtimeTracesDestination.attrArn, + }); + runtimeTracesDelivery.addDependency(runtimeTracesSource); + runtimeTracesDelivery.addDependency(runtimeTracesDestination); + + // --- AgentCore Memory Logs --- + + // CloudWatch Log Group for AgentCore Memory vended log delivery + const memoryLogGroup = new logs.LogGroup(this, 'MemoryLogGroup', { + logGroupName: `/aws/vendedlogs/bedrock-agentcore/memory/${agentMemory.attrMemoryId}`, + retention: logs.RetentionDays.TWO_WEEKS, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + + // Delivery Source — connects the Memory resource as a log source + const memoryLogSource = new logs.CfnDeliverySource(this, 'MemoryLogSource', { + name: `mem-${uniqueSuffix}-log-src`, + logType: 'APPLICATION_LOGS', + resourceArn: `arn:aws:bedrock-agentcore:${this.region}:${this.account}:memory/${agentMemory.attrMemoryId}`, + }); + memoryLogSource.addDependency(agentMemory); + + // Delivery Destination — CloudWatch Logs + const memoryLogDestination = new logs.CfnDeliveryDestination(this, 'MemoryLogDestination', { + name: `mem-${uniqueSuffix}-log-dst`, + destinationResourceArn: memoryLogGroup.logGroupArn, + }); + + // Delivery — connects source to destination + const memoryLogDelivery = new logs.CfnDelivery(this, 'MemoryLogDelivery', { + deliverySourceName: memoryLogSource.ref, + deliveryDestinationArn: memoryLogDestination.attrArn, + }); + memoryLogDelivery.addDependency(memoryLogSource); + memoryLogDelivery.addDependency(memoryLogDestination); + // ================================ // CLOUDFORMATION OUTPUTS // ================================ @@ -508,12 +608,7 @@ export class CdkDataAnalystAssistantAgentcoreStrandsStack extends cdk.Stack { value: agentRuntime.attrAgentRuntimeArn, description: "The ARN of the AgentCore runtime", }); - - new cdk.CfnOutput(this, "AgentEndpointName", { - value: runtimeEndpoint.name, - description: "The name of the AgentCore runtime endpoint", - }); - + new cdk.CfnOutput(this, "MemoryId", { value: agentMemory.attrMemoryId, description: "The ID of the AgentCore Memory", diff --git a/02-use-cases/video-games-sales-assistant/cdk-data-analyst-assistant-agentcore-strands/data-analyst-assistant-agentcore-strands/app.py b/02-use-cases/video-games-sales-assistant/cdk-data-analyst-assistant-agentcore-strands/data-analyst-assistant-agentcore-strands/app.py index 70d3cba46..ac554351d 100644 --- a/02-use-cases/video-games-sales-assistant/cdk-data-analyst-assistant-agentcore-strands/data-analyst-assistant-agentcore-strands/app.py +++ b/02-use-cases/video-games-sales-assistant/cdk-data-analyst-assistant-agentcore-strands/data-analyst-assistant-agentcore-strands/app.py @@ -3,17 +3,16 @@ This application provides an intelligent data analyst assistant specialized in video game sales analysis. It leverages Amazon Bedrock Claude models for natural language processing, Aurora Serverless PostgreSQL -for data storage, and AgentCore Memory for conversation context management. +for data storage, and AgentCore Memory (STM + LTM) for conversation context management. Key Features: - Natural language to SQL query conversion - Video game sales data analysis and insights -- Conversation memory and context awareness +- Short-term memory (conversation persistence) and long-term semantic memory (facts across sessions) - Real-time streaming responses - Comprehensive error handling and logging """ -import logging import json import os from uuid import uuid4 @@ -23,19 +22,17 @@ from strands import Agent, tool from strands_tools import current_time from strands.models import BedrockModel +from bedrock_agentcore.memory.integrations.strands.config import ( + AgentCoreMemoryConfig, + RetrievalConfig, +) +from bedrock_agentcore.memory.integrations.strands.session_manager import ( + AgentCoreMemorySessionManager, +) # Custom module imports from src.tools import get_tables_information, run_sql_query -from src.utils import ( - save_raw_query_result, - load_file_content, - get_agentcore_memory_messages, - MemoryHookProvider, -) - -# Setup logging -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger("personal-agent") +from src.utils import save_raw_query_result, load_file_content # Retrieve AgentCore Memory ID memory_id = os.environ.get("MEMORY_ID") @@ -53,19 +50,14 @@ def load_system_prompt(): """ Load the system prompt configuration for the video games sales analyst assistant. - This prompt defines the assistant's behavior, capabilities, and domain expertise - in video game sales data analysis. Falls back to a default prompt if the - instructions.txt file is not available. - Returns: str: The system prompt configuration for the assistant """ print("\n" + "=" * 50) print("📝 LOADING SYSTEM PROMPT") print("=" * 50) - print("📂 Attempting to load instructions.txt...") - fallback_prompt = """You are a specialized Video Games Sales Data Analyst Assistant with expertise in + fallback_prompt = """You are a specialized Video Games Sales Data Analyst Assistant with expertise in analyzing gaming industry trends, sales performance, and market insights. You can execute SQL queries, interpret gaming data, and provide actionable business intelligence for the video game industry.""" @@ -80,7 +72,6 @@ def load_system_prompt(): return prompt except Exception as e: print(f"❌ Error loading system prompt: {str(e)}") - print("⚠️ Using fallback prompt") print("=" * 50 + "\n") return fallback_prompt @@ -93,10 +84,6 @@ def create_execute_sql_query_tool(user_prompt: str, prompt_uuid: str): """ Create a dynamic SQL query execution tool for video game sales data analysis. - This function generates a specialized tool that executes SQL queries against the - Aurora PostgreSQL database containing video game sales data. Query results are - automatically saved to DynamoDB for audit trails and future reference. - Args: user_prompt (str): The original user question about video game sales data prompt_uuid (str): Unique identifier for tracking this analysis prompt @@ -110,10 +97,6 @@ def execute_sql_query(sql_query: str, description: str) -> str: """ Execute SQL queries against the video game sales database for data analysis. - This tool runs SQL queries against the Aurora PostgreSQL database containing - comprehensive video game sales data, including game titles, platforms, genres, - sales figures, and regional performance metrics. - Args: sql_query (str): The SQL query to execute against the video game sales database description (str): Clear description of what the query analyzes or retrieves @@ -131,47 +114,35 @@ def execute_sql_query(sql_query: str, description: str) -> str: try: print("⏳ Executing video game sales data query via RDS Data API...") - - # Execute the SQL query using the RDS Data API function response_json = json.loads(run_sql_query(sql_query)) - # Check if there was an error if "error" in response_json: print(f"❌ Query execution failed: {response_json['error']}") print("=" * 60 + "\n") return json.dumps(response_json) - # Extract the results records_to_return = response_json.get("result", []) message = response_json.get("message", "") print("✅ Video game sales data query executed successfully") print(f"📊 Data records retrieved: {len(records_to_return)}") - if message: - print(f"💬 Query message: {message}") - # Prepare result object if message != "": result = {"result": records_to_return, "message": message} else: result = {"result": records_to_return} - print("-" * 60) print("💾 Saving analysis results to DynamoDB for audit trail...") - - # Save query results to DynamoDB for future reference save_result = save_raw_query_result( prompt_uuid, user_prompt, sql_query, description, result, message ) if not save_result["success"]: - print( - f"⚠️ Failed to save analysis results to DynamoDB: {save_result['error']}" - ) + print(f"⚠️ Failed to save analysis results: {save_result['error']}") result["saved"] = False result["save_error"] = save_result["error"] else: - print("✅ Analysis results successfully saved to DynamoDB audit trail") + print("✅ Analysis results saved to DynamoDB audit trail") print("=" * 60 + "\n") return json.dumps(result) @@ -189,18 +160,13 @@ def execute_sql_query(sql_query: str, description: str) -> str: async def agent_invocation(payload): """Main entry point for video game sales data analysis requests with streaming responses. - This function processes natural language queries about video game sales data, initializes - the Claude-powered agent with specialized tools, and streams intelligent analysis back - to the client while maintaining conversation context. - Expected payload structure: { "prompt": "Your video game sales analysis question", "prompt_uuid": "optional-unique-prompt-identifier", "user_timezone": "US/Pacific", - "session_id": "optional-conversation-session-id", - "user_id": "optional-user-identifier", - "last_turns": "optional-number-of-conversation-turns-to-retrieve" + "session_id": "conversation-session-id", + "user_id": "cognito-user-sub" } Returns: @@ -216,7 +182,7 @@ async def agent_invocation(payload): user_timezone = payload.get("user_timezone", "US/Pacific") session_id = payload.get("session_id", str(uuid4())) user_id = payload.get("user_id", "guest") - last_k_turns = int(payload.get("last_k_turns", 20)) + user_name = payload.get("user_name", "User") print("\n" + "=" * 80) print("🎮 VIDEO GAME SALES ANALYSIS REQUEST") @@ -227,124 +193,125 @@ async def agent_invocation(payload): print(f"🤖 Claude Model: {bedrock_model_id_env}") print(f"🆔 Prompt UUID: {prompt_uuid}") print(f"🌍 User Timezone: {user_timezone}") - print(f"🔗 Conversation ID: {session_id}") + print(f"🔗 Session ID: {session_id}") print(f"👤 User ID: {user_id}") - print(f"🔄 Context Turns: {last_k_turns}") + print(f"👤 User Name: {user_name}") print("-" * 80) - # Initialize Claude model for video game sales analysis - print(f"🧠 Initializing Claude model for analysis: {bedrock_model_id_env}") + # Initialize Claude model + print(f"🧠 Initializing Claude model: {bedrock_model_id_env}") bedrock_model = BedrockModel(model_id=bedrock_model_id_env) - print("✅ Claude model ready for video game sales analysis") + print("✅ Claude model ready") + # Configure AgentCore Memory with STM + LTM retrieval print("-" * 80) - print("🧠 Retrieving conversation context from AgentCore Memory...") - agentcore_messages = get_agentcore_memory_messages( - memory_id, user_id, session_id, last_k_turns + print("🧠 Configuring AgentCore Memory (STM + LTM)...") + + agentcore_memory_config = AgentCoreMemoryConfig( + memory_id=memory_id, + session_id=session_id, + actor_id=user_id, + retrieval_config={ + "/facts/{actorId}": RetrievalConfig( + top_k=5, + relevance_score=0.3, + ), + }, ) - print("📋 CONVERSATION CONTEXT LOADED:") - print("-" * 50) - if agentcore_messages: - for i, msg in enumerate(agentcore_messages, 1): - role = msg.get("role", "unknown") - role_icon = "🤖" if role == "assistant" else "👤" - content_text = "" - if "content" in msg and msg["content"]: - for content_item in msg["content"]: - if "text" in content_item: - content_text = content_item["text"] - break - content_preview = ( - f"{content_text[:80]}..." - if len(content_text) > 80 - else content_text - ) - print(f" {i}. {role_icon} {role.upper()}: {content_preview}") - else: - print(" 📭 Starting new conversation (no previous context)") - print("-" * 50) + print(f"📋 Memory ID: {memory_id}") + print(f"👤 Actor ID: {user_id}") + print(f"🔗 Session ID: {session_id}") + print("📊 LTM retrieval: /facts/{actorId} (top_k=5, relevance>=0.3)") - # Configure system prompt with user's timezone context - print("📝 Configuring video game sales analyst system prompt...") - system_prompt = DATA_ANALYST_SYSTEM_PROMPT.replace("{timezone}", user_timezone) - print( - f"✅ System prompt configured for video game sales analysis ({len(system_prompt)} characters)" - ) + # Configure system prompt with user context + system_prompt = DATA_ANALYST_SYSTEM_PROMPT.replace( + "{timezone}", user_timezone + ).replace("{user_name}", user_name) print("-" * 80) - print("🔧 Initializing video game sales analyst agent...") - - # Create specialized agent with video game sales analysis capabilities - agent = Agent( - messages=agentcore_messages, - model=bedrock_model, - system_prompt=system_prompt, - hooks=[MemoryHookProvider(memory_id, user_id, session_id, last_k_turns)], - tools=[ - get_tables_information, - current_time, - create_execute_sql_query_tool(user_message, prompt_uuid), - ], - callback_handler=None, - ) + print("🔧 Initializing agent with AgentCoreMemorySessionManager...") - print("✅ Video game sales analyst agent ready with:") - print(f" 📝 {len(agentcore_messages)} conversation context messages") - print( - " 🔧 3 specialized tools (database schema, time utilities, SQL execution)" + # Initialize session manager (explicit close instead of context manager for async generator) + session_manager = AgentCoreMemorySessionManager( + agentcore_memory_config=agentcore_memory_config, + region_name=os.environ.get("AWS_REGION", "us-east-1"), ) - print(" 🧠 Conversation memory management enabled") - - print("-" * 80) - print("🚀 Starting video game sales data analysis...") - print("=" * 80) - - # Stream the response - tool_active = False - - async for item in agent.stream_async(user_message): - if "event" in item: - event = item["event"] - # Check for tool start - if "contentBlockStart" in event and "toolUse" in event[ - "contentBlockStart" - ].get("start", {}): - tool_active = True - event_formatted = {"event": event} - yield json.dumps(event_formatted) + "\n" - - # Check for tool end - elif "contentBlockStop" in event and tool_active: - tool_active = False - - event_formatted = {"event": event} - yield json.dumps(event_formatted) + "\n" + try: + # Create agent — session_manager handles STM loading, LTM retrieval, and message saving + agent = Agent( + model=bedrock_model, + system_prompt=system_prompt, + session_manager=session_manager, + tools=[ + get_tables_information, + current_time, + create_execute_sql_query_tool(user_message, prompt_uuid), + ], + callback_handler=None, + ) - elif "start_event_loop" in item: - yield json.dumps(item) + "\n" - elif "current_tool_use" in item and tool_active: - yield json.dumps(item["current_tool_use"]) + "\n" - elif "data" in item: - yield json.dumps({"data": item["data"]}) + "\n" + print("✅ Agent ready with AgentCore Memory (STM + LTM)") + print(" 🔧 3 tools (database schema, time utilities, SQL execution)") + + print("-" * 80) + print("🚀 Starting video game sales data analysis...") + print("=" * 80) + + # Stream the response + tool_active = False + + async for item in agent.stream_async(user_message): + if "event" in item: + event = item["event"] + + if "contentBlockStart" in event and "toolUse" in event[ + "contentBlockStart" + ].get("start", {}): + tool_active = True + yield json.dumps({"event": event}) + "\n" + + elif "contentBlockStop" in event and tool_active: + tool_active = False + yield json.dumps({"event": event}) + "\n" + + elif "start_event_loop" in item: + yield json.dumps(item) + "\n" + elif "current_tool_use" in item and tool_active: + yield json.dumps(item["current_tool_use"]) + "\n" + elif "data" in item: + yield json.dumps({"data": item["data"]}) + "\n" + finally: + try: + session_manager.close() + except Exception as close_err: + print(f"⚠️ Memory flush warning (non-fatal): {close_err}") except Exception as e: import traceback tb = traceback.extract_tb(e.__traceback__) filename, line_number, function_name, text = tb[-1] - error_message = f"Error: {str(e)} (Line {line_number} in {filename})" print("\n" + "=" * 80) print("💥 VIDEO GAME SALES ANALYSIS ERROR") print("=" * 80) print(f"❌ Error: {str(e)}") - print(f"� Locatiion: Line {line_number} in {filename}") + print(f"📍 Location: Line {line_number} in {filename}") print(f"🔧 Function: {function_name}") if text: print(f"💻 Code: {text}") print("=" * 80 + "\n") - yield f"I apologize, but I encountered an error while analyzing your video game sales data request: {error_message}" + + # Send error as a proper data chunk so the frontend renders it as a normal + # assistant message and the user can continue the conversation. + error_detail = str(e)[:200] + error_msg = ( + "I'm sorry, I encountered a temporary issue processing your request. " + "Please try again — I'm ready to help with your video game sales analysis. " + f"(Details: {error_detail})" + ) + yield json.dumps({"data": error_msg}) + "\n" if __name__ == "__main__": diff --git a/02-use-cases/video-games-sales-assistant/cdk-data-analyst-assistant-agentcore-strands/data-analyst-assistant-agentcore-strands/instructions.txt b/02-use-cases/video-games-sales-assistant/cdk-data-analyst-assistant-agentcore-strands/data-analyst-assistant-agentcore-strands/instructions.txt index 2c92ae0de..3ac6eb690 100644 --- a/02-use-cases/video-games-sales-assistant/cdk-data-analyst-assistant-agentcore-strands/data-analyst-assistant-agentcore-strands/instructions.txt +++ b/02-use-cases/video-games-sales-assistant/cdk-data-analyst-assistant-agentcore-strands/data-analyst-assistant-agentcore-strands/instructions.txt @@ -1,4 +1,4 @@ -You are a multilingual chatbot Data Analyst Assistant named "Gus". You are designed to help with market video game sales data. As a data analyst, your role is to help answer users' questions by generating SQL queries against tables to obtain required results, providing answers for a C-level executive focusing on delivering business insights through extremely concise communication that prioritizes key data points and strategic implications for efficient decision-making, while maintaining a friendly conversational tone. Do not assume table structures or column names. Always verify available schema information before constructing SQL queries. Never introduce external information or personal opinions in your analysis. +You are a multilingual chatbot Data Analyst Assistant named "Gus". You are designed to help with market video game sales data. The current user's name is **{user_name}** — use it for personalized greetings and throughout the conversation. As a data analyst, your role is to help answer users' questions by generating SQL queries against tables to obtain required results, providing answers for a C-level executive focusing on delivering business insights through extremely concise communication that prioritizes key data points and strategic implications for efficient decision-making, while maintaining a friendly conversational tone. Do not assume table structures or column names. Always verify available schema information before constructing SQL queries. Never introduce external information or personal opinions in your analysis. Leverage your PostgreSQL 15.4 knowledge to create appropriate SQL statements. Do not use queries that retrieve all records in a table. If needed, ask for clarification on specific requests. diff --git a/02-use-cases/video-games-sales-assistant/cdk-data-analyst-assistant-agentcore-strands/data-analyst-assistant-agentcore-strands/src/utils/MemoryHookProvider.py b/02-use-cases/video-games-sales-assistant/cdk-data-analyst-assistant-agentcore-strands/data-analyst-assistant-agentcore-strands/src/utils/MemoryHookProvider.py deleted file mode 100644 index ac489dbc9..000000000 --- a/02-use-cases/video-games-sales-assistant/cdk-data-analyst-assistant-agentcore-strands/data-analyst-assistant-agentcore-strands/src/utils/MemoryHookProvider.py +++ /dev/null @@ -1,202 +0,0 @@ -""" -Memory Hook Provider for Bedrock Agent Core - -This module provides a hook provider for Bedrock Agent Core that manages conversation -memory. It handles loading recent conversation history when the agent starts and -saving new messages as they are added to the conversation. - -The MemoryHookProvider class integrates with the Bedrock Agent Core memory system -to provide persistent conversation history across sessions. -""" - -import logging - -from strands.hooks.events import MessageAddedEvent -from strands.hooks.registry import HookProvider, HookRegistry -from bedrock_agentcore.memory import MemoryClient - -# Setup logging -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger("personal-agent") - - -class MemoryHookProvider(HookProvider): - """ - Hook provider for managing conversation memory in Bedrock Agent Core. - - This class provides hooks for loading conversation history when the agent - initializes and saving messages as they are added to the conversation. - - Attributes: - memory_id: ID of the memory resource - actor_id: ID of the user/actor - session_id: ID of the current conversation session - last_k_turns: Number of conversation turns to retrieve from history - """ - - def __init__( - self, - memory_id: str, - actor_id: str, - session_id: str, - last_k_turns: int = 20, - ): - """ - Initialize the memory hook provider. - - Args: - memory_id: ID of the memory resource - actor_id: ID of the user/actor - session_id: ID of the current conversation session - last_k_turns: Number of conversation turns to retrieve from history (default: 20) - """ - self.memory_client = MemoryClient() - self.memory_id = memory_id - self.actor_id = actor_id - self.session_id = session_id - self.last_k_turns = last_k_turns - - def on_message_added(self, event: MessageAddedEvent): - """ - Store messages in memory as they are added to the conversation. - - This method saves each new message to the Bedrock Agent Core memory system - for future reference. - - Args: - event: Message added event - """ - messages = event.agent.messages - - print("\n" + "=" * 70) - print("💾 MEMORY HOOK - MESSAGE ADDED EVENT") - print("=" * 70) - print("📨 AGENT MESSAGES:") - print("-" * 70) - - # Display all messages in a formatted way - for idx, msg in enumerate(messages, 1): - role = msg.get("role", "unknown") - role_icon = ( - "🤖" if role == "assistant" else "👤" if role == "user" else "❓" - ) - print(f" {idx}. {role_icon} {role.upper()}:") - - if "content" in msg and msg["content"]: - for content_idx, content_item in enumerate(msg["content"], 1): - if "text" in content_item: - text_preview = ( - content_item["text"][:150] + "..." - if len(content_item["text"]) > 150 - else content_item["text"] - ) - print(f" 📝 Text: {text_preview}") - elif "toolResult" in content_item: - print( - f" 🔧 Tool Result: {content_item['toolResult'].get('toolUseId', 'N/A')}" - ) - - print("-" * 70) - - try: - last_message = messages[-1] - - print("🔍 PROCESSING LAST MESSAGE:") - print(f" 📋 Role: {last_message.get('role', 'unknown')}") - print(f" 📊 Content items: {len(last_message.get('content', []))}") - - # Check if the message has the expected structure - if ( - "role" in last_message - and "content" in last_message - and last_message["content"] - ): - role = last_message["role"] - - # Look for text content or specific toolResult content - content_to_save = None - - print(" 🔎 Searching for saveable content...") - - for content_idx, content_item in enumerate(last_message["content"], 1): - print( - f" Content item {content_idx}: {list(content_item.keys())}" - ) - - # Check for regular text content - if "text" in content_item: - content_to_save = content_item["text"] - print( - f" ✅ Found text content (length: {len(content_to_save)})" - ) - break - - # Check for toolResult with get_tables_information - elif "toolResult" in content_item: - tool_result = content_item["toolResult"] - if ( - "content" in tool_result - and tool_result["content"] - and "text" in tool_result["content"][0] - ): - tool_text = tool_result["content"][0]["text"] - # Check if it contains the specific toolUsed marker - if "'toolUsed': 'get_tables_information'" in tool_text: - content_to_save = tool_text - print( - f" ✅ Found get_tables_information tool result (length: {len(content_to_save)})" - ) - break - else: - print( - " ❌ Tool result doesn't contain get_tables_information marker" - ) - else: - print( - " ❌ Tool result missing expected content structure" - ) - - if content_to_save: - print("\n" + "=" * 50) - print("💾 SAVING TO MEMORY") - print("=" * 50) - print( - f"📝 Content preview: {content_to_save[:200]}{'...' if len(content_to_save) > 200 else ''}" - ) - print(f"👤 Role: {role}") - print(f"🆔 Memory ID: {self.memory_id}") - print(f"👤 Actor ID: {self.actor_id}") - print(f"🔗 Session ID: {self.session_id}") - print("=" * 50) - - self.memory_client.save_conversation( - memory_id=self.memory_id, - actor_id=self.actor_id, - session_id=self.session_id, - messages=[(content_to_save, role)], - ) - print("✅ SUCCESSFULLY SAVED TO MEMORY") - else: - print("❌ NO SAVEABLE CONTENT FOUND") - print( - " Reasons: No text content or get_tables_information tool result found" - ) - else: - print("❌ INVALID MESSAGE STRUCTURE") - print(" Missing required fields: role, content, or content is empty") - - except Exception as e: - print(f"💥 MEMORY SAVE ERROR: {str(e)}") - logger.error(f"Memory save error: {e}") - - print("=" * 70 + "\n") - - def register_hooks(self, registry: HookRegistry): - """ - Register memory hooks with the hook registry. - - Args: - registry: Hook registry to register with - """ - # Register memory hooks - registry.add_callback(MessageAddedEvent, self.on_message_added) diff --git a/02-use-cases/video-games-sales-assistant/cdk-data-analyst-assistant-agentcore-strands/data-analyst-assistant-agentcore-strands/src/utils/__init__.py b/02-use-cases/video-games-sales-assistant/cdk-data-analyst-assistant-agentcore-strands/data-analyst-assistant-agentcore-strands/src/utils/__init__.py index e8c8f2a12..a323c9e00 100644 --- a/02-use-cases/video-games-sales-assistant/cdk-data-analyst-assistant-agentcore-strands/data-analyst-assistant-agentcore-strands/src/utils/__init__.py +++ b/02-use-cases/video-games-sales-assistant/cdk-data-analyst-assistant-agentcore-strands/data-analyst-assistant-agentcore-strands/src/utils/__init__.py @@ -1,11 +1,7 @@ from .file_utils import load_file_content -from .agentcore_memory_utils import get_agentcore_memory_messages -from .MemoryHookProvider import MemoryHookProvider from .utils import save_raw_query_result __all__ = [ "load_file_content", - "get_agentcore_memory_messages", - "MemoryHookProvider", "save_raw_query_result", ] diff --git a/02-use-cases/video-games-sales-assistant/cdk-data-analyst-assistant-agentcore-strands/data-analyst-assistant-agentcore-strands/src/utils/agentcore_memory_utils.py b/02-use-cases/video-games-sales-assistant/cdk-data-analyst-assistant-agentcore-strands/data-analyst-assistant-agentcore-strands/src/utils/agentcore_memory_utils.py deleted file mode 100644 index bc6425fa4..000000000 --- a/02-use-cases/video-games-sales-assistant/cdk-data-analyst-assistant-agentcore-strands/data-analyst-assistant-agentcore-strands/src/utils/agentcore_memory_utils.py +++ /dev/null @@ -1,148 +0,0 @@ -""" -AgentCore Memory Utilities - -This module provides utility functions for retrieving and formatting conversation -messages from Bedrock Agent Core memory system. -""" - -import logging -from typing import List, Dict, Any -from bedrock_agentcore.memory import MemoryClient - -# Setup logging -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger("agentcore-memory-utils") - - -def get_agentcore_memory_messages( - memory_id: str, - actor_id: str, - session_id: str, - last_k_turns: int = 20, -) -> List[Dict[str, Any]]: - """ - Retrieve conversation messages from AgentCore memory and format them. - - This function retrieves the specified number of conversation turns from memory - and formats them in the standard message format with role and content structure. - - Args: - memory_id: ID of the memory resource - actor_id: ID of the user/actor - session_id: ID of the current conversation session - last_k_turns: Number of conversation turns to retrieve from history (default: 20) - - Returns: - List of formatted messages in the format: - [ - {"role": "user", "content": [{"text": "Hello, my name is Strands!"}]}, - {"role": "assistant", "content": [{"text": "Hi there! How can I help you today?"}]} - ] - - Raises: - Exception: If there's an error retrieving messages from memory - """ - try: - # Initialize memory client - memory_client = MemoryClient() - # Pretty console output for memory retrieval start - print("\n" + "=" * 70) - print("🧠 AGENTCORE MEMORY RETRIEVAL") - print("=" * 70) - print(f"📋 Memory ID: {memory_id}") - print(f"👤 Actor ID: {actor_id}") - print(f"🔗 Session ID: {session_id}") - print(f"🔄 Requesting turns: {last_k_turns}") - print("-" * 70) - - # Load the specified number of conversation turns from memory - print(f"⏳ Retrieving {last_k_turns} conversation turns from memory...") - - recent_turns = memory_client.get_last_k_turns( - memory_id=memory_id, - actor_id=actor_id, - session_id=session_id, - k=last_k_turns, - ) - - formatted_messages = [] - - if recent_turns: - print(f"✅ Successfully retrieved {len(recent_turns)} conversation turns") - print("-" * 70) - - # Process each turn in the conversation - for turn_idx, turn in enumerate(recent_turns, 1): - print(f"📝 Processing Turn {turn_idx}:") - - for msg_idx, message in enumerate(turn, 1): - # Extract role and content from the memory format - raw_role = message.get("role", "user") - - # Normalize role to lowercase to match Bedrock Converse API requirements - role = raw_role.lower() if isinstance(raw_role, str) else "user" - - if role not in ["user", "assistant"]: - print(f"⚠️ Invalid role '{role}' found, defaulting to 'user'") - role = "user" - - # Handle different content formats - content_text = "" - if "content" in message: - if ( - isinstance(message["content"], dict) - and "text" in message["content"] - ): - content_text = message["content"]["text"] - elif isinstance(message["content"], str): - content_text = message["content"] - elif isinstance(message["content"], list): - # Handle list of content items - for content_item in message["content"]: - if ( - isinstance(content_item, dict) - and "text" in content_item - ): - content_text = content_item["text"] - break - elif isinstance(content_item, str): - content_text = content_item - break - - # Skip messages with empty content - if not content_text.strip(): - print(f"⚠️ Skipping message {msg_idx} with empty content") - continue - - # Format message in the required structure - formatted_message = { - "role": role, - "content": [{"text": content_text}], - } - - formatted_messages.append(formatted_message) - - # Pretty output for each processed message - role_icon = "🤖" if role == "assistant" else "👤" - content_preview = ( - content_text[:100] + "..." - if len(content_text) > 100 - else content_text - ) - print(f" {role_icon} {role.upper()}: {content_preview}") - - print("-" * 70) - print(f"✨ Successfully formatted {len(formatted_messages)} messages") - else: - print("📭 No conversation history found in memory") - - print("=" * 70 + "\n") - # Return messages in inverted order (most recent first) - return formatted_messages[::-1] - - except Exception as e: - print("❌ ERROR: Failed to retrieve messages from AgentCore memory") - print(f"💥 Exception: {str(e)}") - print("=" * 70 + "\n") - logger.error(f"Error retrieving messages from memory: {e}") - raise Exception(f"Failed to retrieve messages from AgentCore memory: {str(e)}") diff --git a/02-use-cases/video-games-sales-assistant/images/data-analyst-assistant-agentcore-strands-agents-sdk.gif b/02-use-cases/video-games-sales-assistant/images/data-analyst-assistant-agentcore-strands-agents-sdk.gif index 8b6ac421e..e72f8d1f1 100644 Binary files a/02-use-cases/video-games-sales-assistant/images/data-analyst-assistant-agentcore-strands-agents-sdk.gif and b/02-use-cases/video-games-sales-assistant/images/data-analyst-assistant-agentcore-strands-agents-sdk.gif differ diff --git a/02-use-cases/video-games-sales-assistant/images/preview.png b/02-use-cases/video-games-sales-assistant/images/preview.png index c4c96f88f..e33fd67b4 100644 Binary files a/02-use-cases/video-games-sales-assistant/images/preview.png and b/02-use-cases/video-games-sales-assistant/images/preview.png differ diff --git a/02-use-cases/video-games-sales-assistant/images/preview1.png b/02-use-cases/video-games-sales-assistant/images/preview1.png index ecfa4031f..ff8f7281e 100644 Binary files a/02-use-cases/video-games-sales-assistant/images/preview1.png and b/02-use-cases/video-games-sales-assistant/images/preview1.png differ diff --git a/02-use-cases/video-games-sales-assistant/images/preview2.png b/02-use-cases/video-games-sales-assistant/images/preview2.png index 4d93b86d6..08a8243be 100644 Binary files a/02-use-cases/video-games-sales-assistant/images/preview2.png and b/02-use-cases/video-games-sales-assistant/images/preview2.png differ diff --git a/02-use-cases/video-games-sales-assistant/images/preview3.png b/02-use-cases/video-games-sales-assistant/images/preview3.png index 9b269b2f1..53e23803e 100644 Binary files a/02-use-cases/video-games-sales-assistant/images/preview3.png and b/02-use-cases/video-games-sales-assistant/images/preview3.png differ diff --git a/02-use-cases/video-games-sales-assistant/images/preview4.png b/02-use-cases/video-games-sales-assistant/images/preview4.png index a72d27806..16120998d 100644 Binary files a/02-use-cases/video-games-sales-assistant/images/preview4.png and b/02-use-cases/video-games-sales-assistant/images/preview4.png differ diff --git a/02-use-cases/video-games-sales-assistant/images/preview5.png b/02-use-cases/video-games-sales-assistant/images/preview5.png new file mode 100644 index 000000000..d31fcadb9 Binary files /dev/null and b/02-use-cases/video-games-sales-assistant/images/preview5.png differ