refactor(agents): organise the agents package by what each module holds

- tools.py: the analysts' data tools, previously seven modules under utils
- context.py (was agent_utils, without its tool re-exports), state.py, rating.py and structured.py sit beside schemas.py
- every role package has an __init__
This commit is contained in:
Yijia-Xiao
2026-09-24 04:37:40 +00:00
parent 6097b582d9
commit 852ffead43
54 changed files with 362 additions and 464 deletions
+1 -1
View File
@@ -76,7 +76,7 @@ class _FakeGraph:
return {"messages": [], "company_of_interest": ticker}
def process_signal(self, text):
from tradingagents.agents.utils.rating import parse_rating
from tradingagents.agents.rating import parse_rating
return parse_rating(text)
def record_decision(self, ticker, trade_date, final_state):
+1 -1
View File
@@ -12,12 +12,12 @@ from unittest.mock import MagicMock
import pytest
from tradingagents.agents.context import opponent_argument_or_opening
from tradingagents.agents.researchers.bear_researcher import create_bear_researcher
from tradingagents.agents.researchers.bull_researcher import create_bull_researcher
from tradingagents.agents.risk_mgmt.aggressive_debator import create_aggressive_debator
from tradingagents.agents.risk_mgmt.conservative_debator import create_conservative_debator
from tradingagents.agents.risk_mgmt.neutral_debator import create_neutral_debator
from tradingagents.agents.utils.agent_utils import opponent_argument_or_opening
_REPORTS = {
"company_of_interest": "AAPL", "asset_type": "stock",
+2 -3
View File
@@ -17,9 +17,8 @@ from langchain_core.outputs import ChatGeneration, ChatResult
from langchain_core.runnables import RunnableLambda
from pydantic import Field
from tradingagents.agents import schemas
from tradingagents.agents import context, schemas
from tradingagents.agents.analysts import sentiment_analyst
from tradingagents.agents.utils import agent_utils
from tradingagents.dataflows import router
from tradingagents.dataflows.vendors.yahoo import market as yahoo_market, snapshot
from tradingagents.default_config import DEFAULT_CONFIG
@@ -108,7 +107,7 @@ def offline(monkeypatch, tmp_path):
monkeypatch.setattr(sentiment_analyst, "fetch_stocktwits_messages", lambda *a, **k: "no posts")
monkeypatch.setattr(sentiment_analyst, "fetch_reddit_posts", lambda *a, **k: "no posts")
monkeypatch.setattr(yahoo_market.yf, "Ticker", lambda s: type("T", (), {"info": {"longName": "NVIDIA"}})())
agent_utils.resolve_instrument_identity.cache_clear()
context.resolve_instrument_identity.cache_clear()
return called
+1 -1
View File
@@ -10,7 +10,7 @@ from pathlib import Path
import pytest
from tradingagents.agents.utils.agent_utils import get_language_instruction
from tradingagents.agents.context import get_language_instruction
_AGENTS_DIR = Path(__file__).resolve().parents[1] / "tradingagents" / "agents"
+1 -1
View File
@@ -7,7 +7,7 @@ from unittest.mock import patch
import pytest
from langchain_core.messages import AIMessage, HumanMessage, RemoveMessage
from tradingagents.agents.utils.agent_utils import (
from tradingagents.agents.context import (
build_instrument_context,
create_msg_delete,
get_instrument_context_from_state,
+1 -1
View File
@@ -196,7 +196,7 @@ class TestTradingMemoryLogCore:
def test_an_unreadable_decision_is_tagged_for_review(self, tmp_path):
"""Not a Hold: a fabricated rating is quoted back to the next run as a
call that was never made, and counted in the backtest figures."""
from tradingagents.agents.utils.rating import RATING_REVIEW
from tradingagents.agents.rating import RATING_REVIEW
log = make_log(tmp_path)
log.store_decision("MSFT", "2026-01-12", DECISION_NO_RATING)
+1 -1
View File
@@ -8,7 +8,7 @@ import inspect
import pytest
import tradingagents.agents.analysts.news_analyst as na
from tradingagents.agents.utils.news_data_tools import get_news
from tradingagents.agents.tools import get_news
@pytest.mark.unit
+1 -1
View File
@@ -13,7 +13,7 @@ import json
import pytest
from tradingagents.agents.utils.agent_utils import get_portfolio_context_from_state
from tradingagents.agents.context import get_portfolio_context_from_state
from tradingagents.portfolio import PortfolioContext, load_portfolio
HOLDING = {
+3 -3
View File
@@ -11,7 +11,7 @@ from __future__ import annotations
import pytest
from tradingagents.agents.utils.rating import RATING_REVIEW, extract_rating, parse_rating
from tradingagents.agents.rating import RATING_REVIEW, extract_rating, parse_rating
INVERTED = ("The aggressive analyst pushed hard for a Buy on the AI backlog, but the "
"conservative case on margin compression carried the debate. "
@@ -74,8 +74,8 @@ def test_the_memory_log_records_review_rather_than_a_tradeable_hold(tmp_path):
@pytest.mark.unit
def test_the_signal_and_the_log_agree_on_the_same_decision(tmp_path):
from tradingagents.agents.rating import parse_rating
from tradingagents.agents.utils.memory import TradingMemoryLog
from tradingagents.agents.utils.rating import parse_rating
log = TradingMemoryLog({"memory_log_path": str(tmp_path / "m.md")})
for text in (INVERTED, REFUSAL, "**Rating**: Buy\n\nAccumulate."):
@@ -121,7 +121,7 @@ def test_the_cli_says_when_a_run_produced_no_usable_rating(monkeypatch, tmp_path
pass
def process_signal(self, text):
from tradingagents.agents.utils.rating import parse_rating
from tradingagents.agents.rating import parse_rating
return parse_rating(text)
def get_graph_args(self, callbacks=None):
+1 -6
View File
@@ -6,12 +6,7 @@ header, so the rating is read deterministically; no second model call is made.
import pytest
from tradingagents.agents.utils.rating import (
RATING_REVIEW,
RATINGS_5_TIER,
extract_rating,
parse_rating,
)
from tradingagents.agents.rating import RATING_REVIEW, RATINGS_5_TIER, extract_rating, parse_rating
# ---------------------------------------------------------------------------
# Heuristic parser
+1 -1
View File
@@ -18,8 +18,8 @@ import pytest
import tradingagents.agents.analysts.sentiment_analyst as sentiment
from tradingagents.agents.managers.portfolio_manager import create_portfolio_manager
from tradingagents.agents.managers.research_manager import create_research_manager
from tradingagents.agents.structured import NO_EXTERNAL_TOOLS
from tradingagents.agents.trader.trader import create_trader
from tradingagents.agents.utils.structured import NO_EXTERNAL_TOOLS
def _capturing_llm(captured: dict, result):
+1 -1
View File
@@ -197,7 +197,7 @@ def _structured_trader_llm(captured: dict, proposal: TraderProposal | None = Non
def test_invoke_structured_falls_back_when_result_is_none():
# A thinking model can answer in plain text, leaving the parser with None.
# That must fall back to free text, not crash on render(None) (#1051).
from tradingagents.agents.utils.structured import invoke_structured_or_freetext
from tradingagents.agents.structured import invoke_structured_or_freetext
structured = MagicMock()
structured.invoke.return_value = None
+1 -1
View File
@@ -7,7 +7,7 @@ hit the right instrument instead of failing/mismatching.
"""
import pandas as pd
import tradingagents.agents.utils.agent_utils as au
import tradingagents.agents.context as au
import tradingagents.dataflows.vendors.yahoo.market as yahoo_market
import tradingagents.dataflows.vendors.yahoo.news as ynews
from tradingagents.graph.trading_graph import TradingAgentsGraph
+1 -1
View File
@@ -3,7 +3,7 @@ import unittest
import pytest
from cli.utils import normalize_ticker_symbol
from tradingagents.agents.utils.agent_utils import build_instrument_context
from tradingagents.agents.context import build_instrument_context
@pytest.mark.unit
+18 -25
View File
@@ -15,14 +15,7 @@ from langchain_core.messages import AIMessage
from langgraph.graph import END, START, MessagesState, StateGraph
from langgraph.prebuilt import ToolNode
from tradingagents.agents.utils import (
core_stock_tools,
fundamental_data_tools,
macro_data_tools,
market_data_validation_tools,
news_data_tools,
technical_indicators_tools,
)
from tradingagents.agents import tools
from tradingagents.dataflows.date_window import as_of, as_of_window
TRADE_DATE = "2026-08-14"
@@ -56,16 +49,16 @@ def test_as_of_window(start, end, expected):
DATED_TOOLS = [
core_stock_tools.get_stock_data,
fundamental_data_tools.get_fundamentals,
fundamental_data_tools.get_balance_sheet,
fundamental_data_tools.get_cashflow,
fundamental_data_tools.get_income_statement,
news_data_tools.get_news,
news_data_tools.get_global_news,
technical_indicators_tools.get_indicators,
macro_data_tools.get_macro_indicators,
market_data_validation_tools.get_verified_market_snapshot,
tools.get_stock_data,
tools.get_fundamentals,
tools.get_balance_sheet,
tools.get_cashflow,
tools.get_income_statement,
tools.get_news,
tools.get_global_news,
tools.get_indicators,
tools.get_macro_indicators,
tools.get_verified_market_snapshot,
]
@@ -95,28 +88,28 @@ def _run(tool, args, module):
@pytest.mark.unit
def test_statement_tool_with_omitted_date_uses_the_run_date():
args = _run(fundamental_data_tools.get_balance_sheet, {"ticker": "AAPL"}, fundamental_data_tools)
args = _run(tools.get_balance_sheet, {"ticker": "AAPL"}, tools)
assert args[-1] == TRADE_DATE # #1331: an omitted date no longer means unfiltered
@pytest.mark.unit
def test_future_curr_date_from_the_model_is_clamped():
args = _run(fundamental_data_tools.get_fundamentals,
{"ticker": "AAPL", "curr_date": "2026-09-14"}, fundamental_data_tools)
args = _run(tools.get_fundamentals,
{"ticker": "AAPL", "curr_date": "2026-09-14"}, tools)
assert args == ("get_fundamentals", "AAPL", TRADE_DATE)
@pytest.mark.unit
def test_future_window_from_the_model_is_clamped():
args = _run(core_stock_tools.get_stock_data,
{"symbol": "AAPL", "start_date": "2026-08-01", "end_date": "2026-09-14"}, core_stock_tools)
args = _run(tools.get_stock_data,
{"symbol": "AAPL", "start_date": "2026-08-01", "end_date": "2026-09-14"}, tools)
assert args == ("get_stock_data", "AAPL", "2026-08-01", TRADE_DATE)
@pytest.mark.unit
def test_direct_call_without_state_is_unchanged():
with mock.patch.object(news_data_tools, "route_to_vendor", return_value="ok") as routed:
news_data_tools.get_news.func("AAPL", "2026-09-01", "2026-09-08")
with mock.patch.object(tools, "route_to_vendor", return_value="ok") as routed:
tools.get_news.func("AAPL", "2026-09-01", "2026-09-08")
assert routed.call_args.args == ("get_news", "AAPL", "2026-09-01", "2026-09-08")
+6 -6
View File
@@ -13,7 +13,7 @@ from unittest import mock
import pandas as pd
import pytest
from tradingagents.agents.utils import news_data_tools, prediction_markets_tools
from tradingagents.agents import tools
from tradingagents.dataflows.vendors import polymarket
from tradingagents.dataflows.vendors.alpha_vantage import news as alpha_vantage_news
from tradingagents.dataflows.vendors.yahoo import (
@@ -82,8 +82,8 @@ def test_polymarket_serves_a_current_run():
@pytest.mark.unit
@pytest.mark.parametrize("tool", [news_data_tools.get_insider_transactions,
prediction_markets_tools.get_prediction_markets], ids=lambda t: t.name)
@pytest.mark.parametrize("tool", [tools.get_insider_transactions,
tools.get_prediction_markets], ids=lambda t: t.name)
def test_trade_date_is_injected_not_model_visible(tool):
assert "trade_date" in tool.func.__code__.co_varnames
props = tool.tool_call_schema.model_json_schema()["properties"]
@@ -98,7 +98,7 @@ def test_a_historical_run_is_told_the_identity_is_current(monkeypatch):
They are usually right for a past date, but a company that renamed or was
reclassified since would read wrong, and every agent is told to anchor to
this identity, so the run has to know which date it describes."""
from tradingagents.agents.utils.agent_utils import build_instrument_context
from tradingagents.agents.context import build_instrument_context
identity = {"company_name": "Example Corp", "sector": "Technology",
"industry": "Software", "exchange": "NMS"}
@@ -110,7 +110,7 @@ def test_a_historical_run_is_told_the_identity_is_current(monkeypatch):
@pytest.mark.unit
def test_a_current_run_is_not_cluttered_with_a_vintage_note(monkeypatch):
from tradingagents.agents.utils.agent_utils import build_instrument_context
from tradingagents.agents.context import build_instrument_context
from tradingagents.dataflows.date_window import get_current_date
today = build_instrument_context("EXMP", "stock", {"company_name": "Example Corp"},
@@ -285,7 +285,7 @@ def _dates_after(text: str, cutoff: str) -> list[str]:
def test_an_unavailable_notice_names_no_date_after_the_run():
"""A notice explaining why data is missing named where the vendor's coverage
starts or today's date, both after a historical run's date."""
from tradingagents.agents.utils.agent_utils import build_instrument_context
from tradingagents.agents.context import build_instrument_context
from tradingagents.dataflows.date_window import (
coverage_gap,
get_current_date,
+1 -3
View File
@@ -66,9 +66,7 @@ class TestVerifiedSnapshot:
@pytest.mark.unit
class TestTool:
def test_tool_delegates_to_builder(self, monkeypatch):
from tradingagents.agents.utils.market_data_validation_tools import (
get_verified_market_snapshot,
)
from tradingagents.agents.tools import get_verified_market_snapshot
monkeypatch.setattr(validator, "load_ohlcv", lambda s, d, fill_gaps=True: _sample_ohlcv())
out = get_verified_market_snapshot.invoke(
{"symbol": "COF", "curr_date": "2026-05-20"}