import { defineSource } from '@jaspers-ai/sdk' import { z } from 'zod' // The Bureau of Labor Statistics. Its public API v2 answers without a key; registering raises the // daily limit and is not needed to read a series. Inflation and the labour market, from the office // that measures them. const HOST = 'api.bls.gov' /** The series worth knowing by name, so a request can say "CPI" rather than CUUR0000SA0. */ const KNOWN: Record = { cpi: 'CUUR0000SA0', 'cpi-core': 'CUUR0000SA0L1E', unemployment: 'LNS14000000', payrolls: 'CES0000000001', 'average-hourly-earnings': 'CES0500000003', 'labor-force-participation': 'LNS11300000', ppi: 'WPUFD4', 'job-openings': 'JTS000000000000000JOL', } interface Answer { status: string message?: string[] Results?: { series: { seriesID: string; data: { year: string; period: string; periodName: string; value: string }[] }[] } } export const series = defineSource({ description: `One BLS series over time, monthly. series is a BLS id like CUUR0000SA0, or one of these names: ${Object.keys(KNOWN).join(', ')}. Values come back oldest first, ready to chart.`, hosts: [HOST], input: z.object({ series: z.string().default('cpi'), startYear: z.number().int().optional(), endYear: z.number().int().optional(), }), async run({ series: wanted, startYear, endYear }, ctx) { const id = KNOWN[wanted.trim().toLowerCase()] ?? wanted.trim().toUpperCase() // BLS refuses one year without the other, so naming either fills in the rest. const query = new URLSearchParams() if (startYear || endYear) { const thisYear = new Date().getFullYear() query.set('startyear', String(startYear ?? (endYear ?? thisYear) - 10)) query.set('endyear', String(endYear ?? thisYear)) } const url = `https://${HOST}/publicAPI/v2/timeseries/data/${encodeURIComponent(id)}${query.size ? `?${query.toString()}` : ''}` const response = await ctx.fetch(url) if (!response.ok) throw new Error(`BLS answered ${response.status}.`) const body = (await response.json()) as Answer if (body.status !== 'REQUEST_SUCCEEDED') throw new Error(body.message?.join(' ') || `BLS refused: ${body.status}.`) const found = body.Results?.series?.[0] if (!found || found.data.length === 0) throw new Error(`BLS has no data for ${id}. Check the series id.`) const rows = found.data // M13 is the annual average, which does not belong on a monthly line. .filter((one) => one.period !== 'M13') .map((one) => ({ series: found.seriesID, date: `${one.year}-${one.period.replace('M', '')}-01`, year: Number(one.year), period: one.periodName, value: Number(one.value), })) .filter((one) => Number.isFinite(one.value)) .sort((a, b) => a.date.localeCompare(b.date)) return { series: found.seriesID, rows } }, })