← 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 });
}