diff --git a/backend/app/api/graph.py b/backend/app/api/graph.py index 12ff1ba2d..bbc82c86f 100644 --- a/backend/app/api/graph.py +++ b/backend/app/api/graph.py @@ -17,6 +17,7 @@ from ..utils.logger import get_logger from ..models.task import TaskManager, TaskStatus from ..models.project import ProjectManager, ProjectStatus +from ..utils.messages import msg, get_request_language # 获取日志器 logger = get_logger('mirofish.api') @@ -42,9 +43,9 @@ def get_project(project_id: str): if not project: return jsonify({ "success": False, - "error": f"项目不存在: {project_id}" + "error": msg('project_not_found', id=project_id) }), 404 - + return jsonify({ "success": True, "data": project.to_dict() @@ -76,12 +77,12 @@ def delete_project(project_id: str): if not success: return jsonify({ "success": False, - "error": f"项目不存在或删除失败: {project_id}" + "error": msg('project_delete_failed', id=project_id) }), 404 - + return jsonify({ "success": True, - "message": f"项目已删除: {project_id}" + "message": msg('project_deleted', id=project_id) }) @@ -95,9 +96,9 @@ def reset_project(project_id: str): if not project: return jsonify({ "success": False, - "error": f"项目不存在: {project_id}" + "error": msg('project_not_found', id=project_id) }), 404 - + # 重置到本体已生成状态 if project.ontology: project.status = ProjectStatus.ONTOLOGY_GENERATED @@ -111,7 +112,7 @@ def reset_project(project_id: str): return jsonify({ "success": True, - "message": f"项目已重置: {project_id}", + "message": msg('project_reset', id=project_id), "data": project.to_dict() }) @@ -160,7 +161,7 @@ def generate_ontology(): if not simulation_requirement: return jsonify({ "success": False, - "error": "请提供模拟需求描述 (simulation_requirement)" + "error": msg('missing_simulation_requirement') }), 400 # 获取上传的文件 @@ -168,7 +169,7 @@ def generate_ontology(): if not uploaded_files or all(not f.filename for f in uploaded_files): return jsonify({ "success": False, - "error": "请至少上传一个文档文件" + "error": msg('missing_files') }), 400 # 创建项目 @@ -203,7 +204,7 @@ def generate_ontology(): ProjectManager.delete_project(project.project_id) return jsonify({ "success": False, - "error": "没有成功处理任何文档,请检查文件格式" + "error": msg('no_docs_processed') }), 400 # 保存提取的文本 @@ -285,7 +286,7 @@ def build_graph(): # 检查配置 errors = [] if not Config.ZEP_API_KEY: - errors.append("ZEP_API_KEY未配置") + errors.append(msg('zep_not_configured')) if errors: logger.error(f"配置错误: {errors}") return jsonify({ @@ -301,15 +302,15 @@ def build_graph(): if not project_id: return jsonify({ "success": False, - "error": "请提供 project_id" + "error": msg('missing_project_id') }), 400 - + # 获取项目 project = ProjectManager.get_project(project_id) if not project: return jsonify({ "success": False, - "error": f"项目不存在: {project_id}" + "error": msg('project_not_found', id=project_id) }), 404 # 检查项目状态 @@ -318,13 +319,13 @@ def build_graph(): if project.status == ProjectStatus.CREATED: return jsonify({ "success": False, - "error": "项目尚未生成本体,请先调用 /ontology/generate" + "error": msg('ontology_not_generated') }), 400 if project.status == ProjectStatus.GRAPH_BUILDING and not force: return jsonify({ "success": False, - "error": "图谱正在构建中,请勿重复提交。如需强制重建,请添加 force: true", + "error": msg('graph_building_in_progress'), "task_id": project.graph_build_task_id }), 400 @@ -370,6 +371,9 @@ def build_graph(): project.graph_build_task_id = task_id ProjectManager.save_project(project) + # Capture language before entering background thread + language = get_request_language() + # 启动后台任务 def build_task(): build_logger = get_logger('mirofish.build') @@ -378,7 +382,7 @@ def build_task(): task_manager.update_task( task_id, status=TaskStatus.PROCESSING, - message="初始化图谱构建服务..." + message=msg('init_graph_service', lang=language) ) # 创建图谱构建服务 @@ -387,7 +391,7 @@ def build_task(): # 分块 task_manager.update_task( task_id, - message="文本分块中...", + message=msg('text_chunking', lang=language), progress=5 ) chunks = TextProcessor.split_text( @@ -476,7 +480,7 @@ def wait_progress_callback(msg, progress_ratio): task_manager.update_task( task_id, status=TaskStatus.COMPLETED, - message="图谱构建完成", + message=msg('graph_build_complete', lang=language), progress=100, result={ "project_id": project_id, @@ -512,7 +516,7 @@ def wait_progress_callback(msg, progress_ratio): "data": { "project_id": project_id, "task_id": task_id, - "message": "图谱构建任务已启动,请通过 /task/{task_id} 查询进度" + "message": msg('graph_build_started') } }) @@ -536,7 +540,7 @@ def get_task(task_id: str): if not task: return jsonify({ "success": False, - "error": f"任务不存在: {task_id}" + "error": msg('task_not_found', id=task_id) }), 404 return jsonify({ @@ -570,7 +574,7 @@ def get_graph_data(graph_id: str): if not Config.ZEP_API_KEY: return jsonify({ "success": False, - "error": "ZEP_API_KEY未配置" + "error": msg('zep_not_configured') }), 500 builder = GraphBuilderService(api_key=Config.ZEP_API_KEY) @@ -598,7 +602,7 @@ def delete_graph(graph_id: str): if not Config.ZEP_API_KEY: return jsonify({ "success": False, - "error": "ZEP_API_KEY未配置" + "error": msg('zep_not_configured') }), 500 builder = GraphBuilderService(api_key=Config.ZEP_API_KEY) @@ -606,7 +610,7 @@ def delete_graph(graph_id: str): return jsonify({ "success": True, - "message": f"图谱已删除: {graph_id}" + "message": msg('graph_deleted', id=graph_id) }) except Exception as e: diff --git a/backend/app/api/report.py b/backend/app/api/report.py index e05c73c39..f87d8f858 100644 --- a/backend/app/api/report.py +++ b/backend/app/api/report.py @@ -15,6 +15,7 @@ from ..models.project import ProjectManager from ..models.task import TaskManager, TaskStatus from ..utils.logger import get_logger +from ..utils.messages import msg, get_request_language logger = get_logger('mirofish.api.report') @@ -53,19 +54,19 @@ def generate_report(): if not simulation_id: return jsonify({ "success": False, - "error": "请提供 simulation_id" + "error": msg('missing_simulation_id') }), 400 - + force_regenerate = data.get('force_regenerate', False) - + # 获取模拟信息 manager = SimulationManager() state = manager.get_simulation(simulation_id) - + if not state: return jsonify({ "success": False, - "error": f"模拟不存在: {simulation_id}" + "error": msg('simulation_not_found', id=simulation_id) }), 404 # 检查是否已有报告 @@ -78,7 +79,7 @@ def generate_report(): "simulation_id": simulation_id, "report_id": existing_report.report_id, "status": "completed", - "message": "报告已存在", + "message": msg('report_already_generated'), "already_generated": True } }) @@ -88,21 +89,21 @@ def generate_report(): if not project: return jsonify({ "success": False, - "error": f"项目不存在: {state.project_id}" + "error": msg('project_not_found', id=state.project_id) }), 404 - + graph_id = state.graph_id or project.graph_id if not graph_id: return jsonify({ "success": False, - "error": "缺少图谱ID,请确保已构建图谱" + "error": msg('missing_graph_id') }), 400 - + simulation_requirement = project.simulation_requirement if not simulation_requirement: return jsonify({ "success": False, - "error": "缺少模拟需求描述" + "error": msg('missing_requirement') }), 400 # 提前生成 report_id,以便立即返回给前端 @@ -120,6 +121,9 @@ def generate_report(): } ) + # Capture language before entering background thread + language = get_request_language() + # 定义后台任务 def run_generate(): try: @@ -127,7 +131,7 @@ def run_generate(): task_id, status=TaskStatus.PROCESSING, progress=0, - message="初始化Report Agent..." + message=msg('init_report_agent', lang=language) ) # 创建Report Agent @@ -164,7 +168,7 @@ def progress_callback(stage, progress, message): } ) else: - task_manager.fail_task(task_id, report.error or "报告生成失败") + task_manager.fail_task(task_id, report.error or msg('report_generation_failed', lang=language)) except Exception as e: logger.error(f"报告生成失败: {str(e)}") @@ -181,7 +185,7 @@ def progress_callback(stage, progress, message): "report_id": report_id, "task_id": task_id, "status": "generating", - "message": "报告生成任务已启动,请通过 /api/report/generate/status 查询进度", + "message": msg('report_task_started_long'), "already_generated": False } }) @@ -234,7 +238,7 @@ def get_generate_status(): "report_id": existing_report.report_id, "status": "completed", "progress": 100, - "message": "报告已生成", + "message": msg('report_already_generated'), "already_completed": True } }) @@ -242,7 +246,7 @@ def get_generate_status(): if not task_id: return jsonify({ "success": False, - "error": "请提供 task_id 或 simulation_id" + "error": msg('missing_simulation_id') }), 400 task_manager = TaskManager() @@ -251,7 +255,7 @@ def get_generate_status(): if not task: return jsonify({ "success": False, - "error": f"任务不存在: {task_id}" + "error": msg('task_not_found', id=task_id) }), 404 return jsonify({ @@ -294,14 +298,14 @@ def get_report(report_id: str): if not report: return jsonify({ "success": False, - "error": f"报告不存在: {report_id}" + "error": msg('report_not_found') }), 404 - + return jsonify({ "success": True, "data": report.to_dict() }) - + except Exception as e: logger.error(f"获取报告失败: {str(e)}") return jsonify({ @@ -331,7 +335,7 @@ def get_report_by_simulation(simulation_id: str): if not report: return jsonify({ "success": False, - "error": f"该模拟暂无报告: {simulation_id}", + "error": msg('report_not_found'), "has_report": False }), 404 @@ -403,9 +407,9 @@ def download_report(report_id: str): if not report: return jsonify({ "success": False, - "error": f"报告不存在: {report_id}" + "error": msg('report_not_found') }), 404 - + md_path = ReportManager._get_report_markdown_path(report_id) if not os.path.exists(md_path): @@ -445,12 +449,12 @@ def delete_report(report_id: str): if not success: return jsonify({ "success": False, - "error": f"报告不存在: {report_id}" + "error": msg('report_not_found') }), 404 - + return jsonify({ "success": True, - "message": f"报告已删除: {report_id}" + "message": msg('report_deleted', id=report_id) }) except Exception as e: @@ -501,37 +505,37 @@ def chat_with_report_agent(): if not simulation_id: return jsonify({ "success": False, - "error": "请提供 simulation_id" + "error": msg('missing_simulation_id') }), 400 - + if not message: return jsonify({ "success": False, "error": "请提供 message" }), 400 - + # 获取模拟和项目信息 manager = SimulationManager() state = manager.get_simulation(simulation_id) - + if not state: return jsonify({ "success": False, - "error": f"模拟不存在: {simulation_id}" + "error": msg('simulation_not_found', id=simulation_id) }), 404 - + project = ProjectManager.get_project(state.project_id) if not project: return jsonify({ "success": False, - "error": f"项目不存在: {state.project_id}" + "error": msg('project_not_found', id=state.project_id) }), 404 - + graph_id = state.graph_id or project.graph_id if not graph_id: return jsonify({ "success": False, - "error": "缺少图谱ID" + "error": msg('missing_graph_id') }), 400 simulation_requirement = project.simulation_requirement or "" @@ -949,7 +953,7 @@ def search_graph_tool(): if not graph_id or not query: return jsonify({ "success": False, - "error": "请提供 graph_id 和 query" + "error": msg('missing_graph_id') }), 400 from ..services.zep_tools import ZepToolsService @@ -993,11 +997,11 @@ def get_graph_statistics_tool(): if not graph_id: return jsonify({ "success": False, - "error": "请提供 graph_id" + "error": msg('missing_graph_id') }), 400 - + from ..services.zep_tools import ZepToolsService - + tools = ZepToolsService() result = tools.get_graph_statistics(graph_id) diff --git a/backend/app/api/simulation.py b/backend/app/api/simulation.py index 3a0f68168..2ec349af6 100644 --- a/backend/app/api/simulation.py +++ b/backend/app/api/simulation.py @@ -15,31 +15,36 @@ from ..services.simulation_runner import SimulationRunner, RunnerStatus from ..utils.logger import get_logger from ..models.project import ProjectManager +from ..utils.messages import msg, get_request_language logger = get_logger('mirofish.api.simulation') # Interview prompt 优化前缀 -# 添加此前缀可以避免Agent调用工具,直接用文本回复 -INTERVIEW_PROMPT_PREFIX = "结合你的人设、所有的过往记忆与行动,不调用任何工具直接用文本回复我:" +def get_interview_prefix(lang=None): + return msg('interview_prefix', lang=lang) -def optimize_interview_prompt(prompt: str) -> str: +def optimize_interview_prompt(prompt: str, lang=None) -> str: """ 优化Interview提问,添加前缀避免Agent调用工具 - + Args: prompt: 原始提问 - + lang: Language code ('en' or 'zh'). If None, reads from request header. + Returns: 优化后的提问 """ if not prompt: return prompt - # 避免重复添加前缀 - if prompt.startswith(INTERVIEW_PROMPT_PREFIX): + prefix = get_interview_prefix(lang=lang) + # 避免重复添加前缀(check both languages) + en_prefix = msg('interview_prefix', lang='en') + zh_prefix = msg('interview_prefix', lang='zh') + if prompt.startswith(en_prefix) or prompt.startswith(zh_prefix): return prompt - return f"{INTERVIEW_PROMPT_PREFIX}{prompt}" + return f"{prefix}{prompt}" # ============== 实体读取接口 ============== @@ -59,7 +64,7 @@ def get_graph_entities(graph_id: str): if not Config.ZEP_API_KEY: return jsonify({ "success": False, - "error": "ZEP_API_KEY未配置" + "error": msg('zep_not_configured') }), 500 entity_types_str = request.args.get('entity_types', '') @@ -96,7 +101,7 @@ def get_entity_detail(graph_id: str, entity_uuid: str): if not Config.ZEP_API_KEY: return jsonify({ "success": False, - "error": "ZEP_API_KEY未配置" + "error": msg('zep_not_configured') }), 500 reader = ZepEntityReader() @@ -105,7 +110,7 @@ def get_entity_detail(graph_id: str, entity_uuid: str): if not entity: return jsonify({ "success": False, - "error": f"实体不存在: {entity_uuid}" + "error": msg('entity_not_found', id=entity_uuid) }), 404 return jsonify({ @@ -129,7 +134,7 @@ def get_entities_by_type(graph_id: str, entity_type: str): if not Config.ZEP_API_KEY: return jsonify({ "success": False, - "error": "ZEP_API_KEY未配置" + "error": msg('zep_not_configured') }), 500 enrich = request.args.get('enrich', 'true').lower() == 'true' @@ -197,21 +202,21 @@ def create_simulation(): if not project_id: return jsonify({ "success": False, - "error": "请提供 project_id" + "error": msg('missing_project_id') }), 400 project = ProjectManager.get_project(project_id) if not project: return jsonify({ "success": False, - "error": f"项目不存在: {project_id}" + "error": msg('project_not_found', id=project_id) }), 404 graph_id = data.get('graph_id') or project.graph_id if not graph_id: return jsonify({ "success": False, - "error": "项目尚未构建图谱,请先调用 /api/graph/build" + "error": msg('graph_not_built') }), 400 manager = SimulationManager() @@ -408,7 +413,7 @@ def prepare_simulation(): if not simulation_id: return jsonify({ "success": False, - "error": "请提供 simulation_id" + "error": msg('missing_simulation_id') }), 400 manager = SimulationManager() @@ -417,7 +422,7 @@ def prepare_simulation(): if not state: return jsonify({ "success": False, - "error": f"模拟不存在: {simulation_id}" + "error": msg('simulation_not_found', id=simulation_id) }), 404 # 检查是否强制重新生成 @@ -449,7 +454,7 @@ def prepare_simulation(): if not project: return jsonify({ "success": False, - "error": f"项目不存在: {state.project_id}" + "error": msg('project_not_found', id=state.project_id) }), 404 # 获取模拟需求 @@ -457,7 +462,7 @@ def prepare_simulation(): if not simulation_requirement: return jsonify({ "success": False, - "error": "项目缺少模拟需求描述 (simulation_requirement)" + "error": msg('missing_simulation_requirement') }), 400 # 获取文档文本 @@ -702,7 +707,7 @@ def get_prepare_status(): }) return jsonify({ "success": False, - "error": "请提供 task_id 或 simulation_id" + "error": msg('missing_simulation_id') }), 400 task_manager = TaskManager() @@ -728,7 +733,7 @@ def get_prepare_status(): return jsonify({ "success": False, - "error": f"任务不存在: {task_id}" + "error": msg('task_not_found', id=task_id) }), 404 task_dict = task.to_dict() @@ -757,7 +762,7 @@ def get_simulation(simulation_id: str): if not state: return jsonify({ "success": False, - "error": f"模拟不存在: {simulation_id}" + "error": msg('simulation_not_found', id=simulation_id) }), 404 result = state.to_dict() @@ -1061,7 +1066,7 @@ def get_simulation_profiles_realtime(simulation_id: str): if not os.path.exists(sim_dir): return jsonify({ "success": False, - "error": f"模拟不存在: {simulation_id}" + "error": msg('simulation_not_found', id=simulation_id) }), 404 # 确定文件路径 @@ -1164,7 +1169,7 @@ def get_simulation_config_realtime(simulation_id: str): if not os.path.exists(sim_dir): return jsonify({ "success": False, - "error": f"模拟不存在: {simulation_id}" + "error": msg('simulation_not_found', id=simulation_id) }), 404 # 配置文件路径 @@ -1389,7 +1394,7 @@ def generate_profiles(): if not graph_id: return jsonify({ "success": False, - "error": "请提供 graph_id" + "error": msg('missing_graph_id') }), 400 entity_types = data.get('entity_types') @@ -1491,7 +1496,7 @@ def start_simulation(): if not simulation_id: return jsonify({ "success": False, - "error": "请提供 simulation_id" + "error": msg('missing_simulation_id') }), 400 platform = data.get('platform', 'parallel') @@ -1527,7 +1532,7 @@ def start_simulation(): if not state: return jsonify({ "success": False, - "error": f"模拟不存在: {simulation_id}" + "error": msg('simulation_not_found', id=simulation_id) }), 404 force_restarted = False @@ -1663,7 +1668,7 @@ def stop_simulation(): if not simulation_id: return jsonify({ "success": False, - "error": "请提供 simulation_id" + "error": msg('missing_simulation_id') }), 400 run_state = SimulationRunner.stop_simulation(simulation_id) @@ -2197,7 +2202,7 @@ def interview_agent(): if not simulation_id: return jsonify({ "success": False, - "error": "请提供 simulation_id" + "error": msg('missing_simulation_id') }), 400 if agent_id is None: @@ -2318,7 +2323,7 @@ def interview_agents_batch(): if not simulation_id: return jsonify({ "success": False, - "error": "请提供 simulation_id" + "error": msg('missing_simulation_id') }), 400 if not interviews or not isinstance(interviews, list): @@ -2445,7 +2450,7 @@ def interview_all_agents(): if not simulation_id: return jsonify({ "success": False, - "error": "请提供 simulation_id" + "error": msg('missing_simulation_id') }), 400 if not prompt: @@ -2549,7 +2554,7 @@ def get_interview_history(): if not simulation_id: return jsonify({ "success": False, - "error": "请提供 simulation_id" + "error": msg('missing_simulation_id') }), 400 history = SimulationRunner.get_interview_history( @@ -2608,7 +2613,7 @@ def get_env_status(): if not simulation_id: return jsonify({ "success": False, - "error": "请提供 simulation_id" + "error": msg('missing_simulation_id') }), 400 env_alive = SimulationRunner.check_env_alive(simulation_id) @@ -2676,7 +2681,7 @@ def close_simulation_env(): if not simulation_id: return jsonify({ "success": False, - "error": "请提供 simulation_id" + "error": msg('missing_simulation_id') }), 400 result = SimulationRunner.close_simulation_env( diff --git a/backend/app/models/project.py b/backend/app/models/project.py index 089789374..c9c7256fd 100644 --- a/backend/app/models/project.py +++ b/backend/app/models/project.py @@ -51,6 +51,9 @@ class Project: # 错误信息 error: Optional[str] = None + + # 语言 + language: str = 'en' def to_dict(self) -> Dict[str, Any]: """转换为字典""" @@ -69,7 +72,8 @@ def to_dict(self) -> Dict[str, Any]: "simulation_requirement": self.simulation_requirement, "chunk_size": self.chunk_size, "chunk_overlap": self.chunk_overlap, - "error": self.error + "error": self.error, + "language": self.language } @classmethod @@ -94,7 +98,8 @@ def from_dict(cls, data: Dict[str, Any]) -> 'Project': simulation_requirement=data.get('simulation_requirement'), chunk_size=data.get('chunk_size', 500), chunk_overlap=data.get('chunk_overlap', 50), - error=data.get('error') + error=data.get('error'), + language=data.get('language', 'en') ) diff --git a/backend/app/services/oasis_profile_generator.py b/backend/app/services/oasis_profile_generator.py index 57836c539..dd5f5319c 100644 --- a/backend/app/services/oasis_profile_generator.py +++ b/backend/app/services/oasis_profile_generator.py @@ -178,13 +178,15 @@ class OasisProfileGenerator: ] def __init__( - self, + self, api_key: Optional[str] = None, base_url: Optional[str] = None, model_name: Optional[str] = None, zep_api_key: Optional[str] = None, - graph_id: Optional[str] = None + graph_id: Optional[str] = None, + language: str = 'en' ): + self.language = language self.api_key = api_key or Config.LLM_API_KEY self.base_url = base_url or Config.LLM_BASE_URL self.model_name = model_name or Config.LLM_MODEL_NAME @@ -668,9 +670,13 @@ def fix_string_newlines(match): "persona": entity_summary or f"{entity_name}是一个{entity_type}。" } - def _get_system_prompt(self, is_individual: bool) -> str: + def _get_system_prompt(self, is_individual: bool, language: str = None) -> str: """获取系统提示词""" - base_prompt = "你是社交媒体用户画像生成专家。生成详细、真实的人设用于舆论模拟,最大程度还原已有现实情况。必须返回有效的JSON格式,所有字符串值不能包含未转义的换行符。使用中文。" + lang = language or self.language + if lang == 'en': + base_prompt = "You are a social media user profile generation expert. Generate detailed, realistic personas for public opinion simulation, faithfully reproducing known real-world information. You must return valid JSON format, and all string values must not contain unescaped newline characters. Use English." + else: + base_prompt = "你是社交媒体用户画像生成专家。生成详细、真实的人设用于舆论模拟,最大程度还原已有现实情况。必须返回有效的JSON格式,所有字符串值不能包含未转义的换行符。使用中文。" return base_prompt def _build_individual_persona_prompt( @@ -679,14 +685,53 @@ def _build_individual_persona_prompt( entity_type: str, entity_summary: str, entity_attributes: Dict[str, Any], - context: str + context: str, + language: str = None ) -> str: """构建个人实体的详细人设提示词""" - - attrs_str = json.dumps(entity_attributes, ensure_ascii=False) if entity_attributes else "无" - context_str = context[:3000] if context else "无额外上下文" - - return f"""为实体生成详细的社交媒体用户人设,最大程度还原已有现实情况。 + lang = language or self.language + + attrs_str = json.dumps(entity_attributes, ensure_ascii=False) if entity_attributes else ("None" if lang == 'en' else "无") + context_str = context[:3000] if context else ("No additional context" if lang == 'en' else "无额外上下文") + + if lang == 'en': + return f"""Generate a detailed social media user persona for this entity, faithfully reproducing known real-world information. + +Entity Name: {entity_name} +Entity Type: {entity_type} +Entity Summary: {entity_summary} +Entity Attributes: {attrs_str} + +Context Information: +{context_str} + +Please generate JSON with the following fields: + +1. bio: Social media bio, 200 words +2. persona: Detailed persona description (2000 words of plain text), must include: + - Basic information (age, occupation, educational background, location) + - Background (important experiences, connection to events, social relationships) + - Personality traits (MBTI type, core personality, emotional expression style) + - Social media behavior (posting frequency, content preferences, interaction style, language characteristics) + - Stances and viewpoints (attitudes on topics, content that might provoke or move them) + - Unique characteristics (catchphrases, special experiences, personal hobbies) + - Personal memory (important part of the persona, introduce this individual's connection to events, and their existing actions and reactions in the events) +3. age: Age number (must be an integer) +4. gender: Gender, must be English: "male" or "female" +5. mbti: MBTI type (e.g., INTJ, ENFP) +6. country: Country (e.g., "United States", "China") +7. profession: Profession +8. interested_topics: Array of interested topics + +Important: +- All field values must be strings or numbers, do not use newline characters +- persona must be a coherent text description +- Use English (gender field must be English male/female) +- Content must be consistent with entity information +- age must be a valid integer, gender must be "male" or "female" +""" + else: + return f"""为实体生成详细的社交媒体用户人设,最大程度还原已有现实情况。 实体名称: {entity_name} 实体类型: {entity_type} @@ -728,14 +773,52 @@ def _build_group_persona_prompt( entity_type: str, entity_summary: str, entity_attributes: Dict[str, Any], - context: str + context: str, + language: str = None ) -> str: """构建群体/机构实体的详细人设提示词""" - - attrs_str = json.dumps(entity_attributes, ensure_ascii=False) if entity_attributes else "无" - context_str = context[:3000] if context else "无额外上下文" - - return f"""为机构/群体实体生成详细的社交媒体账号设定,最大程度还原已有现实情况。 + lang = language or self.language + + attrs_str = json.dumps(entity_attributes, ensure_ascii=False) if entity_attributes else ("None" if lang == 'en' else "无") + context_str = context[:3000] if context else ("No additional context" if lang == 'en' else "无额外上下文") + + if lang == 'en': + return f"""Generate a detailed social media account profile for an organization/group entity, faithfully reproducing known real-world information. + +Entity Name: {entity_name} +Entity Type: {entity_type} +Entity Summary: {entity_summary} +Entity Attributes: {attrs_str} + +Context Information: +{context_str} + +Please generate JSON with the following fields: + +1. bio: Official account bio, 200 words, professional and appropriate +2. persona: Detailed account profile description (2000 words of plain text), must include: + - Organization basic information (official name, nature of organization, founding background, main functions) + - Account positioning (account type, target audience, core functions) + - Speaking style (language characteristics, common expressions, taboo topics) + - Content characteristics (content types, posting frequency, active time periods) + - Stance and attitude (official stance on core topics, approach to handling controversies) + - Special notes (represented group profile, operational habits) + - Organizational memory (important part of the persona, introduce this organization's connection to events, and its existing actions and reactions in events) +3. age: Fixed at 30 (virtual age for organizational accounts) +4. gender: Fixed at "other" (organizational accounts use "other" to indicate non-individual) +5. mbti: MBTI type, used to describe account style, e.g., ISTJ for rigorous and conservative +6. country: Country (e.g., "United States", "China") +7. profession: Organizational function description +8. interested_topics: Array of focus areas + +Important: +- All field values must be strings or numbers, no null values allowed +- persona must be a coherent text description, do not use newline characters +- Use English (gender field must be English "other") +- age must be integer 30, gender must be string "other" +- Organizational account statements must match its identity positioning""" + else: + return f"""为机构/群体实体生成详细的社交媒体账号设定,最大程度还原已有现实情况。 实体名称: {entity_name} 实体类型: {entity_type} diff --git a/backend/app/services/ontology_generator.py b/backend/app/services/ontology_generator.py index 2d3e39bd8..b668ec220 100644 --- a/backend/app/services/ontology_generator.py +++ b/backend/app/services/ontology_generator.py @@ -154,6 +154,151 @@ - COMPETES_WITH: 竞争 """ +ONTOLOGY_SYSTEM_PROMPT_EN = """You are a professional knowledge graph ontology design expert. Your task is to analyze the given text content and simulation requirements, and design entity types and relationship types suitable for **social media public opinion simulation**. + +**Important: You must output valid JSON format data. Do not output anything else.** + +## Core Task Background + +We are building a **social media public opinion simulation system**. In this system: +- Each entity is an "account" or "agent" that can speak, interact, and spread information on social media +- Entities influence each other through forwarding, commenting, and responding +- We need to simulate the reactions of various parties and the information propagation paths during public opinion events + +Therefore, **entities must be real-world agents that can speak and interact on social media**: + +**Acceptable**: +- Specific individuals (public figures, parties involved, opinion leaders, scholars, ordinary people) +- Companies and enterprises (including their official accounts) +- Organizations and institutions (universities, associations, NGOs, unions, etc.) +- Government departments and regulatory agencies +- Media organizations (newspapers, TV stations, self-media, websites) +- Social media platforms themselves +- Representatives of specific groups (e.g., alumni associations, fan groups, advocacy groups, etc.) + +**Not acceptable**: +- Abstract concepts (e.g., "public opinion", "sentiment", "trend") +- Topics/themes (e.g., "academic integrity", "education reform") +- Viewpoints/attitudes (e.g., "supporters", "opponents") + +## Output Format + +Please output in JSON format with the following structure: + +```json +{ + "entity_types": [ + { + "name": "Entity type name (English, PascalCase)", + "description": "Brief description (English, no more than 100 characters)", + "attributes": [ + { + "name": "attribute_name (English, snake_case)", + "type": "text", + "description": "Attribute description" + } + ], + "examples": ["Example entity 1", "Example entity 2"] + } + ], + "edge_types": [ + { + "name": "Relationship type name (English, UPPER_SNAKE_CASE)", + "description": "Brief description (English, no more than 100 characters)", + "source_targets": [ + {"source": "Source entity type", "target": "Target entity type"} + ], + "attributes": [] + } + ], + "analysis_summary": "Brief analysis summary of the text content (in English)" +} +``` + +## Design Guidelines (Extremely Important!) + +### 1. Entity Type Design - Must Be Strictly Followed + +**Quantity requirement: Exactly 10 entity types** + +**Hierarchy requirements (must include both specific types and fallback types)**: + +Your 10 entity types must include the following layers: + +A. **Fallback types (must include, placed at the end of the list)**: + - `Person`: Fallback type for any natural person. When a person does not belong to other more specific person types, classify them here. + - `Organization`: Fallback type for any organization. When an organization does not belong to other more specific organization types, classify it here. + +B. **Specific types (8, designed based on text content)**: + - Design more specific types for the main roles appearing in the text + - Example: If the text involves academic events, you can have `Student`, `Professor`, `University` + - Example: If the text involves business events, you can have `Company`, `CEO`, `Employee` + +**Why fallback types are needed**: +- Various people appear in the text, such as "elementary school teachers", "bystanders", "some netizen" +- If there is no specific type match, they should be classified as `Person` +- Similarly, small organizations, temporary groups, etc. should be classified as `Organization` + +**Design principles for specific types**: +- Identify high-frequency or key role types from the text +- Each specific type should have clear boundaries to avoid overlap +- The description must clearly explain the difference between this type and the fallback type + +### 2. Relationship Type Design + +- Quantity: 6-10 +- Relationships should reflect real connections in social media interactions +- Ensure that the source_targets of relationships cover your defined entity types + +### 3. Attribute Design + +- 1-3 key attributes per entity type +- **Note**: Attribute names cannot use `name`, `uuid`, `group_id`, `created_at`, `summary` (these are system reserved words) +- Recommended: `full_name`, `title`, `role`, `position`, `location`, `description`, etc. + +## Entity Type Reference + +**Individual (specific)**: +- Student: Student +- Professor: Professor/Scholar +- Journalist: Journalist +- Celebrity: Celebrity/Influencer +- Executive: Executive +- Official: Government official +- Lawyer: Lawyer +- Doctor: Doctor + +**Individual (fallback)**: +- Person: Any natural person (used when not belonging to the above specific types) + +**Organization (specific)**: +- University: University/College +- Company: Company/Enterprise +- GovernmentAgency: Government agency +- MediaOutlet: Media organization +- Hospital: Hospital +- School: Elementary/Secondary school +- NGO: Non-governmental organization + +**Organization (fallback)**: +- Organization: Any organization (used when not belonging to the above specific types) + +## Relationship Type Reference + +- WORKS_FOR: Works for +- STUDIES_AT: Studies at +- AFFILIATED_WITH: Affiliated with +- REPRESENTS: Represents +- REGULATES: Regulates +- REPORTS_ON: Reports on +- COMMENTS_ON: Comments on +- RESPONDS_TO: Responds to +- SUPPORTS: Supports +- OPPOSES: Opposes +- COLLABORATES_WITH: Collaborates with +- COMPETES_WITH: Competes with +""" + class OntologyGenerator: """ @@ -168,28 +313,33 @@ 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: 语言选择,'en' 为英文,'zh' 为中文 + Returns: 本体定义(entity_types, edge_types等) """ # 构建用户消息 user_message = self._build_user_message( - document_texts, + document_texts, simulation_requirement, - additional_context + additional_context, + language=language ) - + + system_prompt = ONTOLOGY_SYSTEM_PROMPT_EN if language == 'en' else ONTOLOGY_SYSTEM_PROMPT + messages = [ - {"role": "system", "content": ONTOLOGY_SYSTEM_PROMPT}, + {"role": "system", "content": system_prompt}, {"role": "user", "content": user_message} ] @@ -212,20 +362,52 @@ def _build_user_message( self, document_texts: List[str], simulation_requirement: str, - additional_context: Optional[str] + additional_context: Optional[str], + language: str = 'en' ) -> str: """构建用户消息""" - + # 合并文本 combined_text = "\n\n---\n\n".join(document_texts) original_length = len(combined_text) - + # 如果文本超过5万字,截断(仅影响传给LLM的内容,不影响图谱构建) if len(combined_text) > self.MAX_TEXT_LENGTH_FOR_LLM: combined_text = combined_text[:self.MAX_TEXT_LENGTH_FOR_LLM] - combined_text += f"\n\n...(原文共{original_length}字,已截取前{self.MAX_TEXT_LENGTH_FOR_LLM}字用于本体分析)..." - - message = f"""## 模拟需求 + if language == 'en': + combined_text += f"\n\n...(Original text has {original_length} characters, truncated to first {self.MAX_TEXT_LENGTH_FOR_LLM} characters for ontology analysis)..." + else: + combined_text += f"\n\n...(原文共{original_length}字,已截取前{self.MAX_TEXT_LENGTH_FOR_LLM}字用于本体分析)..." + + if language == 'en': + message = f"""## Simulation Requirement + +{simulation_requirement} + +## Document Content + +{combined_text} +""" + + if additional_context: + message += f""" +## Additional Notes + +{additional_context} +""" + + message += """ +Based on the above content, design entity types and relationship types suitable for social media public opinion simulation. + +**Rules that must be followed**: +1. You must output exactly 10 entity types +2. The last 2 must be fallback types: Person (individual fallback) and Organization (organization fallback) +3. The first 8 are specific types designed based on the text content +4. All entity types must be real-world agents that can speak on social media, not abstract concepts +5. Attribute names cannot use reserved words like name, uuid, group_id; use full_name, org_name, etc. instead +""" + else: + message = f"""## 模拟需求 {simulation_requirement} @@ -233,15 +415,15 @@ def _build_user_message( {combined_text} """ - - if additional_context: - message += f""" + + if additional_context: + message += f""" ## 额外说明 {additional_context} """ - - message += """ + + message += """ 请根据以上内容,设计适合社会舆论模拟的实体类型和关系类型。 **必须遵守的规则**: @@ -251,7 +433,7 @@ def _build_user_message( 4. 所有实体类型必须是现实中可以发声的主体,不能是抽象概念 5. 属性名不能使用 name、uuid、group_id 等保留字,用 full_name、org_name 等替代 """ - + return message def _validate_and_process(self, result: Dict[str, Any]) -> Dict[str, Any]: diff --git a/backend/app/services/report_agent.py b/backend/app/services/report_agent.py index 02ca5bdc2..99d0854f9 100644 --- a/backend/app/services/report_agent.py +++ b/backend/app/services/report_agent.py @@ -472,6 +472,80 @@ def to_dict(self) -> Dict[str, Any]: # ── 工具描述 ── +TOOL_DESC_INSIGHT_FORGE_EN = """\ +[Deep Insight Retrieval - Powerful Retrieval Tool] +This is our most powerful retrieval function, designed for in-depth analysis. It will: +1. Automatically decompose your question into multiple sub-questions +2. Retrieve information from the simulation graph across multiple dimensions +3. Integrate results from semantic search, entity analysis, and relationship chain tracing +4. Return the most comprehensive and in-depth retrieval content + +[Use Cases] +- Need to deeply analyze a topic +- Need to understand multiple aspects of an event +- Need to obtain rich supporting material for report sections + +[Returns] +- Relevant fact quotations (can be directly cited) +- Core entity insights +- Relationship chain analysis""" + +TOOL_DESC_PANORAMA_SEARCH_EN = """\ +[Panorama Search - Get the Full Picture] +This tool is used to get the complete picture of simulation results, especially suitable for understanding event evolution. It will: +1. Get all related nodes and relationships +2. Distinguish between currently valid facts and historical/expired facts +3. Help you understand how public opinion evolved + +[Use Cases] +- Need to understand the complete development trajectory of an event +- Need to compare public opinion changes across different stages +- Need to obtain comprehensive entity and relationship information + +[Returns] +- Currently valid facts (latest simulation results) +- Historical/expired facts (evolution records) +- All involved entities""" + +TOOL_DESC_QUICK_SEARCH_EN = """\ +[Quick Search - Fast Retrieval] +A lightweight fast retrieval tool, suitable for simple, direct information queries. + +[Use Cases] +- Need to quickly find specific information +- Need to verify a fact +- Simple information retrieval + +[Returns] +- List of facts most relevant to the query""" + +TOOL_DESC_INTERVIEW_AGENTS_EN = """\ +[Deep Interview - Real Agent Interview (Dual Platform)] +Calls the OASIS simulation environment's interview API to conduct real interviews with running simulation Agents! +This is not LLM simulation, but calls the actual interview interface to get the original responses from simulation Agents. +By default, interviews are conducted simultaneously on both Twitter and Reddit platforms for more comprehensive perspectives. + +Workflow: +1. Automatically reads persona files to understand all simulation Agents +2. Intelligently selects Agents most relevant to the interview topic (e.g., students, media, officials, etc.) +3. Automatically generates interview questions +4. Calls /api/simulation/interview/batch interface to conduct real interviews on both platforms +5. Integrates all interview results, providing multi-perspective analysis + +[Use Cases] +- Need to understand event perspectives from different roles (What do students think? What does media think? What do officials say?) +- Need to collect opinions and stances from multiple parties +- Need to obtain real answers from simulation Agents (from OASIS simulation environment) +- Want to make reports more vivid, including "interview transcripts" + +[Returns] +- Interviewed Agent identity information +- Each Agent's interview responses on both Twitter and Reddit platforms +- Key quotes (can be directly cited) +- Interview summary and viewpoint comparison + +[Important] Requires OASIS simulation environment to be running!""" + TOOL_DESC_INSIGHT_FORGE = """\ 【深度洞察检索 - 强大的检索工具】 这是我们强大的检索函数,专为深度分析设计。它会: @@ -855,6 +929,311 @@ def to_dict(self) -> Dict[str, Any]: CHAT_OBSERVATION_SUFFIX = "\n\n请简洁回答问题。" +# ═══════════════════════════════════════════════════════════════ +# English Prompt Templates +# ═══════════════════════════════════════════════════════════════ + +PLAN_SYSTEM_PROMPT_EN = """\ +You are a "Future Prediction Report" writing expert with a "God's eye view" of the simulated world — you can observe every Agent's behavior, statements, and interactions within the simulation. + +[Core Concept] +We have built a simulated world and injected specific "simulation requirements" as variables. The evolution results of the simulated world serve as predictions of what may happen in the future. You are observing not "experimental data", but "a rehearsal of the future". + +[Your Task] +Write a "Future Prediction Report" that answers: +1. Under our set conditions, what happened in the future? +2. How did various Agents (groups of people) react and act? +3. What noteworthy future trends and risks does this simulation reveal? + +[Report Positioning] +- This is a simulation-based future prediction report, revealing "if this happens, what will the future look like" +- Focus on prediction results: event trajectories, group reactions, emergent phenomena, potential risks +- Agent behaviors and statements in the simulated world are predictions of future human behavior +- This is NOT an analysis of current real-world conditions +- This is NOT a generic public opinion overview + +[Section Limit] +- Minimum 2 sections, maximum 5 sections +- No sub-sections needed, each section should contain complete content directly +- Content should be concise, focused on core prediction findings +- Section structure should be designed by you based on prediction results + +Please output the report outline in JSON format: +{ + "title": "Report Title", + "summary": "Report summary (one sentence summarizing core prediction findings)", + "sections": [ + { + "title": "Section Title", + "description": "Section content description" + } + ] +} + +Note: The sections array must have at least 2 and at most 5 elements!""" + +PLAN_USER_PROMPT_TEMPLATE_EN = """\ +[Prediction Scenario Setup] +Variable injected into the simulated world (simulation requirement): {simulation_requirement} + +[Simulated World Scale] +- Number of entities participating in simulation: {total_nodes} +- Number of relationships generated between entities: {total_edges} +- Entity type distribution: {entity_types} +- Active Agent count: {total_entities} + +[Sample Future Facts Predicted by Simulation] +{related_facts_json} + +Please examine this future rehearsal from a "God's eye view": +1. Under our set conditions, what state did the future present? +2. How did various groups (Agents) react and act? +3. What noteworthy future trends does this simulation reveal? + +Design the most appropriate report section structure based on prediction results. + +[Reminder] Report sections: minimum 2, maximum 5, content should be concise and focused on core prediction findings.""" + +SECTION_SYSTEM_PROMPT_TEMPLATE_EN = """\ +You are a "Future Prediction Report" writing expert, currently writing a section of the report. + +Report Title: {report_title} +Report Summary: {report_summary} +Prediction Scenario (Simulation Requirement): {simulation_requirement} + +Current section to write: {section_title} + +═══════════════════════════════════════════════════════════════ +[Core Concept] +═══════════════════════════════════════════════════════════════ + +The simulated world is a rehearsal of the future. We injected specific conditions (simulation requirements) into the simulated world, +and the behaviors and interactions of Agents in the simulation are predictions of future human behavior. + +Your task is to: +- Reveal what happened in the future under the set conditions +- Predict how various groups (Agents) reacted and acted +- Discover noteworthy future trends, risks, and opportunities + +Do NOT write this as an analysis of current real-world conditions +DO focus on "what will happen in the future" — simulation results ARE the predicted future + +═══════════════════════════════════════════════════════════════ +[Most Important Rules - Must Follow] +═══════════════════════════════════════════════════════════════ + +1. [Must Call Tools to Observe the Simulated World] + - You are observing the future rehearsal from a "God's eye view" + - All content must come from events and Agent behaviors in the simulated world + - Do NOT use your own knowledge to write report content + - Each section must call tools at least 3 times (maximum 5) to observe the simulated world, which represents the future + +2. [Must Quote Agents' Original Statements and Actions] + - Agent statements and behaviors are predictions of future human behavior + - Use quotation format to present these predictions in the report, for example: + > "A certain group would express: original content..." + - These quotations are the core evidence of simulation predictions + +3. [Language Consistency - Quoted Content Must Be in English] + - Tool-returned content may contain Chinese or mixed language expressions + - The report must be written entirely in English + - When quoting tool-returned Chinese or mixed-language content, you must translate it into fluent English before including it in the report + - Maintain original meaning during translation, ensuring natural and smooth expression + - This rule applies to both body text and content in quotation blocks (> format) + +4. [Faithfully Present Prediction Results] + - Report content must reflect simulation results from the simulated world representing the future + - Do not add information that does not exist in the simulation + - If information is insufficient in some aspect, state this honestly + +═══════════════════════════════════════════════════════════════ +[Format Specification - Extremely Important!] +═══════════════════════════════════════════════════════════════ + +[One Section = Minimum Content Unit] +- Each section is the minimum chunk unit of the report +- DO NOT use any Markdown headings (#, ##, ###, #### etc.) within a section +- DO NOT add section main title at the beginning of content +- Section titles are automatically added by the system, you only need to write body text +- USE **bold**, paragraph breaks, quotes, and lists to organize content, but do not use headings + +[Correct Example] +``` +This section analyzes the public opinion propagation dynamics. Through in-depth analysis of simulation data, we found... + +**Initial Explosion Phase** + +Weibo, as the first scene of public opinion, served the core function of initial information release: + +> "Weibo contributed 68% of the initial voice volume..." + +**Emotion Amplification Phase** + +The Douyin platform further amplified the event's impact: + +- Strong visual impact +- High emotional resonance +``` + +[Wrong Example] +``` +## Executive Summary <- Wrong! No headings allowed +### 1. Initial Phase <- Wrong! No ### sub-sections +#### 1.1 Detailed Analysis <- Wrong! No #### subdivisions + +This section analyzes... +``` + +═══════════════════════════════════════════════════════════════ +[Available Retrieval Tools] (Call 3-5 times per section) +═══════════════════════════════════════════════════════════════ + +{tools_description} + +[Tool Usage Suggestions - Use Different Tools, Don't Just Use One] +- insight_forge: Deep insight analysis, automatically decomposes questions and retrieves facts and relationships from multiple dimensions +- panorama_search: Wide-angle panoramic search, understand event overview, timeline, and evolution +- quick_search: Quickly verify a specific information point +- interview_agents: Interview simulation Agents, get first-person perspectives and real reactions from different roles + +═══════════════════════════════════════════════════════════════ +[Workflow] +═══════════════════════════════════════════════════════════════ + +Each reply you can only do one of the following two things (not both): + +Option A - Call a tool: +Output your thinking, then call a tool using this format: + +{{"name": "tool_name", "parameters": {{"param_name": "param_value"}}}} + +The system will execute the tool and return the result to you. You do not need to and cannot write tool results yourself. + +Option B - Output final content: +When you have obtained sufficient information through tools, output the section content starting with "Final Answer:". + +Strict prohibitions: +- Do NOT include both a tool call and Final Answer in the same reply +- Do NOT fabricate tool results (Observations), all tool results are injected by the system +- Call at most one tool per reply + +═══════════════════════════════════════════════════════════════ +[Section Content Requirements] +═══════════════════════════════════════════════════════════════ + +1. Content must be based on simulation data retrieved by tools +2. Extensively quote original text to demonstrate simulation effects +3. Use Markdown format (but NO headings): + - Use **bold text** to mark key points (instead of sub-headings) + - Use lists (- or 1.2.3.) to organize points + - Use blank lines to separate different paragraphs + - DO NOT use #, ##, ###, #### or any heading syntax +4. [Quote Format - Must Be Standalone Paragraphs] + Quotes must be standalone paragraphs, with a blank line before and after, not mixed into paragraphs: + + Correct format: + ``` + The response was considered to lack substance. + + > "The response pattern appeared rigid and slow in the fast-changing social media environment." + + This assessment reflected widespread public dissatisfaction. + ``` + + Wrong format: + ``` + The response was considered to lack substance. > "The response pattern..." This assessment reflected... + ``` +5. Maintain logical coherence with other sections +6. [Avoid Repetition] Carefully read the completed sections below, do not repeat the same information +7. [Emphasis] Do not add any headings! Use **bold** instead of sub-section headings""" + +SECTION_USER_PROMPT_TEMPLATE_EN = """\ +Completed section content (please read carefully to avoid repetition): +{previous_content} + +═══════════════════════════════════════════════════════════════ +[Current Task] Write section: {section_title} +═══════════════════════════════════════════════════════════════ + +[Important Reminders] +1. Carefully read the completed sections above to avoid repeating the same content! +2. You must call tools to get simulation data before starting +3. Please use different tools, don't just use one type +4. Report content must come from retrieval results, do not use your own knowledge + +[Format Warning - Must Follow] +- DO NOT write any headings (#, ##, ###, #### are all prohibited) +- DO NOT write "{section_title}" as the opening +- Section titles are automatically added by the system +- Write body text directly, use **bold** instead of sub-section headings + +Please begin: +1. First think (Thought) about what information this section needs +2. Then call tools (Action) to get simulation data +3. After collecting sufficient information, output Final Answer (pure body text, no headings)""" + +REACT_OBSERVATION_TEMPLATE_EN = """\ +Observation (retrieval results): + +=== Tool {tool_name} returned === +{result} + +═══════════════════════════════════════════════════════════════ +Tools called {tool_calls_count}/{max_tool_calls} times (used: {used_tools_str}){unused_hint} +- If information is sufficient: output section content starting with "Final Answer:" (must quote the above original text) +- If more information is needed: call a tool to continue retrieval +═══════════════════════════════════════════════════════════════""" + +REACT_INSUFFICIENT_TOOLS_MSG_EN = ( + "[Note] You have only called tools {tool_calls_count} times, at least {min_tool_calls} times required. " + "Please call more tools to get more simulation data, then output Final Answer. {unused_hint}" +) + +REACT_INSUFFICIENT_TOOLS_MSG_ALT_EN = ( + "Currently only {tool_calls_count} tool calls made, at least {min_tool_calls} required. " + "Please call tools to get simulation data. {unused_hint}" +) + +REACT_TOOL_LIMIT_MSG_EN = ( + "Tool call limit reached ({tool_calls_count}/{max_tool_calls}), no more tool calls allowed. " + 'Please immediately output section content starting with "Final Answer:" based on the information already obtained.' +) + +REACT_UNUSED_TOOLS_HINT_EN = "\nTip: You haven't used: {unused_list}. Consider trying different tools for multi-angle information." + +REACT_FORCE_FINAL_MSG_EN = "Tool call limit reached. Please directly output Final Answer: and generate section content." + +CHAT_SYSTEM_PROMPT_TEMPLATE_EN = """\ +You are a concise and efficient simulation prediction assistant. + +[Background] +Prediction conditions: {simulation_requirement} + +[Generated Analysis Report] +{report_content} + +[Rules] +1. Prioritize answering questions based on the above report content +2. Answer questions directly, avoid lengthy reasoning +3. Only call tools to retrieve more data when report content is insufficient to answer +4. Answers should be concise, clear, and well-organized + +[Available Tools] (Use only when needed, maximum 1-2 calls) +{tools_description} + +[Tool Call Format] + +{{"name": "tool_name", "parameters": {{"param_name": "param_value"}}}} + + +[Response Style] +- Concise and direct, no lengthy essays +- Use > format to quote key content +- Provide conclusions first, then explain reasons""" + +CHAT_OBSERVATION_SUFFIX_EN = "\n\nPlease answer the question concisely." + # ═══════════════════════════════════════════════════════════════ # ReportAgent 主类 @@ -881,27 +1260,30 @@ class ReportAgent: MAX_TOOL_CALLS_PER_CHAT = 2 def __init__( - self, + self, graph_id: str, simulation_id: str, simulation_requirement: str, llm_client: Optional[LLMClient] = None, - zep_tools: Optional[ZepToolsService] = None + zep_tools: Optional[ZepToolsService] = None, + language: str = 'en' ): """ 初始化Report Agent - + Args: graph_id: 图谱ID simulation_id: 模拟ID simulation_requirement: 模拟需求描述 llm_client: LLM客户端(可选) zep_tools: Zep工具服务(可选) + language: 语言选择,'en' 为英文,'zh' 为中文 """ self.graph_id = graph_id self.simulation_id = simulation_id self.simulation_requirement = simulation_requirement - + self.language = language + self.llm = llm_client or LLMClient() self.zep_tools = zep_tools or ZepToolsService() @@ -917,40 +1299,76 @@ def __init__( def _define_tools(self) -> Dict[str, Dict[str, Any]]: """定义可用工具""" - return { - "insight_forge": { - "name": "insight_forge", - "description": TOOL_DESC_INSIGHT_FORGE, - "parameters": { - "query": "你想深入分析的问题或话题", - "report_context": "当前报告章节的上下文(可选,有助于生成更精准的子问题)" - } - }, - "panorama_search": { - "name": "panorama_search", - "description": TOOL_DESC_PANORAMA_SEARCH, - "parameters": { - "query": "搜索查询,用于相关性排序", - "include_expired": "是否包含过期/历史内容(默认True)" - } - }, - "quick_search": { - "name": "quick_search", - "description": TOOL_DESC_QUICK_SEARCH, - "parameters": { - "query": "搜索查询字符串", - "limit": "返回结果数量(可选,默认10)" + if self.language == 'en': + return { + "insight_forge": { + "name": "insight_forge", + "description": TOOL_DESC_INSIGHT_FORGE_EN, + "parameters": { + "query": "The question or topic you want to analyze in depth", + "report_context": "Context of the current report section (optional, helps generate more precise sub-questions)" + } + }, + "panorama_search": { + "name": "panorama_search", + "description": TOOL_DESC_PANORAMA_SEARCH_EN, + "parameters": { + "query": "Search query, used for relevance ranking", + "include_expired": "Whether to include expired/historical content (default True)" + } + }, + "quick_search": { + "name": "quick_search", + "description": TOOL_DESC_QUICK_SEARCH_EN, + "parameters": { + "query": "Search query string", + "limit": "Number of results to return (optional, default 10)" + } + }, + "interview_agents": { + "name": "interview_agents", + "description": TOOL_DESC_INTERVIEW_AGENTS_EN, + "parameters": { + "interview_topic": "Interview topic or requirement description (e.g., 'Understand students' views on the dormitory formaldehyde incident')", + "max_agents": "Maximum number of Agents to interview (optional, default 5, max 10)" + } } - }, - "interview_agents": { - "name": "interview_agents", - "description": TOOL_DESC_INTERVIEW_AGENTS, - "parameters": { - "interview_topic": "采访主题或需求描述(如:'了解学生对宿舍甲醛事件的看法')", - "max_agents": "最多采访的Agent数量(可选,默认5,最大10)" + } + else: + return { + "insight_forge": { + "name": "insight_forge", + "description": TOOL_DESC_INSIGHT_FORGE, + "parameters": { + "query": "你想深入分析的问题或话题", + "report_context": "当前报告章节的上下文(可选,有助于生成更精准的子问题)" + } + }, + "panorama_search": { + "name": "panorama_search", + "description": TOOL_DESC_PANORAMA_SEARCH, + "parameters": { + "query": "搜索查询,用于相关性排序", + "include_expired": "是否包含过期/历史内容(默认True)" + } + }, + "quick_search": { + "name": "quick_search", + "description": TOOL_DESC_QUICK_SEARCH, + "parameters": { + "query": "搜索查询字符串", + "limit": "返回结果数量(可选,默认10)" + } + }, + "interview_agents": { + "name": "interview_agents", + "description": TOOL_DESC_INTERVIEW_AGENTS, + "parameters": { + "interview_topic": "采访主题或需求描述(如:'了解学生对宿舍甲醛事件的看法')", + "max_agents": "最多采访的Agent数量(可选,默认5,最大10)" + } } } - } def _execute_tool(self, tool_name: str, parameters: Dict[str, Any], report_context: str = "") -> str: """ @@ -976,8 +1394,8 @@ def _execute_tool(self, tool_name: str, parameters: Dict[str, Any], report_conte simulation_requirement=self.simulation_requirement, report_context=ctx ) - return result.to_text() - + return result.to_text(language=self.language) + elif tool_name == "panorama_search": # 广度搜索 - 获取全貌 query = parameters.get("query", "") @@ -989,8 +1407,8 @@ def _execute_tool(self, tool_name: str, parameters: Dict[str, Any], report_conte query=query, include_expired=include_expired ) - return result.to_text() - + return result.to_text(language=self.language) + elif tool_name == "quick_search": # 简单搜索 - 快速检索 query = parameters.get("query", "") @@ -1002,8 +1420,8 @@ def _execute_tool(self, tool_name: str, parameters: Dict[str, Any], report_conte query=query, limit=limit ) - return result.to_text() - + return result.to_text(language=self.language) + elif tool_name == "interview_agents": # 深度采访 - 调用真实的OASIS采访API获取模拟Agent的回答(双平台) interview_topic = parameters.get("interview_topic", parameters.get("query", "")) @@ -1017,7 +1435,7 @@ def _execute_tool(self, tool_name: str, parameters: Dict[str, Any], report_conte simulation_requirement=self.simulation_requirement, max_agents=max_agents ) - return result.to_text() + return result.to_text(language=self.language) # ========== 向后兼容的旧工具(内部重定向到新工具) ========== @@ -1125,12 +1543,18 @@ def _is_valid_tool_call(self, data: dict) -> bool: def _get_tools_description(self) -> str: """生成工具描述文本""" - desc_parts = ["可用工具:"] + if self.language == 'en': + desc_parts = ["Available tools:"] + else: + desc_parts = ["可用工具:"] for name, tool in self.tools.items(): params_desc = ", ".join([f"{k}: {v}" for k, v in tool["parameters"].items()]) desc_parts.append(f"- {name}: {tool['description']}") if params_desc: - desc_parts.append(f" 参数: {params_desc}") + if self.language == 'en': + desc_parts.append(f" Parameters: {params_desc}") + else: + desc_parts.append(f" 参数: {params_desc}") return "\n".join(desc_parts) def plan_outline( @@ -1162,8 +1586,9 @@ def plan_outline( if progress_callback: progress_callback("planning", 30, "正在生成报告大纲...") - system_prompt = PLAN_SYSTEM_PROMPT - user_prompt = PLAN_USER_PROMPT_TEMPLATE.format( + system_prompt = PLAN_SYSTEM_PROMPT_EN if self.language == 'en' else PLAN_SYSTEM_PROMPT + plan_user_template = PLAN_USER_PROMPT_TEMPLATE_EN if self.language == 'en' else PLAN_USER_PROMPT_TEMPLATE + user_prompt = plan_user_template.format( simulation_requirement=self.simulation_requirement, total_nodes=context.get('graph_statistics', {}).get('total_nodes', 0), total_edges=context.get('graph_statistics', {}).get('total_edges', 0), @@ -1192,30 +1617,42 @@ def plan_outline( content="" )) + default_title = "Simulation Analysis Report" if self.language == 'en' else "模拟分析报告" outline = ReportOutline( - title=response.get("title", "模拟分析报告"), + title=response.get("title", default_title), summary=response.get("summary", ""), sections=sections ) - + if progress_callback: progress_callback("planning", 100, "大纲规划完成") - + logger.info(f"大纲规划完成: {len(sections)} 个章节") return outline - + except Exception as e: logger.error(f"大纲规划失败: {str(e)}") # 返回默认大纲(3个章节,作为fallback) - return ReportOutline( - title="未来预测报告", - summary="基于模拟预测的未来趋势与风险分析", - sections=[ - ReportSection(title="预测场景与核心发现"), - ReportSection(title="人群行为预测分析"), - ReportSection(title="趋势展望与风险提示") - ] - ) + if self.language == 'en': + return ReportOutline( + title="Future Prediction Report", + summary="Future trend and risk analysis based on simulation predictions", + sections=[ + ReportSection(title="Prediction Scenario and Core Findings"), + ReportSection(title="Group Behavior Prediction Analysis"), + ReportSection(title="Trend Outlook and Risk Alerts") + ] + ) + else: + return ReportOutline( + title="未来预测报告", + summary="基于模拟预测的未来趋势与风险分析", + sections=[ + ReportSection(title="预测场景与核心发现"), + ReportSection(title="人群行为预测分析"), + ReportSection(title="趋势展望与风险提示") + ] + ) def _generate_section_react( self, @@ -1251,7 +1688,8 @@ def _generate_section_react( if self.report_logger: self.report_logger.log_section_start(section.title, section_index) - system_prompt = SECTION_SYSTEM_PROMPT_TEMPLATE.format( + section_sys_template = SECTION_SYSTEM_PROMPT_TEMPLATE_EN if self.language == 'en' else SECTION_SYSTEM_PROMPT_TEMPLATE + system_prompt = section_sys_template.format( report_title=outline.title, report_summary=outline.summary, simulation_requirement=self.simulation_requirement, @@ -1268,9 +1706,10 @@ def _generate_section_react( previous_parts.append(truncated) previous_content = "\n\n---\n\n".join(previous_parts) else: - previous_content = "(这是第一个章节)" - - user_prompt = SECTION_USER_PROMPT_TEMPLATE.format( + previous_content = "(This is the first section)" if self.language == 'en' else "(这是第一个章节)" + + section_user_template = SECTION_USER_PROMPT_TEMPLATE_EN if self.language == 'en' else SECTION_USER_PROMPT_TEMPLATE + user_prompt = section_user_template.format( previous_content=previous_content, section_title=section.title, ) @@ -1377,10 +1816,15 @@ def _generate_section_react( if tool_calls_count < min_tool_calls: messages.append({"role": "assistant", "content": response}) unused_tools = all_tools - used_tools - unused_hint = f"(这些工具还未使用,推荐用一下他们: {', '.join(unused_tools)})" if unused_tools else "" + if self.language == 'en': + unused_hint = f"(These tools haven't been used yet, try them: {', '.join(unused_tools)})" if unused_tools else "" + insuff_msg = REACT_INSUFFICIENT_TOOLS_MSG_EN + else: + unused_hint = f"(这些工具还未使用,推荐用一下他们: {', '.join(unused_tools)})" if unused_tools else "" + insuff_msg = REACT_INSUFFICIENT_TOOLS_MSG messages.append({ "role": "user", - "content": REACT_INSUFFICIENT_TOOLS_MSG.format( + "content": insuff_msg.format( tool_calls_count=tool_calls_count, min_tool_calls=min_tool_calls, unused_hint=unused_hint, @@ -1406,9 +1850,10 @@ def _generate_section_react( # 工具额度已耗尽 → 明确告知,要求输出 Final Answer if tool_calls_count >= self.MAX_TOOL_CALLS_PER_SECTION: messages.append({"role": "assistant", "content": response}) + tool_limit_msg = REACT_TOOL_LIMIT_MSG_EN if self.language == 'en' else REACT_TOOL_LIMIT_MSG messages.append({ "role": "user", - "content": REACT_TOOL_LIMIT_MSG.format( + "content": tool_limit_msg.format( tool_calls_count=tool_calls_count, max_tool_calls=self.MAX_TOOL_CALLS_PER_SECTION, ), @@ -1451,12 +1896,15 @@ def _generate_section_react( unused_tools = all_tools - used_tools unused_hint = "" if unused_tools and tool_calls_count < self.MAX_TOOL_CALLS_PER_SECTION: - unused_hint = REACT_UNUSED_TOOLS_HINT.format(unused_list="、".join(unused_tools)) + unused_tools_hint_tmpl = REACT_UNUSED_TOOLS_HINT_EN if self.language == 'en' else REACT_UNUSED_TOOLS_HINT + joiner = ", " if self.language == 'en' else "、" + unused_hint = unused_tools_hint_tmpl.format(unused_list=joiner.join(unused_tools)) + obs_template = REACT_OBSERVATION_TEMPLATE_EN if self.language == 'en' else REACT_OBSERVATION_TEMPLATE messages.append({"role": "assistant", "content": response}) messages.append({ "role": "user", - "content": REACT_OBSERVATION_TEMPLATE.format( + "content": obs_template.format( tool_name=call["name"], result=result, tool_calls_count=tool_calls_count, @@ -1473,11 +1921,16 @@ def _generate_section_react( if tool_calls_count < min_tool_calls: # 工具调用次数不足,推荐未用过的工具 unused_tools = all_tools - used_tools - unused_hint = f"(这些工具还未使用,推荐用一下他们: {', '.join(unused_tools)})" if unused_tools else "" + if self.language == 'en': + unused_hint = f"(These tools haven't been used yet, try them: {', '.join(unused_tools)})" if unused_tools else "" + insuff_alt_msg = REACT_INSUFFICIENT_TOOLS_MSG_ALT_EN + else: + unused_hint = f"(这些工具还未使用,推荐用一下他们: {', '.join(unused_tools)})" if unused_tools else "" + insuff_alt_msg = REACT_INSUFFICIENT_TOOLS_MSG_ALT messages.append({ "role": "user", - "content": REACT_INSUFFICIENT_TOOLS_MSG_ALT.format( + "content": insuff_alt_msg.format( tool_calls_count=tool_calls_count, min_tool_calls=min_tool_calls, unused_hint=unused_hint, @@ -1501,7 +1954,8 @@ def _generate_section_react( # 达到最大迭代次数,强制生成内容 logger.warning(f"章节 {section.title} 达到最大迭代次数,强制生成") - messages.append({"role": "user", "content": REACT_FORCE_FINAL_MSG}) + force_msg = REACT_FORCE_FINAL_MSG_EN if self.language == 'en' else REACT_FORCE_FINAL_MSG + messages.append({"role": "user", "content": force_msg}) response = self.llm.chat( messages=messages, @@ -1800,9 +2254,11 @@ def chat( except Exception as e: logger.warning(f"获取报告内容失败: {e}") - system_prompt = CHAT_SYSTEM_PROMPT_TEMPLATE.format( + chat_sys_template = CHAT_SYSTEM_PROMPT_TEMPLATE_EN if self.language == 'en' else CHAT_SYSTEM_PROMPT_TEMPLATE + no_report_text = "(No report yet)" if self.language == 'en' else "(暂无报告)" + system_prompt = chat_sys_template.format( simulation_requirement=self.simulation_requirement, - report_content=report_content if report_content else "(暂无报告)", + report_content=report_content if report_content else no_report_text, tools_description=self._get_tools_description(), ) @@ -1860,7 +2316,7 @@ def chat( observation = "\n".join([f"[{r['tool']}结果]\n{r['result']}" for r in tool_results]) messages.append({ "role": "user", - "content": observation + CHAT_OBSERVATION_SUFFIX + "content": observation + (CHAT_OBSERVATION_SUFFIX_EN if self.language == 'en' else CHAT_OBSERVATION_SUFFIX) }) # 达到最大迭代,获取最终响应 diff --git a/backend/app/services/simulation_config_generator.py b/backend/app/services/simulation_config_generator.py index cc362508b..4fec03d84 100644 --- a/backend/app/services/simulation_config_generator.py +++ b/backend/app/services/simulation_config_generator.py @@ -225,8 +225,10 @@ def __init__( self, api_key: Optional[str] = None, base_url: Optional[str] = None, - model_name: Optional[str] = None + model_name: Optional[str] = None, + language: str = 'en' ): + self.language = language self.api_key = api_key or Config.LLM_API_KEY self.base_url = base_url or Config.LLM_BASE_URL self.model_name = model_name or Config.LLM_MODEL_NAME @@ -584,8 +586,11 @@ def _generate_time_config(self, context: str, num_entities: int) -> Dict[str, An - work_hours (int数组): 工作时段 - reasoning (string): 简要说明为什么这样配置""" - system_prompt = "你是社交媒体模拟专家。返回纯JSON格式,时间配置需符合中国人作息习惯。" - + if self.language == 'en': + system_prompt = "You are a social media simulation expert. Return pure JSON format." + else: + system_prompt = "你是社交媒体模拟专家。返回纯JSON格式,时间配置需符合中国人作息习惯。" + try: return self._call_llm_with_retry(prompt, system_prompt) except Exception as e: @@ -700,8 +705,11 @@ def _generate_event_config( "reasoning": "<简要说明>" }}""" - system_prompt = "你是舆论分析专家。返回纯JSON格式。注意 poster_type 必须精确匹配可用实体类型。" - + if self.language == 'en': + system_prompt = "You are a public opinion analysis expert. Return pure JSON format. Note that poster_type must exactly match available entity types." + else: + system_prompt = "你是舆论分析专家。返回纯JSON格式。注意 poster_type 必须精确匹配可用实体类型。" + try: return self._call_llm_with_retry(prompt, system_prompt) except Exception as e: @@ -863,8 +871,11 @@ def _generate_agent_configs_batch( ] }}""" - system_prompt = "你是社交媒体行为分析专家。返回纯JSON,配置需符合中国人作息习惯。" - + if self.language == 'en': + system_prompt = "You are a social media behavior analysis expert. Return pure JSON format." + else: + system_prompt = "你是社交媒体行为分析专家。返回纯JSON,配置需符合中国人作息习惯。" + try: result = self._call_llm_with_retry(prompt, system_prompt) llm_configs = {cfg["agent_id"]: cfg for cfg in result.get("agent_configs", [])} diff --git a/backend/app/services/zep_tools.py b/backend/app/services/zep_tools.py index 384cf540f..56b13c532 100644 --- a/backend/app/services/zep_tools.py +++ b/backend/app/services/zep_tools.py @@ -41,15 +41,22 @@ def to_dict(self) -> Dict[str, Any]: "total_count": self.total_count } - def to_text(self) -> str: + def to_text(self, language: str = None) -> str: """转换为文本格式,供LLM理解""" - text_parts = [f"搜索查询: {self.query}", f"找到 {self.total_count} 条相关信息"] - - if self.facts: - text_parts.append("\n### 相关事实:") - for i, fact in enumerate(self.facts, 1): - text_parts.append(f"{i}. {fact}") - + lang = language or 'zh' + if lang == 'en': + text_parts = [f"Search query: {self.query}", f"Found {self.total_count} relevant items"] + if self.facts: + text_parts.append("\n### Relevant Facts:") + for i, fact in enumerate(self.facts, 1): + text_parts.append(f"{i}. {fact}") + else: + text_parts = [f"搜索查询: {self.query}", f"找到 {self.total_count} 条相关信息"] + if self.facts: + text_parts.append("\n### 相关事实:") + for i, fact in enumerate(self.facts, 1): + text_parts.append(f"{i}. {fact}") + return "\n".join(text_parts) @@ -71,10 +78,15 @@ def to_dict(self) -> Dict[str, Any]: "attributes": self.attributes } - def to_text(self) -> str: + def to_text(self, language: str = None) -> str: """转换为文本格式""" - entity_type = next((l for l in self.labels if l not in ["Entity", "Node"]), "未知类型") - return f"实体: {self.name} (类型: {entity_type})\n摘要: {self.summary}" + lang = language or 'zh' + if lang == 'en': + entity_type = next((l for l in self.labels if l not in ["Entity", "Node"]), "Unknown type") + return f"Entity: {self.name} (Type: {entity_type})\nSummary: {self.summary}" + else: + entity_type = next((l for l in self.labels if l not in ["Entity", "Node"]), "未知类型") + return f"实体: {self.name} (类型: {entity_type})\n摘要: {self.summary}" @dataclass @@ -108,19 +120,29 @@ def to_dict(self) -> Dict[str, Any]: "expired_at": self.expired_at } - def to_text(self, include_temporal: bool = False) -> str: + def to_text(self, include_temporal: bool = False, language: str = None) -> str: """转换为文本格式""" + lang = language or 'zh' source = self.source_node_name or self.source_node_uuid[:8] target = self.target_node_name or self.target_node_uuid[:8] - base_text = f"关系: {source} --[{self.name}]--> {target}\n事实: {self.fact}" - - if include_temporal: - valid_at = self.valid_at or "未知" - invalid_at = self.invalid_at or "至今" - base_text += f"\n时效: {valid_at} - {invalid_at}" - if self.expired_at: - base_text += f" (已过期: {self.expired_at})" - + + if lang == 'en': + base_text = f"Relationship: {source} --[{self.name}]--> {target}\nFact: {self.fact}" + if include_temporal: + valid_at = self.valid_at or "Unknown" + invalid_at = self.invalid_at or "Present" + base_text += f"\nValidity: {valid_at} - {invalid_at}" + if self.expired_at: + base_text += f" (Expired: {self.expired_at})" + else: + base_text = f"关系: {source} --[{self.name}]--> {target}\n事实: {self.fact}" + if include_temporal: + valid_at = self.valid_at or "未知" + invalid_at = self.invalid_at or "至今" + base_text += f"\n时效: {valid_at} - {invalid_at}" + if self.expired_at: + base_text += f" (已过期: {self.expired_at})" + return base_text @property @@ -167,46 +189,79 @@ def to_dict(self) -> Dict[str, Any]: "total_relationships": self.total_relationships } - def to_text(self) -> str: + def to_text(self, language: str = None) -> str: """转换为详细的文本格式,供LLM理解""" - text_parts = [ - f"## 未来预测深度分析", - f"分析问题: {self.query}", - f"预测场景: {self.simulation_requirement}", - f"\n### 预测数据统计", - f"- 相关预测事实: {self.total_facts}条", - f"- 涉及实体: {self.total_entities}个", - f"- 关系链: {self.total_relationships}条" - ] - - # 子问题 - if self.sub_queries: - text_parts.append(f"\n### 分析的子问题") - for i, sq in enumerate(self.sub_queries, 1): - text_parts.append(f"{i}. {sq}") - - # 语义搜索结果 - if self.semantic_facts: - text_parts.append(f"\n### 【关键事实】(请在报告中引用这些原文)") - for i, fact in enumerate(self.semantic_facts, 1): - text_parts.append(f"{i}. \"{fact}\"") - - # 实体洞察 - if self.entity_insights: - text_parts.append(f"\n### 【核心实体】") - for entity in self.entity_insights: - text_parts.append(f"- **{entity.get('name', '未知')}** ({entity.get('type', '实体')})") - if entity.get('summary'): - text_parts.append(f" 摘要: \"{entity.get('summary')}\"") - if entity.get('related_facts'): - text_parts.append(f" 相关事实: {len(entity.get('related_facts', []))}条") - - # 关系链 - if self.relationship_chains: - text_parts.append(f"\n### 【关系链】") - for chain in self.relationship_chains: - text_parts.append(f"- {chain}") - + lang = language or 'zh' + + if lang == 'en': + text_parts = [ + f"## Future Prediction Deep Analysis", + f"Analysis question: {self.query}", + f"Prediction scenario: {self.simulation_requirement}", + f"\n### Prediction Data Statistics", + f"- Relevant prediction facts: {self.total_facts}", + f"- Entities involved: {self.total_entities}", + f"- Relationship chains: {self.total_relationships}" + ] + + if self.sub_queries: + text_parts.append(f"\n### Analyzed Sub-questions") + for i, sq in enumerate(self.sub_queries, 1): + text_parts.append(f"{i}. {sq}") + + if self.semantic_facts: + text_parts.append(f"\n### [Key Facts] (Please cite these original texts in the report)") + for i, fact in enumerate(self.semantic_facts, 1): + text_parts.append(f'{i}. "{fact}"') + + if self.entity_insights: + text_parts.append(f"\n### [Core Entities]") + for entity in self.entity_insights: + text_parts.append(f"- **{entity.get('name', 'Unknown')}** ({entity.get('type', 'Entity')})") + if entity.get('summary'): + text_parts.append(f' Summary: "{entity.get("summary")}"') + if entity.get('related_facts'): + text_parts.append(f" Related facts: {len(entity.get('related_facts', []))}") + + if self.relationship_chains: + text_parts.append(f"\n### [Relationship Chains]") + for chain in self.relationship_chains: + text_parts.append(f"- {chain}") + else: + text_parts = [ + f"## 未来预测深度分析", + f"分析问题: {self.query}", + f"预测场景: {self.simulation_requirement}", + f"\n### 预测数据统计", + f"- 相关预测事实: {self.total_facts}条", + f"- 涉及实体: {self.total_entities}个", + f"- 关系链: {self.total_relationships}条" + ] + + if self.sub_queries: + text_parts.append(f"\n### 分析的子问题") + for i, sq in enumerate(self.sub_queries, 1): + text_parts.append(f"{i}. {sq}") + + if self.semantic_facts: + text_parts.append(f"\n### 【关键事实】(请在报告中引用这些原文)") + for i, fact in enumerate(self.semantic_facts, 1): + text_parts.append(f"{i}. \"{fact}\"") + + if self.entity_insights: + text_parts.append(f"\n### 【核心实体】") + for entity in self.entity_insights: + text_parts.append(f"- **{entity.get('name', '未知')}** ({entity.get('type', '实体')})") + if entity.get('summary'): + text_parts.append(f" 摘要: \"{entity.get('summary')}\"") + if entity.get('related_facts'): + text_parts.append(f" 相关事实: {len(entity.get('related_facts', []))}条") + + if self.relationship_chains: + text_parts.append(f"\n### 【关系链】") + for chain in self.relationship_chains: + text_parts.append(f"- {chain}") + return "\n".join(text_parts) @@ -246,37 +301,63 @@ def to_dict(self) -> Dict[str, Any]: "historical_count": self.historical_count } - def to_text(self) -> str: + def to_text(self, language: str = None) -> str: """转换为文本格式(完整版本,不截断)""" - text_parts = [ - f"## 广度搜索结果(未来全景视图)", - f"查询: {self.query}", - f"\n### 统计信息", - f"- 总节点数: {self.total_nodes}", - f"- 总边数: {self.total_edges}", - f"- 当前有效事实: {self.active_count}条", - f"- 历史/过期事实: {self.historical_count}条" - ] - - # 当前有效的事实(完整输出,不截断) - if self.active_facts: - text_parts.append(f"\n### 【当前有效事实】(模拟结果原文)") - for i, fact in enumerate(self.active_facts, 1): - text_parts.append(f"{i}. \"{fact}\"") - - # 历史/过期事实(完整输出,不截断) - if self.historical_facts: - text_parts.append(f"\n### 【历史/过期事实】(演变过程记录)") - for i, fact in enumerate(self.historical_facts, 1): - text_parts.append(f"{i}. \"{fact}\"") - - # 关键实体(完整输出,不截断) - if self.all_nodes: - text_parts.append(f"\n### 【涉及实体】") - for node in self.all_nodes: - entity_type = next((l for l in node.labels if l not in ["Entity", "Node"]), "实体") - text_parts.append(f"- **{node.name}** ({entity_type})") - + lang = language or 'zh' + + if lang == 'en': + text_parts = [ + f"## Panorama Search Results (Future Full View)", + f"Query: {self.query}", + f"\n### Statistics", + f"- Total nodes: {self.total_nodes}", + f"- Total edges: {self.total_edges}", + f"- Currently valid facts: {self.active_count}", + f"- Historical/expired facts: {self.historical_count}" + ] + + if self.active_facts: + text_parts.append(f"\n### [Currently Valid Facts] (Simulation result originals)") + for i, fact in enumerate(self.active_facts, 1): + text_parts.append(f'{i}. "{fact}"') + + if self.historical_facts: + text_parts.append(f"\n### [Historical/Expired Facts] (Evolution process records)") + for i, fact in enumerate(self.historical_facts, 1): + text_parts.append(f'{i}. "{fact}"') + + if self.all_nodes: + text_parts.append(f"\n### [Involved Entities]") + for node in self.all_nodes: + entity_type = next((l for l in node.labels if l not in ["Entity", "Node"]), "Entity") + text_parts.append(f"- **{node.name}** ({entity_type})") + else: + text_parts = [ + f"## 广度搜索结果(未来全景视图)", + f"查询: {self.query}", + f"\n### 统计信息", + f"- 总节点数: {self.total_nodes}", + f"- 总边数: {self.total_edges}", + f"- 当前有效事实: {self.active_count}条", + f"- 历史/过期事实: {self.historical_count}条" + ] + + if self.active_facts: + text_parts.append(f"\n### 【当前有效事实】(模拟结果原文)") + for i, fact in enumerate(self.active_facts, 1): + text_parts.append(f"{i}. \"{fact}\"") + + if self.historical_facts: + text_parts.append(f"\n### 【历史/过期事实】(演变过程记录)") + for i, fact in enumerate(self.historical_facts, 1): + text_parts.append(f"{i}. \"{fact}\"") + + if self.all_nodes: + text_parts.append(f"\n### 【涉及实体】") + for node in self.all_nodes: + entity_type = next((l for l in node.labels if l not in ["Entity", "Node"]), "实体") + text_parts.append(f"- **{node.name}** ({entity_type})") + return "\n".join(text_parts) @@ -300,14 +381,20 @@ def to_dict(self) -> Dict[str, Any]: "key_quotes": self.key_quotes } - def to_text(self) -> str: + def to_text(self, language: str = None) -> str: + lang = language or 'zh' text = f"**{self.agent_name}** ({self.agent_role})\n" - # 显示完整的agent_bio,不截断 - text += f"_简介: {self.agent_bio}_\n\n" + if lang == 'en': + text += f"_Bio: {self.agent_bio}_\n\n" + else: + text += f"_简介: {self.agent_bio}_\n\n" text += f"**Q:** {self.question}\n\n" text += f"**A:** {self.response}\n" if self.key_quotes: - text += "\n**关键引言:**\n" + if lang == 'en': + text += "\n**Key Quotes:**\n" + else: + text += "\n**关键引言:**\n" for quote in self.key_quotes: # 清理各种引号 clean_quote = quote.replace('\u201c', '').replace('\u201d', '').replace('"', '') @@ -371,28 +458,52 @@ def to_dict(self) -> Dict[str, Any]: "interviewed_count": self.interviewed_count } - def to_text(self) -> str: + def to_text(self, language: str = None) -> str: """转换为详细的文本格式,供LLM理解和报告引用""" - text_parts = [ - "## 深度采访报告", - f"**采访主题:** {self.interview_topic}", - f"**采访人数:** {self.interviewed_count} / {self.total_agents} 位模拟Agent", - "\n### 采访对象选择理由", - self.selection_reasoning or "(自动选择)", - "\n---", - "\n### 采访实录", - ] - - if self.interviews: - for i, interview in enumerate(self.interviews, 1): - text_parts.append(f"\n#### 采访 #{i}: {interview.agent_name}") - text_parts.append(interview.to_text()) - text_parts.append("\n---") + lang = language or 'zh' + + if lang == 'en': + text_parts = [ + "## Deep Interview Report", + f"**Interview Topic:** {self.interview_topic}", + f"**Interviewees:** {self.interviewed_count} / {self.total_agents} simulation Agents", + "\n### Interviewee Selection Reasoning", + self.selection_reasoning or "(Auto-selected)", + "\n---", + "\n### Interview Transcripts", + ] + + if self.interviews: + for i, interview in enumerate(self.interviews, 1): + text_parts.append(f"\n#### Interview #{i}: {interview.agent_name}") + text_parts.append(interview.to_text(language=lang)) + text_parts.append("\n---") + else: + text_parts.append("(No interview records)\n\n---") + + text_parts.append("\n### Interview Summary and Core Viewpoints") + text_parts.append(self.summary or "(No summary)") else: - text_parts.append("(无采访记录)\n\n---") + text_parts = [ + "## 深度采访报告", + f"**采访主题:** {self.interview_topic}", + f"**采访人数:** {self.interviewed_count} / {self.total_agents} 位模拟Agent", + "\n### 采访对象选择理由", + self.selection_reasoning or "(自动选择)", + "\n---", + "\n### 采访实录", + ] + + if self.interviews: + for i, interview in enumerate(self.interviews, 1): + text_parts.append(f"\n#### 采访 #{i}: {interview.agent_name}") + text_parts.append(interview.to_text(language=lang)) + text_parts.append("\n---") + else: + text_parts.append("(无采访记录)\n\n---") - text_parts.append("\n### 采访摘要与核心观点") - text_parts.append(self.summary or "(无摘要)") + text_parts.append("\n### 采访摘要与核心观点") + text_parts.append(self.summary or "(无摘要)") return "\n".join(text_parts) @@ -421,11 +532,12 @@ class ZepToolsService: MAX_RETRIES = 3 RETRY_DELAY = 2.0 - def __init__(self, api_key: Optional[str] = None, llm_client: Optional[LLMClient] = None): + def __init__(self, api_key: Optional[str] = None, llm_client: Optional[LLMClient] = None, language: str = 'en'): self.api_key = api_key or Config.ZEP_API_KEY + self.language = language if not self.api_key: raise ValueError("ZEP_API_KEY 未配置") - + self.client = Zep(api_key=self.api_key) # LLM客户端用于InsightForge生成子问题 self._llm_client = llm_client diff --git a/backend/app/utils/messages.py b/backend/app/utils/messages.py new file mode 100644 index 000000000..99d8565b3 --- /dev/null +++ b/backend/app/utils/messages.py @@ -0,0 +1,204 @@ +""" +Internationalization messages for API responses. +Supports English and Chinese. +""" +from flask import request + + +def get_request_language(): + """Get language from Accept-Language header, default to 'en'.""" + lang = request.headers.get('Accept-Language', 'en') + if lang.startswith('zh'): + return 'zh' + return 'en' + + +MESSAGES = { + # Project messages + 'project_not_found': { + 'en': 'Project not found: {id}', + 'zh': '项目不存在: {id}' + }, + 'project_deleted': { + 'en': 'Project deleted: {id}', + 'zh': '项目已删除: {id}' + }, + 'project_delete_failed': { + 'en': 'Project not found or delete failed: {id}', + 'zh': '项目不存在或删除失败: {id}' + }, + 'project_reset': { + 'en': 'Project reset: {id}', + 'zh': '项目已重置: {id}' + }, + 'missing_simulation_requirement': { + 'en': 'Please provide simulation_requirement', + 'zh': '请提供模拟需求描述 (simulation_requirement)' + }, + 'missing_files': { + 'en': 'Please upload at least one document file', + 'zh': '请至少上传一个文档文件' + }, + 'no_docs_processed': { + 'en': 'No documents were processed successfully. Please check file formats.', + 'zh': '没有成功处理任何文档,请检查文件格式' + }, + 'ontology_complete': { + 'en': 'Ontology generated: {entity_count} entity types, {edge_count} relation types', + 'zh': '本体生成完成: {entity_count} 个实体类型, {edge_count} 个关系类型' + }, + 'graph_build_started': { + 'en': 'Graph build task started', + 'zh': '图谱构建任务已启动' + }, + 'missing_project_id': { + 'en': 'Please provide project_id', + 'zh': '请提供 project_id' + }, + 'ontology_not_generated': { + 'en': 'Ontology not generated yet. Please call /ontology/generate first.', + 'zh': '项目尚未生成本体,请先调用 /ontology/generate' + }, + 'graph_building_in_progress': { + 'en': 'Graph is being built. Do not resubmit. To force rebuild, add force: true.', + 'zh': '图谱正在构建中,请勿重复提交。如需强制重建,请添加 force: true' + }, + 'graph_deleted': { + 'en': 'Graph deleted: {id}', + 'zh': '图谱已删除: {id}' + }, + 'task_not_found': { + 'en': 'Task not found: {id}', + 'zh': '任务不存在: {id}' + }, + # Simulation messages + 'missing_simulation_id': { + 'en': 'Please provide simulation_id', + 'zh': '请提供 simulation_id' + }, + 'simulation_not_found': { + 'en': 'Simulation not found: {id}', + 'zh': '模拟不存在: {id}' + }, + 'graph_not_built': { + 'en': 'Graph not built yet for this project', + 'zh': '项目尚未构建图谱' + }, + 'zep_not_configured': { + 'en': 'ZEP_API_KEY not configured', + 'zh': 'ZEP_API_KEY未配置' + }, + 'entity_not_found': { + 'en': 'Entity not found: {id}', + 'zh': '实体不存在: {id}' + }, + # Report messages + 'report_task_started': { + 'en': 'Report generation task started', + 'zh': '报告生成任务已启动' + }, + 'report_already_generated': { + 'en': 'Report already generated', + 'zh': '报告已生成' + }, + 'report_not_found': { + 'en': 'Report not found', + 'zh': '报告不存在' + }, + 'report_deleted': { + 'en': 'Report deleted: {id}', + 'zh': '报告已删除: {id}' + }, + 'report_generation_failed': { + 'en': 'Report generation failed', + 'zh': '报告生成失败' + }, + 'missing_requirement': { + 'en': 'Missing simulation requirement description', + 'zh': '缺少模拟需求描述' + }, + 'missing_graph_id': { + 'en': 'Missing graph ID', + 'zh': '缺少图谱ID' + }, + 'init_report_agent': { + 'en': 'Initializing Report Agent...', + 'zh': '初始化Report Agent...' + }, + 'report_task_started_long': { + 'en': 'Report generation task started. Check progress via /api/report/generate/status.', + 'zh': '报告生成任务已启动,请通过 /api/report/generate/status 查询进度' + }, + # Config messages + 'llm_not_configured': { + 'en': 'LLM_API_KEY not configured', + 'zh': 'LLM_API_KEY 未配置' + }, + 'zep_not_configured_config': { + 'en': 'ZEP_API_KEY not configured', + 'zh': 'ZEP_API_KEY 未配置' + }, + # Graph build progress messages + 'init_graph_service': { + 'en': 'Initializing graph build service...', + 'zh': '初始化图谱构建服务...' + }, + 'text_chunking': { + 'en': 'Text chunking...', + 'zh': '文本分块中...' + }, + 'graph_build_complete': { + 'en': 'Graph build complete', + 'zh': '图谱构建完成' + }, + 'text_extraction_complete': { + 'en': 'Text extraction complete, {length} characters total', + 'zh': '文本提取完成,共 {length} 字符' + }, + 'calling_llm': { + 'en': 'Calling LLM to generate ontology...', + 'zh': '调用 LLM 生成本体定义...' + }, + # Interview prompt + 'interview_prefix': { + 'en': 'Based on your persona, all past memories and actions, respond directly with text without calling any tools: ', + 'zh': '结合你的人设、所有的过往记忆与行动,不调用任何工具直接用文本回复我:' + }, + 'agent_responding': { + 'en': 'Agent responding...', + 'zh': 'Agent回复...' + }, + 'default_question': { + 'en': 'Please explain the public opinion trends', + 'zh': '请解释一下舆情走向' + }, +} + + +def msg(key, lang=None, **kwargs): + """Get a translated message by key. + + Args: + key: Message key + lang: Language code ('en' or 'zh'). If None, reads from request header. + **kwargs: Format arguments + + Returns: + Translated and formatted message string + """ + if lang is None: + try: + lang = get_request_language() + except RuntimeError: + # Outside of request context (e.g., background threads) + lang = 'en' + + message_dict = MESSAGES.get(key, {}) + template = message_dict.get(lang, message_dict.get('en', key)) + + if kwargs: + try: + return template.format(**kwargs) + except (KeyError, IndexError): + return template + return template diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 8c4fa710d..78dc84764 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,6 +11,7 @@ "axios": "^1.13.2", "d3": "^7.9.0", "vue": "^3.5.24", + "vue-i18n": "^12.0.0-alpha.3", "vue-router": "^4.6.3" }, "devDependencies": { @@ -506,6 +507,67 @@ "node": ">=18" } }, + "node_modules/@intlify/core-base": { + "version": "12.0.0-alpha.3", + "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-12.0.0-alpha.3.tgz", + "integrity": "sha512-LEvBHBUbiOOtIBkp4IIQENVC5Fg2YHsvdXN1+WRIxQ8hzHbHSBiqZ2l68B/yg8sE1a4S7dqhkaAedunShWPH+Q==", + "license": "MIT", + "dependencies": { + "@intlify/message-compiler": "12.0.0-alpha.3", + "@intlify/shared": "12.0.0-alpha.3" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@intlify/message-compiler": { + "version": "12.0.0-alpha.3", + "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-12.0.0-alpha.3.tgz", + "integrity": "sha512-mDDTN3gfYOHhBnpnlby19UHyvMaOnzdlpsIrxUfs44R/vCATfn8pMOkE8PXD2t410xkocEj3FpDcC9XC/0v4Dg==", + "license": "MIT", + "dependencies": { + "@intlify/shared": "12.0.0-alpha.3", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@intlify/shared": { + "version": "12.0.0-alpha.3", + "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-12.0.0-alpha.3.tgz", + "integrity": "sha512-ryaNYBvxQjyJUmVuBBg+HHUsmGnfxcEUPR0NCeG4/K9N2qtyFE35C80S15IN6iYFE2MGWLN7HfOSyg0MXZIc9w==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@intlify/vue-i18n-core": { + "version": "12.0.0-alpha.3", + "resolved": "https://registry.npmjs.org/@intlify/vue-i18n-core/-/vue-i18n-core-12.0.0-alpha.3.tgz", + "integrity": "sha512-YwAfTQILHN+VoK0P/Yv47GbKnEf1lhfbliyVyW3knAL1EmT8m0m3rwffXJnwyQhYw8Jpx85CpL49WkSgyi6d/g==", + "license": "MIT", + "dependencies": { + "@intlify/core-base": "12.0.0-alpha.3", + "@intlify/shared": "12.0.0-alpha.3", + "@vue/devtools-api": "^6.5.0" + }, + "engines": { + "node": ">= 16" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", @@ -1331,7 +1393,6 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -1809,7 +1870,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -1943,7 +2003,6 @@ "integrity": "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -2018,7 +2077,6 @@ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.25.tgz", "integrity": "sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==", "license": "MIT", - "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.25", "@vue/compiler-sfc": "3.5.25", @@ -2035,6 +2093,28 @@ } } }, + "node_modules/vue-i18n": { + "version": "12.0.0-alpha.3", + "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-12.0.0-alpha.3.tgz", + "integrity": "sha512-+KQgD9LJoHfGCdJh3gaLdVS/Sps1n860+6wsjyeNLWJeEofjdVH7KPjz4rAeBlTAUaIDlIjHoXQY0Lk+8B6S9w==", + "deprecated": "This version is NOT deprecated. Previous deprecation was a mistake.", + "license": "MIT", + "dependencies": { + "@intlify/core-base": "12.0.0-alpha.3", + "@intlify/shared": "12.0.0-alpha.3", + "@intlify/vue-i18n-core": "12.0.0-alpha.3", + "@vue/devtools-api": "^6.5.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, "node_modules/vue-router": { "version": "4.6.3", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.3.tgz", diff --git a/frontend/package.json b/frontend/package.json index f7e995a14..8430a6b00 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,6 +12,7 @@ "axios": "^1.13.2", "d3": "^7.9.0", "vue": "^3.5.24", + "vue-i18n": "^12.0.0-alpha.3", "vue-router": "^4.6.3" }, "devDependencies": { diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js index e2d9465b2..d9c18d6a2 100644 --- a/frontend/src/api/index.js +++ b/frontend/src/api/index.js @@ -12,6 +12,8 @@ const service = axios.create({ // 请求拦截器 service.interceptors.request.use( config => { + const locale = localStorage.getItem('mirofish-locale') || 'en' + config.headers['Accept-Language'] = locale return config }, error => { diff --git a/frontend/src/components/GraphPanel.vue b/frontend/src/components/GraphPanel.vue index 314c966e4..630aae23f 100644 --- a/frontend/src/components/GraphPanel.vue +++ b/frontend/src/components/GraphPanel.vue @@ -4,11 +4,11 @@ Graph Relationship Visualization
- -
@@ -27,7 +27,7 @@ - {{ isSimulating ? 'GraphRAG长短期记忆实时更新中' : '实时更新中...' }} + {{ isSimulating ? $t('graph.memoryUpdating') : $t('graph.updating') }} @@ -39,8 +39,8 @@ - 还有少量内容处理中,建议稍后手动刷新图谱 - diff --git a/frontend/src/components/LanguageSwitcher.vue b/frontend/src/components/LanguageSwitcher.vue new file mode 100644 index 000000000..aa381eca4 --- /dev/null +++ b/frontend/src/components/LanguageSwitcher.vue @@ -0,0 +1,74 @@ + + + + + diff --git a/frontend/src/components/Step1GraphBuild.vue b/frontend/src/components/Step1GraphBuild.vue index de33a3fd1..6c377e6b0 100644 --- a/frontend/src/components/Step1GraphBuild.vue +++ b/frontend/src/components/Step1GraphBuild.vue @@ -6,25 +6,25 @@
01 - 本体生成 + {{ $t('step1.ontology.title') }}
- 已完成 - 生成中 - 等待 + {{ $t('common.status.completed') }} + {{ $t('common.status.processing') }} + {{ $t('common.status.pending') }}

POST /api/graph/ontology/generate

- LLM分析文档内容与模拟需求,提取出现实种子,自动生成合适的本体结构 + {{ $t('step1.ontology.desc') }}

- {{ ontologyProgress.message || '正在分析文档...' }} + {{ ontologyProgress.message || $t('step1.ontology.progress') }}
@@ -110,34 +110,34 @@
02 - GraphRAG构建 + {{ $t('step1.graphBuild.title') }}
- 已完成 + {{ $t('common.status.completed') }} {{ buildProgress?.progress || 0 }}% - 等待 + {{ $t('common.status.pending') }}

POST /api/graph/build

- 基于生成的本体,将文档自动分块后调用 Zep 构建知识图谱,提取实体和关系,并形成时序记忆与社区摘要 + {{ $t('step1.graphBuild.desc') }}

{{ graphStats.nodes }} - 实体节点 + {{ $t('step1.graphBuild.entityNodes') }}
{{ graphStats.edges }} - 关系边 + {{ $t('step1.graphBuild.relationEdges') }}
{{ graphStats.types }} - SCHEMA类型 + {{ $t('step1.graphBuild.schemaTypes') }}
@@ -148,23 +148,23 @@
03 - 构建完成 + {{ $t('step1.complete.title') }}
- 进行中 + {{ $t('common.status.processing') }}

POST /api/simulation/create

-

图谱构建已完成,请进入下一步进行模拟环境搭建

+

{{ $t('step1.complete.desc') }}

@@ -211,7 +211,7 @@ const creatingSimulation = ref(false) // 进入环境搭建 - 创建 simulation 并跳转 const handleEnterEnvSetup = async () => { if (!props.projectData?.project_id || !props.projectData?.graph_id) { - console.error('缺少项目或图谱信息') + console.error('Missing project or graph info') return } @@ -232,12 +232,12 @@ const handleEnterEnvSetup = async () => { params: { simulationId: res.data.simulation_id } }) } else { - console.error('创建模拟失败:', res.error) - alert('创建模拟失败: ' + (res.error || '未知错误')) + console.error('Failed to create simulation:', res.error) + alert('Failed to create simulation: ' + (res.error || 'Unknown error')) } } catch (err) { - console.error('创建模拟异常:', err) - alert('创建模拟异常: ' + err.message) + console.error('Simulation creation error:', err) + alert('Simulation creation error: ' + err.message) } finally { creatingSimulation.value = false } diff --git a/frontend/src/components/Step2EnvSetup.vue b/frontend/src/components/Step2EnvSetup.vue index eae776aaf..66200ce50 100644 --- a/frontend/src/components/Step2EnvSetup.vue +++ b/frontend/src/components/Step2EnvSetup.vue @@ -6,18 +6,18 @@
01 - 模拟实例初始化 + {{ $t('step2.init.title') }}
- 已完成 - 初始化 + {{ $t('common.status.completed') }} + {{ $t('common.status.initializing') }}

POST /api/simulation/create

- 新建simulation实例,拉取模拟世界参数模版 + {{ $t('step2.init.desc') }}

@@ -35,7 +35,7 @@
Task ID - {{ taskId || '异步任务已完成' }} + {{ taskId || $t('step2.init.taskDone') }}
@@ -46,41 +46,41 @@
02 - 生成 Agent 人设 + {{ $t('step2.profiles.title') }}
- 已完成 + {{ $t('common.status.completed') }} {{ prepareProgress }}% - 等待 + {{ $t('common.status.pending') }}

POST /api/simulation/prepare

- 结合上下文,自动调用工具从知识图谱梳理实体与关系,初始化模拟个体,并基于现实种子赋予他们独特的行为与记忆 + {{ $t('step2.profiles.desc') }}

{{ profiles.length }} - 当前Agent数 + {{ $t('step2.profiles.currentAgents') }}
{{ expectedTotal || '-' }} - 预期Agent总数 + {{ $t('step2.profiles.expectedTotal') }}
{{ totalTopicsCount }} - 现实种子当前关联话题数 + {{ $t('step2.profiles.relatedTopics') }}
- 已生成的 Agent 人设 + {{ $t('step2.profiles.generatedProfiles') }}
@{{ profile.name || `agent_${idx}` }}
- {{ profile.profession || '未知职业' }} + {{ profile.profession || $t('step2.profiles.unknownJob') }}
-

{{ profile.bio || '暂无简介' }}

+

{{ profile.bio || $t('step2.profiles.noBio') }}

03 - 生成双平台模拟配置 + {{ $t('step2.config.title') }}
- 已完成 - 生成中 - 等待 + {{ $t('common.status.completed') }} + {{ $t('common.status.processing') }} + {{ $t('common.status.pending') }}

POST /api/simulation/prepare

- LLM 根据模拟需求与现实种子,智能设置世界时间流速、推荐算法、每个个体的活跃时间段、发言频率、事件触发等参数 + {{ $t('step2.config.desc') }}

@@ -139,40 +139,40 @@
- 模拟时长 - {{ simulationConfig.time_config?.total_simulation_hours || '-' }} 小时 + {{ $t('step2.config.simDuration') }} + {{ simulationConfig.time_config?.total_simulation_hours || '-' }} {{ $t('step2.config.hours') }}
- 每轮时长 - {{ simulationConfig.time_config?.minutes_per_round || '-' }} 分钟 + {{ $t('step2.config.roundDuration') }} + {{ simulationConfig.time_config?.minutes_per_round || '-' }} {{ $t('step2.config.minutes') }}
- 总轮次 - {{ Math.floor((simulationConfig.time_config?.total_simulation_hours * 60 / simulationConfig.time_config?.minutes_per_round)) || '-' }} 轮 + {{ $t('step2.config.totalRounds') }} + {{ Math.floor((simulationConfig.time_config?.total_simulation_hours * 60 / simulationConfig.time_config?.minutes_per_round)) || '-' }} {{ $t('step2.config.rounds') }}
- 每小时活跃 + {{ $t('step2.config.activePerHour') }} {{ simulationConfig.time_config?.agents_per_hour_min }}-{{ simulationConfig.time_config?.agents_per_hour_max }}
- 高峰时段 + {{ $t('step2.config.peakHours') }} {{ simulationConfig.time_config?.peak_hours?.join(':00, ') }}:00 ×{{ simulationConfig.time_config?.peak_activity_multiplier }}
- 工作时段 + {{ $t('step2.config.workHours') }} {{ simulationConfig.time_config?.work_hours?.[0] }}:00-{{ simulationConfig.time_config?.work_hours?.slice(-1)[0] }}:00 ×{{ simulationConfig.time_config?.work_activity_multiplier }}
- 早间时段 + {{ $t('step2.config.morningHours') }} {{ simulationConfig.time_config?.morning_hours?.[0] }}:00-{{ simulationConfig.time_config?.morning_hours?.slice(-1)[0] }}:00 ×{{ simulationConfig.time_config?.morning_activity_multiplier }}
- 低谷时段 + {{ $t('step2.config.offPeakHours') }} {{ simulationConfig.time_config?.off_peak_hours?.[0] }}:00-{{ simulationConfig.time_config?.off_peak_hours?.slice(-1)[0] }}:00 ×{{ simulationConfig.time_config?.off_peak_activity_multiplier }}
@@ -182,8 +182,8 @@
- Agent 配置 - {{ simulationConfig.agent_configs?.length || 0 }} 个 + {{ $t('step2.config.agentConfig') }} + {{ simulationConfig.agent_configs?.length || 0 }} {{ $t('step2.config.count') }}
- 活跃时段 + {{ $t('step2.config.activeHours') }}
- 发帖/时 + {{ $t('step2.config.postsPerHour') }} {{ agent.posts_per_hour }}
- 评论/时 + {{ $t('step2.config.commentsPerHour') }} {{ agent.comments_per_hour }}
- 响应延迟 + {{ $t('step2.config.responseDelay') }} {{ agent.response_delay_min }}-{{ agent.response_delay_max }}min
- 活跃度 + {{ $t('step2.config.activityLevel') }} {{ (agent.activity_level * 100).toFixed(0) }}%
- 情感倾向 + {{ $t('step2.config.sentimentBias') }} {{ agent.sentiment_bias > 0 ? '+' : '' }}{{ agent.sentiment_bias?.toFixed(1) }}
- 影响力 + {{ $t('step2.config.influence') }} {{ agent.influence_weight?.toFixed(1) }}
@@ -267,32 +267,32 @@
- 推荐算法配置 + {{ $t('step2.config.recommendAlgo') }}
- 平台 1:广场 / 信息流 + {{ $t('step2.config.platform1') }}
- 时效权重 + {{ $t('step2.config.recencyWeight') }} {{ simulationConfig.twitter_config.recency_weight }}
- 热度权重 + {{ $t('step2.config.popularityWeight') }} {{ simulationConfig.twitter_config.popularity_weight }}
- 相关性权重 + {{ $t('step2.config.relevanceWeight') }} {{ simulationConfig.twitter_config.relevance_weight }}
- 病毒阈值 + {{ $t('step2.config.viralThreshold') }} {{ simulationConfig.twitter_config.viral_threshold }}
- 回音室强度 + {{ $t('step2.config.echoChamber') }} {{ simulationConfig.twitter_config.echo_chamber_strength }}
@@ -303,23 +303,23 @@
- 时效权重 + {{ $t('step2.config.recencyWeight') }} {{ simulationConfig.reddit_config.recency_weight }}
- 热度权重 + {{ $t('step2.config.popularityWeight') }} {{ simulationConfig.reddit_config.popularity_weight }}
- 相关性权重 + {{ $t('step2.config.relevanceWeight') }} {{ simulationConfig.reddit_config.relevance_weight }}
- 病毒阈值 + {{ $t('step2.config.viralThreshold') }} {{ simulationConfig.reddit_config.viral_threshold }}
- 回音室强度 + {{ $t('step2.config.echoChamber') }} {{ simulationConfig.reddit_config.echo_chamber_strength }}
@@ -351,19 +351,19 @@
04 - 初始激活编排 + {{ $t('step2.orchestration.title') }}
- 已完成 - 编排中 - 等待 + {{ $t('common.status.completed') }} + {{ $t('step2.orchestration.orchestrating') }} + {{ $t('common.status.pending') }}

POST /api/simulation/prepare

- 基于叙事方向,自动生成初始激活事件与热点话题,引导模拟世界的初始状态 + {{ $t('step2.orchestration.desc') }}

@@ -380,14 +380,14 @@ - 叙事引导方向 + {{ $t('step2.orchestration.narrative') }}

{{ simulationConfig.event_config.narrative_direction }}

- 初始热点话题 + {{ $t('step2.orchestration.hotTopics') }}
# {{ topic }} @@ -397,7 +397,7 @@
- 初始激活序列 ({{ simulationConfig.event_config.initial_posts.length }}) + {{ $t('step2.orchestration.activationSeq') }} ({{ simulationConfig.event_config.initial_posts.length }})
@@ -423,29 +423,29 @@
05 - 准备完成 + {{ $t('step2.ready.title') }}
- 进行中 - 等待 + {{ $t('common.status.processing') }} + {{ $t('common.status.pending') }}

POST /api/simulation/start

-

模拟环境已准备完成,可以开始运行模拟

+

{{ $t('step2.ready.desc') }}

- 模拟轮数设定 - MiroFish 自动规划推演现实 {{ simulationConfig?.time_config?.total_simulation_hours || '-' }} 小时,每轮代表现实 {{ simulationConfig?.time_config?.minutes_per_round || '-' }} 分钟时间流逝 + {{ $t('step2.ready.roundsTitle') }} +
@@ -454,10 +454,10 @@
{{ customMaxRounds }} - + {{ $t('step2.config.rounds') }}
- 若Agent规模为100:预计耗时约 {{ Math.round(customMaxRounds * 0.6) }} 分钟 + {{ $t('step2.ready.estimatedTime', { time: Math.round(customMaxRounds * 0.6) }) }}
@@ -478,7 +478,7 @@ :class="{ active: customMaxRounds === 40 }" @click="customMaxRounds = 40" :style="{ position: 'absolute', left: `calc(${(40 - 10) / (autoGeneratedRounds - 10) * 100}% - 30px)` }" - >40 (推荐) + >40 ({{ $t('step2.ready.recommended') }}) {{ autoGeneratedRounds }}
@@ -488,7 +488,7 @@
{{ autoGeneratedRounds }} - + {{ $t('step2.config.rounds') }}
@@ -497,11 +497,11 @@ - 若Agent规模为100:预计耗时 {{ Math.round(autoGeneratedRounds * 0.6) }} 分钟 + {{ $t('step2.ready.estimatedTime', { time: Math.round(autoGeneratedRounds * 0.6) }) }}
-

若首次运行,强烈建议切换至‘自定义模式’减少模拟轮数,以便快速预览效果并降低报错风险 ➝

+

{{ $t('step2.ready.firstRunTip') }}

@@ -514,14 +514,14 @@ class="action-btn secondary" @click="$emit('go-back')" > - ← 返回图谱构建 + ← {{ $t('step2.ready.backToGraph') }}
diff --git a/frontend/src/components/Step3Simulation.vue b/frontend/src/components/Step3Simulation.vue index 74d0e1e7b..fcaa2ccbd 100644 --- a/frontend/src/components/Step3Simulation.vue +++ b/frontend/src/components/Step3Simulation.vue @@ -97,7 +97,7 @@ @click="handleNextStep" > - {{ isGeneratingReport ? '启动中...' : '开始生成结果报告' }} + {{ isGeneratingReport ? $t('step3.starting') : $t('step3.generateReport') }}
@@ -288,6 +288,7 @@ diff --git a/frontend/src/views/MainView.vue b/frontend/src/views/MainView.vue index 6ff299112..5963ba435 100644 --- a/frontend/src/views/MainView.vue +++ b/frontend/src/views/MainView.vue @@ -15,12 +15,13 @@ :class="{ active: viewMode === mode }" @click="viewMode = mode" > - {{ { graph: '图谱', split: '双栏', workbench: '工作台' }[mode] }} + {{ $t('common.viewModes.' + mode) }}
+
Step {{ currentStep }}/5 {{ stepNames[currentStep - 1] }} @@ -77,21 +78,24 @@ diff --git a/frontend/src/views/SimulationRunView.vue b/frontend/src/views/SimulationRunView.vue index 14ebc5f9d..ab0e87953 100644 --- a/frontend/src/views/SimulationRunView.vue +++ b/frontend/src/views/SimulationRunView.vue @@ -15,7 +15,7 @@ :class="{ active: viewMode === mode }" @click="viewMode = mode" > - {{ { graph: '图谱', split: '双栏', workbench: '工作台' }[mode] }} + {{ $t('common.viewModes.' + mode) }}
@@ -23,7 +23,7 @@
Step 3/5 - 开始模拟 + {{ $t('common.stepNames.simulation') }}
@@ -69,6 +69,7 @@