← back to Norma

app/api/gmail/draft-reply/route.ts

127 lines

import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { auditLog } from '@/lib/audit';

/**
 * POST /api/gmail/draft-reply
 *
 * AI-generate a reply to an email using Gemini.
 * Body: { messageId: string, tone?: string, context?: string }
 *
 * Returns: { needs: string, nextActions: string[], draft: string }
 */
export async function POST(request: NextRequest) {
  const auth = requireRole(request, 'admin', 'staff');
  if (auth instanceof NextResponse) return auth;

  const key = process.env.GEMINI_API_KEY;
  if (!key) {
    return NextResponse.json({ error: 'GEMINI_API_KEY not configured' }, { status: 500 });
  }

  const body = await request.json();
  const { messageId, tone, context } = body;

  if (!messageId) {
    return NextResponse.json({ error: 'messageId is required' }, { status: 400 });
  }

  // Fetch the original message
  const msgRes = await query(
    `SELECT id, subject, from_address, to_addresses, body_text, snippet, date_sent
     FROM gmail_messages WHERE id = $1`,
    [messageId],
  );

  if (!msgRes.rows[0]) {
    return NextResponse.json({ error: 'Message not found' }, { status: 404 });
  }

  const msg = msgRes.rows[0];
  const bodyText = msg.body_text || msg.snippet || '(no content)';

  // Build AI prompt
  const prompt = `You are an AI assistant for a nonprofit organization. Read this email and:
1. Determine what the sender needs (phone number, appointment, more info, action, etc.)
2. Draft a professional reply
3. Recommend next actions

Original email:
From: ${msg.from_address}
Subject: ${msg.subject || '(no subject)'}
Date: ${msg.date_sent || 'unknown'}
Body: ${bodyText.slice(0, 3000)}

${context ? `Additional context: ${context}` : ''}

Tone: ${tone || 'professional'}

Reply format — you MUST use these exact labels:
NEEDS: [what the sender needs — one brief sentence]
NEXT_ACTIONS: [recommended actions, comma-separated]
DRAFT:
[the reply text — professional email body only, no greeting line with the name unless you can determine it from the email]`;

  const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${key}`;

  const geminiRes = await fetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      contents: [{ role: 'user', parts: [{ text: prompt }] }],
      generationConfig: { temperature: 0.6, maxOutputTokens: 2048 },
    }),
  });

  if (!geminiRes.ok) {
    const errText = await geminiRes.text();
    console.error('[gmail/draft-reply] Gemini error:', errText.slice(0, 300));
    return NextResponse.json(
      { error: `AI generation failed: ${geminiRes.status}` },
      { status: 502 },
    );
  }

  const data = await geminiRes.json();
  const reply = data.candidates?.[0]?.content?.parts?.[0]?.text || '';

  // Parse the structured response
  let needs = '';
  let nextActions: string[] = [];
  let draft = '';

  const needsMatch = reply.match(new RegExp('NEEDS:\\s*(.+?)(?:\\n|NEXT_ACTIONS:)', 's'));
  if (needsMatch) {
    needs = needsMatch[1].trim();
  }

  const actionsMatch = reply.match(new RegExp('NEXT_ACTIONS:\\s*(.+?)(?:\\n|DRAFT:)', 's'));
  if (actionsMatch) {
    nextActions = actionsMatch[1]
      .split(',')
      .map((a: string) => a.trim())
      .filter(Boolean);
  }

  const draftMatch = reply.match(/DRAFT:\s*\n?([\s\S]+)$/);
  if (draftMatch) {
    draft = draftMatch[1].trim();
  }

  // Fallback if parsing failed
  if (!draft && !needs) {
    draft = reply;
    needs = 'Unable to determine — see draft';
    nextActions = ['Review and send manually'];
  }

  await auditLog('gmail.ai-draft', 'message', messageId, {
    tone: tone || 'professional',
    needsLength: needs.length,
    draftLength: draft.length,
  });

  return NextResponse.json({ needs, nextActions, draft });
}