- 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)
123 lines
3.5 KiB
TypeScript
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}`);
|
|
}
|
|
};
|