← back to Norma

app/api/collaborations/discover/route.ts

154 lines

import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { getOrgId } from '@/lib/orgId';
import { getBrand } from '@/lib/brand';

const GEMINI_URL = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=' + (process.env.GEMINI_API_KEY || '');

/**
 * POST /api/collaborations/discover
 * Uses AI to discover potential collaborations based on the org's mission.
 * Body: { type: 'nonprofit' | 'politician' | 'corporation' | 'municipality' }
 */
export async function POST(request: NextRequest) {
  const auth = requireRole(request, 'admin', 'staff');
  if (auth instanceof NextResponse) return auth;

  const orgId = auth.role === 'admin' ? getOrgId(request) : auth.orgId;

  try {
    const body = await request.json();
    const collabType = body.type || 'nonprofit';

    // Get existing collaborations to avoid duplicates
    const existing = await query<{ name: string }>(
      'SELECT name FROM collaborations WHERE collab_type = $1',
      [collabType]
    );
    const existingNames = existing.rows.map(function(r) { return r.name; });

    // Get recent news for context (scoped to org)
    const newsRes = orgId
      ? await query<{ headline: string; outlet: string }>(
          'SELECT headline, outlet FROM news_items WHERE org_id = $1 ORDER BY published_at DESC NULLS LAST LIMIT 15',
          [orgId]
        )
      : await query<{ headline: string; outlet: string }>(
          'SELECT headline, outlet FROM news_items ORDER BY published_at DESC NULLS LAST LIMIT 15'
        );

    const brand = await getBrand();
    const focusDesc = brand.focusAreas.join(', ');

    const typePrompts: Record<string, string> = {
      nonprofit: `non-profit organizations that work on ${focusDesc}, or related policy issues. Include organizations with similar missions and advocacy groups.`,
      politician: `U.S. elected officials (senators, representatives, governors, state legislators) who have been vocal about ${focusDesc}. Include their party, state, and relevant committee assignments. Focus on those active in relevant committees or who have introduced related legislation.`,
      corporation: `corporations and companies that offer related benefits programs or have expressed corporate social responsibility interest in ${focusDesc}. Include employers known for relevant assistance programs.`,
      municipality: `cities, counties, and local government bodies that have discussed ${focusDesc}, created relevant local programs, passed resolutions, or addressed these issues in their meetings.`,
    };

    const prompt = 'You are a research assistant for ' + brand.name + ' (' + brand.shortName + '), a non-profit focused on ' + focusDesc + '.\n\n'
      + 'Generate a list of 8-10 ' + typePrompts[collabType] + '\n\n'
      + 'For each, provide:\n'
      + '- name: Full name (of person or organization)\n'
      + '- title: Their title/role (or null for orgs)\n'
      + '- organization: Their org (for politicians, their office/chamber)\n'
      + '- state: State abbreviation (if applicable)\n'
      + '- district: District info (for politicians)\n'
      + '- party: Political party (for politicians only)\n'
      + '- website_url: Their website\n'
      + '- focus_areas: Array of 2-3 relevant focus areas\n'
      + '- ai_reason: 2-3 sentences explaining why ' + brand.shortName + ' should collaborate with them\n'
      + '- ai_relevance: Score 0.0-1.0 of how relevant they are to ' + focusDesc + '\n'
      + '- ai_talking_points: Array of 2-3 suggested conversation starters\n\n'
      + 'EXCLUDE these already-known names: ' + existingNames.join(', ') + '\n\n'
      + 'Recent news context for relevance:\n'
      + newsRes.rows.map(function(n) { return '- ' + n.headline; }).join('\n') + '\n\n'
      + 'Return ONLY valid JSON: { "results": [ { name, title, organization, state, district, party, website_url, focus_areas, ai_reason, ai_relevance, ai_talking_points } ] }';

    const geminiRes = await fetch(GEMINI_URL, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        contents: [{ parts: [{ text: prompt }] }],
        generationConfig: {
          temperature: 0.7,
          maxOutputTokens: 4096,
          responseMimeType: 'application/json',
        },
      }),
    });

    if (!geminiRes.ok) {
      const errText = await geminiRes.text();
      return NextResponse.json({ error: 'Gemini API error: ' + errText.slice(0, 200) }, { status: 502 });
    }

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

    let results: Array<{
      name: string;
      title?: string;
      organization?: string;
      state?: string;
      district?: string;
      party?: string;
      website_url?: string;
      focus_areas?: string[];
      ai_reason?: string;
      ai_relevance?: number;
      ai_talking_points?: string[];
    }> = [];

    try {
      const parsed = JSON.parse(text);
      results = parsed.results || parsed;
    } catch {
      return NextResponse.json({ error: 'Failed to parse AI response' }, { status: 500 });
    }

    // Insert into DB
    let inserted = 0;
    for (const r of results) {
      if (!r.name) continue;
      // Skip duplicates
      if (existingNames.some(function(n) { return n.toLowerCase() === r.name.toLowerCase(); })) continue;

      try {
        await query(
          'INSERT INTO collaborations (collab_type, name, title, organization, website_url, state, district, party, focus_areas, ai_reason, ai_relevance, ai_talking_points, status) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13)',
          [
            collabType,
            r.name,
            r.title || null,
            r.organization || null,
            r.website_url || null,
            r.state || null,
            r.district || null,
            r.party || null,
            r.focus_areas || null,
            r.ai_reason || null,
            r.ai_relevance || null,
            r.ai_talking_points || null,
            'suggested',
          ]
        );
        inserted++;
      } catch {
        // Skip duplicates
      }
    }

    return NextResponse.json({
      success: true,
      discovered: results.length,
      inserted: inserted,
      type: collabType,
    });
  } catch (err) {
    return NextResponse.json({ error: (err as Error).message }, { status: 500 });
  }
}