Skip to content

Commit 9ba8163

Browse files
authored
Merge pull request #2990 from pjdavis-aws/pjdavis-aws-feature-appsync-events-lambda-agentcore-cdk
New serverless pattern - appsync-events-lambda-agentcore
2 parents 29b2c52 + ed8be2c commit 9ba8163

43 files changed

Lines changed: 2929 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
*.swp
2+
package-lock.json
3+
__pycache__
4+
.pytest_cache
5+
.venv
6+
*.egg-info
7+
.DS_Store
8+
9+
# CDK asset staging directory
10+
.cdk.staging
11+
cdk.out
12+
13+
# Dev tooling
14+
.kiro
15+
mise.local.toml
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[format]
2+
max-line-length=150
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
# AWS AppSync Events integration with AWS Lambda and Amazon Bedrock AgentCore
2+
3+
This pattern deploys a real-time streaming chat service using AWS AppSync Events with AWS Lambda to invoke a Strands agent running on Amazon Bedrock AgentCore Runtime.
4+
5+
Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/appsync-events-lambda-agentcore-cdk
6+
7+
Important: this application uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details. You are responsible for any AWS costs incurred. No warranty is implied in this example.
8+
9+
## Requirements
10+
11+
* [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and log in. The IAM user that you use must have sufficient permissions to make necessary AWS service calls and manage AWS resources.
12+
* [AWS CLI installed and configured](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html)
13+
* [Git installed](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
14+
* [Python 3.14](https://www.python.org/downloads/) with [pip](https://pip.pypa.io/en/stable/installation/)
15+
* [Node.js 22](https://nodejs.org/en/download/)
16+
* [AWS CDK v2](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html) (`npm install -g aws-cdk`)
17+
* [Finch](https://runfinch.com/) or [Docker](https://docs.docker.com/get-docker/) (used for CDK bundling)
18+
19+
## Deployment Instructions
20+
21+
1. Create a new directory, navigate to that directory in a terminal and clone the GitHub repository:
22+
```
23+
git clone https://github.com/aws-samples/serverless-patterns
24+
```
25+
1. Change directory to the pattern directory:
26+
```
27+
cd appsync-events-lambda-agentcore-cdk
28+
```
29+
1. Create and activate a Python virtual environment:
30+
```
31+
python -m venv .venv
32+
source .venv/bin/activate # On Windows: .venv\Scripts\activate
33+
```
34+
1. Install Python dependencies:
35+
```
36+
pip install -r requirements.txt
37+
```
38+
1. Set your target AWS region (must be a region where [Bedrock AgentCore](https://docs.aws.amazon.com/general/latest/gr/bedrock_agentcore.html) is available):
39+
```
40+
export AWS_REGION=eu-west-1 # On Windows: set AWS_REGION=eu-west-1
41+
```
42+
```
43+
export AWS_REGION=eu-west-1 # On Windows: set AWS_REGION=eu-west-1
44+
```
45+
1. If you are using [Finch](https://runfinch.com/) instead of Docker, set the `CDK_DOCKER` environment variable:
46+
```
47+
export CDK_DOCKER=finch # On Windows: set CDK_DOCKER=finch
48+
```
49+
1. Bootstrap CDK in your account/region (if not already done):
50+
```
51+
cdk bootstrap
52+
```
53+
1. Deploy the stack:
54+
```
55+
cdk deploy
56+
```
57+
1. Note the outputs from the CDK deployment process. These contain the AppSync Events HTTP endpoint, WebSocket endpoint, and API key needed for testing.
58+
59+
## How it works
60+
61+
![Architecture diagram](images/architecture.png)
62+
63+
Figure 1 - Architecture
64+
65+
1. The client publishes a message to the inbound channel (`/chat/{conversationId}`) via HTTP POST to AppSync Events.
66+
2. AppSync Events triggers the agent invoker Lambda function via direct Lambda integration.
67+
3. The agent invoker validates the payload, invokes the stream relay Lambda asynchronously, and returns immediately. This two-Lambda split is necessary because AppSync invokes the handler synchronously — a long-running stream would block the response.
68+
4. The stream relay calls `invoke_agent_runtime` on the Bedrock AgentCore Runtime, which hosts a Strands agent container, and consumes the Server-Sent Events (SSE) stream.
69+
5. The stream relay publishes each chunk back to the response channel on AppSync Events (`/responses/chat/{conversationId}`).
70+
6. The client receives agent response tokens in real time via the WebSocket subscription.
71+
72+
The client subscribes to the response channel before publishing. Separate channel namespaces (`chat` for inbound, `responses` for outbound) ensure the stream relay's publishes do not re-trigger the agent invoker.
73+
74+
The agent is a Strands-based research assistant with access to `http_request`, `calculator`, and `current_time` tools, backed by S3 session persistence for multi-turn conversations.
75+
76+
## Testing
77+
78+
### Automated tests
79+
80+
Install the test dependencies:
81+
82+
```bash
83+
pip install -r requirements-dev.txt
84+
```
85+
86+
Run the tests:
87+
88+
```bash
89+
pytest tests/unit -v # unit tests (no deployed stack needed)
90+
pytest tests/integration -v -s # integration tests with streaming output
91+
```
92+
93+
### Using the AppSync Pub/Sub Editor
94+
95+
You can test the deployed service directly from the AWS Console using the AppSync Events built-in Pub/Sub Editor. No additional tooling required.
96+
97+
1. Open the [AWS AppSync console](https://console.aws.amazon.com/appsync/) in the region you deployed to (e.g. `eu-west-1`).
98+
1. Select the Event API created by the stack (look for the API with "EventApi" in the name).
99+
1. Choose the **Pub/Sub Editor** tab.
100+
1. Scroll to the bottom of the page. The API key is pre-populated in the authorization token field. Choose **Connect** to establish a WebSocket connection.
101+
1. In the **Subscribe** panel, select `responses` from the namespace dropdown, then enter the path:
102+
```
103+
/chat/test-conversation-1
104+
```
105+
> **Note:** The namespace is automatically prepended to the path. Since the namespace is `responses`, the full channel becomes `/responses/chat/test-conversation-1`. Do not include the namespace in the path field.
106+
1. Choose **Subscribe**.
107+
108+
![AppSync Pub/Sub Editor — Subscribe panel](images/appsync-pubsub-subscribe.jpg)
109+
110+
Figure 2 - AppSync Pub/Sub Editor - Subscribe panel
111+
112+
1. Scroll back to the top of the page to the **Publish** panel. Select `chat` from the namespace dropdown, then enter the path:
113+
```
114+
/test-conversation-1
115+
```
116+
117+
> **Note:** The namespace `chat` is prepended automatically, so the full channel is `/chat/test-conversation-1`. Do not enter `/chat/test-conversation-1` as the path — that would produce `/chat/chat/test-conversation-1` and messages will not reach the subscriber.
118+
119+
Enter this JSON as the event payload:
120+
121+
```json
122+
[
123+
{
124+
"message": "What is 347 multiplied by 29?",
125+
"sessionId": "test-conversation-1"
126+
}
127+
]
128+
```
129+
Choose **Publish**. When prompted, choose **WebSocket** as the publish method.
130+
131+
![AppSync Pub/Sub Editor — Publish panel](images/appsync-pubsub-publish.jpg)
132+
133+
Figure 3 - AppSync Pub/Sub Editor - Publish panel
134+
135+
1. Scroll back down to the bottom of the page to watch the subscription panel — you should see streaming chunk events arrive in real time, followed by a final completion event containing the full response.
136+
137+
![AppSync Pub/Sub Editor — Subscribe results](images/appsync-pubsub-subscribe-result.jpg)
138+
139+
Figure 4 - AppSync Pub/Sub Editor - Subscribe results
140+
141+
The following demo shows the full publish and subscribe flow:
142+
143+
![Pub/Sub Editor demo](images/pubsub-demo.gif)
144+
145+
Figure 5 - AppSync Pub/Sub Editor - Demo
146+
147+
A few things to note:
148+
149+
- The `sessionId` value ties messages to a conversation. Use the same `sessionId` across publishes to test multi-turn conversation with session persistence.
150+
- The subscribe channel must be prefixed with `/responses` — the agent invoker publishes responses to `/responses/chat/{conversationId}` to avoid re-triggering itself.
151+
- You can try different prompts to exercise the agent's tools: ask it to fetch a URL (`http_request`), do arithmetic (`calculator`), or tell you the current time (`current_time`).
152+
153+
## Authentication
154+
155+
This example uses an API key for authentication to keep things simple. API keys are suitable for development and testing but are not recommended for production workloads.
156+
157+
AppSync Events supports several authentication methods that are better suited for production:
158+
159+
- **Amazon Cognito user pools** — ideal for end-user authentication in web and mobile apps.
160+
- **AWS IAM** — best for server-to-server or backend service communication.
161+
- **OpenID Connect (OIDC)** — use with third-party identity providers.
162+
- **Lambda authorizers** — for custom authorization logic.
163+
164+
You can configure multiple authorization modes on a single API and apply different modes per channel namespace. See the [AppSync Events authorization and authentication](https://docs.aws.amazon.com/appsync/latest/eventapi/configure-event-api-auth.html) documentation for details.
165+
166+
## Cleanup
167+
168+
1. Delete the stack (answer `y` when prompted to confirm):
169+
```
170+
cdk destroy
171+
```
172+
173+
----
174+
Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
175+
176+
SPDX-License-Identifier: MIT-0
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
FROM ghcr.io/astral-sh/uv:python3.14-bookworm-slim
2+
3+
WORKDIR /app
4+
5+
ENV UV_SYSTEM_PYTHON=1 UV_COMPILE_BYTECODE=1
6+
7+
COPY chat/requirements.txt requirements.txt
8+
RUN uv pip install --system --no-cache -r requirements.txt
9+
10+
ARG AWS_REGION
11+
ENV AWS_REGION=${AWS_REGION}
12+
13+
RUN useradd -m -u 1000 bedrock_agentcore
14+
USER bedrock_agentcore
15+
16+
EXPOSE 8080
17+
18+
COPY chat/ /app
19+
20+
CMD ["opentelemetry-instrument", "python", "-m", "entrypoint"]
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
"""Chat agent entrypoint for AgentCore runtime.
2+
3+
Pure streaming agent with S3-backed session persistence.
4+
Yields response chunks via SSE. Has no knowledge of delivery
5+
mechanism (AppSync, WebSocket, etc.).
6+
"""
7+
8+
import os
9+
import logging
10+
11+
from strands import Agent
12+
from strands.models import BedrockModel
13+
from strands.session.s3_session_manager import S3SessionManager
14+
from strands_tools import http_request, calculator, current_time
15+
from bedrock_agentcore.runtime import BedrockAgentCoreApp
16+
17+
logging.basicConfig(level=logging.INFO)
18+
logger = logging.getLogger(__name__)
19+
20+
app = BedrockAgentCoreApp()
21+
22+
MODEL_ID = os.environ.get("BEDROCK_MODEL_ID")
23+
if not MODEL_ID:
24+
raise ValueError("BEDROCK_MODEL_ID environment variable is required")
25+
26+
REGION = os.environ.get("AWS_REGION")
27+
if not REGION:
28+
raise ValueError("AWS_REGION environment variable is required")
29+
SESSION_BUCKET = os.environ.get("SESSION_BUCKET")
30+
31+
SYSTEM_PROMPT = """\
32+
You are a research assistant with access to the web, a calculator, and a clock.
33+
34+
You can:
35+
- Fetch and summarise content from any public URL using http_request
36+
- Perform mathematical calculations using calculator
37+
- Check the current date and time in any timezone using current_time
38+
39+
When fetching web content, prefer converting HTML to markdown for readability
40+
by setting convert_to_markdown=true. Always cite the URL you fetched.
41+
Keep responses clear and concise.
42+
"""
43+
44+
45+
def _create_agent(session_id: str | None = None) -> Agent:
46+
"""Create a Strands agent with Bedrock model and optional session."""
47+
model = BedrockModel(model_id=MODEL_ID, region_name=REGION)
48+
49+
kwargs = {
50+
"system_prompt": SYSTEM_PROMPT,
51+
"model": model,
52+
"tools": [http_request, calculator, current_time],
53+
}
54+
55+
if session_id and SESSION_BUCKET:
56+
kwargs["session_manager"] = S3SessionManager(
57+
session_id=session_id,
58+
bucket=SESSION_BUCKET,
59+
region_name=REGION,
60+
)
61+
62+
return Agent(**kwargs)
63+
64+
65+
@app.entrypoint
66+
async def invoke(payload=None):
67+
"""Stream agent response as SSE events."""
68+
if not payload:
69+
yield {"status": "error", "error": "payload is required"}
70+
return
71+
72+
query = payload.get("content") or payload.get("prompt")
73+
if not query:
74+
yield {"status": "error", "error": "content or prompt is required"}
75+
return
76+
77+
session_id = payload.get("sessionId")
78+
logger.info("Processing query: %s (session: %s)", query[:100], session_id)
79+
80+
agent = _create_agent(session_id)
81+
82+
async for event in agent.stream_async(query):
83+
if "data" in event:
84+
yield {"data": event["data"]}
85+
elif "result" in event:
86+
result = event["result"]
87+
yield {
88+
"result": {
89+
"stop_reason": str(result.stop_reason),
90+
"message": result.message,
91+
},
92+
}
93+
94+
95+
if __name__ == "__main__":
96+
app.run()
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
strands-agents>=1.29.0
2+
strands-agents-tools>=0.2.22
3+
bedrock-agentcore>=1.4.4
4+
aws-opentelemetry-distro>=0.15.0
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#!/usr/bin/env python3
2+
"""CDK app entrypoint for the AppSync Events + Lambda + AgentCore stack."""
3+
import os
4+
5+
import aws_cdk as cdk
6+
from cdk_nag import AwsSolutionsChecks
7+
8+
from cdk.stack import ChatStack
9+
10+
11+
app = cdk.App()
12+
13+
stack_name = app.node.try_get_context("stack_name") or "AppsyncLambdaAgentcore"
14+
15+
region = os.environ.get("AWS_REGION")
16+
if not region:
17+
raise EnvironmentError("AWS_REGION environment variable must be set")
18+
19+
ChatStack(
20+
app,
21+
stack_name,
22+
env=cdk.Environment(region=region),
23+
)
24+
25+
cdk.Aspects.of(app).add(AwsSolutionsChecks(verbose=True))
26+
27+
app.synth()

0 commit comments

Comments
 (0)