added fallbacks for tools
This commit is contained in:
parent
e7d8305a25
commit
c0f0415844
|
|
@ -1,6 +1,7 @@
|
||||||
import os
|
import os
|
||||||
import requests
|
import requests
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
import json
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
|
|
||||||
|
|
@ -34,9 +35,15 @@ def format_datetime_for_api(date_input) -> str:
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Date must be string or datetime object, got {type(date_input)}")
|
raise ValueError(f"Date must be string or datetime object, got {type(date_input)}")
|
||||||
|
|
||||||
|
class AlphaVantageRateLimitError(Exception):
|
||||||
|
"""Exception raised when Alpha Vantage API rate limit is exceeded."""
|
||||||
|
pass
|
||||||
|
|
||||||
def _make_api_request(function_name: str, params: dict) -> dict | str:
|
def _make_api_request(function_name: str, params: dict) -> dict | str:
|
||||||
"""Helper function to make API requests and handle responses.
|
"""Helper function to make API requests and handle responses.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
AlphaVantageRateLimitError: When API rate limit is exceeded
|
||||||
"""
|
"""
|
||||||
# Create a copy of params to avoid modifying the original
|
# Create a copy of params to avoid modifying the original
|
||||||
api_params = params.copy()
|
api_params = params.copy()
|
||||||
|
|
@ -61,6 +68,18 @@ def _make_api_request(function_name: str, params: dict) -> dict | str:
|
||||||
|
|
||||||
response_text = response.text
|
response_text = response.text
|
||||||
|
|
||||||
|
# Check if response is JSON (error responses are typically JSON)
|
||||||
|
try:
|
||||||
|
response_json = json.loads(response_text)
|
||||||
|
# Check for rate limit error
|
||||||
|
if "Information" in response_json:
|
||||||
|
info_message = response_json["Information"]
|
||||||
|
if "rate limit" in info_message.lower() or "api key" in info_message.lower():
|
||||||
|
raise AlphaVantageRateLimitError(f"Alpha Vantage rate limit exceeded: {info_message}")
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
# Response is not JSON (likely CSV data), which is normal
|
||||||
|
pass
|
||||||
|
|
||||||
return response_text
|
return response_text
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ from typing import Annotated
|
||||||
|
|
||||||
# Import from vendor-specific modules
|
# Import from vendor-specific modules
|
||||||
from .local import get_YFin_data, get_finnhub_news, get_finnhub_company_insider_sentiment, get_finnhub_company_insider_transactions, get_simfin_balance_sheet, get_simfin_cashflow, get_simfin_income_statements, get_reddit_global_news, get_reddit_company_news
|
from .local import get_YFin_data, get_finnhub_news, get_finnhub_company_insider_sentiment, get_finnhub_company_insider_transactions, get_simfin_balance_sheet, get_simfin_cashflow, get_simfin_income_statements, get_reddit_global_news, get_reddit_company_news
|
||||||
from .y_finance import get_YFin_data_online, get_stock_stats_indicators_window
|
from .y_finance import get_YFin_data_online, get_stock_stats_indicators_window, get_balance_sheet as get_yfinance_balance_sheet, get_cashflow as get_yfinance_cashflow, get_income_statement as get_yfinance_income_statement, get_insider_transactions as get_yfinance_insider_transactions
|
||||||
from .google import get_google_news
|
from .google import get_google_news
|
||||||
from .openai import get_stock_news_openai, get_global_news_openai, get_fundamentals_openai
|
from .openai import get_stock_news_openai, get_global_news_openai, get_fundamentals_openai
|
||||||
from .alpha_vantage import (
|
from .alpha_vantage import (
|
||||||
|
|
@ -15,6 +15,7 @@ from .alpha_vantage import (
|
||||||
get_insider_transactions as get_alpha_vantage_insider_transactions,
|
get_insider_transactions as get_alpha_vantage_insider_transactions,
|
||||||
get_news as get_alpha_vantage_news
|
get_news as get_alpha_vantage_news
|
||||||
)
|
)
|
||||||
|
from .alpha_vantage_common import AlphaVantageRateLimitError
|
||||||
|
|
||||||
# Configuration and routing logic
|
# Configuration and routing logic
|
||||||
from .config import get_config
|
from .config import get_config
|
||||||
|
|
@ -81,14 +82,17 @@ VENDOR_METHODS = {
|
||||||
},
|
},
|
||||||
"get_balance_sheet": {
|
"get_balance_sheet": {
|
||||||
"alpha_vantage": get_alpha_vantage_balance_sheet,
|
"alpha_vantage": get_alpha_vantage_balance_sheet,
|
||||||
|
"yfinance": get_yfinance_balance_sheet,
|
||||||
"local": get_simfin_balance_sheet,
|
"local": get_simfin_balance_sheet,
|
||||||
},
|
},
|
||||||
"get_cashflow": {
|
"get_cashflow": {
|
||||||
"alpha_vantage": get_alpha_vantage_cashflow,
|
"alpha_vantage": get_alpha_vantage_cashflow,
|
||||||
|
"yfinance": get_yfinance_cashflow,
|
||||||
"local": get_simfin_cashflow,
|
"local": get_simfin_cashflow,
|
||||||
},
|
},
|
||||||
"get_income_statement": {
|
"get_income_statement": {
|
||||||
"alpha_vantage": get_alpha_vantage_income_statement,
|
"alpha_vantage": get_alpha_vantage_income_statement,
|
||||||
|
"yfinance": get_yfinance_income_statement,
|
||||||
"local": get_simfin_income_statements,
|
"local": get_simfin_income_statements,
|
||||||
},
|
},
|
||||||
# news_data
|
# news_data
|
||||||
|
|
@ -107,6 +111,7 @@ VENDOR_METHODS = {
|
||||||
},
|
},
|
||||||
"get_insider_transactions": {
|
"get_insider_transactions": {
|
||||||
"alpha_vantage": get_alpha_vantage_insider_transactions,
|
"alpha_vantage": get_alpha_vantage_insider_transactions,
|
||||||
|
"yfinance": get_yfinance_insider_transactions,
|
||||||
"local": get_finnhub_company_insider_transactions,
|
"local": get_finnhub_company_insider_transactions,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -134,42 +139,102 @@ def get_vendor(category: str, method: str = None) -> str:
|
||||||
return config.get("data_vendors", {}).get(category, "default")
|
return config.get("data_vendors", {}).get(category, "default")
|
||||||
|
|
||||||
def route_to_vendor(method: str, *args, **kwargs):
|
def route_to_vendor(method: str, *args, **kwargs):
|
||||||
"""Route method calls to appropriate vendor implementation."""
|
"""Route method calls to appropriate vendor implementation with fallback support."""
|
||||||
category = get_category_for_method(method)
|
category = get_category_for_method(method)
|
||||||
vendor_config = get_vendor(category, method)
|
vendor_config = get_vendor(category, method)
|
||||||
|
|
||||||
# Handle comma-separated vendors
|
# Handle comma-separated vendors
|
||||||
vendors = [v.strip() for v in vendor_config.split(',')]
|
primary_vendors = [v.strip() for v in vendor_config.split(',')]
|
||||||
|
|
||||||
if method not in VENDOR_METHODS:
|
if method not in VENDOR_METHODS:
|
||||||
raise ValueError(f"Method '{method}' not supported")
|
raise ValueError(f"Method '{method}' not supported")
|
||||||
|
|
||||||
# Collect all methods to run
|
# Get all available vendors for this method for fallback
|
||||||
methods_to_run = []
|
all_available_vendors = list(VENDOR_METHODS[method].keys())
|
||||||
|
|
||||||
for vendor in vendors:
|
# Create fallback vendor list: primary vendors first, then remaining vendors as fallbacks
|
||||||
|
fallback_vendors = primary_vendors.copy()
|
||||||
|
for vendor in all_available_vendors:
|
||||||
|
if vendor not in fallback_vendors:
|
||||||
|
fallback_vendors.append(vendor)
|
||||||
|
|
||||||
|
# Debug: Print fallback ordering
|
||||||
|
primary_str = " → ".join(primary_vendors)
|
||||||
|
fallback_str = " → ".join(fallback_vendors)
|
||||||
|
print(f"DEBUG: {method} - Primary: [{primary_str}] | Full fallback order: [{fallback_str}]")
|
||||||
|
|
||||||
|
# Track results and execution state
|
||||||
|
results = []
|
||||||
|
vendor_attempt_count = 0
|
||||||
|
any_primary_vendor_attempted = False
|
||||||
|
successful_vendor = None
|
||||||
|
|
||||||
|
for vendor in fallback_vendors:
|
||||||
if vendor not in VENDOR_METHODS[method]:
|
if vendor not in VENDOR_METHODS[method]:
|
||||||
print(f"Info: Vendor '{vendor}' not supported for method '{method}', ignoring")
|
if vendor in primary_vendors:
|
||||||
|
print(f"INFO: Vendor '{vendor}' not supported for method '{method}', falling back to next vendor")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
vendor_impl = VENDOR_METHODS[method][vendor]
|
vendor_impl = VENDOR_METHODS[method][vendor]
|
||||||
|
is_primary_vendor = vendor in primary_vendors
|
||||||
|
vendor_attempt_count += 1
|
||||||
|
|
||||||
|
# Track if we attempted any primary vendor
|
||||||
|
if is_primary_vendor:
|
||||||
|
any_primary_vendor_attempted = True
|
||||||
|
|
||||||
|
# Debug: Print current attempt
|
||||||
|
vendor_type = "PRIMARY" if is_primary_vendor else "FALLBACK"
|
||||||
|
print(f"DEBUG: Attempting {vendor_type} vendor '{vendor}' for {method} (attempt #{vendor_attempt_count})")
|
||||||
|
|
||||||
# Handle list of methods for a vendor
|
# Handle list of methods for a vendor
|
||||||
if isinstance(vendor_impl, list):
|
if isinstance(vendor_impl, list):
|
||||||
methods_to_run.extend(vendor_impl)
|
vendor_methods = [(impl, vendor) for impl in vendor_impl]
|
||||||
|
print(f"DEBUG: Vendor '{vendor}' has multiple implementations: {len(vendor_methods)} functions")
|
||||||
else:
|
else:
|
||||||
# Single method implementation
|
vendor_methods = [(vendor_impl, vendor)]
|
||||||
methods_to_run.append(vendor_impl)
|
|
||||||
|
|
||||||
# Run all methods and collect results
|
# Run methods for this vendor
|
||||||
results = []
|
vendor_results = []
|
||||||
for impl_func in methods_to_run:
|
for impl_func, vendor_name in vendor_methods:
|
||||||
try:
|
try:
|
||||||
|
print(f"DEBUG: Calling {impl_func.__name__} from vendor '{vendor_name}'...")
|
||||||
result = impl_func(*args, **kwargs)
|
result = impl_func(*args, **kwargs)
|
||||||
results.append(result)
|
vendor_results.append(result)
|
||||||
|
print(f"SUCCESS: {impl_func.__name__} from vendor '{vendor_name}' completed successfully")
|
||||||
|
|
||||||
|
except AlphaVantageRateLimitError as e:
|
||||||
|
if vendor == "alpha_vantage":
|
||||||
|
print(f"RATE_LIMIT: Alpha Vantage rate limit exceeded, falling back to next available vendor")
|
||||||
|
print(f"DEBUG: Rate limit details: {e}")
|
||||||
|
# Continue to next vendor for fallback
|
||||||
|
continue
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Log error but continue with other implementations
|
# Log error but continue with other implementations
|
||||||
print(f"Warning: {impl_func.__name__} failed: {e}")
|
print(f"FAILED: {impl_func.__name__} from vendor '{vendor_name}' failed: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Add this vendor's results
|
||||||
|
if vendor_results:
|
||||||
|
results.extend(vendor_results)
|
||||||
|
successful_vendor = vendor
|
||||||
|
result_summary = f"Got {len(vendor_results)} result(s)"
|
||||||
|
print(f"SUCCESS: Vendor '{vendor}' succeeded - {result_summary}")
|
||||||
|
|
||||||
|
# Stopping logic: Stop after first successful vendor for single-vendor configs
|
||||||
|
# Multiple vendor configs (comma-separated) may want to collect from multiple sources
|
||||||
|
if len(primary_vendors) == 1:
|
||||||
|
print(f"DEBUG: Stopping after successful vendor '{vendor}' (single-vendor config)")
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
print(f"FAILED: Vendor '{vendor}' produced no results")
|
||||||
|
|
||||||
|
# Final result summary
|
||||||
|
if not results:
|
||||||
|
print(f"FAILURE: All {vendor_attempt_count} vendor attempts failed for method '{method}'")
|
||||||
|
raise RuntimeError(f"All vendor implementations failed for method '{method}'")
|
||||||
|
else:
|
||||||
|
print(f"FINAL: Method '{method}' completed with {len(results)} result(s) from {vendor_attempt_count} vendor attempt(s)")
|
||||||
|
|
||||||
# Return single result if only one, otherwise concatenate as string
|
# Return single result if only one, otherwise concatenate as string
|
||||||
if len(results) == 1:
|
if len(results) == 1:
|
||||||
|
|
|
||||||
|
|
@ -182,3 +182,117 @@ def get_stockstats_indicator(
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
return str(indicator_value)
|
return str(indicator_value)
|
||||||
|
|
||||||
|
|
||||||
|
def get_balance_sheet(
|
||||||
|
ticker: Annotated[str, "ticker symbol of the company"],
|
||||||
|
freq: Annotated[str, "frequency of data: 'annual' or 'quarterly'"] = "quarterly",
|
||||||
|
curr_date: Annotated[str, "current date (not used for yfinance)"] = None
|
||||||
|
):
|
||||||
|
"""Get balance sheet data from yfinance."""
|
||||||
|
try:
|
||||||
|
ticker_obj = yf.Ticker(ticker.upper())
|
||||||
|
|
||||||
|
if freq.lower() == "quarterly":
|
||||||
|
data = ticker_obj.quarterly_balance_sheet
|
||||||
|
else:
|
||||||
|
data = ticker_obj.balance_sheet
|
||||||
|
|
||||||
|
if data.empty:
|
||||||
|
return f"No balance sheet data found for symbol '{ticker}'"
|
||||||
|
|
||||||
|
# Convert to CSV string for consistency with other functions
|
||||||
|
csv_string = data.to_csv()
|
||||||
|
|
||||||
|
# Add header information
|
||||||
|
header = f"# Balance Sheet data for {ticker.upper()} ({freq})\n"
|
||||||
|
header += f"# Data retrieved on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
||||||
|
|
||||||
|
return header + csv_string
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return f"Error retrieving balance sheet for {ticker}: {str(e)}"
|
||||||
|
|
||||||
|
|
||||||
|
def get_cashflow(
|
||||||
|
ticker: Annotated[str, "ticker symbol of the company"],
|
||||||
|
freq: Annotated[str, "frequency of data: 'annual' or 'quarterly'"] = "quarterly",
|
||||||
|
curr_date: Annotated[str, "current date (not used for yfinance)"] = None
|
||||||
|
):
|
||||||
|
"""Get cash flow data from yfinance."""
|
||||||
|
try:
|
||||||
|
ticker_obj = yf.Ticker(ticker.upper())
|
||||||
|
|
||||||
|
if freq.lower() == "quarterly":
|
||||||
|
data = ticker_obj.quarterly_cashflow
|
||||||
|
else:
|
||||||
|
data = ticker_obj.cashflow
|
||||||
|
|
||||||
|
if data.empty:
|
||||||
|
return f"No cash flow data found for symbol '{ticker}'"
|
||||||
|
|
||||||
|
# Convert to CSV string for consistency with other functions
|
||||||
|
csv_string = data.to_csv()
|
||||||
|
|
||||||
|
# Add header information
|
||||||
|
header = f"# Cash Flow data for {ticker.upper()} ({freq})\n"
|
||||||
|
header += f"# Data retrieved on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
||||||
|
|
||||||
|
return header + csv_string
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return f"Error retrieving cash flow for {ticker}: {str(e)}"
|
||||||
|
|
||||||
|
|
||||||
|
def get_income_statement(
|
||||||
|
ticker: Annotated[str, "ticker symbol of the company"],
|
||||||
|
freq: Annotated[str, "frequency of data: 'annual' or 'quarterly'"] = "quarterly",
|
||||||
|
curr_date: Annotated[str, "current date (not used for yfinance)"] = None
|
||||||
|
):
|
||||||
|
"""Get income statement data from yfinance."""
|
||||||
|
try:
|
||||||
|
ticker_obj = yf.Ticker(ticker.upper())
|
||||||
|
|
||||||
|
if freq.lower() == "quarterly":
|
||||||
|
data = ticker_obj.quarterly_income_stmt
|
||||||
|
else:
|
||||||
|
data = ticker_obj.income_stmt
|
||||||
|
|
||||||
|
if data.empty:
|
||||||
|
return f"No income statement data found for symbol '{ticker}'"
|
||||||
|
|
||||||
|
# Convert to CSV string for consistency with other functions
|
||||||
|
csv_string = data.to_csv()
|
||||||
|
|
||||||
|
# Add header information
|
||||||
|
header = f"# Income Statement data for {ticker.upper()} ({freq})\n"
|
||||||
|
header += f"# Data retrieved on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
||||||
|
|
||||||
|
return header + csv_string
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return f"Error retrieving income statement for {ticker}: {str(e)}"
|
||||||
|
|
||||||
|
|
||||||
|
def get_insider_transactions(
|
||||||
|
ticker: Annotated[str, "ticker symbol of the company"]
|
||||||
|
):
|
||||||
|
"""Get insider transactions data from yfinance."""
|
||||||
|
try:
|
||||||
|
ticker_obj = yf.Ticker(ticker.upper())
|
||||||
|
data = ticker_obj.insider_transactions
|
||||||
|
|
||||||
|
if data is None or data.empty:
|
||||||
|
return f"No insider transactions data found for symbol '{ticker}'"
|
||||||
|
|
||||||
|
# Convert to CSV string for consistency with other functions
|
||||||
|
csv_string = data.to_csv()
|
||||||
|
|
||||||
|
# Add header information
|
||||||
|
header = f"# Insider Transactions data for {ticker.upper()}\n"
|
||||||
|
header += f"# Data retrieved on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
||||||
|
|
||||||
|
return header + csv_string
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return f"Error retrieving insider transactions for {ticker}: {str(e)}"
|
||||||
Loading…
Reference in New Issue