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