Improve Korean report localization
This commit is contained in:
parent
6050c25bb2
commit
e17db7bd35
11
cli/main.py
11
cli/main.py
|
|
@ -646,9 +646,9 @@ def get_analysis_date():
|
|||
)
|
||||
|
||||
|
||||
def save_report_to_disk(final_state, ticker: str, save_path: Path):
|
||||
def save_report_to_disk(final_state, ticker: str, save_path: Path, *, language: str = "English"):
|
||||
"""Save complete analysis report to disk with organized subfolders."""
|
||||
return save_report_bundle(final_state, ticker, save_path)
|
||||
return save_report_bundle(final_state, ticker, save_path, language=language)
|
||||
|
||||
|
||||
def display_complete_report(final_state):
|
||||
|
|
@ -1114,7 +1114,12 @@ def run_analysis():
|
|||
).strip()
|
||||
save_path = Path(save_path_str)
|
||||
try:
|
||||
report_file = save_report_to_disk(final_state, selections["ticker"], save_path)
|
||||
report_file = save_report_to_disk(
|
||||
final_state,
|
||||
selections["ticker"],
|
||||
save_path,
|
||||
language=selections.get("output_language", "English"),
|
||||
)
|
||||
console.print(f"\n[green]✓ Report saved to:[/green] {save_path.resolve()}")
|
||||
console.print(f" [dim]Complete report:[/dim] {report_file.name}")
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,98 @@
|
|||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from tradingagents.graph.trading_graph import TradingAgentsGraph
|
||||
from tradingagents.reporting import save_report_bundle
|
||||
|
||||
|
||||
class ReportLocalizationTests(unittest.TestCase):
|
||||
def test_save_report_bundle_uses_korean_labels(self):
|
||||
final_state = {
|
||||
"market_report": "시장 보고서 본문",
|
||||
"sentiment_report": "소셜 보고서 본문",
|
||||
"news_report": "뉴스 보고서 본문",
|
||||
"fundamentals_report": "펀더멘털 보고서 본문",
|
||||
"investment_debate_state": {
|
||||
"bull_history": "강세 의견",
|
||||
"bear_history": "약세 의견",
|
||||
"judge_decision": "리서치 매니저 판단",
|
||||
},
|
||||
"trader_investment_plan": "트레이딩 계획",
|
||||
"risk_debate_state": {
|
||||
"aggressive_history": "공격적 의견",
|
||||
"conservative_history": "보수적 의견",
|
||||
"neutral_history": "중립 의견",
|
||||
"judge_decision": "포트폴리오 최종 판단",
|
||||
},
|
||||
}
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
report_path = save_report_bundle(
|
||||
final_state,
|
||||
"GOOGL",
|
||||
Path(tmpdir),
|
||||
language="Korean",
|
||||
)
|
||||
report_text = report_path.read_text(encoding="utf-8")
|
||||
|
||||
self.assertIn("트레이딩 분석 리포트", report_text)
|
||||
self.assertIn("생성 시각", report_text)
|
||||
self.assertIn("애널리스트 팀 리포트", report_text)
|
||||
self.assertIn("포트폴리오 매니저 최종 판단", report_text)
|
||||
self.assertIn("시장 애널리스트", report_text)
|
||||
|
||||
def test_localize_final_state_rewrites_user_facing_fields(self):
|
||||
graph = TradingAgentsGraph.__new__(TradingAgentsGraph)
|
||||
graph.quick_thinking_llm = object()
|
||||
final_state = {
|
||||
"market_report": "market",
|
||||
"sentiment_report": "social",
|
||||
"news_report": "news",
|
||||
"fundamentals_report": "fundamentals",
|
||||
"investment_plan": "investment plan",
|
||||
"trader_investment_plan": "trader plan",
|
||||
"final_trade_decision": "final decision",
|
||||
"investment_debate_state": {
|
||||
"bull_history": "bull",
|
||||
"bear_history": "bear",
|
||||
"history": "debate history",
|
||||
"current_response": "latest debate",
|
||||
"judge_decision": "manager decision",
|
||||
},
|
||||
"risk_debate_state": {
|
||||
"aggressive_history": "aggressive",
|
||||
"conservative_history": "conservative",
|
||||
"neutral_history": "neutral",
|
||||
"history": "risk history",
|
||||
"current_aggressive_response": "aggr latest",
|
||||
"current_conservative_response": "cons latest",
|
||||
"current_neutral_response": "neutral latest",
|
||||
"judge_decision": "portfolio decision",
|
||||
},
|
||||
}
|
||||
|
||||
with (
|
||||
patch("tradingagents.graph.trading_graph.get_output_language", return_value="Korean"),
|
||||
patch(
|
||||
"tradingagents.graph.trading_graph.rewrite_in_output_language",
|
||||
side_effect=lambda llm, content, content_type="report": f"KO::{content_type}::{content}",
|
||||
),
|
||||
):
|
||||
localized = graph._localize_final_state(final_state)
|
||||
|
||||
self.assertEqual(localized["market_report"], "KO::market analyst report::market")
|
||||
self.assertEqual(localized["investment_plan"], "KO::research manager investment plan::investment plan")
|
||||
self.assertEqual(
|
||||
localized["investment_debate_state"]["judge_decision"],
|
||||
"KO::research manager decision::manager decision",
|
||||
)
|
||||
self.assertEqual(
|
||||
localized["risk_debate_state"]["current_neutral_response"],
|
||||
"KO::neutral risk analyst latest response::neutral latest",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
from langchain_core.messages import HumanMessage, RemoveMessage
|
||||
import re
|
||||
|
||||
# Import tools from separate utility files
|
||||
from tradingagents.agents.utils.core_stock_tools import (
|
||||
|
|
@ -27,11 +28,87 @@ def get_language_instruction() -> str:
|
|||
Only applied to user-facing agents (analysts, portfolio manager).
|
||||
Internal debate agents stay in English for reasoning quality.
|
||||
"""
|
||||
from tradingagents.dataflows.config import get_config
|
||||
lang = get_config().get("output_language", "English")
|
||||
lang = get_output_language()
|
||||
if lang.strip().lower() == "english":
|
||||
return ""
|
||||
return f" Write your entire response in {lang}."
|
||||
return (
|
||||
f" Write your entire response in {lang}. "
|
||||
f"Do not mix in English for headings, summaries, recommendations, table labels, or narrative text. "
|
||||
f"Keep only ticker symbols, company names, dates, and raw numeric values unchanged when needed."
|
||||
)
|
||||
|
||||
|
||||
def get_output_language() -> str:
|
||||
from tradingagents.dataflows.config import get_config
|
||||
|
||||
return str(get_config().get("output_language", "English")).strip() or "English"
|
||||
|
||||
|
||||
def rewrite_in_output_language(llm, content: str, *, content_type: str = "report") -> str:
|
||||
"""Rewrite already-generated content into the configured output language.
|
||||
|
||||
This lets the graph keep English-centric reasoning prompts where useful while
|
||||
ensuring the persisted user-facing report is consistently localized.
|
||||
"""
|
||||
if not content:
|
||||
return content
|
||||
|
||||
lang = get_output_language()
|
||||
if lang.lower() == "english":
|
||||
return content
|
||||
|
||||
messages = [
|
||||
(
|
||||
"system",
|
||||
"You are a financial editor rewriting existing analysis for end users. "
|
||||
f"Rewrite the user's {content_type} entirely in {lang}. "
|
||||
"Requirements: preserve the original meaning, preserve markdown structure, preserve tables, preserve ticker symbols, preserve dates, preserve numbers, and preserve factual details. "
|
||||
"Translate all headings, labels, bullet text, narrative prose, recommendations, quoted headlines, and English source titles so the output reads naturally and consistently in the target language. "
|
||||
"Do not leave English article titles or English section names in the output unless they are unavoidable proper nouns or acronyms. "
|
||||
"Keep only unavoidable Latin-script proper nouns or acronyms such as ticker symbols, company names, product names, RSI, MACD, ATR, EBITDA, and CAPEX. "
|
||||
"If the source contains English control phrases or analyst role labels, rewrite them into natural user-facing target-language labels. "
|
||||
"Output only the rewritten content.",
|
||||
),
|
||||
("human", content),
|
||||
]
|
||||
|
||||
rewritten = llm.invoke(messages).content
|
||||
if not isinstance(rewritten, str) or not rewritten.strip():
|
||||
return content
|
||||
return _normalize_localized_finance_terms(rewritten, lang)
|
||||
|
||||
|
||||
def _normalize_localized_finance_terms(content: str, language: str) -> str:
|
||||
if language.strip().lower() != "korean":
|
||||
return content
|
||||
|
||||
replacements = {
|
||||
"FINAL TRANSACTION PROPOSAL": "최종 거래 제안",
|
||||
"**BUY**": "**매수**",
|
||||
"**HOLD**": "**보유**",
|
||||
"**SELL**": "**매도**",
|
||||
"**OVERWEIGHT**": "**비중 확대**",
|
||||
"**UNDERWEIGHT**": "**비중 축소**",
|
||||
}
|
||||
|
||||
normalized = content
|
||||
for source, target in replacements.items():
|
||||
normalized = normalized.replace(source, target)
|
||||
regex_replacements = (
|
||||
(r"\bBuy\b", "매수"),
|
||||
(r"\bHold\b", "보유"),
|
||||
(r"\bSell\b", "매도"),
|
||||
(r"\bOverweight\b", "비중 확대"),
|
||||
(r"\bUnderweight\b", "비중 축소"),
|
||||
(r"\bBUY\b", "매수"),
|
||||
(r"\bHOLD\b", "보유"),
|
||||
(r"\bSELL\b", "매도"),
|
||||
(r"\bOVERWEIGHT\b", "비중 확대"),
|
||||
(r"\bUNDERWEIGHT\b", "비중 축소"),
|
||||
)
|
||||
for pattern, replacement in regex_replacements:
|
||||
normalized = re.sub(pattern, replacement, normalized)
|
||||
return normalized
|
||||
|
||||
|
||||
def build_instrument_context(ticker: str) -> str:
|
||||
|
|
|
|||
|
|
@ -30,7 +30,9 @@ from tradingagents.agents.utils.agent_utils import (
|
|||
get_income_statement,
|
||||
get_news,
|
||||
get_insider_transactions,
|
||||
get_global_news
|
||||
get_global_news,
|
||||
get_output_language,
|
||||
rewrite_in_output_language,
|
||||
)
|
||||
|
||||
from .conditional_logic import ConditionalLogic
|
||||
|
|
@ -226,6 +228,9 @@ class TradingAgentsGraph:
|
|||
# Standard mode without tracing
|
||||
final_state = self.graph.invoke(init_agent_state, **args)
|
||||
|
||||
signal = self.process_signal(final_state["final_trade_decision"])
|
||||
final_state = self._localize_final_state(final_state)
|
||||
|
||||
# Store current state for reflection
|
||||
self.curr_state = final_state
|
||||
|
||||
|
|
@ -233,7 +238,7 @@ class TradingAgentsGraph:
|
|||
self._log_state(trade_date, final_state)
|
||||
|
||||
# Return decision and processed signal
|
||||
return final_state, self.process_signal(final_state["final_trade_decision"])
|
||||
return final_state, signal
|
||||
|
||||
def _log_state(self, trade_date, final_state):
|
||||
"""Log the final state to a JSON file."""
|
||||
|
|
@ -296,3 +301,61 @@ class TradingAgentsGraph:
|
|||
def process_signal(self, full_signal):
|
||||
"""Process a signal to extract the core decision."""
|
||||
return self.signal_processor.process_signal(full_signal)
|
||||
|
||||
def _localize_final_state(self, final_state: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Rewrite persisted user-facing outputs into the configured output language."""
|
||||
language = get_output_language()
|
||||
if language.lower() == "english":
|
||||
return final_state
|
||||
|
||||
localized = dict(final_state)
|
||||
|
||||
for field_name, content_type in (
|
||||
("market_report", "market analyst report"),
|
||||
("sentiment_report", "social sentiment report"),
|
||||
("news_report", "news analyst report"),
|
||||
("fundamentals_report", "fundamentals analyst report"),
|
||||
("investment_plan", "research manager investment plan"),
|
||||
("trader_investment_plan", "trader plan"),
|
||||
("final_trade_decision", "portfolio manager final decision"),
|
||||
):
|
||||
localized[field_name] = rewrite_in_output_language(
|
||||
self.quick_thinking_llm,
|
||||
localized.get(field_name, ""),
|
||||
content_type=content_type,
|
||||
)
|
||||
|
||||
investment_debate = dict(localized.get("investment_debate_state") or {})
|
||||
for field_name, content_type in (
|
||||
("bull_history", "bull researcher debate history"),
|
||||
("bear_history", "bear researcher debate history"),
|
||||
("history", "investment debate transcript"),
|
||||
("current_response", "investment debate latest response"),
|
||||
("judge_decision", "research manager decision"),
|
||||
):
|
||||
investment_debate[field_name] = rewrite_in_output_language(
|
||||
self.quick_thinking_llm,
|
||||
investment_debate.get(field_name, ""),
|
||||
content_type=content_type,
|
||||
)
|
||||
localized["investment_debate_state"] = investment_debate
|
||||
|
||||
risk_debate = dict(localized.get("risk_debate_state") or {})
|
||||
for field_name, content_type in (
|
||||
("aggressive_history", "aggressive risk analyst debate history"),
|
||||
("conservative_history", "conservative risk analyst debate history"),
|
||||
("neutral_history", "neutral risk analyst debate history"),
|
||||
("history", "risk debate transcript"),
|
||||
("current_aggressive_response", "aggressive risk analyst latest response"),
|
||||
("current_conservative_response", "conservative risk analyst latest response"),
|
||||
("current_neutral_response", "neutral risk analyst latest response"),
|
||||
("judge_decision", "portfolio manager decision"),
|
||||
):
|
||||
risk_debate[field_name] = rewrite_in_output_language(
|
||||
self.quick_thinking_llm,
|
||||
risk_debate.get(field_name, ""),
|
||||
content_type=content_type,
|
||||
)
|
||||
localized["risk_debate_state"] = risk_debate
|
||||
|
||||
return localized
|
||||
|
|
|
|||
|
|
@ -11,22 +11,24 @@ def save_report_bundle(
|
|||
save_path: Path,
|
||||
*,
|
||||
generated_at: dt.datetime | None = None,
|
||||
language: str = "English",
|
||||
) -> Path:
|
||||
"""Persist a complete TradingAgents report bundle to disk."""
|
||||
|
||||
generated_at = generated_at or dt.datetime.now()
|
||||
save_path = Path(save_path)
|
||||
save_path.mkdir(parents=True, exist_ok=True)
|
||||
labels = _labels_for(language)
|
||||
|
||||
sections: list[str] = []
|
||||
|
||||
analysts_dir = save_path / "1_analysts"
|
||||
analyst_parts: list[tuple[str, str]] = []
|
||||
for file_name, title, key in (
|
||||
("market.md", "Market Analyst", "market_report"),
|
||||
("sentiment.md", "Social Analyst", "sentiment_report"),
|
||||
("news.md", "News Analyst", "news_report"),
|
||||
("fundamentals.md", "Fundamentals Analyst", "fundamentals_report"),
|
||||
("market.md", labels["market_analyst"], "market_report"),
|
||||
("sentiment.md", labels["social_analyst"], "sentiment_report"),
|
||||
("news.md", labels["news_analyst"], "news_report"),
|
||||
("fundamentals.md", labels["fundamentals_analyst"], "fundamentals_report"),
|
||||
):
|
||||
content = _coerce_text(final_state.get(key))
|
||||
if not content:
|
||||
|
|
@ -37,7 +39,7 @@ def save_report_bundle(
|
|||
|
||||
if analyst_parts:
|
||||
sections.append(
|
||||
"## I. Analyst Team Reports\n\n"
|
||||
f"## {labels['section_analysts']}\n\n"
|
||||
+ "\n\n".join(f"### {title}\n{content}" for title, content in analyst_parts)
|
||||
)
|
||||
|
||||
|
|
@ -45,9 +47,9 @@ def save_report_bundle(
|
|||
research_dir = save_path / "2_research"
|
||||
research_parts: list[tuple[str, str]] = []
|
||||
for file_name, title, key in (
|
||||
("bull.md", "Bull Researcher", "bull_history"),
|
||||
("bear.md", "Bear Researcher", "bear_history"),
|
||||
("manager.md", "Research Manager", "judge_decision"),
|
||||
("bull.md", labels["bull_researcher"], "bull_history"),
|
||||
("bear.md", labels["bear_researcher"], "bear_history"),
|
||||
("manager.md", labels["research_manager"], "judge_decision"),
|
||||
):
|
||||
content = _coerce_text(debate.get(key))
|
||||
if not content:
|
||||
|
|
@ -58,7 +60,7 @@ def save_report_bundle(
|
|||
|
||||
if research_parts:
|
||||
sections.append(
|
||||
"## II. Research Team Decision\n\n"
|
||||
f"## {labels['section_research']}\n\n"
|
||||
+ "\n\n".join(f"### {title}\n{content}" for title, content in research_parts)
|
||||
)
|
||||
|
||||
|
|
@ -67,15 +69,17 @@ def save_report_bundle(
|
|||
trading_dir = save_path / "3_trading"
|
||||
trading_dir.mkdir(exist_ok=True)
|
||||
_write_text(trading_dir / "trader.md", trader_plan)
|
||||
sections.append(f"## III. Trading Team Plan\n\n### Trader\n{trader_plan}")
|
||||
sections.append(
|
||||
f"## {labels['section_trading']}\n\n### {labels['trader']}\n{trader_plan}"
|
||||
)
|
||||
|
||||
risk = final_state.get("risk_debate_state") or {}
|
||||
risk_dir = save_path / "4_risk"
|
||||
risk_parts: list[tuple[str, str]] = []
|
||||
for file_name, title, key in (
|
||||
("aggressive.md", "Aggressive Analyst", "aggressive_history"),
|
||||
("conservative.md", "Conservative Analyst", "conservative_history"),
|
||||
("neutral.md", "Neutral Analyst", "neutral_history"),
|
||||
("aggressive.md", labels["aggressive_analyst"], "aggressive_history"),
|
||||
("conservative.md", labels["conservative_analyst"], "conservative_history"),
|
||||
("neutral.md", labels["neutral_analyst"], "neutral_history"),
|
||||
):
|
||||
content = _coerce_text(risk.get(key))
|
||||
if not content:
|
||||
|
|
@ -86,7 +90,7 @@ def save_report_bundle(
|
|||
|
||||
if risk_parts:
|
||||
sections.append(
|
||||
"## IV. Risk Management Team Decision\n\n"
|
||||
f"## {labels['section_risk']}\n\n"
|
||||
+ "\n\n".join(f"### {title}\n{content}" for title, content in risk_parts)
|
||||
)
|
||||
|
||||
|
|
@ -96,13 +100,13 @@ def save_report_bundle(
|
|||
portfolio_dir.mkdir(exist_ok=True)
|
||||
_write_text(portfolio_dir / "decision.md", portfolio_decision)
|
||||
sections.append(
|
||||
"## V. Portfolio Manager Decision\n\n"
|
||||
f"### Portfolio Manager\n{portfolio_decision}"
|
||||
f"## {labels['section_portfolio']}\n\n"
|
||||
f"### {labels['portfolio_manager']}\n{portfolio_decision}"
|
||||
)
|
||||
|
||||
header = (
|
||||
f"# Trading Analysis Report: {ticker}\n\n"
|
||||
f"Generated: {generated_at.strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
||||
f"# {labels['report_title']}: {ticker}\n\n"
|
||||
f"{labels['generated_at']}: {generated_at.strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
||||
)
|
||||
complete_report = save_path / "complete_report.md"
|
||||
_write_text(complete_report, header + "\n\n".join(sections))
|
||||
|
|
@ -121,3 +125,50 @@ def _coerce_text(value: Any) -> str:
|
|||
|
||||
def _write_text(path: Path, content: str) -> None:
|
||||
path.write_text(content, encoding="utf-8")
|
||||
|
||||
|
||||
def _labels_for(language: str) -> dict[str, str]:
|
||||
if str(language).strip().lower() == "korean":
|
||||
return {
|
||||
"report_title": "트레이딩 분석 리포트",
|
||||
"generated_at": "생성 시각",
|
||||
"section_analysts": "I. 애널리스트 팀 리포트",
|
||||
"section_research": "II. 리서치 팀 판단",
|
||||
"section_trading": "III. 트레이딩 팀 계획",
|
||||
"section_risk": "IV. 리스크 관리 팀 판단",
|
||||
"section_portfolio": "V. 포트폴리오 매니저 최종 판단",
|
||||
"market_analyst": "시장 애널리스트",
|
||||
"social_analyst": "소셜 심리 애널리스트",
|
||||
"news_analyst": "뉴스 애널리스트",
|
||||
"fundamentals_analyst": "펀더멘털 애널리스트",
|
||||
"bull_researcher": "강세 리서처",
|
||||
"bear_researcher": "약세 리서처",
|
||||
"research_manager": "리서치 매니저",
|
||||
"trader": "트레이더",
|
||||
"aggressive_analyst": "공격적 리스크 애널리스트",
|
||||
"conservative_analyst": "보수적 리스크 애널리스트",
|
||||
"neutral_analyst": "중립 리스크 애널리스트",
|
||||
"portfolio_manager": "포트폴리오 매니저",
|
||||
}
|
||||
|
||||
return {
|
||||
"report_title": "Trading Analysis Report",
|
||||
"generated_at": "Generated",
|
||||
"section_analysts": "I. Analyst Team Reports",
|
||||
"section_research": "II. Research Team Decision",
|
||||
"section_trading": "III. Trading Team Plan",
|
||||
"section_risk": "IV. Risk Management Team Decision",
|
||||
"section_portfolio": "V. Portfolio Manager Decision",
|
||||
"market_analyst": "Market Analyst",
|
||||
"social_analyst": "Social Analyst",
|
||||
"news_analyst": "News Analyst",
|
||||
"fundamentals_analyst": "Fundamentals Analyst",
|
||||
"bull_researcher": "Bull Researcher",
|
||||
"bear_researcher": "Bear Researcher",
|
||||
"research_manager": "Research Manager",
|
||||
"trader": "Trader",
|
||||
"aggressive_analyst": "Aggressive Analyst",
|
||||
"conservative_analyst": "Conservative Analyst",
|
||||
"neutral_analyst": "Neutral Analyst",
|
||||
"portfolio_manager": "Portfolio Manager",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -172,7 +172,13 @@ def _run_single_ticker(
|
|||
final_state, decision = graph.propagate(ticker, trade_date)
|
||||
|
||||
report_dir = ticker_dir / "report"
|
||||
report_file = save_report_bundle(final_state, ticker, report_dir, generated_at=ticker_started)
|
||||
report_file = save_report_bundle(
|
||||
final_state,
|
||||
ticker,
|
||||
report_dir,
|
||||
generated_at=ticker_started,
|
||||
language=config.run.output_language,
|
||||
)
|
||||
final_state_path = ticker_dir / "final_state.json"
|
||||
_write_json(final_state_path, _serialize_final_state(final_state))
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue