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' ;
2026-04-13 17:59:47 -04:00
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb' ;
2026-04-11 23:15:15 -04:00
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-13 17:59:47 -04:00
public readonly woPoLambda : lambdaNodejs.NodejsFunction ;
2026-04-11 23:15:15 -04:00
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 ) ;
2026-04-13 17:59:47 -04:00
// ── WO/PO direct lookup action group Lambda ──────────────────────────────
const workOrdersTable = dynamodb . Table . fromTableName (
this , 'WorkOrdersTable' , 'WorkOrders' ,
) ;
const commentsTable = dynamodb . Table . fromTableName (
this , 'WorkOrderCommentsTable' , 'WorkOrderComments' ,
) ;
const poTable = dynamodb . Table . fromTableName (
this , 'PurchaseOrdersTable' , 'purchase-orders' ,
) ;
2026-04-13 19:11:39 -04:00
// Site assignments table — stores Amazon facility site codes and addresses
const sitesTable = new dynamodb . Table ( this , 'SiteAssignmentsTable' , {
tableName : 'SiteAssignments' ,
partitionKey : { name : 'siteCode' , type : dynamodb . AttributeType . STRING } ,
billingMode : dynamodb.BillingMode.PAY_PER_REQUEST ,
removalPolicy : cdk.RemovalPolicy.RETAIN ,
} ) ;
sitesTable . addGlobalSecondaryIndex ( {
indexName : 'by-state' ,
partitionKey : { name : 'state' , type : dynamodb . AttributeType . STRING } ,
projectionType : dynamodb.ProjectionType.ALL ,
} ) ;
2026-04-13 17:59:47 -04:00
this . woPoLambda = new lambdaNodejs . NodejsFunction ( this , 'WoPoLookupFn' , {
functionName : 'seahaven-wo-po-lookup' ,
entry : path.join ( __dirname , '../../lambda/wo-po-lookup/index.ts' ) ,
handler : 'handler' ,
runtime : lambda.Runtime.NODEJS_22_X ,
timeout : cdk.Duration.seconds ( 30 ) ,
memorySize : 256 ,
environment : {
WORK_ORDERS_TABLE : workOrdersTable.tableName ,
COMMENTS_TABLE : commentsTable.tableName ,
PO_TABLE : poTable.tableName ,
2026-04-13 19:11:39 -04:00
SITES_TABLE : sitesTable.tableName ,
2026-04-13 17:59:47 -04:00
} ,
bundling ,
} ) ;
workOrdersTable . grantReadData ( this . woPoLambda ) ;
commentsTable . grantReadData ( this . woPoLambda ) ;
poTable . grantReadData ( this . woPoLambda ) ;
2026-04-13 19:11:39 -04:00
sitesTable . grantReadData ( this . woPoLambda ) ;
2026-04-13 17:59:47 -04:00
2026-04-11 23:15:15 -04:00
// ── 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 ,
} ) ;
2026-04-13 17:59:47 -04:00
this . woPoLambda . addPermission ( 'BedrockInvokeWoPo' , {
principal : new iam . ServicePrincipal ( 'bedrock.amazonaws.com' ) ,
sourceAccount : props.accountId ,
} ) ;
2026-04-11 23:15:15 -04:00
// ── Agent instruction (system prompt) ────────────────────────────────────
2026-04-13 18:50:35 -04:00
const instruction = ` You are the Sea Haven Industries internal assistant, accessible to employees via Slack direct message. Sea Haven is a facility services company that provides maintenance, repair, and facility services to clients like Amazon.
IMPORTANT : You work FOR Sea Haven . Sea Haven is our company — we are the vendor / contractor that gets dispatched to job sites . When work orders or comments mention "Sea Haven" being dispatched or assigned , that means OUR team was sent . Never suggest Sea Haven as an external vendor to contact — we ARE Sea Haven . When users ask for a "local vendor," they mean a subcontractor or specialty trade vendor to handle work on our behalf .
2026-04-11 23:15:15 -04:00
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
2026-04-13 17:05:26 -04:00
4 . Work order lookups — status , history , assignments , comments , and details for any work order by its WO number
5 . Purchase order lookups — status , line items , suppliers , ship - to details , and dates for any PO by its PO number
2026-04-13 19:11:39 -04:00
6 . Amazon site lookups — address , location , and details for any Amazon facility by its site code ( e . g . , "ABE2" , "DFW6" ) , or listing all sites in a given state
2026-04-11 23:15:15 -04:00
# # 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
2026-04-13 17:05:26 -04:00
# # Work Order & Purchase Order Queries :
2026-04-13 18:34:47 -04:00
When a user asks about a work order ( WO ) or purchase order ( PO ) by number , ALWAYS use the WO_PO_Lookup action group to retrieve the record directly . Do NOT use the knowledge base for WO / PO lookups by number — the action group queries the database directly and is more reliable . Present the returned data clearly and concisely . The output is already well - structured — relay the key information without adding excessive formatting or repeating section headers verbatim . Summarize the current status and most recent updates first , then include the full comment history .
2026-04-13 17:05:26 -04:00
2026-04-13 19:11:39 -04:00
# # Site Lookups :
When a user asks about an Amazon site by its code ( e . g . , "ABE2" , "DFW6" , "WWY1" ) , ALWAYS use the WO_PO_Lookup . lookup_site function . You can also list all sites in a state by passing the state abbreviation . Do NOT use the knowledge base for site code lookups — the action group queries the database directly and is more reliable .
2026-04-11 23:15:15 -04:00
# # 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 ,
2026-04-13 17:05:26 -04:00
description : 'Sea Haven internal documents: SOPs, SA8000 compliance docs, employee handbook, approved vendor lists, work orders, and purchase orders' ,
2026-04-11 23:15:15 -04:00
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 ,
} ,
} ,
} ,
] ,
} ,
} ,
2026-04-13 17:59:47 -04:00
{
actionGroupName : 'WO_PO_Lookup' ,
2026-04-13 19:11:39 -04:00
description : 'Look up work orders, purchase orders, and Amazon site assignments directly from the database' ,
2026-04-13 17:59:47 -04:00
actionGroupState : 'ENABLED' ,
actionGroupExecutor : { lambda : this.woPoLambda.functionArn } ,
functionSchema : {
functions : [
{
name : 'lookup_work_order' ,
description : 'Look up a work order by its ID number. Returns status, description, site, assignment, dates, and full comment history.' ,
parameters : {
work_order_id : {
type : 'string' ,
description : 'The work order ID number (e.g., "10046966057")' ,
required : true ,
} ,
} ,
} ,
{
name : 'lookup_purchase_order' ,
description : 'Look up a purchase order by its PO number. Returns status, supplier, ship-to, line items, amounts, and dates.' ,
parameters : {
po_number : {
type : 'string' ,
description : 'The purchase order number (e.g., "2D-20023475")' ,
required : true ,
} ,
} ,
} ,
2026-04-13 19:11:39 -04:00
{
name : 'lookup_site' ,
description : 'Look up an Amazon facility site by its site code or list all sites in a state. Returns address, city, state, coordinates, and notes.' ,
parameters : {
site_code : {
type : 'string' ,
description : 'The Amazon site code (e.g., "ABE2", "DFW6", "WWY1")' ,
required : false ,
} ,
state : {
type : 'string' ,
description : 'Two-letter state abbreviation to list all sites in that state (e.g., "TX", "CA", "GA")' ,
required : false ,
} ,
} ,
} ,
2026-04-13 17:59:47 -04:00
] ,
} ,
} ,
2026-04-11 23:15:15 -04:00
] ,
} ) ;
// ── 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-13 19:11:39 -04:00
description : 'Production alias — seahaven-assistant v8 (site lookups)' ,
2026-04-11 23:15:15 -04:00
} ) ;
}
}