← back to Norma

agents/price-agent/server.js

145 lines

/**
 * Norma Price Agent — Education Cost Tracker
 *
 * Tracks tuition, fees, room & board, net price, federal loan rates,
 * and cost-of-attendance across all California universities.
 *
 * Port: 9809
 * Skills (5 base + 3 custom):
 *   discover  → crawl-scorecard  (College Scorecard API)
 *   post      → crawl-uc         (UC system web scrape)
 *   reply     → crawl-csu        (CSU system web scrape)
 *   monitor   → detect-changes   (price change detection + alerts)
 *   report    → report           (summary stats → Pulse)
 *   custom    → crawl-ccc        (CA community college fees)
 *   custom    → crawl-federal    (FSA interest rates + loan limits)
 *   custom    → enrich           (Gemini AI extraction fallback)
 */

const { createAgentServer } = require('../shared/agent-base');

// Skills — mapped to base slots
const crawlScorecard = require('./skills/crawl-scorecard');
const crawlUC = require('./skills/crawl-uc');
const crawlCSU = require('./skills/crawl-csu');
const detectChanges = require('./skills/detect-changes');
const report = require('./skills/report');

// Custom skills (added via stack-pop pattern)
const crawlCCC = require('./skills/crawl-ccc');
const crawlFederal = require('./skills/crawl-federal');
const enrich = require('./skills/enrich');

const AGENT_NAME = 'price-agent';
const PORT = parseInt(process.env.PORT) || 9809;

const { app, start, scheduler } = createAgentServer({
  name: AGENT_NAME,
  port: PORT,
  skills: {
    discover: crawlScorecard,
    post: crawlUC,
    reply: crawlCSU,
    monitor: detectChanges,
    report: report,
  },
  cronJobs: [
    {
      name: 'crawl-scorecard',
      schedule: '0 3 * * *',  // Daily 3am PT
      fn: async () => {
        console.log(`[${AGENT_NAME}] Cron: crawl-scorecard starting`);
        return crawlScorecard({ state: 'CA' });
      },
    },
    {
      name: 'crawl-federal',
      schedule: '0 4 * * *',  // Daily 4am PT
      fn: async () => {
        console.log(`[${AGENT_NAME}] Cron: crawl-federal starting`);
        return crawlFederal({});
      },
    },
    {
      name: 'crawl-uc',
      schedule: '0 5 * * 1',  // Monday 5am PT
      fn: async () => {
        console.log(`[${AGENT_NAME}] Cron: crawl-uc starting`);
        return crawlUC({});
      },
    },
    {
      name: 'crawl-csu',
      schedule: '0 5 * * 2',  // Tuesday 5am PT
      fn: async () => {
        console.log(`[${AGENT_NAME}] Cron: crawl-csu starting`);
        return crawlCSU({});
      },
    },
    {
      name: 'crawl-ccc',
      schedule: '0 5 * * 3',  // Wednesday 5am PT
      fn: async () => {
        console.log(`[${AGENT_NAME}] Cron: crawl-ccc starting`);
        return crawlCCC({});
      },
    },
    {
      name: 'detect-changes',
      schedule: '0 6 * * *',  // Daily 6am PT (after crawls finish)
      fn: async () => {
        console.log(`[${AGENT_NAME}] Cron: detect-changes starting`);
        return detectChanges({});
      },
    },
  ],
});

// ──────────────────────────────────────
// Custom skill routes (stack-pop pattern)
// Pop 404 + error handlers, add routes, re-add handlers
// ──────────────────────────────────────
const stack = app._router.stack;
const errorHandler = stack.pop();
const notFoundHandler = stack.pop();

// crawl-ccc
app.post('/api/skill/crawl-ccc', async (req, res) => {
  try {
    const result = await crawlCCC(req.body || {});
    res.json({ success: true, skill: 'crawl-ccc', result });
  } catch (err) {
    console.error(`[${AGENT_NAME}] Skill "crawl-ccc" error:`, err.message);
    res.status(500).json({ success: false, skill: 'crawl-ccc', error: err.message });
  }
});

// crawl-federal
app.post('/api/skill/crawl-federal', async (req, res) => {
  try {
    const result = await crawlFederal(req.body || {});
    res.json({ success: true, skill: 'crawl-federal', result });
  } catch (err) {
    console.error(`[${AGENT_NAME}] Skill "crawl-federal" error:`, err.message);
    res.status(500).json({ success: false, skill: 'crawl-federal', error: err.message });
  }
});

// enrich (Gemini AI extraction)
app.post('/api/skill/enrich', async (req, res) => {
  try {
    const result = await enrich(req.body || {});
    res.json({ success: true, skill: 'enrich', result });
  } catch (err) {
    console.error(`[${AGENT_NAME}] Skill "enrich" error:`, err.message);
    res.status(500).json({ success: false, skill: 'enrich', error: err.message });
  }
});

// Re-add handlers
if (notFoundHandler) stack.push(notFoundHandler);
if (errorHandler) stack.push(errorHandler);

// Start the agent
start();