fix(agents): stop conflict alone from defaulting the verdict to Hold

- both manager prompts and both rating fields listed "materially conflicting"
  as a Hold condition, and every bull/bear and risk debate is conflicting, so
  a modest directional edge collapsed to Hold
- state one rule at all four sites: conflict alone is not a reason to Hold,
  commit to the stronger side sized by how decisively it wins, and Hold only
  when the evidence is still balanced after weighing or too thin for a call
- a modest bull edge now resolves to Overweight; a genuinely balanced debate
  still resolves to Hold #1321
This commit is contained in:
Yijia-Xiao
2026-09-14 18:09:33 +00:00
parent be952b8ecc
commit 62d3479217
4 changed files with 30 additions and 11 deletions

View File

@@ -7,12 +7,14 @@ behavior we added for the Trader, Research Manager, and Sentiment Analyst
so they share the same deterministic output shape.
"""
import inspect
from unittest.mock import MagicMock
import pytest
from pydantic import ValidationError
from tradingagents.agents.analysts.sentiment_analyst import create_sentiment_analyst
from tradingagents.agents.managers.portfolio_manager import create_portfolio_manager
from tradingagents.agents.managers.research_manager import create_research_manager
from tradingagents.agents.schemas import (
PortfolioDecision,
@@ -484,3 +486,20 @@ class TestSentimentAnalystAgent:
llm.with_structured_output.return_value = structured
llm.invoke.return_value = MagicMock(content=plain)
assert create_sentiment_analyst(llm)(_make_sentiment_state())["sentiment_report"] == plain
@pytest.mark.unit
@pytest.mark.parametrize("source", [
pytest.param(lambda: ResearchPlan.model_fields["recommendation"].description, id="ResearchPlan.recommendation"),
pytest.param(lambda: PortfolioDecision.model_fields["rating"].description, id="PortfolioDecision.rating"),
pytest.param(lambda: inspect.getsource(create_research_manager), id="research_manager prompt"),
pytest.param(lambda: inspect.getsource(create_portfolio_manager), id="portfolio_manager prompt"),
])
def test_conflict_alone_is_not_a_hold_trigger(source):
# The debate always contains conflicting arguments, so a Hold condition that
# conflict satisfies fires on every run and swallows directional calls
# (#1321). All four decision sites must state the same rule.
text = " ".join(source().split())
assert "conflict alone is not a reason to Hold" in text or \
"Conflicting arguments alone are not a reason to Hold" in text
assert "materially conflicting" not in text