← back to Norma

agents/moveon-agent/skills/discover.js

93 lines

/**
 * MoveOn Discover Skill
 *
 * Searches MoveOn.org for existing petitions related to advocacy topics.
 * Rate limit: 2/hour
 */

const { RateLimiter } = require('../../shared/rate-limiter');
const { logAction } = require('../../shared/audit-logger');
const { searchPetitions, getPetitionDetails } = require('../lib/moveon');

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

const rateLimiter = new RateLimiter(AGENT, PLATFORM);

/**
 * @param {Object} body - Request body
 * @param {string} [body.query='nonprofit advocacy'] - Search query
 * @param {number} [body.max_results=10] - Max results to return
 * @param {boolean} [body.fetch_details=false] - Fetch full details for each result
 * @returns {Promise<Object>}
 */
module.exports = async function discover(body) {
  const query = body.query || 'nonprofit advocacy';
  const maxResults = Math.min(body.max_results || 10, 25);
  const fetchDetails = body.fetch_details || false;

  // Check rate limit (uses 'search' action type — falls back to default 10/hour)
  const limit = await rateLimiter.checkLimit('monitor');
  if (!limit.allowed) {
    await logAction({
      agent: AGENT,
      actionType: 'discover',
      platform: PLATFORM,
      content: query,
      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,
    };
  }

  // Search MoveOn for petitions
  const results = await searchPetitions(query, maxResults);

  // Optionally fetch full details for each petition
  let enrichedResults = results;
  if (fetchDetails && results.length > 0) {
    enrichedResults = [];
    for (const result of results.slice(0, 5)) {
      // limit detail fetches to 5
      try {
        const details = await getPetitionDetails(result.url);
        enrichedResults.push({ ...result, ...details });
      } catch (err) {
        enrichedResults.push({ ...result, detailError: err.message });
      }
    }
    // Add remaining without details
    if (results.length > 5) {
      enrichedResults.push(...results.slice(5));
    }
  }

  // Record the action
  await rateLimiter.recordAction('monitor');
  await logAction({
    agent: AGENT,
    actionType: 'discover',
    platform: PLATFORM,
    content: query,
    responseData: {
      query,
      resultCount: enrichedResults.length,
      fetchedDetails: fetchDetails,
    },
    status: 'success',
  });

  return {
    query,
    count: enrichedResults.length,
    petitions: enrichedResults,
    fetched_at: new Date().toISOString(),
  };
};