← back to Norma

agents/twitch-agent/skills/report.js

164 lines

/**
 * Twitch Report Skill
 *
 * Aggregates Twitch agent stats and pushes to Pulse.
 * Reports: channels monitored, messages sent, streams found, chat status.
 */

const { logAction, getActionSummary } = require('../../shared/audit-logger');
const { reportAgentStatus, pushSocialFeed } = require('../../shared/pulse-reporter');
const { query: dbQuery } = require('../../shared/db');
const { getChatStatus, getJoinedChannels, getRecentMessages } = require('../lib/twitch');

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

/**
 * @param {Object} body - Request body
 * @param {number} [body.hours_back=4] - How many hours back to report
 * @returns {Promise<Object>}
 */
module.exports = async function report(body) {
  const hoursBack = body.hours_back || 4;

  // Get action summary for the time window
  const actionSummary = await getActionSummary(AGENT, hoursBack);

  // Get current chat status
  const chatStatus = getChatStatus();
  const joinedChannels = getJoinedChannels();
  const recentMessages = getRecentMessages();

  // Get monitor targets from database
  let monitorStats = { total: 0, active: 0, totalItemsFound: 0 };
  try {
    const { rows } = await dbQuery(
      `SELECT
        COUNT(*)::int as total,
        COUNT(*) FILTER (WHERE is_active = true)::int as active,
        COALESCE(SUM(items_found), 0)::int as total_items_found
       FROM sdcc_agent_monitors
       WHERE agent = $1 AND platform = $2`,
      [AGENT, PLATFORM]
    );
    if (rows[0]) {
      monitorStats = {
        total: rows[0].total,
        active: rows[0].active,
        totalItemsFound: rows[0].total_items_found,
      };
    }
  } catch (err) {
    console.error(`[${AGENT}] Failed to get monitor stats:`, err.message);
  }

  // Get action counts for the period
  let actionCounts = { discovers: 0, posts: 0, replies: 0, monitors: 0 };
  try {
    const { rows } = await dbQuery(
      `SELECT action_type, COUNT(*)::int as count
       FROM sdcc_agent_actions
       WHERE agent = $1 AND created_at > NOW() - INTERVAL '1 hour' * $2
       GROUP BY action_type`,
      [AGENT, hoursBack]
    );
    for (const row of rows) {
      if (row.action_type === 'discover') actionCounts.discovers = row.count;
      if (row.action_type === 'post') actionCounts.posts = row.count;
      if (row.action_type === 'reply') actionCounts.replies = row.count;
      if (row.action_type === 'monitor') actionCounts.monitors = row.count;
    }
  } catch (err) {
    console.error(`[${AGENT}] Failed to get action counts:`, err.message);
  }

  // Keyword message stats
  const keywordsByChannel = {};
  for (const msg of recentMessages) {
    if (!keywordsByChannel[msg.channel]) {
      keywordsByChannel[msg.channel] = { count: 0, keywords: new Set() };
    }
    keywordsByChannel[msg.channel].count++;
    for (const kw of msg.keywords) {
      keywordsByChannel[msg.channel].keywords.add(kw);
    }
  }

  const channelStats = Object.entries(keywordsByChannel).map(([channel, data]) => ({
    channel,
    messageCount: data.count,
    keywords: [...data.keywords],
  }));

  const reportData = {
    agent: AGENT,
    platform: PLATFORM,
    period_hours: hoursBack,
    actions: actionSummary,
    action_counts: actionCounts,
    chat: {
      ...chatStatus,
      channelsMonitored: joinedChannels.length,
      recentKeywordMessages: recentMessages.length,
      channelBreakdown: channelStats,
    },
    monitors: monitorStats,
    uptime: process.uptime(),
    reported_at: new Date().toISOString(),
  };

  // Push status to Pulse
  const pulseResult = await reportAgentStatus(AGENT, {
    state: chatStatus.connected ? 'running' : 'idle',
    uptime: process.uptime(),
    lastAction: actionSummary.length > 0 ? actionSummary[0] : null,
    stats: {
      channelsMonitored: joinedChannels.length,
      keywordMessagesDetected: recentMessages.length,
      actionCounts,
      monitors: monitorStats,
    },
  });

  // Push notable keyword messages as social feed items
  const topMessages = recentMessages
    .sort((a, b) => b.keywords.length - a.keywords.length)
    .slice(0, 5);

  for (const msg of topMessages) {
    await pushSocialFeed({
      agent: AGENT,
      platform: PLATFORM,
      type: 'mention',
      title: `Keyword match in #${msg.channel}`,
      url: `https://twitch.tv/${msg.channel}`,
      content: `@${msg.username}: "${msg.message.substring(0, 200)}"`,
      metadata: {
        channel: msg.channel,
        username: msg.username,
        keywords: msg.keywords,
        timestamp: msg.timestamp,
      },
    });
  }

  // Log the report action
  await logAction({
    agent: AGENT,
    actionType: 'report',
    platform: PLATFORM,
    responseData: {
      periodHours: hoursBack,
      channelsMonitored: joinedChannels.length,
      actionsInPeriod: Object.values(actionCounts).reduce((sum, c) => sum + c, 0),
      keywordMessages: recentMessages.length,
    },
    status: 'success',
  });

  return {
    ...reportData,
    pulse_response: pulseResult,
  };
};