BID · Console
Baseline · Intelligence · Decision
scripts/run-demo.ts 6,766 bytes · typescript
/**
 * 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);
});