mirror of
https://github.com/TauricResearch/TradingAgents.git
synced 2026-08-01 19:34:24 +03:00
fix(graph): give the shared debate/risk routers a complete path_map
- should_continue_debate (2 edges) and should_continue_risk_analysis (3 edges) each returned more targets than any one edge mapped; a fall-through under prompt/i18n/refactor drift crashed LangGraph mid-run - share a complete DEBATE_PATH_MAP / RISK_ANALYSIS_PATH_MAP across every edge #1088
This commit is contained in:
81
tests/test_risk_router_path_map.py
Normal file
81
tests/test_risk_router_path_map.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
"""Shared-router / path_map completeness (#1088).
|
||||||
|
|
||||||
|
Both `should_continue_risk_analysis` (three risk edges) and
|
||||||
|
`should_continue_debate` (two research-debate edges) are single routers whose
|
||||||
|
return set is larger than any one edge previously mapped. Each edge now shares a
|
||||||
|
complete path map (`RISK_ANALYSIS_PATH_MAP` / `DEBATE_PATH_MAP`), so a
|
||||||
|
fall-through return can never hit a missing entry -- which would crash LangGraph
|
||||||
|
mid-run on prompt/i18n/refactor drift in the speaker labels.
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from tradingagents.graph.conditional_logic import ConditionalLogic
|
||||||
|
from tradingagents.graph.setup import DEBATE_PATH_MAP, RISK_ANALYSIS_PATH_MAP
|
||||||
|
|
||||||
|
|
||||||
|
def _state(latest_speaker, count=0):
|
||||||
|
return {"risk_debate_state": {"latest_speaker": latest_speaker, "count": count}}
|
||||||
|
|
||||||
|
|
||||||
|
def _debate_state(current_response, count=0):
|
||||||
|
return {"investment_debate_state": {"current_response": current_response, "count": count}}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
@pytest.mark.parametrize("latest_speaker", [
|
||||||
|
"Aggressive", "Aggressive Analyst",
|
||||||
|
"Conservative", "Conservative Analyst",
|
||||||
|
"Neutral", "Neutral Analyst",
|
||||||
|
"", # drift: empty label
|
||||||
|
"Aggressive Risk Analyst", # drift: node renamed
|
||||||
|
"Agresivo", # drift: i18n / translated label
|
||||||
|
])
|
||||||
|
def test_router_return_always_routable(latest_speaker):
|
||||||
|
logic = ConditionalLogic(max_risk_discuss_rounds=1)
|
||||||
|
target = logic.should_continue_risk_analysis(_state(latest_speaker))
|
||||||
|
assert target in RISK_ANALYSIS_PATH_MAP
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_router_terminates_at_round_limit():
|
||||||
|
logic = ConditionalLogic(max_risk_discuss_rounds=1)
|
||||||
|
# count >= 3 * rounds routes to the Portfolio Manager (debate ends)
|
||||||
|
assert logic.should_continue_risk_analysis(_state("Neutral", count=3)) == "Portfolio Manager"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_path_map_covers_full_router_range():
|
||||||
|
logic = ConditionalLogic(max_risk_discuss_rounds=1)
|
||||||
|
returns = {
|
||||||
|
logic.should_continue_risk_analysis(_state(s, c))
|
||||||
|
for s in ("Aggressive", "Conservative", "Neutral", "drift")
|
||||||
|
for c in (0, 99)
|
||||||
|
}
|
||||||
|
# Every value the router can emit is a key in the shared map...
|
||||||
|
assert returns <= set(RISK_ANALYSIS_PATH_MAP)
|
||||||
|
# ...and the terminal target is reachable.
|
||||||
|
assert "Portfolio Manager" in returns
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
@pytest.mark.parametrize("current_response", [
|
||||||
|
"Bull", "Bull Researcher", "Bear", "Bear Researcher",
|
||||||
|
"", # drift: empty label
|
||||||
|
"Optimista", # drift: i18n / translated label
|
||||||
|
])
|
||||||
|
def test_debate_router_return_always_routable(current_response):
|
||||||
|
logic = ConditionalLogic(max_debate_rounds=1)
|
||||||
|
target = logic.should_continue_debate(_debate_state(current_response))
|
||||||
|
assert target in DEBATE_PATH_MAP
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_debate_path_map_covers_full_router_range():
|
||||||
|
logic = ConditionalLogic(max_debate_rounds=1)
|
||||||
|
returns = {
|
||||||
|
logic.should_continue_debate(_debate_state(s, c))
|
||||||
|
for s in ("Bull", "Bear", "drift")
|
||||||
|
for c in (0, 99)
|
||||||
|
}
|
||||||
|
assert returns <= set(DEBATE_PATH_MAP)
|
||||||
|
assert "Research Manager" in returns # terminal reachable
|
||||||
@@ -25,6 +25,22 @@ from tradingagents.agents.utils.agent_states import AgentState
|
|||||||
from .analyst_execution import build_analyst_execution_plan
|
from .analyst_execution import build_analyst_execution_plan
|
||||||
from .conditional_logic import ConditionalLogic
|
from .conditional_logic import ConditionalLogic
|
||||||
|
|
||||||
|
# Every target a shared conditional router can return. Each edge driven by the
|
||||||
|
# router maps all of them, so a fall-through return (e.g. under prompt/i18n/
|
||||||
|
# refactor drift in the speaker labels) can never hit a missing path_map entry
|
||||||
|
# and crash LangGraph mid-run (#1088).
|
||||||
|
DEBATE_PATH_MAP = {
|
||||||
|
"Bull Researcher": "Bull Researcher",
|
||||||
|
"Bear Researcher": "Bear Researcher",
|
||||||
|
"Research Manager": "Research Manager",
|
||||||
|
}
|
||||||
|
RISK_ANALYSIS_PATH_MAP = {
|
||||||
|
"Aggressive Analyst": "Aggressive Analyst",
|
||||||
|
"Conservative Analyst": "Conservative Analyst",
|
||||||
|
"Neutral Analyst": "Neutral Analyst",
|
||||||
|
"Portfolio Manager": "Portfolio Manager",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class GraphSetup:
|
class GraphSetup:
|
||||||
"""Handles the setup and configuration of the agent graph."""
|
"""Handles the setup and configuration of the agent graph."""
|
||||||
@@ -118,49 +134,22 @@ class GraphSetup:
|
|||||||
else:
|
else:
|
||||||
workflow.add_edge(current_clear, "Bull Researcher")
|
workflow.add_edge(current_clear, "Bull Researcher")
|
||||||
|
|
||||||
# Add remaining edges
|
# Both research-debate edges share the complete DEBATE_PATH_MAP (#1088).
|
||||||
workflow.add_conditional_edges(
|
for debate_node in ("Bull Researcher", "Bear Researcher"):
|
||||||
"Bull Researcher",
|
workflow.add_conditional_edges(
|
||||||
self.conditional_logic.should_continue_debate,
|
debate_node,
|
||||||
{
|
self.conditional_logic.should_continue_debate,
|
||||||
"Bear Researcher": "Bear Researcher",
|
DEBATE_PATH_MAP,
|
||||||
"Research Manager": "Research Manager",
|
)
|
||||||
},
|
|
||||||
)
|
|
||||||
workflow.add_conditional_edges(
|
|
||||||
"Bear Researcher",
|
|
||||||
self.conditional_logic.should_continue_debate,
|
|
||||||
{
|
|
||||||
"Bull Researcher": "Bull Researcher",
|
|
||||||
"Research Manager": "Research Manager",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
workflow.add_edge("Research Manager", "Trader")
|
workflow.add_edge("Research Manager", "Trader")
|
||||||
workflow.add_edge("Trader", "Aggressive Analyst")
|
workflow.add_edge("Trader", "Aggressive Analyst")
|
||||||
workflow.add_conditional_edges(
|
# All three risk edges share the complete RISK_ANALYSIS_PATH_MAP (#1088).
|
||||||
"Aggressive Analyst",
|
for risk_node in ("Aggressive Analyst", "Conservative Analyst", "Neutral Analyst"):
|
||||||
self.conditional_logic.should_continue_risk_analysis,
|
workflow.add_conditional_edges(
|
||||||
{
|
risk_node,
|
||||||
"Conservative Analyst": "Conservative Analyst",
|
self.conditional_logic.should_continue_risk_analysis,
|
||||||
"Portfolio Manager": "Portfolio Manager",
|
RISK_ANALYSIS_PATH_MAP,
|
||||||
},
|
)
|
||||||
)
|
|
||||||
workflow.add_conditional_edges(
|
|
||||||
"Conservative Analyst",
|
|
||||||
self.conditional_logic.should_continue_risk_analysis,
|
|
||||||
{
|
|
||||||
"Neutral Analyst": "Neutral Analyst",
|
|
||||||
"Portfolio Manager": "Portfolio Manager",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
workflow.add_conditional_edges(
|
|
||||||
"Neutral Analyst",
|
|
||||||
self.conditional_logic.should_continue_risk_analysis,
|
|
||||||
{
|
|
||||||
"Aggressive Analyst": "Aggressive Analyst",
|
|
||||||
"Portfolio Manager": "Portfolio Manager",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
workflow.add_edge("Portfolio Manager", END)
|
workflow.add_edge("Portfolio Manager", END)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user