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; promptSessionAttributes: Record; } 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 { 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; 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) .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 { 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; 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'); } // ── Site lookup ───────────────────────────────────────────────────────────── async function lookupSite(siteCode: string, state?: string): Promise { // Exact site code lookup if (siteCode) { const res = await dynamo.send( new GetItemCommand({ TableName: process.env.SITES_TABLE!, Key: { siteCode: { S: siteCode.toUpperCase() } }, }), ); if (res.Item) { const s = unmarshall(res.Item) as Record; const lines: string[] = []; lines.push(`SITE ${s.siteCode}`); lines.push(`Address: ${s.address === 'TBD' ? 'TBD (not yet established)' : s.fullAddress || s.address}`); if (s.city) lines.push(`City: ${s.city}`); if (s.state) lines.push(`State: ${s.state}`); if (s.zip) lines.push(`Zip: ${s.zip}`); if (s.latitude && s.longitude) lines.push(`Coordinates: ${s.latitude}, ${s.longitude}`); if (s.notes) lines.push(`Notes: ${s.notes}`); return lines.join('\n'); } return `No site found for code "${siteCode}". Please provide a valid Amazon site code (e.g., "ABE2", "DFW6").`; } // State-based search via GSI if (state) { const res = await dynamo.send( new QueryCommand({ TableName: process.env.SITES_TABLE!, IndexName: 'by-state', KeyConditionExpression: '#st = :state', ExpressionAttributeNames: { '#st': 'state' }, ExpressionAttributeValues: { ':state': { S: state.toUpperCase() } }, }), ); const sites = (res.Items ?? []).map((item) => unmarshall(item) as Record); if (sites.length === 0) { return `No sites found in state "${state}".`; } sites.sort((a, b) => (a.siteCode ?? '').localeCompare(b.siteCode ?? '')); const lines = [`Sites in ${state.toUpperCase()} (${sites.length} total):`]; for (const s of sites) { const addr = s.address === 'TBD' ? 'TBD' : `${s.address}, ${s.city}`; const note = s.notes ? ` — ${s.notes}` : ''; lines.push(`- ${s.siteCode}: ${addr}${note}`); } return lines.join('\n'); } return 'Please provide a site code (e.g., "ABE2") or a state abbreviation (e.g., "TX") to search.'; } // ── Handler ────────────────────────────────────────────────────────────────── export const handler = async (event: BedrockActionEvent): Promise => { 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 if (event.function === 'lookup_site') { const code = params.site_code?.trim() ?? ''; const state = params.state?.trim(); if (!code && !state) return makeResponse(event, 'Please provide a site code or state abbreviation.'); result = await lookupSite(code, state); } 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}`); } };