diff --git a/.gitignore b/.gitignore index 55d3ef197..8723361d5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +# i18n generated locale cache (auto-generated, not en.json base) +backend/app/i18n/[!e]*.json +backend/app/i18n/e[!n]*.json + # OS .DS_Store Thumbs.db diff --git a/backend/app/__init__.py b/backend/app/__init__.py index aba624bba..87ee9565d 100644 --- a/backend/app/__init__.py +++ b/backend/app/__init__.py @@ -9,7 +9,7 @@ # 需要在所有其他导入之前设置 warnings.filterwarnings("ignore", message=".*resource_tracker.*") -from flask import Flask, request +from flask import Flask, request, g from flask_cors import CORS from .config import Config @@ -48,6 +48,13 @@ def create_app(config_class=Config): if should_log_startup: logger.info("已注册模拟进程清理函数") + # Locale middleware — normalize Accept-Language header to a 2-letter code + @app.before_request + def set_locale(): + # Normalize 'hu-HU,hu;q=0.9,en;q=0.8' -> 'hu' + accept_lang = request.headers.get("Accept-Language", "en") + g.locale = accept_lang.split(",")[0].split("-")[0].lower() + # 请求日志中间件 @app.before_request def log_request(): @@ -63,10 +70,11 @@ def log_response(response): return response # 注册蓝图 - from .api import graph_bp, simulation_bp, report_bp + from .api import graph_bp, simulation_bp, report_bp, locale_bp app.register_blueprint(graph_bp, url_prefix='/api/graph') app.register_blueprint(simulation_bp, url_prefix='/api/simulation') app.register_blueprint(report_bp, url_prefix='/api/report') + app.register_blueprint(locale_bp, url_prefix='/api/locale') # 健康检查 @app.route('/health') diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py index ffda743a3..ef35be541 100644 --- a/backend/app/api/__init__.py +++ b/backend/app/api/__init__.py @@ -7,8 +7,10 @@ graph_bp = Blueprint('graph', __name__) simulation_bp = Blueprint('simulation', __name__) report_bp = Blueprint('report', __name__) +locale_bp = Blueprint('locale', __name__) from . import graph # noqa: E402, F401 from . import simulation # noqa: E402, F401 from . import report # noqa: E402, F401 +from . import locale # noqa: E402, F401 diff --git a/backend/app/api/graph.py b/backend/app/api/graph.py index 12ff1ba2d..0b3452c32 100644 --- a/backend/app/api/graph.py +++ b/backend/app/api/graph.py @@ -6,7 +6,7 @@ import os import traceback import threading -from flask import request, jsonify +from flask import request, jsonify, g from . import graph_bp from ..config import Config @@ -213,11 +213,13 @@ def generate_ontology(): # 生成本体 logger.info("调用 LLM 生成本体定义...") + locale = getattr(g, 'locale', 'en') generator = OntologyGenerator() ontology = generator.generate( document_texts=document_texts, simulation_requirement=simulation_requirement, - additional_context=additional_context if additional_context else None + additional_context=additional_context if additional_context else None, + language=locale ) # 保存本体到项目 @@ -370,6 +372,9 @@ def build_graph(): project.graph_build_task_id = task_id ProjectManager.save_project(project) + # Capture locale before spawning thread — g is not accessible inside threads + locale = getattr(g, 'locale', 'en') + # 启动后台任务 def build_task(): build_logger = get_logger('mirofish.build') diff --git a/backend/app/api/locale.py b/backend/app/api/locale.py new file mode 100644 index 000000000..60bd4d710 --- /dev/null +++ b/backend/app/api/locale.py @@ -0,0 +1,24 @@ +""" +Locale API endpoint. +GET /api/locale/ — returns locale JSON for given language tag. +""" + +from flask import jsonify + +from . import locale_bp +from ..services.locale_generator import get_locale +from ..utils.logger import get_logger + +logger = get_logger(__name__) + + +@locale_bp.route("/", methods=["GET"]) +def get_locale_route(lang: str): + try: + data = get_locale(lang) + return jsonify({"success": True, "data": data}) + except ValueError as e: + return jsonify({"success": False, "error": str(e)}), 400 + except Exception as e: + logger.error(f"Failed to get locale '{lang}': {e}") + return jsonify({"success": False, "error": str(e)}), 500 diff --git a/backend/app/api/report.py b/backend/app/api/report.py index e05c73c39..49bcc8cd6 100644 --- a/backend/app/api/report.py +++ b/backend/app/api/report.py @@ -6,7 +6,7 @@ import os import traceback import threading -from flask import request, jsonify, send_file +from flask import request, jsonify, send_file, g from . import report_bp from ..config import Config @@ -120,6 +120,9 @@ def generate_report(): } ) + # Capture locale before spawning thread — g is not accessible inside threads + locale = getattr(g, 'locale', 'en') + # 定义后台任务 def run_generate(): try: @@ -129,14 +132,14 @@ def run_generate(): progress=0, message="初始化Report Agent..." ) - + # 创建Report Agent agent = ReportAgent( graph_id=graph_id, simulation_id=simulation_id, simulation_requirement=simulation_requirement ) - + # 进度回调 def progress_callback(stage, progress, message): task_manager.update_task( @@ -144,11 +147,12 @@ def progress_callback(stage, progress, message): progress=progress, message=f"[{stage}] {message}" ) - + # 生成报告(传入预先生成的 report_id) report = agent.generate_report( progress_callback=progress_callback, - report_id=report_id + report_id=report_id, + language=locale ) # 保存报告 @@ -542,8 +546,9 @@ def chat_with_report_agent(): simulation_id=simulation_id, simulation_requirement=simulation_requirement ) - - result = agent.chat(message=message, chat_history=chat_history) + + locale = getattr(g, 'locale', 'en') + result = agent.chat(message=message, chat_history=chat_history, language=locale) return jsonify({ "success": True, diff --git a/backend/app/api/simulation.py b/backend/app/api/simulation.py index 3a0f68168..a0b05aa18 100644 --- a/backend/app/api/simulation.py +++ b/backend/app/api/simulation.py @@ -5,7 +5,7 @@ import os import traceback -from flask import request, jsonify, send_file +from flask import request, jsonify, send_file, g from . import simulation_bp from ..config import Config @@ -500,6 +500,9 @@ def prepare_simulation(): state.status = SimulationStatus.PREPARING manager._save_simulation_state(state) + # Capture locale before spawning thread — g is not accessible inside threads + locale = getattr(g, 'locale', 'en') + # 定义后台任务 def run_prepare(): try: @@ -582,7 +585,8 @@ def progress_callback(stage, progress, message, **kwargs): defined_entity_types=entity_types_list, use_llm_for_profiles=use_llm_for_profiles, progress_callback=progress_callback, - parallel_profile_count=parallel_profile_count + parallel_profile_count=parallel_profile_count, + language=locale ) # 任务完成 diff --git a/backend/app/i18n/en.json b/backend/app/i18n/en.json new file mode 100644 index 000000000..b1d320d16 --- /dev/null +++ b/backend/app/i18n/en.json @@ -0,0 +1,201 @@ +{ + "nav": { + "github": "Visit our GitHub", + "mirofish": "MIROFISH" + }, + "home": { + "tag": "Simple Universal Swarm Intelligence Engine", + "version": "/ v0.1-Preview", + "title_line1": "Upload Any Report", + "title_line2": "Predict the Future", + "desc1": "Even from a single paragraph, MiroFish can automatically generate a parallel world with up to million-scale Agents based on real-world seeds. Inject variables from a god's-eye view to find the \"local optimum\" in complex group interactions.", + "slogan": "Let the future be rehearsed in Agent swarms, and decisions forged through simulation", + "status_label": "System Status", + "status_ready": "Ready", + "status_desc": "Prediction engine on standby. Upload unstructured data to initialize the simulation sequence.", + "metric_cost_value": "Low Cost", + "metric_cost_label": "~$5 per simulation", + "metric_scale_value": "Scalable", + "metric_scale_label": "Up to million-scale Agents", + "workflow_title": "Workflow Sequence", + "step1_title": "Graph Build", + "step1_desc": "Reality seed extraction & individual/group memory injection & GraphRAG build", + "step2_title": "Environment Setup", + "step2_desc": "Entity-relation extraction & persona generation & environment config Agent injection", + "step3_title": "Run Simulation", + "step3_desc": "Dual-platform parallel simulation & auto-parse prediction requirements & dynamic temporal memory updates", + "step4_title": "Report Generation", + "step4_desc": "ReportAgent uses a rich toolset for deep interaction with the post-simulation environment", + "step5_title": "Deep Interaction", + "step5_desc": "Chat with any agent in the simulated world & interact with ReportAgent", + "seed_label": "01 / Reality Seed", + "seed_formats": "Supported formats: PDF, MD, TXT", + "upload_title": "Drag & Drop Files", + "upload_hint": "or click to browse", + "input_params": "Input Parameters", + "prompt_label": ">_ 02 / Simulation Prompt", + "prompt_placeholder": "// Enter your simulation or prediction requirement in natural language (e.g. If a major company announces layoffs, what public sentiment trends would emerge?)", + "engine_badge": "Engine: MiroFish-V1.0", + "launch_btn": "Launch Engine", + "launching_btn": "Initializing..." + }, + "history": { + "title": "History Database", + "empty": "No simulation history found.", + "status_completed": "Completed", + "status_running": "Running", + "status_failed": "Failed", + "view_btn": "View", + "delete_btn": "Delete", + "simulation_history": "Simulation History", + "not_started": "Not started", + "unnamed": "Unnamed Simulation", + "files_empty": "No files", + "no_attached_files": "No attached files", + "simulation_requirement": "Simulation Requirement", + "attached_files": "Attached Files", + "playback_hint": "Step3 'Start Simulation' and Step5 'Deep Interaction' must be launched while running — history playback not supported" + }, + "graph": { + "building": "Building knowledge graph...", + "complete": "Graph build complete", + "error": "Graph build failed", + "ontology_generation": "Ontology Generation", + "graphrag_build": "GraphRAG Build", + "build_complete": "Build Complete", + "enter_env_setup": "Enter Environment Setup ➝", + "creating": "Creating...", + "analyzing": "Analyzing documents...", + "entity_nodes": "Entity Nodes", + "relation_edges": "Relation Edges", + "schema_types": "Schema Types", + "system_dashboard": "SYSTEM DASHBOARD", + "proceed_desc": "Graph build complete. Proceed to the next step to set up the simulation environment.", + "generated_entity_types": "GENERATED ENTITY TYPES", + "generated_relation_types": "GENERATED RELATION TYPES", + "attributes": "ATTRIBUTES", + "examples": "EXAMPLES", + "connections": "CONNECTIONS" + }, + "simulation": { + "preparing": "Preparing simulation...", + "running": "Simulation running", + "complete": "Simulation complete", + "failed": "Simulation failed", + "twitter_platform": "Twitter", + "reddit_platform": "Reddit", + "rounds": "Rounds", + "agents": "Agents", + "start_btn": "Start Simulation", + "stop_btn": "Stop", + "elapsed_time": "Elapsed Time", + "waiting": "Waiting for agent actions...", + "generate_report": "Generate Report", + "starting": "Starting...", + "system_monitor": "SIMULATION MONITOR", + "total_events": "TOTAL EVENTS" + }, + "report": { + "generating": "Generating report...", + "complete": "Report ready", + "download_btn": "Download", + "interact_btn": "Interact with ReportAgent", + "waiting": "Waiting for Report Agent...", + "deep_interaction": "Deep Interaction", + "prediction_report": "Prediction Report", + "console_output": "CONSOLE OUTPUT", + "report_complete": "Report Generation Complete" + }, + "interaction": { + "placeholder": "Ask a question...", + "send_btn": "Send", + "select_agent": "Select an agent", + "report_agent": "ReportAgent", + "chat_report_agent": "Chat with Report Agent", + "chat_any_agent": "Chat with any agent", + "send_survey": "Send Survey", + "interactive_tools": "Interactive Tools", + "select_agent_dropdown": "Select Agent", + "survey_select_targets": "Select Survey Targets", + "survey_selected": "Selected {count} / {total}", + "survey_question_label": "Survey Question", + "survey_submit_btn": "Send Survey", + "survey_results": "Survey Results", + "survey_replies": "{count} replies", + "select_all": "Select All", + "clear_selection": "Clear", + "report_agent_full_name": "Report Agent - Chat", + "unknown_profession": "Unknown Profession", + "bio_label": "Bio", + "you": "You", + "select_individual": "Please select a simulated individual first", + "quick_chat_subtitle": "Quick chat with Report Agent — 4 specialized tools, full MiroFish memory", + "questioner": "Questioner" + }, + "common": { + "loading": "Loading...", + "error": "An error occurred", + "retry": "Retry", + "back": "Back", + "next": "Next", + "cancel": "Cancel", + "confirm": "Confirm", + "agent": "Agent" + }, + "view": { + "graph": "Graph", + "split": "Split", + "workbench": "Workbench" + }, + "steps": { + "graph_build": "Graph Build", + "env_setup": "Environment Setup", + "run_simulation": "Run Simulation", + "report_generation": "Report Generation", + "deep_interaction": "Deep Interaction" + }, + "workflow": { + "step_counter": "Step {current}/{total}" + }, + "status": { + "error": "Error", + "ready": "Ready", + "preparing": "Preparing", + "running": "Running", + "completed": "Completed", + "generating": "Generating", + "processing": "Processing", + "initializing": "Initializing", + "building_graph": "Building Graph", + "generating_ontology": "Generating Ontology" + }, + "env": { + "sim_instance_init": "Simulation Instance Init", + "gen_agent_personas": "Generate Agent Personas", + "gen_dual_platform_config": "Generate Dual-Platform Config", + "initial_activation": "Initial Activation Orchestration", + "setup_complete": "Setup Complete", + "back_to_graph": "Back to Graph Build", + "start_dual_world": "Start Dual-World Simulation ➝", + "narrative_direction": "Narrative Direction", + "initial_hot_topics": "Initial Hot Topics", + "current_agents": "Current Agents", + "expected_total": "Expected Total", + "current_topics": "Current Topics", + "system_dashboard": "SYSTEM DASHBOARD", + "orchestrating": "Orchestrating" + }, + "graph_panel": { + "title": "Graph Relationship Visualization", + "refresh": "Refresh", + "loading": "Loading graph data...", + "waiting": "Waiting for ontology generation...", + "entity_types": "Entity Types", + "show_edge_labels": "Show Edge Labels", + "node_details": "Node Details", + "relationship": "Relationship", + "memory_updating": "GraphRAG memory updating in real-time", + "updating": "Updating in real-time...", + "processing_hint": "Some content still processing, consider refreshing manually" + } +} diff --git a/backend/app/services/locale_generator.py b/backend/app/services/locale_generator.py new file mode 100644 index 000000000..f412c3778 --- /dev/null +++ b/backend/app/services/locale_generator.py @@ -0,0 +1,95 @@ +""" +Locale generator service. +Uses LLM to translate the English base locale into any target language. +Generated locale files are cached to disk. +""" + +import json +import re +from pathlib import Path + +from ..utils.llm_client import LLMClient +from ..utils.logger import get_logger + +logger = get_logger(__name__) + +I18N_DIR = Path(__file__).parent.parent / "i18n" +BASE_LOCALE = "en" + + +def _load_base() -> dict: + path = I18N_DIR / f"{BASE_LOCALE}.json" + with open(path, "r", encoding="utf-8") as f: + return json.load(f) + + +def get_locale(lang: str) -> dict: + """ + Return locale dict for given language tag (e.g. 'hu', 'ja', 'fr'). + Loads from cache if available, otherwise generates via LLM and caches. + """ + # Normalize: 'hu-HU' -> 'hu' + lang = lang.split("-")[0].lower() + + # Validate: only standard BCP 47 primary language subtags (2-3 alpha chars) + if not re.match(r'^[a-z]{2,3}$', lang): + raise ValueError(f"Invalid language code: {lang!r}") + + if lang == BASE_LOCALE: + return _load_base() + + cache_path = I18N_DIR / f"{lang}.json" + if cache_path.exists(): + with open(cache_path, "r", encoding="utf-8") as f: + return json.load(f) + + logger.info(f"Generating locale for '{lang}' via LLM...") + locale = _generate(lang) + + # Ensure directory exists before writing + cache_path.parent.mkdir(parents=True, exist_ok=True) + cache_path.write_text(json.dumps(locale, ensure_ascii=False, indent=2), encoding="utf-8") + logger.info(f"Locale '{lang}' cached to {cache_path}") + + return locale + + +def _generate(lang: str) -> dict: + base = _load_base() + client = LLMClient() + + prompt = f"""You are a professional UI translator. Translate the following JSON locale file from English to the language with BCP 47 tag "{lang}". + +Rules: +- Keep all JSON keys exactly as-is (do not translate keys) +- Translate only the string values +- Preserve placeholders like {{variable_name}} unchanged +- Keep technical terms like "MiroFish", "GraphRAG", "ReportAgent", "Agent" as-is +- Return only valid JSON, no markdown, no explanation +- CRITICAL: Never put unescaped double-quote characters (") inside a string value. If you need quotation marks in the translation, use single quotes (') instead +- CRITICAL: If the source string contains \\\" (escaped double quote), replace it with a single quote (') in your translation + +English source: +{json.dumps(base, ensure_ascii=False, indent=2)}""" + + # Use chat() directly (no response_format) because some free-tier models + # (e.g. gemini-2.0-flash-exp:free) don't support JSON mode and truncate output. + # Parse the response manually after stripping markdown fences. + # 8192 tokens needed for large locale files (Hungarian and similar languages + # produce longer strings than English). + raw = client.chat( + messages=[{"role": "user", "content": prompt}], + temperature=0.2, + max_tokens=8192 + ) + + # Strip markdown code fences if present + cleaned = raw.strip() + cleaned = re.sub(r'^```(?:json)?\s*\n?', '', cleaned, flags=re.IGNORECASE) + cleaned = re.sub(r'\n?```\s*$', '', cleaned) + cleaned = cleaned.strip() + + try: + return json.loads(cleaned) + except json.JSONDecodeError as exc: + raise ValueError(f"LLM返回的JSON格式无效: {cleaned[:200]}") from exc diff --git a/backend/app/services/oasis_profile_generator.py b/backend/app/services/oasis_profile_generator.py index 57836c539..b69b5bb1f 100644 --- a/backend/app/services/oasis_profile_generator.py +++ b/backend/app/services/oasis_profile_generator.py @@ -139,12 +139,19 @@ def to_dict(self) -> Dict[str, Any]: } +def _with_language(messages: list, language: str) -> list: + """Prepend language instruction to messages when not English.""" + if language and language != "en": + return [{"role": "system", "content": f"Generate all your output in the language with BCP 47 tag '{language}'. Do not use any other language."}] + list(messages) + return list(messages) + + class OasisProfileGenerator: """ OASIS Profile生成器 - + 将Zep图谱中的实体转换为OASIS模拟所需的Agent Profile - + 优化特性: 1. 调用Zep图谱检索功能获取更丰富的上下文 2. 生成非常详细的人设(包括基本信息、职业经历、性格特征、社交媒体行为等) @@ -209,10 +216,11 @@ def __init__( logger.warning(f"Zep客户端初始化失败: {e}") def generate_profile_from_entity( - self, - entity: EntityNode, + self, + entity: EntityNode, user_id: int, - use_llm: bool = True + use_llm: bool = True, + language: str = "en" ) -> OasisAgentProfile: """ 从Zep实体生成OASIS Agent Profile @@ -241,7 +249,8 @@ def generate_profile_from_entity( entity_type=entity_type, entity_summary=entity.summary, entity_attributes=entity.attributes, - context=context + context=context, + language=language ) else: # 使用规则生成基础人设 @@ -499,7 +508,8 @@ def _generate_profile_with_llm( entity_type: str, entity_summary: str, entity_attributes: Dict[str, Any], - context: str + context: str, + language: str = "en" ) -> Dict[str, Any]: """ 使用LLM生成非常详细的人设 @@ -528,10 +538,10 @@ def _generate_profile_with_llm( try: response = self.client.chat.completions.create( model=self.model_name, - messages=[ + messages=_with_language([ {"role": "system", "content": self._get_system_prompt(is_individual)}, {"role": "user", "content": prompt} - ], + ], language), response_format={"type": "json_object"}, temperature=0.7 - (attempt * 0.1) # 每次重试降低温度 # 不设置max_tokens,让LLM自由发挥 @@ -855,7 +865,8 @@ def generate_profiles_from_entities( graph_id: Optional[str] = None, parallel_count: int = 5, realtime_output_path: Optional[str] = None, - output_platform: str = "reddit" + output_platform: str = "reddit", + language: str = "en" ) -> List[OasisAgentProfile]: """ 批量从实体生成Agent Profile(支持并行生成) @@ -923,7 +934,8 @@ def generate_single_profile(idx: int, entity: EntityNode) -> tuple: profile = self.generate_profile_from_entity( entity=entity, user_id=idx, - use_llm=use_llm + use_llm=use_llm, + language=language ) # 实时输出生成的人设到控制台和日志 diff --git a/backend/app/services/ontology_generator.py b/backend/app/services/ontology_generator.py index 2d3e39bd8..f8996e86a 100644 --- a/backend/app/services/ontology_generator.py +++ b/backend/app/services/ontology_generator.py @@ -168,36 +168,44 @@ def generate( self, document_texts: List[str], simulation_requirement: str, - additional_context: Optional[str] = None + additional_context: Optional[str] = None, + language: str = "en" ) -> Dict[str, Any]: """ 生成本体定义 - + Args: document_texts: 文档文本列表 simulation_requirement: 模拟需求描述 additional_context: 额外上下文 - + language: BCP 47 language tag for LLM output (default: "en") + Returns: 本体定义(entity_types, edge_types等) """ # 构建用户消息 user_message = self._build_user_message( - document_texts, + document_texts, simulation_requirement, additional_context ) - + messages = [ {"role": "system", "content": ONTOLOGY_SYSTEM_PROMPT}, {"role": "user", "content": user_message} ] - + + # Prepend language instruction when not English + if language and language != "en": + messages = [ + {"role": "system", "content": f"Generate all your output in the language with BCP 47 tag '{language}'. Do not use any other language."} + ] + messages + # 调用LLM result = self.llm_client.chat_json( messages=messages, temperature=0.3, - max_tokens=4096 + max_tokens=8192 ) # 验证和后处理 diff --git a/backend/app/services/report_agent.py b/backend/app/services/report_agent.py index 02ca5bdc2..bd7d45f5a 100644 --- a/backend/app/services/report_agent.py +++ b/backend/app/services/report_agent.py @@ -901,18 +901,21 @@ def __init__( self.graph_id = graph_id self.simulation_id = simulation_id self.simulation_requirement = simulation_requirement - + self.llm = llm_client or LLMClient() self.zep_tools = zep_tools or ZepToolsService() - + + # Active language for LLM calls (set per-call by generate_report / chat) + self.language: str = "en" + # 工具定义 self.tools = self._define_tools() - + # 日志记录器(在 generate_report 中初始化) self.report_logger: Optional[ReportLogger] = None # 控制台日志记录器(在 generate_report 中初始化) self.console_logger: Optional[ReportConsoleLogger] = None - + logger.info(f"ReportAgent 初始化完成: graph_id={graph_id}, simulation_id={simulation_id}") def _define_tools(self) -> Dict[str, Dict[str, Any]]: @@ -952,6 +955,14 @@ def _define_tools(self) -> Dict[str, Dict[str, Any]]: } } + def _with_language(self, messages: list) -> list: + """Prepend language instruction to messages when not English.""" + if self.language and self.language != "en": + return [ + {"role": "system", "content": f"Generate all your output in the language with BCP 47 tag '{self.language}'. Do not use any other language."} + ] + list(messages) + return list(messages) + def _execute_tool(self, tool_name: str, parameters: Dict[str, Any], report_context: str = "") -> str: """ 执行工具调用 @@ -1174,10 +1185,10 @@ def plan_outline( try: response = self.llm.chat_json( - messages=[ + messages=self._with_language([ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt} - ], + ]), temperature=0.3 ) @@ -1275,11 +1286,11 @@ def _generate_section_react( section_title=section.title, ) - messages = [ + messages = self._with_language([ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt} - ] - + ]) + # ReACT循环 tool_calls_count = 0 max_iterations = 5 # 最大迭代轮数 @@ -1530,9 +1541,10 @@ def _generate_section_react( return final_answer def generate_report( - self, + self, progress_callback: Optional[Callable[[str, int, str], None]] = None, - report_id: Optional[str] = None + report_id: Optional[str] = None, + language: str = "en" ) -> Report: """ 生成完整报告(分章节实时输出) @@ -1556,7 +1568,10 @@ def generate_report( Report: 完整报告 """ import uuid - + + # Store language for use by _with_language() in LLM calls + self.language = language + # 如果没有传入 report_id,则自动生成 if not report_id: report_id = f"report_{uuid.uuid4().hex[:12]}" @@ -1764,9 +1779,10 @@ def generate_report( return report def chat( - self, + self, message: str, - chat_history: List[Dict[str, str]] = None + chat_history: List[Dict[str, str]] = None, + language: str = "en" ) -> Dict[str, Any]: """ 与Report Agent对话 @@ -1785,7 +1801,10 @@ def chat( } """ logger.info(f"Report Agent对话: {message[:50]}...") - + + # Store language for _with_language() calls + self.language = language + chat_history = chat_history or [] # 获取已生成的报告内容 @@ -1807,15 +1826,15 @@ def chat( ) # 构建消息 - messages = [{"role": "system", "content": system_prompt}] - + messages = self._with_language([{"role": "system", "content": system_prompt}]) + # 添加历史对话 for h in chat_history[-10:]: # 限制历史长度 messages.append(h) - + # 添加用户消息 messages.append({ - "role": "user", + "role": "user", "content": message }) diff --git a/backend/app/services/simulation_config_generator.py b/backend/app/services/simulation_config_generator.py index cc362508b..cfa5b85bf 100644 --- a/backend/app/services/simulation_config_generator.py +++ b/backend/app/services/simulation_config_generator.py @@ -196,10 +196,17 @@ def to_json(self, indent: int = 2) -> str: return json.dumps(self.to_dict(), ensure_ascii=False, indent=indent) +def _with_language(messages: list, language: str) -> list: + """Prepend language instruction to messages when not English.""" + if language and language != "en": + return [{"role": "system", "content": f"Generate all your output in the language with BCP 47 tag '{language}'. Do not use any other language."}] + list(messages) + return list(messages) + + class SimulationConfigGenerator: """ 模拟配置智能生成器 - + 使用LLM分析模拟需求、文档内容、图谱实体信息, 自动生成最佳的模拟参数配置 @@ -250,6 +257,7 @@ def generate_config( enable_twitter: bool = True, enable_reddit: bool = True, progress_callback: Optional[Callable[[int, int, str], None]] = None, + language: str = "en" ) -> SimulationParameters: """ 智能生成完整的模拟配置(分步生成) @@ -269,7 +277,10 @@ def generate_config( SimulationParameters: 完整的模拟参数 """ logger.info(f"开始智能生成模拟配置: simulation_id={simulation_id}, 实体数={len(entities)}") - + + # Store language so _call_llm_with_retry can use it + self._language = language + # 计算总步骤数 num_batches = math.ceil(len(entities) / self.AGENTS_PER_BATCH) total_steps = 3 + num_batches # 时间配置 + 事件配置 + N批Agent + 平台配置 @@ -433,18 +444,21 @@ def _summarize_entities(self, entities: List[EntityNode]) -> str: def _call_llm_with_retry(self, prompt: str, system_prompt: str) -> Dict[str, Any]: """带重试的LLM调用,包含JSON修复逻辑""" import re - + + # Use language stored by generate_config (falls back to "en") + language = getattr(self, '_language', 'en') + max_attempts = 3 last_error = None - + for attempt in range(max_attempts): try: response = self.client.chat.completions.create( model=self.model_name, - messages=[ + messages=_with_language([ {"role": "system", "content": system_prompt}, {"role": "user", "content": prompt} - ], + ], language), response_format={"type": "json_object"}, temperature=0.7 - (attempt * 0.1) # 每次重试降低温度 # 不设置max_tokens,让LLM自由发挥 diff --git a/backend/app/services/simulation_manager.py b/backend/app/services/simulation_manager.py index 96c496fd4..06b1d8ad1 100644 --- a/backend/app/services/simulation_manager.py +++ b/backend/app/services/simulation_manager.py @@ -234,7 +234,8 @@ def prepare_simulation( defined_entity_types: Optional[List[str]] = None, use_llm_for_profiles: bool = True, progress_callback: Optional[callable] = None, - parallel_profile_count: int = 3 + parallel_profile_count: int = 3, + language: str = "en" ) -> SimulationState: """ 准备模拟环境(全程自动化) @@ -342,7 +343,8 @@ def profile_progress(current, total, msg): graph_id=state.graph_id, # 传入graph_id用于Zep检索 parallel_count=parallel_profile_count, # 并行生成数量 realtime_output_path=realtime_output_path, # 实时保存路径 - output_platform=realtime_platform # 输出格式 + output_platform=realtime_platform, # 输出格式 + language=language ) state.profiles_count = len(profiles) @@ -407,7 +409,8 @@ def profile_progress(current, total, msg): document_text=document_text, entities=filtered.entities, enable_twitter=state.enable_twitter, - enable_reddit=state.enable_reddit + enable_reddit=state.enable_reddit, + language=language ) if progress_callback: diff --git a/backend/app/utils/llm_client.py b/backend/app/utils/llm_client.py index 6c1a81f49..247524d3b 100644 --- a/backend/app/utils/llm_client.py +++ b/backend/app/utils/llm_client.py @@ -37,30 +37,39 @@ def chat( messages: List[Dict[str, str]], temperature: float = 0.7, max_tokens: int = 4096, - response_format: Optional[Dict] = None + response_format: Optional[Dict] = None, + language: Optional[str] = None ) -> str: """ 发送聊天请求 - + Args: messages: 消息列表 temperature: 温度参数 max_tokens: 最大token数 response_format: 响应格式(如JSON模式) - + language: 输出语言(BCP 47语言标签,如'zh-CN', 'fr'等) + Returns: 模型响应文本 """ + if language and language != "en": + lang_instruction = { + "role": "system", + "content": f"Generate all your output in the language with BCP 47 tag '{language}'. Do not use any other language." + } + messages = [lang_instruction] + list(messages) + kwargs = { "model": self.model, "messages": messages, "temperature": temperature, "max_tokens": max_tokens, } - + if response_format: kwargs["response_format"] = response_format - + response = self.client.chat.completions.create(**kwargs) content = response.choices[0].message.content # 部分模型(如MiniMax M2.5)会在content中包含思考内容,需要移除 @@ -71,16 +80,18 @@ def chat_json( self, messages: List[Dict[str, str]], temperature: float = 0.3, - max_tokens: int = 4096 + max_tokens: int = 4096, + language: Optional[str] = None ) -> Dict[str, Any]: """ 发送聊天请求并返回JSON - + Args: messages: 消息列表 temperature: 温度参数 max_tokens: 最大token数 - + language: 输出语言(BCP 47语言标签,如'zh-CN', 'fr'等) + Returns: 解析后的JSON对象 """ @@ -88,7 +99,8 @@ def chat_json( messages=messages, temperature=temperature, max_tokens=max_tokens, - response_format={"type": "json_object"} + response_format={"type": "json_object"}, + language=language ) # 清理markdown代码块标记 cleaned_response = response.strip() diff --git a/docker-compose.yml b/docker-compose.yml index 637f1dfae..148085d4c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,14 +1,14 @@ services: mirofish: image: ghcr.io/666ghj/mirofish:latest - # 加速镜像(如拉取缓慢可替换上方地址) - # image: ghcr.nju.edu.cn/666ghj/mirofish:latest container_name: mirofish env_file: - .env ports: - - "3000:3000" + - "3001:3000" - "5001:5001" restart: unless-stopped volumes: - - ./backend/uploads:/app/backend/uploads \ No newline at end of file + - ./backend/uploads:/app/backend/uploads + - ./backend/app:/app/backend/app + - ./frontend/src:/app/frontend/src \ No newline at end of file diff --git a/docs/superpowers/plans/2026-03-22-infinite-i18n.md b/docs/superpowers/plans/2026-03-22-infinite-i18n.md new file mode 100644 index 000000000..1fa3f6a9b --- /dev/null +++ b/docs/superpowers/plans/2026-03-22-infinite-i18n.md @@ -0,0 +1,775 @@ +# Infinite Language Support (LLM-Powered i18n) Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** MiroFish automatically detects the user's browser language and serves the full UI + AI outputs in that language, supporting any language on earth via LLM-generated translations cached as JSON files. + +**Architecture:** Browser sends `Accept-Language` header → backend checks if `backend/app/i18n/{lang}.json` exists → if not, LLM generates it from the English base → frontend fetches it via `GET /api/locale/{lang}` → vue-i18n renders all UI strings in the detected language. All LLM service calls receive the detected language explicitly (captured in the request handler and passed through to background threads and subprocesses) and instruct the model to generate output in that language. + +**Tech Stack:** vue-i18n@9 (frontend), Flask endpoint (backend), existing LLMClient (translation generation), Python json (locale file caching) + +**Key architectural note:** Flask's `g` object is request-context-local and is NOT accessible from background threads or subprocesses. The language must be captured from `g.locale` in the request handler and passed explicitly as a parameter through every function call that reaches an LLM. + +--- + +## File Map + +### New Files +- `backend/app/i18n/` — directory for locale JSON files (create with `mkdir`) +- `backend/app/i18n/en.json` — English base locale (source of truth for all UI strings) +- `backend/app/services/locale_generator.py` — Generates locale JSON for unknown languages via LLM +- `backend/app/api/locale.py` — Flask blueprint: `GET /api/locale/` +- `frontend/src/i18n/index.js` — vue-i18n setup, detects browser language, fetches locale from API + +### Modified Files +- `backend/app/__init__.py` — Add `before_request` locale middleware + register locale blueprint +- `backend/app/api/locale.py` — (new, as above) +- `backend/app/api/graph.py` — Capture `g.locale` before thread spawn, pass to generator +- `backend/app/api/simulation.py` — Capture `g.locale` before thread spawn, pass to services +- `backend/app/utils/llm_client.py` — Add optional `language` param to `chat()` and `chat_json()` +- `backend/app/services/ontology_generator.py` — Accept and pass `language` param to LLM call +- `backend/app/services/oasis_profile_generator.py` — Inject language instruction directly into messages list (uses OpenAI client directly, not LLMClient) +- `backend/app/services/simulation_config_generator.py` — Same as above +- `backend/app/services/report_agent.py` — Accept and pass `language` param to LLM call +- `frontend/src/main.js` — Mount vue-i18n +- `frontend/src/api/index.js` — Add `Accept-Language` header to all requests +- `frontend/src/views/Home.vue` — Replace hardcoded strings with `$t()` +- `frontend/src/views/MainView.vue` — Replace hardcoded strings with `$t()` +- `frontend/src/views/SimulationView.vue` — Replace hardcoded strings with `$t()` +- `frontend/src/views/SimulationRunView.vue` — Replace hardcoded strings with `$t()` +- `frontend/src/views/ReportView.vue` — Replace hardcoded strings with `$t()` +- `frontend/src/views/InteractionView.vue` — Replace hardcoded strings with `$t()` +- `frontend/src/components/Step1GraphBuild.vue` — Replace hardcoded strings with `$t()` +- `frontend/src/components/Step2EnvSetup.vue` — Replace hardcoded strings with `$t()` +- `frontend/src/components/Step3Simulation.vue` — Replace hardcoded strings with `$t()` +- `frontend/src/components/Step4Report.vue` — Replace hardcoded strings with `$t()` +- `frontend/src/components/Step5Interaction.vue` — Replace hardcoded strings with `$t()` +- `frontend/src/components/HistoryDatabase.vue` — Replace hardcoded strings with `$t()` +- `frontend/src/components/GraphPanel.vue` — Replace hardcoded strings with `$t()` +- `frontend/package.json` — Add vue-i18n dependency + +--- + +## Task 1: Create English Base Locale JSON + +**Files:** +- Create: `backend/app/i18n/en.json` + +This file is the single source of truth. Every UI string lives here. The LLM uses this to generate other languages. + +- [ ] **Step 1: Create the i18n directory and English base locale file** + +```bash +mkdir -p backend/app/i18n +``` + +Then create `backend/app/i18n/en.json`: + +```json +{ + "nav": { + "github": "Visit our GitHub", + "mirofish": "MIROFISH" + }, + "home": { + "tag": "Simple Universal Swarm Intelligence Engine", + "version": "/ v0.1-Preview", + "title_line1": "Upload Any Report", + "title_line2": "Predict the Future", + "desc1": "Even from a single paragraph, MiroFish can automatically generate a parallel world with up to million-scale Agents based on real-world seeds. Inject variables from a god's-eye view to find the \"local optimum\" in complex group interactions.", + "slogan": "Let the future be rehearsed in Agent swarms, and decisions forged through simulation", + "status_label": "System Status", + "status_ready": "Ready", + "status_desc": "Prediction engine on standby. Upload unstructured data to initialize the simulation sequence.", + "metric_cost_value": "Low Cost", + "metric_cost_label": "~$5 per simulation", + "metric_scale_value": "Scalable", + "metric_scale_label": "Up to million-scale Agents", + "workflow_title": "Workflow Sequence", + "step1_title": "Graph Build", + "step1_desc": "Reality seed extraction & individual/group memory injection & GraphRAG build", + "step2_title": "Environment Setup", + "step2_desc": "Entity-relation extraction & persona generation & environment config Agent injection", + "step3_title": "Run Simulation", + "step3_desc": "Dual-platform parallel simulation & auto-parse prediction requirements & dynamic temporal memory updates", + "step4_title": "Report Generation", + "step4_desc": "ReportAgent uses a rich toolset for deep interaction with the post-simulation environment", + "step5_title": "Deep Interaction", + "step5_desc": "Chat with any agent in the simulated world & interact with ReportAgent", + "seed_label": "01 / Reality Seed", + "seed_formats": "Supported formats: PDF, MD, TXT", + "upload_title": "Drag & Drop Files", + "upload_hint": "or click to browse", + "input_params": "Input Parameters", + "prompt_label": ">_ 02 / Simulation Prompt", + "prompt_placeholder": "// Enter your simulation or prediction requirement in natural language (e.g. If a major company announces layoffs, what public sentiment trends would emerge?)", + "engine_badge": "Engine: MiroFish-V1.0", + "launch_btn": "Launch Engine", + "launching_btn": "Initializing..." + }, + "history": { + "title": "History Database", + "empty": "No simulation history found.", + "status_completed": "Completed", + "status_running": "Running", + "status_failed": "Failed", + "view_btn": "View", + "delete_btn": "Delete" + }, + "graph": { + "building": "Building knowledge graph...", + "complete": "Graph build complete", + "error": "Graph build failed" + }, + "simulation": { + "preparing": "Preparing simulation...", + "running": "Simulation running", + "complete": "Simulation complete", + "failed": "Simulation failed", + "twitter_platform": "Twitter", + "reddit_platform": "Reddit", + "rounds": "Rounds", + "agents": "Agents", + "start_btn": "Start Simulation", + "stop_btn": "Stop" + }, + "report": { + "generating": "Generating report...", + "complete": "Report ready", + "download_btn": "Download", + "interact_btn": "Interact with ReportAgent" + }, + "interaction": { + "placeholder": "Ask a question...", + "send_btn": "Send", + "select_agent": "Select an agent", + "report_agent": "ReportAgent" + }, + "common": { + "loading": "Loading...", + "error": "An error occurred", + "retry": "Retry", + "back": "Back", + "next": "Next", + "cancel": "Cancel", + "confirm": "Confirm" + } +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add backend/app/i18n/en.json +git commit -m "feat(i18n): add English base locale JSON" +``` + +--- + +## Task 2: Create Locale Generator Service + +**Files:** +- Create: `backend/app/services/locale_generator.py` + +Uses `LLMClient.chat_json()` (which already handles JSON fence-stripping and parsing) to translate the English base into any target language. Result is saved as `backend/app/i18n/{lang}.json`. The directory is created defensively before writing. + +- [ ] **Step 1: Create the locale generator** + +```python +""" +Locale generator service. +Uses LLM to translate the English base locale into any target language. +Generated locale files are cached to disk. +""" + +import json +from pathlib import Path + +from ..utils.llm_client import LLMClient +from ..utils.logger import get_logger + +logger = get_logger(__name__) + +I18N_DIR = Path(__file__).parent.parent / "i18n" +BASE_LOCALE = "en" + + +def _load_base() -> dict: + path = I18N_DIR / f"{BASE_LOCALE}.json" + with open(path, "r", encoding="utf-8") as f: + return json.load(f) + + +def get_locale(lang: str) -> dict: + """ + Return locale dict for given language tag (e.g. 'hu', 'ja', 'fr'). + Loads from cache if available, otherwise generates via LLM and caches. + """ + # Normalize: 'hu-HU' -> 'hu' + lang = lang.split("-")[0].lower() + + if lang == BASE_LOCALE: + return _load_base() + + cache_path = I18N_DIR / f"{lang}.json" + if cache_path.exists(): + with open(cache_path, "r", encoding="utf-8") as f: + return json.load(f) + + logger.info(f"Generating locale for '{lang}' via LLM...") + locale = _generate(lang) + + # Ensure directory exists before writing + cache_path.parent.mkdir(parents=True, exist_ok=True) + cache_path.write_text(json.dumps(locale, ensure_ascii=False, indent=2), encoding="utf-8") + logger.info(f"Locale '{lang}' cached to {cache_path}") + + return locale + + +def _generate(lang: str) -> dict: + base = _load_base() + client = LLMClient() + + prompt = f"""You are a professional UI translator. Translate the following JSON locale file from English to the language with BCP 47 tag "{lang}". + +Rules: +- Keep all JSON keys exactly as-is (do not translate keys) +- Translate only the string values +- Preserve placeholders like {{variable_name}} unchanged +- Keep technical terms like "MiroFish", "GraphRAG", "ReportAgent", "Agent" as-is +- Return only valid JSON, no markdown, no explanation + +English source: +{json.dumps(base, ensure_ascii=False, indent=2)}""" + + # Use chat_json() which handles JSON fence-stripping and parsing consistently + return client.chat_json( + messages=[{"role": "user", "content": prompt}], + temperature=0.2, + max_tokens=4096 + ) +``` + +- [ ] **Step 2: Commit** + +```bash +git add backend/app/services/locale_generator.py +git commit -m "feat(i18n): add LLM-powered locale generator service" +``` + +--- + +## Task 3: Create Locale API Endpoint + +**Files:** +- Create: `backend/app/api/locale.py` +- Modify: `backend/app/__init__.py` (app factory — NOT `api/__init__.py`) + +**Important:** Blueprint registration always happens in `backend/app/__init__.py` (the app factory). The file `backend/app/api/__init__.py` only imports route modules — it does not call `app.register_blueprint()`. + +- [ ] **Step 1: Create locale blueprint** + +```python +""" +Locale API endpoint. +GET /api/locale/ — returns locale JSON for given language tag. +""" + +from flask import Blueprint, jsonify +from ..services.locale_generator import get_locale +from ..utils.logger import get_logger + +logger = get_logger(__name__) +locale_bp = Blueprint("locale", __name__) + + +@locale_bp.route("/", methods=["GET"]) +def get_locale_route(lang: str): + try: + data = get_locale(lang) + return jsonify({"success": True, "data": data}) + except Exception as e: + logger.error(f"Failed to get locale '{lang}': {e}") + return jsonify({"success": False, "error": str(e)}), 500 +``` + +- [ ] **Step 2: Register the blueprint in the app factory** + +Open `backend/app/__init__.py`. Find where other blueprints are registered (e.g. `app.register_blueprint(graph_bp, ...)`) and add: + +```python +from .api.locale import locale_bp +app.register_blueprint(locale_bp, url_prefix="/api/locale") +``` + +- [ ] **Step 3: Test the endpoint with curl** + +```bash +curl http://localhost:5001/api/locale/en +# Expected: {"success": true, "data": {...English strings...}} + +curl http://localhost:5001/api/locale/hu +# Expected: {"success": true, "data": {...Hungarian strings...}} (LLM generates on first call) +# Also verify: backend/app/i18n/hu.json was created on disk +``` + +- [ ] **Step 4: Commit** + +```bash +git add backend/app/api/locale.py backend/app/__init__.py +git commit -m "feat(i18n): add GET /api/locale/ endpoint" +``` + +--- + +## Task 4: Add Language to LLM Client + +**Files:** +- Modify: `backend/app/utils/llm_client.py` + +Add an optional `language` parameter to `chat()` and `chat_json()`. When provided, prepend a system message instructing the LLM to generate output in that language. + +- [ ] **Step 1: Modify `chat()` method signature and body** + +```python +def chat( + self, + messages: List[Dict[str, str]], + temperature: float = 0.7, + max_tokens: int = 4096, + response_format: Optional[Dict] = None, + language: Optional[str] = None, +) -> str: + if language and language != "en": + lang_instruction = { + "role": "system", + "content": f"Generate all your output in the language with BCP 47 tag '{language}'. Do not use any other language." + } + messages = [lang_instruction] + list(messages) + + kwargs = { + "model": self.model, + "messages": messages, + "temperature": temperature, + "max_tokens": max_tokens, + } + if response_format: + kwargs["response_format"] = response_format + + response = self.client.chat.completions.create(**kwargs) + content = response.choices[0].message.content + content = re.sub(r'[\s\S]*?', '', content).strip() + return content +``` + +- [ ] **Step 2: Modify `chat_json()` to pass language through** + +```python +def chat_json( + self, + messages: List[Dict[str, str]], + temperature: float = 0.3, + max_tokens: int = 4096, + language: Optional[str] = None, +) -> Dict[str, Any]: + response = self.chat( + messages=messages, + temperature=temperature, + max_tokens=max_tokens, + response_format={"type": "json_object"}, + language=language, + ) + # ... rest of existing JSON parsing code unchanged +``` + +- [ ] **Step 3: Commit** + +```bash +git add backend/app/utils/llm_client.py +git commit -m "feat(i18n): add language parameter to LLMClient.chat() and chat_json()" +``` + +--- + +## Task 5: Add Locale Middleware + Capture Language Before Thread Spawn + +**Files:** +- Modify: `backend/app/__init__.py` +- Modify: `backend/app/api/graph.py` +- Modify: `backend/app/api/simulation.py` + +**Critical:** Flask's `g` object is request-context-local and NOT accessible inside background threads or subprocesses. The language must be captured from the request context before the thread is spawned and passed explicitly as a parameter. + +- [ ] **Step 1: Add `before_request` locale middleware in app factory** + +In `backend/app/__init__.py`, inside `create_app()`: + +```python +from flask import request, g + +@app.before_request +def set_locale(): + # Normalize 'hu-HU,hu;q=0.9,en;q=0.8' -> 'hu' + accept_lang = request.headers.get("Accept-Language", "en") + g.locale = accept_lang.split(",")[0].split("-")[0].lower() +``` + +- [ ] **Step 2: Capture locale in `graph.py` before thread spawn** + +Find the request handler in `backend/app/api/graph.py` that spawns a background thread for ontology generation. Capture `g.locale` before `thread.start()` and pass it to the thread target: + +```python +from flask import g + +# In the request handler, before thread.start(): +locale = getattr(g, 'locale', 'en') + +def build_task(): + ... + result = ontology_generator.generate( + document_texts=..., + simulation_requirement=..., + language=locale # explicit parameter, NOT g.locale + ) +``` + +- [ ] **Step 3: Capture locale in `simulation.py` before thread spawn** + +Same pattern in `backend/app/api/simulation.py` for the prepare/run threads: + +```python +locale = getattr(g, 'locale', 'en') + +def run_prepare(): + ... + profile_generator.generate_profiles_from_entities(..., language=locale) + config_generator.generate_config(..., language=locale) +``` + +- [ ] **Step 4: Commit** + +```bash +git add backend/app/__init__.py backend/app/api/graph.py backend/app/api/simulation.py +git commit -m "feat(i18n): add locale middleware, capture language before thread spawn" +``` + +--- + +## Task 6: Inject Language Into Backend Services + +**Files:** +- Modify: `backend/app/services/ontology_generator.py` +- Modify: `backend/app/services/report_agent.py` +- Modify: `backend/app/services/oasis_profile_generator.py` +- Modify: `backend/app/services/simulation_config_generator.py` + +**Note:** `ontology_generator.py` and `report_agent.py` use `LLMClient` — pass `language` to `chat()`/`chat_json()`. `oasis_profile_generator.py` and `simulation_config_generator.py` call `OpenAI()` directly — inject the language instruction into the `messages` list manually. + +- [ ] **Step 1: Update `ontology_generator.py`** + +Add `language: str = "en"` to the `generate()` method signature. Pass it to every `self.llm_client.chat_json()` call: + +```python +def generate(self, document_texts, simulation_requirement, language: str = "en"): + ... + result = self.llm_client.chat_json(messages=messages, language=language) +``` + +- [ ] **Step 2: Update `report_agent.py`** + +Same pattern — add `language` param, pass to all `self.llm_client.chat()` calls. + +- [ ] **Step 3: Update `oasis_profile_generator.py`** + +This service calls `self.client.chat.completions.create()` directly. Add `language: str = "en"` to relevant method signatures. Before each `messages` list is used in an LLM call, prepend the language instruction when language is not English: + +```python +def _build_messages_with_language(messages: list, language: str) -> list: + if language and language != "en": + return [{"role": "system", "content": f"Generate all your output in the language with BCP 47 tag '{language}'. Do not use any other language."}] + messages + return messages + +# In each method, before calling self.client.chat.completions.create(): +messages = _build_messages_with_language(messages, language) +``` + +- [ ] **Step 4: Update `simulation_config_generator.py`** + +Same as Step 3 — inject language instruction into messages before each `self.client.chat.completions.create()` call. + +- [ ] **Step 5: Commit** + +```bash +git add backend/app/services/ontology_generator.py \ + backend/app/services/report_agent.py \ + backend/app/services/oasis_profile_generator.py \ + backend/app/services/simulation_config_generator.py +git commit -m "feat(i18n): inject user language into all LLM service calls" +``` + +--- + +## Task 7: Install vue-i18n and Set Up Frontend + +**Files:** +- Modify: `frontend/package.json` +- Create: `frontend/src/i18n/index.js` +- Modify: `frontend/src/main.js` +- Modify: `frontend/src/api/index.js` + +**Note on Axios interceptor:** The existing response interceptor in `api/index.js` already unwraps the Axios response and returns `response.data` directly. So after `await api.get('/api/locale/hu')`, the result is already `{ success: true, data: {...} }` — not an Axios response object. Use `res.data` to get the locale object. + +- [ ] **Step 1: Install vue-i18n** + +```bash +cd frontend && npm install vue-i18n@9 +``` + +- [ ] **Step 2: Create i18n setup module** + +Create `frontend/src/i18n/index.js`: + +```js +import { createI18n } from 'vue-i18n' +import api from '../api/index.js' + +// Detect browser language, normalize to short tag (e.g. 'hu-HU' -> 'hu') +export function detectLanguage() { + const raw = navigator.language || navigator.languages?.[0] || 'en' + return raw.split('-')[0].toLowerCase() +} + +export async function setupI18n() { + const lang = detectLanguage() + + // Note: api.get() returns response.data directly (interceptor unwraps it). + // So res is { success: true, data: {...locale...} }, and res.data is the locale object. + let messages = {} + try { + const res = await api.get(`/api/locale/${lang}`) + messages = res.data + } catch (e) { + console.warn(`Could not load locale '${lang}', falling back to English`) + try { + const res = await api.get('/api/locale/en') + messages = res.data + } catch { + messages = {} + } + } + + return createI18n({ + legacy: false, + locale: lang, + fallbackLocale: 'en', + messages: { [lang]: messages } + }) +} +``` + +- [ ] **Step 3: Update main.js to bootstrap i18n before mount** + +```js +import { createApp } from 'vue' +import App from './App.vue' +import router from './router' +import { setupI18n } from './i18n/index.js' + +async function bootstrap() { + const i18n = await setupI18n() + const app = createApp(App) + app.use(router) + app.use(i18n) + app.mount('#app') +} + +bootstrap() +``` + +- [ ] **Step 4: Add Accept-Language header to Axios interceptor** + +In `frontend/src/api/index.js`, update the request interceptor: + +```js +import { detectLanguage } from '../i18n/index.js' + +service.interceptors.request.use( + config => { + config.headers['Accept-Language'] = detectLanguage() + return config + }, + error => { + console.error('Request error:', error) + return Promise.reject(error) + } +) +``` + +- [ ] **Step 5: Commit** + +```bash +git add frontend/package.json frontend/src/i18n/index.js frontend/src/main.js frontend/src/api/index.js +git commit -m "feat(i18n): install vue-i18n, auto-detect language, send Accept-Language header" +``` + +--- + +## Task 8: Replace Hardcoded Strings in Home.vue + +**Files:** +- Modify: `frontend/src/views/Home.vue` + +- [ ] **Step 1: Add `useI18n` and replace all hardcoded strings** + +In ` diff --git a/frontend/src/views/MainView.vue b/frontend/src/views/MainView.vue index 6ff299112..1b4cb67c0 100644 --- a/frontend/src/views/MainView.vue +++ b/frontend/src/views/MainView.vue @@ -15,14 +15,14 @@ :class="{ active: viewMode === mode }" @click="viewMode = mode" > - {{ { graph: '图谱', split: '双栏', workbench: '工作台' }[mode] }} + {{ { graph: $t('view.graph'), split: $t('view.split'), workbench: $t('view.workbench') }[mode] }}
- Step {{ currentStep }}/5 + {{ $t('workflow.step_counter', { current: currentStep, total: 5 }) }} {{ stepNames[currentStep - 1] }}
@@ -77,6 +77,8 @@