Commit 1a941ef5 authored by xeron56's avatar xeron56

Limit CLI languages and remove legacy promotions

parent 0d1afdc8
...@@ -66,7 +66,7 @@ DSE_PASSWORD= ...@@ -66,7 +66,7 @@ DSE_PASSWORD=
#TRADINGAGENTS_DEEP_THINK_LLM=google/gemini-3.1-pro-preview #TRADINGAGENTS_DEEP_THINK_LLM=google/gemini-3.1-pro-preview
#TRADINGAGENTS_QUICK_THINK_LLM=google/gemini-3.5-flash #TRADINGAGENTS_QUICK_THINK_LLM=google/gemini-3.5-flash
#TRADINGAGENTS_LLM_BACKEND_URL= #TRADINGAGENTS_LLM_BACKEND_URL=
#TRADINGAGENTS_OUTPUT_LANGUAGE=English #TRADINGAGENTS_OUTPUT_LANGUAGE=English # Supported: English or Bangla
#TRADINGAGENTS_MAX_DEBATE_ROUNDS=1 #TRADINGAGENTS_MAX_DEBATE_ROUNDS=1
#TRADINGAGENTS_MAX_RISK_ROUNDS=1 #TRADINGAGENTS_MAX_RISK_ROUNDS=1
#TRADINGAGENTS_CHECKPOINT_ENABLED=false #TRADINGAGENTS_CHECKPOINT_ENABLED=false
......
...@@ -217,7 +217,7 @@ The CLI asks for: ...@@ -217,7 +217,7 @@ The CLI asks for:
1. DSE ticker. 1. DSE ticker.
2. Analysis date. 2. Analysis date.
3. Output language. 3. Output language: English or Bangla (`বাংলা`).
4. Analyst selection. 4. Analyst selection.
5. Research/debate depth. 5. Research/debate depth.
6. LLM provider and models. 6. LLM provider and models.
......
import getpass
import requests
from rich.console import Console
from rich.panel import Panel
from cli.config import CLI_CONFIG
def fetch_announcements(url: str = None, timeout: float = None) -> dict:
"""Fetch announcements from endpoint. Returns dict with announcements and settings."""
endpoint = url or CLI_CONFIG["announcements_url"]
timeout = timeout or CLI_CONFIG["announcements_timeout"]
fallback = CLI_CONFIG["announcements_fallback"]
try:
response = requests.get(endpoint, timeout=timeout)
response.raise_for_status()
data = response.json()
return {
"announcements": data.get("announcements", [fallback]),
"require_attention": data.get("require_attention", False),
}
except Exception:
return {
"announcements": [fallback],
"require_attention": False,
}
def display_announcements(console: Console, data: dict) -> None:
"""Display announcements panel. Prompts for Enter if require_attention is True."""
announcements = data.get("announcements", [])
require_attention = data.get("require_attention", False)
if not announcements:
return
content = "\n".join(announcements)
panel = Panel(
content,
border_style="cyan",
padding=(1, 2),
title="Announcements",
)
console.print(panel)
if require_attention:
getpass.getpass("Press Enter to continue...")
else:
console.print()
CLI_CONFIG = {
# Announcements
"announcements_url": "https://api.tauric.ai/v1/announcements",
"announcements_timeout": 1.0,
"announcements_fallback": "[cyan]For more information, please visit[/cyan] [link=https://github.com/TauricResearch]https://github.com/TauricResearch[/link]",
}
...@@ -19,7 +19,6 @@ from rich.spinner import Spinner ...@@ -19,7 +19,6 @@ from rich.spinner import Spinner
from rich.table import Table from rich.table import Table
from rich.text import Text from rich.text import Text
from cli.announcements import display_announcements, fetch_announcements
from cli.stats_handler import StatsCallbackHandler from cli.stats_handler import StatsCallbackHandler
from cli.utils import ( from cli.utils import (
ask_anthropic_effort, ask_anthropic_effort,
...@@ -523,8 +522,7 @@ def get_user_selections(): ...@@ -523,8 +522,7 @@ def get_user_selections():
welcome_content += f"[bold green]{PRODUCT_DISPLAY_NAME}[/bold green]\n" welcome_content += f"[bold green]{PRODUCT_DISPLAY_NAME}[/bold green]\n"
welcome_content += f"[dim]{PRODUCT_TAGLINE}[/dim]\n\n" welcome_content += f"[dim]{PRODUCT_TAGLINE}[/dim]\n\n"
welcome_content += "[bold]Workflow Steps:[/bold]\n" welcome_content += "[bold]Workflow Steps:[/bold]\n"
welcome_content += "I. Analyst Team → II. Research Team → III. Trader → IV. Risk Management → V. Portfolio Management\n\n" welcome_content += "I. Analyst Team → II. Research Team → III. Trader → IV. Risk Management → V. Portfolio Management"
welcome_content += "[dim]Powered by the TradingAgents framework[/dim]"
# Create and center the welcome box # Create and center the welcome box
welcome_box = Panel( welcome_box = Panel(
...@@ -536,11 +534,6 @@ def get_user_selections(): ...@@ -536,11 +534,6 @@ def get_user_selections():
) )
console.print(Align.center(welcome_box)) console.print(Align.center(welcome_box))
console.print() console.print()
console.print() # Add vertical space before announcements
# Fetch and display announcements (silent on failure)
announcements = fetch_announcements()
display_announcements(console, announcements)
# Create a boxed questionnaire for each step # Create a boxed questionnaire for each step
def create_question_box(title, prompt, default=None): def create_question_box(title, prompt, default=None):
......
...@@ -7,6 +7,7 @@ from rich.console import Console ...@@ -7,6 +7,7 @@ from rich.console import Console
from cli.models import AnalystType, AssetType from cli.models import AnalystType, AssetType
from dohasecuritiesstockai.default_config import DEFAULT_CONFIG from dohasecuritiesstockai.default_config import DEFAULT_CONFIG
from dohasecuritiesstockai.languages import OUTPUT_LANGUAGE_CHOICES
from dohasecuritiesstockai.llm_clients.api_key_env import get_api_key_env from dohasecuritiesstockai.llm_clients.api_key_env import get_api_key_env
from dohasecuritiesstockai.llm_clients.model_catalog import get_model_options from dohasecuritiesstockai.llm_clients.model_catalog import get_model_options
...@@ -681,22 +682,12 @@ def ensure_api_key(provider: str) -> str | None: ...@@ -681,22 +682,12 @@ def ensure_api_key(provider: str) -> str | None:
def ask_output_language() -> str: def ask_output_language() -> str:
"""Ask for report output language.""" """Ask for the English or Bangla report-output language."""
choice = questionary.select( choice = questionary.select(
"Select Output Language:", "Select Output Language:",
choices=[ choices=[
questionary.Choice("English (default)", "English"), questionary.Choice(label, value)
questionary.Choice("Chinese (中文)", "Chinese"), for label, value in OUTPUT_LANGUAGE_CHOICES
questionary.Choice("Japanese (日本語)", "Japanese"),
questionary.Choice("Korean (한국어)", "Korean"),
questionary.Choice("Hindi (हिन्दी)", "Hindi"),
questionary.Choice("Spanish (Español)", "Spanish"),
questionary.Choice("Portuguese (Português)", "Portuguese"),
questionary.Choice("French (Français)", "French"),
questionary.Choice("German (Deutsch)", "German"),
questionary.Choice("Arabic (العربية)", "Arabic"),
questionary.Choice("Russian (Русский)", "Russian"),
questionary.Choice("Custom language", "custom"),
], ],
style=questionary.Style([ style=questionary.Style([
("selected", "fg:yellow noinherit"), ("selected", "fg:yellow noinherit"),
...@@ -709,10 +700,4 @@ def ask_output_language() -> str: ...@@ -709,10 +700,4 @@ def ask_output_language() -> str:
# rather than exiting the run (unlike the required model/provider prompts). # rather than exiting the run (unlike the required model/provider prompts).
if choice is None: if choice is None:
return "English" return "English"
if choice == "custom":
return (questionary.text(
"Enter language name (e.g. Turkish, Vietnamese, Thai, Indonesian):",
validate=lambda x: len(x.strip()) > 0 or "Please enter a language name.",
).ask() or "").strip() or "English"
return choice return choice
...@@ -61,10 +61,15 @@ def get_language_instruction() -> str: ...@@ -61,10 +61,15 @@ def get_language_instruction() -> str:
report rather than a mix of languages. report rather than a mix of languages.
""" """
from dohasecuritiesstockai.dataflows.config import get_config from dohasecuritiesstockai.dataflows.config import get_config
lang = get_config().get("output_language", "English") from dohasecuritiesstockai.languages import normalize_output_language
if lang.strip().lower() == "english":
language = normalize_output_language(get_config().get("output_language", "English"))
if language == "English":
return "" return ""
return f" Write your entire response in {lang}." return (
" Write your entire response in natural Bangla using বাংলা script."
" Keep ticker symbols, standard financial abbreviations, and numeric values unchanged."
)
def _clean_identity_value(value: Any) -> str | None: def _clean_identity_value(value: Any) -> str | None:
...@@ -219,4 +224,3 @@ def create_msg_delete(): ...@@ -219,4 +224,3 @@ def create_msg_delete():
return {"messages": removal_operations + [placeholder]} return {"messages": removal_operations + [placeholder]}
return delete_messages return delete_messages
...@@ -40,7 +40,7 @@ _RSS = "https://www.reddit.com/r/{sub}/search.rss?{qs}" ...@@ -40,7 +40,7 @@ _RSS = "https://www.reddit.com/r/{sub}/search.rss?{qs}"
# blocks generic/anonymous tokens like bare "Mozilla/5.0" or "curl/…" but # blocks generic/anonymous tokens like bare "Mozilla/5.0" or "curl/…" but
# serves this one on both endpoints; the RSS feed accepts it even when the # serves this one on both endpoints; the RSS feed accepts it even when the
# JSON search endpoint 403s, so no browser-spoofing is needed. # JSON search endpoint 403s, so no browser-spoofing is needed.
_UA = "dohasecuritiesstockai/0.2 (+https://github.com/TauricResearch/TradingAgents)" _UA = "dohasecuritiesstockai/0.2 (+https://github.com/xeron56/DohasecuritiesStockAi)"
_ATOM_NS = {"atom": "http://www.w3.org/2005/Atom"} _ATOM_NS = {"atom": "http://www.w3.org/2005/Atom"}
# Default subreddits ordered roughly by signal density for ticker-specific # Default subreddits ordered roughly by signal density for ticker-specific
......
...@@ -24,7 +24,7 @@ from .symbol_utils import crypto_base ...@@ -24,7 +24,7 @@ from .symbol_utils import crypto_base
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_API = "https://api.stocktwits.com/api/2/streams/symbol/{ticker}.json" _API = "https://api.stocktwits.com/api/2/streams/symbol/{ticker}.json"
_UA = "dohasecuritiesstockai/0.2 (+https://github.com/TauricResearch/TradingAgents)" _UA = "dohasecuritiesstockai/0.2 (+https://github.com/xeron56/DohasecuritiesStockAi)"
def _stocktwits_symbol(ticker: str) -> str: def _stocktwits_symbol(ticker: str) -> str:
......
import os import os
from dohasecuritiesstockai.languages import normalize_output_language
# Keep the established data directory so the package rename does not orphan # Keep the established data directory so the package rename does not orphan
# existing reports, checkpoints, caches, or the trading-memory log. # existing reports, checkpoints, caches, or the trading-memory log.
_TRADINGAGENTS_HOME = os.path.join(os.path.expanduser("~"), ".tradingagents") _TRADINGAGENTS_HOME = os.path.join(os.path.expanduser("~"), ".tradingagents")
...@@ -72,7 +74,10 @@ def _apply_env_overrides(config: dict) -> dict: ...@@ -72,7 +74,10 @@ def _apply_env_overrides(config: dict) -> dict:
if raw is None or raw == "": if raw is None or raw == "":
continue continue
try: try:
config[key] = _coerce(raw, config.get(key)) value = _coerce(raw, config.get(key))
if key == "output_language":
value = normalize_output_language(value)
config[key] = value
except ValueError as exc: except ValueError as exc:
raise ValueError(f"Invalid value for {env_var}: {exc}") from exc raise ValueError(f"Invalid value for {env_var}: {exc}") from exc
return config return config
......
"""Supported report-output languages."""
OUTPUT_LANGUAGE_CHOICES = (
("English (default)", "English"),
("Bangla (বাংলা)", "Bangla"),
)
_OUTPUT_LANGUAGE_ALIASES = {
"english": "English",
"en": "English",
"bangla": "Bangla",
"bengali": "Bangla",
"বাংলা": "Bangla",
"bn": "Bangla",
}
def normalize_output_language(value: str | None) -> str:
"""Return the canonical supported language name or raise ``ValueError``."""
normalized = str(value or "English").strip().casefold()
language = _OUTPUT_LANGUAGE_ALIASES.get(normalized)
if language is None:
raise ValueError("output language must be English or Bangla")
return language
...@@ -40,7 +40,7 @@ class TestCliSkipsPromptsFromEnv(unittest.TestCase): ...@@ -40,7 +40,7 @@ class TestCliSkipsPromptsFromEnv(unittest.TestCase):
"TRADINGAGENTS_DEEP_THINK_LLM": "kimi-k2.5", "TRADINGAGENTS_DEEP_THINK_LLM": "kimi-k2.5",
"TRADINGAGENTS_QUICK_THINK_LLM": "deepseek-v4-pro", "TRADINGAGENTS_QUICK_THINK_LLM": "deepseek-v4-pro",
"TRADINGAGENTS_LLM_BACKEND_URL": "https://opencode.ai/zen/go/v1", "TRADINGAGENTS_LLM_BACKEND_URL": "https://opencode.ai/zen/go/v1",
"TRADINGAGENTS_OUTPUT_LANGUAGE": "Japanese", "TRADINGAGENTS_OUTPUT_LANGUAGE": "Bangla",
} }
fake_cfg = dict(m.DEFAULT_CONFIG) fake_cfg = dict(m.DEFAULT_CONFIG)
fake_cfg.update({ fake_cfg.update({
...@@ -48,13 +48,11 @@ class TestCliSkipsPromptsFromEnv(unittest.TestCase): ...@@ -48,13 +48,11 @@ class TestCliSkipsPromptsFromEnv(unittest.TestCase):
"backend_url": "https://opencode.ai/zen/go/v1", "backend_url": "https://opencode.ai/zen/go/v1",
"quick_think_llm": "deepseek-v4-pro", "quick_think_llm": "deepseek-v4-pro",
"deep_think_llm": "kimi-k2.5", "deep_think_llm": "kimi-k2.5",
"output_language": "Japanese", "output_language": "Bangla",
}) })
with mock.patch.dict(os.environ, env, clear=False), \ with mock.patch.dict(os.environ, env, clear=False), \
mock.patch.object(m, "DEFAULT_CONFIG", fake_cfg), \ mock.patch.object(m, "DEFAULT_CONFIG", fake_cfg), \
mock.patch.object(m, "fetch_announcements", return_value=None), \
mock.patch.object(m, "display_announcements"), \
mock.patch.object(m, "get_ticker", return_value="AAPL"), \ mock.patch.object(m, "get_ticker", return_value="AAPL"), \
mock.patch.object(m, "get_analysis_date", return_value="2026-05-29"), \ mock.patch.object(m, "get_analysis_date", return_value="2026-05-29"), \
mock.patch.object(m, "select_analysts", return_value=[]), \ mock.patch.object(m, "select_analysts", return_value=[]), \
...@@ -79,7 +77,7 @@ class TestCliSkipsPromptsFromEnv(unittest.TestCase): ...@@ -79,7 +77,7 @@ class TestCliSkipsPromptsFromEnv(unittest.TestCase):
self.assertEqual(sel["backend_url"], "https://opencode.ai/zen/go/v1") self.assertEqual(sel["backend_url"], "https://opencode.ai/zen/go/v1")
self.assertEqual(sel["shallow_thinker"], "deepseek-v4-pro") self.assertEqual(sel["shallow_thinker"], "deepseek-v4-pro")
self.assertEqual(sel["deep_thinker"], "kimi-k2.5") self.assertEqual(sel["deep_thinker"], "kimi-k2.5")
self.assertEqual(sel["output_language"], "Japanese") self.assertEqual(sel["output_language"], "Bangla")
@pytest.mark.unit @pytest.mark.unit
...@@ -96,8 +94,6 @@ class TestResearchDepthSkippedFromEnv(unittest.TestCase): ...@@ -96,8 +94,6 @@ class TestResearchDepthSkippedFromEnv(unittest.TestCase):
with mock.patch.dict(os.environ, env, clear=False), \ with mock.patch.dict(os.environ, env, clear=False), \
mock.patch.object(m, "DEFAULT_CONFIG", fake_cfg), \ mock.patch.object(m, "DEFAULT_CONFIG", fake_cfg), \
mock.patch.object(m, "fetch_announcements", return_value=None), \
mock.patch.object(m, "display_announcements"), \
mock.patch.object(m, "get_ticker", return_value="AAPL"), \ mock.patch.object(m, "get_ticker", return_value="AAPL"), \
mock.patch.object(m, "get_analysis_date", return_value="2026-05-29"), \ mock.patch.object(m, "get_analysis_date", return_value="2026-05-29"), \
mock.patch.object(m, "select_analysts", return_value=[]), \ mock.patch.object(m, "select_analysts", return_value=[]), \
...@@ -126,8 +122,6 @@ class TestReasoningEffortSkippedFromEnv(unittest.TestCase): ...@@ -126,8 +122,6 @@ class TestReasoningEffortSkippedFromEnv(unittest.TestCase):
with mock.patch.dict(os.environ, env, clear=False), \ with mock.patch.dict(os.environ, env, clear=False), \
mock.patch.object(m, "DEFAULT_CONFIG", fake_cfg), \ mock.patch.object(m, "DEFAULT_CONFIG", fake_cfg), \
mock.patch.object(m, "fetch_announcements", return_value=None), \
mock.patch.object(m, "display_announcements"), \
mock.patch.object(m, "get_ticker", return_value="AAPL"), \ mock.patch.object(m, "get_ticker", return_value="AAPL"), \
mock.patch.object(m, "get_analysis_date", return_value="2026-05-29"), \ mock.patch.object(m, "get_analysis_date", return_value="2026-05-29"), \
mock.patch.object(m, "select_analysts", return_value=[]), \ mock.patch.object(m, "select_analysts", return_value=[]), \
......
...@@ -35,13 +35,26 @@ def test_string_overrides(monkeypatch): ...@@ -35,13 +35,26 @@ def test_string_overrides(monkeypatch):
TRADINGAGENTS_DEEP_THINK_LLM="gemini-3-pro-preview", TRADINGAGENTS_DEEP_THINK_LLM="gemini-3-pro-preview",
TRADINGAGENTS_QUICK_THINK_LLM="gemini-3-flash-preview", TRADINGAGENTS_QUICK_THINK_LLM="gemini-3-flash-preview",
TRADINGAGENTS_LLM_BACKEND_URL="https://example.invalid/v1", TRADINGAGENTS_LLM_BACKEND_URL="https://example.invalid/v1",
TRADINGAGENTS_OUTPUT_LANGUAGE="Chinese", TRADINGAGENTS_OUTPUT_LANGUAGE="Bangla",
) )
assert dc.DEFAULT_CONFIG["llm_provider"] == "google" assert dc.DEFAULT_CONFIG["llm_provider"] == "google"
assert dc.DEFAULT_CONFIG["deep_think_llm"] == "gemini-3-pro-preview" 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["quick_think_llm"] == "gemini-3-flash-preview"
assert dc.DEFAULT_CONFIG["backend_url"] == "https://example.invalid/v1" assert dc.DEFAULT_CONFIG["backend_url"] == "https://example.invalid/v1"
assert dc.DEFAULT_CONFIG["output_language"] == "Chinese" assert dc.DEFAULT_CONFIG["output_language"] == "Bangla"
@pytest.mark.parametrize("alias", ["Bangla", "Bengali", "বাংলা", "bn"])
def test_bangla_output_language_aliases(monkeypatch, alias):
dc = _reload_with_env(monkeypatch, TRADINGAGENTS_OUTPUT_LANGUAGE=alias)
assert dc.DEFAULT_CONFIG["output_language"] == "Bangla"
def test_unsupported_output_language_is_rejected(monkeypatch):
with pytest.raises(ValueError, match="English or Bangla"):
_reload_with_env(monkeypatch, TRADINGAGENTS_OUTPUT_LANGUAGE="Chinese")
monkeypatch.delenv("TRADINGAGENTS_OUTPUT_LANGUAGE", raising=False)
importlib.reload(default_config_module)
def test_int_coercion(monkeypatch): def test_int_coercion(monkeypatch):
......
...@@ -39,13 +39,21 @@ class TestLanguageInstruction: ...@@ -39,13 +39,21 @@ class TestLanguageInstruction:
set_config({"output_language": "English"}) set_config({"output_language": "English"})
assert get_language_instruction() == "" assert get_language_instruction() == ""
def test_non_english_emits_directive(self): def test_bangla_emits_bangla_script_directive(self):
from dohasecuritiesstockai.dataflows.config import set_config from dohasecuritiesstockai.dataflows.config import set_config
set_config({"output_language": "中文"}) set_config({"output_language": "Bangla"})
out = get_language_instruction() out = get_language_instruction()
assert "中文" in out assert "Bangla" in out
assert "বাংলা" in out
assert "entire response" in out assert "entire response" in out
def test_unsupported_language_is_rejected(self):
from dohasecuritiesstockai.dataflows.config import set_config
set_config({"output_language": "Chinese"})
with pytest.raises(ValueError, match="English or Bangla"):
get_language_instruction()
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.parametrize("rel", REPORT_AGENTS) @pytest.mark.parametrize("rel", REPORT_AGENTS)
......
"""OpenRouter model selection: prompts are labeled by mode (#1000); required """OpenRouter and output-language selection behavior."""
prompts exit cleanly on cancel; the output-language prompt defaults to English
on cancel; and the OpenRouter list is newest-first."""
from unittest import mock from unittest import mock
...@@ -111,12 +109,23 @@ class TestCancelExitsCleanly: ...@@ -111,12 +109,23 @@ class TestCancelExitsCleanly:
@pytest.mark.unit @pytest.mark.unit
class TestLanguageDefaultsToEnglish: class TestOutputLanguage:
def test_select_cancel_defaults_english(self): def test_select_cancel_defaults_english(self):
with mock.patch.object(utils.questionary, "select", return_value=_asks(None)): with mock.patch.object(utils.questionary, "select", return_value=_asks(None)):
assert utils.ask_output_language() == "English" assert utils.ask_output_language() == "English"
def test_custom_language_cancel_defaults_english(self): def test_selector_only_offers_english_and_bangla(self):
with mock.patch.object(utils.questionary, "select", return_value=_asks("custom")), \ captured = {}
mock.patch.object(utils.questionary, "text", return_value=_asks(None)):
assert utils.ask_output_language() == "English" def fake_select(message, **kwargs):
captured["message"] = message
captured["labels"] = [choice.title for choice in kwargs["choices"]]
captured["values"] = [choice.value for choice in kwargs["choices"]]
return _asks("Bangla")
with mock.patch.object(utils.questionary, "select", side_effect=fake_select):
assert utils.ask_output_language() == "Bangla"
assert captured["message"] == "Select Output Language:"
assert captured["values"] == ["English", "Bangla"]
assert captured["labels"] == ["English (default)", "Bangla (বাংলা)"]
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment