feat(fred): search_series and get_series - #37
Merged
Conversation
Discovery, without which every question starts with a guessed series ID. search_series is one tool with three paths (free-text, release, category) because the answer is the same in all three: a list of series. Ordered by popularity rather than FRED's search_rank default, which buries UNRATE under regional variants. Filters go through tag_names, not filter_variable. FRED takes exactly one filter_variable per request, so "monthly and not seasonally adjusted" would mean applying the second filter locally, over a page FRED had already truncated, against a count that no longer describes the result. tag_names takes both, server-side, and count stays true. Verified live: 17,128 matches for monthly+NSA unemployment, UNRATENSA first. get_series folds /series, /series/release, /series/categories and /series/tags into one call via include=[...]. A bad ID among good ones fails only its own entry, since FRED's "The series does not exist" never says which series it means. Also fixes a credential leak found while testing: httpx logs the full request URL at INFO, and FRED takes the API key as a query parameter, so every call wrote the key to stderr. httpx is pinned to WARNING before anything can emit.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #32. Second of the
fredstack (#30). Stacked on #36, so review that first; this PR's diff is againstfred/scaffold.search_seriesThree discovery paths, one tool, one output shape:
query/series/searchrelease_id/release/seriescategory_id/category/seriesThey are one tool because the answer is identical in all three cases: a list of series. Splitting them would make the model choose between tools that return the same thing.
Ordered by popularity by default. FRED's own default is
search_rank, which buriesUNRATEunder hundreds of regional variants.Filters go through
tag_names, notfilter_variableThis is the one non-obvious decision here, and the first attempt got it wrong.
FRED's series-list endpoints accept exactly one
filter_variableper request. So "monthly and not seasonally adjusted" means pushing one filter to the API and applying the other locally, over a page FRED has already truncated tolimit, against acountthat describes the API-side filter only. Live, that returned zero results for monthly + NSA unemployment: the top rows FRED returned were all seasonally adjusted, and the local filter removed every one of them. Over-fetching papers over it without fixing it.tag_namestakes several tags at once and FRED applies all of them:So the tool maps
frequencyandseasonal_adjustmentonto FRED's own tag vocabulary (/fred/tags?tag_group_id=freqand=seas) and sends them together. No over-fetch, no client-side filtering, no caveat note, andcountmeans what it says. The change deletedmatches_filtersand_sa_shortrather than adding to them.One trap the tests pin down:
frequency="sa"means semiannual andseasonal_adjustment="sa"means adjusted. Same spelling, different tags (semiannualvssa). Also, FRED'sseasgroup holds onlysaandnsa, so"SAAR"resolves tosa, the tag SAAR series actually carry.get_seriesFolds
/series,/series/release,/series/categoriesand/series/tagsinto one call, fetched concurrently, selected withinclude=["metadata"|"notes"|"release"|"categories"|"tags"|"all"].A bad ID among good ones fails only its own entry. FRED's
"The series does not exist"never says which series it means, so failing the whole call would leave a model holding three IDs with no idea which to fix:{"series": [ {"id": "UNRATE", "units": "Percent", ...}, {"id": "NOSUCHSERIES", "error": "Bad Request. The series does not exist.", "suggestion": "Check this ID with search_series; the others in this call were returned."} ]}Correction layer
src/schemas.pycorrects before it validates."unrate"and"UNRATE, CPIAUCSL"and["unrate"]all become["UNRATE", ...];"monthly"becomesm;"unadjusted"becomes thensatag;include="notes,tags"becomes a list.series_idsis annotatedlist[str] | strdeliberately. The MCP layer validates against the annotation before the tool body runs, so a strictlist[str]turnsget_series("UNRATE")into a rawToolErrorthat never reaches the correction layer, which is the exact failure the correction layer exists to prevent.Incidental fix: the API key was being logged
Found while smoke-testing against the real API. httpx logs the full request line at INFO, and FRED takes the key as a query parameter, so every call wrote the key to stderr:
configure_loggingnow pins httpx to WARNING before anything can emit. Our own logging records the path only.Verification
125 tests, 98% coverage,
make checkclean. Integration tests drive the registered MCP server against a mock FRED built from trimmed real captures, so the shaping is asserted against FRED's shapes rather than invented ones.Smoke-tested live against the real API: