diff --git a/.gitignore b/.gitignore index 55d3ef197..01a7716e9 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ .DS_Store Thumbs.db -# 环境变量(保护敏感信息) +# Biến môi trường (bảo vệ thông tin nhạy cảm) .env .env.local .env.*.local @@ -36,7 +36,7 @@ yarn-error.log* *.swp *.swo -# 测试 +# Kiểm thử .pytest_cache/ .coverage htmlcov/ @@ -45,16 +45,16 @@ htmlcov/ .cursor/ .claude/ -# 文档与测试程序 +# Tài liệu và chương trình kiểm thử mydoc/ mytest/ -# 日志文件 +# File log backend/logs/ *.log -# 上传文件 +# File tải lên backend/uploads/ -# Docker 数据 +# Dữ liệu Docker data/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index e65646860..8918b2ce4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,29 +1,29 @@ FROM python:3.11 -# 安装 Node.js (满足 >=18)及必要工具 +# Cài đặt Node.js (đáp ứng >=18) và các công cụ cần thiết RUN apt-get update \ && apt-get install -y --no-install-recommends nodejs npm \ && rm -rf /var/lib/apt/lists/* -# 从 uv 官方镜像复制 uv +# Sao chép uv từ image chính thức của uv COPY --from=ghcr.io/astral-sh/uv:0.9.26 /uv /uvx /bin/ WORKDIR /app -# 先复制依赖描述文件以利用缓存 +# Sao chép trước file mô tả dependency để tận dụng cache COPY package.json package-lock.json ./ COPY frontend/package.json frontend/package-lock.json ./frontend/ COPY backend/pyproject.toml backend/uv.lock ./backend/ -# 安装依赖(Node + Python) +# Cài đặt dependency (Node + Python) RUN npm ci \ && npm ci --prefix frontend \ && cd backend && uv sync --frozen -# 复制项目源码 +# Sao chép mã nguồn dự án COPY . . EXPOSE 3000 5001 -# 同时启动前后端(开发模式) +# Khởi chạy đồng thời frontend và backend (chế độ phát triển) CMD ["npm", "run", "dev"] \ No newline at end of file diff --git a/backend/app/__init__.py b/backend/app/__init__.py index aba624bba..904b9bfde 100644 --- a/backend/app/__init__.py +++ b/backend/app/__init__.py @@ -1,12 +1,12 @@ """ -MiroFish Backend - Flask应用工厂 +MiroFish Backend - Flask application factory """ import os import warnings -# 抑制 multiprocessing resource_tracker 的警告(来自第三方库如 transformers) -# 需要在所有其他导入之前设置 +# Ẩn cảnh báo từ multiprocessing resource_tracker (đến từ thư viện bên thứ ba như transformers) +# Cần đặt trước mọi import khác warnings.filterwarnings("ignore", message=".*resource_tracker.*") from flask import Flask, request @@ -17,64 +17,64 @@ def create_app(config_class=Config): - """Flask应用工厂函数""" + """Hàm factory để khởi tạo Flask app""" app = Flask(__name__) app.config.from_object(config_class) - # 设置JSON编码:确保中文直接显示(而不是 \uXXXX 格式) - # Flask >= 2.3 使用 app.json.ensure_ascii,旧版本使用 JSON_AS_ASCII 配置 + # Thiết lập JSON encoding: đảm bảo tiếng Trung hiển thị trực tiếp (không ở dạng \uXXXX) + # Flask >= 2.3 dùng app.json.ensure_ascii, phiên bản cũ dùng JSON_AS_ASCII if hasattr(app, 'json') and hasattr(app.json, 'ensure_ascii'): app.json.ensure_ascii = False - # 设置日志 + # Thiết lập logger logger = setup_logger('mirofish') - # 只在 reloader 子进程中打印启动信息(避免 debug 模式下打印两次) + # Chỉ in log khởi động ở tiến trình con của reloader (tránh in 2 lần khi debug) is_reloader_process = os.environ.get('WERKZEUG_RUN_MAIN') == 'true' debug_mode = app.config.get('DEBUG', False) should_log_startup = not debug_mode or is_reloader_process if should_log_startup: logger.info("=" * 50) - logger.info("MiroFish Backend 启动中...") + logger.info("MiroFish Backend is starting...") logger.info("=" * 50) - # 启用CORS + # Bật CORS CORS(app, resources={r"/api/*": {"origins": "*"}}) - # 注册模拟进程清理函数(确保服务器关闭时终止所有模拟进程) + # Đăng ký hàm dọn tiến trình mô phỏng (đảm bảo dừng toàn bộ khi server tắt) from .services.simulation_runner import SimulationRunner SimulationRunner.register_cleanup() if should_log_startup: - logger.info("已注册模拟进程清理函数") + logger.info("Simulation process cleanup hook registered") - # 请求日志中间件 + # Middleware log request @app.before_request def log_request(): logger = get_logger('mirofish.request') - logger.debug(f"请求: {request.method} {request.path}") + logger.debug(f"Request: {request.method} {request.path}") if request.content_type and 'json' in request.content_type: - logger.debug(f"请求体: {request.get_json(silent=True)}") + logger.debug(f"Request body: {request.get_json(silent=True)}") @app.after_request def log_response(response): logger = get_logger('mirofish.request') - logger.debug(f"响应: {response.status_code}") + logger.debug(f"Response: {response.status_code}") return response - # 注册蓝图 + # Đăng ký blueprint from .api import graph_bp, simulation_bp, report_bp app.register_blueprint(graph_bp, url_prefix='/api/graph') app.register_blueprint(simulation_bp, url_prefix='/api/simulation') app.register_blueprint(report_bp, url_prefix='/api/report') - # 健康检查 + # Health check @app.route('/health') def health(): return {'status': 'ok', 'service': 'MiroFish Backend'} if should_log_startup: - logger.info("MiroFish Backend 启动完成") + logger.info("MiroFish Backend started successfully") return app diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py index ffda743a3..dc64a92d7 100644 --- a/backend/app/api/__init__.py +++ b/backend/app/api/__init__.py @@ -1,5 +1,5 @@ """ -API路由模块 +Mô-đun tuyến API """ from flask import Blueprint diff --git a/backend/app/api/graph.py b/backend/app/api/graph.py index 12ff1ba2d..cb7f0dffd 100644 --- a/backend/app/api/graph.py +++ b/backend/app/api/graph.py @@ -1,6 +1,6 @@ """ -图谱相关API路由 -采用项目上下文机制,服务端持久化状态 +API routes liên quan đến Graph +Sử dụng cơ chế project context, trạng thái được persist phía server """ import os @@ -18,12 +18,12 @@ from ..models.task import TaskManager, TaskStatus from ..models.project import ProjectManager, ProjectStatus -# 获取日志器 +# Logger logger = get_logger('mirofish.api') def allowed_file(filename: str) -> bool: - """检查文件扩展名是否允许""" + """Kiểm tra phần mở rộng file có được cho phép hay không""" if not filename or '.' not in filename: return False ext = os.path.splitext(filename)[1].lower().lstrip('.') @@ -35,14 +35,14 @@ def allowed_file(filename: str) -> bool: @graph_bp.route('/project/', methods=['GET']) def get_project(project_id: str): """ - 获取项目详情 + Lấy chi tiết project """ project = ProjectManager.get_project(project_id) if not project: return jsonify({ "success": False, - "error": f"项目不存在: {project_id}" + "error": f"Project does not exist: {project_id}" }), 404 return jsonify({ @@ -54,7 +54,7 @@ def get_project(project_id: str): @graph_bp.route('/project/list', methods=['GET']) def list_projects(): """ - 列出所有项目 + Liệt kê tất cả project """ limit = request.args.get('limit', 50, type=int) projects = ProjectManager.list_projects(limit=limit) @@ -69,36 +69,36 @@ def list_projects(): @graph_bp.route('/project/', methods=['DELETE']) def delete_project(project_id: str): """ - 删除项目 + Xóa project """ success = ProjectManager.delete_project(project_id) if not success: return jsonify({ "success": False, - "error": f"项目不存在或删除失败: {project_id}" + "error": f"Project does not exist or delete failed: {project_id}" }), 404 return jsonify({ "success": True, - "message": f"项目已删除: {project_id}" + "message": f"Project deleted: {project_id}" }) @graph_bp.route('/project//reset', methods=['POST']) def reset_project(project_id: str): """ - 重置项目状态(用于重新构建图谱) + Reset trạng thái project (dùng để rebuild graph) """ project = ProjectManager.get_project(project_id) if not project: return jsonify({ "success": False, - "error": f"项目不存在: {project_id}" + "error": f"Project does not exist: {project_id}" }), 404 - # 重置到本体已生成状态 + # Reset về trạng thái ontology_generated nếu đã có ontology, ngược lại về created if project.ontology: project.status = ProjectStatus.ONTOLOGY_GENERATED else: @@ -111,27 +111,27 @@ def reset_project(project_id: str): return jsonify({ "success": True, - "message": f"项目已重置: {project_id}", + "message": f"Project reset successfully: {project_id}", "data": project.to_dict() }) -# ============== 接口1:上传文件并生成本体 ============== +# ============== API 1: Upload file và generate ontology ============== @graph_bp.route('/ontology/generate', methods=['POST']) def generate_ontology(): """ - 接口1:上传文件,分析生成本体定义 + API 1: Upload file, phân tích và generate định nghĩa ontology - 请求方式:multipart/form-data + Request type: multipart/form-data - 参数: - files: 上传的文件(PDF/MD/TXT),可多个 - simulation_requirement: 模拟需求描述(必填) - project_name: 项目名称(可选) - additional_context: 额外说明(可选) + Parameters: + files: File upload (PDF/MD/TXT), có thể nhiều file + simulation_requirement: Mô tả yêu cầu simulation (bắt buộc) + project_name: Tên project (tùy chọn) + additional_context: Thông tin bổ sung (tùy chọn) - 返回: + Response: { "success": true, "data": { @@ -147,42 +147,42 @@ def generate_ontology(): } """ try: - logger.info("=== 开始生成本体定义 ===") + logger.info("=== Start generating ontology definition ===") - # 获取参数 + # Get parameters simulation_requirement = request.form.get('simulation_requirement', '') project_name = request.form.get('project_name', 'Unnamed Project') additional_context = request.form.get('additional_context', '') - logger.debug(f"项目名称: {project_name}") - logger.debug(f"模拟需求: {simulation_requirement[:100]}...") + logger.debug(f"Project name: {project_name}") + logger.debug(f"Simulation requirement: {simulation_requirement[:100]}...") if not simulation_requirement: return jsonify({ "success": False, - "error": "请提供模拟需求描述 (simulation_requirement)" + "error": "Please provide simulation requirement description (simulation_requirement)" }), 400 - # 获取上传的文件 + # Get uploaded files uploaded_files = request.files.getlist('files') if not uploaded_files or all(not f.filename for f in uploaded_files): return jsonify({ "success": False, - "error": "请至少上传一个文档文件" + "error": "Please upload at least one document file" }), 400 - # 创建项目 + # Create project project = ProjectManager.create_project(name=project_name) project.simulation_requirement = simulation_requirement - logger.info(f"创建项目: {project.project_id}") + logger.info(f"Project created: {project.project_id}") - # 保存文件并提取文本 + # Save and extract text document_texts = [] all_text = "" for file in uploaded_files: if file and file.filename and allowed_file(file.filename): - # 保存文件到项目目录 + # Save file into project directory file_info = ProjectManager.save_file_to_project( project.project_id, file, @@ -193,7 +193,7 @@ def generate_ontology(): "size": file_info["size"] }) - # 提取文本 + # Extract text text = FileParser.extract_text(file_info["path"]) text = TextProcessor.preprocess_text(text) document_texts.append(text) @@ -203,16 +203,16 @@ def generate_ontology(): ProjectManager.delete_project(project.project_id) return jsonify({ "success": False, - "error": "没有成功处理任何文档,请检查文件格式" + "error": "No documents were successfully processed. Please check the file formats." }), 400 - # 保存提取的文本 + # Save extracted text to project project.total_text_length = len(all_text) ProjectManager.save_extracted_text(project.project_id, all_text) - logger.info(f"文本提取完成,共 {len(all_text)} 字符") + logger.info(f"Text extraction completed, total {len(all_text)} characters") - # 生成本体 - logger.info("调用 LLM 生成本体定义...") + # Generate ontology definition using LLM + logger.info("Calling LLM to generate ontology definition...") generator = OntologyGenerator() ontology = generator.generate( document_texts=document_texts, @@ -220,10 +220,10 @@ def generate_ontology(): additional_context=additional_context if additional_context else None ) - # 保存本体到项目 + # Save ontology to project entity_count = len(ontology.get("entity_types", [])) edge_count = len(ontology.get("edge_types", [])) - logger.info(f"本体生成完成: {entity_count} 个实体类型, {edge_count} 个关系类型") + logger.info(f"Ontology generation completed: {entity_count} entity types, {edge_count} edge types") project.ontology = { "entity_types": ontology.get("entity_types", []), @@ -232,7 +232,7 @@ def generate_ontology(): project.analysis_summary = ontology.get("analysis_summary", "") project.status = ProjectStatus.ONTOLOGY_GENERATED ProjectManager.save_project(project) - logger.info(f"=== 本体生成完成 === 项目ID: {project.project_id}") + logger.info(f"=== Ontology generation completed === Project ID: {project.project_id}") return jsonify({ "success": True, @@ -254,140 +254,140 @@ def generate_ontology(): }), 500 -# ============== 接口2:构建图谱 ============== +# ============== API 2: Build graph ============== @graph_bp.route('/build', methods=['POST']) def build_graph(): """ - 接口2:根据project_id构建图谱 + API 2: Build graph dựa trên project_id - 请求(JSON): + Request (JSON): { - "project_id": "proj_xxxx", // 必填,来自接口1 - "graph_name": "图谱名称", // 可选 - "chunk_size": 500, // 可选,默认500 - "chunk_overlap": 50 // 可选,默认50 + "project_id": "proj_xxxx", // bắt buộc, từ API 1 + "graph_name": "Graph name", // tùy chọn + "chunk_size": 500, // tùy chọn, mặc định 500 + "chunk_overlap": 50 // tùy chọn, mặc định 50 } - 返回: + Response: { "success": true, "data": { "project_id": "proj_xxxx", "task_id": "task_xxxx", - "message": "图谱构建任务已启动" + "message": "Graph build task started" } } """ try: - logger.info("=== 开始构建图谱 ===") + logger.info("=== Start building graph ===") - # 检查配置 + # Kiểm tra cấu hình errors = [] if not Config.ZEP_API_KEY: - errors.append("ZEP_API_KEY未配置") + errors.append("ZEP_API_KEY not configured") if errors: - logger.error(f"配置错误: {errors}") + logger.error(f"Configuration error: {errors}") return jsonify({ "success": False, - "error": "配置错误: " + "; ".join(errors) + "error": "Configuration error: " + "; ".join(errors) }), 500 - # 解析请求 + # Parse request data = request.get_json() or {} project_id = data.get('project_id') - logger.debug(f"请求参数: project_id={project_id}") + logger.debug(f"Request parameters: project_id={project_id}") if not project_id: return jsonify({ "success": False, - "error": "请提供 project_id" + "error": "Please provide project_id" }), 400 - # 获取项目 + # Lấy project project = ProjectManager.get_project(project_id) if not project: return jsonify({ "success": False, - "error": f"项目不存在: {project_id}" + "error": f"Project does not exist: {project_id}" }), 404 - # 检查项目状态 - force = data.get('force', False) # 强制重新构建 + # Kiểm tra trạng thái project + force = data.get('force', False) # force rebuild if project.status == ProjectStatus.CREATED: return jsonify({ "success": False, - "error": "项目尚未生成本体,请先调用 /ontology/generate" + "error": "Ontology has not been generated. Please call /ontology/generate first" }), 400 if project.status == ProjectStatus.GRAPH_BUILDING and not force: return jsonify({ "success": False, - "error": "图谱正在构建中,请勿重复提交。如需强制重建,请添加 force: true", + "error": "Graph is currently building. Do not submit again. Use force: true to rebuild", "task_id": project.graph_build_task_id }), 400 - # 如果强制重建,重置状态 + # Nếu force rebuild thì reset trạng thái if force and project.status in [ProjectStatus.GRAPH_BUILDING, ProjectStatus.FAILED, ProjectStatus.GRAPH_COMPLETED]: project.status = ProjectStatus.ONTOLOGY_GENERATED project.graph_id = None project.graph_build_task_id = None project.error = None - # 获取配置 + # Lấy cấu hình graph_name = data.get('graph_name', project.name or 'MiroFish Graph') chunk_size = data.get('chunk_size', project.chunk_size or Config.DEFAULT_CHUNK_SIZE) chunk_overlap = data.get('chunk_overlap', project.chunk_overlap or Config.DEFAULT_CHUNK_OVERLAP) - # 更新项目配置 + # Cập nhật cấu hình project project.chunk_size = chunk_size project.chunk_overlap = chunk_overlap - # 获取提取的文本 + # Lấy text đã extract text = ProjectManager.get_extracted_text(project_id) if not text: return jsonify({ "success": False, - "error": "未找到提取的文本内容" + "error": "Extracted text not found" }), 400 - # 获取本体 + # Lấy ontology ontology = project.ontology if not ontology: return jsonify({ "success": False, - "error": "未找到本体定义" + "error": "Ontology definition not found" }), 400 - # 创建异步任务 + # Tạo async task task_manager = TaskManager() - task_id = task_manager.create_task(f"构建图谱: {graph_name}") - logger.info(f"创建图谱构建任务: task_id={task_id}, project_id={project_id}") + task_id = task_manager.create_task(f"Build graph: {graph_name}") + logger.info(f"Graph build task created: task_id={task_id}, project_id={project_id}") - # 更新项目状态 + # Cập nhật trạng thái project project.status = ProjectStatus.GRAPH_BUILDING project.graph_build_task_id = task_id ProjectManager.save_project(project) - # 启动后台任务 + # Khởi động background task def build_task(): build_logger = get_logger('mirofish.build') try: - build_logger.info(f"[{task_id}] 开始构建图谱...") + build_logger.info(f"[{task_id}] Start building graph...") task_manager.update_task( task_id, status=TaskStatus.PROCESSING, - message="初始化图谱构建服务..." + message="Initializing graph builder service..." ) - # 创建图谱构建服务 + # Tạo graph builder service builder = GraphBuilderService(api_key=Config.ZEP_API_KEY) - # 分块 + # Chunk text task_manager.update_task( task_id, - message="文本分块中...", + message="Splitting text into chunks...", progress=5 ) chunks = TextProcessor.split_text( @@ -397,29 +397,29 @@ def build_task(): ) total_chunks = len(chunks) - # 创建图谱 + # Tạo graph task_manager.update_task( task_id, - message="创建Zep图谱...", + message="Creating Zep graph...", progress=10 ) graph_id = builder.create_graph(name=graph_name) - # 更新项目的graph_id + # Cập nhật graph_id của project project.graph_id = graph_id ProjectManager.save_project(project) - # 设置本体 + # Thiết lập ontology task_manager.update_task( task_id, - message="设置本体定义...", + message="Setting ontology definition...", progress=15 ) builder.set_ontology(graph_id, ontology) - # 添加文本(progress_callback 签名是 (msg, progress_ratio)) + # Callback cập nhật progress khi add text def add_progress_callback(msg, progress_ratio): - progress = 15 + int(progress_ratio * 40) # 15% - 55% + progress = 15 + int(progress_ratio * 40) task_manager.update_task( task_id, message=msg, @@ -428,7 +428,7 @@ def add_progress_callback(msg, progress_ratio): task_manager.update_task( task_id, - message=f"开始添加 {total_chunks} 个文本块...", + message=f"Adding {total_chunks} text chunks...", progress=15 ) @@ -439,15 +439,15 @@ def add_progress_callback(msg, progress_ratio): progress_callback=add_progress_callback ) - # 等待Zep处理完成(查询每个episode的processed状态) + # Chờ Zep xử lý xong task_manager.update_task( task_id, - message="等待Zep处理数据...", + message="Waiting for Zep to process data...", progress=55 ) def wait_progress_callback(msg, progress_ratio): - progress = 55 + int(progress_ratio * 35) # 55% - 90% + progress = 55 + int(progress_ratio * 35) task_manager.update_task( task_id, message=msg, @@ -456,27 +456,27 @@ def wait_progress_callback(msg, progress_ratio): builder._wait_for_episodes(episode_uuids, wait_progress_callback) - # 获取图谱数据 + # Lấy graph data task_manager.update_task( task_id, - message="获取图谱数据...", + message="Fetching graph data...", progress=95 ) graph_data = builder.get_graph_data(graph_id) - # 更新项目状态 + # Cập nhật trạng thái project project.status = ProjectStatus.GRAPH_COMPLETED ProjectManager.save_project(project) node_count = graph_data.get("node_count", 0) edge_count = graph_data.get("edge_count", 0) - build_logger.info(f"[{task_id}] 图谱构建完成: graph_id={graph_id}, 节点={node_count}, 边={edge_count}") + build_logger.info(f"[{task_id}] Graph build completed: graph_id={graph_id}, nodes={node_count}, edges={edge_count}") - # 完成 + # Hoàn thành task task_manager.update_task( task_id, status=TaskStatus.COMPLETED, - message="图谱构建完成", + message="Graph build completed", progress=100, result={ "project_id": project_id, @@ -488,8 +488,8 @@ def wait_progress_callback(msg, progress_ratio): ) except Exception as e: - # 更新项目状态为失败 - build_logger.error(f"[{task_id}] 图谱构建失败: {str(e)}") + # Cập nhật trạng thái project là failed + build_logger.error(f"[{task_id}] Graph build failed: {str(e)}") build_logger.debug(traceback.format_exc()) project.status = ProjectStatus.FAILED @@ -499,11 +499,11 @@ def wait_progress_callback(msg, progress_ratio): task_manager.update_task( task_id, status=TaskStatus.FAILED, - message=f"构建失败: {str(e)}", + message=f"Build failed: {str(e)}", error=traceback.format_exc() ) - # 启动后台线程 + # Chạy background thread thread = threading.Thread(target=build_task, daemon=True) thread.start() @@ -512,7 +512,7 @@ def wait_progress_callback(msg, progress_ratio): "data": { "project_id": project_id, "task_id": task_id, - "message": "图谱构建任务已启动,请通过 /task/{task_id} 查询进度" + "message": "Graph build task started. Check progress via /task/{task_id}" } }) @@ -524,19 +524,19 @@ def wait_progress_callback(msg, progress_ratio): }), 500 -# ============== 任务查询接口 ============== +# ============== Task query APIs ============== @graph_bp.route('/task/', methods=['GET']) def get_task(task_id: str): """ - 查询任务状态 + Truy vấn trạng thái task """ task = TaskManager().get_task(task_id) if not task: return jsonify({ "success": False, - "error": f"任务不存在: {task_id}" + "error": f"Task does not exist: {task_id}" }), 404 return jsonify({ @@ -548,7 +548,7 @@ def get_task(task_id: str): @graph_bp.route('/tasks', methods=['GET']) def list_tasks(): """ - 列出所有任务 + Liệt kê tất cả tasks """ tasks = TaskManager().list_tasks() @@ -559,18 +559,18 @@ def list_tasks(): }) -# ============== 图谱数据接口 ============== +# ============== Graph data APIs ============== @graph_bp.route('/data/', methods=['GET']) def get_graph_data(graph_id: str): """ - 获取图谱数据(节点和边) + Lấy dữ liệu graph (nodes và edges) """ try: if not Config.ZEP_API_KEY: return jsonify({ "success": False, - "error": "ZEP_API_KEY未配置" + "error": "ZEP_API_KEY not configured" }), 500 builder = GraphBuilderService(api_key=Config.ZEP_API_KEY) @@ -592,13 +592,13 @@ def get_graph_data(graph_id: str): @graph_bp.route('/delete/', methods=['DELETE']) def delete_graph(graph_id: str): """ - 删除Zep图谱 + Xóa Zep graph """ try: if not Config.ZEP_API_KEY: return jsonify({ "success": False, - "error": "ZEP_API_KEY未配置" + "error": "ZEP_API_KEY not configured" }), 500 builder = GraphBuilderService(api_key=Config.ZEP_API_KEY) @@ -606,7 +606,7 @@ def delete_graph(graph_id: str): return jsonify({ "success": True, - "message": f"图谱已删除: {graph_id}" + "message": f"Graph deleted: {graph_id}" }) except Exception as e: diff --git a/backend/app/api/report.py b/backend/app/api/report.py index e05c73c39..1111d2814 100644 --- a/backend/app/api/report.py +++ b/backend/app/api/report.py @@ -1,6 +1,6 @@ """ -Report API路由 -提供模拟报告生成、获取、对话等接口 +Report API routes +Cung cấp các API cho việc generate báo cáo simulation, lấy báo cáo, và chat """ import os @@ -19,30 +19,30 @@ logger = get_logger('mirofish.api.report') -# ============== 报告生成接口 ============== +# ============== Report generation API ============== @report_bp.route('/generate', methods=['POST']) def generate_report(): """ - 生成模拟分析报告(异步任务) + Generate báo cáo phân tích simulation (async task) - 这是一个耗时操作,接口会立即返回task_id, - 使用 GET /api/report/generate/status 查询进度 + Đây là thao tác tốn thời gian, API sẽ trả về task_id ngay lập tức, + dùng GET /api/report/generate/status để kiểm tra progress - 请求(JSON): + Request (JSON): { - "simulation_id": "sim_xxxx", // 必填,模拟ID - "force_regenerate": false // 可选,强制重新生成 + "simulation_id": "sim_xxxx", // bắt buộc + "force_regenerate": false // optional, force regenerate } - 返回: + Response: { "success": true, "data": { "simulation_id": "sim_xxxx", "task_id": "task_xxxx", "status": "generating", - "message": "报告生成任务已启动" + "message": "Report generation task started" } } """ @@ -53,22 +53,22 @@ def generate_report(): if not simulation_id: return jsonify({ "success": False, - "error": "请提供 simulation_id" + "error": "Please provide simulation_id" }), 400 force_regenerate = data.get('force_regenerate', False) - # 获取模拟信息 + # Lấy thông tin simulation manager = SimulationManager() state = manager.get_simulation(simulation_id) if not state: return jsonify({ "success": False, - "error": f"模拟不存在: {simulation_id}" + "error": f"Simulation does not exist: {simulation_id}" }), 404 - # 检查是否已有报告 + # Kiểm tra đã có report chưa if not force_regenerate: existing_report = ReportManager.get_report_by_simulation(simulation_id) if existing_report and existing_report.status == ReportStatus.COMPLETED: @@ -78,38 +78,38 @@ def generate_report(): "simulation_id": simulation_id, "report_id": existing_report.report_id, "status": "completed", - "message": "报告已存在", + "message": "Report already exists", "already_generated": True } }) - # 获取项目信息 + # Lấy thông tin project project = ProjectManager.get_project(state.project_id) if not project: return jsonify({ "success": False, - "error": f"项目不存在: {state.project_id}" + "error": f"Project does not exist: {state.project_id}" }), 404 graph_id = state.graph_id or project.graph_id if not graph_id: return jsonify({ "success": False, - "error": "缺少图谱ID,请确保已构建图谱" + "error": "Missing graph_id, please ensure the graph has been built" }), 400 simulation_requirement = project.simulation_requirement if not simulation_requirement: return jsonify({ "success": False, - "error": "缺少模拟需求描述" + "error": "Missing simulation requirement description" }), 400 - # 提前生成 report_id,以便立即返回给前端 + # Tạo report_id trước để có thể trả về ngay cho frontend import uuid report_id = f"report_{uuid.uuid4().hex[:12]}" - # 创建异步任务 + # Tạo async task task_manager = TaskManager() task_id = task_manager.create_task( task_type="report_generate", @@ -120,24 +120,24 @@ def generate_report(): } ) - # 定义后台任务 + # Định nghĩa background task def run_generate(): try: task_manager.update_task( task_id, status=TaskStatus.PROCESSING, progress=0, - message="初始化Report Agent..." + message="Initializing Report Agent..." ) - # 创建Report Agent + # Tạo Report Agent agent = ReportAgent( graph_id=graph_id, simulation_id=simulation_id, simulation_requirement=simulation_requirement ) - # 进度回调 + # Callback cập nhật progress def progress_callback(stage, progress, message): task_manager.update_task( task_id, @@ -145,13 +145,13 @@ def progress_callback(stage, progress, message): message=f"[{stage}] {message}" ) - # 生成报告(传入预先生成的 report_id) + # Generate report report = agent.generate_report( progress_callback=progress_callback, report_id=report_id ) - # 保存报告 + # Lưu report ReportManager.save_report(report) if report.status == ReportStatus.COMPLETED: @@ -164,13 +164,13 @@ 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 "Report generation failed") except Exception as e: - logger.error(f"报告生成失败: {str(e)}") + logger.error(f"Report generation failed: {str(e)}") task_manager.fail_task(task_id, str(e)) - # 启动后台线程 + # Chạy background thread thread = threading.Thread(target=run_generate, daemon=True) thread.start() @@ -181,13 +181,13 @@ def progress_callback(stage, progress, message): "report_id": report_id, "task_id": task_id, "status": "generating", - "message": "报告生成任务已启动,请通过 /api/report/generate/status 查询进度", + "message": "Report generation task started. Use /api/report/generate/status to check progress", "already_generated": False } }) except Exception as e: - logger.error(f"启动报告生成任务失败: {str(e)}") + logger.error(f"Failed to start report generation task: {str(e)}") return jsonify({ "success": False, "error": str(e), @@ -198,15 +198,15 @@ def progress_callback(stage, progress, message): @report_bp.route('/generate/status', methods=['POST']) def get_generate_status(): """ - 查询报告生成任务进度 + Truy vấn progress của task generate report - 请求(JSON): + Request (JSON): { - "task_id": "task_xxxx", // 可选,generate返回的task_id - "simulation_id": "sim_xxxx" // 可选,模拟ID + "task_id": "task_xxxx", // optional, task_id trả về từ generate + "simulation_id": "sim_xxxx" // optional } - 返回: + Response: { "success": true, "data": { @@ -223,7 +223,7 @@ def get_generate_status(): task_id = data.get('task_id') simulation_id = data.get('simulation_id') - # 如果提供了simulation_id,先检查是否已有完成的报告 + # Nếu có simulation_id, kiểm tra xem report đã hoàn thành chưa if simulation_id: existing_report = ReportManager.get_report_by_simulation(simulation_id) if existing_report and existing_report.status == ReportStatus.COMPLETED: @@ -234,7 +234,7 @@ def get_generate_status(): "report_id": existing_report.report_id, "status": "completed", "progress": 100, - "message": "报告已生成", + "message": "Report generated", "already_completed": True } }) @@ -242,7 +242,7 @@ def get_generate_status(): if not task_id: return jsonify({ "success": False, - "error": "请提供 task_id 或 simulation_id" + "error": "Please provide task_id or simulation_id" }), 400 task_manager = TaskManager() @@ -251,7 +251,7 @@ def get_generate_status(): if not task: return jsonify({ "success": False, - "error": f"任务不存在: {task_id}" + "error": f"Task does not exist: {task_id}" }), 404 return jsonify({ @@ -260,21 +260,21 @@ def get_generate_status(): }) except Exception as e: - logger.error(f"查询任务状态失败: {str(e)}") + logger.error(f"Failed to query task status: {str(e)}") return jsonify({ "success": False, "error": str(e) }), 500 -# ============== 报告获取接口 ============== +# ============== Report retrieval APIs ============== @report_bp.route('/', methods=['GET']) def get_report(report_id: str): """ - 获取报告详情 + Lấy chi tiết report - 返回: + Response: { "success": true, "data": { @@ -294,7 +294,7 @@ def get_report(report_id: str): if not report: return jsonify({ "success": False, - "error": f"报告不存在: {report_id}" + "error": f"Report does not exist: {report_id}" }), 404 return jsonify({ @@ -303,7 +303,7 @@ def get_report(report_id: str): }) except Exception as e: - logger.error(f"获取报告失败: {str(e)}") + logger.error(f"Failed to retrieve report: {str(e)}") return jsonify({ "success": False, "error": str(e), @@ -314,9 +314,9 @@ def get_report(report_id: str): @report_bp.route('/by-simulation/', methods=['GET']) def get_report_by_simulation(simulation_id: str): """ - 根据模拟ID获取报告 + Lấy report theo simulation_id - 返回: + Response: { "success": true, "data": { @@ -331,7 +331,7 @@ def get_report_by_simulation(simulation_id: str): if not report: return jsonify({ "success": False, - "error": f"该模拟暂无报告: {simulation_id}", + "error": f"No report found for this simulation: {simulation_id}", "has_report": False }), 404 @@ -342,7 +342,7 @@ def get_report_by_simulation(simulation_id: str): }) except Exception as e: - logger.error(f"获取报告失败: {str(e)}") + logger.error(f"Failed to retrieve report: {str(e)}") return jsonify({ "success": False, "error": str(e), @@ -353,13 +353,13 @@ def get_report_by_simulation(simulation_id: str): @report_bp.route('/list', methods=['GET']) def list_reports(): """ - 列出所有报告 + Liệt kê tất cả reports - Query参数: - simulation_id: 按模拟ID过滤(可选) - limit: 返回数量限制(默认50) + Query parameters: + simulation_id: Filter theo simulation_id (optional) + limit: Giới hạn số lượng trả về (default 50) - 返回: + Response: { "success": true, "data": [...], @@ -382,7 +382,7 @@ def list_reports(): }) except Exception as e: - logger.error(f"列出报告失败: {str(e)}") + logger.error(f"Failed to list reports: {str(e)}") return jsonify({ "success": False, "error": str(e), @@ -393,9 +393,9 @@ def list_reports(): @report_bp.route('//download', methods=['GET']) def download_report(report_id: str): """ - 下载报告(Markdown格式) + Download report (Markdown format) - 返回Markdown文件 + Trả về file Markdown """ try: report = ReportManager.get_report(report_id) @@ -403,13 +403,13 @@ def download_report(report_id: str): if not report: return jsonify({ "success": False, - "error": f"报告不存在: {report_id}" + "error": f"Report does not exist: {report_id}" }), 404 md_path = ReportManager._get_report_markdown_path(report_id) if not os.path.exists(md_path): - # 如果MD文件不存在,生成一个临时文件 + # Nếu file MD không tồn tại, tạo file tạm import tempfile with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f: f.write(report.markdown_content) @@ -428,7 +428,7 @@ def download_report(report_id: str): ) except Exception as e: - logger.error(f"下载报告失败: {str(e)}") + logger.error(f"Failed to download report: {str(e)}") return jsonify({ "success": False, "error": str(e), @@ -438,23 +438,23 @@ def download_report(report_id: str): @report_bp.route('/', methods=['DELETE']) def delete_report(report_id: str): - """删除报告""" + """Xóa report""" try: success = ReportManager.delete_report(report_id) if not success: return jsonify({ "success": False, - "error": f"报告不存在: {report_id}" + "error": f"Report does not exist: {report_id}" }), 404 return jsonify({ "success": True, - "message": f"报告已删除: {report_id}" + "message": f"Report deleted: {report_id}" }) except Exception as e: - logger.error(f"删除报告失败: {str(e)}") + logger.error(f"Failed to delete report: {str(e)}") return jsonify({ "success": False, "error": str(e), @@ -462,32 +462,32 @@ def delete_report(report_id: str): }), 500 -# ============== Report Agent对话接口 ============== +# ============== Report Agent chat API ============== @report_bp.route('/chat', methods=['POST']) def chat_with_report_agent(): """ - 与Report Agent对话 + Chat với Report Agent - Report Agent可以在对话中自主调用检索工具来回答问题 + Report Agent có thể tự động gọi các retrieval tool trong quá trình trả lời - 请求(JSON): + Request (JSON): { - "simulation_id": "sim_xxxx", // 必填,模拟ID - "message": "请解释一下舆情走向", // 必填,用户消息 - "chat_history": [ // 可选,对话历史 + "simulation_id": "sim_xxxx", // bắt buộc + "message": "Please explain the public opinion trend", // bắt buộc + "chat_history": [ // optional {"role": "user", "content": "..."}, {"role": "assistant", "content": "..."} ] } - 返回: + Response: { "success": true, "data": { - "response": "Agent回复...", - "tool_calls": [调用的工具列表], - "sources": [信息来源] + "response": "Agent reply...", + "tool_calls": [list of called tools], + "sources": [information sources] } } """ @@ -501,42 +501,42 @@ def chat_with_report_agent(): if not simulation_id: return jsonify({ "success": False, - "error": "请提供 simulation_id" + "error": "Please provide simulation_id" }), 400 if not message: return jsonify({ "success": False, - "error": "请提供 message" + "error": "Please provide message" }), 400 - # 获取模拟和项目信息 + # Lấy thông tin simulation và project manager = SimulationManager() state = manager.get_simulation(simulation_id) if not state: return jsonify({ "success": False, - "error": f"模拟不存在: {simulation_id}" + "error": f"Simulation does not exist: {simulation_id}" }), 404 project = ProjectManager.get_project(state.project_id) if not project: return jsonify({ "success": False, - "error": f"项目不存在: {state.project_id}" + "error": f"Project does not exist: {state.project_id}" }), 404 graph_id = state.graph_id or project.graph_id if not graph_id: return jsonify({ "success": False, - "error": "缺少图谱ID" + "error": "Missing graph_id" }), 400 simulation_requirement = project.simulation_requirement or "" - # 创建Agent并进行对话 + # Tạo Agent và chat agent = ReportAgent( graph_id=graph_id, simulation_id=simulation_id, @@ -551,7 +551,7 @@ def chat_with_report_agent(): }) except Exception as e: - logger.error(f"对话失败: {str(e)}") + logger.error(f"Chat failed: {str(e)}") return jsonify({ "success": False, "error": str(e), @@ -559,22 +559,22 @@ def chat_with_report_agent(): }), 500 -# ============== 报告进度与分章节接口 ============== +# ============== Report progress & sections APIs ============== @report_bp.route('//progress', methods=['GET']) def get_report_progress(report_id: str): """ - 获取报告生成进度(实时) + Lấy progress generate report (real-time) - 返回: + Response: { "success": true, "data": { "status": "generating", "progress": 45, - "message": "正在生成章节: 关键发现", - "current_section": "关键发现", - "completed_sections": ["执行摘要", "模拟背景"], + "message": "Generating section: Key Findings", + "current_section": "Key Findings", + "completed_sections": ["Executive Summary", "Simulation Background"], "updated_at": "2025-12-09T..." } } @@ -585,7 +585,7 @@ def get_report_progress(report_id: str): if not progress: return jsonify({ "success": False, - "error": f"报告不存在或进度信息不可用: {report_id}" + "error": f"Report does not exist or progress unavailable: {report_id}" }), 404 return jsonify({ @@ -594,7 +594,7 @@ def get_report_progress(report_id: str): }) except Exception as e: - logger.error(f"获取报告进度失败: {str(e)}") + logger.error(f"Failed to retrieve report progress: {str(e)}") return jsonify({ "success": False, "error": str(e), @@ -605,11 +605,12 @@ def get_report_progress(report_id: str): @report_bp.route('//sections', methods=['GET']) def get_report_sections(report_id: str): """ - 获取已生成的章节列表(分章节输出) + Lấy danh sách các section đã generate (output theo từng section) - 前端可以轮询此接口获取已生成的章节内容,无需等待整个报告完成 + Frontend có thể poll API này để lấy section đã generate + mà không cần chờ toàn bộ report hoàn thành - 返回: + Response: { "success": true, "data": { @@ -618,9 +619,8 @@ def get_report_sections(report_id: str): { "filename": "section_01.md", "section_index": 1, - "content": "## 执行摘要\\n\\n..." - }, - ... + "content": "## Executive Summary\\n\\n..." + } ], "total_sections": 3, "is_complete": false @@ -630,7 +630,7 @@ def get_report_sections(report_id: str): try: sections = ReportManager.get_generated_sections(report_id) - # 获取报告状态 + # Lấy trạng thái report report = ReportManager.get_report(report_id) is_complete = report is not None and report.status == ReportStatus.COMPLETED @@ -645,7 +645,7 @@ def get_report_sections(report_id: str): }) except Exception as e: - logger.error(f"获取章节列表失败: {str(e)}") + logger.error(f"Failed to retrieve section list: {str(e)}") return jsonify({ "success": False, "error": str(e), @@ -656,14 +656,14 @@ def get_report_sections(report_id: str): @report_bp.route('//section/', methods=['GET']) def get_single_section(report_id: str, section_index: int): """ - 获取单个章节内容 + Lấy nội dung một section - 返回: + Response: { "success": true, "data": { "filename": "section_01.md", - "content": "## 执行摘要\\n\\n..." + "content": "## Executive Summary\\n\\n..." } } """ @@ -673,7 +673,7 @@ def get_single_section(report_id: str, section_index: int): if not os.path.exists(section_path): return jsonify({ "success": False, - "error": f"章节不存在: section_{section_index:02d}.md" + "error": f"Section does not exist: section_{section_index:02d}.md" }), 404 with open(section_path, 'r', encoding='utf-8') as f: @@ -689,7 +689,7 @@ def get_single_section(report_id: str, section_index: int): }) except Exception as e: - logger.error(f"获取章节内容失败: {str(e)}") + logger.error(f"Failed to retrieve section content: {str(e)}") return jsonify({ "success": False, "error": str(e), @@ -748,22 +748,22 @@ def check_report_status(simulation_id: str): }), 500 -# ============== Agent 日志接口 ============== +# ============== API nhật ký Agent ============== @report_bp.route('//agent-log', methods=['GET']) def get_agent_log(report_id: str): """ - 获取 Report Agent 的详细执行日志 + Lấy nhật ký thực thi chi tiết của Report Agent - 实时获取报告生成过程中的每一步动作,包括: - - 报告开始、规划开始/完成 - - 每个章节的开始、工具调用、LLM响应、完成 - - 报告完成或失败 + Lấy theo thời gian thực từng bước trong quá trình tạo báo cáo, bao gồm: + - Bắt đầu báo cáo, bắt đầu / hoàn thành planning + - Bắt đầu từng section, tool call, LLM response, hoàn thành + - Báo cáo hoàn thành hoặc thất bại - Query参数: - from_line: 从第几行开始读取(可选,默认0,用于增量获取) + Query parameters: + from_line: đọc từ dòng nào (optional, mặc định 0, dùng cho incremental fetch) - 返回: + Response: { "success": true, "data": { @@ -774,7 +774,7 @@ def get_agent_log(report_id: str): "report_id": "report_xxxx", "action": "tool_call", "stage": "generating", - "section_title": "执行摘要", + "section_title": "Executive Summary", "section_index": 1, "details": { "tool_name": "insight_forge", @@ -801,7 +801,7 @@ def get_agent_log(report_id: str): }) except Exception as e: - logger.error(f"获取Agent日志失败: {str(e)}") + logger.error(f"Failed to get Agent log: {str(e)}") return jsonify({ "success": False, "error": str(e), @@ -812,9 +812,9 @@ def get_agent_log(report_id: str): @report_bp.route('//agent-log/stream', methods=['GET']) def stream_agent_log(report_id: str): """ - 获取完整的 Agent 日志(一次性获取全部) + Lấy toàn bộ Agent logs (fetch toàn bộ một lần) - 返回: + Response: { "success": true, "data": { @@ -835,7 +835,7 @@ def stream_agent_log(report_id: str): }) except Exception as e: - logger.error(f"获取Agent日志失败: {str(e)}") + logger.error(f"Failed to get Agent log: {str(e)}") return jsonify({ "success": False, "error": str(e), @@ -843,27 +843,29 @@ def stream_agent_log(report_id: str): }), 500 -# ============== 控制台日志接口 ============== +# ============== API nhật ký Console ============== @report_bp.route('//console-log', methods=['GET']) def get_console_log(report_id: str): """ - 获取 Report Agent 的控制台输出日志 + Lấy console output logs của Report Agent - 实时获取报告生成过程中的控制台输出(INFO、WARNING等), - 这与 agent-log 接口返回的结构化 JSON 日志不同, - 是纯文本格式的控制台风格日志。 + Lấy theo thời gian thực các console output trong quá trình tạo báo cáo + (INFO, WARNING, ...). - Query参数: - from_line: 从第几行开始读取(可选,默认0,用于增量获取) + Khác với API agent-log trả về structured JSON logs, + API này trả về console-style log dạng text thuần. - 返回: + Query parameters: + from_line: đọc từ dòng nào (optional, mặc định 0, dùng cho incremental fetch) + + Response: { "success": true, "data": { "logs": [ - "[19:46:14] INFO: 搜索完成: 找到 15 条相关事实", - "[19:46:14] INFO: 图谱搜索: graph_id=xxx, query=...", + "[19:46:14] INFO: Search completed: found 15 relevant facts", + "[19:46:14] INFO: Graph search: graph_id=xxx, query=...", ... ], "total_lines": 100, @@ -883,7 +885,7 @@ def get_console_log(report_id: str): }) except Exception as e: - logger.error(f"获取控制台日志失败: {str(e)}") + logger.error(f"Failed to get console log: {str(e)}") return jsonify({ "success": False, "error": str(e), @@ -894,9 +896,9 @@ def get_console_log(report_id: str): @report_bp.route('//console-log/stream', methods=['GET']) def stream_console_log(report_id: str): """ - 获取完整的控制台日志(一次性获取全部) + Lấy toàn bộ console logs (fetch toàn bộ một lần) - 返回: + Response: { "success": true, "data": { @@ -917,7 +919,7 @@ def stream_console_log(report_id: str): }) except Exception as e: - logger.error(f"获取控制台日志失败: {str(e)}") + logger.error(f"Failed to get console log: {str(e)}") return jsonify({ "success": False, "error": str(e), @@ -925,17 +927,17 @@ def stream_console_log(report_id: str): }), 500 -# ============== 工具调用接口(供调试使用)============== +# ============== API gọi Tool (dùng cho debugging) ============== @report_bp.route('/tools/search', methods=['POST']) def search_graph_tool(): """ - 图谱搜索工具接口(供调试使用) + API công cụ graph search (dùng cho debugging) - 请求(JSON): + Request (JSON): { "graph_id": "mirofish_xxxx", - "query": "搜索查询", + "query": "search query", "limit": 10 } """ @@ -949,7 +951,7 @@ def search_graph_tool(): if not graph_id or not query: return jsonify({ "success": False, - "error": "请提供 graph_id 和 query" + "error": "Please provide graph_id and query" }), 400 from ..services.zep_tools import ZepToolsService @@ -967,7 +969,7 @@ def search_graph_tool(): }) except Exception as e: - logger.error(f"图谱搜索失败: {str(e)}") + logger.error(f"Graph search failed: {str(e)}") return jsonify({ "success": False, "error": str(e), @@ -978,9 +980,9 @@ def search_graph_tool(): @report_bp.route('/tools/statistics', methods=['POST']) def get_graph_statistics_tool(): """ - 图谱统计工具接口(供调试使用) + API công cụ thống kê graph (dùng cho debugging) - 请求(JSON): + Request (JSON): { "graph_id": "mirofish_xxxx" } @@ -993,7 +995,7 @@ def get_graph_statistics_tool(): if not graph_id: return jsonify({ "success": False, - "error": "请提供 graph_id" + "error": "Please provide graph_id" }), 400 from ..services.zep_tools import ZepToolsService @@ -1007,7 +1009,7 @@ def get_graph_statistics_tool(): }) except Exception as e: - logger.error(f"获取图谱统计失败: {str(e)}") + logger.error(f"Failed to get graph statistics: {str(e)}") return jsonify({ "success": False, "error": str(e), diff --git a/backend/app/api/simulation.py b/backend/app/api/simulation.py index 3a0f68168..48b6e99f3 100644 --- a/backend/app/api/simulation.py +++ b/backend/app/api/simulation.py @@ -1,6 +1,6 @@ """ -模拟相关API路由 -Step2: Zep实体读取与过滤、OASIS模拟准备与运行(全程自动化) +API route liên quan đến mô phỏng. +Step2: Đọc và lọc thực thể Zep, chuẩn bị và chạy mô phỏng OASIS (tự động hoàn toàn). """ import os @@ -19,54 +19,55 @@ logger = get_logger('mirofish.api.simulation') -# Interview prompt 优化前缀 -# 添加此前缀可以避免Agent调用工具,直接用文本回复 -INTERVIEW_PROMPT_PREFIX = "结合你的人设、所有的过往记忆与行动,不调用任何工具直接用文本回复我:" +# Tiền tố tối ưu cho Interview prompt +# Thêm tiền tố này để tránh Agent gọi công cụ, trả lời trực tiếp bằng văn bản +INTERVIEW_PROMPT_PREFIX = "Based on your persona, all past memories and actions, reply directly in plain text without calling any tools:" def optimize_interview_prompt(prompt: str) -> str: """ - 优化Interview提问,添加前缀避免Agent调用工具 + Tối ưu câu hỏi Interview, thêm tiền tố để tránh Agent gọi công cụ. Args: - prompt: 原始提问 + prompt: Câu hỏi gốc Returns: - 优化后的提问 + Câu hỏi đã tối ưu """ if not prompt: return prompt - # 避免重复添加前缀 + # Tránh thêm tiền tố lặp lại if prompt.startswith(INTERVIEW_PROMPT_PREFIX): return prompt return f"{INTERVIEW_PROMPT_PREFIX}{prompt}" -# ============== 实体读取接口 ============== +# ============== API đọc thực thể ============== @simulation_bp.route('/entities/', methods=['GET']) def get_graph_entities(graph_id: str): """ - 获取图谱中的所有实体(已过滤) + Lấy toàn bộ thực thể trong đồ thị (đã lọc). - 只返回符合预定义实体类型的节点(Labels不只是Entity的节点) + Chỉ trả về các node phù hợp với loại thực thể đã định nghĩa trước. + (Label không chỉ là node Entity) - Query参数: - entity_types: 逗号分隔的实体类型列表(可选,用于进一步过滤) - enrich: 是否获取相关边信息(默认true) + Tham số query: + entity_types: Danh sách loại thực thể phân tách bằng dấu phẩy (tùy chọn, dùng để lọc thêm) + enrich: Có lấy thông tin cạnh liên quan hay không (mặc định true) """ try: if not Config.ZEP_API_KEY: return jsonify({ "success": False, - "error": "ZEP_API_KEY未配置" + "error": "ZEP_API_KEY is not configured" }), 500 entity_types_str = request.args.get('entity_types', '') entity_types = [t.strip() for t in entity_types_str.split(',') if t.strip()] if entity_types_str else None enrich = request.args.get('enrich', 'true').lower() == 'true' - logger.info(f"获取图谱实体: graph_id={graph_id}, entity_types={entity_types}, enrich={enrich}") + logger.info(f"Fetching graph entities: graph_id={graph_id}, entity_types={entity_types}, enrich={enrich}") reader = ZepEntityReader() result = reader.filter_defined_entities( @@ -81,7 +82,7 @@ def get_graph_entities(graph_id: str): }) except Exception as e: - logger.error(f"获取图谱实体失败: {str(e)}") + logger.error(f"Failed to fetch graph entities: {str(e)}") return jsonify({ "success": False, "error": str(e), @@ -91,12 +92,12 @@ def get_graph_entities(graph_id: str): @simulation_bp.route('/entities//', methods=['GET']) def get_entity_detail(graph_id: str, entity_uuid: str): - """获取单个实体的详细信息""" + """Lấy thông tin chi tiết của một thực thể.""" try: if not Config.ZEP_API_KEY: return jsonify({ "success": False, - "error": "ZEP_API_KEY未配置" + "error": "ZEP_API_KEY is not configured" }), 500 reader = ZepEntityReader() @@ -105,7 +106,7 @@ def get_entity_detail(graph_id: str, entity_uuid: str): if not entity: return jsonify({ "success": False, - "error": f"实体不存在: {entity_uuid}" + "error": f"Entity does not exist: {entity_uuid}" }), 404 return jsonify({ @@ -114,7 +115,7 @@ def get_entity_detail(graph_id: str, entity_uuid: str): }) except Exception as e: - logger.error(f"获取实体详情失败: {str(e)}") + logger.error(f"Failed to fetch entity details: {str(e)}") return jsonify({ "success": False, "error": str(e), @@ -124,12 +125,12 @@ def get_entity_detail(graph_id: str, entity_uuid: str): @simulation_bp.route('/entities//by-type/', methods=['GET']) def get_entities_by_type(graph_id: str, entity_type: str): - """获取指定类型的所有实体""" + """Lấy toàn bộ thực thể theo loại chỉ định.""" try: if not Config.ZEP_API_KEY: return jsonify({ "success": False, - "error": "ZEP_API_KEY未配置" + "error": "ZEP_API_KEY is not configured" }), 500 enrich = request.args.get('enrich', 'true').lower() == 'true' @@ -151,7 +152,7 @@ def get_entities_by_type(graph_id: str, entity_type: str): }) except Exception as e: - logger.error(f"获取实体失败: {str(e)}") + logger.error(f"Failed to fetch entities: {str(e)}") return jsonify({ "success": False, "error": str(e), @@ -159,24 +160,24 @@ def get_entities_by_type(graph_id: str, entity_type: str): }), 500 -# ============== 模拟管理接口 ============== +# ============== API quản lý mô phỏng ============== @simulation_bp.route('/create', methods=['POST']) def create_simulation(): """ - 创建新的模拟 + Tạo mô phỏng mới. - 注意:max_rounds等参数由LLM智能生成,无需手动设置 + Lưu ý: Các tham số như max_rounds được LLM tạo thông minh, không cần đặt thủ công. - 请求(JSON): + Yêu cầu (JSON): { - "project_id": "proj_xxxx", // 必填 - "graph_id": "mirofish_xxxx", // 可选,如不提供则从project获取 - "enable_twitter": true, // 可选,默认true - "enable_reddit": true // 可选,默认true + "project_id": "proj_xxxx", // bắt buộc + "graph_id": "mirofish_xxxx", // tùy chọn, nếu không cung cấp sẽ lấy từ project + "enable_twitter": true, // tùy chọn, mặc định true + "enable_reddit": true // tùy chọn, mặc định true } - 返回: + Trả về: { "success": true, "data": { @@ -197,21 +198,21 @@ def create_simulation(): if not project_id: return jsonify({ "success": False, - "error": "请提供 project_id" + "error": "Please provide project_id" }), 400 project = ProjectManager.get_project(project_id) if not project: return jsonify({ "success": False, - "error": f"项目不存在: {project_id}" + "error": f"Project does not exist: {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": "Project graph has not been built yet. Please call /api/graph/build first" }), 400 manager = SimulationManager() @@ -228,7 +229,7 @@ def create_simulation(): }) except Exception as e: - logger.error(f"创建模拟失败: {str(e)}") + logger.error(f"Failed to create simulation: {str(e)}") return jsonify({ "success": False, "error": str(e), @@ -238,16 +239,16 @@ def create_simulation(): def _check_simulation_prepared(simulation_id: str) -> tuple: """ - 检查模拟是否已经准备完成 + Kiểm tra mô phỏng đã được chuẩn bị xong chưa. - 检查条件: - 1. state.json 存在且 status 为 "ready" - 2. 必要文件存在:reddit_profiles.json, twitter_profiles.csv, simulation_config.json + Điều kiện kiểm tra: + 1. `state.json` tồn tại và `status` là "ready" + 2. Các file cần thiết tồn tại: reddit_profiles.json, twitter_profiles.csv, simulation_config.json - 注意:运行脚本(run_*.py)保留在 backend/scripts/ 目录,不再复制到模拟目录 + Lưu ý: Script chạy (run_*.py) được giữ ở thư mục backend/scripts/ và không còn sao chép vào thư mục mô phỏng. Args: - simulation_id: 模拟ID + simulation_id: simulation ID Returns: (is_prepared: bool, info: dict) @@ -257,11 +258,11 @@ def _check_simulation_prepared(simulation_id: str) -> tuple: simulation_dir = os.path.join(Config.OASIS_SIMULATION_DATA_DIR, simulation_id) - # 检查目录是否存在 + # Kiểm tra thư mục có tồn tại hay không if not os.path.exists(simulation_dir): - return False, {"reason": "模拟目录不存在"} + return False, {"reason": "Simulation directory does not exist"} - # 必要文件列表(不包括脚本,脚本位于 backend/scripts/) + # Danh sách file cần thiết (không bao gồm script, script nằm ở backend/scripts/) required_files = [ "state.json", "simulation_config.json", @@ -269,7 +270,7 @@ def _check_simulation_prepared(simulation_id: str) -> tuple: "twitter_profiles.csv" ] - # 检查文件是否存在 + # Kiểm tra file có tồn tại hay không existing_files = [] missing_files = [] for f in required_files: @@ -281,12 +282,12 @@ def _check_simulation_prepared(simulation_id: str) -> tuple: if missing_files: return False, { - "reason": "缺少必要文件", + "reason": "Missing required files", "missing_files": missing_files, "existing_files": existing_files } - # 检查state.json中的状态 + # Kiểm tra trạng thái trong state.json state_file = os.path.join(simulation_dir, "state.json") try: import json @@ -296,20 +297,21 @@ def _check_simulation_prepared(simulation_id: str) -> tuple: status = state_data.get("status", "") config_generated = state_data.get("config_generated", False) - # 详细日志 - logger.debug(f"检测模拟准备状态: {simulation_id}, status={status}, config_generated={config_generated}") - - # 如果 config_generated=True 且文件存在,认为准备完成 - # 以下状态都说明准备工作已完成: - # - ready: 准备完成,可以运行 - # - preparing: 如果 config_generated=True 说明已完成 - # - running: 正在运行,说明准备早就完成了 - # - completed: 运行完成,说明准备早就完成了 - # - stopped: 已停止,说明准备早就完成了 - # - failed: 运行失败(但准备是完成的) + # Log chi tiết + logger.debug(f"Checking simulation readiness: {simulation_id}, status={status}, config_generated={config_generated}") + + # Nếu config_generated=True và file tồn tại thì xem như đã chuẩn bị xong + # Các trạng thái dưới đây đều cho thấy quá trình chuẩn bị đã hoàn tất: + # - ready: chuẩn bị xong, có thể chạy + # - preparing: nếu config_generated=True thì xem như đã hoàn tất + # - running: đang chạy, nghĩa là đã chuẩn bị xong từ trước + # - completed: đã chạy xong, nghĩa là đã chuẩn bị xong từ trước + # - stopped: đã dừng, nghĩa là đã chuẩn bị xong từ trước + # - failed: chạy thất bại (nhưng phần chuẩn bị đã hoàn tất) prepared_statuses = ["ready", "preparing", "running", "completed", "stopped", "failed"] + if status in prepared_statuses and config_generated: - # 获取文件统计信息 + # Lấy thông tin thống kê file profiles_file = os.path.join(simulation_dir, "reddit_profiles.json") config_file = os.path.join(simulation_dir, "simulation_config.json") @@ -319,7 +321,7 @@ def _check_simulation_prepared(simulation_id: str) -> tuple: profiles_data = json.load(f) profiles_count = len(profiles_data) if isinstance(profiles_data, list) else 0 - # 如果状态是preparing但文件已完成,自动更新状态为ready + # Nếu trạng thái là preparing nhưng file đã hoàn tất, tự động cập nhật thành ready if status == "preparing": try: state_data["status"] = "ready" @@ -327,12 +329,12 @@ def _check_simulation_prepared(simulation_id: str) -> tuple: state_data["updated_at"] = datetime.now().isoformat() with open(state_file, 'w', encoding='utf-8') as f: json.dump(state_data, f, ensure_ascii=False, indent=2) - logger.info(f"自动更新模拟状态: {simulation_id} preparing -> ready") + logger.info(f"Auto-updated simulation status: {simulation_id} preparing -> ready") status = "ready" except Exception as e: - logger.warning(f"自动更新状态失败: {e}") + logger.warning(f"Failed to auto-update status: {e}") - logger.info(f"模拟 {simulation_id} 检测结果: 已准备完成 (status={status}, config_generated={config_generated})") + logger.info(f"Simulation {simulation_id} check result: prepared (status={status}, config_generated={config_generated})") return True, { "status": status, "entities_count": state_data.get("entities_count", 0), @@ -344,55 +346,55 @@ def _check_simulation_prepared(simulation_id: str) -> tuple: "existing_files": existing_files } else: - logger.warning(f"模拟 {simulation_id} 检测结果: 未准备完成 (status={status}, config_generated={config_generated})") + logger.warning(f"Simulation {simulation_id} check result: not prepared (status={status}, config_generated={config_generated})") return False, { - "reason": f"状态不在已准备列表中或config_generated为false: status={status}, config_generated={config_generated}", + "reason": f"Status is not in prepared list or config_generated is false: status={status}, config_generated={config_generated}", "status": status, "config_generated": config_generated } except Exception as e: - return False, {"reason": f"读取状态文件失败: {str(e)}"} + return False, {"reason": f"Failed to read state file: {str(e)}"} @simulation_bp.route('/prepare', methods=['POST']) def prepare_simulation(): """ - 准备模拟环境(异步任务,LLM智能生成所有参数) + Chuẩn bị môi trường mô phỏng (tác vụ bất đồng bộ, LLM tạo toàn bộ tham số). - 这是一个耗时操作,接口会立即返回task_id, - 使用 GET /api/simulation/prepare/status 查询进度 + Đây là thao tác tốn thời gian, API sẽ trả về task_id ngay lập tức. + Dùng GET /api/simulation/prepare/status để kiểm tra tiến độ. - 特性: - - 自动检测已完成的准备工作,避免重复生成 - - 如果已准备完成,直接返回已有结果 - - 支持强制重新生成(force_regenerate=true) + Đặc điểm: + - Tự động phát hiện phần chuẩn bị đã hoàn tất để tránh sinh lại. + - Nếu đã chuẩn bị xong thì trả về kết quả sẵn có. + - Hỗ trợ buộc sinh lại (force_regenerate=true). - 步骤: - 1. 检查是否已有完成的准备工作 - 2. 从Zep图谱读取并过滤实体 - 3. 为每个实体生成OASIS Agent Profile(带重试机制) - 4. LLM智能生成模拟配置(带重试机制) - 5. 保存配置文件和预设脚本 + Các bước: + 1. Kiểm tra xem đã có phần chuẩn bị hoàn tất hay chưa. + 2. Đọc và lọc thực thể từ đồ thị Zep. + 3. Sinh OASIS Agent Profile cho từng thực thể (có cơ chế retry). + 4. LLM sinh cấu hình mô phỏng một cách thông minh (có cơ chế retry). + 5. Lưu file cấu hình và script thiết lập sẵn. - 请求(JSON): + Yêu cầu (JSON): { - "simulation_id": "sim_xxxx", // 必填,模拟ID - "entity_types": ["Student", "PublicFigure"], // 可选,指定实体类型 - "use_llm_for_profiles": true, // 可选,是否用LLM生成人设 - "parallel_profile_count": 5, // 可选,并行生成人设数量,默认5 - "force_regenerate": false // 可选,强制重新生成,默认false + "simulation_id": "sim_xxxx", // bắt buộc, simulation ID + "entity_types": ["Student", "PublicFigure"], // tùy chọn, chỉ định loại thực thể + "use_llm_for_profiles": true, // tùy chọn, có dùng LLM để sinh persona hay không + "parallel_profile_count": 5, // tùy chọn, số lượng sinh persona song song, mặc định 5 + "force_regenerate": false // tùy chọn, buộc sinh lại, mặc định false } - 返回: + Trả về: { "success": true, "data": { "simulation_id": "sim_xxxx", - "task_id": "task_xxxx", // 新任务时返回 + "task_id": "task_xxxx", // trả về khi là tác vụ mới "status": "preparing|ready", - "message": "准备任务已启动|已有完成的准备工作", - "already_prepared": true|false // 是否已准备完成 + "message": "Preparation task has started|Preparation already exists", + "already_prepared": true|false // đã chuẩn bị xong hay chưa } } """ @@ -408,7 +410,7 @@ def prepare_simulation(): if not simulation_id: return jsonify({ "success": False, - "error": "请提供 simulation_id" + "error": "Please provide simulation_id" }), 400 manager = SimulationManager() @@ -417,76 +419,76 @@ def prepare_simulation(): if not state: return jsonify({ "success": False, - "error": f"模拟不存在: {simulation_id}" + "error": f"Simulation does not exist: {simulation_id}" }), 404 - # 检查是否强制重新生成 + # Kiểm tra có buộc sinh lại hay không force_regenerate = data.get('force_regenerate', False) - logger.info(f"开始处理 /prepare 请求: simulation_id={simulation_id}, force_regenerate={force_regenerate}") + logger.info(f"Start handling /prepare request: simulation_id={simulation_id}, force_regenerate={force_regenerate}") - # 检查是否已经准备完成(避免重复生成) + # Kiểm tra đã chuẩn bị xong hay chưa (tránh sinh lại) if not force_regenerate: - logger.debug(f"检查模拟 {simulation_id} 是否已准备完成...") + logger.debug(f"Checking whether simulation {simulation_id} is already prepared...") is_prepared, prepare_info = _check_simulation_prepared(simulation_id) - logger.debug(f"检查结果: is_prepared={is_prepared}, prepare_info={prepare_info}") + logger.debug(f"Check result: is_prepared={is_prepared}, prepare_info={prepare_info}") if is_prepared: - logger.info(f"模拟 {simulation_id} 已准备完成,跳过重复生成") + logger.info(f"Simulation {simulation_id} is already prepared, skipping regeneration") return jsonify({ "success": True, "data": { "simulation_id": simulation_id, "status": "ready", - "message": "已有完成的准备工作,无需重复生成", + "message": "Preparation already exists, no regeneration needed", "already_prepared": True, "prepare_info": prepare_info } }) else: - logger.info(f"模拟 {simulation_id} 未准备完成,将启动准备任务") + logger.info(f"Simulation {simulation_id} is not prepared, will start preparation task") - # 从项目获取必要信息 + # Lấy thông tin cần thiết từ dự án project = ProjectManager.get_project(state.project_id) if not project: return jsonify({ "success": False, - "error": f"项目不存在: {state.project_id}" + "error": f"Project does not exist: {state.project_id}" }), 404 - # 获取模拟需求 + # Lấy yêu cầu mô phỏng simulation_requirement = project.simulation_requirement or "" if not simulation_requirement: return jsonify({ "success": False, - "error": "项目缺少模拟需求描述 (simulation_requirement)" + "error": "Project is missing simulation requirement description (simulation_requirement)" }), 400 - # 获取文档文本 + # Lấy văn bản tài liệu document_text = ProjectManager.get_extracted_text(state.project_id) or "" entity_types_list = data.get('entity_types') use_llm_for_profiles = data.get('use_llm_for_profiles', True) parallel_profile_count = data.get('parallel_profile_count', 5) - # ========== 同步获取实体数量(在后台任务启动前) ========== - # 这样前端在调用prepare后立即就能获取到预期Agent总数 + # ========== Đồng bộ lấy số lượng thực thể (trước khi chạy tác vụ nền) ========== + # Nhờ đó frontend có thể lấy ngay tổng số Agent dự kiến sau khi gọi prepare try: - logger.info(f"同步获取实体数量: graph_id={state.graph_id}") + logger.info(f"Synchronously fetching entity count: graph_id={state.graph_id}") reader = ZepEntityReader() - # 快速读取实体(不需要边信息,只统计数量) + # Đọc nhanh thực thể (không cần thông tin cạnh, chỉ đếm số lượng) filtered_preview = reader.filter_defined_entities( graph_id=state.graph_id, defined_entity_types=entity_types_list, - enrich_with_edges=False # 不获取边信息,加快速度 + enrich_with_edges=False # Do not fetch edge info to speed up ) - # 保存实体数量到状态(供前端立即获取) + # Lưu số lượng thực thể vào trạng thái (để frontend lấy ngay) state.entities_count = filtered_preview.filtered_count state.entity_types = list(filtered_preview.entity_types) - logger.info(f"预期实体数量: {filtered_preview.filtered_count}, 类型: {filtered_preview.entity_types}") + logger.info(f"Expected entity count: {filtered_preview.filtered_count}, types: {filtered_preview.entity_types}") except Exception as e: - logger.warning(f"同步获取实体数量失败(将在后台任务中重试): {e}") - # 失败不影响后续流程,后台任务会重新获取 + logger.warning(f"Failed to synchronously fetch entity count (will retry in background task): {e}") + # Lỗi này không ảnh hưởng luồng tiếp theo, tác vụ nền sẽ lấy lại - # 创建异步任务 + # Tạo tác vụ bất đồng bộ task_manager = TaskManager() task_id = task_manager.create_task( task_type="simulation_prepare", @@ -496,26 +498,26 @@ def prepare_simulation(): } ) - # 更新模拟状态(包含预先获取的实体数量) + # Cập nhật trạng thái mô phỏng (bao gồm số lượng thực thể đã lấy trước) state.status = SimulationStatus.PREPARING manager._save_simulation_state(state) - # 定义后台任务 + # Định nghĩa tác vụ nền def run_prepare(): try: task_manager.update_task( task_id, status=TaskStatus.PROCESSING, progress=0, - message="开始准备模拟环境..." + message="Start preparing simulation environment..." ) - # 准备模拟(带进度回调) - # 存储阶段进度详情 + # Chuẩn bị mô phỏng (có callback tiến độ) + # Lưu chi tiết tiến độ theo giai đoạn stage_details = {} def progress_callback(stage, progress, message, **kwargs): - # 计算总进度 + # Tính tổng tiến độ stage_weights = { "reading": (0, 20), # 0-20% "generating_profiles": (20, 70), # 20-70% @@ -526,18 +528,18 @@ def progress_callback(stage, progress, message, **kwargs): start, end = stage_weights.get(stage, (0, 100)) current_progress = int(start + (end - start) * progress / 100) - # 构建详细进度信息 + # Tạo thông tin tiến độ chi tiết stage_names = { - "reading": "读取图谱实体", - "generating_profiles": "生成Agent人设", - "generating_config": "生成模拟配置", - "copying_scripts": "准备模拟脚本" + "reading": "Reading graph entities", + "generating_profiles": "Generating agent personas", + "generating_config": "Generating simulation config", + "copying_scripts": "Preparing simulation scripts" } stage_index = list(stage_weights.keys()).index(stage) + 1 if stage in stage_weights else 1 total_stages = len(stage_weights) - # 更新阶段详情 + # Cập nhật chi tiết giai đoạn stage_details[stage] = { "stage_name": stage_names.get(stage, stage), "stage_progress": progress, @@ -546,7 +548,7 @@ def progress_callback(stage, progress, message, **kwargs): "item_name": kwargs.get("item_name", "") } - # 构建详细进度信息 + # Tạo thông tin tiến độ chi tiết detail = stage_details[stage] progress_detail_data = { "current_stage": stage, @@ -559,7 +561,7 @@ def progress_callback(stage, progress, message, **kwargs): "item_description": message } - # 构建简洁消息 + # Tạo thông báo ngắn gọn if detail["total"] > 0: detailed_message = ( f"[{stage_index}/{total_stages}] {stage_names.get(stage, stage)}: " @@ -585,24 +587,24 @@ def progress_callback(stage, progress, message, **kwargs): parallel_profile_count=parallel_profile_count ) - # 任务完成 + # Task completed task_manager.complete_task( task_id, result=result_state.to_simple_dict() ) except Exception as e: - logger.error(f"准备模拟失败: {str(e)}") + logger.error(f"Failed to prepare simulation: {str(e)}") task_manager.fail_task(task_id, str(e)) - # 更新模拟状态为失败 + # Cập nhật trạng thái mô phỏng thành failed state = manager.get_simulation(simulation_id) if state: state.status = SimulationStatus.FAILED state.error = str(e) manager._save_simulation_state(state) - # 启动后台线程 + # Khởi chạy luồng nền thread = threading.Thread(target=run_prepare, daemon=True) thread.start() @@ -612,10 +614,10 @@ def progress_callback(stage, progress, message, **kwargs): "simulation_id": simulation_id, "task_id": task_id, "status": "preparing", - "message": "准备任务已启动,请通过 /api/simulation/prepare/status 查询进度", + "message": "Preparation task has started. Please check progress via /api/simulation/prepare/status", "already_prepared": False, - "expected_entities_count": state.entities_count, # 预期的Agent总数 - "entity_types": state.entity_types # 实体类型列表 + "expected_entities_count": state.entities_count, # Expected total Agent count + "entity_types": state.entity_types # Entity type list } }) @@ -626,7 +628,7 @@ def progress_callback(stage, progress, message, **kwargs): }), 404 except Exception as e: - logger.error(f"启动准备任务失败: {str(e)}") + logger.error(f"Failed to start preparation task: {str(e)}") return jsonify({ "success": False, "error": str(e), @@ -637,19 +639,19 @@ def progress_callback(stage, progress, message, **kwargs): @simulation_bp.route('/prepare/status', methods=['POST']) def get_prepare_status(): """ - 查询准备任务进度 + Query preparation task progress. - 支持两种查询方式: - 1. 通过task_id查询正在进行的任务进度 - 2. 通过simulation_id检查是否已有完成的准备工作 + Supports two query modes: + 1. Query ongoing task progress by task_id. + 2. Check whether completed preparation already exists by simulation_id. - 请求(JSON): + Yêu cầu (JSON): { - "task_id": "task_xxxx", // 可选,prepare返回的task_id - "simulation_id": "sim_xxxx" // 可选,模拟ID(用于检查已完成的准备) + "task_id": "task_xxxx", // tùy chọn, task_id trả về từ prepare + "simulation_id": "sim_xxxx" // tùy chọn, simulation ID (để kiểm tra phần chuẩn bị đã hoàn tất) } - 返回: + Trả về: { "success": true, "data": { @@ -657,8 +659,8 @@ def get_prepare_status(): "status": "processing|completed|ready", "progress": 45, "message": "...", - "already_prepared": true|false, // 是否已有完成的准备 - "prepare_info": {...} // 已准备完成时的详细信息 + "already_prepared": true|false, // đã có phần chuẩn bị hoàn tất hay chưa + "prepare_info": {...} // thông tin chi tiết khi đã chuẩn bị hoàn tất } } """ @@ -670,7 +672,7 @@ def get_prepare_status(): task_id = data.get('task_id') simulation_id = data.get('simulation_id') - # 如果提供了simulation_id,先检查是否已准备完成 + # Nếu có simulation_id, kiểm tra trước xem đã chuẩn bị hoàn tất chưa if simulation_id: is_prepared, prepare_info = _check_simulation_prepared(simulation_id) if is_prepared: @@ -680,36 +682,36 @@ def get_prepare_status(): "simulation_id": simulation_id, "status": "ready", "progress": 100, - "message": "已有完成的准备工作", + "message": "Preparation already completed", "already_prepared": True, "prepare_info": prepare_info } }) - # 如果没有task_id,返回错误 + # Nếu không có task_id thì trả về lỗi if not task_id: if simulation_id: - # 有simulation_id但未准备完成 + # Có simulation_id nhưng chưa chuẩn bị xong return jsonify({ "success": True, "data": { "simulation_id": simulation_id, "status": "not_started", "progress": 0, - "message": "尚未开始准备,请调用 /api/simulation/prepare 开始", + "message": "Preparation has not started yet. Please call /api/simulation/prepare to start", "already_prepared": False } }) return jsonify({ "success": False, - "error": "请提供 task_id 或 simulation_id" + "error": "Please provide task_id or simulation_id" }), 400 task_manager = TaskManager() task = task_manager.get_task(task_id) if not task: - # 任务不存在,但如果有simulation_id,检查是否已准备完成 + # Task không tồn tại, nhưng nếu có simulation_id thì kiểm tra đã chuẩn bị xong chưa if simulation_id: is_prepared, prepare_info = _check_simulation_prepared(simulation_id) if is_prepared: @@ -720,7 +722,7 @@ def get_prepare_status(): "task_id": task_id, "status": "ready", "progress": 100, - "message": "任务已完成(准备工作已存在)", + "message": "Task completed (preparation already exists)", "already_prepared": True, "prepare_info": prepare_info } @@ -728,7 +730,7 @@ def get_prepare_status(): return jsonify({ "success": False, - "error": f"任务不存在: {task_id}" + "error": f"Task does not exist: {task_id}" }), 404 task_dict = task.to_dict() @@ -740,7 +742,7 @@ def get_prepare_status(): }) except Exception as e: - logger.error(f"查询任务状态失败: {str(e)}") + logger.error(f"Failed to query task status: {str(e)}") return jsonify({ "success": False, "error": str(e) @@ -749,7 +751,7 @@ def get_prepare_status(): @simulation_bp.route('/', methods=['GET']) def get_simulation(simulation_id: str): - """获取模拟状态""" + """Lấy trạng thái mô phỏng.""" try: manager = SimulationManager() state = manager.get_simulation(simulation_id) @@ -757,12 +759,12 @@ def get_simulation(simulation_id: str): if not state: return jsonify({ "success": False, - "error": f"模拟不存在: {simulation_id}" + "error": f"Simulation does not exist: {simulation_id}" }), 404 result = state.to_dict() - # 如果模拟已准备好,附加运行说明 + # Nếu mô phỏng đã sẵn sàng, đính kèm hướng dẫn chạy if state.status == SimulationStatus.READY: result["run_instructions"] = manager.get_run_instructions(simulation_id) @@ -772,7 +774,7 @@ def get_simulation(simulation_id: str): }) except Exception as e: - logger.error(f"获取模拟状态失败: {str(e)}") + logger.error(f"Failed to get simulation status: {str(e)}") return jsonify({ "success": False, "error": str(e), @@ -783,10 +785,10 @@ def get_simulation(simulation_id: str): @simulation_bp.route('/list', methods=['GET']) def list_simulations(): """ - 列出所有模拟 + Liệt kê toàn bộ mô phỏng. - Query参数: - project_id: 按项目ID过滤(可选) + Tham số query: + project_id: Lọc theo project ID (tùy chọn) """ try: project_id = request.args.get('project_id') @@ -801,7 +803,7 @@ def list_simulations(): }) except Exception as e: - logger.error(f"列出模拟失败: {str(e)}") + logger.error(f"Failed to list simulations: {str(e)}") return jsonify({ "success": False, "error": str(e), @@ -811,22 +813,22 @@ def list_simulations(): def _get_report_id_for_simulation(simulation_id: str) -> str: """ - 获取 simulation 对应的最新 report_id + Lấy report_id mới nhất tương ứng với simulation. - 遍历 reports 目录,找出 simulation_id 匹配的 report, - 如果有多个则返回最新的(按 created_at 排序) + Duyệt thư mục reports để tìm report khớp simulation_id. + Nếu có nhiều report thì trả về report mới nhất (sắp xếp theo created_at). Args: - simulation_id: 模拟ID + simulation_id: simulation ID Returns: - report_id 或 None + report_id hoặc None """ import json from datetime import datetime - # reports 目录路径:backend/uploads/reports - # __file__ 是 app/api/simulation.py,需要向上两级到 backend/ + # Đường dẫn thư mục reports: backend/uploads/reports + # __file__ là app/api/simulation.py, cần đi lên hai cấp để tới backend/ reports_dir = os.path.join(os.path.dirname(__file__), '../../uploads/reports') if not os.path.exists(reports_dir): return None @@ -859,34 +861,34 @@ def _get_report_id_for_simulation(simulation_id: str) -> str: if not matching_reports: return None - # 按创建时间倒序排序,返回最新的 + # Sắp xếp giảm dần theo thời gian tạo và trả về bản mới nhất matching_reports.sort(key=lambda x: x.get("created_at", ""), reverse=True) return matching_reports[0].get("report_id") except Exception as e: - logger.warning(f"查找 simulation {simulation_id} 的 report 失败: {e}") + logger.warning(f"Failed to find report for simulation {simulation_id}: {e}") return None @simulation_bp.route('/history', methods=['GET']) def get_simulation_history(): """ - 获取历史模拟列表(带项目详情) + Lấy danh sách lịch sử mô phỏng (kèm chi tiết dự án). - 用于首页历史项目展示,返回包含项目名称、描述等丰富信息的模拟列表 + Dùng cho hiển thị lịch sử dự án ở trang chủ, trả về danh sách mô phỏng với thông tin đầy đủ như tên dự án, mô tả. - Query参数: - limit: 返回数量限制(默认20) + Tham số query: + limit: Giới hạn số lượng trả về (mặc định 20) - 返回: + Trả về: { "success": true, "data": [ { "simulation_id": "sim_xxxx", "project_id": "proj_xxxx", - "project_name": "武大舆情分析", - "simulation_requirement": "如果武汉大学发布...", + "project_name": "Phân tích dư luận WU", + "simulation_requirement": "Nếu Đại học Vũ Hán đăng tải...", "status": "completed", "entities_count": 68, "profiles_count": 68, @@ -909,18 +911,18 @@ def get_simulation_history(): manager = SimulationManager() simulations = manager.list_simulations()[:limit] - # 增强模拟数据,只从 Simulation 文件读取 + # Bổ sung dữ liệu mô phỏng, chỉ đọc từ file Simulation enriched_simulations = [] for sim in simulations: sim_dict = sim.to_dict() - # 获取模拟配置信息(从 simulation_config.json 读取 simulation_requirement) + # Lấy thông tin cấu hình mô phỏng (đọc simulation_requirement từ simulation_config.json) config = manager.get_simulation_config(sim.simulation_id) if config: sim_dict["simulation_requirement"] = config.get("simulation_requirement", "") time_config = config.get("time_config", {}) sim_dict["total_simulation_hours"] = time_config.get("total_simulation_hours", 0) - # 推荐轮数(后备值) + # Số vòng khuyến nghị (giá trị dự phòng) recommended_rounds = int( time_config.get("total_simulation_hours", 0) * 60 / max(time_config.get("minutes_per_round", 60), 1) @@ -930,35 +932,35 @@ def get_simulation_history(): sim_dict["total_simulation_hours"] = 0 recommended_rounds = 0 - # 获取运行状态(从 run_state.json 读取用户设置的实际轮数) + # Lấy trạng thái chạy (đọc số vòng thực tế do người dùng thiết lập từ run_state.json) run_state = SimulationRunner.get_run_state(sim.simulation_id) if run_state: sim_dict["current_round"] = run_state.current_round sim_dict["runner_status"] = run_state.runner_status.value - # 使用用户设置的 total_rounds,若无则使用推荐轮数 + # Dùng total_rounds do người dùng thiết lập, nếu không có thì dùng số vòng khuyến nghị sim_dict["total_rounds"] = run_state.total_rounds if run_state.total_rounds > 0 else recommended_rounds else: sim_dict["current_round"] = 0 sim_dict["runner_status"] = "idle" sim_dict["total_rounds"] = recommended_rounds - # 获取关联项目的文件列表(最多3个) + # Lấy danh sách file của dự án liên kết (tối đa 3 file) project = ProjectManager.get_project(sim.project_id) if project and hasattr(project, 'files') and project.files: sim_dict["files"] = [ - {"filename": f.get("filename", "未知文件")} + {"filename": f.get("filename", "Unknown file")} for f in project.files[:3] ] else: sim_dict["files"] = [] - # 获取关联的 report_id(查找该 simulation 最新的 report) + # Lấy report_id liên kết (tìm report mới nhất của simulation này) sim_dict["report_id"] = _get_report_id_for_simulation(sim.simulation_id) - # 添加版本号 + # Thêm số phiên bản sim_dict["version"] = "v1.0.2" - # 格式化日期 + # Định dạng ngày try: created_date = sim_dict.get("created_at", "")[:10] sim_dict["created_date"] = created_date @@ -974,7 +976,7 @@ def get_simulation_history(): }) except Exception as e: - logger.error(f"获取历史模拟失败: {str(e)}") + logger.error(f"Failed to get simulation history: {str(e)}") return jsonify({ "success": False, "error": str(e), @@ -985,10 +987,10 @@ def get_simulation_history(): @simulation_bp.route('//profiles', methods=['GET']) def get_simulation_profiles(simulation_id: str): """ - 获取模拟的Agent Profile + Lấy Agent Profile của mô phỏng. - Query参数: - platform: 平台类型(reddit/twitter,默认reddit) + Tham số query: + platform: Loại nền tảng (reddit/twitter, mặc định reddit) """ try: platform = request.args.get('platform', 'reddit') @@ -1012,7 +1014,7 @@ def get_simulation_profiles(simulation_id: str): }), 404 except Exception as e: - logger.error(f"获取Profile失败: {str(e)}") + logger.error(f"Failed to get profile: {str(e)}") return jsonify({ "success": False, "error": str(e), @@ -1023,25 +1025,25 @@ def get_simulation_profiles(simulation_id: str): @simulation_bp.route('//profiles/realtime', methods=['GET']) def get_simulation_profiles_realtime(simulation_id: str): """ - 实时获取模拟的Agent Profile(用于在生成过程中实时查看进度) + Lấy Agent Profile theo thời gian thực (dùng để theo dõi tiến độ trong quá trình sinh). - 与 /profiles 接口的区别: - - 直接读取文件,不经过 SimulationManager - - 适用于生成过程中的实时查看 - - 返回额外的元数据(如文件修改时间、是否正在生成等) + Khác biệt so với endpoint /profiles: + - Đọc trực tiếp từ file, không đi qua SimulationManager. + - Phù hợp cho việc xem realtime trong quá trình sinh. + - Trả về thêm metadata (như thời điểm sửa file, có đang sinh hay không). - Query参数: - platform: 平台类型(reddit/twitter,默认reddit) + Tham số query: + platform: Loại nền tảng (reddit/twitter, mặc định reddit) - 返回: + Trả về: { "success": true, "data": { "simulation_id": "sim_xxxx", "platform": "reddit", "count": 15, - "total_expected": 93, // 预期总数(如果有) - "is_generating": true, // 是否正在生成 + "total_expected": 93, // tổng số dự kiến (nếu có) + "is_generating": true, // có đang sinh hay không "file_exists": true, "file_modified_at": "2025-12-04T18:20:00", "profiles": [...] @@ -1055,28 +1057,28 @@ def get_simulation_profiles_realtime(simulation_id: str): try: platform = request.args.get('platform', 'reddit') - # 获取模拟目录 + # Lấy thư mục mô phỏng sim_dir = os.path.join(Config.OASIS_SIMULATION_DATA_DIR, simulation_id) if not os.path.exists(sim_dir): return jsonify({ "success": False, - "error": f"模拟不存在: {simulation_id}" + "error": f"Simulation does not exist: {simulation_id}" }), 404 - # 确定文件路径 + # Xác định đường dẫn file if platform == "reddit": profiles_file = os.path.join(sim_dir, "reddit_profiles.json") else: profiles_file = os.path.join(sim_dir, "twitter_profiles.csv") - # 检查文件是否存在 + # Kiểm tra file có tồn tại hay không file_exists = os.path.exists(profiles_file) profiles = [] file_modified_at = None if file_exists: - # 获取文件修改时间 + # Lấy thời gian sửa file file_stat = os.stat(profiles_file) file_modified_at = datetime.fromtimestamp(file_stat.st_mtime).isoformat() @@ -1089,10 +1091,10 @@ def get_simulation_profiles_realtime(simulation_id: str): reader = csv.DictReader(f) profiles = list(reader) except (json.JSONDecodeError, Exception) as e: - logger.warning(f"读取 profiles 文件失败(可能正在写入中): {e}") + logger.warning(f"Failed to read profiles file (it may still be being written): {e}") profiles = [] - # 检查是否正在生成(通过 state.json 判断) + # Kiểm tra có đang sinh hay không (dựa vào state.json) is_generating = False total_expected = None @@ -1122,7 +1124,7 @@ def get_simulation_profiles_realtime(simulation_id: str): }) except Exception as e: - logger.error(f"实时获取Profile失败: {str(e)}") + logger.error(f"Failed to get profile in real time: {str(e)}") return jsonify({ "success": False, "error": str(e), @@ -1133,24 +1135,24 @@ def get_simulation_profiles_realtime(simulation_id: str): @simulation_bp.route('//config/realtime', methods=['GET']) def get_simulation_config_realtime(simulation_id: str): """ - 实时获取模拟配置(用于在生成过程中实时查看进度) + Lấy cấu hình mô phỏng theo thời gian thực (dùng để theo dõi tiến độ trong quá trình sinh). - 与 /config 接口的区别: - - 直接读取文件,不经过 SimulationManager - - 适用于生成过程中的实时查看 - - 返回额外的元数据(如文件修改时间、是否正在生成等) - - 即使配置还没生成完也能返回部分信息 + Khác biệt so với endpoint /config: + - Đọc trực tiếp từ file, không đi qua SimulationManager. + - Phù hợp cho việc xem realtime trong quá trình sinh. + - Trả về thêm metadata (như thời điểm sửa file, có đang sinh hay không). + - Ngay cả khi cấu hình chưa sinh xong vẫn có thể trả về thông tin một phần. - 返回: + Trả về: { "success": true, "data": { "simulation_id": "sim_xxxx", "file_exists": true, "file_modified_at": "2025-12-04T18:20:00", - "is_generating": true, // 是否正在生成 - "generation_stage": "generating_config", // 当前生成阶段 - "config": {...} // 配置内容(如果存在) + "is_generating": true, // có đang sinh hay không + "generation_stage": "generating_config", // giai đoạn sinh hiện tại + "config": {...} // nội dung cấu hình (nếu có) } } """ @@ -1158,25 +1160,25 @@ def get_simulation_config_realtime(simulation_id: str): from datetime import datetime try: - # 获取模拟目录 + # Lấy thư mục mô phỏng sim_dir = os.path.join(Config.OASIS_SIMULATION_DATA_DIR, simulation_id) if not os.path.exists(sim_dir): return jsonify({ "success": False, - "error": f"模拟不存在: {simulation_id}" + "error": f"Simulation does not exist: {simulation_id}" }), 404 - # 配置文件路径 + # Đường dẫn file cấu hình config_file = os.path.join(sim_dir, "simulation_config.json") - # 检查文件是否存在 + # Kiểm tra file có tồn tại hay không file_exists = os.path.exists(config_file) config = None file_modified_at = None if file_exists: - # 获取文件修改时间 + # Lấy thời gian sửa file file_stat = os.stat(config_file) file_modified_at = datetime.fromtimestamp(file_stat.st_mtime).isoformat() @@ -1184,10 +1186,10 @@ def get_simulation_config_realtime(simulation_id: str): with open(config_file, 'r', encoding='utf-8') as f: config = json.load(f) except (json.JSONDecodeError, Exception) as e: - logger.warning(f"读取 config 文件失败(可能正在写入中): {e}") + logger.warning(f"Failed to read config file (it may still be being written): {e}") config = None - # 检查是否正在生成(通过 state.json 判断) + # Kiểm tra có đang sinh hay không (dựa vào state.json) is_generating = False generation_stage = None config_generated = False @@ -1201,7 +1203,7 @@ def get_simulation_config_realtime(simulation_id: str): is_generating = status == "preparing" config_generated = state_data.get("config_generated", False) - # 判断当前阶段 + # Xác định giai đoạn hiện tại if is_generating: if state_data.get("profiles_generated", False): generation_stage = "generating_config" @@ -1212,7 +1214,7 @@ def get_simulation_config_realtime(simulation_id: str): except Exception: pass - # 构建返回数据 + # Tạo dữ liệu trả về response_data = { "simulation_id": simulation_id, "file_exists": file_exists, @@ -1223,7 +1225,7 @@ def get_simulation_config_realtime(simulation_id: str): "config": config } - # 如果配置存在,提取一些关键统计信息 + # Nếu cấu hình tồn tại, trích xuất một số thống kê quan trọng if config: response_data["summary"] = { "total_agents": len(config.get("agent_configs", [])), @@ -1242,7 +1244,7 @@ def get_simulation_config_realtime(simulation_id: str): }) except Exception as e: - logger.error(f"实时获取Config失败: {str(e)}") + logger.error(f"Failed to get config in real time: {str(e)}") return jsonify({ "success": False, "error": str(e), @@ -1253,14 +1255,14 @@ def get_simulation_config_realtime(simulation_id: str): @simulation_bp.route('//config', methods=['GET']) def get_simulation_config(simulation_id: str): """ - 获取模拟配置(LLM智能生成的完整配置) - - 返回包含: - - time_config: 时间配置(模拟时长、轮次、高峰/低谷时段) - - agent_configs: 每个Agent的活动配置(活跃度、发言频率、立场等) - - event_config: 事件配置(初始帖子、热点话题) - - platform_configs: 平台配置 - - generation_reasoning: LLM的配置推理说明 + Lấy cấu hình mô phỏng (cấu hình đầy đủ do LLM sinh). + + Bao gồm: + - time_config: Cấu hình thời gian (thời lượng mô phỏng, số vòng, khung giờ cao điểm/thấp điểm) + - agent_configs: Cấu hình hoạt động của từng Agent (mức độ hoạt động, tần suất phát biểu, lập trường...) + - event_config: Cấu hình sự kiện (bài đăng khởi tạo, chủ đề nóng) + - platform_configs: Cấu hình nền tảng + - generation_reasoning: Giải thích suy luận cấu hình của LLM """ try: manager = SimulationManager() @@ -1269,7 +1271,7 @@ def get_simulation_config(simulation_id: str): if not config: return jsonify({ "success": False, - "error": f"模拟配置不存在,请先调用 /prepare 接口" + "error": "Simulation config does not exist. Please call /prepare first" }), 404 return jsonify({ @@ -1278,7 +1280,7 @@ def get_simulation_config(simulation_id: str): }) except Exception as e: - logger.error(f"获取配置失败: {str(e)}") + logger.error(f"Failed to get config: {str(e)}") return jsonify({ "success": False, "error": str(e), @@ -1288,7 +1290,7 @@ def get_simulation_config(simulation_id: str): @simulation_bp.route('//config/download', methods=['GET']) def download_simulation_config(simulation_id: str): - """下载模拟配置文件""" + """Tải file cấu hình mô phỏng.""" try: manager = SimulationManager() sim_dir = manager._get_simulation_dir(simulation_id) @@ -1297,7 +1299,7 @@ def download_simulation_config(simulation_id: str): if not os.path.exists(config_path): return jsonify({ "success": False, - "error": "配置文件不存在,请先调用 /prepare 接口" + "error": "Config file does not exist. Please call /prepare first" }), 404 return send_file( @@ -1307,7 +1309,7 @@ def download_simulation_config(simulation_id: str): ) except Exception as e: - logger.error(f"下载配置失败: {str(e)}") + logger.error(f"Failed to download config: {str(e)}") return jsonify({ "success": False, "error": str(e), @@ -1318,19 +1320,19 @@ def download_simulation_config(simulation_id: str): @simulation_bp.route('/script//download', methods=['GET']) def download_simulation_script(script_name: str): """ - 下载模拟运行脚本文件(通用脚本,位于 backend/scripts/) + Tải file script chạy mô phỏng (script dùng chung, nằm trong backend/scripts/). - script_name可选值: + script_name có thể là: - run_twitter_simulation.py - run_reddit_simulation.py - run_parallel_simulation.py - action_logger.py """ try: - # 脚本位于 backend/scripts/ 目录 + # Script nằm trong thư mục backend/scripts/ scripts_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../scripts')) - # 验证脚本名称 + # Kiểm tra tên script allowed_scripts = [ "run_twitter_simulation.py", "run_reddit_simulation.py", @@ -1341,7 +1343,7 @@ def download_simulation_script(script_name: str): if script_name not in allowed_scripts: return jsonify({ "success": False, - "error": f"未知脚本: {script_name},可选: {allowed_scripts}" + "error": f"Unknown script: {script_name}. Allowed: {allowed_scripts}" }), 400 script_path = os.path.join(scripts_dir, script_name) @@ -1349,7 +1351,7 @@ def download_simulation_script(script_name: str): if not os.path.exists(script_path): return jsonify({ "success": False, - "error": f"脚本文件不存在: {script_name}" + "error": f"Script file does not exist: {script_name}" }), 404 return send_file( @@ -1359,7 +1361,7 @@ def download_simulation_script(script_name: str): ) except Exception as e: - logger.error(f"下载脚本失败: {str(e)}") + logger.error(f"Failed to download script: {str(e)}") return jsonify({ "success": False, "error": str(e), @@ -1367,19 +1369,19 @@ def download_simulation_script(script_name: str): }), 500 -# ============== Profile生成接口(独立使用) ============== +# ============== API sinh Profile (dùng độc lập) ============== @simulation_bp.route('/generate-profiles', methods=['POST']) def generate_profiles(): """ - 直接从图谱生成OASIS Agent Profile(不创建模拟) + Sinh trực tiếp OASIS Agent Profile từ đồ thị (không tạo mô phỏng). - 请求(JSON): + Yêu cầu (JSON): { - "graph_id": "mirofish_xxxx", // 必填 - "entity_types": ["Student"], // 可选 - "use_llm": true, // 可选 - "platform": "reddit" // 可选 + "graph_id": "mirofish_xxxx", // bắt buộc + "entity_types": ["Student"], // tùy chọn + "use_llm": true, // tùy chọn + "platform": "reddit" // tùy chọn } """ try: @@ -1389,7 +1391,7 @@ def generate_profiles(): if not graph_id: return jsonify({ "success": False, - "error": "请提供 graph_id" + "error": "Please provide graph_id" }), 400 entity_types = data.get('entity_types') @@ -1406,7 +1408,7 @@ def generate_profiles(): if filtered.filtered_count == 0: return jsonify({ "success": False, - "error": "没有找到符合条件的实体" + "error": "No matching entities found" }), 400 generator = OasisProfileGenerator() @@ -1433,7 +1435,7 @@ def generate_profiles(): }) except Exception as e: - logger.error(f"生成Profile失败: {str(e)}") + logger.error(f"Failed to generate profile: {str(e)}") return jsonify({ "success": False, "error": str(e), @@ -1441,35 +1443,35 @@ def generate_profiles(): }), 500 -# ============== 模拟运行控制接口 ============== +# ============== API điều khiển chạy mô phỏng ============== @simulation_bp.route('/start', methods=['POST']) def start_simulation(): """ - 开始运行模拟 + Bắt đầu chạy mô phỏng. - 请求(JSON): + Yêu cầu (JSON): { - "simulation_id": "sim_xxxx", // 必填,模拟ID - "platform": "parallel", // 可选: twitter / reddit / parallel (默认) - "max_rounds": 100, // 可选: 最大模拟轮数,用于截断过长的模拟 - "enable_graph_memory_update": false, // 可选: 是否将Agent活动动态更新到Zep图谱记忆 - "force": false // 可选: 强制重新开始(会停止运行中的模拟并清理日志) + "simulation_id": "sim_xxxx", // bắt buộc, simulation ID + "platform": "parallel", // tùy chọn: twitter / reddit / parallel (mặc định) + "max_rounds": 100, // tùy chọn: số vòng mô phỏng tối đa, dùng để cắt bớt mô phỏng quá dài + "enable_graph_memory_update": false, // tùy chọn: có cập nhật động hoạt động Agent vào bộ nhớ đồ thị Zep hay không + "force": false // tùy chọn: buộc chạy lại (sẽ dừng mô phỏng đang chạy và dọn log) } - 关于 force 参数: - - 启用后,如果模拟正在运行或已完成,会先停止并清理运行日志 - - 清理的内容包括:run_state.json, actions.jsonl, simulation.log 等 - - 不会清理配置文件(simulation_config.json)和 profile 文件 - - 适用于需要重新运行模拟的场景 + Về tham số force: + - Khi bật, nếu mô phỏng đang chạy hoặc đã hoàn tất thì sẽ dừng trước và dọn log chạy. + - Nội dung dọn bao gồm: run_state.json, actions.jsonl, simulation.log... + - Không dọn file cấu hình (simulation_config.json) và file profile. + - Phù hợp cho tình huống cần chạy lại mô phỏng. - 关于 enable_graph_memory_update: - - 启用后,模拟中所有Agent的活动(发帖、评论、点赞等)都会实时更新到Zep图谱 - - 这可以让图谱"记住"模拟过程,用于后续分析或AI对话 - - 需要模拟关联的项目有有效的 graph_id - - 采用批量更新机制,减少API调用次数 + Về enable_graph_memory_update: + - Khi bật, toàn bộ hoạt động của Agent trong mô phỏng (đăng bài, bình luận, thả like...) sẽ được cập nhật realtime vào đồ thị Zep. + - Điều này giúp đồ thị "ghi nhớ" quá trình mô phỏng để phục vụ phân tích hoặc hội thoại AI về sau. + - Dự án liên kết với mô phỏng cần có graph_id hợp lệ. + - Dùng cơ chế cập nhật theo lô để giảm số lần gọi API. - 返回: + Trả về: { "success": true, "data": { @@ -1479,8 +1481,8 @@ def start_simulation(): "twitter_running": true, "reddit_running": true, "started_at": "2025-12-01T10:00:00", - "graph_memory_update_enabled": true, // 是否启用了图谱记忆更新 - "force_restarted": true // 是否是强制重新开始 + "graph_memory_update_enabled": true, // có bật cập nhật bộ nhớ đồ thị hay không + "force_restarted": true // có phải là chạy lại bắt buộc hay không } } """ @@ -1491,98 +1493,98 @@ def start_simulation(): if not simulation_id: return jsonify({ "success": False, - "error": "请提供 simulation_id" + "error": "Please provide simulation_id" }), 400 platform = data.get('platform', 'parallel') - max_rounds = data.get('max_rounds') # 可选:最大模拟轮数 - enable_graph_memory_update = data.get('enable_graph_memory_update', False) # 可选:是否启用图谱记忆更新 - force = data.get('force', False) # 可选:强制重新开始 + max_rounds = data.get('max_rounds') # Tùy chọn: số vòng mô phỏng tối đa + enable_graph_memory_update = data.get('enable_graph_memory_update', False) # Tùy chọn: có bật cập nhật bộ nhớ đồ thị hay không + force = data.get('force', False) # Tùy chọn: buộc chạy lại - # 验证 max_rounds 参数 + # Kiểm tra tham số max_rounds if max_rounds is not None: try: max_rounds = int(max_rounds) if max_rounds <= 0: return jsonify({ "success": False, - "error": "max_rounds 必须是正整数" + "error": "max_rounds must be a positive integer" }), 400 except (ValueError, TypeError): return jsonify({ "success": False, - "error": "max_rounds 必须是有效的整数" + "error": "max_rounds must be a valid integer" }), 400 if platform not in ['twitter', 'reddit', 'parallel']: return jsonify({ "success": False, - "error": f"无效的平台类型: {platform},可选: twitter/reddit/parallel" + "error": f"Invalid platform type: {platform}. Allowed: twitter/reddit/parallel" }), 400 - # 检查模拟是否已准备好 + # Kiểm tra mô phỏng đã sẵn sàng hay chưa manager = SimulationManager() state = manager.get_simulation(simulation_id) if not state: return jsonify({ "success": False, - "error": f"模拟不存在: {simulation_id}" + "error": f"Simulation does not exist: {simulation_id}" }), 404 force_restarted = False - # 智能处理状态:如果准备工作已完成,允许重新启动 + # Xử lý trạng thái thông minh: nếu chuẩn bị đã hoàn tất thì cho phép khởi động lại if state.status != SimulationStatus.READY: - # 检查准备工作是否已完成 + # Kiểm tra phần chuẩn bị đã hoàn tất chưa is_prepared, prepare_info = _check_simulation_prepared(simulation_id) if is_prepared: - # 准备工作已完成,检查是否有正在运行的进程 + # Chuẩn bị đã hoàn tất, kiểm tra có tiến trình đang chạy hay không if state.status == SimulationStatus.RUNNING: - # 检查模拟进程是否真的在运行 + # Kiểm tra tiến trình mô phỏng có thực sự đang chạy không run_state = SimulationRunner.get_run_state(simulation_id) if run_state and run_state.runner_status.value == "running": - # 进程确实在运行 + # Tiến trình thực sự đang chạy if force: - # 强制模式:停止运行中的模拟 - logger.info(f"强制模式:停止运行中的模拟 {simulation_id}") + # Chế độ force: dừng mô phỏng đang chạy + logger.info(f"Force mode: stopping running simulation {simulation_id}") try: SimulationRunner.stop_simulation(simulation_id) except Exception as e: - logger.warning(f"停止模拟时出现警告: {str(e)}") + logger.warning(f"Warning while stopping simulation: {str(e)}") else: return jsonify({ "success": False, - "error": f"模拟正在运行中,请先调用 /stop 接口停止,或使用 force=true 强制重新开始" + "error": "Simulation is running. Please call /stop first, or use force=true to restart" }), 400 - # 如果是强制模式,清理运行日志 + # Nếu là chế độ force thì dọn log chạy if force: - logger.info(f"强制模式:清理模拟日志 {simulation_id}") + logger.info(f"Force mode: cleaning simulation logs {simulation_id}") cleanup_result = SimulationRunner.cleanup_simulation_logs(simulation_id) if not cleanup_result.get("success"): - logger.warning(f"清理日志时出现警告: {cleanup_result.get('errors')}") + logger.warning(f"Warning while cleaning logs: {cleanup_result.get('errors')}") force_restarted = True - # 进程不存在或已结束,重置状态为 ready - logger.info(f"模拟 {simulation_id} 准备工作已完成,重置状态为 ready(原状态: {state.status.value})") + # Tiến trình không tồn tại hoặc đã kết thúc, đặt lại trạng thái về ready + logger.info(f"Simulation {simulation_id} is prepared, resetting status to ready (previous: {state.status.value})") state.status = SimulationStatus.READY manager._save_simulation_state(state) else: - # 准备工作未完成 + # Chuẩn bị chưa hoàn tất return jsonify({ "success": False, - "error": f"模拟未准备好,当前状态: {state.status.value},请先调用 /prepare 接口" + "error": f"Simulation is not ready. Current status: {state.status.value}. Please call /prepare first" }), 400 - # 获取图谱ID(用于图谱记忆更新) + # Lấy graph ID (dùng cho cập nhật bộ nhớ đồ thị) graph_id = None if enable_graph_memory_update: - # 从模拟状态或项目中获取 graph_id + # Lấy graph_id từ trạng thái mô phỏng hoặc từ dự án graph_id = state.graph_id if not graph_id: - # 尝试从项目中获取 + # Thử lấy từ dự án project = ProjectManager.get_project(state.project_id) if project: graph_id = project.graph_id @@ -1590,12 +1592,12 @@ def start_simulation(): if not graph_id: return jsonify({ "success": False, - "error": "启用图谱记忆更新需要有效的 graph_id,请确保项目已构建图谱" + "error": "Enabling graph memory update requires a valid graph_id. Please ensure the project graph is built" }), 400 - logger.info(f"启用图谱记忆更新: simulation_id={simulation_id}, graph_id={graph_id}") + logger.info(f"Graph memory update enabled: simulation_id={simulation_id}, graph_id={graph_id}") - # 启动模拟 + # Khởi chạy mô phỏng run_state = SimulationRunner.start_simulation( simulation_id=simulation_id, platform=platform, @@ -1604,7 +1606,7 @@ def start_simulation(): graph_id=graph_id ) - # 更新模拟状态 + # Cập nhật trạng thái mô phỏng state.status = SimulationStatus.RUNNING manager._save_simulation_state(state) @@ -1628,7 +1630,7 @@ def start_simulation(): }), 400 except Exception as e: - logger.error(f"启动模拟失败: {str(e)}") + logger.error(f"Failed to start simulation: {str(e)}") return jsonify({ "success": False, "error": str(e), @@ -1639,14 +1641,14 @@ def start_simulation(): @simulation_bp.route('/stop', methods=['POST']) def stop_simulation(): """ - 停止模拟 + Dừng mô phỏng. - 请求(JSON): + Yêu cầu (JSON): { - "simulation_id": "sim_xxxx" // 必填,模拟ID + "simulation_id": "sim_xxxx" // bắt buộc, simulation ID } - 返回: + Trả về: { "success": true, "data": { @@ -1663,12 +1665,12 @@ def stop_simulation(): if not simulation_id: return jsonify({ "success": False, - "error": "请提供 simulation_id" + "error": "Please provide simulation_id" }), 400 run_state = SimulationRunner.stop_simulation(simulation_id) - # 更新模拟状态 + # Cập nhật trạng thái mô phỏng manager = SimulationManager() state = manager.get_simulation(simulation_id) if state: @@ -1687,7 +1689,7 @@ def stop_simulation(): }), 400 except Exception as e: - logger.error(f"停止模拟失败: {str(e)}") + logger.error(f"Failed to stop simulation: {str(e)}") return jsonify({ "success": False, "error": str(e), @@ -1695,14 +1697,14 @@ def stop_simulation(): }), 500 -# ============== 实时状态监控接口 ============== +# ============== API giám sát trạng thái thời gian thực ============== @simulation_bp.route('//run-status', methods=['GET']) def get_run_status(simulation_id: str): """ - 获取模拟运行实时状态(用于前端轮询) + Lấy trạng thái chạy mô phỏng theo thời gian thực (dùng cho frontend polling). - 返回: + Trả về: { "success": true, "data": { @@ -1747,7 +1749,7 @@ def get_run_status(simulation_id: str): }) except Exception as e: - logger.error(f"获取运行状态失败: {str(e)}") + logger.error(f"Failed to get run status: {str(e)}") return jsonify({ "success": False, "error": str(e), @@ -1758,14 +1760,14 @@ def get_run_status(simulation_id: str): @simulation_bp.route('//run-status/detail', methods=['GET']) def get_run_status_detail(simulation_id: str): """ - 获取模拟运行详细状态(包含所有动作) + Lấy trạng thái chạy mô phỏng chi tiết (bao gồm toàn bộ hành động). - 用于前端展示实时动态 + Dùng cho frontend hiển thị diễn biến thời gian thực. - Query参数: - platform: 过滤平台(twitter/reddit,可选) + Tham số query: + platform: Lọc theo nền tảng (twitter/reddit, tùy chọn) - 返回: + Trả về: { "success": true, "data": { @@ -1787,8 +1789,8 @@ def get_run_status_detail(simulation_id: str): }, ... ], - "twitter_actions": [...], # Twitter 平台的所有动作 - "reddit_actions": [...] # Reddit 平台的所有动作 + "twitter_actions": [...], # toàn bộ hành động trên Twitter + "reddit_actions": [...] # toàn bộ hành động trên Reddit } } """ @@ -1808,13 +1810,13 @@ def get_run_status_detail(simulation_id: str): } }) - # 获取完整的动作列表 + # Lấy danh sách hành động đầy đủ all_actions = SimulationRunner.get_all_actions( simulation_id=simulation_id, platform=platform_filter ) - # 分平台获取动作 + # Lấy hành động theo từng nền tảng twitter_actions = SimulationRunner.get_all_actions( simulation_id=simulation_id, platform="twitter" @@ -1825,7 +1827,7 @@ def get_run_status_detail(simulation_id: str): platform="reddit" ) if not platform_filter or platform_filter == "reddit" else [] - # 获取当前轮次的动作(recent_actions 只展示最新一轮) + # Lấy hành động của vòng hiện tại (recent_actions chỉ hiển thị vòng mới nhất) current_round = run_state.current_round recent_actions = SimulationRunner.get_all_actions( simulation_id=simulation_id, @@ -1833,13 +1835,13 @@ def get_run_status_detail(simulation_id: str): round_num=current_round ) if current_round > 0 else [] - # 获取基础状态信息 + # Lấy thông tin trạng thái cơ bản result = run_state.to_dict() result["all_actions"] = [a.to_dict() for a in all_actions] result["twitter_actions"] = [a.to_dict() for a in twitter_actions] result["reddit_actions"] = [a.to_dict() for a in reddit_actions] result["rounds_count"] = len(run_state.rounds) - # recent_actions 只展示当前最新一轮两个平台的内容 + # recent_actions chỉ hiển thị nội dung của vòng mới nhất trên hai nền tảng result["recent_actions"] = [a.to_dict() for a in recent_actions] return jsonify({ @@ -1848,7 +1850,7 @@ def get_run_status_detail(simulation_id: str): }) except Exception as e: - logger.error(f"获取详细状态失败: {str(e)}") + logger.error(f"Failed to get detailed run status: {str(e)}") return jsonify({ "success": False, "error": str(e), @@ -1859,16 +1861,16 @@ def get_run_status_detail(simulation_id: str): @simulation_bp.route('//actions', methods=['GET']) def get_simulation_actions(simulation_id: str): """ - 获取模拟中的Agent动作历史 + Lấy lịch sử hành động Agent trong mô phỏng. - Query参数: - limit: 返回数量(默认100) - offset: 偏移量(默认0) - platform: 过滤平台(twitter/reddit) - agent_id: 过滤Agent ID - round_num: 过滤轮次 + Tham số query: + limit: Số lượng trả về (mặc định 100) + offset: Độ lệch (mặc định 0) + platform: Lọc nền tảng (twitter/reddit) + agent_id: Lọc theo Agent ID + round_num: Lọc theo vòng - 返回: + Trả về: { "success": true, "data": { @@ -1902,7 +1904,7 @@ def get_simulation_actions(simulation_id: str): }) except Exception as e: - logger.error(f"获取动作历史失败: {str(e)}") + logger.error(f"Failed to get action history: {str(e)}") return jsonify({ "success": False, "error": str(e), @@ -1913,15 +1915,15 @@ def get_simulation_actions(simulation_id: str): @simulation_bp.route('//timeline', methods=['GET']) def get_simulation_timeline(simulation_id: str): """ - 获取模拟时间线(按轮次汇总) + Lấy timeline mô phỏng (tổng hợp theo vòng). - 用于前端展示进度条和时间线视图 + Dùng cho frontend hiển thị thanh tiến độ và timeline. - Query参数: - start_round: 起始轮次(默认0) - end_round: 结束轮次(默认全部) + Tham số query: + start_round: Vòng bắt đầu (mặc định 0) + end_round: Vòng kết thúc (mặc định toàn bộ) - 返回每轮的汇总信息 + Trả về thông tin tổng hợp theo từng vòng. """ try: start_round = request.args.get('start_round', 0, type=int) @@ -1942,7 +1944,7 @@ def get_simulation_timeline(simulation_id: str): }) except Exception as e: - logger.error(f"获取时间线失败: {str(e)}") + logger.error(f"Failed to get timeline: {str(e)}") return jsonify({ "success": False, "error": str(e), @@ -1953,9 +1955,9 @@ def get_simulation_timeline(simulation_id: str): @simulation_bp.route('//agent-stats', methods=['GET']) def get_agent_stats(simulation_id: str): """ - 获取每个Agent的统计信息 + Lấy thống kê của từng Agent. - 用于前端展示Agent活跃度排行、动作分布等 + Dùng cho frontend hiển thị bảng xếp hạng mức độ hoạt động và phân bố hành động của Agent. """ try: stats = SimulationRunner.get_agent_stats(simulation_id) @@ -1969,7 +1971,7 @@ def get_agent_stats(simulation_id: str): }) except Exception as e: - logger.error(f"获取Agent统计失败: {str(e)}") + logger.error(f"Failed to get agent statistics: {str(e)}") return jsonify({ "success": False, "error": str(e), @@ -1977,19 +1979,19 @@ def get_agent_stats(simulation_id: str): }), 500 -# ============== 数据库查询接口 ============== +# ============== API truy vấn cơ sở dữ liệu ============== @simulation_bp.route('//posts', methods=['GET']) def get_simulation_posts(simulation_id: str): """ - 获取模拟中的帖子 + Lấy bài đăng trong mô phỏng. - Query参数: - platform: 平台类型(twitter/reddit) - limit: 返回数量(默认50) - offset: 偏移量 + Tham số query: + platform: Loại nền tảng (twitter/reddit) + limit: Số lượng trả về (mặc định 50) + offset: Độ lệch - 返回帖子列表(从SQLite数据库读取) + Trả về danh sách bài đăng (đọc từ cơ sở dữ liệu SQLite). """ try: platform = request.args.get('platform', 'reddit') @@ -2011,7 +2013,7 @@ def get_simulation_posts(simulation_id: str): "platform": platform, "count": 0, "posts": [], - "message": "数据库不存在,模拟可能尚未运行" + "message": "Database does not exist. The simulation may not have run yet" } }) @@ -2049,7 +2051,7 @@ def get_simulation_posts(simulation_id: str): }) except Exception as e: - logger.error(f"获取帖子失败: {str(e)}") + logger.error(f"Failed to get posts: {str(e)}") return jsonify({ "success": False, "error": str(e), @@ -2060,12 +2062,12 @@ def get_simulation_posts(simulation_id: str): @simulation_bp.route('//comments', methods=['GET']) def get_simulation_comments(simulation_id: str): """ - 获取模拟中的评论(仅Reddit) + Lấy bình luận trong mô phỏng (chỉ Reddit). - Query参数: - post_id: 过滤帖子ID(可选) - limit: 返回数量 - offset: 偏移量 + Tham số query: + post_id: Lọc theo post ID (tùy chọn) + limit: Số lượng trả về + offset: Độ lệch """ try: post_id = request.args.get('post_id') @@ -2124,7 +2126,7 @@ def get_simulation_comments(simulation_id: str): }) except Exception as e: - logger.error(f"获取评论失败: {str(e)}") + logger.error(f"Failed to get comments: {str(e)}") return jsonify({ "success": False, "error": str(e), @@ -2132,31 +2134,32 @@ def get_simulation_comments(simulation_id: str): }), 500 -# ============== Interview 采访接口 ============== +# ============== Interview API ============== @simulation_bp.route('/interview', methods=['POST']) def interview_agent(): """ - 采访单个Agent + Phỏng vấn một Agent. - 注意:此功能需要模拟环境处于运行状态(完成模拟循环后进入等待命令模式) + Lưu ý: chức năng này yêu cầu môi trường mô phỏng đang chạy + (sau khi hoàn tất vòng mô phỏng, hệ thống vào chế độ chờ lệnh). - 请求(JSON): + Yêu cầu (JSON): { - "simulation_id": "sim_xxxx", // 必填,模拟ID - "agent_id": 0, // 必填,Agent ID - "prompt": "你对这件事有什么看法?", // 必填,采访问题 - "platform": "twitter", // 可选,指定平台(twitter/reddit) - // 不指定时:双平台模拟同时采访两个平台 - "timeout": 60 // 可选,超时时间(秒),默认60 + "simulation_id": "sim_xxxx", // Bắt buộc, simulation ID + "agent_id": 0, // Bắt buộc, Agent ID + "prompt": "Bạn nghĩ gì về vấn đề này?", // Bắt buộc, câu hỏi phỏng vấn + "platform": "twitter", // Tùy chọn, chỉ định nền tảng (twitter/reddit) + // Nếu không chỉ định: mô phỏng 2 nền tảng sẽ phỏng vấn cả hai + "timeout": 60 // Tùy chọn, timeout (giây), mặc định 60 } - 返回(不指定platform,双平台模式): + Trả về (không chỉ định `platform`, chế độ hai nền tảng): { "success": true, "data": { "agent_id": 0, - "prompt": "你对这件事有什么看法?", + "prompt": "Bạn nghĩ gì về vấn đề này?", "result": { "agent_id": 0, "prompt": "...", @@ -2169,15 +2172,15 @@ def interview_agent(): } } - 返回(指定platform): + Trả về (có chỉ định `platform`): { "success": true, "data": { "agent_id": 0, - "prompt": "你对这件事有什么看法?", + "prompt": "Bạn nghĩ gì về vấn đề này?", "result": { "agent_id": 0, - "response": "我认为...", + "response": "Tôi nghĩ...", "platform": "twitter", "timestamp": "2025-12-08T10:00:00" }, @@ -2191,42 +2194,42 @@ def interview_agent(): simulation_id = data.get('simulation_id') agent_id = data.get('agent_id') prompt = data.get('prompt') - platform = data.get('platform') # 可选:twitter/reddit/None + platform = data.get('platform') # Tùy chọn: twitter/reddit/None timeout = data.get('timeout', 60) if not simulation_id: return jsonify({ "success": False, - "error": "请提供 simulation_id" + "error": "Please provide simulation_id" }), 400 if agent_id is None: return jsonify({ "success": False, - "error": "请提供 agent_id" + "error": "Please provide agent_id" }), 400 if not prompt: return jsonify({ "success": False, - "error": "请提供 prompt(采访问题)" + "error": "Please provide prompt (interview question)" }), 400 - # 验证platform参数 + # Validate platform parameter if platform and platform not in ("twitter", "reddit"): return jsonify({ "success": False, - "error": "platform 参数只能是 'twitter' 或 'reddit'" + "error": "platform must be 'twitter' or 'reddit'" }), 400 - # 检查环境状态 + # Check environment status if not SimulationRunner.check_env_alive(simulation_id): return jsonify({ "success": False, - "error": "模拟环境未运行或已关闭。请确保模拟已完成并进入等待命令模式。" + "error": "Simulation environment is not running or has been closed. Ensure simulation has completed and entered command-wait mode." }), 400 - # 优化prompt,添加前缀避免Agent调用工具 + # Optimize prompt by adding a prefix to avoid Agent tool calls optimized_prompt = optimize_interview_prompt(prompt) result = SimulationRunner.interview_agent( @@ -2251,11 +2254,11 @@ def interview_agent(): except TimeoutError as e: return jsonify({ "success": False, - "error": f"等待Interview响应超时: {str(e)}" + "error": f"Timeout waiting for interview response: {str(e)}" }), 504 except Exception as e: - logger.error(f"Interview失败: {str(e)}") + logger.error(f"Interview failed: {str(e)}") return jsonify({ "success": False, "error": str(e), @@ -2266,30 +2269,30 @@ def interview_agent(): @simulation_bp.route('/interview/batch', methods=['POST']) def interview_agents_batch(): """ - 批量采访多个Agent + Phỏng vấn hàng loạt nhiều Agent. - 注意:此功能需要模拟环境处于运行状态 + Lưu ý: chức năng này yêu cầu môi trường mô phỏng đang chạy. - 请求(JSON): + Yêu cầu (JSON): { - "simulation_id": "sim_xxxx", // 必填,模拟ID - "interviews": [ // 必填,采访列表 + "simulation_id": "sim_xxxx", // Bắt buộc, simulation ID + "interviews": [ // Bắt buộc, danh sách phỏng vấn { "agent_id": 0, - "prompt": "你对A有什么看法?", - "platform": "twitter" // 可选,指定该Agent的采访平台 + "prompt": "Bạn nghĩ gì về A?", + "platform": "twitter" // Tùy chọn, chỉ định nền tảng cho Agent này }, { "agent_id": 1, - "prompt": "你对B有什么看法?" // 不指定platform则使用默认值 + "prompt": "Bạn nghĩ gì về B?" // Nếu không chỉ định `platform`, dùng giá trị mặc định } ], - "platform": "reddit", // 可选,默认平台(被每项的platform覆盖) - // 不指定时:双平台模拟每个Agent同时采访两个平台 - "timeout": 120 // 可选,超时时间(秒),默认120 + "platform": "reddit", // Tùy chọn, nền tảng mặc định (bị ghi đè bởi `platform` của từng mục) + // Nếu không chỉ định: mô phỏng 2 nền tảng sẽ phỏng vấn cả hai nền tảng cho mỗi Agent + "timeout": 120 // Tùy chọn, timeout (giây), mặc định 120 } - 返回: + Trả về: { "success": true, "data": { @@ -2312,56 +2315,56 @@ def interview_agents_batch(): simulation_id = data.get('simulation_id') interviews = data.get('interviews') - platform = data.get('platform') # 可选:twitter/reddit/None + platform = data.get('platform') # Tùy chọn: twitter/reddit/None timeout = data.get('timeout', 120) if not simulation_id: return jsonify({ "success": False, - "error": "请提供 simulation_id" + "error": "Please provide simulation_id" }), 400 if not interviews or not isinstance(interviews, list): return jsonify({ "success": False, - "error": "请提供 interviews(采访列表)" + "error": "Please provide interviews (interview list)" }), 400 - # 验证platform参数 + # Validate platform parameter if platform and platform not in ("twitter", "reddit"): return jsonify({ "success": False, - "error": "platform 参数只能是 'twitter' 或 'reddit'" + "error": "platform must be 'twitter' or 'reddit'" }), 400 - # 验证每个采访项 + # Validate each interview item for i, interview in enumerate(interviews): if 'agent_id' not in interview: return jsonify({ "success": False, - "error": f"采访列表第{i+1}项缺少 agent_id" + "error": f"Interview item {i+1} is missing agent_id" }), 400 if 'prompt' not in interview: return jsonify({ "success": False, - "error": f"采访列表第{i+1}项缺少 prompt" + "error": f"Interview item {i+1} is missing prompt" }), 400 - # 验证每项的platform(如果有) + # Validate platform in each item (if provided) item_platform = interview.get('platform') if item_platform and item_platform not in ("twitter", "reddit"): return jsonify({ "success": False, - "error": f"采访列表第{i+1}项的platform只能是 'twitter' 或 'reddit'" + "error": f"platform in interview item {i+1} must be 'twitter' or 'reddit'" }), 400 - # 检查环境状态 + # Check environment status if not SimulationRunner.check_env_alive(simulation_id): return jsonify({ "success": False, - "error": "模拟环境未运行或已关闭。请确保模拟已完成并进入等待命令模式。" + "error": "Simulation environment is not running or has been closed. Ensure simulation has completed and entered command-wait mode." }), 400 - # 优化每个采访项的prompt,添加前缀避免Agent调用工具 + # Optimize prompts in each interview item to avoid Agent tool calls optimized_interviews = [] for interview in interviews: optimized_interview = interview.copy() @@ -2389,11 +2392,11 @@ def interview_agents_batch(): except TimeoutError as e: return jsonify({ "success": False, - "error": f"等待批量Interview响应超时: {str(e)}" + "error": f"Timeout waiting for batch interview response: {str(e)}" }), 504 except Exception as e: - logger.error(f"批量Interview失败: {str(e)}") + logger.error(f"Batch interview failed: {str(e)}") return jsonify({ "success": False, "error": str(e), @@ -2404,20 +2407,20 @@ def interview_agents_batch(): @simulation_bp.route('/interview/all', methods=['POST']) def interview_all_agents(): """ - 全局采访 - 使用相同问题采访所有Agent + Phỏng vấn toàn cục - dùng cùng một câu hỏi để phỏng vấn tất cả Agent. - 注意:此功能需要模拟环境处于运行状态 + Lưu ý: chức năng này yêu cầu môi trường mô phỏng đang chạy. - 请求(JSON): + Yêu cầu (JSON): { - "simulation_id": "sim_xxxx", // 必填,模拟ID - "prompt": "你对这件事整体有什么看法?", // 必填,采访问题(所有Agent使用相同问题) - "platform": "reddit", // 可选,指定平台(twitter/reddit) - // 不指定时:双平台模拟每个Agent同时采访两个平台 - "timeout": 180 // 可选,超时时间(秒),默认180 + "simulation_id": "sim_xxxx", // Bắt buộc, simulation ID + "prompt": "Bạn có quan điểm tổng thể gì về vấn đề này?", // Bắt buộc, câu hỏi phỏng vấn (mọi Agent dùng cùng câu hỏi) + "platform": "reddit", // Tùy chọn, chỉ định nền tảng (twitter/reddit) + // Nếu không chỉ định: mô phỏng 2 nền tảng sẽ phỏng vấn cả hai nền tảng cho mỗi Agent + "timeout": 180 // Tùy chọn, timeout (giây), mặc định 180 } - 返回: + Trả về: { "success": true, "data": { @@ -2439,36 +2442,36 @@ def interview_all_agents(): simulation_id = data.get('simulation_id') prompt = data.get('prompt') - platform = data.get('platform') # 可选:twitter/reddit/None + platform = data.get('platform') # Tùy chọn: twitter/reddit/None timeout = data.get('timeout', 180) if not simulation_id: return jsonify({ "success": False, - "error": "请提供 simulation_id" + "error": "Please provide simulation_id" }), 400 if not prompt: return jsonify({ "success": False, - "error": "请提供 prompt(采访问题)" + "error": "Please provide prompt (interview question)" }), 400 - # 验证platform参数 + # Validate platform parameter if platform and platform not in ("twitter", "reddit"): return jsonify({ "success": False, - "error": "platform 参数只能是 'twitter' 或 'reddit'" + "error": "platform must be 'twitter' or 'reddit'" }), 400 - # 检查环境状态 + # Check environment status if not SimulationRunner.check_env_alive(simulation_id): return jsonify({ "success": False, - "error": "模拟环境未运行或已关闭。请确保模拟已完成并进入等待命令模式。" + "error": "Simulation environment is not running or has been closed. Ensure simulation has completed and entered command-wait mode." }), 400 - # 优化prompt,添加前缀避免Agent调用工具 + # Optimize prompt by adding a prefix to avoid Agent tool calls optimized_prompt = optimize_interview_prompt(prompt) result = SimulationRunner.interview_all_agents( @@ -2492,11 +2495,11 @@ def interview_all_agents(): except TimeoutError as e: return jsonify({ "success": False, - "error": f"等待全局Interview响应超时: {str(e)}" + "error": f"Timeout waiting for global interview response: {str(e)}" }), 504 except Exception as e: - logger.error(f"全局Interview失败: {str(e)}") + logger.error(f"Global interview failed: {str(e)}") return jsonify({ "success": False, "error": str(e), @@ -2507,20 +2510,20 @@ def interview_all_agents(): @simulation_bp.route('/interview/history', methods=['POST']) def get_interview_history(): """ - 获取Interview历史记录 + Lấy lịch sử Interview. - 从模拟数据库中读取所有Interview记录 + Đọc toàn bộ bản ghi Interview từ cơ sở dữ liệu mô phỏng. - 请求(JSON): + Yêu cầu (JSON): { - "simulation_id": "sim_xxxx", // 必填,模拟ID - "platform": "reddit", // 可选,平台类型(reddit/twitter) - // 不指定则返回两个平台的所有历史 - "agent_id": 0, // 可选,只获取该Agent的采访历史 - "limit": 100 // 可选,返回数量,默认100 + "simulation_id": "sim_xxxx", // Bắt buộc, simulation ID + "platform": "reddit", // Tùy chọn, loại nền tảng (reddit/twitter) + // Nếu không chỉ định, trả về toàn bộ lịch sử của cả hai nền tảng + "agent_id": 0, // Tùy chọn, chỉ lấy lịch sử phỏng vấn của Agent này + "limit": 100 // Tùy chọn, số lượng trả về, mặc định 100 } - 返回: + Trả về: { "success": true, "data": { @@ -2528,8 +2531,8 @@ def get_interview_history(): "history": [ { "agent_id": 0, - "response": "我认为...", - "prompt": "你对这件事有什么看法?", + "response": "Tôi nghĩ...", + "prompt": "Bạn nghĩ gì về vấn đề này?", "timestamp": "2025-12-08T10:00:00", "platform": "reddit" }, @@ -2542,14 +2545,14 @@ def get_interview_history(): data = request.get_json() or {} simulation_id = data.get('simulation_id') - platform = data.get('platform') # 不指定则返回两个平台的历史 + platform = data.get('platform') # If omitted, return history from both platforms agent_id = data.get('agent_id') limit = data.get('limit', 100) if not simulation_id: return jsonify({ "success": False, - "error": "请提供 simulation_id" + "error": "Please provide simulation_id" }), 400 history = SimulationRunner.get_interview_history( @@ -2568,7 +2571,7 @@ def get_interview_history(): }) except Exception as e: - logger.error(f"获取Interview历史失败: {str(e)}") + logger.error(f"Failed to get interview history: {str(e)}") return jsonify({ "success": False, "error": str(e), @@ -2579,16 +2582,16 @@ def get_interview_history(): @simulation_bp.route('/env-status', methods=['POST']) def get_env_status(): """ - 获取模拟环境状态 + Lấy trạng thái môi trường mô phỏng. - 检查模拟环境是否存活(可以接收Interview命令) + Kiểm tra môi trường mô phỏng còn hoạt động không (có thể nhận lệnh Interview). - 请求(JSON): + Yêu cầu (JSON): { - "simulation_id": "sim_xxxx" // 必填,模拟ID + "simulation_id": "sim_xxxx" // Bắt buộc, simulation ID } - 返回: + Trả về: { "success": true, "data": { @@ -2596,7 +2599,7 @@ def get_env_status(): "env_alive": true, "twitter_available": true, "reddit_available": true, - "message": "环境正在运行,可以接收Interview命令" + "message": "Environment is running and can receive Interview commands" } } """ @@ -2608,18 +2611,18 @@ def get_env_status(): if not simulation_id: return jsonify({ "success": False, - "error": "请提供 simulation_id" + "error": "Please provide simulation_id" }), 400 env_alive = SimulationRunner.check_env_alive(simulation_id) - # 获取更详细的状态信息 + # Get more detailed status information env_status = SimulationRunner.get_env_status_detail(simulation_id) if env_alive: - message = "环境正在运行,可以接收Interview命令" + message = "Environment is running and can receive Interview commands" else: - message = "环境未运行或已关闭" + message = "Environment is not running or has been closed" return jsonify({ "success": True, @@ -2633,7 +2636,7 @@ def get_env_status(): }) except Exception as e: - logger.error(f"获取环境状态失败: {str(e)}") + logger.error(f"Failed to get environment status: {str(e)}") return jsonify({ "success": False, "error": str(e), @@ -2644,24 +2647,24 @@ def get_env_status(): @simulation_bp.route('/close-env', methods=['POST']) def close_simulation_env(): """ - 关闭模拟环境 + Đóng môi trường mô phỏng. - 向模拟发送关闭环境命令,使其优雅退出等待命令模式。 + Gửi lệnh đóng môi trường tới mô phỏng để thoát chế độ chờ lệnh một cách graceful. - 注意:这不同于 /stop 接口,/stop 会强制终止进程, - 而此接口会让模拟优雅地关闭环境并退出。 + Lưu ý: API này khác với `/stop`; `/stop` sẽ buộc dừng tiến trình, + còn API này sẽ cho mô phỏng đóng môi trường và thoát một cách graceful. - 请求(JSON): + Yêu cầu (JSON): { - "simulation_id": "sim_xxxx", // 必填,模拟ID - "timeout": 30 // 可选,超时时间(秒),默认30 + "simulation_id": "sim_xxxx", // Bắt buộc, simulation ID + "timeout": 30 // Tùy chọn, timeout (giây), mặc định 30 } - 返回: + Trả về: { "success": true, "data": { - "message": "环境关闭命令已发送", + "message": "Environment close command has been sent", "result": {...}, "timestamp": "2025-12-08T10:00:01" } @@ -2676,7 +2679,7 @@ def close_simulation_env(): if not simulation_id: return jsonify({ "success": False, - "error": "请提供 simulation_id" + "error": "Please provide simulation_id" }), 400 result = SimulationRunner.close_simulation_env( @@ -2684,7 +2687,7 @@ def close_simulation_env(): timeout=timeout ) - # 更新模拟状态 + # Update simulation status manager = SimulationManager() state = manager.get_simulation(simulation_id) if state: @@ -2703,9 +2706,9 @@ def close_simulation_env(): }), 400 except Exception as e: - logger.error(f"关闭环境失败: {str(e)}") + logger.error(f"Failed to close environment: {str(e)}") return jsonify({ "success": False, "error": str(e), "traceback": traceback.format_exc() - }), 500 + }), 500 \ No newline at end of file diff --git a/backend/app/config.py b/backend/app/config.py index 953dfa50a..c2cb4a24a 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -1,54 +1,54 @@ """ -配置管理 -统一从项目根目录的 .env 文件加载配置 +Quản lý cấu hình +Tải cấu hình thống nhất từ tệp .env ở thư mục gốc dự án """ import os from dotenv import load_dotenv -# 加载项目根目录的 .env 文件 -# 路径: MiroFish/.env (相对于 backend/app/config.py) +# Tải tệp .env ở thư mục gốc dự án +# Đường dẫn: MiroFish/.env (tương đối với backend/app/config.py) project_root_env = os.path.join(os.path.dirname(__file__), '../../.env') if os.path.exists(project_root_env): load_dotenv(project_root_env, override=True) else: - # 如果根目录没有 .env,尝试加载环境变量(用于生产环境) + # Nếu thư mục gốc không có .env, thử nạp biến môi trường sẵn có (cho production) load_dotenv(override=True) class Config: - """Flask配置类""" + """Lớp cấu hình cho Flask""" - # Flask配置 + # Cấu hình Flask SECRET_KEY = os.environ.get('SECRET_KEY', 'mirofish-secret-key') DEBUG = os.environ.get('FLASK_DEBUG', 'True').lower() == 'true' - # JSON配置 - 禁用ASCII转义,让中文直接显示(而不是 \uXXXX 格式) + # Cấu hình JSON - tắt escape ASCII để tiếng Trung hiển thị trực tiếp (thay vì dạng \uXXXX) JSON_AS_ASCII = False - # LLM配置(统一使用OpenAI格式) + # Cấu hình LLM (thống nhất theo định dạng OpenAI) LLM_API_KEY = os.environ.get('LLM_API_KEY') LLM_BASE_URL = os.environ.get('LLM_BASE_URL', 'https://api.openai.com/v1') LLM_MODEL_NAME = os.environ.get('LLM_MODEL_NAME', 'gpt-4o-mini') - # Zep配置 + # Cấu hình Zep ZEP_API_KEY = os.environ.get('ZEP_API_KEY') - # 文件上传配置 + # Cấu hình upload tệp MAX_CONTENT_LENGTH = 50 * 1024 * 1024 # 50MB UPLOAD_FOLDER = os.path.join(os.path.dirname(__file__), '../uploads') ALLOWED_EXTENSIONS = {'pdf', 'md', 'txt', 'markdown'} - # 文本处理配置 - DEFAULT_CHUNK_SIZE = 500 # 默认切块大小 - DEFAULT_CHUNK_OVERLAP = 50 # 默认重叠大小 + # Cấu hình xử lý văn bản + DEFAULT_CHUNK_SIZE = 500 # Kích thước chunk mặc định + DEFAULT_CHUNK_OVERLAP = 50 # Độ chồng lấp mặc định - # OASIS模拟配置 + # Cấu hình mô phỏng OASIS OASIS_DEFAULT_MAX_ROUNDS = int(os.environ.get('OASIS_DEFAULT_MAX_ROUNDS', '10')) OASIS_SIMULATION_DATA_DIR = os.path.join(os.path.dirname(__file__), '../uploads/simulations') - # OASIS平台可用动作配置 + # Cấu hình action khả dụng theo nền tảng OASIS OASIS_TWITTER_ACTIONS = [ 'CREATE_POST', 'LIKE_POST', 'REPOST', 'FOLLOW', 'DO_NOTHING', 'QUOTE_POST' ] @@ -58,18 +58,18 @@ class Config: 'TREND', 'REFRESH', 'DO_NOTHING', 'FOLLOW', 'MUTE' ] - # Report Agent配置 + # Cấu hình Report Agent REPORT_AGENT_MAX_TOOL_CALLS = int(os.environ.get('REPORT_AGENT_MAX_TOOL_CALLS', '5')) REPORT_AGENT_MAX_REFLECTION_ROUNDS = int(os.environ.get('REPORT_AGENT_MAX_REFLECTION_ROUNDS', '2')) REPORT_AGENT_TEMPERATURE = float(os.environ.get('REPORT_AGENT_TEMPERATURE', '0.5')) @classmethod def validate(cls): - """验证必要配置""" + """Kiểm tra các cấu hình bắt buộc""" errors = [] if not cls.LLM_API_KEY: - errors.append("LLM_API_KEY 未配置") + errors.append("LLM_API_KEY is not configured") if not cls.ZEP_API_KEY: - errors.append("ZEP_API_KEY 未配置") + errors.append("ZEP_API_KEY is not configured") return errors diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index 55bec6195..8306bdea9 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -1,5 +1,5 @@ """ -数据模型模块 +Mô-đun mô hình dữ liệu """ from .task import TaskManager, TaskStatus diff --git a/backend/app/models/project.py b/backend/app/models/project.py index 089789374..2464eba4a 100644 --- a/backend/app/models/project.py +++ b/backend/app/models/project.py @@ -1,6 +1,7 @@ """ -项目上下文管理 -用于在服务端持久化项目状态,避免前端在接口间传递大量数据 +Quản lý context (ngữ cảnh) của dự án +Được sử dụng để lưu trữ trạng thái dự án trên server (persistence), +giúp tránh việc frontend phải gửi đi một lượng lớn dữ liệu mỗi lần gọi API. """ import os @@ -15,45 +16,45 @@ class ProjectStatus(str, Enum): - """项目状态""" - CREATED = "created" # 刚创建,文件已上传 - ONTOLOGY_GENERATED = "ontology_generated" # 本体已生成 - GRAPH_BUILDING = "graph_building" # 图谱构建中 - GRAPH_COMPLETED = "graph_completed" # 图谱构建完成 - FAILED = "failed" # 失败 + """Trạng thái hiện tại của dự án""" + CREATED = "created" # Dự án vừa được tạo, các file đã được tải lên thành công + ONTOLOGY_GENERATED = "ontology_generated" # Đã hoàn tất khởi tạo Ontology + GRAPH_BUILDING = "graph_building" # Tri thức đồ thị (Knowledge Graph) đang được xây dựng + GRAPH_COMPLETED = "graph_completed" # Đã hoàn thành quá trình Graph + FAILED = "failed" # Thiết lập / Xử lý gặp lỗi @dataclass class Project: - """项目数据模型""" + """Mô hình dữ liệu (Data model) của dự án""" project_id: str name: str status: ProjectStatus created_at: str updated_at: str - # 文件信息 + # File information files: List[Dict[str, str]] = field(default_factory=list) # [{filename, path, size}] total_text_length: int = 0 - # 本体信息(接口1生成后填充) + # Thông tin ontology (được điền sau khi API 1 xử lý xong) ontology: Optional[Dict[str, Any]] = None analysis_summary: Optional[str] = None - # 图谱信息(接口2完成后填充) + # Thông tin graph (được điền sau khi API 2 hoàn thành) graph_id: Optional[str] = None graph_build_task_id: Optional[str] = None - # 配置 + # Cấu hình simulation_requirement: Optional[str] = None chunk_size: int = 500 chunk_overlap: int = 50 - # 错误信息 + # Thông tin lỗi error: Optional[str] = None def to_dict(self) -> Dict[str, Any]: - """转换为字典""" + """Biến đổi đối tượng (Object) thành Dictionary (để dễ dàng chuyển thành JSON và lưu trữ)""" return { "project_id": self.project_id, "name": self.name, @@ -74,7 +75,7 @@ def to_dict(self) -> Dict[str, Any]: @classmethod def from_dict(cls, data: Dict[str, Any]) -> 'Project': - """从字典创建""" + """Khởi tạo một instance Project từ dữ liệu kiểu Dictionary (khi load lên từ hệ thống lưu trữ)""" status = data.get('status', 'created') if isinstance(status, str): status = ProjectStatus(status) @@ -99,46 +100,46 @@ def from_dict(cls, data: Dict[str, Any]) -> 'Project': class ProjectManager: - """项目管理器 - 负责项目的持久化存储和检索""" + """Quản lý các dự án (ProjectManager) - Chịu trách nhiệm lưu trữ và truy xuất thông tin dự án""" - # 项目存储根目录 + # Thư mục gốc để lưu trữ toàn bộ dữ liệu dự án trên máy chủ PROJECTS_DIR = os.path.join(Config.UPLOAD_FOLDER, 'projects') @classmethod def _ensure_projects_dir(cls): - """确保项目目录存在""" + """Đảm bảo thư mục lưu trữ dự án đã được tạo, nếu không có thì self tạo mới""" os.makedirs(cls.PROJECTS_DIR, exist_ok=True) @classmethod def _get_project_dir(cls, project_id: str) -> str: - """获取项目目录路径""" + """Đường dẫn tới thư mục lưu trữ tương ứng với project_id""" return os.path.join(cls.PROJECTS_DIR, project_id) @classmethod def _get_project_meta_path(cls, project_id: str) -> str: - """获取项目元数据文件路径""" + """Đường dẫn lấy file cài đặt metadata (thường là project.json)""" return os.path.join(cls._get_project_dir(project_id), 'project.json') @classmethod def _get_project_files_dir(cls, project_id: str) -> str: - """获取项目文件存储目录""" + """Đường dẫn đến thư mục chứa các file nguyên thuỷ do người dùng kéo thả tải lên cho dự án""" return os.path.join(cls._get_project_dir(project_id), 'files') @classmethod def _get_project_text_path(cls, project_id: str) -> str: - """获取项目提取文本存储路径""" + """Lấy vị trí của tệp văn bản (txt) đã được hệ thống trích xuất nội dung""" return os.path.join(cls._get_project_dir(project_id), 'extracted_text.txt') @classmethod def create_project(cls, name: str = "Unnamed Project") -> Project: """ - 创建新项目 + Khởi tạo và tạo mới cấu trúc dự án trên server Args: - name: 项目名称 + name: Tên của dự án Returns: - 新创建的Project对象 + Project object vừa được tạo """ cls._ensure_projects_dir() @@ -153,20 +154,20 @@ def create_project(cls, name: str = "Unnamed Project") -> Project: updated_at=now ) - # 创建项目目录结构 + # Thiết lập các thư mục con trong không gian thư mục của project project_dir = cls._get_project_dir(project_id) files_dir = cls._get_project_files_dir(project_id) os.makedirs(project_dir, exist_ok=True) os.makedirs(files_dir, exist_ok=True) - # 保存项目元数据 + # Ghi các trường thông tin (metadata) của project vào file cứng cls.save_project(project) return project @classmethod def save_project(cls, project: Project) -> None: - """保存项目元数据""" + """Ghi chồng cấu hình cập nhật (metadata mới) đối của project vào file (Mặc định: project.json) """ project.updated_at = datetime.now().isoformat() meta_path = cls._get_project_meta_path(project.project_id) @@ -176,10 +177,10 @@ def save_project(cls, project: Project) -> None: @classmethod def get_project(cls, project_id: str) -> Optional[Project]: """ - 获取项目 + Get Project Args: - project_id: 项目ID + project_id: Project ID Returns: Project对象,如果不存在返回None @@ -197,13 +198,13 @@ def get_project(cls, project_id: str) -> Optional[Project]: @classmethod def list_projects(cls, limit: int = 50) -> List[Project]: """ - 列出所有项目 + Lấy danh sách tất cả các dự án (projects) đang có trên system Args: - limit: 返回数量限制 + limit: Giới hạn số lượng hiển thị (mặc định lấy 50 project) Returns: - 项目列表,按创建时间倒序 + Danh sách gồm các Object Project, xếp theo ngày/giờ giảm dần (từ mới tạo -> cũ nhất) """ cls._ensure_projects_dir() @@ -213,7 +214,7 @@ def list_projects(cls, limit: int = 50) -> List[Project]: if project: projects.append(project) - # 按创建时间倒序排序 + # Sắp xếp lại lịch sử project theo thứ tự giảm dần thời gian projects.sort(key=lambda p: p.created_at, reverse=True) return projects[:limit] @@ -221,47 +222,47 @@ def list_projects(cls, limit: int = 50) -> List[Project]: @classmethod def delete_project(cls, project_id: str) -> bool: """ - 删除项目及其所有文件 + Xoá vĩnh viễn dữ liệu về project và mọi file liên quan của nó khỏi server Args: - project_id: 项目ID + project_id: Mã ID của Project Returns: - 是否删除成功 + Boolean đại diện cờ Thành công / Thất bại của việc xoá """ project_dir = cls._get_project_dir(project_id) if not os.path.exists(project_dir): return False - shutil.rmtree(project_dir) + shutil.rmtree(project_dir) # Xoá toàn bộ thư mục dữ liệu project_id return True @classmethod def save_file_to_project(cls, project_id: str, file_storage, original_filename: str) -> Dict[str, str]: """ - 保存上传的文件到项目目录 + Ghi dữ liệu file đính kèm mà người dùng upload lên vào kho dự án Args: - project_id: 项目ID - file_storage: Flask的FileStorage对象 - original_filename: 原始文件名 + project_id: Mã định danh của Project + file_storage: Đối tượng Request File (từ framework, VD: của thư viện Flask/FastAPI) chứa nội dung file byte + original_filename: Tên ban đầu từ máy tính người dùng Returns: - 文件信息字典 {filename, path, size} + Object chứa kết quả lưu file mới gồm {tên ban đầu, tên hash được lưu, đường dẫn đầy đủ, dung lượng} """ files_dir = cls._get_project_files_dir(project_id) os.makedirs(files_dir, exist_ok=True) - # 生成安全的文件名 + # Biến đổi tên file thành chuỗi an toàn độc nhất (UUID) để giữ các phiên bản không bị ghi đè, với phần mở rộng ban đầu ext = os.path.splitext(original_filename)[1].lower() safe_filename = f"{uuid.uuid4().hex[:8]}{ext}" file_path = os.path.join(files_dir, safe_filename) - # 保存文件 + # Uỷ quyền lưu vào đường dẫn đích file_storage.save(file_path) - # 获取文件大小 + # Đếm kích thước dung lượng (byte) của tập tin tĩnh tại ổ cứng file_size = os.path.getsize(file_path) return { @@ -273,14 +274,14 @@ def save_file_to_project(cls, project_id: str, file_storage, original_filename: @classmethod def save_extracted_text(cls, project_id: str, text: str) -> None: - """保存提取的文本""" + """Tạo/ghi văn bản trích xuất (từ nội dung phân tích File upload) cho dự án vào folder dữ liệu""" text_path = cls._get_project_text_path(project_id) with open(text_path, 'w', encoding='utf-8') as f: f.write(text) @classmethod def get_extracted_text(cls, project_id: str) -> Optional[str]: - """获取提取的文本""" + """Đọc và lấy nội dung File văn bản được trích xuất nếu có trước đó""" text_path = cls._get_project_text_path(project_id) if not os.path.exists(text_path): @@ -291,7 +292,7 @@ def get_extracted_text(cls, project_id: str) -> Optional[str]: @classmethod def get_project_files(cls, project_id: str) -> List[str]: - """获取项目的所有文件路径""" + """Lấy danh sách link đường dẫn gốc (absolute path) của các files (Tài liệu upload) thuộc dự án này""" files_dir = cls._get_project_files_dir(project_id) if not os.path.exists(files_dir): diff --git a/backend/app/models/task.py b/backend/app/models/task.py index e15f35fbd..757a4d06a 100644 --- a/backend/app/models/task.py +++ b/backend/app/models/task.py @@ -1,6 +1,6 @@ """ -任务状态管理 -用于跟踪长时间运行的任务(如图谱构建) +Quản lý trạng thái Task (tác vụ) +Được sử dụng để theo dõi các tác vụ chạy ngầm mất nhiều thời gian (ví dụ: xây dựng Knowledge Graph) """ import uuid @@ -12,30 +12,30 @@ class TaskStatus(str, Enum): - """任务状态枚举""" - PENDING = "pending" # 等待中 - PROCESSING = "processing" # 处理中 - COMPLETED = "completed" # 已完成 - FAILED = "failed" # 失败 + """Định nghĩa các trạng thái (Enum) mà một Task có thể có""" + PENDING = "pending" # Đang chờ (Task mới được tạo, chưa được xử lý) + PROCESSING = "processing" # Đang xử lý (Hệ thống đang chạy ngầm Task này) + COMPLETED = "completed" # Đã hoàn thành thành công + FAILED = "failed" # Xảy ra lỗi và thất bại @dataclass class Task: - """任务数据类""" + """Lớp dữ liệu lưu trữ thông tin của một Task cụ thể""" task_id: str task_type: str status: TaskStatus created_at: datetime updated_at: datetime - progress: int = 0 # 总进度百分比 0-100 - message: str = "" # 状态消息 - result: Optional[Dict] = None # 任务结果 - error: Optional[str] = None # 错误信息 - metadata: Dict = field(default_factory=dict) # 额外元数据 - progress_detail: Dict = field(default_factory=dict) # 详细进度信息 + progress: int = 0 # Phần trăm tiến độ quá trình chạy (0-100) + message: str = "" # Thông báo trạng thái hiện tại để hiển thị cho người dùng + result: Optional[Dict] = None # Kết quả trả về sau khi Task chạy xong + error: Optional[str] = None # Thông tin chi tiết mỗi khi Task bị lỗi + metadata: Dict = field(default_factory=dict) # Siêu dữ liệu bổ sung kèm theo (ví dụ: project_id) + progress_detail: Dict = field(default_factory=dict) # Nội dung thông tin chi tiết về các bước trong tiến trình def to_dict(self) -> Dict[str, Any]: - """转换为字典""" + """Chuyển đổi Class thành Dictionary để map vào JSON API Response""" return { "task_id": self.task_id, "task_type": self.task_type, @@ -53,15 +53,15 @@ def to_dict(self) -> Dict[str, Any]: class TaskManager: """ - 任务管理器 - 线程安全的任务状态管理 + Trình quản lý Task + Đảm bảo quản lý trạng thái của các tác vụ được đồng bộ tốt trên nhiều luồng chạy (Thread-safe) """ _instance = None _lock = threading.Lock() def __new__(cls): - """单例模式""" + """Kế thừa Singleton Pattern (Chỉ khởi tạo 1 instance duy nhất trên toàn ứng dụng)""" if cls._instance is None: with cls._lock: if cls._instance is None: @@ -72,14 +72,14 @@ def __new__(cls): def create_task(self, task_type: str, metadata: Optional[Dict] = None) -> str: """ - 创建新任务 + Tạo mới một Task và cho vào hàng đợi (quản lý state) Args: - task_type: 任务类型 - metadata: 额外元数据 + task_type: Loại tác vụ (vd: 'build_graph', 'generate_report', ...) + metadata: Dữ liệu đính kèm (vd: project_id liên quan để cập nhật dữ liệu sau này) Returns: - 任务ID + Chuỗi Mã định danh ngẫu nhiên (UUID) của Task """ task_id = str(uuid.uuid4()) now = datetime.now() @@ -99,7 +99,7 @@ def create_task(self, task_type: str, metadata: Optional[Dict] = None) -> str: return task_id def get_task(self, task_id: str) -> Optional[Task]: - """获取任务""" + """Lấy thông tin một Task đang chạy/kết thúc dựa theo Task UUID""" with self._task_lock: return self._tasks.get(task_id) @@ -114,16 +114,16 @@ def update_task( progress_detail: Optional[Dict] = None ): """ - 更新任务状态 + Cập nhật tiến trình của Task Args: - task_id: 任务ID - status: 新状态 - progress: 进度 - message: 消息 - result: 结果 - error: 错误信息 - progress_detail: 详细进度信息 + task_id: Mã ID của Task đang chạy + status: Trạng thái cập nhật (Pending, Processing, Completed, Failed) + progress: % Tiến độ hiện tại + message: Tin nhắn mô tả ngắn gọn hiện trạng làm gì + result: Trả về kết quả đầu ra khi thành công + error: Lời nhắn/Exception khi thất bại + progress_detail: Các sub-tiến trình chi tiết """ with self._task_lock: task = self._tasks.get(task_id) @@ -143,26 +143,26 @@ def update_task( task.progress_detail = progress_detail def complete_task(self, task_id: str, result: Dict): - """标记任务完成""" + """Hành động đánh dấu tác vụ đã kết thúc Thành Công và gán 100% cho progress""" self.update_task( task_id, status=TaskStatus.COMPLETED, progress=100, - message="任务完成", + message="The task has been completed!", result=result ) def fail_task(self, task_id: str, error: str): - """标记任务失败""" + """Hành động đánh dấu tác vụ đã Thất Bại do lỗi""" self.update_task( task_id, status=TaskStatus.FAILED, - message="任务失败", + message="The task has an error!?", error=error ) def list_tasks(self, task_type: Optional[str] = None) -> list: - """列出任务""" + """Liệt kê danh sách tất cả các Task (Hoặc filter theo type của task)""" with self._task_lock: tasks = list(self._tasks.values()) if task_type: @@ -170,7 +170,7 @@ def list_tasks(self, task_type: Optional[str] = None) -> list: return [t.to_dict() for t in sorted(tasks, key=lambda x: x.created_at, reverse=True)] def cleanup_old_tasks(self, max_age_hours: int = 24): - """清理旧任务""" + """Dọn dẹp/xoá khỏi bộ nhớ các Task đã cũ (Đã hoàn thành hoặc lỗi sau N giờ) để tránh rò rỉ hoặc xài tốn RAM""" from datetime import timedelta cutoff = datetime.now() - timedelta(hours=max_age_hours) diff --git a/backend/app/services/graph_builder.py b/backend/app/services/graph_builder.py index 0e0444bf3..efa5fb77d 100644 --- a/backend/app/services/graph_builder.py +++ b/backend/app/services/graph_builder.py @@ -1,6 +1,6 @@ """ -图谱构建服务 -接口2:使用Zep API构建Standalone Graph +Dịch vụ xây dựng Đồ thị Tri thức (Knowledge Graph) +API 2: Sử dụng Zep API để xây dựng một Standalone Graph (Đồ thị độc lập) """ import os @@ -21,7 +21,7 @@ @dataclass class GraphInfo: - """图谱信息""" + """Các trường thông tin cơ bản của Graph""" graph_id: str node_count: int edge_count: int @@ -38,14 +38,14 @@ def to_dict(self) -> Dict[str, Any]: class GraphBuilderService: """ - 图谱构建服务 - 负责调用Zep API构建知识图谱 + Dịch vụ tạo lập Graph + Đảm nhiệm logic gọi request lên Zep API để thiết lập Graph """ def __init__(self, api_key: Optional[str] = None): self.api_key = api_key or Config.ZEP_API_KEY if not self.api_key: - raise ValueError("ZEP_API_KEY 未配置") + raise ValueError("ZEP_API_KEY has not been configured.") self.client = Zep(api_key=self.api_key) self.task_manager = TaskManager() @@ -60,20 +60,20 @@ def build_graph_async( batch_size: int = 3 ) -> str: """ - 异步构建图谱 + Khởi chạy tiến trình bất đồng bộ xây dựng Graph Args: - text: 输入文本 - ontology: 本体定义(来自接口1的输出) - graph_name: 图谱名称 - chunk_size: 文本块大小 - chunk_overlap: 块重叠大小 - batch_size: 每批发送的块数量 + text: Văn bản toàn văn làm nguồn vào + ontology: Từ điển chuẩn cấu trúc Ontology (Đầu ra từ API số 1) + graph_name: Tên đặt cho Graph + chunk_size: Kích thước từng khối text (chunk) + chunk_overlap: Giới hạn những từ đè lên nhau giữa các chunk (bảo toàn flow hội thoại / ngữ cảnh) + batch_size: Chuyển dữ liệu theo mảng batch để tiết kiệm số lần Request Returns: - 任务ID + Trạng thái Task ID vừa khởi tạo """ - # 创建任务 + # Đưa Task vào danh sách quản lý task_id = self.task_manager.create_task( task_type="graph_build", metadata={ @@ -83,7 +83,7 @@ def build_graph_async( } ) - # 在后台线程中执行构建 + # Bắt đầu gọi Workder ở luồng ảo (Back ground Thread) để người dùng không tắc giao diện đợi xử lý thread = threading.Thread( target=self._build_graph_worker, args=(task_id, text, ontology, graph_name, chunk_size, chunk_overlap, batch_size) @@ -103,21 +103,21 @@ def _build_graph_worker( chunk_overlap: int, batch_size: int ): - """图谱构建工作线程""" + """Tiến trình cài đặt ngầm tạo Graph với các bước tuần tự""" try: self.task_manager.update_task( task_id, status=TaskStatus.PROCESSING, progress=5, - message="开始构建图谱..." + message="Building Knowledge Graph..." ) - # 1. 创建图谱 + # Bước 1. Init tạo khung xương Graph trên Zep graph_id = self.create_graph(graph_name) self.task_manager.update_task( task_id, progress=10, - message=f"图谱已创建: {graph_id}" + message=f"Created empty graph: {graph_id}" ) # 2. 设置本体 @@ -125,54 +125,54 @@ def _build_graph_worker( self.task_manager.update_task( task_id, progress=15, - message="本体已设置" + message="Ontology scheme applied successfully" ) - # 3. 文本分块 + # Bước 3. Chia nhỏ văn bản gốc chunks = TextProcessor.split_text(text, chunk_size, chunk_overlap) total_chunks = len(chunks) self.task_manager.update_task( task_id, progress=20, - message=f"文本已分割为 {total_chunks} 个块" + message=f"Split text into {total_chunks} chunk(s)" ) - # 4. 分批发送数据 + # Bước 4. Gửi các đợt chunk tới Zep dưới dạng batch episode_uuids = self.add_text_batches( graph_id, chunks, batch_size, lambda msg, prog: self.task_manager.update_task( task_id, - progress=20 + int(prog * 0.4), # 20-60% + progress=20 + int(prog * 0.4), # Thể hiện từ 20-60% message=msg ) ) - # 5. 等待Zep处理完成 + # Bước 5. Đợi hàm Backend của Cloud Zep xử lý đồng bộ xong các episode self.task_manager.update_task( task_id, progress=60, - message="等待Zep处理数据..." + message="Waiting for Zep to process data..." ) self._wait_for_episodes( episode_uuids, lambda msg, prog: self.task_manager.update_task( task_id, - progress=60 + int(prog * 0.3), # 60-90% + progress=60 + int(prog * 0.3), # Thể hiện từ 60-90% message=msg ) ) - # 6. 获取图谱信息 + # Bước 6. Thống kê lại Graph hoàn thiện self.task_manager.update_task( task_id, progress=90, - message="获取图谱信息..." + message="Fetching finalized graph info..." ) graph_info = self._get_graph_info(graph_id) - # 完成 + # Thông báo hoàn tất self.task_manager.complete_task(task_id, { "graph_id": graph_id, "graph_info": graph_info.to_dict(), @@ -185,7 +185,7 @@ def _build_graph_worker( self.task_manager.fail_task(task_id, error_msg) def create_graph(self, name: str) -> str: - """创建Zep图谱(公开方法)""" + """Khai báo một Graph mới với Zep API (Sử dụng công khai public)""" graph_id = f"mirofish_{uuid.uuid4().hex[:16]}" self.client.graph.create( @@ -197,74 +197,74 @@ def create_graph(self, name: str) -> str: return graph_id def set_ontology(self, graph_id: str, ontology: Dict[str, Any]): - """设置图谱本体(公开方法)""" + """Cấu hình dữ liệu Ontology (Bản thể học) cho Graph trên server Zep (Public access)""" import warnings from typing import Optional from pydantic import Field from zep_cloud.external_clients.ontology import EntityModel, EntityText, EdgeModel - # 抑制 Pydantic v2 关于 Field(default=None) 的警告 - # 这是 Zep SDK 要求的用法,警告来自动态类创建,可以安全忽略 + # Ẩn bỏ đi các Warning (Cảnh báo) của thư viện Pydantic v2 liên quan đến Field(default=None) + # Vì đây là format bắt buộc phải có từ Zep SDK, các cảnh báo này phát sinh do tự động khởi tạo lớp ảo, hoàn toàn có thể bỏ qua được. warnings.filterwarnings('ignore', category=UserWarning, module='pydantic') - # Zep 保留名称,不能作为属性名 + # Danh sách các tên định danh (variable/name) trùng với từ khoá bảo lưu của Zep, không được dùng làm tên thuộc tính RESERVED_NAMES = {'uuid', 'name', 'group_id', 'name_embedding', 'summary', 'created_at'} def safe_attr_name(attr_name: str) -> str: - """将保留名称转换为安全名称""" + """Hàm thay đổi các tên thuộc tính bị trùng với keyword của hệ thống để an toàn hơn""" if attr_name.lower() in RESERVED_NAMES: return f"entity_{attr_name}" return attr_name - # 动态创建实体类型 + # Khởi tạo động (Dynamic Class Creation) các Model Loại Thực thể từ JSON đầu vào entity_types = {} for entity_def in ontology.get("entity_types", []): name = entity_def["name"] description = entity_def.get("description", f"A {name} entity.") - # 创建属性字典和类型注解(Pydantic v2 需要) + # Chuẩn bị file từ điển cho Attribute và kiểu chú thích (Theo chuẩn Pydantic v2) attrs = {"__doc__": description} annotations = {} for attr_def in entity_def.get("attributes", []): - attr_name = safe_attr_name(attr_def["name"]) # 使用安全名称 + attr_name = safe_attr_name(attr_def["name"]) # Áp dụng hàm chống bị trùng từ khoá attr_desc = attr_def.get("description", attr_name) - # Zep API 需要 Field 的 description,这是必需的 + # Zep API bắt buộc phải nhận vào field description attrs[attr_name] = Field(description=attr_desc, default=None) - annotations[attr_name] = Optional[EntityText] # 类型注解 + annotations[attr_name] = Optional[EntityText] # Chú thích kiểu dữ liệu attrs["__annotations__"] = annotations - # 动态创建类 + # Dựng Class ảo entity_class = type(name, (EntityModel,), attrs) entity_class.__doc__ = description entity_types[name] = entity_class - # 动态创建边类型 + # Tương tự, dựa vào JSON để khởi tạo động khai báo các Model Loại Quan Hệ edge_definitions = {} for edge_def in ontology.get("edge_types", []): name = edge_def["name"] description = edge_def.get("description", f"A {name} relationship.") - # 创建属性字典和类型注解 + # Dọn các attribute dictionary và typing tương tự attrs = {"__doc__": description} annotations = {} for attr_def in edge_def.get("attributes", []): - attr_name = safe_attr_name(attr_def["name"]) # 使用安全名称 + attr_name = safe_attr_name(attr_def["name"]) # Filter an toàn attr_desc = attr_def.get("description", attr_name) - # Zep API 需要 Field 的 description,这是必需的 + # Đảm bảo giữ format Zep API attrs[attr_name] = Field(description=attr_desc, default=None) - annotations[attr_name] = Optional[str] # 边属性用str类型 + annotations[attr_name] = Optional[str] # Định dạng Data cho thuộc tình của loại Quan Hệ là chuỗi String attrs["__annotations__"] = annotations - # 动态创建类 + # Khởi tạo Class động với Tên chuẩn format (PascalCase) class_name = ''.join(word.capitalize() for word in name.split('_')) edge_class = type(class_name, (EdgeModel,), attrs) edge_class.__doc__ = description - # 构建source_targets + # Mapping thông số luồng thực thể gắn kết với Quan Hệ (Source/Targets config) source_targets = [] for st in edge_def.get("source_targets", []): source_targets.append( @@ -277,7 +277,7 @@ def safe_attr_name(attr_name: str) -> str: if source_targets: edge_definitions[name] = (edge_class, source_targets) - # 调用Zep API设置本体 + # Action Gọi lệnh thay đổi Ontology cho môi trường GraphID của Zep if entity_types or edge_definitions: self.client.graph.set_ontology( graph_ids=[graph_id], @@ -292,7 +292,7 @@ def add_text_batches( batch_size: int = 3, progress_callback: Optional[Callable] = None ) -> List[str]: - """分批添加文本到图谱,返回所有 episode 的 uuid 列表""" + """Tải các đoạn văn bản (text chunks) lên Graph theo từng gói nhỏ (batch) và trả về id (Episode UUID) của mọi phân đoạn dữ liệu gửi đi.""" episode_uuids = [] total_chunks = len(chunks) @@ -304,36 +304,36 @@ def add_text_batches( if progress_callback: progress = (i + len(batch_chunks)) / total_chunks progress_callback( - f"发送第 {batch_num}/{total_batches} 批数据 ({len(batch_chunks)} 块)...", + f"Sending data batch {batch_num}/{total_batches} ({len(batch_chunks)} chunks)...", progress ) - # 构建episode数据 + # Chuẩn bị định dạng gói dữ liệu (Episode data) để tương thích Zep Graph episodes = [ EpisodeData(data=chunk, type="text") for chunk in batch_chunks ] - # 发送到Zep + # Khởi chạy gửi cho Zep Server try: batch_result = self.client.graph.add_batch( graph_id=graph_id, episodes=episodes ) - # 收集返回的 episode uuid + # Cập nhật và thu thập lại UUID của các Episode được trả về sau khi tạo mới if batch_result and isinstance(batch_result, list): for ep in batch_result: ep_uuid = getattr(ep, 'uuid_', None) or getattr(ep, 'uuid', None) if ep_uuid: episode_uuids.append(ep_uuid) - # 避免请求过快 + # Cài thời gian chờ (delay) nhỏ để tránh rate-limit bị quá tải số lượng requests time.sleep(1) except Exception as e: if progress_callback: - progress_callback(f"批次 {batch_num} 发送失败: {str(e)}", 0) + progress_callback(f"Failed to send batch {batch_num}: {str(e)}", 0) raise return episode_uuids @@ -344,10 +344,10 @@ def _wait_for_episodes( progress_callback: Optional[Callable] = None, timeout: int = 600 ): - """等待所有 episode 处理完成(通过查询每个 episode 的 processed 状态)""" + """Chạy vòng lặp để kiểm tra và chờ cho tới khi mọi Episode (các khối Text) đều hoàn tất quá trình process từ hệ thống""" if not episode_uuids: if progress_callback: - progress_callback("无需等待(没有 episode)", 1.0) + progress_callback("No episodes to scan (Progress 100%)", 1.0) return start_time = time.time() @@ -356,18 +356,19 @@ def _wait_for_episodes( total_episodes = len(episode_uuids) if progress_callback: - progress_callback(f"开始等待 {total_episodes} 个文本块处理...", 0) + progress_callback(f"Waiting for analysis of {total_episodes} text chunks to begin...", 0) while pending_episodes: + # Ngắt thoát và trả về lỗi nếu bị Timeout (Chạy quá thời gian cho phép) if time.time() - start_time > timeout: if progress_callback: progress_callback( - f"部分文本块超时,已完成 {completed_count}/{total_episodes}", + f"Some text segments have timed out, but {completed_count}/{total_episodes} have completed successfully", completed_count / total_episodes ) break - # 检查每个 episode 的处理状态 + # Duyệt vòng lặp mỗi episode uuid để lấy cập nhật tiến trình check của từng episode một for ep_uuid in list(pending_episodes): try: episode = self.client.graph.episode.get(uuid_=ep_uuid) @@ -378,31 +379,31 @@ def _wait_for_episodes( completed_count += 1 except Exception as e: - # 忽略单个查询错误,继续 + # Tạm thời bỏ qua nếu request lỗi, vòng lặp kế theo sẽ tự động call tiếp để get status pass elapsed = int(time.time() - start_time) if progress_callback: progress_callback( - f"Zep处理中... {completed_count}/{total_episodes} 完成, {len(pending_episodes)} 待处理 ({elapsed}秒)", + f"Zep is processing in the background... {completed_count}/{total_episodes} done, {len(pending_episodes)} tasks remaining ({elapsed}s elapsed)", completed_count / total_episodes if total_episodes > 0 else 0 ) if pending_episodes: - time.sleep(3) # 每3秒检查一次 + time.sleep(3) # Lặp chu kỳ check mỗi 3 giây if progress_callback: - progress_callback(f"处理完成: {completed_count}/{total_episodes}", 1.0) + progress_callback(f"Data upload process completed: {completed_count}/{total_episodes}", 1.0) def _get_graph_info(self, graph_id: str) -> GraphInfo: - """获取图谱信息""" - # 获取节点(分页) + """Lấy/Get dữ liệu Graph Info hiện tại""" + # Load các điểm nút/Entity đang có (Qua trình duyệt web/paging) nodes = fetch_all_nodes(self.client, graph_id) - # 获取边(分页) + # Lấy theo mảng phân trang thông tin các Edges/Mối Liên Kết edges = fetch_all_edges(self.client, graph_id) - # 统计实体类型 + # Nối lại và thống kê những Entity Types entity_types = set() for node in nodes: if node.labels: @@ -419,25 +420,26 @@ def _get_graph_info(self, graph_id: str) -> GraphInfo: def get_graph_data(self, graph_id: str) -> Dict[str, Any]: """ - 获取完整图谱数据(包含详细信息) + Gói gọn toàn bộ dữ liệu cấu trúc (Bao gồm dữ liệu Graph chi tiết) Args: - graph_id: 图谱ID + graph_id: ID của đồ thị Returns: - 包含nodes和edges的字典,包括时间信息、属性等详细数据 + Một object Dictionary bao hàm thông tin dữ liệu về Mạng lưới Cụm (nodes) và Cạnh (edges), + và toàn bộ chi tiết đi kèm khác (Time khởi tạo, Property). """ nodes = fetch_all_nodes(self.client, graph_id) edges = fetch_all_edges(self.client, graph_id) - # 创建节点映射用于获取节点名称 + # Giữ một map tra cứu để phục vụ lấy 'Tên' nhanh theo ID UUID node_map = {} for node in nodes: node_map[node.uuid_] = node.name or "" nodes_data = [] for node in nodes: - # 获取创建时间 + # Lấy thông số về Thời gian được ghi nhận/khởi tạo created_at = getattr(node, 'created_at', None) if created_at: created_at = str(created_at) @@ -453,7 +455,7 @@ def get_graph_data(self, graph_id: str) -> Dict[str, Any]: edges_data = [] for edge in edges: - # 获取时间信息 + # Thu thập các timestamp gắn với cạnh created_at = getattr(edge, 'created_at', None) valid_at = getattr(edge, 'valid_at', None) invalid_at = getattr(edge, 'invalid_at', None) diff --git a/backend/app/services/oasis_profile_generator.py b/backend/app/services/oasis_profile_generator.py index 57836c539..4d2c5a974 100644 --- a/backend/app/services/oasis_profile_generator.py +++ b/backend/app/services/oasis_profile_generator.py @@ -1,11 +1,11 @@ """ -OASIS Agent Profile生成器 -将Zep图谱中的实体转换为OASIS模拟平台所需的Agent Profile格式 +Trình tạo tạo ra Profile Agent (Hồ sơ Nhân vật) cho Agent bằng Framework OASIS +Chuyển đổi dữ liệu Thực thể được Query từ Zep ra chuẩn định dạng của các Agent tham gia vào mạng -优化改进: -1. 调用Zep检索功能二次丰富节点信息 -2. 优化提示词生成非常详细的人设 -3. 区分个人实体和抽象群体实体 +Nâng cấp cải thiện: +1. Kết hợp dùng chức năng Search trên Zep để lấy Profile giàu sắc thái +2. Gen các cấu hình về tính cách một cách sắc xảo và cực sâu cho Prompt +3. Nhận dạng rạch ròi người với Group/Công ty/Phe phái trên social network """ import json @@ -27,23 +27,23 @@ @dataclass class OasisAgentProfile: - """OASIS Agent Profile数据结构""" - # 通用字段 + """Cấu trúc của Dataclass Profile Agent qua quy định của OASIS""" + # Các Field thông dụng (Common data) user_id: int user_name: str name: str bio: str persona: str - # 可选字段 - Reddit风格 + # Chọn bổ sung (Tùy chọn) - Thông số nền tảng Reddit (Karma) karma: int = 1000 - # 可选字段 - Twitter风格 + # Chọn bổ sung (Tùy chọn) - Thông số nền tảng Twitter friend_count: int = 100 follower_count: int = 150 statuses_count: int = 500 - # 额外人设信息 + # Một số Thông tin Data cá nhân bổ sung (Bóp để tăng tính Thực tế nếu LLM sinh ra) age: Optional[int] = None gender: Optional[str] = None mbti: Optional[str] = None @@ -51,17 +51,17 @@ class OasisAgentProfile: profession: Optional[str] = None interested_topics: List[str] = field(default_factory=list) - # 来源实体信息 + # Lịch sử thông tin entity gốc được lấy source_entity_uuid: Optional[str] = None source_entity_type: Optional[str] = None created_at: str = field(default_factory=lambda: datetime.now().strftime("%Y-%m-%d")) def to_reddit_format(self) -> Dict[str, Any]: - """转换为Reddit平台格式""" + """Convert trả ra cho định dạng Agent reddit""" profile = { "user_id": self.user_id, - "username": self.user_name, # OASIS 库要求字段名为 username(无下划线) + "username": self.user_name, # Source mã của OASIS Library yêu cầu không có dấu "_" cho param username "name": self.name, "bio": self.bio, "persona": self.persona, @@ -69,7 +69,7 @@ def to_reddit_format(self) -> Dict[str, Any]: "created_at": self.created_at, } - # 添加额外人设信息(如果有) + # Merge Thông tin Data Profile Cá nhân (Nếu CÓ) if self.age: profile["age"] = self.age if self.gender: @@ -86,10 +86,10 @@ def to_reddit_format(self) -> Dict[str, Any]: return profile def to_twitter_format(self) -> Dict[str, Any]: - """转换为Twitter平台格式""" + """Convert trả ra cho định dạng Agent Twitter""" profile = { "user_id": self.user_id, - "username": self.user_name, # OASIS 库要求字段名为 username(无下划线) + "username": self.user_name, # Tương tự như trên "name": self.name, "bio": self.bio, "persona": self.persona, @@ -99,7 +99,7 @@ def to_twitter_format(self) -> Dict[str, Any]: "created_at": self.created_at, } - # 添加额外人设信息 + # Merge Thông tin Data Profile Cả nhân if self.age: profile["age"] = self.age if self.gender: @@ -116,7 +116,7 @@ def to_twitter_format(self) -> Dict[str, Any]: return profile def to_dict(self) -> Dict[str, Any]: - """转换为完整字典格式""" + """Quy đổi thành toàn bộ Dictionary Cấu Trúc Khép Kín """ return { "user_id": self.user_id, "user_name": self.user_name, @@ -141,17 +141,17 @@ def to_dict(self) -> Dict[str, Any]: class OasisProfileGenerator: """ - OASIS Profile生成器 + Trình Gen Profile cho Simulation (Hệ OASIS) - 将Zep图谱中的实体转换为OASIS模拟所需的Agent Profile + Sử dụng các Node Entity lấy được từ ZEP -> OASIS Mocks cho Simulation Agent - 优化特性: - 1. 调用Zep图谱检索功能获取更丰富的上下文 - 2. 生成非常详细的人设(包括基本信息、职业经历、性格特征、社交媒体行为等) - 3. 区分个人实体和抽象群体实体 + Các Option Cải Tiến Tối Ưu Tích Hợp: + 1. Có liên kết với Server Zep API cho bước Query Dữ liệu từ Vector Database + 2. Tập trung Mô tả Tiểu sửa (Background) (Nhấn mạnh Nghề nghiệp/Tính cách/StatusMXH/vvv) + 3. Ngắt rời loại Person ra bên ngoài để nhận thức Phân Cấp Group """ - # MBTI类型列表 + # 16 tính cách của Con người (Quy Chuẩn) MBTI_TYPES = [ "INTJ", "INTP", "ENTJ", "ENTP", "INFJ", "INFP", "ENFJ", "ENFP", @@ -159,19 +159,19 @@ class OasisProfileGenerator: "ISTP", "ISFP", "ESTP", "ESFP" ] - # 常见国家列表 + # List mảng quốc tịch Cơ Bản COUNTRIES = [ "China", "US", "UK", "Japan", "Germany", "France", "Canada", "Australia", "Brazil", "India", "South Korea" ] - # 个人类型实体(需要生成具体人设) + # Thực thể nhận biết là người 1 mình (Độc Lập, 1 Person) INDIVIDUAL_ENTITY_TYPES = [ "student", "alumni", "professor", "person", "publicfigure", "expert", "faculty", "official", "journalist", "activist" ] - # 群体/机构类型实体(需要生成群体代表人设) + # Thực Thể được xem là một tổ chức GROUP_ENTITY_TYPES = [ "university", "governmentagency", "organization", "ngo", "mediaoutlet", "company", "institution", "group", "community" @@ -190,14 +190,14 @@ def __init__( self.model_name = model_name or Config.LLM_MODEL_NAME if not self.api_key: - raise ValueError("LLM_API_KEY 未配置") + raise ValueError("Không tìm thấy LLM_API_KEY") self.client = OpenAI( api_key=self.api_key, base_url=self.base_url ) - # Zep客户端用于检索丰富上下文 + # Kết nối lên trên ZEP Database Search Context self.zep_api_key = zep_api_key or Config.ZEP_API_KEY self.zep_client = None self.graph_id = graph_id @@ -206,7 +206,7 @@ def __init__( try: self.zep_client = Zep(api_key=self.zep_api_key) except Exception as e: - logger.warning(f"Zep客户端初始化失败: {e}") + logger.warning(f"Failed to initialize Zep client: {e}") def generate_profile_from_entity( self, @@ -215,27 +215,27 @@ def generate_profile_from_entity( use_llm: bool = True ) -> OasisAgentProfile: """ - 从Zep实体生成OASIS Agent Profile + Bắt đầu Gen Profile từ Entity được móc từ data từ zep Args: - entity: Zep实体节点 - user_id: 用户ID(用于OASIS) - use_llm: 是否使用LLM生成详细人设 + entity: Thực thể Zep + user_id: Số ID để map (sử dụng trên OASIS) + use_llm: Chọn bật tắt xem tạo Profile có dùng gen nhân vật bằng LLM Returns: OasisAgentProfile """ entity_type = entity.get_entity_type() or "Entity" - # 基础信息 + # Mức cơ bản thông tin name = entity.name user_name = self._generate_username(name) - # 构建上下文信息 + # Build các Context thông tin liên quan lại context = self._build_entity_context(entity) if use_llm: - # 使用LLM生成详细人设 + # Gửi Prompt lên LLM profile_data = self._generate_profile_with_llm( entity_name=name, entity_type=entity_type, @@ -244,7 +244,7 @@ def generate_profile_from_entity( context=context ) else: - # 使用规则生成基础人设 + # Chạy hàm Auto rule nếu LLm tắt profile_data = self._generate_profile_rule_based( entity_name=name, entity_type=entity_type, @@ -273,27 +273,26 @@ def generate_profile_from_entity( ) def _generate_username(self, name: str) -> str: - """生成用户名""" - # 移除特殊字符,转换为小写 + """Thêm chức năng Generate Username username ngẫu nhiên""" + # Hút bỏ khoảng trống và dấu đặc biệt username = name.lower().replace(" ", "_") username = ''.join(c for c in username if c.isalnum() or c == '_') - # 添加随机后缀避免重复 + # Chèn thêm hậu tố cho bớt đụng hàng suffix = random.randint(100, 999) return f"{username}_{suffix}" def _search_zep_for_entity(self, entity: EntityNode) -> Dict[str, Any]: """ - 使用Zep图谱混合搜索功能获取实体相关的丰富信息 + Dùng hỗn hợp lệnh Query DB Vector qua Zep để lấy các fact/sự kiện liên quan về 1 thực thể. - Zep没有内置混合搜索接口,需要分别搜索edges和nodes然后合并结果。 - 使用并行请求同时搜索,提高效率。 + Vì Zep chưa hỗ trợ hỗn hợp cả 2 cùng một lúc, nên cần tìm song song từ Edge và Node sau đó gộp kết quả. Args: - entity: 实体节点对象 + entity: Đầu cắm Thực Thể Node Returns: - 包含facts, node_summaries, context的字典 + Dictionary gồm facts, node_summaries, context """ import concurrent.futures @@ -308,15 +307,15 @@ def _search_zep_for_entity(self, entity: EntityNode) -> Dict[str, Any]: "context": "" } - # 必须有graph_id才能进行搜索 + # Yêu cầu graph_id mới truy vấn được if not self.graph_id: - logger.debug(f"跳过Zep检索:未设置graph_id") + logger.debug(f"Skipping Zep search: graph_id not set") return results - comprehensive_query = f"关于{entity_name}的所有信息、活动、事件、关系和背景" + comprehensive_query = f"Provide all facts, activities, relationships, and context about: {entity_name}" def search_edges(): - """搜索边(事实/关系)- 带重试机制""" + """Lookup cạnh relations - Kết hợp cơ chế retry""" max_retries = 3 last_exception = None delay = 2.0 @@ -333,15 +332,15 @@ def search_edges(): except Exception as e: last_exception = e if attempt < max_retries - 1: - logger.debug(f"Zep边搜索第 {attempt + 1} 次失败: {str(e)[:80]}, 重试中...") + logger.debug(f"Zep Edge search failed on attempt {attempt + 1}: {str(e)[:80]}, retrying...") time.sleep(delay) delay *= 2 else: - logger.debug(f"Zep边搜索在 {max_retries} 次尝试后仍失败: {e}") + logger.debug(f"Zep Edge search entirely failed after {max_retries} attempts: {e}") return None def search_nodes(): - """搜索节点(实体摘要)- 带重试机制""" + """Lookup mảng Node entity tóm tắt - Kết hợp cơ chế retry""" max_retries = 3 last_exception = None delay = 2.0 @@ -358,24 +357,24 @@ def search_nodes(): except Exception as e: last_exception = e if attempt < max_retries - 1: - logger.debug(f"Zep节点搜索第 {attempt + 1} 次失败: {str(e)[:80]}, 重试中...") + logger.debug(f"Zep Node search failed on attempt {attempt + 1}: {str(e)[:80]}, retrying...") time.sleep(delay) delay *= 2 else: - logger.debug(f"Zep节点搜索在 {max_retries} 次尝试后仍失败: {e}") + logger.debug(f"Zep Node search entirely failed after {max_retries} attempts: {e}") return None try: - # 并行执行edges和nodes搜索 + # Cho chạy cả task Cạnh và Node song song with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor: edge_future = executor.submit(search_edges) node_future = executor.submit(search_nodes) - # 获取结果 + # Fetch result trả về edge_result = edge_future.result(timeout=30) node_result = node_future.result(timeout=30) - # 处理边搜索结果 + # Quản lý kết quả fact của các cạnh Edge all_facts = set() if edge_result and hasattr(edge_result, 'edges') and edge_result.edges: for edge in edge_result.edges: @@ -383,58 +382,58 @@ def search_nodes(): all_facts.add(edge.fact) results["facts"] = list(all_facts) - # 处理节点搜索结果 + # Quản lý kết quả tên thực thể và summary của quá trình search Node all_summaries = set() if node_result and hasattr(node_result, 'nodes') and node_result.nodes: for node in node_result.nodes: if hasattr(node, 'summary') and node.summary: all_summaries.add(node.summary) if hasattr(node, 'name') and node.name and node.name != entity_name: - all_summaries.add(f"相关实体: {node.name}") + all_summaries.add(f"Related Entities: {node.name}") results["node_summaries"] = list(all_summaries) - # 构建综合上下文 + # Tổng hợp ra 1 chuỗi Context bao quanh context_parts = [] if results["facts"]: - context_parts.append("事实信息:\n" + "\n".join(f"- {f}" for f in results["facts"][:20])) + context_parts.append("Facts & Infomation:\n" + "\n".join(f"- {f}" for f in results["facts"][:20])) if results["node_summaries"]: - context_parts.append("相关实体:\n" + "\n".join(f"- {s}" for s in results["node_summaries"][:10])) + context_parts.append("Related Entities:\n" + "\n".join(f"- {s}" for s in results["node_summaries"][:10])) results["context"] = "\n\n".join(context_parts) - logger.info(f"Zep混合检索完成: {entity_name}, 获取 {len(results['facts'])} 条事实, {len(results['node_summaries'])} 个相关节点") + logger.info(f"Zep unified search completed: {entity_name}, fetched {len(results['facts'])} facts, {len(results['node_summaries'])} related nodes") except concurrent.futures.TimeoutError: - logger.warning(f"Zep检索超时 ({entity_name})") + logger.warning(f"Zep Retrieval Time-Out ({entity_name})") except Exception as e: - logger.warning(f"Zep检索失败 ({entity_name}): {e}") + logger.warning(f"Zep Retrieval Failed ({entity_name}): {e}") return results def _build_entity_context(self, entity: EntityNode) -> str: """ - 构建实体的完整上下文信息 + Nối tất cả info thu được liên quan thành 1 chuỗi Context bao quanh hoàn chỉnh cho Entity - 包括: - 1. 实体本身的边信息(事实) - 2. 关联节点的详细信息 - 3. Zep混合检索到的丰富信息 + Nó sẽ lấy: + 1. Context từ các cạnh hiện tại đã gắn Entity (Dữ liệu về relation/fact) + 2. Mô tả sơ lược thêm của các Node dính liền + 3. Cuối cùng nhồi thêm những thứ moi được từ quá trình chạy Zep search hỗn hợp bên trên """ context_parts = [] - # 1. 添加实体属性信息 + # 1. Thu thập Attributes/Properties của node nếu có if entity.attributes: attrs = [] for key, value in entity.attributes.items(): if value and str(value).strip(): attrs.append(f"- {key}: {value}") if attrs: - context_parts.append("### 实体属性\n" + "\n".join(attrs)) + context_parts.append("### Entity Attributes\n" + "\n".join(attrs)) - # 2. 添加相关边信息(事实/关系) + # 2. Add các facts và mô phỏng cạnh (Relationship/Facts) existing_facts = set() if entity.related_edges: relationships = [] - for edge in entity.related_edges: # 不限制数量 + for edge in entity.related_edges: # Khum bị giới hạn SL fact = edge.get("fact", "") edge_name = edge.get("edge_name", "") direction = edge.get("direction", "") @@ -444,22 +443,22 @@ def _build_entity_context(self, entity: EntityNode) -> str: existing_facts.add(fact) elif edge_name: if direction == "outgoing": - relationships.append(f"- {entity.name} --[{edge_name}]--> (相关实体)") + relationships.append(f"- {entity.name} --[{edge_name}]--> (Related Entity)") else: - relationships.append(f"- (相关实体) --[{edge_name}]--> {entity.name}") + relationships.append(f"- (Related Entity) --[{edge_name}]--> {entity.name}") if relationships: - context_parts.append("### 相关事实和关系\n" + "\n".join(relationships)) + context_parts.append("### Facts & Relationships\n" + "\n".join(relationships)) - # 3. 添加关联节点的详细信息 + # 3. Kẹp chi tiết miêu tả về node anh em cạnh bên if entity.related_nodes: related_info = [] - for node in entity.related_nodes: # 不限制数量 + for node in entity.related_nodes: # Không block giới hạn số lượng node_name = node.get("name", "") node_labels = node.get("labels", []) node_summary = node.get("summary", "") - # 过滤掉默认标签 + # Bỏ nhãn mặc định khỏi string xuất ra custom_labels = [l for l in node_labels if l not in ["Entity", "Node"]] label_str = f" ({', '.join(custom_labels)})" if custom_labels else "" @@ -469,28 +468,28 @@ def _build_entity_context(self, entity: EntityNode) -> str: related_info.append(f"- **{node_name}**{label_str}") if related_info: - context_parts.append("### 关联实体信息\n" + "\n".join(related_info)) + context_parts.append("### Related Entity Info\n" + "\n".join(related_info)) - # 4. 使用Zep混合检索获取更丰富的信息 + # 4. Sử dụng kết quả Query Search từ hàm zep zep_results = self._search_zep_for_entity(entity) if zep_results.get("facts"): - # 去重:排除已存在的事实 + # Lọc bớt cặn trùng lắp: không add những Fact đã có ở mục số 2 new_facts = [f for f in zep_results["facts"] if f not in existing_facts] if new_facts: - context_parts.append("### Zep检索到的事实信息\n" + "\n".join(f"- {f}" for f in new_facts[:15])) + context_parts.append("### Facts retrieved via ZEP\n" + "\n".join(f"- {f}" for f in new_facts[:15])) if zep_results.get("node_summaries"): - context_parts.append("### Zep检索到的相关节点\n" + "\n".join(f"- {s}" for s in zep_results["node_summaries"][:10])) + context_parts.append("### Entity nodes retrieved via Zep\n" + "\n".join(f"- {s}" for s in zep_results["node_summaries"][:10])) return "\n\n".join(context_parts) def _is_individual_entity(self, entity_type: str) -> bool: - """判断是否是个人类型实体""" + """KTra và True cho các dạng người Single Person""" return entity_type.lower() in self.INDIVIDUAL_ENTITY_TYPES def _is_group_entity(self, entity_type: str) -> bool: - """判断是否是群体/机构类型实体""" + """KTra xem thực thể hiện tại là Group/Media/Company ...""" return entity_type.lower() in self.GROUP_ENTITY_TYPES def _generate_profile_with_llm( @@ -502,11 +501,11 @@ def _generate_profile_with_llm( context: str ) -> Dict[str, Any]: """ - 使用LLM生成非常详细的人设 + Dùng LLM cấp lại Profile mô phỏng tính cách cụ thể và rõ nét nhất - 根据实体类型区分: - - 个人实体:生成具体的人物设定 - - 群体/机构实体:生成代表性账号设定 + Kiểm tra đầu vào Entity type để chia nhánh: + - Cá nhân: Tạo setting miêu tả cá nhân, công việc riêng + - Tập Thể/Cơ quan/Tổ chức: Tạo Profile cho 1 tài khoản đại điện tổ chức đó """ is_individual = self._is_individual_entity(entity_type) @@ -520,7 +519,7 @@ def _generate_profile_with_llm( entity_name, entity_type, entity_summary, entity_attributes, context ) - # 尝试多次生成,直到成功或达到最大重试次数 + # Retry liên tục vòng lặp nếu LLM timeout hoặc fail max_attempts = 3 last_error = None @@ -533,34 +532,34 @@ def _generate_profile_with_llm( {"role": "user", "content": prompt} ], response_format={"type": "json_object"}, - temperature=0.7 - (attempt * 0.1) # 每次重试降低温度 - # 不设置max_tokens,让LLM自由发挥 + temperature=0.7 - (attempt * 0.1) # Giảm tính sáng tạo ngẫu nhiên đi một chút mỗi khi fail để tăng khả năng thành công ở vòng tiếp theo + # Thả Rông Max tokens ) content = response.choices[0].message.content - # 检查是否被截断(finish_reason不是'stop') + # Check LLM trả về vì sao bị kẹt lại/Dừng lại (Finish_Reason khác "stop") finish_reason = response.choices[0].finish_reason if finish_reason == 'length': - logger.warning(f"LLM输出被截断 (attempt {attempt+1}), 尝试修复...") + logger.warning(f"LLM output truncated (attempt {attempt+1}), attempting to fix...") content = self._fix_truncated_json(content) - # 尝试解析JSON + # Parse chép vào JSON try: result = json.loads(content) - # 验证必需字段 + # Xác minh tham số được Bot gen thành công chưa if "bio" not in result or not result["bio"]: result["bio"] = entity_summary[:200] if entity_summary else f"{entity_type}: {entity_name}" if "persona" not in result or not result["persona"]: - result["persona"] = entity_summary or f"{entity_name}是一个{entity_type}。" + result["persona"] = entity_summary or f"{entity_name} is a {entity_type}." return result except json.JSONDecodeError as je: - logger.warning(f"JSON解析失败 (attempt {attempt+1}): {str(je)[:80]}") + logger.warning(f"JSON Parsing Failed (attempt {attempt+1}): {str(je)[:80]}") - # 尝试修复JSON + # Tool sửa lỗi JSON syntax tự chế result = self._try_fix_json(content, entity_name, entity_type, entity_summary) if result.get("_fixed"): del result["_fixed"] @@ -569,75 +568,75 @@ def _generate_profile_with_llm( last_error = je except Exception as e: - logger.warning(f"LLM调用失败 (attempt {attempt+1}): {str(e)[:80]}") + logger.warning(f"LLM call Failed (attempt {attempt+1}): {str(e)[:80]}") last_error = e import time - time.sleep(1 * (attempt + 1)) # 指数退避 + time.sleep(1 * (attempt + 1)) # Exponential backoff - logger.warning(f"LLM生成人设失败({max_attempts}次尝试): {last_error}, 使用规则生成") + logger.warning(f"Generating profile through LLM failed totally after {max_attempts} attempts: {last_error}, switching to basic hard-code rules configs") return self._generate_profile_rule_based( entity_name, entity_type, entity_summary, entity_attributes ) def _fix_truncated_json(self, content: str) -> str: - """修复被截断的JSON(输出被max_tokens限制截断)""" + """Fix Output JSON bị Max_tokens đè cắt gãy""" import re - # 如果JSON被截断,尝试闭合它 + # Bọc ngoài content = content.strip() - # 计算未闭合的括号 + # Điểm kiểm chứng xem dấu ngoặc được đầy đủ hay chưa open_braces = content.count('{') - content.count('}') open_brackets = content.count('[') - content.count(']') - # 检查是否有未闭合的字符串 - # 简单检查:如果最后一个引号后没有逗号或闭合括号,可能是字符串被截断 + # Check string xem đủ không + # Nếu phần tử cuối cùng k phải dấu câu đóng block, tự chèn vào if content and content[-1] not in '",}]': - # 尝试闭合字符串 + # Ngoặc cho đít chuỗi content += '"' - # 闭合括号 + # Ngoặc block content += ']' * open_brackets content += '}' * open_braces return content def _try_fix_json(self, content: str, entity_name: str, entity_type: str, entity_summary: str = "") -> Dict[str, Any]: - """尝试修复损坏的JSON""" + """Thử Fix nội dung JSON""" import re - # 1. 首先尝试修复被截断的情况 + # 1. Bọc json trước content = self._fix_truncated_json(content) - # 2. 尝试提取JSON部分 + # 2. Extract block lớn json_match = re.search(r'\{[\s\S]*\}', content) if json_match: json_str = json_match.group() - # 3. 处理字符串中的换行符问题 - # 找到所有字符串值并替换其中的换行符 + # 3. Clean lại các chuỗi xuống dòng + # Regex lôi code ra ngoài def fix_string_newlines(match): s = match.group(0) - # 替换字符串内的实际换行符为空格 + # Escape code line chuyển cho space cho an toàn s = s.replace('\n', ' ').replace('\r', ' ') - # 替换多余空格 + # Chém các khoảng trống còn dư quá gắt s = re.sub(r'\s+', ' ', s) return s - # 匹配JSON字符串值 + # Khớp lại nội dung json_str = re.sub(r'"[^"\\]*(?:\\.[^"\\]*)*"', fix_string_newlines, json_str) - # 4. 尝试解析 + # 4. Bắt đầu json parse try: result = json.loads(json_str) result["_fixed"] = True return result except json.JSONDecodeError as e: - # 5. 如果还是失败,尝试更激进的修复 + # 5. Phá lấu clean xóa nếu còn bị lỗi Control char ẩn (0x00 đến 0x1f ...) try: - # 移除所有控制字符 + # Chém control character json_str = re.sub(r'[\x00-\x1f\x7f-\x9f]', ' ', json_str) - # 替换所有连续空白 + # Gọt lại khoảng trống dư json_str = re.sub(r'\s+', ' ', json_str) result = json.loads(json_str) result["_fixed"] = True @@ -645,32 +644,32 @@ def fix_string_newlines(match): except: pass - # 6. 尝试从内容中提取部分信息 + # 6. Rescue lấy các property còn lại mót ra từ đóng hỗn độn bio_match = re.search(r'"bio"\s*:\s*"([^"]*)"', content) - persona_match = re.search(r'"persona"\s*:\s*"([^"]*)', content) # 可能被截断 + persona_match = re.search(r'"persona"\s*:\s*"([^"]*)', content) # Bị cắt khúc thì ráng chịu bio = bio_match.group(1) if bio_match else (entity_summary[:200] if entity_summary else f"{entity_type}: {entity_name}") - persona = persona_match.group(1) if persona_match else (entity_summary or f"{entity_name}是一个{entity_type}。") + persona = persona_match.group(1) if persona_match else (entity_summary or f"{entity_name} is a {entity_type}.") - # 如果提取到了有意义的内容,标记为已修复 + # Lụm mót được data xịn thì mark là fix thành công if bio_match or persona_match: - logger.info(f"从损坏的JSON中提取了部分信息") + logger.info(f"Successfully extracted partial info from corrupted JSON") return { "bio": bio, "persona": persona, "_fixed": True } - # 7. 完全失败,返回基础结构 - logger.warning(f"JSON修复失败,返回基础结构") + # 7. Failed sạch, quăng cái khung mặc định ra + logger.warning(f"Failed to fix JSON, returning basic structured data") return { "bio": entity_summary[:200] if entity_summary else f"{entity_type}: {entity_name}", - "persona": entity_summary or f"{entity_name}是一个{entity_type}。" + "persona": entity_summary or f"{entity_name} is a {entity_type}." } def _get_system_prompt(self, is_individual: bool) -> str: - """获取系统提示词""" - base_prompt = "你是社交媒体用户画像生成专家。生成详细、真实的人设用于舆论模拟,最大程度还原已有现实情况。必须返回有效的JSON格式,所有字符串值不能包含未转义的换行符。使用中文。" + """Lấy prompt cho hệ thống""" + base_prompt = "Bạn là một chuyên gia tạo chân dung người dùng mạng xã hội. Tạo một nhân vật chi tiết, chân thực để mô phỏng dư luận, phục hồi bối cảnh thế giới thực ở mức tối đa. Bắt buộc phải trả về định dạng JSON hợp lệ, tất cả các chuỗi không được chứa ký tự xuống dòng chưa được escape. Sử dụng tiếng Việt." return base_prompt def _build_individual_persona_prompt( @@ -681,45 +680,45 @@ def _build_individual_persona_prompt( entity_attributes: Dict[str, Any], context: str ) -> str: - """构建个人实体的详细人设提示词""" + """Tạo prompt nhân vật chi tiết cho thực thể cá nhân""" - attrs_str = json.dumps(entity_attributes, ensure_ascii=False) if entity_attributes else "无" - context_str = context[:3000] if context else "无额外上下文" + attrs_str = json.dumps(entity_attributes, ensure_ascii=False) if entity_attributes else "Không có" + context_str = context[:3000] if context else "Không có ngữ cảnh bổ sung" - return f"""为实体生成详细的社交媒体用户人设,最大程度还原已有现实情况。 + return f"""Tạo một thiết lập người dùng mạng xã hội chi tiết cho thực thể, phản ánh tối đa tình hình thực tế hiện có. -实体名称: {entity_name} -实体类型: {entity_type} -实体摘要: {entity_summary} -实体属性: {attrs_str} +Tên thực thể: {entity_name} +Loại thực thể: {entity_type} +Tóm tắt thực thể: {entity_summary} +Thuộc tính thực thể: {attrs_str} -上下文信息: +Thông tin ngữ cảnh: {context_str} -请生成JSON,包含以下字段: +Vui lòng tạo JSON, bao gồm các trường sau: -1. bio: 社交媒体简介,200字 -2. persona: 详细人设描述(2000字的纯文本),需包含: - - 基本信息(年龄、职业、教育背景、所在地) - - 人物背景(重要经历、与事件的关联、社会关系) - - 性格特征(MBTI类型、核心性格、情绪表达方式) - - 社交媒体行为(发帖频率、内容偏好、互动风格、语言特点) - - 立场观点(对话题的态度、可能被激怒/感动的内容) - - 独特特征(口头禅、特殊经历、个人爱好) - - 个人记忆(人设的重要部分,要介绍这个个体与事件的关联,以及这个个体在事件中的已有动作与反应) -3. age: 年龄数字(必须是整数) -4. gender: 性别,必须是英文: "male" 或 "female" -5. mbti: MBTI类型(如INTJ、ENFP等) -6. country: 国家(使用中文,如"中国") -7. profession: 职业 -8. interested_topics: 感兴趣话题数组 +1. bio: Giới thiệu mạng xã hội, 200 chữ +2. persona: Mô tả chi tiết nhân vật (văn bản thuần 2000 chữ), cần bao gồm: + - Thông tin cơ bản (Tuổi, Nghề nghiệp, Nền tảng học vấn, Vị trí hiện tại) + - Bối cảnh nhân vật (Kinh nghiệm quan trọng, Mối liên hệ với sự kiện, Quan hệ xã hội) + - Đặc điểm tính cách (Loại MBTI, Tính cách cốt lõi, Cách thể hiện cảm xúc) + - Hành vi Mạng xã hội (Tần suất đăng bài, Sở thích nội dung, Phong cách tương tác, Đặc điểm ngôn ngữ) + - Quan điểm lập trường (Thái độ đối với chủ đề, Nội dung có thể gây phẫn nộ/cảm động) + - Đặc điểm độc đáo (Câu cửa miệng, Kinh nghiệm đặc biệt, Sở thích cá nhân) + - Ký ức cá nhân (Phần quan trọng của nhân vật, cần giới thiệu rõ mối liên hệ giữa cá nhân này với sự kiện, cũng như các hành động và phản ứng của người này trong sự kiện) +3. age: Tuổi (bắt buộc phải là số nguyên) +4. gender: Giới tính, bắt buộc bằng tiếng Anh: "male" hoặc "female" +5. mbti: Loại MBTI (ví dụ: INTJ, ENFP, v.v.) +6. country: Quốc gia (sử dụng tiếng Việt, ví dụ "Việt Nam") +7. profession: Nghề nghiệp +8. interested_topics: Mảng các chủ đề quan tâm -重要: -- 所有字段值必须是字符串或数字,不要使用换行符 -- persona必须是一段连贯的文字描述 -- 使用中文(除了gender字段必须用英文male/female) -- 内容要与实体信息保持一致 -- age必须是有效的整数,gender必须是"male"或"female" +Quan trọng: +- Tất cả giá trị các trường phải là chuỗi hoặc số, không sử dụng ký tự xuống dòng (\n) +- persona phải là một đoạn mô tả văn bản liên tục, không ngắt quãng +- Sử dụng tiếng Việt (ngoại trừ trường gender bắt buộc dùng tiếng Anh male/female) +- Nội dung phải nhất quán với thông tin của thực thể +- age phải là số nguyên hợp lệ, gender phải là "male" hoặc "female" """ def _build_group_persona_prompt( @@ -730,45 +729,45 @@ def _build_group_persona_prompt( entity_attributes: Dict[str, Any], context: str ) -> str: - """构建群体/机构实体的详细人设提示词""" + """Tạo prompt chi tiết cho tài khoản đại diện tổ chức/nhóm""" - attrs_str = json.dumps(entity_attributes, ensure_ascii=False) if entity_attributes else "无" - context_str = context[:3000] if context else "无额外上下文" + attrs_str = json.dumps(entity_attributes, ensure_ascii=False) if entity_attributes else "Không có" + context_str = context[:3000] if context else "Không có ngữ cảnh bổ sung" - return f"""为机构/群体实体生成详细的社交媒体账号设定,最大程度还原已有现实情况。 + return f"""Tạo một thiết lập tài khoản mạng xã hội chi tiết cho tổ chức/nhóm, phản ánh tối đa tình hình thực tế. -实体名称: {entity_name} -实体类型: {entity_type} -实体摘要: {entity_summary} -实体属性: {attrs_str} +Tên thực thể: {entity_name} +Loại thực thể: {entity_type} +Tóm tắt thực thể: {entity_summary} +Thuộc tính thực thể: {attrs_str} -上下文信息: +Thông tin ngữ cảnh: {context_str} -请生成JSON,包含以下字段: +Vui lòng tạo JSON, bao gồm các trường sau: -1. bio: 官方账号简介,200字,专业得体 -2. persona: 详细账号设定描述(2000字的纯文本),需包含: - - 机构基本信息(正式名称、机构性质、成立背景、主要职能) - - 账号定位(账号类型、目标受众、核心功能) - - 发言风格(语言特点、常用表达、禁忌话题) - - 发布内容特点(内容类型、发布频率、活跃时间段) - - 立场态度(对核心话题的官方立场、面对争议的处理方式) - - 特殊说明(代表的群体画像、运营习惯) - - 机构记忆(机构人设的重要部分,要介绍这个机构与事件的关联,以及这个机构在事件中的已有动作与反应) -3. age: 固定填30(机构账号的虚拟年龄) -4. gender: 固定填"other"(机构账号使用other表示非个人) -5. mbti: MBTI类型,用于描述账号风格,如ISTJ代表严谨保守 -6. country: 国家(使用中文,如"中国") -7. profession: 机构职能描述 -8. interested_topics: 关注领域数组 +1. bio: Giới thiệu tài khoản chính thức, 200 chữ, chuyên nghiệp và đúng mực +2. persona: Mô tả chi tiết thiết lập tài khoản (văn bản thuần 2000 chữ), cần bao gồm: + - Thông tin cơ bản của tổ chức (Tên chính thức, Tính chất tổ chức, Bối cảnh thành lập, Chức năng chính) + - Định hướng tài khoản (Loại tài khoản, Đối tượng mục tiêu, Chức năng cốt lõi) + - Phong cách phát ngôn (Đặc điểm ngôn ngữ, Các biểu đạt thường dùng, Chủ đề cấm kỵ) + - Đặc điểm nội dung đăng tải (Loại nội dung, Tần suất đăng, Khung giờ hoạt động) + - Lập trường thái độ (Lập trường chính thức đối với chủ đề cốt lõi, Cách xử lý khi đối mặt với tranh cãi) + - Lưu ý đặc biệt (Chân dung nhóm đại diện, Thói quen vận hành) + - Ký ức tổ chức (Phần quan trọng của thiết lập tổ chức, cần giới thiệu rõ mối liên hệ giữa tổ chức này với sự kiện, cũng như các hành động và phản ứng của tổ chức trong sự kiện) +3. age: Điền cố định 30 (Tuổi ảo của tài khoản tổ chức) +4. gender: Điền cố định "other" (Tài khoản tổ chức sử dụng other để biểu thị tính phi cá nhân) +5. mbti: Loại MBTI, dùng để mô tả phong cách tài khoản, ví dụ ISTJ đại diện cho sự nghiêm ngặt, bảo thủ +6. country: Quốc gia (sử dụng tiếng Việt, ví dụ "Việt Nam") +7. profession: Mô tả chức năng tổ chức +8. interested_topics: Mảng các lĩnh vực quan tâm -重要: -- 所有字段值必须是字符串或数字,不允许null值 -- persona必须是一段连贯的文字描述,不要使用换行符 -- 使用中文(除了gender字段必须用英文"other") -- age必须是整数30,gender必须是字符串"other" -- 机构账号发言要符合其身份定位""" +Quan trọng: +- Tất cả giá trị các trường phải là chuỗi hoặc số, không cho phép giá trị null +- persona phải là một đoạn mô tả văn bản liên tục, không sử dụng ký tự xuống dòng (\n) +- Sử dụng tiếng Việt (ngoại trừ trường gender bắt buộc dùng tiếng Anh "other") +- age phải là số nguyên 30, gender phải là chuỗi "other" +- Phát ngôn của tài khoản tổ chức phải phù hợp với định vị danh tính của nó""" def _generate_profile_rule_based( self, @@ -777,9 +776,9 @@ def _generate_profile_rule_based( entity_summary: str, entity_attributes: Dict[str, Any] ) -> Dict[str, Any]: - """使用规则生成基础人设""" + """Sử dụng rule để tạo Profile cơ bản khi dự phòng""" - # 根据实体类型生成不同的人设 + # Phân nhánh theo loại thực thể để tạo Profile thủ công entity_type_lower = entity_type.lower() if entity_type_lower in ["student", "alumni"]: @@ -810,10 +809,10 @@ def _generate_profile_rule_based( return { "bio": f"Official account for {entity_name}. News and updates.", "persona": f"{entity_name} is a media entity that reports news and facilitates public discourse. The account shares timely updates and engages with the audience on current events.", - "age": 30, # 机构虚拟年龄 - "gender": "other", # 机构使用other - "mbti": "ISTJ", # 机构风格:严谨保守 - "country": "中国", + "age": 30, # Tuổi ảo của cơ quan/tổ chức + "gender": "other", # Cơ quan dùng "other" + "mbti": "ISTJ", # Phong cách tổ chức: nghiêm túc bảo thủ + "country": "Việt Nam", "profession": "Media", "interested_topics": ["General News", "Current Events", "Public Affairs"], } @@ -822,16 +821,16 @@ def _generate_profile_rule_based( return { "bio": f"Official account of {entity_name}.", "persona": f"{entity_name} is an institutional entity that communicates official positions, announcements, and engages with stakeholders on relevant matters.", - "age": 30, # 机构虚拟年龄 - "gender": "other", # 机构使用other - "mbti": "ISTJ", # 机构风格:严谨保守 - "country": "中国", + "age": 30, # Tuổi ảo của cơ quan/tổ chức + "gender": "other", # Cơ quan dùng "other" + "mbti": "ISTJ", # Phong cách tổ chức: nghiêm túc bảo thủ + "country": "Việt Nam", "profession": entity_type, "interested_topics": ["Public Policy", "Community", "Official Announcements"], } else: - # 默认人设 + # Profile mặc định (Fallback default) return { "bio": entity_summary[:150] if entity_summary else f"{entity_type}: {entity_name}", "persona": entity_summary or f"{entity_name} is a {entity_type.lower()} participating in social discussions.", @@ -844,7 +843,7 @@ def _generate_profile_rule_based( } def set_graph_id(self, graph_id: str): - """设置图谱ID用于Zep检索""" + """Lưu lại Graph ID để dùng cho việc tra cứu Zep""" self.graph_id = graph_id def generate_profiles_from_entities( @@ -858,52 +857,52 @@ def generate_profiles_from_entities( output_platform: str = "reddit" ) -> List[OasisAgentProfile]: """ - 批量从实体生成Agent Profile(支持并行生成) + Khởi tạo hàng loạt các Agent Profile từ các thực thể (Hỗ trợ Gen đa luồng song song) Args: - entities: 实体列表 - use_llm: 是否使用LLM生成详细人设 - progress_callback: 进度回调函数 (current, total, message) - graph_id: 图谱ID,用于Zep检索获取更丰富上下文 - parallel_count: 并行生成数量,默认5 - realtime_output_path: 实时写入的文件路径(如果提供,每生成一个就写入一次) - output_platform: 输出平台格式 ("reddit" 或 "twitter") + entities: Danh sách thực thể + use_llm: Có sử dụng LLM để tạo tính cách chi tiết hay không + progress_callback: Hàm CallBack báo tiến độ (current, total, message) + graph_id: Đưa Graph ID vào để Zep retrieval thêm nhiều ngữ cảnh phong phú + parallel_count: Số luồng song song, mặc định 5 + realtime_output_path: Đường dẫn lưu file realtime (Gen ra đứa nào auto save đứa đó luôn) + output_platform: Format lưu trữ output ("reddit" hoạc "twitter") Returns: - Agent Profile列表 + Danh sách Profile Agent """ import concurrent.futures from threading import Lock - # 设置graph_id用于Zep检索 + # Lưu Graph ID lại cho Zep xử lý search if graph_id: self.graph_id = graph_id total = len(entities) - profiles = [None] * total # 预分配列表保持顺序 - completed_count = [0] # 使用列表以便在闭包中修改 + profiles = [None] * total # Cấp trước 1 mảng để giữ đúng thứ tự Index + completed_count = [0] # Phải dùng List để closure của các Sub Thread update được lock = Lock() - # 实时写入文件的辅助函数 + # Hàm con hỗ trợ việc ghi realtime file trong Thread def save_profiles_realtime(): - """实时保存已生成的 profiles 到文件""" + """Lưu file json ngay lập tức khi profile được tạo mới thành công""" if not realtime_output_path: return with lock: - # 过滤出已生成的 profiles + # Lọc ra những profile đã làm xong existing_profiles = [p for p in profiles if p is not None] if not existing_profiles: return try: if output_platform == "reddit": - # Reddit JSON 格式 + # Cấu trúc dành cho định dạng Reddit profiles_data = [p.to_reddit_format() for p in existing_profiles] with open(realtime_output_path, 'w', encoding='utf-8') as f: json.dump(profiles_data, f, ensure_ascii=False, indent=2) else: - # Twitter CSV 格式 + # Cấu trúc dành cho định dạng Twitter (CSV) import csv profiles_data = [p.to_twitter_format() for p in existing_profiles] if profiles_data: @@ -913,10 +912,10 @@ def save_profiles_realtime(): writer.writeheader() writer.writerows(profiles_data) except Exception as e: - logger.warning(f"实时保存 profiles 失败: {e}") + logger.warning(f"Failed to save profile in realtime: {e}") def generate_single_profile(idx: int, entity: EntityNode) -> tuple: - """生成单个profile的工作函数""" + """Hàm Worker gen từng profile riêng lẻ""" entity_type = entity.get_entity_type() or "Entity" try: @@ -926,14 +925,14 @@ def generate_single_profile(idx: int, entity: EntityNode) -> tuple: use_llm=use_llm ) - # 实时输出生成的人设到控制台和日志 + # Print output để nhìn trực tiếp Log terminal self._print_generated_profile(entity.name, entity_type, profile) return idx, profile, None except Exception as e: - logger.error(f"生成实体 {entity.name} 的人设失败: {str(e)}") - # 创建一个基础profile + logger.error(f"Failed to generate profile for entity {entity.name}: {str(e)}") + # Rơi vào tạo Profile dự phòng (Fallback) fallback_profile = OasisAgentProfile( user_id=idx, user_name=self._generate_username(entity.name), @@ -945,20 +944,20 @@ def generate_single_profile(idx: int, entity: EntityNode) -> tuple: ) return idx, fallback_profile, str(e) - logger.info(f"开始并行生成 {total} 个Agent人设(并行数: {parallel_count})...") + logger.info(f"Start parallel profile generation for {total} entities (Concurrency: {parallel_count})...") print(f"\n{'='*60}") - print(f"开始生成Agent人设 - 共 {total} 个实体,并行数: {parallel_count}") + print(f"Starting Agent Profile Generation - Total {total} entities, concurrency: {parallel_count}") print(f"{'='*60}\n") - # 使用线程池并行执行 + # Chạy đa luồng thread pool with concurrent.futures.ThreadPoolExecutor(max_workers=parallel_count) as executor: - # 提交所有任务 + # Giao Task future_to_entity = { executor.submit(generate_single_profile, idx, entity): (idx, entity) for idx, entity in enumerate(entities) } - # 收集结果 + # Thu gom kết quả for future in concurrent.futures.as_completed(future_to_entity): idx, entity = future_to_entity[future] entity_type = entity.get_entity_type() or "Entity" @@ -971,23 +970,23 @@ def generate_single_profile(idx: int, entity: EntityNode) -> tuple: completed_count[0] += 1 current = completed_count[0] - # 实时写入文件 + # Ghi file Realtime save_profiles_realtime() if progress_callback: progress_callback( current, total, - f"已完成 {current}/{total}: {entity.name}({entity_type})" + f"Completed {current}/{total}: {entity.name} ({entity_type})" ) if error: - logger.warning(f"[{current}/{total}] {entity.name} 使用备用人设: {error}") + logger.warning(f"[{current}/{total}] Entity {entity.name} applied fallback profile due to error: {error}") else: - logger.info(f"[{current}/{total}] 成功生成人设: {entity.name} ({entity_type})") + logger.info(f"[{current}/{total}] Automatically generated profile for: {entity.name} ({entity_type})") except Exception as e: - logger.error(f"处理实体 {entity.name} 时发生异常: {str(e)}") + logger.error(f"Error handling profile for entity {entity.name}: {str(e)}") with lock: completed_count[0] += 1 profiles[idx] = OasisAgentProfile( @@ -999,44 +998,44 @@ def generate_single_profile(idx: int, entity: EntityNode) -> tuple: source_entity_uuid=entity.uuid, source_entity_type=entity_type, ) - # 实时写入文件(即使是备用人设) + # Ghi file Realtime file (Dù là profile xài fallback) save_profiles_realtime() print(f"\n{'='*60}") - print(f"人设生成完成!共生成 {len([p for p in profiles if p])} 个Agent") + print(f"Profile generation complete! Successfully created {len([p for p in profiles if p])} Agents") print(f"{'='*60}\n") return profiles def _print_generated_profile(self, entity_name: str, entity_type: str, profile: OasisAgentProfile): - """实时输出生成的人设到控制台(完整内容,不截断)""" + """Xuất thông tin Profile vưa gen ra Terminal để review dễ dàng (Kéo dài không bị gãy log)""" separator = "-" * 70 - # 构建完整输出内容(不截断) - topics_str = ', '.join(profile.interested_topics) if profile.interested_topics else '无' + # Xây cấu trúc Log + topics_str = ', '.join(profile.interested_topics) if profile.interested_topics else 'Không có' output_lines = [ f"\n{separator}", - f"[已生成] {entity_name} ({entity_type})", + f"[Generated] {entity_name} ({entity_type})", f"{separator}", - f"用户名: {profile.user_name}", + f"Tên tài khoản (Username): {profile.user_name}", f"", - f"【简介】", + f"【Tiểu sử / Bio】", f"{profile.bio}", f"", - f"【详细人设】", + f"【Nhân cách cụ thể / Persona】", f"{profile.persona}", f"", - f"【基本属性】", - f"年龄: {profile.age} | 性别: {profile.gender} | MBTI: {profile.mbti}", - f"职业: {profile.profession} | 国家: {profile.country}", - f"兴趣话题: {topics_str}", + f"【Thuộc tính cơ bản / Attributes】", + f"Tuổi: {profile.age} | Giới tính: {profile.gender} | MBTI: {profile.mbti}", + f"Nghề nghiệp: {profile.profession} | Quốc gia: {profile.country}", + f"Chủ đề quan tâm: {topics_str}", separator ] output = "\n".join(output_lines) - # 只输出到控制台(避免重复,logger不再输出完整内容) + # Chỉ in ra Console bằng lệnh print (Logger sẽ làm rối và có thể bị truncate) print(output) def save_profiles( @@ -1046,16 +1045,16 @@ def save_profiles( platform: str = "reddit" ): """ - 保存Profile到文件(根据平台选择正确格式) + Ghi file Profile xuống thư mục (Cấu trúc file tuỳ thuộc vào nền tảng) - OASIS平台格式要求: - - Twitter: CSV格式 - - Reddit: JSON格式 + Định dạng mặc định của Framework OASIS yêu cầu: + - Twitter: Định dạng file CSV + - Reddit: Định dạng file JSON Args: - profiles: Profile列表 - file_path: 文件路径 - platform: 平台类型 ("reddit" 或 "twitter") + profiles: Danh sách Profile + file_path: Đường dẫn lưu file + platform: Tên nền tảng ("reddit" hoặc "twitter") """ if platform == "twitter": self._save_twitter_csv(profiles, file_path) @@ -1064,73 +1063,76 @@ def save_profiles( def _save_twitter_csv(self, profiles: List[OasisAgentProfile], file_path: str): """ - 保存Twitter Profile为CSV格式(符合OASIS官方要求) - - OASIS Twitter要求的CSV字段: - - user_id: 用户ID(根据CSV顺序从0开始) - - name: 用户真实姓名 - - username: 系统中的用户名 - - user_char: 详细人设描述(注入到LLM系统提示中,指导Agent行为) - - description: 简短的公开简介(显示在用户资料页面) - - user_char vs description 区别: - - user_char: 内部使用,LLM系统提示,决定Agent如何思考和行动 - - description: 外部显示,其他用户可见的简介 + Lưu Profile hệ Twitter ở định dạng CSV (Bám vào yêu cầu kỹ thuật do OASIS ban hành) + + Các trường bắt buộc để tương thích OASIS Twitter File CSV: + - user_id: Mã định danh ID (Từ 0 theo Index mảng) + - name: Tên thật của Agent đó + - username: Tên Alias/Tài khoản xài trong hệ thống + - user_char: Bản nháp Setting cụ thể truyền vào System Prompt LLM, định hình mọi ý nghĩ/phát ngôn + - description: Bản Bio gắn ngoài hiển thị cho các User khác thấy (Ngắn gọn) + + Sự khác biệt user_char và description: + - user_char: Data nội bộ chỉ Gen AI thấy (Giống prompt điều khiển não) + - description: Public Info đưa lên trang cá nhân """ import csv - # 确保文件扩展名是.csv + # Check đuôi file có nhầm thành json không if not file_path.endswith('.csv'): file_path = file_path.replace('.json', '.csv') with open(file_path, 'w', newline='', encoding='utf-8') as f: writer = csv.writer(f) - # 写入OASIS要求的表头 + # Khởi tạo Header theo chuẩn file OASIS headers = ['user_id', 'name', 'username', 'user_char', 'description'] writer.writerow(headers) - # 写入数据行 + # Xuất từng dòng Dữ liệu Profile for idx, profile in enumerate(profiles): - # user_char: 完整人设(bio + persona),用于LLM系统提示 + # user_char: Nhân cách tổng (bio + persona) - Thả cho Prompt System LLM user_char = profile.bio if profile.persona and profile.persona != profile.bio: user_char = f"{profile.bio} {profile.persona}" - # 处理换行符(CSV中用空格替代) + # Làm sạch dấu newline để nhét vào dòng CSV user_char = user_char.replace('\n', ' ').replace('\r', ' ') - # description: 简短简介,用于外部显示 + # description: Thông tin Bio hiển thị công khai mạng xã hội description = profile.bio.replace('\n', ' ').replace('\r', ' ') row = [ - idx, # user_id: 从0开始的顺序ID - profile.name, # name: 真实姓名 - profile.user_name, # username: 用户名 - user_char, # user_char: 完整人设(内部LLM使用) - description # description: 简短简介(外部显示) + idx, # user_id: ID bắt đầu từ 0 + profile.name, # name: Tên thực + profile.user_name, # username: Tên định danh + user_char, # user_char: Mô tả ẩn của Bot + description # description: Bảng mô tả Công khai ] writer.writerow(row) - logger.info(f"已保存 {len(profiles)} 个Twitter Profile到 {file_path} (OASIS CSV格式)") + logger.info(f"Saved {len(profiles)} Twitter Profiles to {file_path} (OASIS CSV Format)") def _normalize_gender(self, gender: Optional[str]) -> str: """ - 标准化gender字段为OASIS要求的英文格式 + Biên dịch, chuẩn hóa cột Gender về đúng dạng mà OASIS engine chấp nhận - OASIS要求: male, female, other + OASIS quy định buộc xài enum: male, female, other """ if not gender: return "other" gender_lower = gender.lower().strip() - # 中文映射 + # Mapping các Keyword gender_map = { "男": "male", "女": "female", "机构": "other", "其他": "other", - # 英文已有 + "nam": "male", + "nữ": "female", + "tổ chức": "other", + # Giữ nguyên Tiếng Anh Default "male": "male", "female": "female", "other": "other", @@ -1140,41 +1142,41 @@ def _normalize_gender(self, gender: Optional[str]) -> str: def _save_reddit_json(self, profiles: List[OasisAgentProfile], file_path: str): """ - 保存Reddit Profile为JSON格式 - - 使用与 to_reddit_format() 一致的格式,确保 OASIS 能正确读取。 - 必须包含 user_id 字段,这是 OASIS agent_graph.get_agent() 匹配的关键! - - 必需字段: - - user_id: 用户ID(整数,用于匹配 initial_posts 中的 poster_agent_id) - - username: 用户名 - - name: 显示名称 - - bio: 简介 - - persona: 详细人设 - - age: 年龄(整数) - - gender: "male", "female", 或 "other" - - mbti: MBTI类型 - - country: 国家 + Lưu Profile hệ Reddit bằng JSON (Bám vào yêu cầu kỹ thuật do OASIS ban hành) + + Format cấu trúc dựa tương đồng với hàm to_reddit_format(). + Luôn luôn phải có thuộc tính user_id, KEY QUAN TRỌNG ĐỂ HỖ TRỢ HÀM agent_graph.get_agent() MAP CÁC PROFILE !!! + + Các field bắt buộc: + - user_id: User ID dạng Int + - username: ID Account + - name: Tên hiển thị + - bio: Thông tin hiển thị Bio cá nhân + - persona: Prompt Settings điều khiển Bot nội bộ + - age: Tuổi (Int) + - gender: "male", "female", hoặc "other" + - mbti: Kiểu loại nhóm MBTI + - country: Quốc gia Country """ data = [] for idx, profile in enumerate(profiles): - # 使用与 to_reddit_format() 一致的格式 + # Parse Format chung với hàm class to_reddit_format() item = { - "user_id": profile.user_id if profile.user_id is not None else idx, # 关键:必须包含 user_id + "user_id": profile.user_id if profile.user_id is not None else idx, # Quan trọng: Bắt buộc kèm "user_id" "username": profile.user_name, "name": profile.name, "bio": profile.bio[:150] if profile.bio else f"{profile.name}", "persona": profile.persona or f"{profile.name} is a participant in social discussions.", "karma": profile.karma if profile.karma else 1000, "created_at": profile.created_at, - # OASIS必需字段 - 确保都有默认值 + # Fix bù tham số ảo cho các properties bị trống "age": profile.age if profile.age else 30, "gender": self._normalize_gender(profile.gender), "mbti": profile.mbti if profile.mbti else "ISTJ", - "country": profile.country if profile.country else "中国", + "country": profile.country if profile.country else "Việt Nam", } - # 可选字段 + # Cột Tuỳ chọn if profile.profession: item["profession"] = profile.profession if profile.interested_topics: @@ -1185,16 +1187,16 @@ def _save_reddit_json(self, profiles: List[OasisAgentProfile], file_path: str): with open(file_path, 'w', encoding='utf-8') as f: json.dump(data, f, ensure_ascii=False, indent=2) - logger.info(f"已保存 {len(profiles)} 个Reddit Profile到 {file_path} (JSON格式,包含user_id字段)") + logger.info(f"Saved {len(profiles)} Reddit Profiles to {file_path} (JSON Config File - with user_id mapped)") - # 保留旧方法名作为别名,保持向后兼容 + # Giữ lại Function name cũ để hệ thống vẫn tương thích backward. def save_profiles_to_json( self, profiles: List[OasisAgentProfile], file_path: str, platform: str = "reddit" ): - """[已废弃] 请使用 save_profiles() 方法""" - logger.warning("save_profiles_to_json已废弃,请使用save_profiles方法") + """[Deprecated - Hết hạn dùng] KHUYÊN DÙNG LỆNH save_profiles() THAY VÌ PHƯƠNG THỨC NÀY""" + logger.warning("save_profiles_to_json is Deprecated. Use save_profiles method instead!") self.save_profiles(profiles, file_path, platform) diff --git a/backend/app/services/ontology_generator.py b/backend/app/services/ontology_generator.py index 2d3e39bd8..3303ad1f6 100644 --- a/backend/app/services/ontology_generator.py +++ b/backend/app/services/ontology_generator.py @@ -1,6 +1,6 @@ """ -本体生成服务 -接口1:分析文本内容,生成适合社会模拟的实体和关系类型定义 +Dịch vụ tạo Ontology (Hệ thực thể / Quan hệ) +API 1: Phân tích nội dung văn bản, khởi tạo các định nghĩa về loại thực thể và quan hệ phù hợp cho việc mô phỏng mạng xã hội """ import json @@ -8,157 +8,157 @@ from ..utils.llm_client import LLMClient -# 本体生成的系统提示词 -ONTOLOGY_SYSTEM_PROMPT = """你是一个专业的知识图谱本体设计专家。你的任务是分析给定的文本内容和模拟需求,设计适合**社交媒体舆论模拟**的实体类型和关系类型。 +# System prompt dùng cho việc tự động sinh Ontology +ONTOLOGY_SYSTEM_PROMPT = """Bạn là một chuyên gia thiết kế bản thể học (Ontology) cho Tri thức đồ thị (Knowledge Graph). Nhiệm vụ của bạn là phân tích nội dung văn bản được cung cấp và nhu cầu để thiết kế các loại thực thể (Entity) và loại mối quan hệ (Relationship) thiết kế phù hợp cho **Mô phỏng dư luận trên mạng xã hội**. -**重要:你必须输出有效的JSON格式数据,不要输出任何其他内容。** +**QUAN TRỌNG: Bạn BẮT BUỘC phải đầu ra một cấu trúc định dạng JSON hợp lệ, KHÔNG ĐƯỢC xuất thêm bất kỳ văn bản nào khác.** -## 核心任务背景 +## Bối cảnh nhiệm vụ cốt lõi -我们正在构建一个**社交媒体舆论模拟系统**。在这个系统中: -- 每个实体都是一个可以在社交媒体上发声、互动、传播信息的"账号"或"主体" -- 实体之间会相互影响、转发、评论、回应 -- 我们需要模拟舆论事件中各方的反应和信息传播路径 +Chúng tôi đang xây dựng một **hệ thống mô phỏng tin đồn và dư luận mạng xã hội**. Trong hệ thống này: +- Mỗi thực thể là một "tài khoản" hoặc "chủ thể" có thể lên tiếng, tương tác và lan truyền thông tin trên mạng xã hội. +- Các thực thể có thể gây ảnh hưởng, chuyển tiếp (retweet), bình luận hoặc phản hồi lẫn nhau. +- Chúng tôi cần mô phỏng phản ứng của các bên và đường truyền thông tin trong các sự kiện dư luận. -因此,**实体必须是现实中真实存在的、可以在社媒上发声和互动的主体**: +Do đó, **thực thể phải là các chủ thể có thật trong thế giới thực, có khả năng lên tiếng và tương tác trên mạng xã hội**: -**可以是**: -- 具体的个人(公众人物、当事人、意见领袖、专家学者、普通人) -- 公司、企业(包括其官方账号) -- 组织机构(大学、协会、NGO、工会等) -- 政府部门、监管机构 -- 媒体机构(报纸、电视台、自媒体、网站) -- 社交媒体平台本身 -- 特定群体代表(如校友会、粉丝团、维权群体等) +**CÓ THỂ LÀ**: +- Cá nhân cụ thể (nhân vật của công chúng, các bên liên quan, KOL, chuyên gia / học giả, người bình thường) +- Công ty, doanh nghiệp (bao gồm cả tài khoản chính thức của họ) +- Tổ chức (trường đại học, hiệp hội, tổ chức phi chính phủ (NGO), công đoàn, v.v.) +- Các cơ quan chính phủ, cơ quan quản lý +- Tổ chức báo chí / truyền thông (báo đài, đài truyền hình, tự do truyền thông, trang web) +- Bản thân nền tảng mạng xã hội +- Đại diện nhóm cụ thể (như hội cựu sinh viên, fan group, nhóm bảo vệ quyền lợi, v.v.) -**不可以是**: -- 抽象概念(如"舆论"、"情绪"、"趋势") -- 主题/话题(如"学术诚信"、"教育改革") -- 观点/态度(如"支持方"、"反对方") +**KHÔNG ĐƯỢC LÀ**: +- Khái niệm trừu tượng (như "dư luận", "cảm xúc", "xu hướng") +- Chủ đề / đề tài (như "tính toàn vẹn học thuật", "cải cách giáo dục") +- Quan điểm / thái độ (như "phe ủng hộ", "bên phản đối") -## 输出格式 +## Định dạng đầu ra -请输出JSON格式,包含以下结构: +Hãy trả về dưới định dạng JSON, bao gồm cấu trúc sau: ```json { "entity_types": [ { - "name": "实体类型名称(英文,PascalCase)", - "description": "简短描述(英文,不超过100字符)", + "name": "Tên loại thực thể (Tiếng Anh, PascalCase)", + "description": "Mô tả ngắn gọn (Tiếng Anh, tối đa 100 ký tự)", "attributes": [ { - "name": "属性名(英文,snake_case)", + "name": "Tên thuộc tính (Tiếng Anh, snake_case)", "type": "text", - "description": "属性描述" + "description": "Mô tả của thuộc tính" } ], - "examples": ["示例实体1", "示例实体2"] + "examples": ["Ví dụ thực thể 1", "Ví dụ thực thể 2"] } ], "edge_types": [ { - "name": "关系类型名称(英文,UPPER_SNAKE_CASE)", - "description": "简短描述(英文,不超过100字符)", + "name": "Tên loại quan hệ (Tiếng Anh, UPPER_SNAKE_CASE)", + "description": "Mô tả ngắn (Tiếng Anh, tối đa 100 ký tự)", "source_targets": [ - {"source": "源实体类型", "target": "目标实体类型"} + {"source": "Loại thực thể nguồn", "target": "Loại thực thể đích"} ], "attributes": [] } ], - "analysis_summary": "对文本内容的简要分析说明(中文)" + "analysis_summary": "Giải thích ngắn gọn phân tích của bạn về văn bản (Tiếng Việt)" } ``` -## 设计指南(极其重要!) +## Hướng dẫn Thiết kế (CỰC KỲ QUAN TRỌNG!) -### 1. 实体类型设计 - 必须严格遵守 +### 1. Thiết kế loại Thực thể (Entity Types) - Phải tuân thủ nghiêm ngặt -**数量要求:必须正好10个实体类型** +**Yêu cầu số lượng: Đúng 10 loại Thực thể.** -**层次结构要求(必须同时包含具体类型和兜底类型)**: +**Yêu cầu về cấu trúc phân cấp (Phải có cả Loại cụ thể và Loại bao quát/fallback):** -你的10个实体类型必须包含以下层次: +10 loại thực thể của bạn phải bao gồm cấp độ sau: -A. **兜底类型(必须包含,放在列表最后2个)**: - - `Person`: 任何自然人个体的兜底类型。当一个人不属于其他更具体的人物类型时,归入此类。 - - `Organization`: 任何组织机构的兜底类型。当一个组织不属于其他更具体的组织类型时,归入此类。 +A. **Loại bao quát (Fallback Types) (BẮT BUỘC, phải nằm ở 2 vị trí cuối cùng trong mảng)**: + - `Person`: Là loại bao quát cho MỌI cá nhân tự nhiên. Nếu một người không thuộc các loại cụ thể ở trên, người đó sẽ thuộc `Person`. + - `Organization`: Là loại bao quát cho MỌI tổ chức. Đặc trưng cho các tổ chức nhỏ hoặc không phù hợp với các loại tổ chức cụ thể khác. -B. **具体类型(8个,根据文本内容设计)**: - - 针对文本中出现的主要角色,设计更具体的类型 - - 例如:如果文本涉及学术事件,可以有 `Student`, `Professor`, `University` - - 例如:如果文本涉及商业事件,可以有 `Company`, `CEO`, `Employee` +B. **Loại cụ thể (8 loại, phụ thuộc vào nội dung văn bản)**: + - Thiết kế các loại cụ thể cho các vai chính được nhắc đến nhiều nhất trong văn bản. + - Ví dụ: Nếu văn bản nói về scandal trường học, có thể có: `Student`, `Professor`, `University` + - Ví dụ: Nếu văn bản là câu chuyện kinh doanh, có thể có: `Company`, `CEO`, `Employee` -**为什么需要兜底类型**: -- 文本中会出现各种人物,如"中小学教师"、"路人甲"、"某位网友" -- 如果没有专门的类型匹配,他们应该被归入 `Person` -- 同理,小型组织、临时团体等应该归入 `Organization` +**Tại sao cần các loại Bao quát (Fallback):** +- Văn bản thường chứa các thông tin như "giáo viên tiểu học", "một người qua đường", "một cư dân mạng" +- Nếu không có loại được định nghĩa riêng cho họ, họ nên thuộc về loại `Person` +- Tương tự, tổ chức nhỏ bé hoặc nhóm học tập tạm thời nên thuộc `Organization` -**具体类型的设计原则**: -- 从文本中识别出高频出现或关键的角色类型 -- 每个具体类型应该有明确的边界,避免重叠 -- description 必须清晰说明这个类型和兜底类型的区别 +**Nguyên tắc cho các loại Cụ thể:** +- Nhận dạng tần suất xuất hiện và sức ảnh hưởng tới cốt truyện để xây dựng loại thực thể. +- Mỗi loại nên có một ranh giới rõ ràng, không bị chồng chéo. +- Thuộc tính mô tả (description) phải giải thích vì sao loại này tách biệt. -### 2. 关系类型设计 +### 2. Thiết kế Cạnh/Quan hệ (Edge Types) -- 数量:6-10个 -- 关系应该反映社媒互动中的真实联系 -- 确保关系的 source_targets 涵盖你定义的实体类型 +- Số lượng: Khoảng 6-10 loại quan hệ +- Các mối quan hệ này phải giải thích và gắn kết được hành vi tương tác trên mạng xã hội của các nhân vật. +- Đảm bảo mapping quan hệ hai chiều `source_targets` khớp với các thực thể phía trên. -### 3. 属性设计 +### 3. Thiết kế Thuộc tính (Attributes) -- 每个实体类型1-3个关键属性 -- **注意**:属性名不能使用 `name`、`uuid`、`group_id`、`created_at`、`summary`(这些是系统保留字) -- 推荐使用:`full_name`, `title`, `role`, `position`, `location`, `description` 等 +- Mỗi loại thực thể cần 1-3 thuộc tính chính để làm rõ nhân thân. +- **CHÚ Ý**: Không sử dụng các ID nội bộ làm thuộc tính như `name`, `uuid`, `group_id`, `created_at`, `summary` (chúng là từ khóa hệ thống). +- Khuyên dùng: `full_name`, `title`, `role`, `position`, `location`, `description`,... -## 实体类型参考 +## Loại Thực thể tham khảo -**个人类(具体)**: -- Student: 学生 -- Professor: 教授/学者 -- Journalist: 记者 -- Celebrity: 明星/网红 -- Executive: 高管 -- Official: 政府官员 -- Lawyer: 律师 -- Doctor: 医生 +**Loại cá nhân (Cụ thể):** +- Student: Học sinh/Sinh viên +- Professor: Giáo sư/Học giả +- Journalist: Nhà báo/Phóng viên +- Celebrity: Người nổi tiếng/Idol +- Executive: Các giám đốc, CEO, cấp lãnh đạo +- Official: Các vị công chức chính phủ +- Lawyer: Luật sư +- Doctor: Y sĩ/Bác sĩ -**个人类(兜底)**: -- Person: 任何自然人(不属于上述具体类型时使用) +**Loại cá nhân (Bao quát):** +- Person: Là loại bao quát cho MỌI cá nhân tự nhiên nào không thuộc chi tiết ở trên. -**组织类(具体)**: -- University: 高校 -- Company: 公司企业 -- GovernmentAgency: 政府机构 -- MediaOutlet: 媒体机构 -- Hospital: 医院 -- School: 中小学 -- NGO: 非政府组织 +**Loại tổ chức (Cụ thể):** +- University: Đại học hoặc học viện +- Company: Doanh nghiệp hay Công ty, tập đoàn +- GovernmentAgency: Cơ quan quản lý, các cơ quan ban ngành công quyền +- MediaOutlet: Truyền thông hay Tạp chí, Đài tin tức +- Hospital: Bệnh viện / Trung tâm y tế +- School: Bậc tiểu/trung học +- NGO: Các loại Tổ chức phi chính phủ hoặc từ thiện -**组织类(兜底)**: -- Organization: 任何组织机构(不属于上述具体类型时使用) +**Loại tổ chức (Bao quát):** +- Organization: Là loại bao quát cho MỌI cơ cấu hợp tác không thuôc chi tiết tổ chức ở trên. -## 关系类型参考 +## Loại Khái niệm Liên kết (Quan Hệ) -- WORKS_FOR: 工作于 -- STUDIES_AT: 就读于 -- AFFILIATED_WITH: 隶属于 -- REPRESENTS: 代表 -- REGULATES: 监管 -- REPORTS_ON: 报道 -- COMMENTS_ON: 评论 -- RESPONDS_TO: 回应 -- SUPPORTS: 支持 -- OPPOSES: 反对 -- COLLABORATES_WITH: 合作 -- COMPETES_WITH: 竞争 +- WORKS_FOR: Làm việc và ăn lương bởi tổ chức +- STUDIES_AT: Đang học tại nhà trường +- AFFILIATED_WITH: Liên quan, Trực thuộc vào đơn vị +- REPRESENTS: Thể hiện tư cách hành động đại diện cho tập thể +- REGULATES: Theo dõi, quản lý, thanh tra chính sách +- REPORTS_ON: Tác nghiệp báo chí, có tin về hiện tượng +- COMMENTS_ON: Có phản hồi hoặc lên tiếng về tranh cãi +- RESPONDS_TO: Hành động đáp trả +- SUPPORTS: Theo phe ủng hộ điều luật +- OPPOSES: Phản đối chính sách +- COLLABORATES_WITH: Tham gia phối ứng xử lý sự cố. +- COMPETES_WITH: Quan hệ thù địch. """ class OntologyGenerator: """ - 本体生成器 - 分析文本内容,生成实体和关系类型定义 + Trình khởi tạo Ontology + Phân tích nội dung đoạn văn bản truyền vào, sau đó tự động suy luận ra định nghĩa của các loại Thực thể và Mối quan hệ """ def __init__(self, llm_client: Optional[LLMClient] = None): @@ -171,17 +171,17 @@ def generate( additional_context: Optional[str] = None ) -> Dict[str, Any]: """ - 生成本体定义 + Khởi tạo định nghĩa Ontology Args: - document_texts: 文档文本列表 - simulation_requirement: 模拟需求描述 - additional_context: 额外上下文 + document_texts: Danh sách mảng các văn bản nội dung nguồn + simulation_requirement: Chuỗi mô tả nhu cầu mô phỏng của người dùng + additional_context: Văn bản cung cấp thêm các ngữ cảnh phụ (nếu có) Returns: - 本体定义(entity_types, edge_types等) + Dictionary gồm cấu trúc Ontology (entity_types, edge_types v.v.) """ - # 构建用户消息 + # Tạo câu lệnh Prompt gửi cho mô hình LLM user_message = self._build_user_message( document_texts, simulation_requirement, @@ -193,19 +193,19 @@ def generate( {"role": "user", "content": user_message} ] - # 调用LLM + # Gửi request đến LLM result = self.llm_client.chat_json( messages=messages, temperature=0.3, max_tokens=4096 ) - # 验证和后处理 + # Kiểm tra tính hợp lệ và xử lý tinh chỉnh kết quả đầu ra result = self._validate_and_process(result) return result - # 传给 LLM 的文本最大长度(5万字) + # Định mức giới hạn độ dài ký tự tối đa của đoạn văn bản có thể gửi cho LLM (5 vạn chữ) MAX_TEXT_LENGTH_FOR_LLM = 50000 def _build_user_message( @@ -214,50 +214,50 @@ def _build_user_message( simulation_requirement: str, additional_context: Optional[str] ) -> str: - """构建用户消息""" + """Ghép các thông tin đầu vào thành User Prompt hoàn chỉnh để gửi tới LLM""" - # 合并文本 + # Gộp tất cả các đoạn văn bản thành một string duy nhất combined_text = "\n\n---\n\n".join(document_texts) original_length = len(combined_text) - # 如果文本超过5万字,截断(仅影响传给LLM的内容,不影响图谱构建) + # Nếu vượt quá giới hạn tối đa, thực hiện cắt bớt (Việc này chỉ ảnh hưởng prompt gửi nhận diện Ontology, không ảnh hưởng thư viện Graph building ở sau) 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}字用于本体分析)..." + combined_text += f"\n\n...(Văn bản gốc dài {original_length} chữ, đã chủ động cắt lấy {self.MAX_TEXT_LENGTH_FOR_LLM} chữ đầu tiên để phục vụ phân tích Ontology)..." - message = f"""## 模拟需求 + message = f"""## Nhu cầu mô phỏng {simulation_requirement} -## 文档内容 +## Nội dung tài liệu {combined_text} """ if additional_context: message += f""" -## 额外说明 +## Giải thích bổ sung {additional_context} """ message += """ -请根据以上内容,设计适合社会舆论模拟的实体类型和关系类型。 - -**必须遵守的规则**: -1. 必须正好输出10个实体类型 -2. 最后2个必须是兜底类型:Person(个人兜底)和 Organization(组织兜底) -3. 前8个是根据文本内容设计的具体类型 -4. 所有实体类型必须是现实中可以发声的主体,不能是抽象概念 -5. 属性名不能使用 name、uuid、group_id 等保留字,用 full_name、org_name 等替代 +Dựa vào các thông tin trên đây, hãy thiết kế các loại mô hình Thực Thể và Quan Hệ phù hợp để phục vụ việc mô phỏng dư luận trên mạng xã hội. + +**Các quy tắc BẮT BUỘC tuân thủ**: +1. Số lượng chính xác: Xuất phải CHUẨN XÁC 10 loại Thực thể +2. 2 vị trí cuối cùng bắt buộc là từ Khoá phụ (Fallback): Person (Cho cá nhân) và Organization (Cho Tổ chức) +3. 8 vị trí đầu tiên phải phân tích và suy luận dựa vào cấu trúc của chính văn bản truyền vào +4. Tất cả các thực thể được liệt kê phải đóng vai trò là Chủ thể (nhân vật có thể lên tiếng ngoài đời thực), KHÔNG ĐƯỢC dùng làm khái niệm trừu tượng. +5. Tên biến thuộc tính KHÔNG ĐƯỢC là name, uuid, group_id hay các biến số bảo lưu của hệ thống khác. Vui lòng chuyển thành full_name, org_name, v.v. """ return message def _validate_and_process(self, result: Dict[str, Any]) -> Dict[str, Any]: - """验证和后处理结果""" + """Tiền kiểm tra tính trọn vẹn và cấu trúc của dữ liệu phản hồi JSON""" - # 确保必要字段存在 + # Đảm bảo các thuộc tính mảng bắt buộc phải xuất hiện if "entity_types" not in result: result["entity_types"] = [] if "edge_types" not in result: @@ -265,17 +265,17 @@ def _validate_and_process(self, result: Dict[str, Any]) -> Dict[str, Any]: if "analysis_summary" not in result: result["analysis_summary"] = "" - # 验证实体类型 + # Tiền xử lý để loại thực thể hợp lệ for entity in result["entity_types"]: if "attributes" not in entity: entity["attributes"] = [] if "examples" not in entity: entity["examples"] = [] - # 确保description不超过100字符 + # Cắt ngắn description nếu vượt quá độ dài tối đa 100 character if len(entity.get("description", "")) > 100: entity["description"] = entity["description"][:97] + "..." - # 验证关系类型 + # Tiền xử lý để loại quan hệ hợp lệ for edge in result["edge_types"]: if "source_targets" not in edge: edge["source_targets"] = [] @@ -284,11 +284,11 @@ def _validate_and_process(self, result: Dict[str, Any]) -> Dict[str, Any]: if len(edge.get("description", "")) > 100: edge["description"] = edge["description"][:97] + "..." - # Zep API 限制:最多 10 个自定义实体类型,最多 10 个自定义边类型 + # Ràng buộc số lượng đầu ra của API Zep: Tối đa 10 loại thực thể tự tuỳ chỉnh, và Tối đa 10 loại cạnh quan hệ tùy chỉnh MAX_ENTITY_TYPES = 10 MAX_EDGE_TYPES = 10 - # 兜底类型定义 + # Khai báo định nghĩa về 2 đối tượng bao quát (fallback) mặc định person_fallback = { "name": "Person", "description": "Any individual person not fitting other specific person types.", @@ -309,12 +309,12 @@ def _validate_and_process(self, result: Dict[str, Any]) -> Dict[str, Any]: "examples": ["small business", "community group"] } - # 检查是否已有兜底类型 + # Sàng lọc kiểm tra xem kết quả đầu ra đã chứa sẵn các danh mục rỗng (fallback) ở vị trí chuẩn chưa entity_names = {e["name"] for e in result["entity_types"]} has_person = "Person" in entity_names has_organization = "Organization" in entity_names - # 需要添加的兜底类型 + # Danh sách cần phải gán bù vào fallbacks_to_add = [] if not has_person: fallbacks_to_add.append(person_fallback) @@ -325,17 +325,17 @@ def _validate_and_process(self, result: Dict[str, Any]) -> Dict[str, Any]: current_count = len(result["entity_types"]) needed_slots = len(fallbacks_to_add) - # 如果添加后会超过 10 个,需要移除一些现有类型 + # Nếu thêm vào bị quá giới hạn 10 loại, cần phải loại bỏ bớt các loại Entity đằng trước if current_count + needed_slots > MAX_ENTITY_TYPES: - # 计算需要移除多少个 + # Tính lượng bị thừa ra so với hạn mức (Để bỏ đi) to_remove = current_count + needed_slots - MAX_ENTITY_TYPES - # 从末尾移除(保留前面更重要的具体类型) + # Bỏ bớt n vị trí tính từ cuối mảng (Đảm bảo chừa lại nhóm các thực thể cụ thể đã phân tích ở trên) result["entity_types"] = result["entity_types"][:-to_remove] - # 添加兜底类型 + # Nối cụm Fallbacks vừa khởi tạo vào cuối chuỗi result["entity_types"].extend(fallbacks_to_add) - # 最终确保不超过限制(防御性编程) + # Đảm bảo phòng thủ một lần cuối cùng không có quá 10 Element Array if len(result["entity_types"]) > MAX_ENTITY_TYPES: result["entity_types"] = result["entity_types"][:MAX_ENTITY_TYPES] @@ -346,29 +346,29 @@ def _validate_and_process(self, result: Dict[str, Any]) -> Dict[str, Any]: def generate_python_code(self, ontology: Dict[str, Any]) -> str: """ - 将本体定义转换为Python代码(类似ontology.py) + Dựng (Generate) file Script với nội dung Python Class tương ứng khai báo dữ liệu Ontology để máy đọc (Tương tự như file ontology.py) Args: - ontology: 本体定义 + ontology: Từ điển định nghĩa Ontology Returns: - Python代码字符串 + Chuỗi đoạn code File Python cần tạo để lưu """ code_lines = [ '"""', - '自定义实体类型定义', - '由MiroFish自动生成,用于社会舆论模拟', + 'Các loại đối tượng (Thực thể) tuỳ chỉnh', + 'Được khởi tạo tự động bởi công cụ MiroFish, ứng dụng vào việc chạy giả lập diễn biến dư luận', '"""', '', 'from pydantic import Field', 'from zep_cloud.external_clients.ontology import EntityModel, EntityText, EdgeModel', '', '', - '# ============== 实体类型定义 ==============', + '# ============== Định nghĩa Tên Lớp Các thực thể (Entity) ==============', '', ] - # 生成实体类型 + # Khởi tạo các đoạn mã tương ứng với Định nghĩa thực thể Entity for entity in ontology.get("entity_types", []): name = entity["name"] desc = entity.get("description", f"A {name} entity.") @@ -391,13 +391,13 @@ def generate_python_code(self, ontology: Dict[str, Any]) -> str: code_lines.append('') code_lines.append('') - code_lines.append('# ============== 关系类型定义 ==============') + code_lines.append('# ============== Định nghĩa Các Nhóm Quan Hệ/Hành Vi (Edge) ==============') code_lines.append('') - # 生成关系类型 + # Khởi tạo các đoạn mã tạo lập Relationship (Edges) for edge in ontology.get("edge_types", []): name = edge["name"] - # 转换为PascalCase类名 + # Đổi cấu trúc tên format Class theo chuẩn PascalCase của Python class_name = ''.join(word.capitalize() for word in name.split('_')) desc = edge.get("description", f"A {name} relationship.") @@ -419,8 +419,8 @@ def generate_python_code(self, ontology: Dict[str, Any]) -> str: code_lines.append('') code_lines.append('') - # 生成类型字典 - code_lines.append('# ============== 类型配置 ==============') + # Tự động kết xuất ra dictionary mapping từ Tên Loại - sang class Object + code_lines.append('# ============== Các tuỳ chỉnh Map Cấu Hình ==============') code_lines.append('') code_lines.append('ENTITY_TYPES = {') for entity in ontology.get("entity_types", []): @@ -436,7 +436,7 @@ def generate_python_code(self, ontology: Dict[str, Any]) -> str: code_lines.append('}') code_lines.append('') - # 生成边的source_targets映射 + # Cấu hình mảng mapping giới hạn Source->Target cho từng cạnh (Edges source_targets config) code_lines.append('EDGE_SOURCE_TARGETS = {') for edge in ontology.get("edge_types", []): name = edge["name"] diff --git a/backend/app/services/report_agent.py b/backend/app/services/report_agent.py index 02ca5bdc2..687c0924a 100644 --- a/backend/app/services/report_agent.py +++ b/backend/app/services/report_agent.py @@ -1,12 +1,12 @@ """ -Report Agent服务 -使用LangChain + Zep实现ReACT模式的模拟报告生成 - -功能: -1. 根据模拟需求和Zep图谱信息生成报告 -2. 先规划目录结构,然后分段生成 -3. 每段采用ReACT多轮思考与反思模式 -4. 支持与用户对话,在对话中自主调用检索工具 +Dịch vụ Report Agent +Sử dụng LangChain + Zep để thực hiện tạo báo cáo mô phỏng theo mô hình ReACT + +Chức năng: +1. Dựa trên yêu cầu mô phỏng và thông tin đồ thị Zep để tạo ra báo cáo +2. Lên kế hoạch cho cấu trúc mục lục trước, sau đó tạo từng đoạn +3. Mỗi đoạn áp dụng mô hình ReACT để suy nghĩ đa vòng và phản xạ +4. Hỗ trợ hội thoại với người dùng, tự động gọi công cụ tìm kiếm trong hội thoại """ import os @@ -34,18 +34,18 @@ class ReportLogger: """ - Report Agent 详细日志记录器 + Trình ghi chi tiết log của Report Agent - 在报告文件夹中生成 agent_log.jsonl 文件,记录每一步详细动作。 - 每行是一个完整的 JSON 对象,包含时间戳、动作类型、详细内容等。 + Tạo file agent_log.jsonl trong thư mục báo cáo, ghi lại chi tiết từng bước. + Mỗi dòng là một đối tượng JSON hoàn chỉnh, bao gồm timestamp, loại hành động, nội dung chi tiết... """ def __init__(self, report_id: str): """ - 初始化日志记录器 + Khởi tạo trình ghi log Args: - report_id: 报告ID,用于确定日志文件路径 + report_id: ID của báo cáo, dùng để quyết định đường dẫn file log """ self.report_id = report_id self.log_file_path = os.path.join( @@ -55,12 +55,12 @@ def __init__(self, report_id: str): self._ensure_log_file() def _ensure_log_file(self): - """确保日志文件所在目录存在""" + """Đảm bảo thư mục chứa file log đã tồn tại""" log_dir = os.path.dirname(self.log_file_path) os.makedirs(log_dir, exist_ok=True) def _get_elapsed_time(self) -> float: - """获取从开始到现在的耗时(秒)""" + """Lấy thời gian tiêu tốn từ lúc bắt đầu tới hiện tại (giây)""" return (datetime.now() - self.start_time).total_seconds() def log( @@ -72,14 +72,14 @@ def log( section_index: int = None ): """ - 记录一条日志 + Ghi lại một dòng log Args: - action: 动作类型,如 'start', 'tool_call', 'llm_response', 'section_complete' 等 - stage: 当前阶段,如 'planning', 'generating', 'completed' - details: 详细内容字典,不截断 - section_title: 当前章节标题(可选) - section_index: 当前章节索引(可选) + action: Loại hành động, ví dụ 'start', 'tool_call', 'llm_response', 'section_complete' v.v.. + stage: Giai đoạn hiện tại, ví dụ 'planning', 'generating', 'completed' + details: Dictionary chứa nội dung chi tiết + section_title: Tiêu đề chương hiện tại (tùy chọn) + section_index: Index của chương hiện tại (tùy chọn) """ log_entry = { "timestamp": datetime.now().isoformat(), @@ -92,12 +92,12 @@ def log( "details": details } - # 追加写入 JSONL 文件 + # Ghi nối tiếp vào file JSONL with open(self.log_file_path, 'a', encoding='utf-8') as f: f.write(json.dumps(log_entry, ensure_ascii=False) + '\n') def log_start(self, simulation_id: str, graph_id: str, simulation_requirement: str): - """记录报告生成开始""" + """Ghi log báo cáo bắt đầu tạo""" self.log( action="report_start", stage="pending", @@ -105,52 +105,52 @@ def log_start(self, simulation_id: str, graph_id: str, simulation_requirement: s "simulation_id": simulation_id, "graph_id": graph_id, "simulation_requirement": simulation_requirement, - "message": "报告生成任务开始" + "message": "Report generation task started" } ) def log_planning_start(self): - """记录大纲规划开始""" + """Ghi log kế hoạch dàn ý bắt đầu""" self.log( action="planning_start", stage="planning", - details={"message": "开始规划报告大纲"} + details={"message": "Start planning report outline"} ) def log_planning_context(self, context: Dict[str, Any]): - """记录规划时获取的上下文信息""" + """Ghi log thông tin context lấy được khi lên kế hoạch""" self.log( action="planning_context", stage="planning", details={ - "message": "获取模拟上下文信息", + "message": "Fetch simulation context info", "context": context } ) def log_planning_complete(self, outline_dict: Dict[str, Any]): - """记录大纲规划完成""" + """Ghi log kế hoạch dàn ý hoàn thành""" self.log( action="planning_complete", stage="planning", details={ - "message": "大纲规划完成", + "message": "Outline planning completed", "outline": outline_dict } ) def log_section_start(self, section_title: str, section_index: int): - """记录章节生成开始""" + """Ghi log tiến trình bắt đầu tạo chương""" self.log( action="section_start", stage="generating", section_title=section_title, section_index=section_index, - details={"message": f"开始生成章节: {section_title}"} + details={"message": f"Start generating section: {section_title}"} ) def log_react_thought(self, section_title: str, section_index: int, iteration: int, thought: str): - """记录 ReACT 思考过程""" + """Ghi log quá trình suy nghĩ ReACT""" self.log( action="react_thought", stage="generating", @@ -159,7 +159,7 @@ def log_react_thought(self, section_title: str, section_index: int, iteration: i details={ "iteration": iteration, "thought": thought, - "message": f"ReACT 第{iteration}轮思考" + "message": f"ReACT thought iteration {iteration}" } ) @@ -171,7 +171,7 @@ def log_tool_call( parameters: Dict[str, Any], iteration: int ): - """记录工具调用""" + """Ghi log thao tác gọi công cụ""" self.log( action="tool_call", stage="generating", @@ -181,7 +181,7 @@ def log_tool_call( "iteration": iteration, "tool_name": tool_name, "parameters": parameters, - "message": f"调用工具: {tool_name}" + "message": f"Calling tool: {tool_name}" } ) @@ -193,7 +193,7 @@ def log_tool_result( result: str, iteration: int ): - """记录工具调用结果(完整内容,不截断)""" + """Ghi log kết quả gọi công cụ (Toàn bộ nội dung)""" self.log( action="tool_result", stage="generating", @@ -202,9 +202,9 @@ def log_tool_result( details={ "iteration": iteration, "tool_name": tool_name, - "result": result, # 完整结果,不截断 + "result": result, # Kết quả đầy đủ, không cắt ngắn "result_length": len(result), - "message": f"工具 {tool_name} 返回结果" + "message": f"Tool {tool_name} returned result" } ) @@ -217,7 +217,7 @@ def log_llm_response( has_tool_calls: bool, has_final_answer: bool ): - """记录 LLM 响应(完整内容,不截断)""" + """Ghi nhận phản hồi LLM (nội dung đầy đủ, không cắt ngắn)""" self.log( action="llm_response", stage="generating", @@ -225,11 +225,11 @@ def log_llm_response( section_index=section_index, details={ "iteration": iteration, - "response": response, # 完整响应,不截断 + "response": response, # Phản hồi đầy đủ, không cắt ngắn "response_length": len(response), "has_tool_calls": has_tool_calls, "has_final_answer": has_final_answer, - "message": f"LLM 响应 (工具调用: {has_tool_calls}, 最终答案: {has_final_answer})" + "message": f"LLM response (Tool call: {has_tool_calls}, Final answer: {has_final_answer})" } ) @@ -240,17 +240,17 @@ def log_section_content( content: str, tool_calls_count: int ): - """记录章节内容生成完成(仅记录内容,不代表整个章节完成)""" + """Ghi nhận nội dung chương đã tạo (chỉ ghi nhận nội dung, không có nghĩa là toàn bộ chương đã hoàn thành)""" self.log( action="section_content", stage="generating", section_title=section_title, section_index=section_index, details={ - "content": content, # 完整内容,不截断 + "content": content, # Nội dung đầy đủ, không cắt ngắn "content_length": len(content), "tool_calls_count": tool_calls_count, - "message": f"章节 {section_title} 内容生成完成" + "message": f"Section {section_title} content generation completed" } ) @@ -261,9 +261,9 @@ def log_section_full_complete( full_content: str ): """ - 记录章节生成完成 + Ghi nhận chương đã hoàn thành - 前端应监听此日志来判断一个章节是否真正完成,并获取完整内容 + Frontend nên lắng nghe nhật ký này để xác định chương đó có thực sự hoàn thành hay không, và lấy nội dung đầy đủ """ self.log( action="section_complete", @@ -273,24 +273,24 @@ def log_section_full_complete( details={ "content": full_content, "content_length": len(full_content), - "message": f"章节 {section_title} 生成完成" + "message": f"Section {section_title} generation completed" } ) def log_report_complete(self, total_sections: int, total_time_seconds: float): - """记录报告生成完成""" + """Ghi nhận việc tạo báo cáo hoàn tất""" self.log( action="report_complete", stage="completed", details={ "total_sections": total_sections, "total_time_seconds": round(total_time_seconds, 2), - "message": "报告生成完成" + "message": "Report generation completed" } ) def log_error(self, error_message: str, stage: str, section_title: str = None): - """记录错误""" + """Ghi nhận lỗi""" self.log( action="error", stage=stage, @@ -298,25 +298,25 @@ def log_error(self, error_message: str, stage: str, section_title: str = None): section_index=None, details={ "error": error_message, - "message": f"发生错误: {error_message}" + "message": f"An error occurred: {error_message}" } ) class ReportConsoleLogger: """ - Report Agent 控制台日志记录器 + Trình ghi Log qua console của Report Agent - 将控制台风格的日志(INFO、WARNING等)写入报告文件夹中的 console_log.txt 文件。 - 这些日志与 agent_log.jsonl 不同,是纯文本格式的控制台输出。 + Ghi nhật ký kiểu console (INFO, WARNING, v.v.) vào tệp console_log.txt trong mục lưu báo cáo. + Khác với agent_log.jsonl, những nhật ký này ở định dạng văn bản thuần túy. """ def __init__(self, report_id: str): """ - 初始化控制台日志记录器 + Khởi tạo trình ghi log console Args: - report_id: 报告ID,用于确定日志文件路径 + report_id: ID báo cáo, dùng để tự xác định đường dẫn file log """ self.report_id = report_id self.log_file_path = os.path.join( @@ -327,15 +327,15 @@ def __init__(self, report_id: str): self._setup_file_handler() def _ensure_log_file(self): - """确保日志文件所在目录存在""" + """Đảm bảo thư mục lưu file log tồn tại""" log_dir = os.path.dirname(self.log_file_path) os.makedirs(log_dir, exist_ok=True) def _setup_file_handler(self): - """设置文件处理器,将日志同时写入文件""" + """Cấu hình trình xử lý tệp (FileHandler) để ghi nhật ký vào tệp""" import logging - # 创建文件处理器 + # Tạo file handler self._file_handler = logging.FileHandler( self.log_file_path, mode='a', @@ -343,14 +343,14 @@ def _setup_file_handler(self): ) self._file_handler.setLevel(logging.INFO) - # 使用与控制台相同的简洁格式 + # Cấu trúc log đơn giản tương tự như phiên làm việc console formatter = logging.Formatter( '[%(asctime)s] %(levelname)s: %(message)s', datefmt='%H:%M:%S' ) self._file_handler.setFormatter(formatter) - # 添加到 report_agent 相关的 logger + # Thêm file hander vào logger của report_agent loggers_to_attach = [ 'mirofish.report_agent', 'mirofish.zep_tools', @@ -358,12 +358,12 @@ def _setup_file_handler(self): for logger_name in loggers_to_attach: target_logger = logging.getLogger(logger_name) - # 避免重复添加 + # Tránh thêm lại handler trùng lặp if self._file_handler not in target_logger.handlers: target_logger.addHandler(self._file_handler) def close(self): - """关闭文件处理器并从 logger 中移除""" + """Đóng file handler và gỡ nó khỏi cấu hình logger""" import logging if self._file_handler: @@ -381,12 +381,12 @@ def close(self): self._file_handler = None def __del__(self): - """析构时确保关闭文件处理器""" + """Đảm bảo đóng file handler khi hàm hủy (destructor) gọi""" self.close() class ReportStatus(str, Enum): - """报告状态""" + """Trạng thái báo cáo""" PENDING = "pending" PLANNING = "planning" GENERATING = "generating" @@ -396,7 +396,7 @@ class ReportStatus(str, Enum): @dataclass class ReportSection: - """报告章节""" + """Chương báo cáo""" title: str content: str = "" @@ -407,7 +407,7 @@ def to_dict(self) -> Dict[str, Any]: } def to_markdown(self, level: int = 2) -> str: - """转换为Markdown格式""" + """Chuyển đổi sang định dạng Markdown""" md = f"{'#' * level} {self.title}\n\n" if self.content: md += f"{self.content}\n\n" @@ -416,7 +416,7 @@ def to_markdown(self, level: int = 2) -> str: @dataclass class ReportOutline: - """报告大纲""" + """Dàn ý báo cáo""" title: str summary: str sections: List[ReportSection] @@ -429,7 +429,7 @@ def to_dict(self) -> Dict[str, Any]: } def to_markdown(self) -> str: - """转换为Markdown格式""" + """Chuyển đổi sang định dạng Markdown""" md = f"# {self.title}\n\n" md += f"> {self.summary}\n\n" for section in self.sections: @@ -439,7 +439,7 @@ def to_markdown(self) -> str: @dataclass class Report: - """完整报告""" + """Báo cáo đầy đủ""" report_id: str simulation_id: str graph_id: str @@ -467,417 +467,417 @@ def to_dict(self) -> Dict[str, Any]: # ═══════════════════════════════════════════════════════════════ -# Prompt 模板常量 +# Hằng số Prompt Mẫu # ═══════════════════════════════════════════════════════════════ -# ── 工具描述 ── +# ── Mô tả Công cụ ── TOOL_DESC_INSIGHT_FORGE = """\ -【深度洞察检索 - 强大的检索工具】 -这是我们强大的检索函数,专为深度分析设计。它会: -1. 自动将你的问题分解为多个子问题 -2. 从多个维度检索模拟图谱中的信息 -3. 整合语义搜索、实体分析、关系链追踪的结果 -4. 返回最全面、最深度的检索内容 - -【使用场景】 -- 需要深入分析某个话题 -- 需要了解事件的多个方面 -- 需要获取支撑报告章节的丰富素材 - -【返回内容】 -- 相关事实原文(可直接引用) -- 核心实体洞察 -- 关系链分析""" +[Deep Insight Retrieval - Powerful Retrieval Tool] +This is our powerful retrieval function, specifically designed for deep analysis. It will: +1. Automatically decompose your question into multiple sub-questions +2. Retrieve information from the simulation graph across multiple dimensions +3. Integrate the results of semantic search, entity analysis, and relationship chain tracking +4. Return the most comprehensive and deeply retrieved content + +[Usage Scenarios] +- When you need to analyze a topic deeply +- When you need to understand multiple aspects of an event +- When you need rich material to support a report section + +[Returned Content] +- Relevant original facts (can be cited directly) +- Core entity insights +- Relationship chain analysis""" TOOL_DESC_PANORAMA_SEARCH = """\ -【广度搜索 - 获取全貌视图】 -这个工具用于获取模拟结果的完整全貌,特别适合了解事件演变过程。它会: -1. 获取所有相关节点和关系 -2. 区分当前有效的事实和历史/过期的事实 -3. 帮助你了解舆情是如何演变的 - -【使用场景】 -- 需要了解事件的完整发展脉络 -- 需要对比不同阶段的舆情变化 -- 需要获取全面的实体和关系信息 - -【返回内容】 -- 当前有效事实(模拟最新结果) -- 历史/过期事实(演变记录) -- 所有涉及的实体""" +[Panorama Search - Get a Full View] +This tool is used to get a complete overview of the simulation results, especially suitable for understanding the evolution of an event. It will: +1. Get all relevant nodes and relationships +2. Distinguish between current valid facts and historical/expired facts +3. Help you understand how public opinion evolves + +[Usage Scenarios] +- Need to understand the full development context of an event +- Need to compare public opinion changes across different stages +- Need comprehensive entity and relationship information + +[Returned Content] +- Current valid facts (latest simulation results) +- Historical/expired facts (evolution record) +- All involved entities""" TOOL_DESC_QUICK_SEARCH = """\ -【简单搜索 - 快速检索】 -轻量级的快速检索工具,适合简单、直接的信息查询。 +[Quick Search - Fast Retrieval] +A lightweight fast retrieval tool, suitable for simple, direct information queries. -【使用场景】 -- 需要快速查找某个具体信息 -- 需要验证某个事实 -- 简单的信息检索 +[Usage Scenarios] +- Need to quickly look up a specific piece of information +- Need to verify a fact +- Simple information retrieval -【返回内容】 -- 与查询最相关的事实列表""" +[Returned Content] +- List of facts most relevant to the query""" TOOL_DESC_INTERVIEW_AGENTS = """\ -【深度采访 - 真实Agent采访(双平台)】 -调用OASIS模拟环境的采访API,对正在运行的模拟Agent进行真实采访! -这不是LLM模拟,而是调用真实的采访接口获取模拟Agent的原始回答。 -默认在Twitter和Reddit两个平台同时采访,获取更全面的观点。 - -功能流程: -1. 自动读取人设文件,了解所有模拟Agent -2. 智能选择与采访主题最相关的Agent(如学生、媒体、官方等) -3. 自动生成采访问题 -4. 调用 /api/simulation/interview/batch 接口在双平台进行真实采访 -5. 整合所有采访结果,提供多视角分析 - -【使用场景】 -- 需要从不同角色视角了解事件看法(学生怎么看?媒体怎么看?官方怎么说?) -- 需要收集多方意见和立场 -- 需要获取模拟Agent的真实回答(来自OASIS模拟环境) -- 想让报告更生动,包含"采访实录" - -【返回内容】 -- 被采访Agent的身份信息 -- 各Agent在Twitter和Reddit两个平台的采访回答 -- 关键引言(可直接引用) -- 采访摘要和观点对比 - -【重要】需要OASIS模拟环境正在运行才能使用此功能!""" - -# ── 大纲规划 prompt ── +[Deep Interview - Real Agent Interview (Dual Platform)] +Call the Oasis simulation environment's interview API to conduct real interviews with currently running simulation Agents! +This is not an LLM simulation, but calls the real interview endpoint to get the simulation Agent's original answer. +By default, it interviews simultaneously on Twitter and Reddit to get a more comprehensive perspective. + +Functional Flow: +1. Automatically reads persona files to understand all simulation Agents +2. Smartly selects Agents most relevant to the interview topic (e.g., student, media, official) +3. Automatically generates interview questions +4. Calls the /api/simulation/interview/batch endpoint for real interviews on dual platforms +5. Integrates all interview results, providing multi-perspective analysis + +[Usage Scenarios] +- Need to understand views on an event from different role perspectives (What do students think? Media? Officials?) +- Need to collect multiple opinions and stances +- Need to get the simulation Agent's real answer (from the Oasis simulation environment) +- Want to make the report more vivid, including "interview transcripts" + +[Returned Content] +- Identity info of the interviewed Agents +- Each Agent's interview answers on both Twitter and Reddit +- Key quotes (can be cited directly) +- Interview summary and perspective comparison + +[IMPORTANT] The Oasis simulation environment MUST be running to use this feature!""" + +# ── Prompt hoạch định dàn ý ── PLAN_SYSTEM_PROMPT = """\ -你是一个「未来预测报告」的撰写专家,拥有对模拟世界的「上帝视角」——你可以洞察模拟中每一位Agent的行为、言论和互动。 - -【核心理念】 -我们构建了一个模拟世界,并向其中注入了特定的「模拟需求」作为变量。模拟世界的演化结果,就是对未来可能发生情况的预测。你正在观察的不是"实验数据",而是"未来的预演"。 - -【你的任务】 -撰写一份「未来预测报告」,回答: -1. 在我们设定的条件下,未来发生了什么? -2. 各类Agent(人群)是如何反应和行动? -3. 这个模拟揭示了哪些值得关注的未来趋势和风险? - -【报告定位】 -- ✅ 这是一份基于模拟的未来预测报告,揭示"如果这样,未来会怎样" -- ✅ 聚焦于预测结果:事件走向、群体反应、涌现现象、潜在风险 -- ✅ 模拟世界中的Agent言行就是对未来人群行为的预测 -- ❌ 不是对现实世界现状的分析 -- ❌ 不是泛泛而谈的舆情综述 - -【章节数量限制】 -- 最少2个章节,最多5个章节 -- 不需要子章节,每个章节直接撰写完整内容 -- 内容要精炼,聚焦于核心预测发现 -- 章节结构由你根据预测结果自主设计 - -请输出JSON格式的报告大纲,格式如下: +You are a writing expert for "Future Prediction Reports", possessing a "God's eye view" of the simulated world - you can observe the behaviors, speeches, and interactions of every Agent in the simulation. + +[Core Concept] +We have built a simulated world and injected specific "simulation requirements" into it as variables. The evolutionary outcome of the simulated world is the prediction of what might happen in the future. What you are observing is not "experimental data", but a "preview of the future". + +[Your Task] +Write a "Future Prediction Report" to answer: +1. Under our set conditions, what happened in the future? +2. How did various Agents (groups) react and act? +3. What noteworthy future trends and risks did this simulation reveal? + +[Report Positioning] +- ✅ This is a simulation-based future prediction report, revealing "if this, what will the future be like" +- ✅ Focus on prediction results: event direction, group reactions, emergent phenomena, potential risks +- ✅ The actions and words of Agents in the simulated world are predictions of future human behavior +- ❌ Not an analysis of the real world's current status +- ❌ Not a general public opinion overview + +[Chapter Quantity Limit] +- Minimum of 2 chapters, maximum of 5 chapters +- No sub-chapters needed, write complete content directly for each chapter +- Content must be concise, focused on core prediction findings +- Chapter structure should be designed by you independently based on prediction results + +Please output the report outline in JSON format as follows: { - "title": "报告标题", - "summary": "报告摘要(一句话概括核心预测发现)", + "title": "Report Title", + "summary": "Report Summary (One sentence summarizing the core prediction findings)", "sections": [ { - "title": "章节标题", - "description": "章节内容描述" + "title": "Chapter Title", + "description": "Chapter Content Description" } ] } -注意:sections数组最少2个,最多5个元素!""" +Note: The sections array must have a minimum of 2 and a maximum of 5 elements!""" PLAN_USER_PROMPT_TEMPLATE = """\ -【预测场景设定】 -我们向模拟世界注入的变量(模拟需求):{simulation_requirement} +[Prediction Scenario Context] +Variables (simulation requirements) we injected into the simulated world: {simulation_requirement} -【模拟世界规模】 -- 参与模拟的实体数量: {total_nodes} -- 实体间产生的关系数量: {total_edges} -- 实体类型分布: {entity_types} -- 活跃Agent数量: {total_entities} +[Simulated World Scale] +- Number of entities participating in the simulation: {total_nodes} +- Number of relationships generated between entities: {total_edges} +- Entity type distribution: {entity_types} +- Number of active Agents: {total_entities} -【模拟预测到的部分未来事实样本】 +[Sample of some future facts predicted by the simulation] {related_facts_json} -请以「上帝视角」审视这个未来预演: -1. 在我们设定的条件下,未来呈现出了什么样的状态? -2. 各类人群(Agent)是如何反应和行动的? -3. 这个模拟揭示了哪些值得关注的未来趋势? +Please examine this future preview 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 did this simulation reveal? -根据预测结果,设计最合适的报告章节结构。 +Based on the prediction results, design the most suitable report chapter structure. -【再次提醒】报告章节数量:最少2个,最多5个,内容要精炼聚焦于核心预测发现。""" +[Reminder] Report chapter quantity: Minimum 2, maximum 5, content should be concise and focused on core prediction findings.""" -# ── 章节生成 prompt ── +# ── Prompt tạo chương ── SECTION_SYSTEM_PROMPT_TEMPLATE = """\ -你是一个「未来预测报告」的撰写专家,正在撰写报告的一个章节。 +You are a writing expert for "Future Prediction Reports", currently writing one section of the report. -报告标题: {report_title} -报告摘要: {report_summary} -预测场景(模拟需求): {simulation_requirement} +Report Title: {report_title} +Report Summary: {report_summary} +Prediction Scenario (Simulation Requirement): {simulation_requirement} -当前要撰写的章节: {section_title} +Section currently being written: {section_title} ═══════════════════════════════════════════════════════════════ -【核心理念】 +[Core Concept] ═══════════════════════════════════════════════════════════════ -模拟世界是对未来的预演。我们向模拟世界注入了特定条件(模拟需求), -模拟中Agent的行为和互动,就是对未来人群行为的预测。 +The simulated world is a preview of the future. We injected specific conditions (simulation requirements) into the simulated world. +The behaviors and interactions of Agents in the simulation are predictions of future human behavior. -你的任务是: -- 揭示在设定条件下,未来发生了什么 -- 预测各类人群(Agent)是如何反应和行动的 -- 发现值得关注的未来趋势、风险和机会 +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 the real world's current status +✅ Focus on "what the future will be" - the simulation results are the predicted future ═══════════════════════════════════════════════════════════════ -【最重要的规则 - 必须遵守】 +[Most Important Rules - MUST Obey] ═══════════════════════════════════════════════════════════════ -1. 【必须调用工具观察模拟世界】 - - 你正在以「上帝视角」观察未来的预演 - - 所有内容必须来自模拟世界中发生的事件和Agent言行 - - 禁止使用你自己的知识来编写报告内容 - - 每个章节至少调用3次工具(最多5次)来观察模拟的世界,它代表了未来 - -2. 【必须引用Agent的原始言行】 - - Agent的发言和行为是对未来人群行为的预测 - - 在报告中使用引用格式展示这些预测,例如: - > "某类人群会表示:原文内容..." - - 这些引用是模拟预测的核心证据 - -3. 【语言一致性 - 引用内容必须翻译为报告语言】 - - 工具返回的内容可能包含英文或中英文混杂的表述 - - 如果模拟需求和材料原文是中文的,报告必须全部使用中文撰写 - - 当你引用工具返回的英文或中英混杂内容时,必须将其翻译为流畅的中文后再写入报告 - - 翻译时保持原意不变,确保表述自然通顺 - - 这一规则同时适用于正文和引用块(> 格式)中的内容 - -4. 【忠实呈现预测结果】 - - 报告内容必须反映模拟世界中的代表未来的模拟结果 - - 不要添加模拟中不存在的信息 - - 如果某方面信息不足,如实说明 +1. [MUST use tools to observe the simulated world] + - You are observing the future preview from a "God's eye view" + - All content MUST come from events, words, and actions of Agents occurred in the simulated world + - It is strictly forbidden to use your own knowledge to write report content + - For each chapter, you MUST call tools at least 3 times (maximum 5 times) to observe the simulated world, which represents the future + +2. [MUST quote the exact original words and actions of Agents] + - The Agent's statements and behaviors are predictions of future human behavior + - Use quote formatting in the report to display these predictions, for example: + > "A certain group of people will say: Original content..." + - These quotes are the core evidence of the simulation prediction + +3. [Language Consistency - Quoted Content Must Be Translated to Report Language] + - The content returned by the tools may contain English or mixed Chinese and English expressions + - If the simulation requirements and original materials are in Chinese, the report must be written entirely in Chinese + - When you quote English or mixed content returned by the tool, you must translate it into fluent Chinese before writing it into the report + - Keep the original meaning unchanged when translating, and ensure the expression is natural and fluent + - This rule applies to both the main text and the content in the quote block (> format) + +4. [Faithful Presentation of Prediction Results] + - Report content must reflect the simulation results representing the future in the simulated world + - Do not add information that does not exist in the simulation + - If information in a certain aspect is insufficient, state it truthfully ═══════════════════════════════════════════════════════════════ -【⚠️ 格式规范 - 极其重要!】 +[⚠️ Formatting Specifications - Extremely Important!] ═══════════════════════════════════════════════════════════════ -【一个章节 = 最小内容单位】 -- 每个章节是报告的最小分块单位 -- ❌ 禁止在章节内使用任何 Markdown 标题(#、##、###、#### 等) -- ❌ 禁止在内容开头添加章节主标题 -- ✅ 章节标题由系统自动添加,你只需撰写纯正文内容 -- ✅ 使用**粗体**、段落分隔、引用、列表来组织内容,但不要用标题 +[One Chapter = Minimum Content Unit] +- Each chapter is the minimum blocking unit of the report +- ❌ Do not use any Markdown headings (#, ##, ###, ####, etc.) within the chapter +- ❌ Do not add a main chapter heading at the beginning of the content +- ✅ Chapter titles are added automatically by the system, you only need to write the plain text content +- ✅ Use **bold text**, paragraph breaks, quotes, and lists to organize content, but do not use headings -【正确示例】 +[Correct Example] ``` -本章节分析了事件的舆论传播态势。通过对模拟数据的深入分析,我们发现... +This chapter analyzes the public opinion dissemination trend of the event. Through deep analysis of simulation data, we found... -**首发引爆阶段** +**Initial Outbreak Stage** -微博作为舆情的第一现场,承担了信息首发的核心功能: +Weibo, as the first scene of public opinion, assumed the core function of initial information release: -> "微博贡献了68%的首发声量..." +> "Weibo contributed 68% of the initial buzz..." -**情绪放大阶段** +**Emotion Amplification Stage** -抖音平台进一步放大了事件影响力: +The Douyin platform further amplified the event's impact: -- 视觉冲击力强 -- 情绪共鸣度高 +- Strong visual impact +- High emotional resonance ``` -【错误示例】 +[Incorrect Example] ``` -## 执行摘要 ← 错误!不要添加任何标题 -### 一、首发阶段 ← 错误!不要用###分小节 -#### 1.1 详细分析 ← 错误!不要用####细分 +## Executive Summary ← Error! Do not add any headings +### 1. Initial Stage ← Error! Do not use ### for sub-sections +#### 1.1 Detailed Analysis ← Error! Do not use #### for further division -本章节分析了... +This chapter analyzes... ``` ═══════════════════════════════════════════════════════════════ -【可用检索工具】(每章节调用3-5次) +[Available Retrieval Tools] (Call 3-5 times per section) ═══════════════════════════════════════════════════════════════ {tools_description} -【工具使用建议 - 请混合使用不同工具,不要只用一种】 -- insight_forge: 深度洞察分析,自动分解问题并多维度检索事实和关系 -- panorama_search: 广角全景搜索,了解事件全貌、时间线和演变过程 -- quick_search: 快速验证某个具体信息点 -- interview_agents: 采访模拟Agent,获取不同角色的第一人称观点和真实反应 +[Tool Usage Suggestions - Please mix different tools, do not 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, understands the whole picture, timeline, and evolution process of an event +- quick_search: Quickly verifies a specific information point +- interview_agents: Interviews simulation Agents to get first-person views and real reactions from different roles ═══════════════════════════════════════════════════════════════ -【工作流程】 +[Workflow] ═══════════════════════════════════════════════════════════════ -每次回复你只能做以下两件事之一(不可同时做): +For each reply you can only do one of the following two things (not both simultaneously): -选项A - 调用工具: -输出你的思考,然后用以下格式调用一个工具: +Option A - Call a tool: +Output your thoughts, then use the following format to call a tool: -{{"name": "工具名称", "parameters": {{"参数名": "参数值"}}}} +{{"name": "Tool Name", "parameters": {{"Parameter Name": "Parameter Value"}}}} -系统会执行工具并把结果返回给你。你不需要也不能自己编写工具返回结果。 +The system will execute the tool and return the result to you. You do not need to and cannot write the tool return result yourself. -选项B - 输出最终内容: -当你已通过工具获取了足够信息,以 "Final Answer:" 开头输出章节内容。 +Option B - Output Final Content: +When you have obtained enough information through tools, output the chapter content starting with "Final Answer:". -⚠️ 严格禁止: -- 禁止在一次回复中同时包含工具调用和 Final Answer -- 禁止自己编造工具返回结果(Observation),所有工具结果由系统注入 -- 每次回复最多调用一个工具 +⚠️ Strictly Forbidden: +- Forbidden to include both tool calls and Final Answer in a single reply +- Forbidden to fabricate tool return results (Observation) yourself, all tool results are injected by the system +- Call a maximum of one tool per reply ═══════════════════════════════════════════════════════════════ -【章节内容要求】 +[Chapter Content Requirements] ═══════════════════════════════════════════════════════════════ -1. 内容必须基于工具检索到的模拟数据 -2. 大量引用原文来展示模拟效果 -3. 使用Markdown格式(但禁止使用标题): - - 使用 **粗体文字** 标记重点(代替子标题) - - 使用列表(-或1.2.3.)组织要点 - - 使用空行分隔不同段落 - - ❌ 禁止使用 #、##、###、#### 等任何标题语法 -4. 【引用格式规范 - 必须单独成段】 - 引用必须独立成段,前后各有一个空行,不能混在段落中: - - ✅ 正确格式: +1. Content must be based on simulation data retrieved by tools +2. Quote the original text extensively to demonstrate the simulation effect +3. Use Markdown format (but forbid using headings): + - Use **bold text** to mark key points (instead of subheadings) + - Use lists (- or 1. 2. 3.) to organize points + - Use blank lines to separate different paragraphs + - ❌ Forbidden to use #, ##, ###, #### and any other heading syntax +4. [Quote Formatting Specifications - Must be a separate paragraph] + Quotes must be an independent paragraph, with a blank line before and after, cannot be mixed in the paragraph: + + ✅ Correct format: ``` - 校方的回应被认为缺乏实质内容。 + The school's response was considered to lack substantive content. - > "校方的应对模式在瞬息万变的社交媒体环境中显得僵化和迟缓。" + > "The school's response model appears rigid and slow in the rapidly changing social media environment." - 这一评价反映了公众的普遍不满。 + This evaluation reflects the general dissatisfaction of the public. ``` - ❌ 错误格式: + ❌ Incorrect format: ``` - 校方的回应被认为缺乏实质内容。> "校方的应对模式..." 这一评价反映了... + The school's response was considered to lack substantive content. > "The school's response model..." This evaluation reflects... ``` -5. 保持与其他章节的逻辑连贯性 -6. 【避免重复】仔细阅读下方已完成的章节内容,不要重复描述相同的信息 -7. 【再次强调】不要添加任何标题!用**粗体**代替小节标题""" +5. Maintain logical coherence with other chapters +6. [Avoid Repetition] Carefully read the completed chapter content below, do not repeat the same information +7. [Emphasize Again] Do not add any headings! Use **bold** instead of section headings""" SECTION_USER_PROMPT_TEMPLATE = """\ -已完成的章节内容(请仔细阅读,避免重复): +Completed Chapter Content (Please read carefully to avoid duplication): {previous_content} ═══════════════════════════════════════════════════════════════ -【当前任务】撰写章节: {section_title} +[Current Task] Writing Chapter: {section_title} ═══════════════════════════════════════════════════════════════ -【重要提醒】 -1. 仔细阅读上方已完成的章节,避免重复相同的内容! -2. 开始前必须先调用工具获取模拟数据 -3. 请混合使用不同工具,不要只用一种 -4. 报告内容必须来自检索结果,不要使用自己的知识 +[Important Reminders] +1. Read the completed chapters above carefully to avoid repeating the same content! +2. Must call tools first to get simulation data before starting +3. Please mix different tools, do not use only one +4. Report content must come from retrieval results, do not use your own knowledge -【⚠️ 格式警告 - 必须遵守】 -- ❌ 不要写任何标题(#、##、###、####都不行) -- ❌ 不要写"{section_title}"作为开头 -- ✅ 章节标题由系统自动添加 -- ✅ 直接写正文,用**粗体**代替小节标题 +[⚠️ Formatting Warning - Must be Obeyed] +- ❌ Do not write any headings (no #, ##, ###, ####) +- ❌ Do not write "{section_title}" as the beginning +- ✅ Chapter titles are automatically added by the system +- ✅ Write the main text directly, use **bold** instead of section headings -请开始: -1. 首先思考(Thought)这个章节需要什么信息 -2. 然后调用工具(Action)获取模拟数据 -3. 收集足够信息后输出 Final Answer(纯正文,无任何标题)""" +Please begin: +1. First, think (Thought) what information this chapter needs +2. Then, call tools (Action) to get simulation data +3. After collecting enough information, output Final Answer (plain text, no headings)""" -# ── ReACT 循环内消息模板 ── +# ── ReACT Message Templates ── REACT_OBSERVATION_TEMPLATE = """\ -Observation(检索结果): +Observation (Retrieval Result): -═══ 工具 {tool_name} 返回 ═══ +═══ Tool {tool_name} Returned ═══ {result} ═══════════════════════════════════════════════════════════════ -已调用工具 {tool_calls_count}/{max_tool_calls} 次(已用: {used_tools_str}){unused_hint} -- 如果信息充分:以 "Final Answer:" 开头输出章节内容(必须引用上述原文) -- 如果需要更多信息:调用一个工具继续检索 +Tool 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 retrieving ═══════════════════════════════════════════════════════════════""" REACT_INSUFFICIENT_TOOLS_MSG = ( - "【注意】你只调用了{tool_calls_count}次工具,至少需要{min_tool_calls}次。" - "请再调用工具获取更多模拟数据,然后再输出 Final Answer。{unused_hint}" + "[Notice] You only called the tool {tool_calls_count} times, at least {min_tool_calls} times are needed. " + "Please call the tool again to fetch more simulation data, and then output Final Answer. {unused_hint}" ) REACT_INSUFFICIENT_TOOLS_MSG_ALT = ( - "当前只调用了 {tool_calls_count} 次工具,至少需要 {min_tool_calls} 次。" - "请调用工具获取模拟数据。{unused_hint}" + "Currently tool called {tool_calls_count} times, at least {min_tool_calls} times are needed. " + "Please call tools to fetch simulation data. {unused_hint}" ) REACT_TOOL_LIMIT_MSG = ( - "工具调用次数已达上限({tool_calls_count}/{max_tool_calls}),不能再调用工具。" - '请立即基于已获取的信息,以 "Final Answer:" 开头输出章节内容。' + "Tool call limit reached ({tool_calls_count}/{max_tool_calls}), cannot call tools anymore. " + 'Please output your section content starting with "Final Answer:" immediately based on retrieved information.' ) -REACT_UNUSED_TOOLS_HINT = "\n💡 你还没有使用过: {unused_list},建议尝试不同工具获取多角度信息" +REACT_UNUSED_TOOLS_HINT = "\n💡 You haven't used: {unused_list}, suggesting trying different tools for multiple perspectives" -REACT_FORCE_FINAL_MSG = "已达到工具调用限制,请直接输出 Final Answer: 并生成章节内容。" +REACT_FORCE_FINAL_MSG = "Tool call limit reached, please output Final Answer: and generate section content directly." # ── Chat prompt ── CHAT_SYSTEM_PROMPT_TEMPLATE = """\ -你是一个简洁高效的模拟预测助手。 +You are a concise and efficient simulation prediction assistant. -【背景】 -预测条件: {simulation_requirement} +[Background] +Prediction condition: {simulation_requirement} -【已生成的分析报告】 +[Generated Analysis Report] {report_content} -【规则】 -1. 优先基于上述报告内容回答问题 -2. 直接回答问题,避免冗长的思考论述 -3. 仅在报告内容不足以回答时,才调用工具检索更多数据 -4. 回答要简洁、清晰、有条理 +[Rules] +1. Prioritize answering based on the report content above +2. Answer the question directly, avoid lengthy reasoning +3. Only call tools to retrieve more data if the report content is insufficient to answer +4. Answers must be concise, clear, and organized -【可用工具】(仅在需要时使用,最多调用1-2次) +[Available Tools] (Use only when necessary, call 1-2 times max) {tools_description} -【工具调用格式】 +[Tool Call Format] -{{"name": "工具名称", "parameters": {{"参数名": "参数值"}}}} +{{"name": "Tool Name", "parameters": {{"Parameter Name": "Parameter Value"}}}} -【回答风格】 -- 简洁直接,不要长篇大论 -- 使用 > 格式引用关键内容 -- 优先给出结论,再解释原因""" +[Answering Style] +- Concise and direct, avoid long paragraphs +- Use > format to quote key content +- Provide conclusion first, then explain the reason""" -CHAT_OBSERVATION_SUFFIX = "\n\n请简洁回答问题。" +CHAT_OBSERVATION_SUFFIX = "\n\nPlease answer the question concisely." # ═══════════════════════════════════════════════════════════════ -# ReportAgent 主类 +# Class chính: ReportAgent # ═══════════════════════════════════════════════════════════════ class ReportAgent: """ - Report Agent - 模拟报告生成Agent + Report Agent - Tác nhân tạo báo cáo mô phỏng - 采用ReACT(Reasoning + Acting)模式: - 1. 规划阶段:分析模拟需求,规划报告目录结构 - 2. 生成阶段:逐章节生成内容,每章节可多次调用工具获取信息 - 3. 反思阶段:检查内容完整性和准确性 + Sử dụng chế độ ReACT (Reasoning + Acting): + 1. Giai đoạn lập kế hoạch (Planning): Phân tích yêu cầu mô phỏng, hoạch định cấu trúc thư mục báo cáo + 2. Giai đoạn tạo văn bản (Generating): Tạo nội dung theo từng cấu trúc, mỗi cấu trúc có thể gọi công cụ nhiều lần để lấy thông tin + 3. Giai đoạn xem xét (Reflecting): Kiểm tra tính toàn vẹn và độ chính xác của nội dung """ - # 最大工具调用次数(每个章节) + # Số lần gọi công cụ tối đa (mỗi chương) MAX_TOOL_CALLS_PER_SECTION = 5 - # 最大反思轮数 + # Số vòng xem xét tối đa MAX_REFLECTION_ROUNDS = 3 - # 对话中的最大工具调用次数 + # Số lần gọi công cụ tối đa trong quá trình trò chuyện MAX_TOOL_CALLS_PER_CHAT = 2 def __init__( @@ -889,14 +889,14 @@ def __init__( zep_tools: Optional[ZepToolsService] = None ): """ - 初始化Report Agent + Khởi tạo Report Agent Args: - graph_id: 图谱ID - simulation_id: 模拟ID - simulation_requirement: 模拟需求描述 - llm_client: LLM客户端(可选) - zep_tools: Zep工具服务(可选) + graph_id: ID Đồ thị + simulation_id: ID mô phỏng + simulation_requirement: Mô tả yêu cầu mô phỏng + llm_client: Client của LLM (không bắt buộc) + zep_tools: Dịch vụ công cụ Zep (không bắt buộc) """ self.graph_id = graph_id self.simulation_id = simulation_id @@ -905,66 +905,66 @@ def __init__( self.llm = llm_client or LLMClient() self.zep_tools = zep_tools or ZepToolsService() - # 工具定义 + # Định nghĩa các công cụ self.tools = self._define_tools() - # 日志记录器(在 generate_report 中初始化) + # Trình ghi log báo cáo (được khởi tạo trong generate_report) self.report_logger: Optional[ReportLogger] = None - # 控制台日志记录器(在 generate_report 中初始化) + # Trình ghi log console (được khởi tạo trong generate_report) self.console_logger: Optional[ReportConsoleLogger] = None - logger.info(f"ReportAgent 初始化完成: graph_id={graph_id}, simulation_id={simulation_id}") + logger.info(f"ReportAgent initialized: graph_id={graph_id}, simulation_id={simulation_id}") def _define_tools(self) -> Dict[str, Dict[str, Any]]: - """定义可用工具""" + """Định nghĩa các công cụ khả dụng""" return { "insight_forge": { "name": "insight_forge", "description": TOOL_DESC_INSIGHT_FORGE, "parameters": { - "query": "你想深入分析的问题或话题", - "report_context": "当前报告章节的上下文(可选,有助于生成更精准的子问题)" + "query": "The question or topic you want to deeply analyze", + "report_context": "The context of the current report section (optional, helps generate more precise sub-questions)" } }, "panorama_search": { "name": "panorama_search", "description": TOOL_DESC_PANORAMA_SEARCH, "parameters": { - "query": "搜索查询,用于相关性排序", - "include_expired": "是否包含过期/历史内容(默认True)" + "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, "parameters": { - "query": "搜索查询字符串", - "limit": "返回结果数量(可选,默认10)" + "query": "Search query string", + "limit": "Number of returned results (optional, default 10)" } }, "interview_agents": { "name": "interview_agents", "description": TOOL_DESC_INTERVIEW_AGENTS, "parameters": { - "interview_topic": "采访主题或需求描述(如:'了解学生对宿舍甲醛事件的看法')", - "max_agents": "最多采访的Agent数量(可选,默认5,最大10)" + "interview_topic": "Interview topic or requirement description (e.g.: 'Understand students opinions on dorm formaldehyde issue')", + "max_agents": "Maximum number of agents to interview (optional, default 5, maximum 10)" } } } def _execute_tool(self, tool_name: str, parameters: Dict[str, Any], report_context: str = "") -> str: """ - 执行工具调用 + Thực thi lệnh gọi công cụ Args: - tool_name: 工具名称 - parameters: 工具参数 - report_context: 报告上下文(用于InsightForge) + tool_name: Tên công cụ + parameters: Các tham số cho công cụ + report_context: Ngữ cảnh của đoạn báo cáo (dành cho InsightForge) Returns: - 工具执行结果(文本格式) + Kết quả của công cụ trả về (định dạng text) """ - logger.info(f"执行工具: {tool_name}, 参数: {parameters}") + logger.info(f"Executing tool: {tool_name}, Parameters: {parameters}") try: if tool_name == "insight_forge": @@ -979,7 +979,7 @@ def _execute_tool(self, tool_name: str, parameters: Dict[str, Any], report_conte return result.to_text() elif tool_name == "panorama_search": - # 广度搜索 - 获取全貌 + # Tìm kiếm diện rộng - Lấy toàn cảnh kết quả query = parameters.get("query", "") include_expired = parameters.get("include_expired", True) if isinstance(include_expired, str): @@ -992,7 +992,7 @@ def _execute_tool(self, tool_name: str, parameters: Dict[str, Any], report_conte return result.to_text() elif tool_name == "quick_search": - # 简单搜索 - 快速检索 + # Tìm kiếm đơn giản - Lấy dữ liệu nhanh query = parameters.get("query", "") limit = parameters.get("limit", 10) if isinstance(limit, str): @@ -1005,7 +1005,7 @@ def _execute_tool(self, tool_name: str, parameters: Dict[str, Any], report_conte return result.to_text() elif tool_name == "interview_agents": - # 深度采访 - 调用真实的OASIS采访API获取模拟Agent的回答(双平台) + # Phỏng vấn chuyên sâu - Gọi API phỏng vấn OASIS thật để lấy ý kiến của các Agent đang chạy (đa nền tảng) interview_topic = parameters.get("interview_topic", parameters.get("query", "")) max_agents = parameters.get("max_agents", 5) if isinstance(max_agents, str): @@ -1019,11 +1019,11 @@ def _execute_tool(self, tool_name: str, parameters: Dict[str, Any], report_conte ) return result.to_text() - # ========== 向后兼容的旧工具(内部重定向到新工具) ========== + # ========== Công cụ cũ giữ lại để tương thích (chuyển hướng sang công cụ mới) ========== elif tool_name == "search_graph": - # 重定向到 quick_search - logger.info("search_graph 已重定向到 quick_search") + # Lái qua quick_search + logger.info("search_graph redirected to quick_search") return self._execute_tool("quick_search", parameters, report_context) elif tool_name == "get_graph_statistics": @@ -1039,8 +1039,8 @@ def _execute_tool(self, tool_name: str, parameters: Dict[str, Any], report_conte return json.dumps(result, ensure_ascii=False, indent=2) elif tool_name == "get_simulation_context": - # 重定向到 insight_forge,因为它更强大 - logger.info("get_simulation_context 已重定向到 insight_forge") + # Lái qua insight_forge do nó xịn hơn + logger.info("get_simulation_context redirected to insight_forge") query = parameters.get("query", self.simulation_requirement) return self._execute_tool("insight_forge", {"query": query}, report_context) @@ -1054,26 +1054,26 @@ def _execute_tool(self, tool_name: str, parameters: Dict[str, Any], report_conte return json.dumps(result, ensure_ascii=False, indent=2) else: - return f"未知工具: {tool_name}。请使用以下工具之一: insight_forge, panorama_search, quick_search" + return f"Unknown tool: {tool_name}. Please use one of: insight_forge, panorama_search, quick_search" except Exception as e: - logger.error(f"工具执行失败: {tool_name}, 错误: {str(e)}") - return f"工具执行失败: {str(e)}" + logger.error(f"Tool execution failed: {tool_name}, Error: {str(e)}") + return f"Tool execution failed: {str(e)}" - # 合法的工具名称集合,用于裸 JSON 兜底解析时校验 + # Tập hợp các tool khả dụng, để kiểm tra tính hợp lệ khi quét JSON VALID_TOOL_NAMES = {"insight_forge", "panorama_search", "quick_search", "interview_agents"} def _parse_tool_calls(self, response: str) -> List[Dict[str, Any]]: """ - 从LLM响应中解析工具调用 + Phân tích kết quả trả về từ LLM để lấy thông tin gọi công cụ - 支持的格式(按优先级): + Định dạng hỗ trợ (theo mức độ ưu tiên): 1. {"name": "tool_name", "parameters": {...}} - 2. 裸 JSON(响应整体或单行就是一个工具调用 JSON) + 2. JSON trần (Toàn bộ chuỗi response hoặc từng dòng là một JSON trực tiếp cho công cụ) """ tool_calls = [] - # 格式1: XML风格(标准格式) + # Định dạng 1: Phong cách XML (Định dạng tiêu chuẩn) xml_pattern = r'\s*(\{.*?\})\s*' for match in re.finditer(xml_pattern, response, re.DOTALL): try: @@ -1085,8 +1085,8 @@ def _parse_tool_calls(self, response: str) -> List[Dict[str, Any]]: if tool_calls: return tool_calls - # 格式2: 兜底 - LLM 直接输出裸 JSON(没包 标签) - # 只在格式1未匹配时尝试,避免误匹配正文中的 JSON + # Định dạng 2: Dự phòng phòng hờ LLM trả về chuỗi JSON trần không có tag + # Chỉ chạy logic này khi không thấy định dạng 1, tránh dính JSON vô ý trong văn bản chính stripped = response.strip() if stripped.startswith('{') and stripped.endswith('}'): try: @@ -1097,7 +1097,7 @@ def _parse_tool_calls(self, response: str) -> List[Dict[str, Any]]: except json.JSONDecodeError: pass - # 响应可能包含思考文字 + 裸 JSON,尝试提取最后一个 JSON 对象 + # Reply có thể chứa cả nội dung suy nghĩ (Thought) + JSON trần, thử móc ra object JSON cuối cùng json_pattern = r'(\{"(?:name|tool)"\s*:.*?\})\s*$' match = re.search(json_pattern, stripped, re.DOTALL) if match: @@ -1111,11 +1111,11 @@ def _parse_tool_calls(self, response: str) -> List[Dict[str, Any]]: return tool_calls def _is_valid_tool_call(self, data: dict) -> bool: - """校验解析出的 JSON 是否是合法的工具调用""" - # 支持 {"name": ..., "parameters": ...} 和 {"tool": ..., "params": ...} 两种键名 + """Kiểm tra JSON giải mã được có phải là lời gọi công cụ hợp lệ hay không""" + # Hỗ trợ cả 2 định dạng {"name": ..., "parameters": ...} và {"tool": ..., "params": ...} tool_name = data.get("name") or data.get("tool") if tool_name and tool_name in self.VALID_TOOL_NAMES: - # 统一键名为 name / parameters + # Đồng nhất key về chuẩn name / parameters if "tool" in data: data["name"] = data.pop("tool") if "params" in data and "parameters" not in data: @@ -1124,13 +1124,13 @@ def _is_valid_tool_call(self, data: dict) -> bool: return False def _get_tools_description(self) -> str: - """生成工具描述文本""" - desc_parts = ["可用工具:"] + """Tạo đoạn văn mô tả công cụ""" + desc_parts = ["Available tools:"] 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}") + desc_parts.append(f" Parameters: {params_desc}") return "\n".join(desc_parts) def plan_outline( @@ -1138,29 +1138,29 @@ def plan_outline( progress_callback: Optional[Callable] = None ) -> ReportOutline: """ - 规划报告大纲 + Hoạch định dàn ý báo cáo - 使用LLM分析模拟需求,规划报告的目录结构 + Sử dụng LLM để phân tích yêu cầu mô phỏng, hoạch định cấu trúc thư mục của báo cáo Args: - progress_callback: 进度回调函数 + progress_callback: Hàm callback tiến trình Returns: - ReportOutline: 报告大纲 + ReportOutline: Dàn ý báo cáo """ - logger.info("开始规划报告大纲...") + logger.info("Start planning report outline...") if progress_callback: - progress_callback("planning", 0, "正在分析模拟需求...") + progress_callback("planning", 0, "Analyzing simulation requirements...") - # 首先获取模拟上下文 + # Đầu tiên cần lấy ngữ cảnh mô phỏng context = self.zep_tools.get_simulation_context( graph_id=self.graph_id, simulation_requirement=self.simulation_requirement ) if progress_callback: - progress_callback("planning", 30, "正在生成报告大纲...") + progress_callback("planning", 30, "Generating report outline...") system_prompt = PLAN_SYSTEM_PROMPT user_prompt = PLAN_USER_PROMPT_TEMPLATE.format( @@ -1182,9 +1182,9 @@ def plan_outline( ) if progress_callback: - progress_callback("planning", 80, "正在解析大纲结构...") + progress_callback("planning", 80, "Parsing outline structure...") - # 解析大纲 + # Phân tích cú pháp lấy dàn ý sections = [] for section_data in response.get("sections", []): sections.append(ReportSection( @@ -1193,27 +1193,27 @@ def plan_outline( )) outline = ReportOutline( - title=response.get("title", "模拟分析报告"), + title=response.get("title", "Simulation Analysis Report"), summary=response.get("summary", ""), sections=sections ) if progress_callback: - progress_callback("planning", 100, "大纲规划完成") + progress_callback("planning", 100, "Outline planning completed") - logger.info(f"大纲规划完成: {len(sections)} 个章节") + logger.info(f"Outline planning completed: {len(sections)} chapters") return outline except Exception as e: - logger.error(f"大纲规划失败: {str(e)}") - # 返回默认大纲(3个章节,作为fallback) + logger.error(f"Outline planning failed: {str(e)}") + # Trả về dàn ý mặc định (3 chương, đóng vai trò bản dự phòng) return ReportOutline( - title="未来预测报告", - summary="基于模拟预测的未来趋势与风险分析", + title="Future Prediction Report", + summary="Future trends and risk analysis based on simulation prediction", sections=[ - ReportSection(title="预测场景与核心发现"), - ReportSection(title="人群行为预测分析"), - ReportSection(title="趋势展望与风险提示") + ReportSection(title="Prediction Scenarios and Core Findings"), + ReportSection(title="Population Behavior Prediction Analysis"), + ReportSection(title="Trend Outlook and Risk Warning") ] ) @@ -1226,28 +1226,28 @@ def _generate_section_react( section_index: int = 0 ) -> str: """ - 使用ReACT模式生成单个章节内容 + Sử dụng chế độ ReACT để tạo nội dung cho từng chương - ReACT循环: - 1. Thought(思考)- 分析需要什么信息 - 2. Action(行动)- 调用工具获取信息 - 3. Observation(观察)- 分析工具返回结果 - 4. 重复直到信息足够或达到最大次数 - 5. Final Answer(最终回答)- 生成章节内容 + Vòng lặp ReACT: + 1. Thought (Suy nghĩ) - Phân tích xem cần thông tin gì + 2. Action (Hành động) - Gọi công cụ để lấy thông tin + 3. Observation (Quan sát) - Phân tích kết quả công cụ trả về + 4. Lặp lại quá trình tới khi đủ thông tin hoặc chạy hết số lượt cho phép + 5. Final Answer (Câu trả lời cuối cùng) - Sinh nội dung chương Args: - section: 要生成的章节 - outline: 完整大纲 - previous_sections: 之前章节的内容(用于保持连贯性) - progress_callback: 进度回调 - section_index: 章节索引(用于日志记录) + section: Chương cần sinh + outline: Dàn ý đầy đủ + previous_sections: Nội dung các chương trước (dành cho việc duy trì tính gắn kết logic) + progress_callback: Callback theo dõi tiến trình + section_index: Chỉ mục của chương hiện tại (dùng để log) Returns: - 章节内容(Markdown格式) + Nội dung chương (Định dạng Markdown) """ - logger.info(f"ReACT生成章节: {section.title}") + logger.info(f"ReACT generating chapter: {section.title}") - # 记录章节开始日志 + # Ghi nhận log khởi tạo chương if self.report_logger: self.report_logger.log_section_start(section.title, section_index) @@ -1259,16 +1259,16 @@ def _generate_section_react( tools_description=self._get_tools_description(), ) - # 构建用户prompt - 每个已完成章节各传入最大4000字 + # Xây dựng prompt cho user - mỗi chương trước đó sẽ truyền vào tối đa 4000 ký tự if previous_sections: previous_parts = [] for sec in previous_sections: - # 每个章节最多4000字 + # Mỗi chương tối đa 4000 ký tự truncated = sec[:4000] + "..." if len(sec) > 4000 else sec previous_parts.append(truncated) previous_content = "\n\n---\n\n".join(previous_parts) else: - previous_content = "(这是第一个章节)" + previous_content = "(This is the first chapter)" user_prompt = SECTION_USER_PROMPT_TEMPLATE.format( previous_content=previous_content, @@ -1280,77 +1280,77 @@ def _generate_section_react( {"role": "user", "content": user_prompt} ] - # ReACT循环 + # Vòng lặp ReACT tool_calls_count = 0 - max_iterations = 5 # 最大迭代轮数 - min_tool_calls = 3 # 最少工具调用次数 - conflict_retries = 0 # 工具调用与Final Answer同时出现的连续冲突次数 - used_tools = set() # 记录已调用过的工具名 + max_iterations = 5 # Số vòng lặp tối đa + min_tool_calls = 3 # Số lần gọi công cụ tối thiểu + conflict_retries = 0 # Số lần gọi công cụ và trả về Final Answer bị xung đột liên tiếp + used_tools = set() # Lưu lại tên các công cụ đã được gọi all_tools = {"insight_forge", "panorama_search", "quick_search", "interview_agents"} - # 报告上下文,用于InsightForge的子问题生成 - report_context = f"章节标题: {section.title}\n模拟需求: {self.simulation_requirement}" + # Ngữ cảnh báo cáo, dùng để tự sinh câu hỏi thứ cấp trong InsightForge + report_context = f"Chapter Title: {section.title}\nSimulation Requirement: {self.simulation_requirement}" for iteration in range(max_iterations): if progress_callback: progress_callback( "generating", int((iteration / max_iterations) * 100), - f"深度检索与撰写中 ({tool_calls_count}/{self.MAX_TOOL_CALLS_PER_SECTION})" + f"Deep retrieval and writing in progress ({tool_calls_count}/{self.MAX_TOOL_CALLS_PER_SECTION})" ) - # 调用LLM + # Gọi LLM response = self.llm.chat( messages=messages, temperature=0.5, max_tokens=4096 ) - # 检查 LLM 返回是否为 None(API 异常或内容为空) + # Kiểm tra xem phản hồi có rỗng/chưa có (None) không (do API lỗi hoặc content null) if response is None: - logger.warning(f"章节 {section.title} 第 {iteration + 1} 次迭代: LLM 返回 None") - # 如果还有迭代次数,添加消息并重试 + logger.warning(f"Chapter {section.title} iteration {iteration + 1}: LLM returned None") + # Nếu còn lượt thử, thêm message tiếp if iteration < max_iterations - 1: - messages.append({"role": "assistant", "content": "(响应为空)"}) - messages.append({"role": "user", "content": "请继续生成内容。"}) + messages.append({"role": "assistant", "content": "(Empty response)"}) + messages.append({"role": "user", "content": "Please continue generating content."}) continue - # 最后一次迭代也返回 None,跳出循环进入强制收尾 + # Còn nếu nó về None lần cuối thì nhảy thoát vòng lặp kết thúc break - logger.debug(f"LLM响应: {response[:200]}...") + logger.debug(f"LLM response: {response[:200]}...") - # 解析一次,复用结果 + # Parse dữ liệu một lần để tiết kiệm tool_calls = self._parse_tool_calls(response) has_tool_calls = bool(tool_calls) has_final_answer = "Final Answer:" in response - # ── 冲突处理:LLM 同时输出了工具调用和 Final Answer ── + # ── Giải quyết xung đột: LLM cùng lúc nhả cả tool gọi lẫn output Final Answer ── if has_tool_calls and has_final_answer: conflict_retries += 1 logger.warning( - f"章节 {section.title} 第 {iteration+1} 轮: " - f"LLM 同时输出工具调用和 Final Answer(第 {conflict_retries} 次冲突)" + f"Chapter {section.title} iteration {iteration+1}: " + f"LLM output tool call and Final Answer simultaneously (Conflict #{conflict_retries})" ) if conflict_retries <= 2: - # 前两次:丢弃本次响应,要求 LLM 重新回复 + # Trong 2 lần đầu: Vứt phản hồi này, bắt LLM tạo lại messages.append({"role": "assistant", "content": response}) messages.append({ "role": "user", "content": ( - "【格式错误】你在一次回复中同时包含了工具调用和 Final Answer,这是不允许的。\n" - "每次回复只能做以下两件事之一:\n" - "- 调用一个工具(输出一个 块,不要写 Final Answer)\n" - "- 输出最终内容(以 'Final Answer:' 开头,不要包含 )\n" - "请重新回复,只做其中一件事。" + "[Format Error] You included both tool call and Final Answer in a single reply, which is not allowed.\n" + "Each reply can only do one of two things:\n" + "- Call a tool (output a block, do not write Final Answer)\n" + "- Output final content (starting with 'Final Answer:', do not include )\n" + "Please reply again, doing only one of these things." ), }) continue else: - # 第三次:降级处理,截断到第一个工具调用,强制执行 + # Lần 3: Hạ cấp xử lý, ngắt cứng đến lệnh gọi công cụ đầu tiên và tiếp tục bám theo logger.warning( - f"章节 {section.title}: 连续 {conflict_retries} 次冲突," - "降级为截断执行第一个工具调用" + f"Chapter {section.title}: {conflict_retries} consecutive conflicts, " + "degrading to truncated execution of the first tool call" ) first_tool_end = response.find('') if first_tool_end != -1: @@ -1360,7 +1360,7 @@ def _generate_section_react( has_final_answer = False conflict_retries = 0 - # 记录 LLM 响应日志 + # Ghi lại log phản hồi của LLM if self.report_logger: self.report_logger.log_llm_response( section_title=section.title, @@ -1371,13 +1371,13 @@ def _generate_section_react( has_final_answer=has_final_answer ) - # ── 情况1:LLM 输出了 Final Answer ── + # ── Trường hợp 1: LLM đã xuất ra Final Answer ── if has_final_answer: - # 工具调用次数不足,拒绝并要求继续调工具 + # Nếu số lần gọi công cụ chưa đủ, từ chối và yêu cầu tiếp tục gọi 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 "" + unused_hint = f"(These tools are unused, suggest using them: {', '.join(unused_tools)})" if unused_tools else "" messages.append({ "role": "user", "content": REACT_INSUFFICIENT_TOOLS_MSG.format( @@ -1388,9 +1388,9 @@ def _generate_section_react( }) continue - # 正常结束 + # Kết thúc bình thường final_answer = response.split("Final Answer:")[-1].strip() - logger.info(f"章节 {section.title} 生成完成(工具调用: {tool_calls_count}次)") + logger.info(f"Chapter {section.title} generation completed (Tool calls: {tool_calls_count})") if self.report_logger: self.report_logger.log_section_content( @@ -1401,9 +1401,9 @@ def _generate_section_react( ) return final_answer - # ── 情况2:LLM 尝试调用工具 ── + # ── Trường hợp 2: LLM cố gắng gọi công cụ ── if has_tool_calls: - # 工具额度已耗尽 → 明确告知,要求输出 Final Answer + # Đã hết hạn mức gọi công cụ → Thông báo rõ ràng, yêu cầu xuất Final Answer if tool_calls_count >= self.MAX_TOOL_CALLS_PER_SECTION: messages.append({"role": "assistant", "content": response}) messages.append({ @@ -1415,10 +1415,10 @@ def _generate_section_react( }) continue - # 只执行第一个工具调用 + # Chỉ thực thi lệnh gọi đầu tiên call = tool_calls[0] if len(tool_calls) > 1: - logger.info(f"LLM 尝试调用 {len(tool_calls)} 个工具,只执行第一个: {call['name']}") + logger.info(f"LLM attempted to call {len(tool_calls)} tools, only executing the first one: {call['name']}") if self.report_logger: self.report_logger.log_tool_call( @@ -1447,7 +1447,7 @@ def _generate_section_react( tool_calls_count += 1 used_tools.add(call['name']) - # 构建未使用工具提示 + # Tạo gợi ý cho các công cụ chưa dùng unused_tools = all_tools - used_tools unused_hint = "" if unused_tools and tool_calls_count < self.MAX_TOOL_CALLS_PER_SECTION: @@ -1467,13 +1467,13 @@ def _generate_section_react( }) continue - # ── 情况3:既没有工具调用,也没有 Final Answer ── + # ── Trường hợp 3: Không có gọi công cụ, cũng không có Final Answer ── messages.append({"role": "assistant", "content": response}) if tool_calls_count < min_tool_calls: - # 工具调用次数不足,推荐未用过的工具 + # Gọi công cụ chưa đủ, gợi ý các công cụ khác unused_tools = all_tools - used_tools - unused_hint = f"(这些工具还未使用,推荐用一下他们: {', '.join(unused_tools)})" if unused_tools else "" + unused_hint = f"(These tools are unused, suggest using them: {', '.join(unused_tools)})" if unused_tools else "" messages.append({ "role": "user", @@ -1485,9 +1485,9 @@ def _generate_section_react( }) continue - # 工具调用已足够,LLM 输出了内容但没带 "Final Answer:" 前缀 - # 直接将这段内容作为最终答案,不再空转 - logger.info(f"章节 {section.title} 未检测到 'Final Answer:' 前缀,直接采纳LLM输出作为最终内容(工具调用: {tool_calls_count}次)") + # Đã đủ số lần gọi công cụ, LLM sinh nội dung nhưng quên mất tiền tố "Final Answer:" + # Cứ lấy thẳng nội dung này làm kết quả luôn cho khỏi vòng vo + logger.info(f"Chapter {section.title} 'Final Answer:' prefix not detected, directly adopting LLM output as final content (Tool calls: {tool_calls_count})") final_answer = response.strip() if self.report_logger: @@ -1499,8 +1499,8 @@ def _generate_section_react( ) return final_answer - # 达到最大迭代次数,强制生成内容 - logger.warning(f"章节 {section.title} 达到最大迭代次数,强制生成") + # Hết số vòng lặp tối đa, bắt buộc bắt đầu tự sản xuất + logger.warning(f"Chapter {section.title} reached max iterations, forcing generation") messages.append({"role": "user", "content": REACT_FORCE_FINAL_MSG}) response = self.llm.chat( @@ -1509,16 +1509,16 @@ def _generate_section_react( max_tokens=4096 ) - # 检查强制收尾时 LLM 返回是否为 None + # Kiểm tra nếu ép buộc kết thúc mà LLM vẫn nhả None if response is None: - logger.error(f"章节 {section.title} 强制收尾时 LLM 返回 None,使用默认错误提示") - final_answer = f"(本章节生成失败:LLM 返回空响应,请稍后重试)" + logger.error(f"Chapter {section.title} LLM returned None during forced completion, using default error prompt") + final_answer = f"(This chapter generation failed: LLM returned empty response, please try again later)" elif "Final Answer:" in response: final_answer = response.split("Final Answer:")[-1].strip() else: final_answer = response - # 记录章节内容生成完成日志 + # Ghi log quá trình tạo nội dung chương hoàn tất if self.report_logger: self.report_logger.log_section_content( section_title=section.title, @@ -1535,29 +1535,29 @@ def generate_report( report_id: Optional[str] = None ) -> Report: """ - 生成完整报告(分章节实时输出) + Tạo báo cáo hoàn chỉnh (Xuất theo thời gian thực từng chương) - 每个章节生成完成后立即保存到文件夹,不需要等待整个报告完成。 - 文件结构: + Mỗi chương sau khi hoàn thành sẽ được lưu ngay vào mục, không cần đợi cả báo cáo xong. + Cấu trúc thư mục: reports/{report_id}/ - meta.json - 报告元信息 - outline.json - 报告大纲 - progress.json - 生成进度 - section_01.md - 第1章节 - section_02.md - 第2章节 + meta.json - Thông tin meta + outline.json - Dàn ý + progress.json - Tiến độ + section_01.md - Chương 1 + section_02.md - Chương 2 ... - full_report.md - 完整报告 + full_report.md - Báo cáo tổng Args: - progress_callback: 进度回调函数 (stage, progress, message) - report_id: 报告ID(可选,如果不传则自动生成) + progress_callback: Callback tiến độ (stage, progress, message) + report_id: ID báo cáo (Có thể rỗng để tự phát sinh) Returns: - Report: 完整报告 + Report: Báo cáo đầy đủ """ import uuid - # 如果没有传入 report_id,则自动生成 + # Tạo report_id mới tự động nếu chưa có truyền vào if not report_id: report_id = f"report_{uuid.uuid4().hex[:12]}" start_time = datetime.now() @@ -1571,14 +1571,14 @@ def generate_report( created_at=datetime.now().isoformat() ) - # 已完成的章节标题列表(用于进度追踪) + # Cập nhật mảng những tiêu đề chương xong (Để tính tiến độ) completed_section_titles = [] try: - # 初始化:创建报告文件夹并保存初始状态 + # Khởi tạo: Tạo thư mục lưu và ghi nhận trạng thái ReportManager._ensure_report_folder(report_id) - # 初始化日志记录器(结构化日志 agent_log.jsonl) + # Khởi tạo bộ ghi log (Log có cấu trúc ghi ra file agent_log.jsonl) self.report_logger = ReportLogger(report_id) self.report_logger.log_start( simulation_id=self.simulation_id, @@ -1586,27 +1586,27 @@ def generate_report( simulation_requirement=self.simulation_requirement ) - # 初始化控制台日志记录器(console_log.txt) + # Khởi tạo logger để lưu thêm console (console_log.txt) self.console_logger = ReportConsoleLogger(report_id) ReportManager.update_progress( - report_id, "pending", 0, "初始化报告...", + report_id, "pending", 0, "Initializing report...", completed_sections=[] ) ReportManager.save_report(report) - # 阶段1: 规划大纲 + # Bước 1: Lên dàn ý report.status = ReportStatus.PLANNING ReportManager.update_progress( - report_id, "planning", 5, "开始规划报告大纲...", + report_id, "planning", 5, "Start planning report outline...", completed_sections=[] ) - # 记录规划开始日志 + # Ghi log bắt đầu giai đoạn lên dàn ý self.report_logger.log_planning_start() if progress_callback: - progress_callback("planning", 0, "开始规划报告大纲...") + progress_callback("planning", 0, "Start planning report outline...") outline = self.plan_outline( progress_callback=lambda stage, prog, msg: @@ -1614,33 +1614,33 @@ def generate_report( ) report.outline = outline - # 记录规划完成日志 + # Ghi log hoàn thành dàn ý self.report_logger.log_planning_complete(outline.to_dict()) - # 保存大纲到文件 + # Lưu file dàn ý ReportManager.save_outline(report_id, outline) ReportManager.update_progress( - report_id, "planning", 15, f"大纲规划完成,共{len(outline.sections)}个章节", + report_id, "planning", 15, f"Outline planning completed, total {len(outline.sections)} chapters", completed_sections=[] ) ReportManager.save_report(report) - logger.info(f"大纲已保存到文件: {report_id}/outline.json") + logger.info(f"Outline saved to file: {report_id}/outline.json") - # 阶段2: 逐章节生成(分章节保存) + # Giai đoạn 2: Tạo từng chương (lưu theo từng chương) report.status = ReportStatus.GENERATING total_sections = len(outline.sections) - generated_sections = [] # 保存内容用于上下文 + generated_sections = [] # Lưu nội dung lại cho ngữ cảnh những lần sau for i, section in enumerate(outline.sections): section_num = i + 1 base_progress = 20 + int((i / total_sections) * 70) - # 更新进度 + # Cập nhật tiến độ ReportManager.update_progress( report_id, "generating", base_progress, - f"正在生成章节: {section.title} ({section_num}/{total_sections})", + f"Generating chapter: {section.title} ({section_num}/{total_sections})", current_section=section.title, completed_sections=completed_section_titles ) @@ -1649,10 +1649,10 @@ def generate_report( progress_callback( "generating", base_progress, - f"正在生成章节: {section.title} ({section_num}/{total_sections})" + f"Generating chapter: {section.title} ({section_num}/{total_sections})" ) - # 生成主章节内容 + # Tạo nội dung chương section_content = self._generate_section_react( section=section, outline=outline, @@ -1669,11 +1669,11 @@ def generate_report( section.content = section_content generated_sections.append(f"## {section.title}\n\n{section_content}") - # 保存章节 + # Lưu chương ReportManager.save_section(report_id, section_num, section) completed_section_titles.append(section.title) - # 记录章节完成日志 + # Ghi lại kết quả khi chương ra lò full_section_content = f"## {section.title}\n\n{section_content}" if self.report_logger: @@ -1683,54 +1683,54 @@ def generate_report( full_content=full_section_content.strip() ) - logger.info(f"章节已保存: {report_id}/section_{section_num:02d}.md") + logger.info(f"Chapter saved: {report_id}/section_{section_num:02d}.md") - # 更新进度 + # Cập nhật thanh tiến độ ReportManager.update_progress( report_id, "generating", base_progress + int(70 / total_sections), - f"章节 {section.title} 已完成", + f"Chapter {section.title} completed", current_section=None, completed_sections=completed_section_titles ) - # 阶段3: 组装完整报告 + # Giai đoạn 3: Ghép lại toàn bộ báo cáo if progress_callback: - progress_callback("generating", 95, "正在组装完整报告...") + progress_callback("generating", 95, "Assembling full report...") ReportManager.update_progress( - report_id, "generating", 95, "正在组装完整报告...", + report_id, "generating", 95, "Assembling full report...", completed_sections=completed_section_titles ) - # 使用ReportManager组装完整报告 + # Dùng ReportManager để ráp báo cáo report.markdown_content = ReportManager.assemble_full_report(report_id, outline) report.status = ReportStatus.COMPLETED report.completed_at = datetime.now().isoformat() - # 计算总耗时 + # Tính toán thời gian total_time_seconds = (datetime.now() - start_time).total_seconds() - # 记录报告完成日志 + # Ghi nhận log khi tạo thành công if self.report_logger: self.report_logger.log_report_complete( total_sections=total_sections, total_time_seconds=total_time_seconds ) - # 保存最终报告 + # Lưu kết quả cuối cùng ReportManager.save_report(report) ReportManager.update_progress( - report_id, "completed", 100, "报告生成完成", + report_id, "completed", 100, "Report generation completed", completed_sections=completed_section_titles ) if progress_callback: - progress_callback("completed", 100, "报告生成完成") + progress_callback("completed", 100, "Report generation completed") - logger.info(f"报告生成完成: {report_id}") + logger.info(f"Report generation completed: {report_id}") - # 关闭控制台日志记录器 + # Tắt ghi log console if self.console_logger: self.console_logger.close() self.console_logger = None @@ -1738,25 +1738,25 @@ def generate_report( return report except Exception as e: - logger.error(f"报告生成失败: {str(e)}") + logger.error(f"Report generation failed: {str(e)}") report.status = ReportStatus.FAILED report.error = str(e) - # 记录错误日志 + # Ghi lại log lỗi if self.report_logger: self.report_logger.log_error(str(e), "failed") - # 保存失败状态 + # Lưu lại trạng thái failed try: ReportManager.save_report(report) ReportManager.update_progress( - report_id, "failed", -1, f"报告生成失败: {str(e)}", + report_id, "failed", -1, f"Report generation failed: {str(e)}", completed_sections=completed_section_titles ) except Exception: - pass # 忽略保存失败的错误 + pass # Lờ đi nếu bị lỗi trong khi lưu - # 关闭控制台日志记录器 + # Tắt console log if self.console_logger: self.console_logger.close() self.console_logger = None @@ -1769,59 +1769,59 @@ def chat( chat_history: List[Dict[str, str]] = None ) -> Dict[str, Any]: """ - 与Report Agent对话 + Trò chuyện cùng Report Agent - 在对话中Agent可以自主调用检索工具来回答问题 + Trong quá trình chat, Agent có khả năng tự gọi công cụ tìm kiếm để trả lời Args: - message: 用户消息 - chat_history: 对话历史 + message: Tin nhắn mới của người dùng + chat_history: Lịch sử đàm thoại Returns: { - "response": "Agent回复", - "tool_calls": [调用的工具列表], - "sources": [信息来源] + "response": "Nội dung trả lời của Agent", + "tool_calls": [Danh sách các công cụ đã sử dụng], + "sources": [Nguồn thông tin] } """ - logger.info(f"Report Agent对话: {message[:50]}...") + logger.info(f"Report Agent chat: {message[:50]}...") chat_history = chat_history or [] - # 获取已生成的报告内容 + # Nhúng nội dung báo cáo cũ vào report_content = "" try: report = ReportManager.get_report_by_simulation(self.simulation_id) if report and report.markdown_content: - # 限制报告长度,避免上下文过长 + # Cắt bớt độ dài vì tránh nghẽn bộ nhớ report_content = report.markdown_content[:15000] if len(report.markdown_content) > 15000: - report_content += "\n\n... [报告内容已截断] ..." + report_content += "\n\n... [Report content truncated] ..." except Exception as e: - logger.warning(f"获取报告内容失败: {e}") + logger.warning(f"Failed to get report content: {e}") system_prompt = CHAT_SYSTEM_PROMPT_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 available)", tools_description=self._get_tools_description(), ) - # 构建消息 + # Xây dựng message messages = [{"role": "system", "content": system_prompt}] - # 添加历史对话 - for h in chat_history[-10:]: # 限制历史长度 + # Nạp lịch sử cuộc trò chuyện + for h in chat_history[-10:]: # Giới hạn số lượng tin nhắn quá khứ messages.append(h) - # 添加用户消息 + # Nạp tin nhắn mới nhất messages.append({ "role": "user", "content": message }) - # ReACT循环(简化版) + # Vòng lặp ReACT (phiên bản thu gọn) tool_calls_made = [] - max_iterations = 2 # 减少迭代轮数 + max_iterations = 2 # Giảm số lần lặp for iteration in range(max_iterations): response = self.llm.chat( @@ -1829,11 +1829,11 @@ def chat( temperature=0.5 ) - # 解析工具调用 + # Phân tích lệnh gọi công cụ tool_calls = self._parse_tool_calls(response) if not tool_calls: - # 没有工具调用,直接返回响应 + # Không thấy gọi công cụ, trả luôn kết quả clean_response = re.sub(r'.*?', '', response, flags=re.DOTALL) clean_response = re.sub(r'\[TOOL_CALL\].*?\)', '', clean_response) @@ -1843,33 +1843,33 @@ def chat( "sources": [tc.get("parameters", {}).get("query", "") for tc in tool_calls_made] } - # 执行工具调用(限制数量) + # Khởi chạy công cụ (giới hạn số lượng) tool_results = [] - for call in tool_calls[:1]: # 每轮最多执行1次工具调用 + for call in tool_calls[:1]: # Cả vòng chỉ cho chạy công cụ tối đa 1 lần if len(tool_calls_made) >= self.MAX_TOOL_CALLS_PER_CHAT: break result = self._execute_tool(call["name"], call.get("parameters", {})) tool_results.append({ "tool": call["name"], - "result": result[:1500] # 限制结果长度 + "result": result[:1500] # Giới hạn kích thước kết quả để tránh nghẽn }) tool_calls_made.append(call) - # 将结果添加到消息 + # Đắp kết quả vào message messages.append({"role": "assistant", "content": response}) - observation = "\n".join([f"[{r['tool']}结果]\n{r['result']}" for r in tool_results]) + observation = "\n".join([f"[{r['tool']} result]\n{r['result']}" for r in tool_results]) messages.append({ "role": "user", "content": observation + CHAT_OBSERVATION_SUFFIX }) - # 达到最大迭代,获取最终响应 + # Đã đạt giới hạn lặp, sinh câu trả lời chốt chặn final_response = self.llm.chat( messages=messages, temperature=0.5 ) - # 清理响应 + # Dọn dẹp phản hồi clean_response = re.sub(r'.*?', '', final_response, flags=re.DOTALL) clean_response = re.sub(r'\[TOOL_CALL\].*?\)', '', clean_response) @@ -1882,95 +1882,95 @@ def chat( class ReportManager: """ - 报告管理器 + Trình Quản lý Báo cáo - 负责报告的持久化存储和检索 + Phụ trách việc lưu trữ lâu dài và trích xuất báo cáo - 文件结构(分章节输出): + Cấu trúc thư mục (Lưu báo cáo phân thành từng chương): reports/ {report_id}/ - meta.json - 报告元信息和状态 - outline.json - 报告大纲 - progress.json - 生成进度 - section_01.md - 第1章节 - section_02.md - 第2章节 + meta.json - File metadata và trạng thái + outline.json - Dàn ý + progress.json - Tiến trình phát sinh báo cáo + section_01.md - Chương 1 + section_02.md - Chương 2 ... - full_report.md - 完整报告 + full_report.md - Báo cáo hoàn chỉnh """ - # 报告存储目录 + # Phân vùng lưu trữ gốc REPORTS_DIR = os.path.join(Config.UPLOAD_FOLDER, 'reports') @classmethod def _ensure_reports_dir(cls): - """确保报告根目录存在""" + """Đảm bảo thư mục lưu trữ gốc luôn tồn tại""" os.makedirs(cls.REPORTS_DIR, exist_ok=True) @classmethod def _get_report_folder(cls, report_id: str) -> str: - """获取报告文件夹路径""" + """Lấy đường dẫn thư mục báo cáo""" return os.path.join(cls.REPORTS_DIR, report_id) @classmethod def _ensure_report_folder(cls, report_id: str) -> str: - """确保报告文件夹存在并返回路径""" + """Đảm bảo thư mục báo cáo tồn tại (và trả lại đường dẫn)""" folder = cls._get_report_folder(report_id) os.makedirs(folder, exist_ok=True) return folder @classmethod def _get_report_path(cls, report_id: str) -> str: - """获取报告元信息文件路径""" + """Lấy file chi tiết cơ bản của báo cáo""" return os.path.join(cls._get_report_folder(report_id), "meta.json") @classmethod def _get_report_markdown_path(cls, report_id: str) -> str: - """获取完整报告Markdown文件路径""" + """Lấy file báo cáo dạng Markdown hợp nhất""" return os.path.join(cls._get_report_folder(report_id), "full_report.md") @classmethod def _get_outline_path(cls, report_id: str) -> str: - """获取大纲文件路径""" + """Lấy file mang nội dung dàn ý""" return os.path.join(cls._get_report_folder(report_id), "outline.json") @classmethod def _get_progress_path(cls, report_id: str) -> str: - """获取进度文件路径""" + """Lấy file tiến độ tạo báo cáo""" return os.path.join(cls._get_report_folder(report_id), "progress.json") @classmethod def _get_section_path(cls, report_id: str, section_index: int) -> str: - """获取章节Markdown文件路径""" + """Lấy file cho một chương Markdown chỉ định""" return os.path.join(cls._get_report_folder(report_id), f"section_{section_index:02d}.md") @classmethod def _get_agent_log_path(cls, report_id: str) -> str: - """获取 Agent 日志文件路径""" + """Đường dẫn của Nhật ký Agent""" return os.path.join(cls._get_report_folder(report_id), "agent_log.jsonl") @classmethod def _get_console_log_path(cls, report_id: str) -> str: - """获取控制台日志文件路径""" + """Đường dẫn của file console log""" return os.path.join(cls._get_report_folder(report_id), "console_log.txt") @classmethod def get_console_log(cls, report_id: str, from_line: int = 0) -> Dict[str, Any]: """ - 获取控制台日志内容 + Lấy nội dung ghi chép file console_log - 这是报告生成过程中的控制台输出日志(INFO、WARNING等), - 与 agent_log.jsonl 的结构化日志不同。 + Đây là text được in ra khi tạo báo cáo (INFO, WARNING, v.v.), + Khác với báo cáo có cấu trúc JSON của agent_log.jsonl Args: - report_id: 报告ID - from_line: 从第几行开始读取(用于增量获取,0 表示从头开始) + report_id: ID báo cáo + from_line: Bắt đầu lấy từ dòng nào (để phục vụ chế độ tải tuần tự cho front-end, truyền 0 để lấy từ đầu) Returns: { - "logs": [日志行列表], - "total_lines": 总行数, - "from_line": 起始行号, - "has_more": 是否还有更多日志 + "logs": [Danh sách text log], + "total_lines": Tổng dòng hiện có, + "from_line": Số thứ tự dòng bắt đầu, + "has_more": Cờ xét còn trang sau không } """ log_path = cls._get_console_log_path(report_id) @@ -1990,26 +1990,27 @@ def get_console_log(cls, report_id: str, from_line: int = 0) -> Dict[str, Any]: for i, line in enumerate(f): total_lines = i + 1 if i >= from_line: - # 保留原始日志行,去掉末尾换行符 + # Giữ nguyên bản text, gạt bỏ breakline cuối chuỗi logs.append(line.rstrip('\n\r')) return { "logs": logs, "total_lines": total_lines, "from_line": from_line, - "has_more": False # 已读取到末尾 + "has_more": False # Vì đã đọc kịch sàn file } + @classmethod def get_console_log_stream(cls, report_id: str) -> List[str]: """ - 获取完整的控制台日志(一次性获取全部) + Lấy toàn bộ console log (lấy một lần duy nhất) Args: - report_id: 报告ID + report_id: ID báo cáo Returns: - 日志行列表 + Danh sách dòng log """ result = cls.get_console_log(report_id, from_line=0) return result["logs"] @@ -2017,18 +2018,18 @@ def get_console_log_stream(cls, report_id: str) -> List[str]: @classmethod def get_agent_log(cls, report_id: str, from_line: int = 0) -> Dict[str, Any]: """ - 获取 Agent 日志内容 + Lấy nội dung Agent log Args: - report_id: 报告ID - from_line: 从第几行开始读取(用于增量获取,0 表示从头开始) + report_id: ID báo cáo + from_line: Bắt đầu lấy từ dòng nào (để phục vụ chế độ tải tuần tự cho front-end, truyền 0 để lấy từ đầu) Returns: { - "logs": [日志条目列表], - "total_lines": 总行数, - "from_line": 起始行号, - "has_more": 是否还有更多日志 + "logs": [Danh sách đối tượng log], + "total_lines": Tổng dòng hiện có, + "from_line": Số thứ tự dòng bắt đầu, + "has_more": Cờ xét còn trang sau không } """ log_path = cls._get_agent_log_path(report_id) @@ -2052,26 +2053,26 @@ def get_agent_log(cls, report_id: str, from_line: int = 0) -> Dict[str, Any]: log_entry = json.loads(line.strip()) logs.append(log_entry) except json.JSONDecodeError: - # 跳过解析失败的行 + # Bỏ qua những dòng parse trượt continue return { "logs": logs, "total_lines": total_lines, "from_line": from_line, - "has_more": False # 已读取到末尾 + "has_more": False # Đã tới kịch trần file } @classmethod def get_agent_log_stream(cls, report_id: str) -> List[Dict[str, Any]]: """ - 获取完整的 Agent 日志(用于一次性获取全部) + Lấy toàn bộ nội dung agent_log (để lấy một cú tóm gọn) Args: - report_id: 报告ID + report_id: ID báo cáo Returns: - 日志条目列表 + Danh sách đối tượng log """ result = cls.get_agent_log(report_id, from_line=0) return result["logs"] @@ -2079,16 +2080,16 @@ def get_agent_log_stream(cls, report_id: str) -> List[Dict[str, Any]]: @classmethod def save_outline(cls, report_id: str, outline: ReportOutline) -> None: """ - 保存报告大纲 + Lưu báo cáo dàn ý - 在规划阶段完成后立即调用 + Gọi ngay lập tức khi hoàn thành việc lên kế hoạch (planning) """ cls._ensure_report_folder(report_id) with open(cls._get_outline_path(report_id), 'w', encoding='utf-8') as f: json.dump(outline.to_dict(), f, ensure_ascii=False, indent=2) - logger.info(f"大纲已保存: {report_id}") + logger.info(f"Outline saved: {report_id}") @classmethod def save_section( @@ -2098,49 +2099,49 @@ def save_section( section: ReportSection ) -> str: """ - 保存单个章节 + Lưu từng chương đơn lẻ - 在每个章节生成完成后立即调用,实现分章节输出 + Gọi ngay sau khi tạo xong chương báo cáo, thực hiện việc in ấn từng phần Args: - report_id: 报告ID - section_index: 章节索引(从1开始) - section: 章节对象 + report_id: ID báo cáo + section_index: Chỉ mục chương (Cơ sở 1) + section: Đối tượng chương Returns: - 保存的文件路径 + Đường dẫn thư mục được ghi """ cls._ensure_report_folder(report_id) - # 构建章节Markdown内容 - 清理可能存在的重复标题 + # Build Markdown content cho chương - xóa những tiêu đề trùng (nếu có) cleaned_content = cls._clean_section_content(section.content, section.title) md_content = f"## {section.title}\n\n" if cleaned_content: md_content += f"{cleaned_content}\n\n" - # 保存文件 + # Ghi đè tệp tin file_suffix = f"section_{section_index:02d}.md" file_path = os.path.join(cls._get_report_folder(report_id), file_suffix) with open(file_path, 'w', encoding='utf-8') as f: f.write(md_content) - logger.info(f"章节已保存: {report_id}/{file_suffix}") + logger.info(f"Chapter saved: {report_id}/{file_suffix}") return file_path @classmethod def _clean_section_content(cls, content: str, section_title: str) -> str: """ - 清理章节内容 + Làm sạch nội dung chương - 1. 移除内容开头与章节标题重复的Markdown标题行 - 2. 将所有 ### 及以下级别的标题转换为粗体文本 + 1. Xóa các dòng tiêu đề Markdown ở đầu trùng lặp với tiêu đề chương + 2. Biến đổi những tiêu đề cấp ### trở xuống thành in đậm Args: - content: 原始内容 - section_title: 章节标题 + content: Nội dung thô + section_title: Tiêu đề chương Returns: - 清理后的内容 + Nội dung sau khi làm sạch """ import re @@ -2155,26 +2156,26 @@ def _clean_section_content(cls, content: str, section_title: str) -> str: for i, line in enumerate(lines): stripped = line.strip() - # 检查是否是Markdown标题行 + # Kiểm tra dòng này có phải heading sinh từ MD không heading_match = re.match(r'^(#{1,6})\s+(.+)$', stripped) if heading_match: level = len(heading_match.group(1)) title_text = heading_match.group(2).strip() - # 检查是否是与章节标题重复的标题(跳过前5行内的重复) + # Bỏ qua nếu tiêu đề ở tầm 5 dòng đầu lại trùng khớp với nhan đề chính if i < 5: if title_text == section_title or title_text.replace(' ', '') == section_title.replace(' ', ''): skip_next_empty = True continue - # 将所有级别的标题(#, ##, ###, ####等)转换为粗体 - # 因为章节标题由系统添加,内容中不应有任何标题 + # Đổi toàn bộ các tag MD heading (#, ##, ###, v.v...) sang in đậm Text + # Do heading đã được config sinh từ tool gốc, không nên có MD style tại đây cleaned_lines.append(f"**{title_text}**") - cleaned_lines.append("") # 添加空行 + cleaned_lines.append("") # Gắn thêm dòng trống continue - # 如果上一行是被跳过的标题,且当前行为空,也跳过 + # Nếu dòng kề là dòng trắng thuộc chuỗi heading trùng lặp, cần lược bỏ luôn if skip_next_empty and stripped == '': skip_next_empty = False continue @@ -2182,14 +2183,14 @@ def _clean_section_content(cls, content: str, section_title: str) -> str: skip_next_empty = False cleaned_lines.append(line) - # 移除开头的空行 + # Bỏ đi mấy block dòng trắng vô danh ở mở đầu while cleaned_lines and cleaned_lines[0].strip() == '': cleaned_lines.pop(0) - # 移除开头的分隔线 + # Xóa các gạch đường phân cách ở đầu while cleaned_lines and cleaned_lines[0].strip() in ['---', '***', '___']: cleaned_lines.pop(0) - # 同时移除分隔线后的空行 + # Remove đồng thời new lines gắn theo gạch ngang while cleaned_lines and cleaned_lines[0].strip() == '': cleaned_lines.pop(0) @@ -2206,9 +2207,9 @@ def update_progress( completed_sections: List[str] = None ) -> None: """ - 更新报告生成进度 + Cập nhật tiến độ tạo báo cáo - 前端可以通过读取progress.json获取实时进度 + Frontend có thể lấy tiến độ real-time theo progress.json """ cls._ensure_report_folder(report_id) @@ -2226,7 +2227,7 @@ def update_progress( @classmethod def get_progress(cls, report_id: str) -> Optional[Dict[str, Any]]: - """获取报告生成进度""" + """Lấy tiến độ tạo báo cáo""" path = cls._get_progress_path(report_id) if not os.path.exists(path): @@ -2238,9 +2239,9 @@ def get_progress(cls, report_id: str) -> Optional[Dict[str, Any]]: @classmethod def get_generated_sections(cls, report_id: str) -> List[Dict[str, Any]]: """ - 获取已生成的章节列表 + Lấy danh sách các chương đã tạo xong - 返回所有已保存的章节文件信息 + Trả về toàn bộ thông tin tệp từng phần """ folder = cls._get_report_folder(report_id) @@ -2254,7 +2255,7 @@ def get_generated_sections(cls, report_id: str) -> List[Dict[str, Any]]: with open(file_path, 'r', encoding='utf-8') as f: content = f.read() - # 从文件名解析章节索引 + # Phân tích index của chương từ tên tệp parts = filename.replace('.md', '').split('_') section_index = int(parts[1]) @@ -2269,48 +2270,48 @@ def get_generated_sections(cls, report_id: str) -> List[Dict[str, Any]]: @classmethod def assemble_full_report(cls, report_id: str, outline: ReportOutline) -> str: """ - 组装完整报告 + Lắp ráp toàn bộ báo cáo - 从已保存的章节文件组装完整报告,并进行标题清理 + Từ các chương đã lưu, tổng chắp báo cáo lại và dọn dẹp nhan đề """ folder = cls._get_report_folder(report_id) - # 构建报告头部 + # Build phần Header báo cáo md_content = f"# {outline.title}\n\n" md_content += f"> {outline.summary}\n\n" md_content += f"---\n\n" - # 按顺序读取所有章节文件 + # Đọc files từ mọi chương theo thứ tự sections = cls.get_generated_sections(report_id) for section_info in sections: md_content += section_info["content"] - # 后处理:清理整个报告的标题问题 + # Hậu kiểm: dọn dẹp cấu trúc nhan đề trong toàn bộ file Markdown md_content = cls._post_process_report(md_content, outline) - # 保存完整报告 + # Ghi lại tệp rốt cục full_path = cls._get_report_markdown_path(report_id) with open(full_path, 'w', encoding='utf-8') as f: f.write(md_content) - logger.info(f"完整报告已组装: {report_id}") + logger.info(f"Full report assembled: {report_id}") return md_content @classmethod def _post_process_report(cls, content: str, outline: ReportOutline) -> str: """ - 后处理报告内容 + Hậu kiểm nội dung báo cáo - 1. 移除重复的标题 - 2. 保留报告主标题(#)和章节标题(##),移除其他级别的标题(###, ####等) - 3. 清理多余的空行和分隔线 + 1. Xóa nhan đề trùng lặp + 2. Dữ nguyên nhan đề chính (#) và nhan đề chương (##), xóa/giảm các tag nhan đề phụ (###, #### v.v) + 3. Dọn dẹp khoảng trống và đường ranh giới thừa thãi Args: - content: 原始报告内容 - outline: 报告大纲 + content: Nội dung thô của báo cáo + outline: Dàn ý Returns: - 处理后的内容 + Nội dung sau khi xử lý """ import re @@ -2318,7 +2319,7 @@ def _post_process_report(cls, content: str, outline: ReportOutline) -> str: processed_lines = [] prev_was_heading = False - # 收集大纲中的所有章节标题 + # Gom góp tất thảy nhan đề từ outline section_titles = set() for section in outline.sections: section_titles.add(section.title) @@ -2328,14 +2329,14 @@ def _post_process_report(cls, content: str, outline: ReportOutline) -> str: line = lines[i] stripped = line.strip() - # 检查是否是标题行 + # Kiểm tra coi đây có là nhan đề hay không heading_match = re.match(r'^(#{1,6})\s+(.+)$', stripped) if heading_match: level = len(heading_match.group(1)) title = heading_match.group(2).strip() - # 检查是否是重复标题(在连续5行内出现相同内容的标题) + # Kiểm tra nhan đề này đang quẩn quanh lặp lại chuỗi cũ chăng (ngưỡng 5 dòng) is_duplicate = False for j in range(max(0, len(processed_lines) - 5), len(processed_lines)): prev_line = processed_lines[j].strip() @@ -2347,43 +2348,43 @@ def _post_process_report(cls, content: str, outline: ReportOutline) -> str: break if is_duplicate: - # 跳过重复标题及其后的空行 + # Rũ đi dòng nhan đề trùng lặp và các dòng newline ngay sau i += 1 while i < len(lines) and lines[i].strip() == '': i += 1 continue - # 标题层级处理: - # - # (level=1) 只保留报告主标题 - # - ## (level=2) 保留章节标题 - # - ### 及以下 (level>=3) 转换为粗体文本 + # Xử lý quy mô cấp bậc tiêu đề: + # - # (level=1) Chỉ dành cho nhan đề lõi của toàn văn kiện + # - ## (level=2) Bám dính tiêu đề chương mục + # - ### và thấp hơn (level>=3) Băm thành chữ in đậm if level == 1: if title == outline.title: - # 保留报告主标题 + # Giữ nhan đề gốc processed_lines.append(line) prev_was_heading = True elif title in section_titles: - # 章节标题错误使用了#,修正为## + # Do nhan đề chương đánh bừa ra thẻ #, buộc ép về lại ## processed_lines.append(f"## {title}") prev_was_heading = True else: - # 其他一级标题转为粗体 + # Các tiêu đề cấp 1 tạp nham cho in đậm processed_lines.append(f"**{title}**") processed_lines.append("") prev_was_heading = False elif level == 2: if title in section_titles or title == outline.title: - # 保留章节标题 + # Bảo lưu nhan đề chương processed_lines.append(line) prev_was_heading = True else: - # 非章节的二级标题转为粗体 + # Nếu chả phải chương thì in đậm nốt thẻ cấp 2 processed_lines.append(f"**{title}**") processed_lines.append("") prev_was_heading = False else: - # ### 及以下级别的标题转换为粗体文本 + # Thẻ cấp 3 trở đi cho in đậm processed_lines.append(f"**{title}**") processed_lines.append("") prev_was_heading = False @@ -2392,12 +2393,12 @@ def _post_process_report(cls, content: str, outline: ReportOutline) -> str: continue elif stripped == '---' and prev_was_heading: - # 跳过标题后紧跟的分隔线 + # Tránh lôi đường chia gạch dưới title i += 1 continue elif stripped == '' and prev_was_heading: - # 标题后只保留一个空行 + # Ngậm 1 dòng trắng kề sau nhan đề if processed_lines and processed_lines[-1].strip() != '': processed_lines.append(line) prev_was_heading = False @@ -2408,7 +2409,7 @@ def _post_process_report(cls, content: str, outline: ReportOutline) -> str: i += 1 - # 清理连续的多个空行(保留最多2个) + # Vuốt láng khoảng trắng đa tầng (chừa max 2) result_lines = [] empty_count = 0 for line in processed_lines: @@ -2424,31 +2425,31 @@ def _post_process_report(cls, content: str, outline: ReportOutline) -> str: @classmethod def save_report(cls, report: Report) -> None: - """保存报告元信息和完整报告""" + """Lưu lại metadata và toàn văn báo cáo""" cls._ensure_report_folder(report.report_id) - # 保存元信息JSON + # Ghi meta file dạng JSON with open(cls._get_report_path(report.report_id), 'w', encoding='utf-8') as f: json.dump(report.to_dict(), f, ensure_ascii=False, indent=2) - # 保存大纲 + # Ghi mục lục tổng quan if report.outline: cls.save_outline(report.report_id, report.outline) - # 保存完整Markdown报告 + # Lưu file MD trọn gói if report.markdown_content: with open(cls._get_report_markdown_path(report.report_id), 'w', encoding='utf-8') as f: f.write(report.markdown_content) - logger.info(f"报告已保存: {report.report_id}") + logger.info(f"Report saved: {report.report_id}") @classmethod def get_report(cls, report_id: str) -> Optional[Report]: - """获取报告""" + """Lấy về toàn văn mục báo cáo""" path = cls._get_report_path(report_id) if not os.path.exists(path): - # 兼容旧格式:检查直接存储在reports目录下的文件 + # Tương thích ngược: thử xem file .json trần trụi ở ngay thư mục gốc old_path = os.path.join(cls.REPORTS_DIR, f"{report_id}.json") if os.path.exists(old_path): path = old_path @@ -2458,7 +2459,7 @@ def get_report(cls, report_id: str) -> Optional[Report]: with open(path, 'r', encoding='utf-8') as f: data = json.load(f) - # 重建Report对象 + # Cấu trúc lại model outline = None if data.get('outline'): outline_data = data['outline'] @@ -2474,7 +2475,7 @@ def get_report(cls, report_id: str) -> Optional[Report]: sections=sections ) - # 如果markdown_content为空,尝试从full_report.md读取 + # Nếu chưa bếch được đoạn markdown thì tìm từ file .md markdown_content = data.get('markdown_content', '') if not markdown_content: full_report_path = cls._get_report_markdown_path(report_id) @@ -2497,17 +2498,17 @@ def get_report(cls, report_id: str) -> Optional[Report]: @classmethod def get_report_by_simulation(cls, simulation_id: str) -> Optional[Report]: - """根据模拟ID获取报告""" + """Tìm báo cáo thông qua ID phiến giả lập""" cls._ensure_reports_dir() for item in os.listdir(cls.REPORTS_DIR): item_path = os.path.join(cls.REPORTS_DIR, item) - # 新格式:文件夹 + # Định dạng mới coi nó là directory if os.path.isdir(item_path): report = cls.get_report(item) if report and report.simulation_id == simulation_id: return report - # 兼容旧格式:JSON文件 + # Tương thích format cũ: .json elif item.endswith('.json'): report_id = item[:-5] report = cls.get_report(report_id) @@ -2518,19 +2519,19 @@ def get_report_by_simulation(cls, simulation_id: str) -> Optional[Report]: @classmethod def list_reports(cls, simulation_id: Optional[str] = None, limit: int = 50) -> List[Report]: - """列出报告""" + """Liệt kê rổ báo cáo""" cls._ensure_reports_dir() reports = [] for item in os.listdir(cls.REPORTS_DIR): item_path = os.path.join(cls.REPORTS_DIR, item) - # 新格式:文件夹 + # Standard mới if os.path.isdir(item_path): report = cls.get_report(item) if report: if simulation_id is None or report.simulation_id == simulation_id: reports.append(report) - # 兼容旧格式:JSON文件 + # Standard quá độ .json elif item.endswith('.json'): report_id = item[:-5] report = cls.get_report(report_id) @@ -2538,25 +2539,25 @@ def list_reports(cls, simulation_id: Optional[str] = None, limit: int = 50) -> L if simulation_id is None or report.simulation_id == simulation_id: reports.append(report) - # 按创建时间倒序 + # Sort descending bởi thời gian tạo reports.sort(key=lambda r: r.created_at, reverse=True) return reports[:limit] @classmethod def delete_report(cls, report_id: str) -> bool: - """删除报告(整个文件夹)""" + """Xóa rễ cụm folder tương ứng với report""" import shutil folder_path = cls._get_report_folder(report_id) - # 新格式:删除整个文件夹 + # Định dạng mới quăng folder là mượt if os.path.exists(folder_path) and os.path.isdir(folder_path): shutil.rmtree(folder_path) - logger.info(f"报告文件夹已删除: {report_id}") + logger.info(f"Report folder removed: {report_id}") return True - # 兼容旧格式:删除单独的文件 + # Mode tương thích: xóa file deleted = False old_json_path = os.path.join(cls.REPORTS_DIR, f"{report_id}.json") old_md_path = os.path.join(cls.REPORTS_DIR, f"{report_id}.md") diff --git a/backend/app/services/simulation_config_generator.py b/backend/app/services/simulation_config_generator.py index cc362508b..926ff1b48 100644 --- a/backend/app/services/simulation_config_generator.py +++ b/backend/app/services/simulation_config_generator.py @@ -1,13 +1,13 @@ """ -模拟配置智能生成器 -使用LLM根据模拟需求、文档内容、图谱信息自动生成细致的模拟参数 -实现全程自动化,无需人工设置参数 - -采用分步生成策略,避免一次性生成过长内容导致失败: -1. 生成时间配置 -2. 生成事件配置 -3. 分批生成Agent配置 -4. 生成平台配置 +Trình tạo tạo ra cấu hình Simulation tự động +Sử dụng LLM theo yêu cầu mô phỏng, nội dung tài liệu và thông tin đồ thị để tự động thiết lập chi tiết các tham số +Tất cả đều tự động mà không cần can thiệp thủ công tạo tham số + +Áp dụng chiến lược tạo từng bước để tránh lỗi do cố gắng tạo nội dung quá dài cùng một lúc: +1. Tạo cấu hình thời gian +2. Tạo cấu hình các Event +3. Tạo cấu hình cho các Agent theo đợt +4. Tạo cấu hình nền tảng """ import json @@ -24,121 +24,121 @@ logger = get_logger('mirofish.simulation_config') -# 中国作息时间配置(北京时间) +# Cấu hình thời gian thói quen Trung Quốc (Theo giờ Bắc Kinh) CHINA_TIMEZONE_CONFIG = { - # 深夜时段(几乎无人活动) + # Khung giờ khuya (Hầu như không có hoạt động) "dead_hours": [0, 1, 2, 3, 4, 5], - # 早间时段(逐渐醒来) + # Khung giờ sáng (Dần thức dậy) "morning_hours": [6, 7, 8], - # 工作时段 + # Khung giờ làm việc "work_hours": [9, 10, 11, 12, 13, 14, 15, 16, 17, 18], - # 晚间高峰(最活跃) + # Khung giờ cao điểm buổi tối (Hoạt động mạnh nhất) "peak_hours": [19, 20, 21, 22], - # 夜间时段(活跃度下降) + # Khung giờ ban đêm (Hoạt động giảm sút) "night_hours": [23], - # 活跃度系数 + # Hệ số hoạt động tương ứng với mỗi thời điểm "activity_multipliers": { - "dead": 0.05, # 凌晨几乎无人 - "morning": 0.4, # 早间逐渐活跃 - "work": 0.7, # 工作时段中等 - "peak": 1.5, # 晚间高峰 - "night": 0.5 # 深夜下降 + "dead": 0.05, # Gần như không có ai lúc rạng sáng + "morning": 0.4, # Sáng sớm bắt đầu dần sôi động + "work": 0.7, # Mức trung bình trong giờ làm việc + "peak": 1.5, # Cao điểm tối + "night": 0.5 # Giảm sút đêm khuya } } @dataclass class AgentActivityConfig: - """单个Agent的活动配置""" + """Cấu hình hoạt động cho một Agent""" agent_id: int entity_uuid: str entity_name: str entity_type: str - # 活跃度配置 (0.0-1.0) - activity_level: float = 0.5 # 整体活跃度 + # Mức độ hoạt động (0.0-1.0) + activity_level: float = 0.5 # Hoạt động tổng thể - # 发言频率(每小时预期发言次数) + # Tần suất phát ngôn (Số lần comment dự kiến mỗi giờ) posts_per_hour: float = 1.0 comments_per_hour: float = 2.0 - # 活跃时间段(24小时制,0-23) + # Khoảng thời gian hoạt động (Hệ 24 giờ, 0-23) active_hours: List[int] = field(default_factory=lambda: list(range(8, 23))) - # 响应速度(对热点事件的反应延迟,单位:模拟分钟) + # Tốc độ phản hồi (Độ trễ phản ứng với sự kiện nóng, đơn vị: phút mô phỏng) response_delay_min: int = 5 response_delay_max: int = 60 - # 情感倾向 (-1.0到1.0,负面到正面) + # Khuynh hướng cảm xúc (-1.0 đến 1.0, từ tiêu cực đến tích cực) sentiment_bias: float = 0.0 - # 立场(对特定话题的态度) + # Lập trường (Thái độ đối với chủ đề cụ thể) stance: str = "neutral" # supportive, opposing, neutral, observer - # 影响力权重(决定其发言被其他Agent看到的概率) + # Trọng số ảnh hưởng (Xác định mức độ bài đăng được Agent khác nhìn thấy) influence_weight: float = 1.0 @dataclass class TimeSimulationConfig: - """时间模拟配置(基于中国人作息习惯)""" - # 模拟总时长(模拟小时数) - total_simulation_hours: int = 72 # 默认模拟72小时(3天) + """Cấu hình thời gian mô phỏng (Dựa trên thói quen sinh hoạt của người Trung)""" + # Tổng thời gian mô phỏng (Giờ) + total_simulation_hours: int = 72 # Mặc định là chạy mô phỏng 72 tiếng (3 ngày) - # 每轮代表的时间(模拟分钟)- 默认60分钟(1小时),加快时间流速 + # Số phút đại diện cho mỗi vòng - Mặc định 60 phút (1 giờ), đẩy nhanh thời gian minutes_per_round: int = 60 - # 每小时激活的Agent数量范围 + # Phạm vi số lượng Agent kích hoạt mỗi giờ agents_per_hour_min: int = 5 agents_per_hour_max: int = 20 - # 高峰时段(晚间19-22点,中国人最活跃的时间) + # Giờ cao điểm (19-22 giờ tối, thời gian sôi động nhất) peak_hours: List[int] = field(default_factory=lambda: [19, 20, 21, 22]) peak_activity_multiplier: float = 1.5 - # 低谷时段(凌晨0-5点,几乎无人活动) + # Khung giờ chết (0-5 giờ, hầu như không ai on) off_peak_hours: List[int] = field(default_factory=lambda: [0, 1, 2, 3, 4, 5]) - off_peak_activity_multiplier: float = 0.05 # 凌晨活跃度极低 + off_peak_activity_multiplier: float = 0.05 # Rạng sáng gần như bằng không - # 早间时段 + # Khung giờ buổi sáng morning_hours: List[int] = field(default_factory=lambda: [6, 7, 8]) morning_activity_multiplier: float = 0.4 - # 工作时段 + # Khung giờ làm việc work_hours: List[int] = field(default_factory=lambda: [9, 10, 11, 12, 13, 14, 15, 16, 17, 18]) work_activity_multiplier: float = 0.7 @dataclass class EventConfig: - """事件配置""" - # 初始事件(模拟开始时的触发事件) + """Cấu hình sự kiện cho Simulation""" + # Các bài Post/Sự kiện khởi đầu (Bắt đầu ngay khi chạy mô phỏng) initial_posts: List[Dict[str, Any]] = field(default_factory=list) - # 定时事件(在特定时间触发的事件) + # Các sự kiện được lập lịch vào các thời điểm nhất định scheduled_events: List[Dict[str, Any]] = field(default_factory=list) - # 热点话题关键词 + # Từ khóa dành cho các chủ đề đang hot (Hot topics) hot_topics: List[str] = field(default_factory=list) - # 舆论引导方向 + # Hướng dẫn dư luận / Đường lối thảo luận narrative_direction: str = "" @dataclass class PlatformConfig: - """平台特定配置""" + """Cấu hình đặc thù dành riêng cho các nền tảng""" platform: str # twitter or reddit - # 推荐算法权重 - recency_weight: float = 0.4 # 时间新鲜度 - popularity_weight: float = 0.3 # 热度 - relevance_weight: float = 0.3 # 相关性 + # Trọng số cho các thuật toán đề xuất + recency_weight: float = 0.4 # Độ mới của bài + popularity_weight: float = 0.3 # Mức độ phổ biến truyền miệng + relevance_weight: float = 0.3 # Mức độ quan tâm / tương quan - # 病毒传播阈值(达到多少互动后触发扩散) + # Ngưỡng lan truyền virus (Cần bao nhiêu tương tác để nội dung bắt đầu phát tán mạnh) viral_threshold: int = 10 - # 回声室效应强度(相似观点聚集程度) + # Độ mạnh của hiệu ứng lan truyền trong nhóm chung chí hướng (buồng phản âm) echo_chamber_strength: float = 0.5 @@ -151,29 +151,29 @@ class SimulationParameters: graph_id: str simulation_requirement: str - # 时间配置 + # Cấu hình thời gian time_config: TimeSimulationConfig = field(default_factory=TimeSimulationConfig) - # Agent配置列表 + # Danh sách cấu hình Agent agent_configs: List[AgentActivityConfig] = field(default_factory=list) - # 事件配置 + # Cấu hình Event event_config: EventConfig = field(default_factory=EventConfig) - # 平台配置 + # Cấu hình nền tảng twitter_config: Optional[PlatformConfig] = None reddit_config: Optional[PlatformConfig] = None - # LLM配置 + # Cấu hình LLM llm_model: str = "" llm_base_url: str = "" - # 生成元数据 + # Dữ liệu metadata khi tạo generated_at: str = field(default_factory=lambda: datetime.now().isoformat()) - generation_reasoning: str = "" # LLM的推理说明 + generation_reasoning: str = "" # Giải thích suy luận từ LLM def to_dict(self) -> Dict[str, Any]: - """转换为字典""" + """Convert sang định dạng Dictionary""" time_dict = asdict(self.time_config) return { "simulation_id": self.simulation_id, @@ -192,34 +192,34 @@ def to_dict(self) -> Dict[str, Any]: } def to_json(self, indent: int = 2) -> str: - """转换为JSON字符串""" + """Convert sang định dạng chuỗi JSON""" return json.dumps(self.to_dict(), ensure_ascii=False, indent=indent) class SimulationConfigGenerator: """ - 模拟配置智能生成器 + Trình tạo cấu hình Simulation tự động bằng LLM - 使用LLM分析模拟需求、文档内容、图谱实体信息, - 自动生成最佳的模拟参数配置 + Sử dụng LLM phân tích yêu cầu mô phỏng, nội dung tài liệu, Entity từ đồ thị, + Tự động xây dựng các thông số cấu trúc tối ưu cho đợt Simulation - 采用分步生成策略: - 1. 生成时间配置和事件配置(轻量级) - 2. 分批生成Agent配置(每批10-20个) - 3. 生成平台配置 + Áp dụng chiến lược tạo từng bước: + 1. Tạo cấu hình thời gian và cấu hình Event (Nhẹ, chạy nhanh) + 2. Phân nhỏ đợt tạo cấu hình cho Agent (Khoảng 10-20 agent mỗi đợt) + 3. Tạo cấu hình nền tảng """ - # 上下文最大字符数 + # Số lượng ký tự tối đa của bộ context MAX_CONTEXT_LENGTH = 50000 - # 每批生成的Agent数量 + # Số lượng Agent để gen cho một lần AGENTS_PER_BATCH = 15 - # 各步骤的上下文截断长度(字符数) - TIME_CONFIG_CONTEXT_LENGTH = 10000 # 时间配置 - EVENT_CONFIG_CONTEXT_LENGTH = 8000 # 事件配置 - ENTITY_SUMMARY_LENGTH = 300 # 实体摘要 - AGENT_SUMMARY_LENGTH = 300 # Agent配置中的实体摘要 - ENTITIES_PER_TYPE_DISPLAY = 20 # 每类实体显示数量 + # Số lượng ký tự giới hạn ở các bước để cắt chuỗi (Ký tự đoạn) + TIME_CONFIG_CONTEXT_LENGTH = 10000 # Cấu hình thời gian + EVENT_CONFIG_CONTEXT_LENGTH = 8000 # Cấu hình sự kiện + ENTITY_SUMMARY_LENGTH = 300 # Tóm tắt các thực thể + AGENT_SUMMARY_LENGTH = 300 # Tóm tắt cấu hình Agent + ENTITIES_PER_TYPE_DISPLAY = 20 # Lượng thực thể cho mổi loại để hiển thị def __init__( self, @@ -232,7 +232,7 @@ def __init__( self.model_name = model_name or Config.LLM_MODEL_NAME if not self.api_key: - raise ValueError("LLM_API_KEY 未配置") + raise ValueError("LLM_API_KEY has not been configured") self.client = OpenAI( api_key=self.api_key, @@ -252,27 +252,27 @@ def generate_config( progress_callback: Optional[Callable[[int, int, str], None]] = None, ) -> SimulationParameters: """ - 智能生成完整的模拟配置(分步生成) + Tạo cấu hình Simulation thông minh tự động hoàn chỉnh (Bằng tư duy chia từng bước) Args: - simulation_id: 模拟ID - project_id: 项目ID - graph_id: 图谱ID - simulation_requirement: 模拟需求描述 - document_text: 原始文档内容 - entities: 过滤后的实体列表 - enable_twitter: 是否启用Twitter - enable_reddit: 是否启用Reddit - progress_callback: 进度回调函数(current_step, total_steps, message) + simulation_id: Nhận dạng quy trình chạy Simulation + project_id: Mã định danh dự án + graph_id: Đồ thị đồ thị + simulation_requirement: Yêu cầu của quá trình mô phỏng + document_text: Nội dung file tài liệu nguồn + entities: Danh sách các thực thể đã được lọc + enable_twitter: Cờ hiệu để bật Twitter + enable_reddit: Cờ hiệu để bật Reddit + progress_callback: Hàm callback lấy trạng thái tiến trình hiện tại (current_step, total_steps, message) Returns: - SimulationParameters: 完整的模拟参数 + SimulationParameters: Bộ tổng cấu hình thông số đầy đủ """ - logger.info(f"开始智能生成模拟配置: simulation_id={simulation_id}, 实体数={len(entities)}") + logger.info(f"Start generating simulation configuration: simulation_id={simulation_id}, entity_count={len(entities)}") - # 计算总步骤数 + # Tính toán tổng số bước num_batches = math.ceil(len(entities) / self.AGENTS_PER_BATCH) - total_steps = 3 + num_batches # 时间配置 + 事件配置 + N批Agent + 平台配置 + total_steps = 3 + num_batches # Cấu hình tgian + Sự kiện + Nx(Agent Batch) + Nền tảng current_step = 0 def report_progress(step: int, message: str): @@ -282,7 +282,7 @@ def report_progress(step: int, message: str): progress_callback(step, total_steps, message) logger.info(f"[{step}/{total_steps}] {message}") - # 1. 构建基础上下文信息 + # 1. Xây dựng thông tin ngữ cảnh cơ bản context = self._build_context( simulation_requirement=simulation_requirement, document_text=document_text, @@ -291,20 +291,19 @@ def report_progress(step: int, message: str): reasoning_parts = [] - # ========== 步骤1: 生成时间配置 ========== - report_progress(1, "生成时间配置...") + # ========== Bước 1: Tạo bộ cấu hình về Thời Gian ========== + report_progress(1, "Generating time configuration...") num_entities = len(entities) time_config_result = self._generate_time_config(context, num_entities) time_config = self._parse_time_config(time_config_result, num_entities) - reasoning_parts.append(f"时间配置: {time_config_result.get('reasoning', '成功')}") - - # ========== 步骤2: 生成事件配置 ========== - report_progress(2, "生成事件配置和热点话题...") + reasoning_parts.append(f"Time config reasoning: {time_config_result.get('reasoning', 'Success')}") + # ========== Bước 2: Tạo cấu hình Event ========== + report_progress(2, "Generating event configuration and hot topics...") event_config_result = self._generate_event_config(context, simulation_requirement, entities) event_config = self._parse_event_config(event_config_result) - reasoning_parts.append(f"事件配置: {event_config_result.get('reasoning', '成功')}") + reasoning_parts.append(f"Event config reasoning: {event_config_result.get('reasoning', 'Success')}") - # ========== 步骤3-N: 分批生成Agent配置 ========== + # ========== Bước 3-N: Chia thành các đợt để lấy cấu hình Agent ========== all_agent_configs = [] for batch_idx in range(num_batches): start_idx = batch_idx * self.AGENTS_PER_BATCH @@ -313,7 +312,7 @@ def report_progress(step: int, message: str): report_progress( 3 + batch_idx, - f"生成Agent配置 ({start_idx + 1}-{end_idx}/{len(entities)})..." + f"Generating agent configuration ({start_idx + 1}-{end_idx}/{len(entities)})..." ) batch_configs = self._generate_agent_configs_batch( @@ -324,16 +323,16 @@ def report_progress(step: int, message: str): ) all_agent_configs.extend(batch_configs) - reasoning_parts.append(f"Agent配置: 成功生成 {len(all_agent_configs)} 个") + reasoning_parts.append(f"Agent config reasoning: Successfully generated {len(all_agent_configs)} agents") - # ========== 为初始帖子分配发布者 Agent ========== - logger.info("为初始帖子分配合适的发布者 Agent...") + # ========== Tiến hành gán người (Agent) để đăng các bài Initial Post ========== + logger.info("Assigning poster agents for initial posts...") event_config = self._assign_initial_post_agents(event_config, all_agent_configs) assigned_count = len([p for p in event_config.initial_posts if p.get("poster_agent_id") is not None]) - reasoning_parts.append(f"初始帖子分配: {assigned_count} 个帖子已分配发布者") + reasoning_parts.append(f"Initial post assignment: {assigned_count} posts have been assigned to publishers") - # ========== 最后一步: 生成平台配置 ========== - report_progress(total_steps, "生成平台配置...") + # ========== Bước cuối: Thiết lập nền tảng ========== + report_progress(total_steps, "Generating platform configuration...") twitter_config = None reddit_config = None @@ -357,7 +356,7 @@ def report_progress(step: int, message: str): echo_chamber_strength=0.6 ) - # 构建最终参数 + # Xây dựng các tham số cuối cùng kết thúc quy trình params = SimulationParameters( simulation_id=simulation_id, project_id=project_id, @@ -373,7 +372,7 @@ def report_progress(step: int, message: str): generation_reasoning=" | ".join(reasoning_parts) ) - logger.info(f"模拟配置生成完成: {len(params.agent_configs)} 个Agent配置") + logger.info(f"Simulation configuration generation complete: {len(params.agent_configs)} agent configs created") return params @@ -383,33 +382,33 @@ def _build_context( document_text: str, entities: List[EntityNode] ) -> str: - """构建LLM上下文,截断到最大长度""" + """Thực hiện xây dựng nội dung Prompt Ngữ cảnh cho LLM, với độ dài có thể bị giới hạn""" - # 实体摘要 + # Tóm tắt lại Thực thể entity_summary = self._summarize_entities(entities) - # 构建上下文 + # Xây dựng nội dung context_parts = [ - f"## 模拟需求\n{simulation_requirement}", - f"\n## 实体信息 ({len(entities)}个)\n{entity_summary}", + f"## Simulation Requirements\n{simulation_requirement}", + f"\n## Entity Information ({len(entities)} entities)\n{entity_summary}", ] current_length = sum(len(p) for p in context_parts) - remaining_length = self.MAX_CONTEXT_LENGTH - current_length - 500 # 留500字符余量 + remaining_length = self.MAX_CONTEXT_LENGTH - current_length - 500 # Dành sẵn 500 ký tự trống if remaining_length > 0 and document_text: doc_text = document_text[:remaining_length] if len(document_text) > remaining_length: - doc_text += "\n...(文档已截断)" - context_parts.append(f"\n## 原始文档内容\n{doc_text}") + doc_text += "\n...(Document Truncated)" + context_parts.append(f"\n## Original Document Content\n{doc_text}") return "\n".join(context_parts) def _summarize_entities(self, entities: List[EntityNode]) -> str: - """生成实体摘要""" + """Tạo chuỗi văn bản Tóm tắt cho các Thực thể""" lines = [] - # 按类型分组 + # Phân nhóm bằng Loại by_type: Dict[str, List[EntityNode]] = {} for e in entities: t = e.get_entity_type() or "Unknown" @@ -418,20 +417,20 @@ def _summarize_entities(self, entities: List[EntityNode]) -> str: by_type[t].append(e) for entity_type, type_entities in by_type.items(): - lines.append(f"\n### {entity_type} ({len(type_entities)}个)") - # 使用配置的显示数量和摘要长度 + lines.append(f"\n### {entity_type} ({len(type_entities)} entity)") + # Số lượng đã được thiết lập mặc định và Giới hạn chiều dài của bảng tóm tắt display_count = self.ENTITIES_PER_TYPE_DISPLAY summary_len = self.ENTITY_SUMMARY_LENGTH for e in type_entities[:display_count]: summary_preview = (e.summary[:summary_len] + "...") if len(e.summary) > summary_len else e.summary lines.append(f"- {e.name}: {summary_preview}") if len(type_entities) > display_count: - lines.append(f" ... 还有 {len(type_entities) - display_count} 个") + lines.append(f" ... and {len(type_entities) - display_count} more entities") return "\n".join(lines) def _call_llm_with_retry(self, prompt: str, system_prompt: str) -> Dict[str, Any]: - """带重试的LLM调用,包含JSON修复逻辑""" + """Tích hợp cơ chế retry mỗi lúc gọi Request LLM bị lỗi và Logic sửa lỗi JSON string""" import re max_attempts = 3 @@ -446,25 +445,25 @@ def _call_llm_with_retry(self, prompt: str, system_prompt: str) -> Dict[str, Any {"role": "user", "content": prompt} ], response_format={"type": "json_object"}, - temperature=0.7 - (attempt * 0.1) # 每次重试降低温度 - # 不设置max_tokens,让LLM自由发挥 + temperature=0.7 - (attempt * 0.1) # Giảm temperature cho mỗi lần retry + # Không đặt max_tokens, cho AI sáng tạo tự do tối đa ) content = response.choices[0].message.content finish_reason = response.choices[0].finish_reason - # 检查是否被截断 + # Kiểm tra nội dung trã về xem có phải bị chặn vì thiếu token (Length vượt qua max) hay không if finish_reason == 'length': - logger.warning(f"LLM输出被截断 (attempt {attempt+1})") + logger.warning(f"LLM output was truncated (attempt {attempt+1})") content = self._fix_truncated_json(content) - # 尝试解析JSON + # Phân tích nội dung JSON try: return json.loads(content) except json.JSONDecodeError as e: - logger.warning(f"JSON解析失败 (attempt {attempt+1}): {str(e)[:80]}") + logger.warning(f"Failed to parse JSON (attempt {attempt+1}): {str(e)[:80]}") - # 尝试修复JSON + # Tiến hành sửa chữa nội dung JSON nếu bị lỗi fixed = self._try_fix_config_json(content) if fixed: return fixed @@ -472,44 +471,44 @@ def _call_llm_with_retry(self, prompt: str, system_prompt: str) -> Dict[str, Any last_error = e except Exception as e: - logger.warning(f"LLM调用失败 (attempt {attempt+1}): {str(e)[:80]}") + logger.warning(f"Failed to call LLM (attempt {attempt+1}): {str(e)[:80]}") last_error = e import time time.sleep(2 * (attempt + 1)) - raise last_error or Exception("LLM调用失败") + raise last_error or Exception("LLM connection completely failed") def _fix_truncated_json(self, content: str) -> str: - """修复被截断的JSON""" + """Đóng dấu ngoặc JSON một cách an toàn cho các string bị cắt ngang""" content = content.strip() - # 计算未闭合的括号 + # Đếm các dấu ngoặc mở bị bỏ sót chưa đóng open_braces = content.count('{') - content.count('}') open_brackets = content.count('[') - content.count(']') - # 检查是否有未闭合的字符串 + # Đảm bảo các thuộc tính string đã được bọc đủ dấu ngoặc kép if content and content[-1] not in '",}]': content += '"' - # 闭合括号 + # Thêm ngoặc đóng cho toàn bộ content += ']' * open_brackets content += '}' * open_braces return content def _try_fix_config_json(self, content: str) -> Optional[Dict[str, Any]]: - """尝试修复配置JSON""" + """Cố gắng khôi phục, chắp ghép lại file cấu trúc config JSON""" import re - # 修复被截断的情况 + # Điền những dấu ngoặc vào chuỗi bị cắt content = self._fix_truncated_json(content) - # 提取JSON部分 + # Regex ra đúng phần ruột nội dung JSON json_match = re.search(r'\{[\s\S]*\}', content) if json_match: json_str = json_match.group() - # 移除字符串中的换行符 + # Loại bỏ các đoạn tab, ngắt line cho string def fix_string(match): s = match.group(0) s = s.replace('\n', ' ').replace('\r', ' ') @@ -521,7 +520,7 @@ def fix_string(match): try: return json.loads(json_str) except: - # 尝试移除所有控制字符 + # Tìm và xóa các control character json_str = re.sub(r'[\x00-\x1f\x7f-\x9f]', ' ', json_str) json_str = re.sub(r'\s+', ' ', json_str) try: @@ -532,35 +531,35 @@ def fix_string(match): return None def _generate_time_config(self, context: str, num_entities: int) -> Dict[str, Any]: - """生成时间配置""" - # 使用配置的上下文截断长度 + """Tạo cấu hình thời gian (Time config) cho các tiến trình""" + # Áp dụng nội dung ngữ cảnh đã được giới hạn chiều dài context_truncated = context[:self.TIME_CONFIG_CONTEXT_LENGTH] - # 计算最大允许值(80%的agent数) + # Cắt lấy số lượng Tối đa số lượng (Chiếm 80% từ số lượng lượng Agent thực thể) max_agents_allowed = max(1, int(num_entities * 0.9)) - prompt = f"""基于以下模拟需求,生成时间模拟配置。 + prompt = f"""Dựa vào yêu cầu Mô phỏng này, hãy tự động gen cho 1 file Thông số thời gian {context_truncated} -## 任务 -请生成时间配置JSON。 +## Task công việc +Vui lòng xuất ra kết quả Thời gian dưới định dạng format JSON -### 基本原则(仅供参考,需根据具体事件和参与群体灵活调整): -- 用户群体为中国人,需符合北京时间作息习惯 -- 凌晨0-5点几乎无人活动(活跃度系数0.05) -- 早上6-8点逐渐活跃(活跃度系数0.4) -- 工作时间9-18点中等活跃(活跃度系数0.7) -- 晚间19-22点是高峰期(活跃度系数1.5) -- 23点后活跃度下降(活跃度系数0.5) -- 一般规律:凌晨低活跃、早间渐增、工作时段中等、晚间高峰 -- **重要**:以下示例值仅供参考,你需要根据事件性质、参与群体特点来调整具体时段 - - 例如:学生群体高峰可能是21-23点;媒体全天活跃;官方机构只在工作时间 - - 例如:突发热点可能导致深夜也有讨论,off_peak_hours 可适当缩短 +### Logic cơ bản có thể cần để tham khảo (Hãy dựa trên nhu cầu của user và hoàn cảnh để suy ra): +- Vị trí của người tham gia là người dùng mạng Trung Quốc, cần sinh hoạt bằng thói quen sinh học giờ chuẩn Bắc Kinh (China Time: GMT+8). +- Không xuất hiện hay có dấu hiệu online của người dùng từ 0-5 giờ sáng (Hệ số Active 0.05). +- Tăng nhẹ số lượng truy cập lại mức trung thành khoảng giữa 6-8 giờ sáng (Hệ số Active 0.4). +- Số lượng active hoạt động ở mức bình ổn khoảng từ 9-18 giờ sáng (Hệ số Active 0.7). +- Khung giờ sôi động nhất sẽ tập trung quanh 19-22 giờ tối (Hệ số Active 1.5). +- Tỷ lệ giảm lại sau 23 giờ (Hệ số Active 0.5). +- Cơ chế bình thường: Đêm không online, sáng bắt đầu đăng bài, giờ hành chính bình bình và cao trào trong buổi tối thức đêm +- **Chỉ Dẫn Rất Quan Trọng**: Những thông tin từ list được lấy để tham chiếu. Còn thông số thật sự còn phải tùy theo Đặc điểm, Tình Huống đối tượng ở Mạng và Thời Điểm Sự Kiện để gen ra + - Ví dụ: Số lượng sinh viên thức đêm ở từ 21-23 giờ thường là lớn; Media báo đài thì hay đăng tin liên tục cả ngày theo ca; Tài khoản văn phòng của các Cơ Quan chức năng chỉ trả lời giờ làm việc Hành Chính... + - Hoặc ví dụ: Các Biến Cố hoặc Drama xảy ra trong đêm khuya thì sẽ dẫn đến Lượng truy cập ban đêm có dấu hiệu đi lên, trong khi đó Off_peak_hours vì lẽ đó mà sẽ có khi co lại cho ngắn... -### 返回JSON格式(不要markdown) +### Định dạng Format của JSON Return (Lưu ý Tuyệt đối Không Return Markdown code block, chỉ Return Format Json Thuần Túy) -示例: +Ví dụ Format như sau: {{ "total_simulation_hours": 72, "minutes_per_round": 60, @@ -570,70 +569,70 @@ def _generate_time_config(self, context: str, num_entities: int) -> Dict[str, An "off_peak_hours": [0, 1, 2, 3, 4, 5], "morning_hours": [6, 7, 8], "work_hours": [9, 10, 11, 12, 13, 14, 15, 16, 17, 18], - "reasoning": "针对该事件的时间配置说明" + "reasoning": "Một đoạn văn lời nói cho biết Bạn đã dựa theo yêu cầu như thế nào để gen các Thông Số trên" }} -字段说明: -- total_simulation_hours (int): 模拟总时长,24-168小时,突发事件短、持续话题长 -- minutes_per_round (int): 每轮时长,30-120分钟,建议60分钟 -- agents_per_hour_min (int): 每小时最少激活Agent数(取值范围: 1-{max_agents_allowed}) -- agents_per_hour_max (int): 每小时最多激活Agent数(取值范围: 1-{max_agents_allowed}) -- peak_hours (int数组): 高峰时段,根据事件参与群体调整 -- off_peak_hours (int数组): 低谷时段,通常深夜凌晨 -- morning_hours (int数组): 早间时段 -- work_hours (int数组): 工作时段 -- reasoning (string): 简要说明为什么这样配置""" +Các Khóa của Json có nghĩa là: +- total_simulation_hours (int): Mô tả tổng giới hạn thời gian (Đơn vị giờ), Có giá trị trong khung 24-168h, Tùy vào biến cố Drama nóng để chọn. Các chủ đề chạy Drama ít hơn thì nên cấp ngắn +- minutes_per_round (int): Số Time trên mỗi Khung Đợt Thời Gian Thực Của Simulation để mô phỏng cho 1 phiên trong game, lấy giá trị 30-120 phút. Đề xuất: 60 (1 giờ) +- agents_per_hour_min (int): Số Agent online tối thiểu trong một tiếng mô phỏng (Phạm vi {1}-{max_agents_allowed}) +- agents_per_hour_max (int): Số lượng lên mạng tối đa (Phạm vi {1}-{max_agents_allowed}) +- peak_hours (mảng int list): Thời điểm đỉnh sóng Cao Điểm, cân nhắc theo Đối tượng để quyết định +- off_peak_hours (mảng int list): Đỉnh sóng Đáy, ít ai quan tâm +- morning_hours (mảng int list): Khoảng thời điểm đầu buổi sáng +- work_hours (mảng int list): Khung hành chính công việc +- reasoning (string): Sự giải thích từ LLM""" - system_prompt = "你是社交媒体模拟专家。返回纯JSON格式,时间配置需符合中国人作息习惯。" + system_prompt = "Bạn là 1 Tool chuyên mô phỏng môi trường làm việc trên mxh bằng thuật toán LLM để cung cấp ra Cấu hình Time. Hãy xuất JSON." try: return self._call_llm_with_retry(prompt, system_prompt) except Exception as e: - logger.warning(f"时间配置LLM生成失败: {e}, 使用默认配置") + logger.warning(f"Failed to generate Time Config through LLM {e}. Returning the basic default rules...") return self._get_default_time_config(num_entities) def _get_default_time_config(self, num_entities: int) -> Dict[str, Any]: - """获取默认时间配置(中国人作息)""" + """Tạo sẵn file chuẩn nếu bị đơ để trả ra theo múi giờ chuẩn sinh hoạt China""" return { "total_simulation_hours": 72, - "minutes_per_round": 60, # 每轮1小时,加快时间流速 + "minutes_per_round": 60, # 1 Hour / Vòng -> Rút gắn Time "agents_per_hour_min": max(1, num_entities // 15), "agents_per_hour_max": max(5, num_entities // 5), "peak_hours": [19, 20, 21, 22], "off_peak_hours": [0, 1, 2, 3, 4, 5], "morning_hours": [6, 7, 8], "work_hours": [9, 10, 11, 12, 13, 14, 15, 16, 17, 18], - "reasoning": "使用默认中国人作息配置(每轮1小时)" + "reasoning": "Mặc định sử dụng Thời gian làm việc của người dùng Trung Quốc (1 Giờ/vòng)" } def _parse_time_config(self, result: Dict[str, Any], num_entities: int) -> TimeSimulationConfig: - """解析时间配置结果,并验证agents_per_hour值不超过总agent数""" - # 获取原始值 + """Phân tích nội dung được định hình của JSON qua hàm parse kiểm tra, Xác nhận nếu lượng agents_per_hour vượt ngưỡng giới hạn """ + # Lấy giá trị chưa chỉnh sửa agents_per_hour_min = result.get("agents_per_hour_min", max(1, num_entities // 15)) agents_per_hour_max = result.get("agents_per_hour_max", max(5, num_entities // 5)) - # 验证并修正:确保不超过总agent数 + # Tiến hành kiểm tra xác minh: Đảm bảo độ lớn không lớn hơn con số Total Agent if agents_per_hour_min > num_entities: - logger.warning(f"agents_per_hour_min ({agents_per_hour_min}) 超过总Agent数 ({num_entities}),已修正") + logger.warning(f"agents_per_hour_min ({agents_per_hour_min}) exceeds total number of Agents ({num_entities}), corrected.") agents_per_hour_min = max(1, num_entities // 10) if agents_per_hour_max > num_entities: - logger.warning(f"agents_per_hour_max ({agents_per_hour_max}) 超过总Agent数 ({num_entities}),已修正") + logger.warning(f"agents_per_hour_max ({agents_per_hour_max}) exceeds total number of Agents ({num_entities}), corrected.") agents_per_hour_max = max(agents_per_hour_min + 1, num_entities // 2) - # 确保 min < max + # Đảm bảo min luôn luôn nhỏ hơn max if agents_per_hour_min >= agents_per_hour_max: agents_per_hour_min = max(1, agents_per_hour_max // 2) - logger.warning(f"agents_per_hour_min >= max,已修正为 {agents_per_hour_min}") + logger.warning(f"agents_per_hour_min >= max, modified to {agents_per_hour_min}") return TimeSimulationConfig( total_simulation_hours=result.get("total_simulation_hours", 72), - minutes_per_round=result.get("minutes_per_round", 60), # 默认每轮1小时 + minutes_per_round=result.get("minutes_per_round", 60), # Mặc định mỗi vòng = 1 giờ agents_per_hour_min=agents_per_hour_min, agents_per_hour_max=agents_per_hour_max, peak_hours=result.get("peak_hours", [19, 20, 21, 22]), off_peak_hours=result.get("off_peak_hours", [0, 1, 2, 3, 4, 5]), - off_peak_activity_multiplier=0.05, # 凌晨几乎无人 + off_peak_activity_multiplier=0.05, # Gần như 0 mạng sáng rạng sáng morning_hours=result.get("morning_hours", [6, 7, 8]), morning_activity_multiplier=0.4, work_hours=result.get("work_hours", list(range(9, 19))), @@ -647,14 +646,14 @@ def _generate_event_config( simulation_requirement: str, entities: List[EntityNode] ) -> Dict[str, Any]: - """生成事件配置""" + """Tạo ra cho các thông số Event config""" - # 获取可用的实体类型列表,供 LLM 参考 + # Tự liệt kê các Loại có thể xuất hiện để LLM tham khảo entity_types_available = list(set( e.get_entity_type() or "Unknown" for e in entities )) - # 为每种类型列出代表性实体名称 + # Ghi các Thực thể điển hình của mổi loại type_examples = {} for e in entities: etype = e.get_entity_type() or "Unknown" @@ -668,53 +667,53 @@ def _generate_event_config( for t, examples in type_examples.items() ]) - # 使用配置的上下文截断长度 + # Có chặn để lấy chuỗi theo cấu hình chiều dài giới hạn context_truncated = context[:self.EVENT_CONFIG_CONTEXT_LENGTH] - prompt = f"""基于以下模拟需求,生成事件配置。 + prompt = f"""Gen cấu hình Event dưới các tham chiếu từ Yêu cầu (Requirements): -模拟需求: {simulation_requirement} +Simulation Requirements: {simulation_requirement} {context_truncated} -## 可用实体类型及示例 +## Các Entity Type có cung cấp & VD minh họa: {type_info} -## 任务 -请生成事件配置JSON: -- 提取热点话题关键词 -- 描述舆论发展方向 -- 设计初始帖子内容,**每个帖子必须指定 poster_type(发布者类型)** +## Task công việc +Vui lòng xuất ra kết quả Thời gian dưới định dạng format JSON: +- Chỉ định List các Hot Keyword để kéo trend +- Miêu tả định hướng thảo luận cho trend hiện tại +- Đăng tải Post đầu tiên (Initial_Post) lên với nguyên tắc: **Phải đi kèm với tham số người up Post (poster_type)** -**重要**: poster_type 必须从上面的"可用实体类型"中选择,这样初始帖子才能分配给合适的 Agent 发布。 -例如:官方声明应由 Official/University 类型发布,新闻由 MediaOutlet 发布,学生观点由 Student 发布。 +**RẤT QUAN TRỌNG**: Người Poster Type (poster_type) Phải trùng khớp/được lấy từ danh mục từ mục "Các Entity Type" đã cho để gán. Tránh báo lỗi cho Agent + Ví dụ: official announcements should be posted by Official/University type, news by MediaOutlet, and student opinions by Student. -返回JSON格式(不要markdown): +Format trả ra (Tuyệt đối Không Markdown, chỉ lấy format chuỗi chuẩn): {{ - "hot_topics": ["关键词1", "关键词2", ...], - "narrative_direction": "<舆论发展方向描述>", + "hot_topics": ["Keyword1", "Keyword2", ...], + "narrative_direction": "<Đoạn Text dài định hướng Dư luận (Narrative)>", "initial_posts": [ - {{"content": "帖子内容", "poster_type": "实体类型(必须从可用类型中选择)"}}, + {{"content": "Post Content...", "poster_type": "Người sẽ Post ra nội dung (Hạn chế tùy tiện vì nó lấy từ mảng danh sách Loại Entity cho trước)"}}, ... ], - "reasoning": "<简要说明>" + "reasoning": "" }}""" - system_prompt = "你是舆论分析专家。返回纯JSON格式。注意 poster_type 必须精确匹配可用实体类型。" + system_prompt = "Bạn là 1 Chuyên gia về Data Dư luận, Yêu cầu làm việc trên chuỗi JSON nghiêm ngặt. Tránh Lỗi." try: return self._call_llm_with_retry(prompt, system_prompt) except Exception as e: - logger.warning(f"事件配置LLM生成失败: {e}, 使用默认配置") + logger.warning(f"Failed to load LLM Event configurations: {e}, using default configs instead.") return { "hot_topics": [], "narrative_direction": "", "initial_posts": [], - "reasoning": "使用默认配置" + "reasoning": "Sử dụng Config mặc định do LLM lỗi" } def _parse_event_config(self, result: Dict[str, Any]) -> EventConfig: - """解析事件配置结果""" + """Parse lấy các Thuộc Tính cấu hình Event""" return EventConfig( initial_posts=result.get("initial_posts", []), scheduled_events=[], @@ -728,14 +727,14 @@ def _assign_initial_post_agents( agent_configs: List[AgentActivityConfig] ) -> EventConfig: """ - 为初始帖子分配合适的发布者 Agent + Khớp quyền Agent với loại Poster_type cho các Bài Post đầu - 根据每个帖子的 poster_type 匹配最合适的 agent_id + So sánh cho phù hợp của mỗi post để phân bố Agent id tối ưu nhất """ if not event_config.initial_posts: return event_config - # 按实体类型建立 agent 索引 + # Build hệ thống agent index bằng kiểu loại agents_by_type: Dict[str, List[AgentActivityConfig]] = {} for agent in agent_configs: etype = agent.entity_type.lower() @@ -743,7 +742,7 @@ def _assign_initial_post_agents( agents_by_type[etype] = [] agents_by_type[etype].append(agent) - # 类型映射表(处理 LLM 可能输出的不同格式) + # Bảng Alias ánh xạ tương đương (Cho phép LLM sử dụng nhiều quy ước format khác nhau) type_aliases = { "official": ["official", "university", "governmentagency", "government"], "university": ["university", "official"], @@ -755,7 +754,7 @@ def _assign_initial_post_agents( "person": ["person", "student", "alumni"], } - # 记录每种类型已使用的 agent 索引,避免重复使用同一个 agent + # Ghi chú từng loại agent đã dùng index nào, tránh dùng lại cùng 1 agent lặp đi lặp lại used_indices: Dict[str, int] = {} updated_posts = [] @@ -763,17 +762,17 @@ def _assign_initial_post_agents( poster_type = post.get("poster_type", "").lower() content = post.get("content", "") - # 尝试找到匹配的 agent + # Khớp tìm agent phù hợp matched_agent_id = None - # 1. 直接匹配 + # 1. Trùng khớp trực tiếp lấy luôn if poster_type in agents_by_type: agents = agents_by_type[poster_type] idx = used_indices.get(poster_type, 0) % len(agents) matched_agent_id = agents[idx].agent_id used_indices[poster_type] = idx + 1 else: - # 2. 使用别名匹配 + # 2. Sử dụng bí danh alias để khớp nếu dùng sai keyword for alias_key, aliases in type_aliases.items(): if poster_type in aliases or alias_key == poster_type: for alias in aliases: @@ -786,11 +785,11 @@ def _assign_initial_post_agents( if matched_agent_id is not None: break - # 3. 如果仍未找到,使用影响力最高的 agent + # 3. Nếu xui xẻo vẫn không tìm thấy, lấy thẳng Agent có điểm Influence (Sức ảnh hưởng) cao nhất if matched_agent_id is None: - logger.warning(f"未找到类型 '{poster_type}' 的匹配 Agent,使用影响力最高的 Agent") + logger.warning(f"Could not find matching Agent type '{poster_type}', assigning to highest influence Agent instead") if agent_configs: - # 按影响力排序,选择影响力最高的 + # Sort ảnh hưởng giảm dần, lấy index [0] sorted_agents = sorted(agent_configs, key=lambda a: a.influence_weight, reverse=True) matched_agent_id = sorted_agents[0].agent_id else: @@ -802,7 +801,7 @@ def _assign_initial_post_agents( "poster_agent_id": matched_agent_id }) - logger.info(f"初始帖子分配: poster_type='{poster_type}' -> agent_id={matched_agent_id}") + logger.info(f"Initial post assignment: poster_type='{poster_type}' -> agent_id={matched_agent_id}") event_config.initial_posts = updated_posts return event_config @@ -814,9 +813,9 @@ def _generate_agent_configs_batch( start_idx: int, simulation_requirement: str ) -> List[AgentActivityConfig]: - """分批生成Agent配置""" + """Chia đợt gửi lên gọi tạo Cấu hình mạng lưới Agents""" - # 构建实体信息(使用配置的摘要长度) + # Build các node Entity (Dựa trên cấu hình lượng chữ giới hạn) entity_list = [] summary_len = self.AGENT_SUMMARY_LENGTH for i, e in enumerate(entities): @@ -827,58 +826,58 @@ def _generate_agent_configs_batch( "summary": e.summary[:summary_len] if e.summary else "" }) - prompt = f"""基于以下信息,为每个实体生成社交媒体活动配置。 + prompt = f"""Tạo profile Social Media Activity Configs cho từng Thực thể sau. -模拟需求: {simulation_requirement} +Nhu cầu: {simulation_requirement} -## 实体列表 +## List các thực thể ```json {json.dumps(entity_list, ensure_ascii=False, indent=2)} ``` -## 任务 -为每个实体生成活动配置,注意: -- **时间符合中国人作息**:凌晨0-5点几乎不活动,晚间19-22点最活跃 -- **官方机构**(University/GovernmentAgency):活跃度低(0.1-0.3),工作时间(9-17)活动,响应慢(60-240分钟),影响力高(2.5-3.0) -- **媒体**(MediaOutlet):活跃度中(0.4-0.6),全天活动(8-23),响应快(5-30分钟),影响力高(2.0-2.5) -- **个人**(Student/Person/Alumni):活跃度高(0.6-0.9),主要晚间活动(18-23),响应快(1-15分钟),影响力低(0.8-1.2) -- **公众人物/专家**:活跃度中(0.4-0.6),影响力中高(1.5-2.0) +## Task Công Việc +Trả ra cho Từng Entity các bộ Activity Profile tham chiều theo các quy tắc ngầm sau: +- **Tập quán Sinh hoạt Trung Quốc**: 0-5h sáng gần như sẽ hiếm ai onl, 19-22h tối lượng tương tác rất sôi nổi +- **Đại diện Cơ quan (University/GovernmentAgency)**: Tần suất (0.1-0.3), làm việc trong giờ hành chính (9-17h), delay hơi trễ (60-240 phút), Trọng lượng lời nói cao (2.5-3.0) +- **Truyền Thông Báo Đài (MediaOutlet)**: Tần suất TB (0.4-0.6), Hầu như online nguyên ngày (8-23h), Trễ ít (5-30 phút), Trọng lượng cũng Cao (2.0-2.5) +- **Người Dùng Bình thường (Student/Person/Alumni)**: Tần suất cao (0.6-0.9), Onl chủ yếu để cãi nhau buổi tối (18-23h), Tương tác lẹ như hack (1-15 min), Uy tín lời nói khá lèo tèo (0.8-1.2) +- **Học giả/Chuyên gia/Kols**: Tần suất TB (0.4-0.6), Uy tín tương đối (1.5-2.0) -返回JSON格式(不要markdown): +Trả đúng 1 object JSON ko format MD: {{ "agent_configs": [ {{ - "agent_id": <必须与输入一致>, + "agent_id": , "activity_level": <0.0-1.0>, - "posts_per_hour": <发帖频率>, - "comments_per_hour": <评论频率>, - "active_hours": [<活跃小时列表,考虑中国人作息>], - "response_delay_min": <最小响应延迟分钟>, - "response_delay_max": <最大响应延迟分钟>, - "sentiment_bias": <-1.0到1.0>, + "posts_per_hour": , + "comments_per_hour": , + "active_hours": [], + "response_delay_min": , + "response_delay_max": , + "sentiment_bias": <-1.0 đến 1.0 (Tiêu cực sang Tích Cực)>, "stance": "", - "influence_weight": <影响力权重> + "influence_weight": }}, ... ] }}""" - system_prompt = "你是社交媒体行为分析专家。返回纯JSON,配置需符合中国人作息习惯。" + system_prompt = "Hệ thống Analysis Chuyên gia. Luôn trả về Format Object Array bằng JSON. Và tuân thủ sinh học." try: result = self._call_llm_with_retry(prompt, system_prompt) llm_configs = {cfg["agent_id"]: cfg for cfg in result.get("agent_configs", [])} except Exception as e: - logger.warning(f"Agent配置批次LLM生成失败: {e}, 使用规则生成") + logger.warning(f"Failed LLM generating Agent batch configs: {e}, falling back to default manual rules.") llm_configs = {} - # 构建AgentActivityConfig对象 + # Tạo object list cho AgentActivityConfig configs = [] for i, entity in enumerate(entities): agent_id = start_idx + i cfg = llm_configs.get(agent_id, {}) - # 如果LLM没有生成,使用规则生成 + # Gán Manual tự động nếu Bot LLM thiếu xót if not cfg: cfg = self._generate_agent_config_by_rule(entity) @@ -902,11 +901,11 @@ def _generate_agent_configs_batch( return configs def _generate_agent_config_by_rule(self, entity: EntityNode) -> Dict[str, Any]: - """基于规则生成单个Agent配置(中国人作息)""" + """Tự động gen cấu hình 1 người (agent) dựa trên bộ rule cứng có sẵn nếu gọi bot LLM bị fail (Luật theo múi giờ sinh học)""" entity_type = (entity.get_entity_type() or "Unknown").lower() if entity_type in ["university", "governmentagency", "ngo"]: - # 官方机构:工作时间活动,低频率,高影响力 + # Cơ quan chức năng Nhà nước / Doanh nghiệp: làm việc trong khung giờ chuẩn hành chính, trả lời ít nhưng nặng đô return { "activity_level": 0.2, "posts_per_hour": 0.1, @@ -919,7 +918,7 @@ def _generate_agent_config_by_rule(self, entity: EntityNode) -> Dict[str, Any]: "influence_weight": 3.0 } elif entity_type in ["mediaoutlet"]: - # 媒体:全天活动,中等频率,高影响力 + # Báo đài truyền thông: cả ngày đưa tin, ra bài lẹ giật tít, tốc độ cao return { "activity_level": 0.5, "posts_per_hour": 0.8, @@ -932,7 +931,7 @@ def _generate_agent_config_by_rule(self, entity: EntityNode) -> Dict[str, Any]: "influence_weight": 2.5 } elif entity_type in ["professor", "expert", "official"]: - # 专家/教授:工作+晚间活动,中等频率 + # Giáo sư đại học/Người phát biểu: Chỉ nói ban ngày và tối, ra bài ít return { "activity_level": 0.4, "posts_per_hour": 0.3, @@ -945,12 +944,12 @@ def _generate_agent_config_by_rule(self, entity: EntityNode) -> Dict[str, Any]: "influence_weight": 2.0 } elif entity_type in ["student"]: - # 学生:晚间为主,高频率 + # Tần suất cho lứa Sinh viên: hay ra bài / cãi nhau liên tục ban đêm rất nhiều return { "activity_level": 0.8, "posts_per_hour": 0.6, "comments_per_hour": 1.5, - "active_hours": [8, 9, 10, 11, 12, 13, 18, 19, 20, 21, 22, 23], # 上午+晚间 + "active_hours": [8, 9, 10, 11, 12, 13, 18, 19, 20, 21, 22, 23], # Sáng + Đêm Tối "response_delay_min": 1, "response_delay_max": 15, "sentiment_bias": 0.0, @@ -958,12 +957,12 @@ def _generate_agent_config_by_rule(self, entity: EntityNode) -> Dict[str, Any]: "influence_weight": 0.8 } elif entity_type in ["alumni"]: - # 校友:晚间为主 + # Cựu sinh viên: Thường online đêm là chính return { "activity_level": 0.6, "posts_per_hour": 0.4, "comments_per_hour": 0.8, - "active_hours": [12, 13, 19, 20, 21, 22, 23], # 午休+晚间 + "active_hours": [12, 13, 19, 20, 21, 22, 23], # Giờ nghỉ trưa + Buổi tối "response_delay_min": 5, "response_delay_max": 30, "sentiment_bias": 0.0, @@ -971,12 +970,12 @@ def _generate_agent_config_by_rule(self, entity: EntityNode) -> Dict[str, Any]: "influence_weight": 1.0 } else: - # 普通人:晚间高峰 + # Thuộc cho số đông (Cư dân mạng / Người Qua Đường): Phấn khích về đêm return { "activity_level": 0.7, "posts_per_hour": 0.5, "comments_per_hour": 1.2, - "active_hours": [9, 10, 11, 12, 13, 18, 19, 20, 21, 22, 23], # 白天+晚间 + "active_hours": [9, 10, 11, 12, 13, 18, 19, 20, 21, 22, 23], # Ban Ngày rảnh + Buổi tối rảnh "response_delay_min": 2, "response_delay_max": 20, "sentiment_bias": 0.0, diff --git a/backend/app/services/simulation_ipc.py b/backend/app/services/simulation_ipc.py index 9d70d0bea..afddff042 100644 --- a/backend/app/services/simulation_ipc.py +++ b/backend/app/services/simulation_ipc.py @@ -1,11 +1,11 @@ """ -模拟IPC通信模块 -用于Flask后端和模拟脚本之间的进程间通信 +Module Giao tiếp IPC của Mô phỏng +Dùng cho giao tiếp liên tiến trình giữa backend Flask và file script mô phỏng -通过文件系统实现简单的命令/响应模式: -1. Flask写入命令到 commands/ 目录 -2. 模拟脚本轮询命令目录,执行命令并写入响应到 responses/ 目录 -3. Flask轮询响应目录获取结果 +Cấu trúc lệnh/phản hồi đơn giản được hiện thực hóa thông qua hệ thống tệp: +1. Flask ghi lệnh vào thư mục commands/ +2. Kịch bản mô phỏng thăm dò (poll) thư mục lệnh, thực thi lệnh và ghi chuỗi phản hồi vào thư mục responses/ +3. Flask thăm dò lại thư mục phản hồi để nhận kết quả """ import os @@ -23,14 +23,14 @@ class CommandType(str, Enum): - """命令类型""" - INTERVIEW = "interview" # 单个Agent采访 - BATCH_INTERVIEW = "batch_interview" # 批量采访 - CLOSE_ENV = "close_env" # 关闭环境 + """Các loại lệnh (command)""" + INTERVIEW = "interview" # Phỏng vấn agent đơn lẻ + BATCH_INTERVIEW = "batch_interview" # Phỏng vấn hàng loạt + CLOSE_ENV = "close_env" # Đóng môi trường class CommandStatus(str, Enum): - """命令状态""" + """Trạng thái của các lệnh (command)""" PENDING = "pending" PROCESSING = "processing" COMPLETED = "completed" @@ -39,7 +39,7 @@ class CommandStatus(str, Enum): @dataclass class IPCCommand: - """IPC命令""" + """Lệnh (Command) IPC""" command_id: str command_type: CommandType args: Dict[str, Any] @@ -65,7 +65,7 @@ def from_dict(cls, data: Dict[str, Any]) -> 'IPCCommand': @dataclass class IPCResponse: - """IPC响应""" + """Phản hồi (Response) IPC""" command_id: str status: CommandStatus result: Optional[Dict[str, Any]] = None @@ -94,23 +94,23 @@ def from_dict(cls, data: Dict[str, Any]) -> 'IPCResponse': class SimulationIPCClient: """ - 模拟IPC客户端(Flask端使用) + Client (Máy khách) IPC Mô phỏng (dùng phía Flask) - 用于向模拟进程发送命令并等待响应 + Được dùng để gửi file tới tiến trình mô phỏng và chờ response trả về """ def __init__(self, simulation_dir: str): """ - 初始化IPC客户端 + Khởi tạo Client IPC Args: - simulation_dir: 模拟数据目录 + simulation_dir: Thư mục chứa dữ liệu mô phỏng """ self.simulation_dir = simulation_dir self.commands_dir = os.path.join(simulation_dir, "ipc_commands") self.responses_dir = os.path.join(simulation_dir, "ipc_responses") - # 确保目录存在 + # Đảm bảo rằng thư mục đã tồn tại os.makedirs(self.commands_dir, exist_ok=True) os.makedirs(self.responses_dir, exist_ok=True) @@ -122,19 +122,19 @@ def send_command( poll_interval: float = 0.5 ) -> IPCResponse: """ - 发送命令并等待响应 + Gửi lệnh ra và đợi kết quả phản hồi lại Args: - command_type: 命令类型 - args: 命令参数 - timeout: 超时时间(秒) - poll_interval: 轮询间隔(秒) + command_type: Loại lệnh + args: Tham số của lệnh + timeout: Thời gian timeout (giây) + poll_interval: Khoảng thời gian giữa các lần thăm dò (giây) Returns: IPCResponse Raises: - TimeoutError: 等待响应超时 + TimeoutError: Lỗi quá thời gian chờ phản hồi """ command_id = str(uuid.uuid4()) command = IPCCommand( @@ -143,14 +143,14 @@ def send_command( args=args ) - # 写入命令文件 + # Ghi vào file lệnh command_file = os.path.join(self.commands_dir, f"{command_id}.json") with open(command_file, 'w', encoding='utf-8') as f: json.dump(command.to_dict(), f, ensure_ascii=False, indent=2) - logger.info(f"发送IPC命令: {command_type.value}, command_id={command_id}") + logger.info(f"Send IPC command: {command_type.value}, command_id={command_id}") - # 等待响应 + # Chờ kết quả phản hồi response_file = os.path.join(self.responses_dir, f"{command_id}.json") start_time = time.time() @@ -161,30 +161,30 @@ def send_command( response_data = json.load(f) response = IPCResponse.from_dict(response_data) - # 清理命令和响应文件 + # Xóa file lệnh và file phản hồi đi try: os.remove(command_file) os.remove(response_file) except OSError: pass - logger.info(f"收到IPC响应: command_id={command_id}, status={response.status.value}") + logger.info(f"Received IPC response: command_id={command_id}, status={response.status.value}") return response except (json.JSONDecodeError, KeyError) as e: - logger.warning(f"解析响应失败: {e}") + logger.warning(f"Failed to parse response: {e}") time.sleep(poll_interval) - # 超时 - logger.error(f"等待IPC响应超时: command_id={command_id}") + # Timed out + logger.error(f"Timeout waiting for IPC response: command_id={command_id}") - # 清理命令文件 + # Xóa file lệnh đi try: os.remove(command_file) except OSError: pass - raise TimeoutError(f"等待命令响应超时 ({timeout}秒)") + raise TimeoutError(f"Wait command response timed out ({timeout} seconds)") def send_interview( self, @@ -194,19 +194,19 @@ def send_interview( timeout: float = 60.0 ) -> IPCResponse: """ - 发送单个Agent采访命令 + Gửi lệnh phỏng vấn agent đơn lẻ Args: agent_id: Agent ID - prompt: 采访问题 - platform: 指定平台(可选) - - "twitter": 只采访Twitter平台 - - "reddit": 只采访Reddit平台 - - None: 双平台模拟时同时采访两个平台,单平台模拟时采访该平台 - timeout: 超时时间 + prompt: Câu hỏi phỏng vấn + platform: Chỉ định nền tảng (Tùy chọn) + - "twitter": Chỉ phỏng vấn ở nền tảng Twitter + - "reddit": Chỉ phỏng vấn ở nền tảng Reddit + - None: Phỏng vấn đồng thời cả hai nền tảng khi mô phỏng nền tảng kép, phỏng vấn một nền tảng đó khi mô phỏng nền tảng đơn + timeout: Thời gian timeout Returns: - IPCResponse,result字段包含采访结果 + IPCResponse, trong đó trường result sẽ chứa kết quả cuộc phỏng vấn """ args = { "agent_id": agent_id, @@ -228,18 +228,18 @@ def send_batch_interview( timeout: float = 120.0 ) -> IPCResponse: """ - 发送批量采访命令 + Gửi lệnh phỏng vấn hàng loạt Args: - interviews: 采访列表,每个元素包含 {"agent_id": int, "prompt": str, "platform": str(可选)} - platform: 默认平台(可选,会被每个采访项的platform覆盖) - - "twitter": 默认只采访Twitter平台 - - "reddit": 默认只采访Reddit平台 - - None: 双平台模拟时每个Agent同时采访两个平台 - timeout: 超时时间 + interviews: Danh sách phỏng vấn, mỗi phần tử chứa {"agent_id": int, "prompt": str, "platform": str(Tùy chọn)} + platform: Nền tảng mặc định (Tùy chọn, sẽ bị ghi đè bởi "platform" của từng mục phỏng vấn riêng lẻ) + - "twitter": Mặc định chỉ phỏng vấn ở nền tảng Twitter + - "reddit": Mặc định chỉ phỏng vấn ở nền tảng Reddit + - None: Mỗi Agent sẽ được phỏng vấn đồng thời trên cả hai nền tảng khi mô phỏng nền tảng kép + timeout: Thời gian timeout Returns: - IPCResponse,result字段包含所有采访结果 + IPCResponse, trong đó trường result sẽ chứa tất cả các kết quả phỏng vấn """ args = {"interviews": interviews} if platform: @@ -253,10 +253,10 @@ def send_batch_interview( def send_close_env(self, timeout: float = 30.0) -> IPCResponse: """ - 发送关闭环境命令 + Gửi lệnh đóng môi trường Args: - timeout: 超时时间 + timeout: Thời gian timeout Returns: IPCResponse @@ -269,9 +269,9 @@ def send_close_env(self, timeout: float = 30.0) -> IPCResponse: def check_env_alive(self) -> bool: """ - 检查模拟环境是否存活 + Kiểm tra xem môi trường mô phỏng còn sống hay không - 通过检查 env_status.json 文件来判断 + Được xác định thông qua việc kiểm tra tệp tin env_status.json """ status_file = os.path.join(self.simulation_dir, "env_status.json") if not os.path.exists(status_file): @@ -287,41 +287,41 @@ def check_env_alive(self) -> bool: class SimulationIPCServer: """ - 模拟IPC服务器(模拟脚本端使用) + Server (Máy chủ) IPC Mô phỏng (dùng phía kịch bản mô phỏng) - 轮询命令目录,执行命令并返回响应 + Tiến hành thăm dò thư mục lệnh, thực thi lệnh và trả về kết quả phản hồi """ def __init__(self, simulation_dir: str): """ - 初始化IPC服务器 + Khởi tạo Máy chủ IPC Args: - simulation_dir: 模拟数据目录 + simulation_dir: Thư mục chứa dữ liệu mô phỏng """ self.simulation_dir = simulation_dir self.commands_dir = os.path.join(simulation_dir, "ipc_commands") self.responses_dir = os.path.join(simulation_dir, "ipc_responses") - # 确保目录存在 + # Đảm bảo rằng thư mục đã tồn tại os.makedirs(self.commands_dir, exist_ok=True) os.makedirs(self.responses_dir, exist_ok=True) - # 环境状态 + # Trạng thái môi trường self._running = False def start(self): - """标记服务器为运行状态""" + """Đánh dấu Máy chủ đang ở trạng thái chạy""" self._running = True self._update_env_status("alive") def stop(self): - """标记服务器为停止状态""" + """Đánh dấu Máy chủ đang ở trạng thái dừng""" self._running = False self._update_env_status("stopped") def _update_env_status(self, status: str): - """更新环境状态文件""" + """Cập nhật tệp trạng thái môi trường""" status_file = os.path.join(self.simulation_dir, "env_status.json") with open(status_file, 'w', encoding='utf-8') as f: json.dump({ @@ -331,15 +331,15 @@ def _update_env_status(self, status: str): def poll_commands(self) -> Optional[IPCCommand]: """ - 轮询命令目录,返回第一个待处理的命令 + Thăm dò thư mục lệnh, trả về lệnh chờ xử lý đầu tiên Returns: - IPCCommand 或 None + IPCCommand hoặc None """ if not os.path.exists(self.commands_dir): return None - # 按时间排序获取命令文件 + # Lấy danh sách file lệnh và sắp xếp theo thời gian command_files = [] for filename in os.listdir(self.commands_dir): if filename.endswith('.json'): @@ -354,23 +354,23 @@ def poll_commands(self) -> Optional[IPCCommand]: data = json.load(f) return IPCCommand.from_dict(data) except (json.JSONDecodeError, KeyError, OSError) as e: - logger.warning(f"读取命令文件失败: {filepath}, {e}") + logger.warning(f"Failed to read command file: {filepath}, {e}") continue return None def send_response(self, response: IPCResponse): """ - 发送响应 + Gửi phản hồi Args: - response: IPC响应 + response: Phản hồi IPC """ response_file = os.path.join(self.responses_dir, f"{response.command_id}.json") with open(response_file, 'w', encoding='utf-8') as f: json.dump(response.to_dict(), f, ensure_ascii=False, indent=2) - # 删除命令文件 + # Xóa file lệnh đi command_file = os.path.join(self.commands_dir, f"{response.command_id}.json") try: os.remove(command_file) @@ -378,7 +378,7 @@ def send_response(self, response: IPCResponse): pass def send_success(self, command_id: str, result: Dict[str, Any]): - """发送成功响应""" + """Gửi phản hồi thành công""" self.send_response(IPCResponse( command_id=command_id, status=CommandStatus.COMPLETED, @@ -386,7 +386,7 @@ def send_success(self, command_id: str, result: Dict[str, Any]): )) def send_error(self, command_id: str, error: str): - """发送错误响应""" + """Gửi phản hồi lỗi""" self.send_response(IPCResponse( command_id=command_id, status=CommandStatus.FAILED, diff --git a/backend/app/services/simulation_manager.py b/backend/app/services/simulation_manager.py index 96c496fd4..d9d7b0222 100644 --- a/backend/app/services/simulation_manager.py +++ b/backend/app/services/simulation_manager.py @@ -1,7 +1,7 @@ """ -OASIS模拟管理器 -管理Twitter和Reddit双平台并行模拟 -使用预设脚本 + LLM智能生成配置参数 +Trình Quản lý Mô Phỏng OASIS +Đảm nhiệm xây dựng và điều phối chạy mô phỏng song song trên hai nền tảng giả lập Twitter và Reddit. +Sử dụng các kịch bản có sẵn kết hợp cùng LLM để thiết lập thông minh bộ tham số mô phỏng. """ import os @@ -22,60 +22,60 @@ class SimulationStatus(str, Enum): - """模拟状态""" - CREATED = "created" - PREPARING = "preparing" - READY = "ready" - RUNNING = "running" - PAUSED = "paused" - STOPPED = "stopped" # 模拟被手动停止 - COMPLETED = "completed" # 模拟自然完成 - FAILED = "failed" + """Trạng thái hiện tại của quá trình mô phỏng""" + CREATED = "created" # Đã khởi tạo + PREPARING = "preparing" # Đang chuẩn bị (chuẩn bị dữ liệu/profile) + READY = "ready" # Đã sẵn sàng chạy + RUNNING = "running" # Đang xử lý giả lập + PAUSED = "paused" # Tạm dừng + STOPPED = "stopped" # Hệ thống mô phỏng bị người dùng chủ động dừng lại + COMPLETED = "completed" # Quá trình mô phỏng kết thúc tự nhiên một cách thành công + FAILED = "failed" # Bị lỗi hệ thống gián đoạn class PlatformType(str, Enum): - """平台类型""" + """Phân loại nền tảng giả lập Mạng xã hội""" TWITTER = "twitter" REDDIT = "reddit" @dataclass class SimulationState: - """模拟状态""" + """Class lưu trữ cấu trúc Dữ liệu/Trạng thái của một lượt mô phỏng""" simulation_id: str project_id: str graph_id: str - # 平台启用状态 + # Cờ trạng thái bật/tắt nền tảng chạy enable_twitter: bool = True enable_reddit: bool = True - # 状态 + # Current status status: SimulationStatus = SimulationStatus.CREATED - # 准备阶段数据 + # Dữ liệu thu thập / thống kê của Preparing Phase entities_count: int = 0 profiles_count: int = 0 entity_types: List[str] = field(default_factory=list) - # 配置生成信息 + # Thông tin các nội dung cấu hình mà LLM đã tự động tạo config_generated: bool = False config_reasoning: str = "" - # 运行时数据 + # Dữ liệu cập nhật theo thời gian thực (Runtime Phase) current_round: int = 0 twitter_status: str = "not_started" reddit_status: str = "not_started" - # 时间戳 + # Nhãn Timestamp lịch sử created_at: str = field(default_factory=lambda: datetime.now().isoformat()) updated_at: str = field(default_factory=lambda: datetime.now().isoformat()) - # 错误信息 + # Lịch sử thông báo Lỗi (nếu có để render trả về frontend) error: Optional[str] = None def to_dict(self) -> Dict[str, Any]: - """完整状态字典(内部使用)""" + """Tạo thành Dictionary đầy đủ nhất (Dùng cho việc lưu cấu hình local cho hệ thống bên trong đọc)""" return { "simulation_id": self.simulation_id, "project_id": self.project_id, @@ -97,7 +97,7 @@ def to_dict(self) -> Dict[str, Any]: } def to_simple_dict(self) -> Dict[str, Any]: - """简化状态字典(API返回使用)""" + """Tạo thành Dictionary bao hàm các thông số vắn tắt hơn (Dùng cho API response trả về Client - Frontend)""" return { "simulation_id": self.simulation_id, "project_id": self.project_id, @@ -113,36 +113,36 @@ def to_simple_dict(self) -> Dict[str, Any]: class SimulationManager: """ - 模拟管理器 + Kịch bản Quản lý trung tâm của tính năng Mô Phỏng - 核心功能: - 1. 从Zep图谱读取实体并过滤 - 2. 生成OASIS Agent Profile - 3. 使用LLM智能生成模拟配置参数 - 4. 准备预设脚本所需的所有文件 + Luồng thiết lập cốt lõi: + 1. Trích xuất nhóm các Thực Thể (Entity) được định nghĩa sẵn trong hệ thống lưu trữ Graph của Zep. + 2. Chế lại thành các hồ sơ Profile thiết lập tiêu chuẩn của OASIS framework (Agent) + 3. Trao quyền cho sức mạnh mô hình LLM tự đánh giá số liệu rồi tự sinh ra cấu hình cài đặt cho quá trình mô phỏng + 4. Cài đặt các thư mục và tập tin tương ứng, phục vụ sẵn sàng để những Script lập trình riêng (pre-set script) có thể khai thác sử dụng. """ - # 模拟数据存储目录 + # Nơi chứa thư mục chứa Dữ liệu mô phỏng Local SIMULATION_DATA_DIR = os.path.join( os.path.dirname(__file__), '../../uploads/simulations' ) def __init__(self): - # 确保目录存在 + # Đảm bảo môi trường file data đã được set up os.makedirs(self.SIMULATION_DATA_DIR, exist_ok=True) - # 内存中的模拟状态缓存 + # Biến dictionary ở mức Application theo dõi trạng thái simulation qua Cache RAM. self._simulations: Dict[str, SimulationState] = {} def _get_simulation_dir(self, simulation_id: str) -> str: - """获取模拟数据目录""" + """Lấy trả về các đường dẫn thư mục gốc tương ứng với Simulation ID""" sim_dir = os.path.join(self.SIMULATION_DATA_DIR, simulation_id) os.makedirs(sim_dir, exist_ok=True) return sim_dir def _save_simulation_state(self, state: SimulationState): - """保存模拟状态到文件""" + """Bật tính năng lưu state định dạng JSON ra ổ cứng""" sim_dir = self._get_simulation_dir(state.simulation_id) state_file = os.path.join(sim_dir, "state.json") @@ -154,7 +154,7 @@ def _save_simulation_state(self, state: SimulationState): self._simulations[state.simulation_id] = state def _load_simulation_state(self, simulation_id: str) -> Optional[SimulationState]: - """从文件加载模拟状态""" + """Load ngược lại data của tiến trình Mô Phỏng thông qua tệp cấu hình JSON""" if simulation_id in self._simulations: return self._simulations[simulation_id] @@ -198,16 +198,16 @@ def create_simulation( enable_reddit: bool = True, ) -> SimulationState: """ - 创建新的模拟 + Khởi tạo môi trường ảo / mới cho Mô Phỏng Args: - project_id: 项目ID - graph_id: Zep图谱ID - enable_twitter: 是否启用Twitter模拟 - enable_reddit: 是否启用Reddit模拟 + project_id: Mã ID của Project (Quản lý cấp đầu vào) + graph_id: Đồ thị ID tương ứng lấy bên Zep + enable_twitter: Công tắc (Bật/Tắt) luồng giả lập Twitter + enable_reddit: Công tắc (Bật/Tắt) luồng giả lập Reddit Returns: - SimulationState + Đối tượng Class SimulationState """ import uuid simulation_id = f"sim_{uuid.uuid4().hex[:12]}" @@ -222,7 +222,7 @@ def create_simulation( ) self._save_simulation_state(state) - logger.info(f"创建模拟: {simulation_id}, project={project_id}, graph={graph_id}") + logger.info(f"Created simulation: {simulation_id}, project={project_id}, graph={graph_id}") return state @@ -237,30 +237,30 @@ def prepare_simulation( parallel_profile_count: int = 3 ) -> SimulationState: """ - 准备模拟环境(全程自动化) + Giai đoạn chuẩn bị dữ liệu tạo giả lập mô phỏng (Tiến trình Automation 100%) - 步骤: - 1. 从Zep图谱读取并过滤实体 - 2. 为每个实体生成OASIS Agent Profile(可选LLM增强,支持并行) - 3. 使用LLM智能生成模拟配置参数(时间、活跃度、发言频率等) - 4. 保存配置文件和Profile文件 - 5. 复制预设脚本到模拟目录 + Các bước diễn ra: + 1. Gọi lấy các cụm Entity (thực thể) và bộ lọt (filter) từ Zep Graph API + 2. Tự động khởi tạo hàng loạt Agent Profile chạy OASIS tương ứng với Entity (Hỗ trợ gọi AI LLM để làm mượt văn bản / tăng tốc chạy song song) + 3. Hỏi và bắt bot LLM suy luận ra hệ tham số setting thông minh nhất (thời gian mô phỏng rò rỉ, hệ số tần suất nói chuyện hoạt động ...) + 4. In ra các file cấu hình và JSON của profile để hệ thống dễ đọc + 5. Copy nguyên các Scripts chuẩn được cấu hình sẵn (preset) ném vào thư mục để chạy Args: - simulation_id: 模拟ID - simulation_requirement: 模拟需求描述(用于LLM生成配置) - document_text: 原始文档内容(用于LLM理解背景) - defined_entity_types: 预定义的实体类型(可选) - use_llm_for_profiles: 是否使用LLM生成详细人设 - progress_callback: 进度回调函数 (stage, progress, message) - parallel_profile_count: 并行生成人设的数量,默认3 + simulation_id: Mã ID của chu trình giả lập + simulation_requirement: Chuỗi text từ người dùng yêu cầu mô phỏng gì (gửi cho config sinh cấu hình) + document_text: Nội dung file Raw nguyên thủy (Cho LLM đánh giá context bối cảnh ban đầu) + defined_entity_types: Dánh sách các Entity Model có sẵn do Zep định nghĩa (Option) + use_llm_for_profiles: Toggle tính năng sử dụng mô hình LLM để buff thêm chi tiết cài đặt con Bot + progress_callback: Hàm callback update log progress (chuyển về màn hình Frontend view) format (stage, progress, message) + parallel_profile_count: Giới hạn concurrent threading chạy LLM gọi profile (Default là 3 luồng cùng lúc để làm nhanh hơn) Returns: - SimulationState + Class cài đặt Data - SimulationState """ state = self._load_simulation_state(simulation_id) if not state: - raise ValueError(f"模拟不存在: {simulation_id}") + raise ValueError(f"Giả lập với ID: {simulation_id} không tồn tại") try: state.status = SimulationStatus.PREPARING @@ -268,14 +268,14 @@ def prepare_simulation( sim_dir = self._get_simulation_dir(simulation_id) - # ========== 阶段1: 读取并过滤实体 ========== + # ========== Giai đoạn 1: Kết nối lấy Node Data Entity và Sàng lọc ========== if progress_callback: - progress_callback("reading", 0, "正在连接Zep图谱...") + progress_callback("reading", 0, "Connecting to Zep Graph data store...") reader = ZepEntityReader() if progress_callback: - progress_callback("reading", 30, "正在读取节点数据...") + progress_callback("reading", 30, "Extracting Node data from graph...") filtered = reader.filter_defined_entities( graph_id=state.graph_id, @@ -289,29 +289,29 @@ def prepare_simulation( if progress_callback: progress_callback( "reading", 100, - f"完成,共 {filtered.filtered_count} 个实体", + f"Extraction complete, filtered entity count: {filtered.filtered_count} empty entities", current=filtered.filtered_count, total=filtered.filtered_count ) if filtered.filtered_count == 0: state.status = SimulationStatus.FAILED - state.error = "没有找到符合条件的实体,请检查图谱是否正确构建" + state.error = "No valid entities extracted for simulation. Please check if the Graph was generated properly with valid text." self._save_simulation_state(state) return state - # ========== 阶段2: 生成Agent Profile ========== + # ========== Giai đoạn 2: Bắt đầu sinh Agent Profiles cho OASIS ========== total_entities = len(filtered.entities) if progress_callback: progress_callback( "generating_profiles", 0, - "开始生成...", + "Ready for AI Generation process...", current=0, total=total_entities ) - # 传入graph_id以启用Zep检索功能,获取更丰富的上下文 + # Gửi mã graph_id để bộ Profile có thể fetch thêm tài liệu nếu model cần lục vấn sâu generator = OasisProfileGenerator(graph_id=state.graph_id) def profile_progress(current, total, msg): @@ -325,7 +325,7 @@ def profile_progress(current, total, msg): item_name=msg ) - # 设置实时保存的文件路径(优先使用 Reddit JSON 格式) + # Khai báo đường dẫn tạm để AI lưu Real-time kết quả (Đặt ưu tiên Platform Reddit JSON làm chuẩn) realtime_output_path = None realtime_platform = "reddit" if state.enable_reddit: @@ -339,20 +339,20 @@ def profile_progress(current, total, msg): entities=filtered.entities, use_llm=use_llm_for_profiles, progress_callback=profile_progress, - graph_id=state.graph_id, # 传入graph_id用于Zep检索 - parallel_count=parallel_profile_count, # 并行生成数量 - realtime_output_path=realtime_output_path, # 实时保存路径 - output_platform=realtime_platform # 输出格式 + graph_id=state.graph_id, # Để tìm kiếm Zep Search Index + parallel_count=parallel_profile_count, # Số dòng luồng Async + realtime_output_path=realtime_output_path, # Lưu log thời gian thực + output_platform=realtime_platform # Đuôi file xuất ) state.profiles_count = len(profiles) - # 保存Profile文件(注意:Twitter使用CSV格式,Reddit使用JSON格式) - # Reddit 已经在生成过程中实时保存了,这里再保存一次确保完整性 + # Backup lại kết quả Profile (Twitter xuất ra text CSV, Reddit thì bắt buộc JSON cho cấu trúc OASIS) + # Reddit đã được render đồng thời ở block trên nhưng đây là re-save toàn bộ if progress_callback: progress_callback( "generating_profiles", 95, - "保存Profile文件...", + "Compressing Profile data...", current=total_entities, total=total_entities ) @@ -365,7 +365,7 @@ def profile_progress(current, total, msg): ) if state.enable_twitter: - # Twitter使用CSV格式!这是OASIS的要求 + # Riêng Twitter với code Script base OAsis của họ yêu cầu CSV format generator.save_profiles( profiles=profiles, file_path=os.path.join(sim_dir, "twitter_profiles.csv"), @@ -375,16 +375,16 @@ def profile_progress(current, total, msg): if progress_callback: progress_callback( "generating_profiles", 100, - f"完成,共 {len(profiles)} 个Profile", + f"Done, created {len(profiles)} Profiles", current=len(profiles), total=len(profiles) ) - # ========== 阶段3: LLM智能生成模拟配置 ========== + # ========== Giai đoạn 3: Uỷ thác cho LLM phân tích và xuất tham số mô phỏng ========== if progress_callback: progress_callback( "generating_config", 0, - "正在分析模拟需求...", + "Analyzing input requirements...", current=0, total=3 ) @@ -394,7 +394,7 @@ def profile_progress(current, total, msg): if progress_callback: progress_callback( "generating_config", 30, - "正在调用LLM生成配置...", + "LLM Bot is generating configuration...", current=1, total=3 ) @@ -413,12 +413,12 @@ def profile_progress(current, total, msg): if progress_callback: progress_callback( "generating_config", 70, - "正在保存配置文件...", + "Saving Config parameters...", current=2, total=3 ) - # 保存配置文件 + # Lưu file cứng simulation_config.json config_path = os.path.join(sim_dir, "simulation_config.json") with open(config_path, 'w', encoding='utf-8') as f: f.write(sim_params.to_json()) @@ -429,25 +429,25 @@ def profile_progress(current, total, msg): if progress_callback: progress_callback( "generating_config", 100, - "配置生成完成", + "Configuration Generation complete", current=3, total=3 ) - # 注意:运行脚本保留在 backend/scripts/ 目录,不再复制到模拟目录 - # 启动模拟时,simulation_runner 会从 scripts/ 目录运行脚本 + # Lưu ý kiến trúc: Các scripts thao tác thực thi vẫn để gốc ở `backend/scripts/`, SẼ KHÔNG CẦN chép đè sang folder Project + # Tại thời gian Khởi chạy, `simulation_runner` sẽ nạp base chạy thẳng từ folder `scripts/` đó. - # 更新状态 + # Cập nhật status state.status = SimulationStatus.READY self._save_simulation_state(state) - logger.info(f"模拟准备完成: {simulation_id}, " - f"entities={state.entities_count}, profiles={state.profiles_count}") + logger.info(f"Finished simulation preparation phase for ID: {simulation_id}, " + f"Total entities={state.entities_count}, Created profiles={state.profiles_count}") return state except Exception as e: - logger.error(f"模拟准备失败: {simulation_id}, error={str(e)}") + logger.error(f"Error occurred during Simulation preparation (Sim ID: {simulation_id}), ERROR CODE: {str(e)}") import traceback logger.error(traceback.format_exc()) state.status = SimulationStatus.FAILED @@ -456,16 +456,16 @@ def profile_progress(current, total, msg): raise def get_simulation(self, simulation_id: str) -> Optional[SimulationState]: - """获取模拟状态""" + """Đọc và lấy State hiện tại của Simulator""" return self._load_simulation_state(simulation_id) def list_simulations(self, project_id: Optional[str] = None) -> List[SimulationState]: - """列出所有模拟""" + """Liệt kê toàn bộ danh sách các Mô Phỏng (Simulations) đã khởi tạo""" simulations = [] if os.path.exists(self.SIMULATION_DATA_DIR): for sim_id in os.listdir(self.SIMULATION_DATA_DIR): - # 跳过隐藏文件(如 .DS_Store)和非目录文件 + # Loại bỏ các folder/file rác do hệ điều hành sinh ra (ví dụ: .DS_Store của macOS) hoặc không phải thư mục sim_path = os.path.join(self.SIMULATION_DATA_DIR, sim_id) if sim_id.startswith('.') or not os.path.isdir(sim_path): continue @@ -478,10 +478,10 @@ def list_simulations(self, project_id: Optional[str] = None) -> List[SimulationS return simulations def get_profiles(self, simulation_id: str, platform: str = "reddit") -> List[Dict[str, Any]]: - """获取模拟的Agent Profile""" + """Lấy/Tải dữ liệu Agent Profile do AI sinh ra dựa theo nền tảng mạng xã hội""" state = self._load_simulation_state(simulation_id) if not state: - raise ValueError(f"模拟不存在: {simulation_id}") + raise ValueError(f"Simulation with ID {simulation_id} does not exist") sim_dir = self._get_simulation_dir(simulation_id) profile_path = os.path.join(sim_dir, f"{platform}_profiles.json") @@ -493,7 +493,7 @@ def get_profiles(self, simulation_id: str, platform: str = "reddit") -> List[Dic return json.load(f) def get_simulation_config(self, simulation_id: str) -> Optional[Dict[str, Any]]: - """获取模拟配置""" + """Lấy thông số cấu hình của bản mô phỏng""" sim_dir = self._get_simulation_dir(simulation_id) config_path = os.path.join(sim_dir, "simulation_config.json") @@ -504,7 +504,7 @@ def get_simulation_config(self, simulation_id: str) -> Optional[Dict[str, Any]]: return json.load(f) def get_run_instructions(self, simulation_id: str) -> Dict[str, str]: - """获取运行说明""" + """Output ra hướng dẫn / Các câu lệnh dòng lệnh (CMD) để thực thi chạy bản đồ mô phỏng này""" sim_dir = self._get_simulation_dir(simulation_id) config_path = os.path.join(sim_dir, "simulation_config.json") scripts_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../scripts')) @@ -519,10 +519,10 @@ def get_run_instructions(self, simulation_id: str) -> Dict[str, str]: "parallel": f"python {scripts_dir}/run_parallel_simulation.py --config {config_path}", }, "instructions": ( - f"1. 激活conda环境: conda activate MiroFish\n" - f"2. 运行模拟 (脚本位于 {scripts_dir}):\n" - f" - 单独运行Twitter: python {scripts_dir}/run_twitter_simulation.py --config {config_path}\n" - f" - 单独运行Reddit: python {scripts_dir}/run_reddit_simulation.py --config {config_path}\n" - f" - 并行运行双平台: python {scripts_dir}/run_parallel_simulation.py --config {config_path}" + f"1. Khởi động môi trường môi trường lập trình Conda (nếu có): conda activate MiroFish\n" + f"2. Bắt đầu Run giả lập (Scripts gốc được gọi ra tại {scripts_dir}):\n" + f" - Nếu muốn chỉ giả lập trên Twitter: python {scripts_dir}/run_twitter_simulation.py --config {config_path}\n" + f" - Nếu muốn chỉ giả lập trên Reddit: python {scripts_dir}/run_reddit_simulation.py --config {config_path}\n" + f" - Chạy giả lập cả hai phân luồng song song: python {scripts_dir}/run_parallel_simulation.py --config {config_path}" ) } diff --git a/backend/app/services/simulation_runner.py b/backend/app/services/simulation_runner.py index 8c35380d1..bc5193fd1 100644 --- a/backend/app/services/simulation_runner.py +++ b/backend/app/services/simulation_runner.py @@ -25,37 +25,37 @@ logger = get_logger('mirofish.simulation_runner') -# 标记是否已注册清理函数 +# Cờ đánh dấu đã đăng ký hàm dọn dẹp hay chưa _cleanup_registered = False -# 平台检测 +# Kiểm tra hệ điều hành IS_WINDOWS = sys.platform == 'win32' class RunnerStatus(str, Enum): - """运行器状态""" - IDLE = "idle" - STARTING = "starting" - RUNNING = "running" - PAUSED = "paused" - STOPPING = "stopping" - STOPPED = "stopped" - COMPLETED = "completed" - FAILED = "failed" + """Trạng thái của bộ chạy tiến trình mô phỏng""" + IDLE = "idle" # Rảnh rỗi, chưa chạy + STARTING = "starting" # Đang khởi động + RUNNING = "running" # Đang chạy + PAUSED = "paused" # Đã tạm dừng + STOPPING = "stopping" # Đang dừng lại + STOPPED = "stopped" # Đã dừng + COMPLETED = "completed" # Đã hoàn thành + FAILED = "failed" # Bị lỗi @dataclass class AgentAction: - """Agent动作记录""" - round_num: int - timestamp: str - platform: str # twitter / reddit - agent_id: int - agent_name: str - action_type: str # CREATE_POST, LIKE_POST, etc. - action_args: Dict[str, Any] = field(default_factory=dict) - result: Optional[str] = None - success: bool = True + """Bản ghi hành động của Agent""" + round_num: int # Số thứ tự của vòng (round) mô phỏng + timestamp: str # Dấu thời gian + platform: str # Nền tảng thực hiện: twitter / reddit + agent_id: int # ID của agent + agent_name: str # Tên của agent + action_type: str # Loại hành động: CREATE_POST, LIKE_POST, v.v. + action_args: Dict[str, Any] = field(default_factory=dict) # Tham số của hành động + result: Optional[str] = None # Kết quả thực thi + success: bool = True # Hành động có thành công hay không def to_dict(self) -> Dict[str, Any]: return { @@ -73,15 +73,15 @@ def to_dict(self) -> Dict[str, Any]: @dataclass class RoundSummary: - """每轮摘要""" - round_num: int - start_time: str - end_time: Optional[str] = None - simulated_hour: int = 0 - twitter_actions: int = 0 - reddit_actions: int = 0 - active_agents: List[int] = field(default_factory=list) - actions: List[AgentAction] = field(default_factory=list) + """Tóm tắt thông tin của mỗi vòng (round)""" + round_num: int # Số thứ tự vòng + start_time: str # Thời gian bắt đầu + end_time: Optional[str] = None # Thời gian kết thúc + simulated_hour: int = 0 # Số giờ đã mô phỏng trong vòng này + twitter_actions: int = 0 # Số hành động trên Twitter + reddit_actions: int = 0 # Số hành động trên Reddit + active_agents: List[int] = field(default_factory=list) # Danh sách ID các agent đang hoạt động + actions: List[AgentAction] = field(default_factory=list) # Danh sách các hành động def to_dict(self) -> Dict[str, Any]: return { @@ -99,52 +99,52 @@ def to_dict(self) -> Dict[str, Any]: @dataclass class SimulationRunState: - """模拟运行状态(实时)""" + """Trạng thái đang thực thi của tiến trình mô phỏng (cập nhật theo thời gian thực)""" simulation_id: str runner_status: RunnerStatus = RunnerStatus.IDLE - # 进度信息 + # Thông tin tiến độ current_round: int = 0 total_rounds: int = 0 simulated_hours: int = 0 total_simulation_hours: int = 0 - # 各平台独立轮次和模拟时间(用于双平台并行显示) + # Các vòng lặp và thời gian độc lập cho từng nền tảng (sử dụng để hiển thị song song hai nền tảng) twitter_current_round: int = 0 reddit_current_round: int = 0 twitter_simulated_hours: int = 0 reddit_simulated_hours: int = 0 - # 平台状态 + # Trạng thái nền tảng đang chạy twitter_running: bool = False reddit_running: bool = False twitter_actions_count: int = 0 reddit_actions_count: int = 0 - # 平台完成状态(通过检测 actions.jsonl 中的 simulation_end 事件) + # Trạng thái hoàn thành chung của nền tảng (phát hiện qua sự kiện simulation_end trong actions.jsonl) twitter_completed: bool = False reddit_completed: bool = False - # 每轮摘要 + # Tóm tắt lại ở mỗi vòng rounds: List[RoundSummary] = field(default_factory=list) - # 最近动作(用于前端实时展示) + # Các hành động gần nhất (để hiển thị theo thời gian thực (real-time) trên frontend) recent_actions: List[AgentAction] = field(default_factory=list) max_recent_actions: int = 50 - # 时间戳 + # Dấu thời gian started_at: Optional[str] = None updated_at: str = field(default_factory=lambda: datetime.now().isoformat()) completed_at: Optional[str] = None - # 错误信息 + # Thông tin lỗi error: Optional[str] = None - # 进程ID(用于停止) + # ID tiến trình (PID) (để dừng/hủy tiến trình) process_pid: Optional[int] = None def add_action(self, action: AgentAction): - """添加动作到最近动作列表""" + """Thêm một hành động vào danh sách các hành động gần nhất""" self.recent_actions.insert(0, action) if len(self.recent_actions) > self.max_recent_actions: self.recent_actions = self.recent_actions[:self.max_recent_actions] @@ -165,7 +165,7 @@ def to_dict(self) -> Dict[str, Any]: "simulated_hours": self.simulated_hours, "total_simulation_hours": self.total_simulation_hours, "progress_percent": round(self.current_round / max(self.total_rounds, 1) * 100, 1), - # 各平台独立轮次和时间 + # Vòng lặp và thời gian độc lập cho mỗi nền tảng "twitter_current_round": self.twitter_current_round, "reddit_current_round": self.reddit_current_round, "twitter_simulated_hours": self.twitter_simulated_hours, @@ -185,7 +185,7 @@ def to_dict(self) -> Dict[str, Any]: } def to_detail_dict(self) -> Dict[str, Any]: - """包含最近动作的详细信息""" + """Chi tiết thông tin bao gồm các hành động gần nhất""" result = self.to_dict() result["recent_actions"] = [a.to_dict() for a in self.recent_actions] result["rounds_count"] = len(self.rounds) @@ -194,45 +194,45 @@ def to_detail_dict(self) -> Dict[str, Any]: class SimulationRunner: """ - 模拟运行器 + Trình chạy mô phỏng - 负责: - 1. 在后台进程中运行OASIS模拟 - 2. 解析运行日志,记录每个Agent的动作 - 3. 提供实时状态查询接口 - 4. 支持暂停/停止/恢复操作 + Quy trách nhiệm: + 1. Chạy mô phỏng OASIS trong tiến trình nền (background process) + 2. Phân tích nhật ký chạy (log), ghi lại hành động của mỗi Agent + 3. Cung cấp API truy vấn trạng thái thời gian thực + 4. Hỗ trợ thao tác tạm dừng (pause)/dừng (stop)/tiếp tục (resume) """ - # 运行状态存储目录 + # Thư mục lưu trữ trạng thái chạy RUN_STATE_DIR = os.path.join( os.path.dirname(__file__), '../../uploads/simulations' ) - # 脚本目录 + # Thư mục chứa các script con (script chạy ứng dụng) SCRIPTS_DIR = os.path.join( os.path.dirname(__file__), '../../scripts' ) - # 内存中的运行状态 + # Trạng thái chạy trong bộ nhớ Memory (RAM) _run_states: Dict[str, SimulationRunState] = {} _processes: Dict[str, subprocess.Popen] = {} _action_queues: Dict[str, Queue] = {} _monitor_threads: Dict[str, threading.Thread] = {} - _stdout_files: Dict[str, Any] = {} # 存储 stdout 文件句柄 - _stderr_files: Dict[str, Any] = {} # 存储 stderr 文件句柄 + _stdout_files: Dict[str, Any] = {} # Lưu trữ tay cầm file đầu ra chuẩn (stdout) + _stderr_files: Dict[str, Any] = {} # Lưu trữ tay cầm file lỗi chuẩn (stderr) - # 图谱记忆更新配置 - _graph_memory_enabled: Dict[str, bool] = {} # simulation_id -> enabled + # Cấu hình cập nhật bộ nhớ Đồ thị (Graph Memory) + _graph_memory_enabled: Dict[str, bool] = {} # simulation_id -> enabled (Bật/tắt) @classmethod def get_run_state(cls, simulation_id: str) -> Optional[SimulationRunState]: - """获取运行状态""" + """Lấy trạng thái chạy hiện tại""" if simulation_id in cls._run_states: return cls._run_states[simulation_id] - # 尝试从文件加载 + # Thử tải từ file nếu không có trong memory state = cls._load_run_state(simulation_id) if state: cls._run_states[simulation_id] = state @@ -240,7 +240,7 @@ def get_run_state(cls, simulation_id: str) -> Optional[SimulationRunState]: @classmethod def _load_run_state(cls, simulation_id: str) -> Optional[SimulationRunState]: - """从文件加载运行状态""" + """Tải trạng thái chạy từ tệp tin (run_state.json)""" state_file = os.path.join(cls.RUN_STATE_DIR, simulation_id, "run_state.json") if not os.path.exists(state_file): return None @@ -256,7 +256,7 @@ def _load_run_state(cls, simulation_id: str) -> Optional[SimulationRunState]: total_rounds=data.get("total_rounds", 0), simulated_hours=data.get("simulated_hours", 0), total_simulation_hours=data.get("total_simulation_hours", 0), - # 各平台独立轮次和时间 + # Các vòng lặp và thời gian độc lập cho mỗi nền tảng twitter_current_round=data.get("twitter_current_round", 0), reddit_current_round=data.get("reddit_current_round", 0), twitter_simulated_hours=data.get("twitter_simulated_hours", 0), @@ -274,7 +274,7 @@ def _load_run_state(cls, simulation_id: str) -> Optional[SimulationRunState]: process_pid=data.get("process_pid"), ) - # 加载最近动作 + # Tải danh sách các hành động gần đây actions_data = data.get("recent_actions", []) for a in actions_data: state.recent_actions.append(AgentAction( @@ -291,12 +291,12 @@ def _load_run_state(cls, simulation_id: str) -> Optional[SimulationRunState]: return state except Exception as e: - logger.error(f"加载运行状态失败: {str(e)}") + logger.error(f"Failed to load run state: {str(e)}") return None @classmethod def _save_run_state(cls, state: SimulationRunState): - """保存运行状态到文件""" + """Lưu trạng thái chạy vào file""" sim_dir = os.path.join(cls.RUN_STATE_DIR, state.simulation_id) os.makedirs(sim_dir, exist_ok=True) state_file = os.path.join(sim_dir, "run_state.json") @@ -313,50 +313,50 @@ def start_simulation( cls, simulation_id: str, platform: str = "parallel", # twitter / reddit / parallel - max_rounds: int = None, # 最大模拟轮数(可选,用于截断过长的模拟) - enable_graph_memory_update: bool = False, # 是否将活动更新到Zep图谱 - graph_id: str = None # Zep图谱ID(启用图谱更新时必需) + max_rounds: int = None, # Số vòng mô phỏng tối đa (tùy chọn, dùng để cắt ngắn các mô phỏng quá dài) + enable_graph_memory_update: bool = False, # Có liên tục cập nhật hoạt động của Agent vào Zep graph hay không + graph_id: str = None # ID của Zep graph (Bắt buộc nếu bật tính năng cập nhật sơ đồ (graph)) ) -> SimulationRunState: """ - 启动模拟 + Bắt đầu mô phỏng Args: - simulation_id: 模拟ID - platform: 运行平台 (twitter/reddit/parallel) - max_rounds: 最大模拟轮数(可选,用于截断过长的模拟) - enable_graph_memory_update: 是否将Agent活动动态更新到Zep图谱 - graph_id: Zep图谱ID(启用图谱更新时必需) + simulation_id: ID mô phỏng + platform: Nền tảng chạy (twitter/reddit/parallel) + max_rounds: Số vòng chạy tối đa (để cắt bớt) + enable_graph_memory_update: Có cập nhật hành vi Agent vào Zep Graph hay không + graph_id: Zep Graph ID Returns: - SimulationRunState + SimulationRunState (Trạng thái sau khi cấu hình) """ - # 检查是否已在运行 + # Kiểm tra xem có tiến trình nào đang chạy không existing = cls.get_run_state(simulation_id) if existing and existing.runner_status in [RunnerStatus.RUNNING, RunnerStatus.STARTING]: - raise ValueError(f"模拟已在运行中: {simulation_id}") + raise ValueError(f"Simulation is already running: {simulation_id}") - # 加载模拟配置 + # Tải cấu hình mô phỏng sim_dir = os.path.join(cls.RUN_STATE_DIR, simulation_id) config_path = os.path.join(sim_dir, "simulation_config.json") if not os.path.exists(config_path): - raise ValueError(f"模拟配置不存在,请先调用 /prepare 接口") + raise ValueError(f"Simulation configuration not found, please call the /prepare API first") with open(config_path, 'r', encoding='utf-8') as f: config = json.load(f) - # 初始化运行状态 + # Khởi tạo trạng thái chạy time_config = config.get("time_config", {}) total_hours = time_config.get("total_simulation_hours", 72) minutes_per_round = time_config.get("minutes_per_round", 30) total_rounds = int(total_hours * 60 / minutes_per_round) - # 如果指定了最大轮数,则截断 + # Nếu chỉ định maximum rounds, tiến hành việc cắt bớt if max_rounds is not None and max_rounds > 0: original_rounds = total_rounds total_rounds = min(total_rounds, max_rounds) if total_rounds < original_rounds: - logger.info(f"轮数已截断: {original_rounds} -> {total_rounds} (max_rounds={max_rounds})") + logger.info(f"Rounds truncated: {original_rounds} -> {total_rounds} (max_rounds={max_rounds})") state = SimulationRunState( simulation_id=simulation_id, @@ -368,22 +368,22 @@ def start_simulation( cls._save_run_state(state) - # 如果启用图谱记忆更新,创建更新器 + # Nếu tính năng cập nhật bộ nhớ graph được bật, tạo một updater if enable_graph_memory_update: if not graph_id: - raise ValueError("启用图谱记忆更新时必须提供 graph_id") + raise ValueError("graph_id is required to enable graph memory updates") try: ZepGraphMemoryManager.create_updater(simulation_id, graph_id) cls._graph_memory_enabled[simulation_id] = True - logger.info(f"已启用图谱记忆更新: simulation_id={simulation_id}, graph_id={graph_id}") + logger.info(f"Graph memory update enabled: simulation_id={simulation_id}, graph_id={graph_id}") except Exception as e: - logger.error(f"创建图谱记忆更新器失败: {e}") + logger.error(f"Failed to create graph memory updater: {e}") cls._graph_memory_enabled[simulation_id] = False else: cls._graph_memory_enabled[simulation_id] = False - # 确定运行哪个脚本(脚本位于 backend/scripts/ 目录) + # Xác định script nào sẽ chạy (các script nằm trong thư mục backend/scripts/) if platform == "twitter": script_name = "run_twitter_simulation.py" state.twitter_running = True @@ -398,64 +398,64 @@ def start_simulation( script_path = os.path.join(cls.SCRIPTS_DIR, script_name) if not os.path.exists(script_path): - raise ValueError(f"脚本不存在: {script_path}") + raise ValueError(f"Script no longer exists: {script_path}") - # 创建动作队列 + # Tạo hàng đợi các hành động (Queue) action_queue = Queue() cls._action_queues[simulation_id] = action_queue - # 启动模拟进程 + # Bắt đầu chạy tiến trình mô phỏng try: - # 构建运行命令,使用完整路径 - # 新的日志结构: - # twitter/actions.jsonl - Twitter 动作日志 - # reddit/actions.jsonl - Reddit 动作日志 - # simulation.log - 主进程日志 + # Xây dựng lệnh chạy, sử dụng full path + # Cấu trúc log mới: + # twitter/actions.jsonl - Log cho các hành động trên Twitter + # reddit/actions.jsonl - Log cho các hành động trên Reddit + # simulation.log - Log cho tiến trình chính cmd = [ - sys.executable, # Python解释器 + sys.executable, # Python Interpreter script_path, - "--config", config_path, # 使用完整配置文件路径 + "--config", config_path, # Use full path to config ] - # 如果指定了最大轮数,添加到命令行参数 + # Nếu có thiết lập giới hạn vòng tối đa, hãy truyền nó qua dòng lệnh (command line args) if max_rounds is not None and max_rounds > 0: cmd.extend(["--max-rounds", str(max_rounds)]) - # 创建主日志文件,避免 stdout/stderr 管道缓冲区满导致进程阻塞 + # Tạo tệp log chính để tránh bộ đệm ống dẫn (pipe buffer) stdout/stderr của tiến trình đầy main_log_path = os.path.join(sim_dir, "simulation.log") main_log_file = open(main_log_path, 'w', encoding='utf-8') - # 设置子进程环境变量,确保 Windows 上使用 UTF-8 编码 - # 这可以修复第三方库(如 OASIS)读取文件时未指定编码的问题 + # Đặt môi trường cho quy trình con để đảm bảo trên Windows được mã hóa thành UTF-8 + # Điều này sửa lỗi thư viện của bên thứ 3 khi họ gọi file hệ thống nếu không chỉ định rõ encode. env = os.environ.copy() - env['PYTHONUTF8'] = '1' # Python 3.7+ 支持,让所有 open() 默认使用 UTF-8 - env['PYTHONIOENCODING'] = 'utf-8' # 确保 stdout/stderr 使用 UTF-8 + env['PYTHONUTF8'] = '1' # Python 3.7+ hỗ trợ điều này, giúp mọi hàm open() mặc định theo UTF-8 + env['PYTHONIOENCODING'] = 'utf-8' # Đảm bảo đầu ra có stdout/stderr dưới dạng UTF-8 - # 设置工作目录为模拟目录(数据库等文件会生成在此) - # 使用 start_new_session=True 创建新的进程组,确保可以通过 os.killpg 终止所有子进程 + # Đặt thư mục làm việc (CWD - Current Working Directory) thành thư mục nơi mô phỏng + # Thiết lập start_new_session=True sẽ tạo ra nhóm các tiến trình con mới, vì thế thông qua os.killpg có thể hủy toàn bộ những cái đó process = subprocess.Popen( cmd, cwd=sim_dir, stdout=main_log_file, - stderr=subprocess.STDOUT, # stderr 也写入同一个文件 + stderr=subprocess.STDOUT, # Đẩy luồng stderr cũng vào file đó text=True, - encoding='utf-8', # 显式指定编码 + encoding='utf-8', # Explicitly specify encoding bufsize=1, - env=env, # 传递带有 UTF-8 设置的环境变量 - start_new_session=True, # 创建新进程组,确保服务器关闭时能终止所有相关进程 + env=env, # Đi kèm bộ setting Environment có set UTF-8 + start_new_session=True, # Bắt đầu tạo 1 luồng xử lý mới (New process group) ) - # 保存文件句柄以便后续关闭 + # Ghi lại file để cho bước đóng (close) được thực hiện dễ dàng cls._stdout_files[simulation_id] = main_log_file - cls._stderr_files[simulation_id] = None # 不再需要单独的 stderr + cls._stderr_files[simulation_id] = None # Không cần lưu file stderr độc lập nữa state.process_pid = process.pid state.runner_status = RunnerStatus.RUNNING cls._processes[simulation_id] = process cls._save_run_state(state) - # 启动监控线程 + # Khởi động tiểu trình giám sát (Monitor thread) monitor_thread = threading.Thread( target=cls._monitor_simulation, args=(simulation_id,), @@ -464,7 +464,7 @@ def start_simulation( monitor_thread.start() cls._monitor_threads[simulation_id] = monitor_thread - logger.info(f"模拟启动成功: {simulation_id}, pid={process.pid}, platform={platform}") + logger.info(f"Simulation started successfully: {simulation_id}, pid={process.pid}, platform={platform}") except Exception as e: state.runner_status = RunnerStatus.FAILED @@ -476,7 +476,7 @@ def start_simulation( @classmethod def _monitor_simulation(cls, simulation_id: str): - """监控模拟进程,解析动作日志""" + """Giám sát (Monitor) phân tích nhật ký ghi lại các hành động""" sim_dir = os.path.join(cls.RUN_STATE_DIR, simulation_id) # 新的日志结构:分平台的动作日志 @@ -584,18 +584,18 @@ def _read_action_log( platform: str ) -> int: """ - 读取动作日志文件 + Đọc tệp tin nhật ký (log) của hệ thống Args: - log_path: 日志文件路径 - position: 上次读取位置 - state: 运行状态对象 - platform: 平台名称 (twitter/reddit) + log_path: Đường dẫn tệp nhật ký + position: Vị trí đọc trước đó + state: Đối tượng trạng thái đang chạy + platform: Nền tảng (twitter/reddit) Returns: - 新的读取位置 + Vị trí đọc mới """ - # 检查是否启用了图谱记忆更新 + # Kiểm tra xem có bật tính năng cập nhật bộ nhớ graph hay không graph_memory_enabled = cls._graph_memory_enabled.get(state.simulation_id, False) graph_updater = None if graph_memory_enabled: @@ -610,36 +610,36 @@ def _read_action_log( try: action_data = json.loads(line) - # 处理事件类型的条目 + # Xử lý các mục của loại sự kiện if "event_type" in action_data: event_type = action_data.get("event_type") - # 检测 simulation_end 事件,标记平台已完成 + # Phát hiện sự kiện simulation_end và đánh dấu nền tảng đã hoàn thành if event_type == "simulation_end": if platform == "twitter": state.twitter_completed = True state.twitter_running = False - logger.info(f"Twitter 模拟已完成: {state.simulation_id}, total_rounds={action_data.get('total_rounds')}, total_actions={action_data.get('total_actions')}") + logger.info(f"Twitter simulation completed: {state.simulation_id}, total_rounds={action_data.get('total_rounds')}, total_actions={action_data.get('total_actions')}") elif platform == "reddit": state.reddit_completed = True state.reddit_running = False - logger.info(f"Reddit 模拟已完成: {state.simulation_id}, total_rounds={action_data.get('total_rounds')}, total_actions={action_data.get('total_actions')}") + logger.info(f"Reddit simulation completed: {state.simulation_id}, total_rounds={action_data.get('total_rounds')}, total_actions={action_data.get('total_actions')}") - # 检查是否所有启用的平台都已完成 - # 如果只运行了一个平台,只检查那个平台 - # 如果运行了两个平台,需要两个都完成 + # Kiểm tra xem có phải tất cả các nền tảng được bật đều đã hoàn thành hay không + # Nếu chỉ một nền tảng đang chạy, hãy chỉ kiểm tra nền tảng đó + # Nếu 2 nền tảng đang chạy thì yêu cầu phải hoàn thành cả 2 nền tảng all_completed = cls._check_all_platforms_completed(state) if all_completed: state.runner_status = RunnerStatus.COMPLETED state.completed_at = datetime.now().isoformat() - logger.info(f"所有平台模拟已完成: {state.simulation_id}") + logger.info(f"Simulation completed for all platforms: {state.simulation_id}") - # 更新轮次信息(从 round_end 事件) + # Cập nhật thông tin vòng (round_num) (từ sự kiện round_end) elif event_type == "round_end": round_num = action_data.get("round", 0) simulated_hours = action_data.get("simulated_hours", 0) - # 更新各平台独立的轮次和时间 + # Cập nhật thời gian và vòng thứ tự độc lập cho nền tảng if platform == "twitter": if round_num > state.twitter_current_round: state.twitter_current_round = round_num @@ -649,10 +649,10 @@ def _read_action_log( state.reddit_current_round = round_num state.reddit_simulated_hours = simulated_hours - # 总体轮次取两个平台的最大值 + # Số vòng chung sẽ là số lớn nhất của hai nền tảng if round_num > state.current_round: state.current_round = round_num - # 总体时间取两个平台的最大值 + # Thời gian chung sẽ là số lớn nhất của hai nền tảng state.simulated_hours = max(state.twitter_simulated_hours, state.reddit_simulated_hours) continue @@ -670,11 +670,11 @@ def _read_action_log( ) state.add_action(action) - # 更新轮次 + # Cập nhật thông tin (vòng) round if action.round_num and action.round_num > state.current_round: state.current_round = action.round_num - # 如果启用了图谱记忆更新,将活动发送到Zep + # Nếu cập nhật bộ nhớ graph được bật, thêm hoạt động vào Zep graph if graph_updater: graph_updater.add_activity_from_dict(action_data, platform) @@ -682,52 +682,52 @@ def _read_action_log( pass return f.tell() except Exception as e: - logger.warning(f"读取动作日志失败: {log_path}, error={e}") + logger.warning(f"Failed to read action logs: {log_path}, error={e}") return position @classmethod def _check_all_platforms_completed(cls, state: SimulationRunState) -> bool: """ - 检查所有启用的平台是否都已完成模拟 + Kiểm tra xem tất cả các nền tảng có hoàn thành quá trình mô phỏng hay chưa? - 通过检查对应的 actions.jsonl 文件是否存在来判断平台是否被启用 + Kiểm tra xem nền tảng có được kích hoạt (hay enable) hay không bằng cách xem tệp tin actions.jsonl có tồn tại hay không Returns: - True 如果所有启用的平台都已完成 + True Nếu tất cả các nền tảng được bật đều đã hoàn thành """ sim_dir = os.path.join(cls.RUN_STATE_DIR, state.simulation_id) twitter_log = os.path.join(sim_dir, "twitter", "actions.jsonl") reddit_log = os.path.join(sim_dir, "reddit", "actions.jsonl") - # 检查哪些平台被启用(通过文件是否存在判断) + # Kiểm tra xem mình có đang bật nền tảng nào không (Sử dụng cách kiểm tra tệp tin có tồn tại (exist) hay không) twitter_enabled = os.path.exists(twitter_log) reddit_enabled = os.path.exists(reddit_log) - # 如果平台被启用但未完成,则返回 False + # Nền tảng nào chưa xong thì trả về false if twitter_enabled and not state.twitter_completed: return False if reddit_enabled and not state.reddit_completed: return False - # 至少有一个平台被启用且已完成 + # Phải có ít nhất 1 nền tảng chạy xong thì mới là true. (nếu 1 nền tảng không chạy => False. False and False = False. True and False = True) return twitter_enabled or reddit_enabled @classmethod def _terminate_process(cls, process: subprocess.Popen, simulation_id: str, timeout: int = 10): """ - 跨平台终止进程及其子进程 + Khả năng tương thích nền tảng, dừng một quá trình và các quá trình con (nhánh) Args: - process: 要终止的进程 - simulation_id: 模拟ID(用于日志) - timeout: 等待进程退出的超时时间(秒) + process: Quy trình để chấm dứt (kill) + simulation_id: Ghi log ID + timeout: Thời gian chờ cho phép tiến trình kết thúc tính bằng giây (seconds) """ if IS_WINDOWS: - # Windows: 使用 taskkill 命令终止进程树 - # /F = 强制终止, /T = 终止进程树(包括子进程) - logger.info(f"终止进程树 (Windows): simulation={simulation_id}, pid={process.pid}") + # Windows: Sử dụng câu lệnh taskkill để xóa cả tiến trình theo cấu trúc branch tree + # /F = Force termination (Xoa bằng mọi giá), /T = Terminate tree (xóa nhánh tiến trình) bao gồm các sub-process + logger.info(f"Đang dừng quá trình (Windows): simulation={simulation_id}, pid={process.pid}") try: - # 先尝试优雅终止 + # Trước hết hãy cố găng dừng mềm subprocess.run( ['taskkill', '/PID', str(process.pid), '/T'], capture_output=True, @@ -736,8 +736,8 @@ def _terminate_process(cls, process: subprocess.Popen, simulation_id: str, timeo try: process.wait(timeout=timeout) except subprocess.TimeoutExpired: - # 强制终止 - logger.warning(f"进程未响应,强制终止: {simulation_id}") + # Nếu không thể dùng dừng mềm (sau timeout), xóa cứng bằng /F + logger.warning(f"Process unresponsive, force terminating: {simulation_id}") subprocess.run( ['taskkill', '/F', '/PID', str(process.pid), '/T'], capture_output=True, @@ -745,53 +745,53 @@ def _terminate_process(cls, process: subprocess.Popen, simulation_id: str, timeo ) process.wait(timeout=5) except Exception as e: - logger.warning(f"taskkill 失败,尝试 terminate: {e}") + logger.warning(f"taskkill failed, attempting terminate: {e}") process.terminate() try: process.wait(timeout=5) except subprocess.TimeoutExpired: process.kill() else: - # Unix: 使用进程组终止 - # 由于使用了 start_new_session=True,进程组 ID 等于主进程 PID + # Unix: Sử dụng process group chấm dứt + # Dùng start_new_session=True, giá trị pgid sẽ bằng đúng với PID gốc của tiến trình pgid = os.getpgid(process.pid) - logger.info(f"终止进程组 (Unix): simulation={simulation_id}, pgid={pgid}") + logger.info(f"Đang dừng nhóm tiến trình (Unix): simulation={simulation_id}, pgid={pgid}") - # 先发送 SIGTERM 给整个进程组 + # Gửi SIGTERM tới toàn bộ process group os.killpg(pgid, signal.SIGTERM) try: process.wait(timeout=timeout) except subprocess.TimeoutExpired: - # 如果超时后还没结束,强制发送 SIGKILL - logger.warning(f"进程组未响应 SIGTERM,强制终止: {simulation_id}") + # Nếu xảy ra hiện tượng chưa tự hủy sau timeout, xóa cưỡng bức bằng SIGKILL + logger.warning(f"Process group unresponsive to SIGTERM, force terminating: {simulation_id}") os.killpg(pgid, signal.SIGKILL) process.wait(timeout=5) @classmethod def stop_simulation(cls, simulation_id: str) -> SimulationRunState: - """停止模拟""" + """Dừng lại tiến trình mô phỏng""" state = cls.get_run_state(simulation_id) if not state: - raise ValueError(f"模拟不存在: {simulation_id}") + raise ValueError(f"Simulation not found: {simulation_id}") if state.runner_status not in [RunnerStatus.RUNNING, RunnerStatus.PAUSED]: - raise ValueError(f"模拟未在运行: {simulation_id}, status={state.runner_status}") + raise ValueError(f"Simulation is not running: {simulation_id}, status={state.runner_status}") state.runner_status = RunnerStatus.STOPPING cls._save_run_state(state) - # 终止进程 + # Kết thúc tiến trình con (child process) process = cls._processes.get(simulation_id) if process and process.poll() is None: try: cls._terminate_process(process, simulation_id) except ProcessLookupError: - # 进程已经不存在 + # Quá trình không còn ở đây nữa (Đã thoát hoặc bị đóng) pass except Exception as e: - logger.error(f"终止进程组失败: {simulation_id}, error={e}") - # 回退到直接终止进程 + logger.error(f"Failed to terminate process group: {simulation_id}, error={e}") + # Thử thêm một cách nữa để chăc chắn hủy tiến trình try: process.terminate() process.wait(timeout=5) @@ -804,16 +804,16 @@ def stop_simulation(cls, simulation_id: str) -> SimulationRunState: state.completed_at = datetime.now().isoformat() cls._save_run_state(state) - # 停止图谱记忆更新器 + # Dừng quá trình graph memory updater if cls._graph_memory_enabled.get(simulation_id, False): try: ZepGraphMemoryManager.stop_updater(simulation_id) - logger.info(f"已停止图谱记忆更新: simulation_id={simulation_id}") + logger.info(f"Graph memory update stopped: simulation_id={simulation_id}") except Exception as e: - logger.error(f"停止图谱记忆更新器失败: {e}") + logger.error(f"Failed to stop graph memory updater: {e}") cls._graph_memory_enabled.pop(simulation_id, None) - logger.info(f"模拟已停止: {simulation_id}") + logger.info(f"Simulation stopped: {simulation_id}") return state @classmethod @@ -826,14 +826,14 @@ def _read_actions_from_file( round_num: Optional[int] = None ) -> List[AgentAction]: """ - 从单个动作文件中读取动作 + Đọc các hoạt động từ một tệp duy nhất Args: - file_path: 动作日志文件路径 - default_platform: 默认平台(当动作记录中没有 platform 字段时使用) - platform_filter: 过滤平台 - agent_id: 过滤 Agent ID - round_num: 过滤轮次 + file_path: Đường dẫn tệp log của hành động đó + default_platform: Nền tảng mặc định (nếu trong nhật ký không có platform) + platform_filter: Lọc nền tảng (chỉ định platform cần đọc log) + agent_id: Lọc ID của Agent cụ thể + round_num: Lọc số vòng của Agent """ if not os.path.exists(file_path): return [] @@ -849,18 +849,18 @@ def _read_actions_from_file( try: data = json.loads(line) - # 跳过非动作记录(如 simulation_start, round_start, round_end 等事件) + # Bỏ qua các bản ghi không phải hành động (chẳng hạn như là sự kiện về hệ thống: simulation_start, round_start, round_end v.v.) if "event_type" in data: continue - # 跳过没有 agent_id 的记录(非 Agent 动作) + # Bỏ lỡ các sự kiện không phải do agent tạo ra (Không có ID đặc trưng của Agent) if "agent_id" not in data: continue - # 获取平台:优先使用记录中的 platform,否则使用默认平台 + # Lấy nền tảng (Platform): Ưu tiên lấy từ bản ghi nếu có `platform`, nếu không thì dùng `default_platform` record_platform = data.get("platform") or default_platform or "" - # 过滤 + # Bộ lọc (Filtering) if platform_filter and record_platform != platform_filter: continue if agent_id is not None and data.get("agent_id") != agent_id: @@ -894,54 +894,54 @@ def get_all_actions( round_num: Optional[int] = None ) -> List[AgentAction]: """ - 获取所有平台的完整动作历史(无分页限制) + Lấy thông tin tất cả lịch sử hoạt động của các nền tảng (không giới hạn phân trang) Args: - simulation_id: 模拟ID - platform: 过滤平台(twitter/reddit) - agent_id: 过滤Agent - round_num: 过滤轮次 + simulation_id: ID mô phỏng + platform: Bộ lọc nền tảng hoạt động (twitter/reddit) + agent_id: Lọc Agent + round_num: Lọc số vòng Returns: - 完整的动作列表(按时间戳排序,新的在前) + Danh sách đầy đủ các actions (sắp xếp theo thời gian mới nhất lên trước) """ sim_dir = os.path.join(cls.RUN_STATE_DIR, simulation_id) actions = [] - # 读取 Twitter 动作文件(根据文件路径自动设置 platform 为 twitter) + # Đọc tệp tin Actions của Twitter (Khai báo tự động điền twitter theo cấu trúc tệp tin log) twitter_actions_log = os.path.join(sim_dir, "twitter", "actions.jsonl") if not platform or platform == "twitter": actions.extend(cls._read_actions_from_file( twitter_actions_log, - default_platform="twitter", # 自动填充 platform 字段 + default_platform="twitter", # Điền dữ liệu tự động cho record `platform` platform_filter=platform, agent_id=agent_id, round_num=round_num )) - # 读取 Reddit 动作文件(根据文件路径自动设置 platform 为 reddit) + # Đọc tệp tin Actions của Reddit (Tự động điền phần 'reddit' căn cứ thư mục chứa tệp tin) reddit_actions_log = os.path.join(sim_dir, "reddit", "actions.jsonl") if not platform or platform == "reddit": actions.extend(cls._read_actions_from_file( reddit_actions_log, - default_platform="reddit", # 自动填充 platform 字段 + default_platform="reddit", # Automatically fill the platform field platform_filter=platform, agent_id=agent_id, round_num=round_num )) - # 如果分平台文件不存在,尝试读取旧的单一文件格式 + # Nếu thư mục chạy các nền tảng chạy parallel này (twitter / reddit) không có ở đó. Hãy thử với các tệp định dạng cũ if not actions: actions_log = os.path.join(sim_dir, "actions.jsonl") actions = cls._read_actions_from_file( actions_log, - default_platform=None, # 旧格式文件中应该有 platform 字段 + default_platform=None, # Các file json log định dạng cũ đã có sẵn record về platform nên không điền default platform_filter=platform, agent_id=agent_id, round_num=round_num ) - # 按时间戳排序(新的在前) + # Sắp xếp lại log theo thời gian timestamp giảm dần (từ mới hơn lên trước) actions.sort(key=lambda x: x.timestamp, reverse=True) return actions @@ -957,18 +957,18 @@ def get_actions( round_num: Optional[int] = None ) -> List[AgentAction]: """ - 获取动作历史(带分页) + Lấy thông tin lịch sử diễn ra (Hỗ trợ phân trang bằng offset và limit) Args: - simulation_id: 模拟ID - limit: 返回数量限制 - offset: 偏移量 - platform: 过滤平台 - agent_id: 过滤Agent - round_num: 过滤轮次 + simulation_id: Simulation ID + limit: Record Count returns limit + offset: Offset + platform: Filter Platform + agent_id: Filter Agent by ID + round_num: Filter round loop Returns: - 动作列表 + Actions list """ actions = cls.get_all_actions( simulation_id=simulation_id, @@ -977,7 +977,7 @@ def get_actions( round_num=round_num ) - # 分页 + # Phân trang return actions[offset:offset + limit] @classmethod @@ -988,19 +988,19 @@ def get_timeline( end_round: Optional[int] = None ) -> List[Dict[str, Any]]: """ - 获取模拟时间线(按轮次汇总) + Lấy thời gian (timeline) mô phỏng diễn ra (tóm tắt theo vòng được khai báo) Args: - simulation_id: 模拟ID - start_round: 起始轮次 - end_round: 结束轮次 + simulation_id: ID Mô phỏng + start_round: Bắt đầu từ một vòng lặp nhất định (First round Number) + end_round: Kết thúc từ vòng ở đó (End round Number) Returns: - 每轮的汇总信息 + Cung cấp đầy đủ thông tin về các round bị gói gọn """ actions = cls.get_actions(simulation_id, limit=10000) - # 按轮次分组 + # Nhóm tiến trình lại vào trong Vòng (round grouping) rounds: Dict[int, Dict[str, Any]] = {} for action in actions: @@ -1033,7 +1033,7 @@ def get_timeline( r["action_types"][action.action_type] = r["action_types"].get(action.action_type, 0) + 1 r["last_action_time"] = action.timestamp - # 转换为列表 + # Chuyển đổi trạng thái về Lists Arrays Data type result = [] for round_num in sorted(rounds.keys()): r = rounds[round_num] @@ -1054,10 +1054,10 @@ def get_timeline( @classmethod def get_agent_stats(cls, simulation_id: str) -> List[Dict[str, Any]]: """ - 获取每个Agent的统计信息 + Lấy thông kê của mọi agent Returns: - Agent统计列表 + Danh sách thống kê Agent """ actions = cls.get_actions(simulation_id, limit=10000) @@ -1089,7 +1089,7 @@ def get_agent_stats(cls, simulation_id: str) -> List[Dict[str, Any]]: stats["action_types"][action.action_type] = stats["action_types"].get(action.action_type, 0) + 1 stats["last_action_time"] = action.timestamp - # 按总动作数排序 + # Sắp xếp theo tổng số hành động giảm dần (reverse = true) result = sorted(agent_stats.values(), key=lambda x: x["total_actions"], reverse=True) return result @@ -1097,51 +1097,51 @@ def get_agent_stats(cls, simulation_id: str) -> List[Dict[str, Any]]: @classmethod def cleanup_simulation_logs(cls, simulation_id: str) -> Dict[str, Any]: """ - 清理模拟的运行日志(用于强制重新开始模拟) + Xóa tệp log chạy để buộc mô phỏng được khởi động lại - 会删除以下文件: + Xóa sạch các tệp tin này bao gồm: - run_state.json - twitter/actions.jsonl - reddit/actions.jsonl - simulation.log - stdout.log / stderr.log - - twitter_simulation.db(模拟数据库) - - reddit_simulation.db(模拟数据库) - - env_status.json(环境状态) + - twitter_simulation.db(Dữ liệu nền tảng twitter) + - reddit_simulation.db(Dữ liệu nền tảng reddit) + - env_status.json(Trạng thái file Environment status) - 注意:不会删除配置文件(simulation_config.json)和 profile 文件 + Chú ý: Các file liên kết đến cấu hình mô phỏng hay config thiết lập (như là simulation_config.json) hay Profile đều sẽ KHÔNG bị xóa đi. Args: - simulation_id: 模拟ID + simulation_id: Simulation ID Returns: - 清理结果信息 + Kết quả của lệnh xóa sạch (clean up) """ import shutil sim_dir = os.path.join(cls.RUN_STATE_DIR, simulation_id) if not os.path.exists(sim_dir): - return {"success": True, "message": "模拟目录不存在,无需清理"} + return {"success": True, "message": "Simulation directory does not exist, no need to clean."} cleaned_files = [] errors = [] - # 要删除的文件列表(包括数据库文件) + # Các tệp tin cần bị loại bỏ bao gồm log, database... files_to_delete = [ "run_state.json", "simulation.log", "stdout.log", "stderr.log", - "twitter_simulation.db", # Twitter 平台数据库 - "reddit_simulation.db", # Reddit 平台数据库 - "env_status.json", # 环境状态文件 + "twitter_simulation.db", # Twitter Database + "reddit_simulation.db", # Reddit Database + "env_status.json", # Env state status file ] - # 要删除的目录列表(包含动作日志) + # Nhưng tệp tin có cấp quyền cần xóa (có liên quan nhật ký hoạt động actions.jsonl) dirs_to_clean = ["twitter", "reddit"] - # 删除文件 + # Loại bỏ các tệp không cần tới (Delete them) for filename in files_to_delete: file_path = os.path.join(sim_dir, filename) if os.path.exists(file_path): @@ -1149,9 +1149,9 @@ def cleanup_simulation_logs(cls, simulation_id: str) -> Dict[str, Any]: os.remove(file_path) cleaned_files.append(filename) except Exception as e: - errors.append(f"删除 {filename} 失败: {str(e)}") + errors.append(f"Failed to delete {filename}: {str(e)}") - # 清理平台目录中的动作日志 + # Kiểm tra lại các file theo thư mục chứa action history action.jsonl files for dir_name in dirs_to_clean: dir_path = os.path.join(sim_dir, dir_name) if os.path.exists(dir_path): @@ -1161,13 +1161,13 @@ def cleanup_simulation_logs(cls, simulation_id: str) -> Dict[str, Any]: os.remove(actions_file) cleaned_files.append(f"{dir_name}/actions.jsonl") except Exception as e: - errors.append(f"删除 {dir_name}/actions.jsonl 失败: {str(e)}") + errors.append(f"Failed to delete {dir_name}/actions.jsonl: {str(e)}") - # 清理内存中的运行状态 + # Xóa (clear) cache nhớ run state if simulation_id in cls._run_states: del cls._run_states[simulation_id] - logger.info(f"清理模拟日志完成: {simulation_id}, 删除文件: {cleaned_files}") + logger.info(f"Clean up complete for simulation: {simulation_id}, Deleted files: {cleaned_files}") return { "success": len(errors) == 0, @@ -1175,71 +1175,71 @@ def cleanup_simulation_logs(cls, simulation_id: str) -> Dict[str, Any]: "errors": errors if errors else None } - # 防止重复清理的标志 + # Flags Ngăn việc phải làm quá nhiều việc cho một action cleanup đã làm từ đầu hay khi gọi lần tới lệnh giống _cleanup_done = False @classmethod def cleanup_all_simulations(cls): """ - 清理所有运行中的模拟进程 + Dọn dẹp tất cả các tiến trình mô phỏng đang chạy - 在服务器关闭时调用,确保所有子进程被终止 + Được gọi (call) khi đóng máy chủ, nhằm vào việc muốn các máy chủ con (child processes) bị tắt theo """ - # 防止重复清理 + # Nếu đã clean (dọn dẹp) thì không làm nữa if cls._cleanup_done: return cls._cleanup_done = True - # 检查是否有内容需要清理(避免空进程的进程打印无用日志) + # Kiểm tra xem có gì để dọn dẹp không (tránh log rỗng khi không có tiến trình chạy) has_processes = bool(cls._processes) has_updaters = bool(cls._graph_memory_enabled) if not has_processes and not has_updaters: - return # 没有需要清理的内容,静默返回 + return # Không có gì để dọn, kết thúc - logger.info("正在清理所有模拟进程...") + logger.info("Cleaning up all simulation processes...") - # 首先停止所有图谱记忆更新器(stop_all 内部会打印日志) + # Dừng tất cả cái update đồ thị nhớ (Bộ nhớ Graph) (Stop_all ghi nhận ở bên trong) try: ZepGraphMemoryManager.stop_all() except Exception as e: - logger.error(f"停止图谱记忆更新器失败: {e}") + logger.error(f"Failed to stop Zep Graph update daemon: {e}") cls._graph_memory_enabled.clear() - # 复制字典以避免在迭代时修改 + # Tạo bản sao Dictionary dict() từ cls._processes.items để không bị hỏng List lúc lặp (iterating) processes = list(cls._processes.items()) for simulation_id, process in processes: try: - if process.poll() is None: # 进程仍在运行 - logger.info(f"终止模拟进程: {simulation_id}, pid={process.pid}") + if process.poll() is None: # Process (Tiến trình) Vẫn đang chạy + logger.info(f"Terminating simulation process: {simulation_id}, pid={process.pid}") try: - # 使用跨平台的进程终止方法 + # Áp dụng giải pháp dừng liên nền tảng (Cross-platform termination method) cls._terminate_process(process, simulation_id, timeout=5) except (ProcessLookupError, OSError): - # 进程可能已经不存在,尝试直接终止 + # Trong trường hợp có thể các process này đã biến mất ở đâu đó rồi, xóa một cách bắt buộc try: process.terminate() process.wait(timeout=3) except Exception: process.kill() - # 更新 run_state.json + # Cập nhật run_state.json state = cls.get_run_state(simulation_id) if state: state.runner_status = RunnerStatus.STOPPED state.twitter_running = False state.reddit_running = False state.completed_at = datetime.now().isoformat() - state.error = "服务器关闭,模拟被终止" + state.error = "Server shut down, simulation terminated." cls._save_run_state(state) - # 同时更新 state.json,将状态设为 stopped + # Đồng thời cập nhật trạng thái `stopped` cho tệp (file) state.json try: sim_dir = os.path.join(cls.RUN_STATE_DIR, simulation_id) state_file = os.path.join(sim_dir, "state.json") - logger.info(f"尝试更新 state.json: {state_file}") + logger.info(f"Attempting to update state.json: {state_file}") if os.path.exists(state_file): with open(state_file, 'r', encoding='utf-8') as f: state_data = json.load(f) @@ -1247,16 +1247,16 @@ def cleanup_all_simulations(cls): state_data['updated_at'] = datetime.now().isoformat() with open(state_file, 'w', encoding='utf-8') as f: json.dump(state_data, f, indent=2, ensure_ascii=False) - logger.info(f"已更新 state.json 状态为 stopped: {simulation_id}") + logger.info(f"Updated state.json status to 'stopped': {simulation_id}") else: - logger.warning(f"state.json 不存在: {state_file}") + logger.warning(f"state.json not found: {state_file}") except Exception as state_err: - logger.warning(f"更新 state.json 失败: {simulation_id}, error={state_err}") + logger.warning(f"Failed to update state.json: {simulation_id}, error={state_err}") except Exception as e: - logger.error(f"清理进程失败: {simulation_id}, error={e}") + logger.error(f"Failed to clean up process: {simulation_id}, error={e}") - # 清理文件句柄 + # Đóng tất cả tệp xử lý file handles (Log file, Errors File) for simulation_id, file_handle in list(cls._stdout_files.items()): try: if file_handle: @@ -1273,89 +1273,89 @@ def cleanup_all_simulations(cls): pass cls._stderr_files.clear() - # 清理内存中的状态 + # Dọn dẹp trạng thái ở trong Ram Memory cls._processes.clear() cls._action_queues.clear() - logger.info("模拟进程清理完成") + logger.info("Simulation process clean up completed.") @classmethod def register_cleanup(cls): """ - 注册清理函数 + Đăng ký một lệnh Dọn dẹp (Cleanup command) - 在 Flask 应用启动时调用,确保服务器关闭时清理所有模拟进程 + Trong lúc chuẩn bị khởi tạo App Flask, mình sẽ thiết lập nó sao cho gọi là máy chủ kết thúc (tắt) mọi quá trình (Simulation Process) """ global _cleanup_registered if _cleanup_registered: return - # Flask debug 模式下,只在 reloader 子进程中注册清理(实际运行应用的进程) - # WERKZEUG_RUN_MAIN=true 表示是 reloader 子进程 - # 如果不是 debug 模式,则没有这个环境变量,也需要注册 + # Flask ở trong cơ chế gỡ rối `debug`, lúc này chỉ đăng ký ứng dụng để cho thằng app.run làm (Werkzeug) + # WERKZEUG_RUN_MAIN=true Đại diện quá trình tiến trình máy chủ được nạp lại + # Nhưng nó sẽ ko apply điều này ở Production nếu app không debug is_reloader_process = os.environ.get('WERKZEUG_RUN_MAIN') == 'true' is_debug_mode = os.environ.get('FLASK_DEBUG') == '1' or os.environ.get('WERKZEUG_RUN_MAIN') is not None - # 在 debug 模式下,只在 reloader 子进程中注册;非 debug 模式下始终注册 + # Trong DebugMode, chúng ta chỉ cho phép re-loader Child-process chạy. Production vẫn luôn phải chạy process này if is_debug_mode and not is_reloader_process: - _cleanup_registered = True # 标记已注册,防止子进程再次尝试 + _cleanup_registered = True # Check list đã lưu lại. Hủy quyền yêu cầu thêm return - # 保存原有的信号处理器 + # Lưu các tín hiệu để trả về Signal handling sau khi dừng Process hoàn tất original_sigint = signal.getsignal(signal.SIGINT) original_sigterm = signal.getsignal(signal.SIGTERM) - # SIGHUP 只在 Unix 系统存在(macOS/Linux),Windows 没有 + # SIGHUP Chỉ xuất hiện trong Unix (Mac/Linux), Windows không có cái này original_sighup = None has_sighup = hasattr(signal, 'SIGHUP') if has_sighup: original_sighup = signal.getsignal(signal.SIGHUP) def cleanup_handler(signum=None, frame=None): - """信号处理器:先清理模拟进程,再调用原处理器""" - # 只有在有进程需要清理时才打印日志 + """Xử lý điều hướng tín hiệu (Signal Routing): Bắt đầu dọn tiến trình xong gởi lệnh báo (Original Processing Router)""" + # Chỉ báo nhật kí (log) nếu có tiến trình (Process) cần xử lý if cls._processes or cls._graph_memory_enabled: - logger.info(f"收到信号 {signum},开始清理...") + logger.info(f"Received signal {signum}, starting clean up...") cls.cleanup_all_simulations() - # 调用原有的信号处理器,让 Flask 正常退出 + # Gửi tín hiệu gọi hàm báo (handling functions) lúc đấy của Flask => App được tự do ngắt điện if signum == signal.SIGINT and callable(original_sigint): original_sigint(signum, frame) elif signum == signal.SIGTERM and callable(original_sigterm): original_sigterm(signum, frame) elif has_sighup and signum == signal.SIGHUP: - # SIGHUP: 终端关闭时发送 + # SIGHUP: Được trả về khi máy chủ bị dừng (Terminal Closed) if callable(original_sighup): original_sighup(signum, frame) else: - # 默认行为:正常退出 + # Mặc định hành vi: Đóng bình thường => sys.exit(0) "Tạm biệt các hành khách" sys.exit(0) else: - # 如果原处理器不可调用(如 SIG_DFL),则使用默认行为 + # Hành động ở cơ sở gốc (Root) không gọi được (SIG_DFL) => Hãy phát cảnh báo raise KeyboardInterrupt - # 注册 atexit 处理器(作为备用) + # Một phương án khác nếu tín hiệu đăng ký xử lý gặp khó (Fallback Option) atexit.register(cls.cleanup_all_simulations) - # 注册信号处理器(仅在主线程中) + # Đăng ký quản lý báo tín hiệu (Chỉ riêng trong chủ / main thread có cái luồng) try: - # SIGTERM: kill 命令默认信号 + # SIGTERM: Tín hiệu gốc của Kill Server (Linux/Mac) signal.signal(signal.SIGTERM, cleanup_handler) - # SIGINT: Ctrl+C + # SIGINT: Bấm lệnh Control + C / Ctrl+C signal.signal(signal.SIGINT, cleanup_handler) - # SIGHUP: 终端关闭(仅 Unix 系统) + # SIGHUP: Máy bị đóng (Unix OS) if has_sighup: signal.signal(signal.SIGHUP, cleanup_handler) except ValueError: - # 不在主线程中,只能使用 atexit - logger.warning("无法注册信号处理器(不在主线程),仅使用 atexit") + # Không ở MainThread => Sử dụng được duy nhất fallback Atexit + logger.warning("Failed to register signal handlers (not in main thread). Falling back to atexit.") _cleanup_registered = True @classmethod def get_running_simulations(cls) -> List[str]: """ - 获取所有正在运行的模拟ID列表 + Lấy danh sách tất cả các ID của các phiên mô phỏng đang hoạt động """ running = [] for sim_id, process in cls._processes.items(): @@ -1363,18 +1363,18 @@ def get_running_simulations(cls) -> List[str]: running.append(sim_id) return running - # ============== Interview 功能 ============== + # ============== Tính năng Phỏng vấn (Interview) ============== @classmethod def check_env_alive(cls, simulation_id: str) -> bool: """ - 检查模拟环境是否存活(可以接收Interview命令) + Kiểm tra xem environment còn sống không (có thể nhận lệnh Interview) Args: - simulation_id: 模拟ID + simulation_id: Simulation ID Returns: - True 表示环境存活,False 表示环境已关闭 + True Nếu environment còn sống, False nghĩa là đã đóng """ sim_dir = os.path.join(cls.RUN_STATE_DIR, simulation_id) if not os.path.exists(sim_dir): @@ -1386,13 +1386,13 @@ def check_env_alive(cls, simulation_id: str) -> bool: @classmethod def get_env_status_detail(cls, simulation_id: str) -> Dict[str, Any]: """ - 获取模拟环境的详细状态信息 + Lấy thông tin chi tiết về trạng thái của environment Args: - simulation_id: 模拟ID + simulation_id: Mô phỏng ID Returns: - 状态详情字典,包含 status, twitter_available, reddit_available, timestamp + Bảng trạng thái chi tiết (Dictionary) bao gồm: status, twitter_available, reddit_available, timestamp """ sim_dir = os.path.join(cls.RUN_STATE_DIR, simulation_id) status_file = os.path.join(sim_dir, "env_status.json") @@ -1429,35 +1429,35 @@ def interview_agent( timeout: float = 60.0 ) -> Dict[str, Any]: """ - 采访单个Agent + Phỏng vấn trên 1 Agent Args: - simulation_id: 模拟ID + simulation_id: ID mô phỏng agent_id: Agent ID - prompt: 采访问题 - platform: 指定平台(可选) - - "twitter": 只采访Twitter平台 - - "reddit": 只采访Reddit平台 - - None: 双平台模拟时同时采访两个平台,返回整合结果 - timeout: 超时时间(秒) + prompt: Câu hỏi phỏng vấn + platform: Chỉ định nền tảng (Tùy chọn/Optional) + - "twitter": Chỉ PV trên account Twitter + - "reddit": Chỉ PV trên account Reddit + - None: Phỏng vấn chéo trên cả hai nền tảng, trả về kết quả hợp lại (Nếu chạy mô phỏng nền tảng kép) + timeout: Thời gian chờ tối đa (giây) Returns: - 采访结果字典 + Từ điền chứa kết quả PV Raises: - ValueError: 模拟不存在或环境未运行 - TimeoutError: 等待响应超时 + ValueError: Không có mô phỏng hoặc environment ko chạy + TimeoutError: Đang chờ phản hồi bị timeout """ sim_dir = os.path.join(cls.RUN_STATE_DIR, simulation_id) if not os.path.exists(sim_dir): - raise ValueError(f"模拟不存在: {simulation_id}") + raise ValueError(f"Simulation does not exist: {simulation_id}") ipc_client = SimulationIPCClient(sim_dir) if not ipc_client.check_env_alive(): - raise ValueError(f"模拟环境未运行或已关闭,无法执行Interview: {simulation_id}") + raise ValueError(f"Simulation environment is not running or closed, cannot perform Interview: {simulation_id}") - logger.info(f"发送Interview命令: simulation_id={simulation_id}, agent_id={agent_id}, platform={platform}") + logger.info(f"Sending Interview Command: simulation_id={simulation_id}, agent_id={agent_id}, platform={platform}") response = ipc_client.send_interview( agent_id=agent_id, @@ -1492,34 +1492,34 @@ def interview_agents_batch( timeout: float = 120.0 ) -> Dict[str, Any]: """ - 批量采访多个Agent + Phỏng vấn hàng loạt nhiều Agent Args: - simulation_id: 模拟ID - interviews: 采访列表,每个元素包含 {"agent_id": int, "prompt": str, "platform": str(可选)} - platform: 默认平台(可选,会被每个采访项的platform覆盖) - - "twitter": 默认只采访Twitter平台 - - "reddit": 默认只采访Reddit平台 - - None: 双平台模拟时每个Agent同时采访两个平台 - timeout: 超时时间(秒) + simulation_id: ID mô phỏng + interviews: Danh sách nội dung phỏng vấn, mỗi phần tử (element) chứa {"agent_id": int, "prompt": str, "platform": str(tùy chọn)} + platform: Nền tảng mặc định (Nếu không chọn riêng cho từng phần tử) + - "twitter": Mặc định chỉ dùng mạng Twitter + - "reddit": Mặc định chỉ dùng mạng Reddit + - None: Phỏng vấn gộp trên cả hai nền tảng với mỗi Agent + timeout: Hết thời gian chờ (ms) (seconds) Returns: - 批量采访结果字典 + Dict từ điển với các kết quả phỏng vấn hàng loạt Raises: - ValueError: 模拟不存在或环境未运行 - TimeoutError: 等待响应超时 + ValueError: Chưa có tiến trình chạy mô phỏng + TimeoutError: Phỏng vấn lâu quá (Timeout timeout timeout) """ sim_dir = os.path.join(cls.RUN_STATE_DIR, simulation_id) if not os.path.exists(sim_dir): - raise ValueError(f"模拟不存在: {simulation_id}") + raise ValueError(f"Simulation does not exist: {simulation_id}") ipc_client = SimulationIPCClient(sim_dir) if not ipc_client.check_env_alive(): - raise ValueError(f"模拟环境未运行或已关闭,无法执行Interview: {simulation_id}") + raise ValueError(f"Simulation environment is not running or closed, cannot perform Interview: {simulation_id}") - logger.info(f"发送批量Interview命令: simulation_id={simulation_id}, count={len(interviews)}, platform={platform}") + logger.info(f"Sending batch Interview command: simulation_id={simulation_id}, count={len(interviews)}, platform={platform}") response = ipc_client.send_batch_interview( interviews=interviews, @@ -1551,39 +1551,39 @@ def interview_all_agents( timeout: float = 180.0 ) -> Dict[str, Any]: """ - 采访所有Agent(全局采访) + Phỏng vấn TOÀN BỘ Agent (Phỏng vấn tổng quan - Global interview) - 使用相同的问题采访模拟中的所有Agent + Hỏi một câu hỏi với toàn bộ Agent đang có trong phiên mô phỏng hiện tại Args: - simulation_id: 模拟ID - prompt: 采访问题(所有Agent使用相同问题) - platform: 指定平台(可选) - - "twitter": 只采访Twitter平台 - - "reddit": 只采访Reddit平台 - - None: 双平台模拟时每个Agent同时采访两个平台 - timeout: 超时时间(秒) + simulation_id: ID mô phỏng + prompt: Câu hỏi (Cho tất cả các Agent) + platform: Quyết định nền tảng (Platform decision) + - "twitter": Trỏ tới Twitter Platform + - "reddit": Trỏ tới Reddit Platform + - None: Interview kết hợp trên cả nền tảng của từng agent + timeout: Timeout Returns: - 全局采访结果字典 + Kết quả của toàn thể hội đồng Agents (Lớp/Nhóm) """ sim_dir = os.path.join(cls.RUN_STATE_DIR, simulation_id) if not os.path.exists(sim_dir): - raise ValueError(f"模拟不存在: {simulation_id}") + raise ValueError(f"Simulation does not exist: {simulation_id}") - # 从配置文件获取所有Agent信息 + # Fetch All Agents profile (Lấy thông tin agents từ phần thiết lập) config_path = os.path.join(sim_dir, "simulation_config.json") if not os.path.exists(config_path): - raise ValueError(f"模拟配置不存在: {simulation_id}") + raise ValueError(f"Simulation config not found: {simulation_id}") with open(config_path, 'r', encoding='utf-8') as f: config = json.load(f) agent_configs = config.get("agent_configs", []) if not agent_configs: - raise ValueError(f"模拟配置中没有Agent: {simulation_id}") + raise ValueError(f"No Agent defined under simulation configs: {simulation_id}") - # 构建批量采访列表 + # Tập hợp danh sách phỏng vấn tất cả interviews = [] for agent_config in agent_configs: agent_id = agent_config.get("agent_id") @@ -1593,7 +1593,7 @@ def interview_all_agents( "prompt": prompt }) - logger.info(f"发送全局Interview命令: simulation_id={simulation_id}, agent_count={len(interviews)}, platform={platform}") + logger.info(f"Sending GLOBAL Interview command: simulation_id={simulation_id}, agent_count={len(interviews)}, platform={platform}") return cls.interview_agents_batch( simulation_id=simulation_id, @@ -1609,45 +1609,50 @@ def close_simulation_env( timeout: float = 30.0 ) -> Dict[str, Any]: """ - 关闭模拟环境(而不是停止模拟进程) + Đóng Environment giả lập (Không phải dừng process tắt hẳn nó đi) - 向模拟发送关闭环境命令,使其优雅退出等待命令模式 + Gửi lệnh ra hiệu cho Simulation ngắt bỏ tiến trình để các processes thoát ra an toàn và êm đẹp về trạng thái đang chờ nhận lệnh Args: - simulation_id: 模拟ID - timeout: 超时时间(秒) + simulation_id: ID Simulation + timeout: Timeout chờ kết nối Returns: - 操作结果字典 + Kiểu từ điển: Quá trình (Process Status execution) """ sim_dir = os.path.join(cls.RUN_STATE_DIR, simulation_id) if not os.path.exists(sim_dir): - raise ValueError(f"模拟不存在: {simulation_id}") + raise ValueError(f"Simulation does not exist: {simulation_id}") ipc_client = SimulationIPCClient(sim_dir) if not ipc_client.check_env_alive(): return { "success": True, - "message": "环境已经关闭" + "message": "Environment is already closed" } - logger.info(f"发送关闭环境命令: simulation_id={simulation_id}") + logger.info(f"Sending command to close Environment: simulation_id={simulation_id}") try: response = ipc_client.send_close_env(timeout=timeout) return { "success": response.status.value == "completed", - "message": "环境关闭命令已发送", + "message": "Environment close command sent", "result": response.result, "timestamp": response.timestamp } except TimeoutError: - # 超时可能是因为环境正在关闭 + # Hết thời gian chờ nguyên nhân lớn nhất là vì Simulation environment đang đóng giữa chừng. return { "success": True, - "message": "环境关闭命令已发送(等待响应超时,环境可能正在关闭)" + "message": "Environment close command sent (timeout waiting for response, env might be closing)" + } + except Exception as e: + return { + "success": False, + "message": f"Failed to send close env command: {str(e)}" } @classmethod @@ -1658,7 +1663,7 @@ def _get_interview_history_from_db( agent_id: Optional[int] = None, limit: int = 100 ) -> List[Dict[str, Any]]: - """从单个数据库获取Interview历史""" + """Lấy lịch sử phỏng vấn từ Local Database của nền tảng""" import sqlite3 if not os.path.exists(db_path): @@ -1704,7 +1709,7 @@ def _get_interview_history_from_db( conn.close() except Exception as e: - logger.error(f"读取Interview历史失败 ({platform_name}): {e}") + logger.error(f"Failed to load Interview history ({platform_name}): {e}") return results @@ -1717,29 +1722,29 @@ def get_interview_history( limit: int = 100 ) -> List[Dict[str, Any]]: """ - 获取Interview历史记录(从数据库读取) + Lịch sử lấy danh sách câu trả lời của các câu hỏi với agents (Đọc từ DataBase db) Args: - simulation_id: 模拟ID - platform: 平台类型(reddit/twitter/None) - - "reddit": 只获取Reddit平台的历史 - - "twitter": 只获取Twitter平台的历史 - - None: 获取两个平台的所有历史 - agent_id: 指定Agent ID(可选,只获取该Agent的历史) - limit: 每个平台返回数量限制 + simulation_id: Nhận dạng ID cho mỗi Simulation + platform: Chị định Nền tảng (reddit/twitter/None) + - "reddit": Chỉ trên reddit + - "twitter": Chỉ lấy records ghi được trên mạng xã hội twitter giả lập + - None: Kết hợp lấy logs của cả hai social network + agent_id: Cung cấp tùy chọn cho loại Agent qua ID + limit: Lượng dữ liệu load tối đa cho 1 request get query trên 1 nền tảng Returns: - Interview历史记录列表 + Danh sách lưu vết record lịch sử phỏng vấn của các agents """ sim_dir = os.path.join(cls.RUN_STATE_DIR, simulation_id) results = [] - # 确定要查询的平台 + # Xác nhận nền tảng cung cấp cho truy vấn if platform in ("reddit", "twitter"): platforms = [platform] else: - # 不指定platform时,查询两个平台 + # Nếu người dùng để trống, có nghĩa là gọi tất cả kết quả platforms = ["twitter", "reddit"] for p in platforms: @@ -1752,10 +1757,10 @@ def get_interview_history( ) results.extend(platform_results) - # 按时间降序排序 + # Sắp xếp chúng lại bằng thời gian gần nhất lên trước results.sort(key=lambda x: x.get("timestamp", ""), reverse=True) - # 如果查询了多个平台,限制总数 + # Cho trường hợp query kết hợp nhiều nền tảng, phải tiến hành gọt lấy đúng 1 giới hạn nhất định if len(platforms) > 1 and len(results) > limit: results = results[:limit] diff --git a/backend/app/services/text_processor.py b/backend/app/services/text_processor.py index 91e32acc5..5e9d0c283 100644 --- a/backend/app/services/text_processor.py +++ b/backend/app/services/text_processor.py @@ -1,5 +1,5 @@ """ -文本处理服务 +Dịch vụ xử lý văn bản (Text processing service) """ from typing import List, Optional @@ -7,11 +7,11 @@ class TextProcessor: - """文本处理器""" + """Trình xử lý văn bản""" @staticmethod def extract_from_files(file_paths: List[str]) -> str: - """从多个文件提取文本""" + """Trích xuất và kết hợp văn bản từ nhiều file (các đường dẫn file truyền vào)""" return FileParser.extract_from_multiple(file_paths) @staticmethod @@ -21,37 +21,37 @@ def split_text( overlap: int = 50 ) -> List[str]: """ - 分割文本 + Chia nhỏ văn bản (Phân mảnh văn bản dài thành các đoạn nhỏ hơn - chunking) Args: - text: 原始文本 - chunk_size: 块大小 - overlap: 重叠大小 + text: Văn bản gốc cần chia + chunk_size: Kích thước tối đa của mỗi đoạn (chunk) + overlap: Số ký tự chồng chéo giữa các đoạn liên tiếp (để giữ bối cảnh không bị đứt đoạn) Returns: - 文本块列表 + Danh sách gồm các mảng văn bản sau khi chia """ return split_text_into_chunks(text, chunk_size, overlap) @staticmethod def preprocess_text(text: str) -> str: """ - 预处理文本 - - 移除多余空白 - - 标准化换行 + Tiền xử lý văn bản (Làm sạch văn bản) + - Xoá bỏ các khoảng trắng thừa + - Chuẩn hoá định dạng xuống dòng (Line breaks) Args: - text: 原始文本 + text: Văn bản thô ban đầu Returns: - 处理后的文本 + Văn bản đã được tinh giản, làm sạch """ import re - # 标准化换行 + # Chuẩn hoá ký tự xuống dòng (Windows \r\n hoặc Mac cũ \r thành \n chuẩn Linux) text = text.replace('\r\n', '\n').replace('\r', '\n') - # 移除连续空行(保留最多两个换行) + # Xoá các dòng trống liên tiếp (Chỉ giữ lại tối đa 2 lần xuống dòng liên tiếp) text = re.sub(r'\n{3,}', '\n\n', text) # 移除行首行尾空白 @@ -62,7 +62,7 @@ def preprocess_text(text: str) -> str: @staticmethod def get_text_stats(text: str) -> dict: - """获取文本统计信息""" + """Lấy một số thông tin thống kê về đoạn văn bản (Số ký tự, số dòng, số từ)""" return { "total_chars": len(text), "total_lines": text.count('\n') + 1, diff --git a/backend/app/services/zep_entity_reader.py b/backend/app/services/zep_entity_reader.py index 71661be49..38d9bdca5 100644 --- a/backend/app/services/zep_entity_reader.py +++ b/backend/app/services/zep_entity_reader.py @@ -1,6 +1,6 @@ """ -Zep实体读取与过滤服务 -从Zep图谱中读取节点,筛选出符合预定义实体类型的节点 +Dịch vụ đọc và lọc thực thể Zep +Đọc các node từ đồ thị Zep, lọc ra các node phù hợp với các loại thực thể đã được định nghĩa trước """ import time @@ -15,21 +15,21 @@ logger = get_logger('mirofish.zep_entity_reader') -# 用于泛型返回类型 +# Dùng cho các kiểu trả về generic T = TypeVar('T') @dataclass class EntityNode: - """实体节点数据结构""" + """Cấu trúc dữ liệu của node thực thể""" uuid: str name: str labels: List[str] summary: str attributes: Dict[str, Any] - # 相关的边信息 + # Thông tin edge liên quan related_edges: List[Dict[str, Any]] = field(default_factory=list) - # 相关的其他节点信息 + # Thông tin các node khác liên quan related_nodes: List[Dict[str, Any]] = field(default_factory=list) def to_dict(self) -> Dict[str, Any]: @@ -44,7 +44,7 @@ def to_dict(self) -> Dict[str, Any]: } def get_entity_type(self) -> Optional[str]: - """获取实体类型(排除默认的Entity标签)""" + """Lấy loại thực thể (loại trừ nhãn Entity mặc định)""" for label in self.labels: if label not in ["Entity", "Node"]: return label @@ -53,7 +53,7 @@ def get_entity_type(self) -> Optional[str]: @dataclass class FilteredEntities: - """过滤后的实体集合""" + """Tập hợp các thực thể sau khi lọc""" entities: List[EntityNode] entity_types: Set[str] total_count: int @@ -70,18 +70,18 @@ def to_dict(self) -> Dict[str, Any]: class ZepEntityReader: """ - Zep实体读取与过滤服务 + Dịch vụ đọc và lọc thực thể Zep - 主要功能: - 1. 从Zep图谱读取所有节点 - 2. 筛选出符合预定义实体类型的节点(Labels不只是Entity的节点) - 3. 获取每个实体的相关边和关联节点信息 + Chức năng chính: + 1. Đọc toàn bộ các node từ đồ thị Zep + 2. Lọc ra các node phù hợp với các loại thực thể đã được định nghĩa (Các node có Labels không chỉ là Entity) + 3. Lấy ra thông tin edge cũng như các node liên quan đối với từng thực thể """ def __init__(self, api_key: Optional[str] = None): self.api_key = api_key or Config.ZEP_API_KEY if not self.api_key: - raise ValueError("ZEP_API_KEY 未配置") + raise ValueError("ZEP_API_KEY is not configured") self.client = Zep(api_key=self.api_key) @@ -93,16 +93,16 @@ def _call_with_retry( initial_delay: float = 2.0 ) -> T: """ - 带重试机制的Zep API调用 + Gọi hàm Zep API có cơ chế thử lại (retry) Args: - func: 要执行的函数(无参数的lambda或callable) - operation_name: 操作名称,用于日志 - max_retries: 最大重试次数(默认3次,即最多尝试3次) - initial_delay: 初始延迟秒数 + func: Hàm cần thực thi (lambda không tham số hoặc callable) + operation_name: Tên thao tác, dùng cho log + max_retries: Số lần thử lại tối đa (mặc định 3 lần, tức là thử tối đa 3 lần) + initial_delay: Số giây trì hoãn ban đầu Returns: - API调用结果 + Kết quả của lệnh gọi API """ last_exception = None delay = initial_delay @@ -114,27 +114,27 @@ def _call_with_retry( last_exception = e if attempt < max_retries - 1: logger.warning( - f"Zep {operation_name} 第 {attempt + 1} 次尝试失败: {str(e)[:100]}, " - f"{delay:.1f}秒后重试..." + f"Zep {operation_name} attempt {attempt + 1} failed: {str(e)[:100]}, " + f"retrying in {delay:.1f} seconds..." ) time.sleep(delay) - delay *= 2 # 指数退避 + delay *= 2 # Lùi bước nhịp mũ (Exponential backoff) else: - logger.error(f"Zep {operation_name} 在 {max_retries} 次尝试后仍失败: {str(e)}") + logger.error(f"Zep {operation_name} failed after {max_retries} attempts: {str(e)}") raise last_exception def get_all_nodes(self, graph_id: str) -> List[Dict[str, Any]]: """ - 获取图谱的所有节点(分页获取) + Lấy toàn bộ các node của đồ thị (có phân trang) Args: - graph_id: 图谱ID + graph_id: ID của đồ thị Returns: - 节点列表 + Danh sách node """ - logger.info(f"获取图谱 {graph_id} 的所有节点...") + logger.info(f"Fetching all nodes for graph {graph_id}...") nodes = fetch_all_nodes(self.client, graph_id) @@ -148,20 +148,20 @@ def get_all_nodes(self, graph_id: str) -> List[Dict[str, Any]]: "attributes": node.attributes or {}, }) - logger.info(f"共获取 {len(nodes_data)} 个节点") + logger.info(f"Total {len(nodes_data)} nodes fetched") return nodes_data def get_all_edges(self, graph_id: str) -> List[Dict[str, Any]]: """ - 获取图谱的所有边(分页获取) + Lấy toàn bộ các edge của đồ thị (có phân trang) Args: - graph_id: 图谱ID + graph_id: ID của đồ thị Returns: - 边列表 + Danh sách edge """ - logger.info(f"获取图谱 {graph_id} 的所有边...") + logger.info(f"Fetching all edges for graph {graph_id}...") edges = fetch_all_edges(self.client, graph_id) @@ -176,24 +176,24 @@ def get_all_edges(self, graph_id: str) -> List[Dict[str, Any]]: "attributes": edge.attributes or {}, }) - logger.info(f"共获取 {len(edges_data)} 条边") + logger.info(f"Total {len(edges_data)} edges fetched") return edges_data def get_node_edges(self, node_uuid: str) -> List[Dict[str, Any]]: """ - 获取指定节点的所有相关边(带重试机制) + Lấy tất cả các edge liên quan của node được chỉ định (có cơ chế thử lại) Args: - node_uuid: 节点UUID + node_uuid: UUID của node Returns: - 边列表 + Danh sách edge """ try: - # 使用重试机制调用Zep API + # Sử dụng cơ chế thử lại để gọi Zep API edges = self._call_with_retry( func=lambda: self.client.graph.node.get_entity_edges(node_uuid=node_uuid), - operation_name=f"获取节点边(node={node_uuid[:8]}...)" + operation_name=f"Fetch node edges(node={node_uuid[:8]}...)" ) edges_data = [] @@ -209,7 +209,7 @@ def get_node_edges(self, node_uuid: str) -> List[Dict[str, Any]]: return edges_data except Exception as e: - logger.warning(f"获取节点 {node_uuid} 的边失败: {str(e)}") + logger.warning(f"Failed to fetch edges for node {node_uuid}: {str(e)}") return [] def filter_defined_entities( @@ -219,47 +219,47 @@ def filter_defined_entities( enrich_with_edges: bool = True ) -> FilteredEntities: """ - 筛选出符合预定义实体类型的节点 + Lọc ra các node phù hợp với các loại thực thể đã được định nghĩa - 筛选逻辑: - - 如果节点的Labels只有一个"Entity",说明这个实体不符合我们预定义的类型,跳过 - - 如果节点的Labels包含除"Entity"和"Node"之外的标签,说明符合预定义类型,保留 + Logic lọc: + - Nếu Labels của node chỉ có một nhãn là "Entity", tức là thực thể này không hợp với loại chúng ta định nghĩa, tiến hành bỏ qua + - Nếu Labels của node chứa các nhãn khác ngoài "Entity" và "Node", tức là hợp lệ, tiến hành giữ lại Args: - graph_id: 图谱ID - defined_entity_types: 预定义的实体类型列表(可选,如果提供则只保留这些类型) - enrich_with_edges: 是否获取每个实体的相关边信息 + graph_id: ID của đồ thị + defined_entity_types: Danh sách các loại thực thể định nghĩa trước (không bắt buộc, nếu có thì chỉ giữ lại các loại đó) + enrich_with_edges: Có lấy thông tin edge liên quan của từng thực thể hay không Returns: - FilteredEntities: 过滤后的实体集合 + FilteredEntities: Tập hợp các thực thể sau khi lọc """ - logger.info(f"开始筛选图谱 {graph_id} 的实体...") + logger.info(f"Start filtering entities for graph {graph_id}...") - # 获取所有节点 + # Lấy toàn bộ các node all_nodes = self.get_all_nodes(graph_id) total_count = len(all_nodes) - # 获取所有边(用于后续关联查找) + # Lấy toàn bộ các edge (để lấy liên kết sau này) all_edges = self.get_all_edges(graph_id) if enrich_with_edges else [] - # 构建节点UUID到节点数据的映射 + # Xây dựng map ánh xạ từ UUID của node sang dữ liệu node node_map = {n["uuid"]: n for n in all_nodes} - # 筛选符合条件的实体 + # Lọc các thực thể đáp ứng điều kiện filtered_entities = [] entity_types_found = set() for node in all_nodes: labels = node.get("labels", []) - # 筛选逻辑:Labels必须包含除"Entity"和"Node"之外的标签 + # Logic lọc: Labels bắt buộc phải chứa các nhãn khác "Entity" và "Node" custom_labels = [l for l in labels if l not in ["Entity", "Node"]] if not custom_labels: - # 只有默认标签,跳过 + # Chỉ có nhãn mặc định, bỏ qua continue - # 如果指定了预定义类型,检查是否匹配 + # Nếu đã chỉ định loại thực thể cho trước, kiểm tra xem có khớp hay không if defined_entity_types: matching_labels = [l for l in custom_labels if l in defined_entity_types] if not matching_labels: @@ -270,7 +270,7 @@ def filter_defined_entities( entity_types_found.add(entity_type) - # 创建实体节点对象 + # Tạo object cho node thực thể entity = EntityNode( uuid=node["uuid"], name=node["name"], @@ -279,7 +279,7 @@ def filter_defined_entities( attributes=node["attributes"], ) - # 获取相关边和节点 + # Lấy các edge và node liên quan if enrich_with_edges: related_edges = [] related_node_uuids = set() @@ -304,7 +304,7 @@ def filter_defined_entities( entity.related_edges = related_edges - # 获取关联节点的基本信息 + # Lấy thông tin cơ bản của các node được liên kết related_nodes = [] for related_uuid in related_node_uuids: if related_uuid in node_map: @@ -320,8 +320,8 @@ def filter_defined_entities( filtered_entities.append(entity) - logger.info(f"筛选完成: 总节点 {total_count}, 符合条件 {len(filtered_entities)}, " - f"实体类型: {entity_types_found}") + logger.info(f"Filtering completed: Total nodes {total_count}, Matched {len(filtered_entities)}, " + f"Entity types: {entity_types_found}") return FilteredEntities( entities=filtered_entities, @@ -336,33 +336,33 @@ def get_entity_with_context( entity_uuid: str ) -> Optional[EntityNode]: """ - 获取单个实体及其完整上下文(边和关联节点,带重试机制) + Lấy thông tin của một thực thể cụ thể và ngữ cảnh đầy đủ của nó (edge và node liên kết, với cơ chế thử lại) Args: - graph_id: 图谱ID - entity_uuid: 实体UUID + graph_id: ID của đồ thị + entity_uuid: UUID của thực thể Returns: - EntityNode或None + EntityNode hoặc None """ try: - # 使用重试机制获取节点 + # Sử dụng cơ chế thử lại để lấy thông tin node node = self._call_with_retry( func=lambda: self.client.graph.node.get(uuid_=entity_uuid), - operation_name=f"获取节点详情(uuid={entity_uuid[:8]}...)" + operation_name=f"Fetch node detail(uuid={entity_uuid[:8]}...)" ) if not node: return None - # 获取节点的边 + # Lấy các edge của node edges = self.get_node_edges(entity_uuid) - # 获取所有节点用于关联查找 + # Lấy tất cả các node để tìm liên kết all_nodes = self.get_all_nodes(graph_id) node_map = {n["uuid"]: n for n in all_nodes} - # 处理相关边和节点 + # Xử lý các edge và node liên quan related_edges = [] related_node_uuids = set() @@ -384,7 +384,7 @@ def get_entity_with_context( }) related_node_uuids.add(edge["source_node_uuid"]) - # 获取关联节点信息 + # Lấy thông tin về node được liên kết related_nodes = [] for related_uuid in related_node_uuids: if related_uuid in node_map: @@ -407,7 +407,7 @@ def get_entity_with_context( ) except Exception as e: - logger.error(f"获取实体 {entity_uuid} 失败: {str(e)}") + logger.error(f"Failed to fetch entity {entity_uuid}: {str(e)}") return None def get_entities_by_type( @@ -417,15 +417,15 @@ def get_entities_by_type( enrich_with_edges: bool = True ) -> List[EntityNode]: """ - 获取指定类型的所有实体 + Lấy tất cả các thực thể dựa theo loại cụ thể Args: - graph_id: 图谱ID - entity_type: 实体类型(如 "Student", "PublicFigure" 等) - enrich_with_edges: 是否获取相关边信息 + graph_id: ID của đồ thị + entity_type: Loại thực thể (ví dụ: "Student", "PublicFigure", v.v..) + enrich_with_edges: Có lấy thông tin edge liên quan hay không Returns: - 实体列表 + Danh sách thực thể """ result = self.filter_defined_entities( graph_id=graph_id, diff --git a/backend/app/services/zep_graph_memory_updater.py b/backend/app/services/zep_graph_memory_updater.py index a8f3cecd9..bf8004011 100644 --- a/backend/app/services/zep_graph_memory_updater.py +++ b/backend/app/services/zep_graph_memory_updater.py @@ -1,6 +1,6 @@ """ -Zep图谱记忆更新服务 -将模拟中的Agent活动动态更新到Zep图谱中 +Dịch vụ cập nhật bộ nhớ đồ thị Zep +Cập nhật động các hoạt động của Agent trong mô phỏng lên đồ thị Zep """ import os @@ -22,7 +22,7 @@ @dataclass class AgentActivity: - """Agent活动记录""" + """Bản ghi hoạt động của Agent""" platform: str # twitter / reddit agent_id: int agent_name: str @@ -33,12 +33,12 @@ class AgentActivity: def to_episode_text(self) -> str: """ - 将活动转换为可以发送给Zep的文本描述 + Chuyển đổi hoạt động thành mô tả văn bản để gửi cho Zep - 采用自然语言描述格式,让Zep能够从中提取实体和关系 - 不添加模拟相关的前缀,避免误导图谱更新 + Sử dụng định dạng mô tả bằng ngôn ngữ tự nhiên để Zep có thể trích xuất thực thể và mối quan hệ + Không thêm tiền tố liên quan đến mô phỏng để tránh gây nhiễu khi cập nhật đồ thị """ - # 根据不同的动作类型生成不同的描述 + # Tạo mô tả khác nhau dựa trên từng loại hành động action_descriptions = { "CREATE_POST": self._describe_create_post, "LIKE_POST": self._describe_like_post, @@ -57,222 +57,222 @@ def to_episode_text(self) -> str: describe_func = action_descriptions.get(self.action_type, self._describe_generic) description = describe_func() - # 直接返回 "agent名称: 活动描述" 格式,不添加模拟前缀 + # Trả về trực tiếp định dạng "Tên agent: Mô tả hoạt động", không thêm tiền tố mô phỏng return f"{self.agent_name}: {description}" def _describe_create_post(self) -> str: content = self.action_args.get("content", "") if content: - return f"发布了一条帖子:「{content}」" - return "发布了一条帖子" + return f"posted a post: `{content}`" + return "posted a post" def _describe_like_post(self) -> str: - """点赞帖子 - 包含帖子原文和作者信息""" + """Like bài viết - bao gồm nội dung gốc bài viết và thông tin tác giả""" post_content = self.action_args.get("post_content", "") post_author = self.action_args.get("post_author_name", "") if post_content and post_author: - return f"点赞了{post_author}的帖子:「{post_content}」" + return f"liked {post_author}'s post: `{post_content}`" elif post_content: - return f"点赞了一条帖子:「{post_content}」" + return f"liked a post: `{post_content}`" elif post_author: - return f"点赞了{post_author}的一条帖子" - return "点赞了一条帖子" + return f"liked a post by {post_author}" + return "liked a post" def _describe_dislike_post(self) -> str: - """踩帖子 - 包含帖子原文和作者信息""" + """Dislike bài viết - bao gồm nội dung gốc và thông tin tác giả""" post_content = self.action_args.get("post_content", "") post_author = self.action_args.get("post_author_name", "") if post_content and post_author: - return f"踩了{post_author}的帖子:「{post_content}」" + return f"disliked {post_author}'s post: `{post_content}`" elif post_content: - return f"踩了一条帖子:「{post_content}」" + return f"disliked a post: `{post_content}`" elif post_author: - return f"踩了{post_author}的一条帖子" - return "踩了一条帖子" + return f"disliked a post by {post_author}" + return "disliked a post" def _describe_repost(self) -> str: - """转发帖子 - 包含原帖内容和作者信息""" + """Repost bài viết - bao gồm nội dung gốc và thông tin tác giả""" original_content = self.action_args.get("original_content", "") original_author = self.action_args.get("original_author_name", "") if original_content and original_author: - return f"转发了{original_author}的帖子:「{original_content}」" + return f"reposted {original_author}'s post: `{original_content}`" elif original_content: - return f"转发了一条帖子:「{original_content}」" + return f"reposted a post: `{original_content}`" elif original_author: - return f"转发了{original_author}的一条帖子" - return "转发了一条帖子" + return f"reposted a post by {original_author}" + return "reposted a post" def _describe_quote_post(self) -> str: - """引用帖子 - 包含原帖内容、作者信息和引用评论""" + """Quote bài viết - bao gồm nội dung bài gốc, tác giả và nội dung bình luận quote""" original_content = self.action_args.get("original_content", "") original_author = self.action_args.get("original_author_name", "") quote_content = self.action_args.get("quote_content", "") or self.action_args.get("content", "") base = "" if original_content and original_author: - base = f"引用了{original_author}的帖子「{original_content}」" + base = f"quoted {original_author}'s post `{original_content}`" elif original_content: - base = f"引用了一条帖子「{original_content}」" + base = f"quoted a post `{original_content}`" elif original_author: - base = f"引用了{original_author}的一条帖子" + base = f"quoted a post by {original_author}" else: - base = "引用了一条帖子" + base = "quoted a post" if quote_content: - base += f",并评论道:「{quote_content}」" + base += f" and commented: `{quote_content}`" return base def _describe_follow(self) -> str: - """关注用户 - 包含被关注用户的名称""" + """Follow người dùng - bao gồm tên người được follow""" target_user_name = self.action_args.get("target_user_name", "") if target_user_name: - return f"关注了用户「{target_user_name}」" - return "关注了一个用户" + return f"followed user `{target_user_name}`" + return "followed a user" def _describe_create_comment(self) -> str: - """发表评论 - 包含评论内容和所评论的帖子信息""" + """Tạo bình luận - bao gồm nội dung bình luận và thông tin bài viết được bình luận""" content = self.action_args.get("content", "") post_content = self.action_args.get("post_content", "") post_author = self.action_args.get("post_author_name", "") if content: if post_content and post_author: - return f"在{post_author}的帖子「{post_content}」下评论道:「{content}」" + return f"commented under {post_author}'s post `{post_content}`: `{content}`" elif post_content: - return f"在帖子「{post_content}」下评论道:「{content}」" + return f"commented under post `{post_content}`: `{content}`" elif post_author: - return f"在{post_author}的帖子下评论道:「{content}」" - return f"评论道:「{content}」" - return "发表了评论" + return f"commented under {post_author}'s post: `{content}`" + return f"commented: `{content}`" + return "posted a comment" def _describe_like_comment(self) -> str: - """点赞评论 - 包含评论内容和作者信息""" + """Like bình luận - bao gồm nội dung bình luận và tác giả""" comment_content = self.action_args.get("comment_content", "") comment_author = self.action_args.get("comment_author_name", "") if comment_content and comment_author: - return f"点赞了{comment_author}的评论:「{comment_content}」" + return f"liked {comment_author}'s comment: `{comment_content}`" elif comment_content: - return f"点赞了一条评论:「{comment_content}」" + return f"liked a comment: `{comment_content}`" elif comment_author: - return f"点赞了{comment_author}的一条评论" - return "点赞了一条评论" + return f"liked a comment by {comment_author}" + return "liked a comment" def _describe_dislike_comment(self) -> str: - """踩评论 - 包含评论内容和作者信息""" + """Dislike bình luận - bao gồm nội dung bình luận và tác giả""" comment_content = self.action_args.get("comment_content", "") comment_author = self.action_args.get("comment_author_name", "") if comment_content and comment_author: - return f"踩了{comment_author}的评论:「{comment_content}」" + return f"disliked {comment_author}'s comment: `{comment_content}`" elif comment_content: - return f"踩了一条评论:「{comment_content}」" + return f"disliked a comment: `{comment_content}`" elif comment_author: - return f"踩了{comment_author}的一条评论" - return "踩了一条评论" + return f"disliked a comment by {comment_author}" + return "disliked a comment" def _describe_search(self) -> str: - """搜索帖子 - 包含搜索关键词""" + """Tìm kiếm bài viết - bao gồm từ khóa tìm kiếm""" query = self.action_args.get("query", "") or self.action_args.get("keyword", "") - return f"搜索了「{query}」" if query else "进行了搜索" + return f"searched for `{query}`" if query else "performed a search" def _describe_search_user(self) -> str: - """搜索用户 - 包含搜索关键词""" + """Tìm kiếm người dùng - bao gồm từ khóa tìm kiếm""" query = self.action_args.get("query", "") or self.action_args.get("username", "") - return f"搜索了用户「{query}」" if query else "搜索了用户" + return f"searched for user `{query}`" if query else "searched for a user" def _describe_mute(self) -> str: - """屏蔽用户 - 包含被屏蔽用户的名称""" + """Mute người dùng - bao gồm tên người bị mute""" target_user_name = self.action_args.get("target_user_name", "") if target_user_name: - return f"屏蔽了用户「{target_user_name}」" - return "屏蔽了一个用户" + return f"muted user `{target_user_name}`" + return "muted a user" def _describe_generic(self) -> str: - # 对于未知的动作类型,生成通用描述 - return f"执行了{self.action_type}操作" + # Tạo mô tả chung cho các loại hành động không xác định + return f"performed {self.action_type} action" class ZepGraphMemoryUpdater: """ - Zep图谱记忆更新器 + Trình cập nhật bộ nhớ đồ thị Zep - 监控模拟的actions日志文件,将新的agent活动实时更新到Zep图谱中。 - 按平台分组,每累积BATCH_SIZE条活动后批量发送到Zep。 + Giám sát file log actions của mô phỏng, cập nhật trực tiếp các hoạt động mới của agent lên đồ thị Zep. + Nhóm theo nền tảng, gửi hàng loạt lên Zep sau khi tích lũy đủ số lượng hoạt động (BATCH_SIZE). - 所有有意义的行为都会被更新到Zep,action_args中会包含完整的上下文信息: - - 点赞/踩的帖子原文 - - 转发/引用的帖子原文 - - 关注/屏蔽的用户名 - - 点赞/踩的评论原文 + Tất cả các hành vi có ý nghĩa đều được cập nhật lên Zep, action_args chứa đầy đủ thông tin ngữ cảnh: + - Nội dung gốc của bài viết được like/dislike + - Nội dung gốc của bài viết được repost/quote + - Tên người dùng được follow/mute + - Nội dung gốc của bình luận được like/dislike """ - # 批量发送大小(每个平台累积多少条后发送) + # Số lượng gửi mỗi lô (gửi sau khi mỗi nền tảng tích lũy đủ số lượng này) BATCH_SIZE = 5 - # 平台名称映射(用于控制台显示) + # Ánh xạ tên nền tảng (dùng để hiển thị trên console) PLATFORM_DISPLAY_NAMES = { - 'twitter': '世界1', - 'reddit': '世界2', + 'twitter': 'World 1', + 'reddit': 'World 2', } - # 发送间隔(秒),避免请求过快 + # Thời gian gửi cách nhau (giây), tránh request quá nhanh SEND_INTERVAL = 0.5 - # 重试配置 + # Cấu hình thử lại (retry) MAX_RETRIES = 3 - RETRY_DELAY = 2 # 秒 + RETRY_DELAY = 2 # giây def __init__(self, graph_id: str, api_key: Optional[str] = None): """ - 初始化更新器 + Khởi tạo trình cập nhật Args: - graph_id: Zep图谱ID - api_key: Zep API Key(可选,默认从配置读取) + graph_id: ID của đồ thị Zep + api_key: Zep API Key (tự chọn, mặc định lấy từ config) """ self.graph_id = graph_id self.api_key = api_key or Config.ZEP_API_KEY if not self.api_key: - raise ValueError("ZEP_API_KEY未配置") + raise ValueError("ZEP_API_KEY is not configured") self.client = Zep(api_key=self.api_key) - # 活动队列 + # Hàng đợi hoạt động self._activity_queue: Queue = Queue() - # 按平台分组的活动缓冲区(每个平台各自累积到BATCH_SIZE后批量发送) + # Bộ đệm hoạt động nhóm theo nền tảng (gửi hàng loạt sau khi đạt đến BATCH_SIZE) self._platform_buffers: Dict[str, List[AgentActivity]] = { 'twitter': [], 'reddit': [], } self._buffer_lock = threading.Lock() - # 控制标志 + # Cờ điều khiển self._running = False self._worker_thread: Optional[threading.Thread] = None - # 统计 - self._total_activities = 0 # 实际添加到队列的活动数 - self._total_sent = 0 # 成功发送到Zep的批次数 - self._total_items_sent = 0 # 成功发送到Zep的活动条数 - self._failed_count = 0 # 发送失败的批次数 - self._skipped_count = 0 # 被过滤跳过的活动数(DO_NOTHING) + # Thống kê + self._total_activities = 0 # Số lượng hoạt động thực tế được thêm vào hàng đợi + self._total_sent = 0 # Số đợt hàng gửi đi thành công tới Zep + self._total_items_sent = 0 # Số lượng các hoạt động đã gửi thành công tới Zep + self._failed_count = 0 # Số lượt gửi đi thất bại + self._skipped_count = 0 # Số lượng các hoạt động bị filter bỏ qua (DO_NOTHING) - logger.info(f"ZepGraphMemoryUpdater 初始化完成: graph_id={graph_id}, batch_size={self.BATCH_SIZE}") + logger.info(f"ZepGraphMemoryUpdater initialized: graph_id={graph_id}, batch_size={self.BATCH_SIZE}") def _get_platform_display_name(self, platform: str) -> str: - """获取平台的显示名称""" + """Lấy tên hiển thị của nền tảng""" return self.PLATFORM_DISPLAY_NAMES.get(platform.lower(), platform) def start(self): - """启动后台工作线程""" + """Khởi chạy luồng làm việc dưới background""" if self._running: return @@ -283,19 +283,19 @@ def start(self): name=f"ZepMemoryUpdater-{self.graph_id[:8]}" ) self._worker_thread.start() - logger.info(f"ZepGraphMemoryUpdater 已启动: graph_id={self.graph_id}") + logger.info(f"ZepGraphMemoryUpdater started: graph_id={self.graph_id}") def stop(self): - """停止后台工作线程""" + """Tắt luồng làm việc dưới background""" self._running = False - # 发送剩余的活动 + # Gửi nốt các hoạt động còn lại self._flush_remaining() if self._worker_thread and self._worker_thread.is_alive(): self._worker_thread.join(timeout=10) - logger.info(f"ZepGraphMemoryUpdater 已停止: graph_id={self.graph_id}, " + logger.info(f"ZepGraphMemoryUpdater stopped: graph_id={self.graph_id}, " f"total_activities={self._total_activities}, " f"batches_sent={self._total_sent}, " f"items_sent={self._total_items_sent}, " @@ -304,43 +304,43 @@ def stop(self): def add_activity(self, activity: AgentActivity): """ - 添加一个agent活动到队列 - - 所有有意义的行为都会被添加到队列,包括: - - CREATE_POST(发帖) - - CREATE_COMMENT(评论) - - QUOTE_POST(引用帖子) - - SEARCH_POSTS(搜索帖子) - - SEARCH_USER(搜索用户) - - LIKE_POST/DISLIKE_POST(点赞/踩帖子) - - REPOST(转发) - - FOLLOW(关注) - - MUTE(屏蔽) - - LIKE_COMMENT/DISLIKE_COMMENT(点赞/踩评论) - - action_args中会包含完整的上下文信息(如帖子原文、用户名等)。 + Thêm một hoạt động của agent vào hàng đợi + + Tất cả các hành vi có ý nghĩa đều sẽ được thêm vào hàng đợi, bao gồm: + - CREATE_POST (Đăng bài) + - CREATE_COMMENT (Bình luận) + - QUOTE_POST (Trích dẫn bài viết) + - SEARCH_POSTS (Tìm kiếm bài viết) + - SEARCH_USER (Tìm kiếm người dùng) + - LIKE_POST/DISLIKE_POST (Like/Dislike bài viết) + - REPOST (Repost) + - FOLLOW (Theo dõi) + - MUTE (Chặn) + - LIKE_COMMENT/DISLIKE_COMMENT (Like/dislike bình luận) + + action_args sẽ bao gồm toàn bộ thông tin ngữ cảnh (như nội dung gốc của bài viết, tên người dùng, v.v..). Args: - activity: Agent活动记录 + activity: Bản ghi hoạt động của Agent """ - # 跳过DO_NOTHING类型的活动 + # Bỏ qua những hoạt động thuộc loại DO_NOTHING if activity.action_type == "DO_NOTHING": self._skipped_count += 1 return self._activity_queue.put(activity) self._total_activities += 1 - logger.debug(f"添加活动到Zep队列: {activity.agent_name} - {activity.action_type}") + logger.debug(f"Action added to Zep queue: {activity.agent_name} - {activity.action_type}") def add_activity_from_dict(self, data: Dict[str, Any], platform: str): """ - 从字典数据添加活动 + Thêm hoạt động từ dữ liệu dictionary Args: - data: 从actions.jsonl解析的字典数据 - platform: 平台名称 (twitter/reddit) + data: Dữ liệu dictionary parse từ actions.jsonl + platform: Tên nền tảng (twitter/reddit) """ - # 跳过事件类型的条目 + # Bỏ qua các mục liên quan tới thuộc loại sự kiện (event_type) if "event_type" in data: return @@ -357,52 +357,52 @@ def add_activity_from_dict(self, data: Dict[str, Any], platform: str): self.add_activity(activity) def _worker_loop(self): - """后台工作循环 - 按平台批量发送活动到Zep""" + """Vòng lặp làm việc chung (background) - Gửi hàng loạt các hoạt động lên Zep theo từng nền tảng""" while self._running or not self._activity_queue.empty(): try: - # 尝试从队列获取活动(超时1秒) + # Thử lấy hoạt động từ hàng đợi (Timeout: 1 giây) try: activity = self._activity_queue.get(timeout=1) - # 将活动添加到对应平台的缓冲区 + # Thêm hoạt động vào bộ đệm của nền tảng tương ứng platform = activity.platform.lower() with self._buffer_lock: if platform not in self._platform_buffers: self._platform_buffers[platform] = [] self._platform_buffers[platform].append(activity) - # 检查该平台是否达到批量大小 + # Kiểm tra xem nền tảng đã đủ số lượng batch (gửi hàng loạt) chưa if len(self._platform_buffers[platform]) >= self.BATCH_SIZE: batch = self._platform_buffers[platform][:self.BATCH_SIZE] self._platform_buffers[platform] = self._platform_buffers[platform][self.BATCH_SIZE:] - # 释放锁后再发送 + # Gửi sau khi giải phóng lock self._send_batch_activities(batch, platform) - # 发送间隔,避免请求过快 + # Thời gian giãn giữa mỗi lần gửi để tránh request quá nhanh time.sleep(self.SEND_INTERVAL) except Empty: pass except Exception as e: - logger.error(f"工作循环异常: {e}") + logger.error(f"Worker loop error: {e}") time.sleep(1) def _send_batch_activities(self, activities: List[AgentActivity], platform: str): """ - 批量发送活动到Zep图谱(合并为一条文本) + Gửi hàng loạt các hoạt động lên đồ thị Zep (Gộp chung vào một đoạn text) Args: - activities: Agent活动列表 - platform: 平台名称 + activities: Danh sách hoạt động của Agent + platform: Tên nền tảng """ if not activities: return - # 将多条活动合并为一条文本,用换行分隔 + # Gộp nhiều hoạt động vào một văn bản chung, tách nhau bởi xuống dòng episode_texts = [activity.to_episode_text() for activity in activities] combined_text = "\n".join(episode_texts) - # 带重试的发送 + # Gửi với cơ chế thử lại for attempt in range(self.MAX_RETRIES): try: self.client.graph.add( @@ -414,21 +414,21 @@ def _send_batch_activities(self, activities: List[AgentActivity], platform: str) self._total_sent += 1 self._total_items_sent += len(activities) display_name = self._get_platform_display_name(platform) - logger.info(f"成功批量发送 {len(activities)} 条{display_name}活动到图谱 {self.graph_id}") - logger.debug(f"批量内容预览: {combined_text[:200]}...") + logger.info(f"Successfully sent batch of {len(activities)} {display_name} actions to graph {self.graph_id}") + logger.debug(f"Batch content preview: {combined_text[:200]}...") return except Exception as e: if attempt < self.MAX_RETRIES - 1: - logger.warning(f"批量发送到Zep失败 (尝试 {attempt + 1}/{self.MAX_RETRIES}): {e}") + logger.warning(f"Failed to send batch to Zep (attempt {attempt + 1}/{self.MAX_RETRIES}): {e}") time.sleep(self.RETRY_DELAY * (attempt + 1)) else: - logger.error(f"批量发送到Zep失败,已重试{self.MAX_RETRIES}次: {e}") + logger.error(f"Failed to send batch to Zep after {self.MAX_RETRIES} attempts: {e}") self._failed_count += 1 def _flush_remaining(self): - """发送队列和缓冲区中剩余的活动""" - # 首先处理队列中剩余的活动,添加到缓冲区 + """Gửi các hoạt động còn sót lại trong hàng đợi và bộ đệm""" + # Đầu tiên xử lý các hoạt động sót lại trong hàng đợi, đưa vào bộ đệm while not self._activity_queue.empty(): try: activity = self._activity_queue.get_nowait() @@ -440,41 +440,41 @@ def _flush_remaining(self): except Empty: break - # 然后发送各平台缓冲区中剩余的活动(即使不足BATCH_SIZE条) + # Tiếp sau đó là gửi các hoạt động nằm trong bộ đệm của từng nền tảng đi (mặc dù chưa đạt đến số lượng BATCH_SIZE) with self._buffer_lock: for platform, buffer in self._platform_buffers.items(): if buffer: display_name = self._get_platform_display_name(platform) - logger.info(f"发送{display_name}平台剩余的 {len(buffer)} 条活动") + logger.info(f"Sending remaining {len(buffer)} actions for platform {display_name}") self._send_batch_activities(buffer, platform) - # 清空所有缓冲区 + # Dọn sạch toàn bộ bộ đệm for platform in self._platform_buffers: self._platform_buffers[platform] = [] def get_stats(self) -> Dict[str, Any]: - """获取统计信息""" + """Lấy thông tin thống kê""" with self._buffer_lock: buffer_sizes = {p: len(b) for p, b in self._platform_buffers.items()} return { "graph_id": self.graph_id, "batch_size": self.BATCH_SIZE, - "total_activities": self._total_activities, # 添加到队列的活动总数 - "batches_sent": self._total_sent, # 成功发送的批次数 - "items_sent": self._total_items_sent, # 成功发送的活动条数 - "failed_count": self._failed_count, # 发送失败的批次数 - "skipped_count": self._skipped_count, # 被过滤跳过的活动数(DO_NOTHING) + "total_activities": self._total_activities, # Tổng số hoạt động được thêm vào hàng đợi + "batches_sent": self._total_sent, # Số đợt hàng đã gửi thành công + "items_sent": self._total_items_sent, # Số lượng hoạt động đã gửi thành công + "failed_count": self._failed_count, # Số đợt hàng gửi thất bại + "skipped_count": self._skipped_count, # Số lượng hoạt động bị bỏ qua (DO_NOTHING) "queue_size": self._activity_queue.qsize(), - "buffer_sizes": buffer_sizes, # 各平台缓冲区大小 + "buffer_sizes": buffer_sizes, # Kích thước bộ đệm của từng nền tảng "running": self._running, } class ZepGraphMemoryManager: """ - 管理多个模拟的Zep图谱记忆更新器 + Quản lý các trình cập nhật bộ nhớ đồ thị Zep cho nhiều mô phỏng - 每个模拟可以有自己的更新器实例 + Mỗi mô phỏng có thể có instance trình cập nhật của riêng nó """ _updaters: Dict[str, ZepGraphMemoryUpdater] = {} @@ -483,17 +483,17 @@ class ZepGraphMemoryManager: @classmethod def create_updater(cls, simulation_id: str, graph_id: str) -> ZepGraphMemoryUpdater: """ - 为模拟创建图谱记忆更新器 + Tạo trình cập nhật bộ nhớ đồ thị cho mô phỏng Args: - simulation_id: 模拟ID - graph_id: Zep图谱ID + simulation_id: ID của mô phỏng + graph_id: ID của đồ thị Zep Returns: - ZepGraphMemoryUpdater实例 + Instance của ZepGraphMemoryUpdater """ with cls._lock: - # 如果已存在,先停止旧的 + # Nếu đã tồn tại, dừng cái cũ lại trước if simulation_id in cls._updaters: cls._updaters[simulation_id].stop() @@ -501,30 +501,30 @@ def create_updater(cls, simulation_id: str, graph_id: str) -> ZepGraphMemoryUpda updater.start() cls._updaters[simulation_id] = updater - logger.info(f"创建图谱记忆更新器: simulation_id={simulation_id}, graph_id={graph_id}") + logger.info(f"Created graph memory updater: simulation_id={simulation_id}, graph_id={graph_id}") return updater @classmethod def get_updater(cls, simulation_id: str) -> Optional[ZepGraphMemoryUpdater]: - """获取模拟的更新器""" + """Lấy trình cập nhật của mô phỏng""" return cls._updaters.get(simulation_id) @classmethod def stop_updater(cls, simulation_id: str): - """停止并移除模拟的更新器""" + """Dừng và gỡ bỏ trình cập nhật của mô phỏng""" with cls._lock: if simulation_id in cls._updaters: cls._updaters[simulation_id].stop() del cls._updaters[simulation_id] - logger.info(f"已停止图谱记忆更新器: simulation_id={simulation_id}") + logger.info(f"Graph memory updater stopped: simulation_id={simulation_id}") - # 防止 stop_all 重复调用的标志 + # Cờ ngăn chặn gọi stop_all lặp lại _stop_all_done = False @classmethod def stop_all(cls): - """停止所有更新器""" - # 防止重复调用 + """Dừng toàn bộ các trình cập nhật""" + # Ngăn gọi lặp lại if cls._stop_all_done: return cls._stop_all_done = True @@ -535,13 +535,13 @@ def stop_all(cls): try: updater.stop() except Exception as e: - logger.error(f"停止更新器失败: simulation_id={simulation_id}, error={e}") + logger.error(f"Failed to stop updater: simulation_id={simulation_id}, error={e}") cls._updaters.clear() - logger.info("已停止所有图谱记忆更新器") + logger.info("All graph memory updaters stopped") @classmethod def get_all_stats(cls) -> Dict[str, Dict[str, Any]]: - """获取所有更新器的统计信息""" + """Lấy thông tin thống kê của toàn bộ các trình cập nhật""" return { sim_id: updater.get_stats() for sim_id, updater in cls._updaters.items() diff --git a/backend/app/services/zep_tools.py b/backend/app/services/zep_tools.py index 384cf540f..95c1ec475 100644 --- a/backend/app/services/zep_tools.py +++ b/backend/app/services/zep_tools.py @@ -1,11 +1,11 @@ """ -Zep检索工具服务 -封装图谱搜索、节点读取、边查询等工具,供Report Agent使用 +Dịch vụ cung cấp các công cụ tìm kiếm trên nền tảng Zep Cloud. +Đóng gói các công cụ tìm kiếm đồ thị (graph search), đọc thông tin node, truy vấn cạnh (edge), v.v., để Report Agent sử dụng. -核心检索工具(优化后): -1. InsightForge(深度洞察检索)- 最强大的混合检索,自动生成子问题并多维度检索 -2. PanoramaSearch(广度搜索)- 获取全貌,包括过期内容 -3. QuickSearch(简单搜索)- 快速检索 +Các công cụ tìm kiếm cốt lõi (sau khi tối ưu hóa): +1. InsightForge (Tìm kiếm chiều sâu) - Công cụ tìm kiếm kết hợp mạnh mẽ nhất, tự động tạo các câu hỏi phụ và tìm kiếm đa chiều. +2. PanoramaSearch (Tìm kiếm theo chiều rộng) - Lấy toàn cảnh thông tin, bao gồm cả nội dung đã hết hạn (expired). +3. QuickSearch (Tìm kiếm cơ bản) - Tìm kiếm nhanh chóng với truy vấn đơn giản. """ import time @@ -25,7 +25,7 @@ @dataclass class SearchResult: - """搜索结果""" + """Kết quả tìm kiếm cơ bản.""" facts: List[str] edges: List[Dict[str, Any]] nodes: List[Dict[str, Any]] @@ -42,11 +42,11 @@ def to_dict(self) -> Dict[str, Any]: } def to_text(self) -> str: - """转换为文本格式,供LLM理解""" - text_parts = [f"搜索查询: {self.query}", f"找到 {self.total_count} 条相关信息"] + """Chuyển đổi kết quả sang định dạng văn bản (text) để cho LLM dễ dàng hiểu và xử lý.""" + text_parts = [f"Search query: {self.query}", f"Found {self.total_count} related info items"] if self.facts: - text_parts.append("\n### 相关事实:") + text_parts.append("\n### Related facts:") for i, fact in enumerate(self.facts, 1): text_parts.append(f"{i}. {fact}") @@ -55,7 +55,7 @@ def to_text(self) -> str: @dataclass class NodeInfo: - """节点信息""" + """Thông tin chi tiết về một Node (Thực thể) trong đồ thị tri thức.""" uuid: str name: str labels: List[str] @@ -72,14 +72,14 @@ def to_dict(self) -> Dict[str, Any]: } def to_text(self) -> 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}" + """Chuyển đổi thông tin Node sang định dạng văn bản để hiển thị hoặc đưa cho LLM.""" + 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}" @dataclass class EdgeInfo: - """边信息""" + """Thông tin chi tiết về một Cạnh (mối quan hệ/edge) nối giữa hai Node trong đồ thị.""" uuid: str name: str fact: str @@ -87,7 +87,7 @@ class EdgeInfo: target_node_uuid: str source_node_name: Optional[str] = None target_node_name: Optional[str] = None - # 时间信息 + # Thông tin thời gian của sự kiện, tính hợp lệ theo thời gian created_at: Optional[str] = None valid_at: Optional[str] = None invalid_at: Optional[str] = None @@ -109,47 +109,48 @@ def to_dict(self) -> Dict[str, Any]: } def to_text(self, include_temporal: bool = False) -> str: - """转换为文本格式""" + """Chuyển đổi thông tin mối quan hệ (Cạnh/Edge) sang dạng văn bản. + Nếu include_temporal=True, sẽ bao gồm thông tin về dòng thời gian của mối quan hệ.""" 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}" + base_text = f"Relationship: {source} --[{self.name}]--> {target}\nFact: {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}" + valid_at = self.valid_at or "Unknown start" + invalid_at = self.invalid_at or "Until now" + base_text += f"\nTime validity: {valid_at} - {invalid_at}" if self.expired_at: - base_text += f" (已过期: {self.expired_at})" + base_text += f" (Expired at: {self.expired_at})" return base_text @property def is_expired(self) -> bool: - """是否已过期""" + """Kiểm tra mối quan hệ này đã hết hạn (không còn chính xác theo thực tế hiện tại) hay chưa.""" return self.expired_at is not None @property def is_invalid(self) -> bool: - """是否已失效""" + """Kiểm tra mối quan hệ này đã bắt đầu bị vô hiệu hóa hay chưa.""" return self.invalid_at is not None @dataclass class InsightForgeResult: """ - 深度洞察检索结果 (InsightForge) - 包含多个子问题的检索结果,以及综合分析 + Kết quả của truy vấn InsightForge (Tìm kiếm chiều sâu). + Bao gồm kết quả từ nhiều câu hỏi phụ (sub_queries) và các phân tích tổng hợp. """ query: str simulation_requirement: str sub_queries: List[str] - # 各维度检索结果 - semantic_facts: List[str] = field(default_factory=list) # 语义搜索结果 - entity_insights: List[Dict[str, Any]] = field(default_factory=list) # 实体洞察 - relationship_chains: List[str] = field(default_factory=list) # 关系链 + # Kết quả tìm kiếm từ nhiều góc nhìn khác nhau (đa chiều) + semantic_facts: List[str] = field(default_factory=list) # Kết quả tìm kiếm theo ngữ nghĩa (semantic search) + entity_insights: List[Dict[str, Any]] = field(default_factory=list) # Phân tích sâu về các thực thể (insight) + relationship_chains: List[str] = field(default_factory=list) # Chuỗi mối quan hệ nối tiếp nhau (relationship chains) - # 统计信息 + # Thông kê kết quả total_facts: int = 0 total_entities: int = 0 total_relationships: int = 0 @@ -168,42 +169,42 @@ def to_dict(self) -> Dict[str, Any]: } def to_text(self) -> str: - """转换为详细的文本格式,供LLM理解""" + """Chuyển đổi sang định dạng văn bản chi tiết để cung cấp ngữ cảnh cho 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}条" + f"## Deep Analysis for Future Simulation", + f"Analysis query: {self.query}", + f"Simulation scenario: {self.simulation_requirement}", + f"\n### Simulation Data Statistics", + f"- Related simulation facts: {self.total_facts} items", + f"- Entities involved: {self.total_entities}", + f"- Relationship chains: {self.total_relationships}" ] - # 子问题 + # Các câu hỏi phụ (sub queries) được sinh ra để truy vấn sâu hơn if self.sub_queries: - text_parts.append(f"\n### 分析的子问题") + text_parts.append(f"\n### Analyzed sub-queries") for i, sq in enumerate(self.sub_queries, 1): text_parts.append(f"{i}. {sq}") - # 语义搜索结果 + # Kết quả tìm kiếm theo ngữ nghĩa if self.semantic_facts: - text_parts.append(f"\n### 【关键事实】(请在报告中引用这些原文)") + 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}\"") - # 实体洞察 + # Thông tin sâu sắc về thực thể if self.entity_insights: - text_parts.append(f"\n### 【核心实体】") + text_parts.append(f"\n### 【Core Entities】") for entity in self.entity_insights: - text_parts.append(f"- **{entity.get('name', '未知')}** ({entity.get('type', '实体')})") + text_parts.append(f"- **{entity.get('name', 'Unknown')}** ({entity.get('type', 'Entity')})") if entity.get('summary'): - text_parts.append(f" 摘要: \"{entity.get('summary')}\"") + text_parts.append(f" Summary: \"{entity.get('summary')}\"") if entity.get('related_facts'): - text_parts.append(f" 相关事实: {len(entity.get('related_facts', []))}条") + text_parts.append(f" Related facts: {len(entity.get('related_facts', []))} items") - # 关系链 + # Chuỗi mối quan hệ (graph paths) if self.relationship_chains: - text_parts.append(f"\n### 【关系链】") + text_parts.append(f"\n### 【Relationship Chains】") for chain in self.relationship_chains: text_parts.append(f"- {chain}") @@ -213,21 +214,21 @@ def to_text(self) -> str: @dataclass class PanoramaResult: """ - 广度搜索结果 (Panorama) - 包含所有相关信息,包括过期内容 + Kết quả tìm kiếm theo chiều rộng (Panorama search). + Chứa toàn bộ thông tin liên quan từ đồ thị, bao gồm cả những sự kiện/relatioships đã hết hạn (expired). """ query: str - # 全部节点 + # Toàn bộ Node tìm được all_nodes: List[NodeInfo] = field(default_factory=list) - # 全部边(包括过期的) + # Toàn bộ Edge tìm được (kể cả đã hết hạn) all_edges: List[EdgeInfo] = field(default_factory=list) - # 当前有效的事实 + # Các sự kiện/thực trạng đang hoạt động hợp lệ hiện tại active_facts: List[str] = field(default_factory=list) - # 已过期/失效的事实(历史记录) + # Các sự kiện thực trạng đã hết hạn/không còn hiệu lực (Dữ liệu lịch sử) historical_facts: List[str] = field(default_factory=list) - # 统计 + # Thống kê total_nodes: int = 0 total_edges: int = 0 active_count: int = 0 @@ -247,34 +248,34 @@ def to_dict(self) -> Dict[str, Any]: } def to_text(self) -> str: - """转换为文本格式(完整版本,不截断)""" + """Chuyển đổi sang định dạng văn bản (phiên bản đầy đủ, không bị cắt bớt)""" text_parts = [ - f"## 广度搜索结果(未来全景视图)", - f"查询: {self.query}", - f"\n### 统计信息", - f"- 总节点数: {self.total_nodes}", - f"- 总边数: {self.total_edges}", - f"- 当前有效事实: {self.active_count}条", - f"- 历史/过期事实: {self.historical_count}条" + f"## Panorama Search Results (Future Panorama 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} items", + f"- Historical/expired facts: {self.historical_count} items" ] - # 当前有效的事实(完整输出,不截断) + # Các sự kiện hợp lệ hiện tại (xuất ra đầy đủ, không cắt bớt) if self.active_facts: - text_parts.append(f"\n### 【当前有效事实】(模拟结果原文)") + text_parts.append(f"\n### 【Currently Valid Facts】(Simulated Result Text)") for i, fact in enumerate(self.active_facts, 1): text_parts.append(f"{i}. \"{fact}\"") - # 历史/过期事实(完整输出,不截断) + # Sự kiện lịch sử/đã hết hạn (xuất ra đầy đủ, không cắt bớt) if self.historical_facts: - text_parts.append(f"\n### 【历史/过期事实】(演变过程记录)") + text_parts.append(f"\n### 【Historical/Expired Facts】(Evolution Records)") for i, fact in enumerate(self.historical_facts, 1): text_parts.append(f"{i}. \"{fact}\"") - # 关键实体(完整输出,不截断) + # Các thực thể cốt lõi (xuất ra đầy đủ, không cắt bớt) if self.all_nodes: - text_parts.append(f"\n### 【涉及实体】") + 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_type = next((l for l in node.labels if l not in ["Entity", "Node"]), "Entity") text_parts.append(f"- **{node.name}** ({entity_type})") return "\n".join(text_parts) @@ -282,13 +283,13 @@ def to_text(self) -> str: @dataclass class AgentInterview: - """单个Agent的采访结果""" + """Kết quả phỏng vấn của một Agent cá nhân""" agent_name: str - agent_role: str # 角色类型(如:学生、教师、媒体等) - agent_bio: str # 简介 - question: str # 采访问题 - response: str # 采访回答 - key_quotes: List[str] = field(default_factory=list) # 关键引言 + agent_role: str # Loại vai trò (ví dụ: Học sinh, Giáo viên, Truyền thông, v.v.) + agent_bio: str # Tiểu sử ngắn gọn + question: str # Câu hỏi phỏng vấn + response: str # Câu trả lời phỏng vấn + key_quotes: List[str] = field(default_factory=list) # Các câu trích dẫn quan trọng def to_dict(self) -> Dict[str, Any]: return { @@ -302,21 +303,21 @@ def to_dict(self) -> Dict[str, Any]: def to_text(self) -> str: text = f"**{self.agent_name}** ({self.agent_role})\n" - # 显示完整的agent_bio,不截断 - text += f"_简介: {self.agent_bio}_\n\n" + # Hiển thị bio đầy đủ, không cắt bớt + text += f"_Bio: {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" + text += "\n**Key Quotes:**\n" for quote in self.key_quotes: - # 清理各种引号 + # Làm sạch các loại dấu ngoặc kép clean_quote = quote.replace('\u201c', '').replace('\u201d', '').replace('"', '') clean_quote = clean_quote.replace('\u300c', '').replace('\u300d', '') clean_quote = clean_quote.strip() - # 去掉开头的标点 + # Bỏ các dấu chấm câu ở đầu chuỗi while clean_quote and clean_quote[0] in ',,;;::、。!?\n\r\t ': clean_quote = clean_quote[1:] - # 过滤包含问题编号的垃圾内容(问题1-9) + # Bỏ qua nội dung rác chứa số câu hỏi (ví dụ: Câu hỏi 1-9) skip = False for d in '123456789': if f'\u95ee\u9898{d}' in clean_quote: @@ -324,7 +325,7 @@ def to_text(self) -> str: break if skip: continue - # 截断过长内容(按句号截断,而非硬截断) + # Cắt bớt phần nội dung quá dài (Dựa vào dấu chấm câu chứ không cắt ngang chữ) if len(clean_quote) > 150: dot_pos = clean_quote.find('\u3002', 80) if dot_pos > 0: @@ -339,23 +340,23 @@ def to_text(self) -> str: @dataclass class InterviewResult: """ - 采访结果 (Interview) - 包含多个模拟Agent的采访回答 + Kết quả phỏng vấn các Agent giả lập. + Chứa danh sách các câu trả lời phỏng vấn từ các tác nhân AI. """ - interview_topic: str # 采访主题 - interview_questions: List[str] # 采访问题列表 + interview_topic: str # Chủ đề phỏng vấn + interview_questions: List[str] # Danh sách các câu hỏi phỏng vấn - # 采访选择的Agent + # Danh sách các Agent được chọn để phỏng vấn selected_agents: List[Dict[str, Any]] = field(default_factory=list) - # 各Agent的采访回答 + # Bảng lưu kết quả trả lời của các Agent interviews: List[AgentInterview] = field(default_factory=list) - # 选择Agent的理由 + # Nêu lý do chọn các Agent này selection_reasoning: str = "" - # 整合后的采访摘要 + # Bản tóm tắt lại nội dung sau cuộc phỏng vấn summary: str = "" - # 统计 + # Thống kê total_agents: int = 0 interviewed_count: int = 0 @@ -372,74 +373,74 @@ def to_dict(self) -> Dict[str, Any]: } def to_text(self) -> str: - """转换为详细的文本格式,供LLM理解和报告引用""" + """Chuyển đổi thành định dạng văn bản chi tiết để cung cấp cho LLM hoặc Report.""" text_parts = [ - "## 深度采访报告", - f"**采访主题:** {self.interview_topic}", - f"**采访人数:** {self.interviewed_count} / {self.total_agents} 位模拟Agent", - "\n### 采访对象选择理由", - self.selection_reasoning or "(自动选择)", + "## Deep Interview Report", + f"**Interview Topic:** {self.interview_topic}", + f"**Interview Count:** {self.interviewed_count} / {self.total_agents} simulated agents", + "\n### Reasoning behind agent selection", + self.selection_reasoning or "(Auto selection)", "\n---", - "\n### 采访实录", + "\n### Interview Transcripts", ] if self.interviews: for i, interview in enumerate(self.interviews, 1): - text_parts.append(f"\n#### 采访 #{i}: {interview.agent_name}") + text_parts.append(f"\n#### Interview #{i}: {interview.agent_name}") text_parts.append(interview.to_text()) text_parts.append("\n---") else: - text_parts.append("(无采访记录)\n\n---") + text_parts.append("(No interview records)\n\n---") - text_parts.append("\n### 采访摘要与核心观点") - text_parts.append(self.summary or "(无摘要)") + text_parts.append("\n### Interview Summary & Core Insights") + text_parts.append(self.summary or "(No summary)") return "\n".join(text_parts) class ZepToolsService: """ - Zep检索工具服务 + Dịch vụ công cụ tìm kiếm Zep - 【核心检索工具 - 优化后】 - 1. insight_forge - 深度洞察检索(最强大,自动生成子问题,多维度检索) - 2. panorama_search - 广度搜索(获取全貌,包括过期内容) - 3. quick_search - 简单搜索(快速检索) - 4. interview_agents - 深度采访(采访模拟Agent,获取多视角观点) + 【Các công cụ tìm kiếm cốt lõi - Đã tối ưu hóa】 + 1. insight_forge - Tìm kiếm chiều sâu (Mạnh nhất, tự động tạo câu hỏi phụ và tìm kiếm đa chiều) + 2. panorama_search - Tìm kiếm chiều rộng (Lấy toàn cảnh, bao gồm cả nội dung hết hạn) + 3. quick_search - Tìm kiếm cơ bản (Tìm kiếm nhanh với từ khóa) + 4. interview_agents - Phỏng vấn sâu (Phỏng vấn các Agent giả lập, thu thập góc nhìn đa chiều) - 【基础工具】 - - search_graph - 图谱语义搜索 - - get_all_nodes - 获取图谱所有节点 - - get_all_edges - 获取图谱所有边(含时间信息) - - get_node_detail - 获取节点详细信息 - - get_node_edges - 获取节点相关的边 - - get_entities_by_type - 按类型获取实体 - - get_entity_summary - 获取实体的关系摘要 + 【Các công cụ cơ bản】 + - search_graph - Tìm kiếm ngữ nghĩa trong graph + - get_all_nodes - Lấy tất cả các nodes (thực thể) trong graph + - get_all_edges - Lấy tất cả các edges (mối quan hệ) trong graph (bao gồm thông tin thời gian) + - get_node_detail - Lấy chi tiết một node (thực thể) + - get_node_edges - Lấy các mối quan hệ (edges) liên quan đến một node + - get_entities_by_type - Phân loại và lấy các thực thể theo type + - get_entity_summary - Lấy tóm tắt về các mối quan hệ của một thực thể """ - # 重试配置 + # Cấu hình retry khi gọi API lỗi MAX_RETRIES = 3 RETRY_DELAY = 2.0 def __init__(self, api_key: Optional[str] = None, llm_client: Optional[LLMClient] = None): self.api_key = api_key or Config.ZEP_API_KEY if not self.api_key: - raise ValueError("ZEP_API_KEY 未配置") + raise ValueError("ZEP_API_KEY is not configured") self.client = Zep(api_key=self.api_key) - # LLM客户端用于InsightForge生成子问题 + # LLM client được sử dụng bởi InsightForge để sinh ra các sub-queries self._llm_client = llm_client - logger.info("ZepToolsService 初始化完成") + logger.info("ZepToolsService initialized successfully") @property def llm(self) -> LLMClient: - """延迟初始化LLM客户端""" + """Khởi tạo muộn (lazy init) cho LLM client""" if self._llm_client is None: self._llm_client = LLMClient() return self._llm_client def _call_with_retry(self, func, operation_name: str, max_retries: int = None): - """带重试机制的API调用""" + """Cơ chế gọi hàm an toàn, tự động thử lại (retry) khi gặp lỗi.""" max_retries = max_retries or self.MAX_RETRIES last_exception = None delay = self.RETRY_DELAY @@ -451,13 +452,13 @@ def _call_with_retry(self, func, operation_name: str, max_retries: int = None): last_exception = e if attempt < max_retries - 1: logger.warning( - f"Zep {operation_name} 第 {attempt + 1} 次尝试失败: {str(e)[:100]}, " - f"{delay:.1f}秒后重试..." + f"Zep {operation_name} attempt {attempt + 1} failed: {str(e)[:100]}, " + f"retrying in {delay:.1f}s..." ) time.sleep(delay) delay *= 2 else: - logger.error(f"Zep {operation_name} 在 {max_retries} 次尝试后仍失败: {str(e)}") + logger.error(f"Zep {operation_name} failed after {max_retries} attempts: {str(e)}") raise last_exception @@ -469,23 +470,23 @@ def search_graph( scope: str = "edges" ) -> SearchResult: """ - 图谱语义搜索 + Tìm kiếm ngữ nghĩa trên Graph (Đồ thị tri thức) - 使用混合搜索(语义+BM25)在图谱中搜索相关信息。 - 如果Zep Cloud的search API不可用,则降级为本地关键词匹配。 + Sử dụng tìm kiếm lai (hybrid search: ngữ nghĩa + BM25) để tìm kiếm thông tin liên quan trong đồ thị. + Nếu API search của Zep Cloud không khả dụng, sẽ fallback (hạ cấp) xuống tìm kiếm khớp từ khóa cục bộ. Args: - graph_id: 图谱ID (Standalone Graph) - query: 搜索查询 - limit: 返回结果数量 - scope: 搜索范围,"edges" 或 "nodes" + graph_id: ID của đồ thị (Standalone Graph) + query: Truy vấn tìm kiếm (Text) + limit: Số lượng kết quả tối đa trả về + scope: Phạm vi tìm kiếm, có thể là "edges" (cạnh/fact) hoặc "nodes" (thực thể) Returns: - SearchResult: 搜索结果 + SearchResult: Đối tượng chứa kết quả tìm kiếm đã phân tích """ - logger.info(f"图谱搜索: graph_id={graph_id}, query={query[:50]}...") + logger.info(f"Graph search: graph_id={graph_id}, query={query[:50]}...") - # 尝试使用Zep Cloud Search API + # Thử sử dụng API Zep Cloud Search try: search_results = self._call_with_retry( func=lambda: self.client.graph.search( @@ -495,14 +496,14 @@ def search_graph( scope=scope, reranker="cross_encoder" ), - operation_name=f"图谱搜索(graph={graph_id})" + operation_name=f"Graph Search(graph={graph_id})" ) facts = [] edges = [] nodes = [] - # 解析边搜索结果 + # Phân tích kết quả tìm kiếm cạnh (edges/relationships) if hasattr(search_results, 'edges') and search_results.edges: for edge in search_results.edges: if hasattr(edge, 'fact') and edge.fact: @@ -515,7 +516,7 @@ def search_graph( "target_node_uuid": getattr(edge, 'target_node_uuid', ''), }) - # 解析节点搜索结果 + # Phân tích kết quả tìm kiếm thực thể (nodes) if hasattr(search_results, 'nodes') and search_results.nodes: for node in search_results.nodes: nodes.append({ @@ -524,11 +525,11 @@ def search_graph( "labels": getattr(node, 'labels', []), "summary": getattr(node, 'summary', ''), }) - # 节点摘要也算作事实 + # Phần tóm tắt (summary) của node cũng được coi là một fact if hasattr(node, 'summary') and node.summary: facts.append(f"[{node.name}]: {node.summary}") - logger.info(f"搜索完成: 找到 {len(facts)} 条相关事实") + logger.info(f"Search completed: Found {len(facts)} related facts") return SearchResult( facts=facts, @@ -539,8 +540,8 @@ def search_graph( ) except Exception as e: - logger.warning(f"Zep Search API失败,降级为本地搜索: {str(e)}") - # 降级:使用本地关键词匹配搜索 + logger.warning(f"Zep Search API failed, gracefully degrading to local search: {str(e)}") + # Hạ cấp: Sử dụng tìm kiếm theo từ khóa cục bộ return self._local_search(graph_id, query, limit, scope) def _local_search( @@ -551,38 +552,38 @@ def _local_search( scope: str = "edges" ) -> SearchResult: """ - 本地关键词匹配搜索(作为Zep Search API的降级方案) + Tìm kiếm khớp từ khóa cục bộ (Local keyword matching), một fallback strategy nếu API Zep Search lỗi. - 获取所有边/节点,然后在本地进行关键词匹配 + Sẽ lấy tất cả các cạnh/thực thể, sau đó so khớp từ khóa locally. Args: - graph_id: 图谱ID - query: 搜索查询 - limit: 返回结果数量 - scope: 搜索范围 + graph_id: ID của đồ thị + query: Từ khóa truy vấn + limit: Số lượng kết quả cực đại + scope: Phạm vi tính toán tìm kiếm (nodes/edges/both) Returns: - SearchResult: 搜索结果 + SearchResult: Kết quả của tìm kiếm mô phỏng cục bộ """ - logger.info(f"使用本地搜索: query={query[:30]}...") + logger.info(f"Using local search: query={query[:30]}...") facts = [] edges_result = [] nodes_result = [] - # 提取查询关键词(简单分词) + # Tách từ khóa khỏi truy vấn (chiến lược đơn giản) query_lower = query.lower() keywords = [w.strip() for w in query_lower.replace(',', ' ').replace(',', ' ').split() if len(w.strip()) > 1] def match_score(text: str) -> int: - """计算文本与查询的匹配分数""" + """Hàm tiện ích tính điểm số chuẩn khớp (match score) của từng văn bản""" if not text: return 0 text_lower = text.lower() - # 完全匹配查询 + # Nếu khớp nguyên câu hoàn toàn (exact match) if query_lower in text_lower: return 100 - # 关键词匹配 + # Nếu khớp một vài từ khóa (keyword match) score = 0 for keyword in keywords: if keyword in text_lower: @@ -591,7 +592,7 @@ def match_score(text: str) -> int: try: if scope in ["edges", "both"]: - # 获取所有边并匹配 + # Lấy toàn bộ edges để so khớp all_edges = self.get_all_edges(graph_id) scored_edges = [] for edge in all_edges: @@ -599,7 +600,7 @@ def match_score(text: str) -> int: if score > 0: scored_edges.append((score, edge)) - # 按分数排序 + # Sắp xếp các cạnh dựa trên điểm số khớp từ khóa scored_edges.sort(key=lambda x: x[0], reverse=True) for score, edge in scored_edges[:limit]: @@ -614,7 +615,7 @@ def match_score(text: str) -> int: }) if scope in ["nodes", "both"]: - # 获取所有节点并匹配 + # Tương tự như với cạnh, chúng ta lấy tất cả thực thể và so khớp all_nodes = self.get_all_nodes(graph_id) scored_nodes = [] for node in all_nodes: @@ -634,10 +635,10 @@ def match_score(text: str) -> int: if node.summary: facts.append(f"[{node.name}]: {node.summary}") - logger.info(f"本地搜索完成: 找到 {len(facts)} 条相关事实") + logger.info(f"Local search completed: Found {len(facts)} related facts") except Exception as e: - logger.error(f"本地搜索失败: {str(e)}") + logger.error(f"Local search failed: {str(e)}") return SearchResult( facts=facts, @@ -649,15 +650,15 @@ def match_score(text: str) -> int: def get_all_nodes(self, graph_id: str) -> List[NodeInfo]: """ - 获取图谱的所有节点(分页获取) + Lấy tất cả các nodes (thực thể) của một đồ thị sử dụng việc phân trang hợp lý. Args: - graph_id: 图谱ID + graph_id: ID của đồ thị (Graph ID) Returns: - 节点列表 + List[NodeInfo]: Danh sách các thực thể (nodes) """ - logger.info(f"获取图谱 {graph_id} 的所有节点...") + logger.info(f"Fetching all nodes for graph {graph_id}...") nodes = fetch_all_nodes(self.client, graph_id) @@ -672,21 +673,21 @@ def get_all_nodes(self, graph_id: str) -> List[NodeInfo]: attributes=node.attributes or {} )) - logger.info(f"获取到 {len(result)} 个节点") + logger.info(f"Fetched {len(result)} nodes") return result def get_all_edges(self, graph_id: str, include_temporal: bool = True) -> List[EdgeInfo]: """ - 获取图谱的所有边(分页获取,包含时间信息) + Lấy tất cả các edges (mối quan hệ) trong đồ thị bằng cách lấy nhiều trang dữ liệu Args: - graph_id: 图谱ID - include_temporal: 是否包含时间信息(默认True) + graph_id: ID của đồ thị (Graph ID) + include_temporal: Có lấy cả các field chứa temporal data (created_at, valid_at, v.v) hay không Returns: - 边列表(包含created_at, valid_at, invalid_at, expired_at) + List[EdgeInfo]: Danh sách các cảnh (bao gồm thông tin lịch sử thời gian) """ - logger.info(f"获取图谱 {graph_id} 的所有边...") + logger.info(f"Fetching all edges for graph {graph_id}...") edges = fetch_all_edges(self.client, graph_id) @@ -701,7 +702,7 @@ def get_all_edges(self, graph_id: str, include_temporal: bool = True) -> List[Ed target_node_uuid=edge.target_node_uuid or "" ) - # 添加时间信息 + # Bổ sung thông tin thời gian hợp lệ (temporal info) if include_temporal: edge_info.created_at = getattr(edge, 'created_at', None) edge_info.valid_at = getattr(edge, 'valid_at', None) @@ -710,25 +711,25 @@ def get_all_edges(self, graph_id: str, include_temporal: bool = True) -> List[Ed result.append(edge_info) - logger.info(f"获取到 {len(result)} 条边") + logger.info(f"Fetched {len(result)} edges") return result def get_node_detail(self, node_uuid: str) -> Optional[NodeInfo]: """ - 获取单个节点的详细信息 + Lấy thông tin chi tiết của một Node (Thực thể) cá biệt Args: - node_uuid: 节点UUID + node_uuid: UUID của node cần lấy Returns: - 节点信息或None + Được đóng gói thành NodeInfo hoặc None nếu lỗi/không tìm thấy """ - logger.info(f"获取节点详情: {node_uuid[:8]}...") + logger.info(f"Fetching node detail: {node_uuid[:8]}...") try: node = self._call_with_retry( func=lambda: self.client.graph.node.get(uuid_=node_uuid), - operation_name=f"获取节点详情(uuid={node_uuid[:8]}...)" + operation_name=f"Fetching node detail (uuid={node_uuid[:8]}...)" ) if not node: @@ -742,39 +743,39 @@ def get_node_detail(self, node_uuid: str) -> Optional[NodeInfo]: attributes=node.attributes or {} ) except Exception as e: - logger.error(f"获取节点详情失败: {str(e)}") + logger.error(f"Failed to fetch node detail: {str(e)}") return None def get_node_edges(self, graph_id: str, node_uuid: str) -> List[EdgeInfo]: """ - 获取节点相关的所有边 + Lấy tất cả các edges (mối quan hệ) liên quan trực tiếp đến một node. - 通过获取图谱所有边,然后过滤出与指定节点相关的边 + Bằng cách cách kéo xuống tất cả edges và lọc qua node_uuid (ở hai đầu source hoặc target). Args: - graph_id: 图谱ID - node_uuid: 节点UUID + graph_id: ID của đồ thị (Graph ID) + node_uuid: UUID của node Returns: - 边列表 + Danh sách các EdgeInfo """ - logger.info(f"获取节点 {node_uuid[:8]}... 的相关边") + logger.info(f"Fetching edges related to node {node_uuid[:8]}...") try: - # 获取图谱所有边,然后过滤 + # Lấy tất cả các edges rồi dùng filter (lọc) all_edges = self.get_all_edges(graph_id) result = [] for edge in all_edges: - # 检查边是否与指定节点相关(作为源或目标) + # Kiểm tra xem edge có dính dáng đến node này ở bất kỳ đầu nào không (source hay target) if edge.source_node_uuid == node_uuid or edge.target_node_uuid == node_uuid: result.append(edge) - logger.info(f"找到 {len(result)} 条与节点相关的边") + logger.info(f"Found {len(result)} edges related to node") return result except Exception as e: - logger.warning(f"获取节点边失败: {str(e)}") + logger.warning(f"Failed to fetch node edges: {str(e)}") return [] def get_entities_by_type( @@ -783,26 +784,26 @@ def get_entities_by_type( entity_type: str ) -> List[NodeInfo]: """ - 按类型获取实体 + Lấy dánh sách các thực thể (nodes) phân theo loại (type/label) Args: - graph_id: 图谱ID - entity_type: 实体类型(如 Student, PublicFigure 等) + graph_id: ID của đồ thị (Graph ID) + entity_type: Loại thực thể (ví dụ: Student, PublicFigure, v.v) Returns: - 符合类型的实体列表 + Danh sách các NodeInfo thuộc type được yêu cầu """ - logger.info(f"获取类型为 {entity_type} 的实体...") + logger.info(f"Fetching entities of type {entity_type}...") all_nodes = self.get_all_nodes(graph_id) filtered = [] for node in all_nodes: - # 检查labels是否包含指定类型 + # Kiểm tra xem mảng labels của node này có chứa type yêu cầu không if entity_type in node.labels: filtered.append(node) - logger.info(f"找到 {len(filtered)} 个 {entity_type} 类型的实体") + logger.info(f"Found {len(filtered)} entities of type {entity_type}") return filtered def get_entity_summary( @@ -811,27 +812,27 @@ def get_entity_summary( entity_name: str ) -> Dict[str, Any]: """ - 获取指定实体的关系摘要 + Lấy tóm tắt về một thực thể cụ thể và các mối quan hệ (edges) của nó. - 搜索与该实体相关的所有信息,并生成摘要 + Sẽ tìm kiếm mọi thông tin có liên quan đến thực thể này và tổng hợp thành bản tóm tắt. Args: - graph_id: 图谱ID - entity_name: 实体名称 + graph_id: ID của đồ thị + entity_name: Tên của thực thể Returns: - 实体摘要信息 + Dict chứa tóm tắt thông tin của thực thể """ - logger.info(f"获取实体 {entity_name} 的关系摘要...") + logger.info(f"Fetching relationship summary for entity {entity_name}...") - # 先搜索该实体相关的信息 + # Đầu tiên, tìm kiếm thông tin liên quan đến tên thực thể search_result = self.search_graph( graph_id=graph_id, query=entity_name, limit=20 ) - # 尝试在所有节点中找到该实体 + # Tiếp theo, cố gắng dò tìm node đại diện cho thực thể này trong toàn bộ nodes all_nodes = self.get_all_nodes(graph_id) entity_node = None for node in all_nodes: @@ -841,7 +842,7 @@ def get_entity_summary( related_edges = [] if entity_node: - # 传入graph_id参数 + # Lấy tất cả các edges có dính dáng đến node này related_edges = self.get_node_edges(graph_id, entity_node.uuid) return { @@ -854,27 +855,27 @@ def get_entity_summary( def get_graph_statistics(self, graph_id: str) -> Dict[str, Any]: """ - 获取图谱的统计信息 + Lấy thống kê tổng quan của một đồ thị tri thức Args: - graph_id: 图谱ID + graph_id: ID của đồ thị Returns: - 统计信息 + Dict chứa số liệu thống kê """ - logger.info(f"获取图谱 {graph_id} 的统计信息...") + logger.info(f"Fetching statistics for graph {graph_id}...") nodes = self.get_all_nodes(graph_id) edges = self.get_all_edges(graph_id) - # 统计实体类型分布 + # Thống kê phân bổ loại thực thể (entity types / labels) entity_types = {} for node in nodes: for label in node.labels: if label not in ["Entity", "Node"]: entity_types[label] = entity_types.get(label, 0) + 1 - # 统计关系类型分布 + # Thống kê phân bổ tên mối quan hệ (relation types / edge names) relation_types = {} for edge in edges: relation_types[edge.name] = relation_types.get(edge.name, 0) + 1 @@ -894,34 +895,34 @@ def get_simulation_context( limit: int = 30 ) -> Dict[str, Any]: """ - 获取模拟相关的上下文信息 + Lấy thông tin ngữ cảnh liên quan đến mô phỏng (simulation) - 综合搜索与模拟需求相关的所有信息 + Tìm kiếm tổng hợp mọi thông tin có liên quan đến yêu cầu mô phỏng. Args: - graph_id: 图谱ID - simulation_requirement: 模拟需求描述 - limit: 每类信息的数量限制 + graph_id: ID của đồ thị (Graph ID) + simulation_requirement: Mô tả của yêu cầu mô phỏng + limit: Giới hạn số lượng thông tin mỗi loại Returns: - 模拟上下文信息 + Dict chứa ngữ cảnh (context) cần thiết cho mô phỏng """ - logger.info(f"获取模拟上下文: {simulation_requirement[:50]}...") + logger.info(f"Fetching simulation context: {simulation_requirement[:50]}...") - # 搜索与模拟需求相关的信息 + # Tìm kiếm các thông tin trong graph liên quan chặt chẽ đến yêu cầu mô phỏng search_result = self.search_graph( graph_id=graph_id, query=simulation_requirement, limit=limit ) - # 获取图谱统计 + # Lấy thông số thống kê của đồ thị stats = self.get_graph_statistics(graph_id) - # 获取所有实体节点 + # Lấy tất cả node all_nodes = self.get_all_nodes(graph_id) - # 筛选有实际类型的实体(非纯Entity节点) + # Lọc ra các thực thể có mang type thật (Loại bỏ các node chỉ có label chung chung như 'Entity' / 'Node') entities = [] for node in all_nodes: custom_labels = [l for l in node.labels if l not in ["Entity", "Node"]] @@ -936,11 +937,11 @@ def get_simulation_context( "simulation_requirement": simulation_requirement, "related_facts": search_result.facts, "graph_statistics": stats, - "entities": entities[:limit], # 限制数量 + "entities": entities[:limit], # Giới hạn số lượng thực thể "total_entities": len(entities) } - # ========== 核心检索工具(优化后) ========== + # ========== Các công cụ tìm kiếm lõi (Đã tối ưu) ========== def insight_forge( self, @@ -951,26 +952,26 @@ def insight_forge( max_sub_queries: int = 5 ) -> InsightForgeResult: """ - 【InsightForge - 深度洞察检索】 + 【InsightForge - Tìm kiếm chiều sâu / Deep Insight Search】 - 最强大的混合检索函数,自动分解问题并多维度检索: - 1. 使用LLM将问题分解为多个子问题 - 2. 对每个子问题进行语义搜索 - 3. 提取相关实体并获取其详细信息 - 4. 追踪关系链 - 5. 整合所有结果,生成深度洞察 + Hàm tìm kiếm lai (hybrid retrieval) mạnh mẽ nhất, tự động phân rã câu hỏi và tìm kiếm đa chiều: + 1. Sử dụng LLM phân rã yêu cầu thành các sub-queries (câu hỏi phụ). + 2. Chạy semantic search cho từng câu hỏi phụ. + 3. Rút trích các thực thể liên quan và nội dung chi tiết của chúng. + 4. Truy vết chuỗi quan hệ (relationship chains). + 5. Tổng hợp toàn bộ tạo thành insight báo cáo chi tiết. Args: - graph_id: 图谱ID - query: 用户问题 - simulation_requirement: 模拟需求描述 - report_context: 报告上下文(可选,用于更精准的子问题生成) - max_sub_queries: 最大子问题数量 + graph_id: ID của đồ thị (Graph ID) + query: Câu hỏi / Yêu cầu của người dùng + simulation_requirement: Yêu cầu mô phỏng + report_context: Ngữ cảnh bản báo cáo (không bắt buộc, dùng để sinh sub-query chính xác hơn) + max_sub_queries: Số lượng câu hỏi phụ lớn nhất tạo ra Returns: - InsightForgeResult: 深度洞察检索结果 + InsightForgeResult: Kết quả dạng tìm kiếm đa chiều """ - logger.info(f"InsightForge 深度洞察检索: {query[:50]}...") + logger.info(f"InsightForge deep search: {query[:50]}...") result = InsightForgeResult( query=query, @@ -978,7 +979,7 @@ def insight_forge( sub_queries=[] ) - # Step 1: 使用LLM生成子问题 + # Bước 1: Dùng LLM sinh các câu hỏi phụ sub_queries = self._generate_sub_queries( query=query, simulation_requirement=simulation_requirement, @@ -986,9 +987,9 @@ def insight_forge( max_queries=max_sub_queries ) result.sub_queries = sub_queries - logger.info(f"生成 {len(sub_queries)} 个子问题") + logger.info(f"Generated {len(sub_queries)} sub-queries") - # Step 2: 对每个子问题进行语义搜索 + # Bước 2: Thực thi semantic search cho mỗi câu hỏi phụ all_facts = [] all_edges = [] seen_facts = set() @@ -1008,7 +1009,7 @@ def insight_forge( all_edges.extend(search_result.edges) - # 对原始问题也进行搜索 + # Thực hiện tìm kiếm cho riêng câu hỏi gốc nữa main_search = self.search_graph( graph_id=graph_id, query=query, @@ -1023,7 +1024,8 @@ def insight_forge( result.semantic_facts = all_facts result.total_facts = len(all_facts) - # Step 3: 从边中提取相关实体UUID,只获取这些实体的信息(不获取全部节点) + # Bước 3: Lấy các ID của thực thể từ chuỗi cạnh tương ứng (edges) + # Chỉ lấy chi tiết của các thực thể này thay vì tải toàn bộ nodes entity_uuids = set() for edge_data in all_edges: if isinstance(edge_data, dict): @@ -1034,21 +1036,21 @@ def insight_forge( if target_uuid: entity_uuids.add(target_uuid) - # 获取所有相关实体的详情(不限制数量,完整输出) + # Truy xuất chi tiết tất cả thực thể liên quan (Sẽ xuất đầy đủ, không cắt bớt) entity_insights = [] - node_map = {} # 用于后续关系链构建 + node_map = {} # Lưu trữ map node cho bước dựng chain tiếp theo - for uuid in list(entity_uuids): # 处理所有实体,不截断 + for uuid in list(entity_uuids): # Xử lý tất cả các thực thể, không cắt bớt (truncate) if not uuid: continue try: - # 单独获取每个相关节点的信息 + # Gọi API riêng biệt lấy chi tiết từng node node = self.get_node_detail(uuid) if node: node_map[uuid] = node - entity_type = next((l for l in node.labels if l not in ["Entity", "Node"]), "实体") + entity_type = next((l for l in node.labels if l not in ["Entity", "Node"]), "Entity") - # 获取该实体相关的所有事实(不截断) + # Lấy tất cả thông tin fact liên quan đến (các) thực thể này related_facts = [ f for f in all_facts if node.name.lower() in f.lower() @@ -1059,18 +1061,18 @@ def insight_forge( "name": node.name, "type": entity_type, "summary": node.summary, - "related_facts": related_facts # 完整输出,不截断 + "related_facts": related_facts # Trả về toàn bộ danh sách, không cắt bớt }) except Exception as e: - logger.debug(f"获取节点 {uuid} 失败: {e}") + logger.debug(f"Failed to fetch node {uuid}: {e}") continue result.entity_insights = entity_insights result.total_entities = len(entity_insights) - # Step 4: 构建所有关系链(不限制数量) + # Bước 4: Khôi phục tất cả các chuỗi quan hệ (không giới hạn số lượng) relationship_chains = [] - for edge_data in all_edges: # 处理所有边,不截断 + for edge_data in all_edges: # Xử lý toàn bộ các edges if isinstance(edge_data, dict): source_uuid = edge_data.get('source_node_uuid', '') target_uuid = edge_data.get('target_node_uuid', '') @@ -1086,7 +1088,7 @@ def insight_forge( result.relationship_chains = relationship_chains result.total_relationships = len(relationship_chains) - logger.info(f"InsightForge完成: {result.total_facts}条事实, {result.total_entities}个实体, {result.total_relationships}条关系") + logger.info(f"InsightForge completed: {result.total_facts} facts, {result.total_entities} entities, {result.total_relationships} relationships") return result def _generate_sub_queries( @@ -1097,27 +1099,28 @@ def _generate_sub_queries( max_queries: int = 5 ) -> List[str]: """ - 使用LLM生成子问题 + Dùng LLM sinh các câu hỏi phụ. - 将复杂问题分解为多个可以独立检索的子问题 + Giúp phân rã một câu hỏi lớn / phức tạp thành nhiều câu hỏi nhỏ lẻ + có thể query độc lập trên cơ sở dữ liệu. """ - system_prompt = """你是一个专业的问题分析专家。你的任务是将一个复杂问题分解为多个可以在模拟世界中独立观察的子问题。 + system_prompt = """You are a professional problem analysis expert. Your task is to break down a complex query into multiple sub-queries that can be independently observed in the simulated world. -要求: -1. 每个子问题应该足够具体,可以在模拟世界中找到相关的Agent行为或事件 -2. 子问题应该覆盖原问题的不同维度(如:谁、什么、为什么、怎么样、何时、何地) -3. 子问题应该与模拟场景相关 -4. 返回JSON格式:{"sub_queries": ["子问题1", "子问题2", ...]}""" +Requirements: +1. Each sub-query should be specific enough to find concrete Agent behaviors or events. +2. Sub-queries should cover different dimensions of the original query (Who, What, Why, How, When, Where). +3. Sub-queries must relate to the simulation context. +4. Return exactly in JSON format: {"sub_queries": ["sub_query 1", "sub_query 2", ...]}""" - user_prompt = f"""模拟需求背景: + user_prompt = f"""Simulation background: {simulation_requirement} -{f"报告上下文:{report_context[:500]}" if report_context else ""} +{f"Report context: {report_context[:500]}" if report_context else ""} -请将以下问题分解为{max_queries}个子问题: +Please break down the following query into {max_queries} sub-queries: {query} -返回JSON格式的子问题列表。""" +Return the JSON format.""" try: response = self.llm.chat_json( @@ -1129,17 +1132,17 @@ def _generate_sub_queries( ) sub_queries = response.get("sub_queries", []) - # 确保是字符串列表 + # Ép kiểu để chắc chắn danh sách toàn kiểu string return [str(sq) for sq in sub_queries[:max_queries]] except Exception as e: - logger.warning(f"生成子问题失败: {str(e)},使用默认子问题") - # 降级:返回基于原问题的变体 + logger.warning(f"Failed to generate sub-queries: {str(e)}, using default sub-queries") + # Hạ cấp (fallback): Trả về các biến thể chung chung của câu hỏi ban đầu return [ query, - f"{query} 的主要参与者", - f"{query} 的原因和影响", - f"{query} 的发展过程" + f"Main participants of {query}", + f"Causes and impacts of {query}", + f"Development process of {query}" ][:max_queries] def panorama_search( @@ -1150,40 +1153,40 @@ def panorama_search( limit: int = 50 ) -> PanoramaResult: """ - 【PanoramaSearch - 广度搜索】 + 【PanoramaSearch - Tìm kiếm theo chiều rộng / Panorama Search】 - 获取全貌视图,包括所有相关内容和历史/过期信息: - 1. 获取所有相关节点 - 2. 获取所有边(包括已过期/失效的) - 3. 分类整理当前有效和历史信息 + Lấy góc nhìn toàn cảnh, bao gồm tất cả các nội dung liên quan kể cả lịch sử/hết hạn: + 1. Lấy tất cả nodes (thực thể). + 2. Lấy tất cả edges (mối quan hệ), bao gồm cả những sự kiện đã lỗi thời (expired). + 3. Phân loại và phân nhóm các loại thông tin thời gian thực / lịch sử. - 这个工具适用于需要了解事件全貌、追踪演变过程的场景。 + Công cụ này phù hợp khi cần một cái nhìn tổng thể về một sự kiện và diễn biến theo thời gian của nó. Args: - graph_id: 图谱ID - query: 搜索查询(用于相关性排序) - include_expired: 是否包含过期内容(默认True) - limit: 返回结果数量限制 + graph_id: ID đồ thị + query: Truy vấn tìm kiếm (để sắp xếp theo độ phù hợp) + include_expired: Cờ bao gồm cả sự kiện hết hạn (mặc định là True) + limit: Số lượng items giới hạn lúc trả về Returns: - PanoramaResult: 广度搜索结果 + PanoramaResult: Kết quả dạng toàn cảnh """ - logger.info(f"PanoramaSearch 广度搜索: {query[:50]}...") + logger.info(f"PanoramaSearch broad search: {query[:50]}...") result = PanoramaResult(query=query) - # 获取所有节点 + # Lấy toàn bộ thực thể all_nodes = self.get_all_nodes(graph_id) node_map = {n.uuid: n for n in all_nodes} result.all_nodes = all_nodes result.total_nodes = len(all_nodes) - # 获取所有边(包含时间信息) + # Lấy toàn bộ mối quan hệ (Cần lấy theo temporal) all_edges = self.get_all_edges(graph_id, include_temporal=True) result.all_edges = all_edges result.total_edges = len(all_edges) - # 分类事实 + # Phân loại facts (sự thật/cạnh) active_facts = [] historical_facts = [] @@ -1191,28 +1194,29 @@ def panorama_search( if not edge.fact: continue - # 为事实添加实体名称 + # Khôi phục tên thực thể từ ID để hiển thị đẹp hơn source_name = node_map.get(edge.source_node_uuid, NodeInfo('', '', [], '', {})).name or edge.source_node_uuid[:8] target_name = node_map.get(edge.target_node_uuid, NodeInfo('', '', [], '', {})).name or edge.target_node_uuid[:8] - # 判断是否过期/失效 + # Nhận biết dữ liệu bị hết hạn / lịch sử dựa vào cờ thời gian ZepCloud cung cấp is_historical = edge.is_expired or edge.is_invalid if is_historical: - # 历史/过期事实,添加时间标记 - valid_at = edge.valid_at or "未知" - invalid_at = edge.invalid_at or edge.expired_at or "未知" + # Dữ liệu lịch sử, cần chú thích lại mốc thời gian rõ ràng khi in ra + valid_at = edge.valid_at or "Unknown" + invalid_at = edge.invalid_at or edge.expired_at or "Unknown" fact_with_time = f"[{valid_at} - {invalid_at}] {edge.fact}" historical_facts.append(fact_with_time) else: - # 当前有效事实 + # Dữ liệu hiện đang hiệu lực trong bối cảnh snapshot mới nhất active_facts.append(edge.fact) - # 基于查询进行相关性排序 + # Phân rã query thành từ khóa query_lower = query.lower() keywords = [w.strip() for w in query_lower.replace(',', ' ').replace(',', ' ').split() if len(w.strip()) > 1] def relevance_score(fact: str) -> int: + """Hàm tính điểm mức độ phù hợp để xếp hạng các sự kiện/thực thể vừa trích xuất""" fact_lower = fact.lower() score = 0 if query_lower in fact_lower: @@ -1222,7 +1226,7 @@ def relevance_score(fact: str) -> int: score += 10 return score - # 排序并限制数量 + # Sắp xếp dựa theo điểm từ cao đến thấp và lọc theo limit active_facts.sort(key=relevance_score, reverse=True) historical_facts.sort(key=relevance_score, reverse=True) @@ -1231,7 +1235,7 @@ def relevance_score(fact: str) -> int: result.active_count = len(active_facts) result.historical_count = len(historical_facts) - logger.info(f"PanoramaSearch完成: {result.active_count}条有效, {result.historical_count}条历史") + logger.info(f"PanoramaSearch completed: {result.active_count} active facts, {result.historical_count} historical facts") return result def quick_search( @@ -1241,24 +1245,24 @@ def quick_search( limit: int = 10 ) -> SearchResult: """ - 【QuickSearch - 简单搜索】 + 【QuickSearch - Tìm kiếm cơ bản】 - 快速、轻量级的检索工具: - 1. 直接调用Zep语义搜索 - 2. 返回最相关的结果 - 3. 适用于简单、直接的检索需求 + Công cụ tìm kiếm nhỏ gọn: + 1. Gọi trực tiếp Zep Semantic Search + 2. Trả về kết quả phù hợp nhất nguyên bản + 3. Phù hợp cho những nhu cầu tìm kiếm đơn giản, trực tiếp Args: - graph_id: 图谱ID - query: 搜索查询 - limit: 返回结果数量 + graph_id: ID đồ thị + query: Từ khóa truy vấn + limit: Số lượng kết quả Returns: - SearchResult: 搜索结果 + SearchResult: Kết quả của tìm kiếm cơ bản """ - logger.info(f"QuickSearch 简单搜索: {query[:50]}...") + logger.info(f"QuickSearch basic search: {query[:50]}...") - # 直接调用现有的search_graph方法 + # Gọi trực tiếp method search_graph hiện tại result = self.search_graph( graph_id=graph_id, query=query, @@ -1266,7 +1270,7 @@ def quick_search( scope="edges" ) - logger.info(f"QuickSearch完成: {result.total_count}条结果") + logger.info(f"QuickSearch completed: {result.total_count} results") return result def interview_agents( @@ -1278,53 +1282,53 @@ def interview_agents( custom_questions: List[str] = None ) -> InterviewResult: """ - 【InterviewAgents - 深度采访】 + 【InterviewAgents - Phỏng vấn Sâu / Interview Agents】 - 调用真实的OASIS采访API,采访模拟中正在运行的Agent: - 1. 自动读取人设文件,了解所有模拟Agent - 2. 使用LLM分析采访需求,智能选择最相关的Agent - 3. 使用LLM生成采访问题 - 4. 调用 /api/simulation/interview/batch 接口进行真实采访(双平台同时采访) - 5. 整合所有采访结果,生成采访报告 + Gọi API phỏng vấn OASIS thực thụ để phỏng vấn các Agents đang chạy trong mô phỏng: + 1. Tự động đọc file thiết lập character (profile), nắm bắt tất cả Agents. + 2. Dùng LLM phân tích yêu cầu phỏng vấn, chọn lọc Agent phù hợp một cách thông minh. + 3. Dùng LLM sinh ra các bộ câu hỏi phỏng vấn. + 4. Gọi API /api/simulation/interview/batch tiến hành phỏng vấn thực tế (phỏng vấn đồng thời trên 2 nền tảng nếu có). + 5. Tổng hợp lại mọi câu trả lời và báo cáo. - 【重要】此功能需要模拟环境处于运行状态(OASIS环境未关闭) + 【QUAN TRỌNG】 Chức năng này yêu cầu Môi trường Mô phỏng đang chạy (OASIS environment chưa bị đóng). - 【使用场景】 - - 需要从不同角色视角了解事件看法 - - 需要收集多方意见和观点 - - 需要获取模拟Agent的真实回答(非LLM模拟) + 【Use cases - Trường hợp dùng】 + - Muốn xem nhận định từ các góc nhìn khác nhau (ví dụ: góc nhìn từ học sinh/giáo viên). + - Thu thập ý kiến, quan điểm đa chiều. + - Chờ lấy câu trả lời THỰC TẾ từ sim Agents (chứ không phải cho LLM giả lập câu trả lời). Args: - simulation_id: 模拟ID(用于定位人设文件和调用采访API) - interview_requirement: 采访需求描述(非结构化,如"了解学生对事件的看法") - simulation_requirement: 模拟需求背景(可选) - max_agents: 最多采访的Agent数量 - custom_questions: 自定义采访问题(可选,若不提供则自动生成) + simulation_id: ID mô phỏng (dùng để định vị file profiles và call target API). + interview_requirement: Mục đích phỏng vấn phi cấu trúc (ví dụ: "Muốn biết học sinh nghĩ thế nào về vụ này"). + simulation_requirement: Yêu cầu của hệ thống ban đầu (không bắt buộc). + max_agents: Số điện lượng Agents tối đa muốn phỏng vấn. + custom_questions: Các câu hỏi điền tay (nếu không cung cấp thì sẽ tự tạo). Returns: - InterviewResult: 采访结果 + InterviewResult: Kẻt quả phỏng vấn tổng hợp """ from .simulation_runner import SimulationRunner - logger.info(f"InterviewAgents 深度采访(真实API): {interview_requirement[:50]}...") + logger.info(f"InterviewAgents Deep Interview (Real API): {interview_requirement[:50]}...") result = InterviewResult( interview_topic=interview_requirement, interview_questions=custom_questions or [] ) - # Step 1: 读取人设文件 + # Bước 1: Load file cấu hình agent profiles profiles = self._load_agent_profiles(simulation_id) if not profiles: - logger.warning(f"未找到模拟 {simulation_id} 的人设文件") - result.summary = "未找到可采访的Agent人设文件" + logger.warning(f"Did not find agent profiles for simulation {simulation_id}") + result.summary = "Agent profiles not found for interview" return result result.total_agents = len(profiles) - logger.info(f"加载到 {len(profiles)} 个Agent人设") + logger.info(f"Loaded {len(profiles)} agent profiles") - # Step 2: 使用LLM选择要采访的Agent(返回agent_id列表) + # Bước 2: Nhờ LLM lựa chọn Agent để phỏng vấn (sẽ trả về mảng agent_id) selected_agents, selected_indices, selection_reasoning = self._select_agents_for_interview( profiles=profiles, interview_requirement=interview_requirement, @@ -1334,123 +1338,123 @@ def interview_agents( result.selected_agents = selected_agents result.selection_reasoning = selection_reasoning - logger.info(f"选择了 {len(selected_agents)} 个Agent进行采访: {selected_indices}") + logger.info(f"Selected {len(selected_agents)} agents for interview: {selected_indices}") - # Step 3: 生成采访问题(如果没有提供) + # Bước 3: Sinh câu hỏi phỏng vấn (nếu user không đưa sẵn) if not result.interview_questions: result.interview_questions = self._generate_interview_questions( interview_requirement=interview_requirement, simulation_requirement=simulation_requirement, selected_agents=selected_agents ) - logger.info(f"生成了 {len(result.interview_questions)} 个采访问题") + logger.info(f"Generated {len(result.interview_questions)} interview questions") - # 将问题合并为一个采访prompt + # Gộp các câu hỏi lại tạo thành 1 chuỗi prompt phỏng vấn hoàn chỉnh combined_prompt = "\n".join([f"{i+1}. {q}" for i, q in enumerate(result.interview_questions)]) - # 添加优化前缀,约束Agent回复格式 + # Thêm các prefix tối ưu hoá, ràng buộc format câu trả lời của Agent INTERVIEW_PROMPT_PREFIX = ( - "你正在接受一次采访。请结合你的人设、所有的过往记忆与行动," - "以纯文本方式直接回答以下问题。\n" - "回复要求:\n" - "1. 直接用自然语言回答,不要调用任何工具\n" - "2. 不要返回JSON格式或工具调用格式\n" - "3. 不要使用Markdown标题(如#、##、###)\n" - "4. 按问题编号逐一回答,每个回答以「问题X:」开头(X为问题编号)\n" - "5. 每个问题的回答之间用空行分隔\n" - "6. 回答要有实质内容,每个问题至少回答2-3句话\n\n" + "You are being interviewed. Please combine your profile, all your past memories and actions, " + "and directly answer the following questions in pure text.\n" + "Reply requirements:\n" + "1. Answer directly in natural language, do not call any tools.\n" + "2. Do not return JSON format or tool call formats.\n" + "3. Do not use Markdown headers (like #, ##, ###).\n" + "4. Answer questions one by one according to their numbers, start each answer with 'Question X:' (X is the number).\n" + "5. Separate each answer with a blank line.\n" + "6. Answers must have substance, at least 2-3 sentences per question.\n\n" ) optimized_prompt = f"{INTERVIEW_PROMPT_PREFIX}{combined_prompt}" - # Step 4: 调用真实的采访API(不指定platform,默认双平台同时采访) + # Bước 4: Gọi trực tiếp API Phỏng vấn (Mặc định không chỉ định platform để chạy trên cả 2 nền tảng) try: - # 构建批量采访列表(不指定platform,双平台采访) + # Dựng cấu trúc payload của batch request (không truyền param platform sẽ ngầm hiểu bằng trên cả 2 nền tảng) interviews_request = [] for agent_idx in selected_indices: interviews_request.append({ "agent_id": agent_idx, - "prompt": optimized_prompt # 使用优化后的prompt - # 不指定platform,API会在twitter和reddit两个平台都采访 + "prompt": optimized_prompt # Dùng prompt đã bọc prefix tối ưu + # Bỏ trống platform -> API sẽ call tới Agent của trên Twitter và Reddit list }) - logger.info(f"调用批量采访API(双平台): {len(interviews_request)} 个Agent") + logger.info(f"Calling batch interview API (dual platforms): {len(interviews_request)} agents") - # 调用 SimulationRunner 的批量采访方法(不传platform,双平台采访) + # Khởi động calling via class SimulationRunner method (thời gian timeout phải cao do chọc 2 nền tảng song song) api_result = SimulationRunner.interview_agents_batch( simulation_id=simulation_id, interviews=interviews_request, - platform=None, # 不指定platform,双平台采访 - timeout=180.0 # 双平台需要更长超时 + platform=None, # Không định dạng nền tảng -> Dual platform call + timeout=180.0 # Tăng timeout do phải chờ API trên 2 platforms xử lý ) - logger.info(f"采访API返回: {api_result.get('interviews_count', 0)} 个结果, success={api_result.get('success')}") + logger.info(f"Interview API returned: {api_result.get('interviews_count', 0)} results, success={api_result.get('success')}") - # 检查API调用是否成功 + # Xác nhận API có success không hay failure if not api_result.get("success", False): - error_msg = api_result.get("error", "未知错误") - logger.warning(f"采访API返回失败: {error_msg}") - result.summary = f"采访API调用失败:{error_msg}。请检查OASIS模拟环境状态。" + error_msg = api_result.get("error", "Unknown error") + logger.warning(f"Interview API failed: {error_msg}") + result.summary = f"Interview API call failed: {error_msg}. Please check OASIS simulation status." return result - # Step 5: 解析API返回结果,构建AgentInterview对象 - # 双平台模式返回格式: {"twitter_0": {...}, "reddit_0": {...}, "twitter_1": {...}, ...} + # Bước 5: Bóc tách data từ JSON output của API, biên dịch sang Object `AgentInterview` + # Cấu trúc của dual platform output: {"twitter_0": {...}, "reddit_0": {...}, "twitter_1": {...}, ...} api_data = api_result.get("result", {}) results_dict = api_data.get("results", {}) if isinstance(api_data, dict) else {} for i, agent_idx in enumerate(selected_indices): agent = selected_agents[i] agent_name = agent.get("realname", agent.get("username", f"Agent_{agent_idx}")) - agent_role = agent.get("profession", "未知") + agent_role = agent.get("profession", "Unknown") agent_bio = agent.get("bio", "") - # 获取该Agent在两个平台的采访结果 + # Fetch cả response text trên hai sàn twitter_result = results_dict.get(f"twitter_{agent_idx}", {}) reddit_result = results_dict.get(f"reddit_{agent_idx}", {}) twitter_response = twitter_result.get("response", "") reddit_response = reddit_result.get("response", "") - # 清理可能的工具调用 JSON 包裹 + # Thao tác dọn dẹp (phòng trường hợp API xuất bừa JSON của function tools) twitter_response = self._clean_tool_call_response(twitter_response) reddit_response = self._clean_tool_call_response(reddit_response) - # 始终输出双平台标记 - twitter_text = twitter_response if twitter_response else "(该平台未获得回复)" - reddit_text = reddit_response if reddit_response else "(该平台未获得回复)" - response_text = f"【Twitter平台回答】\n{twitter_text}\n\n【Reddit平台回答】\n{reddit_text}" + # Format lại khi xuất log hoặc hiển thị (chỉ rõ câu trả lời nào từ nền tảng nào) + twitter_text = twitter_response if twitter_response else "(No reply received on this platform)" + reddit_text = reddit_response if reddit_response else "(No reply received on this platform)" + response_text = f"【Twitter Platform】\n{twitter_text}\n\n【Reddit Platform】\n{reddit_text}" - # 提取关键引言(从两个平台的回答中) + # Gộp chung nội dung phục vụ tìm các câu trích dẫn tiêu biểu (quotes) import re combined_responses = f"{twitter_response} {reddit_response}" - # 清理响应文本:去掉标记、编号、Markdown 等干扰 + # Lược bỏ các keyword nhiễu (Markdown format, số thứ tự, config name tool, prefix, ...) clean_text = re.sub(r'#{1,6}\s+', '', combined_responses) clean_text = re.sub(r'\{[^}]*tool_name[^}]*\}', '', clean_text) clean_text = re.sub(r'[*_`|>~\-]{2,}', '', clean_text) - clean_text = re.sub(r'问题\d+[::]\s*', '', clean_text) + clean_text = re.sub(r'Question\s*\d+[::]\s*', '', clean_text) clean_text = re.sub(r'【[^】]+】', '', clean_text) - # 策略1(主): 提取完整的有实质内容的句子 - sentences = re.split(r'[。!?]', clean_text) + # Luồng số 1 : Mổ câu theo dấu kết thúc cầu (. ? !). Chọn các câu đủ độ dài (không quá ngắn ko quá dài) + sentences = re.split(r'[.!?。!?]', clean_text) meaningful = [ s.strip() for s in sentences if 20 <= len(s.strip()) <= 150 and not re.match(r'^[\s\W,,;;::、]+', s.strip()) - and not s.strip().startswith(('{', '问题')) + and not s.strip().startswith(('{', 'Question')) ] meaningful.sort(key=len, reverse=True) - key_quotes = [s + "。" for s in meaningful[:3]] + key_quotes = [s + "." for s in meaningful[:3]] - # 策略2(补充): 正确配对的中文引号「」内长文本 + # Luồng số 2 : Trích xuất từ dấu ngoặc kéo nếu như regex 1 thất bại if not key_quotes: - paired = re.findall(r'\u201c([^\u201c\u201d]{15,100})\u201d', clean_text) + paired = re.findall(r'["\u201c]([^\u201c\u201d"]{15,100})["\u201d]', clean_text) paired += re.findall(r'\u300c([^\u300c\u300d]{15,100})\u300d', clean_text) key_quotes = [q for q in paired if not re.match(r'^[,,;;::、]', q)][:3] interview = AgentInterview( agent_name=agent_name, agent_role=agent_role, - agent_bio=agent_bio[:1000], # 扩大bio长度限制 + agent_bio=agent_bio[:1000], # Tăng giới hạn độ dài của bio question=combined_prompt, response=response_text, key_quotes=key_quotes[:5] @@ -1460,30 +1464,30 @@ def interview_agents( result.interviewed_count = len(result.interviews) except ValueError as e: - # 模拟环境未运行 - logger.warning(f"采访API调用失败(环境未运行?): {e}") - result.summary = f"采访失败:{str(e)}。模拟环境可能已关闭,请确保OASIS环境正在运行。" + # Nếu Môi trường chưa được khởi động + logger.warning(f"Interview API failed (environment not running?): {e}") + result.summary = f"Interview failed: {str(e)}. Simulation environment might be closed, please ensure OASIS environment is running." return result except Exception as e: - logger.error(f"采访API调用异常: {e}") + logger.error(f"Interview API exception: {e}") import traceback logger.error(traceback.format_exc()) - result.summary = f"采访过程发生错误:{str(e)}" + result.summary = f"Error during interview: {str(e)}" return result - # Step 6: 生成采访摘要 + # Bước 6: Tổng hợp lại thành Summary hoàn chỉnh if result.interviews: result.summary = self._generate_interview_summary( interviews=result.interviews, interview_requirement=interview_requirement ) - logger.info(f"InterviewAgents完成: 采访了 {result.interviewed_count} 个Agent(双平台)") + logger.info(f"InterviewAgents completed: Interviewed {result.interviewed_count} agents (dual platform)") return result @staticmethod def _clean_tool_call_response(response: str) -> str: - """清理 Agent 回复中的 JSON 工具调用包裹,提取实际内容""" + """Dọn dẹp chuỗi JSON của tool call trong câu trả lời từ Agent, và xuất ra content thật sự (nếu có)""" if not response or not response.strip().startswith('{'): return response text = response.strip() @@ -1503,11 +1507,11 @@ def _clean_tool_call_response(response: str) -> str: return response def _load_agent_profiles(self, simulation_id: str) -> List[Dict[str, Any]]: - """加载模拟的Agent人设文件""" + """Tải file chứa danh sách profile của các Agents trong kịch bản mô phỏng""" import os import csv - # 构建人设文件路径 + # Đường dẫn cấu trúc tới thư mục mô phỏng sim_dir = os.path.join( os.path.dirname(__file__), f'../../uploads/simulations/{simulation_id}' @@ -1515,36 +1519,36 @@ def _load_agent_profiles(self, simulation_id: str) -> List[Dict[str, Any]]: profiles = [] - # 优先尝试读取Reddit JSON格式 + # Cố gắng ưu tiên tải định dạng JSON của Reddit reddit_profile_path = os.path.join(sim_dir, "reddit_profiles.json") if os.path.exists(reddit_profile_path): try: with open(reddit_profile_path, 'r', encoding='utf-8') as f: profiles = json.load(f) - logger.info(f"从 reddit_profiles.json 加载了 {len(profiles)} 个人设") + logger.info(f"Loaded {len(profiles)} profiles from reddit_profiles.json") return profiles except Exception as e: - logger.warning(f"读取 reddit_profiles.json 失败: {e}") + logger.warning(f"Failed to read reddit_profiles.json: {e}") - # 尝试读取Twitter CSV格式 + # Nếu không có hoặc lỗi, thử tải định dạng CSV của Twitter twitter_profile_path = os.path.join(sim_dir, "twitter_profiles.csv") if os.path.exists(twitter_profile_path): try: with open(twitter_profile_path, 'r', encoding='utf-8') as f: reader = csv.DictReader(f) for row in reader: - # CSV格式转换为统一格式 + # Chuẩn hóa về format chung profiles.append({ "realname": row.get("name", ""), "username": row.get("username", ""), "bio": row.get("description", ""), "persona": row.get("user_char", ""), - "profession": "未知" + "profession": "Unknown" }) - logger.info(f"从 twitter_profiles.csv 加载了 {len(profiles)} 个人设") + logger.info(f"Loaded {len(profiles)} profiles from twitter_profiles.csv") return profiles except Exception as e: - logger.warning(f"读取 twitter_profiles.csv 失败: {e}") + logger.warning(f"Failed to read twitter_profiles.csv: {e}") return profiles @@ -1556,51 +1560,51 @@ def _select_agents_for_interview( max_agents: int ) -> tuple: """ - 使用LLM选择要采访的Agent + Sử dụng LLM phân tích profile và lựa chọn các Agent phù hợp nhất cho phỏng vấn. Returns: tuple: (selected_agents, selected_indices, reasoning) - - selected_agents: 选中Agent的完整信息列表 - - selected_indices: 选中Agent的索引列表(用于API调用) - - reasoning: 选择理由 + - selected_agents: Danh sách info hoàn chỉnh của các Agent được chọn + - selected_indices: Danh sách số index (phục vụ gọi API phỏng vấn sau này) + - reasoning: Mô tả vì sao chọn các role/agent này """ - # 构建Agent摘要列表 + # Lược trích ngắn lại các profile đem cho LLM đọc để tiết kiệm token agent_summaries = [] for i, profile in enumerate(profiles): summary = { "index": i, "name": profile.get("realname", profile.get("username", f"Agent_{i}")), - "profession": profile.get("profession", "未知"), - "bio": profile.get("bio", "")[:200], + "profession": profile.get("profession", "Unknown"), + "bio": profile.get("bio", "")[:200], # Cắt ngắn bio "interested_topics": profile.get("interested_topics", []) } agent_summaries.append(summary) - system_prompt = """你是一个专业的采访策划专家。你的任务是根据采访需求,从模拟Agent列表中选择最适合采访的对象。 + system_prompt = """You are a professional interview planning expert. Your task is to select the most suitable target agents for an interview based on requirements. -选择标准: -1. Agent的身份/职业与采访主题相关 -2. Agent可能持有独特或有价值的观点 -3. 选择多样化的视角(如:支持方、反对方、中立方、专业人士等) -4. 优先选择与事件直接相关的角色 +Selection criteria: +1. Agent identity/profession is related to the interview topic. +2. Agent might hold unique or valuable opinions. +3. Select diverse perspectives (e.g., supporters, opponents, neutrals, professionals, etc.). +4. Prioritize characters directly related to the event. -返回JSON格式: +Return in JSON format: { - "selected_indices": [选中Agent的索引列表], - "reasoning": "选择理由说明" + "selected_indices": [array of selected agent indices], + "reasoning": "explanation for the selection" }""" - user_prompt = f"""采访需求: + user_prompt = f"""Interview requirements: {interview_requirement} -模拟背景: -{simulation_requirement if simulation_requirement else "未提供"} +Simulation background: +{simulation_requirement if simulation_requirement else "Not provided"} -可选择的Agent列表(共{len(agent_summaries)}个): +Available Agents (total {len(agent_summaries)}): {json.dumps(agent_summaries, ensure_ascii=False, indent=2)} -请选择最多{max_agents}个最适合采访的Agent,并说明选择理由。""" +Please select up to {max_agents} most suitable agents for the interview and explain your reasoning.""" try: response = self.llm.chat_json( @@ -1612,9 +1616,9 @@ def _select_agents_for_interview( ) selected_indices = response.get("selected_indices", [])[:max_agents] - reasoning = response.get("reasoning", "基于相关性自动选择") + reasoning = response.get("reasoning", "Auto selected based on relevance") - # 获取选中的Agent完整信息 + # Map index vào lại danh sách profile chuẩn selected_agents = [] valid_indices = [] for idx in selected_indices: @@ -1625,11 +1629,11 @@ def _select_agents_for_interview( return selected_agents, valid_indices, reasoning except Exception as e: - logger.warning(f"LLM选择Agent失败,使用默认选择: {e}") - # 降级:选择前N个 + logger.warning(f"LLM failed to select agents, falling back to default: {e}") + # Hạ cấp (fallback): Chọn N Agent đầu tiên trong danh sách selected = profiles[:max_agents] indices = list(range(min(max_agents, len(profiles)))) - return selected, indices, "使用默认选择策略" + return selected, indices, "Using default selection strategy" def _generate_interview_questions( self, @@ -1637,29 +1641,29 @@ def _generate_interview_questions( simulation_requirement: str, selected_agents: List[Dict[str, Any]] ) -> List[str]: - """使用LLM生成采访问题""" + """Sử dụng LLM để sinh ra các câu chất vấn hợp với tính chất sự việc""" - agent_roles = [a.get("profession", "未知") for a in selected_agents] + agent_roles = [a.get("profession", "Unknown") for a in selected_agents] - system_prompt = """你是一个专业的记者/采访者。根据采访需求,生成3-5个深度采访问题。 + system_prompt = """You are a professional journalist/interviewer. Generate 3-5 deep interview questions based on requirements. -问题要求: -1. 开放性问题,鼓励详细回答 -2. 针对不同角色可能有不同答案 -3. 涵盖事实、观点、感受等多个维度 -4. 语言自然,像真实采访一样 -5. 每个问题控制在50字以内,简洁明了 -6. 直接提问,不要包含背景说明或前缀 +Question requirements: +1. Open-ended questions, encourage detailed answers. +2. Formulated so different roles might have different answers. +3. Cover multiple dimensions like facts, opinions, feelings, etc. +4. Natural language, sounds like a real interview. +5. Keep each question within 50 words, concise and clear. +6. Ask directly, do not include background explanations or prefixes. -返回JSON格式:{"questions": ["问题1", "问题2", ...]}""" +Return in JSON format: {"questions": ["question 1", "question 2", ...]}""" - user_prompt = f"""采访需求:{interview_requirement} + user_prompt = f"""Interview requirements: {interview_requirement} -模拟背景:{simulation_requirement if simulation_requirement else "未提供"} +Simulation background: {simulation_requirement if simulation_requirement else "Not provided"} -采访对象角色:{', '.join(agent_roles)} +Interviewee roles: {', '.join(agent_roles)} -请生成3-5个采访问题。""" +Please generate 3-5 interview questions.""" try: response = self.llm.chat_json( @@ -1670,14 +1674,14 @@ def _generate_interview_questions( temperature=0.5 ) - return response.get("questions", [f"关于{interview_requirement},您有什么看法?"]) + return response.get("questions", [f"What is your opinion on {interview_requirement}?"]) except Exception as e: - logger.warning(f"生成采访问题失败: {e}") + logger.warning(f"Failed to generate interview questions: {e}") return [ - f"关于{interview_requirement},您的观点是什么?", - "这件事对您或您所代表的群体有什么影响?", - "您认为应该如何解决或改进这个问题?" + f"What is your perspective on {interview_requirement}?", + "How does this event impact you or the group you represent?", + "How do you think this issue should be resolved or improved?" ] def _generate_interview_summary( @@ -1685,38 +1689,38 @@ def _generate_interview_summary( interviews: List[AgentInterview], interview_requirement: str ) -> str: - """生成采访摘要""" + """Tạo bản tóm tắt nội dung sau khi phỏng vấn""" if not interviews: - return "未完成任何采访" + return "No interviews completed" - # 收集所有采访内容 + # Gom các trả lời phỏng vấn lại cho LLM tóm tắt interview_texts = [] for interview in interviews: interview_texts.append(f"【{interview.agent_name}({interview.agent_role})】\n{interview.response[:500]}") - system_prompt = """你是一个专业的新闻编辑。请根据多位受访者的回答,生成一份采访摘要。 + system_prompt = """You are a professional news editor. Please generate an interview summary based on the answers from multiple interviewees. -摘要要求: -1. 提炼各方主要观点 -2. 指出观点的共识和分歧 -3. 突出有价值的引言 -4. 客观中立,不偏袒任何一方 -5. 控制在1000字内 +Summary requirements: +1. Extract main viewpoints from all parties. +2. Point out consensus and disagreements among opinions. +3. Highlight valuable quotes. +4. Objective and neutral, do not favor any side. +5. Keep it within 1000 words. -格式约束(必须遵守): -- 使用纯文本段落,用空行分隔不同部分 -- 不要使用Markdown标题(如#、##、###) -- 不要使用分割线(如---、***) -- 引用受访者原话时使用中文引号「」 -- 可以使用**加粗**标记关键词,但不要使用其他Markdown语法""" +Formatting constraints (Must obey): +- Use plain text paragraphs, separate different sections with blank lines. +- Do not use Markdown headers (like #, ##, ###). +- Do not use dividers (like ---, ***). +- Use normal quotes when citing interviewee actions/words. +- You can use **bold** to mark keywords, but no other Markdown syntax.""" - user_prompt = f"""采访主题:{interview_requirement} + user_prompt = f"""Interview topic: {interview_requirement} -采访内容: -{"".join(interview_texts)} +Interview content: +{"\n\n".join(interview_texts)} -请生成采访摘要。""" +Please generate the interview summary.""" try: summary = self.llm.chat( @@ -1730,6 +1734,6 @@ def _generate_interview_summary( return summary except Exception as e: - logger.warning(f"生成采访摘要失败: {e}") - # 降级:简单拼接 - return f"共采访了{len(interviews)}位受访者,包括:" + "、".join([i.agent_name for i in interviews]) + logger.warning(f"Failed to generate interview summary: {e}") + # Hạ cấp (fallback): Nối chuỗi cơ bản kèm theo tên những người được phỏng vấn + return f"Interviewed {len(interviews)} people in total, including: " + ", ".join([i.agent_name for i in interviews]) diff --git a/backend/app/utils/__init__.py b/backend/app/utils/__init__.py index 5848792b8..f9f976140 100644 --- a/backend/app/utils/__init__.py +++ b/backend/app/utils/__init__.py @@ -1,5 +1,5 @@ """ -工具模块 +Mô-đun tiện ích """ from .file_parser import FileParser diff --git a/backend/app/utils/file_parser.py b/backend/app/utils/file_parser.py index 3f1d8ed2e..f486fd4e4 100644 --- a/backend/app/utils/file_parser.py +++ b/backend/app/utils/file_parser.py @@ -1,6 +1,6 @@ """ -文件解析工具 -支持PDF、Markdown、TXT文件的文本提取 +Tiện ích phân tích tệp +Hỗ trợ trích xuất văn bản từ tệp PDF, Markdown, TXT """ import os @@ -10,29 +10,29 @@ def _read_text_with_fallback(file_path: str) -> str: """ - 读取文本文件,UTF-8失败时自动探测编码。 - - 采用多级回退策略: - 1. 首先尝试 UTF-8 解码 - 2. 使用 charset_normalizer 检测编码 - 3. 回退到 chardet 检测编码 - 4. 最终使用 UTF-8 + errors='replace' 兜底 - + Đọc tệp văn bản, tự động phát hiện mã hóa nếu UTF-8 thất bại. + + Áp dụng chiến lược fallback nhiều tầng: + 1. Thử giải mã bằng UTF-8 trước + 2. Dùng charset_normalizer để phát hiện mã hóa + 3. Fallback sang chardet de phat hien ma hoa + 4. Cuối cùng dùng UTF-8 + errors='replace' để đảm bảo không vỡ ký tự + Args: - file_path: 文件路径 - + file_path: Đường dẫn tệp + Returns: - 解码后的文本内容 + Nội dung văn bản sau khi giải mã """ data = Path(file_path).read_bytes() - # 首先尝试 UTF-8 + # Thử UTF-8 trước try: return data.decode('utf-8') except UnicodeDecodeError: pass - # 尝试使用 charset_normalizer 检测编码 + # Thử phát hiện mã hóa bằng charset_normalizer encoding = None try: from charset_normalizer import from_bytes @@ -42,7 +42,7 @@ def _read_text_with_fallback(file_path: str) -> str: except Exception: pass - # 回退到 chardet + # Fallback sang chardet if not encoding: try: import chardet @@ -51,7 +51,7 @@ def _read_text_with_fallback(file_path: str) -> str: except Exception: pass - # 最终兜底:使用 UTF-8 + replace + # Fallback cuối: UTF-8 + replace if not encoding: encoding = 'utf-8' @@ -59,30 +59,30 @@ def _read_text_with_fallback(file_path: str) -> str: class FileParser: - """文件解析器""" + """Bộ phân tích tệp""" SUPPORTED_EXTENSIONS = {'.pdf', '.md', '.markdown', '.txt'} @classmethod def extract_text(cls, file_path: str) -> str: """ - 从文件中提取文本 - + Trích xuất văn bản từ tệp + Args: - file_path: 文件路径 - + file_path: Đường dẫn tệp + Returns: - 提取的文本内容 + Nội dung văn bản đã trích xuất """ path = Path(file_path) if not path.exists(): - raise FileNotFoundError(f"文件不存在: {file_path}") + raise FileNotFoundError(f"File does not exist: {file_path}") suffix = path.suffix.lower() if suffix not in cls.SUPPORTED_EXTENSIONS: - raise ValueError(f"不支持的文件格式: {suffix}") + raise ValueError(f"Unsupported file format: {suffix}") if suffix == '.pdf': return cls._extract_from_pdf(file_path) @@ -91,15 +91,15 @@ def extract_text(cls, file_path: str) -> str: elif suffix == '.txt': return cls._extract_from_txt(file_path) - raise ValueError(f"无法处理的文件格式: {suffix}") + raise ValueError(f"Cannot process file format: {suffix}") @staticmethod def _extract_from_pdf(file_path: str) -> str: - """从PDF提取文本""" + """Trích xuất văn bản từ PDF""" try: import fitz # PyMuPDF except ImportError: - raise ImportError("需要安装PyMuPDF: pip install PyMuPDF") + raise ImportError("PyMuPDF is required: pip install PyMuPDF") text_parts = [] with fitz.open(file_path) as doc: @@ -112,24 +112,24 @@ def _extract_from_pdf(file_path: str) -> str: @staticmethod def _extract_from_md(file_path: str) -> str: - """从Markdown提取文本,支持自动编码检测""" + """Trích xuất văn bản từ Markdown, hỗ trợ tự động phát hiện mã hóa""" return _read_text_with_fallback(file_path) @staticmethod def _extract_from_txt(file_path: str) -> str: - """从TXT提取文本,支持自动编码检测""" + """Trích xuất văn bản từ TXT, hỗ trợ tự động phát hiện mã hóa""" return _read_text_with_fallback(file_path) @classmethod def extract_from_multiple(cls, file_paths: List[str]) -> str: """ - 从多个文件提取文本并合并 - + Trích xuất văn bản từ nhiều tệp và gộp lại + Args: - file_paths: 文件路径列表 - + file_paths: Danh sách đường dẫn tệp + Returns: - 合并后的文本 + Văn bản đã gộp """ all_texts = [] @@ -137,9 +137,9 @@ def extract_from_multiple(cls, file_paths: List[str]) -> str: try: text = cls.extract_text(file_path) filename = Path(file_path).name - all_texts.append(f"=== 文档 {i}: {filename} ===\n{text}") + all_texts.append(f"=== Document {i}: {filename} ===\n{text}") except Exception as e: - all_texts.append(f"=== 文档 {i}: {file_path} (提取失败: {str(e)}) ===") + all_texts.append(f"=== Document {i}: {file_path} (extract failed: {str(e)}) ===") return "\n\n".join(all_texts) @@ -150,15 +150,15 @@ def split_text_into_chunks( overlap: int = 50 ) -> List[str]: """ - 将文本分割成小块 - + Chia văn bản thành các đoạn nhỏ + Args: - text: 原始文本 - chunk_size: 每块的字符数 - overlap: 重叠字符数 - + text: Văn bản gốc + chunk_size: Số ký tự mỗi đoạn + overlap: Số ký tự chồng lấp + Returns: - 文本块列表 + Danh sách các đoạn văn bản """ if len(text) <= chunk_size: return [text] if text.strip() else [] @@ -169,9 +169,9 @@ def split_text_into_chunks( while start < len(text): end = start + chunk_size - # 尝试在句子边界处分割 + # Cố gắng cắt tại ranh giới câu if end < len(text): - # 查找最近的句子结束符 + # Tìm dấu kết thúc câu gần nhất for sep in ['。', '!', '?', '.\n', '!\n', '?\n', '\n\n', '. ', '! ', '? ']: last_sep = text[start:end].rfind(sep) if last_sep != -1 and last_sep > chunk_size * 0.3: @@ -182,7 +182,7 @@ def split_text_into_chunks( if chunk: chunks.append(chunk) - # 下一个块从重叠位置开始 + # Đoạn tiếp theo bắt đầu từ vị trí overlap start = end - overlap if end < len(text) else len(text) return chunks diff --git a/backend/app/utils/llm_client.py b/backend/app/utils/llm_client.py index 6c1a81f49..faa602940 100644 --- a/backend/app/utils/llm_client.py +++ b/backend/app/utils/llm_client.py @@ -1,6 +1,6 @@ """ -LLM客户端封装 -统一使用OpenAI格式调用 +Lớp bao bọc LLM client +Thống nhất gọi theo định dạng OpenAI """ import json @@ -12,7 +12,7 @@ class LLMClient: - """LLM客户端""" + """LLM client""" def __init__( self, @@ -25,7 +25,7 @@ def __init__( self.model = model or Config.LLM_MODEL_NAME if not self.api_key: - raise ValueError("LLM_API_KEY 未配置") + raise ValueError("LLM_API_KEY is not configured") self.client = OpenAI( api_key=self.api_key, @@ -40,16 +40,16 @@ def chat( response_format: Optional[Dict] = None ) -> str: """ - 发送聊天请求 - + Gửi yêu cầu chat + Args: - messages: 消息列表 - temperature: 温度参数 - max_tokens: 最大token数 - response_format: 响应格式(如JSON模式) - + messages: Danh sách message + temperature: Tham số nhiệt độ + max_tokens: Số token tối đa + response_format: Định dạng response (ví dụ JSON mode) + Returns: - 模型响应文本 + Nội dung response từ model """ kwargs = { "model": self.model, @@ -63,7 +63,7 @@ def chat( response = self.client.chat.completions.create(**kwargs) content = response.choices[0].message.content - # 部分模型(如MiniMax M2.5)会在content中包含思考内容,需要移除 + # Một số model (vd MiniMax M2.5) chèn nội dung vào content, cần loại bỏ content = re.sub(r'[\s\S]*?', '', content).strip() return content @@ -74,15 +74,15 @@ def chat_json( max_tokens: int = 4096 ) -> Dict[str, Any]: """ - 发送聊天请求并返回JSON - + Gửi yêu cầu chat và trả về JSON + Args: - messages: 消息列表 - temperature: 温度参数 - max_tokens: 最大token数 - + messages: Danh sách message + temperature: Tham số nhiệt độ + max_tokens: Số token tối đa + Returns: - 解析后的JSON对象 + JSON object sau khi parse """ response = self.chat( messages=messages, @@ -90,7 +90,7 @@ def chat_json( max_tokens=max_tokens, response_format={"type": "json_object"} ) - # 清理markdown代码块标记 + # Làm sạch markdown code fence cleaned_response = response.strip() cleaned_response = re.sub(r'^```(?:json)?\s*\n?', '', cleaned_response, flags=re.IGNORECASE) cleaned_response = re.sub(r'\n?```\s*$', '', cleaned_response) @@ -99,5 +99,5 @@ def chat_json( try: return json.loads(cleaned_response) except json.JSONDecodeError: - raise ValueError(f"LLM返回的JSON格式无效: {cleaned_response}") + raise ValueError(f"LLM returned invalid JSON: {cleaned_response}") diff --git a/backend/app/utils/logger.py b/backend/app/utils/logger.py index 1978c0b84..50780014f 100644 --- a/backend/app/utils/logger.py +++ b/backend/app/utils/logger.py @@ -1,6 +1,6 @@ """ -日志配置模块 -提供统一的日志管理,同时输出到控制台和文件 +Mô-đun cấu hình logging +Cung cấp quản lý log thống nhất, đồng thời ghi ra console và file """ import os @@ -12,47 +12,47 @@ def _ensure_utf8_stdout(): """ - 确保 stdout/stderr 使用 UTF-8 编码 - 解决 Windows 控制台中文乱码问题 + Đảm bảo stdout/stderr sử dụng mã hóa UTF-8 + Khắc phục lỗi vỡ font tiếng Trung trên console Windows """ if sys.platform == 'win32': - # Windows 下重新配置标准输出为 UTF-8 + # Trên Windows, cấu hình lại stdout/stderr sang UTF-8 if hasattr(sys.stdout, 'reconfigure'): sys.stdout.reconfigure(encoding='utf-8', errors='replace') if hasattr(sys.stderr, 'reconfigure'): sys.stderr.reconfigure(encoding='utf-8', errors='replace') -# 日志目录 +# Thư mục log LOG_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'logs') def setup_logger(name: str = 'mirofish', level: int = logging.DEBUG) -> logging.Logger: """ - 设置日志器 - + Thiết lập logger + Args: - name: 日志器名称 - level: 日志级别 - + name: Tên logger + level: Mức độ log + Returns: - 配置好的日志器 + Logger đã được cấu hình """ - # 确保日志目录存在 + # Đảm bảo thư mục log tồn tại os.makedirs(LOG_DIR, exist_ok=True) - # 创建日志器 + # Tạo logger logger = logging.getLogger(name) logger.setLevel(level) - # 阻止日志向上传播到根 logger,避免重复输出 + # Chặn log propagate lên root logger để tránh in trùng lặp logger.propagate = False - # 如果已经有处理器,不重复添加 + # Nếu đã có handler thì không thêm lại if logger.handlers: return logger - # 日志格式 + # Định dạng log detailed_formatter = logging.Formatter( '[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d] %(message)s', datefmt='%Y-%m-%d %H:%M:%S' @@ -63,7 +63,7 @@ def setup_logger(name: str = 'mirofish', level: int = logging.DEBUG) -> logging. datefmt='%H:%M:%S' ) - # 1. 文件处理器 - 详细日志(按日期命名,带轮转) + # 1. File handler - log chi tiết (đặt tên theo ngày, có rolling) log_filename = datetime.now().strftime('%Y-%m-%d') + '.log' file_handler = RotatingFileHandler( os.path.join(LOG_DIR, log_filename), @@ -74,14 +74,14 @@ def setup_logger(name: str = 'mirofish', level: int = logging.DEBUG) -> logging. file_handler.setLevel(logging.DEBUG) file_handler.setFormatter(detailed_formatter) - # 2. 控制台处理器 - 简洁日志(INFO及以上) - # 确保 Windows 下使用 UTF-8 编码,避免中文乱码 + # 2. Console handler - log gọn (INFO trở lên) + # Đảm bảo Windows dùng UTF-8 để tránh lỗi ký tự _ensure_utf8_stdout() console_handler = logging.StreamHandler(sys.stdout) console_handler.setLevel(logging.INFO) console_handler.setFormatter(simple_formatter) - # 添加处理器 + # Gắn handler logger.addHandler(file_handler) logger.addHandler(console_handler) @@ -90,13 +90,13 @@ def setup_logger(name: str = 'mirofish', level: int = logging.DEBUG) -> logging. def get_logger(name: str = 'mirofish') -> logging.Logger: """ - 获取日志器(如果不存在则创建) - + Lấy logger (nếu chưa có sẽ tạo mới) + Args: - name: 日志器名称 - + name: Tên logger + Returns: - 日志器实例 + Instance logger """ logger = logging.getLogger(name) if not logger.handlers: @@ -104,11 +104,11 @@ def get_logger(name: str = 'mirofish') -> logging.Logger: return logger -# 创建默认日志器 +# Tạo logger mặc định logger = setup_logger() -# 便捷方法 +# Hàm gọi nhanh def debug(msg, *args, **kwargs): logger.debug(msg, *args, **kwargs) diff --git a/backend/app/utils/retry.py b/backend/app/utils/retry.py index 819b1cfcf..28808270b 100644 --- a/backend/app/utils/retry.py +++ b/backend/app/utils/retry.py @@ -1,6 +1,6 @@ """ -API调用重试机制 -用于处理LLM等外部API调用的重试逻辑 +Cơ chế retry cho API call +Dùng để xử lý retry khi gọi API bên ngoài như LLM """ import time @@ -22,17 +22,17 @@ def retry_with_backoff( on_retry: Optional[Callable[[Exception, int], None]] = None ): """ - 带指数退避的重试装饰器 - + Decorator retry với exponential backoff + Args: - max_retries: 最大重试次数 - initial_delay: 初始延迟(秒) - max_delay: 最大延迟(秒) - backoff_factor: 退避因子 - jitter: 是否添加随机抖动 - exceptions: 需要重试的异常类型 - on_retry: 重试时的回调函数 (exception, retry_count) - + max_retries: Số lần retry tối đa + initial_delay: Độ trễ ban đầu (giây) + max_delay: Độ trễ tối đa (giây) + backoff_factor: Hệ số tăng độ trễ + jitter: Có thêm nhiễu ngẫu nhiên hay không + exceptions: Các loại exception cần retry + on_retry: Callback khi retry (exception, retry_count) + Usage: @retry_with_backoff(max_retries=3) def call_llm_api(): @@ -52,17 +52,17 @@ def wrapper(*args, **kwargs) -> Any: last_exception = e if attempt == max_retries: - logger.error(f"函数 {func.__name__} 在 {max_retries} 次重试后仍失败: {str(e)}") + logger.error(f"Function {func.__name__} still failed after {max_retries} retries: {str(e)}") raise - # 计算延迟 + # Tính độ trễ current_delay = min(delay, max_delay) if jitter: current_delay = current_delay * (0.5 + random.random()) logger.warning( - f"函数 {func.__name__} 第 {attempt + 1} 次尝试失败: {str(e)}, " - f"{current_delay:.1f}秒后重试..." + f"Function {func.__name__} attempt {attempt + 1} failed: {str(e)}, " + f"retrying in {current_delay:.1f}s..." ) if on_retry: @@ -87,7 +87,7 @@ def retry_with_backoff_async( on_retry: Optional[Callable[[Exception, int], None]] = None ): """ - 异步版本的重试装饰器 + Phiên bản bất đồng bộ của retry decorator """ import asyncio @@ -105,7 +105,7 @@ async def wrapper(*args, **kwargs) -> Any: last_exception = e if attempt == max_retries: - logger.error(f"异步函数 {func.__name__} 在 {max_retries} 次重试后仍失败: {str(e)}") + logger.error(f"Async function {func.__name__} still failed after {max_retries} retries: {str(e)}") raise current_delay = min(delay, max_delay) @@ -113,8 +113,8 @@ async def wrapper(*args, **kwargs) -> Any: current_delay = current_delay * (0.5 + random.random()) logger.warning( - f"异步函数 {func.__name__} 第 {attempt + 1} 次尝试失败: {str(e)}, " - f"{current_delay:.1f}秒后重试..." + f"Async function {func.__name__} attempt {attempt + 1} failed: {str(e)}, " + f"retrying in {current_delay:.1f}s..." ) if on_retry: @@ -131,7 +131,7 @@ async def wrapper(*args, **kwargs) -> Any: class RetryableAPIClient: """ - 可重试的API客户端封装 + Lớp bao API client có retry """ def __init__( @@ -154,16 +154,16 @@ def call_with_retry( **kwargs ) -> Any: """ - 执行函数调用并在失败时重试 - + Thực thi hàm và retry nếu thất bại + Args: - func: 要调用的函数 - *args: 函数参数 - exceptions: 需要重试的异常类型 - **kwargs: 函数关键字参数 - + func: Hàm cần gọi + *args: Tham số hàm + exceptions: Các loại exception cần retry + **kwargs: Tham số keyword của hàm + Returns: - 函数返回值 + Giá trị trả về của hàm """ last_exception = None delay = self.initial_delay @@ -176,15 +176,15 @@ def call_with_retry( last_exception = e if attempt == self.max_retries: - logger.error(f"API调用在 {self.max_retries} 次重试后仍失败: {str(e)}") + logger.error(f"API call still failed after {self.max_retries} retries: {str(e)}") raise current_delay = min(delay, self.max_delay) current_delay = current_delay * (0.5 + random.random()) logger.warning( - f"API调用第 {attempt + 1} 次尝试失败: {str(e)}, " - f"{current_delay:.1f}秒后重试..." + f"API call attempt {attempt + 1} failed: {str(e)}, " + f"retrying in {current_delay:.1f}s..." ) time.sleep(current_delay) @@ -200,16 +200,16 @@ def call_batch_with_retry( continue_on_failure: bool = True ) -> Tuple[list, list]: """ - 批量调用并对每个失败项单独重试 - + Xử lý theo lô và retry riêng cho từng mục thất bại + Args: - items: 要处理的项目列表 - process_func: 处理函数,接收单个item作为参数 - exceptions: 需要重试的异常类型 - continue_on_failure: 单项失败后是否继续处理其他项 - + items: Danh sách mục cần xử lý + process_func: Hàm xử lý, nhận 1 item mỗi lần + exceptions: Các loại exception cần retry + continue_on_failure: Có tiếp tục khi 1 mục thất bại không + Returns: - (成功结果列表, 失败项列表) + (Danh sách kết quả thành công, danh sách mục thất bại) """ results = [] failures = [] @@ -224,7 +224,7 @@ def call_batch_with_retry( results.append(result) except Exception as e: - logger.error(f"处理第 {idx + 1} 项失败: {str(e)}") + logger.error(f"Failed to process item {idx + 1}: {str(e)}") failures.append({ "index": idx, "item": item, diff --git a/backend/app/utils/zep_paging.py b/backend/app/utils/zep_paging.py index 943cd1ae2..f038b83bd 100644 --- a/backend/app/utils/zep_paging.py +++ b/backend/app/utils/zep_paging.py @@ -1,7 +1,8 @@ -"""Zep Graph 分页读取工具。 +"""Tiện ích đọc phân trang cho Zep Graph. -Zep 的 node/edge 列表接口使用 UUID cursor 分页, -本模块封装自动翻页逻辑(含单页重试),对调用方透明地返回完整列表。 +API danh sách node/edge của Zep dùng UUID cursor để phân trang, +module này đóng gói logic tự động lật trang (kèm retry từng trang) +và trả về đầy đủ danh sách cho bên gọi một cách trong suốt. """ from __future__ import annotations @@ -31,7 +32,7 @@ def _fetch_page_with_retry( page_description: str = "page", **kwargs: Any, ) -> list[Any]: - """单页请求,失败时指数退避重试。仅重试网络/IO类瞬态错误。""" + """Yêu cầu 1 trang, retry với exponential backoff khi thất bại. Chỉ retry lỗi tạm thời mạng/IO.""" if max_retries < 1: raise ValueError("max_retries must be >= 1") @@ -64,7 +65,7 @@ def fetch_all_nodes( max_retries: int = _DEFAULT_MAX_RETRIES, retry_delay: float = _DEFAULT_RETRY_DELAY, ) -> list[Any]: - """分页获取图谱节点,最多返回 max_items 条(默认 2000)。每页请求自带重试。""" + """Lấy node theo trang, tối đa max_items mục (mặc định 2000). Mỗi trang đều có retry.""" all_nodes: list[Any] = [] cursor: str | None = None page_num = 0 @@ -109,7 +110,7 @@ def fetch_all_edges( max_retries: int = _DEFAULT_MAX_RETRIES, retry_delay: float = _DEFAULT_RETRY_DELAY, ) -> list[Any]: - """分页获取图谱所有边,返回完整列表。每页请求自带重试。""" + """Lấy toàn bộ edge theo trang, trả về đầy đủ danh sách. Mỗi trang đều có retry.""" all_edges: list[Any] = [] cursor: str | None = None page_num = 0 diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 4f5361d53..b9ba53ddf 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "mirofish-backend" version = "0.1.0" -description = "MiroFish - 简洁通用的群体智能引擎,预测万物" +description = "MiroFish - A concise and general collective intelligence engine for prediction" requires-python = ">=3.11" license = { text = "AGPL-3.0" } authors = [ @@ -9,27 +9,27 @@ authors = [ ] dependencies = [ - # 核心框架 + # Khung lõi "flask>=3.0.0", "flask-cors>=6.0.0", - # LLM 相关 + # Liên quan đến LLM "openai>=1.0.0", # Zep Cloud "zep-cloud==3.13.0", - # OASIS 社交媒体模拟 + # Mô phỏng mạng xã hội OASIS "camel-oasis==0.2.5", "camel-ai==0.2.78", - # 文件处理 + # Xử lý tệp "PyMuPDF>=1.24.0", - # 编码检测(支持非UTF-8编码的文本文件) + # Phát hiện mã hóa (hỗ trợ tệp văn bản không dùng UTF-8) "charset-normalizer>=3.0.0", "chardet>=5.0.0", - # 工具库 + # Thư viện tiện ích "python-dotenv>=1.0.0", "pydantic>=2.0.0", ] diff --git a/backend/requirements.txt b/backend/requirements.txt index 4f146296b..a644297e0 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -5,31 +5,31 @@ # Install: pip install -r requirements.txt # =========================================== -# ============= 核心框架 ============= +# ============= Khung lõi ============= flask>=3.0.0 flask-cors>=6.0.0 -# ============= LLM 相关 ============= -# OpenAI SDK(统一使用 OpenAI 格式调用 LLM) +# ============= Liên quan đến LLM ============= +# OpenAI SDK (thống nhất dùng định dạng OpenAI để gọi LLM) openai>=1.0.0 # ============= Zep Cloud ============= zep-cloud==3.13.0 -# ============= OASIS 社交媒体模拟 ============= -# OASIS 社交模拟框架 +# ============= Mô phỏng mạng xã hội OASIS ============= +# Khung mô phỏng mạng xã hội OASIS camel-oasis==0.2.5 camel-ai==0.2.78 -# ============= 文件处理 ============= +# ============= Xử lý tệp ============= PyMuPDF>=1.24.0 -# 编码检测(支持非UTF-8编码的文本文件) +# Phát hiện mã hóa (hỗ trợ tệp văn bản không dùng UTF-8) charset-normalizer>=3.0.0 chardet>=5.0.0 -# ============= 工具库 ============= -# 环境变量加载 +# ============= Thư viện tiện ích ============= +# Nạp biến môi trường python-dotenv>=1.0.0 -# 数据验证 +# Xác thực dữ liệu pydantic>=2.0.0 diff --git a/backend/run.py b/backend/run.py index 4e3b04fa9..fb6dd4ced 100644 --- a/backend/run.py +++ b/backend/run.py @@ -1,21 +1,21 @@ """ -MiroFish Backend 启动入口 +MiroFish Backend entrypoint """ import os import sys -# 解决 Windows 控制台中文乱码问题:在所有导入之前设置 UTF-8 编码 +# Khắc phục lỗi hiển thị tiếng Trung trên console Windows: đặt UTF-8 trước mọi import if sys.platform == 'win32': - # 设置环境变量确保 Python 使用 UTF-8 + # Đặt biến môi trường để đảm bảo Python dùng UTF-8 os.environ.setdefault('PYTHONIOENCODING', 'utf-8') - # 重新配置标准输出流为 UTF-8 + # Cấu hình lại stdout/stderr sang UTF-8 if hasattr(sys.stdout, 'reconfigure'): sys.stdout.reconfigure(encoding='utf-8', errors='replace') if hasattr(sys.stderr, 'reconfigure'): sys.stderr.reconfigure(encoding='utf-8', errors='replace') -# 添加项目根目录到路径 +# Thêm thư mục gốc của project vào path sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from app import create_app @@ -23,25 +23,25 @@ def main(): - """主函数""" - # 验证配置 + """Hàm chính""" + # Kiểm tra cấu hình errors = Config.validate() if errors: - print("配置错误:") + print("Configuration errors:") for err in errors: print(f" - {err}") - print("\n请检查 .env 文件中的配置") + print("\nPlease check configuration in the .env file") sys.exit(1) - # 创建应用 + # Tạo ứng dụng app = create_app() - # 获取运行配置 + # Lấy cấu hình chạy host = os.environ.get('FLASK_HOST', '0.0.0.0') port = int(os.environ.get('FLASK_PORT', 5001)) debug = Config.DEBUG - # 启动服务 + # Khởi động dịch vụ app.run(host=host, port=port, debug=debug, threaded=True) diff --git a/backend/scripts/action_logger.py b/backend/scripts/action_logger.py index 38d025a6c..9cad8f88d 100644 --- a/backend/scripts/action_logger.py +++ b/backend/scripts/action_logger.py @@ -1,15 +1,15 @@ """ -动作日志记录器 -用于记录OASIS模拟中每个Agent的动作,供后端监控使用 +Trình ghi log hành động +Dùng để ghi lại hành động của từng Agent trong mô phỏng OASIS, phục vụ backend giám sát -日志结构: +Cấu trúc log: sim_xxx/ ├── twitter/ - │ └── actions.jsonl # Twitter 平台动作日志 + │ └── actions.jsonl # Log hành động nền tảng Twitter ├── reddit/ - │ └── actions.jsonl # Reddit 平台动作日志 - ├── simulation.log # 主模拟进程日志 - └── run_state.json # 运行状态(API 查询用) + │ └── actions.jsonl # Log hành động nền tảng Reddit + ├── simulation.log # Log tiến trình mô phỏng chính + └── run_state.json # Trạng thái chạy (cho API truy vấn) """ import json @@ -20,15 +20,15 @@ class PlatformActionLogger: - """单平台动作日志记录器""" + """Trình ghi log hành động cho một nền tảng""" def __init__(self, platform: str, base_dir: str): """ - 初始化日志记录器 + Khởi tạo logger Args: - platform: 平台名称 (twitter/reddit) - base_dir: 模拟目录的基础路径 + platform: Tên nền tảng (twitter/reddit) + base_dir: Đường dẫn thư mục mô phỏng gốc """ self.platform = platform self.base_dir = base_dir @@ -37,7 +37,7 @@ def __init__(self, platform: str, base_dir: str): self._ensure_dir() def _ensure_dir(self): - """确保目录存在""" + """Đảm bảo thư mục tồn tại""" os.makedirs(self.log_dir, exist_ok=True) def log_action( @@ -50,7 +50,7 @@ def log_action( result: Optional[str] = None, success: bool = True ): - """记录一个动作""" + """Ghi lại một hành động""" entry = { "round": round_num, "timestamp": datetime.now().isoformat(), @@ -66,7 +66,7 @@ def log_action( f.write(json.dumps(entry, ensure_ascii=False) + '\n') def log_round_start(self, round_num: int, simulated_hour: int): - """记录轮次开始""" + """Ghi lại thời điểm bắt đầu round""" entry = { "round": round_num, "timestamp": datetime.now().isoformat(), @@ -78,7 +78,7 @@ def log_round_start(self, round_num: int, simulated_hour: int): f.write(json.dumps(entry, ensure_ascii=False) + '\n') def log_round_end(self, round_num: int, actions_count: int): - """记录轮次结束""" + """Ghi lại thời điểm kết thúc round""" entry = { "round": round_num, "timestamp": datetime.now().isoformat(), @@ -90,7 +90,7 @@ def log_round_end(self, round_num: int, actions_count: int): f.write(json.dumps(entry, ensure_ascii=False) + '\n') def log_simulation_start(self, config: Dict[str, Any]): - """记录模拟开始""" + """Ghi lại thời điểm bắt đầu mô phỏng""" entry = { "timestamp": datetime.now().isoformat(), "event_type": "simulation_start", @@ -103,7 +103,7 @@ def log_simulation_start(self, config: Dict[str, Any]): f.write(json.dumps(entry, ensure_ascii=False) + '\n') def log_simulation_end(self, total_rounds: int, total_actions: int): - """记录模拟结束""" + """Ghi lại thời điểm kết thúc mô phỏng""" entry = { "timestamp": datetime.now().isoformat(), "event_type": "simulation_end", @@ -118,35 +118,35 @@ def log_simulation_end(self, total_rounds: int, total_actions: int): class SimulationLogManager: """ - 模拟日志管理器 - 统一管理所有日志文件,按平台分离 + Trình quản lý log mô phỏng + Quản lý thống nhất mọi tệp log, tách riêng theo nền tảng """ def __init__(self, simulation_dir: str): """ - 初始化日志管理器 + Khởi tạo trình quản lý log Args: - simulation_dir: 模拟目录路径 + simulation_dir: Đường dẫn thư mục mô phỏng """ self.simulation_dir = simulation_dir self.twitter_logger: Optional[PlatformActionLogger] = None self.reddit_logger: Optional[PlatformActionLogger] = None self._main_logger: Optional[logging.Logger] = None - # 设置主日志 + # Thiết lập log chính self._setup_main_logger() def _setup_main_logger(self): - """设置主模拟日志""" + """Thiết lập log mô phỏng chính""" log_path = os.path.join(self.simulation_dir, "simulation.log") - # 创建 logger + # Tạo logger self._main_logger = logging.getLogger(f"simulation.{os.path.basename(self.simulation_dir)}") self._main_logger.setLevel(logging.INFO) self._main_logger.handlers.clear() - # 文件处理器 + # File handler file_handler = logging.FileHandler(log_path, encoding='utf-8', mode='w') file_handler.setLevel(logging.INFO) file_handler.setFormatter(logging.Formatter( @@ -155,7 +155,7 @@ def _setup_main_logger(self): )) self._main_logger.addHandler(file_handler) - # 控制台处理器 + # Console handler console_handler = logging.StreamHandler() console_handler.setLevel(logging.INFO) console_handler.setFormatter(logging.Formatter( @@ -167,19 +167,19 @@ def _setup_main_logger(self): self._main_logger.propagate = False def get_twitter_logger(self) -> PlatformActionLogger: - """获取 Twitter 平台日志记录器""" + """Lấy logger cho nền tảng Twitter""" if self.twitter_logger is None: self.twitter_logger = PlatformActionLogger("twitter", self.simulation_dir) return self.twitter_logger def get_reddit_logger(self) -> PlatformActionLogger: - """获取 Reddit 平台日志记录器""" + """Lấy logger cho nền tảng Reddit""" if self.reddit_logger is None: self.reddit_logger = PlatformActionLogger("reddit", self.simulation_dir) return self.reddit_logger def log(self, message: str, level: str = "info"): - """记录主日志""" + """Ghi log chính""" if self._main_logger: getattr(self._main_logger, level.lower(), self._main_logger.info)(message) @@ -196,12 +196,12 @@ def debug(self, message: str): self.log(message, "debug") -# ============ 兼容旧接口 ============ +# ============ Tương thích giao diện cũ ============ class ActionLogger: """ - 动作日志记录器(兼容旧接口) - 建议使用 SimulationLogManager 代替 + Trình ghi log hành động (tương thích giao diện cũ) + Khuyến nghị dùng SimulationLogManager thay thế """ def __init__(self, log_path: str): @@ -288,12 +288,12 @@ def log_simulation_end(self, platform: str, total_rounds: int, total_actions: in f.write(json.dumps(entry, ensure_ascii=False) + '\n') -# 全局日志实例(兼容旧接口) +# Biến logger toàn cục (tương thích giao diện cũ) _global_logger: Optional[ActionLogger] = None def get_logger(log_path: Optional[str] = None) -> ActionLogger: - """获取全局日志实例(兼容旧接口)""" + """Lấy instance logger toàn cục (tương thích giao diện cũ)""" global _global_logger if log_path: diff --git a/backend/scripts/run_parallel_simulation.py b/backend/scripts/run_parallel_simulation.py index 2a627ffd0..4ccde2d55 100644 --- a/backend/scripts/run_parallel_simulation.py +++ b/backend/scripts/run_parallel_simulation.py @@ -1,62 +1,62 @@ """ -OASIS 双平台并行模拟预设脚本 -同时运行Twitter和Reddit模拟,读取相同的配置文件 +Kịch bản mô phỏng song song hai nền tảng OASIS +Chạy đồng thời mô phỏng Twitter và Reddit, đọc cùng một tệp cấu hình -功能特性: -- 双平台(Twitter + Reddit)并行模拟 -- 完成模拟后不立即关闭环境,进入等待命令模式 -- 支持通过IPC接收Interview命令 -- 支持单个Agent采访和批量采访 -- 支持远程关闭环境命令 +Tính năng: +- Mô phỏng song song hai nền tảng (Twitter + Reddit) +- Không đóng môi trường ngay sau khi hoàn tất mô phỏng, chuyển sang chế độ chờ lệnh +- Hỗ trợ nhận lệnh Interview qua IPC +- Hỗ trợ phỏng vấn một Agent và phỏng vấn hàng loạt +- Hỗ trợ lệnh đóng môi trường từ xa -使用方式: +Cách dùng: python run_parallel_simulation.py --config simulation_config.json - python run_parallel_simulation.py --config simulation_config.json --no-wait # 完成后立即关闭 + python run_parallel_simulation.py --config simulation_config.json --no-wait # Đóng ngay sau khi hoàn tất python run_parallel_simulation.py --config simulation_config.json --twitter-only python run_parallel_simulation.py --config simulation_config.json --reddit-only -日志结构: +Cấu trúc log: sim_xxx/ ├── twitter/ - │ └── actions.jsonl # Twitter 平台动作日志 + │ └── actions.jsonl # Log hành động nền tảng Twitter ├── reddit/ - │ └── actions.jsonl # Reddit 平台动作日志 - ├── simulation.log # 主模拟进程日志 - └── run_state.json # 运行状态(API 查询用) + │ └── actions.jsonl # Log hành động nền tảng Reddit + ├── simulation.log # Log tiến trình mô phỏng chính + └── run_state.json # Trạng thái chạy (cho API truy vấn) """ # ============================================================ -# 解决 Windows 编码问题:在所有 import 之前设置 UTF-8 编码 -# 这是为了修复 OASIS 第三方库读取文件时未指定编码的问题 +# Khắc phục vấn đề mã hóa trên Windows: đặt UTF-8 trước mọi import +# Mục tiêu là sửa lỗi thư viện OASIS bên thứ ba đọc file mà không chỉ định encoding # ============================================================ import sys import os if sys.platform == 'win32': - # 设置 Python 默认 I/O 编码为 UTF-8 - # 这会影响所有未指定编码的 open() 调用 + # Đặt mã hóa I/O mặc định của Python là UTF-8 + # Thiết lập này ảnh hưởng đến mọi lời gọi open() không chỉ định encoding os.environ.setdefault('PYTHONUTF8', '1') os.environ.setdefault('PYTHONIOENCODING', 'utf-8') - # 重新配置标准输出流为 UTF-8(解决控制台中文乱码) + # Cấu hình lại stdout/stderr sang UTF-8 (tránh lỗi hiển thị ký tự tiếng Trung trên console) if hasattr(sys.stdout, 'reconfigure'): sys.stdout.reconfigure(encoding='utf-8', errors='replace') if hasattr(sys.stderr, 'reconfigure'): sys.stderr.reconfigure(encoding='utf-8', errors='replace') - # 强制设置默认编码(影响 open() 函数的默认编码) - # 注意:这需要在 Python 启动时就设置,运行时设置可能不生效 - # 所以我们还需要 monkey-patch 内置的 open 函数 + # Ép đặt mã hóa mặc định (ảnh hưởng encoding mặc định của open()) + # Lưu ý: tốt nhất cần thiết lập khi Python khởi động, thiết lập lúc runtime có thể không hiệu quả + # Vì vậy cần monkey-patch thêm hàm open tích hợp import builtins _original_open = builtins.open def _utf8_open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None): """ - 包装 open() 函数,对于文本模式默认使用 UTF-8 编码 - 这可以修复第三方库(如 OASIS)读取文件时未指定编码的问题 + Wrapper cho hàm open(), mặc định dùng UTF-8 cho chế độ văn bản + Điều này giúp sửa lỗi thư viện bên thứ ba (như OASIS) đọc file không chỉ định encoding """ - # 只对文本模式(非二进制)且未指定编码的情况设置默认编码 + # Chỉ đặt encoding mặc định cho chế độ văn bản (không phải binary) khi chưa chỉ định encoding if encoding is None and 'b' not in mode: encoding = 'utf-8' return _original_open(file, mode, buffering, encoding, errors, @@ -77,52 +77,52 @@ def _utf8_open(file, mode='r', buffering=-1, encoding=None, errors=None, from typing import Dict, Any, List, Optional, Tuple -# 全局变量:用于信号处理 +# Biến toàn cục dùng cho xử lý tín hiệu _shutdown_event = None _cleanup_done = False -# 添加 backend 目录到路径 -# 脚本固定位于 backend/scripts/ 目录 +# Thêm thư mục backend vào sys.path +# Script nằm cố định trong thư mục backend/scripts/ _scripts_dir = os.path.dirname(os.path.abspath(__file__)) _backend_dir = os.path.abspath(os.path.join(_scripts_dir, '..')) _project_root = os.path.abspath(os.path.join(_backend_dir, '..')) sys.path.insert(0, _scripts_dir) sys.path.insert(0, _backend_dir) -# 加载项目根目录的 .env 文件(包含 LLM_API_KEY 等配置) +# Tải tệp .env ở thư mục gốc dự án (chứa các cấu hình như LLM_API_KEY) from dotenv import load_dotenv _env_file = os.path.join(_project_root, '.env') if os.path.exists(_env_file): load_dotenv(_env_file) - print(f"已加载环境配置: {_env_file}") + print(f"Environment config loaded: {_env_file}") else: - # 尝试加载 backend/.env + # Thử tải backend/.env _backend_env = os.path.join(_backend_dir, '.env') if os.path.exists(_backend_env): load_dotenv(_backend_env) - print(f"已加载环境配置: {_backend_env}") + print(f"Environment config loaded: {_backend_env}") class MaxTokensWarningFilter(logging.Filter): - """过滤掉 camel-ai 关于 max_tokens 的警告(我们故意不设置 max_tokens,让模型自行决定)""" + """Lọc cảnh báo max_tokens của camel-ai (chủ động không đặt max_tokens để model tự quyết định)""" def filter(self, record): - # 过滤掉包含 max_tokens 警告的日志 + # Lọc log cảnh báo liên quan đến max_tokens if "max_tokens" in record.getMessage() and "Invalid or missing" in record.getMessage(): return False return True -# 在模块加载时立即添加过滤器,确保在 camel 代码执行前生效 +# Thêm filter ngay khi module được nạp để bảo đảm có hiệu lực trước khi mã camel chạy logging.getLogger().addFilter(MaxTokensWarningFilter()) def disable_oasis_logging(): """ - 禁用 OASIS 库的详细日志输出 - OASIS 的日志太冗余(记录每个 agent 的观察和动作),我们使用自己的 action_logger + Tắt log chi tiết của thư viện OASIS + Log của OASIS quá dài dòng (ghi từng quan sát và hành động của agent), ở đây dùng action_logger riêng """ - # 禁用 OASIS 的所有日志器 + # Tắt toàn bộ logger của OASIS oasis_loggers = [ "social.agent", "social.twitter", @@ -133,22 +133,22 @@ def disable_oasis_logging(): for logger_name in oasis_loggers: logger = logging.getLogger(logger_name) - logger.setLevel(logging.CRITICAL) # 只记录严重错误 + logger.setLevel(logging.CRITICAL) # Chỉ ghi lỗi nghiêm trọng logger.handlers.clear() logger.propagate = False def init_logging_for_simulation(simulation_dir: str): """ - 初始化模拟的日志配置 + Khởi tạo cấu hình log cho mô phỏng Args: - simulation_dir: 模拟目录路径 + simulation_dir: Đường dẫn thư mục mô phỏng """ - # 禁用 OASIS 的详细日志 + # Tắt log chi tiết của OASIS disable_oasis_logging() - # 清理旧的 log 目录(如果存在) + # Dọn thư mục log cũ (nếu tồn tại) old_log_dir = os.path.join(simulation_dir, "log") if os.path.exists(old_log_dir): import shutil @@ -169,12 +169,12 @@ def init_logging_for_simulation(simulation_dir: str): generate_reddit_agent_graph ) except ImportError as e: - print(f"错误: 缺少依赖 {e}") - print("请先安装: pip install oasis-ai camel-ai") + print(f"Error: Missing dependency {e}") + print("Please install first: pip install oasis-ai camel-ai") sys.exit(1) -# Twitter可用动作(不包含INTERVIEW,INTERVIEW只能通过ManualAction手动触发) +# Action khả dụng trên Twitter (không gồm INTERVIEW; INTERVIEW chỉ kích hoạt thủ công qua ManualAction) TWITTER_ACTIONS = [ ActionType.CREATE_POST, ActionType.LIKE_POST, @@ -184,7 +184,7 @@ def init_logging_for_simulation(simulation_dir: str): ActionType.QUOTE_POST, ] -# Reddit可用动作(不包含INTERVIEW,INTERVIEW只能通过ManualAction手动触发) +# Action khả dụng trên Reddit (không gồm INTERVIEW; INTERVIEW chỉ kích hoạt thủ công qua ManualAction) REDDIT_ACTIONS = [ ActionType.LIKE_POST, ActionType.DISLIKE_POST, @@ -202,13 +202,13 @@ def init_logging_for_simulation(simulation_dir: str): ] -# IPC相关常量 +# Hằng số liên quan đến IPC IPC_COMMANDS_DIR = "ipc_commands" IPC_RESPONSES_DIR = "ipc_responses" ENV_STATUS_FILE = "env_status.json" class CommandType: - """命令类型常量""" + """Hằng số loại lệnh""" INTERVIEW = "interview" BATCH_INTERVIEW = "batch_interview" CLOSE_ENV = "close_env" @@ -216,9 +216,9 @@ class CommandType: class ParallelIPCHandler: """ - 双平台IPC命令处理器 + Bộ xử lý lệnh IPC cho hai nền tảng - 管理两个平台的环境,处理Interview命令 + Quản lý môi trường của cả hai nền tảng và xử lý lệnh Interview """ def __init__( @@ -239,12 +239,12 @@ def __init__( self.responses_dir = os.path.join(simulation_dir, IPC_RESPONSES_DIR) self.status_file = os.path.join(simulation_dir, ENV_STATUS_FILE) - # 确保目录存在 + # Đảm bảo thư mục tồn tại os.makedirs(self.commands_dir, exist_ok=True) os.makedirs(self.responses_dir, exist_ok=True) def update_status(self, status: str): - """更新环境状态""" + """Cập nhật trạng thái môi trường""" with open(self.status_file, 'w', encoding='utf-8') as f: json.dump({ "status": status, @@ -254,11 +254,11 @@ def update_status(self, status: str): }, f, ensure_ascii=False, indent=2) def poll_command(self) -> Optional[Dict[str, Any]]: - """轮询获取待处理命令""" + """Poll để lấy lệnh đang chờ xử lý""" if not os.path.exists(self.commands_dir): return None - # 获取命令文件(按时间排序) + # Lấy tệp lệnh (sắp xếp theo thời gian) command_files = [] for filename in os.listdir(self.commands_dir): if filename.endswith('.json'): @@ -277,7 +277,7 @@ def poll_command(self) -> Optional[Dict[str, Any]]: return None def send_response(self, command_id: str, status: str, result: Dict = None, error: str = None): - """发送响应""" + """Gửi phản hồi""" response = { "command_id": command_id, "status": status, @@ -290,7 +290,7 @@ def send_response(self, command_id: str, status: str, result: Dict = None, error with open(response_file, 'w', encoding='utf-8') as f: json.dump(response, f, ensure_ascii=False, indent=2) - # 删除命令文件 + # Xóa tệp lệnh command_file = os.path.join(self.commands_dir, f"{command_id}.json") try: os.remove(command_file) @@ -299,13 +299,13 @@ def send_response(self, command_id: str, status: str, result: Dict = None, error def _get_env_and_graph(self, platform: str): """ - 获取指定平台的环境和agent_graph + Lấy env và agent_graph của nền tảng được chỉ định Args: - platform: 平台名称 ("twitter" 或 "reddit") + platform: Tên nền tảng ("twitter" hoặc "reddit") Returns: - (env, agent_graph, platform_name) 或 (None, None, None) + (env, agent_graph, platform_name) hoặc (None, None, None) """ if platform == "twitter" and self.twitter_env: return self.twitter_env, self.twitter_agent_graph, "twitter" @@ -316,15 +316,15 @@ def _get_env_and_graph(self, platform: str): async def _interview_single_platform(self, agent_id: int, prompt: str, platform: str) -> Dict[str, Any]: """ - 在单个平台上执行Interview + Thực thi Interview trên một nền tảng Returns: - 包含结果的字典,或包含error的字典 + Dictionary chứa kết quả hoặc lỗi """ env, agent_graph, actual_platform = self._get_env_and_graph(platform) if not env or not agent_graph: - return {"platform": platform, "error": f"{platform}平台不可用"} + return {"platform": platform, "error": f"{platform} platform is unavailable"} try: agent = agent_graph.get_agent(agent_id) @@ -344,36 +344,36 @@ async def _interview_single_platform(self, agent_id: int, prompt: str, platform: async def handle_interview(self, command_id: str, agent_id: int, prompt: str, platform: str = None) -> bool: """ - 处理单个Agent采访命令 + Xử lý lệnh phỏng vấn một Agent Args: - command_id: 命令ID + command_id: ID lệnh agent_id: Agent ID - prompt: 采访问题 - platform: 指定平台(可选) - - "twitter": 只采访Twitter平台 - - "reddit": 只采访Reddit平台 - - None/不指定: 同时采访两个平台,返回整合结果 + prompt: Câu hỏi phỏng vấn + platform: Nền tảng chỉ định (tùy chọn) + - "twitter": Chỉ phỏng vấn trên Twitter + - "reddit": Chỉ phỏng vấn trên Reddit + - None/không chỉ định: Phỏng vấn đồng thời cả hai nền tảng, trả kết quả gộp Returns: - True 表示成功,False 表示失败 + True là thành công, False là thất bại """ - # 如果指定了平台,只采访该平台 + # Nếu có chỉ định nền tảng, chỉ phỏng vấn trên nền tảng đó if platform in ("twitter", "reddit"): result = await self._interview_single_platform(agent_id, prompt, platform) if "error" in result: self.send_response(command_id, "failed", error=result["error"]) - print(f" Interview失败: agent_id={agent_id}, platform={platform}, error={result['error']}") + print(f" Interview failed: agent_id={agent_id}, platform={platform}, error={result['error']}") return False else: self.send_response(command_id, "completed", result=result) - print(f" Interview完成: agent_id={agent_id}, platform={platform}") + print(f" Interview completed: agent_id={agent_id}, platform={platform}") return True - # 未指定平台:同时采访两个平台 + # Không chỉ định nền tảng: phỏng vấn đồng thời hai nền tảng if not self.twitter_env and not self.reddit_env: - self.send_response(command_id, "failed", error="没有可用的模拟环境") + self.send_response(command_id, "failed", error="No simulation environment available") return False results = { @@ -383,7 +383,7 @@ async def handle_interview(self, command_id: str, agent_id: int, prompt: str, pl } success_count = 0 - # 并行采访两个平台 + # Phỏng vấn song song hai nền tảng tasks = [] platforms_to_interview = [] @@ -395,7 +395,7 @@ async def handle_interview(self, command_id: str, agent_id: int, prompt: str, pl tasks.append(self._interview_single_platform(agent_id, prompt, "reddit")) platforms_to_interview.append("reddit") - # 并行执行 + # Chạy song song platform_results = await asyncio.gather(*tasks) for platform_name, platform_result in zip(platforms_to_interview, platform_results): @@ -405,30 +405,30 @@ async def handle_interview(self, command_id: str, agent_id: int, prompt: str, pl if success_count > 0: self.send_response(command_id, "completed", result=results) - print(f" Interview完成: agent_id={agent_id}, 成功平台数={success_count}/{len(platforms_to_interview)}") + print(f" Interview completed: agent_id={agent_id}, successful platforms={success_count}/{len(platforms_to_interview)}") return True else: - errors = [f"{p}: {r.get('error', '未知错误')}" for p, r in results["platforms"].items()] + errors = [f"{p}: {r.get('error', 'Unknown error')}" for p, r in results["platforms"].items()] self.send_response(command_id, "failed", error="; ".join(errors)) - print(f" Interview失败: agent_id={agent_id}, 所有平台都失败") + print(f" Interview failed: agent_id={agent_id}, all platforms failed") return False async def handle_batch_interview(self, command_id: str, interviews: List[Dict], platform: str = None) -> bool: """ - 处理批量采访命令 + Xử lý lệnh phỏng vấn hàng loạt Args: - command_id: 命令ID + command_id: ID lệnh interviews: [{"agent_id": int, "prompt": str, "platform": str(optional)}, ...] - platform: 默认平台(可被每个interview项覆盖) - - "twitter": 只采访Twitter平台 - - "reddit": 只采访Reddit平台 - - None/不指定: 每个Agent同时采访两个平台 + platform: Nền tảng mặc định (có thể bị ghi đè ở từng mục interview) + - "twitter": Chỉ phỏng vấn Twitter + - "reddit": Chỉ phỏng vấn Reddit + - None/không chỉ định: Mỗi Agent được phỏng vấn trên cả hai nền tảng """ - # 按平台分组 + # Nhóm theo nền tảng twitter_interviews = [] reddit_interviews = [] - both_platforms_interviews = [] # 需要同时采访两个平台的 + both_platforms_interviews = [] # Cần phỏng vấn đồng thời hai nền tảng for interview in interviews: item_platform = interview.get("platform", platform) @@ -437,10 +437,10 @@ async def handle_batch_interview(self, command_id: str, interviews: List[Dict], elif item_platform == "reddit": reddit_interviews.append(interview) else: - # 未指定平台:两个平台都采访 + # Không chỉ định nền tảng: phỏng vấn cả hai nền tảng both_platforms_interviews.append(interview) - # 把 both_platforms_interviews 拆分到两个平台 + # Tách both_platforms_interviews vào hai nền tảng if both_platforms_interviews: if self.twitter_env: twitter_interviews.extend(both_platforms_interviews) @@ -449,7 +449,7 @@ async def handle_batch_interview(self, command_id: str, interviews: List[Dict], results = {} - # 处理Twitter平台的采访 + # Xử lý phỏng vấn trên nền tảng Twitter if twitter_interviews and self.twitter_env: try: twitter_actions = {} @@ -463,7 +463,7 @@ async def handle_batch_interview(self, command_id: str, interviews: List[Dict], action_args={"prompt": prompt} ) except Exception as e: - print(f" 警告: 无法获取Twitter Agent {agent_id}: {e}") + print(f" Warning: Cannot get Twitter Agent {agent_id}: {e}") if twitter_actions: await self.twitter_env.step(twitter_actions) @@ -474,9 +474,9 @@ async def handle_batch_interview(self, command_id: str, interviews: List[Dict], result["platform"] = "twitter" results[f"twitter_{agent_id}"] = result except Exception as e: - print(f" Twitter批量Interview失败: {e}") + print(f" Twitter batch interview failed: {e}") - # 处理Reddit平台的采访 + # Xử lý phỏng vấn trên nền tảng Reddit if reddit_interviews and self.reddit_env: try: reddit_actions = {} @@ -490,7 +490,7 @@ async def handle_batch_interview(self, command_id: str, interviews: List[Dict], action_args={"prompt": prompt} ) except Exception as e: - print(f" 警告: 无法获取Reddit Agent {agent_id}: {e}") + print(f" Warning: Cannot get Reddit Agent {agent_id}: {e}") if reddit_actions: await self.reddit_env.step(reddit_actions) @@ -501,21 +501,21 @@ async def handle_batch_interview(self, command_id: str, interviews: List[Dict], result["platform"] = "reddit" results[f"reddit_{agent_id}"] = result except Exception as e: - print(f" Reddit批量Interview失败: {e}") + print(f" Reddit batch interview failed: {e}") if results: self.send_response(command_id, "completed", result={ "interviews_count": len(results), "results": results }) - print(f" 批量Interview完成: {len(results)} 个Agent") + print(f" Batch interview completed: {len(results)} agents") return True else: - self.send_response(command_id, "failed", error="没有成功的采访") + self.send_response(command_id, "failed", error="No successful interviews") return False def _get_interview_result(self, agent_id: int, platform: str) -> Dict[str, Any]: - """从数据库获取最新的Interview结果""" + """Lấy kết quả Interview mới nhất từ cơ sở dữ liệu""" db_path = os.path.join(self.simulation_dir, f"{platform}_simulation.db") result = { @@ -531,7 +531,7 @@ def _get_interview_result(self, agent_id: int, platform: str) -> Dict[str, Any]: conn = sqlite3.connect(db_path) cursor = conn.cursor() - # 查询最新的Interview记录 + # Truy vấn bản ghi Interview mới nhất cursor.execute(""" SELECT user_id, info, created_at FROM trace @@ -553,16 +553,16 @@ def _get_interview_result(self, agent_id: int, platform: str) -> Dict[str, Any]: conn.close() except Exception as e: - print(f" 读取Interview结果失败: {e}") + print(f" Failed to read interview result: {e}") return result async def process_commands(self) -> bool: """ - 处理所有待处理命令 + Xử lý tất cả lệnh đang chờ Returns: - True 表示继续运行,False 表示应该退出 + True để tiếp tục chạy, False để thoát """ command = self.poll_command() if not command: @@ -572,7 +572,7 @@ async def process_commands(self) -> bool: command_type = command.get("command_type") args = command.get("args", {}) - print(f"\n收到IPC命令: {command_type}, id={command_id}") + print(f"\nReceived IPC command: {command_type}, id={command_id}") if command_type == CommandType.INTERVIEW: await self.handle_interview( @@ -592,25 +592,25 @@ async def process_commands(self) -> bool: return True elif command_type == CommandType.CLOSE_ENV: - print("收到关闭环境命令") - self.send_response(command_id, "completed", result={"message": "环境即将关闭"}) + print("Received close environment command") + self.send_response(command_id, "completed", result={"message": "Environment will close soon"}) return False else: - self.send_response(command_id, "failed", error=f"未知命令类型: {command_type}") + self.send_response(command_id, "failed", error=f"Unknown command type: {command_type}") return True def load_config(config_path: str) -> Dict[str, Any]: - """加载配置文件""" + """Tải tệp cấu hình""" with open(config_path, 'r', encoding='utf-8') as f: return json.load(f) -# 需要过滤掉的非核心动作类型(这些动作对分析价值较低) +# Các loại action không cốt lõi cần lọc (giá trị phân tích thấp) FILTERED_ACTIONS = {'refresh', 'sign_up'} -# 动作类型映射表(数据库中的名称 -> 标准名称) +# Bảng ánh xạ loại action (tên trong DB -> tên chuẩn) ACTION_TYPE_MAP = { 'create_post': 'CREATE_POST', 'like_post': 'LIKE_POST', @@ -632,15 +632,15 @@ def load_config(config_path: str) -> Dict[str, Any]: def get_agent_names_from_config(config: Dict[str, Any]) -> Dict[int, str]: """ - 从 simulation_config 中获取 agent_id -> entity_name 的映射 + Lấy ánh xạ agent_id -> entity_name từ simulation_config - 这样可以在 actions.jsonl 中显示真实的实体名称,而不是 "Agent_0" 这样的代号 + Mục tiêu là hiển thị tên thực thể thật trong actions.jsonl thay vì mã như "Agent_0" Args: - config: simulation_config.json 的内容 + config: Nội dung của simulation_config.json Returns: - agent_id -> entity_name 的映射字典 + Dictionary ánh xạ agent_id -> entity_name """ agent_names = {} agent_configs = config.get("agent_configs", []) @@ -660,17 +660,17 @@ def fetch_new_actions_from_db( agent_names: Dict[int, str] ) -> Tuple[List[Dict[str, Any]], int]: """ - 从数据库中获取新的动作记录,并补充完整的上下文信息 + Lấy bản ghi action mới từ DB và bổ sung ngữ cảnh đầy đủ Args: - db_path: 数据库文件路径 - last_rowid: 上次读取的最大 rowid 值(使用 rowid 而不是 created_at,因为不同平台的 created_at 格式不同) - agent_names: agent_id -> agent_name 映射 + db_path: Đường dẫn tệp cơ sở dữ liệu + last_rowid: Giá trị rowid lớn nhất đã đọc trước đó (dùng rowid thay vì created_at vì định dạng created_at khác nhau giữa nền tảng) + agent_names: Ánh xạ agent_id -> agent_name Returns: (actions_list, new_last_rowid) - - actions_list: 动作列表,每个元素包含 agent_id, agent_name, action_type, action_args(含上下文信息) - - new_last_rowid: 新的最大 rowid 值 + - actions_list: Danh sách action, mỗi phần tử gồm agent_id, agent_name, action_type, action_args (có ngữ cảnh) + - new_last_rowid: Giá trị rowid lớn nhất mới """ actions = [] new_last_rowid = last_rowid @@ -682,8 +682,8 @@ def fetch_new_actions_from_db( conn = sqlite3.connect(db_path) cursor = conn.cursor() - # 使用 rowid 来追踪已处理的记录(rowid 是 SQLite 的内置自增字段) - # 这样可以避免 created_at 格式差异问题(Twitter 用整数,Reddit 用日期时间字符串) + # Dùng rowid để theo dõi bản ghi đã xử lý (rowid là trường tự tăng tích hợp của SQLite) + # Cách này tránh vấn đề khác biệt định dạng created_at (Twitter dùng số nguyên, Reddit dùng chuỗi datetime) cursor.execute(""" SELECT rowid, user_id, action, info FROM trace @@ -692,20 +692,20 @@ def fetch_new_actions_from_db( """, (last_rowid,)) for rowid, user_id, action, info_json in cursor.fetchall(): - # 更新最大 rowid + # Cập nhật rowid lớn nhất new_last_rowid = rowid - # 过滤非核心动作 + # Lọc action không cốt lõi if action in FILTERED_ACTIONS: continue - # 解析动作参数 + # Parse tham số action try: action_args = json.loads(info_json) if info_json else {} except json.JSONDecodeError: action_args = {} - # 精简 action_args,只保留关键字段(保留完整内容,不截断) + # Tinh gọn action_args, chỉ giữ trường quan trọng (giữ nguyên nội dung, không cắt) simplified_args = {} if 'content' in action_args: simplified_args['content'] = action_args['content'] @@ -726,10 +726,10 @@ def fetch_new_actions_from_db( if 'dislike_id' in action_args: simplified_args['dislike_id'] = action_args['dislike_id'] - # 转换动作类型名称 + # Chuyển tên loại action action_type = ACTION_TYPE_MAP.get(action, action.upper()) - # 补充上下文信息(帖子内容、用户名等) + # Bổ sung ngữ cảnh (nội dung bài viết, tên người dùng...) _enrich_action_context(cursor, action_type, simplified_args, agent_names) actions.append({ @@ -741,7 +741,7 @@ def fetch_new_actions_from_db( conn.close() except Exception as e: - print(f"读取数据库动作失败: {e}") + print(f"Failed to read actions from database: {e}") return actions, new_last_rowid @@ -753,16 +753,16 @@ def _enrich_action_context( agent_names: Dict[int, str] ) -> None: """ - 为动作补充上下文信息(帖子内容、用户名等) + Bổ sung ngữ cảnh cho action (nội dung bài viết, tên người dùng...) Args: - cursor: 数据库游标 - action_type: 动作类型 - action_args: 动作参数(会被修改) - agent_names: agent_id -> agent_name 映射 + cursor: DB cursor + action_type: Loại action + action_args: Tham số action (sẽ bị cập nhật) + agent_names: Ánh xạ agent_id -> agent_name """ try: - # 点赞/踩帖子:补充帖子内容和作者 + # Like/dislike bài viết: bổ sung nội dung bài và tác giả if action_type in ('LIKE_POST', 'DISLIKE_POST'): post_id = action_args.get('post_id') if post_id: @@ -771,11 +771,11 @@ def _enrich_action_context( action_args['post_content'] = post_info.get('content', '') action_args['post_author_name'] = post_info.get('author_name', '') - # 转发帖子:补充原帖内容和作者 + # Repost: bổ sung nội dung và tác giả bài gốc elif action_type == 'REPOST': new_post_id = action_args.get('new_post_id') if new_post_id: - # 转发帖子的 original_post_id 指向原帖 + # original_post_id của bài repost trỏ đến bài gốc cursor.execute(""" SELECT original_post_id FROM post WHERE post_id = ? """, (new_post_id,)) @@ -787,7 +787,7 @@ def _enrich_action_context( action_args['original_content'] = original_info.get('content', '') action_args['original_author_name'] = original_info.get('author_name', '') - # 引用帖子:补充原帖内容、作者和引用评论 + # Quote post: bổ sung nội dung bài gốc, tác giả và phần quote elif action_type == 'QUOTE_POST': quoted_id = action_args.get('quoted_id') new_post_id = action_args.get('new_post_id') @@ -798,7 +798,7 @@ def _enrich_action_context( action_args['original_content'] = original_info.get('content', '') action_args['original_author_name'] = original_info.get('author_name', '') - # 获取引用帖子的评论内容(quote_content) + # Lấy nội dung quote của bài trích dẫn (quote_content) if new_post_id: cursor.execute(""" SELECT quote_content FROM post WHERE post_id = ? @@ -807,11 +807,11 @@ def _enrich_action_context( if row and row[0]: action_args['quote_content'] = row[0] - # 关注用户:补充被关注用户的名称 + # Follow user: bổ sung tên người dùng được follow elif action_type == 'FOLLOW': follow_id = action_args.get('follow_id') if follow_id: - # 从 follow 表获取 followee_id + # Lấy followee_id từ bảng follow cursor.execute(""" SELECT followee_id FROM follow WHERE follow_id = ? """, (follow_id,)) @@ -822,16 +822,16 @@ def _enrich_action_context( if target_name: action_args['target_user_name'] = target_name - # 屏蔽用户:补充被屏蔽用户的名称 + # Mute user: bổ sung tên người dùng bị mute elif action_type == 'MUTE': - # 从 action_args 中获取 user_id 或 target_id + # Lấy user_id hoặc target_id từ action_args target_id = action_args.get('user_id') or action_args.get('target_id') if target_id: target_name = _get_user_name(cursor, target_id, agent_names) if target_name: action_args['target_user_name'] = target_name - # 点赞/踩评论:补充评论内容和作者 + # Like/dislike comment: bổ sung nội dung comment và tác giả elif action_type in ('LIKE_COMMENT', 'DISLIKE_COMMENT'): comment_id = action_args.get('comment_id') if comment_id: @@ -840,7 +840,7 @@ def _enrich_action_context( action_args['comment_content'] = comment_info.get('content', '') action_args['comment_author_name'] = comment_info.get('author_name', '') - # 发表评论:补充所评论的帖子信息 + # Create comment: bổ sung thông tin bài viết được bình luận elif action_type == 'CREATE_COMMENT': post_id = action_args.get('post_id') if post_id: @@ -850,8 +850,8 @@ def _enrich_action_context( action_args['post_author_name'] = post_info.get('author_name', '') except Exception as e: - # 补充上下文失败不影响主流程 - print(f"补充动作上下文失败: {e}") + # Bổ sung ngữ cảnh thất bại không ảnh hưởng luồng chính + print(f"Failed to enrich action context: {e}") def _get_post_info( @@ -860,15 +860,15 @@ def _get_post_info( agent_names: Dict[int, str] ) -> Optional[Dict[str, str]]: """ - 获取帖子信息 + Lấy thông tin bài viết Args: - cursor: 数据库游标 - post_id: 帖子ID - agent_names: agent_id -> agent_name 映射 + cursor: DB cursor + post_id: Post ID + agent_names: Ánh xạ agent_id -> agent_name Returns: - 包含 content 和 author_name 的字典,或 None + Dictionary chứa content và author_name, hoặc None """ try: cursor.execute(""" @@ -883,12 +883,12 @@ def _get_post_info( user_id = row[1] agent_id = row[2] - # 优先使用 agent_names 中的名称 + # Ưu tiên dùng tên từ agent_names author_name = '' if agent_id is not None and agent_id in agent_names: author_name = agent_names[agent_id] elif user_id: - # 从 user 表获取名称 + # Lấy tên từ bảng user cursor.execute("SELECT name, user_name FROM user WHERE user_id = ?", (user_id,)) user_row = cursor.fetchone() if user_row: @@ -906,15 +906,15 @@ def _get_user_name( agent_names: Dict[int, str] ) -> Optional[str]: """ - 获取用户名称 + Lấy tên người dùng Args: - cursor: 数据库游标 - user_id: 用户ID - agent_names: agent_id -> agent_name 映射 + cursor: DB cursor + user_id: User ID + agent_names: Ánh xạ agent_id -> agent_name Returns: - 用户名称,或 None + Tên người dùng, hoặc None """ try: cursor.execute(""" @@ -926,7 +926,7 @@ def _get_user_name( name = row[1] user_name = row[2] - # 优先使用 agent_names 中的名称 + # Ưu tiên dùng tên từ agent_names if agent_id is not None and agent_id in agent_names: return agent_names[agent_id] return name or user_name or '' @@ -941,15 +941,15 @@ def _get_comment_info( agent_names: Dict[int, str] ) -> Optional[Dict[str, str]]: """ - 获取评论信息 + Lấy thông tin bình luận Args: - cursor: 数据库游标 - comment_id: 评论ID - agent_names: agent_id -> agent_name 映射 + cursor: DB cursor + comment_id: Comment ID + agent_names: Ánh xạ agent_id -> agent_name Returns: - 包含 content 和 author_name 的字典,或 None + Dictionary chứa content và author_name, hoặc None """ try: cursor.execute(""" @@ -964,12 +964,12 @@ def _get_comment_info( user_id = row[1] agent_id = row[2] - # 优先使用 agent_names 中的名称 + # Ưu tiên dùng tên từ agent_names author_name = '' if agent_id is not None and agent_id in agent_names: author_name = agent_names[agent_id] elif user_id: - # 从 user 表获取名称 + # Lấy tên từ bảng user cursor.execute("SELECT name, user_name FROM user WHERE user_id = ?", (user_id,)) user_row = cursor.fetchone() if user_row: @@ -983,53 +983,53 @@ def _get_comment_info( def create_model(config: Dict[str, Any], use_boost: bool = False): """ - 创建LLM模型 + Tạo mô hình LLM - 支持双 LLM 配置,用于并行模拟时提速: - - 通用配置:LLM_API_KEY, LLM_BASE_URL, LLM_MODEL_NAME - - 加速配置(可选):LLM_BOOST_API_KEY, LLM_BOOST_BASE_URL, LLM_BOOST_MODEL_NAME + Hỗ trợ cấu hình hai LLM để tăng tốc khi mô phỏng song song: + - Cấu hình chung: LLM_API_KEY, LLM_BASE_URL, LLM_MODEL_NAME + - Cấu hình tăng tốc (tùy chọn): LLM_BOOST_API_KEY, LLM_BOOST_BASE_URL, LLM_BOOST_MODEL_NAME - 如果配置了加速 LLM,并行模拟时可以让不同平台使用不同的 API 服务商,提高并发能力。 + Nếu có cấu hình LLM tăng tốc, mỗi nền tảng có thể dùng nhà cung cấp API khác nhau để tăng khả năng song song. Args: - config: 模拟配置字典 - use_boost: 是否使用加速 LLM 配置(如果可用) + config: Dictionary cấu hình mô phỏng + use_boost: Có dùng cấu hình LLM tăng tốc hay không (nếu khả dụng) """ - # 检查是否有加速配置 + # Kiểm tra có cấu hình tăng tốc không boost_api_key = os.environ.get("LLM_BOOST_API_KEY", "") boost_base_url = os.environ.get("LLM_BOOST_BASE_URL", "") boost_model = os.environ.get("LLM_BOOST_MODEL_NAME", "") has_boost_config = bool(boost_api_key) - # 根据参数和配置情况选择使用哪个 LLM + # Chọn LLM theo tham số và trạng thái cấu hình if use_boost and has_boost_config: - # 使用加速配置 + # Dùng cấu hình tăng tốc llm_api_key = boost_api_key llm_base_url = boost_base_url llm_model = boost_model or os.environ.get("LLM_MODEL_NAME", "") - config_label = "[加速LLM]" + config_label = "[Boost LLM]" else: - # 使用通用配置 + # Dùng cấu hình chung llm_api_key = os.environ.get("LLM_API_KEY", "") llm_base_url = os.environ.get("LLM_BASE_URL", "") llm_model = os.environ.get("LLM_MODEL_NAME", "") - config_label = "[通用LLM]" + config_label = "[General LLM]" - # 如果 .env 中没有模型名,则使用 config 作为备用 + # Nếu .env không có model name, dùng config làm phương án dự phòng if not llm_model: llm_model = config.get("llm_model", "gpt-4o-mini") - # 设置 camel-ai 所需的环境变量 + # Thiết lập biến môi trường cần thiết cho camel-ai if llm_api_key: os.environ["OPENAI_API_KEY"] = llm_api_key if not os.environ.get("OPENAI_API_KEY"): - raise ValueError("缺少 API Key 配置,请在项目根目录 .env 文件中设置 LLM_API_KEY") + raise ValueError("Missing API key config. Please set LLM_API_KEY in the project root .env file") if llm_base_url: os.environ["OPENAI_API_BASE_URL"] = llm_base_url - print(f"{config_label} model={llm_model}, base_url={llm_base_url[:40] if llm_base_url else '默认'}...") + print(f"{config_label} model={llm_model}, base_url={llm_base_url[:40] if llm_base_url else 'default'}...") return ModelFactory.create( model_platform=ModelPlatformType.OPENAI, @@ -1043,7 +1043,7 @@ def get_active_agents_for_round( current_hour: int, round_num: int ) -> List: - """根据时间和配置决定本轮激活哪些Agent""" + """Quyết định Agent nào được kích hoạt trong round hiện tại dựa trên thời gian và cấu hình""" time_config = config.get("time_config", {}) agent_configs = config.get("agent_configs", []) @@ -1091,7 +1091,7 @@ def get_active_agents_for_round( class PlatformSimulation: - """平台模拟结果容器""" + """Container kết quả mô phỏng theo nền tảng""" def __init__(self): self.env = None self.agent_graph = None @@ -1105,17 +1105,17 @@ async def run_twitter_simulation( main_logger: Optional[SimulationLogManager] = None, max_rounds: Optional[int] = None ) -> PlatformSimulation: - """运行Twitter模拟 + """Chạy mô phỏng Twitter Args: - config: 模拟配置 - simulation_dir: 模拟目录 - action_logger: 动作日志记录器 - main_logger: 主日志管理器 - max_rounds: 最大模拟轮数(可选,用于截断过长的模拟) + config: Cấu hình mô phỏng + simulation_dir: Thư mục mô phỏng + action_logger: Logger hành động + main_logger: Trình quản lý log chính + max_rounds: Số round tối đa (tùy chọn, dùng để cắt ngắn mô phỏng quá dài) Returns: - PlatformSimulation: 包含env和agent_graph的结果对象 + PlatformSimulation: Đối tượng kết quả chứa env và agent_graph """ result = PlatformSimulation() @@ -1124,15 +1124,15 @@ def log_info(msg): main_logger.info(f"[Twitter] {msg}") print(f"[Twitter] {msg}") - log_info("初始化...") + log_info("Initializing...") - # Twitter 使用通用 LLM 配置 + # Twitter dùng cấu hình LLM chung model = create_model(config, use_boost=False) - # OASIS Twitter使用CSV格式 + # OASIS Twitter dùng định dạng CSV profile_path = os.path.join(simulation_dir, "twitter_profiles.csv") if not os.path.exists(profile_path): - log_info(f"错误: Profile文件不存在: {profile_path}") + log_info(f"Error: Profile file not found: {profile_path}") return result result.agent_graph = await generate_twitter_agent_graph( @@ -1141,9 +1141,9 @@ def log_info(msg): available_actions=TWITTER_ACTIONS, ) - # 从配置文件获取 Agent 真实名称映射(使用 entity_name 而非默认的 Agent_X) + # Lấy ánh xạ tên thật của Agent từ config (dùng entity_name thay vì Agent_X mặc định) agent_names = get_agent_names_from_config(config) - # 如果配置中没有某个 agent,则使用 OASIS 的默认名称 + # Nếu config không có Agent nào đó thì dùng tên mặc định của OASIS for agent_id, agent in result.agent_graph.get_agents(): if agent_id not in agent_names: agent_names[agent_id] = getattr(agent, 'name', f'Agent_{agent_id}') @@ -1156,23 +1156,23 @@ def log_info(msg): agent_graph=result.agent_graph, platform=oasis.DefaultPlatformType.TWITTER, database_path=db_path, - semaphore=30, # 限制最大并发 LLM 请求数,防止 API 过载 + semaphore=30, # Giới hạn số request LLM đồng thời để tránh quá tải API ) await result.env.reset() - log_info("环境已启动") + log_info("Environment started") if action_logger: action_logger.log_simulation_start(config) total_actions = 0 - last_rowid = 0 # 跟踪数据库中最后处理的行号(使用 rowid 避免 created_at 格式差异) + last_rowid = 0 # Theo dõi row đã xử lý cuối cùng trong DB (dùng rowid để tránh khác biệt định dạng created_at) - # 执行初始事件 + # Thực thi sự kiện khởi tạo event_config = config.get("event_config", {}) initial_posts = event_config.get("initial_posts", []) - # 记录 round 0 开始(初始事件阶段) + # Ghi log bắt đầu round 0 (giai đoạn sự kiện khởi tạo) if action_logger: action_logger.log_round_start(0, 0) # round 0, simulated_hour 0 @@ -1204,32 +1204,32 @@ def log_info(msg): if initial_actions: await result.env.step(initial_actions) - log_info(f"已发布 {len(initial_actions)} 条初始帖子") + log_info(f"Published {len(initial_actions)} initial posts") - # 记录 round 0 结束 + # Ghi log kết thúc round 0 if action_logger: action_logger.log_round_end(0, initial_action_count) - # 主模拟循环 + # Vòng lặp mô phỏng chính time_config = config.get("time_config", {}) total_hours = time_config.get("total_simulation_hours", 72) minutes_per_round = time_config.get("minutes_per_round", 30) total_rounds = (total_hours * 60) // minutes_per_round - # 如果指定了最大轮数,则截断 + # Nếu chỉ định max rounds thì cắt ngắn if max_rounds is not None and max_rounds > 0: original_rounds = total_rounds total_rounds = min(total_rounds, max_rounds) if total_rounds < original_rounds: - log_info(f"轮数已截断: {original_rounds} -> {total_rounds} (max_rounds={max_rounds})") + log_info(f"Rounds truncated: {original_rounds} -> {total_rounds} (max_rounds={max_rounds})") start_time = datetime.now() for round_num in range(total_rounds): - # 检查是否收到退出信号 + # Kiểm tra có nhận tín hiệu thoát không if _shutdown_event and _shutdown_event.is_set(): if main_logger: - main_logger.info(f"收到退出信号,在第 {round_num + 1} 轮停止模拟") + main_logger.info(f"Received shutdown signal, stop simulation at round {round_num + 1}") break simulated_minutes = round_num * minutes_per_round @@ -1240,12 +1240,12 @@ def log_info(msg): result.env, config, simulated_hour, round_num ) - # 无论是否有活跃agent,都记录round开始 + # Dù có Agent hoạt động hay không, vẫn ghi log bắt đầu round if action_logger: action_logger.log_round_start(round_num + 1, simulated_hour) if not active_agents: - # 没有活跃agent时也记录round结束(actions_count=0) + # Không có Agent hoạt động thì vẫn ghi log kết thúc round (actions_count=0) if action_logger: action_logger.log_round_end(round_num + 1, 0) continue @@ -1253,7 +1253,7 @@ def log_info(msg): actions = {agent: LLMAction() for _, agent in active_agents} await result.env.step(actions) - # 从数据库获取实际执行的动作并记录 + # Lấy action thực tế đã chạy từ DB và ghi log actual_actions, last_rowid = fetch_new_actions_from_db( db_path, last_rowid, agent_names ) @@ -1278,14 +1278,14 @@ def log_info(msg): progress = (round_num + 1) / total_rounds * 100 log_info(f"Day {simulated_day}, {simulated_hour:02d}:00 - Round {round_num + 1}/{total_rounds} ({progress:.1f}%)") - # 注意:不关闭环境,保留给Interview使用 + # Lưu ý: Không đóng environment, giữ lại để dùng cho Interview if action_logger: action_logger.log_simulation_end(total_rounds, total_actions) result.total_actions = total_actions elapsed = (datetime.now() - start_time).total_seconds() - log_info(f"模拟循环完成! 耗时: {elapsed:.1f}秒, 总动作: {total_actions}") + log_info(f"Simulation loop completed! Elapsed: {elapsed:.1f}s, total actions: {total_actions}") return result @@ -1297,17 +1297,17 @@ async def run_reddit_simulation( main_logger: Optional[SimulationLogManager] = None, max_rounds: Optional[int] = None ) -> PlatformSimulation: - """运行Reddit模拟 + """Chạy mô phỏng Reddit Args: - config: 模拟配置 - simulation_dir: 模拟目录 - action_logger: 动作日志记录器 - main_logger: 主日志管理器 - max_rounds: 最大模拟轮数(可选,用于截断过长的模拟) + config: Cấu hình mô phỏng + simulation_dir: Thư mục mô phỏng + action_logger: Logger hành động + main_logger: Trình quản lý log chính + max_rounds: Số round tối đa (tùy chọn, dùng để cắt ngắn mô phỏng quá dài) Returns: - PlatformSimulation: 包含env和agent_graph的结果对象 + PlatformSimulation: Đối tượng kết quả chứa env và agent_graph """ result = PlatformSimulation() @@ -1316,14 +1316,14 @@ def log_info(msg): main_logger.info(f"[Reddit] {msg}") print(f"[Reddit] {msg}") - log_info("初始化...") + log_info("Initializing...") - # Reddit 使用加速 LLM 配置(如果有的话,否则回退到通用配置) + # Reddit dùng cấu hình LLM tăng tốc (nếu có, nếu không thì fallback về cấu hình chung) model = create_model(config, use_boost=True) profile_path = os.path.join(simulation_dir, "reddit_profiles.json") if not os.path.exists(profile_path): - log_info(f"错误: Profile文件不存在: {profile_path}") + log_info(f"Error: Profile file not found: {profile_path}") return result result.agent_graph = await generate_reddit_agent_graph( @@ -1332,9 +1332,9 @@ def log_info(msg): available_actions=REDDIT_ACTIONS, ) - # 从配置文件获取 Agent 真实名称映射(使用 entity_name 而非默认的 Agent_X) + # Lấy ánh xạ tên thật của Agent từ config (dùng entity_name thay vì Agent_X mặc định) agent_names = get_agent_names_from_config(config) - # 如果配置中没有某个 agent,则使用 OASIS 的默认名称 + # Nếu config không có Agent nào đó thì dùng tên mặc định của OASIS for agent_id, agent in result.agent_graph.get_agents(): if agent_id not in agent_names: agent_names[agent_id] = getattr(agent, 'name', f'Agent_{agent_id}') @@ -1347,23 +1347,23 @@ def log_info(msg): agent_graph=result.agent_graph, platform=oasis.DefaultPlatformType.REDDIT, database_path=db_path, - semaphore=30, # 限制最大并发 LLM 请求数,防止 API 过载 + semaphore=30, # Giới hạn số request LLM đồng thời để tránh quá tải API ) await result.env.reset() - log_info("环境已启动") + log_info("Environment started") if action_logger: action_logger.log_simulation_start(config) total_actions = 0 - last_rowid = 0 # 跟踪数据库中最后处理的行号(使用 rowid 避免 created_at 格式差异) + last_rowid = 0 # Theo dõi row đã xử lý cuối cùng trong DB (dùng rowid để tránh khác biệt định dạng created_at) - # 执行初始事件 + # Thực thi sự kiện khởi tạo event_config = config.get("event_config", {}) initial_posts = event_config.get("initial_posts", []) - # 记录 round 0 开始(初始事件阶段) + # Ghi log bắt đầu round 0 (giai đoạn sự kiện khởi tạo) if action_logger: action_logger.log_round_start(0, 0) # round 0, simulated_hour 0 @@ -1403,32 +1403,32 @@ def log_info(msg): if initial_actions: await result.env.step(initial_actions) - log_info(f"已发布 {len(initial_actions)} 条初始帖子") + log_info(f"Published {len(initial_actions)} initial posts") - # 记录 round 0 结束 + # Ghi log kết thúc round 0 if action_logger: action_logger.log_round_end(0, initial_action_count) - # 主模拟循环 + # Vòng lặp mô phỏng chính time_config = config.get("time_config", {}) total_hours = time_config.get("total_simulation_hours", 72) minutes_per_round = time_config.get("minutes_per_round", 30) total_rounds = (total_hours * 60) // minutes_per_round - # 如果指定了最大轮数,则截断 + # Nếu chỉ định max rounds thì cắt ngắn if max_rounds is not None and max_rounds > 0: original_rounds = total_rounds total_rounds = min(total_rounds, max_rounds) if total_rounds < original_rounds: - log_info(f"轮数已截断: {original_rounds} -> {total_rounds} (max_rounds={max_rounds})") + log_info(f"Rounds truncated: {original_rounds} -> {total_rounds} (max_rounds={max_rounds})") start_time = datetime.now() for round_num in range(total_rounds): - # 检查是否收到退出信号 + # Kiểm tra có nhận tín hiệu thoát không if _shutdown_event and _shutdown_event.is_set(): if main_logger: - main_logger.info(f"收到退出信号,在第 {round_num + 1} 轮停止模拟") + main_logger.info(f"Received shutdown signal, stop simulation at round {round_num + 1}") break simulated_minutes = round_num * minutes_per_round @@ -1439,12 +1439,12 @@ def log_info(msg): result.env, config, simulated_hour, round_num ) - # 无论是否有活跃agent,都记录round开始 + # Dù có Agent hoạt động hay không, vẫn ghi log bắt đầu round if action_logger: action_logger.log_round_start(round_num + 1, simulated_hour) if not active_agents: - # 没有活跃agent时也记录round结束(actions_count=0) + # Không có Agent hoạt động thì vẫn ghi log kết thúc round (actions_count=0) if action_logger: action_logger.log_round_end(round_num + 1, 0) continue @@ -1452,7 +1452,7 @@ def log_info(msg): actions = {agent: LLMAction() for _, agent in active_agents} await result.env.step(actions) - # 从数据库获取实际执行的动作并记录 + # Lấy action thực tế đã chạy từ DB và ghi log actual_actions, last_rowid = fetch_new_actions_from_db( db_path, last_rowid, agent_names ) @@ -1477,76 +1477,76 @@ def log_info(msg): progress = (round_num + 1) / total_rounds * 100 log_info(f"Day {simulated_day}, {simulated_hour:02d}:00 - Round {round_num + 1}/{total_rounds} ({progress:.1f}%)") - # 注意:不关闭环境,保留给Interview使用 + # Lưu ý: Không đóng environment, giữ lại để dùng cho Interview if action_logger: action_logger.log_simulation_end(total_rounds, total_actions) result.total_actions = total_actions elapsed = (datetime.now() - start_time).total_seconds() - log_info(f"模拟循环完成! 耗时: {elapsed:.1f}秒, 总动作: {total_actions}") + log_info(f"Simulation loop completed! Elapsed: {elapsed:.1f}s, total actions: {total_actions}") return result async def main(): - parser = argparse.ArgumentParser(description='OASIS双平台并行模拟') + parser = argparse.ArgumentParser(description='OASIS dual-platform parallel simulation') parser.add_argument( '--config', type=str, required=True, - help='配置文件路径 (simulation_config.json)' + help='Path to config file (simulation_config.json)' ) parser.add_argument( '--twitter-only', action='store_true', - help='只运行Twitter模拟' + help='Run Twitter simulation only' ) parser.add_argument( '--reddit-only', action='store_true', - help='只运行Reddit模拟' + help='Run Reddit simulation only' ) parser.add_argument( '--max-rounds', type=int, default=None, - help='最大模拟轮数(可选,用于截断过长的模拟)' + help='Maximum simulation rounds (optional, used to truncate long simulations)' ) parser.add_argument( '--no-wait', action='store_true', default=False, - help='模拟完成后立即关闭环境,不进入等待命令模式' + help='Close environment immediately after simulation, do not enter command wait mode' ) args = parser.parse_args() - # 在 main 函数开始时创建 shutdown 事件,确保整个程序都能响应退出信号 + # Tạo shutdown event khi vào main để toàn bộ chương trình có thể phản hồi tín hiệu thoát global _shutdown_event _shutdown_event = asyncio.Event() if not os.path.exists(args.config): - print(f"错误: 配置文件不存在: {args.config}") + print(f"Error: Config file not found: {args.config}") sys.exit(1) config = load_config(args.config) simulation_dir = os.path.dirname(args.config) or "." wait_for_commands = not args.no_wait - # 初始化日志配置(禁用 OASIS 日志,清理旧文件) + # Khởi tạo cấu hình log (tắt log OASIS, dọn file cũ) init_logging_for_simulation(simulation_dir) - # 创建日志管理器 + # Tạo trình quản lý log log_manager = SimulationLogManager(simulation_dir) twitter_logger = log_manager.get_twitter_logger() reddit_logger = log_manager.get_reddit_logger() log_manager.info("=" * 60) - log_manager.info("OASIS 双平台并行模拟") - log_manager.info(f"配置文件: {args.config}") - log_manager.info(f"模拟ID: {config.get('simulation_id', 'unknown')}") - log_manager.info(f"等待命令模式: {'启用' if wait_for_commands else '禁用'}") + log_manager.info("OASIS dual-platform parallel simulation") + log_manager.info(f"Config file: {args.config}") + log_manager.info(f"Simulation ID: {config.get('simulation_id', 'unknown')}") + log_manager.info(f"Command wait mode: {'enabled' if wait_for_commands else 'disabled'}") log_manager.info("=" * 60) time_config = config.get("time_config", {}) @@ -1554,25 +1554,25 @@ async def main(): minutes_per_round = time_config.get('minutes_per_round', 30) config_total_rounds = (total_hours * 60) // minutes_per_round - log_manager.info(f"模拟参数:") - log_manager.info(f" - 总模拟时长: {total_hours}小时") - log_manager.info(f" - 每轮时间: {minutes_per_round}分钟") - log_manager.info(f" - 配置总轮数: {config_total_rounds}") + log_manager.info(f"Simulation parameters:") + log_manager.info(f" - Total simulation duration: {total_hours} hours") + log_manager.info(f" - Minutes per round: {minutes_per_round}") + log_manager.info(f" - Total configured rounds: {config_total_rounds}") if args.max_rounds: - log_manager.info(f" - 最大轮数限制: {args.max_rounds}") + log_manager.info(f" - Max rounds limit: {args.max_rounds}") if args.max_rounds < config_total_rounds: - log_manager.info(f" - 实际执行轮数: {args.max_rounds} (已截断)") - log_manager.info(f" - Agent数量: {len(config.get('agent_configs', []))}") + log_manager.info(f" - Actual executed rounds: {args.max_rounds} (truncated)") + log_manager.info(f" - Agent count: {len(config.get('agent_configs', []))}") - log_manager.info("日志结构:") - log_manager.info(f" - 主日志: simulation.log") - log_manager.info(f" - Twitter动作: twitter/actions.jsonl") - log_manager.info(f" - Reddit动作: reddit/actions.jsonl") + log_manager.info("Log structure:") + log_manager.info(f" - Main log: simulation.log") + log_manager.info(f" - Twitter actions: twitter/actions.jsonl") + log_manager.info(f" - Reddit actions: reddit/actions.jsonl") log_manager.info("=" * 60) start_time = datetime.now() - # 存储两个平台的模拟结果 + # Lưu kết quả mô phỏng của hai nền tảng twitter_result: Optional[PlatformSimulation] = None reddit_result: Optional[PlatformSimulation] = None @@ -1581,7 +1581,7 @@ async def main(): elif args.reddit_only: reddit_result = await run_reddit_simulation(config, simulation_dir, reddit_logger, log_manager, args.max_rounds) else: - # 并行运行(每个平台使用独立的日志记录器) + # Chạy song song (mỗi nền tảng dùng logger riêng) results = await asyncio.gather( run_twitter_simulation(config, simulation_dir, twitter_logger, log_manager, args.max_rounds), run_reddit_simulation(config, simulation_dir, reddit_logger, log_manager, args.max_rounds), @@ -1590,17 +1590,17 @@ async def main(): total_elapsed = (datetime.now() - start_time).total_seconds() log_manager.info("=" * 60) - log_manager.info(f"模拟循环完成! 总耗时: {total_elapsed:.1f}秒") + log_manager.info(f"Simulation loop completed! Total elapsed: {total_elapsed:.1f}s") - # 是否进入等待命令模式 + # Có vào chế độ chờ lệnh hay không if wait_for_commands: log_manager.info("") log_manager.info("=" * 60) - log_manager.info("进入等待命令模式 - 环境保持运行") - log_manager.info("支持的命令: interview, batch_interview, close_env") + log_manager.info("Entering command wait mode - environment stays running") + log_manager.info("Supported commands: interview, batch_interview, close_env") log_manager.info("=" * 60) - # 创建IPC处理器 + # Tạo bộ xử lý IPC ipc_handler = ParallelIPCHandler( simulation_dir=simulation_dir, twitter_env=twitter_result.env if twitter_result else None, @@ -1610,40 +1610,40 @@ async def main(): ) ipc_handler.update_status("alive") - # 等待命令循环(使用全局 _shutdown_event) + # Vòng lặp chờ lệnh (dùng _shutdown_event toàn cục) try: while not _shutdown_event.is_set(): should_continue = await ipc_handler.process_commands() if not should_continue: break - # 使用 wait_for 替代 sleep,这样可以响应 shutdown_event + # Dùng wait_for thay cho sleep để có thể phản hồi shutdown_event try: await asyncio.wait_for(_shutdown_event.wait(), timeout=0.5) - break # 收到退出信号 + break # Đã nhận tín hiệu thoát except asyncio.TimeoutError: - pass # 超时继续循环 + pass # Timeout thì tiếp tục vòng lặp except KeyboardInterrupt: - print("\n收到中断信号") + print("\nInterrupt signal received") except asyncio.CancelledError: - print("\n任务被取消") + print("\nTask cancelled") except Exception as e: - print(f"\n命令处理出错: {e}") + print(f"\nCommand processing error: {e}") - log_manager.info("\n关闭环境...") + log_manager.info("\nClosing environment...") ipc_handler.update_status("stopped") - # 关闭环境 + # Đóng environment if twitter_result and twitter_result.env: await twitter_result.env.close() - log_manager.info("[Twitter] 环境已关闭") + log_manager.info("[Twitter] Environment closed") if reddit_result and reddit_result.env: await reddit_result.env.close() - log_manager.info("[Reddit] 环境已关闭") + log_manager.info("[Reddit] Environment closed") log_manager.info("=" * 60) - log_manager.info(f"全部完成!") - log_manager.info(f"日志文件:") + log_manager.info(f"All done!") + log_manager.info(f"Log files:") log_manager.info(f" - {os.path.join(simulation_dir, 'simulation.log')}") log_manager.info(f" - {os.path.join(simulation_dir, 'twitter', 'actions.jsonl')}") log_manager.info(f" - {os.path.join(simulation_dir, 'reddit', 'actions.jsonl')}") @@ -1652,29 +1652,29 @@ async def main(): def setup_signal_handlers(loop=None): """ - 设置信号处理器,确保收到 SIGTERM/SIGINT 时能够正确退出 + Thiết lập signal handler để thoát đúng cách khi nhận SIGTERM/SIGINT - 持久化模拟场景:模拟完成后不退出,等待 interview 命令 - 当收到终止信号时,需要: - 1. 通知 asyncio 循环退出等待 - 2. 让程序有机会正常清理资源(关闭数据库、环境等) - 3. 然后才退出 + Kịch bản mô phỏng bền vững: không thoát ngay sau khi mô phỏng xong, tiếp tục chờ lệnh interview + Khi nhận tín hiệu dừng, cần: + 1. Thông báo vòng lặp asyncio thoát khỏi trạng thái chờ + 2. Cho chương trình cơ hội dọn tài nguyên đúng cách (đóng DB, environment...) + 3. Sau đó mới thoát """ def signal_handler(signum, frame): global _cleanup_done sig_name = "SIGTERM" if signum == signal.SIGTERM else "SIGINT" - print(f"\n收到 {sig_name} 信号,正在退出...") + print(f"\nReceived {sig_name}, shutting down...") if not _cleanup_done: _cleanup_done = True - # 设置事件通知 asyncio 循环退出(让循环有机会清理资源) + # Set event để thông báo vòng lặp asyncio thoát (để kịp dọn tài nguyên) if _shutdown_event: _shutdown_event.set() - # 不要直接 sys.exit(),让 asyncio 循环正常退出并清理资源 - # 如果是重复收到信号,才强制退出 + # Không gọi sys.exit() ngay, để asyncio thoát tự nhiên và dọn tài nguyên + # Nếu nhận tín hiệu lặp lại thì mới ép thoát else: - print("强制退出...") + print("Force exit...") sys.exit(1) signal.signal(signal.SIGTERM, signal_handler) @@ -1686,14 +1686,14 @@ def signal_handler(signum, frame): try: asyncio.run(main()) except KeyboardInterrupt: - print("\n程序被中断") + print("\nProgram interrupted") except SystemExit: pass finally: - # 清理 multiprocessing 资源跟踪器(防止退出时的警告) + # Dọn resource tracker của multiprocessing (tránh cảnh báo khi thoát) try: from multiprocessing import resource_tracker resource_tracker._resource_tracker._stop() except Exception: pass - print("模拟进程已退出") + print("Simulation process exited") diff --git a/backend/scripts/run_reddit_simulation.py b/backend/scripts/run_reddit_simulation.py index 14907cbda..cc0636164 100644 --- a/backend/scripts/run_reddit_simulation.py +++ b/backend/scripts/run_reddit_simulation.py @@ -1,16 +1,16 @@ """ -OASIS Reddit模拟预设脚本 -此脚本读取配置文件中的参数来执行模拟,实现全程自动化 +Kịch bản thiết lập sẵn mô phỏng OASIS Reddit +Script này đọc tham số trong file cấu hình để chạy mô phỏng tự động hoàn toàn -功能特性: -- 完成模拟后不立即关闭环境,进入等待命令模式 -- 支持通过IPC接收Interview命令 -- 支持单个Agent采访和批量采访 -- 支持远程关闭环境命令 +Tính năng: +- Sau khi hoàn tất mô phỏng, không đóng môi trường ngay mà chuyển sang chế độ chờ lệnh +- Hỗ trợ nhận lệnh Interview qua IPC +- Hỗ trợ phỏng vấn một Agent hoặc phỏng vấn hàng loạt +- Hỗ trợ lệnh đóng môi trường từ xa -使用方式: +Cách dùng: python run_reddit_simulation.py --config /path/to/simulation_config.json - python run_reddit_simulation.py --config /path/to/simulation_config.json --no-wait # 完成后立即关闭 + python run_reddit_simulation.py --config /path/to/simulation_config.json --no-wait # đóng ngay sau khi hoàn tất """ import argparse @@ -25,18 +25,18 @@ from datetime import datetime from typing import Dict, Any, List, Optional -# 全局变量:用于信号处理 +# Biến toàn cục: dùng cho xử lý tín hiệu _shutdown_event = None _cleanup_done = False -# 添加项目路径 +# Thêm đường dẫn dự án _scripts_dir = os.path.dirname(os.path.abspath(__file__)) _backend_dir = os.path.abspath(os.path.join(_scripts_dir, '..')) _project_root = os.path.abspath(os.path.join(_backend_dir, '..')) sys.path.insert(0, _scripts_dir) sys.path.insert(0, _backend_dir) -# 加载项目根目录的 .env 文件(包含 LLM_API_KEY 等配置) +# Tải file .env ở thư mục gốc dự án (bao gồm LLM_API_KEY và các cấu hình khác) from dotenv import load_dotenv _env_file = os.path.join(_project_root, '.env') if os.path.exists(_env_file): @@ -51,7 +51,7 @@ class UnicodeFormatter(logging.Formatter): - """自定义格式化器,将 Unicode 转义序列转换为可读字符""" + """Bộ định dạng tùy chỉnh, chuyển chuỗi escape Unicode thành ký tự dễ đọc""" UNICODE_ESCAPE_PATTERN = re.compile(r'\\u([0-9a-fA-F]{4})') @@ -68,24 +68,24 @@ def replace_unicode(match): class MaxTokensWarningFilter(logging.Filter): - """过滤掉 camel-ai 关于 max_tokens 的警告(我们故意不设置 max_tokens,让模型自行决定)""" + """Lọc cảnh báo max_tokens của camel-ai (chúng ta cố ý không đặt max_tokens để mô hình tự quyết định)""" def filter(self, record): - # 过滤掉包含 max_tokens 警告的日志 + # Lọc log chứa cảnh báo max_tokens if "max_tokens" in record.getMessage() and "Invalid or missing" in record.getMessage(): return False return True -# 在模块加载时立即添加过滤器,确保在 camel 代码执行前生效 +# Thêm bộ lọc ngay khi tải module để có hiệu lực trước khi mã camel chạy logging.getLogger().addFilter(MaxTokensWarningFilter()) def setup_oasis_logging(log_dir: str): - """配置 OASIS 的日志,使用固定名称的日志文件""" + """Cấu hình log OASIS với tên file cố định""" os.makedirs(log_dir, exist_ok=True) - # 清理旧的日志文件 + # Dọn các file log cũ for f in os.listdir(log_dir): old_log = os.path.join(log_dir, f) if os.path.isfile(old_log) and f.endswith('.log'): @@ -126,25 +126,25 @@ def setup_oasis_logging(log_dir: str): generate_reddit_agent_graph ) except ImportError as e: - print(f"错误: 缺少依赖 {e}") - print("请先安装: pip install oasis-ai camel-ai") + print(f"Error: missing dependency {e}") + print("Please install first: pip install oasis-ai camel-ai") sys.exit(1) -# IPC相关常量 +# Hằng số liên quan IPC IPC_COMMANDS_DIR = "ipc_commands" IPC_RESPONSES_DIR = "ipc_responses" ENV_STATUS_FILE = "env_status.json" class CommandType: - """命令类型常量""" + """Hằng số loại lệnh""" INTERVIEW = "interview" BATCH_INTERVIEW = "batch_interview" CLOSE_ENV = "close_env" class IPCHandler: - """IPC命令处理器""" + """Bộ xử lý lệnh IPC""" def __init__(self, simulation_dir: str, env, agent_graph): self.simulation_dir = simulation_dir @@ -155,12 +155,12 @@ def __init__(self, simulation_dir: str, env, agent_graph): self.status_file = os.path.join(simulation_dir, ENV_STATUS_FILE) self._running = True - # 确保目录存在 + # Đảm bảo thư mục tồn tại os.makedirs(self.commands_dir, exist_ok=True) os.makedirs(self.responses_dir, exist_ok=True) def update_status(self, status: str): - """更新环境状态""" + """Cập nhật trạng thái môi trường""" with open(self.status_file, 'w', encoding='utf-8') as f: json.dump({ "status": status, @@ -168,11 +168,11 @@ def update_status(self, status: str): }, f, ensure_ascii=False, indent=2) def poll_command(self) -> Optional[Dict[str, Any]]: - """轮询获取待处理命令""" + """Poll để lấy lệnh đang chờ xử lý""" if not os.path.exists(self.commands_dir): return None - # 获取命令文件(按时间排序) + # Lấy file lệnh (sắp xếp theo thời gian) command_files = [] for filename in os.listdir(self.commands_dir): if filename.endswith('.json'): @@ -191,7 +191,7 @@ def poll_command(self) -> Optional[Dict[str, Any]]: return None def send_response(self, command_id: str, status: str, result: Dict = None, error: str = None): - """发送响应""" + """Gửi phản hồi""" response = { "command_id": command_id, "status": status, @@ -204,7 +204,7 @@ def send_response(self, command_id: str, status: str, result: Dict = None, error with open(response_file, 'w', encoding='utf-8') as f: json.dump(response, f, ensure_ascii=False, indent=2) - # 删除命令文件 + # Xóa file lệnh command_file = os.path.join(self.commands_dir, f"{command_id}.json") try: os.remove(command_file) @@ -213,49 +213,49 @@ def send_response(self, command_id: str, status: str, result: Dict = None, error async def handle_interview(self, command_id: str, agent_id: int, prompt: str) -> bool: """ - 处理单个Agent采访命令 + Xử lý lệnh phỏng vấn một Agent Returns: - True 表示成功,False 表示失败 + True là thành công, False là thất bại """ try: - # 获取Agent + # Lấy Agent agent = self.agent_graph.get_agent(agent_id) - # 创建Interview动作 + # Tạo hành động Interview interview_action = ManualAction( action_type=ActionType.INTERVIEW, action_args={"prompt": prompt} ) - # 执行Interview + # Thực thi Interview actions = {agent: interview_action} await self.env.step(actions) - # 从数据库获取结果 + # Lấy kết quả từ cơ sở dữ liệu result = self._get_interview_result(agent_id) self.send_response(command_id, "completed", result=result) - print(f" Interview完成: agent_id={agent_id}") + print(f" Interview completed: agent_id={agent_id}") return True except Exception as e: error_msg = str(e) - print(f" Interview失败: agent_id={agent_id}, error={error_msg}") + print(f" Interview failed: agent_id={agent_id}, error={error_msg}") self.send_response(command_id, "failed", error=error_msg) return False async def handle_batch_interview(self, command_id: str, interviews: List[Dict]) -> bool: """ - 处理批量采访命令 + Xử lý lệnh phỏng vấn hàng loạt Args: interviews: [{"agent_id": int, "prompt": str}, ...] """ try: - # 构建动作字典 + # Tạo dict hành động actions = {} - agent_prompts = {} # 记录每个agent的prompt + agent_prompts = {} # Ghi lại prompt của từng agent for interview in interviews: agent_id = interview.get("agent_id") @@ -269,16 +269,16 @@ async def handle_batch_interview(self, command_id: str, interviews: List[Dict]) ) agent_prompts[agent_id] = prompt except Exception as e: - print(f" 警告: 无法获取Agent {agent_id}: {e}") + print(f" Warning: cannot get Agent {agent_id}: {e}") if not actions: - self.send_response(command_id, "failed", error="没有有效的Agent") + self.send_response(command_id, "failed", error="No valid agents") return False - # 执行批量Interview + # Thực thi Interview hàng loạt await self.env.step(actions) - # 获取所有结果 + # Lấy toàn bộ kết quả results = {} for agent_id in agent_prompts.keys(): result = self._get_interview_result(agent_id) @@ -288,17 +288,17 @@ async def handle_batch_interview(self, command_id: str, interviews: List[Dict]) "interviews_count": len(results), "results": results }) - print(f" 批量Interview完成: {len(results)} 个Agent") + print(f" Batch interview completed: {len(results)} agents") return True except Exception as e: error_msg = str(e) - print(f" 批量Interview失败: {error_msg}") + print(f" Batch interview failed: {error_msg}") self.send_response(command_id, "failed", error=error_msg) return False def _get_interview_result(self, agent_id: int) -> Dict[str, Any]: - """从数据库获取最新的Interview结果""" + """Lấy kết quả Interview mới nhất từ cơ sở dữ liệu""" db_path = os.path.join(self.simulation_dir, "reddit_simulation.db") result = { @@ -314,7 +314,7 @@ def _get_interview_result(self, agent_id: int) -> Dict[str, Any]: conn = sqlite3.connect(db_path) cursor = conn.cursor() - # 查询最新的Interview记录 + # Truy vấn bản ghi Interview mới nhất cursor.execute(""" SELECT user_id, info, created_at FROM trace @@ -336,16 +336,16 @@ def _get_interview_result(self, agent_id: int) -> Dict[str, Any]: conn.close() except Exception as e: - print(f" 读取Interview结果失败: {e}") + print(f" Failed to read Interview result: {e}") return result async def process_commands(self) -> bool: """ - 处理所有待处理命令 + Xử lý toàn bộ lệnh đang chờ Returns: - True 表示继续运行,False 表示应该退出 + True là tiếp tục chạy, False là nên thoát """ command = self.poll_command() if not command: @@ -355,7 +355,7 @@ async def process_commands(self) -> bool: command_type = command.get("command_type") args = command.get("args", {}) - print(f"\n收到IPC命令: {command_type}, id={command_id}") + print(f"\nReceived IPC command: {command_type}, id={command_id}") if command_type == CommandType.INTERVIEW: await self.handle_interview( @@ -373,19 +373,19 @@ async def process_commands(self) -> bool: return True elif command_type == CommandType.CLOSE_ENV: - print("收到关闭环境命令") - self.send_response(command_id, "completed", result={"message": "环境即将关闭"}) + print("Received close environment command") + self.send_response(command_id, "completed", result={"message": "Environment will close soon"}) return False else: - self.send_response(command_id, "failed", error=f"未知命令类型: {command_type}") + self.send_response(command_id, "failed", error=f"Unknown command type: {command_type}") return True class RedditSimulationRunner: - """Reddit模拟运行器""" + """Bộ chạy mô phỏng Reddit""" - # Reddit可用动作(不包含INTERVIEW,INTERVIEW只能通过ManualAction手动触发) + # Các hành động khả dụng cho Reddit (không bao gồm INTERVIEW; INTERVIEW chỉ kích hoạt thủ công qua ManualAction) AVAILABLE_ACTIONS = [ ActionType.LIKE_POST, ActionType.DISLIKE_POST, @@ -404,11 +404,11 @@ class RedditSimulationRunner: def __init__(self, config_path: str, wait_for_commands: bool = True): """ - 初始化模拟运行器 + Khởi tạo bộ chạy mô phỏng Args: - config_path: 配置文件路径 (simulation_config.json) - wait_for_commands: 模拟完成后是否等待命令(默认True) + config_path: Đường dẫn file cấu hình (simulation_config.json) + wait_for_commands: Có chờ lệnh sau khi mô phỏng hoàn tất hay không (mặc định True) """ self.config_path = config_path self.config = self._load_config() @@ -419,47 +419,47 @@ def __init__(self, config_path: str, wait_for_commands: bool = True): self.ipc_handler = None def _load_config(self) -> Dict[str, Any]: - """加载配置文件""" + """Tải file cấu hình""" with open(self.config_path, 'r', encoding='utf-8') as f: return json.load(f) def _get_profile_path(self) -> str: - """获取Profile文件路径""" + """Lấy đường dẫn file Profile""" return os.path.join(self.simulation_dir, "reddit_profiles.json") def _get_db_path(self) -> str: - """获取数据库路径""" + """Lấy đường dẫn cơ sở dữ liệu""" return os.path.join(self.simulation_dir, "reddit_simulation.db") def _create_model(self): """ - 创建LLM模型 + Tạo mô hình LLM - 统一使用项目根目录 .env 文件中的配置(优先级最高): - - LLM_API_KEY: API密钥 - - LLM_BASE_URL: API基础URL - - LLM_MODEL_NAME: 模型名称 + Thống nhất dùng cấu hình trong file .env tại thư mục gốc dự án (ưu tiên cao nhất): + - LLM_API_KEY: API key + - LLM_BASE_URL: URL cơ sở API + - LLM_MODEL_NAME: Tên mô hình """ - # 优先从 .env 读取配置 + # Ưu tiên đọc cấu hình từ .env llm_api_key = os.environ.get("LLM_API_KEY", "") llm_base_url = os.environ.get("LLM_BASE_URL", "") llm_model = os.environ.get("LLM_MODEL_NAME", "") - # 如果 .env 中没有,则使用 config 作为备用 + # Nếu .env không có thì dùng config làm dự phòng if not llm_model: llm_model = self.config.get("llm_model", "gpt-4o-mini") - # 设置 camel-ai 所需的环境变量 + # Thiết lập biến môi trường cần cho camel-ai if llm_api_key: os.environ["OPENAI_API_KEY"] = llm_api_key if not os.environ.get("OPENAI_API_KEY"): - raise ValueError("缺少 API Key 配置,请在项目根目录 .env 文件中设置 LLM_API_KEY") + raise ValueError("Missing API key configuration. Please set LLM_API_KEY in the project root .env file") if llm_base_url: os.environ["OPENAI_API_BASE_URL"] = llm_base_url - print(f"LLM配置: model={llm_model}, base_url={llm_base_url[:40] if llm_base_url else '默认'}...") + print(f"LLM config: model={llm_model}, base_url={llm_base_url[:40] if llm_base_url else 'default'}...") return ModelFactory.create( model_platform=ModelPlatformType.OPENAI, @@ -473,7 +473,7 @@ def _get_active_agents_for_round( round_num: int ) -> List: """ - 根据时间和配置决定本轮激活哪些Agent + Quyết định Agent nào được kích hoạt trong vòng này dựa trên thời gian và cấu hình """ time_config = self.config.get("time_config", {}) agent_configs = self.config.get("agent_configs", []) @@ -521,16 +521,16 @@ def _get_active_agents_for_round( return active_agents async def run(self, max_rounds: int = None): - """运行Reddit模拟 + """Chạy mô phỏng Reddit Args: - max_rounds: 最大模拟轮数(可选,用于截断过长的模拟) + max_rounds: Số vòng mô phỏng tối đa (tùy chọn, dùng để cắt bớt mô phỏng quá dài) """ print("=" * 60) - print("OASIS Reddit模拟") - print(f"配置文件: {self.config_path}") - print(f"模拟ID: {self.config.get('simulation_id', 'unknown')}") - print(f"等待命令模式: {'启用' if self.wait_for_commands else '禁用'}") + print("OASIS Reddit Simulation") + print(f"Config file: {self.config_path}") + print(f"Simulation ID: {self.config.get('simulation_id', 'unknown')}") + print(f"Wait mode: {'enabled' if self.wait_for_commands else 'disabled'}") print("=" * 60) time_config = self.config.get("time_config", {}) @@ -538,28 +538,28 @@ async def run(self, max_rounds: int = None): minutes_per_round = time_config.get("minutes_per_round", 30) total_rounds = (total_hours * 60) // minutes_per_round - # 如果指定了最大轮数,则截断 + # Nếu chỉ định số vòng tối đa thì sẽ cắt bớt if max_rounds is not None and max_rounds > 0: original_rounds = total_rounds total_rounds = min(total_rounds, max_rounds) if total_rounds < original_rounds: - print(f"\n轮数已截断: {original_rounds} -> {total_rounds} (max_rounds={max_rounds})") + print(f"\nRounds truncated: {original_rounds} -> {total_rounds} (max_rounds={max_rounds})") - print(f"\n模拟参数:") - print(f" - 总模拟时长: {total_hours}小时") - print(f" - 每轮时间: {minutes_per_round}分钟") - print(f" - 总轮数: {total_rounds}") + print(f"\nSimulation parameters:") + print(f" - Total simulation duration: {total_hours} hours") + print(f" - Minutes per round: {minutes_per_round} min") + print(f" - Total rounds: {total_rounds}") if max_rounds: - print(f" - 最大轮数限制: {max_rounds}") - print(f" - Agent数量: {len(self.config.get('agent_configs', []))}") + print(f" - Max round limit: {max_rounds}") + print(f" - Agent count: {len(self.config.get('agent_configs', []))}") - print("\n初始化LLM模型...") + print("\nInitializing LLM model...") model = self._create_model() - print("加载Agent Profile...") + print("Loading Agent Profile...") profile_path = self._get_profile_path() if not os.path.exists(profile_path): - print(f"错误: Profile文件不存在: {profile_path}") + print(f"Error: Profile file does not exist: {profile_path}") return self.agent_graph = await generate_reddit_agent_graph( @@ -571,29 +571,29 @@ async def run(self, max_rounds: int = None): db_path = self._get_db_path() if os.path.exists(db_path): os.remove(db_path) - print(f"已删除旧数据库: {db_path}") + print(f"Removed old database: {db_path}") - print("创建OASIS环境...") + print("Creating OASIS environment...") self.env = oasis.make( agent_graph=self.agent_graph, platform=oasis.DefaultPlatformType.REDDIT, database_path=db_path, - semaphore=30, # 限制最大并发 LLM 请求数,防止 API 过载 + semaphore=30, # Giới hạn số request LLM đồng thời tối đa để tránh quá tải API ) await self.env.reset() - print("环境初始化完成\n") + print("Environment initialization completed\n") - # 初始化IPC处理器 + # Khởi tạo bộ xử lý IPC self.ipc_handler = IPCHandler(self.simulation_dir, self.env, self.agent_graph) self.ipc_handler.update_status("running") - # 执行初始事件 + # Thực thi sự kiện ban đầu event_config = self.config.get("event_config", {}) initial_posts = event_config.get("initial_posts", []) if initial_posts: - print(f"执行初始事件 ({len(initial_posts)}条初始帖子)...") + print(f"Executing initial events ({len(initial_posts)} initial posts)...") initial_actions = {} for post in initial_posts: agent_id = post.get("poster_agent_id", 0) @@ -613,14 +613,14 @@ async def run(self, max_rounds: int = None): action_args={"content": content} ) except Exception as e: - print(f" 警告: 无法为Agent {agent_id}创建初始帖子: {e}") + print(f" Warning: cannot create initial post for Agent {agent_id}: {e}") if initial_actions: await self.env.step(initial_actions) - print(f" 已发布 {len(initial_actions)} 条初始帖子") + print(f" Published {len(initial_actions)} initial posts") - # 主模拟循环 - print("\n开始模拟循环...") + # Vòng lặp mô phỏng chính + print("\nStarting simulation loop...") start_time = datetime.now() for round_num in range(total_rounds): @@ -651,20 +651,20 @@ async def run(self, max_rounds: int = None): f"- elapsed: {elapsed:.1f}s") total_elapsed = (datetime.now() - start_time).total_seconds() - print(f"\n模拟循环完成!") - print(f" - 总耗时: {total_elapsed:.1f}秒") - print(f" - 数据库: {db_path}") + print(f"\nSimulation loop completed!") + print(f" - Total elapsed: {total_elapsed:.1f}s") + print(f" - Database: {db_path}") - # 是否进入等待命令模式 + # Có vào chế độ chờ lệnh hay không if self.wait_for_commands: print("\n" + "=" * 60) - print("进入等待命令模式 - 环境保持运行") - print("支持的命令: interview, batch_interview, close_env") + print("Entering wait mode - environment remains running") + print("Supported commands: interview, batch_interview, close_env") print("=" * 60) self.ipc_handler.update_status("alive") - # 等待命令循环(使用全局 _shutdown_event) + # Vòng lặp chờ lệnh (dùng _shutdown_event toàn cục) try: while not _shutdown_event.is_set(): should_continue = await self.ipc_handler.process_commands() @@ -672,58 +672,58 @@ async def run(self, max_rounds: int = None): break try: await asyncio.wait_for(_shutdown_event.wait(), timeout=0.5) - break # 收到退出信号 + break # Nhận tín hiệu thoát except asyncio.TimeoutError: pass except KeyboardInterrupt: - print("\n收到中断信号") + print("\nReceived interrupt signal") except asyncio.CancelledError: - print("\n任务被取消") + print("\nTask was cancelled") except Exception as e: - print(f"\n命令处理出错: {e}") + print(f"\nCommand processing error: {e}") - print("\n关闭环境...") + print("\nClosing environment...") - # 关闭环境 + # Đóng môi trường self.ipc_handler.update_status("stopped") await self.env.close() - print("环境已关闭") + print("Environment closed") print("=" * 60) async def main(): - parser = argparse.ArgumentParser(description='OASIS Reddit模拟') + parser = argparse.ArgumentParser(description='OASIS Reddit Simulation') parser.add_argument( '--config', type=str, required=True, - help='配置文件路径 (simulation_config.json)' + help='Config file path (simulation_config.json)' ) parser.add_argument( '--max-rounds', type=int, default=None, - help='最大模拟轮数(可选,用于截断过长的模拟)' + help='Max simulation rounds (optional, to truncate overly long simulations)' ) parser.add_argument( '--no-wait', action='store_true', default=False, - help='模拟完成后立即关闭环境,不进入等待命令模式' + help='Close environment immediately after simulation, do not enter wait mode' ) args = parser.parse_args() - # 在 main 函数开始时创建 shutdown 事件 + # Tạo sự kiện shutdown ở đầu hàm main global _shutdown_event _shutdown_event = asyncio.Event() if not os.path.exists(args.config): - print(f"错误: 配置文件不存在: {args.config}") + print(f"Error: config file does not exist: {args.config}") sys.exit(1) - # 初始化日志配置(使用固定文件名,清理旧日志) + # Khởi tạo cấu hình log (dùng tên file cố định, dọn log cũ) simulation_dir = os.path.dirname(args.config) or "." setup_oasis_logging(os.path.join(simulation_dir, "log")) @@ -736,20 +736,20 @@ async def main(): def setup_signal_handlers(): """ - 设置信号处理器,确保收到 SIGTERM/SIGINT 时能够正确退出 - 让程序有机会正常清理资源(关闭数据库、环境等) + Cài đặt bộ xử lý tín hiệu, bảo đảm thoát đúng khi nhận SIGTERM/SIGINT. + Giúp chương trình có cơ hội dọn tài nguyên đúng cách (đóng cơ sở dữ liệu, môi trường...). """ def signal_handler(signum, frame): global _cleanup_done sig_name = "SIGTERM" if signum == signal.SIGTERM else "SIGINT" - print(f"\n收到 {sig_name} 信号,正在退出...") + print(f"\nReceived {sig_name}, shutting down...") if not _cleanup_done: _cleanup_done = True if _shutdown_event: _shutdown_event.set() else: - # 重复收到信号才强制退出 - print("强制退出...") + # Chỉ buộc thoát khi nhận tín hiệu lặp lại + print("Force exit...") sys.exit(1) signal.signal(signal.SIGTERM, signal_handler) @@ -761,9 +761,9 @@ def signal_handler(signum, frame): try: asyncio.run(main()) except KeyboardInterrupt: - print("\n程序被中断") + print("\nProgram interrupted") except SystemExit: pass finally: - print("模拟进程已退出") + print("Simulation process exited") diff --git a/backend/scripts/run_twitter_simulation.py b/backend/scripts/run_twitter_simulation.py index caab9e9d3..aa3cec532 100644 --- a/backend/scripts/run_twitter_simulation.py +++ b/backend/scripts/run_twitter_simulation.py @@ -1,16 +1,16 @@ """ -OASIS Twitter模拟预设脚本 -此脚本读取配置文件中的参数来执行模拟,实现全程自动化 +Kịch bản thiết lập sẵn mô phỏng OASIS Twitter +Script này đọc tham số trong file cấu hình để chạy mô phỏng tự động hoàn toàn -功能特性: -- 完成模拟后不立即关闭环境,进入等待命令模式 -- 支持通过IPC接收Interview命令 -- 支持单个Agent采访和批量采访 -- 支持远程关闭环境命令 +Tính năng: +- Sau khi hoàn tất mô phỏng, không đóng môi trường ngay mà chuyển sang chế độ chờ lệnh +- Hỗ trợ nhận lệnh Interview qua IPC +- Hỗ trợ phỏng vấn một Agent hoặc phỏng vấn hàng loạt +- Hỗ trợ lệnh đóng môi trường từ xa -使用方式: +Cách dùng: python run_twitter_simulation.py --config /path/to/simulation_config.json - python run_twitter_simulation.py --config /path/to/simulation_config.json --no-wait # 完成后立即关闭 + python run_twitter_simulation.py --config /path/to/simulation_config.json --no-wait # đóng ngay sau khi hoàn tất """ import argparse @@ -25,18 +25,18 @@ from datetime import datetime from typing import Dict, Any, List, Optional -# 全局变量:用于信号处理 +# Biến toàn cục: dùng cho xử lý tín hiệu _shutdown_event = None _cleanup_done = False -# 添加项目路径 +# Thêm đường dẫn dự án _scripts_dir = os.path.dirname(os.path.abspath(__file__)) _backend_dir = os.path.abspath(os.path.join(_scripts_dir, '..')) _project_root = os.path.abspath(os.path.join(_backend_dir, '..')) sys.path.insert(0, _scripts_dir) sys.path.insert(0, _backend_dir) -# 加载项目根目录的 .env 文件(包含 LLM_API_KEY 等配置) +# Tải file .env ở thư mục gốc dự án (bao gồm LLM_API_KEY và các cấu hình khác) from dotenv import load_dotenv _env_file = os.path.join(_project_root, '.env') if os.path.exists(_env_file): @@ -51,7 +51,7 @@ class UnicodeFormatter(logging.Formatter): - """自定义格式化器,将 Unicode 转义序列转换为可读字符""" + """Bộ định dạng tùy chỉnh, chuyển chuỗi escape Unicode thành ký tự dễ đọc""" UNICODE_ESCAPE_PATTERN = re.compile(r'\\u([0-9a-fA-F]{4})') @@ -68,24 +68,24 @@ def replace_unicode(match): class MaxTokensWarningFilter(logging.Filter): - """过滤掉 camel-ai 关于 max_tokens 的警告(我们故意不设置 max_tokens,让模型自行决定)""" + """Lọc cảnh báo max_tokens của camel-ai (chúng ta cố ý không đặt max_tokens để mô hình tự quyết định)""" def filter(self, record): - # 过滤掉包含 max_tokens 警告的日志 + # Lọc log chứa cảnh báo max_tokens if "max_tokens" in record.getMessage() and "Invalid or missing" in record.getMessage(): return False return True -# 在模块加载时立即添加过滤器,确保在 camel 代码执行前生效 +# Thêm bộ lọc ngay khi tải module để có hiệu lực trước khi mã camel chạy logging.getLogger().addFilter(MaxTokensWarningFilter()) def setup_oasis_logging(log_dir: str): - """配置 OASIS 的日志,使用固定名称的日志文件""" + """Cấu hình log OASIS với tên file cố định""" os.makedirs(log_dir, exist_ok=True) - # 清理旧的日志文件 + # Dọn các file log cũ for f in os.listdir(log_dir): old_log = os.path.join(log_dir, f) if os.path.isfile(old_log) and f.endswith('.log'): @@ -126,25 +126,25 @@ def setup_oasis_logging(log_dir: str): generate_twitter_agent_graph ) except ImportError as e: - print(f"错误: 缺少依赖 {e}") - print("请先安装: pip install oasis-ai camel-ai") + print(f"Error: missing dependency {e}") + print("Please install first: pip install oasis-ai camel-ai") sys.exit(1) -# IPC相关常量 +# Hằng số liên quan IPC IPC_COMMANDS_DIR = "ipc_commands" IPC_RESPONSES_DIR = "ipc_responses" ENV_STATUS_FILE = "env_status.json" class CommandType: - """命令类型常量""" + """Hằng số loại lệnh""" INTERVIEW = "interview" BATCH_INTERVIEW = "batch_interview" CLOSE_ENV = "close_env" class IPCHandler: - """IPC命令处理器""" + """Bộ xử lý lệnh IPC""" def __init__(self, simulation_dir: str, env, agent_graph): self.simulation_dir = simulation_dir @@ -155,12 +155,12 @@ def __init__(self, simulation_dir: str, env, agent_graph): self.status_file = os.path.join(simulation_dir, ENV_STATUS_FILE) self._running = True - # 确保目录存在 + # Đảm bảo thư mục tồn tại os.makedirs(self.commands_dir, exist_ok=True) os.makedirs(self.responses_dir, exist_ok=True) def update_status(self, status: str): - """更新环境状态""" + """Cập nhật trạng thái môi trường""" with open(self.status_file, 'w', encoding='utf-8') as f: json.dump({ "status": status, @@ -168,11 +168,11 @@ def update_status(self, status: str): }, f, ensure_ascii=False, indent=2) def poll_command(self) -> Optional[Dict[str, Any]]: - """轮询获取待处理命令""" + """Poll để lấy lệnh đang chờ xử lý""" if not os.path.exists(self.commands_dir): return None - # 获取命令文件(按时间排序) + # Lấy file lệnh (sắp xếp theo thời gian) command_files = [] for filename in os.listdir(self.commands_dir): if filename.endswith('.json'): @@ -191,7 +191,7 @@ def poll_command(self) -> Optional[Dict[str, Any]]: return None def send_response(self, command_id: str, status: str, result: Dict = None, error: str = None): - """发送响应""" + """Gửi phản hồi""" response = { "command_id": command_id, "status": status, @@ -204,7 +204,7 @@ def send_response(self, command_id: str, status: str, result: Dict = None, error with open(response_file, 'w', encoding='utf-8') as f: json.dump(response, f, ensure_ascii=False, indent=2) - # 删除命令文件 + # Xóa file lệnh command_file = os.path.join(self.commands_dir, f"{command_id}.json") try: os.remove(command_file) @@ -213,49 +213,49 @@ def send_response(self, command_id: str, status: str, result: Dict = None, error async def handle_interview(self, command_id: str, agent_id: int, prompt: str) -> bool: """ - 处理单个Agent采访命令 + Xử lý lệnh phỏng vấn một Agent Returns: - True 表示成功,False 表示失败 + True là thành công, False là thất bại """ try: - # 获取Agent + # Lấy Agent agent = self.agent_graph.get_agent(agent_id) - # 创建Interview动作 + # Tạo hành động Interview interview_action = ManualAction( action_type=ActionType.INTERVIEW, action_args={"prompt": prompt} ) - # 执行Interview + # Thực thi Interview actions = {agent: interview_action} await self.env.step(actions) - # 从数据库获取结果 + # Lấy kết quả từ cơ sở dữ liệu result = self._get_interview_result(agent_id) self.send_response(command_id, "completed", result=result) - print(f" Interview完成: agent_id={agent_id}") + print(f" Interview completed: agent_id={agent_id}") return True except Exception as e: error_msg = str(e) - print(f" Interview失败: agent_id={agent_id}, error={error_msg}") + print(f" Interview failed: agent_id={agent_id}, error={error_msg}") self.send_response(command_id, "failed", error=error_msg) return False async def handle_batch_interview(self, command_id: str, interviews: List[Dict]) -> bool: """ - 处理批量采访命令 + Xử lý lệnh phỏng vấn hàng loạt Args: interviews: [{"agent_id": int, "prompt": str}, ...] """ try: - # 构建动作字典 + # Tạo dict hành động actions = {} - agent_prompts = {} # 记录每个agent的prompt + agent_prompts = {} # Ghi lại prompt của từng agent for interview in interviews: agent_id = interview.get("agent_id") @@ -269,16 +269,16 @@ async def handle_batch_interview(self, command_id: str, interviews: List[Dict]) ) agent_prompts[agent_id] = prompt except Exception as e: - print(f" 警告: 无法获取Agent {agent_id}: {e}") + print(f" Warning: cannot get Agent {agent_id}: {e}") if not actions: - self.send_response(command_id, "failed", error="没有有效的Agent") + self.send_response(command_id, "failed", error="No valid agents") return False - # 执行批量Interview + # Thực thi Interview hàng loạt await self.env.step(actions) - # 获取所有结果 + # Lấy toàn bộ kết quả results = {} for agent_id in agent_prompts.keys(): result = self._get_interview_result(agent_id) @@ -288,17 +288,17 @@ async def handle_batch_interview(self, command_id: str, interviews: List[Dict]) "interviews_count": len(results), "results": results }) - print(f" 批量Interview完成: {len(results)} 个Agent") + print(f" Batch interview completed: {len(results)} agents") return True except Exception as e: error_msg = str(e) - print(f" 批量Interview失败: {error_msg}") + print(f" Batch interview failed: {error_msg}") self.send_response(command_id, "failed", error=error_msg) return False def _get_interview_result(self, agent_id: int) -> Dict[str, Any]: - """从数据库获取最新的Interview结果""" + """Lấy kết quả Interview mới nhất từ cơ sở dữ liệu""" db_path = os.path.join(self.simulation_dir, "twitter_simulation.db") result = { @@ -314,7 +314,7 @@ def _get_interview_result(self, agent_id: int) -> Dict[str, Any]: conn = sqlite3.connect(db_path) cursor = conn.cursor() - # 查询最新的Interview记录 + # Truy vấn bản ghi Interview mới nhất cursor.execute(""" SELECT user_id, info, created_at FROM trace @@ -336,16 +336,16 @@ def _get_interview_result(self, agent_id: int) -> Dict[str, Any]: conn.close() except Exception as e: - print(f" 读取Interview结果失败: {e}") + print(f" Failed to read Interview result: {e}") return result async def process_commands(self) -> bool: """ - 处理所有待处理命令 + Xử lý toàn bộ lệnh đang chờ Returns: - True 表示继续运行,False 表示应该退出 + True là tiếp tục chạy, False là nên thoát """ command = self.poll_command() if not command: @@ -355,7 +355,7 @@ async def process_commands(self) -> bool: command_type = command.get("command_type") args = command.get("args", {}) - print(f"\n收到IPC命令: {command_type}, id={command_id}") + print(f"\nReceived IPC command: {command_type}, id={command_id}") if command_type == CommandType.INTERVIEW: await self.handle_interview( @@ -373,19 +373,19 @@ async def process_commands(self) -> bool: return True elif command_type == CommandType.CLOSE_ENV: - print("收到关闭环境命令") - self.send_response(command_id, "completed", result={"message": "环境即将关闭"}) + print("Received close environment command") + self.send_response(command_id, "completed", result={"message": "Environment will close soon"}) return False else: - self.send_response(command_id, "failed", error=f"未知命令类型: {command_type}") + self.send_response(command_id, "failed", error=f"Unknown command type: {command_type}") return True class TwitterSimulationRunner: - """Twitter模拟运行器""" + """Bộ chạy mô phỏng Twitter""" - # Twitter可用动作(不包含INTERVIEW,INTERVIEW只能通过ManualAction手动触发) + # Các hành động khả dụng cho Twitter (không bao gồm INTERVIEW; INTERVIEW chỉ kích hoạt thủ công qua ManualAction) AVAILABLE_ACTIONS = [ ActionType.CREATE_POST, ActionType.LIKE_POST, @@ -397,11 +397,11 @@ class TwitterSimulationRunner: def __init__(self, config_path: str, wait_for_commands: bool = True): """ - 初始化模拟运行器 + Khởi tạo bộ chạy mô phỏng Args: - config_path: 配置文件路径 (simulation_config.json) - wait_for_commands: 模拟完成后是否等待命令(默认True) + config_path: Đường dẫn file cấu hình (simulation_config.json) + wait_for_commands: Có chờ lệnh sau khi mô phỏng hoàn tất hay không (mặc định True) """ self.config_path = config_path self.config = self._load_config() @@ -412,47 +412,47 @@ def __init__(self, config_path: str, wait_for_commands: bool = True): self.ipc_handler = None def _load_config(self) -> Dict[str, Any]: - """加载配置文件""" + """Tải file cấu hình""" with open(self.config_path, 'r', encoding='utf-8') as f: return json.load(f) def _get_profile_path(self) -> str: - """获取Profile文件路径(OASIS Twitter使用CSV格式)""" + """Lấy đường dẫn file Profile (OASIS Twitter dùng định dạng CSV)""" return os.path.join(self.simulation_dir, "twitter_profiles.csv") def _get_db_path(self) -> str: - """获取数据库路径""" + """Lấy đường dẫn cơ sở dữ liệu""" return os.path.join(self.simulation_dir, "twitter_simulation.db") def _create_model(self): """ - 创建LLM模型 + Tạo mô hình LLM - 统一使用项目根目录 .env 文件中的配置(优先级最高): - - LLM_API_KEY: API密钥 - - LLM_BASE_URL: API基础URL - - LLM_MODEL_NAME: 模型名称 + Thống nhất dùng cấu hình trong file .env tại thư mục gốc dự án (ưu tiên cao nhất): + - LLM_API_KEY: API key + - LLM_BASE_URL: URL cơ sở API + - LLM_MODEL_NAME: Tên mô hình """ - # 优先从 .env 读取配置 + # Ưu tiên đọc cấu hình từ .env llm_api_key = os.environ.get("LLM_API_KEY", "") llm_base_url = os.environ.get("LLM_BASE_URL", "") llm_model = os.environ.get("LLM_MODEL_NAME", "") - # 如果 .env 中没有,则使用 config 作为备用 + # Nếu .env không có thì dùng config làm dự phòng if not llm_model: llm_model = self.config.get("llm_model", "gpt-4o-mini") - # 设置 camel-ai 所需的环境变量 + # Thiết lập biến môi trường cần cho camel-ai if llm_api_key: os.environ["OPENAI_API_KEY"] = llm_api_key if not os.environ.get("OPENAI_API_KEY"): - raise ValueError("缺少 API Key 配置,请在项目根目录 .env 文件中设置 LLM_API_KEY") + raise ValueError("Missing API key configuration. Please set LLM_API_KEY in the project root .env file") if llm_base_url: os.environ["OPENAI_API_BASE_URL"] = llm_base_url - print(f"LLM配置: model={llm_model}, base_url={llm_base_url[:40] if llm_base_url else '默认'}...") + print(f"LLM config: model={llm_model}, base_url={llm_base_url[:40] if llm_base_url else 'default'}...") return ModelFactory.create( model_platform=ModelPlatformType.OPENAI, @@ -466,24 +466,24 @@ def _get_active_agents_for_round( round_num: int ) -> List: """ - 根据时间和配置决定本轮激活哪些Agent + Quyết định Agent nào được kích hoạt trong vòng này dựa trên thời gian và cấu hình Args: - env: OASIS环境 - current_hour: 当前模拟小时(0-23) - round_num: 当前轮数 + env: Môi trường OASIS + current_hour: Giờ mô phỏng hiện tại (0-23) + round_num: Vòng hiện tại Returns: - 激活的Agent列表 + Danh sách Agent được kích hoạt """ time_config = self.config.get("time_config", {}) agent_configs = self.config.get("agent_configs", []) - # 基础激活数量 + # Số lượng kích hoạt cơ bản base_min = time_config.get("agents_per_hour_min", 5) base_max = time_config.get("agents_per_hour_max", 20) - # 根据时段调整 + # Điều chỉnh theo khung giờ peak_hours = time_config.get("peak_hours", [9, 10, 11, 14, 15, 20, 21, 22]) off_peak_hours = time_config.get("off_peak_hours", [0, 1, 2, 3, 4, 5]) @@ -496,28 +496,28 @@ def _get_active_agents_for_round( target_count = int(random.uniform(base_min, base_max) * multiplier) - # 根据每个Agent的配置计算激活概率 + # Tính xác suất kích hoạt theo cấu hình của từng Agent candidates = [] for cfg in agent_configs: agent_id = cfg.get("agent_id", 0) active_hours = cfg.get("active_hours", list(range(8, 23))) activity_level = cfg.get("activity_level", 0.5) - # 检查是否在活跃时间 + # Kiểm tra có thuộc giờ hoạt động hay không if current_hour not in active_hours: continue - # 根据活跃度计算概率 + # Tính xác suất theo mức độ hoạt động if random.random() < activity_level: candidates.append(agent_id) - # 随机选择 + # Chọn ngẫu nhiên selected_ids = random.sample( candidates, min(target_count, len(candidates)) ) if candidates else [] - # 转换为Agent对象 + # Chuyển thành đối tượng Agent active_agents = [] for agent_id in selected_ids: try: @@ -529,50 +529,50 @@ def _get_active_agents_for_round( return active_agents async def run(self, max_rounds: int = None): - """运行Twitter模拟 + """Chạy mô phỏng Twitter Args: - max_rounds: 最大模拟轮数(可选,用于截断过长的模拟) + max_rounds: Số vòng mô phỏng tối đa (tùy chọn, dùng để cắt bớt mô phỏng quá dài) """ print("=" * 60) - print("OASIS Twitter模拟") - print(f"配置文件: {self.config_path}") - print(f"模拟ID: {self.config.get('simulation_id', 'unknown')}") - print(f"等待命令模式: {'启用' if self.wait_for_commands else '禁用'}") + print("OASIS Twitter Simulation") + print(f"Config file: {self.config_path}") + print(f"Simulation ID: {self.config.get('simulation_id', 'unknown')}") + print(f"Wait mode: {'enabled' if self.wait_for_commands else 'disabled'}") print("=" * 60) - # 加载时间配置 + # Tải cấu hình thời gian time_config = self.config.get("time_config", {}) total_hours = time_config.get("total_simulation_hours", 72) minutes_per_round = time_config.get("minutes_per_round", 30) - # 计算总轮数 + # Tính tổng số vòng total_rounds = (total_hours * 60) // minutes_per_round - # 如果指定了最大轮数,则截断 + # Nếu chỉ định số vòng tối đa thì sẽ cắt bớt if max_rounds is not None and max_rounds > 0: original_rounds = total_rounds total_rounds = min(total_rounds, max_rounds) if total_rounds < original_rounds: - print(f"\n轮数已截断: {original_rounds} -> {total_rounds} (max_rounds={max_rounds})") + print(f"\nRounds truncated: {original_rounds} -> {total_rounds} (max_rounds={max_rounds})") - print(f"\n模拟参数:") - print(f" - 总模拟时长: {total_hours}小时") - print(f" - 每轮时间: {minutes_per_round}分钟") - print(f" - 总轮数: {total_rounds}") + print(f"\nSimulation parameters:") + print(f" - Total simulation duration: {total_hours} hours") + print(f" - Minutes per round: {minutes_per_round} min") + print(f" - Total rounds: {total_rounds}") if max_rounds: - print(f" - 最大轮数限制: {max_rounds}") - print(f" - Agent数量: {len(self.config.get('agent_configs', []))}") + print(f" - Max round limit: {max_rounds}") + print(f" - Agent count: {len(self.config.get('agent_configs', []))}") - # 创建模型 - print("\n初始化LLM模型...") + # Tạo mô hình + print("\nInitializing LLM model...") model = self._create_model() - # 加载Agent图 - print("加载Agent Profile...") + # Tải đồ thị Agent + print("Loading Agent Profile...") profile_path = self._get_profile_path() if not os.path.exists(profile_path): - print(f"错误: Profile文件不存在: {profile_path}") + print(f"Error: Profile file does not exist: {profile_path}") return self.agent_graph = await generate_twitter_agent_graph( @@ -581,34 +581,34 @@ async def run(self, max_rounds: int = None): available_actions=self.AVAILABLE_ACTIONS, ) - # 数据库路径 + # Đường dẫn cơ sở dữ liệu db_path = self._get_db_path() if os.path.exists(db_path): os.remove(db_path) - print(f"已删除旧数据库: {db_path}") + print(f"Removed old database: {db_path}") - # 创建环境 - print("创建OASIS环境...") + # Tạo môi trường + print("Creating OASIS environment...") self.env = oasis.make( agent_graph=self.agent_graph, platform=oasis.DefaultPlatformType.TWITTER, database_path=db_path, - semaphore=30, # 限制最大并发 LLM 请求数,防止 API 过载 + semaphore=30, # Giới hạn số request LLM đồng thời tối đa để tránh quá tải API ) await self.env.reset() - print("环境初始化完成\n") + print("Environment initialization completed\n") - # 初始化IPC处理器 + # Khởi tạo bộ xử lý IPC self.ipc_handler = IPCHandler(self.simulation_dir, self.env, self.agent_graph) self.ipc_handler.update_status("running") - # 执行初始事件 + # Thực thi sự kiện ban đầu event_config = self.config.get("event_config", {}) initial_posts = event_config.get("initial_posts", []) if initial_posts: - print(f"执行初始事件 ({len(initial_posts)}条初始帖子)...") + print(f"Executing initial events ({len(initial_posts)} initial posts)...") initial_actions = {} for post in initial_posts: agent_id = post.get("poster_agent_id", 0) @@ -620,23 +620,23 @@ async def run(self, max_rounds: int = None): action_args={"content": content} ) except Exception as e: - print(f" 警告: 无法为Agent {agent_id}创建初始帖子: {e}") + print(f" Warning: cannot create initial post for Agent {agent_id}: {e}") if initial_actions: await self.env.step(initial_actions) - print(f" 已发布 {len(initial_actions)} 条初始帖子") + print(f" Published {len(initial_actions)} initial posts") - # 主模拟循环 - print("\n开始模拟循环...") + # Vòng lặp mô phỏng chính + print("\nStarting simulation loop...") start_time = datetime.now() for round_num in range(total_rounds): - # 计算当前模拟时间 + # Tính thời gian mô phỏng hiện tại simulated_minutes = round_num * minutes_per_round simulated_hour = (simulated_minutes // 60) % 24 simulated_day = simulated_minutes // (60 * 24) + 1 - # 获取本轮激活的Agent + # Lấy các Agent được kích hoạt trong vòng này active_agents = self._get_active_agents_for_round( self.env, simulated_hour, round_num ) @@ -644,16 +644,16 @@ async def run(self, max_rounds: int = None): if not active_agents: continue - # 构建动作 + # Tạo hành động actions = { agent: LLMAction() for _, agent in active_agents } - # 执行动作 + # Thực thi hành động await self.env.step(actions) - # 打印进度 + # In tiến độ if (round_num + 1) % 10 == 0 or round_num == 0: elapsed = (datetime.now() - start_time).total_seconds() progress = (round_num + 1) / total_rounds * 100 @@ -663,20 +663,20 @@ async def run(self, max_rounds: int = None): f"- elapsed: {elapsed:.1f}s") total_elapsed = (datetime.now() - start_time).total_seconds() - print(f"\n模拟循环完成!") - print(f" - 总耗时: {total_elapsed:.1f}秒") - print(f" - 数据库: {db_path}") + print(f"\nSimulation loop completed!") + print(f" - Total elapsed: {total_elapsed:.1f}s") + print(f" - Database: {db_path}") - # 是否进入等待命令模式 + # Có vào chế độ chờ lệnh hay không if self.wait_for_commands: print("\n" + "=" * 60) - print("进入等待命令模式 - 环境保持运行") - print("支持的命令: interview, batch_interview, close_env") + print("Entering wait mode - environment remains running") + print("Supported commands: interview, batch_interview, close_env") print("=" * 60) self.ipc_handler.update_status("alive") - # 等待命令循环(使用全局 _shutdown_event) + # Vòng lặp chờ lệnh (dùng _shutdown_event toàn cục) try: while not _shutdown_event.is_set(): should_continue = await self.ipc_handler.process_commands() @@ -684,58 +684,58 @@ async def run(self, max_rounds: int = None): break try: await asyncio.wait_for(_shutdown_event.wait(), timeout=0.5) - break # 收到退出信号 + break # Nhận tín hiệu thoát except asyncio.TimeoutError: pass except KeyboardInterrupt: - print("\n收到中断信号") + print("\nReceived interrupt signal") except asyncio.CancelledError: - print("\n任务被取消") + print("\nTask was cancelled") except Exception as e: - print(f"\n命令处理出错: {e}") + print(f"\nCommand processing error: {e}") - print("\n关闭环境...") + print("\nClosing environment...") - # 关闭环境 + # Đóng môi trường self.ipc_handler.update_status("stopped") await self.env.close() - print("环境已关闭") + print("Environment closed") print("=" * 60) async def main(): - parser = argparse.ArgumentParser(description='OASIS Twitter模拟') + parser = argparse.ArgumentParser(description='OASIS Twitter Simulation') parser.add_argument( '--config', type=str, required=True, - help='配置文件路径 (simulation_config.json)' + help='Config file path (simulation_config.json)' ) parser.add_argument( '--max-rounds', type=int, default=None, - help='最大模拟轮数(可选,用于截断过长的模拟)' + help='Max simulation rounds (optional, to truncate overly long simulations)' ) parser.add_argument( '--no-wait', action='store_true', default=False, - help='模拟完成后立即关闭环境,不进入等待命令模式' + help='Close environment immediately after simulation, do not enter wait mode' ) args = parser.parse_args() - # 在 main 函数开始时创建 shutdown 事件 + # Tạo sự kiện shutdown ở đầu hàm main global _shutdown_event _shutdown_event = asyncio.Event() if not os.path.exists(args.config): - print(f"错误: 配置文件不存在: {args.config}") + print(f"Error: config file does not exist: {args.config}") sys.exit(1) - # 初始化日志配置(使用固定文件名,清理旧日志) + # Khởi tạo cấu hình log (dùng tên file cố định, dọn log cũ) simulation_dir = os.path.dirname(args.config) or "." setup_oasis_logging(os.path.join(simulation_dir, "log")) @@ -748,20 +748,20 @@ async def main(): def setup_signal_handlers(): """ - 设置信号处理器,确保收到 SIGTERM/SIGINT 时能够正确退出 - 让程序有机会正常清理资源(关闭数据库、环境等) + Cài đặt bộ xử lý tín hiệu, bảo đảm thoát đúng khi nhận SIGTERM/SIGINT. + Giúp chương trình có cơ hội dọn tài nguyên đúng cách (đóng cơ sở dữ liệu, môi trường...). """ def signal_handler(signum, frame): global _cleanup_done sig_name = "SIGTERM" if signum == signal.SIGTERM else "SIGINT" - print(f"\n收到 {sig_name} 信号,正在退出...") + print(f"\nReceived {sig_name}, shutting down...") if not _cleanup_done: _cleanup_done = True if _shutdown_event: _shutdown_event.set() else: - # 重复收到信号才强制退出 - print("强制退出...") + # Chỉ buộc thoát khi nhận tín hiệu lặp lại + print("Force exit...") sys.exit(1) signal.signal(signal.SIGTERM, signal_handler) @@ -773,8 +773,8 @@ def signal_handler(signum, frame): try: asyncio.run(main()) except KeyboardInterrupt: - print("\n程序被中断") + print("\nProgram interrupted") except SystemExit: pass finally: - print("模拟进程已退出") + print("Simulation process exited") diff --git a/backend/scripts/test_profile_format.py b/backend/scripts/test_profile_format.py index 354e8b5ca..a2029b21c 100644 --- a/backend/scripts/test_profile_format.py +++ b/backend/scripts/test_profile_format.py @@ -1,8 +1,8 @@ """ -测试Profile格式生成是否符合OASIS要求 -验证: -1. Twitter Profile生成CSV格式 -2. Reddit Profile生成JSON详细格式 +Kiểm tra việc sinh định dạng Profile có đúng yêu cầu OASIS hay không. +Xác thực: +1. Twitter Profile sinh ở định dạng CSV. +2. Reddit Profile sinh ở định dạng JSON chi tiết. """ import os @@ -11,19 +11,19 @@ import csv import tempfile -# 添加项目路径 +# Thêm đường dẫn dự án sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from app.services.oasis_profile_generator import OasisProfileGenerator, OasisAgentProfile def test_profile_formats(): - """测试Profile格式""" + """Kiểm tra định dạng Profile""" print("=" * 60) - print("OASIS Profile格式测试") + print("Kiểm tra định dạng OASIS Profile") print("=" * 60) - # 创建测试Profile数据 + # Tạo dữ liệu Profile kiểm thử test_profiles = [ OasisAgentProfile( user_id=0, @@ -63,84 +63,84 @@ def test_profile_formats(): generator = OasisProfileGenerator.__new__(OasisProfileGenerator) - # 使用临时目录 + # Dùng thư mục tạm with tempfile.TemporaryDirectory() as temp_dir: twitter_path = os.path.join(temp_dir, "twitter_profiles.csv") reddit_path = os.path.join(temp_dir, "reddit_profiles.json") - # 测试Twitter CSV格式 - print("\n1. 测试Twitter Profile (CSV格式)") + # Kiểm tra định dạng Twitter CSV + print("\n1. Kiểm tra Twitter Profile (định dạng CSV)") print("-" * 40) generator._save_twitter_csv(test_profiles, twitter_path) - # 读取并验证CSV + # Đọc và xác thực CSV with open(twitter_path, 'r', encoding='utf-8') as f: reader = csv.DictReader(f) rows = list(reader) - print(f" 文件: {twitter_path}") - print(f" 行数: {len(rows)}") - print(f" 表头: {list(rows[0].keys())}") - print(f"\n 示例数据 (第1行):") + print(f" File: {twitter_path}") + print(f" Rows: {len(rows)}") + print(f" Headers: {list(rows[0].keys())}") + print(f"\n Sample data (row 1):") for key, value in rows[0].items(): print(f" {key}: {value}") - # 验证必需字段 + # Xác thực các trường bắt buộc required_twitter_fields = ['user_id', 'user_name', 'name', 'bio', 'friend_count', 'follower_count', 'statuses_count', 'created_at'] missing = set(required_twitter_fields) - set(rows[0].keys()) if missing: - print(f"\n [错误] 缺少字段: {missing}") + print(f"\n [ERROR] Missing fields: {missing}") else: - print(f"\n [通过] 所有必需字段都存在") + print(f"\n [PASS] All required fields are present") - # 测试Reddit JSON格式 - print("\n2. 测试Reddit Profile (JSON详细格式)") + # Kiểm tra định dạng Reddit JSON + print("\n2. Kiểm tra Reddit Profile (định dạng JSON chi tiết)") print("-" * 40) generator._save_reddit_json(test_profiles, reddit_path) - # 读取并验证JSON + # Đọc và xác thực JSON with open(reddit_path, 'r', encoding='utf-8') as f: reddit_data = json.load(f) - print(f" 文件: {reddit_path}") - print(f" 条目数: {len(reddit_data)}") - print(f" 字段: {list(reddit_data[0].keys())}") - print(f"\n 示例数据 (第1条):") + print(f" File: {reddit_path}") + print(f" Entries: {len(reddit_data)}") + print(f" Fields: {list(reddit_data[0].keys())}") + print(f"\n Sample data (entry 1):") print(json.dumps(reddit_data[0], ensure_ascii=False, indent=4)) - # 验证详细格式字段 + # Xác thực các trường của định dạng chi tiết required_reddit_fields = ['realname', 'username', 'bio', 'persona'] optional_reddit_fields = ['age', 'gender', 'mbti', 'country', 'profession', 'interested_topics'] missing = set(required_reddit_fields) - set(reddit_data[0].keys()) if missing: - print(f"\n [错误] 缺少必需字段: {missing}") + print(f"\n [ERROR] Missing required fields: {missing}") else: - print(f"\n [通过] 所有必需字段都存在") + print(f"\n [PASS] All required fields are present") present_optional = set(optional_reddit_fields) & set(reddit_data[0].keys()) - print(f" [信息] 可选字段: {present_optional}") + print(f" [INFO] Optional fields: {present_optional}") print("\n" + "=" * 60) - print("测试完成!") + print("Test completed!") print("=" * 60) def show_expected_formats(): - """显示OASIS期望的格式""" + """Hiển thị định dạng OASIS mong đợi""" print("\n" + "=" * 60) - print("OASIS 期望的Profile格式参考") + print("Tham chiếu định dạng Profile OASIS mong đợi") print("=" * 60) - print("\n1. Twitter Profile (CSV格式)") + print("\n1. Twitter Profile (định dạng CSV)") print("-" * 40) twitter_example = """user_id,user_name,name,bio,friend_count,follower_count,statuses_count,created_at 0,user0,User Zero,I am user zero with interests in technology.,100,150,500,2023-01-01 1,user1,User One,Tech enthusiast and coffee lover.,200,250,1000,2023-01-02""" print(twitter_example) - print("\n2. Reddit Profile (JSON详细格式)") + print("\n2. Reddit Profile (định dạng JSON chi tiết)") print("-" * 40) reddit_example = [ { diff --git a/docker-compose.yml b/docker-compose.yml index 637f1dfae..8d6ed7548 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ services: mirofish: image: ghcr.io/666ghj/mirofish:latest - # 加速镜像(如拉取缓慢可替换上方地址) + # Mirror tăng tốc (nếu kéo image chậm có thể thay địa chỉ ở trên) # image: ghcr.nju.edu.cn/666ghj/mirofish:latest container_name: mirofish env_file: diff --git a/frontend/index.html b/frontend/index.html index 009c924a4..300347dc5 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -7,8 +7,8 @@ - - MiroFish - 预测万物 + + MiroFish - Predicting everything
diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 8c4fa710d..5ed2431f0 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -37,12 +37,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", "license": "MIT", "dependencies": { - "@babel/types": "^7.28.5" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -52,9 +52,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -65,9 +65,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", "cpu": [ "ppc64" ], @@ -82,9 +82,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", "cpu": [ "arm" ], @@ -99,9 +99,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", "cpu": [ "arm64" ], @@ -116,9 +116,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", "cpu": [ "x64" ], @@ -133,9 +133,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", "cpu": [ "arm64" ], @@ -150,9 +150,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", "cpu": [ "x64" ], @@ -167,9 +167,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", "cpu": [ "arm64" ], @@ -184,9 +184,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", "cpu": [ "x64" ], @@ -201,9 +201,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", "cpu": [ "arm" ], @@ -218,9 +218,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", "cpu": [ "arm64" ], @@ -235,9 +235,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", "cpu": [ "ia32" ], @@ -252,9 +252,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", "cpu": [ "loong64" ], @@ -269,9 +269,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", "cpu": [ "mips64el" ], @@ -286,9 +286,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", "cpu": [ "ppc64" ], @@ -303,9 +303,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", "cpu": [ "riscv64" ], @@ -320,9 +320,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", "cpu": [ "s390x" ], @@ -337,9 +337,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", "cpu": [ "x64" ], @@ -354,9 +354,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", "cpu": [ "arm64" ], @@ -371,9 +371,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", "cpu": [ "x64" ], @@ -388,9 +388,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", "cpu": [ "arm64" ], @@ -405,9 +405,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", "cpu": [ "x64" ], @@ -422,9 +422,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", "cpu": [ "arm64" ], @@ -439,9 +439,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", "cpu": [ "x64" ], @@ -456,9 +456,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", "cpu": [ "arm64" ], @@ -473,9 +473,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", "cpu": [ "ia32" ], @@ -490,9 +490,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", "cpu": [ "x64" ], @@ -513,16 +513,16 @@ "license": "MIT" }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.50", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.50.tgz", - "integrity": "sha512-5e76wQiQVeL1ICOZVUg4LSOVYg9jyhGCin+icYozhsUzM+fHE7kddi1bdiE0jwVqTfkjba3jUFbEkoC9WkdvyA==", + "version": "1.0.0-rc.2", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz", + "integrity": "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==", "dev": true, "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", - "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.0.tgz", + "integrity": "sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==", "cpu": [ "arm" ], @@ -534,9 +534,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", - "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.0.tgz", + "integrity": "sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==", "cpu": [ "arm64" ], @@ -548,9 +548,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", - "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.0.tgz", + "integrity": "sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==", "cpu": [ "arm64" ], @@ -562,9 +562,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", - "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.0.tgz", + "integrity": "sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==", "cpu": [ "x64" ], @@ -576,9 +576,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", - "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.0.tgz", + "integrity": "sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==", "cpu": [ "arm64" ], @@ -590,9 +590,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", - "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.0.tgz", + "integrity": "sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==", "cpu": [ "x64" ], @@ -604,9 +604,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", - "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.0.tgz", + "integrity": "sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==", "cpu": [ "arm" ], @@ -618,9 +618,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", - "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.0.tgz", + "integrity": "sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==", "cpu": [ "arm" ], @@ -632,9 +632,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", - "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.0.tgz", + "integrity": "sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==", "cpu": [ "arm64" ], @@ -646,9 +646,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", - "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.0.tgz", + "integrity": "sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==", "cpu": [ "arm64" ], @@ -660,9 +660,23 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", - "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.0.tgz", + "integrity": "sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.0.tgz", + "integrity": "sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==", "cpu": [ "loong64" ], @@ -674,9 +688,23 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", - "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.0.tgz", + "integrity": "sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.0.tgz", + "integrity": "sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==", "cpu": [ "ppc64" ], @@ -688,9 +716,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", - "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.0.tgz", + "integrity": "sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==", "cpu": [ "riscv64" ], @@ -702,9 +730,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", - "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.0.tgz", + "integrity": "sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==", "cpu": [ "riscv64" ], @@ -716,9 +744,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", - "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.0.tgz", + "integrity": "sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==", "cpu": [ "s390x" ], @@ -730,9 +758,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", - "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.0.tgz", + "integrity": "sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==", "cpu": [ "x64" ], @@ -744,9 +772,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", - "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.0.tgz", + "integrity": "sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==", "cpu": [ "x64" ], @@ -757,10 +785,24 @@ "linux" ] }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.0.tgz", + "integrity": "sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", - "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.0.tgz", + "integrity": "sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==", "cpu": [ "arm64" ], @@ -772,9 +814,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", - "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.0.tgz", + "integrity": "sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==", "cpu": [ "arm64" ], @@ -786,9 +828,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", - "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.0.tgz", + "integrity": "sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==", "cpu": [ "ia32" ], @@ -800,9 +842,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", - "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.0.tgz", + "integrity": "sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==", "cpu": [ "x64" ], @@ -814,9 +856,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", - "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.0.tgz", + "integrity": "sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==", "cpu": [ "x64" ], @@ -835,70 +877,70 @@ "license": "MIT" }, "node_modules/@vitejs/plugin-vue": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.2.tgz", - "integrity": "sha512-iHmwV3QcVGGvSC1BG5bZ4z6iwa1SOpAPWmnjOErd4Ske+lZua5K9TtAVdx0gMBClJ28DViCbSmZitjWZsWO3LA==", + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.5.tgz", + "integrity": "sha512-bL3AxKuQySfk1iGcBsQnoRVexTPJq0Z/ixFVM8OhVJAP6ZXXXLtM7NFKWhLl30Kg7uTBqIaPXbh+nuQCuBDedg==", "dev": true, "license": "MIT", "dependencies": { - "@rolldown/pluginutils": "1.0.0-beta.50" + "@rolldown/pluginutils": "1.0.0-rc.2" }, "engines": { "node": "^20.19.0 || >=22.12.0" }, "peerDependencies": { - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", "vue": "^3.2.25" } }, "node_modules/@vue/compiler-core": { - "version": "3.5.25", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.25.tgz", - "integrity": "sha512-vay5/oQJdsNHmliWoZfHPoVZZRmnSWhug0BYT34njkYTPqClh3DNWLkZNJBVSjsNMrg0CCrBfoKkjZQPM/QVUw==", + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.30.tgz", + "integrity": "sha512-s3DfdZkcu/qExZ+td75015ljzHc6vE+30cFMGRPROYjqkroYI5NV2X1yAMX9UeyBNWB9MxCfPcsjpLS11nzkkw==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.5", - "@vue/shared": "3.5.25", - "entities": "^4.5.0", + "@babel/parser": "^7.29.0", + "@vue/shared": "3.5.30", + "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-dom": { - "version": "3.5.25", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.25.tgz", - "integrity": "sha512-4We0OAcMZsKgYoGlMjzYvaoErltdFI2/25wqanuTu+S4gismOTRTBPi4IASOjxWdzIwrYSjnqONfKvuqkXzE2Q==", + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.30.tgz", + "integrity": "sha512-eCFYESUEVYHhiMuK4SQTldO3RYxyMR/UQL4KdGD1Yrkfdx4m/HYuZ9jSfPdA+nWJY34VWndiYdW/wZXyiPEB9g==", "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.25", - "@vue/shared": "3.5.25" + "@vue/compiler-core": "3.5.30", + "@vue/shared": "3.5.30" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.25", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.25.tgz", - "integrity": "sha512-PUgKp2rn8fFsI++lF2sO7gwO2d9Yj57Utr5yEsDf3GNaQcowCLKL7sf+LvVFvtJDXUp/03+dC6f2+LCv5aK1ag==", + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.30.tgz", + "integrity": "sha512-LqmFPDn89dtU9vI3wHJnwaV6GfTRD87AjWpTWpyrdVOObVtjIuSeZr181z5C4PmVx/V3j2p+0f7edFKGRMpQ5A==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.5", - "@vue/compiler-core": "3.5.25", - "@vue/compiler-dom": "3.5.25", - "@vue/compiler-ssr": "3.5.25", - "@vue/shared": "3.5.25", + "@babel/parser": "^7.29.0", + "@vue/compiler-core": "3.5.30", + "@vue/compiler-dom": "3.5.30", + "@vue/compiler-ssr": "3.5.30", + "@vue/shared": "3.5.30", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", - "postcss": "^8.5.6", + "postcss": "^8.5.8", "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.25", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.25.tgz", - "integrity": "sha512-ritPSKLBcParnsKYi+GNtbdbrIE1mtuFEJ4U1sWeuOMlIziK5GtOL85t5RhsNy4uWIXPgk+OUdpnXiTdzn8o3A==", + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.30.tgz", + "integrity": "sha512-NsYK6OMTnx109PSL2IAyf62JP6EUdk4Dmj6AkWcJGBvN0dQoMYtVekAmdqgTtWQgEJo+Okstbf/1p7qZr5H+bA==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.25", - "@vue/shared": "3.5.25" + "@vue/compiler-dom": "3.5.30", + "@vue/shared": "3.5.30" } }, "node_modules/@vue/devtools-api": { @@ -908,53 +950,53 @@ "license": "MIT" }, "node_modules/@vue/reactivity": { - "version": "3.5.25", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.25.tgz", - "integrity": "sha512-5xfAypCQepv4Jog1U4zn8cZIcbKKFka3AgWHEFQeK65OW+Ys4XybP6z2kKgws4YB43KGpqp5D/K3go2UPPunLA==", + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.30.tgz", + "integrity": "sha512-179YNgKATuwj9gB+66snskRDOitDiuOZqkYia7mHKJaidOMo/WJxHKF8DuGc4V4XbYTJANlfEKb0yxTQotnx4Q==", "license": "MIT", "dependencies": { - "@vue/shared": "3.5.25" + "@vue/shared": "3.5.30" } }, "node_modules/@vue/runtime-core": { - "version": "3.5.25", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.25.tgz", - "integrity": "sha512-Z751v203YWwYzy460bzsYQISDfPjHTl+6Zzwo/a3CsAf+0ccEjQ8c+0CdX1WsumRTHeywvyUFtW6KvNukT/smA==", + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.30.tgz", + "integrity": "sha512-e0Z+8PQsUTdwV8TtEsLzUM7SzC7lQwYKePydb7K2ZnmS6jjND+WJXkmmfh/swYzRyfP1EY3fpdesyYoymCzYfg==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.25", - "@vue/shared": "3.5.25" + "@vue/reactivity": "3.5.30", + "@vue/shared": "3.5.30" } }, "node_modules/@vue/runtime-dom": { - "version": "3.5.25", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.25.tgz", - "integrity": "sha512-a4WrkYFbb19i9pjkz38zJBg8wa/rboNERq3+hRRb0dHiJh13c+6kAbgqCPfMaJ2gg4weWD3APZswASOfmKwamA==", + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.30.tgz", + "integrity": "sha512-2UIGakjU4WSQ0T4iwDEW0W7vQj6n7AFn7taqZ9Cvm0Q/RA2FFOziLESrDL4GmtI1wV3jXg5nMoJSYO66egDUBw==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.25", - "@vue/runtime-core": "3.5.25", - "@vue/shared": "3.5.25", - "csstype": "^3.1.3" + "@vue/reactivity": "3.5.30", + "@vue/runtime-core": "3.5.30", + "@vue/shared": "3.5.30", + "csstype": "^3.2.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.5.25", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.25.tgz", - "integrity": "sha512-UJaXR54vMG61i8XNIzTSf2Q7MOqZHpp8+x3XLGtE3+fL+nQd+k7O5+X3D/uWrnQXOdMw5VPih+Uremcw+u1woQ==", + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.30.tgz", + "integrity": "sha512-v+R34icapydRwbZRD0sXwtHqrQJv38JuMB4JxbOxd8NEpGLny7cncMp53W9UH/zo4j8eDHjQ1dEJXwzFQknjtQ==", "license": "MIT", "dependencies": { - "@vue/compiler-ssr": "3.5.25", - "@vue/shared": "3.5.25" + "@vue/compiler-ssr": "3.5.30", + "@vue/shared": "3.5.30" }, "peerDependencies": { - "vue": "3.5.25" + "vue": "3.5.30" } }, "node_modules/@vue/shared": { - "version": "3.5.25", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.25.tgz", - "integrity": "sha512-AbOPdQQnAnzs58H2FrrDxYj/TJfmeS2jdfEEhgiKINy+bnOANmVizIEgq1r+C5zsbs6l1CCQxtcj71rwNQ4jWg==", + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.30.tgz", + "integrity": "sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ==", "license": "MIT" }, "node_modules/asynckit": { @@ -964,13 +1006,13 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", - "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, @@ -1220,9 +1262,9 @@ } }, "node_modules/d3-format": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", - "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", "license": "ISC", "engines": { "node": ">=12" @@ -1331,7 +1373,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" } @@ -1417,9 +1458,9 @@ } }, "node_modules/delaunator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", - "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", + "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", "license": "ISC", "dependencies": { "robust-predicates": "^3.0.2" @@ -1449,9 +1490,9 @@ } }, "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -1506,9 +1547,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", + "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1519,32 +1560,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" } }, "node_modules/estree-walker": { @@ -1804,12 +1845,11 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -1818,9 +1858,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", "funding": [ { "type": "opencollective", @@ -1852,15 +1892,15 @@ "license": "MIT" }, "node_modules/robust-predicates": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", - "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", + "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", "license": "Unlicense" }, "node_modules/rollup": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", - "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.0.tgz", + "integrity": "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1874,28 +1914,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.53.3", - "@rollup/rollup-android-arm64": "4.53.3", - "@rollup/rollup-darwin-arm64": "4.53.3", - "@rollup/rollup-darwin-x64": "4.53.3", - "@rollup/rollup-freebsd-arm64": "4.53.3", - "@rollup/rollup-freebsd-x64": "4.53.3", - "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", - "@rollup/rollup-linux-arm-musleabihf": "4.53.3", - "@rollup/rollup-linux-arm64-gnu": "4.53.3", - "@rollup/rollup-linux-arm64-musl": "4.53.3", - "@rollup/rollup-linux-loong64-gnu": "4.53.3", - "@rollup/rollup-linux-ppc64-gnu": "4.53.3", - "@rollup/rollup-linux-riscv64-gnu": "4.53.3", - "@rollup/rollup-linux-riscv64-musl": "4.53.3", - "@rollup/rollup-linux-s390x-gnu": "4.53.3", - "@rollup/rollup-linux-x64-gnu": "4.53.3", - "@rollup/rollup-linux-x64-musl": "4.53.3", - "@rollup/rollup-openharmony-arm64": "4.53.3", - "@rollup/rollup-win32-arm64-msvc": "4.53.3", - "@rollup/rollup-win32-ia32-msvc": "4.53.3", - "@rollup/rollup-win32-x64-gnu": "4.53.3", - "@rollup/rollup-win32-x64-msvc": "4.53.3", + "@rollup/rollup-android-arm-eabi": "4.60.0", + "@rollup/rollup-android-arm64": "4.60.0", + "@rollup/rollup-darwin-arm64": "4.60.0", + "@rollup/rollup-darwin-x64": "4.60.0", + "@rollup/rollup-freebsd-arm64": "4.60.0", + "@rollup/rollup-freebsd-x64": "4.60.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.0", + "@rollup/rollup-linux-arm-musleabihf": "4.60.0", + "@rollup/rollup-linux-arm64-gnu": "4.60.0", + "@rollup/rollup-linux-arm64-musl": "4.60.0", + "@rollup/rollup-linux-loong64-gnu": "4.60.0", + "@rollup/rollup-linux-loong64-musl": "4.60.0", + "@rollup/rollup-linux-ppc64-gnu": "4.60.0", + "@rollup/rollup-linux-ppc64-musl": "4.60.0", + "@rollup/rollup-linux-riscv64-gnu": "4.60.0", + "@rollup/rollup-linux-riscv64-musl": "4.60.0", + "@rollup/rollup-linux-s390x-gnu": "4.60.0", + "@rollup/rollup-linux-x64-gnu": "4.60.0", + "@rollup/rollup-linux-x64-musl": "4.60.0", + "@rollup/rollup-openbsd-x64": "4.60.0", + "@rollup/rollup-openharmony-arm64": "4.60.0", + "@rollup/rollup-win32-arm64-msvc": "4.60.0", + "@rollup/rollup-win32-ia32-msvc": "4.60.0", + "@rollup/rollup-win32-x64-gnu": "4.60.0", + "@rollup/rollup-win32-x64-msvc": "4.60.0", "fsevents": "~2.3.2" } }, @@ -1938,14 +1981,13 @@ } }, "node_modules/vite": { - "version": "7.2.7", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.7.tgz", - "integrity": "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "esbuild": "^0.25.0", + "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", @@ -2014,17 +2056,16 @@ } }, "node_modules/vue": { - "version": "3.5.25", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.25.tgz", - "integrity": "sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==", + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.30.tgz", + "integrity": "sha512-hTHLc6VNZyzzEH/l7PFGjpcTvUgiaPK5mdLkbjrTeWSRcEfxFrv56g/XckIYlE9ckuobsdwqd5mk2g1sBkMewg==", "license": "MIT", - "peer": true, "dependencies": { - "@vue/compiler-dom": "3.5.25", - "@vue/compiler-sfc": "3.5.25", - "@vue/runtime-dom": "3.5.25", - "@vue/server-renderer": "3.5.25", - "@vue/shared": "3.5.25" + "@vue/compiler-dom": "3.5.30", + "@vue/compiler-sfc": "3.5.30", + "@vue/runtime-dom": "3.5.30", + "@vue/server-renderer": "3.5.30", + "@vue/shared": "3.5.30" }, "peerDependencies": { "typescript": "*" @@ -2036,9 +2077,9 @@ } }, "node_modules/vue-router": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.3.tgz", - "integrity": "sha512-ARBedLm9YlbvQomnmq91Os7ck6efydTSpRP3nuOKCvgJOHNrhRoJDSKtee8kcL1Vf7nz6U+PMBL+hTvR3bTVQg==", + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", "license": "MIT", "dependencies": { "@vue/devtools-api": "^6.6.4" diff --git a/frontend/src/App.vue b/frontend/src/App.vue index b7cd71ca6..e7279753c 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -3,11 +3,11 @@