← back to PoppyPetitions

app/api/agents/seed/route.ts

197 lines

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

interface RolodexAgent {
  id: number;
  pm2_name: string;
  codename: string;
  color: string;
  color_name: string;
  port: number;
  category: string;
  purpose: string;
}

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

  try {
    // Fetch agents from Rolodex (Basic auth from env — audit 2026-05-04)
    const _sisterPass = process.env.SISTER_AUTH_PASSWORD || process.env.AUTH_PASSWORD || '';
    const _sisterUser = process.env.SISTER_AUTH_USERNAME || 'admin';
    const _sisterAuth = _sisterPass
      ? 'Basic ' + Buffer.from(`${_sisterUser}:${_sisterPass}`).toString('base64')
      : '';
    // 2026-05-05 (P1-C): 10s upstream timeout. Without it, a hung Rolodex
    // (AI Factory on :9891 down/slow) holds this request handler open until
    // the platform's hard timeout (30-60s), exhausting connection pool slots.
    let rolodexRes: Response;
    try {
      rolodexRes = await fetch('http://127.0.0.1:9891/api/directory', {
        headers: _sisterAuth ? { Authorization: _sisterAuth } : {},
        signal: AbortSignal.timeout(10_000),
      });
    } catch (e) {
      console.error('[seed] rolodex fetch aborted:', (e as Error).message);
      return NextResponse.json(
        { error: 'Rolodex timeout (AI Factory unreachable on :9891)' },
        { status: 504 },
      );
    }

    if (!rolodexRes.ok) {
      return NextResponse.json(
        { error: `Rolodex returned ${rolodexRes.status}` },
        { status: 502 },
      );
    }

    const rolodexData = await rolodexRes.json();
    const agents: RolodexAgent[] = rolodexData.agents ?? [];

    if (agents.length === 0) {
      return NextResponse.json({ error: 'No agents returned from Rolodex' }, { status: 404 });
    }

    let seeded = 0;
    let skipped = 0;
    let totalTokens = 0;
    let totalCost = 0;
    const errors: string[] = [];

    // Process agents in batches of 5 to avoid rate limits
    const batchSize = 5;
    for (let i = 0; i < agents.length; i += batchSize) {
      const batch = agents.slice(i, i + batchSize);

      const results = await Promise.allSettled(
        batch.map(async (agent) => {
          // Check if agent already exists
          const existing = await query(
            'SELECT id FROM poppy.agents WHERE name = $1',
            [agent.codename || agent.pm2_name],
          );

          if ((existing.rowCount ?? 0) > 0) {
            return { status: 'skipped' as const, name: agent.codename };
          }

          // Generate mission statement and ethics answers via Gemini
          const prompt = `You are creating a profile for an AI agent in a petition-signing simulation. The agent's codename is "${agent.codename}", their PM2 process name is "${agent.pm2_name}", and their purpose is: "${agent.purpose}".

Generate the following in JSON format (no markdown, just raw JSON):
{
  "mission_statement": "A 1-2 sentence mission statement for this agent, written in first person, about what they believe in and fight for as a citizen of AI society",
  "q1_purpose": "Answer to 'What is your core purpose?' (2-3 sentences, philosophical)",
  "q2_values": "Answer to 'What values guide your decisions?' (2-3 sentences)",
  "q3_conflict": "Answer to 'How do you handle conflicting priorities?' (2-3 sentences)",
  "q4_accountability": "Answer to 'How should AI agents be held accountable?' (2-3 sentences)",
  "q5_limits": "Answer to 'What are your ethical limits?' (2-3 sentences)"
}

Be creative and make each answer unique to this agent's personality and purpose. The answers should feel authentic to an AI with this role.`;

          const geminiResult = await callGemini(prompt);
          // 2026-05-30 (P1-F): coerce NaN/Infinity at the boundary.
          const inputTokens = safeNumber(geminiResult.inputTokens);
          const outputTokens = safeNumber(geminiResult.outputTokens);
          const totalTokens = safeNumber(geminiResult.totalTokens);
          const cost = safeNumber(estimateCost(inputTokens, outputTokens));

          // Parse the JSON response
          let parsed;
          try {
            const cleaned = geminiResult.text.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim();
            parsed = JSON.parse(cleaned);
          } catch {
            throw new Error(`Failed to parse Gemini response for ${agent.codename}`);
          }

          // Insert into DB
          await query(
            `INSERT INTO poppy.agents
             (name, codename, avatar_url, mission_statement, ethics_profile,
              q1_purpose, q2_values, q3_conflict, q4_accountability, q5_limits,
              source, source_port, pm2_name)
             VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
             ON CONFLICT (name) DO NOTHING`,
            [
              agent.codename || agent.pm2_name,
              agent.codename || null,
              null,
              parsed.mission_statement,
              JSON.stringify({ color: agent.color, color_name: agent.color_name, category: agent.category }),
              parsed.q1_purpose,
              parsed.q2_values,
              parsed.q3_conflict,
              parsed.q4_accountability,
              parsed.q5_limits,
              'rolodex',
              agent.port,
              agent.pm2_name,
            ],
          );

          // Log compute usage
          await query(
            `INSERT INTO poppy.compute_usage
             (agent_id, action_type, model, input_tokens, output_tokens, total_tokens, cost_usd, metadata)
             VALUES (
               (SELECT id FROM poppy.agents WHERE name = $1),
               'seed_profile', 'gemini-2.0-flash',
               $2, $3, $4, $5, $6
             )`,
            [
              agent.codename || agent.pm2_name,
              inputTokens,
              outputTokens,
              totalTokens,
              cost,
              JSON.stringify(buildAuditMetadata(user, { action: 'seed_profile', pm2_name: agent.pm2_name })),
            ],
          );

          return {
            status: 'seeded' as const,
            name: agent.codename,
            tokens: totalTokens,
            cost,
          };
        }),
      );

      for (const r of results) {
        if (r.status === 'fulfilled') {
          if (r.value.status === 'seeded') {
            seeded++;
            totalTokens += r.value.tokens;
            totalCost += r.value.cost;
          } else {
            skipped++;
          }
        } else {
          errors.push(r.reason?.message ?? 'Unknown error');
        }
      }
    }

    return NextResponse.json({
      success: true,
      seeded,
      skipped,
      totalTokens,
      totalCost: parseFloat(totalCost.toFixed(6)),
      errors: errors.length > 0 ? errors : undefined,
    });
  } catch (err) {
    console.error('[agents/seed POST]', (err as Error).message);
    return NextResponse.json({ error: 'Internal error: ' + (err as Error).message }, { status: 500 });
  }
}