import * as cdk from 'aws-cdk-lib'; import * as ec2 from 'aws-cdk-lib/aws-ec2'; import { Construct } from 'constructs'; import { ConversationLogConstruct } from './constructs/conversation-log'; import { KnowledgeBaseConstruct } from './constructs/knowledge-base'; import { BedrockAgentConstruct } from './constructs/bedrock-agent'; import { SlackHandlerConstruct } from './constructs/slack-handler'; import { SocketModeConstruct } from './constructs/socket-mode'; import { NotionSyncConstruct } from './constructs/notion-sync'; import { PoSyncConstruct } from './constructs/po-sync'; import { WorkorderSyncConstruct } from './constructs/workorder-sync'; import { MonitoringConstruct } from './constructs/monitoring'; export class SeahavenSlackBotStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); const wildcardCertArn = this.node.tryGetContext('wildcardCertArn') as string | undefined; if (!wildcardCertArn || wildcardCertArn.includes('CHANGE-ME')) { throw new Error( 'Set wildcardCertArn in cdk.json or pass --context wildcardCertArn=arn:aws:acm:...\n' + 'Run: aws acm list-certificates --region us-east-1 to find your cert ARN.', ); } // ── VPC (existing) — QBO Lambdas + Socket Mode run here ────────────────── const vpc = ec2.Vpc.fromLookup(this, 'SeahavenVpc', { vpcId: 'vpc-0d3d4b67bd0cf8a68' }); const lambdaSecurityGroup = new ec2.SecurityGroup(this, 'QBOLambdaSG', { vpc, securityGroupName: 'seahaven-qbo-lambda', description: 'QBO Lambdas - outbound HTTPS only', allowAllOutbound: true, }); // ── Conversation history + unanswered questions (DynamoDB) ──────────────── const conversationLog = new ConversationLogConstruct(this, 'ConversationLog'); // ── Bedrock Knowledge Base (OpenSearch Serverless + S3) ─────────────────── const knowledgeBase = new KnowledgeBaseConstruct(this, 'KnowledgeBase', { accountId: this.account, region: this.region, }); // ── Bedrock Agent (Alex — Claude Sonnet + action groups) ───────────────── const bedrockAgent = new BedrockAgentConstruct(this, 'BedrockAgent', { accountId: this.account, region: this.region, knowledgeBaseId: knowledgeBase.knowledgeBase.knowledgeBaseId, knowledgeBaseArn: knowledgeBase.knowledgeBase.knowledgeBaseArn, vpc, lambdaSecurityGroup, }); // ── Notion → KB daily sync (EventBridge + Lambda) ──────────────────────── const notionSync = new NotionSyncConstruct(this, 'NotionSync', { region: this.region, kbDocsBucket: knowledgeBase.docsBucket, knowledgeBaseId: knowledgeBase.knowledgeBase.knowledgeBaseId, dataSourceId: knowledgeBase.dataSource.dataSourceId, }); // ── Purchase Orders → KB daily sync (EventBridge + Lambda) ──────────────── const poSync = new PoSyncConstruct(this, 'PoSync', { region: this.region, kbDocsBucket: knowledgeBase.docsBucket, knowledgeBaseId: knowledgeBase.knowledgeBase.knowledgeBaseId, dataSourceId: knowledgeBase.dataSource.dataSourceId, }); // ── Work Orders → KB daily sync (EventBridge + Lambda) ──────────────────── const workorderSync = new WorkorderSyncConstruct(this, 'WorkorderSync', { region: this.region, kbDocsBucket: knowledgeBase.docsBucket, knowledgeBaseId: knowledgeBase.knowledgeBase.knowledgeBaseId, dataSourceId: knowledgeBase.dataSource.dataSourceId, }); // ── Slack handler (processor + app home + QBO OAuth + API Gateway) ──────── const slackHandler = new SlackHandlerConstruct(this, 'SlackHandler', { accountId: this.account, region: this.region, agentId: bedrockAgent.agent.attrAgentId, agentAliasId: bedrockAgent.agentAlias.attrAgentAliasId, conversationTable: conversationLog.table, unansweredTable: conversationLog.unansweredTable, wildcardCertArn, vpc, lambdaSecurityGroup, }); // ── Socket Mode (ECS Fargate — replaces webhook Lambda) ────────────────── const socketMode = new SocketModeConstruct(this, 'SocketMode', { vpc, processorLambda: slackHandler.processorLambda, appHomeLambda: slackHandler.appHomeLambda, }); // ── CloudWatch alarm coverage → site-alerts SNS ─────────────────────────── new MonitoringConstruct(this, 'Monitoring', { lambdas: [ { name: 'slack-processor', fn: slackHandler.processorLambda, timeout: cdk.Duration.minutes(5) }, { name: 'app-home', fn: slackHandler.appHomeLambda, timeout: cdk.Duration.seconds(10) }, { name: 'qbo-oauth', fn: slackHandler.qboOAuthLambda, timeout: cdk.Duration.seconds(15) }, { name: 'qbo-lookup', fn: bedrockAgent.qboLambda, timeout: cdk.Duration.seconds(30) }, { name: 'maps-lookup', fn: bedrockAgent.mapsLambda, timeout: cdk.Duration.seconds(30) }, { name: 'wo-po-lookup', fn: bedrockAgent.woPoLambda, timeout: cdk.Duration.seconds(30) }, { name: 'po-sync', fn: poSync.syncLambda, timeout: cdk.Duration.minutes(15) }, { name: 'workorder-sync', fn: workorderSync.syncLambda, timeout: cdk.Duration.minutes(5) }, { name: 'notion-sync', fn: notionSync.syncLambda, timeout: cdk.Duration.minutes(5) }, ], tables: [ { name: 'ddb-conversations', table: conversationLog.table }, { name: 'ddb-unanswered-questions', table: conversationLog.unansweredTable }, ], httpApi: slackHandler.api, ecsService: socketMode.service, ecsCluster: socketMode.cluster, // RunningTaskCount alarm requires Container Insights, enabled on the // socket-mode cluster (see socket-mode.ts). This adds CloudWatch metric + // log cost — flagged in the PR as "NEEDS ADAM SIGN-OFF (cost/config)". // Drop this commit (and the cluster's containerInsightsV2 line) to decline. enableRunningTaskAlarm: true, }); // ── Stack outputs ───────────────────────────────────────────────────────── new cdk.CfnOutput(this, 'KBDocsBucketName', { value: knowledgeBase.docsBucket.bucketName, description: 'Upload SA8000 docs, SOPs, and employee handbook here to populate the KB', }); new cdk.CfnOutput(this, 'AgentId', { value: bedrockAgent.agent.attrAgentId, description: 'Bedrock Agent ID (Alex)', }); } }