Spaces:
Sleeping
Sleeping
Fix: Metric extraction to match MCP data structure
Browse files- Fix fundamentals to use sec_edgar/yahoo_finance structure
- Fix valuation to use yahoo_finance.data structure
- Fix volatility to use yahoo_finance.data + market_volatility_context
- Fix macro to use bea_bls/fred structure
- Reduce critic source_data truncation to 4000 chars (was 8000)
- Fallback to multi_source if metrics path empty
Fixes "No metrics extracted" error and 413 Payload Too Large.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- src/nodes/analyzer.py +40 -29
- src/nodes/critic.py +2 -2
src/nodes/analyzer.py
CHANGED
|
@@ -579,10 +579,17 @@ def _extract_key_metrics(raw_data: str) -> dict:
|
|
| 579 |
}
|
| 580 |
|
| 581 |
# Extract fundamentals with temporal data
|
|
|
|
|
|
|
| 582 |
fin = metrics.get("fundamentals", {})
|
|
|
|
|
|
|
| 583 |
if fin and "error" not in fin:
|
| 584 |
-
|
| 585 |
-
|
|
|
|
|
|
|
|
|
|
| 586 |
extracted["fundamentals"] = {
|
| 587 |
"revenue": _extract_temporal_metric(fin_data.get("revenue", {})),
|
| 588 |
"revenue_cagr_3yr": fin_data.get("revenue_growth_3yr"),
|
|
@@ -590,42 +597,41 @@ def _extract_key_metrics(raw_data: str) -> dict:
|
|
| 590 |
"gross_margin": _extract_temporal_metric(fin_data.get("gross_margin_pct", {})),
|
| 591 |
"operating_margin": _extract_temporal_metric(fin_data.get("operating_margin_pct", {})),
|
| 592 |
"eps": _extract_temporal_metric(fin_data.get("eps", {})),
|
| 593 |
-
"debt_to_equity": _extract_temporal_metric(
|
| 594 |
-
"free_cash_flow": _extract_temporal_metric(
|
| 595 |
"net_income": _extract_temporal_metric(fin_data.get("net_income", {})),
|
| 596 |
}
|
| 597 |
|
| 598 |
# Extract valuation (with temporal data)
|
|
|
|
| 599 |
val = metrics.get("valuation", {})
|
|
|
|
|
|
|
| 600 |
if val and "error" not in val:
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
# Get valuation date from sources or response-level
|
| 604 |
-
val_date = (
|
| 605 |
-
val.get("sources", {}).get("yahoo_finance", {}).get("regular_market_time")
|
| 606 |
-
or val.get("as_of")
|
| 607 |
-
or (val.get("generated_at", "")[:10] if val.get("generated_at") else None)
|
| 608 |
-
)
|
| 609 |
extracted["valuation"] = {
|
| 610 |
-
"pe_trailing": {"value":
|
| 611 |
-
"pe_forward": {"value":
|
| 612 |
-
"pb_ratio": {"value":
|
| 613 |
-
"ps_ratio": {"value":
|
| 614 |
-
"ev_ebitda": {"value":
|
| 615 |
"valuation_signal": val.get("overall_signal"),
|
| 616 |
"as_of": val_date,
|
| 617 |
}
|
| 618 |
|
| 619 |
# Extract volatility (with temporal data)
|
|
|
|
| 620 |
vol = metrics.get("volatility", {})
|
|
|
|
|
|
|
| 621 |
if vol and "error" not in vol:
|
| 622 |
-
|
| 623 |
-
|
| 624 |
vol_date = vol.get("generated_at", "")[:10] if vol.get("generated_at") else None
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
hv_data = vol_metrics.get("historical_volatility", {})
|
| 629 |
extracted["volatility"] = {
|
| 630 |
"beta": {"value": beta_data.get("value") if isinstance(beta_data, dict) else beta_data,
|
| 631 |
"end_date": beta_data.get("date") or vol_date if isinstance(beta_data, dict) else vol_date},
|
|
@@ -637,14 +643,19 @@ def _extract_key_metrics(raw_data: str) -> dict:
|
|
| 637 |
}
|
| 638 |
|
| 639 |
# Extract macro (with temporal data)
|
|
|
|
| 640 |
macro = metrics.get("macro", {})
|
|
|
|
|
|
|
| 641 |
if macro and "error" not in macro:
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
|
|
|
|
|
|
|
| 648 |
extracted["macro"] = {
|
| 649 |
"gdp_growth": {"value": gdp.get("value") if isinstance(gdp, dict) else gdp,
|
| 650 |
"end_date": gdp.get("date") or gdp.get("period") if isinstance(gdp, dict) else None},
|
|
|
|
| 579 |
}
|
| 580 |
|
| 581 |
# Extract fundamentals with temporal data
|
| 582 |
+
# Structure: metrics.fundamentals = {"sec_edgar": {"data": {...}}, "yahoo_finance": {"data": {...}}}
|
| 583 |
+
# Also check multi_source.fundamentals_all for the same structure
|
| 584 |
fin = metrics.get("fundamentals", {})
|
| 585 |
+
if not fin or "error" in fin:
|
| 586 |
+
fin = data.get("multi_source", {}).get("fundamentals_all", {})
|
| 587 |
if fin and "error" not in fin:
|
| 588 |
+
# Use SEC EDGAR as primary, Yahoo Finance as fallback
|
| 589 |
+
sec_data = fin.get("sec_edgar", {}).get("data", {})
|
| 590 |
+
yf_data = fin.get("yahoo_finance", {}).get("data", {})
|
| 591 |
+
# Merge with SEC as primary
|
| 592 |
+
fin_data = {**yf_data, **sec_data} # SEC overwrites YF where both exist
|
| 593 |
extracted["fundamentals"] = {
|
| 594 |
"revenue": _extract_temporal_metric(fin_data.get("revenue", {})),
|
| 595 |
"revenue_cagr_3yr": fin_data.get("revenue_growth_3yr"),
|
|
|
|
| 597 |
"gross_margin": _extract_temporal_metric(fin_data.get("gross_margin_pct", {})),
|
| 598 |
"operating_margin": _extract_temporal_metric(fin_data.get("operating_margin_pct", {})),
|
| 599 |
"eps": _extract_temporal_metric(fin_data.get("eps", {})),
|
| 600 |
+
"debt_to_equity": _extract_temporal_metric(fin_data.get("debt_to_equity", {})),
|
| 601 |
+
"free_cash_flow": _extract_temporal_metric(fin_data.get("free_cash_flow", {})),
|
| 602 |
"net_income": _extract_temporal_metric(fin_data.get("net_income", {})),
|
| 603 |
}
|
| 604 |
|
| 605 |
# Extract valuation (with temporal data)
|
| 606 |
+
# Structure: {"yahoo_finance": {"data": {...}, "regular_market_time": "..."}}
|
| 607 |
val = metrics.get("valuation", {})
|
| 608 |
+
if not val or "error" in val:
|
| 609 |
+
val = data.get("multi_source", {}).get("valuation_all", {})
|
| 610 |
if val and "error" not in val:
|
| 611 |
+
yf_val = val.get("yahoo_finance", {}).get("data", {})
|
| 612 |
+
val_date = val.get("yahoo_finance", {}).get("regular_market_time")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 613 |
extracted["valuation"] = {
|
| 614 |
+
"pe_trailing": {"value": yf_val.get("trailing_pe"), "end_date": val_date},
|
| 615 |
+
"pe_forward": {"value": yf_val.get("forward_pe"), "end_date": val_date},
|
| 616 |
+
"pb_ratio": {"value": yf_val.get("pb_ratio"), "end_date": val_date},
|
| 617 |
+
"ps_ratio": {"value": yf_val.get("ps_ratio"), "end_date": val_date},
|
| 618 |
+
"ev_ebitda": {"value": yf_val.get("ev_ebitda"), "end_date": val_date},
|
| 619 |
"valuation_signal": val.get("overall_signal"),
|
| 620 |
"as_of": val_date,
|
| 621 |
}
|
| 622 |
|
| 623 |
# Extract volatility (with temporal data)
|
| 624 |
+
# Structure: {"yahoo_finance": {"data": {...}}, "market_volatility_context": {"vix": {...}, "vxn": {...}}}
|
| 625 |
vol = metrics.get("volatility", {})
|
| 626 |
+
if not vol or "error" in vol:
|
| 627 |
+
vol = data.get("multi_source", {}).get("volatility_all", {})
|
| 628 |
if vol and "error" not in vol:
|
| 629 |
+
yf_vol = vol.get("yahoo_finance", {}).get("data", {})
|
| 630 |
+
mkt_ctx = vol.get("market_volatility_context", {})
|
| 631 |
vol_date = vol.get("generated_at", "")[:10] if vol.get("generated_at") else None
|
| 632 |
+
vix_data = mkt_ctx.get("vix", {})
|
| 633 |
+
beta_data = yf_vol.get("beta", {})
|
| 634 |
+
hv_data = yf_vol.get("historical_volatility", {})
|
|
|
|
| 635 |
extracted["volatility"] = {
|
| 636 |
"beta": {"value": beta_data.get("value") if isinstance(beta_data, dict) else beta_data,
|
| 637 |
"end_date": beta_data.get("date") or vol_date if isinstance(beta_data, dict) else vol_date},
|
|
|
|
| 643 |
}
|
| 644 |
|
| 645 |
# Extract macro (with temporal data)
|
| 646 |
+
# Structure: {"bea_bls": {"data": {...}}, "fred": {"data": {...}}}
|
| 647 |
macro = metrics.get("macro", {})
|
| 648 |
+
if not macro or "error" in macro:
|
| 649 |
+
macro = data.get("multi_source", {}).get("macro_all", {})
|
| 650 |
if macro and "error" not in macro:
|
| 651 |
+
bea_bls = macro.get("bea_bls", {}).get("data", {})
|
| 652 |
+
fred = macro.get("fred", {}).get("data", {})
|
| 653 |
+
# Merge sources (BEA/BLS primary, FRED fallback)
|
| 654 |
+
macro_data = {**fred, **bea_bls}
|
| 655 |
+
gdp = macro_data.get("gdp_growth", {})
|
| 656 |
+
interest = macro_data.get("interest_rate", {})
|
| 657 |
+
inflation = macro_data.get("cpi_inflation", {})
|
| 658 |
+
unemp = macro_data.get("unemployment", {})
|
| 659 |
extracted["macro"] = {
|
| 660 |
"gdp_growth": {"value": gdp.get("value") if isinstance(gdp, dict) else gdp,
|
| 661 |
"end_date": gdp.get("date") or gdp.get("period") if isinstance(gdp, dict) else None},
|
src/nodes/critic.py
CHANGED
|
@@ -182,8 +182,8 @@ def run_llm_evaluation(report: str, source_data: str, iteration: int, llm) -> di
|
|
| 182 |
Returns:
|
| 183 |
Evaluation result dict with scores, status, and feedback
|
| 184 |
"""
|
| 185 |
-
# Truncate source data if too long
|
| 186 |
-
max_source_len =
|
| 187 |
if len(source_data) > max_source_len:
|
| 188 |
source_data = source_data[:max_source_len] + "\n... [truncated]"
|
| 189 |
|
|
|
|
| 182 |
Returns:
|
| 183 |
Evaluation result dict with scores, status, and feedback
|
| 184 |
"""
|
| 185 |
+
# Truncate source data if too long (Groq has ~8K token limit)
|
| 186 |
+
max_source_len = 4000
|
| 187 |
if len(source_data) > max_source_len:
|
| 188 |
source_data = source_data[:max_source_len] + "\n... [truncated]"
|
| 189 |
|