MicroFish/backend/app/api/simulation.py

2626 lines
94 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
API routes liên quan đến simulation
Step2: Đọc và lọc Zep entities, chuẩn bị và chạy OASIS simulation (tự động toàn bộ)
"""
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')
# Prefix tối ưu Interview prompt
# Thêm prefix này để tránh Agent gọi tool, chỉ trả lời bằng text
INTERVIEW_PROMPT_PREFIX = "Based on your persona, all past memories, and actions, reply directly in plain text without calling any tools:"
def optimize_interview_prompt(prompt: str) -> str:
"""
Tối ưu câu hỏi Interview, thêm prefix để tránh Agent gọi tool
Args:
prompt: câu hỏi gốc
Returns:
câu hỏi sau khi tối ưu
"""
if not prompt:
return prompt
# Tránh thêm prefix trùng lặp
if prompt.startswith(INTERVIEW_PROMPT_PREFIX):
return prompt
return f"{INTERVIEW_PROMPT_PREFIX}{prompt}"
# ============== API đọc entity ==============
@simulation_bp.route('/entities/<graph_id>', methods=['GET'])
def get_graph_entities(graph_id: str):
"""
Lấy toàn bộ entity trong graph (đã lọc)
Chỉ trả về node thuộc entity types đã định nghĩa
(labels không chỉ giới hạn ở Entity)
Query parameters:
entity_types: danh sách entity types phân tách bằng dấu phẩy (optional)
enrich: có lấy edge information hay không (default 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 entity"""
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 not found: {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 tất cả entity 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
def _check_simulation_prepared(simulation_id: str) -> tuple:
"""
Kiểm tra simulation đã được chuẩn bị xong hay chưa
Điều kiện kiểm tra:
1. state.json tồn tại và status = "ready"
2. Các file bắt buộc tồn tại: reddit_profiles.json, twitter_profiles.csv, simulation_config.json
Lưu ý:
Script chạy (run_*.py) được giữ trong thư mục backend/scripts/,
không còn copy vào thư mục simulation nữa
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 simulation có tồn tại không
if not os.path.exists(simulation_dir):
return False, {"reason": "Simulation directory does not exist"}
# Danh sách file bắt buộc (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 các 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)
# Debug log chi tiết
logger.debug(f"Checking simulation preparation status: {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 sau đều có nghĩa là preparation đã hoàn thành:
# - ready: chuẩn bị xong, có thể chạy
# - preparing: nếu config_generated=True thì coi như đã hoàn thành
# - running: đang chạy, nghĩa là preparation đã xong
# - completed: đã chạy xong
# - stopped: đã dừng
# - failed: chạy thất bại (nhưng preparation vẫn đã hoàn thành)
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 đã sẵn sàng thì tự động update sang 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 not in prepared list or config_generated=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 simulation (async task, tất cả tham số được LLM sinh tự động)
Đây là một thao tác tốn thời gian. API sẽ trả về ngay task_id,
dùng GET /api/simulation/prepare/status để kiểm tra tiến độ.
Tính năng:
- Tự động phát hiện preparation đã hoàn thành để tránh generate lại
- Nếu đã chuẩn bị xong thì trả về kết quả hiện có
- Hỗ trợ force regenerate (force_regenerate=true)
Các bước:
1. Kiểm tra xem preparation đã hoàn thành chưa
2. Đọc và lọc entities từ Zep graph
3. Generate OASIS Agent Profile cho từng entity (có retry)
4. LLM generate simulation config (có retry)
5. Lưu config file và preset scripts
Request (JSON):
{
"simulation_id": "sim_xxxx", // bắt buộc
"entity_types": ["Student", "PublicFigure"], // optional
"use_llm_for_profiles": true, // optional
"parallel_profile_count": 5, // optional (default 5)
"force_regenerate": false // optional (default false)
}
Response:
{
"success": true,
"data": {
"simulation_id": "sim_xxxx",
"task_id": "task_xxxx",
"status": "preparing|ready",
"message": "Preparation task started | Already prepared",
"already_prepared": true|false
}
}
"""
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 not found: {simulation_id}"
}), 404
# Kiểm tra có bắt buộc regenerate hay không
force_regenerate = data.get('force_regenerate', False)
logger.info(f"Start processing /prepare request: simulation_id={simulation_id}, force_regenerate={force_regenerate}")
# Kiểm tra simulation đã prepare xong chưa (tránh generate lặp 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} already prepared, skipping regeneration")
return jsonify({
"success": True,
"data": {
"simulation_id": simulation_id,
"status": "ready",
"message": "Preparation already completed, no regeneration required",
"already_prepared": True,
"prepare_info": prepare_info
}
})
else:
logger.info(f"Simulation {simulation_id} not prepared, starting preparation task")
# Lấy thông tin cần thiết từ project
project = ProjectManager.get_project(state.project_id)
if not project:
return jsonify({
"success": False,
"error": f"Project not found: {state.project_id}"
}), 404
# Lấy simulation requirement
simulation_requirement = project.simulation_requirement or ""
if not simulation_requirement:
return jsonify({
"success": False,
"error": "Project missing simulation requirement description (simulation_requirement)"
}), 400
# Lấy text đã extract từ document
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 entity (trước khi background task chạy) ==========
# Như vậy sau khi gọi prepare, frontend có thể lấy ngay tổng số Agent dự kiến
try:
logger.info(f"Synchronously fetching entity count: graph_id={state.graph_id}")
reader = ZepEntityReader()
# Đọc entity nhanh (không cần edge info, 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 # Không lấy edge info để tăng tốc
)
# Lưu entity count vào state (để frontend có thể 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 flow tiếp theo, background task sẽ lấy lại
# Tạo async task
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 simulation status (bao gồm entity count đã lấy trước)
state.status = SimulationStatus.PREPARING
manager._save_simulation_state(state)
# Định nghĩa background task
def run_prepare():
try:
task_manager.update_task(
task_id,
status=TaskStatus.PROCESSING,
progress=0,
message="Start preparing simulation environment..."
)
# Prepare simulation (có progress callback)
# Lưu chi tiết tiến trình từng stage
stage_details = {}
def progress_callback(stage, progress, message, **kwargs):
# Tính tổng progress
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ên stage
stage_names = {
"reading": "Reading graph entities",
"generating_profiles": "Generating agent profiles",
"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 stage
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", "")
}
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
}
# Build progress message
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 hoàn thành
task_manager.complete_task(
task_id,
result=result_state.to_simple_dict()
)
except Exception as e:
logger.error(f"Simulation preparation failed: {str(e)}")
task_manager.fail_task(task_id, str(e))
# Cập nhật simulation status = 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 background thread
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 started. Check progress via /api/simulation/prepare/status",
"already_prepared": False,
"expected_entities_count": state.entities_count, # Tổng số Agent dự kiến
"entity_types": state.entity_types # Danh sách entity type
}
})
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():
"""
Kiểm tra tiến độ tác vụ chuẩn bị
Hỗ trợ hai cách truy vấn:
1. Dùng task_id để truy vấn tiến độ tác vụ đang chạy
2. Dùng simulation_id để kiểm tra đã có bản chuẩn bị hoàn tất hay chưa
Yêu cầu (JSON):
{
"task_id": "task_xxxx", // tuỳ chọn, task_id trả về từ prepare
"simulation_id": "sim_xxxx" // tuỳ chọn, simulation ID (dùng để kiểm tra 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ó bản chuẩn bị hoàn tất hay chưa
"prepare_info": {...} // thông tin chi tiết khi đã chuẩn bị xong
}
}
"""
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ị 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,
"status": "ready",
"progress": 100,
"message": "Preparation already exists",
"already_prepared": True,
"prepare_info": prepare_info
}
})
# Nếu không có task_id thì trả lỗi
if not task_id:
if simulation_id:
# Có simulation_id nhưng chưa chuẩn bị hoàn tất
return jsonify({
"success": True,
"data": {
"simulation_id": simulation_id,
"status": "not_started",
"progress": 0,
"message": "Preparation has not started. 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ị hoàn tất
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 (existing preparation found)",
"already_prepared": True,
"prepare_info": prepare_info
}
})
return jsonify({
"success": False,
"error": f"Task not found: {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 simulation"""
try:
manager = SimulationManager()
state = manager.get_simulation(simulation_id)
if not state:
return jsonify({
"success": False,
"error": f"Simulation not found: {simulation_id}"
}), 404
result = state.to_dict()
# Nếu simulation đã sẵn sàng thì đí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"Lấy trạng thái simulationfailed: {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ê tất cả simulation
Tham số Query:
project_id: lọc theo project ID (tuỳ 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 thì trả về bản mới nhất (sắ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 2 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 created_at giảm dần 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 simulation lịch sử (kèm chi tiết project)
Dùng cho trang chủ để hiển thị lịch sử project, trả về dữ liệu mở rộng như tên và mô tả project
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": "Wuhan University public opinion analysis",
"simulation_requirement": "If Wuhan University publishes...",
"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]
# Mở rộng dữ liệu simulation, 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 simulation (đọ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 đề xuất (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ế người dùng đặt trong 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 đặt, nếu không có thì dùng số vòng đề xuất
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 project 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 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 simulation
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 profiles: {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 của simulation theo thời gian thực (dùng để theo dõi tiến độ trong lúc generate)
Khác biệt so với endpoint /profiles:
- Đọc file trực tiếp, không qua SimulationManager
- Phù hợp để xem realtime trong quá trình generate
- Trả thêm metadata (như thời điểm sửa file, có đang generate 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 dự kiến (nếu có)
"is_generating": true, // có đang generate 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 simulation
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 not found: {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 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 (file may still be being written): {e}")
profiles = []
# Kiểm tra có đang generate 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"realtimeFailed to get profiles: {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 simulation theo thời gian thực (để xem tiến độ khi đang generate)
Khác biệt so với endpoint /config:
- Đọc file trực tiếp, không qua SimulationManager
- Phù hợp để xem realtime trong quá trình generate
- Trả thêm metadata (như thời điểm sửa file, có đang generate hay không)
- Ngay cả khi config chưa generate xong vẫn có thể trả về một phần thông tin
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 generate không
"generation_stage": "generating_config", // giai đoạn generate hiện tại
"config": {...} // nội dung config (nếu có)
}
}
"""
import json
from datetime import datetime
try:
# Lấy thư mục simulation
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 not found: {simulation_id}"
}), 404
# Đường dẫn file config
config_file = os.path.join(sim_dir, "simulation_config.json")
# Kiểm tra file có tồn tại 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 (file may still be being written): {e}")
config = None
# Kiểm tra có đang generate 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 phản hồi
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 config tồn tại, trích xuất một số thống kê chính
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 realtime config: {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 simulation (đầy đủ, do LLM sinh)
Response includes:
- time_config: cấu hình thời gian (thời lượng simulation, số vòng, khung cao điểm/thấp điểm)
- agent_configs: cấu hình hoạt động cho 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: phần giải thích suy luận khi sinh config của LLM
"""
try:
manager = SimulationManager()
config = manager.get_simulation_config(simulation_id)
if not config:
return jsonify({
"success": False,
"error": f"Simulation config not found. 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 xuống file cấu hình simulation"""
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 not found. 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 xuống script chạy simulation (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'))
# Xác thực 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 not found: {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 tạo Profile (dùng độc lập) ==============
@simulation_bp.route('/generate-profiles', methods=['POST'])
def generate_profiles():
"""
Sinh OASIS Agent Profile trực tiếp từ graph (không tạo simulation)
Yêu cầu (JSON):
{
"graph_id": "mirofish_xxxx", // required
"entity_types": ["Student"], // optional
"use_llm": true, // optional
"platform": "reddit" // optional
}
"""
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')
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 entities matched the filter"
}), 400
generator = OasisProfileGenerator()
profiles = generator.generate_profiles_from_entities(
entities=filtered.entities,
use_llm=use_llm
)
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 profiles: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
# ============== Simulation run control endpoints ==============
@simulation_bp.route('/start', methods=['POST'])
def start_simulation():
"""
Bắt đầu chạy simulation
Yêu cầu (JSON):
{
"simulation_id": "sim_xxxx", // requiredsimulation ID
"platform": "parallel", // optional: twitter / reddit / parallel (default)
"max_rounds": 100, // optional: maximum simulation roundsused to cap overly long simulations
"enable_graph_memory_update": false, // optional: whether to dynamically write Agent activity to Zep graph memory
"force": false // optional: force restart (stops running simulation and cleans logs)
}
Về tham số force:
- when enabled, if simulation is running or completed, it will stop first and clean run logs
- cleanup includes run_state.json, actions.jsonl, simulation.log, etc.
- config file (simulation_config.json) and profile files are not deleted
- suitable when you need to rerun a simulation
Về enable_graph_memory_update:
- Khi bật, toàn bộ hoạt động của Agent (đăng bài, bình luận, like...) sẽ được cập nhật realtime lên Zep graph
- this allows the graph to remember the simulation process for later analysis or AI chat
- requires a valid graph_id on the linked project
- uses batched updates to reduce API calls
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, // whether graph memory update is enabled
"force_restarted": true // whether this was a forced restart
}
}
"""
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') # optionalmaximum simulation rounds
enable_graph_memory_update = data.get('enable_graph_memory_update', False) # optionalcó bật cập nhật graph memory hay không
force = data.get('force', False) # optionalkhởi động lại cưỡng bức
# 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 simulation đã sẵn sàng chưa
manager = SimulationManager()
state = manager.get_simulation(simulation_id)
if not state:
return jsonify({
"success": False,
"error": f"Simulation not found: {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 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 simulation 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:
# Force mode: stop running simulation
logger.info(f"Force mode: stop 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": f"Simulation is running. Call /stop first, or use force=true to restart forcibly"
}), 400
# Nếu là force mode, dọn dẹp log chạy
if force:
logger.info(f"Force mode: cleanup 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
# process does not exist or has ended, resetting status to ready
logger.info(f"Simulation {simulation_id} preparation completed, resetting status to ready (previous status: {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 graph memory)
graph_id = None
if enable_graph_memory_update:
# Lấy graph_id từ trạng thái simulation hoặc project
graph_id = state.graph_id
if not graph_id:
# Thử lấy từ project
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 has been built"
}), 400
logger.info(f"Graph memory update enabled: simulation_id={simulation_id}, graph_id={graph_id}")
# Khởi động simulation
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 simulation
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"Khởi động simulationfailed: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
@simulation_bp.route('/stop', methods=['POST'])
def stop_simulation():
"""
Stop simulation
Yêu cầu (JSON):
{
"simulation_id": "sim_xxxx" // requiredsimulation 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 simulation
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"Stop simulationfailed: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
# ============== API giám sát trạng thái realtime ==============
@simulation_bp.route('/<simulation_id>/run-status', methods=['GET'])
def get_run_status(simulation_id: str):
"""
Lấy trạng thái chạy simulation realtime (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):
"""
Get detailed simulation run status (including all actions)
Dùng cho frontend hiển thị diễn biến realtime
Tham số Query:
platform: lọc nền tảng (twitter/reddit, optional)
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": [...], # tất cả action trên Twitter
"reddit_actions": [...] # tất cả action 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": []
}
})
# Get full action list
all_actions = SimulationRunner.get_all_actions(
simulation_id=simulation_id,
platform=platform_filter
)
# Get actions by platform
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 []
# Get actions for current round (recent_actions shows only the latest round)
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 []
# Get base status info
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 vòng mới nhất của 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 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):
"""
Get agent action history in simulation
Tham số Query:
limit: số lượng trả về (default 100)
offset: độ lệch (default 0)
platform: filter platform (twitter/reddit)
agent_id: filter Agent ID
round_num: filter round
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):
"""
Get simulation timeline (aggregated by rounds)
For frontend progress bar and timeline view
Tham số Query:
start_round: vòng bắt đầu (default 0)
end_round: vòng kết thúc (default all)
Return per-round summary info
"""
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):
"""
Get per-agent statistics
For frontend agent activity ranking and action distribution
"""
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 stats: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
# ============== Database query endpoints ==============
@simulation_bp.route('/<simulation_id>/posts', methods=['GET'])
def get_simulation_posts(simulation_id: str):
"""
Get posts from simulation
Tham số Query:
platform: platform type (twitter/reddit)
limit: số lượng trả về (default 50)
offset: độ lệch
Return post list (read from 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. Simulation may not have started."
}
})
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):
"""
Get comments from simulation (Reddit only)
Tham số Query:
post_id: lọc theo post ID (optional)
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 endpoints ==============
@simulation_bp.route('/interview', methods=['POST'])
def interview_agent():
"""
Interview a single Agent
Note: this feature requires the simulation environment to be running (after simulation loop finishes, it enters command-wait mode)
Yêu cầu (JSON):
{
"simulation_id": "sim_xxxx", // requiredsimulation ID
"agent_id": 0, // requiredAgent ID
"prompt": "What is your view on this event?", // requiredinterview question
"platform": "twitter", // optional, chỉ định nền tảng (twitter/reddit)
// if not specified: in dual-platform simulation, interview both platforms simultaneously
"timeout": 60 // optionaltimeout (seconds)default60
}
Response (platform not specified, dual-platform mode):
{
"success": true,
"data": {
"agent_id": 0,
"prompt": "What is your view on this event?",
"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"
}
}
Response (platform specified):
{
"success": true,
"data": {
"agent_id": 0,
"prompt": "What is your view on this event?",
"result": {
"agent_id": 0,
"response": "I think...",
"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') # optionaltwitter/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 either twitter or reddit"
}), 400
# Kiểm tra trạng thái môi trường
if not SimulationRunner.check_env_alive(simulation_id):
return jsonify({
"success": False,
"error": "Simulation environment is not running or has been closed. Ensure simulation is completed and in command-wait mode."
}), 400
# Tối ưu prompt, thêm prefix để tránh Agent gọi tool
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"Interview response timeout: {str(e)}"
}), 504
except Exception as e:
logger.error(f"Interviewfailed: {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():
"""
Batch interview multiple Agents
Lưu ý: tính năng này yêu cầu môi trường simulation đang chạy
Yêu cầu (JSON):
{
"simulation_id": "sim_xxxx", // requiredsimulation ID
"interviews": [ // requiredinterview list
{
"agent_id": 0,
"prompt": "What is your opinion on A?",
"platform": "twitter" // optionalspecify interview platform for this Agent
},
{
"agent_id": 1,
"prompt": "What is your opinion on B?" // nếu không chỉ định platform thì dùng giá trị mặc định
}
],
"platform": "reddit", // optional, nền tảng mặc định (bị ghi đè bởi platform từng mục)
// nếu không chỉ định: với mô phỏng hai nền tảng, mỗi Agent sẽ được phỏng vấn đồng thời trên cả hai nền tảng
"timeout": 120 // optionaltimeout (seconds)default120
}
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') # optionaltwitter/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 either 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 list item #{i+1} is missing agent_id"
}), 400
if 'prompt' not in interview:
return jsonify({
"success": False,
"error": f"Interview list item #{i+1} is missing prompt"
}), 400
# Validate platform for 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"Interview list item #{i+1} platform must be twitter or reddit"
}), 400
# Kiểm tra trạng thái môi trường
if not SimulationRunner.check_env_alive(simulation_id):
return jsonify({
"success": False,
"error": "Simulation environment is not running or has been closed. Ensure simulation is completed and in command-wait mode."
}), 400
# Optimize each prompt and add prefix to prevent 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"Batch interview response timeout: {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():
"""
Global interview - use the same question for all Agents
Lưu ý: tính năng này yêu cầu môi trường simulation đang chạy
Yêu cầu (JSON):
{
"simulation_id": "sim_xxxx", // requiredsimulation ID
"prompt": "What is your overall view on this event?", // requiredinterview questionall Agents use the same question
"platform": "reddit", // optional, chỉ định nền tảng (twitter/reddit)
// nếu không chỉ định: với mô phỏng hai nền tảng, mỗi Agent sẽ được phỏng vấn đồng thời trên cả hai nền tảng
"timeout": 180 // optionaltimeout (seconds)default180
}
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') # optionaltwitter/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 either twitter or reddit"
}), 400
# Kiểm tra trạng thái môi trường
if not SimulationRunner.check_env_alive(simulation_id):
return jsonify({
"success": False,
"error": "Simulation environment is not running or has been closed. Ensure simulation is completed and in command-wait mode."
}), 400
# Tối ưu prompt, thêm prefix để tránh Agent gọi tool
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"Global interview response timeout: {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():
"""
Get interview history
Read all interview records from simulation databases
Yêu cầu (JSON):
{
"simulation_id": "sim_xxxx", // requiredsimulation ID
"platform": "reddit", // optionalplatform type (reddit/twitter)
// if not specified, return history from both platforms
"agent_id": 0, // optionalonly get interview history of this Agent
"limit": 100 // optional, return count, default 100
}
Trả về:
{
"success": true,
"data": {
"count": 10,
"history": [
{
"agent_id": 0,
"response": "I think...",
"prompt": "What is your view on this event?",
"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 not specified, return history for 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():
"""
Get simulation environment status
Check if simulation environment is alive (can receive Interview commands)
Yêu cầu (JSON):
{
"simulation_id": "sim_xxxx" // requiredsimulation 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():
"""
Close simulation environment
Send a close-environment command so simulation exits command-wait mode gracefully.
Note: this differs from /stop, where /stop forcefully terminates the process,
while this endpoint closes environment and exits gracefully.
Yêu cầu (JSON):
{
"simulation_id": "sim_xxxx", // requiredsimulation ID
"timeout": 30 // optionaltimeout (seconds)default30
}
Trả về:
{
"success": true,
"data": {
"message": "Environment close command 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
)
# Cập nhật trạng thái simulation
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