fix(dataflows): degrade gracefully when an optional vendor fails

Optional enrichment vendors (FRED macro, Polymarket events) raised on a bad LLM
indicator, a missing key, or a network blip, which aborted the whole run.

- Router: mark macro_data and prediction_markets optional; a sole-vendor failure
  returns a sentinel instead of re-raising. Core categories still raise.
- FRED: reject a descriptive phrase up front and return guidance instead of
  400ing the API; an unknown series returns a not-found message, not a crash.
This commit is contained in:
Yijia-Xiao
2026-06-21 21:28:59 +00:00
parent 7bb16c5daa
commit ee1ece3347
4 changed files with 90 additions and 16 deletions

View File

@@ -93,13 +93,26 @@ def get_api_key() -> str:
def _resolve_series_id(indicator: str) -> str:
"""Map a friendly alias to a FRED series ID, or pass a raw ID through."""
"""Map a friendly alias to a FRED series ID, or pass a raw ID through.
Raises ``ValueError`` when the input is neither a known alias nor a plausible
series ID — typically a descriptive phrase the LLM passed instead (e.g.
"bank of japan rate"). FRED IDs are short and alphanumeric, so this rejects
it up front with guidance rather than letting it 400 the API.
"""
key = indicator.strip().lower().replace(" ", "_").replace("-", "_")
if key in MACRO_SERIES:
return MACRO_SERIES[key]
# Not a known alias: treat the input as a raw FRED series ID (FRED IDs are
# conventionally uppercase, e.g. "DGS10", "CPIAUCSL").
return indicator.strip().upper()
candidate = indicator.strip().upper()
# FRED series IDs never contain whitespace and are short; reject anything
# else (a descriptive phrase the LLM passed) rather than 400ing the API.
if not candidate or len(candidate) > 30 or any(c.isspace() for c in candidate):
raise ValueError(
f"'{indicator}' is not a known macro alias or a valid FRED series ID. "
f"Use an alias (e.g. 'cpi', 'unemployment', '10y_treasury') or a raw "
f"FRED series ID (e.g. 'CPIAUCSL')."
)
return candidate
def _request(path: str, params: dict) -> dict:
@@ -143,11 +156,18 @@ def get_macro_data(
end_dt = datetime.strptime(curr_date, "%Y-%m-%d")
start_date = (end_dt - timedelta(days=look_back_days)).strftime("%Y-%m-%d")
series_id = _resolve_series_id(indicator)
# Invalid LLM-supplied indicator: return guidance rather than raising, so a
# bad argument doesn't abort the run (the routing layer also degrades macro
# data, but a specific message is more useful to the analyst).
try:
series_id = _resolve_series_id(indicator)
except ValueError as e:
return f"FRED: {e}"
meta = _request("series", {"series_id": series_id}).get("seriess") or []
if not meta:
raise ValueError(
return (
f"FRED series '{series_id}' not found. Pass a known alias "
f"(e.g. 'cpi', 'unemployment') or a valid FRED series ID."
)

View File

@@ -84,6 +84,13 @@ VENDOR_LIST = [
"alpha_vantage",
]
# Optional enrichment categories. These add macro/event context to the news
# analyst but are not core to a decision, so a vendor failure here degrades to a
# sentinel instead of aborting the run (a bad LLM-supplied indicator, a missing
# key, or a network blip should not crash an analysis over flavour data). Core
# categories (prices, fundamentals, news) still raise so a broken primary is loud.
OPTIONAL_CATEGORIES = {"macro_data", "prediction_markets"}
# Mapping of methods to their vendor-specific implementations
VENDOR_METHODS = {
# core_stock_apis
@@ -240,8 +247,16 @@ def route_to_vendor(method: str, *args, **kwargs):
)
# No vendor returned data and none reported clean "no data" — surface the
# first real error (e.g. the primary vendor's network failure).
# first real error (e.g. the primary vendor's network failure). Optional
# enrichment categories degrade to a sentinel instead, so flavour data can't
# abort the run.
if first_error is not None:
if category in OPTIONAL_CATEGORIES:
logger.warning("Optional %s unavailable for %s: %s", category, method, first_error)
return (
f"DATA_UNAVAILABLE: optional {category} could not be retrieved "
f"({first_error}). Proceed without it; do not fabricate values."
)
raise first_error
raise RuntimeError(f"No available vendor for '{method}'")