← back to Patty

app/api/orbit/batch-link/route.ts

169 lines

import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';
import { verifyAuth } from '@/lib/auth';
import { callGemini } from '@/lib/gemini';

export async function POST(request: NextRequest) {
  const user = verifyAuth(request);
  if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });

  try {
    // Fetch top markets, petitions, and trending topics
    const [marketsRes, petitionsRes, trendingRes] = await Promise.all([
      query(`SELECT id, ticker, title, category, yes_bid, volume, petition_relevance
             FROM orbit_markets ORDER BY volume DESC NULLS LAST LIMIT 25`),
      query(`SELECT id, title, category, status FROM petitions ORDER BY created_at DESC LIMIT 20`),
      query(`SELECT id, title, content, category, tags FROM trending_topics ORDER BY engagement_score DESC LIMIT 15`),
    ]);

    const markets = marketsRes.rows;
    const petitions = petitionsRes.rows;
    const trending = trendingRes.rows;

    if (markets.length === 0) {
      return NextResponse.json({ error: 'No markets available to link' }, { status: 400 });
    }

    const marketSummary = markets.map((m, i) =>
      `M${i + 1}. [${m.ticker}] "${m.title}" (${m.category}, ${m.yes_bid}% yes, vol=${m.volume})`
    ).join('\n');

    const petitionSummary = petitions.length > 0
      ? petitions.map((p, i) => `P${i + 1}. "${p.title}" (${p.category}, ${p.status})`).join('\n')
      : 'No petitions yet.';

    const trendingSummary = trending.length > 0
      ? trending.map((t, i) => `T${i + 1}. "${t.title}" — ${(t.content || '').substring(0, 100)}`).join('\n')
      : 'No trending topics.';

    const prompt = `You are an AI analyst connecting prediction markets, petitions, and trending topics for an advocacy platform.

MARKETS:
${marketSummary}

PETITIONS:
${petitionSummary}

TRENDING TOPICS:
${trendingSummary}

Generate connections between these items. For each connection, provide:
1. market_index (M1, M2...) and petition_index (P1, P2...) OR trending_index (T1, T2...)
2. link_type: "influence" (market affects petition), "counter" (opposing), "support" (aligned), or "alliance" (unexpected connection)
3. strength: 0.0 to 1.0 (how strong is the connection)
4. ai_reasoning: one sentence explaining the connection

Also suggest NEW petition ideas from trending topics that don't have petitions yet.

Return a JSON object:
{
  "links": [
    { "market_idx": "M1", "petition_idx": "P1", "link_type": "influence", "strength": 0.8, "ai_reasoning": "..." },
    { "market_idx": "M3", "trending_idx": "T2", "link_type": "support", "strength": 0.7, "ai_reasoning": "..." }
  ],
  "suggested_petitions": [
    { "trending_idx": "T1", "title": "...", "summary": "...", "suggested_target": "...", "category": "..." }
  ]
}

Generate 15-25 links and 3-5 petition suggestions. Only include strong, meaningful connections (strength > 0.4).
Return ONLY valid JSON, no markdown.`;

    type BatchLinkResult = {
      links: Array<{
        market_idx: string;
        petition_idx?: string;
        trending_idx?: string;
        link_type: string;
        strength: number;
        ai_reasoning: string;
      }>;
      suggested_petitions: Array<{
        trending_idx: string;
        title: string;
        summary: string;
        suggested_target: string;
        category: string;
      }>;
    };

    // 2026-05-05 (tick 29): migrated to lib/gemini.ts wrapper.
    const r = await callGemini<BatchLinkResult>({ prompt, maxTokens: 4096, temperature: 0.7 });
    if (!r.ok) {
      console.error('[batch-link] gemini failed:', r.reason, r.detail);
      return NextResponse.json(
        { error: r.reason === 'no_key' ? 'AI service not configured' : 'AI analysis failed' },
        { status: r.status },
      );
    }
    const result = r.data;

    // Insert market-petition links
    let linksInserted = 0;
    for (const link of (result.links || [])) {
      const mIdx = parseInt(link.market_idx?.replace('M', '')) - 1;
      const market = markets[mIdx];
      if (!market) continue;

      // Link to petition
      if (link.petition_idx) {
        const pIdx = parseInt(link.petition_idx.replace('P', '')) - 1;
        const petition = petitions[pIdx];
        if (!petition) continue;

        try {
          await query(`
            INSERT INTO orbit_links (market_id, petition_id, link_type, strength, ai_reasoning)
            VALUES ($1, $2, $3, $4, $5)
            ON CONFLICT (market_id, petition_id) DO UPDATE SET
              link_type = $3, strength = $4, ai_reasoning = $5
          `, [market.id, petition.id, link.link_type || 'influence', link.strength || 0.5, link.ai_reasoning || '']);
          linksInserted++;
        } catch (e) {
          console.error('[api/orbit/batch-link] Link insert error:', (e as Error).message);
        }
      }
    }

    // Create suggested petitions from trending topics
    let petitionsCreated = 0;
    for (const sp of (result.suggested_petitions || [])) {
      const tIdx = parseInt(sp.trending_idx?.replace('T', '')) - 1;
      const topic = trending[tIdx];
      if (!topic || !sp.title) continue;

      try {
        await query(`
          INSERT INTO petitions (title, summary, category, status, target_name)
          VALUES ($1, $2, $3, 'draft', $4)
        `, [sp.title, sp.summary || '', sp.category || 'policy', sp.suggested_target || '']);
        petitionsCreated++;
      } catch (e) {
        console.error('[api/orbit/batch-link] Petition insert error:', (e as Error).message);
      }
    }

    // Update petition_relevance on markets
    for (const link of (result.links || [])) {
      const mIdx = parseInt(link.market_idx?.replace('M', '')) - 1;
      const market = markets[mIdx];
      if (!market) continue;
      const relevance = Math.round((link.strength || 0.5) * 100);
      try {
        await query(`UPDATE orbit_markets SET petition_relevance = GREATEST(petition_relevance, $1) WHERE id = $2`,
          [relevance, market.id]);
      } catch { /* ignore */ }
    }

    return NextResponse.json({
      links_inserted: linksInserted,
      petitions_created: petitionsCreated,
      total_links_analyzed: (result.links || []).length,
      total_suggestions: (result.suggested_petitions || []).length,
    });
  } catch (err) {
    console.error('[api/orbit/batch-link] Error:', (err as Error).message);
    return NextResponse.json({ error: 'Failed to generate links' }, { status: 500 });
  }
}