This repository has been archived on 2026-08-04. You can view files and clone it, but cannot push or open issues or pull requests.
seahaven-slack-bot/lambda/app-home/index.ts
Adam Moussa 006dbf7c6b feat: Alex rollout — persona, Socket Mode, payments, channels, App Home, unanswered questions
- Rename bot to Alex with friendly/professional persona (Bedrock Agent instruction rewrite)
- Replace HTTP webhook Lambda with ECS Fargate Socket Mode service (persistent WebSocket)
- Add payment/invoice lookup via PaymentsDashboard table (vendor, invoice, check search)
- Switch from manual SiteAssignments to auto-populated verified-sites table
- Add channel support via app_mention events (threaded replies)
- Add App Home tab with Block Kit capabilities view
- Add unanswered questions logging to seahaven-unanswered-questions DynamoDB table
- Fix pre-existing compliance: add arm64 + 60-day log retention to all Lambdas
- Remove webhook Lambda and seed-sites script (both obsolete)
2026-04-30 16:38:54 -04:00

123 lines
3.5 KiB
TypeScript

import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
const secretsClient = new SecretsManagerClient({});
interface SlackCredentials {
botToken: string;
signingSecret: string;
}
let cachedCredentials: SlackCredentials | undefined;
async function getSlackCredentials(): Promise<SlackCredentials> {
if (!cachedCredentials) {
const res = await secretsClient.send(
new GetSecretValueCommand({ SecretId: process.env.SLACK_SECRET_ARN! }),
);
cachedCredentials = JSON.parse(res.SecretString!) as SlackCredentials;
}
return cachedCredentials;
}
interface AppHomeEvent {
userId: string;
}
export const handler = async (event: AppHomeEvent): Promise<void> => {
const credentials = await getSlackCredentials();
const blocks = [
{
type: 'header',
text: { type: 'plain_text', text: "Hi, I'm Alex!", emoji: true },
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: "I'm Sea Haven's internal AI assistant. Here's what I can help you with:",
},
},
{ type: 'divider' },
{
type: 'section',
text: {
type: 'mrkdwn',
text: '*Vendor Lookup*\nFind existing vendors in QuickBooks or discover new ones nearby.\n_Try: "Find me a plumber near ABE2"_',
},
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: '*Work Order Status*\nLook up any work order by its WO number.\n_Try: "What\'s the status of WO 10046966057?"_',
},
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: '*Purchase Order Details*\nLook up PO status, line items, and shipping info.\n_Try: "Show me PO 2D-20023475"_',
},
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: '*Payment & Invoice Status*\nCheck if a vendor has been paid or if an invoice is scheduled.\n_Try: "Has invoice 12345 been paid?" or "What payments went to Acme?"_',
},
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: '*Site Lookups*\nFind the address and details for any Amazon facility by site code.\n_Try: "Where is DFW6?" or "List all sites in Texas"_',
},
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: '*Company Policies & SOPs*\nAnswer questions about the employee handbook, SA8000 compliance, and SOPs.\n_Try: "What\'s our PTO policy?"_',
},
},
{ type: 'divider' },
{
type: 'section',
text: {
type: 'mrkdwn',
text: '*How to reach me:*\n• Send me a DM right here\n• @mention me in any channel I\'m in',
},
},
{
type: 'context',
elements: [
{
type: 'mrkdwn',
text: "I only know about data in our systems. If I can't answer something, I'll let you know — and my team reviews those gaps regularly to keep improving.",
},
],
},
];
const res = await fetch('https://slack.com/api/views.publish', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${credentials.botToken}`,
},
body: JSON.stringify({
user_id: event.userId,
view: { type: 'home', blocks },
}),
});
if (!res.ok) {
throw new Error(`Slack views.publish HTTP error: ${res.status}`);
}
const data = (await res.json()) as { ok: boolean; error?: string };
if (!data.ok) {
throw new Error(`Slack views.publish API error: ${data.error}`);
}
};