/**
* Demo entry point.
*
* Two modes:
* npm run demo — hardcoded JPM+BAC revenue job
* npm run demo -- "<plain English question>"
* — uses a small Claude Haiku intake call to construct a typed
* JobRequest from the question, then runs the full pipeline.
*
* Either way, drives the real BID pipeline against live SEC EDGAR
* plus Anthropic Claude. Cost scales near-linearly with entity count.
*
* Std 12: requires ANTHROPIC_API_KEY; fails with a clear message if
* the key is missing — never fakes the run.
*
* For a free SEC-only check (no Anthropic) across all six banks, use:
*
* npm run sec:verify
*/
import { mkdir, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { runAnalysis } from '../src/orchestrator.js';
import type { JobRequest } from '../src/types.js';
import { registerConnector } from '../src/tools/retrieval/dispatcher.js';
import { SecEdgarConnector } from '../src/tools/retrieval/connectors/sec-edgar.js';
import { estimateCostUsd, usageSnapshot } from '../src/observability/usage.js';
import { buildJobRequestFromQuestion } from './intake.js';
import { renderReport } from './render-report.js';
import { renderFlow } from './render-flow.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const OUTPUT_DIR = path.resolve(__dirname, '..', 'output');
const DEFAULT_JOB: JobRequest = {
analysisId: 'demo-jpm-bac-revenue',
question:
'What is the most recent reported total revenue for JPMorgan Chase and Bank of America? Use SEC EDGAR.',
entities: [
{ id: 'JPMorgan Chase', aliases: ['JPM', 'JPMorgan Chase & Co'] },
{ id: 'Bank of America', aliases: ['BAC', 'Bank of America Corporation'] },
],
targetMetrics: [
{
key: 'total_revenue',
definition:
'Total revenue for the fiscal year, as reported in the most recent annual 10-K. For banks, this is typically the XBRL tag "Revenues" or "RevenuesNetOfInterestExpense" depending on the bank\'s reporting choice.',
unit: 'USD',
},
],
sources: ['sec-edgar'],
period: 'latest-annual',
seedMappings: [
{ sourceLabel: 'Revenues', targetKey: 'total_revenue' },
{ sourceLabel: 'RevenuesNetOfInterestExpense', targetKey: 'total_revenue' },
],
};
async function main(): Promise<void> {
if (!process.env.ANTHROPIC_API_KEY) {
// eslint-disable-next-line no-console
console.error(
'ANTHROPIC_API_KEY is not set. This demo drives Anthropic Claude end-to-end against ' +
'live SEC EDGAR — set the key and rerun. The framework does not fake runs (Std 12).',
);
process.exit(1);
}
const question = process.argv[2]?.trim();
let job: JobRequest;
if (question) {
// eslint-disable-next-line no-console
console.log(`BID Baseline POC — intake from English question:\n "${question}"\n`);
const intake = await buildJobRequestFromQuestion(question);
// eslint-disable-next-line no-console
console.log(
`Intake done via ${intake.modelUsed} (${intake.inputTokens} in / ${intake.outputTokens} out tokens, ${intake.methodologiesShown} methodology(ies) in scope). Constructed JobRequest:`,
);
// eslint-disable-next-line no-console
console.log(JSON.stringify(intake.jobRequest, null, 2));
job = intake.jobRequest;
} else {
job = DEFAULT_JOB;
// eslint-disable-next-line no-console
console.log('BID Baseline POC — running default SEC EDGAR demo job:');
// eslint-disable-next-line no-console
console.log(JSON.stringify(job, null, 2));
}
/* Register the SEC EDGAR connector with the retrieval dispatcher so
* JobRequest.sources=["sec-edgar"] is a known source. */
registerConnector(new SecEdgarConnector());
// eslint-disable-next-line no-console
console.log('\nRunning pipeline…');
const startedAt = Date.now();
const result = await runAnalysis(job);
const elapsedMs = Date.now() - startedAt;
const usage = usageSnapshot();
const totalCostUsd = usage.byAgent.reduce(
(acc, r) => acc + estimateCostUsd(r.inputTokens, r.outputTokens, r.model),
0,
);
await mkdir(OUTPUT_DIR, { recursive: true });
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
const file = path.join(OUTPUT_DIR, `run-${stamp}.json`);
const audit = { ...result, jobRequest: job, elapsedMs, usage };
await writeFile(file, JSON.stringify(audit, null, 2), 'utf8');
// eslint-disable-next-line no-console
console.log(`\nAudit trail written: ${path.relative(process.cwd(), file)}`);
// eslint-disable-next-line no-console
console.log(`Elapsed: ${(elapsedMs / 1000).toFixed(1)}s`);
/* Anthropic usage summary — observability only (Std 5 / Std 6 cost-
* appropriate-execution lever requires we can measure cost-per-run). */
// eslint-disable-next-line no-console
console.log(`\nAnthropic usage (this run):`);
// eslint-disable-next-line no-console
console.log(` ${'agent'.padEnd(34)} ${'model'.padEnd(20)} ${'calls'.padStart(6)} ${'in tok'.padStart(8)} ${'out tok'.padStart(8)} ${'~cost USD'.padStart(10)}`);
for (const r of usage.byAgent) {
const cost = estimateCostUsd(r.inputTokens, r.outputTokens, r.model);
// eslint-disable-next-line no-console
console.log(` ${r.agent.padEnd(34)} ${r.model.padEnd(20)} ${String(r.calls).padStart(6)} ${String(r.inputTokens).padStart(8)} ${String(r.outputTokens).padStart(8)} ${('$' + cost.toFixed(5)).padStart(10)}`);
}
// eslint-disable-next-line no-console
console.log(` ${'TOTAL'.padEnd(34)} ${''.padEnd(20)} ${String(usage.totals.calls).padStart(6)} ${String(usage.totals.inputTokens).padStart(8)} ${String(usage.totals.outputTokens).padStart(8)} ${('$' + totalCostUsd.toFixed(5)).padStart(10)}`);
const htmlPath = await renderReport(file);
// eslint-disable-next-line no-console
console.log(`HTML report written: ${path.relative(process.cwd(), htmlPath)}`);
const flowPath = await renderFlow(file);
// eslint-disable-next-line no-console
console.log(`Flow page written: ${path.relative(process.cwd(), flowPath)}`);
if (result.ok && result.finalHandoff) {
// eslint-disable-next-line no-console
console.log('\nFinal Baseline output (analytics-ready):');
// eslint-disable-next-line no-console
console.log(JSON.stringify(result.finalHandoff.payload, null, 2));
} else if (result.failure) {
// eslint-disable-next-line no-console
console.log('\nRun failed:');
// eslint-disable-next-line no-console
console.log(JSON.stringify(result.failure, null, 2));
process.exitCode = 1;
}
}
main().catch(err => {
// eslint-disable-next-line no-console
console.error('Unhandled demo error:', err);
process.exit(1);
});