Merge pull request #1 from TQuynh109/feature/translation

Feature/translation
This commit is contained in:
Nguyễn Ngọc Thanh Thư 2026-03-26 12:30:05 +07:00 committed by GitHub
commit 34bab77a72
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
60 changed files with 6118 additions and 6075 deletions

12
.gitignore vendored
View File

@ -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/

View File

@ -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"]

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -1,6 +1,7 @@
"""
项目上下文管理
用于在服务端持久化项目状态避免前端在接口间传递大量数据
Quản 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 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 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 -> 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 mọi file liên quan của khỏi server
Args:
project_id: 项目ID
project_id: 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 người dùng upload lên vào kho dự án
Args:
project_id: 项目ID
file_storage: Flask的FileStorage对象
original_filename: 原始文件名
project_id: đị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):

View File

@ -1,6 +1,6 @@
"""
任务状态管理
用于跟踪长时间运行的任务如图谱构建
Quản 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 ( 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 Task
Đảm bảo quản 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 cho vào hàng đợi (quản 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 đị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: 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 tả ngắn gọn hiện trạng làm
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)

View File

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,8 @@
"""Zep Graph 分页读取工具。
"""Tiện ích đọc phân trang cho Zep Graph.
Zep node/edge 列表接口使用 UUID cursor 分页
本模块封装自动翻页逻辑含单页重试对调用方透明地返回完整列表
API danh sách node/edge của Zep dùng UUID cursor để phân trang,
module này đóng gói logic tự động lật trang (kèm retry từng trang)
trả về đầy đủ danh sách cho bên gọi một cách trong suốt.
"""
from __future__ import annotations
@ -31,7 +32,7 @@ def _fetch_page_with_retry(
page_description: str = "page",
**kwargs: Any,
) -> list[Any]:
"""单页请求,失败时指数退避重试。仅重试网络/IO类瞬态错误。"""
"""Yêu cầu 1 trang, retry với exponential backoff khi thất bại. Chỉ retry lỗi tạm thời mạng/IO."""
if max_retries < 1:
raise ValueError("max_retries must be >= 1")
@ -64,7 +65,7 @@ def fetch_all_nodes(
max_retries: int = _DEFAULT_MAX_RETRIES,
retry_delay: float = _DEFAULT_RETRY_DELAY,
) -> list[Any]:
"""分页获取图谱节点,最多返回 max_items 条(默认 2000。每页请求自带重试。"""
"""Lấy node theo trang, tối đa max_items mục (mặc định 2000). Mỗi trang đều có retry."""
all_nodes: list[Any] = []
cursor: str | None = None
page_num = 0
@ -109,7 +110,7 @@ def fetch_all_edges(
max_retries: int = _DEFAULT_MAX_RETRIES,
retry_delay: float = _DEFAULT_RETRY_DELAY,
) -> list[Any]:
"""分页获取图谱所有边,返回完整列表。每页请求自带重试。"""
"""Lấy toàn bộ edge theo trang, trả về đầy đủ danh sách. Mỗi trang đều có retry."""
all_edges: list[Any] = []
cursor: str | None = None
page_num = 0

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@ -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:

View 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

View File

@ -3,11 +3,11 @@
</template>
<script setup>
// 使 Vue Router
// S dng Vue Router đ qun 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;
}

View File

@ -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 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) {

View File

@ -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 {

View File

@ -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) => {

View File

@ -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 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 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 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 } })
}

View File

@ -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) // Mc đnh hin th nhãn cnh
const expandedSelfLoops = ref(new Set()) // Các self-loop đang m
const showSimulationFinishedHint = ref(false) // Thông báo sau khi mô phng kết thúc
const wasSimulating = ref(false) // Theo dõi trưc đó có đang mô phng hay không
//
// Đóng thông báo kết thúc mô phng
const dismissFinishedHint = () => {
showSimulationFinishedHint.value = false
}
// isSimulating
// Theo dõi thay đi isSimulating đ phát hin kết thúc mô phng
watch(() => props.isSimulating, (newValue, oldValue) => {
if (wasSimulating.value && !newValue) {
//
// T trng thái mô phng chuyn sang không mô phng, hin th thông báo kết thúc
showSimulationFinishedHint.value = true
}
wasSimulating.value = newValue
}, { immediate: true })
// /
// Toggle trng thái m/đóng ca 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 loi entity đ hin th legend
const entityTypes = computed(() => {
if (!props.graphData?.nodes) return []
const typeMap = {}
//
// Bng 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 thi 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 trng thái m
}
let currentSimulation = null
@ -328,7 +328,7 @@ let linkLabelBgRef = null
const renderGraph = () => {
if (!graphSvg.value || !props.graphData) return
// 仿
// Dng simulation trưc đó
if (currentSimulation) {
currentSimulation.stop()
}
@ -349,7 +349,7 @@ const renderGraph = () => {
if (nodesData.length === 0) return
// Prep data
// Chun b d liu
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 liu cnh, tính s lưng và ch s cnh gia cùng mt cp 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))
//
// Thng kê s cnh gia mi cp node, thu thp self-loop
tempEdges.forEach(e => {
if (e.source_node_uuid === e.target_node_uuid) {
// -
// Self-loop - gom vào mng
if (!selfLoopEdges[e.source_node_uuid]) {
selfLoopEdges[e.source_node_uuid] = []
}
@ -386,9 +386,9 @@ const renderGraph = () => {
}
})
//
// Ghi nhn đang x lý cnh th my ca mi cp 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 - mi node ch thêm mt self-loop tng hp
if (processedSelfLoopNodes.has(e.source_node_uuid)) {
return //
return // Đã x lý ri, 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 ca self-loop
}
})
return
@ -428,19 +428,19 @@ const renderGraph = () => {
const currentIndex = edgePairIndex[pairKey] || 0
edgePairIndex[pairKey] = currentIndex + 1
// UUID < UUID
// Kim tra hưng cnh có trùng vi hưng chun hay không (source UUID < target UUID)
const isReversed = e.source_node_uuid > e.target_node_uuid
// 线
// Tính đ cong: nhiu cnh thì tách ra, mt cnh thì là đưng thng
let curvature = 0
if (totalCount > 1) {
//
//
// Phân b đ cong đng đu, đm bo d phân bit
// Phm vi đ cong tăng theo s lưng cnh, càng nhiu cnh thì phm vi càng ln
const curvatureRange = Math.min(1.2, 0.6 + totalCount * 0.15)
curvature = ((currentIndex / (totalCount - 1)) - 0.5) * curvatureRange * 2
//
//
// Nếu hưng cnh ngưc vi hưng chun, đo đ cong
// Nh vy tt c cnh đưc phân b trong cùng h quy chiếu, tránh chng ln 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 - điu chnh khong cách node theo s lưng cnh
const simulation = d3.forceSimulation(nodes)
.force('link', d3.forceLink(edges).id(d => d.id).distance(d => {
//
// 150 40
// Điu chnh khong cách da trên s cnh gia cp node này
// Khong cách cơ bn là 150, mi cnh 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 lc hút v trung tâm đ các cm node đc lp gom li gn 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 dn cho đưng cong
const getLinkPath = (d) => {
const sx = d.source.x, sy = d.source.y
const tx = d.target.x, ty = d.target.y
//
// Kim tra self-loop
if (d.isSelfLoop) {
//
// Self-loop: v mt cung tròn đi ra t node ri quay li
const loopRadius = 30
//
const x1 = sx + 8 //
// Bt đu t bên phi node, đi mt vòng ri quay li
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 chiu kim đng h)
return `M${x1},${y1} A${loopRadius},${loopRadius} 0 1,1 ${x2},${y2}`
}
if (d.curvature === 0) {
// 线
// Đưng thng
return `M${sx},${sy} L${tx},${ty}`
}
// 线 -
// Tính đim điu khin ca đưng cong - điu chnh đng theo s cnh và khong cách
const dx = tx - sx, dy = ty - sy
const dist = Math.sqrt(dx * dx + dy * dy)
// 线线
//
// Đ lch vuông góc vi hưng ni, tính theo t l khong cách đ đm bo nhìn rõ
// Càng nhiu cnh, t l đ lch theo khong cách càng ln
const pairTotal = d.pairTotal || 1
const offsetRatio = 0.25 + pairTotal * 0.05 // 25%5%
const offsetRatio = 0.25 + pairTotal * 0.05 // Giá tr cơ bn là 25%, và nó tăng thêm 5% cho mi cnh 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 đim ca đư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
//
// Kim tra self-loop
if (d.isSelfLoop) {
//
// V trí label self-loop: bên phi 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 đim ca đưng Bezier bc hai ti 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 thc Bezier bc hai B(t) = (1-t)²P0 + 2(1-t)tP1 + t²P2, vi 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 cnh đã chn 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 cnh đang chn
d3.select(event.target).attr('stroke', '#3498db').attr('stroke-width', 3)
selectedItem.value = {
@ -590,7 +590,7 @@ const renderGraph = () => {
}
})
// Link labels background (使)
// Nn label ca cnh (nn trng 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 cnh 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 cnh 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 đ điu khin hin 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 nhn v trí, không restart simulation (phân bit 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) => {
//
// Kim tra có thc s bt đu drag hay chưa (dch chuyn 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 tht s drag mi restart simulation
d._isDragging = true
simulation.alphaTarget(0.3).restart()
}
@ -686,7 +686,7 @@ const renderGraph = () => {
}
})
.on('end', (event, d) => {
// 仿
// Ch khi thc s drag mi cho simulation gim dn
if (d._isDragging) {
simulation.alphaTarget(0)
}
@ -697,12 +697,12 @@ const renderGraph = () => {
)
.on('click', (event, d) => {
event.stopPropagation()
//
// Reset style ca tt 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 chn
d3.select(event.target).attr('stroke', '#E91E63').attr('stroke-width', 4)
//
// Highlight các cnh ni vi 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', () => {
// 线
// Cp nht đưng cong
link.attr('d', d => getLinkPath(d))
//
// Cp nht v trí nhãn cnh (không xoay, hin 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', '') // Loi b chuyn đng xoay, gi nguyên v trí nm ngang.
})
//
// Cp nht nn nhãn cnh
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', '') // Loi b phép xoay
})
node
@ -773,7 +773,7 @@ const renderGraph = () => {
.attr('y', d => d.y)
})
//
// Click vào vùng trng đ đó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 tc hin th nhãn cnh
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);

View File

@ -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()
//
// Trng 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 hin đưc chn (dùng cho modal)
let observer = null
let isAnimating = false //
let expandDebounceTimer = null //
let pendingState = null //
let isAnimating = false // Khóa animation, tránh nhp nháy
let expandDebounceTimer = null // B hn gi debounce
let pendingState = null // Ghi li trng thái đích ch thc thi
// -
// Cu hình layout th - điu chnh sang t l rng hơn
const CARDS_PER_ROW = 4
const CARD_WIDTH = 280
const CARD_HEIGHT = 280
const CARD_GAP = 24
//
// Tính đng style chiu cao container
const containerStyle = computed(() => {
if (!isExpanded.value) {
//
// Trng thái thu gn: chiu cao c đnh
return { minHeight: '420px' }
}
//
// Trng thái m rng: tính chiu 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 chiu cao thc tế cn dùng: s hàng * chiu cao th + (s hàng-1) * khong cách + mt ít khong đm dưi
const expandedHeight = rows * CARD_HEIGHT + (rows - 1) * CARD_GAP + 10
return { minHeight: `${expandedHeight}px` }
})
//
// Ly style ca th
const getCardStyle = (index) => {
const total = projects.value.length
if (isExpanded.value) {
//
// Trng thái m rng: layout dng 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 hin ti đ căn gia tng 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 rng xung dưi, tăng khong cách vi tiêu đ
const y = 20 + row * (CARD_HEIGHT + CARD_GAP)
return {
@ -267,14 +267,14 @@ const getCardStyle = (index) => {
transition: transition
}
} else {
//
// Trng thái thu gn: xếp chng hình qut
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
//
// Điu chnh v trí bt đu, gn tiêu đ nhưng vn gi khong cách phù hp
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) => {
}
}
//
// Ly 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 bt đu
return 'not-started'
} else if (current >= total) {
//
// Đã hoàn thành
return 'completed'
} else {
//
// Đang thc hin
return 'in-progress'
}
}
//
// Format ngày tháng (ch hin th phn ngày)
const formatDate = (dateStr) => {
if (!dateStr) return ''
try {
@ -316,7 +316,7 @@ const formatDate = (dateStr) => {
}
}
// :
// Format thi gian (hin th gi:phút)
const formatTime = (dateStr) => {
if (!dateStr) return ''
try {
@ -329,35 +329,35 @@ const formatTime = (dateStr) => {
}
}
//
// Ct ngn văn bn
const truncateText = (text, maxLength) => {
if (!text) return ''
return text.length > maxLength ? text.slice(0, maxLength) + '...' : text
}
// 20
// To tiêu đ t yêu cu mô phng (ly 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 đ hin th (ly 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 hin th s vòng (vòng hin ti/tng 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`
}
//
// Ly loi 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'
}
//
// Ly text nhãn loi file
const getFileTypeLabel = (filename) => {
if (!filename) return 'FILE'
const ext = filename.split('.').pop()?.toUpperCase()
return ext || 'FILE'
}
//
// Ct ngn tên file (gi li phn m rng)
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
// Điu hưng đến trang xây dng graph (Project)
const goToProject = () => {
if (selectedProject.value?.project_id) {
router.push({
@ -412,7 +412,7 @@ const goToProject = () => {
}
}
// Simulation
// Điu hưng đến trang cu hình môi trưng (Simulation)
const goToSimulation = () => {
if (selectedProject.value?.simulation_id) {
router.push({
@ -423,7 +423,7 @@ const goToSimulation = () => {
}
}
// Report
// Điu 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 = () => {
}
}
//
// Ti lch 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
// Khi to IntersectionObserver
const initObserver = () => {
if (observer) {
observer.disconnect()
@ -461,47 +461,47 @@ const initObserver = () => {
entries.forEach((entry) => {
const shouldExpand = entry.isIntersecting
//
// Cp nht trng thái đích ch thc thi (dù đang animation vn phi ghi li trng thái mi nht)
pendingState = shouldExpand
//
// Xóa b hn gi debounce trưc đó (ý đnh cun mi s ghi đè ý đnh cũ)
if (expandDebounceTimer) {
clearTimeout(expandDebounceTimer)
expandDebounceTimer = null
}
//
// Nếu đang animation thì ch ghi trng thái, ch x lý sau khi animation kết thúc
if (isAnimating) return
//
// Nếu trng thái đích trùng vi trng thái hin ti thì không cn x lý
if (shouldExpand === isExpanded.value) {
pendingState = null
return
}
// 使
// (50ms)(200ms)
// Dùng debounce đ trì hoãn chuyn trng thái, tránh nhp nháy nhanh
// Khi m rng thì delay ngn hơn (50ms), khi thu gn delay dài hơn (200ms) đ n đnh hơn
const delay = shouldExpand ? 50 : 200
expandDebounceTimer = setTimeout(() => {
//
// Kim tra có đang animation không
if (isAnimating) return
//
// Kim tra trng thái ch có còn cn thc thi không (có th đã b ln cun sau ghi đè)
if (pendingState === null || pendingState === isExpanded.value) return
//
// Bt khóa animation
isAnimating = true
isExpanded.value = pendingState
pendingState = null
//
// Sau khi animation hoàn tt thì m khóa, đng thi kim tra xem có trng thái mi đang ch hay không
setTimeout(() => {
isAnimating = false
//
// Sau khi animation kết thúc, kim tra xem có trng thái ch mi không
if (pendingState !== null && pendingState !== isExpanded.value) {
//
// Delay thêm mt chút trưc khi thc thi đ tránh chuyn quá nhanh
expandDebounceTimer = setTimeout(() => {
if (pendingState !== null && pendingState !== isExpanded.value) {
isAnimating = true
@ -518,20 +518,20 @@ const initObserver = () => {
})
},
{
// 使使
// Dùng nhiu ngưng đ vic phát hin mưt hơn
threshold: [0.4, 0.6, 0.8],
// rootMargin
// Điu chnh rootMargin, co đáy viewport lên trên đ phi cun nhiu hơn mi kích hot m rng
rootMargin: '0px 0px -150px 0px'
}
)
//
// Bt đu quan sát
if (historyContainer.value) {
observer.observe(historyContainer.value)
}
}
//
// Theo dõi thay đi route, khi quay li trang ch thì ti li d liu
watch(() => route.path, (newPath) => {
if (newPath === '/') {
loadHistory()
@ -539,28 +539,28 @@ watch(() => route.path, (newPath) => {
})
onMounted(async () => {
// DOM
// Đm bo DOM đã render xong ri mi ti d liu
await nextTick()
await loadHistory()
// DOM
// Ch DOM render xong ri mi khi to observer
setTimeout(() => {
initObserver()
}, 100)
})
// 使 keep-alive
// Nếu dùng keep-alive, ti li d liu khi component đưc kích hot
onActivated(() => {
loadHistory()
})
onUnmounted(() => {
// Intersection Observer
// Dn dp Intersection Observer
if (observer) {
observer.disconnect()
observer = null
}
//
// Dn dp b hn 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;

View File

@ -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 - to simulation và chuyn 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
// Chuyn đến trang mô phng
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

View File

@ -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 content nhưng chưa được xử 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 đã 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 truyn t Step2
minutesPerRound: {
type: Number,
default: 30 // 30
default: 30 // mc đnh mi 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 bt đu, 1: đang chy, 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 (cng dn tăng dn)
const actionIds = ref(new Set()) // tp ID action đ kh trùng lp
const scrollContainer = ref(null)
// Computed
//
// Hin th action theo th t thi gian (mi nht cui, tc phía dưi)
const chronologicalActions = computed(() => {
return allActions.value
})
//
// Đếm action ca tng 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 thi gian mô phng đã trôi qua (tính theo round và phút mi 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
// Thi gian mô phng đã trôi qua ca Twitter
const twitterElapsedTime = computed(() => {
return formatElapsedTime(runStatus.value.twitter_current_round || 0)
})
// Reddit
// Thi gian mô phng đã trôi qua ca 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 trng thái (dùng đ khi đng li mô phng)
const resetAllState = () => {
phase.value = 0
runStatus.value = {}
@ -373,46 +373,46 @@ const resetAllState = () => {
startError.value = null
isStarting.value = false
isStopping.value = false
stopPolling() //
stopPolling() // dng các polling cũ nếu có
}
//
// Khi đng mô phng
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 ln mô phng 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 bt đu li t đu
enable_graph_memory_update: true // bt cp nht 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
}
}
//
// Dng mô phng
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 trng thái
let statusTimer = null
let detailTimer = null
@ -482,7 +482,7 @@ const stopPolling = () => {
}
}
//
// Theo dõi round trưc đó ca tng platform đ phát hin thay đi và ghi log
const prevTwitterRound = ref(0)
const prevRedditRound = ref(0)
@ -497,7 +497,7 @@ const fetchRunStatus = async () => {
runStatus.value = data
//
// Kim tra thay đi round ca tng 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
// Kim tra mô phng đã hoàn thành chưa (theo runner_status hoc trng thái hoàn thành ca platform)
const isCompleted = data.runner_status === 'completed' || data.runner_status === 'stopped'
// runner_status
// twitter_completed reddit_completed
// Kim tra thêm: nếu backend chưa kp cp nht runner_status nhưng platform đã báo xong
// Da 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)
}
}
//
// Kim tra tt c platform đang bt đã hoàn thành chưa
const checkPlatformsCompleted = (data) => {
// false
// Nếu không có d liu platform nào thì tr v false
if (!data) return false
//
// Kim tra trng thái hoàn thành ca tng platform
const twitterCompleted = data.twitter_completed === true
const redditCompleted = data.reddit_completed === true
//
// actions_count count > 0 running true
// Nếu có ít nht mt platform hoàn thành, kim tra xem tt c platform đang bt đã hoàn thành chưa
// Dùng actions_count đ suy ra platform có đưc bt không (count > 0 hoc tng 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 bt thì tr v false
if (!twitterEnabled && !redditEnabled) return false
//
// Kim tra tt c platform đang bt đã 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 đ ly đy đ danh sách action
const serverActions = res.data.all_actions || []
//
// Thêm tăng dn các action mi (kh trùng lp)
let newActionsAdded = 0
serverActions.forEach(action => {
// ID
// To 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 cun, đ ngưi dùng t do xem timeline
// Action mi 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}`)
//
// Chuyn 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 xung cui
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

View File

@ -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;

View File

@ -1,6 +1,7 @@
/**
* 临时存储待上传的文件和需求
* 用于首页点击启动引擎后立即跳转在Process页面再进行API调用
* Lưu tạm file cần upload 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'

View File

@ -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 = () => {
})
}
// - APIProcess
// 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;

View File

@ -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>

View File

@ -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 dng đ th, 2: Thiết lp môi trưng, 3: Bt đu mô phng, 4: To 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 chuyn t Step 2 sang Step 3, ghi li cu hình s vòng mô phng
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

View File

@ -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>

View File

@ -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 li Step 2, cn đóng mô phng đang chy
addLog('Preparing to return to Step 2, shutting down simulation...')
// Dng vic polling
stopGraphRefresh()
try {
//
// Trưc tiên, th đóng môi trưng mô phng mt 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 chy, kim tra xem có cn dng 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 chy mô phng, auto-refresh s không hin th loading toàn màn hình đ tránh b nhp nháy
// Ch hin th loading khi refresh th công hoc khi ti ln đ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()
})

View File

@ -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>