mirror of
https://github.com/TauricResearch/TradingAgents.git
synced 2026-06-17 05:16:14 +03:00
feat(config): TRADINGAGENTS_* env-var overlay for DEFAULT_CONFIG
Adds a single _ENV_OVERRIDES table in default_config.py with type-aware coercion (str/int/bool), so users can switch llm_provider, deep/quick models, backend URL, output language, debate rounds, and the checkpoint flag purely via .env. Centralizes load_dotenv in the package __init__ so the overlay applies for every entry point (CLI, main.py, programmatic). Drops the hardcoded model assignments and duplicate dotenv loads in main.py and cli/main.py. Verified live with OpenAI and Gemini. #602
This commit is contained in:
13
.env.example
13
.env.example
@@ -11,3 +11,16 @@ ZHIPU_CN_API_KEY=
|
||||
MINIMAX_API_KEY=
|
||||
MINIMAX_CN_API_KEY=
|
||||
OPENROUTER_API_KEY=
|
||||
|
||||
# Optional: override DEFAULT_CONFIG without editing code.
|
||||
# Any TRADINGAGENTS_* variable below, when set, replaces the matching key
|
||||
# in tradingagents/default_config.py. Values are coerced to the type of
|
||||
# the existing default (bool / int / str), so "true"/"3" work as expected.
|
||||
#TRADINGAGENTS_LLM_PROVIDER=openai
|
||||
#TRADINGAGENTS_DEEP_THINK_LLM=gpt-5.4
|
||||
#TRADINGAGENTS_QUICK_THINK_LLM=gpt-5.4-mini
|
||||
#TRADINGAGENTS_LLM_BACKEND_URL=
|
||||
#TRADINGAGENTS_OUTPUT_LANGUAGE=English
|
||||
#TRADINGAGENTS_MAX_DEBATE_ROUNDS=1
|
||||
#TRADINGAGENTS_MAX_RISK_ROUNDS=1
|
||||
#TRADINGAGENTS_CHECKPOINT_ENABLED=false
|
||||
|
||||
12
cli/main.py
12
cli/main.py
@@ -5,13 +5,6 @@ import questionary
|
||||
from pathlib import Path
|
||||
from functools import wraps
|
||||
from rich.console import Console
|
||||
from dotenv import find_dotenv, load_dotenv
|
||||
|
||||
# Search starts from the user's CWD so the installed `tradingagents`
|
||||
# console script picks up the project's .env instead of walking up from
|
||||
# site-packages.
|
||||
load_dotenv(find_dotenv(usecwd=True))
|
||||
load_dotenv(find_dotenv(".env.enterprise", usecwd=True), override=False)
|
||||
from rich.panel import Panel
|
||||
from rich.spinner import Spinner
|
||||
from rich.live import Live
|
||||
@@ -569,6 +562,11 @@ def get_user_selections():
|
||||
elif selected_llm_provider == "glm":
|
||||
selected_llm_provider, backend_url = ask_glm_region()
|
||||
|
||||
# Confirm the provider's API key is present; prompt the user to paste
|
||||
# one and persist it to .env if it's missing, so the analysis run
|
||||
# doesn't fail later at the first API call.
|
||||
ensure_api_key(selected_llm_provider)
|
||||
|
||||
# Step 7: Thinking agents
|
||||
console.print(
|
||||
create_question_box(
|
||||
|
||||
21
main.py
21
main.py
@@ -1,23 +1,12 @@
|
||||
from tradingagents.graph.trading_graph import TradingAgentsGraph
|
||||
from tradingagents.default_config import DEFAULT_CONFIG
|
||||
|
||||
from dotenv import find_dotenv, load_dotenv
|
||||
|
||||
load_dotenv(find_dotenv(usecwd=True))
|
||||
|
||||
# Create a custom config
|
||||
# DEFAULT_CONFIG already applies TRADINGAGENTS_* env-var overrides
|
||||
# (llm_provider, deep_think_llm, quick_think_llm, backend_url, etc.),
|
||||
# so users can switch models or endpoints purely via .env without
|
||||
# editing this script. Override individual keys here only when you
|
||||
# want a hard-coded value that should ignore the environment.
|
||||
config = DEFAULT_CONFIG.copy()
|
||||
config["deep_think_llm"] = "gpt-5.4-mini" # Use a different model
|
||||
config["quick_think_llm"] = "gpt-5.4-mini" # Use a different model
|
||||
config["max_debate_rounds"] = 1 # Increase debate rounds
|
||||
|
||||
# Configure data vendors (default uses yfinance, no extra API keys needed)
|
||||
config["data_vendors"] = {
|
||||
"core_stock_apis": "yfinance", # Options: alpha_vantage, yfinance
|
||||
"technical_indicators": "yfinance", # Options: alpha_vantage, yfinance
|
||||
"fundamental_data": "yfinance", # Options: alpha_vantage, yfinance
|
||||
"news_data": "yfinance", # Options: alpha_vantage, yfinance
|
||||
}
|
||||
|
||||
# Initialize with custom config
|
||||
ta = TradingAgentsGraph(debug=True, config=config)
|
||||
|
||||
98
tests/test_env_overrides.py
Normal file
98
tests/test_env_overrides.py
Normal file
@@ -0,0 +1,98 @@
|
||||
"""Tests for TRADINGAGENTS_* env-var overlay onto DEFAULT_CONFIG."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
|
||||
import pytest
|
||||
|
||||
import tradingagents.default_config as default_config_module
|
||||
|
||||
|
||||
def _reload_with_env(monkeypatch, **overrides):
|
||||
"""Set/clear env vars then reload default_config to re-evaluate DEFAULT_CONFIG."""
|
||||
for key in list(default_config_module._ENV_OVERRIDES):
|
||||
monkeypatch.delenv(key, raising=False)
|
||||
for key, val in overrides.items():
|
||||
monkeypatch.setenv(key, val)
|
||||
return importlib.reload(default_config_module)
|
||||
|
||||
|
||||
def test_no_env_uses_built_in_defaults(monkeypatch):
|
||||
dc = _reload_with_env(monkeypatch)
|
||||
assert dc.DEFAULT_CONFIG["llm_provider"] == "openai"
|
||||
assert dc.DEFAULT_CONFIG["deep_think_llm"] == "gpt-5.4"
|
||||
assert dc.DEFAULT_CONFIG["quick_think_llm"] == "gpt-5.4-mini"
|
||||
assert dc.DEFAULT_CONFIG["backend_url"] is None
|
||||
assert dc.DEFAULT_CONFIG["max_debate_rounds"] == 1
|
||||
assert dc.DEFAULT_CONFIG["checkpoint_enabled"] is False
|
||||
|
||||
|
||||
def test_string_overrides(monkeypatch):
|
||||
dc = _reload_with_env(
|
||||
monkeypatch,
|
||||
TRADINGAGENTS_LLM_PROVIDER="google",
|
||||
TRADINGAGENTS_DEEP_THINK_LLM="gemini-3-pro-preview",
|
||||
TRADINGAGENTS_QUICK_THINK_LLM="gemini-3-flash-preview",
|
||||
TRADINGAGENTS_LLM_BACKEND_URL="https://example.invalid/v1",
|
||||
TRADINGAGENTS_OUTPUT_LANGUAGE="Chinese",
|
||||
)
|
||||
assert dc.DEFAULT_CONFIG["llm_provider"] == "google"
|
||||
assert dc.DEFAULT_CONFIG["deep_think_llm"] == "gemini-3-pro-preview"
|
||||
assert dc.DEFAULT_CONFIG["quick_think_llm"] == "gemini-3-flash-preview"
|
||||
assert dc.DEFAULT_CONFIG["backend_url"] == "https://example.invalid/v1"
|
||||
assert dc.DEFAULT_CONFIG["output_language"] == "Chinese"
|
||||
|
||||
|
||||
def test_int_coercion(monkeypatch):
|
||||
dc = _reload_with_env(
|
||||
monkeypatch,
|
||||
TRADINGAGENTS_MAX_DEBATE_ROUNDS="3",
|
||||
TRADINGAGENTS_MAX_RISK_ROUNDS="2",
|
||||
)
|
||||
assert dc.DEFAULT_CONFIG["max_debate_rounds"] == 3
|
||||
assert isinstance(dc.DEFAULT_CONFIG["max_debate_rounds"], int)
|
||||
assert dc.DEFAULT_CONFIG["max_risk_discuss_rounds"] == 2
|
||||
assert isinstance(dc.DEFAULT_CONFIG["max_risk_discuss_rounds"], int)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"raw,expected",
|
||||
[
|
||||
("true", True), ("True", True), ("1", True), ("yes", True), ("on", True),
|
||||
("false", False), ("False", False), ("0", False), ("no", False), ("off", False),
|
||||
],
|
||||
)
|
||||
def test_bool_coercion(monkeypatch, raw, expected):
|
||||
dc = _reload_with_env(monkeypatch, TRADINGAGENTS_CHECKPOINT_ENABLED=raw)
|
||||
assert dc.DEFAULT_CONFIG["checkpoint_enabled"] is expected
|
||||
|
||||
|
||||
def test_empty_env_value_is_passthrough(monkeypatch):
|
||||
"""Empty TRADINGAGENTS_* values must not clobber the built-in default."""
|
||||
dc = _reload_with_env(
|
||||
monkeypatch,
|
||||
TRADINGAGENTS_LLM_PROVIDER="",
|
||||
TRADINGAGENTS_MAX_DEBATE_ROUNDS="",
|
||||
)
|
||||
assert dc.DEFAULT_CONFIG["llm_provider"] == "openai"
|
||||
assert dc.DEFAULT_CONFIG["max_debate_rounds"] == 1
|
||||
|
||||
|
||||
def test_invalid_int_raises(monkeypatch):
|
||||
"""Garbage int values should surface a ValueError at import, not silently misconfigure."""
|
||||
monkeypatch.setenv("TRADINGAGENTS_MAX_DEBATE_ROUNDS", "not-a-number")
|
||||
with pytest.raises(ValueError):
|
||||
importlib.reload(default_config_module)
|
||||
# Restore module state for subsequent tests in this process
|
||||
monkeypatch.delenv("TRADINGAGENTS_MAX_DEBATE_ROUNDS", raising=False)
|
||||
importlib.reload(default_config_module)
|
||||
|
||||
|
||||
def test_unknown_env_var_is_ignored(monkeypatch):
|
||||
"""Env vars outside _ENV_OVERRIDES must not bleed into DEFAULT_CONFIG."""
|
||||
dc = _reload_with_env(
|
||||
monkeypatch,
|
||||
TRADINGAGENTS_NONEXISTENT_KEY="oops",
|
||||
)
|
||||
assert "nonexistent_key" not in dc.DEFAULT_CONFIG
|
||||
@@ -1,5 +1,20 @@
|
||||
import warnings
|
||||
|
||||
# Load .env files at package import so DEFAULT_CONFIG's env-var overlay
|
||||
# (and every llm_clients consumer) sees the user's keys regardless of
|
||||
# which entry point started the process. find_dotenv(usecwd=True) walks
|
||||
# from the CWD, so the installed `tradingagents` console script picks up
|
||||
# the project's .env instead of stepping up from site-packages.
|
||||
# load_dotenv defaults to override=False, so it never clobbers values
|
||||
# the caller has already exported.
|
||||
try:
|
||||
from dotenv import find_dotenv, load_dotenv
|
||||
|
||||
load_dotenv(find_dotenv(usecwd=True))
|
||||
load_dotenv(find_dotenv(".env.enterprise", usecwd=True), override=False)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# langchain-core 1.3.3 calls surface_langchain_deprecation_warnings() in
|
||||
# its own __init__, which prepends default-action filters for its
|
||||
# subclassed warning categories. To suppress a specific warning we must
|
||||
|
||||
@@ -2,7 +2,45 @@ import os
|
||||
|
||||
_TRADINGAGENTS_HOME = os.path.join(os.path.expanduser("~"), ".tradingagents")
|
||||
|
||||
DEFAULT_CONFIG = {
|
||||
# Single source of truth for env-var → config-key overrides. To expose
|
||||
# a new config key for environment-based override, add a row here — no
|
||||
# entry-point script changes required. Coercion is driven by the type
|
||||
# of the existing default, so users can keep writing plain strings in
|
||||
# their .env file.
|
||||
_ENV_OVERRIDES = {
|
||||
"TRADINGAGENTS_LLM_PROVIDER": "llm_provider",
|
||||
"TRADINGAGENTS_DEEP_THINK_LLM": "deep_think_llm",
|
||||
"TRADINGAGENTS_QUICK_THINK_LLM": "quick_think_llm",
|
||||
"TRADINGAGENTS_LLM_BACKEND_URL": "backend_url",
|
||||
"TRADINGAGENTS_OUTPUT_LANGUAGE": "output_language",
|
||||
"TRADINGAGENTS_MAX_DEBATE_ROUNDS": "max_debate_rounds",
|
||||
"TRADINGAGENTS_MAX_RISK_ROUNDS": "max_risk_discuss_rounds",
|
||||
"TRADINGAGENTS_CHECKPOINT_ENABLED": "checkpoint_enabled",
|
||||
}
|
||||
|
||||
|
||||
def _coerce(value: str, reference):
|
||||
"""Coerce env-var string to the type of the existing default value."""
|
||||
if isinstance(reference, bool):
|
||||
return value.strip().lower() in ("true", "1", "yes", "on")
|
||||
if isinstance(reference, int) and not isinstance(reference, bool):
|
||||
return int(value)
|
||||
if isinstance(reference, float):
|
||||
return float(value)
|
||||
return value
|
||||
|
||||
|
||||
def _apply_env_overrides(config: dict) -> dict:
|
||||
"""Apply TRADINGAGENTS_* env vars to the config dict in-place."""
|
||||
for env_var, key in _ENV_OVERRIDES.items():
|
||||
raw = os.environ.get(env_var)
|
||||
if raw is None or raw == "":
|
||||
continue
|
||||
config[key] = _coerce(raw, config.get(key))
|
||||
return config
|
||||
|
||||
|
||||
DEFAULT_CONFIG = _apply_env_overrides({
|
||||
"project_dir": os.path.abspath(os.path.join(os.path.dirname(__file__), ".")),
|
||||
"results_dir": os.getenv("TRADINGAGENTS_RESULTS_DIR", os.path.join(_TRADINGAGENTS_HOME, "logs")),
|
||||
"data_cache_dir": os.getenv("TRADINGAGENTS_CACHE_DIR", os.path.join(_TRADINGAGENTS_HOME, "cache")),
|
||||
@@ -62,4 +100,4 @@ DEFAULT_CONFIG = {
|
||||
"tool_vendors": {
|
||||
# Example: "get_stock_data": "alpha_vantage", # Override category default
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user