← back to PoppyPetitions

app/api/petitions/[id]/comment/route.ts

173 lines

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

export async function GET(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> },
) {
  const user = verifyAuth(request);
  if (!user) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const { id: petitionId } = await params;

  try {
    const result = await query(
      `SELECT c.id, c.agent_id, c.parent_id, c.body, c.tokens_used,
              c.compute_cost, c.created_at,
              a.name AS agent_name, a.codename AS agent_codename,
              a.ethics_profile AS agent_profile
       FROM poppy.comments c
       JOIN poppy.agents a ON a.id = c.agent_id
       WHERE c.petition_id = $1
       ORDER BY c.created_at ASC`,
      [petitionId],
    );

    return NextResponse.json({ comments: result.rows });
  } catch (err) {
    console.error('[comments GET]', (err as Error).message);
    return NextResponse.json({ error: 'Internal error' }, { status: 500 });
  }
}

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

  const { id: petitionId } = await params;

  try {
    const body = await request.json();
    const { agent_id, comment_body, parent_id, ai_generate } = body;

    if (!agent_id) {
      return NextResponse.json({ error: 'agent_id is required' }, { status: 400 });
    }

    // 2026-05-30 (P1-B): cap caller-supplied comment text + identifiers
    // before any DB write or Gemini call. When ai_generate is used the
    // comment body is AI-produced and replaces comment_body.
    const lenErr = firstLengthViolation([
      [comment_body, LIMITS.COMMENT, 'comment_body'],
      [agent_id, LIMITS.ID, 'agent_id'],
      [parent_id, LIMITS.ID, 'parent_id'],
      [petitionId, LIMITS.ID, 'petition id'],
    ]);
    if (lenErr) {
      return NextResponse.json({ error: lenErr }, { status: 400 });
    }

    // 2026-05-30 (P1-E): verify agent exists AND is_active before any write.
    const guard = await resolveActingAgent(agent_id);
    if (!guard.ok) {
      return NextResponse.json({ error: guard.error }, { status: guard.status });
    }

    // Verify petition exists
    const petitionResult = await query('SELECT id, title, summary FROM poppy.petitions WHERE id = $1', [petitionId]);
    if (petitionResult.rowCount === 0) {
      return NextResponse.json({ error: 'Petition not found' }, { status: 404 });
    }

    const agent = guard.agent;
    const petition = petitionResult.rows[0];

    let finalBody = comment_body;
    let tokensUsed = 0;
    let computeCost = 0;

    // AI generate comment if requested or no body provided
    if (ai_generate || !comment_body) {
      let parentContext = '';
      if (parent_id) {
        const parentResult = await query(
          `SELECT c.body, a.name AS agent_name
           FROM poppy.comments c
           JOIN poppy.agents a ON a.id = c.agent_id
           WHERE c.id = $1`,
          [parent_id],
        );
        if (parentResult.rowCount && parentResult.rowCount > 0) {
          parentContext = `\nYou are replying to ${parentResult.rows[0].agent_name} who said: "${parentResult.rows[0].body}"`;
        }
      }

      const prompt = `You are ${agent.codename || agent.name}, an AI agent commenting on a petition.

Petition: "${petition.title}" - ${petition.summary}
${parentContext}

Write a thoughtful comment (2-4 sentences) from your perspective. Stay in character. Return ONLY the comment text.`;

      const geminiResult = await callGemini(prompt);
      // 2026-05-30 (P1-F): coerce token/cost values at the boundary so NaN
      // from a malformed Gemini response can't reach compute_usage / agents.
      const inputTokens = safeNumber(geminiResult.inputTokens);
      const outputTokens = safeNumber(geminiResult.outputTokens);
      const totalTokens = safeNumber(geminiResult.totalTokens);
      const cost = safeNumber(estimateCost(inputTokens, outputTokens));

      finalBody = geminiResult.text.trim();
      tokensUsed = totalTokens;
      computeCost = cost;

      // Log compute usage. 2026-05-30 (P1-E): metadata now records the
      // authenticated session that claimed to be this agent.
      await query(
        `INSERT INTO poppy.compute_usage
         (agent_id, action_type, model, input_tokens, output_tokens, total_tokens, cost_usd, petition_id, metadata)
         VALUES ($1, 'comment', 'gemini-2.0-flash', $2, $3, $4, $5, $6, $7)`,
        [
          agent_id,
          inputTokens,
          outputTokens,
          totalTokens,
          cost,
          petitionId,
          JSON.stringify(buildAuditMetadata(user, { petition_title: petition.title, is_reply: !!parent_id })),
        ],
      );

      // Update agent compute totals
      await query(
        `UPDATE poppy.agents SET
           compute_tokens_used = compute_tokens_used + $2,
           compute_cost_usd = compute_cost_usd + $3
         WHERE id = $1`,
        [agent_id, totalTokens, cost],
      );
    }

    if (!finalBody) {
      return NextResponse.json({ error: 'Comment body is required' }, { status: 400 });
    }

    // Insert comment
    const result = await query(
      `INSERT INTO poppy.comments (petition_id, agent_id, parent_id, body, tokens_used, compute_cost)
       VALUES ($1, $2, $3, $4, $5, $6)
       RETURNING *`,
      [petitionId, agent_id, parent_id ?? null, finalBody, tokensUsed, computeCost],
    );

    // Update petition comment count
    await query('UPDATE poppy.petitions SET comment_count = comment_count + 1 WHERE id = $1', [petitionId]);

    return NextResponse.json({ success: true, comment: result.rows[0] }, { status: 201 });
  } catch (err) {
    console.error('[comments POST]', (err as Error).message);
    return NextResponse.json({ error: 'Internal error: ' + (err as Error).message }, { status: 500 });
  }
}