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)
This commit is contained in:
Yijia-Xiao
2026-09-17 05:00:08 +00:00
parent 486dec1710
commit 63989515c9
3 changed files with 100 additions and 15 deletions

View File

@@ -42,6 +42,7 @@ from cli.utils import (
select_research_depth, select_research_depth,
select_shallow_thinking_agent, select_shallow_thinking_agent,
) )
from tradingagents.agents.utils.rating import is_review
from tradingagents.backtest import iter_grid, run_backtest, summarize from tradingagents.backtest import iter_grid, run_backtest, summarize
from tradingagents.default_config import DEFAULT_CONFIG from tradingagents.default_config import DEFAULT_CONFIG
from tradingagents.graph.analyst_execution import ( from tradingagents.graph.analyst_execution import (
@@ -916,21 +917,16 @@ def extract_content_string(content):
"""Extract string content from various message formats. """Extract string content from various message formats.
Returns None if no meaningful text content is found. Returns None if no meaningful text content is found.
""" """
import ast
def is_empty(val): def is_empty(val):
"""Check if value is empty using Python's truthiness.""" """Whether a value carries nothing to show.
if val is None or val == '':
return True 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): if isinstance(val, str):
s = val.strip() return not val.strip()
if not s: return val is None or not bool(val)
return True
try:
return not bool(ast.literal_eval(s))
except (ValueError, SyntaxError):
return False # Can't parse = real text
return not bool(val)
if is_empty(content): if is_empty(content):
return None return None
@@ -1097,7 +1093,9 @@ def run_analysis(checkpoint: bool | None = None, portfolio=None):
# Now start the display layout # Now start the display layout
layout = create_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 # Initial display
update_display(layout, stats_handler=stats_handler, start_time=start_time) 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) # Post-analysis prompts (outside Live context for clean interaction)
console.print("\n[bold cyan]Analysis Complete![/bold cyan]\n") 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]") console.print(f"[dim]{analyst_wall_time_tracker.format_summary()}[/dim]")
# Prompt to save report # Prompt to save report

77
tests/test_cli_display.py Normal file
View File

@@ -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)

View File

@@ -659,7 +659,8 @@ class TradingAgentsGraph:
log_path = directory / f"full_states_log_{trade_date}.json" log_path = directory / f"full_states_log_{trade_date}.json"
with open(log_path, "w", encoding="utf-8") as f: 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): def process_signal(self, full_signal):
"""Process a signal to extract the core decision.""" """Process a signal to extract the core decision."""