Skip to content

Commit b14d953

Browse files
committed
fix: opening debate speakers no longer rebut nonexistent arguments (#1176)
1 parent 592e228 commit b14d953

6 files changed

Lines changed: 245 additions & 11 deletions

File tree

tests/test_debate_fabrication.py

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
"""Debate opening statements must not prompt a rebuttal of a nonexistent argument.
2+
3+
Regressions for #1176: the first speaker in each debate receives an empty
4+
``current_response`` (or empty opponent responses) yet the prompt demands it
5+
rebut the other side -- so models fabricate the opponent's position. Opening
6+
speakers now get an explicit "present your own case" instruction instead of a
7+
rebuttal framing.
8+
"""
9+
10+
from unittest.mock import MagicMock
11+
12+
import pytest
13+
14+
from tradingagents.agents.researchers.bear_researcher import create_bear_researcher
15+
from tradingagents.agents.researchers.bull_researcher import create_bull_researcher
16+
from tradingagents.agents.risk_mgmt.aggressive_debator import create_aggressive_debator
17+
from tradingagents.agents.risk_mgmt.conservative_debator import create_conservative_debator
18+
from tradingagents.agents.risk_mgmt.neutral_debator import create_neutral_debator
19+
20+
21+
def _captured_prompt(factory, state) -> str:
22+
"""Run one agent node with a fake LLM and return the prompt it received."""
23+
llm = MagicMock()
24+
response = MagicMock()
25+
response.content = "argument"
26+
llm.invoke.return_value = response
27+
factory(llm)(state)
28+
return llm.invoke.call_args.args[0]
29+
30+
31+
def _investment_state(current_response: str) -> dict:
32+
return {
33+
"company_of_interest": "NVDA",
34+
"asset_type": "stock",
35+
"market_report": "market",
36+
"sentiment_report": "sentiment",
37+
"news_report": "news",
38+
"fundamentals_report": "fundamentals",
39+
"investment_debate_state": {
40+
"history": "",
41+
"bull_history": "",
42+
"bear_history": "",
43+
"current_response": current_response,
44+
"count": 0,
45+
},
46+
}
47+
48+
49+
def _risk_state(aggressive: str = "", conservative: str = "", neutral: str = "") -> dict:
50+
return {
51+
"company_of_interest": "NVDA",
52+
"asset_type": "stock",
53+
"market_report": "market",
54+
"sentiment_report": "sentiment",
55+
"news_report": "news",
56+
"fundamentals_report": "fundamentals",
57+
"trader_investment_plan": "trader plan",
58+
"risk_debate_state": {
59+
"history": "",
60+
"aggressive_history": "",
61+
"conservative_history": "",
62+
"neutral_history": "",
63+
"current_aggressive_response": aggressive,
64+
"current_conservative_response": conservative,
65+
"current_neutral_response": neutral,
66+
"count": 0,
67+
},
68+
}
69+
70+
71+
# ---------------------------------------------------------------------------
72+
# Bull / Bear researchers
73+
# ---------------------------------------------------------------------------
74+
75+
76+
@pytest.mark.unit
77+
def test_bull_opening_does_not_reference_bear_argument():
78+
prompt = _captured_prompt(create_bull_researcher, _investment_state(current_response=""))
79+
assert "has not spoken yet" in prompt
80+
assert "Last bear argument:" not in prompt
81+
assert "rebut any bear argument" in prompt
82+
83+
84+
@pytest.mark.unit
85+
def test_bull_rebuttal_keeps_real_bear_argument():
86+
prompt = _captured_prompt(
87+
create_bull_researcher,
88+
_investment_state(current_response="Bear Analyst: rates are too high"),
89+
)
90+
assert "Last bear argument: Bear Analyst: rates are too high" in prompt
91+
assert "has not spoken yet" not in prompt
92+
93+
94+
@pytest.mark.unit
95+
def test_bear_opening_does_not_reference_bull_argument():
96+
prompt = _captured_prompt(create_bear_researcher, _investment_state(current_response=""))
97+
assert "has not spoken yet" in prompt
98+
assert "Last bull argument:" not in prompt
99+
assert "rebut any bull argument" in prompt
100+
101+
102+
@pytest.mark.unit
103+
def test_bear_rebuttal_keeps_real_bull_argument():
104+
prompt = _captured_prompt(
105+
create_bear_researcher,
106+
_investment_state(current_response="Bull Analyst: AI demand is exploding"),
107+
)
108+
assert "Last bull argument: Bull Analyst: AI demand is exploding" in prompt
109+
assert "has not spoken yet" not in prompt
110+
111+
112+
# ---------------------------------------------------------------------------
113+
# Risk debate (Aggressive / Conservative / Neutral)
114+
# ---------------------------------------------------------------------------
115+
116+
117+
@pytest.mark.unit
118+
def test_aggressive_opening_does_not_quote_others():
119+
prompt = _captured_prompt(create_aggressive_debator, _risk_state())
120+
assert "not spoken yet" in prompt
121+
assert "last arguments from the conservative analyst:" not in prompt.lower()
122+
assert "last arguments from the neutral analyst:" not in prompt.lower()
123+
124+
125+
@pytest.mark.unit
126+
def test_aggressive_rebuttal_quotes_spoken_analysts_only():
127+
prompt = _captured_prompt(
128+
create_aggressive_debator,
129+
_risk_state(conservative="Conservative Analyst: too risky"),
130+
)
131+
assert "Conservative Analyst: too risky" in prompt
132+
assert "has not spoken yet" not in prompt
133+
134+
135+
@pytest.mark.unit
136+
def test_conservative_opening_does_not_quote_others():
137+
prompt = _captured_prompt(create_conservative_debator, _risk_state())
138+
assert "not spoken yet" in prompt
139+
assert "last arguments from the aggressive analyst:" not in prompt.lower()
140+
assert "last arguments from the neutral analyst:" not in prompt.lower()
141+
142+
143+
@pytest.mark.unit
144+
def test_neutral_opening_does_not_quote_others():
145+
prompt = _captured_prompt(create_neutral_debator, _risk_state())
146+
assert "not spoken yet" in prompt
147+
assert "last arguments from the aggressive analyst:" not in prompt.lower()
148+
assert "last arguments from the conservative analyst:" not in prompt.lower()
149+
150+
151+
@pytest.mark.unit
152+
def test_neutral_rebuttal_quotes_spoken_analysts_only():
153+
prompt = _captured_prompt(
154+
create_neutral_debator,
155+
_risk_state(aggressive="Aggressive Analyst: go all in"),
156+
)
157+
assert "Aggressive Analyst: go all in" in prompt
158+
assert "has not spoken yet" not in prompt

tradingagents/agents/researchers/bear_researcher.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,34 @@ def bear_node(state) -> dict:
2424
else "Asset fundamentals report (may be unavailable for crypto)"
2525
)
2626

27+
if current_response.strip():
28+
engagement_instruction = (
29+
"- Bull Counterpoints: Critically analyze the bull argument with specific data and sound reasoning, "
30+
"exposing weaknesses or over-optimistic assumptions.\n"
31+
"- Engagement: Present your argument in a conversational style, directly engaging with the bull "
32+
"analyst's points and debating effectively rather than simply listing facts.\n\n"
33+
f"Last bull argument: {current_response}\n"
34+
"Use this information to deliver a compelling bear argument, refute the bull's claims, and engage "
35+
f"in a dynamic debate that demonstrates the risks and weaknesses of investing in the {target_label}."
36+
)
37+
else:
38+
# The debate opens with this speaker: no bull argument exists yet,
39+
# so prompting a rebuttal makes the model fabricate one (#1176).
40+
engagement_instruction = (
41+
"- This is the opening statement of the debate: the bull analyst has not spoken yet. Do not claim, "
42+
"paraphrase, or rebut any bull argument, because none exists yet.\n"
43+
"- Present your bear case on its own merits, using the provided research and data as evidence.\n\n"
44+
"Open with a clear statement of your position, then support it with the strongest available evidence."
45+
)
46+
2747
prompt = f"""You are a Bear Analyst making the case against investing in the {target_label}. 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.
2848
2949
Key points to focus on:
3050
3151
- Risks and Challenges: Highlight factors like market saturation, financial instability, or macroeconomic threats that could hinder the stock's performance.
3252
- Competitive Weaknesses: Emphasize vulnerabilities such as weaker market positioning, declining innovation, or threats from competitors.
3353
- Negative Indicators: Use evidence from financial data, market trends, or recent adverse news to support your position.
34-
- Bull Counterpoints: Critically analyze the bull argument with specific data and sound reasoning, exposing weaknesses or over-optimistic assumptions.
35-
- Engagement: Present your argument in a conversational style, directly engaging with the bull analyst's points and debating effectively rather than simply listing facts.
54+
{engagement_instruction}
3655
3756
Resources available:
3857
@@ -42,8 +61,6 @@ def bear_node(state) -> dict:
4261
Latest world affairs news: {news_report}
4362
{fundamentals_label}: {fundamentals_report}
4463
Conversation history of the debate: {history}
45-
Last bull argument: {current_response}
46-
Use this information to deliver a compelling bear argument, refute the bull's claims, and engage in a dynamic debate that demonstrates the risks and weaknesses of investing in the {target_label}.
4764
""" + get_language_instruction()
4865

4966
response = llm.invoke(prompt)

tradingagents/agents/researchers/bull_researcher.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,33 @@ def bull_node(state) -> dict:
2424
else "Asset fundamentals report (may be unavailable for crypto)"
2525
)
2626

27+
if current_response.strip():
28+
engagement_instruction = (
29+
"- Bear Counterpoints: Critically analyze the bear argument with specific data and sound reasoning, "
30+
"addressing concerns thoroughly and showing why the bull perspective holds stronger merit.\n"
31+
"- Engagement: Present your argument in a conversational style, engaging directly with the bear "
32+
"analyst's points and debating effectively rather than just listing data.\n\n"
33+
f"Last bear argument: {current_response}\n"
34+
"Use this information to deliver a compelling bull argument, refute the bear's concerns, and engage "
35+
"in a dynamic debate that demonstrates the strengths of the bull position."
36+
)
37+
else:
38+
# The debate opens with this speaker: no bear argument exists yet,
39+
# so prompting a rebuttal makes the model fabricate one (#1176).
40+
engagement_instruction = (
41+
"- This is the opening statement of the debate: the bear analyst has not spoken yet. Do not claim, "
42+
"paraphrase, or rebut any bear argument, because none exists yet.\n"
43+
"- Present your bull case on its own merits, using the provided research and data as evidence.\n\n"
44+
"Open with a clear statement of your position, then support it with the strongest available evidence."
45+
)
46+
2747
prompt = f"""You are a Bull Analyst advocating for investing in the {target_label}. 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.
2848
2949
Key points to focus on:
3050
- Growth Potential: Highlight the company's market opportunities, revenue projections, and scalability.
3151
- Competitive Advantages: Emphasize factors like unique products, strong branding, or dominant market positioning.
3252
- Positive Indicators: Use financial health, industry trends, and recent positive news as evidence.
33-
- Bear Counterpoints: Critically analyze the bear argument with specific data and sound reasoning, addressing concerns thoroughly and showing why the bull perspective holds stronger merit.
34-
- Engagement: Present your argument in a conversational style, engaging directly with the bear analyst's points and debating effectively rather than just listing data.
53+
{engagement_instruction}
3554
3655
Resources available:
3756
{instrument_context}
@@ -40,8 +59,6 @@ def bull_node(state) -> dict:
4059
Latest world affairs news: {news_report}
4160
{fundamentals_label}: {fundamentals_report}
4261
Conversation history of the debate: {history}
43-
Last bear argument: {current_response}
44-
Use this information to deliver a compelling bull argument, refute the bear's concerns, and engage in a dynamic debate that demonstrates the strengths of the bull position.
4562
""" + get_language_instruction()
4663

4764
response = llm.invoke(prompt)

tradingagents/agents/risk_mgmt/aggressive_debator.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,20 @@ def aggressive_node(state) -> dict:
2121

2222
trader_decision = state["trader_investment_plan"]
2323

24+
if any(r.strip() for r in (current_conservative_response, current_neutral_response)):
25+
opponents_block = (
26+
f"Here are the last arguments from the conservative analyst: {current_conservative_response} "
27+
f"Here are the last arguments from the neutral analyst: {current_neutral_response}."
28+
)
29+
else:
30+
# The risk debate opens with this speaker: neither other analyst
31+
# has spoken, so quoting their positions would fabricate them (#1176).
32+
opponents_block = (
33+
"The conservative and neutral analysts have not spoken yet. Do not claim, paraphrase, or "
34+
"rebut either analyst's position, because none exists yet. Present your own argument based on the "
35+
"available data."
36+
)
37+
2438
prompt = f"""As the Aggressive Risk Analyst, your role is to actively champion high-reward, high-risk opportunities, emphasizing bold strategies and competitive advantages. When evaluating the trader's decision or plan, focus intently on the potential upside, growth potential, and innovative benefits—even when these come with elevated risk. Use the provided market data and sentiment analysis to strengthen your arguments and challenge the opposing views. Specifically, respond directly to each point made by the conservative and neutral analysts, countering with data-driven rebuttals and persuasive reasoning. Highlight where their caution might miss critical opportunities or where their assumptions may be overly conservative. Here is the trader's decision:
2539
2640
{trader_decision}
@@ -32,7 +46,7 @@ def aggressive_node(state) -> dict:
3246
Social Media Sentiment Report: {sentiment_report}
3347
Latest World Affairs Report: {news_report}
3448
Company Fundamentals Report: {fundamentals_report}
35-
Here is the current conversation history: {history} Here are the last arguments from the conservative analyst: {current_conservative_response} Here are the last arguments from the neutral analyst: {current_neutral_response}. If there are no responses from the other viewpoints yet, present your own argument based on the available data.
49+
Here is the current conversation history: {history} {opponents_block}
3650
3751
Engage actively by addressing any specific concerns raised, refuting the weaknesses in their logic, and asserting the benefits of risk-taking to outpace market norms. Maintain a focus on debating and persuading, not just presenting data. Challenge each counterpoint to underscore why a high-risk approach is optimal. Output conversationally as if you are speaking without any special formatting.""" + get_language_instruction()
3852

tradingagents/agents/risk_mgmt/conservative_debator.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,20 @@ def conservative_node(state) -> dict:
2121

2222
trader_decision = state["trader_investment_plan"]
2323

24+
if any(r.strip() for r in (current_aggressive_response, current_neutral_response)):
25+
opponents_block = (
26+
f"Here are the last arguments from the aggressive analyst: {current_aggressive_response} "
27+
f"Here are the last arguments from the neutral analyst: {current_neutral_response}."
28+
)
29+
else:
30+
# The risk debate opens with this speaker: neither other analyst
31+
# has spoken, so quoting their positions would fabricate them (#1176).
32+
opponents_block = (
33+
"The aggressive and neutral analysts have not spoken yet. Do not claim, paraphrase, or "
34+
"rebut either analyst's position, because none exists yet. Present your own argument based on the "
35+
"available data."
36+
)
37+
2438
prompt = f"""As the Conservative Risk Analyst, your primary objective is to protect assets, minimize volatility, and ensure steady, reliable growth. You prioritize stability, security, and risk mitigation, carefully assessing potential losses, economic downturns, and market volatility. When evaluating the trader's decision or plan, critically examine high-risk elements, pointing out where the decision may expose the firm to undue risk and where more cautious alternatives could secure long-term gains. Here is the trader's decision:
2539
2640
{trader_decision}
@@ -32,7 +46,7 @@ def conservative_node(state) -> dict:
3246
Social Media Sentiment Report: {sentiment_report}
3347
Latest World Affairs Report: {news_report}
3448
Company Fundamentals Report: {fundamentals_report}
35-
Here is the current conversation history: {history} Here is the last response from the aggressive analyst: {current_aggressive_response} Here is the last response from the neutral analyst: {current_neutral_response}. If there are no responses from the other viewpoints yet, present your own argument based on the available data.
49+
Here is the current conversation history: {history} {opponents_block}
3650
3751
Engage by questioning their optimism and emphasizing the potential downsides they may have overlooked. Address each of their counterpoints to showcase why a conservative stance is ultimately the safest path for the firm's assets. Focus on debating and critiquing their arguments to demonstrate the strength of a low-risk strategy over their approaches. Output conversationally as if you are speaking without any special formatting.""" + get_language_instruction()
3852

tradingagents/agents/risk_mgmt/neutral_debator.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,20 @@ def neutral_node(state) -> dict:
2121

2222
trader_decision = state["trader_investment_plan"]
2323

24+
if any(r.strip() for r in (current_aggressive_response, current_conservative_response)):
25+
opponents_block = (
26+
f"Here are the last arguments from the aggressive analyst: {current_aggressive_response} "
27+
f"Here are the last arguments from the conservative analyst: {current_conservative_response}."
28+
)
29+
else:
30+
# The risk debate opens with this speaker: neither other analyst
31+
# has spoken, so quoting their positions would fabricate them (#1176).
32+
opponents_block = (
33+
"The aggressive and conservative analysts have not spoken yet. Do not claim, paraphrase, or "
34+
"rebut either analyst's position, because none exists yet. Present your own argument based on the "
35+
"available data."
36+
)
37+
2438
prompt = f"""As the Neutral Risk Analyst, your role is to provide a balanced perspective, weighing both the potential benefits and risks of the trader's decision or plan. You prioritize a well-rounded approach, evaluating the upsides and downsides while factoring in broader market trends, potential economic shifts, and diversification strategies.Here is the trader's decision:
2539
2640
{trader_decision}
@@ -32,7 +46,7 @@ def neutral_node(state) -> dict:
3246
Social Media Sentiment Report: {sentiment_report}
3347
Latest World Affairs Report: {news_report}
3448
Company Fundamentals Report: {fundamentals_report}
35-
Here is the current conversation history: {history} Here is the last response from the aggressive analyst: {current_aggressive_response} Here is the last response from the conservative analyst: {current_conservative_response}. If there are no responses from the other viewpoints yet, present your own argument based on the available data.
49+
Here is the current conversation history: {history} {opponents_block}
3650
3751
Engage actively by analyzing both sides critically, addressing weaknesses in the aggressive and conservative arguments to advocate for a more balanced approach. Challenge each of their points to illustrate why a moderate risk strategy might offer the best of both worlds, providing growth potential while safeguarding against extreme volatility. Focus on debating rather than simply presenting data, aiming to show that a balanced view can lead to the most reliable outcomes. Output conversationally as if you are speaking without any special formatting.""" + get_language_instruction()
3852

0 commit comments

Comments
 (0)