Skip to content

Commit 535faa5

Browse files
sundargthbAbishek10Abishek KumarclaudeSundar Raghavan
authored
Feature/bidirectional streaming (#180)
* feat: add WebSocket support with @app.websocket decorator (#41) * feat: add WebSocket support with @app.websocket decorator Add bidirectional streaming support for agent invocations: - Add @app.websocket decorator for registering WebSocket handlers - Register /ws endpoint in BedrockAgentCoreApp constructor - Implement _handle_websocket method with proper error handling - Follow existing decorator patterns (entrypoint, ping) for consistency - Support WebSocketDisconnect for graceful connection handling - Integrate with existing RequestContext for session management test: add unit and integration tests for WebSocket support Add comprehensive test coverage for WebSocket decorator: Unit tests (10 tests): - WebSocket route initialization and registration - Decorator functionality and handler storage - Basic send/receive communication - Context integration with session IDs - Exception handling and error cases - Multiple message handling - Graceful disconnect handling - Custom request headers via context - Streaming data functionality Integration test: - End-to-end WebSocket echo server - Streaming multiple messages - Session ID propagation through headers - Real WebSocket client connection testing All tests pass successfully. * fix: remove exception details from WebSocket close reason Don't send exception messages to clients when closing WebSocket connections with code 1011. This prevents leaking internal error details to clients. Changes: - Remove reason parameter from websocket.close() calls - Exception details are still logged server-side - Tests continue to pass as they only check for disconnect exceptions --------- Co-authored-by: Abishek Kumar <kumabish@amazon.com> * feat: add AgentRuntimeClient for WebSocket authentication (#42) * feat: add AgentRuntimeClient for WebSocket authentication - Implemented AgentRuntimeClient with methods for generating WebSocket credentials - Added generate_ws_connection() for backend services (returns URL + SigV4 headers) - Added generate_presigned_url() for frontend clients (returns presigned URL) - Support for endpoint qualifiers and custom query parameters (presigned URLs only) - Comprehensive unit tests with 22 test cases covering all functionality - Integration tests for validating credential format - Complete documentation with usage examples 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: move websockets to dev dependencies for CI The websockets package was in optional dependencies which caused CI test failures. Moving it to dev dependencies ensures it's installed during CI test runs. * fix: skip integration tests when AWS credentials unavailable Add skipif decorator to integration tests that require AWS credentials. This prevents CI failures when credentials are not configured. * refactor: use mocked credentials instead of skipping tests Replace skipif decorators with pytest fixture that mocks boto3 credentials using botocore.credentials.Credentials class. This ensures integration tests run in all environments (local and CI) while still validating URL and header generation logic. Benefits: - Tests always run, providing better coverage - Uses real Credentials class for proper SigV4 signing - No need for AWS credentials to be configured * refactor: rename AgentRuntimeClient to AgentCoreRuntimeClient - Rename class from AgentRuntimeClient to AgentCoreRuntimeClient - Rename module from agent_runtime_client.py to agent_core_runtime_client.py - Update all unit and integration tests to use new class name - Update __init__.py exports - Update documentation examples This provides a more accurate name that reflects the AgentCore Runtime service. --------- Co-authored-by: Abishek Kumar <kumabish@amazon.com> Co-authored-by: Claude <noreply@anthropic.com> * chore: exclude integration testing workflow changes * fix: add websockets to integration test --------- Co-authored-by: Abishek K <Abishek10@users.noreply.github.com> Co-authored-by: Abishek Kumar <kumabish@amazon.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Sundar Raghavan <sdraghav@amazon.com>
1 parent 95bbfa4 commit 535faa5

File tree

11 files changed

+2373
-804
lines changed

11 files changed

+2373
-804
lines changed

.github/workflows/integration-testing.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ jobs:
130130
- name: Install dependencies
131131
run: |
132132
pip install -e .
133-
pip install --no-cache-dir pytest requests strands-agents uvicorn httpx starlette
133+
pip install --no-cache-dir pytest requests strands-agents uvicorn httpx starlette websockets
134134
135135
- name: Run integration tests
136136
env:
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
# AgentCoreRuntimeClient Examples
2+
3+
This document provides practical examples for using the `AgentCoreRuntimeClient` to authenticate WebSocket connections to AgentCore Runtime.
4+
5+
## Basic Usage
6+
7+
### Backend Service (SigV4 Headers)
8+
9+
```python
10+
from bedrock_agentcore.runtime import AgentCoreRuntimeClient
11+
import websockets
12+
import asyncio
13+
14+
async def main():
15+
# Initialize client
16+
client = AgentCoreRuntimeClient(region="us-west-2")
17+
18+
# Generate WebSocket connection with authentication
19+
ws_url, headers = client.generate_ws_connection(
20+
runtime_arn="arn:aws:bedrock-agentcore:us-west-2:123456789012:runtime/my-runtime"
21+
)
22+
23+
# Connect using any WebSocket library
24+
async with websockets.connect(ws_url, extra_headers=headers) as ws:
25+
# Send message
26+
await ws.send('{"inputText": "Hello!"}')
27+
28+
# Receive response
29+
response = await ws.recv()
30+
print(f"Received: {response}")
31+
32+
asyncio.run(main())
33+
```
34+
35+
### Frontend Client (Presigned URL)
36+
37+
```python
38+
from bedrock_agentcore.runtime import AgentCoreRuntimeClient
39+
40+
# Backend: Generate presigned URL
41+
client = AgentCoreRuntimeClient(region="us-west-2")
42+
43+
presigned_url = client.generate_presigned_url(
44+
runtime_arn="arn:aws:bedrock-agentcore:us-west-2:123456789012:runtime/my-runtime",
45+
expires=300 # 5 minutes
46+
)
47+
48+
# Share presigned_url with frontend
49+
# Frontend JavaScript: new WebSocket(presigned_url)
50+
```
51+
52+
## Advanced Usage
53+
54+
### With Endpoint Qualifier
55+
56+
```python
57+
client = AgentCoreRuntimeClient(region="us-west-2")
58+
59+
# For generate_ws_connection (header-based auth)
60+
ws_url, headers = client.generate_ws_connection(
61+
runtime_arn="arn:aws:bedrock-agentcore:us-west-2:123456789012:runtime/my-runtime",
62+
endpoint_name="DEFAULT"
63+
)
64+
# URL will include: ?qualifier=DEFAULT
65+
66+
# For generate_presigned_url (query-based auth)
67+
presigned_url = client.generate_presigned_url(
68+
runtime_arn="arn:aws:bedrock-agentcore:us-west-2:123456789012:runtime/my-runtime",
69+
endpoint_name="DEFAULT"
70+
)
71+
# URL will include: ?qualifier=DEFAULT&X-Amz-Algorithm=...
72+
```
73+
74+
### With Custom Query Parameters (Presigned URL only)
75+
76+
```python
77+
client = AgentCoreRuntimeClient(region="us-west-2")
78+
79+
# custom_headers parameter is only available for presigned URLs
80+
presigned_url = client.generate_presigned_url(
81+
runtime_arn="arn:aws:bedrock-agentcore:us-west-2:123456789012:runtime/my-runtime",
82+
custom_headers={"custom_param": "value", "another": "param"}
83+
)
84+
85+
# URL will include: ?custom_param=value&another=param&X-Amz-Algorithm=...
86+
```
87+
88+
### With Explicit Session ID
89+
90+
```python
91+
client = AgentCoreRuntimeClient(region="us-west-2")
92+
93+
ws_url, headers = client.generate_ws_connection(
94+
runtime_arn="arn:aws:bedrock-agentcore:us-west-2:123456789012:runtime/my-runtime",
95+
session_id="my-custom-session-id"
96+
)
97+
```
98+
99+
## Error Handling
100+
101+
```python
102+
from bedrock_agentcore.runtime import AgentCoreRuntimeClient
103+
104+
client = AgentCoreRuntimeClient(region="us-west-2")
105+
106+
try:
107+
ws_url, headers = client.generate_ws_connection(
108+
runtime_arn="invalid-arn"
109+
)
110+
except ValueError as e:
111+
print(f"Invalid ARN format: {e}")
112+
except RuntimeError as e:
113+
print(f"AWS credentials error: {e}")
114+
```
115+
116+
## Custom Boto3 Session
117+
118+
You can provide your own boto3 session for custom credential handling:
119+
120+
```python
121+
import boto3
122+
from bedrock_agentcore.runtime import AgentCoreRuntimeClient
123+
124+
# Create a custom session with specific profile
125+
session = boto3.Session(profile_name="my-profile")
126+
127+
# Or with specific credentials
128+
session = boto3.Session(
129+
aws_access_key_id="YOUR_ACCESS_KEY",
130+
aws_secret_access_key="YOUR_SECRET_KEY",
131+
aws_session_token="YOUR_SESSION_TOKEN"
132+
)
133+
134+
# Initialize client with custom session
135+
client = AgentCoreRuntimeClient(region="us-west-2", session=session)
136+
137+
# Use the client normally
138+
ws_url, headers = client.generate_ws_connection(runtime_arn)
139+
```
140+
141+
## OAuth Authentication
142+
143+
For scenarios using OAuth bearer tokens instead of AWS credentials:
144+
145+
```python
146+
from bedrock_agentcore.runtime import AgentCoreRuntimeClient
147+
import websockets
148+
import asyncio
149+
150+
async def main():
151+
# Initialize client
152+
client = AgentCoreRuntimeClient(region="us-west-2")
153+
154+
# Your OAuth bearer token (e.g., from JWT authentication)
155+
bearer_token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
156+
157+
# Generate WebSocket connection with OAuth
158+
ws_url, headers = client.generate_ws_connection_oauth(
159+
runtime_arn="arn:aws:bedrock-agentcore:us-west-2:123456789012:runtime/my-runtime",
160+
bearer_token=bearer_token,
161+
endpoint_name="DEFAULT" # Optional
162+
)
163+
164+
# Connect using OAuth authentication
165+
async with websockets.connect(ws_url, extra_headers=headers) as ws:
166+
await ws.send('{"inputText": "Hello!"}')
167+
response = await ws.recv()
168+
print(f"Received: {response}")
169+
170+
asyncio.run(main())
171+
```
172+
173+
### OAuth with Custom Session ID
174+
175+
```python
176+
client = AgentCoreRuntimeClient(region="us-west-2")
177+
178+
ws_url, headers = client.generate_ws_connection_oauth(
179+
runtime_arn="arn:aws:bedrock-agentcore:us-west-2:123456789012:runtime/my-runtime",
180+
bearer_token="your-oauth-token",
181+
session_id="custom-oauth-session-id"
182+
)
183+
```
184+
185+
## Using Different WebSocket Libraries
186+
187+
### With websockets library
188+
189+
```python
190+
import websockets
191+
192+
ws_url, headers = client.generate_ws_connection(runtime_arn)
193+
async with websockets.connect(ws_url, extra_headers=headers) as ws:
194+
await ws.send(message)
195+
```
196+
197+
### With aiohttp library
198+
199+
```python
200+
import aiohttp
201+
202+
ws_url, headers = client.generate_ws_connection(runtime_arn)
203+
async with aiohttp.ClientSession() as session:
204+
async with session.ws_connect(ws_url, headers=headers) as ws:
205+
await ws.send_str(message)
206+
```

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ dependencies = [
3333
"starlette>=0.46.2",
3434
"typing-extensions>=4.13.2,<5.0.0",
3535
"uvicorn>=0.34.2",
36+
"pre-commit>=4.2.0",
3637
]
3738

3839
[project.scripts]
@@ -143,6 +144,7 @@ dev = [
143144
"pytest-asyncio>=0.24.0",
144145
"pytest-cov>=6.0.0",
145146
"ruff>=0.12.0",
147+
"websockets>=14.1",
146148
"wheel>=0.45.1",
147149
"strands-agents>=1.18.0",
148150
]

src/bedrock_agentcore/runtime/__init__.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,15 @@
66
- BedrockAgentCoreContext: Agent identity context
77
"""
88

9+
from .agent_core_runtime_client import AgentCoreRuntimeClient
910
from .app import BedrockAgentCoreApp
1011
from .context import BedrockAgentCoreContext, RequestContext
1112
from .models import PingStatus
1213

13-
__all__ = ["BedrockAgentCoreApp", "RequestContext", "BedrockAgentCoreContext", "PingStatus"]
14+
__all__ = [
15+
"AgentCoreRuntimeClient",
16+
"BedrockAgentCoreApp",
17+
"RequestContext",
18+
"BedrockAgentCoreContext",
19+
"PingStatus",
20+
]

0 commit comments

Comments
 (0)