Merge pull request #1 from TQuynh109/feature/translation
Feature/translation
This commit is contained in:
commit
34bab77a72
|
|
@ -2,7 +2,7 @@
|
|||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# 环境变量(保护敏感信息)
|
||||
# Biến môi trường (bảo vệ thông tin nhạy cảm)
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
|
@ -36,7 +36,7 @@ yarn-error.log*
|
|||
*.swp
|
||||
*.swo
|
||||
|
||||
# 测试
|
||||
# Kiểm thử
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
|
|
@ -45,16 +45,16 @@ htmlcov/
|
|||
.cursor/
|
||||
.claude/
|
||||
|
||||
# 文档与测试程序
|
||||
# Tài liệu và chương trình kiểm thử
|
||||
mydoc/
|
||||
mytest/
|
||||
|
||||
# 日志文件
|
||||
# File log
|
||||
backend/logs/
|
||||
*.log
|
||||
|
||||
# 上传文件
|
||||
# File tải lên
|
||||
backend/uploads/
|
||||
|
||||
# Docker 数据
|
||||
# Dữ liệu Docker
|
||||
data/
|
||||
12
Dockerfile
12
Dockerfile
|
|
@ -1,29 +1,29 @@
|
|||
FROM python:3.11
|
||||
|
||||
# 安装 Node.js (满足 >=18)及必要工具
|
||||
# Cài đặt Node.js (đáp ứng >=18) và các công cụ cần thiết
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends nodejs npm \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 从 uv 官方镜像复制 uv
|
||||
# Sao chép uv từ image chính thức của uv
|
||||
COPY --from=ghcr.io/astral-sh/uv:0.9.26 /uv /uvx /bin/
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 先复制依赖描述文件以利用缓存
|
||||
# Sao chép trước file mô tả dependency để tận dụng cache
|
||||
COPY package.json package-lock.json ./
|
||||
COPY frontend/package.json frontend/package-lock.json ./frontend/
|
||||
COPY backend/pyproject.toml backend/uv.lock ./backend/
|
||||
|
||||
# 安装依赖(Node + Python)
|
||||
# Cài đặt dependency (Node + Python)
|
||||
RUN npm ci \
|
||||
&& npm ci --prefix frontend \
|
||||
&& cd backend && uv sync --frozen
|
||||
|
||||
# 复制项目源码
|
||||
# Sao chép mã nguồn dự án
|
||||
COPY . .
|
||||
|
||||
EXPOSE 3000 5001
|
||||
|
||||
# 同时启动前后端(开发模式)
|
||||
# Khởi chạy đồng thời frontend và backend (chế độ phát triển)
|
||||
CMD ["npm", "run", "dev"]
|
||||
|
|
@ -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,7 @@
|
|||
"""
|
||||
项目上下文管理
|
||||
用于在服务端持久化项目状态,避免前端在接口间传递大量数据
|
||||
Quản lý context (ngữ cảnh) của dự án
|
||||
Được sử dụng để lưu trữ trạng thái dự án trên server (persistence),
|
||||
giúp tránh việc frontend phải gửi đi một lượng lớn dữ liệu mỗi lần gọi API.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
|
@ -15,45 +16,45 @@ from ..config import Config
|
|||
|
||||
|
||||
class ProjectStatus(str, Enum):
|
||||
"""项目状态"""
|
||||
CREATED = "created" # 刚创建,文件已上传
|
||||
ONTOLOGY_GENERATED = "ontology_generated" # 本体已生成
|
||||
GRAPH_BUILDING = "graph_building" # 图谱构建中
|
||||
GRAPH_COMPLETED = "graph_completed" # 图谱构建完成
|
||||
FAILED = "failed" # 失败
|
||||
"""Trạng thái hiện tại của dự án"""
|
||||
CREATED = "created" # Dự án vừa được tạo, các file đã được tải lên thành công
|
||||
ONTOLOGY_GENERATED = "ontology_generated" # Đã hoàn tất khởi tạo Ontology
|
||||
GRAPH_BUILDING = "graph_building" # Tri thức đồ thị (Knowledge Graph) đang được xây dựng
|
||||
GRAPH_COMPLETED = "graph_completed" # Đã hoàn thành quá trình Graph
|
||||
FAILED = "failed" # Thiết lập / Xử lý gặp lỗi
|
||||
|
||||
|
||||
@dataclass
|
||||
class Project:
|
||||
"""项目数据模型"""
|
||||
"""Mô hình dữ liệu (Data model) của dự án"""
|
||||
project_id: str
|
||||
name: str
|
||||
status: ProjectStatus
|
||||
created_at: str
|
||||
updated_at: str
|
||||
|
||||
# 文件信息
|
||||
# File information
|
||||
files: List[Dict[str, str]] = field(default_factory=list) # [{filename, path, size}]
|
||||
total_text_length: int = 0
|
||||
|
||||
# 本体信息(接口1生成后填充)
|
||||
# Thông tin ontology (được điền sau khi API 1 xử lý xong)
|
||||
ontology: Optional[Dict[str, Any]] = None
|
||||
analysis_summary: Optional[str] = None
|
||||
|
||||
# 图谱信息(接口2完成后填充)
|
||||
# Thông tin graph (được điền sau khi API 2 hoàn thành)
|
||||
graph_id: Optional[str] = None
|
||||
graph_build_task_id: Optional[str] = None
|
||||
|
||||
# 配置
|
||||
# Cấu hình
|
||||
simulation_requirement: Optional[str] = None
|
||||
chunk_size: int = 500
|
||||
chunk_overlap: int = 50
|
||||
|
||||
# 错误信息
|
||||
# Thông tin lỗi
|
||||
error: Optional[str] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""转换为字典"""
|
||||
"""Biến đổi đối tượng (Object) thành Dictionary (để dễ dàng chuyển thành JSON và lưu trữ)"""
|
||||
return {
|
||||
"project_id": self.project_id,
|
||||
"name": self.name,
|
||||
|
|
@ -74,7 +75,7 @@ class Project:
|
|||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'Project':
|
||||
"""从字典创建"""
|
||||
"""Khởi tạo một instance Project từ dữ liệu kiểu Dictionary (khi load lên từ hệ thống lưu trữ)"""
|
||||
status = data.get('status', 'created')
|
||||
if isinstance(status, str):
|
||||
status = ProjectStatus(status)
|
||||
|
|
@ -99,46 +100,46 @@ class Project:
|
|||
|
||||
|
||||
class ProjectManager:
|
||||
"""项目管理器 - 负责项目的持久化存储和检索"""
|
||||
"""Quản lý các dự án (ProjectManager) - Chịu trách nhiệm lưu trữ và truy xuất thông tin dự án"""
|
||||
|
||||
# 项目存储根目录
|
||||
# Thư mục gốc để lưu trữ toàn bộ dữ liệu dự án trên máy chủ
|
||||
PROJECTS_DIR = os.path.join(Config.UPLOAD_FOLDER, 'projects')
|
||||
|
||||
@classmethod
|
||||
def _ensure_projects_dir(cls):
|
||||
"""确保项目目录存在"""
|
||||
"""Đảm bảo thư mục lưu trữ dự án đã được tạo, nếu không có thì self tạo mới"""
|
||||
os.makedirs(cls.PROJECTS_DIR, exist_ok=True)
|
||||
|
||||
@classmethod
|
||||
def _get_project_dir(cls, project_id: str) -> str:
|
||||
"""获取项目目录路径"""
|
||||
"""Đường dẫn tới thư mục lưu trữ tương ứng với project_id"""
|
||||
return os.path.join(cls.PROJECTS_DIR, project_id)
|
||||
|
||||
@classmethod
|
||||
def _get_project_meta_path(cls, project_id: str) -> str:
|
||||
"""获取项目元数据文件路径"""
|
||||
"""Đường dẫn lấy file cài đặt metadata (thường là project.json)"""
|
||||
return os.path.join(cls._get_project_dir(project_id), 'project.json')
|
||||
|
||||
@classmethod
|
||||
def _get_project_files_dir(cls, project_id: str) -> str:
|
||||
"""获取项目文件存储目录"""
|
||||
"""Đường dẫn đến thư mục chứa các file nguyên thuỷ do người dùng kéo thả tải lên cho dự án"""
|
||||
return os.path.join(cls._get_project_dir(project_id), 'files')
|
||||
|
||||
@classmethod
|
||||
def _get_project_text_path(cls, project_id: str) -> str:
|
||||
"""获取项目提取文本存储路径"""
|
||||
"""Lấy vị trí của tệp văn bản (txt) đã được hệ thống trích xuất nội dung"""
|
||||
return os.path.join(cls._get_project_dir(project_id), 'extracted_text.txt')
|
||||
|
||||
@classmethod
|
||||
def create_project(cls, name: str = "Unnamed Project") -> Project:
|
||||
"""
|
||||
创建新项目
|
||||
Khởi tạo và tạo mới cấu trúc dự án trên server
|
||||
|
||||
Args:
|
||||
name: 项目名称
|
||||
name: Tên của dự án
|
||||
|
||||
Returns:
|
||||
新创建的Project对象
|
||||
Project object vừa được tạo
|
||||
"""
|
||||
cls._ensure_projects_dir()
|
||||
|
||||
|
|
@ -153,20 +154,20 @@ class ProjectManager:
|
|||
updated_at=now
|
||||
)
|
||||
|
||||
# 创建项目目录结构
|
||||
# Thiết lập các thư mục con trong không gian thư mục của project
|
||||
project_dir = cls._get_project_dir(project_id)
|
||||
files_dir = cls._get_project_files_dir(project_id)
|
||||
os.makedirs(project_dir, exist_ok=True)
|
||||
os.makedirs(files_dir, exist_ok=True)
|
||||
|
||||
# 保存项目元数据
|
||||
# Ghi các trường thông tin (metadata) của project vào file cứng
|
||||
cls.save_project(project)
|
||||
|
||||
return project
|
||||
|
||||
@classmethod
|
||||
def save_project(cls, project: Project) -> None:
|
||||
"""保存项目元数据"""
|
||||
"""Ghi chồng cấu hình cập nhật (metadata mới) đối của project vào file (Mặc định: project.json) """
|
||||
project.updated_at = datetime.now().isoformat()
|
||||
meta_path = cls._get_project_meta_path(project.project_id)
|
||||
|
||||
|
|
@ -176,10 +177,10 @@ class ProjectManager:
|
|||
@classmethod
|
||||
def get_project(cls, project_id: str) -> Optional[Project]:
|
||||
"""
|
||||
获取项目
|
||||
Get Project
|
||||
|
||||
Args:
|
||||
project_id: 项目ID
|
||||
project_id: Project ID
|
||||
|
||||
Returns:
|
||||
Project对象,如果不存在返回None
|
||||
|
|
@ -197,13 +198,13 @@ class ProjectManager:
|
|||
@classmethod
|
||||
def list_projects(cls, limit: int = 50) -> List[Project]:
|
||||
"""
|
||||
列出所有项目
|
||||
Lấy danh sách tất cả các dự án (projects) đang có trên system
|
||||
|
||||
Args:
|
||||
limit: 返回数量限制
|
||||
limit: Giới hạn số lượng hiển thị (mặc định lấy 50 project)
|
||||
|
||||
Returns:
|
||||
项目列表,按创建时间倒序
|
||||
Danh sách gồm các Object Project, xếp theo ngày/giờ giảm dần (từ mới tạo -> cũ nhất)
|
||||
"""
|
||||
cls._ensure_projects_dir()
|
||||
|
||||
|
|
@ -213,7 +214,7 @@ class ProjectManager:
|
|||
if project:
|
||||
projects.append(project)
|
||||
|
||||
# 按创建时间倒序排序
|
||||
# Sắp xếp lại lịch sử project theo thứ tự giảm dần thời gian
|
||||
projects.sort(key=lambda p: p.created_at, reverse=True)
|
||||
|
||||
return projects[:limit]
|
||||
|
|
@ -221,47 +222,47 @@ class ProjectManager:
|
|||
@classmethod
|
||||
def delete_project(cls, project_id: str) -> bool:
|
||||
"""
|
||||
删除项目及其所有文件
|
||||
Xoá vĩnh viễn dữ liệu về project và mọi file liên quan của nó khỏi server
|
||||
|
||||
Args:
|
||||
project_id: 项目ID
|
||||
project_id: Mã ID của Project
|
||||
|
||||
Returns:
|
||||
是否删除成功
|
||||
Boolean đại diện cờ Thành công / Thất bại của việc xoá
|
||||
"""
|
||||
project_dir = cls._get_project_dir(project_id)
|
||||
|
||||
if not os.path.exists(project_dir):
|
||||
return False
|
||||
|
||||
shutil.rmtree(project_dir)
|
||||
shutil.rmtree(project_dir) # Xoá toàn bộ thư mục dữ liệu project_id
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def save_file_to_project(cls, project_id: str, file_storage, original_filename: str) -> Dict[str, str]:
|
||||
"""
|
||||
保存上传的文件到项目目录
|
||||
Ghi dữ liệu file đính kèm mà người dùng upload lên vào kho dự án
|
||||
|
||||
Args:
|
||||
project_id: 项目ID
|
||||
file_storage: Flask的FileStorage对象
|
||||
original_filename: 原始文件名
|
||||
project_id: Mã định danh của Project
|
||||
file_storage: Đối tượng Request File (từ framework, VD: của thư viện Flask/FastAPI) chứa nội dung file byte
|
||||
original_filename: Tên ban đầu từ máy tính người dùng
|
||||
|
||||
Returns:
|
||||
文件信息字典 {filename, path, size}
|
||||
Object chứa kết quả lưu file mới gồm {tên ban đầu, tên hash được lưu, đường dẫn đầy đủ, dung lượng}
|
||||
"""
|
||||
files_dir = cls._get_project_files_dir(project_id)
|
||||
os.makedirs(files_dir, exist_ok=True)
|
||||
|
||||
# 生成安全的文件名
|
||||
# Biến đổi tên file thành chuỗi an toàn độc nhất (UUID) để giữ các phiên bản không bị ghi đè, với phần mở rộng ban đầu
|
||||
ext = os.path.splitext(original_filename)[1].lower()
|
||||
safe_filename = f"{uuid.uuid4().hex[:8]}{ext}"
|
||||
file_path = os.path.join(files_dir, safe_filename)
|
||||
|
||||
# 保存文件
|
||||
# Uỷ quyền lưu vào đường dẫn đích
|
||||
file_storage.save(file_path)
|
||||
|
||||
# 获取文件大小
|
||||
# Đếm kích thước dung lượng (byte) của tập tin tĩnh tại ổ cứng
|
||||
file_size = os.path.getsize(file_path)
|
||||
|
||||
return {
|
||||
|
|
@ -273,14 +274,14 @@ class ProjectManager:
|
|||
|
||||
@classmethod
|
||||
def save_extracted_text(cls, project_id: str, text: str) -> None:
|
||||
"""保存提取的文本"""
|
||||
"""Tạo/ghi văn bản trích xuất (từ nội dung phân tích File upload) cho dự án vào folder dữ liệu"""
|
||||
text_path = cls._get_project_text_path(project_id)
|
||||
with open(text_path, 'w', encoding='utf-8') as f:
|
||||
f.write(text)
|
||||
|
||||
@classmethod
|
||||
def get_extracted_text(cls, project_id: str) -> Optional[str]:
|
||||
"""获取提取的文本"""
|
||||
"""Đọc và lấy nội dung File văn bản được trích xuất nếu có trước đó"""
|
||||
text_path = cls._get_project_text_path(project_id)
|
||||
|
||||
if not os.path.exists(text_path):
|
||||
|
|
@ -291,7 +292,7 @@ class ProjectManager:
|
|||
|
||||
@classmethod
|
||||
def get_project_files(cls, project_id: str) -> List[str]:
|
||||
"""获取项目的所有文件路径"""
|
||||
"""Lấy danh sách link đường dẫn gốc (absolute path) của các files (Tài liệu upload) thuộc dự án này"""
|
||||
files_dir = cls._get_project_files_dir(project_id)
|
||||
|
||||
if not os.path.exists(files_dir):
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"""
|
||||
任务状态管理
|
||||
用于跟踪长时间运行的任务(如图谱构建)
|
||||
Quản lý trạng thái Task (tác vụ)
|
||||
Được sử dụng để theo dõi các tác vụ chạy ngầm mất nhiều thời gian (ví dụ: xây dựng Knowledge Graph)
|
||||
"""
|
||||
|
||||
import uuid
|
||||
|
|
@ -12,30 +12,30 @@ from dataclasses import dataclass, field
|
|||
|
||||
|
||||
class TaskStatus(str, Enum):
|
||||
"""任务状态枚举"""
|
||||
PENDING = "pending" # 等待中
|
||||
PROCESSING = "processing" # 处理中
|
||||
COMPLETED = "completed" # 已完成
|
||||
FAILED = "failed" # 失败
|
||||
"""Định nghĩa các trạng thái (Enum) mà một Task có thể có"""
|
||||
PENDING = "pending" # Đang chờ (Task mới được tạo, chưa được xử lý)
|
||||
PROCESSING = "processing" # Đang xử lý (Hệ thống đang chạy ngầm Task này)
|
||||
COMPLETED = "completed" # Đã hoàn thành thành công
|
||||
FAILED = "failed" # Xảy ra lỗi và thất bại
|
||||
|
||||
|
||||
@dataclass
|
||||
class Task:
|
||||
"""任务数据类"""
|
||||
"""Lớp dữ liệu lưu trữ thông tin của một Task cụ thể"""
|
||||
task_id: str
|
||||
task_type: str
|
||||
status: TaskStatus
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
progress: int = 0 # 总进度百分比 0-100
|
||||
message: str = "" # 状态消息
|
||||
result: Optional[Dict] = None # 任务结果
|
||||
error: Optional[str] = None # 错误信息
|
||||
metadata: Dict = field(default_factory=dict) # 额外元数据
|
||||
progress_detail: Dict = field(default_factory=dict) # 详细进度信息
|
||||
progress: int = 0 # Phần trăm tiến độ quá trình chạy (0-100)
|
||||
message: str = "" # Thông báo trạng thái hiện tại để hiển thị cho người dùng
|
||||
result: Optional[Dict] = None # Kết quả trả về sau khi Task chạy xong
|
||||
error: Optional[str] = None # Thông tin chi tiết mỗi khi Task bị lỗi
|
||||
metadata: Dict = field(default_factory=dict) # Siêu dữ liệu bổ sung kèm theo (ví dụ: project_id)
|
||||
progress_detail: Dict = field(default_factory=dict) # Nội dung thông tin chi tiết về các bước trong tiến trình
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""转换为字典"""
|
||||
"""Chuyển đổi Class thành Dictionary để map vào JSON API Response"""
|
||||
return {
|
||||
"task_id": self.task_id,
|
||||
"task_type": self.task_type,
|
||||
|
|
@ -53,15 +53,15 @@ class Task:
|
|||
|
||||
class TaskManager:
|
||||
"""
|
||||
任务管理器
|
||||
线程安全的任务状态管理
|
||||
Trình quản lý Task
|
||||
Đảm bảo quản lý trạng thái của các tác vụ được đồng bộ tốt trên nhiều luồng chạy (Thread-safe)
|
||||
"""
|
||||
|
||||
_instance = None
|
||||
_lock = threading.Lock()
|
||||
|
||||
def __new__(cls):
|
||||
"""单例模式"""
|
||||
"""Kế thừa Singleton Pattern (Chỉ khởi tạo 1 instance duy nhất trên toàn ứng dụng)"""
|
||||
if cls._instance is None:
|
||||
with cls._lock:
|
||||
if cls._instance is None:
|
||||
|
|
@ -72,14 +72,14 @@ class TaskManager:
|
|||
|
||||
def create_task(self, task_type: str, metadata: Optional[Dict] = None) -> str:
|
||||
"""
|
||||
创建新任务
|
||||
Tạo mới một Task và cho vào hàng đợi (quản lý state)
|
||||
|
||||
Args:
|
||||
task_type: 任务类型
|
||||
metadata: 额外元数据
|
||||
task_type: Loại tác vụ (vd: 'build_graph', 'generate_report', ...)
|
||||
metadata: Dữ liệu đính kèm (vd: project_id liên quan để cập nhật dữ liệu sau này)
|
||||
|
||||
Returns:
|
||||
任务ID
|
||||
Chuỗi Mã định danh ngẫu nhiên (UUID) của Task
|
||||
"""
|
||||
task_id = str(uuid.uuid4())
|
||||
now = datetime.now()
|
||||
|
|
@ -99,7 +99,7 @@ class TaskManager:
|
|||
return task_id
|
||||
|
||||
def get_task(self, task_id: str) -> Optional[Task]:
|
||||
"""获取任务"""
|
||||
"""Lấy thông tin một Task đang chạy/kết thúc dựa theo Task UUID"""
|
||||
with self._task_lock:
|
||||
return self._tasks.get(task_id)
|
||||
|
||||
|
|
@ -114,16 +114,16 @@ class TaskManager:
|
|||
progress_detail: Optional[Dict] = None
|
||||
):
|
||||
"""
|
||||
更新任务状态
|
||||
Cập nhật tiến trình của Task
|
||||
|
||||
Args:
|
||||
task_id: 任务ID
|
||||
status: 新状态
|
||||
progress: 进度
|
||||
message: 消息
|
||||
result: 结果
|
||||
error: 错误信息
|
||||
progress_detail: 详细进度信息
|
||||
task_id: Mã ID của Task đang chạy
|
||||
status: Trạng thái cập nhật (Pending, Processing, Completed, Failed)
|
||||
progress: % Tiến độ hiện tại
|
||||
message: Tin nhắn mô tả ngắn gọn hiện trạng làm gì
|
||||
result: Trả về kết quả đầu ra khi thành công
|
||||
error: Lời nhắn/Exception khi thất bại
|
||||
progress_detail: Các sub-tiến trình chi tiết
|
||||
"""
|
||||
with self._task_lock:
|
||||
task = self._tasks.get(task_id)
|
||||
|
|
@ -143,26 +143,26 @@ class TaskManager:
|
|||
task.progress_detail = progress_detail
|
||||
|
||||
def complete_task(self, task_id: str, result: Dict):
|
||||
"""标记任务完成"""
|
||||
"""Hành động đánh dấu tác vụ đã kết thúc Thành Công và gán 100% cho progress"""
|
||||
self.update_task(
|
||||
task_id,
|
||||
status=TaskStatus.COMPLETED,
|
||||
progress=100,
|
||||
message="任务完成",
|
||||
message="The task has been completed!",
|
||||
result=result
|
||||
)
|
||||
|
||||
def fail_task(self, task_id: str, error: str):
|
||||
"""标记任务失败"""
|
||||
"""Hành động đánh dấu tác vụ đã Thất Bại do lỗi"""
|
||||
self.update_task(
|
||||
task_id,
|
||||
status=TaskStatus.FAILED,
|
||||
message="任务失败",
|
||||
message="The task has an error!?",
|
||||
error=error
|
||||
)
|
||||
|
||||
def list_tasks(self, task_type: Optional[str] = None) -> list:
|
||||
"""列出任务"""
|
||||
"""Liệt kê danh sách tất cả các Task (Hoặc filter theo type của task)"""
|
||||
with self._task_lock:
|
||||
tasks = list(self._tasks.values())
|
||||
if task_type:
|
||||
|
|
@ -170,7 +170,7 @@ class TaskManager:
|
|||
return [t.to_dict() for t in sorted(tasks, key=lambda x: x.created_at, reverse=True)]
|
||||
|
||||
def cleanup_old_tasks(self, max_age_hours: int = 24):
|
||||
"""清理旧任务"""
|
||||
"""Dọn dẹp/xoá khỏi bộ nhớ các Task đã cũ (Đã hoàn thành hoặc lỗi sau N giờ) để tránh rò rỉ hoặc xài tốn RAM"""
|
||||
from datetime import timedelta
|
||||
cutoff = datetime.now() - timedelta(hours=max_age_hours)
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
services:
|
||||
mirofish:
|
||||
image: ghcr.io/666ghj/mirofish:latest
|
||||
# 加速镜像(如拉取缓慢可替换上方地址)
|
||||
# Mirror tăng tốc (nếu kéo image chậm có thể thay địa chỉ ở trên)
|
||||
# image: ghcr.nju.edu.cn/666ghj/mirofish:latest
|
||||
container_name: mirofish
|
||||
env_file:
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@
|
|||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" href="/icon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="MiroFish - 社交媒体舆论模拟系统" />
|
||||
<title>MiroFish - 预测万物</title>
|
||||
<meta name="description" content="MiroFish - Social Media Public Opinion Simulation System" />
|
||||
<title>MiroFish - Predicting everything</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -3,11 +3,11 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
// 使用 Vue Router 来管理页面
|
||||
// Sử dụng Vue Router để quản lý trang
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* 全局样式重置 */
|
||||
/* Reset style toàn cục */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
/* Style thanh cuộn */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
|
|
@ -40,7 +40,7 @@
|
|||
background: #333333;
|
||||
}
|
||||
|
||||
/* 全局按钮样式 */
|
||||
/* Style button toàn cục */
|
||||
button {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import service, { requestWithRetry } from './index'
|
||||
|
||||
/**
|
||||
* 生成本体(上传文档和模拟需求)
|
||||
* @param {Object} data - 包含files, simulation_requirement, project_name等
|
||||
* Tạo ontology (upload file + yêu cầu mô phỏng)
|
||||
* @param {Object} data - Bao gồm files, simulation_requirement, project_name...
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function generateOntology(formData) {
|
||||
|
|
@ -19,8 +19,8 @@ export function generateOntology(formData) {
|
|||
}
|
||||
|
||||
/**
|
||||
* 构建图谱
|
||||
* @param {Object} data - 包含project_id, graph_name等
|
||||
* Xây dựng graph
|
||||
* @param {Object} data - Bao gồm project_id, graph_name...
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function buildGraph(data) {
|
||||
|
|
@ -34,8 +34,8 @@ export function buildGraph(data) {
|
|||
}
|
||||
|
||||
/**
|
||||
* 查询任务状态
|
||||
* @param {String} taskId - 任务ID
|
||||
* Lấy trạng thái task
|
||||
* @param {String} taskId - ID của task
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getTaskStatus(taskId) {
|
||||
|
|
@ -46,8 +46,8 @@ export function getTaskStatus(taskId) {
|
|||
}
|
||||
|
||||
/**
|
||||
* 获取图谱数据
|
||||
* @param {String} graphId - 图谱ID
|
||||
* Lấy dữ liệu graph
|
||||
* @param {String} graphId - ID của graph
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getGraphData(graphId) {
|
||||
|
|
@ -58,8 +58,8 @@ export function getGraphData(graphId) {
|
|||
}
|
||||
|
||||
/**
|
||||
* 获取项目信息
|
||||
* @param {String} projectId - 项目ID
|
||||
* Lấy thông tin project
|
||||
* @param {String} projectId - ID của project
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getProject(projectId) {
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
import axios from 'axios'
|
||||
|
||||
// 创建axios实例
|
||||
// Tạo instance axios
|
||||
const service = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:5001',
|
||||
timeout: 300000, // 5分钟超时(本体生成可能需要较长时间)
|
||||
timeout: 300000, // Thời gian timeout 5 phút (việc tạo nội dung có thể mất nhiều thời gian)
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
// 请求拦截器
|
||||
// Interceptor request
|
||||
service.interceptors.request.use(
|
||||
config => {
|
||||
return config
|
||||
|
|
@ -20,12 +20,12 @@ service.interceptors.request.use(
|
|||
}
|
||||
)
|
||||
|
||||
// 响应拦截器(容错重试机制)
|
||||
// Interceptor response (cơ chế retry chịu lỗi)
|
||||
service.interceptors.response.use(
|
||||
response => {
|
||||
const res = response.data
|
||||
|
||||
// 如果返回的状态码不是success,则抛出错误
|
||||
// Nếu mã trạng thái trả về không phải success thì ném lỗi
|
||||
if (!res.success && res.success !== undefined) {
|
||||
console.error('API Error:', res.error || res.message || 'Unknown error')
|
||||
return Promise.reject(new Error(res.error || res.message || 'Error'))
|
||||
|
|
@ -36,12 +36,12 @@ service.interceptors.response.use(
|
|||
error => {
|
||||
console.error('Response error:', error)
|
||||
|
||||
// 处理超时
|
||||
// Xử lý timeout
|
||||
if (error.code === 'ECONNABORTED' && error.message.includes('timeout')) {
|
||||
console.error('Request timeout')
|
||||
}
|
||||
|
||||
// 处理网络错误
|
||||
// Xử lý lỗi mạng
|
||||
if (error.message === 'Network Error') {
|
||||
console.error('Network error - please check your connection')
|
||||
}
|
||||
|
|
@ -50,7 +50,7 @@ service.interceptors.response.use(
|
|||
}
|
||||
)
|
||||
|
||||
// 带重试的请求函数
|
||||
// Hàm request có retry
|
||||
export const requestWithRetry = async (requestFn, maxRetries = 3, delay = 1000) => {
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import service, { requestWithRetry } from './index'
|
||||
|
||||
/**
|
||||
* 开始报告生成
|
||||
* Bắt đầu tạo báo cáo
|
||||
* @param {Object} data - { simulation_id, force_regenerate? }
|
||||
*/
|
||||
export const generateReport = (data) => {
|
||||
|
|
@ -9,7 +9,7 @@ export const generateReport = (data) => {
|
|||
}
|
||||
|
||||
/**
|
||||
* 获取报告生成状态
|
||||
* Lấy trạng thái tạo báo cáo
|
||||
* @param {string} reportId
|
||||
*/
|
||||
export const getReportStatus = (reportId) => {
|
||||
|
|
@ -17,25 +17,25 @@ export const getReportStatus = (reportId) => {
|
|||
}
|
||||
|
||||
/**
|
||||
* 获取 Agent 日志(增量)
|
||||
* Lấy log Agent (dạng tăng dần)
|
||||
* @param {string} reportId
|
||||
* @param {number} fromLine - 从第几行开始获取
|
||||
* @param {number} fromLine - Lấy từ dòng thứ bao nhiêu
|
||||
*/
|
||||
export const getAgentLog = (reportId, fromLine = 0) => {
|
||||
return service.get(`/api/report/${reportId}/agent-log`, { params: { from_line: fromLine } })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取控制台日志(增量)
|
||||
* Lấy log console (dạng tăng dần)
|
||||
* @param {string} reportId
|
||||
* @param {number} fromLine - 从第几行开始获取
|
||||
* @param {number} fromLine - Lấy từ dòng thứ bao nhiêu
|
||||
*/
|
||||
export const getConsoleLog = (reportId, fromLine = 0) => {
|
||||
return service.get(`/api/report/${reportId}/console-log`, { params: { from_line: fromLine } })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取报告详情
|
||||
* Lấy chi tiết báo cáo
|
||||
* @param {string} reportId
|
||||
*/
|
||||
export const getReport = (reportId) => {
|
||||
|
|
@ -43,7 +43,7 @@ export const getReport = (reportId) => {
|
|||
}
|
||||
|
||||
/**
|
||||
* 与 Report Agent 对话
|
||||
* Chat với Report Agent
|
||||
* @param {Object} data - { simulation_id, message, chat_history? }
|
||||
*/
|
||||
export const chatWithReport = (data) => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import service, { requestWithRetry } from './index'
|
||||
|
||||
/**
|
||||
* 创建模拟
|
||||
* Tạo simulation
|
||||
* @param {Object} data - { project_id, graph_id?, enable_twitter?, enable_reddit? }
|
||||
*/
|
||||
export const createSimulation = (data) => {
|
||||
|
|
@ -9,7 +9,7 @@ export const createSimulation = (data) => {
|
|||
}
|
||||
|
||||
/**
|
||||
* 准备模拟环境(异步任务)
|
||||
* Chuẩn bị môi trường simulation (task bất đồng bộ)
|
||||
* @param {Object} data - { simulation_id, entity_types?, use_llm_for_profiles?, parallel_profile_count?, force_regenerate? }
|
||||
*/
|
||||
export const prepareSimulation = (data) => {
|
||||
|
|
@ -17,7 +17,7 @@ export const prepareSimulation = (data) => {
|
|||
}
|
||||
|
||||
/**
|
||||
* 查询准备任务进度
|
||||
* Kiểm tra tiến độ task chuẩn bị
|
||||
* @param {Object} data - { task_id?, simulation_id? }
|
||||
*/
|
||||
export const getPrepareStatus = (data) => {
|
||||
|
|
@ -25,7 +25,7 @@ export const getPrepareStatus = (data) => {
|
|||
}
|
||||
|
||||
/**
|
||||
* 获取模拟状态
|
||||
* Lấy trạng thái simulation
|
||||
* @param {string} simulationId
|
||||
*/
|
||||
export const getSimulation = (simulationId) => {
|
||||
|
|
@ -33,7 +33,7 @@ export const getSimulation = (simulationId) => {
|
|||
}
|
||||
|
||||
/**
|
||||
* 获取模拟的 Agent Profiles
|
||||
* Lấy Agent Profiles của simulation
|
||||
* @param {string} simulationId
|
||||
* @param {string} platform - 'reddit' | 'twitter'
|
||||
*/
|
||||
|
|
@ -42,7 +42,7 @@ export const getSimulationProfiles = (simulationId, platform = 'reddit') => {
|
|||
}
|
||||
|
||||
/**
|
||||
* 实时获取生成中的 Agent Profiles
|
||||
* Lấy Agent Profiles đang được tạo (realtime)
|
||||
* @param {string} simulationId
|
||||
* @param {string} platform - 'reddit' | 'twitter'
|
||||
*/
|
||||
|
|
@ -51,7 +51,7 @@ export const getSimulationProfilesRealtime = (simulationId, platform = 'reddit')
|
|||
}
|
||||
|
||||
/**
|
||||
* 获取模拟配置
|
||||
* Lấy cấu hình simulation
|
||||
* @param {string} simulationId
|
||||
*/
|
||||
export const getSimulationConfig = (simulationId) => {
|
||||
|
|
@ -59,17 +59,17 @@ export const getSimulationConfig = (simulationId) => {
|
|||
}
|
||||
|
||||
/**
|
||||
* 实时获取生成中的模拟配置
|
||||
* Lấy cấu hình simulation đang được tạo (realtime)
|
||||
* @param {string} simulationId
|
||||
* @returns {Promise} 返回配置信息,包含元数据和配置内容
|
||||
* @returns {Promise} Trả về thông tin cấu hình, bao gồm metadata và nội dung cấu hình
|
||||
*/
|
||||
export const getSimulationConfigRealtime = (simulationId) => {
|
||||
return service.get(`/api/simulation/${simulationId}/config/realtime`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出所有模拟
|
||||
* @param {string} projectId - 可选,按项目ID过滤
|
||||
* Liệt kê tất cả simulation
|
||||
* @param {string} projectId - Tùy chọn, lọc theo project ID
|
||||
*/
|
||||
export const listSimulations = (projectId) => {
|
||||
const params = projectId ? { project_id: projectId } : {}
|
||||
|
|
@ -77,7 +77,7 @@ export const listSimulations = (projectId) => {
|
|||
}
|
||||
|
||||
/**
|
||||
* 启动模拟
|
||||
* Bắt đầu simulation
|
||||
* @param {Object} data - { simulation_id, platform?, max_rounds?, enable_graph_memory_update? }
|
||||
*/
|
||||
export const startSimulation = (data) => {
|
||||
|
|
@ -85,7 +85,7 @@ export const startSimulation = (data) => {
|
|||
}
|
||||
|
||||
/**
|
||||
* 停止模拟
|
||||
* Dừng simulation
|
||||
* @param {Object} data - { simulation_id }
|
||||
*/
|
||||
export const stopSimulation = (data) => {
|
||||
|
|
@ -93,7 +93,7 @@ export const stopSimulation = (data) => {
|
|||
}
|
||||
|
||||
/**
|
||||
* 获取模拟运行实时状态
|
||||
* Lấy trạng thái chạy realtime của simulation
|
||||
* @param {string} simulationId
|
||||
*/
|
||||
export const getRunStatus = (simulationId) => {
|
||||
|
|
@ -101,7 +101,7 @@ export const getRunStatus = (simulationId) => {
|
|||
}
|
||||
|
||||
/**
|
||||
* 获取模拟运行详细状态(包含最近动作)
|
||||
* Lấy trạng thái chạy chi tiết (bao gồm hành động gần nhất)
|
||||
* @param {string} simulationId
|
||||
*/
|
||||
export const getRunStatusDetail = (simulationId) => {
|
||||
|
|
@ -109,11 +109,11 @@ export const getRunStatusDetail = (simulationId) => {
|
|||
}
|
||||
|
||||
/**
|
||||
* 获取模拟中的帖子
|
||||
* Lấy danh sách bài post trong simulation
|
||||
* @param {string} simulationId
|
||||
* @param {string} platform - 'reddit' | 'twitter'
|
||||
* @param {number} limit - 返回数量
|
||||
* @param {number} offset - 偏移量
|
||||
* @param {number} limit - Số lượng trả về
|
||||
* @param {number} offset - Độ lệch
|
||||
*/
|
||||
export const getSimulationPosts = (simulationId, platform = 'reddit', limit = 50, offset = 0) => {
|
||||
return service.get(`/api/simulation/${simulationId}/posts`, {
|
||||
|
|
@ -122,10 +122,10 @@ export const getSimulationPosts = (simulationId, platform = 'reddit', limit = 50
|
|||
}
|
||||
|
||||
/**
|
||||
* 获取模拟时间线(按轮次汇总)
|
||||
* Lấy timeline simulation (tổng hợp theo từng round)
|
||||
* @param {string} simulationId
|
||||
* @param {number} startRound - 起始轮次
|
||||
* @param {number} endRound - 结束轮次
|
||||
* @param {number} startRound - Round bắt đầu
|
||||
* @param {number} endRound - Round kết thúc
|
||||
*/
|
||||
export const getSimulationTimeline = (simulationId, startRound = 0, endRound = null) => {
|
||||
const params = { start_round: startRound }
|
||||
|
|
@ -136,7 +136,7 @@ export const getSimulationTimeline = (simulationId, startRound = 0, endRound = n
|
|||
}
|
||||
|
||||
/**
|
||||
* 获取Agent统计信息
|
||||
* Lấy thống kê Agent
|
||||
* @param {string} simulationId
|
||||
*/
|
||||
export const getAgentStats = (simulationId) => {
|
||||
|
|
@ -144,7 +144,7 @@ export const getAgentStats = (simulationId) => {
|
|||
}
|
||||
|
||||
/**
|
||||
* 获取模拟动作历史
|
||||
* Lấy lịch sử hành động simulation
|
||||
* @param {string} simulationId
|
||||
* @param {Object} params - { limit, offset, platform, agent_id, round_num }
|
||||
*/
|
||||
|
|
@ -153,7 +153,7 @@ export const getSimulationActions = (simulationId, params = {}) => {
|
|||
}
|
||||
|
||||
/**
|
||||
* 关闭模拟环境(优雅退出)
|
||||
* Đóng môi trường simulation (thoát an toàn)
|
||||
* @param {Object} data - { simulation_id, timeout? }
|
||||
*/
|
||||
export const closeSimulationEnv = (data) => {
|
||||
|
|
@ -161,7 +161,7 @@ export const closeSimulationEnv = (data) => {
|
|||
}
|
||||
|
||||
/**
|
||||
* 获取模拟环境状态
|
||||
* Lấy trạng thái môi trường simulation
|
||||
* @param {Object} data - { simulation_id }
|
||||
*/
|
||||
export const getEnvStatus = (data) => {
|
||||
|
|
@ -169,7 +169,7 @@ export const getEnvStatus = (data) => {
|
|||
}
|
||||
|
||||
/**
|
||||
* 批量采访 Agent
|
||||
* Phỏng vấn hàng loạt Agent
|
||||
* @param {Object} data - { simulation_id, interviews: [{ agent_id, prompt }] }
|
||||
*/
|
||||
export const interviewAgents = (data) => {
|
||||
|
|
@ -177,11 +177,10 @@ export const interviewAgents = (data) => {
|
|||
}
|
||||
|
||||
/**
|
||||
* 获取历史模拟列表(带项目详情)
|
||||
* 用于首页历史项目展示
|
||||
* @param {number} limit - 返回数量限制
|
||||
* Lấy lịch sử simulation (kèm thông tin project)
|
||||
* Dùng cho hiển thị trang chủ
|
||||
* @param {number} limit - Giới hạn số lượng trả về
|
||||
*/
|
||||
export const getSimulationHistory = (limit = 20) => {
|
||||
return service.get('/api/simulation/history', { params: { limit } })
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,24 +2,24 @@
|
|||
<div class="graph-panel">
|
||||
<div class="panel-header">
|
||||
<span class="panel-title">Graph Relationship Visualization</span>
|
||||
<!-- 顶部工具栏 (Internal Top Right) -->
|
||||
<!-- Thanh công cụ phía trên (bên phải nội bộ) -->
|
||||
<div class="header-tools">
|
||||
<button class="tool-btn" @click="$emit('refresh')" :disabled="loading" title="刷新图谱">
|
||||
<button class="tool-btn" @click="$emit('refresh')" :disabled="loading" title="Refresh graph">
|
||||
<span class="icon-refresh" :class="{ 'spinning': loading }">↻</span>
|
||||
<span class="btn-text">Refresh</span>
|
||||
</button>
|
||||
<button class="tool-btn" @click="$emit('toggle-maximize')" title="最大化/还原">
|
||||
<button class="tool-btn" @click="$emit('toggle-maximize')" title="Maximize/Restore">
|
||||
<span class="icon-maximize">⛶</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="graph-container" ref="graphContainer">
|
||||
<!-- 图谱可视化 -->
|
||||
<!-- Visualization of graph -->
|
||||
<div v-if="graphData" class="graph-view">
|
||||
<svg ref="graphSvg" class="graph-svg"></svg>
|
||||
|
||||
<!-- 构建中/模拟中提示 -->
|
||||
<!-- Hint when building/simulating -->
|
||||
<div v-if="currentPhase === 1 || isSimulating" class="graph-building-hint">
|
||||
<div class="memory-icon-wrapper">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="memory-icon">
|
||||
|
|
@ -27,10 +27,10 @@
|
|||
<path d="M14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 4.96.44 2.5 2.5 0 0 0 2.96-3.08 3 3 0 0 0 .34-5.58 2.5 2.5 0 0 0-1.32-4.24 2.5 2.5 0 0 0-4.44-4.04z" />
|
||||
</svg>
|
||||
</div>
|
||||
{{ isSimulating ? 'GraphRAG长短期记忆实时更新中' : '实时更新中...' }}
|
||||
{{ isSimulating ? 'GraphRAG long-term and short-term memory updating in real time' : 'Updating in real time...' }}
|
||||
</div>
|
||||
|
||||
<!-- 模拟结束后的提示 -->
|
||||
<!-- Hint after simulation finished -->
|
||||
<div v-if="showSimulationFinishedHint" class="graph-building-hint finished-hint">
|
||||
<div class="hint-icon-wrapper">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="hint-icon">
|
||||
|
|
@ -39,8 +39,8 @@
|
|||
<line x1="12" y1="8" x2="12.01" y2="8"></line>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="hint-text">还有少量内容处理中,建议稍后手动刷新图谱</span>
|
||||
<button class="hint-close-btn" @click="dismissFinishedHint" title="关闭提示">
|
||||
<span class="hint-text">Some content is still being processed, it is recommended to manually refresh the graph later</span>
|
||||
<button class="hint-close-btn" @click="dismissFinishedHint" title="Close hint">
|
||||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
|
|
@ -48,7 +48,7 @@
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 节点/边详情面板 -->
|
||||
<!-- Panel chi tiết node/cạnh -->
|
||||
<div v-if="selectedItem" class="detail-panel">
|
||||
<div class="detail-panel-header">
|
||||
<span class="detail-title">{{ selectedItem.type === 'node' ? 'Node Details' : 'Relationship' }}</span>
|
||||
|
|
@ -58,7 +58,7 @@
|
|||
<button class="detail-close" @click="closeDetailPanel">×</button>
|
||||
</div>
|
||||
|
||||
<!-- 节点详情 -->
|
||||
<!-- Chi tiết node -->
|
||||
<div v-if="selectedItem.type === 'node'" class="detail-content">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Name:</span>
|
||||
|
|
@ -101,9 +101,9 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 边详情 -->
|
||||
<!-- Chi tiết cạnh -->
|
||||
<div v-else class="detail-content">
|
||||
<!-- 自环组详情 -->
|
||||
<!-- Chi tiết nhóm self-loop -->
|
||||
<template v-if="selectedItem.data.isSelfLoopGroup">
|
||||
<div class="edge-relation-header self-loop-header">
|
||||
{{ selectedItem.data.source_name }} - Self Relations
|
||||
|
|
@ -154,7 +154,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 普通边详情 -->
|
||||
<!-- Chi tiết cạnh thông thường -->
|
||||
<template v-else>
|
||||
<div class="edge-relation-header">
|
||||
{{ selectedItem.data.source_name }} → {{ selectedItem.data.name || 'RELATED_TO' }} → {{ selectedItem.data.target_name }}
|
||||
|
|
@ -200,20 +200,20 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<!-- Trạng thái loading -->
|
||||
<div v-else-if="loading" class="graph-state">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>图谱数据加载中...</p>
|
||||
<p>Graph data is loading...</p>
|
||||
</div>
|
||||
|
||||
<!-- 等待/空状态 -->
|
||||
<!-- Trạng thái chờ/rỗng -->
|
||||
<div v-else class="graph-state">
|
||||
<div class="empty-icon">❖</div>
|
||||
<p class="empty-text">等待本体生成...</p>
|
||||
<p class="empty-text">Waiting for ontology generation...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部图例 (Bottom Left) -->
|
||||
<!-- Chú giải phía dưới (bên trái) -->
|
||||
<div v-if="graphData && entityTypes.length" class="graph-legend">
|
||||
<span class="legend-title">Entity Types</span>
|
||||
<div class="legend-items">
|
||||
|
|
@ -224,7 +224,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 显示边标签开关 -->
|
||||
<!-- Công tắc hiển thị nhãn cạnh -->
|
||||
<div v-if="graphData" class="edge-labels-toggle">
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" v-model="showEdgeLabels" />
|
||||
|
|
@ -251,26 +251,26 @@ const emit = defineEmits(['refresh', 'toggle-maximize'])
|
|||
const graphContainer = ref(null)
|
||||
const graphSvg = ref(null)
|
||||
const selectedItem = ref(null)
|
||||
const showEdgeLabels = ref(true) // 默认显示边标签
|
||||
const expandedSelfLoops = ref(new Set()) // 展开的自环项
|
||||
const showSimulationFinishedHint = ref(false) // 模拟结束后的提示
|
||||
const wasSimulating = ref(false) // 追踪之前是否在模拟中
|
||||
const showEdgeLabels = ref(true) // Mặc định hiển thị nhãn cạnh
|
||||
const expandedSelfLoops = ref(new Set()) // Các self-loop đang mở
|
||||
const showSimulationFinishedHint = ref(false) // Thông báo sau khi mô phỏng kết thúc
|
||||
const wasSimulating = ref(false) // Theo dõi trước đó có đang mô phỏng hay không
|
||||
|
||||
// 关闭模拟结束提示
|
||||
// Đóng thông báo kết thúc mô phỏng
|
||||
const dismissFinishedHint = () => {
|
||||
showSimulationFinishedHint.value = false
|
||||
}
|
||||
|
||||
// 监听 isSimulating 变化,检测模拟结束
|
||||
// Theo dõi thay đổi isSimulating để phát hiện kết thúc mô phỏng
|
||||
watch(() => props.isSimulating, (newValue, oldValue) => {
|
||||
if (wasSimulating.value && !newValue) {
|
||||
// 从模拟中变为非模拟状态,显示结束提示
|
||||
// Từ trạng thái mô phỏng chuyển sang không mô phỏng, hiển thị thông báo kết thúc
|
||||
showSimulationFinishedHint.value = true
|
||||
}
|
||||
wasSimulating.value = newValue
|
||||
}, { immediate: true })
|
||||
|
||||
// 切换自环项展开/折叠状态
|
||||
// Toggle trạng thái mở/đóng của self-loop
|
||||
const toggleSelfLoop = (id) => {
|
||||
const newSet = new Set(expandedSelfLoops.value)
|
||||
if (newSet.has(id)) {
|
||||
|
|
@ -281,11 +281,11 @@ const toggleSelfLoop = (id) => {
|
|||
expandedSelfLoops.value = newSet
|
||||
}
|
||||
|
||||
// 计算实体类型用于图例
|
||||
// Tính toán loại entity để hiển thị legend
|
||||
const entityTypes = computed(() => {
|
||||
if (!props.graphData?.nodes) return []
|
||||
const typeMap = {}
|
||||
// 美观的颜色调色板
|
||||
// Bảng màu đẹp
|
||||
const colors = ['#FF6B35', '#004E89', '#7B2D8E', '#1A936F', '#C5283D', '#E9724C', '#3498db', '#9b59b6', '#27ae60', '#f39c12']
|
||||
|
||||
props.graphData.nodes.forEach(node => {
|
||||
|
|
@ -298,7 +298,7 @@ const entityTypes = computed(() => {
|
|||
return Object.values(typeMap)
|
||||
})
|
||||
|
||||
// 格式化时间
|
||||
// Format thời gian
|
||||
const formatDateTime = (dateStr) => {
|
||||
if (!dateStr) return ''
|
||||
try {
|
||||
|
|
@ -318,7 +318,7 @@ const formatDateTime = (dateStr) => {
|
|||
|
||||
const closeDetailPanel = () => {
|
||||
selectedItem.value = null
|
||||
expandedSelfLoops.value = new Set() // 重置展开状态
|
||||
expandedSelfLoops.value = new Set() // Reset trạng thái mở
|
||||
}
|
||||
|
||||
let currentSimulation = null
|
||||
|
|
@ -328,7 +328,7 @@ let linkLabelBgRef = null
|
|||
const renderGraph = () => {
|
||||
if (!graphSvg.value || !props.graphData) return
|
||||
|
||||
// 停止之前的仿真
|
||||
// Dừng simulation trước đó
|
||||
if (currentSimulation) {
|
||||
currentSimulation.stop()
|
||||
}
|
||||
|
|
@ -349,7 +349,7 @@ const renderGraph = () => {
|
|||
|
||||
if (nodesData.length === 0) return
|
||||
|
||||
// Prep data
|
||||
// Chuẩn bị dữ liệu
|
||||
const nodeMap = {}
|
||||
nodesData.forEach(n => nodeMap[n.uuid] = n)
|
||||
|
||||
|
|
@ -362,16 +362,16 @@ const renderGraph = () => {
|
|||
|
||||
const nodeIds = new Set(nodes.map(n => n.id))
|
||||
|
||||
// 处理边数据,计算同一对节点间的边数量和索引
|
||||
// Xử lý dữ liệu cạnh, tính số lượng và chỉ số cạnh giữa cùng một cặp node
|
||||
const edgePairCount = {}
|
||||
const selfLoopEdges = {} // 按节点分组的自环边
|
||||
const selfLoopEdges = {} // Self-loop được nhóm theo node
|
||||
const tempEdges = edgesData
|
||||
.filter(e => nodeIds.has(e.source_node_uuid) && nodeIds.has(e.target_node_uuid))
|
||||
|
||||
// 统计每对节点之间的边数量,收集自环边
|
||||
// Thống kê số cạnh giữa mỗi cặp node, thu thập self-loop
|
||||
tempEdges.forEach(e => {
|
||||
if (e.source_node_uuid === e.target_node_uuid) {
|
||||
// 自环 - 收集到数组中
|
||||
// Self-loop - gom vào mảng
|
||||
if (!selfLoopEdges[e.source_node_uuid]) {
|
||||
selfLoopEdges[e.source_node_uuid] = []
|
||||
}
|
||||
|
|
@ -386,9 +386,9 @@ const renderGraph = () => {
|
|||
}
|
||||
})
|
||||
|
||||
// 记录当前处理到每对节点的第几条边
|
||||
// Ghi nhận đang xử lý cạnh thứ mấy của mỗi cặp node
|
||||
const edgePairIndex = {}
|
||||
const processedSelfLoopNodes = new Set() // 已处理的自环节点
|
||||
const processedSelfLoopNodes = new Set() // Các node self-loop đã xử lý
|
||||
|
||||
const edges = []
|
||||
|
||||
|
|
@ -396,9 +396,9 @@ const renderGraph = () => {
|
|||
const isSelfLoop = e.source_node_uuid === e.target_node_uuid
|
||||
|
||||
if (isSelfLoop) {
|
||||
// 自环边 - 每个节点只添加一条合并的自环
|
||||
// Self-loop - mỗi node chỉ thêm một self-loop tổng hợp
|
||||
if (processedSelfLoopNodes.has(e.source_node_uuid)) {
|
||||
return // 已处理过,跳过
|
||||
return // Đã xử lý rồi, bỏ qua
|
||||
}
|
||||
processedSelfLoopNodes.add(e.source_node_uuid)
|
||||
|
||||
|
|
@ -417,7 +417,7 @@ const renderGraph = () => {
|
|||
source_name: nodeName,
|
||||
target_name: nodeName,
|
||||
selfLoopCount: allSelfLoops.length,
|
||||
selfLoopEdges: allSelfLoops // 存储所有自环边的详细信息
|
||||
selfLoopEdges: allSelfLoops // Lưu toàn bộ thông tin chi tiết của self-loop
|
||||
}
|
||||
})
|
||||
return
|
||||
|
|
@ -428,19 +428,19 @@ const renderGraph = () => {
|
|||
const currentIndex = edgePairIndex[pairKey] || 0
|
||||
edgePairIndex[pairKey] = currentIndex + 1
|
||||
|
||||
// 判断边的方向是否与标准化方向一致(源UUID < 目标UUID)
|
||||
// Kiểm tra hướng cạnh có trùng với hướng chuẩn hay không (source UUID < target UUID)
|
||||
const isReversed = e.source_node_uuid > e.target_node_uuid
|
||||
|
||||
// 计算曲率:多条边时分散开,单条边为直线
|
||||
// Tính độ cong: nhiều cạnh thì tách ra, một cạnh thì là đường thẳng
|
||||
let curvature = 0
|
||||
if (totalCount > 1) {
|
||||
// 均匀分布曲率,确保明显区分
|
||||
// 曲率范围根据边数量增加,边越多曲率范围越大
|
||||
// Phân bố độ cong đồng đều, đảm bảo dễ phân biệt
|
||||
// Phạm vi độ cong tăng theo số lượng cạnh, càng nhiều cạnh thì phạm vi càng lớn
|
||||
const curvatureRange = Math.min(1.2, 0.6 + totalCount * 0.15)
|
||||
curvature = ((currentIndex / (totalCount - 1)) - 0.5) * curvatureRange * 2
|
||||
|
||||
// 如果边的方向与标准化方向相反,翻转曲率
|
||||
// 这样确保所有边在同一参考系下分布,不会因方向不同而重叠
|
||||
// Nếu hướng cạnh ngược với hướng chuẩn, đảo độ cong
|
||||
// Nhờ vậy tất cả cạnh được phân bố trong cùng hệ quy chiếu, tránh chồng lấn do khác hướng
|
||||
if (isReversed) {
|
||||
curvature = -curvature
|
||||
}
|
||||
|
|
@ -463,16 +463,16 @@ const renderGraph = () => {
|
|||
})
|
||||
})
|
||||
|
||||
// Color scale
|
||||
// Thang màu
|
||||
const colorMap = {}
|
||||
entityTypes.value.forEach(t => colorMap[t.name] = t.color)
|
||||
const getColor = (type) => colorMap[type] || '#999'
|
||||
|
||||
// Simulation - 根据边数量动态调整节点间距
|
||||
// Simulation - điều chỉnh khoảng cách node theo số lượng cạnh
|
||||
const simulation = d3.forceSimulation(nodes)
|
||||
.force('link', d3.forceLink(edges).id(d => d.id).distance(d => {
|
||||
// 根据这对节点之间的边数量动态调整距离
|
||||
// 基础距离 150,每多一条边增加 40
|
||||
// Điều chỉnh khoảng cách dựa trên số cạnh giữa cặp node này
|
||||
// Khoảng cách cơ bản là 150, mỗi cạnh thêm tăng 50
|
||||
const baseDistance = 150
|
||||
const edgeCount = d.pairTotal || 1
|
||||
return baseDistance + (edgeCount - 1) * 50
|
||||
|
|
@ -480,7 +480,7 @@ const renderGraph = () => {
|
|||
.force('charge', d3.forceManyBody().strength(-400))
|
||||
.force('center', d3.forceCenter(width / 2, height / 2))
|
||||
.force('collide', d3.forceCollide(50))
|
||||
// 添加向中心的引力,让独立的节点群聚集到中心区域
|
||||
// Thêm lực hút về trung tâm để các cụm node độc lập gom lại gần trung tâm
|
||||
.force('x', d3.forceX(width / 2).strength(0.04))
|
||||
.force('y', d3.forceY(height / 2).strength(0.04))
|
||||
|
||||
|
|
@ -488,44 +488,44 @@ const renderGraph = () => {
|
|||
|
||||
const g = svg.append('g')
|
||||
|
||||
// Zoom
|
||||
// Thu phóng
|
||||
svg.call(d3.zoom().extent([[0, 0], [width, height]]).scaleExtent([0.1, 4]).on('zoom', (event) => {
|
||||
g.attr('transform', event.transform)
|
||||
}))
|
||||
|
||||
// Links - 使用 path 支持曲线
|
||||
// Links - dùng path để hỗ trợ đường cong
|
||||
const linkGroup = g.append('g').attr('class', 'links')
|
||||
|
||||
// 计算曲线路径
|
||||
// Tính đường dẫn cho đường cong
|
||||
const getLinkPath = (d) => {
|
||||
const sx = d.source.x, sy = d.source.y
|
||||
const tx = d.target.x, ty = d.target.y
|
||||
|
||||
// 检测自环
|
||||
// Kiểm tra self-loop
|
||||
if (d.isSelfLoop) {
|
||||
// 自环:绘制一个圆弧从节点出发再返回
|
||||
// Self-loop: vẽ một cung tròn đi ra từ node rồi quay lại
|
||||
const loopRadius = 30
|
||||
// 从节点右侧出发,绕一圈回来
|
||||
const x1 = sx + 8 // 起点偏移
|
||||
// Bắt đầu từ bên phải node, đi một vòng rồi quay lại
|
||||
const x1 = sx + 8
|
||||
const y1 = sy - 4
|
||||
const x2 = sx + 8 // 终点偏移
|
||||
const x2 = sx + 8
|
||||
const y2 = sy + 4
|
||||
// 使用圆弧绘制自环(sweep-flag=1 顺时针)
|
||||
// Dùng cung tròn để vẽ self-loop (sweep-flag=1 theo chiều kim đồng hồ)
|
||||
return `M${x1},${y1} A${loopRadius},${loopRadius} 0 1,1 ${x2},${y2}`
|
||||
}
|
||||
|
||||
if (d.curvature === 0) {
|
||||
// 直线
|
||||
// Đường thẳng
|
||||
return `M${sx},${sy} L${tx},${ty}`
|
||||
}
|
||||
|
||||
// 计算曲线控制点 - 根据边数量和距离动态调整
|
||||
// Tính điểm điều khiển của đường cong - điều chỉnh động theo số cạnh và khoảng cách
|
||||
const dx = tx - sx, dy = ty - sy
|
||||
const dist = Math.sqrt(dx * dx + dy * dy)
|
||||
// 垂直于连线方向的偏移,根据距离比例计算,保证曲线明显可见
|
||||
// 边越多,偏移量占距离的比例越大
|
||||
// Độ lệch vuông góc với hướng nối, tính theo tỉ lệ khoảng cách để đảm bảo nhìn rõ
|
||||
// Càng nhiều cạnh, tỉ lệ độ lệch theo khoảng cách càng lớn
|
||||
const pairTotal = d.pairTotal || 1
|
||||
const offsetRatio = 0.25 + pairTotal * 0.05 // 基础25%,每多一条边增加5%
|
||||
const offsetRatio = 0.25 + pairTotal * 0.05 // Giá trị cơ bản là 25%, và nó tăng thêm 5% cho mỗi cạnh bổ sung.
|
||||
const baseOffset = Math.max(35, dist * offsetRatio)
|
||||
const offsetX = -dy / dist * d.curvature * baseOffset
|
||||
const offsetY = dx / dist * d.curvature * baseOffset
|
||||
|
|
@ -535,14 +535,14 @@ const renderGraph = () => {
|
|||
return `M${sx},${sy} Q${cx},${cy} ${tx},${ty}`
|
||||
}
|
||||
|
||||
// 计算曲线中点(用于标签定位)
|
||||
// Tính trung điểm của đường cong (để đặt label)
|
||||
const getLinkMidpoint = (d) => {
|
||||
const sx = d.source.x, sy = d.source.y
|
||||
const tx = d.target.x, ty = d.target.y
|
||||
|
||||
// 检测自环
|
||||
// Kiểm tra self-loop
|
||||
if (d.isSelfLoop) {
|
||||
// 自环标签位置:节点右侧
|
||||
// Vị trí label self-loop: bên phải node
|
||||
return { x: sx + 70, y: sy }
|
||||
}
|
||||
|
||||
|
|
@ -550,7 +550,7 @@ const renderGraph = () => {
|
|||
return { x: (sx + tx) / 2, y: (sy + ty) / 2 }
|
||||
}
|
||||
|
||||
// 二次贝塞尔曲线的中点 t=0.5
|
||||
// Trung điểm của đường Bezier bậc hai tại t=0.5
|
||||
const dx = tx - sx, dy = ty - sy
|
||||
const dist = Math.sqrt(dx * dx + dy * dy)
|
||||
const pairTotal = d.pairTotal || 1
|
||||
|
|
@ -561,7 +561,7 @@ const renderGraph = () => {
|
|||
const cx = (sx + tx) / 2 + offsetX
|
||||
const cy = (sy + ty) / 2 + offsetY
|
||||
|
||||
// 二次贝塞尔曲线公式 B(t) = (1-t)²P0 + 2(1-t)tP1 + t²P2, t=0.5
|
||||
// Công thức Bezier bậc hai B(t) = (1-t)²P0 + 2(1-t)tP1 + t²P2, với t=0.5
|
||||
const midX = 0.25 * sx + 0.5 * cx + 0.25 * tx
|
||||
const midY = 0.25 * sy + 0.5 * cy + 0.25 * ty
|
||||
|
||||
|
|
@ -577,11 +577,11 @@ const renderGraph = () => {
|
|||
.style('cursor', 'pointer')
|
||||
.on('click', (event, d) => {
|
||||
event.stopPropagation()
|
||||
// 重置之前选中边的样式
|
||||
// Reset style cạnh đã chọn trước đó
|
||||
linkGroup.selectAll('path').attr('stroke', '#C0C0C0').attr('stroke-width', 1.5)
|
||||
linkLabelBg.attr('fill', 'rgba(255,255,255,0.95)')
|
||||
linkLabels.attr('fill', '#666')
|
||||
// 高亮当前选中的边
|
||||
// Highlight cạnh đang chọn
|
||||
d3.select(event.target).attr('stroke', '#3498db').attr('stroke-width', 3)
|
||||
|
||||
selectedItem.value = {
|
||||
|
|
@ -590,7 +590,7 @@ const renderGraph = () => {
|
|||
}
|
||||
})
|
||||
|
||||
// Link labels background (白色背景使文字更清晰)
|
||||
// Nền label của cạnh (nền trắng giúp chữ rõ hơn)
|
||||
const linkLabelBg = linkGroup.selectAll('rect')
|
||||
.data(edges)
|
||||
.enter().append('rect')
|
||||
|
|
@ -605,7 +605,7 @@ const renderGraph = () => {
|
|||
linkGroup.selectAll('path').attr('stroke', '#C0C0C0').attr('stroke-width', 1.5)
|
||||
linkLabelBg.attr('fill', 'rgba(255,255,255,0.95)')
|
||||
linkLabels.attr('fill', '#666')
|
||||
// 高亮对应的边
|
||||
// Highlight cạnh tương ứng
|
||||
link.filter(l => l === d).attr('stroke', '#3498db').attr('stroke-width', 3)
|
||||
d3.select(event.target).attr('fill', 'rgba(52, 152, 219, 0.1)')
|
||||
|
||||
|
|
@ -633,7 +633,7 @@ const renderGraph = () => {
|
|||
linkGroup.selectAll('path').attr('stroke', '#C0C0C0').attr('stroke-width', 1.5)
|
||||
linkLabelBg.attr('fill', 'rgba(255,255,255,0.95)')
|
||||
linkLabels.attr('fill', '#666')
|
||||
// 高亮对应的边
|
||||
// Highlight cạnh tương ứng
|
||||
link.filter(l => l === d).attr('stroke', '#3498db').attr('stroke-width', 3)
|
||||
d3.select(event.target).attr('fill', '#3498db')
|
||||
|
||||
|
|
@ -643,14 +643,14 @@ const renderGraph = () => {
|
|||
}
|
||||
})
|
||||
|
||||
// 保存引用供外部控制显隐
|
||||
// Lưu reference để điều khiển hiển thị từ bên ngoài
|
||||
linkLabelsRef = linkLabels
|
||||
linkLabelBgRef = linkLabelBg
|
||||
|
||||
// Nodes group
|
||||
// Nhóm node
|
||||
const nodeGroup = g.append('g').attr('class', 'nodes')
|
||||
|
||||
// Node circles
|
||||
// Hình tròn node
|
||||
const node = nodeGroup.selectAll('circle')
|
||||
.data(nodes)
|
||||
.enter().append('circle')
|
||||
|
|
@ -661,7 +661,7 @@ const renderGraph = () => {
|
|||
.style('cursor', 'pointer')
|
||||
.call(d3.drag()
|
||||
.on('start', (event, d) => {
|
||||
// 只记录位置,不重启仿真(区分点击和拖拽)
|
||||
// Chỉ ghi nhận vị trí, không restart simulation (phân biệt click và drag)
|
||||
d.fx = d.x
|
||||
d.fy = d.y
|
||||
d._dragStartX = event.x
|
||||
|
|
@ -669,13 +669,13 @@ const renderGraph = () => {
|
|||
d._isDragging = false
|
||||
})
|
||||
.on('drag', (event, d) => {
|
||||
// 检测是否真正开始拖拽(移动超过阈值)
|
||||
// Kiểm tra có thực sự bắt đầu drag hay chưa (dịch chuyển vượt ngưỡng)
|
||||
const dx = event.x - d._dragStartX
|
||||
const dy = event.y - d._dragStartY
|
||||
const distance = Math.sqrt(dx * dx + dy * dy)
|
||||
|
||||
if (!d._isDragging && distance > 3) {
|
||||
// 首次检测到真正拖拽,才重启仿真
|
||||
// Chỉ khi thật sự drag mới restart simulation
|
||||
d._isDragging = true
|
||||
simulation.alphaTarget(0.3).restart()
|
||||
}
|
||||
|
|
@ -686,7 +686,7 @@ const renderGraph = () => {
|
|||
}
|
||||
})
|
||||
.on('end', (event, d) => {
|
||||
// 只有真正拖拽过才让仿真逐渐停止
|
||||
// Chỉ khi thực sự drag mới cho simulation giảm dần
|
||||
if (d._isDragging) {
|
||||
simulation.alphaTarget(0)
|
||||
}
|
||||
|
|
@ -697,12 +697,12 @@ const renderGraph = () => {
|
|||
)
|
||||
.on('click', (event, d) => {
|
||||
event.stopPropagation()
|
||||
// 重置所有节点样式
|
||||
// Reset style của tất cả node
|
||||
node.attr('stroke', '#fff').attr('stroke-width', 2.5)
|
||||
linkGroup.selectAll('path').attr('stroke', '#C0C0C0').attr('stroke-width', 1.5)
|
||||
// 高亮选中节点
|
||||
// Highlight node được chọn
|
||||
d3.select(event.target).attr('stroke', '#E91E63').attr('stroke-width', 4)
|
||||
// 高亮与此节点相连的边
|
||||
// Highlight các cạnh nối với node này
|
||||
link.filter(l => l.source.id === d.id || l.target.id === d.id)
|
||||
.attr('stroke', '#E91E63')
|
||||
.attr('stroke-width', 2.5)
|
||||
|
|
@ -725,7 +725,7 @@ const renderGraph = () => {
|
|||
}
|
||||
})
|
||||
|
||||
// Node Labels
|
||||
// Nhãn node
|
||||
const nodeLabels = nodeGroup.selectAll('text')
|
||||
.data(nodes)
|
||||
.enter().append('text')
|
||||
|
|
@ -739,19 +739,19 @@ const renderGraph = () => {
|
|||
.style('font-family', 'system-ui, sans-serif')
|
||||
|
||||
simulation.on('tick', () => {
|
||||
// 更新曲线路径
|
||||
// Cập nhật đường cong
|
||||
link.attr('d', d => getLinkPath(d))
|
||||
|
||||
// 更新边标签位置(无旋转,水平显示更清晰)
|
||||
// Cập nhật vị trí nhãn cạnh (không xoay, hiển thị ngang rõ hơn)
|
||||
linkLabels.each(function(d) {
|
||||
const mid = getLinkMidpoint(d)
|
||||
d3.select(this)
|
||||
.attr('x', mid.x)
|
||||
.attr('y', mid.y)
|
||||
.attr('transform', '') // 移除旋转,保持水平
|
||||
.attr('transform', '') // Loại bỏ chuyển động xoay, giữ nguyên vị trí nằm ngang.
|
||||
})
|
||||
|
||||
// 更新边标签背景
|
||||
// Cập nhật nền nhãn cạnh
|
||||
linkLabelBg.each(function(d, i) {
|
||||
const mid = getLinkMidpoint(d)
|
||||
const textEl = linkLabels.nodes()[i]
|
||||
|
|
@ -761,7 +761,7 @@ const renderGraph = () => {
|
|||
.attr('y', mid.y - bbox.height / 2 - 2)
|
||||
.attr('width', bbox.width + 8)
|
||||
.attr('height', bbox.height + 4)
|
||||
.attr('transform', '') // 移除旋转
|
||||
.attr('transform', '') // Loại bỏ phép xoay
|
||||
})
|
||||
|
||||
node
|
||||
|
|
@ -773,7 +773,7 @@ const renderGraph = () => {
|
|||
.attr('y', d => d.y)
|
||||
})
|
||||
|
||||
// 点击空白处关闭详情面板
|
||||
// Click vào vùng trống để đóng panel chi tiết
|
||||
svg.on('click', () => {
|
||||
selectedItem.value = null
|
||||
node.attr('stroke', '#fff').attr('stroke-width', 2.5)
|
||||
|
|
@ -787,7 +787,7 @@ watch(() => props.graphData, () => {
|
|||
nextTick(renderGraph)
|
||||
}, { deep: true })
|
||||
|
||||
// 监听边标签显示开关
|
||||
// Theo dõi công tắc hiển thị nhãn cạnh
|
||||
watch(showEdgeLabels, (newVal) => {
|
||||
if (linkLabelsRef) {
|
||||
linkLabelsRef.style('display', newVal ? 'block' : 'none')
|
||||
|
|
@ -1250,7 +1250,7 @@ input:checked + .slider:before {
|
|||
50% { opacity: 1; transform: scale(1.15); filter: drop-shadow(0 0 8px rgba(76, 175, 80, 0.6)); }
|
||||
}
|
||||
|
||||
/* 模拟结束后的提示样式 */
|
||||
/* Style thông báo sau khi mô phỏng kết thúc */
|
||||
.graph-building-hint.finished-hint {
|
||||
background: rgba(0, 0, 0, 0.65);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
|
|
|
|||
|
|
@ -4,20 +4,20 @@
|
|||
:class="{ 'no-projects': projects.length === 0 && !loading }"
|
||||
ref="historyContainer"
|
||||
>
|
||||
<!-- 背景装饰:技术网格线(只在有项目时显示) -->
|
||||
<!-- Background decoration: tech grid lines (shown only when projects exist) -->
|
||||
<div v-if="projects.length > 0 || loading" class="tech-grid-bg">
|
||||
<div class="grid-pattern"></div>
|
||||
<div class="gradient-overlay"></div>
|
||||
</div>
|
||||
|
||||
<!-- 标题区域 -->
|
||||
<!-- Title section -->
|
||||
<div class="section-header">
|
||||
<div class="section-line"></div>
|
||||
<span class="section-title">推演记录</span>
|
||||
<span class="section-title">Simulation History</span>
|
||||
<div class="section-line"></div>
|
||||
</div>
|
||||
|
||||
<!-- 卡片容器(只在有项目时显示) -->
|
||||
<!-- Card container (shown only when projects exist) -->
|
||||
<div v-if="projects.length > 0" class="cards-container" :class="{ expanded: isExpanded }" :style="containerStyle">
|
||||
<div
|
||||
v-for="(project, index) in projects"
|
||||
|
|
@ -29,33 +29,33 @@
|
|||
@mouseleave="hoveringCard = null"
|
||||
@click="navigateToProject(project)"
|
||||
>
|
||||
<!-- 卡片头部:simulation_id 和 功能可用状态 -->
|
||||
<!-- Card header: simulation_id and feature availability status -->
|
||||
<div class="card-header">
|
||||
<span class="card-id">{{ formatSimulationId(project.simulation_id) }}</span>
|
||||
<div class="card-status-icons">
|
||||
<span
|
||||
class="status-icon"
|
||||
:class="{ available: project.project_id, unavailable: !project.project_id }"
|
||||
title="图谱构建"
|
||||
title="Graph Build"
|
||||
>◇</span>
|
||||
<span
|
||||
class="status-icon available"
|
||||
title="环境搭建"
|
||||
title="Environment Setup"
|
||||
>◈</span>
|
||||
<span
|
||||
class="status-icon"
|
||||
:class="{ available: project.report_id, unavailable: !project.report_id }"
|
||||
title="分析报告"
|
||||
title="Analysis Report"
|
||||
>◆</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文件列表区域 -->
|
||||
<!-- File list area -->
|
||||
<div class="card-files-wrapper">
|
||||
<!-- 角落装饰 - 取景框风格 -->
|
||||
<!-- Corner decoration - viewfinder style -->
|
||||
<div class="corner-mark top-left-only"></div>
|
||||
|
||||
<!-- 文件列表 -->
|
||||
<!-- File list -->
|
||||
<div class="files-list" v-if="project.files && project.files.length > 0">
|
||||
<div
|
||||
v-for="(file, fileIndex) in project.files.slice(0, 3)"
|
||||
|
|
@ -65,25 +65,25 @@
|
|||
<span class="file-tag" :class="getFileType(file.filename)">{{ getFileTypeLabel(file.filename) }}</span>
|
||||
<span class="file-name">{{ truncateFilename(file.filename, 20) }}</span>
|
||||
</div>
|
||||
<!-- 如果有更多文件,显示提示 -->
|
||||
<!-- Show hint if there are more files -->
|
||||
<div v-if="project.files.length > 3" class="files-more">
|
||||
+{{ project.files.length - 3 }} 个文件
|
||||
+{{ project.files.length - 3 }} files
|
||||
</div>
|
||||
</div>
|
||||
<!-- 无文件时的占位 -->
|
||||
<!-- Placeholder when there are no files -->
|
||||
<div class="files-empty" v-else>
|
||||
<span class="empty-file-icon">◇</span>
|
||||
<span class="empty-file-text">暂无文件</span>
|
||||
<span class="empty-file-text">No files yet</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 卡片标题(使用模拟需求的前20字作为标题) -->
|
||||
<!-- Card title (use first 20 characters of simulation requirement as title) -->
|
||||
<h3 class="card-title">{{ getSimulationTitle(project.simulation_requirement) }}</h3>
|
||||
|
||||
<!-- 卡片描述(模拟需求完整展示) -->
|
||||
<!-- Card description (full simulation requirement display) -->
|
||||
<p class="card-desc">{{ truncateText(project.simulation_requirement, 55) }}</p>
|
||||
|
||||
<!-- 卡片底部 -->
|
||||
<!-- Card footer -->
|
||||
<div class="card-footer">
|
||||
<div class="card-datetime">
|
||||
<span class="card-date">{{ formatDate(project.created_at) }}</span>
|
||||
|
|
@ -94,23 +94,23 @@
|
|||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 底部装饰线 (hover时展开) -->
|
||||
<!-- Bottom decorative line (expands on hover) -->
|
||||
<div class="card-bottom-line"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<!-- Loading state -->
|
||||
<div v-if="loading" class="loading-state">
|
||||
<span class="loading-spinner"></span>
|
||||
<span class="loading-text">加载中...</span>
|
||||
<span class="loading-text">Loading...</span>
|
||||
</div>
|
||||
|
||||
<!-- 历史回放详情弹窗 -->
|
||||
<!-- History playback details modal -->
|
||||
<Teleport to="body">
|
||||
<Transition name="modal">
|
||||
<div v-if="selectedProject" class="modal-overlay" @click.self="closeModal">
|
||||
<div class="modal-content">
|
||||
<!-- 弹窗头部 -->
|
||||
<!-- Modal header -->
|
||||
<div class="modal-header">
|
||||
<div class="modal-title-section">
|
||||
<span class="modal-id">{{ formatSimulationId(selectedProject.simulation_id) }}</span>
|
||||
|
|
@ -122,35 +122,35 @@
|
|||
<button class="modal-close" @click="closeModal">×</button>
|
||||
</div>
|
||||
|
||||
<!-- 弹窗内容 -->
|
||||
<!-- Modal content -->
|
||||
<div class="modal-body">
|
||||
<!-- 模拟需求 -->
|
||||
<!-- Simulation requirement -->
|
||||
<div class="modal-section">
|
||||
<div class="modal-label">模拟需求</div>
|
||||
<div class="modal-requirement">{{ selectedProject.simulation_requirement || '无' }}</div>
|
||||
<div class="modal-label">Simulation Requirement</div>
|
||||
<div class="modal-requirement">{{ selectedProject.simulation_requirement || 'None' }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 文件列表 -->
|
||||
<!-- File list -->
|
||||
<div class="modal-section">
|
||||
<div class="modal-label">关联文件</div>
|
||||
<div class="modal-label">Related Files</div>
|
||||
<div class="modal-files" v-if="selectedProject.files && selectedProject.files.length > 0">
|
||||
<div v-for="(file, index) in selectedProject.files" :key="index" class="modal-file-item">
|
||||
<span class="file-tag" :class="getFileType(file.filename)">{{ getFileTypeLabel(file.filename) }}</span>
|
||||
<span class="modal-file-name">{{ file.filename }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-empty" v-else>暂无关联文件</div>
|
||||
<div class="modal-empty" v-else>No related files</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 推演回放分割线 -->
|
||||
<!-- Simulation playback divider -->
|
||||
<div class="modal-divider">
|
||||
<span class="divider-line"></span>
|
||||
<span class="divider-text">推演回放</span>
|
||||
<span class="divider-text">Simulation Playback</span>
|
||||
<span class="divider-line"></span>
|
||||
</div>
|
||||
|
||||
<!-- 导航按钮 -->
|
||||
<!-- Navigation buttons -->
|
||||
<div class="modal-actions">
|
||||
<button
|
||||
class="modal-btn btn-project"
|
||||
|
|
@ -159,7 +159,7 @@
|
|||
>
|
||||
<span class="btn-step">Step1</span>
|
||||
<span class="btn-icon">◇</span>
|
||||
<span class="btn-text">图谱构建</span>
|
||||
<span class="btn-text">Graph Build</span>
|
||||
</button>
|
||||
<button
|
||||
class="modal-btn btn-simulation"
|
||||
|
|
@ -167,7 +167,7 @@
|
|||
>
|
||||
<span class="btn-step">Step2</span>
|
||||
<span class="btn-icon">◈</span>
|
||||
<span class="btn-text">环境搭建</span>
|
||||
<span class="btn-text">Environment Setup</span>
|
||||
</button>
|
||||
<button
|
||||
class="modal-btn btn-report"
|
||||
|
|
@ -176,12 +176,12 @@
|
|||
>
|
||||
<span class="btn-step">Step4</span>
|
||||
<span class="btn-icon">◆</span>
|
||||
<span class="btn-text">分析报告</span>
|
||||
<span class="btn-text">Analysis Report</span>
|
||||
</button>
|
||||
</div>
|
||||
<!-- 不可回放提示 -->
|
||||
<!-- Playback unavailable hint -->
|
||||
<div class="modal-playback-hint">
|
||||
<span class="hint-text">Step3「开始模拟」与 Step5「深度互动」需在运行中启动,不支持历史回放</span>
|
||||
<span class="hint-text">Step3 "Start Simulation" and Step5 "Deep Interaction" must be launched during runtime and do not support historical playback</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -198,56 +198,56 @@ import { getSimulationHistory } from '../api/simulation'
|
|||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
// 状态
|
||||
// Trạng thái
|
||||
const projects = ref([])
|
||||
const loading = ref(true)
|
||||
const isExpanded = ref(false)
|
||||
const hoveringCard = ref(null)
|
||||
const historyContainer = ref(null)
|
||||
const selectedProject = ref(null) // 当前选中的项目(用于弹窗)
|
||||
const selectedProject = ref(null) // Dự án hiện được chọn (dùng cho modal)
|
||||
let observer = null
|
||||
let isAnimating = false // 动画锁,防止闪烁
|
||||
let expandDebounceTimer = null // 防抖定时器
|
||||
let pendingState = null // 记录待执行的目标状态
|
||||
let isAnimating = false // Khóa animation, tránh nhấp nháy
|
||||
let expandDebounceTimer = null // Bộ hẹn giờ debounce
|
||||
let pendingState = null // Ghi lại trạng thái đích chờ thực thi
|
||||
|
||||
// 卡片布局配置 - 调整为更宽的比例
|
||||
// Cấu hình layout thẻ - điều chỉnh sang tỷ lệ rộng hơn
|
||||
const CARDS_PER_ROW = 4
|
||||
const CARD_WIDTH = 280
|
||||
const CARD_HEIGHT = 280
|
||||
const CARD_GAP = 24
|
||||
|
||||
// 动态计算容器高度样式
|
||||
// Tính động style chiều cao container
|
||||
const containerStyle = computed(() => {
|
||||
if (!isExpanded.value) {
|
||||
// 折叠态:固定高度
|
||||
// Trạng thái thu gọn: chiều cao cố định
|
||||
return { minHeight: '420px' }
|
||||
}
|
||||
|
||||
// 展开态:根据卡片数量动态计算高度
|
||||
// Trạng thái mở rộng: tính chiều cao động theo số lượng thẻ
|
||||
const total = projects.value.length
|
||||
if (total === 0) {
|
||||
return { minHeight: '280px' }
|
||||
}
|
||||
|
||||
const rows = Math.ceil(total / CARDS_PER_ROW)
|
||||
// 计算实际需要的高度:行数 * 卡片高度 + (行数-1) * 间距 + 少量底部间距
|
||||
// Tính chiều cao thực tế cần dùng: số hàng * chiều cao thẻ + (số hàng-1) * khoảng cách + một ít khoảng đệm dưới
|
||||
const expandedHeight = rows * CARD_HEIGHT + (rows - 1) * CARD_GAP + 10
|
||||
|
||||
return { minHeight: `${expandedHeight}px` }
|
||||
})
|
||||
|
||||
// 获取卡片样式
|
||||
// Lấy style của thẻ
|
||||
const getCardStyle = (index) => {
|
||||
const total = projects.value.length
|
||||
|
||||
if (isExpanded.value) {
|
||||
// 展开态:网格布局
|
||||
// Trạng thái mở rộng: layout dạng lưới
|
||||
const transition = 'transform 700ms cubic-bezier(0.23, 1, 0.32, 1), opacity 700ms cubic-bezier(0.23, 1, 0.32, 1), box-shadow 0.3s ease, border-color 0.3s ease'
|
||||
|
||||
const col = index % CARDS_PER_ROW
|
||||
const row = Math.floor(index / CARDS_PER_ROW)
|
||||
|
||||
// 计算当前行的卡片数量,确保每行居中
|
||||
// Tính số lượng thẻ ở hàng hiện tại để căn giữa từng hàng
|
||||
const currentRowStart = row * CARDS_PER_ROW
|
||||
const currentRowCards = Math.min(CARDS_PER_ROW, total - currentRowStart)
|
||||
|
||||
|
|
@ -257,7 +257,7 @@ const getCardStyle = (index) => {
|
|||
const colInRow = index % CARDS_PER_ROW
|
||||
const x = startX + colInRow * (CARD_WIDTH + CARD_GAP)
|
||||
|
||||
// 向下展开,增加与标题的间距
|
||||
// Mở rộng xuống dưới, tăng khoảng cách với tiêu đề
|
||||
const y = 20 + row * (CARD_HEIGHT + CARD_GAP)
|
||||
|
||||
return {
|
||||
|
|
@ -267,14 +267,14 @@ const getCardStyle = (index) => {
|
|||
transition: transition
|
||||
}
|
||||
} else {
|
||||
// 折叠态:扇形堆叠
|
||||
// Trạng thái thu gọn: xếp chồng hình quạt
|
||||
const transition = 'transform 700ms cubic-bezier(0.23, 1, 0.32, 1), opacity 700ms cubic-bezier(0.23, 1, 0.32, 1), box-shadow 0.3s ease, border-color 0.3s ease'
|
||||
|
||||
const centerIndex = (total - 1) / 2
|
||||
const offset = index - centerIndex
|
||||
|
||||
const x = offset * 35
|
||||
// 调整起始位置,靠近标题但保持适当间距
|
||||
// Điều chỉnh vị trí bắt đầu, gần tiêu đề nhưng vẫn giữ khoảng cách phù hợp
|
||||
const y = 25 + Math.abs(offset) * 8
|
||||
const r = offset * 3
|
||||
const s = 0.95 - Math.abs(offset) * 0.05
|
||||
|
|
@ -288,24 +288,24 @@ const getCardStyle = (index) => {
|
|||
}
|
||||
}
|
||||
|
||||
// 根据轮数进度获取样式类
|
||||
// Lấy class style theo tiến độ số vòng
|
||||
const getProgressClass = (simulation) => {
|
||||
const current = simulation.current_round || 0
|
||||
const total = simulation.total_rounds || 0
|
||||
|
||||
if (total === 0 || current === 0) {
|
||||
// 未开始
|
||||
// Chưa bắt đầu
|
||||
return 'not-started'
|
||||
} else if (current >= total) {
|
||||
// 已完成
|
||||
// Đã hoàn thành
|
||||
return 'completed'
|
||||
} else {
|
||||
// 进行中
|
||||
// Đang thực hiện
|
||||
return 'in-progress'
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化日期(只显示日期部分)
|
||||
// Format ngày tháng (chỉ hiển thị phần ngày)
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr) return ''
|
||||
try {
|
||||
|
|
@ -316,7 +316,7 @@ const formatDate = (dateStr) => {
|
|||
}
|
||||
}
|
||||
|
||||
// 格式化时间(显示时:分)
|
||||
// Format thời gian (hiển thị giờ:phút)
|
||||
const formatTime = (dateStr) => {
|
||||
if (!dateStr) return ''
|
||||
try {
|
||||
|
|
@ -329,35 +329,35 @@ const formatTime = (dateStr) => {
|
|||
}
|
||||
}
|
||||
|
||||
// 截断文本
|
||||
// Cắt ngắn văn bản
|
||||
const truncateText = (text, maxLength) => {
|
||||
if (!text) return ''
|
||||
return text.length > maxLength ? text.slice(0, maxLength) + '...' : text
|
||||
}
|
||||
|
||||
// 从模拟需求生成标题(取前20字)
|
||||
// Tạo tiêu đề từ yêu cầu mô phỏng (lấy 20 ký tự đầu)
|
||||
const getSimulationTitle = (requirement) => {
|
||||
if (!requirement) return '未命名模拟'
|
||||
if (!requirement) return 'Unnamed simulation'
|
||||
const title = requirement.slice(0, 20)
|
||||
return requirement.length > 20 ? title + '...' : title
|
||||
}
|
||||
|
||||
// 格式化 simulation_id 显示(截取前6位)
|
||||
// Format simulation_id để hiển thị (lấy 6 ký tự đầu)
|
||||
const formatSimulationId = (simulationId) => {
|
||||
if (!simulationId) return 'SIM_UNKNOWN'
|
||||
const prefix = simulationId.replace('sim_', '').slice(0, 6)
|
||||
return `SIM_${prefix.toUpperCase()}`
|
||||
}
|
||||
|
||||
// 格式化轮数显示(当前轮/总轮数)
|
||||
// Format hiển thị số vòng (vòng hiện tại/tổng số vòng)
|
||||
const formatRounds = (simulation) => {
|
||||
const current = simulation.current_round || 0
|
||||
const total = simulation.total_rounds || 0
|
||||
if (total === 0) return '未开始'
|
||||
return `${current}/${total} 轮`
|
||||
if (total === 0) return 'Not started'
|
||||
return `${current}/${total} rounds`
|
||||
}
|
||||
|
||||
// 获取文件类型(用于样式)
|
||||
// Lấy loại file (dùng cho style)
|
||||
const getFileType = (filename) => {
|
||||
if (!filename) return 'other'
|
||||
const ext = filename.split('.').pop()?.toLowerCase()
|
||||
|
|
@ -373,16 +373,16 @@ const getFileType = (filename) => {
|
|||
return typeMap[ext] || 'other'
|
||||
}
|
||||
|
||||
// 获取文件类型标签文本
|
||||
// Lấy text nhãn loại file
|
||||
const getFileTypeLabel = (filename) => {
|
||||
if (!filename) return 'FILE'
|
||||
const ext = filename.split('.').pop()?.toUpperCase()
|
||||
return ext || 'FILE'
|
||||
}
|
||||
|
||||
// 截断文件名(保留扩展名)
|
||||
// Cắt ngắn tên file (giữ lại phần mở rộng)
|
||||
const truncateFilename = (filename, maxLength) => {
|
||||
if (!filename) return '未知文件'
|
||||
if (!filename) return 'Unknown file'
|
||||
if (filename.length <= maxLength) return filename
|
||||
|
||||
const ext = filename.includes('.') ? '.' + filename.split('.').pop() : ''
|
||||
|
|
@ -391,17 +391,17 @@ const truncateFilename = (filename, maxLength) => {
|
|||
return truncatedName + ext
|
||||
}
|
||||
|
||||
// 打开项目详情弹窗
|
||||
// Mở modal chi tiết dự án
|
||||
const navigateToProject = (simulation) => {
|
||||
selectedProject.value = simulation
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
// Đóng modal
|
||||
const closeModal = () => {
|
||||
selectedProject.value = null
|
||||
}
|
||||
|
||||
// 导航到图谱构建页面(Project)
|
||||
// Điều hướng đến trang xây dựng graph (Project)
|
||||
const goToProject = () => {
|
||||
if (selectedProject.value?.project_id) {
|
||||
router.push({
|
||||
|
|
@ -412,7 +412,7 @@ const goToProject = () => {
|
|||
}
|
||||
}
|
||||
|
||||
// 导航到环境配置页面(Simulation)
|
||||
// Điều hướng đến trang cấu hình môi trường (Simulation)
|
||||
const goToSimulation = () => {
|
||||
if (selectedProject.value?.simulation_id) {
|
||||
router.push({
|
||||
|
|
@ -423,7 +423,7 @@ const goToSimulation = () => {
|
|||
}
|
||||
}
|
||||
|
||||
// 导航到分析报告页面(Report)
|
||||
// Điều hướng đến trang báo cáo phân tích (Report)
|
||||
const goToReport = () => {
|
||||
if (selectedProject.value?.report_id) {
|
||||
router.push({
|
||||
|
|
@ -434,7 +434,7 @@ const goToReport = () => {
|
|||
}
|
||||
}
|
||||
|
||||
// 加载历史项目
|
||||
// Tải lịch sử dự án
|
||||
const loadHistory = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
|
|
@ -443,14 +443,14 @@ const loadHistory = async () => {
|
|||
projects.value = response.data || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载历史项目失败:', error)
|
||||
console.error('Failed to load history projects:', error)
|
||||
projects.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化 IntersectionObserver
|
||||
// Khởi tạo IntersectionObserver
|
||||
const initObserver = () => {
|
||||
if (observer) {
|
||||
observer.disconnect()
|
||||
|
|
@ -461,47 +461,47 @@ const initObserver = () => {
|
|||
entries.forEach((entry) => {
|
||||
const shouldExpand = entry.isIntersecting
|
||||
|
||||
// 更新待执行的目标状态(无论是否在动画中都要记录最新的目标状态)
|
||||
// Cập nhật trạng thái đích chờ thực thi (dù đang animation vẫn phải ghi lại trạng thái mới nhất)
|
||||
pendingState = shouldExpand
|
||||
|
||||
// 清除之前的防抖定时器(新的滚动意图会覆盖旧的)
|
||||
// Xóa bộ hẹn giờ debounce trước đó (ý định cuộn mới sẽ ghi đè ý định cũ)
|
||||
if (expandDebounceTimer) {
|
||||
clearTimeout(expandDebounceTimer)
|
||||
expandDebounceTimer = null
|
||||
}
|
||||
|
||||
// 如果正在动画中,只记录状态,等动画结束后处理
|
||||
// Nếu đang animation thì chỉ ghi trạng thái, chờ xử lý sau khi animation kết thúc
|
||||
if (isAnimating) return
|
||||
|
||||
// 如果目标状态与当前状态相同,不需要处理
|
||||
// Nếu trạng thái đích trùng với trạng thái hiện tại thì không cần xử lý
|
||||
if (shouldExpand === isExpanded.value) {
|
||||
pendingState = null
|
||||
return
|
||||
}
|
||||
|
||||
// 使用防抖延迟状态切换,防止快速闪烁
|
||||
// 展开时延迟较短(50ms),收起时延迟较长(200ms)以增加稳定性
|
||||
// Dùng debounce để trì hoãn chuyển trạng thái, tránh nhấp nháy nhanh
|
||||
// Khi mở rộng thì delay ngắn hơn (50ms), khi thu gọn delay dài hơn (200ms) để ổn định hơn
|
||||
const delay = shouldExpand ? 50 : 200
|
||||
|
||||
expandDebounceTimer = setTimeout(() => {
|
||||
// 检查是否正在动画
|
||||
// Kiểm tra có đang animation không
|
||||
if (isAnimating) return
|
||||
|
||||
// 检查待执行状态是否仍需要执行(可能已被后续滚动覆盖)
|
||||
// Kiểm tra trạng thái chờ có còn cần thực thi không (có thể đã bị lần cuộn sau ghi đè)
|
||||
if (pendingState === null || pendingState === isExpanded.value) return
|
||||
|
||||
// 设置动画锁
|
||||
// Bật khóa animation
|
||||
isAnimating = true
|
||||
isExpanded.value = pendingState
|
||||
pendingState = null
|
||||
|
||||
// 动画完成后解除锁定,并检查是否有待处理的状态变化
|
||||
// Sau khi animation hoàn tất thì mở khóa, đồng thời kiểm tra xem có trạng thái mới đang chờ hay không
|
||||
setTimeout(() => {
|
||||
isAnimating = false
|
||||
|
||||
// 动画结束后,检查是否有新的待执行状态
|
||||
// Sau khi animation kết thúc, kiểm tra xem có trạng thái chờ mới không
|
||||
if (pendingState !== null && pendingState !== isExpanded.value) {
|
||||
// 延迟一小段时间再执行,避免太快切换
|
||||
// Delay thêm một chút trước khi thực thi để tránh chuyển quá nhanh
|
||||
expandDebounceTimer = setTimeout(() => {
|
||||
if (pendingState !== null && pendingState !== isExpanded.value) {
|
||||
isAnimating = true
|
||||
|
|
@ -518,20 +518,20 @@ const initObserver = () => {
|
|||
})
|
||||
},
|
||||
{
|
||||
// 使用多个阈值,使检测更平滑
|
||||
// Dùng nhiều ngưỡng để việc phát hiện mượt hơn
|
||||
threshold: [0.4, 0.6, 0.8],
|
||||
// 调整 rootMargin,视口底部向上收缩,需要滚动更多才触发展开
|
||||
// Điều chỉnh rootMargin, co đáy viewport lên trên để phải cuộn nhiều hơn mới kích hoạt mở rộng
|
||||
rootMargin: '0px 0px -150px 0px'
|
||||
}
|
||||
)
|
||||
|
||||
// 开始观察
|
||||
// Bắt đầu quan sát
|
||||
if (historyContainer.value) {
|
||||
observer.observe(historyContainer.value)
|
||||
}
|
||||
}
|
||||
|
||||
// 监听路由变化,当返回首页时重新加载数据
|
||||
// Theo dõi thay đổi route, khi quay lại trang chủ thì tải lại dữ liệu
|
||||
watch(() => route.path, (newPath) => {
|
||||
if (newPath === '/') {
|
||||
loadHistory()
|
||||
|
|
@ -539,28 +539,28 @@ watch(() => route.path, (newPath) => {
|
|||
})
|
||||
|
||||
onMounted(async () => {
|
||||
// 确保 DOM 渲染完成后再加载数据
|
||||
// Đảm bảo DOM đã render xong rồi mới tải dữ liệu
|
||||
await nextTick()
|
||||
await loadHistory()
|
||||
|
||||
// 等待 DOM 渲染后初始化观察器
|
||||
// Chờ DOM render xong rồi mới khởi tạo observer
|
||||
setTimeout(() => {
|
||||
initObserver()
|
||||
}, 100)
|
||||
})
|
||||
|
||||
// 如果使用 keep-alive,在组件激活时重新加载数据
|
||||
// Nếu dùng keep-alive, tải lại dữ liệu khi component được kích hoạt
|
||||
onActivated(() => {
|
||||
loadHistory()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// 清理 Intersection Observer
|
||||
// Dọn dẹp Intersection Observer
|
||||
if (observer) {
|
||||
observer.disconnect()
|
||||
observer = null
|
||||
}
|
||||
// 清理防抖定时器
|
||||
// Dọn dẹp bộ hẹn giờ debounce
|
||||
if (expandDebounceTimer) {
|
||||
clearTimeout(expandDebounceTimer)
|
||||
expandDebounceTimer = null
|
||||
|
|
@ -569,7 +569,7 @@ onUnmounted(() => {
|
|||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 容器 */
|
||||
/* Container */
|
||||
.history-database {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
|
@ -579,13 +579,13 @@ onUnmounted(() => {
|
|||
overflow: visible;
|
||||
}
|
||||
|
||||
/* 无项目时简化显示 */
|
||||
/* Simplified display when there are no projects */
|
||||
.history-database.no-projects {
|
||||
min-height: auto;
|
||||
padding: 40px 0 20px;
|
||||
}
|
||||
|
||||
/* 技术网格背景 */
|
||||
/* Tech grid background */
|
||||
.tech-grid-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
|
@ -596,7 +596,7 @@ onUnmounted(() => {
|
|||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 使用CSS背景图案创建固定间距的正方形网格 */
|
||||
/* Use CSS background pattern to create a square grid with fixed spacing */
|
||||
.grid-pattern {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
|
@ -607,7 +607,7 @@ onUnmounted(() => {
|
|||
linear-gradient(to right, rgba(0, 0, 0, 0.05) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, rgba(0, 0, 0, 0.05) 1px, transparent 1px);
|
||||
background-size: 50px 50px;
|
||||
/* 从左上角开始定位,高度变化时只在底部扩展,不影响已有网格位置 */
|
||||
/* Start positioning from top-left; when height changes it only expands at the bottom without affecting the existing grid position */
|
||||
background-position: top left;
|
||||
}
|
||||
|
||||
|
|
@ -623,7 +623,7 @@ onUnmounted(() => {
|
|||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 标题区域 */
|
||||
/* Title section */
|
||||
.section-header {
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
|
|
@ -651,7 +651,7 @@ onUnmounted(() => {
|
|||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* 卡片容器 */
|
||||
/* Card container */
|
||||
.cards-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
|
@ -659,10 +659,10 @@ onUnmounted(() => {
|
|||
align-items: flex-start;
|
||||
padding: 0 40px;
|
||||
transition: min-height 700ms cubic-bezier(0.23, 1, 0.32, 1);
|
||||
/* min-height 由 JS 动态计算,根据卡片数量自适应 */
|
||||
/* min-height is dynamically calculated by JS and adapts to the number of cards */
|
||||
}
|
||||
|
||||
/* 项目卡片 */
|
||||
/* Project card */
|
||||
.project-card {
|
||||
position: absolute;
|
||||
width: 280px;
|
||||
|
|
@ -685,7 +685,7 @@ onUnmounted(() => {
|
|||
z-index: 1000 !important;
|
||||
}
|
||||
|
||||
/* 卡片头部 */
|
||||
/* Card header */
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
|
@ -703,7 +703,7 @@ onUnmounted(() => {
|
|||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 功能状态图标组 */
|
||||
/* Feature status icon group */
|
||||
.card-status-icons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -720,17 +720,17 @@ onUnmounted(() => {
|
|||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 不同功能的颜色 */
|
||||
.status-icon:nth-child(1).available { color: #3B82F6; } /* 图谱构建 - 蓝色 */
|
||||
.status-icon:nth-child(2).available { color: #F59E0B; } /* 环境搭建 - 橙色 */
|
||||
.status-icon:nth-child(3).available { color: #10B981; } /* 分析报告 - 绿色 */
|
||||
/* Colors for different features */
|
||||
.status-icon:nth-child(1).available { color: #3B82F6; } /* Graph Build - blue */
|
||||
.status-icon:nth-child(2).available { color: #F59E0B; } /* Environment Setup - orange */
|
||||
.status-icon:nth-child(3).available { color: #10B981; } /* Analysis Report - green */
|
||||
|
||||
.status-icon.unavailable {
|
||||
color: #D1D5DB;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* 轮数进度显示 */
|
||||
/* Round progress display */
|
||||
.card-progress {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -744,13 +744,13 @@ onUnmounted(() => {
|
|||
font-size: 0.5rem;
|
||||
}
|
||||
|
||||
/* 进度状态颜色 */
|
||||
.card-progress.completed { color: #10B981; } /* 已完成 - 绿色 */
|
||||
.card-progress.in-progress { color: #F59E0B; } /* 进行中 - 橙色 */
|
||||
.card-progress.not-started { color: #9CA3AF; } /* 未开始 - 灰色 */
|
||||
/* Progress status colors */
|
||||
.card-progress.completed { color: #10B981; } /* Completed - green */
|
||||
.card-progress.in-progress { color: #F59E0B; } /* In progress - orange */
|
||||
.card-progress.not-started { color: #9CA3AF; } /* Not started - gray */
|
||||
.card-status.pending { color: #9CA3AF; }
|
||||
|
||||
/* 文件列表区域 */
|
||||
/* File list area */
|
||||
.card-files-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
|
@ -770,7 +770,7 @@ onUnmounted(() => {
|
|||
gap: 4px;
|
||||
}
|
||||
|
||||
/* 更多文件提示 */
|
||||
/* More files hint */
|
||||
.files-more {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -800,7 +800,7 @@ onUnmounted(() => {
|
|||
border-color: #e5e7eb;
|
||||
}
|
||||
|
||||
/* 简约文件标签样式 */
|
||||
/* Minimal file tag style */
|
||||
.file-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
|
@ -818,7 +818,7 @@ onUnmounted(() => {
|
|||
min-width: 28px;
|
||||
}
|
||||
|
||||
/* 低饱和度配色方案 - Morandi色系 */
|
||||
/* Low-saturation palette - Morandi style */
|
||||
.file-tag.pdf { background: #f2e6e6; color: #a65a5a; }
|
||||
.file-tag.doc { background: #e6eff5; color: #5a7ea6; }
|
||||
.file-tag.xls { background: #e6f2e8; color: #5aa668; }
|
||||
|
|
@ -839,7 +839,7 @@ onUnmounted(() => {
|
|||
letter-spacing: 0.1px;
|
||||
}
|
||||
|
||||
/* 无文件时的占位 */
|
||||
/* Placeholder when there are no files */
|
||||
.files-empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -860,13 +860,13 @@ onUnmounted(() => {
|
|||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* 悬停时文件区域效果 */
|
||||
/* File area effect on hover */
|
||||
.project-card:hover .card-files-wrapper {
|
||||
border-color: #d1d5db;
|
||||
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
|
||||
}
|
||||
|
||||
/* 角落装饰 */
|
||||
/* Corner decoration */
|
||||
.corner-mark.top-left-only {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
|
|
@ -879,7 +879,7 @@ onUnmounted(() => {
|
|||
z-index: 10;
|
||||
}
|
||||
|
||||
/* 卡片标题 */
|
||||
/* Card title */
|
||||
.card-title {
|
||||
font-family: 'Inter', -apple-system, sans-serif;
|
||||
font-size: 0.9rem;
|
||||
|
|
@ -897,7 +897,7 @@ onUnmounted(() => {
|
|||
color: #2563EB;
|
||||
}
|
||||
|
||||
/* 卡片描述 */
|
||||
/* Card description */
|
||||
.card-desc {
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 0.75rem;
|
||||
|
|
@ -911,7 +911,7 @@ onUnmounted(() => {
|
|||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
/* 卡片底部 */
|
||||
/* Card footer */
|
||||
.card-footer {
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
|
@ -925,14 +925,14 @@ onUnmounted(() => {
|
|||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 日期时间组合 */
|
||||
/* Date and time group */
|
||||
.card-datetime {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* 底部轮数进度显示 */
|
||||
/* Bottom round progress display */
|
||||
.card-footer .card-progress {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -946,12 +946,12 @@ onUnmounted(() => {
|
|||
font-size: 0.5rem;
|
||||
}
|
||||
|
||||
/* 进度状态颜色 - 底部 */
|
||||
/* Progress status colors - bottom */
|
||||
.card-footer .card-progress.completed { color: #10B981; }
|
||||
.card-footer .card-progress.in-progress { color: #F59E0B; }
|
||||
.card-footer .card-progress.not-started { color: #9CA3AF; }
|
||||
|
||||
/* 底部装饰线 */
|
||||
/* Bottom decorative line */
|
||||
.card-bottom-line {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
|
|
@ -967,7 +967,7 @@ onUnmounted(() => {
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
/* Empty state */
|
||||
.empty-state, .loading-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -995,7 +995,7 @@ onUnmounted(() => {
|
|||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
/* Responsive */
|
||||
@media (max-width: 1200px) {
|
||||
.project-card {
|
||||
width: 240px;
|
||||
|
|
@ -1011,7 +1011,7 @@ onUnmounted(() => {
|
|||
}
|
||||
}
|
||||
|
||||
/* ===== 历史回放详情弹窗样式 ===== */
|
||||
/* ===== Style modal chi tiết phát lại lịch sử ===== */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
|
|
@ -1037,7 +1037,7 @@ onUnmounted(() => {
|
|||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
/* 动画过渡 */
|
||||
/* Hiệu ứng chuyển động */
|
||||
.modal-enter-active,
|
||||
.modal-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
|
|
@ -1066,7 +1066,7 @@ onUnmounted(() => {
|
|||
opacity: 0;
|
||||
}
|
||||
|
||||
/* 弹窗头部 */
|
||||
/* Phần đầu modal */
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
|
@ -1133,7 +1133,7 @@ onUnmounted(() => {
|
|||
color: #111827;
|
||||
}
|
||||
|
||||
/* 弹窗内容 */
|
||||
/* Nội dung modal */
|
||||
.modal-body {
|
||||
padding: 24px 32px;
|
||||
}
|
||||
|
|
@ -1175,7 +1175,7 @@ onUnmounted(() => {
|
|||
padding-right: 4px;
|
||||
}
|
||||
|
||||
/* 自定义滚动条样式 */
|
||||
/* Style thanh cuộn tùy chỉnh */
|
||||
.modal-files::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
|
@ -1229,7 +1229,7 @@ onUnmounted(() => {
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
/* 推演回放分割线 */
|
||||
/* Đường phân cách phát lại mô phỏng */
|
||||
.modal-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -1253,7 +1253,7 @@ onUnmounted(() => {
|
|||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 导航按钮 */
|
||||
/* Nút điều hướng */
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
|
|
@ -1320,7 +1320,7 @@ onUnmounted(() => {
|
|||
color: #111827;
|
||||
}
|
||||
|
||||
/* 不可回放提示 */
|
||||
/* Gợi ý không thể phát lại */
|
||||
.modal-playback-hint {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -1,30 +1,30 @@
|
|||
<template>
|
||||
<div class="workbench-panel">
|
||||
<div class="scroll-container">
|
||||
<!-- Step 01: Ontology -->
|
||||
<!-- Bước 01: Ontology -->
|
||||
<div class="step-card" :class="{ 'active': currentPhase === 0, 'completed': currentPhase > 0 }">
|
||||
<div class="card-header">
|
||||
<div class="step-info">
|
||||
<span class="step-num">01</span>
|
||||
<span class="step-title">本体生成</span>
|
||||
<span class="step-title">Ontology Generation</span>
|
||||
</div>
|
||||
<div class="step-status">
|
||||
<span v-if="currentPhase > 0" class="badge success">已完成</span>
|
||||
<span v-else-if="currentPhase === 0" class="badge processing">生成中</span>
|
||||
<span v-else class="badge pending">等待</span>
|
||||
<span v-if="currentPhase > 0" class="badge success">Completed</span>
|
||||
<span v-else-if="currentPhase === 0" class="badge processing">Processing</span>
|
||||
<span v-else class="badge pending">Pending</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p class="api-note">POST /api/graph/ontology/generate</p>
|
||||
<p class="description">
|
||||
LLM分析文档内容与模拟需求,提取出现实种子,自动生成合适的本体结构
|
||||
LLM analyzes document content and simulation requirements, extracts real-world seeds, and automatically generates a suitable ontology structure
|
||||
</p>
|
||||
|
||||
<!-- Loading / Progress -->
|
||||
<div v-if="currentPhase === 0 && ontologyProgress" class="progress-section">
|
||||
<div class="spinner-sm"></div>
|
||||
<span>{{ ontologyProgress.message || '正在分析文档...' }}</span>
|
||||
<span>{{ ontologyProgress.message || 'Analyzing document...' }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Detail Overlay -->
|
||||
|
|
@ -105,72 +105,72 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 02: Graph Build -->
|
||||
<!-- Bước 02: Xây dựng Graph -->
|
||||
<div class="step-card" :class="{ 'active': currentPhase === 1, 'completed': currentPhase > 1 }">
|
||||
<div class="card-header">
|
||||
<div class="step-info">
|
||||
<span class="step-num">02</span>
|
||||
<span class="step-title">GraphRAG构建</span>
|
||||
<span class="step-title">GraphRAG Build</span>
|
||||
</div>
|
||||
<div class="step-status">
|
||||
<span v-if="currentPhase > 1" class="badge success">已完成</span>
|
||||
<span v-if="currentPhase > 1" class="badge success">Completed</span>
|
||||
<span v-else-if="currentPhase === 1" class="badge processing">{{ buildProgress?.progress || 0 }}%</span>
|
||||
<span v-else class="badge pending">等待</span>
|
||||
<span v-else class="badge pending">Pending</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p class="api-note">POST /api/graph/build</p>
|
||||
<p class="description">
|
||||
基于生成的本体,将文档自动分块后调用 Zep 构建知识图谱,提取实体和关系,并形成时序记忆与社区摘要
|
||||
Based on the generated ontology, documents are automatically chunked and Zep is used to build the knowledge graph, extract entities and relationships, and generate temporal memory and community summaries
|
||||
</p>
|
||||
|
||||
<!-- Stats Cards -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<span class="stat-value">{{ graphStats.nodes }}</span>
|
||||
<span class="stat-label">实体节点</span>
|
||||
<span class="stat-label">Entity Nodes</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-value">{{ graphStats.edges }}</span>
|
||||
<span class="stat-label">关系边</span>
|
||||
<span class="stat-label">Relation Edges</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-value">{{ graphStats.types }}</span>
|
||||
<span class="stat-label">SCHEMA类型</span>
|
||||
<span class="stat-label">Schema Types</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 03: Complete -->
|
||||
<!-- Bước 03: Hoàn thành -->
|
||||
<div class="step-card" :class="{ 'active': currentPhase === 2, 'completed': currentPhase >= 2 }">
|
||||
<div class="card-header">
|
||||
<div class="step-info">
|
||||
<span class="step-num">03</span>
|
||||
<span class="step-title">构建完成</span>
|
||||
<span class="step-title">Build Complete</span>
|
||||
</div>
|
||||
<div class="step-status">
|
||||
<span v-if="currentPhase >= 2" class="badge accent">进行中</span>
|
||||
<span v-if="currentPhase >= 2" class="badge accent">In Progress</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p class="api-note">POST /api/simulation/create</p>
|
||||
<p class="description">图谱构建已完成,请进入下一步进行模拟环境搭建</p>
|
||||
<p class="description">Graph build completed, proceed to next step to set up simulation environment</p>
|
||||
<button
|
||||
class="action-btn"
|
||||
:disabled="currentPhase < 2 || creatingSimulation"
|
||||
@click="handleEnterEnvSetup"
|
||||
>
|
||||
<span v-if="creatingSimulation" class="spinner-sm"></span>
|
||||
{{ creatingSimulation ? '创建中...' : '进入环境搭建 ➝' }}
|
||||
{{ creatingSimulation ? 'Creating...' : 'Enter Environment Setup ➝' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom Info / Logs -->
|
||||
<!-- Log hệ thống -->
|
||||
<div class="system-logs">
|
||||
<div class="log-header">
|
||||
<span class="log-title">SYSTEM DASHBOARD</span>
|
||||
|
|
@ -208,10 +208,10 @@ const selectedOntologyItem = ref(null)
|
|||
const logContent = ref(null)
|
||||
const creatingSimulation = ref(false)
|
||||
|
||||
// 进入环境搭建 - 创建 simulation 并跳转
|
||||
// Vào bước setup môi trường - tạo simulation và chuyển trang
|
||||
const handleEnterEnvSetup = async () => {
|
||||
if (!props.projectData?.project_id || !props.projectData?.graph_id) {
|
||||
console.error('缺少项目或图谱信息')
|
||||
console.error('Missing project or graph info')
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -226,18 +226,18 @@ const handleEnterEnvSetup = async () => {
|
|||
})
|
||||
|
||||
if (res.success && res.data?.simulation_id) {
|
||||
// 跳转到 simulation 页面
|
||||
// Chuyển đến trang mô phỏng
|
||||
router.push({
|
||||
name: 'Simulation',
|
||||
params: { simulationId: res.data.simulation_id }
|
||||
})
|
||||
} else {
|
||||
console.error('创建模拟失败:', res.error)
|
||||
alert('创建模拟失败: ' + (res.error || '未知错误'))
|
||||
console.error('Create simulation failed:', res.error)
|
||||
alert('Create simulation failed: ' + (res.error || 'Unknown error'))
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('创建模拟异常:', err)
|
||||
alert('创建模拟异常: ' + err.message)
|
||||
console.error('Create simulation exception:', err)
|
||||
alert('Create simulation exception: ' + err.message)
|
||||
} finally {
|
||||
creatingSimulation.value = false
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -3,7 +3,7 @@
|
|||
<!-- Top Control Bar -->
|
||||
<div class="control-bar">
|
||||
<div class="status-group">
|
||||
<!-- Twitter 平台进度 -->
|
||||
<!-- Twitter platform progress -->
|
||||
<div class="platform-status twitter" :class="{ active: runStatus.twitter_running, completed: runStatus.twitter_completed }">
|
||||
<div class="platform-header">
|
||||
<svg class="platform-icon" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2">
|
||||
|
|
@ -30,7 +30,7 @@
|
|||
<span class="stat-value mono">{{ runStatus.twitter_actions_count || 0 }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<!-- 可用动作提示 -->
|
||||
<!-- Gợi ý các hành động khả dụng -->
|
||||
<div class="actions-tooltip">
|
||||
<div class="tooltip-title">Available Actions</div>
|
||||
<div class="tooltip-actions">
|
||||
|
|
@ -44,7 +44,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reddit 平台进度 -->
|
||||
<!-- Reddit platform progress -->
|
||||
<div class="platform-status reddit" :class="{ active: runStatus.reddit_running, completed: runStatus.reddit_completed }">
|
||||
<div class="platform-header">
|
||||
<svg class="platform-icon" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2">
|
||||
|
|
@ -71,7 +71,7 @@
|
|||
<span class="stat-value mono">{{ runStatus.reddit_actions_count || 0 }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<!-- 可用动作提示 -->
|
||||
<!-- Gợi ý các hành động khả dụng -->
|
||||
<div class="actions-tooltip">
|
||||
<div class="tooltip-title">Available Actions</div>
|
||||
<div class="tooltip-actions">
|
||||
|
|
@ -97,7 +97,7 @@
|
|||
@click="handleNextStep"
|
||||
>
|
||||
<span v-if="isGeneratingReport" class="loading-spinner-small"></span>
|
||||
{{ isGeneratingReport ? '启动中...' : '开始生成结果报告' }}
|
||||
{{ isGeneratingReport ? 'Starting...' : 'Start Generating Report' }}
|
||||
<span v-if="!isGeneratingReport" class="arrow-icon">→</span>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -157,12 +157,12 @@
|
|||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<!-- CREATE_POST: 发布帖子 -->
|
||||
<!-- CREATE_POST: Publish post -->
|
||||
<div v-if="action.action_type === 'CREATE_POST' && action.action_args?.content" class="content-text main-text">
|
||||
{{ action.action_args.content }}
|
||||
</div>
|
||||
|
||||
<!-- QUOTE_POST: 引用帖子 -->
|
||||
<!-- QUOTE_POST: Quote post -->
|
||||
<template v-if="action.action_type === 'QUOTE_POST'">
|
||||
<div v-if="action.action_args?.quote_content" class="content-text">
|
||||
{{ action.action_args.quote_content }}
|
||||
|
|
@ -178,7 +178,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<!-- REPOST: 转发帖子 -->
|
||||
<!-- REPOST: Repost post -->
|
||||
<template v-if="action.action_type === 'REPOST'">
|
||||
<div class="repost-info">
|
||||
<svg class="icon-small" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><polyline points="17 1 21 5 17 9"></polyline><path d="M3 11V9a4 4 0 0 1 4-4h14"></path><polyline points="7 23 3 19 7 15"></polyline><path d="M21 13v2a4 4 0 0 1-4 4H3"></path></svg>
|
||||
|
|
@ -189,7 +189,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<!-- LIKE_POST: 点赞帖子 -->
|
||||
<!-- LIKE_POST: Like post -->
|
||||
<template v-if="action.action_type === 'LIKE_POST'">
|
||||
<div class="like-info">
|
||||
<svg class="icon-small filled" viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg>
|
||||
|
|
@ -200,7 +200,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<!-- CREATE_COMMENT: 发表评论 -->
|
||||
<!-- CREATE_COMMENT: Publish comment -->
|
||||
<template v-if="action.action_type === 'CREATE_COMMENT'">
|
||||
<div v-if="action.action_args?.content" class="content-text">
|
||||
{{ action.action_args.content }}
|
||||
|
|
@ -211,7 +211,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<!-- SEARCH_POSTS: 搜索帖子 -->
|
||||
<!-- SEARCH_POSTS: Search posts -->
|
||||
<template v-if="action.action_type === 'SEARCH_POSTS'">
|
||||
<div class="search-info">
|
||||
<svg class="icon-small" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
|
||||
|
|
@ -220,7 +220,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<!-- FOLLOW: 关注用户 -->
|
||||
<!-- FOLLOW: Follow user -->
|
||||
<template v-if="action.action_type === 'FOLLOW'">
|
||||
<div class="follow-info">
|
||||
<svg class="icon-small" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="20" y1="8" x2="20" y2="14"></line><line x1="23" y1="11" x2="17" y2="11"></line></svg>
|
||||
|
|
@ -240,7 +240,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<!-- DO_NOTHING: 无操作(静默) -->
|
||||
<!-- DO_NOTHING: Không thao tác (im lặng) -->
|
||||
<template v-if="action.action_type === 'DO_NOTHING'">
|
||||
<div class="idle-info">
|
||||
<svg class="icon-small" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>
|
||||
|
|
@ -248,7 +248,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 通用回退:未知类型或有 content 但未被上述处理 -->
|
||||
<!-- Fallback chung: loại chưa biết hoặc có content nhưng chưa được xử lý ở trên -->
|
||||
<div v-if="!['CREATE_POST', 'QUOTE_POST', 'REPOST', 'LIKE_POST', 'CREATE_COMMENT', 'SEARCH_POSTS', 'FOLLOW', 'UPVOTE_POST', 'DOWNVOTE_POST', 'DO_NOTHING'].includes(action.action_type) && action.action_args?.content" class="content-text">
|
||||
{{ action.action_args.content }}
|
||||
</div>
|
||||
|
|
@ -256,7 +256,7 @@
|
|||
|
||||
<div class="card-footer">
|
||||
<span class="time-tag">R{{ action.round_num }} • {{ formatActionTime(action.timestamp) }}</span>
|
||||
<!-- Platform tag removed as it is in header now -->
|
||||
<!-- Đã bỏ platform tag vì đã có ở header -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -298,10 +298,10 @@ import { generateReport } from '../api/report'
|
|||
|
||||
const props = defineProps({
|
||||
simulationId: String,
|
||||
maxRounds: Number, // 从Step2传入的最大轮数
|
||||
maxRounds: Number, // max rounds được truyền từ Step2
|
||||
minutesPerRound: {
|
||||
type: Number,
|
||||
default: 30 // 默认每轮30分钟
|
||||
default: 30 // mặc định mỗi round là 30 phút
|
||||
},
|
||||
projectData: Object,
|
||||
graphData: Object,
|
||||
|
|
@ -314,22 +314,22 @@ const router = useRouter()
|
|||
|
||||
// State
|
||||
const isGeneratingReport = ref(false)
|
||||
const phase = ref(0) // 0: 未开始, 1: 运行中, 2: 已完成
|
||||
const phase = ref(0) // 0: chưa bắt đầu, 1: đang chạy, 2: đã hoàn thành
|
||||
const isStarting = ref(false)
|
||||
const isStopping = ref(false)
|
||||
const startError = ref(null)
|
||||
const runStatus = ref({})
|
||||
const allActions = ref([]) // 所有动作(增量累积)
|
||||
const actionIds = ref(new Set()) // 用于去重的动作ID集合
|
||||
const allActions = ref([]) // toàn bộ action (cộng dồn tăng dần)
|
||||
const actionIds = ref(new Set()) // tập ID action để khử trùng lặp
|
||||
const scrollContainer = ref(null)
|
||||
|
||||
// Computed
|
||||
// 按时间顺序显示动作(最新的在最后面,即底部)
|
||||
// Hiển thị action theo thứ tự thời gian (mới nhất ở cuối, tức phía dưới)
|
||||
const chronologicalActions = computed(() => {
|
||||
return allActions.value
|
||||
})
|
||||
|
||||
// 各平台动作计数
|
||||
// Đếm action của từng platform
|
||||
const twitterActionsCount = computed(() => {
|
||||
return allActions.value.filter(a => a.platform === 'twitter').length
|
||||
})
|
||||
|
|
@ -338,7 +338,7 @@ const redditActionsCount = computed(() => {
|
|||
return allActions.value.filter(a => a.platform === 'reddit').length
|
||||
})
|
||||
|
||||
// 格式化模拟流逝时间(根据轮次和每轮分钟数计算)
|
||||
// Format thời gian mô phỏng đã trôi qua (tính theo round và phút mỗi round)
|
||||
const formatElapsedTime = (currentRound) => {
|
||||
if (!currentRound || currentRound <= 0) return '0h 0m'
|
||||
const totalMinutes = currentRound * props.minutesPerRound
|
||||
|
|
@ -347,12 +347,12 @@ const formatElapsedTime = (currentRound) => {
|
|||
return `${hours}h ${minutes}m`
|
||||
}
|
||||
|
||||
// Twitter平台的模拟流逝时间
|
||||
// Thời gian mô phỏng đã trôi qua của Twitter
|
||||
const twitterElapsedTime = computed(() => {
|
||||
return formatElapsedTime(runStatus.value.twitter_current_round || 0)
|
||||
})
|
||||
|
||||
// Reddit平台的模拟流逝时间
|
||||
// Thời gian mô phỏng đã trôi qua của Reddit
|
||||
const redditElapsedTime = computed(() => {
|
||||
return formatElapsedTime(runStatus.value.reddit_current_round || 0)
|
||||
})
|
||||
|
|
@ -362,7 +362,7 @@ const addLog = (msg) => {
|
|||
emit('add-log', msg)
|
||||
}
|
||||
|
||||
// 重置所有状态(用于重新启动模拟)
|
||||
// Reset toàn bộ trạng thái (dùng để khởi động lại mô phỏng)
|
||||
const resetAllState = () => {
|
||||
phase.value = 0
|
||||
runStatus.value = {}
|
||||
|
|
@ -373,46 +373,46 @@ const resetAllState = () => {
|
|||
startError.value = null
|
||||
isStarting.value = false
|
||||
isStopping.value = false
|
||||
stopPolling() // 停止之前可能存在的轮询
|
||||
stopPolling() // dừng các polling cũ nếu có
|
||||
}
|
||||
|
||||
// 启动模拟
|
||||
// Khởi động mô phỏng
|
||||
const doStartSimulation = async () => {
|
||||
if (!props.simulationId) {
|
||||
addLog('错误:缺少 simulationId')
|
||||
addLog('Error: missing simulationId')
|
||||
return
|
||||
}
|
||||
|
||||
// 先重置所有状态,确保不会受到上一次模拟的影响
|
||||
// Reset toàn bộ state trước để tránh bị ảnh hưởng từ lần mô phỏng trước
|
||||
resetAllState()
|
||||
|
||||
isStarting.value = true
|
||||
startError.value = null
|
||||
addLog('正在启动双平台并行模拟...')
|
||||
addLog('Starting dual-platform parallel simulation...')
|
||||
emit('update-status', 'processing')
|
||||
|
||||
try {
|
||||
const params = {
|
||||
simulation_id: props.simulationId,
|
||||
platform: 'parallel',
|
||||
force: true, // 强制重新开始
|
||||
enable_graph_memory_update: true // 开启动态图谱更新
|
||||
force: true, // ép bắt đầu lại từ đầu
|
||||
enable_graph_memory_update: true // bật cập nhật graph động
|
||||
}
|
||||
|
||||
if (props.maxRounds) {
|
||||
params.max_rounds = props.maxRounds
|
||||
addLog(`设置最大模拟轮数: ${props.maxRounds}`)
|
||||
addLog(`Set maximum simulation rounds: ${props.maxRounds}`)
|
||||
}
|
||||
|
||||
addLog('已开启动态图谱更新模式')
|
||||
addLog('Dynamic graph memory update mode enabled')
|
||||
|
||||
const res = await startSimulation(params)
|
||||
|
||||
if (res.success && res.data) {
|
||||
if (res.data.force_restarted) {
|
||||
addLog('✓ 已清理旧的模拟日志,重新开始模拟')
|
||||
addLog('✓ Old simulation logs cleared, restarting simulation')
|
||||
}
|
||||
addLog('✓ 模拟引擎启动成功')
|
||||
addLog('✓ Simulation engine started successfully')
|
||||
addLog(` ├─ PID: ${res.data.process_pid || '-'}`)
|
||||
|
||||
phase.value = 1
|
||||
|
|
@ -421,45 +421,45 @@ const doStartSimulation = async () => {
|
|||
startStatusPolling()
|
||||
startDetailPolling()
|
||||
} else {
|
||||
startError.value = res.error || '启动失败'
|
||||
addLog(`✗ 启动失败: ${res.error || '未知错误'}`)
|
||||
startError.value = res.error || 'Start failed'
|
||||
addLog(`✗ Start failed: ${res.error || 'Unknown error'}`)
|
||||
emit('update-status', 'error')
|
||||
}
|
||||
} catch (err) {
|
||||
startError.value = err.message
|
||||
addLog(`✗ 启动异常: ${err.message}`)
|
||||
addLog(`✗ Start exception: ${err.message}`)
|
||||
emit('update-status', 'error')
|
||||
} finally {
|
||||
isStarting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 停止模拟
|
||||
// Dừng mô phỏng
|
||||
const handleStopSimulation = async () => {
|
||||
if (!props.simulationId) return
|
||||
|
||||
isStopping.value = true
|
||||
addLog('正在停止模拟...')
|
||||
addLog('Stopping simulation...')
|
||||
|
||||
try {
|
||||
const res = await stopSimulation({ simulation_id: props.simulationId })
|
||||
|
||||
if (res.success) {
|
||||
addLog('✓ 模拟已停止')
|
||||
addLog('✓ Simulation stopped')
|
||||
phase.value = 2
|
||||
stopPolling()
|
||||
emit('update-status', 'completed')
|
||||
} else {
|
||||
addLog(`停止失败: ${res.error || '未知错误'}`)
|
||||
addLog(`Stop failed: ${res.error || 'Unknown error'}`)
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(`停止异常: ${err.message}`)
|
||||
addLog(`Stop exception: ${err.message}`)
|
||||
} finally {
|
||||
isStopping.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 轮询状态
|
||||
// Polling trạng thái
|
||||
let statusTimer = null
|
||||
let detailTimer = null
|
||||
|
||||
|
|
@ -482,7 +482,7 @@ const stopPolling = () => {
|
|||
}
|
||||
}
|
||||
|
||||
// 追踪各平台的上一次轮次,用于检测变化并输出日志
|
||||
// Theo dõi round trước đó của từng platform để phát hiện thay đổi và ghi log
|
||||
const prevTwitterRound = ref(0)
|
||||
const prevRedditRound = ref(0)
|
||||
|
||||
|
|
@ -497,7 +497,7 @@ const fetchRunStatus = async () => {
|
|||
|
||||
runStatus.value = data
|
||||
|
||||
// 分别检测各平台的轮次变化并输出日志
|
||||
// Kiểm tra thay đổi round của từng platform và ghi log
|
||||
if (data.twitter_current_round > prevTwitterRound.value) {
|
||||
addLog(`[Plaza] R${data.twitter_current_round}/${data.total_rounds} | T:${data.twitter_simulated_hours || 0}h | A:${data.twitter_actions_count}`)
|
||||
prevTwitterRound.value = data.twitter_current_round
|
||||
|
|
@ -508,46 +508,46 @@ const fetchRunStatus = async () => {
|
|||
prevRedditRound.value = data.reddit_current_round
|
||||
}
|
||||
|
||||
// 检测模拟是否已完成(通过 runner_status 或平台完成状态判断)
|
||||
// Kiểm tra mô phỏng đã hoàn thành chưa (theo runner_status hoặc trạng thái hoàn thành của platform)
|
||||
const isCompleted = data.runner_status === 'completed' || data.runner_status === 'stopped'
|
||||
|
||||
// 额外检查:如果后端还没来得及更新 runner_status,但平台已经报告完成
|
||||
// 通过检测 twitter_completed 和 reddit_completed 状态判断
|
||||
// Kiểm tra thêm: nếu backend chưa kịp cập nhật runner_status nhưng platform đã báo xong
|
||||
// Dựa vào twitter_completed và reddit_completed để xác định
|
||||
const platformsCompleted = checkPlatformsCompleted(data)
|
||||
|
||||
if (isCompleted || platformsCompleted) {
|
||||
if (platformsCompleted && !isCompleted) {
|
||||
addLog('✓ 检测到所有平台模拟已结束')
|
||||
addLog('✓ Detected all platform simulations have finished')
|
||||
}
|
||||
addLog('✓ 模拟已完成')
|
||||
addLog('✓ Simulation completed')
|
||||
phase.value = 2
|
||||
stopPolling()
|
||||
emit('update-status', 'completed')
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('获取运行状态失败:', err)
|
||||
console.warn('Failed to fetch run status:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// 检查所有启用的平台是否已完成
|
||||
// Kiểm tra tất cả platform đang bật đã hoàn thành chưa
|
||||
const checkPlatformsCompleted = (data) => {
|
||||
// 如果没有任何平台数据,返回 false
|
||||
// Nếu không có dữ liệu platform nào thì trả về false
|
||||
if (!data) return false
|
||||
|
||||
// 检查各平台的完成状态
|
||||
// Kiểm tra trạng thái hoàn thành của từng platform
|
||||
const twitterCompleted = data.twitter_completed === true
|
||||
const redditCompleted = data.reddit_completed === true
|
||||
|
||||
// 如果至少有一个平台完成了,检查是否所有启用的平台都完成了
|
||||
// 通过 actions_count 判断平台是否被启用(如果 count > 0 或 running 曾为 true)
|
||||
// Nếu có ít nhất một platform hoàn thành, kiểm tra xem tất cả platform đang bật đã hoàn thành chưa
|
||||
// Dùng actions_count để suy ra platform có được bật không (count > 0 hoặc từng running)
|
||||
const twitterEnabled = (data.twitter_actions_count > 0) || data.twitter_running || twitterCompleted
|
||||
const redditEnabled = (data.reddit_actions_count > 0) || data.reddit_running || redditCompleted
|
||||
|
||||
// 如果没有任何平台被启用,返回 false
|
||||
// Nếu không có platform nào được bật thì trả về false
|
||||
if (!twitterEnabled && !redditEnabled) return false
|
||||
|
||||
// 检查所有启用的平台是否都已完成
|
||||
// Kiểm tra tất cả platform đang bật đã hoàn thành chưa
|
||||
if (twitterEnabled && !twitterCompleted) return false
|
||||
if (redditEnabled && !redditCompleted) return false
|
||||
|
||||
|
|
@ -561,13 +561,13 @@ const fetchRunStatusDetail = async () => {
|
|||
const res = await getRunStatusDetail(props.simulationId)
|
||||
|
||||
if (res.success && res.data) {
|
||||
// 使用 all_actions 获取完整的动作列表
|
||||
// Dùng all_actions để lấy đầy đủ danh sách action
|
||||
const serverActions = res.data.all_actions || []
|
||||
|
||||
// 增量添加新动作(去重)
|
||||
// Thêm tăng dần các action mới (khử trùng lặp)
|
||||
let newActionsAdded = 0
|
||||
serverActions.forEach(action => {
|
||||
// 生成唯一ID
|
||||
// Tạo unique ID
|
||||
const actionId = action.id || `${action.timestamp}-${action.platform}-${action.agent_id}-${action.action_type}`
|
||||
|
||||
if (!actionIds.value.has(actionId)) {
|
||||
|
|
@ -580,11 +580,11 @@ const fetchRunStatusDetail = async () => {
|
|||
}
|
||||
})
|
||||
|
||||
// 不自动滚动,让用户自由查看时间轴
|
||||
// 新动作会在底部追加
|
||||
// Không tự động cuộn, để người dùng tự do xem timeline
|
||||
// Action mới sẽ được thêm ở phía dưới
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('获取详细状态失败:', err)
|
||||
console.warn('Failed to fetch detailed status:', err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -640,17 +640,17 @@ const formatActionTime = (timestamp) => {
|
|||
|
||||
const handleNextStep = async () => {
|
||||
if (!props.simulationId) {
|
||||
addLog('错误:缺少 simulationId')
|
||||
addLog('Error: missing simulationId')
|
||||
return
|
||||
}
|
||||
|
||||
if (isGeneratingReport.value) {
|
||||
addLog('报告生成请求已发送,请稍候...')
|
||||
addLog('Report generation request has been sent, please wait...')
|
||||
return
|
||||
}
|
||||
|
||||
isGeneratingReport.value = true
|
||||
addLog('正在启动报告生成...')
|
||||
addLog('Starting report generation...')
|
||||
|
||||
try {
|
||||
const res = await generateReport({
|
||||
|
|
@ -660,21 +660,21 @@ const handleNextStep = async () => {
|
|||
|
||||
if (res.success && res.data) {
|
||||
const reportId = res.data.report_id
|
||||
addLog(`✓ 报告生成任务已启动: ${reportId}`)
|
||||
addLog(`✓ Report generation task started: ${reportId}`)
|
||||
|
||||
// 跳转到报告页面
|
||||
// Chuyển sang trang report
|
||||
router.push({ name: 'Report', params: { reportId } })
|
||||
} else {
|
||||
addLog(`✗ 启动报告生成失败: ${res.error || '未知错误'}`)
|
||||
addLog(`✗ Failed to start report generation: ${res.error || 'Unknown error'}`)
|
||||
isGeneratingReport.value = false
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(`✗ 启动报告生成异常: ${err.message}`)
|
||||
addLog(`✗ Report generation exception: ${err.message}`)
|
||||
isGeneratingReport.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll log to bottom
|
||||
// Scroll log xuống cuối
|
||||
const logContent = ref(null)
|
||||
watch(() => props.systemLogs?.length, () => {
|
||||
nextTick(() => {
|
||||
|
|
@ -685,7 +685,7 @@ watch(() => props.systemLogs?.length, () => {
|
|||
})
|
||||
|
||||
onMounted(() => {
|
||||
addLog('Step3 模拟运行初始化')
|
||||
addLog('Step3 simulation run initialization')
|
||||
if (props.simulationId) {
|
||||
doStartSimulation()
|
||||
}
|
||||
|
|
@ -966,7 +966,7 @@ onUnmounted(() => {
|
|||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
background: #EAEAEA; /* Cleaner line */
|
||||
background: #EAEAEA; /* đường line gọn hơn */
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
|
|
@ -1030,7 +1030,7 @@ onUnmounted(() => {
|
|||
}
|
||||
.timeline-item.twitter .timeline-card {
|
||||
margin-left: auto;
|
||||
margin-right: 32px; /* Gap from axis */
|
||||
margin-right: 32px; /* khoảng cách tới trục */
|
||||
}
|
||||
|
||||
/* Right side (Reddit) */
|
||||
|
|
@ -1040,7 +1040,7 @@ onUnmounted(() => {
|
|||
}
|
||||
.timeline-item.reddit .timeline-card {
|
||||
margin-right: auto;
|
||||
margin-left: 32px; /* Gap from axis */
|
||||
margin-left: 32px; /* khoảng cách tới trục */
|
||||
}
|
||||
|
||||
/* Card Content Styles */
|
||||
|
|
@ -1144,7 +1144,7 @@ onUnmounted(() => {
|
|||
color: #999;
|
||||
}
|
||||
.icon-small.filled {
|
||||
color: #999; /* Keep icons neutral unless highlighted */
|
||||
color: #999; /* giữ icon trung tính nếu không cần highlight */
|
||||
}
|
||||
|
||||
.search-query {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -58,7 +58,7 @@
|
|||
<path d="M12 2a10 10 0 0 1 10 10" stroke-width="4" stroke="#4B5563" stroke-linecap="round"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="loading-text">正在生成{{ section.title }}...</span>
|
||||
<span class="loading-text">Generating{{ section.title }}...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -98,7 +98,7 @@
|
|||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path>
|
||||
</svg>
|
||||
<span>与Report Agent对话</span>
|
||||
<span>Chat with Report Agent</span>
|
||||
</button>
|
||||
<div class="agent-dropdown" v-if="profiles.length > 0">
|
||||
<button
|
||||
|
|
@ -110,13 +110,13 @@
|
|||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
|
||||
<circle cx="12" cy="7" r="4"></circle>
|
||||
</svg>
|
||||
<span>{{ selectedAgent ? selectedAgent.username : '与世界中任意个体对话' }}</span>
|
||||
<span>{{ selectedAgent ? selectedAgent.username : 'Chat with any individual in the world' }}</span>
|
||||
<svg class="dropdown-arrow" :class="{ open: showAgentDropdown }" viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="6 9 12 15 18 9"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
<div v-if="showAgentDropdown" class="dropdown-menu">
|
||||
<div class="dropdown-header">选择对话对象</div>
|
||||
<div class="dropdown-header">Choose someone to talk to</div>
|
||||
<div
|
||||
v-for="(agent, idx) in profiles"
|
||||
:key="idx"
|
||||
|
|
@ -126,7 +126,7 @@
|
|||
<div class="agent-avatar">{{ (agent.username || 'A')[0] }}</div>
|
||||
<div class="agent-info">
|
||||
<span class="agent-name">{{ agent.username }}</span>
|
||||
<span class="agent-role">{{ agent.profession || '未知职业' }}</span>
|
||||
<span class="agent-role">{{ agent.profession || 'Occupation unknown' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -141,7 +141,7 @@
|
|||
<path d="M9 11l3 3L22 4"></path>
|
||||
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path>
|
||||
</svg>
|
||||
<span>发送问卷调查到世界中</span>
|
||||
<span>Send surveys to the world</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -155,7 +155,7 @@
|
|||
<div class="tools-card-avatar">R</div>
|
||||
<div class="tools-card-info">
|
||||
<div class="tools-card-name">Report Agent - Chat</div>
|
||||
<div class="tools-card-subtitle">报告生成智能体的快速对话版本,可调用 4 种专业工具,拥有MiroFish的完整记忆</div>
|
||||
<div class="tools-card-subtitle">Fast conversational mode for the report-generation agent, with access to 4 professional tools and full MiroFish memory</div>
|
||||
</div>
|
||||
<button class="tools-card-toggle" @click="showToolsDetail = !showToolsDetail">
|
||||
<svg :class="{ 'is-expanded': showToolsDetail }" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2">
|
||||
|
|
@ -172,8 +172,8 @@
|
|||
</svg>
|
||||
</div>
|
||||
<div class="tool-content">
|
||||
<div class="tool-name">InsightForge 深度归因</div>
|
||||
<div class="tool-desc">对齐现实世界种子数据与模拟环境状态,结合Global/Local Memory机制,提供跨时空的深度归因分析</div>
|
||||
<div class="tool-name">InsightForge Deep Attribution</div>
|
||||
<div class="tool-desc">Aligns real-world seed data with simulation state, combining Global/Local Memory for deep cross-temporal attribution analysis</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tool-item tool-blue">
|
||||
|
|
@ -184,8 +184,8 @@
|
|||
</svg>
|
||||
</div>
|
||||
<div class="tool-content">
|
||||
<div class="tool-name">PanoramaSearch 全景追踪</div>
|
||||
<div class="tool-desc">基于图结构的广度遍历算法,重构事件传播路径,捕获全量信息流动的拓扑结构</div>
|
||||
<div class="tool-name">PanoramaSearch Panorama Tracking</div>
|
||||
<div class="tool-desc">Uses graph-based breadth traversal to reconstruct event propagation paths and capture full information-flow topology</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tool-item tool-orange">
|
||||
|
|
@ -195,8 +195,8 @@
|
|||
</svg>
|
||||
</div>
|
||||
<div class="tool-content">
|
||||
<div class="tool-name">QuickSearch 快速检索</div>
|
||||
<div class="tool-desc">基于 GraphRAG 的即时查询接口,优化索引效率,用于快速提取具体的节点属性与离散事实</div>
|
||||
<div class="tool-name">QuickSearch Fast Retrieval</div>
|
||||
<div class="tool-desc">An instant GraphRAG query interface with optimized indexing for quickly extracting node attributes and discrete facts</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tool-item tool-green">
|
||||
|
|
@ -208,8 +208,8 @@
|
|||
</svg>
|
||||
</div>
|
||||
<div class="tool-content">
|
||||
<div class="tool-name">InterviewSubAgent 虚拟访谈</div>
|
||||
<div class="tool-desc">自主式访谈,能够并行与模拟世界中个体进行多轮对话,采集非结构化的观点数据与心理状态</div>
|
||||
<div class="tool-name">InterviewSubAgent Virtual Interview</div>
|
||||
<div class="tool-desc">Autonomous interviews that run parallel multi-turn conversations with simulated individuals to collect unstructured opinions and mental-state signals</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -224,7 +224,7 @@
|
|||
<div class="profile-card-name">{{ selectedAgent.username }}</div>
|
||||
<div class="profile-card-meta">
|
||||
<span v-if="selectedAgent.name" class="profile-card-handle">@{{ selectedAgent.name }}</span>
|
||||
<span class="profile-card-profession">{{ selectedAgent.profession || '未知职业' }}</span>
|
||||
<span class="profile-card-profession">{{ selectedAgent.profession || 'Occupation unknown' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="profile-card-toggle" @click="showFullProfile = !showFullProfile">
|
||||
|
|
@ -235,7 +235,7 @@
|
|||
</div>
|
||||
<div v-if="showFullProfile && selectedAgent.bio" class="profile-card-body">
|
||||
<div class="profile-card-bio">
|
||||
<div class="profile-card-label">简介</div>
|
||||
<div class="profile-card-label">Bio</div>
|
||||
<p>{{ selectedAgent.bio }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -250,7 +250,7 @@
|
|||
</svg>
|
||||
</div>
|
||||
<p class="empty-text">
|
||||
{{ chatTarget === 'report_agent' ? '与 Report Agent 对话,深入了解报告内容' : '与模拟个体对话,了解他们的观点' }}
|
||||
{{ chatTarget === 'report_agent' ? 'Chat with the Report Agent to explore the report in depth' : 'Chat with simulated individuals to understand their viewpoints' }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
|
|
@ -292,7 +292,7 @@
|
|||
<textarea
|
||||
v-model="chatInput"
|
||||
class="chat-input"
|
||||
placeholder="输入您的问题..."
|
||||
placeholder="Type your question..."
|
||||
@keydown.enter.exact.prevent="sendMessage"
|
||||
:disabled="isSending || (!selectedAgent && chatTarget === 'agent')"
|
||||
rows="1"
|
||||
|
|
@ -317,8 +317,8 @@
|
|||
<div class="survey-setup">
|
||||
<div class="setup-section">
|
||||
<div class="section-header">
|
||||
<span class="section-title">选择调查对象</span>
|
||||
<span class="selection-count">已选 {{ selectedAgents.size }} / {{ profiles.length }}</span>
|
||||
<span class="section-title">Select survey targets</span>
|
||||
<span class="selection-count">Selected {{ selectedAgents.size }} / {{ profiles.length }}</span>
|
||||
</div>
|
||||
<div class="agents-grid">
|
||||
<label
|
||||
|
|
@ -335,7 +335,7 @@
|
|||
<div class="checkbox-avatar">{{ (agent.username || 'A')[0] }}</div>
|
||||
<div class="checkbox-info">
|
||||
<span class="checkbox-name">{{ agent.username }}</span>
|
||||
<span class="checkbox-role">{{ agent.profession || '未知职业' }}</span>
|
||||
<span class="checkbox-role">{{ agent.profession || 'Occupation unknown' }}</span>
|
||||
</div>
|
||||
<div class="checkbox-indicator">
|
||||
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="3">
|
||||
|
|
@ -345,20 +345,20 @@
|
|||
</label>
|
||||
</div>
|
||||
<div class="selection-actions">
|
||||
<button class="action-link" @click="selectAllAgents">全选</button>
|
||||
<button class="action-link" @click="selectAllAgents">Select all</button>
|
||||
<span class="action-divider">|</span>
|
||||
<button class="action-link" @click="clearAgentSelection">清空</button>
|
||||
<button class="action-link" @click="clearAgentSelection">Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setup-section">
|
||||
<div class="section-header">
|
||||
<span class="section-title">问卷问题</span>
|
||||
<span class="section-title">Survey question</span>
|
||||
</div>
|
||||
<textarea
|
||||
v-model="surveyQuestion"
|
||||
class="survey-input"
|
||||
placeholder="输入您想问所有被选中对象的问题..."
|
||||
placeholder="Type the question you want to ask all selected targets..."
|
||||
rows="3"
|
||||
></textarea>
|
||||
</div>
|
||||
|
|
@ -369,15 +369,15 @@
|
|||
@click="submitSurvey"
|
||||
>
|
||||
<span v-if="isSurveying" class="loading-spinner"></span>
|
||||
<span v-else>发送问卷</span>
|
||||
<span v-else>Send survey</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Survey Results -->
|
||||
<div v-if="surveyResults.length > 0" class="survey-results">
|
||||
<div class="results-header">
|
||||
<span class="results-title">调查结果</span>
|
||||
<span class="results-count">{{ surveyResults.length }} 条回复</span>
|
||||
<span class="results-title">Survey Results</span>
|
||||
<span class="results-count">{{ surveyResults.length }} responses</span>
|
||||
</div>
|
||||
<div class="results-list">
|
||||
<div
|
||||
|
|
@ -389,7 +389,7 @@
|
|||
<div class="result-avatar">{{ (result.agent_name || 'A')[0] }}</div>
|
||||
<div class="result-info">
|
||||
<span class="result-name">{{ result.agent_name }}</span>
|
||||
<span class="result-role">{{ result.profession || '未知职业' }}</span>
|
||||
<span class="result-role">{{ result.profession || 'Occupation unknown' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="result-question">
|
||||
|
|
@ -434,7 +434,7 @@ const showToolsDetail = ref(true)
|
|||
// Chat State
|
||||
const chatInput = ref('')
|
||||
const chatHistory = ref([])
|
||||
const chatHistoryCache = ref({}) // 缓存所有对话记录: { 'report_agent': [], 'agent_0': [], 'agent_1': [], ... }
|
||||
const chatHistoryCache = ref({}) // Luu bo nho dem cho toan bo lich su hoi thoai: { 'report_agent': [], 'agent_0': [], 'agent_1': [], ... }
|
||||
const isSending = ref(false)
|
||||
const chatMessages = ref(null)
|
||||
const chatInputRef = ref(null)
|
||||
|
|
@ -484,7 +484,7 @@ const selectChatTarget = (target) => {
|
|||
}
|
||||
}
|
||||
|
||||
// 保存当前对话记录到缓存
|
||||
// Luu lich su hoi thoai hien tai vao bo nho dem
|
||||
const saveChatHistory = () => {
|
||||
if (chatHistory.value.length === 0) return
|
||||
|
||||
|
|
@ -496,7 +496,7 @@ const saveChatHistory = () => {
|
|||
}
|
||||
|
||||
const selectReportAgentChat = () => {
|
||||
// 保存当前对话记录
|
||||
// Luu lich su hoi thoai hien tai
|
||||
saveChatHistory()
|
||||
|
||||
activeTab.value = 'chat'
|
||||
|
|
@ -505,7 +505,7 @@ const selectReportAgentChat = () => {
|
|||
selectedAgentIndex.value = null
|
||||
showAgentDropdown.value = false
|
||||
|
||||
// 恢复 Report Agent 的对话记录
|
||||
// Khoi phuc lich su hoi thoai cua Report Agent
|
||||
chatHistory.value = chatHistoryCache.value['report_agent'] || []
|
||||
}
|
||||
|
||||
|
|
@ -525,7 +525,7 @@ const toggleAgentDropdown = () => {
|
|||
}
|
||||
|
||||
const selectAgent = (agent, idx) => {
|
||||
// 保存当前对话记录
|
||||
// Luu lich su hoi thoai hien tai
|
||||
saveChatHistory()
|
||||
|
||||
selectedAgent.value = agent
|
||||
|
|
@ -533,9 +533,9 @@ const selectAgent = (agent, idx) => {
|
|||
chatTarget.value = 'agent'
|
||||
showAgentDropdown.value = false
|
||||
|
||||
// 恢复该 Agent 的对话记录
|
||||
// Khoi phuc lich su hoi thoai cua Agent nay
|
||||
chatHistory.value = chatHistoryCache.value[`agent_${idx}`] || []
|
||||
addLog(`选择对话对象: ${agent.username}`)
|
||||
addLog(`Selected chat target: ${agent.username}`)
|
||||
}
|
||||
|
||||
const formatTime = (timestamp) => {
|
||||
|
|
@ -563,7 +563,7 @@ const renderMarkdown = (content) => {
|
|||
html = html.replace(/^# (.+)$/gm, '<h2 class="md-h2">$1</h2>')
|
||||
html = html.replace(/^> (.+)$/gm, '<blockquote class="md-quote">$1</blockquote>')
|
||||
|
||||
// 处理列表 - 支持子列表
|
||||
// Xu ly danh sach - ho tro danh sach con
|
||||
html = html.replace(/^(\s*)- (.+)$/gm, (match, indent, text) => {
|
||||
const level = Math.floor(indent.length / 2)
|
||||
return `<li class="md-li" data-level="${level}">${text}</li>`
|
||||
|
|
@ -573,17 +573,17 @@ const renderMarkdown = (content) => {
|
|||
return `<li class="md-oli" data-level="${level}">${text}</li>`
|
||||
})
|
||||
|
||||
// 包装无序列表
|
||||
// Bao boc danh sach khong thu tu
|
||||
html = html.replace(/(<li class="md-li"[^>]*>.*?<\/li>\s*)+/g, '<ul class="md-ul">$&</ul>')
|
||||
// 包装有序列表
|
||||
// Bao boc danh sach co thu tu
|
||||
html = html.replace(/(<li class="md-oli"[^>]*>.*?<\/li>\s*)+/g, '<ol class="md-ol">$&</ol>')
|
||||
|
||||
// 清理列表项之间的所有空白
|
||||
// Lam sach khoang trang giua cac muc trong danh sach
|
||||
html = html.replace(/<\/li>\s+<li/g, '</li><li')
|
||||
// 清理列表开始标签后的空白
|
||||
// Lam sach khoang trang sau the mo dau danh sach
|
||||
html = html.replace(/<ul class="md-ul">\s+/g, '<ul class="md-ul">')
|
||||
html = html.replace(/<ol class="md-ol">\s+/g, '<ol class="md-ol">')
|
||||
// 清理列表结束标签前的空白
|
||||
// Lam sach khoang trang truoc the dong danh sach
|
||||
html = html.replace(/\s+<\/ul>/g, '</ul>')
|
||||
html = html.replace(/\s+<\/ol>/g, '</ol>')
|
||||
|
||||
|
|
@ -599,17 +599,17 @@ const renderMarkdown = (content) => {
|
|||
html = html.replace(/(<\/h[2-5]>)<\/p>/g, '$1')
|
||||
html = html.replace(/<p class="md-p">(<ul|<ol|<blockquote|<pre|<hr)/g, '$1')
|
||||
html = html.replace(/(<\/ul>|<\/ol>|<\/blockquote>|<\/pre>)<\/p>/g, '$1')
|
||||
// 清理块级元素前后的 <br> 标签
|
||||
// Lam sach the <br> truoc va sau phan tu khoi
|
||||
html = html.replace(/<br>\s*(<ul|<ol|<blockquote)/g, '$1')
|
||||
html = html.replace(/(<\/ul>|<\/ol>|<\/blockquote>)\s*<br>/g, '$1')
|
||||
// 清理 <p><br> 紧跟块级元素的情况(多余空行导致)
|
||||
// Lam sach truong hop <p><br> dung truoc phan tu khoi (do dong trong du)
|
||||
html = html.replace(/<p class="md-p">(<br>\s*)+(<ul|<ol|<blockquote|<pre|<hr)/g, '$2')
|
||||
// 清理连续的 <br> 标签
|
||||
// Lam sach cac the <br> lien tiep
|
||||
html = html.replace(/(<br>\s*){2,}/g, '<br>')
|
||||
// 清理块级元素后紧跟的段落开始标签前的 <br>
|
||||
// Lam sach <br> truoc the mo dau doan van ngay sau phan tu khoi
|
||||
html = html.replace(/(<\/ol>|<\/ul>|<\/blockquote>)<br>(<p|<div)/g, '$1$2')
|
||||
|
||||
// 修复非连续有序列表的编号:当单项 <ol> 被段落内容隔开时,保持编号递增
|
||||
// Sua so thu tu danh sach co thu tu khong lien tuc: giu chi so tang dan khi cac muc <ol> don le bi ngan boi doan van
|
||||
const tokens = html.split(/(<ol class="md-ol">(?:<li class="md-oli"[^>]*>[\s\S]*?<\/li>)+<\/ol>)/g)
|
||||
let olCounter = 0
|
||||
let inSequence = false
|
||||
|
|
@ -662,22 +662,22 @@ const sendMessage = async () => {
|
|||
await sendToAgent(message)
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(`发送失败: ${err.message}`)
|
||||
addLog(`Send failed: ${err.message}`)
|
||||
chatHistory.value.push({
|
||||
role: 'assistant',
|
||||
content: `抱歉,发生了错误: ${err.message}`,
|
||||
content: `Sorry, an error occurred: ${err.message}`,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
} finally {
|
||||
isSending.value = false
|
||||
scrollToBottom()
|
||||
// 自动保存对话记录到缓存
|
||||
// Tu dong luu lich su hoi thoai vao bo nho dem
|
||||
saveChatHistory()
|
||||
}
|
||||
}
|
||||
|
||||
const sendToReportAgent = async (message) => {
|
||||
addLog(`向 Report Agent 发送: ${message.substring(0, 50)}...`)
|
||||
addLog(`Sent to Report Agent: ${message.substring(0, 50)}...`)
|
||||
|
||||
// Build chat history for API
|
||||
const historyForApi = chatHistory.value
|
||||
|
|
@ -697,21 +697,21 @@ const sendToReportAgent = async (message) => {
|
|||
if (res.success && res.data) {
|
||||
chatHistory.value.push({
|
||||
role: 'assistant',
|
||||
content: res.data.response || res.data.answer || '无响应',
|
||||
content: res.data.response || res.data.answer || 'No response',
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
addLog('Report Agent 已回复')
|
||||
addLog('Report Agent replied')
|
||||
} else {
|
||||
throw new Error(res.error || '请求失败')
|
||||
throw new Error(res.error || 'Request failed')
|
||||
}
|
||||
}
|
||||
|
||||
const sendToAgent = async (message) => {
|
||||
if (!selectedAgent.value || selectedAgentIndex.value === null) {
|
||||
throw new Error('请先选择一个模拟个体')
|
||||
throw new Error('Please select a simulated individual first')
|
||||
}
|
||||
|
||||
addLog(`向 ${selectedAgent.value.username} 发送: ${message.substring(0, 50)}...`)
|
||||
addLog(`Sent to ${selectedAgent.value.username}: ${message.substring(0, 50)}...`)
|
||||
|
||||
// Build prompt with chat history
|
||||
let prompt = message
|
||||
|
|
@ -719,9 +719,9 @@ const sendToAgent = async (message) => {
|
|||
const historyContext = chatHistory.value
|
||||
.filter(msg => msg.content !== message)
|
||||
.slice(-6)
|
||||
.map(msg => `${msg.role === 'user' ? '提问者' : '你'}:${msg.content}`)
|
||||
.map(msg => `${msg.role === 'user' ? 'Questioner' : 'You'}: ${msg.content}`)
|
||||
.join('\n')
|
||||
prompt = `以下是我们之前的对话:\n${historyContext}\n\n现在我的新问题是:${message}`
|
||||
prompt = `Here is our previous conversation:\n${historyContext}\n\nMy new question is: ${message}`
|
||||
}
|
||||
|
||||
const res = await interviewAgents({
|
||||
|
|
@ -733,17 +733,17 @@ const sendToAgent = async (message) => {
|
|||
})
|
||||
|
||||
if (res.success && res.data) {
|
||||
// 正确的数据路径: res.data.result.results 是一个对象字典
|
||||
// 格式: {"twitter_0": {...}, "reddit_0": {...}} 或单平台 {"reddit_0": {...}}
|
||||
// Correct data path: res.data.result.results is an object dictionary
|
||||
// Format: {"twitter_0": {...}, "reddit_0": {...}} or single-platform {"reddit_0": {...}}
|
||||
const resultData = res.data.result || res.data
|
||||
const resultsDict = resultData.results || resultData
|
||||
|
||||
// 将对象字典转换为数组,优先获取 reddit 平台的回复
|
||||
// Convert dictionary-style results and prefer reddit response first
|
||||
let responseContent = null
|
||||
const agentId = selectedAgentIndex.value
|
||||
|
||||
if (typeof resultsDict === 'object' && !Array.isArray(resultsDict)) {
|
||||
// 优先使用 reddit 平台回复,其次 twitter
|
||||
// Prefer reddit response, then twitter
|
||||
const redditKey = `reddit_${agentId}`
|
||||
const twitterKey = `twitter_${agentId}`
|
||||
const agentResult = resultsDict[redditKey] || resultsDict[twitterKey] || Object.values(resultsDict)[0]
|
||||
|
|
@ -751,7 +751,7 @@ const sendToAgent = async (message) => {
|
|||
responseContent = agentResult.response || agentResult.answer
|
||||
}
|
||||
} else if (Array.isArray(resultsDict) && resultsDict.length > 0) {
|
||||
// 兼容数组格式
|
||||
// Backward compatible with array format
|
||||
responseContent = resultsDict[0].response || resultsDict[0].answer
|
||||
}
|
||||
|
||||
|
|
@ -761,12 +761,12 @@ const sendToAgent = async (message) => {
|
|||
content: responseContent,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
addLog(`${selectedAgent.value.username} 已回复`)
|
||||
addLog(`${selectedAgent.value.username} replied`)
|
||||
} else {
|
||||
throw new Error('无响应数据')
|
||||
throw new Error('No response data')
|
||||
}
|
||||
} else {
|
||||
throw new Error(res.error || '请求失败')
|
||||
throw new Error(res.error || 'Request failed')
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -803,7 +803,7 @@ const submitSurvey = async () => {
|
|||
if (selectedAgents.value.size === 0 || !surveyQuestion.value.trim()) return
|
||||
|
||||
isSurveying.value = true
|
||||
addLog(`发送问卷给 ${selectedAgents.value.size} 个对象...`)
|
||||
addLog(`Sending survey to ${selectedAgents.value.size} targets...`)
|
||||
|
||||
try {
|
||||
const interviews = Array.from(selectedAgents.value).map(idx => ({
|
||||
|
|
@ -817,33 +817,33 @@ const submitSurvey = async () => {
|
|||
})
|
||||
|
||||
if (res.success && res.data) {
|
||||
// 正确的数据路径: res.data.result.results 是一个对象字典
|
||||
// 格式: {"twitter_0": {...}, "reddit_0": {...}, "twitter_1": {...}, ...}
|
||||
// Correct data path: res.data.result.results is an object dictionary
|
||||
// Format: {"twitter_0": {...}, "reddit_0": {...}, "twitter_1": {...}, ...}
|
||||
const resultData = res.data.result || res.data
|
||||
const resultsDict = resultData.results || resultData
|
||||
|
||||
// 将对象字典转换为数组格式
|
||||
// Convert object dictionary to array format
|
||||
const surveyResultsList = []
|
||||
|
||||
for (const interview of interviews) {
|
||||
const agentIdx = interview.agent_id
|
||||
const agent = profiles.value[agentIdx]
|
||||
|
||||
// 优先使用 reddit 平台回复,其次 twitter
|
||||
let responseContent = '无响应'
|
||||
// Prefer reddit response, then twitter
|
||||
let responseContent = 'No response'
|
||||
|
||||
if (typeof resultsDict === 'object' && !Array.isArray(resultsDict)) {
|
||||
const redditKey = `reddit_${agentIdx}`
|
||||
const twitterKey = `twitter_${agentIdx}`
|
||||
const agentResult = resultsDict[redditKey] || resultsDict[twitterKey]
|
||||
if (agentResult) {
|
||||
responseContent = agentResult.response || agentResult.answer || '无响应'
|
||||
responseContent = agentResult.response || agentResult.answer || 'No response'
|
||||
}
|
||||
} else if (Array.isArray(resultsDict)) {
|
||||
// 兼容数组格式
|
||||
// Backward compatible with array format
|
||||
const matchedResult = resultsDict.find(r => r.agent_id === agentIdx)
|
||||
if (matchedResult) {
|
||||
responseContent = matchedResult.response || matchedResult.answer || '无响应'
|
||||
responseContent = matchedResult.response || matchedResult.answer || 'No response'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -857,12 +857,12 @@ const submitSurvey = async () => {
|
|||
}
|
||||
|
||||
surveyResults.value = surveyResultsList
|
||||
addLog(`收到 ${surveyResults.value.length} 条回复`)
|
||||
addLog(`Received ${surveyResults.value.length} responses`)
|
||||
} else {
|
||||
throw new Error(res.error || '请求失败')
|
||||
throw new Error(res.error || 'Request failed')
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(`问卷发送失败: ${err.message}`)
|
||||
addLog(`Survey send failed: ${err.message}`)
|
||||
} finally {
|
||||
isSurveying.value = false
|
||||
}
|
||||
|
|
@ -873,7 +873,7 @@ const loadReportData = async () => {
|
|||
if (!props.reportId) return
|
||||
|
||||
try {
|
||||
addLog(`加载报告数据: ${props.reportId}`)
|
||||
addLog(`Loading report data: ${props.reportId}`)
|
||||
|
||||
// Get report info
|
||||
const reportRes = await getReport(props.reportId)
|
||||
|
|
@ -882,7 +882,7 @@ const loadReportData = async () => {
|
|||
await loadAgentLogs()
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(`加载报告失败: ${err.message}`)
|
||||
addLog(`Failed to load report: ${err.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -904,10 +904,10 @@ const loadAgentLogs = async () => {
|
|||
}
|
||||
})
|
||||
|
||||
addLog('报告数据加载完成')
|
||||
addLog('Report data loaded')
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(`加载报告日志失败: ${err.message}`)
|
||||
addLog(`Failed to load report logs: ${err.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -918,10 +918,10 @@ const loadProfiles = async () => {
|
|||
const res = await getSimulationProfilesRealtime(props.simulationId, 'reddit')
|
||||
if (res.success && res.data) {
|
||||
profiles.value = res.data.profiles || []
|
||||
addLog(`加载了 ${profiles.value.length} 个模拟个体`)
|
||||
addLog(`Loaded ${profiles.value.length} simulated individuals`)
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(`加载模拟个体失败: ${err.message}`)
|
||||
addLog(`Failed to load simulated individuals: ${err.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -935,7 +935,7 @@ const handleClickOutside = (e) => {
|
|||
|
||||
// Lifecycle
|
||||
onMounted(() => {
|
||||
addLog('Step5 深度互动初始化')
|
||||
addLog('Step5 deep interaction initialized')
|
||||
loadReportData()
|
||||
loadProfiles()
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
|
|
@ -980,7 +980,7 @@ watch(() => props.simulationId, (newId) => {
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Left Panel - Report Style (与 Step4Report.vue 完全一致) */
|
||||
/* Left Panel - Report Style (identical to Step4Report.vue) */
|
||||
.left-panel.report-style {
|
||||
width: 45%;
|
||||
min-width: 450px;
|
||||
|
|
@ -2028,7 +2028,7 @@ watch(() => props.simulationId, (newId) => {
|
|||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* 修复有序列表编号 - 使用 CSS 计数器让多个 ol 连续编号 */
|
||||
/* Fix ordered-list numbering - use CSS counter to keep numbering continuous across multiple ol blocks */
|
||||
.message-text {
|
||||
counter-reset: list-counter;
|
||||
}
|
||||
|
|
@ -2054,7 +2054,7 @@ watch(() => props.simulationId, (newId) => {
|
|||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 无序列表样式 */
|
||||
/* Unordered list styles */
|
||||
.message-text :deep(.md-ul) {
|
||||
padding-left: 20px;
|
||||
margin: 8px 0;
|
||||
|
|
@ -2533,7 +2533,7 @@ watch(() => props.simulationId, (newId) => {
|
|||
margin: 6px 0;
|
||||
}
|
||||
|
||||
/* 聊天/问卷区域的引用样式 */
|
||||
/* Quote styles for chat/survey area */
|
||||
.chat-messages :deep(.md-quote),
|
||||
.result-answer :deep(.md-quote) {
|
||||
margin: 12px 0;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
/**
|
||||
* 临时存储待上传的文件和需求
|
||||
* 用于首页点击启动引擎后立即跳转,在Process页面再进行API调用
|
||||
* Lưu tạm file cần upload và yêu cầu
|
||||
* Dùng để chuyển trang ngay sau khi nhấn “khởi động engine” ở trang chủ,
|
||||
* sau đó mới gọi API ở trang Process
|
||||
*/
|
||||
import { reactive } from 'vue'
|
||||
|
||||
|
|
|
|||
|
|
@ -1,35 +1,35 @@
|
|||
<template>
|
||||
<div class="home-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<!-- Top Navigation Bar -->
|
||||
<nav class="navbar">
|
||||
<div class="nav-brand">MIROFISH</div>
|
||||
<div class="nav-links">
|
||||
<a href="https://github.com/666ghj/MiroFish" target="_blank" class="github-link">
|
||||
访问我们的Github主页 <span class="arrow">↗</span>
|
||||
Visit our GitHub page <span class="arrow">↗</span>
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="main-content">
|
||||
<!-- 上半部分:Hero 区域 -->
|
||||
<!-- Upper Section: Hero Area -->
|
||||
<section class="hero-section">
|
||||
<div class="hero-left">
|
||||
<div class="tag-row">
|
||||
<span class="orange-tag">简洁通用的群体智能引擎</span>
|
||||
<span class="version-text">/ v0.1-预览版</span>
|
||||
<span class="orange-tag">A concise and general-purpose collective intelligence engine</span>
|
||||
<span class="version-text">/ v0.1 - Preview Version</span>
|
||||
</div>
|
||||
|
||||
<h1 class="main-title">
|
||||
上传任意报告<br>
|
||||
<span class="gradient-text">即刻推演未来</span>
|
||||
Upload any report<br>
|
||||
<span class="gradient-text">Instantly simulate the future</span>
|
||||
</h1>
|
||||
|
||||
<div class="hero-desc">
|
||||
<p>
|
||||
即使只有一段文字,<span class="highlight-bold">MiroFish</span> 也能基于其中的现实种子,全自动生成与之对应的至多<span class="highlight-orange">百万级Agent</span>构成的平行世界。通过上帝视角注入变量,在复杂的群体交互中寻找动态环境下的<span class="highlight-code">“局部最优解”</span>
|
||||
Even with just a piece of text, <span class="highlight-bold">MiroFish</span> can extract real-world signals and automatically generate a parallel world with up to <span class="highlight-orange">millions of agents</span>. By injecting variables from a god-like perspective, it explores <span class="highlight-code">"locally optimal solutions"</span> within complex multi-agent interactions in dynamic environments.
|
||||
</p>
|
||||
<p class="slogan-text">
|
||||
让未来在 Agent 群中预演,让决策在百战后胜出<span class="blinking-cursor">_</span>
|
||||
Let the future be rehearsed within agent swarms, and let decisions emerge after countless simulations<span class="blinking-cursor">_</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -37,7 +37,7 @@
|
|||
</div>
|
||||
|
||||
<div class="hero-right">
|
||||
<!-- Logo 区域 -->
|
||||
<!-- Logo Area -->
|
||||
<div class="logo-container">
|
||||
<img src="../assets/logo/MiroFish_logo_left.jpeg" alt="MiroFish Logo" class="hero-logo" />
|
||||
</div>
|
||||
|
|
@ -48,84 +48,84 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 下半部分:双栏布局 -->
|
||||
<!-- Lower Section: Two-column Layout -->
|
||||
<section class="dashboard-section">
|
||||
<!-- 左栏:状态与步骤 -->
|
||||
<!-- Left Column: Status and Steps -->
|
||||
<div class="left-panel">
|
||||
<div class="panel-header">
|
||||
<span class="status-dot">■</span> 系统状态
|
||||
<span class="status-dot">■</span> System Status
|
||||
</div>
|
||||
|
||||
<h2 class="section-title">准备就绪</h2>
|
||||
<h2 class="section-title">Ready</h2>
|
||||
<p class="section-desc">
|
||||
预测引擎待命中,可上传多份非结构化数据以初始化模拟序列
|
||||
The prediction engine is on standby. Multiple unstructured data files can be uploaded to initialize the simulation sequence.
|
||||
</p>
|
||||
|
||||
<!-- 数据指标卡片 -->
|
||||
<!-- Data Metric Cards -->
|
||||
<div class="metrics-row">
|
||||
<div class="metric-card">
|
||||
<div class="metric-value">低成本</div>
|
||||
<div class="metric-label">常规模拟平均5$/次</div>
|
||||
<div class="metric-value">Low Cost</div>
|
||||
<div class="metric-label">Average cost: $5 per simulation</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-value">高可用</div>
|
||||
<div class="metric-label">最多百万级Agent模拟</div>
|
||||
<div class="metric-value">High Availability</div>
|
||||
<div class="metric-label">Supports simulations with up to millions of agents</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 项目模拟步骤介绍 (新增区域) -->
|
||||
<!-- Project Simulation Workflow (Added Section) -->
|
||||
<div class="steps-container">
|
||||
<div class="steps-header">
|
||||
<span class="diamond-icon">◇</span> 工作流序列
|
||||
<span class="diamond-icon">◇</span> Workflow Sequence
|
||||
</div>
|
||||
<div class="workflow-list">
|
||||
<div class="workflow-item">
|
||||
<span class="step-num">01</span>
|
||||
<div class="step-info">
|
||||
<div class="step-title">图谱构建</div>
|
||||
<div class="step-desc">现实种子提取 & 个体与群体记忆注入 & GraphRAG构建</div>
|
||||
<div class="step-title">Graph Construction</div>
|
||||
<div class="step-desc">Real-world seed extraction & individual and collective memory injection & GraphRAG construction</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="workflow-item">
|
||||
<span class="step-num">02</span>
|
||||
<div class="step-info">
|
||||
<div class="step-title">环境搭建</div>
|
||||
<div class="step-desc">实体关系抽取 & 人设生成 & 环境配置Agent注入仿真参数</div>
|
||||
<div class="step-title">Environment Setup</div>
|
||||
<div class="step-desc">Entity relationship extraction & persona generation & environment configuration with agent-injected simulation parameters</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="workflow-item">
|
||||
<span class="step-num">03</span>
|
||||
<div class="step-info">
|
||||
<div class="step-title">开始模拟</div>
|
||||
<div class="step-desc">双平台并行模拟 & 自动解析预测需求 & 动态更新时序记忆</div>
|
||||
<div class="step-title">Start Simulation</div>
|
||||
<div class="step-desc">Dual-platform parallel simulation & automatic parsing of prediction requirements & dynamic temporal memory updates</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="workflow-item">
|
||||
<span class="step-num">04</span>
|
||||
<div class="step-info">
|
||||
<div class="step-title">报告生成</div>
|
||||
<div class="step-desc">ReportAgent拥有丰富的工具集与模拟后环境进行深度交互</div>
|
||||
<div class="step-title">Report Generation</div>
|
||||
<div class="step-desc">ReportAgent uses a rich toolset to deeply interact with the post-simulation environment</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="workflow-item">
|
||||
<span class="step-num">05</span>
|
||||
<div class="step-info">
|
||||
<div class="step-title">深度互动</div>
|
||||
<div class="step-desc">与模拟世界中的任意一位进行对话 & 与ReportAgent进行对话</div>
|
||||
<div class="step-title">Deep Interaction</div>
|
||||
<div class="step-desc">Converse with any entity in the simulated world & interact with the ReportAgent</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右栏:交互控制台 -->
|
||||
<!-- Right Column: Interactive Console -->
|
||||
<div class="right-panel">
|
||||
<div class="console-box">
|
||||
<!-- 上传区域 -->
|
||||
<!-- Upload Area -->
|
||||
<div class="console-section">
|
||||
<div class="console-header">
|
||||
<span class="console-label">01 / 现实种子</span>
|
||||
<span class="console-meta">支持格式: PDF, MD, TXT</span>
|
||||
<span class="console-label">01 / Real-world Seeds</span>
|
||||
<span class="console-meta">Supported formats: PDF, MD, TXT</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
|
@ -148,8 +148,8 @@
|
|||
|
||||
<div v-if="files.length === 0" class="upload-placeholder">
|
||||
<div class="upload-icon">↑</div>
|
||||
<div class="upload-title">拖拽文件上传</div>
|
||||
<div class="upload-hint">或点击浏览文件系统</div>
|
||||
<div class="upload-title">Drag files to upload</div>
|
||||
<div class="upload-hint">Or click to browse the file system</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="file-list">
|
||||
|
|
@ -162,37 +162,37 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分割线 -->
|
||||
<!-- Divider -->
|
||||
<div class="console-divider">
|
||||
<span>输入参数</span>
|
||||
<span>Input Parameters</span>
|
||||
</div>
|
||||
|
||||
<!-- 输入区域 -->
|
||||
<!-- Input Area -->
|
||||
<div class="console-section">
|
||||
<div class="console-header">
|
||||
<span class="console-label">>_ 02 / 模拟提示词</span>
|
||||
<span class="console-label">>_ 02 / Simulation Prompt</span>
|
||||
</div>
|
||||
<div class="input-wrapper">
|
||||
<textarea
|
||||
v-model="formData.simulationRequirement"
|
||||
class="code-input"
|
||||
placeholder="// 用自然语言输入模拟或预测需求(例.武大若发布撤销肖某处分的公告,会引发什么舆情走向)"
|
||||
placeholder="// Describe your simulation or prediction request in natural language (e.g. If Wuhan University announces the revocation of disciplinary action against someone, what public opinion trends would emerge?)"
|
||||
rows="6"
|
||||
:disabled="loading"
|
||||
></textarea>
|
||||
<div class="model-badge">引擎: MiroFish-V1.0</div>
|
||||
<div class="model-badge">Engine: MiroFish-V1.0</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 启动按钮 -->
|
||||
<!-- Start Button -->
|
||||
<div class="console-section btn-section">
|
||||
<button
|
||||
class="start-engine-btn"
|
||||
@click="startSimulation"
|
||||
:disabled="!canSubmit || loading"
|
||||
>
|
||||
<span v-if="!loading">启动引擎</span>
|
||||
<span v-else>初始化中...</span>
|
||||
<span v-if="!loading">Start Engine</span>
|
||||
<span v-else>Initializing...</span>
|
||||
<span class="btn-arrow">→</span>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -200,7 +200,7 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 历史项目数据库 -->
|
||||
<!-- Historical Project Database -->
|
||||
<HistoryDatabase />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -213,41 +213,41 @@ import HistoryDatabase from '../components/HistoryDatabase.vue'
|
|||
|
||||
const router = useRouter()
|
||||
|
||||
// 表单数据
|
||||
// Form data
|
||||
const formData = ref({
|
||||
simulationRequirement: ''
|
||||
})
|
||||
|
||||
// 文件列表
|
||||
// File list
|
||||
const files = ref([])
|
||||
|
||||
// 状态
|
||||
// State
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
const isDragOver = ref(false)
|
||||
|
||||
// 文件输入引用
|
||||
// File input reference
|
||||
const fileInput = ref(null)
|
||||
|
||||
// 计算属性:是否可以提交
|
||||
// Computed property: whether submission is allowed
|
||||
const canSubmit = computed(() => {
|
||||
return formData.value.simulationRequirement.trim() !== '' && files.value.length > 0
|
||||
})
|
||||
|
||||
// 触发文件选择
|
||||
// Trigger file selection
|
||||
const triggerFileInput = () => {
|
||||
if (!loading.value) {
|
||||
fileInput.value?.click()
|
||||
}
|
||||
}
|
||||
|
||||
// 处理文件选择
|
||||
// Handle file selection
|
||||
const handleFileSelect = (event) => {
|
||||
const selectedFiles = Array.from(event.target.files)
|
||||
addFiles(selectedFiles)
|
||||
}
|
||||
|
||||
// 处理拖拽相关
|
||||
// Handle drag-related events
|
||||
const handleDragOver = (e) => {
|
||||
if (!loading.value) {
|
||||
isDragOver.value = true
|
||||
|
|
@ -266,7 +266,7 @@ const handleDrop = (e) => {
|
|||
addFiles(droppedFiles)
|
||||
}
|
||||
|
||||
// 添加文件
|
||||
// Add files
|
||||
const addFiles = (newFiles) => {
|
||||
const validFiles = newFiles.filter(file => {
|
||||
const ext = file.name.split('.').pop().toLowerCase()
|
||||
|
|
@ -275,12 +275,12 @@ const addFiles = (newFiles) => {
|
|||
files.value.push(...validFiles)
|
||||
}
|
||||
|
||||
// 移除文件
|
||||
// Remove file
|
||||
const removeFile = (index) => {
|
||||
files.value.splice(index, 1)
|
||||
}
|
||||
|
||||
// 滚动到底部
|
||||
// Scroll to bottom
|
||||
const scrollToBottom = () => {
|
||||
window.scrollTo({
|
||||
top: document.body.scrollHeight,
|
||||
|
|
@ -288,15 +288,15 @@ const scrollToBottom = () => {
|
|||
})
|
||||
}
|
||||
|
||||
// 开始模拟 - 立即跳转,API调用在Process页面进行
|
||||
// Start simulation - immediate redirection, API call handled on Process page
|
||||
const startSimulation = () => {
|
||||
if (!canSubmit.value || loading.value) return
|
||||
|
||||
// 存储待上传的数据
|
||||
// Store pending upload data
|
||||
import('../store/pendingUpload.js').then(({ setPendingUpload }) => {
|
||||
setPendingUpload(files.value, formData.value.simulationRequirement)
|
||||
|
||||
// 立即跳转到Process页面(使用特殊标识表示新建项目)
|
||||
// Immediately redirect to Process page (using special marker for new project)
|
||||
router.push({
|
||||
name: 'Process',
|
||||
params: { projectId: 'new' }
|
||||
|
|
@ -306,7 +306,7 @@ const startSimulation = () => {
|
|||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 全局变量与重置 */
|
||||
/* Global variables and reset */
|
||||
:root {
|
||||
--black: #000000;
|
||||
--white: #FFFFFF;
|
||||
|
|
@ -315,8 +315,8 @@ const startSimulation = () => {
|
|||
--gray-text: #666666;
|
||||
--border: #E5E5E5;
|
||||
/*
|
||||
使用 Space Grotesk 作为主要标题字体,JetBrains Mono 作为代码/标签字体
|
||||
确保已在 index.html 引入这些 Google Fonts
|
||||
Use Space Grotesk as the primary heading font, and JetBrains Mono as the code/tag font.
|
||||
Make sure these Google Fonts are imported in index.html.
|
||||
*/
|
||||
--font-mono: 'JetBrains Mono', monospace;
|
||||
--font-sans: 'Space Grotesk', 'Noto Sans SC', system-ui, sans-serif;
|
||||
|
|
@ -330,7 +330,7 @@ const startSimulation = () => {
|
|||
color: var(--black);
|
||||
}
|
||||
|
||||
/* 顶部导航 */
|
||||
/* Top navigation */
|
||||
.navbar {
|
||||
height: 60px;
|
||||
background: var(--black);
|
||||
|
|
@ -373,14 +373,14 @@ const startSimulation = () => {
|
|||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
/* 主要内容区 */
|
||||
/* Main content area */
|
||||
.main-content {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 60px 40px;
|
||||
}
|
||||
|
||||
/* Hero 区域 */
|
||||
/* Hero area */
|
||||
.hero-section {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
|
@ -511,7 +511,7 @@ const startSimulation = () => {
|
|||
}
|
||||
|
||||
.hero-logo {
|
||||
max-width: 500px; /* 调整logo大小 */
|
||||
max-width: 500px; /* Adjust logo size */
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
|
@ -533,7 +533,7 @@ const startSimulation = () => {
|
|||
border-color: var(--orange);
|
||||
}
|
||||
|
||||
/* Dashboard 双栏布局 */
|
||||
/* Dashboard two-column layout */
|
||||
.dashboard-section {
|
||||
display: flex;
|
||||
gap: 60px;
|
||||
|
|
@ -548,7 +548,7 @@ const startSimulation = () => {
|
|||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 左侧面板 */
|
||||
/* Left panel */
|
||||
.left-panel {
|
||||
flex: 0.8;
|
||||
}
|
||||
|
|
@ -604,7 +604,7 @@ const startSimulation = () => {
|
|||
color: #999;
|
||||
}
|
||||
|
||||
/* 项目模拟步骤介绍 */
|
||||
/* Project simulation steps introduction */
|
||||
.steps-container {
|
||||
border: 1px solid var(--border);
|
||||
padding: 30px;
|
||||
|
|
@ -660,14 +660,14 @@ const startSimulation = () => {
|
|||
color: var(--gray-text);
|
||||
}
|
||||
|
||||
/* 右侧交互控制台 */
|
||||
/* Right interactive console */
|
||||
.right-panel {
|
||||
flex: 1.2;
|
||||
}
|
||||
|
||||
.console-box {
|
||||
border: 1px solid #CCC; /* 外部实线 */
|
||||
padding: 8px; /* 内边距形成双重边框感 */
|
||||
border: 1px solid #CCC; /* Outer solid border */
|
||||
padding: 8px; /* Padding creates a double-border effect */
|
||||
}
|
||||
|
||||
.console-section {
|
||||
|
|
@ -835,7 +835,7 @@ const startSimulation = () => {
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 可点击状态(非禁用) */
|
||||
/* Clickable (not disabled) */
|
||||
.start-engine-btn:not(:disabled) {
|
||||
background: var(--black);
|
||||
border: 1px solid var(--black);
|
||||
|
|
@ -860,14 +860,14 @@ const startSimulation = () => {
|
|||
border: 1px solid #E5E5E5;
|
||||
}
|
||||
|
||||
/* 引导动画:微妙的边框脉冲 */
|
||||
/* Guided animation: subtle border pulses */
|
||||
@keyframes pulse-border {
|
||||
0% { box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.2); }
|
||||
70% { box-shadow: 0 0 0 6px rgba(0, 0, 0, 0); }
|
||||
100% { box-shadow: 0 0 0 0 rgba(0, 0, 0, 0); }
|
||||
}
|
||||
|
||||
/* 响应式适配 */
|
||||
/* Responsive adaptation */
|
||||
@media (max-width: 1024px) {
|
||||
.dashboard-section {
|
||||
flex-direction: column;
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
:class="{ active: viewMode === mode }"
|
||||
@click="viewMode = mode"
|
||||
>
|
||||
{{ { graph: '图谱', split: '双栏', workbench: '工作台' }[mode] }}
|
||||
{{ { graph: 'Graph', split: 'Split', workbench: 'Workbench' }[mode] }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -23,7 +23,7 @@
|
|||
<div class="header-right">
|
||||
<div class="workflow-step">
|
||||
<span class="step-num">Step 5/5</span>
|
||||
<span class="step-name">深度互动</span>
|
||||
<span class="step-name">Deep Interaction</span>
|
||||
</div>
|
||||
<div class="step-divider"></div>
|
||||
<span class="status-indicator" :class="statusClass">
|
||||
|
|
@ -47,7 +47,7 @@
|
|||
/>
|
||||
</div>
|
||||
|
||||
<!-- Right Panel: Step5 深度互动 -->
|
||||
<!-- Right Panel: Step5 Deep Interaction -->
|
||||
<div class="panel-wrapper right" :style="rightPanelStyle">
|
||||
<Step5Interaction
|
||||
:reportId="currentReportId"
|
||||
|
|
@ -78,7 +78,7 @@ const props = defineProps({
|
|||
reportId: String
|
||||
})
|
||||
|
||||
// Layout State - 默认切换到工作台视角
|
||||
// Layout State - Default to Workbench view
|
||||
const viewMode = ref('workbench')
|
||||
|
||||
// Data State
|
||||
|
|
@ -140,28 +140,28 @@ const toggleMaximize = (target) => {
|
|||
// --- Data Logic ---
|
||||
const loadReportData = async () => {
|
||||
try {
|
||||
addLog(`加载报告数据: ${currentReportId.value}`)
|
||||
addLog(`Loading report data: ${currentReportId.value}`)
|
||||
|
||||
// 获取 report 信息以获取 simulation_id
|
||||
// Get report information to obtain simulation_id
|
||||
const reportRes = await getReport(currentReportId.value)
|
||||
if (reportRes.success && reportRes.data) {
|
||||
const reportData = reportRes.data
|
||||
simulationId.value = reportData.simulation_id
|
||||
|
||||
if (simulationId.value) {
|
||||
// 获取 simulation 信息
|
||||
// Get simulation information
|
||||
const simRes = await getSimulation(simulationId.value)
|
||||
if (simRes.success && simRes.data) {
|
||||
const simData = simRes.data
|
||||
|
||||
// 获取 project 信息
|
||||
|
||||
// Get project information
|
||||
if (simData.project_id) {
|
||||
const projRes = await getProject(simData.project_id)
|
||||
if (projRes.success && projRes.data) {
|
||||
projectData.value = projRes.data
|
||||
addLog(`项目加载成功: ${projRes.data.project_id}`)
|
||||
|
||||
// 获取 graph 数据
|
||||
addLog(`Project loaded successfully: ${projRes.data.project_id}`)
|
||||
|
||||
// Get graph data
|
||||
if (projRes.data.graph_id) {
|
||||
await loadGraph(projRes.data.graph_id)
|
||||
}
|
||||
|
|
@ -170,10 +170,10 @@ const loadReportData = async () => {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
addLog(`获取报告信息失败: ${reportRes.error || '未知错误'}`)
|
||||
addLog(`Failed to fetch report: ${reportRes.error || 'Unknown error'}`)
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(`加载异常: ${err.message}`)
|
||||
addLog(`Load error: ${err.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -184,10 +184,10 @@ const loadGraph = async (graphId) => {
|
|||
const res = await getGraphData(graphId)
|
||||
if (res.success) {
|
||||
graphData.value = res.data
|
||||
addLog('图谱数据加载成功')
|
||||
addLog('Graph data loaded successfully')
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(`图谱加载失败: ${err.message}`)
|
||||
addLog(`Graph load failed: ${err.message}`)
|
||||
} finally {
|
||||
graphLoading.value = false
|
||||
}
|
||||
|
|
@ -208,7 +208,7 @@ watch(() => route.params.reportId, (newId) => {
|
|||
}, { immediate: true })
|
||||
|
||||
onMounted(() => {
|
||||
addLog('InteractionView 初始化')
|
||||
addLog('InteractionView initialized')
|
||||
loadReportData()
|
||||
})
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@
|
|||
|
||||
<!-- Right Panel: Step Components -->
|
||||
<div class="panel-wrapper right" :style="rightPanelStyle">
|
||||
<!-- Step 1: 图谱构建 -->
|
||||
<!-- Step 1: Build graph -->
|
||||
<Step1GraphBuild
|
||||
v-if="currentStep === 1"
|
||||
:currentPhase="currentPhase"
|
||||
|
|
@ -59,7 +59,7 @@
|
|||
:systemLogs="systemLogs"
|
||||
@next-step="handleNextStep"
|
||||
/>
|
||||
<!-- Step 2: 环境搭建 -->
|
||||
<!-- Step 2: Thiết lập môi trường -->
|
||||
<Step2EnvSetup
|
||||
v-else-if="currentStep === 2"
|
||||
:projectData="projectData"
|
||||
|
|
@ -90,8 +90,8 @@ const router = useRouter()
|
|||
const viewMode = ref('split') // graph | split | workbench
|
||||
|
||||
// Step State
|
||||
const currentStep = ref(1) // 1: 图谱构建, 2: 环境搭建, 3: 开始模拟, 4: 报告生成, 5: 深度互动
|
||||
const stepNames = ['图谱构建', '环境搭建', '开始模拟', '报告生成', '深度互动']
|
||||
const currentStep = ref(1) // 1: Xây dựng đồ thị, 2: Thiết lập môi trường, 3: Bắt đầu mô phỏng, 4: Tạo báo cáo, 5: Tương tác sâu
|
||||
const stepNames = ['Graph Construction', 'Environment Setup', 'Start Simulation', 'Report Generation', 'Deep Interaction']
|
||||
|
||||
// Data State
|
||||
const currentProjectId = ref(route.params.projectId)
|
||||
|
|
@ -159,11 +159,11 @@ const toggleMaximize = (target) => {
|
|||
const handleNextStep = (params = {}) => {
|
||||
if (currentStep.value < 5) {
|
||||
currentStep.value++
|
||||
addLog(`进入 Step ${currentStep.value}: ${stepNames[currentStep.value - 1]}`)
|
||||
addLog(`Entering Step ${currentStep.value}: ${stepNames[currentStep.value - 1]}`)
|
||||
|
||||
// 如果是从 Step 2 进入 Step 3,记录模拟轮数配置
|
||||
// Nếu chuyển từ Step 2 sang Step 3, ghi lại cấu hình số vòng mô phỏng
|
||||
if (currentStep.value === 3 && params.maxRounds) {
|
||||
addLog(`自定义模拟轮数: ${params.maxRounds} 轮`)
|
||||
addLog(`Custom simulation rounds: ${params.maxRounds} rounds`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -15,7 +15,7 @@
|
|||
:class="{ active: viewMode === mode }"
|
||||
@click="viewMode = mode"
|
||||
>
|
||||
{{ { graph: '图谱', split: '双栏', workbench: '工作台' }[mode] }}
|
||||
{{ { graph: 'Graph', split: 'Split', workbench: 'Workbench' }[mode] }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -23,7 +23,7 @@
|
|||
<div class="header-right">
|
||||
<div class="workflow-step">
|
||||
<span class="step-num">Step 4/5</span>
|
||||
<span class="step-name">报告生成</span>
|
||||
<span class="step-name">Report Generation</span>
|
||||
</div>
|
||||
<div class="step-divider"></div>
|
||||
<span class="status-indicator" :class="statusClass">
|
||||
|
|
@ -47,7 +47,7 @@
|
|||
/>
|
||||
</div>
|
||||
|
||||
<!-- Right Panel: Step4 报告生成 -->
|
||||
<!-- Right Panel: Step4 Report Generation -->
|
||||
<div class="panel-wrapper right" :style="rightPanelStyle">
|
||||
<Step4Report
|
||||
:reportId="currentReportId"
|
||||
|
|
@ -78,7 +78,7 @@ const props = defineProps({
|
|||
reportId: String
|
||||
})
|
||||
|
||||
// Layout State - 默认切换到工作台视角
|
||||
// Layout State - Default to Workbench view
|
||||
const viewMode = ref('workbench')
|
||||
|
||||
// Data State
|
||||
|
|
@ -139,28 +139,27 @@ const toggleMaximize = (target) => {
|
|||
// --- Data Logic ---
|
||||
const loadReportData = async () => {
|
||||
try {
|
||||
addLog(`加载报告数据: ${currentReportId.value}`)
|
||||
addLog(`Loading report data: ${currentReportId.value}`)
|
||||
|
||||
// 获取 report 信息以获取 simulation_id
|
||||
// Get report information to obtain simulation_id
|
||||
const reportRes = await getReport(currentReportId.value)
|
||||
if (reportRes.success && reportRes.data) {
|
||||
const reportData = reportRes.data
|
||||
simulationId.value = reportData.simulation_id
|
||||
|
||||
if (simulationId.value) {
|
||||
// 获取 simulation 信息
|
||||
// Get simulation information
|
||||
const simRes = await getSimulation(simulationId.value)
|
||||
if (simRes.success && simRes.data) {
|
||||
const simData = simRes.data
|
||||
|
||||
// 获取 project 信息
|
||||
// Get project information
|
||||
if (simData.project_id) {
|
||||
const projRes = await getProject(simData.project_id)
|
||||
if (projRes.success && projRes.data) {
|
||||
projectData.value = projRes.data
|
||||
addLog(`项目加载成功: ${projRes.data.project_id}`)
|
||||
|
||||
// 获取 graph 数据
|
||||
addLog(`Project loaded successfully: ${projRes.data.project_id}`)
|
||||
|
||||
// Get graph data
|
||||
if (projRes.data.graph_id) {
|
||||
await loadGraph(projRes.data.graph_id)
|
||||
}
|
||||
|
|
@ -169,10 +168,10 @@ const loadReportData = async () => {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
addLog(`获取报告信息失败: ${reportRes.error || '未知错误'}`)
|
||||
addLog(`Failed to fetch report: ${reportRes.error || 'Unknown error'}`)
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(`加载异常: ${err.message}`)
|
||||
addLog(`Load error: ${err.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -183,10 +182,10 @@ const loadGraph = async (graphId) => {
|
|||
const res = await getGraphData(graphId)
|
||||
if (res.success) {
|
||||
graphData.value = res.data
|
||||
addLog('图谱数据加载成功')
|
||||
addLog('Graph data loaded successfully')
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(`图谱加载失败: ${err.message}`)
|
||||
addLog(`Graph load failed: ${err.message}`)
|
||||
} finally {
|
||||
graphLoading.value = false
|
||||
}
|
||||
|
|
@ -207,7 +206,7 @@ watch(() => route.params.reportId, (newId) => {
|
|||
}, { immediate: true })
|
||||
|
||||
onMounted(() => {
|
||||
addLog('ReportView 初始化')
|
||||
addLog('ReportView initialized')
|
||||
loadReportData()
|
||||
})
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
:class="{ active: viewMode === mode }"
|
||||
@click="viewMode = mode"
|
||||
>
|
||||
{{ { graph: '图谱', split: '双栏', workbench: '工作台' }[mode] }}
|
||||
{{ { graph: 'Graph', split: 'Split', workbench: 'Workbench' }[mode] }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -23,7 +23,7 @@
|
|||
<div class="header-right">
|
||||
<div class="workflow-step">
|
||||
<span class="step-num">Step 3/5</span>
|
||||
<span class="step-name">开始模拟</span>
|
||||
<span class="step-name">Start Simulation</span>
|
||||
</div>
|
||||
<div class="step-divider"></div>
|
||||
<span class="status-indicator" :class="statusClass">
|
||||
|
|
@ -47,7 +47,7 @@
|
|||
/>
|
||||
</div>
|
||||
|
||||
<!-- Right Panel: Step3 开始模拟 -->
|
||||
<!-- Right Panel: Step3 Start Simulation -->
|
||||
<div class="panel-wrapper right" :style="rightPanelStyle">
|
||||
<Step3Simulation
|
||||
:simulationId="currentSimulationId"
|
||||
|
|
@ -87,9 +87,9 @@ const viewMode = ref('split')
|
|||
|
||||
// Data State
|
||||
const currentSimulationId = ref(route.params.simulationId)
|
||||
// 直接在初始化时从 query 参数获取 maxRounds,确保子组件能立即获取到值
|
||||
// Get maxRounds directly from the query parameter during initialization to ensure that child components can immediately access the value.
|
||||
const maxRounds = ref(route.query.maxRounds ? parseInt(route.query.maxRounds) : null)
|
||||
const minutesPerRound = ref(30) // 默认每轮30分钟
|
||||
const minutesPerRound = ref(30) // The default round lasts 30 minutes.
|
||||
const projectData = ref(null)
|
||||
const graphData = ref(null)
|
||||
const graphLoading = ref(false)
|
||||
|
|
@ -145,104 +145,102 @@ const toggleMaximize = (target) => {
|
|||
}
|
||||
|
||||
const handleGoBack = async () => {
|
||||
// 在返回 Step 2 之前,先关闭正在运行的模拟
|
||||
addLog('准备返回 Step 2,正在关闭模拟...')
|
||||
|
||||
// 停止轮询
|
||||
// Trước khi quay lại Step 2, cần đóng mô phỏng đang chạy
|
||||
addLog('Preparing to return to Step 2, shutting down simulation...')
|
||||
|
||||
// Dừng việc polling
|
||||
stopGraphRefresh()
|
||||
|
||||
try {
|
||||
// 先尝试优雅关闭模拟环境
|
||||
// Trước tiên, thử đóng môi trường mô phỏng một cách an toàn (gracefully)
|
||||
const envStatusRes = await getEnvStatus({ simulation_id: currentSimulationId.value })
|
||||
|
||||
|
||||
if (envStatusRes.success && envStatusRes.data?.env_alive) {
|
||||
addLog('正在关闭模拟环境...')
|
||||
addLog('Closing simulation environment...')
|
||||
try {
|
||||
await closeSimulationEnv({
|
||||
simulation_id: currentSimulationId.value,
|
||||
timeout: 10
|
||||
})
|
||||
addLog('✓ 模拟环境已关闭')
|
||||
addLog('✓ Simulation environment closed')
|
||||
} catch (closeErr) {
|
||||
addLog(`关闭模拟环境失败,尝试强制停止...`)
|
||||
addLog(`Failed to close simulation environment, attempting force stop...`)
|
||||
try {
|
||||
await stopSimulation({ simulation_id: currentSimulationId.value })
|
||||
addLog('✓ 模拟已强制停止')
|
||||
addLog('✓ The simulation has been forcibly stopped')
|
||||
} catch (stopErr) {
|
||||
addLog(`强制停止失败: ${stopErr.message}`)
|
||||
addLog(`Forced stop failed: ${stopErr.message}`)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 环境未运行,检查是否需要停止进程
|
||||
// Nếu môi trường không chạy, kiểm tra xem có cần dừng tiến trình hay không
|
||||
if (isSimulating.value) {
|
||||
addLog('正在停止模拟进程...')
|
||||
addLog('Stopping the simulation process...')
|
||||
try {
|
||||
await stopSimulation({ simulation_id: currentSimulationId.value })
|
||||
addLog('✓ 模拟已停止')
|
||||
addLog('✓ Simulation has stopped')
|
||||
} catch (err) {
|
||||
addLog(`停止模拟失败: ${err.message}`)
|
||||
addLog(`Stop simulation failed: ${err.message}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(`检查模拟状态失败: ${err.message}`)
|
||||
addLog(`Simulation status check failed: ${err.message}`)
|
||||
}
|
||||
|
||||
// 返回到 Step 2 (环境搭建)
|
||||
|
||||
// Return to Step 2 (Environment Setup)
|
||||
router.push({ name: 'Simulation', params: { simulationId: currentSimulationId.value } })
|
||||
}
|
||||
|
||||
const handleNextStep = () => {
|
||||
// Step3Simulation 组件会直接处理报告生成和路由跳转
|
||||
// 这个方法仅作为备用
|
||||
addLog('进入 Step 4: 报告生成')
|
||||
addLog('Entering Step 4: Report Generation')
|
||||
}
|
||||
|
||||
// --- Data Logic ---
|
||||
const loadSimulationData = async () => {
|
||||
try {
|
||||
addLog(`加载模拟数据: ${currentSimulationId.value}`)
|
||||
|
||||
// 获取 simulation 信息
|
||||
addLog(`Loading simulation data: ${currentSimulationId.value}`)
|
||||
|
||||
// Get simulation information
|
||||
const simRes = await getSimulation(currentSimulationId.value)
|
||||
if (simRes.success && simRes.data) {
|
||||
const simData = simRes.data
|
||||
|
||||
// 获取 simulation config 以获取 minutes_per_round
|
||||
// Get simulation config to get minutes_per_round
|
||||
try {
|
||||
const configRes = await getSimulationConfig(currentSimulationId.value)
|
||||
if (configRes.success && configRes.data?.time_config?.minutes_per_round) {
|
||||
minutesPerRound.value = configRes.data.time_config.minutes_per_round
|
||||
addLog(`时间配置: 每轮 ${minutesPerRound.value} 分钟`)
|
||||
addLog(`Time configuration: ${minutesPerRound.value} minutes per round`)
|
||||
}
|
||||
} catch (configErr) {
|
||||
addLog(`获取时间配置失败,使用默认值: ${minutesPerRound.value}分钟/轮`)
|
||||
addLog(`Failed to fetch time configuration, using default value: ${minutesPerRound.value} minutes per round`)
|
||||
}
|
||||
|
||||
// 获取 project 信息
|
||||
// Get project information
|
||||
if (simData.project_id) {
|
||||
const projRes = await getProject(simData.project_id)
|
||||
if (projRes.success && projRes.data) {
|
||||
projectData.value = projRes.data
|
||||
addLog(`项目加载成功: ${projRes.data.project_id}`)
|
||||
addLog(`Project loaded successfully: ${projRes.data.project_id}`)
|
||||
|
||||
// 获取 graph 数据
|
||||
// Get graph data
|
||||
if (projRes.data.graph_id) {
|
||||
await loadGraph(projRes.data.graph_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
addLog(`加载模拟数据失败: ${simRes.error || '未知错误'}`)
|
||||
addLog(`Failed to load simulation data: ${simRes.error || 'Unknown error'}`)
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(`加载异常: ${err.message}`)
|
||||
addLog(`Load error: ${err.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
const loadGraph = async (graphId) => {
|
||||
// 当正在模拟时,自动刷新不显示全屏 loading,以免闪烁
|
||||
// 手动刷新或初始加载时显示 loading
|
||||
// Khi đang chạy mô phỏng, auto-refresh sẽ không hiển thị loading toàn màn hình để tránh bị nhấp nháy
|
||||
// Chỉ hiển thị loading khi refresh thủ công hoặc khi tải lần đầu
|
||||
if (!isSimulating.value) {
|
||||
graphLoading.value = true
|
||||
}
|
||||
|
|
@ -252,11 +250,11 @@ const loadGraph = async (graphId) => {
|
|||
if (res.success) {
|
||||
graphData.value = res.data
|
||||
if (!isSimulating.value) {
|
||||
addLog('图谱数据加载成功')
|
||||
addLog('Map data loaded successfully')
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(`图谱加载失败: ${err.message}`)
|
||||
addLog(`Failed to load map: ${err.message}`)
|
||||
} finally {
|
||||
graphLoading.value = false
|
||||
}
|
||||
|
|
@ -268,13 +266,13 @@ const refreshGraph = () => {
|
|||
}
|
||||
}
|
||||
|
||||
// --- Auto Refresh Logic ---
|
||||
// Auto refresh
|
||||
let graphRefreshTimer = null
|
||||
|
||||
const startGraphRefresh = () => {
|
||||
if (graphRefreshTimer) return
|
||||
addLog('开启图谱实时刷新 (30s)')
|
||||
// 立即刷新一次,然后每30秒刷新
|
||||
addLog('Starting real-time graph refresh (30s)')
|
||||
// Refresh immediately, then every 30 seconds
|
||||
graphRefreshTimer = setInterval(refreshGraph, 30000)
|
||||
}
|
||||
|
||||
|
|
@ -282,7 +280,7 @@ const stopGraphRefresh = () => {
|
|||
if (graphRefreshTimer) {
|
||||
clearInterval(graphRefreshTimer)
|
||||
graphRefreshTimer = null
|
||||
addLog('停止图谱实时刷新')
|
||||
addLog('Stop the real-time refresh of the graph')
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -295,13 +293,7 @@ watch(isSimulating, (newValue) => {
|
|||
}, { immediate: true })
|
||||
|
||||
onMounted(() => {
|
||||
addLog('SimulationRunView 初始化')
|
||||
|
||||
// 记录 maxRounds 配置(值已在初始化时从 query 参数获取)
|
||||
if (maxRounds.value) {
|
||||
addLog(`自定义模拟轮数: ${maxRounds.value}`)
|
||||
}
|
||||
|
||||
addLog('SimulationRunView initialized')
|
||||
loadSimulationData()
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
:class="{ active: viewMode === mode }"
|
||||
@click="viewMode = mode"
|
||||
>
|
||||
{{ { graph: '图谱', split: '双栏', workbench: '工作台' }[mode] }}
|
||||
{{ { graph: 'Graph', split: 'Split', workbench: 'Workbench' }[mode] }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -23,7 +23,7 @@
|
|||
<div class="header-right">
|
||||
<div class="workflow-step">
|
||||
<span class="step-num">Step 2/5</span>
|
||||
<span class="step-name">环境搭建</span>
|
||||
<span class="step-name">Environment Setup</span>
|
||||
</div>
|
||||
<div class="step-divider"></div>
|
||||
<span class="status-indicator" :class="statusClass">
|
||||
|
|
@ -46,7 +46,7 @@
|
|||
/>
|
||||
</div>
|
||||
|
||||
<!-- Right Panel: Step2 环境搭建 -->
|
||||
<!-- Right Panel: Step2 Environment Setup -->
|
||||
<div class="panel-wrapper right" :style="rightPanelStyle">
|
||||
<Step2EnvSetup
|
||||
:simulationId="currentSimulationId"
|
||||
|
|
@ -137,7 +137,7 @@ const toggleMaximize = (target) => {
|
|||
}
|
||||
|
||||
const handleGoBack = () => {
|
||||
// 返回到 process 页面
|
||||
// Quay lai trang process
|
||||
if (projectData.value?.project_id) {
|
||||
router.push({ name: 'Process', params: { projectId: projectData.value.project_id } })
|
||||
} else {
|
||||
|
|
@ -146,122 +146,122 @@ const handleGoBack = () => {
|
|||
}
|
||||
|
||||
const handleNextStep = (params = {}) => {
|
||||
addLog('进入 Step 3: 开始模拟')
|
||||
addLog('Entering Step 3: Start Simulation')
|
||||
|
||||
// 记录模拟轮数配置
|
||||
// Ghi lai cau hinh so vong simulation
|
||||
if (params.maxRounds) {
|
||||
addLog(`自定义模拟轮数: ${params.maxRounds} 轮`)
|
||||
addLog(`Custom simulation rounds: ${params.maxRounds} rounds`)
|
||||
} else {
|
||||
addLog('使用自动配置的模拟轮数')
|
||||
addLog('Using auto-configured simulation rounds')
|
||||
}
|
||||
|
||||
// 构建路由参数
|
||||
// Tao tham so route
|
||||
const routeParams = {
|
||||
name: 'SimulationRun',
|
||||
params: { simulationId: currentSimulationId.value }
|
||||
}
|
||||
|
||||
// 如果有自定义轮数,通过 query 参数传递
|
||||
// Neu co so vong tuy chinh, truyen qua query
|
||||
if (params.maxRounds) {
|
||||
routeParams.query = { maxRounds: params.maxRounds }
|
||||
}
|
||||
|
||||
// 跳转到 Step 3 页面
|
||||
// Dieu huong sang trang Step 3
|
||||
router.push(routeParams)
|
||||
}
|
||||
|
||||
// --- Data Logic ---
|
||||
|
||||
/**
|
||||
* 检查并关闭正在运行的模拟
|
||||
* 当用户从 Step 3 返回到 Step 2 时,默认用户要退出模拟
|
||||
* Kiem tra va dong simulation dang chay
|
||||
* Khi nguoi dung quay tu Step 3 ve Step 2, mac dinh la ho muon thoat simulation
|
||||
*/
|
||||
const checkAndStopRunningSimulation = async () => {
|
||||
if (!currentSimulationId.value) return
|
||||
|
||||
try {
|
||||
// 先检查模拟环境是否存活
|
||||
// Kiem tra truoc xem moi truong simulation con song khong
|
||||
const envStatusRes = await getEnvStatus({ simulation_id: currentSimulationId.value })
|
||||
|
||||
if (envStatusRes.success && envStatusRes.data?.env_alive) {
|
||||
addLog('检测到模拟环境正在运行,正在关闭...')
|
||||
addLog('Detected running simulation environment, closing...')
|
||||
|
||||
// 尝试优雅关闭模拟环境
|
||||
// Thu dong moi truong theo cach graceful
|
||||
try {
|
||||
const closeRes = await closeSimulationEnv({
|
||||
simulation_id: currentSimulationId.value,
|
||||
timeout: 10 // 10秒超时
|
||||
timeout: 10 // Timeout 10 giay
|
||||
})
|
||||
|
||||
if (closeRes.success) {
|
||||
addLog('✓ 模拟环境已关闭')
|
||||
addLog('✓ Simulation environment closed')
|
||||
} else {
|
||||
addLog(`关闭模拟环境失败: ${closeRes.error || '未知错误'}`)
|
||||
// 如果优雅关闭失败,尝试强制停止
|
||||
addLog(`Failed to close simulation environment: ${closeRes.error || 'Unknown error'}`)
|
||||
// Neu dong graceful that bai, thu force stop
|
||||
await forceStopSimulation()
|
||||
}
|
||||
} catch (closeErr) {
|
||||
addLog(`关闭模拟环境异常: ${closeErr.message}`)
|
||||
// 如果优雅关闭异常,尝试强制停止
|
||||
addLog(`Exception while closing simulation environment: ${closeErr.message}`)
|
||||
// Neu dong graceful gap exception, thu force stop
|
||||
await forceStopSimulation()
|
||||
}
|
||||
} else {
|
||||
// 环境未运行,但可能进程还在,检查模拟状态
|
||||
// Moi truong khong chay, nhung co the process van con, kiem tra trang thai simulation
|
||||
const simRes = await getSimulation(currentSimulationId.value)
|
||||
if (simRes.success && simRes.data?.status === 'running') {
|
||||
addLog('检测到模拟状态为运行中,正在停止...')
|
||||
addLog('Detected simulation status running, stopping...')
|
||||
await forceStopSimulation()
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// 检查环境状态失败不影响后续流程
|
||||
console.warn('检查模拟状态失败:', err)
|
||||
// Loi kiem tra trang thai moi truong khong anh huong luong xu ly tiep theo
|
||||
console.warn('Failed to check simulation status:', err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制停止模拟
|
||||
* Force stop simulation
|
||||
*/
|
||||
const forceStopSimulation = async () => {
|
||||
try {
|
||||
const stopRes = await stopSimulation({ simulation_id: currentSimulationId.value })
|
||||
if (stopRes.success) {
|
||||
addLog('✓ 模拟已强制停止')
|
||||
addLog('✓ Simulation force-stopped')
|
||||
} else {
|
||||
addLog(`强制停止模拟失败: ${stopRes.error || '未知错误'}`)
|
||||
addLog(`Failed to force stop simulation: ${stopRes.error || 'Unknown error'}`)
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(`强制停止模拟异常: ${err.message}`)
|
||||
addLog(`Force stop simulation exception: ${err.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
const loadSimulationData = async () => {
|
||||
try {
|
||||
addLog(`加载模拟数据: ${currentSimulationId.value}`)
|
||||
addLog(`Loading simulation data: ${currentSimulationId.value}`)
|
||||
|
||||
// 获取 simulation 信息
|
||||
// Lay thong tin simulation
|
||||
const simRes = await getSimulation(currentSimulationId.value)
|
||||
if (simRes.success && simRes.data) {
|
||||
const simData = simRes.data
|
||||
|
||||
// 获取 project 信息
|
||||
// Lay thong tin project
|
||||
if (simData.project_id) {
|
||||
const projRes = await getProject(simData.project_id)
|
||||
if (projRes.success && projRes.data) {
|
||||
projectData.value = projRes.data
|
||||
addLog(`项目加载成功: ${projRes.data.project_id}`)
|
||||
addLog(`Project loaded: ${projRes.data.project_id}`)
|
||||
|
||||
// 获取 graph 数据
|
||||
// Lay du lieu graph
|
||||
if (projRes.data.graph_id) {
|
||||
await loadGraph(projRes.data.graph_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
addLog(`加载模拟数据失败: ${simRes.error || '未知错误'}`)
|
||||
addLog(`Failed to load simulation data: ${simRes.error || 'Unknown error'}`)
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(`加载异常: ${err.message}`)
|
||||
addLog(`Load exception: ${err.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -271,10 +271,10 @@ const loadGraph = async (graphId) => {
|
|||
const res = await getGraphData(graphId)
|
||||
if (res.success) {
|
||||
graphData.value = res.data
|
||||
addLog('图谱数据加载成功')
|
||||
addLog('Graph data loaded')
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(`图谱加载失败: ${err.message}`)
|
||||
addLog(`Graph load failed: ${err.message}`)
|
||||
} finally {
|
||||
graphLoading.value = false
|
||||
}
|
||||
|
|
@ -287,12 +287,12 @@ const refreshGraph = () => {
|
|||
}
|
||||
|
||||
onMounted(async () => {
|
||||
addLog('SimulationView 初始化')
|
||||
addLog('SimulationView initialized')
|
||||
|
||||
// 检查并关闭正在运行的模拟(用户从 Step 3 返回时)
|
||||
// Kiem tra va dong simulation dang chay (khi nguoi dung quay ve tu Step 3)
|
||||
await checkAndStopRunningSimulation()
|
||||
|
||||
// 加载模拟数据
|
||||
// Tai du lieu simulation
|
||||
loadSimulationData()
|
||||
})
|
||||
</script>
|
||||
|
|
|
|||
Loading…
Reference in New Issue