-
Notifications
You must be signed in to change notification settings - Fork 1k
New serverless pattern - appsync-events-lambda-agentcore #2990
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
pjdavis-aws
wants to merge
32
commits into
aws-samples:main
Choose a base branch
from
pjdavis-aws:pjdavis-aws-feature-appsync-events-lambda-agentcore-cdk
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
32 commits
Select commit
Hold shift + click to select a range
a770794
feat: add AppSync Events + Lambda + AgentCore real-time chat pattern
0903b3e
feat: add tools to chat agent and integration tests for each tool
3d6acf3
chore: bump minimum dependency versions to latest across all requirem…
7e19294
refactor: move stack name from cdk.json context to mise environment v…
39efbd8
docs: enhance README with architecture diagram, console testing guide…
ff1b7f1
chore: remove unused code and add .DS_Store to gitignore
235b83b
chore: add cdk-nag AwsSolutions checks with granular suppressions
c11bf54
refactor: consolidate naming conventions and construct structure
a1b0be4
feat(init): install all requirements files and add Windows fallback
1c3e486
style: fix formatting and minor linting issues
aed1333
docs: trim low-value comments
80da8a7
fix(agent): require AWS_REGION instead of defaulting to eu-west-1
395a9cd
refactor(tests): derive unit test region from AWS_REGION env var
b771fdd
chore: fixed typo in folder name
f12e09b
docs: replace template placeholders and improve documentation
9d88f89
chore: add deploy, destroy tasks and Windows fallback to mise.toml
25107c1
chore: remove orphaned example-pattern.json from typo'd directory
7108696
feat: add cross-region inference profile resolution for multi-model s…
959c7af
chore: improve portability and add API key security note
2b0c28d
Update appsync-events-lambda-agentcore-cdk/README.md
pjdavis-aws d02e2bb
Update appsync-events-lambda-agentcore-cdk/README.md
pjdavis-aws 1208f26
Update appsync-events-lambda-agentcore-cdk/README.md
pjdavis-aws 0d10ff9
Update appsync-events-lambda-agentcore-cdk/README.md
pjdavis-aws ad681f6
Update appsync-events-lambda-agentcore-cdk/README.md
pjdavis-aws cb12090
Update appsync-events-lambda-agentcore-cdk/README.md
pjdavis-aws 44715d4
chore: set default Lambda LOG_LEVEL to INFO
0f2fe98
docs: add confirmation prompt instruction to cdk destroy step
667498f
fix(tests): extend session test timeout to 3 minutes per turn
b5897c1
fix(tests): replace non-deterministic http_request session test with…
0ec9c25
docs: clarify namespace is auto-prepended to Pub/Sub Editor paths, ad…
dd831e2
fix(docs): use HTML video tag for inline video playback on GitHub
9d88a05
docs: convert demo video to GIF for inline playback in README
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| *.swp | ||
| package-lock.json | ||
| __pycache__ | ||
| .pytest_cache | ||
| .venv | ||
| *.egg-info | ||
| .DS_Store | ||
|
|
||
| # CDK asset staging directory | ||
| .cdk.staging | ||
| cdk.out | ||
|
|
||
| # Dev tooling | ||
| .kiro | ||
| mise.local.toml |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| [format] | ||
| max-line-length=150 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,176 @@ | ||
| # AWS AppSync Events integration with AWS Lambda and Amazon Bedrock AgentCore | ||
|
|
||
| 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. | ||
|
|
||
| Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/appsync-events-lambda-agentcore-cdk | ||
|
|
||
| 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. | ||
|
|
||
| ## Requirements | ||
|
|
||
| * [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. | ||
| * [AWS CLI installed and configured](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) | ||
| * [Git installed](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) | ||
| * [Python 3.14](https://www.python.org/downloads/) with [pip](https://pip.pypa.io/en/stable/installation/) | ||
| * [Node.js 22](https://nodejs.org/en/download/) | ||
| * [AWS CDK v2](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html) (`npm install -g aws-cdk`) | ||
| * [Finch](https://runfinch.com/) or [Docker](https://docs.docker.com/get-docker/) (used for CDK bundling) | ||
|
|
||
| ## Deployment Instructions | ||
|
|
||
| 1. Create a new directory, navigate to that directory in a terminal and clone the GitHub repository: | ||
| ``` | ||
| git clone https://github.com/aws-samples/serverless-patterns | ||
| ``` | ||
| 1. Change directory to the pattern directory: | ||
| ``` | ||
| cd appsync-events-lambda-agentcore-cdk | ||
| ``` | ||
| 1. Create and activate a Python virtual environment: | ||
| ``` | ||
| python -m venv .venv | ||
| source .venv/bin/activate # On Windows: .venv\Scripts\activate | ||
| ``` | ||
| 1. Install Python dependencies: | ||
| ``` | ||
| pip install -r requirements.txt | ||
| ``` | ||
| 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): | ||
| ``` | ||
| export AWS_REGION=eu-west-1 # On Windows: set AWS_REGION=eu-west-1 | ||
| ``` | ||
| ``` | ||
| export AWS_REGION=eu-west-1 # On Windows: set AWS_REGION=eu-west-1 | ||
| ``` | ||
| 1. If you are using [Finch](https://runfinch.com/) instead of Docker, set the `CDK_DOCKER` environment variable: | ||
| ``` | ||
| export CDK_DOCKER=finch # On Windows: set CDK_DOCKER=finch | ||
| ``` | ||
| 1. Bootstrap CDK in your account/region (if not already done): | ||
| ``` | ||
| cdk bootstrap | ||
| ``` | ||
| 1. Deploy the stack: | ||
| ``` | ||
| cdk deploy | ||
| ``` | ||
| 1. Note the outputs from the CDK deployment process. These contain the AppSync Events HTTP endpoint, WebSocket endpoint, and API key needed for testing. | ||
|
|
||
| ## How it works | ||
|
|
||
|  | ||
|
|
||
| Figure 1 - Architecture | ||
|
|
||
| 1. The client publishes a message to the inbound channel (`/chat/{conversationId}`) via HTTP POST to AppSync Events. | ||
| 2. AppSync Events triggers the agent invoker Lambda via direct Lambda integration. | ||
| 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. | ||
| 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. | ||
| 5. The stream relay publishes each chunk back to the response channel on AppSync Events (`/responses/chat/{conversationId}`). | ||
| 6. The client receives agent response tokens in real time via the WebSocket subscription. | ||
|
|
||
| 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. | ||
|
|
||
| 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. | ||
|
|
||
| ## Testing | ||
|
|
||
| ### Automated tests | ||
|
|
||
| Install the test dependencies: | ||
|
|
||
| ```bash | ||
| pip install -r requirements-dev.txt | ||
| ``` | ||
|
|
||
| Run the tests: | ||
|
|
||
| ```bash | ||
| pytest tests/unit -v # unit tests (no deployed stack needed) | ||
| pytest tests/integration -v -s # integration tests with streaming output | ||
pjdavis-aws marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ``` | ||
|
|
||
| ### Using the AppSync Pub/Sub Editor | ||
|
|
||
| You can test the deployed service directly from the AWS Console using the AppSync Events built-in Pub/Sub Editor. No additional tooling required. | ||
|
|
||
| 1. Open the [AWS AppSync console](https://console.aws.amazon.com/appsync/) in the region you deployed to (e.g. `eu-west-1`). | ||
| 1. Select the Event API created by the stack (look for the API with "EventApi" in the name). | ||
| 1. Choose the **Pub/Sub Editor** tab. | ||
| 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. | ||
| 1. In the **Subscribe** panel, select `responses` from the namespace dropdown, then enter the path: | ||
| ``` | ||
| /chat/test-conversation-1 | ||
| ``` | ||
| > **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. | ||
| 1. Choose **Subscribe**. | ||
|
|
||
|  | ||
|
|
||
| Figure 2 - AppSync Pub/Sub Editor - Subscribe panel | ||
|
|
||
| 1. Scroll back to the top of the page to the **Publish** panel. Select `chat` from the namespace dropdown, then enter the path: | ||
| ``` | ||
| /test-conversation-1 | ||
| ``` | ||
|
|
||
| > **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. | ||
|
|
||
| Enter this JSON as the event payload: | ||
|
|
||
| ```json | ||
| [ | ||
| { | ||
| "message": "What is 347 multiplied by 29?", | ||
| "sessionId": "test-conversation-1" | ||
| } | ||
| ] | ||
| ``` | ||
| Choose **Publish**. When prompted, choose **WebSocket** as the publish method. | ||
|
|
||
|  | ||
|
|
||
| Figure 3 - AppSync Pub/Sub Editor - Publish panel | ||
|
|
||
| 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. | ||
pjdavis-aws marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
|  | ||
|
|
||
| Figure 4 - AppSync Pub/Sub Editor - Subscribe results | ||
|
|
||
| The following demo shows the full publish and subscribe flow: | ||
|
|
||
|  | ||
|
|
||
| Figure 5 - AppSync Pub/Sub Editor - Demo | ||
|
|
||
| A few things to note: | ||
|
|
||
| - The `sessionId` value ties messages to a conversation. Use the same `sessionId` across publishes to test multi-turn conversation with session persistence. | ||
| - The subscribe channel must be prefixed with `/responses` — the agent invoker publishes responses to `/responses/chat/{conversationId}` to avoid re-triggering itself. | ||
| - 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`). | ||
|
|
||
| ## Authentication | ||
|
|
||
| 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. | ||
|
|
||
| AppSync Events supports several authentication methods that are better suited for production: | ||
|
|
||
| - **Amazon Cognito user pools** — ideal for end-user authentication in web and mobile apps. | ||
| - **AWS IAM** — best for server-to-server or backend service communication. | ||
| - **OpenID Connect (OIDC)** — use with third-party identity providers. | ||
| - **Lambda authorizers** — for custom authorization logic. | ||
|
|
||
| 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. | ||
|
|
||
| ## Cleanup | ||
|
|
||
| 1. Delete the stack (answer `y` when prompted to confirm): | ||
| ``` | ||
| cdk destroy | ||
| ``` | ||
pjdavis-aws marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| ---- | ||
| Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
|
|
||
| SPDX-License-Identifier: MIT-0 | ||
20 changes: 20 additions & 0 deletions
20
appsync-events-lambda-agentcore-cdk/agents/chat/Dockerfile
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| FROM ghcr.io/astral-sh/uv:python3.14-bookworm-slim | ||
|
|
||
| WORKDIR /app | ||
|
|
||
| ENV UV_SYSTEM_PYTHON=1 UV_COMPILE_BYTECODE=1 | ||
|
|
||
| COPY chat/requirements.txt requirements.txt | ||
| RUN uv pip install --system --no-cache -r requirements.txt | ||
|
|
||
| ARG AWS_REGION | ||
| ENV AWS_REGION=${AWS_REGION} | ||
|
|
||
| RUN useradd -m -u 1000 bedrock_agentcore | ||
| USER bedrock_agentcore | ||
|
|
||
| EXPOSE 8080 | ||
|
|
||
| COPY chat/ /app | ||
|
|
||
| CMD ["opentelemetry-instrument", "python", "-m", "entrypoint"] |
96 changes: 96 additions & 0 deletions
96
appsync-events-lambda-agentcore-cdk/agents/chat/entrypoint.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| """Chat agent entrypoint for AgentCore runtime. | ||
|
|
||
| Pure streaming agent with S3-backed session persistence. | ||
| Yields response chunks via SSE. Has no knowledge of delivery | ||
| mechanism (AppSync, WebSocket, etc.). | ||
| """ | ||
|
|
||
| import os | ||
| import logging | ||
|
|
||
| from strands import Agent | ||
| from strands.models import BedrockModel | ||
| from strands.session.s3_session_manager import S3SessionManager | ||
| from strands_tools import http_request, calculator, current_time | ||
| from bedrock_agentcore.runtime import BedrockAgentCoreApp | ||
|
|
||
| logging.basicConfig(level=logging.INFO) | ||
| logger = logging.getLogger(__name__) | ||
|
|
||
| app = BedrockAgentCoreApp() | ||
|
|
||
| MODEL_ID = os.environ.get("BEDROCK_MODEL_ID") | ||
| if not MODEL_ID: | ||
| raise ValueError("BEDROCK_MODEL_ID environment variable is required") | ||
|
|
||
| REGION = os.environ.get("AWS_REGION") | ||
| if not REGION: | ||
| raise ValueError("AWS_REGION environment variable is required") | ||
| SESSION_BUCKET = os.environ.get("SESSION_BUCKET") | ||
|
|
||
| SYSTEM_PROMPT = """\ | ||
| You are a research assistant with access to the web, a calculator, and a clock. | ||
|
|
||
| You can: | ||
| - Fetch and summarise content from any public URL using http_request | ||
| - Perform mathematical calculations using calculator | ||
| - Check the current date and time in any timezone using current_time | ||
|
|
||
| When fetching web content, prefer converting HTML to markdown for readability | ||
| by setting convert_to_markdown=true. Always cite the URL you fetched. | ||
| Keep responses clear and concise. | ||
| """ | ||
|
|
||
|
|
||
| def _create_agent(session_id: str | None = None) -> Agent: | ||
| """Create a Strands agent with Bedrock model and optional session.""" | ||
| model = BedrockModel(model_id=MODEL_ID, region_name=REGION) | ||
|
|
||
| kwargs = { | ||
| "system_prompt": SYSTEM_PROMPT, | ||
| "model": model, | ||
| "tools": [http_request, calculator, current_time], | ||
| } | ||
|
|
||
| if session_id and SESSION_BUCKET: | ||
| kwargs["session_manager"] = S3SessionManager( | ||
| session_id=session_id, | ||
| bucket=SESSION_BUCKET, | ||
| region_name=REGION, | ||
| ) | ||
|
|
||
| return Agent(**kwargs) | ||
|
|
||
|
|
||
| @app.entrypoint | ||
| async def invoke(payload=None): | ||
| """Stream agent response as SSE events.""" | ||
| if not payload: | ||
| yield {"status": "error", "error": "payload is required"} | ||
| return | ||
|
|
||
| query = payload.get("content") or payload.get("prompt") | ||
| if not query: | ||
| yield {"status": "error", "error": "content or prompt is required"} | ||
| return | ||
|
|
||
| session_id = payload.get("sessionId") | ||
| logger.info("Processing query: %s (session: %s)", query[:100], session_id) | ||
|
|
||
| agent = _create_agent(session_id) | ||
|
|
||
| async for event in agent.stream_async(query): | ||
| if "data" in event: | ||
| yield {"data": event["data"]} | ||
| elif "result" in event: | ||
| result = event["result"] | ||
| yield { | ||
| "result": { | ||
| "stop_reason": str(result.stop_reason), | ||
| "message": result.message, | ||
| }, | ||
| } | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| app.run() |
4 changes: 4 additions & 0 deletions
4
appsync-events-lambda-agentcore-cdk/agents/chat/requirements.txt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| strands-agents>=1.29.0 | ||
| strands-agents-tools>=0.2.22 | ||
| bedrock-agentcore>=1.4.4 | ||
| aws-opentelemetry-distro>=0.15.0 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| #!/usr/bin/env python3 | ||
| """CDK app entrypoint for the AppSync Events + Lambda + AgentCore stack.""" | ||
| import os | ||
|
|
||
| import aws_cdk as cdk | ||
| from cdk_nag import AwsSolutionsChecks | ||
|
|
||
| from cdk.stack import ChatStack | ||
|
|
||
|
|
||
| app = cdk.App() | ||
|
|
||
| stack_name = app.node.try_get_context("stack_name") or "AppsyncLambdaAgentcore" | ||
|
|
||
| region = os.environ.get("AWS_REGION") | ||
| if not region: | ||
| raise EnvironmentError("AWS_REGION environment variable must be set") | ||
|
|
||
| ChatStack( | ||
| app, | ||
| stack_name, | ||
| env=cdk.Environment(region=region), | ||
| ) | ||
|
|
||
| cdk.Aspects.of(app).add(AwsSolutionsChecks(verbose=True)) | ||
|
|
||
| app.synth() |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.