AWS Builder Center
 Building a  Chat Interface with Strands Agents and WebSockets

Building a Chat Interface with Strands Agents and WebSockets

Create a serverless implementation of strands agents.

AWS Solutions Architect
Welcome to Day 6 of Challenge #2 ! Welcome to your guide for creating a serverless implementation of a chat bot that is powered by AI agents! Today, you'll build a complete chat system that combines AWS Lambda, API Gateway WebSockets, and Streamlit to create an interactive AI assistant.

What You Will Accomplish Today:

✅ Deploy a Strands agent as a Lambda function✅ Configure API Gateway WebSocket for real-time communication✅ Build a responsive Streamlit chat interface✅ Implement thread-safe message handling

What You Will Need:

💻 AWS account with Lambda and API Gateway access
🐍 Python 3.10+ installed locally
📦 Strands SDK and dependencies
⏰ About 30 minutes
🔑 Basic understanding of WebSockets and async programming

Step 1: Understanding the Architecture 🏗️

Before we start coding, let's understand how all the pieces fit together.
Frontend (Streamlit): Provides the chat UI and manages conversation history
WebSocket Connection: Enables real-time bidirectional communication
Backend (Lambda + Strands): Processes messages using your AI agent
How Messages Flow
  1. User types a message in Streamlit
  2. Message is sent via WebSocket to API Gateway
  3. API Gateway triggers Lambda function
  4. Lambda invokes Strands agent to generate response
  5. Response is sent back through WebSocket
  6. Streamlit displays the response in the chat

Step 2: Setting Up the Lambda Backend 🔧

First, let's create a Lambda layer that will be used by our function. A Lambda Layer is a ZIP file containing libraries and dependencies that your Lambda function can use. It's used to add external packages when they are too large to bundle with your base Lambda code. To make this layer:
Create a requirements.txt file:
1
2
strands-sdk
strands-agents-tools
Package your Lambda layer:
1
2
3
mkdir python
pip install -r requirements.txt -t python/
zip -r layer.zip python/
After you create a the ZIP file, upload it to an Amazon S3 bucket. Copy the S3 Link URL (https://mybucket.s3.us-east-1.amazonaws.com/path/to/object.zip) From there go to the Lambda console and in the left side bar menu, select “Lambda layer”. Create a new layer using the S3 Link URL. You don’t have to worry about the optional settings. Next, let’s create the actual Lambda function that will host your Strands agent.
Create the Lambda Function
Navigate to the AWS Lambda console and create a new function:
  • Function name: chat-agent-handler(feel free to name it whatever you'd like)
  • Runtime: Python 3.11+
  • Architecture: x86_64
  • Execution role: Create new with basic Lambda permissions
After creating the Lambda function, go to its execution role and add the AmazonAPIGatewayInvokeFullAccess policy and AmazonBedrockFullAccess to it.
Attach the Layer to Your Function:
1. In the Lambda console, scroll to the "Layers" section
2. Click "Add a layer"
3. Select "Custom layers"
4. Choose the layer you created
5. Select version 1
6. Click "Add"

Step 3: Implementing the Lambda Handler 🎯

Now let's write the Lambda function that processes chat messages using Strands.
First, we are gonna set up the agent. For todays challenge, we are gonna have our Agent be a League of Legends expert.
1
## Important Rules:
"""Next, let’s create some helper functions.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# ============================================================================
# HELPER FUNCTIONS
# ============================================================================

def send_response(connection_id, domain_name, stage, data):
"""
Send a message back to the client through the WebSocket connection.

This function uses the API Gateway Management API to post messages back
to connected WebSocket clients.
"""


# Construct the API Gateway Management API endpoint
# Format: https://{domain}/{stage}
endpoint = f'https://{domain_name}/{stage}'

# Initialize the API Gateway Management API client
client = boto3.client('apigatewaymanagementapi', endpoint_url=endpoint)

try:
print(f"Sending message to connection {connection_id}")
print(f" Message type: {data.get('type')}, Content length: {len(data.get('content', ''))}")

# Send the message to the connected client
client.post_to_connection(
ConnectionId=connection_id,
Data=json.dumps(data).encode('utf-8')
)

print(f"Message sent successfully")

except client.exceptions.GoneException:
# Connection no longer exists (client disconnected)
print(f"Connection {connection_id} is no longer available")
raise

except Exception as e:
print(f"Error sending message: {str(e)}")
raise

def handle_message(event, messages):
"""
Process user messages using the Strands AI agent.

This function initializes a Strands agent with the configured system prompt
and available tools, then processes the user's message to generate a response.

"""

try:
# Initialize the Strands agent with system prompt and tools
# The agent is created fresh for each invocation to ensure clean state
agent = Agent(
system_prompt=SYSTEM_PROMPT,
tools=[http_request] # Add additional tools as needed
)

# Extract the most recent user message from the conversation
# Messages array contains full conversation history for context
user_message = messages[-1]['content'] if messages else ''

print(f"Processing user message: {user_message[:100]}...")

# Invoke the agent to generate a response
# The agent will use its tools and reasoning to formulate an answer
response = agent(user_message)

print(f"Agent response generated: {len(str(response))} characters")

return str(response)

except Exception as e:
print(f"Error in handle_message: {str(e)}")
raise
Next, let’s add the Lambda handler. This will handle our web sockets and also the processing of the conversation.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# ============================================================================
# MAIN LAMBDA HANDLER
# ============================================================================

def lambda_handler(event, context):
"""
Main Lambda handler for WebSocket API Gateway events.
"""

try:

# Extract WebSocket connection information from the event

request_context = event['requestContext']
connection_id = request_context['connectionId']
domain_name = request_context['domainName']
stage = request_context['stage']
route_key = request_context.get('routeKey')

print(f"Received event: route={route_key}, connection={connection_id}")


# Handle $default route - process incoming chat messages

if route_key == '$default':
# Parse the message body from the WebSocket message
body = json.loads(event.get('body', '{}'))
messages = body.get('messages', [])

print(f"Processing {len(messages)} message(s) in conversation")

try:
# Generate response using the Strands agent
response_text = handle_message(event, messages)

# Send the response content back to the client
# Using 'chunk' type to support potential streaming in the future
send_response(connection_id, domain_name, stage, {
'type': 'chunk',
'content': response_text
})

# Send end signal to indicate response is complete
# Client can use this to stop showing loading indicators
send_response(connection_id, domain_name, stage, {
'type': 'end',
'content': ''
})

# Return success response
# Note: The client receives messages via WebSocket, not this HTTP response
return {
'statusCode': 200,
'body': json.dumps({
'type': 'chunk',
'content': response_text
})
}

except Exception as e:
# Handle errors during message processing
error_message = f"Error processing message: {str(e)}"
print(f"{error_message}")

# Attempt to send error message back to client
try:
send_response(connection_id, domain_name, stage, {
'type': 'error',
'content': 'Sorry, I encountered an error processing your message.'
})
except:
# If we can't send error to client, just log it
print("Could not send error message to client")

return {
'statusCode': 500,
'body': json.dumps({'message': str(e)})
}


# Handle unexpected errors at the handler level

except KeyError as e:
# Missing required fields in the event
error_message = f"Missing required field in event: {str(e)}"
print(f"{error_message}")
return {
'statusCode': 400,
'body': json.dumps({'message': error_message})
}

except Exception as e:
# Catch-all for any other unexpected errors
error_message = f"Unexpected error in lambda_handler: {str(e)}"
print(f"{error_message}")
return {
'statusCode': 500,
'body': json.dumps({'message': 'Internal server error'})
}
Update Lambda Configuration
In the Lambda console:
  • Timeout: Set to 2min (agents may take time to process)
  • Memory: Set to 200 MB

Step 4: Configuring API Gateway WebSocket 🌐

Now let's set up the API Gateway to handle WebSocket connections.
Create WebSocket API
  1. Go to API Gateway Console
  2. Click Create API → WebSocket API
  3. Set API name: chat-agent-websocket
  4. Set Route selection expression: $request.body.action
Configure Route
For simplicity, we will only configure the default route. However, if you wanted to have different logic when you connect or disconnect, you can add routes for those.
$default Route
  • Route key: $default
  • Integration: Lambda function
  • Lambda function: chat-agent-handler(or whatever you named it)
  • Stage: Production
Deploy the API
  1. Click Actions → Deploy API
  2. Note your WebSocket URL: wss://YOUR-API-ID.execute-api.REGION.amazonaws.com/production and @connections URL: https://YOUR-API-ID.execute-api.REGION.amazonaws.com/production
  3. Update the endpoint In chat-agent-handler lambda, replace the @connections URL with your actual API Gateway endpoint:

Step 5: Building the Streamlit Frontend 💬

Time to create the beautiful chat interface that users will interact with.
Install Streamlit Dependencies
1
pip install streamlit websocket-client
Create the Chat Application
Create app.py:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
"""Streamlit Chat Application with WebSocket LLM Backend
This application provides a chat interface that communicates with an LLMvia WebSocket connections. Messages are sent to AWS API Gateway and responsesare received asynchronously through a background thread."""


import streamlit as st
import json
import websocket
import logging
import threading
import time
from queue import Queue, Empty

# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# ============================================================================
# SESSION STATE INITIALIZATION
# ============================================================================

# Store conversation history
if 'messages' not in st.session_state:
st.session_state.messages = []

# Track if we're waiting for an LLM response
if 'waiting_for_response' not in st.session_state:
st.session_state.waiting_for_response = False

# ============================================================================
# WEBSOCKET SETUP
# ============================================================================

@st.cache_resource
def init_websocket():
""" Initialize WebSocket connection and response queue.
Uses @st.cache_resource to maintain a single persistent connection across Streamlit reruns. The queue enables thread-safe communication between the WebSocket background thread and Streamlit's main thread.
Returns: tuple: (WebSocket connection, Queue for responses) """

response_queue = Queue()

# WebSocket event handlers
def on_message(ws, message):
"""Handle incoming messages from the WebSocket"""
try:
response = json.loads(message)

# Only process content chunks (ignore end/status messages)
if response.get('type') == 'chunk':
content = response.get('content', '')
response_queue.put(content)
logger.info(f"Received response ({len(content)} chars)")
except Exception as e:
logger.error(f"Error processing message: {e}")

def on_error(ws, error):
"""Handle WebSocket errors"""
logger.error(f"WebSocket error: {error}")

def on_close(ws, close_status_code, close_msg):
"""Handle WebSocket disconnection"""
logger.warning(f"WebSocket closed: {close_status_code}")

def on_open(ws):
"""Handle successful WebSocket connection"""
logger.info("WebSocket connected")

# Create WebSocket connection (use your API Gateway URL)
ws = websocket.WebSocketApp(
"wss://YOUR-API-ID.execute-api.YOUR-REGION.amazonaws.com/production/",
on_message=on_message,
on_error=on_error,
on_close=on_close,
on_open=on_open )

# Run WebSocket in background daemon thread
wst = threading.Thread(target=ws.run_forever)
wst.daemon = True
wst.start()

return ws, response_queue
# Initialize WebSocket connection (only runs once)
if "ws" not in st.session_state:
st.session_state.ws, st.session_state.response_queue = init_websocket()

# ============================================================================
# RESPONSE HANDLING
# ============================================================================

# Get response queue reference
response_queue = st.session_state.response_queue
# Check if a response has arrived while we're waiting
if st.session_state.waiting_for_response:
try:
# Non-blocking check for response in queue
current_response = response_queue.get_nowait()

# Add assistant's response to conversation history
st.session_state.messages.append({
"role": "assistant",
"content": current_response })

# Stop waiting
st.session_state.waiting_for_response = False
logger.info("Response received and added to chat")

except Empty:
# No response yet, continue waiting
pass

# ============================================================================
# UI RENDERING
# ============================================================================

st.title("Chat with League LLM")

# Display conversation history
for message in st.session_state.messages:
with st.chat_message(message["role"]):
st.markdown(message["content"])

# Show "Thinking..." indicator while waiting for response
if st.session_state.waiting_for_response:
with st.chat_message("assistant"):
st.write("Thinking...")

# Poll every 0.5 seconds to check for new responses
time.sleep(0.5)
st.rerun()

# ============================================================================
# USER INPUT HANDLING
# ============================================================================

# Handle new user messages
if prompt := st.chat_input("What would you like to know?"):
logger.info(f"User sent: {prompt}")

# Add user message to conversation history
st.session_state.messages.append({
"role": "user",
"content": prompt })

# Clear any stale responses from the queue
while not response_queue.empty():
try:
response_queue.get_nowait()
except Empty:
break

# Prepare message payload with full conversation context
message_data = {
"messages": [
{"role": m["role"], "content": m["content"]}
for m in st.session_state.messages ]
}

# Send message to LLM via WebSocket
st.session_state.ws.send(json.dumps(message_data))

# Set waiting state and refresh UI
st.session_state.waiting_for_response = True
st.rerun()
Update the WebSocket URLIn app.py, replace the WebSocket URL with your actual API Gateway endpoint:
1
2
3
4
ws = websocket.WebSocketApp(
"wss://abc123.execute-api.us-east-1.amazonaws.com/production/",
# ... rest of the code
)

Step 6: Testing Your Chat Application 🧪

Let's verify that everything works end-to-end!Run the Streamlit App
1
streamlit run app.py
Your browser should open to http://localhost:8501Test the Chat Flow
  1. Type "Hello!" in the chat input
  2. You should see "Thinking..." appear
  3. Within a few seconds, the agent's response should appear
  4. Try follow-up questions to test the agent
Check CloudWatch LogsMonitor your Lambda function logs for:
  • ✅ Successful WebSocket connections
  • ✅ Message processing
  • ✅ Agent responses
  • ❌ Any errors or timeouts

Troubleshooting Common Issues 🔍

WebSocket Connection FailsProblem: WebSocket error: [Errno 111] Connection refusedSolution:
  • Verify your API Gateway WebSocket URL is correct
  • Ensure the API is deployed to the production stage
  • Check that routes are properly configured
Lambda TimeoutProblem: Messages take too long and Lambda times outSolution:
  • Increase Lambda timeout
  • Increase memory
  • Optimize your agent's prompt for faster responses
Messages Not AppearingProblem: "Thinking..." shows but response never appearsSolution:
  • Check Lambda CloudWatch logs for errors
  • Verify Lambda has permission to call execute-api:ManageConnections
Queue is Always EmptyProblem: Logs show "Response queued" but Streamlit never receives itSolution:
  • Verify WebSocket is initialized with @st.cache_resource
  • Check that both threads reference the same Queue instance
  • Look for exceptions in the on_message handler
Checklist ✅Verify you've completed all components:
  • Lambda function deployed with Strands agent
  • Lambda has proper IAM permissions for Bedrock and API Gateway
  • API Gateway WebSocket configured
  • WebSocket API deployed to production stage
  • Streamlit app created with proper WebSocket URL
  • Message flow tested end-to-end
  • CloudWatch logging configured

Next Steps: Enhancing Your Chat Application 🚀

Now that you have a working chat application, you can take the project further with everything you’ve learned from the previous challenges, such as adding a knowledge base or even memory.

Conclusion 🎉

Congratulations! You've built a production-ready real-time chat application powered by AI agents. You now have:✅ A serverless backend that scales automatically✅ Real-time WebSocket communication✅ A beautiful chat interfaceThis architecture forms the foundation for countless AI-powered applications. You could adapt this to build:Keep experimenting, and happy building! 🚀
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article