Skip to content

Commit 1241c73

Browse files
committed
Initial commit
1 parent 4321213 commit 1241c73

File tree

12 files changed

+534
-0
lines changed

12 files changed

+534
-0
lines changed

cx-agent-frontend/Dockerfile

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Multi-stage build for Streamlit frontend
2+
FROM python:3.11-slim as builder
3+
4+
# Install uv
5+
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
6+
7+
# Set working directory
8+
WORKDIR /app
9+
10+
# Copy dependency files
11+
COPY pyproject.toml ./
12+
13+
# Install dependencies
14+
RUN uv sync --frozen --no-dev
15+
16+
# Production stage
17+
FROM python:3.11-slim as production
18+
19+
# Install uv
20+
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
21+
22+
# Create non-root user
23+
RUN groupadd -r appuser && useradd -r -g appuser appuser
24+
25+
# Set working directory
26+
WORKDIR /app
27+
28+
# Copy virtual environment from builder
29+
COPY --from=builder /app/.venv /app/.venv
30+
31+
# Copy application code
32+
COPY src/ ./src/
33+
34+
# Change ownership to non-root user
35+
RUN chown -R appuser:appuser /app
36+
37+
# Switch to non-root user
38+
USER appuser
39+
40+
# Set environment variables
41+
ENV PATH="/app/.venv/bin:$PATH"
42+
ENV PYTHONPATH="/app/src"
43+
44+
# Expose port
45+
EXPOSE 8501
46+
47+
# Run application
48+
CMD ["streamlit", "run", "src/app.py", "--server.port=8501", "--server.address=0.0.0.0"]

cx-agent-frontend/pyproject.toml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,32 @@
1+
[project]
2+
name = "cx-agent-frontend"
3+
version = "0.1.0"
4+
description = "Clean Streamlit Frontend for CX Agent"
5+
requires-python = ">=3.11"
6+
dependencies = [
7+
"streamlit>=1.28.0",
8+
"requests>=2.31.0",
9+
"pydantic>=2.5.0",
10+
"boto3>=1.34.0",
11+
]
112

13+
[project.optional-dependencies]
14+
dev = [
15+
"ruff>=0.1.0",
16+
"mypy>=1.7.0",
17+
]
18+
19+
[build-system]
20+
requires = ["hatchling"]
21+
build-backend = "hatchling.build"
22+
23+
[tool.hatch.build.targets.wheel]
24+
packages = ["src"]
25+
26+
[tool.ruff]
27+
target-version = "py311"
28+
line-length = 88
29+
30+
[tool.mypy]
31+
python_version = "3.11"
32+
strict = true

cx-agent-frontend/src/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Frontend package

cx-agent-frontend/src/app.py

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
"""Main Streamlit application."""
2+
3+
import uuid
4+
from datetime import datetime
5+
6+
import streamlit as st
7+
8+
from components.chat import render_message, render_sidebar
9+
from components.config import render_agentcore_config
10+
from models.message import Message
11+
from services.conversation_client import ConversationClient
12+
from services.agentcore_client import AgentCoreClient
13+
14+
15+
def init_session_state():
16+
"""Initialize session state variables."""
17+
if "conversation_id" not in st.session_state:
18+
st.session_state.conversation_id = None
19+
if "messages" not in st.session_state:
20+
st.session_state.messages = []
21+
if "user_id" not in st.session_state:
22+
st.session_state.user_id = f"user_{str(uuid.uuid4())[:8]}"
23+
if "agent_runtime_arn" not in st.session_state:
24+
st.session_state.agent_runtime_arn = ""
25+
if "region" not in st.session_state:
26+
st.session_state.region = "us-east-1"
27+
if "use_agentcore" not in st.session_state:
28+
st.session_state.use_agentcore = False
29+
if "auth_token" not in st.session_state:
30+
st.session_state.auth_token = ""
31+
32+
33+
def main():
34+
"""Main application."""
35+
st.set_page_config(
36+
page_title="CX Agent Chat",
37+
page_icon="🤖",
38+
layout="wide",
39+
initial_sidebar_state="expanded",
40+
)
41+
42+
# Custom CSS for better design
43+
st.markdown("""
44+
<style>
45+
.stButton > button {
46+
border-radius: 20px;
47+
border: 1px solid #e0e0e0;
48+
transition: all 0.3s ease;
49+
}
50+
.stButton > button:hover {
51+
transform: translateY(-2px);
52+
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
53+
}
54+
.feedback-section {
55+
background-color: #f8f9fa;
56+
padding: 10px;
57+
border-radius: 10px;
58+
margin: 10px 0;
59+
}
60+
</style>
61+
""", unsafe_allow_html=True)
62+
63+
st.title("🤖 CX Agent Chat")
64+
if st.session_state.get('use_agentcore', False):
65+
st.caption("Powered by AWS Bedrock AgentCore Runtime")
66+
else:
67+
st.caption("Powered by LiteLLM Gateway hosted on AWS")
68+
69+
init_session_state()
70+
71+
# Render configuration
72+
config_valid = render_agentcore_config()
73+
model = render_sidebar()
74+
75+
# Initialize client based on configuration
76+
if st.session_state.use_agentcore and config_valid:
77+
auth_token = st.session_state.get('auth_token', '')
78+
client = AgentCoreClient(
79+
agent_runtime_arn=st.session_state.agent_runtime_arn,
80+
region=st.session_state.region,
81+
auth_token=auth_token
82+
)
83+
if auth_token:
84+
st.info("🚀 Connected to AgentCore Runtime")
85+
else:
86+
st.warning("⚠️ AgentCore configured but no auth token provided")
87+
else:
88+
client = ConversationClient()
89+
if st.session_state.use_agentcore:
90+
st.warning("⚠️ AgentCore configuration invalid, using local backend")
91+
else:
92+
st.info("🔧 Using local backend")
93+
94+
# Display chat messages
95+
for message in st.session_state.messages:
96+
render_message(message, client)
97+
98+
# Chat input
99+
if prompt := st.chat_input("Type your message..."):
100+
# Create conversation if needed
101+
if not st.session_state.conversation_id:
102+
with st.spinner("Creating conversation..."):
103+
conversation_id = client.create_conversation(st.session_state.user_id)
104+
if not conversation_id:
105+
st.stop()
106+
st.session_state.conversation_id = conversation_id
107+
108+
# Add user message
109+
user_message = Message(role="user", content=prompt, timestamp=datetime.now())
110+
st.session_state.messages.append(user_message)
111+
render_message(user_message)
112+
113+
# Send message and get response
114+
with st.spinner("Thinking..."):
115+
response = client.send_message(
116+
st.session_state.conversation_id, prompt, model
117+
)
118+
119+
if response:
120+
# Debug: Show what we received
121+
st.write("Debug - API Response:", response)
122+
123+
# Add assistant message
124+
metadata = {
125+
"model": model,
126+
"status": response.get("status", "success"),
127+
}
128+
129+
# Add tools_used to metadata if available
130+
if "tools_used" in response and response["tools_used"]:
131+
metadata["tools_used"] = ",".join(response["tools_used"])
132+
133+
assistant_message = Message(
134+
role="assistant",
135+
content=response["response"],
136+
timestamp=datetime.now(),
137+
metadata=metadata,
138+
)
139+
st.session_state.messages.append(assistant_message)
140+
render_message(assistant_message, client)
141+
142+
st.rerun()
143+
144+
145+
if __name__ == "__main__":
146+
main()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Components package
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
"""Chat components."""
2+
3+
import streamlit as st
4+
5+
from models.message import Message
6+
from services.conversation_client import ConversationClient
7+
8+
9+
def render_message(message: Message, client: ConversationClient = None):
10+
"""Render a single message."""
11+
with st.chat_message(message.role):
12+
st.write(message.content)
13+
14+
# Add feedback buttons for assistant messages
15+
if message.role == "assistant" and client:
16+
message_id = str(message.metadata.get("message_id", hash(message.content)))
17+
18+
# Check if feedback already given
19+
feedback_key = f"feedback_given_{message_id}"
20+
if feedback_key not in st.session_state:
21+
st.session_state[feedback_key] = False
22+
23+
if not st.session_state[feedback_key]:
24+
st.markdown("---")
25+
st.markdown("**Was this helpful?**")
26+
27+
col1, col2, col3 = st.columns([2, 2, 6])
28+
29+
with col1:
30+
if st.button("👍 Helpful", key=f"up_{message_id}", use_container_width=True):
31+
if client.submit_feedback(message_id, st.session_state.get("conversation_id", "default"), 1.0, ""):
32+
st.session_state[feedback_key] = True
33+
st.success("✓ Thank you for your feedback!")
34+
st.rerun()
35+
36+
with col2:
37+
if st.button("👎 Not helpful", key=f"down_{message_id}", use_container_width=True):
38+
with st.expander("📝 Tell us more (optional)", expanded=True):
39+
feedback_text = st.text_area(
40+
"How can we improve?",
41+
key=f"text_{message_id}",
42+
placeholder="Your feedback helps us improve...",
43+
height=80
44+
)
45+
if st.button("📤 Submit Feedback", key=f"submit_{message_id}", type="primary"):
46+
if client.submit_feedback(message_id, st.session_state.get("conversation_id", "default"), 0.0, feedback_text):
47+
st.session_state[feedback_key] = True
48+
st.success("✓ Thank you for your feedback!")
49+
st.rerun()
50+
else:
51+
st.markdown("<div style='color: #28a745; font-size: 0.9em;'>✓ Feedback submitted</div>", unsafe_allow_html=True)
52+
53+
# Show tool calls if available
54+
if message.metadata and "tools_used" in message.metadata:
55+
tools_str = message.metadata["tools_used"]
56+
if tools_str:
57+
tools_used = [tool.strip() for tool in tools_str.split(",") if tool.strip()]
58+
if tools_used:
59+
st.markdown("**🔧 Tools Used:**")
60+
for tool in tools_used:
61+
st.markdown(f"• `{tool}`")
62+
63+
# Show metadata if available
64+
if message.metadata:
65+
with st.expander("Message Details", expanded=False):
66+
st.json(message.metadata)
67+
68+
69+
def render_sidebar():
70+
"""Render sidebar with configuration."""
71+
with st.sidebar:
72+
st.header("Configuration")
73+
74+
# Model selection
75+
model = st.selectbox(
76+
"Model",
77+
options=[
78+
"gpt-4o-mini",
79+
"gpt-4o",
80+
"bedrock/anthropic.claude-3-5-sonnet-20240620-v1:0",
81+
"bedrock/anthropic.claude-3-5-haiku-20241022-v1:0",
82+
],
83+
index=0,
84+
)
85+
86+
# User ID
87+
user_id = st.text_input("User ID", value=st.session_state.user_id)
88+
st.session_state.user_id = user_id
89+
90+
# Conversation controls
91+
st.header("Conversation")
92+
93+
if st.button("New Conversation", type="primary"):
94+
st.session_state.conversation_id = None
95+
st.session_state.messages = []
96+
st.rerun()
97+
98+
if st.session_state.conversation_id:
99+
st.success(f"Active: {st.session_state.conversation_id[:8]}...")
100+
101+
return model
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
"""Configuration components for AgentCore."""
2+
3+
import streamlit as st
4+
5+
6+
def render_agentcore_config():
7+
"""Render AgentCore configuration in sidebar."""
8+
with st.sidebar:
9+
st.header("⚙️ Configuration")
10+
11+
# Backend selection
12+
use_agentcore = st.checkbox(
13+
"Use AWS AgentCore",
14+
value=st.session_state.get("use_agentcore", False),
15+
help="Toggle between local backend and AWS AgentCore"
16+
)
17+
st.session_state.use_agentcore = use_agentcore
18+
19+
if use_agentcore:
20+
st.subheader("AgentCore Settings")
21+
22+
# Agent Runtime ARN
23+
agent_runtime_arn = st.text_input(
24+
"Agent Runtime ARN",
25+
value=st.session_state.get("agent_runtime_arn", ""),
26+
placeholder="arn:aws:bedrock-agent-runtime:region:account:agent/agent-id",
27+
help="AWS Bedrock Agent Runtime ARN"
28+
)
29+
st.session_state.agent_runtime_arn = agent_runtime_arn
30+
31+
# Region
32+
region = st.selectbox(
33+
"AWS Region",
34+
["us-east-1", "us-west-2", "eu-west-1", "ap-southeast-1"],
35+
index=0 if st.session_state.get("region", "us-east-1") == "us-east-1" else
36+
["us-east-1", "us-west-2", "eu-west-1", "ap-southeast-1"].index(st.session_state.get("region", "us-east-1"))
37+
)
38+
st.session_state.region = region
39+
40+
# Auth Token
41+
auth_token = st.text_input(
42+
"JWT Auth Token",
43+
value=st.session_state.get("auth_token", ""),
44+
type="password",
45+
placeholder="Enter JWT token from Cognito",
46+
help="JWT token for authentication with AgentCore"
47+
)
48+
st.session_state.auth_token = auth_token
49+
50+
# Validation
51+
config_valid = bool(agent_runtime_arn.strip() and auth_token.strip())
52+
if not agent_runtime_arn.strip():
53+
st.error("⚠️ Agent Runtime ARN is required")
54+
if not auth_token.strip():
55+
st.error("⚠️ JWT Auth Token is required")
56+
57+
return config_valid
58+
else:
59+
st.info("Using local backend")
60+
return True

0 commit comments

Comments
 (0)