← 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();