← back to Norma
app/api/agents/petition/generate/route.ts
117 lines
import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
function getGeminiUrl(): string {
const geminiApiKey = process.env.GEMINI_API_KEY;
if (!geminiApiKey) {
throw new Error('[agents/petition/generate] GEMINI_API_KEY env var is required');
}
return `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${geminiApiKey}`;
}
export async function POST(request: NextRequest) {
const auth = requireRole(request, 'admin', 'staff');
if (auth instanceof NextResponse) return auth;
try {
const body = await request.json();
const { org_id, target_type, target_id, title, issue_category } = body;
if (!org_id || !title) {
return NextResponse.json({ error: 'org_id and title are required' }, { status: 400 });
}
// Fetch organization details
const orgResult = await query(
'SELECT name, mission, key_issues, focus_populations, city, state FROM organizations WHERE id = $1',
[org_id],
);
if (orgResult.rowCount === 0) {
return NextResponse.json({ error: 'Organization not found' }, { status: 404 });
}
const org = orgResult.rows[0];
// Fetch target info if provided
let targetInfo = '';
if (target_id) {
const polResult = await query(
'SELECT name, title, party, state, district, office_level FROM politicians WHERE id = $1',
[target_id],
);
if (polResult.rowCount && polResult.rowCount > 0) {
const pol = polResult.rows[0];
targetInfo = `Target: ${pol.title || ''} ${pol.name} (${pol.party || 'Unknown'}-${pol.state || ''})${pol.district ? `, District ${pol.district}` : ''}, ${pol.office_level || 'federal'}`;
}
}
const prompt = `You are a professional petition writer for nonprofit organizations. Generate a compelling petition.
Organization: ${org.name}
Mission: ${org.mission || 'Not specified'}
Key Issues: ${(org.key_issues || []).join(', ') || 'Not specified'}
Focus Populations: ${(org.focus_populations || []).join(', ') || 'Not specified'}
Location: ${[org.city, org.state].filter(Boolean).join(', ') || 'National'}
Petition Title: ${title}
Target Type: ${target_type || 'general'}
${targetInfo}
Issue Category: ${issue_category || 'general advocacy'}
Generate a JSON response with these exact fields:
{
"petition_text": "The full petition text (2-4 paragraphs, formal but accessible tone, include specific asks and supporting data points)",
"talking_points": ["Array of 5-7 key talking points for supporters"],
"suggested_signatures_goal": number (reasonable goal based on the issue scope)
}
Return ONLY valid JSON, no markdown formatting.`;
const geminiResponse = await fetch(getGeminiUrl(), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contents: [{ parts: [{ text: prompt }] }],
generationConfig: {
temperature: 0.7,
maxOutputTokens: 2048,
},
}),
});
if (!geminiResponse.ok) {
const errText = await geminiResponse.text();
console.error('[agents/petition/generate] Gemini error:', errText);
return NextResponse.json({ error: 'AI generation failed' }, { status: 502 });
}
const geminiData = await geminiResponse.json();
const rawText = geminiData?.candidates?.[0]?.content?.parts?.[0]?.text || '';
// Parse JSON from response (strip any markdown code fences)
const jsonMatch = rawText.replace(/```json\s*/g, '').replace(/```\s*/g, '').trim();
let parsed;
try {
parsed = JSON.parse(jsonMatch);
} catch {
// If JSON parsing fails, return raw text as petition
parsed = {
petition_text: rawText,
talking_points: ['Contact your representatives', 'Share this petition', 'Spread awareness'],
suggested_signatures_goal: 5000,
};
}
return NextResponse.json({
petition_text: parsed.petition_text || rawText,
talking_points: parsed.talking_points || [],
suggested_signatures_goal: parsed.suggested_signatures_goal || 5000,
org_name: org.name,
target_info: targetInfo || null,
});
} catch (err) {
console.error('[agents/petition/generate] Error:', (err as Error).message);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}