Skip to content

Commit b4718bc

Browse files
authoredJan 26, 2024
Add Bedrock Agent example (#357)
* add Bedrock Agent example * Fix bedrock-agent-fastapi example. * Update Dockerfile - add `AWS_LWA_READINESS_CHECK_PROTOCOL` env * Update template.yaml - change `MemorySize` 256 to 1024 - fix Policies define `arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess` to `AmazonS3ReadOnlyAccess` - add BedrockAgentPermission - add Output * Update README.md - update Dockerfile - add "Generate OpenAPI schema", "Create an agent.", "Test locally" section. - remove "Run the docker locally" section.
1 parent 79ffab9 commit b4718bc

11 files changed

+645
-0
lines changed
 
+244
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
2+
# Created by https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode
3+
4+
### Linux ###
5+
*~
6+
7+
# temporary files which can be created if a process still has a handle open of a deleted file
8+
.fuse_hidden*
9+
10+
# KDE directory preferences
11+
.directory
12+
13+
# Linux trash folder which might appear on any partition or disk
14+
.Trash-*
15+
16+
# .nfs files are created when an open file is removed but is still being accessed
17+
.nfs*
18+
19+
### OSX ###
20+
*.DS_Store
21+
.AppleDouble
22+
.LSOverride
23+
24+
# Icon must end with two \r
25+
Icon
26+
27+
# Thumbnails
28+
._*
29+
30+
# Files that might appear in the root of a volume
31+
.DocumentRevisions-V100
32+
.fseventsd
33+
.Spotlight-V100
34+
.TemporaryItems
35+
.Trashes
36+
.VolumeIcon.icns
37+
.com.apple.timemachine.donotpresent
38+
39+
# Directories potentially created on remote AFP share
40+
.AppleDB
41+
.AppleDesktop
42+
Network Trash Folder
43+
Temporary Items
44+
.apdisk
45+
46+
### PyCharm ###
47+
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
48+
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
49+
50+
# User-specific stuff:
51+
.idea/**/workspace.xml
52+
.idea/**/tasks.xml
53+
.idea/dictionaries
54+
55+
# Sensitive or high-churn files:
56+
.idea/**/dataSources/
57+
.idea/**/dataSources.ids
58+
.idea/**/dataSources.xml
59+
.idea/**/dataSources.local.xml
60+
.idea/**/sqlDataSources.xml
61+
.idea/**/dynamic.xml
62+
.idea/**/uiDesigner.xml
63+
64+
# Gradle:
65+
.idea/**/gradle.xml
66+
.idea/**/libraries
67+
68+
# CMake
69+
cmake-build-debug/
70+
71+
# Mongo Explorer plugin:
72+
.idea/**/mongoSettings.xml
73+
74+
## File-based project format:
75+
*.iws
76+
77+
## Plugin-specific files:
78+
79+
# IntelliJ
80+
/out/
81+
82+
# mpeltonen/sbt-idea plugin
83+
.idea_modules/
84+
85+
# JIRA plugin
86+
atlassian-ide-plugin.xml
87+
88+
# Cursive Clojure plugin
89+
.idea/replstate.xml
90+
91+
# Ruby plugin and RubyMine
92+
/.rakeTasks
93+
94+
# Crashlytics plugin (for Android Studio and IntelliJ)
95+
com_crashlytics_export_strings.xml
96+
crashlytics.properties
97+
crashlytics-build.properties
98+
fabric.properties
99+
100+
### PyCharm Patch ###
101+
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
102+
103+
# *.iml
104+
# modules.xml
105+
# .idea/misc.xml
106+
# *.ipr
107+
108+
# Sonarlint plugin
109+
.idea/sonarlint
110+
111+
### Python ###
112+
# Byte-compiled / optimized / DLL files
113+
__pycache__/
114+
*.py[cod]
115+
*$py.class
116+
117+
# C extensions
118+
*.so
119+
120+
# Distribution / packaging
121+
.Python
122+
build/
123+
develop-eggs/
124+
dist/
125+
downloads/
126+
eggs/
127+
.eggs/
128+
lib/
129+
lib64/
130+
parts/
131+
sdist/
132+
var/
133+
wheels/
134+
*.egg-info/
135+
.installed.cfg
136+
*.egg
137+
138+
# PyInstaller
139+
# Usually these files are written by a python script from a template
140+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
141+
*.manifest
142+
*.spec
143+
144+
# Installer logs
145+
pip-log.txt
146+
pip-delete-this-directory.txt
147+
148+
# Unit test / coverage reports
149+
htmlcov/
150+
.tox/
151+
.coverage
152+
.coverage.*
153+
.cache
154+
.pytest_cache/
155+
nosetests.xml
156+
coverage.xml
157+
*.cover
158+
.hypothesis/
159+
160+
# Translations
161+
*.mo
162+
*.pot
163+
164+
# Flask stuff:
165+
instance/
166+
.webassets-cache
167+
168+
# Scrapy stuff:
169+
.scrapy
170+
171+
# Sphinx documentation
172+
docs/_build/
173+
174+
# PyBuilder
175+
target/
176+
177+
# Jupyter Notebook
178+
.ipynb_checkpoints
179+
180+
# pyenv
181+
.python-version
182+
183+
# celery beat schedule file
184+
celerybeat-schedule.*
185+
186+
# SageMath parsed files
187+
*.sage.py
188+
189+
# Environments
190+
.env
191+
.venv
192+
env/
193+
venv/
194+
ENV/
195+
env.bak/
196+
venv.bak/
197+
198+
# Spyder project settings
199+
.spyderproject
200+
.spyproject
201+
202+
# Rope project settings
203+
.ropeproject
204+
205+
# mkdocs documentation
206+
/site
207+
208+
# mypy
209+
.mypy_cache/
210+
211+
### VisualStudioCode ###
212+
.vscode/*
213+
!.vscode/settings.json
214+
!.vscode/tasks.json
215+
!.vscode/launch.json
216+
!.vscode/extensions.json
217+
.history
218+
219+
### Windows ###
220+
# Windows thumbnail cache files
221+
Thumbs.db
222+
ehthumbs.db
223+
ehthumbs_vista.db
224+
225+
# Folder config file
226+
Desktop.ini
227+
228+
# Recycle Bin used on file shares
229+
$RECYCLE.BIN/
230+
231+
# Windows Installer files
232+
*.cab
233+
*.msi
234+
*.msm
235+
*.msp
236+
237+
# Windows shortcuts
238+
*.lnk
239+
240+
# Build folder
241+
242+
*/build/*
243+
244+
# End of https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode
+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Agents for Amazon Bedrock with FastAPI example
2+
3+
This project demonstrates the integration of "Agents for Amazon Bedrock" with a FastAPI application on AWS Lambda. It showcases how to effectively manage and process Agents for Amazon Bedrock within an serverless FastAPI application environment.
4+
5+
The application can be deployed in an AWS account using the [Serverless Application Model](https://github.com/awslabs/serverless-application-model). The `template.yaml` file in the root folder contains the application definition.
6+
7+
The top level folder is a typical AWS SAM project. The `app` directory is an FastAPI application with a [Dockerfile](app/Dockerfile).
8+
9+
```dockerfile
10+
FROM public.ecr.aws/docker/library/python:3.12.0-slim
11+
COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.8.1 /lambda-adapter /opt/extensions/lambda-adapter
12+
ENV PORT=8000 AWS_LWA_READINESS_CHECK_PROTOCOL=tcp
13+
WORKDIR /var/task
14+
COPY requirements.txt ./
15+
RUN python -m pip install -r requirements.txt
16+
COPY *.py ./
17+
CMD exec uvicorn --port=$PORT main:app
18+
```
19+
20+
Line 2 copies lambda adapter binary into /opt/extenions. This is the only change to run the FastAPI application on Lambda.
21+
22+
```dockerfile
23+
COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.8.1 /lambda-adapter /opt/extensions/lambda-adapter
24+
```
25+
26+
## Pre-requisites
27+
28+
The following tools should be installed and configured.
29+
* [AWS CLI](https://aws.amazon.com/cli/)
30+
* [SAM CLI](https://github.com/awslabs/aws-sam-cli)
31+
* [Python](https://www.python.org/)
32+
* [Docker](https://www.docker.com/products/docker-desktop)
33+
34+
## Deploy to Lambda
35+
Navigate to the sample's folder and use the SAM CLI to build a container image
36+
```shell
37+
$ sam build
38+
```
39+
40+
This command compiles the application and prepares a deployment package in the `.aws-sam` sub-directory.
41+
42+
To deploy the application in your AWS account, you can use the SAM CLI's guided deployment process and follow the instructions on the screen
43+
44+
```shell
45+
$ sam deploy --guided
46+
```
47+
48+
## Generate OpenAPI schema
49+
50+
Before you create your agent, you should set up action groups that you want to add to your agent. When you create an action group, you must define the APIs that the agent can invoke with an OpenAPI schema in JSON or YAML format. (see [reference](https://docs.aws.amazon.com/bedrock/latest/userguide/agents-api-schema.html))
51+
52+
FastAPI can generate OpenAPI schema.
53+
54+
(in app directory)
55+
56+
```shell
57+
python -c "import main;import json; print(json.dumps(main.app.openapi()))" > openapi.json
58+
```
59+
60+
61+
## Create an agent.
62+
63+
see [reference](https://docs.aws.amazon.com/bedrock/latest/userguide/agents-create.html)
64+
65+
## Test locally
66+
67+
Sample event exists in events directory. You can test locally bellow command.
68+
69+
```shell
70+
sam local invoke --event events/s3_bucket_count.json
71+
```
72+
73+
74+
## Test
75+
76+
Test your agent on Management Console. (see [reference](https://docs.aws.amazon.com/bedrock/latest/userguide/agents-test.html))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
FROM public.ecr.aws/docker/library/python:3.12.0-slim
2+
COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.8.1 /lambda-adapter /opt/extensions/lambda-adapter
3+
ENV PORT=8000 AWS_LWA_READINESS_CHECK_PROTOCOL=tcp
4+
WORKDIR /var/task
5+
COPY requirements.txt ./
6+
RUN python -m pip install -r requirements.txt
7+
COPY *.py ./
8+
CMD exec uvicorn --port=$PORT main:app

‎examples/bedrock-agent-fastapi/app/__init__.py

Whitespace-only changes.
+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import datetime
2+
3+
import boto3
4+
from middleware import BedrockAgentMiddleware
5+
from fastapi import FastAPI, Query
6+
from pydantic import BaseModel, Field
7+
8+
app = FastAPI(
9+
description="This agent allows you to query the S3 information in your AWS account.",
10+
)
11+
app.openapi_version = "3.0.2"
12+
app.add_middleware(BedrockAgentMiddleware)
13+
14+
s3 = boto3.resource("s3")
15+
16+
17+
class S3BucketCountResponse(BaseModel):
18+
count: int = Field(description="the number of S3 buckets")
19+
20+
21+
@app.get("/s3_bucket_count")
22+
async def get_s3_bucket_count() -> S3BucketCountResponse:
23+
"""
24+
This method returns the number of S3 buckets in your AWS account.
25+
26+
Return:
27+
S3BucketCountResponse: A json object containing the number of S3 buckets in your AWS account.
28+
"""
29+
30+
count = len(list(s3.buckets.all()))
31+
32+
return S3BucketCountResponse(count=count)
33+
34+
35+
class S3ObjectCountResponse(BaseModel):
36+
count: int = Field(description="the number of S3 objects")
37+
38+
39+
@app.get("/s3_object_count")
40+
async def get_s3_object_count(
41+
bucket_name: str = Query(description="Bucket name"),
42+
) -> S3ObjectCountResponse:
43+
"""
44+
This method returns the number of S3 objects in your specified bucket.
45+
46+
Return:
47+
S3ObjectCountResponse: A json object containing the number of S3 objects in your specified bucket.
48+
"""
49+
50+
count = len(list(s3.Bucket(bucket_name).objects.all()))
51+
return S3ObjectCountResponse(count=count)
52+
53+
54+
class S3GetObjectRequest(BaseModel):
55+
bucket_name: str = Field(description="Bucket name")
56+
object_key: str = Field(description="Object key")
57+
58+
59+
class S3GetObjectResponse(BaseModel):
60+
last_modified: datetime.datetime = Field(description="the last modified date")
61+
62+
63+
@app.post("/s3_object")
64+
async def get_s3_object(request: S3GetObjectRequest):
65+
"""
66+
This method returns the last modified date of S3 object.
67+
68+
Return:
69+
S3GetObjectResponse: A json object containing the last modified date of S3 objects.
70+
"""
71+
72+
object = s3.Object(request.bucket_name, request.object_key)
73+
last_modified = object.get()["LastModified"]
74+
return S3GetObjectResponse(last_modified=last_modified)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import json
2+
from urllib.parse import urlencode
3+
4+
from fastapi import Response
5+
from fastapi.responses import JSONResponse
6+
from starlette.middleware.base import BaseHTTPMiddleware
7+
8+
9+
class BedrockAgentMiddleware(BaseHTTPMiddleware):
10+
async def dispatch(self, request, call_next):
11+
# pass through any non-events requests
12+
if request.url.path != "/events":
13+
return await call_next(request)
14+
15+
# convert the request body to json object
16+
req_body = await request.body()
17+
req_body = json.loads(req_body)
18+
19+
request.scope["path"] = req_body["apiPath"]
20+
request.scope["method"] = req_body["httpMethod"]
21+
22+
# query params
23+
params = {}
24+
parameters = req_body.get("parameters", [])
25+
for item in parameters:
26+
params[item["name"]] = item["value"]
27+
request.scope["query_string"] = urlencode(params).encode()
28+
29+
# body
30+
content = req_body.get("requestBody", {}).get("content", None)
31+
if content:
32+
for key in content.keys():
33+
content_type = key
34+
break
35+
36+
data = {}
37+
content_val = content.get(content_type, {})
38+
for item in content_val.get("properties", []):
39+
data[item["name"]] = item["value"]
40+
request._body = json.dumps(data).encode()
41+
42+
# Pass the request to be processed by the rest of the application
43+
response = await call_next(request)
44+
45+
if isinstance(response, Response) and hasattr(response, "body"):
46+
res_body = response.body
47+
elif hasattr(response, "body_iterator"):
48+
res_body = b""
49+
async for chunk in response.body_iterator:
50+
res_body += chunk
51+
response.body_iterator = self.recreate_iterator(res_body)
52+
else:
53+
res_body = None
54+
# Now you have the body, you can do whatever you want with it
55+
print(res_body)
56+
57+
res_status_code = response.status_code
58+
res_content_type = response.headers["content-type"]
59+
60+
response = JSONResponse(
61+
content={
62+
"messageVersion": "1.0",
63+
"response": {
64+
"actionGroup": req_body["actionGroup"],
65+
"apiPath": req_body["apiPath"],
66+
"httpMethod": req_body["httpMethod"],
67+
"httpStatusCode": res_status_code,
68+
"responseBody": {
69+
res_content_type: {"body": res_body.decode("utf-8")}
70+
},
71+
"sessionAttributes": req_body["sessionAttributes"],
72+
"promptSessionAttributes": req_body["promptSessionAttributes"],
73+
},
74+
}
75+
)
76+
77+
print(response)
78+
return response
79+
80+
@staticmethod
81+
async def recreate_iterator(body):
82+
yield body
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
boto3
2+
3+
fastapi
4+
uvicorn[standard]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"messageVersion": "1.0",
3+
"agent": {
4+
"name": "AgentName",
5+
"id": "AgentID",
6+
"alias": "AgentAlias",
7+
"version": "AgentVersion"
8+
},
9+
"inputText": "InputText",
10+
"sessionId": "SessionID",
11+
"actionGroup": "ActionGroup",
12+
"apiPath": "/s3_bucket_count",
13+
"httpMethod": "GET",
14+
"parameters": [
15+
{
16+
"name": "param1",
17+
"type": "string",
18+
"value": "value1"
19+
}
20+
],
21+
"requestBody": {
22+
"content": {
23+
"application/json": {
24+
"properties": [
25+
{
26+
"name": "prop1",
27+
"type": "string",
28+
"value": "value1"
29+
}
30+
]
31+
}
32+
}
33+
},
34+
"sessionAttributes": {
35+
"attr1": "value1"
36+
},
37+
"promptSessionAttributes": {
38+
"promptAttr1": "value1"
39+
}
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"messageVersion": "1.0",
3+
"agent": {
4+
"name": "AgentName",
5+
"id": "AgentID",
6+
"alias": "AgentAlias",
7+
"version": "AgentVersion"
8+
},
9+
"inputText": "InputText",
10+
"sessionId": "SessionID",
11+
"actionGroup": "ActionGroup",
12+
"apiPath": "/s3_object",
13+
"httpMethod": "POST",
14+
"parameters": [
15+
],
16+
"requestBody": {
17+
"content": {
18+
"application/json": {
19+
"properties": [
20+
{
21+
"name": "bucket_name",
22+
"type": "string",
23+
"value": "bucket_001"
24+
},
25+
{
26+
"name": "object_key",
27+
"type": "string",
28+
"value": "object.txt"
29+
}
30+
]
31+
}
32+
}
33+
},
34+
"sessionAttributes": {
35+
"attr1": "value1"
36+
},
37+
"promptSessionAttributes": {
38+
"promptAttr1": "value1"
39+
}
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"messageVersion": "1.0",
3+
"agent": {
4+
"name": "AgentName",
5+
"id": "AgentID",
6+
"alias": "AgentAlias",
7+
"version": "AgentVersion"
8+
},
9+
"inputText": "InputText",
10+
"sessionId": "SessionID",
11+
"actionGroup": "ActionGroup",
12+
"apiPath": "/s3_object_count",
13+
"httpMethod": "GET",
14+
"parameters": [
15+
{
16+
"name": "bucket_name",
17+
"type": "string",
18+
"value": "bucket_001"
19+
}
20+
],
21+
"requestBody": {
22+
"content": {
23+
"application/json": {
24+
"properties": [
25+
{
26+
"name": "prop1",
27+
"type": "string",
28+
"value": "value1"
29+
}
30+
]
31+
}
32+
}
33+
},
34+
"sessionAttributes": {
35+
"attr1": "value1"
36+
},
37+
"promptSessionAttributes": {
38+
"promptAttr1": "value1"
39+
}
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
AWSTemplateFormatVersion: '2010-09-09'
2+
Transform: AWS::Serverless-2016-10-31
3+
Description: >
4+
bedrock-agent-fastapi
5+
6+
FastAPI app that work with Agents for Amazon Bedrock
7+
8+
# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
9+
Globals:
10+
Function:
11+
Timeout: 60
12+
13+
Resources:
14+
BedrockAgentFastAPIFunction:
15+
Type: AWS::Serverless::Function
16+
Properties:
17+
PackageType: Image
18+
MemorySize: 1024
19+
Policies: AmazonS3ReadOnlyAccess
20+
Metadata:
21+
Dockerfile: Dockerfile
22+
DockerContext: ./app
23+
DockerTag: python3.12-v1
24+
25+
BedrockAgentPermission:
26+
Type: AWS::Lambda::Permission
27+
Properties:
28+
FunctionName: !Ref BedrockAgentFastAPIFunction
29+
Action: lambda:InvokeFunction
30+
Principal: bedrock.amazonaws.com
31+
SourceAccount: !Ref 'AWS::AccountId'
32+
SourceArn: !Sub arn:aws:bedrock:${AWS::Region}:${AWS::AccountId}:agent/*
33+
34+
Outputs:
35+
BedrockAgentFastAPIFunction:
36+
Description: "BedrockAgentFastAPIFunction Lambda Function ARN"
37+
Value: !GetAtt BedrockAgentFastAPIFunction.Arn

0 commit comments

Comments
 (0)
Please sign in to comment.