← back to Norma

agents/moveon-agent/skills/post.js

157 lines

/**
 * MoveOn Post Skill
 *
 * Prepares petition data for MoveOn.org submission.
 * Full browser automation would require Playwright — this skill
 * prepares the petition draft, stores it in the pipeline, and
 * returns the MoveOn URL template for manual or automated posting.
 *
 * Rate limit: 1/day
 */

const { RateLimiter } = require('../../shared/rate-limiter');
const { logAction } = require('../../shared/audit-logger');
const { generatePetitionContent } = require('../../shared/ai');
const { preparePetitionDraft } = require('../lib/moveon');
const { query: dbQuery } = require('../../shared/db');

const AGENT = 'moveon-agent';
const PLATFORM = 'moveon';

const rateLimiter = new RateLimiter(AGENT, PLATFORM);

/**
 * @param {Object} body - Request body
 * @param {string} [body.title] - Petition title (auto-generated if not provided)
 * @param {string} [body.body] - Petition body (auto-generated if not provided)
 * @param {string} [body.target='U.S. Department of Education'] - Petition target
 * @param {string} [body.category='Education'] - Petition category
 * @param {string} [body.topic='civic advocacy'] - Topic for AI generation
 * @param {string} [body.tone='urgent'] - Tone for AI generation
 * @param {string} [body.pipeline_id] - Existing pipeline entry ID to use
 * @returns {Promise<Object>}
 */
module.exports = async function post(body) {
  // Check rate limit
  const limit = await rateLimiter.checkLimit('post');
  if (!limit.allowed) {
    await logAction({
      agent: AGENT,
      actionType: 'post',
      platform: PLATFORM,
      status: 'rate_limited',
      errorMessage: `Rate limited. Retry after ${Math.ceil(limit.retryAfterMs / 1000)}s`,
    });

    return {
      rate_limited: true,
      retry_after_seconds: Math.ceil(limit.retryAfterMs / 1000),
      current: limit.current,
      max: limit.max,
    };
  }

  let title = body.title;
  let petitionBody = body.body;
  let target = body.target || 'U.S. Department of Education';
  let category = body.category || 'Education';
  let talkingPoints = [];
  let aiConfidence = null;

  // If no title/body provided, generate via AI
  if (!title || !petitionBody) {
    const topic = body.topic || 'civic advocacy';
    const tone = body.tone || 'urgent';

    console.log(`[${AGENT}] Generating petition content via AI for topic: "${topic}"`);

    const generated = await generatePetitionContent({
      topic,
      target,
      category,
      tone,
    });

    title = title || generated.title;
    petitionBody = petitionBody || generated.body;
    talkingPoints = generated.talking_points || [];
    aiConfidence = generated.confidence || null;
    target = generated.target || target;
  }

  // Prepare the MoveOn draft
  const draft = preparePetitionDraft({
    title,
    body: petitionBody,
    target,
    category,
  });

  // Store in petition_pipeline if not already linked
  let pipelineId = body.pipeline_id || null;

  if (!pipelineId) {
    try {
      const result = await dbQuery(
        `INSERT INTO petition_pipeline
          (title, body, target, category, platform, tone, talking_points, status, ai_confidence, source_type)
         VALUES ($1, $2, $3, $4, $5, $6, $7, 'draft', $8, 'agent')
         RETURNING id`,
        [
          title,
          petitionBody,
          target,
          category,
          PLATFORM,
          body.tone || 'urgent',
          talkingPoints,
          aiConfidence,
        ]
      );
      pipelineId = result.rows[0].id;
    } catch (err) {
      console.error(`[${AGENT}] Failed to insert pipeline entry:`, err.message);
    }
  }

  // Record the action
  await rateLimiter.recordAction('post');
  await logAction({
    agent: AGENT,
    actionType: 'post',
    platform: PLATFORM,
    content: title,
    petitionId: pipelineId,
    targetUrl: draft.previewUrl,
    responseData: {
      title,
      target,
      category,
      aiGenerated: !body.title || !body.body,
      aiConfidence,
      talkingPoints,
      pipelineId,
    },
    status: 'success',
  });

  return {
    petition: {
      title,
      body: petitionBody,
      target,
      category,
      talking_points: talkingPoints,
      ai_confidence: aiConfidence,
    },
    draft: draft.formFields,
    moveon_url: draft.previewUrl,
    instructions: draft.instructions,
    pipeline_id: pipelineId,
    status: 'draft_prepared',
    simulated: true,
    posted: false,
    note: 'Petition draft prepared in simulation mode. Full browser automation requires Playwright. Use the moveon_url or instructions to submit manually.',
  };
};