← back to Norma

agents/onboard-agent/skills/report.js

126 lines

/**
 * OnBoard Report Skill
 *
 * Provides discovery statistics and recent match data.
 * Returns counts by status, mind_map_links by type, and recent top matches.
 */

const { query } = require('../../shared/db');
const { logAction } = require('../../shared/audit-logger');

const AGENT = 'onboard-agent';
const PLATFORM = 'internal';

/**
 * @param {Object} body - Request body
 * @param {number} [body.hours_back=24] - Hours back for recent matches
 * @param {number} [body.top_n=10] - Number of top matches to return
 * @returns {Promise<Object>}
 */
module.exports = async function report(body = {}) {
  const hoursBack = body.hours_back || 24;
  const topN = body.top_n || 10;

  // 1. Count onboard_discoveries by status
  const { rows: statusCounts } = await query(
    `SELECT status, COUNT(*)::int AS count
     FROM onboard_discoveries
     GROUP BY status
     ORDER BY count DESC`
  );

  const discoveries = {};
  for (const row of statusCounts) {
    discoveries[row.status] = row.count;
  }

  // 2. Count mind_map_links by link_type (for links created by this agent)
  const { rows: linkCounts } = await query(
    `SELECT link_type, COUNT(*)::int AS count
     FROM mind_map_links
     WHERE auto_generated = true
       AND metadata->>'discovered_by' = 'onboard-agent'
     GROUP BY link_type
     ORDER BY count DESC`
  );

  const links = {};
  for (const row of linkCounts) {
    links[row.link_type] = row.count;
  }

  // 3. Total mind_map_links (all types)
  const { rows: totalLinks } = await query(
    `SELECT COUNT(*)::int AS count FROM mind_map_links
     WHERE auto_generated = true
       AND metadata->>'discovered_by' = 'onboard-agent'`
  );

  // 4. Recent top matches (last N hours)
  const { rows: recentMatches } = await query(
    `SELECT d.id, d.match_score, d.trigger_label, d.status,
            d.recommended_actions, d.created_at,
            o.name AS org_name, o.city, o.state, o.ntee_code
     FROM onboard_discoveries d
     JOIN organizations o ON d.org_id = o.id
     WHERE d.created_at > NOW() - INTERVAL '1 hour' * $1
     ORDER BY d.match_score DESC
     LIMIT $2`,
    [hoursBack, topN]
  );

  // 5. Total orgs discovered by this agent
  const { rows: orgCount } = await query(
    `SELECT COUNT(*)::int AS count FROM organizations
     WHERE source = 'onboard-agent'`
  );

  // 6. Agent action summary (last 24h)
  const { rows: actionSummary } = await query(
    `SELECT action_type, status, COUNT(*)::int AS count,
            MAX(created_at) AS last_at
     FROM sdcc_agent_actions
     WHERE agent = $1
       AND created_at > NOW() - INTERVAL '24 hours'
     GROUP BY action_type, status
     ORDER BY count DESC`,
    [AGENT]
  );

  // Log the report action
  await logAction({
    agent: AGENT,
    actionType: 'report',
    platform: PLATFORM,
    status: 'success',
    content: `Report generated (${hoursBack}h window)`,
    responseData: {
      discovery_count: Object.values(discoveries).reduce((a, b) => a + b, 0),
      link_count: totalLinks[0]?.count || 0,
      recent_match_count: recentMatches.length,
    },
  });

  return {
    discoveries,
    total_discoveries: Object.values(discoveries).reduce((a, b) => a + b, 0),
    orgs_discovered: orgCount[0]?.count || 0,
    links,
    total_links: totalLinks[0]?.count || 0,
    recent_matches: recentMatches.map(m => ({
      id: m.id,
      org_name: m.org_name,
      location: `${m.city || '?'}, ${m.state || '?'}`,
      ntee_code: m.ntee_code,
      match_score: m.match_score,
      trigger_topic: m.trigger_label,
      status: m.status,
      recommended_actions: m.recommended_actions,
      discovered_at: m.created_at,
    })),
    agent_actions_24h: actionSummary,
    report_window_hours: hoursBack,
    generated_at: new Date().toISOString(),
  };
};