Add local streamlit app for agentcore gateway testing (#339)
* Add local streamlit app for agentcore gateway testing * Update contributors * Remove example for simplicity * Add example agent back in * Add architecture diagram
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
# Python-generated files
|
||||
__pycache__/
|
||||
*.py[oc]
|
||||
build/
|
||||
dist/
|
||||
wheels/
|
||||
*.egg-info
|
||||
|
||||
# Virtual environments
|
||||
.venv
|
||||
|
||||
# Other
|
||||
.DS_Store
|
||||
.bedrock_agentcore.yaml
|
||||
.dockerignore
|
||||
Dockerfile
|
||||
@@ -0,0 +1 @@
|
||||
3.11
|
||||
@@ -0,0 +1,33 @@
|
||||
[server]
|
||||
enableStaticServing = true
|
||||
|
||||
[theme]
|
||||
primaryColor = "#5C7FFF"
|
||||
textColor = "#232F3E"
|
||||
font = "amazon-ember, sans-serif"
|
||||
headingFont = "amazon-ember-heading, sans-serif"
|
||||
codeFont = "amazon-ember-mono, monospace"
|
||||
showSidebarBorder = true
|
||||
|
||||
[[theme.fontFaces]]
|
||||
family = "amazon-ember"
|
||||
url = "app/static/Amazon-Ember-Medium.ttf"
|
||||
style = "normal"
|
||||
|
||||
[[theme.fontFaces]]
|
||||
family = "amazon-ember"
|
||||
url = "app/static/Amazon-Ember-MediumItalic.ttf"
|
||||
style = "italic"
|
||||
|
||||
[[theme.fontFaces]]
|
||||
family = "amazon-ember-mono"
|
||||
url = "app/static/AmazonEmberMono_Rg.ttf"
|
||||
style = "normal"
|
||||
|
||||
[[theme.fontFaces]]
|
||||
family = "amazon-ember-heading"
|
||||
url = "app/static/AmazonEmber_He.ttf"
|
||||
style = "normal"
|
||||
|
||||
[theme.sidebar]
|
||||
primaryColor = "#962EFF"
|
||||
@@ -0,0 +1,164 @@
|
||||
# Bedrock AgentCore Chat Interface
|
||||
|
||||
A Streamlit web application for interacting with AI agents deployed on Amazon Bedrock AgentCore Runtime. This application provides an intuitive chat interface to communicate with your deployed agents in real-time.
|
||||
|
||||
## About Amazon Bedrock AgentCore
|
||||
|
||||
Amazon Bedrock AgentCore is a comprehensive service that enables you to deploy and operate highly effective AI agents securely at scale using any framework and model. AgentCore Runtime is a secure, serverless runtime purpose-built for deploying and scaling dynamic AI agents and tools using popular open-source frameworks like LangGraph, CrewAI, and Strands Agents.
|
||||
|
||||
## Features
|
||||
|
||||
- **Real-time Chat Interface**: Interactive chat with deployed AgentCore agents
|
||||
- **Agent Discovery**: Automatically discover and select from available agents in your AWS account
|
||||
- **Version Management**: Choose specific versions of your deployed agents
|
||||
- **Multi-Region Support**: Connect to agents deployed in different AWS regions
|
||||
- **Streaming Responses**: Real-time streaming of agent responses
|
||||
- **Response Formatting**: Auto-format responses with options to view raw output
|
||||
- **Session Management**: Maintain conversation context with unique session IDs
|
||||
- **Tool Visibility**: Optional display of tools used by agents during execution
|
||||
- **Thinking Process**: Optional display of agent reasoning (when available)
|
||||
|
||||
## Architecture
|
||||
|
||||

|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Python 3.11 or higher
|
||||
- [uv package manager](https://docs.astral.sh/uv/getting-started/installation/)
|
||||
- AWS CLI configured with appropriate credentials
|
||||
- Access to Amazon Bedrock AgentCore service
|
||||
- Deployed agents on Bedrock AgentCore Runtime
|
||||
|
||||
### Required AWS Permissions
|
||||
|
||||
Your AWS credentials need the following permissions:
|
||||
|
||||
- `bedrock-agentcore-control:ListAgentRuntimes`
|
||||
- `bedrock-agentcore-control:ListAgentRuntimeVersions`
|
||||
- `bedrock-agentcore:InvokeAgentRuntime`
|
||||
|
||||
## Installation
|
||||
|
||||
1. **Clone the repository**:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/awslabs/amazon-bedrock-agentcore-samples.git
|
||||
cd amazon-bedrock-agentcore-samples/03-integrations/ux-examples/streamlit-chat
|
||||
```
|
||||
|
||||
2. **Install dependencies using uv**:
|
||||
|
||||
```bash
|
||||
uv sync
|
||||
```
|
||||
|
||||
## (Optional) Deploy the Example Agent
|
||||
|
||||
1. **Install dev dependencies using uv** (recommended):
|
||||
|
||||
```bash
|
||||
uv sync --dev
|
||||
```
|
||||
|
||||
2. **Configure the agent**:
|
||||
|
||||
```bash
|
||||
cd example
|
||||
uv run agentcore configure -e agent.py
|
||||
```
|
||||
|
||||
3. **Deploy to AgentCore Runtime**:
|
||||
|
||||
```bash
|
||||
uv run agentcore launch
|
||||
cd ..
|
||||
```
|
||||
|
||||
## Running the Application
|
||||
|
||||
### Using uv (recommended)
|
||||
|
||||
```bash
|
||||
uv run streamlit run app.py
|
||||
```
|
||||
|
||||
The application will start and be available at `http://localhost:8501`.
|
||||
|
||||
## Usage
|
||||
|
||||
1. **Configure AWS Region**: Select your preferred AWS region from the sidebar
|
||||
2. **Select Agent**: Choose from automatically discovered agents in your account
|
||||
3. **Choose Version**: Select the specific version of your agent to use
|
||||
4. **Start Chatting**: Type your message in the chat input and press Enter
|
||||
|
||||
### Configuration Options
|
||||
|
||||
- **Auto-format responses**: Clean and format agent responses for better readability
|
||||
- **Show raw response**: Display the unprocessed response from the agent
|
||||
- **Show tools**: Display when agents use tools during execution
|
||||
- **Show thinking**: Display agent reasoning process (when available)
|
||||
- **Session Management**: Generate new session IDs to start fresh conversations
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
streamlit-chat/
|
||||
├── app.py # Main Streamlit application
|
||||
├── example.py # Example agent
|
||||
├── static/ # UI assets (fonts, icons, logos)
|
||||
├── pyproject.toml # Project dependencies
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## Configuration Files
|
||||
|
||||
- **`pyproject.toml`**: Defines project dependencies and metadata
|
||||
- **`.streamlit/config.toml`**: Streamlit-specific configuration
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **No agents found**: Ensure you have deployed agents in the selected region and have proper AWS permissions
|
||||
2. **Connection errors**: Verify your AWS credentials and network connectivity
|
||||
3. **Permission denied**: Check that your IAM user/role has the required Bedrock AgentCore permissions
|
||||
|
||||
### Debug Mode
|
||||
|
||||
Enable debug logging by setting the Streamlit logger level in the application or check the browser console for additional error information.
|
||||
|
||||
## Development
|
||||
|
||||
### Adding New Features
|
||||
|
||||
The application is built with modularity in mind. Key areas for extension:
|
||||
|
||||
- **Response Processing**: Modify `clean_response_text()` for custom formatting
|
||||
- **Agent Selection**: Extend `fetch_agent_runtimes()` for custom filtering
|
||||
- **UI Components**: Add new Streamlit components in the sidebar or main area
|
||||
|
||||
### Dependencies
|
||||
|
||||
- **boto3**: AWS SDK for Python
|
||||
- **streamlit**: Web application framework
|
||||
- **uv**: Fast Python package installer and resolver
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes
|
||||
4. Test thoroughly
|
||||
5. Submit a pull request
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the terms specified in the repository license file.
|
||||
|
||||
## Resources
|
||||
|
||||
- [Amazon Bedrock AgentCore Documentation](https://docs.aws.amazon.com/bedrock-agentcore/)
|
||||
- [Bedrock AgentCore Samples](https://github.com/awslabs/amazon-bedrock-agentcore-samples/)
|
||||
- [Streamlit Documentation](https://docs.streamlit.io/)
|
||||
- [Strands Agents Framework](https://github.com/awslabs/strands-agents)
|
||||
@@ -0,0 +1,627 @@
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
import uuid
|
||||
from typing import Dict, Iterator, List
|
||||
|
||||
import boto3
|
||||
import streamlit as st
|
||||
from streamlit.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
logger.setLevel("INFO")
|
||||
|
||||
# Page config
|
||||
st.set_page_config(
|
||||
page_title="Bedrock AgentCore Chat",
|
||||
page_icon="static/gen-ai-dark.svg",
|
||||
layout="wide",
|
||||
initial_sidebar_state="expanded",
|
||||
)
|
||||
|
||||
# Remove Streamlit deployment components
|
||||
st.markdown(
|
||||
"""
|
||||
<style>
|
||||
.stAppDeployButton {display:none;}
|
||||
#MainMenu {visibility: hidden;}
|
||||
</style>
|
||||
""",
|
||||
unsafe_allow_html=True,
|
||||
)
|
||||
|
||||
HUMAN_AVATAR = "static/user-profile.svg"
|
||||
AI_AVATAR = "static/gen-ai-dark.svg"
|
||||
|
||||
|
||||
def fetch_agent_runtimes(region: str = "us-east-1") -> List[Dict]:
|
||||
"""Fetch available agent runtimes from bedrock-agentcore-control"""
|
||||
try:
|
||||
client = boto3.client("bedrock-agentcore-control", region_name=region)
|
||||
response = client.list_agent_runtimes(maxResults=100)
|
||||
|
||||
# Filter only READY agents and sort by name
|
||||
ready_agents = [
|
||||
agent
|
||||
for agent in response.get("agentRuntimes", [])
|
||||
if agent.get("status") == "READY"
|
||||
]
|
||||
|
||||
# Sort by most recent update time (newest first)
|
||||
ready_agents.sort(key=lambda x: x.get("lastUpdatedAt", ""), reverse=True)
|
||||
|
||||
return ready_agents
|
||||
except Exception as e:
|
||||
st.error(f"Error fetching agent runtimes: {e}")
|
||||
return []
|
||||
|
||||
|
||||
def fetch_agent_runtime_versions(
|
||||
agent_runtime_id: str, region: str = "us-east-1"
|
||||
) -> List[Dict]:
|
||||
"""Fetch versions for a specific agent runtime"""
|
||||
try:
|
||||
client = boto3.client("bedrock-agentcore-control", region_name=region)
|
||||
response = client.list_agent_runtime_versions(agentRuntimeId=agent_runtime_id)
|
||||
|
||||
# Filter only READY versions
|
||||
ready_versions = [
|
||||
version
|
||||
for version in response.get("agentRuntimes", [])
|
||||
if version.get("status") == "READY"
|
||||
]
|
||||
|
||||
# Sort by most recent update time (newest first)
|
||||
ready_versions.sort(key=lambda x: x.get("lastUpdatedAt", ""), reverse=True)
|
||||
|
||||
return ready_versions
|
||||
except Exception as e:
|
||||
st.error(f"Error fetching agent runtime versions: {e}")
|
||||
return []
|
||||
|
||||
|
||||
def clean_response_text(text: str, show_thinking: bool = True) -> str:
|
||||
"""Clean and format response text for better presentation"""
|
||||
if not text:
|
||||
return text
|
||||
|
||||
# Handle the consecutive quoted chunks pattern
|
||||
# Pattern: "word1" "word2" "word3" -> word1 word2 word3
|
||||
text = re.sub(r'"\s*"', "", text)
|
||||
text = re.sub(r'^"', "", text)
|
||||
text = re.sub(r'"$', "", text)
|
||||
|
||||
# Replace literal \n with actual newlines
|
||||
text = text.replace("\\n", "\n")
|
||||
|
||||
# Replace literal \t with actual tabs
|
||||
text = text.replace("\\t", "\t")
|
||||
|
||||
# Clean up multiple spaces
|
||||
text = re.sub(r" {3,}", " ", text)
|
||||
|
||||
# Fix newlines that got converted to spaces
|
||||
text = text.replace(" \n ", "\n")
|
||||
text = text.replace("\n ", "\n")
|
||||
text = text.replace(" \n", "\n")
|
||||
|
||||
# Handle numbered lists
|
||||
text = re.sub(r"\n(\d+)\.\s+", r"\n\1. ", text)
|
||||
text = re.sub(r"^(\d+)\.\s+", r"\1. ", text)
|
||||
|
||||
# Handle bullet points
|
||||
text = re.sub(r"\n-\s+", r"\n- ", text)
|
||||
text = re.sub(r"^-\s+", r"- ", text)
|
||||
|
||||
# Handle section headers
|
||||
text = re.sub(r"\n([A-Za-z][A-Za-z\s]{2,30}):\s*\n", r"\n**\1:**\n\n", text)
|
||||
|
||||
# Clean up multiple newlines
|
||||
text = re.sub(r"\n{3,}", "\n\n", text)
|
||||
|
||||
# Clean up thinking
|
||||
|
||||
if not show_thinking:
|
||||
text = re.sub(r"<thinking>.*?</thinking>", "", text)
|
||||
|
||||
return text.strip()
|
||||
|
||||
|
||||
def extract_text_from_response(data) -> str:
|
||||
"""Extract text content from response data in various formats"""
|
||||
if isinstance(data, dict):
|
||||
# Handle format: {'role': 'assistant', 'content': [{'text': 'Hello!'}]}
|
||||
if "role" in data and "content" in data:
|
||||
content = data["content"]
|
||||
if isinstance(content, list) and len(content) > 0:
|
||||
if isinstance(content[0], dict) and "text" in content[0]:
|
||||
return str(content[0]["text"])
|
||||
else:
|
||||
return str(content[0])
|
||||
elif isinstance(content, str):
|
||||
return content
|
||||
else:
|
||||
return str(content)
|
||||
|
||||
# Handle other common formats
|
||||
if "text" in data:
|
||||
return str(data["text"])
|
||||
elif "content" in data:
|
||||
content = data["content"]
|
||||
if isinstance(content, str):
|
||||
return content
|
||||
else:
|
||||
return str(content)
|
||||
elif "message" in data:
|
||||
return str(data["message"])
|
||||
elif "response" in data:
|
||||
return str(data["response"])
|
||||
elif "result" in data:
|
||||
return str(data["result"])
|
||||
|
||||
return str(data)
|
||||
|
||||
|
||||
def parse_streaming_chunk(chunk: str) -> str:
|
||||
"""Parse individual streaming chunk and extract meaningful content"""
|
||||
logger.debug(f"parse_streaming_chunk: received chunk: {chunk}")
|
||||
logger.debug(f"parse_streaming_chunk: chunk type: {type(chunk)}")
|
||||
|
||||
try:
|
||||
# Try to parse as JSON first
|
||||
if chunk.strip().startswith("{"):
|
||||
logger.debug("parse_streaming_chunk: Attempting JSON parse")
|
||||
data = json.loads(chunk)
|
||||
logger.debug(f"parse_streaming_chunk: Successfully parsed JSON: {data}")
|
||||
|
||||
# Handle the specific format: {'role': 'assistant', 'content': [{'text': '...'}]}
|
||||
if isinstance(data, dict) and "role" in data and "content" in data:
|
||||
content = data["content"]
|
||||
if isinstance(content, list) and len(content) > 0:
|
||||
first_item = content[0]
|
||||
if isinstance(first_item, dict) and "text" in first_item:
|
||||
extracted_text = first_item["text"]
|
||||
logger.debug(
|
||||
f"parse_streaming_chunk: Extracted text: {extracted_text}"
|
||||
)
|
||||
return extracted_text
|
||||
else:
|
||||
return str(first_item)
|
||||
else:
|
||||
return str(content)
|
||||
else:
|
||||
# Use the general extraction function for other formats
|
||||
return extract_text_from_response(data)
|
||||
|
||||
# If not JSON, return the chunk as-is
|
||||
logger.debug("parse_streaming_chunk: Not JSON, returning as-is")
|
||||
return chunk
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"parse_streaming_chunk: JSON decode error: {e}")
|
||||
|
||||
# Try to handle Python dict string representation (with single quotes)
|
||||
if chunk.strip().startswith("{") and "'" in chunk:
|
||||
logger.debug(
|
||||
"parse_streaming_chunk: Attempting to handle Python dict string"
|
||||
)
|
||||
try:
|
||||
# Try to convert single quotes to double quotes for JSON parsing
|
||||
# This is a simple approach - might need refinement for complex cases
|
||||
json_chunk = chunk.replace("'", '"')
|
||||
data = json.loads(json_chunk)
|
||||
logger.debug(
|
||||
f"parse_streaming_chunk: Successfully converted and parsed: {data}"
|
||||
)
|
||||
|
||||
# Handle the specific format
|
||||
if isinstance(data, dict) and "role" in data and "content" in data:
|
||||
content = data["content"]
|
||||
if isinstance(content, list) and len(content) > 0:
|
||||
first_item = content[0]
|
||||
if isinstance(first_item, dict) and "text" in first_item:
|
||||
extracted_text = first_item["text"]
|
||||
logger.debug(
|
||||
f"parse_streaming_chunk: Extracted text from converted dict: {extracted_text}"
|
||||
)
|
||||
return extracted_text
|
||||
else:
|
||||
return str(first_item)
|
||||
else:
|
||||
return str(content)
|
||||
else:
|
||||
return extract_text_from_response(data)
|
||||
except json.JSONDecodeError:
|
||||
logger.debug(
|
||||
"parse_streaming_chunk: Failed to convert Python dict string"
|
||||
)
|
||||
pass
|
||||
|
||||
# If all parsing fails, return the chunk as-is
|
||||
logger.debug("parse_streaming_chunk: All parsing failed, returning chunk as-is")
|
||||
return chunk
|
||||
|
||||
|
||||
def invoke_agent_streaming(
|
||||
prompt: str,
|
||||
agent_arn: str,
|
||||
runtime_session_id: str,
|
||||
region: str = "us-east-1",
|
||||
show_tool: bool = True,
|
||||
) -> Iterator[str]:
|
||||
"""Invoke agent and yield streaming response chunks"""
|
||||
try:
|
||||
agentcore_client = boto3.client("bedrock-agentcore", region_name=region)
|
||||
|
||||
boto3_response = agentcore_client.invoke_agent_runtime(
|
||||
agentRuntimeArn=agent_arn,
|
||||
qualifier="DEFAULT",
|
||||
runtimeSessionId=runtime_session_id,
|
||||
payload=json.dumps({"prompt": prompt}),
|
||||
)
|
||||
|
||||
logger.debug(f"contentType: {boto3_response.get('contentType', 'NOT_FOUND')}")
|
||||
|
||||
if "text/event-stream" in boto3_response.get("contentType", ""):
|
||||
logger.debug("Using streaming response path")
|
||||
# Handle streaming response
|
||||
for line in boto3_response["response"].iter_lines(chunk_size=1):
|
||||
if line:
|
||||
line = line.decode("utf-8")
|
||||
logger.debug(f"Raw line: {line}")
|
||||
if line.startswith("data: "):
|
||||
line = line[6:]
|
||||
logger.debug(f"Line after removing 'data: ': {line}")
|
||||
# Parse and clean each chunk
|
||||
parsed_chunk = parse_streaming_chunk(line)
|
||||
if parsed_chunk.strip(): # Only yield non-empty chunks
|
||||
if "🔧 Using tool:" in parsed_chunk and not show_tool:
|
||||
yield ""
|
||||
else:
|
||||
yield parsed_chunk
|
||||
else:
|
||||
logger.debug(
|
||||
f"Line doesn't start with 'data: ', skipping: {line}"
|
||||
)
|
||||
else:
|
||||
logger.debug("Using non-streaming response path")
|
||||
# Handle non-streaming JSON response
|
||||
try:
|
||||
response_obj = boto3_response.get("response")
|
||||
logger.debug(f"response_obj type: {type(response_obj)}")
|
||||
|
||||
if hasattr(response_obj, "read"):
|
||||
# Read the response content
|
||||
content = response_obj.read()
|
||||
if isinstance(content, bytes):
|
||||
content = content.decode("utf-8")
|
||||
|
||||
logger.debug(f"Raw content: {content}")
|
||||
|
||||
try:
|
||||
# Try to parse as JSON and extract text
|
||||
response_data = json.loads(content)
|
||||
logger.debug(f"Parsed JSON: {response_data}")
|
||||
|
||||
# Handle the specific format we're seeing
|
||||
if isinstance(response_data, dict):
|
||||
# Check for 'result' wrapper first
|
||||
if "result" in response_data:
|
||||
actual_data = response_data["result"]
|
||||
else:
|
||||
actual_data = response_data
|
||||
|
||||
# Extract text from the nested structure
|
||||
if "role" in actual_data and "content" in actual_data:
|
||||
content_list = actual_data["content"]
|
||||
if (
|
||||
isinstance(content_list, list)
|
||||
and len(content_list) > 0
|
||||
):
|
||||
first_item = content_list[0]
|
||||
if (
|
||||
isinstance(first_item, dict)
|
||||
and "text" in first_item
|
||||
):
|
||||
extracted_text = first_item["text"]
|
||||
logger.debug(
|
||||
f"Extracted text: {extracted_text}"
|
||||
)
|
||||
yield extracted_text
|
||||
else:
|
||||
yield str(first_item)
|
||||
else:
|
||||
yield str(content_list)
|
||||
else:
|
||||
# Use general extraction
|
||||
text = extract_text_from_response(actual_data)
|
||||
yield text
|
||||
else:
|
||||
yield str(response_data)
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"JSON decode error: {e}")
|
||||
# If not JSON, yield raw content
|
||||
yield content
|
||||
elif isinstance(response_obj, dict):
|
||||
# Direct dict response
|
||||
text = extract_text_from_response(response_obj)
|
||||
yield text
|
||||
else:
|
||||
logger.debug(f"Unexpected response_obj type: {type(response_obj)}")
|
||||
yield "No response content"
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Exception in non-streaming: {e}")
|
||||
yield f"Error reading response: {e}"
|
||||
|
||||
except Exception as e:
|
||||
yield f"Error invoking agent: {e}"
|
||||
|
||||
|
||||
def main():
|
||||
st.logo("static/agentcore-service-icon.png", size="large")
|
||||
st.title("Amazon Bedrock AgentCore Chat")
|
||||
|
||||
# Sidebar for settings
|
||||
with st.sidebar:
|
||||
st.header("Settings")
|
||||
|
||||
# Region selection (moved up since it affects agent fetching)
|
||||
region = st.selectbox(
|
||||
"AWS Region",
|
||||
["us-east-1", "us-west-2", "eu-west-1", "ap-southeast-1"],
|
||||
index=0,
|
||||
)
|
||||
|
||||
# Agent selection
|
||||
st.subheader("Agent Selection")
|
||||
|
||||
# Fetch available agents
|
||||
with st.spinner("Loading available agents..."):
|
||||
available_agents = fetch_agent_runtimes(region)
|
||||
|
||||
if available_agents:
|
||||
# Get unique agent names and their runtime IDs
|
||||
unique_agents = {}
|
||||
for agent in available_agents:
|
||||
name = agent.get("agentRuntimeName", "Unknown")
|
||||
runtime_id = agent.get("agentRuntimeId", "")
|
||||
if name not in unique_agents:
|
||||
unique_agents[name] = runtime_id
|
||||
|
||||
# Create agent name options
|
||||
agent_names = list(unique_agents.keys())
|
||||
|
||||
# Agent name selection dropdown
|
||||
col1, col2 = st.columns([2, 1])
|
||||
|
||||
with col1:
|
||||
selected_agent_name = st.selectbox(
|
||||
"Agent Name",
|
||||
options=agent_names,
|
||||
help="Choose an agent to chat with",
|
||||
)
|
||||
|
||||
# Get versions for the selected agent using the specific API
|
||||
if selected_agent_name and selected_agent_name in unique_agents:
|
||||
agent_runtime_id = unique_agents[selected_agent_name]
|
||||
|
||||
with st.spinner("Loading versions..."):
|
||||
agent_versions = fetch_agent_runtime_versions(
|
||||
agent_runtime_id, region
|
||||
)
|
||||
|
||||
if agent_versions:
|
||||
version_options = []
|
||||
version_arn_map = {}
|
||||
|
||||
for version in agent_versions:
|
||||
version_num = version.get("agentRuntimeVersion", "Unknown")
|
||||
arn = version.get("agentRuntimeArn", "")
|
||||
updated = version.get("lastUpdatedAt", "")
|
||||
description = version.get("description", "")
|
||||
|
||||
# Format version display with update time
|
||||
version_display = f"v{version_num}"
|
||||
if updated:
|
||||
try:
|
||||
if hasattr(updated, "strftime"):
|
||||
updated_str = updated.strftime("%m/%d %H:%M")
|
||||
version_display += f" ({updated_str})"
|
||||
except:
|
||||
pass
|
||||
|
||||
version_options.append(version_display)
|
||||
version_arn_map[version_display] = {
|
||||
"arn": arn,
|
||||
"description": description,
|
||||
}
|
||||
|
||||
with col2:
|
||||
selected_version = st.selectbox(
|
||||
"Version",
|
||||
options=version_options,
|
||||
help="Choose the version to use",
|
||||
)
|
||||
|
||||
# Get the ARN for the selected agent and version
|
||||
version_info = version_arn_map.get(selected_version, {})
|
||||
agent_arn = version_info.get("arn", "")
|
||||
description = version_info.get("description", "")
|
||||
|
||||
# Show selected agent info
|
||||
if agent_arn:
|
||||
st.info(f"Selected: {selected_agent_name} {selected_version}")
|
||||
if description:
|
||||
st.caption(f"Description: {description}")
|
||||
with st.expander("View ARN"):
|
||||
st.code(agent_arn)
|
||||
else:
|
||||
st.warning(f"No versions found for {selected_agent_name}")
|
||||
agent_arn = ""
|
||||
else:
|
||||
agent_arn = ""
|
||||
else:
|
||||
st.error("No agent runtimes found or error loading agents")
|
||||
agent_arn = ""
|
||||
|
||||
# Fallback manual input
|
||||
st.subheader("Manual ARN Input")
|
||||
agent_arn = st.text_input(
|
||||
"Agent ARN", value="", help="Enter your Bedrock AgentCore ARN manually"
|
||||
)
|
||||
if st.button("Refresh", key="refresh_agents", help="Refresh agent list"):
|
||||
st.rerun()
|
||||
|
||||
# Runtime Session ID
|
||||
st.subheader("Session Configuration")
|
||||
|
||||
# Initialize session ID in session state if not exists
|
||||
if "runtime_session_id" not in st.session_state:
|
||||
st.session_state.runtime_session_id = str(uuid.uuid4())
|
||||
|
||||
# Session ID input with generate button
|
||||
runtime_session_id = st.text_input(
|
||||
"Runtime Session ID",
|
||||
value=st.session_state.runtime_session_id,
|
||||
help="Unique identifier for this runtime session",
|
||||
)
|
||||
|
||||
if st.button("Refresh", help="Generate new session ID and clear chat"):
|
||||
st.session_state.runtime_session_id = str(uuid.uuid4())
|
||||
st.session_state.messages = [] # Clear chat messages when resetting session
|
||||
st.rerun()
|
||||
|
||||
# Update session state if user manually changed the ID
|
||||
if runtime_session_id != st.session_state.runtime_session_id:
|
||||
st.session_state.runtime_session_id = runtime_session_id
|
||||
|
||||
# Response formatting options
|
||||
st.subheader("Display Options")
|
||||
auto_format = st.checkbox(
|
||||
"Auto-format responses",
|
||||
value=True,
|
||||
help="Automatically clean and format responses",
|
||||
)
|
||||
show_raw = st.checkbox(
|
||||
"Show raw response",
|
||||
value=False,
|
||||
help="Display the raw unprocessed response",
|
||||
)
|
||||
show_tools = st.checkbox(
|
||||
"Show tools",
|
||||
value=True,
|
||||
help="Display tools used",
|
||||
)
|
||||
show_thinking = st.checkbox(
|
||||
"Show thinking",
|
||||
value=False,
|
||||
help="Display the AI thinking text",
|
||||
)
|
||||
|
||||
# Clear chat button
|
||||
if st.button("🗑️ Clear Chat"):
|
||||
st.session_state.messages = []
|
||||
st.rerun()
|
||||
|
||||
# Connection status
|
||||
st.divider()
|
||||
if agent_arn:
|
||||
st.success("✅ Agent selected and ready")
|
||||
else:
|
||||
st.error("❌ Please select an agent")
|
||||
|
||||
# Initialize chat history
|
||||
if "messages" not in st.session_state:
|
||||
st.session_state.messages = []
|
||||
|
||||
# Display chat messages
|
||||
for message in st.session_state.messages:
|
||||
with st.chat_message(message["role"], avatar=message["avatar"]):
|
||||
st.markdown(message["content"])
|
||||
|
||||
# Chat input
|
||||
if prompt := st.chat_input("Type your message here..."):
|
||||
if not agent_arn:
|
||||
st.error("Please select an agent in the sidebar first.")
|
||||
return
|
||||
|
||||
# Add user message to chat history
|
||||
st.session_state.messages.append(
|
||||
{"role": "user", "content": prompt, "avatar": HUMAN_AVATAR}
|
||||
)
|
||||
with st.chat_message("user", avatar=HUMAN_AVATAR):
|
||||
st.markdown(prompt)
|
||||
|
||||
# Generate assistant response
|
||||
with st.chat_message("assistant", avatar=AI_AVATAR):
|
||||
message_placeholder = st.empty()
|
||||
chunk_buffer = ""
|
||||
|
||||
try:
|
||||
# Stream the response
|
||||
for chunk in invoke_agent_streaming(
|
||||
prompt,
|
||||
agent_arn,
|
||||
st.session_state.runtime_session_id,
|
||||
region,
|
||||
show_tools,
|
||||
):
|
||||
# Let's see what we get
|
||||
logger.debug(f"MAIN LOOP: chunk type: {type(chunk)}")
|
||||
logger.debug(f"MAIN LOOP: chunk content: {chunk}")
|
||||
|
||||
# Ensure chunk is a string before concatenating
|
||||
if not isinstance(chunk, str):
|
||||
logger.debug(
|
||||
f"MAIN LOOP: Converting non-string chunk to string"
|
||||
)
|
||||
chunk = str(chunk)
|
||||
|
||||
# Add chunk to buffer
|
||||
chunk_buffer += chunk
|
||||
|
||||
# Only update display every few chunks or when we hit certain characters
|
||||
if (
|
||||
len(chunk_buffer) % 3 == 0
|
||||
or chunk.endswith(" ")
|
||||
or chunk.endswith("\n")
|
||||
):
|
||||
if auto_format:
|
||||
# Clean the accumulated response
|
||||
cleaned_response = clean_response_text(
|
||||
chunk_buffer, show_thinking
|
||||
)
|
||||
message_placeholder.markdown(cleaned_response + " ▌")
|
||||
else:
|
||||
# Show raw response
|
||||
message_placeholder.markdown(chunk_buffer + " ▌")
|
||||
|
||||
time.sleep(0.01) # Reduced delay since we're batching updates
|
||||
|
||||
# Final response without cursor
|
||||
if auto_format:
|
||||
full_response = clean_response_text(chunk_buffer, show_thinking)
|
||||
else:
|
||||
full_response = chunk_buffer
|
||||
|
||||
message_placeholder.markdown(full_response)
|
||||
|
||||
# Show raw response in expander if requested
|
||||
if show_raw and auto_format:
|
||||
with st.expander("View raw response"):
|
||||
st.text(chunk_buffer)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"❌ **Error:** {str(e)}"
|
||||
message_placeholder.markdown(error_msg)
|
||||
full_response = error_msg
|
||||
|
||||
# Add assistant response to chat history
|
||||
st.session_state.messages.append(
|
||||
{"role": "assistant", "content": full_response, "avatar": AI_AVATAR}
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,61 @@
|
||||
# Basic strands agent streaming example.
|
||||
# To test locally, run `uv run agent.py` and then
|
||||
# curl -X POST http://localhost:8080/invocations -H "Content-Type: application/json" -d '{"prompt": "Hello!"}'
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import datetime
|
||||
import json
|
||||
|
||||
from bedrock_agentcore.runtime import BedrockAgentCoreApp
|
||||
from strands import Agent, tool
|
||||
from strands.models import BedrockModel
|
||||
from strands_tools import calculator
|
||||
|
||||
app = BedrockAgentCoreApp()
|
||||
|
||||
|
||||
@tool
|
||||
def weather():
|
||||
"""Get weather"""
|
||||
return "sunny"
|
||||
|
||||
|
||||
model_id = "us.amazon.nova-pro-v1:0"
|
||||
model = BedrockModel(
|
||||
model_id=model_id,
|
||||
)
|
||||
agent = Agent(
|
||||
model=model,
|
||||
tools=[calculator, weather],
|
||||
system_prompt="You're a helpful assistant. You can do simple math calculation, and tell the weather.",
|
||||
)
|
||||
|
||||
|
||||
@app.entrypoint
|
||||
async def strands_agent_bedrock(payload):
|
||||
"""
|
||||
Invoke the agent with a payload
|
||||
"""
|
||||
user_input = payload.get("prompt")
|
||||
agent_stream = agent.stream_async(user_input)
|
||||
tool_name = None
|
||||
try:
|
||||
async for event in agent_stream:
|
||||
|
||||
if (
|
||||
"current_tool_use" in event
|
||||
and event["current_tool_use"].get("name") != tool_name
|
||||
):
|
||||
tool_name = event["current_tool_use"]["name"]
|
||||
yield f"\n\n🔧 Using tool: {tool_name}\n\n"
|
||||
|
||||
if "data" in event:
|
||||
tool_name = None
|
||||
yield event["data"]
|
||||
except Exception as e:
|
||||
yield f"Error: {str(e)}"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run()
|
||||
@@ -0,0 +1,5 @@
|
||||
bedrock-agentcore
|
||||
boto3
|
||||
strands-agents
|
||||
strands-agents-tools
|
||||
uv
|
||||
@@ -0,0 +1,18 @@
|
||||
[project]
|
||||
name = "streamlit-chat"
|
||||
version = "0.1.0"
|
||||
description = "Simple Streamlit application for chatting with AI agents deployed to Amazon Bedrock AgentCore Runtime."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"boto3>=1.40.23",
|
||||
"streamlit>=1.49.1",
|
||||
"uv>=0.8.15",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"bedrock-agentcore>=0.1.3",
|
||||
"bedrock-agentcore-starter-toolkit>=0.1.8",
|
||||
"strands-agents>=1.7.1",
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path class="stroke-linejoin-round" fill="#962EFF" d="M6.15 10.3649L8 15.0049L9.86 10.3649L14.5 8.50488L9.86 6.65488L8 2.00488L6.15 6.65488L1.5 8.50488L6.15 10.3649Z" />
|
||||
<path class="filled no-stroke" fill="#962EFF" d="M2.38 4.915C2.4 4.965 2.45 4.995 2.5 4.995C2.55 4.995 2.62 4.915 2.62 4.915L3.28 3.275L4.92 2.615C4.97 2.595 5 2.545 5 2.495C5 2.445 4.92 2.375 4.92 2.375L3.28 1.715L2.62 0.075C2.58 -0.025 2.42 -0.025 2.38 0.075L1.72 1.715L0.0799942 2.375C0.0299942 2.395 0 2.445 0 2.495C0 2.545 0.0799942 2.615 0.0799942 2.615L1.72 3.275L2.38 4.915Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 625 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.91802 2.37562L3.27534 1.71766L2.61948 0.072746C2.57948 -0.0242487 2.41952 -0.0242487 2.37952 0.072746L1.72266 1.71766L0.0809838 2.37562C0.0329934 2.39562 0 2.44361 0 2.49561C0 2.54761 0.0319936 2.59561 0.0809838 2.61561L0.130974 2.6356L1.72266 3.27257L2.37952 4.91748C2.39952 4.96548 2.44751 4.99847 2.4995 4.99847C2.55149 4.99847 2.59948 4.96648 2.61948 4.91748L3.27634 3.27257L4.91902 2.61461C4.96701 2.59461 5 2.54661 5 2.49461C5 2.44262 4.96801 2.39462 4.91902 2.37462L4.91802 2.37562Z" fill="#FAF5FF"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.99907 1.00458C8.40831 1.0042 8.77649 1.25314 8.92848 1.63299L10.6291 5.88317L14.8704 7.57368C15.2502 7.7251 15.4996 8.09251 15.5 8.50136C15.5004 8.91021 15.2517 9.27808 14.8721 9.43021L10.6291 11.1306L8.9282 15.3723C8.77603 15.7518 8.40804 16.0004 7.99907 16C7.5901 15.9996 7.22257 15.7503 7.07111 15.3705L5.38008 11.1305L1.12861 9.43048C0.748637 9.27854 0.499621 8.91048 0.5 8.50136C0.50038 8.09224 0.750079 7.72465 1.13033 7.57341L5.38008 5.88316L7.07084 1.63472C7.22212 1.25458 7.58983 1.00496 7.99907 1.00458ZM8.0025 4.70229L7.07917 7.02241C6.97752 7.27782 6.77516 7.48012 6.51967 7.58173L4.19884 8.50479L6.52139 9.43353C6.77585 9.53528 6.97739 9.73698 7.07889 9.99148L8.0025 12.3073L8.9318 9.98975C9.03344 9.73628 9.23437 9.53541 9.48792 9.4338L11.8062 8.50479L9.48965 7.58146C9.23507 7.47999 9.03331 7.27851 8.93152 7.02413L8.0025 4.70229Z" fill="#FAF5FF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#5C7FFF" d="M8 7C9.66 7 11 5.66 11 4C11 2.34 9.66 1 8 1C6.34 1 5 2.34 5 4C5 5.66 6.34 7 8 7Z" />
|
||||
<path fill="#5C7FFF" d="M2 16V13C2 11.34 3.34 10 5 10H11C12.66 10 14 11.34 14 13V16" class="stroke-linejoin-round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 294 B |
+1793
File diff suppressed because it is too large
Load Diff
@@ -29,3 +29,4 @@
|
||||
- w601sxs
|
||||
- erezweinstein5
|
||||
- HardikThakkar94
|
||||
- brianloyal
|
||||
Reference in New Issue
Block a user