-
Notifications
You must be signed in to change notification settings - Fork 23
Expand file tree
/
Copy pathfetch-aggregate-data.tool.ts
More file actions
149 lines (118 loc) · 4.64 KB
/
fetch-aggregate-data.tool.ts
File metadata and controls
149 lines (118 loc) · 4.64 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
import { Tool } from '@modelcontextprotocol/sdk/types.js'
import { z } from 'zod'
import { BaseTool } from './base.tool.js'
import { buildCitation } from '../helpers/citation.js'
import {
FetchAggregateDataToolSchema,
TableArgs,
TableSchema,
} from '../schema/fetch-aggregate-data.schema.js'
import { ToolContent } from '../types/base.types.js'
import {
datasetValidator,
validateGeographyArgs,
} from '../schema/validators.js'
export const toolDescription = `
Fetches statistical data from U.S. Census Bureau datasets including population, demographics, income, housing, employment, and economic indicators. Use this tool when users request Census statistics, demographic breakdowns, or socioeconomic data for specific geographic areas. Requires a dataset identifier, year/vintage, geographic scope (state, county, tract, etc.), and specific variables or table groups. Returns structured data with proper citations for authoritative government statistics.
Important: ACS 1-year estimates (acs/acs1) are only available for geographic areas with populations of 65,000 or more. For smaller areas, use ACS 5-year estimates (acs/acs5) instead. See: https://www.census.gov/programs-surveys/acs/guidance/estimates.html
`
export class FetchAggregateDataTool extends BaseTool<TableArgs> {
name = 'fetch-aggregate-data'
description = toolDescription
inputSchema: Tool['inputSchema'] = TableSchema as Tool['inputSchema']
readonly requiresApiKey = true
get argsSchema() {
return FetchAggregateDataToolSchema.superRefine((args, ctx) => {
//Check that the correct tool is used to fetch data
const identifiedDataset = datasetValidator(args.dataset)
if (identifiedDataset.tool !== this.name) {
ctx.addIssue({
path: ['dataset'],
code: z.ZodIssueCode.custom,
message: identifiedDataset.message,
})
}
validateGeographyArgs(args, ctx)
})
}
constructor() {
super()
this.handler = this.handler.bind(this)
}
validateArgs(input: unknown) {
return this.argsSchema.safeParse(input)
}
async toolHandler(
args: TableArgs,
apiKey: string,
): Promise<{ content: ToolContent[] }> {
const baseUrl = `https://api.census.gov/data/${args.year}/${args.dataset}`
let getParams = ''
if (args.get.variables || args.get.group) {
if (args.get.variables) {
getParams = args.get.variables.join(',')
}
if (args.get.group) {
if (getParams != '') {
getParams += ','
}
getParams += `group(${args.get.group})`
}
}
const query = new URLSearchParams({
get: getParams,
})
if (args.for) {
query.append('for', args.for)
}
if (args.in) {
query.append('in', args.in)
}
if (args.ucgid) {
query.append('ucgid', args.ucgid)
}
if (args.predicates) {
for (const [key, value] of Object.entries(args.predicates)) {
query.append(key, value)
}
}
const descriptive = args.descriptive?.toString() ?? 'false'
query.append('descriptive', descriptive)
query.append('key', apiKey)
const url = `${baseUrl}?${query.toString()}`
try {
const fetch = (await import('node-fetch')).default
const res = await fetch(url)
console.log(`URL Attempted: ${url}`)
if (!res.ok) {
if (res.status === 400 && isAcs1YearDataset(args.dataset)) {
return this.createErrorResponse(
`Census API error: ${res.status} ${res.statusText}. ` +
`Note: ACS 1-year estimates (acs/acs1) are only available for geographic areas ` +
`with populations of 65,000 or more. If the requested area has a smaller population, ` +
`try using ACS 5-year estimates instead by changing the dataset to "acs/acs5". ` +
`See: https://www.census.gov/programs-surveys/acs/guidance/estimates.html`,
)
}
return this.createErrorResponse(
`Census API error: ${res.status} ${res.statusText}`,
)
}
const data = (await res.json()) as string[][]
const [headers, ...rows] = data
const output = rows
.map((row) => headers.map((h, i) => `${h}: ${row[i]}`).join(', '))
.join('\n')
const citation = buildCitation(url)
return this.createSuccessResponse(
`Response from ${args.dataset}:\n${output}\n${citation}`,
)
} catch (err) {
return this.createErrorResponse(`Fetch failed: ${(err as Error).message}`)
}
}
}
function isAcs1YearDataset(dataset: string): boolean {
const normalized = dataset.toLowerCase().replace(/\s+/g, '')
return normalized.includes('acs/acs1') || normalized.includes('acs1')
}