diff --git a/lambda/slack-processor/index.ts b/lambda/slack-processor/index.ts index 990f371..b69b68e 100644 --- a/lambda/slack-processor/index.ts +++ b/lambda/slack-processor/index.ts @@ -126,6 +126,22 @@ async function invokeAgent(sessionId: string, inputText: string): Promise + .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<$2|$1>') + // Collapse 3+ consecutive newlines to 2 + .replace(/\n{3,}/g, '\n\n'); +} + export const handler = async (event: ProcessorEvent): Promise => { const { userId, channelId, text, ts, threadTs } = event; @@ -150,7 +166,7 @@ export const handler = async (event: ProcessorEvent): Promise => { answer = 'Sorry, I encountered an error processing your request. Please try again in a moment.'; } - await updateSlack(credentials.botToken, channelId, placeholderTs, answer); + await updateSlack(credentials.botToken, channelId, placeholderTs, toSlackMrkdwn(answer)); const now = new Date().toISOString(); const ttl = Math.floor(Date.now() / 1000) + 90 * 24 * 60 * 60; // 90-day TTL diff --git a/lambda/wo-po-lookup/index.ts b/lambda/wo-po-lookup/index.ts index bd001f0..4829798 100644 --- a/lambda/wo-po-lookup/index.ts +++ b/lambda/wo-po-lookup/index.ts @@ -61,21 +61,37 @@ async function lookupWorkOrder(workOrderId: string): Promise { const wo = unmarshall(woRes.Item) as Record; - const lines: string[] = [`Work Order: ${wo.work_order_id}`]; + const lines: string[] = []; + lines.push(`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}`); + 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( @@ -91,13 +107,11 @@ async function lookupWorkOrder(workOrderId: string): Promise { .sort((a, b) => (a.created_at ?? '').localeCompare(b.created_at ?? '')); if (comments.length > 0) { - lines.push(''); - lines.push('Comment History:'); + lines.push('COMMENT HISTORY (oldest to newest)'); for (const c of comments) { - const ts = c.created_at ?? 'unknown date'; + const date = (c.created_at ?? 'unknown').split('T')[0]; const author = c.commenter ?? 'unknown'; - const type = c.record_type ? ` [${c.record_type}]` : ''; - lines.push(`- ${ts} — ${author}${type}: ${c.text ?? '(no text)'}`); + lines.push(`[${date}] ${author}: ${c.text ?? '(no text)'}`); } } @@ -120,40 +134,56 @@ async function lookupPurchaseOrder(poNumber: string): Promise { const po = unmarshall(poRes.Item) as Record; - const lines: string[] = [`Purchase Order: ${po.po_number}`]; + 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.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}`); + lines.push(''); - if (po.supplier?.name) lines.push(`Supplier: ${po.supplier.name}`); + // 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(ship.name); - if (ship.address) parts.push(ship.address); - if (ship.location_code) parts.push(`(${ship.location_code})`); + 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: ${parts.join(', ')}`); + 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(''); - lines.push('Line Items:'); - for (const li of items) { + 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})` : ''; - lines.push(`- ${desc}${amt}${needBy}`); + 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}`); } } diff --git a/lib/constructs/bedrock-agent.ts b/lib/constructs/bedrock-agent.ts index 49bc8c5..a5a055a 100644 --- a/lib/constructs/bedrock-agent.ts +++ b/lib/constructs/bedrock-agent.ts @@ -162,7 +162,7 @@ When presenting vendor results: - Google Maps vendors: clearly label these as NEW (not yet vetted), include name, address, phone, and rating ## Work Order & Purchase Order Queries: -When a user asks about a work order (WO) or purchase order (PO) by number, ALWAYS use the WO_PO_Lookup action group to retrieve the record directly. Do NOT use the knowledge base for WO/PO lookups by number — the action group queries the database directly and is more reliable. Present all returned details including status, dates, assignments, and comment history. +When a user asks about a work order (WO) or purchase order (PO) by number, ALWAYS use the WO_PO_Lookup action group to retrieve the record directly. Do NOT use the knowledge base for WO/PO lookups by number — the action group queries the database directly and is more reliable. Present the returned data clearly and concisely. The output is already well-structured — relay the key information without adding excessive formatting or repeating section headers verbatim. Summarize the current status and most recent updates first, then include the full comment history. ## General Questions: Use the knowledge base for policy, SOP, SA8000 compliance, and handbook questions. Cite the specific document or section when possible. If the information is not in the knowledge base, say so clearly — do not guess. @@ -277,7 +277,7 @@ Keep responses concise, professional, and actionable.`; this.agentAlias = new bedrock.CfnAgentAlias(this, 'AgentAlias', { agentId: this.agent.attrAgentId, agentAliasName: 'live', - description: 'Production alias — seahaven-assistant v5 (WO + PO direct lookup)', + description: 'Production alias — seahaven-assistant v6 (Slack formatting)', }); } }