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/wo-po-lookup/index.ts
Adam Moussa 24ebe8bdcc Add WO/PO direct lookup action group for reliable ID-based queries
Vector search couldn't match exact work order/PO numbers, so queries
always came back empty. This adds a dedicated lambda that queries
DynamoDB directly by ID, wired as a Bedrock Agent action group.
2026-04-13 17:59:47 -04:00

190 lines
7 KiB
TypeScript

import { DynamoDBClient, GetItemCommand, QueryCommand } from '@aws-sdk/client-dynamodb';
import { unmarshall } from '@aws-sdk/util-dynamodb';
const dynamo = new DynamoDBClient({});
// ── Bedrock action group event / response types ───────────────────────────────
interface ActionParameter {
name: string;
type: string;
value: string;
}
interface BedrockActionEvent {
messageVersion: string;
agent: { name: string; id: string; alias: string; version: string };
inputText: string;
sessionId: string;
actionGroup: string;
function: string;
parameters?: ActionParameter[];
sessionAttributes: Record<string, string>;
promptSessionAttributes: Record<string, string>;
}
interface BedrockActionResponse {
messageVersion: string;
response: {
actionGroup: string;
function: string;
functionResponse: {
responseBody: { TEXT: { body: string } };
};
};
}
function makeResponse(event: BedrockActionEvent, body: string): BedrockActionResponse {
return {
messageVersion: '1.0',
response: {
actionGroup: event.actionGroup,
function: event.function,
functionResponse: { responseBody: { TEXT: { body } } },
},
};
}
// ── Work Order lookup ────────────────────────────────────────────────────────
async function lookupWorkOrder(workOrderId: string): Promise<string> {
const woRes = await dynamo.send(
new GetItemCommand({
TableName: process.env.WORK_ORDERS_TABLE!,
Key: { work_order_id: { S: workOrderId } },
}),
);
if (!woRes.Item) {
return `Work order ${workOrderId} was not found.`;
}
const wo = unmarshall(woRes.Item) as Record<string, any>;
const lines: string[] = [`Work Order: ${wo.work_order_id}`];
if (wo.description) lines.push(`Description: ${wo.description}`);
if (wo.wo_status) lines.push(`Status: ${wo.wo_status}`);
if (wo.customer) lines.push(`Customer: ${wo.customer}`);
if (wo.site_code) lines.push(`Site Code: ${wo.site_code}`);
if (wo.building) lines.push(`Building: ${wo.building}`);
if (wo.address) lines.push(`Address: ${wo.address}`);
if (wo.severity) lines.push(`Severity: ${wo.severity}`);
if (wo.priority) lines.push(`Priority: ${wo.priority}`);
if (wo.assigned_to) lines.push(`Assigned To: ${wo.assigned_to}`);
if (wo.date_reported) lines.push(`Date Reported: ${wo.date_reported}`);
if (wo.scheduled_start) lines.push(`Scheduled Start: ${wo.scheduled_start}`);
if (wo.due_date) lines.push(`Due Date: ${wo.due_date}`);
if (wo.created_at) lines.push(`Created At: ${wo.created_at}`);
if (wo.updated_at) lines.push(`Updated At: ${wo.updated_at}`);
// Fetch comments
const commentsRes = await dynamo.send(
new QueryCommand({
TableName: process.env.COMMENTS_TABLE!,
KeyConditionExpression: 'work_order_id = :woid',
ExpressionAttributeValues: { ':woid': { S: workOrderId } },
}),
);
const comments = (commentsRes.Items ?? [])
.map((item) => unmarshall(item) as Record<string, any>)
.sort((a, b) => (a.created_at ?? '').localeCompare(b.created_at ?? ''));
if (comments.length > 0) {
lines.push('');
lines.push('Comment History:');
for (const c of comments) {
const ts = c.created_at ?? 'unknown date';
const author = c.commenter ?? 'unknown';
const type = c.record_type ? ` [${c.record_type}]` : '';
lines.push(`- ${ts} — ${author}${type}: ${c.text ?? '(no text)'}`);
}
}
return lines.join('\n');
}
// ── Purchase Order lookup ────────────────────────────────────────────────────
async function lookupPurchaseOrder(poNumber: string): Promise<string> {
const poRes = await dynamo.send(
new GetItemCommand({
TableName: process.env.PO_TABLE!,
Key: { po_number: { S: poNumber } },
}),
);
if (!poRes.Item) {
return `Purchase order ${poNumber} was not found.`;
}
const po = unmarshall(poRes.Item) as Record<string, any>;
const lines: string[] = [`Purchase Order: ${po.po_number}`];
if (po.po_status) lines.push(`Status: ${po.po_status}`);
if (po.email_type === 'cancellation') lines.push(`Cancelled: ${po.cancelled_at ?? 'yes'}`);
if (po.source_system) lines.push(`Source: ${po.source_system}`);
if (po.order_date) lines.push(`Order Date: ${po.order_date}`);
if (po.revision_date) lines.push(`Revision Date: ${po.revision_date}`);
if (po.payment_terms) lines.push(`Payment Terms: ${po.payment_terms}`);
if (po.requisition_number) lines.push(`Requisition #: ${po.requisition_number}`);
if (po.department) lines.push(`Department: ${po.department}`);
if (po.submitted_by) lines.push(`Submitted By: ${po.submitted_by}`);
if (po.on_behalf_of) lines.push(`On Behalf Of: ${po.on_behalf_of}`);
if (po.total_amount !== undefined) lines.push(`Total: ${po.currency ?? 'USD'} ${po.total_amount}`);
if (po.supplier?.name) lines.push(`Supplier: ${po.supplier.name}`);
const ship = po.ship_to;
if (ship) {
const parts: string[] = [];
if (ship.name) parts.push(ship.name);
if (ship.address) parts.push(ship.address);
if (ship.location_code) parts.push(`(${ship.location_code})`);
if (ship.attn) parts.push(`Attn: ${ship.attn}`);
if (parts.length > 0) lines.push(`Ship To: ${parts.join(', ')}`);
}
const items = po.line_items;
if (Array.isArray(items) && items.length > 0) {
lines.push('');
lines.push('Line Items:');
for (const li of items) {
const desc = li.description ?? 'No description';
const amt = li.amount ? ` — ${li.currency ?? 'USD'} ${li.amount}` : '';
const needBy = li.need_by ? ` (need by ${li.need_by})` : '';
lines.push(`- ${desc}${amt}${needBy}`);
}
}
return lines.join('\n');
}
// ── Handler ──────────────────────────────────────────────────────────────────
export const handler = async (event: BedrockActionEvent): Promise<BedrockActionResponse> => {
const params = Object.fromEntries(
(event.parameters ?? []).map((p) => [p.name, p.value]),
);
try {
let result: string;
if (event.function === 'lookup_work_order') {
const woId = params.work_order_id?.trim();
if (!woId) return makeResponse(event, 'Please provide a work order ID.');
result = await lookupWorkOrder(woId);
} else if (event.function === 'lookup_purchase_order') {
const poNum = params.po_number?.trim();
if (!poNum) return makeResponse(event, 'Please provide a purchase order number.');
result = await lookupPurchaseOrder(poNum);
} else {
result = `Unknown function: ${event.function}`;
}
return makeResponse(event, result);
} catch (err) {
console.error('Lookup error:', err);
return makeResponse(event, `Lookup failed: ${(err as Error).message}`);
}
};