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