Improve Korean report localization

This commit is contained in:
nornen0202 2026-04-07 01:30:12 +09:00
parent 6050c25bb2
commit e17db7bd35
6 changed files with 327 additions and 27 deletions

View File

@ -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:

View File

@ -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()

View File

@ -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:

View File

@ -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

View File

@ -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",
}

View File

@ -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))