- Add markdown-to-Slack mrkdwn conversion in processor (** → *, ## → bold) - Restructure lambda output with cleaner sections and date formatting - Update agent instruction to present data concisely
220 lines
7.6 KiB
TypeScript
220 lines
7.6 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[] = [];
|
|
lines.push(`WORK ORDER ${wo.work_order_id}`);
|
|
if (wo.description) lines.push(`Description: ${wo.description}`);
|
|
lines.push('');
|
|
|
|
// Details section
|
|
const details: string[] = [];
|
|
if (wo.wo_status) details.push(`Status: ${wo.wo_status}`);
|
|
if (wo.customer) details.push(`Customer: ${wo.customer}`);
|
|
if (wo.site_code) details.push(`Site: ${wo.site_code}${wo.building && wo.building !== wo.site_code ? ` (${wo.building})` : ''}`);
|
|
if (wo.address) details.push(`Address: ${wo.address}`);
|
|
if (wo.severity) details.push(`Severity: ${wo.severity}`);
|
|
if (wo.priority) details.push(`Priority: ${wo.priority}`);
|
|
if (wo.assigned_to) details.push(`Assigned To: ${wo.assigned_to}`);
|
|
if (details.length > 0) {
|
|
lines.push('DETAILS');
|
|
lines.push(...details);
|
|
lines.push('');
|
|
}
|
|
|
|
// Dates section
|
|
const dates: string[] = [];
|
|
if (wo.date_reported) dates.push(`Reported: ${wo.date_reported}`);
|
|
if (wo.scheduled_start) dates.push(`Scheduled Start: ${wo.scheduled_start}`);
|
|
if (wo.due_date) dates.push(`Due: ${wo.due_date}`);
|
|
if (wo.updated_at) dates.push(`Last Updated: ${wo.updated_at.split('T')[0]}`);
|
|
if (dates.length > 0) {
|
|
lines.push('DATES');
|
|
lines.push(...dates);
|
|
lines.push('');
|
|
}
|
|
|
|
// 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('COMMENT HISTORY (oldest to newest)');
|
|
for (const c of comments) {
|
|
const date = (c.created_at ?? 'unknown').split('T')[0];
|
|
const author = c.commenter ?? 'unknown';
|
|
lines.push(`[${date}] ${author}: ${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[] = [];
|
|
lines.push(`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.total_amount !== undefined) lines.push(`Total: ${po.currency ?? 'USD'} ${po.total_amount}`);
|
|
lines.push('');
|
|
|
|
// Details section
|
|
const details: string[] = [];
|
|
if (po.supplier?.name) details.push(`Supplier: ${po.supplier.name}`);
|
|
if (po.source_system) details.push(`Source: ${po.source_system}`);
|
|
if (po.order_date) details.push(`Order Date: ${po.order_date}`);
|
|
if (po.revision_date) details.push(`Revision Date: ${po.revision_date}`);
|
|
if (po.payment_terms) details.push(`Payment Terms: ${po.payment_terms}`);
|
|
if (po.requisition_number) details.push(`Requisition #: ${po.requisition_number}`);
|
|
if (po.department) details.push(`Department: ${po.department}`);
|
|
if (po.submitted_by) details.push(`Submitted By: ${po.submitted_by}`);
|
|
if (po.on_behalf_of) details.push(`On Behalf Of: ${po.on_behalf_of}`);
|
|
if (details.length > 0) {
|
|
lines.push('DETAILS');
|
|
lines.push(...details);
|
|
lines.push('');
|
|
}
|
|
|
|
// Ship-to section
|
|
const ship = po.ship_to;
|
|
if (ship) {
|
|
const parts: string[] = [];
|
|
if (ship.name) parts.push(`Name: ${ship.name}`);
|
|
if (ship.address) parts.push(`Address: ${ship.address}`);
|
|
if (ship.location_code) parts.push(`Location Code: ${ship.location_code}`);
|
|
if (ship.attn) parts.push(`Attn: ${ship.attn}`);
|
|
if (parts.length > 0) {
|
|
lines.push('SHIP TO');
|
|
lines.push(...parts);
|
|
lines.push('');
|
|
}
|
|
}
|
|
|
|
// Line items section
|
|
const items = po.line_items;
|
|
if (Array.isArray(items) && items.length > 0) {
|
|
lines.push('LINE ITEMS');
|
|
for (let i = 0; i < items.length; i++) {
|
|
const li = items[i];
|
|
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}` : '';
|
|
const cat = li.category ? ` | Category: ${li.category}` : '';
|
|
lines.push(`${i + 1}. ${desc}${amt}${needBy}${cat}`);
|
|
}
|
|
}
|
|
|
|
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}`);
|
|
}
|
|
};
|