From 63989515c986eca2b938ba4e1acdced509603474 Mon Sep 17 00:00:00 2001 From: Yijia-Xiao Date: Thu, 17 Sep 2026 05:00:08 +0000 Subject: [PATCH] fix(cli): show what the run actually produced (#649, #784) - a non-blank message is text, whatever it would mean as a Python literal - the live layout renders on the alternate screen, so a tall layout does not scroll - a run with no readable rating says so instead of looking like a normal result - the state log keeps non-ASCII readable (#1081) --- cli/main.py | 35 ++++++++----- tests/test_cli_display.py | 77 ++++++++++++++++++++++++++++ tradingagents/graph/trading_graph.py | 3 +- 3 files changed, 100 insertions(+), 15 deletions(-) create mode 100644 tests/test_cli_display.py diff --git a/cli/main.py b/cli/main.py index 00a233426..12e2cfd1c 100644 --- a/cli/main.py +++ b/cli/main.py @@ -42,6 +42,7 @@ from cli.utils import ( select_research_depth, select_shallow_thinking_agent, ) +from tradingagents.agents.utils.rating import is_review from tradingagents.backtest import iter_grid, run_backtest, summarize from tradingagents.default_config import DEFAULT_CONFIG from tradingagents.graph.analyst_execution import ( @@ -916,21 +917,16 @@ def extract_content_string(content): """Extract string content from various message formats. Returns None if no meaningful text content is found. """ - import ast - def is_empty(val): - """Check if value is empty using Python's truthiness.""" - if val is None or val == '': - return True + """Whether a value carries nothing to show. + + Text is judged by whether anything was written, not by what it would + mean as Python: a report saying "0" or "None" is a message the run + produced, and reading it as a falsy literal dropped it from the display. + """ if isinstance(val, str): - s = val.strip() - if not s: - return True - try: - return not bool(ast.literal_eval(s)) - except (ValueError, SyntaxError): - return False # Can't parse = real text - return not bool(val) + return not val.strip() + return val is None or not bool(val) if is_empty(content): return None @@ -1097,7 +1093,9 @@ def run_analysis(checkpoint: bool | None = None, portfolio=None): # Now start the display layout layout = create_layout() - with Live(layout, refresh_per_second=4): + # The alternate screen keeps a layout taller than the window from redrawing + # by scrolling; the final report prints after this block, on the normal screen. + with Live(layout, refresh_per_second=4, screen=True): # Initial display update_display(layout, stats_handler=stats_handler, start_time=start_time) @@ -1286,6 +1284,15 @@ def run_analysis(checkpoint: bool | None = None, portfolio=None): # Post-analysis prompts (outside Live context for clean interaction) console.print("\n[bold cyan]Analysis Complete![/bold cyan]\n") + + # A decision nobody can read is not a position. Say so here rather than + # leaving the run to look like a normal result. + if is_review(graph.process_signal(final_state.get("final_trade_decision", ""))): + console.print( + "[yellow]No rating could be read from the final decision, so this run " + "is recorded for review rather than as a position. Re-run, or read the " + "decision text below and judge it yourself.[/yellow]\n" + ) console.print(f"[dim]{analyst_wall_time_tracker.format_summary()}[/dim]") # Prompt to save report diff --git a/tests/test_cli_display.py b/tests/test_cli_display.py new file mode 100644 index 000000000..6c7eadd65 --- /dev/null +++ b/tests/test_cli_display.py @@ -0,0 +1,77 @@ +"""What the live display shows, and what the run log keeps. + +The display drops a message it judges empty, and the state log is written for a +person to read afterwards. Both got that wrong in ways that hide real content. +""" + +from __future__ import annotations + +import json + +import pytest + +from cli.main import extract_content_string + + +@pytest.mark.unit +@pytest.mark.parametrize("text", ["0", "False", "None", "[]", "{}", "0.0"]) +def test_a_message_that_reads_like_a_python_value_is_still_text(text): + """These were parsed as Python and judged empty, so the message vanished.""" + assert extract_content_string(text) == text + + +@pytest.mark.unit +@pytest.mark.parametrize("value, expected", [ + (" Hold ", "Hold"), + ("", None), + (" ", None), + (None, None), + ([], None), + ({}, None), + ({"text": "from a dict"}, "from a dict"), + ([{"type": "text", "text": "part one"}, {"type": "text", "text": "part two"}], "part one part two"), +]) +def test_the_other_shapes_are_unchanged(value, expected): + assert extract_content_string(value) == expected + + +@pytest.mark.unit +def test_the_state_log_keeps_non_ascii_readable(tmp_path): + """Reports can be in any language; the log is read by a person.""" + from tradingagents.graph.trading_graph import TradingAgentsGraph + + graph = object.__new__(TradingAgentsGraph) + graph.config = {"results_dir": str(tmp_path)} + graph.ticker = "600519.SS" + graph.log_states_dict = {} + + graph._log_state("2026-09-01", { + "company_of_interest": "600519.SS", "trade_date": "2026-09-01", + "market_report": "市场", "sentiment_report": "情绪", "news_report": "新闻", + "fundamentals_report": "基本面", "investment_plan": "计划", + "trader_investment_plan": "交易计划", "final_trade_decision": "评级: 买入", + "investment_debate_state": {"bull_history": "", "bear_history": "", "history": "", + "current_response": "", "judge_decision": "", "count": 0}, + "risk_debate_state": {"aggressive_history": "", "conservative_history": "", + "neutral_history": "", "history": "", "judge_decision": "", + "latest_speaker": "", "current_aggressive_response": "", + "current_conservative_response": "", "current_neutral_response": "", + "count": 0}, + }) + + written = next(tmp_path.rglob("full_states_log*.json")).read_text(encoding="utf-8") + assert "买入" in written + assert "\\u" not in written + assert json.loads(written) # still valid JSON + + +@pytest.mark.unit +def test_the_live_display_does_not_scroll_the_terminal(): + """A layout taller than the window makes rich redraw by scrolling, which + reads as flicker; the alternate screen holds it in place (#784). The final + report prints after the live view ends, so nothing is lost when it closes.""" + import inspect + + import cli.main as m + + assert "screen=True" in inspect.getsource(m.run_analysis) diff --git a/tradingagents/graph/trading_graph.py b/tradingagents/graph/trading_graph.py index ad4d268f3..d0ab1237f 100644 --- a/tradingagents/graph/trading_graph.py +++ b/tradingagents/graph/trading_graph.py @@ -659,7 +659,8 @@ class TradingAgentsGraph: log_path = directory / f"full_states_log_{trade_date}.json" with open(log_path, "w", encoding="utf-8") as f: - json.dump(self.log_states_dict[str(trade_date)], f, indent=4) + # Reports can be in any language and this file is read by a person. + json.dump(self.log_states_dict[str(trade_date)], f, indent=4, ensure_ascii=False) def process_signal(self, full_signal): """Process a signal to extract the core decision."""