1
0
mirror of synced 2026-05-22 22:53:35 +00:00

feat(02-use-cases): Long-term AgentCore Memory Facts (#1254)

* Long-term AgentCore Memory Facts

* Lib folder updated to utils

* Lib folder updated to utils

* User name included

---------

Co-authored-by: Uriel Ramirez <beralfon@amazon.com>
This commit is contained in:
Uriel Ramirez
2026-04-15 08:21:46 -06:00
committed by GitHub
parent 49b49bae60
commit d4b3f6389a
30 changed files with 684 additions and 783 deletions
@@ -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
@@ -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
@@ -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.
> **🚀 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**.
## Prerequisites
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/)**.
Before you begin, ensure you have:
<div align="center">
<img src="./images/data-analyst-assistant-agentcore-strands-agents-sdk.gif" alt="Conversational Data Analyst Assistant Solution with Amazon Bedrock AgentCore">
</div>
- [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)
🤖 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.
Verify credentials:
✨ This solution enables users to:
```bash
aws sts get-caller-identity
```
- 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
## Set Up the Front-End Application
🚀 This reference solution can help you explore use cases like:
### Install Dependencies
- 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
Navigate to the application folder and install the dependencies:
## Solution Overview
```bash
pnpm install
```
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.
## Configure Environment Variables
Run the following script to automatically copy the example file, fetch the CDK output values, and update your `.env.local`:
```bash
cp .env.local.example .env.local
export STACK_NAME=CdkDataAnalystAssistantAgentcoreStrandsStack
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)
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.
## 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?
```
```
Which are the most popular consoles and why?
```
```
Give me a short summary and conclusion of our conversation.
```
## Deploy Your Application with Amplify Hosting
To deploy your application you can use AWS Amplify Hosting.
![Video Games Sales Assistant](./images/gen-ai-assistant-diagram.png)
> [!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.
> 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.
### 1. Connect Repository
### CDK Infrastructure Deployment
Open the [Amplify Console](https://console.aws.amazon.com/amplify/), click **Create new app**, and select your Git provider and branch.
The AWS CDK stack deploys and configures the following managed services:
### 2. Configure Environment Variables
**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
Under **App settings → Advanced settings → Environment variables**, add:
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.
| 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 | — |
**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
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.
### 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. 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
| 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.<br>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.
> 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.
### 3. Deploy
> [!TIP]
> You can also change the data source to connect to your preferred database engine by adapting the Agent's instructions and tool implementations.
Review and click **Save and deploy**. Amplify runs the build pipeline defined in `amplify.yml`:
> [!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/)**.
- **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
The **user interaction workflow** operates as follows:
## Clean Up Resources
- 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
To avoid incurring ongoing costs, remove the resources created by this tutorial:
## Deployment Instructions
1. **Delete the Amplify Hosting app**: Amplify Console → App settings → General settings → **Delete app**. This removes hosting and the backend stack (Cognito, IAM roles).
The deployment consists of two main steps:
2. **Delete the sandbox** (if still running):
```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.
> *It is recommended to use the Oregon (us-west-2) or N. Virginia (us-east-1) regions to deploy the application.*
## AWS Service Calls and Routes
### Routes
| 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
These run on the Next.js server through API routes.
| 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 |
> [!IMPORTANT]
> Remember to clean up resources after testing to avoid unnecessary costs by following the clean-up steps provided.
## Application Features
Congratulations! Your Data Analyst Assistant can provide you with the following conversational experience:
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
## License
This project is licensed under the Apache-2.0 License.
This project is licensed under the Apache-2.0 License.
@@ -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.
@@ -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
@@ -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 {
@@ -56,22 +56,66 @@ function AssistantContent({ assistantConfig }: { assistantConfig: AssistantConfi
}}
>
<h1 className="text-lg sm:text-xl font-semibold text-purple-700" style={{ letterSpacing: '-0.01em' }}>{assistantConfig.appName}</h1>
<div className="flex items-center gap-3">
<span className="text-sm text-gray-700 hidden sm:inline">{userName}</span>
<div className="flex items-center gap-2">
{/* Memory button */}
{assistantConfig.memoryId && (
<button
onClick={() => window.dispatchEvent(new CustomEvent('open-memory-panel'))}
className="flex"
style={{
display: 'flex',
alignItems: 'center',
gap: 6,
padding: '5px 12px 5px 8px',
color: '#7c3aed',
cursor: 'pointer',
background: 'transparent',
border: '1px solid transparent',
borderRadius: 8,
fontSize: 13,
fontWeight: 500,
transition: 'all 0.15s',
}}
onMouseEnter={(e) => { e.currentTarget.style.background = 'rgba(124,58,237,0.06)'; e.currentTarget.style.borderColor = 'rgba(124,58,237,0.2)'; }}
onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.borderColor = 'transparent'; }}
aria-label="View memory facts"
title="Long-term memory"
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src="/images/agentcore-memory.png" alt="" width={18} height={18} style={{ width: 18, height: 18 }} />
<span className="hidden sm:inline">Memory Facts</span>
</button>
)}
{/* Separator */}
<div style={{ width: 1, height: 24, background: '#e5e7eb', margin: '0 4px' }} />
{/* User name */}
<span className="text-sm text-gray-600 hidden sm:inline" style={{ fontWeight: 500 }}>{userName}</span>
{/* Sign out button */}
<button
onClick={handleSignOut}
className="p-1.5"
style={{
color: '#7c3aed',
display: 'flex',
alignItems: 'center',
gap: 5,
padding: '5px 10px',
color: '#6b7280',
cursor: 'pointer',
background: 'none',
border: 'none',
transition: 'color 0.15s',
background: 'transparent',
border: '1px solid transparent',
borderRadius: 8,
fontSize: 13,
fontWeight: 500,
transition: 'all 0.15s',
}}
onMouseEnter={(e) => { e.currentTarget.style.background = 'rgba(0,0,0,0.04)'; e.currentTarget.style.borderColor = '#e5e7eb'; }}
onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.borderColor = 'transparent'; }}
aria-label="Sign out"
title="Sign out"
>
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<svg width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15m3-3l3-3m0 0l-3-3m3 3H9" />
</svg>
</button>
@@ -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),
};
@@ -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<React.SetStateAction<ControlAnswer[]>>;
@@ -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 = {
@@ -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<MemoryFact[]> => {
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 [];
}
};
@@ -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;
}
@@ -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<Answer[]>([]);
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<string | null>(null);
const [inputHovered, setInputHovered] = useState(false);
@@ -29,8 +31,26 @@ export default function Chat({ config, identityPoolId, userPoolId }: ChatProps)
const chatContainerRef = useRef<HTMLDivElement>(null);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const isSubmittingRef = useRef(false);
const [memoryPanelOpen, setMemoryPanelOpen] = useState(false);
const [memoryFacts, setMemoryFacts] = useState<MemoryFact[]>([]);
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 (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<div style={{ display: 'flex', flexDirection: 'column', height: '100%', position: 'relative', overflow: 'hidden' }}>
{errorMessage && (
<div style={{ position: 'fixed', top: 64, left: '50%', transform: 'translateX(-50%)', width: '80%', maxWidth: 640, zIndex: 50, background: '#fef2f2', border: '1px solid #fecaca', color: '#b91c1c', padding: '12px 16px', borderRadius: 12, display: 'flex', alignItems: 'center', justifyContent: 'space-between', boxShadow: '0 4px 12px rgba(0,0,0,0.08)' }}>
<span style={{ fontSize: 14 }}>{errorMessage}</span>
@@ -268,6 +309,68 @@ export default function Chat({ config, identityPoolId, userPoolId }: ChatProps)
</button>
</div>
</div>
{/* Memory Facts Panel — full width on tablet, constrained on desktop */}
{memoryPanelOpen && (
<div className="memory-panel" style={{ position: 'absolute', top: 0, right: 0, bottom: 0, width: '100%', background: '#fff', boxShadow: '-4px 0 24px rgba(0,0,0,0.1)', zIndex: 40, display: 'flex', flexDirection: 'column', borderLeft: '1px solid #e5e7eb' }}>
{/* Panel header */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '14px 20px', borderBottom: '1px solid #e5e7eb' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#7c3aed" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round">
<path d="M12 2a7 7 0 0 1 7 7c0 2.38-1.19 4.47-3 5.74V17a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-2.26C6.19 13.47 5 11.38 5 9a7 7 0 0 1 7-7z" />
<line x1="9" y1="21" x2="15" y2="21" />
</svg>
<div>
<span style={{ fontWeight: 600, fontSize: 15, color: '#1f2937' }}>Long-term Memory</span>
<span style={{ fontSize: 12, color: '#9ca3af', fontWeight: 400, marginLeft: 6 }}>({memoryFacts.length})</span>
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<button onClick={loadMemoryFacts} disabled={memoryLoading} style={{ background: 'none', border: 'none', cursor: memoryLoading ? 'default' : 'pointer', color: memoryLoading ? '#d1d5db' : '#7c3aed', padding: 6, borderRadius: 6, transition: 'color 0.15s' }} aria-label="Refresh memory facts" title="Refresh">
<svg className={memoryLoading ? 'animate-spin' : ''} width="18" height="18" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}><path strokeLinecap="round" strokeLinejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /></svg>
</button>
<button onClick={() => setMemoryPanelOpen(false)} style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#6b7280', padding: 6, borderRadius: 6 }} aria-label="Close memory panel">
<svg width="18" height="18" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}><path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
</button>
</div>
</div>
{/* Description */}
<div style={{ padding: '10px 20px', background: 'rgba(124,58,237,0.03)', borderBottom: '1px solid #f3f4f6' }}>
<p style={{ fontSize: 12, color: '#6b7280', lineHeight: 1.5, margin: 0 }}>
Facts extracted by <a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/memory.html" target="_blank" rel="noopener noreferrer" style={{ color: '#7c3aed', textDecoration: 'none' }}>Amazon Bedrock AgentCore Memory</a> from your conversations. These persist across sessions and help the assistant provide personalized, context-aware responses.
</p>
</div>
{/* Facts list */}
<div style={{ flex: 1, overflowY: 'auto', padding: '12px 20px' }}>
{memoryLoading && memoryFacts.length === 0 ? (
<div style={{ textAlign: 'center', padding: 40, color: '#9ca3af' }}>
<svg className="animate-spin" style={{ width: 24, height: 24, margin: '0 auto 12px', color: '#7c3aed' }} viewBox="0 0 24 24" fill="none"><circle style={{ opacity: 0.25 }} cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" /><path style={{ opacity: 0.75 }} fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" /></svg>
<p style={{ fontSize: 13 }}>Loading memory facts...</p>
</div>
) : memoryFacts.length === 0 ? (
<div style={{ textAlign: 'center', padding: 40, color: '#9ca3af' }}>
<p style={{ fontSize: 14, marginBottom: 4 }}>No facts extracted yet</p>
<p style={{ fontSize: 12 }}>Facts are extracted asynchronously after conversations. They may take 20-40 seconds to appear.</p>
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
{memoryFacts.map((fact) => (
<div key={fact.memoryRecordId} style={{ padding: '10px 14px', borderRadius: 10, background: 'linear-gradient(135deg, rgba(124,58,237,0.04) 0%, rgba(168,85,247,0.08) 100%)', border: '1px solid rgba(124,58,237,0.1)' }}>
<p style={{ fontSize: 13, color: '#1f2937', lineHeight: 1.5, margin: 0 }}>{fact.content}</p>
{fact.createdAt && (
<p style={{ fontSize: 11, color: '#9ca3af', marginTop: 6, marginBottom: 0 }}>
{new Date(fact.createdAt).toLocaleString()}
</p>
)}
</div>
))}
</div>
)}
</div>
</div>
)}
</div>
);
}
@@ -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;
}
}
@@ -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<T>(
ClientClass: new (config: { region: string; credentials: ReturnType<typeof fromCognitoIdentityPool> }) => 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 });
}
@@ -6,3 +6,7 @@ node_modules
# CDK asset staging directory
.cdk.staging
cdk.out
# Python
__pycache__/
*.pyc
@@ -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/<runtimeId>`)
- Runtime traces → AWS X-Ray
- Memory extraction logs → CloudWatch Logs (`/aws/vendedlogs/bedrock-agentcore/memory/<memoryId>`)
- 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.
@@ -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",
@@ -3,17 +3,16 @@ Video Games Sales Data Analyst Assistant - Main Application
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 bedrock_agentcore import BedrockAgentCoreApp
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 create_execute_sql_query_tool(user_prompt: str, prompt_uuid: 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 create_execute_sql_query_tool(user_prompt: str, prompt_uuid: 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 create_execute_sql_query_tool(user_prompt: str, prompt_uuid: 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("🧠 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(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 context
system_prompt = DATA_ANALYST_SYSTEM_PROMPT.replace(
"{timezone}", user_timezone
).replace("{user_name}", user_name)
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("🔧 Initializing agent with AgentCoreMemorySessionManager...")
# 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 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)
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,
)
# 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)"
)
print("✅ Agent ready with AgentCore Memory (STM + LTM)")
print(" 🔧 3 tools (database schema, time utilities, SQL execution)")
print("-" * 80)
print("🔧 Initializing video game sales analyst agent...")
print("-" * 80)
print("🚀 Starting video game sales data analysis...")
print("=" * 80)
# 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,
)
# Stream the response
tool_active = False
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)"
)
print(" 🧠 Conversation memory management enabled")
async for item in agent.stream_async(user_message):
if "event" in item:
event = item["event"]
print("-" * 80)
print("🚀 Starting video game sales data analysis...")
print("=" * 80)
if "contentBlockStart" in event and "toolUse" in event[
"contentBlockStart"
].get("start", {}):
tool_active = True
yield json.dumps({"event": event}) + "\n"
# Stream the response
tool_active = False
elif "contentBlockStop" in event and tool_active:
tool_active = False
yield json.dumps({"event": event}) + "\n"
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"
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"
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__":
@@ -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.
@@ -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)
@@ -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",
]
@@ -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)}")
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 MiB

After

Width:  |  Height:  |  Size: 4.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 232 KiB

After

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 395 KiB

After

Width:  |  Height:  |  Size: 612 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 330 KiB

After

Width:  |  Height:  |  Size: 447 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 353 KiB

After

Width:  |  Height:  |  Size: 375 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 566 KiB

After

Width:  |  Height:  |  Size: 311 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 447 KiB