packages/constructs/L3/ai/bedrock-agent-l3-construct/lib/bedrock-agent-l3-construct.ts (296 lines of code) (raw):

/*! * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ import { MdaaRoleRef } from '@aws-mdaa/iam-role-helper'; import { MdaaL3Construct, MdaaL3ConstructProps } from '@aws-mdaa/l3-construct'; import { Role, IRole, PolicyStatement, Effect, ManagedPolicy } from 'aws-cdk-lib/aws-iam'; import { Construct } from 'constructs'; import { aws_bedrock as bedrock, CfnOutput, IResolvable } from 'aws-cdk-lib'; import { StringParameter } from 'aws-cdk-lib/aws-ssm'; import { DECRYPT_ACTIONS, ENCRYPT_ACTIONS, MdaaKmsKey } from '@aws-mdaa/kms-constructs'; import { MdaaManagedPolicy } from '@aws-mdaa/iam-constructs'; import { resolve } from 'path'; import { parse, stringify } from 'yaml'; import { readFileSync } from 'fs'; import { FunctionProps, LambdaFunctionL3Construct, LayerProps } from '@aws-mdaa/dataops-lambda-l3-construct'; import { CfnPermission } from 'aws-cdk-lib/aws-lambda'; export interface LambdaFunctionProps { /** * List of layer definitions */ readonly layers?: LayerProps[]; /** * List of function definitions */ readonly functions?: FunctionProps[]; } export interface APISchemaProperty extends bedrock.CfnAgent.APISchemaProperty { /** * Provide relative path to JSON/YAML formatted OpenAPI schema */ readonly openApiSchemaPath?: string; } export interface AgentActionGroupProperty { /** * Name of action group */ readonly actionGroupName: string; /** * Specify whether the action group is available for the agent to invoke or not * @default ENABLED * Valid states: ENABLED | DISABLED */ readonly actionGroupState?: string; /** * Description of action group * @default - No description. */ readonly description?: string; /** * Action group executor */ readonly actionGroupExecutor: bedrock.CfnAgent.ActionGroupExecutorProperty; /** * API Schema for action group */ readonly apiSchema?: APISchemaProperty; /** * Functions that each define parameters that agent needs to invoke from the user */ readonly functionSchema?: bedrock.CfnAgent.FunctionSchemaProperty; } export interface BedrockAgentProps { /** * Specifies whether to automatically update the DRAFT version of the agent after making changes to the agent */ readonly autoPrepare?: boolean; /** * The description of the agent */ readonly description?: string; /** * The foundation model used for orchestration by the agent */ readonly foundationModel: string; /** * The number of seconds for which Amazon Bedrock keeps information about a user's conversation with the agent */ readonly idleSessionTtlInSeconds?: number; /** * Instructions that tell the agent what it should do and how it should interact with users */ readonly instruction: string; /** * Contains configurations to override prompt templates in different parts of an agent sequence */ readonly promptOverrideConfiguration?: bedrock.CfnAgent.PromptOverrideConfigurationProperty; /** * The knowledge bases associated with the agent. */ readonly knowledgeBases?: bedrock.CfnAgent.AgentKnowledgeBaseProperty[]; /** * The action groups that belong to an agent */ readonly actionGroups?: AgentActionGroupProperty[]; /** * Configuration information for a guardrail that you use with the Converse operation * See also: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-bedrock-agent-guardrailconfiguration.html */ readonly guardrailConfiguration?: bedrock.CfnAgent.GuardrailConfigurationProperty; /** * Name of Alias for Agent pointing to specific Agent Version */ readonly agentAliasName?: string; } export interface BedrockAgentL3ConstructProps extends MdaaL3ConstructProps { /** * List of admin roles which will be provided access to agent resources (like KMS/Bucket) */ readonly dataAdminRoles: MdaaRoleRef[]; /** * Reference to role which will be used as execution role on all agent(s). * The role must have assume role trust with bedrock.amazonaws.com. Additional managed polies * may be added to the role to grant it access to agent resources and relevant services. */ readonly bedrockAgentExecutionRole: MdaaRoleRef; /** * Bedrock Agents */ readonly agents: { [agentName: string]: BedrockAgentProps }; /** (Optional) * The Amazon Resource Name (ARN) of the AWS KMS key that encrypts the agent resources. * If not provided, a customer managed key will be created */ readonly kmsKeyArn?: string; /** * (Optional) S3 Bucket Arn for the agent */ readonly agentBucketArn?: string; /** * (Optional) Lambda Functions and associated layers Used By Agent Action Groups */ readonly lambdaFunctions?: LambdaFunctionProps; } //This stack creates all of the resources required for a Data Science agent //to use SageMaker Studio on top of a Data Lake export class BedrockAgentL3Construct extends MdaaL3Construct { protected readonly props: BedrockAgentL3ConstructProps; public readonly agents: bedrock.CfnAgent[] = []; constructor(scope: Construct, id: string, props: BedrockAgentL3ConstructProps) { super(scope, id, props); this.props = props; const bedrockAgentExecutionRoleResolved = props.roleHelper.resolveRoleRefWithRefId( this.props.bedrockAgentExecutionRole, 'agent-execution-role', ); const bedrockAgentExecutionRole = Role.fromRoleArn( this, 'agent-execution-role', bedrockAgentExecutionRoleResolved.arn(), ); const dataAdminRoles = this.props.roleHelper.resolveRoleRefsWithOrdinals(this.props.dataAdminRoles, 'DataAdmin'); const dataAdminRoleIds = dataAdminRoles.map(x => x.id()); const allRoleIds = [...dataAdminRoleIds, bedrockAgentExecutionRoleResolved.id()]; const kmsKeyArn = this.getOrCreateAgentKmsKey(props, allRoleIds); // Create Necessary Lambda Functions containing the business logic that is carried out upon invoking the action. const generatedFunctions: { [name: string]: string } = {}; if (props.lambdaFunctions) { const agentLambdas = new LambdaFunctionL3Construct(this, 'agent-lambda-functions', { kmsArn: kmsKeyArn, roleHelper: props.roleHelper, naming: this.props.naming, functions: props.lambdaFunctions?.functions, layers: props.lambdaFunctions?.layers, }); // Create a map of function-name to function-arn for easy lookup Object.entries(agentLambdas.functionsMap).forEach(([name, lambda]) => { generatedFunctions[name] = lambda.functionArn; }); } // Create Agent Managed Policy const agentManagedPolicy = this.createBedrockAgentPolicy(props); agentManagedPolicy.attachToRole(bedrockAgentExecutionRole); // Create Bedrock Agent(s) Object.entries(props.agents).forEach(([agentName, agentConfig]) => { const agent = this.createBedrockAgent( agentName, agentConfig, bedrockAgentExecutionRole, kmsKeyArn, generatedFunctions, ); this.agents.push(agent); }); return this; } private getActionGroups( agentConfig: BedrockAgentProps, functionsArnMap: { [name: string]: string }, ): bedrock.CfnAgent.AgentActionGroupProperty[] { // Check every actionGroup within props.agent.actionGroup and if actionGroup.apiSchema.openSchemaPath property is defined, read the yaml file and load it to payload // If not defined, push it to the agentActionGroups array const agentActionGroups: bedrock.CfnAgent.AgentActionGroupProperty[] = []; const actionGroups = agentConfig.actionGroups ?? []; actionGroups.forEach(actionGroup => { // Check if openApiSchemaPath is defined, if yes, read the schema from local file let apiSchema; if (actionGroup.apiSchema?.openApiSchemaPath) { const configFilePath = resolve(__dirname, actionGroup.apiSchema?.openApiSchemaPath); console.log('Reading config file from path' + configFilePath); const payload = parse(readFileSync(configFilePath, 'utf8')); apiSchema = { payload: stringify(payload) }; } else { apiSchema = actionGroup?.apiSchema; } const ag: bedrock.CfnAgent.AgentActionGroupProperty = { actionGroupName: actionGroup.actionGroupName, apiSchema: apiSchema, functionSchema: actionGroup.functionSchema, description: actionGroup.description, actionGroupState: actionGroup.actionGroupState, actionGroupExecutor: this.processActionGroupExecutor(actionGroup.actionGroupExecutor, functionsArnMap), }; agentActionGroups.push(ag); }); return agentActionGroups; } private processActionGroupExecutor( executor: bedrock.CfnAgent.ActionGroupExecutorProperty, functionsArnMap: { [name: string]: string }, ): bedrock.CfnAgent.ActionGroupExecutorProperty | undefined | IResolvable { if (!executor || !executor.lambda) { return executor; } // Check if props using generated Lambda Function. // If the executor property, starts with generated-function:<function-name>, then replace this with the Function ARN from the map if (executor.lambda.startsWith('generated-function:')) { const functionName = executor.lambda.split(':')[1]; const lambdaArn = functionsArnMap[functionName.trim()]; if (lambdaArn) { return { lambda: lambdaArn, }; } else { throw new Error(`Code references non-existant Generated Lambda function: ${functionName} `); } } return executor; } private createAgentAlias(agentName: string, agentId: string, agentConfig: BedrockAgentProps) { if (agentConfig.agentAliasName) { const agentAlias = new bedrock.CfnAgentAlias(this, `mdaa-bedrock-agent-${agentName}-alias`, { agentId: agentId, agentAliasName: agentConfig.agentAliasName, }); new StringParameter(agentAlias, `${agentName}/alias`, { parameterName: this.props.naming.ssmPath(`${agentName}/alias`, false), stringValue: agentConfig.agentAliasName, }); new CfnOutput(agentAlias, 'AgentAlias', { value: agentConfig.agentAliasName, description: 'The alias name of the Bedrock Agent', exportName: this.props.naming.resourceName(`${agentName}-AgentAlias`), }); } } private createBedrockAgent( agentName: string, agentConfig: BedrockAgentProps, bedrockAgentExecutionRole: IRole, kmsKeyArn: string, generatedFunctions: { [name: string]: string }, ): bedrock.CfnAgent { // Prepare action group(s) for the Agent const agentActionGroups: bedrock.CfnAgent.AgentActionGroupProperty[] = this.getActionGroups( agentConfig, generatedFunctions, ); // Create Bedrock Agent const agent = new bedrock.CfnAgent(this, `mdaa-bedrock-agent-${agentName}`, { agentName: this.props.naming.resourceName(agentName), autoPrepare: agentConfig.autoPrepare ?? false, customerEncryptionKeyArn: kmsKeyArn, description: agentConfig.description, foundationModel: agentConfig.foundationModel, idleSessionTtlInSeconds: agentConfig.idleSessionTtlInSeconds ?? 3600, instruction: agentConfig.instruction, promptOverrideConfiguration: agentConfig.promptOverrideConfiguration, agentResourceRoleArn: bedrockAgentExecutionRole.roleArn, knowledgeBases: agentConfig.knowledgeBases, guardrailConfiguration: agentConfig.guardrailConfiguration, actionGroups: agentActionGroups, }); // Create an alias for the agent this.createAgentAlias(agentName, agent.attrAgentId, agentConfig); // Add Lambda Permission to allow Bedrock Service Principal to Invoke Lambda on behalf of Specific Agent if (agentActionGroups) { agentActionGroups?.forEach((ag, index) => { if (ag?.actionGroupExecutor && !('resolve' in ag.actionGroupExecutor)) { const lambdaArn = ag?.actionGroupExecutor?.lambda; if (lambdaArn) { // Create the permission for Bedrock to invoke Lambda new CfnPermission(this, `BedrockInvokePermission-${index}`, { action: 'lambda:InvokeFunction', functionName: lambdaArn, principal: 'bedrock.amazonaws.com', sourceArn: agent.attrAgentArn, }); } } }); } new StringParameter(agent, `${agentName}/name`, { parameterName: this.props.naming.ssmPath(`${agentName}/name`, false), stringValue: this.props.naming.resourceName(agentName), }); new StringParameter(agent, `${agentName}/id`, { parameterName: this.props.naming.ssmPath(`${agentName}/id`, false), stringValue: agent.attrAgentId, }); new CfnOutput(agent, 'AgentId', { value: agent.attrAgentId, description: 'The ID of the created Bedrock Agent', exportName: this.props.naming.resourceName(`${agentName}-AgentId`), }); return agent; } private getOrCreateAgentKmsKey(props: BedrockAgentL3ConstructProps, allRoleIds: string[]): string { if (props.kmsKeyArn) { return props.kmsKeyArn; } else { const kmsKey = new MdaaKmsKey(this.scope, 'bedrock-agent-cmk', { naming: this.props.naming, keyUserRoleIds: allRoleIds, }); // Provide encrypt/decrypt access to bedrock service const BedrockServiceEncryptPolicy = new PolicyStatement({ sid: 'AllowBedrockToEncryptDecryptAgentResourceOnUsersBehalf', effect: Effect.ALLOW, // Use of * mirrors what is done in the CDK methods for adding policy helpers. resources: ['*'], actions: [...ENCRYPT_ACTIONS, ...DECRYPT_ACTIONS], }); BedrockServiceEncryptPolicy.addServicePrincipal('bedrock.amazonaws.com'); kmsKey.addToResourcePolicy(BedrockServiceEncryptPolicy); // Allow the attachment of persistent resources const BedrockServiceGrantPolicy = new PolicyStatement({ sid: 'Allow the attachment of persistent resources', effect: Effect.ALLOW, resources: ['*'], actions: ['kms:CreateGrant', 'kms:ListGrants', 'kms:RevokeGrant'], conditions: { Bool: { 'kms:GrantIsForAWSResource': true, }, }, }); BedrockServiceGrantPolicy.addServicePrincipal('bedrock.amazonaws.com'); kmsKey.addToResourcePolicy(BedrockServiceGrantPolicy); return kmsKey.keyArn; } } private createBedrockAgentPolicy(props: BedrockAgentL3ConstructProps): ManagedPolicy { // Extract list of foundation models & guardrails for each agent. const foundationModelArns: Set<string> = new Set<string>(); const guardrailArns: Set<string> = new Set<string>(); const knowledgeBaseArns: Set<string> = new Set<string>(); Object.entries(props.agents).forEach(agent => { foundationModelArns.add(`arn:aws:bedrock:${this.region}::foundation-model/${agent[1].foundationModel}`); // Check if Agent uses guardrail(s) if (agent[1].guardrailConfiguration?.guardrailIdentifier) { guardrailArns.add( `arn:aws:bedrock:${this.region}:${this.account}:guardrail/${agent[1].guardrailConfiguration?.guardrailIdentifier}`, ); } // Check if Agent uses knowledgebase(s) if (agent[1].knowledgeBases) { agent[1].knowledgeBases.forEach(kb => { knowledgeBaseArns.add(`arn:aws:bedrock:${this.region}:${this.account}:knowledge-base/${kb.knowledgeBaseId}`); }); } }); // Add a Policy to allow invoke access to the foundation model const agentManagedPolicy = new MdaaManagedPolicy(this, 'agent-managed-pol', { managedPolicyName: 'agent-managed-policy', naming: this.props.naming, }); // Allow access to the foundation model const invokeModelStatement = new PolicyStatement({ sid: 'InvokeFoundationModel', effect: Effect.ALLOW, resources: [...foundationModelArns], actions: ['bedrock:InvokeModel', 'bedrock:InvokeModelWithResponseStream'], }); agentManagedPolicy.addStatements(invokeModelStatement); // Apply Guardrail policy if Guardrails is mentioned if (guardrailArns.size > 0) { const guardrailStatement = new PolicyStatement({ sid: 'AllowApplyBedrockGuardrail', effect: Effect.ALLOW, resources: [...guardrailArns], actions: ['bedrock:ApplyGuardrail'], }); agentManagedPolicy.addStatements(guardrailStatement); } // Apply Knowledge Base policy if Knowledge Bases is mentioned if (knowledgeBaseArns.size > 0) { const knowledgeBaseStatement = new PolicyStatement({ sid: 'AllowBedrockKnowledgeBase', effect: Effect.ALLOW, resources: [...knowledgeBaseArns], actions: ['bedrock:Retrieve'], }); agentManagedPolicy.addStatements(knowledgeBaseStatement); } return agentManagedPolicy; } }