mirror of
https://github.com/TauricResearch/TradingAgents.git
synced 2026-06-17 05:16:14 +03:00
fix(data): respect the configured vendor chain and log vendor failures
The router silently extended every request to all available vendors regardless of config, so an explicit single-vendor choice still fell back to others and returned data from an unexpected source (#988, #289), and serious primary-vendor errors were swallowed without a trace (#989). The configured vendor list is now the exact chain (list several for ordered fallback; "default" uses all), unknown vendors raise, and swallowed vendor errors are logged. Adds an autouse config isolation fixture so vendor config can't leak between tests.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import logging
|
||||
from typing import Annotated
|
||||
|
||||
# Import from vendor-specific modules
|
||||
@@ -28,6 +29,8 @@ from .symbol_utils import NoMarketDataError
|
||||
# Configuration and routing logic
|
||||
from .config import get_config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Tools organized by category
|
||||
TOOLS_CATEGORIES = {
|
||||
"core_stock_apis": {
|
||||
@@ -141,34 +144,43 @@ def route_to_vendor(method: str, *args, **kwargs):
|
||||
if method not in VENDOR_METHODS:
|
||||
raise ValueError(f"Method '{method}' not supported")
|
||||
|
||||
# Build fallback chain: primary vendors first, then remaining available vendors
|
||||
all_available_vendors = list(VENDOR_METHODS[method].keys())
|
||||
fallback_vendors = primary_vendors.copy()
|
||||
for vendor in all_available_vendors:
|
||||
if vendor not in fallback_vendors:
|
||||
fallback_vendors.append(vendor)
|
||||
|
||||
# The configured vendor list IS the chain: we do NOT silently fall back to
|
||||
# vendors the user did not choose (#988/#289) — that returned data from an
|
||||
# unexpected source and caused cross-vendor inconsistencies. For multi-vendor
|
||||
# fallback, list them in order, e.g. data_vendors="yfinance,alpha_vantage".
|
||||
# The "default" sentinel (no explicit config) uses all available vendors.
|
||||
explicit = [v for v in primary_vendors if v and v != "default"]
|
||||
if explicit:
|
||||
vendor_chain = [v for v in explicit if v in VENDOR_METHODS[method]]
|
||||
if not vendor_chain:
|
||||
raise ValueError(
|
||||
f"Configured vendor(s) {explicit} not available for '{method}'. "
|
||||
f"Available: {all_available_vendors}."
|
||||
)
|
||||
else:
|
||||
vendor_chain = all_available_vendors
|
||||
|
||||
last_no_data: NoMarketDataError | None = None
|
||||
first_error: Exception | None = None
|
||||
for vendor in fallback_vendors:
|
||||
if vendor not in VENDOR_METHODS[method]:
|
||||
continue
|
||||
|
||||
for vendor in vendor_chain:
|
||||
vendor_impl = VENDOR_METHODS[method][vendor]
|
||||
impl_func = vendor_impl[0] if isinstance(vendor_impl, list) else vendor_impl
|
||||
|
||||
try:
|
||||
return impl_func(*args, **kwargs)
|
||||
except AlphaVantageRateLimitError:
|
||||
continue # Rate limits: try the next vendor
|
||||
logger.warning("Vendor %r rate-limited for %s; trying next vendor.", vendor, method)
|
||||
continue
|
||||
except NoMarketDataError as e:
|
||||
last_no_data = e # No data here; another vendor may have it
|
||||
last_no_data = e # No data here; another configured vendor may have it
|
||||
continue
|
||||
except Exception as e:
|
||||
# A fallback vendor failing for an incidental reason (e.g. no API
|
||||
# key configured) must not crash the call when another vendor
|
||||
# already determined the symbol simply has no data. Remember the
|
||||
# first error so a genuine primary-vendor failure still surfaces.
|
||||
# Don't let one vendor's failure crash the call when another can
|
||||
# serve it, but never swallow silently: a broken primary must be
|
||||
# visible in the logs (#989), not hidden behind a fallback's verdict.
|
||||
logger.warning("Vendor %r failed for %s: %s", vendor, method, e)
|
||||
if first_error is None:
|
||||
first_error = e
|
||||
continue
|
||||
@@ -178,6 +190,13 @@ def route_to_vendor(method: str, *args, **kwargs):
|
||||
# empty string, so the agent reports "unavailable" instead of inventing a
|
||||
# value. This takes precedence over incidental fallback errors.
|
||||
if last_no_data is not None:
|
||||
if first_error is not None:
|
||||
# A vendor also hit a real error; surface it in logs so the no-data
|
||||
# verdict can't hide a broken primary (network/auth/etc.).
|
||||
logger.warning(
|
||||
"Returning NO_DATA for %s, but a vendor errored earlier: %s",
|
||||
method, first_error,
|
||||
)
|
||||
sym = last_no_data.symbol
|
||||
canonical = last_no_data.canonical
|
||||
resolved = "" if canonical == sym else f" (resolved to '{canonical}')"
|
||||
|
||||
@@ -97,7 +97,10 @@ DEFAULT_CONFIG = _apply_env_overrides({
|
||||
"oil commodities supply chain energy",
|
||||
],
|
||||
# Data vendor configuration
|
||||
# Category-level configuration (default for all tools in category)
|
||||
# Category-level configuration (default for all tools in category).
|
||||
# The configured value is the exact vendor chain — requests are NOT silently
|
||||
# routed to vendors you didn't choose. For ordered fallback, list several,
|
||||
# e.g. "yfinance,alpha_vantage". "default" uses all available vendors.
|
||||
"data_vendors": {
|
||||
"core_stock_apis": "yfinance", # Options: alpha_vantage, yfinance
|
||||
"technical_indicators": "yfinance", # Options: alpha_vantage, yfinance
|
||||
|
||||
Reference in New Issue
Block a user