MicroFish/backend/app/api/simulation.py

2717 lines
99 KiB
Python

"""
API route liên quan đến mô phỏng.
Step2: Đọc và lọc thực thể Zep, chuẩn bị và chạy mô phỏng OASIS (tự động hoàn toàn).
"""
import os
import traceback
from flask import request, jsonify, send_file
from . import simulation_bp
from ..config import Config
from ..services.zep_entity_reader import ZepEntityReader
from ..services.oasis_profile_generator import OasisProfileGenerator
from ..services.simulation_manager import SimulationManager, SimulationStatus
from ..services.simulation_runner import SimulationRunner, RunnerStatus
from ..utils.logger import get_logger
from ..models.project import ProjectManager
logger = get_logger('mirofish.api.simulation')
# Tiền tố tối ưu cho Interview prompt
# Thêm tiền tố này để tránh Agent gọi công cụ, trả lời trực tiếp bằng văn bản
# INTERVIEW_PROMPT_PREFIX = "Based on your persona, all past memories and actions, reply directly in plain text without calling any tools:"
INTERVIEW_PROMPT_PREFIX = "Dựa trên nhân vật, tất cả ký ức và hành động trong quá khứ của bạn, hãy trả lời trực tiếp bằng văn bản thuần túy mà không gọi bất kỳ công cụ nào:"
def optimize_interview_prompt(prompt: str) -> str:
"""
Tối ưu câu hỏi Interview, thêm tiền tố để tránh Agent gọi công cụ.
Args:
prompt: Câu hỏi gốc
Returns:
Câu hỏi đã tối ưu
"""
if not prompt:
return prompt
# Tránh thêm tiền tố lặp lại
if prompt.startswith(INTERVIEW_PROMPT_PREFIX):
return prompt
return f"{INTERVIEW_PROMPT_PREFIX}{prompt}"
# ============== API đọc thực thể ==============
@simulation_bp.route('/entities/<graph_id>', methods=['GET'])
def get_graph_entities(graph_id: str):
"""
Lấy toàn bộ thực thể trong đồ thị (đã lọc).
Chỉ trả về các node phù hợp với loại thực thể đã định nghĩa trước.
(Label không chỉ là node Entity)
Tham số query:
entity_types: Danh sách loại thực thể phân tách bằng dấu phẩy (tùy chọn, dùng để lọc thêm)
enrich: Có lấy thông tin cạnh liên quan hay không (mặc định true)
"""
try:
if not Config.ZEP_API_KEY:
return jsonify({
"success": False,
"error": "ZEP_API_KEY is not configured"
}), 500
entity_types_str = request.args.get('entity_types', '')
entity_types = [t.strip() for t in entity_types_str.split(',') if t.strip()] if entity_types_str else None
enrich = request.args.get('enrich', 'true').lower() == 'true'
logger.info(f"Fetching graph entities: graph_id={graph_id}, entity_types={entity_types}, enrich={enrich}")
reader = ZepEntityReader()
result = reader.filter_defined_entities(
graph_id=graph_id,
defined_entity_types=entity_types,
enrich_with_edges=enrich
)
return jsonify({
"success": True,
"data": result.to_dict()
})
except Exception as e:
logger.error(f"Failed to fetch graph entities: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
@simulation_bp.route('/entities/<graph_id>/<entity_uuid>', methods=['GET'])
def get_entity_detail(graph_id: str, entity_uuid: str):
"""Lấy thông tin chi tiết của một thực thể."""
try:
if not Config.ZEP_API_KEY:
return jsonify({
"success": False,
"error": "ZEP_API_KEY is not configured"
}), 500
reader = ZepEntityReader()
entity = reader.get_entity_with_context(graph_id, entity_uuid)
if not entity:
return jsonify({
"success": False,
"error": f"Entity does not exist: {entity_uuid}"
}), 404
return jsonify({
"success": True,
"data": entity.to_dict()
})
except Exception as e:
logger.error(f"Failed to fetch entity details: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
@simulation_bp.route('/entities/<graph_id>/by-type/<entity_type>', methods=['GET'])
def get_entities_by_type(graph_id: str, entity_type: str):
"""Lấy toàn bộ thực thể theo loại chỉ định."""
try:
if not Config.ZEP_API_KEY:
return jsonify({
"success": False,
"error": "ZEP_API_KEY is not configured"
}), 500
enrich = request.args.get('enrich', 'true').lower() == 'true'
reader = ZepEntityReader()
entities = reader.get_entities_by_type(
graph_id=graph_id,
entity_type=entity_type,
enrich_with_edges=enrich
)
return jsonify({
"success": True,
"data": {
"entity_type": entity_type,
"count": len(entities),
"entities": [e.to_dict() for e in entities]
}
})
except Exception as e:
logger.error(f"Failed to fetch entities: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
# ============== API quản lý mô phỏng ==============
@simulation_bp.route('/create', methods=['POST'])
def create_simulation():
"""
Tạo mô phỏng mới.
Lưu ý: Các tham số như max_rounds được LLM tạo thông minh, không cần đặt thủ công.
Yêu cầu (JSON):
{
"project_id": "proj_xxxx", // bắt buộc
"graph_id": "mirofish_xxxx", // tùy chọn, nếu không cung cấp sẽ lấy từ project
"enable_twitter": true, // tùy chọn, mặc định true
"enable_reddit": true // tùy chọn, mặc định true
}
Trả về:
{
"success": true,
"data": {
"simulation_id": "sim_xxxx",
"project_id": "proj_xxxx",
"graph_id": "mirofish_xxxx",
"status": "created",
"enable_twitter": true,
"enable_reddit": true,
"created_at": "2025-12-01T10:00:00"
}
}
"""
try:
data = request.get_json() or {}
project_id = data.get('project_id')
if not project_id:
return jsonify({
"success": False,
"error": "Please provide project_id"
}), 400
project = ProjectManager.get_project(project_id)
if not project:
return jsonify({
"success": False,
"error": f"Project does not exist: {project_id}"
}), 404
graph_id = data.get('graph_id') or project.graph_id
if not graph_id:
return jsonify({
"success": False,
"error": "Project graph has not been built yet. Please call /api/graph/build first"
}), 400
manager = SimulationManager()
state = manager.create_simulation(
project_id=project_id,
graph_id=graph_id,
enable_twitter=data.get('enable_twitter', True),
enable_reddit=data.get('enable_reddit', True),
)
return jsonify({
"success": True,
"data": state.to_dict()
})
except Exception as e:
logger.error(f"Failed to create simulation: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
def _check_simulation_prepared(simulation_id: str) -> tuple:
"""
Kiểm tra mô phỏng đã được chuẩn bị xong chưa.
Điều kiện kiểm tra:
1. `state.json` tồn tại và `status` là "ready"
2. Các file cần thiết tồn tại: reddit_profiles.json, twitter_profiles.csv, simulation_config.json
Lưu ý: Script chạy (run_*.py) được giữ ở thư mục backend/scripts/ và không còn sao chép vào thư mục mô phỏng.
Args:
simulation_id: simulation ID
Returns:
(is_prepared: bool, info: dict)
"""
import os
from ..config import Config
simulation_dir = os.path.join(Config.OASIS_SIMULATION_DATA_DIR, simulation_id)
# Kiểm tra thư mục có tồn tại hay không
if not os.path.exists(simulation_dir):
return False, {"reason": "Simulation directory does not exist"}
# Danh sách file cần thiết (không bao gồm script, script nằm ở backend/scripts/)
required_files = [
"state.json",
"simulation_config.json",
"reddit_profiles.json",
"twitter_profiles.csv"
]
# Kiểm tra file có tồn tại hay không
existing_files = []
missing_files = []
for f in required_files:
file_path = os.path.join(simulation_dir, f)
if os.path.exists(file_path):
existing_files.append(f)
else:
missing_files.append(f)
if missing_files:
return False, {
"reason": "Missing required files",
"missing_files": missing_files,
"existing_files": existing_files
}
# Kiểm tra trạng thái trong state.json
state_file = os.path.join(simulation_dir, "state.json")
try:
import json
with open(state_file, 'r', encoding='utf-8') as f:
state_data = json.load(f)
status = state_data.get("status", "")
config_generated = state_data.get("config_generated", False)
# Log chi tiết
logger.debug(f"Checking simulation readiness: {simulation_id}, status={status}, config_generated={config_generated}")
# Nếu config_generated=True và file tồn tại thì xem như đã chuẩn bị xong
# Các trạng thái dưới đây đều cho thấy quá trình chuẩn bị đã hoàn tất:
# - ready: chuẩn bị xong, có thể chạy
# - preparing: nếu config_generated=True thì xem như đã hoàn tất
# - running: đang chạy, nghĩa là đã chuẩn bị xong từ trước
# - completed: đã chạy xong, nghĩa là đã chuẩn bị xong từ trước
# - stopped: đã dừng, nghĩa là đã chuẩn bị xong từ trước
# - failed: chạy thất bại (nhưng phần chuẩn bị đã hoàn tất)
prepared_statuses = ["ready", "preparing", "running", "completed", "stopped", "failed"]
if status in prepared_statuses and config_generated:
# Lấy thông tin thống kê file
profiles_file = os.path.join(simulation_dir, "reddit_profiles.json")
config_file = os.path.join(simulation_dir, "simulation_config.json")
profiles_count = 0
if os.path.exists(profiles_file):
with open(profiles_file, 'r', encoding='utf-8') as f:
profiles_data = json.load(f)
profiles_count = len(profiles_data) if isinstance(profiles_data, list) else 0
# Nếu trạng thái là preparing nhưng file đã hoàn tất, tự động cập nhật thành ready
if status == "preparing":
try:
state_data["status"] = "ready"
from datetime import datetime
state_data["updated_at"] = datetime.now().isoformat()
with open(state_file, 'w', encoding='utf-8') as f:
json.dump(state_data, f, ensure_ascii=False, indent=2)
logger.info(f"Auto-updated simulation status: {simulation_id} preparing -> ready")
status = "ready"
except Exception as e:
logger.warning(f"Failed to auto-update status: {e}")
logger.info(f"Simulation {simulation_id} check result: prepared (status={status}, config_generated={config_generated})")
return True, {
"status": status,
"entities_count": state_data.get("entities_count", 0),
"profiles_count": profiles_count,
"entity_types": state_data.get("entity_types", []),
"config_generated": config_generated,
"created_at": state_data.get("created_at"),
"updated_at": state_data.get("updated_at"),
"existing_files": existing_files
}
else:
logger.warning(f"Simulation {simulation_id} check result: not prepared (status={status}, config_generated={config_generated})")
return False, {
"reason": f"Status is not in prepared list or config_generated is false: status={status}, config_generated={config_generated}",
"status": status,
"config_generated": config_generated
}
except Exception as e:
return False, {"reason": f"Failed to read state file: {str(e)}"}
@simulation_bp.route('/prepare', methods=['POST'])
def prepare_simulation():
"""
Chuẩn bị môi trường mô phỏng (tác vụ bất đồng bộ, LLM tạo toàn bộ tham số).
Đây là thao tác tốn thời gian, API sẽ trả về task_id ngay lập tức.
Dùng GET /api/simulation/prepare/status để kiểm tra tiến độ.
Đặc điểm:
- Tự động phát hiện phần chuẩn bị đã hoàn tất để tránh sinh lại.
- Nếu đã chuẩn bị xong thì trả về kết quả sẵn có.
- Hỗ trợ buộc sinh lại (force_regenerate=true).
Các bước:
1. Kiểm tra xem đã có phần chuẩn bị hoàn tất hay chưa.
2. Đọc và lọc thực thể từ đồ thị Zep.
3. Sinh OASIS Agent Profile cho từng thực thể (có cơ chế retry).
4. LLM sinh cấu hình mô phỏng một cách thông minh (có cơ chế retry).
5. Lưu file cấu hình và script thiết lập sẵn.
Yêu cầu (JSON):
{
"simulation_id": "sim_xxxx", // bắt buộc, simulation ID
"entity_types": ["Student", "PublicFigure"], // tùy chọn, chỉ định loại thực thể
"use_llm_for_profiles": true, // tùy chọn, có dùng LLM để sinh persona hay không
"parallel_profile_count": 5, // tùy chọn, số lượng sinh persona song song, mặc định 5
"force_regenerate": false // tùy chọn, buộc sinh lại, mặc định false
}
Trả về:
{
"success": true,
"data": {
"simulation_id": "sim_xxxx",
"task_id": "task_xxxx", // trả về khi là tác vụ mới
"status": "preparing|ready",
"message": "Preparation task has started|Preparation already exists",
"already_prepared": true|false // đã chuẩn bị xong hay chưa
}
}
"""
import threading
import os
from ..models.task import TaskManager, TaskStatus
from ..config import Config
try:
data = request.get_json() or {}
simulation_id = data.get('simulation_id')
if not simulation_id:
return jsonify({
"success": False,
"error": "Please provide simulation_id"
}), 400
manager = SimulationManager()
state = manager.get_simulation(simulation_id)
if not state:
return jsonify({
"success": False,
"error": f"Simulation does not exist: {simulation_id}"
}), 404
# Kiểm tra có buộc sinh lại hay không
force_regenerate = data.get('force_regenerate', False)
logger.info(f"Start handling /prepare request: simulation_id={simulation_id}, force_regenerate={force_regenerate}")
# Kiểm tra đã chuẩn bị xong hay chưa (tránh sinh lại)
if not force_regenerate:
logger.debug(f"Checking whether simulation {simulation_id} is already prepared...")
is_prepared, prepare_info = _check_simulation_prepared(simulation_id)
logger.debug(f"Check result: is_prepared={is_prepared}, prepare_info={prepare_info}")
if is_prepared:
logger.info(f"Simulation {simulation_id} is already prepared, skipping regeneration")
return jsonify({
"success": True,
"data": {
"simulation_id": simulation_id,
"status": "ready",
"message": "Preparation already exists, no regeneration needed",
"already_prepared": True,
"prepare_info": prepare_info
}
})
else:
logger.info(f"Simulation {simulation_id} is not prepared, will start preparation task")
# Lấy thông tin cần thiết từ dự án
project = ProjectManager.get_project(state.project_id)
if not project:
return jsonify({
"success": False,
"error": f"Project does not exist: {state.project_id}"
}), 404
# Lấy yêu cầu mô phỏng
simulation_requirement = project.simulation_requirement or ""
if not simulation_requirement:
return jsonify({
"success": False,
"error": "Project is missing simulation requirement description (simulation_requirement)"
}), 400
# Lấy văn bản tài liệu
document_text = ProjectManager.get_extracted_text(state.project_id) or ""
entity_types_list = data.get('entity_types')
use_llm_for_profiles = data.get('use_llm_for_profiles', True)
parallel_profile_count = data.get('parallel_profile_count', 5)
# ========== Đồng bộ lấy số lượng thực thể (trước khi chạy tác vụ nền) ==========
# Nhờ đó frontend có thể lấy ngay tổng số Agent dự kiến sau khi gọi prepare
try:
logger.info(f"Synchronously fetching entity count: graph_id={state.graph_id}")
reader = ZepEntityReader()
# Đọc nhanh thực thể (không cần thông tin cạnh, chỉ đếm số lượng)
filtered_preview = reader.filter_defined_entities(
graph_id=state.graph_id,
defined_entity_types=entity_types_list,
enrich_with_edges=False # Do not fetch edge info to speed up
)
# Lưu số lượng thực thể vào trạng thái (để frontend lấy ngay)
state.entities_count = filtered_preview.filtered_count
state.entity_types = list(filtered_preview.entity_types)
logger.info(f"Expected entity count: {filtered_preview.filtered_count}, types: {filtered_preview.entity_types}")
except Exception as e:
logger.warning(f"Failed to synchronously fetch entity count (will retry in background task): {e}")
# Lỗi này không ảnh hưởng luồng tiếp theo, tác vụ nền sẽ lấy lại
# Tạo tác vụ bất đồng bộ
task_manager = TaskManager()
task_id = task_manager.create_task(
task_type="simulation_prepare",
metadata={
"simulation_id": simulation_id,
"project_id": state.project_id
}
)
# Cập nhật trạng thái mô phỏng (bao gồm số lượng thực thể đã lấy trước)
state.status = SimulationStatus.PREPARING
manager._save_simulation_state(state)
# Định nghĩa tác vụ nền
def run_prepare():
try:
task_manager.update_task(
task_id,
status=TaskStatus.PROCESSING,
progress=0,
message="Start preparing simulation environment..."
)
# Chuẩn bị mô phỏng (có callback tiến độ)
# Lưu chi tiết tiến độ theo giai đoạn
stage_details = {}
def progress_callback(stage, progress, message, **kwargs):
# Tính tổng tiến độ
stage_weights = {
"reading": (0, 20), # 0-20%
"generating_profiles": (20, 70), # 20-70%
"generating_config": (70, 90), # 70-90%
"copying_scripts": (90, 100) # 90-100%
}
start, end = stage_weights.get(stage, (0, 100))
current_progress = int(start + (end - start) * progress / 100)
# Tạo thông tin tiến độ chi tiết
stage_names = {
"reading": "Reading graph entities",
"generating_profiles": "Generating agent personas",
"generating_config": "Generating simulation config",
"copying_scripts": "Preparing simulation scripts"
}
stage_index = list(stage_weights.keys()).index(stage) + 1 if stage in stage_weights else 1
total_stages = len(stage_weights)
# Cập nhật chi tiết giai đoạn
stage_details[stage] = {
"stage_name": stage_names.get(stage, stage),
"stage_progress": progress,
"current": kwargs.get("current", 0),
"total": kwargs.get("total", 0),
"item_name": kwargs.get("item_name", "")
}
# Tạo thông tin tiến độ chi tiết
detail = stage_details[stage]
progress_detail_data = {
"current_stage": stage,
"current_stage_name": stage_names.get(stage, stage),
"stage_index": stage_index,
"total_stages": total_stages,
"stage_progress": progress,
"current_item": detail["current"],
"total_items": detail["total"],
"item_description": message
}
# Tạo thông báo ngắn gọn
if detail["total"] > 0:
detailed_message = (
f"[{stage_index}/{total_stages}] {stage_names.get(stage, stage)}: "
f"{detail['current']}/{detail['total']} - {message}"
)
else:
detailed_message = f"[{stage_index}/{total_stages}] {stage_names.get(stage, stage)}: {message}"
task_manager.update_task(
task_id,
progress=current_progress,
message=detailed_message,
progress_detail=progress_detail_data
)
result_state = manager.prepare_simulation(
simulation_id=simulation_id,
simulation_requirement=simulation_requirement,
document_text=document_text,
defined_entity_types=entity_types_list,
use_llm_for_profiles=use_llm_for_profiles,
progress_callback=progress_callback,
parallel_profile_count=parallel_profile_count
)
# Task completed
task_manager.complete_task(
task_id,
result=result_state.to_simple_dict()
)
except Exception as e:
logger.error(f"Failed to prepare simulation: {str(e)}")
task_manager.fail_task(task_id, str(e))
# Cập nhật trạng thái mô phỏng thành failed
state = manager.get_simulation(simulation_id)
if state:
state.status = SimulationStatus.FAILED
state.error = str(e)
manager._save_simulation_state(state)
# Khởi chạy luồng nền
thread = threading.Thread(target=run_prepare, daemon=True)
thread.start()
return jsonify({
"success": True,
"data": {
"simulation_id": simulation_id,
"task_id": task_id,
"status": "preparing",
"message": "Preparation task has started. Please check progress via /api/simulation/prepare/status",
"already_prepared": False,
"expected_entities_count": state.entities_count, # Expected total Agent count
"entity_types": state.entity_types # Entity type list
}
})
except ValueError as e:
return jsonify({
"success": False,
"error": str(e)
}), 404
except Exception as e:
logger.error(f"Failed to start preparation task: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
@simulation_bp.route('/prepare/status', methods=['POST'])
def get_prepare_status():
"""
Query preparation task progress.
Supports two query modes:
1. Query ongoing task progress by task_id.
2. Check whether completed preparation already exists by simulation_id.
Yêu cầu (JSON):
{
"task_id": "task_xxxx", // tùy chọn, task_id trả về từ prepare
"simulation_id": "sim_xxxx" // tùy chọn, simulation ID (để kiểm tra phần chuẩn bị đã hoàn tất)
}
Trả về:
{
"success": true,
"data": {
"task_id": "task_xxxx",
"status": "processing|completed|ready",
"progress": 45,
"message": "...",
"already_prepared": true|false, // đã có phần chuẩn bị hoàn tất hay chưa
"prepare_info": {...} // thông tin chi tiết khi đã chuẩn bị hoàn tất
}
}
"""
from ..models.task import TaskManager
try:
data = request.get_json() or {}
task_id = data.get('task_id')
simulation_id = data.get('simulation_id')
# Nếu có simulation_id, kiểm tra trước xem đã chuẩn bị hoàn tất chưa
if simulation_id:
is_prepared, prepare_info = _check_simulation_prepared(simulation_id)
if is_prepared:
return jsonify({
"success": True,
"data": {
"simulation_id": simulation_id,
"status": "ready",
"progress": 100,
"message": "Preparation already completed",
"already_prepared": True,
"prepare_info": prepare_info
}
})
# Nếu không có task_id thì trả về lỗi
if not task_id:
if simulation_id:
# Có simulation_id nhưng chưa chuẩn bị xong
return jsonify({
"success": True,
"data": {
"simulation_id": simulation_id,
"status": "not_started",
"progress": 0,
"message": "Preparation has not started yet. Please call /api/simulation/prepare to start",
"already_prepared": False
}
})
return jsonify({
"success": False,
"error": "Please provide task_id or simulation_id"
}), 400
task_manager = TaskManager()
task = task_manager.get_task(task_id)
if not task:
# Task không tồn tại, nhưng nếu có simulation_id thì kiểm tra đã chuẩn bị xong chưa
if simulation_id:
is_prepared, prepare_info = _check_simulation_prepared(simulation_id)
if is_prepared:
return jsonify({
"success": True,
"data": {
"simulation_id": simulation_id,
"task_id": task_id,
"status": "ready",
"progress": 100,
"message": "Task completed (preparation already exists)",
"already_prepared": True,
"prepare_info": prepare_info
}
})
return jsonify({
"success": False,
"error": f"Task does not exist: {task_id}"
}), 404
task_dict = task.to_dict()
task_dict["already_prepared"] = False
return jsonify({
"success": True,
"data": task_dict
})
except Exception as e:
logger.error(f"Failed to query task status: {str(e)}")
return jsonify({
"success": False,
"error": str(e)
}), 500
@simulation_bp.route('/<simulation_id>', methods=['GET'])
def get_simulation(simulation_id: str):
"""Lấy trạng thái mô phỏng."""
try:
manager = SimulationManager()
state = manager.get_simulation(simulation_id)
if not state:
return jsonify({
"success": False,
"error": f"Simulation does not exist: {simulation_id}"
}), 404
result = state.to_dict()
# Nếu mô phỏng đã sẵn sàng, đính kèm hướng dẫn chạy
if state.status == SimulationStatus.READY:
result["run_instructions"] = manager.get_run_instructions(simulation_id)
return jsonify({
"success": True,
"data": result
})
except Exception as e:
logger.error(f"Failed to get simulation status: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
@simulation_bp.route('/list', methods=['GET'])
def list_simulations():
"""
Liệt kê toàn bộ mô phỏng.
Tham số query:
project_id: Lọc theo project ID (tùy chọn)
"""
try:
project_id = request.args.get('project_id')
manager = SimulationManager()
simulations = manager.list_simulations(project_id=project_id)
return jsonify({
"success": True,
"data": [s.to_dict() for s in simulations],
"count": len(simulations)
})
except Exception as e:
logger.error(f"Failed to list simulations: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
def _get_report_id_for_simulation(simulation_id: str) -> str:
"""
Lấy report_id mới nhất tương ứng với simulation.
Duyệt thư mục reports để tìm report khớp simulation_id.
Nếu có nhiều report thì trả về report mới nhất (sắp xếp theo created_at).
Args:
simulation_id: simulation ID
Returns:
report_id hoặc None
"""
import json
from datetime import datetime
# Đường dẫn thư mục reports: backend/uploads/reports
# __file__ là app/api/simulation.py, cần đi lên hai cấp để tới backend/
reports_dir = os.path.join(os.path.dirname(__file__), '../../uploads/reports')
if not os.path.exists(reports_dir):
return None
matching_reports = []
try:
for report_folder in os.listdir(reports_dir):
report_path = os.path.join(reports_dir, report_folder)
if not os.path.isdir(report_path):
continue
meta_file = os.path.join(report_path, "meta.json")
if not os.path.exists(meta_file):
continue
try:
with open(meta_file, 'r', encoding='utf-8') as f:
meta = json.load(f)
if meta.get("simulation_id") == simulation_id:
matching_reports.append({
"report_id": meta.get("report_id"),
"created_at": meta.get("created_at", ""),
"status": meta.get("status", "")
})
except Exception:
continue
if not matching_reports:
return None
# Sắp xếp giảm dần theo thời gian tạo và trả về bản mới nhất
matching_reports.sort(key=lambda x: x.get("created_at", ""), reverse=True)
return matching_reports[0].get("report_id")
except Exception as e:
logger.warning(f"Failed to find report for simulation {simulation_id}: {e}")
return None
@simulation_bp.route('/history', methods=['GET'])
def get_simulation_history():
"""
Lấy danh sách lịch sử mô phỏng (kèm chi tiết dự án).
Dùng cho hiển thị lịch sử dự án ở trang chủ, trả về danh sách mô phỏng với thông tin đầy đủ như tên dự án, mô tả.
Tham số query:
limit: Giới hạn số lượng trả về (mặc định 20)
Trả về:
{
"success": true,
"data": [
{
"simulation_id": "sim_xxxx",
"project_id": "proj_xxxx",
"project_name": "Phân tích dư luận WU",
"simulation_requirement": "Nếu Đại học Vũ Hán đăng tải...",
"status": "completed",
"entities_count": 68,
"profiles_count": 68,
"entity_types": ["Student", "Professor", ...],
"created_at": "2024-12-10",
"updated_at": "2024-12-10",
"total_rounds": 120,
"current_round": 120,
"report_id": "report_xxxx",
"version": "v1.0.2"
},
...
],
"count": 7
}
"""
try:
limit = request.args.get('limit', 20, type=int)
manager = SimulationManager()
simulations = manager.list_simulations()[:limit]
# Bổ sung dữ liệu mô phỏng, chỉ đọc từ file Simulation
enriched_simulations = []
for sim in simulations:
sim_dict = sim.to_dict()
# Lấy thông tin cấu hình mô phỏng (đọc simulation_requirement từ simulation_config.json)
config = manager.get_simulation_config(sim.simulation_id)
if config:
sim_dict["simulation_requirement"] = config.get("simulation_requirement", "")
time_config = config.get("time_config", {})
sim_dict["total_simulation_hours"] = time_config.get("total_simulation_hours", 0)
# Số vòng khuyến nghị (giá trị dự phòng)
recommended_rounds = int(
time_config.get("total_simulation_hours", 0) * 60 /
max(time_config.get("minutes_per_round", 60), 1)
)
else:
sim_dict["simulation_requirement"] = ""
sim_dict["total_simulation_hours"] = 0
recommended_rounds = 0
# Lấy trạng thái chạy (đọc số vòng thực tế do người dùng thiết lập từ run_state.json)
run_state = SimulationRunner.get_run_state(sim.simulation_id)
if run_state:
sim_dict["current_round"] = run_state.current_round
sim_dict["runner_status"] = run_state.runner_status.value
# Dùng total_rounds do người dùng thiết lập, nếu không có thì dùng số vòng khuyến nghị
sim_dict["total_rounds"] = run_state.total_rounds if run_state.total_rounds > 0 else recommended_rounds
else:
sim_dict["current_round"] = 0
sim_dict["runner_status"] = "idle"
sim_dict["total_rounds"] = recommended_rounds
# Lấy danh sách file của dự án liên kết (tối đa 3 file)
project = ProjectManager.get_project(sim.project_id)
if project and hasattr(project, 'files') and project.files:
sim_dict["files"] = [
{"filename": f.get("filename", "Unknown file")}
for f in project.files[:3]
]
else:
sim_dict["files"] = []
# Lấy report_id liên kết (tìm report mới nhất của simulation này)
sim_dict["report_id"] = _get_report_id_for_simulation(sim.simulation_id)
# Thêm số phiên bản
sim_dict["version"] = "v1.0.2"
# Định dạng ngày
try:
created_date = sim_dict.get("created_at", "")[:10]
sim_dict["created_date"] = created_date
except:
sim_dict["created_date"] = ""
enriched_simulations.append(sim_dict)
return jsonify({
"success": True,
"data": enriched_simulations,
"count": len(enriched_simulations)
})
except Exception as e:
logger.error(f"Failed to get simulation history: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
@simulation_bp.route('/<simulation_id>/profiles', methods=['GET'])
def get_simulation_profiles(simulation_id: str):
"""
Lấy Agent Profile của mô phỏng.
Tham số query:
platform: Loại nền tảng (reddit/twitter, mặc định reddit)
"""
try:
platform = request.args.get('platform', 'reddit')
manager = SimulationManager()
profiles = manager.get_profiles(simulation_id, platform=platform)
return jsonify({
"success": True,
"data": {
"platform": platform,
"count": len(profiles),
"profiles": profiles
}
})
except ValueError as e:
return jsonify({
"success": False,
"error": str(e)
}), 404
except Exception as e:
logger.error(f"Failed to get profile: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
@simulation_bp.route('/<simulation_id>/profiles/realtime', methods=['GET'])
def get_simulation_profiles_realtime(simulation_id: str):
"""
Lấy Agent Profile theo thời gian thực (dùng để theo dõi tiến độ trong quá trình sinh).
Khác biệt so với endpoint /profiles:
- Đọc trực tiếp từ file, không đi qua SimulationManager.
- Phù hợp cho việc xem realtime trong quá trình sinh.
- Trả về thêm metadata (như thời điểm sửa file, có đang sinh hay không).
Tham số query:
platform: Loại nền tảng (reddit/twitter, mặc định reddit)
Trả về:
{
"success": true,
"data": {
"simulation_id": "sim_xxxx",
"platform": "reddit",
"count": 15,
"total_expected": 93, // tổng số dự kiến (nếu có)
"is_generating": true, // có đang sinh hay không
"file_exists": true,
"file_modified_at": "2025-12-04T18:20:00",
"profiles": [...]
}
}
"""
import json
import csv
from datetime import datetime
try:
platform = request.args.get('platform', 'reddit')
# Lấy thư mục mô phỏng
sim_dir = os.path.join(Config.OASIS_SIMULATION_DATA_DIR, simulation_id)
if not os.path.exists(sim_dir):
return jsonify({
"success": False,
"error": f"Simulation does not exist: {simulation_id}"
}), 404
# Xác định đường dẫn file
if platform == "reddit":
profiles_file = os.path.join(sim_dir, "reddit_profiles.json")
else:
profiles_file = os.path.join(sim_dir, "twitter_profiles.csv")
# Kiểm tra file có tồn tại hay không
file_exists = os.path.exists(profiles_file)
profiles = []
file_modified_at = None
if file_exists:
# Lấy thời gian sửa file
file_stat = os.stat(profiles_file)
file_modified_at = datetime.fromtimestamp(file_stat.st_mtime).isoformat()
try:
if platform == "reddit":
with open(profiles_file, 'r', encoding='utf-8') as f:
profiles = json.load(f)
else:
with open(profiles_file, 'r', encoding='utf-8') as f:
reader = csv.DictReader(f)
profiles = list(reader)
except (json.JSONDecodeError, Exception) as e:
logger.warning(f"Failed to read profiles file (it may still be being written): {e}")
profiles = []
# Kiểm tra có đang sinh hay không (dựa vào state.json)
is_generating = False
total_expected = None
state_file = os.path.join(sim_dir, "state.json")
if os.path.exists(state_file):
try:
with open(state_file, 'r', encoding='utf-8') as f:
state_data = json.load(f)
status = state_data.get("status", "")
is_generating = status == "preparing"
total_expected = state_data.get("entities_count")
except Exception:
pass
return jsonify({
"success": True,
"data": {
"simulation_id": simulation_id,
"platform": platform,
"count": len(profiles),
"total_expected": total_expected,
"is_generating": is_generating,
"file_exists": file_exists,
"file_modified_at": file_modified_at,
"profiles": profiles
}
})
except Exception as e:
logger.error(f"Failed to get profile in real time: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
@simulation_bp.route('/<simulation_id>/config/realtime', methods=['GET'])
def get_simulation_config_realtime(simulation_id: str):
"""
Lấy cấu hình mô phỏng theo thời gian thực (dùng để theo dõi tiến độ trong quá trình sinh).
Khác biệt so với endpoint /config:
- Đọc trực tiếp từ file, không đi qua SimulationManager.
- Phù hợp cho việc xem realtime trong quá trình sinh.
- Trả về thêm metadata (như thời điểm sửa file, có đang sinh hay không).
- Ngay cả khi cấu hình chưa sinh xong vẫn có thể trả về thông tin một phần.
Trả về:
{
"success": true,
"data": {
"simulation_id": "sim_xxxx",
"file_exists": true,
"file_modified_at": "2025-12-04T18:20:00",
"is_generating": true, // có đang sinh hay không
"generation_stage": "generating_config", // giai đoạn sinh hiện tại
"config": {...} // nội dung cấu hình (nếu có)
}
}
"""
import json
from datetime import datetime
try:
# Lấy thư mục mô phỏng
sim_dir = os.path.join(Config.OASIS_SIMULATION_DATA_DIR, simulation_id)
if not os.path.exists(sim_dir):
return jsonify({
"success": False,
"error": f"Simulation does not exist: {simulation_id}"
}), 404
# Đường dẫn file cấu hình
config_file = os.path.join(sim_dir, "simulation_config.json")
# Kiểm tra file có tồn tại hay không
file_exists = os.path.exists(config_file)
config = None
file_modified_at = None
if file_exists:
# Lấy thời gian sửa file
file_stat = os.stat(config_file)
file_modified_at = datetime.fromtimestamp(file_stat.st_mtime).isoformat()
try:
with open(config_file, 'r', encoding='utf-8') as f:
config = json.load(f)
except (json.JSONDecodeError, Exception) as e:
logger.warning(f"Failed to read config file (it may still be being written): {e}")
config = None
# Kiểm tra có đang sinh hay không (dựa vào state.json)
is_generating = False
generation_stage = None
config_generated = False
state_file = os.path.join(sim_dir, "state.json")
if os.path.exists(state_file):
try:
with open(state_file, 'r', encoding='utf-8') as f:
state_data = json.load(f)
status = state_data.get("status", "")
is_generating = status == "preparing"
config_generated = state_data.get("config_generated", False)
# Xác định giai đoạn hiện tại
if is_generating:
if state_data.get("profiles_generated", False):
generation_stage = "generating_config"
else:
generation_stage = "generating_profiles"
elif status == "ready":
generation_stage = "completed"
except Exception:
pass
# Tạo dữ liệu trả về
response_data = {
"simulation_id": simulation_id,
"file_exists": file_exists,
"file_modified_at": file_modified_at,
"is_generating": is_generating,
"generation_stage": generation_stage,
"config_generated": config_generated,
"config": config
}
# Nếu cấu hình tồn tại, trích xuất một số thống kê quan trọng
if config:
response_data["summary"] = {
"total_agents": len(config.get("agent_configs", [])),
"simulation_hours": config.get("time_config", {}).get("total_simulation_hours"),
"initial_posts_count": len(config.get("event_config", {}).get("initial_posts", [])),
"hot_topics_count": len(config.get("event_config", {}).get("hot_topics", [])),
"has_twitter_config": "twitter_config" in config,
"has_reddit_config": "reddit_config" in config,
"generated_at": config.get("generated_at"),
"llm_model": config.get("llm_model")
}
return jsonify({
"success": True,
"data": response_data
})
except Exception as e:
logger.error(f"Failed to get config in real time: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
@simulation_bp.route('/<simulation_id>/config', methods=['GET'])
def get_simulation_config(simulation_id: str):
"""
Lấy cấu hình mô phỏng (cấu hình đầy đủ do LLM sinh).
Bao gồm:
- time_config: Cấu hình thời gian (thời lượng mô phỏng, số vòng, khung giờ cao điểm/thấp điểm)
- agent_configs: Cấu hình hoạt động của từng Agent (mức độ hoạt động, tần suất phát biểu, lập trường...)
- event_config: Cấu hình sự kiện (bài đăng khởi tạo, chủ đề nóng)
- platform_configs: Cấu hình nền tảng
- generation_reasoning: Giải thích suy luận cấu hình của LLM
"""
try:
manager = SimulationManager()
config = manager.get_simulation_config(simulation_id)
if not config:
return jsonify({
"success": False,
"error": "Simulation config does not exist. Please call /prepare first"
}), 404
return jsonify({
"success": True,
"data": config
})
except Exception as e:
logger.error(f"Failed to get config: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
@simulation_bp.route('/<simulation_id>/config/download', methods=['GET'])
def download_simulation_config(simulation_id: str):
"""Tải file cấu hình mô phỏng."""
try:
manager = SimulationManager()
sim_dir = manager._get_simulation_dir(simulation_id)
config_path = os.path.join(sim_dir, "simulation_config.json")
if not os.path.exists(config_path):
return jsonify({
"success": False,
"error": "Config file does not exist. Please call /prepare first"
}), 404
return send_file(
config_path,
as_attachment=True,
download_name="simulation_config.json"
)
except Exception as e:
logger.error(f"Failed to download config: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
@simulation_bp.route('/script/<script_name>/download', methods=['GET'])
def download_simulation_script(script_name: str):
"""
Tải file script chạy mô phỏng (script dùng chung, nằm trong backend/scripts/).
script_name có thể là:
- run_twitter_simulation.py
- run_reddit_simulation.py
- run_parallel_simulation.py
- action_logger.py
"""
try:
# Script nằm trong thư mục backend/scripts/
scripts_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../scripts'))
# Kiểm tra tên script
allowed_scripts = [
"run_twitter_simulation.py",
"run_reddit_simulation.py",
"run_parallel_simulation.py",
"action_logger.py"
]
if script_name not in allowed_scripts:
return jsonify({
"success": False,
"error": f"Unknown script: {script_name}. Allowed: {allowed_scripts}"
}), 400
script_path = os.path.join(scripts_dir, script_name)
if not os.path.exists(script_path):
return jsonify({
"success": False,
"error": f"Script file does not exist: {script_name}"
}), 404
return send_file(
script_path,
as_attachment=True,
download_name=script_name
)
except Exception as e:
logger.error(f"Failed to download script: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
# ============== API sinh Profile (dùng độc lập) ==============
@simulation_bp.route('/generate-profiles', methods=['POST'])
def generate_profiles():
"""
Sinh trực tiếp OASIS Agent Profile từ đồ thị (không tạo mô phỏng).
Yêu cầu (JSON):
{
"graph_id": "mirofish_xxxx", // bắt buộc
"entity_types": ["Student"], // tùy chọn
"use_llm": true, // tùy chọn
"platform": "reddit" // tùy chọn
}
"""
try:
data = request.get_json() or {}
graph_id = data.get('graph_id')
if not graph_id:
return jsonify({
"success": False,
"error": "Please provide graph_id"
}), 400
entity_types = data.get('entity_types')
use_llm = data.get('use_llm', True)
platform = data.get('platform', 'reddit')
project_id = data.get('project_id')
reader = ZepEntityReader()
filtered = reader.filter_defined_entities(
graph_id=graph_id,
defined_entity_types=entity_types,
enrich_with_edges=True
)
if filtered.filtered_count == 0:
return jsonify({
"success": False,
"error": "No matching entities found"
}), 400
generator = OasisProfileGenerator()
profiles = generator.generate_profiles_from_entities(
entities=filtered.entities,
use_llm=use_llm,
project_id=project_id,
)
if platform == "reddit":
profiles_data = [p.to_reddit_format() for p in profiles]
elif platform == "twitter":
profiles_data = [p.to_twitter_format() for p in profiles]
else:
profiles_data = [p.to_dict() for p in profiles]
return jsonify({
"success": True,
"data": {
"platform": platform,
"entity_types": list(filtered.entity_types),
"count": len(profiles_data),
"profiles": profiles_data
}
})
except Exception as e:
logger.error(f"Failed to generate profile: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
# ============== API điều khiển chạy mô phỏng ==============
@simulation_bp.route('/start', methods=['POST'])
def start_simulation():
"""
Bắt đầu chạy mô phỏng.
Yêu cầu (JSON):
{
"simulation_id": "sim_xxxx", // bắt buộc, simulation ID
"platform": "parallel", // tùy chọn: twitter / reddit / parallel (mặc định)
"max_rounds": 100, // tùy chọn: số vòng mô phỏng tối đa, dùng để cắt bớt mô phỏng quá dài
"enable_graph_memory_update": false, // tùy chọn: có cập nhật động hoạt động Agent vào bộ nhớ đồ thị Zep hay không
"force": false // tùy chọn: buộc chạy lại (sẽ dừng mô phỏng đang chạy và dọn log)
}
Về tham số force:
- Khi bật, nếu mô phỏng đang chạy hoặc đã hoàn tất thì sẽ dừng trước và dọn log chạy.
- Nội dung dọn bao gồm: run_state.json, actions.jsonl, simulation.log...
- Không dọn file cấu hình (simulation_config.json) và file profile.
- Phù hợp cho tình huống cần chạy lại mô phỏng.
Về enable_graph_memory_update:
- Khi bật, toàn bộ hoạt động của Agent trong mô phỏng (đăng bài, bình luận, thả like...) sẽ được cập nhật realtime vào đồ thị Zep.
- Điều này giúp đồ thị "ghi nhớ" quá trình mô phỏng để phục vụ phân tích hoặc hội thoại AI về sau.
- Dự án liên kết với mô phỏng cần có graph_id hợp lệ.
- Dùng cơ chế cập nhật theo lô để giảm số lần gọi API.
Trả về:
{
"success": true,
"data": {
"simulation_id": "sim_xxxx",
"runner_status": "running",
"process_pid": 12345,
"twitter_running": true,
"reddit_running": true,
"started_at": "2025-12-01T10:00:00",
"graph_memory_update_enabled": true, // có bật cập nhật bộ nhớ đồ thị hay không
"force_restarted": true // có phải là chạy lại bắt buộc hay không
}
}
"""
try:
data = request.get_json() or {}
simulation_id = data.get('simulation_id')
if not simulation_id:
return jsonify({
"success": False,
"error": "Please provide simulation_id"
}), 400
platform = data.get('platform', 'parallel')
max_rounds = data.get('max_rounds') # Tùy chọn: số vòng mô phỏng tối đa
enable_graph_memory_update = data.get('enable_graph_memory_update', False) # Tùy chọn: có bật cập nhật bộ nhớ đồ thị hay không
force = data.get('force', False) # Tùy chọn: buộc chạy lại
# Kiểm tra tham số max_rounds
if max_rounds is not None:
try:
max_rounds = int(max_rounds)
if max_rounds <= 0:
return jsonify({
"success": False,
"error": "max_rounds must be a positive integer"
}), 400
except (ValueError, TypeError):
return jsonify({
"success": False,
"error": "max_rounds must be a valid integer"
}), 400
if platform not in ['twitter', 'reddit', 'parallel']:
return jsonify({
"success": False,
"error": f"Invalid platform type: {platform}. Allowed: twitter/reddit/parallel"
}), 400
# Kiểm tra mô phỏng đã sẵn sàng hay chưa
manager = SimulationManager()
state = manager.get_simulation(simulation_id)
if not state:
return jsonify({
"success": False,
"error": f"Simulation does not exist: {simulation_id}"
}), 404
force_restarted = False
# Xử lý trạng thái thông minh: nếu chuẩn bị đã hoàn tất thì cho phép khởi động lại
if state.status != SimulationStatus.READY:
# Kiểm tra phần chuẩn bị đã hoàn tất chưa
is_prepared, prepare_info = _check_simulation_prepared(simulation_id)
if is_prepared:
# Chuẩn bị đã hoàn tất, kiểm tra có tiến trình đang chạy hay không
if state.status == SimulationStatus.RUNNING:
# Kiểm tra tiến trình mô phỏng có thực sự đang chạy không
run_state = SimulationRunner.get_run_state(simulation_id)
if run_state and run_state.runner_status.value == "running":
# Tiến trình thực sự đang chạy
if force:
# Chế độ force: dừng mô phỏng đang chạy
logger.info(f"Force mode: stopping running simulation {simulation_id}")
try:
SimulationRunner.stop_simulation(simulation_id)
except Exception as e:
logger.warning(f"Warning while stopping simulation: {str(e)}")
else:
return jsonify({
"success": False,
"error": "Simulation is running. Please call /stop first, or use force=true to restart"
}), 400
# Nếu là chế độ force thì dọn log chạy
if force:
logger.info(f"Force mode: cleaning simulation logs {simulation_id}")
cleanup_result = SimulationRunner.cleanup_simulation_logs(simulation_id)
if not cleanup_result.get("success"):
logger.warning(f"Warning while cleaning logs: {cleanup_result.get('errors')}")
force_restarted = True
# Tiến trình không tồn tại hoặc đã kết thúc, đặt lại trạng thái về ready
logger.info(f"Simulation {simulation_id} is prepared, resetting status to ready (previous: {state.status.value})")
state.status = SimulationStatus.READY
manager._save_simulation_state(state)
else:
# Chuẩn bị chưa hoàn tất
return jsonify({
"success": False,
"error": f"Simulation is not ready. Current status: {state.status.value}. Please call /prepare first"
}), 400
# Lấy graph ID (dùng cho cập nhật bộ nhớ đồ thị)
graph_id = None
if enable_graph_memory_update:
# Lấy graph_id từ trạng thái mô phỏng hoặc từ dự án
graph_id = state.graph_id
if not graph_id:
# Thử lấy từ dự án
project = ProjectManager.get_project(state.project_id)
if project:
graph_id = project.graph_id
if not graph_id:
return jsonify({
"success": False,
"error": "Enabling graph memory update requires a valid graph_id. Please ensure the project graph is built"
}), 400
logger.info(f"Graph memory update enabled: simulation_id={simulation_id}, graph_id={graph_id}")
# Khởi chạy mô phỏng
run_state = SimulationRunner.start_simulation(
simulation_id=simulation_id,
platform=platform,
max_rounds=max_rounds,
enable_graph_memory_update=enable_graph_memory_update,
graph_id=graph_id
)
# Cập nhật trạng thái mô phỏng
state.status = SimulationStatus.RUNNING
manager._save_simulation_state(state)
response_data = run_state.to_dict()
if max_rounds:
response_data['max_rounds_applied'] = max_rounds
response_data['graph_memory_update_enabled'] = enable_graph_memory_update
response_data['force_restarted'] = force_restarted
if enable_graph_memory_update:
response_data['graph_id'] = graph_id
return jsonify({
"success": True,
"data": response_data
})
except ValueError as e:
return jsonify({
"success": False,
"error": str(e)
}), 400
except Exception as e:
logger.error(f"Failed to start simulation: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
@simulation_bp.route('/stop', methods=['POST'])
def stop_simulation():
"""
Dừng mô phỏng.
Yêu cầu (JSON):
{
"simulation_id": "sim_xxxx" // bắt buộc, simulation ID
}
Trả về:
{
"success": true,
"data": {
"simulation_id": "sim_xxxx",
"runner_status": "stopped",
"completed_at": "2025-12-01T12:00:00"
}
}
"""
try:
data = request.get_json() or {}
simulation_id = data.get('simulation_id')
if not simulation_id:
return jsonify({
"success": False,
"error": "Please provide simulation_id"
}), 400
run_state = SimulationRunner.stop_simulation(simulation_id)
# Cập nhật trạng thái mô phỏng
manager = SimulationManager()
state = manager.get_simulation(simulation_id)
if state:
state.status = SimulationStatus.PAUSED
manager._save_simulation_state(state)
return jsonify({
"success": True,
"data": run_state.to_dict()
})
except ValueError as e:
return jsonify({
"success": False,
"error": str(e)
}), 400
except Exception as e:
logger.error(f"Failed to stop simulation: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
# ============== API giám sát trạng thái thời gian thực ==============
@simulation_bp.route('/<simulation_id>/run-status', methods=['GET'])
def get_run_status(simulation_id: str):
"""
Lấy trạng thái chạy mô phỏng theo thời gian thực (dùng cho frontend polling).
Trả về:
{
"success": true,
"data": {
"simulation_id": "sim_xxxx",
"runner_status": "running",
"current_round": 5,
"total_rounds": 144,
"progress_percent": 3.5,
"simulated_hours": 2,
"total_simulation_hours": 72,
"twitter_running": true,
"reddit_running": true,
"twitter_actions_count": 150,
"reddit_actions_count": 200,
"total_actions_count": 350,
"started_at": "2025-12-01T10:00:00",
"updated_at": "2025-12-01T10:30:00"
}
}
"""
try:
run_state = SimulationRunner.get_run_state(simulation_id)
if not run_state:
return jsonify({
"success": True,
"data": {
"simulation_id": simulation_id,
"runner_status": "idle",
"current_round": 0,
"total_rounds": 0,
"progress_percent": 0,
"twitter_actions_count": 0,
"reddit_actions_count": 0,
"total_actions_count": 0,
}
})
return jsonify({
"success": True,
"data": run_state.to_dict()
})
except Exception as e:
logger.error(f"Failed to get run status: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
@simulation_bp.route('/<simulation_id>/run-status/detail', methods=['GET'])
def get_run_status_detail(simulation_id: str):
"""
Lấy trạng thái chạy mô phỏng chi tiết (bao gồm toàn bộ hành động).
Dùng cho frontend hiển thị diễn biến thời gian thực.
Tham số query:
platform: Lọc theo nền tảng (twitter/reddit, tùy chọn)
Trả về:
{
"success": true,
"data": {
"simulation_id": "sim_xxxx",
"runner_status": "running",
"current_round": 5,
...
"all_actions": [
{
"round_num": 5,
"timestamp": "2025-12-01T10:30:00",
"platform": "twitter",
"agent_id": 3,
"agent_name": "Agent Name",
"action_type": "CREATE_POST",
"action_args": {"content": "..."},
"result": null,
"success": true
},
...
],
"twitter_actions": [...], # toàn bộ hành động trên Twitter
"reddit_actions": [...] # toàn bộ hành động trên Reddit
}
}
"""
try:
run_state = SimulationRunner.get_run_state(simulation_id)
platform_filter = request.args.get('platform')
if not run_state:
return jsonify({
"success": True,
"data": {
"simulation_id": simulation_id,
"runner_status": "idle",
"all_actions": [],
"twitter_actions": [],
"reddit_actions": []
}
})
# Lấy danh sách hành động đầy đủ
all_actions = SimulationRunner.get_all_actions(
simulation_id=simulation_id,
platform=platform_filter
)
# Lấy hành động theo từng nền tảng
twitter_actions = SimulationRunner.get_all_actions(
simulation_id=simulation_id,
platform="twitter"
) if not platform_filter or platform_filter == "twitter" else []
reddit_actions = SimulationRunner.get_all_actions(
simulation_id=simulation_id,
platform="reddit"
) if not platform_filter or platform_filter == "reddit" else []
# Lấy hành động của vòng hiện tại (recent_actions chỉ hiển thị vòng mới nhất)
current_round = run_state.current_round
recent_actions = SimulationRunner.get_all_actions(
simulation_id=simulation_id,
platform=platform_filter,
round_num=current_round
) if current_round > 0 else []
# Lấy thông tin trạng thái cơ bản
result = run_state.to_dict()
result["all_actions"] = [a.to_dict() for a in all_actions]
result["twitter_actions"] = [a.to_dict() for a in twitter_actions]
result["reddit_actions"] = [a.to_dict() for a in reddit_actions]
result["rounds_count"] = len(run_state.rounds)
# recent_actions chỉ hiển thị nội dung của vòng mới nhất trên hai nền tảng
result["recent_actions"] = [a.to_dict() for a in recent_actions]
return jsonify({
"success": True,
"data": result
})
except Exception as e:
logger.error(f"Failed to get detailed run status: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
@simulation_bp.route('/<simulation_id>/actions', methods=['GET'])
def get_simulation_actions(simulation_id: str):
"""
Lấy lịch sử hành động Agent trong mô phỏng.
Tham số query:
limit: Số lượng trả về (mặc định 100)
offset: Độ lệch (mặc định 0)
platform: Lọc nền tảng (twitter/reddit)
agent_id: Lọc theo Agent ID
round_num: Lọc theo vòng
Trả về:
{
"success": true,
"data": {
"count": 100,
"actions": [...]
}
}
"""
try:
limit = request.args.get('limit', 100, type=int)
offset = request.args.get('offset', 0, type=int)
platform = request.args.get('platform')
agent_id = request.args.get('agent_id', type=int)
round_num = request.args.get('round_num', type=int)
actions = SimulationRunner.get_actions(
simulation_id=simulation_id,
limit=limit,
offset=offset,
platform=platform,
agent_id=agent_id,
round_num=round_num
)
return jsonify({
"success": True,
"data": {
"count": len(actions),
"actions": [a.to_dict() for a in actions]
}
})
except Exception as e:
logger.error(f"Failed to get action history: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
@simulation_bp.route('/<simulation_id>/timeline', methods=['GET'])
def get_simulation_timeline(simulation_id: str):
"""
Lấy timeline mô phỏng (tổng hợp theo vòng).
Dùng cho frontend hiển thị thanh tiến độ và timeline.
Tham số query:
start_round: Vòng bắt đầu (mặc định 0)
end_round: Vòng kết thúc (mặc định toàn bộ)
Trả về thông tin tổng hợp theo từng vòng.
"""
try:
start_round = request.args.get('start_round', 0, type=int)
end_round = request.args.get('end_round', type=int)
timeline = SimulationRunner.get_timeline(
simulation_id=simulation_id,
start_round=start_round,
end_round=end_round
)
return jsonify({
"success": True,
"data": {
"rounds_count": len(timeline),
"timeline": timeline
}
})
except Exception as e:
logger.error(f"Failed to get timeline: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
@simulation_bp.route('/<simulation_id>/agent-stats', methods=['GET'])
def get_agent_stats(simulation_id: str):
"""
Lấy thống kê của từng Agent.
Dùng cho frontend hiển thị bảng xếp hạng mức độ hoạt động và phân bố hành động của Agent.
"""
try:
stats = SimulationRunner.get_agent_stats(simulation_id)
return jsonify({
"success": True,
"data": {
"agents_count": len(stats),
"stats": stats
}
})
except Exception as e:
logger.error(f"Failed to get agent statistics: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
# ============== API truy vấn cơ sở dữ liệu ==============
@simulation_bp.route('/<simulation_id>/posts', methods=['GET'])
def get_simulation_posts(simulation_id: str):
"""
Lấy bài đăng trong mô phỏng.
Tham số query:
platform: Loại nền tảng (twitter/reddit)
limit: Số lượng trả về (mặc định 50)
offset: Độ lệch
Trả về danh sách bài đăng (đọc từ cơ sở dữ liệu SQLite).
"""
try:
platform = request.args.get('platform', 'reddit')
limit = request.args.get('limit', 50, type=int)
offset = request.args.get('offset', 0, type=int)
sim_dir = os.path.join(
os.path.dirname(__file__),
f'../../uploads/simulations/{simulation_id}'
)
db_file = f"{platform}_simulation.db"
db_path = os.path.join(sim_dir, db_file)
if not os.path.exists(db_path):
return jsonify({
"success": True,
"data": {
"platform": platform,
"count": 0,
"posts": [],
"message": "Database does not exist. The simulation may not have run yet"
}
})
import sqlite3
conn = sqlite3.connect(db_path)
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
try:
cursor.execute("""
SELECT * FROM post
ORDER BY created_at DESC
LIMIT ? OFFSET ?
""", (limit, offset))
posts = [dict(row) for row in cursor.fetchall()]
cursor.execute("SELECT COUNT(*) FROM post")
total = cursor.fetchone()[0]
except sqlite3.OperationalError:
posts = []
total = 0
conn.close()
return jsonify({
"success": True,
"data": {
"platform": platform,
"total": total,
"count": len(posts),
"posts": posts
}
})
except Exception as e:
logger.error(f"Failed to get posts: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
@simulation_bp.route('/<simulation_id>/comments', methods=['GET'])
def get_simulation_comments(simulation_id: str):
"""
Lấy bình luận trong mô phỏng (chỉ Reddit).
Tham số query:
post_id: Lọc theo post ID (tùy chọn)
limit: Số lượng trả về
offset: Độ lệch
"""
try:
post_id = request.args.get('post_id')
limit = request.args.get('limit', 50, type=int)
offset = request.args.get('offset', 0, type=int)
sim_dir = os.path.join(
os.path.dirname(__file__),
f'../../uploads/simulations/{simulation_id}'
)
db_path = os.path.join(sim_dir, "reddit_simulation.db")
if not os.path.exists(db_path):
return jsonify({
"success": True,
"data": {
"count": 0,
"comments": []
}
})
import sqlite3
conn = sqlite3.connect(db_path)
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
try:
if post_id:
cursor.execute("""
SELECT * FROM comment
WHERE post_id = ?
ORDER BY created_at DESC
LIMIT ? OFFSET ?
""", (post_id, limit, offset))
else:
cursor.execute("""
SELECT * FROM comment
ORDER BY created_at DESC
LIMIT ? OFFSET ?
""", (limit, offset))
comments = [dict(row) for row in cursor.fetchall()]
except sqlite3.OperationalError:
comments = []
conn.close()
return jsonify({
"success": True,
"data": {
"count": len(comments),
"comments": comments
}
})
except Exception as e:
logger.error(f"Failed to get comments: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
# ============== Interview API ==============
@simulation_bp.route('/interview', methods=['POST'])
def interview_agent():
"""
Phỏng vấn một Agent.
Lưu ý: chức năng này yêu cầu môi trường mô phỏng đang chạy
(sau khi hoàn tất vòng mô phỏng, hệ thống vào chế độ chờ lệnh).
Yêu cầu (JSON):
{
"simulation_id": "sim_xxxx", // Bắt buộc, simulation ID
"agent_id": 0, // Bắt buộc, Agent ID
"prompt": "Bạn nghĩ gì về vấn đề này?", // Bắt buộc, câu hỏi phỏng vấn
"platform": "twitter", // Tùy chọn, chỉ định nền tảng (twitter/reddit)
// Nếu không chỉ định: mô phỏng 2 nền tảng sẽ phỏng vấn cả hai
"timeout": 60 // Tùy chọn, timeout (giây), mặc định 60
}
Trả về (không chỉ định `platform`, chế độ hai nền tảng):
{
"success": true,
"data": {
"agent_id": 0,
"prompt": "Bạn nghĩ gì về vấn đề này?",
"result": {
"agent_id": 0,
"prompt": "...",
"platforms": {
"twitter": {"agent_id": 0, "response": "...", "platform": "twitter"},
"reddit": {"agent_id": 0, "response": "...", "platform": "reddit"}
}
},
"timestamp": "2025-12-08T10:00:01"
}
}
Trả về (có chỉ định `platform`):
{
"success": true,
"data": {
"agent_id": 0,
"prompt": "Bạn nghĩ gì về vấn đề này?",
"result": {
"agent_id": 0,
"response": "Tôi nghĩ...",
"platform": "twitter",
"timestamp": "2025-12-08T10:00:00"
},
"timestamp": "2025-12-08T10:00:01"
}
}
"""
try:
data = request.get_json() or {}
simulation_id = data.get('simulation_id')
agent_id = data.get('agent_id')
prompt = data.get('prompt')
platform = data.get('platform') # Tùy chọn: twitter/reddit/None
timeout = data.get('timeout', 60)
if not simulation_id:
return jsonify({
"success": False,
"error": "Please provide simulation_id"
}), 400
if agent_id is None:
return jsonify({
"success": False,
"error": "Please provide agent_id"
}), 400
if not prompt:
return jsonify({
"success": False,
"error": "Please provide prompt (interview question)"
}), 400
# Validate platform parameter
if platform and platform not in ("twitter", "reddit"):
return jsonify({
"success": False,
"error": "platform must be 'twitter' or 'reddit'"
}), 400
# Check environment status
if not SimulationRunner.check_env_alive(simulation_id):
return jsonify({
"success": False,
"error": "Simulation environment is not running or has been closed. Ensure simulation has completed and entered command-wait mode."
}), 400
# Optimize prompt by adding a prefix to avoid Agent tool calls
optimized_prompt = optimize_interview_prompt(prompt)
result = SimulationRunner.interview_agent(
simulation_id=simulation_id,
agent_id=agent_id,
prompt=optimized_prompt,
platform=platform,
timeout=timeout
)
return jsonify({
"success": result.get("success", False),
"data": result
})
except ValueError as e:
return jsonify({
"success": False,
"error": str(e)
}), 400
except TimeoutError as e:
return jsonify({
"success": False,
"error": f"Timeout waiting for interview response: {str(e)}"
}), 504
except Exception as e:
logger.error(f"Interview failed: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
@simulation_bp.route('/interview/batch', methods=['POST'])
def interview_agents_batch():
"""
Phỏng vấn hàng loạt nhiều Agent.
Lưu ý: chức năng này yêu cầu môi trường mô phỏng đang chạy.
Yêu cầu (JSON):
{
"simulation_id": "sim_xxxx", // Bắt buộc, simulation ID
"interviews": [ // Bắt buộc, danh sách phỏng vấn
{
"agent_id": 0,
"prompt": "Bạn nghĩ gì về A?",
"platform": "twitter" // Tùy chọn, chỉ định nền tảng cho Agent này
},
{
"agent_id": 1,
"prompt": "Bạn nghĩ gì về B?" // Nếu không chỉ định `platform`, dùng giá trị mặc định
}
],
"platform": "reddit", // Tùy chọn, nền tảng mặc định (bị ghi đè bởi `platform` của từng mục)
// Nếu không chỉ định: mô phỏng 2 nền tảng sẽ phỏng vấn cả hai nền tảng cho mỗi Agent
"timeout": 120 // Tùy chọn, timeout (giây), mặc định 120
}
Trả về:
{
"success": true,
"data": {
"interviews_count": 2,
"result": {
"interviews_count": 4,
"results": {
"twitter_0": {"agent_id": 0, "response": "...", "platform": "twitter"},
"reddit_0": {"agent_id": 0, "response": "...", "platform": "reddit"},
"twitter_1": {"agent_id": 1, "response": "...", "platform": "twitter"},
"reddit_1": {"agent_id": 1, "response": "...", "platform": "reddit"}
}
},
"timestamp": "2025-12-08T10:00:01"
}
}
"""
try:
data = request.get_json() or {}
simulation_id = data.get('simulation_id')
interviews = data.get('interviews')
platform = data.get('platform') # Tùy chọn: twitter/reddit/None
timeout = data.get('timeout', 120)
if not simulation_id:
return jsonify({
"success": False,
"error": "Please provide simulation_id"
}), 400
if not interviews or not isinstance(interviews, list):
return jsonify({
"success": False,
"error": "Please provide interviews (interview list)"
}), 400
# Validate platform parameter
if platform and platform not in ("twitter", "reddit"):
return jsonify({
"success": False,
"error": "platform must be 'twitter' or 'reddit'"
}), 400
# Validate each interview item
for i, interview in enumerate(interviews):
if 'agent_id' not in interview:
return jsonify({
"success": False,
"error": f"Interview item {i+1} is missing agent_id"
}), 400
if 'prompt' not in interview:
return jsonify({
"success": False,
"error": f"Interview item {i+1} is missing prompt"
}), 400
# Validate platform in each item (if provided)
item_platform = interview.get('platform')
if item_platform and item_platform not in ("twitter", "reddit"):
return jsonify({
"success": False,
"error": f"platform in interview item {i+1} must be 'twitter' or 'reddit'"
}), 400
# Check environment status
if not SimulationRunner.check_env_alive(simulation_id):
return jsonify({
"success": False,
"error": "Simulation environment is not running or has been closed. Ensure simulation has completed and entered command-wait mode."
}), 400
# Optimize prompts in each interview item to avoid Agent tool calls
optimized_interviews = []
for interview in interviews:
optimized_interview = interview.copy()
optimized_interview['prompt'] = optimize_interview_prompt(interview.get('prompt', ''))
optimized_interviews.append(optimized_interview)
result = SimulationRunner.interview_agents_batch(
simulation_id=simulation_id,
interviews=optimized_interviews,
platform=platform,
timeout=timeout
)
return jsonify({
"success": result.get("success", False),
"data": result
})
except ValueError as e:
return jsonify({
"success": False,
"error": str(e)
}), 400
except TimeoutError as e:
return jsonify({
"success": False,
"error": f"Timeout waiting for batch interview response: {str(e)}"
}), 504
except Exception as e:
logger.error(f"Batch interview failed: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
@simulation_bp.route('/interview/all', methods=['POST'])
def interview_all_agents():
"""
Phỏng vấn toàn cục - dùng cùng một câu hỏi để phỏng vấn tất cả Agent.
Lưu ý: chức năng này yêu cầu môi trường mô phỏng đang chạy.
Yêu cầu (JSON):
{
"simulation_id": "sim_xxxx", // Bắt buộc, simulation ID
"prompt": "Bạn có quan điểm tổng thể gì về vấn đề này?", // Bắt buộc, câu hỏi phỏng vấn (mọi Agent dùng cùng câu hỏi)
"platform": "reddit", // Tùy chọn, chỉ định nền tảng (twitter/reddit)
// Nếu không chỉ định: mô phỏng 2 nền tảng sẽ phỏng vấn cả hai nền tảng cho mỗi Agent
"timeout": 180 // Tùy chọn, timeout (giây), mặc định 180
}
Trả về:
{
"success": true,
"data": {
"interviews_count": 50,
"result": {
"interviews_count": 100,
"results": {
"twitter_0": {"agent_id": 0, "response": "...", "platform": "twitter"},
"reddit_0": {"agent_id": 0, "response": "...", "platform": "reddit"},
...
}
},
"timestamp": "2025-12-08T10:00:01"
}
}
"""
try:
data = request.get_json() or {}
simulation_id = data.get('simulation_id')
prompt = data.get('prompt')
platform = data.get('platform') # Tùy chọn: twitter/reddit/None
timeout = data.get('timeout', 180)
if not simulation_id:
return jsonify({
"success": False,
"error": "Please provide simulation_id"
}), 400
if not prompt:
return jsonify({
"success": False,
"error": "Please provide prompt (interview question)"
}), 400
# Validate platform parameter
if platform and platform not in ("twitter", "reddit"):
return jsonify({
"success": False,
"error": "platform must be 'twitter' or 'reddit'"
}), 400
# Check environment status
if not SimulationRunner.check_env_alive(simulation_id):
return jsonify({
"success": False,
"error": "Simulation environment is not running or has been closed. Ensure simulation has completed and entered command-wait mode."
}), 400
# Optimize prompt by adding a prefix to avoid Agent tool calls
optimized_prompt = optimize_interview_prompt(prompt)
result = SimulationRunner.interview_all_agents(
simulation_id=simulation_id,
prompt=optimized_prompt,
platform=platform,
timeout=timeout
)
return jsonify({
"success": result.get("success", False),
"data": result
})
except ValueError as e:
return jsonify({
"success": False,
"error": str(e)
}), 400
except TimeoutError as e:
return jsonify({
"success": False,
"error": f"Timeout waiting for global interview response: {str(e)}"
}), 504
except Exception as e:
logger.error(f"Global interview failed: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
@simulation_bp.route('/interview/history', methods=['POST'])
def get_interview_history():
"""
Lấy lịch sử Interview.
Đọc toàn bộ bản ghi Interview từ cơ sở dữ liệu mô phỏng.
Yêu cầu (JSON):
{
"simulation_id": "sim_xxxx", // Bắt buộc, simulation ID
"platform": "reddit", // Tùy chọn, loại nền tảng (reddit/twitter)
// Nếu không chỉ định, trả về toàn bộ lịch sử của cả hai nền tảng
"agent_id": 0, // Tùy chọn, chỉ lấy lịch sử phỏng vấn của Agent này
"limit": 100 // Tùy chọn, số lượng trả về, mặc định 100
}
Trả về:
{
"success": true,
"data": {
"count": 10,
"history": [
{
"agent_id": 0,
"response": "Tôi nghĩ...",
"prompt": "Bạn nghĩ gì về vấn đề này?",
"timestamp": "2025-12-08T10:00:00",
"platform": "reddit"
},
...
]
}
}
"""
try:
data = request.get_json() or {}
simulation_id = data.get('simulation_id')
platform = data.get('platform') # If omitted, return history from both platforms
agent_id = data.get('agent_id')
limit = data.get('limit', 100)
if not simulation_id:
return jsonify({
"success": False,
"error": "Please provide simulation_id"
}), 400
history = SimulationRunner.get_interview_history(
simulation_id=simulation_id,
platform=platform,
agent_id=agent_id,
limit=limit
)
return jsonify({
"success": True,
"data": {
"count": len(history),
"history": history
}
})
except Exception as e:
logger.error(f"Failed to get interview history: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
@simulation_bp.route('/env-status', methods=['POST'])
def get_env_status():
"""
Lấy trạng thái môi trường mô phỏng.
Kiểm tra môi trường mô phỏng còn hoạt động không (có thể nhận lệnh Interview).
Yêu cầu (JSON):
{
"simulation_id": "sim_xxxx" // Bắt buộc, simulation ID
}
Trả về:
{
"success": true,
"data": {
"simulation_id": "sim_xxxx",
"env_alive": true,
"twitter_available": true,
"reddit_available": true,
"message": "Environment is running and can receive Interview commands"
}
}
"""
try:
data = request.get_json() or {}
simulation_id = data.get('simulation_id')
if not simulation_id:
return jsonify({
"success": False,
"error": "Please provide simulation_id"
}), 400
env_alive = SimulationRunner.check_env_alive(simulation_id)
# Get more detailed status information
env_status = SimulationRunner.get_env_status_detail(simulation_id)
if env_alive:
message = "Environment is running and can receive Interview commands"
else:
message = "Environment is not running or has been closed"
return jsonify({
"success": True,
"data": {
"simulation_id": simulation_id,
"env_alive": env_alive,
"twitter_available": env_status.get("twitter_available", False),
"reddit_available": env_status.get("reddit_available", False),
"message": message
}
})
except Exception as e:
logger.error(f"Failed to get environment status: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
@simulation_bp.route('/close-env', methods=['POST'])
def close_simulation_env():
"""
Đóng môi trường mô phỏng.
Gửi lệnh đóng môi trường tới mô phỏng để thoát chế độ chờ lệnh một cách graceful.
Lưu ý: API này khác với `/stop`; `/stop` sẽ buộc dừng tiến trình,
còn API này sẽ cho mô phỏng đóng môi trường và thoát một cách graceful.
Yêu cầu (JSON):
{
"simulation_id": "sim_xxxx", // Bắt buộc, simulation ID
"timeout": 30 // Tùy chọn, timeout (giây), mặc định 30
}
Trả về:
{
"success": true,
"data": {
"message": "Environment close command has been sent",
"result": {...},
"timestamp": "2025-12-08T10:00:01"
}
}
"""
try:
data = request.get_json() or {}
simulation_id = data.get('simulation_id')
timeout = data.get('timeout', 30)
if not simulation_id:
return jsonify({
"success": False,
"error": "Please provide simulation_id"
}), 400
result = SimulationRunner.close_simulation_env(
simulation_id=simulation_id,
timeout=timeout
)
# Update simulation status
manager = SimulationManager()
state = manager.get_simulation(simulation_id)
if state:
state.status = SimulationStatus.COMPLETED
manager._save_simulation_state(state)
return jsonify({
"success": result.get("success", False),
"data": result
})
except ValueError as e:
return jsonify({
"success": False,
"error": str(e)
}), 400
except Exception as e:
logger.error(f"Failed to close environment: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500