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

agentcore memory browser (#422)

* agentcore memory browser

* Update README.md

Signed-off-by: Anil Gurrala <136643863+visitani@users.noreply.github.com>

* resolved comments

* removed memory id from exception messages

* removed the sys

* Remove .vscode/settings.json and add to .gitignore

* Fixed securtiy related issues

* renamed folder name

* renamed folder name

* fixed linting errors

* fixed linting errors

* format change fixed

---------

Signed-off-by: Anil Gurrala <136643863+visitani@users.noreply.github.com>
Signed-off-by: Akarsha Sehwag <akshseh@amazon.de>
Co-authored-by: Akarsha Sehwag <akshseh@amazon.de>
This commit is contained in:
Anil Gurrala
2025-11-14 11:26:01 -08:00
committed by GitHub
parent 0bef0881a4
commit 5171660819
17 changed files with 26887 additions and 1 deletions
+1 -1
View File
@@ -239,4 +239,4 @@ cdk.out/
### Bedrock AgentCore ###
.bedrock_agentcore/
.bedrock_agentcore.yaml
**/screenshots/screenshot_*.png
.vscode/
@@ -0,0 +1,12 @@
# AgentCore Memory Dashboard - Frontend Configuration
# Backend API URL
REACT_APP_BACKEND_URL=http://localhost:8000
# Dashboard Settings
REACT_APP_MAX_MEMORY_ENTRIES=50
REACT_APP_REFRESH_INTERVAL=5000
REACT_APP_DEBUG_MODE=true
# Note: Memory ID, Actor ID, and Session ID are entered by users through the UI
# No need to configure them here as environment variables
@@ -0,0 +1,78 @@
# Dependencies
node_modules/
backend/venv/
backend/.venv/
.venv/
# Environment variables with sensitive/personal data
# backend/.env - now cleaned and safe to share
# Keep both .env files as they only contain non-sensitive config
# .env (frontend) - safe to share
# backend/.env - safe to share (cleaned of personal AWS profile)
# Python cache
__pycache__/
*.pyc
*.pyo
*.pyd
.Python
*.so
# Build outputs
build/
dist/
# IDE files
.vscode/
.idea/
*.swp
*.swo
# OS files
.DS_Store
Thumbs.db
# Logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Coverage directory used by tools like istanbul
coverage/
# nyc test coverage
.nyc_output
# Dependency directories
jspm_packages/
# Optional npm cache directory
.npm
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env.local
.env.development.local
.env.test.local
.env.production.local
# AWS credentials (if accidentally placed in project)
.aws/
credentials
config
@@ -0,0 +1,241 @@
# AgentCore Memory Dashboard
A lightweight React + FastAPI dashboard for browsing AWS Bedrock AgentCore Memory data.
**📦 Repository Size**: ~2MB (dependencies excluded - see setup instructions below)
## ✨ Key Features
- **Dynamic Configuration**: Memory ID, Actor ID, and Session ID entered through UI
- **Short-Term Memory**: Query conversation events and turns
- **Long-Term Memory**: Browse facts, preferences, and summaries
- **Real-time Search**: Content filtering with live results
## 📋 Prerequisites
- **Node.js** 16+
- **Python** 3.8+
- **AWS CLI** configured with credentials
- **AWS Bedrock AgentCore Memory** access
## 🔑 AWS Credentials Setup
### Step 1: Install AWS CLI
```bash
# macOS
brew install awscli
# Linux
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install
# Windows
# Download and run the AWS CLI MSI installer from AWS website
```
### Step 2: Configure AWS Credentials
Choose one of these methods:
#### Option A: AWS Configure (Recommended)
```bash
aws configure
```
Enter your:
- AWS Access Key ID
- AWS Secret Access Key
- Default region (e.g., `us-east-1`)
- Default output format (e.g., `json`)
#### Option B: Environment Variables
```bash
export AWS_ACCESS_KEY_ID=your-access-key-id
export AWS_SECRET_ACCESS_KEY=your-secret-access-key
export AWS_DEFAULT_REGION=us-east-1
```
#### Option C: AWS Credentials File
Create `~/.aws/credentials`:
```ini
[default]
aws_access_key_id = your-access-key-id
aws_secret_access_key = your-secret-access-key
```
Create `~/.aws/config`:
```ini
[default]
region = us-east-1
output = json
```
### Step 3: Verify AWS Access
```bash
# Test AWS connection
aws sts get-caller-identity
# Test Bedrock access
aws bedrock list-foundation-models --region us-east-1
```
### Step 4: Required IAM Permissions
Your AWS user/role needs these permissions:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"bedrock-agentcore:ListMemoryRecords",
"bedrock-agentcore:ListEvents",
"bedrock-agentcore:GetLastKTurns",
"bedrock-agentcore:RetrieveMemories",
"bedrock-agentcore:GetMemoryStrategies"
],
"Resource": "*"
}
]
}
```
## 🚀 Quick Start Guide
### Step 1: Clone and Setup
```bash
# Clone the repository
git clone <repository-url>
cd 01-tutorials/04-AgentCore-memory/03-advanced-patterns/04-memory-browser
# Install frontend dependencies (this will download ~200MB of packages)
npm install
```
**Note**:
- 📦 **Dependencies not included**: `node_modules` and `backend/venv` are excluded from the repository
- 🔧 **First-time setup**: Run `npm install` to download all frontend dependencies
-**Frontend `.env`**: Already configured with default settings
-**Backend `.env`**: Needs to be created (see Step 2)
### Step 2: Configure Environment Variables
#### Backend Configuration
Copy the example file and customize:
```bash
# Copy the example file
cp backend/.env.example backend/.env
# Edit backend/.env and set your AWS profile (if needed)
# AWS_PROFILE=your-profile-name
```
The `backend/.env` file should contain:
```env
# AWS Configuration (region will be auto-detected from AWS CLI/profile if not set)
# AWS_REGION=us-east-1
# Server Configuration
# Security: Use 127.0.0.1 for local development (recommended)
# Only use 0.0.0.0 if you need to access from other machines on your network
BACKEND_HOST=127.0.0.1
BACKEND_PORT=8000
DEBUG=true
# CORS Configuration
ALLOWED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000
# Optional: AWS Profile (if using multiple profiles)
# AWS_PROFILE=your-profile-name
```
**Security Note**: The backend now binds to `127.0.0.1` (localhost only) by default for security. This prevents exposure to all network interfaces. If you need to access the backend from other machines on your network, set `BACKEND_HOST=0.0.0.0` in your `.env` file, but be aware this exposes the service to your entire network.
**Note**: AWS region is automatically detected from your AWS CLI configuration. Only set `AWS_REGION` if you need to override the default.
#### Frontend Configuration
The frontend `.env` file is already configured with default values. You can modify it if needed:
```env
# Backend API URL
REACT_APP_BACKEND_URL=http://localhost:8000
# Dashboard Settings
REACT_APP_MAX_MEMORY_ENTRIES=50
REACT_APP_REFRESH_INTERVAL=5000
```
### Step 3: Install Backend Dependencies
```bash
# Navigate to backend directory
cd backend
# Create Python virtual environment (isolated Python packages)
python3 -m venv venv
# Activate virtual environment
# On macOS/Linux:
source venv/bin/activate
# On Windows:
# venv\Scripts\activate
# Install Python dependencies (~50MB of packages)
pip install -r requirements.txt
# Return to project root
cd ..
```
**Note**: The virtual environment (`backend/venv/`) is excluded from the repository to keep it lightweight.
### Step 4: Start the Application
#### Option A: Start Both Services Together (Recommended)
```bash
# From project root directory
npm run dev
```
This will start both the backend (FastAPI) and frontend (React) simultaneously.
#### Option B: Start Services Separately
```bash
# Terminal 1: Start backend
npm run start-backend
# Terminal 2: Start frontend
npm start
```
### Step 5: Access the Dashboard
- **Frontend**: http://localhost:3000
- **Backend API**: http://localhost:8000
- **API Documentation**: http://localhost:8000/docs
### Step 6: Configure Memory Access
1. Open the dashboard at http://localhost:3000
2. Enter your **Memory ID** and **Actor ID** in the header
3. Click **Configure** to validate access
4. Start querying your AgentCore Memory data!
## 📊 Dashboard Features
### Short-Term Memory
- Query conversation events and turns
- Filter by content, event type, and role
### Long-Term Memory
- **User Input Required**: Memory ID and Namespace (entered via UI)
- Namespace-based querying with content filtering
- Browse facts, preferences, and summaries
## 🔧 Troubleshooting
### Common Issues
- **Backend won't start**: Check Python virtual environment is activated
- **Frontend can't connect**: Verify backend is running on port 8000
- **AWS permission errors**: Run `aws sts get-caller-identity` to verify credentials
- **Memory ID not found**: Check Memory ID exists and you have proper permissions
---
@@ -0,0 +1,20 @@
# AgentCore Memory Backend Configuration
# AWS Configuration (region will be auto-detected from AWS CLI/profile if not set)
# AWS_REGION=us-east-1
# Optional: AWS Profile (if using multiple profiles)
# AWS_PROFILE=your-profile-name
# Server Configuration
# Security: Use 127.0.0.1 for local development (recommended)
# Only use 0.0.0.0 if you need to access from other machines on your network
BACKEND_HOST=127.0.0.1
BACKEND_PORT=8000
DEBUG=true
# CORS Configuration
ALLOWED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000
# Note: Memory ID, Actor ID, and Session ID are entered by users through the dashboard UI
# No need to configure them here as environment variables
@@ -0,0 +1,7 @@
fastapi>=0.110.0
uvicorn[standard]>=0.27.0
pydantic>=2.6.0
boto3>=1.34.0
python-dotenv==1.0.0
requests>=2.31.0
bedrock-agentcore>=0.1.0
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,49 @@
{
"name": "agentcore-memory-dashboard",
"version": "1.0.0",
"private": true,
"dependencies": {
"@aws-sdk/client-bedrock-agentcore": "^3.887.0",
"@aws-sdk/credential-providers": "^3.887.0",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.3.0",
"@testing-library/user-event": "^13.5.0",
"axios": "^1.4.0",
"date-fns": "^2.30.0",
"lucide-react": "^0.263.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"recharts": "^2.8.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"start-backend": "./start-backend.sh",
"dev": "concurrently \"npm run start-backend\" \"npm start\"",
"get-credentials": "node scripts/get-aws-credentials.js"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"concurrently": "^9.2.1"
}
}
@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="AgentCore Memory Dashboard - Track short-term and long-term memory"
/>
<title>AgentCore Memory Dashboard</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>
@@ -0,0 +1,82 @@
#!/usr/bin/env node
/**
* Helper script to get temporary AWS credentials for the React app
* This script uses the AWS CLI configuration to get temporary credentials
* that can be used in the browser environment.
*/
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
async function getTemporaryCredentials() {
try {
console.log('🔑 Setting up AWS configuration for the dashboard...');
// First, check if we can access AWS
try {
const identity = execSync('aws sts get-caller-identity --output json', { encoding: 'utf8' });
const identityData = JSON.parse(identity);
console.log(`✅ AWS Identity confirmed: ${identityData.Arn}`);
} catch (error) {
throw new Error('AWS CLI not configured or no valid credentials found');
}
// Get current AWS region
let region = null;
try {
const configOutput = execSync('aws configure get region', { encoding: 'utf8' });
region = configOutput.trim();
if (!region) {
throw new Error('No AWS region configured. Run: aws configure set region <your-region>');
}
} catch (error) {
throw new Error('AWS region not configured. Run: aws configure set region <your-region>');
}
// Create environment variables content for frontend configuration
const envContent = `
# AgentCore Memory Dashboard - Frontend Configuration
# Generated on: ${new Date().toISOString()}
#
# Note: This dashboard uses a backend proxy approach for AWS credentials
# since browser applications cannot directly use AWS CLI credentials for security reasons.
# Backend API URL
REACT_APP_BACKEND_URL=http://localhost:8000
# Dashboard Settings
REACT_APP_MAX_MEMORY_ENTRIES=50
REACT_APP_REFRESH_INTERVAL=5000
REACT_APP_DEBUG_MODE=true
# Note: Memory ID, Actor ID, and Session ID are entered by users through the UI
# No hardcoded values needed here
`.trim();
// Write to .env file
const envPath = path.join(__dirname, '..', '.env');
fs.writeFileSync(envPath, envContent);
console.log('✅ Frontend configuration saved to .env file');
console.log('🔧 Dashboard configured to use backend proxy for AWS credentials');
console.log('🚀 You can now run: npm run dev');
console.log('');
console.log('📝 Note: Memory ID, Actor ID, and Session ID will be entered through the UI');
console.log(' No hardcoded values are stored in configuration files.');
} catch (error) {
console.error('❌ Error setting up configuration:');
console.error(error.message);
console.error('\n💡 Troubleshooting:');
console.error('1. Run: aws configure list');
console.error('2. Run: aws sts get-caller-identity');
console.error('3. Make sure you have AWS CLI installed and configured');
console.error('4. Ensure your AWS credentials have Bedrock AgentCore permissions');
process.exit(1);
}
}
// Run the script
getTemporaryCredentials();
@@ -0,0 +1,571 @@
import React, { useState, useEffect } from 'react';
import {
Clock,
Database,
RefreshCw,
MessageSquare,
Search,
CheckCircle,
Loader
} from 'lucide-react';
import ShortTermMemoryForm from './components/ShortTermMemoryForm';
import LongTermMemoryForm from './components/LongTermMemoryForm';
function App() {
const [activeTab, setActiveTab] = useState('shortterm');
const [lastUpdated, setLastUpdated] = useState(new Date());
const [shortTermMemories, setShortTermMemories] = useState([]);
const [longTermMemories, setLongTermMemories] = useState([]);
// Global configuration for both memory types
const [globalConfig, setGlobalConfig] = useState({ memory_id: '', actor_id: '' });
const [availableNamespaces, setAvailableNamespaces] = useState([]);
const [isConfigured, setIsConfigured] = useState(false);
const [hasBeenConfigured, setHasBeenConfigured] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
// Short-term memory filter states
const [eventTypeFilter, setEventTypeFilter] = useState('all');
const [roleFilter, setRoleFilter] = useState('all');
const [sortBy, setSortBy] = useState('timestamp');
const [sortOrder, setSortOrder] = useState('desc');
const [contentSearch, setContentSearch] = useState('');
// Long-term memory filter states
const [ltSearchQuery, setLtSearchQuery] = useState('');
const [ltSortOrder, setLtSortOrder] = useState('desc');
const refreshData = () => {
setLoading(true);
setLastUpdated(new Date());
setTimeout(() => setLoading(false), 1000); // Simulate refresh
};
const handleShortTermMemoryFetch = (memories) => {
setShortTermMemories(memories);
console.log('Short-term memories fetched:', memories);
console.log('Current filters:', { eventTypeFilter, roleFilter, contentSearch });
// Debug: Show what types and roles are actually in the data
const types = [...new Set(memories.map(m => m.type))];
const roles = [...new Set(memories.map(m => m.role))];
console.log('Available types in data:', types);
console.log('Available roles in data:', roles);
};
const handleLongTermMemoryFetch = (memories) => {
setLongTermMemories(memories);
console.log('Long-term memories fetched:', memories);
};
const handleGlobalConfigUpdate = async (config) => {
setLoading(true);
setError('');
console.log('Global memory configuration updated:', config);
// Discover namespaces for long-term memory to validate the Memory ID
try {
const response = await fetch('http://localhost:8000/api/agentcore/listNamespaces', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
memory_id: config.memory_id,
max_results: 100
})
});
const textResponse = await response.text();
let data;
try {
data = JSON.parse(textResponse);
} catch (parseError) {
console.error('Non-JSON response from backend:', textResponse);
throw new Error(`Backend returned non-JSON response (status ${response.status}). Check backend logs.`);
}
if (!response.ok) {
const errorMessage = data.detail || `Request failed with status ${response.status}`;
throw new Error(errorMessage);
}
// Configuration is valid, update state
setGlobalConfig(config);
setIsConfigured(true);
setHasBeenConfigured(true);
if (data.namespaces && data.namespaces.length > 0) {
setAvailableNamespaces(data.namespaces);
console.log('Available namespaces discovered:', data.namespaces);
}
} catch (err) {
console.error('❌ Memory configuration error:', err);
// Parse specific error messages from backend
let errorMessage = 'Failed to validate memory configuration';
// Handle fetch API errors (not axios)
if (err.message && err.message.includes('Failed to fetch')) {
errorMessage = 'Unable to connect to backend server. Please ensure the backend is running.';
} else if (err.message) {
errorMessage = err.message;
}
setError(errorMessage);
// Don't update configuration if validation failed
setIsConfigured(false);
setHasBeenConfigured(false);
} finally {
setLoading(false);
}
};
const handleNamespacesFound = (namespaces) => {
setAvailableNamespaces(namespaces);
console.log('Available namespaces:', namespaces);
};
const tabs = [
{ id: 'shortterm', label: 'Short-Term Memory', icon: <MessageSquare size={16} /> },
{ id: 'longterm', label: 'Long-Term Memory', icon: <Database size={16} /> }
];
const renderResultsContent = () => {
switch (activeTab) {
case 'shortterm':
return shortTermMemories.length > 0 ? (
<div className="results-section">
<div className="results-header">
<div className="results-title">
<h2>Short-Term Memory Results</h2>
<div className="results-count">
<span className="count-badge">
{shortTermMemories.filter(memory => {
if (contentSearch && (!memory.content || !memory.content.toLowerCase().includes(contentSearch.toLowerCase()))) {
return false;
}
if (eventTypeFilter !== 'all' && memory.type !== eventTypeFilter) {
return false;
}
if (roleFilter !== 'all' && memory.role !== roleFilter) {
return false;
}
return true;
}).length}
</span>
<span className="count-text">of {shortTermMemories.length} entries</span>
</div>
</div>
</div>
{/* Search and Filter Controls */}
<div className="results-controls">
<div className="search-control">
<Search size={16} className="search-icon" />
<input
type="text"
placeholder="Search in memory content..."
value={contentSearch}
onChange={(e) => setContentSearch(e.target.value)}
className="search-input"
/>
</div>
<select
value={eventTypeFilter}
onChange={(e) => setEventTypeFilter(e.target.value)}
className="filter-select"
>
<option value="all">All Types</option>
<option value="conversation">Conversations</option>
<option value="event">Events</option>
</select>
<select
value={roleFilter}
onChange={(e) => setRoleFilter(e.target.value)}
className="filter-select"
>
<option value="all">All Roles</option>
<option value="user">User Only</option>
<option value="assistant">Assistant Only</option>
</select>
<select
value={sortOrder}
onChange={(e) => setSortOrder(e.target.value)}
className="sort-select"
>
<option value="desc">Newest First</option>
<option value="asc">Oldest First</option>
</select>
</div>
<div className="memory-list">
{(() => {
const filtered = shortTermMemories.filter(memory => {
if (contentSearch && (!memory.content || !memory.content.toLowerCase().includes(contentSearch.toLowerCase()))) {
return false;
}
if (eventTypeFilter !== 'all' && memory.type !== eventTypeFilter) {
return false;
}
if (roleFilter !== 'all') {
const memoryRole = (memory.role || '').toLowerCase();
if (roleFilter === 'user' && memoryRole !== 'user') {
return false;
}
if (roleFilter === 'assistant' && memoryRole !== 'assistant') {
return false;
}
}
return true;
});
console.log(`Filtering: ${shortTermMemories.length}${filtered.length} results`);
console.log('Filter breakdown:', {
contentSearch: contentSearch || 'none',
eventTypeFilter,
roleFilter,
totalMemories: shortTermMemories.length,
filteredMemories: filtered.length
});
return filtered;
})()
.sort((a, b) => {
if (sortBy === 'timestamp') {
const aTime = new Date(a.timestamp || 0);
const bTime = new Date(b.timestamp || 0);
return sortOrder === 'desc' ? bTime - aTime : aTime - bTime;
} else if (sortBy === 'size') {
const aSize = a.size || 0;
const bSize = b.size || 0;
return sortOrder === 'desc' ? bSize - aSize : aSize - bSize;
}
return 0;
})
.map((memory, index) => (
<div key={memory.id || index} className="memory-item shortterm">
<div className="memory-item-header">
<div className="memory-badges">
<span className={`memory-type ${memory.type || 'event'}`}>
{memory.type || 'Event'}
</span>
{memory.event_type && (
<span className="event-type-badge">
{memory.event_type}
</span>
)}
{memory.role && (
<span className="role-badge">
{memory.role}
</span>
)}
</div>
<span className="memory-timestamp">
{new Date(memory.timestamp).toLocaleString()}
</span>
</div>
<div className="memory-content">
{memory.content}
</div>
<div className="memory-metadata">
{memory.actor_id && <span>Actor: {memory.actor_id}</span>}
{memory.session_id && <span>Session: {memory.session_id}</span>}
{memory.event_id && <span>Event ID: {memory.event_id}</span>}
<span>Size: {memory.size} chars</span>
</div>
</div>
))}
</div>
</div>
) : (
<div className="empty-state">
<MessageSquare size={48} className="empty-icon" />
<h3>No Records Found</h3>
<p>No short-term memory data found for this session. Try different parameters.</p>
</div>
);
case 'longterm':
return longTermMemories.length > 0 ? (
<div className="results-section">
<div className="results-header">
<div className="results-title">
<h2>Long-Term Memory Results</h2>
<div className="results-count">
<span className="count-badge">
{longTermMemories.filter(memory => {
if (ltSearchQuery && (!memory.content || !memory.content.toLowerCase().includes(ltSearchQuery.toLowerCase()))) {
return false;
}
return true;
}).length}
</span>
<span className="count-text">of {longTermMemories.length} entries</span>
</div>
</div>
</div>
{/* Search and Sort Controls */}
<div className="results-controls">
<div className="search-control">
<Search size={16} className="search-icon" />
<input
type="text"
placeholder="Search in memory content..."
value={ltSearchQuery}
onChange={(e) => setLtSearchQuery(e.target.value)}
className="search-input"
/>
</div>
<select
value={ltSortOrder}
onChange={(e) => setLtSortOrder(e.target.value)}
className="sort-select"
>
<option value="desc">Newest First</option>
<option value="asc">Oldest First</option>
</select>
</div>
<div className="memory-list">
{longTermMemories
.filter(memory => {
if (ltSearchQuery && (!memory.content || !memory.content.toLowerCase().includes(ltSearchQuery.toLowerCase()))) {
return false;
}
return true;
})
.sort((a, b) => {
const aTime = new Date(a.timestamp || 0);
const bTime = new Date(b.timestamp || 0);
return ltSortOrder === 'desc' ? bTime - aTime : aTime - bTime;
})
.map((memory, index) => (
<div key={memory.id || index} className="memory-item longterm">
<div className="memory-item-header">
<div className="memory-badges">
<span className={`memory-type ${memory.type || 'record'}`}>
{memory.type || 'Record'}
</span>
{memory.namespace && (
<span className="namespace-badge">
{memory.namespace}
</span>
)}
</div>
<span className="memory-timestamp">
{new Date(memory.timestamp).toLocaleString()}
</span>
</div>
<div className="memory-content">
{memory.content}
</div>
<div className="memory-metadata">
{memory.namespace && <span>Namespace: {memory.namespace}</span>}
{memory.score && <span>Score: {memory.score}</span>}
<span>Size: {memory.size} chars</span>
</div>
</div>
))}
</div>
</div>
) : (
<div className="empty-state">
<Database size={48} className="empty-icon" />
<h3>No Data Found</h3>
<p>No long-term memory records found for this namespace. Try different filters.</p>
</div>
);
default:
return (
<div className="empty-state">
<Database size={48} className="empty-icon" />
<h3>Select Memory Type</h3>
<p>Choose Short-Term or Long-Term memory from the sidebar to get started.</p>
</div>
);
}
};
return (
<div className="dashboard">
<div className="container">
<header className="modern-header">
<div className="header-brand">
<div className="brand-icon">
<Database size={28} />
</div>
<div className="brand-text">
<h1>AgentCore Memory</h1>
<span className="brand-subtitle">Real-time monitoring dashboard</span>
</div>
</div>
{/* Memory Configuration in Header */}
<div className="header-config">
<div className="config-field-inline">
<label>Memory ID <span className="required-asterisk">*</span></label>
<input
type="text"
value={globalConfig.memory_id}
onChange={(e) => setGlobalConfig(prev => ({ ...prev, memory_id: e.target.value }))}
placeholder="your-memory-id-here"
className="header-input"
required
/>
</div>
<div className="config-field-inline">
<label>Actor ID <span className="required-asterisk">*</span></label>
<input
type="text"
value={globalConfig.actor_id}
onChange={(e) => setGlobalConfig(prev => ({ ...prev, actor_id: e.target.value }))}
placeholder="DEFAULT"
className="header-input"
required
/>
</div>
<button
onClick={async () => {
if (hasBeenConfigured) {
// Reconfigure: Clear everything and reset
setGlobalConfig({ memory_id: '', actor_id: '' });
setHasBeenConfigured(false);
setIsConfigured(false);
setShortTermMemories([]);
setLongTermMemories([]);
setAvailableNamespaces([]);
setError('');
} else {
// Configure: Validate and update
if (globalConfig.memory_id.trim() && globalConfig.actor_id.trim()) {
await handleGlobalConfigUpdate(globalConfig);
}
}
}}
className="header-update-btn"
disabled={loading || (!hasBeenConfigured && (!globalConfig.memory_id.trim() || !globalConfig.actor_id.trim()))}
>
{loading ? (
<>
<Loader size={14} className="spinning" />
Validating...
</>
) : (
hasBeenConfigured ? 'Reconfigure' : 'Configure'
)}
</button>
</div>
<div className="header-actions">
<div className="status-indicator">
<div className="status-dot"></div>
<span>Live</span>
</div>
<div className="last-updated-compact">
<Clock size={14} />
<span>{lastUpdated.toLocaleTimeString()}</span>
</div>
<button className="refresh-button-modern" onClick={refreshData} disabled={loading}>
<RefreshCw size={16} className={loading ? 'animate-spin' : ''} />
<span>{loading ? 'Refreshing...' : 'Refresh'}</span>
</button>
</div>
{/* Error Display */}
{error && (
<div className="error-banner-modern">
<div className="error-icon"></div>
<span>{error}</span>
</div>
)}
</header>
<>
{/* Sidebar Layout */}
{(globalConfig.memory_id.trim() && globalConfig.actor_id.trim()) && (
<div className="dashboard-layout">
{/* Sidebar */}
<div className="sidebar">
{/* Memory Type Selection */}
<div className="sidebar-section">
<div className="sidebar-section-header">
<MessageSquare size={16} />
<h3>Memory Type</h3>
</div>
<div className="memory-type-selector">
{tabs.map((tab) => (
<button
key={tab.id}
className={`memory-type-btn ${activeTab === tab.id ? 'active' : ''}`}
onClick={() => setActiveTab(tab.id)}
>
<div className="tab-icon">{tab.icon}</div>
<span className="tab-label">{tab.label}</span>
</button>
))}
</div>
</div>
{/* Query Parameters */}
<div className="sidebar-section">
<div className="sidebar-section-header">
<Search size={16} />
<h3>Query Parameters</h3>
</div>
{activeTab === 'shortterm' && (
<ShortTermMemoryForm
onMemoryFetch={handleShortTermMemoryFetch}
memoryConfig={globalConfig}
/>
)}
{activeTab === 'longterm' && (
<LongTermMemoryForm
onMemoryFetch={handleLongTermMemoryFetch}
memoryConfig={globalConfig}
availableNamespaces={availableNamespaces}
/>
)}
</div>
</div>
{/* Main Content Area */}
<div className="main-area">
{(globalConfig.memory_id.trim() && globalConfig.actor_id.trim()) ? (
<div className="results-container">
{renderResultsContent()}
</div>
) : (
<div className="welcome-screen">
<Database size={48} className="welcome-icon" />
<h2>Welcome to AgentCore Memory Dashboard</h2>
<p>Configure your Memory ID and Actor ID in the sidebar to get started.</p>
</div>
)}
</div>
</div>
)}
</>
</div>
</div>
);
}
export default App;
@@ -0,0 +1,464 @@
import { useState } from 'react';
import { AlertCircle, CheckCircle, Loader, Layers, User, MessageCircle, X } from 'lucide-react';
const LongTermMemoryForm = ({ onMemoryFetch, memoryConfig, availableNamespaces }) => {
const [formData, setFormData] = useState({
namespace: '',
max_results: 20
});
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
// Modal state for collecting missing values
const [showModal, setShowModal] = useState(false);
const [modalData, setModalData] = useState({
originalNamespace: '',
missingValues: {},
resolvedNamespace: ''
});
// Helper function to detect placeholders in namespace
const detectPlaceholders = (namespace) => {
const placeholderPattern = /\{(\w+)\}/g;
const placeholders = [];
let match;
while ((match = placeholderPattern.exec(namespace)) !== null) {
placeholders.push(match[1]);
}
return placeholders;
};
// Helper function to resolve namespace with available values
const resolveNamespace = (namespace, values = {}) => {
let resolved = namespace;
// Use provided values or fall back to memoryConfig
const allValues = {
actorId: values.actorId || memoryConfig.actor_id,
sessionId: values.sessionId || memoryConfig.session_id,
...values
};
// Replace all placeholders
Object.entries(allValues).forEach(([key, value]) => {
if (value && value.trim()) {
resolved = resolved.replace(new RegExp(`\\{${key}\\}`, 'g'), value);
}
});
return resolved;
};
// Helper function to get missing values
const getMissingValues = (namespace) => {
const placeholders = detectPlaceholders(namespace);
const missing = {};
placeholders.forEach(placeholder => {
const configKey = placeholder === 'actorId' ? 'actor_id' :
placeholder === 'sessionId' ? 'session_id' : placeholder;
if (!memoryConfig[configKey] || !memoryConfig[configKey].trim()) {
missing[placeholder] = '';
}
});
return missing;
};
const handleNamespaceSelection = (originalNamespace) => {
const missingValues = getMissingValues(originalNamespace);
if (Object.keys(missingValues).length > 0) {
// Show modal to collect missing values
setModalData({
originalNamespace,
missingValues,
resolvedNamespace: resolveNamespace(originalNamespace)
});
setShowModal(true);
} else {
// No missing values, proceed directly
const resolvedNamespace = resolveNamespace(originalNamespace);
setFormData(prev => ({ ...prev, namespace: resolvedNamespace }));
handleAutoFetch({ ...formData, namespace: resolvedNamespace });
}
};
const handleInputChange = (field, value) => {
setFormData(prev => ({
...prev,
[field]: value
}));
setError('');
setSuccess('');
// Auto-fetch when namespace is selected (for manual input)
if (field === 'namespace' && value.trim()) {
const updatedFormData = { ...formData, [field]: value };
handleAutoFetch(updatedFormData);
}
};
const handleModalValueChange = (key, value) => {
setModalData(prev => ({
...prev,
missingValues: {
...prev.missingValues,
[key]: value
}
}));
};
const handleModalSubmit = () => {
const resolvedNamespace = resolveNamespace(modalData.originalNamespace, modalData.missingValues);
setFormData(prev => ({ ...prev, namespace: resolvedNamespace }));
setShowModal(false);
handleAutoFetch({ ...formData, namespace: resolvedNamespace });
};
const handleModalCancel = () => {
setShowModal(false);
setModalData({
originalNamespace: '',
missingValues: {},
resolvedNamespace: ''
});
};
const handleAutoFetch = async (currentFormData) => {
if (!currentFormData.namespace.trim()) return;
setLoading(true);
setError('');
setSuccess('');
const requestPayload = {
...currentFormData,
memory_id: memoryConfig.memory_id,
content_type: 'all',
sort_by: 'timestamp',
sort_order: 'desc'
};
console.log('🚀 Auto-fetching long-term memory:', requestPayload);
try {
const response = await fetch('http://localhost:8000/api/agentcore/getLongTermMemory', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestPayload)
});
console.log('📡 Response status:', response.status);
const textResponse = await response.text();
let data;
try {
data = JSON.parse(textResponse);
} catch (parseError) {
console.error('Non-JSON response from backend:', textResponse);
throw new Error(`Backend returned non-JSON response (status ${response.status}). Check backend logs.`);
}
if (!response.ok) {
console.error('❌ Error response:', data);
const errorMessage = data.detail || `Request failed with status ${response.status}`;
throw new Error(errorMessage);
}
console.log('✅ Response data:', data);
if (data.memories && data.memories.length > 0) {
setSuccess(`Found ${data.memories.length} long-term memory entries!`);
onMemoryFetch(data.memories);
} else {
setSuccess('Query completed successfully.');
onMemoryFetch([]); // Pass empty array to show empty state in main area
}
} catch (err) {
console.error('❌ Long-term memory fetch error:', err);
// Parse specific error messages from backend
let errorMessage = 'Failed to fetch long-term memory';
if (err.response?.status === 404) {
errorMessage = err.response.data?.detail || 'Memory ID or namespace not found. Please verify they exist and you have access permissions.';
} else if (err.response?.status === 403) {
errorMessage = err.response.data?.detail || 'Access denied. Please check your AWS credentials and permissions.';
} else if (err.response?.data?.detail) {
errorMessage = err.response.data.detail;
} else if (err.message) {
errorMessage = err.message;
}
setError(errorMessage);
} finally {
setLoading(false);
}
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!formData.namespace.trim()) {
setError('Namespace is required.');
return;
}
setLoading(true);
setError('');
setSuccess('');
const requestPayload = {
...formData,
memory_id: memoryConfig.memory_id,
content_type: 'all',
sort_by: 'timestamp',
sort_order: 'desc'
};
console.log('🚀 Sending long-term memory request:', requestPayload);
try {
const response = await fetch('http://localhost:8000/api/agentcore/getLongTermMemory', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestPayload)
});
console.log('📡 Response status:', response.status);
const textResponse = await response.text();
let data;
try {
data = JSON.parse(textResponse);
} catch (parseError) {
console.error('Non-JSON response from backend:', textResponse);
throw new Error(`Backend returned non-JSON response (status ${response.status}). Check backend logs.`);
}
if (!response.ok) {
console.error('❌ Error response:', data);
const errorMessage = data.detail || `Request failed with status ${response.status}`;
throw new Error(errorMessage);
}
console.log('✅ Response data:', data);
if (data.memories && data.memories.length > 0) {
setSuccess(`Found ${data.memories.length} long-term memory entries!`);
onMemoryFetch(data.memories);
} else {
setSuccess('Query completed successfully.');
onMemoryFetch([]); // Pass empty array to show empty state in main area
}
} catch (err) {
console.error('❌ Long-term memory submit error:', err);
// Parse specific error messages from backend
let errorMessage = 'Failed to fetch long-term memory';
if (err.response?.status === 404) {
errorMessage = err.response.data?.detail || 'Memory ID or namespace not found. Please verify they exist and you have access permissions.';
} else if (err.response?.status === 403) {
errorMessage = err.response.data?.detail || 'Access denied. Please check your AWS credentials and permissions.';
} else if (err.response?.data?.detail) {
errorMessage = err.response.data.detail;
} else if (err.message) {
errorMessage = err.message;
}
setError(errorMessage);
} finally {
setLoading(false);
}
};
console.log('🔍 LongTermMemoryForm render:', {
availableNamespaces,
availableNamespacesLength: availableNamespaces.length,
memoryConfig
});
return (
<div className="long-term-memory-form">
<div className="memory-form">
<div className="form-grid">
<div className="form-group">
<label htmlFor="namespace">
<Layers size={16} />
Namespace (Required)
</label>
{availableNamespaces.length > 0 ? (
<div className="namespace-selector">
{availableNamespaces.map((ns, index) => {
// Check if this namespace has missing values
const missingValues = getMissingValues(ns.namespace);
const hasMissingValues = Object.keys(missingValues).length > 0;
// For display, show resolved namespace only if no values are missing
const displayNamespace = hasMissingValues ? ns.namespace : resolveNamespace(ns.namespace);
const isSelected = formData.namespace === displayNamespace ||
(!hasMissingValues && formData.namespace === resolveNamespace(ns.namespace));
return (
<div
key={index}
className={`namespace-option ${isSelected ? 'selected' : ''}`}
onClick={() => handleNamespaceSelection(ns.namespace)}
>
<div className="namespace-type">
<span className={`type-badge ${ns.type.toLowerCase().replace(/[^a-z0-9]/g, '-')}`}>
{(() => {
// Standard AgentCore strategy types
const standardTypes = {
'SEMANTIC': 'Facts',
'USER_PREFERENCE': 'Preferences',
'SUMMARIZATION': 'Summaries'
};
// If it's a standard type, use the friendly name
if (standardTypes[ns.type]) {
return standardTypes[ns.type];
}
// For custom types, format them nicely
return ns.type
.split('_')
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(' ');
})()}
</span>
</div>
<div className="namespace-path">
{displayNamespace.split('/').slice(0, -1).join('/') || displayNamespace}
{hasMissingValues && (
<span className="missing-values-indicator"> (requires values)</span>
)}
</div>
</div>
);
})}
</div>
) : (
<input
id="namespace"
type="text"
value={formData.namespace}
onChange={(e) => handleInputChange('namespace', e.target.value)}
placeholder="e.g., your-namespace/facts, company/user/preferences"
className="form-input"
required
/>
)}
<div className="form-help">
{availableNamespaces.length > 0
? `Select from ${availableNamespaces.length} available namespaces discovered from your memory strategies`
: 'Specify the exact namespace to query (e.g., your-namespace/facts, company/user/preferences)'
}
</div>
</div>
{loading && (
<div className="loading-indicator">
<Loader size={16} className="spinning" />
<span>Loading memory data...</span>
</div>
)}
</div>
{/* Status Messages */}
{error && (
<div className="status-message error">
<AlertCircle size={16} />
<span>{error}</span>
</div>
)}
{success && (
<div className="status-message success">
<CheckCircle size={16} />
<span>{success}</span>
</div>
)}
</div>
{/* Modal for collecting missing values */}
{showModal && (
<div className="modal-overlay">
<div className="modal-content">
<div className="modal-header">
<h3>Complete Namespace Configuration</h3>
<button className="modal-close" onClick={handleModalCancel}>
<X size={20} />
</button>
</div>
<div className="modal-body">
<p>This namespace requires additional values:</p>
<div className="namespace-preview">
<strong>Namespace:</strong> {modalData.originalNamespace}
</div>
<div className="missing-values-form">
{Object.entries(modalData.missingValues).map(([key, value]) => (
<div key={key} className="form-group">
<label htmlFor={`modal-${key}`}>
{key === 'actorId' ? <User size={16} /> : <MessageCircle size={16} />}
{key === 'actorId' ? 'Actor ID' :
key === 'sessionId' ? 'Session ID' : key}
</label>
<input
id={`modal-${key}`}
type="text"
value={value}
onChange={(e) => handleModalValueChange(key, e.target.value)}
placeholder={key === 'actorId' ? 'e.g., DEFAULT, user123' :
key === 'sessionId' ? 'e.g., session-abc123' : `Enter ${key}`}
className="form-input"
/>
</div>
))}
</div>
<div className="resolved-preview">
<strong>Resolved namespace:</strong>
<code>{resolveNamespace(modalData.originalNamespace, modalData.missingValues)}</code>
</div>
</div>
<div className="modal-footer">
<button className="modal-btn cancel" onClick={handleModalCancel}>
Cancel
</button>
<button
className="modal-btn submit"
onClick={handleModalSubmit}
disabled={Object.values(modalData.missingValues).some(v => !v.trim())}
>
Use Namespace
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default LongTermMemoryForm;
@@ -0,0 +1,237 @@
import React, { useState, useEffect } from 'react';
import { User, MessageCircle, Search, AlertCircle, CheckCircle, Loader, HelpCircle, Clock, ChevronDown, Database } from 'lucide-react';
const ShortTermMemoryForm = ({ onMemoryFetch, memoryConfig }) => {
const [formData, setFormData] = useState({
session_id: '',
max_results: 20
});
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
const [searchHistory, setSearchHistory] = useState([]);
const [showActorHistory, setShowActorHistory] = useState(false);
const [showSessionHistory, setShowSessionHistory] = useState(false);
// Load search history from localStorage on component mount
useEffect(() => {
const savedHistory = localStorage.getItem('agentcore-search-history');
if (savedHistory) {
try {
setSearchHistory(JSON.parse(savedHistory));
} catch (e) {
console.error('Failed to parse search history:', e);
}
}
}, []);
// No need for useEffect since we get memoryConfig as prop
// Save search to history
const saveToHistory = (actorId, sessionId) => {
const newEntry = {
actor_id: actorId,
session_id: sessionId,
timestamp: new Date().toISOString(),
id: Date.now()
};
const updatedHistory = [
newEntry,
...searchHistory.filter(item =>
!(item.actor_id === actorId && item.session_id === sessionId)
)
].slice(0, 10); // Keep only last 10 searches
setSearchHistory(updatedHistory);
localStorage.setItem('agentcore-search-history', JSON.stringify(updatedHistory));
};
const handleInputChange = (field, value) => {
setFormData(prev => ({
...prev,
[field]: value
}));
setError('');
setSuccess('');
};
const handleHistorySelect = (historyItem) => {
setFormData(prev => ({
...prev,
actor_id: historyItem.actor_id,
session_id: historyItem.session_id
}));
setShowActorHistory(false);
setShowSessionHistory(false);
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!memoryConfig.actor_id || !memoryConfig.actor_id.trim()) {
setError('Actor ID is required in configuration');
return;
}
if (!formData.session_id.trim()) {
setError('Session ID is required');
return;
}
setLoading(true);
setError('');
setSuccess('');
try {
const response = await fetch('http://localhost:8000/api/agentcore/getShortTermMemory', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...formData,
memory_id: memoryConfig.memory_id,
actor_id: memoryConfig.actor_id
})
});
const textResponse = await response.text();
let data;
try {
data = JSON.parse(textResponse);
} catch (parseError) {
console.error('Non-JSON response from backend:', textResponse);
throw new Error(`Backend returned non-JSON response (status ${response.status}). Check backend logs.`);
}
if (!response.ok) {
const errorMessage = data.detail || `Request failed with status ${response.status}`;
throw new Error(errorMessage);
}
// data is already parsed above
if (data.memories && data.memories.length > 0) {
setSuccess(`Found ${data.memories.length} short-term memory entries!`);
onMemoryFetch(data.memories);
// Save successful search to history
saveToHistory(formData.actor_id, formData.session_id);
} else {
setSuccess('Query completed successfully.');
onMemoryFetch([]); // Pass empty array to show empty state in main area
}
} catch (err) {
console.error('❌ Short-term memory fetch error:', err);
// Parse specific error messages from backend
let errorMessage = 'Failed to fetch short-term memory';
if (err.response?.status === 404) {
errorMessage = err.response.data?.detail || 'Memory ID not found. Please verify the Memory ID exists and you have access permissions.';
} else if (err.response?.status === 403) {
errorMessage = err.response.data?.detail || 'Access denied. Please check your AWS credentials and permissions.';
} else if (err.response?.data?.detail) {
errorMessage = err.response.data.detail;
} else if (err.message) {
errorMessage = err.message;
}
setError(errorMessage);
} finally {
setLoading(false);
}
};
const clearHistory = () => {
setSearchHistory([]);
localStorage.removeItem('agentcore-search-history');
};
const getUniqueActorIds = () => {
const actors = [...new Set(searchHistory.map(item => item.actor_id))];
return actors.slice(0, 5);
};
const getUniqueSessionIds = () => {
const sessions = [...new Set(searchHistory.map(item => item.session_id))];
return sessions.slice(0, 5);
};
return (
<div className="short-term-memory-form">
<form onSubmit={handleSubmit} className="memory-form">
<div className="form-grid">
<div className="form-group">
<label htmlFor="session_id">
<MessageCircle size={16} />
Session ID (Required)
</label>
<input
id="session_id"
type="text"
value={formData.session_id}
onChange={(e) => handleInputChange('session_id', e.target.value)}
placeholder="e.g., session-abc123, conv-456def"
className="form-input"
required
/>
<div className="form-help">
Specify the exact session identifier to query (e.g., session-abc123, conv-456def)
</div>
</div>
<div className="form-group">
<label>&nbsp;</label>
<button
type="submit"
disabled={loading || !formData.session_id.trim()}
className="submit-button-inline"
>
{loading ? (
<>
<Loader size={16} className="spinning" />
Fetching...
</>
) : (
<>
<Search size={16} />
Fetch Short-Term Memory
</>
)}
</button>
<div className="form-help">&nbsp;</div>
</div>
</div>
{/* Status Messages */}
{error && (
<div className="status-message error">
<AlertCircle size={16} />
<span>{error}</span>
</div>
)}
{success && (
<div className="status-message success">
<CheckCircle size={16} />
<span>{success}</span>
</div>
)}
</form>
</div>
);
};
export default ShortTermMemoryForm;
@@ -0,0 +1,11 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
@@ -0,0 +1,48 @@
#!/bin/bash
# Start AgentCore Memory Dashboard Backend
echo "🚀 Starting AgentCore Memory Dashboard Backend..."
# Check if we're in the right directory
if [ ! -f "backend/app.py" ]; then
echo "❌ Error: backend/app.py not found. Please run this script from the agentcore-memory-dashboard directory."
exit 1
fi
# Create virtual environment if it doesn't exist
if [ ! -d "backend/venv" ]; then
echo "📦 Creating Python virtual environment..."
cd backend
python3 -m venv venv
cd ..
fi
# Activate virtual environment
echo "🔧 Activating virtual environment..."
source backend/venv/bin/activate
# Install dependencies
echo "📦 Installing Python dependencies..."
cd backend
pip install -r requirements.txt
# Check if bedrock-agentcore is available
echo "🔍 Checking AgentCore Memory SDK..."
python -c "
try:
from bedrock_agentcore.memory import MemoryClient
print('✅ bedrock-agentcore SDK is available')
except ImportError:
print('⚠️ bedrock-agentcore SDK not found')
print(' The backend will use mock data for development')
print(' To install: pip install bedrock-agentcore')
"
# Start the backend server
echo "🚀 Starting FastAPI backend server..."
echo "📍 Backend will be available at: http://localhost:8000"
echo "📖 API documentation at: http://localhost:8000/docs"
echo ""
echo "Press Ctrl+C to stop the server"
uvicorn app:app --host 0.0.0.0 --port 8000 --reload