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 warnings
# 抑制 multiprocessing resource_tracker 的警告(来自第三方库如 transformers
# 需要在所有其他导入之前设置
# Ẩn cảnh báo từ multiprocessing resource_tracker (đến từ thư viện bên thứ ba như transformers)
# Cần đặt trước mọi import khác
warnings.filterwarnings("ignore", message=".*resource_tracker.*")
from flask import Flask, request
@ -17,64 +17,64 @@ from .utils.logger import setup_logger, get_logger
def create_app(config_class=Config):
"""Flask应用工厂函数"""
"""Hàm factory để khởi tạo Flask app"""
app = Flask(__name__)
app.config.from_object(config_class)
# 设置JSON编码确保中文直接显示而不是 \uXXXX 格式)
# Flask >= 2.3 使用 app.json.ensure_ascii旧版本使用 JSON_AS_ASCII 配置
# Thiết lập JSON encoding: đảm bảo tiếng Trung hiển thị trực tiếp (không ở dạng \uXXXX)
# Flask >= 2.3 dùng app.json.ensure_ascii, phiên bản cũ dùng JSON_AS_ASCII
if hasattr(app, 'json') and hasattr(app.json, 'ensure_ascii'):
app.json.ensure_ascii = False
# 设置日志
# Thiết lập logger
logger = setup_logger('mirofish')
# 只在 reloader 子进程中打印启动信息(避免 debug 模式下打印两次)
# Chỉ in log khởi động ở tiến trình con của reloader (tránh in 2 lần khi debug)
is_reloader_process = os.environ.get('WERKZEUG_RUN_MAIN') == 'true'
debug_mode = app.config.get('DEBUG', False)
should_log_startup = not debug_mode or is_reloader_process
if should_log_startup:
logger.info("=" * 50)
logger.info("MiroFish Backend 启动中...")
logger.info("MiroFish Backend is starting...")
logger.info("=" * 50)
# 启用CORS
# Bật CORS
CORS(app, resources={r"/api/*": {"origins": "*"}})
# 注册模拟进程清理函数(确保服务器关闭时终止所有模拟进程)
# Đăng ký hàm dọn tiến trình mô phỏng (đảm bảo dừng toàn bộ khi server tắt)
from .services.simulation_runner import SimulationRunner
SimulationRunner.register_cleanup()
if should_log_startup:
logger.info("已注册模拟进程清理函数")
logger.info("Simulation process cleanup hook registered")
# 请求日志中间件
# Middleware log request
@app.before_request
def log_request():
logger = get_logger('mirofish.request')
logger.debug(f"请求: {request.method} {request.path}")
logger.debug(f"Request: {request.method} {request.path}")
if request.content_type and 'json' in request.content_type:
logger.debug(f"请求体: {request.get_json(silent=True)}")
logger.debug(f"Request body: {request.get_json(silent=True)}")
@app.after_request
def log_response(response):
logger = get_logger('mirofish.request')
logger.debug(f"响应: {response.status_code}")
logger.debug(f"Response: {response.status_code}")
return response
# 注册蓝图
# Đăng ký blueprint
from .api import graph_bp, simulation_bp, report_bp
app.register_blueprint(graph_bp, url_prefix='/api/graph')
app.register_blueprint(simulation_bp, url_prefix='/api/simulation')
app.register_blueprint(report_bp, url_prefix='/api/report')
# 健康检查
# Health check
@app.route('/health')
def health():
return {'status': 'ok', 'service': 'MiroFish Backend'}
if should_log_startup:
logger.info("MiroFish Backend 启动完成")
logger.info("MiroFish Backend started successfully")
return app

View File

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

View File

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

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

File diff suppressed because it is too large Load Diff

View File

@ -1,54 +1,54 @@
"""
配置管理
统一从项目根目录的 .env 文件加载配置
Quản cấu hình
Tải cấu hình thống nhất từ tệp .env thư mục gốc dự án
"""
import os
from dotenv import load_dotenv
# 加载项目根目录的 .env 文件
# 路径: MiroFish/.env (相对于 backend/app/config.py)
# Tải tệp .env ở thư mục gốc dự án
# Đường dẫn: MiroFish/.env (tương đối với backend/app/config.py)
project_root_env = os.path.join(os.path.dirname(__file__), '../../.env')
if os.path.exists(project_root_env):
load_dotenv(project_root_env, override=True)
else:
# 如果根目录没有 .env尝试加载环境变量用于生产环境
# Nếu thư mục gốc không có .env, thử nạp biến môi trường sẵn có (cho production)
load_dotenv(override=True)
class Config:
"""Flask配置类"""
"""Lớp cấu hình cho Flask"""
# Flask配置
# Cấu hình Flask
SECRET_KEY = os.environ.get('SECRET_KEY', 'mirofish-secret-key')
DEBUG = os.environ.get('FLASK_DEBUG', 'True').lower() == 'true'
# JSON配置 - 禁用ASCII转义让中文直接显示而不是 \uXXXX 格式)
# Cấu hình JSON - tắt escape ASCII để tiếng Trung hiển thị trực tiếp (thay vì dạng \uXXXX)
JSON_AS_ASCII = False
# LLM配置统一使用OpenAI格式
# Cấu hình LLM (thống nhất theo định dạng OpenAI)
LLM_API_KEY = os.environ.get('LLM_API_KEY')
LLM_BASE_URL = os.environ.get('LLM_BASE_URL', 'https://api.openai.com/v1')
LLM_MODEL_NAME = os.environ.get('LLM_MODEL_NAME', 'gpt-4o-mini')
# Zep配置
# Cấu hình Zep
ZEP_API_KEY = os.environ.get('ZEP_API_KEY')
# 文件上传配置
# Cấu hình upload tệp
MAX_CONTENT_LENGTH = 50 * 1024 * 1024 # 50MB
UPLOAD_FOLDER = os.path.join(os.path.dirname(__file__), '../uploads')
ALLOWED_EXTENSIONS = {'pdf', 'md', 'txt', 'markdown'}
# 文本处理配置
DEFAULT_CHUNK_SIZE = 500 # 默认切块大小
DEFAULT_CHUNK_OVERLAP = 50 # 默认重叠大小
# Cấu hình xử lý văn bản
DEFAULT_CHUNK_SIZE = 500 # Kích thước chunk mặc định
DEFAULT_CHUNK_OVERLAP = 50 # Độ chồng lấp mặc định
# OASIS模拟配置
# Cấu hình mô phỏng OASIS
OASIS_DEFAULT_MAX_ROUNDS = int(os.environ.get('OASIS_DEFAULT_MAX_ROUNDS', '10'))
OASIS_SIMULATION_DATA_DIR = os.path.join(os.path.dirname(__file__), '../uploads/simulations')
# OASIS平台可用动作配置
# Cấu hình action khả dụng theo nền tảng OASIS
OASIS_TWITTER_ACTIONS = [
'CREATE_POST', 'LIKE_POST', 'REPOST', 'FOLLOW', 'DO_NOTHING', 'QUOTE_POST'
]
@ -58,18 +58,18 @@ class Config:
'TREND', 'REFRESH', 'DO_NOTHING', 'FOLLOW', 'MUTE'
]
# Report Agent配置
# Cấu hình Report Agent
REPORT_AGENT_MAX_TOOL_CALLS = int(os.environ.get('REPORT_AGENT_MAX_TOOL_CALLS', '5'))
REPORT_AGENT_MAX_REFLECTION_ROUNDS = int(os.environ.get('REPORT_AGENT_MAX_REFLECTION_ROUNDS', '2'))
REPORT_AGENT_TEMPERATURE = float(os.environ.get('REPORT_AGENT_TEMPERATURE', '0.5'))
@classmethod
def validate(cls):
"""验证必要配置"""
"""Kiểm tra các cấu hình bắt buộc"""
errors = []
if not cls.LLM_API_KEY:
errors.append("LLM_API_KEY 未配置")
errors.append("LLM_API_KEY is not configured")
if not cls.ZEP_API_KEY:
errors.append("ZEP_API_KEY 未配置")
errors.append("ZEP_API_KEY is not configured")
return errors

View File

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

View File

@ -1,6 +1,6 @@
"""
图谱构建服务
接口2使用Zep API构建Standalone Graph
Dịch vụ xây dựng Đồ thị Tri thức (Knowledge Graph)
API 2: Sử dụng Zep API để xây dựng một Standalone Graph (Đồ thị độc lập)
"""
import os
@ -21,7 +21,7 @@ from .text_processor import TextProcessor
@dataclass
class GraphInfo:
"""图谱信息"""
"""Các trường thông tin cơ bản của Graph"""
graph_id: str
node_count: int
edge_count: int
@ -38,14 +38,14 @@ class GraphInfo:
class GraphBuilderService:
"""
图谱构建服务
负责调用Zep API构建知识图谱
Dịch vụ tạo lập Graph
Đảm nhiệm logic gọi request lên Zep API để thiết lập Graph
"""
def __init__(self, api_key: Optional[str] = None):
self.api_key = api_key or Config.ZEP_API_KEY
if not self.api_key:
raise ValueError("ZEP_API_KEY 未配置")
raise ValueError("ZEP_API_KEY has not been configured.")
self.client = Zep(api_key=self.api_key)
self.task_manager = TaskManager()
@ -60,20 +60,20 @@ class GraphBuilderService:
batch_size: int = 3
) -> str:
"""
异步构建图谱
Khởi chạy tiến trình bất đồng bộ xây dựng Graph
Args:
text: 输入文本
ontology: 本体定义来自接口1的输出
graph_name: 图谱名称
chunk_size: 文本块大小
chunk_overlap: 块重叠大小
batch_size: 每批发送的块数量
text: Văn bản toàn văn làm nguồn vào
ontology: Từ điển chuẩn cấu trúc Ontology (Đầu ra từ API số 1)
graph_name: Tên đặt cho Graph
chunk_size: Kích thước từng khối text (chunk)
chunk_overlap: Giới hạn những từ đè lên nhau giữa các chunk (bảo toàn flow hội thoại / ngữ cảnh)
batch_size: Chuyển dữ liệu theo mảng batch để tiết kiệm số lần Request
Returns:
任务ID
Trạng thái Task ID vừa khởi tạo
"""
# 创建任务
# Đưa Task vào danh sách quản lý
task_id = self.task_manager.create_task(
task_type="graph_build",
metadata={
@ -83,7 +83,7 @@ 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(
target=self._build_graph_worker,
args=(task_id, text, ontology, graph_name, chunk_size, chunk_overlap, batch_size)
@ -103,21 +103,21 @@ class GraphBuilderService:
chunk_overlap: int,
batch_size: int
):
"""图谱构建工作线程"""
"""Tiến trình cài đặt ngầm tạo Graph với các bước tuần tự"""
try:
self.task_manager.update_task(
task_id,
status=TaskStatus.PROCESSING,
progress=5,
message="开始构建图谱..."
message="Building Knowledge Graph..."
)
# 1. 创建图谱
# Bước 1. Init tạo khung xương Graph trên Zep
graph_id = self.create_graph(graph_name)
self.task_manager.update_task(
task_id,
progress=10,
message=f"图谱已创建: {graph_id}"
message=f"Created empty graph: {graph_id}"
)
# 2. 设置本体
@ -125,54 +125,54 @@ class GraphBuilderService:
self.task_manager.update_task(
task_id,
progress=15,
message="本体已设置"
message="Ontology scheme applied successfully"
)
# 3. 文本分块
# Bước 3. Chia nhỏ văn bản gốc
chunks = TextProcessor.split_text(text, chunk_size, chunk_overlap)
total_chunks = len(chunks)
self.task_manager.update_task(
task_id,
progress=20,
message=f"文本已分割为 {total_chunks} 个块"
message=f"Split text into {total_chunks} chunk(s)"
)
# 4. 分批发送数据
# Bước 4. Gửi các đợt chunk tới Zep dưới dạng batch
episode_uuids = self.add_text_batches(
graph_id, chunks, batch_size,
lambda msg, prog: self.task_manager.update_task(
task_id,
progress=20 + int(prog * 0.4), # 20-60%
progress=20 + int(prog * 0.4), # Thể hiện từ 20-60%
message=msg
)
)
# 5. 等待Zep处理完成
# Bước 5. Đợi hàm Backend của Cloud Zep xử lý đồng bộ xong các episode
self.task_manager.update_task(
task_id,
progress=60,
message="等待Zep处理数据..."
message="Waiting for Zep to process data..."
)
self._wait_for_episodes(
episode_uuids,
lambda msg, prog: self.task_manager.update_task(
task_id,
progress=60 + int(prog * 0.3), # 60-90%
progress=60 + int(prog * 0.3), # Thể hiện từ 60-90%
message=msg
)
)
# 6. 获取图谱信息
# Bước 6. Thống kê lại Graph hoàn thiện
self.task_manager.update_task(
task_id,
progress=90,
message="获取图谱信息..."
message="Fetching finalized graph info..."
)
graph_info = self._get_graph_info(graph_id)
# 完成
# Thông báo hoàn tất
self.task_manager.complete_task(task_id, {
"graph_id": graph_id,
"graph_info": graph_info.to_dict(),
@ -185,7 +185,7 @@ class GraphBuilderService:
self.task_manager.fail_task(task_id, error_msg)
def create_graph(self, name: str) -> str:
"""创建Zep图谱公开方法"""
"""Khai báo một Graph mới với Zep API (Sử dụng công khai public)"""
graph_id = f"mirofish_{uuid.uuid4().hex[:16]}"
self.client.graph.create(
@ -197,74 +197,74 @@ class GraphBuilderService:
return graph_id
def set_ontology(self, graph_id: str, ontology: Dict[str, Any]):
"""设置图谱本体(公开方法)"""
"""Cấu hình dữ liệu Ontology (Bản thể học) cho Graph trên server Zep (Public access)"""
import warnings
from typing import Optional
from pydantic import Field
from zep_cloud.external_clients.ontology import EntityModel, EntityText, EdgeModel
# 抑制 Pydantic v2 关于 Field(default=None) 的警告
# 这是 Zep SDK 要求的用法,警告来自动态类创建,可以安全忽略
# Ẩn bỏ đi các Warning (Cảnh báo) của thư viện Pydantic v2 liên quan đến Field(default=None)
# Vì đây là format bắt buộc phải có từ Zep SDK, các cảnh báo này phát sinh do tự động khởi tạo lớp ảo, hoàn toàn có thể bỏ qua được.
warnings.filterwarnings('ignore', category=UserWarning, module='pydantic')
# Zep 保留名称,不能作为属性名
# Danh sách các tên định danh (variable/name) trùng với từ khoá bảo lưu của Zep, không được dùng làm tên thuộc tính
RESERVED_NAMES = {'uuid', 'name', 'group_id', 'name_embedding', 'summary', 'created_at'}
def safe_attr_name(attr_name: str) -> str:
"""将保留名称转换为安全名称"""
"""Hàm thay đổi các tên thuộc tính bị trùng với keyword của hệ thống để an toàn hơn"""
if attr_name.lower() in RESERVED_NAMES:
return f"entity_{attr_name}"
return attr_name
# 动态创建实体类型
# Khởi tạo động (Dynamic Class Creation) các Model Loại Thực thể từ JSON đầu vào
entity_types = {}
for entity_def in ontology.get("entity_types", []):
name = entity_def["name"]
description = entity_def.get("description", f"A {name} entity.")
# 创建属性字典和类型注解Pydantic v2 需要)
# Chuẩn bị file từ điển cho Attribute và kiểu chú thích (Theo chuẩn Pydantic v2)
attrs = {"__doc__": description}
annotations = {}
for attr_def in entity_def.get("attributes", []):
attr_name = safe_attr_name(attr_def["name"]) # 使用安全名称
attr_name = safe_attr_name(attr_def["name"]) # Áp dụng hàm chống bị trùng từ khoá
attr_desc = attr_def.get("description", attr_name)
# Zep API 需要 Field 的 description这是必需的
# Zep API bắt buộc phải nhận vào field description
attrs[attr_name] = Field(description=attr_desc, default=None)
annotations[attr_name] = Optional[EntityText] # 类型注解
annotations[attr_name] = Optional[EntityText] # Chú thích kiểu dữ liệu
attrs["__annotations__"] = annotations
# 动态创建类
# Dựng Class ảo
entity_class = type(name, (EntityModel,), attrs)
entity_class.__doc__ = description
entity_types[name] = entity_class
# 动态创建边类型
# Tương tự, dựa vào JSON để khởi tạo động khai báo các Model Loại Quan Hệ
edge_definitions = {}
for edge_def in ontology.get("edge_types", []):
name = edge_def["name"]
description = edge_def.get("description", f"A {name} relationship.")
# 创建属性字典和类型注解
# Dọn các attribute dictionary và typing tương tự
attrs = {"__doc__": description}
annotations = {}
for attr_def in edge_def.get("attributes", []):
attr_name = safe_attr_name(attr_def["name"]) # 使用安全名称
attr_name = safe_attr_name(attr_def["name"]) # Filter an toàn
attr_desc = attr_def.get("description", attr_name)
# Zep API 需要 Field 的 description这是必需的
# Đảm bảo giữ format Zep API
attrs[attr_name] = Field(description=attr_desc, default=None)
annotations[attr_name] = Optional[str] # 边属性用str类型
annotations[attr_name] = Optional[str] # Định dạng Data cho thuộc tình của loại Quan Hệ là chuỗi String
attrs["__annotations__"] = annotations
# 动态创建类
# Khởi tạo Class động với Tên chuẩn format (PascalCase)
class_name = ''.join(word.capitalize() for word in name.split('_'))
edge_class = type(class_name, (EdgeModel,), attrs)
edge_class.__doc__ = description
# 构建source_targets
# Mapping thông số luồng thực thể gắn kết với Quan Hệ (Source/Targets config)
source_targets = []
for st in edge_def.get("source_targets", []):
source_targets.append(
@ -277,7 +277,7 @@ class GraphBuilderService:
if source_targets:
edge_definitions[name] = (edge_class, source_targets)
# 调用Zep API设置本体
# Action Gọi lệnh thay đổi Ontology cho môi trường GraphID của Zep
if entity_types or edge_definitions:
self.client.graph.set_ontology(
graph_ids=[graph_id],
@ -292,7 +292,7 @@ class GraphBuilderService:
batch_size: int = 3,
progress_callback: Optional[Callable] = None
) -> List[str]:
"""分批添加文本到图谱,返回所有 episode 的 uuid 列表"""
"""Tải các đoạn văn bản (text chunks) lên Graph theo từng gói nhỏ (batch) và trả về id (Episode UUID) của mọi phân đoạn dữ liệu gửi đi."""
episode_uuids = []
total_chunks = len(chunks)
@ -304,36 +304,36 @@ class GraphBuilderService:
if progress_callback:
progress = (i + len(batch_chunks)) / total_chunks
progress_callback(
f"发送第 {batch_num}/{total_batches} 批数据 ({len(batch_chunks)} )...",
f"Sending data batch {batch_num}/{total_batches} ({len(batch_chunks)} chunks)...",
progress
)
# 构建episode数据
# Chuẩn bị định dạng gói dữ liệu (Episode data) để tương thích Zep Graph
episodes = [
EpisodeData(data=chunk, type="text")
for chunk in batch_chunks
]
# 发送到Zep
# Khởi chạy gửi cho Zep Server
try:
batch_result = self.client.graph.add_batch(
graph_id=graph_id,
episodes=episodes
)
# 收集返回的 episode uuid
# Cập nhật và thu thập lại UUID của các Episode được trả về sau khi tạo mới
if batch_result and isinstance(batch_result, list):
for ep in batch_result:
ep_uuid = getattr(ep, 'uuid_', None) or getattr(ep, 'uuid', None)
if ep_uuid:
episode_uuids.append(ep_uuid)
# 避免请求过快
# Cài thời gian chờ (delay) nhỏ để tránh rate-limit bị quá tải số lượng requests
time.sleep(1)
except Exception as e:
if progress_callback:
progress_callback(f"批次 {batch_num} 发送失败: {str(e)}", 0)
progress_callback(f"Failed to send batch {batch_num}: {str(e)}", 0)
raise
return episode_uuids
@ -344,10 +344,10 @@ class GraphBuilderService:
progress_callback: Optional[Callable] = None,
timeout: int = 600
):
"""等待所有 episode 处理完成(通过查询每个 episode 的 processed 状态)"""
"""Chạy vòng lặp để kiểm tra và chờ cho tới khi mọi Episode (các khối Text) đều hoàn tất quá trình process từ hệ thống"""
if not episode_uuids:
if progress_callback:
progress_callback("无需等待(没有 episode", 1.0)
progress_callback("No episodes to scan (Progress 100%)", 1.0)
return
start_time = time.time()
@ -356,18 +356,19 @@ class GraphBuilderService:
total_episodes = len(episode_uuids)
if progress_callback:
progress_callback(f"开始等待 {total_episodes} 个文本块处理...", 0)
progress_callback(f"Waiting for analysis of {total_episodes} text chunks to begin...", 0)
while pending_episodes:
# Ngắt thoát và trả về lỗi nếu bị Timeout (Chạy quá thời gian cho phép)
if time.time() - start_time > timeout:
if progress_callback:
progress_callback(
f"部分文本块超时,已完成 {completed_count}/{total_episodes}",
f"Some text segments have timed out, but {completed_count}/{total_episodes} have completed successfully",
completed_count / total_episodes
)
break
# 检查每个 episode 的处理状态
# Duyệt vòng lặp mỗi episode uuid để lấy cập nhật tiến trình check của từng episode một
for ep_uuid in list(pending_episodes):
try:
episode = self.client.graph.episode.get(uuid_=ep_uuid)
@ -378,31 +379,31 @@ class GraphBuilderService:
completed_count += 1
except Exception as e:
# 忽略单个查询错误,继续
# Tạm thời bỏ qua nếu request lỗi, vòng lặp kế theo sẽ tự động call tiếp để get status
pass
elapsed = int(time.time() - start_time)
if progress_callback:
progress_callback(
f"Zep处理中... {completed_count}/{total_episodes} 完成, {len(pending_episodes)} 待处理 ({elapsed})",
f"Zep is processing in the background... {completed_count}/{total_episodes} done, {len(pending_episodes)} tasks remaining ({elapsed}s elapsed)",
completed_count / total_episodes if total_episodes > 0 else 0
)
if pending_episodes:
time.sleep(3) # 每3秒检查一次
time.sleep(3) # Lặp chu kỳ check mỗi 3 giây
if progress_callback:
progress_callback(f"处理完成: {completed_count}/{total_episodes}", 1.0)
progress_callback(f"Data upload process completed: {completed_count}/{total_episodes}", 1.0)
def _get_graph_info(self, graph_id: str) -> GraphInfo:
"""获取图谱信息"""
# 获取节点(分页)
"""Lấy/Get dữ liệu Graph Info hiện tại"""
# Load các điểm nút/Entity đang có (Qua trình duyệt web/paging)
nodes = fetch_all_nodes(self.client, graph_id)
# 获取边(分页)
# Lấy theo mảng phân trang thông tin các Edges/Mối Liên Kết
edges = fetch_all_edges(self.client, graph_id)
# 统计实体类型
# Nối lại và thống kê những Entity Types
entity_types = set()
for node in nodes:
if node.labels:
@ -419,25 +420,26 @@ class GraphBuilderService:
def get_graph_data(self, graph_id: str) -> Dict[str, Any]:
"""
获取完整图谱数据包含详细信息
Gói gọn toàn bộ dữ liệu cấu trúc (Bao gồm dữ liệu Graph chi tiết)
Args:
graph_id: 图谱ID
graph_id: ID của đồ thị
Returns:
包含nodes和edges的字典包括时间信息属性等详细数据
Một object Dictionary bao hàm thông tin dữ liệu về Mạng lưới Cụm (nodes) Cạnh (edges),
toàn bộ chi tiết đi kèm khác (Time khởi tạo, Property).
"""
nodes = fetch_all_nodes(self.client, graph_id)
edges = fetch_all_edges(self.client, graph_id)
# 创建节点映射用于获取节点名称
# Giữ một map tra cứu để phục vụ lấy 'Tên' nhanh theo ID UUID
node_map = {}
for node in nodes:
node_map[node.uuid_] = node.name or ""
nodes_data = []
for node in nodes:
# 获取创建时间
# Lấy thông số về Thời gian được ghi nhận/khởi tạo
created_at = getattr(node, 'created_at', None)
if created_at:
created_at = str(created_at)
@ -453,7 +455,7 @@ class GraphBuilderService:
edges_data = []
for edge in edges:
# 获取时间信息
# Thu thập các timestamp gắn với cạnh
created_at = getattr(edge, 'created_at', None)
valid_at = getattr(edge, 'valid_at', None)
invalid_at = getattr(edge, 'invalid_at', None)

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
"""
本体生成服务
接口1分析文本内容生成适合社会模拟的实体和关系类型定义
Dịch vụ tạo Ontology (Hệ thực thể / Quan hệ)
API 1: Phân tích nội dung văn bản, khởi tạo các định nghĩa về loại thực thể quan hệ phù hợp cho việc phỏng mạng hội
"""
import json
@ -8,157 +8,157 @@ from typing import Dict, Any, List, Optional
from ..utils.llm_client import LLMClient
# 本体生成的系统提示词
ONTOLOGY_SYSTEM_PROMPT = """你是一个专业的知识图谱本体设计专家。你的任务是分析给定的文本内容和模拟需求,设计适合**社交媒体舆论模拟**的实体类型和关系类型。
# System prompt dùng cho việc tự động sinh Ontology
ONTOLOGY_SYSTEM_PROMPT = """Bạn là một chuyên gia thiết kế bản thể học (Ontology) cho Tri thức đồ thị (Knowledge Graph). Nhiệm vụ của bạn là phân tích nội dung văn bản được cung cấp và nhu cầu để thiết kế các loại thực thể (Entity) và loại mối quan hệ (Relationship) thiết kế phù hợp cho **Mô phỏng dư luận trên mạng xã hội**.
**重要你必须输出有效的JSON格式数据不要输出任何其他内容**
**QUAN TRỌNG: Bạn BẮT BUỘC phải đầu ra một cấu trúc định dạng JSON hợp lệ, KHÔNG ĐƯỢC xuất thêm bất kỳ văn bản nào khác.**
## 核心任务背景
## Bối cảnh nhiệm vụ cốt lõi
我们正在构建一个**社交媒体舆论模拟系统**在这个系统中
- 每个实体都是一个可以在社交媒体上发声互动传播信息的"账号""主体"
- 实体之间会相互影响转发评论回应
- 我们需要模拟舆论事件中各方的反应和信息传播路径
Chúng tôi đang xây dựng một **hệ thống phỏng tin đồn luận mạng hội**. Trong hệ thống này:
- Mỗi thực thể một "tài khoản" hoặc "chủ thể" thể lên tiếng, tương tác lan truyền thông tin trên mạng hội.
- Các thực thể 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 phỏng phản ứng của các bên đường truyền thông tin trong các sự kiện luận.
因此**实体必须是现实中真实存在的可以在社媒上发声和互动的主体**
Do đó, **thực thể phải các chủ thể thật trong thế giới thực, khả năng lên tiếng tương tác trên mạng hội**:
**可以是**
- 具体的个人公众人物当事人意见领袖专家学者普通人
- 公司企业包括其官方账号
- 组织机构大学协会NGO工会等
- 政府部门监管机构
- 媒体机构报纸电视台自媒体网站
- 社交媒体平台本身
- 特定群体代表如校友会粉丝团维权群体等
**THỂ **:
- nhân cụ thể (nhân vật của công chúng, các bên liên quan, KOL, chuyên gia / học giả, người bình thường)
- Công ty, doanh nghiệp (bao gồm cả tài khoản chính thức của họ)
- Tổ chức (trường đại học, hiệp hội, tổ chức phi chính phủ (NGO), công đoàn, v.v.)
- Các quan chính phủ, quan quản
- 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 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 **:
- Khái niệm trừu tượng (như "dư luận", "cảm xúc", "xu hướng")
- Chủ đề / đề tài (như "tính toàn vẹn học thuật", "cải cách giáo dục")
- Quan điểm / thái độ (như "phe ủng hộ", "bên phản đối")
## 输出格式
## Định dạng đầu ra
请输出JSON格式包含以下结构
Hãy trả về dưới định dạng JSON, bao gồm cấu trúc sau:
```json
{
"entity_types": [
{
"name": "实体类型名称英文PascalCase",
"description": "简短描述英文不超过100字符",
"name": "Tên loại thực thể (Tiếng Anh, PascalCase)",
"description": "Mô tả ngắn gọn (Tiếng Anh, tối đa 100 ký tự)",
"attributes": [
{
"name": "属性名英文snake_case",
"name": "Tên thuộc tính (Tiếng Anh, snake_case)",
"type": "text",
"description": "属性描述"
"description": "Mô tả của thuộc tính"
}
],
"examples": ["示例实体1", "示例实体2"]
"examples": ["Ví dụ thực thể 1", "Ví dụ thực thể 2"]
}
],
"edge_types": [
{
"name": "关系类型名称英文UPPER_SNAKE_CASE",
"description": "简短描述英文不超过100字符",
"name": "Tên loại quan hệ (Tiếng Anh, UPPER_SNAKE_CASE)",
"description": "Mô tả ngắn (Tiếng Anh, tối đa 100 ký tự)",
"source_targets": [
{"source": "源实体类型", "target": "目标实体类型"}
{"source": "Loại thực thể nguồn", "target": "Loại thực thể đích"}
],
"attributes": []
}
],
"analysis_summary": "对文本内容的简要分析说明(中文)"
"analysis_summary": "Giải thích ngắn gọn phân tích của bạn về văn bản (Tiếng Việt)"
}
```
## 设计指南(极其重要!)
## Hướng dẫn Thiết kế (CỰC KỲ QUAN TRỌNG!)
### 1. 实体类型设计 - 必须严格遵守
### 1. Thiết kế loại Thực thể (Entity Types) - Phải tuân thủ nghiêm ngặt
**数量要求必须正好10个实体类型**
**Yêu cầu số lượng: Đúng 10 loại Thực thể.**
**层次结构要求必须同时包含具体类型和兜底类型**
**Yêu cầu về cấu trúc phân cấp (Phải cả Loại cụ thể Loại bao quát/fallback):**
你的10个实体类型必须包含以下层次
10 loại thực thể của bạn phải bao gồm cấp độ sau:
A. **兜底类型必须包含放在列表最后2个**
- `Person`: 任何自然人个体的兜底类型当一个人不属于其他更具体的人物类型时归入此类
- `Organization`: 任何组织机构的兜底类型当一个组织不属于其他更具体的组织类型时归入此类
A. **Loại bao quát (Fallback Types) (BẮT BUỘC, phải nằm 2 vị trí cuối cùng trong mảng)**:
- `Person`: loại bao quát cho MỌI 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`: loại bao quát cho MỌI tổ chức. Đặc trưng cho các tổ chức nhỏ hoặc không phù hợp với các loại tổ chức cụ thể khác.
B. **具体类型8根据文本内容设计**
- 针对文本中出现的主要角色设计更具体的类型
- 例如如果文本涉及学术事件可以有 `Student`, `Professor`, `University`
- 例如如果文本涉及商业事件可以有 `Company`, `CEO`, `Employee`
B. **Loại cụ thể (8 loại, phụ thuộc vào nội dung văn bản)**:
- Thiết kế các loại cụ thể cho các vai chính được nhắc đến nhiều nhất trong văn bản.
- dụ: Nếu văn bản nói về scandal trường học, thể : `Student`, `Professor`, `University`
- dụ: Nếu văn bản câu chuyện kinh doanh, thể : `Company`, `CEO`, `Employee`
**为什么需要兜底类型**
- 文本中会出现各种人物"中小学教师""路人甲""某位网友"
- 如果没有专门的类型匹配他们应该被归入 `Person`
- 同理小型组织临时团体等应该归入 `Organization`
**Tại sao cần các loại Bao quát (Fallback):**
- Văn bản thường chứa các thông tin như "giáo viên tiểu học", "một người qua đường", "một cư dân mạng"
- Nếu không loại được định nghĩa riêng cho họ, họ nên thuộc về loại `Person`
- Tương tự, tổ chức nhỏ hoặc nhóm học tập tạm thời nên thuộc `Organization`
**具体类型的设计原则**
- 从文本中识别出高频出现或关键的角色类型
- 每个具体类型应该有明确的边界避免重叠
- description 必须清晰说明这个类型和兜底类型的区别
**Nguyên tắc cho các loại Cụ thể:**
- Nhận dạng tần suất xuất hiện 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 một ranh giới ràng, không bị chồng chéo.
- Thuộc tính tả (description) phải giải thích sao loại này tách biệt.
### 2. 关系类型设计
### 2. Thiết kế Cạnh/Quan hệ (Edge Types)
- 数量6-10
- 关系应该反映社媒互动中的真实联系
- 确保关系的 source_targets 涵盖你定义的实体类型
- Số lượng: Khoảng 6-10 loại quan hệ
- Các mối quan hệ này phải giải thích gắn kết được hành vi tương tác trên mạng hội của các nhân vật.
- Đảm bảo mapping quan hệ hai chiều `source_targets` khớp với các thực thể phía trên.
### 3. 属性设计
### 3. Thiết kế Thuộc tính (Attributes)
- 每个实体类型1-3个关键属性
- **注意**属性名不能使用 `name``uuid``group_id``created_at``summary`这些是系统保留字
- 推荐使用`full_name`, `title`, `role`, `position`, `location`, `description`
- Mỗi loại thực thể cần 1-3 thuộc tính chính để làm nhân thân.
- **CHÚ Ý**: Không sử dụng các ID nội bộ làm thuộc tính như `name`, `uuid`, `group_id`, `created_at`, `summary` (chúng từ khóa hệ thống).
- Khuyên dùng: `full_name`, `title`, `role`, `position`, `location`, `description`,...
## 实体类型参考
## Loại Thực thể tham khảo
**个人类具体**
- Student: 学生
- Professor: 教授/学者
- Journalist: 记者
- Celebrity: 明星/网红
- Executive: 高管
- Official: 政府官员
- Lawyer: 律师
- Doctor: 医生
**Loại nhân (Cụ thể):**
- Student: Học sinh/Sinh viên
- Professor: Giáo /Học giả
- Journalist: Nhà báo/Phóng viên
- Celebrity: Người nổi tiếng/Idol
- Executive: Các giám đốc, CEO, cấp lãnh đạo
- Official: Các vị công chức chính phủ
- Lawyer: Luật
- Doctor: Y /Bác
**个人类兜底**
- Person: 任何自然人不属于上述具体类型时使用
**Loại nhân (Bao quát):**
- Person: loại bao quát cho MỌI nhân tự nhiên nào không thuộc chi tiết trên.
**组织类具体**
- University: 高校
- Company: 公司企业
- GovernmentAgency: 政府机构
- MediaOutlet: 媒体机构
- Hospital: 医院
- School: 中小学
- NGO: 非政府组织
**Loại tổ chức (Cụ thể):**
- University: Đại học hoặc học viện
- Company: Doanh nghiệp hay Công ty, tập đoàn
- GovernmentAgency: quan quản , các quan ban ngành công quyền
- MediaOutlet: Truyền thông hay Tạp chí, Đài tin tức
- Hospital: Bệnh viện / Trung tâm y tế
- School: Bậc tiểu/trung học
- NGO: Các loại Tổ chức phi chính phủ hoặc từ thiện
**组织类兜底**
- Organization: 任何组织机构不属于上述具体类型时使用
**Loại tổ chức (Bao quát):**
- Organization: loại bao quát cho MỌI cấu hợp tác không thuôc chi tiết tổ chức trên.
## 关系类型参考
## Loại Khái niệm Liên kết (Quan Hệ)
- WORKS_FOR: 工作于
- STUDIES_AT: 就读于
- AFFILIATED_WITH: 隶属于
- REPRESENTS: 代表
- REGULATES: 监管
- REPORTS_ON: 报道
- COMMENTS_ON: 评论
- RESPONDS_TO: 回应
- SUPPORTS: 支持
- OPPOSES: 反对
- COLLABORATES_WITH: 合作
- COMPETES_WITH: 竞争
- WORKS_FOR: Làm việc ăn lương bởi tổ chức
- STUDIES_AT: Đang học tại nhà trường
- AFFILIATED_WITH: Liên quan, Trực thuộc vào đơn vị
- REPRESENTS: Thể hiện cách hành động đại diện cho tập thể
- REGULATES: Theo dõi, quản , thanh tra chính sách
- REPORTS_ON: Tác nghiệp báo chí, tin về hiện tượng
- COMMENTS_ON: phản hồi hoặc lên tiếng về tranh cãi
- RESPONDS_TO: Hành động đáp trả
- SUPPORTS: Theo phe ủng hộ điều luật
- OPPOSES: Phản đối chính sách
- COLLABORATES_WITH: Tham gia phối ứng xử sự cố.
- COMPETES_WITH: Quan hệ thù địch.
"""
class OntologyGenerator:
"""
本体生成器
分析文本内容生成实体和关系类型定义
Trình khởi tạo Ontology
Phân tích nội dung đoạn văn bản truyền vào, sau đó tự động suy luận ra định nghĩa của các loại Thực thể Mối quan hệ
"""
def __init__(self, llm_client: Optional[LLMClient] = None):
@ -171,17 +171,17 @@ class OntologyGenerator:
additional_context: Optional[str] = None
) -> Dict[str, Any]:
"""
生成本体定义
Khởi tạo định nghĩa Ontology
Args:
document_texts: 文档文本列表
simulation_requirement: 模拟需求描述
additional_context: 额外上下文
document_texts: Danh sách mảng các văn bản nội dung nguồn
simulation_requirement: Chuỗi tả nhu cầu phỏng của người dùng
additional_context: Văn bản cung cấp thêm các ngữ cảnh phụ (nếu )
Returns:
本体定义entity_types, edge_types等
Dictionary gồm cấu trúc Ontology (entity_types, edge_types v.v.)
"""
# 构建用户消息
# Tạo câu lệnh Prompt gửi cho mô hình LLM
user_message = self._build_user_message(
document_texts,
simulation_requirement,
@ -193,19 +193,19 @@ class OntologyGenerator:
{"role": "user", "content": user_message}
]
# 调用LLM
# Gửi request đến LLM
result = self.llm_client.chat_json(
messages=messages,
temperature=0.3,
max_tokens=4096
)
# 验证和后处理
# Kiểm tra tính hợp lệ và xử lý tinh chỉnh kết quả đầu ra
result = self._validate_and_process(result)
return result
# 传给 LLM 的文本最大长度5万字
# Định mức giới hạn độ dài ký tự tối đa của đoạn văn bản có thể gửi cho LLM (5 vạn chữ)
MAX_TEXT_LENGTH_FOR_LLM = 50000
def _build_user_message(
@ -214,50 +214,50 @@ class OntologyGenerator:
simulation_requirement: str,
additional_context: Optional[str]
) -> str:
"""构建用户消息"""
"""Ghép các thông tin đầu vào thành User Prompt hoàn chỉnh để gửi tới LLM"""
# 合并文本
# Gộp tất cả các đoạn văn bản thành một string duy nhất
combined_text = "\n\n---\n\n".join(document_texts)
original_length = len(combined_text)
# 如果文本超过5万字截断仅影响传给LLM的内容不影响图谱构建
# Nếu vượt quá giới hạn tối đa, thực hiện cắt bớt (Việc này chỉ ảnh hưởng prompt gửi nhận diện Ontology, không ảnh hưởng thư viện Graph building ở sau)
if len(combined_text) > self.MAX_TEXT_LENGTH_FOR_LLM:
combined_text = combined_text[:self.MAX_TEXT_LENGTH_FOR_LLM]
combined_text += f"\n\n...(原文共{original_length}字,已截取前{self.MAX_TEXT_LENGTH_FOR_LLM}字用于本体分析)..."
combined_text += f"\n\n...(Văn bản gốc dài {original_length} chữ, đã chủ động cắt lấy {self.MAX_TEXT_LENGTH_FOR_LLM} chữ đầu tiên để phục vụ phân tích Ontology)..."
message = f"""## 模拟需求
message = f"""## Nhu cầu mô phỏng
{simulation_requirement}
## 文档内容
## Nội dung tài liệu
{combined_text}
"""
if additional_context:
message += f"""
## 额外说明
## Giải thích bổ sung
{additional_context}
"""
message += """
请根据以上内容设计适合社会舆论模拟的实体类型和关系类型
Dựa vào các thông tin trên đây, hãy thiết kế các loại hình Thực Thể Quan Hệ phù hợp để phục vụ việc phỏng luận trên mạng hội.
**必须遵守的规则**
1. 必须正好输出10个实体类型
2. 最后2个必须是兜底类型Person个人兜底 Organization组织兜底
3. 前8个是根据文本内容设计的具体类型
4. 所有实体类型必须是现实中可以发声的主体不能是抽象概念
5. 属性名不能使用 nameuuidgroup_id 等保留字 full_nameorg_name 等替代
**Các quy tắc BẮT BUỘC tuân thủ**:
1. Số lượng chính xác: Xuất phải CHUẨN XÁC 10 loại Thực thể
2. 2 vị trí cuối cùng bắt buộc từ Khoá phụ (Fallback): Person (Cho nhân) Organization (Cho Tổ chức)
3. 8 vị trí đầu tiên phải phân tích suy luận dựa vào cấu trúc của chính văn bản truyền vào
4. Tất cả các thực thể được liệt phải đóng vai trò Chủ thể (nhân vật thể lên tiếng ngoài đời thực), KHÔNG ĐƯỢC dùng làm khái niệm trừu tượng.
5. Tên biến thuộc tính KHÔNG ĐƯỢC name, uuid, group_id hay các biến số bảo lưu của hệ thống khác. Vui lòng chuyển thành full_name, org_name, v.v.
"""
return message
def _validate_and_process(self, result: Dict[str, Any]) -> Dict[str, Any]:
"""验证和后处理结果"""
"""Tiền kiểm tra tính trọn vẹn và cấu trúc của dữ liệu phản hồi JSON"""
# 确保必要字段存在
# Đảm bảo các thuộc tính mảng bắt buộc phải xuất hiện
if "entity_types" not in result:
result["entity_types"] = []
if "edge_types" not in result:
@ -265,17 +265,17 @@ class OntologyGenerator:
if "analysis_summary" not in result:
result["analysis_summary"] = ""
# 验证实体类型
# Tiền xử lý để loại thực thể hợp lệ
for entity in result["entity_types"]:
if "attributes" not in entity:
entity["attributes"] = []
if "examples" not in entity:
entity["examples"] = []
# 确保description不超过100字符
# Cắt ngắn description nếu vượt quá độ dài tối đa 100 character
if len(entity.get("description", "")) > 100:
entity["description"] = entity["description"][:97] + "..."
# 验证关系类型
# Tiền xử lý để loại quan hệ hợp lệ
for edge in result["edge_types"]:
if "source_targets" not in edge:
edge["source_targets"] = []
@ -284,11 +284,11 @@ class OntologyGenerator:
if len(edge.get("description", "")) > 100:
edge["description"] = edge["description"][:97] + "..."
# Zep API 限制:最多 10 个自定义实体类型,最多 10 个自定义边类型
# Ràng buộc số lượng đầu ra của API Zep: Tối đa 10 loại thực thể tự tuỳ chỉnh, và Tối đa 10 loại cạnh quan hệ tùy chỉnh
MAX_ENTITY_TYPES = 10
MAX_EDGE_TYPES = 10
# 兜底类型定义
# Khai báo định nghĩa về 2 đối tượng bao quát (fallback) mặc định
person_fallback = {
"name": "Person",
"description": "Any individual person not fitting other specific person types.",
@ -309,12 +309,12 @@ class OntologyGenerator:
"examples": ["small business", "community group"]
}
# 检查是否已有兜底类型
# Sàng lọc kiểm tra xem kết quả đầu ra đã chứa sẵn các danh mục rỗng (fallback) ở vị trí chuẩn chưa
entity_names = {e["name"] for e in result["entity_types"]}
has_person = "Person" in entity_names
has_organization = "Organization" in entity_names
# 需要添加的兜底类型
# Danh sách cần phải gán bù vào
fallbacks_to_add = []
if not has_person:
fallbacks_to_add.append(person_fallback)
@ -325,17 +325,17 @@ class OntologyGenerator:
current_count = len(result["entity_types"])
needed_slots = len(fallbacks_to_add)
# 如果添加后会超过 10 个,需要移除一些现有类型
# Nếu thêm vào bị quá giới hạn 10 loại, cần phải loại bỏ bớt các loại Entity đằng trước
if current_count + needed_slots > MAX_ENTITY_TYPES:
# 计算需要移除多少个
# Tính lượng bị thừa ra so với hạn mức (Để bỏ đi)
to_remove = current_count + needed_slots - MAX_ENTITY_TYPES
# 从末尾移除(保留前面更重要的具体类型)
# Bỏ bớt n vị trí tính từ cuối mảng (Đảm bảo chừa lại nhóm các thực thể cụ thể đã phân tích ở trên)
result["entity_types"] = result["entity_types"][:-to_remove]
# 添加兜底类型
# Nối cụm Fallbacks vừa khởi tạo vào cuối chuỗi
result["entity_types"].extend(fallbacks_to_add)
# 最终确保不超过限制(防御性编程)
# Đảm bảo phòng thủ một lần cuối cùng không có quá 10 Element Array
if len(result["entity_types"]) > MAX_ENTITY_TYPES:
result["entity_types"] = result["entity_types"][:MAX_ENTITY_TYPES]
@ -346,29 +346,29 @@ class OntologyGenerator:
def generate_python_code(self, ontology: Dict[str, Any]) -> str:
"""
将本体定义转换为Python代码类似ontology.py
Dựng (Generate) file Script với nội dung Python Class tương ứng khai báo dữ liệu Ontology để máy đọc (Tương tự như file ontology.py)
Args:
ontology: 本体定义
ontology: Từ điển định nghĩa Ontology
Returns:
Python代码字符串
Chuỗi đoạn code File Python cần tạo để lưu
"""
code_lines = [
'"""',
'自定义实体类型定义',
'由MiroFish自动生成用于社会舆论模拟',
'Các loại đối tượng (Thực thể) tuỳ chỉnh',
'Được khởi tạo tự động bởi công cụ MiroFish, ứng dụng vào việc chạy giả lập diễn biến dư luận',
'"""',
'',
'from pydantic import Field',
'from zep_cloud.external_clients.ontology import EntityModel, EntityText, EdgeModel',
'',
'',
'# ============== 实体类型定义 ==============',
'# ============== Định nghĩa Tên Lớp Các thực thể (Entity) ==============',
'',
]
# 生成实体类型
# Khởi tạo các đoạn mã tương ứng với Định nghĩa thực thể Entity
for entity in ontology.get("entity_types", []):
name = entity["name"]
desc = entity.get("description", f"A {name} entity.")
@ -391,13 +391,13 @@ class OntologyGenerator:
code_lines.append('')
code_lines.append('')
code_lines.append('# ============== 关系类型定义 ==============')
code_lines.append('# ============== Định nghĩa Các Nhóm Quan Hệ/Hành Vi (Edge) ==============')
code_lines.append('')
# 生成关系类型
# Khởi tạo các đoạn mã tạo lập Relationship (Edges)
for edge in ontology.get("edge_types", []):
name = edge["name"]
# 转换为PascalCase类名
# Đổi cấu trúc tên format Class theo chuẩn PascalCase của Python
class_name = ''.join(word.capitalize() for word in name.split('_'))
desc = edge.get("description", f"A {name} relationship.")
@ -419,8 +419,8 @@ class OntologyGenerator:
code_lines.append('')
code_lines.append('')
# 生成类型字典
code_lines.append('# ============== 类型配置 ==============')
# Tự động kết xuất ra dictionary mapping từ Tên Loại - sang class Object
code_lines.append('# ============== Các tuỳ chỉnh Map Cấu Hình ==============')
code_lines.append('')
code_lines.append('ENTITY_TYPES = {')
for entity in ontology.get("entity_types", []):
@ -436,7 +436,7 @@ class OntologyGenerator:
code_lines.append('}')
code_lines.append('')
# 生成边的source_targets映射
# Cấu hình mảng mapping giới hạn Source->Target cho từng cạnh (Edges source_targets config)
code_lines.append('EDGE_SOURCE_TARGETS = {')
for edge in ontology.get("edge_types", []):
name = edge["name"]

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

View File

@ -1,7 +1,7 @@
"""
OASIS模拟管理器
管理Twitter和Reddit双平台并行模拟
使用预设脚本 + LLM智能生成配置参数
Trình Quản Phỏng OASIS
Đảm nhiệm xây dựng điều phối chạy phỏng song song trên hai nền tảng giả lập Twitter Reddit.
Sử dụng các kịch bản sẵn kết hợp cùng LLM để thiết lập thông minh bộ tham số phỏng.
"""
import os
@ -22,60 +22,60 @@ logger = get_logger('mirofish.simulation')
class SimulationStatus(str, Enum):
"""模拟状态"""
CREATED = "created"
PREPARING = "preparing"
READY = "ready"
RUNNING = "running"
PAUSED = "paused"
STOPPED = "stopped" # 模拟被手动停止
COMPLETED = "completed" # 模拟自然完成
FAILED = "failed"
"""Trạng thái hiện tại của quá trình mô phỏng"""
CREATED = "created" # Đã khởi tạo
PREPARING = "preparing" # Đang chuẩn bị (chuẩn bị dữ liệu/profile)
READY = "ready" # Đã sẵn sàng chạy
RUNNING = "running" # Đang xử lý giả lập
PAUSED = "paused" # Tạm dừng
STOPPED = "stopped" # Hệ thống mô phỏng bị người dùng chủ động dừng lại
COMPLETED = "completed" # Quá trình mô phỏng kết thúc tự nhiên một cách thành công
FAILED = "failed" # Bị lỗi hệ thống gián đoạn
class PlatformType(str, Enum):
"""平台类型"""
"""Phân loại nền tảng giả lập Mạng xã hội"""
TWITTER = "twitter"
REDDIT = "reddit"
@dataclass
class SimulationState:
"""模拟状态"""
"""Class lưu trữ cấu trúc Dữ liệu/Trạng thái của một lượt mô phỏng"""
simulation_id: str
project_id: str
graph_id: str
# 平台启用状态
# Cờ trạng thái bật/tắt nền tảng chạy
enable_twitter: bool = True
enable_reddit: bool = True
# 状态
# Current status
status: SimulationStatus = SimulationStatus.CREATED
# 准备阶段数据
# Dữ liệu thu thập / thống kê của Preparing Phase
entities_count: int = 0
profiles_count: int = 0
entity_types: List[str] = field(default_factory=list)
# 配置生成信息
# Thông tin các nội dung cấu hình mà LLM đã tự động tạo
config_generated: bool = False
config_reasoning: str = ""
# 运行时数据
# Dữ liệu cập nhật theo thời gian thực (Runtime Phase)
current_round: int = 0
twitter_status: str = "not_started"
reddit_status: str = "not_started"
# 时间戳
# Nhãn Timestamp lịch sử
created_at: str = field(default_factory=lambda: datetime.now().isoformat())
updated_at: str = field(default_factory=lambda: datetime.now().isoformat())
# 错误信息
# Lịch sử thông báo Lỗi (nếu có để render trả về frontend)
error: Optional[str] = None
def to_dict(self) -> Dict[str, Any]:
"""完整状态字典(内部使用)"""
"""Tạo thành Dictionary đầy đủ nhất (Dùng cho việc lưu cấu hình local cho hệ thống bên trong đọc)"""
return {
"simulation_id": self.simulation_id,
"project_id": self.project_id,
@ -97,7 +97,7 @@ class SimulationState:
}
def to_simple_dict(self) -> Dict[str, Any]:
"""简化状态字典API返回使用"""
"""Tạo thành Dictionary bao hàm các thông số vắn tắt hơn (Dùng cho API response trả về Client - Frontend)"""
return {
"simulation_id": self.simulation_id,
"project_id": self.project_id,
@ -113,36 +113,36 @@ class SimulationState:
class SimulationManager:
"""
模拟管理器
Kịch bản Quản trung tâm của tính năng Phỏng
核心功能
1. 从Zep图谱读取实体并过滤
2. 生成OASIS Agent Profile
3. 使用LLM智能生成模拟配置参数
4. 准备预设脚本所需的所有文件
Luồng thiết lập cốt lõi:
1. Trích xuất nhóm các Thực Thể (Entity) được định nghĩa sẵn trong hệ thống lưu trữ Graph của Zep.
2. Chế lại thành các hồ Profile thiết lập tiêu chuẩn của OASIS framework (Agent)
3. Trao quyền cho sức mạnh 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 phỏng
4. Cài đặt các thư mục 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) thể khai thác sử dụng.
"""
# 模拟数据存储目录
# Nơi chứa thư mục chứa Dữ liệu mô phỏng Local
SIMULATION_DATA_DIR = os.path.join(
os.path.dirname(__file__),
'../../uploads/simulations'
)
def __init__(self):
# 确保目录存在
# Đảm bảo môi trường file data đã được set up
os.makedirs(self.SIMULATION_DATA_DIR, exist_ok=True)
# 内存中的模拟状态缓存
# Biến dictionary ở mức Application theo dõi trạng thái simulation qua Cache RAM.
self._simulations: Dict[str, SimulationState] = {}
def _get_simulation_dir(self, simulation_id: str) -> str:
"""获取模拟数据目录"""
"""Lấy trả về các đường dẫn thư mục gốc tương ứng với Simulation ID"""
sim_dir = os.path.join(self.SIMULATION_DATA_DIR, simulation_id)
os.makedirs(sim_dir, exist_ok=True)
return sim_dir
def _save_simulation_state(self, state: SimulationState):
"""保存模拟状态到文件"""
"""Bật tính năng lưu state định dạng JSON ra ổ cứng"""
sim_dir = self._get_simulation_dir(state.simulation_id)
state_file = os.path.join(sim_dir, "state.json")
@ -154,7 +154,7 @@ class SimulationManager:
self._simulations[state.simulation_id] = state
def _load_simulation_state(self, simulation_id: str) -> Optional[SimulationState]:
"""从文件加载模拟状态"""
"""Load ngược lại data của tiến trình Mô Phỏng thông qua tệp cấu hình JSON"""
if simulation_id in self._simulations:
return self._simulations[simulation_id]
@ -198,16 +198,16 @@ class SimulationManager:
enable_reddit: bool = True,
) -> SimulationState:
"""
创建新的模拟
Khởi tạo môi trường ảo / mới cho Phỏng
Args:
project_id: 项目ID
graph_id: Zep图谱ID
enable_twitter: 是否启用Twitter模拟
enable_reddit: 是否启用Reddit模拟
project_id: ID của Project (Quản cấp đầu vào)
graph_id: Đồ thị ID tương ứng lấy bên Zep
enable_twitter: Công tắc (Bật/Tắt) luồng giả lập Twitter
enable_reddit: Công tắc (Bật/Tắt) luồng giả lập Reddit
Returns:
SimulationState
Đối tượng Class SimulationState
"""
import uuid
simulation_id = f"sim_{uuid.uuid4().hex[:12]}"
@ -222,7 +222,7 @@ class SimulationManager:
)
self._save_simulation_state(state)
logger.info(f"创建模拟: {simulation_id}, project={project_id}, graph={graph_id}")
logger.info(f"Created simulation: {simulation_id}, project={project_id}, graph={graph_id}")
return state
@ -237,30 +237,30 @@ class SimulationManager:
parallel_profile_count: int = 3
) -> SimulationState:
"""
准备模拟环境全程自动化
Giai đoạn chuẩn bị dữ liệu tạo giả lập phỏng (Tiến trình Automation 100%)
步骤
1. 从Zep图谱读取并过滤实体
2. 为每个实体生成OASIS Agent Profile可选LLM增强支持并行
3. 使用LLM智能生成模拟配置参数时间活跃度发言频率等
4. 保存配置文件和Profile文件
5. 复制预设脚本到模拟目录
Các bước diễn ra:
1. Gọi lấy các cụm Entity (thực thể) bộ lọt (filter) từ Zep Graph API
2. Tự động khởi tạo hàng loạt Agent Profile chạy OASIS tương ứng với Entity (Hỗ trợ gọi AI LLM để làm mượt văn bản / tăng tốc chạy song song)
3. Hỏi bắt bot LLM suy luận ra hệ tham số setting thông minh nhất (thời gian phỏng rỉ, hệ số tần suất nói chuyện hoạt động ...)
4. In ra các file cấu hình JSON của profile để hệ thống dễ đọc
5. Copy nguyên các Scripts chuẩn được cấu hình sẵn (preset) ném vào thư mục để chạy
Args:
simulation_id: 模拟ID
simulation_requirement: 模拟需求描述用于LLM生成配置
document_text: 原始文档内容用于LLM理解背景
defined_entity_types: 预定义的实体类型可选
use_llm_for_profiles: 是否使用LLM生成详细人设
progress_callback: 进度回调函数 (stage, progress, message)
parallel_profile_count: 并行生成人设的数量默认3
simulation_id: ID của chu trình giả lập
simulation_requirement: Chuỗi text từ người dùng yêu cầu phỏng (gửi cho config sinh cấu hình)
document_text: Nội dung file Raw nguyên thủy (Cho LLM đánh giá context bối cảnh ban đầu)
defined_entity_types: Dánh sách các Entity Model sẵn do Zep định nghĩa (Option)
use_llm_for_profiles: Toggle tính năng sử dụng hình LLM để buff thêm chi tiết cài đặt con Bot
progress_callback: Hàm callback update log progress (chuyển về màn hình Frontend view) format (stage, progress, message)
parallel_profile_count: Giới hạn concurrent threading chạy LLM gọi profile (Default 3 luồng cùng lúc để làm nhanh hơn)
Returns:
SimulationState
Class cài đặt Data - SimulationState
"""
state = self._load_simulation_state(simulation_id)
if not state:
raise ValueError(f"模拟不存在: {simulation_id}")
raise ValueError(f"Giả lập với ID: {simulation_id} không tồn tại")
try:
state.status = SimulationStatus.PREPARING
@ -268,14 +268,14 @@ class SimulationManager:
sim_dir = self._get_simulation_dir(simulation_id)
# ========== 阶段1: 读取并过滤实体 ==========
# ========== Giai đoạn 1: Kết nối lấy Node Data Entity và Sàng lọc ==========
if progress_callback:
progress_callback("reading", 0, "正在连接Zep图谱...")
progress_callback("reading", 0, "Connecting to Zep Graph data store...")
reader = ZepEntityReader()
if progress_callback:
progress_callback("reading", 30, "正在读取节点数据...")
progress_callback("reading", 30, "Extracting Node data from graph...")
filtered = reader.filter_defined_entities(
graph_id=state.graph_id,
@ -289,29 +289,29 @@ class SimulationManager:
if progress_callback:
progress_callback(
"reading", 100,
f"完成,共 {filtered.filtered_count} 个实体",
f"Extraction complete, filtered entity count: {filtered.filtered_count} empty entities",
current=filtered.filtered_count,
total=filtered.filtered_count
)
if filtered.filtered_count == 0:
state.status = SimulationStatus.FAILED
state.error = "没有找到符合条件的实体,请检查图谱是否正确构建"
state.error = "No valid entities extracted for simulation. Please check if the Graph was generated properly with valid text."
self._save_simulation_state(state)
return state
# ========== 阶段2: 生成Agent Profile ==========
# ========== Giai đoạn 2: Bắt đầu sinh Agent Profiles cho OASIS ==========
total_entities = len(filtered.entities)
if progress_callback:
progress_callback(
"generating_profiles", 0,
"开始生成...",
"Ready for AI Generation process...",
current=0,
total=total_entities
)
# 传入graph_id以启用Zep检索功能获取更丰富的上下文
# Gửi mã graph_id để bộ Profile có thể fetch thêm tài liệu nếu model cần lục vấn sâu
generator = OasisProfileGenerator(graph_id=state.graph_id)
def profile_progress(current, total, msg):
@ -325,7 +325,7 @@ class SimulationManager:
item_name=msg
)
# 设置实时保存的文件路径(优先使用 Reddit JSON 格式)
# Khai báo đường dẫn tạm để AI lưu Real-time kết quả (Đặt ưu tiên Platform Reddit JSON làm chuẩn)
realtime_output_path = None
realtime_platform = "reddit"
if state.enable_reddit:
@ -339,20 +339,20 @@ class SimulationManager:
entities=filtered.entities,
use_llm=use_llm_for_profiles,
progress_callback=profile_progress,
graph_id=state.graph_id, # 传入graph_id用于Zep检索
parallel_count=parallel_profile_count, # 并行生成数量
realtime_output_path=realtime_output_path, # 实时保存路径
output_platform=realtime_platform # 输出格式
graph_id=state.graph_id, # Để tìm kiếm Zep Search Index
parallel_count=parallel_profile_count, # Số dòng luồng Async
realtime_output_path=realtime_output_path, # Lưu log thời gian thực
output_platform=realtime_platform # Đuôi file xuất
)
state.profiles_count = len(profiles)
# 保存Profile文件注意Twitter使用CSV格式Reddit使用JSON格式
# Reddit 已经在生成过程中实时保存了,这里再保存一次确保完整性
# Backup lại kết quả Profile (Twitter xuất ra text CSV, Reddit thì bắt buộc JSON cho cấu trúc OASIS)
# Reddit đã được render đồng thời ở block trên nhưng đây là re-save toàn bộ
if progress_callback:
progress_callback(
"generating_profiles", 95,
"保存Profile文件...",
"Compressing Profile data...",
current=total_entities,
total=total_entities
)
@ -365,7 +365,7 @@ class SimulationManager:
)
if state.enable_twitter:
# Twitter使用CSV格式这是OASIS的要求
# Riêng Twitter với code Script base OAsis của họ yêu cầu CSV format
generator.save_profiles(
profiles=profiles,
file_path=os.path.join(sim_dir, "twitter_profiles.csv"),
@ -375,16 +375,16 @@ class SimulationManager:
if progress_callback:
progress_callback(
"generating_profiles", 100,
f"完成,共 {len(profiles)} Profile",
f"Done, created {len(profiles)} Profiles",
current=len(profiles),
total=len(profiles)
)
# ========== 阶段3: LLM智能生成模拟配置 ==========
# ========== Giai đoạn 3: Uỷ thác cho LLM phân tích và xuất tham số mô phỏng ==========
if progress_callback:
progress_callback(
"generating_config", 0,
"正在分析模拟需求...",
"Analyzing input requirements...",
current=0,
total=3
)
@ -394,7 +394,7 @@ class SimulationManager:
if progress_callback:
progress_callback(
"generating_config", 30,
"正在调用LLM生成配置...",
"LLM Bot is generating configuration...",
current=1,
total=3
)
@ -413,12 +413,12 @@ class SimulationManager:
if progress_callback:
progress_callback(
"generating_config", 70,
"正在保存配置文件...",
"Saving Config parameters...",
current=2,
total=3
)
# 保存配置文件
# Lưu file cứng simulation_config.json
config_path = os.path.join(sim_dir, "simulation_config.json")
with open(config_path, 'w', encoding='utf-8') as f:
f.write(sim_params.to_json())
@ -429,25 +429,25 @@ class SimulationManager:
if progress_callback:
progress_callback(
"generating_config", 100,
"配置生成完成",
"Configuration Generation complete",
current=3,
total=3
)
# 注意:运行脚本保留在 backend/scripts/ 目录,不再复制到模拟目录
# 启动模拟时simulation_runner 会从 scripts/ 目录运行脚本
# Lưu ý kiến trúc: Các scripts thao tác thực thi vẫn để gốc ở `backend/scripts/`, SẼ KHÔNG CẦN chép đè sang folder Project
# Tại thời gian Khởi chạy, `simulation_runner` sẽ nạp base chạy thẳng từ folder `scripts/` đó.
# 更新状态
# Cập nhật status
state.status = SimulationStatus.READY
self._save_simulation_state(state)
logger.info(f"模拟准备完成: {simulation_id}, "
f"entities={state.entities_count}, profiles={state.profiles_count}")
logger.info(f"Finished simulation preparation phase for ID: {simulation_id}, "
f"Total entities={state.entities_count}, Created profiles={state.profiles_count}")
return state
except Exception as e:
logger.error(f"模拟准备失败: {simulation_id}, error={str(e)}")
logger.error(f"Error occurred during Simulation preparation (Sim ID: {simulation_id}), ERROR CODE: {str(e)}")
import traceback
logger.error(traceback.format_exc())
state.status = SimulationStatus.FAILED
@ -456,16 +456,16 @@ class SimulationManager:
raise
def get_simulation(self, simulation_id: str) -> Optional[SimulationState]:
"""获取模拟状态"""
"""Đọc và lấy State hiện tại của Simulator"""
return self._load_simulation_state(simulation_id)
def list_simulations(self, project_id: Optional[str] = None) -> List[SimulationState]:
"""列出所有模拟"""
"""Liệt kê toàn bộ danh sách các Mô Phỏng (Simulations) đã khởi tạo"""
simulations = []
if os.path.exists(self.SIMULATION_DATA_DIR):
for sim_id in os.listdir(self.SIMULATION_DATA_DIR):
# 跳过隐藏文件(如 .DS_Store和非目录文件
# Loại bỏ các folder/file rác do hệ điều hành sinh ra (ví dụ: .DS_Store của macOS) hoặc không phải thư mục
sim_path = os.path.join(self.SIMULATION_DATA_DIR, sim_id)
if sim_id.startswith('.') or not os.path.isdir(sim_path):
continue
@ -478,10 +478,10 @@ class SimulationManager:
return simulations
def get_profiles(self, simulation_id: str, platform: str = "reddit") -> List[Dict[str, Any]]:
"""获取模拟的Agent Profile"""
"""Lấy/Tải dữ liệu Agent Profile do AI sinh ra dựa theo nền tảng mạng xã hội"""
state = self._load_simulation_state(simulation_id)
if not state:
raise ValueError(f"模拟不存在: {simulation_id}")
raise ValueError(f"Simulation with ID {simulation_id} does not exist")
sim_dir = self._get_simulation_dir(simulation_id)
profile_path = os.path.join(sim_dir, f"{platform}_profiles.json")
@ -493,7 +493,7 @@ class SimulationManager:
return json.load(f)
def get_simulation_config(self, simulation_id: str) -> Optional[Dict[str, Any]]:
"""获取模拟配置"""
"""Lấy thông số cấu hình của bản mô phỏng"""
sim_dir = self._get_simulation_dir(simulation_id)
config_path = os.path.join(sim_dir, "simulation_config.json")
@ -504,7 +504,7 @@ class SimulationManager:
return json.load(f)
def get_run_instructions(self, simulation_id: str) -> Dict[str, str]:
"""获取运行说明"""
"""Output ra hướng dẫn / Các câu lệnh dòng lệnh (CMD) để thực thi chạy bản đồ mô phỏng này"""
sim_dir = self._get_simulation_dir(simulation_id)
config_path = os.path.join(sim_dir, "simulation_config.json")
scripts_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../scripts'))
@ -519,10 +519,10 @@ class SimulationManager:
"parallel": f"python {scripts_dir}/run_parallel_simulation.py --config {config_path}",
},
"instructions": (
f"1. 激活conda环境: conda activate MiroFish\n"
f"2. 运行模拟 (脚本位于 {scripts_dir}):\n"
f" - 单独运行Twitter: python {scripts_dir}/run_twitter_simulation.py --config {config_path}\n"
f" - 单独运行Reddit: python {scripts_dir}/run_reddit_simulation.py --config {config_path}\n"
f" - 并行运行双平台: python {scripts_dir}/run_parallel_simulation.py --config {config_path}"
f"1. Khởi động môi trường môi trường lập trình Conda (nếu có): conda activate MiroFish\n"
f"2. Bắt đầu Run giả lập (Scripts gốc được gọi ra tại {scripts_dir}):\n"
f" - Nếu muốn chỉ giả lập trên Twitter: python {scripts_dir}/run_twitter_simulation.py --config {config_path}\n"
f" - Nếu muốn chỉ giả lập trên Reddit: python {scripts_dir}/run_reddit_simulation.py --config {config_path}\n"
f" - Chạy giả lập cả hai phân luồng song song: python {scripts_dir}/run_parallel_simulation.py --config {config_path}"
)
}

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
"""
文本处理服务
Dịch vụ xử văn bản (Text processing service)
"""
from typing import List, Optional
@ -7,11 +7,11 @@ from ..utils.file_parser import FileParser, split_text_into_chunks
class TextProcessor:
"""文本处理器"""
"""Trình xử lý văn bản"""
@staticmethod
def extract_from_files(file_paths: List[str]) -> str:
"""从多个文件提取文本"""
"""Trích xuất và kết hợp văn bản từ nhiều file (các đường dẫn file truyền vào)"""
return FileParser.extract_from_multiple(file_paths)
@staticmethod
@ -21,37 +21,37 @@ class TextProcessor:
overlap: int = 50
) -> List[str]:
"""
分割文本
Chia nhỏ văn bản (Phân mảnh văn bản dài thành các đoạn nhỏ hơn - chunking)
Args:
text: 原始文本
chunk_size: 块大小
overlap: 重叠大小
text: Văn bản gốc cần chia
chunk_size: Kích thước tối đa của mỗi đoạn (chunk)
overlap: Số tự chồng chéo giữa các đoạn liên tiếp (để giữ bối cảnh không bị đứt đoạn)
Returns:
文本块列表
Danh sách gồm các mảng văn bản sau khi chia
"""
return split_text_into_chunks(text, chunk_size, overlap)
@staticmethod
def preprocess_text(text: str) -> str:
"""
预处理文本
- 移除多余空白
- 标准化换行
Tiền xử văn bản (Làm sạch văn bản)
- Xoá bỏ các khoảng trắng thừa
- Chuẩn hoá định dạng xuống dòng (Line breaks)
Args:
text: 原始文本
text: Văn bản thô ban đầu
Returns:
处理后的文本
Văn bản đã được tinh giản, làm sạch
"""
import re
# 标准化换行
# Chuẩn hoá ký tự xuống dòng (Windows \r\n hoặc Mac cũ \r thành \n chuẩn Linux)
text = text.replace('\r\n', '\n').replace('\r', '\n')
# 移除连续空行(保留最多两个换行)
# Xoá các dòng trống liên tiếp (Chỉ giữ lại tối đa 2 lần xuống dòng liên tiếp)
text = re.sub(r'\n{3,}', '\n\n', text)
# 移除行首行尾空白
@ -62,7 +62,7 @@ class TextProcessor:
@staticmethod
def get_text_stats(text: str) -> dict:
"""获取文本统计信息"""
"""Lấy một số thông tin thống kê về đoạn văn bản (Số ký tự, số dòng, số từ)"""
return {
"total_chars": len(text),
"total_lines": text.count('\n') + 1,

View File

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

View File

@ -1,6 +1,6 @@
"""
Zep图谱记忆更新服务
将模拟中的Agent活动动态更新到Zep图谱中
Dịch vụ cập nhật bộ nhớ đồ thị Zep
Cập nhật động các hoạt động của Agent trong phỏng lên đồ thị Zep
"""
import os
@ -22,7 +22,7 @@ logger = get_logger('mirofish.zep_graph_memory_updater')
@dataclass
class AgentActivity:
"""Agent活动记录"""
"""Bản ghi hoạt động của Agent"""
platform: str # twitter / reddit
agent_id: int
agent_name: str
@ -33,12 +33,12 @@ class AgentActivity:
def to_episode_text(self) -> str:
"""
将活动转换为可以发送给Zep的文本描述
Chuyển đổi hoạt động thành tả văn bản để gửi cho Zep
采用自然语言描述格式让Zep能够从中提取实体和关系
不添加模拟相关的前缀避免误导图谱更新
Sử dụng định dạng tả bằng ngôn ngữ tự nhiên để Zep thể trích xuất thực thể mối quan hệ
Không thêm tiền tố liên quan đến phỏng để tránh gây nhiễu khi cập nhật đồ thị
"""
# 根据不同的动作类型生成不同的描述
# Tạo mô tả khác nhau dựa trên từng loại hành động
action_descriptions = {
"CREATE_POST": self._describe_create_post,
"LIKE_POST": self._describe_like_post,
@ -57,222 +57,222 @@ class AgentActivity:
describe_func = action_descriptions.get(self.action_type, self._describe_generic)
description = describe_func()
# 直接返回 "agent名称: 活动描述" 格式,不添加模拟前缀
# Trả về trực tiếp định dạng "Tên agent: Mô tả hoạt động", không thêm tiền tố mô phỏng
return f"{self.agent_name}: {description}"
def _describe_create_post(self) -> str:
content = self.action_args.get("content", "")
if content:
return f"发布了一条帖子:「{content}"
return "发布了一条帖子"
return f"posted a post: `{content}`"
return "posted a post"
def _describe_like_post(self) -> str:
"""点赞帖子 - 包含帖子原文和作者信息"""
"""Like bài viết - bao gồm nội dung gốc bài viết và thông tin tác giả"""
post_content = self.action_args.get("post_content", "")
post_author = self.action_args.get("post_author_name", "")
if post_content and post_author:
return f"点赞了{post_author}的帖子:「{post_content}"
return f"liked {post_author}'s post: `{post_content}`"
elif post_content:
return f"点赞了一条帖子:「{post_content}"
return f"liked a post: `{post_content}`"
elif post_author:
return f"点赞了{post_author}的一条帖子"
return "点赞了一条帖子"
return f"liked a post by {post_author}"
return "liked a post"
def _describe_dislike_post(self) -> str:
"""踩帖子 - 包含帖子原文和作者信息"""
"""Dislike bài viết - bao gồm nội dung gốc và thông tin tác giả"""
post_content = self.action_args.get("post_content", "")
post_author = self.action_args.get("post_author_name", "")
if post_content and post_author:
return f"踩了{post_author}的帖子:「{post_content}"
return f"disliked {post_author}'s post: `{post_content}`"
elif post_content:
return f"踩了一条帖子:「{post_content}"
return f"disliked a post: `{post_content}`"
elif post_author:
return f"踩了{post_author}的一条帖子"
return "踩了一条帖子"
return f"disliked a post by {post_author}"
return "disliked a post"
def _describe_repost(self) -> str:
"""转发帖子 - 包含原帖内容和作者信息"""
"""Repost bài viết - bao gồm nội dung gốc và thông tin tác giả"""
original_content = self.action_args.get("original_content", "")
original_author = self.action_args.get("original_author_name", "")
if original_content and original_author:
return f"转发了{original_author}的帖子:「{original_content}"
return f"reposted {original_author}'s post: `{original_content}`"
elif original_content:
return f"转发了一条帖子:「{original_content}"
return f"reposted a post: `{original_content}`"
elif original_author:
return f"转发了{original_author}的一条帖子"
return "转发了一条帖子"
return f"reposted a post by {original_author}"
return "reposted a post"
def _describe_quote_post(self) -> str:
"""引用帖子 - 包含原帖内容、作者信息和引用评论"""
"""Quote bài viết - bao gồm nội dung bài gốc, tác giả và nội dung bình luận quote"""
original_content = self.action_args.get("original_content", "")
original_author = self.action_args.get("original_author_name", "")
quote_content = self.action_args.get("quote_content", "") or self.action_args.get("content", "")
base = ""
if original_content and original_author:
base = f"引用了{original_author}的帖子「{original_content}"
base = f"quoted {original_author}'s post `{original_content}`"
elif original_content:
base = f"引用了一条帖子「{original_content}"
base = f"quoted a post `{original_content}`"
elif original_author:
base = f"引用了{original_author}的一条帖子"
base = f"quoted a post by {original_author}"
else:
base = "引用了一条帖子"
base = "quoted a post"
if quote_content:
base += f",并评论道:「{quote_content}"
base += f" and commented: `{quote_content}`"
return base
def _describe_follow(self) -> str:
"""关注用户 - 包含被关注用户的名称"""
"""Follow người dùng - bao gồm tên người được follow"""
target_user_name = self.action_args.get("target_user_name", "")
if target_user_name:
return f"关注了用户「{target_user_name}"
return "关注了一个用户"
return f"followed user `{target_user_name}`"
return "followed a user"
def _describe_create_comment(self) -> str:
"""发表评论 - 包含评论内容和所评论的帖子信息"""
"""Tạo bình luận - bao gồm nội dung bình luận và thông tin bài viết được bình luận"""
content = self.action_args.get("content", "")
post_content = self.action_args.get("post_content", "")
post_author = self.action_args.get("post_author_name", "")
if content:
if post_content and post_author:
return f"{post_author}的帖子「{post_content}」下评论道:「{content}"
return f"commented under {post_author}'s post `{post_content}`: `{content}`"
elif post_content:
return f"在帖子「{post_content}」下评论道:「{content}"
return f"commented under post `{post_content}`: `{content}`"
elif post_author:
return f"{post_author}的帖子下评论道:「{content}"
return f"评论道:「{content}"
return "发表了评论"
return f"commented under {post_author}'s post: `{content}`"
return f"commented: `{content}`"
return "posted a comment"
def _describe_like_comment(self) -> str:
"""点赞评论 - 包含评论内容和作者信息"""
"""Like bình luận - bao gồm nội dung bình luận và tác giả"""
comment_content = self.action_args.get("comment_content", "")
comment_author = self.action_args.get("comment_author_name", "")
if comment_content and comment_author:
return f"点赞了{comment_author}的评论:「{comment_content}"
return f"liked {comment_author}'s comment: `{comment_content}`"
elif comment_content:
return f"点赞了一条评论:「{comment_content}"
return f"liked a comment: `{comment_content}`"
elif comment_author:
return f"点赞了{comment_author}的一条评论"
return "点赞了一条评论"
return f"liked a comment by {comment_author}"
return "liked a comment"
def _describe_dislike_comment(self) -> str:
"""踩评论 - 包含评论内容和作者信息"""
"""Dislike bình luận - bao gồm nội dung bình luận và tác giả"""
comment_content = self.action_args.get("comment_content", "")
comment_author = self.action_args.get("comment_author_name", "")
if comment_content and comment_author:
return f"踩了{comment_author}的评论:「{comment_content}"
return f"disliked {comment_author}'s comment: `{comment_content}`"
elif comment_content:
return f"踩了一条评论:「{comment_content}"
return f"disliked a comment: `{comment_content}`"
elif comment_author:
return f"踩了{comment_author}的一条评论"
return "踩了一条评论"
return f"disliked a comment by {comment_author}"
return "disliked a comment"
def _describe_search(self) -> str:
"""搜索帖子 - 包含搜索关键词"""
"""Tìm kiếm bài viết - bao gồm từ khóa tìm kiếm"""
query = self.action_args.get("query", "") or self.action_args.get("keyword", "")
return f"搜索了「{query}" if query else "进行了搜索"
return f"searched for `{query}`" if query else "performed a search"
def _describe_search_user(self) -> str:
"""搜索用户 - 包含搜索关键词"""
"""Tìm kiếm người dùng - bao gồm từ khóa tìm kiếm"""
query = self.action_args.get("query", "") or self.action_args.get("username", "")
return f"搜索了用户「{query}" if query else "搜索了用户"
return f"searched for user `{query}`" if query else "searched for a user"
def _describe_mute(self) -> str:
"""屏蔽用户 - 包含被屏蔽用户的名称"""
"""Mute người dùng - bao gồm tên người bị mute"""
target_user_name = self.action_args.get("target_user_name", "")
if target_user_name:
return f"屏蔽了用户「{target_user_name}"
return "屏蔽了一个用户"
return f"muted user `{target_user_name}`"
return "muted a user"
def _describe_generic(self) -> str:
# 对于未知的动作类型,生成通用描述
return f"执行了{self.action_type}操作"
# Tạo mô tả chung cho các loại hành động không xác định
return f"performed {self.action_type} action"
class ZepGraphMemoryUpdater:
"""
Zep图谱记忆更新器
Trình cập nhật bộ nhớ đồ thị Zep
监控模拟的actions日志文件将新的agent活动实时更新到Zep图谱中
按平台分组每累积BATCH_SIZE条活动后批量发送到Zep
Giám sát file log actions của phỏng, cập nhật trực tiếp các hoạt động mới của agent lên đồ thị Zep.
Nhóm theo nền tảng, gửi hàng loạt lên Zep sau khi tích lũy đủ số lượng hoạt động (BATCH_SIZE).
所有有意义的行为都会被更新到Zepaction_args中会包含完整的上下文信息
- 点赞/踩的帖子原文
- 转发/引用的帖子原文
- 关注/屏蔽的用户名
- 点赞/踩的评论原文
Tất cả các hành vi ý nghĩa đều được cập nhật lên Zep, action_args chứa đầy đủ thông tin ngữ cảnh:
- Nội dung gốc của bài viết được like/dislike
- Nội dung gốc của bài viết được repost/quote
- Tên người dùng được follow/mute
- Nội dung gốc của bình luận được like/dislike
"""
# 批量发送大小(每个平台累积多少条后发送)
# Số lượng gửi mỗi lô (gửi sau khi mỗi nền tảng tích lũy đủ số lượng này)
BATCH_SIZE = 5
# 平台名称映射(用于控制台显示)
# Ánh xạ tên nền tảng (dùng để hiển thị trên console)
PLATFORM_DISPLAY_NAMES = {
'twitter': '世界1',
'reddit': '世界2',
'twitter': 'World 1',
'reddit': 'World 2',
}
# 发送间隔(秒),避免请求过快
# Thời gian gửi cách nhau (giây), tránh request quá nhanh
SEND_INTERVAL = 0.5
# 重试配置
# Cấu hình thử lại (retry)
MAX_RETRIES = 3
RETRY_DELAY = 2 #
RETRY_DELAY = 2 # giây
def __init__(self, graph_id: str, api_key: Optional[str] = None):
"""
初始化更新器
Khởi tạo trình cập nhật
Args:
graph_id: Zep图谱ID
api_key: Zep API Key可选默认从配置读取
graph_id: ID của đồ thị Zep
api_key: Zep API Key (tự chọn, mặc định lấy từ config)
"""
self.graph_id = graph_id
self.api_key = api_key or Config.ZEP_API_KEY
if not self.api_key:
raise ValueError("ZEP_API_KEY未配置")
raise ValueError("ZEP_API_KEY is not configured")
self.client = Zep(api_key=self.api_key)
# 活动队列
# Hàng đợi hoạt động
self._activity_queue: Queue = Queue()
# 按平台分组的活动缓冲区每个平台各自累积到BATCH_SIZE后批量发送
# Bộ đệm hoạt động nhóm theo nền tảng (gửi hàng loạt sau khi đạt đến BATCH_SIZE)
self._platform_buffers: Dict[str, List[AgentActivity]] = {
'twitter': [],
'reddit': [],
}
self._buffer_lock = threading.Lock()
# 控制标志
# Cờ điều khiển
self._running = False
self._worker_thread: Optional[threading.Thread] = None
# 统计
self._total_activities = 0 # 实际添加到队列的活动数
self._total_sent = 0 # 成功发送到Zep的批次数
self._total_items_sent = 0 # 成功发送到Zep的活动条数
self._failed_count = 0 # 发送失败的批次数
self._skipped_count = 0 # 被过滤跳过的活动数DO_NOTHING
# Thống kê
self._total_activities = 0 # Số lượng hoạt động thực tế được thêm vào hàng đợi
self._total_sent = 0 # Số đợt hàng gửi đi thành công tới Zep
self._total_items_sent = 0 # Số lượng các hoạt động đã gửi thành công tới Zep
self._failed_count = 0 # Số lượt gửi đi thất bại
self._skipped_count = 0 # Số lượng các hoạt động bị filter bỏ qua (DO_NOTHING)
logger.info(f"ZepGraphMemoryUpdater 初始化完成: graph_id={graph_id}, batch_size={self.BATCH_SIZE}")
logger.info(f"ZepGraphMemoryUpdater initialized: graph_id={graph_id}, batch_size={self.BATCH_SIZE}")
def _get_platform_display_name(self, platform: str) -> str:
"""获取平台的显示名称"""
"""Lấy tên hiển thị của nền tảng"""
return self.PLATFORM_DISPLAY_NAMES.get(platform.lower(), platform)
def start(self):
"""启动后台工作线程"""
"""Khởi chạy luồng làm việc dưới background"""
if self._running:
return
@ -283,19 +283,19 @@ class ZepGraphMemoryUpdater:
name=f"ZepMemoryUpdater-{self.graph_id[:8]}"
)
self._worker_thread.start()
logger.info(f"ZepGraphMemoryUpdater 已启动: graph_id={self.graph_id}")
logger.info(f"ZepGraphMemoryUpdater started: graph_id={self.graph_id}")
def stop(self):
"""停止后台工作线程"""
"""Tắt luồng làm việc dưới background"""
self._running = False
# 发送剩余的活动
# Gửi nốt các hoạt động còn lại
self._flush_remaining()
if self._worker_thread and self._worker_thread.is_alive():
self._worker_thread.join(timeout=10)
logger.info(f"ZepGraphMemoryUpdater 已停止: graph_id={self.graph_id}, "
logger.info(f"ZepGraphMemoryUpdater stopped: graph_id={self.graph_id}, "
f"total_activities={self._total_activities}, "
f"batches_sent={self._total_sent}, "
f"items_sent={self._total_items_sent}, "
@ -304,43 +304,43 @@ class ZepGraphMemoryUpdater:
def add_activity(self, activity: AgentActivity):
"""
添加一个agent活动到队列
Thêm một hoạt động của agent vào hàng đợi
所有有意义的行为都会被添加到队列包括
- CREATE_POST发帖
- CREATE_COMMENT评论
- QUOTE_POST引用帖子
- SEARCH_POSTS搜索帖子
- SEARCH_USER搜索用户
- LIKE_POST/DISLIKE_POST点赞/踩帖子
- REPOST转发
- FOLLOW关注
- MUTE屏蔽
- LIKE_COMMENT/DISLIKE_COMMENT点赞/踩评论
Tất cả các hành vi ý nghĩa đều sẽ được thêm vào hàng đợi, bao gồm:
- CREATE_POST (Đăng bài)
- CREATE_COMMENT (Bình luận)
- QUOTE_POST (Trích dẫn bài viết)
- SEARCH_POSTS (Tìm kiếm bài viết)
- SEARCH_USER (Tìm kiếm người dùng)
- LIKE_POST/DISLIKE_POST (Like/Dislike bài viết)
- REPOST (Repost)
- FOLLOW (Theo dõi)
- MUTE (Chặn)
- LIKE_COMMENT/DISLIKE_COMMENT (Like/dislike bình luận)
action_args中会包含完整的上下文信息如帖子原文用户名等
action_args sẽ bao gồm toàn bộ thông tin ngữ cảnh (như nội dung gốc của bài viết, tên người dùng, v.v..).
Args:
activity: Agent活动记录
activity: Bản ghi hoạt động của Agent
"""
# 跳过DO_NOTHING类型的活动
# Bỏ qua những hoạt động thuộc loại DO_NOTHING
if activity.action_type == "DO_NOTHING":
self._skipped_count += 1
return
self._activity_queue.put(activity)
self._total_activities += 1
logger.debug(f"添加活动到Zep队列: {activity.agent_name} - {activity.action_type}")
logger.debug(f"Action added to Zep queue: {activity.agent_name} - {activity.action_type}")
def add_activity_from_dict(self, data: Dict[str, Any], platform: str):
"""
从字典数据添加活动
Thêm hoạt động từ dữ liệu dictionary
Args:
data: 从actions.jsonl解析的字典数据
platform: 平台名称 (twitter/reddit)
data: Dữ liệu dictionary parse từ actions.jsonl
platform: Tên nền tảng (twitter/reddit)
"""
# 跳过事件类型的条目
# Bỏ qua các mục liên quan tới thuộc loại sự kiện (event_type)
if "event_type" in data:
return
@ -357,52 +357,52 @@ class ZepGraphMemoryUpdater:
self.add_activity(activity)
def _worker_loop(self):
"""后台工作循环 - 按平台批量发送活动到Zep"""
"""Vòng lặp làm việc chung (background) - Gửi hàng loạt các hoạt động lên Zep theo từng nền tảng"""
while self._running or not self._activity_queue.empty():
try:
# 尝试从队列获取活动超时1秒
# Thử lấy hoạt động từ hàng đợi (Timeout: 1 giây)
try:
activity = self._activity_queue.get(timeout=1)
# 将活动添加到对应平台的缓冲区
# Thêm hoạt động vào bộ đệm của nền tảng tương ứng
platform = activity.platform.lower()
with self._buffer_lock:
if platform not in self._platform_buffers:
self._platform_buffers[platform] = []
self._platform_buffers[platform].append(activity)
# 检查该平台是否达到批量大小
# Kiểm tra xem nền tảng đã đủ số lượng batch (gửi hàng loạt) chưa
if len(self._platform_buffers[platform]) >= self.BATCH_SIZE:
batch = self._platform_buffers[platform][:self.BATCH_SIZE]
self._platform_buffers[platform] = self._platform_buffers[platform][self.BATCH_SIZE:]
# 释放锁后再发送
# Gửi sau khi giải phóng lock
self._send_batch_activities(batch, platform)
# 发送间隔,避免请求过快
# Thời gian giãn giữa mỗi lần gửi để tránh request quá nhanh
time.sleep(self.SEND_INTERVAL)
except Empty:
pass
except Exception as e:
logger.error(f"工作循环异常: {e}")
logger.error(f"Worker loop error: {e}")
time.sleep(1)
def _send_batch_activities(self, activities: List[AgentActivity], platform: str):
"""
批量发送活动到Zep图谱合并为一条文本
Gửi hàng loạt các hoạt động lên đồ thị Zep (Gộp chung vào một đoạn text)
Args:
activities: Agent活动列表
platform: 平台名称
activities: Danh sách hoạt động của Agent
platform: Tên nền tảng
"""
if not activities:
return
# 将多条活动合并为一条文本,用换行分隔
# Gộp nhiều hoạt động vào một văn bản chung, tách nhau bởi xuống dòng
episode_texts = [activity.to_episode_text() for activity in activities]
combined_text = "\n".join(episode_texts)
# 带重试的发送
# Gửi với cơ chế thử lại
for attempt in range(self.MAX_RETRIES):
try:
self.client.graph.add(
@ -414,21 +414,21 @@ class ZepGraphMemoryUpdater:
self._total_sent += 1
self._total_items_sent += len(activities)
display_name = self._get_platform_display_name(platform)
logger.info(f"成功批量发送 {len(activities)}{display_name}活动到图谱 {self.graph_id}")
logger.debug(f"批量内容预览: {combined_text[:200]}...")
logger.info(f"Successfully sent batch of {len(activities)} {display_name} actions to graph {self.graph_id}")
logger.debug(f"Batch content preview: {combined_text[:200]}...")
return
except Exception as e:
if attempt < self.MAX_RETRIES - 1:
logger.warning(f"批量发送到Zep失败 (尝试 {attempt + 1}/{self.MAX_RETRIES}): {e}")
logger.warning(f"Failed to send batch to Zep (attempt {attempt + 1}/{self.MAX_RETRIES}): {e}")
time.sleep(self.RETRY_DELAY * (attempt + 1))
else:
logger.error(f"批量发送到Zep失败已重试{self.MAX_RETRIES}: {e}")
logger.error(f"Failed to send batch to Zep after {self.MAX_RETRIES} attempts: {e}")
self._failed_count += 1
def _flush_remaining(self):
"""发送队列和缓冲区中剩余的活动"""
# 首先处理队列中剩余的活动,添加到缓冲区
"""Gửi các hoạt động còn sót lại trong hàng đợi và bộ đệm"""
# Đầu tiên xử lý các hoạt động sót lại trong hàng đợi, đưa vào bộ đệm
while not self._activity_queue.empty():
try:
activity = self._activity_queue.get_nowait()
@ -440,41 +440,41 @@ class ZepGraphMemoryUpdater:
except Empty:
break
# 然后发送各平台缓冲区中剩余的活动即使不足BATCH_SIZE条
# Tiếp sau đó là gửi các hoạt động nằm trong bộ đệm của từng nền tảng đi (mặc dù chưa đạt đến số lượng BATCH_SIZE)
with self._buffer_lock:
for platform, buffer in self._platform_buffers.items():
if buffer:
display_name = self._get_platform_display_name(platform)
logger.info(f"发送{display_name}平台剩余的 {len(buffer)} 条活动")
logger.info(f"Sending remaining {len(buffer)} actions for platform {display_name}")
self._send_batch_activities(buffer, platform)
# 清空所有缓冲区
# Dọn sạch toàn bộ bộ đệm
for platform in self._platform_buffers:
self._platform_buffers[platform] = []
def get_stats(self) -> Dict[str, Any]:
"""获取统计信息"""
"""Lấy thông tin thống kê"""
with self._buffer_lock:
buffer_sizes = {p: len(b) for p, b in self._platform_buffers.items()}
return {
"graph_id": self.graph_id,
"batch_size": self.BATCH_SIZE,
"total_activities": self._total_activities, # 添加到队列的活动总数
"batches_sent": self._total_sent, # 成功发送的批次数
"items_sent": self._total_items_sent, # 成功发送的活动条数
"failed_count": self._failed_count, # 发送失败的批次数
"skipped_count": self._skipped_count, # 被过滤跳过的活动数DO_NOTHING
"total_activities": self._total_activities, # Tổng số hoạt động được thêm vào hàng đợi
"batches_sent": self._total_sent, # Số đợt hàng đã gửi thành công
"items_sent": self._total_items_sent, # Số lượng hoạt động đã gửi thành công
"failed_count": self._failed_count, # Số đợt hàng gửi thất bại
"skipped_count": self._skipped_count, # Số lượng hoạt động bị bỏ qua (DO_NOTHING)
"queue_size": self._activity_queue.qsize(),
"buffer_sizes": buffer_sizes, # 各平台缓冲区大小
"buffer_sizes": buffer_sizes, # Kích thước bộ đệm của từng nền tảng
"running": self._running,
}
class ZepGraphMemoryManager:
"""
管理多个模拟的Zep图谱记忆更新器
Quản các trình cập nhật bộ nhớ đồ thị Zep cho nhiều phỏng
每个模拟可以有自己的更新器实例
Mỗi phỏng thể instance trình cập nhật của riêng
"""
_updaters: Dict[str, ZepGraphMemoryUpdater] = {}
@ -483,17 +483,17 @@ class ZepGraphMemoryManager:
@classmethod
def create_updater(cls, simulation_id: str, graph_id: str) -> ZepGraphMemoryUpdater:
"""
为模拟创建图谱记忆更新器
Tạo trình cập nhật bộ nhớ đồ thị cho phỏng
Args:
simulation_id: 模拟ID
graph_id: Zep图谱ID
simulation_id: ID của phỏng
graph_id: ID của đồ thị Zep
Returns:
ZepGraphMemoryUpdater实例
Instance của ZepGraphMemoryUpdater
"""
with cls._lock:
# 如果已存在,先停止旧的
# Nếu đã tồn tại, dừng cái cũ lại trước
if simulation_id in cls._updaters:
cls._updaters[simulation_id].stop()
@ -501,30 +501,30 @@ class ZepGraphMemoryManager:
updater.start()
cls._updaters[simulation_id] = updater
logger.info(f"创建图谱记忆更新器: simulation_id={simulation_id}, graph_id={graph_id}")
logger.info(f"Created graph memory updater: simulation_id={simulation_id}, graph_id={graph_id}")
return updater
@classmethod
def get_updater(cls, simulation_id: str) -> Optional[ZepGraphMemoryUpdater]:
"""获取模拟的更新器"""
"""Lấy trình cập nhật của mô phỏng"""
return cls._updaters.get(simulation_id)
@classmethod
def stop_updater(cls, simulation_id: str):
"""停止并移除模拟的更新器"""
"""Dừng và gỡ bỏ trình cập nhật của mô phỏng"""
with cls._lock:
if simulation_id in cls._updaters:
cls._updaters[simulation_id].stop()
del cls._updaters[simulation_id]
logger.info(f"已停止图谱记忆更新器: simulation_id={simulation_id}")
logger.info(f"Graph memory updater stopped: simulation_id={simulation_id}")
# 防止 stop_all 重复调用的标志
# Cờ ngăn chặn gọi stop_all lặp lại
_stop_all_done = False
@classmethod
def stop_all(cls):
"""停止所有更新器"""
# 防止重复调用
"""Dừng toàn bộ các trình cập nhật"""
# Ngăn gọi lặp lại
if cls._stop_all_done:
return
cls._stop_all_done = True
@ -535,13 +535,13 @@ class ZepGraphMemoryManager:
try:
updater.stop()
except Exception as e:
logger.error(f"停止更新器失败: simulation_id={simulation_id}, error={e}")
logger.error(f"Failed to stop updater: simulation_id={simulation_id}, error={e}")
cls._updaters.clear()
logger.info("已停止所有图谱记忆更新器")
logger.info("All graph memory updaters stopped")
@classmethod
def get_all_stats(cls) -> Dict[str, Dict[str, Any]]:
"""获取所有更新器的统计信息"""
"""Lấy thông tin thống kê của toàn bộ các trình cập nhật"""
return {
sim_id: updater.get_stats()
for sim_id, updater in cls._updaters.items()

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
"""
日志配置模块
提供统一的日志管理同时输出到控制台和文件
-đun cấu hình logging
Cung cấp quản log thống nhất, đồng thời ghi ra console file
"""
import os
@ -12,47 +12,47 @@ from logging.handlers import RotatingFileHandler
def _ensure_utf8_stdout():
"""
确保 stdout/stderr 使用 UTF-8 编码
解决 Windows 控制台中文乱码问题
Đảm bảo stdout/stderr sử dụng hóa UTF-8
Khắc phục lỗi vỡ font tiếng Trung trên console Windows
"""
if sys.platform == 'win32':
# Windows 下重新配置标准输出为 UTF-8
# Trên Windows, cấu hình lại stdout/stderr sang UTF-8
if hasattr(sys.stdout, 'reconfigure'):
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
if hasattr(sys.stderr, 'reconfigure'):
sys.stderr.reconfigure(encoding='utf-8', errors='replace')
# 日志目录
# Thư mục log
LOG_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'logs')
def setup_logger(name: str = 'mirofish', level: int = logging.DEBUG) -> logging.Logger:
"""
设置日志器
Thiết lập logger
Args:
name: 日志器名称
level: 日志级别
name: Tên logger
level: Mức độ log
Returns:
配置好的日志器
Logger đã được cấu hình
"""
# 确保日志目录存在
# Đảm bảo thư mục log tồn tại
os.makedirs(LOG_DIR, exist_ok=True)
# 创建日志器
# Tạo logger
logger = logging.getLogger(name)
logger.setLevel(level)
# 阻止日志向上传播到根 logger避免重复输出
# Chặn log propagate lên root logger để tránh in trùng lặp
logger.propagate = False
# 如果已经有处理器,不重复添加
# Nếu đã có handler thì không thêm lại
if logger.handlers:
return logger
# 日志格式
# Định dạng log
detailed_formatter = logging.Formatter(
'[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d] %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
@ -63,7 +63,7 @@ def setup_logger(name: str = 'mirofish', level: int = logging.DEBUG) -> logging.
datefmt='%H:%M:%S'
)
# 1. 文件处理器 - 详细日志(按日期命名,带轮转)
# 1. File handler - log chi tiết (đặt tên theo ngày, có rolling)
log_filename = datetime.now().strftime('%Y-%m-%d') + '.log'
file_handler = RotatingFileHandler(
os.path.join(LOG_DIR, log_filename),
@ -74,14 +74,14 @@ def setup_logger(name: str = 'mirofish', level: int = logging.DEBUG) -> logging.
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(detailed_formatter)
# 2. 控制台处理器 - 简洁日志INFO及以上
# 确保 Windows 下使用 UTF-8 编码,避免中文乱码
# 2. Console handler - log gọn (INFO trở lên)
# Đảm bảo Windows dùng UTF-8 để tránh lỗi ký tự
_ensure_utf8_stdout()
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(logging.INFO)
console_handler.setFormatter(simple_formatter)
# 添加处理器
# Gắn handler
logger.addHandler(file_handler)
logger.addHandler(console_handler)
@ -90,13 +90,13 @@ def setup_logger(name: str = 'mirofish', level: int = logging.DEBUG) -> logging.
def get_logger(name: str = 'mirofish') -> logging.Logger:
"""
获取日志器如果不存在则创建
Lấy logger (nếu chưa sẽ tạo mới)
Args:
name: 日志器名称
name: Tên logger
Returns:
日志器实例
Instance logger
"""
logger = logging.getLogger(name)
if not logger.handlers:
@ -104,11 +104,11 @@ def get_logger(name: str = 'mirofish') -> logging.Logger:
return logger
# 创建默认日志器
# Tạo logger mặc định
logger = setup_logger()
# 便捷方法
# Hàm gọi nhanh
def debug(msg, *args, **kwargs):
logger.debug(msg, *args, **kwargs)

View File

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

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)
trả về đầy đủ danh sách cho bên gọi một cách trong suốt.
"""
from __future__ import annotations
@ -31,7 +32,7 @@ def _fetch_page_with_retry(
page_description: str = "page",
**kwargs: Any,
) -> list[Any]:
"""单页请求,失败时指数退避重试。仅重试网络/IO类瞬态错误。"""
"""Yêu cầu 1 trang, retry với exponential backoff khi thất bại. Chỉ retry lỗi tạm thời mạng/IO."""
if max_retries < 1:
raise ValueError("max_retries must be >= 1")
@ -64,7 +65,7 @@ def fetch_all_nodes(
max_retries: int = _DEFAULT_MAX_RETRIES,
retry_delay: float = _DEFAULT_RETRY_DELAY,
) -> list[Any]:
"""分页获取图谱节点,最多返回 max_items 条(默认 2000。每页请求自带重试。"""
"""Lấy node theo trang, tối đa max_items mục (mặc định 2000). Mỗi trang đều có retry."""
all_nodes: list[Any] = []
cursor: str | None = None
page_num = 0
@ -109,7 +110,7 @@ def fetch_all_edges(
max_retries: int = _DEFAULT_MAX_RETRIES,
retry_delay: float = _DEFAULT_RETRY_DELAY,
) -> list[Any]:
"""分页获取图谱所有边,返回完整列表。每页请求自带重试。"""
"""Lấy toàn bộ edge theo trang, trả về đầy đủ danh sách. Mỗi trang đều có retry."""
all_edges: list[Any] = []
cursor: str | None = None
page_num = 0

View File

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

View File

@ -5,31 +5,31 @@
# Install: pip install -r requirements.txt
# ===========================================
# ============= 核心框架 =============
# ============= Khung lõi =============
flask>=3.0.0
flask-cors>=6.0.0
# ============= LLM 相关 =============
# OpenAI SDK(统一使用 OpenAI 格式调用 LLM
# ============= Liên quan đến LLM =============
# OpenAI SDK (thống nhất dùng định dạng OpenAI để gọi LLM)
openai>=1.0.0
# ============= Zep Cloud =============
zep-cloud==3.13.0
# ============= OASIS 社交媒体模拟 =============
# OASIS 社交模拟框架
# ============= Mô phỏng mạng xã hội OASIS =============
# Khung mô phỏng mạng xã hội OASIS
camel-oasis==0.2.5
camel-ai==0.2.78
# ============= 文件处理 =============
# ============= Xử lý tệp =============
PyMuPDF>=1.24.0
# 编码检测支持非UTF-8编码的文本文件
# Phát hiện mã hóa (hỗ trợ tệp văn bản không dùng UTF-8)
charset-normalizer>=3.0.0
chardet>=5.0.0
# ============= 工具库 =============
# 环境变量加载
# ============= Thư viện tiện ích =============
# Nạp biến môi trường
python-dotenv>=1.0.0
# 数据验证
# Xác thực dữ liệu
pydantic>=2.0.0

View File

@ -1,21 +1,21 @@
"""
MiroFish Backend 启动入口
MiroFish Backend entrypoint
"""
import os
import sys
# 解决 Windows 控制台中文乱码问题:在所有导入之前设置 UTF-8 编码
# Khắc phục lỗi hiển thị tiếng Trung trên console Windows: đặt UTF-8 trước mọi import
if sys.platform == 'win32':
# 设置环境变量确保 Python 使用 UTF-8
# Đặt biến môi trường để đảm bảo Python dùng UTF-8
os.environ.setdefault('PYTHONIOENCODING', 'utf-8')
# 重新配置标准输出流为 UTF-8
# Cấu hình lại stdout/stderr sang UTF-8
if hasattr(sys.stdout, 'reconfigure'):
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
if hasattr(sys.stderr, 'reconfigure'):
sys.stderr.reconfigure(encoding='utf-8', errors='replace')
# 添加项目根目录到路径
# Thêm thư mục gốc của project vào path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from app import create_app
@ -23,25 +23,25 @@ from app.config import Config
def main():
"""主函数"""
# 验证配置
"""Hàm chính"""
# Kiểm tra cấu hình
errors = Config.validate()
if errors:
print("配置错误:")
print("Configuration errors:")
for err in errors:
print(f" - {err}")
print("\n请检查 .env 文件中的配置")
print("\nPlease check configuration in the .env file")
sys.exit(1)
# 创建应用
# Tạo ứng dụng
app = create_app()
# 获取运行配置
# Lấy cấu hình chạy
host = os.environ.get('FLASK_HOST', '0.0.0.0')
port = int(os.environ.get('FLASK_PORT', 5001))
debug = Config.DEBUG
# 启动服务
# Khởi động dịch vụ
app.run(host=host, port=port, debug=debug, threaded=True)

View File

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

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

View File

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

View File

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