translate backend folder
This commit is contained in:
parent
950e8a80cb
commit
1b986f730d
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
"""
|
||||
API路由模块
|
||||
Mô-đun tuyến API
|
||||
"""
|
||||
|
||||
from flask import Blueprint
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"""
|
||||
图谱相关API路由
|
||||
采用项目上下文机制,服务端持久化状态
|
||||
API routes liên quan đến Graph
|
||||
Sử dụng cơ chế project context, trạng thái được persist phía server
|
||||
"""
|
||||
|
||||
import os
|
||||
|
|
@ -18,12 +18,12 @@ from ..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 kê tất cả project
|
||||
"""
|
||||
limit = request.args.get('limit', 50, type=int)
|
||||
projects = ProjectManager.list_projects(limit=limit)
|
||||
|
|
@ -69,36 +69,36 @@ def list_projects():
|
|||
@graph_bp.route('/project/<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 và generate định nghĩa ontology
|
||||
|
||||
请求方式:multipart/form-data
|
||||
Request type: multipart/form-data
|
||||
|
||||
参数:
|
||||
files: 上传的文件(PDF/MD/TXT),可多个
|
||||
simulation_requirement: 模拟需求描述(必填)
|
||||
project_name: 项目名称(可选)
|
||||
additional_context: 额外说明(可选)
|
||||
Parameters:
|
||||
files: File upload (PDF/MD/TXT), có thể nhiều file
|
||||
simulation_requirement: Mô tả yêu cầu simulation (bắt buộc)
|
||||
project_name: Tên project (tùy chọn)
|
||||
additional_context: Thông tin bổ sung (tùy chọn)
|
||||
|
||||
返回:
|
||||
Response:
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
|
|
@ -147,42 +147,42 @@ def generate_ontology():
|
|||
}
|
||||
"""
|
||||
try:
|
||||
logger.info("=== 开始生成本体定义 ===")
|
||||
logger.info("=== Start generating ontology definition ===")
|
||||
|
||||
# 获取参数
|
||||
# Get parameters
|
||||
simulation_requirement = request.form.get('simulation_requirement', '')
|
||||
project_name = request.form.get('project_name', 'Unnamed Project')
|
||||
additional_context = request.form.get('additional_context', '')
|
||||
|
||||
logger.debug(f"项目名称: {project_name}")
|
||||
logger.debug(f"模拟需求: {simulation_requirement[:100]}...")
|
||||
logger.debug(f"Project name: {project_name}")
|
||||
logger.debug(f"Simulation requirement: {simulation_requirement[:100]}...")
|
||||
|
||||
if not simulation_requirement:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "请提供模拟需求描述 (simulation_requirement)"
|
||||
"error": "Please provide simulation requirement description (simulation_requirement)"
|
||||
}), 400
|
||||
|
||||
# 获取上传的文件
|
||||
# Get uploaded files
|
||||
uploaded_files = request.files.getlist('files')
|
||||
if not uploaded_files or all(not f.filename for f in uploaded_files):
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "请至少上传一个文档文件"
|
||||
"error": "Please upload at least one document file"
|
||||
}), 400
|
||||
|
||||
# 创建项目
|
||||
# Create project
|
||||
project = ProjectManager.create_project(name=project_name)
|
||||
project.simulation_requirement = simulation_requirement
|
||||
logger.info(f"创建项目: {project.project_id}")
|
||||
logger.info(f"Project created: {project.project_id}")
|
||||
|
||||
# 保存文件并提取文本
|
||||
# Save and extract text
|
||||
document_texts = []
|
||||
all_text = ""
|
||||
|
||||
for file in uploaded_files:
|
||||
if file and file.filename and allowed_file(file.filename):
|
||||
# 保存文件到项目目录
|
||||
# Save file into project directory
|
||||
file_info = ProjectManager.save_file_to_project(
|
||||
project.project_id,
|
||||
file,
|
||||
|
|
@ -193,7 +193,7 @@ def generate_ontology():
|
|||
"size": file_info["size"]
|
||||
})
|
||||
|
||||
# 提取文本
|
||||
# Extract text
|
||||
text = FileParser.extract_text(file_info["path"])
|
||||
text = TextProcessor.preprocess_text(text)
|
||||
document_texts.append(text)
|
||||
|
|
@ -203,16 +203,16 @@ def generate_ontology():
|
|||
ProjectManager.delete_project(project.project_id)
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "没有成功处理任何文档,请检查文件格式"
|
||||
"error": "No documents were successfully processed. Please check the file formats."
|
||||
}), 400
|
||||
|
||||
# 保存提取的文本
|
||||
# Save extracted text to project
|
||||
project.total_text_length = len(all_text)
|
||||
ProjectManager.save_extracted_text(project.project_id, all_text)
|
||||
logger.info(f"文本提取完成,共 {len(all_text)} 字符")
|
||||
logger.info(f"Text extraction completed, total {len(all_text)} characters")
|
||||
|
||||
# 生成本体
|
||||
logger.info("调用 LLM 生成本体定义...")
|
||||
# Generate ontology definition using LLM
|
||||
logger.info("Calling LLM to generate ontology definition...")
|
||||
generator = OntologyGenerator()
|
||||
ontology = generator.generate(
|
||||
document_texts=document_texts,
|
||||
|
|
@ -220,10 +220,10 @@ def generate_ontology():
|
|||
additional_context=additional_context if additional_context else None
|
||||
)
|
||||
|
||||
# 保存本体到项目
|
||||
# Save ontology to project
|
||||
entity_count = len(ontology.get("entity_types", []))
|
||||
edge_count = len(ontology.get("edge_types", []))
|
||||
logger.info(f"本体生成完成: {entity_count} 个实体类型, {edge_count} 个关系类型")
|
||||
logger.info(f"Ontology generation completed: {entity_count} entity types, {edge_count} edge types")
|
||||
|
||||
project.ontology = {
|
||||
"entity_types": ontology.get("entity_types", []),
|
||||
|
|
@ -232,7 +232,7 @@ def generate_ontology():
|
|||
project.analysis_summary = ontology.get("analysis_summary", "")
|
||||
project.status = ProjectStatus.ONTOLOGY_GENERATED
|
||||
ProjectManager.save_project(project)
|
||||
logger.info(f"=== 本体生成完成 === 项目ID: {project.project_id}")
|
||||
logger.info(f"=== Ontology generation completed === Project ID: {project.project_id}")
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
|
|
@ -254,140 +254,140 @@ def generate_ontology():
|
|||
}), 500
|
||||
|
||||
|
||||
# ============== 接口2:构建图谱 ==============
|
||||
# ============== API 2: Build graph ==============
|
||||
|
||||
@graph_bp.route('/build', methods=['POST'])
|
||||
def build_graph():
|
||||
"""
|
||||
接口2:根据project_id构建图谱
|
||||
API 2: Build graph dựa trên project_id
|
||||
|
||||
请求(JSON):
|
||||
Request (JSON):
|
||||
{
|
||||
"project_id": "proj_xxxx", // 必填,来自接口1
|
||||
"graph_name": "图谱名称", // 可选
|
||||
"chunk_size": 500, // 可选,默认500
|
||||
"chunk_overlap": 50 // 可选,默认50
|
||||
"project_id": "proj_xxxx", // bắt buộc, từ API 1
|
||||
"graph_name": "Graph name", // tùy chọn
|
||||
"chunk_size": 500, // tùy chọn, mặc định 500
|
||||
"chunk_overlap": 50 // tùy chọn, mặc định 50
|
||||
}
|
||||
|
||||
返回:
|
||||
Response:
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"project_id": "proj_xxxx",
|
||||
"task_id": "task_xxxx",
|
||||
"message": "图谱构建任务已启动"
|
||||
"message": "Graph build task started"
|
||||
}
|
||||
}
|
||||
"""
|
||||
try:
|
||||
logger.info("=== 开始构建图谱 ===")
|
||||
logger.info("=== Start building graph ===")
|
||||
|
||||
# 检查配置
|
||||
# Kiểm tra cấu hình
|
||||
errors = []
|
||||
if not Config.ZEP_API_KEY:
|
||||
errors.append("ZEP_API_KEY未配置")
|
||||
errors.append("ZEP_API_KEY not configured")
|
||||
if errors:
|
||||
logger.error(f"配置错误: {errors}")
|
||||
logger.error(f"Configuration error: {errors}")
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "配置错误: " + "; ".join(errors)
|
||||
"error": "Configuration error: " + "; ".join(errors)
|
||||
}), 500
|
||||
|
||||
# 解析请求
|
||||
# Parse request
|
||||
data = request.get_json() or {}
|
||||
project_id = data.get('project_id')
|
||||
logger.debug(f"请求参数: project_id={project_id}")
|
||||
logger.debug(f"Request parameters: project_id={project_id}")
|
||||
|
||||
if not project_id:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "请提供 project_id"
|
||||
"error": "Please provide project_id"
|
||||
}), 400
|
||||
|
||||
# 获取项目
|
||||
# Lấy project
|
||||
project = ProjectManager.get_project(project_id)
|
||||
if not project:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": f"项目不存在: {project_id}"
|
||||
"error": f"Project does not exist: {project_id}"
|
||||
}), 404
|
||||
|
||||
# 检查项目状态
|
||||
force = data.get('force', False) # 强制重新构建
|
||||
# Kiểm tra trạng thái project
|
||||
force = data.get('force', False) # force rebuild
|
||||
|
||||
if project.status == ProjectStatus.CREATED:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "项目尚未生成本体,请先调用 /ontology/generate"
|
||||
"error": "Ontology has not been generated. Please call /ontology/generate first"
|
||||
}), 400
|
||||
|
||||
if project.status == ProjectStatus.GRAPH_BUILDING and not force:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "图谱正在构建中,请勿重复提交。如需强制重建,请添加 force: true",
|
||||
"error": "Graph is currently building. Do not submit again. Use force: true to rebuild",
|
||||
"task_id": project.graph_build_task_id
|
||||
}), 400
|
||||
|
||||
# 如果强制重建,重置状态
|
||||
# Nếu force rebuild thì reset trạng thái
|
||||
if force and project.status in [ProjectStatus.GRAPH_BUILDING, ProjectStatus.FAILED, ProjectStatus.GRAPH_COMPLETED]:
|
||||
project.status = ProjectStatus.ONTOLOGY_GENERATED
|
||||
project.graph_id = None
|
||||
project.graph_build_task_id = None
|
||||
project.error = None
|
||||
|
||||
# 获取配置
|
||||
# Lấy cấu hình
|
||||
graph_name = data.get('graph_name', project.name or 'MiroFish Graph')
|
||||
chunk_size = data.get('chunk_size', project.chunk_size or Config.DEFAULT_CHUNK_SIZE)
|
||||
chunk_overlap = data.get('chunk_overlap', project.chunk_overlap or Config.DEFAULT_CHUNK_OVERLAP)
|
||||
|
||||
# 更新项目配置
|
||||
# Cập nhật cấu hình project
|
||||
project.chunk_size = chunk_size
|
||||
project.chunk_overlap = chunk_overlap
|
||||
|
||||
# 获取提取的文本
|
||||
# Lấy text đã extract
|
||||
text = ProjectManager.get_extracted_text(project_id)
|
||||
if not text:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "未找到提取的文本内容"
|
||||
"error": "Extracted text not found"
|
||||
}), 400
|
||||
|
||||
# 获取本体
|
||||
# Lấy ontology
|
||||
ontology = project.ontology
|
||||
if not ontology:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "未找到本体定义"
|
||||
"error": "Ontology definition not found"
|
||||
}), 400
|
||||
|
||||
# 创建异步任务
|
||||
# Tạo async task
|
||||
task_manager = TaskManager()
|
||||
task_id = task_manager.create_task(f"构建图谱: {graph_name}")
|
||||
logger.info(f"创建图谱构建任务: task_id={task_id}, project_id={project_id}")
|
||||
task_id = task_manager.create_task(f"Build graph: {graph_name}")
|
||||
logger.info(f"Graph build task created: task_id={task_id}, project_id={project_id}")
|
||||
|
||||
# 更新项目状态
|
||||
# Cập nhật trạng thái project
|
||||
project.status = ProjectStatus.GRAPH_BUILDING
|
||||
project.graph_build_task_id = task_id
|
||||
ProjectManager.save_project(project)
|
||||
|
||||
# 启动后台任务
|
||||
# Khởi động background task
|
||||
def build_task():
|
||||
build_logger = get_logger('mirofish.build')
|
||||
try:
|
||||
build_logger.info(f"[{task_id}] 开始构建图谱...")
|
||||
build_logger.info(f"[{task_id}] Start building graph...")
|
||||
task_manager.update_task(
|
||||
task_id,
|
||||
status=TaskStatus.PROCESSING,
|
||||
message="初始化图谱构建服务..."
|
||||
message="Initializing graph builder service..."
|
||||
)
|
||||
|
||||
# 创建图谱构建服务
|
||||
# Tạo graph builder service
|
||||
builder = GraphBuilderService(api_key=Config.ZEP_API_KEY)
|
||||
|
||||
# 分块
|
||||
# Chunk text
|
||||
task_manager.update_task(
|
||||
task_id,
|
||||
message="文本分块中...",
|
||||
message="Splitting text into chunks...",
|
||||
progress=5
|
||||
)
|
||||
chunks = TextProcessor.split_text(
|
||||
|
|
@ -397,29 +397,29 @@ def build_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 kê 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 và edges)
|
||||
"""
|
||||
try:
|
||||
if not Config.ZEP_API_KEY:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "ZEP_API_KEY未配置"
|
||||
"error": "ZEP_API_KEY not configured"
|
||||
}), 500
|
||||
|
||||
builder = GraphBuilderService(api_key=Config.ZEP_API_KEY)
|
||||
|
|
@ -592,13 +592,13 @@ def get_graph_data(graph_id: str):
|
|||
@graph_bp.route('/delete/<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:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"""
|
||||
Report API路由
|
||||
提供模拟报告生成、获取、对话等接口
|
||||
Report API routes
|
||||
Cung cấp các API cho việc generate báo cáo simulation, lấy báo cáo, và chat
|
||||
"""
|
||||
|
||||
import os
|
||||
|
|
@ -19,30 +19,30 @@ 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 là thao tác tốn thời gian, API sẽ trả về task_id ngay lập tức,
|
||||
dùng GET /api/report/generate/status để kiểm tra progress
|
||||
|
||||
请求(JSON):
|
||||
Request (JSON):
|
||||
{
|
||||
"simulation_id": "sim_xxxx", // 必填,模拟ID
|
||||
"force_regenerate": false // 可选,强制重新生成
|
||||
"simulation_id": "sim_xxxx", // bắt buộc
|
||||
"force_regenerate": false // optional, force regenerate
|
||||
}
|
||||
|
||||
返回:
|
||||
Response:
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"simulation_id": "sim_xxxx",
|
||||
"task_id": "task_xxxx",
|
||||
"status": "generating",
|
||||
"message": "报告生成任务已启动"
|
||||
"message": "Report generation task started"
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
|
@ -53,22 +53,22 @@ def generate_report():
|
|||
if not simulation_id:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "请提供 simulation_id"
|
||||
"error": "Please provide simulation_id"
|
||||
}), 400
|
||||
|
||||
force_regenerate = data.get('force_regenerate', False)
|
||||
|
||||
# 获取模拟信息
|
||||
# Lấy thông tin simulation
|
||||
manager = SimulationManager()
|
||||
state = manager.get_simulation(simulation_id)
|
||||
|
||||
if not state:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": f"模拟不存在: {simulation_id}"
|
||||
"error": f"Simulation does not exist: {simulation_id}"
|
||||
}), 404
|
||||
|
||||
# 检查是否已有报告
|
||||
# Kiểm tra đã có report chưa
|
||||
if not force_regenerate:
|
||||
existing_report = ReportManager.get_report_by_simulation(simulation_id)
|
||||
if existing_report and existing_report.status == ReportStatus.COMPLETED:
|
||||
|
|
@ -78,38 +78,38 @@ def generate_report():
|
|||
"simulation_id": simulation_id,
|
||||
"report_id": existing_report.report_id,
|
||||
"status": "completed",
|
||||
"message": "报告已存在",
|
||||
"message": "Report already exists",
|
||||
"already_generated": True
|
||||
}
|
||||
})
|
||||
|
||||
# 获取项目信息
|
||||
# Lấy thông tin project
|
||||
project = ProjectManager.get_project(state.project_id)
|
||||
if not project:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": f"项目不存在: {state.project_id}"
|
||||
"error": f"Project does not exist: {state.project_id}"
|
||||
}), 404
|
||||
|
||||
graph_id = state.graph_id or project.graph_id
|
||||
if not graph_id:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "缺少图谱ID,请确保已构建图谱"
|
||||
"error": "Missing graph_id, please ensure the graph has been built"
|
||||
}), 400
|
||||
|
||||
simulation_requirement = project.simulation_requirement
|
||||
if not simulation_requirement:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "缺少模拟需求描述"
|
||||
"error": "Missing simulation requirement description"
|
||||
}), 400
|
||||
|
||||
# 提前生成 report_id,以便立即返回给前端
|
||||
# Tạo report_id trước để có thể trả về ngay cho frontend
|
||||
import uuid
|
||||
report_id = f"report_{uuid.uuid4().hex[:12]}"
|
||||
|
||||
# 创建异步任务
|
||||
# Tạo async task
|
||||
task_manager = TaskManager()
|
||||
task_id = task_manager.create_task(
|
||||
task_type="report_generate",
|
||||
|
|
@ -120,24 +120,24 @@ def generate_report():
|
|||
}
|
||||
)
|
||||
|
||||
# 定义后台任务
|
||||
# Định nghĩa background task
|
||||
def run_generate():
|
||||
try:
|
||||
task_manager.update_task(
|
||||
task_id,
|
||||
status=TaskStatus.PROCESSING,
|
||||
progress=0,
|
||||
message="初始化Report Agent..."
|
||||
message="Initializing Report Agent..."
|
||||
)
|
||||
|
||||
# 创建Report Agent
|
||||
# Tạo Report Agent
|
||||
agent = ReportAgent(
|
||||
graph_id=graph_id,
|
||||
simulation_id=simulation_id,
|
||||
simulation_requirement=simulation_requirement
|
||||
)
|
||||
|
||||
# 进度回调
|
||||
# Callback cập nhật progress
|
||||
def progress_callback(stage, progress, message):
|
||||
task_manager.update_task(
|
||||
task_id,
|
||||
|
|
@ -145,13 +145,13 @@ def 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 kê tất cả reports
|
||||
|
||||
Query参数:
|
||||
simulation_id: 按模拟ID过滤(可选)
|
||||
limit: 返回数量限制(默认50)
|
||||
Query parameters:
|
||||
simulation_id: Filter theo simulation_id (optional)
|
||||
limit: Giới hạn số lượng trả về (default 50)
|
||||
|
||||
返回:
|
||||
Response:
|
||||
{
|
||||
"success": true,
|
||||
"data": [...],
|
||||
|
|
@ -382,7 +382,7 @@ def list_reports():
|
|||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"列出报告失败: {str(e)}")
|
||||
logger.error(f"Failed to list reports: {str(e)}")
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
|
|
@ -393,9 +393,9 @@ def list_reports():
|
|||
@report_bp.route('/<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 có thể tự động gọi các retrieval tool trong quá trình trả lời
|
||||
|
||||
请求(JSON):
|
||||
Request (JSON):
|
||||
{
|
||||
"simulation_id": "sim_xxxx", // 必填,模拟ID
|
||||
"message": "请解释一下舆情走向", // 必填,用户消息
|
||||
"chat_history": [ // 可选,对话历史
|
||||
"simulation_id": "sim_xxxx", // bắt buộc
|
||||
"message": "Please explain the public opinion trend", // bắt buộc
|
||||
"chat_history": [ // optional
|
||||
{"role": "user", "content": "..."},
|
||||
{"role": "assistant", "content": "..."}
|
||||
]
|
||||
}
|
||||
|
||||
返回:
|
||||
Response:
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"response": "Agent回复...",
|
||||
"tool_calls": [调用的工具列表],
|
||||
"sources": [信息来源]
|
||||
"response": "Agent reply...",
|
||||
"tool_calls": [list of called tools],
|
||||
"sources": [information sources]
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
|
@ -501,42 +501,42 @@ def chat_with_report_agent():
|
|||
if not simulation_id:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "请提供 simulation_id"
|
||||
"error": "Please provide simulation_id"
|
||||
}), 400
|
||||
|
||||
if not message:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "请提供 message"
|
||||
"error": "Please provide message"
|
||||
}), 400
|
||||
|
||||
# 获取模拟和项目信息
|
||||
# Lấy thông tin simulation và project
|
||||
manager = SimulationManager()
|
||||
state = manager.get_simulation(simulation_id)
|
||||
|
||||
if not state:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": f"模拟不存在: {simulation_id}"
|
||||
"error": f"Simulation does not exist: {simulation_id}"
|
||||
}), 404
|
||||
|
||||
project = ProjectManager.get_project(state.project_id)
|
||||
if not project:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": f"项目不存在: {state.project_id}"
|
||||
"error": f"Project does not exist: {state.project_id}"
|
||||
}), 404
|
||||
|
||||
graph_id = state.graph_id or project.graph_id
|
||||
if not graph_id:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "缺少图谱ID"
|
||||
"error": "Missing graph_id"
|
||||
}), 400
|
||||
|
||||
simulation_requirement = project.simulation_requirement or ""
|
||||
|
||||
# 创建Agent并进行对话
|
||||
# Tạo Agent và chat
|
||||
agent = ReportAgent(
|
||||
graph_id=graph_id,
|
||||
simulation_id=simulation_id,
|
||||
|
|
@ -551,7 +551,7 @@ def chat_with_report_agent():
|
|||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"对话失败: {str(e)}")
|
||||
logger.error(f"Chat failed: {str(e)}")
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
|
|
@ -559,22 +559,22 @@ def chat_with_report_agent():
|
|||
}), 500
|
||||
|
||||
|
||||
# ============== 报告进度与分章节接口 ==============
|
||||
# ============== Report progress & sections APIs ==============
|
||||
|
||||
@report_bp.route('/<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 có thể poll API này để lấy section đã generate
|
||||
mà không cần chờ toàn bộ report hoàn thành
|
||||
|
||||
返回:
|
||||
Response:
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
|
|
@ -618,9 +619,8 @@ def get_report_sections(report_id: str):
|
|||
{
|
||||
"filename": "section_01.md",
|
||||
"section_index": 1,
|
||||
"content": "## 执行摘要\\n\\n..."
|
||||
},
|
||||
...
|
||||
"content": "## Executive Summary\\n\\n..."
|
||||
}
|
||||
],
|
||||
"total_sections": 3,
|
||||
"is_complete": false
|
||||
|
|
@ -630,7 +630,7 @@ def get_report_sections(report_id: str):
|
|||
try:
|
||||
sections = ReportManager.get_generated_sections(report_id)
|
||||
|
||||
# 获取报告状态
|
||||
# Lấy trạng thái report
|
||||
report = ReportManager.get_report(report_id)
|
||||
is_complete = report is not None and report.status == ReportStatus.COMPLETED
|
||||
|
||||
|
|
@ -645,7 +645,7 @@ def get_report_sections(report_id: str):
|
|||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取章节列表失败: {str(e)}")
|
||||
logger.error(f"Failed to retrieve section list: {str(e)}")
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
|
|
@ -656,14 +656,14 @@ def get_report_sections(report_id: str):
|
|||
@report_bp.route('/<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 ký thực thi chi tiết của Report Agent
|
||||
|
||||
实时获取报告生成过程中的每一步动作,包括:
|
||||
- 报告开始、规划开始/完成
|
||||
- 每个章节的开始、工具调用、LLM响应、完成
|
||||
- 报告完成或失败
|
||||
Lấy theo thời gian thực từng bước trong quá trình tạo báo cáo, bao gồm:
|
||||
- Bắt đầu báo cáo, bắt đầu / hoàn thành planning
|
||||
- Bắt đầu từng section, tool call, LLM response, hoàn thành
|
||||
- Báo cáo hoàn thành hoặc thất bại
|
||||
|
||||
Query参数:
|
||||
from_line: 从第几行开始读取(可选,默认0,用于增量获取)
|
||||
Query parameters:
|
||||
from_line: đọc từ dòng nào (optional, mặc định 0, dùng cho incremental fetch)
|
||||
|
||||
返回:
|
||||
Response:
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
|
|
@ -774,7 +774,7 @@ def get_agent_log(report_id: str):
|
|||
"report_id": "report_xxxx",
|
||||
"action": "tool_call",
|
||||
"stage": "generating",
|
||||
"section_title": "执行摘要",
|
||||
"section_title": "Executive Summary",
|
||||
"section_index": 1,
|
||||
"details": {
|
||||
"tool_name": "insight_forge",
|
||||
|
|
@ -801,7 +801,7 @@ def get_agent_log(report_id: str):
|
|||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取Agent日志失败: {str(e)}")
|
||||
logger.error(f"Failed to get Agent log: {str(e)}")
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
|
|
@ -812,9 +812,9 @@ def get_agent_log(report_id: str):
|
|||
@report_bp.route('/<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
|
||||
|
||||
实时获取报告生成过程中的控制台输出(INFO、WARNING等),
|
||||
这与 agent-log 接口返回的结构化 JSON 日志不同,
|
||||
是纯文本格式的控制台风格日志。
|
||||
Lấy theo thời gian thực các console output trong quá trình tạo báo cáo
|
||||
(INFO, WARNING, ...).
|
||||
|
||||
Query参数:
|
||||
from_line: 从第几行开始读取(可选,默认0,用于增量获取)
|
||||
Khác với API agent-log trả về structured JSON logs,
|
||||
API này trả về console-style log dạng text thuần.
|
||||
|
||||
返回:
|
||||
Query parameters:
|
||||
from_line: đọc từ dòng nào (optional, mặc định 0, dùng cho incremental fetch)
|
||||
|
||||
Response:
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"logs": [
|
||||
"[19:46:14] INFO: 搜索完成: 找到 15 条相关事实",
|
||||
"[19:46:14] INFO: 图谱搜索: graph_id=xxx, query=...",
|
||||
"[19:46:14] INFO: Search completed: found 15 relevant facts",
|
||||
"[19:46:14] INFO: Graph search: graph_id=xxx, query=...",
|
||||
...
|
||||
],
|
||||
"total_lines": 100,
|
||||
|
|
@ -883,7 +885,7 @@ def get_console_log(report_id: str):
|
|||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取控制台日志失败: {str(e)}")
|
||||
logger.error(f"Failed to get console log: {str(e)}")
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
|
|
@ -894,9 +896,9 @@ def get_console_log(report_id: str):
|
|||
@report_bp.route('/<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 kê graph (dùng cho debugging)
|
||||
|
||||
请求(JSON):
|
||||
Request (JSON):
|
||||
{
|
||||
"graph_id": "mirofish_xxxx"
|
||||
}
|
||||
|
|
@ -993,7 +995,7 @@ def get_graph_statistics_tool():
|
|||
if not graph_id:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "请提供 graph_id"
|
||||
"error": "Please provide graph_id"
|
||||
}), 400
|
||||
|
||||
from ..services.zep_tools import ZepToolsService
|
||||
|
|
@ -1007,7 +1009,7 @@ def get_graph_statistics_tool():
|
|||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取图谱统计失败: {str(e)}")
|
||||
logger.error(f"Failed to get graph statistics: {str(e)}")
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,54 +1,54 @@
|
|||
"""
|
||||
配置管理
|
||||
统一从项目根目录的 .env 文件加载配置
|
||||
Quản lý cấu hình
|
||||
Tải cấu hình thống nhất từ tệp .env ở thư mục gốc dự án
|
||||
"""
|
||||
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# 加载项目根目录的 .env 文件
|
||||
# 路径: MiroFish/.env (相对于 backend/app/config.py)
|
||||
# Tải tệp .env ở thư mục gốc dự án
|
||||
# Đường dẫn: MiroFish/.env (tương đối với backend/app/config.py)
|
||||
project_root_env = os.path.join(os.path.dirname(__file__), '../../.env')
|
||||
|
||||
if os.path.exists(project_root_env):
|
||||
load_dotenv(project_root_env, override=True)
|
||||
else:
|
||||
# 如果根目录没有 .env,尝试加载环境变量(用于生产环境)
|
||||
# Nếu thư mục gốc không có .env, thử nạp biến môi trường sẵn có (cho production)
|
||||
load_dotenv(override=True)
|
||||
|
||||
|
||||
class Config:
|
||||
"""Flask配置类"""
|
||||
"""Lớp cấu hình cho Flask"""
|
||||
|
||||
# Flask配置
|
||||
# Cấu hình Flask
|
||||
SECRET_KEY = os.environ.get('SECRET_KEY', 'mirofish-secret-key')
|
||||
DEBUG = os.environ.get('FLASK_DEBUG', 'True').lower() == 'true'
|
||||
|
||||
# JSON配置 - 禁用ASCII转义,让中文直接显示(而不是 \uXXXX 格式)
|
||||
# Cấu hình JSON - tắt escape ASCII để tiếng Trung hiển thị trực tiếp (thay vì dạng \uXXXX)
|
||||
JSON_AS_ASCII = False
|
||||
|
||||
# LLM配置(统一使用OpenAI格式)
|
||||
# Cấu hình LLM (thống nhất theo định dạng OpenAI)
|
||||
LLM_API_KEY = os.environ.get('LLM_API_KEY')
|
||||
LLM_BASE_URL = os.environ.get('LLM_BASE_URL', 'https://api.openai.com/v1')
|
||||
LLM_MODEL_NAME = os.environ.get('LLM_MODEL_NAME', 'gpt-4o-mini')
|
||||
|
||||
# Zep配置
|
||||
# Cấu hình Zep
|
||||
ZEP_API_KEY = os.environ.get('ZEP_API_KEY')
|
||||
|
||||
# 文件上传配置
|
||||
# Cấu hình upload tệp
|
||||
MAX_CONTENT_LENGTH = 50 * 1024 * 1024 # 50MB
|
||||
UPLOAD_FOLDER = os.path.join(os.path.dirname(__file__), '../uploads')
|
||||
ALLOWED_EXTENSIONS = {'pdf', 'md', 'txt', 'markdown'}
|
||||
|
||||
# 文本处理配置
|
||||
DEFAULT_CHUNK_SIZE = 500 # 默认切块大小
|
||||
DEFAULT_CHUNK_OVERLAP = 50 # 默认重叠大小
|
||||
# Cấu hình xử lý văn bản
|
||||
DEFAULT_CHUNK_SIZE = 500 # Kích thước chunk mặc định
|
||||
DEFAULT_CHUNK_OVERLAP = 50 # Độ chồng lấp mặc định
|
||||
|
||||
# OASIS模拟配置
|
||||
# Cấu hình mô phỏng OASIS
|
||||
OASIS_DEFAULT_MAX_ROUNDS = int(os.environ.get('OASIS_DEFAULT_MAX_ROUNDS', '10'))
|
||||
OASIS_SIMULATION_DATA_DIR = os.path.join(os.path.dirname(__file__), '../uploads/simulations')
|
||||
|
||||
# OASIS平台可用动作配置
|
||||
# Cấu hình action khả dụng theo nền tảng OASIS
|
||||
OASIS_TWITTER_ACTIONS = [
|
||||
'CREATE_POST', 'LIKE_POST', 'REPOST', 'FOLLOW', 'DO_NOTHING', 'QUOTE_POST'
|
||||
]
|
||||
|
|
@ -58,18 +58,18 @@ class Config:
|
|||
'TREND', 'REFRESH', 'DO_NOTHING', 'FOLLOW', 'MUTE'
|
||||
]
|
||||
|
||||
# Report Agent配置
|
||||
# Cấu hình Report Agent
|
||||
REPORT_AGENT_MAX_TOOL_CALLS = int(os.environ.get('REPORT_AGENT_MAX_TOOL_CALLS', '5'))
|
||||
REPORT_AGENT_MAX_REFLECTION_ROUNDS = int(os.environ.get('REPORT_AGENT_MAX_REFLECTION_ROUNDS', '2'))
|
||||
REPORT_AGENT_TEMPERATURE = float(os.environ.get('REPORT_AGENT_TEMPERATURE', '0.5'))
|
||||
|
||||
@classmethod
|
||||
def validate(cls):
|
||||
"""验证必要配置"""
|
||||
"""Kiểm tra các cấu hình bắt buộc"""
|
||||
errors = []
|
||||
if not cls.LLM_API_KEY:
|
||||
errors.append("LLM_API_KEY 未配置")
|
||||
errors.append("LLM_API_KEY is not configured")
|
||||
if not cls.ZEP_API_KEY:
|
||||
errors.append("ZEP_API_KEY 未配置")
|
||||
errors.append("ZEP_API_KEY is not configured")
|
||||
return errors
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
"""
|
||||
数据模型模块
|
||||
Mô-đun mô hình dữ liệu
|
||||
"""
|
||||
|
||||
from .task import TaskManager, TaskStatus
|
||||
|
|
|
|||
|
|
@ -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) và Cạnh (edges),
|
||||
và toàn bộ chi tiết đi kèm khác (Time khởi tạo, Property).
|
||||
"""
|
||||
nodes = fetch_all_nodes(self.client, graph_id)
|
||||
edges = fetch_all_edges(self.client, graph_id)
|
||||
|
||||
# 创建节点映射用于获取节点名称
|
||||
# Giữ một map tra cứu để phục vụ lấy 'Tên' nhanh theo ID UUID
|
||||
node_map = {}
|
||||
for node in nodes:
|
||||
node_map[node.uuid_] = node.name or ""
|
||||
|
||||
nodes_data = []
|
||||
for node in nodes:
|
||||
# 获取创建时间
|
||||
# Lấy thông số về Thời gian được ghi nhận/khởi tạo
|
||||
created_at = getattr(node, 'created_at', None)
|
||||
if created_at:
|
||||
created_at = str(created_at)
|
||||
|
|
@ -453,7 +455,7 @@ 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
|
|
@ -1,6 +1,6 @@
|
|||
"""
|
||||
本体生成服务
|
||||
接口1:分析文本内容,生成适合社会模拟的实体和关系类型定义
|
||||
Dịch vụ tạo Ontology (Hệ thực thể / Quan hệ)
|
||||
API 1: Phân tích nội dung văn bản, khởi tạo các định nghĩa về loại thực thể và quan hệ phù hợp cho việc mô phỏng mạng xã hội
|
||||
"""
|
||||
|
||||
import json
|
||||
|
|
@ -8,157 +8,157 @@ from 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 mô phỏng tin đồn và dư luận mạng xã hội**. Trong hệ thống này:
|
||||
- Mỗi thực thể là một "tài khoản" hoặc "chủ thể" có thể lên tiếng, tương tác và lan truyền thông tin trên mạng xã hội.
|
||||
- Các thực thể có thể gây ảnh hưởng, chuyển tiếp (retweet), bình luận hoặc phản hồi lẫn nhau.
|
||||
- Chúng tôi cần mô phỏng phản ứng của các bên và đường truyền thông tin trong các sự kiện dư luận.
|
||||
|
||||
因此,**实体必须是现实中真实存在的、可以在社媒上发声和互动的主体**:
|
||||
Do đó, **thực thể phải là các chủ thể có thật trong thế giới thực, có khả năng lên tiếng và tương tác trên mạng xã hội**:
|
||||
|
||||
**可以是**:
|
||||
- 具体的个人(公众人物、当事人、意见领袖、专家学者、普通人)
|
||||
- 公司、企业(包括其官方账号)
|
||||
- 组织机构(大学、协会、NGO、工会等)
|
||||
- 政府部门、监管机构
|
||||
- 媒体机构(报纸、电视台、自媒体、网站)
|
||||
- 社交媒体平台本身
|
||||
- 特定群体代表(如校友会、粉丝团、维权群体等)
|
||||
**CÓ THỂ LÀ**:
|
||||
- Cá nhân cụ thể (nhân vật của công chúng, các bên liên quan, KOL, chuyên gia / học giả, người bình thường)
|
||||
- Công ty, doanh nghiệp (bao gồm cả tài khoản chính thức của họ)
|
||||
- Tổ chức (trường đại học, hiệp hội, tổ chức phi chính phủ (NGO), công đoàn, v.v.)
|
||||
- Các cơ quan chính phủ, cơ quan quản lý
|
||||
- Tổ chức báo chí / truyền thông (báo đài, đài truyền hình, tự do truyền thông, trang web)
|
||||
- Bản thân nền tảng mạng xã hội
|
||||
- Đại diện nhóm cụ thể (như hội cựu sinh viên, fan group, nhóm bảo vệ quyền lợi, v.v.)
|
||||
|
||||
**不可以是**:
|
||||
- 抽象概念(如"舆论"、"情绪"、"趋势")
|
||||
- 主题/话题(如"学术诚信"、"教育改革")
|
||||
- 观点/态度(如"支持方"、"反对方")
|
||||
**KHÔNG ĐƯỢC LÀ**:
|
||||
- Khái niệm trừu tượng (như "dư luận", "cảm xúc", "xu hướng")
|
||||
- Chủ đề / đề tài (như "tính toàn vẹn học thuật", "cải cách giáo dục")
|
||||
- Quan điểm / thái độ (như "phe ủng hộ", "bên phản đối")
|
||||
|
||||
## 输出格式
|
||||
## Định dạng đầu ra
|
||||
|
||||
请输出JSON格式,包含以下结构:
|
||||
Hãy trả về dưới định dạng JSON, bao gồm cấu trúc sau:
|
||||
|
||||
```json
|
||||
{
|
||||
"entity_types": [
|
||||
{
|
||||
"name": "实体类型名称(英文,PascalCase)",
|
||||
"description": "简短描述(英文,不超过100字符)",
|
||||
"name": "Tên loại thực thể (Tiếng Anh, PascalCase)",
|
||||
"description": "Mô tả ngắn gọn (Tiếng Anh, tối đa 100 ký tự)",
|
||||
"attributes": [
|
||||
{
|
||||
"name": "属性名(英文,snake_case)",
|
||||
"name": "Tên thuộc tính (Tiếng Anh, snake_case)",
|
||||
"type": "text",
|
||||
"description": "属性描述"
|
||||
"description": "Mô tả của thuộc tính"
|
||||
}
|
||||
],
|
||||
"examples": ["示例实体1", "示例实体2"]
|
||||
"examples": ["Ví dụ thực thể 1", "Ví dụ thực thể 2"]
|
||||
}
|
||||
],
|
||||
"edge_types": [
|
||||
{
|
||||
"name": "关系类型名称(英文,UPPER_SNAKE_CASE)",
|
||||
"description": "简短描述(英文,不超过100字符)",
|
||||
"name": "Tên loại quan hệ (Tiếng Anh, UPPER_SNAKE_CASE)",
|
||||
"description": "Mô tả ngắn (Tiếng Anh, tối đa 100 ký tự)",
|
||||
"source_targets": [
|
||||
{"source": "源实体类型", "target": "目标实体类型"}
|
||||
{"source": "Loại thực thể nguồn", "target": "Loại thực thể đích"}
|
||||
],
|
||||
"attributes": []
|
||||
}
|
||||
],
|
||||
"analysis_summary": "对文本内容的简要分析说明(中文)"
|
||||
"analysis_summary": "Giải thích ngắn gọn phân tích của bạn về văn bản (Tiếng Việt)"
|
||||
}
|
||||
```
|
||||
|
||||
## 设计指南(极其重要!)
|
||||
## Hướng dẫn Thiết kế (CỰC KỲ QUAN TRỌNG!)
|
||||
|
||||
### 1. 实体类型设计 - 必须严格遵守
|
||||
### 1. Thiết kế loại Thực thể (Entity Types) - Phải tuân thủ nghiêm ngặt
|
||||
|
||||
**数量要求:必须正好10个实体类型**
|
||||
**Yêu cầu số lượng: Đúng 10 loại Thực thể.**
|
||||
|
||||
**层次结构要求(必须同时包含具体类型和兜底类型)**:
|
||||
**Yêu cầu về cấu trúc phân cấp (Phải có cả Loại cụ thể và Loại bao quát/fallback):**
|
||||
|
||||
你的10个实体类型必须包含以下层次:
|
||||
10 loại thực thể của bạn phải bao gồm cấp độ sau:
|
||||
|
||||
A. **兜底类型(必须包含,放在列表最后2个)**:
|
||||
- `Person`: 任何自然人个体的兜底类型。当一个人不属于其他更具体的人物类型时,归入此类。
|
||||
- `Organization`: 任何组织机构的兜底类型。当一个组织不属于其他更具体的组织类型时,归入此类。
|
||||
A. **Loại bao quát (Fallback Types) (BẮT BUỘC, phải nằm ở 2 vị trí cuối cùng trong mảng)**:
|
||||
- `Person`: Là loại bao quát cho MỌI cá nhân tự nhiên. Nếu một người không thuộc các loại cụ thể ở trên, người đó sẽ thuộc `Person`.
|
||||
- `Organization`: Là loại bao quát cho MỌI tổ chức. Đặc trưng cho các tổ chức nhỏ hoặc không phù hợp với các loại tổ chức cụ thể khác.
|
||||
|
||||
B. **具体类型(8个,根据文本内容设计)**:
|
||||
- 针对文本中出现的主要角色,设计更具体的类型
|
||||
- 例如:如果文本涉及学术事件,可以有 `Student`, `Professor`, `University`
|
||||
- 例如:如果文本涉及商业事件,可以有 `Company`, `CEO`, `Employee`
|
||||
B. **Loại cụ thể (8 loại, phụ thuộc vào nội dung văn bản)**:
|
||||
- Thiết kế các loại cụ thể cho các vai chính được nhắc đến nhiều nhất trong văn bản.
|
||||
- Ví dụ: Nếu văn bản nói về scandal trường học, có thể có: `Student`, `Professor`, `University`
|
||||
- Ví dụ: Nếu văn bản là câu chuyện kinh doanh, có thể có: `Company`, `CEO`, `Employee`
|
||||
|
||||
**为什么需要兜底类型**:
|
||||
- 文本中会出现各种人物,如"中小学教师"、"路人甲"、"某位网友"
|
||||
- 如果没有专门的类型匹配,他们应该被归入 `Person`
|
||||
- 同理,小型组织、临时团体等应该归入 `Organization`
|
||||
**Tại sao cần các loại Bao quát (Fallback):**
|
||||
- Văn bản thường chứa các thông tin như "giáo viên tiểu học", "một người qua đường", "một cư dân mạng"
|
||||
- Nếu không có loại được định nghĩa riêng cho họ, họ nên thuộc về loại `Person`
|
||||
- Tương tự, tổ chức nhỏ bé hoặc nhóm học tập tạm thời nên thuộc `Organization`
|
||||
|
||||
**具体类型的设计原则**:
|
||||
- 从文本中识别出高频出现或关键的角色类型
|
||||
- 每个具体类型应该有明确的边界,避免重叠
|
||||
- description 必须清晰说明这个类型和兜底类型的区别
|
||||
**Nguyên tắc cho các loại Cụ thể:**
|
||||
- Nhận dạng tần suất xuất hiện và sức ảnh hưởng tới cốt truyện để xây dựng loại thực thể.
|
||||
- Mỗi loại nên có một ranh giới rõ ràng, không bị chồng chéo.
|
||||
- Thuộc tính mô tả (description) phải giải thích vì sao loại này tách biệt.
|
||||
|
||||
### 2. 关系类型设计
|
||||
### 2. Thiết kế Cạnh/Quan hệ (Edge Types)
|
||||
|
||||
- 数量:6-10个
|
||||
- 关系应该反映社媒互动中的真实联系
|
||||
- 确保关系的 source_targets 涵盖你定义的实体类型
|
||||
- Số lượng: Khoảng 6-10 loại quan hệ
|
||||
- Các mối quan hệ này phải giải thích và gắn kết được hành vi tương tác trên mạng xã hội của các nhân vật.
|
||||
- Đảm bảo mapping quan hệ hai chiều `source_targets` khớp với các thực thể phía trên.
|
||||
|
||||
### 3. 属性设计
|
||||
### 3. Thiết kế Thuộc tính (Attributes)
|
||||
|
||||
- 每个实体类型1-3个关键属性
|
||||
- **注意**:属性名不能使用 `name`、`uuid`、`group_id`、`created_at`、`summary`(这些是系统保留字)
|
||||
- 推荐使用:`full_name`, `title`, `role`, `position`, `location`, `description` 等
|
||||
- Mỗi loại thực thể cần 1-3 thuộc tính chính để làm rõ nhân thân.
|
||||
- **CHÚ Ý**: Không sử dụng các ID nội bộ làm thuộc tính như `name`, `uuid`, `group_id`, `created_at`, `summary` (chúng là từ khóa hệ thống).
|
||||
- Khuyên dùng: `full_name`, `title`, `role`, `position`, `location`, `description`,...
|
||||
|
||||
## 实体类型参考
|
||||
## Loại Thực thể tham khảo
|
||||
|
||||
**个人类(具体)**:
|
||||
- Student: 学生
|
||||
- Professor: 教授/学者
|
||||
- Journalist: 记者
|
||||
- Celebrity: 明星/网红
|
||||
- Executive: 高管
|
||||
- Official: 政府官员
|
||||
- Lawyer: 律师
|
||||
- Doctor: 医生
|
||||
**Loại cá nhân (Cụ thể):**
|
||||
- Student: Học sinh/Sinh viên
|
||||
- Professor: Giáo sư/Học giả
|
||||
- Journalist: Nhà báo/Phóng viên
|
||||
- Celebrity: Người nổi tiếng/Idol
|
||||
- Executive: Các giám đốc, CEO, cấp lãnh đạo
|
||||
- Official: Các vị công chức chính phủ
|
||||
- Lawyer: Luật sư
|
||||
- Doctor: Y sĩ/Bác sĩ
|
||||
|
||||
**个人类(兜底)**:
|
||||
- Person: 任何自然人(不属于上述具体类型时使用)
|
||||
**Loại cá nhân (Bao quát):**
|
||||
- Person: Là loại bao quát cho MỌI cá nhân tự nhiên nào không thuộc chi tiết ở trên.
|
||||
|
||||
**组织类(具体)**:
|
||||
- University: 高校
|
||||
- Company: 公司企业
|
||||
- GovernmentAgency: 政府机构
|
||||
- MediaOutlet: 媒体机构
|
||||
- Hospital: 医院
|
||||
- School: 中小学
|
||||
- NGO: 非政府组织
|
||||
**Loại tổ chức (Cụ thể):**
|
||||
- University: Đại học hoặc học viện
|
||||
- Company: Doanh nghiệp hay Công ty, tập đoàn
|
||||
- GovernmentAgency: Cơ quan quản lý, các cơ quan ban ngành công quyền
|
||||
- MediaOutlet: Truyền thông hay Tạp chí, Đài tin tức
|
||||
- Hospital: Bệnh viện / Trung tâm y tế
|
||||
- School: Bậc tiểu/trung học
|
||||
- NGO: Các loại Tổ chức phi chính phủ hoặc từ thiện
|
||||
|
||||
**组织类(兜底)**:
|
||||
- Organization: 任何组织机构(不属于上述具体类型时使用)
|
||||
**Loại tổ chức (Bao quát):**
|
||||
- Organization: Là loại bao quát cho MỌI cơ cấu hợp tác không thuôc chi tiết tổ chức ở trên.
|
||||
|
||||
## 关系类型参考
|
||||
## Loại Khái niệm Liên kết (Quan Hệ)
|
||||
|
||||
- WORKS_FOR: 工作于
|
||||
- STUDIES_AT: 就读于
|
||||
- AFFILIATED_WITH: 隶属于
|
||||
- REPRESENTS: 代表
|
||||
- REGULATES: 监管
|
||||
- REPORTS_ON: 报道
|
||||
- COMMENTS_ON: 评论
|
||||
- RESPONDS_TO: 回应
|
||||
- SUPPORTS: 支持
|
||||
- OPPOSES: 反对
|
||||
- COLLABORATES_WITH: 合作
|
||||
- COMPETES_WITH: 竞争
|
||||
- WORKS_FOR: Làm việc và ăn lương bởi tổ chức
|
||||
- STUDIES_AT: Đang học tại nhà trường
|
||||
- AFFILIATED_WITH: Liên quan, Trực thuộc vào đơn vị
|
||||
- REPRESENTS: Thể hiện tư cách hành động đại diện cho tập thể
|
||||
- REGULATES: Theo dõi, quản lý, thanh tra chính sách
|
||||
- REPORTS_ON: Tác nghiệp báo chí, có tin về hiện tượng
|
||||
- COMMENTS_ON: Có phản hồi hoặc lên tiếng về tranh cãi
|
||||
- RESPONDS_TO: Hành động đáp trả
|
||||
- SUPPORTS: Theo phe ủng hộ điều luật
|
||||
- OPPOSES: Phản đối chính sách
|
||||
- COLLABORATES_WITH: Tham gia phối ứng xử lý sự cố.
|
||||
- COMPETES_WITH: Quan hệ thù địch.
|
||||
"""
|
||||
|
||||
|
||||
class OntologyGenerator:
|
||||
"""
|
||||
本体生成器
|
||||
分析文本内容,生成实体和关系类型定义
|
||||
Trình khởi tạo Ontology
|
||||
Phân tích nội dung đoạn văn bản truyền vào, sau đó tự động suy luận ra định nghĩa của các loại Thực thể và Mối quan hệ
|
||||
"""
|
||||
|
||||
def __init__(self, llm_client: Optional[LLMClient] = None):
|
||||
|
|
@ -171,17 +171,17 @@ 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 mô tả nhu cầu mô phỏng của người dùng
|
||||
additional_context: Văn bản cung cấp thêm các ngữ cảnh phụ (nếu có)
|
||||
|
||||
Returns:
|
||||
本体定义(entity_types, edge_types等)
|
||||
Dictionary gồm cấu trúc Ontology (entity_types, edge_types v.v.)
|
||||
"""
|
||||
# 构建用户消息
|
||||
# Tạo câu lệnh Prompt gửi cho mô hình LLM
|
||||
user_message = self._build_user_message(
|
||||
document_texts,
|
||||
simulation_requirement,
|
||||
|
|
@ -193,19 +193,19 @@ 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 mô hình Thực Thể và Quan Hệ phù hợp để phục vụ việc mô phỏng dư luận trên mạng xã hội.
|
||||
|
||||
**必须遵守的规则**:
|
||||
1. 必须正好输出10个实体类型
|
||||
2. 最后2个必须是兜底类型:Person(个人兜底)和 Organization(组织兜底)
|
||||
3. 前8个是根据文本内容设计的具体类型
|
||||
4. 所有实体类型必须是现实中可以发声的主体,不能是抽象概念
|
||||
5. 属性名不能使用 name、uuid、group_id 等保留字,用 full_name、org_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 là từ Khoá phụ (Fallback): Person (Cho cá nhân) và Organization (Cho Tổ chức)
|
||||
3. 8 vị trí đầu tiên phải phân tích và suy luận dựa vào cấu trúc của chính văn bản truyền vào
|
||||
4. Tất cả các thực thể được liệt kê phải đóng vai trò là Chủ thể (nhân vật có thể lên tiếng ngoài đời thực), KHÔNG ĐƯỢC dùng làm khái niệm trừu tượng.
|
||||
5. Tên biến thuộc tính KHÔNG ĐƯỢC là name, uuid, group_id hay các biến số bảo lưu của hệ thống khác. Vui lòng chuyển thành full_name, org_name, v.v.
|
||||
"""
|
||||
|
||||
return message
|
||||
|
||||
def _validate_and_process(self, result: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""验证和后处理结果"""
|
||||
"""Tiền kiểm tra tính trọn vẹn và cấu trúc của dữ liệu phản hồi JSON"""
|
||||
|
||||
# 确保必要字段存在
|
||||
# Đảm bảo các thuộc tính mảng bắt buộc phải xuất hiện
|
||||
if "entity_types" not in result:
|
||||
result["entity_types"] = []
|
||||
if "edge_types" not in result:
|
||||
|
|
@ -265,17 +265,17 @@ 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
|
|
@ -1,11 +1,11 @@
|
|||
"""
|
||||
模拟IPC通信模块
|
||||
用于Flask后端和模拟脚本之间的进程间通信
|
||||
Module Giao tiếp IPC của Mô phỏng
|
||||
Dùng cho giao tiếp liên tiến trình giữa backend Flask và file script mô phỏng
|
||||
|
||||
通过文件系统实现简单的命令/响应模式:
|
||||
1. Flask写入命令到 commands/ 目录
|
||||
2. 模拟脚本轮询命令目录,执行命令并写入响应到 responses/ 目录
|
||||
3. Flask轮询响应目录获取结果
|
||||
Cấu trúc lệnh/phản hồi đơn giản được hiện thực hóa thông qua hệ thống tệp:
|
||||
1. Flask ghi lệnh vào thư mục commands/
|
||||
2. Kịch bản mô phỏng thăm dò (poll) thư mục lệnh, thực thi lệnh và ghi chuỗi phản hồi vào thư mục responses/
|
||||
3. Flask thăm dò lại thư mục phản hồi để nhận kết quả
|
||||
"""
|
||||
|
||||
import os
|
||||
|
|
@ -23,14 +23,14 @@ 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 Mô phỏng (dùng phía Flask)
|
||||
|
||||
用于向模拟进程发送命令并等待响应
|
||||
Được dùng để gửi file tới tiến trình mô phỏng và chờ response trả về
|
||||
"""
|
||||
|
||||
def __init__(self, simulation_dir: str):
|
||||
"""
|
||||
初始化IPC客户端
|
||||
Khởi tạo Client IPC
|
||||
|
||||
Args:
|
||||
simulation_dir: 模拟数据目录
|
||||
simulation_dir: Thư mục chứa dữ liệu mô phỏng
|
||||
"""
|
||||
self.simulation_dir = simulation_dir
|
||||
self.commands_dir = os.path.join(simulation_dir, "ipc_commands")
|
||||
self.responses_dir = os.path.join(simulation_dir, "ipc_responses")
|
||||
|
||||
# 确保目录存在
|
||||
# Đảm bảo rằng thư mục đã tồn tại
|
||||
os.makedirs(self.commands_dir, exist_ok=True)
|
||||
os.makedirs(self.responses_dir, exist_ok=True)
|
||||
|
||||
|
|
@ -122,19 +122,19 @@ class SimulationIPCClient:
|
|||
poll_interval: float = 0.5
|
||||
) -> IPCResponse:
|
||||
"""
|
||||
发送命令并等待响应
|
||||
Gửi lệnh ra và đợi kết quả phản hồi lại
|
||||
|
||||
Args:
|
||||
command_type: 命令类型
|
||||
args: 命令参数
|
||||
timeout: 超时时间(秒)
|
||||
poll_interval: 轮询间隔(秒)
|
||||
command_type: Loại lệnh
|
||||
args: Tham số của lệnh
|
||||
timeout: Thời gian timeout (giây)
|
||||
poll_interval: Khoảng thời gian giữa các lần thăm dò (giây)
|
||||
|
||||
Returns:
|
||||
IPCResponse
|
||||
|
||||
Raises:
|
||||
TimeoutError: 等待响应超时
|
||||
TimeoutError: Lỗi quá thời gian chờ phản hồi
|
||||
"""
|
||||
command_id = str(uuid.uuid4())
|
||||
command = IPCCommand(
|
||||
|
|
@ -143,14 +143,14 @@ 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 mô phỏng nền tảng kép, phỏng vấn một nền tảng đó khi mô phỏng nền tảng đơn
|
||||
timeout: Thời gian timeout
|
||||
|
||||
Returns:
|
||||
IPCResponse,result字段包含采访结果
|
||||
IPCResponse, trong đó trường result sẽ chứa kết quả cuộc phỏng vấn
|
||||
"""
|
||||
args = {
|
||||
"agent_id": agent_id,
|
||||
|
|
@ -228,18 +228,18 @@ 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 mô phỏng nền tảng kép
|
||||
timeout: Thời gian timeout
|
||||
|
||||
Returns:
|
||||
IPCResponse,result字段包含所有采访结果
|
||||
IPCResponse, trong đó trường result sẽ chứa tất cả các kết quả phỏng vấn
|
||||
"""
|
||||
args = {"interviews": interviews}
|
||||
if platform:
|
||||
|
|
@ -253,10 +253,10 @@ 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 mô phỏng còn sống hay không
|
||||
|
||||
通过检查 env_status.json 文件来判断
|
||||
Được xác định thông qua việc kiểm tra tệp tin env_status.json
|
||||
"""
|
||||
status_file = os.path.join(self.simulation_dir, "env_status.json")
|
||||
if not os.path.exists(status_file):
|
||||
|
|
@ -287,41 +287,41 @@ class SimulationIPCClient:
|
|||
|
||||
class SimulationIPCServer:
|
||||
"""
|
||||
模拟IPC服务器(模拟脚本端使用)
|
||||
Server (Máy chủ) IPC Mô phỏng (dùng phía kịch bản mô phỏng)
|
||||
|
||||
轮询命令目录,执行命令并返回响应
|
||||
Tiến hành thăm dò thư mục lệnh, thực thi lệnh và trả về kết quả phản hồi
|
||||
"""
|
||||
|
||||
def __init__(self, simulation_dir: str):
|
||||
"""
|
||||
初始化IPC服务器
|
||||
Khởi tạo Máy chủ IPC
|
||||
|
||||
Args:
|
||||
simulation_dir: 模拟数据目录
|
||||
simulation_dir: Thư mục chứa dữ liệu mô phỏng
|
||||
"""
|
||||
self.simulation_dir = simulation_dir
|
||||
self.commands_dir = os.path.join(simulation_dir, "ipc_commands")
|
||||
self.responses_dir = os.path.join(simulation_dir, "ipc_responses")
|
||||
|
||||
# 确保目录存在
|
||||
# Đảm bảo rằng thư mục đã tồn tại
|
||||
os.makedirs(self.commands_dir, exist_ok=True)
|
||||
os.makedirs(self.responses_dir, exist_ok=True)
|
||||
|
||||
# 环境状态
|
||||
# Trạng thái môi trường
|
||||
self._running = False
|
||||
|
||||
def start(self):
|
||||
"""标记服务器为运行状态"""
|
||||
"""Đánh dấu Máy chủ đang ở trạng thái chạy"""
|
||||
self._running = True
|
||||
self._update_env_status("alive")
|
||||
|
||||
def stop(self):
|
||||
"""标记服务器为停止状态"""
|
||||
"""Đánh dấu Máy chủ đang ở trạng thái dừng"""
|
||||
self._running = False
|
||||
self._update_env_status("stopped")
|
||||
|
||||
def _update_env_status(self, status: str):
|
||||
"""更新环境状态文件"""
|
||||
"""Cập nhật tệp trạng thái môi trường"""
|
||||
status_file = os.path.join(self.simulation_dir, "env_status.json")
|
||||
with open(status_file, 'w', encoding='utf-8') as f:
|
||||
json.dump({
|
||||
|
|
@ -331,15 +331,15 @@ class SimulationIPCServer:
|
|||
|
||||
def poll_commands(self) -> Optional[IPCCommand]:
|
||||
"""
|
||||
轮询命令目录,返回第一个待处理的命令
|
||||
Thăm dò thư mục lệnh, trả về lệnh chờ xử lý đầu tiên
|
||||
|
||||
Returns:
|
||||
IPCCommand 或 None
|
||||
IPCCommand hoặc None
|
||||
"""
|
||||
if not os.path.exists(self.commands_dir):
|
||||
return None
|
||||
|
||||
# 按时间排序获取命令文件
|
||||
# Lấy danh sách file lệnh và sắp xếp theo thời gian
|
||||
command_files = []
|
||||
for filename in os.listdir(self.commands_dir):
|
||||
if filename.endswith('.json'):
|
||||
|
|
@ -354,23 +354,23 @@ 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,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"""
|
||||
OASIS模拟管理器
|
||||
管理Twitter和Reddit双平台并行模拟
|
||||
使用预设脚本 + LLM智能生成配置参数
|
||||
Trình Quản lý Mô Phỏng OASIS
|
||||
Đảm nhiệm xây dựng và điều phối chạy mô phỏng song song trên hai nền tảng giả lập Twitter và Reddit.
|
||||
Sử dụng các kịch bản có sẵn kết hợp cùng LLM để thiết lập thông minh bộ tham số mô phỏng.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
|
@ -22,60 +22,60 @@ 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 lý trung tâm của tính năng Mô Phỏng
|
||||
|
||||
核心功能:
|
||||
1. 从Zep图谱读取实体并过滤
|
||||
2. 生成OASIS Agent Profile
|
||||
3. 使用LLM智能生成模拟配置参数
|
||||
4. 准备预设脚本所需的所有文件
|
||||
Luồng thiết lập cốt lõi:
|
||||
1. Trích xuất nhóm các Thực Thể (Entity) được định nghĩa sẵn trong hệ thống lưu trữ Graph của Zep.
|
||||
2. Chế lại thành các hồ sơ Profile thiết lập tiêu chuẩn của OASIS framework (Agent)
|
||||
3. Trao quyền cho sức mạnh mô hình LLM tự đánh giá số liệu rồi tự sinh ra cấu hình cài đặt cho quá trình mô phỏng
|
||||
4. Cài đặt các thư mục và tập tin tương ứng, phục vụ sẵn sàng để những Script lập trình riêng (pre-set script) có thể khai thác sử dụng.
|
||||
"""
|
||||
|
||||
# 模拟数据存储目录
|
||||
# Nơi chứa thư mục chứa Dữ liệu mô phỏng Local
|
||||
SIMULATION_DATA_DIR = os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
'../../uploads/simulations'
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
# 确保目录存在
|
||||
# Đảm bảo môi trường file data đã được set up
|
||||
os.makedirs(self.SIMULATION_DATA_DIR, exist_ok=True)
|
||||
|
||||
# 内存中的模拟状态缓存
|
||||
# Biến dictionary ở mức Application theo dõi trạng thái simulation qua Cache RAM.
|
||||
self._simulations: Dict[str, SimulationState] = {}
|
||||
|
||||
def _get_simulation_dir(self, simulation_id: str) -> str:
|
||||
"""获取模拟数据目录"""
|
||||
"""Lấy trả về các đường dẫn thư mục gốc tương ứng với Simulation ID"""
|
||||
sim_dir = os.path.join(self.SIMULATION_DATA_DIR, simulation_id)
|
||||
os.makedirs(sim_dir, exist_ok=True)
|
||||
return sim_dir
|
||||
|
||||
def _save_simulation_state(self, state: SimulationState):
|
||||
"""保存模拟状态到文件"""
|
||||
"""Bật tính năng lưu state định dạng JSON ra ổ cứng"""
|
||||
sim_dir = self._get_simulation_dir(state.simulation_id)
|
||||
state_file = os.path.join(sim_dir, "state.json")
|
||||
|
||||
|
|
@ -154,7 +154,7 @@ 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 Mô Phỏng
|
||||
|
||||
Args:
|
||||
project_id: 项目ID
|
||||
graph_id: Zep图谱ID
|
||||
enable_twitter: 是否启用Twitter模拟
|
||||
enable_reddit: 是否启用Reddit模拟
|
||||
project_id: Mã ID của Project (Quản lý cấp đầu vào)
|
||||
graph_id: Đồ thị ID tương ứng lấy bên Zep
|
||||
enable_twitter: Công tắc (Bật/Tắt) luồng giả lập Twitter
|
||||
enable_reddit: Công tắc (Bật/Tắt) luồng giả lập Reddit
|
||||
|
||||
Returns:
|
||||
SimulationState
|
||||
Đối tượng Class SimulationState
|
||||
"""
|
||||
import uuid
|
||||
simulation_id = f"sim_{uuid.uuid4().hex[:12]}"
|
||||
|
|
@ -222,7 +222,7 @@ 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 mô phỏng (Tiến trình Automation 100%)
|
||||
|
||||
步骤:
|
||||
1. 从Zep图谱读取并过滤实体
|
||||
2. 为每个实体生成OASIS Agent Profile(可选LLM增强,支持并行)
|
||||
3. 使用LLM智能生成模拟配置参数(时间、活跃度、发言频率等)
|
||||
4. 保存配置文件和Profile文件
|
||||
5. 复制预设脚本到模拟目录
|
||||
Các bước diễn ra:
|
||||
1. Gọi lấy các cụm Entity (thực thể) và bộ lọt (filter) từ Zep Graph API
|
||||
2. Tự động khởi tạo hàng loạt Agent Profile chạy OASIS tương ứng với Entity (Hỗ trợ gọi AI LLM để làm mượt văn bản / tăng tốc chạy song song)
|
||||
3. Hỏi và bắt bot LLM suy luận ra hệ tham số setting thông minh nhất (thời gian mô phỏng rò rỉ, hệ số tần suất nói chuyện hoạt động ...)
|
||||
4. In ra các file cấu hình và JSON của profile để hệ thống dễ đọc
|
||||
5. Copy nguyên các Scripts chuẩn được cấu hình sẵn (preset) ném vào thư mục để chạy
|
||||
|
||||
Args:
|
||||
simulation_id: 模拟ID
|
||||
simulation_requirement: 模拟需求描述(用于LLM生成配置)
|
||||
document_text: 原始文档内容(用于LLM理解背景)
|
||||
defined_entity_types: 预定义的实体类型(可选)
|
||||
use_llm_for_profiles: 是否使用LLM生成详细人设
|
||||
progress_callback: 进度回调函数 (stage, progress, message)
|
||||
parallel_profile_count: 并行生成人设的数量,默认3
|
||||
simulation_id: Mã ID của chu trình giả lập
|
||||
simulation_requirement: Chuỗi text từ người dùng yêu cầu mô phỏng gì (gửi cho config sinh cấu hình)
|
||||
document_text: Nội dung file Raw nguyên thủy (Cho LLM đánh giá context bối cảnh ban đầu)
|
||||
defined_entity_types: Dánh sách các Entity Model có sẵn do Zep định nghĩa (Option)
|
||||
use_llm_for_profiles: Toggle tính năng sử dụng mô hình LLM để buff thêm chi tiết cài đặt con Bot
|
||||
progress_callback: Hàm callback update log progress (chuyển về màn hình Frontend view) format (stage, progress, message)
|
||||
parallel_profile_count: Giới hạn concurrent threading chạy LLM gọi profile (Default là 3 luồng cùng lúc để làm nhanh hơn)
|
||||
|
||||
Returns:
|
||||
SimulationState
|
||||
Class cài đặt Data - SimulationState
|
||||
"""
|
||||
state = self._load_simulation_state(simulation_id)
|
||||
if not state:
|
||||
raise ValueError(f"模拟不存在: {simulation_id}")
|
||||
raise ValueError(f"Giả lập với ID: {simulation_id} không tồn tại")
|
||||
|
||||
try:
|
||||
state.status = SimulationStatus.PREPARING
|
||||
|
|
@ -268,14 +268,14 @@ 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
|
|
@ -1,5 +1,5 @@
|
|||
"""
|
||||
文本处理服务
|
||||
Dịch vụ xử lý văn bản (Text processing service)
|
||||
"""
|
||||
|
||||
from typing import List, Optional
|
||||
|
|
@ -7,11 +7,11 @@ 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ố ký tự chồng chéo giữa các đoạn liên tiếp (để giữ bối cảnh không bị đứt đoạn)
|
||||
|
||||
Returns:
|
||||
文本块列表
|
||||
Danh sách gồm các mảng văn bản sau khi chia
|
||||
"""
|
||||
return split_text_into_chunks(text, chunk_size, overlap)
|
||||
|
||||
@staticmethod
|
||||
def preprocess_text(text: str) -> str:
|
||||
"""
|
||||
预处理文本
|
||||
- 移除多余空白
|
||||
- 标准化换行
|
||||
Tiền xử lý văn bản (Làm sạch văn bản)
|
||||
- Xoá bỏ các khoảng trắng thừa
|
||||
- Chuẩn hoá định dạng xuống dòng (Line breaks)
|
||||
|
||||
Args:
|
||||
text: 原始文本
|
||||
text: Văn bản thô ban đầu
|
||||
|
||||
Returns:
|
||||
处理后的文本
|
||||
Văn bản đã được tinh giản, làm sạch
|
||||
"""
|
||||
import re
|
||||
|
||||
# 标准化换行
|
||||
# Chuẩn hoá ký tự xuống dòng (Windows \r\n hoặc Mac cũ \r thành \n chuẩn Linux)
|
||||
text = text.replace('\r\n', '\n').replace('\r', '\n')
|
||||
|
||||
# 移除连续空行(保留最多两个换行)
|
||||
# Xoá các dòng trống liên tiếp (Chỉ giữ lại tối đa 2 lần xuống dòng liên tiếp)
|
||||
text = re.sub(r'\n{3,}', '\n\n', text)
|
||||
|
||||
# 移除行首行尾空白
|
||||
|
|
@ -62,7 +62,7 @@ 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,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"""
|
||||
Zep实体读取与过滤服务
|
||||
从Zep图谱中读取节点,筛选出符合预定义实体类型的节点
|
||||
Dịch vụ đọc và lọc thực thể Zep
|
||||
Đọc các node từ đồ thị Zep, lọc ra các node phù hợp với các loại thực thể đã được định nghĩa trước
|
||||
"""
|
||||
|
||||
import time
|
||||
|
|
@ -15,21 +15,21 @@ 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 và lọc thực thể Zep
|
||||
|
||||
主要功能:
|
||||
1. 从Zep图谱读取所有节点
|
||||
2. 筛选出符合预定义实体类型的节点(Labels不只是Entity的节点)
|
||||
3. 获取每个实体的相关边和关联节点信息
|
||||
Chức năng chính:
|
||||
1. Đọc toàn bộ các node từ đồ thị Zep
|
||||
2. Lọc ra các node phù hợp với các loại thực thể đã được định nghĩa (Các node có Labels không chỉ là Entity)
|
||||
3. Lấy ra thông tin edge cũng như các node liên quan đối với từng thực thể
|
||||
"""
|
||||
|
||||
def __init__(self, api_key: Optional[str] = None):
|
||||
self.api_key = api_key or Config.ZEP_API_KEY
|
||||
if not self.api_key:
|
||||
raise ValueError("ZEP_API_KEY 未配置")
|
||||
raise ValueError("ZEP_API_KEY is not configured")
|
||||
|
||||
self.client = Zep(api_key=self.api_key)
|
||||
|
||||
|
|
@ -93,16 +93,16 @@ class ZepEntityReader:
|
|||
initial_delay: float = 2.0
|
||||
) -> T:
|
||||
"""
|
||||
带重试机制的Zep API调用
|
||||
Gọi hàm Zep API có cơ chế thử lại (retry)
|
||||
|
||||
Args:
|
||||
func: 要执行的函数(无参数的lambda或callable)
|
||||
operation_name: 操作名称,用于日志
|
||||
max_retries: 最大重试次数(默认3次,即最多尝试3次)
|
||||
initial_delay: 初始延迟秒数
|
||||
func: Hàm cần thực thi (lambda không tham số hoặc callable)
|
||||
operation_name: Tên thao tác, dùng cho log
|
||||
max_retries: Số lần thử lại tối đa (mặc định 3 lần, tức là thử tối đa 3 lần)
|
||||
initial_delay: Số giây trì hoãn ban đầu
|
||||
|
||||
Returns:
|
||||
API调用结果
|
||||
Kết quả của lệnh gọi API
|
||||
"""
|
||||
last_exception = None
|
||||
delay = initial_delay
|
||||
|
|
@ -114,27 +114,27 @@ 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ị (có phân trang)
|
||||
|
||||
Args:
|
||||
graph_id: 图谱ID
|
||||
graph_id: ID của đồ thị
|
||||
|
||||
Returns:
|
||||
节点列表
|
||||
Danh sách node
|
||||
"""
|
||||
logger.info(f"获取图谱 {graph_id} 的所有节点...")
|
||||
logger.info(f"Fetching all nodes for graph {graph_id}...")
|
||||
|
||||
nodes = fetch_all_nodes(self.client, graph_id)
|
||||
|
||||
|
|
@ -148,20 +148,20 @@ 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ị (có phân trang)
|
||||
|
||||
Args:
|
||||
graph_id: 图谱ID
|
||||
graph_id: ID của đồ thị
|
||||
|
||||
Returns:
|
||||
边列表
|
||||
Danh sách edge
|
||||
"""
|
||||
logger.info(f"获取图谱 {graph_id} 的所有边...")
|
||||
logger.info(f"Fetching all edges for graph {graph_id}...")
|
||||
|
||||
edges = fetch_all_edges(self.client, graph_id)
|
||||
|
||||
|
|
@ -176,24 +176,24 @@ 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 (có cơ chế thử lại)
|
||||
|
||||
Args:
|
||||
node_uuid: 节点UUID
|
||||
node_uuid: UUID của node
|
||||
|
||||
Returns:
|
||||
边列表
|
||||
Danh sách edge
|
||||
"""
|
||||
try:
|
||||
# 使用重试机制调用Zep API
|
||||
# Sử dụng cơ chế thử lại để gọi Zep API
|
||||
edges = self._call_with_retry(
|
||||
func=lambda: self.client.graph.node.get_entity_edges(node_uuid=node_uuid),
|
||||
operation_name=f"获取节点边(node={node_uuid[:8]}...)"
|
||||
operation_name=f"Fetch node edges(node={node_uuid[:8]}...)"
|
||||
)
|
||||
|
||||
edges_data = []
|
||||
|
|
@ -209,7 +209,7 @@ 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ỉ có một nhãn là "Entity", tức là thực thể này không hợp với loại chúng ta định nghĩa, tiến hành bỏ qua
|
||||
- Nếu Labels của node chứa các nhãn khác ngoài "Entity" và "Node", tức là hợp lệ, tiến hành giữ lại
|
||||
|
||||
Args:
|
||||
graph_id: 图谱ID
|
||||
defined_entity_types: 预定义的实体类型列表(可选,如果提供则只保留这些类型)
|
||||
enrich_with_edges: 是否获取每个实体的相关边信息
|
||||
graph_id: ID của đồ thị
|
||||
defined_entity_types: Danh sách các loại thực thể định nghĩa trước (không bắt buộc, nếu có thì chỉ giữ lại các loại đó)
|
||||
enrich_with_edges: Có lấy thông tin edge liên quan của từng thực thể hay không
|
||||
|
||||
Returns:
|
||||
FilteredEntities: 过滤后的实体集合
|
||||
FilteredEntities: Tập hợp các thực thể sau khi lọc
|
||||
"""
|
||||
logger.info(f"开始筛选图谱 {graph_id} 的实体...")
|
||||
logger.info(f"Start filtering entities for graph {graph_id}...")
|
||||
|
||||
# 获取所有节点
|
||||
# Lấy toàn bộ các node
|
||||
all_nodes = self.get_all_nodes(graph_id)
|
||||
total_count = len(all_nodes)
|
||||
|
||||
# 获取所有边(用于后续关联查找)
|
||||
# Lấy toàn bộ các edge (để lấy liên kết sau này)
|
||||
all_edges = self.get_all_edges(graph_id) if enrich_with_edges else []
|
||||
|
||||
# 构建节点UUID到节点数据的映射
|
||||
# Xây dựng map ánh xạ từ UUID của node sang dữ liệu node
|
||||
node_map = {n["uuid"]: n for n in all_nodes}
|
||||
|
||||
# 筛选符合条件的实体
|
||||
# Lọc các thực thể đáp ứng điều kiện
|
||||
filtered_entities = []
|
||||
entity_types_found = set()
|
||||
|
||||
for node in all_nodes:
|
||||
labels = node.get("labels", [])
|
||||
|
||||
# 筛选逻辑:Labels必须包含除"Entity"和"Node"之外的标签
|
||||
# Logic lọc: Labels bắt buộc phải chứa các nhãn khác "Entity" và "Node"
|
||||
custom_labels = [l for l in labels if l not in ["Entity", "Node"]]
|
||||
|
||||
if not custom_labels:
|
||||
# 只有默认标签,跳过
|
||||
# Chỉ có nhãn mặc định, bỏ qua
|
||||
continue
|
||||
|
||||
# 如果指定了预定义类型,检查是否匹配
|
||||
# Nếu đã chỉ định loại thực thể cho trước, kiểm tra xem có khớp hay không
|
||||
if defined_entity_types:
|
||||
matching_labels = [l for l in custom_labels if l in defined_entity_types]
|
||||
if not matching_labels:
|
||||
|
|
@ -270,7 +270,7 @@ 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ể và ngữ cảnh đầy đủ của nó (edge và node liên kết, với cơ chế thử lại)
|
||||
|
||||
Args:
|
||||
graph_id: 图谱ID
|
||||
entity_uuid: 实体UUID
|
||||
graph_id: ID của đồ thị
|
||||
entity_uuid: UUID của thực thể
|
||||
|
||||
Returns:
|
||||
EntityNode或None
|
||||
EntityNode hoặc None
|
||||
"""
|
||||
try:
|
||||
# 使用重试机制获取节点
|
||||
# Sử dụng cơ chế thử lại để lấy thông tin node
|
||||
node = self._call_with_retry(
|
||||
func=lambda: self.client.graph.node.get(uuid_=entity_uuid),
|
||||
operation_name=f"获取节点详情(uuid={entity_uuid[:8]}...)"
|
||||
operation_name=f"Fetch node detail(uuid={entity_uuid[:8]}...)"
|
||||
)
|
||||
|
||||
if not node:
|
||||
return None
|
||||
|
||||
# 获取节点的边
|
||||
# Lấy các edge của node
|
||||
edges = self.get_node_edges(entity_uuid)
|
||||
|
||||
# 获取所有节点用于关联查找
|
||||
# Lấy tất cả các node để tìm liên kết
|
||||
all_nodes = self.get_all_nodes(graph_id)
|
||||
node_map = {n["uuid"]: n for n in all_nodes}
|
||||
|
||||
# 处理相关边和节点
|
||||
# Xử lý các edge và node liên quan
|
||||
related_edges = []
|
||||
related_node_uuids = set()
|
||||
|
||||
|
|
@ -384,7 +384,7 @@ 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ể (ví dụ: "Student", "PublicFigure", v.v..)
|
||||
enrich_with_edges: Có lấy thông tin edge liên quan hay không
|
||||
|
||||
Returns:
|
||||
实体列表
|
||||
Danh sách thực thể
|
||||
"""
|
||||
result = self.filter_defined_entities(
|
||||
graph_id=graph_id,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"""
|
||||
Zep图谱记忆更新服务
|
||||
将模拟中的Agent活动动态更新到Zep图谱中
|
||||
Dịch vụ cập nhật bộ nhớ đồ thị Zep
|
||||
Cập nhật động các hoạt động của Agent trong mô phỏng lên đồ thị Zep
|
||||
"""
|
||||
|
||||
import os
|
||||
|
|
@ -22,7 +22,7 @@ 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 mô tả văn bản để gửi cho Zep
|
||||
|
||||
采用自然语言描述格式,让Zep能够从中提取实体和关系
|
||||
不添加模拟相关的前缀,避免误导图谱更新
|
||||
Sử dụng định dạng mô tả bằng ngôn ngữ tự nhiên để Zep có thể trích xuất thực thể và mối quan hệ
|
||||
Không thêm tiền tố liên quan đến mô phỏng để tránh gây nhiễu khi cập nhật đồ thị
|
||||
"""
|
||||
# 根据不同的动作类型生成不同的描述
|
||||
# Tạo mô tả khác nhau dựa trên từng loại hành động
|
||||
action_descriptions = {
|
||||
"CREATE_POST": self._describe_create_post,
|
||||
"LIKE_POST": self._describe_like_post,
|
||||
|
|
@ -57,222 +57,222 @@ 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 mô phỏng, cập nhật trực tiếp các hoạt động mới của agent lên đồ thị Zep.
|
||||
Nhóm theo nền tảng, gửi hàng loạt lên Zep sau khi tích lũy đủ số lượng hoạt động (BATCH_SIZE).
|
||||
|
||||
所有有意义的行为都会被更新到Zep,action_args中会包含完整的上下文信息:
|
||||
- 点赞/踩的帖子原文
|
||||
- 转发/引用的帖子原文
|
||||
- 关注/屏蔽的用户名
|
||||
- 点赞/踩的评论原文
|
||||
Tất cả các hành vi có ý nghĩa đều được cập nhật lên Zep, action_args chứa đầy đủ thông tin ngữ cảnh:
|
||||
- Nội dung gốc của bài viết được like/dislike
|
||||
- Nội dung gốc của bài viết được repost/quote
|
||||
- Tên người dùng được follow/mute
|
||||
- Nội dung gốc của bình luận được like/dislike
|
||||
"""
|
||||
|
||||
# 批量发送大小(每个平台累积多少条后发送)
|
||||
# Số lượng gửi mỗi lô (gửi sau khi mỗi nền tảng tích lũy đủ số lượng này)
|
||||
BATCH_SIZE = 5
|
||||
|
||||
# 平台名称映射(用于控制台显示)
|
||||
# Ánh xạ tên nền tảng (dùng để hiển thị trên console)
|
||||
PLATFORM_DISPLAY_NAMES = {
|
||||
'twitter': '世界1',
|
||||
'reddit': '世界2',
|
||||
'twitter': 'World 1',
|
||||
'reddit': 'World 2',
|
||||
}
|
||||
|
||||
# 发送间隔(秒),避免请求过快
|
||||
# Thời gian gửi cách nhau (giây), tránh request quá nhanh
|
||||
SEND_INTERVAL = 0.5
|
||||
|
||||
# 重试配置
|
||||
# Cấu hình thử lại (retry)
|
||||
MAX_RETRIES = 3
|
||||
RETRY_DELAY = 2 # 秒
|
||||
RETRY_DELAY = 2 # giây
|
||||
|
||||
def __init__(self, graph_id: str, api_key: Optional[str] = None):
|
||||
"""
|
||||
初始化更新器
|
||||
Khởi tạo trình cập nhật
|
||||
|
||||
Args:
|
||||
graph_id: Zep图谱ID
|
||||
api_key: Zep API Key(可选,默认从配置读取)
|
||||
graph_id: ID của đồ thị Zep
|
||||
api_key: Zep API Key (tự chọn, mặc định lấy từ config)
|
||||
"""
|
||||
self.graph_id = graph_id
|
||||
self.api_key = api_key or Config.ZEP_API_KEY
|
||||
|
||||
if not self.api_key:
|
||||
raise ValueError("ZEP_API_KEY未配置")
|
||||
raise ValueError("ZEP_API_KEY is not configured")
|
||||
|
||||
self.client = Zep(api_key=self.api_key)
|
||||
|
||||
# 活动队列
|
||||
# Hàng đợi hoạt động
|
||||
self._activity_queue: Queue = Queue()
|
||||
|
||||
# 按平台分组的活动缓冲区(每个平台各自累积到BATCH_SIZE后批量发送)
|
||||
# Bộ đệm hoạt động nhóm theo nền tảng (gửi hàng loạt sau khi đạt đến BATCH_SIZE)
|
||||
self._platform_buffers: Dict[str, List[AgentActivity]] = {
|
||||
'twitter': [],
|
||||
'reddit': [],
|
||||
}
|
||||
self._buffer_lock = threading.Lock()
|
||||
|
||||
# 控制标志
|
||||
# Cờ điều khiển
|
||||
self._running = False
|
||||
self._worker_thread: Optional[threading.Thread] = None
|
||||
|
||||
# 统计
|
||||
self._total_activities = 0 # 实际添加到队列的活动数
|
||||
self._total_sent = 0 # 成功发送到Zep的批次数
|
||||
self._total_items_sent = 0 # 成功发送到Zep的活动条数
|
||||
self._failed_count = 0 # 发送失败的批次数
|
||||
self._skipped_count = 0 # 被过滤跳过的活动数(DO_NOTHING)
|
||||
# Thống kê
|
||||
self._total_activities = 0 # Số lượng hoạt động thực tế được thêm vào hàng đợi
|
||||
self._total_sent = 0 # Số đợt hàng gửi đi thành công tới Zep
|
||||
self._total_items_sent = 0 # Số lượng các hoạt động đã gửi thành công tới Zep
|
||||
self._failed_count = 0 # Số lượt gửi đi thất bại
|
||||
self._skipped_count = 0 # Số lượng các hoạt động bị filter bỏ qua (DO_NOTHING)
|
||||
|
||||
logger.info(f"ZepGraphMemoryUpdater 初始化完成: graph_id={graph_id}, batch_size={self.BATCH_SIZE}")
|
||||
logger.info(f"ZepGraphMemoryUpdater initialized: graph_id={graph_id}, batch_size={self.BATCH_SIZE}")
|
||||
|
||||
def _get_platform_display_name(self, platform: str) -> str:
|
||||
"""获取平台的显示名称"""
|
||||
"""Lấy tên hiển thị của nền tảng"""
|
||||
return self.PLATFORM_DISPLAY_NAMES.get(platform.lower(), platform)
|
||||
|
||||
def start(self):
|
||||
"""启动后台工作线程"""
|
||||
"""Khởi chạy luồng làm việc dưới background"""
|
||||
if self._running:
|
||||
return
|
||||
|
||||
|
|
@ -283,19 +283,19 @@ 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 có ý nghĩa đều sẽ được thêm vào hàng đợi, bao gồm:
|
||||
- CREATE_POST (Đăng bài)
|
||||
- CREATE_COMMENT (Bình luận)
|
||||
- QUOTE_POST (Trích dẫn bài viết)
|
||||
- SEARCH_POSTS (Tìm kiếm bài viết)
|
||||
- SEARCH_USER (Tìm kiếm người dùng)
|
||||
- LIKE_POST/DISLIKE_POST (Like/Dislike bài viết)
|
||||
- REPOST (Repost)
|
||||
- FOLLOW (Theo dõi)
|
||||
- MUTE (Chặn)
|
||||
- LIKE_COMMENT/DISLIKE_COMMENT (Like/dislike bình luận)
|
||||
|
||||
action_args中会包含完整的上下文信息(如帖子原文、用户名等)。
|
||||
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 lý các trình cập nhật bộ nhớ đồ thị Zep cho nhiều mô phỏng
|
||||
|
||||
每个模拟可以有自己的更新器实例
|
||||
Mỗi mô phỏng có thể có instance trình cập nhật của riêng nó
|
||||
"""
|
||||
|
||||
_updaters: Dict[str, ZepGraphMemoryUpdater] = {}
|
||||
|
|
@ -483,17 +483,17 @@ class ZepGraphMemoryManager:
|
|||
@classmethod
|
||||
def create_updater(cls, simulation_id: str, graph_id: str) -> ZepGraphMemoryUpdater:
|
||||
"""
|
||||
为模拟创建图谱记忆更新器
|
||||
Tạo trình cập nhật bộ nhớ đồ thị cho mô phỏng
|
||||
|
||||
Args:
|
||||
simulation_id: 模拟ID
|
||||
graph_id: Zep图谱ID
|
||||
simulation_id: ID của mô phỏng
|
||||
graph_id: ID của đồ thị Zep
|
||||
|
||||
Returns:
|
||||
ZepGraphMemoryUpdater实例
|
||||
Instance của ZepGraphMemoryUpdater
|
||||
"""
|
||||
with cls._lock:
|
||||
# 如果已存在,先停止旧的
|
||||
# Nếu đã tồn tại, dừng cái cũ lại trước
|
||||
if simulation_id in cls._updaters:
|
||||
cls._updaters[simulation_id].stop()
|
||||
|
||||
|
|
@ -501,30 +501,30 @@ 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
|
|
@ -1,5 +1,5 @@
|
|||
"""
|
||||
工具模块
|
||||
Mô-đun tiện ích
|
||||
"""
|
||||
|
||||
from .file_parser import FileParser
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"""
|
||||
文件解析工具
|
||||
支持PDF、Markdown、TXT文件的文本提取
|
||||
Tiện ích phân tích tệp
|
||||
Hỗ trợ trích xuất văn bản từ tệp PDF, Markdown, TXT
|
||||
"""
|
||||
|
||||
import os
|
||||
|
|
@ -10,29 +10,29 @@ 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 mã hóa nếu UTF-8 thất bại.
|
||||
|
||||
Áp dụng chiến lược fallback nhiều tầng:
|
||||
1. Thử giải mã bằng UTF-8 trước
|
||||
2. Dùng charset_normalizer để phát hiện mã hóa
|
||||
3. Fallback sang chardet de phat hien ma hoa
|
||||
4. Cuối cùng dùng UTF-8 + errors='replace' để đảm bảo không vỡ ký tự
|
||||
|
||||
Args:
|
||||
file_path: 文件路径
|
||||
|
||||
file_path: Đường dẫn tệp
|
||||
|
||||
Returns:
|
||||
解码后的文本内容
|
||||
Nội dung văn bản sau khi giải mã
|
||||
"""
|
||||
data = Path(file_path).read_bytes()
|
||||
|
||||
# 首先尝试 UTF-8
|
||||
# Thử UTF-8 trước
|
||||
try:
|
||||
return data.decode('utf-8')
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
|
||||
# 尝试使用 charset_normalizer 检测编码
|
||||
# Thử phát hiện mã hóa bằng charset_normalizer
|
||||
encoding = None
|
||||
try:
|
||||
from charset_normalizer import from_bytes
|
||||
|
|
@ -42,7 +42,7 @@ def _read_text_with_fallback(file_path: str) -> str:
|
|||
except Exception:
|
||||
pass
|
||||
|
||||
# 回退到 chardet
|
||||
# Fallback sang chardet
|
||||
if not encoding:
|
||||
try:
|
||||
import chardet
|
||||
|
|
@ -51,7 +51,7 @@ def _read_text_with_fallback(file_path: str) -> str:
|
|||
except Exception:
|
||||
pass
|
||||
|
||||
# 最终兜底:使用 UTF-8 + replace
|
||||
# Fallback cuối: UTF-8 + replace
|
||||
if not encoding:
|
||||
encoding = 'utf-8'
|
||||
|
||||
|
|
@ -59,30 +59,30 @@ def _read_text_with_fallback(file_path: str) -> str:
|
|||
|
||||
|
||||
class FileParser:
|
||||
"""文件解析器"""
|
||||
"""Bộ phân tích tệp"""
|
||||
|
||||
SUPPORTED_EXTENSIONS = {'.pdf', '.md', '.markdown', '.txt'}
|
||||
|
||||
@classmethod
|
||||
def extract_text(cls, file_path: str) -> str:
|
||||
"""
|
||||
从文件中提取文本
|
||||
|
||||
Trích xuất văn bản từ tệp
|
||||
|
||||
Args:
|
||||
file_path: 文件路径
|
||||
|
||||
file_path: Đường dẫn tệp
|
||||
|
||||
Returns:
|
||||
提取的文本内容
|
||||
Nội dung văn bản đã trích xuất
|
||||
"""
|
||||
path = Path(file_path)
|
||||
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(f"文件不存在: {file_path}")
|
||||
raise FileNotFoundError(f"File does not exist: {file_path}")
|
||||
|
||||
suffix = path.suffix.lower()
|
||||
|
||||
if suffix not in cls.SUPPORTED_EXTENSIONS:
|
||||
raise ValueError(f"不支持的文件格式: {suffix}")
|
||||
raise ValueError(f"Unsupported file format: {suffix}")
|
||||
|
||||
if suffix == '.pdf':
|
||||
return cls._extract_from_pdf(file_path)
|
||||
|
|
@ -91,15 +91,15 @@ 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 và gộp lại
|
||||
|
||||
Args:
|
||||
file_paths: 文件路径列表
|
||||
|
||||
file_paths: Danh sách đường dẫn tệp
|
||||
|
||||
Returns:
|
||||
合并后的文本
|
||||
Văn bản đã gộp
|
||||
"""
|
||||
all_texts = []
|
||||
|
||||
|
|
@ -137,9 +137,9 @@ 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ố ký tự mỗi đoạn
|
||||
overlap: Số ký tự chồng lấp
|
||||
|
||||
Returns:
|
||||
文本块列表
|
||||
Danh sách các đoạn văn bản
|
||||
"""
|
||||
if len(text) <= chunk_size:
|
||||
return [text] if text.strip() else []
|
||||
|
|
@ -169,9 +169,9 @@ def split_text_into_chunks(
|
|||
while start < len(text):
|
||||
end = start + chunk_size
|
||||
|
||||
# 尝试在句子边界处分割
|
||||
# Cố gắng cắt tại ranh giới câu
|
||||
if end < len(text):
|
||||
# 查找最近的句子结束符
|
||||
# Tìm dấu kết thúc câu gần nhất
|
||||
for sep in ['。', '!', '?', '.\n', '!\n', '?\n', '\n\n', '. ', '! ', '? ']:
|
||||
last_sep = text[start:end].rfind(sep)
|
||||
if last_sep != -1 and last_sep > chunk_size * 0.3:
|
||||
|
|
@ -182,7 +182,7 @@ def split_text_into_chunks(
|
|||
if chunk:
|
||||
chunks.append(chunk)
|
||||
|
||||
# 下一个块从重叠位置开始
|
||||
# Đoạn tiếp theo bắt đầu từ vị trí overlap
|
||||
start = end - overlap if end < len(text) else len(text)
|
||||
|
||||
return chunks
|
||||
|
|
|
|||
|
|
@ -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 (ví 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 và trả về JSON
|
||||
|
||||
Args:
|
||||
messages: 消息列表
|
||||
temperature: 温度参数
|
||||
max_tokens: 最大token数
|
||||
|
||||
messages: Danh sách message
|
||||
temperature: Tham số nhiệt độ
|
||||
max_tokens: Số token tối đa
|
||||
|
||||
Returns:
|
||||
解析后的JSON对象
|
||||
JSON object sau khi parse
|
||||
"""
|
||||
response = self.chat(
|
||||
messages=messages,
|
||||
|
|
@ -90,7 +90,7 @@ 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}")
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"""
|
||||
日志配置模块
|
||||
提供统一的日志管理,同时输出到控制台和文件
|
||||
Mô-đun cấu hình logging
|
||||
Cung cấp quản lý log thống nhất, đồng thời ghi ra console và file
|
||||
"""
|
||||
|
||||
import os
|
||||
|
|
@ -12,47 +12,47 @@ from logging.handlers import RotatingFileHandler
|
|||
|
||||
def _ensure_utf8_stdout():
|
||||
"""
|
||||
确保 stdout/stderr 使用 UTF-8 编码
|
||||
解决 Windows 控制台中文乱码问题
|
||||
Đảm bảo stdout/stderr sử dụng mã hóa UTF-8
|
||||
Khắc phục lỗi vỡ font tiếng Trung trên console Windows
|
||||
"""
|
||||
if sys.platform == 'win32':
|
||||
# Windows 下重新配置标准输出为 UTF-8
|
||||
# Trên Windows, cấu hình lại stdout/stderr sang UTF-8
|
||||
if hasattr(sys.stdout, 'reconfigure'):
|
||||
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
||||
if hasattr(sys.stderr, 'reconfigure'):
|
||||
sys.stderr.reconfigure(encoding='utf-8', errors='replace')
|
||||
|
||||
|
||||
# 日志目录
|
||||
# Thư mục log
|
||||
LOG_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'logs')
|
||||
|
||||
|
||||
def setup_logger(name: str = 'mirofish', level: int = logging.DEBUG) -> logging.Logger:
|
||||
"""
|
||||
设置日志器
|
||||
|
||||
Thiết lập logger
|
||||
|
||||
Args:
|
||||
name: 日志器名称
|
||||
level: 日志级别
|
||||
|
||||
name: Tên logger
|
||||
level: Mức độ log
|
||||
|
||||
Returns:
|
||||
配置好的日志器
|
||||
Logger đã được cấu hình
|
||||
"""
|
||||
# 确保日志目录存在
|
||||
# Đảm bảo thư mục log tồn tại
|
||||
os.makedirs(LOG_DIR, exist_ok=True)
|
||||
|
||||
# 创建日志器
|
||||
# Tạo logger
|
||||
logger = logging.getLogger(name)
|
||||
logger.setLevel(level)
|
||||
|
||||
# 阻止日志向上传播到根 logger,避免重复输出
|
||||
# Chặn log propagate lên root logger để tránh in trùng lặp
|
||||
logger.propagate = False
|
||||
|
||||
# 如果已经有处理器,不重复添加
|
||||
# Nếu đã có handler thì không thêm lại
|
||||
if logger.handlers:
|
||||
return logger
|
||||
|
||||
# 日志格式
|
||||
# Định dạng log
|
||||
detailed_formatter = logging.Formatter(
|
||||
'[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d] %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
|
|
@ -63,7 +63,7 @@ def setup_logger(name: str = 'mirofish', level: int = logging.DEBUG) -> logging.
|
|||
datefmt='%H:%M:%S'
|
||||
)
|
||||
|
||||
# 1. 文件处理器 - 详细日志(按日期命名,带轮转)
|
||||
# 1. File handler - log chi tiết (đặt tên theo ngày, có rolling)
|
||||
log_filename = datetime.now().strftime('%Y-%m-%d') + '.log'
|
||||
file_handler = RotatingFileHandler(
|
||||
os.path.join(LOG_DIR, log_filename),
|
||||
|
|
@ -74,14 +74,14 @@ def setup_logger(name: str = 'mirofish', level: int = logging.DEBUG) -> logging.
|
|||
file_handler.setLevel(logging.DEBUG)
|
||||
file_handler.setFormatter(detailed_formatter)
|
||||
|
||||
# 2. 控制台处理器 - 简洁日志(INFO及以上)
|
||||
# 确保 Windows 下使用 UTF-8 编码,避免中文乱码
|
||||
# 2. Console handler - log gọn (INFO trở lên)
|
||||
# Đảm bảo Windows dùng UTF-8 để tránh lỗi ký tự
|
||||
_ensure_utf8_stdout()
|
||||
console_handler = logging.StreamHandler(sys.stdout)
|
||||
console_handler.setLevel(logging.INFO)
|
||||
console_handler.setFormatter(simple_formatter)
|
||||
|
||||
# 添加处理器
|
||||
# Gắn handler
|
||||
logger.addHandler(file_handler)
|
||||
logger.addHandler(console_handler)
|
||||
|
||||
|
|
@ -90,13 +90,13 @@ def setup_logger(name: str = 'mirofish', level: int = logging.DEBUG) -> logging.
|
|||
|
||||
def get_logger(name: str = 'mirofish') -> logging.Logger:
|
||||
"""
|
||||
获取日志器(如果不存在则创建)
|
||||
|
||||
Lấy logger (nếu chưa có sẽ tạo mới)
|
||||
|
||||
Args:
|
||||
name: 日志器名称
|
||||
|
||||
name: Tên logger
|
||||
|
||||
Returns:
|
||||
日志器实例
|
||||
Instance logger
|
||||
"""
|
||||
logger = logging.getLogger(name)
|
||||
if not logger.handlers:
|
||||
|
|
@ -104,11 +104,11 @@ def get_logger(name: str = 'mirofish') -> logging.Logger:
|
|||
return logger
|
||||
|
||||
|
||||
# 创建默认日志器
|
||||
# Tạo logger mặc định
|
||||
logger = setup_logger()
|
||||
|
||||
|
||||
# 便捷方法
|
||||
# Hàm gọi nhanh
|
||||
def debug(msg, *args, **kwargs):
|
||||
logger.debug(msg, *args, **kwargs)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"""
|
||||
API调用重试机制
|
||||
用于处理LLM等外部API调用的重试逻辑
|
||||
Cơ chế retry cho API call
|
||||
Dùng để xử lý retry khi gọi API bên ngoài như LLM
|
||||
"""
|
||||
|
||||
import time
|
||||
|
|
@ -22,17 +22,17 @@ def retry_with_backoff(
|
|||
on_retry: Optional[Callable[[Exception, int], None]] = None
|
||||
):
|
||||
"""
|
||||
带指数退避的重试装饰器
|
||||
|
||||
Decorator retry với exponential backoff
|
||||
|
||||
Args:
|
||||
max_retries: 最大重试次数
|
||||
initial_delay: 初始延迟(秒)
|
||||
max_delay: 最大延迟(秒)
|
||||
backoff_factor: 退避因子
|
||||
jitter: 是否添加随机抖动
|
||||
exceptions: 需要重试的异常类型
|
||||
on_retry: 重试时的回调函数 (exception, retry_count)
|
||||
|
||||
max_retries: Số lần retry tối đa
|
||||
initial_delay: Độ trễ ban đầu (giây)
|
||||
max_delay: Độ trễ tối đa (giây)
|
||||
backoff_factor: Hệ số tăng độ trễ
|
||||
jitter: Có thêm nhiễu ngẫu nhiên hay không
|
||||
exceptions: Các loại exception cần retry
|
||||
on_retry: Callback khi retry (exception, retry_count)
|
||||
|
||||
Usage:
|
||||
@retry_with_backoff(max_retries=3)
|
||||
def call_llm_api():
|
||||
|
|
@ -52,17 +52,17 @@ def 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 có retry
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
|
|
@ -154,16 +154,16 @@ class RetryableAPIClient:
|
|||
**kwargs
|
||||
) -> Any:
|
||||
"""
|
||||
执行函数调用并在失败时重试
|
||||
|
||||
Thực thi hàm và retry nếu thất bại
|
||||
|
||||
Args:
|
||||
func: 要调用的函数
|
||||
*args: 函数参数
|
||||
exceptions: 需要重试的异常类型
|
||||
**kwargs: 函数关键字参数
|
||||
|
||||
func: Hàm cần gọi
|
||||
*args: Tham số hàm
|
||||
exceptions: Các loại exception cần retry
|
||||
**kwargs: Tham số keyword của hàm
|
||||
|
||||
Returns:
|
||||
函数返回值
|
||||
Giá trị trả về của hàm
|
||||
"""
|
||||
last_exception = None
|
||||
delay = self.initial_delay
|
||||
|
|
@ -176,15 +176,15 @@ 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ử lý theo lô và retry riêng cho từng mục thất bại
|
||||
|
||||
Args:
|
||||
items: 要处理的项目列表
|
||||
process_func: 处理函数,接收单个item作为参数
|
||||
exceptions: 需要重试的异常类型
|
||||
continue_on_failure: 单项失败后是否继续处理其他项
|
||||
|
||||
items: Danh sách mục cần xử lý
|
||||
process_func: Hàm xử lý, nhận 1 item mỗi lần
|
||||
exceptions: Các loại exception cần retry
|
||||
continue_on_failure: Có tiếp tục khi 1 mục thất bại không
|
||||
|
||||
Returns:
|
||||
(成功结果列表, 失败项列表)
|
||||
(Danh sách kết quả thành công, danh sách mục thất bại)
|
||||
"""
|
||||
results = []
|
||||
failures = []
|
||||
|
|
@ -224,7 +224,7 @@ 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,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
"""Zep Graph 分页读取工具。
|
||||
"""Tiện ích đọc phân trang cho Zep Graph.
|
||||
|
||||
Zep 的 node/edge 列表接口使用 UUID cursor 分页,
|
||||
本模块封装自动翻页逻辑(含单页重试),对调用方透明地返回完整列表。
|
||||
API danh sách node/edge của Zep dùng UUID cursor để phân trang,
|
||||
module này đóng gói logic tự động lật trang (kèm retry từng trang)
|
||||
và trả về đầy đủ danh sách cho bên gọi một cách trong suốt.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -31,7 +32,7 @@ def _fetch_page_with_retry(
|
|||
page_description: str = "page",
|
||||
**kwargs: Any,
|
||||
) -> list[Any]:
|
||||
"""单页请求,失败时指数退避重试。仅重试网络/IO类瞬态错误。"""
|
||||
"""Yêu cầu 1 trang, retry với exponential backoff khi thất bại. Chỉ retry lỗi tạm thời mạng/IO."""
|
||||
if max_retries < 1:
|
||||
raise ValueError("max_retries must be >= 1")
|
||||
|
||||
|
|
@ -64,7 +65,7 @@ def fetch_all_nodes(
|
|||
max_retries: int = _DEFAULT_MAX_RETRIES,
|
||||
retry_delay: float = _DEFAULT_RETRY_DELAY,
|
||||
) -> list[Any]:
|
||||
"""分页获取图谱节点,最多返回 max_items 条(默认 2000)。每页请求自带重试。"""
|
||||
"""Lấy node theo trang, tối đa max_items mục (mặc định 2000). Mỗi trang đều có retry."""
|
||||
all_nodes: list[Any] = []
|
||||
cursor: str | None = None
|
||||
page_num = 0
|
||||
|
|
@ -109,7 +110,7 @@ def fetch_all_edges(
|
|||
max_retries: int = _DEFAULT_MAX_RETRIES,
|
||||
retry_delay: float = _DEFAULT_RETRY_DELAY,
|
||||
) -> list[Any]:
|
||||
"""分页获取图谱所有边,返回完整列表。每页请求自带重试。"""
|
||||
"""Lấy toàn bộ edge theo trang, trả về đầy đủ danh sách. Mỗi trang đều có retry."""
|
||||
all_edges: list[Any] = []
|
||||
cursor: str | None = None
|
||||
page_num = 0
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
"""
|
||||
动作日志记录器
|
||||
用于记录OASIS模拟中每个Agent的动作,供后端监控使用
|
||||
Trình ghi log hành động
|
||||
Dùng để ghi lại hành động của từng Agent trong mô phỏng OASIS, phục vụ backend giám sát
|
||||
|
||||
日志结构:
|
||||
Cấu trúc log:
|
||||
sim_xxx/
|
||||
├── twitter/
|
||||
│ └── actions.jsonl # Twitter 平台动作日志
|
||||
│ └── actions.jsonl # Log hành động nền tảng Twitter
|
||||
├── reddit/
|
||||
│ └── actions.jsonl # Reddit 平台动作日志
|
||||
├── simulation.log # 主模拟进程日志
|
||||
└── run_state.json # 运行状态(API 查询用)
|
||||
│ └── actions.jsonl # Log hành động nền tảng Reddit
|
||||
├── simulation.log # Log tiến trình mô phỏng chính
|
||||
└── run_state.json # Trạng thái chạy (cho API truy vấn)
|
||||
"""
|
||||
|
||||
import json
|
||||
|
|
@ -20,15 +20,15 @@ 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 mô 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 lý log mô phỏng
|
||||
Quản lý thống nhất mọi tệp log, tách riêng theo nền tảng
|
||||
"""
|
||||
|
||||
def __init__(self, simulation_dir: str):
|
||||
"""
|
||||
初始化日志管理器
|
||||
Khởi tạo trình quản lý log
|
||||
|
||||
Args:
|
||||
simulation_dir: 模拟目录路径
|
||||
simulation_dir: Đường dẫn thư mục mô phỏng
|
||||
"""
|
||||
self.simulation_dir = simulation_dir
|
||||
self.twitter_logger: Optional[PlatformActionLogger] = None
|
||||
self.reddit_logger: Optional[PlatformActionLogger] = None
|
||||
self._main_logger: Optional[logging.Logger] = None
|
||||
|
||||
# 设置主日志
|
||||
# Thiết lập log chính
|
||||
self._setup_main_logger()
|
||||
|
||||
def _setup_main_logger(self):
|
||||
"""设置主模拟日志"""
|
||||
"""Thiết lập log mô phỏng chính"""
|
||||
log_path = os.path.join(self.simulation_dir, "simulation.log")
|
||||
|
||||
# 创建 logger
|
||||
# Tạo logger
|
||||
self._main_logger = logging.getLogger(f"simulation.{os.path.basename(self.simulation_dir)}")
|
||||
self._main_logger.setLevel(logging.INFO)
|
||||
self._main_logger.handlers.clear()
|
||||
|
||||
# 文件处理器
|
||||
# File handler
|
||||
file_handler = logging.FileHandler(log_path, encoding='utf-8', mode='w')
|
||||
file_handler.setLevel(logging.INFO)
|
||||
file_handler.setFormatter(logging.Formatter(
|
||||
|
|
@ -155,7 +155,7 @@ 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 cũ)
|
||||
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
|
|
@ -1,16 +1,16 @@
|
|||
"""
|
||||
OASIS Reddit模拟预设脚本
|
||||
此脚本读取配置文件中的参数来执行模拟,实现全程自动化
|
||||
Kịch bản thiết lập sẵn mô phỏng OASIS Reddit
|
||||
Script này đọc tham số trong file cấu hình để chạy mô phỏng tự động hoàn toàn
|
||||
|
||||
功能特性:
|
||||
- 完成模拟后不立即关闭环境,进入等待命令模式
|
||||
- 支持通过IPC接收Interview命令
|
||||
- 支持单个Agent采访和批量采访
|
||||
- 支持远程关闭环境命令
|
||||
Tính năng:
|
||||
- Sau khi hoàn tất mô phỏng, không đóng môi trường ngay mà chuyển sang chế độ chờ lệnh
|
||||
- Hỗ trợ nhận lệnh Interview qua IPC
|
||||
- Hỗ trợ phỏng vấn một Agent hoặc phỏng vấn hàng loạt
|
||||
- Hỗ trợ lệnh đóng môi trường từ xa
|
||||
|
||||
使用方式:
|
||||
Cách dùng:
|
||||
python run_reddit_simulation.py --config /path/to/simulation_config.json
|
||||
python run_reddit_simulation.py --config /path/to/simulation_config.json --no-wait # 完成后立即关闭
|
||||
python run_reddit_simulation.py --config /path/to/simulation_config.json --no-wait # đóng ngay sau khi hoàn tất
|
||||
"""
|
||||
|
||||
import argparse
|
||||
|
|
@ -25,18 +25,18 @@ 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ý lệnh phỏng vấn một Agent
|
||||
|
||||
Returns:
|
||||
True 表示成功,False 表示失败
|
||||
True là thành công, False là thất bại
|
||||
"""
|
||||
try:
|
||||
# 获取Agent
|
||||
# Lấy Agent
|
||||
agent = self.agent_graph.get_agent(agent_id)
|
||||
|
||||
# 创建Interview动作
|
||||
# Tạo hành động Interview
|
||||
interview_action = ManualAction(
|
||||
action_type=ActionType.INTERVIEW,
|
||||
action_args={"prompt": prompt}
|
||||
)
|
||||
|
||||
# 执行Interview
|
||||
# Thực thi Interview
|
||||
actions = {agent: interview_action}
|
||||
await self.env.step(actions)
|
||||
|
||||
# 从数据库获取结果
|
||||
# Lấy kết quả từ cơ sở dữ liệu
|
||||
result = self._get_interview_result(agent_id)
|
||||
|
||||
self.send_response(command_id, "completed", result=result)
|
||||
print(f" Interview完成: agent_id={agent_id}")
|
||||
print(f" Interview completed: agent_id={agent_id}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
print(f" Interview失败: agent_id={agent_id}, error={error_msg}")
|
||||
print(f" Interview failed: agent_id={agent_id}, error={error_msg}")
|
||||
self.send_response(command_id, "failed", error=error_msg)
|
||||
return False
|
||||
|
||||
async def handle_batch_interview(self, command_id: str, interviews: List[Dict]) -> bool:
|
||||
"""
|
||||
处理批量采访命令
|
||||
Xử lý lệnh phỏng vấn hàng loạt
|
||||
|
||||
Args:
|
||||
interviews: [{"agent_id": int, "prompt": str}, ...]
|
||||
"""
|
||||
try:
|
||||
# 构建动作字典
|
||||
# Tạo dict hành động
|
||||
actions = {}
|
||||
agent_prompts = {} # 记录每个agent的prompt
|
||||
agent_prompts = {} # Ghi lại prompt của từng agent
|
||||
|
||||
for interview in interviews:
|
||||
agent_id = interview.get("agent_id")
|
||||
|
|
@ -269,16 +269,16 @@ 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ử lý toàn bộ lệnh đang chờ
|
||||
|
||||
Returns:
|
||||
True 表示继续运行,False 表示应该退出
|
||||
True là tiếp tục chạy, False là nên thoát
|
||||
"""
|
||||
command = self.poll_command()
|
||||
if not command:
|
||||
|
|
@ -355,7 +355,7 @@ 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可用动作(不包含INTERVIEW,INTERVIEW只能通过ManualAction手动触发)
|
||||
# Các hành động khả dụng cho Reddit (không bao gồm INTERVIEW; INTERVIEW chỉ kích hoạt thủ công qua ManualAction)
|
||||
AVAILABLE_ACTIONS = [
|
||||
ActionType.LIKE_POST,
|
||||
ActionType.DISLIKE_POST,
|
||||
|
|
@ -404,11 +404,11 @@ class RedditSimulationRunner:
|
|||
|
||||
def __init__(self, config_path: str, wait_for_commands: bool = True):
|
||||
"""
|
||||
初始化模拟运行器
|
||||
Khởi tạo bộ chạy mô phỏng
|
||||
|
||||
Args:
|
||||
config_path: 配置文件路径 (simulation_config.json)
|
||||
wait_for_commands: 模拟完成后是否等待命令(默认True)
|
||||
config_path: Đường dẫn file cấu hình (simulation_config.json)
|
||||
wait_for_commands: Có chờ lệnh sau khi mô phỏng hoàn tất hay không (mặc định True)
|
||||
"""
|
||||
self.config_path = config_path
|
||||
self.config = self._load_config()
|
||||
|
|
@ -419,47 +419,47 @@ 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 mô hình LLM
|
||||
|
||||
统一使用项目根目录 .env 文件中的配置(优先级最高):
|
||||
- LLM_API_KEY: API密钥
|
||||
- LLM_BASE_URL: API基础URL
|
||||
- LLM_MODEL_NAME: 模型名称
|
||||
Thống nhất dùng cấu hình trong file .env tại thư mục gốc dự án (ưu tiên cao nhất):
|
||||
- LLM_API_KEY: API key
|
||||
- LLM_BASE_URL: URL cơ sở API
|
||||
- LLM_MODEL_NAME: Tên mô hình
|
||||
"""
|
||||
# 优先从 .env 读取配置
|
||||
# Ưu tiên đọc cấu hình từ .env
|
||||
llm_api_key = os.environ.get("LLM_API_KEY", "")
|
||||
llm_base_url = os.environ.get("LLM_BASE_URL", "")
|
||||
llm_model = os.environ.get("LLM_MODEL_NAME", "")
|
||||
|
||||
# 如果 .env 中没有,则使用 config 作为备用
|
||||
# Nếu .env không có thì dùng config làm dự phòng
|
||||
if not llm_model:
|
||||
llm_model = self.config.get("llm_model", "gpt-4o-mini")
|
||||
|
||||
# 设置 camel-ai 所需的环境变量
|
||||
# Thiết lập biến môi trường cần cho camel-ai
|
||||
if llm_api_key:
|
||||
os.environ["OPENAI_API_KEY"] = llm_api_key
|
||||
|
||||
if not os.environ.get("OPENAI_API_KEY"):
|
||||
raise ValueError("缺少 API Key 配置,请在项目根目录 .env 文件中设置 LLM_API_KEY")
|
||||
raise ValueError("Missing API key configuration. Please set LLM_API_KEY in the project root .env file")
|
||||
|
||||
if llm_base_url:
|
||||
os.environ["OPENAI_API_BASE_URL"] = llm_base_url
|
||||
|
||||
print(f"LLM配置: model={llm_model}, base_url={llm_base_url[:40] if llm_base_url else '默认'}...")
|
||||
print(f"LLM config: model={llm_model}, base_url={llm_base_url[:40] if llm_base_url else 'default'}...")
|
||||
|
||||
return ModelFactory.create(
|
||||
model_platform=ModelPlatformType.OPENAI,
|
||||
|
|
@ -473,7 +473,7 @@ 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 và 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 mô phỏng tối đa (tùy chọn, dùng để cắt bớt mô phỏng quá dài)
|
||||
"""
|
||||
print("=" * 60)
|
||||
print("OASIS Reddit模拟")
|
||||
print(f"配置文件: {self.config_path}")
|
||||
print(f"模拟ID: {self.config.get('simulation_id', 'unknown')}")
|
||||
print(f"等待命令模式: {'启用' if self.wait_for_commands else '禁用'}")
|
||||
print("OASIS Reddit Simulation")
|
||||
print(f"Config file: {self.config_path}")
|
||||
print(f"Simulation ID: {self.config.get('simulation_id', 'unknown')}")
|
||||
print(f"Wait mode: {'enabled' if self.wait_for_commands else 'disabled'}")
|
||||
print("=" * 60)
|
||||
|
||||
time_config = self.config.get("time_config", {})
|
||||
|
|
@ -538,28 +538,28 @@ 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ử lý tín hiệu, bảo đảm thoát đúng khi nhận SIGTERM/SIGINT.
|
||||
Giúp chương trình có cơ hội dọn tài nguyên đúng cách (đóng cơ sở dữ liệu, môi trường...).
|
||||
"""
|
||||
def signal_handler(signum, frame):
|
||||
global _cleanup_done
|
||||
sig_name = "SIGTERM" if signum == signal.SIGTERM else "SIGINT"
|
||||
print(f"\n收到 {sig_name} 信号,正在退出...")
|
||||
print(f"\nReceived {sig_name}, shutting down...")
|
||||
if not _cleanup_done:
|
||||
_cleanup_done = True
|
||||
if _shutdown_event:
|
||||
_shutdown_event.set()
|
||||
else:
|
||||
# 重复收到信号才强制退出
|
||||
print("强制退出...")
|
||||
# Chỉ buộc thoát khi nhận tín hiệu lặp lại
|
||||
print("Force exit...")
|
||||
sys.exit(1)
|
||||
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
|
|
@ -761,9 +761,9 @@ if __name__ == "__main__":
|
|||
try:
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
print("\n程序被中断")
|
||||
print("\nProgram interrupted")
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
print("模拟进程已退出")
|
||||
print("Simulation process exited")
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
"""
|
||||
OASIS Twitter模拟预设脚本
|
||||
此脚本读取配置文件中的参数来执行模拟,实现全程自动化
|
||||
Kịch bản thiết lập sẵn mô phỏng OASIS Twitter
|
||||
Script này đọc tham số trong file cấu hình để chạy mô phỏng tự động hoàn toàn
|
||||
|
||||
功能特性:
|
||||
- 完成模拟后不立即关闭环境,进入等待命令模式
|
||||
- 支持通过IPC接收Interview命令
|
||||
- 支持单个Agent采访和批量采访
|
||||
- 支持远程关闭环境命令
|
||||
Tính năng:
|
||||
- Sau khi hoàn tất mô phỏng, không đóng môi trường ngay mà chuyển sang chế độ chờ lệnh
|
||||
- Hỗ trợ nhận lệnh Interview qua IPC
|
||||
- Hỗ trợ phỏng vấn một Agent hoặc phỏng vấn hàng loạt
|
||||
- Hỗ trợ lệnh đóng môi trường từ xa
|
||||
|
||||
使用方式:
|
||||
Cách dùng:
|
||||
python run_twitter_simulation.py --config /path/to/simulation_config.json
|
||||
python run_twitter_simulation.py --config /path/to/simulation_config.json --no-wait # 完成后立即关闭
|
||||
python run_twitter_simulation.py --config /path/to/simulation_config.json --no-wait # đóng ngay sau khi hoàn tất
|
||||
"""
|
||||
|
||||
import argparse
|
||||
|
|
@ -25,18 +25,18 @@ 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ý lệnh phỏng vấn một Agent
|
||||
|
||||
Returns:
|
||||
True 表示成功,False 表示失败
|
||||
True là thành công, False là thất bại
|
||||
"""
|
||||
try:
|
||||
# 获取Agent
|
||||
# Lấy Agent
|
||||
agent = self.agent_graph.get_agent(agent_id)
|
||||
|
||||
# 创建Interview动作
|
||||
# Tạo hành động Interview
|
||||
interview_action = ManualAction(
|
||||
action_type=ActionType.INTERVIEW,
|
||||
action_args={"prompt": prompt}
|
||||
)
|
||||
|
||||
# 执行Interview
|
||||
# Thực thi Interview
|
||||
actions = {agent: interview_action}
|
||||
await self.env.step(actions)
|
||||
|
||||
# 从数据库获取结果
|
||||
# Lấy kết quả từ cơ sở dữ liệu
|
||||
result = self._get_interview_result(agent_id)
|
||||
|
||||
self.send_response(command_id, "completed", result=result)
|
||||
print(f" Interview完成: agent_id={agent_id}")
|
||||
print(f" Interview completed: agent_id={agent_id}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
print(f" Interview失败: agent_id={agent_id}, error={error_msg}")
|
||||
print(f" Interview failed: agent_id={agent_id}, error={error_msg}")
|
||||
self.send_response(command_id, "failed", error=error_msg)
|
||||
return False
|
||||
|
||||
async def handle_batch_interview(self, command_id: str, interviews: List[Dict]) -> bool:
|
||||
"""
|
||||
处理批量采访命令
|
||||
Xử lý lệnh phỏng vấn hàng loạt
|
||||
|
||||
Args:
|
||||
interviews: [{"agent_id": int, "prompt": str}, ...]
|
||||
"""
|
||||
try:
|
||||
# 构建动作字典
|
||||
# Tạo dict hành động
|
||||
actions = {}
|
||||
agent_prompts = {} # 记录每个agent的prompt
|
||||
agent_prompts = {} # Ghi lại prompt của từng agent
|
||||
|
||||
for interview in interviews:
|
||||
agent_id = interview.get("agent_id")
|
||||
|
|
@ -269,16 +269,16 @@ 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ử lý toàn bộ lệnh đang chờ
|
||||
|
||||
Returns:
|
||||
True 表示继续运行,False 表示应该退出
|
||||
True là tiếp tục chạy, False là nên thoát
|
||||
"""
|
||||
command = self.poll_command()
|
||||
if not command:
|
||||
|
|
@ -355,7 +355,7 @@ 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可用动作(不包含INTERVIEW,INTERVIEW只能通过ManualAction手动触发)
|
||||
# Các hành động khả dụng cho Twitter (không bao gồm INTERVIEW; INTERVIEW chỉ kích hoạt thủ công qua ManualAction)
|
||||
AVAILABLE_ACTIONS = [
|
||||
ActionType.CREATE_POST,
|
||||
ActionType.LIKE_POST,
|
||||
|
|
@ -397,11 +397,11 @@ class TwitterSimulationRunner:
|
|||
|
||||
def __init__(self, config_path: str, wait_for_commands: bool = True):
|
||||
"""
|
||||
初始化模拟运行器
|
||||
Khởi tạo bộ chạy mô phỏng
|
||||
|
||||
Args:
|
||||
config_path: 配置文件路径 (simulation_config.json)
|
||||
wait_for_commands: 模拟完成后是否等待命令(默认True)
|
||||
config_path: Đường dẫn file cấu hình (simulation_config.json)
|
||||
wait_for_commands: Có chờ lệnh sau khi mô phỏng hoàn tất hay không (mặc định True)
|
||||
"""
|
||||
self.config_path = config_path
|
||||
self.config = self._load_config()
|
||||
|
|
@ -412,47 +412,47 @@ 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 mô hình LLM
|
||||
|
||||
统一使用项目根目录 .env 文件中的配置(优先级最高):
|
||||
- LLM_API_KEY: API密钥
|
||||
- LLM_BASE_URL: API基础URL
|
||||
- LLM_MODEL_NAME: 模型名称
|
||||
Thống nhất dùng cấu hình trong file .env tại thư mục gốc dự án (ưu tiên cao nhất):
|
||||
- LLM_API_KEY: API key
|
||||
- LLM_BASE_URL: URL cơ sở API
|
||||
- LLM_MODEL_NAME: Tên mô hình
|
||||
"""
|
||||
# 优先从 .env 读取配置
|
||||
# Ưu tiên đọc cấu hình từ .env
|
||||
llm_api_key = os.environ.get("LLM_API_KEY", "")
|
||||
llm_base_url = os.environ.get("LLM_BASE_URL", "")
|
||||
llm_model = os.environ.get("LLM_MODEL_NAME", "")
|
||||
|
||||
# 如果 .env 中没有,则使用 config 作为备用
|
||||
# Nếu .env không có thì dùng config làm dự phòng
|
||||
if not llm_model:
|
||||
llm_model = self.config.get("llm_model", "gpt-4o-mini")
|
||||
|
||||
# 设置 camel-ai 所需的环境变量
|
||||
# Thiết lập biến môi trường cần cho camel-ai
|
||||
if llm_api_key:
|
||||
os.environ["OPENAI_API_KEY"] = llm_api_key
|
||||
|
||||
if not os.environ.get("OPENAI_API_KEY"):
|
||||
raise ValueError("缺少 API Key 配置,请在项目根目录 .env 文件中设置 LLM_API_KEY")
|
||||
raise ValueError("Missing API key configuration. Please set LLM_API_KEY in the project root .env file")
|
||||
|
||||
if llm_base_url:
|
||||
os.environ["OPENAI_API_BASE_URL"] = llm_base_url
|
||||
|
||||
print(f"LLM配置: model={llm_model}, base_url={llm_base_url[:40] if llm_base_url else '默认'}...")
|
||||
print(f"LLM config: model={llm_model}, base_url={llm_base_url[:40] if llm_base_url else 'default'}...")
|
||||
|
||||
return ModelFactory.create(
|
||||
model_platform=ModelPlatformType.OPENAI,
|
||||
|
|
@ -466,24 +466,24 @@ 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 và cấu hình
|
||||
|
||||
Args:
|
||||
env: OASIS环境
|
||||
current_hour: 当前模拟小时(0-23)
|
||||
round_num: 当前轮数
|
||||
env: Môi trường OASIS
|
||||
current_hour: Giờ mô phỏng hiện tại (0-23)
|
||||
round_num: Vòng hiện tại
|
||||
|
||||
Returns:
|
||||
激活的Agent列表
|
||||
Danh sách Agent được kích hoạt
|
||||
"""
|
||||
time_config = self.config.get("time_config", {})
|
||||
agent_configs = self.config.get("agent_configs", [])
|
||||
|
||||
# 基础激活数量
|
||||
# Số lượng kích hoạt cơ bản
|
||||
base_min = time_config.get("agents_per_hour_min", 5)
|
||||
base_max = time_config.get("agents_per_hour_max", 20)
|
||||
|
||||
# 根据时段调整
|
||||
# Điều chỉnh theo khung giờ
|
||||
peak_hours = time_config.get("peak_hours", [9, 10, 11, 14, 15, 20, 21, 22])
|
||||
off_peak_hours = time_config.get("off_peak_hours", [0, 1, 2, 3, 4, 5])
|
||||
|
||||
|
|
@ -496,28 +496,28 @@ 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 mô phỏng tối đa (tùy chọn, dùng để cắt bớt mô phỏng quá dài)
|
||||
"""
|
||||
print("=" * 60)
|
||||
print("OASIS Twitter模拟")
|
||||
print(f"配置文件: {self.config_path}")
|
||||
print(f"模拟ID: {self.config.get('simulation_id', 'unknown')}")
|
||||
print(f"等待命令模式: {'启用' if self.wait_for_commands else '禁用'}")
|
||||
print("OASIS Twitter Simulation")
|
||||
print(f"Config file: {self.config_path}")
|
||||
print(f"Simulation ID: {self.config.get('simulation_id', 'unknown')}")
|
||||
print(f"Wait mode: {'enabled' if self.wait_for_commands else 'disabled'}")
|
||||
print("=" * 60)
|
||||
|
||||
# 加载时间配置
|
||||
# Tải cấu hình thời gian
|
||||
time_config = self.config.get("time_config", {})
|
||||
total_hours = time_config.get("total_simulation_hours", 72)
|
||||
minutes_per_round = time_config.get("minutes_per_round", 30)
|
||||
|
||||
# 计算总轮数
|
||||
# Tính tổng số vòng
|
||||
total_rounds = (total_hours * 60) // minutes_per_round
|
||||
|
||||
# 如果指定了最大轮数,则截断
|
||||
# Nếu chỉ định số vòng tối đa thì sẽ cắt bớt
|
||||
if max_rounds is not None and max_rounds > 0:
|
||||
original_rounds = total_rounds
|
||||
total_rounds = min(total_rounds, max_rounds)
|
||||
if total_rounds < original_rounds:
|
||||
print(f"\n轮数已截断: {original_rounds} -> {total_rounds} (max_rounds={max_rounds})")
|
||||
print(f"\nRounds truncated: {original_rounds} -> {total_rounds} (max_rounds={max_rounds})")
|
||||
|
||||
print(f"\n模拟参数:")
|
||||
print(f" - 总模拟时长: {total_hours}小时")
|
||||
print(f" - 每轮时间: {minutes_per_round}分钟")
|
||||
print(f" - 总轮数: {total_rounds}")
|
||||
print(f"\nSimulation parameters:")
|
||||
print(f" - Total simulation duration: {total_hours} hours")
|
||||
print(f" - Minutes per round: {minutes_per_round} min")
|
||||
print(f" - Total rounds: {total_rounds}")
|
||||
if max_rounds:
|
||||
print(f" - 最大轮数限制: {max_rounds}")
|
||||
print(f" - Agent数量: {len(self.config.get('agent_configs', []))}")
|
||||
print(f" - Max round limit: {max_rounds}")
|
||||
print(f" - Agent count: {len(self.config.get('agent_configs', []))}")
|
||||
|
||||
# 创建模型
|
||||
print("\n初始化LLM模型...")
|
||||
# Tạo mô hình
|
||||
print("\nInitializing LLM model...")
|
||||
model = self._create_model()
|
||||
|
||||
# 加载Agent图
|
||||
print("加载Agent Profile...")
|
||||
# Tải đồ thị Agent
|
||||
print("Loading Agent Profile...")
|
||||
profile_path = self._get_profile_path()
|
||||
if not os.path.exists(profile_path):
|
||||
print(f"错误: Profile文件不存在: {profile_path}")
|
||||
print(f"Error: Profile file does not exist: {profile_path}")
|
||||
return
|
||||
|
||||
self.agent_graph = await generate_twitter_agent_graph(
|
||||
|
|
@ -581,34 +581,34 @@ 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ử lý tín hiệu, bảo đảm thoát đúng khi nhận SIGTERM/SIGINT.
|
||||
Giúp chương trình có cơ hội dọn tài nguyên đúng cách (đóng cơ sở dữ liệu, môi trường...).
|
||||
"""
|
||||
def signal_handler(signum, frame):
|
||||
global _cleanup_done
|
||||
sig_name = "SIGTERM" if signum == signal.SIGTERM else "SIGINT"
|
||||
print(f"\n收到 {sig_name} 信号,正在退出...")
|
||||
print(f"\nReceived {sig_name}, shutting down...")
|
||||
if not _cleanup_done:
|
||||
_cleanup_done = True
|
||||
if _shutdown_event:
|
||||
_shutdown_event.set()
|
||||
else:
|
||||
# 重复收到信号才强制退出
|
||||
print("强制退出...")
|
||||
# Chỉ buộc thoát khi nhận tín hiệu lặp lại
|
||||
print("Force exit...")
|
||||
sys.exit(1)
|
||||
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
|
|
@ -773,8 +773,8 @@ if __name__ == "__main__":
|
|||
try:
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
print("\n程序被中断")
|
||||
print("\nProgram interrupted")
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
print("模拟进程已退出")
|
||||
print("Simulation process exited")
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
"""
|
||||
测试Profile格式生成是否符合OASIS要求
|
||||
验证:
|
||||
1. Twitter Profile生成CSV格式
|
||||
2. Reddit Profile生成JSON详细格式
|
||||
Kiểm tra việc sinh định dạng Profile có đúng yêu cầu OASIS hay không.
|
||||
Xác thực:
|
||||
1. Twitter Profile sinh ở định dạng CSV.
|
||||
2. Reddit Profile sinh ở định dạng JSON chi tiết.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
|
@ -11,19 +11,19 @@ import 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 = [
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue