2026-04-11 23:15:15 -04:00
import * as cdk from 'aws-cdk-lib' ;
import { Construct } from 'constructs' ;
import * as iam from 'aws-cdk-lib/aws-iam' ;
import * as lambda from 'aws-cdk-lib/aws-lambda' ;
import * as lambdaNodejs from 'aws-cdk-lib/aws-lambda-nodejs' ;
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager' ;
import * as bedrock from 'aws-cdk-lib/aws-bedrock' ;
import * as path from 'path' ;
export interface BedrockAgentProps {
accountId : string ;
region : string ;
knowledgeBaseId : string ;
knowledgeBaseArn : string ;
}
export class BedrockAgentConstruct extends Construct {
public readonly agent : bedrock.CfnAgent ;
public readonly agentAlias : bedrock.CfnAgentAlias ;
public readonly qboLambda : lambdaNodejs.NodejsFunction ;
public readonly mapsLambda : lambdaNodejs.NodejsFunction ;
2026-04-11 23:52:44 -04:00
// Cross-region inference profile — required for Claude 4.x on Bedrock Agents
private static readonly MODEL_ID = 'us.anthropic.claude-sonnet-4-5-20250929-v1:0' ;
2026-04-11 23:15:15 -04:00
constructor ( scope : Construct , id : string , props : BedrockAgentProps ) {
super ( scope , id ) ;
// Reference secrets created manually in Secrets Manager (see README for structure)
const qboSecret = secretsmanager . Secret . fromSecretNameV2 (
this , 'QBOSecret' , 'seahaven/qbo/oauth' ,
) ;
const mapsSecret = secretsmanager . Secret . fromSecretNameV2 (
this , 'MapsSecret' , 'seahaven/google/maps-api-key' ,
) ;
const bundling : lambdaNodejs.BundlingOptions = {
externalModules : [ '@aws-sdk/*' ] ,
minify : true ,
sourceMap : false ,
} ;
// ── QBO vendor lookup action group Lambda ─────────────────────────────────
this . qboLambda = new lambdaNodejs . NodejsFunction ( this , 'QBOLookupFn' , {
functionName : 'seahaven-qbo-lookup' ,
entry : path.join ( __dirname , '../../lambda/qbo-lookup/index.ts' ) ,
handler : 'handler' ,
runtime : lambda.Runtime.NODEJS_22_X ,
timeout : cdk.Duration.seconds ( 30 ) ,
memorySize : 256 ,
environment : { QBO_SECRET_ARN : qboSecret.secretArn } ,
bundling ,
} ) ;
qboSecret . grantRead ( this . qboLambda ) ;
// ── Google Maps lookup action group Lambda ────────────────────────────────
this . mapsLambda = new lambdaNodejs . NodejsFunction ( this , 'MapsLookupFn' , {
functionName : 'seahaven-maps-lookup' ,
entry : path.join ( __dirname , '../../lambda/maps-lookup/index.ts' ) ,
handler : 'handler' ,
runtime : lambda.Runtime.NODEJS_22_X ,
timeout : cdk.Duration.seconds ( 30 ) ,
memorySize : 256 ,
environment : { MAPS_SECRET_ARN : mapsSecret.secretArn } ,
bundling ,
} ) ;
mapsSecret . grantRead ( this . mapsLambda ) ;
// ── Bedrock Agent execution role ──────────────────────────────────────────
const agentRole = new iam . Role ( this , 'AgentRole' , {
roleName : 'AmazonBedrockExecutionRoleForAgents_seahaven' ,
assumedBy : new iam . ServicePrincipal ( 'bedrock.amazonaws.com' , {
conditions : {
StringEquals : { 'aws:SourceAccount' : props . accountId } ,
ArnLike : {
'aws:SourceArn' : ` arn:aws:bedrock: ${ props . region } : ${ props . accountId } :agent/* ` ,
} ,
} ,
} ) ,
} ) ;
agentRole . addToPolicy ( new iam . PolicyStatement ( {
2026-04-11 23:52:44 -04:00
actions : [ 'bedrock:InvokeModel' , 'bedrock:InvokeModelWithResponseStream' ] ,
2026-04-11 23:15:15 -04:00
resources : [
2026-04-11 23:52:44 -04:00
// Cross-region inference profile (us.*) routes to multiple regions — wildcard region required
` arn:aws:bedrock:*::foundation-model/anthropic.claude-sonnet-4-5-20250929-v1:0 ` ,
` arn:aws:bedrock: ${ props . region } : ${ props . accountId } :inference-profile/ ${ BedrockAgentConstruct . MODEL_ID } ` ,
2026-04-11 23:15:15 -04:00
] ,
} ) ) ;
agentRole . addToPolicy ( new iam . PolicyStatement ( {
actions : [ 'bedrock:Retrieve' ] ,
resources : [ props . knowledgeBaseArn ] ,
} ) ) ;
// Allow Bedrock to invoke the action group Lambdas
this . qboLambda . addPermission ( 'BedrockInvokeQBO' , {
principal : new iam . ServicePrincipal ( 'bedrock.amazonaws.com' ) ,
sourceAccount : props.accountId ,
} ) ;
this . mapsLambda . addPermission ( 'BedrockInvokeMaps' , {
principal : new iam . ServicePrincipal ( 'bedrock.amazonaws.com' ) ,
sourceAccount : props.accountId ,
} ) ;
// ── Agent instruction (system prompt) ────────────────────────────────────
const instruction = ` You are the Sea Haven Industries internal assistant, accessible to employees via Slack direct message. Sea Haven is a facility services company.
You help employees with :
1 . Finding vendors and contractors for facility work
2 . Company policies , SOPs , and SA8000 social accountability compliance questions
3 . Employee handbook questions
# # Vendor Query Rules — STRICTLY follow this priority order :
Step 1 : ALWAYS call QBO_Lookup . search_vendors first . This searches our QuickBooks Online account for existing , vetted vendors we already have a relationship with .
Step 2 : If QBO_Lookup returns no suitable match , search the knowledge base for approved vendor documentation or lists .
Step 3 : ONLY if both QBO and the knowledge base return nothing suitable should you call Google_Maps_Lookup . search_nearby_vendors to find new options .
When presenting vendor results :
- QBO vendors : include name , trade / specialty , phone , email , and last updated date
- Knowledge base vendors : cite the source document
- Google Maps vendors : clearly label these as NEW ( not yet vetted ) , include name , address , phone , and rating
# # General Questions :
Use the knowledge base for policy , SOP , SA8000 compliance , and handbook questions . Cite the specific document or section when possible . If the information is not in the knowledge base , say so clearly — do not guess .
Keep responses concise , professional , and actionable . ` ;
// ── CfnAgent ──────────────────────────────────────────────────────────────
this . agent = new bedrock . CfnAgent ( this , 'Agent' , {
agentName : 'seahaven-assistant' ,
description : 'Sea Haven Industries internal Slack assistant' ,
agentResourceRoleArn : agentRole.roleArn ,
foundationModel : BedrockAgentConstruct.MODEL_ID ,
instruction ,
idleSessionTtlInSeconds : 1800 , // 30 min — matches the processor's session window
knowledgeBases : [
{
knowledgeBaseId : props.knowledgeBaseId ,
description : 'Sea Haven internal documents: SOPs, SA8000 compliance docs, employee handbook, approved vendor lists' ,
knowledgeBaseState : 'ENABLED' ,
} ,
] ,
actionGroups : [
{
actionGroupName : 'QBO_Lookup' ,
description : 'Search QuickBooks Online for existing Sea Haven vendors by trade or name' ,
actionGroupState : 'ENABLED' ,
actionGroupExecutor : { lambda : this.qboLambda.functionArn } ,
functionSchema : {
functions : [
{
name : 'search_vendors' ,
description : 'Search QuickBooks Online for existing vendors by trade category or company name. Returns contact info and outstanding balance.' ,
parameters : {
trade : {
type : 'string' ,
description : 'Trade or service type to search for (e.g., "plumbing", "electrical", "HVAC", "janitorial", "landscaping")' ,
required : false ,
} ,
name : {
type : 'string' ,
description : 'Vendor company name or partial name to search for' ,
required : false ,
} ,
} ,
} ,
] ,
} ,
} ,
{
actionGroupName : 'Google_Maps_Lookup' ,
description : 'Search Google Maps Places for vendors near a location. Use ONLY when QBO and the knowledge base have no suitable vendor.' ,
actionGroupState : 'ENABLED' ,
actionGroupExecutor : { lambda : this.mapsLambda.functionArn } ,
functionSchema : {
functions : [
{
name : 'search_nearby_vendors' ,
description : 'Search Google Maps Places for local vendors and contractors by trade type and location.' ,
parameters : {
trade : {
type : 'string' ,
description : 'Trade or service type to search for (e.g., "plumbing contractor", "electrician")' ,
required : true ,
} ,
location : {
type : 'string' ,
2026-04-12 00:01:33 -04:00
description : 'Location to search near — can be a city, address, zip code, or facility/business name (e.g., "Seattle WA", "Amazon BFI9", "3230 International Pl DuPont WA"). Pass whatever location context the user provided; Google Maps will resolve it.' ,
2026-04-11 23:15:15 -04:00
required : true ,
} ,
} ,
} ,
] ,
} ,
} ,
] ,
} ) ;
// ── Agent alias (stable ARN for invocations) ──────────────────────────────
// Creating the alias also triggers agent preparation in CloudFormation.
this . agentAlias = new bedrock . CfnAgentAlias ( this , 'AgentAlias' , {
agentId : this.agent.attrAgentId ,
agentAliasName : 'live' ,
2026-04-12 00:01:33 -04:00
description : 'Production alias — seahaven-assistant v3 (flexible location lookup)' ,
2026-04-11 23:15:15 -04:00
} ) ;
}
}