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:
+1
-1
@@ -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
|
||||
|
||||
---
|
||||
+20
@@ -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
|
||||
+1563
File diff suppressed because it is too large
Load Diff
+7
@@ -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
|
||||
+22368
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"
|
||||
}
|
||||
}
|
||||
+18
@@ -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>
|
||||
+82
@@ -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;
|
||||
+464
@@ -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;
|
||||
+237
@@ -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> </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"> </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;
|
||||
+1117
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
);
|
||||
+48
@@ -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
|
||||
Reference in New Issue
Block a user