
Agentic Code Interpreter with AWS CDK
In this article, we'll walk you through building a skeleton CDK L2 construct for Amazon Bedrock Agent for its code interpreter support.
1
new Agent();
cdk deploy
I know it just works! Hassle-free!Vpc
, it should create a well-architected one for me unless I specify an existing one.agentId
, agentArn
or even agentStatus
.new Agent()
as many as I can and they won't conflict.addXxx()
or grantXxx()
. They are not mandatory but would be very helpful.cdk destroy
, all created Agents and everything from there should be just eliminated. Nothing would incur on my AWS billing after that.- Create an Agent - no problem, we have bedrock.CfnAgent L1 construct.
- Create an IAM role with scoped policies and the least required privileges. This would take a while because I need to read the document. But hopefully I should just have to do it once.
- Create an
ActionGroup
that supports CodeInterpreter capabilities, which is currently in public preview. This would be a little challenging because CFN has not supported it yet. The ParentActionGroupSignature only supportsAMAZON.UserInput
at this moment. But that's fine, we have AwsCustomResource to the rescue. - We need to wait for the Agent until it enters the
NOT_PREPARED
state before we can continue the next step as described in the video here. Well, this is a little bit challenging but let's see what's happening then. - We have to "prepare" the Agent. Obviously, CloudFormation does not support that at this moment. The only option is AwsCustomResource. No big deal.
- Last but not least, we need to create an Alias for that Agent and we can retrieve the
agentAliasId
from there. The good news is that we have bedrock.CfnAgent L1 construct.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// We need an interface for our Agent L2 construct.
export interface IAgent extends IResource {}
// props for the Agent
export interface AgentProps {}
// the L2 construct
export class Agent extends cdk.Resource implements IAgent {
constructor(scope: Construct, id: string, props: AgentProps) {
super(scope, id);
// create the CfnAgent here
const resource = new bedrock.CfnAgent(...)
}
private createServiceRole(): iam.Role {...}
private createCustomResourceRole(): iam.Role {...}
private loadInstruction(): string {...}
private createCodeInterpreterActionGroup(): cr.AwsCustomResource {...}
private prepareAgent(agentId: string): cr.AwsCustomResource {...}
public addAlias(aliasName?: string) {...}
}
- IAgent - This represents the "Agent". It could be a new one created with
new Agent();
or an imported one byAgent.fromAgentAttributes()
. We need to define what attributes or properties it should have. I checked the return values of AWS::Bedrock::Agent from Cloudformation document. I believeagentId
,agentArn
andagentVersion
are mandatory and I decided to addagentStatus
as optional.
1
2
3
4
5
6
export interface IAgent extends IResource {
readonly agentId: string;
readonly agentArn: string;
readonly agentStatus?: string;
readonly agentVersion: string;
}
- AgentProps - This is the props when you
new Agent(scope, id, props);
We just need to figure out what are props are required from CFN's doc. Looking at all the properties from here, the good news is only AgentName is required. But we still can make it optional in CDK and generate a name when undefined so users can literally pass no props when theynew Agent();
. This would be amazing! I ended up with this interface that has an optionalrole
only. If you pass an existingiam.IRole
in as the ServiceRole, I would just pick up from there; otherwise, generate a scoped one for you.
1
2
3
export interface AgentProps {
role?: iam.IRole;
}
- Agent - Now we need to implement the constructor of this class. My preferred pattern is like - if you pass some prop, I just use it, otherwise I generate one for you with a private construct method. This makes the main logic very clean. It looks like:
1
2
3
const resource = new bedrock.CfnAgent(this, 'Resource', {
agentResourceRoleArn: props?.role?.roleArn ?? this.createServiceRole().roleArn,
})
resource
using the L1 construct, you need to assign the attributes that are defined in IAgent
interface so we can reference the attributes like Arn
or resource ID from there. In CDK , we use getResourceArnAttribute() to get the resource ARN. I ended up with all the assignments as below:1
2
3
4
5
6
7
8
this.agentId = resource.attrAgentId;
this.agentArn = this.getResourceArnAttribute(resource.attrAgentArn, {
service: 'bedrock',
resource: 'agent',
resourceName: this.physicalName,
}),
this.agentStatus = resource.attrAgentStatus;
this.agentVersion = resource.attrAgentVersion;
- this.createServiceRole() - This should be simply new iam.Role() with bedrock as the service principal and simply
addToPolicy()
with relevant actions and resources. I ended up with this:
1
2
3
4
5
6
7
8
9
10
11
private createServiceRole(): iam.Role {
const role = new iam.Role(this, 'ServiceRole', {
assumedBy: new iam.ServicePrincipal('bedrock.amazonaws.com'),
});
role.addToPolicy(new iam.PolicyStatement({
actions: ['bedrock:InvokeModel'],
resources: [`arn:aws:bedrock:${Stack.of(this).region}::foundation-model/${this.model.modelId}`],
}));
return role;
}
- this.createCustomResourceRole() - This role is for
AwsCustomResource
that would create the action group and prepare agent behind the scene. This is not perfect and well-scoped because we use*
in the resources. We could optimize it later.
1
2
3
4
5
6
7
8
9
10
private createCustomResourceRole(): iam.Role {
const role = new iam.Role(this, 'CustomResourceRole', {
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
});
role.addToPolicy(new iam.PolicyStatement({
actions: ['bedrock:CreateAgentActionGroup', 'bedrock:PrepareAgent'],
resources: ['*'],
}));
return role;
}
- this.loadInstruction() - We need a method to read in the instructions from an external file like instructions.txt. This method allows us to pass it to the L1 with
instruction: this.loadInstruction()
Amazon Q literally generated this function for me. Looks awesome!
1
2
3
4
5
6
7
8
9
10
private loadInstruction(): string {
try {
const filePath = path.join(__dirname, 'instruction.txt');
const content = fs.readFileSync(filePath, 'utf-8');
return content.trim();
} catch (error) {
console.error('Error loading instruction:', error);
return 'You are a helpful assistant.'; // Default instruction
}
}
- this.createCodeInterpreterActionGroup() - As mentioned, cloudformation does not support codeinterpreter at this moment. This method essentially invokes a SDK call to create an ActionGroup wtih AMAZON.CodeInterpreter parentActionGroupSignature for us. Easy peasy!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private createCodeInterpreterActionGroup(id: string): cr.AwsCustomResource {
const resource = new cr.AwsCustomResource(this, id, {
resourceType: 'Custom::BedrockAgentActionGroup',
onCreate: {
service: '@aws-sdk/client-bedrock-agent',
action: 'CreateAgentActionGroupCommand',
parameters: {
actionGroupName: id,
agentId: this.agentId,
actionGroupState: 'ENABLED',
agentVersion: 'DRAFT',
parentActionGroupSignature: 'AMAZON.CodeInterpreter',
},
physicalResourceId: cr.PhysicalResourceId.of(id),
},
role: this.customResourceRole,
});
// custom resource must be created until the default policy of the role is ready
resource.node.addDependency(this.customResourceRole.node.tryFindChild('DefaultPolicy') as iam.CfnPolicy)
return resource;
}
addDependency()
line, this custom resource could be provisioned before the default policy of the role is created and that could cause an error. The addDependency() ensures the custom resource would only be created after the default policy of the role is ready. This trick is very important.- this.prepareAgent() - Prepare the agent. Yet another custom resource. No fancy.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private prepareAgent(agentId: string): cr.AwsCustomResource {
const resource = new cr.AwsCustomResource(this, 'prepare', {
resourceType: 'Custom::BedrockAgentActionGroup',
onCreate: {
service: '@aws-sdk/client-bedrock-agent',
action: 'PrepareAgentCommand',
parameters: {
agentId,
},
physicalResourceId: cr.PhysicalResourceId.of('prepare'),
},
role: this.customResourceRole,
});
// custom resource must be created until the default policy of the role is ready
resource.node.addDependency(this.customResourceRole.node.tryFindChild('DefaultPolicy') as iam.CfnPolicy)
return resource;
}
- this.addAlias() - Last but not least, I decided to define a public addAlias method for it so the expected user experience would be like:
1
2
3
4
5
// create an Agent
const agent = new Agent();
// add an Alias for this agent
agent.addAlias('my-alias-name');
1
2
3
4
5
6
7
8
public addAlias(aliasName?: string) {
const cfnalias = new bedrock.CfnAgentAlias(this, `${this.id}alias`, {
agentId: this.agentId,
agentAliasName: aliasName ?? `${this.id}alias`,
})
cfnalias.node.addDependency(...this.dependable)
return cfnalias;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
export interface ActionGroupExecutor {
readonly customControl?: string;
readonly lambda?: lambda.IFunction;
}
export interface AgentApiSchema {
readonly payload?: string;
readonly bucket?: s3.IBucket;
readonly key?: string;
}
export interface FunctionParameters {
readonly description?: string;
readonly required?: boolean;
readonly type: string;
}
export interface AgentFunction {
readonly description?: string;
readonly functioName: string;
readonly parameters?: FunctionParameters;
}
export interface FunctionSchema {
readonly function: AgentFunction[];
}
export interface IActionGroup {
readonly executor?: ActionGroupExecutor;
readonly groupName: string;
readonly groupState?: string;
readonly apiSchema?: AgentApiSchema;
readonly description?: string;
readonly functionSchema?: FunctionSchema;
readonly parentActionGroupSignature?: string;
readonly skipResourceInUseCheckOnDelete?: boolean
}
export interface ActionGroupProps {
readonly executor?: ActionGroupExecutor;
readonly groupName: string;
readonly groupState?: string;
readonly apiSchema?: AgentApiSchema;
readonly description?: string;
readonly functionSchema?: FunctionSchema;
readonly parentActionGroupSignature?: string;
readonly skipResourceInUseCheckOnDelete?: boolean
}
export class ActionGroup implements IActionGroup {
readonly executor?: ActionGroupExecutor;
readonly groupName: string;
readonly groupState?: string;
readonly apiSchema?: AgentApiSchema;
readonly description?: string;
readonly functionSchema?: FunctionSchema;
readonly parentActionGroupSignature?: string;
readonly skipResourceInUseCheckOnDelete?: boolean
constructor(scope: Construct, props: ActionGroupProps) {
this.executor = props.executor;
this.groupName = props.groupName;
this.groupState = props.groupState;
this.apiSchema = props.apiSchema;
this.description = props.description;
this.functionSchema = props.functionSchema;
this.parentActionGroupSignature = props.parentActionGroupSignature;
this.skipResourceInUseCheckOnDelete = props.skipResourceInUseCheckOnDelete;
}
}
export interface IAgent extends IResource {
readonly agentId: string;
readonly agentArn: string;
readonly agentStatus?: string;
readonly agentVersion: string;
}
export interface AgentProps {
role?: iam.IRole;
actionGroups?: IActionGroup[];
}
// create an AIAgent Stack
export class Agent extends Resource implements IAgent {
private readonly model:bedrock.FoundationModelIdentifier
public readonly agentId: string;
public readonly agentArn: string;
public readonly agentStatus?: string;
public readonly agentVersion: string;
public readonly id: string;
public readonly customResourceRole: iam.IRole;
/**
* dependable resources of this Agent
*/
public readonly dependable: IDependable[] = [];
constructor(scope: Construct, id: string, props?: AgentProps) {
super(scope, id);
this.id = id;
this.model = bedrock.FoundationModelIdentifier.ANTHROPIC_CLAUDE_3_SONNET_20240229_V1_0;
const resource = new bedrock.CfnAgent(this, 'Resource', {
agentName: `${id}agent`,
foundationModel: this.model.modelId,
agentResourceRoleArn: props?.role?.roleArn ?? this.createServiceRole().roleArn,
instruction: this.loadInstruction(),
actionGroups: props?.actionGroups?.map((actionGroup) => ({
actionGroupName: actionGroup.groupName,
actionGroupExecutor: {
lambda: actionGroup.executor?.lambda?.functionArn,
customControl: actionGroup.executor?.customControl,
},
apiSchema: actionGroup.apiSchema,
description: actionGroup.description,
functionSchema: actionGroup.functionSchema ? {
functions: actionGroup.functionSchema.function.map((agentFunction) => ({
description: agentFunction.description,
name: agentFunction.functioName,
parameters: {
required: agentFunction.parameters?.required,
type: agentFunction.parameters?.type,
description: agentFunction.parameters?.description,
}
}))
} : undefined,
parentActionGroupSignature: actionGroup.parentActionGroupSignature,
skipResourceInUseCheckOnDelete: actionGroup.skipResourceInUseCheckOnDelete,
})) as bedrock.CfnAgent.AgentActionGroupProperty[],
});
this.agentId = resource.attrAgentId;
this.agentArn = this.getResourceArnAttribute(resource.attrAgentArn, {
service: 'bedrock',
resource: 'agent',
resourceName: this.physicalName,
}),
this.agentStatus = resource.attrAgentStatus;
this.agentVersion = resource.attrAgentVersion;
new CfnOutput(this, 'AgentId', { value: this.agentId });
this.customResourceRole = this.createCustomResourceRole();
// create action group
const ag = this.createCodeInterpreterActionGroup('CodeInterpreterActionGroup');
// prepare the agent
const preparation = this.prepareAgent(this.agentId);
preparation.node.addDependency(ag);
this.dependable.push(preparation)
}
private createCustomResourceRole(): iam.Role {
const role = new iam.Role(this, 'CustomResourceRole', {
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
});
role.addToPolicy(new iam.PolicyStatement({
actions: ['bedrock:CreateAgentActionGroup', 'bedrock:PrepareAgent'],
resources: ['*'],
}));
return role;
}
private loadInstruction(): string {
try {
const filePath = path.join(__dirname, 'instruction.txt');
const content = fs.readFileSync(filePath, 'utf-8');
return content.trim();
} catch (error) {
console.error('Error loading instruction:', error);
return 'You are a helpful assistant.'; // Default instruction
}
}
private createServiceRole(): iam.Role {
const role = new iam.Role(this, 'ServiceRole', {
assumedBy: new iam.ServicePrincipal('bedrock.amazonaws.com'),
});
role.addToPolicy(new iam.PolicyStatement({
actions: ['bedrock:InvokeModel'],
resources: [`arn:aws:bedrock:${Stack.of(this).region}::foundation-model/${this.model.modelId}`],
}));
return role;
}
private createCodeInterpreterActionGroup(id: string): cr.AwsCustomResource {
const resource = new cr.AwsCustomResource(this, id, {
resourceType: 'Custom::BedrockAgentActionGroup',
onCreate: {
service: '@aws-sdk/client-bedrock-agent',
action: 'CreateAgentActionGroupCommand',
parameters: {
actionGroupName: id,
agentId: this.agentId,
actionGroupState: 'ENABLED',
agentVersion: 'DRAFT',
parentActionGroupSignature: 'AMAZON.CodeInterpreter',
},
physicalResourceId: cr.PhysicalResourceId.of(id),
},
role: this.customResourceRole,
});
// custom resource must be created until the default policy of the role is ready
resource.node.addDependency(this.customResourceRole.node.tryFindChild('DefaultPolicy') as iam.CfnPolicy)
return resource;
}
private prepareAgent(agentId: string): cr.AwsCustomResource {
const resource = new cr.AwsCustomResource(this, 'prepare', {
resourceType: 'Custom::BedrockAgentActionGroup',
onCreate: {
service: '@aws-sdk/client-bedrock-agent',
action: 'PrepareAgentCommand',
parameters: {
agentId,
},
physicalResourceId: cr.PhysicalResourceId.of('prepare'),
},
role: this.customResourceRole,
});
// custom resource must be created until the default policy of the role is ready
resource.node.addDependency(this.customResourceRole.node.tryFindChild('DefaultPolicy') as iam.CfnPolicy)
return resource;
}
public addAlias(aliasName?: string) {
const cfnalias = new bedrock.CfnAgentAlias(this, `${this.id}alias`, {
agentId: this.agentId,
agentAliasName: aliasName ?? `${this.id}alias`,
})
cfnalias.node.addDependency(...this.dependable)
return cfnalias;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
const app = new cdk.App();
const stack = new Stack(app, 'dummy-stack');
// create an agent that supports code interpreter
const agent = new Agent(stack, 'my-agent');
// create an alias for the agent
const cfnalias = agent.addAlias();
// output the aliasId
new cdk.CfnOutput(stack, 'AgentAliasId', { value: cfnalias.attrAgentAliasId });
cdk diff
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Resources
[+] AWS::IAM::Role my-agent/ServiceRole myagentServiceRoleB7D0824C
[+] AWS::IAM::Policy my-agent/ServiceRole/DefaultPolicy myagentServiceRoleDefaultPolicyA6E2267C
[+] AWS::Bedrock::Agent my-agent myagentE3D29236
[+] AWS::IAM::Role my-agent/CustomResourceRole myagentCustomResourceRole2B6858D3
[+] AWS::IAM::Policy my-agent/CustomResourceRole/DefaultPolicy myagentCustomResourceRoleDefaultPolicyE0E266F3
[+] Custom::BedrockAgentActionGroup my-agent/CodeInterpreterActionGroup/Resource myagentCodeInterpreterActionGroupE39538C0
[+] Custom::BedrockAgentActionGroup my-agent/prepare/Resource myagentprepare920C7AFE
[+] AWS::Bedrock::AgentAlias my-agent/my-agentalias myagentmyagentalias2E256E2B
[+] AWS::Lambda::Function AWS679f53fac002430cb0da5b7982bd2287 AWS679f53fac002430cb0da5b7982bd22872D164C4C
Outputs
[+] Output my-agent/AgentId myagentAgentId10E55C5B: {"Value":{"Fn::GetAtt":["myagentE3D29236","AgentId"]}}
[+] Output AgentAliasId AgentAliasId: {"Value":{"Fn::GetAtt":["myagentmyagentalias2E256E2B","AgentAliasId"]}}
1
2
3
4
5
6
7
8
✅ dummy-stack
✨ Deployment time: 74.58s
Outputs:
dummy-stack.AgentAliasId = DXGB8QQBEO
dummy-stack.myagentAgentId10E55C5B = TYT1Y2XXZR
- Build your "It just works" developer experience with a skeleton L2 construct.
- A skeleton L2 construct is a MVP(Minimum Viable Product) of a CDK L2 construct that only supports mandatory props with very clear and clean definition that could be extended in future development iteration. It should be built with AI like Amazon Q Developer in your IDE and typically could be drafted within a couple of minutes.
- Define your private and public construct helper methods to separate them from your constructor code logic. This makes your code clean and easy to read.
- Use the
props.foo ?? this.createFoo()
pattern. - Don't hesitate to use AwsCustomResource when necessary.
- Using Amazon Bedrock Agents to interactively generate infrastructure as code - https://aws.amazon.com/blogs/machine-learning/using-agents-for-amazon-bedrock-to-interactively-generate-infrastructure-as-code/
Any opinions in this post are those of the individual author and may not reflect the opinions of AWS.