From 0669f30aae3f5ea4f6ebf981b666ae09fe64d66f Mon Sep 17 00:00:00 2001 From: cdogwash72 Date: Tue, 14 Jul 2026 01:34:59 +0000 Subject: [PATCH 1/3] Fix 'No Decision' on every run: graph.stream chunks are node-name keyed final_state accumulates {node_name: state} chunks, so final_state.get('final_trade_decision') could never hit - every run displayed 'No Decision' regardless of model or market view. Parse the 5-tier rating from the Portfolio Manager's judge_decision text via the shared deterministic parse_rating; also fix the settle-time report copy that used the same wrong shape. Co-Authored-By: Claude Fable 5 --- gui/run_manager.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/gui/run_manager.py b/gui/run_manager.py index a2542ffb468..f8f5d4984b3 100644 --- a/gui/run_manager.py +++ b/gui/run_manager.py @@ -29,6 +29,7 @@ from queue import Empty, Queue from typing import Any +from tradingagents.agents.utils.rating import parse_rating from .agent_map import ( ANALYSTS, FIXED_TEAMS, @@ -363,9 +364,14 @@ def _worker(run: Run, persist_cb=None) -> None: run.roster[agent] = "completed" run.emit({"type": "agents_update", "agents": dict(run.roster)}) - for section in run.reports: - if section in final_state and final_state[section]: - run.reports[section] = str(final_state[section]) + # final_state is keyed by node name (graph.stream chunk shape); + # report fields live INSIDE each node's output state. + for node_state in final_state.values(): + if not isinstance(node_state, dict): + continue + for section in run.reports: + if node_state.get(section): + run.reports[section] = str(node_state[section]) if debate_state.get("bull_history") or debate_state.get("bear_history"): # Format a combined debate report so the existing UI sees it. @@ -373,7 +379,13 @@ def _worker(run: Run, persist_cb=None) -> None: if risk_state.get("aggressive_history") or risk_state.get("conservative_history"): run.reports["final_trade_decision"] = _format_risk(risk_state) - run.decision = (str(final_state.get("final_trade_decision", "")).strip() or None) + # Portfolio Manager verdict: parse the 5-tier rating out of the + # judge decision text (deterministic — same parser the memory + # log uses). final_state never carried this key (node-name keys), + # which is why every run displayed "No Decision". + _pm_text = (risk_state.get("judge_decision") + or run.reports.get("final_trade_decision") or "") + run.decision = parse_rating(_pm_text) if _pm_text.strip() else None _persist_reports(run, results_dir, debate_state, risk_state) From 613a95468e382942bf608d7739b4b1af2dd7d6fc Mon Sep 17 00:00:00 2001 From: cdogwash72 Date: Mon, 13 Jul 2026 02:44:54 +0000 Subject: [PATCH 2/3] GUI runs now maintain the memory log (History tab was permanently empty) The GUI streams graph.graph.stream() directly, bypassing propagate() - so store_decision never ran (trading_memory.md never created), agents never received past_context, and pending outcomes were never resolved. All three now happen in the GUI run path, failure-isolated so memory problems can never block an analysis. Co-Authored-By: Claude Fable 5 --- gui/run_manager.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/gui/run_manager.py b/gui/run_manager.py index f8f5d4984b3..c62a08c5fa8 100644 --- a/gui/run_manager.py +++ b/gui/run_manager.py @@ -334,9 +334,21 @@ def _worker(run: Run, persist_cb=None) -> None: risk_state: dict = {} final_state: dict = {} + # Memory-log parity with CLI propagate(): resolve pending outcomes + # for this ticker, then inject past context so agents see prior + # decisions. Failures here must never block the run. + past_context = "" + try: + if hasattr(graph, "_resolve_pending_entries"): + graph._resolve_pending_entries(run.ticker) + past_context = graph.memory_log.get_past_context(run.ticker) or "" + except Exception: + logger.exception("memory-log context load failed (continuing)") + init_state = { "trade_date": run.date, "company_of_interest": run.ticker, + "past_context": past_context, "investment_debate_state": {"bull_history": "", "bear_history": "", "judge_decision": "", "count": 0}, "risk_debate_state": {"aggressive_history": "", "conservative_history": "", @@ -387,6 +399,18 @@ def _worker(run: Run, persist_cb=None) -> None: or run.reports.get("final_trade_decision") or "") run.decision = parse_rating(_pm_text) if _pm_text.strip() else None + # CLI-parity memory write: propagate() stores the decision for + # deferred reflection; GUI streaming bypassed it, so the History + # tab's trading_memory.md was never created. + if _pm_text.strip(): + try: + graph.memory_log.store_decision( + ticker=run.ticker, trade_date=run.date, + final_trade_decision=_pm_text, + ) + except Exception: + logger.exception("memory-log store failed (continuing)") + _persist_reports(run, results_dir, debate_state, risk_state) run.status = "completed" From e76ab5abd4304c390d8a25a03ea591019e9820ba Mon Sep 17 00:00:00 2001 From: cdogwash72 Date: Tue, 14 Jul 2026 01:35:25 +0000 Subject: [PATCH 3/3] Fix crash when fewer than 4 analysts are selected The GUI (and library callers) allow any analyst subset, but the bull/ bear researchers, all three risk debators, and the final-state report extraction hard-read every report key - selecting fewer than 4 analysts raised KeyError mid-debate. All report reads now .get() with an explicit '(not produced this run)' marker. Reproduce (before): run with analysts=['market'] -> KeyError: 'sentiment_report' in bull_researcher. Co-Authored-By: Claude Fable 5 --- tradingagents/agents/researchers/bear_researcher.py | 11 +++++++---- tradingagents/agents/researchers/bull_researcher.py | 11 +++++++---- tradingagents/agents/risk_mgmt/aggressive_debator.py | 11 +++++++---- .../agents/risk_mgmt/conservative_debator.py | 11 +++++++---- tradingagents/agents/risk_mgmt/neutral_debator.py | 11 +++++++---- tradingagents/graph/trading_graph.py | 8 ++++---- 6 files changed, 39 insertions(+), 24 deletions(-) diff --git a/tradingagents/agents/researchers/bear_researcher.py b/tradingagents/agents/researchers/bear_researcher.py index a15882f8f00..0f5534d8d82 100644 --- a/tradingagents/agents/researchers/bear_researcher.py +++ b/tradingagents/agents/researchers/bear_researcher.py @@ -8,10 +8,13 @@ def bear_node(state) -> dict: bear_history = investment_debate_state.get("bear_history", "") current_response = investment_debate_state.get("current_response", "") - market_research_report = state["market_report"] - sentiment_report = state["sentiment_report"] - news_report = state["news_report"] - fundamentals_report = state["fundamentals_report"] + # .get(): analyst subsets are legal — a missing report reads as + # "(not produced this run)" instead of crashing the debate. + _missing = "(not produced this run)" + market_research_report = state.get("market_report") or _missing + sentiment_report = state.get("sentiment_report") or _missing + news_report = state.get("news_report") or _missing + fundamentals_report = state.get("fundamentals_report") or _missing prompt = f"""You are a Bear Analyst making the case against investing in the stock. Your goal is to present a well-reasoned argument emphasizing risks, challenges, and negative indicators. Leverage the provided research and data to highlight potential downsides and counter bullish arguments effectively. diff --git a/tradingagents/agents/researchers/bull_researcher.py b/tradingagents/agents/researchers/bull_researcher.py index 62c6b2f53c4..a670159ca3f 100644 --- a/tradingagents/agents/researchers/bull_researcher.py +++ b/tradingagents/agents/researchers/bull_researcher.py @@ -8,10 +8,13 @@ def bull_node(state) -> dict: bull_history = investment_debate_state.get("bull_history", "") current_response = investment_debate_state.get("current_response", "") - market_research_report = state["market_report"] - sentiment_report = state["sentiment_report"] - news_report = state["news_report"] - fundamentals_report = state["fundamentals_report"] + # .get(): analyst subsets are legal — a missing report reads as + # "(not produced this run)" instead of crashing the debate. + _missing = "(not produced this run)" + market_research_report = state.get("market_report") or _missing + sentiment_report = state.get("sentiment_report") or _missing + news_report = state.get("news_report") or _missing + fundamentals_report = state.get("fundamentals_report") or _missing prompt = f"""You are a Bull Analyst advocating for investing in the stock. Your task is to build a strong, evidence-based case emphasizing growth potential, competitive advantages, and positive market indicators. Leverage the provided research and data to address concerns and counter bearish arguments effectively. diff --git a/tradingagents/agents/risk_mgmt/aggressive_debator.py b/tradingagents/agents/risk_mgmt/aggressive_debator.py index 2e93161498c..91e606a8617 100644 --- a/tradingagents/agents/risk_mgmt/aggressive_debator.py +++ b/tradingagents/agents/risk_mgmt/aggressive_debator.py @@ -10,10 +10,13 @@ def aggressive_node(state) -> dict: current_conservative_response = risk_debate_state.get("current_conservative_response", "") current_neutral_response = risk_debate_state.get("current_neutral_response", "") - market_research_report = state["market_report"] - sentiment_report = state["sentiment_report"] - news_report = state["news_report"] - fundamentals_report = state["fundamentals_report"] + # .get(): analyst subsets are legal — a missing report reads as + # "(not produced this run)" instead of crashing the debate. + _missing = "(not produced this run)" + market_research_report = state.get("market_report") or _missing + sentiment_report = state.get("sentiment_report") or _missing + news_report = state.get("news_report") or _missing + fundamentals_report = state.get("fundamentals_report") or _missing trader_decision = state["trader_investment_plan"] diff --git a/tradingagents/agents/risk_mgmt/conservative_debator.py b/tradingagents/agents/risk_mgmt/conservative_debator.py index 370c4edd4d1..49b47b9dd3e 100644 --- a/tradingagents/agents/risk_mgmt/conservative_debator.py +++ b/tradingagents/agents/risk_mgmt/conservative_debator.py @@ -10,10 +10,13 @@ def conservative_node(state) -> dict: current_aggressive_response = risk_debate_state.get("current_aggressive_response", "") current_neutral_response = risk_debate_state.get("current_neutral_response", "") - market_research_report = state["market_report"] - sentiment_report = state["sentiment_report"] - news_report = state["news_report"] - fundamentals_report = state["fundamentals_report"] + # .get(): analyst subsets are legal — a missing report reads as + # "(not produced this run)" instead of crashing the debate. + _missing = "(not produced this run)" + market_research_report = state.get("market_report") or _missing + sentiment_report = state.get("sentiment_report") or _missing + news_report = state.get("news_report") or _missing + fundamentals_report = state.get("fundamentals_report") or _missing trader_decision = state["trader_investment_plan"] diff --git a/tradingagents/agents/risk_mgmt/neutral_debator.py b/tradingagents/agents/risk_mgmt/neutral_debator.py index afcde15e5f5..fe1a79e5eb2 100644 --- a/tradingagents/agents/risk_mgmt/neutral_debator.py +++ b/tradingagents/agents/risk_mgmt/neutral_debator.py @@ -10,10 +10,13 @@ def neutral_node(state) -> dict: current_aggressive_response = risk_debate_state.get("current_aggressive_response", "") current_conservative_response = risk_debate_state.get("current_conservative_response", "") - market_research_report = state["market_report"] - sentiment_report = state["sentiment_report"] - news_report = state["news_report"] - fundamentals_report = state["fundamentals_report"] + # .get(): analyst subsets are legal — a missing report reads as + # "(not produced this run)" instead of crashing the debate. + _missing = "(not produced this run)" + market_research_report = state.get("market_report") or _missing + sentiment_report = state.get("sentiment_report") or _missing + news_report = state.get("news_report") or _missing + fundamentals_report = state.get("fundamentals_report") or _missing trader_decision = state["trader_investment_plan"] diff --git a/tradingagents/graph/trading_graph.py b/tradingagents/graph/trading_graph.py index c0d8ecdd923..8c80a6a5fa2 100644 --- a/tradingagents/graph/trading_graph.py +++ b/tradingagents/graph/trading_graph.py @@ -385,10 +385,10 @@ def _log_state(self, trade_date, final_state): self.log_states_dict[str(trade_date)] = { "company_of_interest": final_state["company_of_interest"], "trade_date": final_state["trade_date"], - "market_report": final_state["market_report"], - "sentiment_report": final_state["sentiment_report"], - "news_report": final_state["news_report"], - "fundamentals_report": final_state["fundamentals_report"], + "market_report": final_state.get("market_report", ""), + "sentiment_report": final_state.get("sentiment_report", ""), + "news_report": final_state.get("news_report", ""), + "fundamentals_report": final_state.get("fundamentals_report", ""), "investment_debate_state": { "bull_history": final_state["investment_debate_state"]["bull_history"], "bear_history": final_state["investment_debate_state"]["bear_history"],