import { NextResponse } from 'next/server'; import Anthropic from '@anthropic-ai/sdk'; import postgresClient from '@/lib/services/postgres-client'; const MODEL = 'claude-sonnet-4-6'; const SYSTEM = 'You are a senior MSP consultant. Respond with valid JSON only — no markdown fences, no prose before or after the JSON object.'; export async function POST() { const apiKey = process.env.ANTHROPIC_API_KEY; if (!apiKey) { return NextResponse.json({ error: 'ANTHROPIC_API_KEY not configured' }, { status: 503 }); } try { const [statsRes, categoryRes, resolutionRes, skillsRes, complexityRes, proceduresRes] = await Promise.all([ postgresClient.query(` SELECT COUNT(*) AS total_analyzed, ROUND(AVG(hours_worked)::numeric, 2) AS avg_hours, ROUND(100.0 * COUNT(*) FILTER (WHERE same_day_close) / NULLIF(COUNT(*), 0), 1) AS same_day_pct, ROUND(100.0 * COUNT(*) FILTER (WHERE preventable = true) / NULLIF(COUNT(*) FILTER (WHERE preventable IS NOT NULL), 0), 1) AS preventable_pct, ROUND(100.0 * COUNT(*) FILTER (WHERE device_was_offline = true) / NULLIF(COUNT(*) FILTER (WHERE device_was_offline IS NOT NULL), 0), 1) AS offline_pct, ROUND(100.0 * COUNT(*) FILTER (WHERE backup_completed_before_tech = true) / NULLIF(COUNT(*) FILTER (WHERE backup_completed_before_tech IS NOT NULL), 0), 1) AS auto_resolved_pct, SUM(hours_worked) AS total_hours FROM veeam_ticket_analysis WHERE ticket_created_at >= DATE_TRUNC('year', NOW()) `), postgresClient.query(` SELECT problem_category, COUNT(*) AS count, ROUND(AVG(hours_worked)::numeric, 2) AS avg_hours, ROUND(100.0 * COUNT(*) FILTER (WHERE same_day_close) / NULLIF(COUNT(*), 0), 1) AS same_day_pct, ROUND(100.0 * COUNT(*) FILTER (WHERE preventable = true) / NULLIF(COUNT(*) FILTER (WHERE preventable IS NOT NULL), 0), 1) AS preventable_pct, ROUND(100.0 * COUNT(*) FILTER (WHERE device_was_offline = true) / NULLIF(COUNT(*) FILTER (WHERE device_was_offline IS NOT NULL), 0), 1) AS offline_pct FROM veeam_ticket_analysis WHERE ticket_created_at >= DATE_TRUNC('year', NOW()) GROUP BY problem_category ORDER BY count DESC `), postgresClient.query(` SELECT resolution_type, COUNT(*) AS count FROM veeam_ticket_analysis WHERE ticket_created_at >= DATE_TRUNC('year', NOW()) GROUP BY resolution_type ORDER BY count DESC `), postgresClient.query(` SELECT skill, COUNT(*) AS count FROM veeam_ticket_analysis, unnest(skills_required) AS skill WHERE ticket_created_at >= DATE_TRUNC('year', NOW()) GROUP BY skill ORDER BY count DESC LIMIT 20 `), postgresClient.query(` SELECT complexity, COUNT(*) AS count FROM veeam_ticket_analysis WHERE ticket_created_at >= DATE_TRUNC('year', NOW()) GROUP BY complexity ORDER BY CASE complexity WHEN 'trivial' THEN 1 WHEN 'low' THEN 2 WHEN 'medium' THEN 3 WHEN 'high' THEN 4 ELSE 5 END `), // Sample recommended procedures per category (up to 3 per category) postgresClient.query(` SELECT problem_category, recommended_procedure FROM ( SELECT problem_category, recommended_procedure, ROW_NUMBER() OVER (PARTITION BY problem_category ORDER BY analyzed_at DESC) AS rn FROM veeam_ticket_analysis WHERE ticket_created_at >= DATE_TRUNC('year', NOW()) AND recommended_procedure IS NOT NULL AND recommended_procedure != '' ) ranked WHERE rn <= 3 ORDER BY problem_category, rn `), ]); const stats = statsRes.rows[0]; const categories = categoryRes.rows; const resolutions = resolutionRes.rows; const skills = skillsRes.rows; const complexity = complexityRes.rows; const procedures = proceduresRes.rows; if (parseInt(stats.total_analyzed) === 0) { return NextResponse.json({ error: 'No analyzed tickets yet — run the analysis first.' }, { status: 400 }); } const procByCategory: Record = {}; for (const p of procedures) { if (!procByCategory[p.problem_category]) procByCategory[p.problem_category] = []; procByCategory[p.problem_category].push(p.recommended_procedure); } const prompt = `Review this YTD backup ticket data for a managed services provider and produce a practical operations summary. ## Dataset - Tickets analyzed: ${stats.total_analyzed} (YTD, all had tech time logged) - Total tech time: ${parseFloat(stats.total_hours ?? 0).toFixed(1)}h - Avg hours per ticket: ${stats.avg_hours}h - Same-day close rate: ${stats.same_day_pct}% - Device was offline at alert time: ${stats.offline_pct}% - Backup auto-completed before tech action: ${stats.auto_resolved_pct}% - Assessed as preventable: ${stats.preventable_pct}% ## Problem Categories ${categories.map(c => `- ${c.problem_category}: ${c.count} tickets, avg ${c.avg_hours}h, ${c.same_day_pct}% same-day close, ${c.offline_pct ?? 0}% offline at time`).join('\n')} ## Resolution Types ${resolutions.map(r => `- ${r.resolution_type}: ${r.count} tickets`).join('\n')} ## Complexity Breakdown ${complexity.map(c => `- ${c.complexity}: ${c.count} tickets`).join('\n')} ## Top Skills Required (by frequency) ${skills.map(s => `- ${s.skill}: ${s.count} tickets`).join('\n')} ## Sample AI-Generated SOP Steps (per category) ${Object.entries(procByCategory).map(([cat, procs]) => `${cat}:\n${procs.map(p => ` • ${p}`).join('\n')}` ).join('\n')} --- Return a JSON object with exactly this structure: { "headline": "one sentence executive summary", "key_findings": ["3-4 bullet point findings with specific numbers"], "issue_breakdown": [ { "category": "exact machine key from Problem Categories above (e.g. device_offline, agent_issue)", "insight": "1-2 sentence insight about this category", "sop": "concrete, actionable SOP recommendation for handling this type" } ], "skills_assessment": "2-3 sentences on the skills picture — what's needed most, any gaps", "quick_wins": ["2-3 specific things the MSP could do to reduce ticket volume or time spent"], "automation_opportunities": "1-2 sentences on what could be automated or suppressed", "training_priority": "one sentence on the highest-value training investment" } Be specific and direct. Reference actual numbers from the data. Avoid generic MSP advice.`; const client = new Anthropic({ apiKey }); const message = await client.messages.create({ model: MODEL, max_tokens: 4000, system: SYSTEM, messages: [{ role: 'user', content: prompt }], }); if (message.stop_reason === 'max_tokens') { console.error('[VEEAM-SUMMARY] Response truncated at max_tokens'); return NextResponse.json({ error: 'Model response was truncated — try again' }, { status: 502 }); } const raw = message.content[0].type === 'text' ? message.content[0].text : '{}'; const start = raw.indexOf('{'); const end = raw.lastIndexOf('}'); const json = start !== -1 && end > start ? raw.slice(start, end + 1) : raw; let analysis: Record = {}; try { analysis = JSON.parse(json); } catch (e) { console.error('[VEEAM-SUMMARY] JSON parse failed. Raw response:', raw); return NextResponse.json({ error: 'Model returned non-JSON response', raw }, { status: 502 }); } return NextResponse.json({ analysis, model: MODEL, generated_at: new Date().toISOString() }); } catch (e: any) { console.error('[VEEAM-SUMMARY] Unexpected error:', e); return NextResponse.json({ error: e.message ?? 'Internal server error' }, { status: 500 }); } }