// src/utils/grokSummarizer.js import axios from 'axios'; import { logger } from './logger.js'; import { getWorkOrderNotes } from '../integrations/serviceChannel/client.js'; /** * Summarize a batch of work orders using Grok */ export async function summarizeWorkOrders(workOrders) { if (workOrders.length === 0) return []; logger('grok:summarizer', `Starting batch summarization for ${workOrders.length} work orders`); const batchSize = 8; // Keep small to avoid token limits const batches = []; for (let i = 0; i < workOrders.length; i += batchSize) { batches.push(workOrders.slice(i, i + batchSize)); } const allSummaries = []; for (const batch of batches) { logger('grok:summarizer', `Processing batch of ${batch.length} work orders`); try { // Fetch notes for this batch in parallel const batchWithNotes = await Promise.all( batch.map(async (wo) => { const notes = await getWorkOrderNotes(wo.id).catch((err) => { logger('grok:summarizer', `Failed to fetch notes for WO ${wo.id}: ${err.message}`, 'warn'); return []; }); return { ...wo, notes }; }) ); const prompt = ` You are a concise technical summarizer for Service Channel work orders (Audio Visual trade). For each work order below, create a **short but descriptive summary** (2-3 sentences max) focused ONLY on: - What the problem was (from description) - What troubleshooting and resolution steps were taken (prioritize notes/comments for specific actions) Output **ONLY** a valid JSON array, no extra text: [ { "woNumber": "string", "summary": "2-3 sentence summary here", "status": "string", "openedDate": "string", "totalInvoiceCost": number } ] Work orders with notes: ${JSON.stringify(batchWithNotes, null, 2)} `; const response = await axios.post( process.env.XAI_URL, { model: process.env.XAI_MODEL, messages: [ { role: 'system', content: 'Output ONLY valid JSON array. No other text.' }, { role: 'user', content: prompt } ], temperature: 0.3, max_tokens: 2000 }, { headers: { Authorization: `Bearer ${process.env.XAI_API_KEY}`, 'Content-Type': 'application/json' } } ); const content = response.data.choices[0].message.content.trim(); let summaries = []; try { summaries = JSON.parse(content); } catch (parseErr) { logger('grok:summarizer', `JSON parse failed for batch. Raw content: ${content.substring(0, 300)}...`, 'warn'); summaries = batchWithNotes.map(wo => ({ woNumber: wo.woNumber, summary: wo.summary || 'No summary available', status: wo.status, openedDate: wo.openedDate, totalInvoiceCost: wo.totalInvoiceCost })); } allSummaries.push(...summaries); logger('grok:summarizer', `Successfully summarized batch of ${batch.length} work orders`); } catch (err) { logger('grok:summarizer', `Batch summarization failed: ${err.message}`, 'error'); // Fallback: return basic info allSummaries.push(...batch.map(wo => ({ woNumber: wo.woNumber, summary: wo.summary || 'Summary unavailable due to error', status: wo.status, openedDate: wo.openedDate, totalInvoiceCost: wo.totalInvoiceCost }))); } } logger('grok:summarizer', `Completed summarization of ${allSummaries.length} work orders`); return allSummaries; } /** * Generate a Grok summary from a single ticket + notes */ export async function summarizeTicketWithGrok(ticket, notes = []) { logger('grok:summarizer', `Summarizing ticket ${ticket.Id || 'N/A'}`); try { let contextText = `Ticket ID: ${ticket.Id || 'N/A'}\n`; contextText += `Title/Description: ${ticket.Description || 'N/A'}\n`; contextText += `Trade: ${ticket.Trade || 'N/A'}\n`; let statusValue = 'N/A'; if (ticket.Status) { if (typeof ticket.Status === 'string') statusValue = ticket.Status; else if (typeof ticket.Status === 'object') { statusValue = ticket.Status.Primary || ticket.Status.Extended || 'N/A'; } } contextText += `Status: ${statusValue}\n`; contextText += `Priority: ${ticket.Priority || 'N/A'}\n`; contextText += `Location/Store: ${ticket.Location?.StoreId || 'N/A'}\n`; contextText += `Not-to-Exceed (NTE): $${ticket.Nte || 'N/A'}\n\n`; contextText += `Notes / Updates (chronological, most recent last):\n\n`; notes.forEach(note => { const date = new Date(note.date || '').toLocaleString('en-US', { timeZone: 'America/New_York', dateStyle: 'short', timeStyle: 'short' }); const author = note.createdBy || 'Unknown'; const type = note.NoteType || note.Type || 'Note'; contextText += `[${date}] ${author} (${type}):\n`; contextText += `${(note.text || '').trim()}\n`; contextText += '---\n'; }); const prompt = ` You are an expert HVAC/facilities technician and ServiceChannel ticket analyst. Summarize this ticket clearly and concisely. Use the **ticket description** as the primary source for the **main problem**. Use the notes to provide timeline, actions, status updates, and pending items. Structure your summary with these sections: - **Main Problem** (from description) - **Key Events & Timeline** (from notes) - **Actions Taken** - **Current Status / Blockers** - **Pending / Next Steps** Keep it professional, neutral, factual, under 250 words. Use bullet points where helpful. Ticket data & notes: ${contextText} `; const response = await axios.post( process.env.XAI_URL, { model: process.env.XAI_MODEL, messages: [ { role: 'system', content: 'You are a concise, technical ticket analyst.' }, { role: 'user', content: prompt } ], temperature: 0.3, max_tokens: 500, top_p: 0.95 }, { headers: { Authorization: `Bearer ${process.env.XAI_API_KEY}`, 'Content-Type': 'application/json' } } ); const summary = response.data.choices[0].message.content.trim(); logger('grok:summarizer', `Successfully summarized ticket ${ticket.Id || 'N/A'}`); return summary; } catch (error) { logger('grok:summarizer', `Grok summarization error for ticket ${ticket.Id || 'N/A'}: ${error.message}`, 'error'); return 'Summary unavailable due to error.'; } }