From 9cf3a023fa010c44d0807401ec72b48bbd97d599 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 17:00:35 +0000 Subject: [PATCH 1/2] fix: use run_tool_loop instead of invoke in analyst agents This commit updates the `fundamentals_analyst`, `market_analyst`, `social_media_analyst`, and `news_analyst` files to use `run_tool_loop` instead of `.invoke()`. Using `.invoke()` resulted in the LLM execution stopping immediately upon a tool call request without executing the tool, returning an empty report or raw JSON. The `run_tool_loop` function ensures tools are executed recursively and the final text content is returned. Co-authored-by: aguzererler <6199053+aguzererler@users.noreply.github.com> --- .../agents/analysts/fundamentals_analyst.py | 8 +++----- tradingagents/agents/analysts/market_analyst.py | 13 ++++++------- tradingagents/agents/analysts/news_analyst.py | 8 +++----- .../agents/analysts/social_media_analyst.py | 8 +++----- 4 files changed, 15 insertions(+), 22 deletions(-) diff --git a/tradingagents/agents/analysts/fundamentals_analyst.py b/tradingagents/agents/analysts/fundamentals_analyst.py index 6b63b1b4..3600ced6 100644 --- a/tradingagents/agents/analysts/fundamentals_analyst.py +++ b/tradingagents/agents/analysts/fundamentals_analyst.py @@ -11,6 +11,7 @@ from tradingagents.agents.utils.fundamental_data_tools import ( get_sector_relative, ) from tradingagents.agents.utils.news_data_tools import get_insider_transactions +from tradingagents.agents.utils.tool_runner import run_tool_loop from tradingagents.dataflows.config import get_config @@ -66,12 +67,9 @@ def create_fundamentals_analyst(llm): chain = prompt | llm.bind_tools(tools) - result = chain.invoke(state["messages"]) + result = run_tool_loop(chain, state["messages"], tools) - report = "" - - if len(result.tool_calls) == 0: - report = result.content + report = result.content or "" return { "messages": [result], diff --git a/tradingagents/agents/analysts/market_analyst.py b/tradingagents/agents/analysts/market_analyst.py index e5a9982d..898b9fe9 100644 --- a/tradingagents/agents/analysts/market_analyst.py +++ b/tradingagents/agents/analysts/market_analyst.py @@ -3,6 +3,7 @@ import time from tradingagents.agents.utils.core_stock_tools import get_stock_data from tradingagents.agents.utils.technical_indicators_tools import get_indicators from tradingagents.agents.utils.fundamental_data_tools import get_macro_regime +from tradingagents.agents.utils.tool_runner import run_tool_loop from tradingagents.dataflows.config import get_config @@ -73,16 +74,14 @@ Volume-Based Indicators: chain = prompt | llm.bind_tools(tools) - result = chain.invoke(state["messages"]) + result = run_tool_loop(chain, state["messages"], tools) - report = "" + report = result.content or "" macro_regime_report = "" - if len(result.tool_calls) == 0: - report = result.content - # Extract macro regime section if present - if "Macro Regime Classification" in report or "RISK-ON" in report.upper() or "RISK-OFF" in report.upper() or "TRANSITION" in report.upper(): - macro_regime_report = report + # Extract macro regime section if present + if report and ("Macro Regime Classification" in report or "RISK-ON" in report.upper() or "RISK-OFF" in report.upper() or "TRANSITION" in report.upper()): + macro_regime_report = report return { "messages": [result], diff --git a/tradingagents/agents/analysts/news_analyst.py b/tradingagents/agents/analysts/news_analyst.py index 7c29b7b4..d32590b8 100644 --- a/tradingagents/agents/analysts/news_analyst.py +++ b/tradingagents/agents/analysts/news_analyst.py @@ -1,6 +1,7 @@ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder import json from tradingagents.agents.utils.news_data_tools import get_news, get_global_news +from tradingagents.agents.utils.tool_runner import run_tool_loop from tradingagents.dataflows.config import get_config @@ -42,12 +43,9 @@ def create_news_analyst(llm): prompt = prompt.partial(instrument_context=instrument_context) chain = prompt | llm.bind_tools(tools) - result = chain.invoke(state["messages"]) + result = run_tool_loop(chain, state["messages"], tools) - report = "" - - if len(result.tool_calls) == 0: - report = result.content + report = result.content or "" return { "messages": [result], diff --git a/tradingagents/agents/analysts/social_media_analyst.py b/tradingagents/agents/analysts/social_media_analyst.py index 9c34a5f1..caa61a41 100644 --- a/tradingagents/agents/analysts/social_media_analyst.py +++ b/tradingagents/agents/analysts/social_media_analyst.py @@ -2,6 +2,7 @@ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder import time import json from tradingagents.agents.utils.news_data_tools import get_news +from tradingagents.agents.utils.tool_runner import run_tool_loop from tradingagents.dataflows.config import get_config @@ -43,12 +44,9 @@ def create_social_media_analyst(llm): chain = prompt | llm.bind_tools(tools) - result = chain.invoke(state["messages"]) + result = run_tool_loop(chain, state["messages"], tools) - report = "" - - if len(result.tool_calls) == 0: - report = result.content + report = result.content or "" return { "messages": [result], From 3721aab1104e66321af76c9cb41cfcf3091e400f Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 17:22:28 +0000 Subject: [PATCH 2/2] test: add unit tests for analyst agents tool looping Added comprehensive unit tests for `fundamentals_analyst`, `market_analyst`, `social_media_analyst`, and `news_analyst` to verify that they correctly handle recursive tool calling via `run_tool_loop`. A MockLLM was created to simulate a two-turn conversation (tool call request followed by a final report generation) to ensure the `.invoke()` bug does not regress. Added missing `build_instrument_context` imports to those agents to prevent NameErrors. Co-authored-by: aguzererler <6199053+aguzererler@users.noreply.github.com> --- tests/unit/agents/test_analyst_agents.py | 73 +++++++++++++++++++ .../agents/analysts/fundamentals_analyst.py | 1 + .../agents/analysts/market_analyst.py | 1 + tradingagents/agents/analysts/news_analyst.py | 1 + .../agents/analysts/social_media_analyst.py | 1 + 5 files changed, 77 insertions(+) create mode 100644 tests/unit/agents/test_analyst_agents.py diff --git a/tests/unit/agents/test_analyst_agents.py b/tests/unit/agents/test_analyst_agents.py new file mode 100644 index 00000000..b576a97b --- /dev/null +++ b/tests/unit/agents/test_analyst_agents.py @@ -0,0 +1,73 @@ +from unittest.mock import MagicMock +import pytest +from langchain_core.messages import AIMessage, HumanMessage, ToolMessage +from langchain_core.runnables import Runnable +from tradingagents.agents.analysts.fundamentals_analyst import create_fundamentals_analyst +from tradingagents.agents.analysts.market_analyst import create_market_analyst +from tradingagents.agents.analysts.social_media_analyst import create_social_media_analyst +from tradingagents.agents.analysts.news_analyst import create_news_analyst + +class MockRunnable(Runnable): + def __init__(self, invoke_responses): + self.invoke_responses = invoke_responses + self.call_count = 0 + + def invoke(self, input, config=None, **kwargs): + response = self.invoke_responses[self.call_count] + self.call_count += 1 + return response + +class MockLLM(Runnable): + def __init__(self, invoke_responses): + self.runnable = MockRunnable(invoke_responses) + self.tools_bound = None + + def invoke(self, input, config=None, **kwargs): + return self.runnable.invoke(input, config=config, **kwargs) + + def bind_tools(self, tools): + self.tools_bound = tools + return self.runnable + +@pytest.fixture +def mock_state(): + return { + "messages": [HumanMessage(content="Analyze AAPL.")], + "trade_date": "2024-05-15", + "company_of_interest": "AAPL", + } + +@pytest.fixture +def mock_llm_with_tool_call(): + # 1. First call: The LLM decides to use a tool + tool_call_msg = AIMessage( + content="", + tool_calls=[ + {"name": "mock_tool", "args": {"query": "test"}, "id": "call_123"} + ] + ) + # 2. Second call: The LLM receives the tool output and writes the report + final_report_msg = AIMessage( + content="This is the final report after running the tool." + ) + return MockLLM([tool_call_msg, final_report_msg]) + +def test_fundamentals_analyst_tool_loop(mock_state, mock_llm_with_tool_call): + node = create_fundamentals_analyst(mock_llm_with_tool_call) + result = node(mock_state) + assert "This is the final report after running the tool." in result["fundamentals_report"] + +def test_market_analyst_tool_loop(mock_state, mock_llm_with_tool_call): + node = create_market_analyst(mock_llm_with_tool_call) + result = node(mock_state) + assert "This is the final report after running the tool." in result["market_report"] + +def test_social_media_analyst_tool_loop(mock_state, mock_llm_with_tool_call): + node = create_social_media_analyst(mock_llm_with_tool_call) + result = node(mock_state) + assert "This is the final report after running the tool." in result["sentiment_report"] + +def test_news_analyst_tool_loop(mock_state, mock_llm_with_tool_call): + node = create_news_analyst(mock_llm_with_tool_call) + result = node(mock_state) + assert "This is the final report after running the tool." in result["news_report"] diff --git a/tradingagents/agents/analysts/fundamentals_analyst.py b/tradingagents/agents/analysts/fundamentals_analyst.py index 3600ced6..1a5f8aef 100644 --- a/tradingagents/agents/analysts/fundamentals_analyst.py +++ b/tradingagents/agents/analysts/fundamentals_analyst.py @@ -12,6 +12,7 @@ from tradingagents.agents.utils.fundamental_data_tools import ( ) from tradingagents.agents.utils.news_data_tools import get_insider_transactions from tradingagents.agents.utils.tool_runner import run_tool_loop +from tradingagents.agents.utils.agent_utils import build_instrument_context from tradingagents.dataflows.config import get_config diff --git a/tradingagents/agents/analysts/market_analyst.py b/tradingagents/agents/analysts/market_analyst.py index 898b9fe9..78df7f7d 100644 --- a/tradingagents/agents/analysts/market_analyst.py +++ b/tradingagents/agents/analysts/market_analyst.py @@ -4,6 +4,7 @@ from tradingagents.agents.utils.core_stock_tools import get_stock_data from tradingagents.agents.utils.technical_indicators_tools import get_indicators from tradingagents.agents.utils.fundamental_data_tools import get_macro_regime from tradingagents.agents.utils.tool_runner import run_tool_loop +from tradingagents.agents.utils.agent_utils import build_instrument_context from tradingagents.dataflows.config import get_config diff --git a/tradingagents/agents/analysts/news_analyst.py b/tradingagents/agents/analysts/news_analyst.py index d32590b8..ec50880c 100644 --- a/tradingagents/agents/analysts/news_analyst.py +++ b/tradingagents/agents/analysts/news_analyst.py @@ -2,6 +2,7 @@ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder import json from tradingagents.agents.utils.news_data_tools import get_news, get_global_news from tradingagents.agents.utils.tool_runner import run_tool_loop +from tradingagents.agents.utils.agent_utils import build_instrument_context from tradingagents.dataflows.config import get_config diff --git a/tradingagents/agents/analysts/social_media_analyst.py b/tradingagents/agents/analysts/social_media_analyst.py index caa61a41..ac7c0afd 100644 --- a/tradingagents/agents/analysts/social_media_analyst.py +++ b/tradingagents/agents/analysts/social_media_analyst.py @@ -3,6 +3,7 @@ import time import json from tradingagents.agents.utils.news_data_tools import get_news from tradingagents.agents.utils.tool_runner import run_tool_loop +from tradingagents.agents.utils.agent_utils import build_instrument_context from tradingagents.dataflows.config import get_config