Skip to content

Commit de711e4

Browse files
authored
Add PetClinic AI Agents to demo app (#147)
*Description of changes:* Adds two specialized Pet Clinic AI agents built using Strands SDK and deployed to AWS Bedrock AgentCore Runtime: **Agents**: - *Pet Clinic Agent*: General assistant handling appointments, clinic info, and emergency contacts. Delegates nutrition queries to the Nutrition Agent. - *Nutrition Agent*: Specialized agent for pet nutrition recommendations, diet guidelines, and feeding advice. **Infrastructure**: - CDK stacks for agent deployment with custom CloudFormation resources (note: this is temporary as Bedrock AgentCore Runtime does not have CDK infrastructure yet) - Lambda traffic generator triggered every 2 minutes via EventBridge **Custom CDK Resource** - Create: <img width="884" height="266" alt="image" src="https://github.com/user-attachments/assets/5fb7fbd5-7a99-44b7-956c-df8e7f8ccb58" /> Delete: <img width="648" height="76" alt="image" src="https://github.com/user-attachments/assets/4c1808f3-b97d-4f6c-b014-f9e5fb4e3d6e" /> By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
1 parent e358f7a commit de711e4

File tree

22 files changed

+1708
-0
lines changed

22 files changed

+1708
-0
lines changed

.gitignore

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,20 @@
1111

1212
# python
1313
**/__pycache__/
14+
venv/
15+
.venv/
16+
env/
17+
.env
18+
19+
# CDK
20+
cdk.out/
21+
*.assets.json
22+
*.template.json
23+
24+
# Node.js
25+
node_modules/
26+
package-lock.json
27+
npm-debug.log*
1428

1529
# Maven
1630
target/

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,3 +168,34 @@ The following instructions set up an ECS cluster with all services running in Fa
168168
```
169169
cd scripts/ecs/appsignals/ && ./setup-ecs-demo.sh --operation=delete --region=region-name
170170
```
171+
172+
# Bedrock AgentCore Runtime Demo
173+
174+
The following instructions set up AI agents deployed to Bedrock AgentCore Runtime. You can run these steps in your personal AWS account to follow along (Not recommended for production usage).
175+
176+
The setup includes:
177+
178+
- **Primary Agent**: A general pet clinic assistant that handles appointment scheduling, clinic information, and emergency contacts. Any nutrition related queries will be delegated to the Nutrition Agent.
179+
- **Nutrition Agent**: A specialized agent focused on pet nutrition, diet recommendations, and feeding guidelines
180+
- **Traffic Generator**: A Lambda function scheduled via AWS EventBridge that sends queries to the Primary Agent every 1 minute.
181+
182+
**Prerequisites:**
183+
- AWS CLI 2.x configured with appropriate permissions
184+
- AWS CDK >= v2.1024.0 installed
185+
- Node.js >= v18.0.0 installed
186+
- Docker installed and running (for building agent container images)
187+
- Access to Amazon Bedrock foundation models (Claude 3.5 Haiku recommended)
188+
189+
## Setup Instructions
190+
191+
1. **Deploy the agents and traffic generator**. Replace `region-name` with your desired AWS region (e.g., `us-east-1`):
192+
193+
```shell
194+
cd scripts/agents && ./setup-agents-demo.sh --region=region-name
195+
```
196+
197+
2. **Clean up resources** when finished:
198+
199+
```shell
200+
cd scripts/agents && ./setup-agents-demo.sh --operation=delete --region=region-name
201+
```

cdk/agents/app.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#!/usr/bin/env node
2+
const cdk = require('aws-cdk-lib');
3+
const { PetClinicAgentsStack } = require('./lib/pet-clinic-agents-stack');
4+
const { PetClinicAgentsTrafficGeneratorStack } = require('./lib/pet-clinic-agents-traffic-generator-stack');
5+
6+
const app = new cdk.App();
7+
8+
// Deploy Pet Clinic agents
9+
const agentsStack = new PetClinicAgentsStack(app, 'PetClinicAgentsStack', {
10+
env: {
11+
account: process.env.CDK_DEFAULT_ACCOUNT,
12+
region: process.env.CDK_DEFAULT_REGION,
13+
},
14+
});
15+
16+
// Deploy Pet Clinic agents traffic generator
17+
new PetClinicAgentsTrafficGeneratorStack(app, 'PetClinicAgentsTrafficGeneratorStack', {
18+
env: {
19+
account: process.env.CDK_DEFAULT_ACCOUNT,
20+
region: process.env.CDK_DEFAULT_REGION,
21+
},
22+
primaryAgentArn: agentsStack.primaryAgentArn,
23+
nutritionAgentArn: agentsStack.nutritionAgentArn,
24+
});
25+
26+
app.synth();

cdk/agents/cdk.json

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
{
2+
"app": "node app.js",
3+
"watch": {
4+
"include": [
5+
"**"
6+
],
7+
"exclude": [
8+
"README.md",
9+
"cdk*.json",
10+
"**/*.d.ts",
11+
"**/*.js",
12+
"tsconfig.json",
13+
"package*.json",
14+
"yarn.lock",
15+
"node_modules",
16+
"test"
17+
]
18+
},
19+
"context": {
20+
"@aws-cdk/aws-lambda:recognizeLayerVersion": true,
21+
"@aws-cdk/core:checkSecretUsage": true,
22+
"@aws-cdk/core:target-partitions": [
23+
"aws",
24+
"aws-cn"
25+
],
26+
"@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
27+
"@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
28+
"@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
29+
"@aws-cdk/aws-iam:minimizePolicies": true,
30+
"@aws-cdk/core:validateSnapshotRemovalPolicy": true,
31+
"@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
32+
"@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
33+
"@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
34+
"@aws-cdk/aws-apigateway:disableCloudWatchRole": true,
35+
"@aws-cdk/core:enablePartitionLiterals": true,
36+
"@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,
37+
"@aws-cdk/aws-iam:standardizedServicePrincipals": true,
38+
"@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,
39+
"@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true,
40+
"@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true,
41+
"@aws-cdk/aws-route53-patters:useCertificate": true,
42+
"@aws-cdk/customresources:installLatestAwsSdkDefault": false,
43+
"@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true,
44+
"@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true,
45+
"@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true,
46+
"@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true,
47+
"@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true,
48+
"@aws-cdk/aws-redshift:columnId": true,
49+
"@aws-cdk/aws-stepfunctions-tasks:enableLogging": true,
50+
"@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true,
51+
"@aws-cdk/aws-apigateway:requestValidatorUniqueId": true,
52+
"@aws-cdk/aws-kms:aliasNameRef": true,
53+
"@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true,
54+
"@aws-cdk/core:includePrefixInUniqueNameGeneration": true,
55+
"@aws-cdk/aws-efs:denyAnonymousAccess": true,
56+
"@aws-cdk/aws-opensearchservice:enableLogging": true,
57+
"@aws-cdk/aws-nordicapis-apigateway:authorizerChangeDeploymentLogicalId": true,
58+
"@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true,
59+
"@aws-cdk/aws-ecs-patterns:removeDefaultDesiredCount": true,
60+
"@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true,
61+
"@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForSourceAction": true
62+
}
63+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import json
2+
import boto3
3+
from botocore.config import Config
4+
from urllib.request import urlopen, Request
5+
6+
client = boto3.client('bedrock-agentcore-control', config=Config(retries={'max_attempts': 5, 'mode': 'standard'}))
7+
8+
def handler(event, context):
9+
print(f"Event: {json.dumps(event)}")
10+
11+
request_type = event['RequestType']
12+
properties = event['ResourceProperties']
13+
14+
try:
15+
if request_type == 'Create':
16+
return create_agent(properties, event, context)
17+
elif request_type == 'Update':
18+
return update_agent(properties, event, context)
19+
elif request_type == 'Delete':
20+
return delete_agent(properties, event, context)
21+
except Exception as e:
22+
return send_response(event, context, 'FAILED', {'Error': str(e)})
23+
24+
def create_agent(properties, event, context):
25+
agent_name = properties['AgentName']
26+
image_uri = properties['ImageUri']
27+
execution_role = properties['ExecutionRole']
28+
29+
try:
30+
response = client.create_agent_runtime(
31+
agentRuntimeName=agent_name,
32+
description=f'{agent_name} agent for Application Signals demo',
33+
agentRuntimeArtifact={
34+
'containerConfiguration': {
35+
'containerUri': image_uri
36+
}
37+
},
38+
roleArn=execution_role,
39+
networkConfiguration={
40+
'networkMode': 'PUBLIC'
41+
},
42+
protocolConfiguration={
43+
'serverProtocol': 'HTTP'
44+
}
45+
)
46+
47+
agent_arn = response['agentRuntimeArn']
48+
49+
return send_response(event, context, 'SUCCESS', {
50+
'AgentArn': agent_arn,
51+
'AgentName': agent_name
52+
}, agent_arn)
53+
54+
except Exception as e:
55+
print(f"Agent creation failed: {str(e)}")
56+
raise e
57+
58+
def update_agent(properties, event, context):
59+
try:
60+
physical_resource_id = event.get('PhysicalResourceId')
61+
62+
if not physical_resource_id or physical_resource_id == event['LogicalResourceId']:
63+
return send_response(event, context, 'FAILED', {'Error': 'No agent to update'})
64+
65+
agent_runtime_id = physical_resource_id.split('/')[-1] if '/' in physical_resource_id else physical_resource_id.split(':')[-1]
66+
67+
agent_name = properties['AgentName']
68+
image_uri = properties['ImageUri']
69+
execution_role = properties['ExecutionRole']
70+
71+
response = client.update_agent_runtime(
72+
agentRuntimeId=agent_runtime_id,
73+
description=f'{agent_name} agent for Application Signals demo',
74+
agentRuntimeArtifact={
75+
'containerConfiguration': {
76+
'containerUri': image_uri
77+
}
78+
},
79+
roleArn=execution_role,
80+
networkConfiguration={
81+
'networkMode': 'PUBLIC'
82+
},
83+
protocolConfiguration={
84+
'serverProtocol': 'HTTP'
85+
}
86+
)
87+
88+
agent_arn = response['agentRuntimeArn']
89+
90+
return send_response(event, context, 'SUCCESS', {
91+
'AgentArn': agent_arn,
92+
'AgentName': agent_name
93+
}, agent_arn)
94+
95+
except Exception as e:
96+
print(f"Agent update failed: {str(e)}")
97+
return send_response(event, context, 'FAILED', {'Error': str(e)})
98+
99+
def delete_agent(properties, event, context):
100+
try:
101+
physical_resource_id = event.get('PhysicalResourceId')
102+
103+
if not physical_resource_id or physical_resource_id == event['LogicalResourceId']:
104+
return send_response(event, context, 'SUCCESS', {})
105+
106+
agent_runtime_id = physical_resource_id.split('/')[-1] if '/' in physical_resource_id else physical_resource_id.split(':')[-1]
107+
client.delete_agent_runtime(agentRuntimeId=agent_runtime_id)
108+
return send_response(event, context, 'SUCCESS', {})
109+
110+
except Exception as e:
111+
return send_response(event, context, 'FAILED', {'Error': str(e)})
112+
113+
def send_response(event, context, status, data=None, physical_resource_id=None):
114+
response_data = data or {}
115+
116+
response_body = {
117+
'Status': status,
118+
'Reason': f'See CloudWatch Log Stream: {context.log_stream_name}',
119+
'PhysicalResourceId': physical_resource_id or event.get('PhysicalResourceId', event['LogicalResourceId']),
120+
'StackId': event['StackId'],
121+
'RequestId': event['RequestId'],
122+
'LogicalResourceId': event['LogicalResourceId'],
123+
'Data': response_data
124+
}
125+
126+
response_url = event['ResponseURL']
127+
json_response = json.dumps(response_body)
128+
129+
headers = {
130+
'content-type': '',
131+
'content-length': str(len(json_response))
132+
}
133+
134+
req = Request(response_url, data=json_response.encode('utf-8'), headers=headers, method='PUT')
135+
urlopen(req)
136+
137+
return response_body
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
boto3

0 commit comments

Comments
 (0)