translate backend folder

This commit is contained in:
TQuynh109 2026-03-25 03:42:26 +00:00
parent 950e8a80cb
commit 1b986f730d
33 changed files with 4364 additions and 4434 deletions

View File

@ -1,12 +1,12 @@
""" """
MiroFish Backend - Flask应用工厂 MiroFish Backend - Flask application factory
""" """
import os import os
import warnings 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.*") warnings.filterwarnings("ignore", message=".*resource_tracker.*")
from flask import Flask, request from flask import Flask, request
@ -17,64 +17,64 @@ from .utils.logger import setup_logger, get_logger
def create_app(config_class=Config): def create_app(config_class=Config):
"""Flask应用工厂函数""" """Hàm factory để khởi tạo Flask app"""
app = Flask(__name__) app = Flask(__name__)
app.config.from_object(config_class) app.config.from_object(config_class)
# 设置JSON编码:确保中文直接显示(而不是 \uXXXX 格式) # 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 使用 app.json.ensure_ascii,旧版本使用 JSON_AS_ASCII 配置 # 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'): if hasattr(app, 'json') and hasattr(app.json, 'ensure_ascii'):
app.json.ensure_ascii = False app.json.ensure_ascii = False
# 设置日志 # Thiết lập logger
logger = setup_logger('mirofish') 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' is_reloader_process = os.environ.get('WERKZEUG_RUN_MAIN') == 'true'
debug_mode = app.config.get('DEBUG', False) debug_mode = app.config.get('DEBUG', False)
should_log_startup = not debug_mode or is_reloader_process should_log_startup = not debug_mode or is_reloader_process
if should_log_startup: if should_log_startup:
logger.info("=" * 50) logger.info("=" * 50)
logger.info("MiroFish Backend 启动中...") logger.info("MiroFish Backend is starting...")
logger.info("=" * 50) logger.info("=" * 50)
# 启用CORS # Bật CORS
CORS(app, resources={r"/api/*": {"origins": "*"}}) 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 from .services.simulation_runner import SimulationRunner
SimulationRunner.register_cleanup() SimulationRunner.register_cleanup()
if should_log_startup: if should_log_startup:
logger.info("已注册模拟进程清理函数") logger.info("Simulation process cleanup hook registered")
# 请求日志中间件 # Middleware log request
@app.before_request @app.before_request
def log_request(): def log_request():
logger = get_logger('mirofish.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: 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 @app.after_request
def log_response(response): def log_response(response):
logger = get_logger('mirofish.request') logger = get_logger('mirofish.request')
logger.debug(f"响应: {response.status_code}") logger.debug(f"Response: {response.status_code}")
return response return response
# 注册蓝图 # Đăng ký blueprint
from .api import graph_bp, simulation_bp, report_bp from .api import graph_bp, simulation_bp, report_bp
app.register_blueprint(graph_bp, url_prefix='/api/graph') app.register_blueprint(graph_bp, url_prefix='/api/graph')
app.register_blueprint(simulation_bp, url_prefix='/api/simulation') app.register_blueprint(simulation_bp, url_prefix='/api/simulation')
app.register_blueprint(report_bp, url_prefix='/api/report') app.register_blueprint(report_bp, url_prefix='/api/report')
# 健康检查 # Health check
@app.route('/health') @app.route('/health')
def health(): def health():
return {'status': 'ok', 'service': 'MiroFish Backend'} return {'status': 'ok', 'service': 'MiroFish Backend'}
if should_log_startup: if should_log_startup:
logger.info("MiroFish Backend 启动完成") logger.info("MiroFish Backend started successfully")
return app return app

View File

@ -1,5 +1,5 @@
""" """
API路由模块 Mô-đun tuyến API
""" """
from flask import Blueprint from flask import Blueprint

View File

@ -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 import os
@ -18,12 +18,12 @@ from ..utils.logger import get_logger
from ..models.task import TaskManager, TaskStatus from ..models.task import TaskManager, TaskStatus
from ..models.project import ProjectManager, ProjectStatus from ..models.project import ProjectManager, ProjectStatus
# 获取日志器 # Logger
logger = get_logger('mirofish.api') logger = get_logger('mirofish.api')
def allowed_file(filename: str) -> bool: 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: if not filename or '.' not in filename:
return False return False
ext = os.path.splitext(filename)[1].lower().lstrip('.') ext = os.path.splitext(filename)[1].lower().lstrip('.')
@ -35,14 +35,14 @@ def allowed_file(filename: str) -> bool:
@graph_bp.route('/project/<project_id>', methods=['GET']) @graph_bp.route('/project/<project_id>', methods=['GET'])
def get_project(project_id: str): def get_project(project_id: str):
""" """
获取项目详情 Lấy chi tiết project
""" """
project = ProjectManager.get_project(project_id) project = ProjectManager.get_project(project_id)
if not project: if not project:
return jsonify({ return jsonify({
"success": False, "success": False,
"error": f"项目不存在: {project_id}" "error": f"Project does not exist: {project_id}"
}), 404 }), 404
return jsonify({ return jsonify({
@ -54,7 +54,7 @@ def get_project(project_id: str):
@graph_bp.route('/project/list', methods=['GET']) @graph_bp.route('/project/list', methods=['GET'])
def list_projects(): def list_projects():
""" """
列出所有项目 Liệt kê tất cả project
""" """
limit = request.args.get('limit', 50, type=int) limit = request.args.get('limit', 50, type=int)
projects = ProjectManager.list_projects(limit=limit) projects = ProjectManager.list_projects(limit=limit)
@ -69,36 +69,36 @@ def list_projects():
@graph_bp.route('/project/<project_id>', methods=['DELETE']) @graph_bp.route('/project/<project_id>', methods=['DELETE'])
def delete_project(project_id: str): def delete_project(project_id: str):
""" """
删除项目 Xóa project
""" """
success = ProjectManager.delete_project(project_id) success = ProjectManager.delete_project(project_id)
if not success: if not success:
return jsonify({ return jsonify({
"success": False, "success": False,
"error": f"项目不存在或删除失败: {project_id}" "error": f"Project does not exist or delete failed: {project_id}"
}), 404 }), 404
return jsonify({ return jsonify({
"success": True, "success": True,
"message": f"项目已删除: {project_id}" "message": f"Project deleted: {project_id}"
}) })
@graph_bp.route('/project/<project_id>/reset', methods=['POST']) @graph_bp.route('/project/<project_id>/reset', methods=['POST'])
def reset_project(project_id: str): def reset_project(project_id: str):
""" """
重置项目状态(用于重新构建图谱) Reset trạng thái project (dùng để rebuild graph)
""" """
project = ProjectManager.get_project(project_id) project = ProjectManager.get_project(project_id)
if not project: if not project:
return jsonify({ return jsonify({
"success": False, "success": False,
"error": f"项目不存在: {project_id}" "error": f"Project does not exist: {project_id}"
}), 404 }), 404
# 重置到本体已生成状态 # Reset về trạng thái ontology_generated nếu đã có ontology, ngược lại về created
if project.ontology: if project.ontology:
project.status = ProjectStatus.ONTOLOGY_GENERATED project.status = ProjectStatus.ONTOLOGY_GENERATED
else: else:
@ -111,27 +111,27 @@ def reset_project(project_id: str):
return jsonify({ return jsonify({
"success": True, "success": True,
"message": f"项目已重置: {project_id}", "message": f"Project reset successfully: {project_id}",
"data": project.to_dict() "data": project.to_dict()
}) })
# ============== 接口1:上传文件并生成本体 ============== # ============== API 1: Upload file và generate ontology ==============
@graph_bp.route('/ontology/generate', methods=['POST']) @graph_bp.route('/ontology/generate', methods=['POST'])
def generate_ontology(): 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
参数: Parameters:
files: 上传的文件(PDF/MD/TXT),可多个 files: File upload (PDF/MD/TXT), có thể nhiều file
simulation_requirement: 模拟需求描述(必填) simulation_requirement: Mô tả yêu cầu simulation (bắt buộc)
project_name: 项目名称(可选) project_name: Tên project (tùy chọn)
additional_context: 额外说明(可选) additional_context: Thông tin bổ sung (tùy chọn)
返回: Response:
{ {
"success": true, "success": true,
"data": { "data": {
@ -147,42 +147,42 @@ def generate_ontology():
} }
""" """
try: try:
logger.info("=== 开始生成本体定义 ===") logger.info("=== Start generating ontology definition ===")
# 获取参数 # Get parameters
simulation_requirement = request.form.get('simulation_requirement', '') simulation_requirement = request.form.get('simulation_requirement', '')
project_name = request.form.get('project_name', 'Unnamed Project') project_name = request.form.get('project_name', 'Unnamed Project')
additional_context = request.form.get('additional_context', '') additional_context = request.form.get('additional_context', '')
logger.debug(f"项目名称: {project_name}") logger.debug(f"Project name: {project_name}")
logger.debug(f"模拟需求: {simulation_requirement[:100]}...") logger.debug(f"Simulation requirement: {simulation_requirement[:100]}...")
if not simulation_requirement: if not simulation_requirement:
return jsonify({ return jsonify({
"success": False, "success": False,
"error": "请提供模拟需求描述 (simulation_requirement)" "error": "Please provide simulation requirement description (simulation_requirement)"
}), 400 }), 400
# 获取上传的文件 # Get uploaded files
uploaded_files = request.files.getlist('files') uploaded_files = request.files.getlist('files')
if not uploaded_files or all(not f.filename for f in uploaded_files): if not uploaded_files or all(not f.filename for f in uploaded_files):
return jsonify({ return jsonify({
"success": False, "success": False,
"error": "请至少上传一个文档文件" "error": "Please upload at least one document file"
}), 400 }), 400
# 创建项目 # Create project
project = ProjectManager.create_project(name=project_name) project = ProjectManager.create_project(name=project_name)
project.simulation_requirement = simulation_requirement 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 = [] document_texts = []
all_text = "" all_text = ""
for file in uploaded_files: for file in uploaded_files:
if file and file.filename and allowed_file(file.filename): if file and file.filename and allowed_file(file.filename):
# 保存文件到项目目录 # Save file into project directory
file_info = ProjectManager.save_file_to_project( file_info = ProjectManager.save_file_to_project(
project.project_id, project.project_id,
file, file,
@ -193,7 +193,7 @@ def generate_ontology():
"size": file_info["size"] "size": file_info["size"]
}) })
# 提取文本 # Extract text
text = FileParser.extract_text(file_info["path"]) text = FileParser.extract_text(file_info["path"])
text = TextProcessor.preprocess_text(text) text = TextProcessor.preprocess_text(text)
document_texts.append(text) document_texts.append(text)
@ -203,16 +203,16 @@ def generate_ontology():
ProjectManager.delete_project(project.project_id) ProjectManager.delete_project(project.project_id)
return jsonify({ return jsonify({
"success": False, "success": False,
"error": "没有成功处理任何文档,请检查文件格式" "error": "No documents were successfully processed. Please check the file formats."
}), 400 }), 400
# 保存提取的文本 # Save extracted text to project
project.total_text_length = len(all_text) project.total_text_length = len(all_text)
ProjectManager.save_extracted_text(project.project_id, 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")
# 生成本体 # Generate ontology definition using LLM
logger.info("调用 LLM 生成本体定义...") logger.info("Calling LLM to generate ontology definition...")
generator = OntologyGenerator() generator = OntologyGenerator()
ontology = generator.generate( ontology = generator.generate(
document_texts=document_texts, document_texts=document_texts,
@ -220,10 +220,10 @@ def generate_ontology():
additional_context=additional_context if additional_context else None additional_context=additional_context if additional_context else None
) )
# 保存本体到项目 # Save ontology to project
entity_count = len(ontology.get("entity_types", [])) entity_count = len(ontology.get("entity_types", []))
edge_count = len(ontology.get("edge_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 = { project.ontology = {
"entity_types": ontology.get("entity_types", []), "entity_types": ontology.get("entity_types", []),
@ -232,7 +232,7 @@ def generate_ontology():
project.analysis_summary = ontology.get("analysis_summary", "") project.analysis_summary = ontology.get("analysis_summary", "")
project.status = ProjectStatus.ONTOLOGY_GENERATED project.status = ProjectStatus.ONTOLOGY_GENERATED
ProjectManager.save_project(project) ProjectManager.save_project(project)
logger.info(f"=== 本体生成完成 === 项目ID: {project.project_id}") logger.info(f"=== Ontology generation completed === Project ID: {project.project_id}")
return jsonify({ return jsonify({
"success": True, "success": True,
@ -254,140 +254,140 @@ def generate_ontology():
}), 500 }), 500
# ============== 接口2:构建图谱 ============== # ============== API 2: Build graph ==============
@graph_bp.route('/build', methods=['POST']) @graph_bp.route('/build', methods=['POST'])
def build_graph(): def build_graph():
""" """
接口2:根据project_id构建图谱 API 2: Build graph dựa trên project_id
请求(JSON): Request (JSON):
{ {
"project_id": "proj_xxxx", // 必填,来自接口1 "project_id": "proj_xxxx", // bắt buộc, từ API 1
"graph_name": "图谱名称", // 可选 "graph_name": "Graph name", // tùy chọn
"chunk_size": 500, // 可选,默认500 "chunk_size": 500, // tùy chọn, mặc định 500
"chunk_overlap": 50 // 可选,默认50 "chunk_overlap": 50 // tùy chọn, mặc định 50
} }
返回: Response:
{ {
"success": true, "success": true,
"data": { "data": {
"project_id": "proj_xxxx", "project_id": "proj_xxxx",
"task_id": "task_xxxx", "task_id": "task_xxxx",
"message": "图谱构建任务已启动" "message": "Graph build task started"
} }
} }
""" """
try: try:
logger.info("=== 开始构建图谱 ===") logger.info("=== Start building graph ===")
# 检查配置 # Kiểm tra cấu hình
errors = [] errors = []
if not Config.ZEP_API_KEY: if not Config.ZEP_API_KEY:
errors.append("ZEP_API_KEY未配置") errors.append("ZEP_API_KEY not configured")
if errors: if errors:
logger.error(f"配置错误: {errors}") logger.error(f"Configuration error: {errors}")
return jsonify({ return jsonify({
"success": False, "success": False,
"error": "配置错误: " + "; ".join(errors) "error": "Configuration error: " + "; ".join(errors)
}), 500 }), 500
# 解析请求 # Parse request
data = request.get_json() or {} data = request.get_json() or {}
project_id = data.get('project_id') 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: if not project_id:
return jsonify({ return jsonify({
"success": False, "success": False,
"error": "请提供 project_id" "error": "Please provide project_id"
}), 400 }), 400
# 获取项目 # Lấy project
project = ProjectManager.get_project(project_id) project = ProjectManager.get_project(project_id)
if not project: if not project:
return jsonify({ return jsonify({
"success": False, "success": False,
"error": f"项目不存在: {project_id}" "error": f"Project does not exist: {project_id}"
}), 404 }), 404
# 检查项目状态 # Kiểm tra trạng thái project
force = data.get('force', False) # 强制重新构建 force = data.get('force', False) # force rebuild
if project.status == ProjectStatus.CREATED: if project.status == ProjectStatus.CREATED:
return jsonify({ return jsonify({
"success": False, "success": False,
"error": "项目尚未生成本体,请先调用 /ontology/generate" "error": "Ontology has not been generated. Please call /ontology/generate first"
}), 400 }), 400
if project.status == ProjectStatus.GRAPH_BUILDING and not force: if project.status == ProjectStatus.GRAPH_BUILDING and not force:
return jsonify({ return jsonify({
"success": False, "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 "task_id": project.graph_build_task_id
}), 400 }), 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]: if force and project.status in [ProjectStatus.GRAPH_BUILDING, ProjectStatus.FAILED, ProjectStatus.GRAPH_COMPLETED]:
project.status = ProjectStatus.ONTOLOGY_GENERATED project.status = ProjectStatus.ONTOLOGY_GENERATED
project.graph_id = None project.graph_id = None
project.graph_build_task_id = None project.graph_build_task_id = None
project.error = None project.error = None
# 获取配置 # Lấy cấu hình
graph_name = data.get('graph_name', project.name or 'MiroFish Graph') 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_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) 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_size = chunk_size
project.chunk_overlap = chunk_overlap project.chunk_overlap = chunk_overlap
# 获取提取的文本 # Lấy text đã extract
text = ProjectManager.get_extracted_text(project_id) text = ProjectManager.get_extracted_text(project_id)
if not text: if not text:
return jsonify({ return jsonify({
"success": False, "success": False,
"error": "未找到提取的文本内容" "error": "Extracted text not found"
}), 400 }), 400
# 获取本体 # Lấy ontology
ontology = project.ontology ontology = project.ontology
if not ontology: if not ontology:
return jsonify({ return jsonify({
"success": False, "success": False,
"error": "未找到本体定义" "error": "Ontology definition not found"
}), 400 }), 400
# 创建异步任务 # Tạo async task
task_manager = TaskManager() task_manager = TaskManager()
task_id = task_manager.create_task(f"构建图谱: {graph_name}") task_id = task_manager.create_task(f"Build graph: {graph_name}")
logger.info(f"创建图谱构建任务: task_id={task_id}, project_id={project_id}") 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.status = ProjectStatus.GRAPH_BUILDING
project.graph_build_task_id = task_id project.graph_build_task_id = task_id
ProjectManager.save_project(project) ProjectManager.save_project(project)
# 启动后台任务 # Khởi động background task
def build_task(): def build_task():
build_logger = get_logger('mirofish.build') build_logger = get_logger('mirofish.build')
try: try:
build_logger.info(f"[{task_id}] 开始构建图谱...") build_logger.info(f"[{task_id}] Start building graph...")
task_manager.update_task( task_manager.update_task(
task_id, task_id,
status=TaskStatus.PROCESSING, status=TaskStatus.PROCESSING,
message="初始化图谱构建服务..." message="Initializing graph builder service..."
) )
# 创建图谱构建服务 # Tạo graph builder service
builder = GraphBuilderService(api_key=Config.ZEP_API_KEY) builder = GraphBuilderService(api_key=Config.ZEP_API_KEY)
# 分块 # Chunk text
task_manager.update_task( task_manager.update_task(
task_id, task_id,
message="文本分块中...", message="Splitting text into chunks...",
progress=5 progress=5
) )
chunks = TextProcessor.split_text( chunks = TextProcessor.split_text(
@ -397,29 +397,29 @@ def build_graph():
) )
total_chunks = len(chunks) total_chunks = len(chunks)
# 创建图谱 # Tạo graph
task_manager.update_task( task_manager.update_task(
task_id, task_id,
message="创建Zep图谱...", message="Creating Zep graph...",
progress=10 progress=10
) )
graph_id = builder.create_graph(name=graph_name) graph_id = builder.create_graph(name=graph_name)
# 更新项目的graph_id # Cập nhật graph_id của project
project.graph_id = graph_id project.graph_id = graph_id
ProjectManager.save_project(project) ProjectManager.save_project(project)
# 设置本体 # Thiết lập ontology
task_manager.update_task( task_manager.update_task(
task_id, task_id,
message="设置本体定义...", message="Setting ontology definition...",
progress=15 progress=15
) )
builder.set_ontology(graph_id, ontology) 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): 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_manager.update_task(
task_id, task_id,
message=msg, message=msg,
@ -428,7 +428,7 @@ def build_graph():
task_manager.update_task( task_manager.update_task(
task_id, task_id,
message=f"开始添加 {total_chunks} 个文本块...", message=f"Adding {total_chunks} text chunks...",
progress=15 progress=15
) )
@ -439,15 +439,15 @@ def build_graph():
progress_callback=add_progress_callback progress_callback=add_progress_callback
) )
# 等待Zep处理完成(查询每个episode的processed状态) # Chờ Zep xử lý xong
task_manager.update_task( task_manager.update_task(
task_id, task_id,
message="等待Zep处理数据...", message="Waiting for Zep to process data...",
progress=55 progress=55
) )
def wait_progress_callback(msg, progress_ratio): 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_manager.update_task(
task_id, task_id,
message=msg, message=msg,
@ -456,27 +456,27 @@ def build_graph():
builder._wait_for_episodes(episode_uuids, wait_progress_callback) builder._wait_for_episodes(episode_uuids, wait_progress_callback)
# 获取图谱数据 # Lấy graph data
task_manager.update_task( task_manager.update_task(
task_id, task_id,
message="获取图谱数据...", message="Fetching graph data...",
progress=95 progress=95
) )
graph_data = builder.get_graph_data(graph_id) graph_data = builder.get_graph_data(graph_id)
# 更新项目状态 # Cập nhật trạng thái project
project.status = ProjectStatus.GRAPH_COMPLETED project.status = ProjectStatus.GRAPH_COMPLETED
ProjectManager.save_project(project) ProjectManager.save_project(project)
node_count = graph_data.get("node_count", 0) node_count = graph_data.get("node_count", 0)
edge_count = graph_data.get("edge_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_manager.update_task(
task_id, task_id,
status=TaskStatus.COMPLETED, status=TaskStatus.COMPLETED,
message="图谱构建完成", message="Graph build completed",
progress=100, progress=100,
result={ result={
"project_id": project_id, "project_id": project_id,
@ -488,8 +488,8 @@ def build_graph():
) )
except Exception as e: except Exception as e:
# 更新项目状态为失败 # Cập nhật trạng thái project là failed
build_logger.error(f"[{task_id}] 图谱构建失败: {str(e)}") build_logger.error(f"[{task_id}] Graph build failed: {str(e)}")
build_logger.debug(traceback.format_exc()) build_logger.debug(traceback.format_exc())
project.status = ProjectStatus.FAILED project.status = ProjectStatus.FAILED
@ -499,11 +499,11 @@ def build_graph():
task_manager.update_task( task_manager.update_task(
task_id, task_id,
status=TaskStatus.FAILED, status=TaskStatus.FAILED,
message=f"构建失败: {str(e)}", message=f"Build failed: {str(e)}",
error=traceback.format_exc() error=traceback.format_exc()
) )
# 启动后台线程 # Chạy background thread
thread = threading.Thread(target=build_task, daemon=True) thread = threading.Thread(target=build_task, daemon=True)
thread.start() thread.start()
@ -512,7 +512,7 @@ def build_graph():
"data": { "data": {
"project_id": project_id, "project_id": project_id,
"task_id": task_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 build_graph():
}), 500 }), 500
# ============== 任务查询接口 ============== # ============== Task query APIs ==============
@graph_bp.route('/task/<task_id>', methods=['GET']) @graph_bp.route('/task/<task_id>', methods=['GET'])
def get_task(task_id: str): def get_task(task_id: str):
""" """
查询任务状态 Truy vấn trạng thái task
""" """
task = TaskManager().get_task(task_id) task = TaskManager().get_task(task_id)
if not task: if not task:
return jsonify({ return jsonify({
"success": False, "success": False,
"error": f"任务不存在: {task_id}" "error": f"Task does not exist: {task_id}"
}), 404 }), 404
return jsonify({ return jsonify({
@ -548,7 +548,7 @@ def get_task(task_id: str):
@graph_bp.route('/tasks', methods=['GET']) @graph_bp.route('/tasks', methods=['GET'])
def list_tasks(): def list_tasks():
""" """
列出所有任务 Liệt kê tất cả tasks
""" """
tasks = TaskManager().list_tasks() tasks = TaskManager().list_tasks()
@ -559,18 +559,18 @@ def list_tasks():
}) })
# ============== 图谱数据接口 ============== # ============== Graph data APIs ==============
@graph_bp.route('/data/<graph_id>', methods=['GET']) @graph_bp.route('/data/<graph_id>', methods=['GET'])
def get_graph_data(graph_id: str): def get_graph_data(graph_id: str):
""" """
获取图谱数据(节点和边) Lấy dữ liệu graph (nodes và edges)
""" """
try: try:
if not Config.ZEP_API_KEY: if not Config.ZEP_API_KEY:
return jsonify({ return jsonify({
"success": False, "success": False,
"error": "ZEP_API_KEY未配置" "error": "ZEP_API_KEY not configured"
}), 500 }), 500
builder = GraphBuilderService(api_key=Config.ZEP_API_KEY) builder = GraphBuilderService(api_key=Config.ZEP_API_KEY)
@ -592,13 +592,13 @@ def get_graph_data(graph_id: str):
@graph_bp.route('/delete/<graph_id>', methods=['DELETE']) @graph_bp.route('/delete/<graph_id>', methods=['DELETE'])
def delete_graph(graph_id: str): def delete_graph(graph_id: str):
""" """
删除Zep图谱 Xóa Zep graph
""" """
try: try:
if not Config.ZEP_API_KEY: if not Config.ZEP_API_KEY:
return jsonify({ return jsonify({
"success": False, "success": False,
"error": "ZEP_API_KEY未配置" "error": "ZEP_API_KEY not configured"
}), 500 }), 500
builder = GraphBuilderService(api_key=Config.ZEP_API_KEY) builder = GraphBuilderService(api_key=Config.ZEP_API_KEY)
@ -606,7 +606,7 @@ def delete_graph(graph_id: str):
return jsonify({ return jsonify({
"success": True, "success": True,
"message": f"图谱已删除: {graph_id}" "message": f"Graph deleted: {graph_id}"
}) })
except Exception as e: except Exception as e:

View File

@ -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 import os
@ -19,30 +19,30 @@ from ..utils.logger import get_logger
logger = get_logger('mirofish.api.report') logger = get_logger('mirofish.api.report')
# ============== 报告生成接口 ============== # ============== Report generation API ==============
@report_bp.route('/generate', methods=['POST']) @report_bp.route('/generate', methods=['POST'])
def generate_report(): def generate_report():
""" """
生成模拟分析报告(异步任务) Generate báo cáo phân tích simulation (async task)
这是一个耗时操作,接口会立即返回task_id, Đây là thao tác tốn thời gian, API sẽ trả về task_id ngay lập tức,
使用 GET /api/report/generate/status 查询进度 dùng GET /api/report/generate/status để kiểm tra progress
请求(JSON): Request (JSON):
{ {
"simulation_id": "sim_xxxx", // 必填,模拟ID "simulation_id": "sim_xxxx", // bắt buộc
"force_regenerate": false // 可选,强制重新生成 "force_regenerate": false // optional, force regenerate
} }
返回: Response:
{ {
"success": true, "success": true,
"data": { "data": {
"simulation_id": "sim_xxxx", "simulation_id": "sim_xxxx",
"task_id": "task_xxxx", "task_id": "task_xxxx",
"status": "generating", "status": "generating",
"message": "报告生成任务已启动" "message": "Report generation task started"
} }
} }
""" """
@ -53,22 +53,22 @@ def generate_report():
if not simulation_id: if not simulation_id:
return jsonify({ return jsonify({
"success": False, "success": False,
"error": "请提供 simulation_id" "error": "Please provide simulation_id"
}), 400 }), 400
force_regenerate = data.get('force_regenerate', False) force_regenerate = data.get('force_regenerate', False)
# 获取模拟信息 # Lấy thông tin simulation
manager = SimulationManager() manager = SimulationManager()
state = manager.get_simulation(simulation_id) state = manager.get_simulation(simulation_id)
if not state: if not state:
return jsonify({ return jsonify({
"success": False, "success": False,
"error": f"模拟不存在: {simulation_id}" "error": f"Simulation does not exist: {simulation_id}"
}), 404 }), 404
# 检查是否已有报告 # Kiểm tra đã có report chưa
if not force_regenerate: if not force_regenerate:
existing_report = ReportManager.get_report_by_simulation(simulation_id) existing_report = ReportManager.get_report_by_simulation(simulation_id)
if existing_report and existing_report.status == ReportStatus.COMPLETED: if existing_report and existing_report.status == ReportStatus.COMPLETED:
@ -78,38 +78,38 @@ def generate_report():
"simulation_id": simulation_id, "simulation_id": simulation_id,
"report_id": existing_report.report_id, "report_id": existing_report.report_id,
"status": "completed", "status": "completed",
"message": "报告已存在", "message": "Report already exists",
"already_generated": True "already_generated": True
} }
}) })
# 获取项目信息 # Lấy thông tin project
project = ProjectManager.get_project(state.project_id) project = ProjectManager.get_project(state.project_id)
if not project: if not project:
return jsonify({ return jsonify({
"success": False, "success": False,
"error": f"项目不存在: {state.project_id}" "error": f"Project does not exist: {state.project_id}"
}), 404 }), 404
graph_id = state.graph_id or project.graph_id graph_id = state.graph_id or project.graph_id
if not graph_id: if not graph_id:
return jsonify({ return jsonify({
"success": False, "success": False,
"error": "缺少图谱ID,请确保已构建图谱" "error": "Missing graph_id, please ensure the graph has been built"
}), 400 }), 400
simulation_requirement = project.simulation_requirement simulation_requirement = project.simulation_requirement
if not simulation_requirement: if not simulation_requirement:
return jsonify({ return jsonify({
"success": False, "success": False,
"error": "缺少模拟需求描述" "error": "Missing simulation requirement description"
}), 400 }), 400
# 提前生成 report_id,以便立即返回给前端 # Tạo report_id trước để có thể trả về ngay cho frontend
import uuid import uuid
report_id = f"report_{uuid.uuid4().hex[:12]}" report_id = f"report_{uuid.uuid4().hex[:12]}"
# 创建异步任务 # Tạo async task
task_manager = TaskManager() task_manager = TaskManager()
task_id = task_manager.create_task( task_id = task_manager.create_task(
task_type="report_generate", task_type="report_generate",
@ -120,24 +120,24 @@ def generate_report():
} }
) )
# 定义后台任务 # Định nghĩa background task
def run_generate(): def run_generate():
try: try:
task_manager.update_task( task_manager.update_task(
task_id, task_id,
status=TaskStatus.PROCESSING, status=TaskStatus.PROCESSING,
progress=0, progress=0,
message="初始化Report Agent..." message="Initializing Report Agent..."
) )
# 创建Report Agent # Tạo Report Agent
agent = ReportAgent( agent = ReportAgent(
graph_id=graph_id, graph_id=graph_id,
simulation_id=simulation_id, simulation_id=simulation_id,
simulation_requirement=simulation_requirement simulation_requirement=simulation_requirement
) )
# 进度回调 # Callback cập nhật progress
def progress_callback(stage, progress, message): def progress_callback(stage, progress, message):
task_manager.update_task( task_manager.update_task(
task_id, task_id,
@ -145,13 +145,13 @@ def generate_report():
message=f"[{stage}] {message}" message=f"[{stage}] {message}"
) )
# 生成报告(传入预先生成的 report_id) # Generate report
report = agent.generate_report( report = agent.generate_report(
progress_callback=progress_callback, progress_callback=progress_callback,
report_id=report_id report_id=report_id
) )
# 保存报告 # Lưu report
ReportManager.save_report(report) ReportManager.save_report(report)
if report.status == ReportStatus.COMPLETED: if report.status == ReportStatus.COMPLETED:
@ -164,13 +164,13 @@ def generate_report():
} }
) )
else: 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: 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)) task_manager.fail_task(task_id, str(e))
# 启动后台线程 # Chạy background thread
thread = threading.Thread(target=run_generate, daemon=True) thread = threading.Thread(target=run_generate, daemon=True)
thread.start() thread.start()
@ -181,13 +181,13 @@ def generate_report():
"report_id": report_id, "report_id": report_id,
"task_id": task_id, "task_id": task_id,
"status": "generating", "status": "generating",
"message": "报告生成任务已启动,请通过 /api/report/generate/status 查询进度", "message": "Report generation task started. Use /api/report/generate/status to check progress",
"already_generated": False "already_generated": False
} }
}) })
except Exception as e: except Exception as e:
logger.error(f"启动报告生成任务失败: {str(e)}") logger.error(f"Failed to start report generation task: {str(e)}")
return jsonify({ return jsonify({
"success": False, "success": False,
"error": str(e), "error": str(e),
@ -198,15 +198,15 @@ def generate_report():
@report_bp.route('/generate/status', methods=['POST']) @report_bp.route('/generate/status', methods=['POST'])
def get_generate_status(): def get_generate_status():
""" """
查询报告生成任务进度 Truy vấn progress của task generate report
请求(JSON): Request (JSON):
{ {
"task_id": "task_xxxx", // 可选,generate返回的task_id "task_id": "task_xxxx", // optional, task_id trả về từ generate
"simulation_id": "sim_xxxx" // 可选,模拟ID "simulation_id": "sim_xxxx" // optional
} }
返回: Response:
{ {
"success": true, "success": true,
"data": { "data": {
@ -223,7 +223,7 @@ def get_generate_status():
task_id = data.get('task_id') task_id = data.get('task_id')
simulation_id = data.get('simulation_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: if simulation_id:
existing_report = ReportManager.get_report_by_simulation(simulation_id) existing_report = ReportManager.get_report_by_simulation(simulation_id)
if existing_report and existing_report.status == ReportStatus.COMPLETED: if existing_report and existing_report.status == ReportStatus.COMPLETED:
@ -234,7 +234,7 @@ def get_generate_status():
"report_id": existing_report.report_id, "report_id": existing_report.report_id,
"status": "completed", "status": "completed",
"progress": 100, "progress": 100,
"message": "报告已生成", "message": "Report generated",
"already_completed": True "already_completed": True
} }
}) })
@ -242,7 +242,7 @@ def get_generate_status():
if not task_id: if not task_id:
return jsonify({ return jsonify({
"success": False, "success": False,
"error": "请提供 task_id 或 simulation_id" "error": "Please provide task_id or simulation_id"
}), 400 }), 400
task_manager = TaskManager() task_manager = TaskManager()
@ -251,7 +251,7 @@ def get_generate_status():
if not task: if not task:
return jsonify({ return jsonify({
"success": False, "success": False,
"error": f"任务不存在: {task_id}" "error": f"Task does not exist: {task_id}"
}), 404 }), 404
return jsonify({ return jsonify({
@ -260,21 +260,21 @@ def get_generate_status():
}) })
except Exception as e: except Exception as e:
logger.error(f"查询任务状态失败: {str(e)}") logger.error(f"Failed to query task status: {str(e)}")
return jsonify({ return jsonify({
"success": False, "success": False,
"error": str(e) "error": str(e)
}), 500 }), 500
# ============== 报告获取接口 ============== # ============== Report retrieval APIs ==============
@report_bp.route('/<report_id>', methods=['GET']) @report_bp.route('/<report_id>', methods=['GET'])
def get_report(report_id: str): def get_report(report_id: str):
""" """
获取报告详情 Lấy chi tiết report
返回: Response:
{ {
"success": true, "success": true,
"data": { "data": {
@ -294,7 +294,7 @@ def get_report(report_id: str):
if not report: if not report:
return jsonify({ return jsonify({
"success": False, "success": False,
"error": f"报告不存在: {report_id}" "error": f"Report does not exist: {report_id}"
}), 404 }), 404
return jsonify({ return jsonify({
@ -303,7 +303,7 @@ def get_report(report_id: str):
}) })
except Exception as e: except Exception as e:
logger.error(f"获取报告失败: {str(e)}") logger.error(f"Failed to retrieve report: {str(e)}")
return jsonify({ return jsonify({
"success": False, "success": False,
"error": str(e), "error": str(e),
@ -314,9 +314,9 @@ def get_report(report_id: str):
@report_bp.route('/by-simulation/<simulation_id>', methods=['GET']) @report_bp.route('/by-simulation/<simulation_id>', methods=['GET'])
def get_report_by_simulation(simulation_id: str): def get_report_by_simulation(simulation_id: str):
""" """
根据模拟ID获取报告 Lấy report theo simulation_id
返回: Response:
{ {
"success": true, "success": true,
"data": { "data": {
@ -331,7 +331,7 @@ def get_report_by_simulation(simulation_id: str):
if not report: if not report:
return jsonify({ return jsonify({
"success": False, "success": False,
"error": f"该模拟暂无报告: {simulation_id}", "error": f"No report found for this simulation: {simulation_id}",
"has_report": False "has_report": False
}), 404 }), 404
@ -342,7 +342,7 @@ def get_report_by_simulation(simulation_id: str):
}) })
except Exception as e: except Exception as e:
logger.error(f"获取报告失败: {str(e)}") logger.error(f"Failed to retrieve report: {str(e)}")
return jsonify({ return jsonify({
"success": False, "success": False,
"error": str(e), "error": str(e),
@ -353,13 +353,13 @@ def get_report_by_simulation(simulation_id: str):
@report_bp.route('/list', methods=['GET']) @report_bp.route('/list', methods=['GET'])
def list_reports(): def list_reports():
""" """
列出所有报告 Liệt kê tất cả reports
Query参数: Query parameters:
simulation_id: 按模拟ID过滤(可选) simulation_id: Filter theo simulation_id (optional)
limit: 返回数量限制(默认50) limit: Giới hạn số lượng trả về (default 50)
返回: Response:
{ {
"success": true, "success": true,
"data": [...], "data": [...],
@ -382,7 +382,7 @@ def list_reports():
}) })
except Exception as e: except Exception as e:
logger.error(f"列出报告失败: {str(e)}") logger.error(f"Failed to list reports: {str(e)}")
return jsonify({ return jsonify({
"success": False, "success": False,
"error": str(e), "error": str(e),
@ -393,9 +393,9 @@ def list_reports():
@report_bp.route('/<report_id>/download', methods=['GET']) @report_bp.route('/<report_id>/download', methods=['GET'])
def download_report(report_id: str): def download_report(report_id: str):
""" """
下载报告(Markdown格式) Download report (Markdown format)
返回Markdown文件 Trả về file Markdown
""" """
try: try:
report = ReportManager.get_report(report_id) report = ReportManager.get_report(report_id)
@ -403,13 +403,13 @@ def download_report(report_id: str):
if not report: if not report:
return jsonify({ return jsonify({
"success": False, "success": False,
"error": f"报告不存在: {report_id}" "error": f"Report does not exist: {report_id}"
}), 404 }), 404
md_path = ReportManager._get_report_markdown_path(report_id) md_path = ReportManager._get_report_markdown_path(report_id)
if not os.path.exists(md_path): 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 import tempfile
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f: with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
f.write(report.markdown_content) f.write(report.markdown_content)
@ -428,7 +428,7 @@ def download_report(report_id: str):
) )
except Exception as e: except Exception as e:
logger.error(f"下载报告失败: {str(e)}") logger.error(f"Failed to download report: {str(e)}")
return jsonify({ return jsonify({
"success": False, "success": False,
"error": str(e), "error": str(e),
@ -438,23 +438,23 @@ def download_report(report_id: str):
@report_bp.route('/<report_id>', methods=['DELETE']) @report_bp.route('/<report_id>', methods=['DELETE'])
def delete_report(report_id: str): def delete_report(report_id: str):
"""删除报告""" """Xóa report"""
try: try:
success = ReportManager.delete_report(report_id) success = ReportManager.delete_report(report_id)
if not success: if not success:
return jsonify({ return jsonify({
"success": False, "success": False,
"error": f"报告不存在: {report_id}" "error": f"Report does not exist: {report_id}"
}), 404 }), 404
return jsonify({ return jsonify({
"success": True, "success": True,
"message": f"报告已删除: {report_id}" "message": f"Report deleted: {report_id}"
}) })
except Exception as e: except Exception as e:
logger.error(f"删除报告失败: {str(e)}") logger.error(f"Failed to delete report: {str(e)}")
return jsonify({ return jsonify({
"success": False, "success": False,
"error": str(e), "error": str(e),
@ -462,32 +462,32 @@ def delete_report(report_id: str):
}), 500 }), 500
# ============== Report Agent对话接口 ============== # ============== Report Agent chat API ==============
@report_bp.route('/chat', methods=['POST']) @report_bp.route('/chat', methods=['POST'])
def chat_with_report_agent(): 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 "simulation_id": "sim_xxxx", // bắt buộc
"message": "请解释一下舆情走向", // 必填,用户消息 "message": "Please explain the public opinion trend", // bắt buộc
"chat_history": [ // 可选,对话历史 "chat_history": [ // optional
{"role": "user", "content": "..."}, {"role": "user", "content": "..."},
{"role": "assistant", "content": "..."} {"role": "assistant", "content": "..."}
] ]
} }
返回: Response:
{ {
"success": true, "success": true,
"data": { "data": {
"response": "Agent回复...", "response": "Agent reply...",
"tool_calls": [调用的工具列表], "tool_calls": [list of called tools],
"sources": [信息来源] "sources": [information sources]
} }
} }
""" """
@ -501,42 +501,42 @@ def chat_with_report_agent():
if not simulation_id: if not simulation_id:
return jsonify({ return jsonify({
"success": False, "success": False,
"error": "请提供 simulation_id" "error": "Please provide simulation_id"
}), 400 }), 400
if not message: if not message:
return jsonify({ return jsonify({
"success": False, "success": False,
"error": "请提供 message" "error": "Please provide message"
}), 400 }), 400
# 获取模拟和项目信息 # Lấy thông tin simulation và project
manager = SimulationManager() manager = SimulationManager()
state = manager.get_simulation(simulation_id) state = manager.get_simulation(simulation_id)
if not state: if not state:
return jsonify({ return jsonify({
"success": False, "success": False,
"error": f"模拟不存在: {simulation_id}" "error": f"Simulation does not exist: {simulation_id}"
}), 404 }), 404
project = ProjectManager.get_project(state.project_id) project = ProjectManager.get_project(state.project_id)
if not project: if not project:
return jsonify({ return jsonify({
"success": False, "success": False,
"error": f"项目不存在: {state.project_id}" "error": f"Project does not exist: {state.project_id}"
}), 404 }), 404
graph_id = state.graph_id or project.graph_id graph_id = state.graph_id or project.graph_id
if not graph_id: if not graph_id:
return jsonify({ return jsonify({
"success": False, "success": False,
"error": "缺少图谱ID" "error": "Missing graph_id"
}), 400 }), 400
simulation_requirement = project.simulation_requirement or "" simulation_requirement = project.simulation_requirement or ""
# 创建Agent并进行对话 # Tạo Agent và chat
agent = ReportAgent( agent = ReportAgent(
graph_id=graph_id, graph_id=graph_id,
simulation_id=simulation_id, simulation_id=simulation_id,
@ -551,7 +551,7 @@ def chat_with_report_agent():
}) })
except Exception as e: except Exception as e:
logger.error(f"对话失败: {str(e)}") logger.error(f"Chat failed: {str(e)}")
return jsonify({ return jsonify({
"success": False, "success": False,
"error": str(e), "error": str(e),
@ -559,22 +559,22 @@ def chat_with_report_agent():
}), 500 }), 500
# ============== 报告进度与分章节接口 ============== # ============== Report progress & sections APIs ==============
@report_bp.route('/<report_id>/progress', methods=['GET']) @report_bp.route('/<report_id>/progress', methods=['GET'])
def get_report_progress(report_id: str): def get_report_progress(report_id: str):
""" """
获取报告生成进度(实时) Lấy progress generate report (real-time)
返回: Response:
{ {
"success": true, "success": true,
"data": { "data": {
"status": "generating", "status": "generating",
"progress": 45, "progress": 45,
"message": "正在生成章节: 关键发现", "message": "Generating section: Key Findings",
"current_section": "关键发现", "current_section": "Key Findings",
"completed_sections": ["执行摘要", "模拟背景"], "completed_sections": ["Executive Summary", "Simulation Background"],
"updated_at": "2025-12-09T..." "updated_at": "2025-12-09T..."
} }
} }
@ -585,7 +585,7 @@ def get_report_progress(report_id: str):
if not progress: if not progress:
return jsonify({ return jsonify({
"success": False, "success": False,
"error": f"报告不存在或进度信息不可用: {report_id}" "error": f"Report does not exist or progress unavailable: {report_id}"
}), 404 }), 404
return jsonify({ return jsonify({
@ -594,7 +594,7 @@ def get_report_progress(report_id: str):
}) })
except Exception as e: except Exception as e:
logger.error(f"获取报告进度失败: {str(e)}") logger.error(f"Failed to retrieve report progress: {str(e)}")
return jsonify({ return jsonify({
"success": False, "success": False,
"error": str(e), "error": str(e),
@ -605,11 +605,12 @@ def get_report_progress(report_id: str):
@report_bp.route('/<report_id>/sections', methods=['GET']) @report_bp.route('/<report_id>/sections', methods=['GET'])
def get_report_sections(report_id: str): 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, "success": true,
"data": { "data": {
@ -618,9 +619,8 @@ def get_report_sections(report_id: str):
{ {
"filename": "section_01.md", "filename": "section_01.md",
"section_index": 1, "section_index": 1,
"content": "## 执行摘要\\n\\n..." "content": "## Executive Summary\\n\\n..."
}, }
...
], ],
"total_sections": 3, "total_sections": 3,
"is_complete": false "is_complete": false
@ -630,7 +630,7 @@ def get_report_sections(report_id: str):
try: try:
sections = ReportManager.get_generated_sections(report_id) sections = ReportManager.get_generated_sections(report_id)
# 获取报告状态 # Lấy trạng thái report
report = ReportManager.get_report(report_id) report = ReportManager.get_report(report_id)
is_complete = report is not None and report.status == ReportStatus.COMPLETED 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: except Exception as e:
logger.error(f"获取章节列表失败: {str(e)}") logger.error(f"Failed to retrieve section list: {str(e)}")
return jsonify({ return jsonify({
"success": False, "success": False,
"error": str(e), "error": str(e),
@ -656,14 +656,14 @@ def get_report_sections(report_id: str):
@report_bp.route('/<report_id>/section/<int:section_index>', methods=['GET']) @report_bp.route('/<report_id>/section/<int:section_index>', methods=['GET'])
def get_single_section(report_id: str, section_index: int): def get_single_section(report_id: str, section_index: int):
""" """
获取单个章节内容 Lấy nội dung một section
返回: Response:
{ {
"success": true, "success": true,
"data": { "data": {
"filename": "section_01.md", "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): if not os.path.exists(section_path):
return jsonify({ return jsonify({
"success": False, "success": False,
"error": f"章节不存在: section_{section_index:02d}.md" "error": f"Section does not exist: section_{section_index:02d}.md"
}), 404 }), 404
with open(section_path, 'r', encoding='utf-8') as f: 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: except Exception as e:
logger.error(f"获取章节内容失败: {str(e)}") logger.error(f"Failed to retrieve section content: {str(e)}")
return jsonify({ return jsonify({
"success": False, "success": False,
"error": str(e), "error": str(e),
@ -748,22 +748,22 @@ def check_report_status(simulation_id: str):
}), 500 }), 500
# ============== Agent 日志接口 ============== # ============== API nhật ký Agent ==============
@report_bp.route('/<report_id>/agent-log', methods=['GET']) @report_bp.route('/<report_id>/agent-log', methods=['GET'])
def get_agent_log(report_id: str): def get_agent_log(report_id: str):
""" """
获取 Report Agent 的详细执行日志 Lấy nhật ký thực thi chi tiết của Report Agent
实时获取报告生成过程中的每一步动作,包括: 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
- 每个章节的开始、工具调用、LLM响应、完成 - 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参数: Query parameters:
from_line: 从第几行开始读取(可选,默认0,用于增量获取) from_line: đọc từ dòng nào (optional, mặc định 0, dùng cho incremental fetch)
返回: Response:
{ {
"success": true, "success": true,
"data": { "data": {
@ -774,7 +774,7 @@ def get_agent_log(report_id: str):
"report_id": "report_xxxx", "report_id": "report_xxxx",
"action": "tool_call", "action": "tool_call",
"stage": "generating", "stage": "generating",
"section_title": "执行摘要", "section_title": "Executive Summary",
"section_index": 1, "section_index": 1,
"details": { "details": {
"tool_name": "insight_forge", "tool_name": "insight_forge",
@ -801,7 +801,7 @@ def get_agent_log(report_id: str):
}) })
except Exception as e: except Exception as e:
logger.error(f"获取Agent日志失败: {str(e)}") logger.error(f"Failed to get Agent log: {str(e)}")
return jsonify({ return jsonify({
"success": False, "success": False,
"error": str(e), "error": str(e),
@ -812,9 +812,9 @@ def get_agent_log(report_id: str):
@report_bp.route('/<report_id>/agent-log/stream', methods=['GET']) @report_bp.route('/<report_id>/agent-log/stream', methods=['GET'])
def stream_agent_log(report_id: str): 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, "success": true,
"data": { "data": {
@ -835,7 +835,7 @@ def stream_agent_log(report_id: str):
}) })
except Exception as e: except Exception as e:
logger.error(f"获取Agent日志失败: {str(e)}") logger.error(f"Failed to get Agent log: {str(e)}")
return jsonify({ return jsonify({
"success": False, "success": False,
"error": str(e), "error": str(e),
@ -843,27 +843,29 @@ def stream_agent_log(report_id: str):
}), 500 }), 500
# ============== 控制台日志接口 ============== # ============== API nhật ký Console ==============
@report_bp.route('/<report_id>/console-log', methods=['GET']) @report_bp.route('/<report_id>/console-log', methods=['GET'])
def get_console_log(report_id: str): def get_console_log(report_id: str):
""" """
获取 Report Agent 的控制台输出日志 Lấy console output logs của Report Agent
实时获取报告生成过程中的控制台输出(INFO、WARNING等), Lấy theo thời gian thực các console output trong quá trình tạo báo cáo
这与 agent-log 接口返回的结构化 JSON 日志不同, (INFO, WARNING, ...).
是纯文本格式的控制台风格日志。
Query参数: Khác với API agent-log trả về structured JSON logs,
from_line: 从第几行开始读取(可选,默认0,用于增量获取) 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, "success": true,
"data": { "data": {
"logs": [ "logs": [
"[19:46:14] INFO: 搜索完成: 找到 15 条相关事实", "[19:46:14] INFO: Search completed: found 15 relevant facts",
"[19:46:14] INFO: 图谱搜索: graph_id=xxx, query=...", "[19:46:14] INFO: Graph search: graph_id=xxx, query=...",
... ...
], ],
"total_lines": 100, "total_lines": 100,
@ -883,7 +885,7 @@ def get_console_log(report_id: str):
}) })
except Exception as e: except Exception as e:
logger.error(f"获取控制台日志失败: {str(e)}") logger.error(f"Failed to get console log: {str(e)}")
return jsonify({ return jsonify({
"success": False, "success": False,
"error": str(e), "error": str(e),
@ -894,9 +896,9 @@ def get_console_log(report_id: str):
@report_bp.route('/<report_id>/console-log/stream', methods=['GET']) @report_bp.route('/<report_id>/console-log/stream', methods=['GET'])
def stream_console_log(report_id: str): 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, "success": true,
"data": { "data": {
@ -917,7 +919,7 @@ def stream_console_log(report_id: str):
}) })
except Exception as e: except Exception as e:
logger.error(f"获取控制台日志失败: {str(e)}") logger.error(f"Failed to get console log: {str(e)}")
return jsonify({ return jsonify({
"success": False, "success": False,
"error": str(e), "error": str(e),
@ -925,17 +927,17 @@ def stream_console_log(report_id: str):
}), 500 }), 500
# ============== 工具调用接口(供调试使用)============== # ============== API gọi Tool (dùng cho debugging) ==============
@report_bp.route('/tools/search', methods=['POST']) @report_bp.route('/tools/search', methods=['POST'])
def search_graph_tool(): def search_graph_tool():
""" """
图谱搜索工具接口(供调试使用) API công cụ graph search (dùng cho debugging)
请求(JSON): Request (JSON):
{ {
"graph_id": "mirofish_xxxx", "graph_id": "mirofish_xxxx",
"query": "搜索查询", "query": "search query",
"limit": 10 "limit": 10
} }
""" """
@ -949,7 +951,7 @@ def search_graph_tool():
if not graph_id or not query: if not graph_id or not query:
return jsonify({ return jsonify({
"success": False, "success": False,
"error": "请提供 graph_id 和 query" "error": "Please provide graph_id and query"
}), 400 }), 400
from ..services.zep_tools import ZepToolsService from ..services.zep_tools import ZepToolsService
@ -967,7 +969,7 @@ def search_graph_tool():
}) })
except Exception as e: except Exception as e:
logger.error(f"图谱搜索失败: {str(e)}") logger.error(f"Graph search failed: {str(e)}")
return jsonify({ return jsonify({
"success": False, "success": False,
"error": str(e), "error": str(e),
@ -978,9 +980,9 @@ def search_graph_tool():
@report_bp.route('/tools/statistics', methods=['POST']) @report_bp.route('/tools/statistics', methods=['POST'])
def get_graph_statistics_tool(): def get_graph_statistics_tool():
""" """
图谱统计工具接口(供调试使用) API công cụ thống kê graph (dùng cho debugging)
请求(JSON): Request (JSON):
{ {
"graph_id": "mirofish_xxxx" "graph_id": "mirofish_xxxx"
} }
@ -993,7 +995,7 @@ def get_graph_statistics_tool():
if not graph_id: if not graph_id:
return jsonify({ return jsonify({
"success": False, "success": False,
"error": "请提供 graph_id" "error": "Please provide graph_id"
}), 400 }), 400
from ..services.zep_tools import ZepToolsService from ..services.zep_tools import ZepToolsService
@ -1007,7 +1009,7 @@ def get_graph_statistics_tool():
}) })
except Exception as e: except Exception as e:
logger.error(f"获取图谱统计失败: {str(e)}") logger.error(f"Failed to get graph statistics: {str(e)}")
return jsonify({ return jsonify({
"success": False, "success": False,
"error": str(e), "error": str(e),

File diff suppressed because it is too large Load Diff

View File

@ -1,54 +1,54 @@
""" """
配置管理 Quản lý cấu hình
统一从项目根目录的 .env 文件加载配置 Tải cấu hình thống nhất từ tệp .env ở thư mục gốc dự án
""" """
import os import os
from dotenv import load_dotenv from dotenv import load_dotenv
# 加载项目根目录的 .env 文件 # Tải tệp .env ở thư mục gốc dự án
# 路径: MiroFish/.env (相对于 backend/app/config.py) # Đườ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') project_root_env = os.path.join(os.path.dirname(__file__), '../../.env')
if os.path.exists(project_root_env): if os.path.exists(project_root_env):
load_dotenv(project_root_env, override=True) load_dotenv(project_root_env, override=True)
else: 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) load_dotenv(override=True)
class Config: 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') SECRET_KEY = os.environ.get('SECRET_KEY', 'mirofish-secret-key')
DEBUG = os.environ.get('FLASK_DEBUG', 'True').lower() == 'true' 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 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_API_KEY = os.environ.get('LLM_API_KEY')
LLM_BASE_URL = os.environ.get('LLM_BASE_URL', 'https://api.openai.com/v1') 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') 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') ZEP_API_KEY = os.environ.get('ZEP_API_KEY')
# 文件上传配置 # Cấu hình upload tệp
MAX_CONTENT_LENGTH = 50 * 1024 * 1024 # 50MB MAX_CONTENT_LENGTH = 50 * 1024 * 1024 # 50MB
UPLOAD_FOLDER = os.path.join(os.path.dirname(__file__), '../uploads') UPLOAD_FOLDER = os.path.join(os.path.dirname(__file__), '../uploads')
ALLOWED_EXTENSIONS = {'pdf', 'md', 'txt', 'markdown'} ALLOWED_EXTENSIONS = {'pdf', 'md', 'txt', 'markdown'}
# 文本处理配置 # Cấu hình xử lý văn bản
DEFAULT_CHUNK_SIZE = 500 # 默认切块大小 DEFAULT_CHUNK_SIZE = 500 # Kích thước chunk mặc định
DEFAULT_CHUNK_OVERLAP = 50 # 默认重叠大小 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_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_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 = [ OASIS_TWITTER_ACTIONS = [
'CREATE_POST', 'LIKE_POST', 'REPOST', 'FOLLOW', 'DO_NOTHING', 'QUOTE_POST' 'CREATE_POST', 'LIKE_POST', 'REPOST', 'FOLLOW', 'DO_NOTHING', 'QUOTE_POST'
] ]
@ -58,18 +58,18 @@ class Config:
'TREND', 'REFRESH', 'DO_NOTHING', 'FOLLOW', 'MUTE' '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_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_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')) REPORT_AGENT_TEMPERATURE = float(os.environ.get('REPORT_AGENT_TEMPERATURE', '0.5'))
@classmethod @classmethod
def validate(cls): def validate(cls):
"""验证必要配置""" """Kiểm tra các cấu hình bắt buộc"""
errors = [] errors = []
if not cls.LLM_API_KEY: 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: if not cls.ZEP_API_KEY:
errors.append("ZEP_API_KEY 未配置") errors.append("ZEP_API_KEY is not configured")
return errors return errors

View File

@ -1,5 +1,5 @@
""" """
数据模型模块 Mô-đun mô hình dữ liệu
""" """
from .task import TaskManager, TaskStatus from .task import TaskManager, TaskStatus

View File

@ -1,6 +1,6 @@
""" """
图谱构建服务 Dịch vụ xây dựng Đồ thị Tri thức (Knowledge Graph)
接口2:使用Zep API构建Standalone Graph API 2: Sử dụng Zep API để xây dựng một Standalone Graph (Đồ thị độc lập)
""" """
import os import os
@ -21,7 +21,7 @@ from .text_processor import TextProcessor
@dataclass @dataclass
class GraphInfo: class GraphInfo:
"""图谱信息""" """Các trường thông tin cơ bản của Graph"""
graph_id: str graph_id: str
node_count: int node_count: int
edge_count: int edge_count: int
@ -38,14 +38,14 @@ class GraphInfo:
class GraphBuilderService: class GraphBuilderService:
""" """
图谱构建服务 Dịch vụ tạo lập Graph
负责调用Zep API构建知识图谱 Đảm nhiệm logic gọi request lên Zep API để thiết lập Graph
""" """
def __init__(self, api_key: Optional[str] = None): def __init__(self, api_key: Optional[str] = None):
self.api_key = api_key or Config.ZEP_API_KEY self.api_key = api_key or Config.ZEP_API_KEY
if not self.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.client = Zep(api_key=self.api_key)
self.task_manager = TaskManager() self.task_manager = TaskManager()
@ -60,20 +60,20 @@ class GraphBuilderService:
batch_size: int = 3 batch_size: int = 3
) -> str: ) -> str:
""" """
异步构建图谱 Khởi chạy tiến trình bất đồng bộ xây dựng Graph
Args: Args:
text: 输入文本 text: Văn bản toàn văn làm nguồn vào
ontology: 本体定义(来自接口1的输出) ontology: Từ điển chuẩn cấu trúc Ontology (Đầu ra từ API số 1)
graph_name: 图谱名称 graph_name: Tên đặt cho Graph
chunk_size: 文本块大小 chunk_size: Kích thước từng khối text (chunk)
chunk_overlap: 块重叠大小 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: 每批发送的块数量 batch_size: Chuyển dữ liệu theo mảng batch để tiết kiệm số lần Request
Returns: 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_id = self.task_manager.create_task(
task_type="graph_build", task_type="graph_build",
metadata={ metadata={
@ -83,7 +83,7 @@ class GraphBuilderService:
} }
) )
# 在后台线程中执行构建 # 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( thread = threading.Thread(
target=self._build_graph_worker, target=self._build_graph_worker,
args=(task_id, text, ontology, graph_name, chunk_size, chunk_overlap, batch_size) args=(task_id, text, ontology, graph_name, chunk_size, chunk_overlap, batch_size)
@ -103,21 +103,21 @@ class GraphBuilderService:
chunk_overlap: int, chunk_overlap: int,
batch_size: 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: try:
self.task_manager.update_task( self.task_manager.update_task(
task_id, task_id,
status=TaskStatus.PROCESSING, status=TaskStatus.PROCESSING,
progress=5, 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) graph_id = self.create_graph(graph_name)
self.task_manager.update_task( self.task_manager.update_task(
task_id, task_id,
progress=10, progress=10,
message=f"图谱已创建: {graph_id}" message=f"Created empty graph: {graph_id}"
) )
# 2. 设置本体 # 2. 设置本体
@ -125,54 +125,54 @@ class GraphBuilderService:
self.task_manager.update_task( self.task_manager.update_task(
task_id, task_id,
progress=15, 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) chunks = TextProcessor.split_text(text, chunk_size, chunk_overlap)
total_chunks = len(chunks) total_chunks = len(chunks)
self.task_manager.update_task( self.task_manager.update_task(
task_id, task_id,
progress=20, 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( episode_uuids = self.add_text_batches(
graph_id, chunks, batch_size, graph_id, chunks, batch_size,
lambda msg, prog: self.task_manager.update_task( lambda msg, prog: self.task_manager.update_task(
task_id, task_id,
progress=20 + int(prog * 0.4), # 20-60% progress=20 + int(prog * 0.4), # Thể hiện từ 20-60%
message=msg 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( self.task_manager.update_task(
task_id, task_id,
progress=60, progress=60,
message="等待Zep处理数据..." message="Waiting for Zep to process data..."
) )
self._wait_for_episodes( self._wait_for_episodes(
episode_uuids, episode_uuids,
lambda msg, prog: self.task_manager.update_task( lambda msg, prog: self.task_manager.update_task(
task_id, task_id,
progress=60 + int(prog * 0.3), # 60-90% progress=60 + int(prog * 0.3), # Thể hiện từ 60-90%
message=msg message=msg
) )
) )
# 6. 获取图谱信息 # Bước 6. Thống kê lại Graph hoàn thiện
self.task_manager.update_task( self.task_manager.update_task(
task_id, task_id,
progress=90, progress=90,
message="获取图谱信息..." message="Fetching finalized graph info..."
) )
graph_info = self._get_graph_info(graph_id) graph_info = self._get_graph_info(graph_id)
# 完成 # Thông báo hoàn tất
self.task_manager.complete_task(task_id, { self.task_manager.complete_task(task_id, {
"graph_id": graph_id, "graph_id": graph_id,
"graph_info": graph_info.to_dict(), "graph_info": graph_info.to_dict(),
@ -185,7 +185,7 @@ class GraphBuilderService:
self.task_manager.fail_task(task_id, error_msg) self.task_manager.fail_task(task_id, error_msg)
def create_graph(self, name: str) -> str: 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]}" graph_id = f"mirofish_{uuid.uuid4().hex[:16]}"
self.client.graph.create( self.client.graph.create(
@ -197,74 +197,74 @@ class GraphBuilderService:
return graph_id return graph_id
def set_ontology(self, graph_id: str, ontology: Dict[str, Any]): 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 import warnings
from typing import Optional from typing import Optional
from pydantic import Field from pydantic import Field
from zep_cloud.external_clients.ontology import EntityModel, EntityText, EdgeModel from zep_cloud.external_clients.ontology import EntityModel, EntityText, EdgeModel
# 抑制 Pydantic v2 关于 Field(default=None) 的警告 # Ẩn bỏ đi các Warning (Cảnh báo) của thư viện Pydantic v2 liên quan đến Field(default=None)
# 这是 Zep SDK 要求的用法,警告来自动态类创建,可以安全忽略 # 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') 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'} RESERVED_NAMES = {'uuid', 'name', 'group_id', 'name_embedding', 'summary', 'created_at'}
def safe_attr_name(attr_name: str) -> str: 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: if attr_name.lower() in RESERVED_NAMES:
return f"entity_{attr_name}" return f"entity_{attr_name}"
return 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 = {} entity_types = {}
for entity_def in ontology.get("entity_types", []): for entity_def in ontology.get("entity_types", []):
name = entity_def["name"] name = entity_def["name"]
description = entity_def.get("description", f"A {name} entity.") 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} attrs = {"__doc__": description}
annotations = {} annotations = {}
for attr_def in entity_def.get("attributes", []): 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) 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) 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 attrs["__annotations__"] = annotations
# 动态创建类 # Dựng Class ảo
entity_class = type(name, (EntityModel,), attrs) entity_class = type(name, (EntityModel,), attrs)
entity_class.__doc__ = description entity_class.__doc__ = description
entity_types[name] = entity_class 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 = {} edge_definitions = {}
for edge_def in ontology.get("edge_types", []): for edge_def in ontology.get("edge_types", []):
name = edge_def["name"] name = edge_def["name"]
description = edge_def.get("description", f"A {name} relationship.") description = edge_def.get("description", f"A {name} relationship.")
# 创建属性字典和类型注解 # Dọn các attribute dictionary và typing tương tự
attrs = {"__doc__": description} attrs = {"__doc__": description}
annotations = {} annotations = {}
for attr_def in edge_def.get("attributes", []): 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) 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) 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 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('_')) class_name = ''.join(word.capitalize() for word in name.split('_'))
edge_class = type(class_name, (EdgeModel,), attrs) edge_class = type(class_name, (EdgeModel,), attrs)
edge_class.__doc__ = description 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 = [] source_targets = []
for st in edge_def.get("source_targets", []): for st in edge_def.get("source_targets", []):
source_targets.append( source_targets.append(
@ -277,7 +277,7 @@ class GraphBuilderService:
if source_targets: if source_targets:
edge_definitions[name] = (edge_class, 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: if entity_types or edge_definitions:
self.client.graph.set_ontology( self.client.graph.set_ontology(
graph_ids=[graph_id], graph_ids=[graph_id],
@ -292,7 +292,7 @@ class GraphBuilderService:
batch_size: int = 3, batch_size: int = 3,
progress_callback: Optional[Callable] = None progress_callback: Optional[Callable] = None
) -> List[str]: ) -> 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 = [] episode_uuids = []
total_chunks = len(chunks) total_chunks = len(chunks)
@ -304,36 +304,36 @@ class GraphBuilderService:
if progress_callback: if progress_callback:
progress = (i + len(batch_chunks)) / total_chunks progress = (i + len(batch_chunks)) / total_chunks
progress_callback( progress_callback(
f"发送第 {batch_num}/{total_batches} 批数据 ({len(batch_chunks)} 块)...", f"Sending data batch {batch_num}/{total_batches} ({len(batch_chunks)} chunks)...",
progress progress
) )
# 构建episode数据 # Chuẩn bị định dạng gói dữ liệu (Episode data) để tương thích Zep Graph
episodes = [ episodes = [
EpisodeData(data=chunk, type="text") EpisodeData(data=chunk, type="text")
for chunk in batch_chunks for chunk in batch_chunks
] ]
# 发送到Zep # Khởi chạy gửi cho Zep Server
try: try:
batch_result = self.client.graph.add_batch( batch_result = self.client.graph.add_batch(
graph_id=graph_id, graph_id=graph_id,
episodes=episodes 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): if batch_result and isinstance(batch_result, list):
for ep in batch_result: for ep in batch_result:
ep_uuid = getattr(ep, 'uuid_', None) or getattr(ep, 'uuid', None) ep_uuid = getattr(ep, 'uuid_', None) or getattr(ep, 'uuid', None)
if ep_uuid: if ep_uuid:
episode_uuids.append(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) time.sleep(1)
except Exception as e: except Exception as e:
if progress_callback: if progress_callback:
progress_callback(f"批次 {batch_num} 发送失败: {str(e)}", 0) progress_callback(f"Failed to send batch {batch_num}: {str(e)}", 0)
raise raise
return episode_uuids return episode_uuids
@ -344,10 +344,10 @@ class GraphBuilderService:
progress_callback: Optional[Callable] = None, progress_callback: Optional[Callable] = None,
timeout: int = 600 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 not episode_uuids:
if progress_callback: if progress_callback:
progress_callback("无需等待(没有 episode)", 1.0) progress_callback("No episodes to scan (Progress 100%)", 1.0)
return return
start_time = time.time() start_time = time.time()
@ -356,18 +356,19 @@ class GraphBuilderService:
total_episodes = len(episode_uuids) total_episodes = len(episode_uuids)
if progress_callback: 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: 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 time.time() - start_time > timeout:
if progress_callback: if progress_callback:
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 completed_count / total_episodes
) )
break 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): for ep_uuid in list(pending_episodes):
try: try:
episode = self.client.graph.episode.get(uuid_=ep_uuid) episode = self.client.graph.episode.get(uuid_=ep_uuid)
@ -378,31 +379,31 @@ class GraphBuilderService:
completed_count += 1 completed_count += 1
except Exception as e: 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 pass
elapsed = int(time.time() - start_time) elapsed = int(time.time() - start_time)
if progress_callback: if progress_callback:
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 completed_count / total_episodes if total_episodes > 0 else 0
) )
if pending_episodes: if pending_episodes:
time.sleep(3) # 每3秒检查一次 time.sleep(3) # Lặp chu kỳ check mỗi 3 giây
if progress_callback: 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: 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) 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) edges = fetch_all_edges(self.client, graph_id)
# 统计实体类型 # Nối lại và thống kê những Entity Types
entity_types = set() entity_types = set()
for node in nodes: for node in nodes:
if node.labels: if node.labels:
@ -419,25 +420,26 @@ class GraphBuilderService:
def get_graph_data(self, graph_id: str) -> Dict[str, Any]: 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: Args:
graph_id: 图谱ID graph_id: ID của đồ thị
Returns: 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) nodes = fetch_all_nodes(self.client, graph_id)
edges = fetch_all_edges(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 = {} node_map = {}
for node in nodes: for node in nodes:
node_map[node.uuid_] = node.name or "" node_map[node.uuid_] = node.name or ""
nodes_data = [] nodes_data = []
for node in nodes: 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) created_at = getattr(node, 'created_at', None)
if created_at: if created_at:
created_at = str(created_at) created_at = str(created_at)
@ -453,7 +455,7 @@ class GraphBuilderService:
edges_data = [] edges_data = []
for edge in edges: for edge in edges:
# 获取时间信息 # Thu thập các timestamp gắn với cạnh
created_at = getattr(edge, 'created_at', None) created_at = getattr(edge, 'created_at', None)
valid_at = getattr(edge, 'valid_at', None) valid_at = getattr(edge, 'valid_at', None)
invalid_at = getattr(edge, 'invalid_at', None) invalid_at = getattr(edge, 'invalid_at', None)

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
""" """
本体生成服务 Dịch vụ tạo Ontology (Hệ thực thể / Quan hệ)
接口1:分析文本内容,生成适合社会模拟的实体和关系类型定义 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 import json
@ -8,157 +8,157 @@ from typing import Dict, Any, List, Optional
from ..utils.llm_client import LLMClient from ..utils.llm_client import LLMClient
# 本体生成的系统提示词 # System prompt dùng cho việc tự động sinh Ontology
ONTOLOGY_SYSTEM_PROMPT = """你是一个专业的知识图谱本体设计专家。你的任务是分析给定的文本内容和模拟需求,设计适合**社交媒体舆论模拟**的实体类型和关系类型。 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**:
**可以是**: **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ọ)
- 组织机构(大学、协会、NGO、工会等) - 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 ```json
{ {
"entity_types": [ "entity_types": [
{ {
"name": "实体类型名称(英文,PascalCase)", "name": "Tên loại thực thể (Tiếng Anh, PascalCase)",
"description": "简短描述(英文,不超过100字符)", "description": "Mô tả ngắn gọn (Tiếng Anh, tối đa 100 ký tự)",
"attributes": [ "attributes": [
{ {
"name": "属性名(英文,snake_case)", "name": "Tên thuộc tính (Tiếng Anh, snake_case)",
"type": "text", "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": [ "edge_types": [
{ {
"name": "关系类型名称(英文,UPPER_SNAKE_CASE)", "name": "Tên loại quan hệ (Tiếng Anh, UPPER_SNAKE_CASE)",
"description": "简短描述(英文,不超过100字符)", "description": "Mô tả ngắn (Tiếng Anh, tối đa 100 ký tự)",
"source_targets": [ "source_targets": [
{"source": "源实体类型", "target": "目标实体类型"} {"source": "Loại thực thể nguồn", "target": "Loại thực thể đích"}
], ],
"attributes": [] "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个)**: 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`: 任何自然人个体的兜底类型。当一个人不属于其他更具体的人物类型时,归入此类。 - `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`: 任何组织机构的兜底类型。当一个组织不属于其他更具体的组织类型时,归入此类。 - `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个,根据文本内容设计)**: 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.
- 例如:如果文本涉及学术事件,可以有 `Student`, `Professor`, `University` - Ví dụ: Nếu văn bản nói về scandal trường học, có thể có: `Student`, `Professor`, `University`
- 例如:如果文本涉及商业事件,可以有 `Company`, `CEO`, `Employee` - Ví dụ: Nếu văn bản là câu chuyện kinh doanh, có thể có: `Company`, `CEO`, `Employee`
**为什么需要兜底类型**: **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"
- 如果没有专门的类型匹配,他们应该被归入 `Person` - Nếu không có loại được định nghĩa riêng cho họ, họ nên thuộc về loại `Person`
- 同理,小型组织、临时团体等应该归入 `Organization` - 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`
**具体类型的设计原则**: **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.
- description 必须清晰说明这个类型和兜底类型的区别 - 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个 - 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.
- 确保关系的 source_targets 涵盖你定义的实体类型 - Đả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个关键属性 - Mỗi loại thực thể cần 1-3 thuộc tính chính để làm rõ nhân thân.
- **注意**:属性名不能使用 `name`、`uuid`、`group_id`、`created_at`、`summary`(这些是系统保留字) - **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).
- 推荐使用:`full_name`, `title`, `role`, `position`, `location`, `description` 等 - Khuyên dùng: `full_name`, `title`, `role`, `position`, `location`, `description`,...
## 实体类型参考 ## Loại Thực thể tham khảo
**个人类(具体)**: **Loại cá nhân (Cụ thể):**
- Student: 学生 - Student: Học sinh/Sinh viên
- Professor: 教授/学者 - Professor: Giáo sư/Học giả
- Journalist: 记者 - Journalist: Nhà báo/Phóng viên
- Celebrity: 明星/网红 - Celebrity: Người nổi tiếng/Idol
- Executive: 高管 - Executive: Các giám đốc, CEO, cấp lãnh đạo
- Official: 政府官员 - Official: Các vị công chức chính phủ
- Lawyer: 律师 - Lawyer: Luật sư
- Doctor: 医生 - Doctor: Y sĩ/Bác sĩ
**个人类(兜底)**: **Loại cá nhân (Bao quát):**
- Person: 任何自然人(不属于上述具体类型时使用) - 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.
**组织类(具体)**: **Loại tổ chức (Cụ thể):**
- University: 高校 - University: Đại học hoặc học viện
- Company: 公司企业 - Company: Doanh nghiệp hay Công ty, tập đoàn
- GovernmentAgency: 政府机构 - GovernmentAgency: Cơ quan quản lý, các cơ quan ban ngành công quyền
- MediaOutlet: 媒体机构 - MediaOutlet: Truyền thông hay Tạp chí, Đài tin tức
- Hospital: 医院 - Hospital: Bệnh viện / Trung tâm y tế
- School: 中小学 - School: Bậc tiểu/trung học
- NGO: 非政府组织 - NGO: Các loại Tổ chức phi chính phủ hoặc từ thiện
**组织类(兜底)**: **Loại tổ chức (Bao quát):**
- Organization: 任何组织机构(不属于上述具体类型时使用) - 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: 工作于 - WORKS_FOR: Làm việc và ăn lương bởi tổ chức
- STUDIES_AT: 就读于 - STUDIES_AT: Đang học tại nhà trường
- AFFILIATED_WITH: 隶属于 - AFFILIATED_WITH: Liên quan, Trực thuộc vào đơn vị
- REPRESENTS: 代表 - REPRESENTS: Thể hiện tư cách hành động đại diện cho tập thể
- REGULATES: 监管 - REGULATES: Theo dõi, quản lý, thanh tra chính sách
- REPORTS_ON: 报道 - REPORTS_ON: Tác nghiệp báo chí, có tin về hiện tượng
- COMMENTS_ON: 评论 - COMMENTS_ON: Có phản hồi hoặc lên tiếng về tranh cãi
- RESPONDS_TO: 回应 - RESPONDS_TO: Hành động đáp trả
- SUPPORTS: 支持 - SUPPORTS: Theo phe ủng hộ điều luật
- OPPOSES: 反对 - OPPOSES: Phản đối chính sách
- COLLABORATES_WITH: 合作 - COLLABORATES_WITH: Tham gia phối ứng xử lý sự cố.
- COMPETES_WITH: 竞争 - COMPETES_WITH: Quan hệ thù địch.
""" """
class OntologyGenerator: 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): def __init__(self, llm_client: Optional[LLMClient] = None):
@ -171,17 +171,17 @@ class OntologyGenerator:
additional_context: Optional[str] = None additional_context: Optional[str] = None
) -> Dict[str, Any]: ) -> Dict[str, Any]:
""" """
生成本体定义 Khởi tạo định nghĩa Ontology
Args: Args:
document_texts: 文档文本列表 document_texts: Danh sách mảng các văn bản nội dung nguồn
simulation_requirement: 模拟需求描述 simulation_requirement: Chuỗi mô tả nhu cầu mô phỏng của người dùng
additional_context: 额外上下文 additional_context: Văn bản cung cấp thêm các ngữ cảnh phụ (nếu có)
Returns: 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( user_message = self._build_user_message(
document_texts, document_texts,
simulation_requirement, simulation_requirement,
@ -193,19 +193,19 @@ class OntologyGenerator:
{"role": "user", "content": user_message} {"role": "user", "content": user_message}
] ]
# 调用LLM # Gửi request đến LLM
result = self.llm_client.chat_json( result = self.llm_client.chat_json(
messages=messages, messages=messages,
temperature=0.3, temperature=0.3,
max_tokens=4096 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) result = self._validate_and_process(result)
return 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 MAX_TEXT_LENGTH_FOR_LLM = 50000
def _build_user_message( def _build_user_message(
@ -214,50 +214,50 @@ class OntologyGenerator:
simulation_requirement: str, simulation_requirement: str,
additional_context: Optional[str] additional_context: Optional[str]
) -> 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) combined_text = "\n\n---\n\n".join(document_texts)
original_length = len(combined_text) 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: if len(combined_text) > self.MAX_TEXT_LENGTH_FOR_LLM:
combined_text = 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} {simulation_requirement}
## 文档内容 ## Nội dung tài liệu
{combined_text} {combined_text}
""" """
if additional_context: if additional_context:
message += f""" message += f"""
## 额外说明 ## Giải thích bổ sung
{additional_context} {additional_context}
""" """
message += """ message += """
请根据以上内容,设计适合社会舆论模拟的实体类型和关系类型。 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. 必须正好输出10个实体类型 1. Số lượng chính xác: Xuất phải CHUẨN XÁC 10 loại Thực thể
2. 最后2个必须是兜底类型:Person(个人兜底)和 Organization(组织兜底) 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个是根据文本内容设计的具体类型 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. 所有实体类型必须是现实中可以发声的主体,不能是抽象概念 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. 属性名不能使用 name、uuid、group_id 等保留字,用 full_name、org_name 等替代 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 return message
def _validate_and_process(self, result: Dict[str, Any]) -> Dict[str, Any]: 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: if "entity_types" not in result:
result["entity_types"] = [] result["entity_types"] = []
if "edge_types" not in result: if "edge_types" not in result:
@ -265,17 +265,17 @@ class OntologyGenerator:
if "analysis_summary" not in result: if "analysis_summary" not in result:
result["analysis_summary"] = "" result["analysis_summary"] = ""
# 验证实体类型 # Tiền xử lý để loại thực thể hợp lệ
for entity in result["entity_types"]: for entity in result["entity_types"]:
if "attributes" not in entity: if "attributes" not in entity:
entity["attributes"] = [] entity["attributes"] = []
if "examples" not in entity: if "examples" not in entity:
entity["examples"] = [] 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: if len(entity.get("description", "")) > 100:
entity["description"] = entity["description"][:97] + "..." entity["description"] = entity["description"][:97] + "..."
# 验证关系类型 # Tiền xử lý để loại quan hệ hợp lệ
for edge in result["edge_types"]: for edge in result["edge_types"]:
if "source_targets" not in edge: if "source_targets" not in edge:
edge["source_targets"] = [] edge["source_targets"] = []
@ -284,11 +284,11 @@ class OntologyGenerator:
if len(edge.get("description", "")) > 100: if len(edge.get("description", "")) > 100:
edge["description"] = edge["description"][:97] + "..." 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_ENTITY_TYPES = 10
MAX_EDGE_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 = { person_fallback = {
"name": "Person", "name": "Person",
"description": "Any individual person not fitting other specific person types.", "description": "Any individual person not fitting other specific person types.",
@ -309,12 +309,12 @@ class OntologyGenerator:
"examples": ["small business", "community group"] "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"]} entity_names = {e["name"] for e in result["entity_types"]}
has_person = "Person" in entity_names has_person = "Person" in entity_names
has_organization = "Organization" in entity_names has_organization = "Organization" in entity_names
# 需要添加的兜底类型 # Danh sách cần phải gán bù vào
fallbacks_to_add = [] fallbacks_to_add = []
if not has_person: if not has_person:
fallbacks_to_add.append(person_fallback) fallbacks_to_add.append(person_fallback)
@ -325,17 +325,17 @@ class OntologyGenerator:
current_count = len(result["entity_types"]) current_count = len(result["entity_types"])
needed_slots = len(fallbacks_to_add) 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: 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 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] 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) 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: if len(result["entity_types"]) > MAX_ENTITY_TYPES:
result["entity_types"] = result["entity_types"][:MAX_ENTITY_TYPES] result["entity_types"] = result["entity_types"][:MAX_ENTITY_TYPES]
@ -346,29 +346,29 @@ class OntologyGenerator:
def generate_python_code(self, ontology: Dict[str, Any]) -> str: 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: Args:
ontology: 本体定义 ontology: Từ điển định nghĩa Ontology
Returns: Returns:
Python代码字符串 Chuỗi đoạn code File Python cần tạo để lưu
""" """
code_lines = [ code_lines = [
'"""', '"""',
'自定义实体类型定义', 'Các loại đối tượng (Thực thể) tuỳ chỉnh',
'由MiroFish自动生成,用于社会舆论模拟', 'Đượ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 pydantic import Field',
'from zep_cloud.external_clients.ontology import EntityModel, EntityText, EdgeModel', '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", []): for entity in ontology.get("entity_types", []):
name = entity["name"] name = entity["name"]
desc = entity.get("description", f"A {name} entity.") desc = entity.get("description", f"A {name} entity.")
@ -391,13 +391,13 @@ class OntologyGenerator:
code_lines.append('') code_lines.append('')
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('') 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", []): for edge in ontology.get("edge_types", []):
name = edge["name"] 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('_')) class_name = ''.join(word.capitalize() for word in name.split('_'))
desc = edge.get("description", f"A {name} relationship.") desc = edge.get("description", f"A {name} relationship.")
@ -419,8 +419,8 @@ class OntologyGenerator:
code_lines.append('') 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('# ============== 类型配置 ==============') code_lines.append('# ============== Các tuỳ chỉnh Map Cấu Hình ==============')
code_lines.append('') code_lines.append('')
code_lines.append('ENTITY_TYPES = {') code_lines.append('ENTITY_TYPES = {')
for entity in ontology.get("entity_types", []): for entity in ontology.get("entity_types", []):
@ -436,7 +436,7 @@ class OntologyGenerator:
code_lines.append('}') code_lines.append('}')
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 = {') code_lines.append('EDGE_SOURCE_TARGETS = {')
for edge in ontology.get("edge_types", []): for edge in ontology.get("edge_types", []):
name = edge["name"] name = edge["name"]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,11 @@
""" """
模拟IPC通信模块 Module Giao tiếp IPC của Mô phỏng
用于Flask后端和模拟脚本之间的进程间通信 Dùng cho giao tiếp liên tiến trình giữa backend Flask và file script mô phỏng
通过文件系统实现简单的命令/响应模式: 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写入命令到 commands/ 目录 1. Flask ghi lệnh vào thư mục commands/
2. 模拟脚本轮询命令目录,执行命令并写入响应到 responses/ 目录 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轮询响应目录获取结果 3. Flask thăm dò lại thư mục phản hồi để nhận kết quả
""" """
import os import os
@ -23,14 +23,14 @@ logger = get_logger('mirofish.simulation_ipc')
class CommandType(str, Enum): class CommandType(str, Enum):
"""命令类型""" """Các loại lệnh (command)"""
INTERVIEW = "interview" # 单个Agent采访 INTERVIEW = "interview" # Phỏng vấn agent đơn lẻ
BATCH_INTERVIEW = "batch_interview" # 批量采访 BATCH_INTERVIEW = "batch_interview" # Phỏng vấn hàng loạt
CLOSE_ENV = "close_env" # 关闭环境 CLOSE_ENV = "close_env" # Đóng môi trường
class CommandStatus(str, Enum): class CommandStatus(str, Enum):
"""命令状态""" """Trạng thái của các lệnh (command)"""
PENDING = "pending" PENDING = "pending"
PROCESSING = "processing" PROCESSING = "processing"
COMPLETED = "completed" COMPLETED = "completed"
@ -39,7 +39,7 @@ class CommandStatus(str, Enum):
@dataclass @dataclass
class IPCCommand: class IPCCommand:
"""IPC命令""" """Lệnh (Command) IPC"""
command_id: str command_id: str
command_type: CommandType command_type: CommandType
args: Dict[str, Any] args: Dict[str, Any]
@ -65,7 +65,7 @@ class IPCCommand:
@dataclass @dataclass
class IPCResponse: class IPCResponse:
"""IPC响应""" """Phản hồi (Response) IPC"""
command_id: str command_id: str
status: CommandStatus status: CommandStatus
result: Optional[Dict[str, Any]] = None result: Optional[Dict[str, Any]] = None
@ -94,23 +94,23 @@ class IPCResponse:
class SimulationIPCClient: 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): def __init__(self, simulation_dir: str):
""" """
初始化IPC客户端 Khởi tạo Client IPC
Args: Args:
simulation_dir: 模拟数据目录 simulation_dir: Thư mục chứa dữ liệu mô phỏng
""" """
self.simulation_dir = simulation_dir self.simulation_dir = simulation_dir
self.commands_dir = os.path.join(simulation_dir, "ipc_commands") self.commands_dir = os.path.join(simulation_dir, "ipc_commands")
self.responses_dir = os.path.join(simulation_dir, "ipc_responses") 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.commands_dir, exist_ok=True)
os.makedirs(self.responses_dir, exist_ok=True) os.makedirs(self.responses_dir, exist_ok=True)
@ -122,19 +122,19 @@ class SimulationIPCClient:
poll_interval: float = 0.5 poll_interval: float = 0.5
) -> IPCResponse: ) -> IPCResponse:
""" """
发送命令并等待响应 Gửi lệnh ra và đợi kết quả phản hồi lại
Args: Args:
command_type: 命令类型 command_type: Loại lệnh
args: 命令参数 args: Tham số của lệnh
timeout: 超时时间(秒) timeout: Thời gian timeout (giây)
poll_interval: 轮询间隔(秒) poll_interval: Khoảng thời gian giữa các lần thăm dò (giây)
Returns: Returns:
IPCResponse IPCResponse
Raises: Raises:
TimeoutError: 等待响应超时 TimeoutError: Lỗi quá thời gian chờ phản hồi
""" """
command_id = str(uuid.uuid4()) command_id = str(uuid.uuid4())
command = IPCCommand( command = IPCCommand(
@ -143,14 +143,14 @@ class SimulationIPCClient:
args=args args=args
) )
# 写入命令文件 # Ghi vào file lệnh
command_file = os.path.join(self.commands_dir, f"{command_id}.json") command_file = os.path.join(self.commands_dir, f"{command_id}.json")
with open(command_file, 'w', encoding='utf-8') as f: with open(command_file, 'w', encoding='utf-8') as f:
json.dump(command.to_dict(), f, ensure_ascii=False, indent=2) 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") response_file = os.path.join(self.responses_dir, f"{command_id}.json")
start_time = time.time() start_time = time.time()
@ -161,30 +161,30 @@ class SimulationIPCClient:
response_data = json.load(f) response_data = json.load(f)
response = IPCResponse.from_dict(response_data) response = IPCResponse.from_dict(response_data)
# 清理命令和响应文件 # Xóa file lệnh và file phản hồi đi
try: try:
os.remove(command_file) os.remove(command_file)
os.remove(response_file) os.remove(response_file)
except OSError: except OSError:
pass 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 return response
except (json.JSONDecodeError, KeyError) as e: except (json.JSONDecodeError, KeyError) as e:
logger.warning(f"解析响应失败: {e}") logger.warning(f"Failed to parse response: {e}")
time.sleep(poll_interval) time.sleep(poll_interval)
# 超时 # Timed out
logger.error(f"等待IPC响应超时: command_id={command_id}") logger.error(f"Timeout waiting for IPC response: command_id={command_id}")
# 清理命令文件 # Xóa file lệnh đi
try: try:
os.remove(command_file) os.remove(command_file)
except OSError: except OSError:
pass pass
raise TimeoutError(f"等待命令响应超时 ({timeout}秒)") raise TimeoutError(f"Wait command response timed out ({timeout} seconds)")
def send_interview( def send_interview(
self, self,
@ -194,19 +194,19 @@ class SimulationIPCClient:
timeout: float = 60.0 timeout: float = 60.0
) -> IPCResponse: ) -> IPCResponse:
""" """
发送单个Agent采访命令 Gửi lệnh phỏng vấn agent đơn lẻ
Args: Args:
agent_id: Agent ID agent_id: Agent ID
prompt: 采访问题 prompt: Câu hỏi phỏng vấn
platform: 指定平台(可选) platform: Chỉ định nền tảng (Tùy chọn)
- "twitter": 只采访Twitter平台 - "twitter": Chỉ phỏng vấn ở nền tảng Twitter
- "reddit": 只采访Reddit平台 - "reddit": Chỉ phỏng vấn ở nền tảng Reddit
- None: 双平台模拟时同时采访两个平台,单平台模拟时采访该平台 - 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: 超时时间 timeout: Thời gian timeout
Returns: Returns:
IPCResponse,result字段包含采访结果 IPCResponse, trong đó trường result sẽ chứa kết quả cuộc phỏng vấn
""" """
args = { args = {
"agent_id": agent_id, "agent_id": agent_id,
@ -228,18 +228,18 @@ class SimulationIPCClient:
timeout: float = 120.0 timeout: float = 120.0
) -> IPCResponse: ) -> IPCResponse:
""" """
发送批量采访命令 Gửi lệnh phỏng vấn hàng loạt
Args: Args:
interviews: 采访列表,每个元素包含 {"agent_id": int, "prompt": str, "platform": str(可选)} 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: 默认平台(可选,会被每个采访项的platform覆盖) 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": 默认只采访Twitter平台 - "twitter": Mặc định chỉ phỏng vấn ở nền tảng Twitter
- "reddit": 默认只采访Reddit平台 - "reddit": Mặc định chỉ phỏng vấn ở nền tảng Reddit
- None: 双平台模拟时每个Agent同时采访两个平台 - 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: 超时时间 timeout: Thời gian timeout
Returns: 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} args = {"interviews": interviews}
if platform: if platform:
@ -253,10 +253,10 @@ class SimulationIPCClient:
def send_close_env(self, timeout: float = 30.0) -> IPCResponse: def send_close_env(self, timeout: float = 30.0) -> IPCResponse:
""" """
发送关闭环境命令 Gửi lệnh đóng môi trường
Args: Args:
timeout: 超时时间 timeout: Thời gian timeout
Returns: Returns:
IPCResponse IPCResponse
@ -269,9 +269,9 @@ class SimulationIPCClient:
def check_env_alive(self) -> bool: 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") status_file = os.path.join(self.simulation_dir, "env_status.json")
if not os.path.exists(status_file): if not os.path.exists(status_file):
@ -287,41 +287,41 @@ class SimulationIPCClient:
class SimulationIPCServer: 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): def __init__(self, simulation_dir: str):
""" """
初始化IPC服务器 Khởi tạo Máy chủ IPC
Args: Args:
simulation_dir: 模拟数据目录 simulation_dir: Thư mục chứa dữ liệu mô phỏng
""" """
self.simulation_dir = simulation_dir self.simulation_dir = simulation_dir
self.commands_dir = os.path.join(simulation_dir, "ipc_commands") self.commands_dir = os.path.join(simulation_dir, "ipc_commands")
self.responses_dir = os.path.join(simulation_dir, "ipc_responses") 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.commands_dir, exist_ok=True)
os.makedirs(self.responses_dir, exist_ok=True) os.makedirs(self.responses_dir, exist_ok=True)
# 环境状态 # Trạng thái môi trường
self._running = False self._running = False
def start(self): def start(self):
"""标记服务器为运行状态""" """Đánh dấu Máy chủ đang ở trạng thái chạy"""
self._running = True self._running = True
self._update_env_status("alive") self._update_env_status("alive")
def stop(self): def stop(self):
"""标记服务器为停止状态""" """Đánh dấu Máy chủ đang ở trạng thái dừng"""
self._running = False self._running = False
self._update_env_status("stopped") self._update_env_status("stopped")
def _update_env_status(self, status: str): 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") status_file = os.path.join(self.simulation_dir, "env_status.json")
with open(status_file, 'w', encoding='utf-8') as f: with open(status_file, 'w', encoding='utf-8') as f:
json.dump({ json.dump({
@ -331,15 +331,15 @@ class SimulationIPCServer:
def poll_commands(self) -> Optional[IPCCommand]: 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: Returns:
IPCCommand 或 None IPCCommand hoặc None
""" """
if not os.path.exists(self.commands_dir): if not os.path.exists(self.commands_dir):
return None return None
# 按时间排序获取命令文件 # Lấy danh sách file lệnh và sắp xếp theo thời gian
command_files = [] command_files = []
for filename in os.listdir(self.commands_dir): for filename in os.listdir(self.commands_dir):
if filename.endswith('.json'): if filename.endswith('.json'):
@ -354,23 +354,23 @@ class SimulationIPCServer:
data = json.load(f) data = json.load(f)
return IPCCommand.from_dict(data) return IPCCommand.from_dict(data)
except (json.JSONDecodeError, KeyError, OSError) as e: except (json.JSONDecodeError, KeyError, OSError) as e:
logger.warning(f"读取命令文件失败: {filepath}, {e}") logger.warning(f"Failed to read command file: {filepath}, {e}")
continue continue
return None return None
def send_response(self, response: IPCResponse): def send_response(self, response: IPCResponse):
""" """
发送响应 Gửi phản hồi
Args: Args:
response: IPC响应 response: Phản hồi IPC
""" """
response_file = os.path.join(self.responses_dir, f"{response.command_id}.json") response_file = os.path.join(self.responses_dir, f"{response.command_id}.json")
with open(response_file, 'w', encoding='utf-8') as f: with open(response_file, 'w', encoding='utf-8') as f:
json.dump(response.to_dict(), f, ensure_ascii=False, indent=2) 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") command_file = os.path.join(self.commands_dir, f"{response.command_id}.json")
try: try:
os.remove(command_file) os.remove(command_file)
@ -378,7 +378,7 @@ class SimulationIPCServer:
pass pass
def send_success(self, command_id: str, result: Dict[str, Any]): 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( self.send_response(IPCResponse(
command_id=command_id, command_id=command_id,
status=CommandStatus.COMPLETED, status=CommandStatus.COMPLETED,
@ -386,7 +386,7 @@ class SimulationIPCServer:
)) ))
def send_error(self, command_id: str, error: str): def send_error(self, command_id: str, error: str):
"""发送错误响应""" """Gửi phản hồi lỗi"""
self.send_response(IPCResponse( self.send_response(IPCResponse(
command_id=command_id, command_id=command_id,
status=CommandStatus.FAILED, status=CommandStatus.FAILED,

View File

@ -1,7 +1,7 @@
""" """
OASIS模拟管理器 Trình Quản lý Mô Phỏng OASIS
管理Twitter和Reddit双平台并行模拟 Đả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.
使用预设脚本 + LLM智能生成配置参数 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 import os
@ -22,60 +22,60 @@ logger = get_logger('mirofish.simulation')
class SimulationStatus(str, Enum): class SimulationStatus(str, Enum):
"""模拟状态""" """Trạng thái hiện tại của quá trình mô phỏng"""
CREATED = "created" CREATED = "created" # Đã khởi tạo
PREPARING = "preparing" PREPARING = "preparing" # Đang chuẩn bị (chuẩn bị dữ liệu/profile)
READY = "ready" READY = "ready" # Đã sẵn sàng chạy
RUNNING = "running" RUNNING = "running" # Đang xử lý giả lập
PAUSED = "paused" PAUSED = "paused" # Tạm dừng
STOPPED = "stopped" # 模拟被手动停止 STOPPED = "stopped" # Hệ thống mô phỏng bị người dùng chủ động dừng lại
COMPLETED = "completed" # 模拟自然完成 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" FAILED = "failed" # Bị lỗi hệ thống gián đoạn
class PlatformType(str, Enum): class PlatformType(str, Enum):
"""平台类型""" """Phân loại nền tảng giả lập Mạng xã hội"""
TWITTER = "twitter" TWITTER = "twitter"
REDDIT = "reddit" REDDIT = "reddit"
@dataclass @dataclass
class SimulationState: 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 simulation_id: str
project_id: str project_id: str
graph_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_twitter: bool = True
enable_reddit: bool = True enable_reddit: bool = True
# 状态 # Current status
status: SimulationStatus = SimulationStatus.CREATED status: SimulationStatus = SimulationStatus.CREATED
# 准备阶段数据 # Dữ liệu thu thập / thống kê của Preparing Phase
entities_count: int = 0 entities_count: int = 0
profiles_count: int = 0 profiles_count: int = 0
entity_types: List[str] = field(default_factory=list) 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_generated: bool = False
config_reasoning: str = "" config_reasoning: str = ""
# 运行时数据 # Dữ liệu cập nhật theo thời gian thực (Runtime Phase)
current_round: int = 0 current_round: int = 0
twitter_status: str = "not_started" twitter_status: str = "not_started"
reddit_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()) created_at: str = field(default_factory=lambda: datetime.now().isoformat())
updated_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 error: Optional[str] = None
def to_dict(self) -> Dict[str, Any]: 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 { return {
"simulation_id": self.simulation_id, "simulation_id": self.simulation_id,
"project_id": self.project_id, "project_id": self.project_id,
@ -97,7 +97,7 @@ class SimulationState:
} }
def to_simple_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 { return {
"simulation_id": self.simulation_id, "simulation_id": self.simulation_id,
"project_id": self.project_id, "project_id": self.project_id,
@ -113,36 +113,36 @@ class SimulationState:
class SimulationManager: class SimulationManager:
""" """
模拟管理器 Kịch bản Quản lý trung tâm của tính năng Mô Phỏng
核心功能: Luồng thiết lập cốt lõi:
1. 从Zep图谱读取实体并过滤 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. 生成OASIS Agent Profile 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. 使用LLM智能生成模拟配置参数 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. 准备预设脚本所需的所有文件 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( SIMULATION_DATA_DIR = os.path.join(
os.path.dirname(__file__), os.path.dirname(__file__),
'../../uploads/simulations' '../../uploads/simulations'
) )
def __init__(self): def __init__(self):
# 确保目录存在 # Đảm bảo môi trường file data đã được set up
os.makedirs(self.SIMULATION_DATA_DIR, exist_ok=True) 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] = {} self._simulations: Dict[str, SimulationState] = {}
def _get_simulation_dir(self, simulation_id: str) -> str: 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) sim_dir = os.path.join(self.SIMULATION_DATA_DIR, simulation_id)
os.makedirs(sim_dir, exist_ok=True) os.makedirs(sim_dir, exist_ok=True)
return sim_dir return sim_dir
def _save_simulation_state(self, state: SimulationState): 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) sim_dir = self._get_simulation_dir(state.simulation_id)
state_file = os.path.join(sim_dir, "state.json") state_file = os.path.join(sim_dir, "state.json")
@ -154,7 +154,7 @@ class SimulationManager:
self._simulations[state.simulation_id] = state self._simulations[state.simulation_id] = state
def _load_simulation_state(self, simulation_id: str) -> Optional[SimulationState]: 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: if simulation_id in self._simulations:
return self._simulations[simulation_id] return self._simulations[simulation_id]
@ -198,16 +198,16 @@ class SimulationManager:
enable_reddit: bool = True, enable_reddit: bool = True,
) -> SimulationState: ) -> SimulationState:
""" """
创建新的模拟 Khởi tạo môi trường ảo / mới cho Mô Phỏng
Args: Args:
project_id: 项目ID project_id: Mã ID của Project (Quản lý cấp đầu vào)
graph_id: Zep图谱ID graph_id: Đồ thị ID tương ứng lấy bên Zep
enable_twitter: 是否启用Twitter模拟 enable_twitter: Công tắc (Bật/Tắt) luồng giả lập Twitter
enable_reddit: 是否启用Reddit模拟 enable_reddit: Công tắc (Bật/Tắt) luồng giả lập Reddit
Returns: Returns:
SimulationState Đối tượng Class SimulationState
""" """
import uuid import uuid
simulation_id = f"sim_{uuid.uuid4().hex[:12]}" simulation_id = f"sim_{uuid.uuid4().hex[:12]}"
@ -222,7 +222,7 @@ class SimulationManager:
) )
self._save_simulation_state(state) 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 return state
@ -237,30 +237,30 @@ class SimulationManager:
parallel_profile_count: int = 3 parallel_profile_count: int = 3
) -> SimulationState: ) -> SimulationState:
""" """
准备模拟环境(全程自动化) Giai đoạn chuẩn bị dữ liệu tạo giả lập mô phỏng (Tiến trình Automation 100%)
步骤: Các bước diễn ra:
1. 从Zep图谱读取并过滤实体 1. Gọi lấy các cụm Entity (thực thể) và bộ lọt (filter) từ Zep Graph API
2. 为每个实体生成OASIS Agent Profile(可选LLM增强,支持并行) 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. 使用LLM智能生成模拟配置参数(时间、活跃度、发言频率等) 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. 保存配置文件和Profile文件 4. In ra các file cấu hình và JSON của profile để hệ thống dễ đọc
5. 复制预设脚本到模拟目录 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: Args:
simulation_id: 模拟ID simulation_id: Mã ID của chu trình giả lập
simulation_requirement: 模拟需求描述(用于LLM生成配置) 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: 原始文档内容(用于LLM理解背景) 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: 预定义的实体类型(可选) defined_entity_types: Dánh sách các Entity Model có sẵn do Zep định nghĩa (Option)
use_llm_for_profiles: 是否使用LLM生成详细人设 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: 进度回调函数 (stage, progress, message) progress_callback: Hàm callback update log progress (chuyển về màn hình Frontend view) format (stage, progress, message)
parallel_profile_count: 并行生成人设的数量,默认3 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: Returns:
SimulationState Class cài đặt Data - SimulationState
""" """
state = self._load_simulation_state(simulation_id) state = self._load_simulation_state(simulation_id)
if not state: 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: try:
state.status = SimulationStatus.PREPARING state.status = SimulationStatus.PREPARING
@ -268,14 +268,14 @@ class SimulationManager:
sim_dir = self._get_simulation_dir(simulation_id) 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: if progress_callback:
progress_callback("reading", 0, "正在连接Zep图谱...") progress_callback("reading", 0, "Connecting to Zep Graph data store...")
reader = ZepEntityReader() reader = ZepEntityReader()
if progress_callback: if progress_callback:
progress_callback("reading", 30, "正在读取节点数据...") progress_callback("reading", 30, "Extracting Node data from graph...")
filtered = reader.filter_defined_entities( filtered = reader.filter_defined_entities(
graph_id=state.graph_id, graph_id=state.graph_id,
@ -289,29 +289,29 @@ class SimulationManager:
if progress_callback: if progress_callback:
progress_callback( progress_callback(
"reading", 100, "reading", 100,
f"完成,共 {filtered.filtered_count} 个实体", f"Extraction complete, filtered entity count: {filtered.filtered_count} empty entities",
current=filtered.filtered_count, current=filtered.filtered_count,
total=filtered.filtered_count total=filtered.filtered_count
) )
if filtered.filtered_count == 0: if filtered.filtered_count == 0:
state.status = SimulationStatus.FAILED 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) self._save_simulation_state(state)
return state return state
# ========== 阶段2: 生成Agent Profile ========== # ========== Giai đoạn 2: Bắt đầu sinh Agent Profiles cho OASIS ==========
total_entities = len(filtered.entities) total_entities = len(filtered.entities)
if progress_callback: if progress_callback:
progress_callback( progress_callback(
"generating_profiles", 0, "generating_profiles", 0,
"开始生成...", "Ready for AI Generation process...",
current=0, current=0,
total=total_entities 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) generator = OasisProfileGenerator(graph_id=state.graph_id)
def profile_progress(current, total, msg): def profile_progress(current, total, msg):
@ -325,7 +325,7 @@ class SimulationManager:
item_name=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_output_path = None
realtime_platform = "reddit" realtime_platform = "reddit"
if state.enable_reddit: if state.enable_reddit:
@ -339,20 +339,20 @@ class SimulationManager:
entities=filtered.entities, entities=filtered.entities,
use_llm=use_llm_for_profiles, use_llm=use_llm_for_profiles,
progress_callback=profile_progress, progress_callback=profile_progress,
graph_id=state.graph_id, # 传入graph_id用于Zep检索 graph_id=state.graph_id, # Để tìm kiếm Zep Search Index
parallel_count=parallel_profile_count, # 并行生成数量 parallel_count=parallel_profile_count, # Số dòng luồng Async
realtime_output_path=realtime_output_path, # 实时保存路径 realtime_output_path=realtime_output_path, # Lưu log thời gian thực
output_platform=realtime_platform # 输出格式 output_platform=realtime_platform # Đuôi file xuất
) )
state.profiles_count = len(profiles) state.profiles_count = len(profiles)
# 保存Profile文件(注意:Twitter使用CSV格式,Reddit使用JSON格式) # 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 已经在生成过程中实时保存了,这里再保存一次确保完整性 # Reddit đã được render đồng thời ở block trên nhưng đây là re-save toàn bộ
if progress_callback: if progress_callback:
progress_callback( progress_callback(
"generating_profiles", 95, "generating_profiles", 95,
"保存Profile文件...", "Compressing Profile data...",
current=total_entities, current=total_entities,
total=total_entities total=total_entities
) )
@ -365,7 +365,7 @@ class SimulationManager:
) )
if state.enable_twitter: 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( generator.save_profiles(
profiles=profiles, profiles=profiles,
file_path=os.path.join(sim_dir, "twitter_profiles.csv"), file_path=os.path.join(sim_dir, "twitter_profiles.csv"),
@ -375,16 +375,16 @@ class SimulationManager:
if progress_callback: if progress_callback:
progress_callback( progress_callback(
"generating_profiles", 100, "generating_profiles", 100,
f"完成,共 {len(profiles)} 个Profile", f"Done, created {len(profiles)} Profiles",
current=len(profiles), current=len(profiles),
total=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: if progress_callback:
progress_callback( progress_callback(
"generating_config", 0, "generating_config", 0,
"正在分析模拟需求...", "Analyzing input requirements...",
current=0, current=0,
total=3 total=3
) )
@ -394,7 +394,7 @@ class SimulationManager:
if progress_callback: if progress_callback:
progress_callback( progress_callback(
"generating_config", 30, "generating_config", 30,
"正在调用LLM生成配置...", "LLM Bot is generating configuration...",
current=1, current=1,
total=3 total=3
) )
@ -413,12 +413,12 @@ class SimulationManager:
if progress_callback: if progress_callback:
progress_callback( progress_callback(
"generating_config", 70, "generating_config", 70,
"正在保存配置文件...", "Saving Config parameters...",
current=2, current=2,
total=3 total=3
) )
# 保存配置文件 # Lưu file cứng simulation_config.json
config_path = os.path.join(sim_dir, "simulation_config.json") config_path = os.path.join(sim_dir, "simulation_config.json")
with open(config_path, 'w', encoding='utf-8') as f: with open(config_path, 'w', encoding='utf-8') as f:
f.write(sim_params.to_json()) f.write(sim_params.to_json())
@ -429,25 +429,25 @@ class SimulationManager:
if progress_callback: if progress_callback:
progress_callback( progress_callback(
"generating_config", 100, "generating_config", 100,
"配置生成完成", "Configuration Generation complete",
current=3, current=3,
total=3 total=3
) )
# 注意:运行脚本保留在 backend/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
# 启动模拟时,simulation_runner 会从 scripts/ 目录运行脚本 # 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 state.status = SimulationStatus.READY
self._save_simulation_state(state) self._save_simulation_state(state)
logger.info(f"模拟准备完成: {simulation_id}, " logger.info(f"Finished simulation preparation phase for ID: {simulation_id}, "
f"entities={state.entities_count}, profiles={state.profiles_count}") f"Total entities={state.entities_count}, Created profiles={state.profiles_count}")
return state return state
except Exception as e: 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 import traceback
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
state.status = SimulationStatus.FAILED state.status = SimulationStatus.FAILED
@ -456,16 +456,16 @@ class SimulationManager:
raise raise
def get_simulation(self, simulation_id: str) -> Optional[SimulationState]: 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) return self._load_simulation_state(simulation_id)
def list_simulations(self, project_id: Optional[str] = None) -> List[SimulationState]: 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 = [] simulations = []
if os.path.exists(self.SIMULATION_DATA_DIR): if os.path.exists(self.SIMULATION_DATA_DIR):
for sim_id in os.listdir(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) sim_path = os.path.join(self.SIMULATION_DATA_DIR, sim_id)
if sim_id.startswith('.') or not os.path.isdir(sim_path): if sim_id.startswith('.') or not os.path.isdir(sim_path):
continue continue
@ -478,10 +478,10 @@ class SimulationManager:
return simulations return simulations
def get_profiles(self, simulation_id: str, platform: str = "reddit") -> List[Dict[str, Any]]: 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) state = self._load_simulation_state(simulation_id)
if not state: 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) sim_dir = self._get_simulation_dir(simulation_id)
profile_path = os.path.join(sim_dir, f"{platform}_profiles.json") profile_path = os.path.join(sim_dir, f"{platform}_profiles.json")
@ -493,7 +493,7 @@ class SimulationManager:
return json.load(f) return json.load(f)
def get_simulation_config(self, simulation_id: str) -> Optional[Dict[str, Any]]: 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) sim_dir = self._get_simulation_dir(simulation_id)
config_path = os.path.join(sim_dir, "simulation_config.json") config_path = os.path.join(sim_dir, "simulation_config.json")
@ -504,7 +504,7 @@ class SimulationManager:
return json.load(f) return json.load(f)
def get_run_instructions(self, simulation_id: str) -> Dict[str, str]: 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) sim_dir = self._get_simulation_dir(simulation_id)
config_path = os.path.join(sim_dir, "simulation_config.json") config_path = os.path.join(sim_dir, "simulation_config.json")
scripts_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../scripts')) scripts_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../scripts'))
@ -519,10 +519,10 @@ class SimulationManager:
"parallel": f"python {scripts_dir}/run_parallel_simulation.py --config {config_path}", "parallel": f"python {scripts_dir}/run_parallel_simulation.py --config {config_path}",
}, },
"instructions": ( "instructions": (
f"1. 激活conda环境: conda activate MiroFish\n" 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. 运行模拟 (脚本位于 {scripts_dir}):\n" f"2. Bắt đầu Run giả lập (Scripts gốc được gọi ra tại {scripts_dir}):\n"
f" - 单独运行Twitter: python {scripts_dir}/run_twitter_simulation.py --config {config_path}\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" - 单独运行Reddit: python {scripts_dir}/run_reddit_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" - 并行运行双平台: python {scripts_dir}/run_parallel_simulation.py --config {config_path}" f" - Chạy giả lập cả hai phân luồng song song: python {scripts_dir}/run_parallel_simulation.py --config {config_path}"
) )
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
""" """
文本处理服务 Dịch vụ xử lý văn bản (Text processing service)
""" """
from typing import List, Optional from typing import List, Optional
@ -7,11 +7,11 @@ from ..utils.file_parser import FileParser, split_text_into_chunks
class TextProcessor: class TextProcessor:
"""文本处理器""" """Trình xử lý văn bản"""
@staticmethod @staticmethod
def extract_from_files(file_paths: List[str]) -> str: 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) return FileParser.extract_from_multiple(file_paths)
@staticmethod @staticmethod
@ -21,37 +21,37 @@ class TextProcessor:
overlap: int = 50 overlap: int = 50
) -> List[str]: ) -> 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: Args:
text: 原始文本 text: Văn bản gốc cần chia
chunk_size: 块大小 chunk_size: Kích thước tối đa của mỗi đoạn (chunk)
overlap: 重叠大小 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: 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) return split_text_into_chunks(text, chunk_size, overlap)
@staticmethod @staticmethod
def preprocess_text(text: str) -> str: 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: Args:
text: 原始文本 text: Văn bản thô ban đầu
Returns: Returns:
处理后的文本 Văn bản đã được tinh giản, làm sạch
""" """
import re 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') 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) text = re.sub(r'\n{3,}', '\n\n', text)
# 移除行首行尾空白 # 移除行首行尾空白
@ -62,7 +62,7 @@ class TextProcessor:
@staticmethod @staticmethod
def get_text_stats(text: str) -> dict: 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 { return {
"total_chars": len(text), "total_chars": len(text),
"total_lines": text.count('\n') + 1, "total_lines": text.count('\n') + 1,

View File

@ -1,6 +1,6 @@
""" """
Zep实体读取与过滤服务 Dịch vụ đọc và lọc thực thể Zep
从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 import time
@ -15,21 +15,21 @@ from ..utils.zep_paging import fetch_all_nodes, fetch_all_edges
logger = get_logger('mirofish.zep_entity_reader') logger = get_logger('mirofish.zep_entity_reader')
# 用于泛型返回类型 # Dùng cho các kiểu trả về generic
T = TypeVar('T') T = TypeVar('T')
@dataclass @dataclass
class EntityNode: class EntityNode:
"""实体节点数据结构""" """Cấu trúc dữ liệu của node thực thể"""
uuid: str uuid: str
name: str name: str
labels: List[str] labels: List[str]
summary: str summary: str
attributes: Dict[str, Any] attributes: Dict[str, Any]
# 相关的边信息 # Thông tin edge liên quan
related_edges: List[Dict[str, Any]] = field(default_factory=list) 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) related_nodes: List[Dict[str, Any]] = field(default_factory=list)
def to_dict(self) -> Dict[str, Any]: def to_dict(self) -> Dict[str, Any]:
@ -44,7 +44,7 @@ class EntityNode:
} }
def get_entity_type(self) -> Optional[str]: 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: for label in self.labels:
if label not in ["Entity", "Node"]: if label not in ["Entity", "Node"]:
return label return label
@ -53,7 +53,7 @@ class EntityNode:
@dataclass @dataclass
class FilteredEntities: class FilteredEntities:
"""过滤后的实体集合""" """Tập hợp các thực thể sau khi lọc"""
entities: List[EntityNode] entities: List[EntityNode]
entity_types: Set[str] entity_types: Set[str]
total_count: int total_count: int
@ -70,18 +70,18 @@ class FilteredEntities:
class ZepEntityReader: class ZepEntityReader:
""" """
Zep实体读取与过滤服务 Dịch vụ đọc và lọc thực thể Zep
主要功能: Chức năng chính:
1. 从Zep图谱读取所有节点 1. Đọc toàn bộ các node từ đồ thị Zep
2. 筛选出符合预定义实体类型的节点(Labels不只是Entity的节点) 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. 获取每个实体的相关边和关联节点信息 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): def __init__(self, api_key: Optional[str] = None):
self.api_key = api_key or Config.ZEP_API_KEY self.api_key = api_key or Config.ZEP_API_KEY
if not self.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) self.client = Zep(api_key=self.api_key)
@ -93,16 +93,16 @@ class ZepEntityReader:
initial_delay: float = 2.0 initial_delay: float = 2.0
) -> T: ) -> T:
""" """
带重试机制的Zep API调用 Gọi hàm Zep API có cơ chế thử lại (retry)
Args: Args:
func: 要执行的函数(无参数的lambda或callable) func: Hàm cần thực thi (lambda không tham số hoặc callable)
operation_name: 操作名称,用于日志 operation_name: Tên thao tác, dùng cho log
max_retries: 最大重试次数(默认3次,即最多尝试3次) 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: 初始延迟秒数 initial_delay: Số giây trì hoãn ban đầu
Returns: Returns:
API调用结果 Kết quả của lệnh gọi API
""" """
last_exception = None last_exception = None
delay = initial_delay delay = initial_delay
@ -114,27 +114,27 @@ class ZepEntityReader:
last_exception = e last_exception = e
if attempt < max_retries - 1: if attempt < max_retries - 1:
logger.warning( logger.warning(
f"Zep {operation_name} 第 {attempt + 1} 次尝试失败: {str(e)[:100]}, " f"Zep {operation_name} attempt {attempt + 1} failed: {str(e)[:100]}, "
f"{delay:.1f}秒后重试..." f"retrying in {delay:.1f} seconds..."
) )
time.sleep(delay) time.sleep(delay)
delay *= 2 # 指数退避 delay *= 2 # Lùi bước nhịp mũ (Exponential backoff)
else: 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 raise last_exception
def get_all_nodes(self, graph_id: str) -> List[Dict[str, Any]]: 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: Args:
graph_id: 图谱ID graph_id: ID của đồ thị
Returns: 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) nodes = fetch_all_nodes(self.client, graph_id)
@ -148,20 +148,20 @@ class ZepEntityReader:
"attributes": node.attributes or {}, "attributes": node.attributes or {},
}) })
logger.info(f"共获取 {len(nodes_data)} 个节点") logger.info(f"Total {len(nodes_data)} nodes fetched")
return nodes_data return nodes_data
def get_all_edges(self, graph_id: str) -> List[Dict[str, Any]]: 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: Args:
graph_id: 图谱ID graph_id: ID của đồ thị
Returns: 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) edges = fetch_all_edges(self.client, graph_id)
@ -176,24 +176,24 @@ class ZepEntityReader:
"attributes": edge.attributes or {}, "attributes": edge.attributes or {},
}) })
logger.info(f"共获取 {len(edges_data)} 条边") logger.info(f"Total {len(edges_data)} edges fetched")
return edges_data return edges_data
def get_node_edges(self, node_uuid: str) -> List[Dict[str, Any]]: 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: Args:
node_uuid: 节点UUID node_uuid: UUID của node
Returns: Returns:
边列表 Danh sách edge
""" """
try: try:
# 使用重试机制调用Zep API # Sử dụng cơ chế thử lại để gọi Zep API
edges = self._call_with_retry( edges = self._call_with_retry(
func=lambda: self.client.graph.node.get_entity_edges(node_uuid=node_uuid), 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 = [] edges_data = []
@ -209,7 +209,7 @@ class ZepEntityReader:
return edges_data return edges_data
except Exception as e: 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 [] return []
def filter_defined_entities( def filter_defined_entities(
@ -219,47 +219,47 @@ class ZepEntityReader:
enrich_with_edges: bool = True enrich_with_edges: bool = True
) -> FilteredEntities: ) -> FilteredEntities:
""" """
筛选出符合预定义实体类型的节点 Lọc ra các node phù hợp với các loại thực thể đã được định nghĩa
筛选逻辑: Logic lọc:
- 如果节点的Labels只有一个"Entity",说明这个实体不符合我们预定义的类型,跳过 - 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
- 如果节点的Labels包含除"Entity"和"Node"之外的标签,说明符合预定义类型,保留 - 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: Args:
graph_id: 图谱ID graph_id: ID của đồ thị
defined_entity_types: 预定义的实体类型列表(可选,如果提供则只保留这些类型) 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: 是否获取每个实体的相关边信息 enrich_with_edges: Có lấy thông tin edge liên quan của từng thực thể hay không
Returns: 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) all_nodes = self.get_all_nodes(graph_id)
total_count = len(all_nodes) 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 [] 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} 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 = [] filtered_entities = []
entity_types_found = set() entity_types_found = set()
for node in all_nodes: for node in all_nodes:
labels = node.get("labels", []) 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"]] custom_labels = [l for l in labels if l not in ["Entity", "Node"]]
if not custom_labels: if not custom_labels:
# 只有默认标签,跳过 # Chỉ có nhãn mặc định, bỏ qua
continue 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: if defined_entity_types:
matching_labels = [l for l in custom_labels if l in defined_entity_types] matching_labels = [l for l in custom_labels if l in defined_entity_types]
if not matching_labels: if not matching_labels:
@ -270,7 +270,7 @@ class ZepEntityReader:
entity_types_found.add(entity_type) entity_types_found.add(entity_type)
# 创建实体节点对象 # Tạo object cho node thực thể
entity = EntityNode( entity = EntityNode(
uuid=node["uuid"], uuid=node["uuid"],
name=node["name"], name=node["name"],
@ -279,7 +279,7 @@ class ZepEntityReader:
attributes=node["attributes"], attributes=node["attributes"],
) )
# 获取相关边和节点 # Lấy các edge và node liên quan
if enrich_with_edges: if enrich_with_edges:
related_edges = [] related_edges = []
related_node_uuids = set() related_node_uuids = set()
@ -304,7 +304,7 @@ class ZepEntityReader:
entity.related_edges = related_edges 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 = [] related_nodes = []
for related_uuid in related_node_uuids: for related_uuid in related_node_uuids:
if related_uuid in node_map: if related_uuid in node_map:
@ -320,8 +320,8 @@ class ZepEntityReader:
filtered_entities.append(entity) filtered_entities.append(entity)
logger.info(f"筛选完成: 总节点 {total_count}, 符合条件 {len(filtered_entities)}, " logger.info(f"Filtering completed: Total nodes {total_count}, Matched {len(filtered_entities)}, "
f"实体类型: {entity_types_found}") f"Entity types: {entity_types_found}")
return FilteredEntities( return FilteredEntities(
entities=filtered_entities, entities=filtered_entities,
@ -336,33 +336,33 @@ class ZepEntityReader:
entity_uuid: str entity_uuid: str
) -> Optional[EntityNode]: ) -> 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: Args:
graph_id: 图谱ID graph_id: ID của đồ thị
entity_uuid: 实体UUID entity_uuid: UUID của thực thể
Returns: Returns:
EntityNode或None EntityNode hoặc None
""" """
try: try:
# 使用重试机制获取节点 # Sử dụng cơ chế thử lại để lấy thông tin node
node = self._call_with_retry( node = self._call_with_retry(
func=lambda: self.client.graph.node.get(uuid_=entity_uuid), 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: if not node:
return None return None
# 获取节点的边 # Lấy các edge của node
edges = self.get_node_edges(entity_uuid) 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) all_nodes = self.get_all_nodes(graph_id)
node_map = {n["uuid"]: n for n in all_nodes} node_map = {n["uuid"]: n for n in all_nodes}
# 处理相关边和节点 # Xử lý các edge và node liên quan
related_edges = [] related_edges = []
related_node_uuids = set() related_node_uuids = set()
@ -384,7 +384,7 @@ class ZepEntityReader:
}) })
related_node_uuids.add(edge["source_node_uuid"]) related_node_uuids.add(edge["source_node_uuid"])
# 获取关联节点信息 # Lấy thông tin về node được liên kết
related_nodes = [] related_nodes = []
for related_uuid in related_node_uuids: for related_uuid in related_node_uuids:
if related_uuid in node_map: if related_uuid in node_map:
@ -407,7 +407,7 @@ class ZepEntityReader:
) )
except Exception as e: except Exception as e:
logger.error(f"获取实体 {entity_uuid} 失败: {str(e)}") logger.error(f"Failed to fetch entity {entity_uuid}: {str(e)}")
return None return None
def get_entities_by_type( def get_entities_by_type(
@ -417,15 +417,15 @@ class ZepEntityReader:
enrich_with_edges: bool = True enrich_with_edges: bool = True
) -> List[EntityNode]: ) -> List[EntityNode]:
""" """
获取指定类型的所有实体 Lấy tất cả các thực thể dựa theo loại cụ thể
Args: Args:
graph_id: 图谱ID graph_id: ID của đồ thị
entity_type: 实体类型(如 "Student", "PublicFigure" 等) entity_type: Loại thực thể (ví dụ: "Student", "PublicFigure", v.v..)
enrich_with_edges: 是否获取相关边信息 enrich_with_edges: Có lấy thông tin edge liên quan hay không
Returns: Returns:
实体列表 Danh sách thực thể
""" """
result = self.filter_defined_entities( result = self.filter_defined_entities(
graph_id=graph_id, graph_id=graph_id,

View File

@ -1,6 +1,6 @@
""" """
Zep图谱记忆更新服务 Dịch vụ cập nhật bộ nhớ đồ thị Zep
将模拟中的Agent活动动态更新到Zep图谱中 Cập nhật động các hoạt động của Agent trong mô phỏng lên đồ thị Zep
""" """
import os import os
@ -22,7 +22,7 @@ logger = get_logger('mirofish.zep_graph_memory_updater')
@dataclass @dataclass
class AgentActivity: class AgentActivity:
"""Agent活动记录""" """Bản ghi hoạt động của Agent"""
platform: str # twitter / reddit platform: str # twitter / reddit
agent_id: int agent_id: int
agent_name: str agent_name: str
@ -33,12 +33,12 @@ class AgentActivity:
def to_episode_text(self) -> str: 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 = { action_descriptions = {
"CREATE_POST": self._describe_create_post, "CREATE_POST": self._describe_create_post,
"LIKE_POST": self._describe_like_post, "LIKE_POST": self._describe_like_post,
@ -57,222 +57,222 @@ class AgentActivity:
describe_func = action_descriptions.get(self.action_type, self._describe_generic) describe_func = action_descriptions.get(self.action_type, self._describe_generic)
description = describe_func() 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}" return f"{self.agent_name}: {description}"
def _describe_create_post(self) -> str: def _describe_create_post(self) -> str:
content = self.action_args.get("content", "") content = self.action_args.get("content", "")
if content: if content:
return f"发布了一条帖子:「{content}」" return f"posted a post: `{content}`"
return "发布了一条帖子" return "posted a post"
def _describe_like_post(self) -> str: 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_content = self.action_args.get("post_content", "")
post_author = self.action_args.get("post_author_name", "") post_author = self.action_args.get("post_author_name", "")
if post_content and post_author: if post_content and post_author:
return f"点赞了{post_author}的帖子:「{post_content}」" return f"liked {post_author}'s post: `{post_content}`"
elif post_content: elif post_content:
return f"点赞了一条帖子:「{post_content}」" return f"liked a post: `{post_content}`"
elif post_author: elif post_author:
return f"点赞了{post_author}的一条帖子" return f"liked a post by {post_author}"
return "点赞了一条帖子" return "liked a post"
def _describe_dislike_post(self) -> str: 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_content = self.action_args.get("post_content", "")
post_author = self.action_args.get("post_author_name", "") post_author = self.action_args.get("post_author_name", "")
if post_content and post_author: if post_content and post_author:
return f"踩了{post_author}的帖子:「{post_content}」" return f"disliked {post_author}'s post: `{post_content}`"
elif post_content: elif post_content:
return f"踩了一条帖子:「{post_content}」" return f"disliked a post: `{post_content}`"
elif post_author: elif post_author:
return f"踩了{post_author}的一条帖子" return f"disliked a post by {post_author}"
return "踩了一条帖子" return "disliked a post"
def _describe_repost(self) -> str: 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_content = self.action_args.get("original_content", "")
original_author = self.action_args.get("original_author_name", "") original_author = self.action_args.get("original_author_name", "")
if original_content and original_author: if original_content and original_author:
return f"转发了{original_author}的帖子:「{original_content}」" return f"reposted {original_author}'s post: `{original_content}`"
elif original_content: elif original_content:
return f"转发了一条帖子:「{original_content}」" return f"reposted a post: `{original_content}`"
elif original_author: elif original_author:
return f"转发了{original_author}的一条帖子" return f"reposted a post by {original_author}"
return "转发了一条帖子" return "reposted a post"
def _describe_quote_post(self) -> str: 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_content = self.action_args.get("original_content", "")
original_author = self.action_args.get("original_author_name", "") original_author = self.action_args.get("original_author_name", "")
quote_content = self.action_args.get("quote_content", "") or self.action_args.get("content", "") quote_content = self.action_args.get("quote_content", "") or self.action_args.get("content", "")
base = "" base = ""
if original_content and original_author: if original_content and original_author:
base = f"引用了{original_author}的帖子「{original_content}」" base = f"quoted {original_author}'s post `{original_content}`"
elif original_content: elif original_content:
base = f"引用了一条帖子「{original_content}」" base = f"quoted a post `{original_content}`"
elif original_author: elif original_author:
base = f"引用了{original_author}的一条帖子" base = f"quoted a post by {original_author}"
else: else:
base = "引用了一条帖子" base = "quoted a post"
if quote_content: if quote_content:
base += f",并评论道:「{quote_content}」" base += f" and commented: `{quote_content}`"
return base return base
def _describe_follow(self) -> str: 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", "") target_user_name = self.action_args.get("target_user_name", "")
if target_user_name: if target_user_name:
return f"关注了用户「{target_user_name}」" return f"followed user `{target_user_name}`"
return "关注了一个用户" return "followed a user"
def _describe_create_comment(self) -> str: 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", "") content = self.action_args.get("content", "")
post_content = self.action_args.get("post_content", "") post_content = self.action_args.get("post_content", "")
post_author = self.action_args.get("post_author_name", "") post_author = self.action_args.get("post_author_name", "")
if content: if content:
if post_content and post_author: 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: elif post_content:
return f"在帖子「{post_content}」下评论道:「{content}」" return f"commented under post `{post_content}`: `{content}`"
elif post_author: elif post_author:
return f"在{post_author}的帖子下评论道:「{content}」" return f"commented under {post_author}'s post: `{content}`"
return f"评论道:「{content}」" return f"commented: `{content}`"
return "发表了评论" return "posted a comment"
def _describe_like_comment(self) -> str: 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_content = self.action_args.get("comment_content", "")
comment_author = self.action_args.get("comment_author_name", "") comment_author = self.action_args.get("comment_author_name", "")
if comment_content and comment_author: if comment_content and comment_author:
return f"点赞了{comment_author}的评论:「{comment_content}」" return f"liked {comment_author}'s comment: `{comment_content}`"
elif comment_content: elif comment_content:
return f"点赞了一条评论:「{comment_content}」" return f"liked a comment: `{comment_content}`"
elif comment_author: elif comment_author:
return f"点赞了{comment_author}的一条评论" return f"liked a comment by {comment_author}"
return "点赞了一条评论" return "liked a comment"
def _describe_dislike_comment(self) -> str: 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_content = self.action_args.get("comment_content", "")
comment_author = self.action_args.get("comment_author_name", "") comment_author = self.action_args.get("comment_author_name", "")
if comment_content and comment_author: if comment_content and comment_author:
return f"踩了{comment_author}的评论:「{comment_content}」" return f"disliked {comment_author}'s comment: `{comment_content}`"
elif comment_content: elif comment_content:
return f"踩了一条评论:「{comment_content}」" return f"disliked a comment: `{comment_content}`"
elif comment_author: elif comment_author:
return f"踩了{comment_author}的一条评论" return f"disliked a comment by {comment_author}"
return "踩了一条评论" return "disliked a comment"
def _describe_search(self) -> str: 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", "") 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: 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", "") 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: 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", "") target_user_name = self.action_args.get("target_user_name", "")
if target_user_name: if target_user_name:
return f"屏蔽了用户「{target_user_name}」" return f"muted user `{target_user_name}`"
return "屏蔽了一个用户" return "muted a user"
def _describe_generic(self) -> str: def _describe_generic(self) -> str:
# 对于未知的动作类型,生成通用描述 # Tạo mô tả chung cho các loại hành động không xác định
return f"执行了{self.action_type}操作" return f"performed {self.action_type} action"
class ZepGraphMemoryUpdater: class ZepGraphMemoryUpdater:
""" """
Zep图谱记忆更新器 Trình cập nhật bộ nhớ đồ thị Zep
监控模拟的actions日志文件,将新的agent活动实时更新到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.
按平台分组,每累积BATCH_SIZE条活动后批量发送到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 BATCH_SIZE = 5
# 平台名称映射(用于控制台显示) # Ánh xạ tên nền tảng (dùng để hiển thị trên console)
PLATFORM_DISPLAY_NAMES = { PLATFORM_DISPLAY_NAMES = {
'twitter': '世界1', 'twitter': 'World 1',
'reddit': '世界2', 'reddit': 'World 2',
} }
# 发送间隔(秒),避免请求过快 # Thời gian gửi cách nhau (giây), tránh request quá nhanh
SEND_INTERVAL = 0.5 SEND_INTERVAL = 0.5
# 重试配置 # Cấu hình thử lại (retry)
MAX_RETRIES = 3 MAX_RETRIES = 3
RETRY_DELAY = 2 # 秒 RETRY_DELAY = 2 # giây
def __init__(self, graph_id: str, api_key: Optional[str] = None): def __init__(self, graph_id: str, api_key: Optional[str] = None):
""" """
初始化更新器 Khởi tạo trình cập nhật
Args: Args:
graph_id: Zep图谱ID graph_id: ID của đồ thị Zep
api_key: Zep API Key(可选,默认从配置读取) api_key: Zep API Key (tự chọn, mặc định lấy từ config)
""" """
self.graph_id = graph_id self.graph_id = graph_id
self.api_key = api_key or Config.ZEP_API_KEY self.api_key = api_key or Config.ZEP_API_KEY
if not self.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) self.client = Zep(api_key=self.api_key)
# 活动队列 # Hàng đợi hoạt động
self._activity_queue: Queue = Queue() 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]] = { self._platform_buffers: Dict[str, List[AgentActivity]] = {
'twitter': [], 'twitter': [],
'reddit': [], 'reddit': [],
} }
self._buffer_lock = threading.Lock() self._buffer_lock = threading.Lock()
# 控制标志 # Cờ điều khiển
self._running = False self._running = False
self._worker_thread: Optional[threading.Thread] = None self._worker_thread: Optional[threading.Thread] = None
# 统计 # Thống kê
self._total_activities = 0 # 实际添加到队列的活动数 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 # 成功发送到Zep的批次数 self._total_sent = 0 # Số đợt hàng gửi đi thành công tới Zep
self._total_items_sent = 0 # 成功发送到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 # 发送失败的批次数 self._failed_count = 0 # Số lượt gửi đi thất bại
self._skipped_count = 0 # 被过滤跳过的活动数(DO_NOTHING) 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: 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) return self.PLATFORM_DISPLAY_NAMES.get(platform.lower(), platform)
def start(self): def start(self):
"""启动后台工作线程""" """Khởi chạy luồng làm việc dưới background"""
if self._running: if self._running:
return return
@ -283,19 +283,19 @@ class ZepGraphMemoryUpdater:
name=f"ZepMemoryUpdater-{self.graph_id[:8]}" name=f"ZepMemoryUpdater-{self.graph_id[:8]}"
) )
self._worker_thread.start() 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): def stop(self):
"""停止后台工作线程""" """Tắt luồng làm việc dưới background"""
self._running = False self._running = False
# 发送剩余的活动 # Gửi nốt các hoạt động còn lại
self._flush_remaining() self._flush_remaining()
if self._worker_thread and self._worker_thread.is_alive(): if self._worker_thread and self._worker_thread.is_alive():
self._worker_thread.join(timeout=10) 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"total_activities={self._total_activities}, "
f"batches_sent={self._total_sent}, " f"batches_sent={self._total_sent}, "
f"items_sent={self._total_items_sent}, " f"items_sent={self._total_items_sent}, "
@ -304,43 +304,43 @@ class ZepGraphMemoryUpdater:
def add_activity(self, activity: AgentActivity): def add_activity(self, activity: AgentActivity):
""" """
添加一个agent活动到队列 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(发帖) - CREATE_POST (Đăng bài)
- CREATE_COMMENT(评论) - CREATE_COMMENT (Bình luận)
- QUOTE_POST(引用帖子) - QUOTE_POST (Trích dẫn bài viết)
- SEARCH_POSTS(搜索帖子) - SEARCH_POSTS (Tìm kiếm bài viết)
- SEARCH_USER(搜索用户) - SEARCH_USER (Tìm kiếm người dùng)
- LIKE_POST/DISLIKE_POST(点赞/踩帖子) - LIKE_POST/DISLIKE_POST (Like/Dislike bài viết)
- REPOST(转发) - REPOST (Repost)
- FOLLOW(关注) - FOLLOW (Theo dõi)
- MUTE(屏蔽) - MUTE (Chặn)
- LIKE_COMMENT/DISLIKE_COMMENT(点赞/踩评论) - LIKE_COMMENT/DISLIKE_COMMENT (Like/dislike bình luận)
action_args中会包含完整的上下文信息(如帖子原文、用户名等)。 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: 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": if activity.action_type == "DO_NOTHING":
self._skipped_count += 1 self._skipped_count += 1
return return
self._activity_queue.put(activity) self._activity_queue.put(activity)
self._total_activities += 1 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): def add_activity_from_dict(self, data: Dict[str, Any], platform: str):
""" """
从字典数据添加活动 Thêm hoạt động từ dữ liệu dictionary
Args: Args:
data: 从actions.jsonl解析的字典数据 data: Dữ liệu dictionary parse từ actions.jsonl
platform: 平台名称 (twitter/reddit) 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: if "event_type" in data:
return return
@ -357,52 +357,52 @@ class ZepGraphMemoryUpdater:
self.add_activity(activity) self.add_activity(activity)
def _worker_loop(self): 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(): while self._running or not self._activity_queue.empty():
try: try:
# 尝试从队列获取活动(超时1秒) # Thử lấy hoạt động từ hàng đợi (Timeout: 1 giây)
try: try:
activity = self._activity_queue.get(timeout=1) 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() platform = activity.platform.lower()
with self._buffer_lock: with self._buffer_lock:
if platform not in self._platform_buffers: if platform not in self._platform_buffers:
self._platform_buffers[platform] = [] self._platform_buffers[platform] = []
self._platform_buffers[platform].append(activity) 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: if len(self._platform_buffers[platform]) >= self.BATCH_SIZE:
batch = 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:] 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) 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) time.sleep(self.SEND_INTERVAL)
except Empty: except Empty:
pass pass
except Exception as e: except Exception as e:
logger.error(f"工作循环异常: {e}") logger.error(f"Worker loop error: {e}")
time.sleep(1) time.sleep(1)
def _send_batch_activities(self, activities: List[AgentActivity], platform: str): 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: Args:
activities: Agent活动列表 activities: Danh sách hoạt động của Agent
platform: 平台名称 platform: Tên nền tảng
""" """
if not activities: if not activities:
return 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] episode_texts = [activity.to_episode_text() for activity in activities]
combined_text = "\n".join(episode_texts) combined_text = "\n".join(episode_texts)
# 带重试的发送 # Gửi với cơ chế thử lại
for attempt in range(self.MAX_RETRIES): for attempt in range(self.MAX_RETRIES):
try: try:
self.client.graph.add( self.client.graph.add(
@ -414,21 +414,21 @@ class ZepGraphMemoryUpdater:
self._total_sent += 1 self._total_sent += 1
self._total_items_sent += len(activities) self._total_items_sent += len(activities)
display_name = self._get_platform_display_name(platform) display_name = self._get_platform_display_name(platform)
logger.info(f"成功批量发送 {len(activities)} 条{display_name}活动到图谱 {self.graph_id}") logger.info(f"Successfully sent batch of {len(activities)} {display_name} actions to graph {self.graph_id}")
logger.debug(f"批量内容预览: {combined_text[:200]}...") logger.debug(f"Batch content preview: {combined_text[:200]}...")
return return
except Exception as e: except Exception as e:
if attempt < self.MAX_RETRIES - 1: 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)) time.sleep(self.RETRY_DELAY * (attempt + 1))
else: 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 self._failed_count += 1
def _flush_remaining(self): 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(): while not self._activity_queue.empty():
try: try:
activity = self._activity_queue.get_nowait() activity = self._activity_queue.get_nowait()
@ -440,41 +440,41 @@ class ZepGraphMemoryUpdater:
except Empty: except Empty:
break 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: with self._buffer_lock:
for platform, buffer in self._platform_buffers.items(): for platform, buffer in self._platform_buffers.items():
if buffer: if buffer:
display_name = self._get_platform_display_name(platform) 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) self._send_batch_activities(buffer, platform)
# 清空所有缓冲区 # Dọn sạch toàn bộ bộ đệm
for platform in self._platform_buffers: for platform in self._platform_buffers:
self._platform_buffers[platform] = [] self._platform_buffers[platform] = []
def get_stats(self) -> Dict[str, Any]: def get_stats(self) -> Dict[str, Any]:
"""获取统计信息""" """Lấy thông tin thống kê"""
with self._buffer_lock: with self._buffer_lock:
buffer_sizes = {p: len(b) for p, b in self._platform_buffers.items()} buffer_sizes = {p: len(b) for p, b in self._platform_buffers.items()}
return { return {
"graph_id": self.graph_id, "graph_id": self.graph_id,
"batch_size": self.BATCH_SIZE, "batch_size": self.BATCH_SIZE,
"total_activities": self._total_activities, # 添加到队列的活动总数 "total_activities": self._total_activities, # Tổng số hoạt động được thêm vào hàng đợi
"batches_sent": self._total_sent, # 成功发送的批次数 "batches_sent": self._total_sent, # Số đợt hàng đã gửi thành công
"items_sent": self._total_items_sent, # 成功发送的活动条数 "items_sent": self._total_items_sent, # Số lượng hoạt động đã gửi thành công
"failed_count": self._failed_count, # 发送失败的批次数 "failed_count": self._failed_count, # Số đợt hàng gửi thất bại
"skipped_count": self._skipped_count, # 被过滤跳过的活动数(DO_NOTHING) "skipped_count": self._skipped_count, # Số lượng hoạt động bị bỏ qua (DO_NOTHING)
"queue_size": self._activity_queue.qsize(), "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, "running": self._running,
} }
class ZepGraphMemoryManager: 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] = {} _updaters: Dict[str, ZepGraphMemoryUpdater] = {}
@ -483,17 +483,17 @@ class ZepGraphMemoryManager:
@classmethod @classmethod
def create_updater(cls, simulation_id: str, graph_id: str) -> ZepGraphMemoryUpdater: 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: Args:
simulation_id: 模拟ID simulation_id: ID của mô phỏng
graph_id: Zep图谱ID graph_id: ID của đồ thị Zep
Returns: Returns:
ZepGraphMemoryUpdater实例 Instance của ZepGraphMemoryUpdater
""" """
with cls._lock: with cls._lock:
# 如果已存在,先停止旧的 # Nếu đã tồn tại, dừng cái cũ lại trước
if simulation_id in cls._updaters: if simulation_id in cls._updaters:
cls._updaters[simulation_id].stop() cls._updaters[simulation_id].stop()
@ -501,30 +501,30 @@ class ZepGraphMemoryManager:
updater.start() updater.start()
cls._updaters[simulation_id] = updater 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 return updater
@classmethod @classmethod
def get_updater(cls, simulation_id: str) -> Optional[ZepGraphMemoryUpdater]: 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) return cls._updaters.get(simulation_id)
@classmethod @classmethod
def stop_updater(cls, simulation_id: str): 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: with cls._lock:
if simulation_id in cls._updaters: if simulation_id in cls._updaters:
cls._updaters[simulation_id].stop() cls._updaters[simulation_id].stop()
del cls._updaters[simulation_id] 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 _stop_all_done = False
@classmethod @classmethod
def stop_all(cls): 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: if cls._stop_all_done:
return return
cls._stop_all_done = True cls._stop_all_done = True
@ -535,13 +535,13 @@ class ZepGraphMemoryManager:
try: try:
updater.stop() updater.stop()
except Exception as e: 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() cls._updaters.clear()
logger.info("已停止所有图谱记忆更新器") logger.info("All graph memory updaters stopped")
@classmethod @classmethod
def get_all_stats(cls) -> Dict[str, Dict[str, Any]]: 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 { return {
sim_id: updater.get_stats() sim_id: updater.get_stats()
for sim_id, updater in cls._updaters.items() for sim_id, updater in cls._updaters.items()

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
""" """
工具模块 Mô-đun tiện ích
""" """
from .file_parser import FileParser from .file_parser import FileParser

View File

@ -1,6 +1,6 @@
""" """
文件解析工具 Tiện ích phân tích tệp
支持PDF、Markdown、TXT文件的文本提取 Hỗ trợ trích xuất văn bản từ tệp PDF, Markdown, TXT
""" """
import os import os
@ -10,29 +10,29 @@ from typing import List, Optional
def _read_text_with_fallback(file_path: str) -> str: def _read_text_with_fallback(file_path: str) -> str:
""" """
读取文本文件,UTF-8失败时自动探测编码。 Đọ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. 首先尝试 UTF-8 解码 1. Thử giải mã bằng UTF-8 trước
2. 使用 charset_normalizer 检测编码 2. Dùng charset_normalizer để phát hiện mã hóa
3. 回退到 chardet 检测编码 3. Fallback sang chardet de phat hien ma hoa
4. 最终使用 UTF-8 + errors='replace' 兜底 4. Cuối cùng dùng UTF-8 + errors='replace' để đảm bảo không vỡ ký tự
Args: Args:
file_path: 文件路径 file_path: Đường dẫn tệp
Returns: Returns:
解码后的文本内容 Nội dung văn bản sau khi giải mã
""" """
data = Path(file_path).read_bytes() data = Path(file_path).read_bytes()
# 首先尝试 UTF-8 # Thử UTF-8 trước
try: try:
return data.decode('utf-8') return data.decode('utf-8')
except UnicodeDecodeError: except UnicodeDecodeError:
pass pass
# 尝试使用 charset_normalizer 检测编码 # Thử phát hiện mã hóa bằng charset_normalizer
encoding = None encoding = None
try: try:
from charset_normalizer import from_bytes from charset_normalizer import from_bytes
@ -42,7 +42,7 @@ def _read_text_with_fallback(file_path: str) -> str:
except Exception: except Exception:
pass pass
# 回退到 chardet # Fallback sang chardet
if not encoding: if not encoding:
try: try:
import chardet import chardet
@ -51,7 +51,7 @@ def _read_text_with_fallback(file_path: str) -> str:
except Exception: except Exception:
pass pass
# 最终兜底:使用 UTF-8 + replace # Fallback cuối: UTF-8 + replace
if not encoding: if not encoding:
encoding = 'utf-8' encoding = 'utf-8'
@ -59,30 +59,30 @@ def _read_text_with_fallback(file_path: str) -> str:
class FileParser: class FileParser:
"""文件解析器""" """Bộ phân tích tệp"""
SUPPORTED_EXTENSIONS = {'.pdf', '.md', '.markdown', '.txt'} SUPPORTED_EXTENSIONS = {'.pdf', '.md', '.markdown', '.txt'}
@classmethod @classmethod
def extract_text(cls, file_path: str) -> str: def extract_text(cls, file_path: str) -> str:
""" """
从文件中提取文本 Trích xuất văn bản từ tệp
Args: Args:
file_path: 文件路径 file_path: Đường dẫn tệp
Returns: Returns:
提取的文本内容 Nội dung văn bản đã trích xuất
""" """
path = Path(file_path) path = Path(file_path)
if not path.exists(): if not path.exists():
raise FileNotFoundError(f"文件不存在: {file_path}") raise FileNotFoundError(f"File does not exist: {file_path}")
suffix = path.suffix.lower() suffix = path.suffix.lower()
if suffix not in cls.SUPPORTED_EXTENSIONS: if suffix not in cls.SUPPORTED_EXTENSIONS:
raise ValueError(f"不支持的文件格式: {suffix}") raise ValueError(f"Unsupported file format: {suffix}")
if suffix == '.pdf': if suffix == '.pdf':
return cls._extract_from_pdf(file_path) return cls._extract_from_pdf(file_path)
@ -91,15 +91,15 @@ class FileParser:
elif suffix == '.txt': elif suffix == '.txt':
return cls._extract_from_txt(file_path) return cls._extract_from_txt(file_path)
raise ValueError(f"无法处理的文件格式: {suffix}") raise ValueError(f"Cannot process file format: {suffix}")
@staticmethod @staticmethod
def _extract_from_pdf(file_path: str) -> str: def _extract_from_pdf(file_path: str) -> str:
"""从PDF提取文本""" """Trích xuất văn bản từ PDF"""
try: try:
import fitz # PyMuPDF import fitz # PyMuPDF
except ImportError: except ImportError:
raise ImportError("需要安装PyMuPDF: pip install PyMuPDF") raise ImportError("PyMuPDF is required: pip install PyMuPDF")
text_parts = [] text_parts = []
with fitz.open(file_path) as doc: with fitz.open(file_path) as doc:
@ -112,24 +112,24 @@ class FileParser:
@staticmethod @staticmethod
def _extract_from_md(file_path: str) -> str: 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) return _read_text_with_fallback(file_path)
@staticmethod @staticmethod
def _extract_from_txt(file_path: str) -> str: 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) return _read_text_with_fallback(file_path)
@classmethod @classmethod
def extract_from_multiple(cls, file_paths: List[str]) -> str: 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: Args:
file_paths: 文件路径列表 file_paths: Danh sách đường dẫn tệp
Returns: Returns:
合并后的文本 Văn bản đã gộp
""" """
all_texts = [] all_texts = []
@ -137,9 +137,9 @@ class FileParser:
try: try:
text = cls.extract_text(file_path) text = cls.extract_text(file_path)
filename = Path(file_path).name 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: 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) return "\n\n".join(all_texts)
@ -150,15 +150,15 @@ def split_text_into_chunks(
overlap: int = 50 overlap: int = 50
) -> List[str]: ) -> List[str]:
""" """
将文本分割成小块 Chia văn bản thành các đoạn nhỏ
Args: Args:
text: 原始文本 text: Văn bản gốc
chunk_size: 每块的字符数 chunk_size: Số ký tự mỗi đoạn
overlap: 重叠字符数 overlap: Số ký tự chồng lấp
Returns: Returns:
文本块列表 Danh sách các đoạn văn bản
""" """
if len(text) <= chunk_size: if len(text) <= chunk_size:
return [text] if text.strip() else [] return [text] if text.strip() else []
@ -169,9 +169,9 @@ def split_text_into_chunks(
while start < len(text): while start < len(text):
end = start + chunk_size end = start + chunk_size
# 尝试在句子边界处分割 # Cố gắng cắt tại ranh giới câu
if end < len(text): 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', '. ', '! ', '? ']: for sep in ['。', '!', '?', '.\n', '!\n', '?\n', '\n\n', '. ', '! ', '? ']:
last_sep = text[start:end].rfind(sep) last_sep = text[start:end].rfind(sep)
if last_sep != -1 and last_sep > chunk_size * 0.3: if last_sep != -1 and last_sep > chunk_size * 0.3:
@ -182,7 +182,7 @@ def split_text_into_chunks(
if chunk: if chunk:
chunks.append(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) start = end - overlap if end < len(text) else len(text)
return chunks return chunks

View File

@ -1,6 +1,6 @@
""" """
LLM客户端封装 Lớp bao bọc LLM client
统一使用OpenAI格式调用 Thống nhất gọi theo định dạng OpenAI
""" """
import json import json
@ -12,7 +12,7 @@ from ..config import Config
class LLMClient: class LLMClient:
"""LLM客户端""" """LLM client"""
def __init__( def __init__(
self, self,
@ -25,7 +25,7 @@ class LLMClient:
self.model = model or Config.LLM_MODEL_NAME self.model = model or Config.LLM_MODEL_NAME
if not self.api_key: if not self.api_key:
raise ValueError("LLM_API_KEY 未配置") raise ValueError("LLM_API_KEY is not configured")
self.client = OpenAI( self.client = OpenAI(
api_key=self.api_key, api_key=self.api_key,
@ -40,16 +40,16 @@ class LLMClient:
response_format: Optional[Dict] = None response_format: Optional[Dict] = None
) -> str: ) -> str:
""" """
发送聊天请求 Gửi yêu cầu chat
Args: Args:
messages: 消息列表 messages: Danh sách message
temperature: 温度参数 temperature: Tham số nhiệt độ
max_tokens: 最大token数 max_tokens: Số token tối đa
response_format: 响应格式(如JSON模式) response_format: Định dạng response (ví dụ JSON mode)
Returns: Returns:
模型响应文本 Nội dung response từ model
""" """
kwargs = { kwargs = {
"model": self.model, "model": self.model,
@ -63,7 +63,7 @@ class LLMClient:
response = self.client.chat.completions.create(**kwargs) response = self.client.chat.completions.create(**kwargs)
content = response.choices[0].message.content content = response.choices[0].message.content
# 部分模型(如MiniMax M2.5)会在content中包含<think>思考内容,需要移除 # Một số model (vd MiniMax M2.5) chèn nội dung <think> vào content, cần loại bỏ
content = re.sub(r'<think>[\s\S]*?</think>', '', content).strip() content = re.sub(r'<think>[\s\S]*?</think>', '', content).strip()
return content return content
@ -74,15 +74,15 @@ class LLMClient:
max_tokens: int = 4096 max_tokens: int = 4096
) -> Dict[str, Any]: ) -> Dict[str, Any]:
""" """
发送聊天请求并返回JSON Gửi yêu cầu chat và trả về JSON
Args: Args:
messages: 消息列表 messages: Danh sách message
temperature: 温度参数 temperature: Tham số nhiệt độ
max_tokens: 最大token数 max_tokens: Số token tối đa
Returns: Returns:
解析后的JSON对象 JSON object sau khi parse
""" """
response = self.chat( response = self.chat(
messages=messages, messages=messages,
@ -90,7 +90,7 @@ class LLMClient:
max_tokens=max_tokens, max_tokens=max_tokens,
response_format={"type": "json_object"} response_format={"type": "json_object"}
) )
# 清理markdown代码块标记 # Làm sạch markdown code fence
cleaned_response = response.strip() cleaned_response = response.strip()
cleaned_response = re.sub(r'^```(?:json)?\s*\n?', '', cleaned_response, flags=re.IGNORECASE) cleaned_response = re.sub(r'^```(?:json)?\s*\n?', '', cleaned_response, flags=re.IGNORECASE)
cleaned_response = re.sub(r'\n?```\s*$', '', cleaned_response) cleaned_response = re.sub(r'\n?```\s*$', '', cleaned_response)
@ -99,5 +99,5 @@ class LLMClient:
try: try:
return json.loads(cleaned_response) return json.loads(cleaned_response)
except json.JSONDecodeError: except json.JSONDecodeError:
raise ValueError(f"LLM返回的JSON格式无效: {cleaned_response}") raise ValueError(f"LLM returned invalid JSON: {cleaned_response}")

View File

@ -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 import os
@ -12,47 +12,47 @@ from logging.handlers import RotatingFileHandler
def _ensure_utf8_stdout(): def _ensure_utf8_stdout():
""" """
确保 stdout/stderr 使用 UTF-8 编码 Đảm bảo stdout/stderr sử dụng mã hóa UTF-8
解决 Windows 控制台中文乱码问题 Khắc phục lỗi vỡ font tiếng Trung trên console Windows
""" """
if sys.platform == 'win32': 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'): if hasattr(sys.stdout, 'reconfigure'):
sys.stdout.reconfigure(encoding='utf-8', errors='replace') sys.stdout.reconfigure(encoding='utf-8', errors='replace')
if hasattr(sys.stderr, 'reconfigure'): if hasattr(sys.stderr, 'reconfigure'):
sys.stderr.reconfigure(encoding='utf-8', errors='replace') 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') 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: def setup_logger(name: str = 'mirofish', level: int = logging.DEBUG) -> logging.Logger:
""" """
设置日志器 Thiết lập logger
Args: Args:
name: 日志器名称 name: Tên logger
level: 日志级别 level: Mức độ log
Returns: 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) os.makedirs(LOG_DIR, exist_ok=True)
# 创建日志器 # Tạo logger
logger = logging.getLogger(name) logger = logging.getLogger(name)
logger.setLevel(level) logger.setLevel(level)
# 阻止日志向上传播到根 logger,避免重复输出 # Chặn log propagate lên root logger để tránh in trùng lặp
logger.propagate = False logger.propagate = False
# 如果已经有处理器,不重复添加 # Nếu đã có handler thì không thêm lại
if logger.handlers: if logger.handlers:
return logger return logger
# 日志格式 # Định dạng log
detailed_formatter = logging.Formatter( detailed_formatter = logging.Formatter(
'[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d] %(message)s', '[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d] %(message)s',
datefmt='%Y-%m-%d %H:%M:%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' 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' log_filename = datetime.now().strftime('%Y-%m-%d') + '.log'
file_handler = RotatingFileHandler( file_handler = RotatingFileHandler(
os.path.join(LOG_DIR, log_filename), 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.setLevel(logging.DEBUG)
file_handler.setFormatter(detailed_formatter) file_handler.setFormatter(detailed_formatter)
# 2. 控制台处理器 - 简洁日志(INFO及以上) # 2. Console handler - log gọn (INFO trở lên)
# 确保 Windows 下使用 UTF-8 编码,避免中文乱码 # Đảm bảo Windows dùng UTF-8 để tránh lỗi ký tự
_ensure_utf8_stdout() _ensure_utf8_stdout()
console_handler = logging.StreamHandler(sys.stdout) console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(logging.INFO) console_handler.setLevel(logging.INFO)
console_handler.setFormatter(simple_formatter) console_handler.setFormatter(simple_formatter)
# 添加处理器 # Gắn handler
logger.addHandler(file_handler) logger.addHandler(file_handler)
logger.addHandler(console_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: def get_logger(name: str = 'mirofish') -> logging.Logger:
""" """
获取日志器(如果不存在则创建) Lấy logger (nếu chưa có sẽ tạo mới)
Args: Args:
name: 日志器名称 name: Tên logger
Returns: Returns:
日志器实例 Instance logger
""" """
logger = logging.getLogger(name) logger = logging.getLogger(name)
if not logger.handlers: if not logger.handlers:
@ -104,11 +104,11 @@ def get_logger(name: str = 'mirofish') -> logging.Logger:
return logger return logger
# 创建默认日志器 # Tạo logger mặc định
logger = setup_logger() logger = setup_logger()
# 便捷方法 # Hàm gọi nhanh
def debug(msg, *args, **kwargs): def debug(msg, *args, **kwargs):
logger.debug(msg, *args, **kwargs) logger.debug(msg, *args, **kwargs)

View File

@ -1,6 +1,6 @@
""" """
API调用重试机制 Cơ chế retry cho API call
用于处理LLM等外部API调用的重试逻辑 Dùng để xử lý retry khi gọi API bên ngoài như LLM
""" """
import time import time
@ -22,17 +22,17 @@ def retry_with_backoff(
on_retry: Optional[Callable[[Exception, int], None]] = None on_retry: Optional[Callable[[Exception, int], None]] = None
): ):
""" """
带指数退避的重试装饰器 Decorator retry với exponential backoff
Args: Args:
max_retries: 最大重试次数 max_retries: Số lần retry tối đa
initial_delay: 初始延迟(秒) initial_delay: Độ trễ ban đầu (giây)
max_delay: 最大延迟(秒) max_delay: Độ trễ tối đa (giây)
backoff_factor: 退避因子 backoff_factor: Hệ số tăng độ trễ
jitter: 是否添加随机抖动 jitter: Có thêm nhiễu ngẫu nhiên hay không
exceptions: 需要重试的异常类型 exceptions: Các loại exception cần retry
on_retry: 重试时的回调函数 (exception, retry_count) on_retry: Callback khi retry (exception, retry_count)
Usage: Usage:
@retry_with_backoff(max_retries=3) @retry_with_backoff(max_retries=3)
def call_llm_api(): def call_llm_api():
@ -52,17 +52,17 @@ def retry_with_backoff(
last_exception = e last_exception = e
if attempt == max_retries: 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 raise
# 计算延迟 # Tính độ trễ
current_delay = min(delay, max_delay) current_delay = min(delay, max_delay)
if jitter: if jitter:
current_delay = current_delay * (0.5 + random.random()) current_delay = current_delay * (0.5 + random.random())
logger.warning( logger.warning(
f"函数 {func.__name__} 第 {attempt + 1} 次尝试失败: {str(e)}, " f"Function {func.__name__} attempt {attempt + 1} failed: {str(e)}, "
f"{current_delay:.1f}秒后重试..." f"retrying in {current_delay:.1f}s..."
) )
if on_retry: if on_retry:
@ -87,7 +87,7 @@ def retry_with_backoff_async(
on_retry: Optional[Callable[[Exception, int], None]] = None on_retry: Optional[Callable[[Exception, int], None]] = None
): ):
""" """
异步版本的重试装饰器 Phiên bản bất đồng bộ của retry decorator
""" """
import asyncio import asyncio
@ -105,7 +105,7 @@ def retry_with_backoff_async(
last_exception = e last_exception = e
if attempt == max_retries: 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 raise
current_delay = min(delay, max_delay) current_delay = min(delay, max_delay)
@ -113,8 +113,8 @@ def retry_with_backoff_async(
current_delay = current_delay * (0.5 + random.random()) current_delay = current_delay * (0.5 + random.random())
logger.warning( logger.warning(
f"异步函数 {func.__name__} 第 {attempt + 1} 次尝试失败: {str(e)}, " f"Async function {func.__name__} attempt {attempt + 1} failed: {str(e)}, "
f"{current_delay:.1f}秒后重试..." f"retrying in {current_delay:.1f}s..."
) )
if on_retry: if on_retry:
@ -131,7 +131,7 @@ def retry_with_backoff_async(
class RetryableAPIClient: class RetryableAPIClient:
""" """
可重试的API客户端封装 Lớp bao API client có retry
""" """
def __init__( def __init__(
@ -154,16 +154,16 @@ class RetryableAPIClient:
**kwargs **kwargs
) -> Any: ) -> Any:
""" """
执行函数调用并在失败时重试 Thực thi hàm và retry nếu thất bại
Args: Args:
func: 要调用的函数 func: Hàm cần gọi
*args: 函数参数 *args: Tham số hàm
exceptions: 需要重试的异常类型 exceptions: Các loại exception cần retry
**kwargs: 函数关键字参数 **kwargs: Tham số keyword của hàm
Returns: Returns:
函数返回值 Giá trị trả về của hàm
""" """
last_exception = None last_exception = None
delay = self.initial_delay delay = self.initial_delay
@ -176,15 +176,15 @@ class RetryableAPIClient:
last_exception = e last_exception = e
if attempt == self.max_retries: 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 raise
current_delay = min(delay, self.max_delay) current_delay = min(delay, self.max_delay)
current_delay = current_delay * (0.5 + random.random()) current_delay = current_delay * (0.5 + random.random())
logger.warning( logger.warning(
f"API调用第 {attempt + 1} 次尝试失败: {str(e)}, " f"API call attempt {attempt + 1} failed: {str(e)}, "
f"{current_delay:.1f}秒后重试..." f"retrying in {current_delay:.1f}s..."
) )
time.sleep(current_delay) time.sleep(current_delay)
@ -200,16 +200,16 @@ class RetryableAPIClient:
continue_on_failure: bool = True continue_on_failure: bool = True
) -> Tuple[list, list]: ) -> Tuple[list, list]:
""" """
批量调用并对每个失败项单独重试 Xử lý theo lô và retry riêng cho từng mục thất bại
Args: Args:
items: 要处理的项目列表 items: Danh sách mục cần xử lý
process_func: 处理函数,接收单个item作为参数 process_func: Hàm xử lý, nhận 1 item mỗi lần
exceptions: 需要重试的异常类型 exceptions: Các loại exception cần retry
continue_on_failure: 单项失败后是否继续处理其他项 continue_on_failure: Có tiếp tục khi 1 mục thất bại không
Returns: Returns:
(成功结果列表, 失败项列表) (Danh sách kết quả thành công, danh sách mục thất bại)
""" """
results = [] results = []
failures = [] failures = []
@ -224,7 +224,7 @@ class RetryableAPIClient:
results.append(result) results.append(result)
except Exception as e: except Exception as e:
logger.error(f"处理第 {idx + 1} 项失败: {str(e)}") logger.error(f"Failed to process item {idx + 1}: {str(e)}")
failures.append({ failures.append({
"index": idx, "index": idx,
"item": item, "item": item,

View File

@ -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 from __future__ import annotations
@ -31,7 +32,7 @@ def _fetch_page_with_retry(
page_description: str = "page", page_description: str = "page",
**kwargs: Any, **kwargs: Any,
) -> list[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: if max_retries < 1:
raise ValueError("max_retries must be >= 1") raise ValueError("max_retries must be >= 1")
@ -64,7 +65,7 @@ def fetch_all_nodes(
max_retries: int = _DEFAULT_MAX_RETRIES, max_retries: int = _DEFAULT_MAX_RETRIES,
retry_delay: float = _DEFAULT_RETRY_DELAY, retry_delay: float = _DEFAULT_RETRY_DELAY,
) -> list[Any]: ) -> 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] = [] all_nodes: list[Any] = []
cursor: str | None = None cursor: str | None = None
page_num = 0 page_num = 0
@ -109,7 +110,7 @@ def fetch_all_edges(
max_retries: int = _DEFAULT_MAX_RETRIES, max_retries: int = _DEFAULT_MAX_RETRIES,
retry_delay: float = _DEFAULT_RETRY_DELAY, retry_delay: float = _DEFAULT_RETRY_DELAY,
) -> list[Any]: ) -> 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] = [] all_edges: list[Any] = []
cursor: str | None = None cursor: str | None = None
page_num = 0 page_num = 0

View File

@ -1,7 +1,7 @@
[project] [project]
name = "mirofish-backend" name = "mirofish-backend"
version = "0.1.0" version = "0.1.0"
description = "MiroFish - 简洁通用的群体智能引擎,预测万物" description = "MiroFish - A concise and general collective intelligence engine for prediction"
requires-python = ">=3.11" requires-python = ">=3.11"
license = { text = "AGPL-3.0" } license = { text = "AGPL-3.0" }
authors = [ authors = [
@ -9,27 +9,27 @@ authors = [
] ]
dependencies = [ dependencies = [
# 核心框架 # Khung lõi
"flask>=3.0.0", "flask>=3.0.0",
"flask-cors>=6.0.0", "flask-cors>=6.0.0",
# LLM 相关 # Liên quan đến LLM
"openai>=1.0.0", "openai>=1.0.0",
# Zep Cloud # Zep Cloud
"zep-cloud==3.13.0", "zep-cloud==3.13.0",
# OASIS 社交媒体模拟 # Mô phỏng mạng xã hội OASIS
"camel-oasis==0.2.5", "camel-oasis==0.2.5",
"camel-ai==0.2.78", "camel-ai==0.2.78",
# 文件处理 # Xử lý tệp
"PyMuPDF>=1.24.0", "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", "charset-normalizer>=3.0.0",
"chardet>=5.0.0", "chardet>=5.0.0",
# 工具库 # Thư viện tiện ích
"python-dotenv>=1.0.0", "python-dotenv>=1.0.0",
"pydantic>=2.0.0", "pydantic>=2.0.0",
] ]

View File

@ -5,31 +5,31 @@
# Install: pip install -r requirements.txt # Install: pip install -r requirements.txt
# =========================================== # ===========================================
# ============= 核心框架 ============= # ============= Khung lõi =============
flask>=3.0.0 flask>=3.0.0
flask-cors>=6.0.0 flask-cors>=6.0.0
# ============= LLM 相关 ============= # ============= Liên quan đến LLM =============
# OpenAI SDK(统一使用 OpenAI 格式调用 LLM) # OpenAI SDK (thống nhất dùng định dạng OpenAI để gọi LLM)
openai>=1.0.0 openai>=1.0.0
# ============= Zep Cloud ============= # ============= Zep Cloud =============
zep-cloud==3.13.0 zep-cloud==3.13.0
# ============= OASIS 社交媒体模拟 ============= # ============= Mô phỏng mạng xã hội OASIS =============
# OASIS 社交模拟框架 # Khung mô phỏng mạng xã hội OASIS
camel-oasis==0.2.5 camel-oasis==0.2.5
camel-ai==0.2.78 camel-ai==0.2.78
# ============= 文件处理 ============= # ============= Xử lý tệp =============
PyMuPDF>=1.24.0 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 charset-normalizer>=3.0.0
chardet>=5.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 python-dotenv>=1.0.0
# 数据验证 # Xác thực dữ liệu
pydantic>=2.0.0 pydantic>=2.0.0

View File

@ -1,21 +1,21 @@
""" """
MiroFish Backend 启动入口 MiroFish Backend entrypoint
""" """
import os import os
import sys 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': 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') os.environ.setdefault('PYTHONIOENCODING', 'utf-8')
# 重新配置标准输出流为 UTF-8 # Cấu hình lại stdout/stderr sang UTF-8
if hasattr(sys.stdout, 'reconfigure'): if hasattr(sys.stdout, 'reconfigure'):
sys.stdout.reconfigure(encoding='utf-8', errors='replace') sys.stdout.reconfigure(encoding='utf-8', errors='replace')
if hasattr(sys.stderr, 'reconfigure'): if hasattr(sys.stderr, 'reconfigure'):
sys.stderr.reconfigure(encoding='utf-8', errors='replace') 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__))) sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from app import create_app from app import create_app
@ -23,25 +23,25 @@ from app.config import Config
def main(): def main():
"""主函数""" """Hàm chính"""
# 验证配置 # Kiểm tra cấu hình
errors = Config.validate() errors = Config.validate()
if errors: if errors:
print("配置错误:") print("Configuration errors:")
for err in errors: for err in errors:
print(f" - {err}") print(f" - {err}")
print("\n请检查 .env 文件中的配置") print("\nPlease check configuration in the .env file")
sys.exit(1) sys.exit(1)
# 创建应用 # Tạo ứng dụng
app = create_app() app = create_app()
# 获取运行配置 # Lấy cấu hình chạy
host = os.environ.get('FLASK_HOST', '0.0.0.0') host = os.environ.get('FLASK_HOST', '0.0.0.0')
port = int(os.environ.get('FLASK_PORT', 5001)) port = int(os.environ.get('FLASK_PORT', 5001))
debug = Config.DEBUG debug = Config.DEBUG
# 启动服务 # Khởi động dịch vụ
app.run(host=host, port=port, debug=debug, threaded=True) app.run(host=host, port=port, debug=debug, threaded=True)

View File

@ -1,15 +1,15 @@
""" """
动作日志记录器 Trình ghi log hành động
用于记录OASIS模拟中每个Agent的动作,供后端监控使用 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/ sim_xxx/
├── twitter/ ├── twitter/
│ └── actions.jsonl # Twitter 平台动作日志 │ └── actions.jsonl # Log hành động nền tảng Twitter
├── reddit/ ├── reddit/
│ └── actions.jsonl # Reddit 平台动作日志 │ └── actions.jsonl # Log hành động nền tảng Reddit
├── simulation.log # 主模拟进程日志 ├── simulation.log # Log tiến trình mô phỏng chính
└── run_state.json # 运行状态(API 查询用) └── run_state.json # Trạng thái chạy (cho API truy vấn)
""" """
import json import json
@ -20,15 +20,15 @@ from typing import Dict, Any, Optional
class PlatformActionLogger: class PlatformActionLogger:
"""单平台动作日志记录器""" """Trình ghi log hành động cho một nền tảng"""
def __init__(self, platform: str, base_dir: str): def __init__(self, platform: str, base_dir: str):
""" """
初始化日志记录器 Khởi tạo logger
Args: Args:
platform: 平台名称 (twitter/reddit) platform: Tên nền tảng (twitter/reddit)
base_dir: 模拟目录的基础路径 base_dir: Đường dẫn thư mục mô phỏng gốc
""" """
self.platform = platform self.platform = platform
self.base_dir = base_dir self.base_dir = base_dir
@ -37,7 +37,7 @@ class PlatformActionLogger:
self._ensure_dir() self._ensure_dir()
def _ensure_dir(self): def _ensure_dir(self):
"""确保目录存在""" """Đảm bảo thư mục tồn tại"""
os.makedirs(self.log_dir, exist_ok=True) os.makedirs(self.log_dir, exist_ok=True)
def log_action( def log_action(
@ -50,7 +50,7 @@ class PlatformActionLogger:
result: Optional[str] = None, result: Optional[str] = None,
success: bool = True success: bool = True
): ):
"""记录一个动作""" """Ghi lại một hành động"""
entry = { entry = {
"round": round_num, "round": round_num,
"timestamp": datetime.now().isoformat(), "timestamp": datetime.now().isoformat(),
@ -66,7 +66,7 @@ class PlatformActionLogger:
f.write(json.dumps(entry, ensure_ascii=False) + '\n') f.write(json.dumps(entry, ensure_ascii=False) + '\n')
def log_round_start(self, round_num: int, simulated_hour: int): def log_round_start(self, round_num: int, simulated_hour: int):
"""记录轮次开始""" """Ghi lại thời điểm bắt đầu round"""
entry = { entry = {
"round": round_num, "round": round_num,
"timestamp": datetime.now().isoformat(), "timestamp": datetime.now().isoformat(),
@ -78,7 +78,7 @@ class PlatformActionLogger:
f.write(json.dumps(entry, ensure_ascii=False) + '\n') f.write(json.dumps(entry, ensure_ascii=False) + '\n')
def log_round_end(self, round_num: int, actions_count: int): def log_round_end(self, round_num: int, actions_count: int):
"""记录轮次结束""" """Ghi lại thời điểm kết thúc round"""
entry = { entry = {
"round": round_num, "round": round_num,
"timestamp": datetime.now().isoformat(), "timestamp": datetime.now().isoformat(),
@ -90,7 +90,7 @@ class PlatformActionLogger:
f.write(json.dumps(entry, ensure_ascii=False) + '\n') f.write(json.dumps(entry, ensure_ascii=False) + '\n')
def log_simulation_start(self, config: Dict[str, Any]): def log_simulation_start(self, config: Dict[str, Any]):
"""记录模拟开始""" """Ghi lại thời điểm bắt đầu mô phỏng"""
entry = { entry = {
"timestamp": datetime.now().isoformat(), "timestamp": datetime.now().isoformat(),
"event_type": "simulation_start", "event_type": "simulation_start",
@ -103,7 +103,7 @@ class PlatformActionLogger:
f.write(json.dumps(entry, ensure_ascii=False) + '\n') f.write(json.dumps(entry, ensure_ascii=False) + '\n')
def log_simulation_end(self, total_rounds: int, total_actions: int): 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 = { entry = {
"timestamp": datetime.now().isoformat(), "timestamp": datetime.now().isoformat(),
"event_type": "simulation_end", "event_type": "simulation_end",
@ -118,35 +118,35 @@ class PlatformActionLogger:
class SimulationLogManager: 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): def __init__(self, simulation_dir: str):
""" """
初始化日志管理器 Khởi tạo trình quản lý log
Args: Args:
simulation_dir: 模拟目录路径 simulation_dir: Đường dẫn thư mục mô phỏng
""" """
self.simulation_dir = simulation_dir self.simulation_dir = simulation_dir
self.twitter_logger: Optional[PlatformActionLogger] = None self.twitter_logger: Optional[PlatformActionLogger] = None
self.reddit_logger: Optional[PlatformActionLogger] = None self.reddit_logger: Optional[PlatformActionLogger] = None
self._main_logger: Optional[logging.Logger] = None self._main_logger: Optional[logging.Logger] = None
# 设置主日志 # Thiết lập log chính
self._setup_main_logger() self._setup_main_logger()
def _setup_main_logger(self): 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") 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 = logging.getLogger(f"simulation.{os.path.basename(self.simulation_dir)}")
self._main_logger.setLevel(logging.INFO) self._main_logger.setLevel(logging.INFO)
self._main_logger.handlers.clear() self._main_logger.handlers.clear()
# 文件处理器 # File handler
file_handler = logging.FileHandler(log_path, encoding='utf-8', mode='w') file_handler = logging.FileHandler(log_path, encoding='utf-8', mode='w')
file_handler.setLevel(logging.INFO) file_handler.setLevel(logging.INFO)
file_handler.setFormatter(logging.Formatter( file_handler.setFormatter(logging.Formatter(
@ -155,7 +155,7 @@ class SimulationLogManager:
)) ))
self._main_logger.addHandler(file_handler) self._main_logger.addHandler(file_handler)
# 控制台处理器 # Console handler
console_handler = logging.StreamHandler() console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO) console_handler.setLevel(logging.INFO)
console_handler.setFormatter(logging.Formatter( console_handler.setFormatter(logging.Formatter(
@ -167,19 +167,19 @@ class SimulationLogManager:
self._main_logger.propagate = False self._main_logger.propagate = False
def get_twitter_logger(self) -> PlatformActionLogger: def get_twitter_logger(self) -> PlatformActionLogger:
"""获取 Twitter 平台日志记录器""" """Lấy logger cho nền tảng Twitter"""
if self.twitter_logger is None: if self.twitter_logger is None:
self.twitter_logger = PlatformActionLogger("twitter", self.simulation_dir) self.twitter_logger = PlatformActionLogger("twitter", self.simulation_dir)
return self.twitter_logger return self.twitter_logger
def get_reddit_logger(self) -> PlatformActionLogger: def get_reddit_logger(self) -> PlatformActionLogger:
"""获取 Reddit 平台日志记录器""" """Lấy logger cho nền tảng Reddit"""
if self.reddit_logger is None: if self.reddit_logger is None:
self.reddit_logger = PlatformActionLogger("reddit", self.simulation_dir) self.reddit_logger = PlatformActionLogger("reddit", self.simulation_dir)
return self.reddit_logger return self.reddit_logger
def log(self, message: str, level: str = "info"): def log(self, message: str, level: str = "info"):
"""记录主日志""" """Ghi log chính"""
if self._main_logger: if self._main_logger:
getattr(self._main_logger, level.lower(), self._main_logger.info)(message) getattr(self._main_logger, level.lower(), self._main_logger.info)(message)
@ -196,12 +196,12 @@ class SimulationLogManager:
self.log(message, "debug") self.log(message, "debug")
# ============ 兼容旧接口 ============ # ============ Tương thích giao diện cũ ============
class ActionLogger: class ActionLogger:
""" """
动作日志记录器(兼容旧接口) Trình ghi log hành động (tương thích giao diện cũ)
建议使用 SimulationLogManager 代替 Khuyến nghị dùng SimulationLogManager thay thế
""" """
def __init__(self, log_path: str): def __init__(self, log_path: str):
@ -288,12 +288,12 @@ class ActionLogger:
f.write(json.dumps(entry, ensure_ascii=False) + '\n') 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 _global_logger: Optional[ActionLogger] = None
def get_logger(log_path: Optional[str] = None) -> ActionLogger: 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 global _global_logger
if log_path: if log_path:

File diff suppressed because it is too large Load Diff

View File

@ -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
功能特性: 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
- 支持通过IPC接收Interview命令 - Hỗ trợ nhận lệnh Interview qua IPC
- 支持单个Agent采访和批量采访 - 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
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 import argparse
@ -25,18 +25,18 @@ import sqlite3
from datetime import datetime from datetime import datetime
from typing import Dict, Any, List, Optional 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 _shutdown_event = None
_cleanup_done = False _cleanup_done = False
# 添加项目路径 # Thêm đường dẫn dự án
_scripts_dir = os.path.dirname(os.path.abspath(__file__)) _scripts_dir = os.path.dirname(os.path.abspath(__file__))
_backend_dir = os.path.abspath(os.path.join(_scripts_dir, '..')) _backend_dir = os.path.abspath(os.path.join(_scripts_dir, '..'))
_project_root = os.path.abspath(os.path.join(_backend_dir, '..')) _project_root = os.path.abspath(os.path.join(_backend_dir, '..'))
sys.path.insert(0, _scripts_dir) sys.path.insert(0, _scripts_dir)
sys.path.insert(0, _backend_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 from dotenv import load_dotenv
_env_file = os.path.join(_project_root, '.env') _env_file = os.path.join(_project_root, '.env')
if os.path.exists(_env_file): if os.path.exists(_env_file):
@ -51,7 +51,7 @@ import re
class UnicodeFormatter(logging.Formatter): 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})') UNICODE_ESCAPE_PATTERN = re.compile(r'\\u([0-9a-fA-F]{4})')
@ -68,24 +68,24 @@ class UnicodeFormatter(logging.Formatter):
class MaxTokensWarningFilter(logging.Filter): 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): 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(): if "max_tokens" in record.getMessage() and "Invalid or missing" in record.getMessage():
return False return False
return True 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()) logging.getLogger().addFilter(MaxTokensWarningFilter())
def setup_oasis_logging(log_dir: str): 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) os.makedirs(log_dir, exist_ok=True)
# 清理旧的日志文件 # Dọn các file log cũ
for f in os.listdir(log_dir): for f in os.listdir(log_dir):
old_log = os.path.join(log_dir, f) old_log = os.path.join(log_dir, f)
if os.path.isfile(old_log) and f.endswith('.log'): if os.path.isfile(old_log) and f.endswith('.log'):
@ -126,25 +126,25 @@ try:
generate_reddit_agent_graph generate_reddit_agent_graph
) )
except ImportError as e: except ImportError as e:
print(f"错误: 缺少依赖 {e}") print(f"Error: missing dependency {e}")
print("请先安装: pip install oasis-ai camel-ai") print("Please install first: pip install oasis-ai camel-ai")
sys.exit(1) sys.exit(1)
# IPC相关常量 # Hằng số liên quan IPC
IPC_COMMANDS_DIR = "ipc_commands" IPC_COMMANDS_DIR = "ipc_commands"
IPC_RESPONSES_DIR = "ipc_responses" IPC_RESPONSES_DIR = "ipc_responses"
ENV_STATUS_FILE = "env_status.json" ENV_STATUS_FILE = "env_status.json"
class CommandType: class CommandType:
"""命令类型常量""" """Hằng số loại lệnh"""
INTERVIEW = "interview" INTERVIEW = "interview"
BATCH_INTERVIEW = "batch_interview" BATCH_INTERVIEW = "batch_interview"
CLOSE_ENV = "close_env" CLOSE_ENV = "close_env"
class IPCHandler: class IPCHandler:
"""IPC命令处理器""" """Bộ xử lý lệnh IPC"""
def __init__(self, simulation_dir: str, env, agent_graph): def __init__(self, simulation_dir: str, env, agent_graph):
self.simulation_dir = simulation_dir self.simulation_dir = simulation_dir
@ -155,12 +155,12 @@ class IPCHandler:
self.status_file = os.path.join(simulation_dir, ENV_STATUS_FILE) self.status_file = os.path.join(simulation_dir, ENV_STATUS_FILE)
self._running = True self._running = True
# 确保目录存在 # Đảm bảo thư mục tồn tại
os.makedirs(self.commands_dir, exist_ok=True) os.makedirs(self.commands_dir, exist_ok=True)
os.makedirs(self.responses_dir, exist_ok=True) os.makedirs(self.responses_dir, exist_ok=True)
def update_status(self, status: str): 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: with open(self.status_file, 'w', encoding='utf-8') as f:
json.dump({ json.dump({
"status": status, "status": status,
@ -168,11 +168,11 @@ class IPCHandler:
}, f, ensure_ascii=False, indent=2) }, f, ensure_ascii=False, indent=2)
def poll_command(self) -> Optional[Dict[str, Any]]: 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): if not os.path.exists(self.commands_dir):
return None return None
# 获取命令文件(按时间排序) # Lấy file lệnh (sắp xếp theo thời gian)
command_files = [] command_files = []
for filename in os.listdir(self.commands_dir): for filename in os.listdir(self.commands_dir):
if filename.endswith('.json'): if filename.endswith('.json'):
@ -191,7 +191,7 @@ class IPCHandler:
return None return None
def send_response(self, command_id: str, status: str, result: Dict = None, error: str = None): def send_response(self, command_id: str, status: str, result: Dict = None, error: str = None):
"""发送响应""" """Gửi phản hồi"""
response = { response = {
"command_id": command_id, "command_id": command_id,
"status": status, "status": status,
@ -204,7 +204,7 @@ class IPCHandler:
with open(response_file, 'w', encoding='utf-8') as f: with open(response_file, 'w', encoding='utf-8') as f:
json.dump(response, f, ensure_ascii=False, indent=2) 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") command_file = os.path.join(self.commands_dir, f"{command_id}.json")
try: try:
os.remove(command_file) os.remove(command_file)
@ -213,49 +213,49 @@ class IPCHandler:
async def handle_interview(self, command_id: str, agent_id: int, prompt: str) -> bool: 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: Returns:
True 表示成功,False 表示失败 True là thành công, False là thất bại
""" """
try: try:
# 获取Agent # Lấy Agent
agent = self.agent_graph.get_agent(agent_id) agent = self.agent_graph.get_agent(agent_id)
# 创建Interview动作 # Tạo hành động Interview
interview_action = ManualAction( interview_action = ManualAction(
action_type=ActionType.INTERVIEW, action_type=ActionType.INTERVIEW,
action_args={"prompt": prompt} action_args={"prompt": prompt}
) )
# 执行Interview # Thực thi Interview
actions = {agent: interview_action} actions = {agent: interview_action}
await self.env.step(actions) await self.env.step(actions)
# 从数据库获取结果 # Lấy kết quả từ cơ sở dữ liệu
result = self._get_interview_result(agent_id) result = self._get_interview_result(agent_id)
self.send_response(command_id, "completed", result=result) 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 return True
except Exception as e: except Exception as e:
error_msg = str(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) self.send_response(command_id, "failed", error=error_msg)
return False return False
async def handle_batch_interview(self, command_id: str, interviews: List[Dict]) -> bool: 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: Args:
interviews: [{"agent_id": int, "prompt": str}, ...] interviews: [{"agent_id": int, "prompt": str}, ...]
""" """
try: try:
# 构建动作字典 # Tạo dict hành động
actions = {} actions = {}
agent_prompts = {} # 记录每个agent的prompt agent_prompts = {} # Ghi lại prompt của từng agent
for interview in interviews: for interview in interviews:
agent_id = interview.get("agent_id") agent_id = interview.get("agent_id")
@ -269,16 +269,16 @@ class IPCHandler:
) )
agent_prompts[agent_id] = prompt agent_prompts[agent_id] = prompt
except Exception as e: except Exception as e:
print(f" 警告: 无法获取Agent {agent_id}: {e}") print(f" Warning: cannot get Agent {agent_id}: {e}")
if not actions: if not actions:
self.send_response(command_id, "failed", error="没有有效的Agent") self.send_response(command_id, "failed", error="No valid agents")
return False return False
# 执行批量Interview # Thực thi Interview hàng loạt
await self.env.step(actions) await self.env.step(actions)
# 获取所有结果 # Lấy toàn bộ kết quả
results = {} results = {}
for agent_id in agent_prompts.keys(): for agent_id in agent_prompts.keys():
result = self._get_interview_result(agent_id) result = self._get_interview_result(agent_id)
@ -288,17 +288,17 @@ class IPCHandler:
"interviews_count": len(results), "interviews_count": len(results),
"results": results "results": results
}) })
print(f" 批量Interview完成: {len(results)} 个Agent") print(f" Batch interview completed: {len(results)} agents")
return True return True
except Exception as e: except Exception as e:
error_msg = str(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) self.send_response(command_id, "failed", error=error_msg)
return False return False
def _get_interview_result(self, agent_id: int) -> Dict[str, Any]: 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") db_path = os.path.join(self.simulation_dir, "reddit_simulation.db")
result = { result = {
@ -314,7 +314,7 @@ class IPCHandler:
conn = sqlite3.connect(db_path) conn = sqlite3.connect(db_path)
cursor = conn.cursor() cursor = conn.cursor()
# 查询最新的Interview记录 # Truy vấn bản ghi Interview mới nhất
cursor.execute(""" cursor.execute("""
SELECT user_id, info, created_at SELECT user_id, info, created_at
FROM trace FROM trace
@ -336,16 +336,16 @@ class IPCHandler:
conn.close() conn.close()
except Exception as e: except Exception as e:
print(f" 读取Interview结果失败: {e}") print(f" Failed to read Interview result: {e}")
return result return result
async def process_commands(self) -> bool: async def process_commands(self) -> bool:
""" """
处理所有待处理命令 Xử lý toàn bộ lệnh đang chờ
Returns: Returns:
True 表示继续运行,False 表示应该退出 True là tiếp tục chạy, False là nên thoát
""" """
command = self.poll_command() command = self.poll_command()
if not command: if not command:
@ -355,7 +355,7 @@ class IPCHandler:
command_type = command.get("command_type") command_type = command.get("command_type")
args = command.get("args", {}) 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: if command_type == CommandType.INTERVIEW:
await self.handle_interview( await self.handle_interview(
@ -373,19 +373,19 @@ class IPCHandler:
return True return True
elif command_type == CommandType.CLOSE_ENV: elif command_type == CommandType.CLOSE_ENV:
print("收到关闭环境命令") print("Received close environment command")
self.send_response(command_id, "completed", result={"message": "环境即将关闭"}) self.send_response(command_id, "completed", result={"message": "Environment will close soon"})
return False return False
else: 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 return True
class RedditSimulationRunner: 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 = [ AVAILABLE_ACTIONS = [
ActionType.LIKE_POST, ActionType.LIKE_POST,
ActionType.DISLIKE_POST, ActionType.DISLIKE_POST,
@ -404,11 +404,11 @@ class RedditSimulationRunner:
def __init__(self, config_path: str, wait_for_commands: bool = True): def __init__(self, config_path: str, wait_for_commands: bool = True):
""" """
初始化模拟运行器 Khởi tạo bộ chạy mô phỏng
Args: Args:
config_path: 配置文件路径 (simulation_config.json) config_path: Đường dẫn file cấu hình (simulation_config.json)
wait_for_commands: 模拟完成后是否等待命令(默认True) 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_path = config_path
self.config = self._load_config() self.config = self._load_config()
@ -419,47 +419,47 @@ class RedditSimulationRunner:
self.ipc_handler = None self.ipc_handler = None
def _load_config(self) -> Dict[str, Any]: 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: with open(self.config_path, 'r', encoding='utf-8') as f:
return json.load(f) return json.load(f)
def _get_profile_path(self) -> str: def _get_profile_path(self) -> str:
"""获取Profile文件路径""" """Lấy đường dẫn file Profile"""
return os.path.join(self.simulation_dir, "reddit_profiles.json") return os.path.join(self.simulation_dir, "reddit_profiles.json")
def _get_db_path(self) -> str: 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") return os.path.join(self.simulation_dir, "reddit_simulation.db")
def _create_model(self): def _create_model(self):
""" """
创建LLM模型 Tạo mô hình LLM
统一使用项目根目录 .env 文件中的配置(优先级最高): 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密钥 - LLM_API_KEY: API key
- LLM_BASE_URL: API基础URL - LLM_BASE_URL: URL cơ sở API
- LLM_MODEL_NAME: 模型名称 - 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_api_key = os.environ.get("LLM_API_KEY", "")
llm_base_url = os.environ.get("LLM_BASE_URL", "") llm_base_url = os.environ.get("LLM_BASE_URL", "")
llm_model = os.environ.get("LLM_MODEL_NAME", "") 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: if not llm_model:
llm_model = self.config.get("llm_model", "gpt-4o-mini") 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: if llm_api_key:
os.environ["OPENAI_API_KEY"] = llm_api_key os.environ["OPENAI_API_KEY"] = llm_api_key
if not os.environ.get("OPENAI_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: if llm_base_url:
os.environ["OPENAI_API_BASE_URL"] = 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( return ModelFactory.create(
model_platform=ModelPlatformType.OPENAI, model_platform=ModelPlatformType.OPENAI,
@ -473,7 +473,7 @@ class RedditSimulationRunner:
round_num: int round_num: int
) -> List: ) -> 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", {}) time_config = self.config.get("time_config", {})
agent_configs = self.config.get("agent_configs", []) agent_configs = self.config.get("agent_configs", [])
@ -521,16 +521,16 @@ class RedditSimulationRunner:
return active_agents return active_agents
async def run(self, max_rounds: int = None): async def run(self, max_rounds: int = None):
"""运行Reddit模拟 """Chạy mô phỏng Reddit
Args: 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("=" * 60)
print("OASIS Reddit模拟") print("OASIS Reddit Simulation")
print(f"配置文件: {self.config_path}") print(f"Config file: {self.config_path}")
print(f"模拟ID: {self.config.get('simulation_id', 'unknown')}") print(f"Simulation ID: {self.config.get('simulation_id', 'unknown')}")
print(f"等待命令模式: {'启用' if self.wait_for_commands else '禁用'}") print(f"Wait mode: {'enabled' if self.wait_for_commands else 'disabled'}")
print("=" * 60) print("=" * 60)
time_config = self.config.get("time_config", {}) time_config = self.config.get("time_config", {})
@ -538,28 +538,28 @@ class RedditSimulationRunner:
minutes_per_round = time_config.get("minutes_per_round", 30) minutes_per_round = time_config.get("minutes_per_round", 30)
total_rounds = (total_hours * 60) // minutes_per_round 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: if max_rounds is not None and max_rounds > 0:
original_rounds = total_rounds original_rounds = total_rounds
total_rounds = min(total_rounds, max_rounds) total_rounds = min(total_rounds, max_rounds)
if total_rounds < original_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"\nSimulation parameters:")
print(f" - 总模拟时长: {total_hours}小时") print(f" - Total simulation duration: {total_hours} hours")
print(f" - 每轮时间: {minutes_per_round}分钟") print(f" - Minutes per round: {minutes_per_round} min")
print(f" - 总轮数: {total_rounds}") print(f" - Total rounds: {total_rounds}")
if max_rounds: if max_rounds:
print(f" - 最大轮数限制: {max_rounds}") print(f" - Max round limit: {max_rounds}")
print(f" - Agent数量: {len(self.config.get('agent_configs', []))}") print(f" - Agent count: {len(self.config.get('agent_configs', []))}")
print("\n初始化LLM模型...") print("\nInitializing LLM model...")
model = self._create_model() model = self._create_model()
print("加载Agent Profile...") print("Loading Agent Profile...")
profile_path = self._get_profile_path() profile_path = self._get_profile_path()
if not os.path.exists(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 return
self.agent_graph = await generate_reddit_agent_graph( self.agent_graph = await generate_reddit_agent_graph(
@ -571,29 +571,29 @@ class RedditSimulationRunner:
db_path = self._get_db_path() db_path = self._get_db_path()
if os.path.exists(db_path): if os.path.exists(db_path):
os.remove(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( self.env = oasis.make(
agent_graph=self.agent_graph, agent_graph=self.agent_graph,
platform=oasis.DefaultPlatformType.REDDIT, platform=oasis.DefaultPlatformType.REDDIT,
database_path=db_path, 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() 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 = IPCHandler(self.simulation_dir, self.env, self.agent_graph)
self.ipc_handler.update_status("running") self.ipc_handler.update_status("running")
# 执行初始事件 # Thực thi sự kiện ban đầu
event_config = self.config.get("event_config", {}) event_config = self.config.get("event_config", {})
initial_posts = event_config.get("initial_posts", []) initial_posts = event_config.get("initial_posts", [])
if initial_posts: if initial_posts:
print(f"执行初始事件 ({len(initial_posts)}条初始帖子)...") print(f"Executing initial events ({len(initial_posts)} initial posts)...")
initial_actions = {} initial_actions = {}
for post in initial_posts: for post in initial_posts:
agent_id = post.get("poster_agent_id", 0) agent_id = post.get("poster_agent_id", 0)
@ -613,14 +613,14 @@ class RedditSimulationRunner:
action_args={"content": content} action_args={"content": content}
) )
except Exception as e: 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: if initial_actions:
await self.env.step(initial_actions) await self.env.step(initial_actions)
print(f" 已发布 {len(initial_actions)} 条初始帖子") print(f" Published {len(initial_actions)} initial posts")
# 主模拟循环 # Vòng lặp mô phỏng chính
print("\n开始模拟循环...") print("\nStarting simulation loop...")
start_time = datetime.now() start_time = datetime.now()
for round_num in range(total_rounds): for round_num in range(total_rounds):
@ -651,20 +651,20 @@ class RedditSimulationRunner:
f"- elapsed: {elapsed:.1f}s") f"- elapsed: {elapsed:.1f}s")
total_elapsed = (datetime.now() - start_time).total_seconds() total_elapsed = (datetime.now() - start_time).total_seconds()
print(f"\n模拟循环完成!") print(f"\nSimulation loop completed!")
print(f" - 总耗时: {total_elapsed:.1f}秒") print(f" - Total elapsed: {total_elapsed:.1f}s")
print(f" - 数据库: {db_path}") print(f" - Database: {db_path}")
# 是否进入等待命令模式 # Có vào chế độ chờ lệnh hay không
if self.wait_for_commands: if self.wait_for_commands:
print("\n" + "=" * 60) print("\n" + "=" * 60)
print("进入等待命令模式 - 环境保持运行") print("Entering wait mode - environment remains running")
print("支持的命令: interview, batch_interview, close_env") print("Supported commands: interview, batch_interview, close_env")
print("=" * 60) print("=" * 60)
self.ipc_handler.update_status("alive") self.ipc_handler.update_status("alive")
# 等待命令循环(使用全局 _shutdown_event) # Vòng lặp chờ lệnh (dùng _shutdown_event toàn cục)
try: try:
while not _shutdown_event.is_set(): while not _shutdown_event.is_set():
should_continue = await self.ipc_handler.process_commands() should_continue = await self.ipc_handler.process_commands()
@ -672,58 +672,58 @@ class RedditSimulationRunner:
break break
try: try:
await asyncio.wait_for(_shutdown_event.wait(), timeout=0.5) await asyncio.wait_for(_shutdown_event.wait(), timeout=0.5)
break # 收到退出信号 break # Nhận tín hiệu thoát
except asyncio.TimeoutError: except asyncio.TimeoutError:
pass pass
except KeyboardInterrupt: except KeyboardInterrupt:
print("\n收到中断信号") print("\nReceived interrupt signal")
except asyncio.CancelledError: except asyncio.CancelledError:
print("\n任务被取消") print("\nTask was cancelled")
except Exception as e: 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") self.ipc_handler.update_status("stopped")
await self.env.close() await self.env.close()
print("环境已关闭") print("Environment closed")
print("=" * 60) print("=" * 60)
async def main(): async def main():
parser = argparse.ArgumentParser(description='OASIS Reddit模拟') parser = argparse.ArgumentParser(description='OASIS Reddit Simulation')
parser.add_argument( parser.add_argument(
'--config', '--config',
type=str, type=str,
required=True, required=True,
help='配置文件路径 (simulation_config.json)' help='Config file path (simulation_config.json)'
) )
parser.add_argument( parser.add_argument(
'--max-rounds', '--max-rounds',
type=int, type=int,
default=None, default=None,
help='最大模拟轮数(可选,用于截断过长的模拟)' help='Max simulation rounds (optional, to truncate overly long simulations)'
) )
parser.add_argument( parser.add_argument(
'--no-wait', '--no-wait',
action='store_true', action='store_true',
default=False, default=False,
help='模拟完成后立即关闭环境,不进入等待命令模式' help='Close environment immediately after simulation, do not enter wait mode'
) )
args = parser.parse_args() args = parser.parse_args()
# 在 main 函数开始时创建 shutdown 事件 # Tạo sự kiện shutdown ở đầu hàm main
global _shutdown_event global _shutdown_event
_shutdown_event = asyncio.Event() _shutdown_event = asyncio.Event()
if not os.path.exists(args.config): if not os.path.exists(args.config):
print(f"错误: 配置文件不存在: {args.config}") print(f"Error: config file does not exist: {args.config}")
sys.exit(1) 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 "." simulation_dir = os.path.dirname(args.config) or "."
setup_oasis_logging(os.path.join(simulation_dir, "log")) setup_oasis_logging(os.path.join(simulation_dir, "log"))
@ -736,20 +736,20 @@ async def main():
def setup_signal_handlers(): 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): def signal_handler(signum, frame):
global _cleanup_done global _cleanup_done
sig_name = "SIGTERM" if signum == signal.SIGTERM else "SIGINT" 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: if not _cleanup_done:
_cleanup_done = True _cleanup_done = True
if _shutdown_event: if _shutdown_event:
_shutdown_event.set() _shutdown_event.set()
else: else:
# 重复收到信号才强制退出 # Chỉ buộc thoát khi nhận tín hiệu lặp lại
print("强制退出...") print("Force exit...")
sys.exit(1) sys.exit(1)
signal.signal(signal.SIGTERM, signal_handler) signal.signal(signal.SIGTERM, signal_handler)
@ -761,9 +761,9 @@ if __name__ == "__main__":
try: try:
asyncio.run(main()) asyncio.run(main())
except KeyboardInterrupt: except KeyboardInterrupt:
print("\n程序被中断") print("\nProgram interrupted")
except SystemExit: except SystemExit:
pass pass
finally: finally:
print("模拟进程已退出") print("Simulation process exited")

View File

@ -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
功能特性: 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
- 支持通过IPC接收Interview命令 - Hỗ trợ nhận lệnh Interview qua IPC
- 支持单个Agent采访和批量采访 - 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
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 import argparse
@ -25,18 +25,18 @@ import sqlite3
from datetime import datetime from datetime import datetime
from typing import Dict, Any, List, Optional 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 _shutdown_event = None
_cleanup_done = False _cleanup_done = False
# 添加项目路径 # Thêm đường dẫn dự án
_scripts_dir = os.path.dirname(os.path.abspath(__file__)) _scripts_dir = os.path.dirname(os.path.abspath(__file__))
_backend_dir = os.path.abspath(os.path.join(_scripts_dir, '..')) _backend_dir = os.path.abspath(os.path.join(_scripts_dir, '..'))
_project_root = os.path.abspath(os.path.join(_backend_dir, '..')) _project_root = os.path.abspath(os.path.join(_backend_dir, '..'))
sys.path.insert(0, _scripts_dir) sys.path.insert(0, _scripts_dir)
sys.path.insert(0, _backend_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 from dotenv import load_dotenv
_env_file = os.path.join(_project_root, '.env') _env_file = os.path.join(_project_root, '.env')
if os.path.exists(_env_file): if os.path.exists(_env_file):
@ -51,7 +51,7 @@ import re
class UnicodeFormatter(logging.Formatter): 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})') UNICODE_ESCAPE_PATTERN = re.compile(r'\\u([0-9a-fA-F]{4})')
@ -68,24 +68,24 @@ class UnicodeFormatter(logging.Formatter):
class MaxTokensWarningFilter(logging.Filter): 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): 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(): if "max_tokens" in record.getMessage() and "Invalid or missing" in record.getMessage():
return False return False
return True 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()) logging.getLogger().addFilter(MaxTokensWarningFilter())
def setup_oasis_logging(log_dir: str): 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) os.makedirs(log_dir, exist_ok=True)
# 清理旧的日志文件 # Dọn các file log cũ
for f in os.listdir(log_dir): for f in os.listdir(log_dir):
old_log = os.path.join(log_dir, f) old_log = os.path.join(log_dir, f)
if os.path.isfile(old_log) and f.endswith('.log'): if os.path.isfile(old_log) and f.endswith('.log'):
@ -126,25 +126,25 @@ try:
generate_twitter_agent_graph generate_twitter_agent_graph
) )
except ImportError as e: except ImportError as e:
print(f"错误: 缺少依赖 {e}") print(f"Error: missing dependency {e}")
print("请先安装: pip install oasis-ai camel-ai") print("Please install first: pip install oasis-ai camel-ai")
sys.exit(1) sys.exit(1)
# IPC相关常量 # Hằng số liên quan IPC
IPC_COMMANDS_DIR = "ipc_commands" IPC_COMMANDS_DIR = "ipc_commands"
IPC_RESPONSES_DIR = "ipc_responses" IPC_RESPONSES_DIR = "ipc_responses"
ENV_STATUS_FILE = "env_status.json" ENV_STATUS_FILE = "env_status.json"
class CommandType: class CommandType:
"""命令类型常量""" """Hằng số loại lệnh"""
INTERVIEW = "interview" INTERVIEW = "interview"
BATCH_INTERVIEW = "batch_interview" BATCH_INTERVIEW = "batch_interview"
CLOSE_ENV = "close_env" CLOSE_ENV = "close_env"
class IPCHandler: class IPCHandler:
"""IPC命令处理器""" """Bộ xử lý lệnh IPC"""
def __init__(self, simulation_dir: str, env, agent_graph): def __init__(self, simulation_dir: str, env, agent_graph):
self.simulation_dir = simulation_dir self.simulation_dir = simulation_dir
@ -155,12 +155,12 @@ class IPCHandler:
self.status_file = os.path.join(simulation_dir, ENV_STATUS_FILE) self.status_file = os.path.join(simulation_dir, ENV_STATUS_FILE)
self._running = True self._running = True
# 确保目录存在 # Đảm bảo thư mục tồn tại
os.makedirs(self.commands_dir, exist_ok=True) os.makedirs(self.commands_dir, exist_ok=True)
os.makedirs(self.responses_dir, exist_ok=True) os.makedirs(self.responses_dir, exist_ok=True)
def update_status(self, status: str): 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: with open(self.status_file, 'w', encoding='utf-8') as f:
json.dump({ json.dump({
"status": status, "status": status,
@ -168,11 +168,11 @@ class IPCHandler:
}, f, ensure_ascii=False, indent=2) }, f, ensure_ascii=False, indent=2)
def poll_command(self) -> Optional[Dict[str, Any]]: 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): if not os.path.exists(self.commands_dir):
return None return None
# 获取命令文件(按时间排序) # Lấy file lệnh (sắp xếp theo thời gian)
command_files = [] command_files = []
for filename in os.listdir(self.commands_dir): for filename in os.listdir(self.commands_dir):
if filename.endswith('.json'): if filename.endswith('.json'):
@ -191,7 +191,7 @@ class IPCHandler:
return None return None
def send_response(self, command_id: str, status: str, result: Dict = None, error: str = None): def send_response(self, command_id: str, status: str, result: Dict = None, error: str = None):
"""发送响应""" """Gửi phản hồi"""
response = { response = {
"command_id": command_id, "command_id": command_id,
"status": status, "status": status,
@ -204,7 +204,7 @@ class IPCHandler:
with open(response_file, 'w', encoding='utf-8') as f: with open(response_file, 'w', encoding='utf-8') as f:
json.dump(response, f, ensure_ascii=False, indent=2) 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") command_file = os.path.join(self.commands_dir, f"{command_id}.json")
try: try:
os.remove(command_file) os.remove(command_file)
@ -213,49 +213,49 @@ class IPCHandler:
async def handle_interview(self, command_id: str, agent_id: int, prompt: str) -> bool: 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: Returns:
True 表示成功,False 表示失败 True là thành công, False là thất bại
""" """
try: try:
# 获取Agent # Lấy Agent
agent = self.agent_graph.get_agent(agent_id) agent = self.agent_graph.get_agent(agent_id)
# 创建Interview动作 # Tạo hành động Interview
interview_action = ManualAction( interview_action = ManualAction(
action_type=ActionType.INTERVIEW, action_type=ActionType.INTERVIEW,
action_args={"prompt": prompt} action_args={"prompt": prompt}
) )
# 执行Interview # Thực thi Interview
actions = {agent: interview_action} actions = {agent: interview_action}
await self.env.step(actions) await self.env.step(actions)
# 从数据库获取结果 # Lấy kết quả từ cơ sở dữ liệu
result = self._get_interview_result(agent_id) result = self._get_interview_result(agent_id)
self.send_response(command_id, "completed", result=result) 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 return True
except Exception as e: except Exception as e:
error_msg = str(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) self.send_response(command_id, "failed", error=error_msg)
return False return False
async def handle_batch_interview(self, command_id: str, interviews: List[Dict]) -> bool: 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: Args:
interviews: [{"agent_id": int, "prompt": str}, ...] interviews: [{"agent_id": int, "prompt": str}, ...]
""" """
try: try:
# 构建动作字典 # Tạo dict hành động
actions = {} actions = {}
agent_prompts = {} # 记录每个agent的prompt agent_prompts = {} # Ghi lại prompt của từng agent
for interview in interviews: for interview in interviews:
agent_id = interview.get("agent_id") agent_id = interview.get("agent_id")
@ -269,16 +269,16 @@ class IPCHandler:
) )
agent_prompts[agent_id] = prompt agent_prompts[agent_id] = prompt
except Exception as e: except Exception as e:
print(f" 警告: 无法获取Agent {agent_id}: {e}") print(f" Warning: cannot get Agent {agent_id}: {e}")
if not actions: if not actions:
self.send_response(command_id, "failed", error="没有有效的Agent") self.send_response(command_id, "failed", error="No valid agents")
return False return False
# 执行批量Interview # Thực thi Interview hàng loạt
await self.env.step(actions) await self.env.step(actions)
# 获取所有结果 # Lấy toàn bộ kết quả
results = {} results = {}
for agent_id in agent_prompts.keys(): for agent_id in agent_prompts.keys():
result = self._get_interview_result(agent_id) result = self._get_interview_result(agent_id)
@ -288,17 +288,17 @@ class IPCHandler:
"interviews_count": len(results), "interviews_count": len(results),
"results": results "results": results
}) })
print(f" 批量Interview完成: {len(results)} 个Agent") print(f" Batch interview completed: {len(results)} agents")
return True return True
except Exception as e: except Exception as e:
error_msg = str(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) self.send_response(command_id, "failed", error=error_msg)
return False return False
def _get_interview_result(self, agent_id: int) -> Dict[str, Any]: 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") db_path = os.path.join(self.simulation_dir, "twitter_simulation.db")
result = { result = {
@ -314,7 +314,7 @@ class IPCHandler:
conn = sqlite3.connect(db_path) conn = sqlite3.connect(db_path)
cursor = conn.cursor() cursor = conn.cursor()
# 查询最新的Interview记录 # Truy vấn bản ghi Interview mới nhất
cursor.execute(""" cursor.execute("""
SELECT user_id, info, created_at SELECT user_id, info, created_at
FROM trace FROM trace
@ -336,16 +336,16 @@ class IPCHandler:
conn.close() conn.close()
except Exception as e: except Exception as e:
print(f" 读取Interview结果失败: {e}") print(f" Failed to read Interview result: {e}")
return result return result
async def process_commands(self) -> bool: async def process_commands(self) -> bool:
""" """
处理所有待处理命令 Xử lý toàn bộ lệnh đang chờ
Returns: Returns:
True 表示继续运行,False 表示应该退出 True là tiếp tục chạy, False là nên thoát
""" """
command = self.poll_command() command = self.poll_command()
if not command: if not command:
@ -355,7 +355,7 @@ class IPCHandler:
command_type = command.get("command_type") command_type = command.get("command_type")
args = command.get("args", {}) 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: if command_type == CommandType.INTERVIEW:
await self.handle_interview( await self.handle_interview(
@ -373,19 +373,19 @@ class IPCHandler:
return True return True
elif command_type == CommandType.CLOSE_ENV: elif command_type == CommandType.CLOSE_ENV:
print("收到关闭环境命令") print("Received close environment command")
self.send_response(command_id, "completed", result={"message": "环境即将关闭"}) self.send_response(command_id, "completed", result={"message": "Environment will close soon"})
return False return False
else: 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 return True
class TwitterSimulationRunner: 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 = [ AVAILABLE_ACTIONS = [
ActionType.CREATE_POST, ActionType.CREATE_POST,
ActionType.LIKE_POST, ActionType.LIKE_POST,
@ -397,11 +397,11 @@ class TwitterSimulationRunner:
def __init__(self, config_path: str, wait_for_commands: bool = True): def __init__(self, config_path: str, wait_for_commands: bool = True):
""" """
初始化模拟运行器 Khởi tạo bộ chạy mô phỏng
Args: Args:
config_path: 配置文件路径 (simulation_config.json) config_path: Đường dẫn file cấu hình (simulation_config.json)
wait_for_commands: 模拟完成后是否等待命令(默认True) 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_path = config_path
self.config = self._load_config() self.config = self._load_config()
@ -412,47 +412,47 @@ class TwitterSimulationRunner:
self.ipc_handler = None self.ipc_handler = None
def _load_config(self) -> Dict[str, Any]: 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: with open(self.config_path, 'r', encoding='utf-8') as f:
return json.load(f) return json.load(f)
def _get_profile_path(self) -> str: 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") return os.path.join(self.simulation_dir, "twitter_profiles.csv")
def _get_db_path(self) -> str: 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") return os.path.join(self.simulation_dir, "twitter_simulation.db")
def _create_model(self): def _create_model(self):
""" """
创建LLM模型 Tạo mô hình LLM
统一使用项目根目录 .env 文件中的配置(优先级最高): 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密钥 - LLM_API_KEY: API key
- LLM_BASE_URL: API基础URL - LLM_BASE_URL: URL cơ sở API
- LLM_MODEL_NAME: 模型名称 - 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_api_key = os.environ.get("LLM_API_KEY", "")
llm_base_url = os.environ.get("LLM_BASE_URL", "") llm_base_url = os.environ.get("LLM_BASE_URL", "")
llm_model = os.environ.get("LLM_MODEL_NAME", "") 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: if not llm_model:
llm_model = self.config.get("llm_model", "gpt-4o-mini") 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: if llm_api_key:
os.environ["OPENAI_API_KEY"] = llm_api_key os.environ["OPENAI_API_KEY"] = llm_api_key
if not os.environ.get("OPENAI_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: if llm_base_url:
os.environ["OPENAI_API_BASE_URL"] = 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( return ModelFactory.create(
model_platform=ModelPlatformType.OPENAI, model_platform=ModelPlatformType.OPENAI,
@ -466,24 +466,24 @@ class TwitterSimulationRunner:
round_num: int round_num: int
) -> List: ) -> 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: Args:
env: OASIS环境 env: Môi trường OASIS
current_hour: 当前模拟小时(0-23) current_hour: Giờ mô phỏng hiện tại (0-23)
round_num: 当前轮数 round_num: Vòng hiện tại
Returns: Returns:
激活的Agent列表 Danh sách Agent được kích hoạt
""" """
time_config = self.config.get("time_config", {}) time_config = self.config.get("time_config", {})
agent_configs = self.config.get("agent_configs", []) 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_min = time_config.get("agents_per_hour_min", 5)
base_max = time_config.get("agents_per_hour_max", 20) 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]) 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]) off_peak_hours = time_config.get("off_peak_hours", [0, 1, 2, 3, 4, 5])
@ -496,28 +496,28 @@ class TwitterSimulationRunner:
target_count = int(random.uniform(base_min, base_max) * multiplier) 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 = [] candidates = []
for cfg in agent_configs: for cfg in agent_configs:
agent_id = cfg.get("agent_id", 0) agent_id = cfg.get("agent_id", 0)
active_hours = cfg.get("active_hours", list(range(8, 23))) active_hours = cfg.get("active_hours", list(range(8, 23)))
activity_level = cfg.get("activity_level", 0.5) 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: if current_hour not in active_hours:
continue continue
# 根据活跃度计算概率 # Tính xác suất theo mức độ hoạt động
if random.random() < activity_level: if random.random() < activity_level:
candidates.append(agent_id) candidates.append(agent_id)
# 随机选择 # Chọn ngẫu nhiên
selected_ids = random.sample( selected_ids = random.sample(
candidates, candidates,
min(target_count, len(candidates)) min(target_count, len(candidates))
) if candidates else [] ) if candidates else []
# 转换为Agent对象 # Chuyển thành đối tượng Agent
active_agents = [] active_agents = []
for agent_id in selected_ids: for agent_id in selected_ids:
try: try:
@ -529,50 +529,50 @@ class TwitterSimulationRunner:
return active_agents return active_agents
async def run(self, max_rounds: int = None): async def run(self, max_rounds: int = None):
"""运行Twitter模拟 """Chạy mô phỏng Twitter
Args: 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("=" * 60)
print("OASIS Twitter模拟") print("OASIS Twitter Simulation")
print(f"配置文件: {self.config_path}") print(f"Config file: {self.config_path}")
print(f"模拟ID: {self.config.get('simulation_id', 'unknown')}") print(f"Simulation ID: {self.config.get('simulation_id', 'unknown')}")
print(f"等待命令模式: {'启用' if self.wait_for_commands else '禁用'}") print(f"Wait mode: {'enabled' if self.wait_for_commands else 'disabled'}")
print("=" * 60) print("=" * 60)
# 加载时间配置 # Tải cấu hình thời gian
time_config = self.config.get("time_config", {}) time_config = self.config.get("time_config", {})
total_hours = time_config.get("total_simulation_hours", 72) total_hours = time_config.get("total_simulation_hours", 72)
minutes_per_round = time_config.get("minutes_per_round", 30) 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 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: if max_rounds is not None and max_rounds > 0:
original_rounds = total_rounds original_rounds = total_rounds
total_rounds = min(total_rounds, max_rounds) total_rounds = min(total_rounds, max_rounds)
if total_rounds < original_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"\nSimulation parameters:")
print(f" - 总模拟时长: {total_hours}小时") print(f" - Total simulation duration: {total_hours} hours")
print(f" - 每轮时间: {minutes_per_round}分钟") print(f" - Minutes per round: {minutes_per_round} min")
print(f" - 总轮数: {total_rounds}") print(f" - Total rounds: {total_rounds}")
if max_rounds: if max_rounds:
print(f" - 最大轮数限制: {max_rounds}") print(f" - Max round limit: {max_rounds}")
print(f" - Agent数量: {len(self.config.get('agent_configs', []))}") print(f" - Agent count: {len(self.config.get('agent_configs', []))}")
# 创建模型 # Tạo mô hình
print("\n初始化LLM模型...") print("\nInitializing LLM model...")
model = self._create_model() model = self._create_model()
# 加载Agent图 # Tải đồ thị Agent
print("加载Agent Profile...") print("Loading Agent Profile...")
profile_path = self._get_profile_path() profile_path = self._get_profile_path()
if not os.path.exists(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 return
self.agent_graph = await generate_twitter_agent_graph( self.agent_graph = await generate_twitter_agent_graph(
@ -581,34 +581,34 @@ class TwitterSimulationRunner:
available_actions=self.AVAILABLE_ACTIONS, available_actions=self.AVAILABLE_ACTIONS,
) )
# 数据库路径 # Đường dẫn cơ sở dữ liệu
db_path = self._get_db_path() db_path = self._get_db_path()
if os.path.exists(db_path): if os.path.exists(db_path):
os.remove(db_path) os.remove(db_path)
print(f"已删除旧数据库: {db_path}") print(f"Removed old database: {db_path}")
# 创建环境 # Tạo môi trường
print("创建OASIS环境...") print("Creating OASIS environment...")
self.env = oasis.make( self.env = oasis.make(
agent_graph=self.agent_graph, agent_graph=self.agent_graph,
platform=oasis.DefaultPlatformType.TWITTER, platform=oasis.DefaultPlatformType.TWITTER,
database_path=db_path, 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() 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 = IPCHandler(self.simulation_dir, self.env, self.agent_graph)
self.ipc_handler.update_status("running") self.ipc_handler.update_status("running")
# 执行初始事件 # Thực thi sự kiện ban đầu
event_config = self.config.get("event_config", {}) event_config = self.config.get("event_config", {})
initial_posts = event_config.get("initial_posts", []) initial_posts = event_config.get("initial_posts", [])
if initial_posts: if initial_posts:
print(f"执行初始事件 ({len(initial_posts)}条初始帖子)...") print(f"Executing initial events ({len(initial_posts)} initial posts)...")
initial_actions = {} initial_actions = {}
for post in initial_posts: for post in initial_posts:
agent_id = post.get("poster_agent_id", 0) agent_id = post.get("poster_agent_id", 0)
@ -620,23 +620,23 @@ class TwitterSimulationRunner:
action_args={"content": content} action_args={"content": content}
) )
except Exception as e: 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: if initial_actions:
await self.env.step(initial_actions) await self.env.step(initial_actions)
print(f" 已发布 {len(initial_actions)} 条初始帖子") print(f" Published {len(initial_actions)} initial posts")
# 主模拟循环 # Vòng lặp mô phỏng chính
print("\n开始模拟循环...") print("\nStarting simulation loop...")
start_time = datetime.now() start_time = datetime.now()
for round_num in range(total_rounds): 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_minutes = round_num * minutes_per_round
simulated_hour = (simulated_minutes // 60) % 24 simulated_hour = (simulated_minutes // 60) % 24
simulated_day = simulated_minutes // (60 * 24) + 1 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( active_agents = self._get_active_agents_for_round(
self.env, simulated_hour, round_num self.env, simulated_hour, round_num
) )
@ -644,16 +644,16 @@ class TwitterSimulationRunner:
if not active_agents: if not active_agents:
continue continue
# 构建动作 # Tạo hành động
actions = { actions = {
agent: LLMAction() agent: LLMAction()
for _, agent in active_agents for _, agent in active_agents
} }
# 执行动作 # Thực thi hành động
await self.env.step(actions) await self.env.step(actions)
# 打印进度 # In tiến độ
if (round_num + 1) % 10 == 0 or round_num == 0: if (round_num + 1) % 10 == 0 or round_num == 0:
elapsed = (datetime.now() - start_time).total_seconds() elapsed = (datetime.now() - start_time).total_seconds()
progress = (round_num + 1) / total_rounds * 100 progress = (round_num + 1) / total_rounds * 100
@ -663,20 +663,20 @@ class TwitterSimulationRunner:
f"- elapsed: {elapsed:.1f}s") f"- elapsed: {elapsed:.1f}s")
total_elapsed = (datetime.now() - start_time).total_seconds() total_elapsed = (datetime.now() - start_time).total_seconds()
print(f"\n模拟循环完成!") print(f"\nSimulation loop completed!")
print(f" - 总耗时: {total_elapsed:.1f}秒") print(f" - Total elapsed: {total_elapsed:.1f}s")
print(f" - 数据库: {db_path}") print(f" - Database: {db_path}")
# 是否进入等待命令模式 # Có vào chế độ chờ lệnh hay không
if self.wait_for_commands: if self.wait_for_commands:
print("\n" + "=" * 60) print("\n" + "=" * 60)
print("进入等待命令模式 - 环境保持运行") print("Entering wait mode - environment remains running")
print("支持的命令: interview, batch_interview, close_env") print("Supported commands: interview, batch_interview, close_env")
print("=" * 60) print("=" * 60)
self.ipc_handler.update_status("alive") self.ipc_handler.update_status("alive")
# 等待命令循环(使用全局 _shutdown_event) # Vòng lặp chờ lệnh (dùng _shutdown_event toàn cục)
try: try:
while not _shutdown_event.is_set(): while not _shutdown_event.is_set():
should_continue = await self.ipc_handler.process_commands() should_continue = await self.ipc_handler.process_commands()
@ -684,58 +684,58 @@ class TwitterSimulationRunner:
break break
try: try:
await asyncio.wait_for(_shutdown_event.wait(), timeout=0.5) await asyncio.wait_for(_shutdown_event.wait(), timeout=0.5)
break # 收到退出信号 break # Nhận tín hiệu thoát
except asyncio.TimeoutError: except asyncio.TimeoutError:
pass pass
except KeyboardInterrupt: except KeyboardInterrupt:
print("\n收到中断信号") print("\nReceived interrupt signal")
except asyncio.CancelledError: except asyncio.CancelledError:
print("\n任务被取消") print("\nTask was cancelled")
except Exception as e: 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") self.ipc_handler.update_status("stopped")
await self.env.close() await self.env.close()
print("环境已关闭") print("Environment closed")
print("=" * 60) print("=" * 60)
async def main(): async def main():
parser = argparse.ArgumentParser(description='OASIS Twitter模拟') parser = argparse.ArgumentParser(description='OASIS Twitter Simulation')
parser.add_argument( parser.add_argument(
'--config', '--config',
type=str, type=str,
required=True, required=True,
help='配置文件路径 (simulation_config.json)' help='Config file path (simulation_config.json)'
) )
parser.add_argument( parser.add_argument(
'--max-rounds', '--max-rounds',
type=int, type=int,
default=None, default=None,
help='最大模拟轮数(可选,用于截断过长的模拟)' help='Max simulation rounds (optional, to truncate overly long simulations)'
) )
parser.add_argument( parser.add_argument(
'--no-wait', '--no-wait',
action='store_true', action='store_true',
default=False, default=False,
help='模拟完成后立即关闭环境,不进入等待命令模式' help='Close environment immediately after simulation, do not enter wait mode'
) )
args = parser.parse_args() args = parser.parse_args()
# 在 main 函数开始时创建 shutdown 事件 # Tạo sự kiện shutdown ở đầu hàm main
global _shutdown_event global _shutdown_event
_shutdown_event = asyncio.Event() _shutdown_event = asyncio.Event()
if not os.path.exists(args.config): if not os.path.exists(args.config):
print(f"错误: 配置文件不存在: {args.config}") print(f"Error: config file does not exist: {args.config}")
sys.exit(1) 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 "." simulation_dir = os.path.dirname(args.config) or "."
setup_oasis_logging(os.path.join(simulation_dir, "log")) setup_oasis_logging(os.path.join(simulation_dir, "log"))
@ -748,20 +748,20 @@ async def main():
def setup_signal_handlers(): 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): def signal_handler(signum, frame):
global _cleanup_done global _cleanup_done
sig_name = "SIGTERM" if signum == signal.SIGTERM else "SIGINT" 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: if not _cleanup_done:
_cleanup_done = True _cleanup_done = True
if _shutdown_event: if _shutdown_event:
_shutdown_event.set() _shutdown_event.set()
else: else:
# 重复收到信号才强制退出 # Chỉ buộc thoát khi nhận tín hiệu lặp lại
print("强制退出...") print("Force exit...")
sys.exit(1) sys.exit(1)
signal.signal(signal.SIGTERM, signal_handler) signal.signal(signal.SIGTERM, signal_handler)
@ -773,8 +773,8 @@ if __name__ == "__main__":
try: try:
asyncio.run(main()) asyncio.run(main())
except KeyboardInterrupt: except KeyboardInterrupt:
print("\n程序被中断") print("\nProgram interrupted")
except SystemExit: except SystemExit:
pass pass
finally: finally:
print("模拟进程已退出") print("Simulation process exited")

View File

@ -1,8 +1,8 @@
""" """
测试Profile格式生成是否符合OASIS要求 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生成CSV格式 1. Twitter Profile sinh ở định dạng CSV.
2. Reddit Profile生成JSON详细格式 2. Reddit Profile sinh ở định dạng JSON chi tiết.
""" """
import os import os
@ -11,19 +11,19 @@ import json
import csv import csv
import tempfile import tempfile
# 添加项目路径 # Thêm đường dẫn dự án
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from app.services.oasis_profile_generator import OasisProfileGenerator, OasisAgentProfile from app.services.oasis_profile_generator import OasisProfileGenerator, OasisAgentProfile
def test_profile_formats(): def test_profile_formats():
"""测试Profile格式""" """Kiểm tra định dạng Profile"""
print("=" * 60) print("=" * 60)
print("OASIS Profile格式测试") print("Kiểm tra định dạng OASIS Profile")
print("=" * 60) print("=" * 60)
# 创建测试Profile数据 # Tạo dữ liệu Profile kiểm thử
test_profiles = [ test_profiles = [
OasisAgentProfile( OasisAgentProfile(
user_id=0, user_id=0,
@ -63,84 +63,84 @@ def test_profile_formats():
generator = OasisProfileGenerator.__new__(OasisProfileGenerator) generator = OasisProfileGenerator.__new__(OasisProfileGenerator)
# 使用临时目录 # Dùng thư mục tạm
with tempfile.TemporaryDirectory() as temp_dir: with tempfile.TemporaryDirectory() as temp_dir:
twitter_path = os.path.join(temp_dir, "twitter_profiles.csv") twitter_path = os.path.join(temp_dir, "twitter_profiles.csv")
reddit_path = os.path.join(temp_dir, "reddit_profiles.json") reddit_path = os.path.join(temp_dir, "reddit_profiles.json")
# 测试Twitter CSV格式 # Kiểm tra định dạng Twitter CSV
print("\n1. 测试Twitter Profile (CSV格式)") print("\n1. Kiểm tra Twitter Profile (định dạng CSV)")
print("-" * 40) print("-" * 40)
generator._save_twitter_csv(test_profiles, twitter_path) 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: with open(twitter_path, 'r', encoding='utf-8') as f:
reader = csv.DictReader(f) reader = csv.DictReader(f)
rows = list(reader) rows = list(reader)
print(f" 文件: {twitter_path}") print(f" File: {twitter_path}")
print(f" 行数: {len(rows)}") print(f" Rows: {len(rows)}")
print(f" 表头: {list(rows[0].keys())}") print(f" Headers: {list(rows[0].keys())}")
print(f"\n 示例数据 (第1行):") print(f"\n Sample data (row 1):")
for key, value in rows[0].items(): for key, value in rows[0].items():
print(f" {key}: {value}") 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', required_twitter_fields = ['user_id', 'user_name', 'name', 'bio',
'friend_count', 'follower_count', 'statuses_count', 'created_at'] 'friend_count', 'follower_count', 'statuses_count', 'created_at']
missing = set(required_twitter_fields) - set(rows[0].keys()) missing = set(required_twitter_fields) - set(rows[0].keys())
if missing: if missing:
print(f"\n [错误] 缺少字段: {missing}") print(f"\n [ERROR] Missing fields: {missing}")
else: else:
print(f"\n [通过] 所有必需字段都存在") print(f"\n [PASS] All required fields are present")
# 测试Reddit JSON格式 # Kiểm tra định dạng Reddit JSON
print("\n2. 测试Reddit Profile (JSON详细格式)") print("\n2. Kiểm tra Reddit Profile (định dạng JSON chi tiết)")
print("-" * 40) print("-" * 40)
generator._save_reddit_json(test_profiles, reddit_path) 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: with open(reddit_path, 'r', encoding='utf-8') as f:
reddit_data = json.load(f) reddit_data = json.load(f)
print(f" 文件: {reddit_path}") print(f" File: {reddit_path}")
print(f" 条目数: {len(reddit_data)}") print(f" Entries: {len(reddit_data)}")
print(f" 字段: {list(reddit_data[0].keys())}") print(f" Fields: {list(reddit_data[0].keys())}")
print(f"\n 示例数据 (第1条):") print(f"\n Sample data (entry 1):")
print(json.dumps(reddit_data[0], ensure_ascii=False, indent=4)) 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'] required_reddit_fields = ['realname', 'username', 'bio', 'persona']
optional_reddit_fields = ['age', 'gender', 'mbti', 'country', 'profession', 'interested_topics'] optional_reddit_fields = ['age', 'gender', 'mbti', 'country', 'profession', 'interested_topics']
missing = set(required_reddit_fields) - set(reddit_data[0].keys()) missing = set(required_reddit_fields) - set(reddit_data[0].keys())
if missing: if missing:
print(f"\n [错误] 缺少必需字段: {missing}") print(f"\n [ERROR] Missing required fields: {missing}")
else: else:
print(f"\n [通过] 所有必需字段都存在") print(f"\n [PASS] All required fields are present")
present_optional = set(optional_reddit_fields) & set(reddit_data[0].keys()) 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("\n" + "=" * 60)
print("测试完成!") print("Test completed!")
print("=" * 60) print("=" * 60)
def show_expected_formats(): def show_expected_formats():
"""显示OASIS期望的格式""" """Hiển thị định dạng OASIS mong đợi"""
print("\n" + "=" * 60) print("\n" + "=" * 60)
print("OASIS 期望的Profile格式参考") print("Tham chiếu định dạng Profile OASIS mong đợi")
print("=" * 60) print("=" * 60)
print("\n1. Twitter Profile (CSV格式)") print("\n1. Twitter Profile (định dạng CSV)")
print("-" * 40) print("-" * 40)
twitter_example = """user_id,user_name,name,bio,friend_count,follower_count,statuses_count,created_at 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 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""" 1,user1,User One,Tech enthusiast and coffee lover.,200,250,1000,2023-01-02"""
print(twitter_example) print(twitter_example)
print("\n2. Reddit Profile (JSON详细格式)") print("\n2. Reddit Profile (định dạng JSON chi tiết)")
print("-" * 40) print("-" * 40)
reddit_example = [ reddit_example = [
{ {