← back to Norma

agents/shared/pulse-reporter.js

139 lines

/**
 * Norma Pulse Reporter
 *
 * Utility to push data to the Pulse agent (port 9845).
 * All Norma agents report their activity and status here.
 */

const http = require('http');

const PULSE_HOST = '127.0.0.1';
const PULSE_PORT = 9845;
const AUTH_HEADER = 'Basic ' + Buffer.from(`${process.env.AUTH_USERNAME || 'admin'}:${process.env.AUTH_PASSWORD || ''}`).toString('base64');

/**
 * Make an HTTP POST request to the Pulse agent.
 *
 * @param {string} path - URL path (e.g. '/api/pulse/social-feed')
 * @param {Object} data - JSON payload
 * @returns {Promise<Object>} Parsed JSON response
 */
function _post(path, data) {
  return new Promise((resolve, reject) => {
    const payload = JSON.stringify(data);

    const options = {
      hostname: PULSE_HOST,
      port: PULSE_PORT,
      path: path,
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Content-Length': Buffer.byteLength(payload),
        'Authorization': AUTH_HEADER,
      },
      timeout: 10000,
    };

    const req = http.request(options, (res) => {
      let body = '';
      res.on('data', (chunk) => { body += chunk; });
      res.on('end', () => {
        try {
          const parsed = JSON.parse(body);
          resolve(parsed);
        } catch (e) {
          resolve({ raw: body, statusCode: res.statusCode });
        }
      });
    });

    req.on('error', (err) => {
      console.error(`[${new Date().toISOString()}] [pulse-reporter] Request error for ${path}:`, err.message);
      reject(err);
    });

    req.on('timeout', () => {
      req.destroy();
      reject(new Error(`Pulse request to ${path} timed out after 10s`));
    });

    req.write(payload);
    req.end();
  });
}

/**
 * Push social feed data to Pulse.
 * Used when an agent discovers new relevant posts, petitions, or discussions.
 *
 * @param {Object} data - Social feed data
 * @param {string} data.agent - Agent name
 * @param {string} data.platform - Platform name
 * @param {string} data.type - Content type ('post', 'petition', 'discussion', 'mention')
 * @param {string} data.title - Content title
 * @param {string} [data.url] - URL to the content
 * @param {string} [data.content] - Content body/snippet
 * @param {Object} [data.metadata] - Additional metadata
 * @returns {Promise<Object>}
 */
async function pushSocialFeed(data) {
  try {
    return await _post('/api/pulse/social-feed', data);
  } catch (err) {
    console.error(`[${new Date().toISOString()}] [pulse-reporter] Failed to push social feed:`, err.message);
    return null;
  }
}

/**
 * Push trending topics data to Pulse.
 * Used when an agent detects trending conversations or hashtags.
 *
 * @param {Array<Object>} topics - Array of trending topic objects
 * @param {string} topics[].topic - Topic name or hashtag
 * @param {string} topics[].platform - Platform where it's trending
 * @param {number} [topics[].volume] - Mention count or engagement score
 * @param {string} [topics[].sentiment] - Overall sentiment ('positive', 'negative', 'neutral')
 * @returns {Promise<Object>}
 */
async function pushTrending(topics) {
  try {
    return await _post('/api/pulse/trending', {
      topics: Array.isArray(topics) ? topics : [topics],
      reportedAt: new Date().toISOString(),
    });
  } catch (err) {
    console.error(`[${new Date().toISOString()}] [pulse-reporter] Failed to push trending:`, err.message);
    return null;
  }
}

/**
 * Report agent health/status to Pulse.
 * Called periodically by each agent to confirm it's alive and working.
 *
 * @param {string} agent - Agent name
 * @param {Object} status - Status data
 * @param {string} status.state - Agent state ('running', 'idle', 'error', 'rate_limited')
 * @param {number} [status.uptime] - Uptime in seconds
 * @param {Object} [status.rateLimits] - Current rate limit status
 * @param {Object} [status.lastAction] - Last action taken
 * @param {string} [status.errorMessage] - Current error, if any
 * @returns {Promise<Object>}
 */
async function reportAgentStatus(agent, status) {
  try {
    return await _post('/api/pulse/agent-status', {
      agent,
      ...status,
      reportedAt: new Date().toISOString(),
    });
  } catch (err) {
    console.error(`[${new Date().toISOString()}] [pulse-reporter] Failed to report status for ${agent}:`, err.message);
    return null;
  }
}

module.exports = { pushSocialFeed, pushTrending, reportAgentStatus };