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.
190 lines
7 KiB
TypeScript
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}`);
|
|
}
|
|
};
|